UNPKG

9.47 kBtext/coffeescriptView Raw
1#!/usr/bin/env coffee
2'use strict'
3
4inquirer = require 'inquirer'
5{exec} = require 'child_process'
6async = require 'async'
7chalk = require 'chalk'
8meow = require 'meow'
9path = require 'path'
10fs = require 'fs-extra'
11
12
13#
14# constants
15#
16DEBUG = no
17
18TEMPLATES_DIR = 'templates'
19OUTPUTS_DIR = 'outputs'
20DEFAULT_CONFIG =
21 path: OUTPUTS_DIR
22 extensions: '.enc.txt'
23
24AVAILABLE_FIELDS =
25 name: {}
26 website: {}
27 login: {}
28 password: type: 'password'
29 email: {}
30 seed: msg: 'Input 2FA seed'
31
32#
33# log helpers
34#
35log = => console.log.apply @, arguments if DEBUG
36toJson = (obj) -> JSON.stringify obj, null, ' '
37logJson = (obj) -> log toJson obj
38
39#
40# utilities
41#
42getHomeDir = -> process.env.HOME or process.env.USERPROFILE
43getConfigPath = (name = 'savepass') -> path.join getHomeDir(), "/.config/#{name}/config.json"
44getOutputDir = ->
45 path.resolve unless config.path then OUTPUTS_DIR else config.path.replace /^~/, getHomeDir()
46
47
48getQuestionFor = (fieldType) ->
49 base = AVAILABLE_FIELDS[fieldType]
50
51 type: base.type ? 'input'
52 name: fieldType
53 message: base.msg ? 'Input your ' + fieldType
54
55getChoicesFrom = (templates) ->
56 templates.map (v, i) ->
57 name: v.name
58 value: i
59
60getFrom = (templates) -> (i) -> templates[i]
61
62getConfig = (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
72config = {}
73readOwnConfig = (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
89templates = []
90
91filterTemplates = (filter) ->
92 templates.filter (t) -> -1 < t.fileName.indexOf filter
93
94# read templates from `templates` dir
95readTemplates = (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
135cli = 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
162log 'cli flags:', cli.input, toJson cli.flags
163
164
165async.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 # AKA save to a file
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 # AKA get file name and path
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 # AKA keybase encrypt and sign
225 # NOTE: that step requires internet!
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' # encrypt the message
239 '-s' # but also sign
240 "-m '#{text}'" # pass the message
241 keybaseUser # use my public key
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 # AKA fill template with data
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 # AKA get ALL THE DATA
274 step2 = (chosenTemplate) ->
275 log 'chosen template:', toJson chosenTemplate
276
277 # always ask for password - ignore CLI flag
278 questions = [
279 getQuestionFor 'password'
280 ]
281 encryptables =
282 password: null
283
284 # check for CLI flags
285 for field in chosenTemplate.fields when field isnt 'password'
286 encryptables[field] = cli.flags[field] ? null
287
288 # if field isn't provided via CLI - ask for it
289 unless encryptables[field]
290 questions.push getQuestionFor field
291
292 # ignore fields explicitly disabled
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 # ask for still missing data
300 inquirer.prompt questions, (answers) ->
301 log 'answers', toJson answers
302
303 # merge CLI and prompts
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 # AKA choose template
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 # step0
325 matchingTemplates = filterTemplates cli.flags.template
326 switch matchingTemplates.length
327 when 0 then step1 templates # no `--templates=?` flag passed
328 when 1 then step2 matchingTemplates[0] # exactly one match
329 else step1 matchingTemplates # more than one match
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 file
361 login
362 email
363 password
364 website
365 2FA
366 rule (desc)
367 just password(s)
368
369 creds|credentials
370 one
371 many|multi
372 file
373 desc
374
375###
\No newline at end of file