From e2ec048a09889b2bf71e8bbfef9f0584ff7d69db Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sat, 4 Sep 2021 11:41:56 +0200 Subject: stupidgcm: introduce stupidAEADCommon and use for both chacha & gcm Nice deduplication and brings the GCM decrypt speed up to par. internal/speed$ benchstat old new name old time/op new time/op delta StupidGCM-4 4.71µs ± 0% 4.66µs ± 0% -0.99% (p=0.008 n=5+5) StupidGCMDecrypt-4 5.77µs ± 1% 4.51µs ± 0% -21.80% (p=0.008 n=5+5) name old speed new speed delta StupidGCM-4 870MB/s ± 0% 879MB/s ± 0% +1.01% (p=0.008 n=5+5) StupidGCMDecrypt-4 710MB/s ± 1% 908MB/s ± 0% +27.87% (p=0.008 n=5+5) --- internal/stupidgcm/chacha.c | 194 ----------------------------- internal/stupidgcm/chacha.go | 35 ++++++ internal/stupidgcm/chacha.h | 32 ----- internal/stupidgcm/chacha_test.go | 20 +++ internal/stupidgcm/common.go | 68 +++++++++++ internal/stupidgcm/common_test.go | 17 +-- internal/stupidgcm/gcm.go | 45 +++++++ internal/stupidgcm/gcm_test.go | 28 +++++ internal/stupidgcm/openssl.go | 108 ++++++++++++++++ internal/stupidgcm/openssl_aead.c | 178 +++++++++++++++++++++++++++ internal/stupidgcm/openssl_aead.h | 29 +++++ internal/stupidgcm/stupidchacha.go | 156 ----------------------- internal/stupidgcm/stupidchacha_test.go | 20 --- internal/stupidgcm/stupidgcm.go | 204 ------------------------------- internal/stupidgcm/stupidgcm_test.go | 28 ----- internal/stupidgcm/stupidxchacha.go | 111 ----------------- internal/stupidgcm/stupidxchacha_test.go | 20 --- internal/stupidgcm/xchacha.go | 111 +++++++++++++++++ internal/stupidgcm/xchacha_test.go | 20 +++ 19 files changed, 651 insertions(+), 773 deletions(-) delete mode 100644 internal/stupidgcm/chacha.c create mode 100644 internal/stupidgcm/chacha.go delete mode 100644 internal/stupidgcm/chacha.h create mode 100644 internal/stupidgcm/chacha_test.go create mode 100644 internal/stupidgcm/common.go create mode 100644 internal/stupidgcm/gcm.go create mode 100644 internal/stupidgcm/gcm_test.go create mode 100644 internal/stupidgcm/openssl.go create mode 100644 internal/stupidgcm/openssl_aead.c create mode 100644 internal/stupidgcm/openssl_aead.h delete mode 100644 internal/stupidgcm/stupidchacha.go delete mode 100644 internal/stupidgcm/stupidchacha_test.go delete mode 100644 internal/stupidgcm/stupidgcm.go delete mode 100644 internal/stupidgcm/stupidgcm_test.go delete mode 100644 internal/stupidgcm/stupidxchacha.go delete mode 100644 internal/stupidgcm/stupidxchacha_test.go create mode 100644 internal/stupidgcm/xchacha.go create mode 100644 internal/stupidgcm/xchacha_test.go diff --git a/internal/stupidgcm/chacha.c b/internal/stupidgcm/chacha.c deleted file mode 100644 index 05d68af..0000000 --- a/internal/stupidgcm/chacha.c +++ /dev/null @@ -1,194 +0,0 @@ -#include "chacha.h" -#include -#include -//#cgo pkg-config: libcrypto - -static void panic(const char* const msg) -{ - fprintf(stderr, "panic in C code: %s\n", msg); - __builtin_trap(); -} - -static const EVP_CIPHER* getEvpCipher(enum aeadType cipherId) -{ - switch (cipherId) { - case aeadTypeChacha: - return EVP_chacha20_poly1305(); - case aeadTypeGcm: - return EVP_aes_256_gcm(); - } - panic("unknown cipherId"); - return NULL; -} - -// We only support 16-byte tags -static const int supportedTagLen = 16; - -// https://wiki.openssl.org/index.php/EVP_Authenticated_Encryption_and_Decryption#Authenticated_Encryption_using_GCM_mode -int aead_seal( - const enum aeadType cipherId, - const unsigned char* const plaintext, - const int plaintextLen, - const unsigned char* const authData, - const int authDataLen, - const unsigned char* const key, - const int keyLen, - const unsigned char* const iv, - const int ivLen, - unsigned char* const ciphertext, - const int ciphertextBufLen) -{ - const EVP_CIPHER* evpCipher = getEvpCipher(cipherId); - - // Create scratch space "ctx" - EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); - if (!ctx) { - panic("EVP_CIPHER_CTX_new failed"); - } - - // Set cipher - if (EVP_EncryptInit_ex(ctx, evpCipher, NULL, NULL, NULL) != 1) { - panic("EVP_EncryptInit_ex set cipher failed"); - } - - // Check keyLen by trying to set it (fails if keyLen != 32) - if (EVP_CIPHER_CTX_set_key_length(ctx, keyLen) != 1) { - panic("keyLen mismatch"); - } - - // Set IV length so we do not depend on the default - if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, ivLen, NULL) != 1) { - panic("EVP_CTRL_AEAD_SET_IVLEN failed"); - } - - // Set key and IV - if (EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv) != 1) { - panic("EVP_EncryptInit_ex set key & iv failed"); - } - - // Provide authentication data - int outLen = 0; - if (EVP_EncryptUpdate(ctx, NULL, &outLen, authData, authDataLen) != 1) { - panic("EVP_EncryptUpdate authData failed"); - } - if (outLen != authDataLen) { - panic("EVP_EncryptUpdate authData: unexpected length"); - } - - // Encrypt "plaintext" into "ciphertext" - if (plaintextLen > ciphertextBufLen) { - panic("plaintext overflows output buffer"); - } - if (EVP_EncryptUpdate(ctx, ciphertext, &outLen, plaintext, plaintextLen) != 1) { - panic("EVP_EncryptUpdate ciphertext failed"); - } - if (outLen != plaintextLen) { - panic("EVP_EncryptUpdate ciphertext: unexpected length"); - } - int ciphertextLen = outLen; - - // Finalise encryption - // Normally ciphertext bytes may be written at this stage, but this does not occur in GCM mode - if (EVP_EncryptFinal_ex(ctx, ciphertext + plaintextLen, &outLen) != 1) { - panic("EVP_EncryptFinal_ex failed"); - } - if (outLen != 0) { - panic("EVP_EncryptFinal_ex: unexpected length"); - } - - // Get MAC tag and append it to the ciphertext - if (ciphertextLen + supportedTagLen > ciphertextBufLen) { - panic("tag overflows output buffer"); - } - if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, supportedTagLen, ciphertext + plaintextLen) != 1) { - panic("EVP_CTRL_AEAD_GET_TAG failed"); - } - ciphertextLen += supportedTagLen; - - // Free scratch space - EVP_CIPHER_CTX_free(ctx); - - return ciphertextLen; -} - -int aead_open( - const enum aeadType cipherId, - const unsigned char* const ciphertext, - const int ciphertextLen, - const unsigned char* const authData, - const int authDataLen, - unsigned char* const tag, - const int tagLen, - const unsigned char* const key, - const int keyLen, - const unsigned char* const iv, - const int ivLen, - unsigned char* const plaintext, - const int plaintextBufLen) -{ - const EVP_CIPHER* evpCipher = getEvpCipher(cipherId); - - // Create scratch space "ctx" - EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); - if (!ctx) { - panic("EVP_CIPHER_CTX_new failed"); - } - - // Set cipher - if (EVP_DecryptInit_ex(ctx, evpCipher, NULL, NULL, NULL) != 1) { - panic("EVP_DecryptInit_ex set cipher failed"); - } - - // Check keyLen by trying to set it (fails if keyLen != 32) - if (EVP_CIPHER_CTX_set_key_length(ctx, keyLen) != 1) { - panic("keyLen mismatch"); - } - - // Set IV length so we do not depend on the default - if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, ivLen, NULL) != 1) { - panic("EVP_CTRL_AEAD_SET_IVLEN failed"); - } - - // Set key and IV - if (EVP_DecryptInit_ex(ctx, NULL, NULL, key, iv) != 1) { - panic("EVP_DecryptInit_ex set key & iv failed"); - } - - // Provide authentication data - int outLen = 0; - if (EVP_DecryptUpdate(ctx, NULL, &outLen, authData, authDataLen) != 1) { - panic("EVP_DecryptUpdate authData failed"); - } - if (outLen != authDataLen) { - panic("EVP_DecryptUpdate authData: unexpected length"); - } - - // Decrypt "ciphertext" into "plaintext" - if (ciphertextLen > plaintextBufLen) { - panic("ciphertextLen overflows output buffer"); - } - if (EVP_DecryptUpdate(ctx, plaintext, &outLen, ciphertext, ciphertextLen) != 1) { - panic("EVP_DecryptUpdate failed"); - } - int plaintextLen = outLen; - - // Check tag - if (tagLen != supportedTagLen) { - panic("unsupported tag length"); - } - if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, tagLen, tag) != 1) { - panic("EVP_CTRL_AEAD_SET_TAG failed"); - } - if (EVP_DecryptFinal_ex(ctx, plaintext + plaintextLen, &outLen) != 1) { - // authentication failed - return -1; - } - if (outLen != 0) { - panic("EVP_EncryptFinal_ex: unexpected length"); - } - - /* Clean up */ - EVP_CIPHER_CTX_free(ctx); - - return plaintextLen; -} diff --git a/internal/stupidgcm/chacha.go b/internal/stupidgcm/chacha.go new file mode 100644 index 0000000..37f7e1f --- /dev/null +++ b/internal/stupidgcm/chacha.go @@ -0,0 +1,35 @@ +// +build !without_openssl + +package stupidgcm + +import ( + "crypto/cipher" + "log" + + "golang.org/x/crypto/chacha20poly1305" +) + +/* +#include +*/ +import "C" + +type stupidChacha20poly1305 struct { + stupidAEADCommon +} + +// Verify that we satisfy the cipher.AEAD interface +var _ cipher.AEAD = &stupidChacha20poly1305{} + +func newChacha20poly1305(key []byte) *stupidChacha20poly1305 { + if len(key) != chacha20poly1305.KeySize { + log.Panicf("Only %d-byte keys are supported, you passed %d bytes", chacha20poly1305.KeySize, len(key)) + } + return &stupidChacha20poly1305{ + stupidAEADCommon{ + key: append([]byte{}, key...), // private copy + openSSLEVPCipher: C.EVP_chacha20_poly1305(), + nonceSize: chacha20poly1305.NonceSize, + }, + } +} diff --git a/internal/stupidgcm/chacha.h b/internal/stupidgcm/chacha.h deleted file mode 100644 index a5eac04..0000000 --- a/internal/stupidgcm/chacha.h +++ /dev/null @@ -1,32 +0,0 @@ -enum aeadType { - aeadTypeChacha = 1, - aeadTypeGcm = 2, -}; - -int aead_seal( - const enum aeadType cipherId, - const unsigned char* const plaintext, - const int plaintextLen, - const unsigned char* const authData, - const int authDataLen, - const unsigned char* const key, - const int keyLen, - const unsigned char* const iv, - const int ivLen, - unsigned char* const ciphertext, - const int ciphertextBufLen); - -int aead_open( - const enum aeadType cipherId, - const unsigned char* const ciphertext, - const int ciphertextLen, - const unsigned char* const authData, - const int authDataLen, - unsigned char* const tag, - const int tagLen, - const unsigned char* const key, - const int keyLen, - const unsigned char* const iv, - const int ivLen, - unsigned char* const plaintext, - const int plaintextBufLen); diff --git a/internal/stupidgcm/chacha_test.go b/internal/stupidgcm/chacha_test.go new file mode 100644 index 0000000..513b68f --- /dev/null +++ b/internal/stupidgcm/chacha_test.go @@ -0,0 +1,20 @@ +// +build !without_openssl + +package stupidgcm + +import ( + "testing" + + "golang.org/x/crypto/chacha20poly1305" +) + +func TestStupidChacha20poly1305(t *testing.T) { + key := randBytes(32) + c := newChacha20poly1305(key) + ref, err := chacha20poly1305.New(key) + if err != nil { + t.Fatal(err) + } + + testCiphers(t, c, ref) +} diff --git a/internal/stupidgcm/common.go b/internal/stupidgcm/common.go new file mode 100644 index 0000000..3788315 --- /dev/null +++ b/internal/stupidgcm/common.go @@ -0,0 +1,68 @@ +package stupidgcm + +import ( + "log" +) + +/* +#include +*/ +import "C" + +type stupidAEADCommon struct { + wiped bool + key []byte + openSSLEVPCipher *C.EVP_CIPHER + nonceSize int +} + +// Overhead returns the number of bytes that are added for authentication. +// +// Part of the cipher.AEAD interface. +func (c *stupidAEADCommon) Overhead() int { + return tagLen +} + +// NonceSize returns the required size of the nonce / IV +// +// Part of the cipher.AEAD interface. +func (c *stupidAEADCommon) NonceSize() int { + return c.nonceSize +} + +// Seal encrypts "in" using "iv" and "authData" and append the result to "dst" +// +// Part of the cipher.AEAD interface. +func (c *stupidAEADCommon) Seal(dst, iv, in, authData []byte) []byte { + return openSSLSeal(c, dst, iv, in, authData) +} + +// Open decrypts "in" using "iv" and "authData" and append the result to "dst" +// +// Part of the cipher.AEAD interface. +func (c *stupidAEADCommon) Open(dst, iv, in, authData []byte) ([]byte, error) { + return openSSLOpen(c, dst, iv, in, authData) +} + +// Wipe tries to wipe the key from memory by overwriting it with zeros. +// +// This is not bulletproof due to possible GC copies, but +// still raises the bar for extracting the key. +func (c *stupidAEADCommon) Wipe() { + key := c.key + c.wiped = true + c.key = nil + for i := range key { + key[i] = 0 + } +} + +func (c *stupidAEADCommon) Wiped() bool { + if c.wiped { + return true + } + if len(c.key) != keyLen { + log.Panicf("wrong key length %d", len(c.key)) + } + return false +} diff --git a/internal/stupidgcm/common_test.go b/internal/stupidgcm/common_test.go index 8123ce2..a8080ca 100644 --- a/internal/stupidgcm/common_test.go +++ b/internal/stupidgcm/common_test.go @@ -162,25 +162,26 @@ func testCorruption(t *testing.T, c cipher.AEAD) { } } -type Wiper interface { - Wipe() -} - func testWipe(t *testing.T, c cipher.AEAD) { switch c2 := c.(type) { case *StupidGCM: c2.Wipe() - if c2.key != nil { - t.Fatal("key is not nil") + if !c2.Wiped() { + t.Error("c2.wiped is not set") + } + for _, v := range c2.key { + if v != 0 { + t.Fatal("c2._key is not zeroed") + } } case *stupidChacha20poly1305: c2.Wipe() - if !c2.wiped { + if !c2.Wiped() { t.Error("c2.wiped is not set") } for _, v := range c2.key { if v != 0 { - t.Fatal("c2.key is not zeroed") + t.Fatal("c2._key is not zeroed") } } case *stupidXchacha20poly1305: diff --git a/internal/stupidgcm/gcm.go b/internal/stupidgcm/gcm.go new file mode 100644 index 0000000..439e7a7 --- /dev/null +++ b/internal/stupidgcm/gcm.go @@ -0,0 +1,45 @@ +// +build !without_openssl + +// Package stupidgcm is a thin wrapper for OpenSSL's GCM encryption and +// decryption functions. It only support 32-byte keys and 16-bit IVs. +package stupidgcm + +// #include +import "C" + +import ( + "crypto/cipher" + "log" +) + +const ( + // BuiltWithoutOpenssl indicates if openssl been disabled at compile-time + BuiltWithoutOpenssl = false + + keyLen = 32 + ivLen = 16 + tagLen = 16 +) + +// StupidGCM implements the cipher.AEAD interface +type StupidGCM struct { + stupidAEADCommon +} + +// Verify that we satisfy the interface +var _ cipher.AEAD = &StupidGCM{} + +// New returns a new cipher.AEAD implementation.. +func New(keyIn []byte, forceDecode bool) cipher.AEAD { + if len(keyIn) != keyLen { + log.Panicf("Only %d-byte keys are supported", keyLen) + } + return &StupidGCM{ + stupidAEADCommon{ + // Create a private copy of the key + key: append([]byte{}, keyIn...), + openSSLEVPCipher: C.EVP_aes_256_gcm(), + nonceSize: ivLen, + }, + } +} diff --git a/internal/stupidgcm/gcm_test.go b/internal/stupidgcm/gcm_test.go new file mode 100644 index 0000000..5323afa --- /dev/null +++ b/internal/stupidgcm/gcm_test.go @@ -0,0 +1,28 @@ +// +build !without_openssl + +// We compare against Go's built-in GCM implementation. Since stupidgcm only +// supports 128-bit IVs and Go only supports that from 1.5 onward, we cannot +// run these tests on older Go versions. +package stupidgcm + +import ( + "crypto/aes" + "crypto/cipher" + "testing" +) + +func TestStupidGCM(t *testing.T) { + key := randBytes(32) + sGCM := New(key, false) + + gAES, err := aes.NewCipher(key) + if err != nil { + t.Fatal(err) + } + gGCM, err := cipher.NewGCMWithNonceSize(gAES, 16) + if err != nil { + t.Fatal(err) + } + + testCiphers(t, sGCM, gGCM) +} diff --git a/internal/stupidgcm/openssl.go b/internal/stupidgcm/openssl.go new file mode 100644 index 0000000..d57d100 --- /dev/null +++ b/internal/stupidgcm/openssl.go @@ -0,0 +1,108 @@ +package stupidgcm + +import ( + "fmt" + "log" +) + +/* +#include "openssl_aead.h" +#cgo pkg-config: libcrypto +*/ +import "C" + +func openSSLSeal(a *stupidAEADCommon, dst, iv, in, authData []byte) []byte { + if a.Wiped() { + panic("BUG: tried to use wiped key") + } + if len(iv) != a.NonceSize() { + log.Panicf("Only %d-byte IVs are supported, you passed %d bytes", a.NonceSize(), len(iv)) + } + if len(in) == 0 { + log.Panic("Zero-length input data is not supported") + } + + // If the "dst" slice is large enough we can use it as our output buffer + outLen := len(in) + tagLen + var buf []byte + inplace := false + if cap(dst)-len(dst) >= outLen { + inplace = true + buf = dst[len(dst) : len(dst)+outLen] + } else { + buf = make([]byte, outLen) + } + + res := int(C.openssl_aead_seal(a.openSSLEVPCipher, + (*C.uchar)(&in[0]), + C.int(len(in)), + (*C.uchar)(&authData[0]), + C.int(len(authData)), + (*C.uchar)(&a.key[0]), + C.int(len(a.key)), + (*C.uchar)(&iv[0]), + C.int(len(iv)), + (*C.uchar)(&buf[0]), + C.int(len(buf)))) + + if res != outLen { + log.Panicf("expected length %d, got %d", outLen, res) + } + + if inplace { + return dst[:len(dst)+outLen] + } + return append(dst, buf...) +} + +func openSSLOpen(a *stupidAEADCommon, dst, iv, in, authData []byte) ([]byte, error) { + if a.Wiped() { + panic("BUG: tried to use wiped key") + } + if len(iv) != a.NonceSize() { + log.Panicf("Only %d-byte IVs are supported, you passed %d bytes", a.NonceSize(), len(iv)) + } + if len(in) <= tagLen { + return nil, fmt.Errorf("stupidChacha20poly1305: input data too short (%d bytes)", len(in)) + } + + // If the "dst" slice is large enough we can use it as our output buffer + outLen := len(in) - tagLen + var buf []byte + inplace := false + if cap(dst)-len(dst) >= outLen { + inplace = true + buf = dst[len(dst) : len(dst)+outLen] + } else { + buf = make([]byte, len(in)-tagLen) + } + + ciphertext := in[:len(in)-tagLen] + tag := in[len(in)-tagLen:] + + res := int(C.openssl_aead_open(a.openSSLEVPCipher, + (*C.uchar)(&ciphertext[0]), + C.int(len(ciphertext)), + (*C.uchar)(&authData[0]), + C.int(len(authData)), + (*C.uchar)(&tag[0]), + C.int(len(tag)), + (*C.uchar)(&a.key[0]), + C.int(len(a.key)), + (*C.uchar)(&iv[0]), + C.int(len(iv)), + (*C.uchar)(&buf[0]), + C.int(len(buf)))) + + if res < 0 { + return nil, ErrAuth + } + if res != outLen { + log.Panicf("unexpected length %d", res) + } + + if inplace { + return dst[:len(dst)+outLen], nil + } + return append(dst, buf...), nil +} diff --git a/internal/stupidgcm/openssl_aead.c b/internal/stupidgcm/openssl_aead.c new file mode 100644 index 0000000..9dc6866 --- /dev/null +++ b/internal/stupidgcm/openssl_aead.c @@ -0,0 +1,178 @@ +#include "openssl_aead.h" +#include +#include +//#cgo pkg-config: libcrypto + +static void panic(const char* const msg) +{ + fprintf(stderr, "panic in C code: %s\n", msg); + __builtin_trap(); +} + +// We only support 16-byte tags +static const int supportedTagLen = 16; + +// https://wiki.openssl.org/index.php/EVP_Authenticated_Encryption_and_Decryption#Authenticated_Encryption_using_GCM_mode +int openssl_aead_seal( + const EVP_CIPHER* evpCipher, + const unsigned char* const plaintext, + const int plaintextLen, + const unsigned char* const authData, + const int authDataLen, + const unsigned char* const key, + const int keyLen, + const unsigned char* const iv, + const int ivLen, + unsigned char* const ciphertext, + const int ciphertextBufLen) +{ + // Create scratch space "ctx" + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) { + panic("EVP_CIPHER_CTX_new failed"); + } + + // Set cipher + if (EVP_EncryptInit_ex(ctx, evpCipher, NULL, NULL, NULL) != 1) { + panic("EVP_EncryptInit_ex set cipher failed"); + } + + // Check keyLen by trying to set it (fails if keyLen != 32) + if (EVP_CIPHER_CTX_set_key_length(ctx, keyLen) != 1) { + panic("keyLen mismatch"); + } + + // Set IV length so we do not depend on the default + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, ivLen, NULL) != 1) { + panic("EVP_CTRL_AEAD_SET_IVLEN failed"); + } + + // Set key and IV + if (EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv) != 1) { + panic("EVP_EncryptInit_ex set key & iv failed"); + } + + // Provide authentication data + int outLen = 0; + if (EVP_EncryptUpdate(ctx, NULL, &outLen, authData, authDataLen) != 1) { + panic("EVP_EncryptUpdate authData failed"); + } + if (outLen != authDataLen) { + panic("EVP_EncryptUpdate authData: unexpected length"); + } + + // Encrypt "plaintext" into "ciphertext" + if (plaintextLen > ciphertextBufLen) { + panic("plaintext overflows output buffer"); + } + if (EVP_EncryptUpdate(ctx, ciphertext, &outLen, plaintext, plaintextLen) != 1) { + panic("EVP_EncryptUpdate ciphertext failed"); + } + if (outLen != plaintextLen) { + panic("EVP_EncryptUpdate ciphertext: unexpected length"); + } + int ciphertextLen = outLen; + + // Finalise encryption + // Normally ciphertext bytes may be written at this stage, but this does not occur in GCM mode + if (EVP_EncryptFinal_ex(ctx, ciphertext + plaintextLen, &outLen) != 1) { + panic("EVP_EncryptFinal_ex failed"); + } + if (outLen != 0) { + panic("EVP_EncryptFinal_ex: unexpected length"); + } + + // Get MAC tag and append it to the ciphertext + if (ciphertextLen + supportedTagLen > ciphertextBufLen) { + panic("tag overflows output buffer"); + } + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, supportedTagLen, ciphertext + plaintextLen) != 1) { + panic("EVP_CTRL_AEAD_GET_TAG failed"); + } + ciphertextLen += supportedTagLen; + + // Free scratch space + EVP_CIPHER_CTX_free(ctx); + + return ciphertextLen; +} + +int openssl_aead_open( + const EVP_CIPHER* evpCipher, + const unsigned char* const ciphertext, + const int ciphertextLen, + const unsigned char* const authData, + const int authDataLen, + unsigned char* const tag, + const int tagLen, + const unsigned char* const key, + const int keyLen, + const unsigned char* const iv, + const int ivLen, + unsigned char* const plaintext, + const int plaintextBufLen) +{ + // Create scratch space "ctx" + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) { + panic("EVP_CIPHER_CTX_new failed"); + } + + // Set cipher + if (EVP_DecryptInit_ex(ctx, evpCipher, NULL, NULL, NULL) != 1) { + panic("EVP_DecryptInit_ex set cipher failed"); + } + + // Check keyLen by trying to set it (fails if keyLen != 32) + if (EVP_CIPHER_CTX_set_key_length(ctx, keyLen) != 1) { + panic("keyLen mismatch"); + } + + // Set IV length so we do not depend on the default + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, ivLen, NULL) != 1) { + panic("EVP_CTRL_AEAD_SET_IVLEN failed"); + } + + // Set key and IV + if (EVP_DecryptInit_ex(ctx, NULL, NULL, key, iv) != 1) { + panic("EVP_DecryptInit_ex set key & iv failed"); + } + + // Provide authentication data + int outLen = 0; + if (EVP_DecryptUpdate(ctx, NULL, &outLen, authData, authDataLen) != 1) { + panic("EVP_DecryptUpdate authData failed"); + } + if (outLen != authDataLen) { + panic("EVP_DecryptUpdate authData: unexpected length"); + } + + // Decrypt "ciphertext" into "plaintext" + if (ciphertextLen > plaintextBufLen) { + panic("ciphertextLen overflows output buffer"); + } + if (EVP_DecryptUpdate(ctx, plaintext, &outLen, ciphertext, ciphertextLen) != 1) { + panic("EVP_DecryptUpdate failed"); + } + int plaintextLen = outLen; + + // Check tag + if (tagLen != supportedTagLen) { + panic("unsupported tag length"); + } + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, tagLen, tag) != 1) { + panic("EVP_CTRL_AEAD_SET_TAG failed"); + } + if (EVP_DecryptFinal_ex(ctx, plaintext + plaintextLen, &outLen) != 1) { + // authentication failed + return -1; + } + if (outLen != 0) { + panic("EVP_EncryptFinal_ex: unexpected length"); + } + + /* Clean up */ + EVP_CIPHER_CTX_free(ctx); + + return plaintextLen; +} diff --git a/internal/stupidgcm/openssl_aead.h b/internal/stupidgcm/openssl_aead.h new file mode 100644 index 0000000..6a818b6 --- /dev/null +++ b/internal/stupidgcm/openssl_aead.h @@ -0,0 +1,29 @@ +#include + +int openssl_aead_seal( + const EVP_CIPHER* evpCipher, + const unsigned char* const plaintext, + const int plaintextLen, + const unsigned char* const authData, + const int authDataLen, + const unsigned char* const key, + const int keyLen, + const unsigned char* const iv, + const int ivLen, + unsigned char* const ciphertext, + const int ciphertextBufLen); + +int openssl_aead_open( + const EVP_CIPHER* evpCipher, + const unsigned char* const ciphertext, + const int ciphertextLen, + const unsigned char* const authData, + const int authDataLen, + unsigned char* const tag, + const int tagLen, + const unsigned char* const key, + const int keyLen, + const unsigned char* const iv, + const int ivLen, + unsigned char* const plaintext, + const int plaintextBufLen); diff --git a/internal/stupidgcm/stupidchacha.go b/internal/stupidgcm/stupidchacha.go deleted file mode 100644 index 5073aa3..0000000 --- a/internal/stupidgcm/stupidchacha.go +++ /dev/null @@ -1,156 +0,0 @@ -// +build !without_openssl - -package stupidgcm - -import ( - "crypto/cipher" - "fmt" - "log" - - "golang.org/x/crypto/chacha20poly1305" -) - -/* -#include -#include "chacha.h" -#cgo pkg-config: libcrypto -*/ -import "C" - -type stupidChacha20poly1305 struct { - key [chacha20poly1305.KeySize]byte - wiped bool -} - -// Verify that we satisfy the cipher.AEAD interface -var _ cipher.AEAD = &stupidChacha20poly1305{} - -func newChacha20poly1305(key []byte) cipher.AEAD { - if len(key) != chacha20poly1305.KeySize { - log.Panicf("Only %d-byte keys are supported, you passed %d bytes", chacha20poly1305.KeySize, len(key)) - } - ret := new(stupidChacha20poly1305) - copy(ret.key[:], key) - return ret -} - -// NonceSize returns the required size of the nonce / IV. -func (g *stupidChacha20poly1305) NonceSize() int { - return chacha20poly1305.NonceSize -} - -// Overhead returns the number of bytes that are added for authentication. -func (g *stupidChacha20poly1305) Overhead() int { - return tagLen -} - -// Seal encrypts "in" using "iv" and "authData" and append the result to "dst" -func (g *stupidChacha20poly1305) Seal(dst, iv, in, authData []byte) []byte { - if g.wiped { - panic("BUG: tried to use wiped key") - } - if len(iv) != g.NonceSize() { - log.Panicf("Only %d-byte IVs are supported, you passed %d bytes", g.NonceSize(), len(iv)) - } - if len(in) == 0 { - log.Panic("Zero-length input data is not supported") - } - if len(g.key) != chacha20poly1305.KeySize { - log.Panicf("Wrong key length: %d. Key has been wiped?", len(g.key)) - } - - // If the "dst" slice is large enough we can use it as our output buffer - outLen := len(in) + tagLen - var buf []byte - inplace := false - if cap(dst)-len(dst) >= outLen { - inplace = true - buf = dst[len(dst) : len(dst)+outLen] - } else { - buf = make([]byte, outLen) - } - - C.aead_seal(C.aeadTypeChacha, - (*C.uchar)(&in[0]), - C.int(len(in)), - (*C.uchar)(&authData[0]), - C.int(len(authData)), - (*C.uchar)(&g.key[0]), - C.int(len(g.key)), - (*C.uchar)(&iv[0]), - C.int(len(iv)), - (*C.uchar)(&buf[0]), - C.int(len(buf))) - - if inplace { - return dst[:len(dst)+outLen] - } - return append(dst, buf...) -} - -// Open decrypts "in" using "iv" and "authData" and append the result to "dst" -func (g *stupidChacha20poly1305) Open(dst, iv, in, authData []byte) ([]byte, error) { - if g.wiped { - panic("BUG: tried to use wiped key") - } - if len(iv) != g.NonceSize() { - log.Panicf("Only %d-byte IVs are supported", g.NonceSize()) - } - if len(g.key) != chacha20poly1305.KeySize { - log.Panicf("Wrong key length: %d. Key has been wiped?", len(g.key)) - } - if len(in) <= tagLen { - return nil, fmt.Errorf("stupidChacha20poly1305: input data too short (%d bytes)", len(in)) - } - - // If the "dst" slice is large enough we can use it as our output buffer - outLen := len(in) - tagLen - var buf []byte - inplace := false - if cap(dst)-len(dst) >= outLen { - inplace = true - buf = dst[len(dst) : len(dst)+outLen] - } else { - buf = make([]byte, len(in)-tagLen) - } - - ciphertext := in[:len(in)-tagLen] - tag := in[len(in)-tagLen:] - - res := int(C.aead_open(C.aeadTypeChacha, - (*C.uchar)(&ciphertext[0]), - C.int(len(ciphertext)), - (*C.uchar)(&authData[0]), - C.int(len(authData)), - (*C.uchar)(&tag[0]), - C.int(len(tag)), - (*C.uchar)(&g.key[0]), - C.int(len(g.key)), - (*C.uchar)(&iv[0]), - C.int(len(iv)), - (*C.uchar)(&buf[0]), - C.int(len(buf)))) - - if res < 0 { - return nil, ErrAuth - } - if res != outLen { - log.Panicf("unexpected length %d", res) - } - - if inplace { - return dst[:len(dst)+outLen], nil - } - return append(dst, buf...), nil -} - -// Wipe tries to wipe the key from memory by overwriting it with zeros. -// -// This is not bulletproof due to possible GC copies, but -// still raises the bar for extracting the key. -func (g *stupidChacha20poly1305) Wipe() { - g.wiped = true - for i := range g.key { - g.key[i] = 0 - } -} diff --git a/internal/stupidgcm/stupidchacha_test.go b/internal/stupidgcm/stupidchacha_test.go deleted file mode 100644 index 513b68f..0000000 --- a/internal/stupidgcm/stupidchacha_test.go +++ /dev/null @@ -1,20 +0,0 @@ -// +build !without_openssl - -package stupidgcm - -import ( - "testing" - - "golang.org/x/crypto/chacha20poly1305" -) - -func TestStupidChacha20poly1305(t *testing.T) { - key := randBytes(32) - c := newChacha20poly1305(key) - ref, err := chacha20poly1305.New(key) - if err != nil { - t.Fatal(err) - } - - testCiphers(t, c, ref) -} diff --git a/internal/stupidgcm/stupidgcm.go b/internal/stupidgcm/stupidgcm.go deleted file mode 100644 index 46b6b86..0000000 --- a/internal/stupidgcm/stupidgcm.go +++ /dev/null @@ -1,204 +0,0 @@ -// +build !without_openssl - -// Package stupidgcm is a thin wrapper for OpenSSL's GCM encryption and -// decryption functions. It only support 32-byte keys and 16-bit IVs. -package stupidgcm - -// #include -// #include "chacha.h" -// #cgo pkg-config: libcrypto -import "C" - -import ( - "crypto/cipher" - "fmt" - "log" - "unsafe" -) - -const ( - // BuiltWithoutOpenssl indicates if openssl been disabled at compile-time - BuiltWithoutOpenssl = false - - keyLen = 32 - ivLen = 16 - tagLen = 16 -) - -// StupidGCM implements the cipher.AEAD interface -type StupidGCM struct { - key []byte - forceDecode bool -} - -// Verify that we satisfy the cipher.AEAD interface -var _ cipher.AEAD = &StupidGCM{} - -// New returns a new cipher.AEAD implementation.. -func New(keyIn []byte, forceDecode bool) cipher.AEAD { - if len(keyIn) != keyLen { - log.Panicf("Only %d-byte keys are supported", keyLen) - } - // Create a private copy of the key - key := append([]byte{}, keyIn...) - return &StupidGCM{key: key, forceDecode: forceDecode} -} - -// NonceSize returns the required size of the nonce / IV. -func (g *StupidGCM) NonceSize() int { - return ivLen -} - -// Overhead returns the number of bytes that are added for authentication. -func (g *StupidGCM) Overhead() int { - return tagLen -} - -// Seal encrypts "in" using "iv" and "authData" and append the result to "dst" -func (g *StupidGCM) Seal(dst, iv, in, authData []byte) []byte { - if len(iv) != ivLen { - log.Panicf("Only %d-byte IVs are supported", ivLen) - } - if len(in) == 0 { - log.Panic("Zero-length input data is not supported") - } - if len(g.key) != keyLen { - log.Panicf("Wrong key length: %d. Key has been wiped?", len(g.key)) - } - - // If the "dst" slice is large enough we can use it as our output buffer - outLen := len(in) + tagLen - var buf []byte - inplace := false - if cap(dst)-len(dst) >= outLen { - inplace = true - buf = dst[len(dst) : len(dst)+outLen] - } else { - buf = make([]byte, outLen) - } - - C.aead_seal(C.aeadTypeGcm, - (*C.uchar)(&in[0]), - C.int(len(in)), - (*C.uchar)(&authData[0]), - C.int(len(authData)), - (*C.uchar)(&g.key[0]), - C.int(len(g.key)), - (*C.uchar)(&iv[0]), - C.int(len(iv)), - (*C.uchar)(&buf[0]), - C.int(len(buf))) - - if inplace { - return dst[:len(dst)+outLen] - } - return append(dst, buf...) -} - -// Open decrypts "in" using "iv" and "authData" and append the result to "dst" -func (g *StupidGCM) Open(dst, iv, in, authData []byte) ([]byte, error) { - if len(iv) != ivLen { - log.Panicf("Only %d-byte IVs are supported", ivLen) - } - if len(g.key) != keyLen { - log.Panicf("Wrong key length: %d. Key has been wiped?", len(g.key)) - } - if len(in) <= tagLen { - return nil, fmt.Errorf("stupidgcm: input data too short (%d bytes)", len(in)) - } - - // If the "dst" slice is large enough we can use it as our output buffer - outLen := len(in) - tagLen - var buf []byte - inplace := false - if cap(dst)-len(dst) >= outLen { - inplace = true - buf = dst[len(dst) : len(dst)+outLen] - } else { - buf = make([]byte, len(in)-tagLen) - } - - ciphertext := in[:len(in)-tagLen] - tag := in[len(in)-tagLen:] - - // https://wiki.openssl.org/index.php/EVP_Authenticated_Encryption_and_Decryption#Authenticated_Encryption_using_GCM_mode - - // Create scratch space "context" - ctx := C.EVP_CIPHER_CTX_new() - if ctx == nil { - log.Panic("EVP_CIPHER_CTX_new failed") - } - - // Set cipher to AES-256 - if C.EVP_DecryptInit_ex(ctx, C.EVP_aes_256_gcm(), nil, nil, nil) != 1 { - log.Panic("EVP_DecryptInit_ex I failed") - } - - // Use 16-byte IV - if C.EVP_CIPHER_CTX_ctrl(ctx, C.EVP_CTRL_GCM_SET_IVLEN, ivLen, nil) != 1 { - log.Panic("EVP_CIPHER_CTX_ctrl EVP_CTRL_GCM_SET_IVLEN failed") - } - - // Set key and IV - if C.EVP_DecryptInit_ex(ctx, nil, nil, (*C.uchar)(&g.key[0]), (*C.uchar)(&iv[0])) != 1 { - log.Panic("EVP_DecryptInit_ex II failed") - } - - // Set expected GMAC tag - if C.EVP_CIPHER_CTX_ctrl(ctx, C.EVP_CTRL_GCM_SET_TAG, tagLen, (unsafe.Pointer)(&tag[0])) != 1 { - log.Panic("EVP_CIPHER_CTX_ctrl failed") - } - - // Provide authentication data - var resultLen C.int - if C.EVP_DecryptUpdate(ctx, nil, &resultLen, (*C.uchar)(&authData[0]), C.int(len(authData))) != 1 { - log.Panic("EVP_DecryptUpdate authData failed") - } - if int(resultLen) != len(authData) { - log.Panicf("Unexpected length %d", resultLen) - } - - // Decrypt "ciphertext" into "buf" - if C.EVP_DecryptUpdate(ctx, (*C.uchar)(&buf[0]), &resultLen, (*C.uchar)(&ciphertext[0]), C.int(len(ciphertext))) != 1 { - log.Panic("EVP_DecryptUpdate failed") - } - if int(resultLen) != len(ciphertext) { - log.Panicf("Unexpected length %d", resultLen) - } - - // Check GMAC - dummy := make([]byte, 16) - res := C.EVP_DecryptFinal_ex(ctx, (*C.uchar)(&dummy[0]), &resultLen) - if resultLen != 0 { - log.Panicf("Unexpected length %d", resultLen) - } - - // Free scratch space - C.EVP_CIPHER_CTX_free(ctx) - - if res != 1 { - // The error code must always be checked by the calling function, because the decrypted buffer - // may contain corrupted data that we are returning in case the user forced reads - if g.forceDecode { - return append(dst, buf...), ErrAuth - } - return nil, ErrAuth - } - - if inplace { - return dst[:len(dst)+outLen], nil - } - return append(dst, buf...), nil -} - -// Wipe tries to wipe the AES key from memory by overwriting it with zeros -// and setting the reference to nil. -// -// This is not bulletproof due to possible GC copies, but -// still raises the bar for extracting the key. -func (g *StupidGCM) Wipe() { - for i := range g.key { - g.key[i] = 0 - } - g.key = nil -} diff --git a/internal/stupidgcm/stupidgcm_test.go b/internal/stupidgcm/stupidgcm_test.go deleted file mode 100644 index 5323afa..0000000 --- a/internal/stupidgcm/stupidgcm_test.go +++ /dev/null @@ -1,28 +0,0 @@ -// +build !without_openssl - -// We compare against Go's built-in GCM implementation. Since stupidgcm only -// supports 128-bit IVs and Go only supports that from 1.5 onward, we cannot -// run these tests on older Go versions. -package stupidgcm - -import ( - "crypto/aes" - "crypto/cipher" - "testing" -) - -func TestStupidGCM(t *testing.T) { - key := randBytes(32) - sGCM := New(key, false) - - gAES, err := aes.NewCipher(key) - if err != nil { - t.Fatal(err) - } - gGCM, err := cipher.NewGCMWithNonceSize(gAES, 16) - if err != nil { - t.Fatal(err) - } - - testCiphers(t, sGCM, gGCM) -} diff --git a/internal/stupidgcm/stupidxchacha.go b/internal/stupidgcm/stupidxchacha.go deleted file mode 100644 index 9f2ac2f..0000000 --- a/internal/stupidgcm/stupidxchacha.go +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. -// -// Copied from -// https://github.com/golang/crypto/blob/32db794688a5a24a23a43f2a984cecd5b3d8da58/chacha20poly1305/xchacha20poly1305.go -// and adapted for stupidgcm by @rfjakob. - -package stupidgcm - -import ( - "crypto/cipher" - "errors" - - "golang.org/x/crypto/chacha20" - "golang.org/x/crypto/chacha20poly1305" -) - -type stupidXchacha20poly1305 struct { - key [chacha20poly1305.KeySize]byte - wiped bool -} - -// NewXchacha20poly1305 returns a XChaCha20-Poly1305 AEAD that uses the given 256-bit key. -// -// XChaCha20-Poly1305 is a ChaCha20-Poly1305 variant that takes a longer nonce, -// suitable to be generated randomly without risk of collisions. It should be -// preferred when nonce uniqueness cannot be trivially ensured, or whenever -// nonces are randomly generated. -func NewXchacha20poly1305(key []byte) cipher.AEAD { - if len(key) != chacha20poly1305.KeySize { - panic("bad key length") - } - ret := new(stupidXchacha20poly1305) - copy(ret.key[:], key) - return ret -} - -func (*stupidXchacha20poly1305) NonceSize() int { - return chacha20poly1305.NonceSizeX -} - -func (*stupidXchacha20poly1305) Overhead() int { - return 16 -} - -func (x *stupidXchacha20poly1305) Seal(dst, nonce, plaintext, additionalData []byte) []byte { - if x.wiped { - panic("BUG: tried to use wiped key") - } - if len(nonce) != chacha20poly1305.NonceSizeX { - panic("bad nonce length passed to Seal") - } - - // XChaCha20-Poly1305 technically supports a 64-bit counter, so there is no - // size limit. However, since we reuse the ChaCha20-Poly1305 implementation, - // the second half of the counter is not available. This is unlikely to be - // an issue because the cipher.AEAD API requires the entire message to be in - // memory, and the counter overflows at 256 GB. - if uint64(len(plaintext)) > (1<<38)-64 { - panic("plaintext too large") - } - - c := new(stupidChacha20poly1305) - hKey, _ := chacha20.HChaCha20(x.key[:], nonce[0:16]) - copy(c.key[:], hKey) - defer c.Wipe() - - // The first 4 bytes of the final nonce are unused counter space. - cNonce := make([]byte, chacha20poly1305.NonceSize) - copy(cNonce[4:12], nonce[16:24]) - - return c.Seal(dst, cNonce[:], plaintext, additionalData) -} - -func (x *stupidXchacha20poly1305) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) { - if x.wiped { - panic("BUG: tried to use wiped key") - } - if len(nonce) != chacha20poly1305.NonceSizeX { - panic("bad nonce length passed to Open") - } - if len(ciphertext) < 16 { - return nil, errors.New("message too short") - } - if uint64(len(ciphertext)) > (1<<38)-48 { - panic("ciphertext too large") - } - - c := new(stupidChacha20poly1305) - hKey, _ := chacha20.HChaCha20(x.key[:], nonce[0:16]) - copy(c.key[:], hKey) - defer c.Wipe() - - // The first 4 bytes of the final nonce are unused counter space. - cNonce := make([]byte, chacha20poly1305.NonceSize) - copy(cNonce[4:12], nonce[16:24]) - - return c.Open(dst, cNonce[:], ciphertext, additionalData) -} - -// Wipe tries to wipe the key from memory by overwriting it with zeros. -// -// This is not bulletproof due to possible GC copies, but -// still raises the bar for extracting the key. -func (g *stupidXchacha20poly1305) Wipe() { - g.wiped = true - for i := range g.key { - g.key[i] = 0 - } -} diff --git a/internal/stupidgcm/stupidxchacha_test.go b/internal/stupidgcm/stupidxchacha_test.go deleted file mode 100644 index fdea8b5..0000000 --- a/internal/stupidgcm/stupidxchacha_test.go +++ /dev/null @@ -1,20 +0,0 @@ -// +build !without_openssl - -package stupidgcm - -import ( - "testing" - - "golang.org/x/crypto/chacha20poly1305" -) - -func TestStupidXchacha20poly1305(t *testing.T) { - key := randBytes(32) - c := NewXchacha20poly1305(key) - ref, err := chacha20poly1305.NewX(key) - if err != nil { - t.Fatal(err) - } - - testCiphers(t, c, ref) -} diff --git a/internal/stupidgcm/xchacha.go b/internal/stupidgcm/xchacha.go new file mode 100644 index 0000000..d8668dc --- /dev/null +++ b/internal/stupidgcm/xchacha.go @@ -0,0 +1,111 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// Copied from +// https://github.com/golang/crypto/blob/32db794688a5a24a23a43f2a984cecd5b3d8da58/chacha20poly1305/xchacha20poly1305.go +// and adapted for stupidgcm by @rfjakob. + +package stupidgcm + +import ( + "crypto/cipher" + "errors" + + "golang.org/x/crypto/chacha20" + "golang.org/x/crypto/chacha20poly1305" +) + +type stupidXchacha20poly1305 struct { + // array instead of byte slice like + // `struct xchacha20poly1305` in x/crypto/chacha20poly1305 + key [chacha20poly1305.KeySize]byte + wiped bool +} + +// NewXchacha20poly1305 returns a XChaCha20-Poly1305 AEAD that uses the given 256-bit key. +// +// XChaCha20-Poly1305 is a ChaCha20-Poly1305 variant that takes a longer nonce, +// suitable to be generated randomly without risk of collisions. It should be +// preferred when nonce uniqueness cannot be trivially ensured, or whenever +// nonces are randomly generated. +func NewXchacha20poly1305(key []byte) cipher.AEAD { + if len(key) != chacha20poly1305.KeySize { + panic("bad key length") + } + ret := new(stupidXchacha20poly1305) + copy(ret.key[:], key) + return ret +} + +func (*stupidXchacha20poly1305) NonceSize() int { + return chacha20poly1305.NonceSizeX +} + +func (*stupidXchacha20poly1305) Overhead() int { + return tagLen +} + +func (x *stupidXchacha20poly1305) Seal(dst, nonce, plaintext, additionalData []byte) []byte { + if x.wiped { + panic("BUG: tried to use wiped key") + } + if len(nonce) != chacha20poly1305.NonceSizeX { + panic("bad nonce length passed to Seal") + } + + // XChaCha20-Poly1305 technically supports a 64-bit counter, so there is no + // size limit. However, since we reuse the ChaCha20-Poly1305 implementation, + // the second half of the counter is not available. This is unlikely to be + // an issue because the cipher.AEAD API requires the entire message to be in + // memory, and the counter overflows at 256 GB. + if uint64(len(plaintext)) > (1<<38)-64 { + panic("plaintext too large") + } + + hKey, _ := chacha20.HChaCha20(x.key[:], nonce[0:16]) + c := newChacha20poly1305(hKey) + defer c.Wipe() + + // The first 4 bytes of the final nonce are unused counter space. + cNonce := make([]byte, chacha20poly1305.NonceSize) + copy(cNonce[4:12], nonce[16:24]) + + return c.Seal(dst, cNonce[:], plaintext, additionalData) +} + +func (x *stupidXchacha20poly1305) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) { + if x.wiped { + panic("BUG: tried to use wiped key") + } + if len(nonce) != chacha20poly1305.NonceSizeX { + panic("bad nonce length passed to Open") + } + if len(ciphertext) < 16 { + return nil, errors.New("message too short") + } + if uint64(len(ciphertext)) > (1<<38)-48 { + panic("ciphertext too large") + } + + hKey, _ := chacha20.HChaCha20(x.key[:], nonce[0:16]) + c := newChacha20poly1305(hKey) + defer c.Wipe() + + // The first 4 bytes of the final nonce are unused counter space. + cNonce := make([]byte, chacha20poly1305.NonceSize) + copy(cNonce[4:12], nonce[16:24]) + + return c.Open(dst, cNonce[:], ciphertext, additionalData) +} + +// Wipe tries to wipe the key from memory by overwriting it with zeros. +// +// This is not bulletproof due to possible GC copies, but +// still raises the bar for extracting the key. +func (g *stupidXchacha20poly1305) Wipe() { + g.wiped = true + for i := range g.key { + g.key[i] = 0 + } +} diff --git a/internal/stupidgcm/xchacha_test.go b/internal/stupidgcm/xchacha_test.go new file mode 100644 index 0000000..fdea8b5 --- /dev/null +++ b/internal/stupidgcm/xchacha_test.go @@ -0,0 +1,20 @@ +// +build !without_openssl + +package stupidgcm + +import ( + "testing" + + "golang.org/x/crypto/chacha20poly1305" +) + +func TestStupidXchacha20poly1305(t *testing.T) { + key := randBytes(32) + c := NewXchacha20poly1305(key) + ref, err := chacha20poly1305.NewX(key) + if err != nil { + t.Fatal(err) + } + + testCiphers(t, c, ref) +} -- cgit v1.2.3