Skip to content

Commit d915ab0

Browse files
add BasicSearchableEncryptionExample
1 parent ffb0c38 commit d915ab0

File tree

1 file changed

+366
-0
lines changed

1 file changed

+366
-0
lines changed
Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
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 beacon on an encrypted attribute,
29+
put an item with the beacon, and query against that beacon.
30+
This example follows a use case of a database that stores unit inspection information.
31+
32+
Running this example requires access to a DDB table with the
33+
following key configuration:
34+
- Partition key is named "work_id" with type (S)
35+
- Sort key is named "inspection_date" with type (S)
36+
37+
This table must have a Global Secondary Index (GSI) configured named "last4-unit-index":
38+
- Partition key is named "aws_dbe_b_inspector_id_last4" with type (S)
39+
- Sort key is named "aws_dbe_b_unit" with type (S)
40+
41+
In this example for storing unit inspection information, this schema is utilized for the data:
42+
- "work_id" stores a unique identifier for a unit inspection work order (v4 UUID)
43+
- "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD)
44+
- "inspector_id_last4" stores the last 4 digits of the ID of the inspector performing the work
45+
- "unit" stores a 12-digit serial number for the unit being inspected
46+
47+
The example requires the following ordered input command line parameters:
48+
1. DDB table name for table to put/query data from
49+
2. Branch key ID for a branch key that was previously created in your key store. See the
50+
CreateKeyStoreKeyExample.
51+
3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID
52+
provided in arg 2
53+
4. Branch key DDB table name for the DDB table representing the branch key store
54+
*/
55+
func BasicSearchableEncryptionExample(
56+
ddbTableName,
57+
branchKeyId,
58+
branchKeyWrappingKmsKeyArn,
59+
branchKeyDdbTableName string) {
60+
const gsiName = "last4-unit-index"
61+
62+
// 1. Configure Beacons.
63+
// The beacon name must be the name of a table attribute that will be encrypted.
64+
// The `length` parameter dictates how many bits are in the beacon attribute value.
65+
// The following link provides guidance on choosing a beacon length:
66+
// https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html
67+
68+
// The configured DDB table has a GSI on the `aws_dbe_b_inspector_id_last4` AttributeName.
69+
// This field holds the last 4 digits of an inspector ID.
70+
// For our example, this field may range from 0 to 9,999 (10,000 possible values).
71+
// For our example, we assume a full inspector ID is an integer
72+
// ranging from 0 to 99,999,999. We do not assume that the full inspector ID's
73+
// values are uniformly distributed across its range of possible values.
74+
// In many use cases, the prefix of an identifier encodes some information
75+
// about that identifier (e.g. zipcode and SSN prefixes encode geographic
76+
// information), while the suffix does not and is more uniformly distributed.
77+
// We will assume that the inspector ID field matches a similar use case.
78+
// So for this example, we only store and use the last
79+
// 4 digits of the inspector ID, which we assume is uniformly distributed.
80+
// Since the full ID's range is divisible by the range of the last 4 digits,
81+
// then the last 4 digits of the inspector ID are uniformly distributed
82+
// over the range from 0 to 9,999.
83+
// See our documentation for why you should avoid creating beacons over non-uniform distributions
84+
// https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/searchable-encryption.html#are-beacons-right-for-me
85+
// A single inspector ID suffix may be assigned to multiple `work_id`s.
86+
//
87+
// This link provides guidance for choosing a beacon length:
88+
// https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html
89+
// We follow the guidance in the link above to determine reasonable bounds
90+
// for the length of a beacon on the last 4 digits of an inspector ID:
91+
// - min: log(sqrt(10,000))/log(2) ~= 6.6, round up to 7
92+
// - max: log((10,000/2))/log(2) ~= 12.3, round down to 12
93+
// You will somehow need to round results to a nearby integer.
94+
// We choose to round to the nearest integer; you might consider a different rounding approach.
95+
// Rounding up will return fewer expected "false positives" in queries,
96+
// leading to fewer decrypt calls and better performance,
97+
// but it is easier to identify which beacon values encode distinct plaintexts.
98+
// Rounding down will return more expected "false positives" in queries,
99+
// leading to more decrypt calls and worse performance,
100+
// but it is harder to identify which beacon values encode distinct plaintexts.
101+
// We can choose a beacon length between 7 and 12:
102+
// - Closer to 7, we expect more "false positives" to be returned,
103+
// making it harder to identify which beacon values encode distinct plaintexts,
104+
// but leading to more decrypt calls and worse performance
105+
// - Closer to 12, we expect fewer "false positives" returned in queries,
106+
// leading to fewer decrypt calls and better performance,
107+
// but it is easier to identify which beacon values encode distinct plaintexts.
108+
// As an example, we will choose 10.
109+
//
110+
// Values stored in aws_dbe_b_inspector_id_last4 will be 10 bits long (0x000 - 0x3ff)
111+
// There will be 2^10 = 1,024 possible HMAC values.
112+
// With a sufficiently large number of well-distributed inspector IDs,
113+
// for a particular beacon we expect (10,000/1,024) ~= 9.8 4-digit inspector ID suffixes
114+
// sharing that beacon value.
115+
last4Beacon := dbesdkdynamodbencryptiontypes.StandardBeacon{
116+
Name: "inspector_id_last4",
117+
Length: 10,
118+
}
119+
120+
// The configured DDB table has a GSI on the `aws_dbe_b_unit` AttributeName.
121+
// This field holds a unit serial number.
122+
// For this example, this is a 12-digit integer from 0 to 999,999,999,999 (10^12 possible values).
123+
// We will assume values for this attribute are uniformly distributed across this range.
124+
// A single unit serial number may be assigned to multiple `work_id`s.
125+
//
126+
// This link provides guidance for choosing a beacon length:
127+
// https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html
128+
// We follow the guidance in the link above to determine reasonable bounds
129+
// for the length of a beacon on a unit serial number:
130+
// - min: log(sqrt(999,999,999,999))/log(2) ~= 19.9, round up to 20
131+
// - max: log((999,999,999,999/2))/log(2) ~= 38.9, round up to 39
132+
// We can choose a beacon length between 20 and 39:
133+
// - Closer to 20, we expect more "false positives" to be returned,
134+
// making it harder to identify which beacon values encode distinct plaintexts,
135+
// but leading to more decrypt calls and worse performance
136+
// - Closer to 39, we expect fewer "false positives" returned in queries,
137+
// leading to fewer decrypt calls and better performance,
138+
// but it is easier to identify which beacon values encode distinct plaintexts.
139+
// As an example, we will choose 30.
140+
//
141+
// Values stored in aws_dbe_b_unit will be 30 bits long (0x00000000 - 0x3fffffff)
142+
// There will be 2^30 = 1,073,741,824 ~= 1.1B possible HMAC values.
143+
// With a sufficiently large number of well-distributed inspector IDs,
144+
// for a particular beacon we expect (10^12/2^30) ~= 931.3 unit serial numbers
145+
// sharing that beacon value.
146+
unitBeacon := dbesdkdynamodbencryptiontypes.StandardBeacon{
147+
Name: "unit",
148+
Length: 30,
149+
}
150+
151+
standardBeaconList := []dbesdkdynamodbencryptiontypes.StandardBeacon{last4Beacon, unitBeacon}
152+
153+
// 2. Configure Keystore.
154+
// The keystore is a separate DDB table where the client stores encryption and decryption materials.
155+
// In order to configure beacons on the DDB client, you must configure a keystore.
156+
//
157+
// This example expects that you have already set up a KeyStore with a single branch key.
158+
// See the "Create KeyStore Table Example" and "Create KeyStore Key Example" for how to do this.
159+
// After you create a branch key, you should persist its ID for use in this example.
160+
cfg, err := config.LoadDefaultConfig(context.TODO())
161+
utils.HandleError(err)
162+
163+
kmsClient := kms.NewFromConfig(cfg)
164+
ddbClient := dynamodb.NewFromConfig(cfg)
165+
166+
kmsConfig := keystoretypes.KMSConfigurationMemberkmsKeyArn{
167+
Value: branchKeyWrappingKmsKeyArn,
168+
}
169+
keyStoreConfig := keystoretypes.KeyStoreConfig{
170+
KmsClient: kmsClient,
171+
DdbClient: ddbClient,
172+
DdbTableName: branchKeyDdbTableName,
173+
LogicalKeyStoreName: branchKeyDdbTableName,
174+
KmsConfiguration: &kmsConfig,
175+
}
176+
177+
keyStore, err := keystoreclient.NewClient(keyStoreConfig)
178+
utils.HandleError(err)
179+
180+
// 3. Create BeaconVersion.
181+
// The BeaconVersion inside the list holds the list of beacons on the table.
182+
// The BeaconVersion also stores information about the keystore.
183+
// BeaconVersion must be provided:
184+
// - keyStore: The keystore configured in step 2.
185+
// - keySource: A configuration for the key source.
186+
// For simple use cases, we can configure a 'singleKeySource' which
187+
// statically configures a single beaconKey. That is the approach this example takes.
188+
// For use cases where you want to use different beacon keys depending on the data
189+
// (for example if your table holds data for multiple tenants, and you want to use
190+
// a different beacon key per tenant), look into configuring a MultiKeyStore:
191+
// https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/searchable-encryption-multitenant.html
192+
ttl := 6000
193+
cacheTTL := int32(ttl)
194+
singleKeyStore := dbesdkdynamodbencryptiontypes.SingleKeyStore{
195+
// `keyId` references a beacon key.
196+
// For every branch key we create in the keystore,
197+
// we also create a beacon key.
198+
// This beacon key is not the same as the branch key,
199+
// but is created with the same ID as the branch key.
200+
KeyId: branchKeyId,
201+
CacheTTL: cacheTTL,
202+
}
203+
beaconKeySource := dbesdkdynamodbencryptiontypes.BeaconKeySourceMembersingle{
204+
Value: singleKeyStore,
205+
}
206+
beaconVersion := dbesdkdynamodbencryptiontypes.BeaconVersion{
207+
StandardBeacons: standardBeaconList,
208+
Version: 1, // MUST be 1
209+
KeyStore: keyStore,
210+
KeySource: &beaconKeySource,
211+
}
212+
213+
beaconVersions := []dbesdkdynamodbencryptiontypes.BeaconVersion{beaconVersion}
214+
215+
// 4. Create a Hierarchical Keyring
216+
// This is a KMS keyring that utilizes the keystore table.
217+
// This config defines how items are encrypted and decrypted.
218+
// NOTE: You should configure this to use the same keystore as your search config.
219+
matProv, err := mpl.NewClient(mpltypes.MaterialProvidersConfig{})
220+
utils.HandleError(err)
221+
222+
ttlSeconds := int64(ttl)
223+
keyringInput := mpltypes.CreateAwsKmsHierarchicalKeyringInput{
224+
BranchKeyId: &branchKeyId,
225+
KeyStore: keyStore,
226+
TtlSeconds: ttlSeconds,
227+
}
228+
kmsKeyring, err := matProv.CreateAwsKmsHierarchicalKeyring(context.Background(), keyringInput)
229+
utils.HandleError(err)
230+
231+
// 5. Configure which attributes are encrypted and/or signed when writing new items.
232+
// For each attribute that may exist on the items we plan to write to our DynamoDbTable,
233+
// we must explicitly configure how they should be treated during item encryption:
234+
// - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
235+
// - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
236+
// - DO_NOTHING: The attribute is not encrypted and not included in the signature
237+
// Any attributes that will be used in beacons must be configured as ENCRYPT_AND_SIGN.
238+
attributeActionsOnEncrypt := map[string]dbesdkstructuredencryptiontypes.CryptoAction{
239+
"work_id": dbesdkstructuredencryptiontypes.CryptoActionSignOnly, // Our partition attribute must be SIGN_ONLY
240+
"inspection_date": dbesdkstructuredencryptiontypes.CryptoActionSignOnly, // Our sort attribute must be SIGN_ONLY
241+
"inspector_id_last4": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, // Beaconized attributes must be encrypted
242+
"unit": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, // Beaconized attributes must be encrypted
243+
}
244+
245+
// 6. Create the DynamoDb Encryption configuration for the table we will be writing to.
246+
// The beaconVersions are added to the search configuration.
247+
writeVersion := int32(1)
248+
searchConfig := dbesdkdynamodbencryptiontypes.SearchConfig{
249+
WriteVersion: writeVersion, // MUST be 1
250+
Versions: beaconVersions,
251+
}
252+
253+
tableConfig := dbesdkdynamodbencryptiontypes.DynamoDbTableEncryptionConfig{
254+
LogicalTableName: ddbTableName,
255+
PartitionKeyName: "work_id",
256+
SortKeyName: aws.String("inspection_date"),
257+
AttributeActionsOnEncrypt: attributeActionsOnEncrypt,
258+
Keyring: kmsKeyring,
259+
Search: &searchConfig,
260+
}
261+
262+
tableConfigs := map[string]dbesdkdynamodbencryptiontypes.DynamoDbTableEncryptionConfig{
263+
ddbTableName: tableConfig,
264+
}
265+
266+
// 7. Create the DynamoDb Encryption Interceptor
267+
encryptionConfig := dbesdkdynamodbencryptiontypes.DynamoDbTablesEncryptionConfig{
268+
TableEncryptionConfigs: tableConfigs,
269+
}
270+
271+
// 8. Create a new AWS SDK DynamoDb client
272+
dbEsdkMiddleware, err := dbesdkmiddleware.NewDBEsdkMiddleware(encryptionConfig)
273+
utils.HandleError(err)
274+
ddb := dynamodb.NewFromConfig(cfg, dbEsdkMiddleware.CreateMiddleware())
275+
276+
// 9. Put an item into our table using the above client.
277+
// Before the item gets sent to DynamoDb, it will be encrypted
278+
// client-side, according to our configuration.
279+
// Since our configuration includes beacons for `inspector_id_last4` and `unit`,
280+
// the client will add two additional attributes to the item. These attributes will have names
281+
// `aws_dbe_b_inspector_id_last4` and `aws_dbe_b_unit`. Their values will be HMACs
282+
// truncated to as many bits as the beacon's `length` parameter; e.g.
283+
// aws_dbe_b_inspector_id_last4 = truncate(HMAC("4321"), 10)
284+
// aws_dbe_b_unit = truncate(HMAC("123456789012"), 30)
285+
item := map[string]types.AttributeValue{
286+
"work_id": &types.AttributeValueMemberS{Value: "1313ba89-5661-41eb-ba6c-cb1b4cb67b2d"},
287+
"inspection_date": &types.AttributeValueMemberS{Value: "2023-06-13"},
288+
"inspector_id_last4": &types.AttributeValueMemberS{Value: "4321"},
289+
"unit": &types.AttributeValueMemberS{Value: "123456789012"},
290+
}
291+
292+
putRequest := &dynamodb.PutItemInput{
293+
TableName: aws.String(ddbTableName),
294+
Item: item,
295+
}
296+
297+
_, err = ddb.PutItem(context.Background(), putRequest)
298+
utils.HandleError(err)
299+
300+
// 10. Query for the item we just put.
301+
// Note that we are constructing the query as if we were querying on plaintext values.
302+
// However, the DDB encryption client will detect that this attribute name has a beacon configured.
303+
// The client will add the beaconized attribute name and attribute value to the query,
304+
// and transform the query to use the beaconized name and value.
305+
// Internally, the client will query for and receive all items with a matching HMAC value in the beacon field.
306+
// This may include a number of "false positives" with different ciphertext, but the same truncated HMAC.
307+
// e.g. if truncate(HMAC("123456789012"), 30)
308+
// == truncate(HMAC("098765432109"), 30),
309+
// the query will return both items.
310+
// The client will decrypt all returned items to determine which ones have the expected attribute values,
311+
// and only surface items with the correct plaintext to the user.
312+
// This procedure is internal to the client and is abstracted away from the user;
313+
// e.g. the user will only see "123456789012" and never
314+
// "098765432109", though the actual query returned both.
315+
expressionAttributeNames := map[string]string{
316+
"#last4": "inspector_id_last4",
317+
"#unit": "unit",
318+
}
319+
320+
expressionAttributeValues := map[string]types.AttributeValue{
321+
":last4": &types.AttributeValueMemberS{Value: "4321"},
322+
":unit": &types.AttributeValueMemberS{Value: "123456789012"},
323+
}
324+
325+
queryRequest := &dynamodb.QueryInput{
326+
TableName: aws.String(ddbTableName),
327+
IndexName: aws.String(gsiName),
328+
KeyConditionExpression: aws.String("#last4 = :last4 and #unit = :unit"),
329+
ExpressionAttributeNames: expressionAttributeNames,
330+
ExpressionAttributeValues: expressionAttributeValues,
331+
}
332+
333+
// GSIs do not update instantly
334+
// so if the results come back empty
335+
// we retry after a short sleep
336+
for i := 0; i < 10; i++ {
337+
queryResponse, err := ddb.Query(context.Background(), queryRequest)
338+
utils.HandleError(err)
339+
340+
attributeValues := queryResponse.Items
341+
342+
// if no results, sleep and try again
343+
if len(attributeValues) == 0 {
344+
time.Sleep(20 * time.Millisecond)
345+
continue
346+
}
347+
348+
// Validate only 1 item was returned: the item we just put
349+
if len(attributeValues) != 1 {
350+
panic(fmt.Sprintf("Expected 1 item, got %d", len(attributeValues)))
351+
}
352+
returnedItem := attributeValues[0]
353+
// Validate the item has the expected attributes
354+
inspectorIDLast4 := returnedItem["inspector_id_last4"].(*types.AttributeValueMemberS).Value
355+
unit := returnedItem["unit"].(*types.AttributeValueMemberS).Value
356+
if inspectorIDLast4 != "4321" {
357+
panic(fmt.Sprintf("Expected inspector_id_last4 '4321', got '%s'", inspectorIDLast4))
358+
}
359+
if unit != "123456789012" {
360+
panic(fmt.Sprintf("Expected unit '123456789012', got '%s'", unit))
361+
}
362+
break
363+
}
364+
365+
fmt.Println("Basic Searchable Encryption Example completed successfully")
366+
}

0 commit comments

Comments
 (0)