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