Skip to content

Commit

Permalink
Add support for ChaCha20-Poly1305 AEAD
Browse files Browse the repository at this point in the history
Support for ChaCha20-Poly1305 was added in JDK 11 in
2018 (https://openjdk.org/jeps/329). It is a very popular alternative
to AES-GCM, due to its performance [1], and the fact that its used in
a lot of security network protocols and tools[2].

[1] Faster than AEC-GCM without hardware AES acceleration, and similar
    or better performace in multi-core machines even with AES hardware
    acceleration; see https://en.wikipedia.org/wiki/ChaCha20-Poly1305#Performance

[2] IPSec, TLS 1.2, DTLS 1.2, TLS 1.3, Wireguard, OTRv4 and multipe
    other protocols (according to https://en.wikipedia.org/wiki/ChaCha20-Poly1305#Use)

[Re: taoensso#1]
  • Loading branch information
iarenaza committed Dec 22, 2023
1 parent a192f83 commit 2f7a5f5
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 27 deletions.
56 changes: 49 additions & 7 deletions src/taoensso/tempel/impl.clj
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@
;;;; IDs
;;
;; ✓ pbkdf-kit - #{:scrypt-r8p1-v1 :pbkdf2-hmac-sha-256-v1 :sha-512-v1-deprecated}
;; ✓ sym-cipher-kit - #{:aes-gcm-<nbits>-v1 :aes-cbc-<nbits>-v1-deprecated}
;; ✓ sym-cipher-kit - #{:aes-gcm-<nbits>-v1 :aes-cbc-<nbits>-v1-deprecated :chacha20-poly1305-v1}
;;
;; ✓ hash-algo - #{:md5 :sha-1 :sha-256 :sha-512}
;; ✓ sym-cipher-algo - #{:aes-gcm :aes-cbc}
;; ✓ sym-cipher-algo - #{:aes-gcm :aes-cbc :chacha20-poly1305}
;; ✓ asym-cipher-algo - #{:rsa-oaep-sha-256-mgf1}
;; ✓ sig-algo - #{:sha-<nbits>-rsa :sha-<nbits>-ecdsa}
;;
Expand Down Expand Up @@ -253,17 +253,19 @@
(def ^:const min-iv-len "128 bits" 16)

(let [cipher-aes-gcm_ (enc/thread-local (javax.crypto.Cipher/getInstance "AES/GCM/NoPadding"))
cipher-aes-cbc_ (enc/thread-local (javax.crypto.Cipher/getInstance "AES/CBC/PKCS5Padding"))]
cipher-aes-cbc_ (enc/thread-local (javax.crypto.Cipher/getInstance "AES/CBC/PKCS5Padding"))
chacha20-poly1305_ (enc/thread-local (javax.crypto.Cipher/getInstance "ChaCha20-Poly1305"))]

(defn- as-symmetric-cipher
"Returns `javax.crypto.Cipher`, or throws.
Takes `sym-cipher-algo` ∈ #{:aes-gcm :aes-cbc}."
Takes `sym-cipher-algo` ∈ #{:aes-gcm :aes-cbc :chacha20-poly1305}."
^javax.crypto.Cipher [sym-cipher-algo]
(case sym-cipher-algo
:aes-gcm @cipher-aes-gcm_
:aes-cbc @cipher-aes-cbc_
:chacha20-poly1305 @chacha20-poly1305_
(enc/unexpected-arg! sym-cipher-algo
{:expected #{:aes-gcm :aes-cbc}
{:expected #{:aes-gcm :aes-cbc :chacha20-poly1305}
:context `as-symmetric-cipher}))))

(defprotocol ISymmetricCipherKit
Expand Down Expand Up @@ -345,22 +347,56 @@
(.init cipher javax.crypto.Cipher/DECRYPT_MODE key-spec param-spec)
(.doFinal cipher ba-encrypted-content))))

(deftype SymmetricCipherKit-chacha20-poly1305-v1
[^int key-len ^int iv-len]

ISymmetricCipherKit
(sck-kid [_] :chacha20-poly1305-v1)
(sck-key-len [_] key-len)
(sck-iv-len [_] iv-len)
(sck-encrypt [_ ba-iv ba-key ba-content ?ba-aad]
(let [cipher (as-symmetric-cipher :chacha20-poly1305)
ba-key (bytes/ba->sublen key-len ba-key)
ba-iv (bytes/ba->sublen iv-len ba-iv)
key-spec (javax.crypto.spec.SecretKeySpec. ba-key "ChaCha20")
param-spec (javax.crypto.spec.IvParameterSpec. ba-iv)]

(.init cipher javax.crypto.Cipher/ENCRYPT_MODE key-spec param-spec)
(when-let [^bytes ba-aad ?ba-aad] (.updateAAD cipher ba-aad)) ; Influences tag in ciphertext

(.doFinal cipher ba-content)))

(sck-decrypt [_ ba-iv ba-key ba-encrypted-content ?ba-aad]
(let [cipher (as-symmetric-cipher :chacha20-poly1305)
ba-key (bytes/ba->sublen key-len ba-key)
ba-iv (bytes/ba->sublen iv-len ba-iv)
key-spec (javax.crypto.spec.SecretKeySpec. ba-key "ChaCha20")
param-spec (javax.crypto.spec.IvParameterSpec. ba-iv)]

(.init cipher javax.crypto.Cipher/DECRYPT_MODE key-spec param-spec)
(when-let [^bytes ba-aad ?ba-aad] (.updateAAD cipher ba-aad))

(.doFinal cipher ba-encrypted-content))))

(let [;; Ref. NIST SP800-38D §5.2.1.1 for params
sck-aes-gcm-128-v1 (SymmetricCipherKit-aes-gcm-v1. 16 12 128)
sck-aes-gcm-192-v1 (SymmetricCipherKit-aes-gcm-v1. 24 12 128)
sck-aes-gcm-256-v1 (SymmetricCipherKit-aes-gcm-v1. 32 12 128)

sck-aes-cbc-128-v1-deprecated (SymmetricCipherKit-aes-cbc-v1-deprecated. 16 16)
sck-aes-cbc-256-v1-deprecated (SymmetricCipherKit-aes-cbc-v1-deprecated. 32 16)

skc-chacha20-poly1305-v1 (SymmetricCipherKit-chacha20-poly1305-v1. 32 12)
expected #{:aes-gcm-128-v1
:aes-gcm-192-v1
:aes-gcm-256-v1
:aes-cbc-128-v1-deprecated
:aes-cbc-256-v1-deprecated}]
:aes-cbc-256-v1-deprecated
:chacha20-poly1305-v1}]

(defn as-symmetric-cipher-kit
"Returns `ISymmetricCipherKit` implementer, or throws.
Takes `sym-cipher-algo` ∈ #{:aes-gcm-<nbits>-v1 :aes-cbc-<nbits>-v1-deprecated}."
Takes `sym-cipher-algo` ∈ #{:aes-gcm-<nbits>-v1 :aes-cbc-<nbits>-v1-deprecated :chacha20-poly1305}."
[sym-cipher-algo]
(if (keyword? sym-cipher-algo)
(case sym-cipher-algo
Expand All @@ -371,6 +407,8 @@
:aes-cbc-128-v1-deprecated sck-aes-cbc-128-v1-deprecated
:aes-cbc-256-v1-deprecated sck-aes-cbc-256-v1-deprecated

:chacha20-poly1305-v1 skc-chacha20-poly1305-v1

(enc/unexpected-arg! sym-cipher-algo
{:expected expected
:context `as-symmetric-cipher-kit}))
Expand Down Expand Up @@ -401,6 +439,10 @@
{:sym-cipher-algo :aes-gcm-128-v1
:symmetric? true}

:chacha20-poly1305
{:sym-cipher-algo :chacha20-poly1305-v1
:symmetric? true}

:rsa {:kf-algo :rsa, :asymmetric? true, :wild? true}
(:rsa-1024 :rsa-2048 :rsa-3072 :rsa-4096)
{:kf-algo :rsa
Expand Down
104 changes: 84 additions & 20 deletions test/taoensso/tempel_tests.clj
Original file line number Diff line number Diff line change
Expand Up @@ -84,43 +84,107 @@
(comment (with-rand-data 32 64 (fn [ba-cnt ?ba-aad] [(is true) (is false) (is true)])))

(deftest _symmetric-cipher-kit
(let [sck (impl/as-symmetric-cipher-kit :aes-gcm-128-v1)
ba-key (as-ba 32 "pwd")]
(let [aes-gcm-sck (impl/as-symmetric-cipher-kit :aes-gcm-128-v1)
ba-aes-key (as-ba 32 "pwd")
chacha-poly-sck (impl/as-symmetric-cipher-kit :chacha20-poly1305-v1)
ba-chacha-poly-key (as-ba 32 "pwd")
;; Chacha2020-Poly1305 JDK implementation doesn't allow initializing the
;; same cipher instance a second time with the same key and IV (reusing
;; the IV/nonce with the same encryption key completely breaks the
;; security properties of the cipher when encrypting).
;;
;; Because of the way tempel implements symmetric ciphers (keeping a
;; single cipher instance per thread, and calling the initialization
;; method each time an encryption or decryption operation is used), we
;; can't do an encryption operation followed by a decryption operation
;; to test the results. Because in that case **we need** to use the same
;; key and IV (otherwise the decryption will always fail).
;;
;; By the way, pretty much the same happens for AES-GCM cipher
;; instances. But in that case, the JDK implementation only enforces the
;; restriction when initializing the cipher for encryption
;; mode. Decryption mode is not affected in the AES-GCM case (while it
;; is in the Chacha20-Poly1305 case).
;;
;; As a work-around, we "reset" the cipher instance with
;; another (random) IV in between operations.
reset-chacha-poly-fn (fn []
;; Reset the cipher instance internal state by
;; encrypting irrelevant data with a random IV
(impl/sck-encrypt chacha-poly-sck
(impl/rand-ba (impl/sck-iv-len chacha-poly-sck))
ba-chacha-poly-key
;; totally irrelevant data and aad
(byte-array 0) nil))
bit-flip-fn (fn [^"[B" ba idx bit-num]
(aset-byte ba idx (bit-flip (aget ba idx) bit-num))
ba)]

[(testing "Basic operation, with AAD"
[(let [ba-cnt (as-ba "cnt")
ba-aad (impl/rand-ba 128)
ba-iv (impl/rand-ba (impl/sck-iv-len sck))
ba-ecnt (impl/sck-encrypt sck ba-iv ba-key ba-cnt ba-aad)]

[(is (->> (impl/sck-decrypt sck ba-iv ba-key ba-ecnt ba-aad) enc/utf8-ba->str (= "cnt")))
(is (->> (impl/sck-decrypt sck ba-iv (as-ba 32 "!pwd") ba-ecnt ba-aad) (throws? javax.crypto.AEADBadTagException)) "Bad key")])

(let [ba-cnt (as-ba "cnt")
ba-iv (impl/rand-ba (impl/sck-iv-len sck))
ba-ecnt (impl/sck-encrypt sck ba-iv ba-key ba-cnt nil)]

[(is (->> (impl/sck-decrypt sck ba-iv ba-key ba-ecnt nil) enc/utf8-ba->str (= "cnt")) "No AAD")
(is (->> (impl/sck-decrypt sck ba-iv ba-key ba-ecnt (impl/rand-ba 128))
(throws? javax.crypto.AEADBadTagException)) "Bad AAD")])
ba-iv (impl/rand-ba (impl/sck-iv-len aes-gcm-sck))
aes-ba-ecnt (impl/sck-encrypt aes-gcm-sck ba-iv ba-aes-key ba-cnt ba-aad)
aes-ba-bad-ecnt (bit-flip-fn (aclone aes-ba-ecnt) 0 1)
ba-chacha-poly-iv (impl/rand-ba (impl/sck-iv-len chacha-poly-sck))
chacha-poly-ba-ecnt (impl/sck-encrypt chacha-poly-sck ba-chacha-poly-iv ba-chacha-poly-key ba-cnt ba-aad)
chacha-poly-ba-bad-ecnt (bit-flip-fn (aclone chacha-poly-ba-ecnt) 0 1)]

[(is (->> (impl/sck-decrypt aes-gcm-sck ba-iv ba-aes-key aes-ba-ecnt ba-aad) enc/utf8-ba->str (= "cnt")))
(is (->> (impl/sck-decrypt aes-gcm-sck ba-iv (as-ba 32 "!pwd") aes-ba-ecnt ba-aad) (throws? javax.crypto.AEADBadTagException)) "Bad key")
(is (->> (impl/sck-decrypt aes-gcm-sck ba-iv ba-aes-key aes-ba-bad-ecnt ba-aad) (throws? javax.crypto.AEADBadTagException)) "Bad key")
;; Same key and IV as in previous call (to encrypt the data), so we need to reset the cipher.
(reset-chacha-poly-fn)
(is (->> (impl/sck-decrypt chacha-poly-sck ba-chacha-poly-iv ba-chacha-poly-key chacha-poly-ba-ecnt ba-aad) enc/utf8-ba->str (= "cnt")))
;; Different key from previous call, so no need to reset the cipher.
(is (->> (impl/sck-decrypt chacha-poly-sck ba-chacha-poly-iv (as-ba 32 "!pwd") chacha-poly-ba-ecnt ba-aad)
(throws? javax.crypto.AEADBadTagException)) "Bad key")
;; Again, different key from previous call, so no need to reset the cipher.
(is (->> (impl/sck-decrypt chacha-poly-sck ba-chacha-poly-iv ba-chacha-poly-key chacha-poly-ba-bad-ecnt ba-aad)
(throws? javax.crypto.AEADBadTagException)) "Bad ciphertext")])

(let [ba-cnt (as-ba "cnt")
ba-aad (impl/rand-ba 128)
ba-iv (impl/rand-ba (impl/sck-iv-len aes-gcm-sck))
ba-ecnt-no-aad (impl/sck-encrypt aes-gcm-sck ba-iv ba-aes-key ba-cnt nil)
ba-chacha-poly-iv (impl/rand-ba (impl/sck-iv-len chacha-poly-sck))
chacha-poly-ba-ecnt (impl/sck-encrypt chacha-poly-sck ba-chacha-poly-iv ba-chacha-poly-key ba-cnt nil)
ba-bad-aad (bit-flip-fn (aclone ba-aad) 0 1)]

[(is (->> (impl/sck-decrypt aes-gcm-sck ba-iv ba-aes-key ba-ecnt-no-aad nil) enc/utf8-ba->str (= "cnt")) "No AAD")
(is (->> (impl/sck-decrypt aes-gcm-sck ba-iv ba-aes-key ba-ecnt-no-aad (impl/rand-ba 128))
(throws? javax.crypto.AEADBadTagException)) "Bad AAD")
(reset-chacha-poly-fn)
(is (->> (impl/sck-decrypt chacha-poly-sck ba-chacha-poly-iv ba-chacha-poly-key chacha-poly-ba-ecnt nil) enc/utf8-ba->str (= "cnt")) "No AAD")
(reset-chacha-poly-fn)
(is (->> (impl/sck-decrypt chacha-poly-sck ba-chacha-poly-iv ba-chacha-poly-key chacha-poly-ba-ecnt (impl/rand-ba 128))
(throws? javax.crypto.AEADBadTagException)) "Bad AAD")
(reset-chacha-poly-fn)
(is (->> (impl/sck-decrypt chacha-poly-sck ba-chacha-poly-iv ba-chacha-poly-key chacha-poly-ba-ecnt ba-bad-aad)
(throws? javax.crypto.AEADBadTagException)) "Bad AAD")])

(with-rand-data (mbytes 4) 256
(fn [ba-cnt ?ba-aad]
(let [ba-iv (impl/rand-ba (impl/sck-iv-len sck))
ba-ecnt (impl/sck-encrypt sck ba-iv ba-key ba-cnt ?ba-aad)
ba-dcnt (impl/sck-decrypt sck ba-iv ba-key ba-ecnt ?ba-aad)]
(let [ba-iv (impl/rand-ba (impl/sck-iv-len aes-gcm-sck))
ba-ecnt (impl/sck-encrypt aes-gcm-sck ba-iv ba-aes-key ba-cnt ?ba-aad)
ba-dcnt (impl/sck-decrypt aes-gcm-sck ba-iv ba-aes-key ba-ecnt ?ba-aad)]
(is (bytes/ba= ba-cnt ba-dcnt)))
(let [ba-iv (impl/rand-ba (impl/sck-iv-len chacha-poly-sck))
ba-ecnt (impl/sck-encrypt chacha-poly-sck ba-iv ba-chacha-poly-key ba-cnt ?ba-aad)
_ (reset-chacha-poly-fn)
ba-dcnt (impl/sck-decrypt chacha-poly-sck ba-iv ba-chacha-poly-key ba-ecnt ?ba-aad)]
(is (bytes/ba= ba-cnt ba-dcnt)))))])

(testing "Bad ba lengths"
[(is
(->>
(impl/sck-encrypt sck (impl/rand-ba 4) (impl/rand-ba 128) (as-ba "cnt") nil)
(impl/sck-encrypt aes-gcm-sck (impl/rand-ba 4) (impl/rand-ba 128) (as-ba "cnt") nil)
(throws? :ex-info {:length {:target 12, :actual 4}}))
"ba-iv too short")

(is
(->>
(impl/sck-encrypt sck (impl/rand-ba 128) (impl/rand-ba 4) (as-ba "cnt") nil)
(impl/sck-encrypt aes-gcm-sck (impl/rand-ba 128) (impl/rand-ba 4) (as-ba "cnt") nil)
(throws? :ex-info {:length {:target 16, :actual 4}}))
"ba-key too short")])]))

Expand Down

0 comments on commit 2f7a5f5

Please sign in to comment.