From 3987e8649cf14599954e87d10ba7b820d861639c Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Tue, 12 Nov 2024 15:29:48 +0100 Subject: [PATCH] refactor meta/internal/crypto and add key generation method --- pkg/crypto/aes.go | 63 ----------- pkg/meta/internal/crypto/aes.go | 123 +++++++++++++++++++++ pkg/{ => meta/internal}/crypto/aes_test.go | 0 pkg/meta/meta.go | 3 +- pkg/meta/readonly.go | 8 ++ token/invocation/options.go | 9 ++ 6 files changed, 142 insertions(+), 64 deletions(-) delete mode 100644 pkg/crypto/aes.go create mode 100644 pkg/meta/internal/crypto/aes.go rename pkg/{ => meta/internal}/crypto/aes_test.go (100%) diff --git a/pkg/crypto/aes.go b/pkg/crypto/aes.go deleted file mode 100644 index aa4b152..0000000 --- a/pkg/crypto/aes.go +++ /dev/null @@ -1,63 +0,0 @@ -package crypto - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "errors" - "io" -) - -var ErrShortCipherText = errors.New("ciphertext too short") -var ErrNoEncryptionKey = errors.New("encryption key is required") - -func EncryptWithAESKey(data, key []byte) ([]byte, error) { - if key == nil { - return data, ErrNoEncryptionKey - } - - 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 -} - -func DecryptStringWithAESKey(data, key []byte) ([]byte, error) { - if key == nil { - return data, ErrNoEncryptionKey - } - - 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 -} diff --git a/pkg/meta/internal/crypto/aes.go b/pkg/meta/internal/crypto/aes.go new file mode 100644 index 0000000..28f0ae4 --- /dev/null +++ b/pkg/meta/internal/crypto/aes.go @@ -0,0 +1,123 @@ +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") + +// NewKey generates a random AES key of the specified size. +// If no size is provided, it defaults to KeySize256 (32 bytes). +// Returns an error if the specified size is invalid or if key generation fails. +func NewKey(size ...KeySize) ([]byte, error) { + keySize := KeySize256 + if len(size) > 0 { + keySize = size[0] + if !keySize.IsValid() { + return nil, ErrInvalidKeySize + } + } + + key := make([]byte, keySize) + 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 + } + + return nil +} diff --git a/pkg/crypto/aes_test.go b/pkg/meta/internal/crypto/aes_test.go similarity index 100% rename from pkg/crypto/aes_test.go rename to pkg/meta/internal/crypto/aes_test.go diff --git a/pkg/meta/meta.go b/pkg/meta/meta.go index 1e6383b..13b456c 100644 --- a/pkg/meta/meta.go +++ b/pkg/meta/meta.go @@ -1,6 +1,7 @@ package meta import ( + "errors" "fmt" "reflect" "strings" @@ -10,7 +11,7 @@ import ( "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipld/go-ipld-prime/printer" - "github.com/ucan-wg/go-ucan/pkg/crypto" + "github.com/ucan-wg/go-ucan/pkg/meta/internal/crypto" ) var ErrUnsupported = errors.New("failure adding unsupported type to meta") diff --git a/pkg/meta/readonly.go b/pkg/meta/readonly.go index 1c8188d..ce3674e 100644 --- a/pkg/meta/readonly.go +++ b/pkg/meta/readonly.go @@ -17,6 +17,10 @@ func (r ReadOnly) GetString(key string) (string, error) { return r.m.GetString(key) } +func (r ReadOnly) GetEncryptedString(key string, encryptionKey []byte) (string, error) { + return r.m.GetEncryptedString(key, encryptionKey) +} + func (r ReadOnly) GetInt64(key string) (int64, error) { return r.m.GetInt64(key) } @@ -29,6 +33,10 @@ func (r ReadOnly) GetBytes(key string) ([]byte, error) { return r.m.GetBytes(key) } +func (r ReadOnly) GetEncryptedBytes(key string, encryptionKey []byte) ([]byte, error) { + return r.m.GetEncryptedBytes(key, encryptionKey) +} + func (r ReadOnly) GetNode(key string) (ipld.Node, error) { return r.m.GetNode(key) } diff --git a/token/invocation/options.go b/token/invocation/options.go index 9322cd7..e2246dc 100644 --- a/token/invocation/options.go +++ b/token/invocation/options.go @@ -44,6 +44,15 @@ func WithMeta(key string, val any) Option { } } +// WithEncryptedMeta adds a key/value pair in the "meta" field. +// The value is encrypted with the given aesKey. +// Accepted types for the value are: string, []byte. +func WithEncryptedMeta(key string, val any, encryptionKey []byte) Option { + return func(t *Token) error { + return t.meta.AddEncrypted(key, val, encryptionKey) + } +} + // WithNonce sets the Token's nonce with the given value. // // If this option is not used, a random 12-byte nonce is generated for