feat: shared _reencrypt_all + reencrypt cmd + dual rekey (EGB-283)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
649aa78063
commit
a10f390519
2 changed files with 133 additions and 1 deletions
87
secrets
87
secrets
|
|
@ -1772,6 +1772,78 @@ cmd_rm() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Decrypt every blob in the store with the local key and re-encrypt each to the
|
||||
# currently-loaded RECIPIENT_ARGS, then commit + push. The caller MUST have run
|
||||
# _load_recipients (or set RECIPIENT_ARGS) and check_key first. Aborts with the
|
||||
# store untouched on any decrypt failure (you must be a current recipient).
|
||||
# Shared by recipients add/rm, reencrypt, and multi-recipient rekey.
|
||||
_reencrypt_all() {
|
||||
local commit_msg="$1"
|
||||
local tmpdir
|
||||
tmpdir=$(mktemp -d)
|
||||
trap 'rm -rf "${tmpdir:-}"' EXIT INT TERM
|
||||
|
||||
info "Decrypting all blobs with your key..."
|
||||
local file_count=0 dir project f rel dest
|
||||
for dir in "$SECRETS_DIR"/*/; do
|
||||
[ -d "$dir" ] || continue
|
||||
project=$(basename "$dir")
|
||||
case "$project" in .*) continue ;; esac
|
||||
mkdir -p "$tmpdir/$project"
|
||||
while IFS= read -r f; do
|
||||
[ -f "$f" ] || continue
|
||||
rel=${f#"$dir"}; rel=${rel%.age}
|
||||
dest="$tmpdir/$project/$rel"
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
if ! age -d -i "$KEY_FILE" -o "$dest" "$f"; then
|
||||
die "Decryption failed for $project/$rel (are you a current recipient?). Aborted; store unchanged."
|
||||
fi
|
||||
file_count=$((file_count + 1))
|
||||
done < <(find "$dir" -type f -name '*.age')
|
||||
done
|
||||
|
||||
if [ "$file_count" -eq 0 ]; then
|
||||
rm -rf "$tmpdir"; trap - EXIT INT TERM
|
||||
info "No encrypted blobs in the store — nothing to re-encrypt."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local rc=$(( ${#RECIPIENT_ARGS[@]} / 2 ))
|
||||
info "Re-encrypting $file_count blob(s) to $rc recipient(s)..."
|
||||
for dir in "$tmpdir"/*/; do
|
||||
[ -d "$dir" ] || continue
|
||||
project=$(basename "$dir")
|
||||
mkdir -p "$SECRETS_DIR/$project"
|
||||
while IFS= read -r f; do
|
||||
[ -f "$f" ] || continue
|
||||
rel=${f#"$dir"}
|
||||
mkdir -p "$(dirname "$SECRETS_DIR/$project/$rel")"
|
||||
age "${RECIPIENT_ARGS[@]}" -o "$SECRETS_DIR/$project/${rel}.age" "$f"
|
||||
done < <(find "$dir" -type f)
|
||||
done
|
||||
|
||||
ensure_store_protections
|
||||
git -C "$SECRETS_DIR" add -A
|
||||
git -C "$SECRETS_DIR" commit -m "$commit_msg" >/dev/null
|
||||
if git -C "$SECRETS_DIR" remote get-url origin >/dev/null 2>&1; then
|
||||
git -C "$SECRETS_DIR" push >/dev/null 2>&1
|
||||
info "Pushed re-encrypted secrets to remote"
|
||||
else
|
||||
info "Committed re-encrypted secrets locally (no remote configured)"
|
||||
fi
|
||||
rm -rf "$tmpdir"; trap - EXIT INT TERM
|
||||
}
|
||||
|
||||
cmd_reencrypt() {
|
||||
check_cmd age
|
||||
check_cmd git
|
||||
resolve_store
|
||||
check_initialized
|
||||
check_key
|
||||
_load_recipients
|
||||
_reencrypt_all "reencrypt: re-encrypt all to current recipients"
|
||||
}
|
||||
|
||||
cmd_rekey() {
|
||||
check_cmd age
|
||||
check_cmd git
|
||||
|
|
@ -1779,6 +1851,18 @@ cmd_rekey() {
|
|||
check_initialized
|
||||
check_key
|
||||
|
||||
# EGB-283: on a multi-recipient store, rekey means "re-encrypt every blob to
|
||||
# the current recipients.txt set" — NOT a new keypair (rotating an identity is
|
||||
# the member's own age-keygen + recipients rm/add). Legacy stores (no
|
||||
# recipients.txt) keep the original generate-new-keypair behavior below.
|
||||
if [ -e "$RECIPIENTS_FILE" ]; then
|
||||
_load_recipients
|
||||
info "Multi-recipient store — re-encrypting to $RECIPIENTS_FILE_NAME (no new key generated)."
|
||||
_reencrypt_all "rekey: re-encrypt all to current recipients"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ── Legacy single-key rotation (unchanged) ──
|
||||
# Create temp dir with cleanup trap
|
||||
local tmpdir
|
||||
tmpdir=$(mktemp -d)
|
||||
|
|
@ -2611,7 +2695,8 @@ case "${1:-help}" in
|
|||
add) cmd_add "${2:-}" ;;
|
||||
list) cmd_list ;;
|
||||
rm) cmd_rm "${2:-}" ;;
|
||||
rekey) cmd_rekey ;;
|
||||
rekey) cmd_rekey ;;
|
||||
reencrypt) cmd_reencrypt ;;
|
||||
verify) shift; cmd_verify "$@" ;;
|
||||
migrate) shift; cmd_migrate "$@" ;;
|
||||
recipients) shift; cmd_recipients "$@" ;;
|
||||
|
|
|
|||
|
|
@ -76,3 +76,50 @@ make_second_identity() {
|
|||
[[ "$output" == *"alice"* ]] || false
|
||||
[[ "$output" == *"bob"* ]] || false
|
||||
}
|
||||
|
||||
@test "reencrypt re-encrypts existing blobs to a newly added recipient line" {
|
||||
init_with_remote
|
||||
create_project_dir myproj
|
||||
run "$SECRETS_BIN" push # single-key blob (project name = "myproj"; blob at $SECRETS_DIR/myproj/.env.age)
|
||||
[ "$status" -eq 0 ]
|
||||
make_second_identity
|
||||
STORE_PUB=$(age-keygen -y "$SECRETS_DIR/key.txt")
|
||||
printf '%s\n%s\n' "$STORE_PUB" "$BOB_PUB" > "$SECRETS_DIR/recipients.txt"
|
||||
# Bob cannot read the old single-key blob yet.
|
||||
run age -d -i "$TEST_TMPDIR/bob.txt" "$SECRETS_DIR/myproj/.env.age"
|
||||
[ "$status" -ne 0 ]
|
||||
run "$SECRETS_BIN" reencrypt
|
||||
[ "$status" -eq 0 ]
|
||||
# Now he can.
|
||||
run age -d -i "$TEST_TMPDIR/bob.txt" "$SECRETS_DIR/myproj/.env.age"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "rekey on a multi-recipient store keeps recipients and the same key" {
|
||||
init_with_remote
|
||||
create_project_dir myproj
|
||||
run "$SECRETS_BIN" push
|
||||
before=$(cat "$SECRETS_DIR/key.txt")
|
||||
make_second_identity
|
||||
STORE_PUB=$(age-keygen -y "$SECRETS_DIR/key.txt")
|
||||
printf '%s\n%s\n' "$STORE_PUB" "$BOB_PUB" > "$SECRETS_DIR/recipients.txt"
|
||||
run "$SECRETS_BIN" rekey
|
||||
[ "$status" -eq 0 ]
|
||||
# No new keypair was generated.
|
||||
[ "$(cat "$SECRETS_DIR/key.txt")" = "$before" ]
|
||||
# Both recipients can decrypt.
|
||||
run age -d -i "$TEST_TMPDIR/bob.txt" "$SECRETS_DIR/myproj/.env.age"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "rekey on a legacy store still rotates to a new key (unchanged)" {
|
||||
init_with_remote
|
||||
create_project_dir myproj
|
||||
run "$SECRETS_BIN" push
|
||||
before=$(cat "$SECRETS_DIR/key.txt")
|
||||
run "$SECRETS_BIN" rekey
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$(cat "$SECRETS_DIR/key.txt")" != "$before" ]
|
||||
run age -d -i "$SECRETS_DIR/key.txt" "$SECRETS_DIR/myproj/.env.age"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue