Hello everyone,
While hoping to make something in Guile that needed symmetric
encryption, it fell to me to extend guile-gcrypt. Unfortunately, because
of how some procedures like `gcrypt_cipher_gettag` are implemented, the
Guile FFI reports that they don't exist! This blocks AEAD, which is what
I needed, so it's back to the drawing board for me. But, short of AEAD,
some amount of symmetric cryptographic functionality is accessible from
Guile, hence the attached patch.
I've left the AEAD-related code commented out in the module in case
anyone is interested in this challenge.
Regards,
S. Rhoulam
diff --git a/gcrypt/cipher.scm b/gcrypt/cipher.scm
new file mode 100644
index 0000000..bff28c8
--- /dev/null
+++ b/gcrypt/cipher.scm
@@ -0,0 +1,425 @@
+;;; guile-gcrypt --- crypto tooling for guile
+;;; Copyright © 2026 Saad Rhoulam <[email protected]>
+;;;
+;;; This file is part of guile-gcrypt.
+;;;
+;;; guile-gcrypt is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 3 of
+;;; the License, or (at your option) any later version.
+;;;
+;;; guile-gcrypt is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;;; Lesser General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU Lesser General Public License
+;;; along with guile-gcrypt. If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (gcrypt cipher)
+ #:use-module (ice-9 match)
+ #:use-module (gcrypt internal)
+ #:use-module (gcrypt random)
+ #:use-module (rnrs bytevectors)
+ #:use-module (system foreign)
+ #:export (cipher-algorithm
+ lookup-cipher-algorithm
+ cipher-algorithm-name
+ cipher-key-size
+
+ cipher-mode
+ lookup-cipher-mode
+ cipher-mode-name
+
+ decrypt
+ ;; decrypt-aead
+ encrypt
+ ;; encrypt-aead
+ )
+ )
+
+(define-syntax-rule (define-syntax-public name value)
+ (begin
+ (define-syntax name value)
+ (export name)))
+
+(define-syntax-rule (define-cipher-algorithms name->integer
+ symbol->integer integer->symbol cipher-key-size
+ (name id size) ...)
+ "Define cipher algorithms with their NAME, numerical ID, and SIZE in bytes."
+ (begin
+ ;; Make sure NAME is bound to follow best practices for syntax matching
+ ;; (info "(guile) Syntax Rules").
+ (define-syntax-public name
+ (lambda (s)
+ (syntax-violation 'name "\
+syntactic keyword is meant to be used with 'cipher-algorithm'"
+ s s)))
+ ...
+
+ (define-enumerate-type name->integer symbol->integer integer->symbol
+ (name id) ...)
+
+ (define-lookup-procedure cipher-key-size
+ "Return the size in bytes of a key for the given cipher algorithm."
+ (id size) ...)))
+
+(define-syntax-rule (define-cipher-modes name->integer
+ symbol->integer integer->symbol
+ (name id) ...)
+ "Define cipher modes with their NAME and numerical ID."
+ (begin
+ ;; Make sure NAME is bound to follow best practices for syntax matching
+ ;; (info "(guile) Syntax Rules").
+ (define-syntax-public name
+ (lambda (s)
+ (syntax-violation 'name "\
+syntactic keyword is meant to be used with 'cipher-mode'"
+ s s)))
+ ...
+
+ (define-enumerate-type name->integer symbol->integer integer->symbol
+ (name id) ...)))
+
+(define-cipher-algorithms cipher-algorithm
+ lookup-cipher-algorithm cipher-algorithm-name
+ cipher-key-size
+
+ ;; GCRY_CIPHER_*
+
+ (3des 2 24)
+ (cast5 3 16)
+ (aes128 7 16)
+ (aes192 8 24)
+ (aes256 9 32)
+ (twofish 10 32)
+ (twofish128 303 16)
+ (serpent128 304 16)
+ (serpent192 305 24)
+ (serpent256 306 32)
+ (seed 309 16)
+ (camellia128 310 16)
+ (camellia192 311 24)
+ (camellia256 312 32)
+ (sm4 318 16))
+
+(define-cipher-modes cipher-mode
+ lookup-cipher-mode cipher-mode-name
+
+ ;; GCRY_CIPHER_MODE_*
+ (ecb 1)
+ (cfb 2)
+ (cbc 3)
+ (stream 4)
+ (ofb 5)
+ (ctr 6)
+ (aeswrap 7)
+ (ccm 8)
+ (gcm 9)
+ (poly1305 10)
+ (ocb 11)
+ (cfb8 12)
+ (xts 13)
+ (eax 14)
+ (siv 15)
+ (gcm-siv 16))
+
+(define cipher-algo-keylen
+ (libgcrypt->procedure int "gcry_cipher_get_algo_keylen" `(,int)))
+
+(define cipher-algo-blklen
+ (libgcrypt->procedure int "gcry_cipher_get_algo_blklen" `(,int)))
+
+(define %no-error 0) ; GPG_ERR_NO_ERROR
+
+(define-wrapped-pointer-type <cipher>
+ cipher?
+ pointer->cipher cipher->pointer
+ (lambda (cipher port)
+ (format port "#<cipher ~x>"
+ (pointer-address (cipher->pointer cipher)))))
+
+(define %gcry-cipher-open
+ (libgcrypt->procedure int "gcry_cipher_open"
+ ;; gcry_cipher_hd_t *HD, int ALGO,
+ ;; int MODE,
+ ;; unsigned int FLAGS
+ `(* ,int ,int ,unsigned-int)))
+
+(define (cipher-open algorithm mode)
+ "Create a <cipher> object set to use ALGORITHM and MODE"
+ (let* ((cipher (bytevector->pointer (make-bytevector (sizeof '*))))
+ (err (%gcry-cipher-open cipher algorithm mode 0)))
+ (if (= err 0)
+ (pointer->cipher (dereference-pointer cipher))
+ (throw 'gcry-error 'cipher-open err))))
+
+(define %gcry-cipher-setkey
+ (libgcrypt->procedure int "gcry_cipher_setkey" `(* * ,size_t)))
+
+(define (cipher-setkey cipher key)
+ "Set the KEY on <cipher> object CIPHER
+
+In our case, KEY is either a string or a bytevector."
+ (let* ((key (match key
+ ((? bytevector? key)
+ key)
+ ((? string? key)
+ (string->utf8 key))))
+ (err (%gcry-cipher-setkey (cipher->pointer cipher)
+ (bytevector->pointer key)
+ (bytevector-length key))))
+ (if (= err 0)
+ #t
+ (throw 'gcry-error 'cipher-setkey err))))
+
+(define %gcry-cipher-setiv
+ (libgcrypt->procedure int "gcry_cipher_setiv" `(* * ,size_t)))
+
+(define (cipher-setiv cipher iv)
+ "Set the IV on <cipher> object CIPHER
+
+In our case, IV is either a string or a bytevector."
+ (let* ((iv (match iv
+ ((? bytevector? iv)
+ iv)
+ ((? string? iv)
+ (string->utf8 iv))))
+ (err (%gcry-cipher-setiv (cipher->pointer cipher)
+ (bytevector->pointer iv)
+ (bytevector-length iv))))
+ (if (= err 0)
+ #t
+ (throw 'gcry-error 'cipher-setiv err))))
+
+(define %gcry-cipher-setctr
+ (libgcrypt->procedure int "gcry_cipher_setctr" `(* * ,size_t)))
+
+(define (cipher-setctr cipher ctr)
+ "Set the CTR on <cipher> object CIPHER
+
+In our case, CTR is either a string or a bytevector."
+ (let* ((ctr (match ctr
+ ((? bytevector? ctr)
+ ctr)
+ ((? string? ctr)
+ (string->utf8 ctr))))
+ (err (%gcry-cipher-setctr (cipher->pointer cipher)
+ (bytevector->pointer ctr)
+ (bytevector-length ctr))))
+ (if (= err 0)
+ #t
+ (throw 'gcry-error 'cipher-setctr err))))
+
+(define %gcry-cipher-close
+ (libgcrypt->procedure void
+ "gcry_cipher_close"
+ ;; gcry_cipher_hd_t H
+ '(*)))
+
+(define (cipher-close cipher)
+ "Release all resources of CIPHER.
+
+Running this on an already closed <cipher> might segfault :)"
+ (%gcry-cipher-close (cipher->pointer cipher)))
+
+(define %gcry-cipher-encrypt
+ (libgcrypt->procedure int
+ "gcry_cipher_encrypt"
+ ;; gcry_cipher_hd_t, uchar* out, size_t outsize
+ ;; const uchar* in, size_t inlen
+ `(* * ,size_t * ,size_t)))
+
+(define (cipher-encrypt cipher obj)
+ "Runs a prepared cipher to encrypt data. Pad your plaintext if using a non-stream cipher mode."
+ (let* ((bv (match obj
+ ((? bytevector? obj)
+ (bytevector-copy obj))
+ ((? string? obj)
+ (string->utf8 obj))))
+ (err (%gcry-cipher-encrypt (cipher->pointer cipher)
+ (bytevector->pointer bv)
+ (bytevector-length bv)
+ %null-pointer
+ 0)))
+ (if (= err 0)
+ bv
+ (throw 'gcry-error 'cipher-encrypt err))))
+
+(define %gcry-cipher-decrypt
+ (libgcrypt->procedure int
+ "gcry_cipher_decrypt"
+ ;; gcry_cipher_hd_t, uchar* out, size_t outsize
+ ;; const uchar* in, size_t inlen
+ `(* * ,size_t * ,size_t)))
+
+(define (cipher-decrypt cipher obj)
+ "Runs a prepared cipher to decrypt data."
+ (let* ((bv (match obj
+ ((? bytevector? obj)
+ (bytevector-copy obj))
+ ((? string? obj)
+ (string->utf8 obj))))
+ (err (%gcry-cipher-decrypt (cipher->pointer cipher)
+ (bytevector->pointer bv)
+ (bytevector-length bv)
+ %null-pointer
+ 0)))
+ (if (= err 0)
+ bv
+ (throw 'gcry-error 'cipher-decrypt err))))
+
+(define %gcry-cipher-authenticate
+ (libgcrypt->procedure int
+ "gcry_cipher_authenticate"
+ ;; gcry_cipher_hd_t h, const void *abuf, size_t abuflen
+ '(* * ,size_t)))
+
+(define (cipher-authenticate cipher obj)
+ "Process additional authenticated data (AAD), AEAD cipher modes only."
+ (let* ((bv (match obj
+ ((? bytevector? obj)
+ obj)
+ ((? string? obj)
+ (string->utf8 obj))))
+ (err (%gcry-cipher-authenticate (cipher->pointer cipher)
+ (bytevector->pointer bv)
+ (bytevector-length bv))))
+ (if (= err 0)
+ #t
+ (throw 'gcry-error 'cipher-authenticate err))))
+
+;; (define %gcryctl-get-taglen 76)
+;; (define %gcry-cipher-info
+;; (libgcrypt->procedure int
+;; "gcry_cipher_info"
+;; ;; gcry_cipher_hd_t h, int what, void *buffer, size_t *nbytes
+;; '(* ,int * ,size_t)))
+
+;; (define %gcry-cipher-gettag
+;; (libgcrypt->procedure int
+;; "gcry_cipher_gettag"
+;; ;; gcry_cipher_hd_t h, void *tag, size_t taglen
+;; '(* * ,size_t)))
+
+;; (define (cipher-get-tag cipher)
+;; "Retrieve the tag after encrypting data with an AEAD cipher mode."
+;; (let* (
+;; (tag-len-bv (make-bytevector (sizeof size_t) 0))
+;; (tag-len-err (%gcry-cipher-info cipher
+;; %gcryctl-get-taglen
+;; %null-pointer
+;; (bytevector->pointer tag-len-bv)))
+;; (bv-len (bytevector-uint-ref tag-len-bv 0 (native-endianness) (sizeof size_t)))
+;; (bv-len 16)
+;; (bv (make-bytevector bv-len))
+;; (err (%gcry-cipher-gettag (cipher->pointer cipher)
+;; (bytevector->pointer bv)
+;; bv-len)))
+;; (if (= err 0)
+;; bv
+;; (throw 'gcry-error 'cipher-get-tag err))))
+
+;; (define %gcry-cipher-checktag
+;; (libgcrypt->procedure int
+;; "gcry_cipher_checktag"
+;; ;; gcry_cipher_hd_t h, const void *tag, size_t taglen
+;; '(* * size_t)))
+
+;; (define %gpg-err-checksum 10)
+
+;; (define (cipher-check-tag cipher obj)
+;; "Compare the tag to the tag computed while decrypting a ciphertext. AEAD cipher modes only."
+;; (let* ((bv (match obj
+;; ((? bytevector? obj)
+;; obj)
+;; ((? string? obj)
+;; (string->utf8 obj))))
+;; (err (%gcry-cipher-checktag (cipher->pointer cipher)
+;; (bytevector->pointer bv)
+;; (bytevector-length bv))))
+;; (if (= err 0)
+;; #t
+;; (if (= err %gpg-err-checksum)
+;; #f
+;; (throw 'gcry-error 'cipher-check-tag err)))))
+
+;; (define %gcry-cipher-set-decryption-tag
+;; ;; Needed for SIV and GCM-SIV
+;; (libgcrypt->procedure int
+;; "gcry_cipher_set_decryption_tag"
+;; ;; gcry_cipher_hd_t h, const void *tag, size_t taglen
+;; `(* * ,size_t)
+;; ))
+
+;; (define (cipher-set-decryption-tag cipher obj)
+;; "Use only for SIV and GCM-SIV modes. For other modes, use `_checktag'."
+;; (let* ((bv (match obj
+;; ((? bytevector? obj)
+;; obj)
+;; ((? string? obj)
+;; (string->utf8 obj))))
+;; (err (%gcry-cipher-set-decryption-tag (cipher->pointer cipher)
+;; (bytevector->pointer bv)
+;; (bytevector-length bv))))
+;; (if (= err 0)
+;; #t
+;; (throw 'gcry-error 'cipher-set-decryption-tag err))))
+
+(define* (encrypt algorithm mode key iv plaintext #:key (ctr #f))
+ "Encrypt data using the specified algorithm, mode, key, and IV.
+
+Pad your plaintext if the mode requires it!
+Returns a bytevector with the ciphertext."
+ (let ((cipher (cipher-open algorithm mode)))
+ (cipher-setkey cipher key)
+ (if ctr
+ (cipher-setctr cipher iv)
+ (cipher-setiv cipher iv))
+ (let ((ciphertext (cipher-encrypt cipher plaintext)))
+ (cipher-close cipher)
+ ciphertext)))
+
+;; (define* (encrypt-aead algorithm mode key iv plaintext #:key (aad #f) (ctr #f))
+;; "Encrypt data using the specified algorithm, mode, key, and IV.
+
+;; Pad your plaintext if the mode requires it!
+;; Returns `(ciphertext . tag)'."
+;; (let ((cipher (cipher-open algorithm mode)))
+;; (cipher-setkey cipher key)
+;; (if ctr
+;; (cipher-setctr cipher ctr)
+;; (cipher-setiv cipher iv))
+;; (if aad (cipher-authenticate cipher aad))
+;; (let ((ciphertext-tag (cons (cipher-encrypt cipher plaintext)
+;; (cipher-get-tag cipher))))
+;; (cipher-close cipher)
+;; ciphertext-tag)))
+
+(define* (decrypt algorithm mode key iv ciphertext #:key (ctr #f))
+ "Decrypt data using the specified algorithm, mode, key, and IV.
+
+Returns a bytevector with the plaintext."
+ (let ((cipher (cipher-open algorithm mode)))
+ (cipher-setkey cipher key)
+ (if ctr
+ (cipher-setctr cipher iv)
+ (cipher-setiv cipher iv))
+ (let ((plaintext (cipher-decrypt cipher ciphertext)))
+ (cipher-close cipher)
+ plaintext)))
+
+;; (define* (decrypt-aead algorithm mode key iv ciphertext tag #:key (aad #f) (ctr #f))
+;; "Decrypt data using the specified algorithm, mode, key, and IV.
+
+;; Returns `(plaintext . tag-verified?)'."
+;; (let ((cipher (cipher-open algorithm mode)))
+;; (cipher-setkey cipher key)
+;; (if ctr
+;; (cipher-setctr cipher ctr)
+;; (cipher-setiv cipher iv))
+;; (if aad (cipher-authenticate cipher aad))
+;; (let ((plaintext-tagv (cons (cipher-decrypt cipher ciphertext)
+;; (cipher-check-tag cipher tag))))
+;; (cipher-close cipher)
+;; plaintext-tagv)))
diff --git a/tests/cipher.scm b/tests/cipher.scm
new file mode 100644
index 0000000..5695724
--- /dev/null
+++ b/tests/cipher.scm
@@ -0,0 +1,68 @@
+;;; guile-gcrypt --- crypto tooling for guile
+;;; Copyright © 2026 Saad Rhoulam <[email protected]>
+;;;
+;;; This file is part of guile-gcrypt.
+;;;
+;;; guile-gcrypt is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 3 of
+;;; the License, or (at your option) any later version.
+;;;
+;;; guile-gcrypt is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;;; Lesser General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU Lesser General Public License
+;;; along with guile-gcrypt. If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (test-mac)
+ #:use-module (rnrs bytevectors)
+ #:use-module (srfi srfi-64)
+ #:use-module (gcrypt cipher)
+ #:use-module (gcrypt random))
+
+(test-begin "cipher")
+
+(test-equal "lookup-cipher-algorithm"
+ (cipher-algorithm aes128)
+ (lookup-cipher-algorithm 'aes128))
+
+(test-equal "lookup-cipher-mode"
+ (cipher-mode gcm)
+ (lookup-cipher-mode 'gcm))
+
+(test-eq "cipher-algorithm-name"
+ 'aes128
+ (cipher-algorithm-name (cipher-algorithm aes128)))
+
+(test-eq "cipher-mode-name"
+ 'gcm
+ (cipher-mode-name (cipher-mode gcm)))
+
+(test-equal "cipher-key-size"
+ (list 16 24 32)
+ (map cipher-key-size
+ (list (cipher-algorithm aes128)
+ (cipher-algorithm aes192)
+ (cipher-algorithm aes256))))
+
+(define test-key (gen-random-bv 16))
+(define test-iv (gen-random-bv 16))
+(define test-aad (gen-random-bv 8))
+
+(let* ((ct (encrypt (cipher-algorithm aes128) (cipher-mode cbc) test-key test-iv "sixteencharacter")))
+ ;; Should be a bytevector
+ (test-assert (bytevector? ct))
+ ;; Decrypts to original plaintext
+ (test-assert (equal? (string->utf8 "sixteencharacter") (decrypt (cipher-algorithm aes128) (cipher-mode cbc) test-key test-iv ct)))
+ ;; Wrong algorithm fails
+ (test-assert (not (equal? "sixteencharacter" (decrypt (cipher-algorithm serpent128) (cipher-mode cbc) test-key test-iv ct))))
+ ;; Wrong mode fails
+ (test-assert (not (equal? ct (encrypt (cipher-algorithm aes128) (cipher-mode ctr) test-key test-iv "sixteencharacter" #:ctr #t))))
+ ;; Should equal a re-run of itself
+ (test-equal ct (encrypt (cipher-algorithm aes128) (cipher-mode cbc) test-key test-iv "sixteencharacter"))
+ ;; Shouldn't equal something different
+ (test-assert (not (equal? ct (encrypt (cipher-algorithm aes128) (cipher-mode cbc) test-key test-iv "filthycaricature")))))
+
+(test-end "cipher")
_______________________________________________
Gcrypt-devel mailing list
[email protected]
https://lists.gnupg.org/mailman/listinfo/gcrypt-devel