diff --git a/Examples/runtimes/go/main.go b/Examples/runtimes/go/main.go index cb1645bce..bc67a793e 100644 --- a/Examples/runtimes/go/main.go +++ b/Examples/runtimes/go/main.go @@ -7,6 +7,7 @@ import ( "github.com/aws/aws-database-encryption-sdk-dynamodb/examples/itemencryptor" "github.com/aws/aws-database-encryption-sdk-dynamodb/examples/keyring" "github.com/aws/aws-database-encryption-sdk-dynamodb/examples/misc" + "github.com/aws/aws-database-encryption-sdk-dynamodb/examples/searchableencryption" "github.com/aws/aws-database-encryption-sdk-dynamodb/examples/utils" ) @@ -61,4 +62,26 @@ func main() { itemencryptor.ItemEncryptDecryptExample( utils.KmsKeyID(), utils.DdbTableName()) + + // searchable encryption example + searchableencryption.BasicSearchableEncryptionExample( + utils.UnitInspectionTestDdbTableName(), + branchKey1, + utils.TestKeystoreKmsKeyId(), + utils.TestKeystoreName()) + searchableencryption.BeaconStylesSearchableEncryptionExample( + utils.UnitInspectionTestDdbTableName(), + branchKey1, + utils.TestKeystoreKmsKeyId(), + utils.TestKeystoreName()) + searchableencryption.CompoundBeaconSearchableEncryptionExample( + utils.UnitInspectionTestDdbTableName(), + branchKey2, + utils.TestKeystoreKmsKeyId(), + utils.TestKeystoreName()) + searchableencryption.VirtualBeaconSearchableEncryptionExample( + utils.SimpleBeaconTestDdbTableName(), + branchKey2, + utils.TestKeystoreKmsKeyId(), + utils.TestKeystoreName()) } diff --git a/Examples/runtimes/go/searchableencryption/basicsearchableencryptionexample.go b/Examples/runtimes/go/searchableencryption/basicsearchableencryptionexample.go new file mode 100644 index 000000000..ee0cbaf7a --- /dev/null +++ b/Examples/runtimes/go/searchableencryption/basicsearchableencryptionexample.go @@ -0,0 +1,367 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package searchableencryption + +import ( + "context" + "fmt" + "time" + + keystoreclient "github.com/aws/aws-cryptographic-material-providers-library/releases/go/mpl/awscryptographykeystoresmithygenerated" + keystoretypes "github.com/aws/aws-cryptographic-material-providers-library/releases/go/mpl/awscryptographykeystoresmithygeneratedtypes" + 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/awscryptographydbencryptionsdkdynamodbsmithygeneratedtypes" + dbesdkstructuredencryptiontypes "github.com/aws/aws-database-encryption-sdk-dynamodb/awscryptographydbencryptionsdkstructuredencryptionsmithygeneratedtypes" + "github.com/aws/aws-database-encryption-sdk-dynamodb/dbesdkmiddleware" + "github.com/aws/aws-database-encryption-sdk-dynamodb/examples/utils" + + "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-sdk-go-v2/service/kms" +) + +/* +This example demonstrates how to set up a beacon on an encrypted attribute, +put an item with the beacon, and query against that beacon. +This example follows a use case of a database that stores unit inspection information. + +Running this example requires access to a DDB table with the +following key configuration: + - Partition key is named "work_id" with type (S) + - Sort key is named "inspection_date" with type (S) + +This table must have a Global Secondary Index (GSI) configured named "last4-unit-index": + - Partition key is named "aws_dbe_b_inspector_id_last4" with type (S) + - Sort key is named "aws_dbe_b_unit" with type (S) + +In this example for storing unit inspection information, this schema is utilized for the data: + - "work_id" stores a unique identifier for a unit inspection work order (v4 UUID) + - "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD) + - "inspector_id_last4" stores the last 4 digits of the ID of the inspector performing the work + - "unit" stores a 12-digit serial number for the unit being inspected + +The example requires the following ordered input command line parameters: + 1. DDB table name for table to put/query data from + 2. Branch key ID for a branch key that was previously created in your key store. See the + CreateKeyStoreKeyExample. + 3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID + provided in arg 2 + 4. Branch key DDB table name for the DDB table representing the branch key store +*/ +func BasicSearchableEncryptionExample( + ddbTableName, + branchKeyId, + branchKeyWrappingKmsKeyArn, + branchKeyDdbTableName string) { + const gsiName = "last4-unit-index" + partitionKeyName := "work_id" + sortKeyName := "inspection_date" + // 1. Configure Beacons. + // The beacon name must be the name of a table attribute that will be encrypted. + // The `length` parameter dictates how many bits are in the beacon attribute value. + // The following link provides guidance on choosing a beacon length: + // https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html + + // The configured DDB table has a GSI on the `aws_dbe_b_inspector_id_last4` AttributeName. + // This field holds the last 4 digits of an inspector ID. + // For our example, this field may range from 0 to 9,999 (10,000 possible values). + // For our example, we assume a full inspector ID is an integer + // ranging from 0 to 99,999,999. We do not assume that the full inspector ID's + // values are uniformly distributed across its range of possible values. + // In many use cases, the prefix of an identifier encodes some information + // about that identifier (e.g. zipcode and SSN prefixes encode geographic + // information), while the suffix does not and is more uniformly distributed. + // We will assume that the inspector ID field matches a similar use case. + // So for this example, we only store and use the last + // 4 digits of the inspector ID, which we assume is uniformly distributed. + // Since the full ID's range is divisible by the range of the last 4 digits, + // then the last 4 digits of the inspector ID are uniformly distributed + // over the range from 0 to 9,999. + // See our documentation for why you should avoid creating beacons over non-uniform distributions + // https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/searchable-encryption.html#are-beacons-right-for-me + // A single inspector ID suffix may be assigned to multiple `work_id`s. + // + // This link provides guidance for choosing a beacon length: + // https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html + // We follow the guidance in the link above to determine reasonable bounds + // for the length of a beacon on the last 4 digits of an inspector ID: + // - min: log(sqrt(10,000))/log(2) ~= 6.6, round up to 7 + // - max: log((10,000/2))/log(2) ~= 12.3, round down to 12 + // You will somehow need to round results to a nearby integer. + // We choose to round to the nearest integer; you might consider a different rounding approach. + // Rounding up will return fewer expected "false positives" in queries, + // leading to fewer decrypt calls and better performance, + // but it is easier to identify which beacon values encode distinct plaintexts. + // Rounding down will return more expected "false positives" in queries, + // leading to more decrypt calls and worse performance, + // but it is harder to identify which beacon values encode distinct plaintexts. + // We can choose a beacon length between 7 and 12: + // - Closer to 7, we expect more "false positives" to be returned, + // making it harder to identify which beacon values encode distinct plaintexts, + // but leading to more decrypt calls and worse performance + // - Closer to 12, we expect fewer "false positives" returned in queries, + // leading to fewer decrypt calls and better performance, + // but it is easier to identify which beacon values encode distinct plaintexts. + // As an example, we will choose 10. + // + // Values stored in aws_dbe_b_inspector_id_last4 will be 10 bits long (0x000 - 0x3ff) + // There will be 2^10 = 1,024 possible HMAC values. + // With a sufficiently large number of well-distributed inspector IDs, + // for a particular beacon we expect (10,000/1,024) ~= 9.8 4-digit inspector ID suffixes + // sharing that beacon value. + last4Beacon := dbesdkdynamodbencryptiontypes.StandardBeacon{ + Name: "inspector_id_last4", + Length: 10, + } + + // The configured DDB table has a GSI on the `aws_dbe_b_unit` AttributeName. + // This field holds a unit serial number. + // For this example, this is a 12-digit integer from 0 to 999,999,999,999 (10^12 possible values). + // We will assume values for this attribute are uniformly distributed across this range. + // A single unit serial number may be assigned to multiple `work_id`s. + // + // This link provides guidance for choosing a beacon length: + // https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html + // We follow the guidance in the link above to determine reasonable bounds + // for the length of a beacon on a unit serial number: + // - min: log(sqrt(999,999,999,999))/log(2) ~= 19.9, round up to 20 + // - max: log((999,999,999,999/2))/log(2) ~= 38.9, round up to 39 + // We can choose a beacon length between 20 and 39: + // - Closer to 20, we expect more "false positives" to be returned, + // making it harder to identify which beacon values encode distinct plaintexts, + // but leading to more decrypt calls and worse performance + // - Closer to 39, we expect fewer "false positives" returned in queries, + // leading to fewer decrypt calls and better performance, + // but it is easier to identify which beacon values encode distinct plaintexts. + // As an example, we will choose 30. + // + // Values stored in aws_dbe_b_unit will be 30 bits long (0x00000000 - 0x3fffffff) + // There will be 2^30 = 1,073,741,824 ~= 1.1B possible HMAC values. + // With a sufficiently large number of well-distributed inspector IDs, + // for a particular beacon we expect (10^12/2^30) ~= 931.3 unit serial numbers + // sharing that beacon value. + unitBeacon := dbesdkdynamodbencryptiontypes.StandardBeacon{ + Name: "unit", + Length: 30, + } + + standardBeaconList := []dbesdkdynamodbencryptiontypes.StandardBeacon{last4Beacon, unitBeacon} + + // 2. Configure Keystore. + // The keystore is a separate DDB table where the client stores encryption and decryption materials. + // In order to configure beacons on the DDB client, you must configure a keystore. + // + // This example expects that you have already set up a KeyStore with a single branch key. + // See the "Create KeyStore Table Example" and "Create KeyStore Key Example" for how to do this. + // After you create a branch key, you should persist its ID for use in this example. + cfg, err := config.LoadDefaultConfig(context.TODO()) + utils.HandleError(err) + + kmsClient := kms.NewFromConfig(cfg) + ddbClient := dynamodb.NewFromConfig(cfg) + + kmsConfig := keystoretypes.KMSConfigurationMemberkmsKeyArn{ + Value: branchKeyWrappingKmsKeyArn, + } + keyStoreConfig := keystoretypes.KeyStoreConfig{ + KmsClient: kmsClient, + DdbClient: ddbClient, + DdbTableName: branchKeyDdbTableName, + LogicalKeyStoreName: branchKeyDdbTableName, + KmsConfiguration: &kmsConfig, + } + + keyStore, err := keystoreclient.NewClient(keyStoreConfig) + utils.HandleError(err) + + // 3. Create BeaconVersion. + // The BeaconVersion inside the list holds the list of beacons on the table. + // The BeaconVersion also stores information about the keystore. + // BeaconVersion must be provided: + // - keyStore: The keystore configured in step 2. + // - keySource: A configuration for the key source. + // For simple use cases, we can configure a 'singleKeySource' which + // statically configures a single beaconKey. That is the approach this example takes. + // For use cases where you want to use different beacon keys depending on the data + // (for example if your table holds data for multiple tenants, and you want to use + // a different beacon key per tenant), look into configuring a MultiKeyStore: + // https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/searchable-encryption-multitenant.html + ttl := 6000 + cacheTTL := int32(ttl) + singleKeyStore := dbesdkdynamodbencryptiontypes.SingleKeyStore{ + // `keyId` references a beacon key. + // For every branch key we create in the keystore, + // we also create a beacon key. + // This beacon key is not the same as the branch key, + // but is created with the same ID as the branch key. + KeyId: branchKeyId, + CacheTTL: cacheTTL, + } + beaconKeySource := dbesdkdynamodbencryptiontypes.BeaconKeySourceMembersingle{ + Value: singleKeyStore, + } + beaconVersion := dbesdkdynamodbencryptiontypes.BeaconVersion{ + StandardBeacons: standardBeaconList, + Version: 1, // MUST be 1 + KeyStore: keyStore, + KeySource: &beaconKeySource, + } + + beaconVersions := []dbesdkdynamodbencryptiontypes.BeaconVersion{beaconVersion} + + // 4. Create a Hierarchical Keyring + // This is a KMS keyring that utilizes the keystore table. + // This config defines how items are encrypted and decrypted. + // NOTE: You should configure this to use the same keystore as your search config. + matProv, err := mpl.NewClient(mpltypes.MaterialProvidersConfig{}) + utils.HandleError(err) + + ttlSeconds := int64(ttl) + keyringInput := mpltypes.CreateAwsKmsHierarchicalKeyringInput{ + BranchKeyId: &branchKeyId, + KeyStore: keyStore, + TtlSeconds: ttlSeconds, + } + kmsKeyring, err := matProv.CreateAwsKmsHierarchicalKeyring(context.Background(), keyringInput) + utils.HandleError(err) + + // 5. 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 + // Any attributes that will be used in beacons must be configured as ENCRYPT_AND_SIGN. + attributeActionsOnEncrypt := map[string]dbesdkstructuredencryptiontypes.CryptoAction{ + partitionKeyName: dbesdkstructuredencryptiontypes.CryptoActionSignOnly, // Our partition attribute must be SIGN_ONLY + sortKeyName: dbesdkstructuredencryptiontypes.CryptoActionSignOnly, // Our sort attribute must be SIGN_ONLY + "inspector_id_last4": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, // Beaconized attributes must be encrypted + "unit": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, // Beaconized attributes must be encrypted + } + + // 6. Create the DynamoDb Encryption configuration for the table we will be writing to. + // The beaconVersions are added to the search configuration. + writeVersion := int32(1) + searchConfig := dbesdkdynamodbencryptiontypes.SearchConfig{ + WriteVersion: writeVersion, // MUST be 1 + Versions: beaconVersions, + } + + tableConfig := dbesdkdynamodbencryptiontypes.DynamoDbTableEncryptionConfig{ + LogicalTableName: ddbTableName, + PartitionKeyName: partitionKeyName, + SortKeyName: &sortKeyName, + AttributeActionsOnEncrypt: attributeActionsOnEncrypt, + Keyring: kmsKeyring, + Search: &searchConfig, + } + + tableConfigs := map[string]dbesdkdynamodbencryptiontypes.DynamoDbTableEncryptionConfig{ + ddbTableName: tableConfig, + } + + // 7. Create the DynamoDb Encryption Interceptor + encryptionConfig := dbesdkdynamodbencryptiontypes.DynamoDbTablesEncryptionConfig{ + TableEncryptionConfigs: tableConfigs, + } + + // 8. Create a new AWS SDK DynamoDb client + dbEsdkMiddleware, err := dbesdkmiddleware.NewDBEsdkMiddleware(encryptionConfig) + utils.HandleError(err) + ddb := dynamodb.NewFromConfig(cfg, dbEsdkMiddleware.CreateMiddleware()) + + // 9. Put an item into our table using the above client. + // Before the item gets sent to DynamoDb, it will be encrypted + // client-side, according to our configuration. + // Since our configuration includes beacons for `inspector_id_last4` and `unit`, + // the client will add two additional attributes to the item. These attributes will have names + // `aws_dbe_b_inspector_id_last4` and `aws_dbe_b_unit`. Their values will be HMACs + // truncated to as many bits as the beacon's `length` parameter; e.g. + // aws_dbe_b_inspector_id_last4 = truncate(HMAC("4321"), 10) + // aws_dbe_b_unit = truncate(HMAC("123456789012"), 30) + item := map[string]types.AttributeValue{ + partitionKeyName: &types.AttributeValueMemberS{Value: "1313ba89-5661-41eb-ba6c-cb1b4cb67b2d"}, + sortKeyName: &types.AttributeValueMemberS{Value: "2023-06-13"}, + "inspector_id_last4": &types.AttributeValueMemberS{Value: "4321"}, + "unit": &types.AttributeValueMemberS{Value: "123456789012"}, + } + + putRequest := &dynamodb.PutItemInput{ + TableName: aws.String(ddbTableName), + Item: item, + } + + _, err = ddb.PutItem(context.Background(), putRequest) + utils.HandleError(err) + + // 10. Query for the item we just put. + // Note that we are constructing the query as if we were querying on plaintext values. + // However, the DDB encryption client will detect that this attribute name has a beacon configured. + // The client will add the beaconized attribute name and attribute value to the query, + // and transform the query to use the beaconized name and value. + // Internally, the client will query for and receive all items with a matching HMAC value in the beacon field. + // This may include a number of "false positives" with different ciphertext, but the same truncated HMAC. + // e.g. if truncate(HMAC("123456789012"), 30) + // == truncate(HMAC("098765432109"), 30), + // the query will return both items. + // The client will decrypt all returned items to determine which ones have the expected attribute values, + // and only surface items with the correct plaintext to the user. + // This procedure is internal to the client and is abstracted away from the user; + // e.g. the user will only see "123456789012" and never + // "098765432109", though the actual query returned both. + expressionAttributeNames := map[string]string{ + "#last4": "inspector_id_last4", + "#unit": "unit", + } + + expressionAttributeValues := map[string]types.AttributeValue{ + ":last4": &types.AttributeValueMemberS{Value: "4321"}, + ":unit": &types.AttributeValueMemberS{Value: "123456789012"}, + } + + queryRequest := &dynamodb.QueryInput{ + TableName: aws.String(ddbTableName), + IndexName: aws.String(gsiName), + KeyConditionExpression: aws.String("#last4 = :last4 and #unit = :unit"), + ExpressionAttributeNames: expressionAttributeNames, + ExpressionAttributeValues: expressionAttributeValues, + } + + // GSIs do not update instantly + // so if the results come back empty + // we retry after a short sleep + for i := 0; i < 10; i++ { + queryResponse, err := ddb.Query(context.Background(), queryRequest) + utils.HandleError(err) + + attributeValues := queryResponse.Items + + // if no results, sleep and try again + if len(attributeValues) == 0 { + time.Sleep(20 * time.Millisecond) + continue + } + + // Validate only 1 item was returned: the item we just put + if len(attributeValues) != 1 { + panic(fmt.Sprintf("Expected 1 item, got %d", len(attributeValues))) + } + returnedItem := attributeValues[0] + // Validate the item has the expected attributes + inspectorIDLast4 := returnedItem["inspector_id_last4"].(*types.AttributeValueMemberS).Value + unit := returnedItem["unit"].(*types.AttributeValueMemberS).Value + if inspectorIDLast4 != "4321" { + panic(fmt.Sprintf("Expected inspector_id_last4 '4321', got '%s'", inspectorIDLast4)) + } + if unit != "123456789012" { + panic(fmt.Sprintf("Expected unit '123456789012', got '%s'", unit)) + } + break + } + + fmt.Println("Basic Searchable Encryption Example completed successfully") +} diff --git a/Examples/runtimes/go/searchableencryption/beaconstylessearchableencryptionexample.go b/Examples/runtimes/go/searchableencryption/beaconstylessearchableencryptionexample.go new file mode 100644 index 000000000..04dd77df0 --- /dev/null +++ b/Examples/runtimes/go/searchableencryption/beaconstylessearchableencryptionexample.go @@ -0,0 +1,425 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package searchableencryption + +import ( + "context" + "fmt" + + keystoreclient "github.com/aws/aws-cryptographic-material-providers-library/releases/go/mpl/awscryptographykeystoresmithygenerated" + keystoretypes "github.com/aws/aws-cryptographic-material-providers-library/releases/go/mpl/awscryptographykeystoresmithygeneratedtypes" + 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/awscryptographydbencryptionsdkdynamodbsmithygeneratedtypes" + dbesdkstructuredencryptiontypes "github.com/aws/aws-database-encryption-sdk-dynamodb/awscryptographydbencryptionsdkstructuredencryptionsmithygeneratedtypes" + "github.com/aws/aws-database-encryption-sdk-dynamodb/dbesdkmiddleware" + "github.com/aws/aws-database-encryption-sdk-dynamodb/examples/utils" + + "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-sdk-go-v2/service/kms" +) + +/* +This example demonstrates how to use Beacons Styles on Standard Beacons on encrypted attributes, + + put an item with the beacon, and query against that beacon. + +This example follows a use case of a database that stores food information. + + This is an extension of the "BasicSearchableEncryptionExample" in this directory + and uses the same table schema. + +Running this example requires access to a DDB table with the +following key configuration: + - Partition key is named partitionKeyName with type (S) + - Sort key is named "inspection_time" with type (S) + +In this example for storing food information, this schema is utilized for the data: + - partitionKeyName stores a unique identifier for a unit inspection work order (v4 UUID) + - sortKeyName stores an ISO 8601 date for the inspection (YYYY-MM-DD) + - "fruit" stores one type of fruit + - "basket" stores a set of types of fruit + - "dessert" stores one type of dessert + - "veggies" stores a set of types of vegetable + - "work_type" stores a unit inspection category + +The example requires the following ordered input command line parameters: + 1. DDB table name for table to put/query data from + 2. Branch key ID for a branch key that was previously created in your key store. See the + CreateKeyStoreKeyExample. + 3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID + provided in arg 2 + 4. Branch key DDB table ARN for the DDB table representing the branch key store +*/ + +func BeaconStylesSearchableEncryptionExample( + ddbTableName, + branchKeyId, + branchKeyWrappingKmsKeyArn, + branchKeyDdbTableName string) { + partitionKeyName := "work_id" + sortKeyName := "inspection_date" + // 1. Create Beacons. + standardBeaconList := []dbesdkdynamodbencryptiontypes.StandardBeacon{ + // The fruit beacon allows searching on the encrypted fruit attribute + // We have selected 30 as an example beacon length, but you should go to + // https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html + // when creating your beacons. + { + Name: "fruit", + Length: 30, + }, + + // The basket beacon allows searching on the encrypted basket attribute + // basket is used as a Set, and therefore needs a beacon style to reflect that. + // Further, we need to be able to compare the items in basket to the fruit attribute + // so we `share` this beacon with `fruit`. + // Since we need both of these things, we use the SharedSet style. + { + Name: "basket", + Length: 30, + Style: &dbesdkdynamodbencryptiontypes.BeaconStyleMembersharedSet{ + Value: dbesdkdynamodbencryptiontypes.SharedSet{ + Other: "fruit", + }, + }, + }, + + // The dessert beacon allows searching on the encrypted dessert attribute + // We need to be able to compare the dessert attribute to the fruit attribute + // so we `share` this beacon with `fruit`. + { + Name: "dessert", + Length: 30, + Style: &dbesdkdynamodbencryptiontypes.BeaconStyleMembershared{ + Value: dbesdkdynamodbencryptiontypes.Shared{ + Other: "fruit", + }, + }, + }, + + // The veggieBeacon allows searching on the encrypted veggies attribute + // veggies is used as a Set, and therefore needs a beacon style to reflect that. + { + Name: "veggies", + Length: 30, + Style: &dbesdkdynamodbencryptiontypes.BeaconStyleMemberasSet{ + Value: dbesdkdynamodbencryptiontypes.AsSet{}, + }, + }, + + // The work_typeBeacon allows searching on the encrypted work_type attribute + // We only use it as part of the compound work_unit beacon, + // so we disable its use as a standalone beacon + { + Name: "work_type", + Length: 30, + Style: &dbesdkdynamodbencryptiontypes.BeaconStyleMemberpartOnly{ + Value: dbesdkdynamodbencryptiontypes.PartOnly{}, + }, + }, + } + + // Here we build a compound beacon from work_id and work_type + // If we had tried to make a StandardBeacon from work_type, we would have seen an error + // because work_type is "PartOnly" + encryptedPartList := []dbesdkdynamodbencryptiontypes.EncryptedPart{ + { + Name: "work_type", + Prefix: "T-", + }, + } + + signedPartList := []dbesdkdynamodbencryptiontypes.SignedPart{ + { + Name: partitionKeyName, + Prefix: "I-", + }, + } + + compoundBeaconList := []dbesdkdynamodbencryptiontypes.CompoundBeacon{ + { + Name: "work_unit", + Split: ".", + Encrypted: encryptedPartList, + Signed: signedPartList, + }, + } + + // 2. Configure the Keystore + // These are the same constructions as in the Basic example, which describes these in more detail. + cfg, err := config.LoadDefaultConfig(context.TODO()) + utils.HandleError(err) + + kmsClient := kms.NewFromConfig(cfg) + ddbClient := dynamodb.NewFromConfig(cfg) + + kmsConfig := keystoretypes.KMSConfigurationMemberkmsKeyArn{ + Value: branchKeyWrappingKmsKeyArn, + } + keyStoreConfig := keystoretypes.KeyStoreConfig{ + KmsClient: kmsClient, + DdbClient: ddbClient, + DdbTableName: branchKeyDdbTableName, + LogicalKeyStoreName: branchKeyDdbTableName, + KmsConfiguration: &kmsConfig, + } + + keyStore, err := keystoreclient.NewClient(keyStoreConfig) + utils.HandleError(err) + + // 3. Create BeaconVersion. + // This is similar to the Basic example + ttl := 6000 + cacheTTL := int32(ttl) + singleKeyStore := dbesdkdynamodbencryptiontypes.SingleKeyStore{ + KeyId: branchKeyId, + CacheTTL: cacheTTL, + } + beaconKeySource := dbesdkdynamodbencryptiontypes.BeaconKeySourceMembersingle{ + Value: singleKeyStore, + } + beaconVersion := dbesdkdynamodbencryptiontypes.BeaconVersion{ + StandardBeacons: standardBeaconList, + CompoundBeacons: compoundBeaconList, + Version: 1, // MUST be 1 + KeyStore: keyStore, + KeySource: &beaconKeySource, + } + + beaconVersions := []dbesdkdynamodbencryptiontypes.BeaconVersion{beaconVersion} + + // 4. Create a Hierarchical Keyring + // This is the same configuration as in the Basic example. + matProv, err := mpl.NewClient(mpltypes.MaterialProvidersConfig{}) + utils.HandleError(err) + + ttlSeconds := int64(ttl) + keyringInput := mpltypes.CreateAwsKmsHierarchicalKeyringInput{ + BranchKeyId: &branchKeyId, + KeyStore: keyStore, + TtlSeconds: ttlSeconds, + } + kmsKeyring, err := matProv.CreateAwsKmsHierarchicalKeyring(context.Background(), keyringInput) + utils.HandleError(err) + + // 5. Configure which attributes are encrypted and/or signed when writing new items. + attributeActionsOnEncrypt := map[string]dbesdkstructuredencryptiontypes.CryptoAction{ + partitionKeyName: dbesdkstructuredencryptiontypes.CryptoActionSignOnly, // Our partition attribute must be SIGN_ONLY + sortKeyName: dbesdkstructuredencryptiontypes.CryptoActionSignOnly, // Our sort attribute must be SIGN_ONLY + "dessert": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, // Beaconized attributes must be encrypted + "fruit": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, // Beaconized attributes must be encrypted + "basket": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, // Beaconized attributes must be encrypted + "veggies": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, // Beaconized attributes must be encrypted + "work_type": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, // Beaconized attributes must be encrypted + } + + // 6. Create the DynamoDb Encryption configuration for the table we will be writing to. + // The beaconVersions are added to the search configuration. + writeVersion := int32(1) + searchConfig := dbesdkdynamodbencryptiontypes.SearchConfig{ + WriteVersion: writeVersion, // MUST be 1 + Versions: beaconVersions, + } + + tableConfig := dbesdkdynamodbencryptiontypes.DynamoDbTableEncryptionConfig{ + LogicalTableName: ddbTableName, + PartitionKeyName: partitionKeyName, + SortKeyName: &sortKeyName, + AttributeActionsOnEncrypt: attributeActionsOnEncrypt, + Keyring: kmsKeyring, + Search: &searchConfig, + } + + tableConfigs := map[string]dbesdkdynamodbencryptiontypes.DynamoDbTableEncryptionConfig{ + ddbTableName: tableConfig, + } + + // 7. Create config + encryptionConfig := dbesdkdynamodbencryptiontypes.DynamoDbTablesEncryptionConfig{ + TableEncryptionConfigs: tableConfigs, + } + + // 8. Create item one, specifically with "dessert != fruit", and "fruit in basket". + item1 := map[string]types.AttributeValue{ + partitionKeyName: &types.AttributeValueMemberS{Value: "1"}, + sortKeyName: &types.AttributeValueMemberS{Value: "2023-06-13"}, + "dessert": &types.AttributeValueMemberS{Value: "cake"}, + "fruit": &types.AttributeValueMemberS{Value: "banana"}, + "basket": &types.AttributeValueMemberSS{Value: []string{"banana", "apple", "pear"}}, + "veggies": &types.AttributeValueMemberSS{Value: []string{"beans", "carrots", "celery"}}, + "work_type": &types.AttributeValueMemberS{Value: "small"}, + } + + // 9. Create item two, specifically with "dessert == fruit", and "fruit not in basket". + item2 := map[string]types.AttributeValue{ + partitionKeyName: &types.AttributeValueMemberS{Value: "2"}, + sortKeyName: &types.AttributeValueMemberS{Value: "2023-06-13"}, + "dessert": &types.AttributeValueMemberS{Value: "orange"}, + "fruit": &types.AttributeValueMemberS{Value: "orange"}, + "basket": &types.AttributeValueMemberSS{Value: []string{"strawberry", "blueberry", "blackberry"}}, + "veggies": &types.AttributeValueMemberSS{Value: []string{"beans", "carrots", "peas"}}, + "work_type": &types.AttributeValueMemberS{Value: "large"}, + } + + // 10. Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor + dbEsdkMiddleware, err := dbesdkmiddleware.NewDBEsdkMiddleware(encryptionConfig) + utils.HandleError(err) + ddb := dynamodb.NewFromConfig(cfg, dbEsdkMiddleware.CreateMiddleware()) + + // 11. Put the two items + putRequest1 := &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: item1, + } + + _, err = ddb.PutItem(context.Background(), putRequest1) + utils.HandleError(err) + + putRequest2 := &dynamodb.PutItemInput{ + TableName: aws.String(ddbTableName), + Item: item2, + } + + _, err = ddb.PutItem(context.Background(), putRequest2) + utils.HandleError(err) + + // 12. Test the first type of Set operation : + // Select records where the basket attribute holds a particular value + expressionAttributeValues := map[string]types.AttributeValue{ + ":value": &types.AttributeValueMemberS{Value: "banana"}, + } + + scanRequest := &dynamodb.ScanInput{ + TableName: aws.String(ddbTableName), + FilterExpression: aws.String("contains(basket, :value)"), + ExpressionAttributeValues: expressionAttributeValues, + } + + scanResponse, err := ddb.Scan(context.Background(), scanRequest) + utils.HandleError(err) + + // Validate only 1 item was returned: item1 + if len(scanResponse.Items) != 1 { + panic(fmt.Sprintf("Expected 1 item, got %d", len(scanResponse.Items))) + } + returnedItem := scanResponse.Items[0] + if returnedItem[partitionKeyName].(*types.AttributeValueMemberS).Value != item1[partitionKeyName].(*types.AttributeValueMemberS).Value { + panic("Expected item1 to be returned") + } + + // 13. Test the second type of Set operation : + // Select records where the basket attribute holds the fruit attribute + scanRequest = &dynamodb.ScanInput{ + TableName: aws.String(ddbTableName), + FilterExpression: aws.String("contains(basket, fruit)"), + } + + scanResponse, err = ddb.Scan(context.Background(), scanRequest) + utils.HandleError(err) + + // Validate only 1 item was returned: item1 + if len(scanResponse.Items) != 1 { + panic(fmt.Sprintf("Expected 1 item, got %d", len(scanResponse.Items))) + } + returnedItem = scanResponse.Items[0] + if returnedItem[partitionKeyName].(*types.AttributeValueMemberS).Value != item1[partitionKeyName].(*types.AttributeValueMemberS).Value { + panic("Expected item1 to be returned") + } + + // 14. Test the third type of Set operation : + // Select records where the fruit attribute exists in a particular set + basket3 := []string{"boysenberry", "orange", "grape"} + expressionAttributeValues = map[string]types.AttributeValue{ + ":value": &types.AttributeValueMemberSS{Value: basket3}, + } + + scanRequest = &dynamodb.ScanInput{ + TableName: aws.String(ddbTableName), + FilterExpression: aws.String("contains(:value, fruit)"), + ExpressionAttributeValues: expressionAttributeValues, + } + + scanResponse, err = ddb.Scan(context.Background(), scanRequest) + utils.HandleError(err) + + // Validate only 1 item was returned: item2 + if len(scanResponse.Items) != 1 { + panic(fmt.Sprintf("Expected 1 item, got %d", len(scanResponse.Items))) + } + returnedItem = scanResponse.Items[0] + if returnedItem[partitionKeyName].(*types.AttributeValueMemberS).Value != item2[partitionKeyName].(*types.AttributeValueMemberS).Value { + panic("Expected item2 to be returned") + } + + // 15. Test a Shared search. Select records where the dessert attribute matches the fruit attribute + scanRequest = &dynamodb.ScanInput{ + TableName: aws.String(ddbTableName), + FilterExpression: aws.String("dessert = fruit"), + } + + scanResponse, err = ddb.Scan(context.Background(), scanRequest) + utils.HandleError(err) + + // Validate only 1 item was returned: item2 + if len(scanResponse.Items) != 1 { + panic(fmt.Sprintf("Expected 1 item, got %d", len(scanResponse.Items))) + } + returnedItem = scanResponse.Items[0] + if returnedItem[partitionKeyName].(*types.AttributeValueMemberS).Value != item2[partitionKeyName].(*types.AttributeValueMemberS).Value { + panic("Expected item2 to be returned") + } + + // 16. Test the AsSet attribute 'veggies' : + // Select records where the veggies attribute holds a particular value + expressionAttributeValues = map[string]types.AttributeValue{ + ":value": &types.AttributeValueMemberS{Value: "peas"}, + } + + scanRequest = &dynamodb.ScanInput{ + TableName: aws.String(ddbTableName), + FilterExpression: aws.String("contains(veggies, :value)"), + ExpressionAttributeValues: expressionAttributeValues, + } + + scanResponse, err = ddb.Scan(context.Background(), scanRequest) + utils.HandleError(err) + + // Validate only 1 item was returned: item2 + if len(scanResponse.Items) != 1 { + panic(fmt.Sprintf("Expected 1 item, got %d", len(scanResponse.Items))) + } + returnedItem = scanResponse.Items[0] + if returnedItem[partitionKeyName].(*types.AttributeValueMemberS).Value != item2[partitionKeyName].(*types.AttributeValueMemberS).Value { + panic("Expected item2 to be returned") + } + + // 17. Test the compound beacon 'work_unit' : + expressionAttributeValues = map[string]types.AttributeValue{ + ":value": &types.AttributeValueMemberS{Value: "I-1.T-small"}, + } + + scanRequest = &dynamodb.ScanInput{ + TableName: aws.String(ddbTableName), + FilterExpression: aws.String("work_unit = :value"), + ExpressionAttributeValues: expressionAttributeValues, + } + + scanResponse, err = ddb.Scan(context.Background(), scanRequest) + utils.HandleError(err) + + // Validate only 1 item was returned: item1 + if len(scanResponse.Items) != 1 { + panic(fmt.Sprintf("Expected 1 item, got %d", len(scanResponse.Items))) + } + returnedItem = scanResponse.Items[0] + if returnedItem[partitionKeyName].(*types.AttributeValueMemberS).Value != item1[partitionKeyName].(*types.AttributeValueMemberS).Value { + panic("Expected item1 to be returned") + } + + fmt.Println("Beacon Styles Searchable Encryption Example completed successfully") +} diff --git a/Examples/runtimes/go/searchableencryption/compoundbeaconsearchableencryptionexample.go b/Examples/runtimes/go/searchableencryption/compoundbeaconsearchableencryptionexample.go new file mode 100644 index 000000000..2e4154895 --- /dev/null +++ b/Examples/runtimes/go/searchableencryption/compoundbeaconsearchableencryptionexample.go @@ -0,0 +1,357 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package searchableencryption + +import ( + "context" + "fmt" + "time" + + keystoreclient "github.com/aws/aws-cryptographic-material-providers-library/releases/go/mpl/awscryptographykeystoresmithygenerated" + keystoretypes "github.com/aws/aws-cryptographic-material-providers-library/releases/go/mpl/awscryptographykeystoresmithygeneratedtypes" + 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/awscryptographydbencryptionsdkdynamodbsmithygeneratedtypes" + dbesdkstructuredencryptiontypes "github.com/aws/aws-database-encryption-sdk-dynamodb/awscryptographydbencryptionsdkstructuredencryptionsmithygeneratedtypes" + "github.com/aws/aws-database-encryption-sdk-dynamodb/dbesdkmiddleware" + "github.com/aws/aws-database-encryption-sdk-dynamodb/examples/utils" + + "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-sdk-go-v2/service/kms" +) + +/* +This example demonstrates how to set up a compound beacon on encrypted attributes, + + put an item with the beacon, and query against that beacon. + +This example follows a use case of a database that stores unit inspection information. + + This is an extension of the "BasicSearchableEncryptionExample" in this directory. + This example uses the same situation (storing unit inspection information) + and the same table schema. + +However, this example uses a different Global Secondary Index (GSI) + + that is based on a compound beacon configuration composed of + the `last4` and `unit` attributes. + +Running this example requires access to a DDB table with the +following key configuration: + - Partition key is named "work_id" with type (S) + - Sort key is named "inspection_time" with type (S) + +This table must have a Global Secondary Index (GSI) configured named "last4UnitCompound-index": + - Partition key is named "aws_dbe_b_last4UnitCompound" with type (S) + +In this example for storing unit inspection information, this schema is utilized for the data: + - "work_id" stores a unique identifier for a unit inspection work order (v4 UUID) + - "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD) + - "inspector_id_last4" stores the last 4 digits of the ID of the inspector performing the work + - "unit" stores a 12-digit serial number for the unit being inspected + +The example requires the following ordered input command line parameters: + 1. DDB table name for table to put/query data from + 2. Branch key ID for a branch key that was previously created in your key store. See the + CreateKeyStoreKeyExample. + 3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID + provided in arg 2 + 4. Branch key DDB table ARN for the DDB table representing the branch key store +*/ +func CompoundBeaconSearchableEncryptionExample( + ddbTableName, + branchKeyID, + branchKeyWrappingKmsKeyArn, + branchKeyDdbTableName string) { + const gsiName = "last4UnitCompound-index" + partitionKeyName := "work_id" + sortKeyName := "inspection_date" + + // 1. Create Beacons. + // These are the same beacons as in the "BasicSearchableEncryptionExample" in this directory. + // See that file to see details on beacon construction and parameters. + // While we will not directly query against these beacons, + // you must create standard beacons on encrypted fields + // that we wish to use in compound beacons. + // We mark them both as PartOnly to enforce the fact that + // we will not directly query against these beacons. + standardBeaconList := []dbesdkdynamodbencryptiontypes.StandardBeacon{ + { + Name: "inspector_id_last4", + Length: 10, + Style: &dbesdkdynamodbencryptiontypes.BeaconStyleMemberpartOnly{ + Value: dbesdkdynamodbencryptiontypes.PartOnly{}, + }, + }, + { + Name: "unit", + Length: 30, + Style: &dbesdkdynamodbencryptiontypes.BeaconStyleMemberpartOnly{ + Value: dbesdkdynamodbencryptiontypes.PartOnly{}, + }, + }, + } + + // 2. Define encrypted parts. + // Encrypted parts define the beacons that can be used to construct a compound beacon, + // and how the compound beacon prefixes those beacon values. + // A encrypted part must receive: + // - name: Name of a standard beacon + // - prefix: Any string. This is plaintext that prefixes the beaconized value in the compound beacon. + // Prefixes must be unique across the configuration, and must not be a prefix of another prefix; + // i.e. for all configured prefixes, the first N characters of a prefix must not equal another prefix. + // In practice, it is suggested to have a short value distinguishable from other parts served on the prefix. + + encryptedPartList := []dbesdkdynamodbencryptiontypes.EncryptedPart{ + // For this example, we will choose "L-" as the prefix for "Last 4 digits of inspector ID". + // With this prefix and the standard beacon's bit length definition (10), the beaconized + // version of the inspector ID's last 4 digits will appear as + // `L-000` to `L-3ff` inside a compound beacon. + { + Name: "inspector_id_last4", + Prefix: "L-", + }, + // For this example, we will choose "U-" as the prefix for "unit". + // With this prefix and the standard beacon's bit length definition (30), a unit beacon will appear + // as `U-00000000` to `U-3fffffff` inside a compound beacon. + { + Name: "unit", + Prefix: "U-", + }, + } + + constructorParts := []dbesdkdynamodbencryptiontypes.ConstructorPart{ + { + Name: "inspector_id_last4", + Required: true, + }, + { + // This name comes from the "unit" standard beacon. + Name: "unit", + Required: true, + }, + } + constructors := []dbesdkdynamodbencryptiontypes.Constructor{ + { + Parts: constructorParts, + }, + } + + // 3. Define compound beacon. + // A compound beacon allows one to serve multiple beacons or attributes from a single index. + // A compound beacon must receive: + // - name: The name of the beacon. Compound beacon values will be written to `aws_ddb_e_[name]`. + // - split: A character separating parts in a compound beacon + // A compound beacon may also receive: + // - encrypted: A list of encrypted parts. This is effectively a list of beacons. We provide the list + // that we created above. + // - constructors: A list of constructors. This is an ordered list of possible ways to create a beacon. + // We have not defined any constructors here; see the complex example for how to do this. + // The client will provide a default constructor, which will write a compound beacon as: + // all signed parts in the order they are added to the signed list; + // all encrypted parts in order they are added to the encrypted list; all parts required. + // In this example, we expect compound beacons to be written as + // `L-XXX.U-YYYYYYYY`, since our encrypted list looks like + // [last4EncryptedPart, unitEncryptedPart]. + // - signed: A list of signed parts, i.e. plaintext attributes. This would be provided if we + // wanted to use plaintext values as part of constructing our compound beacon. We do not + // provide this here; see the Complex example for an example. + compoundBeaconList := []dbesdkdynamodbencryptiontypes.CompoundBeacon{ + { + Name: "last4UnitCompound", + Constructors: constructors, + Split: ".", + }, + } + + // 4. Configure the Keystore + // These are the same constructions as in the Basic example, which describes these in more detail. + cfg, err := config.LoadDefaultConfig(context.TODO()) + utils.HandleError(err) + + kmsClient := kms.NewFromConfig(cfg) + ddbClient := dynamodb.NewFromConfig(cfg) + + kmsConfig := keystoretypes.KMSConfigurationMemberkmsKeyArn{ + Value: branchKeyWrappingKmsKeyArn, + } + keyStoreConfig := keystoretypes.KeyStoreConfig{ + KmsClient: kmsClient, + DdbClient: ddbClient, + DdbTableName: branchKeyDdbTableName, + LogicalKeyStoreName: branchKeyDdbTableName, + KmsConfiguration: &kmsConfig, + } + + keyStore, err := keystoreclient.NewClient(keyStoreConfig) + utils.HandleError(err) + + // 5. Create BeaconVersion. + // This is similar to the Basic example, except we have also provided a compoundBeaconList. + // We must also continue to provide all of the standard beacons that compose a compound beacon list. + ttl := 6000 + cacheTTL := int32(ttl) + singleKeyStore := dbesdkdynamodbencryptiontypes.SingleKeyStore{ + KeyId: branchKeyID, + CacheTTL: cacheTTL, + } + beaconKeySource := dbesdkdynamodbencryptiontypes.BeaconKeySourceMembersingle{ + Value: singleKeyStore, + } + beaconVersion := dbesdkdynamodbencryptiontypes.BeaconVersion{ + EncryptedParts: encryptedPartList, + StandardBeacons: standardBeaconList, + CompoundBeacons: compoundBeaconList, + Version: 1, // MUST be 1 + KeyStore: keyStore, + KeySource: &beaconKeySource, + } + + beaconVersions := []dbesdkdynamodbencryptiontypes.BeaconVersion{beaconVersion} + + // 6. Create a Hierarchical Keyring + // This is the same configuration as in the Basic example. + matProv, err := mpl.NewClient(mpltypes.MaterialProvidersConfig{}) + utils.HandleError(err) + + ttlSeconds := int64(ttl) + keyringInput := mpltypes.CreateAwsKmsHierarchicalKeyringInput{ + BranchKeyId: &branchKeyID, + KeyStore: keyStore, + TtlSeconds: ttlSeconds, + } + kmsKeyring, err := matProv.CreateAwsKmsHierarchicalKeyring(context.Background(), keyringInput) + utils.HandleError(err) + + // 7. Configure which attributes are encrypted and/or signed when writing new items. + attributeActionsOnEncrypt := map[string]dbesdkstructuredencryptiontypes.CryptoAction{ + partitionKeyName: dbesdkstructuredencryptiontypes.CryptoActionSignOnly, // Our partition attribute must be SIGN_ONLY + sortKeyName: dbesdkstructuredencryptiontypes.CryptoActionSignOnly, // Our sort attribute must be SIGN_ONLY + "inspector_id_last4": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, // Beaconized attributes must be encrypted + "unit": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, // Beaconized attributes must be encrypted + } + + // We do not need to define a crypto action on last4UnitCompound. + // We only need to define crypto actions on attributes that we pass to PutItem. + + // 8. Create the DynamoDb Encryption configuration for the table we will be writing to. + // The beaconVersions are added to the search configuration. + writeVersion := int32(1) + searchConfig := dbesdkdynamodbencryptiontypes.SearchConfig{ + WriteVersion: writeVersion, // MUST be 1 + Versions: beaconVersions, + } + + tableConfig := dbesdkdynamodbencryptiontypes.DynamoDbTableEncryptionConfig{ + LogicalTableName: ddbTableName, + PartitionKeyName: partitionKeyName, + SortKeyName: aws.String(sortKeyName), + AttributeActionsOnEncrypt: attributeActionsOnEncrypt, + Keyring: kmsKeyring, + Search: &searchConfig, + } + + tableConfigs := map[string]dbesdkdynamodbencryptiontypes.DynamoDbTableEncryptionConfig{ + ddbTableName: tableConfig, + } + + // 9. Create config + encryptionConfig := dbesdkdynamodbencryptiontypes.DynamoDbTablesEncryptionConfig{ + TableEncryptionConfigs: tableConfigs, + } + + // 10. Create an item with both attributes used in the compound beacon. + item := map[string]types.AttributeValue{ + partitionKeyName: &types.AttributeValueMemberS{Value: "9ce39272-8068-4efd-a211-cd162ad65d4c"}, + sortKeyName: &types.AttributeValueMemberS{Value: "2023-06-13"}, + "inspector_id_last4": &types.AttributeValueMemberS{Value: "5678"}, + "unit": &types.AttributeValueMemberS{Value: "011899988199"}, + } + + // 11. Create the DynamoDb Encryption Interceptor + dbEsdkMiddleware, err := dbesdkmiddleware.NewDBEsdkMiddleware(encryptionConfig) + utils.HandleError(err) + + // 12. Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor above + ddb := dynamodb.NewFromConfig(cfg, dbEsdkMiddleware.CreateMiddleware()) + + putAndQueryItemWithCompoundBeacon(ddb, ddbTableName, item, gsiName) + + fmt.Println("Compound Beacon Searchable Encryption Example completed successfully") +} + +func putAndQueryItemWithCompoundBeacon(ddb *dynamodb.Client, ddbTableName string, item map[string]types.AttributeValue, gsiName string) { + // 13. Write the item to the table + putRequest := &dynamodb.PutItemInput{ + TableName: aws.String(ddbTableName), + Item: item, + } + + _, err := ddb.PutItem(context.Background(), putRequest) + utils.HandleError(err) + + // 14. Query for the item we just put. + expressionAttributeNames := map[string]string{ + "#compound": "last4UnitCompound", + } + + expressionAttributeValues := map[string]types.AttributeValue{ + // This query expression takes a few factors into consideration: + // - The configured prefix for the last 4 digits of an inspector ID is "L-"; + // the prefix for the unit is "U-" + // - The configured split character, separating component parts, is "." + // - The default constructor adds encrypted parts in the order they are in the encrypted list, which + // configures `last4` to come before `unit`` + // NOTE: We did not need to create a compound beacon for this query. This query could have also been + // done by querying on the partition and sort key, as was done in the Basic example. + // This is intended to be a simple example to demonstrate how one might set up a compound beacon. + // For examples where compound beacons are required, see the Complex example. + // The most basic extension to this example that would require a compound beacon would add a third + // part to the compound beacon, then query against three parts. + ":value": &types.AttributeValueMemberS{Value: "L-5678.U-011899988199"}, + } + + queryRequest := &dynamodb.QueryInput{ + TableName: aws.String(ddbTableName), + IndexName: aws.String(gsiName), + KeyConditionExpression: aws.String("#compound = :value"), + ExpressionAttributeNames: expressionAttributeNames, + ExpressionAttributeValues: expressionAttributeValues, + } + + // GSIs do not update instantly + // so if the results come back empty + // we retry after a short sleep + for i := 0; i < 10; i++ { + queryResponse, err := ddb.Query(context.Background(), queryRequest) + utils.HandleError(err) + + attributeValues := queryResponse.Items + + // if no results, sleep and try again + if len(attributeValues) == 0 { + time.Sleep(20 * time.Millisecond) + continue + } + + // Validate only 1 item was returned: the item we just put + if len(attributeValues) != 1 { + panic(fmt.Sprintf("Expected 1 item, got %d", len(attributeValues))) + } + returnedItem := attributeValues[0] + // Validate the item has the expected attributes + inspectorIdLast4 := returnedItem["inspector_id_last4"].(*types.AttributeValueMemberS).Value + unit := returnedItem["unit"].(*types.AttributeValueMemberS).Value + if inspectorIdLast4 != "5678" { + panic(fmt.Sprintf("Expected inspector_id_last4 '5678', got '%s'", inspectorIdLast4)) + } + if unit != "011899988199" { + panic(fmt.Sprintf("Expected unit '011899988199', got '%s'", unit)) + } + break + } +} diff --git a/Examples/runtimes/go/searchableencryption/virtualbeaconsearchableencryptionexample.go b/Examples/runtimes/go/searchableencryption/virtualbeaconsearchableencryptionexample.go new file mode 100644 index 000000000..5410f7bb3 --- /dev/null +++ b/Examples/runtimes/go/searchableencryption/virtualbeaconsearchableencryptionexample.go @@ -0,0 +1,474 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package searchableencryption + +import ( + "context" + "fmt" + "time" + + keystoreclient "github.com/aws/aws-cryptographic-material-providers-library/releases/go/mpl/awscryptographykeystoresmithygenerated" + keystoretypes "github.com/aws/aws-cryptographic-material-providers-library/releases/go/mpl/awscryptographykeystoresmithygeneratedtypes" + 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/awscryptographydbencryptionsdkdynamodbsmithygeneratedtypes" + dbesdktransforms "github.com/aws/aws-database-encryption-sdk-dynamodb/awscryptographydbencryptionsdkdynamodbtransformssmithygenerated" + dbesdktransformstypes "github.com/aws/aws-database-encryption-sdk-dynamodb/awscryptographydbencryptionsdkdynamodbtransformssmithygeneratedtypes" + dbesdkstructuredencryptiontypes "github.com/aws/aws-database-encryption-sdk-dynamodb/awscryptographydbencryptionsdkstructuredencryptionsmithygeneratedtypes" + "github.com/aws/aws-database-encryption-sdk-dynamodb/dbesdkmiddleware" + "github.com/aws/aws-database-encryption-sdk-dynamodb/examples/utils" + + "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-sdk-go-v2/service/kms" +) + +/* +This example demonstrates how to set up a virtual field from two DDB +attributes, create a standard beacon with that field, put an item with +that beacon, and query against that beacon. + +A virtual field is a field consisting of a transformation of one or more attributes in a DDB item. +Virtual fields are useful in querying against encrypted fields that only have a handful of +possible values. They allow you to take fields with few possible values, concatenate +them to other fields, then query against the combined field. This enables using these types of +fields in queries while making it infeasible to identify which beacon values encode +the few possible distinct plaintexts. This is explained in more detail below. +Virtual fields are not stored in the DDB table. However, they are used to construct +a beacon, the value of which is stored. + +For more information on virtual fields, see + + https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/beacons.html#virtual-field + +For our example, we will construct a virtual field +from two DDB attributes `state` and `hasTestResult` as `state`+prefix(`hasTestResult`, 1). +We will then create a beacon out of this virtual field and use it to search. + +This example follows a use case of a database that stores customer test result metadata. +Records are indexed by `customer_id` and store a `state` attribute, representing the +US state or territory where the customer lives, and a `hasTestResult` boolean attribute, +representing whether the customer has a "test result" available. (Maybe this represents +some medical test result, and this table stores "result available" metadata.) We assume +that values in these fields are uniformly distributed across all possible values for +these fields (56 for `state`, 2 for `hasTestResult`), and are uniformly distributed across +customer IDs. + +The motivation behind this example is to demonstrate how and why one would use a virtual beacon. +In this example, our table stores records with an encrypted boolean `hasTestResult` attribute. +We would like to be able to query for customers in a given state with a `true` hasTestResult +attribute. + +To be able to execute this query securely and efficiently, we want the following +properties on our table: + 1. Hide the distribution of `hasTestResult` attribute values (i.e. it should be infeasible + to determine the percentage of `true`s to `false`s across the dataset from beaconized + values) + 2. Query against a combination of whether `hasTestResult` is true/false and the `state` field + +We cannot achieve these properties with a standard beacon on a true/false attribute. Following +the guidance to choose a beacon length: + + https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html + +For a boolean value (in our case, whether `hasTestResult` is true or false), the acceptable +bounds for beacon length are either 0 or 1. This corresponds to either not storing a beacon +(length 0), or effectively storing another boolean attribute (length 1). With +length 0, this beacon is useless for searching (violating property 2); with length 1, this +beacon may not hide the attribute (violating property 1). +In addition, choosing a longer beacon length does not help us. +Each attribute value is mapped to a distinct beacon. +Since booleans only have 2 possible attribute values, we will still only have 2 possible +beacon values, though those values may be longer. A longer beacon provides no advantages over +beacon of length 1 in this situation. + +A compound beacon also does not help. +To (over)simplify, a compound beacon is a concatenation of standard beacons, +i.e. beacon(`state`)+beacon(`hasTestResult`). +The `hasTestResult` beacon is still visible, so we would still have the problems above. + +To achieve these properties, we instead construct a virtual field and use that in our beacon, +i.e. beacon(`state`+`hasTestResult`). Assuming these fields are well-distributed across +customer IDs and possible values, this gives us both desired properties; we can query against +both attributes while hiding information from the underlying data. This is demonstrated in more +detail below. + +Running this example requires access to a DDB table with the +following primary key configuration: + - Partition key is named "customer_id" with type (S) + - Sort key is named "create_time" with type (S) + +This table must have a Global Secondary Index (GSI) configured named "stateAndHasTestResult-index": + - Partition key is named "aws_dbe_b_stateAndHasTestResult" with type (S) + +In this example for storing customer location data, this schema is utilized for the data: + - "customer_id" stores a unique customer identifier + - "create_time" stores a Unix timestamp + - "state" stores an encrypted 2-letter US state or territory abbreviation + (https://www.faa.gov/air_traffic/publications/atpubs/cnt_html/appendix_a.html) + - "hasTestResult" is not part of the schema, but is an attribute utilized in this example. + It stores a boolean attribute (false/true) indicating whether this customer has a test result + available. + +The example requires the following ordered input command line parameters: + 1. DDB table name for table to put/query data from + 2. Branch key ID for a branch key that was previously created in your key store. See the + CreateKeyStoreKeyExample. + 2. Branch key wrapping KMS key ARN for the KMS key used to create the branch key + 3. Branch key DDB table name for the DDB table representing the branch key store +*/ +func VirtualBeaconSearchableEncryptionExample( + ddbTableName, + branchKeyId, + branchKeyWrappingKmsKeyArn, + branchKeyDdbTableName string) { + const gsiName = "stateAndHasTestResult-index" + + // 1. Construct a length-1 prefix virtual transform. + // `hasTestResult` is a binary attribute, containing either `true` or `false`. + // As an example to demonstrate virtual transforms, we will truncate the value + // of `hasTestResult` in the virtual field to the length-1 prefix of the binary value, i.e.: + // - "true" -> "t" + // - "false -> "f" + // This is not necessary. This is done as a demonstration of virtual transforms. + // Virtual transform operations treat all attributes as strings + // (i.e. the boolean value `true` is interpreted as a string "true"), + // so its length-1 prefix is just "t". + length1PrefixVirtualTransformList := []dbesdkdynamodbencryptiontypes.VirtualTransform{ + &dbesdkdynamodbencryptiontypes.VirtualTransformMemberprefix{ + Value: dbesdkdynamodbencryptiontypes.GetPrefix{ + Length: 1, + }, + }, + } + + // 2. Construct the VirtualParts required for the VirtualField + hasTestResultPart := dbesdkdynamodbencryptiontypes.VirtualPart{ + Loc: "hasTestResult", + // Here, we apply the length-1 prefix virtual transform + Trans: length1PrefixVirtualTransformList, + } + + statePart := dbesdkdynamodbencryptiontypes.VirtualPart{ + Loc: "state", + // Note that we do not apply any transform to the `state` attribute, + // and the virtual field will read in the attribute as-is. + } + + // 3. Construct the VirtualField from the VirtualParts + // Note that the order that virtual parts are added to the virtualPartList + // dictates the order in which they are concatenated to build the virtual field. + // You must add virtual parts in the same order on write as you do on read. + virtualPartList := []dbesdkdynamodbencryptiontypes.VirtualPart{statePart, hasTestResultPart} + + stateAndHasTestResultField := dbesdkdynamodbencryptiontypes.VirtualField{ + Name: "stateAndHasTestResult", + Parts: virtualPartList, + } + + virtualFieldList := []dbesdkdynamodbencryptiontypes.VirtualField{stateAndHasTestResultField} + + // 4. Configure our beacon. + // The virtual field is assumed to hold a US 2-letter state abbreviation + // (56 possible values = 50 states + 6 territories) concatenated with a binary attribute + // (2 possible values: true/false hasTestResult field), we expect a population size of + // 56 * 2 = 112 possible values. + // We will also assume that these values are reasonably well-distributed across + // customer IDs. In practice, this will not be true. We would expect + // more populous states to appear more frequently in the database. + // A more complex analysis would show that a stricter upper bound + // is necessary to account for this by hiding information from the + // underlying distribution. + // + // This link provides guidance for choosing a beacon length: + // https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html + // We follow the guidance in the link above to determine reasonable bounds for beacon length: + // - min: log(sqrt(112))/log(2) ~= 3.4, round down to 3 + // - max: log((112/2))/log(2) ~= 5.8, round up to 6 + // You will somehow need to round results to a nearby integer. + // We choose to round to the nearest integer; you might consider a different rounding approach. + // Rounding up will return fewer expected "false positives" in queries, + // leading to fewer decrypt calls and better performance, + // but it is easier to identify which beacon values encode distinct plaintexts. + // Rounding down will return more expected "false positives" in queries, + // leading to more decrypt calls and worse performance, + // but it is harder to identify which beacon values encode distinct plaintexts. + // We can choose a beacon length between 3 and 6: + // - Closer to 3, we expect more "false positives" to be returned, + // making it harder to identify which beacon values encode distinct plaintexts, + // but leading to more decrypt calls and worse performance + // - Closer to 6, we expect fewer "false positives" returned in queries, + // leading to fewer decrypt calls and better performance, + // but it is easier to identify which beacon values encode distinct plaintexts. + // As an example, we will choose 5. + // Values stored in aws_dbe_b_stateAndHasTestResult will be 5 bits long (0x00 - 0x1f) + // There will be 2^5 = 32 possible HMAC values. + // With a well-distributed dataset (112 values), for a particular beacon we expect + // (112/32) = 3.5 combinations of abbreviation + true/false attribute + // sharing that beacon value. + standardBeaconList := []dbesdkdynamodbencryptiontypes.StandardBeacon{ + { + // This name is the same as our virtual field's name above + Name: "stateAndHasTestResult", + Length: 5, + }, + } + + // 5. Configure Keystore. + // This example expects that you have already set up a KeyStore with a single branch key. + // See the "CreateKeyStoreTableExample" and "CreateKeyStoreKeyExample" files for how to do this. + // After you create a branch key, you should persist its ID for use in this example. + cfg, err := config.LoadDefaultConfig(context.TODO()) + utils.HandleError(err) + + kmsClient := kms.NewFromConfig(cfg) + ddbClient := dynamodb.NewFromConfig(cfg) + + kmsConfig := keystoretypes.KMSConfigurationMemberkmsKeyArn{ + Value: branchKeyWrappingKmsKeyArn, + } + keyStoreConfig := keystoretypes.KeyStoreConfig{ + KmsClient: kmsClient, + DdbClient: ddbClient, + DdbTableName: branchKeyDdbTableName, + LogicalKeyStoreName: branchKeyDdbTableName, + KmsConfiguration: &kmsConfig, + } + + keyStore, err := keystoreclient.NewClient(keyStoreConfig) + utils.HandleError(err) + + // 6. Create BeaconVersion. + // The BeaconVersion inside the list holds the list of beacons on the table. + // The BeaconVersion also stores information about the keystore. + // BeaconVersion must be provided: + // - keyStore: The keystore configured in the previous step. + // - keySource: A configuration for the key source. + // For simple use cases, we can configure a 'singleKeySource' which + // statically configures a single beaconKey. That is the approach this example takes. + // For use cases where you want to use different beacon keys depending on the data + // (for example if your table holds data for multiple tenants, and you want to use + // a different beacon key per tenant), look into configuring a MultiKeyStore: + // https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/searchable-encryption-multitenant.html + // We also provide our standard beacon list and virtual fields here. + ttl := 6000 + cacheTTL := int32(ttl) + singleKeyStore := dbesdkdynamodbencryptiontypes.SingleKeyStore{ + KeyId: branchKeyId, + CacheTTL: cacheTTL, + } + beaconKeySource := dbesdkdynamodbencryptiontypes.BeaconKeySourceMembersingle{ + Value: singleKeyStore, + } + beaconVersion := dbesdkdynamodbencryptiontypes.BeaconVersion{ + VirtualFields: virtualFieldList, + StandardBeacons: standardBeaconList, + Version: 1, // MUST be 1 + KeyStore: keyStore, + KeySource: &beaconKeySource, + } + + beaconVersions := []dbesdkdynamodbencryptiontypes.BeaconVersion{beaconVersion} + + // 7. Create a Hierarchical Keyring + // This is a KMS keyring that utilizes the keystore table. + // This config defines how items are encrypted and decrypted. + // NOTE: You should configure this to use the same keystore as your search config. + matProv, err := mpl.NewClient(mpltypes.MaterialProvidersConfig{}) + utils.HandleError(err) + + ttlSeconds := int64(ttl) + keyringInput := mpltypes.CreateAwsKmsHierarchicalKeyringInput{ + BranchKeyId: &branchKeyId, + KeyStore: keyStore, + TtlSeconds: ttlSeconds, + } + kmsKeyring, err := matProv.CreateAwsKmsHierarchicalKeyring(context.Background(), keyringInput) + utils.HandleError(err) + + // 8. 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 + // Any attributes that will be used in beacons must be configured as ENCRYPT_AND_SIGN. + attributeActionsOnEncrypt := map[string]dbesdkstructuredencryptiontypes.CryptoAction{ + "customer_id": dbesdkstructuredencryptiontypes.CryptoActionSignOnly, // Our partition attribute must be SIGN_ONLY + "create_time": dbesdkstructuredencryptiontypes.CryptoActionSignOnly, // Our sort attribute must be SIGN_ONLY + "state": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, // Beaconized attributes must be encrypted + "hasTestResult": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, // Beaconized attributes must be encrypted + } + + // 9. Create the DynamoDb Encryption configuration for the table we will be writing to. + // The beaconVersions are added to the search configuration. + writeVersion := int32(1) + searchConfig := dbesdkdynamodbencryptiontypes.SearchConfig{ + WriteVersion: writeVersion, // MUST be 1 + Versions: beaconVersions, + } + + tableConfig := dbesdkdynamodbencryptiontypes.DynamoDbTableEncryptionConfig{ + LogicalTableName: ddbTableName, + PartitionKeyName: "customer_id", + SortKeyName: aws.String("create_time"), + AttributeActionsOnEncrypt: attributeActionsOnEncrypt, + Keyring: kmsKeyring, + Search: &searchConfig, + } + + tableConfigs := map[string]dbesdkdynamodbencryptiontypes.DynamoDbTableEncryptionConfig{ + ddbTableName: tableConfig, + } + + // 10. Create config + encryptionConfig := dbesdkdynamodbencryptiontypes.DynamoDbTablesEncryptionConfig{ + TableEncryptionConfigs: tableConfigs, + } + + // 11. Create test items + + // Create item with hasTestResult=true + itemWithHasTestResult := map[string]types.AttributeValue{ + "customer_id": &types.AttributeValueMemberS{Value: "ABC-123"}, + "create_time": &types.AttributeValueMemberN{Value: "1681495205"}, + "state": &types.AttributeValueMemberS{Value: "CA"}, + "hasTestResult": &types.AttributeValueMemberBOOL{Value: true}, + } + + // Create item with hasTestResult=false + itemWithNoHasTestResult := map[string]types.AttributeValue{ + "customer_id": &types.AttributeValueMemberS{Value: "DEF-456"}, + "create_time": &types.AttributeValueMemberN{Value: "1681495205"}, + "state": &types.AttributeValueMemberS{Value: "CA"}, + "hasTestResult": &types.AttributeValueMemberBOOL{Value: false}, + } + + // 12. If developing or debugging, verify config by checking virtual field values directly + transformsClient, err := dbesdktransforms.NewClient(encryptionConfig) + utils.HandleError(err) + resolveInput := dbesdktransformstypes.ResolveAttributesInput{ + TableName: ddbTableName, + Item: itemWithHasTestResult, + } + resolveOutput, err := transformsClient.ResolveAttributes(context.TODO(), resolveInput) + utils.HandleError(err) + // CompoundBeacons is empty because we have no Compound Beacons configured + if len(resolveOutput.CompoundBeacons) != 0 { + panic("CompoundBeacons is not empty although it is not configured") + } + // Verify that VirtualFields has the expected value + virtualFields := resolveOutput.VirtualFields + if (len(virtualFields)) != 1 { + panic("VirtualFields does not have the expected length") + } + if virtualFields["stateAndHasTestResult"] != "CAt" { + panic("VirtualFields does not have the expected value") + } + + // 13. Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor + dbEsdkMiddleware, err := dbesdkmiddleware.NewDBEsdkMiddleware(encryptionConfig) + utils.HandleError(err) + ddb := dynamodb.NewFromConfig(cfg, dbEsdkMiddleware.CreateMiddleware()) + + // 14. Put two items into our table using the above client. + // The two items will differ only in their `customer_id` attribute (primary key) + // and their `hasTestResult` attribute. + // We will query against these items to demonstrate how to use our setup above + // to query against our `stateAndHasTestResult` beacon. + // Before the item gets sent to DynamoDb, it will be encrypted + // client-side, according to our configuration. + // Since our configuration includes a beacon on a virtual field named + // `stateAndHasTestResult`, the client will add an attribute + // to the item with name `aws_dbe_b_stateAndHasTestResult`. + // Its value will be an HMAC truncated to as many bits as the + // beacon's `length` parameter; i.e. 5. + itemWithHasTestResultPutRequest := &dynamodb.PutItemInput{ + TableName: aws.String(ddbTableName), + Item: itemWithHasTestResult, + } + + _, err = ddb.PutItem(context.Background(), itemWithHasTestResultPutRequest) + utils.HandleError(err) + + itemWithNoHasTestResultPutRequest := &dynamodb.PutItemInput{ + TableName: aws.String(ddbTableName), + Item: itemWithNoHasTestResult, + } + + _, err = ddb.PutItem(context.Background(), itemWithNoHasTestResultPutRequest) + utils.HandleError(err) + + // 15. Query by stateAndHasTestResult attribute. + // Note that we are constructing the query as if we were querying on plaintext values. + // However, the DDB encryption client will detect that this attribute name has a beacon configured. + // The client will add the beaconized attribute name and attribute value to the query, + // and transform the query to use the beaconized name and value. + // Internally, the client will query for and receive all items with a matching HMAC value in the beacon field. + // This may include a number of "false positives" with different ciphertext, but the same truncated HMAC. + // e.g. if truncate(HMAC("CAt"), 5) == truncate(HMAC("DCf"), 5), the query will return both items. + // The client will decrypt all returned items to determine which ones have the expected attribute values, + // and only surface items with the correct plaintext to the user. + // This procedure is internal to the client and is abstracted away from the user; + // e.g. the user will only see "CAt" and never "DCf", though the actual query returned both. + expressionAttributeNames := map[string]string{ + "#stateAndHasTestResult": "stateAndHasTestResult", + } + + expressionAttributeValues := map[string]types.AttributeValue{ + // We are querying for the item with `state`="CA" and `hasTestResult`=`true`. + // Since we added virtual parts as `state` then `hasTestResult`, + // we must write our query expression in the same order. + // We constructed our virtual field as `state`+`hasTestResult`, + // so we add the two parts in that order. + // Since we also created a virtual transform that truncated `hasTestResult` + // to its length-1 prefix, i.e. "true" -> "t", + // we write that field as its length-1 prefix in the query. + ":stateAndHasTestResult": &types.AttributeValueMemberS{Value: "CAt"}, + } + + queryRequest := &dynamodb.QueryInput{ + TableName: aws.String(ddbTableName), + IndexName: aws.String(gsiName), + KeyConditionExpression: aws.String("#stateAndHasTestResult = :stateAndHasTestResult"), + ExpressionAttributeNames: expressionAttributeNames, + ExpressionAttributeValues: expressionAttributeValues, + } + + // GSIs do not update instantly + // so if the results come back empty + // we retry after a short sleep + for i := 0; i < 10; i++ { + queryResponse, err := ddb.Query(context.Background(), queryRequest) + utils.HandleError(err) + + attributeValues := queryResponse.Items + + // if no results, sleep and try again + if len(attributeValues) == 0 { + time.Sleep(20 * time.Millisecond) + continue + } + + // Validate only 1 item was returned: the item with the expected attributes + if len(attributeValues) != 1 { + panic(fmt.Sprintf("Expected 1 item, got %d", len(attributeValues))) + } + returnedItem := attributeValues[0] + // Validate the item has the expected attributes + state := returnedItem["state"].(*types.AttributeValueMemberS).Value + hasTestResult := returnedItem["hasTestResult"].(*types.AttributeValueMemberBOOL).Value + if state != "CA" { + panic(fmt.Sprintf("Expected state 'CA', got '%s'", state)) + } + if !hasTestResult { + panic(fmt.Sprintf("Expected hasTestResult true, got %t", hasTestResult)) + } + break + } + + fmt.Println("Virtual Beacon Searchable Encryption Example completed successfully") +} diff --git a/Examples/runtimes/go/utils/exampleUtils.go b/Examples/runtimes/go/utils/exampleUtils.go index 121fd6dd0..b0c780c40 100644 --- a/Examples/runtimes/go/utils/exampleUtils.go +++ b/Examples/runtimes/go/utils/exampleUtils.go @@ -9,22 +9,37 @@ import ( ) const ( - kmsKeyID = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f" - ddbTableName = "DynamoDbEncryptionInterceptorTestTableCS" - keyNamespace = "my-key-namespace" - keyName = "my-key-name" - aesKeyBytes = 32 // 256 bits = 32 bytes - testKeystoreName = "KeyStoreDdbTable" - testLogicalKeystoreName = "KeyStoreDdbTable" - testKeystoreKmsKeyId = "arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126" - defaultRsaPublicKeyFilename = "KmsRsaKeyringPublicKey.pem" - testKmsRsaKeyID = "arn:aws:kms:us-west-2:658956600833:key/8b432da4-dde4-4bc3-a794-c7d68cbab5a6" - defaultKMSKeyAccountID = "658956600833" - defaultKmsKeyRegion = "us-west-2" - exampleRsaPrivateKeyFilename = "RawRsaKeyringExamplePrivateKey.pem" - exampleRsaPublicKeyFilename = "RawRsaKeyringExamplePublicKey.pem" + kmsKeyID = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f" + ddbTableName = "DynamoDbEncryptionInterceptorTestTableCS" + keyNamespace = "my-key-namespace" + keyName = "my-key-name" + aesKeyBytes = 32 // 256 bits = 32 bytes + testKeystoreName = "KeyStoreDdbTable" + testLogicalKeystoreName = "KeyStoreDdbTable" + testKeystoreKmsKeyId = "arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126" + defaultRsaPublicKeyFilename = "KmsRsaKeyringPublicKey.pem" + testKmsRsaKeyID = "arn:aws:kms:us-west-2:658956600833:key/8b432da4-dde4-4bc3-a794-c7d68cbab5a6" + defaultKMSKeyAccountID = "658956600833" + defaultKmsKeyRegion = "us-west-2" + exampleRsaPrivateKeyFilename = "RawRsaKeyringExamplePrivateKey.pem" + exampleRsaPublicKeyFilename = "RawRsaKeyringExamplePublicKey.pem" + unitInspectionTestDdbTableName = "UnitInspectionTestTableCS" + simpleBeaconTestDdbTableName = "SimpleBeaconTestTable" + testComplexDdbTableName = "ComplexBeaconTestTable" ) +func UnitInspectionTestDdbTableName() string { + return unitInspectionTestDdbTableName +} + +func SimpleBeaconTestDdbTableName() string { + return simpleBeaconTestDdbTableName +} + +func TestComplexDdbTableName() string { + return testComplexDdbTableName +} + func ExampleRsaPublicKeyFilename() string { return exampleRsaPublicKeyFilename }