Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app-k9mail/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
android:theme="@style/Theme.K9.DayNight.Dialog.Translucent"
/>

<activity
android:name="com.fsck.k9.ui.settings.account.SmimeAppSelectDialog"
android:configChanges="locale"
android:theme="@style/Theme.K9.DayNight.Dialog.Translucent"
/>

<activity
android:name="com.fsck.k9.ui.notification.DeleteConfirmationActivity"
android:excludeFromRecents="true"
Expand Down
6 changes: 6 additions & 0 deletions app-thunderbird/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
android:theme="@style/Theme.Thunderbird.DayNight.Dialog.Translucent"
/>

<activity
android:name="com.fsck.k9.ui.settings.account.SmimeAppSelectDialog"
android:configChanges="locale"
android:theme="@style/Theme.Thunderbird.DayNight.Dialog.Translucent"
/>

<activity
android:name="com.fsck.k9.ui.notification.DeleteConfirmationActivity"
android:excludeFromRecents="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,19 @@ data class LegacyAccount(
val isStripSignature: Boolean = false,
val isSyncRemoteDeletions: Boolean = false,
val openPgpProvider: String? = null,
/**
* Package name of the installed S/MIME provider app (e.g.
* `"com.ciphermail.android"`) selected for this account, or `null` if
* S/MIME is not enabled. Used to target `ISmimeService` binds with an
* explicit `setPackage()`, avoiding intent-filter ambiguity when more
* than one provider is installed.
*/
val smimeProvider: String? = null,
/** Whether S/MIME is turned on for this account; see LegacyAccountDto.smimeEnabled. */
val smimeEnabled: Boolean = false,
/** Last S/MIME sign/encrypt choice in the composer; see LegacyAccountDto. */
val smimeSign: Boolean = true,
val smimeEncrypt: Boolean = true,
val openPgpKey: Long = 0,
val autocryptPreferEncryptMutual: Boolean = false,
val isOpenPgpHideSignOnly: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,53 @@ open class LegacyAccountDto(
field = value?.takeIf { it.isNotEmpty() }
}

/**
* Package name of the S/MIME provider app this account uses (e.g.
* `"com.ciphermail.android"`), or `null` if S/MIME is not enabled.
*
* Setting an empty string is normalised to `null` so callers can
* unset the provider by writing the result of a possibly-empty
* EditText without first checking for emptiness.
*/
@get:Synchronized
@set:Synchronized
var smimeProvider: String? = null
set(value) {
field = value?.takeIf { it.isNotEmpty() }
}

/**
* Whether the user has turned S/MIME on for this account. Independent of
* [smimeProvider]: S/MIME can be enabled while no provider app is installed
* (provider unresolved). In that state outgoing mail is blocked at send
* time rather than silently sent unencrypted.
*/
@get:Synchronized
@set:Synchronized
var smimeEnabled: Boolean = false

/**
* Per-account memory of the user's last S/MIME sign / encrypt choice in the
* composer. Defaults to true (sign and encrypt). When both are false an
* S/MIME-enabled account sends a plain message.
*/
@get:Synchronized
@set:Synchronized
var smimeSign: Boolean = true

@get:Synchronized
@set:Synchronized
var smimeEncrypt: Boolean = true

/**
* True iff S/MIME is turned on for this account (see [smimeEnabled]).
* Note: enabled does not imply a provider is installed/resolved — callers
* that actually perform crypto must additionally check [smimeProvider] and
* that its package is installed.
*/
val isSmimeProviderConfigured: Boolean
get() = smimeEnabled

