Skip to content

Commit

Permalink
stupidgcm: add our own thin wrapper around openssl gcm
Browse files Browse the repository at this point in the history
...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
rfjakob committed May 4, 2016
1 parent 1bb907b commit c92190b
Show file tree
Hide file tree
Showing 2 changed files with 338 additions and 0 deletions.
184 changes: 184 additions & 0 deletions internal/stupidgcm/stupidgcm.go
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
}
154 changes: 154 additions & 0 deletions internal/stupidgcm/stupidgcm_test.go
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)
}
}

0 comments on commit c92190b

Please sign in to comment.