1 | 'use strict';
|
2 | var AVAILABLE_FIELDS, DEBUG, DEFAULT_CONFIG, OUTPUTS_DIR, TEMPLATES_DIR, async, chalk, cli, config, exec, f, filterTemplates, fs, getChoicesFrom, getConfig, getConfigPath, getFrom, getHomeDir, getOutputDir, getQuestionFor, inquirer, log, logJson, meow, path, readOwnConfig, readTemplates, templates, toJson;
|
3 |
|
4 | inquirer = require('inquirer');
|
5 |
|
6 | exec = require('child_process').exec;
|
7 |
|
8 | async = require('async');
|
9 |
|
10 | chalk = require('chalk');
|
11 |
|
12 | meow = require('meow');
|
13 |
|
14 | path = require('path');
|
15 |
|
16 | fs = require('fs-extra');
|
17 |
|
18 | DEBUG = false;
|
19 |
|
20 | TEMPLATES_DIR = 'templates';
|
21 |
|
22 | OUTPUTS_DIR = 'outputs';
|
23 |
|
24 | DEFAULT_CONFIG = {
|
25 | path: OUTPUTS_DIR,
|
26 | extensions: '.enc.txt'
|
27 | };
|
28 |
|
29 | AVAILABLE_FIELDS = {
|
30 | name: {},
|
31 | website: {},
|
32 | login: {},
|
33 | password: {
|
34 | type: 'password'
|
35 | },
|
36 | email: {},
|
37 | seed: {
|
38 | msg: 'Input 2FA seed'
|
39 | }
|
40 | };
|
41 |
|
42 | log = (function(_this) {
|
43 | return function() {
|
44 | if (DEBUG) {
|
45 | return console.log.apply(_this, arguments);
|
46 | }
|
47 | };
|
48 | })(this);
|
49 |
|
50 | toJson = function(obj) {
|
51 | return JSON.stringify(obj, null, ' ');
|
52 | };
|
53 |
|
54 | logJson = function(obj) {
|
55 | return log(toJson(obj));
|
56 | };
|
57 |
|
58 | getHomeDir = function() {
|
59 | return process.env.HOME || process.env.USERPROFILE;
|
60 | };
|
61 |
|
62 | getConfigPath = function(name) {
|
63 | if (name == null) {
|
64 | name = 'savepass';
|
65 | }
|
66 | return path.join(getHomeDir(), "/.config/" + name + "/config.json");
|
67 | };
|
68 |
|
69 | getOutputDir = function() {
|
70 | return path.resolve(!config.path ? OUTPUTS_DIR : config.path.replace(/^~/, getHomeDir()));
|
71 | };
|
72 |
|
73 | getQuestionFor = function(fieldType) {
|
74 | var base, ref, ref1;
|
75 | base = AVAILABLE_FIELDS[fieldType];
|
76 | return {
|
77 | type: (ref = base.type) != null ? ref : 'input',
|
78 | name: fieldType,
|
79 | message: (ref1 = base.msg) != null ? ref1 : 'Input your ' + fieldType
|
80 | };
|
81 | };
|
82 |
|
83 | getChoicesFrom = function(templates) {
|
84 | return templates.map(function(v, i) {
|
85 | return {
|
86 | name: v.name,
|
87 | value: i
|
88 | };
|
89 | });
|
90 | };
|
91 |
|
92 | getFrom = function(templates) {
|
93 | return function(i) {
|
94 | return templates[i];
|
95 | };
|
96 | };
|
97 |
|
98 | getConfig = function(name, cb) {
|
99 | var ref;
|
100 | if (name == null) {
|
101 | name = 'keybase';
|
102 | }
|
103 | if (typeof name === 'function') {
|
104 | ref = ['savepass', name], name = ref[0], cb = ref[1];
|
105 | } else if (name === 'self') {
|
106 | name = 'savepass';
|
107 | }
|
108 | return fs.readJson(getConfigPath(name), cb);
|
109 | };
|
110 |
|
111 | config = {};
|
112 |
|
113 | readOwnConfig = function(next) {
|
114 | return getConfig(function(err, data) {
|
115 | if (err && err.code === 'ENOENT') {
|
116 | log('creating config file...');
|
117 | config = DEFAULT_CONFIG;
|
118 | log('config (new):', toJson(config));
|
119 | return fs.outputJson(err.path, config, next);
|
120 | } else {
|
121 | config = data;
|
122 | log('config (file):', toJson(config));
|
123 | return next();
|
124 | }
|
125 | });
|
126 | };
|
127 |
|
128 | templates = [];
|
129 |
|
130 | filterTemplates = function(filter) {
|
131 | return templates.filter(function(t) {
|
132 | return -1 < t.fileName.indexOf(filter);
|
133 | });
|
134 | };
|
135 |
|
136 | readTemplates = function(next) {
|
137 | var readTemplate;
|
138 | readTemplate = function(template, next) {
|
139 | var tempPath;
|
140 | tempPath = path.resolve(TEMPLATES_DIR, template);
|
141 | return fs.readFile(tempPath, {
|
142 | encoding: 'utf8'
|
143 | }, function(err, fileContent) {
|
144 | var af, m, re;
|
145 | if (err) {
|
146 | next(err);
|
147 | return;
|
148 | }
|
149 | re = new RegExp('<(' + ((function() {
|
150 | var results;
|
151 | results = [];
|
152 | for (af in AVAILABLE_FIELDS) {
|
153 | results.push(af);
|
154 | }
|
155 | return results;
|
156 | })()).join('|') + ')>', 'g');
|
157 | templates.push({
|
158 | file: tempPath,
|
159 | fileName: template,
|
160 | fileContent: fileContent,
|
161 | fields: (function() {
|
162 | var results;
|
163 | results = [];
|
164 | while (m = re.exec(fileContent)) {
|
165 | results.push(m[1]);
|
166 | }
|
167 | return results;
|
168 | })(),
|
169 | name: template.replace(/\.temp$/, '').replace(/-/g, ' ').split(' ').map(function(word) {
|
170 | return word.charAt(0).toUpperCase() + word.substr(1);
|
171 | }).join(' ')
|
172 | });
|
173 | return next();
|
174 | });
|
175 | };
|
176 | return fs.readdir(TEMPLATES_DIR, function(err, files) {
|
177 | if (err) {
|
178 | console.error('not even directory for templates exist.', toJson(err));
|
179 | return;
|
180 | }
|
181 | return async.each(files, readTemplate, function(err) {
|
182 | if (err) {
|
183 | console.error('reading template file failed... apparently', err);
|
184 | return;
|
185 | }
|
186 | log(templates.length + " templates read:", templates.map(function(t) {
|
187 | return t.fileName;
|
188 | }));
|
189 | return next();
|
190 | });
|
191 | });
|
192 | };
|
193 |
|
194 | cli = meow({
|
195 | help: [
|
196 | 'Usage: ' + chalk.bold('savepass <command>'), '', 'where <command> is one of:', ' add, new, list, ls,', ' remove*, rm*, get*', '', 'Example Usage:', ' savepass add [OPTIONAL <flags>]', ' savepass ls', '', 'Available ' + chalk.bold('add|new') + ' subcomand flags:', ' ' + chalk.bold('--template') + '=<templateName>', ' Specify template name to be used. Available templates can be', ' found in `templates/` folder.', ' ' + chalk.bold('--keybase-user') + '=<keybaseUsername>', ' Encrypt output file for a different user then the one logged in.', ' ' + ((function() {
|
197 | var results;
|
198 | results = [];
|
199 | for (f in AVAILABLE_FIELDS) {
|
200 | results.push(chalk.bold("--" + f));
|
201 | }
|
202 | return results;
|
203 | })()).join(', '), ' Using those flags you can pass values, to be filled into a', ' template, directly from CLI. All flags accept strings or "null" to disable.', ' ' + chalk.bold('Flag --password can only be set to null'), '', 'Specify configs in the json-formatted file:', ' ' + getConfigPath()
|
204 | ].join('\n')
|
205 | });
|
206 |
|
207 | log('cli flags:', cli.input, toJson(cli.flags));
|
208 |
|
209 | async.parallel([readTemplates, readOwnConfig], function(err) {
|
210 | var matchingTemplates, step1, step2, step3, step4, step5, step6;
|
211 | if (err) {
|
212 | console.error(err);
|
213 | return;
|
214 | }
|
215 | switch (cli.input[0]) {
|
216 | case 'add':
|
217 | case 'new':
|
218 | case void 0:
|
219 | step6 = function(path, content) {
|
220 | console.log('saving...');
|
221 | return fs.outputFile(path, content, function(err) {
|
222 | if (err) {
|
223 | return console.error(err);
|
224 | }
|
225 | return console.log('success!');
|
226 | });
|
227 | };
|
228 | step5 = function(fileName, contents) {
|
229 | var filePath, q, qs;
|
230 | log('file name:', fileName);
|
231 | q = {
|
232 | name: 'fileName',
|
233 | message: 'How do you want to name the file?'
|
234 | };
|
235 | if (fileName) {
|
236 | q["default"] = fileName.replace('.', '-');
|
237 | }
|
238 | filePath = null;
|
239 | qs = [
|
240 | q, {
|
241 | name: 'confirm',
|
242 | type: 'confirm',
|
243 | message: function(prevAnswer) {
|
244 | var outputDir;
|
245 | outputDir = getOutputDir();
|
246 | log('Output files dir:', outputDir);
|
247 | filePath = path.resolve(outputDir, prevAnswer.fileName + (config.extensions || DEFAULT_CONFIG.extensions));
|
248 | return ['File will be saved as:', ' ' + filePath].join('\n');
|
249 | }
|
250 | }
|
251 | ];
|
252 | return inquirer.prompt(qs, function(answers) {
|
253 | if (!answers.confirm) {
|
254 | step5(answers.fileName, contents);
|
255 | return;
|
256 | }
|
257 | return step6(filePath, contents);
|
258 | });
|
259 | };
|
260 | step4 = function(name, text) {
|
261 | var getKeybaseUser;
|
262 | getKeybaseUser = function(cb) {
|
263 | if (cli.flags.keybaseUser) {
|
264 | cb(cli.flags.keybaseUser);
|
265 | return;
|
266 | }
|
267 | return getConfig('keybase', function(err, data) {
|
268 | return cb(data.user.name);
|
269 | });
|
270 | };
|
271 | return getKeybaseUser(function(keybaseUser) {
|
272 | console.log("Encrypting and signing for: " + keybaseUser);
|
273 | return exec(['keybase encrypt', '-s', "-m '" + text + "'", keybaseUser].join(' '), function(err, stdout, stderr) {
|
274 | if (err || stderr) {
|
275 | if (err) {
|
276 | console.error(err);
|
277 | }
|
278 | if (stderr) {
|
279 | console.error(stderr);
|
280 | }
|
281 | return;
|
282 | }
|
283 | log('encrypted file:\n', stdout);
|
284 | return step5(name, stdout);
|
285 | });
|
286 | });
|
287 | };
|
288 | step3 = function(template, data) {
|
289 | var key, val;
|
290 | log('Template and data before merge:\n', template, toJson(data));
|
291 | for (key in data) {
|
292 | val = data[key];
|
293 | template = template.replace("<" + key + ">", val != null ? val : '');
|
294 | }
|
295 | return inquirer.prompt({
|
296 | type: 'confirm',
|
297 | name: 'proceed',
|
298 | message: ['The following content will be encrypted and saved:', '', template, 'Do you want to proceed?'].join('\n')
|
299 | }, function(answers) {
|
300 | if (answers.proceed) {
|
301 | return step4(data.website, template);
|
302 | }
|
303 | });
|
304 | };
|
305 | step2 = function(chosenTemplate) {
|
306 | var encryptables, field, j, len, questions, ref, ref1, ref2;
|
307 | log('chosen template:', toJson(chosenTemplate));
|
308 | questions = [getQuestionFor('password')];
|
309 | encryptables = {
|
310 | password: null
|
311 | };
|
312 | ref = chosenTemplate.fields;
|
313 | for (j = 0, len = ref.length; j < len; j++) {
|
314 | field = ref[j];
|
315 | if (!(field !== 'password')) {
|
316 | continue;
|
317 | }
|
318 | encryptables[field] = (ref1 = cli.flags[field]) != null ? ref1 : null;
|
319 | if (!encryptables[field]) {
|
320 | questions.push(getQuestionFor(field));
|
321 | } else if ((ref2 = encryptables[field]) === 'null' || ref2 === 'false') {
|
322 | encryptables[field] = null;
|
323 | }
|
324 | }
|
325 | log('encryptables (CLI)', toJson(encryptables));
|
326 | log('questions', toJson(questions));
|
327 | return inquirer.prompt(questions, function(answers) {
|
328 | var a;
|
329 | log('answers', toJson(answers));
|
330 | for (a in encryptables) {
|
331 | if (answers[a] && answers[a] !== '') {
|
332 | encryptables[a] = answers[a];
|
333 | }
|
334 | }
|
335 | log('encryptables (ALL)', toJson(encryptables));
|
336 | return step3(chosenTemplate.fileContent, encryptables);
|
337 | });
|
338 | };
|
339 | step1 = function(templates) {
|
340 | return inquirer.prompt({
|
341 | type: 'list',
|
342 | name: 'type',
|
343 | message: 'Which template do you want to use?',
|
344 | choices: getChoicesFrom(templates),
|
345 | filter: getFrom(templates)
|
346 | }, function(answer) {
|
347 | return step2(answer.type);
|
348 | });
|
349 | };
|
350 | matchingTemplates = filterTemplates(cli.flags.template);
|
351 | switch (matchingTemplates.length) {
|
352 | case 0:
|
353 | return step1(templates);
|
354 | case 1:
|
355 | return step2(matchingTemplates[0]);
|
356 | default:
|
357 | return step1(matchingTemplates);
|
358 | }
|
359 | break;
|
360 | case 'list':
|
361 | case 'ls':
|
362 | log('ls');
|
363 | return fs.readdir(getOutputDir(), function(err, files) {
|
364 | var fileList;
|
365 | if (err) {
|
366 | console.error(err);
|
367 | return;
|
368 | }
|
369 | fileList = files.filter(function(fileName) {
|
370 | return /\.enc\.txt$/.test(fileName);
|
371 | }).map(function(fileName) {
|
372 | return ['', chalk.green('*'), chalk.bold(fileName.replace(/\.enc\.txt$/, '').replace(/[-_]/g, ' ')), chalk.dim(" (" + fileName + ")")].join(' ');
|
373 | }).join('\n');
|
374 | console.log("Your encrypted files: (from " + (chalk.bold(getOutputDir())) + ")\n");
|
375 | return console.log(fileList);
|
376 | });
|
377 | }
|
378 | });
|
379 |
|
380 |
|
381 |
|
382 |
|
383 |
|
384 |
|
385 |
|
386 |
|
387 |
|
388 |
|
389 |
|
390 |
|
391 |
|
392 |
|
393 |
|
394 |
|
395 |
|
396 |
|
397 |
|