Skip to content

Commit a55988a

Browse files
committed
aesgcm: AES-256-GCM authenticated encryption and decryption
1 parent 91cb625 commit a55988a

2 files changed

Lines changed: 140 additions & 0 deletions

File tree

aesgcm/aesgcm.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Package aesgcm contains utility functions to Encrypt and Decrypt data using AES-256-GCM cipher.
2+
package aesgcm
3+
4+
import (
5+
"crypto/aes"
6+
"crypto/cipher"
7+
"fmt"
8+
"io"
9+
)
10+
11+
// Encrypt encrypts plaintext using key and random entropy. Key must be a valid AES-256 key with a length of 32 bytes.
12+
// The result is a concatenation of nonce (using standard 12-byte nonce size) and the actual ciphertext.
13+
func Encrypt(random io.Reader, key []byte, plaintext []byte, additionalData []byte) ([]byte, error) {
14+
if len(key) != 32 {
15+
return nil, fmt.Errorf("key must be 32 bytes for AES-256 but was %d", len(key))
16+
}
17+
18+
block, err := aes.NewCipher(key)
19+
if err != nil {
20+
return nil, fmt.Errorf("create cipher block: %w", err)
21+
}
22+
23+
aead, err := cipher.NewGCM(block)
24+
if err != nil {
25+
return nil, fmt.Errorf("create GCM: %w", err)
26+
}
27+
28+
nonce := make([]byte, aead.NonceSize())
29+
if _, err := io.ReadFull(random, nonce); err != nil {
30+
return nil, fmt.Errorf("generate random nonce: %w", err)
31+
}
32+
33+
ciphertext := aead.Seal(nil, nonce, plaintext, additionalData)
34+
return append(nonce, ciphertext...), nil
35+
}
36+
37+
// Decrypt decrypts ciphertext using the given key. Key must be a valid AES-256 key with a length of 32 bytes.
38+
// Ciphertext is assumed to be a concatenation of nonce (using standard 12-byte nonce size) and the actual
39+
// ciphertext. As such, it must be at least 12 bytes long.
40+
func Decrypt(key []byte, ciphertext []byte, additionalData []byte) ([]byte, error) {
41+
if len(key) != 32 {
42+
return nil, fmt.Errorf("key must be 32 bytes for AES-256 but was %d", len(key))
43+
}
44+
45+
block, err := aes.NewCipher(key)
46+
if err != nil {
47+
return nil, fmt.Errorf("create cipher block: %w", err)
48+
}
49+
50+
aead, err := cipher.NewGCM(block)
51+
if err != nil {
52+
return nil, fmt.Errorf("create GCM: %w", err)
53+
}
54+
55+
if len(ciphertext) < aead.NonceSize() {
56+
return nil, fmt.Errorf("ciphertext must be at least %d bytes but was %d", aead.NonceSize(), len(ciphertext))
57+
}
58+
59+
nonce := ciphertext[:aead.NonceSize()]
60+
ciphertext = ciphertext[aead.NonceSize():]
61+
plaintext := make([]byte, 0, len(ciphertext))
62+
63+
plaintext, err = aead.Open(plaintext, nonce, ciphertext, additionalData)
64+
if err != nil {
65+
return nil, fmt.Errorf("decrypt: %w", err)
66+
}
67+
return plaintext, nil
68+
}

aesgcm/aesgcm_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package aesgcm_test
2+
3+
import (
4+
"bytes"
5+
"crypto/rand"
6+
"encoding/base64"
7+
"encoding/binary"
8+
"testing"
9+
10+
"github.com/0xsequence/nitrocontrol/aesgcm"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestEncrypt(t *testing.T) {
16+
t.Run("no additional data", func(t *testing.T) {
17+
random := bytes.NewBuffer([]byte("123456789012")) // 12 bytes
18+
key := []byte("12345678901234567890123456789012") // 32 bytes
19+
plaintext := []byte("Hello world")
20+
enc, err := aesgcm.Encrypt(random, key[:], plaintext, nil)
21+
require.NoError(t, err)
22+
assert.Equal(t, "MTIzNDU2Nzg5MDEyyJeRgm1hto6XflsTaSRZcpV9masIpIeV/7/d", base64.StdEncoding.EncodeToString(enc))
23+
})
24+
25+
t.Run("with additional data", func(t *testing.T) {
26+
random := bytes.NewBuffer([]byte("123456789012")) // 12 bytes
27+
key := []byte("12345678901234567890123456789012") // 32 bytes
28+
plaintext := []byte("Hello world")
29+
additionalData := []byte("additional data")
30+
enc, err := aesgcm.Encrypt(random, key[:], plaintext, additionalData)
31+
require.NoError(t, err)
32+
assert.Equal(t, "MTIzNDU2Nzg5MDEyyJeRgm1hto6XfluPj9+SAYdHVgK9WuQz9M6U", base64.StdEncoding.EncodeToString(enc))
33+
})
34+
}
35+
36+
func TestDecrypt(t *testing.T) {
37+
t.Run("no additional data", func(t *testing.T) {
38+
key := []byte("12345678901234567890123456789012") // 32 bytes
39+
ciphertext, _ := base64.StdEncoding.DecodeString("MTIzNDU2Nzg5MDEyyJeRgm1hto6XflsTaSRZcpV9masIpIeV/7/d")
40+
dec, err := aesgcm.Decrypt(key, ciphertext, nil)
41+
require.NoError(t, err)
42+
assert.Equal(t, []byte("Hello world"), dec)
43+
})
44+
45+
t.Run("with additional data", func(t *testing.T) {
46+
key := []byte("12345678901234567890123456789012") // 32 bytes
47+
ciphertext, _ := base64.StdEncoding.DecodeString("MTIzNDU2Nzg5MDEyyJeRgm1hto6XfluPj9+SAYdHVgK9WuQz9M6U")
48+
additionalData := []byte("additional data")
49+
dec, err := aesgcm.Decrypt(key, ciphertext, additionalData)
50+
require.NoError(t, err)
51+
assert.Equal(t, []byte("Hello world"), dec)
52+
})
53+
}
54+
55+
func FuzzEncryptAndDecrypt(f *testing.F) {
56+
f.Add(uint64(0), uint64(0), uint64(0), uint64(0), []byte("hello"), []byte("aad"))
57+
f.Fuzz(func(t *testing.T, u0, u1, u2, u3 uint64, plaintext []byte, additionalData []byte) {
58+
key := make([]byte, 32)
59+
binary.LittleEndian.PutUint64(key, u0)
60+
binary.LittleEndian.PutUint64(key[8:], u1)
61+
binary.LittleEndian.PutUint64(key[16:], u2)
62+
binary.LittleEndian.PutUint64(key[24:], u3)
63+
64+
enc, err := aesgcm.Encrypt(rand.Reader, key[:], plaintext, additionalData)
65+
require.NoError(t, err)
66+
require.Greater(t, len(enc), 16)
67+
68+
dec, err := aesgcm.Decrypt(key, enc, additionalData)
69+
require.NoError(t, err)
70+
require.Equal(t, plaintext, dec)
71+
})
72+
}

0 commit comments

Comments
 (0)