Skip to content

Instantly share code, notes, and snippets.

@yrps
Last active December 30, 2016 10:08
Show Gist options
  • Save yrps/cf2a3155fb147d25e65592c4d067a0a7 to your computer and use it in GitHub Desktop.
Save yrps/cf2a3155fb147d25e65592c4d067a0a7 to your computer and use it in GitHub Desktop.
passuniq - look for duplicate or blank passwords in your password store.
#!/bin/bash
script="${0##*/}"
if [[ -z "${BASH_VERSINFO[0]}" || "${BASH_VERSINFO[0]}" -lt 4 ]]; then
2>&1 printf "%s requires bash>=4.\n" "$script"
exit 128
fi
min=20
declare -A retcode color
retcode=( [blank]=1 [dupe]=2 [short]=4 [error]=8 [invoc]=254 [none]=255 )
color=( [error]=1 [ok]=2 [dupe]=3 [short]=5 [blank]=7 )
usage() {
cat <<USAGE
NAME
$script - identify duplicate, blank, or weak passwords in your password store.
SYNOPSIS
$script [options]
$script [options] subdir
DESCRIPTION
$script uses gpg(1), or gpg2(1) if available, to check if all files in the
password store have unique initial lines as their cleartext.
If available, gpg-agent(1) is used to faciliate batch operation.
OPTIONS
-q, --quiet
This option suppresses password counting.
Use twice to suppress reporting blanks and duplicates.
Under no circumstances are passwords output.
-m LENGTH, --min LENGTH
Consider LENGTH characters to be the minimum length for a good password.
Default is $min. Set to 0 to skip checking password length.
Passwords which consist only of digits will be ignored.
-c COUNT, --count COUNT
Stop processing after COUNT entries. Default is to do them all.
-h, --help
Display this help text and exit.
If a subdir is passed, $script will only search the named subdirectory of the
password store directory.
ENVIRONMENT VARIABLES
PASSWORD_STORE_DIR
If present, overrides the default password store directory.
RETURN CODES
${retcode[none]} if no GPG-encrypted files were found in the password store directory.
${retcode[invoc]} if there were invocation errors.
0 if there are no duplicates, blank passwords, or decryption errors.
Otherwise, a bitwise disjunction denoting problems encountered:
${retcode[blank]} for blank passwords.
${retcode[dupe]} for duplicate passwords.
${retcode[short]} for passwords below minimum length.
${retcode[error]} for decryption errors.
ENVIRONMENT
PASSWORD_STORE_DIR overrides the location to be searched.
The default is ~/.password-store.
SEE ALSO
pass(1), gpg(1), gpg2(1), gpg-agent(1)
USAGE
}
parsed="$(getopt -o qhm:c: -l quiet,help,min:,count: -n "$script" -- "$@")" ||
exit 254
eval set -- "$parsed"
unset parsed
quiet=0
while true; do
case $1 in
-q|--quiet)
((quiet++))
shift
;;
-h|--help)
usage
exit 0
;;
-m|--min)
min="$2"
if [[ ! "$min" =~ ^[0-9]+$ ]]; then
printf "min must be a number of characters, not %s\n" "$min"
exit 254
fi
shift 2
;;
-c|--count)
cnt="$2"
if [[ ! "$cnt" =~ ^[0-9]+$ ]]; then
printf "count must be a number of entries, not %s\n" "$cnt"
exit 254
fi
shift 2
;;
--)
shift
break
;;
esac
done
# Expand double *; do not expand unmatched *
shopt -s globstar nullglob
set -o pipefail
color_print() {
[ -t 1 ] && printf '%s' "$(tput setaf "$1")"
printf '%s' "$2"
[ -t 1 ] && printf '%s' "$(tput sgr0)"
}
symbol() {
[ $quiet -gt 0 ] && return
local c="${color[$1]}"
case $1 in
error)
color_print "$c" $'\u2717' ;;
ok)
color_print "$c" $'\u2713' ;;
dupe)
color_print "$c" $'\u26A0' ;;
short)
color_print "$c" $'\u26A0' ;;
blank)
color_print "$c" $'\u237D' ;;
esac
}
GPG_OPTS=( "--quiet" "--yes" "--batch" "--use-agent" )
GPG="gpg"
export GPG_TTY="${GPG_TTY:-$(tty 2>/dev/null)}"
which gpg2 &>/dev/null && GPG="gpg2"
prefix="${PASSWORD_STORE_DIR:-$HOME/.password-store}"
[ -n "$1" ] && prefix+="/$1"
declare -A lists
passcount=0 blanklist='' shortlist='' errorlist='' _exit=0
for passfile in "$prefix"/**/*.gpg; do
[[ -n "$cnt" && "$passcount" -ge "$cnt" ]] && break
((passcount++))
printed=
val="${passfile#$prefix/}"
val="${val%.gpg}"
read -r key <<< "$(2>/dev/null $GPG --decrypt "${GPG_OPTS[@]}" "$passfile")"
_status="$?"
if [ "$_status" -gt 0 ]; then
errorlist+="$val\n"
_exit=$((_exit | retcode[error]))
symbol error
continue
elif [ -z "$key" ]; then
blanklist+="$val\n"
_exit=$((_exit | retcode[blank]))
symbol blank
continue
fi
if [[ "$min" -gt 0 && "${#key}" -lt "$min" && ! "$key" =~ ^[0-9]+$ ]]; then
shortlist+="$(printf '%2d %s' "${#key}" "$val")"
shortlist+=$'\n'
_exit=$((_exit | retcode[short]))
symbol short
printed=1
fi
if [ -n "${lists["$key"]}" ]; then
_exit=$((_exit | retcode[dupe]))
[ -z "$printed" ] && symbol dupe
printed=1
fi
lists["$key"]+="$val\n"
[ -z "$printed" ] && symbol ok
done
unset printed key val
[ "$quiet" -eq 0 ] && printf "\nPasswords processed: %d\n" "$passcount"
printf "\n"
if [ $_exit -eq 0 ] && [ -z "${!lists[*]}" ]; then
1>&2 printf "No encrypted files were found in %s.\n\n" "$prefix"
exit 255
fi
if [ $quiet -lt 2 ]; then
count_and_print() {
local list="$1" col="$2" plural="$3" singular="$4" files
[ -z "$list" ] && return
files="$(printf "%b" "$list" | wc -l)"
if [ "$files" -eq 1 ]; then
[[ -z "$singular" ]] && return
message="$singular"
else
message="$plural"
fi
color_print "$col" "$(printf "%d %s:" "$files" "$message")"
printf "\n%b\n" "$list"
}
count_and_print "$blanklist" "${color[blank]}" \
"files have no password" "file has no password"
count_and_print "$shortlist" "${color[short]}" \
"files have password length<$min" "file has password length<$min"
for key in "${!lists[@]}"; do
count_and_print "${lists["$key"]}" "${color[dupe]}" \
"files have the same password"
done
count_and_print "$errorlist" "${color[error]}" \
"files had decryption errors" "file had decryption errors"
fi
if [[ "$_exit" -eq 0 && $quiet -lt 2 ]]; then
color_print "${color[ok]}" "No problems detected."
printf "\n\n"
fi
exit $_exit
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment