#!/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}" DPW_AGE_RECIPIENT_SUFFIX="${DPW_AGE_RECIPIENT_SUFFIX:-.recipients}" DPW_AGE_AUTO_SYNC="${DPW_AGE_AUTO_SYNC:-NO}" # No user overrides DPW_AGE_RECIPIENTS_FILE="" 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" _git_sync } # Auto push/pull _git_sync() { [ $USE_GIT -eq 0 ] && return [ "$DPW_AGE_AUTO_SYNC" != "YES" ] && return cd "${DPW_AGE_DIR}" git fetch git merge git push } _set_age_recipients() { _pth="$1"; shift _pth="${DPW_AGE_DIR}/$_pth" id_file="$(dirname "${_pth}")/${DPW_AGE_RECIPIENT_SUFFIX}" 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 export DPW_AGE_RECIPIENTS_FILE="$id_file" return fi # Pop this up a directory level for the next time 'round id_file="$(dirname "$id_file")" id_file="$(dirname "$id_file")/${DPW_AGE_RECIPIENT_SUFFIX}" done echo "No '${DPW_AGE_RECIPIENT_SUFFIX}' files found, is '$DPW_AGE_DIR' initialized?" exit 1 } # Interface sync() { if [ $USE_GIT -eq 0 ] ; then echo "Cannot sync, not a git repository" return fi if [ "$DPW_AGE_AUTO_SYNC" != "YES" ] ; then echo "Warning: DPW_AGE_AUTO_SYNC is not set to YES" echo "Syncing..." DPW_AGE_AUTO_SYNC=YES fi _git_sync } 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 -R "$DPW_AGE_RECIPIENTS_FILE" -e \ > "${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 If generating a key it will automatically be added to the recipients list, if not generating a key a recipient must be supplied. Multiple recipients may be specified Init will also check that you can decrypt the secrets, otherwise it will fail. ( But not clean up after itself ) 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 --git) USE_GIT=1 ; shift ;; --no-git) USE_GIT=0 ; shift ;; --gen-key) genkey=1; shift ;; --no-gen-key) genkey=0; 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 --armor -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" cat < "${DPW_AGE_KEY}.pub" >> "${DPW_AGE_DIR}/${DPW_AGE_RECIPIENT_SUFFIX}" umask "$UMASK" fi if [ $genkey -eq 1 ] ; then recipients="$recipients:$(cat "${DPW_AGE_KEY}.pub")" fi mkdir -p "${DPW_AGE_DIR}" cd "${DPW_AGE_DIR}" DPW_AGE_RECIPIENTS_FILE="${DPW_AGE_DIR}/${DPW_AGE_RECIPIENT_SUFFIX}" echo "$recipients" | tr ':' '\n' >> "${DPW_AGE_RECIPIENTS_FILE}" grep -q YUBIKEY "${DPW_AGE_KEY}" \ && echo "Detected yubikey, you may need to tap it..." # Test the key and recipients before we get too far along tmpf="$(mktemp)" echo "testing our key... works!" | age -R "${DPW_AGE_RECIPIENTS_FILE}" -e \ > "$tmpf" age -i "${DPW_AGE_KEY}" -d < "$tmpf" if [ $USE_GIT -eq 1 ] ; then git init cat >> .git/config <> .gitattributes fi _git_commit echo "Age Password store initialized in ${DPW_AGE_DIR}" } _help() { cat <