#!/usr/bin/env bash # # gen-passwd.sh - a simple password generator. # # (C) Bruno Raoult ("br"), 2022 # Licensed under the GNU General Public License v3.0 or later. # Some rights reserved. See COPYING. # # You should have received a copy of the GNU General Public License along with this # program. If not, see . # # SPDX-License-Identifier: GPL-3.0-or-later # #%MAN_BEGIN% # NAME # gen-passwwd.sh - a simple password generator. # # SYNOPSIS # gen-passwd.sh [OPTIONS] TYPE [LENGTH] # # DESCRIPTION # Generate a random TYPE password with length LENGTH. # Available types are : # dice # A list of digits in range [1-6]. Default length is 5. The purpose of # this is only to help choosing a word in a diceware word list. # mac # A "xx-xx-xx-xx-xx-xx" type address, where 'x' are hexadecimal digits # (ranges 0-9 and a-h). # Length is the number of "bytes" (groups of 2 hexadecimal digits), and # defaults to 6. The default ":" delimiter can be changed with "-s" # option. # This is the default option. # passphrase # Generate words from a diceware-like dictionary. Length is the number # of words ans defaults to 6. # pincode # A numeric password. default LENGTH is 4, with no separator. # string # Password will be a string taken from different character ranges. # By default, alphabetic characters and digits. See -x option for # different character sets. # # OPTIONS # -c, --copy # Copy password to clipboard. # # -C, --capitalize # For 'passphrase' and 'mac' type only. # Passphrase: Capitalize words (first letter of each word). Recommended # if separator is set to null-string (--separator=0). # Mac: use capital hexadecimal digits. # # -d, --dictionary=FILE # Use FILE as wordlist file. Default is en-5. # FILE will be searched in these directories : root, current directory, # and /usr/local/share/br-tools/gen-password directory. # # -g, --gui # Will use a GUI (yad based) to propose the password. This GUI # simply displays the password, allows to copy it to clipboard, # and to re-generate a new password. # # -h, --help # Display usage and exit. # # -l, --list-dictionaries # Display the list of available dictionaries, with names suitable for # the "-d" option. # # -m, --man # Print a man-like help and exit. # # -n, --no-similar-chars # For "string" type only, this option removes similar characters which # could be difficult to differenciate: 0-O, 1-l, 8-B, [], ø-Ø, ~--, ... # # -s, --separator=CHAR # CHAR is used as separator when TYPE allows it. Use "0" to remove # separators. # # -v, --verbose # Print messages on what is being done. # # -x, --extended=RANGE # Specify the ranges of string type. Default is "a:1:a1", as lower case # alphabetic characters (a-z) and digits (0-9), with at least one letter # and one digit. RANGE is a string composed of: # a: lower case alphabetic characters (a-z) # A: upper case alphabetic characters (A-Z) # e: extra European characters (e.g. À, É, é, Ï, ï, Ø, ø...) # 1: digits (0-9) # x: extended characters set 1: #$%&@^`~.,:;{[()]} # y: extended characters set 2: "'\/|_-<>*+!?= # k: japanese hiragana: あいうえおかき... # When a RANGED character is followed by a ':' exactly one character of # this range will appear in generated password: If we want two or more # digits, the syntax would be '-x1:1:1'. # # TODO # Add different languages wordlists. # Replace hiragana with half-width katakana ? # Add usage examples # # AUTHOR # Bruno Raoult. # # SEE ALSO # Pages on Diceware/words lists : # EFF: https://www.eff.org/dice # diceware: https://theworld.com/~reinhold/diceware.html # #%MAN_END% SCRIPT="$0" # full path to script CMDNAME=${0##*/} # script name SHELLVERSION=$(( BASH_VERSINFO[0] * 10 + BASH_VERSINFO[1] )) export LC_CTYPE="C.UTF-8" # to handle non ascii chars # character sets declare -A pw_charsets=( [a]="abcdefghijklmnopqrstuvwxyz" [A]="ABCDEFGHIJKLMNOPQRSTUVWXYZ" [1]="0123456789" [e]="âêîôûáéíóúàèìòùäëïöüãõñçøÂÊÎÔÛÁÉÍÓÚÀÈÌÒÙÄËÏÖÜÃÕÑÇØ¡¿" [x]='#$%&@^`~.,:;{[()]}' [y]=\''"\/|_-<>*+!?=' [k]="あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん" ) # default type, length, separator declare pw_type="mac" declare pw_length=6 declare pw_sep=":" declare pw_cap="" declare pw_dict="" declare pw_copy="" declare pw_gui="" declare pw_verbose="" declare pw_no_similar="" declare pw_charset="a:A:1:aA1" declare -A pw_commands=() declare -a pw_command=() usage() { printf "usage: %s [-s CHAR][-d DICT][-x CHARSET][-Ccgmv] [TYPE] [LENGTH]\n" "$CMDNAME" printf "Use '%s --man' for more help\n" "$CMDNAME" return 0 } man() { sed -n '/^#%MAN_BEGIN%/,/^#%MAN_END%$/{//!s/^#[ ]\{0,1\}//p}' "$SCRIPT" | more } # log() - log messages on stderr # parameters: # -l, -s: long, or short prefix (default: none). Last one is used. # -t: timestamp # -n: no newline # This function accepts either a string, either a format string followed # by arguments : # log -s "%s" "foo" # log -s "foo" log() { local timestr="" prefix="" newline=y todo OPTIND [[ -z $pw_verbose ]] && return 0 while getopts lsnt todo; do case $todo in l) prefix=$(printf "*%.s" {1..30}) ;; s) prefix=$(printf "*%.s" {1..5}) ;; n) newline=n ;; t) timestr=$(date "+%F %T%z ") ;; *) ;; esac done shift $((OPTIND - 1)) [[ $prefix != "" ]] && printf "%s " "$prefix" >&2 [[ $timestr != "" ]] && printf "%s" "$timestr" >&2 # shellcheck disable=SC2059 printf "$@" >&2 [[ $newline = y ]] && printf "\n" >&2 return 0 } # check_dict() - check for dictionary file # $1: the dictionary filename (variable reference). # # @return: 0 on success, $1 will contain full path to dictionary. # @return: 1 if not found # @return: 2 if format is wrong check_dict() { local -n dict="$1" local tmp_dir tmp_dict tmp_key tmp_dummy if [[ -n "$dict" ]]; then for tmp_dir in / ./ /usr/local/share/br-tools/gen-password/; do tmp_dict="$tmp_dir$dict.txt" log -n "checking for %s dictionary... " "$tmp_dict" if [[ -f "$tmp_dict" ]]; then log -n "found, " # shellcheck disable=SC2034 read -r tmp_key tmp_dummy < "$tmp_dict" if ! [[ $tmp_key =~ ^[1-6]+$ ]]; then log "wrong format [%s]" "$tmp_key" return 2 fi log "key length=%d" "${#tmp_key}" dict="$tmp_dict" return 0 else log "not found." fi done printf "cannot find '%s' dictionary file\n" "$dict" exit 1 fi return 0 } # list_dict() - list available dictionaries. # # @return: 0 on success # @return: 1 on error list_dict() { local datadir="/usr/local/share/br-tools/gen-password" file fn fn2 key dummy local -a output local -i res=1 cur=0 i if [[ -d "$datadir" ]]; then printf -v output[0] "#\tlen\tName" for file in "$datadir"/*.txt; do fn=${file##*/} fn=${fn%.txt} # shellcheck disable=SC2034 fn2="$fn" if check_dict fn2; then (( cur++ )) # shellcheck disable=SC2034 read -r key dummy < "$file" printf -v output[cur-1] "%d\t%d\t%s" "$cur" "${#key}" "$fn" fi done if ((cur > 0)); then printf "#\tlen\tName\n" for (( i = 0; i < cur; ++i )); do printf "%s\n" "${output[i]}" done return 0 fi fi printf "No dictionaries found.\n" return 1 } # sanitize() - sanitize string for HTML characters # $1: string to cleanup # # @return: 0, $1 will contain the sanitized string sanitize() { local str="$1" str=${str//&/&} str=${str///>} str=${str//'"'/"} log "sanitized string: '%s' -> '%s'" "$1" "$str" printf -- "%str" "$str" } # srandom() - use RANDOM to simulate SRANDOM # $1: Reference of variable to hold result # # Note: RANDOM is 15 bits, SRANDOM is 32 bits. # # @return: 0, $1 will contain the 32 bits random number srandom() { local -n _ret=$1 (( _ret = RANDOM << 17 | RANDOM << 2 | RANDOM & 3 )) } # rnd() - get a random number integer # $1: An integer, the modulo value # # @return: 0, output a string with the random integer on stdout. # # This function uses SRANDOM for bash >= 5.1 and srandom() function # above for lower versions. rnd() { local mod=$1 ret if (( SHELLVERSION >= 51 )); then # shellcheck disable=SC2153 (( ret = SRANDOM )) else srandom ret fi printf "%d" "$(( ret % mod ))" } # shuffle() - shuffle a string # $1: The string to shuffle # # The string is shuffled using the Fisher–Yates shuffle method : # https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle # # @return: 0, output the shuffled string to stdout. shuffle() { local _str="$1" local _res="" local -i _i _len=${#_str} _cur=0 for (( _i = _len ; _i > 0; --_i )); do _cur=$(rnd "$_i") _res+=${_str:$_cur:1} _str="${_str:0:_cur}${_str:_cur+1}" done printf "%s" "$_res" return 0 } # rnd_hex() - get a random 2-digits hex number # # @return: 0, output a string with the random integer on stdout. rnd_hex() { printf "%02x" "$(rnd 256)" } # rnd_dice() - get a 6 faces 1-6 random number # # @return: 0, output a string {1..6} rnd_dice() { printf "%d" "$(( $(rnd 6) + 1 ))" } # rnd_digit() - get a digit random number # # @return: 0, output a string {0..9} rnd_digit() { printf "%d" "$(( $(rnd 10) ))" } # rnd_word() - get a word from file # $1: The dice rolls # $2: The word list file () # # @return: 0, output a string {0..9} rnd_word() { local roll="$1" file="$2" word="" word=$(sed -n "s/^${roll}[[:blank:]]\+//p" "$file") printf "%s" "$word" } # rnd_charset() - get a random string from a charset # $1: A string with characters to choose from # $2: An integer, the length of returned string # # @return: 0, output a random string from charset $1, with length $2. rnd_charset() { local charset="$1" ret="" local -i len=$2 _i #log "rnd_charset: %d from '%s'" "$len" "$charset" for ((_i=0; _i n)) && log "truncating '%s' to '%s'" "$str" "${str:0:n}" printf "%s" "${str:0:n}" return 0 } pw_commands["string"]=pwd_string # print command() - print a pwd_command parameters # $1: reference of pwd_command array # # @return: 0 print_command() { local -n arr="$1" local -a label=("function" "length" "sep" "cap" "dict" "charset") local -i i for i in "${!arr[@]}"; do log -s "%s=[%s]" "${label[$i]}" "${arr[$i]}" done return 0 } # gui_passwd() - GUI for passwords # $1: reference pwd_command array # # @return: 0 gui_passwd() { local -a _command=("$@") local passwd="" res=0 sane="" while passwd=$("${_command[@]}") sane=$(sanitize "$passwd") yad --title="Password Generator" --text-align=center --text="$sane" \ --borders=20 --button=gtk-copy:0 --button=gtk-refresh:1 \ --button=gtk-ok:252 --window-icon=dialog-password res=$? log "res=%d\n" "$res" if (( res == 0 )); then printf "%s" "$passwd" | xsel -bi fi ((res != 252)) do true; done return $res } parse_opts() { # short and long options local sopts="cCd:ghlmns:vx:" local lopts="copy,capitalize,dictionary:,gui,help,list-dictionaries,man,no-similar-chars,separator:,verbose,extended:" # set by options local tmp="" tmp_length="" tmp_sep="" tmp_cap="" tmp_dict="" tmp_dir="" local tmp_charset="" local c2="" c3="" local -i i if ! tmp=$(getopt -o "$sopts" -l "$lopts" -n "$CMDNAME" -- "$@"); then log "Use '$CMD --help' or 'zob $CMDNAME --man' for help." exit 1 fi eval set -- "$tmp" while true; do case "$1" in '-c'|'--copy') pw_copy=y ;; '-C'|'--capitalize') tmp_cap=y ;; '-d'|'--dictionary') tmp_dict="$2" shift ;; '-g'|'--gui') if ! type -P "yad" > /dev/null; then printf "%s: Please install 'yad' package tu use 'g' option.\n" \ "$CMDNAME" fi pw_gui="y" ;; '-h'|'--help') usage exit 0 ;; '-l'|'--list-dictionaries') list_dict exit 0 ;; '-m'|'--man') man exit 0 ;; '-n'|'no-similar-chars') pw_no_similar=y ;; '-s'|'--separator') tmp_sep="$2" shift ;; '-v'|'--verbose') pw_verbose=y ;; '-x'|'--extended') for (( i = 0; i < ${#2}; ++i)); do c2="${2:i:1}" case "$c2" in a|A|1|x|y|k|e) tmp_charset+="$c2" c3="${2:i+1:1}" if [[ "$c3" == ":" ]]; then tmp_charset+=":" (( i++ )) fi ;; *) printf "unknown character set '%s\n" "${2:$i:1}" usage exit 1 esac done shift ;; '--') shift break ;; *) usage log 'Internal error!' exit 1 ;; esac shift done # parse remaining arguments if (($# > 0)); then # type type=$1 case "$type" in dice) pw_type="dice" tmp_length=5 [[ -z $tmp_sep ]] && tmp_sep=" " ;; mac) pw_type="mac" tmp_length=6 [[ -z $tmp_sep ]] && tmp_sep=":" ;; pincode) pw_type="pincode" tmp_length=4 [[ -z $tmp_sep ]] && tmp_sep="0" ;; passphrase) pw_type="passphrase" tmp_length=6 [[ -z $tmp_dict ]] && tmp_dict="en-5" [[ -z $tmp_sep ]] && tmp_sep=" " [[ -z $tmp_cap ]] && tmp_cap="" ;; string) pw_type="string" tmp_length=10 if [[ -n $pw_no_similar ]]; then pw_charsets[A]="ABCDEFGHIJKLMNPQRSTUVWXYZ" pw_charsets[a]="abcdefghijkmnopqrstuvwxyz" pw_charsets[1]="23456789" pw_charsets[e]="âêîôûáéíóúàèìòùñçÂÊÎÔÛÁÉÍÓÚÀÈÌÒÙÇ¡¿" pw_charsets[x]='#$%&@^`.,:;{()}' pw_charsets[y]='\/|_<>*+!?=' fi if [[ -n $tmp_charset ]]; then pw_charset="$tmp_charset" fi ;; *) printf "%s: Unknown '%s' password type.\n" "$CMDNAME" "$type" usage exit 1 esac shift fi if (($# > 0)); then # length if ! [[ $1 =~ ^[0-9]+$ ]]; then printf "%s: Bad '%s' length.\n" "$CMDNAME" "$1" usage exit 1 fi tmp_length="$1" shift fi [[ -n $tmp_length ]] && pw_length=$tmp_length if ! (( pw_length )); then printf "%s: Bad '%d' length.\n" "$CMDNAME" "$tmp_length" usage exit 1 fi [[ -n $tmp_sep ]] && pw_sep=$tmp_sep [[ $pw_sep = "0" ]] && pw_sep="" [[ -n $tmp_cap ]] && pw_cap=$tmp_cap [[ -n $tmp_dict ]] && pw_dict=$tmp_dict # look for dictionary file check_dict pw_dict || exit 1 } parse_opts "$@" pw_command=("${pw_commands[$pw_type]}" "$pw_length" "$pw_sep" "$pw_cap" "$pw_dict" "$pw_charset") print_command pw_command if [[ -z $pw_gui ]]; then passwd=$("${pw_command[@]}") if [[ -n $pw_copy ]]; then printf "%s" "$passwd" | xsel -bi fi printf "%s\n" "$passwd" else gui_passwd "${pw_command[@]}" fi exit 0