fix: rekey and list recurse into nested manifest blobs (EGB-677 stage 1)
Pre-landing review (testing + checklist specialists, reproduced) caught a data-loss bug: cmd_rekey's decrypt/re-encrypt globs were non-recursive and only special-cased external/. Nested manifest dotenv blobs (<project>/<relpath>.age, new this branch) were never visited, so after a key rotation they stayed encrypted under the discarded old key = permanently undecryptable. cmd_list had the same blind spot (cosmetic: nested entries invisible in listings). Both now walk the entire project tree with `find -type f` (bash 3.2 safe, includes dotfiles natively), unifying top-level / nested / external blobs into one recursive pass and dropping the now-redundant external/ special-casing. Regression tests: nested-blob rekey round-trip (survives rotation) + list shows nested entry. Full suite 193/193.
This commit is contained in:
parent
5489f89446
commit
c6ea724ddb
2 changed files with 70 additions and 48 deletions
85
secrets
85
secrets
|
|
@ -1607,19 +1607,20 @@ cmd_list() {
|
|||
# Skip hidden dirs
|
||||
[[ "$project" == .* ]] && continue
|
||||
echo "$project:"
|
||||
for f in "$dir"*.age "$dir".*.age; do
|
||||
# Recurse the whole project tree so nested manifest blobs
|
||||
# (<project>/<relpath>.age) are visible, not just top-level entries.
|
||||
# External blobs (external/<slug>.age) are labelled distinctly.
|
||||
while IFS= read -r f; do
|
||||
[ -f "$f" ] || continue
|
||||
echo " $(basename "$f" .age)"
|
||||
local rel
|
||||
rel=${f#"$dir"}
|
||||
rel=${rel%.age}
|
||||
case "$rel" in
|
||||
external/*) echo " [external] ${rel#external/}" ;;
|
||||
*) echo " $rel" ;;
|
||||
esac
|
||||
found=1
|
||||
done
|
||||
# External files live in a subdir, invisible to the globs above.
|
||||
if [ -d "${dir}external" ]; then
|
||||
for f in "${dir}external"/*.age; do
|
||||
[ -f "$f" ] || continue
|
||||
echo " [external] $(basename "$f" .age)"
|
||||
found=1
|
||||
done
|
||||
fi
|
||||
done < <(find "$dir" -type f -name '*.age' | sort)
|
||||
done
|
||||
|
||||
if [ "$found" -eq 0 ]; then
|
||||
|
|
@ -1681,29 +1682,23 @@ cmd_rekey() {
|
|||
project=$(basename "$dir")
|
||||
[[ "$project" == .* ]] && continue
|
||||
mkdir -p "$tmpdir/$project"
|
||||
for f in "$dir"*.age "$dir".*.age; do
|
||||
# Walk the WHOLE project tree, not just its top level. Manifest dotenv
|
||||
# entries can nest (<project>/<relpath>.age) and external blobs live in
|
||||
# <project>/external/<slug>.age. A non-recursive glob would skip both,
|
||||
# leaving them encrypted under the old key = permanently undecryptable
|
||||
# after rotation (silent data loss). `find` is bash-3.2 safe and recurses.
|
||||
while IFS= read -r f; do
|
||||
[ -f "$f" ] || continue
|
||||
local name
|
||||
name=$(basename "$f" .age)
|
||||
if ! age -d -i "$KEY_FILE" -o "$tmpdir/$project/$name" "$f"; then
|
||||
die "Decryption failed for $project/$name. Rekey aborted. Old key preserved."
|
||||
local rel dest
|
||||
rel=${f#"$dir"} # path relative to the project dir (keeps .age)
|
||||
rel=${rel%.age} # strip the .age suffix → original relpath
|
||||
dest="$tmpdir/$project/$rel"
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
if ! age -d -i "$KEY_FILE" -o "$dest" "$f"; then
|
||||
die "Decryption failed for $project/$rel. Rekey aborted. Old key preserved."
|
||||
fi
|
||||
file_count=$((file_count + 1))
|
||||
done
|
||||
# External files live in a subdir; rekey them too or they become
|
||||
# undecryptable after rotation.
|
||||
if [ -d "${dir}external" ]; then
|
||||
mkdir -p "$tmpdir/$project/external"
|
||||
for f in "${dir}external"/*.age; do
|
||||
[ -f "$f" ] || continue
|
||||
local ename
|
||||
ename=$(basename "$f" .age)
|
||||
if ! age -d -i "$KEY_FILE" -o "$tmpdir/$project/external/$ename" "$f"; then
|
||||
die "Decryption failed for $project/external/$ename. Rekey aborted. Old key preserved."
|
||||
fi
|
||||
file_count=$((file_count + 1))
|
||||
done
|
||||
fi
|
||||
done < <(find "$dir" -type f -name '*.age')
|
||||
done
|
||||
|
||||
if [ "$file_count" -eq 0 ]; then
|
||||
|
|
@ -1723,29 +1718,23 @@ cmd_rekey() {
|
|||
|
||||
info "Re-encrypting all files with new key..."
|
||||
|
||||
# Re-encrypt all files. The ".*" glob is required: dotenv files decrypt
|
||||
# to dotfiles ("$tmpdir/p/.env") that a bare "*" would silently skip,
|
||||
# leaving their blobs on the old key (undecryptable after rotation).
|
||||
# Re-encrypt all files. `find -type f` recurses into nested dotenv dirs and
|
||||
# external/ and natively includes dotfiles (decrypted dotenv files like
|
||||
# "$tmpdir/p/.env"), which a bare "*" glob would silently skip — leaving
|
||||
# their blobs on the old key (undecryptable after rotation). The walk mirrors
|
||||
# the recursive decrypt above so every blob round-trips back to its relpath.
|
||||
for dir in "$tmpdir"/*/; do
|
||||
[ -d "$dir" ] || continue
|
||||
local project
|
||||
project=$(basename "$dir")
|
||||
mkdir -p "$SECRETS_DIR/$project"
|
||||
for f in "$dir"* "$dir".*; do
|
||||
while IFS= read -r f; do
|
||||
[ -f "$f" ] || continue
|
||||
local name
|
||||
name=$(basename "$f")
|
||||
age -r "$pubkey" -o "$SECRETS_DIR/$project/${name}.age" "$f"
|
||||
done
|
||||
if [ -d "${dir}external" ]; then
|
||||
mkdir -p "$SECRETS_DIR/$project/external"
|
||||
for f in "${dir}external"/*; do
|
||||
[ -f "$f" ] || continue
|
||||
local ename
|
||||
ename=$(basename "$f")
|
||||
age -r "$pubkey" -o "$SECRETS_DIR/$project/external/${ename}.age" "$f"
|
||||
done
|
||||
fi
|
||||
local rel
|
||||
rel=${f#"$dir"} # path relative to the project temp dir
|
||||
mkdir -p "$(dirname "$SECRETS_DIR/$project/$rel")"
|
||||
age -r "$pubkey" -o "$SECRETS_DIR/$project/${rel}.age" "$f"
|
||||
done < <(find "$dir" -type f)
|
||||
done
|
||||
|
||||
# Commit and push (heal .gitignore first so add -A can't stage key.txt)
|
||||
|
|
|
|||
|
|
@ -698,3 +698,36 @@ m_nojq_path() {
|
|||
run jq -r '.external | length' .secrets.json
|
||||
[ "$output" = "1" ]
|
||||
}
|
||||
|
||||
# ─── J: rekey + list recurse into nested manifest blobs (data-loss guard) ──────
|
||||
|
||||
@test "rekey re-encrypts a nested manifest dotenv blob (survives rotation)" {
|
||||
# Regression: cmd_rekey's non-recursive glob skipped <project>/<relpath>.age
|
||||
# blobs, leaving them on the old key = permanently undecryptable after rotation.
|
||||
init_with_remote
|
||||
create_project_dir nestrekey
|
||||
mkdir -p packages/web
|
||||
echo "N=nested" > packages/web/.env.development
|
||||
"$SECRETS_BIN" add packages/web/.env.development >/dev/null
|
||||
"$SECRETS_BIN" push >/dev/null 2>&1
|
||||
[ -f "$SECRETS_DIR/nestrekey/packages/web/.env.development.age" ]
|
||||
run "$SECRETS_BIN" rekey
|
||||
[ "$status" -eq 0 ]
|
||||
rm -rf packages
|
||||
run "$SECRETS_BIN" pull nestrekey
|
||||
[ "$status" -eq 0 ]
|
||||
[ -f packages/web/.env.development ]
|
||||
[ "$(cat packages/web/.env.development)" = "N=nested" ]
|
||||
}
|
||||
|
||||
@test "list shows a nested manifest blob" {
|
||||
init_with_remote
|
||||
create_project_dir nestlist
|
||||
mkdir -p packages/web
|
||||
echo "N=nested" > packages/web/.env.development
|
||||
"$SECRETS_BIN" add packages/web/.env.development >/dev/null
|
||||
"$SECRETS_BIN" push >/dev/null 2>&1
|
||||
run "$SECRETS_BIN" list
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"packages/web/.env.development"* ]] || false
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue