├── .gitignore ├── cli.js ├── demo.png ├── index.js ├── package.json ├── readme.md ├── src ├── _create-kms-key.js ├── _decrypt.js ├── _encrypt.js ├── _lock.js ├── _write-s3.js ├── _write.js ├── create.js ├── delete.js ├── env.js ├── nuke.js ├── rand.js ├── read.js ├── reset.js ├── versions.js └── write.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var chalk = require('chalk') 3 | var prereq = [] 4 | 5 | if (!process.env.AWS_PROFILE) { 6 | prereq.push('AWS_PROFILE') 7 | } 8 | 9 | if (!process.env.AWS_REGION) { 10 | prereq.push('AWS_REGION') 11 | } 12 | 13 | var fail = prereq.length != 0 14 | if (fail) { 15 | console.log(chalk.red('Error!') + ' ' + chalk.yellow('Missing env variables:')) 16 | prereq.forEach(p=> { 17 | console.log(chalk.dim(' - ') + chalk.cyan(p)) 18 | }) 19 | process.exit(1) 20 | } 21 | 22 | // prereq check passed; grab deps 23 | var pad = require('lodash.padstart') 24 | var strftime = require('strftime') 25 | var end = require('lodash.padend') 26 | var secrets = require('.') 27 | var yargs = require('yargs') 28 | 29 | // setup the cli args 30 | var argv = require('yargs') 31 | .usage('Usage: keystash [option]') 32 | .example('keystash', 'List all secrets') 33 | .example('keystash bukkit keyname keyvalue', 'Save a secret') 34 | .example('keystash bukkit keyname', 'Get a secret') 35 | .describe('create', 'Create a keystash bucket') 36 | .alias('c', 'create') 37 | .describe('put', 'Encrypt a key/value pair') 38 | .alias('p', 'put') 39 | .describe('get', 'Get a value by key') 40 | .alias('g', 'get') 41 | .describe('delete', 'Delete a key') 42 | .alias('d', 'delete') 43 | .describe('reset', 'Remove all keys in the latest version') 44 | .describe('rand', 'Generate random data for a key') 45 | .describe('versions', 'List all versions') 46 | .alias('v', 'versions') 47 | .describe('nuke', 'Remove all versions') 48 | .alias('n', 'nuke') 49 | .describe('json', 'Export secrets as JSON to stdout') 50 | .help('h') 51 | .alias('h', 'help') 52 | .version() 53 | .argv 54 | 55 | // helper to check for undefined 56 | // @returns {Boolean} 57 | var undef = val=> typeof val === 'undefined' 58 | 59 | // if no args given 60 | var blank = argv._.length === 0 && 61 | argv.h === false && 62 | argv.help === false && 63 | undef(argv.create) && 64 | undef(argv.put) && 65 | undef(argv.get) && 66 | undef(argv.delete) && 67 | undef(argv.reset) && 68 | undef(argv.versions) && 69 | undef(argv.nuke) 70 | 71 | // read secrets from s3 bucket 72 | var listSecrets = (argv._.length === 1 || argv._.length === 2) && 73 | argv.h === false && 74 | argv.help === false && 75 | undef(argv.create) && 76 | undef(argv.put) && 77 | undef(argv.get) && 78 | undef(argv.delete) && 79 | undef(argv.rand) && 80 | undef(argv.reset) && 81 | undef(argv.versions) && 82 | undef(argv.nuke) && 83 | undef(argv.json) && 84 | undef(argv.env) 85 | 86 | // create a bucket for secrets 87 | var createBucket = argv._.length === 1 && 88 | argv.h === false && 89 | argv.help === false && 90 | argv.create && 91 | undef(argv.put) && 92 | undef(argv.get) && 93 | undef(argv.delete) && 94 | undef(argv.reset) && 95 | undef(argv.versions) && 96 | undef(argv.nuke) 97 | 98 | // add a secret 99 | var putKey = argv._.length >= 1 && 100 | argv.h === false && 101 | argv.help === false && 102 | undef(argv.create) && 103 | argv.put && 104 | undef(argv.get) && 105 | undef(argv.delete) && 106 | undef(argv.reset) && 107 | undef(argv.rand) && 108 | undef(argv.versions) && 109 | undef(argv.nuke) 110 | 111 | // support for syntax: 112 | // keystash BUCKET KEY VAL 113 | if (argv._.length === 3 && undef(argv.put)) { 114 | putKey = true 115 | argv.put = argv._[1] 116 | argv._[1] = argv._[2] 117 | } 118 | 119 | // generate a key 120 | var isRand = !!(argv._.length >= 1 && 121 | argv.h === false && 122 | argv.help === false && 123 | argv.rand) 124 | 125 | // get a secret 126 | var getKey = argv._.length >= 1 && 127 | argv.h === false && 128 | argv.help === false && 129 | undef(argv.create) && 130 | undef(argv.put) && 131 | argv.get && 132 | undef(argv.delete) && 133 | undef(argv.reset) && 134 | undef(argv.rand) && 135 | undef(argv.versions) && 136 | undef(argv.nuke) 137 | 138 | // remove a secret 139 | var delKey = argv._.length >= 1 && 140 | argv.h === false && 141 | argv.help === false && 142 | undef(argv.create) && 143 | undef(argv.put) && 144 | undef(argv.get) && 145 | argv.delete && 146 | undef(argv.rand) && 147 | undef(argv.reset) && 148 | undef(argv.versions) && 149 | undef(argv.nuke) 150 | 151 | // reset all secrets in the latest version 152 | var reset = argv._.length >= 1 && 153 | argv.h === false && 154 | argv.help === false && 155 | undef(argv.create) && 156 | undef(argv.put) && 157 | undef(argv.get) && 158 | undef(argv.delete) && 159 | argv.reset && 160 | undef(argv.versions) && 161 | undef(argv.nuke) 162 | 163 | // get all versions 164 | var versions = argv._.length >= 1 && 165 | argv.h === false && 166 | argv.help === false && 167 | undef(argv.create) && 168 | undef(argv.put) && 169 | undef(argv.get) && 170 | undef(argv.delete) && 171 | undef(argv.reset) && 172 | undef(argv.rand) && 173 | argv.versions && 174 | undef(argv.nuke) 175 | 176 | // remove all versions 177 | var nuke = argv._.length >= 1 && 178 | argv.h === false && 179 | argv.help === false && 180 | undef(argv.create) && 181 | undef(argv.put) && 182 | undef(argv.get) && 183 | undef(argv.delete) && 184 | undef(argv.reset) && 185 | undef(argv.versions) && 186 | argv.nuke 187 | 188 | // export JSON to stdout 189 | var json = argv._.length >= 1 && 190 | argv.h === false && 191 | argv.help === false && 192 | argv.json 193 | 194 | // export to current process 195 | var env = argv._.length >= 1 && 196 | argv.h === false && 197 | argv.help === false && 198 | argv.env 199 | 200 | // command not found 201 | var notFound = !nuke && 202 | !versions && 203 | !reset && 204 | !delKey && 205 | !putKey && 206 | !getKey && 207 | !createBucket && 208 | !listSecrets && 209 | !isRand && 210 | !blank && 211 | !json && 212 | !env 213 | 214 | function list(ns, title, result) { 215 | console.log('') 216 | var head = chalk.dim(ns) 217 | var title = chalk.dim.cyan(title) 218 | console.log(' ' + head + ' ' + title) 219 | console.log(chalk.dim('────────────────────────────────────────────────────────────')) 220 | if (result) { 221 | var out = '' 222 | Object.keys(result).forEach(key=> { 223 | var keyname = pad(chalk.dim(key), 35) 224 | var value = end(chalk.cyan(result[key]), 35) 225 | out += `${keyname} ${value}\n` 226 | }) 227 | console.log(out) 228 | } 229 | process.exit() 230 | } 231 | 232 | if (blank) { 233 | var err = chalk.red('Error!') 234 | var msg = chalk.yellow(' Missing S3 bucket argument.') 235 | console.log(err + msg) 236 | process.exit(1) 237 | } 238 | 239 | if (listSecrets) { 240 | var ns = argv._[0] 241 | var key = argv._[1] 242 | secrets.read({ 243 | ns 244 | }, 245 | function _read(err, result) { 246 | if (err && err.name === 'NoSuchBucket') { 247 | var err = chalk.red('Error!') 248 | var msg = chalk.yellow(' S3 bucket not found.') 249 | console.log(err + msg) 250 | process.exit(1) 251 | } 252 | else if (err) { 253 | var error = chalk.red('Error!') 254 | var msg = chalk.yellow(err.message) 255 | console.log(error + msg) 256 | process.exit(1) 257 | } 258 | else if (key) { 259 | console.log(result[key]) 260 | process.exit() 261 | } 262 | else { 263 | list(ns, 'secrets key', result) 264 | } 265 | }) 266 | } 267 | 268 | if (createBucket) { 269 | var ns = argv._[0] 270 | secrets.create({ 271 | ns 272 | }, 273 | function _create(err, result) { 274 | if (err) { 275 | var error = chalk.red('Error!') 276 | var msg = chalk.yellow(err.message) 277 | console.log(error + msg) 278 | process.exit(1) 279 | } 280 | else { 281 | var rex = chalk.green('Created') 282 | var msg = chalk.cyan(` ${ns}`) 283 | console.log(rex + msg) 284 | process.exit() 285 | } 286 | }) 287 | } 288 | 289 | if (putKey) { 290 | var ns = argv._[0] 291 | var key = argv.put 292 | var value = argv._[1] 293 | if (!key || !value) { 294 | console.log('missing key and/or value') 295 | process.exit(1) 296 | } 297 | secrets.write({ 298 | ns, 299 | key, 300 | value, 301 | }, 302 | function _read(err, result) { 303 | if (err) { 304 | console.log(err) 305 | process.exit(1) 306 | } 307 | else { 308 | list(ns, 'put', result) 309 | } 310 | }) 311 | } 312 | 313 | if (isRand) { 314 | var ns = argv._[0] 315 | var key = argv.rand 316 | if (!key) { 317 | console.log('missing key') 318 | process.exit(1) 319 | } 320 | secrets.rand({ 321 | ns, 322 | key, 323 | }, 324 | function _read(err, result) { 325 | if (err) { 326 | console.log(err) 327 | process.exit(1) 328 | } 329 | else { 330 | list(ns, 'rand', result) 331 | } 332 | }) 333 | } 334 | 335 | if (getKey) { 336 | var key = argv.get 337 | secrets.read({ 338 | ns: argv._[0] 339 | }, 340 | function _read(err, result) { 341 | if (err && err.name === 'NoSuchBucket') { 342 | console.log('bucket not found') 343 | process.exit(1) 344 | } 345 | else if (err) { 346 | console.log(err) 347 | process.exit(1) 348 | } 349 | else { 350 | console.log(result[key]) 351 | process.exit() 352 | } 353 | }) 354 | } 355 | 356 | if (delKey) { 357 | var ns = argv._[0] 358 | var key = argv.delete 359 | secrets.delete({ 360 | ns, 361 | key, 362 | }, 363 | function _read(err, result) { 364 | if (err) { 365 | console.log(err) 366 | process.exit(1) 367 | } 368 | else { 369 | list(ns, 'deleted', result) 370 | } 371 | }) 372 | } 373 | 374 | if (reset) { 375 | var ns = argv._[0] 376 | secrets.reset({ 377 | ns, 378 | }, 379 | function _reset(err, result) { 380 | if (err) { 381 | console.log(err) 382 | process.exit(1) 383 | } 384 | else { 385 | list(ns, 'reset', result) 386 | } 387 | }) 388 | } 389 | 390 | if (versions) { 391 | var ns = argv._[0] 392 | var key = argv._[1] 393 | var version = typeof argv.versions === 'boolean'? false : argv.versions 394 | if (version && !key) { 395 | // display one version 396 | secrets.read({ 397 | ns, 398 | version, 399 | }, 400 | function _read(err, result) { 401 | if (err && err.name === 'NoSuchBucket') { 402 | console.log('bucket not found') 403 | process.exit(1) 404 | } 405 | else if (err) { 406 | console.log(err) 407 | process.exit(1) 408 | } 409 | else { 410 | list(ns, version, result) 411 | process.exit() 412 | } 413 | }) 414 | } 415 | else if (version && key) { 416 | // display one key of a version 417 | secrets.read({ 418 | ns, 419 | version, 420 | }, 421 | function _read(err, result) { 422 | if (err && err.name === 'NoSuchBucket') { 423 | console.log('bucket not found') 424 | process.exit(1) 425 | } 426 | else if (err) { 427 | console.log(err) 428 | process.exit(1) 429 | } 430 | else { 431 | console.log(result[key]) 432 | process.exit() 433 | } 434 | }) 435 | } 436 | else { 437 | // display all versions 438 | secrets.versions({ 439 | ns, 440 | }, 441 | function _versions(err, result) { 442 | if (err) { 443 | console.log(err) 444 | process.exit(1) 445 | } 446 | else { 447 | function remap(v) { 448 | var obj = {} 449 | var d = strftime('%B %d, %Y %l:%M:%S', v.modified) 450 | obj[d] = v.version 451 | return obj 452 | } 453 | var versions = result.map(remap).reduce((a,b)=> Object.assign({}, a, b)) 454 | list(ns, 'versions', versions) 455 | } 456 | }) 457 | } 458 | } 459 | 460 | if (nuke) { 461 | var ns = argv._[0] 462 | secrets.nuke({ 463 | ns, 464 | }, 465 | function _nuke(err, result) { 466 | if (err) { 467 | console.log(err) 468 | process.exit(1) 469 | } 470 | else { 471 | list(ns, 'nuked', versions) 472 | process.exit() 473 | } 474 | }) 475 | } 476 | 477 | if (json) { 478 | var ns = argv._[0] 479 | secrets.read({ 480 | ns 481 | }, 482 | function _read(err, result) { 483 | if (err && err.name === 'NoSuchBucket') { 484 | var err = chalk.red('Error!') 485 | var msg = chalk.yellow(' S3 bucket not found.') 486 | console.log(err + msg) 487 | process.exit(1) 488 | } 489 | else if (err) { 490 | var error = chalk.red('Error!') 491 | var msg = chalk.yellow(err.message) 492 | console.log(error + msg) 493 | process.exit(1) 494 | } 495 | else { 496 | console.log(JSON.stringify(result, null, 2)) 497 | process.exit() 498 | } 499 | }) 500 | } 501 | 502 | if (notFound) { 503 | var err = chalk.red('Error!') 504 | var msg = chalk.cyan(` Command not found.`) 505 | console.log(err + msg) 506 | process.exit(1) 507 | } 508 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beginner-corp/keystash/a495077853ab888d33d894fad21530589ddc4ff2/demo.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var create = require('./src/create') 2 | var del = require('./src/delete') 3 | var env = require('./src/env') 4 | var nuke = require('./src/nuke') 5 | var rand = require('./src/rand') 6 | var read = require('./src/read') 7 | var reset = require('./src/reset') 8 | var versions = require('./src/versions') 9 | var write = require('./src/write') 10 | 11 | if (!process.env.AWS_PROFILE) { 12 | throw ReferenceError('missing process.env.AWS_PROFILE') 13 | } 14 | 15 | if (!process.env.AWS_REGION) { 16 | throw ReferenceError('missing process.env.AWS_REGION') 17 | } 18 | 19 | module.exports = { 20 | create, 21 | delete: del, 22 | env, 23 | nuke, 24 | rand, 25 | read, 26 | reset, 27 | versions, 28 | write, 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keystash", 3 | "version": "1.0.8", 4 | "description": "Store secrets in S3 using KMS envelope encryption.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "NODE_ENV=testing AWS_PROFILE=smallwins AWS_REGION=us-east-1 node test | tap-spec", 8 | "start": "AWS_PROFILE=personal AWS_REGION=us-east-1 ./cli.js" 9 | }, 10 | "bin": { 11 | "keystash": "cli.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/smallwins/keystash.git" 16 | }, 17 | "keywords": [ 18 | "aws", 19 | "s3", 20 | "kms", 21 | "secrets", 22 | "env", 23 | "environment", 24 | "variables", 25 | "encryption", 26 | "crypto", 27 | "envelope" 28 | ], 29 | "author": "Brian LeRoux ", 30 | "license": "Apache-2.0", 31 | "devDependencies": { 32 | "tap-spec": "^4.1.1", 33 | "tape": "^4.8.0" 34 | }, 35 | "dependencies": { 36 | "@smallwins/validate": "^4.3.0", 37 | "aws-sdk": "^2.108.0", 38 | "chalk": "^2.3.2", 39 | "locks": "^0.2.2", 40 | "lodash.padend": "^4.6.1", 41 | "lodash.padstart": "^4.6.1", 42 | "run-parallel": "^1.1.6", 43 | "run-waterfall": "^1.1.3", 44 | "strftime": "^0.10.0", 45 | "yargs": "^8.0.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 🔑💌 `keystash` 2 | 3 | > Save secrets in AWS S3 with KMS envelope encryption 4 | 5 | - Save key/value pairs in an S3 Bucket with KMS envelope encryption 6 | - Additional serverside encryption with S3 7 | - Automatic S3 versioning for durability 8 | - Generate random key data 9 | - Use as a module 10 | - Bundles a simple CLI 11 | 12 | Perfect for: 13 | 14 | - Centralized key management with minimalist command line interface 15 | - Environment variables in modules and npm scripts 16 | - Lightweight and secure personal key value store 17 | 18 | ![demo](https://raw.githubusercontent.com/smallwins/keystash/master/demo.png) 19 | 20 | ## prereq 21 | 22 | - AWS account credentials setup `.aws/credentials` 23 | - `AWS_PROFILE` and `AWS_REGION` environment variables 24 | 25 | > ✨ Tip `export` default `AWS_PROFILE` and `AWS_REGION` env vars your in `.bashrc` or `.bash_profile` and override as neccessary on the command line or in `package.json` to make working with different stashes easy 26 | 27 | ## install 28 | 29 | ```bash 30 | npm i -g keystash 31 | ``` 32 | 33 | ## command line interface 34 | 35 | ``` 36 | keystash [options] 37 | ``` 38 | 39 | ### exmaples 40 | 41 | Setup an S3 bucket: 42 | 43 | - `keystash my-bucket --create ` create an S3 bucket for storing secrets 44 | 45 | Read secrets: 46 | 47 | - `keystash my-bucket` read encrypted secrets from S3 bucket 48 | - `keystash my-bucket BIG_SEKRET` to read a value to stdout 49 | 50 | Write secrets: 51 | 52 | - `keystash my-bucket BIG_SEKRET xxx-xxx` save a secret `BIG_SEKRET` with value `xxx-xxx` 53 | - `keystash my-bucket --rand BIG_SEKRET` to generate (really!) random key data 54 | - `keystash my-bucket --delete BIG_SEKRET` remove `BIG_SEKRET` 55 | - `keystash my-bucket --reset` remove all secrets from latest version 56 | 57 | Working with versions: 58 | 59 | - `keystash my-bucket --versions` list all versions 60 | - `keystash my-bucket --versions some-version-id` get secrets for a given version 61 | - `keystash my-bucket --versions some-version-id some-key` get the key for the given version 62 | - `keystash my-bucket --nuke` remove all versions 63 | 64 | Run `keystash --help` to see short switches. 65 | 66 | ## module install and usage 67 | 68 | Use this module in `npm scripts`. 69 | 70 | ``` 71 | npm i keystash --save 72 | ``` 73 | 74 | ```javascript 75 | // package.json 76 | { 77 | "start": "DB_URL=${keystash some-bucket DB_URL} node index" 78 | } 79 | ``` 80 | 81 | Or a bash script: 82 | 83 | ```bash 84 | AWS_PROFILE=xxx 85 | AWS_REGION=xxx 86 | NODE_ENV=testing 87 | DB_URL=`keystash cred-bucket DB_URL` 88 | 89 | node index 90 | ``` 91 | 92 | Or in module code itself: 93 | 94 | ```javascript 95 | var keystash = require('keystash') 96 | 97 | keystash.read({ns: 's3-bucket-name'}, console.log) 98 | ``` 99 | 100 | See tests for more examples! 101 | 102 | ## api 103 | 104 | ```javascript 105 | var keystash = require('keystash') 106 | ``` 107 | 108 | - `keystash.create({ns}, err=>)` create a `keystash` S3 bucket 109 | - `keystash.delete({ns, key}, (err, result)=>)` remove a key 110 | - `keystash.env({ns}, err=>)` add secrets to `process.env` 111 | - `keystash.nuke({ns}, err=>)` remove all versions 112 | - `keystash.rand({key}, (err, result)=>)` generate a random key 113 | - `keystash.read({ns}, (err, result)=>)` get all secrets 114 | - `keystash.read({ns, version}, (err, result)=>)` get all secrets for given version 115 | - `keystash.reset({ns}, (err, result)=>)` remove all secrets from the current version 116 | - `keystash.versions({ns}, (err, result)=>)` get all versions 117 | - `keystash.write({ns, key, value}, (err, result)=>)` save a secret 118 | 119 | ## acknowledgements 120 | 121 | This module is inspired by [credstash](https://github.com/fugue/credstash). This module differs in that its JavaScript instead of Python and uses S3 to persist secrets instead of Dynamo. [Read more about credstash here.](https://blog.fugue.co/2015-04-21-aws-kms-secrets.html) 122 | 123 | Also thx to [Matt Weagle](https://twitter.com/mweagle) for encouraging KMS envelope encryption and [Ben Kehoe](https://twitter.com/ben11kehoe) for suggesting to use the S3 Object Metadata property to store the KMS cipher. 124 | -------------------------------------------------------------------------------- /src/_create-kms-key.js: -------------------------------------------------------------------------------- 1 | var aws = require('aws-sdk') 2 | var waterfall = require('run-waterfall') 3 | 4 | /** 5 | * creates a kms master key alias/arc 6 | */ 7 | module.exports = function _createKey(callback) { 8 | var kms = new aws.KMS 9 | kms.listAliases({}, function _aliases(err, results) { 10 | if (err) throw err 11 | var hasArc = !!results.Aliases.find(a=> a.AliasName === 'alias/arc') 12 | if (hasArc) { 13 | callback() 14 | } 15 | else { 16 | waterfall([ 17 | function _createKey(callback) { 18 | kms.createKey({ 19 | Tags: [{ 20 | TagKey: 'CreatedBy', 21 | TagValue: 'JSF Architect' 22 | }] 23 | }, callback) 24 | }, 25 | function _addAlias(key, callback) { 26 | kms.createAlias({ 27 | AliasName: 'alias/arc', 28 | TargetKeyId: key.KeyMetadata.KeyId, 29 | }, callback) 30 | } 31 | ], callback) 32 | } 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /src/_decrypt.js: -------------------------------------------------------------------------------- 1 | var assert = require('@smallwins/validate/assert') 2 | var crypto = require('crypto') 3 | var aws = require('aws-sdk') 4 | 5 | module.exports = function _decrypt(params, callback) { 6 | assert(params, { 7 | encrypted: String, 8 | cipher: Buffer, 9 | }) 10 | 11 | var {encrypted, cipher} = params 12 | 13 | var kms = new aws.KMS 14 | kms.decrypt({ 15 | CiphertextBlob: cipher 16 | }, 17 | function _decrypt(err, result) { 18 | if (err) throw err 19 | 20 | var key = result.Plaintext.toString() 21 | var decipher = crypto.createDecipher('aes-256-ctr', key) 22 | var result = decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8') 23 | 24 | callback(null, JSON.parse(result)) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /src/_encrypt.js: -------------------------------------------------------------------------------- 1 | var assert = require('@smallwins/validate/assert') 2 | var crypto = require('crypto') 3 | var aws = require('aws-sdk') 4 | 5 | /** 6 | * encrypts a javscript object payload 7 | */ 8 | module.exports = function _encrypt(params, callback) { 9 | assert(params, { 10 | payload: Object, 11 | ns: String, 12 | }) 13 | var kms = new aws.KMS 14 | kms.generateDataKey({ 15 | KeyId: 'alias/arc', 16 | KeySpec: 'AES_256' 17 | }, 18 | function _dataKey(err, result) { 19 | if (err) { 20 | callback(err) 21 | } 22 | else { 23 | var {CiphertextBlob, Plaintext, KeyId} = result 24 | var cipher = crypto.createCipher('aes-256-ctr', Plaintext.toString()) 25 | var text = JSON.stringify(params.payload) 26 | var encrypted = cipher.update(text, 'utf8', 'hex') + cipher.final('hex') 27 | callback(null, {encrypted, cipher: CiphertextBlob.toString('base64')}) 28 | } 29 | }) 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/_lock.js: -------------------------------------------------------------------------------- 1 | var locks = require('locks') 2 | // create a singleton lock 3 | module.exports = locks.createReadWriteLock() 4 | -------------------------------------------------------------------------------- /src/_write-s3.js: -------------------------------------------------------------------------------- 1 | var assert = require('@smallwins/validate/assert') 2 | var aws = require('aws-sdk') 3 | 4 | module.exports = function _writeS3(params, callback) { 5 | assert(params, { 6 | ns: String, 7 | cipher: String, 8 | payload: String, 9 | }) 10 | 11 | var s3 = new aws.S3 12 | s3.putObject({ 13 | Body: params.payload, 14 | Bucket: params.ns, 15 | Key: 'archive', 16 | ContentType: 'text/plain', 17 | ServerSideEncryption: 'AES256', 18 | Metadata: { 19 | cipher: params.cipher 20 | } 21 | }, 22 | function _done(err){ 23 | if (err) { 24 | callback(err) 25 | } 26 | else { 27 | callback() 28 | } 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /src/_write.js: -------------------------------------------------------------------------------- 1 | var assert = require('@smallwins/validate/assert') 2 | var waterfall = require('run-waterfall') 3 | var crypto = require('crypto') 4 | var aws = require('aws-sdk') 5 | var encrypt = require('./_encrypt') 6 | var write = require('./_write-s3') 7 | 8 | module.exports = function _write(params, callback) { 9 | assert(params, { 10 | payload: Object, 11 | ns: String, 12 | }) 13 | waterfall([ 14 | function e(callback) { 15 | encrypt(params, callback) 16 | }, 17 | function w(result, callback) { 18 | write({ 19 | ns: params.ns, 20 | payload: result.encrypted, 21 | cipher: result.cipher, 22 | }, callback) 23 | } 24 | ], 25 | function _done(err) { 26 | if (err) { 27 | callback(err) 28 | } 29 | else { 30 | callback(null, params.payload) 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /src/create.js: -------------------------------------------------------------------------------- 1 | var aws = require('aws-sdk') 2 | var assert = require('@smallwins/validate/assert') 3 | var parallel = require('run-parallel') 4 | var waterfall = require('run-waterfall') 5 | var createKey = require('./_create-kms-key') 6 | 7 | /** 8 | * create a versioned s3 bucket and master kms key named alias/arc 9 | */ 10 | module.exports = function create(params, callback) { 11 | assert(params, { 12 | ns: String, 13 | }) 14 | function createBucket(callback) { 15 | var s3 = new aws.S3 16 | s3.createBucket({ 17 | Bucket: params.ns, 18 | ACL: 'private', 19 | }, callback) 20 | } 21 | waterfall([ 22 | function _create(callback) { 23 | parallel([ 24 | createBucket, 25 | createKey, 26 | ], callback) 27 | }, 28 | function _version(bucket, callback) { 29 | var s3 = new aws.S3 30 | s3.putBucketVersioning({ 31 | Bucket: params.ns, 32 | VersioningConfiguration: { 33 | MFADelete: 'Disabled', 34 | Status: 'Enabled', 35 | } 36 | }, callback) 37 | }, 38 | ], callback) 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/delete.js: -------------------------------------------------------------------------------- 1 | var assert = require('@smallwins/validate/assert') 2 | var read = require('./read') 3 | var _write = require('./_write') 4 | 5 | /** 6 | * delete a key/value from a ns 7 | */ 8 | module.exports = function _delete(params, callback) { 9 | assert(params, { 10 | ns: String, 11 | key: String, 12 | }) 13 | read(params, function _r(err, result) { 14 | if (err) { 15 | callback(err) 16 | } 17 | else { 18 | delete result[params.key] 19 | _write({ 20 | ns: params.ns, 21 | payload: result, 22 | }, callback) 23 | } 24 | }) 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/env.js: -------------------------------------------------------------------------------- 1 | var assert = require('@smallwins/validate/assert') 2 | var read = require('./read') 3 | 4 | /** 5 | * loads secrets for given ns into process.env 6 | */ 7 | module.exports = function env(params, callback) { 8 | assert(params, { 9 | ns: String 10 | }) 11 | read(params, function _read(err, result) { 12 | if (err) { 13 | callback(err) 14 | } 15 | else { 16 | Object.keys(result).forEach(key=> { 17 | process.env[key] = result[key] 18 | }) 19 | callback() 20 | } 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /src/nuke.js: -------------------------------------------------------------------------------- 1 | var assert = require('@smallwins/validate/assert') 2 | var aws = require('aws-sdk') 3 | var waterfall = require('run-waterfall') 4 | var create = require('./create') 5 | var write = require('./write') 6 | 7 | /** 8 | * reset a ns 9 | */ 10 | module.exports = function _nuke(params, callback) { 11 | assert(params, { 12 | ns: String, 13 | }) 14 | var s3 = new aws.S3 15 | waterfall([ 16 | function(callback) { 17 | s3.listObjectVersions({ 18 | Bucket: params.ns, 19 | Prefix: 'archive', 20 | }, callback) 21 | }, 22 | function(result, callback) { 23 | // loop thru versions deleting 24 | function remap(v) { 25 | var obj = { 26 | Key: 'archive' 27 | } 28 | obj.VersionId = v.VersionId 29 | return obj 30 | } 31 | s3.deleteObjects({ 32 | Bucket: params.ns, 33 | Delete: { 34 | Objects: result.Versions.map(remap) 35 | }, 36 | }, callback) 37 | } 38 | ], callback) 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/rand.js: -------------------------------------------------------------------------------- 1 | var assert = require('@smallwins/validate/assert') 2 | var aws = require('aws-sdk') 3 | var waterfall = require('run-waterfall') 4 | var read = require('./read') 5 | var _write = require('./_write') 6 | 7 | /** 8 | * write a random value to a key 9 | */ 10 | module.exports = function write(params, callback) { 11 | assert(params, { 12 | key: String, 13 | }) 14 | var kms = new aws.KMS 15 | waterfall([ 16 | function(callback) { 17 | read(params, callback) 18 | }, 19 | function(secrets, callback) { 20 | kms.generateRandom({ 21 | NumberOfBytes: 18 22 | }, 23 | function _rando(err, result) { 24 | if (err) { 25 | callback(err) 26 | } 27 | else { 28 | secrets[params.key] = result.Plaintext.toString('base64') 29 | callback(null, secrets) 30 | } 31 | }) 32 | }, 33 | function(result, callback) { 34 | _write({ 35 | ns: params.ns, 36 | payload: result, 37 | }, callback) 38 | }, 39 | ], callback) 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/read.js: -------------------------------------------------------------------------------- 1 | var assert = require('@smallwins/validate/assert') 2 | var aws = require('aws-sdk') 3 | var write = require('./_write') 4 | var _decrypt = require('./_decrypt') 5 | 6 | module.exports = function read(params, callback) { 7 | 8 | assert(params, { 9 | ns: String, 10 | //version: String <-- optional param 11 | }) 12 | 13 | // query params for the archive 14 | var query = { 15 | Bucket: params.ns, 16 | Key: 'archive', 17 | } 18 | 19 | // optional version param 20 | if (params.version) { 21 | query.VersionId = params.version 22 | } 23 | 24 | var s3 = new aws.S3 25 | s3.getObject(query, function _got(err, result) { 26 | if (err) { 27 | // if it doesn't exist create an empty obj 28 | write({ 29 | ns: params.ns, 30 | payload: {}, 31 | }, callback) 32 | } 33 | else { 34 | _decrypt({ 35 | encrypted: result.Body.toString(), 36 | cipher: Buffer.from(result.Metadata.cipher, 'base64') 37 | }, callback) 38 | } 39 | }) 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/reset.js: -------------------------------------------------------------------------------- 1 | var assert = require('@smallwins/validate/assert') 2 | var write = require('./_write') 3 | 4 | /** 5 | * reset a ns 6 | */ 7 | module.exports = function _reset(params, callback) { 8 | assert(params, { 9 | ns: String, 10 | }) 11 | write({ 12 | ns: params.ns, 13 | payload: {}, 14 | }, callback) 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/versions.js: -------------------------------------------------------------------------------- 1 | var assert = require('@smallwins/validate/assert') 2 | var aws = require('aws-sdk') 3 | 4 | /** 5 | * list all versions for given ns 6 | */ 7 | module.exports = function versions(params, callback) { 8 | assert(params, { 9 | ns: String, 10 | }) 11 | var s3 = new aws.S3 12 | s3.listObjectVersions({ 13 | Bucket: params.ns, 14 | Prefix: 'archive', 15 | }, 16 | function _versions(err, result) { 17 | if (err) { 18 | callback(err) 19 | } 20 | else { 21 | var ver = v=> ({version: v.VersionId, modified: v.LastModified}) 22 | callback(null, result.Versions.map(ver)) 23 | } 24 | }) 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/write.js: -------------------------------------------------------------------------------- 1 | var assert = require('@smallwins/validate/assert') 2 | var waterfall = require('run-waterfall') 3 | var crypto = require('crypto') 4 | var aws = require('aws-sdk') 5 | var encrypt = require('./_encrypt') 6 | var writeS3 = require('./_write-s3') 7 | var read = require('./read') 8 | var _write = require('./_write') 9 | var lock = require('./_lock') 10 | 11 | /** 12 | * write a key/value to a ns 13 | */ 14 | module.exports = function write(params, callback) { 15 | var Any = v=> true 16 | assert(params, { 17 | ns: String, 18 | key: String, 19 | value: Any, 20 | }) 21 | lock.writeLock(function _writeLock() { 22 | waterfall([ 23 | function(callback) { 24 | read(params, callback) 25 | }, 26 | function(result, callback) { 27 | result[params.key] = params.value 28 | _write({ 29 | ns: params.ns, 30 | payload: result, 31 | }, callback) 32 | }, 33 | ], 34 | function _done(err, result) { 35 | lock.unlock() 36 | if (err) callback(err) 37 | else callback(null, result) 38 | }) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var env = require('.') 3 | var ns = 'foo-test-creds-ns' 4 | 5 | test('env', t=> { 6 | t.plan(8) 7 | t.ok(env.create, 'env.create') 8 | t.ok(env.delete, 'env.delete') 9 | t.ok(env.nuke, 'env.nuke') 10 | t.ok(env.rand, 'env.rand') 11 | t.ok(env.read, 'env.read') 12 | t.ok(env.reset, 'env.reset') 13 | t.ok(env.versions, 'env.versions') 14 | t.ok(env.write, 'env.write') 15 | }) 16 | 17 | test('env.create', t=> { 18 | t.plan(1) 19 | env.create({ 20 | ns 21 | }, 22 | function _create(err, result) { 23 | if (err) { 24 | t.fail(err) 25 | } 26 | else { 27 | t.ok(result, 'got result') 28 | console.log(result) 29 | } 30 | }) 31 | }) 32 | 33 | test('env.read', t=> { 34 | t.plan(1) 35 | env.read({ 36 | ns 37 | }, 38 | function _read(err, result) { 39 | if (err) { 40 | t.fail(err) 41 | } 42 | else { 43 | t.ok(result, 'got result') 44 | console.log(result) 45 | } 46 | }) 47 | }) 48 | 49 | test('env.write', t=> { 50 | t.plan(1) 51 | env.write({ 52 | ns, 53 | key: 'hi', 54 | value: 'world', 55 | }, 56 | function _write(err, result) { 57 | if (err) { 58 | t.fail(err) 59 | } 60 | else { 61 | t.ok(result, 'got result') 62 | console.log(result) 63 | } 64 | }) 65 | }) 66 | 67 | test('env.write async safe', t=> { 68 | t.plan(6) 69 | var config = { 70 | PRIVATE_API: 'https://private.example.com', 71 | PUBLIC_API: 'https://api.example.com', 72 | SLACK_ENDPOINT: 'https://slack.example.com', 73 | FOO: true, 74 | BAZ: 1111, 75 | } 76 | env.reset({ 77 | ns 78 | }, 79 | function _reset(err, result) { 80 | if (err) { 81 | t.fail(err) 82 | } 83 | else { 84 | t.ok(result, 'reset') 85 | Object.keys(config).forEach(k=> { 86 | env.write({ 87 | ns, 88 | key: k, 89 | value: config[k], 90 | }, 91 | function _read(err, result) { 92 | if (err) { 93 | t.fail(err) 94 | } 95 | else { 96 | t.ok(result, 'got result') 97 | console.log(result) 98 | console.log(config) 99 | } 100 | }) 101 | }) 102 | } 103 | }) 104 | }) 105 | 106 | test('env.delete', t=> { 107 | t.plan(1) 108 | env.delete({ 109 | ns, 110 | key: 'hi', 111 | }, 112 | function _read(err, result) { 113 | if (err) { 114 | t.fail(err) 115 | } 116 | else { 117 | t.ok(result, 'got result') 118 | console.log(result) 119 | } 120 | }) 121 | }) 122 | 123 | var v 124 | test('env.versions', t=> { 125 | t.plan(1) 126 | env.versions({ 127 | ns, 128 | }, 129 | function _read(err, result) { 130 | if (err) { 131 | t.fail(err) 132 | } 133 | else { 134 | t.ok(result, 'got result') 135 | console.log(result.length) 136 | } 137 | }) 138 | }) 139 | --------------------------------------------------------------------------------