|
| 1 | +# Use a Companion App + AIDL Service for S/MIME |
| 2 | + |
| 3 | +## Status |
| 4 | + |
| 5 | +- **Accepted** |
| 6 | + |
| 7 | +## Context |
| 8 | + |
| 9 | +Thunderbird for Android needs to support end-to-end encryption with S/MIME, in addition to its existing OpenPGP support. |
| 10 | +S/MIME has the same broad shape as OpenPGP — sign, encrypt, decrypt, verify, certificate lookup — but uses a different |
| 11 | +trust model (X.509 certificates and CAs) and a substantially different implementation stack (Bouncy Castle's CMS layer, |
| 12 | +PKCS#12 import, OCSP, CRL distribution, key-store management with passphrase entry). |
| 13 | + |
| 14 | +We evaluated three integration strategies: |
| 15 | + |
| 16 | +- **Option A — in-process library.** Bundle a full S/MIME implementation directly into Thunderbird, alongside the |
| 17 | + existing `legacy/crypto-openpgp` module. Tight integration, no IPC, but a very large surface area to maintain: |
| 18 | + certificate stores, OCSP, CRL fetching, PKCS#12 import, keystore UI, passphrase entry, etc. Doubles Thunderbird's |
| 19 | + crypto-attack surface and significantly enlarges the app's dependency footprint. |
| 20 | + |
| 21 | +- **Option B — embed a third-party crypto core.** Pull in an existing S/MIME library (e.g. the CipherMail core) as a |
| 22 | + JAR/AAR. Smaller than Option A but still leaves Thunderbird responsible for keystore lifecycle, certificate UI, and |
| 23 | + passphrase prompts. License compatibility (the reference S/MIME stack is GPL) is also a hard blocker for Thunderbird's |
| 24 | + app distribution. |
| 25 | + |
| 26 | +- **Option C — companion app over AIDL.** Thunderbird depends only on a small AIDL API and binds, at runtime, to a |
| 27 | + separate S/MIME provider app (CipherMail) that owns all key material, certificate stores, and crypto operations. |
| 28 | + This mirrors how OpenKeychain already provides OpenPGP for Thunderbird via `plugins/openpgp-api-lib`. |
| 29 | + |
| 30 | +Option C provides the cleanest symmetry with the existing OpenPGP integration, isolates the GPL'd crypto core in a |
| 31 | +separate process and a separate app distribution, and keeps Thunderbird's binary size and dependency graph essentially |
| 32 | +unchanged. |
| 33 | + |
| 34 | +## Decision |
| 35 | + |
| 36 | +We will integrate S/MIME via a companion-app architecture, paralleling the existing OpenPGP / OpenKeychain integration. |
| 37 | + |
| 38 | +Specifically: |
| 39 | + |
| 40 | +- A new module `plugins/smime-api/smime-api/` defines the AIDL service contract and its Parcelable result types. |
| 41 | + It mirrors `plugins/openpgp-api-lib/openpgp-api/` in layout and licensing (Apache 2.0). |
| 42 | +- The reference provider is **CipherMail** (`com.ciphermail.android`), an existing standalone Android app maintained |
| 43 | + in a separate repository, which adds an `SmimeService` bound service implementing `ISmimeService`. |
| 44 | +- Thunderbird discovers providers at runtime via an `<intent-filter>` query for `com.ciphermail.smime.api.ISmimeService`. |
| 45 | + When more than one provider is installed, `SmimeAppSelectDialog` lets the user choose. |
| 46 | +- The service contract exposes five actions: `CHECK_PERMISSION`, `DECRYPT_VERIFY`, `SIGN_AND_ENCRYPT`, |
| 47 | + `GET_CERTIFICATES`, `IMPORT_CERTIFICATE`. Bulk MIME data streams through `ParcelFileDescriptor` pipes rather than |
| 48 | + Intent extras, keeping Binder transactions small. |
| 49 | +- Cross-process user interaction (e.g. keystore-passphrase entry) follows the same `RESULT_CODE_USER_INTERACTION_REQUIRED` |
| 50 | + + `PendingIntent` pattern OpenKeychain already uses. The provider's passphrase dialog runs in the provider's process; |
| 51 | + Thunderbird launches it via `startIntentSenderForResult` and retries the operation on `RESULT_OK`. |
| 52 | +- Per-account configuration mirrors OpenPGP: a new `smimeProvider` field on `LegacyAccount` selects which installed |
| 53 | + provider that account uses, surfaced as a Preference under Settings → S/MIME. |
| 54 | + |
| 55 | +### Request flow — sign and encrypt (with passphrase unlock) |
| 56 | + |
| 57 | +```mermaid |
| 58 | +sequenceDiagram |
| 59 | + autonumber |
| 60 | + participant TB as Thunderbird |
| 61 | + participant SVC as Provider service<br/>(ISmimeService) |
| 62 | + participant DIA as Provider passphrase<br/>dialog |
| 63 | + participant KEY as Provider keystore |
| 64 | +
|
| 65 | + TB->>SVC: createOutputPipe(pipeId) |
| 66 | + SVC-->>TB: ParcelFileDescriptor (write end) |
| 67 | + TB->>SVC: execute(SIGN_AND_ENCRYPT, inputPipe, pipeId) |
| 68 | + SVC->>KEY: isUnlocked() |
| 69 | + KEY-->>SVC: false |
| 70 | + SVC-->>TB: RESULT_CODE_USER_INTERACTION_REQUIRED<br/>+ immutable PendingIntent |
| 71 | +
|
| 72 | + TB->>DIA: startIntentSenderForResult(PendingIntent) |
| 73 | + Note over DIA: user enters passphrase |
| 74 | + DIA-)KEY: unlock (provider-internal) |
| 75 | + DIA-->>TB: RESULT_OK |
| 76 | +
|
| 77 | + TB->>SVC: execute(SIGN_AND_ENCRYPT, inputPipe, pipeId) [retry] |
| 78 | + SVC->>KEY: sign + encrypt |
| 79 | + SVC->>TB: stream wrapped MIME bytes via output pipe |
| 80 | + SVC-->>TB: RESULT_CODE_SUCCESS |
| 81 | +``` |
| 82 | + |
| 83 | +The `USER_INTERACTION_REQUIRED` reply returns within tens of milliseconds. |
| 84 | +The retry succeeds without further user input because the provider's |
| 85 | +keystore is now unlocked. Decrypt + verify follows the same shape, with |
| 86 | +`SmimeDecryptionResult` and `SmimeSignatureResult` returned as result |
| 87 | +Parcelables in step 11. |
| 88 | + |
| 89 | +## Consequences |
| 90 | + |
| 91 | +### Positive Consequences |
| 92 | + |
| 93 | +- S/MIME implementation, key material, and certificate stores stay outside the Thunderbird process and APK. |
| 94 | +- License isolation: the GPL crypto core ships in a separate app with its own distribution channel. |
| 95 | +- Symmetry with OpenPGP makes the integration easy to reason about; existing patterns (`MessageCryptoHelper`, |
| 96 | + `RecipientPresenter`, `MessageCompose`) extend directly. |
| 97 | +- Multi-provider support is free: any app that declares the AIDL service appears in `SmimeAppSelectDialog`. |
| 98 | +- Thunderbird's binary size and dependency graph are essentially unchanged — only the small AIDL stub is added. |
| 99 | + |
| 100 | +### Negative Consequences |
| 101 | + |
| 102 | +- Users must install two apps to use S/MIME. CipherMail is published on Google Play (and is planned for F-Droid once |
| 103 | + this companion API lands upstream), so the install is a single search-and-install step, but it is still a separate |
| 104 | + user action that Thunderbird cannot perform automatically. The S/MIME preference row surfaces an explanatory message |
| 105 | + when no provider is installed. |
| 106 | +- Cross-process round-trips add latency on the first call after process start (provider cold-start, keystore unlock). |
| 107 | + Subsequent calls are fast. |
| 108 | +- The cross-process passphrase-unlock dance is more involved than an in-process prompt and was a source of early bugs |
| 109 | + (cached-passphrase singletons, broadcast receivers, AndroidKeyStore key loss after reinstall). |
| 110 | +- The contract between Thunderbird and the provider becomes a versioned external API; breaking changes require an |
| 111 | + `EXTRA_API_VERSION` bump and explicit incompatibility handling in `SmimeError.INCOMPATIBLE_API_VERSIONS`. |
| 112 | +- Two repositories must be kept in sync: `plugins/smime-api/smime-api/` here, and the mirrored module in the CipherMail |
| 113 | + repository at `smime-api/`. |
0 commit comments