├── .gitignore ├── .npmrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── bin └── config-shield ├── certs ├── demo.key └── new.key ├── lib ├── convert.js ├── drop-backup.js ├── get-keys.js ├── get-prop.js ├── index.js ├── load-from-file.js ├── remove-all.js ├── remove-prop.js ├── save-to-file.js ├── secure-config.js └── set-prop.js ├── package.json ├── scripts └── cli.js ├── secure-config.json └── test └── convert.tests.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | 11 | package-lock.json 12 | 13 | pids 14 | logs 15 | results 16 | 17 | npm-debug.log 18 | 19 | # Netbeans 20 | /nbproject/ 21 | 22 | # Webstorm Settings 23 | .idea/ 24 | !.idea/inspectionProfiles 25 | !.idea/jsLinters 26 | 27 | # Istanbul Output 28 | coverage*/ 29 | 30 | # NPM Dependencies 31 | node_modules/ 32 | 33 | # Visual Studio files 34 | *.suo 35 | *.sln 36 | *.nsproj 37 | 38 | # Sublime Project files 39 | *.sublime-project 40 | *.sublime-workspace 41 | 42 | # Eclipse 43 | .project 44 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "4.2" 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | **2020/01/21** 2 | 3 | * Security fix - Adar Zandberg from the CxSCA AppSec team at Checkmarx. 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 GoDaddy 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Config Shield 2 | 3 | [![Build Status](https://travis-ci.org/godaddy/node-config-shield.png)](https://travis-ci.org/godaddy/node-config-shield) [![NPM version](https://badge.fury.io/js/config-shield.png)](http://badge.fury.io/js/config-shield) [![Dependency Status](https://gemnasium.com/godaddy/node-config-shield.png)](https://gemnasium.com/godaddy/node-config-shield) [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/godaddy/node-config-shield/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 4 | 5 | [![NPM](https://nodei.co/npm/config-shield.png?downloads=true&stars=true)](https://www.npmjs.org/package/config-shield) 6 | 7 | 8 | ## About 9 | 10 | The mission behind this project is to provide a "safe" process from which to store 11 | properties sensitive in nature, in a manner that is both developer friendly as 12 | well as optimized for production use. 13 | 14 | 15 | 16 | ## Making configuration changes 17 | 18 | Install `config-shield` in your project: 19 | ``` 20 | npm install config-shield --save 21 | ``` 22 | Startup the command-line interface from root of application: 23 | ``` 24 | npm run config-shield 25 | : enter path of config (enter to use secure-config.json)> 26 | : enter path of private key> my.app.key 27 | set simple_property true 28 | set my-json-prop { "nested": { "values": [ 1, 2, 3 ] } } 29 | set null-prop null 30 | set evaluable-prop-as-string "null" 31 | set string-prop this will be stored as string if type cannot be determined 32 | set array-pop [ 1, 2, 3 ] 33 | set boolean-prop true 34 | set number-prop 5 35 | remove number-prop 36 | get my-json-prop 37 | : { "nested": { "values": [ 1, 2, 3 ] } } 38 | save 39 | : changes saved 40 | exit 41 | ``` 42 | Optionally you may also install `config-shield` globally: 43 | ``` 44 | npm install config-shield -g 45 | config-shield 46 | ``` 47 | 48 | ## Deploy your config 49 | 50 | This step should be built into your CICD process, to clone the applicable 51 | environment config and copy `secure-config.json` over. Ideally these 52 | assets will be in a limited-access store to avoid unnecessary risk. 53 | 54 | ***Do not under any circumstance store your production private keys within 55 | your project.*** 56 | 57 | 58 | ## Loading config from your App 59 | ``` 60 | var secureConfig = require('config-shield'); 61 | // one-time load 62 | secureConfig.load({ 63 | configPath: './secure-config.json', // not required if default 64 | privateKeyPath: '/etc/pki/tls/certs/my.app.key' 65 | }); 66 | 67 | var myObj = secureConfig.getProp('my-json-prop'); 68 | ``` 69 | Access your secure config from anywhere in your app: 70 | ``` 71 | var secureConfig = require('config-shield'); 72 | var myObj = secureConfig.getProp('my-json-prop'); 73 | ``` 74 | Multiple configs? No problem: 75 | ``` 76 | var secureConfig = require('config-shield'); 77 | secureConfig.load({ 78 | instance: 'my-other-config', 79 | configPath: './my-other-secure-config.json', 80 | privateKeyPath: '/etc/pki/tls/certs/my.app.key' 81 | }); 82 | 83 | var myOtherSecureConfig = secureConfig.instance('my-other-config'); 84 | var myObj = myOtherSecureConfig.getProp('my-prop'); 85 | ``` 86 | 87 | 88 | ## Developer Environment 89 | 90 | Optionally you may include your development private key within your project to keep 91 | things simple, but ***please do not do this*** for production environments as 92 | you'll be negating the value of this module. Only a limited few should have access 93 | to production private keys. 94 | 95 | 96 | 97 | ## API 98 | ``` 99 | var secureConfig = require('config-shield'); 100 | ``` 101 | * load (options[, cb]) - Load config. 102 | * options.instance (default: 'default') - Name of the config instance. 103 | * options.configPath (required) - Config to load, relative to the current working directory. 104 | * options.privateKeyPath (required) - Private key to load. Or could be any secret. 105 | * options.noCache (default: false) - Will disable caching of decrypted values if true. 106 | * options.alg (default: 'aes-256-ctr') - Algorithm to use for encryption. 107 | * cb (function(err, secureConfig)) - If callback is provided, will load asynchronously, 108 | otherwise will return synchronously. 109 | * save ([configPath][, cb]) - Save config. 110 | * configPath (required) - Config to save, relative to the current working directory. 111 | * cb (function(err)) - If callback is provided, will save asynchronously, 112 | otherwise will return synchronously. 113 | * convert ([options][, cb]) - Convert existing config to new private key. 114 | * options.privateKeyPath (required) - Private key file to load. Or could be any secret file. 115 | * options.backup (default: `false`) - Write old config values as `backup` to allow for a rotationary period where 116 | old key will continue to work. 117 | * options.alg (default: 'aes-256-ctr') - Algorithm to use for encryption. 118 | * cb (function(err)) - If callback is provided, will save asynchronously, 119 | otherwise will return synchronously. 120 | * dropBackup () - Removes all backup keys. 121 | * getProp (propName) - Return decrypted config value. 122 | * setProp (propName, propValue) - Store config value. 123 | * removeProp (propName) - Remove config value. 124 | * removeAll () - Remove all config values. 125 | * getKeys () - Return an array of available property keys. 126 | * getInstance (instanceName) - Return a config instance. 127 | * setInstance (instance) - Set a config instance. 128 | 129 | 130 | 131 | ## Rotating keys 132 | 133 | In the case you have keys that must be rotated, you can use the convert with `backup` option. The process would 134 | require you to: 135 | 136 | 1. Load config with old private key. 137 | 2. Convert with new private key, setting `backup` to `true`. 138 | 3. Deploy your config change. 139 | 4. Rotate your private keys. 140 | 5. Load config with new private key. 141 | 6. Run `dropBackup`. 142 | 7. Deploy your final config change. 143 | 144 | In CLI, would look something like: 145 | 146 | ``` 147 | config-shield 148 | enter path of config> secure-config.json 149 | enter path of private key> old.key 150 | > convert 151 | enter path of private key> new.key 152 | backup old values to enable key rotations? (enter to disable, or `true`)> true 153 | > save 154 | > exit 155 | ``` 156 | 157 | Deploy your change, then update your config one last time: 158 | 159 | ``` 160 | config-shield 161 | enter path of config> secure-config.json 162 | enter path of private key> new.key 163 | > dropBackup 164 | > save 165 | > exit 166 | ``` 167 | 168 | Deploy the final config. If you skip the step of dropping the backup, your config will 169 | become vulnerable to attacks using the old private key, negating most of the value of 170 | rotating keys. 171 | 172 | 173 | ## Future 174 | 175 | Possible future enhancements: 176 | 177 | * tts - Time to stale before auto-reloading config. 178 | 179 | 180 | ## License 181 | 182 | [MIT](https://github.com/godaddy/node-config-shield/blob/master/LICENSE.txt) 183 | -------------------------------------------------------------------------------- /bin/config-shield: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../scripts/cli'); 4 | -------------------------------------------------------------------------------- /certs/demo.key: -------------------------------------------------------------------------------- 1 | some super (OLD) key that few have access to.. could even be a cert's private key. 2 | -------------------------------------------------------------------------------- /certs/new.key: -------------------------------------------------------------------------------- 1 | some super (NEW) key that few have access to.. could even be a cert's private key. 2 | -------------------------------------------------------------------------------- /lib/convert.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | 6 | module.exports = convert; 7 | 8 | function convert(options, cb) { 9 | var instance = this; 10 | 11 | options = options || {}; 12 | options.alg = options.alg || 'aes-256-ctr'; 13 | 14 | var convertAllProperties = function (oldPwd, newPwd, backupOldValue) { 15 | var keys = instance.getKeys(); 16 | keys.forEach(function (k) { 17 | var v = instance.getProp(k, oldPwd); 18 | instance.setProp(k, v, options.alg, newPwd, backupOldValue); 19 | }); 20 | }; 21 | 22 | var privateKey = options.privateKeyPath || instance.config._.privateKeyPath; 23 | var fullPrivateKeyPath; 24 | 25 | if (typeof cb === 'function') { 26 | if (typeof privateKey !== 'string') { 27 | return void cb(new Error('options.privateKeyPath required')); 28 | } 29 | 30 | fullPrivateKeyPath = path.resolve(privateKey); 31 | 32 | fs.readFile(fullPrivateKeyPath, { encoding: 'utf8' }, function (err, newPwd) { 33 | if (err) { 34 | return void cb(err); 35 | } 36 | 37 | convertAllProperties(instance.pwd, newPwd); 38 | 39 | instance.config._.privateKeyPath = fullPrivateKeyPath; 40 | instance.pwd = newPwd; 41 | 42 | return void cb(null, instance); 43 | }); 44 | } else { 45 | if (typeof privateKey !== 'string') { 46 | throw new Error('options.privateKeyPath required'); 47 | } 48 | 49 | fullPrivateKeyPath = path.resolve(privateKey); 50 | 51 | var newPwd = fs.readFileSync(fullPrivateKeyPath, { encoding: 'utf8' }); 52 | 53 | convertAllProperties(instance.pwd, newPwd, options.backup === true); 54 | 55 | instance.config._.privateKeyPath = fullPrivateKeyPath; 56 | instance.pwd = newPwd; 57 | } 58 | 59 | return instance; 60 | } 61 | -------------------------------------------------------------------------------- /lib/drop-backup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = dropBackup; 4 | 5 | function dropBackup() { 6 | var props = this.config.properties; 7 | Object.keys(props).forEach(function (key) { 8 | var item = props[key]; 9 | if (item && item.backup) { 10 | delete item.backup; 11 | } 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /lib/get-keys.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = getKeys; 4 | 5 | function getKeys() { 6 | return Object.keys(this.config.properties); 7 | } 8 | -------------------------------------------------------------------------------- /lib/get-prop.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var crypto = require('crypto'); 4 | 5 | module.exports = getProp; 6 | 7 | function getValueFromItem(item, val, pwd) { 8 | var cipher = crypto.createCipher(item.alg, pwd); 9 | 10 | var dec = cipher.update(val, 'hex', 'utf8'); 11 | dec += cipher.final('utf8'); 12 | 13 | // parse it 14 | return JSON.parse(dec); 15 | } 16 | 17 | function getProp(key, pwd) { 18 | if (key in this.cache) { 19 | return this.cache[key]; // already decrypted 20 | } 21 | 22 | if (!(key in this.config.properties)) { 23 | return undefined; // prop does not exist 24 | } 25 | 26 | var item = this.config.properties[key]; 27 | 28 | var dec; // parse it 29 | try { 30 | dec = getValueFromItem(item, item.value, pwd || this.pwd); 31 | } catch (ex) { 32 | if (!item.backup) throw ex; 33 | 34 | dec = getValueFromItem(item, item.backup, pwd || this.pwd); 35 | } 36 | 37 | if (this.options.noCache !== true) { 38 | // cache result 39 | this.cache[key] = dec.value; 40 | } 41 | 42 | return dec.value; 43 | } 44 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SecureConfig = require('./secure-config'); 4 | 5 | // default instance 6 | var secureConfig = new SecureConfig(); 7 | 8 | // always expose default instance 9 | module.exports = secureConfig; 10 | -------------------------------------------------------------------------------- /lib/load-from-file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SecureConfig = require('./secure-config'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | 7 | module.exports = loadFromFile; 8 | 9 | function loadFromFile(options, cb) { 10 | options = options || {}; 11 | options.instance = options.instance || this.options.instance; 12 | options.configPath = options.configPath || './secure-config.json'; 13 | 14 | // get instance 15 | var instance = this.getInstance(options.instance); 16 | 17 | // if requesting a different instance, create it 18 | if (!instance) { 19 | instance = new SecureConfig(options); 20 | this.setInstance(instance); 21 | } 22 | 23 | var fullConfigPath = path.resolve(options.configPath); 24 | 25 | if (typeof cb === 'function') { 26 | fs.readFile(fullConfigPath, { encoding: 'utf8' }, function (err, data) { 27 | if (err) { 28 | return void cb(err); 29 | } 30 | 31 | var config = JSON.parse(data); 32 | config._ = config._ || {}; 33 | config.properties = config.properties || {}; 34 | 35 | // use explicit option 36 | // or path in config 37 | // otherwise default to demo cert 38 | var fullPrivateKeyPath = path.resolve( 39 | options.privateKeyPath || 40 | config._.privateKeyPath || 41 | __dirname + '/../certs/demo.key' 42 | ); 43 | 44 | fs.readFile(fullPrivateKeyPath, { encoding: 'utf8' }, function (err, pwd) { 45 | if (err) { 46 | return void cb(err); 47 | } 48 | 49 | config._.privateKeyPath = fullPrivateKeyPath; 50 | instance.configPath = fullConfigPath; 51 | instance.config = config; 52 | instance.pwd = pwd; 53 | 54 | return void cb(null, config); 55 | }); 56 | }); 57 | } else { 58 | var data = fs.readFileSync(fullConfigPath, { encoding: 'utf8' }); 59 | var config = JSON.parse(data); 60 | config._ = config._ || {}; 61 | config.properties = config.properties || {}; 62 | 63 | // use explicit option 64 | // or path in config 65 | // otherwise default to demo cert 66 | var fullPrivateKeyPath = path.resolve( 67 | options.privateKeyPath || 68 | config._.privateKeyPath || 69 | __dirname + '/../certs/demo.key' 70 | ); 71 | 72 | var pwd = fs.readFileSync(fullPrivateKeyPath, { encoding: 'utf8' }); 73 | 74 | config._.privateKeyPath = fullPrivateKeyPath; 75 | instance.configPath = fullConfigPath; 76 | instance.config = config; 77 | instance.pwd = pwd; 78 | } 79 | 80 | return instance; 81 | } 82 | -------------------------------------------------------------------------------- /lib/remove-all.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = removeAll; 4 | 5 | function removeAll() { 6 | // reset 7 | this.config.properties = {}; 8 | this.cache = {}; 9 | } 10 | -------------------------------------------------------------------------------- /lib/remove-prop.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = removeProp; 4 | 5 | function removeProp(name) { 6 | // remove prop if exists 7 | delete this.config.properties[name]; 8 | // remove from cache as well 9 | delete this.cache[name]; 10 | } 11 | -------------------------------------------------------------------------------- /lib/save-to-file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | 5 | module.exports = saveToFile; 6 | 7 | function saveToFile(configFile, cb) { 8 | // prettify it so it's a bit easier to read 9 | var json = JSON.stringify(this.config, null, 2); 10 | 11 | if (typeof cb === 'function') { 12 | return fs.writeFile(configFile, json, { encoding: 'utf8' }, cb); 13 | } else { 14 | return fs.writeFileSync(configFile, json, { encoding: 'utf8' }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/secure-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = SecureConfig; 4 | 5 | // init with default 6 | var configInstances = { }; 7 | 8 | function SecureConfig(options) { 9 | if (!(this instanceof SecureConfig)) { 10 | return new SecureConfig(options); 11 | } 12 | 13 | this.options = options || {}; 14 | this.options.instance = this.options.instance || 'default'; 15 | this.config = { _: {}, properties: {} }; 16 | this.cache = {}; 17 | 18 | // track instance 19 | configInstances[this.options.instance] = this; 20 | } 21 | 22 | var p = SecureConfig.prototype; 23 | 24 | p.getProp = require('./get-prop'); 25 | 26 | p.setProp = require('./set-prop'); 27 | 28 | p.removeProp = require('./remove-prop'); 29 | 30 | p.removeAll = require('./remove-all'); 31 | 32 | p.getKeys = require('./get-keys'); 33 | 34 | p.load = require('./load-from-file'); 35 | 36 | p.save = require('./save-to-file'); 37 | 38 | p.convert = require('./convert'); 39 | 40 | p.dropBackup = require('./drop-backup'); 41 | 42 | p.getInstance = function (instanceName) { 43 | return configInstances[instanceName]; 44 | }; 45 | 46 | p.setInstance = function (instance) { 47 | configInstances[instance.options.instance] = instance; 48 | 49 | return instance; 50 | }; 51 | -------------------------------------------------------------------------------- /lib/set-prop.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var crypto = require('crypto'); 4 | 5 | module.exports = setProp; 6 | 7 | function setProp(key, val, alg, pwd, backupOldValue) { 8 | // create or update 9 | var item = this.config.properties[key] || { }; 10 | 11 | // if provided, override 12 | // otherwise use instance option 13 | // default if not specified 14 | item.alg = alg || this.options.alg || 'aes-256-ctr'; 15 | 16 | if (this.options.noCache !== true) { 17 | // update cache 18 | this.cache[key] = val; 19 | } 20 | 21 | var cipher = crypto.createCipher(item.alg, pwd || this.pwd); 22 | 23 | // serialize as JSON to retain type 24 | var json = JSON.stringify({ value: val }); 25 | 26 | var enc = cipher.update(json, 'utf8', 'hex'); 27 | enc += cipher.final('hex'); 28 | 29 | if (backupOldValue === true && 'value' in item) { 30 | // backup old value 31 | item.backup = item.value; 32 | } else { 33 | delete item.backup; // ensure old backup, if any, is removed 34 | } 35 | item.value = enc; 36 | 37 | // insert if doesn't exist 38 | this.config.properties[key] = item; 39 | 40 | return item; 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "config-shield", 3 | "version": "0.2.3", 4 | "description": "Store and retrieve data sensative in nature", 5 | "main": "./lib/index.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "start": "npm run cli", 9 | "cli": "node ./scripts/cli.js" 10 | }, 11 | "bin": { 12 | "config-shield": "./bin/config-shield", 13 | "cshield": "./bin/config-shield" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git@github.com:godaddy/node-config-shield.git" 18 | }, 19 | "author": { 20 | "name": "Aaron Silvas" 21 | }, 22 | "devDependencies": { 23 | "mocha": "^7.2.0" 24 | }, 25 | "readmeFilename": "README.md", 26 | "gitHead": "ff363e0f7ed1645cc3c6c8c34f69c0eda156a3bf", 27 | "maintainers": [ 28 | { 29 | "name": "asilvas", 30 | "email": "asilvas@godaddy.com" 31 | } 32 | ], 33 | "dependencies": { 34 | "json5": "^2.1.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /scripts/cli.js: -------------------------------------------------------------------------------- 1 | var readline = require('readline'); 2 | var JSON5 = require('json5'); 3 | 4 | var rl = readline.createInterface({ 5 | input: process.stdin, 6 | output: process.stdout 7 | }); 8 | 9 | module.exports = cliMain; 10 | 11 | var instance = require('../lib/index.js'); 12 | 13 | var commands = { 14 | 'get': cmdGet, 15 | 'set': cmdSet, 16 | 'remove': cmdRemove, 17 | 'keys': cmdKeys, 18 | 'save': cmdSave, 19 | 'load': cmdLoad, 20 | 'convert': cmdConvert, 21 | 'dropBackup': cmdDropBackup, 22 | 'help': cmdHelp, 23 | 'exit': cmdExit 24 | }; 25 | 26 | function cliMain() { 27 | rl.resume(); 28 | rl.write(': config-shield v' + require('../package.json').version + ' ready\r\n'); 29 | cmdLoad(process.argv[2], process.argv[3]); 30 | } 31 | 32 | cliMain(); 33 | 34 | function cmdHelp() { 35 | rl.write(': Available commands:\r\n'); 36 | Object.keys(commands).forEach(function(cmdName) { 37 | rl.write(': * ' + cmdName + '\r\n'); 38 | }); 39 | enterCommand(); 40 | } 41 | 42 | function enterCommand() { 43 | rl.write('\r\n'); 44 | rl.question('> ', onCommand); 45 | } 46 | 47 | function onCommand(lineInput) { 48 | lineInput = lineInput.replace(/[\r\n]/g, ""); 49 | var parts = lineInput.split(' '); 50 | 51 | var cmd = commands[parts[0]]; 52 | if (!cmd) { 53 | rl.write(': Command "' + parts[0] + '" not found.\r\n'); 54 | cmdHelp(); 55 | return; 56 | } 57 | 58 | var partOne = parts[1] || null; 59 | var partTwo = parts.splice(2).join(' '); 60 | 61 | cmd.call(null, partOne, partTwo); 62 | } 63 | 64 | function cmdGet(key) { 65 | var val = instance.getProp(key); 66 | if (!val) { 67 | rl.write(': "' + key + '" not found'); 68 | } else { 69 | rl.write(': ' + JSON.stringify(val, null, 2)); 70 | } 71 | 72 | enterCommand(); 73 | } 74 | 75 | function cmdSet(key, val) { 76 | var objVal = val; 77 | 78 | try { 79 | var strTest = /^[\'\"](.*?)[\'\"]$/.exec(val); 80 | if (!strTest || strTest.length !== 2) { // do not parse if explicitly a string 81 | objVal = JSON5.parse(val); // attempt to parse 82 | } else { 83 | objVal = strTest[1]; 84 | } 85 | } catch(ex) { 86 | // use as existing string 87 | } 88 | 89 | instance.setProp(key, objVal); 90 | rl.write(': stored as type ' + typeof objVal); 91 | 92 | enterCommand(); 93 | } 94 | 95 | function cmdRemove(key) { 96 | instance.removeProp(key); 97 | 98 | enterCommand(); 99 | } 100 | 101 | function cmdKeys() { 102 | rl.write(': Keys: ' + instance.getKeys()); 103 | 104 | enterCommand(); 105 | } 106 | 107 | function getConfigPath(configPath, cb) { 108 | if (configPath) { 109 | return void cb(configPath); 110 | } 111 | rl.question('enter path of config (enter to use existing path)> ', function(configPath) { 112 | if (configPath.length === 0) { 113 | configPath = instance.configPath; 114 | } 115 | cb(configPath); 116 | }); 117 | } 118 | 119 | function getPrivateKeyPath(privateKeyPath, cb) { 120 | if (privateKeyPath) { 121 | return void cb(privateKeyPath); 122 | } 123 | rl.question('enter path of private key (press enter to use private key path in config)> ', function(privateKeyPath) { 124 | if (privateKeyPath.length === 0) { 125 | privateKeyPath = null; 126 | } 127 | cb(privateKeyPath); 128 | }); 129 | } 130 | 131 | function getBackup(backup, cb) { 132 | if (typeof backup === 'string' && backup.length > 0) { 133 | return void cb(backup); 134 | } 135 | rl.question('backup old values to enable key rotations? (enter to disable, or `true`)> ', function(backup) { 136 | if (backup.length === 0) { 137 | backup = 'false'; 138 | } 139 | cb(backup); 140 | }); 141 | } 142 | 143 | function cmdSave(configPath) { 144 | getConfigPath(null, function (configPath) { 145 | rl.write(': saving... '); 146 | try { 147 | instance.save(configPath); 148 | rl.write('done'); 149 | } catch (ex) { 150 | rl.write('FAILED: ' + ex.toString()); 151 | } 152 | 153 | enterCommand(); 154 | }); 155 | 156 | } 157 | 158 | function cmdLoad(configPath, privateKeyPath) { 159 | getConfigPath(configPath, function (configPath) { 160 | getPrivateKeyPath(privateKeyPath, function (privateKeyPath) { 161 | rl.write(': loading...'); 162 | try { 163 | instance.load({ configPath: configPath, privateKeyPath: privateKeyPath }); 164 | rl.write('done'); 165 | } catch (ex) { 166 | rl.write('FAILED: ' + ex.toString()); 167 | } 168 | 169 | enterCommand(); 170 | }); 171 | }); 172 | } 173 | 174 | function cmdConvert(privateKeyPath, backup) { 175 | getPrivateKeyPath(privateKeyPath, function (privateKeyPath) { 176 | getBackup(backup, function (backup) { 177 | rl.write(': converting...'); 178 | instance.convert({ privateKeyPath: privateKeyPath, backup: backup === 'true' }); 179 | rl.write('done. Type `save` to persist to disk'); 180 | 181 | enterCommand(); 182 | }); 183 | }); 184 | } 185 | 186 | function cmdDropBackup() { 187 | instance.dropBackup(); 188 | 189 | rl.write('backups dropped. Type `save` to persist to disk'); 190 | 191 | enterCommand(); 192 | } 193 | 194 | function cmdExit() { 195 | // todo: prompt if change not saved 196 | 197 | rl.write(': Exiting...'); 198 | rl.close(); 199 | process.exit(0); 200 | } 201 | -------------------------------------------------------------------------------- /secure-config.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/convert.tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | --------------------------------------------------------------------------------