Skip to content

Commit 3fd1f4a

Browse files
auto commit
1 parent 55b7fa6 commit 3fd1f4a

File tree

2 files changed

+799
-0
lines changed

2 files changed

+799
-0
lines changed
Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package searchableencryption
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"time"
10+
11+
keystoreclient "github.com/aws/aws-cryptographic-material-providers-library/releases/go/mpl/awscryptographykeystoresmithygenerated"
12+
keystoretypes "github.com/aws/aws-cryptographic-material-providers-library/releases/go/mpl/awscryptographykeystoresmithygeneratedtypes"
13+
mpl "github.com/aws/aws-cryptographic-material-providers-library/releases/go/mpl/awscryptographymaterialproviderssmithygenerated"
14+
mpltypes "github.com/aws/aws-cryptographic-material-providers-library/releases/go/mpl/awscryptographymaterialproviderssmithygeneratedtypes"
15+
dbesdkdynamodbencryptiontypes "github.com/aws/aws-database-encryption-sdk-dynamodb/awscryptographydbencryptionsdkdynamodbsmithygeneratedtypes"
16+
dbesdkstructuredencryptiontypes "github.com/aws/aws-database-encryption-sdk-dynamodb/awscryptographydbencryptionsdkstructuredencryptionsmithygeneratedtypes"
17+
"github.com/aws/aws-database-encryption-sdk-dynamodb/dbesdkmiddleware"
18+
"github.com/aws/aws-database-encryption-sdk-dynamodb/examples/utils"
19+
20+
"github.com/aws/aws-sdk-go-v2/aws"
21+
"github.com/aws/aws-sdk-go-v2/config"
22+
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
23+
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
24+
"github.com/aws/aws-sdk-go-v2/service/kms"
25+
)
26+
27+
/*
28+
This example demonstrates how to set up a compound beacon on encrypted attributes,
29+
30+
put an item with the beacon, and query against that beacon.
31+
32+
This example follows a use case of a database that stores unit inspection information.
33+
34+
This is an extension of the "BasicSearchableEncryptionExample" in this directory.
35+
This example uses the same situation (storing unit inspection information)
36+
and the same table schema.
37+
38+
However, this example uses a different Global Secondary Index (GSI)
39+
40+
that is based on a compound beacon configuration composed of
41+
the `last4` and `unit` attributes.
42+
43+
Running this example requires access to a DDB table with the
44+
following key configuration:
45+
- Partition key is named "work_id" with type (S)
46+
- Sort key is named "inspection_time" with type (S)
47+
48+
This table must have a Global Secondary Index (GSI) configured named "last4UnitCompound-index":
49+
- Partition key is named "aws_dbe_b_last4UnitCompound" with type (S)
50+
51+
In this example for storing unit inspection information, this schema is utilized for the data:
52+
- "work_id" stores a unique identifier for a unit inspection work order (v4 UUID)
53+
- "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD)
54+
- "inspector_id_last4" stores the last 4 digits of the ID of the inspector performing the work
55+
- "unit" stores a 12-digit serial number for the unit being inspected
56+
57+
The example requires the following ordered input command line parameters:
58+
1. DDB table name for table to put/query data from
59+
2. Branch key ID for a branch key that was previously created in your key store. See the
60+
CreateKeyStoreKeyExample.
61+
3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID
62+
provided in arg 2
63+
4. Branch key DDB table ARN for the DDB table representing the branch key store
64+
*/
65+
func CompoundBeaconSearchableEncryptionExample(
66+
ddbTableName,
67+
branchKeyID,
68+
branchKeyWrappingKmsKeyArn,
69+
branchKeyDdbTableName string) {
70+
const gsiName = "last4UnitCompound-index"
71+
partitionKeyName := "work_id"
72+
sortKeyName := "inspection_date"
73+
74+
// 1. Create Beacons.
75+
// These are the same beacons as in the "BasicSearchableEncryptionExample" in this directory.
76+
// See that file to see details on beacon construction and parameters.
77+
// While we will not directly query against these beacons,
78+
// you must create standard beacons on encrypted fields
79+
// that we wish to use in compound beacons.
80+
// We mark them both as PartOnly to enforce the fact that
81+
// we will not directly query against these beacons.
82+
standardBeaconList := []dbesdkdynamodbencryptiontypes.StandardBeacon{
83+
{
84+
Name: "inspector_id_last4",
85+
Length: 10,
86+
Style: &dbesdkdynamodbencryptiontypes.BeaconStyleMemberpartOnly{
87+
Value: dbesdkdynamodbencryptiontypes.PartOnly{},
88+
},
89+
},
90+
{
91+
Name: "unit",
92+
Length: 30,
93+
Style: &dbesdkdynamodbencryptiontypes.BeaconStyleMemberpartOnly{
94+
Value: dbesdkdynamodbencryptiontypes.PartOnly{},
95+
},
96+
},
97+
}
98+
99+
// 2. Define encrypted parts.
100+
// Encrypted parts define the beacons that can be used to construct a compound beacon,
101+
// and how the compound beacon prefixes those beacon values.
102+
// A encrypted part must receive:
103+
// - name: Name of a standard beacon
104+
// - prefix: Any string. This is plaintext that prefixes the beaconized value in the compound beacon.
105+
// Prefixes must be unique across the configuration, and must not be a prefix of another prefix;
106+
// i.e. for all configured prefixes, the first N characters of a prefix must not equal another prefix.
107+
// In practice, it is suggested to have a short value distinguishable from other parts served on the prefix.
108+
109+
encryptedPartList := []dbesdkdynamodbencryptiontypes.EncryptedPart{
110+
// For this example, we will choose "L-" as the prefix for "Last 4 digits of inspector ID".
111+
// With this prefix and the standard beacon's bit length definition (10), the beaconized
112+
// version of the inspector ID's last 4 digits will appear as
113+
// `L-000` to `L-3ff` inside a compound beacon.
114+
{
115+
Name: "inspector_id_last4",
116+
Prefix: "L-",
117+
},
118+
// For this example, we will choose "U-" as the prefix for "unit".
119+
// With this prefix and the standard beacon's bit length definition (30), a unit beacon will appear
120+
// as `U-00000000` to `U-3fffffff` inside a compound beacon.
121+
{
122+
Name: "unit",
123+
Prefix: "U-",
124+
},
125+
}
126+
127+
constructorParts := []dbesdkdynamodbencryptiontypes.ConstructorPart{
128+
{
129+
Name: "inspector_id_last4",
130+
Required: true,
131+
},
132+
{
133+
// This name comes from the "unit" standard beacon.
134+
Name: "unit",
135+
Required: true,
136+
},
137+
}
138+
constructors := []dbesdkdynamodbencryptiontypes.Constructor{
139+
{
140+
Parts: constructorParts,
141+
},
142+
}
143+
144+
// 3. Define compound beacon.
145+
// A compound beacon allows one to serve multiple beacons or attributes from a single index.
146+
// A compound beacon must receive:
147+
// - name: The name of the beacon. Compound beacon values will be written to `aws_ddb_e_[name]`.
148+
// - split: A character separating parts in a compound beacon
149+
// A compound beacon may also receive:
150+
// - encrypted: A list of encrypted parts. This is effectively a list of beacons. We provide the list
151+
// that we created above.
152+
// - constructors: A list of constructors. This is an ordered list of possible ways to create a beacon.
153+
// We have not defined any constructors here; see the complex example for how to do this.
154+
// The client will provide a default constructor, which will write a compound beacon as:
155+
// all signed parts in the order they are added to the signed list;
156+
// all encrypted parts in order they are added to the encrypted list; all parts required.
157+
// In this example, we expect compound beacons to be written as
158+
// `L-XXX.U-YYYYYYYY`, since our encrypted list looks like
159+
// [last4EncryptedPart, unitEncryptedPart].
160+
// - signed: A list of signed parts, i.e. plaintext attributes. This would be provided if we
161+
// wanted to use plaintext values as part of constructing our compound beacon. We do not
162+
// provide this here; see the Complex example for an example.
163+
compoundBeaconList := []dbesdkdynamodbencryptiontypes.CompoundBeacon{
164+
{
165+
Name: "last4UnitCompound",
166+
Constructors: constructors,
167+
Split: ".",
168+
},
169+
}
170+
171+
// 4. Configure the Keystore
172+
// These are the same constructions as in the Basic example, which describes these in more detail.
173+
cfg, err := config.LoadDefaultConfig(context.TODO())
174+
utils.HandleError(err)
175+
176+
kmsClient := kms.NewFromConfig(cfg)
177+
ddbClient := dynamodb.NewFromConfig(cfg)
178+
179+
kmsConfig := keystoretypes.KMSConfigurationMemberkmsKeyArn{
180+
Value: branchKeyWrappingKmsKeyArn,
181+
}
182+
keyStoreConfig := keystoretypes.KeyStoreConfig{
183+
KmsClient: kmsClient,
184+
DdbClient: ddbClient,
185+
DdbTableName: branchKeyDdbTableName,
186+
LogicalKeyStoreName: branchKeyDdbTableName,
187+
KmsConfiguration: &kmsConfig,
188+
}
189+
190+
keyStore, err := keystoreclient.NewClient(keyStoreConfig)
191+
utils.HandleError(err)
192+
193+
// 5. Create BeaconVersion.
194+
// This is similar to the Basic example, except we have also provided a compoundBeaconList.
195+
// We must also continue to provide all of the standard beacons that compose a compound beacon list.
196+
ttl := 6000
197+
cacheTTL := int32(ttl)
198+
singleKeyStore := dbesdkdynamodbencryptiontypes.SingleKeyStore{
199+
KeyId: branchKeyID,
200+
CacheTTL: cacheTTL,
201+
}
202+
beaconKeySource := dbesdkdynamodbencryptiontypes.BeaconKeySourceMembersingle{
203+
Value: singleKeyStore,
204+
}
205+
beaconVersion := dbesdkdynamodbencryptiontypes.BeaconVersion{
206+
EncryptedParts: encryptedPartList,
207+
StandardBeacons: standardBeaconList,
208+
CompoundBeacons: compoundBeaconList,
209+
Version: 1, // MUST be 1
210+
KeyStore: keyStore,
211+
KeySource: &beaconKeySource,
212+
}
213+
214+
beaconVersions := []dbesdkdynamodbencryptiontypes.BeaconVersion{beaconVersion}
215+
216+
// 6. Create a Hierarchical Keyring
217+
// This is the same configuration as in the Basic example.
218+
matProv, err := mpl.NewClient(mpltypes.MaterialProvidersConfig{})
219+
utils.HandleError(err)
220+
221+
ttlSeconds := int64(ttl)
222+
keyringInput := mpltypes.CreateAwsKmsHierarchicalKeyringInput{
223+
BranchKeyId: &branchKeyID,
224+
KeyStore: keyStore,
225+
TtlSeconds: ttlSeconds,
226+
}
227+
kmsKeyring, err := matProv.CreateAwsKmsHierarchicalKeyring(context.Background(), keyringInput)
228+
utils.HandleError(err)
229+
230+
// 7. Configure which attributes are encrypted and/or signed when writing new items.
231+
attributeActionsOnEncrypt := map[string]dbesdkstructuredencryptiontypes.CryptoAction{
232+
partitionKeyName: dbesdkstructuredencryptiontypes.CryptoActionSignOnly, // Our partition attribute must be SIGN_ONLY
233+
sortKeyName: dbesdkstructuredencryptiontypes.CryptoActionSignOnly, // Our sort attribute must be SIGN_ONLY
234+
"inspector_id_last4": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, // Beaconized attributes must be encrypted
235+
"unit": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, // Beaconized attributes must be encrypted
236+
}
237+
238+
// We do not need to define a crypto action on last4UnitCompound.
239+
// We only need to define crypto actions on attributes that we pass to PutItem.
240+
241+
// 8. Create the DynamoDb Encryption configuration for the table we will be writing to.
242+
// The beaconVersions are added to the search configuration.
243+
writeVersion := int32(1)
244+
searchConfig := dbesdkdynamodbencryptiontypes.SearchConfig{
245+
WriteVersion: writeVersion, // MUST be 1
246+
Versions: beaconVersions,
247+
}
248+
249+
tableConfig := dbesdkdynamodbencryptiontypes.DynamoDbTableEncryptionConfig{
250+
LogicalTableName: ddbTableName,
251+
PartitionKeyName: partitionKeyName,
252+
SortKeyName: aws.String(sortKeyName),
253+
AttributeActionsOnEncrypt: attributeActionsOnEncrypt,
254+
Keyring: kmsKeyring,
255+
Search: &searchConfig,
256+
}
257+
258+
tableConfigs := map[string]dbesdkdynamodbencryptiontypes.DynamoDbTableEncryptionConfig{
259+
ddbTableName: tableConfig,
260+
}
261+
262+
// 9. Create config
263+
encryptionConfig := dbesdkdynamodbencryptiontypes.DynamoDbTablesEncryptionConfig{
264+
TableEncryptionConfigs: tableConfigs,
265+
}
266+
267+
// 10. Create an item with both attributes used in the compound beacon.
268+
item := map[string]types.AttributeValue{
269+
partitionKeyName: &types.AttributeValueMemberS{Value: "9ce39272-8068-4efd-a211-cd162ad65d4c"},
270+
sortKeyName: &types.AttributeValueMemberS{Value: "2023-06-13"},
271+
"inspector_id_last4": &types.AttributeValueMemberS{Value: "5678"},
272+
"unit": &types.AttributeValueMemberS{Value: "011899988199"},
273+
}
274+
275+
// 11. Create the DynamoDb Encryption Interceptor
276+
dbEsdkMiddleware, err := dbesdkmiddleware.NewDBEsdkMiddleware(encryptionConfig)
277+
utils.HandleError(err)
278+
279+
// 12. Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor above
280+
ddb := dynamodb.NewFromConfig(cfg, dbEsdkMiddleware.CreateMiddleware())
281+
282+
putAndQueryItemWithCompoundBeacon(ddb, ddbTableName, item, gsiName)
283+
284+
fmt.Println("Compound Beacon Searchable Encryption Example completed successfully")
285+
}
286+
287+
func putAndQueryItemWithCompoundBeacon(ddb *dynamodb.Client, ddbTableName string, item map[string]types.AttributeValue, gsiName string) {
288+
// 13. Write the item to the table
289+
putRequest := &dynamodb.PutItemInput{
290+
TableName: aws.String(ddbTableName),
291+
Item: item,
292+
}
293+
294+
_, err := ddb.PutItem(context.Background(), putRequest)
295+
utils.HandleError(err)
296+
297+
// 14. Query for the item we just put.
298+
expressionAttributeNames := map[string]string{
299+
"#compound": "last4UnitCompound",
300+
}
301+
302+
expressionAttributeValues := map[string]types.AttributeValue{
303+
// This query expression takes a few factors into consideration:
304+
// - The configured prefix for the last 4 digits of an inspector ID is "L-";
305+
// the prefix for the unit is "U-"
306+
// - The configured split character, separating component parts, is "."
307+
// - The default constructor adds encrypted parts in the order they are in the encrypted list, which
308+
// configures `last4` to come before `unit``
309+
// NOTE: We did not need to create a compound beacon for this query. This query could have also been
310+
// done by querying on the partition and sort key, as was done in the Basic example.
311+
// This is intended to be a simple example to demonstrate how one might set up a compound beacon.
312+
// For examples where compound beacons are required, see the Complex example.
313+
// The most basic extension to this example that would require a compound beacon would add a third
314+
// part to the compound beacon, then query against three parts.
315+
":value": &types.AttributeValueMemberS{Value: "L-5678.U-011899988199"},
316+
}
317+
318+
queryRequest := &dynamodb.QueryInput{
319+
TableName: aws.String(ddbTableName),
320+
IndexName: aws.String(gsiName),
321+
KeyConditionExpression: aws.String("#compound = :value"),
322+
ExpressionAttributeNames: expressionAttributeNames,
323+
ExpressionAttributeValues: expressionAttributeValues,
324+
}
325+
326+
// GSIs do not update instantly
327+
// so if the results come back empty
328+
// we retry after a short sleep
329+
for i := 0; i < 10; i++ {
330+
queryResponse, err := ddb.Query(context.Background(), queryRequest)
331+
utils.HandleError(err)
332+
333+
attributeValues := queryResponse.Items
334+
335+
// if no results, sleep and try again
336+
if len(attributeValues) == 0 {
337+
time.Sleep(20 * time.Millisecond)
338+
continue
339+
}
340+
341+
// Validate only 1 item was returned: the item we just put
342+
if len(attributeValues) != 1 {
343+
panic(fmt.Sprintf("Expected 1 item, got %d", len(attributeValues)))
344+
}
345+
returnedItem := attributeValues[0]
346+
// Validate the item has the expected attributes
347+
inspectorIdLast4 := returnedItem["inspector_id_last4"].(*types.AttributeValueMemberS).Value
348+
unit := returnedItem["unit"].(*types.AttributeValueMemberS).Value
349+
if inspectorIdLast4 != "5678" {
350+
panic(fmt.Sprintf("Expected inspector_id_last4 '5678', got '%s'", inspectorIdLast4))
351+
}
352+
if unit != "011899988199" {
353+
panic(fmt.Sprintf("Expected unit '011899988199', got '%s'", unit))
354+
}
355+
break
356+
}
357+
}

0 commit comments

Comments
 (0)