← Back to PRs

#21554: feat(security): encrypt workspace files and config at rest

by joncode open 2026-02-20 03:00 View on GitHub →
docs gateway cli commands agents size: XL
## Summary Encrypt sensitive workspace files at rest using AES-256-GCM with keys stored in the macOS Keychain. Zero new dependencies — uses only Node.js built-in `node:crypto` and macOS `security` CLI. Related: [Discussion #8177 — Encrypt workspace files and config at rest](https://github.com/openclaw/openclaw/discussions/8177) ## Two-Key Architecture A single master password derives two independent 256-bit keys via scrypt with domain separation: | Key | Protects | Purpose | |-----|----------|---------| | **Workspace Key** | `MEMORY.md`, `USER.md`, `IDENTITY.md`, `TOOLS.md`, `HEARTBEAT.md`, `memory/*.md` | Sensitive agent context and personal info | | **Config Key** | `config.yaml` | API keys, tokens, service credentials | Separate keys limit blast radius — compromising one does not expose the other. ## How It Works ### Encryption - **Algorithm:** AES-256-GCM (authenticated encryption) - **Key derivation:** scrypt (N=131072, r=8, p=1) — memory-hard, built into Node.js - **Key storage:** macOS Keychain (`ai.openclaw.encryption` service) - **File format:** `[6B magic "OCENC\x01"][12B nonce][ciphertext][16B auth tag]` - **Detection:** Magic header allows instant encrypted-vs-plaintext detection without attempting decryption ### Read Path Files are transparently decrypted when read. Both encrypted and plaintext files can coexist (migration-friendly): 1. Check for `OCENC` magic header 2. If encrypted → decrypt with appropriate key from memory 3. If plaintext → read as-is (backward compatible) ### Write Path Agent tools write files in plaintext (no changes to tool libraries needed). On each gateway startup, `bootstrapEncryption()` re-encrypts any files that were written in plaintext since the last startup, maintaining at-rest protection. ## CLI Commands ```bash openclaw security init # Set up encryption (password → keys → encrypt) openclaw security status [--json] # Show encryption status openclaw security change-password # Re-derive keys, re-encrypt everything openclaw security disable # Decrypt everything, remove keys ``` ## Integration Points | File | Change | |------|--------| | `agents/workspace.ts` | Bootstrap files (SOUL.md, USER.md, etc.) read through `readFileAutoDecrypt` | | `memory/manager.ts` | `memory_get` tool reads through encrypted middleware | | `memory/internal.ts` | Memory indexing/search reads through encrypted middleware | | `gateway/server.impl.ts` | `bootstrapEncryption()` on startup, `shutdownEncryption()` on close | | `cli/program/routes.ts` | `openclaw security` CLI route | ## New Files ``` src/security/encryption/ ├── crypto.ts # AES-256-GCM encrypt/decrypt with OCENC magic header ├── key-derivation.ts # scrypt two-key derivation (zero new deps) ├── keychain.ts # macOS Keychain integration via security CLI ├── workspace-fs.ts # Encrypted file read/write with migration support ├── metadata.ts # .encryption-meta.json state tracking ├── setup.ts # High-level orchestrator (init/change-password/disable) ├── fs-middleware.ts # Transparent auto-decrypt middleware (async + sync) ├── integration.ts # Gateway bootstrap hook with re-encryption ├── index.ts # Barrel exports src/commands/security.ts # CLI commands docs/workspace-encryption.md # Full documentation ``` ## Test Coverage **96 tests across 11 test files**, all passing: | Test File | Tests | Coverage | |-----------|-------|----------| | `crypto.test.ts` | 20 | Round-trip, tamper detection, key validation, edge cases | | `key-derivation.test.ts` | 8 | Determinism, domain separation, salt uniqueness | | `keychain.test.ts` | 6 | Mocked execFileSync, store/get/delete lifecycle | | `workspace-fs.test.ts` | 15 | Encrypted read/write, migration, round-trip | | `metadata.test.ts` | 6 | Create, read/write, encryption state | | `setup.test.ts` | 2 | Init flow with mocked keychain | | `fs-middleware.test.ts` | 13 | Auto-decrypt async+sync, key management | | `e2e.test.ts` | 6 | Full lifecycle: init → read → write → re-encrypt → change-password → disable | | `routes.test.ts` | 1 | Security route matching | | Existing tests | 19 | `internal.test.ts` + `workspace.test.ts` still passing | ## Security Considerations **Protects against:** disk theft, backup exposure, unauthorized file access by other users. **Does not protect against:** root/admin access (process memory), keyloggers, active compromise with code execution. **Intentionally unencrypted:** `AGENTS.md`, `SOUL.md`, `BOOTSTRAP.md` (instructions, not secrets), `.encryption-meta.json` (contains only the non-secret salt). ## Platform Support | Platform | Status | |----------|--------| | macOS | ✅ Supported (Keychain) | | Linux | 🔜 Phase 3 (libsecret/secret-service) | | Windows | 🔜 Phase 3 (Credential Manager) |

Most Similar PRs