diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..12c1fbf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+Zmakefile
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..f19df4a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,20 @@
+
+var_exclude = ZPASS_.* XDG_.* REMOTE_.* DISPLAY CONFIGFILE TMPDIR
+fct_exclude = _tty_on
+
+zpass: src/*
+ lxsh -o zpass -m --minimize-var --exclude-var "$(var_exclude)" --minimize-fct --exclude-fct "$(fct_exclude)" --remove-unused src/main.sh
+
+debug: src/*
+ lxsh -o zpass src/main.sh
+
+build: zpass
+
+install: build
+ mv zpass /usr/local/bin
+
+uninstall:
+ rm /usr/local/bin/zpass
+
+clear:
+ rm zpass
diff --git a/README.md b/README.md
index 2a5d052..1180f94 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,83 @@
# zpass
-Basic and simple password management
+
+Basic and simple password management for UNIX using shell.
+Does not require any setup, only uses a password to encrypt the archive.
+
+Systems other than GNU/linux are untested at the moment
+
+# Installing
+
+## Dependencies
+
+Requires:
+- gpg
+- tar
+
+Optional:
+- screen (key caching and clipboard time)
+- openssh (for remote files)
+- zenity (GUI prompt)
+- kdialog (better GUI prompt in KDE)
+- xclip (copy on X)
+- wl-clipboard (copy on wayland)
+
+## Prebuilt
+
+From [zpkg](https://github.com/zawwz/zpkg) package repository
+
+## From source
+
+Requires [lxsh](https://github.com/zawwz/lxsh)
+
+Clone this repository then run `make install`
+
+# Use
+
+By design zpass uses encrypted archive files, wherein a file contains a value.
+You can use predefined operations, or perform custom executions inside the archive.
+
+See `zpass -h` for information on operations and configuration
+
+When using `get` or `copy`, if the path entered is a folder,
+zpass will look for a `default` file in this folder
+
+## Example use
+
+Create a file with `zpass c`.
+A prompt will appear to use a password to encrypt the password archive file.
+If you lose this password, you lose access to all contents of the archive.
+
+You can create new values with either `zpass add `, `zpass new `, or `zpass set `
+
+To copy a value into the clipboard, use `zpass ` or `zpass copy `
+
+## Configuration
+
+zpass will load by default the file `.config/zpass/default.conf` in your home directory
+
+### Configuring remote file
+
+You can configure zpass to use a file on a remote server.
+You need SSH access to the target machine.
+Here is an example configuration:
+```
+ZPASS_REMOTE_ADDR=user@example.com
+ZPASS_SSH_ID=~/.ssh/id_rsa
+```
+
+### Making the cache volatile
+
+If you are caching keys, by default zpass uses `~/.cache` as a caching path.
+This can be troublesome in case the machine stops before the cache timer runs out,
+leaving a file containing the key in plaintext.
+This can be fixed by pointing the cache path to a volatile filesystem.
+For example:
+```
+ZPASS_CACHE_PATH=/tmp/zpasscache
+```
+
+# Troubleshooting
+
+### Prompt keeps appearing even with correct password
+
+Make sure your gpg configuration is correct, you can run `gpg -c < /dev/null` to check
diff --git a/src/archive.sh b/src/archive.sh
new file mode 100644
index 0000000..45f423d
--- /dev/null
+++ b/src/archive.sh
@@ -0,0 +1,101 @@
+#!/bin/sh
+
+# $1 = tmpdir , $2 = keyfile
+unpack() {
+ rm -rf "$1" || return $?
+ mkdir -p "$1" || return $?
+ (
+ set -e
+ cd "$1"
+ decrypt "$2" | tar -xf - 2>/dev/null
+ )
+}
+
+# $1 = tmpdir , $2 = keyfile
+pack()
+{
+ # clean empty dirs
+ archive="archive_$(randalnum 5)"
+ (
+ cd "$1" || exit $?
+ if [ -n "$2" ]
+ then
+ key=$(cat "$2") && rm "$2" || exit $?
+ else
+ key=$(new_key_with_confirm) || exit $?
+ fi
+ tar -cf - -- * | encrypt "$key" > "$1/$archive" || exit $?
+ ) || return $?
+ if [ -n "$ZPASS_REMOTE_ADDR" ]
+ then
+ [ -z "$ZPASS_PATH" ] && datapath="~/.local/share/zpass"
+ if [ -n "$ZPASS_SSH_ID" ]
+ then scp -i "$ZPASS_SSH_ID" "$1/$archive" "$ZPASS_REMOTE_ADDR:$datapath/$ZPASS_FILE$ZPASS_EXTENSION" >/dev/null || return $?
+ else scp "$1/$archive" "$ZPASS_REMOTE_ADDR:$datapath/$ZPASS_FILE$ZPASS_EXTENSION" >/dev/null || return $?
+ fi
+ rm -f "$1/$archive" 2>/dev/null
+ return 0
+ else
+ mv -f "$1/$archive" "$file"
+ fi
+}
+
+# $@ = command to execute inside archive
+# set env __NOPACK to not repack after command
+archive_exec()
+{
+ err=0
+ # tmp files
+ tmpdir="$TMPDIR/zpass_$(randalnum 5)"
+ keyfile="$tmpdir/$(randalnum 5).key"
+ # operation
+ (
+ # unpack
+ unpack "$tmpdir/archive" "$keyfile" || exit $?
+ # execute
+ (cd "$tmpdir/archive" && "$@") || exit $?
+ # repack
+ [ -z "$__NOPACK" ] && { pack "$tmpdir/archive" "$keyfile" || exit $?; }
+ exit 0
+ ) || err=$?
+ # cleanup
+ rm -rf "$tmpdir"
+ return $err
+}
+
+# no argument
+create() {
+ if [ -f "$file" ]
+ then
+ tmpdir="$TMPDIR/zpass_$(randalnum 5)"
+ # pack n repack with no tmp key: create new
+ unpack "$tmpdir" || return $?
+ pack "$tmpdir" || { echo "Encryption error" >&2 && return 1 ; }
+ rm -rf "$tmpdir"
+ else
+ # if remote: file tmp
+ [ -n "$ZPASS_REMOTE_ADDR" ] && {
+ file="$TMPDIR/zpass_$(filehash)$ZPASS_EXTENSION"
+ }
+ # get key
+ [ -z "$ZPASS_KEY" ] && {
+ ZPASS_KEY=$(new_key_with_confirm) || { echo "Cancelled" >&2 && return 100 ; }
+ }
+ # create archive
+ tar -cf - -T /dev/null | encrypt "$ZPASS_KEY" > "$file" || {
+ echo "Encryption error" >&2
+ # echo "$tmperr" >&2
+ rm "$file"
+ return 1
+ }
+ [ -n "$ZPASS_REMOTE_ADDR" ] && {
+ ssh "$ZPASS_REMOTE_ADDR" "mkdir -p '$datapath'"
+ if [ -n "$ZPASS_SSH_ID" ]
+ then scp -i "$ZPASS_SSH_ID" "$file" "$ZPASS_REMOTE_ADDR:$datapath/$ZPASS_FILE$ZPASS_EXTENSION" >/dev/null || return $?
+ else scp "$file" "$ZPASS_REMOTE_ADDR:$datapath/$ZPASS_FILE$ZPASS_EXTENSION" >/dev/null || return $?
+ fi
+ rm -rf "$file" 2>/dev/null
+ }
+ fi
+ return 0
+}
diff --git a/src/cache.sh b/src/cache.sh
new file mode 100644
index 0000000..bf67c77
--- /dev/null
+++ b/src/cache.sh
@@ -0,0 +1,38 @@
+#!/bin/sh
+
+## Cache functions
+
+clear_cache() {
+ rm "$cachepath"/*
+}
+
+write_cache() {
+ echo "$1" > "$cachepath/$(keyfile)"
+ delete_cache "$ZPASS_KEY_CACHE_TIME"
+}
+
+get_key_cached() {
+ [ ! -f "$file" ] && return 0
+ cat "$cachepath/$(keyfile)" 2>/dev/null
+}
+
+# $1 = delay in sec
+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 ; $0 rmc" # call zpass with cache delete
+ else
+ rm -f "$cachepath/$(keyfile)" 2>/dev/null
+ fi
+}
+
+clean_cache() {
+ # key cache
+ find "$cachepath" -type f ! -newermt @$(date -d "-$ZPASS_KEY_CACHE_TIME seconds" +%s) -print0 | xargs -0 rm
+ # tmp folders older than 5 min
+ rm -rd $(find "$TMPDIR" -maxdepth 1 -type d -name 'zpass_*' ! -mmin 5)
+}
diff --git a/src/clipboard.sh b/src/clipboard.sh
new file mode 100644
index 0000000..8db7bf8
--- /dev/null
+++ b/src/clipboard.sh
@@ -0,0 +1,45 @@
+#!/bin/sh
+
+clipboard()
+{
+ unset in
+ read -r in
+ while read -r ln
+ do
+ in="$in
+$ln"
+ done
+ printf "%s" "$in" | xclip -selection clipboard
+ which wl-copy >/dev/null 2>&1 && printf "%s" "$in" | wl-copy
+}
+
+# $1 = delay in sec
+clipboard_clear() {
+ if [ -n "$1" ]
+ then
+ for I in $(screen -ls | grep "$fname"_clipboard | awk '{print $1}')
+ do
+ screen -S "$I" -X stuff "^C"
+ done
+ screen -dmS "$fname"_clipboard sh -c "sleep $1
+ xclip -selection clipboard < /dev/null
+ which wl-copy 2>&1 && wl-copy < /dev/null
+ sleep 1"
+ else
+ echo | clipboard
+ fi
+}
+
+copy_check()
+{
+ if ps -e | grep -qi wayland
+ then
+ which wl-copy >/dev/null 2>&1 || error 1 "ERROR: running wayland but wl-clipboard is not installed"
+ elif [ -n "$DISPLAY" ]
+ then
+ which xclip >/dev/null 2>&1 || error 1 "ERROR: running X but xclip is not installed"
+ else
+ error 1 "ERROR: no graphical server detected"
+ fi
+ return 0
+}
diff --git a/src/config.sh b/src/config.sh
new file mode 100644
index 0000000..8998f10
--- /dev/null
+++ b/src/config.sh
@@ -0,0 +1,46 @@
+#!/bin/sh
+
+# XDG config/cache
+datapath=".local/share/zpass"
+cachepath="$HOME/.cache/zpass"
+configpath="$HOME/.config/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"
+[ -n "$XDG_DATA_HOME" ] && [ -z "$ZPASS_REMOTE_ADDR" ] && datapath="$XDG_DATA_HOME/zpass"
+
+[ -z "$TMPDIR" ] && TMPDIR=/tmp
+
+# stash env config
+tmpenv="$TMPDIR/zpassenv_$(randalnum 5)"
+env | grep '^ZPASS_.*=' | sed "s/'/'\\\''/g;s/=/='/;s/$/'/g" > "$tmpenv"
+
+# load config file
+[ -f "$CONFIGFILE" ] && { . "$CONFIGFILE" || exit $? ; }
+
+. "$tmpenv" || exit $?
+rm -f "$tmpenv" 2>/dev/null
+
+# 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=60 # in seconds
+[ -z "$ZPASS_CLIPBOARD_TIME" ] && ZPASS_CLIPBOARD_TIME=30 # in seconds
+[ -z "$ZPASS_UNK_OP_CALL" ] && ZPASS_UNK_OP_CALL=copy
+[ -z "$ZPASS_RAND_LEN" ] && ZPASS_RAND_LEN=20
+
+# datapath resolution
+# remove tildes
+datapath="${datapath#\~/}"
+[ "$datapath" = '~' ] && datapath=""
+# if not remote and not absolute: add HOME
+[ -z "$ZPASS_REMOTE_ADDR" ] && [ "$(echo "$datapath" | cut -c1)" != '/' ] && datapath="$HOME/$datapath"
+
+file="$datapath/$ZPASS_FILE$ZPASS_EXTENSION"
+
+[ -z "$ZPASS_REMOTE_ADDR" ] && { 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
diff --git a/src/crypt.sh b/src/crypt.sh
new file mode 100644
index 0000000..92f7f20
--- /dev/null
+++ b/src/crypt.sh
@@ -0,0 +1,56 @@
+#!/bin/sh
+
+# $1 = key
+encrypt() {
+ gpg --pinentry-mode loopback --batch --passphrase "$1" -o - -c -
+}
+
+# $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()
+{
+ # get remote file
+ [ -n "$ZPASS_REMOTE_ADDR" ] && {
+ file="$TMPDIR/zpass_$(filehash)$ZPASS_EXTENSION"
+ [ -z "$ZPASS_PATH" ] && datapath="~/.local/share/zpass"
+ if [ -n "$ZPASS_SSH_ID" ]
+ then scp -i "$ZPASS_SSH_ID" "$ZPASS_REMOTE_ADDR:$datapath/$ZPASS_FILE$ZPASS_EXTENSION" "$file" >/dev/null || return $?
+ else scp "$ZPASS_REMOTE_ADDR:$datapath/$ZPASS_FILE$ZPASS_EXTENSION" "$file" >/dev/null || return $?
+ fi
+ }
+ cat "$file" >/dev/null 2>&1 || { echo "File doesn't exist. Use 'zpass create' to create the file" >&2 && 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=$(ask_key) || { echo "Cancelled" >&2 && return 100 ; }
+ tries=$((tries+1))
+ decrypt_with_key "$key" "$1" ; ret=$?
+ done
+ fi
+ fi
+
+ # remove temporary file
+ [ -n "$ZPASS_REMOTE_ADDR" ] && rm -rf "$file" 2>/dev/null
+
+ [ $ret -ne 0 ] && { echo "Could not decrypt '$file'" >&2 ; }
+ return $ret
+}
diff --git a/src/file.sh b/src/file.sh
new file mode 100644
index 0000000..fad9fb7
--- /dev/null
+++ b/src/file.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+list_files() {
+ _cmd_ "cd '$datapath' 2>/dev/null && find . -maxdepth 1 -type f -regex '.*$ZPASS_EXTENSION\$'" | sed 's/$(escape_chars "$ZPASS_EXTENSION")\$//g; s|.*/||g'
+}
+
+remove_files()
+{
+ for N
+ do
+ _cmd_ "rm '$datapath/$N$ZPASS_EXTENSION'" || exit $?
+ done
+}
diff --git a/src/help.sh b/src/help.sh
new file mode 100644
index 0000000..b69b9ff
--- /dev/null
+++ b/src/help.sh
@@ -0,0 +1,46 @@
+#!/bin/sh
+
+fname=$(basename "$0")
+usage()
+{
+ echo "$fname [options]
+[Global Operations]:
+ list-files List eligible files in data path. Shortcut 'lsf'
+ cache-clear Delete all cached keys. Shortcut 'cc'
+ help Display help
+ rm-file Remove files. Shortcut 'rmf'
+[File Operations]:
+ ls [path] List contents at given path
+ tree List all contents
+ create Create a file or change key
+ get Get values of targets
+ copy Copy the target value to clipboard. Shortcut 'x'
+ set Set the value of target
+ add Prompt for input value on paths
+ new Generate a random password at target
+ rm Delete targets
+ mv Move targets
+ exec Execute the following command inside the archive.
+ cached Returns wether or not a key is currently cached. Shortcut 'ch'
+ rm-cache Delete the cached key for this file. Shortcut 'rmc'
+
+[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 '60' Time a key stays in cache for decrypting, in seconds
+ 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
+ ZPASS_RAND_LEN Length of random passwords generated by 'new'
+ ZPASS_REMOTE_ADDR SSH server the file is on
+ ZPASS_REMOTE_PORT '22' SSH server port
+ ZPASS_SSH_ID SSH private key to use
+
+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
+"
+}
diff --git a/src/main.sh b/src/main.sh
new file mode 100644
index 0000000..2319a90
--- /dev/null
+++ b/src/main.sh
@@ -0,0 +1,34 @@
+#!/bin/lxsh
+
+%include util.sh config.sh *.sh
+
+## pre exec
+
+clean_cache 2>/dev/null
+[ $# -lt 1 ] && usage && return 1
+
+arg=$1
+shift 1
+
+## exec
+
+case $arg in
+ -h|h|help) usage && exit 1;;
+ lsf|list-files) list_files ;;
+ rmf|rm-file) remove_files "$@" ;;
+ cc|cache-clear) clear_cache 2>/dev/null ;;
+ ch|cached) get_key_cached >/dev/null 2>&1 ;;
+ rmc|rm-cache) delete_cache 0 >/dev/null 2>&1 ;;
+ c|create) create ;;
+ t|tree) _tree "$@" ;;
+ s|set) _set "$@" ;;
+ a|add) add "$@" ;;
+ n|new) new "$@" ;;
+ g|get) get "$@" ;;
+ x|copy) copy "$1" ;;
+ e|exec) archive_exec "$@" ;;
+ l|ls|list) sanitize_paths "$@" && __NOPACK=y archive_exec ls -Apw1 -- "$@" ;;
+ r|rm) sanitize_paths "$@" && archive_exec rm -rf -- "$@" ;;
+ m|mv) sanitize_paths "$@" && archive_exec mv -f -- "$@" ;;
+ *) [ -n "$ZPASS_UNK_OP_CALL" ] && "$0" $ZPASS_UNK_OP_CALL "$arg" "$@" ;;
+esac
diff --git a/src/operation.sh b/src/operation.sh
new file mode 100644
index 0000000..3aec44e
--- /dev/null
+++ b/src/operation.sh
@@ -0,0 +1,75 @@
+#!/bin/sh
+
+# $@ = paths
+_tree()
+{
+ if [ $# -gt 0 ]
+ then
+ fulltree=$(decrypt | tar -tf - 2>/dev/null) || exit $?;
+
+ for N
+ do
+ [ $# -gt 1 ] && echo "> $N:"
+ echo "$fulltree" | grep "^$(escape_chars "$N")" | sed "s|^$N||g ; "' s|^/||g ; /\/$/d ; /^$/d'
+ done
+
+ else
+ { decrypt | tar -tf - 2>/dev/null || exit $?; } | sed '/\/$/d ; /^$/d'
+ fi
+}
+
+# $@ = paths
+get()
+{
+ [ $# -lt 1 ] && return 1
+ __NOPACK=y archive_exec sh -c '
+ for N
+ do
+ (
+ cat "$N" 2>/dev/null && exit 0
+ [ -d "$1" ] && cat "$N/default" 2>/dev/null && exit 0
+ exit 1
+ ) || { echo "$N: not found" >&2 && exit 1; }
+ done
+ ' sh "$@"
+}
+
+# $1 = path
+copy()
+{
+ copy_check || return $?
+ { get "$1" || return $?; } | remove_trailing_newline | clipboard && clipboard_clear "$ZPASS_CLIPBOARD_TIME"
+}
+
+# $@ = path
+new()
+{
+ [ $# -lt 1 ] && return 1
+ archive_exec sh -c "
+ for N
+ do
+ mkdir -p \"\$(dirname \"\$N\")\" || exit \$?
+ { tr -cd 'a-zA-Z0-9!-.' < /dev/urandom | head -c $ZPASS_RAND_LEN && echo; } > \"\$N\" || exit \$?
+ done
+ " sh "$@"
+}
+
+# $1 = path , $@ = value
+_set()
+{
+ [ $# -lt 1 ] && return 1
+ ref=$1
+ shift 1
+ archive_exec sh -c "mkdir -p '$(dirname "$ref")' && printf '%s\n' '$*' > '$ref'"
+}
+
+add()
+{
+ [ $# -lt 1 ] && return 1
+ archive_exec true # prompt for the key
+ for N
+ do
+ val=$(prompt_password "New value for $N") || return $?
+ _set "$N" "$val" || return $?
+ done
+}
diff --git a/src/prompt.sh b/src/prompt.sh
new file mode 100644
index 0000000..c466af2
--- /dev/null
+++ b/src/prompt.sh
@@ -0,0 +1,62 @@
+#!/bin/sh
+
+# $1 = prompt
+console_prompt_hidden()
+{
+ (
+ _tty_on() { stty echo; }
+ trap _tty_on INT
+ local prompt
+ printf "%s" "$1" >&2
+ stty -echo
+ read -r prompt || { stty echo; return 1; }
+ stty echo
+ printf "\n" >&2
+ echo "$prompt"
+ )
+}
+
+# $1 = prompt message
+prompt_password() {
+ if [ -n "$DISPLAY" ]
+ then
+ if which kdialog >/dev/null 2>&2
+ then kdialog --title "$fname" --password "$1" 2>/dev/null
+ else zenity --title "$fname" --password 2>/dev/null
+ fi
+ else
+ console_prompt_hidden "$1: "
+ fi
+}
+
+# $1 = message
+error_dialog() {
+ if which kdialog >/dev/null 2>&2
+ then kdialog --title "$fname" --error "$1" 2>/dev/null
+ else zenity --title "$fname" --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
+ask_key() {
+ message="Enter key"
+ [ -n "$1" ] && message="$1"
+ key=$(prompt_password "$message") || return $?
+ write_cache "$key" &
+ echo "$key"
+}
diff --git a/src/util.sh b/src/util.sh
new file mode 100644
index 0000000..b1cbf40
--- /dev/null
+++ b/src/util.sh
@@ -0,0 +1,60 @@
+#!/bin/sh
+
+error(){
+ ret=$1 && shift 1 && echo "$*" >&2 && exit $ret
+}
+
+randalnum() {
+ tr -cd '[a-zA-Z]' < /dev/urandom | head -c $1
+}
+
+# $* = input
+escape_chars() {
+ echo "$*" | sed 's|\.|\\\.|g;s|/|\\/|g'
+}
+
+remove_trailing_newline() {
+ awk 'NR>1{print PREV} {PREV=$0} END{printf("%s",$0)}'
+}
+
+# $@ = paths
+sanitize_paths()
+{
+ for N
+ do
+ echo "$N" | grep -q "^/" && echo "Path cannot start with /" >&2 && return 1
+ echo "$N" | grep -qw ".." && echo "Path cannot contain .." >&2 && return 1
+ done
+ return 0
+}
+
+# $1 = file
+getpath() {
+ if [ -n "$ZPASS_REMOTE_ADDR" ]
+ then echo "$ZPASS_REMOTE_PORT:$ZPASS_REMOTE_ADDR:$file"
+ else readlink -f "$file"
+ fi
+}
+
+# $1 = file
+filehash(){
+ getpath "$file" | md5sum | cut -d' ' -f1
+}
+
+keyfile(){
+ printf "%s.key" "$(filehash)"
+}
+
+_cmd_() {
+ if [ -n "$ZPASS_REMOTE_ADDR" ]
+ then
+ if [ -n "$ZPASS_SSH_ID" ]
+ then
+ ssh -i "$ZPASS_SSH_ID" "$ZPASS_REMOTE_ADDR" "$@" || return $?
+ else
+ ssh "$ZPASS_REMOTE_ADDR" "$@" || return $?
+ fi
+ else
+ sh -c "$*"
+ fi
+}