Add AEAD encrypt/decrypt and constant-time compare

ober

69f036f257cc7816a8258dc26fa5d08770765338

diff --git a/chez_crypto_shim.c b/chez_crypto_shim.c
index 831aa40..66d6de8 100644
--- a/chez_crypto_shim.c
+++ b/chez_crypto_shim.c
@@ -7,6 +7,7 @@
 #include <openssl/rand.h>
 #include <openssl/err.h>
 #include <openssl/kdf.h>
+#include <openssl/crypto.h>
 #include <stdlib.h>
 #include <string.h>
 
@@ -243,6 +244,111 @@ int chez_ed25519_verify(const unsigned char *pubkey, int publen,
     return rc;
 }
 
+/* ---- AEAD Encrypt/Decrypt (ChaCha20-Poly1305, AES-GCM, etc.) ---- */
+
+/*
+ * chez_aead_encrypt — AEAD encrypt with authentication tag.
+ *
+ * algo:    cipher name (e.g. "chacha20-poly1305", "aes-256-gcm")
+ * key:     encryption key
+ * nonce:   nonce/IV
+ * noncelen: nonce length (12 for chacha20-poly1305)
+ * aad:     additional authenticated data (can be NULL)
+ * aadlen:  AAD length (0 if no AAD)
+ * in:      plaintext
+ * inlen:   plaintext length
+ * out:     ciphertext output (must be >= inlen bytes)
+ * outlen:  receives ciphertext length
+ * tag:     authentication tag output (must be >= taglen bytes)
+ * taglen:  desired tag length (16 for chacha20-poly1305)
+ *
+ * Returns: 0 on success, negative on error.
+ */
+int chez_aead_encrypt(const char *algo,
+                      const unsigned char *key,
+                      const unsigned char *nonce, int noncelen,
+                      const unsigned char *aad, int aadlen,
+                      const unsigned char *in, int inlen,
+                      unsigned char *out, int *outlen,
+                      unsigned char *tag, int taglen) {
+    const EVP_CIPHER *c = EVP_get_cipherbyname(algo);
+    if (!c) return -1;
+    EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
+    if (!ctx) return -2;
+    int rc = -3;
+    int len1 = 0, len2 = 0;
+
+    if (EVP_EncryptInit_ex(ctx, c, NULL, NULL, NULL) != 1) goto done;
+    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, noncelen, NULL) != 1) goto done;
+    if (EVP_EncryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) goto done;
+
+    if (aad && aadlen > 0) {
+        if (EVP_EncryptUpdate(ctx, NULL, &len1, aad, aadlen) != 1) goto done;
+    }
+    if (EVP_EncryptUpdate(ctx, out, &len1, in, inlen) != 1) goto done;
+    if (EVP_EncryptFinal_ex(ctx, out + len1, &len2) != 1) goto done;
+    *outlen = len1 + len2;
+
+    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, taglen, tag) != 1) goto done;
+    rc = 0;
+
+done:
+    EVP_CIPHER_CTX_free(ctx);
+    return rc;
+}
+
+/*
+ * chez_aead_decrypt — AEAD decrypt and verify authentication tag.
+ *
+ * Returns: 0 on success (tag verified), -1 on auth failure, negative on error.
+ */
+int chez_aead_decrypt(const char *algo,
+                      const unsigned char *key,
+                      const unsigned char *nonce, int noncelen,
+                      const unsigned char *aad, int aadlen,
+                      const unsigned char *in, int inlen,
+                      unsigned char *out, int *outlen,
+                      const unsigned char *tag, int taglen) {
+    const EVP_CIPHER *c = EVP_get_cipherbyname(algo);
+    if (!c) return -1;
+    EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
+    if (!ctx) return -2;
+    int rc = -3;
+    int len1 = 0, len2 = 0;
+
+    if (EVP_DecryptInit_ex(ctx, c, NULL, NULL, NULL) != 1) goto done;
+    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, noncelen, NULL) != 1) goto done;
+    if (EVP_DecryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) goto done;
+
+    if (aad && aadlen > 0) {
+        if (EVP_DecryptUpdate(ctx, NULL, &len1, aad, aadlen) != 1) goto done;
+    }
+    if (EVP_DecryptUpdate(ctx, out, &len1, in, inlen) != 1) goto done;
+
+    /* Set expected tag before finalize */
+    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, taglen,
+                            (void *)(unsigned char *)tag) != 1) goto done;
+
+    /* DecryptFinal returns 0 on tag mismatch */
+    if (EVP_DecryptFinal_ex(ctx, out + len1, &len2) != 1) {
+        rc = -1;  /* authentication failure */
+        goto done;
+    }
+    *outlen = len1 + len2;
+    rc = 0;
+
+done:
+    EVP_CIPHER_CTX_free(ctx);
+    return rc;
+}
+
+/* ---- Constant-time comparison ---- */
+
+int chez_constant_time_compare(const unsigned char *a,
+                                const unsigned char *b, int len) {
+    return CRYPTO_memcmp(a, b, len) == 0 ? 1 : 0;
+}
+
 /* ---- BN (Big Number) ---- */
 
 int chez_bn_bytes(const unsigned char *bin, int binlen) {
diff --git a/src/chez-crypto.sls b/src/chez-crypto.sls
index c8efa6c..2f5aa1d 100644
--- a/src/chez-crypto.sls
+++ b/src/chez-crypto.sls
@@ -20,6 +20,10 @@
     decrypt-init! decrypt-update! decrypt-final!
     ;; Public Key — Ed25519
     ed25519-keygen ed25519-sign ed25519-verify
+    ;; AEAD (Authenticated Encryption)
+    aead-encrypt aead-decrypt
+    ;; Constant-time comparison
+    constant-time-compare?
     ;; KDF
     scrypt
     ;; Error
@@ -59,6 +63,12 @@
   (define c-ed25519-sign   (foreign-procedure "chez_ed25519_sign" (u8* int u8* int u8* u8*) int))
   (define c-ed25519-verify (foreign-procedure "chez_ed25519_verify" (u8* int u8* int u8* int) int))
   (define c-scrypt (foreign-procedure "chez_scrypt" (u8* int u8* int unsigned-64 int int u8* int) int))
+  (define c-aead-encrypt (foreign-procedure "chez_aead_encrypt"
+    (string u8* u8* int u8* int u8* int u8* u8* u8* int) int))
+  (define c-aead-decrypt (foreign-procedure "chez_aead_decrypt"
+    (string u8* u8* int u8* int u8* int u8* u8* u8* int) int))
+  (define c-constant-time-compare (foreign-procedure "chez_constant_time_compare"
+    (u8* u8* int) int))
 
   ;; ---- Helpers ----
   (define (check-rc who rc)
@@ -283,4 +293,60 @@
          (check-rc 'scrypt rc)
          out)]))
 
