summaryrefslogtreecommitdiff
path: root/plugins/genpass
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/genpass')
-rw-r--r--plugins/genpass/README.md15
-rwxr-xr-xplugins/genpass/genpass-apple79
-rwxr-xr-xplugins/genpass/genpass-monkey32
-rwxr-xr-xplugins/genpass/genpass-xkcd68
-rw-r--r--plugins/genpass/genpass.plugin.zsh107
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