Description
The S/MIME plugin v1.0.2 cannot decrypt incoming S/MIME messages although the matching PKCS#12 certificate and private key are imported and successfully unlocked.
The same messages are decrypted correctly by Thunderbird using exactly the same PKCS#12 identity.
Steps to Reproduce
Import a PKCS#12 identity containing an RSA certificate and private key into both Thunderbird and the Bulwark S/MIME plugin.
Unlock the key successfully in Bulwark.
In Thunderbird, send an S/MIME-encrypted message to the same email address using this certificate.
Open the received message in Thunderbird.
Open the same message in Bulwark.
Enter the correct storage passphrase when Bulwark requests it.
Expected Behavior
Bulwark decrypts and displays the message.
Actual Behavior
Bulwark prompts for the storage passphrase but subsequently displays:
Encrypted — couldn’t be decrypted with your keys
The browser console reports:
[plugin:smime] S/MIME decrypt failed Error: Failed to decrypt message with any available key
at smimeDecrypt (:49903:9)
at async onRenderEmailBody (:50913:18)
Thunderbird decrypts the same message immediately.
Only one personal S/MIME certificate is installed in Thunderbird, and the same PKCS#12 identity was imported into Bulwark. Therefore, this is not caused by an old or mismatching private key.
CMS details from another message showing the same failure
The failing smime.p7m is structurally valid and can be parsed successfully with OpenSSL:
contentType: pkcs7-envelopedData (1.2.840.113549.1.7.3)
keyEncryptionAlgorithm:
algorithm: rsaEncryption (1.2.840.113549.1.1.1)
parameter: NULL
contentEncryptionAlgorithm:
algorithm: aes-256-cbc (2.16.840.1.101.3.4.1.42)
The encrypted session key is 256 bytes long, corresponding to a 2048-bit RSA key.
The recipient issuer and serial number match the certificate imported into Bulwark. The plugin therefore finds a matching key record, but both decryption attempts fail.
Suspected cause
unlockPrivateKey() imports a legacy RSAES-PKCS1-v1_5 key using the bundled WebCrypto liner:
legacyDecryptionKey = await getLinerCrypto().subtle.importKey(
"pkcs8",
pkcs8Bytes,
{ name: "RSAES-PKCS1-v1_5" },
false,
["decrypt"]
);
The resulting custom RsaCryptoKey object is then passed to saveSessionKeys() and stored in IndexedDB:
await saveSessionKeys({
id: rec.id,
signingKey,
decryptionKey,
legacyDecryptionKey
});
IndexedDB uses the structured-clone algorithm. Custom class prototypes are not preserved by structured cloning. After reading the object back from IndexedDB, the legacy key may therefore no longer satisfy:
key instanceof RsaCryptoKey
The bundled RSA provider explicitly requires this check in RsaCrypto.checkCryptoKey(). This would explain why:
the matching certificate is found;
the storage passphrase is accepted;
RSA-OAEP is unsuitable for the incoming rsaEncryption recipient;
the legacy RSAES-PKCS1-v1_5 fallback also fails;
Thunderbird decrypts the same message correctly.
Bulwark Version
1.9.2
Stalwart Mail Server Version
0.16.21
Browser
Chrome / Chromium
Operating System
Linux
Screenshots / Screen Recording
Suggested investigation
Inspect the constructor/prototype of legacyDecryptionKey before and after the IndexedDB round trip.
Avoid storing a custom liner RsaCryptoKey instance directly in IndexedDB.
Instead, store a securely wrapped serialized key and re-import it into the liner engine in the consuming iframe/realm.
Preserve and log the original exceptions from both decryptWithKey() attempts. The current empty catch blocks hide the underlying failure and replace it with the generic message:
Failed to decrypt message with any available key
Relevant Logs or Error Output
Additional Context
S/MIME plugin: 1.0.2
Description
The S/MIME plugin v1.0.2 cannot decrypt incoming S/MIME messages although the matching PKCS#12 certificate and private key are imported and successfully unlocked.
The same messages are decrypted correctly by Thunderbird using exactly the same PKCS#12 identity.
Steps to Reproduce
Import a PKCS#12 identity containing an RSA certificate and private key into both Thunderbird and the Bulwark S/MIME plugin.
Unlock the key successfully in Bulwark.
In Thunderbird, send an S/MIME-encrypted message to the same email address using this certificate.
Open the received message in Thunderbird.
Open the same message in Bulwark.
Enter the correct storage passphrase when Bulwark requests it.
Expected Behavior
Bulwark decrypts and displays the message.
Actual Behavior
Bulwark prompts for the storage passphrase but subsequently displays:
Encrypted — couldn’t be decrypted with your keys
The browser console reports:
[plugin:smime] S/MIME decrypt failed Error: Failed to decrypt message with any available key
at smimeDecrypt (:49903:9)
at async onRenderEmailBody (:50913:18)
Thunderbird decrypts the same message immediately.
Only one personal S/MIME certificate is installed in Thunderbird, and the same PKCS#12 identity was imported into Bulwark. Therefore, this is not caused by an old or mismatching private key.
CMS details from another message showing the same failure
The failing smime.p7m is structurally valid and can be parsed successfully with OpenSSL:
contentType: pkcs7-envelopedData (1.2.840.113549.1.7.3)
keyEncryptionAlgorithm:
algorithm: rsaEncryption (1.2.840.113549.1.1.1)
parameter: NULL
contentEncryptionAlgorithm:
algorithm: aes-256-cbc (2.16.840.1.101.3.4.1.42)
The encrypted session key is 256 bytes long, corresponding to a 2048-bit RSA key.
The recipient issuer and serial number match the certificate imported into Bulwark. The plugin therefore finds a matching key record, but both decryption attempts fail.
Suspected cause
unlockPrivateKey() imports a legacy RSAES-PKCS1-v1_5 key using the bundled WebCrypto liner:
legacyDecryptionKey = await getLinerCrypto().subtle.importKey(
"pkcs8",
pkcs8Bytes,
{ name: "RSAES-PKCS1-v1_5" },
false,
["decrypt"]
);
The resulting custom RsaCryptoKey object is then passed to saveSessionKeys() and stored in IndexedDB:
await saveSessionKeys({
id: rec.id,
signingKey,
decryptionKey,
legacyDecryptionKey
});
IndexedDB uses the structured-clone algorithm. Custom class prototypes are not preserved by structured cloning. After reading the object back from IndexedDB, the legacy key may therefore no longer satisfy:
key instanceof RsaCryptoKey
The bundled RSA provider explicitly requires this check in RsaCrypto.checkCryptoKey(). This would explain why:
the matching certificate is found;
the storage passphrase is accepted;
RSA-OAEP is unsuitable for the incoming rsaEncryption recipient;
the legacy RSAES-PKCS1-v1_5 fallback also fails;
Thunderbird decrypts the same message correctly.
Bulwark Version
1.9.2
Stalwart Mail Server Version
0.16.21
Browser
Chrome / Chromium
Operating System
Linux
Screenshots / Screen Recording
Suggested investigation
Inspect the constructor/prototype of legacyDecryptionKey before and after the IndexedDB round trip.
Avoid storing a custom liner RsaCryptoKey instance directly in IndexedDB.
Instead, store a securely wrapped serialized key and re-import it into the liner engine in the consuming iframe/realm.
Preserve and log the original exceptions from both decryptWithKey() attempts. The current empty catch blocks hide the underlying failure and replace it with the generic message:
Failed to decrypt message with any available key
Relevant Logs or Error Output
Additional Context
S/MIME plugin: 1.0.2