+  ;; ---- AEAD (Authenticated Encryption with Associated Data) ----
+
+  ;; aead-encrypt: encrypt plaintext with AEAD cipher.
+  ;; algo: cipher name (e.g. "chacha20-poly1305", "aes-256-gcm")
+  ;; key: bytevector (32 bytes for chacha20-poly1305)
+  ;; nonce: bytevector (12 bytes for chacha20-poly1305)
+  ;; plaintext: bytevector
+  ;; Optional: aad (additional authenticated data bytevector)
+  ;; Returns: (values ciphertext tag) — both bytevectors
+  (define aead-encrypt
+    (case-lambda
+      [(algo key nonce plaintext) (aead-encrypt algo key nonce plaintext (make-bytevector 0))]
+      [(algo key nonce plaintext aad)
+       (let* ([inlen (bytevector-length plaintext)]
+              [out (make-bytevector (+ inlen 64) 0)]
+              [outlen (make-int-buf)]
+              [tag (make-bytevector 16 0)]
+              [rc (c-aead-encrypt algo key nonce (bytevector-length nonce)
+                                 aad (bytevector-length aad)
+                                 plaintext inlen out outlen tag 16)])
+         (check-rc 'aead-encrypt rc)
+         (let* ([n (int-ref outlen)]
+                [ct (make-bytevector n)])
+           (bytevector-copy! out 0 ct 0 n)
+           (values ct tag)))]))
+
+  ;; aead-decrypt: decrypt and verify AEAD ciphertext.
+  ;; Returns plaintext bytevector.
+  ;; Raises error on authentication failure.
+  (define aead-decrypt
+    (case-lambda
+      [(algo key nonce ciphertext tag)
+       (aead-decrypt algo key nonce ciphertext tag (make-bytevector 0))]
+      [(algo key nonce ciphertext tag aad)
+       (let* ([inlen (bytevector-length ciphertext)]
+              [out (make-bytevector (+ inlen 64) 0)]
+              [outlen (make-int-buf)]
+              [rc (c-aead-decrypt algo key nonce (bytevector-length nonce)
+                                 aad (bytevector-length aad)
+                                 ciphertext inlen out outlen tag (bytevector-length tag))])
+         (when (= rc -1)
+           (error 'aead-decrypt "authentication failed" algo))
+         (check-rc 'aead-decrypt rc)
+         (let* ([n (int-ref outlen)]
+                [pt (make-bytevector n)])
+           (bytevector-copy! out 0 pt 0 n)
+           pt))]))
+
+  ;; ---- Constant-time comparison ----
+
+  ;; constant-time-compare?: compare two bytevectors in constant time.
+  ;; Returns #t if equal, #f otherwise. Lengths must match.
+  (define (constant-time-compare? a b)
+    (and (= (bytevector-length a) (bytevector-length b))
+         (= 1 (c-constant-time-compare a b (bytevector-length a)))))
+
   ) ;; end library
diff --git a/tests/crypto-test.ss b/tests/crypto-test.ss
index 4b4a24a..0a064d7 100644
--- a/tests/crypto-test.ss
+++ b/tests/crypto-test.ss
@@ -110,6 +110,43 @@
     (chk (ed25519-verify pub "test message" sig) => #t)
     (chk (ed25519-verify pub "wrong message" sig) => #f)))
 
+;;; ---- AEAD (ChaCha20-Poly1305) ----
+(let* ([key (random-bytes 32)]
+       [nonce (random-bytes 12)]
+       [plain (string->utf8 "AEAD test message")])
+  ;; Encrypt
+  (let-values ([(ct tag) (aead-encrypt "chacha20-poly1305" key nonce plain)])
+    (chk (> (bytevector-length ct) 0) => #t)
+    (chk (= (bytevector-length tag) 16) => #t)
+    ;; Decrypt
+    (let ([pt (aead-decrypt "chacha20-poly1305" key nonce ct tag)])
+      (chk (equal? plain pt) => #t))
+    ;; Tampered ciphertext should fail
+    (let ([bad-ct (bytevector-copy ct)])
+      (bytevector-u8-set! bad-ct 0 (bitwise-xor (bytevector-u8-ref bad-ct 0) #xff))
+      (chk (guard (e [#t #t]) (aead-decrypt "chacha20-poly1305" key nonce bad-ct tag) #f) => #t))))
+
+;; AEAD with AAD
+(let* ([key (random-bytes 32)]
+       [nonce (random-bytes 12)]
+       [plain (string->utf8 "secret")]
+       [aad (string->utf8 "authenticated header")])
+  (let-values ([(ct tag) (aead-encrypt "chacha20-poly1305" key nonce plain aad)])
+    ;; Decrypt with correct AAD
+    (let ([pt (aead-decrypt "chacha20-poly1305" key nonce ct tag aad)])
+      (chk (equal? plain pt) => #t))
+    ;; Decrypt with wrong AAD should fail
+    (chk (guard (e [#t #t])
+           (aead-decrypt "chacha20-poly1305" key nonce ct tag (string->utf8 "wrong")) #f) => #t)))
+
+;;; ---- Constant-time comparison ----
+(let ([a (string->utf8 "hello")]
+      [b (string->utf8 "hello")]
+      [c (string->utf8 "world")])
+  (chk (constant-time-compare? a b) => #t)
+  (chk (constant-time-compare? a c) => #f)
+  (chk (constant-time-compare? a (string->utf8 "hi")) => #f))  ;; different lengths
+
 ;;; ---- Scrypt ----
 (let ([key (scrypt "password" "salt" 32)])
   (chk (= (bytevector-length key) 32) => #t)