genpass-xkcd 1.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. #!/usr/bin/env zsh
  2. #
  3. # Usage: genpass-xkcd [NUM]
  4. #
  5. # Generate a password made of words from /usr/share/dict/words
  6. # with the security margin of at least 128 bits.
  7. #
  8. # Example password: 9-mien-flood-Patti-buxom-dozes-ickier-pay-ailed-Foster
  9. #
  10. # If given a numerical argument, generate that many passwords.
  11. #
  12. # The name of this utility is a reference to https://xkcd.com/936/.
  13. emulate -L zsh -o no_unset -o warn_create_global -o warn_nested_var -o extended_glob
  14. if [[ ARGC -gt 1 || ${1-1} != ${~:-<1-$((16#7FFFFFFF))>} ]]; then
  15. print -ru2 -- "usage: $0 [NUM]"
  16. return 1
  17. fi
  18. zmodload zsh/system zsh/mathfunc || return
  19. local -r dict=/usr/share/dict/words
  20. if [[ ! -e $dict ]]; then
  21. print -ru2 -- "$0: file not found: $dict"
  22. return 1
  23. fi
  24. # Read all dictionary words and leave only those made of 1-6 characters.
  25. local -a words
  26. words=(${(M)${(f)"$(<$dict)"}:#[a-zA-Z](#c1,6)}) || return
  27. if (( $#words < 2 )); then
  28. print -ru2 -- "$0: not enough suitable words in $dict"
  29. return 1
  30. fi
  31. if (( $#words > 16#7FFFFFFF )); then
  32. print -ru2 -- "$0: too many words in $dict"
  33. return 1
  34. fi
  35. # Figure out how many words we need for 128 bits of security margin.
  36. # Each word adds log2($#words) bits.
  37. local -i n=$((ceil(128. / (log($#words) / log(2)))))
  38. {
  39. local c
  40. repeat ${1-1}; do
  41. print -rn -- $n
  42. repeat $n; do
  43. while true; do
  44. # Generate a random number in [0, 2**31).
  45. local -i rnd=0
  46. repeat 4; do
  47. sysread -s1 c || return
  48. (( rnd = (~(1 << 23) & rnd) << 8 | #c ))
  49. done
  50. # Avoid bias towards words in the beginning of the list.
  51. (( rnd < 16#7FFFFFFF / $#words * $#words )) || continue
  52. print -rn -- -$words[rnd%$#words+1]
  53. break
  54. done
  55. done
  56. print
  57. done
  58. } </dev/urandom