#!/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}-*" }