modules/example-node/src/kms-hierarchical-keyring/multi_tenancy.ts (119 lines of code) (raw):

// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { BranchKeyStoreNode, buildClient, CommitmentPolicy, KmsHierarchicalKeyRingNode, BranchKeyIdSupplier, EncryptionContext, } from '@aws-crypto/client-node' /** * This example sets up the Hierarchical Keyring, which establishes a key hierarchy where "branch" * keys are persisted in DynamoDb. These branch keys are used to protect your data keys, and these * branch keys are themselves protected by a KMS Key. * * Establishing a key hierarchy like this has two benefits: * * First, by caching the branch key material, and only calling KMS to re-establish authentication * regularly according to your configured TTL, you limit how often you need to call KMS to protect * your data. This is a performance security tradeoff, where your authentication, audit, and logging * from KMS is no longer one-to-one with every encrypt or decrypt call. Additionally, KMS Cloudtrail * cannot be used to distinguish Encrypt and Decrypt calls, and you cannot restrict who has * Encryption rights from who has Decryption rights since they both ONLY need KMS:Decrypt. However, * the benefit is that you no longer have to make a network call to KMS for every encrypt or * decrypt. * * Second, this key hierarchy facilitates cryptographic isolation of a tenant's data in a * multi-tenant data store. Each tenant can have a unique Branch Key, that is only used to protect * the tenant's data. You can either statically configure a single branch key to ensure you are * restricting access to a single tenant, or you can implement an interface that selects the Branch * Key based on the Encryption Context. * * This example demonstrates configuring a Hierarchical Keyring with a Branch Key ID Supplier to * encrypt and decrypt data for two separate tenants. * * This example requires access to the DDB Table where you are storing the Branch Keys. This * table must be configured with the following primary key configuration: - Partition key is named * "partition_key" with type (S) - Sort key is named "sort_key" with type (S) * * This example also requires using a KMS Key. You need the following access on this key: - * GenerateDataKeyWithoutPlaintext - Decrypt */ /* This builds the client with the REQUIRE_ENCRYPT_REQUIRE_DECRYPT commitment policy, * which enforces that this client only encrypts using committing algorithm suites * and enforces that this client * will only decrypt encrypted messages * that were created with a committing algorithm suite. * This is the default commitment policy * if you build the client with `buildClient()`. */ const { encrypt, decrypt } = buildClient( CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT ) // Implement an example branch key id supplier // Use the encryption contexts to define friendly names for each branch key class ExampleBranchKeyIdSupplier implements BranchKeyIdSupplier { private _branchKeyIdForTenantA: string private _branchKeyIdForTenantB: string constructor(tenant1Id: string, tenant2Id: string) { this._branchKeyIdForTenantA = tenant1Id this._branchKeyIdForTenantB = tenant2Id } getBranchKeyId(encryptionContext: EncryptionContext): string { if ('tenant' in encryptionContext === false) { throw new Error( 'EncryptionContext invalid, does not contain expected tenant key value pair.' ) } const tenantKeyId = encryptionContext['tenant'] let branchKeyId: string if (tenantKeyId === 'TenantA') { branchKeyId = this._branchKeyIdForTenantA } else if (tenantKeyId === 'TenantB') { branchKeyId = this._branchKeyIdForTenantB } else { throw new Error('Item does not contain valid tenant ID') } return branchKeyId } } export async function hKeyringMultiTenancy( keyStoreTableName = 'KeyStoreDdbTable', logicalKeyStoreName = keyStoreTableName, kmsKeyId = 'arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126' ) { // Configure your KeyStore resource. // This SHOULD be the same configuration that you used // to initially create and populate your KeyStore. const keyStore = new BranchKeyStoreNode({ storage: {ddbTableName: keyStoreTableName}, logicalKeyStoreName: logicalKeyStoreName, kmsConfiguration: { identifier: kmsKeyId }, }) // Here, you would call CreateKey to create two new active branch keys. // However, the JS keystore does not currently support this operation, so we // hard code the IDs of two existing active branch keys const branchKeyIdA = '38853b56-19c6-4345-9cb5-afc2a25dcdd1' const branchKeyIdB = '2c583585-5770-467d-8f59-b346d0ed1994' // Create a branch key supplier that maps the branch key id to a more readable format const branchKeyIdSupplier = new ExampleBranchKeyIdSupplier( branchKeyIdA, branchKeyIdB ) // Create the Hierarchical Keyring. const keyring = new KmsHierarchicalKeyRingNode({ branchKeyIdSupplier, keyStore, cacheLimitTtl: 600, // 10 min }) // The Branch Key Id supplier uses the encryption context to determine which branch key id will // be used to encrypt data. // Create encryption context for TenantA const encryptionContextAIn = { tenant: 'TenantA', encryption: 'context', 'is not': 'secret', 'but adds': 'useful metadata', 'that can help you': 'be confident that', 'the data you are handling': 'is what you think it is', } // Create encryption context for TenantB const encryptionContextBIn = { tenant: 'TenantB', encryption: 'context', 'is not': 'secret', 'but adds': 'useful metadata', 'that can help you': 'be confident that', 'the data you are handling': 'is what you think it is', } /* Find data to encrypt. A simple string. */ const cleartext = 'asdf' // Encrypt the data for encryptionContextA & encryptionContextB const { result: encryptResultA } = await encrypt(keyring, cleartext, { encryptionContext: encryptionContextAIn, }) const { result: encryptResultB } = await encrypt(keyring, cleartext, { encryptionContext: encryptionContextBIn, }) // To attest that TenantKeyB cannot decrypt a message written by TenantKeyA // let's construct more restrictive hierarchical keyrings. const keyringA = new KmsHierarchicalKeyRingNode({ branchKeyId: branchKeyIdA, keyStore, cacheLimitTtl: 600, }) const keyringB = new KmsHierarchicalKeyRingNode({ branchKeyId: branchKeyIdB, keyStore, cacheLimitTtl: 600, }) let decryptAFailed = false // Try to use keyring for Tenant B to decrypt a message encrypted with Tenant A's key // Expected to fail. try { await decrypt(keyringB, encryptResultA) } catch (e) { decryptAFailed = true } let decryptBFailed = false // Try to use keyring for Tenant A to decrypt a message encrypted with Tenant B's key // Expected to fail. try { await decrypt(keyringA, encryptResultB) } catch (e) { decryptBFailed = true } // we will assert that both decrypts failed const decryptsFailed = decryptAFailed && decryptBFailed // Decrypt your encrypted data using the same keyring you used on encrypt. const { plaintext: plaintextA, messageHeader: messageHeaderA } = await decrypt(keyring, encryptResultA) /* Grab the encryption context so you can verify it. */ const { encryptionContext: encryptionContextAOut } = messageHeaderA Object.entries(encryptionContextAIn).forEach(([key, value]) => { if (encryptionContextAOut[key] !== value) throw new Error('Encryption Context does not match expected values') }) const { plaintext: plaintextB, messageHeader: messageHeaderB } = await decrypt(keyring, encryptResultB) /* Grab the encryption context so you can verify it. */ const { encryptionContext: encryptionContextBOut } = messageHeaderB Object.entries(encryptionContextBIn).forEach(([key, value]) => { if (encryptionContextBOut[key] !== value) throw new Error('Encryption Context does not match expected values') }) // we will assert that both decrypted plaintexts are the same as the original // cleartext /* Return the values so the code can be tested. */ return { decryptsFailed, cleartext, plaintextA, plaintextB } }