#!/usr/bin/env bash
# vault-tools.sh — helpers for reading and updating an openssl-encrypted
# secrets file (secrets.sh.enc) without ever writing decrypted plaintext
# to persistent disk storage.
#
# Source this file (e.g. from ~/.bashrc) to get the vault_set, vault_unset,
# vault_edit, and vault_list functions in your interactive shell.
#
# Security notes (read before relying on this):
#   - Decrypted content only ever lives in shell variables, or, for
#     vault_edit, a file under /dev/shm (tmpfs/RAM-backed). Neither
#     guarantees the plaintext is scrubbed from RAM — `unset` and `rm`
#     free the memory/inode but don't zero the underlying bytes. That's
#     an inherent limit of doing this in bash, not something these
#     functions can fully close.
#   - /dev/shm is "never touches disk" only as long as the system isn't
#     under enough memory pressure to swap. If swap is enabled and
#     unencrypted, that's a real (if edge-case) gap for vault_edit.
#   - This gives confidentiality via AES-256-CBC, not authenticated
#     encryption — same trade-off discussed earlier, unchanged here.
#
# Each function runs under `set -euo pipefail`, scoped to itself via
# `local -` so it doesn't change shell options in whatever interactive
# shell you sourced this into (a bare `set -e` at the top of a *sourced*
# file would do that, silently, for the rest of your session).
#
# Typical login setup (e.g. in ~/.bash_profile):
#   source ~/vault-tools.sh
#   source <(vault_load)
# This prompts once for the vault password and exports everything in
# the vault into your login shell. Caveat: if decryption fails, this
# exact form doesn't always fail loudly — a missing vault produces no
# output, so `source` trivially "succeeds" on empty input and nothing
# gets loaded; a wrong password usually (not always) surfaces as a
# visible syntax error, since openssl can emit partial garbled output
# before it detects the bad padding. The stderr message from vault_load
# itself always prints either way, so you'll see *something*, but if
# you want a guaranteed clean failure instead of relying on that:
#   if vault_secrets="$(vault_load)"; then
#       source <(printf '%s' "$vault_secrets")
#   else
#       echo "vault load failed, nothing was set" >&2
#   fi
#   unset vault_secrets

# To point at a non-default vault path, export VAULT_FILE before sourcing
# this file, e.g.: export VAULT_FILE=/opt/secrets/team.sh.enc
VAULT_FILE="${VAULT_FILE:-secrets.sh.enc}"
VAULT_CIPHER_OPTS=(-aes-256-cbc -pbkdf2)

# vault_load — decrypt to stdout, meant to be used as
# `source <(vault_load)` to pull every vaulted variable into your current
# shell. Kept as its own function so the cipher settings live in one
# place (VAULT_CIPHER_OPTS) instead of being duplicated in .bash_profile.
vault_load() {
    local -
    set -euo pipefail
    if [[ ! -e "$VAULT_FILE" ]]; then
        echo "${VAULT_FILE} doesn't exist yet — run vault_init first." >&2
        return 1
    fi
    openssl enc "${VAULT_CIPHER_OPTS[@]}" -d -in "$VAULT_FILE"
}

# vault_init — create a brand-new vault. Refuses to run if VAULT_FILE
# already exists, so this can't be used to accidentally clobber one —
# use vault_edit or vault_set to change an existing vault.
vault_init() {
    local -
    set -euo pipefail

    if [[ -e "$VAULT_FILE" ]]; then
        echo "${VAULT_FILE} already exists — refusing to overwrite it. Use vault_edit or vault_set instead." >&2
        return 1
    fi

    local tmp_out seed
    seed=$'# managed by vault-tools.sh — use vault_set/vault_unset/vault_edit\n# only lines of the form "export VAR=value" are recognized by vault_list/vault_set\n'
    tmp_out="$(mktemp)"
    # No -pass here on purpose: openssl prompts twice (enter + verify) when
    # none is given, which is exactly what you want on first-time
    # encryption — a typo here with nothing else to fall back on would
    # otherwise lock the vault before it holds anything worth losing.
    if printf '%s' "$seed" | openssl enc "${VAULT_CIPHER_OPTS[@]}" -salt -out "$tmp_out"; then
        mv -f "$tmp_out" "$VAULT_FILE"
        chmod 600 "$VAULT_FILE"
        echo "Created ${VAULT_FILE}. Use vault_set to add your first secret."
    else
        echo "Failed to create ${VAULT_FILE}." >&2
        rm -f "$tmp_out"
        return 1
    fi
}