@get:Synchronized
@set:Synchronized
var openPgpKey: Long = 0
Expand Down
4 changes: 4 additions & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,21 @@ generator, in this case, **mdbook**. It defines the structure and navigation of
- [0006 - White Label Architecture](architecture/adr/0006-white-label-architecture.md)
- [0007 - Project Structure](architecture/adr/0007-project-structure.md)
- [0008 - Change Shared Module package to `net.thunderbird`](architecture/adr/0008-change-shared-modules-package-name.md)
- [0009 - Use a Companion App + AIDL Service for S/MIME](architecture/adr/0009-smime-companion-app-architecture.md)
- [Proposed]()
- [Rejected]()
- [User Guide]()
- [Setup]()
- [Installing ADB](user-guide/setup/installing-adb.md)
- [Enabling S/MIME via CipherMail](user-guide/setup/enabling-smime.md)
- [Troubleshooting]()
- [Collecting Debug Logs](user-guide/troubleshooting/collecting-debug-logs.md)
- [Find your app version](user-guide/troubleshooting/find-your-app-version.md)
- [Developer](developer/README.md)
- [Database Migration Checklist](developer/db-migration-checklist.md)
- [Foldable Device Support](developer/foldable-device-support.md)
- [Preference Migration Guide](developer/preference-migration-guide.md)
- [Writing an S/MIME Provider](developer/writing-smime-provider.md)
- [Release]()
- [Release Process](release/RELEASE.md)
- [Release Automation](release/AUTOMATION.md)
Expand All @@ -59,6 +62,7 @@ generator, in this case, **mdbook**. It defines the structure and navigation of
- [Manual Release (historical)](release/HISTORICAL_RELEASE.md)
- [Security]()
- [Threat Modeling Guide](security/threat-modeling-guide.md)
- [S/MIME Companion Threat Model](security/smime-companion-threat-model.md)

---

Expand Down
113 changes: 113 additions & 0 deletions docs/architecture/adr/0009-smime-companion-app-architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Use a Companion App + AIDL Service for S/MIME

## Status

- **Accepted**

## Context

