|
| 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