Skip to content

v0.25.0

Choose a tag to compare

@csitte csitte released this 28 Oct 00:13
· 15 commits to main since this release

Changed

  • 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.