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
|
# Skip hidden dirs
|
||||||
[[ "$project" == .* ]] && continue
|
[[ "$project" == .* ]] && continue
|
||||||
echo "$project:"
|
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
|
[ -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
|
found=1
|
||||||
done
|
done < <(find "$dir" -type f -name '*.age' | sort)
|
||||||
# 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
|
done
|
||||||
|
|
||||||
if [ "$found" -eq 0 ]; then
|
if [ "$found" -eq 0 ]; then
|
||||||
|
|
@ -1681,29 +1682,23 @@ cmd_rekey() {
|
||||||
project=$(basename "$dir")
|
project=$(basename "$dir")
|
||||||
[[ "$project" == .* ]] && continue
|
[[ "$project" == .* ]] && continue
|
||||||
mkdir -p "$tmpdir/$project"
|
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
|
[ -f "$f" ] || continue
|
||||||
local name
|
local rel dest
|
||||||
name=$(basename "$f" .age)
|
rel=${f#"$dir"} # path relative to the project dir (keeps .age)
|
||||||
if ! age -d -i "$KEY_FILE" -o "$tmpdir/$project/$name" "$f"; then
|
rel=${rel%.age} # strip the .age suffix → original relpath
|
||||||
die "Decryption failed for $project/$name. Rekey aborted. Old key preserved."
|
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
|
fi
|
||||||
file_count=$((file_count + 1))
|
file_count=$((file_count + 1))
|
||||||
done
|
done < <(find "$dir" -type f -name '*.age')
|
||||||
# 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
|
done
|
||||||
|
|
||||||
if [ "$file_count" -eq 0 ]; then
|
if [ "$file_count" -eq 0 ]; then
|
||||||
|
|
@ -1723,29 +1718,23 @@ cmd_rekey() {
|
||||||
|
|
||||||
info "Re-encrypting all files with new key..."
|
info "Re-encrypting all files with new key..."
|
||||||
|
|
||||||
# Re-encrypt all files. The ".*" glob is required: dotenv files decrypt
|
# Re-encrypt all files. `find -type f` recurses into nested dotenv dirs and
|
||||||
# to dotfiles ("$tmpdir/p/.env") that a bare "*" would silently skip,
|
# external/ and natively includes dotfiles (decrypted dotenv files like
|
||||||
# leaving their blobs on the old key (undecryptable after rotation).
|
# "$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
|
for dir in "$tmpdir"/*/; do
|
||||||
[ -d "$dir" ] || continue
|
[ -d "$dir" ] || continue
|
||||||
local project
|
local project
|
||||||
project=$(basename "$dir")
|
project=$(basename "$dir")
|
||||||
mkdir -p "$SECRETS_DIR/$project"
|
mkdir -p "$SECRETS_DIR/$project"
|
||||||
for f in "$dir"* "$dir".*; do
|
while IFS= read -r f; do
|
||||||
[ -f "$f" ] || continue
|
[ -f "$f" ] || continue
|
||||||
local name
|
local rel
|
||||||
name=$(basename "$f")
|
rel=${f#"$dir"} # path relative to the project temp dir
|
||||||
age -r "$pubkey" -o "$SECRETS_DIR/$project/${name}.age" "$f"
|
mkdir -p "$(dirname "$SECRETS_DIR/$project/$rel")"
|
||||||
done
|
age -r "$pubkey" -o "$SECRETS_DIR/$project/${rel}.age" "$f"
|
||||||
if [ -d "${dir}external" ]; then
|
done < <(find "$dir" -type f)
|
||||||
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
|
|
||||||
done
|
done
|
||||||
|
|
||||||
# Commit and push (heal .gitignore first so add -A can't stage key.txt)
|
# 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
|
run jq -r '.external | length' .secrets.json
|
||||||
[ "$output" = "1" ]
|
[ "$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