-
Notifications
You must be signed in to change notification settings - Fork 258
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
stupidgcm: add our own thin wrapper around openssl gcm
...complete with tests and benchmark. This will allow us to get rid of the dependency to spacemonkeygo/openssl that causes problems on Arch Linux ( #21 )
- Loading branch information
Showing
2 changed files
with
338 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
// 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 <openssl/evp.h> | ||
// #cgo pkg-config: libcrypto | ||
import "C" | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"unsafe" | ||
) | ||
|
||
const ( | ||
keyLen = 32 | ||
ivLen = 16 | ||
tagLen = 16 | ||
) | ||
|
||
// stupidGCM implements the cipher.AEAD interface | ||
type stupidGCM struct { | ||
key []byte | ||
} | ||
|
||
func New(key []byte) stupidGCM { | ||
if len(key) != keyLen { | ||
log.Panicf("Only %d-byte keys are supported", keyLen) | ||
} | ||
return stupidGCM{key: key} | ||
} | ||
|
||
func (g stupidGCM) NonceSize() int { | ||
return ivLen | ||
} | ||
|
||
func (g stupidGCM) Overhead() int { | ||
return tagLen | ||
} | ||
|
||
// Seal - encrypt "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") | ||
} | ||
buf := make([]byte, 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_EncryptInit_ex(ctx, C.EVP_aes_256_gcm(), nil, nil, nil) != 1 { | ||
log.Panic("EVP_EncryptInit_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_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 GMAC tag and append it to the ciphertext in "buf" | ||
if C.EVP_CIPHER_CTX_ctrl(ctx, C.EVP_CTRL_GCM_GET_TAG, tagLen, (unsafe.Pointer)(&buf[len(in)])) != 1 { | ||
log.Panic("EVP_CIPHER_CTX_ctrl EVP_CTRL_GCM_GET_TAG failed") | ||
} | ||
|
||
// Free scratch space | ||
C.EVP_CIPHER_CTX_free(ctx) | ||
|
||
return append(dst, buf...) | ||
} | ||
|
||
// Open - decrypt "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(in) <= tagLen { | ||
log.Panic("Input data too short") | ||
} | ||
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") | ||
} | ||
|
||
// 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_EncryptUpdate 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) | ||
} | ||
|
||
// 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") | ||
} | ||
|
||
// 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 { | ||
return nil, fmt.Errorf("stupidgcm: message authentication failed") | ||
} | ||
|
||
return append(dst, buf...), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
package stupidgcm | ||
|
||
import ( | ||
"bytes" | ||
"crypto/aes" | ||
"crypto/cipher" | ||
"crypto/rand" | ||
"encoding/hex" | ||
"testing" | ||
) | ||
|
||
// Get "n" random bytes from /dev/urandom or panic | ||
func randBytes(n int) []byte { | ||
b := make([]byte, n) | ||
_, err := rand.Read(b) | ||
if err != nil { | ||
panic("Failed to read random bytes: " + err.Error()) | ||
} | ||
return b | ||
} | ||
|
||
// TestEncryptDecrypt encrypts and decrypts using both stupidgcm and Go's built-in | ||
// GCM implemenatation and verifies that the results are identical. | ||
func TestEncryptDecrypt(t *testing.T) { | ||
key := randBytes(32) | ||
sGCM := New(key) | ||
authData := randBytes(24) | ||
iv := randBytes(16) | ||
dst := make([]byte, 71) // 71 = random length | ||
|
||
gAES, err := aes.NewCipher(key) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
gGCM, err := cipher.NewGCMWithNonceSize(gAES, 16) | ||
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.Compare(sOut, gOut) != 0 { | ||
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.Compare(sOut2, gOut2) != 0 { | ||
t.Fatalf("Compare failed for decryption, size %d", i) | ||
} | ||
} | ||
} | ||
|
||
// TestCorruption verifies that changes in the ciphertext result in a decryption | ||
// error | ||
func TestCorruption(t *testing.T) { | ||
key := randBytes(32) | ||
sGCM := New(key) | ||
authData := randBytes(24) | ||
iv := randBytes(16) | ||
|
||
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.Compare(in, sOut2) != 0 { | ||
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") | ||
} | ||
} | ||
|
||
// $ go test -bench . | ||
// PASS | ||
// Benchmark4kEncStupidGCM-2 50000 25622 ns/op 159.86 MB/s | ||
// Benchmark4kEncGoGCM-2 10000 116544 ns/op 35.15 MB/s | ||
// ok github.com/rfjakob/gocryptfs/internal/stupidgcm 3.775s | ||
func Benchmark4kEncStupidGCM(b *testing.B) { | ||
key := randBytes(32) | ||
authData := randBytes(24) | ||
iv := randBytes(16) | ||
in := make([]byte, 4096) | ||
b.SetBytes(int64(len(in))) | ||
|
||
sGCM := New(key) | ||
|
||
for i := 0; i < b.N; i++ { | ||
// Encrypt and append to nonce | ||
sGCM.Seal(iv, iv, in, authData) | ||
} | ||
} | ||
|
||
func Benchmark4kEncGoGCM(b *testing.B) { | ||
key := randBytes(32) | ||
authData := randBytes(24) | ||
iv := randBytes(16) | ||
in := make([]byte, 4096) | ||
b.SetBytes(int64(len(in))) | ||
|
||
gAES, err := aes.NewCipher(key) | ||
if err != nil { | ||
b.Fatal(err) | ||
} | ||
gGCM, err := cipher.NewGCMWithNonceSize(gAES, 16) | ||
if err != nil { | ||
b.Fatal(err) | ||
} | ||
|
||
for i := 0; i < b.N; i++ { | ||
// Encrypt and append to nonce | ||
gGCM.Seal(iv, iv, in, authData) | ||
} | ||
} |