|
1 | 1 | = Proteus |
2 | 2 |
|
3 | | -// What the Signal Protocol is (double ratchet, forward secrecy, break-in recovery) |
4 | | -// Why Wire built Proteus: a Signal-compatible implementation for the Wire protocol |
5 | | -// The prekey model: how a recipient's prekeys allow a sender to open a session without them being online |
6 | | -// One-to-one only: each conversation is a bilateral session; group messaging is n*(n-1) sessions |
7 | | -// Why that doesn't scale: O(n²) messages per group update, no forward secrecy for large groups |
8 | | -// Why Proteus is being deprecated in favor of MLS |
9 | | - |
10 | | -// == CoreCrypto's Proteus Interface |
11 | | - |
12 | | -// ProteusCentral and what it owns (identity, session cache) |
13 | | -// Session lifecycle: proteus_session_from_prekey vs. proteus_session_from_message |
14 | | -// Encrypt/decrypt, fingerprints, last-resort prekey |
15 | | -// How Proteus state lives inside the same TransactionContext as MLS |
16 | | -// Migration note: pointing users toward the CC9→CC10 migration guide |
| 3 | +Proteus is Wire's implementation of the https://signal.org/docs/[Signal Protocol]: a double-ratchet scheme providing forward secrecy and break-in recovery for one-to-one encrypted sessions. |
| 4 | + |
| 5 | +Proteus is intrinsically a direct protocol: every message is individually encrypted for each recipient. |
| 6 | +Groups in Proteus are a fiction managed collaboratively between the backend and the client; they do not exist at the protocol level. |
| 7 | +Encrypting a message for a group of _n_ clients requires sending _n_ individually encrypted copies. |
| 8 | +This effect is compounded by the fact that a logical user in the sense of a human is very likely to have multiple clients in terms of individual devices. |
| 9 | +This O(n²) overhead is manageable for small groups, but becomes expensive as they scale, and it was the primary motivation for adopting MLS. |
| 10 | + |
| 11 | +Proteus remains supported for backwards compatibility, but has not received direct development work in some time. |
| 12 | +Its implementaion is already feature-gated for easy removal, and is intended to be removed as soon as we can confirm that a sufficiency of clients have upgraded their conversations to MLS. |
| 13 | + |
| 14 | +== Initialization |
| 15 | + |
| 16 | +Before any Proteus operation, the Proteus subsystem must be explicitly initialized within a transaction: |
| 17 | + |
| 18 | +[source] |
| 19 | +---- |
| 20 | +transaction_context.proteusInit() |
| 21 | +---- |
| 22 | + |
| 23 | +This loads the device's Proteus identity from the keystore, creating it if it does not yet exist. |
| 24 | +All other Proteus methods will return an error if called before `proteusInit()` has succeeded. |
| 25 | + |
| 26 | +== Identity and Fingerprints |
| 27 | + |
| 28 | +A Proteus identity is a long-lived Ed25519 keypair tied to the device. |
| 29 | +CoreCrypto exposes three fingerprint accessors, each returning a lowercase hex string: |
| 30 | + |
| 31 | +`proteusFingerprint()`:: |
| 32 | + The local device's public key. |
| 33 | + Remains stable for the lifetime of the keystore. |
| 34 | + |
| 35 | +`proteusFingerprintLocal(sessionId)`:: |
| 36 | + The local public key as seen from within a specific session. |
| 37 | + Equivalent to `proteusFingerprint()` but scoped to a session for API symmetry. |
| 38 | + |
| 39 | +`proteusFingerprintRemote(sessionId)`:: |
| 40 | + The remote peer's public key within a session. |
| 41 | + Can be used to verify the peer's identity out-of-band. |
| 42 | + |
| 43 | +`proteusFingerprintPrekeybundle(prekey)`:: |
| 44 | + Extracts the public key fingerprint from a serialized prekey bundle without opening a session. |
| 45 | + Useful for verifying a peer's identity before establishing a session. |
| 46 | + |
| 47 | +== Prekeys |
| 48 | + |
| 49 | +Prekeys are the mechanism that allows a sender to open a Proteus session with a recipient who is offline. |
| 50 | +The recipient publishes a set of one-time prekey bundles to the delivery service in advance; the sender fetches one and uses it to bootstrap the session. |
| 51 | + |
| 52 | +CoreCrypto provides two ways to generate prekeys: |
| 53 | + |
| 54 | +`proteusNewPrekey(id)`:: |
| 55 | + Generates a prekey with an explicit numeric ID (a `u16`). |
| 56 | + Use this when your app manages the prekey ID space itself. |
| 57 | + |
| 58 | +`proteusNewPrekeyAuto()`:: |
| 59 | + Generates a prekey with an automatically incremented ID and returns both the ID and the serialized bundle. |
| 60 | + Prefer this unless you have a specific reason to control IDs manually. |
| 61 | + |
| 62 | +Both return a CBOR-serialized prekey bundle ready to upload to the delivery service. |
| 63 | + |
| 64 | +=== The Last Resort Prekey |
| 65 | + |
| 66 | +Proteus reserves prekey ID 65535 (`u16::MAX`) as a last resort prekey. |
| 67 | +Unlike one-time prekeys, the last resort prekey is never consumed — it stays in the keystore indefinitely so that a sender can always open a session even when all one-time prekeys have been exhausted. |
| 68 | + |
| 69 | +[source] |
| 70 | +---- |
| 71 | +prekey = transaction_context.proteusLastResortPrekey() |
| 72 | +---- |
| 73 | + |
| 74 | +The constant `proteusLastResortPrekeyId()` returns `65535` and can be used to exclude this ID when generating ordinary prekeys. |
| 75 | + |
| 76 | +== Sessions |
| 77 | + |
| 78 | +A Proteus session represents an established encrypted channel with one remote client, identified by a caller-supplied `sessionId` string. |
| 79 | +Session state is cached in memory and persisted to the keystore. |
| 80 | + |
| 81 | +There are two ways to establish a new session, depending on which side initiates: |
| 82 | + |
| 83 | +`proteusSessionFromPrekey(sessionId, prekey)`:: |
| 84 | + Used by the *sender* to initiate a session. |
| 85 | + Takes the remote client's serialized prekey bundle (fetched from the delivery service) and creates a local session ready to encrypt. |
| 86 | + |
| 87 | +`proteusSessionFromMessage(sessionId, envelope)`:: |
| 88 | + Used by the *recipient* upon receiving the first message. |
| 89 | + Decrypts the initial envelope, establishes the session, and returns the plaintext in a single step. |
| 90 | + |
| 91 | +Once established, a session can be checked for existence with `proteusSessionExists(sessionId)` and explicitly removed with `proteusSessionDelete(sessionId)`. |
| 92 | +Manual saves via `proteusSessionSave(sessionId)` are available but not normally required — sessions are persisted automatically when encrypting or decrypting. |
| 93 | + |
| 94 | +== Encrypting and Decrypting |
| 95 | + |
| 96 | +With a session established, encryption and decryption are straightforward: |
| 97 | + |
| 98 | +[source] |
| 99 | +---- |
| 100 | +ciphertext = transaction_context.proteusEncrypt(sessionId, plaintext) |
| 101 | +plaintext = transaction_context.proteusDecrypt(sessionId, ciphertext) |
| 102 | +---- |
| 103 | + |
| 104 | +=== Batched Encryption |
| 105 | + |
| 106 | +Because a group message in Proteus requires one encrypted copy per recipient client, CoreCrypto provides a batched variant to reduce FFI round-trips: |
| 107 | + |
| 108 | +[source] |
| 109 | +---- |
| 110 | +map = transaction_context.proteusEncryptBatched(sessionIds, plaintext) |
| 111 | +// map: { sessionId -> ciphertext } |
| 112 | +---- |
| 113 | + |
| 114 | +This is more efficient than calling `proteusEncrypt` in a loop and should be preferred whenever sending to multiple sessions simultaneously. |
| 115 | + |
| 116 | +=== Safe Decrypt |
| 117 | + |
| 118 | +`proteusDecryptSafe(sessionId, ciphertext)` is a convenience wrapper that handles the common case where you do not know in advance whether the session already exists. |
| 119 | +It opens the session from the message envelope if necessary, then decrypts, in a single call. |
| 120 | +For high-volume decryption to an existing session, the plain `proteusDecrypt` call is slightly more efficient. |
| 121 | + |
| 122 | +== Error Handling |
| 123 | + |
| 124 | +Proteus errors are surfaced as a `ProteusError` with the following variants: |
| 125 | + |
| 126 | +`SessionNotFound`:: |
| 127 | + The requested session ID does not exist in the keystore or in-memory cache. |
| 128 | + |
| 129 | +`DuplicateMessage`:: |
| 130 | + The ciphertext has already been decrypted. |
| 131 | + The double-ratchet discards keys after use, so replaying a message is always an error. |
| 132 | + |
| 133 | +`RemoteIdentityChanged`:: |
| 134 | + The remote peer's identity key no longer matches what was recorded when the session was established. |
| 135 | + This typically indicates a device reset or, in the worst case, a key compromise. |
| 136 | + |
| 137 | +`Other { error_code }`:: |
| 138 | + A lower-level Proteus error that does not map to one of the above categories. |
| 139 | + The numeric `error_code` corresponds to the https://github.com/wireapp/proteus/blob/develop/crates/proteus-traits/src/lib.rs[proteus-traits error table]. |
0 commit comments