From b994217878d269b9973873bfb8be46ebe13bc872 Mon Sep 17 00:00:00 2001
From: Carl Michael Skog <cm@äger.com>
Date: Wed, 16 Oct 2019 17:24:17 +0200
Subject: [PATCH 4/4] Fixes for having keys with multiple subkeys
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

When an init is done with a key with multiple subkeys, the password
file is only encrypted with the most recently created subkey.
This gives the following possible scenario:
Let's say have two pass repositories using the same (main)key, and one
repository adds a subkey, does a 'pass init' with the same key, and does
a 'pass git push'.
Assuming the other repository does not update the key, and does a 'pass
git pull', then it can not read any of the passwords.
'pass show' will give the error message "gpg: decryption failed: No
secret key".

Fix by explicitly enumerate all encryption subkeys and append an
ampersand to each subkey keyid.

Signed-off-by: Carl Michael Skog <cm@äger.com>
---
 src/password-store.sh | 101 +++++++++++++++++++++++++-----------------
 1 file changed, 60 insertions(+), 41 deletions(-)

diff --git a/src/password-store.sh b/src/password-store.sh
index 1d119f2..ecffb81 100755
--- a/src/password-store.sh
+++ b/src/password-store.sh
@@ -67,46 +67,75 @@ verify_file() {
 	[[ $found -eq 1 ]] || die "Signature for $1 is invalid."
 }
 set_gpg_recipients() {
-	GPG_RECIPIENT_ARGS=( )
-	GPG_RECIPIENTS=( )
+	local ids=()
 
 	if [[ -n $PASSWORD_STORE_KEY ]]; then
 		for gpg_id in $PASSWORD_STORE_KEY; do
-			GPG_RECIPIENT_ARGS+=( "-r" "$gpg_id" )
-			GPG_RECIPIENTS+=( "$gpg_id" )
+			ids+=( "$gpg_id" )
 		done
-		return
-	fi
+	else
+		local current="$PREFIX/$1"
+		while [[ $current != "$PREFIX" && ! -f $current/.gpg-id ]]; do
+			current="${current%/*}"
+		done
+		current="$current/.gpg-id"
 
-	local current="$PREFIX/$1"
-	while [[ $current != "$PREFIX" && ! -f $current/.gpg-id ]]; do
-		current="${current%/*}"
-	done
-	current="$current/.gpg-id"
+		if [[ ! -f $current ]]; then
+			cat >&2 <<-_EOF
+			Error: You must run:
+			    $PROGRAM init your-gpg-id
+			before you may use the password store.
 
-	if [[ ! -f $current ]]; then
-		cat >&2 <<-_EOF
-		Error: You must run:
-		    $PROGRAM init your-gpg-id
-		before you may use the password store.
+			_EOF
+			cmd_usage
+			exit 1
+		fi
 
-		_EOF
-		cmd_usage
-		exit 1
-	fi
+		# Cache the lookup of subkeys.
+		# This is only used by reencrypt_path,
+		# where current_gpgid is also defined
+		if [ "$current" = "$current_gpgid" ]; then
+			return
+		fi
+		current_gpgid="$current"
+		verify_file "$current"
 
-	verify_file "$current"
+		local gpg_id
 
-	local gpg_id
-	while read -r gpg_id; do
-		GPG_RECIPIENT_ARGS+=( "-r" "$gpg_id" )
-		GPG_RECIPIENTS+=( "$gpg_id" )
-	done < "$current"
+		while read -r gpg_id; do
+			ids+=( "$gpg_id" )
+		done < "$current"
+	fi
+
+	GPG_RECIPIENT_ARGS=( )
+	GPG_RECIPIENTS=( )
+	GPG_RECIPIENTS=( $(lookup_subkeys "${ids[@]}") )
+	for recipient in ${GPG_RECIPIENTS[@]}; do
+		GPG_RECIPIENT_ARGS+=( "-r" "$recipient" )
+	done
 }