# vault_set VAR VALUE — add or update a single exported variable.
vault_set() {
    local -            # scope any 'set' changes below to this function only
    set -euo pipefail  # safe here — every risky command is if/&&-guarded below
    local var_name="${1:-}" var_value="${2:-}"
    if [[ -z "$var_name" || -z "$var_value" ]]; then
        echo "usage: vault_set VAR_NAME VALUE" >&2
        return 1
    fi
    if [[ ! "$var_name" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
        echo "Invalid variable name: ${var_name}" >&2
        return 1
    fi

    local plaintext new_content="" found=0
    if [[ ! -e "$VAULT_FILE" ]]; then
        echo "${VAULT_FILE} doesn't exist yet — run vault_init first." >&2
        return 1
    fi
    if ! plaintext="$(openssl enc "${VAULT_CIPHER_OPTS[@]}" -d -in "$VAULT_FILE")"; then
        echo "Failed to decrypt ${VAULT_FILE} — check your password. Aborting." >&2
        return 1
    fi
    trap 'unset plaintext new_content' RETURN

    local new_line="export ${var_name}=$(printf '%q' "$var_value")"
    while IFS= read -r line; do
        if [[ "$line" == "export ${var_name}="* ]]; then
            new_content+="${new_line}"$'\n'
            found=1
        else
            new_content+="${line}"$'\n'
        fi
    done <<< "$plaintext"
    [[ "$found" -eq 0 ]] && new_content+="${new_line}"$'\n'

    local tmp_out
    tmp_out="$(mktemp)"
    if printf '%s' "$new_content" | openssl enc "${VAULT_CIPHER_OPTS[@]}" -salt -out "$tmp_out" \
        && mv -f "$tmp_out" "$VAULT_FILE"; then
        echo "Updated ${var_name} in ${VAULT_FILE}."
    else
        echo "Failed to write updated vault — ${VAULT_FILE} left unchanged." >&2
        rm -f "$tmp_out"
        return 1
    fi
}

# vault_unset VAR — remove a variable entirely.
vault_unset() {
    local -
    set -euo pipefail
    local var_name="${1:-}"
    if [[ -z "$var_name" ]]; then
        echo "usage: vault_unset VAR_NAME" >&2
        return 1
    fi

    local plaintext new_content=""
    if [[ ! -e "$VAULT_FILE" ]]; then
        echo "${VAULT_FILE} doesn't exist yet — run vault_init first." >&2
        return 1
    fi
    if ! plaintext="$(openssl enc "${VAULT_CIPHER_OPTS[@]}" -d -in "$VAULT_FILE")"; then
        echo "Failed to decrypt ${VAULT_FILE} — check your password. Aborting." >&2
        return 1
    fi
    trap 'unset plaintext new_content' RETURN

    while IFS= read -r line; do
        [[ "$line" == "export ${var_name}="* ]] && continue
        new_content+="${line}"$'\n'
    done <<< "$plaintext"

    local tmp_out
    tmp_out="$(mktemp)"
    if printf '%s' "$new_content" | openssl enc "${VAULT_CIPHER_OPTS[@]}" -salt -out "$tmp_out" \
        && mv -f "$tmp_out" "$VAULT_FILE"; then
        echo "Removed ${var_name} from ${VAULT_FILE}."
    else
        echo "Failed to write updated vault — ${VAULT_FILE} left unchanged." >&2
        rm -f "$tmp_out"
        return 1
    fi
}

# vault_list — show which variable names exist, without printing values.
vault_list() {
    local -
    set -euo pipefail
    local plaintext
    if [[ ! -e "$VAULT_FILE" ]]; then
        echo "${VAULT_FILE} doesn't exist yet — run vault_init first." >&2
        return 1
    fi
    if ! plaintext="$(openssl enc "${VAULT_CIPHER_OPTS[@]}" -d -in "$VAULT_FILE")"; then
        echo "Failed to decrypt ${VAULT_FILE} — check your password." >&2
        return 1
    fi
    # `|| true`: an empty vault makes grep report "no match" (exit 1), which
    # is correct output, not a real failure — don't let pipefail+errexit
    # treat that as an error.
    grep '^export ' <<< "$plaintext" | sed -E 's/^export ([A-Za-z_][A-Za-z0-9_]*)=.*/\1/' || true
    unset plaintext
}

# vault_edit — open the decrypted vault in $EDITOR for free-form changes.
# Uses /dev/shm (tmpfs/RAM) instead of a regular temp file, so the
# plaintext copy never touches a real disk-backed filesystem (see the
# swap caveat above). This is the fallback for edits vault_set/vault_unset
# don't cover cleanly (reordering, multi-variable changes, comments, etc).
vault_edit() {
    local -
    set -euo pipefail
    local tmp
    if [[ ! -e "$VAULT_FILE" ]]; then
        echo "${VAULT_FILE} doesn't exist yet — run vault_init first." >&2
        return 1
    fi
    tmp="$(mktemp --tmpdir=/dev/shm vault.XXXXXX)"
    trap 'shred -u "$tmp" 2>/dev/null || rm -f "$tmp"' RETURN

    if ! openssl enc "${VAULT_CIPHER_OPTS[@]}" -d -in "$VAULT_FILE" -out "$tmp"; then
        echo "Failed to decrypt ${VAULT_FILE} — check your password. Aborting." >&2
        return 1
    fi

    "${EDITOR:-vi}" "$tmp"

    if ! openssl enc "${VAULT_CIPHER_OPTS[@]}" -salt -in "$tmp" -out "$VAULT_FILE"; then
        echo "Re-encryption failed — ${VAULT_FILE} was NOT updated." >&2
        return 1
    fi
}
