Introduction
Introduction Statistics Contact Development Disclaimer Help
pee.scm - pee - Pee a password manager;Pee - because you have to...
git clone git://vernunftzentrum.de/pee.git
Log
Files
Refs
LICENSE
---
pee.scm (20918B)
---
1 ;; Pee - A password manager for the command line
2 ;;
3 ;; Copyright (c) 2016 Christian Kellermann <[email protected]>
4 ;;
5 ;; Permission to use, copy, modify, and distribute this software for any
6 ;; purpose with or without fee is hereby granted, provided that the above
7 ;; copyright notice and this permission notice appear in all copies.
8 ;;
9 ;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANT…
10 ;; WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 ;; MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE F…
12 ;; ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 ;; WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 ;; ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT …
15 ;; OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16
17 (include "crypto-helper.scm")
18
19 (module pee (main)
20 (import
21 scheme
22 (chicken base)
23 (chicken condition)
24 (chicken bitwise)
25 (chicken io)
26 (chicken port)
27 (chicken random)
28 (chicken format)
29 (chicken string)
30 (chicken file)
31 (chicken pretty-print)
32 (chicken irregex)
33 (chicken time)
34 (chicken pathname)
35 (chicken process-context)
36 (chicken sort)
37 (chicken time posix))
38
39 (import
40 srfi-1
41 srfi-4
42 srfi-13
43 srfi-14
44 fmt
45 matchable
46 tweetnacl
47 getopt-long
48 stty
49 crypto-helper)
50
51 (include "program-meta.scm")
52
53 (define-constant password-chars "abcdefhijklmnopqrstuvwxyzABCDEFGHIJKLMN…
54
55 (include "names.scm")
56
57 (define password-modes
58 '(("all chars" . "")
59 ("alpha-numeric" . "!@#$%^&*()-=~?/\|+,:.<>{}[]")
60 ("easy-to-read" . "l1o0I|!ji")
61 ("some-funny-chars" . "|\\[]{}<>~&")))
62
63 (define (entropy-per-char password-chars)
64 (inexact->exact (floor (* (/ (log (string-length password-chars)) (log…
65
66 (define wanted-entropy (* 20 8))
67
68 (define (chars-for-mode chars mode)
69 (char-set->string
70 (char-set-difference
71 (string->char-set chars)
72 (string->char-set mode))))
73
74 (define (generate-new-password wanted-entropy mode)
75 (define (new-indices count)
76 (let ((password-chars (chars-for-mode password-chars mode)))
77 (let loop ((len count)
78 (idx '()))
79 (clear-line)
80 (printf "~a/~a random bytes recieved.~!" (- count len) count)
81 (cond ((zero? len) (clear-line) idx)
82 (else
83 (let ((new (filter (lambda (n)
84 (< n (string-length password-chars)))
85 (u8vector->list (random-bytes len)))))
86 (loop (- len (length new)) (append new idx))))))))
87 (list->string (map (lambda (i)
88 (string-ref password-chars i))
89 (new-indices (inexact->exact
90 (round (/ (* 100 wanted-entropy) (ent…
91
92 (define (prompt-for msg #!optional default)
93 (if default (printf "~a [~a]: " msg default)
94 (printf "~a: " msg))
95 (let ((l (read-line)))
96 (if (and default (equal? "" l))
97 default
98 l)))
99
100 (define (ask-for-choice msg . options)
101 (with-stty
102 '(not echo icanon opost)
103 (lambda ()
104 (let loop ()
105 (clear-line)
106 (printf "~a [~a]: " msg (apply string-append options))
107 (let ((answer (string (read-char))))
108 (cond ((member answer options) =>
109 (lambda (c)
110 (clear-line)
111 (car (string->list (car c)))))
112 (else (loop))))))))
113
114 (define (clear-line)
115 (printf "\r~a" (string #\escape #\[ #\K)))
116
117 (define (ask-yes-or-no msg)
118 (eqv? #\y (ask-for-choice msg "y" "n")))
119
120 (define (random-username)
121 (let* ((first-random (random-bytes 2))
122 (last-random (random-bytes 2))
123 (->number (lambda (u8v)
124 (bitwise-ior (u8vector-ref u8v 1)
125 (arithmetic-shift (u8vector-ref u8v 1)…
126 (first-index (->number first-random))
127 (last-index (->number last-random))
128 (number-first-names (car (alist-ref 'sizes names)))
129 (number-last-names (cadr (alist-ref 'sizes names)))
130 (first-name (list-ref (alist-ref 'first names)
131 (modulo first-index number-first-names)))
132 (last-name (list-ref (alist-ref 'last names)
133 (modulo last-index number-last-names)))
134 (number (->string (u8vector-ref (random-bytes 1) 0))))
135 (string-titlecase (string-append first-name "_" last-name number))))
136
137 (define (new-password)
138 (define (ask-for-manual-password)
139 (with-stty
140 '(not echo)
141 (lambda ()
142 (printf "Enter new password: ")
143 (let ((l (read-line)))
144 (print "\r")
145 l))))
146 (let manual-loop ()
147 (if (ask-yes-or-no "Invent your own password?")
148 (let ((p1 (ask-for-manual-password))
149 (p2 (ask-for-manual-password)))
150 (unless (equal? p1 p2) (print "Passwords do not match.") (manu…
151 p1)
152 (let password-loop ((e wanted-entropy)
153 (modes password-modes))
154 (let* ((m (car modes))
155 (p (generate-new-password e (cdr m)))
156 (entropy-delta (cond ((< e 64) 8)
157 ((< e 128) 16)
158 (else 32))))
159 (printf "Mode ~a, Length ~a chars, entropy ~a bits~%"
160 (car m)
161 (string-length p)
162 (quotient (* (string-length p) (entropy-per-char (ch…
163 (print p)
164 (let dialog-loop ()
165 (let ((choice (ask-for-choice "Use this password?" "y" "n"…
166 (case choice
167 ((#\space #\n) (password-loop e modes))
168 ((#\+) (password-loop (+ e entropy-delta) modes))
169 ((#\-) (password-loop (max 32 (- e entropy-delta)) mod…
170 ((#\m) (password-loop e (append (cdr modes) (list m))))
171 ((#\?)
172 (printf "y - accept password~%+ - increase password l…
173 (dialog-loop))
174 (else p)))))))))
175
176 (define (get-hashed-passphrase)
177 (with-stty
178 '(not echo)
179 (lambda ()
180 (display "Enter passphrase: " (current-error-port))
181 (let ((l (read-line)))
182 (newline (current-error-port))
183 (hash-passphrase l)))))
184
185 (define (enc/dec-file content passphrase op)
186 (let ((sbox (op passphrase))
187 (nonce (make-u8vector symmetric-box-noncebytes 0)))
188 (sbox content nonce)))
189
190 (define (decrypt-file file passphrase)
191 (let ((content (with-input-from-file file (lambda () (read-string #f))…
192 (enc/dec-file content passphrase symmetric-unbox)))
193
194 (define (check-content content)
195 (condition-case
196 (with-input-from-string
197 (with-output-to-string
198 (lambda () (pp content)))
199 read)
200 (e () (error "Internal error: Writing of unserialisable object detect…
201
202 (define (encrypt-file file content passphrase)
203 (check-content content)
204 (let ((cyphertext (enc/dec-file
205 (with-output-to-string (lambda () (pp content)))
206 passphrase
207 symmetric-box)))
208 (unless cyphertext
209 (print "Error: cannot encrypt password store.")
210 (exit 1))
211 (with-output-to-file file
212 (lambda () (display cyphertext)))))
213
214 (define (db-keys alist) (map car alist))
215
216 (define (update-db db key #!key user password comment)
217 (let ((entry (or (alist-ref key db equal?) (make-list 3 ""))))
218 (alist-update key
219 (match-let (((u p c) entry))
220 (list
221 (or user u)
222 (or password p)
223 (or comment c)))
224 db
225 equal?)))
226
227 (define (print-entries entries #!key show-password (show-headers #t) (pr…
228 (let ((users (map first entries))
229 (passwords (if show-password
230 (map second entries)
231 (make-list (length entries) "***")))
232 (comments (map third entries))
233 (dates (map (lambda (e)
234 (time->string (seconds->local-time (inexact->exact…
235 entries)))
236 (fmt #t
237 (tabular
238 (cat (if show-headers (cat "Label" nl) "") (fmt-join dsp prefi…
239 (cat (if show-headers (cat "Username" nl) "") (fmt-join dsp us…
240 (cat (if show-headers (cat "Passwords" nl) "") (fmt-join dsp p…
241 (cat (if show-headers (cat "Comments" nl) "") (fmt-join dsp co…
242 (cat (if show-headers (cat "Last modified" nl) "") (fmt-join d…
243
244
245 (define (check-access f)
246 (and (file-exists? f)
247 (file-readable? f)
248 (file-writable? f)))
249
250 (define options
251 `((init
252 "Initialise password store"
253 (required #f)
254 (value #f)
255 (single-char #\i))
256 (add
257 "Add a new entry to the password store"
258 (required #f)
259 (value (required ACCOUNT)
260 (predicate ,string?))
261 (single-char #\a))
262 (password
263 "Get the password for a given entry"
264 (required #f)
265 (value (required ACCOUNT)
266 (predicate ,string?))
267 (single-char #\p))
268 (update
269 "Change an existing entry in the database"
270 (required #f)
271 (value (required ACCOUNT)
272 (predicate ,string?))
273 (single-char #\u))
274 (delete
275 "Drop an entry from the database"
276 (required #f)
277 (value (required ACCOUNT)
278 (predicate ,string?))
279 (single-char #\d))
280 (list
281 "Shows all info for an entry. Does not show the password."
282 (required #f)
283 (value (optional ACCOUNT)
284 (predicate ,string?))
285 (single-char #\l))
286 (change-passphrase
287 "Reencrypts the store with a new passphrase. Use it regularily."
288 (required #f)
289 (value #f)
290 (single-char #\c))
291 (database-file
292 "Use FILE as the database"
293 (required #f)
294 (single-char #\f)
295 (value (required FILE)))
296 (merge
297 "Merge their DB file into our DB file"
298 (required #f)
299 (single-char #\m)
300 (value (required THEIRS)
301 (predicate ,check-access)))
302 (export
303 "Prints the database as an association list (s-expression) on stdou…
304 (required #f)
305 (value #f))
306 (import
307 "Reads an association list from FILE, the inverse of export."
308 (required #f)
309 (value (required FILE)
310 (predicate ,check-access)))
311 (check-age
312 "Checks the age of the passwords to remind you of changing it"
313 (required #f)
314 (value (required DAYS)
315 (predicate ,(lambda (o) (number? (string->number o))))))
316 (version
317 "Print program version"
318 (required #f)
319 (single-char #\v)
320 (value #f))
321 (help
322 "Prints this help"
323 (required #f)
324 (single-char #\h)
325 (value #f))))
326
327 (define (banner)
328 (printf "~a Version ~a (~a) -- ~a~%" program-name-string program-versi…
329
330 (define (do-add db-name db p e)
331 (when (alist-ref e db equal?)
332 (print "Error: An entry for '" e "' already exists.")
333 (exit 1))
334 (let ((user (prompt-for "Username" (random-username)))
335 (password (new-password))
336 (comment (prompt-for "Comment")))
337 (encrypt-file db-name
338 (cons (list e user password comment (current-seconds))…
339 p)
340 (print "Entry for " e " added.")))
341
342 (define (do-update db-name db p account)
343 (cond ((alist-ref account db equal?) =>
344 (lambda (e)
345 (let ((user (prompt-for "User" (first e)))
346 (comment (prompt-for "Comment" (third e)))
347 (password (if (ask-yes-or-no "Change password?")
348 (new-password)
349 (second e))))
350 (encrypt-file db-name
351 (alist-update account (list user password com…
352 p)
353 (print "Entry '" account "' has been updated."))))
354 (else (fprintf (current-error-port) "Error: Entry for '~a' not f…
355 (exit 1))))
356
357 (define (do-delete db-name db p account)
358 (cond ((alist-ref account db equal?) =>
359 (lambda (e)
360 (print-entries (list e) prefixes: (list account) show-header:…
361 (cond ((ask-yes-or-no "Really delete account?")
362 (encrypt-file db-name (alist-delete account db equal?…
363 (print "Entry '" account "' deleted."))
364 (else (print "Nothing done.")))))
365 (else (fprintf (current-error-port) "Error: Entry for '~a' not f…
366 (exit 1))))
367
368 (define (do-list db account)
369 (let* ((regex (string->irregex (if (string? account)
370 (string-append ".*" account ".*")
371 ".*")
372 'i 'utf8))
373 (accounts
374 (sort (filter (lambda (k)
375 (irregex-match regex k))
376 (db-keys db))
377 string<=)))
378 (cond ((null? (db-keys db))
379 (print "Error: No keys in password store")
380 (exit 1))
381 ((null? accounts)
382 (print "Error: No entry for " account " found.")
383 (exit 1))
384 (else
385 (print-entries (map
386 (lambda (account-name)
387 (alist-ref account-name db equal?))
388 accounts)
389 prefixes: accounts)))))
390
391 (define (do-password db e)
392 (cond ((alist-ref e db equal?) =>
393 (lambda (e)
394 (display (second e))
395 (when (terminal-port? (current-output-port))
396 (newline))))
397 (else
398 (fprintf (current-error-port) "Error: password for '~a' not fou…
399 (exit 1))))
400
401 (define (do-init db-name content)
402 (define (really-init)
403 (print "I will ask you twice for the passphrase to encrypt the passw…
404 (let ((passphrase1 (get-hashed-passphrase))
405 (passphrase2 (get-hashed-passphrase)))
406 (unless (equal? passphrase1 passphrase2)
407 (print "Error: Passphrases do not match.")
408 (exit 1))
409 (encrypt-file db-name content passphrase1)
410 (print "Password store " db-name " initialised.")))
411 (cond ((and (check-access db-name)
412 (ask-yes-or-no (sprintf "~a does exist, do you want to OVE…
413 (really-init))
414 ((not (check-access db-name))
415 (really-init))
416 (else
417 (print "Nothing done."))))
418
419 (define (do-change-passphrase db-name db old-passphrase)
420 (print "I will ask you twice for the new passphrase.")
421 (let ((passphrase1 (get-hashed-passphrase))
422 (passphrase2 (get-hashed-passphrase)))
423 (cond ((not (equal? passphrase1 passphrase2))
424 (print "Error: Passphrases do not match.")
425 (exit 1))
426 ((equal? passphrase1 old-passphrase)
427 (print "Error: Passphrase is the same as old passphrase")
428 (exit 1))
429 (else (encrypt-file db-name db passphrase1)
430 (print "Password store " db-name " reencrypted.")))))
431
432 (define (do-merge db-name db passphrase theirs)
433 (define (merge-entries account mine theirs)
434 (let ((show-password? (ask-yes-or-no "Show passwords?")))
435 (print "Account " account)
436 (let dialog-loop ()
437 (print-entries (list mine theirs) prefixes: '("MINE" "THEIRS") s…
438 (unless (or (equal? (second mine) (second theirs)) show-password…
439 (print "Password MISMATCH"))
440 (let ((choice (ask-for-choice "Use which version?" "m" "t" "s" "…
441 (case choice
442 ((#\m) (print "Taking my version") mine)
443 ((#\t) (print "Taking their version") theirs)
444 ((#\s) (print "Skipping " account " keeping ours.") mine)
445 ((#\?) (printf "m\ttake my version.~%t\ttake their version~%…
446 (dialog-loop))
447 (else (dialog-loop)))))))
448 (print "Enter passphrase for db file " theirs)
449 (let* ((passphrase-theirs (get-hashed-passphrase))
450 (db-theirs (or (with-input-from-string
451 (or (decrypt-file theirs passphrase-theirs) "#f…
452 (begin (fprintf (current-error-port) "Error whil…
453 (cond ((equal? db db-theirs)
454 (print "Databases are the same."))
455 (else
456 (encrypt-file
457 db-name
458 (fold
459 (lambda (entry db)
460 (let ((account (car entry))
461 (theirs (cdr entry)))
462 (cond ((equal? theirs (alist-ref account db equal?)) db)
463 ((alist-ref account db equal?) =>
464 (lambda (ours)
465 (let ((new (merge-entries account ours theirs)…
466 (alist-update account new db equal?))))
467 (else
468 (print-entries (list theirs) prefixes: (list (sp…
469 (alist-cons account theirs db)))))
470 db
471 db-theirs)
472 passphrase)))))
473
474 (define (do-age-check db days)
475 (define (expires-in-seconds entry)
476 (+ (fifth entry)
477 (* 60 60 24 days)))
478 (let* ((now (current-seconds))
479 (old-passwords
480 (filter
481 (lambda (p)
482 (< (expires-in-seconds p) now))
483 db)))
484 (if (pair? old-passwords)
485 (begin
486 (print "These passwords are older than " days " days.")
487 (do-list old-passwords 'all))
488 (print "Your passwords are younger than " days " days."))))
489
490 (define (main args)
491 (set-buffering-mode! (current-output-port) #:none)
492 (when (null? args)
493 (banner) (print (usage options)) (exit 1))
494 (let* ((opts
495 (condition-case
496 (getopt-long args options)
497 (e (exn)
498 (print "Error: "
499 ((condition-property-accessor 'exn 'arguments) e) " "
500 ((condition-property-accessor 'exn 'message) e))
501 (banner)
502 (print (usage options))
503 (exit 1))))
504 (db-name (or (alist-ref 'database-file opts)
505 (make-pathname (get-environment-variable "HOME") "…
506 (init (alist-ref 'init opts)))
507
508 (unless (null? (alist-ref '@ opts equal?))
509 (fprintf (current-error-port) "Warning: superfluous option g…
510 (fprintf (current-error-port) "Using database file ~a~%" db-name)
511 (unless (or init (check-access db-name))
512 (print "Error database " db-name " does not exist or has wro…
513 (cond
514 ((alist-ref 'help opts) (banner) (print (usage options)))
515 ((alist-ref 'version opts) (banner))
516 ((alist-ref 'import opts) => (lambda (f)
517 (do-init db-name (with-input-from-fi…
518
519 (init (do-init db-name '()))
520 (else
521 (let* ((passphrase (get-hashed-passphrase))
522 (db (or (with-input-from-string
523 (or (decrypt-file db-name passphrase) "#f") rea…
524 (begin (fprintf (current-error-port) "Error while d…
525 (cond
526 ((alist-ref 'change-passphrase opts)
527 (do-change-passphrase db-name db passphrase))
528 ((alist-ref 'add opts) => (lambda (e) (do-add db-name db passph…
529 ((alist-ref 'list opts) => (lambda (e) (do-list db e)))
530 ((alist-ref 'export opts) (pp db))
531 ((alist-ref 'delete opts) => (lambda (e) (do-delete db-name db …
532 ((alist-ref 'update opts) => (lambda (e) (do-update db-name db …
533 ((alist-ref 'password opts) => (lambda (e) (do-password db e)))
534 ((alist-ref 'merge opts) => (lambda (theirs) (do-merge db-name …
535 ((alist-ref 'check-age opts) => (lambda (days) (do-age-check db…
536 (else (banner) (print "Error: Don't know what to do") (print (u…
537 (exit 0)))
538 )
539
540 (import pee (chicken process-context))
541 (main (cdr (argv)))
You are viewing proxied material from vernunftzentrum.de. The copyright of proxied material belongs to its original authors. Any comments or complaints in relation to proxied material should be directed to the original authors of the content concerned. Please see the disclaimer for more details.