#!/bin/sh fname=$(basename "$0") usage() { echo "$fname [Global Operations]: list-files List eligible files in data path. Shortcut 'lsf' cache-clear Delete all cached keys. Shortcut 'cc' help Display help [File Operations]: ls [path] List contents at given path tree List all contents create Create a file or change key get Get the value of target copy Copy the target value to clipboard. Shortcut 'x' set Set the value of target new [length] Generate a random password at target rm Delete the target [Config]: *Variable* *Default value* *Description* ---------------------------------------------------------------------------- CONFIGFILE '\$XDG_CONFIG_HOME/zpass/defaut.conf' Path to the config file to load ZPASS_PATH '\$XDG_DATA_HOME/zpass' Folder containing password files ZPASS_CACHE_PATH '\$XDG_CACHE_HOME/zpass' Path used for caching keys ZPASS_FILE 'default' File to use for operations ZPASS_KEY Key to use for encrypting/decrypting files ZPASS_KEY_CACHE_TIME '1' Time a key stays in cache for decrypting, in minutes ZPASS_CLIPBOARD_TIME '30' Time until clipboard gets cleared after copy, in seconds ZPASS_UNK_OP_CALL 'copy' Operation to call on unrecognized first argument All operations can be shortened to their first char unless specified Unknown first argument will perform the operation described in 'ZPASS_UNK_OP_CALL' on that argument " } # XDG config datapath="$HOME/.local/share/zpass" cachepath="$HOME/.cache/zpass" configpath="$HOME/.config/zpass" [ -n "$XDG_DATA_HOME" ] && datapath="$XDG_DATA_HOME/zpass" [ -n "$XDG_CONFIG_HOME" ] && configpath="$XDG_CONFIG_HOME/zpass" [ -n "$XDG_CACHE_HOME" ] && cachepath="$XDG_CACHE_HOME/zpass" [ -z "$CONFIGFILE" ] && CONFIGFILE="$configpath/default.conf" # stash env config ZPASS_PATH_T=$ZPASS_PATH ZPASS_CACHE_PATH_T=$ZPASS_CACHE_PATH ZPASS_FILE_T=$ZPASS_FILE ZPASS_EXTENSION_T=$ZPASS_EXTENSION ZPASS_KEY_CACHE_TIME_T=$ZPASS_KEY_CACHE_TIME ZPASS_CLIPBOARD_TIME_T=$ZPASS_CLIPBOARD_TIME ZPASS_UNK_OP_CALL_T=$ZPASS_UNK_OP_CALL # load config file [ -f "$CONFIGFILE" ] && { . "$CONFIGFILE" || exit $? ; } # insert env config [ -n "$ZPASS_PATH_T" ] && ZPASS_PATH=$ZPASS_PATH_T [ -n "$ZPASS_CACHE_PATH_T" ] && ZPASS_CACHE_PATH=$ZPASS_CACHE_PATH_T [ -n "$ZPASS_FILE_T" ] && ZPASS_FILE=$ZPASS_FILE_T [ -n "$ZPASS_EXTENSION_T" ] && ZPASS_EXTENSION=$ZPASS_EXTENSION_T [ -n "$ZPASS_KEY_CACHE_TIME_T" ] && ZPASS_KEY_CACHE_TIME=$ZPASS_KEY_CACHE_TIME_T [ -n "$ZPASS_CLIPBOARD_TIME_T" ] && ZPASS_CLIPBOARD_TIME=$ZPASS_CLIPBOARD_TIME_T [ -n "$ZPASS_UNK_OP_CALL_T" ] && ZPASS_UNK_OP_CALL=$ZPASS_UNK_OP_CALL_T # resolve zpass_path [ -n "$ZPASS_PATH" ] && datapath="$ZPASS_PATH" [ -n "$ZPASS_CACHE_PATH" ] && cachepath="$ZPASS_CACHE_PATH" # default ZPASS config [ -z "$ZPASS_FILE" ] && ZPASS_FILE=default [ -z "$ZPASS_EXTENSION" ] && ZPASS_EXTENSION=.tar.gpg [ -z "$ZPASS_KEY_CACHE_TIME" ] && ZPASS_KEY_CACHE_TIME=1 # in minutes [ -z "$ZPASS_CLIPBOARD_TIME" ] && ZPASS_CLIPBOARD_TIME=30 # in seconds [ -z "$ZPASS_UNK_OP_CALL" ] && ZPASS_UNK_OP_CALL=copy file="$datapath/$ZPASS_FILE$ZPASS_EXTENSION" mkdir -p "$datapath" 2>/dev/null || error 1 "Could not create '$datapath'" mkdir -p "$cachepath" 2>/dev/null && chmod -R go-rwx "$cachepath" 2>/dev/null escape_chars() { echo "$*" | sed 's|\.|\\\.|g;s|/|\\/|g' } error(){ ret=$1 && shift 1 && echo $* >&2 && exit $ret } randalnum() { tr -cd '[a-zA-Z]' < /dev/urandom | head -c $1 } randpass() { len=$1 [ -z "$len" ] && len=20 tr -cd 'a-zA-Z0-9!-.' < /dev/urandom | head -c $len } keyfile(){ printf "%s.key" "$(echo "$file" | md5sum | cut -d' ' -f1)" } # $1 = delay in sec clipboard_clear() { if [ -n "$1" ] then for I in $(screen -ls | grep "zpass_clipboard" | awk '{print $1}') do screen -S "$I" -X stuff "^C" done screen -dmS "zpass_clipboard" sh -c "sleep $1 ; printf '' | xclip -selection clipboard ; sleep 1" # call zpass for autoclean else xclip -selection clipboard < /dev/null fi } # $1 = delay in min delete_cache() { if [ "$1" -gt 0 ] 2>/dev/null then for I in $(screen -ls | grep "zpass_$(keyfile)" | awk '{print $1}') do screen -S "$I" -X stuff "^C" done screen -dmS "zpass_$(keyfile)" sh -c "sleep $(($1*60)) ; $0" # call zpass for autoclean else rm -f "$cachepath/$(keyfile)" 2>/dev/null fi } clean_cache() { # key cache find "$cachepath" -type f -mmin +"$ZPASS_KEY_CACHE_TIME" -exec rm '{}' ';' # tmp folders older than 1 min rm -rd $(find /tmp -maxdepth 1 -type d -name "zpass_*" ! -mmin 1) } clear_cache() { rm "$cachepath/*" } write_cache() { echo "$1" > "$cachepath/$(keyfile)" delete_cache $ZPASS_KEY_CACHE_TIME } get_key_cached() { cat "$cachepath/$(keyfile)" 2>/dev/null } # $1 = prompt message prompt_password() { if [ -n "$DISPLAY" ] then if which kdialog >/dev/null 2>&2 then kdialog --title "zpass" --password "$1" 2>/dev/null else zenity --title "zpass" --password 2>/dev/null fi else printf "%s:" "$1" >&2 stty -echo read -r PASSWORD || return $? stty echo printf "\n" >&2 echo $PASSWORD fi } # $1 = message error_dialog() { if which kdialog >/dev/null 2>&2 then kdialog --title "zpass" --error "$1" 2>/dev/null else zenity --title "zpass" --error --text="$1" 2>/dev/null fi } new_key_with_confirm() { [ -n "$ZPASS_KEY" ] && echo "$ZPASS_KEY" && return 0 pass1=1 pass2=2 while [ "$pass1" != "$pass2" ] do pass1=$(prompt_password "Enter new key") || error 100 "Cancelled" pass2=$(prompt_password "Confirm key") || error 100 "Cancelled" [ "$pass1" != "$pass2" ] && error_dialog "Passwords do not match.\nTry again" done write_cache "$pass1" & echo "$pass1" } # $1 = prompt message get_key() { message="Enter key" [ -n "$1" ] && message="$1" key=$(prompt_password "$message") || return $? write_cache "$key" & echo "$key" } # $1 = key , $2 = keyfile to write decrypt_with_key() { gpg --pinentry-mode loopback --batch --passphrase "$1" -o - -d "$file" 2>/dev/null && ret=$? && [ -n "$2" ] && echo "$1" > "$2" return $ret } # $1 = keyfile to write decrypt() { [ ! -f "$file" ] && return 1 # no file if [ -n "$ZPASS_KEY" ] then # key given already decrypt_with_key "$ZPASS_KEY" "$1" ; ret=$? else # prompt for key # attempt decrypt from cache key=$(get_key_cached) && decrypt_with_key "$key" "$1" ret=$? if [ $ret -ne 0 ] then # cache was incorrect: delete delete_cache >/dev/null 2>&1 # try loop tries=0 while [ $ret -ne 0 ] && [ $tries -lt 3 ] do key=$(get_key) || { echo "Cancelled" >&2 && return 100 ; } tries=$((tries+1)) decrypt_with_key "$key" "$1" ; ret=$? done fi fi [ $ret -ne 0 ] && { echo "Could not decrypt '$file'" >&2 && return $ret ; } } # $1 = key encrypt() { gpg --pinentry-mode loopback --batch --passphrase "$1" -o - -c - } # $1 = tmpdir , $2 = keyfile unpack(){ rm -rf "$1" || return $? mkdir -p "$1" || return $? ( cd "$1" { decrypt "$2" || return $?; } | tar -xf - 2>/dev/null || return $? ) } # $1 = tmpdir , $2 = keyfile pack() { # clean empty dirs archive="archive_$(randalnum 5)" find "$1" -empty -type d -delete ( cd "$1" if [ -n "$2" ] then key=$(cat "$2") && rm "$2" || return $? else key=$(new_key_with_confirm) || return $? fi tar -cf - * | encrypt "$key" > "$1/$archive" || return $? ) || return $? mv -f "$1/$archive" "$file" } # $1 = file , $2 = content setval() { # tmp files tmpdir="/tmp/zpass_$(randalnum 5)" keyfile="$tmpdir/$(randalnum 5).key" # operation unpack "$tmpdir" "$keyfile" || return $? [ -n $(dirname "$1") ] && mkdir -p "$tmpdir/$(dirname "$1")" 2>/dev/null # create dir echo "$2" > "$tmpdir/$1" || return $? # set target data pack "$tmpdir" "$keyfile" || return $? rm -rf "$tmpdir" } remove() { # tmp files tmpdir="/tmp/zpass_$(randalnum 5)" keyfile="$tmpdir/$(randalnum 5).key" # operation unpack "$tmpdir" "$keyfile" || return $? rm "$tmpdir/$1" # delete target pack "$tmpdir" "$keyfile" || return $? rm -rf "$tmpdir" } create() { if [ -f "$file" ] then tmpdir="/tmp/zpass_$(randalnum 5)" # pack n repack with no tmp key: create new unpack "$tmpdir" || return $? pack "$tmpdir" || return $? rm -rf "$tmpdir" else [ -z "$ZPASS_KEY" ] && ZPASS_KEY=$(new_key_with_confirm) || { echo "Cancelled" >&2 && return 100 ; } tar -cf - -T /dev/null | encrypt "$ZPASS_KEY" 2>/dev/null > "$file" || { echo "Encryption error" >&2 && return 1 ; } fi } list() { tree=$(decrypt | tar -tf - 2>/dev/null) || return $? ret="$tree" if [ -n "$1" ] then # file ret=$(echo "$tree" | sed -n "/\/$/d;/^$(escape_chars "$1")\$/p") # folder [ -z "$ret" ] && in=$(echo "$1/" | tr -s '/') && ret=$(echo "$tree" | grep "^$in" | sed "s|^$in||g") fi echo "$ret" | sed "/\/..*/d;/^\$/d" } tree() { tree=$(decrypt | tar -tf - 2>/dev/null) || return $? echo "$tree" | sed '/\/$/d;/^$/d' } get(){ { decrypt || return $?; } | tar -xOf - "$1" 2>/dev/null } copy() { { get "$1" || return $?; } | tr -d '\n' | xclip -selection clipboard && clipboard_clear "$ZPASS_CLIPBOARD_TIME" } clean_cache 2>/dev/null [ -z "$1" ] && usage && return 1 case $1 in lsf|list-files) ls "$datapath" 2>/dev/null | grep "$ZPASS_EXTENSION\$" | sed "s/$(escape_chars "$ZPASS_EXTENSION")\$//g" ;; cc|cache-clear) clear_cache 2>/dev/null ;; l|ls|list) list $2 ;; t|tree) tree ;; c|create) create ;; g|get) get "$2" ;; s|set) setval "$2" "$3" ;; n|new) setval "$2" "$(randpass $3)" ;; r|rm) remove "$2" "$3" ;; x|copy|clipboard) copy "$2" ;; -h|h|help) usage ;; *) [ -n "$ZPASS_UNK_OP_CALL" ] && "$0" $ZPASS_UNK_OP_CALL "$@" ;; esac