diff --git a/archon-keymaster/scripts/common.sh b/archon-keymaster/scripts/common.sh new file mode 100755 index 0000000..79305d6 --- /dev/null +++ b/archon-keymaster/scripts/common.sh @@ -0,0 +1,294 @@ +#!/bin/bash +# common.sh — Shared environment loader for archon-keymaster scripts +# +# Sources archon environment (passphrase, wallet path, config dir). +# Supports both CLI (personal wallet) and API (node keymaster) modes. +# +# Usage: source "$(dirname "$0")/../common.sh" +# or: source "$(dirname "$0")/common.sh" (from same dir) +# +# Environment variables (set before sourcing, or auto-detected): +# ARCHON_MODE "cli" (default) or "api" +# ARCHON_API_URL Keymaster API URL (default: http://localhost:4226) +# ARCHON_CONFIG_DIR Directory containing wallet.json (for CLI mode) +# ARCHON_WALLET_PATH Path to wallet.json (for CLI mode) +# ARCHON_PASSPHRASE Wallet passphrase (for CLI mode) +# +# Environment files searched (first found wins): +# $ARCHON_ENV_FILE (explicit override) +# ~/.config/hex/archon.env +# ~/.config/archon/archon.env +# ~/.archon.env + +# --- Environment loading --- + +_archon_load_env() { + # Already loaded? + if [ -n "$_ARCHON_ENV_LOADED" ]; then + return 0 + fi + + # Search for env file + local env_file="${ARCHON_ENV_FILE:-}" + if [ -z "$env_file" ]; then + for candidate in \ + "$HOME/.config/hex/archon.env" \ + "$HOME/.config/archon/archon.env" \ + "$HOME/.archon.env"; do + if [ -f "$candidate" ]; then + env_file="$candidate" + break + fi + done + fi + + if [ -n "$env_file" ] && [ -f "$env_file" ]; then + source "$env_file" + export ARCHON_PASSPHRASE # npx subprocesses need this + fi + + # Set defaults + export ARCHON_MODE="${ARCHON_MODE:-cli}" + export ARCHON_API_URL="${ARCHON_API_URL:-http://localhost:4226}" + + _ARCHON_ENV_LOADED=1 +} + +# --- Mode detection --- + +archon_is_api_mode() { + [ "$ARCHON_MODE" = "api" ] +} + +# --- CLI helpers --- + +# Run a keymaster CLI command (personal wallet) +archon_cli() { + if [ -z "$ARCHON_CONFIG_DIR" ] && [ -z "$ARCHON_WALLET_PATH" ]; then + echo "ERROR: ARCHON_CONFIG_DIR or ARCHON_WALLET_PATH must be set for CLI mode" >&2 + return 1 + fi + npx --yes @didcid/keymaster "$@" +} + +# --- API helpers --- + +# GET request to keymaster API +archon_api_get() { + local path="$1" + curl -sf "${ARCHON_API_URL}/api/v1${path}" +} + +# POST request to keymaster API +archon_api_post() { + local path="$1" + local data="$2" + if [ -n "$data" ]; then + curl -sf -X POST "${ARCHON_API_URL}/api/v1${path}" \ + -H "Content-Type: application/json" \ + -d "$data" + else + curl -sf -X POST "${ARCHON_API_URL}/api/v1${path}" + fi +} + +# DELETE request to keymaster API +archon_api_delete() { + local path="$1" + curl -sf -X DELETE "${ARCHON_API_URL}/api/v1${path}" +} + +# --- Vault operations (mode-aware) --- + +# List vault items (works in both modes) +archon_list_vault_items() { + local vault_id="$1" + if archon_is_api_mode; then + archon_api_get "/vaults/${vault_id}/items" + else + archon_cli list-vault-items "$vault_id" + fi +} + +# Get vault item (works in both modes) +archon_get_vault_item() { + local vault_id="$1" + local item_name="$2" + local output="$3" + if archon_is_api_mode; then + curl -sf "${ARCHON_API_URL}/api/v1/vaults/${vault_id}/items/${item_name}" -o "$output" + else + archon_cli get-vault-item "$vault_id" "$item_name" "$output" + fi +} + +# Add vault item (works in both modes) +archon_add_vault_item() { + local vault_id="$1" + local file_path="$2" + if archon_is_api_mode; then + local filename=$(basename "$file_path") + curl -sf -X POST "${ARCHON_API_URL}/api/v1/vaults/${vault_id}/items" \ + -F "file=@${file_path};filename=${filename}" + else + archon_cli add-vault-item "$vault_id" "$file_path" + fi +} + +# Remove vault item (works in both modes) +archon_remove_vault_item() { + local vault_id="$1" + local item_name="$2" + if archon_is_api_mode; then + archon_api_delete "/vaults/${vault_id}/items/${item_name}" + else + archon_cli remove-vault-item "$vault_id" "$item_name" + fi +} + +# List vault members (works in both modes) +archon_list_vault_members() { + local vault_id="$1" + if archon_is_api_mode; then + archon_api_get "/vaults/${vault_id}/members" + else + archon_cli list-vault-members "$vault_id" + fi +} + +# Add vault member (works in both modes) +archon_add_vault_member() { + local vault_id="$1" + local member="$2" + if archon_is_api_mode; then + archon_api_post "/vaults/${vault_id}/members" "{\"member\": \"$member\"}" + else + archon_cli add-vault-member "$vault_id" "$member" + fi +} + +# Remove vault member (works in both modes) +archon_remove_vault_member() { + local vault_id="$1" + local member="$2" + if archon_is_api_mode; then + archon_api_delete "/vaults/${vault_id}/members/${member}" + else + archon_cli remove-vault-member "$vault_id" "$member" + fi +} + +# --- Group operations (mode-aware) --- + +# Create group +archon_create_group() { + local name="$1" + local registry="${2:-hyperswarm}" + if archon_is_api_mode; then + archon_api_post "/groups" "{\"name\": \"$name\", \"registry\": \"$registry\"}" + else + archon_cli create-group "$name" -r "$registry" + fi +} + +# Add group member +archon_add_group_member() { + local group="$1" + local member="$2" + if archon_is_api_mode; then + archon_api_post "/groups/${group}/add" "{\"member\": \"$member\"}" + else + archon_cli add-group-member "$group" "$member" + fi +} + +# Test group membership +archon_test_group_member() { + local group="$1" + local member="$2" + if archon_is_api_mode; then + archon_api_get "/groups/${group}/test/${member}" | jq -r '.member // false' + else + archon_cli test-group-member "$group" "$member" + fi +} + +# Get group info +archon_get_group() { + local group="$1" + if archon_is_api_mode; then + archon_api_get "/groups/${group}" + else + archon_cli resolve-did "$group" 2>/dev/null | jq '.didDocumentData.group' + fi +} + +# --- Alias operations (mode-aware) --- + +archon_add_alias() { + local alias="$1" + local did="$2" + if archon_is_api_mode; then + archon_api_post "/aliases" "{\"alias\": \"$alias\", \"did\": \"$did\"}" + else + archon_cli add-alias "$alias" "$did" + fi +} + +archon_list_aliases() { + if archon_is_api_mode; then + archon_api_get "/aliases" | jq -r '.aliases' + else + archon_cli list-aliases + fi +} + +# --- Transfer operations --- + +archon_transfer_asset() { + local asset="$1" + local controller="$2" + if archon_is_api_mode; then + archon_api_post "/assets/${asset}/transfer" "{\"controller\": \"$controller\"}" + else + archon_cli transfer-asset "$asset" "$controller" + fi +} + +# --- Passphrase operations --- + +archon_change_passphrase() { + local new_passphrase="$1" + if archon_is_api_mode; then + archon_api_post "/wallet/passphrase" "{\"passphrase\": \"$new_passphrase\"}" + else + archon_cli change-passphrase "$new_passphrase" + fi +} + +# --- Utility --- + +# Resolve a vault name to DID (checks aliases, then treats as DID) +archon_resolve_vault() { + local vault_ref="$1" + if [[ "$vault_ref" == did:* ]]; then + echo "$vault_ref" + return 0 + fi + # Try alias lookup + local did + if archon_is_api_mode; then + did=$(archon_api_get "/aliases" 2>/dev/null | jq -r ".aliases.\"$vault_ref\" // empty") + else + did=$(archon_cli list-aliases 2>/dev/null | jq -r ".\"$vault_ref\" // empty") + fi + if [ -n "$did" ]; then + echo "$did" + else + echo "ERROR: Could not resolve vault '$vault_ref'" >&2 + return 1 + fi +} + +# Auto-load on source +_archon_load_env diff --git a/archon-keymaster/scripts/secrets/README.md b/archon-keymaster/scripts/secrets/README.md new file mode 100644 index 0000000..cb0bcac --- /dev/null +++ b/archon-keymaster/scripts/secrets/README.md @@ -0,0 +1,98 @@ +# Archon Secrets Vault + +Store and retrieve sensitive files in encrypted Archon group vaults. Designed for shared access — multiple DIDs can decrypt the same vault for disaster recovery. + +## Why + +- Secrets should never live in git repos — even private ones +- Archon vaults encrypt data end-to-end with DID keys +- Group vaults let trusted parties (e.g., human + agent) both access secrets +- `--to-ram` mode loads secrets to tmpfs — nothing touches disk +- Backed by IPFS — distributed, resilient, no third-party access + +## Scripts + +| Script | Purpose | +|--------|---------| +| `store.sh` | Pack and encrypt secrets into a vault | +| `restore.sh` | Decrypt and restore secrets from a vault | +| `status.sh` | Check vault status, members, and contents | + +## Quick Start + +```bash +# Set up archon environment +export ARCHON_CONFIG_DIR="/path/to/your/archon/wallet" +source /path/to/archon.env + +# Store secrets with a shared vault (add a trusted DID) +./store.sh --vault my-secrets --dir ~/.config/hex --member did:cid:trusted-partner-did + +# Check vault status +./status.sh --vault my-secrets + +# Restore to disk +./restore.sh --vault my-secrets --dir ~/.config/hex + +# Restore to RAM only (nothing on disk, wiped on reboot) +./restore.sh --vault my-secrets --to-ram + +# Dry run +./restore.sh --vault my-secrets --dry-run + +# Restore as a vault member (shared vault, use DID directly) +./restore.sh --did did:cid:vault-did-here --dir ~/.config/hex +``` + +## Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `SECRETS_VAULT_NAME` | `secrets` | Vault name/alias | +| `SECRETS_VAULT_DID` | *(from alias)* | Vault DID (for shared vaults) | +| `SECRETS_DIR` | `~/.config` | Directory to store/restore | +| `SECRETS_PATTERN` | `*.env` | Glob pattern for files to include | +| `ARCHON_CONFIG_DIR` | *(required)* | Path to your Archon wallet directory | + +## Security Model + +- Files are packed into a tarball, encrypted with vault keys, stored on IPFS +- Only the vault owner and added members can decrypt +- `--to-ram` writes to `/dev/shm` (tmpfs) — wiped on reboot, never touches disk +- Restored files get `chmod 600` automatically +- Temp files cleaned up on exit (even on error) + +## Group Vault (Recommended) + +For disaster recovery, create a shared vault with your trusted partner: + +```bash +# Owner creates vault with a member +./store.sh --vault shared-secrets \ + --dir ~/.config/hex \ + --member did:cid:partner-did + +# Partner restores from the shared vault +./restore.sh --did did:cid:vault-did --dir ~/.config/hex --to-ram +``` + +Both parties can decrypt. If one loses access, the other can recover. + +## Systemd Integration + +To auto-load secrets from vault on boot: + +```ini +[Unit] +Description=Load secrets from Archon vault to RAM +After=archon.service + +[Service] +Type=oneshot +Environment=ARCHON_CONFIG_DIR=/path/to/wallet +ExecStart=/path/to/restore.sh --vault my-secrets --to-ram +RemainAfterExit=yes + +[Install] +WantedBy=default.target +``` diff --git a/archon-keymaster/scripts/secrets/restore.sh b/archon-keymaster/scripts/secrets/restore.sh new file mode 100755 index 0000000..db5c87a --- /dev/null +++ b/archon-keymaster/scripts/secrets/restore.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# Restore secrets from an Archon vault +# Usage: restore.sh [--vault NAME] [--dir PATH] [--to-ram] [--api] [--dry-run] +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$SCRIPT_DIR/../common.sh" + +VAULT_NAME="${SECRETS_VAULT_NAME:-secrets}" +VAULT_DID="${SECRETS_VAULT_DID:-}" +SECRETS_DIR="${SECRETS_DIR:-$HOME/.config}" +LABEL="secrets-bundle" +DRY_RUN=false +TO_RAM=false + +# --- Input validation --- +_validate_name() { + local name="$1" field="$2" + if [[ ! "$name" =~ ^[a-zA-Z0-9._-]+$ ]]; then + echo "ERROR: Invalid $field: '$name' (alphanumeric, dots, hyphens, underscores only)" >&2 + exit 1 + fi +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --vault) VAULT_NAME="$2"; shift 2 ;; + --did) VAULT_DID="$2"; shift 2 ;; + --dir) SECRETS_DIR="$2"; shift 2 ;; + --label) LABEL="$2"; shift 2 ;; + --to-ram) TO_RAM=true; shift ;; + --dry-run) DRY_RUN=true; shift ;; + --api) export ARCHON_MODE="api"; shift ;; + -h|--help) + echo "Usage: restore.sh [OPTIONS]" + echo " --vault NAME Vault name/alias (default: secrets)" + echo " --did DID Vault DID directly (for shared vaults)" + echo " --dir PATH Restore destination (default: ~/.config)" + echo " --to-ram Restore to /dev/shm (RAM) instead of disk" + echo " --api Use keymaster API instead of CLI" + echo " --dry-run Show contents without writing" + exit 0 ;; + *) echo "Unknown: $1" >&2; exit 1 ;; + esac +done + +# Validate inputs +_validate_name "$VAULT_NAME" "vault name" +_validate_name "$LABEL" "label" + +if [ -n "$VAULT_DID" ] && [[ ! "$VAULT_DID" =~ ^did: ]]; then + echo "ERROR: --did must be a DID (starts with 'did:')" >&2 + exit 1 +fi + +# Resolve vault DID +if [ -z "$VAULT_DID" ]; then + VAULT_DID=$(archon_resolve_vault "$VAULT_NAME") || exit 1 +fi + +if [ "$TO_RAM" = true ]; then + SECRETS_DIR="/dev/shm/${VAULT_NAME}" +fi + +WORK_DIR=$(mktemp -d) +trap 'rm -rf "$WORK_DIR"' EXIT +BUNDLE="$WORK_DIR/${LABEL}.tar.gz" + +echo "🔓 Downloading from vault..." +echo " DID: ${VAULT_DID:0:50}..." +archon_get_vault_item "$VAULT_DID" "${LABEL}.tar.gz" "$BUNDLE" + +if [ ! -f "$BUNDLE" ] || [ ! -s "$BUNDLE" ]; then + echo "❌ Failed to retrieve '${LABEL}.tar.gz' from vault." >&2 + echo " Available items:" + archon_list_vault_items "$VAULT_DID" 2>/dev/null | jq -r 'keys[]' 2>/dev/null || true + exit 1 +fi + +# Validate tarball — reject path traversal attempts +echo "📦 Validating bundle..." +if tar tzf "$BUNDLE" 2>/dev/null | grep -qE '(^/|\.\./)'; then + echo "❌ SECURITY: Tarball contains absolute or traversal paths — aborting!" >&2 + exit 1 +fi + +echo "📦 Bundle contents:" +tar tzf "$BUNDLE" 2>/dev/null | sed 's/^/ /' + +if [ "$DRY_RUN" = true ]; then + echo ""; echo "🔍 Dry run — no files written."; exit 0 +fi + +mkdir -p "$SECRETS_DIR" +chmod 700 "$SECRETS_DIR" +tar xzf "$BUNDLE" -C "$SECRETS_DIR" +find "$SECRETS_DIR" -maxdepth 1 -name "*.env" -exec chmod 600 {} \; + +COUNT=$(tar tzf "$BUNDLE" 2>/dev/null | wc -l) +LOCATION="disk ($SECRETS_DIR)" +[ "$TO_RAM" = true ] && LOCATION="RAM ($SECRETS_DIR — wiped on reboot)" +echo "" +echo "✅ Restored $COUNT files to $LOCATION" diff --git a/archon-keymaster/scripts/secrets/status.sh b/archon-keymaster/scripts/secrets/status.sh new file mode 100755 index 0000000..26286ce --- /dev/null +++ b/archon-keymaster/scripts/secrets/status.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# Check secrets vault status +# Usage: status.sh [--vault NAME] [--did DID] [--api] +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$SCRIPT_DIR/../common.sh" + +VAULT_NAME="${SECRETS_VAULT_NAME:-secrets}" +VAULT_DID="${SECRETS_VAULT_DID:-}" + +# --- Input validation --- +_validate_name() { + local name="$1" field="$2" + if [[ ! "$name" =~ ^[a-zA-Z0-9._-]+$ ]]; then + echo "ERROR: Invalid $field: '$name' (alphanumeric, dots, hyphens, underscores only)" >&2 + exit 1 + fi +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --vault) VAULT_NAME="$2"; shift 2 ;; + --did) VAULT_DID="$2"; shift 2 ;; + --api) export ARCHON_MODE="api"; shift ;; + -h|--help) + echo "Usage: status.sh [OPTIONS]" + echo " --vault NAME Vault name/alias (default: secrets)" + echo " --did DID Vault DID directly" + echo " --api Use keymaster API instead of CLI" + exit 0 ;; + *) echo "Unknown: $1" >&2; exit 1 ;; + esac +done + +# Validate inputs +_validate_name "$VAULT_NAME" "vault name" + +if [ -n "$VAULT_DID" ] && [[ ! "$VAULT_DID" =~ ^did: ]]; then + echo "ERROR: --did must be a DID (starts with 'did:')" >&2 + exit 1 +fi + +# Resolve vault DID +if [ -z "$VAULT_DID" ]; then + VAULT_DID=$(archon_resolve_vault "$VAULT_NAME" 2>/dev/null) || VAULT_DID="" +fi + +if [ -z "$VAULT_DID" ]; then + echo "❌ No vault '$VAULT_NAME' found" + echo " Create: store.sh --vault $VAULT_NAME --dir /path/to/secrets" + echo " Shared: status.sh --did " + exit 1 +fi + +echo "🔐 Secrets Vault: $VAULT_NAME" +echo " DID: $VAULT_DID" +echo " Mode: ${ARCHON_MODE:-cli}" +echo "" + +echo "📦 Contents:" +archon_list_vault_items "$VAULT_DID" 2>/dev/null | jq -r ' + to_entries[] | " \(.key) — \(.value.bytes // "?") bytes, added \(.value.added // "unknown")" +' 2>/dev/null || echo " (unable to list — may need owner/member access)" + +echo "" +echo "👥 Members:" +archon_list_vault_members "$VAULT_DID" 2>/dev/null | jq -r ' + to_entries[] | " \(.key | .[0:50])... — added \(.value.added // "unknown")" +' 2>/dev/null || echo " (unable to list — may need owner access)" + +echo "" +echo "💾 Local status:" +if [ -d "/dev/shm/${VAULT_NAME}" ]; then + local_count=$(find "/dev/shm/${VAULT_NAME}" -maxdepth 1 -name "*.env" 2>/dev/null | wc -l) + echo " RAM: ✅ $local_count files loaded (/dev/shm/${VAULT_NAME})" +else + echo " RAM: ❌ not loaded (run: restore.sh --to-ram)" +fi diff --git a/archon-keymaster/scripts/secrets/store.sh b/archon-keymaster/scripts/secrets/store.sh new file mode 100755 index 0000000..de9fc1c --- /dev/null +++ b/archon-keymaster/scripts/secrets/store.sh @@ -0,0 +1,127 @@ +#!/bin/bash +# Store secrets in an Archon vault +# Usage: store.sh [--vault NAME] [--dir PATH] [--pattern "*.env"] [--member DID]... +# +# Packs matching files, encrypts via Archon vault. +# Creates the vault if needed, adds members for shared access. +# Supports group-based access: use --group to set group ownership. +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$SCRIPT_DIR/../common.sh" + +VAULT_NAME="${SECRETS_VAULT_NAME:-secrets}" +SECRETS_DIR="${SECRETS_DIR:-$HOME/.config}" +PATTERN="${SECRETS_PATTERN:-*.env}" +LABEL="secrets-bundle" +MEMBERS=() +GROUP="" + +# --- Input validation --- +_validate_name() { + local name="$1" field="$2" + if [[ ! "$name" =~ ^[a-zA-Z0-9._-]+$ ]]; then + echo "ERROR: Invalid $field: '$name' (alphanumeric, dots, hyphens, underscores only)" >&2 + exit 1 + fi +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --vault) VAULT_NAME="$2"; shift 2 ;; + --dir) SECRETS_DIR="$2"; shift 2 ;; + --pattern) PATTERN="$2"; shift 2 ;; + --label) LABEL="$2"; shift 2 ;; + --member) MEMBERS+=("$2"); shift 2 ;; + --group) GROUP="$2"; shift 2 ;; + --api) export ARCHON_MODE="api"; shift ;; + -h|--help) + echo "Usage: store.sh [OPTIONS]" + echo " --vault NAME Vault name/alias (default: secrets)" + echo " --dir PATH Directory containing secrets (default: ~/.config)" + echo " --pattern GLOB File pattern (default: *.env)" + echo " --label NAME Bundle label (default: secrets-bundle)" + echo " --member DID Add DID as vault member (repeatable)" + echo " --group NAME Group for shared vault ownership" + echo " --api Use keymaster API instead of CLI" + exit 0 ;; + *) echo "Unknown: $1" >&2; exit 1 ;; + esac +done + +# Validate inputs to prevent path traversal +_validate_name "$VAULT_NAME" "vault name" +_validate_name "$LABEL" "label" + +# Validate members are DIDs +for member in "${MEMBERS[@]}"; do + if [[ ! "$member" =~ ^did: ]]; then + echo "ERROR: Member must be a DID (starts with 'did:'): $member" >&2 + exit 1 + fi +done + +# Validate secrets directory exists +if [ ! -d "$SECRETS_DIR" ]; then + echo "ERROR: Secrets directory does not exist: $SECRETS_DIR" >&2 + exit 1 +fi + +WORK_DIR=$(mktemp -d) +trap 'rm -rf "$WORK_DIR"' EXIT + +BUNDLE="$WORK_DIR/${LABEL}.tar.gz" + +echo "📦 Packing secrets from $SECRETS_DIR (pattern: $PATTERN)..." + +# Build file list safely (null-delimited) +mapfile -d '' FILES < <(find "$SECRETS_DIR" -maxdepth 1 -name "$PATTERN" -type f -print0 2>/dev/null) +if [ ${#FILES[@]} -eq 0 ]; then + echo "ERROR: No files matching '$PATTERN' in $SECRETS_DIR" >&2 + exit 1 +fi + +# Create tar with files relative to SECRETS_DIR +tar czf "$BUNDLE" -C "$SECRETS_DIR" "${FILES[@]##*/}" +COUNT=${#FILES[@]} +echo " Packed $COUNT files" + +# Resolve or create vault +VAULT_DID=$(archon_resolve_vault "$VAULT_NAME" 2>/dev/null) || VAULT_DID="" + +if [ -z "$VAULT_DID" ]; then + echo "🔨 Creating vault '$VAULT_NAME'..." + if archon_is_api_mode; then + VAULT_DID=$(archon_api_post "/vaults" "{\"alias\": \"$VAULT_NAME\"}" | jq -r '.did // .') + else + VAULT_DID=$(archon_cli create-vault -a "$VAULT_NAME") + fi + echo " Vault: $VAULT_DID" +fi + +# Add members +for member in "${MEMBERS[@]}"; do + echo "👤 Adding member: ${member:0:40}..." + if ! archon_add_vault_member "$VAULT_DID" "$member" >/dev/null 2>&1; then + echo "⚠️ Failed to add member (may already exist or vault size limit)" + fi +done + +# Transfer to group if specified +if [ -n "$GROUP" ]; then + echo "🔗 Setting group ownership: $GROUP" + if ! archon_transfer_asset "$VAULT_DID" "$GROUP" >/dev/null 2>&1; then + echo "⚠️ Group transfer not supported — vault remains under current owner" + fi +fi + +echo "🔐 Uploading encrypted bundle..." +archon_add_vault_item "$VAULT_DID" "$BUNDLE" + +HASH=$(sha256sum "$BUNDLE" | cut -d' ' -f1) +MEMBER_COUNT=$(archon_list_vault_members "$VAULT_DID" 2>/dev/null | jq 'keys | length' 2>/dev/null || echo "?") +echo "" +echo "✅ Secrets stored in vault '$VAULT_NAME'" +echo " Vault DID: $VAULT_DID" +echo " Bundle: ${LABEL}.tar.gz ($COUNT files)" +echo " Members: $MEMBER_COUNT (+ owner)" +echo " SHA256: ${HASH:0:16}..."