diff options
Diffstat (limited to 'plugins/genpass')
-rw-r--r-- | plugins/genpass/README.md | 15 | ||||
-rwxr-xr-x | plugins/genpass/genpass-apple | 79 | ||||
-rwxr-xr-x | plugins/genpass/genpass-monkey | 32 | ||||
-rwxr-xr-x | plugins/genpass/genpass-xkcd | 68 | ||||
-rw-r--r-- | plugins/genpass/genpass.plugin.zsh | 107 |
5 files changed, 188 insertions, 113 deletions
diff --git a/plugins/genpass/README.md b/plugins/genpass/README.md index e6e7a5138..a5ff4a876 100644 --- a/plugins/genpass/README.md +++ b/plugins/genpass/README.md @@ -5,21 +5,22 @@ has at least a 128-bit security margin and generates passwords from the cryptographically secure `/dev/urandom`. Each generator can also take an optional numeric argument to generate multiple passwords. -Requirements: +To use it from an interactive ZSH, add `genpass` to the plugins array in your +zshrc file: -* `grep(1)` -* GNU coreutils (or appropriate for your system) -* Word list providing `/usr/share/dict/words` + plugins=(... genpass) -To use it, add `genpass` to the plugins array in your zshrc file: +You can also invoke password generators directly (they are implemented as +standalone executable files), which can be handy when you need to generate +passwords in a script: - plugins=(... genpass) + ~/.oh-my-zsh/plugins/genpass/genpass-apple 3 ## genpass-apple Generates a pronounceable pseudoword passphrase of the "cvccvc" consonant/vowel syntax, inspired by [Apple's iCloud Keychain password generator][1]. Each -pseudoword has exactly 1 digit placed at the edge of a "word" and exactly 1 +password has exactly 1 digit placed at the edge of a "word" and exactly 1 capital letter to satisfy most password security requirements. % genpass-apple diff --git a/plugins/genpass/genpass-apple b/plugins/genpass/genpass-apple new file mode 100755 index 000000000..963ab6447 --- /dev/null +++ b/plugins/genpass/genpass-apple @@ -0,0 +1,79 @@ +#!/usr/bin/env zsh +# +# Usage: genpass-apple [NUM] +# +# Generate a password made of 6 pseudowords of 6 characters each +# with the security margin of at least 128 bits. +# +# Example password: xudmec-4ambyj-tavric-mumpub-mydVop-bypjyp +# +# If given a numerical argument, generate that many passwords. + +emulate -L zsh -o no_unset -o warn_create_global -o warn_nested_var + +if [[ ARGC -gt 1 || ${1-1} != ${~:-<1-$((16#7FFFFFFF))>} ]]; then + print -ru2 -- "usage: $0 [NUM]" + return 1 +fi + +zmodload zsh/system zsh/mathfunc || return + +{ + local -r vowels=aeiouy + local -r consonants=bcdfghjklmnpqrstvwxz + local -r digits=0123456789 + + # Sets REPLY to a uniformly distributed random number in [1, $1]. + # Requires: $1 <= 256. + function -$0-rand() { + local c + while true; do + sysread -s1 c || return + # Avoid bias towards smaller numbers. + (( #c < 256 / $1 * $1 )) && break + done + typeset -g REPLY=$((#c % $1 + 1)) + } + + local REPLY chars + + repeat ${1-1}; do + # Generate 6 pseudowords of the form cvccvc where c and v + # denote random consonants and vowels respectively. + local words=() + repeat 6; do + words+=('') + repeat 2; do + for chars in $consonants $vowels $consonants; do + -$0-rand $#chars || return + words[-1]+=$chars[REPLY] + done + done + done + + local pwd=${(j:-:)words} + + # Replace either the first or the last character in one of + # the words with a random digit. + -$0-rand $#digits || return + local digit=$digits[REPLY] + -$0-rand $((2 * $#words)) || return + pwd[REPLY/2*7+2*(REPLY%2)-1]=$digit + + # Convert one lower-case character to upper case. + while true; do + -$0-rand $#pwd || return + [[ $vowels$consonants == *$pwd[REPLY]* ]] && break + done + # NOTE: We aren't using ${(U)c} here because its results are + # locale-dependent. For example, when upper-casing 'i' in Turkish + # locale we would get 'İ', a.k.a. latin capital letter i with dot + # above. We could set LC_CTYPE=C locally but then we would run afoul + # of this zsh bug: https://www.zsh.org/mla/workers/2020/msg00588.html. + local c=$pwd[REPLY] + printf -v c '%o' $((#c - 32)) + printf "%s\\$c%s\\n" "$pwd[1,REPLY-1]" "$pwd[REPLY+1,-1]" || return + done +} always { + unfunction -m -- "-${(b)0}-*" +} </dev/urandom diff --git a/plugins/genpass/genpass-monkey b/plugins/genpass/genpass-monkey new file mode 100755 index 000000000..94ff5e131 --- /dev/null +++ b/plugins/genpass/genpass-monkey @@ -0,0 +1,32 @@ +#!/usr/bin/env zsh +# +# Usage: genpass-monkey [NUM] +# +# Generate a password made of 26 alphanumeric characters +# with the security margin of at least 128 bits. +# +# Example password: nz5ej2kypkvcw0rn5cvhs6qxtm +# +# If given a numerical argument, generate that many passwords. + +emulate -L zsh -o no_unset -o warn_create_global -o warn_nested_var + +if [[ ARGC -gt 1 || ${1-1} != ${~:-<1-$((16#7FFFFFFF))>} ]]; then + print -ru2 -- "usage: $0 [NUM]" + return 1 +fi + +zmodload zsh/system || return + +{ + local -r chars=abcdefghjkmnpqrstvwxyz0123456789 + local c + repeat ${1-1}; do + repeat 26; do + sysread -s1 c || return + # There is uniform because $#chars divides 256. + print -rn -- $chars[#c%$#chars+1] + done + print + done +} </dev/urandom diff --git a/plugins/genpass/genpass-xkcd b/plugins/genpass/genpass-xkcd new file mode 100755 index 000000000..a486ccb40 --- /dev/null +++ b/plugins/genpass/genpass-xkcd @@ -0,0 +1,68 @@ +#!/usr/bin/env zsh +# +# Usage: genpass-xkcd [NUM] +# +# Generate a password made of words from /usr/share/dict/words +# with the security margin of at least 128 bits. +# +# Example password: 9-mien-flood-Patti-buxom-dozes-ickier-pay-ailed-Foster +# +# If given a numerical argument, generate that many passwords. +# +# The name of this utility is a reference to https://xkcd.com/936/. + +emulate -L zsh -o no_unset -o warn_create_global -o warn_nested_var -o extended_glob + +if [[ ARGC -gt 1 || ${1-1} != ${~:-<1-$((16#7FFFFFFF))>} ]]; then + print -ru2 -- "usage: $0 [NUM]" + return 1 +fi + +zmodload zsh/system zsh/mathfunc || return + +local -r dict=/usr/share/dict/words + +if [[ ! -e $dict ]]; then + print -ru2 -- "$0: file not found: $dict" + return 1 +fi + +# Read all dictionary words and leave only those made of 1-6 characters. +local -a words +words=(${(M)${(f)"$(<$dict)"}:#[a-zA-Z](#c1,6)}) || return + +if (( $#words < 2 )); then + print -ru2 -- "$0: not enough suitable words in $dict" + return 1 +fi + +if (( $#words > 16#7FFFFFFF )); then + print -ru2 -- "$0: too many words in $dict" + return 1 +fi + +# Figure out how many words we need for 128 bits of security margin. +# Each word adds log2($#words) bits. +local -i n=$((ceil(128. / log2($#words)))) + +{ + local c + repeat ${1-1}; do + print -rn -- $n + repeat $n; do + while true; do + # Generate a random number in [0, 2**31). + local -i rnd=0 + repeat 4; do + sysread -s1 c || return + (( rnd = (~(1 << 23) & rnd) << 8 | #c )) + done + # Avoid bias towards words in the beginning of the list. + (( rnd < 16#7FFFFFFF / $#words * $#words )) || continue + print -rn -- -$words[rnd%$#words+1] + break + done + done + print + done +} </dev/urandom diff --git a/plugins/genpass/genpass.plugin.zsh b/plugins/genpass/genpass.plugin.zsh index e6a1cef34..a0ea841cd 100644 --- a/plugins/genpass/genpass.plugin.zsh +++ b/plugins/genpass/genpass.plugin.zsh @@ -1,106 +1 @@ -autoload -U regexp-replace -zmodload zsh/mathfunc - -genpass-apple() { - # Generates a 128-bit password of 6 pseudowords of 6 characters each - # EG, xudmec-4ambyj-tavric-mumpub-mydVop-bypjyp - # Can take a numerical argument for generating extra passwords - local -i i j num - - [[ $1 =~ '^[0-9]+$' ]] && num=$1 || num=1 - - local consonants="$(LC_ALL=C tr -cd b-df-hj-np-tv-xz < /dev/urandom \ - | head -c $((24*$num)))" - local vowels="$(LC_ALL=C tr -cd aeiouy < /dev/urandom | head -c $((12*$num)))" - local digits="$(LC_ALL=C tr -cd 0-9 < /dev/urandom | head -c $num)" - - # The digit is placed on a pseudoword edge using $base36. IE, Dvccvc or cvccvD - local position="$(LC_ALL=C tr -cd 056bchinotuz < /dev/urandom | head -c $num)" - local -A base36=(0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 a 10 b 11 c 12 d 13 \ - e 14 f 15 g 16 h 17 i 18 j 19 k 20 l 21 m 22 n 23 o 24 p 25 q 26 r 27 s 28 \ - t 29 u 30 v 31 w 32 x 33 y 34 z 35) - - for i in {1..$num}; do - local pseudo="" - - for j in {1..12}; do - # Uniformly iterate through $consonants and $vowels for each $i and $j - # Creates cvccvccvccvccvccvccvccvccvccvccvccvc for each $num - pseudo="${pseudo}${consonants:$((24*$i+2*${j}-26)):1}" - pseudo="${pseudo}${vowels:$((12*$i+${j}-13)):1}" - pseudo="${pseudo}${consonants:$((24*$i+2*${j}-25)):1}" - done - - local -i digit_pos=${base36[${position[$i]}]} - local -i char_pos=$digit_pos - - # The digit and uppercase character must be in different locations - while [[ $digit_pos == $char_pos ]]; do - char_pos=$base36[$(LC_ALL=C tr -cd 0-9a-z < /dev/urandom | head -c 1)] - done - - # Places the digit on a pseudoword edge - regexp-replace pseudo "^(.{$digit_pos}).(.*)$" \ - '${match[1]}${digits[$i]}${match[2]}' - - # Uppercase a random character (that is not a digit) - regexp-replace pseudo "^(.{$char_pos})(.)(.*)$" \ - '${match[1]}${(U)match[2]}${match[3]}' - - # Hyphenate each 6-character pseudoword - regexp-replace pseudo '^(.{6})(.{6})(.{6})(.{6})(.{6})(.{6})$' \ - '${match[1]}-${match[2]}-${match[3]}-${match[4]}-${match[5]}-${match[6]}' - - printf "${pseudo}\n" - done -} - -genpass-monkey() { - # Generates a 128-bit base32 password as if monkeys banged the keyboard - # EG, nz5ej2kypkvcw0rn5cvhs6qxtm - # Can take a numerical argument for generating extra passwords - local -i i num - - [[ $1 =~ '^[0-9]+$' ]] && num=$1 || num=1 - - local pass=$(LC_ALL=C tr -cd '0-9a-hjkmnp-tv-z' < /dev/urandom \ - | head -c $((26*$num))) - - for i in {1..$num}; do - printf "${pass:$((26*($i-1))):26}\n" - done -} - -genpass-xkcd() { - # Generates a 128-bit XKCD-style passphrase - # e.g, 9-mien-flood-Patti-buxom-dozes-ickier-pay-ailed-Foster - # Can take a numerical argument for generating extra passwords - - if (( ! $+commands[shuf] )); then - echo >&2 "$0: \`shuf\` command not found. Install coreutils (\`brew install coreutils\` on macOS)." - return 1 - fi - - if [[ ! -e /usr/share/dict/words ]]; then - echo >&2 "$0: no wordlist found in \`/usr/share/dict/words\`. Install one first." - return 1 - fi - - local -i i num - - [[ $1 =~ '^[0-9]+$' ]] && num=$1 || num=1 - - # Get all alphabetic words of at most 6 characters in length - local dict=$(LC_ALL=C grep -E '^[a-zA-Z]{1,6}$' /usr/share/dict/words) - - # Calculate the base-2 entropy of each word in $dict - # Entropy is e = L * log2(C), where L is the length of the password (here, - # in words) and C the size of the character set (here, words in $dict). - # Solve for e = 128 bits of entropy. Recall: log2(n) = log(n)/log(2). - local -i n=$((int(ceil(128*log(2)/log(${(w)#dict}))))) - - for i in {1..$num}; do - printf "$n-" - printf "$dict" | shuf -n "$n" | paste -sd '-' - - done -} +autoload -Uz genpass-apple genpass-monkey genpass-xkcd |