Upgrading from 1.23.0 to 1.24.0 on Linux makes Mailspring unusable

Upgrading from 1.23.0 to 1.24.0 on Linux makes Mailspring unusable. On every
launch it shows a fatal dialog and quits:

Mailspring could not read your saved passwords and cannot continue. On Linux,
Mailspring requires a secret service such as org.freedesktop.portal.Secret or
org.freedesktop.Secret.Service. Please ensure a provider is installed and
running, then restart Mailspring.

The message is misleading: a secret service is installed, running and
unlocked. The real cause is that 1.24 changed how it reads credentials and has
no migration path for credentials written by 1.23.

1.23 encrypted the credentials blob with the synchronous safeStorage API
(libsecret backend, v11 prefix). 1.24’s KeyManager reads it with the
asynchronous API (decryptStringAsync), which derives a different key and
therefore cannot decrypt the existing blob. _getKeyHash treats that failure as
fatal and calls app.quit().

This affects all accounts, because the single blob holds every account’s
secrets. It should affect every Linux user who upgrades to 1.24 with saved
credentials.

To Reproduce…

Steps to reproduce the behavior:

  1. Run Mailspring 1.23.0 on Linux with one or more accounts configured, so that
    credentials in ~/.config/Mailspring/config.json is populated. The value
    begins with bytes 118, 49, 49 - that is v11, Chromium’s libsecret-derived
    key prefix.
  2. Upgrade to 1.24.0, leaving the config directory in place.
  3. Launch Mailspring.
  4. See the error dialog above, roughly nine seconds after the window appears.
    The app then quits.

Expected Behavior

1.24 should read credentials saved by 1.23, or migrate them transparently, and
start normally with all accounts intact.

If credentials genuinely cannot be read, the error should not be fatal, and it
should not claim a secret service is missing when one is present. Quitting
outright makes it look like account data has been lost, and leaves no route to
re-enter passwords from inside the app.

Screenshots

Not attached - the dialog text is quoted verbatim above.

Setup

OS and Version: Omarchy 4.0.0.alpha (Arch Linux), kernel 7.2.3-arch1-3, Hyprland
v0.56.2, native Wayland session. gnome-keyring 1:50.0-1, xdg-desktop-portal
1.22.1-2 with xdg-desktop-portal-hyprland 1.4.1-2 and xdg-desktop-portal-gtk
1.15.3-1.

Installation Method: locally built pacman package from the official upstream
mailspring-1.24.0-0.1.x86_64.rpm, keeping the bundled Electron. Upgraded from
the AUR mailspring-bin 1.23.0-2, which uses a system Electron.

Mailspring Version: 1.24.0 (bundled Electron 44.3.0 / Chromium 152.0.7977.78),
upgraded from 1.23.0 (system Electron 41.10.7 / Chromium 146.0.7680.216).

Additional Context

The keyring is healthy, which is why the error message is misleading:

  • org.freedesktop.secrets is running, served by gnome-keyring-daemon
  • The default collection alias resolves to the login collection
  • That collection is unlocked (Locked is false)
  • The item Mailspring Safe Storage exists in it, with attributes
    application=Mailspring and xdg:schema=chrome_libsecret_os_crypt_password_v2

A working migration, which 1.24 could do itself. Electron 44 still exposes
the synchronous API alongside the asynchronous one, and the sync path reads the
old blob without trouble. I verified safeStorage.isEncryptionAvailable() is
true, isAsyncEncryptionAvailable() is true, and decryptString() on the
existing blob returns the correct JSON with all 8 of my secrets.

So this is sufficient to migrate in place:

const plain = safeStorage.decryptString(Buffer.from(old, 'utf-8'));  // old v11 key
const enc   = await safeStorage.encryptStringAsync(plain);           // new async key
AppEnv.config.set('credentials', enc);

I ran exactly that against my own install. All 8 secrets across 4 accounts were
recovered and re-encrypted, verified by reading them back through
decryptStringAsync with shouldReEncrypt: false. No re-authentication was
needed, including for an OAuth account. Mailspring 1.24 has started cleanly ever
since.

A natural fix would be in KeyManager._getKeyHash: when the async decrypt
throws, fall back to decryptString, and if that succeeds, re-encrypt via
_writeKeyHash. The shouldReEncrypt flag already in the code path looks
designed for precisely this.

A second, separable problem this exposed. Before the key mismatch is even
reached, isAsyncEncryptionAvailable() returns false on any desktop that is
not GNOME or KDE, and the failure takes about nine seconds - a D-Bus timeout.
org.freedesktop.portal.Secret has no backend available, because the only
implementations are:

  • /usr/share/xdg-desktop-portal/portals/gnome-keyring.portal, gated UseIn=gnome
  • /usr/share/xdg-desktop-portal/portals/kwallet.portal, gated UseIn=kde

Hyprland’s portal configuration prefers hyprland;gtk, and neither implements
the Secret interface, so the portal interface is simply not exposed. Working
around it by routing the interface explicitly:

# ~/.config/xdg-desktop-portal/portals.conf
[preferred]
default=hyprland;gtk
org.freedesktop.impl.portal.Secret=gnome-keyring

makes isAsyncEncryptionAvailable() return true. That alone does not fix
startup - the credential key mismatch above is the actual blocker - but it does
mean the async path is unavailable out of the box on Sway, Hyprland, river,
Wayfire and similar, which is worth handling regardless.

Just a heads up, they just pushed out a release (1.24.1) to resolve this.