From 9aaf8280c5c65478c9b0d600d9dd6cd049187f56 Mon Sep 17 00:00:00 2001
From: Thomas Voss <mail@thomasvoss.com>
Date: Thu, 24 Aug 2023 21:07:26 +0300
Subject: vlt: Majorly overhaul ‘vlt’
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

With this commit, vault (or ‘vlt’) is now a proper fully useable
password manager that supports password categorization, -adding,
-editing, -deleting, and -fetching with support for both terminal and
GUI usage.  The code isn’t even as bad as I would have expected for a
first commit!
---
 .local/bin/vlt | 319 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 306 insertions(+), 13 deletions(-)

(limited to '.local/bin/vlt')

diff --git a/.local/bin/vlt b/.local/bin/vlt
index 5ef64ce..4121207 100755
--- a/.local/bin/vlt
+++ b/.local/bin/vlt
@@ -2,20 +2,313 @@
 
 set -e
 
-[ $# -ne 0 ] && {
-	echo "Usage: ${0##*/}" >&2
+sanitize()
+{
+	s="${1#"${1%%[![:space:]]*}"}"
+	s="${s%"${s##*[![:space:]]}"}"
+	if [ "${#s}" -eq 0 ]
+	then
+		notify 'Invalid Input' 'Empty strings do not constitute valid input'
+		exit 1
+	fi
+}
+
+usage()
+{
+	echo "Usage: ${0##*/} add [-c]"  >&2
+	echo "       ${0##*/} edit [-c]" >&2
+	echo "       ${0##*/} get"       >&2
+	echo "       ${0##*/} rm [-c]"   >&2
 	exit 1
 }
 
-: ${vault:="${XDG_DATA_HOME:-$HOME/.local/share}/vault"}
-[ -d "$vault" ] || mkdir -p "$vault"
-cd "$vault"
+xecho()
+{
+	printf '%s' "$@"
+}
+
+notify()
+{
+	if [ -t 2 ]
+	then
+		printf "%s: %s\n" "${0##*/}" "$2" >&2
+	else
+		notify-send -a "${0##*/}" -u normal "$1" "$2."
+	fi
+}
+
+prompt()
+{
+	if [ -t 2 ]
+	then
+		printf '%s ' "$1"
+		read -r s
+	else
+		s="`zenity --title=zenity --entry --text="$1"`"
+	fi
+	sanitize "$s"
+}
+
+xprompt()
+{
+	if [ -t 2 ]
+	then
+		printf '%s ' "$1"
+		stty -echo
+		trap 'stty echo' INT
+		read -r s
+		stty echo
+		trap - INT
+		echo
+	else
+		s="`zenity --title=zenity --password --text="$1"`"
+	fi
+	sanitize "$s"
+}
+
+add()
+{
+	shift
+
+	while getopts 'c' opt
+	do
+		case $opt in
+		c)
+			add_c
+			exit 0
+			;;
+		*)
+			usage
+			;;
+		esac
+	done
+
+	readonly data="`enchive extract <"$VAULT"`"
+
+	c="`xecho "$data" | jq -r 'keys | .[]' | osel`"
+
+	prompt 'Password name:'
+	n="$s"
+
+	xecho "$data" \
+		| jq -e --arg c "$c" --arg n "$n" '.[$c] | has($n) | not' >/dev/null || {
+			notify 'Failed To Add Password' "The password ‘$n’ already exists"
+			exit 1
+		}
+
+	if [ "$VAULT_2FA" = "$c" ]
+	then
+		xprompt 'Secret key:'; k="$s"
+		prompt 'Digits:'; d="$s"
+		prompt 'Period:'
+
+		xecho "$data" \
+			| jq --arg c "$c" \
+			     --arg n "$n" \
+			     --arg s "$k" \
+			     --arg d "$d" \
+			     --arg p "$s" \
+				'.[$c] += {($n): {
+					"secret": $s,
+					"digits": ($d | tonumber),
+					"period": ($p | tonumber)
+				}}' \
+			| enchive archive >"$VAULT"
+		[ ! -t 2 ] && notify '2FA Key Added' \
+			"The 2FA key ‘$n’ was added with the digit length ‘$d’ and period ‘$p’"
+	else
+		xprompt 'Password:'
+
+		xecho "$data" \
+			| jq --arg c "$c" --arg n "$n" --arg s "$s" '.[$c] += {($n): $s}' \
+			| enchive archive >"$VAULT"
+		[ ! -t 2 ] && notify 'Password Added' \
+			"The password ‘$n’ was added to the category ‘$c’"
+	fi
+}
+
+add_c()
+{
+	readonly data="`enchive extract <"$VAULT"`"
+
+	prompt 'Category to create:'
+
+	xecho "$data" | jq -e --arg s "$s" 'has($s) | not' >/dev/null || {
+		notify 'Failed To Create Category' "The category ‘$s’ already exists"
+		exit 1
+	}
+
+	xecho "$data" \
+		| jq --arg s "$s" '. + {($s): {}}' \
+		| enchive archive >"$VAULT"
+	[ ! -t 2 ] && \
+		notify 'Category Created' "The password category ‘$s’ was created"
+}
+
+get()
+{
+	readonly data="`enchive extract <"$VAULT"`"
+
+	c="`xecho "$data" | jq -r 'keys | .[]' | osel`"
+	o="`xecho "$data" | jq -r --arg c "$c" '.[$c] | keys | .[]' | osel`"
+
+	xecho "$data" | if [ "$VAULT_2FA" = "$c" ]
+	then
+		eval "`jq -r --arg c "$c" --arg o "$o" '
+		   .[$c]
+		   | .[$o]
+		   | "totp -d"
+		   + (.digits | tostring)
+		   + " -p"
+		   + (.period | tostring)
+		   + " "
+		   + .secret
+		'`" \
+			| wl-copy -no \
+			&& [ ! -t 2 ] \
+			&& notify '2FA Code Copied To The Clipboard' \
+			"The 2FA code for ‘$o’ was copied to the clipboard"
+	else
+		jq -r --arg c "$c" --arg o "$o" '.[$c] | .[$o]' \
+			| wl-copy -no \
+			&& [ ! -t 2 ] \
+			&& notify 'Password Copied To The Clipboard' \
+			"The password for ‘$o’ was copied to the clipboard"
+	fi
+}
+
+rm_()
+{
+	shift
+
+	while getopts 'c' opt
+	do
+		case $opt in
+		c)
+			rm_c
+			exit 0
+			;;
+		*)
+			usage
+			;;
+		esac
+	done
+
+	readonly data="`enchive extract <"$VAULT"`"
+
+	c="`xecho "$data" | jq -r 'keys | .[]' | osel`"
+	n="`xecho "$data" | jq -r --arg c "$c" '.[$c] | keys | .[]' | osel`"
+
+	xecho "$data" \
+		| jq --arg c "$c" --arg n "$n" 'del(.[$c] | .[$n])' \
+		| enchive archive >"$VAULT"
+	[ ! -t 2 ] && notify 'Removed Password' \
+		"The password ‘$n’ was removed from the category ‘$c’"
+}
+
+rm_c()
+{
+	readonly data="`enchive extract <"$VAULT"`"
+
+	c="`xecho "$data" | jq -r 'keys | .[]' | osel`"
+
+	xecho "$data" \
+		| jq -e --arg c "$c" '.[$c] | length == 0' >/dev/null || {
+			notify 'Failed To Remove Category' "The category ‘$c’ is not empty"
+			exit 1
+		}
+
+	xecho "$data" \
+		| jq --arg c "$c" 'del(.[$c])' \
+		| enchive archive >"$VAULT"
+	[ ! -t 2 ] && notify 'Removed Category' "The category ‘$c’ was removed"
+}
+
+edit()
+{
+	shift
+
+	while getopts 'c' opt
+	do
+		case $opt in
+		c)
+			edit_c
+			exit 0
+			;;
+		*)
+			usage
+			;;
+		esac
+	done
+
+	readonly data="`enchive extract <"$VAULT"`"
+
+	c="`xecho "$data" | jq -r 'keys | .[]' | osel`"
+	o="`xecho "$data" | jq -r --arg c "$c" '.[$c] | keys | .[]' | osel`"
+
+	if [ "$VAULT_2FA" = "$c" ]
+	then
+		xprompt 'Secret key:'; k="$s"
+		prompt 'Digits:'; d="$s"
+		prompt 'Period:'
+
+		xecho "$data" \
+			| jq --arg c "$c" \
+			     --arg o "$o" \
+			     --arg s "$k" \
+			     --arg d "$d" \
+			     --arg p "$s" \
+				'.[$c] += {($n): {
+					"secret": $s,
+					"digits": ($d | tonumber),
+					"period": ($p | tonumber)
+				}}' \
+			| enchive archive >"$VAULT"
+		[ ! -t 2 ] && notify '2FA Key Added' \
+			"The 2FA key ‘$n’ was added with the digit length ‘$d’ and period ‘$p’"
+	else
+		xprompt 'Password:'
+		xecho "$data" \
+			| jq --arg c "$c" --arg n "$n" --arg s "$s" '.[$c] += {($n): $s}' \
+			| enchive archive >"$VAULT"
+		[ ! -t 2 ] && notify 'Password Edit' \
+			"The password ‘$n’ in the category ‘$c’ was changed"
+	fi
+}
+
+edit_c()
+{
+	readonly data="`enchive extract <"$VAULT"`"
+
+	c="`xecho "$data" | jq -r 'keys | .[]' | osel`"
+	prompt 'Category name:'
+	xecho "$data" \
+		| jq --arg o "$c" --arg n "$s" \
+			 'with_entries(if .key == $o then .key = $n else . end)' \
+		| enchive archive >"$VAULT"
+	[ ! -t 2 ] && notify 'Category Edit' "The category ‘$c’ was renamed"
+}
+
+: ${VAULT_2FA:="2fa"}
+: ${VAULT_HOME:=${XDG_DATA_HOME:-$HOME/.local/share}/vault}
+readonly VAULT="${VAULT_HOME}/vault.sec"
+
+[ $# -eq 0 ] && usage
 
-# We use ‘*’ instead of ‘.’ to avoid the leading ‘./’
-choice="`find * -type f | osel`"
-password="`enchive extract "$choice" /dev/stdout`"
-[ -n "$password" ] \
-	&& { printf '%s' "$password" | wl-copy -no; } \
-	&& notify-send -a "${0##*/}" -u normal \
-		'Password copied to the clipboard' \
-		"The password for ‘$choice’ was copied to the clipboard."
+case "$1" in
+add)
+	add "$@"
+	;;
+edit)
+	edit "$@"
+	;;
+get)
+	get
+	;;
+rm)
+	rm_ "$@"
+	;;
+*)
+	usage
+	;;
+esac
-- 
cgit v1.2.3