Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 21 additions & 1 deletion docs/architecture-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down Expand Up @@ -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.
52 changes: 36 additions & 16 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.**

---

Expand Down
26 changes: 12 additions & 14 deletions docs/integration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

---
Expand All @@ -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) |
Expand Down Expand Up @@ -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.
47 changes: 28 additions & 19 deletions docs/protocol-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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
```

---
Expand Down Expand Up @@ -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 <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"
└─────────────────────────────��────────────────────────┘
```

Expand Down Expand Up @@ -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
───────────────────────────────────────────────────────
```

---
Expand Down
4 changes: 2 additions & 2 deletions docs/use-cases-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
Loading
Loading