Skip to content

Commit

Permalink
fix: Only encrypt primary key attrs
Browse files Browse the repository at this point in the history
Renamed everything to reflect the fact that we only serialise
and encrypt primary key attrvalues.

Also added tests for supported data types.
  • Loading branch information
ananthb committed Aug 13, 2024
1 parent 7a8cec1 commit 24e3e2a
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 27 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

[![CI 🏗](https://github.com/RealImage/dyno/actions/workflows/ci.yml/badge.svg)](https://github.com/RealImage/dyno/actions/workflows/ci.yml) [![Go Reference](https://pkg.go.dev/badge/github.com/RealImage/dyno.svg)](https://pkg.go.dev/github.com/RealImage/dyno)

Encrypt and decrypt DynamoDB table attribute value maps using either AWS KMS
or AES with a password. Useful for sending clients the LastEvaluatedKey from
the result of a DynamoDB Query or Scan operation, for paginating results without
leaking sensitive information.
Encrypt and decrypt DynamoDB primary key attribute values.
You can either use AWS KMS (don't manage keys; expensive) or AES with
your choice of password.

Use it to send encrypted last evaluated key values that clients can use
as cursors to paginate through DynamoDB results.

## License

Expand Down
9 changes: 5 additions & 4 deletions aesitemcrypter.go → aeskeycrypter.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import (
"golang.org/x/crypto/pbkdf2"
)

// NewAesItemCrypter creates a new ItemCrypter that uses AES GCM encryption.
func NewAesItemCrypter(password, salt []byte) (ItemCrypter, error) {
// NewAesCrypter creates a new KeyCrypter that encrypts DynamoDB primary key attributes
// with AES GCM encryption.
func NewAesCrypter(password, salt []byte) (KeyCrypter, error) {
key := pbkdf2.Key(password, salt, 4096, 32, sha1.New)
block, err := aes.NewCipher(key)
if err != nil {
Expand Down Expand Up @@ -59,9 +60,9 @@ func (c *aesCryptedItem) Encrypt(ctx context.Context,
// The item must have been encrypted with the same encryption context.
func (c *aesCryptedItem) Decrypt(
ctx context.Context,
item string,
itemStr string,
) (map[string]types.AttributeValue, error) {
nonceAndCipherText, err := base64.URLEncoding.DecodeString(item)
nonceAndCipherText, err := base64.URLEncoding.DecodeString(itemStr)
if err != nil {
return nil, err
}
Expand Down
30 changes: 28 additions & 2 deletions aesitemcrypter_test.go → aeskeycrypter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
var testCases = []struct {
name string
item map[string]types.AttributeValue
err bool
}{
{
name: "string",
Expand All @@ -35,12 +36,14 @@ var testCases = []struct {
item: map[string]types.AttributeValue{
"key": &types.AttributeValueMemberBOOL{Value: true},
},
err: true,
},
{
name: "null",
item: map[string]types.AttributeValue{
"key": &types.AttributeValueMemberNULL{Value: true},
},
err: true,
},
{
name: "list",
Expand All @@ -55,6 +58,7 @@ var testCases = []struct {
},
},
},
err: true,
},
{
name: "map",
Expand All @@ -65,14 +69,26 @@ var testCases = []struct {
},
},
},
err: true,
},
{
name: "binarySet",
item: map[string]types.AttributeValue{
"key": &types.AttributeValueMemberBS{
Value: [][]byte{
[]byte("value"),
},
},
},
err: true,
},
}

func TestAesItemCrypter(t *testing.T) {
func TestAesCrypter(t *testing.T) {
password := []byte("password")
salt := []byte("saltsalt")

ic, err := NewAesItemCrypter(password, salt)
ic, err := NewAesCrypter(password, salt)
if err != nil {
t.Fatalf("NewAesItemCrypter() error = %v, want nil", err)
}
Expand All @@ -84,6 +100,16 @@ func TestAesItemCrypter(t *testing.T) {
for _, tc := range testCases {
t.Run("EncryptDecrypt_"+tc.name, func(t *testing.T) {
cipherText, err := ic.Encrypt(context.Background(), tc.item)

if tc.err {
if err == nil {
t.Fatalf("Encrypt() error = nil, want error")
}

// OK
return
}

if err != nil {
t.Fatalf("Encrypt() error = %v, want nil", err)
}
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ require (
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.3 // indirect
github.com/aws/smithy-go v1.20.3 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@ github.com/aws/smithy-go v1.17.0 h1:wWJD7LX6PBV6etBUwO0zElG0nWN9rUhp0WdYeHSHAaI=
github.com/aws/smithy-go v1.17.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
Expand Down
44 changes: 33 additions & 11 deletions itemcrypter.go → keycrypter.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,36 @@
//
// Example:
//
// // Create a new CryptedItem
// cryptedItem := dyno.NewCryptedItem("alias/my-kms-key", kmsClient)
// // Create a new AesCrypter
// crypter := dyno.NewAesCrypter([]byte("encryption-password"), []byte("salt"))
//
// // Encrypt the lastEvaluatedKey
// encryptedLastEvaluatedKey, err := cryptedItem.Encrypt(ctx, map[string]string{
// encryptedLastEvaluatedKey, err := crypter.Encrypt(ctx, map[string]string{
// "clientID": "1234",
// }, lastEvaluatedKey)
//
// // Pass the encryptedLastEvaluatedKey to the client
// // Pass the encryptedLastEvaluatedKey to the client in the response
//
// // Client passes the encryptedLastEvaluatedKey back to the server
// // Client passes the encryptedLastEvaluatedKey back to the server in the next request
//
// // Decrypt the encryptedLastEvaluatedKey
// lastEvaluatedKey, err := cryptedItem.Decrypt(ctx, map[string]string{
// lastEvaluatedKey, err := crypter.Decrypt(ctx, map[string]string{
// "clientID": "1234",
// }, encryptedLastEvaluatedKey)
package dyno

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"

"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)

// ItemCrypter is an interface that encrypts and decrypts dynamodb items.
type ItemCrypter interface {
// KeyCrypter is an interface that encrypts and decrypts DynamoDB primary key attribute values.
type KeyCrypter interface {
Encrypt(ctx context.Context, item map[string]types.AttributeValue) (string, error)
Decrypt(ctx context.Context, item string) (map[string]types.AttributeValue, error)
}
Expand All @@ -52,13 +53,20 @@ func getEncryptionContext(ctx context.Context) (ec map[string]string, ok bool) {
}

func serialize(input map[string]types.AttributeValue) ([]byte, error) {
for _, v := range input {
switch v.(type) {
case *types.AttributeValueMemberS, *types.AttributeValueMemberB, *types.AttributeValueMemberN:
continue
default:
return nil, fmt.Errorf("unsupported type: %T", v)
}
}

var jsonMap map[string]any
if err := attributevalue.UnmarshalMap(input, &jsonMap); err != nil {
return nil, err
}

fmt.Println(jsonMap)

return json.Marshal(jsonMap)
}

Expand All @@ -68,5 +76,19 @@ func deserialize(input []byte) (map[string]types.AttributeValue, error) {
return nil, err
}

return attributevalue.MarshalMap(jsonMap)
output, err := attributevalue.MarshalMap(jsonMap)
if err != nil {
return nil, err
}

// Convert hex strings back to byte slices
for k, v := range output {
if s, ok := v.(*types.AttributeValueMemberS); ok {
if val, err := base64.StdEncoding.DecodeString(s.Value); err == nil {
output[k] = &types.AttributeValueMemberB{Value: val}
}
}
}

return output, nil
}
12 changes: 6 additions & 6 deletions kmsitemcrypter.go → kmskeycrypter.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,22 @@ import (
"github.com/aws/aws-sdk-go-v2/service/kms"
)

func NewKmsItemCrypter(kmsKeyID string, kmsClient *kms.Client) ItemCrypter {
return &kmsCryptedItem{
func NewKmsCrypter(kmsKeyID string, kmsClient *kms.Client) KeyCrypter {
return &kmsCrypter{
kmsKeyID: kmsKeyID,
kmsClient: kmsClient,
}
}

// kmsCryptedItem is a struct that encrypts and decrypts dynamodb items with a KMS key.
type kmsCryptedItem struct {
// kmsCrypter is a struct that encrypts and decrypts dynamodb items with a KMS key.
type kmsCrypter struct {
kmsKeyID string
kmsClient *kms.Client
}

// Encrypt encrypts a dynamodb item. If ctx contains an encryption context, it will be used
// to encrypt the item.
func (c *kmsCryptedItem) Encrypt(
func (c *kmsCrypter) Encrypt(
ctx context.Context,
item map[string]types.AttributeValue,
) (string, error) {
Expand Down Expand Up @@ -51,7 +51,7 @@ func (c *kmsCryptedItem) Encrypt(

// Decrypt decrypts a dynamodb item. If ctx contains an encryption context, it will be used
// to decrypt the item. The item must have been encrypted with the same encryption context.
func (c *kmsCryptedItem) Decrypt(
func (c *kmsCrypter) Decrypt(
ctx context.Context,
item string,
) (map[string]types.AttributeValue, error) {
Expand Down

0 comments on commit 24e3e2a

Please sign in to comment.