From 1b527613367cf0b3f42be75d7b557eccb024e1e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Mar 2026 18:48:05 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20feat:=20disinto=20secrets=20add=20?= =?UTF-8?q?=E2=80=94=20store=20individual=20encrypted=20secrets=20(#31)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 3 ++ bin/disinto | 84 ++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 79 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index dd9365d..bcc5231 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ metrics/supervisor-metrics.jsonl .DS_Store dev/ci-fixes-*.json gardener/dust.jsonl + +# Individual encrypted secrets (managed by disinto secrets add) +secrets/ diff --git a/bin/disinto b/bin/disinto index 5f74751..71a922a 100755 --- a/bin/disinto +++ b/bin/disinto @@ -2023,7 +2023,78 @@ disinto_secrets() { fi } + local secrets_dir="${FACTORY_ROOT}/secrets" + local age_key_file="${HOME}/.config/sops/age/keys.txt" + + # Shared helper: ensure age key exists and export AGE_PUBLIC_KEY + _secrets_ensure_age_key() { + if ! command -v age &>/dev/null; then + echo "Error: age is required." >&2 + echo " Install age: apt install age / brew install age" >&2 + exit 1 + fi + if [ ! -f "$age_key_file" ]; then + echo "Error: age key not found at ${age_key_file}" >&2 + echo " Run 'disinto init' to generate one, or create manually with:" >&2 + echo " mkdir -p ~/.config/sops/age && age-keygen -o ${age_key_file}" >&2 + exit 1 + fi + AGE_PUBLIC_KEY="$(age-keygen -y "$age_key_file" 2>/dev/null)" + if [ -z "$AGE_PUBLIC_KEY" ]; then + echo "Error: failed to read public key from ${age_key_file}" >&2 + exit 1 + fi + export AGE_PUBLIC_KEY + } + case "$subcmd" in + add) + local name="${2:-}" + if [ -z "$name" ]; then + echo "Usage: disinto secrets add " >&2 + exit 1 + fi + _secrets_ensure_age_key + mkdir -p "$secrets_dir" + + printf 'Enter value for %s: ' "$name" >&2 + local value + IFS= read -r value + if [ -z "$value" ]; then + echo "Error: empty value" >&2 + exit 1 + fi + + local enc_path="${secrets_dir}/${name}.enc" + if ! printf '%s' "$value" | age -r "$AGE_PUBLIC_KEY" -o "$enc_path"; then + echo "Error: encryption failed" >&2 + exit 1 + fi + echo "Stored: ${enc_path}" + ;; + show) + local name="${2:-}" + if [ -n "$name" ]; then + # Show individual secret: disinto secrets show + local enc_path="${secrets_dir}/${name}.enc" + if [ ! -f "$enc_path" ]; then + echo "Error: ${enc_path} not found" >&2 + exit 1 + fi + if [ ! -f "$age_key_file" ]; then + echo "Error: age key not found at ${age_key_file}" >&2 + exit 1 + fi + age -d -i "$age_key_file" "$enc_path" + else + # Show all agent secrets: disinto secrets show + if [ ! -f "$enc_file" ]; then + echo "Error: ${enc_file} not found." >&2 + exit 1 + fi + sops -d "$enc_file" + fi + ;; edit) if [ ! -f "$enc_file" ]; then echo "Error: ${enc_file} not found. Run 'disinto secrets migrate' first." >&2 @@ -2031,13 +2102,6 @@ disinto_secrets() { fi sops "$enc_file" ;; - show) - if [ ! -f "$enc_file" ]; then - echo "Error: ${enc_file} not found." >&2 - exit 1 - fi - sops -d "$enc_file" - ;; migrate) if [ ! -f "$env_file" ]; then echo "Error: ${env_file} not found — nothing to migrate." >&2 @@ -2077,9 +2141,13 @@ disinto_secrets() { cat <&2 Usage: disinto secrets +Individual secrets (secrets/.enc): + add Prompt for value, encrypt, store in secrets/.enc + show Decrypt and print an individual secret + Agent secrets (.env.enc): edit Edit agent secrets (FORGE_TOKEN, CLAUDE_API_KEY, etc.) - show Show decrypted agent secrets + show Show decrypted agent secrets (no argument) migrate Encrypt .env -> .env.enc Vault secrets (.env.vault.enc): From ec58cb17457b495ce9177f12d9b388cd5d080558 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Mar 2026 19:10:47 +0000 Subject: [PATCH 2/2] fix: suppress terminal echo for secret input and guard against overwrites - Use `read -rs` to hide typed secret value from terminal - Prompt for confirmation before overwriting an existing secret Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/disinto | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/bin/disinto b/bin/disinto index 71a922a..c4ba0f9 100755 --- a/bin/disinto +++ b/bin/disinto @@ -2059,13 +2059,23 @@ disinto_secrets() { printf 'Enter value for %s: ' "$name" >&2 local value - IFS= read -r value + IFS= read -rs value + echo >&2 if [ -z "$value" ]; then echo "Error: empty value" >&2 exit 1 fi local enc_path="${secrets_dir}/${name}.enc" + if [ -f "$enc_path" ]; then + printf 'Secret %s already exists. Overwrite? [y/N] ' "$name" >&2 + local confirm + read -r confirm + if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then + echo "Aborted." >&2 + exit 1 + fi + fi if ! printf '%s' "$value" | age -r "$AGE_PUBLIC_KEY" -o "$enc_path"; then echo "Error: encryption failed" >&2 exit 1