Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add new cipher #1

Merged
merged 1 commit into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
[![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 primary key attribute values.
You can either use AWS KMS (don't manage keys; expensive) or AES with
your choice of password.
You can either use AWS KMS (you don't manage keys, but its expensive)
or a cipher with your own key. AES-GCM and ChaCha20-Poly1305 are supported.

Use it to send encrypted last evaluated key values that clients can use
as cursors to paginate through DynamoDB results.
Use it to ecnrypt last evaluated key values from DynamoDB Query responses.
Clients can use these encrypted opaque values to paginate through queries.

## License

Dyno is available under the terms of the MIT license.

Qube Cinema © 2023
Qube Cinema © 2023, 2024
23 changes: 16 additions & 7 deletions base64bytes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,20 @@ package dyno
import "testing"

func TestBase64Bytes(t *testing.T) {
var b Base64Bytes
if err := b.Decode("aGVsbG8gd29ybGQ="); err != nil {
t.Fatal(err)
}
if string(b) != "hello world" {
t.Fatalf("expected %q, got %q", "hello world", string(b))
}
t.Run("valid", func(t *testing.T) {
var b Base64Bytes
if err := b.Decode("aGVsbG8gd29ybGQ="); err != nil {
t.Fatal(err)
}
if string(b) != "hello world" {
t.Fatalf("expected %q, got %q", "hello world", string(b))
}
})

t.Run("invalid", func(t *testing.T) {
var b Base64Bytes
if err := b.Decode("hello world"); err == nil {
t.Fatal("expected error")
}
})
}
34 changes: 24 additions & 10 deletions aeskeycrypter.go → ciphercrypter.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,17 @@ import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha1"
"encoding/base64"
"io"

"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/chacha20poly1305"
)

// NewAesCrypter creates a new KeyCrypter that encrypts DynamoDB primary key attributes
// NewAESCrypter creates a new KeyCrypter that encrypts DynamoDB primary key attributes
// with AES GCM encryption.
// The password and salt are used to derive a 32 byte key using PBKDF2.
func NewAesCrypter(password, salt []byte) (KeyCrypter, error) {
key := pbkdf2.Key(password, salt, 4096, 32, sha1.New)
// The key must be 16, 24, or 32 bytes long to select AES-128, AES-192, or AES-256.
func NewAESCrypter(key []byte) (KeyCrypter, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
Expand All @@ -28,17 +26,33 @@ func NewAesCrypter(password, salt []byte) (KeyCrypter, error) {
return nil, err
}

return &aesCryptedItem{
return &cipherCrypterItem{
mode: mode,
}, nil
}

type aesCryptedItem struct {
// NewChaCha20Poly1305Crypter creates a new KeyCrypter that encrypts DynamoDB
// primary key attributes with ChaCha20-Poly1305 encryption.
// The key must be 32 bytes long.
func NewChaCha20Poly1305Crypter(key []byte) (KeyCrypter, error) {
block, err := chacha20poly1305.New(key)
if err != nil {
return nil, err
}

return &cipherCrypterItem{
mode: block,
}, nil
}

type cipherCrypterItem struct {
mode cipher.AEAD
}

// Encrypt encrypts a dynamodb item along with an encryption context.
func (c *aesCryptedItem) Encrypt(ctx context.Context,
// A random nonce is used for each encryption operation.
// The nonce is prepended to the cipher text.
func (c *cipherCrypterItem) Encrypt(ctx context.Context,
item map[string]types.AttributeValue,
) (string, error) {
plainText, err := serialize(item)
Expand All @@ -59,7 +73,7 @@ func (c *aesCryptedItem) Encrypt(ctx context.Context,

// Decrypt decrypts a dynamodb item along with an encryption context.
// The item must have been encrypted with the same encryption context.
func (c *aesCryptedItem) Decrypt(
func (c *cipherCrypterItem) Decrypt(
ctx context.Context,
itemStr string,
) (map[string]types.AttributeValue, error) {
Expand Down
67 changes: 52 additions & 15 deletions aeskeycrypter_test.go → ciphercrypter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,27 @@ import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)

var testCases = []struct {
func TestCipherCrypterBadKeyLengths(t *testing.T) {
t.Run("AES", func(t *testing.T) {
if _, err := NewAESCrypter([]byte("password")); err == nil {
t.Error("NewAESCrypter() error = nil, want error")
}
})

t.Run("ChaCha20Poly1305", func(t *testing.T) {
if _, err := NewChaCha20Poly1305Crypter([]byte("password")); err == nil {
t.Error("NewChaCha20Poly1305Crypter() error = nil, want error")
}
})
}

type encryptDecryptTest struct {
name string
item map[string]types.AttributeValue
err bool
}{
}

var testCases = []encryptDecryptTest{
{
name: "string",
item: map[string]types.AttributeValue{
Expand Down Expand Up @@ -84,26 +100,47 @@ var testCases = []struct {
},
}

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

ic, err := NewAesCrypter(password, salt)
if err != nil {
t.Fatalf("NewAesItemCrypter() error = %v, want nil", err)
}
t.Run("AES", func(t *testing.T) {
ic, err := NewAESCrypter(key)
if err != nil {
t.Fatalf("NewAesItemCrypter() error = %v, want nil", err)
}
if ic == nil {
t.Fatalf("NewAESCrypter() = nil, want not nil")
}
t.Run("EncryptDecrypt", func(t *testing.T) {
encryptDecryptHelper(t, ic, testCases)
})
})

if ic == nil {
t.Fatalf("NewAesItemCrypter() = nil, want not nil")
}
t.Run("ChaCha20Poly1305", func(t *testing.T) {
ic, err := NewChaCha20Poly1305Crypter(key)
if err != nil {
t.Fatalf("NewChaCha20Poly1305Crypter() error = %v, want nil", err)
}
if ic == nil {
t.Fatalf("NewChaCha20Poly1305Crypter() = nil, want not nil")
}
t.Run("EncryptDecrypt", func(t *testing.T) {
encryptDecryptHelper(t, ic, testCases)
})
})
}

func encryptDecryptHelper(t *testing.T, ic KeyCrypter, tcs []encryptDecryptTest) {
t.Helper()

for _, tc := range tcs {
t.Run(tc.name, func(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")
t.Fatal("Encrypt() error = nil, want error")
}

// OK
Expand Down
19 changes: 10 additions & 9 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ module github.com/RealImage/dyno
go 1.22.5

require (
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.10
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.4
github.com/aws/aws-sdk-go-v2/service/kms v1.35.3
golang.org/x/crypto v0.26.0
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.22
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.38.1
github.com/aws/aws-sdk-go-v2/service/kms v1.37.8
golang.org/x/crypto v0.31.0
)

require (
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
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/aws/aws-sdk-go-v2 v1.32.7 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 // indirect
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.24.10 // indirect
github.com/aws/smithy-go v1.22.1 // indirect
golang.org/x/sys v0.28.0 // indirect
)
38 changes: 20 additions & 18 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.10 h1:orAIBscNu5aIjDOnKIrjO+IUFPMLKj3Lp0bPf4chiPc=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.10/go.mod h1:GNjJ8daGhv10hmQYCnmkV8HuY6xXOXV4vzBssSjEIlU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.4 h1:utG3S4T+X7nONPIpRoi1tVcQdAdJxntiVS2yolPJyXc=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.4/go.mod h1:q9vzW3Xr1KEXa8n4waHiFt1PrppNDlMymlYP+xpsFbY=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.3 h1:r27/FnxLPixKBRIlslsvhqscBuMK8uysCYG9Kfgm098=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.3/go.mod h1:jqOFyN+QSWSoQC+ppyc4weiO8iNQXbzRbxDjQ1ayYd4=
github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 h1:UPTdlTOwWUX49fVi7cymEN6hDqCwe3LNv1vi7TXUutk=
github.com/aws/aws-sdk-go-v2/service/kms v1.35.3/go.mod h1:gjDP16zn+WWalyaUqwCCioQ8gU8lzttCCc9jYsiQI/8=
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=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
github.com/aws/aws-sdk-go-v2 v1.32.7 h1:ky5o35oENWi0JYWUZkB7WYvVPP+bcRF5/Iq7JWSb5Rw=
github.com/aws/aws-sdk-go-v2 v1.32.7/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.22 h1:p2LDiYhvM9mMExEY1meHMAmjmVlzD1J1jVG+fGut+mE=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.22/go.mod h1:fo5T2fYMHVF2rHrym50h7Ue/+SECRJlUHUFZLjSX18g=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 h1:I/5wmGMffY4happ8NOCuIUEWGUvvFp5NSeQcXl9RHcI=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26/go.mod h1:FR8f4turZtNy6baO0KJ5FJUmXH/cSkI9fOngs0yl6mA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 h1:zXFLuEuMMUOvEARXFUVJdfqZ4bvvSgdGRq/ATcrQxzM=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26/go.mod h1:3o2Wpy0bogG1kyOPrgkXA8pgIfEEv0+m19O9D5+W8y8=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.38.1 h1:AnSNs7Ogi0LXHPMDBx4RE7imU4/JmzWFziqkMKJA2AY=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.38.1/go.mod h1:J8xqRbx7HIc8ids2P8JbrKx9irONPEYq7Z1FpLDpi3I=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.24.10 h1:aWEbNPNdGiTGSR6/Yy9S0Ad07sMVaT/CFaVq7GuDGx4=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.24.10/go.mod h1:HywkMgYwY0uaybPvvctx6fkm3L1ssRKeGv7TPZ6OQ/M=
github.com/aws/aws-sdk-go-v2/service/kms v1.37.8 h1:KbLZjYqhQ9hyB4HwXiheiflTlYQa0+Fz0Ms/rh5f3mk=
github.com/aws/aws-sdk-go-v2/service/kms v1.37.8/go.mod h1:ANs9kBhK4Ghj9z1W+bsr3WsNaPF71qkgd6eE6Ekol/Y=
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
19 changes: 0 additions & 19 deletions keycrypter.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,6 @@
// returned by a dynamodb query can be encrypted and passed to a client. The client can then
// pass the encrypted LastEvaluatedKey back to the server, which can decrypt it and use it
// to continue the query.
//
// Example:
//
// // Create a new AesCrypter
// crypter := dyno.NewAesCrypter([]byte("encryption-password"), []byte("salt"))
//
// // Encrypt the lastEvaluatedKey
// encryptedLastEvaluatedKey, err := crypter.Encrypt(ctx, map[string]string{
// "clientID": "1234",
// }, lastEvaluatedKey)
//
// // Pass the encryptedLastEvaluatedKey to the client in the response
//
// // Client passes the encryptedLastEvaluatedKey back to the server in the next request
//
// // Decrypt the encryptedLastEvaluatedKey
// lastEvaluatedKey, err := crypter.Decrypt(ctx, map[string]string{
// "clientID": "1234",
// }, encryptedLastEvaluatedKey)
package dyno

import (
Expand Down
46 changes: 46 additions & 0 deletions keycrypter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package dyno

import (
"context"
"fmt"
"reflect"

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

func ExampleItemCrypter() {

Check failure on line 11 in keycrypter_test.go

View workflow job for this annotation

GitHub Actions / Lint & test code.

tests: ExampleItemCrypter refers to unknown identifier: ItemCrypter (govet)
// Create a new ItemCrypter with a random key.
c, err := NewAESCrypter([]byte("encrypt-password"))
if err != nil {
panic(err)
}

ctx := context.Background()

const key = "key"
lastEvaluatedKey := map[string]types.AttributeValue{
key: &types.AttributeValueMemberS{Value: "value"},
}

// Encrypt an item.
encrypted, err := c.Encrypt(ctx, lastEvaluatedKey)
if err != nil {
panic(err)
}

// Decrypt the item.
decrypted, err := c.Decrypt(ctx, encrypted)
if err != nil {
panic(err)
}

if !reflect.DeepEqual(lastEvaluatedKey, decrypted) {
panic("decrypted item does not match original item")
}

value := decrypted[key]
val := value.(*types.AttributeValueMemberS)
fmt.Printf("%s: %s\n", key, val.Value)

// Output: key: value
}
4 changes: 2 additions & 2 deletions kmskeycrypter.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import (
"github.com/aws/aws-sdk-go-v2/service/kms"
)

// NewKmsCrypter returns a KeyCrypter that encrypts and decrypts dynamodb primary key
// NewKMSCrypter returns a KeyCrypter that encrypts and decrypts dynamodb primary key
// attributevalues using AWS KMS.
// The KMS key ID is the ARN of the KMS key used to encrypt and decrypt the items.
// The KMS client is used to call the KMS API. If nil, a new client will be created.
func NewKmsCrypter(kmsKeyID string, kmsClient *kms.Client) KeyCrypter {
func NewKMSCrypter(kmsKeyID string, kmsClient *kms.Client) KeyCrypter {
if kmsClient == nil {
kmsClient = kms.New(kms.Options{})
}
Expand Down
Loading