Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
65 lines
3 KiB
Bash
Executable file
65 lines
3 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Explicit attended custody input only. Publish plaintext only after all files
|
|
# decrypt successfully into a private staging directory; never emit values.
|
|
set +x
|
|
set -euo pipefail
|
|
umask 077
|
|
|
|
fail() { printf 'ERROR: %s\n' "$1" >&2; exit 1; }
|
|
OUTPUT_DIR="${1:-./secrets}"
|
|
AGE_KEY="${2:-${NETKINGDOM_CUSTODY_AGE_KEY_FILE:-}}"
|
|
[[ $# -le 2 ]] || fail "unsupported decrypt argument"
|
|
[[ -n "$AGE_KEY" && -f "$AGE_KEY" ]] || fail "explicit temporary custody key required"
|
|
# Validate an owned, private, non-linked age identity before passing it to age.
|
|
# No HOME/plugin/SSH identity discovery is admitted by this recovery path.
|
|
python3 - "$AGE_KEY" 2>/dev/null <<'PYKEY' || fail "custody key must be an owned private regular age identity"
|
|
import os, pathlib, stat, sys
|
|
p = pathlib.Path(os.path.abspath(sys.argv[1]))
|
|
if any(x.is_symlink() for x in (p, *p.parents)): raise ValueError()
|
|
fd = os.open(p, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
|
|
try:
|
|
st = os.fstat(fd)
|
|
if not stat.S_ISREG(st.st_mode) or st.st_uid != os.getuid() or st.st_mode & 0o077 or st.st_nlink != 1 or st.st_size > 16384:
|
|
raise ValueError()
|
|
data = os.read(fd, 16385).decode('ascii')
|
|
values = [x for x in data.splitlines() if x and not x.startswith('#')]
|
|
if len(values) != 1 or not values[0].startswith('AGE-SECRET-KEY-1'): raise ValueError()
|
|
finally: os.close(fd)
|
|
PYKEY
|
|
|
|
ENC_DIR="$(dirname "$OUTPUT_DIR")/secrets.enc"
|
|
[[ -d "$ENC_DIR" && ! -L "$ENC_DIR" ]] || fail "encrypted source directory unavailable"
|
|
[[ ! -e "$OUTPUT_DIR" && ! -L "$OUTPUT_DIR" ]] || fail "output already exists"
|
|
[[ -d "$(dirname "$OUTPUT_DIR")" ]] || fail "output parent unavailable"
|
|
|
|
STAGING=$(mktemp -d "$(dirname "$OUTPUT_DIR")/.netkingdom-decrypt.XXXXXXXX")
|
|
cleanup() {
|
|
if [[ -d "$STAGING" ]]; then
|
|
find "$STAGING" -type f -exec shred -u {} \; 2>/dev/null || true
|
|
rm -rf -- "$STAGING"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'exit 129' HUP
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
|
|
count=0
|
|
for component_dir in "$ENC_DIR"/*; do
|
|
[[ -e "$component_dir" || -L "$component_dir" ]] || continue
|
|
[[ -d "$component_dir" && ! -L "$component_dir" ]] || fail "unexpected encrypted component"
|
|
component=$(basename "$component_dir")
|
|
mkdir "$STAGING/$component"
|
|
for file in "$component_dir"/*.age; do
|
|
[[ -e "$file" || -L "$file" ]] || continue
|
|
[[ -f "$file" && ! -L "$file" ]] || fail "unexpected encrypted input"
|
|
name=$(basename "${file%.age}")
|
|
age -d -i "$AGE_KEY" -o "$STAGING/$component/$name" "$file" 2>/dev/null || fail "decryption failed; partial plaintext removed"
|
|
count=$((count + 1))
|
|
done
|
|
done
|
|
[[ "$count" -gt 0 ]] || fail "no encrypted files found"
|
|
# Never replace an output created by another process during decryption.
|
|
mv -Tn -- "$STAGING" "$OUTPUT_DIR" || fail "could not publish private output"
|
|
[[ ! -d "$STAGING" ]] || fail "output appeared during decryption"
|
|
printf '%s file(s) decrypted to private output; remove plaintext after the approved operation.\n' "$count"
|