diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/stupidgcm/stupidchacha.go | 222 | ||||
| -rw-r--r-- | internal/stupidgcm/stupidchacha_test.go | 171 | 
2 files changed, 393 insertions, 0 deletions
| diff --git a/internal/stupidgcm/stupidchacha.go b/internal/stupidgcm/stupidchacha.go new file mode 100644 index 0000000..e2f6407 --- /dev/null +++ b/internal/stupidgcm/stupidchacha.go @@ -0,0 +1,222 @@ +// +build !without_openssl + +package stupidgcm + +// #include <openssl/evp.h> +// #cgo pkg-config: libcrypto +import "C" + +import ( +	"crypto/cipher" +	"fmt" +	"log" +	"unsafe" + +	"golang.org/x/crypto/chacha20poly1305" +) + +type stupidChacha20poly1305 struct { +	key []byte +} + +// Verify that we satisfy the cipher.AEAD interface +var _ cipher.AEAD = &stupidChacha20poly1305{} + +func newChacha20poly1305(keyIn []byte) cipher.AEAD { +	if len(keyIn) != chacha20poly1305.KeySize { +		log.Panicf("Only %d-byte keys are supported, you passed %d bytes", chacha20poly1305.KeySize, len(keyIn)) +	} +	// Create a private copy of the key +	key := append([]byte{}, keyIn...) +	return &stupidChacha20poly1305{key: key} +} + +// 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 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) +	} + +	// 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 +	if C.EVP_EncryptInit_ex(ctx, C.EVP_chacha20_poly1305(), nil, nil, nil) != 1 { +		log.Panic("EVP_EncryptInit_ex I failed") +	} + +	// Set key and IV +	if C.EVP_EncryptInit_ex(ctx, nil, nil, (*C.uchar)(&g.key[0]), (*C.uchar)(&iv[0])) != 1 { +		log.Panic("EVP_EncryptInit_ex II failed") +	} + +	// Provide authentication data +	var resultLen C.int +	if C.EVP_EncryptUpdate(ctx, nil, &resultLen, (*C.uchar)(&authData[0]), C.int(len(authData))) != 1 { +		log.Panic("EVP_EncryptUpdate authData failed") +	} +	if int(resultLen) != len(authData) { +		log.Panicf("Unexpected length %d", resultLen) +	} + +	// Encrypt "in" into "buf" +	if C.EVP_EncryptUpdate(ctx, (*C.uchar)(&buf[0]), &resultLen, (*C.uchar)(&in[0]), C.int(len(in))) != 1 { +		log.Panic("EVP_EncryptUpdate failed") +	} +	if int(resultLen) != len(in) { +		log.Panicf("Unexpected length %d", resultLen) +	} + +	// Finalise encryption +	// Because GCM is a stream encryption, this will not write out any data. +	dummy := make([]byte, 16) +	if C.EVP_EncryptFinal_ex(ctx, (*C.uchar)(&dummy[0]), &resultLen) != 1 { +		log.Panic("EVP_EncryptFinal_ex failed") +	} +	if resultLen != 0 { +		log.Panicf("Unexpected length %d", resultLen) +	} + +	// Get MAC tag and append it to the ciphertext in "buf" +	if C.EVP_CIPHER_CTX_ctrl(ctx, C.EVP_CTRL_AEAD_GET_TAG, tagLen, (unsafe.Pointer)(&buf[len(in)])) != 1 { +		log.Panic("EVP_CIPHER_CTX_ctrl EVP_CTRL_AEAD_GET_TAG failed") +	} + +	// Free scratch space +	C.EVP_CIPHER_CTX_free(ctx) + +	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 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:] + +	// 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_chacha20_poly1305(), nil, nil, nil) != 1 { +		log.Panic("EVP_DecryptInit_ex I 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 MAC tag +	if C.EVP_CIPHER_CTX_ctrl(ctx, C.EVP_CTRL_AEAD_SET_TAG, tagLen, (unsafe.Pointer)(&tag[0])) != 1 { +		log.Panic("EVP_CIPHER_CTX_ctrl EVP_CTRL_AEAD_SET_TAG 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 MAC +	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 { +		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 to bar for extracting the key. +func (g *stupidChacha20poly1305) Wipe() { +	for i := range g.key { +		g.key[i] = 0 +	} +	g.key = nil +} diff --git a/internal/stupidgcm/stupidchacha_test.go b/internal/stupidgcm/stupidchacha_test.go new file mode 100644 index 0000000..010055f --- /dev/null +++ b/internal/stupidgcm/stupidchacha_test.go @@ -0,0 +1,171 @@ +// +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 ( +	"bytes" +	"encoding/hex" +	"testing" + +	"golang.org/x/crypto/chacha20poly1305" +) + +// TestEncryptDecrypt encrypts and decrypts using both stupidgcm and Go's built-in +// GCM implementation and verifies that the results are identical. +func TestEncryptDecryptChacha(t *testing.T) { +	key := randBytes(32) +	sGCM := newChacha20poly1305(key) +	authData := randBytes(24) +	iv := randBytes(sGCM.NonceSize()) +	dst := make([]byte, 71) // 71 = random length + +	gGCM, err := chacha20poly1305.New(key) +	if err != nil { +		t.Fatal(err) +	} + +	// Check all block sizes from 1 to 5000 +	for i := 1; i < 5000; i++ { +		in := make([]byte, i) + +		sOut := sGCM.Seal(dst, iv, in, authData) +		gOut := gGCM.Seal(dst, iv, in, authData) + +		// Ciphertext must be identical to Go GCM +		if !bytes.Equal(sOut, gOut) { +			t.Fatalf("Compare failed for encryption, size %d", i) +			t.Log("sOut:") +			t.Log("\n" + hex.Dump(sOut)) +			t.Log("gOut:") +			t.Log("\n" + hex.Dump(gOut)) +		} + +		sOut2, sErr := sGCM.Open(dst, iv, sOut[len(dst):], authData) +		if sErr != nil { +			t.Fatal(sErr) +		} +		gOut2, gErr := gGCM.Open(dst, iv, gOut[len(dst):], authData) +		if gErr != nil { +			t.Fatal(gErr) +		} + +		// Plaintext must be identical to Go GCM +		if !bytes.Equal(sOut2, gOut2) { +			t.Fatalf("Compare failed for decryption, size %d", i) +		} +	} +} + +// Seal re-uses the "dst" buffer it is large enough. +// Check that this works correctly by testing different "dst" capacities from +// 5000 to 16 and "in" lengths from 1 to 5000. +func TestInplaceSealChacha(t *testing.T) { +	key := randBytes(32) +	sGCM := newChacha20poly1305(key) +	authData := randBytes(24) +	iv := randBytes(sGCM.NonceSize()) + +	gGCM, err := chacha20poly1305.New(key) +	if err != nil { +		t.Fatal(err) +	} +	max := 5016 +	// Check all block sizes from 1 to 5000 +	for i := 1; i < max-len(iv); i++ { +		in := make([]byte, i) +		dst := make([]byte, max-i) +		dst = dst[:len(iv)] + +		sOut := sGCM.Seal(dst, iv, in, authData) +		dst2 := make([]byte, len(iv)) +		gOut := gGCM.Seal(dst2, iv, in, authData) + +		// Ciphertext must be identical to Go GCM +		if !bytes.Equal(sOut, gOut) { +			t.Fatalf("Compare failed for encryption, size %d", i) +			t.Log("sOut:") +			t.Log("\n" + hex.Dump(sOut)) +			t.Log("gOut:") +			t.Log("\n" + hex.Dump(gOut)) +		} +	} +} + +// Open re-uses the "dst" buffer it is large enough. +// Check that this works correctly by testing different "dst" capacities from +// 5000 to 16 and "in" lengths from 1 to 5000. +func TestInplaceOpenChacha(t *testing.T) { +	key := randBytes(32) +	sGCM := newChacha20poly1305(key) +	authData := randBytes(24) +	iv := randBytes(sGCM.NonceSize()) + +	gGCM, err := chacha20poly1305.New(key) +	if err != nil { +		t.Fatal(err) +	} +	max := 5016 +	// Check all block sizes from 1 to 5000 +	for i := 1; i < max-len(iv); i++ { +		in := make([]byte, i) + +		gCiphertext := gGCM.Seal(iv, iv, in, authData) + +		dst := make([]byte, max-i) +		// sPlaintext ... stupidgcm plaintext +		sPlaintext, err := sGCM.Open(dst[:0], iv, gCiphertext[len(iv):], authData) +		if err != nil { +			t.Fatal(err) +		} + +		// Plaintext must be identical to Go GCM +		if !bytes.Equal(in, sPlaintext) { +			t.Fatalf("Compare failed, i=%d", i) +		} +	} +} + +// TestCorruption verifies that changes in the ciphertext result in a decryption +// error +func TestCorruptionChacha(t *testing.T) { +	key := randBytes(32) +	sGCM := newChacha20poly1305(key) +	authData := randBytes(24) +	iv := randBytes(sGCM.NonceSize()) + +	in := make([]byte, 354) +	sOut := sGCM.Seal(nil, iv, in, authData) +	sOut2, sErr := sGCM.Open(nil, iv, sOut, authData) +	if sErr != nil { +		t.Fatal(sErr) +	} +	if !bytes.Equal(in, sOut2) { +		t.Fatalf("Compare failed") +	} + +	// Corrupt first byte +	sOut[0]++ +	sOut2, sErr = sGCM.Open(nil, iv, sOut, authData) +	if sErr == nil || sOut2 != nil { +		t.Fatalf("Should have gotten error") +	} +	sOut[0]-- + +	// Corrupt last byte +	sOut[len(sOut)-1]++ +	sOut2, sErr = sGCM.Open(nil, iv, sOut, authData) +	if sErr == nil || sOut2 != nil { +		t.Fatalf("Should have gotten error") +	} +	sOut[len(sOut)-1]-- + +	// Append one byte +	sOut = append(sOut, 0) +	sOut2, sErr = sGCM.Open(nil, iv, sOut, authData) +	if sErr == nil || sOut2 != nil { +		t.Fatalf("Should have gotten error") +	} +} | 
