ChaCha20-Poly1305

An authenticated encryption cipher based on the ChaCha20 stream cipher and the Poly1305 message authentication code (MAC). Available in TLS as an alternative to AES and as a replacement for the older RC4 stream cipher.

A basic implementation based on the code from the linked pages:

# chacha20poly1305.tcl --
#
#       Implementation of the ChaCha20-Poly1305 authenticated cipher.
#
# Copyright (c) 2018 Neil Madden.
# License: Tcl-style

package require Tcl         8.6

if {[info exists argv0] && [file tail [info script]] eq [file tail $argv0]} {
    ::tcl::tm::path add [file dirname [info script]]
}

package require chacha20    1.0.0
package require poly1305    1.0.0
package provide chacha20poly1305 1.0.0

namespace eval ::chacha20poly1305 {
    namespace export encrypt decrypt
    namespace ensemble create

    proc genkey {key nonce} {
        set state [chacha20::block [chacha20::initialState $key 0 $nonce] 0]
        binary format i8 $state
    }

    proc encrypt {key message args} {
        array set options {
            -nonce      ""
            -assocdata  ""
        }
        array set options $args
        set nonce $options(-nonce)
        if {[string length $nonce] != 12} {
            error "nonce must be 12 bytes"
        }
        set assocData $options(-assocdata)
        unset options(-assocdata)

        set ciphertext [::chacha20 encrypt $key $message {*}[array get options] -counter 1]
        set tag [mac $key $nonce $assocData $ciphertext]

        return $ciphertext$tag
    }

    proc mac {key nonce assocData ciphertext} {
        set macKey [genkey $key $nonce]
        #puts "subkey = [binary encode hex $macKey]"
        set padding1 [padding $assocData]
        set padding2 [padding $ciphertext]
        set lengths [binary format ww [string length $assocData] [string length $ciphertext]]

        set macInput "$assocData$padding1$ciphertext$padding2$lengths"
        #puts [binary encode hex $macInput]

        return [::poly1305 compute $macKey $macInput]
    }

    proc decrypt {key ciphertext args} {
        array set options {
            -nonce      ""
            -assocdata  ""
        }
        array set options $args
        set nonce $options(-nonce)
        if {[string length $nonce] != 12} {
            error "nonce must be 12 bytes"
        }
        set assocData $options(-assocdata)
        unset options(-assocdata)
    
        set suppliedTag [string range $ciphertext end-15 end]
        set ciphertext [string range $ciphertext 0 end-16]
        set computedTag [mac $key $nonce $assocData $ciphertext]
        
        if {![poly1305::equals $computedTag $suppliedTag]} {
            error "invalid authentication tag"
        }

        return [chacha20 decrypt $key $ciphertext {*}[array get options] -counter 1]
    }

    proc padding {input} {
        set padLen [expr {(16 - ([string length $input] & 15)) & 15}]
        string repeat \x00 $padLen
    }
}

##### TESTS #####
#
if {![info exists argv0] || [file tail [info script]] ne [file tail $argv0]} {
    # Not running as main script so return to avoid running tests below
    return
}

proc fromHex hex {
    regsub -all {\s+} $hex {} hex
    regsub -all {:} $hex {} hex
    regsub -all {..} $hex {\x\0} hex
    subst $hex
}

proc assertEqual {a b {msg ""}} {
    if {$a ne $b} {
        puts "FAIL - assertion failed: expecting '[binary encode hex $a]' to equal '[binary encode hex $b]' $msg"
    } else {
        puts "PASS - $msg"
    }
}

set key [fromHex {80 81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 8f
                  90 91 92 93 94 95 96 97 98 99 9a 9b 9c 9d 9e 9f}]
set nonce [fromHex {00 00 00 00 00 01 02 03 04 05 06 07}]

assertEqual [chacha20poly1305::genkey $key $nonce] [fromHex {
    8a d5 a0 8b 90 5f 81 cc 81 50 40 27 4a b2 94 71
    a8 33 b6 37 e3 fd 0d a5 08 db b8 e2 fd d1 a6 46
}] "poly1305 subkey generation"

set message "Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it."
set aad [fromHex {50 51 52 53 c0 c1 c2 c3 c4 c5 c6 c7}]
set nonce [fromHex {07 00 00 00 40 41 42 43 44 45 46 47}]

set ctAndTag [chacha20poly1305 encrypt $key $message -assocdata $aad -nonce $nonce]
set ciphertext [string range $ctAndTag 0 end-16]
set tag [string range $ctAndTag end-15 end]

assertEqual $ciphertext [fromHex {
    d3 1a 8d 34 64 8e 60 db 7b 86 af bc 53 ef 7e c2
    a4 ad ed 51 29 6e 08 fe a9 e2 b5 a7 36 ee 62 d6
    3d be a4 5e 8c a9 67 12 82 fa fb 69 da 92 72 8b
    1a 71 de 0a 9e 06 0b 29 05 d6 a5 b6 7e cd 3b 36
    92 dd bd 7f 2d 77 8b 8c 98 03 ae e3 28 09 1b 58
    fa b3 24 e4 fa d6 75 94 55 85 80 8b 48 31 d7 bc
    3f f4 de f0 8e 4b 7a 9d e5 76 d2 65 86 ce c6 4b
    61 16 
}] "ChaPoly AEAD - ciphertext test vector"

assertEqual $tag [fromHex {1a:e1:0b:59:4f:09:e2:6a:7e:90:2e:cb:d0:60:06:91}] "ChaPoly AEAD - authentication tag"

set decrypted [chacha20poly1305 decrypt $key $ctAndTag -assocdata $aad -nonce $nonce]
assertEqual $decrypted $message "ChaPoly AEAD - roundtrip"

# vim: ft=tcl