securepassword.scm 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. #!/usr/bin/env bash
  2. # -*- wisp -*-
  3. exec -a "$0" guile -L $(dirname $(realpath "$0")) -e '(securepassword)' -c '' "$@"
  4. ; !#
  5. ;; Create secure passwords, usable on US and German keyboards without problems
  6. (define-module (securepassword)
  7. #:export (letterblocks-nice main))
  8. (import
  9. (only (srfi srfi-27) random-source-make-integers
  10. make-random-source random-source-randomize!)
  11. (only (srfi srfi-1) first second third iota)
  12. (srfi srfi-11 );; let-values
  13. (srfi srfi-42)
  14. (ice-9 optargs)
  15. (ice-9 format)
  16. (only (ice-9 rdelim) read-line)
  17. (ice-9 match)
  18. (ice-9 pretty-print))
  19. ;; newbase60 without yz_: 54 letters, 5.75 bits of entropy per letter.
  20. (define qwertysafeletters "0123456789ABCDEFGHJKLMNPQRTUVWXabcdefghijkmnopqrstuvwx")
  21. ;; delimiters: 2.3 bits of entropy per delimiter, in the same place on main keys or the num-pad.
  22. (define delimiters ".+-=")
  23. (define random-source (make-random-source))
  24. (random-source-randomize! random-source)
  25. (define random-integer
  26. (random-source-make-integers random-source))
  27. (define (randomletter letters)
  28. (string-ref letters
  29. (random-integer
  30. (string-length letters))))
  31. (define (letter-index letters letter)
  32. (string-index letters letter))
  33. (define (block-value letterblock letters)
  34. (let loop
  35. ((rest letterblock)
  36. (value 0))
  37. (if (equal? "" rest)
  38. value
  39. (loop
  40. (string-drop rest 1)
  41. (+ (* (string-length letters) value)
  42. (letter-index letters (string-ref rest 0)))))))
  43. (define (checkchar letters delimiters . letterblocks)
  44. (let*
  45. ((value (block-value (apply string-append letterblocks) letters))
  46. (modvalue (string-length delimiters))
  47. (checksum (modulo value modvalue)))
  48. (string-ref delimiters checksum)))
  49. (define (flatten e)
  50. (cond
  51. ((pair? e)
  52. `(
  53. (,@ flatten (car e ))
  54. (,@ flatten (cdr e))))
  55. ((null? e)
  56. (list))
  57. (else
  58. (list e))))
  59. (define (blocks-to-passphrase blocks)
  60. (let check
  61. ((passphrase "")
  62. (blocks blocks))
  63. (cond
  64. ((null? blocks)
  65. passphrase)
  66. ((= (length blocks) 1)
  67. (string-append passphrase (first blocks)))
  68. (else
  69. (check
  70. (string-append passphrase
  71. (first blocks)
  72. (string
  73. (checkchar qwertysafeletters delimiters
  74. (first blocks )
  75. (second blocks))))
  76. (cdr blocks))))))
  77. (define (single-block)
  78. (apply string-append
  79. (map (λ (x) (string (randomletter qwertysafeletters)))
  80. (iota 4))))
  81. (define (letterblocks blockcount)
  82. (let loop
  83. ((remaining blockcount)
  84. (blocks '()))
  85. (if (zero? remaining)
  86. (blocks-to-passphrase (reverse blocks))
  87. (loop
  88. (- remaining 1)
  89. (cons (single-block) blocks)))))
  90. (define (letterblock-invalid? password)
  91. (let loop
  92. ((rest password)
  93. (count 5))
  94. (if (< (string-length rest) 5)
  95. (values #f #f #f)
  96. (let*
  97. ((check (string (string-ref rest 4)))
  98. (block1 (string-take rest 4))
  99. (block2
  100. (string-take (string-drop rest 5)
  101. (min 4 (- (string-length rest) 5))))
  102. (calck (string (checkchar qwertysafeletters delimiters block1 block2))))
  103. (if (not (equal? check calck))
  104. (values check calck count)
  105. (loop (string-drop rest 5)
  106. (+ count 5)))))))
  107. (define (lines-from-file filepath)
  108. (let ((port (open-input-file filepath)))
  109. (let reader ((lines '()))
  110. (let ((line (read-line port)))
  111. (if (eof-object? line)
  112. (reverse! lines)
  113. (reader (cons line lines)))))))
  114. (define (split-corpus-line line)
  115. "turn LINE into '(first-letter second-letter weight)
  116. A LINE is formatted as cost ab, with cost a number and a and b the letters. For example:
  117. 10123151.392154863 en
  118. 0.020499130776997592 q6
  119. "
  120. (define (log2 number)
  121. (/ (log number) (log 2)))
  122. (let*
  123. ((space-index (string-index line #\space))
  124. (weight (log2 (string->number (string-take line space-index))))
  125. (first-letter (string-ref line (+ space-index 1)))
  126. (second-letter (string-ref line (+ space-index 2))))
  127. (list first-letter second-letter weight)))
  128. (define (shift-and-scale-cost line-costs upper-limit)
  129. "shift the COST to have cost values between zero (log-scale) and the UPPER-LIMIT."
  130. (let*
  131. ((numbers (map (λ (x) (third x)) line-costs))
  132. (minimum (apply min numbers))
  133. (shifted-up (map (λ (x) (list (first x) (second x) (- (third x) minimum))) line-costs))
  134. (shifted-numbers (map (λ (x) (third x)) shifted-up))
  135. (maximum (apply max shifted-numbers))
  136. (scaling-factor (/ upper-limit maximum)))
  137. (format #t "Scaling factor: ~a\n" scaling-factor)
  138. (map (λ(x) (list (first x) (second x) (inexact->exact (floor (* scaling-factor (third x))))))
  139. shifted-up)))
  140. (define (collapse-weighting letters cost)
  141. (define (index-value char)
  142. (string-index letters char))
  143. (let ((collapsed (make-string (expt (string-length letters) 2) (string-ref letters 0))))
  144. (let update-string ((cost cost))
  145. (if (null? cost)
  146. collapsed
  147. (let*
  148. ((element (car cost))
  149. (one (first element))
  150. (two (second element))
  151. (val (third element))
  152. (idx (+ (index-value two) (* (index-value one) (string-length letters)))))
  153. (string-set! collapsed idx val)
  154. (update-string (cdr cost)))))))
  155. (define weightletters (string-append qwertysafeletters delimiters " "))
  156. (define scalingfactor-inverse (/ 1 2.00834859456416))
  157. (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=")
  158. (define (weighting-from-corpusfile filename)
  159. "this is how the weighting above was calculated"
  160. (let*
  161. ((cost (map split-corpus-line (lines-from-file filename)))
  162. (letters weightletters)
  163. (shifted (shift-and-scale-cost cost (- (string-length letters) 1))))
  164. (collapse-weighting letters
  165. (map (λ (x) (list (first x) (second x) (string-ref letters (third x))))
  166. shifted))))
  167. (define (recreate-corpus-from-weighting)
  168. "Expand the weight string back into a full corpus by using the weightletters"
  169. (let expander
  170. ((weightleft weight-collapsed)
  171. (letter1 weightletters)
  172. (letter2 weightletters))
  173. (cond
  174. ((string-null? weightleft)
  175. #t)
  176. ((string-null? letter1)
  177. (error "could not expand the whole corpus. Letters left: ~a" weightleft))
  178. ((string-null? letter2)
  179. (expander weightleft (string-drop letter1 1) weightletters))
  180. (else
  181. (let ((cost (expt 2 (* scalingfactor-inverse (string-index weightletters (string-ref weightleft 0))))))
  182. (format #t "~f ~a~a\n" cost (string-ref letter1 0) (string-ref letter2 0)))
  183. (expander
  184. (string-drop weightleft 1)
  185. letter1
  186. (string-drop letter2 1))))))
  187. (define (bigram->weight bigram)
  188. "Calculate the weight of the given bigram in a corpus"
  189. (let*
  190. ((letter1 (string-ref bigram 0))
  191. (letter2 (string-ref bigram 1))
  192. (idx1 (string-index weightletters letter1))
  193. (idx2 (string-index weightletters letter2))
  194. ;; for downcased bigrams (for phonetics) we might have to get the uppercase version
  195. (idx1 (if idx1 idx1 (string-index weightletters (char-upcase letter1))))
  196. (idx2 (if idx2 idx2 (string-index weightletters (char-upcase letter2))))
  197. (len (string-length weightletters))
  198. (costchar (string-ref weight-collapsed (+ (* idx1 len) idx2))))
  199. (expt 2 (* scalingfactor-inverse (string-index weightletters costchar)))))
  200. (define (word-weight word)
  201. "calculate the probability weight of the given word to appear in a corpus given by the weight-collapsed"
  202. (let loop
  203. ((s (string-append " " word " "))
  204. (cost 0))
  205. (cond
  206. ((string-null? (string-drop s 1))
  207. cost)
  208. ((string-null? (string-drop s 2))
  209. cost)
  210. (else
  211. (loop
  212. (string-drop s 2)
  213. (+ cost (bigram->weight (string-take s 2))))))))
  214. (define* (string-replace-substring s substr replacement #:optional (start 0) (end (string-length s)))
  215. "Replace every instance of substring in s by replacement."
  216. (let ((substr-length (string-length substr)))
  217. (if (zero? substr-length)
  218. (error "string-replace-substring: empty substr")
  219. (let loop
  220. ((start start)
  221. (pieces (list (substring s 0 start))))
  222. (let ((idx (string-contains s substr start end)))
  223. (if idx
  224. (loop (+ idx substr-length)
  225. (cons* replacement
  226. (substring s start idx)
  227. pieces))
  228. (string-concatenate-reverse
  229. (cons (substring s start)
  230. pieces))))))))
  231. (define* (letterblocks-nice blockcount #:key (best-of 8))
  232. "Generate BEST-OF letterblocks and return the one most likely to appear in the corpus given by weight-collapsed
  233. best-of 8 consumes 3 bits of entropy, but creates passwords which are easier to remember. "
  234. (define (delimiters-to-space s)
  235. "replace all delimiters by spaces"
  236. (let replace
  237. ((s s)
  238. (delim delimiters))
  239. (if (string-null? delim)
  240. s
  241. (replace
  242. (string-replace-substring s (string-take delim 1) " ")
  243. (string-drop delim 1)))))
  244. (car
  245. (sort
  246. (map (λ (x) (letterblocks blockcount))
  247. (iota best-of))
  248. (λ (a b)
  249. (>
  250. (word-weight (delimiters-to-space (string-downcase a)))
  251. (word-weight (delimiters-to-space (string-downcase b))))))))
  252. (define (help args)
  253. (format #t "Usage: ~a [options]
  254. Options:
  255. [<length> [<password-type>]] create password
  256. --check <password> verify the checksums
  257. --help show this message
  258. " (first args)))
  259. (define (main args)
  260. (cond
  261. ((and (> (length args) 1) (equal? "--help" (second args)))
  262. (help args))
  263. ((and (> (length args) 2) (equal? "--check" (second args)))
  264. (let-values (((check calck count) (letterblock-invalid? (third args))))
  265. (cond
  266. (count
  267. (format #t "letterblock invalid. First failed checksum: ~a should have been ~a at position ~a\n"
  268. check calck count)
  269. (exit 1))
  270. (else
  271. (format #t "valid letterblock password\n")))))
  272. (else
  273. (let
  274. (
  275. (len
  276. (if (<= 2 (length args))
  277. (string->number (second args))
  278. 12)))
  279. (display (letterblocks-nice (floor (/ len 4))))
  280. (newline)))))