Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

new function passphrase generator



Uses zsh/random and diceware to generate a passphrase.  I seem to have problems with my completion setup so I wasn't able to do serious testing on the completion function.  Hopefully someone will test it out.


diff --git a/Completion/Zsh/Function/_diceware b/Completion/Zsh/Function/_diceware
new file mode 100644
index 000000000..a706ccd70
--- /dev/null
+++ b/Completion/Zsh/Function/_diceware
@@ -0,0 +1,18 @@
+#compdef diceware
+
+# Completion function for the diceware Zsh function.
+# Provides suggestions for options like --capitalize, --separator, --numbers, +# --num-words, and --file, as well as file path suggestions for the wordlist.
+
+local curcontext="$curcontext" state line
+typeset -A opt_args # To store arguments of options if needed (e.g., --separator=X)
+local -a args # Array to build our arguments specification
+
+_arguments -C -s -S \
+  '(-c --capitalize)'{-c,--capitalize}'[Capitalize the first letter of each word]' \ +  '(-s --separator)'{-s+,--separator}'[Use SEP as the word separator]:separator string:_request_prompt "separator string" separator-string' \ +  '(-N --numbers)'{-N+::,--numbers::}'[Include COUNT random numbers pasted to random words (default: 1)]::count of numbers:_request_prompt "number count" num-count' \ +  '(-n --num-words)'{-n+,--num-words}'[Specify the number of words in the passphrase (default: 6, min 2)]::word count:_request_prompt "word count" word-count' \ +  '(-f --file)'{-f+,--file}'[Specify the path to the diceware wordlist]:wordlist file:_files -g "*.txt"' \ +  '*::arguments:_files' # Catch any other non-option arguments as files (though our function expects none after options)
+
diff --git a/Functions/Misc/diceware b/Functions/Misc/diceware
new file mode 100644
index 000000000..5b838cccc
--- /dev/null
+++ b/Functions/Misc/diceware
@@ -0,0 +1,207 @@
+#!/usr/bin/zsh
+# diceware - Generates a diceware style passphrase
+#
+emulate -L zsh # Ensure Zsh option defaults and FUNCTION_ARGZERO are set
+
+local default_wordlist_file="eff_large_wordlist.txt"
+local -a wordlist_search_paths=(
+  '.'
+  '${ZDOTDIR:-$HOME/.zsh}' # ZDOTDIR or ~/.zsh
+  '${XDG_CONFIG_HOME:-$HOME/.config}/zsh'
+  '/usr/share/diceware'
+  '/usr/local/share/diceware'
+)
+
+local usage
+read -rd '' usage << EOU
+ Usage: $0 [options]
+
+ Options:
+   -c, --capitalize
+       Capitalize the first letter of each word.
+   -s SEP, --separator SEP
+       Use SEP as the word separator (default: " ").
+   -N [COUNT], --numbers [COUNT]
+       Include COUNT random numbers (0-9) pasted to random words.
+       If COUNT is omitted, defaults to 1.
+   -n N, --num-words N
+       Specify the number of words in the passphrase (defaults to 6, minimum 2).
+   -f FILE, --file FILE
+       Specify the diceware wordlist.
+       If omitted, '${default_wordlist_file}' is searched for in:
+          ${(pj:\n           :)wordlist_search_paths}
+   -h, --help             : This message
+
+ Examples:
+# 6 words, searches for wordlist, no options
+   $0
+# 8 words, searches for wordlist
+   $0 -n 8
+# 6 words, custom wordlist path
+   $0 --file /my/list.txt
+# Capitalize, hyphen-separated, 7 words
+   $0 -c --separator '-' -n 7
+# 6 words, 1 number pasted to a random word
+   $0 -N
+# 6 words, 3 numbers pasted to random words
+   $0 --numbers 3
+# Capitalize, dot-separated, 2 numbers, 5 words
+   $0 -c -s'.' -N 2 -n 5
+EOU
+#
+# Requires: zsh/random module, sed
+
+# Load the zsh/random module early. Critical failure if not available.
+if ! zmodload zsh/random; then
+  print -u2 "Error: zsh/random module could not be loaded. Is it installed and available?"
+  return 1
+fi
+
+
+# Declare option variables with default values and `integer` type
+integer capitalize=0
+local separator=" "
+integer num_to_add_to_words=0
+integer num_words=6 # Default number of words
+local wordlist_file=""
+
+# zparseopts setup for short and long options
+local -a parsed_opts # Array to hold parsed options
+zparseopts -E -D -M -A parsed_opts \
+  c='-capitalize' -capitalize \
+  s:='-separator' -separator: \
+  N::='-numbers' -numbers:: \
+  n:='-num-words' -num-words: \
+  f:='-file' -file \
+  h='-help' "'?'"='-help' -help \
+  || { print -u2 "$usage" ; return 1; }
+
+# Process parsed options
+local opt_name
+for opt_name opt_val in "${(@kv)parsed_opts}"; do
+  if [[ $opt_val =~ '^=.*' && $#opt_val -gt 1 ]]; then
+    opt_val="${opt_val#=}"
+  fi
+  case "$opt_name" in
+    --capitalize)
+      capitalize=1
+      ;;
+    --separator)
+      separator="${opt_val}" # zparseopts stores arg in array at option index
+      ;;
+    --numbers)
+      # Check if an argument was provided for -N/--numbers
+      if [[ -z "${opt_val}" ]]; then
+        num_to_add_to_words=1 # -N given without arg defaults to 1
+      else
+        num_to_add_to_words="${opt_val}"
+      fi
+      ;;
+    --num-words)
+      num_words="${opt_val}"
+      ;;
+    --file)
+      wordlist_file="${opt_val}"
+      ;;
+    --help)
+      print -u2 $usage
+      return 0
+      ;;
+    *)
+      print -u2 $usage
+      return 1
+      ;;
+  esac
+done
+
+# Enforce minimum number of words
+if (( num_words < 2 )); then
+  print -u2 "Error: Minimum number of words is 2."
+  print -u2 "$usage"
+  return 1
+fi
+
+# Determine wordlist file if not explicitly specified
+if [[ -z "$wordlist_file" ]]; then
+  for p in "${(@e)wordlist_search_paths}"; do
+    if [[ -f "$p/$default_wordlist_file" ]]; then
+      wordlist_file="$p/$default_wordlist_file"
+      break
+    fi
+  done
+
+  if [[ -z "$wordlist_file" ]]; then
+    print -u2  "
+Error: Diceware wordlist 'eff_large_wordlist.txt' not found in any search path.
+
+Please download the official wordlist from the Electronic Frontier Foundation
+and place it in one of the searched directories (e.g., ~/.config/zsh/).
+
+Canonical URL: https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt
+
+Alternatively, specify a custom path using the -f or --file option."
+    return 1
+  fi
+fi
+
+if [[ "$separator" == "\n" || "$separator" == "\t" ]]; then
+  print -u2 "Warning: Using a newline or tab as a separator can produce unexpected output."
+fi
+
+integer num_lines=$(wc -l < "$wordlist_file" | tr -d ' ')
+if (( num_lines == 0 )); then
+  print -u2 "Error: Wordlist is empty."
+  print -u2 "$usage"
+  return 1
+fi
+
+local -a line_numbers=()
+integer i
+for i in {1..$num_words}
+do
+  line_numbers+=( $(( zrand_int($num_lines,1,1) )) ) # zrand_int used in arithmetic context
+done
+
+local sed_command=""
+if (( num_words > 0 )); then
+    # Correct Zsh specific syntax for sorting numerically, appending 'p', and joining
+    local -a tmp_sed_parts=(${^${(n)line_numbers}}p)
+    sed_command="${(j:;:)tmp_sed_parts}"
+fi
+
+local -a raw_words
+if [[ -n "$sed_command" ]]; then
+    raw_words=( $(sed -n "$sed_command" "$wordlist_file") )
+else
+    raw_words=() # No words to fetch if num_words was 0
+fi
+
+local -a passphrase_words=()
+local x word
+for x word in "${raw_words[@]}"; do
+  if (( capitalize )); then
+    word="${(C)word}" # Capitalize the first letter using Zsh (C) flag
+  fi
+  passphrase_words+=( "$word" )
+done
+
+# Paste random numbers to words if requested
+if (( num_to_add_to_words > 0 && ${#passphrase_words[@]} > 0 )); then
+  for i in {1..$num_to_add_to_words}
+  do
+    integer random_digit=$(( zrand_int(10) )) # 0-9 inclusive
+    # Use zrand_int with (upper, lower, inclusive) for 1-based array indexing directly +    integer target_word_index=$(( zrand_int(${#passphrase_words},1,1) )) # 1 to num_words inclusive
+    integer paste_position=$(( zrand_int(2) )) # 0 for prefix, 1 for suffix
+
+    if (( paste_position == 0 )); then
+ passphrase_words[target_word_index]="${random_digit}${passphrase_words[target_word_index]}" # Prefix
+    else
+ passphrase_words[target_word_index]="${passphrase_words[target_word_index]}${random_digit}" # Suffix
+    fi
+  done
+fi
+
+local final_passphrase="${(pj:$separator:)passphrase_words}"
+
+print "$final_passphrase"





Messages sorted by: Reverse Date, Date, Thread, Author