Merge pull request #51 from ucan-wg/v1-meta-encryption
feat(meta): values symmetric encryption
This commit is contained in:
132
pkg/meta/internal/crypto/aes.go
Normal file
132
pkg/meta/internal/crypto/aes.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
124
pkg/meta/internal/crypto/aes_test.go
Normal file
124
pkg/meta/internal/crypto/aes_test.go
Normal file
@@ -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("<22>`M<><4D><EFBFBD>l\u001AIF<49>\u0012<31><32><EFBFBD>=h<>?<3F>c<EFBFBD> <20><>\u0012<31><32><EFBFBD><EFBFBD>\u001C<31>\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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,22 @@
|
|||||||
package meta
|
package meta
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
"github.com/ipld/go-ipld-prime"
|
||||||
"github.com/ipld/go-ipld-prime/printer"
|
"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"
|
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrNotFound = fmt.Errorf("key-value not found in meta")
|
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.
|
// 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
|
// This also serves as a way to construct the underlying IPLD data with minimum allocations
|
||||||
@@ -50,6 +56,21 @@ func (m *Meta) GetString(key string) (string, error) {
|
|||||||
return v.AsString()
|
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.GetBytes(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypted, err := crypto.DecryptStringWithAESKey(v, encryptionKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(decrypted), nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetInt64 retrieves a value as an int64.
|
// GetInt64 retrieves a value as an int64.
|
||||||
// Returns ErrNotFound if the given key is missing.
|
// Returns ErrNotFound if the given key is missing.
|
||||||
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
||||||
@@ -83,6 +104,21 @@ func (m *Meta) GetBytes(key string) ([]byte, error) {
|
|||||||
return v.AsBytes()
|
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.
|
// GetNode retrieves a value as a raw IPLD node.
|
||||||
// Returns ErrNotFound if the given key is missing.
|
// Returns ErrNotFound if the given key is missing.
|
||||||
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
||||||
@@ -112,6 +148,31 @@ func (m *Meta) Add(key string, val any) error {
|
|||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
case []byte:
|
||||||
|
encrypted, err = crypto.EncryptWithAESKey(val, encryptionKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return ErrNotEncryptable
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.Add(key, encrypted)
|
||||||
|
}
|
||||||
|
|
||||||
// Equals tells if two Meta hold the same key/values.
|
// Equals tells if two Meta hold the same key/values.
|
||||||
func (m *Meta) Equals(other *Meta) bool {
|
func (m *Meta) Equals(other *Meta) bool {
|
||||||
if len(m.Keys) != len(other.Keys) {
|
if len(m.Keys) != len(other.Keys) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package meta_test
|
package meta_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -13,10 +14,64 @@ func TestMeta_Add(t *testing.T) {
|
|||||||
|
|
||||||
type Unsupported struct{}
|
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()
|
t.Parallel()
|
||||||
|
|
||||||
err := (&meta.Meta{}).Add("invalid", &Unsupported{})
|
err := (&meta.Meta{}).Add("invalid", &Unsupported{})
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
_, 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)
|
||||||
|
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.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "encryption key is required")
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ func (r ReadOnly) GetString(key string) (string, error) {
|
|||||||
return r.m.GetString(key)
|
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) {
|
func (r ReadOnly) GetInt64(key string) (int64, error) {
|
||||||
return r.m.GetInt64(key)
|
return r.m.GetInt64(key)
|
||||||
}
|
}
|
||||||
@@ -29,6 +33,10 @@ func (r ReadOnly) GetBytes(key string) ([]byte, error) {
|
|||||||
return r.m.GetBytes(key)
|
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) {
|
func (r ReadOnly) GetNode(key string) (ipld.Node, error) {
|
||||||
return r.m.GetNode(key)
|
return r.m.GetNode(key)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package delegation_test
|
package delegation_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -66,6 +67,8 @@ const (
|
|||||||
|
|
||||||
newCID = "zdpuAn9JgGPvnt2WCmTaKktZdbuvcVGTg9bUT5kQaufwUtZ6e"
|
newCID = "zdpuAn9JgGPvnt2WCmTaKktZdbuvcVGTg9bUT5kQaufwUtZ6e"
|
||||||
rootCID = "zdpuAkgGmUp5JrXvehGuuw9JA8DLQKDaxtK3R8brDQQVC2i5X"
|
rootCID = "zdpuAkgGmUp5JrXvehGuuw9JA8DLQKDaxtK3R8brDQQVC2i5X"
|
||||||
|
|
||||||
|
aesKey = "xQklMmNTnVrmaPBq/0pwV5fEwuv/iClF5HWak9MsgI8="
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConstructors(t *testing.T) {
|
func TestConstructors(t *testing.T) {
|
||||||
@@ -121,6 +124,109 @@ 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.WithEncryptedMetaString(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)
|
||||||
|
|
||||||
|
_, err = decodedTkn.Meta().GetString(tt.key)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
var opts []delegation.Option
|
||||||
|
for k, v := range values {
|
||||||
|
opts = append(opts, delegation.WithEncryptedMetaString(k, v, encryptionKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create token with multiple encrypted values
|
||||||
|
tkn, err := delegation.New(privKey, aud, cmd, pol, opts...)
|
||||||
|
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 {
|
func privKey(t require.TestingT, privKeyCfg string) crypto.PrivKey {
|
||||||
privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg)
|
privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -44,6 +44,22 @@ func WithMeta(key string, val any) 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithNotBefore set's the Token's optional "notBefore" field to the value
|
// WithNotBefore set's the Token's optional "notBefore" field to the value
|
||||||
// of the provided time.Time.
|
// of the provided time.Time.
|
||||||
func WithNotBefore(nbf time.Time) Option {
|
func WithNotBefore(nbf time.Time) Option {
|
||||||
|
|||||||
@@ -44,6 +44,22 @@ func WithMeta(key string, val any) 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithNonce sets the Token's nonce with the given value.
|
// WithNonce sets the Token's nonce with the given value.
|
||||||
//
|
//
|
||||||
// If this option is not used, a random 12-byte nonce is generated for
|
// If this option is not used, a random 12-byte nonce is generated for
|
||||||
|
|||||||
Reference in New Issue
Block a user