securepassword.w 15 KB

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