1.2.4 Breaks Secret Service on KDE

Description

When updating our (Solus) package for Mailspring 1.2.4, after installing and testing we get the old error saying “Mailspring requires a secret service…” on KDE PLasma 6.7.5 This was not an issue on the previous versions. It is not seeing or accepting kwallet when updating existing installs.

Screenshots

Setup

  • Solus 4.9 KDE 6.7.4:
    • Installation Method: Distro Package
  • Mailspring Version: 1.2.4

Additional Context

This comes down to something that was changed in 1.2.4 from 1.2.3. It no longer sees kwallet and is looking on

This comes down to something that was changed in 1.2.4 from 1.2.3. It no longer sees kwallet and is looking only at Freedesktop and Flatpak for some reason. Previously it would just install and go no matter which desktop.

I will try to dig more into it, but it is an issue that did not exist previously

@bengotow probably related to the key store changes mentioned on the changelog.

Hey folks thanks for reporting this - I think you’re right that it’s related to key-manager: use async safeStorage API by LinusDierheimer · Pull Request #2823 · Foundry376/Mailspring · GitHub. @BlueManCZ might be able to weigh in, I’ll see if I can set up a repro for testing this morning.

Could you running these commands locally and reporting back @clintre?

echo "$XDG_CURRENT_DESKTOP / $KDE_SESSION_VERSION"
busctl --user list | grep -i kwallet
mailspring --enable-logging=stderr --v=1 --vmodule="*os_crypt*=2" 2>&1 | grep -i "crypt\|kwallet"
mailspring --password-store=kwallet6     # if this fixes it, it's detection

This should help narrow down what’s going on here.

Yes, as I was looking into it and was coming back here to post it does seem to come back to the change in the key-manager. Here is the output.

echo “$XDG_CURRENT_DESKTOP / $KDE_SESSION_VERSION”
busctl --user list | grep -i kwallet
mailspring --enable-logging=stderr --v=1 --vmodule=“os_crypt=2” 2>&1 | grep -i “crypt|kwallet”
mailspring --password-store=kwallet6
KDE / 6
:1.53                                             2696 kwalletd6       ce   :1.53         user@1000.service -       -
:1.54                                             2696 kwalletd6       ce   :1.54         user@1000.service -       -
org.freedesktop.impl.portal.desktop.kwallet       1924 ksecretd        ce   :1.16         session-2.scope   2       -
org.kde.kwalletd                                  2696 kwalletd6       ce   :1.53         user@1000.service -       -
org.kde.kwalletd5                                 2696 kwalletd6       ce   :1.53         user@1000.service -       -
org.kde.kwalletd6                                 2696 kwalletd6       ce   :1.53         user@1000.service -       -
org.kde.kwalletmanager                               - -               -    (activatable) -                 -       -
Running database migrations
App load time: 157ms
/usr/share/mailspring/resources/app.asar.unpacked/mailsync.bin: /usr/lib/libcurl.so.4: no version information available (required by /usr/share/mailspring/resources/app.asar.unpacked/mailsync.bin)

{“error”:null}
(node:29771) [DEP0169] DeprecationWarning: url.parse() behavior is not standardized and prone to errors that have security implications. Use the WHATWG URL API instead. CVEs are not issued for url.parse() vulnerabilities.
(Use mailspring --trace-deprecation ... to show where the warning was created)
Manual update check (updates.getmailspring.com/check/linux/x64/1.24.0-8e121c64/1e74927a-f76d-493c-a462-5f62bd2d9bfd/stable) returned 204
(node:29771) [DEP0180] DeprecationWarning: fs.Stats constructor is deprecated.

This is looking like potentially a transition error with existing users. On my test system I moved the user’s config.json to config.json.bak and launched Mailspring as a new user and it does not get the error at this point. So there is something with this change along with an existing config that is causing this.

I have not dug more into that aspect yet, but could be something needs to be transitioned. Obviously we don’t want our users having to completely setup all their accounts again, but worst case / last resort, if we cannot find another way prompting them that they will need to re-enter their password and why could be a potential option.

Another obervation

It seemd now when safeStorage.decryptStringAsync() throws an exception due to this initialization it since on KDE Plasma org.freedesktop.Secret.Service is not running as a daemon. This seems to be causing a fatal error instead of a graceful fallback.

This is just from a quick observation, so I may be off on that. But it is in the the right direction, I believe.

since on KDE Plasma org.freedesktop.Secret.Service is not running as a daemon.

kwallet provides this dbus interface since version 5.97.0. Is it possible that the version you are running is older?

mailspring --password-store=kwallet6

