init sources

This commit is contained in:
zawwz 2020-11-27 16:21:00 +01:00
parent c74cd2d662
commit d7c91472e6
14 changed files with 679 additions and 1 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
Zmakefile

20
Makefile Normal file
View file

@ -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

View file

@ -1,2 +1,83 @@
# zpass
Basic and simple password management
Basic and simple password management for UNIX using shell. <br>
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.
<b>If you lose this password, you lose access to all contents of the archive</b>.
You can create new values with either `zpass add <path...>`, `zpass new <path...>`, or `zpass set <path> <value>`
To copy a value into the clipboard, use `zpass <value>` or `zpass copy <value>`
## 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.<br>
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. <br>
This can be fixed by pointing the cache path to a volatile filesystem. <br>
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

101
src/archive.sh Normal file
View file

@ -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
}

38
src/cache.sh Normal file
View file

@ -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)
}

45
src/clipboard.sh Normal file
View file

@ -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
}

46
src/config.sh Normal file
View file

@ -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

56
src/crypt.sh Normal file
View file

@ -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
}

13
src/file.sh Normal file
View file

@ -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
}

46
src/help.sh Normal file
View file

@ -0,0 +1,46 @@
#!/bin/sh
fname=$(basename "$0")
usage()
{
echo "$fname [options] <operation>
[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 <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 <path...> Get values of targets
copy <path> Copy the target value to clipboard. Shortcut 'x'
set <path> <value> Set the value of target
add <path...> Prompt for input value on paths
new <path...> Generate a random password at target
rm <path...> Delete targets
mv <path...> Move targets
exec <cmd> 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
"
}

34
src/main.sh Normal file
View file

@ -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

75
src/operation.sh Normal file
View file

@ -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
}

62
src/prompt.sh Normal file
View file

@ -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"
}

60
src/util.sh Normal file
View file

@ -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
}