diff --git a/README.md b/README.md index 2efb038..b96abb8 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,9 @@ npm install -g @authmesh/cli # CLI for device management ```bash amesh init --name "prod-api" # Identity created. -# Device ID : am_cOixWcOdI8-pLh4P -# Backend : secure-enclave +# Device ID : am_cOixWcOdI8-pLh4P +# Backend : Secure Enclave +# Friendly Name : prod-api ``` ### 2. Pair two machines diff --git a/docs/architecture-decisions.md b/docs/architecture-decisions.md index 0b5b9ec..9234f0f 100644 --- a/docs/architecture-decisions.md +++ b/docs/architecture-decisions.md @@ -46,7 +46,7 @@ Key decisions made during spec review and project bootstrap (March 2026). Each e | 2 | macOS Keychain | Swift helper → software keychain (unsigned binary fallback) | | 3 | Linux TPM 2.0 | `tpm2-tools` subprocess via `execFile` (not `exec`) | -Note: The encrypted-file fallback (Tier 3) is available as an explicit opt-in (`--backend encrypted-file --passphrase`) for cloud VMs and containers without hardware key storage. Hardware backends are always preferred when available. +Note: The encrypted-file fallback (Tier 3) is always available as an automatic fallback for cloud VMs and containers without hardware key storage. The passphrase is auto-generated (256-bit random) and stored in `identity.json`. Hardware backends are always preferred when available. --- @@ -192,3 +192,23 @@ The controller CLI displays this code; the target CLI prompts the operator to en - Auto-granting shell on pairing — violates principle of least privilege - Reusing pairing handshake's random-nonce encryption — birthday-bound risk over long sessions - Session resumption — complexity and nonce-reuse risk outweigh the latency benefit + +--- + +## ADR-010: Auto-generated passphrase stored in identity.json + +**Decision:** The encrypted-file backend auto-generates a 256-bit random passphrase and stores it in `identity.json` alongside the device identity. The `--passphrase` CLI flag has been removed. + +**Why:** The previous model required users to provide and manage a passphrase (via `--passphrase` flag or `AUTH_MESH_PASSPHRASE` env var). This was the #1 onboarding friction point: users forgot passphrases, used weak ones, or had to manage env vars across machines. In practice, the passphrase was often stored in a `.env` file or systemd unit alongside the identity — offering no real second-factor benefit. + +**Security model change:** The encrypted-file backend's security now depends on Unix file permissions (`identity.json` is mode `0o600` in a `0o700` directory) rather than encryption + separate passphrase. The Argon2id + AES-256-GCM encryption layer is retained as defense-in-depth (protects against partial file reads, memory forensics of swap/core dumps, and accidental backups of the key file without the identity file). + +**Threat analysis:** +- **Same-user access:** Unchanged — the user who owns `~/.amesh/` can always access their own keys +- **Root compromise:** Unchanged — root can read everything regardless +- **Backup leak of `~/.amesh/`:** Slightly weaker — backup now contains both passphrase and encrypted key. Previously, the passphrase might have been stored separately. Mitigation: users should exclude `~/.amesh/` from backups, same as SSH keys. +- **Key file leak without identity file:** Still protected — the encryption is meaningful if only `keys/*.key.json` leaks without `identity.json` + +**Backwards compatibility:** Existing identities created before this change (without a `passphrase` field in `identity.json`) still work via the `AUTH_MESH_PASSPHRASE` env var fallback. + +**Memory hygiene:** The passphrase is stripped from the in-memory `Identity` object immediately after the `KeyStore` is created (`delete identity.passphrase`). JavaScript strings are immutable so a copy may remain in the V8/JSC heap, but this reduces the reference window. diff --git a/docs/guide.md b/docs/guide.md index bd18835..d989c95 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -26,24 +26,34 @@ amesh init --name "My Laptop" Output (macOS): ``` Generating P-256 keypair... + +Detecting key storage backend: + Secure Enclave not available (binary not signed) + macOS Keychain selected + Identity created. - Device ID : am_cOixWcOdI8-pLh4P - Public Key: A+B9pwI1/CGINmyozdPj... - Backend : keychain + Device ID : am_cOixWcOdI8-pLh4P + Public Key : A+B9pwI1/CGINmyozdPj... + Backend : macOS Keychain + Friendly Name : My Laptop + +Next steps: + Target: run `amesh listen`, then `amesh invite` from your controller + Controller: run `amesh listen` on a target first, then `amesh invite` here ``` -amesh uses hardware-backed key storage when available (Secure Enclave, macOS Keychain, or TPM 2.0). On machines without hardware key storage (cloud VMs, containers), use the encrypted-file backend: +amesh uses hardware-backed key storage when available (Secure Enclave, macOS Keychain, or TPM 2.0). On machines without hardware key storage (cloud VMs, containers), the encrypted-file backend is selected automatically. You can also force it: ```bash -amesh init --name "prod-api" --backend encrypted-file --passphrase "$AUTH_MESH_PASSPHRASE" +amesh init --name "prod-api" --backend encrypted-file ``` This creates two files: - `~/.amesh/identity.json` — your device ID, public key, friendly name - `~/.amesh/allow_list.json` — HMAC-sealed trust store (starts empty) -The private key is protected by the OS keychain (macOS) or TPM (Linux) and never written to disk as plaintext. +The private key is protected by the OS keychain (macOS), TPM (Linux), or encrypted with Argon2id (file backend). Hardware-backed keys never leave the secure element. To use a custom directory (useful for testing): ```bash @@ -60,21 +70,31 @@ amesh list Output (empty initially): ``` - No trusted devices yet. - Run `amesh listen` to start pairing. + This device + ─────────────────────────────────────────────────────── + Device ID : am_cOixWcOdI8-pLh4P + Friendly Name : My Laptop + Backend : macOS Keychain + Created : 2026-03-30 - Your identity: am_cOixWcOdI8-pLh4P (My Laptop) + No trusted devices yet. + Pair with another device using `amesh listen` + `amesh invite`. ``` After devices are paired, it shows each device's role (`[controller]` or `[target]`): ``` + This device + ─────────────────────────────────────────────────────── + Device ID : am_cOixWcOdI8-pLh4P + Friendly Name : My Laptop + Backend : macOS Keychain + Created : 2026-03-30 + Trusted Devices (2) - ────────────────────────────────────────────────────────── + ─────────────────────────────────────────────────────── am_1a2b3c4d5e6f7a8b MacBook Pro — dev [controller] added 2026-03-28 am_9f8e7d6c5b4a3210 staging-api [target] added 2026-03-29 - ────────────────────────────────────────────────────────── - - Your identity: am_cOixWcOdI8-pLh4P (My Laptop) + ─────────────────────────────────────────────────────── ``` - **[controller]** — this device can authenticate TO you @@ -94,11 +114,11 @@ Prompts for confirmation, then removes the device from the allow list and reseal ## 5. Use the Crypto Primitives Directly -Open a Node.js REPL from the core package: +Open a REPL from the core package: ```bash cd packages/core -node --input-type=module +bun repl ``` ### Sign and verify a message @@ -224,7 +244,7 @@ The server automatically: 5. Verifies the ECDSA-P256-SHA256 signature 6. Attaches `req.authMesh` with the verified device identity -**No API key. No Bearer token. No secret to leak.** +**No API key. No Bearer token. No shared secret.** --- diff --git a/docs/integration-guide.md b/docs/integration-guide.md index 0600a2e..1aa2043 100644 --- a/docs/integration-guide.md +++ b/docs/integration-guide.md @@ -292,12 +292,12 @@ Use the same `amesh.fetch()` and `amesh.verify()` code paths in local developmen ### Setup -Use the `encrypted-file` backend with a simple passphrase for local services: +Use the `encrypted-file` backend for local services (passphrase auto-generated): ```bash # Create identities for local services -AUTH_MESH_DIR=/tmp/amesh-a amesh init --name "local-service-a" --backend encrypted-file --passphrase "dev" -AUTH_MESH_DIR=/tmp/amesh-b amesh init --name "local-service-b" --backend encrypted-file --passphrase "dev" +AUTH_MESH_DIR=/tmp/amesh-a amesh init --name "local-service-a" --backend encrypted-file +AUTH_MESH_DIR=/tmp/amesh-b amesh init --name "local-service-b" --backend encrypted-file # Start the relay (needed only for pairing) bunx @authmesh/relay @@ -323,13 +323,12 @@ const res = await amesh.fetch('http://localhost:4000/api/data', { The only difference is the key storage backend: - **Production:** macOS Keychain, Secure Enclave, or TPM 2.0 -- **Local dev:** `--backend encrypted-file --passphrase "dev"` +- **Local dev:** `--backend encrypted-file` (passphrase auto-generated) ### Tips -- Use a shared passphrase like `"dev"` for all local identities. Security is not the goal — dev/prod parity is. - Use `AUTH_MESH_DIR` to isolate each service's identity directory. -- For Docker Compose, set `AUTH_MESH_PASSPHRASE=dev` and mount `AUTH_MESH_DIR` as a volume so identities persist across restarts. +- For Docker Compose, mount `AUTH_MESH_DIR` as a volume so identities (and their auto-generated passphrases) persist across restarts. - The relay is only needed during initial pairing. Once devices are paired, stop it. --- @@ -339,7 +338,7 @@ The only difference is the key storage backend: | Variable | Description | Default | |----------|-------------|---------| | `AUTH_MESH_DIR` | Directory for identity and keys | `~/.amesh/` | -| `AUTH_MESH_PASSPHRASE` | Passphrase for encrypted-file backend | (optional) | +| `AUTH_MESH_PASSPHRASE` | Override auto-generated passphrase for encrypted-file backend (rarely needed) | (optional) | | `AMESH_BOOTSTRAP_TOKEN` | Bootstrap token for automated pairing | (optional) | | `AMESH_RELAY_URL` | WebSocket relay URL | `wss://relay.authmesh.dev/ws` | | `REDIS_URL` | Redis URL for nonce store | (optional) | @@ -382,13 +381,12 @@ The allow list file (`~/.amesh/allow_list.json`) was modified outside of amesh. You're running in production without a Redis nonce store. Replay attacks could succeed by hitting different instances. See Recipe 3 above. -### "No supported key storage backend detected" +### "Keys are software-protected (no hardware keystore detected)" -amesh prefers hardware-backed storage (Secure Enclave, macOS Keychain, TPM 2.0) but also supports an encrypted-file backend for cloud VMs: +amesh prefers hardware-backed storage (Secure Enclave, macOS Keychain, TPM 2.0). When none is available, it falls back to the encrypted-file backend automatically with an auto-generated passphrase. -```bash -amesh init --name "my-server" --backend encrypted-file --passphrase "your-passphrase" -# Or set AUTH_MESH_PASSPHRASE environment variable -``` +To upgrade to hardware-backed storage: +- **macOS:** Ensure the Swift helper binary is installed alongside the `amesh` binary for Keychain/Secure Enclave support. +- **Linux:** Install `tpm2-tools` for TPM 2.0 support. -On macOS, ensure the Swift helper binary (`amesh-se-helper`) is installed alongside the `amesh` binary for Keychain/Secure Enclave support. +Then re-run `amesh init --force` to regenerate with the hardware backend. diff --git a/docs/protocol-spec.md b/docs/protocol-spec.md index e70d04c..449eff4 100644 --- a/docs/protocol-spec.md +++ b/docs/protocol-spec.md @@ -70,7 +70,7 @@ Every choice below is made for a reason. Do not substitute without understanding | **Crypto — Ciphers** | `@noble/ciphers` (ChaCha20-Poly1305) | Handshake tunnel encryption. Same ecosystem. | | **Hardware — macOS** | Swift helper subprocess → Apple Security.framework | Direct Secure Enclave access via `SecKeyCreateRandomKey` with `kSecAttrTokenIDSecureEnclave`. Generates P-256 keys in hardware. `node-keytar` is deprecated (archived Dec 2022) and cannot access Secure Enclave — it is only a password store. | | **Hardware — Linux** | `tpm2-tools` (subprocess via `execFile`) | Industry standard TPM 2.0 interface. P-256 universally supported. | -| **Hardware — Fallback** | Encrypted file (AES-256-GCM + Argon2id) | Explicit opt-in via `--backend file --passphrase`. For cloud VMs without hardware key storage. | +| **Hardware — Fallback** | Encrypted file (AES-256-GCM + Argon2id) | Automatic fallback. Passphrase auto-generated and stored in identity.json. For cloud VMs without hardware key storage. | | **Relay Server** | Bun.serve() native | Zero deps — no Fastify, no ws. | | **Allow List Storage** | JSON file + HMAC integrity seal | See Section 9 — the plaintext JSON without integrity protection is a critical vulnerability | | **Package Manager** | Bun workspaces | Monorepo-friendly, fast installs, native test runner | @@ -179,19 +179,23 @@ The prefix `am_` makes amesh IDs visually identifiable in logs. ### CLI output ``` -$ amesh init +$ amesh init --name "prod-api-us-east-1" -? What is this device's friendly name? prod-api-us-east-1 +Generating P-256 keypair... -✔ Generating P-256 keypair... -✔ Storing private key in Secure Enclave (macOS) -✔ Identity created. +Detecting key storage backend: + Secure Enclave selected - Device ID : am_8f3a9b2c1d4e5f6a - Public Key: 8f3a9b2c... - Backend : secure-enclave +Identity created. -Run `amesh listen` on this machine, then `amesh invite` from your laptop. + Device ID : am_8f3a9b2c1d4e5f6a + Public Key : A+B9pwI1/CGINmyozdPj... + Backend : Secure Enclave + Friendly Name : prod-api-us-east-1 + +Next steps: + Target: run `amesh listen`, then `amesh invite` from your controller + Controller: run `amesh listen` on a target first, then `amesh invite` here ``` --- @@ -648,11 +652,11 @@ Every device goes through this decision tree at `amesh init`. The selected backe │ NO ▼ ┌──────────────────────────────────────────────────────┐ -│ Tier 3 — Encrypted file (explicit opt-in only) │ -│ Requires: --backend file --passphrase │ +│ Tier 3 — Encrypted file (automatic fallback) │ +│ Passphrase auto-generated, stored in identity.json │ │ → AES-256-GCM + Argon2id, filesystem permissions │ │ → Private key encrypted at rest, decrypted per-sign │ -│ → WARNING printed: "file-based, not hardware" │ +│ → WARNING printed: "software-protected" │ └─────────────────────────────��────────────────────────┘ ``` @@ -714,13 +718,18 @@ Document this limitation clearly in the CLI output and README. ``` $ amesh list + This device + ─────────────────────────────────────────────────────── + Device ID : am_8f3a9b2c1d4e5f6a + Friendly Name : prod-api-us-east-1 + Backend : Secure Enclave + Created : 2026-03-28 + Trusted Devices (2) - ─────────────────────────────────────────────── - am_1a2b3c4d5e6f7a8b MacBook Pro — dev added 2026-03-28 - am_9f8e7d6c5b4a3210 prod-api-us-east added 2026-03-29 - ─────────────────────────────────────────────── - - Your identity: am_8f3a9b2c1d4e5f6a (prod-api-us-east-1) + ─────────────────────────────────────────────────────── + am_1a2b3c4d5e6f7a8b MacBook Pro — dev [controller] added 2026-03-28 + am_9f8e7d6c5b4a3210 prod-api-us-east [target] added 2026-03-29 + ─────────────────────────────────────────────────────── ``` --- diff --git a/docs/use-cases-analysis.md b/docs/use-cases-analysis.md index 54f2417..77a7468 100644 --- a/docs/use-cases-analysis.md +++ b/docs/use-cases-analysis.md @@ -212,8 +212,8 @@ This is the best point in the feedback. Dev/prod parity is a real pain point we ```bash # Dev machine -amesh init --name "local-service-a" --backend encrypted-file --passphrase "dev" -amesh init --name "local-service-b" --backend encrypted-file --passphrase "dev" +amesh init --name "local-service-a" --backend encrypted-file +amesh init --name "local-service-b" --backend encrypted-file # Pair them, then use the exact same amesh.fetch() / amesh.verify() code as production ``` diff --git a/landpage/src/lib/components/DocsSidebar.svelte b/landpage/src/lib/components/DocsSidebar.svelte new file mode 100644 index 0000000..c6d73b4 --- /dev/null +++ b/landpage/src/lib/components/DocsSidebar.svelte @@ -0,0 +1,133 @@ + + + + + + +
+ + {#if mobileOpen} + + {/if} +
diff --git a/landpage/src/lib/components/Nav.svelte b/landpage/src/lib/components/Nav.svelte index 80a32a9..e3af607 100644 --- a/landpage/src/lib/components/Nav.svelte +++ b/landpage/src/lib/components/Nav.svelte @@ -14,12 +14,15 @@