+lookup_subkeys() {
+	local IFS=";"
+	local keyids=()
+	for akeyid; do
+		local group_keys=( $(gpg --list-config --with-colons | sed -n "s/^cfg:group:$akeyid:\\(.*\\)/\\1/p" | head -n 1) )
+
+		# We picked up a group name with some key ids
+		if [[ ${#group_keys} > 0 ]]; then
+			keyids+=( "${group_keys[@]}" )
+		else
+			# If not, it must be a name representing a keyid, uid, etc
+			keyids+=( "$akeyid" )
+		fi
+	done
 
+	$GPG $PASSWORD_STORE_GPG_OPTS --list-keys --with-colons "${keyids[@]}" \
+		| sed -n 's/^sub:[^idr:]*:[^:]*:[^:]*:\([^:]*\):[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[a-zA-Z]*e[a-zA-Z]*:.*/\1\!/p' \
+		| LC_ALL=C sort -u
+}
 reencrypt_path() {
-	local prev_gpg_recipients="" gpg_keys="" current_keys="" index passfile
-	local groups="$($GPG $PASSWORD_STORE_GPG_OPTS --list-config --with-colons | grep "^cfg:group:.*")"
+	local current_keys="" passfile
+	local current_gpgid # Cache the active gpgid file; This is set by set_gpg_recipients
 	while read -r -d "" passfile; do
 		[[ -L $passfile ]] && continue
 		local passfile_dir="${passfile%/*}"
@@ -117,23 +146,13 @@ reencrypt_path() {
 		local passfile_temp="${passfile}.tmp.${RANDOM}.${RANDOM}.${RANDOM}.${RANDOM}.--"
 
 		set_gpg_recipients "$passfile_dir"
-		if [[ $prev_gpg_recipients != "${GPG_RECIPIENTS[*]}" ]]; then
-			for index in "${!GPG_RECIPIENTS[@]}"; do
-				local group="$(sed -n "s/^cfg:group:$(sed 's/[\/&]/\\&/g' <<<"${GPG_RECIPIENTS[$index]}"):\\(.*\\)\$/\\1/p" <<<"$groups" | head -n 1)"
-				[[ -z $group ]] && continue
-				IFS=";" eval 'GPG_RECIPIENTS+=( $group )' # http://unix.stackexchange.com/a/92190
-				unset "GPG_RECIPIENTS[$index]"
-			done
-			gpg_keys="$($GPG $PASSWORD_STORE_GPG_OPTS --list-keys --with-colons "${GPG_RECIPIENTS[@]}" | sed -n 's/^sub:[^idr:]*:[^:]*:[^:]*:\([^:]*\):[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[a-zA-Z]*e[a-zA-Z]*:.*/\1/p' | LC_ALL=C sort -u)"
-		fi
-		current_keys="$(LC_ALL=C $GPG $PASSWORD_STORE_GPG_OPTS -v --no-secmem-warning --no-permission-warning --decrypt --list-only --keyid-format long "$passfile" 2>&1 | sed -n 's/^gpg: public key is \([A-F0-9]\+\)$/\1/p' | LC_ALL=C sort -u)"
+		current_keys=( $(LC_ALL=C $GPG $PASSWORD_STORE_GPG_OPTS -v --no-secmem-warning --no-permission-warning --decrypt --list-only --keyid-format long "$passfile" 2>&1 | sed -n 's/^gpg: public key is \([A-F0-9]\+\)$/\1\!/p' | LC_ALL=C sort -u) )
 
-		if [[ $gpg_keys != "$current_keys" ]]; then
-			echo "$passfile_display: reencrypting to ${gpg_keys//$'\n'/ }"
+		if [[ "${GPG_RECIPIENTS[*]}" != "${current_keys[*]}" ]]; then
+			echo "$passfile_display: reencrypting with encryption subkeys ${GPG_RECIPIENTS[*]%!}"
 			$GPG -d "${GPG_OPTS[@]}" "$passfile" | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile_temp" "${GPG_OPTS[@]}" &&
 			mv "$passfile_temp" "$passfile" || rm -f "$passfile_temp"
 		fi
-		prev_gpg_recipients="${GPG_RECIPIENTS[*]}"
 	done < <(find "$1" -path '*/.git' -prune -o -iname '*.gpg' -print0)
 }
 check_sneaky_paths() {
-- 
2.23.0

