#!/bin/sh set -e export NOTIFY_LONG=vault export NOTIFY_SHORT="${0##*/}" export TEXTDOMAIN=vlt export TEXTDOMAINDIR=/usr/local/share/locale . gettext.sh _() { fmt="$1" shift printf "$(eval_gettext "$fmt")" "$@" } 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() { # TODO: Translate into Swedish cat <<-EOF >&2 Usage: ${0##*/} add [-c] ${0##*/} edit [-c] ${0##*/} get ${0##*/} rm [-c] EOF exit 1 } xecho() { printf '%s' "$@" } prompt() { if [ -t 2 ] then printf '%s: ' "$1" read -r s else s="$(fuzzel -d -l0 -P0 --prompt="$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="$(fuzzel -d -l0 -P0 --password --prompt="$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 ‘%s’ already exists" "$n")" 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 ‘%s’ was added with the digit length ‘%d’ and period ‘%d’" "$n" "$d" "$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 ‘%s’ was added to the category ‘%s’" "$n" "$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" "$s")" exit 1 } xecho "$data" \ | jq --arg s "$s" '. + {($s): {}}' \ | enchive archive >"$VAULT" [ ! -t 2 ] && \ notify "$(_ 'Category created')" "$(_ "The password category ‘%s’ was created" "$s")" } 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 ‘%s’ was copied to the clipboard" "$o")" 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 ‘%s’ was copied to the clipboard" "$o")" 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 "$(_ 'Password removed')" \ "$(_ "The password ‘%s’ was removed from the category ‘%s’" "$n" "$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 ‘%s’ is not empty" "$c")" exit 1 } xecho "$data" \ | jq --arg c "$c" 'del(.[$c])' \ | enchive archive >"$VAULT" [ ! -t 2 ] && notify "$(_ 'Category removed')" "$(_ "The category ‘%s’ was removed" "$c")" } 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)" n="$(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 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 ‘%s’ was added with the digit length ‘%d’ and period ‘%d’" "$n" "$d" "$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 edited')" \ "$(_ "The password ‘%s’ in the category ‘%s’ was edited" "$n" "$c")" 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 edited')" "$(_ "The category ‘%s’ was renamed" "$c")" } raw() { shift enchive extract <"$VAULT" \ | jq --arg c "$1" --arg n "$2" -r '.[$c] | .[$n]' } : ${VAULT_2FA:="2fa"} : ${VAULT_HOME:=${XDG_DATA_HOME:-$HOME/.local/share}/vault} readonly VAULT="${VAULT_HOME}/vault.sec" [ $# -eq 0 ] && usage case "$1" in add) add "$@" ;; edit) edit "$@" ;; get) get ;; raw) raw "$@" ;; rm) rm_ "$@" ;; *) usage ;; esac