ohmyzsh/plugins/genpass/genpass-apple

100 lines
3.1 KiB
Bash
Executable file

#!/usr/bin/env zsh
#
# Usage: genpass-apple [NUM]
#
# Generate a password made of 6 syllables of 3 characters each
# with the security margin of at least 71 bits.
#
# Example password: nukci1-zochob-Werfip
#
# 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=bcdfghjkmnpqrstvwxz
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 i
repeat ${1-1}; do
# Generate 6 syllables of the form cvc where c and v
# denote random consonants and vowels respectively.
local syllables=()
repeat 6; do
syllables+=('')
for chars in $consonants $vowels $consonants; do
-$0-rand $#chars || return
syllables[-1]+=$chars[REPLY]
done
done
# First concatenate all syllables without hyphens
local pwd_chars=${(j::)syllables}
# Valid positions for digit in the 18-char string: 5, 6, 11, 12, 17 (0-indexed)
# In 1-indexed: 6, 7, 12, 13, 18
local -a digit_positions=(6 7 12 13 18)
-$0-rand $#digit_positions || return
local digit_pos=$digit_positions[REPLY]
# Generate random digit
-$0-rand $#digits || return
local digit=$digits[REPLY]
# Special handling for positions 7 and 13 (start of syllable pair)
if [[ $digit_pos == 7 || $digit_pos == 13 ]]; then
# Shift characters backwards to maintain cvcv pattern
for (( i = digit_pos + 5; i > digit_pos; i-- )); do
pwd_chars[i]=$pwd_chars[i-1]
done
fi
# Place the digit
pwd_chars[digit_pos]=$digit
# Now add hyphens to create the final password
local pwd="${pwd_chars[1,6]}-${pwd_chars[7,12]}-${pwd_chars[13,18]}"
# Convert one lower-case character to upper case (excluding digit position)
# Calculate digit position in final string (with hyphens)
local final_digit_pos=$((digit_pos + (digit_pos - 1) / 6))
while true; do
-$0-rand $#pwd || return
# Skip if it's the digit position or a hyphen
[[ REPLY == $final_digit_pos || $pwd[REPLY] == '-' ]] && continue
[[ $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