├── .circleci
└── config.yml
├── .github
└── FUNDING.yml
├── .gitignore
├── .npmignore
├── .prettierignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── changelog.md
├── package.json
├── renovate.json
├── rollup.config.js
├── src
├── cli.test.ts
├── cli.ts
├── decrypt.test.ts
├── decrypt.ts
├── encrypt.test.ts
├── encrypt.ts
├── index.test.ts
├── index.ts
└── types.ts
├── test
├── setup.ts
└── test.d.ts
├── tsconfig.json
├── tslint.json
└── yarn.lock
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | #
4 | # Jobs
5 | #
6 |
7 | jobs:
8 |
9 | # This job builds the base project directory (e.g. ~/package.json)
10 | build:
11 | docker:
12 | - image: circleci/node:8
13 | steps:
14 | - checkout
15 | - restore_cache:
16 | key: dependency-cache-{{ checksum "package.json" }}
17 | - run: npm install
18 | - save_cache:
19 | key: dependency-cache-{{ checksum "package.json" }}
20 | paths:
21 | - node_modules
22 |
23 | # This job runs the lint tool on the whole repository
24 | lint:
25 | docker:
26 | - image: circleci/node:8
27 | steps:
28 | - checkout
29 | - restore_cache:
30 | key: dependency-cache-{{ checksum "package.json" }}
31 | - run:
32 | name: Security check
33 | command: npm run security-check
34 | - run:
35 | name: Lint
36 | command: npm run lint
37 |
38 | # runs the unit tests
39 | unit-test:
40 | docker:
41 | - image: circleci/node:8
42 | steps:
43 | - checkout
44 | - restore_cache:
45 | key: dependency-cache-{{ checksum "package.json" }}
46 | - run:
47 | name: Unit tests
48 | command: |
49 | npm run test &&
50 | ./node_modules/.bin/nyc report --temp-directory=coverage --reporter=text-lcov | ./node_modules/.bin/coveralls
51 |
52 | release:
53 | docker:
54 | - image: circleci/node:8@sha256:6c751e82876608a535426c12257956fd0f47e29c745b5028012944896faf6867
55 | steps:
56 | - checkout
57 | - restore_cache:
58 | key: dependency-cache-{{ checksum "package.json" }}
59 | - run: npm run semantic-release
60 |
61 | #
62 | # Workflows
63 | #
64 |
65 | workflows:
66 | version: 2
67 |
68 | build_test_release:
69 | jobs:
70 | - build
71 | - lint:
72 | requires:
73 | - build
74 | - unit-test:
75 | requires:
76 | - build
77 | - release:
78 | requires:
79 | - lint
80 | - unit-test
81 | filters:
82 | branches:
83 | only: master
84 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: adieuadieu
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Project specific
2 | dist/
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 |
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 | *.pid.lock
16 |
17 | # Directory for instrumented libs generated by jscoverage/JSCover
18 | lib-cov
19 |
20 | # Coverage directory used by tools like istanbul
21 | coverage
22 |
23 | # nyc test coverage
24 | .nyc_output
25 |
26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
27 | .grunt
28 |
29 | # Bower dependency directory (https://bower.io/)
30 | bower_components
31 |
32 | # node-waf configuration
33 | .lock-wscript
34 |
35 | # Compiled binary addons (http://nodejs.org/api/addons.html)
36 | build/Release
37 |
38 | # Dependency directories
39 | node_modules/
40 | jspm_packages/
41 |
42 | # Typescript v1 declaration files
43 | typings/
44 |
45 | # Optional npm cache directory
46 | .npm
47 |
48 | # Optional eslint cache
49 | .eslintcache
50 |
51 | # Optional REPL history
52 | .node_repl_history
53 |
54 | # Output of 'npm pack'
55 | *.tgz
56 |
57 | # Yarn Integrity file
58 | .yarn-integrity
59 |
60 | # dotenv environment variables file
61 | .env
62 |
63 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *.test.js
2 | *.test.d.ts
3 | *.js.map
4 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .nyc_output/
2 | dist/
3 | package.json
4 | package-lock.json
5 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib",
3 | "prettier.eslintIntegration": false,
4 | "eslint.enable": false
5 | }
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Marco Lüthy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # aws-kms-thingy
2 |
3 | Convenience wrapper around the AWS Node.js SDK to simplify encrypting/decrypting secrets with the AWS KMS service. Suitable for use with AWS Lambda.
4 |
5 | [](https://circleci.com/gh/adieuadieu/aws-kms-thingy)
6 | [](https://coveralls.io/github/adieuadieu/aws-kms-thingy)
7 | []()
8 | []()
9 | [](https://github.com/adieuadieu/aws-kms-thingy)
10 |
11 | ## Contents
12 |
13 | 1. [Features](#features)
14 | 1. [Usage](#usage)
15 | 1. [With the CLI](#with-the-cli)
16 | 1. [With AWS Lambda](#with-aws-lambda)
17 | 1. [With Multiple Secrets](#with-multiple-secrets)
18 | 1. [Locally In Development](#locally-in-development)
19 | 1. [API](#api)
20 | 1. [Related Thingies](#related-thingies)
21 | 1. [License](#license)
22 |
23 | ### Features
24 |
25 | * Unencrypted strings simply returned, useful for testing/local development
26 | * Encrypt/decrypt multiple values in one go
27 | * Results are cached, so multiple decrypt/encrypt calls incur only a single call to the AWS SDK
28 | * CLI to encrypt/decrypt secrets
29 | * Well tested
30 |
31 | ## Usage
32 |
33 | The module assumes that the Amazon SDK has access to AWS credentials that are able to access the KMS key used for encryption and decryption.
34 |
35 | ```bash
36 | npm install aws-kms-thingy aws-sdk@^2
37 | ```
38 |
39 | ### With the CLI
40 |
41 | Encrypt with:
42 |
43 | ```bash
44 | aws-kms-thingy encrypt
45 | ```
46 |
47 | You'll be prompted for the string to encrypt.
48 |
49 | Decrypt with:
50 |
51 | ```bash
52 | aws-kms-thingy decrypt
53 | ```
54 |
55 | You'll be prompted for the encrypted string to decrypt.
56 |
57 | ### With AWS Lambda
58 |
59 | Safe to use within a Lambda handler. After cold-start, decrypted values are cached so subsequent invocations won't incur an AWS KMS API call:
60 |
61 | ```javascript
62 | const { decrypt } = require('aws-kms-thingy')
63 |
64 | module.exports.myLambdaHandler = (event, context, callback) => {
65 | decrypt(process.env.SOME_API_TOKEN) // Only incurs network call on cold-start
66 | .then(doStuffWithDecryptedApiToken)
67 | .then(resultOrWhatever => callback(null, resultOrWhatever))
68 | .catch(callback)
69 | }
70 | ```
71 |
72 | ### With Multiple Secrets
73 |
74 | Decrypt multiple values in parallel
75 |
76 | ```typescript
77 | import { decrypt } from 'aws-kms-thingy'
78 |
79 | const [
80 | decryptedApiToken1,
81 | decryptedApiToken2,
82 | decryptedDatabasePassword,
83 | somethingElseSecret,
84 | ] = await decrypt([
85 | process.env.API_TOKEN_1,
86 | process.env.API_TOKEN_2,
87 | process.env.DATABASE_PASSWORD,
88 | process.env.SOMETHING_ELSE_SECRET,
89 | ])
90 | ```
91 |
92 | ### Locally In Development
93 |
94 | Providing a non-base64 encoded value will skip en/decrypting with AWS KMS and just return the same value. This is useful in local development where you may not be necessary to have your secrets encrypted. This helps to avoid the need to write development environment exception code:
95 |
96 | ```typescript
97 | import { decrypt } from 'aws-kms-thingy'
98 |
99 | process.env.DATABASE_PASSWORD = 'foobar'
100 |
101 | const dbPassword = await decrypt(process.env.DATABASE_PASSWORD)
102 |
103 | console.log(dbPassword) // "foobar"
104 | ```
105 |
106 | An `undefined` value is also OK. This does nothing and returns undefined. Useful when environment variables are unset in local development.
107 |
108 | ```typescript
109 | process.env.DATABASE_PASSWORD = undefined // e.g. not set in development
110 |
111 | const dbPassword = await decrypt(process.env.DATABASE_PASSWORD)
112 |
113 | console.log(dbPassword) // undefined
114 | ```
115 |
116 | Alternatively, one can also disable en/decryption entirely with `DISABLE_AWS_KMS_THINGY` environment variable:
117 |
118 | ```typescript
119 | import { decrypt } from 'aws-kms-thingy'
120 |
121 | process.env.DISABLE_AWS_KMS_THINGY = 'true'
122 |
123 | const token = await decrypt('aHR0cDovL2JpdC5seS8xVHFjd243')
124 |
125 | console.log(token) // "aHR0cDovL2JpdC5seS8xVHFjd243"
126 | ```
127 |
128 | ## API
129 |
130 | **Methods**
131 |
132 | * [`encrypt(parameters)`](#api-encrypt)
133 | * [`decrypt(ciphertext)`](#api-decrypt)
134 |
135 | ---
136 |
137 |
138 |
139 | ### encrypt(parameters)
140 |
141 | ```typescript
142 | interface InterfaceEncryptParameters {
143 | readonly plaintext: string
144 | readonly keyId: string
145 | }
146 |
147 | async function encrypt(
148 | parameters:
149 | | InterfaceEncryptParameters
150 | | ReadonlyArray,
151 | ): Promise>
152 | ```
153 |
154 | Encrypt a plaintext string. Requires a AWS KMS key ID (or key Arn).
155 |
156 | ```js
157 | const ciphertext = await encrypt({
158 | plaintext: 'secret text',
159 | keyId:
160 | 'arn:aws:kms:eu-west-1:000000000000:key/55kkmm11-aann-99ff-mmaa-3322115566hh',
161 | })
162 | ```
163 |
164 | ---
165 |
166 |
167 |
168 | ### decrypt(ciphertext)
169 |
170 | AWS KMS encrypted ciphertext contains metadata so it is not necessary to provide context or key ID.
171 |
172 | ```typescript
173 | async function decrypt(
174 | ciphertext: undefined | string | ReadonlyArray,
175 | ): Promise>
176 | ```
177 |
178 | Decrypt KMS-encrypted ciphertext.
179 |
180 | ```js
181 | const plaintext = await decrypt('aHR0cDovL2JpdC5seS8xVHFjd243')
182 | ```
183 |
184 | ## Related Thingies
185 |
186 | * [aws-s3-thingy](https://github.com/adieuadieu/aws-s3-thingy)
187 | * [alagarr](https://github.com/adieuadieu/alagarr) — AWS Lambda/API Gateway Request/Response Thingy
188 | * [aws-kms-crypt](https://github.com/sjakthol/aws-kms-crypt)
189 |
190 | ## License
191 |
192 | **aws-kms-thingy** © [Marco Lüthy](https://github.com/adieuadieu). Released under the [MIT](./LICENSE) license.
193 | Authored and maintained by Marco Lüthy with help from [contributors](https://github.com/adieuadieu/aws-kms-thingy/contributors).
194 |
195 | > [github.com/adieuadieu](https://github.com/adieuadieu) · GitHub [@adieuadieu](https://github.com/adieuadieu) · Twitter [@adieuadieu](https://twitter.com/adieuadieu) · Medium [@marco.luethy](https://medium.com/@marco.luethy)
196 |
--------------------------------------------------------------------------------
/changelog.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aws-kms-thingy",
3 | "version": "0.0.0",
4 | "description": "A wrapper/helper utility for encrypting/decrypting with AWS KMS",
5 | "keywords": [
6 | "aws",
7 | "kms",
8 | "encrypt",
9 | "decrypt",
10 | "helper",
11 | "utility",
12 | "cli",
13 | "command",
14 | "secrets",
15 | "tool"
16 | ],
17 | "engines": {
18 | "npm": ">= 3.0.0",
19 | "node": ">= 6.10.0"
20 | },
21 | "main": "dist/lib.cjs.js",
22 | "module": "dist/lib.es.js",
23 | "types": "dist/src/index.d.ts",
24 | "files": [
25 | "dist"
26 | ],
27 | "bin": {
28 | "akt": "dist/cli.js",
29 | "aws-kms-thingy": "dist/cli.js"
30 | },
31 | "repository": {
32 | "type": "git",
33 | "url": "https://github.com/adieuadieu/aws-kms-thingy.git"
34 | },
35 | "author": "Marco Lüthy (https://github.com/adieuadieu)",
36 | "license": "MIT",
37 | "bugs": {
38 | "url": "https://github.com/adieuadieu/aws-kms-thingy/issues"
39 | },
40 | "homepage": "https://github.com/adieuadieu/aws-kms-thingy",
41 | "scripts": {
42 | "prebuild": "npm run clean",
43 | "build": "tsc -d && rollup -c",
44 | "clean": "rm -Rf dist",
45 | "dev": "tsc -w",
46 | "precommit": "lint-staged",
47 | "prettier": "prettier --write",
48 | "test": "jest",
49 | "watch:test": "jest --watch",
50 | "lint": "npm run lint:tsc && npm run lint:tslint",
51 | "lint:tslint": "tslint -p tsconfig.json -t stylish",
52 | "lint:tsc": "tsc -p tsconfig.json --noEmit --pretty",
53 | "preversion": "git pull && yarn check --integrity && yarn security-check && yarn lint && yarn test",
54 | "postversion": "git push --tags origin HEAD",
55 | "prepublishOnly": "npm run lint && npm test && npm run security-check && npm run build",
56 | "security-check": "nsp check",
57 | "upgrade-dependencies": "yarn upgrade-interactive --latest --exact",
58 | "commitmsg": "commitlint -e $GIT_PARAMS",
59 | "semantic-release": "semantic-release"
60 | },
61 | "dependencies": {},
62 | "devDependencies": {
63 | "@commitlint/cli": "7.2.1",
64 | "@commitlint/config-conventional": "7.1.2",
65 | "@types/aws-lambda": "8.10.15",
66 | "@types/jest": "23.3.10",
67 | "@types/node": "9.6.40",
68 | "aws-sdk-mock": "4.3.0",
69 | "coveralls": "3.0.2",
70 | "husky": "0.14.3",
71 | "jest": "23.6.0",
72 | "lint-staged": "8.1.0",
73 | "nsp": "3.2.1",
74 | "nyc": "13.0.1",
75 | "prettier": "1.15.3",
76 | "rollup": "0.67.4",
77 | "rollup-plugin-hashbang": "1.0.1",
78 | "rollup-plugin-node-resolve": "3.4.0",
79 | "semantic-release": "15.12.4",
80 | "ts-jest": "23.10.5",
81 | "tslint": "5.11.0",
82 | "tslint-functional-preset": "1.5.0",
83 | "typescript": "3.0.3"
84 | },
85 | "peerDependencies": {
86 | "aws-sdk": "^2.197.0"
87 | },
88 | "commitlint": {
89 | "extends": [
90 | "@commitlint/config-conventional"
91 | ]
92 | },
93 | "prettier": {
94 | "printWidth": 80,
95 | "semi": false,
96 | "singleQuote": true,
97 | "trailingComma": "all",
98 | "useTabs": false
99 | },
100 | "lint-staged": {
101 | "*.{ts,tsx}": [
102 | "yarn prettier",
103 | "yarn lint",
104 | "git add"
105 | ],
106 | "*.{json}": [
107 | "yarn prettier",
108 | "git add"
109 | ]
110 | },
111 | "jest": {
112 | "bail": false,
113 | "collectCoverage": true,
114 | "roots": [
115 | "src/"
116 | ],
117 | "setupTestFrameworkScriptFile": "/test/setup.ts",
118 | "transform": {
119 | "^.+\\.tsx?$": "/node_modules/ts-jest/preprocessor.js"
120 | },
121 | "testEnvironment": "node",
122 | "testRegex": "\\.test\\.ts$",
123 | "moduleFileExtensions": [
124 | "ts",
125 | "js"
126 | ],
127 | "coverageThreshold": {
128 | "global": {
129 | "branches": 100,
130 | "functions": 100,
131 | "lines": 100,
132 | "statements": 100
133 | }
134 | },
135 | "coveragePathIgnorePatterns": [
136 | "/node_modules/",
137 | "/test/"
138 | ]
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base", "docker:disable"],
3 | "automerge": true,
4 | "automergeType": "branch-push",
5 | "commitMessagePrefix": "⬆️ ",
6 | "major": {
7 | "automerge": false
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import hashbang from 'rollup-plugin-hashbang'
2 | import resolve from 'rollup-plugin-node-resolve'
3 |
4 | const external = ['aws-sdk', 'readline']
5 |
6 | const plugins = [
7 | hashbang(),
8 | resolve({
9 | extensions: ['.js'],
10 | jsnext: true,
11 | main: true,
12 | module: true,
13 | }),
14 | ]
15 |
16 | export default [
17 | {
18 | external,
19 | input: 'dist/src/index.js',
20 | output: [
21 | { file: 'dist/lib.cjs.js', format: 'cjs' },
22 | { file: 'dist/lib.es.js', format: 'es' },
23 | ],
24 | plugins,
25 | },
26 | {
27 | external,
28 | input: 'dist/src/cli.js',
29 | output: [{ file: 'dist/cli.js', format: 'cjs' }],
30 | plugins,
31 | },
32 | ]
33 |
--------------------------------------------------------------------------------
/src/cli.test.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable no-expression-statement
2 | import { main } from './cli'
3 |
4 | const argvBase: ReadonlyArray = [
5 | '/usr/local/bin/node',
6 | '/home/mockUser',
7 | ]
8 |
9 | const mockDecryptedValue = 'foobar'
10 | const mockEncryptedValue = Buffer.from(mockDecryptedValue).toString('base64')
11 |
12 | jest.mock('readline', () => ({
13 | clearLine: jest.fn(),
14 | createInterface: () => ({
15 | close: jest.fn(),
16 | question: (questionText: any, callback: any) =>
17 | callback(
18 | questionText.match('encrypted')
19 | ? mockEncryptedValue
20 | : mockDecryptedValue,
21 | ),
22 | }),
23 | moveCursor: jest.fn(),
24 | }))
25 |
26 | describe('cli', () => {
27 | it('should be able to encrypt a string', async () => {
28 | const result = await main([...argvBase, 'encrypt'])
29 |
30 | expect(result).toBe(mockEncryptedValue)
31 | })
32 |
33 | it('should be able to decrypt an encrypted string', async () => {
34 | const result = await main([...argvBase, 'decrypt'])
35 |
36 | expect(result).toBe(mockDecryptedValue)
37 | })
38 |
39 | it('should print usage info if no command specified', async () => {
40 | process.stdout.columns = undefined // tslint:disable-line no-object-mutation
41 |
42 | jest.resetAllMocks()
43 | jest.resetModules()
44 | jest.resetModuleRegistry()
45 |
46 | const consoleSpy = jest.spyOn(console, 'info')
47 | const cli = require('./cli')
48 |
49 | const result = await cli.main()
50 |
51 | expect(result).toBe(undefined)
52 | expect(consoleSpy).toHaveBeenCalled()
53 | expect(consoleSpy.mock.calls[0][0]).toContain('Usage:')
54 | })
55 | })
56 |
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // tslint:disable no-console
3 | import * as readline from 'readline'
4 | import * as lib from './'
5 | import { IndexSignature } from './types'
6 |
7 | const COLUMNS = process.stdout.columns || 32
8 |
9 | const askQuestion = (
10 | question: string,
11 | prompt = '→',
12 | sensitive = false,
13 | ): Promise => {
14 | const line = readline.createInterface({
15 | input: process.stdin,
16 | output: process.stdout,
17 | })
18 |
19 | return new Promise(resolve =>
20 | line.question(
21 | `\n${question}:\n\n${prompt} `,
22 | answer =>
23 | !line.close() &&
24 | (sensitive
25 | ? !readline.moveCursor(process.stdin, 0, -1) &&
26 | !readline.clearLine(process.stdin, 0) &&
27 | !console.log(`${prompt} ${'*'.repeat(answer.length)}`)
28 | : true) &&
29 | resolve(answer.trim()),
30 | ),
31 | )
32 | }
33 |
34 | const printUsageText = () =>
35 | console.info(`
36 | Usage: aws-kms-thingy [command]
37 |
38 | Commands:
39 | - encrypt Encrypt a string with an AWS KMS key
40 | - decrypt Decrypt ciphertext encrypted with AWS KMS
41 | `)
42 |
43 | const prettyResult = (title: string, text: string) =>
44 | !console.log(
45 | `\n${'〰️'.repeat(
46 | (COLUMNS - (title.length + 2)) / 4,
47 | )} ${title} ${'〰️'.repeat((COLUMNS - (title.length + 2)) / 4)}\n`,
48 | text,
49 | `\n${'〰️'.repeat(COLUMNS / 2)}\n`,
50 | ) && text
51 |
52 | async function encrypt(): Promise> {
53 | const keyId =
54 | process.env.AWS_KMS_KEY_ARN ||
55 | (await askQuestion('Please enter AWS KMS Key ARN to use'))
56 | const plaintext = await askQuestion('Enter plain text to encrypt', '🔓', true)
57 | const result = (await lib.encrypt({ keyId, plaintext })) as string
58 |
59 | return prettyResult('🔐 Encrypted', result)
60 | }
61 |
62 | async function decrypt(): Promise> {
63 | const ciphertext = await askQuestion('Enter ciphertext to decrypt', '🔐')
64 | const result = (await lib.decrypt(ciphertext)) as string
65 |
66 | return prettyResult('🔓 Decrypted', result)
67 | }
68 |
69 | export async function main([, , action] = process.argv): Promise {
70 | const actions: IndexSignature = {
71 | decrypt,
72 | encrypt,
73 | }
74 |
75 | return action && action in actions
76 | ? !console.log(`aws-kms-thingy ${action}`) && actions[action]()
77 | : printUsageText()
78 | }
79 |
80 | // tslint:disable-next-line no-unused-expression no-expression-statement
81 | main().catch(console.error) // ¯\_(ツ)_/¯
82 |
--------------------------------------------------------------------------------
/src/decrypt.test.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable no-expression-statement
2 | import kmsDecrypt, { dictionary } from './decrypt'
3 |
4 | const mockDecryptedValue = 'foobar'
5 | const mockEncryptedValue = Buffer.from(mockDecryptedValue).toString('base64')
6 |
7 | describe('decrypt()', () => {
8 | it('should decrypt an encrypted string and store it in cache', async () => {
9 | const result = await kmsDecrypt(mockEncryptedValue)
10 |
11 | expect(result).toBe(mockDecryptedValue)
12 | expect(dictionary.get(mockEncryptedValue)).toBe(mockDecryptedValue)
13 | })
14 |
15 | it('should use cache if item exists in cache', async () => {
16 | const encryptedValue = Buffer.from('foobar').toString('base64')
17 |
18 | dictionary.clear()
19 | dictionary.set(encryptedValue, 'cached')
20 |
21 | const result = await kmsDecrypt(encryptedValue)
22 |
23 | expect(result).toBe('cached')
24 | })
25 |
26 | it('should return value as-is if not a base64 encoded secret', async () => {
27 | const result = await kmsDecrypt(mockDecryptedValue)
28 |
29 | expect(result).toBe(mockDecryptedValue)
30 | })
31 |
32 | it('should return original value if error occurred decrypting with SDK', async () => {
33 | const mockError = Buffer.from('mockError').toString('base64')
34 | const result = await kmsDecrypt(mockError)
35 |
36 | expect(result).toBe(mockError)
37 | })
38 |
39 | it('should return value as is if DISABLE_AWS_KMS_THINGY is set', async () => {
40 | // tslint:disable-next-line no-object-mutation
41 | process.env.DISABLE_AWS_KMS_THINGY = 'true'
42 |
43 | const result = await kmsDecrypt(mockEncryptedValue)
44 |
45 | expect(result).toBe(mockEncryptedValue)
46 |
47 | // tslint:disable-next-line no-object-mutation no-delete
48 | delete process.env.DISABLE_AWS_KMS_THINGY
49 | })
50 | })
51 |
--------------------------------------------------------------------------------
/src/decrypt.ts:
--------------------------------------------------------------------------------
1 | import { KMS } from 'aws-sdk' // tslint:disable-line:no-implicit-dependencies
2 |
3 | const kms = new KMS()
4 |
5 | const isBase64 = /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$/
6 |
7 | export const dictionary = new Map()
8 |
9 | async function decrypt(ciphertext: string): Promise {
10 | const result = await kms
11 | .decrypt({ CiphertextBlob: Buffer.from(ciphertext, 'base64') })
12 | .promise()
13 | const plaintext = result.Plaintext ? result.Plaintext.toString() : ciphertext
14 |
15 | return dictionary.set(ciphertext, plaintext) && plaintext
16 | }
17 |
18 | export default (ciphertext: string): Promise | string =>
19 | ciphertext.length === 0 || // empty string?
20 | process.env.DISABLE_AWS_KMS_THINGY || // we shouldn't decrypt?
21 | !isBase64.test(ciphertext) // not a base64 encoded ciphertext?
22 | ? String(ciphertext)
23 | : // previously decrypted and in cache?
24 | dictionary.get(ciphertext) ||
25 | // decrypt it
26 | decrypt(ciphertext)
27 |
--------------------------------------------------------------------------------
/src/encrypt.test.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable no-expression-statement
2 | import kmsEncrypt, { dictionary } from './encrypt'
3 |
4 | const mockDecryptedValue = 'foobar'
5 | const mockEncryptedValue = Buffer.from(mockDecryptedValue).toString('base64')
6 | const keyId = 'foobar-key'
7 |
8 | const mock = (plaintext: string) => ({ keyId, plaintext })
9 |
10 | describe('encrypt()', () => {
11 | it('should encrypt a string and store it in cache', async () => {
12 | const result = await kmsEncrypt(mock(mockDecryptedValue))
13 |
14 | expect(result).toBe(mockEncryptedValue)
15 | expect(dictionary.get(mockDecryptedValue)).toBe(mockEncryptedValue)
16 | })
17 |
18 | it('should use cache if item exists in cache', async () => {
19 | dictionary.clear()
20 | dictionary.set(keyId + mockDecryptedValue, 'cached')
21 |
22 | const result = await kmsEncrypt(mock(mockDecryptedValue))
23 |
24 | expect(result).toBe('cached')
25 | })
26 |
27 | it('should return original value if error occurred encrypting with SDK', async () => {
28 | const mockError = 'mockError'
29 | const result = await kmsEncrypt(mock(mockError))
30 |
31 | expect(result).toBe(mockError)
32 | })
33 |
34 | it('should return value as is if DISABLE_AWS_KMS_THINGY is set', async () => {
35 | // tslint:disable-next-line no-object-mutation
36 | process.env.DISABLE_AWS_KMS_THINGY = 'true'
37 |
38 | const result = await kmsEncrypt(mock(mockDecryptedValue))
39 |
40 | expect(result).toBe(mockDecryptedValue)
41 |
42 | // tslint:disable-next-line no-object-mutation no-delete
43 | delete process.env.DISABLE_AWS_KMS_THINGY
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/src/encrypt.ts:
--------------------------------------------------------------------------------
1 | import { KMS } from 'aws-sdk' // tslint:disable-line:no-implicit-dependencies
2 |
3 | const kms = new KMS()
4 |
5 | export const dictionary = new Map()
6 |
7 | export interface InterfaceEncryptParameters {
8 | readonly plaintext: string
9 | readonly keyId: string
10 | }
11 |
12 | async function encrypt({
13 | plaintext,
14 | keyId,
15 | }: InterfaceEncryptParameters): Promise {
16 | const result = await kms
17 | .encrypt({ KeyId: keyId, Plaintext: plaintext })
18 | .promise()
19 |
20 | const ciphertext = result.CiphertextBlob
21 | ? (result.CiphertextBlob as Buffer).toString('base64')
22 | : plaintext
23 |
24 | return dictionary.set(plaintext, ciphertext) && ciphertext
25 | }
26 |
27 | export default ({
28 | plaintext,
29 | keyId,
30 | }: InterfaceEncryptParameters): Promise | string =>
31 | process.env.DISABLE_AWS_KMS_THINGY // we shouldn't encrypt?
32 | ? String(plaintext)
33 | : // previously encrypted and in cache?
34 | dictionary.get(keyId + plaintext) ||
35 | // encrypt it
36 | encrypt({ plaintext, keyId })
37 |
--------------------------------------------------------------------------------
/src/index.test.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable no-expression-statement
2 | import { decrypt, encrypt } from './'
3 |
4 | const mockDecryptedValue = 'foobar'
5 | const mockEncryptedValue = Buffer.from(mockDecryptedValue).toString('base64')
6 |
7 | const mockArrayOfDecryptedValues: ReadonlyArray = [
8 | 'foobar-1',
9 | 'foobar-2',
10 | 'foobar-3',
11 | 'foobar-4',
12 | ]
13 |
14 | const mockArrayOfEncryptedValues: ReadonlyArray = [
15 | Buffer.from(mockArrayOfDecryptedValues[0]).toString('base64'),
16 | Buffer.from(mockArrayOfDecryptedValues[1]).toString('base64'),
17 | Buffer.from(mockArrayOfDecryptedValues[2]).toString('base64'),
18 | Buffer.from(mockArrayOfDecryptedValues[3]).toString('base64'),
19 | ]
20 |
21 | describe('lib', () => {
22 | it('should encrypt an string', async () => {
23 | const result = await encrypt({
24 | keyId: 'foobar-key',
25 | plaintext: mockDecryptedValue,
26 | })
27 |
28 | expect(result).toBe(mockEncryptedValue)
29 | })
30 |
31 | it('should encrypt an array of strings', async () => {
32 | const result = await encrypt(
33 | mockArrayOfDecryptedValues.map(item => ({
34 | keyId: 'foobar-key',
35 | plaintext: item,
36 | })),
37 | )
38 |
39 | expect(result).toEqual(mockArrayOfEncryptedValues)
40 |
41 | // Tests the ReadonlyArray overload
42 | const [zero, one, two, three] = result
43 |
44 | expect(zero).toEqual(mockArrayOfEncryptedValues[0])
45 | expect(one).toEqual(mockArrayOfEncryptedValues[1])
46 | expect(two).toEqual(mockArrayOfEncryptedValues[2])
47 | expect(three).toEqual(mockArrayOfEncryptedValues[3])
48 | })
49 |
50 | it('should decrypt an encrypted string', async () => {
51 | const result = await decrypt(mockEncryptedValue)
52 |
53 | expect(result).toBe(mockDecryptedValue)
54 | })
55 |
56 | it('should decrypt an array of encrypted strings', async () => {
57 | const result = await decrypt(mockArrayOfEncryptedValues)
58 |
59 | expect(result).toEqual(mockArrayOfDecryptedValues)
60 |
61 | // Tests the ReadonlyArray overload
62 | const [zero, one, two, three] = result
63 |
64 | expect(zero).toEqual(mockArrayOfDecryptedValues[0])
65 | expect(one).toEqual(mockArrayOfDecryptedValues[1])
66 | expect(two).toEqual(mockArrayOfDecryptedValues[2])
67 | expect(three).toEqual(mockArrayOfDecryptedValues[3])
68 | })
69 |
70 | it('should be able to handle empty or undefined parameters', async () => {
71 | expect(await decrypt(undefined)).toBe(undefined)
72 | expect(await decrypt('')).toBe('')
73 | })
74 | })
75 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import kmsDecrypt from './decrypt'
2 | import kmsEncrypt, { InterfaceEncryptParameters } from './encrypt'
3 |
4 | export async function encrypt(
5 | parameters: InterfaceEncryptParameters,
6 | ): Promise
7 | export async function encrypt(
8 | parameters: ReadonlyArray,
9 | ): Promise>
10 | export async function encrypt(parameters: any): Promise {
11 | return 'plaintext' in parameters
12 | ? kmsEncrypt(parameters)
13 | : Promise.all(parameters.map(kmsEncrypt))
14 | }
15 |
16 | export async function decrypt(ciphertext: undefined): Promise
17 | export async function decrypt(ciphertext: string): Promise
18 | export async function decrypt(
19 | ciphertext: ReadonlyArray,
20 | ): Promise>
21 | export async function decrypt(ciphertext: any): Promise {
22 | return typeof ciphertext === 'undefined'
23 | ? undefined // useful in development when process.env.SECRET may be unset
24 | : typeof ciphertext === 'string'
25 | ? kmsDecrypt(ciphertext)
26 | : Promise.all(ciphertext.map(kmsDecrypt))
27 | }
28 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface IndexSignature {
2 | readonly [key: string]: any
3 | }
4 |
--------------------------------------------------------------------------------
/test/setup.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:no-expression-statement
2 | import * as AWS from 'aws-sdk'
3 | import * as mockAWS from 'aws-sdk-mock'
4 |
5 | mockAWS.setSDKInstance(AWS)
6 |
7 | mockAWS.mock('KMS', 'decrypt', (params: any, callback: any) => {
8 | const plain = params.CiphertextBlob.toString()
9 |
10 | return plain === 'mockError'
11 | ? callback(null, {})
12 | : callback(null, { Plaintext: plain })
13 | })
14 |
15 | mockAWS.mock(
16 | 'KMS',
17 | 'encrypt',
18 | ({ Plaintext: plaintext }: any, callback: any) => {
19 | return plaintext === 'mockError'
20 | ? callback(null, {})
21 | : callback(null, {
22 | CiphertextBlob: Buffer.from(plaintext),
23 | })
24 | },
25 | )
26 |
--------------------------------------------------------------------------------
/test/test.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'aws-sdk-mock'
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": false,
4 | "allowSyntheticDefaultImports": false,
5 | "declaration": true,
6 | "esModuleInterop": false,
7 | "forceConsistentCasingInFileNames": true,
8 | "lib": ["es2018", "esnext"],
9 | "module": "es2015",
10 | "moduleResolution": "node",
11 | "noImplicitAny": true,
12 | "noImplicitReturns": true,
13 | "noImplicitThis": true,
14 | "noUnusedParameters": true,
15 | "noUnusedLocals": true,
16 | "outDir": "dist",
17 | "pretty": true,
18 | "removeComments": true,
19 | "rootDir": ".",
20 | "skipLibCheck": false,
21 | "sourceMap": false,
22 | "strictNullChecks": true,
23 | "target": "es2017"
24 | },
25 | "exclude": ["node_modules", "dist", "test"]
26 | }
27 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint-functional-preset"]
3 | }
4 |
--------------------------------------------------------------------------------