Skip to content

Instantly share code, notes, and snippets.

@Birch-san
Last active March 19, 2024 09:38
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Birch-san/a027bf3ba6327371dffca8a491ddec5d to your computer and use it in GitHub Desktop.
Save Birch-san/a027bf3ba6327371dffca8a491ddec5d to your computer and use it in GitHub Desktop.
Detect whether any password in your KeePassXC database was exposed in a data breach (using Troy Hunt's Pwned Passwords API)
#!/usr/bin/env bash
# Licensed by author Alex Birch under CC BY-SA 4.0
# https://creativecommons.org/licenses/by-sa/4.0/
# detects whether any of your passwords have been exposed in a data breach, by
# submitting (prefixes of hashes of) all your passwords to Troy Hunt's
# Pwned Passwords API.
# https://haveibeenpwned.com/Passwords
# usage:
# ./pwnedpass.sh keepassxc_exported_password_database.csv
# dependencies:
# brew install csvkit jq
# example output:
: "
LINE '1': SEVERITY='117316'; TITLE='Sample Entry'; USERNAME='User Name'; PASSWORD='Password'
LINE '2': SEVERITY='2333232'; TITLE='Sample Entry 2'; USERNAME='Michael321'; PASSWORD='12345'
LINE '3': safe!
LINE '4': skipped (empty)
"
# SEV is how many times that particular password has been exposed in a data breach.
# example contents of a compatible CSV:
: '
"Group","Title","Username","Password","URL","Notes"
"Keepass2","Sample Entry","User Name","Password","http://keepass.info/","Notes"
"Keepass2","Sample Entry 2","Michael321","12345","http://keepass.info/help/kb/testform.html",""
'
# if you are creating the CSV through some obscure method: be careful to adhere properly to the CSV
# format; passwords pose lots of text-escaping challenges.
# I use sandbox-exec in front of invocations of csvjson and jq
# since I trust them less than the utilities that ship with macOS.
# https://reverse.put.as/wp-content/uploads/2011/09/Apple-Sandbox-Guide-v1.0.pdf
# if you are on a different OS and less cautious, you could remove the sandbox-exec.
# here's a one-liner to lookup a pwned password:
# PASS='monkey' && SHA1="$(echo -n "$PASS" | shasum | cut -b 1-40)" && curl -s "https://api.pwnedpasswords.com/range/$(echo -n "$SHA1" | cut -b 1-5)" | grep -i "$(echo -n $SHA1 | cut -b 6-)"
set -eo pipefail
function pwned() {
local LINENUM="$1"
local TITLE="$2"
local USERNAME="$3"
local PASSWORD="$4"
# echo "LINENUM='$LINENUM'; TITLE='$TITLE'; USERNAME='$USERNAME'; PASSWORD='$PASSWORD'"
if [ -z "$PASSWORD" ] \
|| [ "$PASSWORD" == 'null' ]; then
echo "LINE '$LINENUM': skipped (empty)" >&2
return
fi
local SHA1="$(echo -n "$PASSWORD" | shasum | cut -b 1-40)"
local PREFIX="$(echo "$SHA1" | cut -b 1-5)"
local SUFFIX="$(echo "$SHA1" | cut -b 6-)"
local RESULTS="$(curl -sf "https://api.pwnedpasswords.com/range/$PREFIX")"
local MATCH="$(echo "$RESULTS" \
| grep -im 1 "$SUFFIX")"
if [ -z "$MATCH" ]; then
echo "LINE '$LINENUM': safe!" >&2
else
local MATCH="$(grep -im 1 "$SUFFIX" <<< "$RESULTS")"
local SEVERITY="$(echo "$MATCH" | awk -F: '{ print $2 }' | sed 's/[^0-9]*//g')"
echo "LINE '$LINENUM': SEVERITY='$SEVERITY'; TITLE='$TITLE'; USERNAME='$USERNAME'; PASSWORD='$PASSWORD'"
fi
}
while read -r line; do
TITLE="$(echo "$line" \
| sandbox-exec 2>/dev/null -n no-internet jq -r '.Title')"
USERNAME="$(echo "$line" \
| sandbox-exec 2>/dev/null -n no-internet jq -r '.Username')"
PASSWORD="$(echo "$line" \
| sandbox-exec 2>/dev/null -n no-internet jq -r '.Password')"
LINENUM="$(echo "$line" \
| sandbox-exec 2>/dev/null -n no-internet jq -r '.line_number')"
pwned "$LINENUM" "$TITLE" "$USERNAME" "$PASSWORD"
done < <(sandbox-exec 2>/dev/null -n no-internet csvcut -l -c Title,Username,Password "${1:-/dev/stdin}" \
| sandbox-exec 2>/dev/null -n no-internet csvjson \
| sandbox-exec 2>/dev/null -n no-internet jq -c '.[]' )
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment