feat(cli): move private key out of identity.json into keychain / NIP-49
identity.json previously stored `privKeyHex` and `nsec` as plaintext fields. 0600 file perms keep other OS users out, but not another app running as the same user — and that is the threat model the CLI actually cares about. Introduce a SecretStore indirection: * identity.json now persists only the public parts plus a typed `secret: IdentitySecret` envelope (keychain | ncryptsec | plaintext). * macOS uses `/usr/bin/security` (Keychain ACLs bind the item to the binary that stored it, so other same-user apps need user consent). * Linux uses `secret-tool` if a Secret Service is running on the session D-Bus; the gain there is at-rest encryption while the keyring is locked. * On any platform without a keychain, auto falls back to NIP-49 (scrypt + XChaCha20) with the passphrase read from --passphrase-file, then $AMY_PASSPHRASE, then a TTY prompt. Another same-user app can read the blob but cannot decrypt it without the passphrase. * `--secret-backend=plaintext` is an explicit opt-in for dev scripts and the interop test harness. Legacy identity.json files that still carry top-level privKeyHex/nsec are read transparently and auto-migrate on the next save. whoami, `create` / `login` existence checks, and init-re-run now use a metadata-only load path so they do not trigger a keychain prompt or ask for a passphrase just to echo the npub. Tests: cli/tests/ setups wire --secret-backend=plaintext through the amy_a / amy_d wrappers so headless CI runs do not stall on a TTY passphrase prompt. https://claude.ai/code/session_01SqdMfLdXvb3GskFLcEj739
This commit is contained in:
@@ -71,15 +71,19 @@ preflight_dm() {
|
||||
# --- amy identity wrappers ---------------------------------------------------
|
||||
# Two identities: A (sender) and D (recipient). We reuse A_DIR for parity
|
||||
# with the existing harness files; D_DIR is new.
|
||||
amy_a() { "$AMY_BIN" --data-dir "$A_DIR" "$@"; }
|
||||
amy_d() { "$AMY_BIN" --data-dir "$D_DIR" "$@"; }
|
||||
#
|
||||
# `--secret-backend=plaintext` keeps these throwaway interop runs headless —
|
||||
# the default `auto` would try the OS keychain (not available in CI) and then
|
||||
# ask for a NIP-49 passphrase. Plaintext still writes 0600-owner-only.
|
||||
amy_a() { "$AMY_BIN" --data-dir "$A_DIR" --secret-backend plaintext "$@"; }
|
||||
amy_d() { "$AMY_BIN" --data-dir "$D_DIR" --secret-backend plaintext "$@"; }
|
||||
|
||||
# --- identity bootstrap ------------------------------------------------------
|
||||
ensure_identity_for() {
|
||||
local who="$1" dir="$2"
|
||||
step "initialising Identity $who (amy at $dir)"
|
||||
local out
|
||||
out=$("$AMY_BIN" --data-dir "$dir" init) || {
|
||||
out=$("$AMY_BIN" --data-dir "$dir" --secret-backend plaintext init) || {
|
||||
fail_msg "amy init failed for $who: $out"; exit 1
|
||||
}
|
||||
local npub hex
|
||||
|
||||
Reference in New Issue
Block a user