Thunderbird for Android needs to support end-to-end encryption with S/MIME, in addition to its existing OpenPGP support.
S/MIME has the same broad shape as OpenPGP — sign, encrypt, decrypt, verify, certificate lookup — but uses a different
trust model (X.509 certificates and CAs) and a substantially different implementation stack (Bouncy Castle's CMS layer,
PKCS#12 import, OCSP, CRL distribution, key-store management with passphrase entry).

We evaluated three integration strategies:

- **Option A — in-process library.** Bundle a full S/MIME implementation directly into Thunderbird, alongside the
existing `legacy/crypto-openpgp` module. Tight integration, no IPC, but a very large surface area to maintain:
certificate stores, OCSP, CRL fetching, PKCS#12 import, keystore UI, passphrase entry, etc. Doubles Thunderbird's
crypto-attack surface and significantly enlarges the app's dependency footprint.

- **Option B — embed a third-party crypto core.** Pull in an existing S/MIME library (e.g. the CipherMail core) as a
JAR/AAR. Smaller than Option A but still leaves Thunderbird responsible for keystore lifecycle, certificate UI, and
passphrase prompts. License compatibility (the reference S/MIME stack is GPL) is also a hard blocker for Thunderbird's
app distribution.

- **Option C — companion app over AIDL.** Thunderbird depends only on a small AIDL API and binds, at runtime, to a
separate S/MIME provider app (CipherMail) that owns all key material, certificate stores, and crypto operations.
This mirrors how OpenKeychain already provides OpenPGP for Thunderbird via `plugins/openpgp-api-lib`.

Option C provides the cleanest symmetry with the existing OpenPGP integration, isolates the GPL'd crypto core in a
separate process and a separate app distribution, and keeps Thunderbird's binary size and dependency graph essentially
unchanged.

## Decision

We will integrate S/MIME via a companion-app architecture, paralleling the existing OpenPGP / OpenKeychain integration.

Specifically:

- A new module `plugins/smime-api/smime-api/` defines the AIDL service contract and its Parcelable result types.
It mirrors `plugins/openpgp-api-lib/openpgp-api/` in layout and licensing (Apache 2.0).
- The reference provider is **CipherMail** (`com.ciphermail.android`), an existing standalone Android app maintained
in a separate repository, which adds an `SmimeService` bound service implementing `ISmimeService`.
- Thunderbird discovers providers at runtime via an `<intent-filter>` query for `com.ciphermail.smime.api.ISmimeService`.
When more than one provider is installed, `SmimeAppSelectDialog` lets the user choose.
- The service contract exposes five actions: `CHECK_PERMISSION`, `DECRYPT_VERIFY`, `SIGN_AND_ENCRYPT`,
`GET_CERTIFICATES`, `IMPORT_CERTIFICATE`. Bulk MIME data streams through `ParcelFileDescriptor` pipes rather than
Intent extras, keeping Binder transactions small.
- Cross-process user interaction (e.g. keystore-passphrase entry) follows the same `RESULT_CODE_USER_INTERACTION_REQUIRED`
+ `PendingIntent` pattern OpenKeychain already uses. The provider's passphrase dialog runs in the provider's process;
Thunderbird launches it via `startIntentSenderForResult` and retries the operation on `RESULT_OK`.
- Per-account configuration mirrors OpenPGP: a new `smimeProvider` field on `LegacyAccount` selects which installed
provider that account uses, surfaced as a Preference under Settings → S/MIME.

### Request flow — sign and encrypt (with passphrase unlock)

```mermaid
sequenceDiagram
autonumber
participant TB as Thunderbird
participant SVC as Provider service<br/>(ISmimeService)
participant DIA as Provider passphrase<br/>dialog
participant KEY as Provider keystore

TB->>SVC: createOutputPipe(pipeId)
SVC-->>TB: ParcelFileDescriptor (write end)
TB->>SVC: execute(SIGN_AND_ENCRYPT, inputPipe, pipeId)
SVC->>KEY: isUnlocked()
KEY-->>SVC: false
SVC-->>TB: RESULT_CODE_USER_INTERACTION_REQUIRED<br/>+ immutable PendingIntent

TB->>DIA: startIntentSenderForResult(PendingIntent)
Note over DIA: user enters passphrase
DIA-)KEY: unlock (provider-internal)
DIA-->>TB: RESULT_OK

TB->>SVC: execute(SIGN_AND_ENCRYPT, inputPipe, pipeId) [retry]
SVC->>KEY: sign + encrypt
SVC->>TB: stream wrapped MIME bytes via output pipe
SVC-->>TB: RESULT_CODE_SUCCESS
```

The `USER_INTERACTION_REQUIRED` reply returns within tens of milliseconds.
The retry succeeds without further user input because the provider's
keystore is now unlocked. Decrypt + verify follows the same shape, with
`SmimeDecryptionResult` and `SmimeSignatureResult` returned as result
Parcelables in step 11.

## Consequences

### Positive Consequences

- S/MIME implementation, key material, and certificate stores stay outside the Thunderbird process and APK.
- License isolation: the GPL crypto core ships in a separate app with its own distribution channel.
- Symmetry with OpenPGP makes the integration easy to reason about; existing patterns (`MessageCryptoHelper`,
`RecipientPresenter`, `MessageCompose`) extend directly.
- Multi-provider support is free: any app that declares the AIDL service appears in `SmimeAppSelectDialog`.
- Thunderbird's binary size and dependency graph are essentially unchanged — only the small AIDL stub is added.

### Negative Consequences

- Users must install two apps to use S/MIME. CipherMail is published on Google Play (and is planned for F-Droid once
this companion API lands upstream), so the install is a single search-and-install step, but it is still a separate
user action that Thunderbird cannot perform automatically. The S/MIME preference row surfaces an explanatory message
when no provider is installed.
- Cross-process round-trips add latency on the first call after process start (provider cold-start, keystore unlock).
Subsequent calls are fast.
- The cross-process passphrase-unlock dance is more involved than an in-process prompt and was a source of early bugs
(cached-passphrase singletons, broadcast receivers, AndroidKeyStore key loss after reinstall).
- The contract between Thunderbird and the provider becomes a versioned external API; breaking changes require an
`EXTRA_API_VERSION` bump and explicit incompatibility handling in `SmimeError.INCOMPATIBLE_API_VERSIONS`.
- Two repositories must be kept in sync: `plugins/smime-api/smime-api/` here, and the mirrored module in the CipherMail
repository at `smime-api/`.
Loading
Loading