Migrating from v9.x to v10.0
This page covers breaking changes that are identical across all platforms. For platform-specific migration steps, see the sub-pages:
Credentials
CC10 introduces first-class Credential and CredentialRef types. Previously, credentials were implicit: mlsInit()
created a basic credential for each cipher suite automatically, and every operation that needed a credential selected
one by taking a (cipherSuite, credentialType) pair. In CC10 you construct credentials explicitly and refer to them by
CredentialRef. The (cipherSuite, credentialType) selector has been removed from every call site.
This makes credential operations much more flexible: whereas in the past CC would always implicitly choose the most recent credential of a given type and cipher suite, clients can simply choose the appropriate credential.
Creating and registering credentials
-
mlsInit()no longer creates any credential or key packages. After initializing MLS, create at least one credential yourself. See also MLS Initialization and Key Packages. -
Create a basic credential with the
Credential.basic(cipherSuite, clientId)static method. To obtain an X509 credential, use the acquisition flow described in X509 Credential Acquisition. ACredentiallives in memory and is independent of any client instance. -
Register the credential with
transactionContext.addCredential(credential). This persists it and returns aCredentialRef: a compact, stable handle you pass to the rest of the API in place of the old selector pair.- Credentials registered with a single client must be distinct on the
(credentialType, signatureScheme, creation timestamp)tuple, where the timestamp has one-second resolution. If you need several credentials sharing a type and signature scheme, wait one full second between registering each. We expect to relax this limitation in the future.
- Credentials registered with a single client must be distinct on the
-
On MLS initialization, previously stored credentials are loaded automatically. Enumerate them with
getCredentials(), or filter withfindCredentials(...), to recover theirCredentialRefs.
Passing credentials to operations
Replace the old (cipherSuite, credentialType) arguments with a CredentialRef:
| Operation | v9.x | v10.0 |
|---|---|---|
| Create a conversation | createConversation(id, creatorCredentialType, config) | createConversation(id, credentialRef, externalSender?) |
| Join by external commit | joinByExternalCommit(groupInfo, config, credentialType) | joinByExternalCommit(groupInfo, credentialRef) |
| Generate key packages | clientKeypackages(cipherSuite, credentialType, amount) | repeated generateKeyPackage(credentialRef) |
| Switch a conversation’s credential | e2eiRotate(conversationId) | setConversationCredential(conversationId, credentialRef) |
A conversation’s cipher suite is now derived from its credential, so createConversation() no longer accepts a separate
cipher suite.
To remove a credential, call removeCredential(credentialRef). This checks that the credential is not in use by any
conversation, removes every key package derived from it, and deletes it from both the working set and the keystore.
Public keys
A Credential carries a public key but exposes no method to export it. To read the public key, register the credential
with addCredential to obtain a CredentialRef, then call coreCrypto.publicKey(credentialRef) returns the raw public
key bytes. This replaces v9.x’s clientPublicKey(cipherSuite, credentialType).
There also exist other helpers to work with public keys:
credentialRef.publicKeyHash()returns the SHA256 hash of the public key.coreCrypto.exportCredentialPem(credentialRef)serializes the public key of a credential into PEM format
X509 Credential Acquisition
The enrollment API that previously drove the ACME and OIDC exchanges step-by-step from the client has been removed.
The whole ACME / DPoP / OIDC sequence is now hidden behind a single object, X509CredentialAcquisition, which the
client constructs once and then calls finalize() on to obtain a Credential. CoreCrypto reaches back into the client
only via well-defined hook points; the client no longer threads nonces, account responses, order requests, or challenge
payloads through its own code.
Where you previously called e2eiNewEnrollment, e2eiNewActivationEnrollment, e2eiNewRotateEnrollment,
directoryResponse, newAccountRequest/Response, newOrderRequest/Response, newAuthzRequest/Response,
createDpopToken, newDpopChallengeRequest/Response, newOidcChallengeRequest/Response,
checkOrderRequest/Response, finalizeRequest/Response, or certificateRequest — delete all of it. The new flow
replaces every one of those calls.
Acquiring an X509 credential
-
Implement
PkiEnvironmentHooks. CoreCrypto will call these hooks during acquisition:httpRequest— perform HTTP requests against the ACME server, CRL distributors, and similar.authenticate— drive the IdP authorization code flow with PKCE and return the resulting ID token.getBackendNonce— obtain a nonce from the Wire backend.fetchBackendAccessToken— exchange the DPoP token for a backend access token.
-
Create a
PkiEnvironmentwithPkiEnvironment.new(hooks, database)(TypeScript) orcreatePkiEnvironment(hooks, database)(Swift/Kotlin). TheDatabasecan be the same used by theCoreCryptoinstance or distinct; they use unrelated tables. That said, it is typically more convenient to use a single database. -
Build an
X509CredentialAcquisitionConfigurationdescribing the certificate you want:acmeUrl- the URL of the ACME servercipher_suite— must be one of the four with a JWS-compatible signature scheme: Ed25519, P256, P384, or P521. Other cipher suites will fail at construction.displayName,clientId,handle,domain, optionalteamvalidityPeriodSecs
-
Construct the acquisition:
X509CredentialAcquisition.new(pkiEnvironment, configuration). -
Call
await acquisition.finalize(). This drives the DPoP and OIDC challenges to completion, calling your hooks as needed, and returns the acquiredCredential. The acquisition can only be finalized once; a second call throws. -
Attach the credential to the client with
transactionContext.addCredential(credential), exactly as for a basic credential. This persists it to the internal database and returns aCredentialRef.
Pausing across the IdP redirect
The IdP authentication flow typically requires an external redirect, after which the app may have been suspended,
backgrounded, or restarted. To support this, the authenticate hook receives an acquisitionSnapshot: bytes parameter
capturing the acquisition state at the point the DPoP challenge has completed and OIDC is about to begin. Persist these
bytes to encrypted storage before launching the IdP flow.
When the app resumes and is ready to complete acquisition, reconstruct the acquisition from the snapshot with
X509CredentialAcquisition.fromBytes(pkiEnvironment, snapshot) and call finalize() on the reconstructed instance.
There is no client-visible serialization method on X509CredentialAcquisition itself; the snapshot bytes are delivered
to you exclusively through the authenticate hook’s acquisitionSnapshot parameter.
Checking for expiration and revocation
Call checkCredentials at least once every 24 hours to check all X509 credentials for expiration and revocation. It is
recommended to do this during an idle period, because HTTP requests are done to fetch new certificate revocation lists.
Key Packages
-
We removed
CoreCryptoContext.clientKeypackages(). To generate a desired amount of key packages, make repeated calls toCoreCryptoContext.generateKeyPackage(). -
We removed
CoreCryptoContext.clientValidKeypackagesCount(). To count remaining key packages, callCoreCryptoContext.getKeyPackages(), filter the results as desired, and count the remaining items. -
We aligned key package spelling to
KeyPackage:- renamed
Keypackage→KeyPackage - renamed
KeypackageRef→KeyPackageRef - renamed
generateKeypackage→generateKeyPackage - renamed
getKeypackages→getKeyPackages - renamed
removeKeypackage→removeKeyPackage - renamed
removeKeypackagesFor→removeKeyPackagesFor
- renamed
MLS Initialization
-
mlsInit()was decoupled from key package creation. To create key packages after initializing MLS, callCoreCryptoContext.generateKeyPackage()in a transaction. -
Removed
CoreCrypto.provideTransport(), addedtransportparameter toCoreCryptoContext.mlsInit(). Instead of providing transport separately from MLS initialization, provide it when callingmlsInit().
Validated Input Types
In several instances we have replaced parameters which used to be parsed at call time with exported types which are parsed at instantiation. This simplifies error propagation, because clients now receive parse errors directly at instantiation. It also simplifies call sites, because they can no longer return parse errors.
-
GroupInfo.new()andWelcome.new()are now fallible constructors. Previously, both accepted any byte sequence unconditionally. They now validate the input as a TLS-encoded MLS structure at construction time and throw if the bytes are malformed. -
We removed
GroupInfo.copyBytes()andWelcome.copyBytes(). The underlying types no longer store raw bytes and cannot be round-tripped back to a byte array. -
Added
Welcome.serialize(), which recovers the TLS-serialized bytes (replacing the removedcopyBytes()). It is fallible and throws if serialization fails. -
GroupInfoandWelcomeno longer support equality comparisons, hashing, or hex string display in generated bindings. -
createConversation()now takes a single optional, parsedExternalSenderobject instead of a list of raw external-sender byte arrays carried on the conversation configuration. Parse the external sender ahead of time with:ExternalSender.parseJwk(jwk)for the JWK form,ExternalSender.parsePublicKey(key, signatureScheme)for the legacy raw public-key form, orExternalSender.parse(key, signatureScheme)to try the JWK form first and fall back to the raw public-key form.
Parse errors are reported at parse time rather than during conversation creation. Call
externalSender.serialize()to recover the raw public-key bytes; these match theparsePublicKeyform and theExternalSenderKeyreturned bygetExternalSender(). Note thatExternalSender(the parsed input type) is distinct fromExternalSenderKey(the raw key type thatgetExternalSender()returns).
No More Buffering of Unmerged Changes While Decrypting
During decryption, CoreCrypto would previously automatically replay previously executed but unmerged (i.e., not yet accepted by the delivery service) operations. This behavior has changed: the responsibility of replaying any unmerged operations is delegated to the consumer.
MlsTransport Interface
-
Instead of returning an
MlsTransportResponseto communicate the reason why a message was rejected by the DS, throw anMlsTransportErrorinstead.MlsTransportResponsewas removed. -
Removed
sendMessagemethod fromMlsTransportinterface. This wasn’t well-documented and wasn’t being used in any case. We remove it for the purpose of making life easier for everyone.
Renaming “Ciphersuite” to “Cipher Suite”
We aligned cipher suite spelling to CipherSuite:
- renamed
Ciphersuite→CipherSuite - renamed
ciphersuiteFromU16→cipherSuiteFromU16 - renamed
ciphersuiteDefault→cipherSuiteDefault - renamed
conversationCiphersuite→conversationCipherSuite - renamed any parameters and fields
ciphersuite→cipherSuite
Client ID initialization
Previously, initialization was done via ClientId.new(bytes), where bytes was a string of a specific format with a user
id, device id, and domain. The new constructor takes care of this for you and ensures all client ids conform to this
fomat: ClientId.new(userId, deviceId, domain). userId must be an instance of the newly added type Uuid, and
deviceId a DeviceId.
Other Changes
-
CoreCrypto.e2eiIsEnvSetup()can’t throw anymore and will always return a boolean. -
removed
.proteusFingerprintPrekeybundle()and.proteusLastResortPrekeyId()fromCoreCryptoContext. Both are available as static methods onCoreCrypto. -
removed
CoreCryptoContext.proteusReloadSessions(). Proteus sessions are now loaded on demand by the new per-session cache, so explicit reloads are no longer needed. Delete any call sites. -
GroupInfoBundle.payloadnow contains a byte array instead of a class instance. -
The
updateDatabaseKeyfunction has been moved; it is now a static methodDatabase.updateKey. -
removed
CoreCryptoContext.markConversationAsChildOf(). No client should actually be using this function and all existing references to it should be removed. -
The duplicate signature error when adding members to a conversation now contain debug information about which members had duplicate signatures.
-
Removed
WelcomeBundletype that was returned fromprocessWelcomeMessage()andjoinByExternalCommit(). They return aConversationIdnow only. -
Removed previously deprecated field
hasEpochChangedfromDecryptedMessage. Use theEpochObserverinterface.