diff --git a/karma.conf.js b/karma.conf.js index 97a19af8e..9f2ea67b3 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,6 +1,5 @@ // Karma configuration process.env.CHROME_BIN = require('puppeteer').executablePath() -const webpack = require('webpack') module.exports = function (config) { config.set({ diff --git a/modules/cache-material/src/get_local_cryptographic_materials_cache.ts b/modules/cache-material/src/get_local_cryptographic_materials_cache.ts index c7ea91176..3a1d7f95b 100644 --- a/modules/cache-material/src/get_local_cryptographic_materials_cache.ts +++ b/modules/cache-material/src/get_local_cryptographic_materials_cache.ts @@ -31,11 +31,11 @@ import { } from './cryptographic_materials_cache' export function getLocalCryptographicMaterialsCache ( - maxSize: number, + capacity: number, proactiveFrequency: number = 1000 * 60 ): CryptographicMaterialsCache { const cache = new LRU>({ - max: maxSize, + max: capacity, dispose (_key, value) { /* Zero out the unencrypted dataKey, when the material is removed from the cache. */ value.response.zeroUnencryptedDataKey() diff --git a/modules/cache-material/test/get_local_cryptographic_materials_cache.test.ts b/modules/cache-material/test/get_local_cryptographic_materials_cache.test.ts index fec9c60bd..cc2c9e822 100644 --- a/modules/cache-material/test/get_local_cryptographic_materials_cache.test.ts +++ b/modules/cache-material/test/get_local_cryptographic_materials_cache.test.ts @@ -158,7 +158,7 @@ describe('getLocalCryptographicMaterialsCache', () => { }) describe('cache eviction', () => { - it('putDecryptionMaterial can exceed maxSize', () => { + it('putDecryptionMaterial can exceed capacity', () => { const { getDecryptionMaterial, putDecryptionMaterial @@ -207,7 +207,7 @@ describe('cache eviction', () => { expect(lost).to.equal(false) }) - it('putEncryptionMaterial can exceed maxSize', () => { + it('putEncryptionMaterial can exceed capacity', () => { const { getEncryptionMaterial, putEncryptionMaterial diff --git a/modules/client-browser/Readme.md b/modules/client-browser/Readme.md index 29fd1af71..6aaa19f76 100644 --- a/modules/client-browser/Readme.md +++ b/modules/client-browser/Readme.md @@ -66,13 +66,13 @@ const context = { const plainText = new Uint8Array([1, 2, 3, 4, 5]) /* Encrypt the string using the keyring and the encryption context - * the Encryption SDK returns an "encrypted message" that includes the ciphertext, + * the Encryption SDK returns an "encrypted message" (`result`) that includes the ciphertext, * the encryption context, and the encrypted data keys. */ -const { ciphertext } = await encrypt(keyring, plainText, { encryptionContext: context }) +const { result } = await encrypt(keyring, plainText, { encryptionContext: context }) -/* Decrypt the ciphertext using the same keyring */ -const { plaintext, messageHeader } = await decrypt(keyring, ciphertext) +/* Decrypt the result using the same keyring */ +const { plaintext, messageHeader } = await decrypt(keyring, result) /* Get the encryption context */ const { encryptionContext } = messageHeader diff --git a/modules/client-node/Readme.md b/modules/client-node/Readme.md index c1d0b7868..c9fccc532 100644 --- a/modules/client-node/Readme.md +++ b/modules/client-node/Readme.md @@ -58,13 +58,13 @@ const context = { const cleartext = 'asdf' /* Encrypt the string using the keyring and the encryption context - * the Encryption SDK returns an "encrypted message" that includes the ciphertext, + * the Encryption SDK returns an "encrypted message" (`result`) that includes the ciphertext * the encryption context, and the encrypted data keys. */ -const { ciphertext } = await encrypt(keyring, cleartext, { context }) +const { result } = await encrypt(keyring, cleartext, { context }) -/* Decrypt the ciphertext using the same keyring */ -const { plaintext, messageHeader } = await decrypt(keyring, ciphertext) +/* Decrypt the result using the same keyring */ +const { plaintext, messageHeader } = await decrypt(keyring, result) /* Get the encryption context */ const { encryptionContext } = messageHeader diff --git a/modules/example-browser/html/aes_simple.html b/modules/example-browser/html/aes_simple.html index 5a49b224a..67ef5b23b 100644 --- a/modules/example-browser/html/aes_simple.html +++ b/modules/example-browser/html/aes_simple.html @@ -7,4 +7,9 @@ + + diff --git a/modules/example-browser/html/caching_cmm.html b/modules/example-browser/html/caching_cmm.html new file mode 100644 index 000000000..1a80f0605 --- /dev/null +++ b/modules/example-browser/html/caching_cmm.html @@ -0,0 +1,15 @@ + + + + + + Client-Side Caching CMM Encryption Test + + + + + + diff --git a/modules/example-browser/html/kms_simple.html b/modules/example-browser/html/kms_simple.html index 55d110018..844de0b4a 100644 --- a/modules/example-browser/html/kms_simple.html +++ b/modules/example-browser/html/kms_simple.html @@ -7,4 +7,9 @@ + + diff --git a/modules/example-browser/html/multi_keyring.html b/modules/example-browser/html/multi_keyring.html index 68e2c8a0a..800d250f9 100644 --- a/modules/example-browser/html/multi_keyring.html +++ b/modules/example-browser/html/multi_keyring.html @@ -7,4 +7,9 @@ + + diff --git a/modules/example-browser/html/rsa_simple.html b/modules/example-browser/html/rsa_simple.html index 089c3ce84..806c68a5f 100644 --- a/modules/example-browser/html/rsa_simple.html +++ b/modules/example-browser/html/rsa_simple.html @@ -7,5 +7,9 @@ + diff --git a/modules/example-browser/karma.conf.js b/modules/example-browser/karma.conf.js new file mode 100644 index 000000000..f6f41e784 --- /dev/null +++ b/modules/example-browser/karma.conf.js @@ -0,0 +1,92 @@ +const webpack = require('webpack') +const {defaultProvider} = require('@aws-sdk/credential-provider-node') + +// Karma configuration +process.env.CHROME_BIN = require('puppeteer').executablePath() + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['mocha', 'chai'], + files: [ + 'test/**/*.ts', + // 'src/**/*.ts' + ], + preprocessors: { + // 'src/**/*.ts': ['credentials'], + 'test/**/*.ts': ['webpack', 'credentials'] + }, + webpack: { + resolve: { + extensions: [ '.ts', '.js' ] + }, + mode: 'development', + module: { + rules: [ + { + test: /\.tsx?$/, + use: [ + { + loader: 'ts-loader', + options: { + configFile: 'tsconfig.module.json', + compilerOptions: { + rootDir: './' + } + } + } + ], + exclude: /node_modules/, + }, + { + test: /\.ts$/, + exclude: [ /\/test\// ], + enforce: 'post', + use: { + loader: 'istanbul-instrumenter-loader', + options: { esModules: true } + } + } + ] + }, + stats: { + colors: true, + modules: true, + reasons: true, + errorDetails: true + }, + devtool: 'inline-source-map', + node: { + fs: 'empty' + }, + }, + coverageIstanbulReporter: { + reports: [ 'json' ], + dir: '.karma_output', + fixWebpackSourcePaths: true + }, + plugins: [ + '@aws-sdk/karma-credential-loader', + 'karma-chrome-launcher', + 'karma-mocha', + 'karma-chai', + 'karma-webpack', + 'karma-coverage-istanbul-reporter' + ], + reporters: ['progress', 'coverage-istanbul'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: false, + browsers: ['ChromeHeadlessDisableCors'], + customLaunchers: { + ChromeHeadlessDisableCors: { + base: 'ChromeHeadless', + flags: ['--disable-web-security'] + } + }, + singleRun: true, + concurrency: Infinity, + exclude: ['**/*.d.ts'] + }) +} diff --git a/modules/example-browser/package.json b/modules/example-browser/package.json index 7c6962d02..2394990be 100644 --- a/modules/example-browser/package.json +++ b/modules/example-browser/package.json @@ -4,11 +4,15 @@ "scripts": { "prepublishOnly": "npm run build", "build": "tsc -b tsconfig.json && tsc -b tsconfig.module.json", - "test": "mocha --require ts-node/register tests/**/*tests.ts", + "lint": "standard src/*.ts test/**/*.ts", + "karma": "karma start karma.conf.js", + "test": "npm run lint && npm run coverage", + "coverage": "npm run karma && nyc report --exclude-after-remap false -t .karma_output --check-coverage", "example-rsa": "webpack -d --config webpack_configs/rsa.webpack.config.js", "example-aes": "webpack -d --config webpack_configs/aes.webpack.config.js", "example-kms": "webpack -d --config webpack_configs/kms.webpack.config.js", - "example-multi-keyring": "webpack -d --config webpack_configs/multi_keyring.webpack.config.js" + "example-multi-keyring": "webpack -d --config webpack_configs/multi_keyring.webpack.config.js", + "example-caching-cmm": "webpack -d --config webpack_configs/caching_cmm.webpack.config.js" }, "author": { "name": "AWS Crypto Tools Team", @@ -25,11 +29,21 @@ "@aws-sdk/credential-provider-node": "^0.1.0-preview.4", "@types/chai": "^4.1.4", "@types/mocha": "^5.2.5", + "@typescript-eslint/eslint-plugin": "^1.9.0", + "@typescript-eslint/parser": "^1.9.0", "chai": "^4.1.2", "mocha": "^5.2.0", "ts-loader": "^5.3.3", "ts-node": "^7.0.1", "typescript": "^3.5.0", + "karma": "^4.1.0", + "karma-chai": "^0.1.0", + "karma-chrome-launcher": "^2.2.0", + "karma-coverage-istanbul-reporter": "^2.0.4", + "karma-mocha": "^1.3.0", + "karma-webpack": "^3.0.5", + "nyc": "^14.0.0", + "standard": "^12.0.1", "webpack": "^4.30.0", "webpack-cli": "^3.3.0" }, diff --git a/modules/example-browser/src/aes_simple.ts b/modules/example-browser/src/aes_simple.ts index 44efd82a0..bfa8ce702 100644 --- a/modules/example-browser/src/aes_simple.ts +++ b/modules/example-browser/src/aes_simple.ts @@ -27,7 +27,8 @@ import { } from '@aws-crypto/client-browser' import { toBase64 } from '@aws-sdk/util-base64-browser' - ;(async function testAES () { +/* This is done to facilitate testing. */ +export async function testAES () { /* You need to specify a name * and a namespace for raw encryption key providers. * The name and namespace that you use in the decryption keyring *must* be an exact, @@ -105,4 +106,7 @@ import { toBase64 } from '@aws-sdk/util-base64-browser' */ document.write('
plaintext:' + plaintext) console.log(plaintext) -})() + + /* Return the values to make testing easy. */ + return { plainText, plaintext } +} diff --git a/modules/example-browser/src/caching_cmm.ts b/modules/example-browser/src/caching_cmm.ts new file mode 100644 index 000000000..eb00badd1 --- /dev/null +++ b/modules/example-browser/src/caching_cmm.ts @@ -0,0 +1,215 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use + * this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* This is a simple example of using a caching CMM with a KMS keyring + * to encrypt and decrypt using the AWS Encryption SDK for Javascript in a browser. + */ + +import { + KmsKeyringBrowser, + KMS, + getClient, + encrypt, + decrypt, + WebCryptoCachingMaterialsManager, + getLocalCryptographicMaterialsCache +} from '@aws-crypto/client-browser' +import { toBase64 } from '@aws-sdk/util-base64-browser' + +/* This is injected by webpack. + * The webpack.DefinePlugin or @aws-sdk/karma-credential-loader will replace the values when bundling. + * The credential values are pulled from @aws-sdk/credential-provider-node + * Use any method you like to get credentials into the browser. + * See kms.webpack.config + */ +declare const credentials: {accessKeyId: string, secretAccessKey:string, sessionToken:string } + +/* This is done to facilitate testing. */ +export async function testCachingCMMExample () { + /* This example uses a KMS keyring. The generator key in a KMS keyring generates and encrypts the data key. + * The caller needs kms:GenerateDataKey permission on the CMK in generatorKeyId. + */ + const generatorKeyId = 'arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt' + + /* Adding additional KMS keys that can decrypt. + * The caller must have kms:Encrypt permission for every CMK in keyIds. + * You might list several keys in different AWS Regions. + * This allows you to decrypt the data in any of the represented Regions. + * In this example, the generator key + * and the additional key are actually the same CMK. + * In `generatorId`, this CMK is identified by its alias ARN. + * In `keyIds`, this CMK is identified by its key ARN. + * In practice, you would specify different CMKs, + * or omit the `keyIds` parameter. + * This is *only* to demonstrate how the CMK ARNs are configured. + */ + const keyIds = ['arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f'] + + /* Need a client provider that will inject correct credentials. + * The credentials here are injected by webpack from your environment bundle is created + * The credential values are pulled using @aws-sdk/credential-provider-node. + * See kms.webpack.config + * You should inject your credential into the browser in a secure manner + * that works with your application. + */ + const { accessKeyId, secretAccessKey, sessionToken } = credentials + + /* getClient takes a KMS client constructor + * and optional configuration values. + * The credentials can be injected here, + * because browsers do not have a standard credential discovery process the way Node.js does. + */ + const clientProvider = getClient(KMS, { + credentials: { + accessKeyId, + secretAccessKey, + sessionToken + } + }) + + /* You must configure the KMS keyring with your KMS CMKs */ + const keyring = new KmsKeyringBrowser({ clientProvider, generatorKeyId, keyIds }) + + /* Create a cache to hold the data keys (and related cryptographic material). + * This example uses the local cache provided by the Encryption SDK. + * The `capacity` value represents the maximum number of entries + * that the cache can hold. + * To make room for an additional entry, + * the cache evicts the oldest cached entry. + * Both encrypt and decrypt requests count independently towards this threshold. + * Entries that exceed any cache threshold are actively removed from the cache. + * By default, the SDK checks one item in the cache every 60 seconds (60,000 milliseconds). + * To change this frequency, pass in a `proactiveFrequency` value + * as the second parameter. This value is in milliseconds. + */ + const capacity = 100 + const cache = getLocalCryptographicMaterialsCache(capacity) + + /* The partition name lets multiple caching CMMs share the same local cryptographic cache. + * By default, the entries for each CMM are cached separately. However, if you want these CMMs to share the cache, + * use the same partition name for both caching CMMs. + * If you don't supply a partition name, the Encryption SDK generates a random name for each caching CMM. + * As a result, sharing elements in the cache MUST be an intentional operation. + */ + const partition = 'local partition name' + + /* maxAge is the time in milliseconds that an entry will be cached. + * Elements are actively removed from the cache. + */ + const maxAge = 1000 * 60 + + /* The maximum number of bytes that will be encrypted under a single data key. + * This value is optional, + * but you should configure the lowest practical value. + */ + const maxBytesEncrypted = 100 + + /* The maximum number of messages that will be encrypted under a single data key. + * This value is optional, + * but you should configure the lowest practical value. + */ + const maxMessagesEncrypted = 10 + + const cachingCMM = new WebCryptoCachingMaterialsManager({ + backingMaterials: keyring, + cache, + partition, + maxAge, + maxBytesEncrypted, + maxMessagesEncrypted + }) + + /* Encryption context is a *very* powerful tool for controlling + * and managing access. + * When you pass an encryption context to the encrypt function, + * the encryption context is cryptographically bound to the ciphertext. + * If you don't pass in the same encryption context when decrypting, + * the decrypt function fails. + * The encryption context is ***not*** secret! + * Encrypted data is opaque. + * You can use an encryption context to assert things about the encrypted data. + * The encryption context helps you to determine + * whether the ciphertext you retrieved is the ciphertext you expect to decrypt. + * For example, if you are are only expecting data from 'us-west-2', + * the appearance of a different AWS Region in the encryption context can indicate malicious interference. + * See: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + * + * Also, cached data keys are reused ***only*** when the encryption contexts passed into the functions are an exact case-sensitive match. + * See: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/data-caching-details.html#caching-encryption-context + */ + const encryptionContext = { + stage: 'demo', + purpose: 'simple demonstration app', + origin: 'us-west-2' + } + + /* Find data to encrypt. */ + const plainText = new Uint8Array([1, 2, 3, 4, 5]) + + /* Encrypt the data. + * The caching CMM only reuses data keys + * when it know the length (or an estimate) of the plaintext. + * However, in the browser, + * you must provide all of the plaintext to the encrypt function. + * Therefore, the encrypt function in the browser knows the length of the plaintext + * and does not accept a plaintextLength option. + */ + const { result } = await encrypt(cachingCMM, plainText, { encryptionContext }) + + /* Log the plain text + * only for testing and to show that it works. + */ + console.log('plainText:', plainText) + document.write('
plainText:' + plainText + '
') + + /* Log the base64-encoded result + * so that you can try decrypting it with another AWS Encryption SDK implementation. + */ + const resultBase64 = toBase64(result) + console.log(resultBase64) + document.write(resultBase64) + + /* Decrypt the data. + * NOTE: This decrypt request will not use the data key + * that was cached during the encrypt operation. + * Data keys for encrypt and decrypt operations are cached separately. + */ + const { plaintext, messageHeader } = await decrypt(cachingCMM, result) + + /* Grab the encryption context so you can verify it. */ + const { encryptionContext: decryptedContext } = messageHeader + + /* Verify the encryption context. + * If you use an algorithm suite with signing, + * the Encryption SDK adds a name-value pair to the encryption context that contains the public key. + * Because the encryption context might contain additional key-value pairs, + * do not include a test that requires that all key-value pairs match. + * Instead, verify that the key-value pairs that you supplied to the `encrypt` function are included in the encryption context that the `decrypt` function returns. + */ + Object + .entries(encryptionContext) + .forEach(([key, value]) => { + if (decryptedContext[key] !== value) throw new Error('Encryption Context does not match expected values') + }) + + /* Log the clear message + * only for testing and to show that it works. + */ + document.write('
Decrypted:' + plaintext) + console.log(plaintext) + + /* Return the values to make testing easy. */ + return { plainText, plaintext } +} diff --git a/modules/example-browser/src/kms_simple.ts b/modules/example-browser/src/kms_simple.ts index 323ba20ce..919abf24c 100644 --- a/modules/example-browser/src/kms_simple.ts +++ b/modules/example-browser/src/kms_simple.ts @@ -32,9 +32,10 @@ import { toBase64 } from '@aws-sdk/util-base64-browser' * Use any method you like to get credentials into the browser. * See kms.webpack.config */ -declare const AWS_CREDENTIALS: {accessKeyId: string, secretAccessKey:string } +declare const credentials: {accessKeyId: string, secretAccessKey:string, sessionToken:string } -;(async function kmsSimpleExample () { +/* This is done to facilitate testing. */ +export async function testKmsSimpleExample () { /* A KMS CMK is required to generate the data key. * You need kms:GenerateDataKey permission on the CMK in generatorKeyId. */ @@ -56,7 +57,7 @@ declare const AWS_CREDENTIALS: {accessKeyId: string, secretAccessKey:string } * You should inject your credential into the browser in a secure manner, * that works with your application. */ - const { accessKeyId, secretAccessKey } = AWS_CREDENTIALS + const { accessKeyId, secretAccessKey, sessionToken } = credentials /* getClient takes a KMS client constructor * and optional configuration values. @@ -66,7 +67,8 @@ declare const AWS_CREDENTIALS: {accessKeyId: string, secretAccessKey:string } const clientProvider = getClient(KMS, { credentials: { accessKeyId, - secretAccessKey + secretAccessKey, + sessionToken } }) @@ -128,6 +130,9 @@ declare const AWS_CREDENTIALS: {accessKeyId: string, secretAccessKey:string } /* Log the clear message * only for testing and to show that it works. */ - document.write('
Decrypted:' + plaintext) + document.write('
plaintext:' + plaintext) console.log(plaintext) -})() + + /* Return the values to make testing easy. */ + return { plainText, plaintext } +} diff --git a/modules/example-browser/src/multi_keyring.ts b/modules/example-browser/src/multi_keyring.ts index 3838f26b5..334f2e6ff 100644 --- a/modules/example-browser/src/multi_keyring.ts +++ b/modules/example-browser/src/multi_keyring.ts @@ -37,9 +37,10 @@ import { toBase64 } from '@aws-sdk/util-base64-browser' * Use any method you like to get credentials into the browser. * See kms.webpack.config */ -declare const AWS_CREDENTIALS: {accessKeyId: string, secretAccessKey:string } +declare const credentials: {accessKeyId: string, secretAccessKey:string, sessionToken:string } -;(async function multiKeyringExample () { +/* This is done to facilitate testing. */ +export async function testMultiKeyringExample () { /* A KMS CMK is required to generate the data key. * You need kms:GenerateDataKey permission on the CMK in generatorKeyId. */ @@ -59,10 +60,10 @@ declare const AWS_CREDENTIALS: {accessKeyId: string, secretAccessKey:string } * from your environment when the bundle is created. * The credential values are pulled using @aws-sdk/credential-provider-node. * See kms.webpack.config - * You should inject your credential into the browser in a secure manner + * You should inject your credentials into the browser in a secure manner * that works with your application. */ - const { accessKeyId, secretAccessKey } = AWS_CREDENTIALS + const { accessKeyId, secretAccessKey, sessionToken } = credentials /* getClient takes a KMS client constructor * and optional configuration values. @@ -73,7 +74,8 @@ declare const AWS_CREDENTIALS: {accessKeyId: string, secretAccessKey:string } const clientProvider = getClient(KMS, { credentials: { accessKeyId, - secretAccessKey + secretAccessKey, + sessionToken } }) @@ -165,6 +167,9 @@ declare const AWS_CREDENTIALS: {accessKeyId: string, secretAccessKey:string } /* Log the clear message * only for testing and to show that it works. */ - document.write('
Decrypted:' + plaintext) + document.write('
plaintext:' + plaintext) console.log(plaintext) -})() + + /* Return the values to make testing easy. */ + return { plainText, plaintext } +} diff --git a/modules/example-browser/src/rsa_simple.ts b/modules/example-browser/src/rsa_simple.ts index 30492d44b..f23a8d417 100644 --- a/modules/example-browser/src/rsa_simple.ts +++ b/modules/example-browser/src/rsa_simple.ts @@ -26,7 +26,8 @@ import { } from '@aws-crypto/client-browser' import { toBase64 } from '@aws-sdk/util-base64-browser' - ;(async function testRSA () { +/* This is done to facilitate testing. */ +export async function testRSA () { /* JWK for the RSA Keys to use. * These keys are *Public*! * *DO NOT USE* @@ -107,4 +108,7 @@ import { toBase64 } from '@aws-sdk/util-base64-browser' */ document.write('
plaintext:' + plaintext) console.log(plaintext) -})() + + /* Return the values to make testing easy. */ + return { plainText, plaintext } +} diff --git a/modules/example-browser/test/index.test.ts b/modules/example-browser/test/index.test.ts new file mode 100644 index 000000000..63dd9b5de --- /dev/null +++ b/modules/example-browser/test/index.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use + * this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect } from 'chai' +import 'mocha' +import { testAES } from '../src/aes_simple' +import { testCachingCMMExample } from '../src/caching_cmm' +import { testKmsSimpleExample } from '../src/kms_simple' +import { testMultiKeyringExample } from '../src/multi_keyring' +import { testRSA } from '../src/rsa_simple' + +describe('test', () => { + it('testAES', async () => { + const { plainText, plaintext } = await testAES() + expect(plainText).to.deep.equal(plaintext) + }) + + it('testCachingCMMExample', async () => { + const { plainText, plaintext } = await testCachingCMMExample() + expect(plainText).to.deep.equal(plaintext) + }) + + it('testKmsSimpleExample', async () => { + const { plainText, plaintext } = await testKmsSimpleExample() + expect(plainText).to.deep.equal(plaintext) + }) + + it('testMultiKeyringExample', async () => { + const { plainText, plaintext } = await testMultiKeyringExample() + expect(plainText).to.deep.equal(plaintext) + }) + + it('testRSA', async () => { + const { plainText, plaintext } = await testRSA() + expect(plainText).to.deep.equal(plaintext) + }) +}) diff --git a/modules/example-browser/webpack_configs/aes.webpack.config.js b/modules/example-browser/webpack_configs/aes.webpack.config.js index d3f6bf0c4..7a305a620 100644 --- a/modules/example-browser/webpack_configs/aes.webpack.config.js +++ b/modules/example-browser/webpack_configs/aes.webpack.config.js @@ -25,6 +25,8 @@ module.exports = { }, output: { filename: 'aes_simple_bundle.js', - path: path.resolve(__dirname, '..', 'build') + path: path.resolve(__dirname, '..', 'build'), + library: 'test', + libraryTarget: 'var' } } diff --git a/modules/example-browser/webpack_configs/caching_cmm.webpack.config.js b/modules/example-browser/webpack_configs/caching_cmm.webpack.config.js new file mode 100644 index 000000000..da6de99ac --- /dev/null +++ b/modules/example-browser/webpack_configs/caching_cmm.webpack.config.js @@ -0,0 +1,42 @@ +const webpack = require('webpack') +const path = require('path') +const {defaultProvider} = require('@aws-sdk/credential-provider-node') + +module.exports = (async () => ({ + entry: './src/caching_cmm.ts', + // devtool: 'inline-source-map', + module: { + rules: [ + { + test: /caching_cmm.ts$/, + use: [ + { + loader: 'ts-loader', + options: { + configFile: 'tsconfig.module.json' + } + } + ], + include: /caching_cmm.ts/, + exclude: [/node_modules/] + } + ] + }, + resolve: { + extensions: [ '.tsx', '.ts', '.js' ] + }, + output: { + filename: 'caching_cmm_bundle.js', + path: path.resolve(__dirname, '..', 'build'), + library: 'test', + libraryTarget: 'var' + }, + plugins: [ + new webpack.DefinePlugin({ + credentials: JSON.stringify(await defaultProvider()()) + }) + ], + node: { + util: 'empty' + } +}))() diff --git a/modules/example-browser/webpack_configs/kms.webpack.config.js b/modules/example-browser/webpack_configs/kms.webpack.config.js index b648c6fd1..b94eab418 100644 --- a/modules/example-browser/webpack_configs/kms.webpack.config.js +++ b/modules/example-browser/webpack_configs/kms.webpack.config.js @@ -27,11 +27,13 @@ module.exports = (async () => ({ }, output: { filename: 'kms_simple_bundle.js', - path: path.resolve(__dirname, '..', 'build') + path: path.resolve(__dirname, '..', 'build'), + library: 'test', + libraryTarget: 'var' }, plugins: [ new webpack.DefinePlugin({ - 'AWS_CREDENTIALS': JSON.stringify(await defaultProvider()()) + credentials: JSON.stringify(await defaultProvider()()) }) ], node: { diff --git a/modules/example-browser/webpack_configs/multi_keyring.webpack.config.js b/modules/example-browser/webpack_configs/multi_keyring.webpack.config.js index 480ee0c5f..0a745bf64 100644 --- a/modules/example-browser/webpack_configs/multi_keyring.webpack.config.js +++ b/modules/example-browser/webpack_configs/multi_keyring.webpack.config.js @@ -27,11 +27,13 @@ module.exports = (async () => ({ }, output: { filename: 'multi_keyring_bundle.js', - path: path.resolve(__dirname, '..', 'build') + path: path.resolve(__dirname, '..', 'build'), + library: 'test', + libraryTarget: 'var' }, plugins: [ new webpack.DefinePlugin({ - 'AWS_CREDENTIALS': JSON.stringify(await defaultProvider()()) + credentials: JSON.stringify(await defaultProvider()()) }) ], node: { diff --git a/modules/example-browser/webpack_configs/rsa.webpack.config.js b/modules/example-browser/webpack_configs/rsa.webpack.config.js index d5c0aed12..bf96f2c98 100644 --- a/modules/example-browser/webpack_configs/rsa.webpack.config.js +++ b/modules/example-browser/webpack_configs/rsa.webpack.config.js @@ -25,6 +25,8 @@ module.exports = { }, output: { filename: 'rsa_simple_bundle.js', - path: path.resolve(__dirname, '..', 'build') + path: path.resolve(__dirname, '..', 'build'), + library: 'test', + libraryTarget: 'var' } } diff --git a/modules/example-node/src/caching_cmm.ts b/modules/example-node/src/caching_cmm.ts new file mode 100644 index 000000000..9d951938e --- /dev/null +++ b/modules/example-node/src/caching_cmm.ts @@ -0,0 +1,161 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use + * this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { KmsKeyringNode, encrypt, decrypt, NodeCachingMaterialsManager, getLocalCryptographicMaterialsCache } from '@aws-crypto/client-node' + +export async function cachingCMMNodeSimpleTest () { + /* A KMS CMK is required to generate the data key. + * You need kms:GenerateDataKey permission on the CMK in generatorKeyId. + */ + const generatorKeyId = 'arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt' + + /* Adding alternate KMS keys that can decrypt. + * Access to kms:Encrypt is required for every CMK in keyIds. + * You might list several keys in different AWS Regions. + * This allows you to decrypt the data in any of the represented Regions. + * In this example, the generator key + * and the additional key are actually the same CMK. + * In `generatorId`, this CMK is identified by its alias ARN. + * In `keyIds`, this CMK is identified by its key ARN. + * In practice, you would specify different CMKs, + * or omit the `keyIds` parameter. + * This is *only* to demonstrate how the CMK ARNs are configured. + */ + const keyIds = ['arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f'] + + /* The KMS keyring must be configured with the desired CMKs + * This example passes the keyring to the caching CMM + * instead of using it directly. + */ + const keyring = new KmsKeyringNode({ generatorKeyId, keyIds }) + + /* Create a cache to hold the data keys (and related cryptographic material). + * This example uses the local cache provided by the Encryption SDK. + * The `capacity` value represents the maximum number of entries + * that the cache can hold. + * To make room for an additional entry, + * the cache evicts the oldest cached entry. + * Both encrypt and decrypt requests count independently towards this threshold. + * Entries that exceed any cache threshold are actively removed from the cache. + * By default, the SDK checks one item in the cache every 60 seconds (60,000 milliseconds). + * To change this frequency, pass in a `proactiveFrequency` value + * as the second parameter. This value is in milliseconds. + */ + const capacity = 100 + const cache = getLocalCryptographicMaterialsCache(capacity) + + /* The partition name lets multiple caching CMMs share the same local cryptographic cache. + * By default, the entries for each CMM are cached separately. However, if you want these CMMs to share the cache, + * use the same partition name for both caching CMMs. + * If you don't supply a partition name, the Encryption SDK generates a random name for each caching CMM. + * As a result, sharing elements in the cache MUST be an intentional operation. + */ + const partition = 'local partition name' + + /* maxAge is the time in milliseconds that an entry will be cached. + * Elements are actively removed from the cache. + */ + const maxAge = 1000 * 60 + + /* The maximum amount of bytes that will be encrypted under a single data key. + * This value is optional, + * but you should configure the lowest value possible. + */ + const maxBytesEncrypted = 100 + + /* The maximum number of messages that will be encrypted under a single data key. + * This value is optional, + * but you should configure the lowest value possible. + */ + const maxMessagesEncrypted = 10 + + const cachingCMM = new NodeCachingMaterialsManager({ + backingMaterials: keyring, + cache, + partition, + maxAge, + maxBytesEncrypted, + maxMessagesEncrypted + }) + + /* Encryption context is a *very* powerful tool for controlling + * and managing access. + * When you pass an encryption context to the encrypt function, + * the encryption context is cryptographically bound to the ciphertext. + * If you don't pass in the same encryption context when decrypting, + * the decrypt function fails. + * The encryption context is ***not*** secret! + * Encrypted data is opaque. + * You can use an encryption context to assert things about the encrypted data. + * The encryption context helps you to determine + * whether the ciphertext you retrieved is the ciphertext you expect to decrypt. + * For example, if you are are only expecting data from 'us-west-2', + * the appearance of a different AWS Region in the encryption context can indicate malicious interference. + * See: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + * + * Also, cached data keys are reused ***only*** when the encryption contexts passed into the functions are an exact case-sensitive match. + * See: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/data-caching-details.html#caching-encryption-context + */ + const encryptionContext = { + stage: 'demo', + purpose: 'simple demonstration app', + origin: 'us-west-2' + } + + /* Find data to encrypt. A simple string. */ + const cleartext = 'asdf' + + /* Encrypt the data. + * The caching CMM only reuses data keys + * when it know the length (or an estimate) of the plaintext. + * If you do not know the length, + * because the data is a stream + * provide an estimate of the largest expected value. + * + * If your estimate is smaller than the actual plaintext length + * the AWS Encryption SDK will throw an exception. + * + * If the plaintext is not a stream, + * the AWS Encryption SDK uses the actual plaintext length + * instead of any length you provide. + */ + const { result } = await encrypt(cachingCMM, cleartext, { encryptionContext, plaintextLength: 4 }) + + /* Decrypt the data. + * NOTE: This decrypt request will not use the data key + * that was cached during the encrypt operation. + * Data keys for encrypt and decrypt operations are cached separately. + */ + const { plaintext, messageHeader } = await decrypt(cachingCMM, result) + + /* Grab the encryption context so you can verify it. */ + const { encryptionContext: decryptedContext } = messageHeader + + /* Verify the encryption context. + * If you use an algorithm suite with signing, + * the Encryption SDK adds a name-value pair to the encryption context that contains the public key. + * Because the encryption context might contain additional key-value pairs, + * do not include a test that requires that all key-value pairs match. + * Instead, verify that the key-value pairs that you supplied to the `encrypt` function are included in the encryption context that the `decrypt` function returns. + */ + Object + .entries(encryptionContext) + .forEach(([key, value]) => { + if (decryptedContext[key] !== value) throw new Error('Encryption Context does not match expected values') + }) + + /* Return the values so the code can be tested. */ + return { plaintext, result, cleartext, messageHeader } +} diff --git a/modules/example-node/test/index.test.ts b/modules/example-node/test/index.test.ts index 7393dd687..73b1971e5 100644 --- a/modules/example-node/test/index.test.ts +++ b/modules/example-node/test/index.test.ts @@ -22,6 +22,7 @@ import { kmsSimpleTest } from '../src/kms_simple' import { kmsStreamTest } from '../src/kms_stream' import { aesTest } from '../src/aes_simple' import { multiKeyringTest } from '../src/multi_keyring' +import { cachingCMMNodeSimpleTest } from '../src/caching_cmm' import { readFileSync } from 'fs' describe('test', () => { @@ -55,4 +56,10 @@ describe('test', () => { expect(plaintext.toString()).to.equal(cleartext) }) + + it('caching CMM node', async () => { + const { cleartext, plaintext } = await cachingCMMNodeSimpleTest() + + expect(plaintext.toString()).to.equal(cleartext) + }) })