123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332 |
- #!/usr/bin/env bash
- # -*- wisp -*-
- exec -a "$0" guile -L $(dirname $(realpath "$0")) -e '(securepassword)' -c '' "$@"
- ; !#
- ;; Create secure passwords, usable on US and German keyboards without problems
- (define-module (securepassword)
- #:export (letterblocks-nice main))
- (import
- (only (srfi srfi-27) random-source-make-integers
- make-random-source random-source-randomize!)
- (only (srfi srfi-1) first second third iota)
- (srfi srfi-11 );; let-values
- (srfi srfi-42)
- (ice-9 optargs)
- (ice-9 format)
- (only (ice-9 rdelim) read-line)
- (ice-9 match)
- (ice-9 pretty-print))
- ;; newbase60 without yz_: 54 letters, 5.75 bits of entropy per letter.
- (define qwertysafeletters "0123456789ABCDEFGHJKLMNPQRTUVWXabcdefghijkmnopqrstuvwx")
- ;; delimiters: 2.3 bits of entropy per delimiter, in the same place on main keys or the num-pad.
- (define delimiters ".+-=")
- (define random-source (make-random-source))
- (random-source-randomize! random-source)
- (define random-integer
- (random-source-make-integers random-source))
- (define (randomletter letters)
- (string-ref letters
- (random-integer
- (string-length letters))))
- (define (letter-index letters letter)
- (string-index letters letter))
- (define (block-value letterblock letters)
- (let loop
- ((rest letterblock)
- (value 0))
- (if (equal? "" rest)
- value
- (loop
- (string-drop rest 1)
- (+ (* (string-length letters) value)
- (letter-index letters (string-ref rest 0)))))))
- (define (checkchar letters delimiters . letterblocks)
- (let*
- ((value (block-value (apply string-append letterblocks) letters))
- (modvalue (string-length delimiters))
- (checksum (modulo value modvalue)))
- (string-ref delimiters checksum)))
- (define (flatten e)
- (cond
- ((pair? e)
- `(
- (,@ flatten (car e ))
- (,@ flatten (cdr e))))
- ((null? e)
- (list))
- (else
- (list e))))
- (define (blocks-to-passphrase blocks)
- (let check
- ((passphrase "")
- (blocks blocks))
- (cond
- ((null? blocks)
- passphrase)
- ((= (length blocks) 1)
- (string-append passphrase (first blocks)))
- (else
- (check
- (string-append passphrase
- (first blocks)
- (string
- (checkchar qwertysafeletters delimiters
- (first blocks )
- (second blocks))))
- (cdr blocks))))))
- (define (single-block)
- (apply string-append
- (map (λ (x) (string (randomletter qwertysafeletters)))
- (iota 4))))
- (define (letterblocks blockcount)
- (let loop
- ((remaining blockcount)
- (blocks '()))
- (if (zero? remaining)
- (blocks-to-passphrase (reverse blocks))
- (loop
- (- remaining 1)
- (cons (single-block) blocks)))))
- (define (letterblock-invalid? password)
- (let loop
- ((rest password)
- (count 5))
- (if (< (string-length rest) 5)
- (values #f #f #f)
- (let*
- ((check (string (string-ref rest 4)))
- (block1 (string-take rest 4))
- (block2
- (string-take (string-drop rest 5)
- (min 4 (- (string-length rest) 5))))
- (calck (string (checkchar qwertysafeletters delimiters block1 block2))))
- (if (not (equal? check calck))
- (values check calck count)
- (loop (string-drop rest 5)
- (+ count 5)))))))
- (define (lines-from-file filepath)
- (let ((port (open-input-file filepath)))
- (let reader ((lines '()))
- (let ((line (read-line port)))
- (if (eof-object? line)
- (reverse! lines)
- (reader (cons line lines)))))))
- (define (split-corpus-line line)
- "turn LINE into '(first-letter second-letter weight)
- A LINE is formatted as cost ab, with cost a number and a and b the letters. For example:
- 10123151.392154863 en
- 0.020499130776997592 q6
- "
- (define (log2 number)
- (/ (log number) (log 2)))
- (let*
- ((space-index (string-index line #\space))
- (weight (log2 (string->number (string-take line space-index))))
- (first-letter (string-ref line (+ space-index 1)))
- (second-letter (string-ref line (+ space-index 2))))
- (list first-letter second-letter weight)))
-
- (define (shift-and-scale-cost line-costs upper-limit)
- "shift the COST to have cost values between zero (log-scale) and the UPPER-LIMIT."
- (let*
- ((numbers (map (λ (x) (third x)) line-costs))
- (minimum (apply min numbers))
- (shifted-up (map (λ (x) (list (first x) (second x) (- (third x) minimum))) line-costs))
- (shifted-numbers (map (λ (x) (third x)) shifted-up))
- (maximum (apply max shifted-numbers))
- (scaling-factor (/ upper-limit maximum)))
- (format #t "Scaling factor: ~a\n" scaling-factor)
- (map (λ(x) (list (first x) (second x) (inexact->exact (floor (* scaling-factor (third x))))))
- shifted-up)))
- (define (collapse-weighting letters cost)
- (define (index-value char)
- (string-index letters char))
- (let ((collapsed (make-string (expt (string-length letters) 2) (string-ref letters 0))))
- (let update-string ((cost cost))
- (if (null? cost)
- collapsed
- (let*
- ((element (car cost))
- (one (first element))
- (two (second element))
- (val (third element))
- (idx (+ (index-value two) (* (index-value one) (string-length letters)))))
- (string-set! collapsed idx val)
- (update-string (cdr cost)))))))
- (define weightletters (string-append qwertysafeletters delimiters " "))
- (define scalingfactor-inverse (/ 1 2.00834859456416))
- (define weight-collapsed "rmjjkkmkjjNTRRPPULMWRWMNFMNRUTJXTXWfWUQMUWdVUVFTeURJQapTiUsommkkmkkmpUTTVQQTTFKPTNPDHRPLRHaXXWXWaUJFUVTLWQXaRNPfUmVjXoqjjjjkijiiQTVVQQUKKRMTHPHNPLUPQWabXaXaQQJRXWUdRUcaPQQbkWgXomiiiiiihiiRPQWNUQJKMVQML8UPJMHQWXWaVWXLQFUaTLLBVWWRFQTjXiWnkhhihihhihNLMMPNNLTMVTHJGUNKKPKTaWTVUVQGURTMRVAQUURGPVjagTnmhhhhhihhhQQPLMTKQHNHVPTRQLPUQJUTXWVXNMMLQWQGQLKaQQHLQiQgNokhhhhhmhhgURNQHKQNFLKNJLCGGRKHKUTXUXTQRJTPTLFQQGVNAULQiRgQojhhhhihhhhMMJRJQPGGFFQGLJHPHFMJUUVWVWJLDFKTQQKKRUPGCJQhPeMnkhhhhhihhiKNLLJLQPKPNQFJCPKHLGGVVVWVXMMQHTRbDP8GUNBKLThPfMmkiijjjjjknPNHJQKJFFQHTEPJGLHGKMVVTUTUGNHELTGAFLRUP9NNQiQfKmLXUVVURNQMcbddbafVUadefdWffaXVXcogiagfeeUikqTiapjjqcaabPdNkPRURQPRQRQdeaWaWaRRXabgUNbUbaVJpLRbsXQUnbHQKnHRnVRoQNMfHbLhRTVUPQPKRLcVcgjVVeTbbaVaTbcVbRPmJLPfTGneHKJNnL6gXQfKMHbWcPiQUTPRRPQKMdXVegeWXWUbbfdWeXecWTrQHbrLLUrVKUUnQ6kcbkVQKbMcUiQQPRQLLMMRdadebabVMWccfcTghddfbdifgUfcgqNahoUdaqnfmfajaMeXgRWTPMTRQPLcabdabUTMUbVRWGaaaTUPoMNHnTNNoRRNTnDJpUTjQLGWFbKfHULMMNMPTQacWUfVacHRaXfgKcabUWHkUDRrRPcjNDdfkJFqWNiPRFaLaThTPVMLPNNMNdUXXdVdWMVbVTVQWfWUUJqNJPpPWJoMEaKnKLcQNkMRBXJVQeDJKFFFEGLRTNPRTNRRVRLNLWPRPQRQJpRMJjPTUbDKHGkTFTTJmFECaCRDaLPNQJMKJHGaVQdbUXTNVXWTfNTVXUXNoPPNmVManQJVhpTJnTPmQUEaQXMfRPQLUNKHFGgWXXeacQMVbUUaKUaWVTNoNRTpPRNpTWEJkL4TWUjdMQaPbQhJRRXXNNLMNeaWbeTWXQTcaRbTUXXVbVrTcRpTQQqKTLVoVQiUPmHNHaJbLgLQULNHHJPKeaceeaeVPaVUdaLXhgXXMpHKQnRRPkMNQQoNQbQXjRNFaKXViMTUTPLLJTHeQcheUeWPVgXWdPcbbTQLoMMUmiFkiUWMUnRQqfVjVNDXNbLgHLGGJLRFCJWJLJVHKDHAWNTHQWVWRWRVDNFN7PEUNJNGPP6ELXmEbAQ7RDaQVRNPKNJJHeVddgXXQMWcbbdLceVaaMnTQRqLTfkLKHQmGLMRPkFMPaKbRhPVTTQNMJMTeVcafbUeUVacXeHceacUToRdToMQqkUNTbnRRoeQhRcgXJaNgXUePNQLKKHXVXWaUaRHVbXeWCdeQUQQMVTaXbWiUMXmqRdEifaNLbNWMdEhPRQLLKPKPFXWcacRaRNQXVbXDVRRWXKjHJRqaVHmKKJJoHJUQPaVMDbEbUgFQNQHQUELEbUTWeVUUTTVcUaLVVQRVPoGKNrLMjpNPNKnMLcURiFQFXWWHfJXLPKNNKKDQLUPUQKMLKUWHUDNXLUFbXTQQWPJMW6MRFVTLQLQQTJQUFXGeUdbXbUVVWTQUTWMVPQGQQQdQQUWDMPFnuusksutrhru.gqbxxxwpkjiRmatVcbXVWVWXVLNQGMTMdKNQNLLDNJBLJXsmfh.gmisifgirfWrqorfiUhQfXpXXbVXTUVTVRNXVTMUJKVVTQbQKTQMLBrbomrec=oatidtjdomqoidbfMhanVdaccacWRWWUUVaTMTJUWRXVTQbVRRDwjgo-jjjxehjmsibrposjmenWkdxXjgedabaWWWkXWdaVbPVVXVXQXaPTWXtttvssuu+gqv prk=.vtrrrqTog-TccWTUUVeWUXVQPRUJMQXTLPQVTNPR9siifusmhscfhkthcsntodienMhavPWaRURRQTWRXMTWTNNFHPURMJQUQQHFrggg+inprdnioogXsrrqfffmTkbwNUWPNMRMPETMNQUTTQMPJTQRNRRQULFwjei+hphvdmrstfXvqwqfpTkQjdxHbbRMHNLHLLcTNNPQTGNRQMVHPQQPTLrr.t+svrheru-upeux.krfkiViVsBMPNNM6C6JQFQFABCFR56KJJNJ6RCNJnaUbqcWUdaaUXjecVaVnWWQULVNeQRVUTKNJRGQVWLTQHRQNLVMPLQUTPWLsgekviihpXmjorfWpospihdjUjWrabbbWVQQTRVMQNTNTQKQRVNQMRKPNQBvodjwjievbjtirsceppqghXjJiVwVabhRJRPVNWVVWaXaTURVTWWNWXTUbLupr+.r.ovhsnvujfmvxsnmesVnd=URbbRLQKeFXRaUWMQTPUTPLQPWbHUPXmqtsktqqnjouxqrbxsttorkjNkUucefbdUQUNRRKLQXRPTKLDJTFRLTDTNFsdfjtpgoqVgigrqbtnqpadbhNjepDRVRHM04L4UQRQQQU3JF6QQ54RQF94MWacafcXXXbRWaXchbbdpWeUcFPRgQhebbaaQRUWWbTTPQUQPPUQVFWRMRWLwrsv+rsqvgsruvndrvwtnpboRme+bcdVKTQPQNVXVVTVVUKNUWQWLRPTRVKtnxk.mprwfpokutfnw.rmofpWng+XdfVXQNNTPaXWXbUXXTRWVUWQWbUQVQumnj-mnwwdknmunbuuutiqkrang+JQUQJKXLBHGMLeLNQLJNKQHbJQTLJKEoptpsurmpdmt.iqXwwvhhhhgJgLtcaecTJWREBHGLLMUQUFDHUDWPKQLWRFpbfdvdiXrVWehucaihddhWXeGjcmRWVbP8fR98MRTVRNTTRMUKDLHTVLRUMucegvdeqvdbdmsdbmkfqXidhTgTpeibXTVWPbWQRNaVTTPTNPNQR4PQDGKPjjkcjjbekbhgdimafnofhdbfUhbmomjiihiggghfajgcccbbaddbNbbddePigjihghgffdgfgkbhjhgchbmQfHvjebXWUTMNQPLQLMMQLNHNXJLCMRFKKKQTPNNRPMPKQUQKQMQVWJNPTNdXgjkmkhheedffjjhihiihgihjfibiifhiakkkiijijihgkihkckmkhigceUqaqacaUUUUTQPNNWQTdTNMKNNfRULdQNQLhWbjXagXWRfifbaTbgeWVcfJJWkrqsronmmjkjuuquttttrtsurtkstrstg.wu+xvvvxrtwvvtmt.xwvxjjnrr=")
- (define (weighting-from-corpusfile filename)
- "this is how the weighting above was calculated"
- (let*
- ((cost (map split-corpus-line (lines-from-file filename)))
- (letters weightletters)
- (shifted (shift-and-scale-cost cost (- (string-length letters) 1))))
- (collapse-weighting letters
- (map (λ (x) (list (first x) (second x) (string-ref letters (third x))))
- shifted))))
- (define (recreate-corpus-from-weighting)
- "Expand the weight string back into a full corpus by using the weightletters"
- (let expander
- ((weightleft weight-collapsed)
- (letter1 weightletters)
- (letter2 weightletters))
- (cond
- ((string-null? weightleft)
- #t)
- ((string-null? letter1)
- (error "could not expand the whole corpus. Letters left: ~a" weightleft))
- ((string-null? letter2)
- (expander weightleft (string-drop letter1 1) weightletters))
- (else
- (let ((cost (expt 2 (* scalingfactor-inverse (string-index weightletters (string-ref weightleft 0))))))
- (format #t "~f ~a~a\n" cost (string-ref letter1 0) (string-ref letter2 0)))
- (expander
- (string-drop weightleft 1)
- letter1
- (string-drop letter2 1))))))
- (define (bigram->weight bigram)
- "Calculate the weight of the given bigram in a corpus"
- (let*
- ((letter1 (string-ref bigram 0))
- (letter2 (string-ref bigram 1))
- (idx1 (string-index weightletters letter1))
- (idx2 (string-index weightletters letter2))
- ;; for downcased bigrams (for phonetics) we might have to get the uppercase version
- (idx1 (if idx1 idx1 (string-index weightletters (char-upcase letter1))))
- (idx2 (if idx2 idx2 (string-index weightletters (char-upcase letter2))))
- (len (string-length weightletters))
- (costchar (string-ref weight-collapsed (+ (* idx1 len) idx2))))
- (expt 2 (* scalingfactor-inverse (string-index weightletters costchar)))))
- (define (word-weight word)
- "calculate the probability weight of the given word to appear in a corpus given by the weight-collapsed"
- (let loop
- ((s (string-append " " word " "))
- (cost 0))
- (cond
- ((string-null? (string-drop s 1))
- cost)
- ((string-null? (string-drop s 2))
- cost)
- (else
- (loop
- (string-drop s 2)
- (+ cost (bigram->weight (string-take s 2))))))))
- (define* (string-replace-substring s substr replacement #:optional (start 0) (end (string-length s)))
- "Replace every instance of substring in s by replacement."
- (let ((substr-length (string-length substr)))
- (if (zero? substr-length)
- (error "string-replace-substring: empty substr")
- (let loop
- ((start start)
- (pieces (list (substring s 0 start))))
- (let ((idx (string-contains s substr start end)))
- (if idx
- (loop (+ idx substr-length)
- (cons* replacement
- (substring s start idx)
- pieces))
- (string-concatenate-reverse
- (cons (substring s start)
- pieces))))))))
- (define* (letterblocks-nice blockcount #:key (best-of 8))
- "Generate BEST-OF letterblocks and return the one most likely to appear in the corpus given by weight-collapsed
- best-of 8 consumes 3 bits of entropy, but creates passwords which are easier to remember. "
- (define (delimiters-to-space s)
- "replace all delimiters by spaces"
- (let replace
- ((s s)
- (delim delimiters))
- (if (string-null? delim)
- s
- (replace
- (string-replace-substring s (string-take delim 1) " ")
- (string-drop delim 1)))))
- (car
- (sort
- (map (λ (x) (letterblocks blockcount))
- (iota best-of))
- (λ (a b)
- (>
- (word-weight (delimiters-to-space (string-downcase a)))
- (word-weight (delimiters-to-space (string-downcase b))))))))
- (define (help args)
- (format #t "Usage: ~a [options]
- Options:
- [<length> [<password-type>]] create password
- --check <password> verify the checksums
- --help show this message
- " (first args)))
- (define (main args)
- (cond
- ((and (> (length args) 1) (equal? "--help" (second args)))
- (help args))
- ((and (> (length args) 2) (equal? "--check" (second args)))
- (let-values (((check calck count) (letterblock-invalid? (third args))))
- (cond
- (count
- (format #t "letterblock invalid. First failed checksum: ~a should have been ~a at position ~a\n"
- check calck count)
- (exit 1))
- (else
- (format #t "valid letterblock password\n")))))
- (else
- (let
- (
- (len
- (if (<= 2 (length args))
- (string->number (second args))
- 12)))
- (display (letterblocks-nice (floor (/ len 4))))
- (newline)))))
|