Manage public keys, over Stalwart's x:PublicKey registry
A new Settings section, next to Identities & signatures: list, add, rename and remove the OpenPGP public keys and S/MIME certificates published on this account. Only public material -- no private key is stored, requested or sent by any of this. This is PR #67 revived. That branch was built against 0.16.19, closed unmerged on 2026-08-26, and shares no ancestry with main after the email scrub, so it is ported rather than rebased: the four files it added are carried over, the three it edited are applied by hand, and everything it claimed was re-probed against the live 0.16.20 on 2026-09-05. The i18n work is new -- nine catalogues landed on 2026-08-31, after that branch was written. What the re-probe confirmed, unchanged from 0.16.19: - An ordinary user may read *and* write their own keys, though the permissions table lists every sysPublicKey* permission as administrative. get and query both answered for a normal account, and a malformed create came back invalidProperties naming `key` rather than forbidden -- a rejection of the key, not of the person. - The server parses the key and says precisely what is wrong. So ihasmail does not validate key material; the server's message is shown verbatim, as password-policy rejections already are. - urn:stalwart:jmap is still absent from the session's top-level capabilities and present per-account, so the check that reads all three places is still the one that works. What it added, none of which was known before: - A key can parse perfectly and still be refused, with different words: a sign-and-certify key with no encryption subkey -- what `gpg --quick-generate-key` produces -- gets "Could not find any suitable keys in OpenPGP public key". That is the rejection somebody exporting from GnuPG will actually meet, and it is not a paste error, so collapsing both to "invalid key" would send them back to the clipboard for a problem that is in the key. - emailAddresses comes back as {} when empty -- an object where a JMAP list property should be an array. It type-checks, then throws in join() while the list renders. normalize() checked the shape already; there is now a test saying why, and the mock answers {} the same way, because one that helpfully returned [] would let that crash ship. - A create answers with the id alone, no createdAt, so adding a key reloads rather than believing the response. - destroy works and leaves the registry empty. PR #67 shipped that path untested -- its live probe was refused before anything was created, so there was nothing to destroy. - Patching `key` is allowed by the server. The mock still refuses it, now deliberately rather than for want of evidence: ihasmail replaces a key by adding one and removing the old, which keeps createdAt meaning what it says. x:EncryptionAtRest still does not exist on 0.16.20 -- asking for it is an unknownMethod. encryptionAtRest is a field on x:AccountSettings, and its value is a typed object ({"@type":"Disabled"}) rather than the bare string ROADMAP described. Nothing here writes it. An empty description is now sent as empty rather than filled in with "Key". The description is stored on the server, so a default invented in the client would be whichever language the adder happened to be using; the list labels a blank one at render time instead. Verified in a browser against the mock, not only in tests: both rejections reach the toast in the server's own words with the form still filled in, a good key renders its card, the kind is labelled from the armour header, renaming persists, removing asks first and empties the list, and the whole section reads correctly in German.
This commit is contained in:
+7
-1
@@ -15,7 +15,13 @@ See [KNOWN-ISSUES.md](KNOWN-ISSUES.md) for what is built but worth knowing about
|
||||
- **A translation anybody has checked.** The translations themselves shipped on 2026-08-31 and are no longer on this page: nine of them, alongside English, and the extraction that had always been the hard half is done — see [FEATURES.md](FEATURES.md#interface-language). What is *not* done is the other half, and it is the half that cannot be bought or automated. All nine were produced by AI against standard dictionaries and **not one has been read by anybody who speaks the language**, which is exactly where a bad translation does harm rather than merely looking untidy. They ship marked Beta, with that said in Settings and a link for reporting anything wrong, because shipping them quietly would ask people to trust text nobody has checked. A language loses the Beta mark when a speaker reads it and says so — a deliberate act by a person, not something a coverage percentage earns. If you speak one of them and are willing to read a few hundred strings, that is the single most useful thing anyone could contribute right now.
|
||||
- **Right-to-left languages.** Arabic, Hebrew and Persian are held back deliberately, and not for want of translators. RTL is bidi and layout work throughout — mirrored panes, gesture directions, icon sides, the message list's own geometry — and a catalogue without it produces a page that is translated and unusable. Adding one is not another entry in the picker.
|
||||
- **Two-factor sign-in.** Today an account with 2FA must use an app password (see [Quick start](README.md#quick-start-docker)), and Settings › Security offers no way to switch 2FA *on* — only off, for an account that already has it. Supporting a TOTP code directly means implementing OAuth: Stalwart offers the authorization-code and device flows and no password grant, so ihasmail would hand sign-in to Stalwart's own login and come back with a token. That is a better security posture than the sealed password it holds now — a refresh token rather than a credential — but it replaces ihasmail's own sign-in page for those users and may need an OAuth client registered. Came out of [#75](https://github.com/Coffey-Labs/ihasmail/issues/75), which is closed: what was reported there was a sign-in refused with nothing but "Invalid credentials", and that was fixed by saying what is actually happening and pointing at app passwords. The OAuth work it uncovered is tracked here rather than as an open issue, so there is no ticket to watch for it.
|
||||
- **S/MIME.** Backlogged, to be built when it is feature-ready — not refused. It is on this page rather than in the tracker because nothing is in progress and there is no ticket to watch, and it is written down at all because until now it existed only in conversation, which is the state a plan is in just before it is forgotten. [PR #67](https://github.com/Coffey-Labs/ihasmail/pull/67) is the starting point: a working public-key manager over Stalwart's `x:PublicKey` registry, built and then closed unmerged on 2026-08-26. Nothing of it is in the tree today. What the probing behind it established, against a live Stalwart **0.16.19** on 2026-08-26 and not re-run since the 0.16.20 upgrade: `encryptionAtRest` is a *field on* `x:AccountSettings` beside `description`, `locale` and `timeZone` — there is no `x:EncryptionAtRest` object, whatever the docs suggest — it is self-service with no administrator involved, taking `Disabled` / `Aes128` / `Aes256` plus `publicKey`, `encryptOnAppend` and `allowSpamTraining`; ordinary users can read and write their own `x:PublicKey` entries even though Stalwart's permissions table lists every `sysPublicKey*` permission as admin-only; and Stalwart parses key material itself and returns a precise complaint, so a client should surface the server's message rather than invent one. **The design caveat that has to be got right before any encryption UI ships:** turning encryption-at-rest *off does not decrypt what is already there*. Enabling it is a one-way door — every message delivered while it was on stays encrypted on disk, readable only by a client holding the private key. That is a property of Stalwart rather than a policy choice, and it is the part a user cannot undo by changing their mind.
|
||||
- **Signing, encrypting, decrypting and verifying mail.** *Managing* keys is built and no longer on this page: Settings › Encryption keys publishes OpenPGP public keys and S/MIME certificates over Stalwart's `x:PublicKey` registry — see [FEATURES.md](FEATURES.md#encryption-keys). What is not built is anything that *uses* them, and the reason is not Stalwart. S/MIME itself is CMS over the message body: JMAP hands a client the MIME blob and the rest is the client's work.
|
||||
|
||||
The blocker is a security model, not code. Signing and decrypting need a private key in a page served by the same host that would handle it, which runs straight into two things ihasmail says about itself: that it never stores a credential, and that it runs immutably with nowhere to keep one. That question has to be answered out loud before it is a feature to build, and it has not been. Verifying a signature is the exception — it needs only public keys, touches none of this, and is tractable on its own.
|
||||
|
||||
Encryption at rest is the one thing known to consume a registered key, and ihasmail does not offer it yet. It is `encryptionAtRest`, a field on `x:AccountSettings` beside `description`, `locale` and `timeZone` — there is no `x:EncryptionAtRest` object whatever the docs suggest, and its value is a typed object (`{"@type": "Disabled"}`) rather than a bare string. It is self-service with no administrator involved. **The design caveat that has to be got right before any encryption UI ships:** turning it *off does not decrypt what is already there*. Enabling it is a one-way door — every message delivered while it was on stays encrypted on disk, readable only by a client holding the private key. That is a property of Stalwart rather than a policy choice, and it is the part a user cannot undo by changing their mind.
|
||||
|
||||
All of the above was probed against a live Stalwart **0.16.20** on 2026-09-05; the findings, including two rejections worth telling apart and a list property that arrives as an object, are in [KNOWN-ISSUES.md](KNOWN-ISSUES.md).
|
||||
|
||||
**Why S/MIME rather than OpenPGP, and why neither is urgent.** End-to-end encrypted mail never reached the mainstream and is not on its way there: as a share of the world's email, PGP-encrypted messages are a rounding error, and the most successful use of OpenPGP is signing packages rather than sending mail. The reasons are structural rather than a matter of better tooling. Everyone in a thread has to take part, so the network effect works against it from the first reply. Key discovery was never solved — keyservers were unauthenticated and got weaponised in the 2019 certificate-flooding attacks, which made specific people's keys unusable by any client that fetched them, and WKD is better without being universal. There is no forward secrecy, so one compromised key retroactively opens everything ever received. The metadata stays in the clear: subject lines are cleartext in classic PGP/MIME, and who corresponded with whom is often the sensitive part. Losing a key loses the mail permanently. And it breaks the client — no server-side search, degraded spam filtering, awkward on a phone — while EFAIL showed in 2018 that the clients themselves were exploitable through MIME and HTML handling. Meanwhile the actual privacy win arrived invisibly and without anyone participating, in STARTTLS, MTA-STS and DANE.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user