From 76c015e78b9086a56e3c2927c7f791fdc4afba15 Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Thu, 31 Oct 2024 18:24:54 +0100 Subject: [PATCH 1/8] feat(meta): values symmetric encryption --- pkg/crypto/aes.go | 62 +++++++++++++++ pkg/crypto/aes_test.go | 113 ++++++++++++++++++++++++++++ pkg/meta/meta.go | 59 ++++++++++++++- pkg/meta/meta_test.go | 61 ++++++++++++++- token/delegation/delegation_test.go | 109 +++++++++++++++++++++++++++ token/delegation/options.go | 9 +++ 6 files changed, 411 insertions(+), 2 deletions(-) create mode 100644 pkg/crypto/aes.go create mode 100644 pkg/crypto/aes_test.go diff --git a/pkg/crypto/aes.go b/pkg/crypto/aes.go new file mode 100644 index 0000000..cfe27f4 --- /dev/null +++ b/pkg/crypto/aes.go @@ -0,0 +1,62 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "errors" + "io" +) + +var ErrShortCipherText = errors.New("ciphertext too short") + +func EncryptWithAESKey(data, key []byte) ([]byte, error) { + if key == nil { + return data, nil + } + + 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, nil + } + + 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/crypto/aes_test.go b/pkg/crypto/aes_test.go new file mode 100644 index 0000000..b532fc1 --- /dev/null +++ b/pkg/crypto/aes_test.go @@ -0,0 +1,113 @@ +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 + _, err := rand.Read(key) + require.NoError(t, err) + + tests := []struct { + name string + data []byte + key []byte + wantErr bool + }{ + { + name: "valid encryption/decryption", + data: []byte("hello world"), + key: key, + wantErr: false, + }, + { + name: "nil key returns original data", + data: []byte("hello world"), + key: nil, + wantErr: false, + }, + { + name: "empty data", + data: []byte{}, + key: key, + wantErr: false, + }, + { + name: "invalid key size", + data: []byte("hello world"), + key: make([]byte, 31), + wantErr: true, + }, + } + + 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 { + require.Error(t, err) + 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", + }, + } + + 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) + }) + } +} diff --git a/pkg/meta/meta.go b/pkg/meta/meta.go index a51eec7..01baf11 100644 --- a/pkg/meta/meta.go +++ b/pkg/meta/meta.go @@ -10,11 +10,13 @@ import ( "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipld/go-ipld-prime/printer" + + "github.com/ucan-wg/go-ucan/pkg/crypto" ) var ErrUnsupported = errors.New("failure adding unsupported type to meta") - var ErrNotFound = errors.New("key-value not found in meta") +var ErrNotEncryptable = errors.New("value of this type cannot be encrypted") // Meta is a container for meta key-value pairs in a UCAN token. // This also serves as a way to construct the underlying IPLD data with minimum allocations and transformations, @@ -51,6 +53,21 @@ func (m *Meta) GetString(key string) (string, error) { return v.AsString() } +// GetEncryptedString decorates GetString and decrypt its output with the given symmetric encryption key. +func (m *Meta) GetEncryptedString(key string, encryptionKey []byte) (string, error) { + v, err := m.GetString(key) + if err != nil { + return "", err + } + + decrypted, err := crypto.DecryptStringWithAESKey([]byte(v), encryptionKey) + if err != nil { + return "", err + } + + return string(decrypted), nil +} + // GetInt64 retrieves a value as an int64. // Returns ErrNotFound if the given key is missing. // Returns datamodel.ErrWrongKind if the value has the wrong type. @@ -84,6 +101,21 @@ func (m *Meta) GetBytes(key string) ([]byte, error) { return v.AsBytes() } +// GetEncryptedBytes decorates GetBytes and decrypt its output with the given symmetric encryption key. +func (m *Meta) GetEncryptedBytes(key string, encryptionKey []byte) ([]byte, error) { + v, err := m.GetBytes(key) + if err != nil { + return nil, err + } + + decrypted, err := crypto.DecryptStringWithAESKey(v, encryptionKey) + if err != nil { + return nil, err + } + + return decrypted, nil +} + // GetNode retrieves a value as a raw IPLD node. // Returns ErrNotFound if the given key is missing. // Returns datamodel.ErrWrongKind if the value has the wrong type. @@ -125,6 +157,31 @@ func (m *Meta) Add(key string, val any) error { return nil } +// AddEncrypted adds a key/value pair in the meta set. +// The value is encrypted with the given encryptionKey. +// Accepted types for the value are: string, []byte. +func (m *Meta) AddEncrypted(key string, val any, encryptionKey []byte) error { + var encrypted []byte + var err error + + switch val := val.(type) { + case string: + encrypted, err = crypto.EncryptWithAESKey([]byte(val), encryptionKey) + if err != nil { + return err + } + return m.Add(key, string(encrypted)) + case []byte: + encrypted, err = crypto.EncryptWithAESKey(val, encryptionKey) + if err != nil { + return err + } + return m.Add(key, encrypted) + default: + return ErrNotEncryptable + } +} + // Equals tells if two Meta hold the same key/values. func (m *Meta) Equals(other *Meta) bool { if len(m.Keys) != len(other.Keys) { diff --git a/pkg/meta/meta_test.go b/pkg/meta/meta_test.go index 9ad3e2c..367f54e 100644 --- a/pkg/meta/meta_test.go +++ b/pkg/meta/meta_test.go @@ -1,6 +1,7 @@ package meta_test import ( + "crypto/rand" "testing" "github.com/stretchr/testify/require" @@ -14,11 +15,69 @@ func TestMeta_Add(t *testing.T) { type Unsupported struct{} - t.Run("error if not primative or Node", func(t *testing.T) { + t.Run("error if not primitive or Node", func(t *testing.T) { t.Parallel() err := (&meta.Meta{}).Add("invalid", &Unsupported{}) require.ErrorIs(t, err, meta.ErrUnsupported) assert.ErrorContains(t, err, "*github.com/ucan-wg/go-ucan/pkg/meta_test.Unsupported") }) + + t.Run("encrypted meta", func(t *testing.T) { + t.Parallel() + + key := make([]byte, 32) + _, err := rand.Read(key) + require.NoError(t, err) + + m := meta.NewMeta() + + // string encryption + err = m.AddEncrypted("secret", "hello world", key) + require.NoError(t, err) + + encrypted, err := m.GetString("secret") + require.NoError(t, err) + require.NotEqual(t, "hello world", encrypted) + + decrypted, err := m.GetEncryptedString("secret", key) + require.NoError(t, err) + require.Equal(t, "hello world", decrypted) + + // bytes encryption + originalBytes := make([]byte, 128) + _, err = rand.Read(originalBytes) + require.NoError(t, err) + err = m.AddEncrypted("secret-bytes", originalBytes, key) + require.NoError(t, err) + + encryptedBytes, err := m.GetBytes("secret-bytes") + require.NoError(t, err) + require.NotEqual(t, originalBytes, encryptedBytes) + + decryptedBytes, err := m.GetEncryptedBytes("secret-bytes", key) + require.NoError(t, err) + require.Equal(t, originalBytes, decryptedBytes) + + // error cases + t.Run("error on unsupported type", func(t *testing.T) { + err := m.AddEncrypted("invalid", 123, key) + require.ErrorIs(t, err, meta.ErrNotEncryptable) + }) + + t.Run("error on invalid key size", func(t *testing.T) { + err := m.AddEncrypted("invalid", "test", []byte("short-key")) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid key size") + }) + + t.Run("error on nil key", func(t *testing.T) { + err := m.AddEncrypted("invalid", "test", nil) + require.NoError(t, err) + // with nil key, value should be stored unencrypted + val, err := m.GetString("invalid") + require.NoError(t, err) + require.Equal(t, "test", val) + }) + }) } diff --git a/token/delegation/delegation_test.go b/token/delegation/delegation_test.go index 3c662fc..113ea52 100644 --- a/token/delegation/delegation_test.go +++ b/token/delegation/delegation_test.go @@ -1,6 +1,7 @@ package delegation_test import ( + "encoding/base64" "testing" "time" @@ -66,6 +67,8 @@ const ( newCID = "zdpuAn9JgGPvnt2WCmTaKktZdbuvcVGTg9bUT5kQaufwUtZ6e" rootCID = "zdpuAkgGmUp5JrXvehGuuw9JA8DLQKDaxtK3R8brDQQVC2i5X" + + aesKey = "xQklMmNTnVrmaPBq/0pwV5fEwuv/iClF5HWak9MsgI8=" ) func TestConstructors(t *testing.T) { @@ -125,6 +128,112 @@ func TestConstructors(t *testing.T) { }) } +func TestEncryptedMeta(t *testing.T) { + t.Parallel() + + privKey := privKey(t, issuerPrivKeyCfg) + aud, err := did.Parse(AudienceDID) + require.NoError(t, err) + cmd, err := command.Parse(subJectCmd) + require.NoError(t, err) + pol, err := policy.FromDagJson(subjectPol) + require.NoError(t, err) + + encryptionKey, err := base64.StdEncoding.DecodeString(aesKey) + require.NoError(t, err) + require.Len(t, encryptionKey, 32) + + tests := []struct { + name string + key string + value string + expectError bool + }{ + { + name: "simple string", + key: "secret1", + value: "hello world", + }, + { + name: "empty string", + key: "secret2", + value: "", + }, + { + name: "special characters", + key: "secret3", + value: "!@#$%^&*()_+-=[]{}|;:,.<>?", + }, + { + name: "unicode characters", + key: "secret4", + value: "Hello, 世界! 👋", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + tkn, err := delegation.New(privKey, aud, cmd, pol, + delegation.WithEncryptedMeta(tt.key, tt.value, encryptionKey), + ) + require.NoError(t, err) + + data, err := tkn.ToDagCbor(privKey) + require.NoError(t, err) + + decodedTkn, _, err := delegation.FromSealed(data) + require.NoError(t, err) + + encrypted, err := decodedTkn.Meta().GetString(tt.key) + require.NoError(t, err) + // Verify the encrypted value is different from original + require.NotEqual(t, tt.value, encrypted) + + decrypted, err := decodedTkn.Meta().GetEncryptedString(tt.key, encryptionKey) + require.NoError(t, err) + // Verify the decrypted value is equal to the original + require.Equal(t, tt.value, decrypted) + + // Try to decrypt with wrong key + wrongKey := make([]byte, 32) + _, err = decodedTkn.Meta().GetEncryptedString(tt.key, wrongKey) + require.Error(t, err) + }) + } + + t.Run("multiple encrypted values in the same token", func(t *testing.T) { + values := map[string]string{ + "secret1": "value1", + "secret2": "value2", + "secret3": "value3", + } + + // Create token with multiple encrypted values + tkn, err := delegation.New(privKey, aud, cmd, pol, delegation.WithMeta("foo", "bar")) + require.NoError(t, err) + + for k, v := range values { + err := tkn.Meta().AddEncrypted(k, v, encryptionKey) + require.NoError(t, err) + } + + data, err := tkn.ToDagCbor(privKey) + require.NoError(t, err) + + decodedTkn, _, err := delegation.FromSealed(data) + require.NoError(t, err) + + for k, v := range values { + decrypted, err := decodedTkn.Meta().GetEncryptedString(k, encryptionKey) + require.NoError(t, err) + require.Equal(t, v, decrypted) + } + }) +} + func privKey(t require.TestingT, privKeyCfg string) crypto.PrivKey { privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg) require.NoError(t, err) diff --git a/token/delegation/options.go b/token/delegation/options.go index 3c0b5db..ee2eac2 100644 --- a/token/delegation/options.go +++ b/token/delegation/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) + } +} + // WithNotBefore set's the Token's optional "notBefore" field to the value // of the provided time.Time. func WithNotBefore(nbf time.Time) Option { From d21c17c4ca532bb2a82f4d353b1fa62b500d15fc Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Mon, 4 Nov 2024 19:11:25 +0100 Subject: [PATCH 2/8] address pr remarks --- pkg/crypto/aes.go | 5 +++-- pkg/crypto/aes_test.go | 13 +++++++++++-- token/delegation/delegation_test.go | 11 +++++------ 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/pkg/crypto/aes.go b/pkg/crypto/aes.go index cfe27f4..aa4b152 100644 --- a/pkg/crypto/aes.go +++ b/pkg/crypto/aes.go @@ -9,10 +9,11 @@ import ( ) 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, nil + return data, ErrNoEncryptionKey } block, err := aes.NewCipher(key) @@ -35,7 +36,7 @@ func EncryptWithAESKey(data, key []byte) ([]byte, error) { func DecryptStringWithAESKey(data, key []byte) ([]byte, error) { if key == nil { - return data, nil + return data, ErrNoEncryptionKey } block, err := aes.NewCipher(key) diff --git a/pkg/crypto/aes_test.go b/pkg/crypto/aes_test.go index b532fc1..3462a10 100644 --- a/pkg/crypto/aes_test.go +++ b/pkg/crypto/aes_test.go @@ -3,6 +3,7 @@ package crypto import ( "bytes" "crypto/rand" + "fmt" "testing" "github.com/stretchr/testify/require" @@ -28,10 +29,10 @@ func TestAESEncryption(t *testing.T) { wantErr: false, }, { - name: "nil key returns original data", + name: "nil key returns error", data: []byte("hello world"), key: nil, - wantErr: false, + wantErr: true, }, { name: "empty data", @@ -59,6 +60,8 @@ func TestAESEncryption(t *testing.T) { } require.NoError(t, err) + fmt.Println(string(encrypted)) + decrypted, err := DecryptStringWithAESKey(encrypted, tt.key) require.NoError(t, err) @@ -98,6 +101,12 @@ func TestDecryptionErrors(t *testing.T) { 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 { diff --git a/token/delegation/delegation_test.go b/token/delegation/delegation_test.go index 113ea52..53d7a8f 100644 --- a/token/delegation/delegation_test.go +++ b/token/delegation/delegation_test.go @@ -210,16 +210,15 @@ func TestEncryptedMeta(t *testing.T) { "secret2": "value2", "secret3": "value3", } + var opts []delegation.Option + for k, v := range values { + opts = append(opts, delegation.WithEncryptedMeta(k, v, encryptionKey)) + } // Create token with multiple encrypted values - tkn, err := delegation.New(privKey, aud, cmd, pol, delegation.WithMeta("foo", "bar")) + tkn, err := delegation.New(privKey, aud, cmd, pol, opts...) require.NoError(t, err) - for k, v := range values { - err := tkn.Meta().AddEncrypted(k, v, encryptionKey) - require.NoError(t, err) - } - data, err := tkn.ToDagCbor(privKey) require.NoError(t, err) From 602bdf9c7a7dec1c6b10b0a52b1ebbcc40cb4dd3 Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Mon, 4 Nov 2024 19:15:34 +0100 Subject: [PATCH 3/8] fix broken meta_test.gotest --- pkg/meta/meta_test.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pkg/meta/meta_test.go b/pkg/meta/meta_test.go index 367f54e..c55d1e3 100644 --- a/pkg/meta/meta_test.go +++ b/pkg/meta/meta_test.go @@ -73,11 +73,8 @@ func TestMeta_Add(t *testing.T) { t.Run("error on nil key", func(t *testing.T) { err := m.AddEncrypted("invalid", "test", nil) - require.NoError(t, err) - // with nil key, value should be stored unencrypted - val, err := m.GetString("invalid") - require.NoError(t, err) - require.Equal(t, "test", val) + require.Error(t, err) + require.Contains(t, err.Error(), "encryption key is required") }) }) } From 3987e8649cf14599954e87d10ba7b820d861639c Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Tue, 12 Nov 2024 15:29:48 +0100 Subject: [PATCH 4/8] 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 From 9f47418bdfe4e75aa67c3d8163ac2ef2af3ab88b Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Tue, 12 Nov 2024 15:31:21 +0100 Subject: [PATCH 5/8] fix merge conflict --- pkg/meta/meta.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/meta/meta.go b/pkg/meta/meta.go index b6f6041..9a48269 100644 --- a/pkg/meta/meta.go +++ b/pkg/meta/meta.go @@ -9,10 +9,9 @@ import ( "github.com/ipld/go-ipld-prime/printer" "github.com/ucan-wg/go-ucan/pkg/meta/internal/crypto" - - "github.com/ucan-wg/go-ucan/pkg/policy/literal" ) + var ErrUnsupported = errors.New("failure adding unsupported type to meta") var ErrNotFound = errors.New("key-value not found in meta") From a26d836025eaf400c4e9e373aa3d6f6cd47235db Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Tue, 12 Nov 2024 16:04:33 +0100 Subject: [PATCH 6/8] validate non-zero aes key and other refactoring --- pkg/meta/internal/crypto/aes.go | 31 +++++++++++++++-------- pkg/meta/internal/crypto/aes_test.go | 38 +++++++++++++++------------- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/pkg/meta/internal/crypto/aes.go b/pkg/meta/internal/crypto/aes.go index 28f0ae4..482402e 100644 --- a/pkg/meta/internal/crypto/aes.go +++ b/pkg/meta/internal/crypto/aes.go @@ -31,20 +31,22 @@ func (ks KeySize) IsValid() bool { 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") -// NewKey generates a random AES key of the specified size. -// If no size is provided, it defaults to KeySize256 (32 bytes). +// 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 NewKey(size ...KeySize) ([]byte, error) { - keySize := KeySize256 - if len(size) > 0 { - keySize = size[0] - if !keySize.IsValid() { - return nil, ErrInvalidKeySize - } +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, keySize) + key := make([]byte, size) if _, err := io.ReadFull(rand.Reader, key); err != nil { return nil, fmt.Errorf("failed to generate AES key: %w", err) } @@ -119,5 +121,12 @@ func validateAESKey(key []byte) error { return ErrInvalidKeySize } - return nil + // check if key is all zeros + for _, b := range key { + if b != 0 { + return nil + } + } + + return ErrZeroKey } diff --git a/pkg/meta/internal/crypto/aes_test.go b/pkg/meta/internal/crypto/aes_test.go index 3462a10..1d0d3e4 100644 --- a/pkg/meta/internal/crypto/aes_test.go +++ b/pkg/meta/internal/crypto/aes_test.go @@ -3,7 +3,6 @@ package crypto import ( "bytes" "crypto/rand" - "fmt" "testing" "github.com/stretchr/testify/require" @@ -13,38 +12,42 @@ func TestAESEncryption(t *testing.T) { t.Parallel() key := make([]byte, 32) // generated random 32-byte key - _, err := rand.Read(key) - require.NoError(t, err) + _, errKey := rand.Read(key) + require.NoError(t, errKey) tests := []struct { name string data []byte key []byte - wantErr bool + wantErr error }{ { - name: "valid encryption/decryption", - data: []byte("hello world"), - key: key, - wantErr: false, + name: "valid encryption/decryption", + data: []byte("hello world"), + key: key, }, { name: "nil key returns error", data: []byte("hello world"), key: nil, - wantErr: true, + wantErr: ErrNoEncryptionKey, }, { - name: "empty data", - data: []byte{}, - key: key, - wantErr: false, + name: "empty data", + data: []byte{}, + key: key, }, { name: "invalid key size", data: []byte("hello world"), key: make([]byte, 31), - wantErr: true, + wantErr: ErrInvalidKeySize, + }, + { + name: "zero key returns error", + data: []byte("hello world"), + key: make([]byte, 32), + wantErr: ErrZeroKey, }, } @@ -54,14 +57,13 @@ func TestAESEncryption(t *testing.T) { t.Parallel() encrypted, err := EncryptWithAESKey(tt.data, tt.key) - if tt.wantErr { - require.Error(t, err) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + return } require.NoError(t, err) - fmt.Println(string(encrypted)) - decrypted, err := DecryptStringWithAESKey(encrypted, tt.key) require.NoError(t, err) From fdff79d23a6d96377e8c91e56e93f764a5a7afac Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Tue, 12 Nov 2024 16:07:39 +0100 Subject: [PATCH 7/8] split WithEncryptedMeta in options.go by type --- token/delegation/delegation_test.go | 4 ++-- token/delegation/options.go | 15 +++++++++++---- token/invocation/options.go | 15 +++++++++++---- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/token/delegation/delegation_test.go b/token/delegation/delegation_test.go index 906f293..4059030 100644 --- a/token/delegation/delegation_test.go +++ b/token/delegation/delegation_test.go @@ -173,7 +173,7 @@ func TestEncryptedMeta(t *testing.T) { t.Parallel() tkn, err := delegation.New(privKey, aud, cmd, pol, - delegation.WithEncryptedMeta(tt.key, tt.value, encryptionKey), + delegation.WithEncryptedMetaString(tt.key, tt.value, encryptionKey), ) require.NoError(t, err) @@ -208,7 +208,7 @@ func TestEncryptedMeta(t *testing.T) { } var opts []delegation.Option for k, v := range values { - opts = append(opts, delegation.WithEncryptedMeta(k, v, encryptionKey)) + opts = append(opts, delegation.WithEncryptedMetaString(k, v, encryptionKey)) } // Create token with multiple encrypted values diff --git a/token/delegation/options.go b/token/delegation/options.go index ee2eac2..4df14e7 100644 --- a/token/delegation/options.go +++ b/token/delegation/options.go @@ -44,10 +44,17 @@ 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 { +// WithEncryptedMetaString adds a key/value pair in the "meta" field. +// The string value is encrypted with the given aesKey. +func WithEncryptedMetaString(key, val string, encryptionKey []byte) Option { + return func(t *Token) error { + return t.meta.AddEncrypted(key, val, encryptionKey) + } +} + +// WithEncryptedMetaBytes adds a key/value pair in the "meta" field. +// The []byte value is encrypted with the given aesKey. +func WithEncryptedMetaBytes(key string, val, encryptionKey []byte) Option { return func(t *Token) error { return t.meta.AddEncrypted(key, val, encryptionKey) } diff --git a/token/invocation/options.go b/token/invocation/options.go index e2246dc..349af2c 100644 --- a/token/invocation/options.go +++ b/token/invocation/options.go @@ -44,10 +44,17 @@ 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 { +// WithEncryptedMetaString adds a key/value pair in the "meta" field. +// The string value is encrypted with the given aesKey. +func WithEncryptedMetaString(key, val string, encryptionKey []byte) Option { + return func(t *Token) error { + return t.meta.AddEncrypted(key, val, encryptionKey) + } +} + +// WithEncryptedMetaBytes adds a key/value pair in the "meta" field. +// The []byte value is encrypted with the given aesKey. +func WithEncryptedMetaBytes(key string, val, encryptionKey []byte) Option { return func(t *Token) error { return t.meta.AddEncrypted(key, val, encryptionKey) } From d3e97aaa082b9e22e941b479bcca191d4810c8aa Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Tue, 12 Nov 2024 16:37:53 +0100 Subject: [PATCH 8/8] AddEncrypted adds ciphertext always as bytes --- pkg/meta/meta.go | 8 ++++---- pkg/meta/meta_test.go | 5 ++--- token/delegation/delegation_test.go | 6 ++---- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/pkg/meta/meta.go b/pkg/meta/meta.go index 9a48269..a08d97c 100644 --- a/pkg/meta/meta.go +++ b/pkg/meta/meta.go @@ -58,12 +58,12 @@ func (m *Meta) GetString(key string) (string, error) { // GetEncryptedString decorates GetString and decrypt its output with the given symmetric encryption key. func (m *Meta) GetEncryptedString(key string, encryptionKey []byte) (string, error) { - v, err := m.GetString(key) + v, err := m.GetBytes(key) if err != nil { return "", err } - decrypted, err := crypto.DecryptStringWithAESKey([]byte(v), encryptionKey) + decrypted, err := crypto.DecryptStringWithAESKey(v, encryptionKey) if err != nil { return "", err } @@ -161,16 +161,16 @@ func (m *Meta) AddEncrypted(key string, val any, encryptionKey []byte) error { if err != nil { return err } - return m.Add(key, string(encrypted)) case []byte: encrypted, err = crypto.EncryptWithAESKey(val, encryptionKey) if err != nil { return err } - return m.Add(key, encrypted) default: return ErrNotEncryptable } + + return m.Add(key, encrypted) } // Equals tells if two Meta hold the same key/values. diff --git a/pkg/meta/meta_test.go b/pkg/meta/meta_test.go index 6ae2aca..2fbb176 100644 --- a/pkg/meta/meta_test.go +++ b/pkg/meta/meta_test.go @@ -34,9 +34,8 @@ func TestMeta_Add(t *testing.T) { err = m.AddEncrypted("secret", "hello world", key) require.NoError(t, err) - encrypted, err := m.GetString("secret") - require.NoError(t, err) - require.NotEqual(t, "hello world", encrypted) + _, err = m.GetString("secret") + require.Error(t, err) // the ciphertext is saved as []byte instead of string decrypted, err := m.GetEncryptedString("secret", key) require.NoError(t, err) diff --git a/token/delegation/delegation_test.go b/token/delegation/delegation_test.go index 4059030..b49578c 100644 --- a/token/delegation/delegation_test.go +++ b/token/delegation/delegation_test.go @@ -183,10 +183,8 @@ func TestEncryptedMeta(t *testing.T) { decodedTkn, _, err := delegation.FromSealed(data) require.NoError(t, err) - encrypted, err := decodedTkn.Meta().GetString(tt.key) - require.NoError(t, err) - // Verify the encrypted value is different from original - require.NotEqual(t, tt.value, encrypted) + _, err = decodedTkn.Meta().GetString(tt.key) + require.Error(t, err) decrypted, err := decodedTkn.Meta().GetEncryptedString(tt.key, encryptionKey) require.NoError(t, err)