1#!/usr/local/bin/bash 2# pass update - Password Store Extension (https://www.passwordstore.org/) 3# Copyright (C) 2017 Alexandre PUJOL <alexandre@pujol.io>. 4# 5# This program is free software: you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program. If not, see <http://www.gnu.org/licenses/>. 17# 18 19# shellcheck disable=SC2086 20 21readonly VERSION="2.1" 22 23warning() { echo -e "Warning: ${*}" >&2; } 24 25cmd_update_version() { 26 cat <<-_EOF 27 $PROGRAM $COMMAND $VERSION - A pass extension that provides an 28 easy flow for updating passwords. 29 _EOF 30} 31 32cmd_update_usage() { 33 cmd_update_version 34 echo 35 cat <<-_EOF 36 Usage: 37 $PROGRAM update [-h] [-n] [-l <s>] [-c | -p] [-p | -m] 38 [-e <r>] [-i <r>] [-E] [-f] pass-names 39 Provide an interactive solution to update a set of passwords. 40 pass-names can refer either to password store path(s) or to 41 directory. 42 43 It prints the old password and waits for the user before generating 44 a new one. This behaviour can be changed using the provided options. 45 46 Only the first line of a password file is updated unless the 47 --multiline opiton is specified. 48 49 Options: 50 -c, --clip Write the password to the clipboard. 51 -n, --no-symbols Do not use any non-alphanumeric characters. 52 -l, --length <s> Provide a password length. 53 -p, --provide Let the user specify a password by hand. 54 -m, --multiline Update a multiline password. 55 -i, --include <r> Only update the passwords that match a regex. 56 -e, --exclude <r> Do not update the passwords that macth a regex. 57 -E, --edit Edit the password using the default editor. 58 -f, --force Force update. 59 -V, --version Show version information. 60 -h, --help Print this help message and exit. 61 62 More information may be found in the pass-update(1) man page. 63 _EOF 64} 65 66# Print the content of a passfile 67# $1: Path in the password store 68_show() { 69 local path="${1%/}" passfile="$PREFIX/$path.gpg" 70 [[ -f $passfile ]] && { $GPG -d "${GPG_OPTS[@]}" "$passfile" || exit $?; } 71} 72 73# Insert data to the password store 74# $1: Path in the password store 75# $2: Data to insert 76_insert() { 77 local path="${1%/}" data="$2" passfile="$PREFIX/$path.gpg" 78 79 set_git "$passfile" 80 mkdir -p -v "$PREFIX/$(dirname "$path")" 81 set_gpg_recipients "$(dirname "$path")" 82 if [[ $MULTLINE -eq 0 ]]; then 83 $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$data" || \ 84 die "Error: Password encryption aborted." 85 else 86 echo -e "Enter the updated contents of $path and press Ctrl+D when finished:\n" 87 $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" || \ 88 die "Error: Password encryption aborted." 89 fi 90 git_add_file "$passfile" "Update password for $path to store." 91} 92 93cmd_update() { 94 # Sanity checks 95 [[ -z "${*}" ]] && die "Usage: $PROGRAM $COMMAND [-h] [-n] [-l <s>] [-c | -p] [-p | -m] [-e <r>] [-i <r>] [-E] [-f] pass-names" 96 [[ ! $LENGTH =~ ^[0-9]+$ ]] && die "Error: pass-length \"$LENGTH\" must be a number." 97 [[ -n "$CLIP" && $PROVIDED -eq 1 ]] && die "Error: cannot use the options --clip and --provide together" 98 [[ $MULTLINE -eq 1 && $PROVIDED -eq 1 ]] && die "Error: cannot use the options --multiline and --provide together" 99 100 # Get a curated list of path to update 101 typeset -a paths=() passfiles=() 102 local path passfile passdir file tmpfile 103 for path in "$@"; do 104 check_sneaky_paths "$path" 105 passfile="$PREFIX/${path%/}.gpg" 106 passdir="$PREFIX/${path%/}" 107 if [[ $path =~ [*] ]]; then 108 for file in "$PREFIX/"$path.gpg; do 109 if [[ -f "$file" ]]; then 110 tmpfile="${file#$PREFIX/}" 111 paths+=("${tmpfile%.gpg}") 112 fi 113 done 114 elif [[ -d "$passdir" ]]; then 115 mapfile -t passfiles < <(find "$passdir" -type f -iname '*.gpg' -printf "$path/%P\n") 116 for file in "${passfiles[@]}"; do 117 paths+=("${file%.gpg}") 118 done 119 elif [[ -f $passfile ]]; then 120 paths+=("$path") 121 else 122 warning "$path is not in the password store." 123 fi 124 done 125 126 local content oldpassword 127 for path in "${paths[@]}"; do 128 if [[ $EDIT -eq 0 ]]; then 129 content="$(_show "$path")" 130 oldpassword="$(echo "$content" | head -n 1)" 131 [[ -n "$INCLUDE" && ! "$oldpassword" =~ $INCLUDE ]] && continue 132 [[ -n "$EXCLUDE" && "$oldpassword" =~ $EXCLUDE ]] && continue 133 134 # Show old password 135 printf "\e[1m\e[37mChanging password for \e[4m%s\e[0m\n" "$path" 136 if [[ -z "$CLIP" ]]; then 137 printf "%s\n" "$content" 138 else 139 clip "$oldpassword" "$path" 140 fi 141 142 # Ask user for confirmation 143 if [[ $YES -eq 0 ]]; then 144 [[ $PROVIDED -eq 1 || $MULTLINE -eq 1 ]] && verb="provide" || verb="generate" 145 read -r -p "Are you ready to $verb a new password? [y/N] " response 146 [[ $response == [yY] ]] || continue 147 fi 148 149 # Update the password 150 if [[ $PROVIDED -eq 1 ]]; then 151 local password password_again 152 while true; do 153 read -r -p "Enter the new password for $path: " -s password || exit 1 154 echo 155 read -r -p "Retype the new password for $path: " -s password_again || exit 1 156 echo 157 if [[ "$password" == "$password_again" ]]; then 158 break 159 else 160 die "Error: the entered passwords do not match." 161 fi 162 done 163 _insert "$path" "$(echo "$content" | sed $'1c \\\n'"$(sed 's/[\/&]/\\&/g' <<<"$password")"$'\n')" 164 elif [[ $MULTLINE -eq 1 ]]; then 165 _insert "$path" 166 else 167 cmd_generate "$path" "$LENGTH" $SYMBOLS $CLIP '--in-place' || exit 1 168 fi 169 else 170 cmd_edit "$path" 171 fi 172 done 173} 174 175# Global options 176YES=0 177MULTLINE=0 178CLIP="" 179SYMBOLS="" 180PROVIDED=0 181EDIT=0 182INCLUDE="" 183EXCLUDE="" 184LENGTH="$GENERATED_LENGTH" 185 186# Getopt options 187small_arg="hVcfnl:pmEi:e:" 188long_arg="help,version,clip,force,no-symbols,length:,provide,multiline,edit,include:,exclude:" 189opts="$($GETOPT -o $small_arg -l $long_arg -n "$PROGRAM $COMMAND" -- "$@")" 190err=$? 191eval set -- "$opts" 192while true; do case $1 in 193 -c|--clip) CLIP="--clip"; shift ;; 194 -f|--force) YES=1; shift ;; 195 -n|--no-symbols) SYMBOLS="--no-symbols"; shift ;; 196 -p|--provide) PROVIDED=1; shift ;; 197 -l|--length) LENGTH="$2"; shift 2 ;; 198 -m|--multiline) MULTLINE=1; shift ;; 199 -i|--include) INCLUDE="$2"; shift 2 ;; 200 -e|--exclude) EXCLUDE="$2"; shift 2 ;; 201 -E|--edit) EDIT=1; shift ;; 202 -h|--help) shift; cmd_update_usage; exit 0 ;; 203 -V|--version) shift; cmd_update_version; exit 0 ;; 204 --) shift; break ;; 205esac done 206 207[[ $err -ne 0 ]] && cmd_update_usage && exit 1 208cmd_update "$@" 209