From fefc6c24ddf29bb29cb7d4c2713a4fde9dab0e25 Mon Sep 17 00:00:00 2001 From: Mitchell Riedstra Date: Tue, 26 Oct 2021 22:24:57 -0400 Subject: Add 'age' backend. Init functions. --- Makefile | 4 +- dpw | 9 ++- dpw-age | 223 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ dpw-gpg | 52 ++++++++++++++- readme.md | 49 ++++++++++++-- 5 files changed, 325 insertions(+), 12 deletions(-) create mode 100755 dpw-age diff --git a/Makefile b/Makefile index 08f7b42..baffd11 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,9 @@ all: install: install -m 755 dpw $(PREFIX)/bin/dpw install -m 755 dpw-gpg $(PREFIX)/bin/dpw-gpg + install -m 755 dpw-age $(PREFIX)/bin/dpw-age install -m 755 dpw-menu $(PREFIX)/bin/dpw-menu uninstall: - rm "$(PREFIX)/bin/dpw" "$(PREFIX)/bin/dpw-gpg" "$(PREFIX)/bin/dpw-menu" + rm "$(PREFIX)/bin/dpw" "$(PREFIX)/bin/dpw-gpg" "$(PREFIX)/bin/dpw-menu" \ + "$(PREFIX)/bin/dpw-menu" diff --git a/dpw b/dpw index a9e6a75..a342366 100755 --- a/dpw +++ b/dpw @@ -53,7 +53,7 @@ Usage: $0 [command] key/to/operate/on dpw is the dynamic password manager inspired by 'pass' -Your current backend is: $DPW_BACKEND +Your current backend is: $DPW_BACKEND ( Set by DPW_BACKEND ) Where command may be one of: EOF @@ -232,6 +232,10 @@ case $a in esac } +init() { +"${DPW_BACKEND}" init "$@" +} + if [ -z "$1" ] ; then list "$@" exit 0 @@ -257,7 +261,8 @@ case $1 in edit) action=edit; shift ;; find|fnd) action=_find; shift ;; otp) action=otp; shift ;; - -h) help ;; + init) action=init; shift ;; + -h|--help|help) help ;; esac "$action" "$@" diff --git a/dpw-age b/dpw-age new file mode 100755 index 0000000..1d98303 --- /dev/null +++ b/dpw-age @@ -0,0 +1,223 @@ +#!/bin/sh +# Copyright 2021 Mitchell Riedstra +# +# Permission to use, copy, modify, and/or distribute this software for any purpose +# with or without fee is hereby granted, provided that the above copyright notice +# and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +# FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +# THIS SOFTWARE. +# +# Example storage plugin that wraps 'age' into a password manager. +# https://github.com/FiloSottile/age +set -e + +UMASK="${PASSWORD_STORE_UMASK:-077}" +umask "$UMASK" + +DPW_AGE_DIR="${DPW_AGE_DIR:-$HOME/.dpw-age}" +DPW_AGE_KEY="${DPW_AGE_KEY:-$HOME/.dpw-age-key}" + +USE_GIT=0 +[ -e "${DPW_AGE_DIR}/.git" ] && USE_GIT=1 + +# Helper functions + +_git_commit() { + [ $USE_GIT -eq 0 ] && return + cd "${DPW_AGE_DIR}" + git add --all + git commit -am "DPW Auto-commit: $1" +} + +_set_age_recipients() { +if [ -n "$DPW_AGE_RECIPIENTS" ] ; then + DPW_AGE_RECIPIENTS="$(echo "$DPW_AGE_RECIPIENTS" | tr ' ' '\n' \ + | sed -e's/^/ -r/' | tr '\n' ' ')" + return +fi +_pth="$1"; shift + +_pth="${DPW_AGE_DIR}/$_pth" + + +id_file="$(dirname "${_pth}")/.recipients" +while true ; do + + # Break if id_file is above our password store directory + case $id_file in + ${DPW_AGE_DIR}*) ;; + *) break ;; + esac + + if [ -e "$id_file" ] ; then + keys="" + while read -r key ; do + if [ -z "$key" ] ; then continue ; fi + keys="$keys -r $key" + done < "$id_file" + + export DPW_AGE_RECIPIENTS="$keys" + return + fi + + # Pop this up a directory level for the next time 'round + id_file="$(dirname "$id_file")" + id_file="$(dirname "$id_file")/.recipients" +done + +echo "No '.recipients' files found, is '$DPW_AGE_DIR' initialized?" +exit 1 +} + + +# Interface + +show() { +pth="$1"; shift +#shellcheck disable=SC2086 +exec age -i "${DPW_AGE_KEY}" -d < "${DPW_AGE_DIR}/${pth}.age" +} + + +insert() { +pth="$1"; shift +_set_age_recipients "$pth" +mkdir -p "$DPW_AGE_DIR/$(dirname "$pth")" +#shellcheck disable=SC2086 +age -e ${DPW_AGE_RECIPIENTS} \ + > "${DPW_AGE_DIR}/${pth}.age" +_git_commit "Insert: $pth" +} + +list() { +find "$DPW_AGE_DIR/$1" -type f -iname "*.age" \ + | while read -r line ; do + line="${line##$DPW_AGE_DIR}" + line="${line#/}" + line="${line#/}" + line="${line%.age}" + echo "$line" + done +} + +remove() { +cd "$DPW_AGE_DIR" +recursive= +force= +while [ $# -gt 0 ] ; do case $1 in + -r) recursive="-r" ; shift ;; + -f) force="-f" ; shift ;; + -rf|-fr) recursive="-r" ; force="-f" ; shift ;; + *) break ;; +esac ; done + +files= +for fn in "$@" ; do + if [ -e "${fn}.age" ] ; then + files="${fn}.age " + else + files="$fn " + fi +done + +#shellcheck disable=SC2086 +rm $recursive $force $files +_git_commit "Remove: $*" +} + +_init_help() { +cat <... + +Git is the default. + +Gen key will generate a passphrase protected private key for you and place it in +\$DPW_AGE_KEY ($DPW_AGE_KEY) and the public side will be '.pub' + +No pass disables the default for a passphrase protected key file + +The key will automatically be added to the recipients list. + +Multiple recipients may be specified + +See: https://github.com/FiloSottile/age for more information on 'age' itself. +EOF +exit 1 +} + +_init() { +USE_GIT=1 +if [ -d "${DPW_AGE_DIR}" ] ; then + echo "Cannot init new password store, one exists" + exit 1 +fi + +recipients= +genkey=1 +passphrase=1 +while [ $# -gt 0 ] ; do case $1 in + --no-git) USE_GIT=0 ; shift ;; + --gen-key) genkey=1; shift ;; + --pass) passphrase=1; shift ;; + --no-pass) passphrase=0; shift ;; + -r|--recipient) recipients="$recipients:$2"; shift ; shift ;; + *) _init_help ;; +esac ; done + +[ $genkey -eq 1 ] && [ -e "${DPW_AGE_KEY}" ] \ + && echo "Cannot continue, \"${DPW_AGE_KEY}\" already exists" && exit + +echo "genkey: $genkey passphrase: $passphrase" + +if [ $genkey -eq 1 ] && [ $passphrase -eq 1 ] ; then + touch "${DPW_AGE_KEY}.pub" # Let the user's UMASK bet set + umask 077 # but override it for the key + age-keygen 2>"${DPW_AGE_KEY}.pub" | age -p > "${DPW_AGE_KEY}" 2>&2 + sed -i.bak -e's/^Public key: //g' "${DPW_AGE_KEY}.pub" + rm "${DPW_AGE_KEY}.pub.bak" + umask "$UMASK" +elif [ $genkey -eq 1 ] ; then + touch "${DPW_AGE_KEY}.pub" # Let the user's UMASK bet set + umask 077 # but override it for the key + age-keygen >"${DPW_AGE_KEY}" + sed -ne's/^# public key: //gp' < "${DPW_AGE_KEY}" > "${DPW_AGE_KEY}.pub" + umask "$UMASK" +fi + +mkdir -p "${DPW_AGE_DIR}" +cd "${DPW_AGE_DIR}" + +if [ $USE_GIT -eq 1 ] ; then + git init + cat >> .git/config <> .gitattributes +fi + +cat < "${DPW_AGE_KEY}.pub" > "${DPW_AGE_DIR}/.recipients" +echo "$recipients" | tr ':' '\n' >> "${DPW_AGE_DIR}/.recipients" + +_git_commit + +echo "Age Password store initialized in ${DPW_AGE_DIR}" +} + +act="$1"; shift +case $act in + show) show "$@" ;; + list) list "$@" ;; + insert) insert "$@" ;; + rm) remove "$@" ;; + init) _init "$@" ;; + *) echo "Bad command $act"; exit 1; ;; +esac diff --git a/dpw-gpg b/dpw-gpg index d26b665..b0669dc 100755 --- a/dpw-gpg +++ b/dpw-gpg @@ -59,7 +59,7 @@ while true ; do if [ -e "$id_file" ] ; then keys="" while read -r key ; do - keys=" -r $key" + keys="$keys -r $key" done < "$id_file" export PASSWORD_STORE_KEY="$keys" @@ -88,7 +88,7 @@ exec gpg $__gpg_opts -d < "${PASSWORD_STORE_DIR}/${pth}.gpg" insert() { pth="$1"; shift _set_gpg_id "$pth" -mkdir -p "$(dirname "$pth")" +mkdir -p "$PASSWORD_STORE_DIR/$(dirname "$pth")" #shellcheck disable=SC2086 gpg $__gpg_opts -e ${PASSWORD_STORE_KEY} \ > "${PASSWORD_STORE_DIR}/${pth}.gpg" @@ -133,11 +133,59 @@ rm $recursive $force $files _git_commit "Remove: $*" } +_init_help() { +cat <... +EOF +exit 1 +} + +_init() { +USE_GIT=1 +if [ -d "${PASSWORD_STORE_DIR}" ] ; then + echo "Cannot init new password store, one exists" + exit 1 +fi +while [ $# -gt 0 ] ; do case $1 in + --no-git) USE_GIT=0 ; shift ;; + -h|--help) _init_help ;; + *) break ;; +esac ; done + +if [ $# -eq 0 ] ; then + echo "No GPG id supplied, bailing" + exit 1 +fi + +mkdir -p "${PASSWORD_STORE_DIR}" +cd "${PASSWORD_STORE_DIR}" + +if [ $USE_GIT -eq 1 ] ; then + git init + cat >> .git/config <> .gitattributes +fi + +for id in "$@" ; do + echo "$id" >> .gpg-id +done + +_git_commit + +echo "Password store initialized" +} + act="$1"; shift case $act in show) show "$@" ;; list) list "$@" ;; insert) insert "$@" ;; rm) remove "$@" ;; + init) _init "$@" ;; *) echo "Bad command $act"; exit 1; ;; esac diff --git a/readme.md b/readme.md index b90da50..53bfe86 100644 --- a/readme.md +++ b/readme.md @@ -1,13 +1,10 @@ # dpw the dynamic password manager Inspired by [`pass`](https://www.passwordstore.org/) and designed to be -compatible out of the box. +partly compatible out of the box. -The main point of this is to provide a somewhat familiar interface for -pluggable backends. Secondary benefits that people may be interested in is the -ISC license, so you can modify the `dpw` script without giving me your changes -even if you use it in a commercial product. ( Though, it might be appreciated to -get code back! ) +The main point of this is to provide a somewhat familiar command line interface +for pluggable backends. For instance, one could write a plugin that talks to Lastpass, Bitwarden Hasicorp's Vault, or similar. @@ -35,12 +32,50 @@ simple, accept the following four commands: * show `` * Dump to stdout * rm `` + * init ( optional ) `dpw` will take care of all the additional commands by wrapping the four above as needed. - ## dmenu script There's also a small dmenu script included that makes copying or typing out passwords and OTP tokens very quick and easy + + +# [age](https://github.com/FiloSottile/age) backend + +Pretty similar to the GPG backend, except it uses the new `age` tool as +the backend. The usage should be straightforward, install the utility +and then set `DPW_BACKEND=dpw-age` in your environment. It will not +clobber any GPG based password store you have unless you explicitly +override `DPW_AGE_DIR` + +It expects `age` and `age-keygen` to be in your path. + +Check over `dpw init --help` for more options when initalizing the store. + +Environment variables: + + * `DPW_AGE_DIR=$HOME/.dpw-age` + * Default directory for storage + * `DPW_AGE_KEY=$HOME/.dpw-age-key` + * default key file + * `PASSWORD_STORE_UMASK=077` + * Umask for files created + * `DPW_AGE_RECIPIENTS` + * If set, no `.recipients` files are read and encryption will be to the keys + specified + + +NOTE: Because there's no agent passphrase protected keys are going to be +somewhat of a pain to use with this. Having a password manager with the +key sitting on disk right next to the files sort of defeats the purpose. + +It's still perhaps useful in the sense that the requirements for setup +are minimal and you don't have to deal with dragging GPG around. + +The Go library is available so in theory it wouldn't be hard to write +a self-contained backend that kept the key in memory. If made setgid +nobody it'll prevent your regular user from being able to extract the +keys from memory. -- cgit v1.2.3