Parent directory

genpass-xkcd

1771 bytes
 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
14emulate -L zsh -o no_unset -o warn_create_global -o warn_nested_var -o extended_glob
15
16if [[ ARGC -gt 1 || ${1-1} != ${~:-<1-$((16#7FFFFFFF))>} ]]; then
17  print -ru2 -- "usage: $0 [NUM]"
18  return 1
19fi
20
21zmodload zsh/system zsh/mathfunc || return
22
23local -r dict=/usr/share/dict/words
24
25if [[ ! -e $dict ]]; then
26  print -ru2 -- "$0: file not found: $dict"
27  return 1
28fi
29
30# Read all dictionary words and leave only those made of 1-6 characters.
31local -a words
32words=(${(M)${(f)"$(<$dict)"}:#[a-zA-Z](#c1,6)}) || return
33
34if (( $#words < 2 )); then
35  print -ru2 -- "$0: not enough suitable words in $dict"
36  return 1
37fi
38
39if (( $#words > 16#7FFFFFFF )); then
40  print -ru2 -- "$0: too many words in $dict"
41  return 1
42fi
43
44# Figure out how many words we need for 128 bits of security margin.
45# Each word adds log2($#words) bits.
46local -i n=$((ceil(128. / (log($#words) / log(2)))))
47
48{
49  local c
50  repeat ${1-1}; do
51    print -rn -- $n
52    repeat $n; do
53      while true; do
54        # Generate a random number in [0, 2**31).
55        local -i rnd=0
56        repeat 4; do
57          sysread -s1 c || return
58          (( rnd = (~(1 << 23) & rnd) << 8 | #c ))
59        done
60        # Avoid bias towards words in the beginning of the list.
61        (( rnd < 16#7FFFFFFF / $#words * $#words )) || continue
62        print -rn -- -$words[rnd%$#words+1]
63        break
64      done
65    done
66    print
67  done
68} </dev/urandom