#!/usr/bin/env bash # # End-to-end test for passv against a real Vault dev server running the # vault-plugin-secrets-gpg engine. Exercises the full pass workflow: init, # insert, show, generate, edit, mv/cp (with cross-gpg-id re-encryption), find, # grep, rm — proving the store round-trips through Vault. # set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PLUGIN_BIN="${PLUGIN_BIN:-${ROOT_DIR}/../vault-plugin-secrets-gpg/dist/vault-plugin-secrets-gpg}" red() { printf '\033[31m%s\033[0m\n' "$*"; } green() { printf '\033[32m%s\033[0m\n' "$*"; } blue() { printf '\033[34m==> %s\033[0m\n' "$*"; } fail() { red "FAIL: $*"; exit 1; } command -v vault >/dev/null || fail "vault binary not found" [ -x "${PLUGIN_BIN}" ] || fail "plugin binary not found at ${PLUGIN_BIN} (build it: make -C ../vault-plugin-secrets-gpg build)" WORK="$(mktemp -d)" PLUGIN_DIR="${WORK}/plugins" export PASSWORD_STORE_DIR="${WORK}/store" mkdir -p "${PLUGIN_DIR}" "${PASSWORD_STORE_DIR}" cp "${PLUGIN_BIN}" "${PLUGIN_DIR}/vault-plugin-secrets-gpg" export VAULT_ADDR="http://127.0.0.1:8281" export VAULT_TOKEN="root" cleanup() { [ -n "${VAULT_PID:-}" ] && kill "${VAULT_PID}" 2>/dev/null || true rm -rf "${WORK}" } trap cleanup EXIT blue "Starting Vault dev server" vault server -dev -dev-root-token-id=root -dev-listen-address=127.0.0.1:8281 \ -dev-plugin-dir="${PLUGIN_DIR}" >"${WORK}/vault.log" 2>&1 & VAULT_PID=$! for i in $(seq 1 30); do vault status >/dev/null 2>&1 && break sleep 0.5 [ "$i" = 30 ] && fail "vault did not become ready" done blue "Enabling gpg secrets engine + creating keys" vault secrets enable -path=gpg vault-plugin-secrets-gpg >/dev/null vault write -field=public_key gpg/keys/personal algorithm=rsa-2048 identity='Personal ' >/dev/null vault write -field=public_key gpg/keys/work algorithm=ed25519 identity='Work ' >/dev/null green "engine ready with keys gpg/personal, gpg/work" PASSV="${ROOT_DIR}/dist/passv" blue "Building passv" make -C "${ROOT_DIR}" build >/dev/null p() { "${PASSV}" "$@"; } blue "init + insert + show" p init gpg/personal printf 's3cr3t\n' | p insert --echo email/gmail printf 'hunter2\n' | p insert --echo email/work [ "$(p show email/gmail)" = "s3cr3t" ] || fail "show email/gmail mismatch" [ "$(p show email/work)" = "hunter2" ] || fail "show email/work mismatch" # Files on disk must be real binary OpenPGP (magic byte 0x85/0x84... gpg packet). head -c1 "${PASSWORD_STORE_DIR}/email/gmail.gpg" | od -An -tx1 | grep -qiE '8[45c]|c[1-9a-f]' \ || fail "email/gmail.gpg is not a binary OpenPGP message" green "insert/show round-trip via Vault OK" blue "generate (+ in-place)" gen="$(p generate -n banking/pin 12 | tail -1)" [ "${#gen}" = 12 ] || fail "generated length wrong: '${gen}'" [ "$(p show banking/pin)" = "${gen}" ] || fail "generated password not stored" green "generate OK (${gen})" blue "grep + find" p grep s3cr3t | grep -q 'email/gmail' || fail "grep did not find secret" p find gmail | grep -q 'email/gmail' || fail "find did not match" green "grep/find OK" blue "mv within same key (verbatim ciphertext move)" p mv email/gmail email/personal-gmail [ "$(p show email/personal-gmail)" = "s3cr3t" ] || fail "mv lost content" p show email/gmail 2>/dev/null && fail "source still present after mv" green "mv OK" blue "cp across keys (re-encrypt personal -> work)" p init -p projects gpg/work p cp email/work projects/shared-login [ "$(p show projects/shared-login)" = "hunter2" ] || fail "cross-key cp lost content" # The copy must now be decryptable by the WORK key specifically. ct="$(base64 -w0 "${PASSWORD_STORE_DIR}/projects/shared-login.gpg")" [ "$(vault write -field=plaintext gpg/decrypt/work ciphertext="${ct}" | base64 -d)" = "hunter2" ] \ || fail "copy was not re-encrypted to the work key" green "cross-key cp re-encrypted correctly" blue "rm" p rm -f banking/pin p show banking/pin 2>/dev/null && fail "entry present after rm" green "rm OK" blue "tree listing" p ls | grep -q 'projects' || fail "tree missing projects dir" if command -v gpg >/dev/null; then blue "dual-mode: one entry readable by BOTH local gpg and Vault" export GNUPGHOME="${WORK}/gnupg" mkdir -p "${GNUPGHOME}"; chmod 700 "${GNUPGHOME}" gpg --batch --passphrase '' --quick-generate-key 'Local Test ' default default never >/dev/null 2>&1 LOCAL_FPR="$(gpg --list-keys --with-colons | awk -F: '/^fpr/{print $10; exit}')" # Import the Vault key's public half so gpg can encrypt to it too. vault read -field=public_key gpg/keys/personal | gpg --batch --import >/dev/null 2>&1 VAULT_FPR="$(vault read -field=fingerprint gpg/keys/personal)" mkdir -p "${PASSWORD_STORE_DIR}/shared" printf '%s\n%s\n' "${LOCAL_FPR}" "${VAULT_FPR}" > "${PASSWORD_STORE_DIR}/shared/.gpg-id" # for plain pass/gpg echo 'gpg/personal' > "${PASSWORD_STORE_DIR}/shared/.vault-id" # for passv # Encrypt to BOTH recipients, the way `pass` would in a multi-id store. printf 'dualsecret' | gpg --batch --yes --trust-model always \ -r "${LOCAL_FPR}" -r "${VAULT_FPR}" --encrypt \ --output "${PASSWORD_STORE_DIR}/shared/db.gpg" [ "$(gpg --batch --decrypt "${PASSWORD_STORE_DIR}/shared/db.gpg" 2>/dev/null)" = "dualsecret" ] \ || fail "local gpg could not decrypt the dual-recipient entry" [ "$(p show shared/db)" = "dualsecret" ] \ || fail "passv (Vault) could not decrypt the dual-recipient entry" green "dual-mode OK: same file opened by local gpg AND Vault" fi green "ALL PASSV END-TO-END CHECKS PASSED"