|
| 1 | +# How the headless server stores credentials |
| 2 | + |
| 3 | +The desktop app seals saved credentials (Linear, Jira, Bitbucket, API keys) with Electron's |
| 4 | +`safeStorage`, which uses the OS keychain. The headless server (`orcad`, the managed SSH server) |
| 5 | +has no Electron, so it seals them itself, on the machine it runs on |
| 6 | +(`src/main/orcad/orcad-secret-store.ts`). |
| 7 | + |
| 8 | +## Where the key lives |
| 9 | + |
| 10 | +Each credential is encrypted with AES-256-GCM. The master key is created on first start and |
| 11 | +stored in one of two places. The choice is recorded in `~/.orca/orcad-secret-store-<machine>.json`. |
| 12 | +That is the directory the credentials live in, whatever `ORCA_USER_DATA` says, so the key lasts |
| 13 | +exactly as long as the credentials it seals. There is one record per machine, so hosts that share |
| 14 | +a home each keep their own key. |
| 15 | + |
| 16 | +| Host | Where the key goes | |
| 17 | +| --- | --- | |
| 18 | +| macOS | The login Keychain (through `/usr/bin/security`) if the server can reach it on first start | |
| 19 | +| Linux | The Secret Service keyring (`secret-tool`) if the session has a D-Bus session bus and a keyring answers | |
| 20 | +| Otherwise, and on Windows | the record file itself, mode `0600` (an owner-only ACL on Windows), mixed with the machine ID (`/etc/machine-id`, macOS hardware UUID, Windows `MachineGuid`) | |
| 21 | + |
| 22 | +The keyring is never used for a key directory under the system temp directory or `/tmp` (tests), |
| 23 | +or when `ORCA_HEADLESS_SECRET_STORE=file` is set. That setting suits a macOS host reached only |
| 24 | +over SSH, where the login Keychain is often locked. |
| 25 | + |
| 26 | +A server started with `ORCA_USER_DATA` under a temporary directory (tests, smoke launchers) |
| 27 | +keeps the old behavior. It stores credentials unencrypted and never creates a key in the real |
| 28 | +`~/.orca`, or re-seals what is there. |
| 29 | + |
| 30 | +Windows does not use DPAPI. Reaching it from Node takes PowerShell or a native addon, and running |
| 31 | +PowerShell for each operation is an EDR signal. |
| 32 | + |
| 33 | +## Limitations |
| 34 | + |
| 35 | +- **The key file is weaker than a keychain.** The key sits next to the credentials, and the |
| 36 | + machine ID it is mixed with is not secret. Profile-state records include it, for example. So the |
| 37 | + key file mainly stops a single credential file from opening on another machine. Anyone who can |
| 38 | + read this account's files, or a backup of `~/.orca`, can unseal the credentials. Treat the key |
| 39 | + file like the credentials themselves. The server logs this gap once with the `[secrets]` prefix. |
| 40 | + A keychain does not protect against other processes running as the same unlocked user either. |
| 41 | +- **The key belongs to the host.** If the recorded keyring is locked or can't be reached, or the |
| 42 | + record is unreadable, the server does not fall back to plaintext. It leaves the saved credentials |
| 43 | + alone and refuses new saves until the key loads again. If the key is gone for good, deleting the |
| 44 | + record starts over, and every integration has to be connected again. A new machine ID counts as |
| 45 | + a new machine, so it gets a new key, and credentials sealed under the old one have to be |
| 46 | + connected again. |
| 47 | +- **Shared home directories.** Integration credentials live in `~/.orca` under the login home. |
| 48 | + Hosts that share that home (over NFS, for example), or a desktop app on the same machine, each |
| 49 | + seal with their own key, so a credential one of them saves can't be read by the others. To keep |
| 50 | + the old shared plaintext behavior on such hosts, set `ORCA_HEADLESS_SECRET_STORE=plaintext`. |
| 51 | +- **Rolling back to an older server.** A server from before this change can't read credentials a |
| 52 | + newer one sealed. An SSH or WSL rollback restores profile state, not `~/.orca`, so after a |
| 53 | + rollback the old server shows "Could not decrypt saved Linear credential… reconnect Linear". |
| 54 | + Connect each integration again on that server. Plaintext copies are deliberately not kept for |
| 55 | + rollback. |
| 56 | +- **GitHub and GitLab** tokens are not stored by Orca at all. `gh` and `glab` keep them, under |
| 57 | + their own storage settings. |
| 58 | + |
| 59 | +## Migration |
| 60 | + |
| 61 | +On startup, once the key loads, the server seals every credential that an older version stored in |
| 62 | +plaintext. Each file is rewritten only after the new value has been decrypted and checked, and |
| 63 | +only if no other process changed the file in the meantime. A file that can't be sealed is left as |
| 64 | +it was. Readers still accept the legacy plaintext form, so no credential is lost. |
0 commit comments