This should not have an affect for the asynchronous API. As the doc states, only the two dbus APIs and a fallback provider are supported. Kwallet supports those cross desktop standards since 2022, and even KDE apps are transitioning to use it, so i do not expect a change there.

What might be the problem here is that electron defaults to org.freedesktop.portal.Secret, despite not running in a sandbox. It works by providing the application with a secret, that it uses to encrypt its secrets in an application owned file. Not a problem for new installs, as the user does not care where the secret is stored.
org.freedesktop.Secret.Service on the other hand stores application secrets for the application in the system wide keychain, e.g. kwallet. This is also where they were stored with the synchronous safeStorage API, so it should be used for existing installs. There probably is a flag to force use of this, but I do not know it out of my head.

Just guessing, but might be worth to try

The problem is we are building this as a package in our repo for users across different desktops and Windows managers. If I was using this just for me, this is not a problem as I know how to fix it.

Confirming I have been seeing the same issue since the update to version 1.24.0.

Edit: also confirming that moving config.json to config.json.bak in $HOME/.config/Mailspring then logging in again solves the issue, and while it did fix it for me, for people with multiple accounts or enterprise users I can see how this is a workaround and not a solution.

Operating System: Debian GNU/Linux 13
KDE Plasma Version: 6.3.6
KDE Frameworks Version: 6.13.0
Qt Version: 6.8.2
Kernel Version: 7.1.8+deb13-amd64 (64-bit)
Graphics Platform: Wayland
Processors: 16 × 13th Gen Intel® Core™ i5-13400F
Memory: 32 GiB of RAM (31.2 GiB usable)
Graphics Processor: AMD Radeon Graphics
Manufacturer: Micro-Star International Co., Ltd.
Product Name: MS-7E06
System Version: 1.0
1 Like

I think i tracked it down:
With the new safeStorage API, electron talks to kwallet using the standard freedesktop dbus API, instead of a kwallet specific one. Passwords encrypted with the old way do not seem to be decryptable using the new way. Mailspring sees a failure while decrypting and handles that as a fatal error:

    let decrypted: DecryptResult;
    try {
      decrypted = await secureStorage.decrypt(Buffer.from(encryptedCredentials, 'utf-8'));
    } catch (err) {
      // Stored-but-unreadable is not the same as nothing stored. Resolving to an empty keyset
      // would let the next read-modify-write mutator persist it, erasing every account's
      // password over a locked keyring or a secret service that has not started yet.
      this._reportFatalError(
        new Error(
          localized('Mailspring could not read your saved passwords and cannot continue.') +
            this._encryptionUnavailableHint()
        )
      );

I think asking the user to reauthenticate is the cleaner solution for handling that error. There is no need to erease the password if he clicks “no”.

For this issue, we can call decryptString if decryptStringAsync failed, to migrate users over. I have opened a PR for that.

@bengotow Mailspring only calls isAsyncEncryptionAvailable for writes, not reads. You could call that first, exit fatally if it returns false, and otherwise prompt to reauthenticate.

Hey folks,

Thanks for all the context - that’s super helpful.

@LinusDierheimer I think you’re totally right that the fix is to try the async API, and then try the old sync API. I got a local setup to debug this, and it looks like the async API is saving secrets in KWallet as Mailspring Keys / Mailspring Safe Storage and the old API was using Chromium Keys / Chromium Safe Storage, so checking both will give a clean migration path. I think this an upstream Electron bug so I’ll
open an issue for it as well.

It looks like isAsyncEncryptionAvailable is actually a bit… wonky. I had Claude trace down through Electron to see what that function actually does, and it looks like it may always return true on Linux because the Chromium implements an uncrypted Posix fallback secret storage, so it always provides a key. It looks like we can detect that plaintext storage is happening though.

I will see if we can get a fix out for this ASAP today. Sorry for the hassle everyone!

I will see if we can get a fix out for this ASAP today. Sorry for the hassle everyone!

No worries. I have been doing this for a while. It happens.

Just built our package on Solus with the 1.24.1 and tested and it seems to all be working as expected now. I truly appreciate the work to resolve this issue in a timely fashion.

think this an upstream Electron bug so I’ll open an issue for it as well.

Pretty sure it is. They explicitly state in their breaking changes for v45:

The synchronous safeStorage methods are deprecated and will be removed in Electron 46, following Chromium’s removal of the synchronous OSCrypt backend they are built on. Use the asynchronous methods instead, which use the same per-platform key stores. Data encrypted with safeStorage.encryptString() can be decrypted with safeStorage.decryptStringAsync().