-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #51 from ucan-wg/v1-meta-encryption
feat(meta): values symmetric encryption
- Loading branch information
Showing
8 changed files
with
520 additions
and
2 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,132 @@ | ||
package crypto | ||
|
||
import ( | ||
"crypto/aes" | ||
"crypto/cipher" | ||
"crypto/rand" | ||
"errors" | ||
"fmt" | ||
"io" | ||
) | ||
|
||
// KeySize represents valid AES key sizes | ||
type KeySize int | ||
|
||
const ( | ||
KeySize128 KeySize = 16 // AES-128 | ||
KeySize192 KeySize = 24 // AES-192 | ||
KeySize256 KeySize = 32 // AES-256 (recommended) | ||
) | ||
|
||
// IsValid returns true if the key size is valid for AES | ||
func (ks KeySize) IsValid() bool { | ||
switch ks { | ||
case KeySize128, KeySize192, KeySize256: | ||
return true | ||
default: | ||
return false | ||
} | ||
} | ||
|
||
var ErrShortCipherText = errors.New("ciphertext too short") | ||
var ErrNoEncryptionKey = errors.New("encryption key is required") | ||
var ErrInvalidKeySize = errors.New("invalid key size: must be 16, 24, or 32 bytes") | ||
var ErrZeroKey = errors.New("encryption key cannot be all zeros") | ||
|
||
// GenerateKey generates a random AES key of default size KeySize256 (32 bytes). | ||
// Returns an error if the specified size is invalid or if key generation fails. | ||
func GenerateKey() ([]byte, error) { | ||
return GenerateKeyWithSize(KeySize256) | ||
} | ||
|
||
// GenerateKeyWithSize generates a random AES key of the specified size. | ||
// Returns an error if the specified size is invalid or if key generation fails. | ||
func GenerateKeyWithSize(size KeySize) ([]byte, error) { | ||
if !size.IsValid() { | ||
return nil, ErrInvalidKeySize | ||
} | ||
|
||
key := make([]byte, size) | ||
if _, err := io.ReadFull(rand.Reader, key); err != nil { | ||
return nil, fmt.Errorf("failed to generate AES key: %w", err) | ||
} | ||
|
||
return key, nil | ||
} | ||
|
||
// EncryptWithAESKey encrypts data using AES-GCM with the provided key. | ||
// The key must be 16, 24, or 32 bytes long (for AES-128, AES-192, or AES-256). | ||
// Returns the encrypted data with the nonce prepended, or an error if encryption fails. | ||
func EncryptWithAESKey(data, key []byte) ([]byte, error) { | ||
if err := validateAESKey(key); err != nil { | ||
return nil, err | ||
} | ||
|
||
block, err := aes.NewCipher(key) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
gcm, err := cipher.NewGCM(block) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
nonce := make([]byte, gcm.NonceSize()) | ||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil { | ||
return nil, err | ||
} | ||
|
||
return gcm.Seal(nonce, nonce, data, nil), nil | ||
} | ||
|
||
// DecryptStringWithAESKey decrypts data that was encrypted with EncryptWithAESKey. | ||
// The key must match the one used for encryption. | ||
// Expects the input to have a prepended nonce. | ||
// Returns the decrypted data or an error if decryption fails. | ||
func DecryptStringWithAESKey(data, key []byte) ([]byte, error) { | ||
if err := validateAESKey(key); err != nil { | ||
return nil, err | ||
} | ||
|
||
block, err := aes.NewCipher(key) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
gcm, err := cipher.NewGCM(block) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if len(data) < gcm.NonceSize() { | ||
return nil, ErrShortCipherText | ||
} | ||
|
||
nonce, ciphertext := data[:gcm.NonceSize()], data[gcm.NonceSize():] | ||
decrypted, err := gcm.Open(nil, nonce, ciphertext, nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return decrypted, nil | ||
} | ||
|
||
func validateAESKey(key []byte) error { | ||
if key == nil { | ||
return ErrNoEncryptionKey | ||
} | ||
|
||
if !KeySize(len(key)).IsValid() { | ||
return ErrInvalidKeySize | ||
} | ||
|
||
// check if key is all zeros | ||
for _, b := range key { | ||
if b != 0 { | ||
return nil | ||
} | ||
} | ||
|
||
return ErrZeroKey | ||
} |
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,124 @@ | ||
package crypto | ||
|
||
import ( | ||
"bytes" | ||
"crypto/rand" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestAESEncryption(t *testing.T) { | ||
t.Parallel() | ||
|
||
key := make([]byte, 32) // generated random 32-byte key | ||
_, errKey := rand.Read(key) | ||
require.NoError(t, errKey) | ||
|
||
tests := []struct { | ||
name string | ||
data []byte | ||
key []byte | ||
wantErr error | ||
}{ | ||
{ | ||
name: "valid encryption/decryption", | ||
data: []byte("hello world"), | ||
key: key, | ||
}, | ||
{ | ||
name: "nil key returns error", | ||
data: []byte("hello world"), | ||
key: nil, | ||
wantErr: ErrNoEncryptionKey, | ||
}, | ||
{ | ||
name: "empty data", | ||
data: []byte{}, | ||
key: key, | ||
}, | ||
{ | ||
name: "invalid key size", | ||
data: []byte("hello world"), | ||
key: make([]byte, 31), | ||
wantErr: ErrInvalidKeySize, | ||
}, | ||
{ | ||
name: "zero key returns error", | ||
data: []byte("hello world"), | ||
key: make([]byte, 32), | ||
wantErr: ErrZeroKey, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
tt := tt | ||
t.Run(tt.name, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
encrypted, err := EncryptWithAESKey(tt.data, tt.key) | ||
if tt.wantErr != nil { | ||
require.ErrorIs(t, err, tt.wantErr) | ||
|
||
return | ||
} | ||
require.NoError(t, err) | ||
|
||
decrypted, err := DecryptStringWithAESKey(encrypted, tt.key) | ||
require.NoError(t, err) | ||
|
||
if tt.key == nil { | ||
require.Equal(t, tt.data, encrypted) | ||
require.Equal(t, tt.data, decrypted) | ||
} else { | ||
require.NotEqual(t, tt.data, encrypted) | ||
require.True(t, bytes.Equal(tt.data, decrypted)) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestDecryptionErrors(t *testing.T) { | ||
t.Parallel() | ||
|
||
key := make([]byte, 32) | ||
_, err := rand.Read(key) | ||
require.NoError(t, err) | ||
|
||
tests := []struct { | ||
name string | ||
data []byte | ||
key []byte | ||
errMsg string | ||
}{ | ||
{ | ||
name: "short ciphertext", | ||
data: []byte("short"), | ||
key: key, | ||
errMsg: "ciphertext too short", | ||
}, | ||
{ | ||
name: "invalid ciphertext", | ||
data: make([]byte, 16), // just nonce size | ||
key: key, | ||
errMsg: "message authentication failed", | ||
}, | ||
{ | ||
name: "missing key", | ||
data: []byte("�`M���l\u001AIF�\u0012���=h�?�c� ��\u0012����\u001C�\u0018Ƽ(g"), | ||
key: nil, | ||
errMsg: "encryption key is required", | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
tt := tt | ||
t.Run(tt.name, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
_, err := DecryptStringWithAESKey(tt.data, tt.key) | ||
require.Error(t, err) | ||
require.Contains(t, err.Error(), tt.errMsg) | ||
}) | ||
} | ||
} |
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
Oops, something went wrong.