Skip to content

Commit 6ce8156

Browse files
authored
feat:Add AES-GCM encryption and decryption utilities with tests (#237)
* Add AES-GCM encryption and decryption utilities Implement AES-GCM encryption and decryption functions with key and nonce validation. * Add AES GCM encryption and decryption tests * Potential fix for pull request finding
1 parent ab78e34 commit 6ce8156

File tree

2 files changed

+147
-0
lines changed

2 files changed

+147
-0
lines changed

x/encodes/secutil/aes_gcm.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package secutil
2+
3+
import (
4+
"crypto/aes"
5+
"crypto/cipher"
6+
"crypto/rand"
7+
"errors"
8+
"io"
9+
)
10+
11+
var (
12+
// ErrInvalidAESKeySize means the AES key length is not 16, 24 or 32 bytes.
13+
ErrInvalidAESKeySize = errors.New("secutil: invalid AES key size")
14+
// ErrInvalidGCMNonceSize means the GCM nonce length does not match AEAD requirements.
15+
ErrInvalidGCMNonceSize = errors.New("secutil: invalid GCM nonce size")
16+
)
17+
18+
// EncryptGCM encrypts plaintext with AES-GCM.
19+
//
20+
// The key length must be 16, 24 or 32 bytes. A random nonce is generated and
21+
// returned alongside the ciphertext.
22+
func EncryptGCM(key, plaintext []byte) (ciphertext, nonce []byte, err error) {
23+
aead, err := newGCM(key)
24+
if err != nil {
25+
return nil, nil, err
26+
}
27+
28+
nonce = make([]byte, aead.NonceSize())
29+
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
30+
return nil, nil, err
31+
}
32+
33+
ciphertext = aead.Seal(nil, nonce, plaintext, nil)
34+
return ciphertext, nonce, nil
35+
}
36+
37+
// DecryptGCM decrypts ciphertext with AES-GCM.
38+
func DecryptGCM(key, nonce, ciphertext []byte) ([]byte, error) {
39+
aead, err := newGCM(key)
40+
if err != nil {
41+
return nil, err
42+
}
43+
if len(nonce) != aead.NonceSize() {
44+
return nil, ErrInvalidGCMNonceSize
45+
}
46+
47+
return aead.Open(nil, nonce, ciphertext, nil)
48+
}
49+
50+
func newGCM(key []byte) (cipher.AEAD, error) {
51+
if err := validateAESKeySize(len(key)); err != nil {
52+
return nil, err
53+
}
54+
55+
block, err := aes.NewCipher(key)
56+
if err != nil {
57+
return nil, err
58+
}
59+
return cipher.NewGCM(block)
60+
}
61+
62+
func validateAESKeySize(n int) error {
63+
switch n {
64+
case 16, 24, 32:
65+
return nil
66+
default:
67+
return ErrInvalidAESKeySize
68+
}
69+
}

x/encodes/secutil/aes_gcm_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package secutil_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/gookit/goutil/testutil/assert"
7+
"github.com/gookit/goutil/x/encodes/secutil"
8+
)
9+
10+
func TestEncryptGCM(t *testing.T) {
11+
t.Run("round trip for valid key sizes", func(t *testing.T) {
12+
cases := []struct {
13+
name string
14+
key []byte
15+
}{
16+
{name: "aes-128", key: []byte("1234567890abcdef")},
17+
{name: "aes-192", key: []byte("1234567890abcdefghijklmn")},
18+
{name: "aes-256", key: []byte("1234567890abcdefghijklmnopqrstuv")},
19+
}
20+
21+
plaintext := []byte("hello from secutil aes gcm")
22+
for _, tc := range cases {
23+
tc := tc
24+
t.Run(tc.name, func(t *testing.T) {
25+
ciphertext, nonce, err := secutil.EncryptGCM(tc.key, plaintext)
26+
assert.NoErr(t, err)
27+
assert.NotEmpty(t, ciphertext)
28+
assert.Eq(t, 12, len(nonce))
29+
30+
got, err := secutil.DecryptGCM(tc.key, nonce, ciphertext)
31+
assert.NoErr(t, err)
32+
assert.Eq(t, string(plaintext), string(got))
33+
})
34+
}
35+
})
36+
37+
t.Run("allow empty plaintext", func(t *testing.T) {
38+
key := []byte("1234567890abcdef")
39+
40+
ciphertext, nonce, err := secutil.EncryptGCM(key, nil)
41+
assert.NoErr(t, err)
42+
assert.NotEmpty(t, ciphertext)
43+
assert.Eq(t, 12, len(nonce))
44+
45+
got, err := secutil.DecryptGCM(key, nonce, ciphertext)
46+
assert.NoErr(t, err)
47+
assert.Empty(t, got)
48+
})
49+
50+
t.Run("reject invalid key size", func(t *testing.T) {
51+
_, _, err := secutil.EncryptGCM([]byte("short-key"), []byte("hello"))
52+
assert.ErrIs(t, err, secutil.ErrInvalidAESKeySize)
53+
})
54+
}
55+
56+
func TestDecryptGCM(t *testing.T) {
57+
key := []byte("1234567890abcdefghijklmnopqrstuv")
58+
plaintext := []byte("hello from secutil aes gcm")
59+
60+
ciphertext, nonce, err := secutil.EncryptGCM(key, plaintext)
61+
assert.NoErr(t, err)
62+
63+
t.Run("reject wrong nonce size", func(t *testing.T) {
64+
_, err := secutil.DecryptGCM(key, nonce[:len(nonce)-1], ciphertext)
65+
assert.ErrIs(t, err, secutil.ErrInvalidGCMNonceSize)
66+
})
67+
68+
t.Run("reject wrong key", func(t *testing.T) {
69+
wrongKey := []byte("abcdef1234567890abcdefghijklmnop")
70+
_, err := secutil.DecryptGCM(wrongKey, nonce, ciphertext)
71+
assert.Err(t, err)
72+
})
73+
74+
t.Run("reject invalid key size", func(t *testing.T) {
75+
_, err := secutil.DecryptGCM([]byte("short-key"), nonce, ciphertext)
76+
assert.ErrIs(t, err, secutil.ErrInvalidAESKeySize)
77+
})
78+
}

0 commit comments

Comments
 (0)