diff --git a/.github/workflows/ci_test_go.yml b/.github/workflows/ci_test_go.yml index 78b3b8abe..d29337eb3 100644 --- a/.github/workflows/ci_test_go.yml +++ b/.github/workflows/ci_test_go.yml @@ -140,8 +140,9 @@ jobs: run: | make test_go - - name: Test Examples + - name: Run and Test Examples if: matrix.library == 'DynamoDbEncryption' working-directory: ./Examples/runtimes/go run: | go run main.go + go test ./... diff --git a/Examples/runtimes/go/migration/PlaintextToAWSDBE/README.md b/Examples/runtimes/go/migration/PlaintextToAWSDBE/README.md new file mode 100644 index 000000000..31170c101 --- /dev/null +++ b/Examples/runtimes/go/migration/PlaintextToAWSDBE/README.md @@ -0,0 +1,51 @@ +# Plaintext DynamoDB Table to AWS Database Encryption SDK Encrypted Table Migration + +This projects demonstrates the steps necessary +to migrate to the AWS Database Encryption SDK for DynamoDb +from a plaintext database. + +[Step 0](plaintext/step0.go) demonstrates the starting state for your system. + +## Step 1 + +In Step 1, you update your system to do the following: + +- continue to read plaintext items +- continue to write plaintext items +- prepare to read encrypted items + +When you deploy changes in Step 1, +you should not expect any behavior change in your system, +and your dataset still consists of plaintext data. + +You must ensure that the changes in Step 1 make it to all your readers before you proceed to Step 2. + +## Step 2 + +In Step 2, you update your system to do the following: + +- continue to read plaintext items +- start writing encrypted items +- continue to read encrypted items + +When you deploy changes in Step 2, +you are introducing encrypted items to your system, +and must make sure that all your readers are updated with the changes from Step 1. + +Before you move onto the next step, you will need to encrypt all plaintext items in your dataset. +Once you have completed this step, +while new items are being encrypted using the new format and will be authenticated on read, +your system will still accept reading plaintext, unauthenticated items. +In order to complete migration to a system where you always authenticate your items, +you should prioritize moving on to Step 3. + +## Step 3 + +Once all old items are encrypted, +update your system to do the following: + +- continue to write encrypted items +- continue to read encrypted items +- do not accept reading plaintext items + +Once you have deployed these changes to your system, you have completed migration. diff --git a/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/common.go b/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/common.go new file mode 100644 index 000000000..4e51f6d98 --- /dev/null +++ b/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/common.go @@ -0,0 +1,71 @@ +package awsdbe + +import ( + "context" + + mpl "github.com/aws/aws-cryptographic-material-providers-library/releases/go/mpl/awscryptographymaterialproviderssmithygenerated" + mpltypes "github.com/aws/aws-cryptographic-material-providers-library/releases/go/mpl/awscryptographymaterialproviderssmithygeneratedtypes" + dbesdkdynamodbencryptiontypes "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/awscryptographydbencryptionsdkdynamodbsmithygeneratedtypes" + dbesdkstructuredencryptiontypes "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/awscryptographydbencryptionsdkstructuredencryptionsmithygeneratedtypes" + "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/examples/utils" +) + +func configureTable(kmsKeyID, ddbTableName string, plaintextOverride dbesdkdynamodbencryptiontypes.PlaintextOverride) dbesdkdynamodbencryptiontypes.DynamoDbTablesEncryptionConfig { + + // Create a Keyring. This Keyring will be responsible for protecting the data keys that protect your data. + // We will use the `CreateMrkMultiKeyring` method to create this keyring, + // as it will correctly handle both single region and Multi-Region KMS Keys. + matProv, err := mpl.NewClient(mpltypes.MaterialProvidersConfig{}) + utils.HandleError(err) + + keyringInput := mpltypes.CreateAwsKmsMrkMultiKeyringInput{ + Generator: &kmsKeyID, + } + kmsKeyring, err := matProv.CreateAwsKmsMrkMultiKeyring(context.Background(), keyringInput) + utils.HandleError(err) + + // Configure which attributes are encrypted and/or signed when writing new items. + // For each attribute that may exist on the items we plan to write to our DynamoDbTable, + // we must explicitly configure how they should be treated during item encryption: + // - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + // - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + // - DO_NOTHING: The attribute is not encrypted and not included in the signature + partitionKeyName := "partition_key" + sortKeyName := "sort_key" + + attributeActions := map[string]dbesdkstructuredencryptiontypes.CryptoAction{ + partitionKeyName: dbesdkstructuredencryptiontypes.CryptoActionSignOnly, + sortKeyName: dbesdkstructuredencryptiontypes.CryptoActionSignOnly, + "attribute1": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, + "attribute2": dbesdkstructuredencryptiontypes.CryptoActionSignOnly, + "attribute3": dbesdkstructuredencryptiontypes.CryptoActionDoNothing, + } + + // Configure which attributes we expect to be excluded in the signature + // when reading items. This value represents all unsigned attributes + // across our entire dataset. If you ever want to add new unsigned attributes + // in the future, you must make an update to this field to all your readers + // before deploying any change to start writing that new data. It is not safe + // to remove attributes from this field. + unsignedAttributes := []string{"attribute3"} + + // Create encryption configuration for table. + tableConfig := dbesdkdynamodbencryptiontypes.DynamoDbTableEncryptionConfig{ + LogicalTableName: ddbTableName, + PartitionKeyName: partitionKeyName, + SortKeyName: &sortKeyName, + AttributeActionsOnEncrypt: attributeActions, + Keyring: kmsKeyring, + AllowedUnsignedAttributes: unsignedAttributes, + PlaintextOverride: &plaintextOverride, + } + + tableConfigsMap := make(map[string]dbesdkdynamodbencryptiontypes.DynamoDbTableEncryptionConfig) + tableConfigsMap[ddbTableName] = tableConfig + + listOfTableConfigs := dbesdkdynamodbencryptiontypes.DynamoDbTablesEncryptionConfig{ + TableEncryptionConfigs: tableConfigsMap, + } + + return listOfTableConfigs +} diff --git a/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/step1.go b/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/step1.go new file mode 100644 index 000000000..44db5e18b --- /dev/null +++ b/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/step1.go @@ -0,0 +1,124 @@ +package awsdbe + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + + "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/dbesdkmiddleware" + plaintexttoawsdbe "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/examples/migration/PlaintextToAWSDBE" + "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/examples/utils" + + dbesdkdynamodbencryptiontypes "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/awscryptographydbencryptionsdkdynamodbsmithygeneratedtypes" +) + +/* +Migration Step 1: This is an example demonstrating how to start using the +AWS Database Encryption SDK with a pre-existing table with plaintext items. +In this example, we configure a DynamoDb Encryption Interceptor to do the following: + - Write items only in plaintext + - Read items in plaintext or, if the item is encrypted, decrypt with our encryption configuration + +While this step configures your client to be ready to start reading encrypted items, +we do not yet expect to be reading any encrypted items, +as our client still writes plaintext items. +Before you move on to step 2, ensure that these changes have successfully been deployed +to all of your readers. + +Running this example requires access to the DDB Table whose name +is provided in the function parameter. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) +*/ +func MigrationStep1(kmsKeyID, ddbTableName, partitionKeyValue, sortKeyWriteValue, sortKeyReadValue string) error { + cfg, err := config.LoadDefaultConfig(context.TODO()) + utils.HandleError(err) + + // 1. Configure your Keyring, attribute actions, + // allowedUnsignedAttributes, and encryption configuration for table. + // This is common across all the steps. + + // Note that while we still are not writing encrypted items, + // and our key will not be used to encrypt items in this example, + // our configuration specifies that we may read encrypted items, + // and we should expect to be able to decrypt and process any encrypted items. + // To that end, we must fully define our encryption configuration in + // this step. + + // This `PlaintextOverrideForcePlaintextWriteAllowPlaintextRead` means: + // - Write: Items are forced to be written as plaintext. + // Items may not be written as encrypted items. + // - Read: Items are allowed to be read as plaintext. + // Items are allowed to be read as encrypted items. + listOfTableConfigs := configureTable(kmsKeyID, ddbTableName, dbesdkdynamodbencryptiontypes.PlaintextOverrideForcePlaintextWriteAllowPlaintextRead) + + // 2. Create DynamoDB client with dbEsdkMiddleware + dbEsdkMiddleware, err := dbesdkmiddleware.NewDBEsdkMiddleware(listOfTableConfigs) + utils.HandleError(err) + + ddb := dynamodb.NewFromConfig(cfg, dbEsdkMiddleware.CreateMiddleware()) + + // 3. Put an item into your table. + // This item will be stored in plaintext. + encryptedAndSignedValue := "this will be encrypted and signed" + signOnlyValue := "this will never be encrypted, but it will be signed" + doNothingValue := "this will never be encrypted nor signed" + item := map[string]types.AttributeValue{ + "partition_key": &types.AttributeValueMemberS{Value: partitionKeyValue}, + "sort_key": &types.AttributeValueMemberN{Value: sortKeyWriteValue}, + "attribute1": &types.AttributeValueMemberS{Value: encryptedAndSignedValue}, + "attribute2": &types.AttributeValueMemberS{Value: signOnlyValue}, + "attribute3": &types.AttributeValueMemberS{Value: doNothingValue}, + } + + putInput := dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: item, + } + + _, err = ddb.PutItem(context.TODO(), &putInput) + + // We return this error because we run test against the error. + // When used in production code, you can decide how you want to handle errors. + if err != nil { + return err + } + + // 4. Get an item back from the table using the DynamoDb Client. + // If this is an item written in plaintext (i.e. any item written + // during Step 0 or 1), then the item will still be in plaintext. + // If this is an item that was encrypted client-side (i.e. any item written + // during Step 2 or after), then the item will be decrypted client-side + // and surfaced as a plaintext item. + key := map[string]types.AttributeValue{ + "partition_key": &types.AttributeValueMemberS{Value: partitionKeyValue}, + "sort_key": &types.AttributeValueMemberN{Value: sortKeyReadValue}, + } + + getInput := &dynamodb.GetItemInput{ + TableName: &ddbTableName, + Key: key, + ConsistentRead: aws.Bool(true), + } + + result, err := ddb.GetItem(context.TODO(), getInput) + // We return this error because we run test against the error. + // When used in production code, you can decide how you want to handle errors. + if err != nil { + return err + } + + // Verify we got the expected item back + err = plaintexttoawsdbe.VerifyReturnedItem(result, partitionKeyValue, sortKeyReadValue, encryptedAndSignedValue, signOnlyValue, doNothingValue) + if err != nil { + return err + } + fmt.Println("MigrationStep1 completed successfully") + return nil +} diff --git a/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/step1_test.go b/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/step1_test.go new file mode 100644 index 000000000..43eaf7f86 --- /dev/null +++ b/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/step1_test.go @@ -0,0 +1,50 @@ +package awsdbe + +import ( + "testing" + + "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/examples/migration/PlaintextToAWSDBE/plaintext" + "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/examples/utils" + "github.com/google/uuid" +) + +func TestMigrationStep1(t *testing.T) { + kmsKeyID := utils.KmsKeyID() + tableName := utils.DdbTableName() + partitionKey := uuid.New().String() + sortKeys := []string{"0", "1", "2", "3"} + + // Successfully executes Step 1 + err := MigrationStep1(kmsKeyID, tableName, partitionKey, sortKeys[1], sortKeys[1]) + utils.HandleError(err) + + // Given: Step 0 has succeeded + err = plaintext.MigrationStep0(tableName, partitionKey, sortKeys[0], sortKeys[0]) + utils.HandleError(err) + + // When: Execute Step 1 with sortReadValue=0, Then: Success (i.e. can read plaintext values) + err = MigrationStep1(kmsKeyID, tableName, partitionKey, sortKeys[1], sortKeys[0]) + utils.HandleError(err) + + // Given: Step 2 has succeeded + err = MigrationStep2(kmsKeyID, tableName, partitionKey, sortKeys[2], sortKeys[2]) + utils.HandleError(err) + + // When: Execute Step 1 with sortReadValue=2, Then: Success (i.e. can read encrypted values) + err = MigrationStep1(kmsKeyID, tableName, partitionKey, sortKeys[1], sortKeys[2]) + utils.HandleError(err) + + // Given: Step 3 has succeeded + err = MigrationStep3(kmsKeyID, tableName, partitionKey, sortKeys[3], sortKeys[3]) + utils.HandleError(err) + + // When: Execute Step 1 with sortReadValue=3, Then: Success (i.e. can read encrypted values) + err = MigrationStep1(kmsKeyID, tableName, partitionKey, sortKeys[1], sortKeys[3]) + utils.HandleError(err) + + // Cleanup + for _, sortKey := range sortKeys { + utils.DeleteItem(tableName, "partition_key", partitionKey, "sort_key", sortKey) + } + +} diff --git a/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/step2.go b/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/step2.go new file mode 100644 index 000000000..1da1bcda3 --- /dev/null +++ b/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/step2.go @@ -0,0 +1,122 @@ +package awsdbe + +import ( + // Standard imports + "context" + "fmt" + + // AWS SDK imports + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + + dbesdkdynamodbencryptiontypes "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/awscryptographydbencryptionsdkdynamodbsmithygeneratedtypes" + "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/dbesdkmiddleware" + plaintexttoawsdbe "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/examples/migration/PlaintextToAWSDBE" + "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/examples/utils" +) + +/* +Migration Step 2: This is an example demonstrating how to update your configuration +to start writing encrypted items, but still continue to read any plaintext or +encrypted items. + +Once you deploy this change to your system, you will have a dataset +containing both encrypted and plaintext items. +Because the changes in Step 1 have been deployed to all our readers, +we can be sure that our entire system is ready to read this new data. + +Before you move onto the next step, you will need to encrypt all plaintext items in your dataset. +How you will want to do this depends on your system. + +Running this example requires access to the DDB Table whose name +is provided in the function parameter. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) +*/ +func MigrationStep2(kmsKeyID, ddbTableName, partitionKeyValue, sortKeyWriteValue, sortKeyReadValue string) error { + cfg, err := config.LoadDefaultConfig(context.TODO()) + utils.HandleError(err) + + // 1. Configure your Keyring, attribute actions, + // allowedUnsignedAttributes, and encryption configuration for table. + // This is common across all the steps. + + // When creating encryption configuration for your table, + // you must use the plaintext override `PlaintextOverrideForbidPlaintextWriteAllowPlaintextRead`. + // This plaintext override means: + // - Write: Items are forbidden to be written as plaintext. + // Items will be written as encrypted items. + // - Read: Items are allowed to be read as plaintext. + // Items are allowed to be read as encrypted items. + + listOfTableConfigs := configureTable(kmsKeyID, ddbTableName, dbesdkdynamodbencryptiontypes.PlaintextOverrideForbidPlaintextWriteAllowPlaintextRead) + + // 2. Create DynamoDB client with dbEsdkMiddleware + dbEsdkMiddleware, err := dbesdkmiddleware.NewDBEsdkMiddleware(listOfTableConfigs) + utils.HandleError(err) + + ddb := dynamodb.NewFromConfig(cfg, dbEsdkMiddleware.CreateMiddleware()) + + // 3. Put an item into your table. + // This item will be encrypted. + encryptedAndSignedValue := "this will be encrypted and signed" + signOnlyValue := "this will never be encrypted, but it will be signed" + doNothingValue := "this will never be encrypted nor signed" + item := map[string]types.AttributeValue{ + "partition_key": &types.AttributeValueMemberS{Value: partitionKeyValue}, + "sort_key": &types.AttributeValueMemberN{Value: sortKeyWriteValue}, + "attribute1": &types.AttributeValueMemberS{Value: encryptedAndSignedValue}, + "attribute2": &types.AttributeValueMemberS{Value: signOnlyValue}, + "attribute3": &types.AttributeValueMemberS{Value: doNothingValue}, + } + + putInput := dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: item, + } + + _, err = ddb.PutItem(context.TODO(), &putInput) + + // We return this error because we run test against the error. + // When used in production code, you can decide how you want to handle errors. + if err != nil { + return err + } + + // 4. Get an item back from the table. + // If this is an item written in plaintext (i.e. any item written + // during Step 0 or 1), then the item will still be in plaintext. + // If this is an item that was encrypted client-side (i.e. any item written + // during Step 2 or after), then the DDB client will decrypt the + // item client-side and surface it in our code as a plaintext item. + key := map[string]types.AttributeValue{ + "partition_key": &types.AttributeValueMemberS{Value: partitionKeyValue}, + "sort_key": &types.AttributeValueMemberN{Value: sortKeyReadValue}, + } + + getInput := &dynamodb.GetItemInput{ + TableName: &ddbTableName, + Key: key, + ConsistentRead: aws.Bool(true), + } + + result, err := ddb.GetItem(context.TODO(), getInput) + // We return this error because we run test against the error. + // When used in production code, you can decide how you want to handle errors. + if err != nil { + return err + } + + // Verify we got the expected item back + err = plaintexttoawsdbe.VerifyReturnedItem(result, partitionKeyValue, sortKeyReadValue, encryptedAndSignedValue, signOnlyValue, doNothingValue) + if err != nil { + return err + } + fmt.Println("MigrationStep2 completed successfully") + return nil +} diff --git a/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/step2_test.go b/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/step2_test.go new file mode 100644 index 000000000..e202cd6e8 --- /dev/null +++ b/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/step2_test.go @@ -0,0 +1,49 @@ +package awsdbe + +import ( + "testing" + + "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/examples/migration/PlaintextToAWSDBE/plaintext" + "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/examples/utils" + "github.com/google/uuid" +) + +func TestMigrationStep2(t *testing.T) { + kmsKeyID := utils.KmsKeyID() + tableName := utils.DdbTableName() + partitionKey := uuid.New().String() + sortKeys := []string{"0", "1", "2", "3"} + + // Successfully executes Step 2 + err := MigrationStep2(kmsKeyID, tableName, partitionKey, sortKeys[2], sortKeys[2]) + utils.HandleError(err) + + // Given: Step 0 has succeeded + err = plaintext.MigrationStep0(tableName, partitionKey, sortKeys[0], sortKeys[0]) + utils.HandleError(err) + + // When: Execute Step 2 with sortReadValue=0, Then: Success (i.e. can read plaintext values) + err = MigrationStep2(kmsKeyID, tableName, partitionKey, sortKeys[2], sortKeys[0]) + utils.HandleError(err) + + // Given: Step 1 has succeeded + err = MigrationStep1(kmsKeyID, tableName, partitionKey, sortKeys[1], sortKeys[1]) + utils.HandleError(err) + + // When: Execute Step 2 with sortReadValue=1, Then: Success (i.e. can read encrypted values) + err = MigrationStep2(kmsKeyID, tableName, partitionKey, sortKeys[2], sortKeys[1]) + utils.HandleError(err) + + // Given: Step 3 has succeeded + err = MigrationStep3(kmsKeyID, tableName, partitionKey, sortKeys[3], sortKeys[3]) + utils.HandleError(err) + + // When: Execute Step 2 with sortReadValue=3, Then: Success (i.e. can read encrypted values) + err = MigrationStep2(kmsKeyID, tableName, partitionKey, sortKeys[2], sortKeys[3]) + utils.HandleError(err) + + // Cleanup + for _, sortKey := range sortKeys { + utils.DeleteItem(tableName, "partition_key", partitionKey, "sort_key", sortKey) + } +} diff --git a/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/step3.go b/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/step3.go new file mode 100644 index 000000000..96304cdce --- /dev/null +++ b/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/step3.go @@ -0,0 +1,119 @@ +package awsdbe + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + + dbesdkdynamodbencryptiontypes "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/awscryptographydbencryptionsdkdynamodbsmithygeneratedtypes" + "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/dbesdkmiddleware" + plaintexttoawsdbe "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/examples/migration/PlaintextToAWSDBE" + "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/examples/utils" +) + +/* +Migration Step 3: This is an example demonstrating how to update your configuration +to stop accepting reading plaintext items. + +Once you complete Step 3, all items being read by your system are encrypted. + +Before you move onto this step, you will need to encrypt all plaintext items in your dataset. +How you will want to do this depends on your system. + +Running this example requires access to the DDB Table whose name +is provided in the function parameter. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) +*/ +func MigrationStep3(kmsKeyID, ddbTableName, partitionKeyValue, sortKeyWriteValue, sortKeyReadValue string) error { + cfg, err := config.LoadDefaultConfig(context.TODO()) + utils.HandleError(err) + + // 1. Configure your Keyring, attribute actions, + // allowedUnsignedAttributes, and encryption configuration for table. + // This is common across all the steps. + + // When creating encryption configuration for your table, + // you can either not specify PlaintextOverride or use + // `PlaintextOverrideForbidPlaintextWriteForbidPlaintextRead` as PlaintextOverride. + // If you do not specify `PlaintextOverrideForbidPlaintextWriteForbidPlaintextRead` + // plaintext override defaults to `PlaintextOverrideForbidPlaintextWriteForbidPlaintextRead`, + // which is the desired behavior for a client interacting with a fully encrypted database. + // This plaintext override means: + // - Write: Items are forbidden to be written as plaintext. + // Items will be written as encrypted items. + // - Read: Items are forbidden to be read as plaintext. + // Items will be read as encrypted items. + + listOfTableConfigs := configureTable(kmsKeyID, ddbTableName, dbesdkdynamodbencryptiontypes.PlaintextOverrideForbidPlaintextWriteForbidPlaintextRead) + + // 2. Create DynamoDB client with dbEsdkMiddleware + dbEsdkMiddleware, err := dbesdkmiddleware.NewDBEsdkMiddleware(listOfTableConfigs) + utils.HandleError(err) + + ddb := dynamodb.NewFromConfig(cfg, dbEsdkMiddleware.CreateMiddleware()) + + // 3. Put an item into your table. + // This item will be encrypted. + encryptedAndSignedValue := "this will be encrypted and signed" + signOnlyValue := "this will never be encrypted, but it will be signed" + doNothingValue := "this will never be encrypted nor signed" + item := map[string]types.AttributeValue{ + "partition_key": &types.AttributeValueMemberS{Value: partitionKeyValue}, + "sort_key": &types.AttributeValueMemberN{Value: sortKeyWriteValue}, + "attribute1": &types.AttributeValueMemberS{Value: encryptedAndSignedValue}, + "attribute2": &types.AttributeValueMemberS{Value: signOnlyValue}, + "attribute3": &types.AttributeValueMemberS{Value: doNothingValue}, + } + + putInput := dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: item, + } + + _, err = ddb.PutItem(context.TODO(), &putInput) + // We return this error because we run test against the error. + // When used in production code, you can decide how you want to handle errors. + if err != nil { + return err + } + + // 4. Get an item back from the table using the DynamoDb Client. + // If this is an item written in plaintext (i.e. any item written + // during Step 0 or 1), then the read will fail, as we have + // configured our client to forbid reading plaintext items. + // If this is an item that was encrypted client-side (i.e. any item written + // during Step 2 or after), then the item will be decrypted client-side + // and surfaced as a plaintext item. + key := map[string]types.AttributeValue{ + "partition_key": &types.AttributeValueMemberS{Value: partitionKeyValue}, + "sort_key": &types.AttributeValueMemberN{Value: sortKeyReadValue}, + } + + getInput := &dynamodb.GetItemInput{ + TableName: &ddbTableName, + Key: key, + ConsistentRead: aws.Bool(true), + } + + result, err := ddb.GetItem(context.TODO(), getInput) + // We return this error because we run test against the error. + // When used in production code, you can decide how you want to handle errors. + if err != nil { + return err + } + + // Verify we got the expected item back + err = plaintexttoawsdbe.VerifyReturnedItem(result, partitionKeyValue, sortKeyReadValue, encryptedAndSignedValue, signOnlyValue, doNothingValue) + if err != nil { + return err + } + fmt.Println("MigrationStep3 completed successfully") + return nil +} diff --git a/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/step3_test.go b/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/step3_test.go new file mode 100644 index 000000000..276b23214 --- /dev/null +++ b/Examples/runtimes/go/migration/PlaintextToAWSDBE/awsdbe/step3_test.go @@ -0,0 +1,49 @@ +package awsdbe + +import ( + "testing" + + "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/examples/migration/PlaintextToAWSDBE/plaintext" + "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/examples/utils" + "github.com/google/uuid" +) + +func TestMigrationStep3(t *testing.T) { + kmsKeyID := utils.KmsKeyID() + tableName := utils.DdbTableName() + partitionKey := uuid.New().String() + sortKeys := []string{"0", "1", "2", "3"} + + // Successfully executes Step 3 + err := MigrationStep3(kmsKeyID, tableName, partitionKey, sortKeys[3], sortKeys[3]) + utils.HandleError(err) + + // Given: Step 0 has succeeded + err = plaintext.MigrationStep0(tableName, partitionKey, sortKeys[0], sortKeys[0]) + utils.HandleError(err) + + // When: Execute Step 3 with sortReadValue=0, Then: should panic (cannot read plaintext values) + err = MigrationStep3(kmsKeyID, tableName, partitionKey, sortKeys[3], sortKeys[0]) + utils.AssertServiceError(err, "DynamoDB", "GetItem", "Encrypted item missing expected header and footer attributes") + + // Given: Step 1 has succeeded + err = MigrationStep1(kmsKeyID, tableName, partitionKey, sortKeys[1], sortKeys[1]) + utils.HandleError(err) + + // When: Execute Step 3 with sortReadValue=1, Then: should error out (cannot read plaintext values) + err = MigrationStep3(kmsKeyID, tableName, partitionKey, sortKeys[3], sortKeys[1]) + utils.AssertServiceError(err, "DynamoDB", "GetItem", "Encrypted item missing expected header and footer attributes") + + // Given: Step 2 has succeeded + err = MigrationStep2(kmsKeyID, tableName, partitionKey, sortKeys[2], sortKeys[2]) + utils.HandleError(err) + + // When: Execute Step 3 with sortReadValue=2, Then: Success (can read encrypted values) + err = MigrationStep3(kmsKeyID, tableName, partitionKey, sortKeys[3], sortKeys[2]) + utils.HandleError(err) + + // Cleanup + for _, sortKey := range sortKeys { + utils.DeleteItem(tableName, "partition_key", partitionKey, "sort_key", sortKey) + } +} diff --git a/Examples/runtimes/go/migration/PlaintextToAWSDBE/migrationutils.go b/Examples/runtimes/go/migration/PlaintextToAWSDBE/migrationutils.go new file mode 100644 index 000000000..c060048f4 --- /dev/null +++ b/Examples/runtimes/go/migration/PlaintextToAWSDBE/migrationutils.go @@ -0,0 +1,58 @@ +package plaintexttoawsdbe + +import ( + "fmt" + + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +func VerifyReturnedItem(result *dynamodb.GetItemOutput, partitionKeyValue, sortKeyValue, encryptedAndSignedValue, signOnlyValue, doNothingValue string) error { + returnedPartitionKey, ok := result.Item["partition_key"].(*types.AttributeValueMemberS) + if !ok { + // We return this error because we run test against the error. + // When used in production code, you can decide how you want to handle errors. + return fmt.Errorf("partition_key is not a string attribute") + } + returnedsortKey, ok := result.Item["sort_key"].(*types.AttributeValueMemberN) + if !ok { + // We return this error because we run test against the error. + // When used in production code, you can decide how you want to handle errors. + return fmt.Errorf("sort_key is not a number attribute") + } + returnedAttribute1, ok := result.Item["attribute1"].(*types.AttributeValueMemberS) + if !ok { + // We return this error because we run test against the error. + // When used in production code, you can decide how you want to handle errors. + return fmt.Errorf("attribute1 is not a string attribute") + } + returnedAttribute2, ok := result.Item["attribute2"].(*types.AttributeValueMemberS) + if !ok { + // We return this error because we run test against the error. + // When used in production code, you can decide how you want to handle errors. + return fmt.Errorf("attribute2 is not a string attribute") + } + returnedAttribute3, ok := result.Item["attribute3"].(*types.AttributeValueMemberS) + if !ok { + // We return this error because we run test against the error. + // When used in production code, you can decide how you want to handle errors. + return fmt.Errorf("attribute3 is not a string attribute") + } + + if returnedPartitionKey.Value != partitionKeyValue { + panic(fmt.Sprintf("Expected partition key %s, got %s", partitionKeyValue, returnedPartitionKey)) + } + if returnedsortKey.Value != sortKeyValue { + panic(fmt.Sprintf("Expected partition key %s, got %s", sortKeyValue, returnedsortKey)) + } + if returnedAttribute1.Value != encryptedAndSignedValue { + panic(fmt.Sprintf("Expected attribute1 value %s, got %s", encryptedAndSignedValue, returnedAttribute1.Value)) + } + if returnedAttribute2.Value != signOnlyValue { + panic(fmt.Sprintf("Expected attribute2 value %s, got %s", signOnlyValue, returnedAttribute2.Value)) + } + if returnedAttribute3.Value != doNothingValue { + panic(fmt.Sprintf("Expected attribute3 value %s, got %s", doNothingValue, returnedAttribute3.Value)) + } + return nil +} diff --git a/Examples/runtimes/go/migration/PlaintextToAWSDBE/plaintext/step0.go b/Examples/runtimes/go/migration/PlaintextToAWSDBE/plaintext/step0.go new file mode 100644 index 000000000..b2b57410c --- /dev/null +++ b/Examples/runtimes/go/migration/PlaintextToAWSDBE/plaintext/step0.go @@ -0,0 +1,93 @@ +package plaintext + +import ( + "context" + "fmt" + + plaintexttoawsdbe "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/examples/migration/PlaintextToAWSDBE" + "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/examples/utils" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +/* +Migration Step 0: This is the pre-migration step for the +plaintext-to-encrypted database migration, and is the starting +state for our migration from a plaintext database to a +client-side encrypted database encrypted using the +AWS Database Encryption SDK for DynamoDb. + +In this example, we configure a DynamoDbClient to +write a plaintext record to a table and read that record. +This emulates the starting state of a plaintext-to-encrypted +database migration; i.e. a plaintext database you can +read and write to with the DynamoDbClient. + +Running this example requires access to the DDB Table whose name +is provided in the function parameter. +This table must be configured with the following +primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) +*/ +func MigrationStep0(ddbTableName, partitionKeyValue, sortKeyWriteValue, sortKeyReadValue string) error { + // 1. Create a standard DynamoDB client + cfg, err := config.LoadDefaultConfig(context.TODO()) + utils.HandleError(err) + ddb := dynamodb.NewFromConfig(cfg) + + // 2. Put an example item into DynamoDB table + // This item will be stored in plaintext. + encryptedAndSignedValue := "this will be encrypted and signed" + signOnlyValue := "this will never be encrypted, but it will be signed" + doNothingValue := "this will never be encrypted nor signed" + item := map[string]types.AttributeValue{ + "partition_key": &types.AttributeValueMemberS{Value: partitionKeyValue}, + "sort_key": &types.AttributeValueMemberN{Value: sortKeyWriteValue}, + "attribute1": &types.AttributeValueMemberS{Value: encryptedAndSignedValue}, + "attribute2": &types.AttributeValueMemberS{Value: signOnlyValue}, + "attribute3": &types.AttributeValueMemberS{Value: doNothingValue}, + } + + putInput := &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: item, + } + _, err = ddb.PutItem(context.TODO(), putInput) + utils.HandleError(err) + + // 3. Get an item back from the table as it was written. + // If this is an item written in plaintext (i.e. any item written + // during Step 0 or 1), then the item will still be in plaintext + // and will be able to be processed. + // If this is an item that was encrypted client-side (i.e. any item written + // during Step 2 or after), then the item will still be encrypted client-side + // and will be unable to be processed in your code. To decrypt and process + // client-side encrypted items, you will need to configure encrypted reads on + // your dynamodb client (this is configured from Step 1 onwards). + key := map[string]types.AttributeValue{ + "partition_key": &types.AttributeValueMemberS{Value: partitionKeyValue}, + "sort_key": &types.AttributeValueMemberN{Value: sortKeyReadValue}, + } + + getInput := &dynamodb.GetItemInput{ + TableName: &ddbTableName, + Key: key, + } + result, err := ddb.GetItem(context.TODO(), getInput) + utils.HandleError(err) + + // 4. Verify we get the expected item back + if result.Item == nil { + panic("No item found") + } + + err = plaintexttoawsdbe.VerifyReturnedItem(result, partitionKeyValue, sortKeyReadValue, encryptedAndSignedValue, signOnlyValue, doNothingValue) + if err != nil { + return err + } + + fmt.Println("MigrationStep0 completed successfully") + return nil +} diff --git a/Examples/runtimes/go/migration/PlaintextToAWSDBE/plaintext/step0_test.go b/Examples/runtimes/go/migration/PlaintextToAWSDBE/plaintext/step0_test.go new file mode 100644 index 000000000..b54382a81 --- /dev/null +++ b/Examples/runtimes/go/migration/PlaintextToAWSDBE/plaintext/step0_test.go @@ -0,0 +1,47 @@ +package plaintext + +import ( + "testing" + + "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/examples/migration/PlaintextToAWSDBE/awsdbe" + "github.com/aws/aws-database-encryption-sdk-dynamodb/releases/go/dynamodb-esdk/examples/utils" + "github.com/google/uuid" +) + +func TestMigrationStep0(t *testing.T) { + kmsKeyID := utils.KmsKeyID() + tableName := utils.DdbTableName() + partitionKey := uuid.New().String() + sortKeys := []string{"0", "1", "2", "3"} + + // Successfully executes step 0 + err := MigrationStep0(tableName, partitionKey, sortKeys[0], sortKeys[0]) + utils.HandleError(err) + + // Given: Step 1 has succeeded + err = awsdbe.MigrationStep1(kmsKeyID, tableName, partitionKey, sortKeys[1], sortKeys[1]) + utils.HandleError(err) + + // When: Execute Step 0 with sortReadValue=1, Then: Success (i.e. can read plaintext values) + err = MigrationStep0(tableName, partitionKey, sortKeys[0], sortKeys[1]) + utils.HandleError(err) + + // Given: Step 2 has succeeded + err = awsdbe.MigrationStep2(kmsKeyID, tableName, partitionKey, sortKeys[2], sortKeys[2]) + utils.HandleError(err) + + // When: Execute Step 0 with sortReadValue=2, Then: should error out when reading encrypted items. + err = MigrationStep0(tableName, partitionKey, sortKeys[0], sortKeys[2]) + utils.AssertErrorMessage(err, "attribute1 is not a string attribute") + + // Given: Step 3 has succeeded (if it exists) + awsdbe.MigrationStep3(kmsKeyID, tableName, partitionKey, sortKeys[3], sortKeys[3]) + // When: Execute Step 0 with sortReadValue=3, Then: should error out + err = MigrationStep0(tableName, partitionKey, sortKeys[0], sortKeys[3]) + utils.AssertErrorMessage(err, "attribute1 is not a string attribute") + + // Cleanup + for _, sortKey := range sortKeys { // Only clean up items we created + utils.DeleteItem(tableName, "partition_key", partitionKey, "sort_key", sortKey) + } +} diff --git a/Examples/runtimes/go/utils/exampleUtils.go b/Examples/runtimes/go/utils/exampleUtils.go index 34b5aa47a..763d2cfe6 100644 --- a/Examples/runtimes/go/utils/exampleUtils.go +++ b/Examples/runtimes/go/utils/exampleUtils.go @@ -4,8 +4,17 @@ package utils import ( + "context" "crypto/rand" + "errors" "os" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/aws/smithy-go" ) const ( @@ -124,6 +133,33 @@ func HandleError(err error) { } } +func AssertServiceError(err error, service string, operation string, errorMessage string) { + if err == nil { + panic("Expected error but got no error") + } + var oe *smithy.OperationError + if errors.As(err, &oe) { + if oe.Service() != service { + panic("Expected service to be: " + service + " but got: " + oe.Service()) + } + if oe.Operation() != operation { + panic("Expected Operation to be: " + operation + " but got: " + oe.Operation()) + } + if !strings.Contains(oe.Unwrap().Error(), errorMessage) { + panic("Expected message to contain: " + errorMessage + " but got: " + oe.Unwrap().Error()) + } + } +} + +func AssertErrorMessage(err error, expectedMessage string) { + if err == nil { + panic("Expected error but got no error") + } + if !strings.Contains(err.Error(), expectedMessage) { + panic("Expected error containing: `" + expectedMessage + "` but got:" + err.Error()) + } +} + func GenerateAes256KeyBytes() []byte { key := make([]byte, aesKeyBytes) // crypto/rand is used here for demonstration. @@ -137,3 +173,32 @@ func FileExists(filename string) bool { _, err := os.Stat(filename) return !os.IsNotExist(err) } + +func DeleteItem( + tableName string, + partitionKeyName string, + partitionKeyValue string, + sortKeyName string, + sortKeyValue string, +) { + cfg, err := config.LoadDefaultConfig(context.TODO()) + HandleError(err) + + // Create DynamoDB client + client := dynamodb.NewFromConfig(cfg) + // Build the key attributes map with both partition and sort keys + key := map[string]types.AttributeValue{ + partitionKeyName: &types.AttributeValueMemberS{Value: partitionKeyValue}, + sortKeyName: &types.AttributeValueMemberN{Value: sortKeyValue}, + } + + // Create the DeleteItem input + input := &dynamodb.DeleteItemInput{ + TableName: aws.String(tableName), + Key: key, + } + + // Execute the DeleteItem operation + _, err = client.DeleteItem(context.TODO(), input) + HandleError(err) +} diff --git a/Examples/runtimes/java/Migration/PlaintextToAWSDBE/README.md b/Examples/runtimes/java/Migration/PlaintextToAWSDBE/README.md index e160362b8..869549e47 100644 --- a/Examples/runtimes/java/Migration/PlaintextToAWSDBE/README.md +++ b/Examples/runtimes/java/Migration/PlaintextToAWSDBE/README.md @@ -1,10 +1,10 @@ -# DyanmoDb Encryption Client to AWS Database Encryption SDK for DynamoDb Migration +# Plaintext DynamoDB Table to AWS Database Encryption SDK Encrypted Table Migration This projects demonstrates the steps necessary to migrate to the AWS Database Encryption SDK for DynamoDb from a plaintext database. -[Step 0](Desktop/workplace/aws-dynamodb-encryption-dafny/Examples/runtimes/java/Migration/PlaintextToAWSDBE/DDBEC/README.md) demonstrates the starting state for your system. +[Step 0](./src/main/java/software/amazon/cryptography/examples/plaintext/MigrationExampleStep0.java) demonstrates the starting state for your system. ## Step 1 diff --git a/Examples/runtimes/java/Migration/PlaintextToAWSDBE/src/main/java/software/amazon/cryptography/examples/awsdbe/MigrationExampleStep2.java b/Examples/runtimes/java/Migration/PlaintextToAWSDBE/src/main/java/software/amazon/cryptography/examples/awsdbe/MigrationExampleStep2.java index fda66dfc1..278cf6509 100644 --- a/Examples/runtimes/java/Migration/PlaintextToAWSDBE/src/main/java/software/amazon/cryptography/examples/awsdbe/MigrationExampleStep2.java +++ b/Examples/runtimes/java/Migration/PlaintextToAWSDBE/src/main/java/software/amazon/cryptography/examples/awsdbe/MigrationExampleStep2.java @@ -135,7 +135,7 @@ public static void MigrationStep2( // during Step 0 or 1), then the item will still be in plaintext. // If this is an item that was encrypted client-side (i.e. any item written // during Step 2 or after), then the DDB enhanced client will decrypt the - // item client-sid and surface it in our code as a plaintext item. + // item client-side and surface it in our code as a plaintext item. SimpleClass itemToGet = new SimpleClass(); itemToGet.setPartitionKey(partitionKey); itemToGet.setSortKey(sortReadValue);