From 0dba5f9befb5845cf912b140b58dd8541659c0e2 Mon Sep 17 00:00:00 2001 From: Tasos Latsas Date: Fri, 6 Dec 2013 17:23:43 +0200 Subject: [PATCH 01/19] clarify distinction between the CLI and the web interface --- README.markdown | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index fff6aee..ed22e85 100644 --- a/README.markdown +++ b/README.markdown @@ -26,7 +26,8 @@ strong. ## Installation -This program is written in JavaScript and is available as a Node program: +This program is written in JavaScript. It provides a CLI and a web-based interface. +The command line interface is available as a Node program. To install with npm run: npm install -g vault @@ -34,6 +35,9 @@ To enable tab-completion for bash, add this to your .bashrc scripts: which vault > /dev/null && . "$( vault --initpath )" +If you want to use the web interface provided with vault (like https://getvau.lt/) you need +to serve the static files found in the `web` folder using your favourite web server. + ## Usage From 957900ce35b22897cf22c1eda7ea51331a265eed Mon Sep 17 00:00:00 2001 From: James Coglan Date: Fri, 6 Dec 2013 16:08:35 +0000 Subject: [PATCH 02/19] Migrate to jstest. --- package.json | 2 +- spec/node.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 2e21e99..b09d0ab 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ , "ssh-agent": "~0.2.1" , "vault-cipher": "~0.3.0" } -, "devDependencies" : { "jsclass": "" } +, "devDependencies" : { "jstest": "" } , "scripts" : { "postinstall" : "./node/scripts/postinstall" , "test" : "node spec/node.js" diff --git a/spec/node.js b/spec/node.js index 0c26137..8962047 100644 --- a/spec/node.js +++ b/spec/node.js @@ -1,10 +1,10 @@ -require('jsclass') -JS.require('JS.Test') +JS = require('jstest') JS.ENV.Vault = require('../lib/vault') require('./vault_spec') require('./stream_spec') require('./node/cli_spec') + JS.Test.autorun() From ff2ade16eb76236729e7e39d905c800d28471efb Mon Sep 17 00:00:00 2001 From: James Coglan Date: Wed, 11 Dec 2013 11:02:51 +0000 Subject: [PATCH 03/19] Fix encoding problem when diacritics are used in passwords on the command line. --- bin/vault | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/vault b/bin/vault index c9652e2..c612270 100755 --- a/bin/vault +++ b/bin/vault @@ -39,7 +39,10 @@ var cli = new CLI({ password: function(callback) { process.stderr.write('Passphrase: '); - pw('*', process.stdin, process.stderr, callback); + pw('*', process.stdin, process.stderr, function(password) { + password = new Buffer(password, 'binary').toString('utf8'); + callback(password); + }); }, selectKey: function(callback) { From 99eb4ae0680a128f43eea03dc92afe0baf537e86 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Wed, 11 Dec 2013 19:28:51 +0000 Subject: [PATCH 04/19] Update .travis.yml with Node versions. --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 84fd7ca..4b66f32 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ language: node_js + node_js: - 0.6 - 0.8 - - 0.9 + - 0.10 + - 0.11 From 880f8326dd3c24a4aaf10f9c49e3e5effc4c002c Mon Sep 17 00:00:00 2001 From: James Coglan Date: Thu, 12 Dec 2013 19:14:52 +0000 Subject: [PATCH 05/19] Migrate some changes from the remote-storage branch to the master branch, mostly support for the --notes option. --- README.markdown | 94 ++++++++++++++++---------- bin/vault | 7 +- node/cli.js | 151 +++++++++++++++++++++++++++++------------- node/editor.js | 57 ++++++++++++++++ node/local_store.js | 105 +++++++++++++++++++++-------- node/usage.txt | 27 ++++++++ package.json | 21 +++--- spec/node/cli_spec.js | 144 +++++++++++++++++++++++----------------- 8 files changed, 426 insertions(+), 180 deletions(-) create mode 100644 node/editor.js create mode 100644 node/usage.txt diff --git a/README.markdown b/README.markdown index ed22e85..5cb601a 100644 --- a/README.markdown +++ b/README.markdown @@ -1,33 +1,35 @@ # vault [](http://travis-ci.org/jcoglan/vault) -Simple password generator. Given a passphrase and the name of a service, returns -a strong password for that service. You only need to remember your passphrase, -which you do not give to anyone, and this program will give a different password -for every service you use. The passphrase can be any text you like. +`vault` is a simple password manager. Given a passphrase and the name of a +service, it returns a strong password for that service. You only need to +remember your passphrase, which you do not give to anyone, and `vault` will give +a different password for every service you use. The passphrase can be any text +you like. Given the same passphrase and service name, the program will generate the same -result every time, so you can use it to 'look up' those impossible-to-remember +result every time, so you can use it to look up those impossible-to-remember passwords when you need them. -According to [Dropbox's zxcvbn password strength measure](http://dl.dropbox.com/u/209/zxcvbn/test/index.html), -if your dictionary English password takes about a second to crack, those -generated by `vault` take over a million times the age of the observable -universe to crack by brute force. +According to [Dropbox's zxcvbn password strength +measure](http://dl.dropbox.com/u/209/zxcvbn/test/index.html), if your dictionary +English password takes about a second to crack, those generated by `vault` take +over a million times the age of the observable universe to crack by brute force. ## Why? -I have a terrible memory and like keeping my stuff safe. [Strong service-specific -passwords are hard to remember](http://xkcd.com/936/), and many services [have -stupid restrictions on passwords](http://me.veekun.com/blog/2011/12/04/fuck-passwords/). -I want to remember one phrase and have a machine deal with making my passwords -strong. +I have a terrible memory and like keeping my stuff safe. [Strong +service-specific passwords are hard to remember](http://xkcd.com/936/), and many +services [have stupid restrictions on +passwords](http://me.veekun.com/blog/2011/12/04/fuck-passwords/). I want to +remember one phrase and have a machine deal with making my passwords strong. ## Installation -This program is written in JavaScript. It provides a CLI and a web-based interface. -The command line interface is available as a Node program. To install with npm run: +This program is written in JavaScript. It provides a CLI and a web-based +interface. The command line interface is available as a Node program. To +install with npm run: npm install -g vault @@ -35,20 +37,21 @@ To enable tab-completion for bash, add this to your .bashrc scripts: which vault > /dev/null && . "$( vault --initpath )" -If you want to use the web interface provided with vault (like https://getvau.lt/) you need -to serve the static files found in the `web` folder using your favourite web server. +If you want to use the web interface provided with vault (like +https://getvau.lt/) you need to serve the static files found in the `web` folder +using your favourite web server. ## Usage The most basic usage involves passing your passphrase and the service name; when -you pass the `-p` flag you will be prompted for your passphrase: +you pass the `--phrase` or `-p` flag you will be prompted for your passphrase: $ vault google -p Passphrase: ********* 2hk!W[L,2rWWI=~=l>,E -You can set the desired length using `-l`: +You can set the desired length using `--length` or `-l`: $ vault google -p -l 6 Passphrase: ********* @@ -80,16 +83,16 @@ Available character classes include: Finally, some sites do not allow passwords containing strings of repeated characters beyond a certain length. For example, a site requiring passwords not to contain more than two of the same character in a row would reject the -password `ZOMG!!!` because of the 3 `!` characters. Vault lets you express this -requirement using `-r` or `--repeat`; this option sets the maximum number of -times the same character can appear in a row. +password `ZOMG!!!` because of the 3 `!` characters. `vault` lets you express +this requirement using `--repeat` or `-r`; this option sets the maximum number +of times the same character can appear in a row. $ vault google -p -r 2 -## Using your private key +## Using your SSH private key -Instead of a simple passphrase, `vault` can use a value signed using your +Instead of a simple passphrase, `vault` can use a value signed using your SSH private key as its input. Use the `--key` or `-k` option: $ vault twitter -k @@ -106,9 +109,9 @@ If you only have one private key, that is used automatically. If you have several, a menu is displayed as above using snippets from the corresponding public keys. You will be prompted to unlock the selected key if necessary. -Note that all the prompts shown to you while using `vault` are printed to -`stderr` and the generated password to `stdout`, so you can pipe `vault` to -`pbcopy` and you'll just get the password in your clipboard, i.e.: +Note that all the prompts shown to you while using `vault` are printed to stderr +and the generated password to stdout, so you can pipe `vault` to `pbcopy` and +you'll just get the password in your clipboard, i.e.: $ vault twitter -k | pbcopy @@ -181,14 +184,35 @@ file. This can be used, for example, to change the encryption key: $ VAULT_KEY=oldkey vault --export settings.json $ VAULT_KEY=newkey valut --import settings.json -Or, you can use it if Vault changes its encryption algorithm in the future. Just -use your current installation to export the settings, upgrade, then import. +Or, you can use it if `vault` changes its encryption algorithm in the future. +Just use your current installation to export the settings, upgrade, then import. $ vault --export settings.json $ npm install -g vault $ vault --import settings.json +## Notes + +You can save notes for any of the services you use. Notes are stored in the +service's settings, but are not used for generating passwords. To edit the notes +for a service, use `--config` with `--notes` or `-n`: + + $ vault -c -n google + +This opens your `$EDITOR` where you can edit the notes. When you save the file +and close the editor, the updated notes will be saved into your `.vault` file. + +When you ask for the password for a service, `vault` will print any notes you +have saved for it. It prints the password to stdout and the notes to stderr, so +you can pipe the password to the clipboard if you like and still the notes +printed in your terminal. + + $ vault google | pbcopy + + The notes will appear here. The password is saved to the clipboard. + + ## Deleting saved settings You can delete any saved setting using the `--delete`, `--delete-globals` and @@ -216,8 +240,8 @@ constraints. This design means that each password is very hard to break by brute force, and ensures that the discovery of one service's password does not lead to other accounts being compromised. It also means you can tailor the output to the character set accepted by each service. The use of a deterministic hash function -means we don't need to store your passwords since they can easily be regenerated; -this means there's no storage to sync or keep secure. +means we don't need to store your passwords since they can easily be +regenerated; this means there's no storage to sync or keep secure. ## License @@ -228,9 +252,9 @@ Copyright (c) 2011-2013 James Coglan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in -the Software without restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the -Software, and to permit persons to whom the Software is furnished to do so, +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all diff --git a/bin/vault b/bin/vault index c612270..81d168f 100755 --- a/bin/vault +++ b/bin/vault @@ -28,11 +28,14 @@ var readline = (parseInt(version[2], 10) <= 6) var cli = new CLI({ config: {path: path, key: key}, - output: process.stdout, + stdout: process.stdout, + stderr: process.stderr, tty: tty.isatty(1), confirm: function(message, callback) { - readline().question(message + ' (Y/n): ', function(input) { + var rl = readline(); + rl.question(message + ' (Y/n): ', function(input) { + rl.close(); callback(input === 'Y'); }); }, diff --git a/node/cli.js b/node/cli.js index b3da182..ad197c7 100644 --- a/node/cli.js +++ b/node/cli.js @@ -1,15 +1,11 @@ var fs = require('fs'), path = require('path'), OptParser = require('./optparser'), + editor = require('./editor'), Vault = require('../lib/vault'), LocalStore = require('./local_store'), - OPTIONS = { 'config': Boolean, - 'delete': String, - 'delete-globals': Boolean, - 'clear': Boolean, - - 'phrase': Boolean, + OPTIONS = { 'phrase': Boolean, 'key': Boolean, 'length': Number, 'repeat': Number, @@ -20,29 +16,41 @@ var fs = require('fs'), 'space': Number, 'dash': Number, 'symbol': Number, + 'notes': Boolean, + + 'config': Boolean, + 'delete': String, + 'delete-globals': Boolean, + 'clear': Boolean, 'export': String, 'import': String, 'initpath': Boolean, - 'cmplt': String + 'cmplt': String, + 'help': Boolean }, SHORTS = { 'c': '--config', - 'x': '--delete', - 'X': '--clear', - 'p': '--phrase', + 'e': '--export', + 'h': '--help', + 'i': '--import', 'k': '--key', 'l': '--length', + 'n': '--notes', + 'p': '--phrase', 'r': '--repeat', - 'e': '--export', - 'i': '--import' + 'x': '--delete', + 'X': '--clear' }; +var exists = fs.existsSync || path.existsSync; + var CLI = function(options) { this._parser = new OptParser(OPTIONS, SHORTS, ['service']); this._store = new LocalStore(options.config); - this._out = options.output; + this._out = options.stdout; + this._err = options.stderr; this._tty = options.tty; this._requestPassword = options.password; @@ -55,27 +63,36 @@ CLI.prototype.run = function(argv, callback, context) { this._parser.parse(argv, function(error, params) { if (error) return callback.call(context, error); - var service = params.service; + var service = params.service, self = this; delete params.service; + if (params.help) + return fs.readFile(__dirname + '/usage.txt', function(error, content) { + self._out.write(content); + callback.call(context, null); + }); + if (params.initpath) { this._out.write(path.resolve(__dirname + '/scripts/init')); - return callback.call(context); + return callback.call(context, null); } if (params.cmplt !== undefined) return this.complete(params.cmplt, callback, context); - this.withPhrase(params, function() { - if (params['delete-globals']) this.deleteGlobals(callback, context); - else if (params.delete) this.delete(params.delete, callback, context); - else if (params.clear) this.deleteAll(callback, context); - - else if (params.export) this.export(params.export, callback, context); - else if (params.import) this.import(params.import, callback, context); + if (params['delete-globals']) return this.deleteGlobals(callback, context); + if (params.delete) return this.delete(params.delete, callback, context); + if (params.clear) return this.deleteAll(callback, context); + if (params.export) return this.export(params.export, callback, context); + if (params.import) return this.import(params.import, callback, context); - else if (params.config) this.configure(service, params, callback, context); - else this.generate(service, params, callback, context); + this.withPhrase(params, function() { + if (params.config) { + delete params.config; + this.configure(service, params, callback, context); + } else { + this.generate(service, params, callback, context); + } }); }, this); }; @@ -86,20 +103,42 @@ CLI.prototype.complete = function(word, callback, context) { var names = Object.keys(OPTIONS).map(function(o) { return '--' + o }); names = names.filter(function(n) { return n.indexOf(word) === 0 }); this._out.write(names.sort().join('\n')); - callback.call(context); + callback.call(context, null); } else { this._store.listServices(function(error, services) { if (error) return callback.call(context, new Error('\n' + error.message)); - services = services.filter(function(s) { return s.indexOf(word) === 0 }); - this._out.write(services.sort().join('\n')); + var all = services.filter(function(s) { return s.indexOf(word) === 0 }); + this._out.write(all.sort().join('\n')); callback.call(context, error); }, this); } }; +CLI.prototype.withNotes = function(service, params, callback) { + if (!params.notes) return callback.call(this); + + if (!service) + return callback.call(this, new Error('No service name given')); + + this._store.currentStore(function(error, store) { + if (error) return callback.call(this, error); + + store.serviceSettings(service, false, function(error, settings) { + var notes = (settings || {}).notes || + '# Notes for "' + service + '" in "' + store.getName() + '"\n' + + '# Save this file and quit your editor to save your notes\n'; + + editor.editTempfile(notes, function(error, text) { + if (error) return callback.call(this, error); + params.notes = /^\s*$/.test(text) ? undefined : text; + callback.call(this); + }, this); + }, this); + }, this); +}; + CLI.prototype.withPhrase = function(params, callback) { - var self = this, - message = params.config ? null : Vault.UUID; + var self = this; params.input = {key: !!params.key, phrase: !!params.phrase}; @@ -119,10 +158,14 @@ CLI.prototype.withPhrase = function(params, callback) { }; CLI.prototype.export = function(path, callback, context) { - this._store.export(function(error, json) { + var self = this; + this._store.export(function(error, config) { + var store = 'local'; if (error) return callback.call(context, error); - json = json || JSON.stringify({global: {}, services: {}}, true, 2); + config = config || {global: {}, services: {}}; + var json = JSON.stringify(config, true, 2); fs.writeFile(path, json, function() { + self._out.write('Exported settings from "' + store + '" to ' + path + '\n'); callback.apply(context, arguments); }); }); @@ -132,22 +175,37 @@ CLI.prototype.import = function(path, callback, context) { var self = this; fs.readFile(path, function(error, content) { if (error) return callback.call(context, error); - self._store.import(content.toString(), callback, context); + var config = JSON.parse(content.toString()); + self._store.import(config, function(error, store) { + if (error) return callback.call(context, error); + self._out.write('Imported settings from ' + path + ' to "' + store + '"\n'); + callback.call(context, null); + }); }); }; CLI.prototype.configure = function(service, params, callback, context) { - var settings = {}; + this.withNotes(service, params, function(error) { + if (error) return callback.call(context, error); - for (var key in params) { - if (key !== 'config' && typeof params[key] !== 'object') - settings[key] = params[key]; - } + var settings = {}; + for (var key in params) { + if (typeof params[key] !== 'object') settings[key] = params[key]; + } - if (service) - this._store.saveService(service, settings, callback, context); - else - this._store.saveGlobals(settings, callback, context); + if (service) + this._store.saveService(service, settings, function(error, store) { + if (error) return callback.call(context, error); + this._out.write('Settings for service "' + service + '" saved to "' + store + '"\n'); + callback.call(context, null); + }, this); + else + this._store.saveGlobals(settings, function(error, store) { + if (error) return callback.call(context, error); + this._out.write('Global settings saved to "' + store + '"\n'); + callback.call(context, null); + }, this); + }); }; CLI.prototype.deleteGlobals = function(callback, context) { @@ -156,7 +214,7 @@ CLI.prototype.deleteGlobals = function(callback, context) { if (confirm) store.deleteGlobals(callback, context); else - callback.call(context); + callback.call(context, null); }); }; @@ -167,7 +225,7 @@ CLI.prototype.delete = function(service, callback, context) { if (confirm) store.deleteService(service, callback, context); else - callback.call(context); + callback.call(context, null); }); }; @@ -177,12 +235,12 @@ CLI.prototype.deleteAll = function(callback, context) { if (confirm) store.clear(callback, context); else - callback.call(context); + callback.call(context, null); }); }; CLI.prototype.generate = function(service, params, callback, context) { - this._store.serviceSettings(service, function(error, settings) { + this._store.serviceSettings(service, true, function(error, settings) { if (error) return callback.call(context, error); Vault.extend(params, settings); @@ -203,6 +261,9 @@ CLI.prototype.generate = function(service, params, callback, context) { this._out.write(password); if (this._tty) this._out.write('\n'); + if (settings.notes !== undefined) + this._err.write('\n' + settings.notes.replace(/^\s*|\s*$/g, '') + '\n\n'); + callback.call(context, null); }; diff --git a/node/editor.js b/node/editor.js new file mode 100644 index 0000000..506c51c --- /dev/null +++ b/node/editor.js @@ -0,0 +1,57 @@ +var async = require('async'), + child = require('child_process'), + crypto = require('crypto'), + fs = require('fs'), + mkdirp = require('mkdirp'), + path = require('path'); + +var Editor = function(path) { + this._path = path; + this._editor = process.env.EDITOR || process.env.VISUAL; +}; + +Editor.DEFAULT_EDITOR = 'vim'; + +Editor.editTempfile = function(content, callback, context) { + var path = '/tmp/' + crypto.randomBytes(16).toString('hex'); + return this.edit(path, content, callback, context); +}; + +Editor.edit = function(path, content, callback, context) { + new Editor(path).edit(content, callback, context); +}; + +Editor.prototype.edit = function(content, callback, context) { + if (!this._editor) + return callback.call(context, new Error('No editor detected, please set $EDITOR and retry')); + + var _path = this._path, + editor = this._editor; + + async.waterfall([ + function(next) { + mkdirp(path.dirname(_path), next); + }, + function(_, next) { + fs.writeFile(_path, content, next); + }, + function(next) { + var proc = child.spawn(editor, [_path], {stdio: [0,1,2]}); + proc.on('exit', function(status) { + next(status === 0 ? null : new Error('Editor exited with non-zero status (' + status + ')')); + }); + }, + function(next) { + fs.readFile(_path, next); + }, + function(content, next) { + fs.unlink(_path, function(error) { next(error, content) }); + } + ], + function(error, content) { + callback.call(context, error, content && content.toString('utf8')); + }); +}; + +module.exports = Editor; + diff --git a/node/local_store.js b/node/local_store.js index 88bb36e..b5aaa38 100644 --- a/node/local_store.js +++ b/node/local_store.js @@ -2,39 +2,58 @@ var fs = require('fs'), Cipher = require('vault-cipher'), Vault = require('../lib/vault'); -var sort = function(object) { - if (typeof object !== 'object') return object; - if (object === null) return null; - - if (object instanceof Array) - return object.map(function(o) { return sort(o) }) +var LocalStore = function(options) { + this._path = options.path; + this._cipher = new Cipher(options.key, {format: 'base64', work: 100, salt: Vault.UUID}); + this._cache = options.cache !== false; +}; - var copy = {}, keys = Object.keys(object).sort(); - for (var i = 0, n = keys.length; i < n; i++) - copy[keys[i]] = sort(object[keys[i]]); +LocalStore.LOCAL = 'local'; - return copy; +LocalStore.prototype.getName = function() { + return LocalStore.LOCAL; }; -var LocalStore = function(options) { - this._path = options.path; - this._cipher = new Cipher(options.key, {format: 'base64', work: 100, salt: Vault.UUID}); +LocalStore.prototype.setSource = function(source) { + this._source = source; }; LocalStore.prototype.clear = function(callback, context) { this.load(function(error, config) { if (error) return callback.call(context, error); - fs.unlink(this._path, function() { - callback.apply(context, arguments); - }); + config.global = {}; + config.services = {}; + + this.dump(config, callback, context); + }, this); +}; + +LocalStore.prototype.getStore = function(source, callback, context) { + this.load(function(error, config) { + if (error) return callback.call(context, error); + + var store = (!source || source === LocalStore.LOCAL) + ? this + : new RemoteStore(source, config.sources[source]); + + callback.call(context, null, store); + }, this); +}; + +LocalStore.prototype.currentStore = function(callback, context) { + this.load(function(error, config) { + if (error) return callback.call(context, error); + + var current = this._source || (config.sources || {}).__current__; + this.getStore(current, callback, context); }, this); }; LocalStore.prototype.listServices = function(callback, context) { this.load(function(error, config) { if (error) return callback.call(context, error); - callback.call(context, null, Object.keys(config.services).sort()); + callback.call(context, null, Object.keys(config.services || {}).sort()); }); }; @@ -57,6 +76,8 @@ LocalStore.prototype.saveService = function(service, settings, callback, context this.load(function(error, config) { if (error) return callback.cal(context, error); + config.services = config.services || {}; + var saved = config.services[service] || {}, updated = {}; @@ -80,7 +101,7 @@ LocalStore.prototype.deleteService = function(service, callback, context) { this.load(function(error, config) { if (error) return callback.call(context, error); - if (!config.services[service]) + if (!config.services || !config.services[service]) return callback.call(context, new Error('Service "' + service + '" is not configured')); delete config.services[service]; @@ -88,12 +109,15 @@ LocalStore.prototype.deleteService = function(service, callback, context) { }, this); }; -LocalStore.prototype.serviceSettings = function(service, callback, context) { +LocalStore.prototype.serviceSettings = function(service, includeGlobal, callback, context) { this.load(function(error, config) { if (error) return callback.call(context, error); + if (!includeGlobal && (!config.services || !config.services[service])) + return callback.call(context, null, null); + var settings = {}; - Vault.extend(settings, config.services[service] || {}); + Vault.extend(settings, (config.services || {})[service] || {}); Vault.extend(settings, config.global || {}); callback.call(context, null, settings); @@ -101,12 +125,16 @@ LocalStore.prototype.serviceSettings = function(service, callback, context) { }; LocalStore.prototype.load = function(callback, context) { + if (this._cache && this._configCache) + return callback.call(context, null, this._configCache); + var self = this; fs.readFile(this._path, function(error, content) { if (error) - return callback.call(context, null, {global: {}, services: {}}); + return callback.call(context, null, {global: {}, services: {}, sources: {}}); self._cipher.decrypt(content.toString(), function(error, plaintext) { + var err = new Error('Your .vault file is unreadable; check your VAULT_KEY and VAULT_PATH settings'); if (error) return callback.call(context, err); @@ -116,6 +144,7 @@ LocalStore.prototype.load = function(callback, context) { } catch (e) { return callback.call(context, err); } + self._configCache = config; callback.call(context, null, config); }); }); @@ -123,23 +152,45 @@ LocalStore.prototype.load = function(callback, context) { LocalStore.prototype.dump = function(config, callback, context) { config = sort(config); - this.import(JSON.stringify(config, true, 2), callback, context); -}; + var json = JSON.stringify(config, true, 2); -LocalStore.prototype.import = function(string, callback, context) { - this._cipher.encrypt(string, function(error, ciphertext) { + this._cipher.encrypt(json, function(error, ciphertext) { fs.writeFile(this._path, ciphertext, function() { if (callback) callback.apply(context, arguments); }); }, this); }; +LocalStore.prototype.import = function(settings, callback, context) { + this.load(function(error, config) { + if (error) return callback.call(context, error); + Vault.extend(config.global, settings.global); + Vault.extend(config.services, settings.services); + this.dump(config, callback, context); + }, this); +}; + LocalStore.prototype.export = function(callback, context) { this.load(function(error, config) { - if (error) callback.call(context, error); - else callback.call(context, null, config && JSON.stringify(config, true, 2)); + if (error) return callback.call(context, error); + var exported = {global: config.global, services: config.services}; + callback.call(context, null, exported); }); }; +var sort = function(object) { + if (typeof object !== 'object') return object; + if (object === null) return null; + + if (object instanceof Array) + return object.map(function(o) { return sort(o) }) + + var copy = {}, keys = Object.keys(object).sort(); + for (var i = 0, n = keys.length; i < n; i++) + copy[keys[i]] = sort(object[keys[i]]); + + return copy; +}; + module.exports = LocalStore; diff --git a/node/usage.txt b/node/usage.txt new file mode 100644 index 0000000..7b5d15a --- /dev/null +++ b/node/usage.txt @@ -0,0 +1,27 @@ +Usage: vault [OPTIONS] [SERVICE] + +Password generation: + -p, --phrase prompt the user for the passphrase + -k, --key use SSH private key to generate passwords + -l, --length NUMBER emit password of length NUMBER + -r, --repeat NUMBER allow maximum of NUMBER repeated adjacent chars + --lower NUMBER include at least NUMBER lowercase letters + --upper NUMBER include at least NUMBER uppercase letters + --number NUMBER include at least NUMBER digits + --space NUMBER include at least NUMBER spaces + --dash NUMBER include at least NUMBER of "-" or "_" + --symbol NUMBER include at least NUMBER symbol chars + +Configuration: + -n, --notes open $EDITOR to edit notes for SERVICE + -c, --config save the given settings for SERVICE or global + -x, --delete NAME delete settings for SERVICE + --delete-globals delete the global shared settings + -X, --clear delete all settings + +Environment variables: + VAULT_KEY encryption key for settings file + VAULT_PATH path to settings file, default is $HOME/.vault + +Full documentation is available at http://github.com/jcoglan/vault + diff --git a/package.json b/package.json index b09d0ab..4c2a74b 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,17 @@ , "homepage" : "https://getvau.lt/" , "author" : "James Coglan (http://jcoglan.com/)" , "keywords" : ["security", "passwords"] +, "license" : "MIT" -, "version" : "0.3.0" +, "version" : "0.4.0" , "engines" : {"node": ">=0.4.0"} , "main" : "./lib/vault.js" , "bin" : {"vault": "./bin/vault"} , "preferGlobal" : true -, "dependencies" : { "posix-argv-parser": "~0.4.2" +, "dependencies" : { "async": "~0.2.0" + , "mkdirp": "~0.3.0" + , "posix-argv-parser": "~0.4.2" , "pw": "~0.0.4" , "ssh-agent": "~0.2.1" , "vault-cipher": "~0.3.0" @@ -21,16 +24,10 @@ , "test" : "node spec/node.js" } -, "bugs" : "http://github.com/jcoglan/vault/issues" - -, "licenses" : [ { "type" : "MIT" - , "url" : "http://www.opensource.org/licenses/mit-license.php" - } - ] +, "repository " : { "type" : "git" + , "url" : "git://github.com/jcoglan/vault.git" + } -, "repositories" : [ { "type" : "git" - , "url" : "git://github.com/jcoglan/vault.git" - } - ] +, "bugs" : "http://github.com/jcoglan/vault/issues" } diff --git a/spec/node/cli_spec.js b/spec/node/cli_spec.js index 98352a4..5e5870c 100644 --- a/spec/node/cli_spec.js +++ b/spec/node/cli_spec.js @@ -1,22 +1,30 @@ var fs = require('fs'), path = require('path'), + editor = require('../../node/editor'), LocalStore = require('../../node/local_store'), CLI = require('../../node/cli') JS.ENV.CliSpec = JS.Test.describe("CLI", function() { with(this) { + define("createStubs", function() {}) + before(function() { with(this) { + createStubs() + this.configPath = path.resolve(__dirname + "/.vault") this.exportPath = path.resolve(__dirname + "/export.json") - this.stdout = {} + this.stdout = {write: function() {}} + this.stderr = {write: function() {}} this.passphrase = "something" + this.confirm = true this.cli = new CLI({ - config: {path: configPath, key: "the key"}, - output: this.stdout, + config: {path: configPath, key: "the key", cache: false}, + stdout: this.stdout, + stderr: this.stderr, tty: false, confirm: function(message, callback) { - callback(true) + callback(confirm) }, password: function(callback) { @@ -35,7 +43,7 @@ JS.ENV.CliSpec = JS.Test.describe("CLI", function() { with(this) { } }) - this.storage = new LocalStore({path: configPath, key: "the key"}) + this.storage = new LocalStore({path: configPath, key: "the key", cache: false}) }}) after(function() { with(this) { @@ -86,22 +94,20 @@ JS.ENV.CliSpec = JS.Test.describe("CLI", function() { with(this) { cli.run(["node", "bin/vault", "google"], function(e) { resume(function() { assertEqual( "No passphrase given; pass `-p` or run `vault -cp`", e.message ) - }) - }) + })}) }}) it("reports an error if no service given", function(resume) { with(this) { cli.run(["node", "bin/vault"], function(e) { resume(function() { assertEqual( "No service name given", e.message ) - }) - }) + })}) }}) it("saves a global passphrase", function(resume) { with(this) { cli.run(["node", "bin/vault", "-cp"], function() { - storage.serviceSettings("internet", function(e, internet) { - storage.serviceSettings("google", function(e, google) { + storage.serviceSettings("internet", true, function(e, internet) { + storage.serviceSettings("google", true, function(e, google) { resume(function() { assertEqual( {phrase: "something"}, internet ) assertEqual( {phrase: "something"}, google ) @@ -110,8 +116,8 @@ JS.ENV.CliSpec = JS.Test.describe("CLI", function() { with(this) { it("saves a service-specific passphrase", function(resume) { with(this) { cli.run(["node", "bin/vault", "-cp", "google"], function() { - storage.serviceSettings("internet", function(e, internet) { - storage.serviceSettings("google", function(e, google) { + storage.serviceSettings("internet", true, function(e, internet) { + storage.serviceSettings("google", true, function(e, google) { resume(function() { assertEqual( {}, internet ) assertEqual( {phrase: "something"}, google ) @@ -120,8 +126,8 @@ JS.ENV.CliSpec = JS.Test.describe("CLI", function() { with(this) { it("saves a global public key", function(resume) { with(this) { cli.run(["node", "bin/vault", "-ck"], function() { - storage.serviceSettings("internet", function(e, internet) { - storage.serviceSettings("google", function(e, google) { + storage.serviceSettings("internet", true, function(e, internet) { + storage.serviceSettings("google", true, function(e, google) { resume(function() { assertEqual( {key: "AAAAPUBLICKEY"}, internet ) assertEqual( {key: "AAAAPUBLICKEY"}, google ) @@ -130,8 +136,8 @@ JS.ENV.CliSpec = JS.Test.describe("CLI", function() { with(this) { it("saves a service-specific public key", function(resume) { with(this) { cli.run(["node", "bin/vault", "-ck", "google"], function() { - storage.serviceSettings("internet", function(e, internet) { - storage.serviceSettings("google", function(e, google) { + storage.serviceSettings("internet", true, function(e, internet) { + storage.serviceSettings("google", true, function(e, google) { resume(function() { assertEqual( {}, internet ) assertEqual( {key: "AAAAPUBLICKEY"}, google ) @@ -140,8 +146,8 @@ JS.ENV.CliSpec = JS.Test.describe("CLI", function() { with(this) { it("saves a global character constraint", function(resume) { with(this) { cli.run(["node", "bin/vault", "-c", "--length", "6", "--symbol", "0"], function() { - storage.serviceSettings("internet", function(e, internet) { - storage.serviceSettings("google", function(e, google) { + storage.serviceSettings("internet", true, function(e, internet) { + storage.serviceSettings("google", true, function(e, google) { resume(function() { assertEqual( {length: 6, symbol: 0}, internet ) assertEqual( {length: 6, symbol: 0}, google ) @@ -150,51 +156,68 @@ JS.ENV.CliSpec = JS.Test.describe("CLI", function() { with(this) { it("saves a service-specific character constraint", function(resume) { with(this) { cli.run(["node", "bin/vault", "-c", "google", "--length", "6", "--symbol", "0"], function() { - storage.serviceSettings("internet", function(e, internet) { - storage.serviceSettings("google", function(e, google) { + storage.serviceSettings("internet", true, function(e, internet) { + storage.serviceSettings("google", true, function(e, google) { resume(function() { assertEqual( {}, internet ) assertEqual( {length: 6, symbol: 0}, google ) })})})}) }}) + it("saves some notes for a service", function(resume) { with(this) { + stub(editor, "edit").yields([null, "Saved notes!"]) + cli.run(["node", "bin/vault", "-c", "--notes", "google"], function() { + storage.serviceSettings("google", true, function(e, google) { + resume(function() { + assertEqual( {notes: "Saved notes!"}, google ) + })})}) + }}) + + it("deletes the notes for a service", function(resume) { with(this) { + stub(editor, "edit").yields([null, " \n\r\t\t \n\r\n "]) + cli.run(["node", "bin/vault", "-c", "--notes", "google"], function() { + storage.serviceSettings("google", true, function(e, google) { + resume(function() { + assertEqual( {}, google ) + })})}) + }}) + it("exports the default settings", function(resume) { with(this) { cli.run(["node", "bin/vault", "-e", exportPath], function() { resume(function() { var json = JSON.parse(fs.readFileSync(exportPath)) assertEqual( {global: {}, services: {}}, json ) - }) - }) + })}) }}) it("imports a saved settings file", function(resume) { with(this) { fs.writeFileSync(exportPath, '{"services":{"google":{"length":8}}}') cli.run(["node", "bin/vault", "-i", exportPath], function() { - storage.serviceSettings("google", function(e, google) { + storage.serviceSettings("google", true, function(e, google) { resume(function() { assertEqual( {length: 8}, google ) }) - }) - }) + })}) }}) }}) describe("with a config file", function() { with(this) { before(function(resume) { with(this) { storage.load(function(error, config) { - config.services.twitter = {lower: 1, symbol: 0} - config.services.nothing = {} - config.services.facebook= {key: "AAAAPUBLICKEY"} config.global = {lower: 0, phrase: "saved passphrase"} + + config.services.twitter = {lower: 1, symbol: 0} + config.services.nothing = {notes: "\nSome notes!\n===========\n\n\n\n"} + config.services.facebook = {key: "AAAAPUBLICKEY"} + storage.dump(config, resume) }) }}) it("reports an error if the key is wrong", function(resume) { with(this) { - cli._store = new LocalStore({path: configPath, key: "the wrong key"}) + cli._store = new LocalStore({path: configPath, key: "the wrong key", cache: false}) cli.run(["node", "bin/vault", "google"], function(e) { resume(function() { assertEqual( "Your .vault file is unreadable; check your VAULT_KEY and VAULT_PATH settings", e.message ) - }) - }) + })}) }}) it("reports an error if the file has been tampered", function(resume) { with(this) { @@ -202,8 +225,7 @@ JS.ENV.CliSpec = JS.Test.describe("CLI", function() { with(this) { cli.run(["node", "bin/vault", "google"], function(e) { resume(function() { assertEqual( "Your .vault file is unreadable; check your VAULT_KEY and VAULT_PATH settings", e.message ) - }) - }) + })}) }}) it("reports an error if the file has a zero-length payload", function(resume) { with(this) { @@ -211,8 +233,7 @@ JS.ENV.CliSpec = JS.Test.describe("CLI", function() { with(this) { cli.run(["node", "bin/vault", "google"], function(e) { resume(function() { assertEqual( "Your .vault file is unreadable; check your VAULT_KEY and VAULT_PATH settings", e.message ) - }) - }) + })}) }}) it("reports an error if the file is too short", function(resume) { with(this) { @@ -220,17 +241,21 @@ JS.ENV.CliSpec = JS.Test.describe("CLI", function() { with(this) { cli.run(["node", "bin/vault", "google"], function(e) { resume(function() { assertEqual( "Your .vault file is unreadable; check your VAULT_KEY and VAULT_PATH settings", e.message ) - }) - }) + })}) }}) it("completes option fragments", function(resume) { with(this) { - expect(stdout, "write").given("--length\n--lower") - cli.run(["node", "bin/vault", "--cmplt", "--l"], function() { resume() }) + expect(stdout, "write").given("--lower") + cli.run(["node", "bin/vault", "--cmplt", "--lo"], function() { resume() }) }}) it("completes option fragments with no letters", function(resume) { with(this) { - expect(stdout, "write").given("--clear\n--cmplt\n--config\n--dash\n--delete\n--delete-globals\n--export\n--import\n--initpath\n--key\n--length\n--lower\n--number\n--phrase\n--repeat\n--space\n--symbol\n--upper") + expect(stdout, "write").given( ["--clear", "--cmplt", "--config", + "--dash", "--delete", "--delete-globals", "--export", "--help", + "--import", "--initpath", "--key", "--length", "--lower", "--notes", + "--number", "--phrase", "--repeat", "--space", "--symbol", + "--upper"].join("\n") ) + cli.run(["node", "bin/vault", "--cmplt", "--"], function() { resume() }) }}) @@ -240,7 +265,7 @@ JS.ENV.CliSpec = JS.Test.describe("CLI", function() { with(this) { }}) it("completes empty service names", function(resume) { with(this) { - expect(stdout, "write").given("facebook\nnothing\ntwitter") + expect(stdout, "write").given(["facebook", "nothing", "twitter"].join("\n")) cli.run(["node", "bin/vault", "--cmplt", ""], function() { resume() }) }}) @@ -269,50 +294,51 @@ JS.ENV.CliSpec = JS.Test.describe("CLI", function() { with(this) { cli.run(["node", "bin/vault", "twitter", "--symbol", "4"], function() { resume() }) }}) + it("outputs a password on stdout and the notes on stderr", function(resume) { with(this) { + expect(stdout, "write").given("1$5QC<' Date: Thu, 12 Dec 2013 19:47:32 +0000 Subject: [PATCH 06/19] Import Composite store from remote-storage branch into master. --- node/cli.js | 19 +++--- node/composite_store.js | 134 ++++++++++++++++++++++++++++++++++++++++ node/local_store.js | 26 +++++++- spec/node/cli_spec.js | 34 +++++----- 4 files changed, 185 insertions(+), 28 deletions(-) create mode 100644 node/composite_store.js diff --git a/node/cli.js b/node/cli.js index ad197c7..d14ca1b 100644 --- a/node/cli.js +++ b/node/cli.js @@ -48,7 +48,7 @@ var exists = fs.existsSync || path.existsSync; var CLI = function(options) { this._parser = new OptParser(OPTIONS, SHORTS, ['service']); - this._store = new LocalStore(options.config); + this._store = new LocalStore(options.config).composite(); this._out = options.stdout; this._err = options.stderr; this._tty = options.tty; @@ -105,11 +105,15 @@ CLI.prototype.complete = function(word, callback, context) { this._out.write(names.sort().join('\n')); callback.call(context, null); } else { - this._store.listServices(function(error, services) { - if (error) return callback.call(context, new Error('\n' + error.message)); - var all = services.filter(function(s) { return s.indexOf(word) === 0 }); - this._out.write(all.sort().join('\n')); - callback.call(context, error); + this._store.listSources(function(error, sources) { + sources = []; // temporary measure until remote sources are added + this._store.listServices(function(error, services) { + if (error) return callback.call(context, new Error('\n' + error.message)); + var all = services.concat(sources); + all = all.filter(function(s) { return s.indexOf(word) === 0 }); + this._out.write(all.sort().join('\n')); + callback.call(context, error); + }, this); }, this); } }; @@ -159,8 +163,7 @@ CLI.prototype.withPhrase = function(params, callback) { CLI.prototype.export = function(path, callback, context) { var self = this; - this._store.export(function(error, config) { - var store = 'local'; + this._store.export(function(error, store, config) { if (error) return callback.call(context, error); config = config || {global: {}, services: {}}; var json = JSON.stringify(config, true, 2); diff --git a/node/composite_store.js b/node/composite_store.js new file mode 100644 index 0000000..8e3d9a3 --- /dev/null +++ b/node/composite_store.js @@ -0,0 +1,134 @@ +var CompositeStore = function(local) { + this._local = local; +}; + +CompositeStore.prototype.setSource = function(source) { + this._source = source; + this._local.setSource(source); +}; + +CompositeStore.prototype.currentStore = function(callback, context) { + this._local.currentStore(callback, context); +}; + +var single = function(name) { + CompositeStore.prototype[name] = function() { + var args = Array.prototype.slice.call(arguments), + arity = this._local[name].length, + params = args.slice(0, arity - 2), + callback = args[arity - 2], + context = args[arity - 1]; + + this._local.currentStore(function(error, store) { + if (error) return callback.call(context, error); + + params.push(function(error) { + var result = [error, store.getName()].concat(Array.prototype.slice.call(arguments, 1)); + callback.apply(context, result); + }); + var method = store[name]; + method.apply(store, params); + }, this); + }; +}; + +var resolveConcat = function(results, callback, context) { + output = Object.keys(results) + .map(function(s) { return results[s] }) + .reduce(function(a, b) { return a.concat(b) }); + + callback.call(context, null, output); +}; + +var resolveChoice = function(results, backends, current, name, params, callback, context) { + var candidates = Object.keys(results); + selected = (candidates.length === 1) ? candidates[0] : current, + method = backends[selected][name]; + + params.push(function(error, result) { + callback.call(context, null, result); + }); + method.apply(backends[selected], params); +}; + +var multi = function(name, concat) { + CompositeStore.prototype[name] = function() { + var args = Array.prototype.slice.call(arguments), + arity = this._local[name].length, + params = args.slice(0, arity - 2), + callback = args[arity - 2], + context = args[arity - 1]; + + if (this._source) + return this._local.getStore(this._source, function(error, store) { + if (error) return callback.call(context, error); + store[name].apply(store, args); + }); + + this._local.listSources(function(error, stores, current) { + if (error) return callback.call(context, error); + + var backends = {}, + results = {}, + length = stores.length, + complete = 0, + called = false; + + var collect = function(storeName, error, result) { + if (called) return; + if (concat || result !== null) results[storeName] = result; + complete += 1; + if (error) { + callback.call(context, error); + called = true; + } else if (complete === length) { + if (concat) + resolveConcat(results, callback, context); + else + resolveChoice(results, backends, current, name, params, callback, context); + + called = true; + } + }; + + stores.forEach(function(source) { + this._local.getStore(source, function(error, store) { + backends[source] = store; + + var method = store[name], + message = params.slice(); + + message.push(function(error, result) { + collect(store.getName(), error, result); + }); + message[arity - 3] = false; + method.apply(store, message); + }); + }, this); + }, this); + }; +}; + +var local = function(name) { + CompositeStore.prototype[name] = function() { + var method = this._local[name]; + return method.apply(this._local, arguments); + }; +}; + +single('saveGlobals'); +single('saveService'); +single('deleteGlobals'); +single('deleteService'); +single('clear'); + +multi('listServices', true); +multi('serviceSettings', false); + +local('listSources'); + +single('export'); +single('import'); + +module.exports = CompositeStore; + diff --git a/node/local_store.js b/node/local_store.js index b5aaa38..8f99dc9 100644 --- a/node/local_store.js +++ b/node/local_store.js @@ -1,6 +1,7 @@ -var fs = require('fs'), - Cipher = require('vault-cipher'), - Vault = require('../lib/vault'); +var fs = require('fs'), + Cipher = require('vault-cipher'), + Vault = require('../lib/vault'), + CompositeStore = require('./composite_store'); var LocalStore = function(options) { this._path = options.path; @@ -29,6 +30,25 @@ LocalStore.prototype.clear = function(callback, context) { }, this); }; +LocalStore.prototype.composite = function() { + return new CompositeStore(this); +}; + +LocalStore.prototype.listSources = function(callback, context) { + this.load(function(error, config) { + if (error) return callback.call(context, error); + + var sources = config.sources || {}, + sourceNames = Object.keys(sources) + .filter(function(s) { return !/^__.+__$/.test(s) }); + + var current = this._source || sources.__current__; + if (!current || !sources[current]) current = LocalStore.LOCAL; + + callback.call(context, null, sourceNames.concat(LocalStore.LOCAL), current); + }, this); +}; + LocalStore.prototype.getStore = function(source, callback, context) { this.load(function(error, config) { if (error) return callback.call(context, error); diff --git a/spec/node/cli_spec.js b/spec/node/cli_spec.js index 5e5870c..0d11b67 100644 --- a/spec/node/cli_spec.js +++ b/spec/node/cli_spec.js @@ -55,39 +55,39 @@ JS.ENV.CliSpec = JS.Test.describe("CLI", function() { with(this) { describe("with no config file", function() { with(this) { it("outputs a generated password", function(resume) { with(this) { expect(stdout, "write").given("2hk!W[L,2rWWI=~=l>,E") - cli.run(["node", "bin/vault", "google", "-p"], function() { resume() }) + cli.run(["node", "bin/vault", "google", "-p"], resume) }}) it("generates a password using a private key", function(resume) { with(this) { expect(stdout, "write").given("c8a-4w1S R") passphrase = "She cells C shells bye the sea shoars" - cli.run(["node", "bin/vault", "google", "-p", "--dash", "2", "--lower", "2", "--space", "3", "--upper", "2", "--symbol", "1", "--number", "1"], function() { resume() }) + cli.run(["node", "bin/vault", "google", "-p", "--dash", "2", "--lower", "2", "--space", "3", "--upper", "2", "--symbol", "1", "--number", "1"], resume) }}) it("outputs a password with a length", function(resume) { with(this) { expect(stdout, "write").given("Tc8k~8") - cli.run(["node", "bin/vault", "google", "-p", "-l", "6"], function() { resume() }) + cli.run(["node", "bin/vault", "google", "-p", "-l", "6"], resume) }}) it("outputs a password with a repetition limit", function(resume) { with(this) { passphrase = "" expect(stdout, "write").given("IVTDzACftqopUXqDHPkuCIhV") - cli.run(["node", "bin/vault", "asd", "-p", "--number", "0", "--symbol", "0", "-l", "24", "-r", "1"], function() { resume() }) + cli.run(["node", "bin/vault", "asd", "-p", "--number", "0", "--symbol", "0", "-l", "24", "-r", "1"], resume) }}) it("reports an error if no passphrase given", function(resume) { with(this) { @@ -246,7 +246,7 @@ JS.ENV.CliSpec = JS.Test.describe("CLI", function() { with(this) { it("completes option fragments", function(resume) { with(this) { expect(stdout, "write").given("--lower") - cli.run(["node", "bin/vault", "--cmplt", "--lo"], function() { resume() }) + cli.run(["node", "bin/vault", "--cmplt", "--lo"], resume) }}) it("completes option fragments with no letters", function(resume) { with(this) { @@ -256,48 +256,48 @@ JS.ENV.CliSpec = JS.Test.describe("CLI", function() { with(this) { "--number", "--phrase", "--repeat", "--space", "--symbol", "--upper"].join("\n") ) - cli.run(["node", "bin/vault", "--cmplt", "--"], function() { resume() }) + cli.run(["node", "bin/vault", "--cmplt", "--"], resume) }}) it("completes service names", function(resume) { with(this) { expect(stdout, "write").given("twitter") - cli.run(["node", "bin/vault", "--cmplt", "tw"], function() { resume() }) + cli.run(["node", "bin/vault", "--cmplt", "tw"], resume) }}) it("completes empty service names", function(resume) { with(this) { expect(stdout, "write").given(["facebook", "nothing", "twitter"].join("\n")) - cli.run(["node", "bin/vault", "--cmplt", ""], function() { resume() }) + cli.run(["node", "bin/vault", "--cmplt", ""], resume) }}) it("outputs a password using the stored passphrase", function(resume) { with(this) { expect(stdout, "write").given("(JA!4O'+&5I'/-V{N100") - cli.run(["node", "bin/vault", "google"], function() { resume() }) + cli.run(["node", "bin/vault", "google"], resume) }}) it("outputs a password using a service-specific passphrase", function(resume) { with(this) { expect(stdout, "write").given("199pS3LWcTpgGBMEDkx9") - cli.run(["node", "bin/vault", "twitter"], function() { resume() }) + cli.run(["node", "bin/vault", "twitter"], resume) }}) it("outputs a password using a service-specific private key", function(resume) { with(this) { expect(stdout, "write").given('++IAP:^*$6,"9~R-}%R-') - cli.run(["node", "bin/vault", "facebook"], function() { resume() }) + cli.run(["node", "bin/vault", "facebook"], resume) }}) it("allows the --phrase flag to override stored keys", function(resume) { with(this) { expect(stdout, "write").given("Q}T.}#S+SE#@+'|}5Q\\A") - cli.run(["node", "bin/vault", "facebook", "-p"], function() { resume() }) + cli.run(["node", "bin/vault", "facebook", "-p"], resume) }}) it("outputs a password using service-specific settings with overrides", function(resume) { with(this) { expect(stdout, "write").given("^g;Y4[k+Sg!1Z1fxY Date: Thu, 12 Dec 2013 19:52:59 +0000 Subject: [PATCH 07/19] Rename readme file. --- README.markdown => README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename README.markdown => README.md (100%) diff --git a/README.markdown b/README.md similarity index 100% rename from README.markdown rename to README.md From a0d815430a9e161e9cfe2756130b3322289dda56 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Thu, 12 Dec 2013 19:55:59 +0000 Subject: [PATCH 08/19] Replace chrome copies of files with a symlink. --- chrome/lib | 1 + chrome/lib/app.js | 129 ----------------------- chrome/lib/crypto-js-3.1.2.js | 20 ---- chrome/lib/style.css | 133 ----------------------- chrome/lib/vault.js | 191 ---------------------------------- 5 files changed, 1 insertion(+), 473 deletions(-) create mode 120000 chrome/lib delete mode 100644 chrome/lib/app.js delete mode 100644 chrome/lib/crypto-js-3.1.2.js delete mode 100644 chrome/lib/style.css delete mode 100644 chrome/lib/vault.js diff --git a/chrome/lib b/chrome/lib new file mode 120000 index 0000000..dc598c5 --- /dev/null +++ b/chrome/lib @@ -0,0 +1 @@ +../lib \ No newline at end of file diff --git a/chrome/lib/app.js b/chrome/lib/app.js deleted file mode 100644 index d620846..0000000 --- a/chrome/lib/app.js +++ /dev/null @@ -1,129 +0,0 @@ -var $ = function(id) { return document.getElementById(id) }; - -var on = function(element, event, listener) { - if (!element) return; - if (element.addEventListener) - element.addEventListener(event, listener, false); - else - element.attachEvent('on' + event, listener); -}; - -var getRadio = function(name) { - var inputs = document.getElementsByTagName('input'), input; - for (var i = 0, n = inputs.length; i < n; i++) { - input = inputs[i]; - if (input.type === 'radio' && input.name === name && input.checked) - return input.value; - } -}; - -var setRadio = function(name, value) { - var inputs = document.getElementsByTagName('input'), input; - for (var i = 0, n = inputs.length; i < n; i++) { - input = inputs[i]; - if (input.type === 'radio' && input.name === name) { - switch (input.value) { - case 'required': - input.checked = (value && value > 0); - break; - case 'allowed': - input.checked = (value === undefined); - break; - case 'forbidden': - input.checked = (value === 0); - break; - } - } - } -}; - -var togglePassword = function(id) { - var field = $(id), - text = $(id + '-text'), - checkbox = $('show-' + id); - - text.style.display = 'none'; - - on(checkbox, 'click', function() { - if (checkbox.checked) { - field.style.display = 'none'; - text.style.display = ''; - } else { - field.style.display = ''; - text.style.display = 'none'; - } - }); - - on(field, 'keyup', function() { text.value = field.value }); - on(text, 'keyup', function() { field.value = text.value }); -}; -togglePassword('passphrase'); - -var service = $('service'), - phrase = $('passphrase'), - required = $('required'), - length = $('vlength'), - repeat = $('repeat'), - word = $('word'), - wordText = $('word-text'), - TYPES = 'lower upper number dash space symbol'.split(' '); - -service.focus(); - -var getSettings = function() { - var plength = parseInt(length.value, 10), - prepeat = parseInt(repeat.value, 10), - rlength = parseInt(required.value, 10), - settings = {phrase: phrase.value, length: plength, repeat: prepeat}, - value; - - for (var i = 0, n = TYPES.length; i < n; i++) { - value = getRadio(TYPES[i]); - if (value === 'forbidden') - settings[TYPES[i]] = 0; - else if (value === 'required') - settings[TYPES[i]] = rlength; - } - - return settings; -}; - -var update = function() { - var settings = getSettings(); - try { - if (service.value && phrase.value) { - word.value = new Vault(settings).generate(service.value); - } else { - word.value = ''; - } - } catch (e) { - word.value = '!! ' + e.message; - } -}; - -var inputs = document.getElementsByTagName('input'); -for (var i = 0, n = inputs.length; i < n; i++) { - if (inputs[i].id === 'word' || inputs[i].type === 'checkbox') continue; - on(inputs[i], 'keyup', update); - on(inputs[i], 'change', update); -} - -var insert = function() { - var password = word.value.replace(/'/g, '\\\''); - chrome.tabs.executeScript(null, { - code: "(document.activeElement||{}).value = '" + password + "';" - }); - window.close(); -}; -var insertPassword = $('insert-password'); -on(insertPassword, 'click', function(e) { - e.preventDefault(); - insert(); -}); -on(service, 'keydown', function(e) { - if (e.keyCode === 13) insert(); -}); -on(phrase, 'keydown', function(e) { - if (e.keyCode === 13) insert(); -}); - diff --git a/chrome/lib/crypto-js-3.1.2.js b/chrome/lib/crypto-js-3.1.2.js deleted file mode 100644 index 576aff1..0000000 --- a/chrome/lib/crypto-js-3.1.2.js +++ /dev/null @@ -1,20 +0,0 @@ -/* -CryptoJS v3.1.2 -code.google.com/p/crypto-js -(c) 2009-2013 by Jeff Mott. All rights reserved. -code.google.com/p/crypto-js/wiki/License -*/ -var CryptoJS=CryptoJS||function(g,j){var e={},d=e.lib={},m=function(){},n=d.Base={extend:function(a){m.prototype=this;var c=new m;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, -q=d.WordArray=n.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=j?c:4*a.length},toString:function(a){return(a||l).stringify(this)},concat:function(a){var c=this.words,p=a.words,f=this.sigBytes;a=a.sigBytes;this.clamp();if(f%4)for(var b=0;b>>2]|=(p[b>>>2]>>>24-8*(b%4)&255)<<24-8*((f+b)%4);else if(65535>>2]=p[b>>>2];else c.push.apply(c,p);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< -32-8*(c%4);a.length=g.ceil(c/4)},clone:function(){var a=n.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],b=0;b>>2]>>>24-8*(f%4)&255;b.push((d>>>4).toString(16));b.push((d&15).toString(16))}return b.join("")},parse:function(a){for(var c=a.length,b=[],f=0;f>>3]|=parseInt(a.substr(f, -2),16)<<24-4*(f%8);return new q.init(b,c/2)}},k=b.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var b=[],f=0;f>>2]>>>24-8*(f%4)&255));return b.join("")},parse:function(a){for(var c=a.length,b=[],f=0;f>>2]|=(a.charCodeAt(f)&255)<<24-8*(f%4);return new q.init(b,c)}},h=b.Utf8={stringify:function(a){try{return decodeURIComponent(escape(k.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data");}},parse:function(a){return k.parse(unescape(encodeURIComponent(a)))}}, -u=d.BufferedBlockAlgorithm=n.extend({reset:function(){this._data=new q.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=h.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var b=this._data,d=b.words,f=b.sigBytes,l=this.blockSize,e=f/(4*l),e=a?g.ceil(e):g.max((e|0)-this._minBufferSize,0);a=e*l;f=g.min(4*a,f);if(a){for(var h=0;ha;a++){if(16>a)m[a]=d[e+a]|0;else{var c=m[a-3]^m[a-8]^m[a-14]^m[a-16];m[a]=c<<1|c>>>31}c=(l<<5|l>>>27)+j+m[a];c=20>a?c+((k&h|~k&g)+1518500249):40>a?c+((k^h^g)+1859775393):60>a?c+((k&h|k&g|h&g)-1894007588):c+((k^h^ -g)-899497514);j=g;g=h;h=k<<30|k>>>2;k=l;l=c}b[0]=b[0]+l|0;b[1]=b[1]+k|0;b[2]=b[2]+h|0;b[3]=b[3]+g|0;b[4]=b[4]+j|0},_doFinalize:function(){var d=this._data,e=d.words,b=8*this._nDataBytes,l=8*d.sigBytes;e[l>>>5]|=128<<24-l%32;e[(l+64>>>9<<4)+14]=Math.floor(b/4294967296);e[(l+64>>>9<<4)+15]=b;d.sigBytes=4*e.length;this._process();return this._hash},clone:function(){var e=d.clone.call(this);e._hash=this._hash.clone();return e}});g.SHA1=d._createHelper(j);g.HmacSHA1=d._createHmacHelper(j)})(); -(function(){var g=CryptoJS,j=g.enc.Utf8;g.algo.HMAC=g.lib.Base.extend({init:function(e,d){e=this._hasher=new e.init;"string"==typeof d&&(d=j.parse(d));var g=e.blockSize,n=4*g;d.sigBytes>n&&(d=e.finalize(d));d.clamp();for(var q=this._oKey=d.clone(),b=this._iKey=d.clone(),l=q.words,k=b.words,h=0;h= 0 && n--) this._required.push(this._allowed); -}; - -Vault.UUID = 'e87eb0f4-34cb-46b9-93ad-766c5ab063e7'; -Vault.DEFAULT_LENGTH = 20; -Vault.DEFAULT_REPEAT = 0; - -Vault.LOWER = 'abcdefghijklmnopqrstuvwxyz'.split(''); -Vault.UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); -Vault.ALPHA = Vault.LOWER.concat(Vault.UPPER); -Vault.NUMBER = '0123456789'.split(''); -Vault.ALPHANUM = Vault.ALPHA.concat(Vault.NUMBER); -Vault.SPACE = [' ']; -Vault.DASH = ['-', '_']; -Vault.SYMBOL = '!"#$%&\'()*+,./:;<=>?@[\\]^{|}~'.split('').concat(Vault.DASH); -Vault.ALL = Vault.ALPHANUM.concat(Vault.SPACE).concat(Vault.SYMBOL); - -Vault.TYPES = 'LOWER UPPER NUMBER SPACE DASH SYMBOL'.split(' '); - -Vault.extend = function(target, source) { - for (var key in source) { - if (!target.hasOwnProperty(key)) - target[key] = source[key]; - } - return target; -}; - -Vault.createHash = function(key, message, entropy) { - var CJS = (typeof CryptoJS !== 'undefined') ? CryptoJS : require('./crypto-js-3.1.2'), - bytes = (entropy || 256) / 8; - - return CJS.PBKDF2(key, message, {keySize: Math.ceil(bytes / 4), iterations: 8}).toString(); -}; - -Vault.indexOf = function(list, item) { - if (list.indexOf) return list.indexOf(item); - for (var i = 0, n = list.length; i < n; i++) { - if (list[i] === item) return i; - } - return -1; -}; - -Vault.map = function(list, callback, context) { - if (list.map) return list.map(callback, context); - var result = []; - for (var i = 0, n = list.length; i < n; i++) - result.push(callback.call(context, list[i])); - return result; -}; - -Vault.toBits = function(digit) { - var string = parseInt(digit, 16).toString(2); - while (string.length < 4) string = '0' + string; - return string; -}; - -Vault.prototype.subtract = function(charset, allowed) { - if (!charset) return; - allowed = allowed || this._allowed; - for (var i = 0, n = charset.length; i < n; i++) { - var index = Vault.indexOf(allowed, charset[i]); - if (index >= 0) allowed.splice(index, 1); - } - return allowed; -}; - -Vault.prototype.require = function(charset, n) { - if (!charset) return; - while (n--) this._required.push(charset); -}; - -Vault.prototype.entropy = function() { - var entropy = 0; - for (var i = 0, n = this._required.length; i < n; i++) { - entropy += Math.ceil(Math.log(i+1) / Math.log(2)); - entropy += Math.ceil(Math.log(this._required[i].length) / Math.log(2)); - } - return entropy; -}; - -Vault.prototype.generate = function(service) { - if (this._required.length > this._length) - throw new Error('Length too small to fit all required characters'); - - if (this._allowed.length === 0) - throw new Error('No characters available to create a password'); - - var required = this._required.slice(), - stream = new Vault.Stream(this._phrase, service, this.entropy()), - result = '', - index, charset, previous, i, same; - - while (result.length < this._length) { - index = stream.generate(required.length); - charset = required.splice(index, 1)[0]; - previous = result.charAt(result.length - 1); - i = this._repeat - 1; - same = previous && (i >= 0); - - while (same && i--) - same = same && result.charAt(result.length + i - this._repeat) === previous; - if (same) - charset = this.subtract([previous], charset.slice()); - - index = stream.generate(charset.length); - result += charset[index]; - } - - return result; -}; - - -// Generate uniformly distributed output in any base from a bit stream -// http://checkmyworking.com/2012/06/converting-a-stream-of-binary-digits-to-a-stream-of-base-n-digits/ - -Vault.Stream = function(phrase, service, entropy) { - this._phrase = phrase; - this._service = service; - - var hash = Vault.createHash(phrase, service + Vault.UUID, 2 * entropy), - bits = Vault.map(hash.split(''), Vault.toBits).join('').split(''); - - this._bases = { - '2': Vault.map(bits, function(s) { return parseInt(s, 2) }) - }; -}; - -Vault.Stream.prototype.generate = function(n, base, inner) { - base = base || 2; - - var value = n, - k = Math.ceil(Math.log(n) / Math.log(base)), - r = Math.pow(base, k) - n, - chunk; - - loop: while (value >= n) { - chunk = this._shift(base, k); - if (!chunk) return inner ? n : null; - - value = this._evaluate(chunk, base); - - if (value >= n) { - if (r === 1) continue loop; - this._push(r, value - n); - value = this.generate(n, r, true); - } - } - return value; -}; - -Vault.Stream.prototype._evaluate = function(chunk, base) { - var sum = 0, - i = chunk.length; - - while (i--) sum += chunk[i] * Math.pow(base, chunk.length - (i+1)); - return sum; -}; - -Vault.Stream.prototype._push = function(base, value) { - this._bases[base] = this._bases[base] || []; - this._bases[base].push(value); -}; - -Vault.Stream.prototype._shift = function(base, k) { - var list = this._bases[base]; - if (!list || list.length < k) return null; - else return list.splice(0,k); -}; - - -if (typeof module === 'object') - module.exports = Vault; - From a1ced4ad27e39672495e0f4b968903416064376b Mon Sep 17 00:00:00 2001 From: James Coglan Date: Thu, 12 Dec 2013 19:56:52 +0000 Subject: [PATCH 09/19] Sync .travis.yml from remote-storage. --- .travis.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4b66f32..f037471 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,8 @@ language: node_js node_js: - - 0.6 - - 0.8 - - 0.10 - - 0.11 + - "0.6" + - "0.8" + - "0.10" + - "0.11" + From 33e6551b4e780a17fd1ac4d6c082ae1e421cd262 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Thu, 12 Dec 2013 20:00:32 +0000 Subject: [PATCH 10/19] Add 'autocapitalize=off' to all form inputs. --- chrome/vault.html | 14 +++++++------- web/index.html | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/chrome/vault.html b/chrome/vault.html index 11de91f..161f614 100644 --- a/chrome/vault.html +++ b/chrome/vault.html @@ -3,7 +3,7 @@ Vault - + @@ -13,12 +13,12 @@
- +
- - + + @@ -27,12 +27,12 @@
- +
- +
@@ -52,7 +52,7 @@ - () + () diff --git a/web/index.html b/web/index.html index dac8756..cffd78e 100644 --- a/web/index.html +++ b/web/index.html @@ -28,7 +28,7 @@
- + @@ -38,12 +38,12 @@
- +
- +
@@ -63,7 +63,7 @@ - () + () From b0e8ac4b113e43708cc2cbb14bad3175eeee9003 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Thu, 12 Dec 2013 20:02:24 +0000 Subject: [PATCH 11/19] Add 'autocapitalize=off' to all form inputs. --- chrome/vault.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chrome/vault.html b/chrome/vault.html index 161f614..650ee44 100644 --- a/chrome/vault.html +++ b/chrome/vault.html @@ -88,7 +88,7 @@
- +
From 64cf0589ae6763dcf8ff323c35dae6613659a72e Mon Sep 17 00:00:00 2001 From: James Coglan Date: Thu, 12 Dec 2013 20:12:00 +0000 Subject: [PATCH 12/19] Clean up the HTML and package metadata. --- chrome/vault.html | 12 ++++++------ package.json | 2 +- web/faq.html | 4 ++-- web/index.html | 10 +++++----- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/chrome/vault.html b/chrome/vault.html index 650ee44..cad8af9 100644 --- a/chrome/vault.html +++ b/chrome/vault.html @@ -1,12 +1,12 @@ - + Vault - - - - + + + + @@ -93,7 +93,7 @@
- + diff --git a/package.json b/package.json index 4c2a74b..63d3157 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ , "test" : "node spec/node.js" } -, "repository " : { "type" : "git" +, "repository" : { "type" : "git" , "url" : "git://github.com/jcoglan/vault.git" } diff --git a/web/faq.html b/web/faq.html index 0b70630..96eb2a3 100644 --- a/web/faq.html +++ b/web/faq.html @@ -1,10 +1,10 @@ - + Vault: Frequently asked questions - + diff --git a/web/index.html b/web/index.html index cffd78e..9ee5192 100644 --- a/web/index.html +++ b/web/index.html @@ -1,12 +1,12 @@ - + Vault - - - + + + @@ -103,7 +103,7 @@
- + From ed3453f05dff454f38c93e40561a6ea7cca02228 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Thu, 12 Dec 2013 20:15:45 +0000 Subject: [PATCH 13/19] Remove an extraneous blank line. --- node/local_store.js | 1 - 1 file changed, 1 deletion(-) diff --git a/node/local_store.js b/node/local_store.js index 8f99dc9..fd32945 100644 --- a/node/local_store.js +++ b/node/local_store.js @@ -154,7 +154,6 @@ LocalStore.prototype.load = function(callback, context) { return callback.call(context, null, {global: {}, services: {}, sources: {}}); self._cipher.decrypt(content.toString(), function(error, plaintext) { - var err = new Error('Your .vault file is unreadable; check your VAULT_KEY and VAULT_PATH settings'); if (error) return callback.call(context, err); From 08e4a9b6c6207a56248d80e3a90a9f2eec772fff Mon Sep 17 00:00:00 2001 From: James Coglan Date: Thu, 12 Dec 2013 21:34:58 +0000 Subject: [PATCH 14/19] Remove .redcar from .npmignore. --- .npmignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.npmignore b/.npmignore index 9e2ba61..cc6a6a3 100644 --- a/.npmignore +++ b/.npmignore @@ -1,7 +1,6 @@ .git .gitignore .npmignore -.redcar .travis.yml chrome node_modules From 6321cf4bdbeac0b37c0647c36cae703644d66ece Mon Sep 17 00:00:00 2001 From: James Coglan Date: Thu, 12 Dec 2013 21:35:34 +0000 Subject: [PATCH 15/19] Remove quotes from travis.yml. --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index f037471..1157212 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: node_js node_js: - - "0.6" - - "0.8" - - "0.10" - - "0.11" + - 0.6 + - 0.8 + - 0.10 + - 0.11 From 30a6a6733892f8a88397b3c6daba6bb652d5358e Mon Sep 17 00:00:00 2001 From: James Coglan Date: Thu, 12 Dec 2013 21:36:43 +0000 Subject: [PATCH 16/19] Revert "Remove quotes from travis.yml." This reverts commit 6321cf4bdbeac0b37c0647c36cae703644d66ece. --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1157212..f037471 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: node_js node_js: - - 0.6 - - 0.8 - - 0.10 - - 0.11 + - "0.6" + - "0.8" + - "0.10" + - "0.11" From a18ba651a0ae2714a3cb269812bf6d9bbcec833b Mon Sep 17 00:00:00 2001 From: James Coglan Date: Fri, 13 Dec 2013 02:25:44 +0000 Subject: [PATCH 17/19] Use posix-argv-parser 1.0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 63d3157..d64ebff 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ , "dependencies" : { "async": "~0.2.0" , "mkdirp": "~0.3.0" - , "posix-argv-parser": "~0.4.2" + , "posix-argv-parser": "~1.0.0" , "pw": "~0.0.4" , "ssh-agent": "~0.2.1" , "vault-cipher": "~0.3.0" From f3e4d28d3da6bc81cda0a323a8ab1788535cea36 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Fri, 13 Dec 2013 12:12:07 +0000 Subject: [PATCH 18/19] Tweaks to the --help copy. --- node/editor.js | 2 +- node/usage.txt | 36 +++++++++++++++++++----------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/node/editor.js b/node/editor.js index 506c51c..8e25eb5 100644 --- a/node/editor.js +++ b/node/editor.js @@ -23,7 +23,7 @@ Editor.edit = function(path, content, callback, context) { Editor.prototype.edit = function(content, callback, context) { if (!this._editor) - return callback.call(context, new Error('No editor detected, please set $EDITOR and retry')); + return callback.call(context, new Error('No editor detected, please set EDITOR and retry')); var _path = this._path, editor = this._editor; diff --git a/node/usage.txt b/node/usage.txt index 7b5d15a..d1b5358 100644 --- a/node/usage.txt +++ b/node/usage.txt @@ -1,27 +1,29 @@ Usage: vault [OPTIONS] [SERVICE] Password generation: - -p, --phrase prompt the user for the passphrase - -k, --key use SSH private key to generate passwords - -l, --length NUMBER emit password of length NUMBER - -r, --repeat NUMBER allow maximum of NUMBER repeated adjacent chars - --lower NUMBER include at least NUMBER lowercase letters - --upper NUMBER include at least NUMBER uppercase letters - --number NUMBER include at least NUMBER digits - --space NUMBER include at least NUMBER spaces - --dash NUMBER include at least NUMBER of "-" or "_" - --symbol NUMBER include at least NUMBER symbol chars + -p, --phrase prompts you for your passphrase + -k, --key uses your SSH private key to generate passwords + -l, --length NUMBER emits password of length NUMBER + -r, --repeat NUMBER allows maximum of NUMBER repeated adjacent chars + --lower NUMBER includes at least NUMBER lowercase letters + --upper NUMBER includes at least NUMBER uppercase letters + --number NUMBER includes at least NUMBER digits + --space NUMBER includes at least NUMBER spaces + --dash NUMBER includes at least NUMBER of "-" or "_" + --symbol NUMBER includes at least NUMBER symbol chars + +Use NUMBER=0, e.g. "--symbol 0", to exclude a character type from the output Configuration: - -n, --notes open $EDITOR to edit notes for SERVICE - -c, --config save the given settings for SERVICE or global - -x, --delete NAME delete settings for SERVICE - --delete-globals delete the global shared settings - -X, --clear delete all settings + -n, --notes SERVICE opens $EDITOR to edit notes for SERVICE + -c, --config saves the given settings for SERVICE or global + -x, --delete SERVICE deletes settings for SERVICE + --delete-globals deletes the global shared settings + -X, --clear deletes all settings Environment variables: - VAULT_KEY encryption key for settings file - VAULT_PATH path to settings file, default is $HOME/.vault + VAULT_KEY encryption key for the settings file + VAULT_PATH path to the settings file, default is $HOME/.vault Full documentation is available at http://github.com/jcoglan/vault From 9e22f228ffe468186a015ab9b5bf0c1ab2d4b201 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sat, 14 Dec 2013 19:06:27 +0000 Subject: [PATCH 19/19] s/cal/call/ in LocalStore. --- node/local_store.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node/local_store.js b/node/local_store.js index fd32945..32f41cd 100644 --- a/node/local_store.js +++ b/node/local_store.js @@ -79,7 +79,7 @@ LocalStore.prototype.listServices = function(callback, context) { LocalStore.prototype.saveGlobals = function(settings, callback, context) { this.load(function(error, config) { - if (error) return callback.cal(context, error); + if (error) return callback.call(context, error); var saved = config.global || {}, updated = {}; @@ -94,7 +94,7 @@ LocalStore.prototype.saveGlobals = function(settings, callback, context) { LocalStore.prototype.saveService = function(service, settings, callback, context) { this.load(function(error, config) { - if (error) return callback.cal(context, error); + if (error) return callback.call(context, error); config.services = config.services || {};