You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The token is encrypted at rest by default, with the key in the OS keystore. Until now token.json was encrypted only when MAILWARDEN_TOKEN_PASSPHRASE was set — and a passphrase in
practice sits in the MCP client's configuration next to the token, so a copied folder carried
both. --auth now generates a random 256-bit key, files it in the OS keystore (Windows Credential
Manager, macOS Keychain, the Secret Service on Linux) through the native @napi-rs/keyring binding
— no shell-out to security, secret-tool or PowerShell — and seals the token with the same
AES-256-GCM as before (envelope v: 2). A copy of the file is useless on another machine or under
another user. Precedence: a passphrase, when set, still wins; then the keystore; where none is
reachable (a Linux host without a desktop session, a platform without a prebuilt binding) the token
is stored in plaintext as before and --auth says why. MAILWARDEN_KEYSTORE=off keeps new tokens
out of the keystore. An existing token is never rewritten: a plaintext one keeps loading, and --check suggests the --auth that encrypts it. Details that matter in use:
One key per token file, filed as mailwarden / token-key-<random id>, never under the account
name — two config directories can both hold a token.work.json, and a shared entry would let one --auth replace the key the other still needs. A re-auth removes the key it replaces.
The granted scopes stay readable in the envelope, bound into the GCM tag, so registration still
hides the filter tools from a token known to lack their scope — a passphrase-encrypted token never
could. An edited scope fails at the next decryption.
On Linux the binding is pinned to the Secret Service. Its own fallback, the kernel keyring, lives
in memory and does not survive a reboot: a key there would leave the token undecryptable after
the next restart.
The key id from the file is validated before it names a keystore entry, so an edited file cannot
point mailwarden at another application's credential.
Every failure names its cause and fix — no keystore here, a key that is not on this machine (the
file was copied, or the entry removed), a keystore that is locked, a file that was edited — in the
server's error and in --check. For a plaintext token, --check also says how the next --auth
would store it. A token whose key is gone cannot be read to confirm its account, so replacing it
takes --auth --force, and the messages say so.
An invalid MAILWARDEN_KEYSTORE value is refused by --auth before the browser opens, and makes --check fail with a Keystore line.
MAILWARDEN_KEYSTORE=off never stores a key. Re-authorizing over a token that has one still
removes that key, so no entry is left orphaned.
The MCPB bundle carries the keystore binding for every platform it declares — macOS, Windows and
Linux (glibc and musl), each on x64 and arm64. npm ci installs the build machine's binary only,
so npm run mcpb adds the others from the lockfile's pinned URLs, checks each against its
integrity hash, and fails when a declared platform has none. Without that, a bundle built on
Windows could not have read a keystore-encrypted token on a Mac — the token npx -y mailwarden --auth writes there. The bundle grows from about 5 to about 12 MiB.
Where it has been tried: end to end against the Windows Credential Manager. On Linux without a
Secret Service the real binding reports a D-Bus error, and the token falls back to plaintext
with that reason; npm run smoke checks this in CI on every run. The macOS Keychain has not been
tried yet, in particular whether it asks for permission in a dialog a background server cannot
show.