Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

CoreCrypto

CoreCrypto is a simplified abstraction over the MLS and Proteus cryptographic protocols for end-to-end encrypted communication. In addition, it provides end-to-end identity (E2EI) support via X509 credentials.

This book describes both core concepts and some platform-specific implementation notes. For detailed API references for a given platform and release, see https://wireapp.github.io/core-crypto.

Proteus

Proteus is Wire’s implementation of the Signal Protocol: a double-ratchet scheme providing forward secrecy and break-in recovery for one-to-one encrypted sessions.

Proteus is intrinsically a direct protocol: every message is individually encrypted for each recipient. Groups in Proteus are a fiction managed collaboratively between the backend and the client; they do not exist at the protocol level. Encrypting a message for a group of n clients requires sending n individually encrypted copies. 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. This O(n²) overhead is manageable for small groups, but becomes expensive as they scale, and it was the primary motivation for adopting MLS.

Proteus remains supported for backwards compatibility, but has not received direct development work in some time. Its implementation 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.

Initialization

Before any Proteus operation, the Proteus subsystem must be explicitly initialized within a transaction:

await ctx.proteusInit()
try await ctx.proteusInit()
ctx.proteusInit()

This loads the device’s Proteus identity from the keystore, creating it if it does not yet exist. All other Proteus methods will return an error if called before proteusInit() has succeeded.

Identity and Fingerprints

A Proteus identity is a long-lived Ed25519 keypair tied to the device. CoreCrypto exposes several fingerprint accessors, each returning a hex string:

proteusFingerprint() — The local device’s public key. Remains stable for the lifetime of the keystore.

proteusFingerprintLocal(sessionId) — The local public key as seen from within a specific session. Equivalent to proteusFingerprint() but scoped to a session for API symmetry.

proteusFingerprintRemote(sessionId) — The remote peer’s public key within a session. Can be used to verify the peer’s identity out-of-band.

proteusFingerprintPrekeybundle(prekey) — Extracts the public key fingerprint from a serialized prekey bundle without opening a session. Useful for verifying a peer’s identity before establishing a session.

Prekeys

Prekeys are the mechanism that allows a sender to open a Proteus session with a recipient who is offline. 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.

CoreCrypto provides two ways to generate prekeys:

proteusNewPrekey(id) — Generates a prekey with an explicit numeric ID (a u16). Use this when your app manages the prekey ID space itself.

proteusNewPrekeyAuto() — Generates a prekey with an automatically incremented ID and returns both the ID and the serialized bundle. Prefer this unless you have a specific reason to control IDs manually.

Both return a CBOR-serialized prekey bundle ready to upload to the delivery service.

The Last Resort Prekey

Proteus reserves prekey ID 65535 (u16::MAX) as a last resort prekey. 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.

const prekey = await ctx.proteusLastResortPrekey()
let prekey = try await ctx.proteusLastResortPrekey()
val prekey = ctx.proteusLastResortPrekey()

The constant proteusLastResortPrekeyId() returns 65535 and can be used to exclude this ID when generating ordinary prekeys.

Sessions

A Proteus session represents an established encrypted channel with one remote client, identified by a caller-supplied sessionId string. Session state is cached in memory and persisted to the keystore.

There are two ways to establish a new session, depending on which side initiates:

proteusSessionFromPrekey(sessionId, prekey) — Used by the sender to initiate a session. Takes the remote client’s serialized prekey bundle (fetched from the delivery service) and creates a local session ready to encrypt.

proteusSessionFromMessage(sessionId, envelope) — Used by the recipient upon receiving the first message. Decrypts the initial envelope, establishes the session, and returns the plaintext in a single step.

Once established, a session can be checked for existence with proteusSessionExists(sessionId) and explicitly removed with proteusSessionDelete(sessionId). Manual saves via proteusSessionSave(sessionId) are available but not normally required — sessions are persisted automatically when encrypting or decrypting.

Encrypting and Decrypting

With a session established, encryption and decryption are straightforward:

const ciphertext = await ctx.proteusEncrypt(sessionId, plaintext)
const plaintext  = await ctx.proteusDecrypt(sessionId, ciphertext)
let ciphertext = try await ctx.proteusEncrypt(sessionId: sessionId, plaintext: plaintext)
let plaintext  = try await ctx.proteusDecrypt(sessionId: sessionId, ciphertext: ciphertext)
val ciphertext = ctx.proteusEncrypt(sessionId, plaintext)
val plaintext  = ctx.proteusDecrypt(sessionId, ciphertext)

Batched Encryption

Because a group message in Proteus requires one encrypted copy per recipient client, CoreCrypto provides a batched variant to reduce FFI round-trips:

const map = await ctx.proteusEncryptBatched(sessionIds, plaintext)
// map: { sessionId -> ciphertext }
let map = try await ctx.proteusEncryptBatched(sessions: sessionIds, plaintext: plaintext)
// map: { sessionId -> ciphertext }
val map = ctx.proteusEncryptBatched(sessionIds, plaintext)
// map: { sessionId -> ciphertext }

This is more efficient than calling proteusEncrypt in a loop and should be preferred whenever sending to multiple sessions simultaneously.

Safe Decrypt

proteusDecryptSafe(sessionId, ciphertext) is a convenience wrapper that handles the common case where you do not know in advance whether the session already exists. It opens the session from the message envelope if necessary, then decrypts, in a single call. For high-volume decryption to an existing session, the plain proteusDecrypt call is slightly more efficient.

Error Handling

Proteus errors are surfaced as a ProteusError with the following variants:

SessionNotFound — The requested session ID does not exist in the keystore or in-memory cache.

DuplicateMessage — The ciphertext has already been decrypted. The double-ratchet discards keys after use, so replaying a message is always an error.

RemoteIdentityChanged — The remote peer’s identity key no longer matches what was recorded when the session was established. This typically indicates a device reset or, in the worst case, a key compromise.

Other { error_code } — A lower-level Proteus error that does not map to one of the above categories. The numeric error_code corresponds to the proteus-traits error table.

MLS

RFC 9420, the Messaging Layer Security standard, solves the group-scaling problem that limits Proteus. Rather than maintaining an independent session per client pair, MLS establishes a single shared group secret from which all members independently derive the same encryption keys. Membership changes are managed through a ratchet tree — a binary tree structure that lets any member compute the new shared secret after an add or remove using only O(log n) key operations rather than O(n²) messages.

The fundamental unit of time in an MLS group is the epoch. Each epoch has a distinct shared secret; when membership changes (or when keys are rotated for other reasons), the group advances to a new epoch and new keys are computed. The two-phase proposal / commit model makes this safe in asynchronous networks: proposals accumulate from multiple senders, and a single commit bundles them into an atomic epoch transition.

Cryptographic Properties

Security Guarantees

MLS provides two complementary security properties that together defend against both historical and future exposure of messages.

Forward Secrecy

Because each epoch derives a distinct shared secret and prior epoch secrets are deleted after use, compromise of the current epoch does not expose messages from any prior epoch. An attacker who obtains today’s keys learns nothing about yesterday’s conversations.

Post-Compromise Security

Forward secrecy protects the past; post-compromise security (PCS) protects the future. If an attacker obtains a member’s key material — for example by compromising a device — they gain access to the current epoch. MLS limits this exposure: once any member contributes a commit that includes a fresh path secret (an Update proposal, or any commit authored by the compromised member), new randomness is injected into the key schedule. The attacker’s copy of the old keys cannot be used to derive the new epoch’s secrets, so the group heals automatically as conversation continues.

Together, these guarantees mean that an MLS group is resilient over time: past messages remain private even after compromise, and future messages recover privacy as soon as keys are rotated.

Efficiency

While changes in the group structure require O(log n) key operations, application messages are more efficient. Because each group has a shared group state governing its current shared secret, application messages encrypt in O(1). A significant improvement over Proteus!

Initialization

Before any MLS operation, MLS must be explicitly initialized within a transaction:

await ctx.mlsInit(clientId, transport)
try await ctx.mlsInit(clientId: clientId, transport: transport)
ctx.mlsInit(clientId, transport)

Client Identity

ClientId is an opaque byte array. CoreCrypto treats it as an identifier and does not inspect or validate its contents. Wire’s convention is to encode the client ID as a UTF-8 string of the form userId:clientId@domain — for example alice_user_id:device1@wire.com — but this is a Wire convention, not a CoreCrypto requirement.

MlsTransport

CoreCrypto intentionally abstains from communication with the Wire backend; instead, the caller provides an implementation of this interface that CoreCrypto calls on demand.

sendCommitBundle(commitBundle) — Called whenever a commit is produced — for example when creating a conversation, adding or removing members, or rotating keys. The commitBundle contains the commit message, a welcome message for any newly added members, and the updated group info. All three should be forwarded to the delivery service in a single request.

The implementor of sendCommitBundle may throw an MlsTransportError indicating the reason for a rejected message.

prepareForTransport(historySecret) — Called before a history secret is transmitted to a new history client. The implementation should package the secret in the application’s transport format (e.g., JSON or Protobuf) and return the serialized bytes. CoreCrypto will then encrypt and send those bytes as an application message.

Observers

CoreCrypto provides two optional observer interfaces that allow the application to react to significant MLS events without polling. Both are registered on the CoreCrypto instance directly (not on a transaction) and persist for the lifetime of the session.

EpochObserver

registerEpochObserver(observer) registers a callback that fires whenever a conversation advances to a new epoch — that is, whenever a commit is merged. The callback receives the conversationId and the new epoch number.

Note

The epochChanged callback must return promptly. CoreCrypto holds internal locks while dispatching it. If your observer needs to do significant work, dispatch it to a background task rather than doing it inline.

HistoryObserver

registerHistoryObserver(observer) registers a callback that fires when a new history client has been created and accepted by the delivery service. The callback receives the conversationId and a HistorySecret — an opaque bundle of key material that the application should forward to the history client so it can decrypt past messages.

The history client feature is Wire’s mechanism for allowing newly added devices to read conversation history; the HistoryObserver is the hook through which the application participates in that handoff.

Credentials and Key Packages

Before a device can join or create MLS groups, it needs a credential — a proof of identity that other group members can verify. CoreCrypto supports two credential types:

Basic — A locally generated keypair with no external attestation. Suitable whenever end-to-end identity verification is not required. This is the default.

X509 — A certificate chain obtained through the E2EI enrollment process, binding the device’s MLS identity to a verified Wire user identity. Required for deployments with end-to-end identity enabled.

Credentials are created and stored via addCredential() on the transaction, which returns a CredentialRef — a short handle used to refer to the credential in subsequent operations.

Key packages — MLS-level advertisements of a device’s identity and capabilities. Other devices fetch a target device’s key package from the delivery service when adding it to a group. Generate them with generateKeypackage(credentialRef) inside a transaction.

Cipher Suites

An MLS cipher suite is a composite designation which specifies the KEM, AEAD, hash, and signature algorithms. CoreCrypto formally supports any of the cipher suites specified in the RFC, but is primarily tested on cipher suite 1 (MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519).

The cipher suite is fixed per conversation at the conversation’s creation.

The Database

The Database is CoreCrypto’s keystore: the persistent store for all key material and cryptographic state, as well as some non-cryptographic internal data. It is opened before constructing a CoreCrypto instance and passed in as an argument, making the storage layer an explicit dependency rather than something CoreCrypto manages internally.

What the Database Stores

The database holds all state that must survive process restarts:

  • MLS group state — the serialized ratchet tree and secrets for every conversation the device participates in
  • Credentials and key packages — the device’s identity material and the key packages it has published to the delivery service
  • Pending messages — application messages and commits that arrived during an epoch transition and are waiting to be merged
  • Proteus identity and sessions — the device’s long-lived Proteus identity keypair and the state of every active Proteus session
  • Proteus prekeys — including the last-resort prekey
  • E2EI state — data about the ACME certificate authority, various certificates and CRLs, refresh tokens, and the like

All entries are encrypted at rest using the DatabaseKey provided at open time.

Backing Storage

On non-browser platforms the database is backed by SQLCipher, an encrypted SQLite variant. On the browser, the database sits atop IndexedDB and uses item-level encryption managed by CoreCrypto. In both cases, data is encrypted at rest.

Future work on the database is intended to unify the backing storages, such that CoreCrypto will internally see an encrypted SQLite connection for all platforms. That work will be part of CC 11, but not CC 10. This change should be invisible to clients.

The Database Key

The DatabaseKey is a 32-byte symmetric key used to encrypt all data at rest. Construct it from a byte array:

const key = DatabaseKey.new(bytes)  // bytes must be exactly 32 bytes
let key = DatabaseKey(bytes: bytes)  // bytes must be exactly 32 bytes
val key = DatabaseKey(bytes)  // bytes must be exactly 32 bytes

CoreCrypto does not generate or store the key — that is entirely the client’s responsibility. The key should be kept in the platform’s secure credential storage.

Note

The database key is never written to disk by CoreCrypto. If the key is lost, the database contents are unrecoverable.

Opening the Database

All platforms define an async Database.open static method accepting a location and key. On platforms with an OS, the location is a filesystem path. On WASM, the location is the IndexedDB store name.

All platforms define an async static method to open a transient database in memory. On WASM, this is Database.inMemory. For Kotlin and Swift, this is an overload of Database.open.

Key Rotation

The database can be re-encrypted with a new key without closing and reopening it:

await db.updateKey(newKey)
try await db.updateKey(key: newKey)
db.updateKey(newKey)

This re-encrypts all stored data in place.

Lifetime and Closing

On browser platforms, explicitly close the database when tearing down:

await db.close()

Single-Transaction Constraint

Only one transaction may write to the database at a time. If a transaction is requested while another transaction is in flight, the call will block until the first transaction concludes.

For more detail, see Transactions → Concurrency.

Exporting a Copy

On native platforms, a fully vacuumed copy of the database can be written to a new file:

try await exportDatabaseCopy(database: db, destinationPath: destinationPath)
exportDatabaseCopy(db, destinationPath)

The copy is encrypted with the same key as the source. This is useful for backup or for migrating the database to a new location. This function is not available on browser platforms.

The Client

In MLS terminology a client is a single device acting on behalf of a user — each phone, desktop, or web session that participates in MLS groups is a distinct client with its own identity and key material. CoreCrypto maps this concept to the CoreCrypto object itself. One CoreCrypto instance, backed by one Database, represents one MLS client.

Note

The Rust library internally calls this type Session to avoid ambiguity — “client” means something different at the Wire application layer (where it refers to a registered device in the Wire backend sense) and at the MLS protocol layer. The name Session does not appear in platform bindings; from Swift, Kotlin, and TypeScript, the type is always CoreCrypto.

Sharing a CoreCrypto Instance

CoreCrypto is cheap to clone. All internal state — the database connection, the MLS session, the Proteus state — is reference-counted, so cloning the object creates a new handle to the same shared state rather than copying anything. This means CoreCrypto can be freely passed to background tasks or across threads without wrapping it in an additional mutex or reference type.

Read-Only Operations

Most MLS work happens inside a transaction (see Transactions and the TransactionContext), but a handful of read-only operations are available directly on CoreCrypto without opening one. The set of read-only operations available on CoreCrypto is intentionally kept narrow.

Any operation that might change state — creating or deleting a conversation, adding members, encrypting or decrypting a message — requires a transaction.

Lifecycle

A CoreCrypto instance is typically created once at application startup and kept alive for the life of the process. The initialization sequence is:

const db = await Database.open(path, key);       // open the keystore
const cc = CoreCrypto.new(db);                   // construct CoreCrypto
await cc.transaction(async (ctx) => {
    await ctx.mlsInit(clientId, transport);      // initialize the MLS session
});
let db = try await Database.open(location: path, key: key)  // open the keystore
let cc = CoreCrypto(database: db)                            // construct CoreCrypto
try await cc.transaction { ctx in
    try await ctx.mlsInit(clientId: clientId, transport: transport)  // initialize the MLS session
}
val db = Database.open(path, key)  // open the keystore
val cc = CoreCrypto(db)            // construct CoreCrypto
cc.transaction { ctx ->
    ctx.mlsInit(clientId, transport)  // initialize the MLS session
}

After mlsInit() is committed, the instance is ready to use.

Multiple Instances

Only one CoreCrypto instance (and its clones) should be active against a given database file at a time. The iOS client has a file lock which prevents two instances of the CoreCrypto from interfering with each other. On other clients opening the same database from two independent CoreCrypto instances — whether in the same process or different processes — is not supported and will produce undefined behavior.

Transactions and the TransactionContext

Every operation in CoreCrypto that can potentially mutate state must happen inside a transaction. Transactions provide an atomicity guarantee: either all of the operations in a transaction are persisted together, or none of them are. This prevents the keystore from being left in an inconsistent state if an operation fails partway through — for example, if a commit is produced but the delivery service rejects it before the local group state is updated.

Opening a Transaction

Transactions are opened with a callback pattern. Call transaction() on the CoreCrypto instance and pass an async function; CoreCrypto calls that function with a CoreCryptoContext (the transaction handle), and commits or rolls back the transaction automatically based on whether the function succeeds or throws:

await cc.transaction(async (ctx) => {
    await ctx.mlsInit(clientId, transport);
    // more operations...
});
try await cc.transaction { ctx in
    try await ctx.mlsInit(clientId: clientId, transport: transport)
    // more operations...
}
cc.transaction { ctx ->
    ctx.mlsInit(clientId, transport)
    // more operations...
}

If the callback completes without throwing, the transaction is committed — all buffered operations are written to the keystore in a single atomic database transaction. If the callback throws, the transaction is rolled back and no changes are persisted.

There is no explicit finish() or abort() to call; both are handled automatically by transaction().

The CoreCryptoContext

The ctx parameter is the CoreCryptoContext: the object through which all mutating operations are performed. It is only valid for the lifetime of the callback — using it after the callback returns will produce an InvalidTransactionContext error.

All operations are buffered in memory inside the context. Nothing is written to the database until the callback returns successfully. This means reads within the same transaction will observe the in-memory state, not the on-disk state — if you create a conversation and immediately query it within the same transaction, the query will find it.

Concurrency

Only one transaction may be active at a time. Calling cc.transaction() while another transaction is already running will block until the first transaction finishes. This is a consequence of the single-writer constraint in the keystore; see the Database chapter for more detail.

The practical implication is that large batches of work — for example, decrypting a backlog of incoming messages — will block any concurrent attempt to encrypt and send a new message. During an initial sync, this is desirable, because there are notable performance improvements to performing a large number of operations in a single transaction. On the other hand, once a client is fully synced and active, the opposite advice applies: because they are blocking, it is advisable to structure transactions to be as short-lived as possible.

Error Handling

If an operation inside a transaction fails, it is usually best to let the error propagate out of the callback. transaction() will catch it, roll back automatically, and rethrow — the caller can then handle the error without worrying about cleanup:

try {
    await cc.transaction(async (ctx) => {
        await ctx.decryptMessage(conversationId, incomingCiphertext);
        await ctx.encryptMessage(conversationId, outgoingPlaintext);
    });
} catch (err) {
    // The transaction was rolled back. No state was changed.
}
do {
    try await cc.transaction { ctx in
        try await ctx.decryptMessage(conversationId: conversationId, payload: incomingCiphertext)
        try await ctx.encryptMessage(conversationId: conversationId, message: outgoingPlaintext)
    }
} catch {
    // The transaction was rolled back. No state was changed.
}
try {
    cc.transaction { ctx ->
        ctx.decryptMessage(conversationId, incomingCiphertext)
        ctx.encryptMessage(conversationId, outgoingPlaintext)
    }
} catch (e: Exception) {
    // The transaction was rolled back. No state was changed.
}

Retry After Delivery Failure

Some operations — those that produce a commit — invoke MlsTransport.sendCommitBundle() inside the transaction. If the delivery service returns Retry, CoreCrypto propagates this as an MlsError and the transaction is rolled back. The caller should fetch and process all pending incoming messages and then retry the operation in a new transaction.

Reuse After Completion

Once transaction() returns (whether by success or failure), the CoreCryptoContext passed to the callback is permanently invalidated. Any attempt to call methods on a finished context returns InvalidTransactionContext. Always perform all work within the callback scope.

Arbitrary Data Storage

The context provides two methods for storing a single blob of arbitrary bytes in the keystore, associated with the device:

await ctx.setData(bytes)
const bytes = await ctx.getData()
try await ctx.setData(data: bytes)
let bytes = try await ctx.getData()
ctx.setData(bytes)
val bytes = ctx.getData()

These were implemented for the purpose of checkpointing during initial sync / batch decryption, but are not limited to that use.

What’s New in CC 10

New APIs

With CC10 we introduce multiple new types that provide their own new API.

Database API

  • Database.getLocation() allows getting the location of a persistent database instance. It returns null if the database is in-memory.

  • added Database.open(location: String, key: DatabaseKey) as a static method to construct a persistent database instance, and Database.open(key: DatabaseKey) / Database.inMemory(key: DatabaseKey) to construct an in-memory database instance.

  • Typescript Only: Added Database.close() and removed CoreCrypto.close(). A Database should be closed if it is not used anymore. Closing a database makes any PkiEnvironment or CoreCrypto instance unusable. Calls to these instances will return a CoreCryptoError.Other. CoreCrypto instances do not need to be closed anymore.

  • It is now safer to close a Database: instead of depending on a unique reference to the instance, it will just invalidate all other references to that instance.

PKI Environment API

  • Added PkiEnvironment constructed via
    • PkiEnvironment(database: Database, hooks: PkiEnvironmentHooks) (swift)
    • PkiEnvironment.new(database: Database, hooks: PkiEnvironmentHooks) (kotlin)
    • PkiEnvironment.create(database: Database, hooks: PkiEnvironmentHooks) (ts)
  • Added PkiEnvironmentHooks interface which has to be implemented by a client and will be used by CoreCrypto during e2ei flow
  • Added CoreCrypto.setPkiEnvironment() to set a PkiEnvironment on a CoreCrypto instance
  • Added CoreCrypto.getPkiEnvironment() to get the PkiEnvironment of a CoreCrypto instance

Credential API

Credential is now a first-class type representing a cryptographic identity. A credential can be created at any time, lives in memory, and is independent of any client instance or storage. There are two variants: basic credentials, created with the Credential.basic static method, and X509 credentials, obtained through the acquisition flow described in the X509 Credential Acquisition section of the migration guide.

Initializing an MLS client no longer automatically generates any credentials; instead, any previously stored credentials are loaded automatically on MLS init. To put a freshly created credential to use, register it with addCredential on a transaction context, which stores it and adds it to the working set. This explicit model is considerably more flexible than before: rather than CoreCrypto implicitly selecting the most recent credential of a given type and cipher suite, clients now choose exactly which credential each operation should use.

Registering a credential returns a CredentialRef: a compact, stable handle that uniquely identifies a single stored credential without shuttling the full credential data back and forth across the FFI boundary. A CredentialRef carries basic metadata about the credential it points to — client id, credential type, signature scheme, cipher suite, earliest validity, and the hash of its public key — and is the value you pass throughout the rest of the credential API.

Two transaction-context methods recover these references for credentials already known to a client: getCredentials returns a CredentialRef for every credential, and findCredentials does the same while efficiently filtering by criteria you specify. To delete a credential, pass its CredentialRef to removeCredential; this verifies 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.

Note

CC v10.0 introduces lots of changes. We provide a migration guide.

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

  1. 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.

  2. 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. A Credential lives in memory and is independent of any client instance.

  3. Register the credential with transactionContext.addCredential(credential). This persists it and returns a CredentialRef: 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.
  4. On MLS initialization, previously stored credentials are loaded automatically. Enumerate them with getCredentials(), or filter with findCredentials(...), to recover their CredentialRefs.

Passing credentials to operations

Replace the old (cipherSuite, credentialType) arguments with a CredentialRef:

Operationv9.xv10.0
Create a conversationcreateConversation(id, creatorCredentialType, config)createConversation(id, credentialRef, externalSender?)
Join by external commitjoinByExternalCommit(groupInfo, config, credentialType)joinByExternalCommit(groupInfo, credentialRef)
Generate key packagesclientKeypackages(cipherSuite, credentialType, amount)repeated generateKeyPackage(credentialRef)
Switch a conversation’s credentiale2eiRotate(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

  1. 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.
  2. Create a PkiEnvironment with PkiEnvironment.new(hooks, database) (TypeScript) or createPkiEnvironment(hooks, database) (Swift/Kotlin). The Database can be the same used by the CoreCrypto instance or distinct; they use unrelated tables. That said, it is typically more convenient to use a single database.

  3. Build an X509CredentialAcquisitionConfiguration describing the certificate you want:

    • acmeUrl - the URL of the ACME server
    • cipher_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, optional team
    • validityPeriodSecs
  4. Construct the acquisition: X509CredentialAcquisition.new(pkiEnvironment, configuration).

  5. Call await acquisition.finalize(). This drives the DPoP and OIDC challenges to completion, calling your hooks as needed, and returns the acquired Credential. The acquisition can only be finalized once; a second call throws.

  6. 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 a CredentialRef.

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

  1. We removed CoreCryptoContext.clientKeypackages(). To generate a desired amount of key packages, make repeated calls to CoreCryptoContext.generateKeyPackage().

  2. We removed CoreCryptoContext.clientValidKeypackagesCount(). To count remaining key packages, call CoreCryptoContext.getKeyPackages(), filter the results as desired, and count the remaining items.

  3. We aligned key package spelling to KeyPackage:

    • renamed KeypackageKeyPackage
    • renamed KeypackageRefKeyPackageRef
    • renamed generateKeypackagegenerateKeyPackage
    • renamed getKeypackagesgetKeyPackages
    • renamed removeKeypackageremoveKeyPackage
    • renamed removeKeypackagesForremoveKeyPackagesFor

MLS Initialization

  1. mlsInit() was decoupled from key package creation. To create key packages after initializing MLS, call CoreCryptoContext.generateKeyPackage() in a transaction.

  2. Removed CoreCrypto.provideTransport(), added transport parameter to CoreCryptoContext.mlsInit(). Instead of providing transport separately from MLS initialization, provide it when calling mlsInit().

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.

  1. GroupInfo.new() and Welcome.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.

  2. We removed GroupInfo.copyBytes() and Welcome.copyBytes(). The underlying types no longer store raw bytes and cannot be round-tripped back to a byte array.

  3. Added Welcome.serialize(), which recovers the TLS-serialized bytes (replacing the removed copyBytes()). It is fallible and throws if serialization fails.

  4. GroupInfo and Welcome no longer support equality comparisons, hashing, or hex string display in generated bindings.

  5. createConversation() now takes a single optional, parsed ExternalSender object 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, or
    • ExternalSender.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 the parsePublicKey form and the ExternalSenderKey returned by getExternalSender(). Note that ExternalSender (the parsed input type) is distinct from ExternalSenderKey (the raw key type that getExternalSender() 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

  1. Instead of returning an MlsTransportResponse to communicate the reason why a message was rejected by the DS, throw an MlsTransportError instead. MlsTransportResponse was removed.

  2. Removed sendMessage method from MlsTransport interface. 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 CiphersuiteCipherSuite
  • renamed ciphersuiteFromU16cipherSuiteFromU16
  • renamed ciphersuiteDefaultcipherSuiteDefault
  • renamed conversationCiphersuiteconversationCipherSuite
  • renamed any parameters and fields ciphersuitecipherSuite

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

  1. CoreCrypto.e2eiIsEnvSetup() can’t throw anymore and will always return a boolean.

  2. removed .proteusFingerprintPrekeybundle() and .proteusLastResortPrekeyId() from CoreCryptoContext. Both are available as static methods on CoreCrypto.

  3. 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.

  4. GroupInfoBundle.payload now contains a byte array instead of a class instance.

  5. The updateDatabaseKey function has been moved; it is now a static method Database.updateKey.

  6. removed CoreCryptoContext.markConversationAsChildOf(). No client should actually be using this function and all existing references to it should be removed.

  7. The duplicate signature error when adding members to a conversation now contain debug information about which members had duplicate signatures.

  8. Removed WelcomeBundle type that was returned from processWelcomeMessage() and joinByExternalCommit(). They return a ConversationId now only.

  9. Removed previously deprecated field hasEpochChanged from DecryptedMessage. Use the EpochObserver interface.

Migrating to CC 10: TypeScript

See the common migration guide for changes that apply to all platforms.

Browser Module Location

We want to prepare the ground for a native module, which runs on node or bun. Therefore, the browser module is now exported as @wireapp/core-crypto/browser. Update your imports:

// before
import { ... } from "@wireapp/core-crypto";

// after
import { ... } from "@wireapp/core-crypto/browser";

CoreCrypto Instantiation

  1. The CoreCrypto constructor now takes a Database instance instead of a DatabaseKey and a path. To instantiate a database, call openDatabase().

  2. Deferred init is now the only way to instantiate CoreCrypto. We replaced CoreCrypto.init(database: Database) with the static function CoreCrypto.new(database: Database). Instead of calling CoreCrypto.deferredInit(), call CoreCrypto.new(). As before, call mlsInit() in a transaction to initialize MLS.

Logging

  1. We removed CoreCrypto.setLogger(logger: CoreCryptoLogger, level: CoreCryptoLogLevel) and CoreCrypto.setMaxLogLevel(level: CoreCryptoLogLevel), as logging is configured globally and not tied to a CoreCrypto instance. To set the log level, use the free function setMaxLogLevel(level: CoreCryptoLogLevel).

  2. We renamed setLoggerOnly(logger: CoreCryptoLogger) to setLogger(logger: CoreCryptoLogger).

Errors

Error Structure

We adjusted the TypeScript error structure. Whenever matching on errors using version >= 9.1.0 type guards, update their usage as shown in the examples below.

Note

For more info, see the corresponding section of the ubrn docs

Extracting the abort reason given via throwing an MlsTransportError:

import { CoreCryptoError, MlsError } from "core-crypto";

try {
    // send a commit that is rejected by the DS
} catch (err) {
  if (CoreCryptoError.Mls.instanceOf(err) &&
      MlsError.MessageRejected.instanceOf(err.inner.mlsError)) {
    const rejectReason = err.inner.mlsError.inner.reason;
    // other things you want to do with this error...
  } else {
      // log error
  }
}

Optional: use switch to handle multiple errors in one go:

import { CoreCryptoError, MlsError, MlsError_Tags } from "core-crypto";

try {
    // send a commit that is rejected by the DS
} catch (err) {
    if (CoreCryptoError.Mls.instanceOf(err)) {
        switch (err.inner.mlsError.tag) {
            case MlsError_Tags.MessageRejected: {
                const rejectedReason = err.inner.mlsError.inner.reason;
                // other things you want to do with this error...
                break;
            }

            // handle other mls errors
        }
    }
}

Catch a proteus error:

import { CoreCryptoError, ProteusError } from "core-crypto";

try {
    // look for a proteus session that doesn't exist
} catch (err) {
  if (CoreCryptoError.Proteus.instanceOf(err) &&
      ProteusError.SessionNotFound.instanceOf(err.inner.exception)) {
     let message = err.inner.exception.message;
     // other things you want to do with this error...
  } else {
      // log error
  }
}

Proteus Error Codes

The proteusErrorCode field was removed from the root error type. There is a deterministic mapping from error code to error type. If you were using the error code, use the error type instead (see above). See the proteus-traits error table for the mapping.

Other

  1. CoreCryptoContext.generateKeyPackage() now returns a KeyPackage instance instead of an Uint8Array. If you need the underlying Uint8Array, call the serialize property on the KeyPackage.

  2. CustomConfiguration.keyRotationSpan now defines milliseconds instead of seconds.

  3. renamed CoreCrypto.reseedRng() to CoreCrypto.reseed()

  4. In versions 9.x, it was possible to instantiate wasm clients in a node-like runtime (without persistence, though). This is no longer supported. Use @wireapp/core-crypto/native instead for fully featured CoreCrypto with persistence.

Migrating to CC 10: Swift

See the common migration guide for changes that apply to all platforms.

CoreCrypto Instantiation

  1. The CoreCrypto constructor now takes a Database instance instead of a DatabaseKey and a path. To instantiate a database, call the Database.open() static method.

  2. Deferred init is now the only way to instantiate CoreCrypto. Instead of calling deferredInit(), call the CoreCrypto constructor. As before, call mlsInit() in a transaction to initialize MLS.

Higher-Level Newtypes

CoreCryptoContext.getExternalSender() now returns an ExternalSenderKey object instead of a byte array. To access the raw bytes, call externalSenderKey.copyBytes().

Logging

  1. We removed CoreCrypto.setLogger(logger: CoreCryptoLogger, level: CoreCryptoLogLevel) and CoreCrypto.setMaxLogLevel(level: CoreCryptoLogLevel), as logging is configured globally and not tied to a CoreCrypto instance. To set the log level, use the free function setMaxLogLevel(level: CoreCryptoLogLevel).

  2. We renamed setLoggerOnly(logger: CoreCryptoLogger) to setLogger(logger: CoreCryptoLogger).

Other

  1. Removed CoreCryptoFfi.reseedRng() and CoreCryptoFfi.randomBytes().

  2. DatabaseKey constructor parameter is now named bytes not key

  3. Removed the following static methods from CoreCrypto that were globally available:

    • version()
    • buildMetadata()

Migrating to CC 10: Kotlin

See the common migration guide for changes that apply to all platforms.

Renames and Moves

  1. CoreCryptoClient has been renamed to CoreCrypto.

  2. historyClient(historySecret: HistorySecret) has been moved into CoreCrypto Companion functions.

CoreCrypto Instantiation

  1. The CoreCrypto constructor now takes a Database instance instead of a DatabaseKey and a path. To instantiate a database, call the Database.open() static method.

  2. Deferred init is now the only way to instantiate CoreCrypto. Instead of calling deferredInit(), call the CoreCrypto constructor. As before, call mlsInit() in a transaction to initialize MLS.

Higher-Level Newtypes

CoreCryptoContext.getExternalSender() now returns an ExternalSenderKey object instead of a byte array. To access the raw bytes, call externalSenderKey.copyBytes().

Logging

  1. We removed level parameter from setLogger(logger: CoreCryptoLogger, level: CoreCryptoLogLevel). To set the log level, use the free function setMaxLogLevel(level: CoreCryptoLogLevel).
  2. We removed setLoggerOnly.

Other

  1. Removed CoreCryptoFfi.reseedRng() and CoreCryptoFfi.randomBytes().
  2. Removed String.toClientId(). Use the regular ClientId constructor to get a ClientId.

Release Notes

CoreCrypto 10

Unreleased

  • Reworked E2EI
  • New Database API
  • New PKI Environment API
  • New Credential API
  • TS-Native library (dropped the non-persistent wasm client support for non-browser contexts)
  • Reworked client id constructor to take typed data instead of a byte string

CoreCrypto 9

v9.3.4 - 2026-04-30

Fixes an issue that could cause epoch observer events to be emitted for epoch changes that would not (yet) actually be persisted to the CoreCrypto database. This is relevant if the CoreCrypto instance is used inside the event handler of the epoch observer (e.g., to update the exported secret). If you created a CoreCrypto transaction inside the handler and didn’t use the CoreCrypto instance directly, this fix is irrelevant.

v9.3.3 - 2026-03-31

  • no more errors when deleting a non-existent credential

v9.3.2 - 2026-03-18

  • serialize structs into camelCase

v9.3.1 - 2026-03-18

Fixes an enum representation bug on web.

v9.3.0 - 2026-02-20

Lidl compat: Ios and Android can now export a compacted version of the CC database, retaining its encryption. This is only relevant for Lidl builds and should be ignored by everyone else.

v9.2.1 - 2026-02-17

Upgrades openssl to version 3.5.5 (used on Android and iOS for encryption at rest).

v9.2.0 - 2026-02-05

Kotlin changes:

  • expose the enitre read-only API on the CoreCrypto type. This allows performing arbitrary read-only operations on data while a transaction is running (e.g., in an EpochObserver event).

  • Introduce KotlinInstant type

v9.1.3 - 2025-12-18

Upgrades the binding generator (uniffi 0.29.5) to include a crash fix for Android

v9.1.2 - 2025-11-05

This release fixes a bug in the TypeScript bindings where the commit delay could in some situations be undefined when receiving a proposal.

v9.1.1 - 2025-10-24

This release fixes the issue where libcore_crypto_ffi.so had segments aligned on 4k instead of 16k on Android platforms.

v9.1.0 - 2025-09-29

Note

Even though this is a minor version bump, it contains a breaking change. See below for more information.

  • added typescript structured errors
  • fix the message rejected reason not being propagated on web
  • improvements to logs when epochs advance

Features

  • Web: structural errors

    Example Usage

    Extract the abort reason given via an MlsTransportResponse

    try {
        // send a commit that is rejected by the DS
    } catch (err) {
      if(isMlsMessageRejectedError(err)) {
        const rejectReason = err.context.context.reason;
        // other things you want to do with this error...
      } else {
          // log error
      }
    }
    

    Extract the proteus error code

    try {
        // look for a proteus session that doesn't exist
    } catch (err) {
      if(isProteusSessionNotFoundError(err)) {
        const errorCode = err.context.context.errorCode;
        // other things you want to do with this error...
      } else {
          // log error
      }
    }
    

Bug Fixes

  • Web: fixed the abort reason of an MlsTransportResponse not being forwarded to rust.

Breaking Changes

  • proteusErrorCode field was removed from the root error type, you can get it from the nested context now (see above). Affected platforms: web

v9.0.1 - 2025-09-18

Breaking Changes

  • v9.0.0 had erroneously renamed migrateDatabaseKeyTypeToBytes to migrateDbKeyTypeToBytes. This has been fixed, and migrateDatabaseKeyTypeToBytes is usable again on all platforms.

    Affected platforms: Android

Bug Fixes

  • Kotlin documentation is now correctly generated and deployed.

v9.0.0 - 2025-09-16

  • we’re now tying the Kotlin wrapper more closely to the generated bindings which allows for greater velocity when making changes in code that affects our API - this causes most of the breaking changes in this release
  • removed cryptobox migration API
  • in Swift, added protection against concurrent access from multiple core crypto instances
  • added implicit obfuscation of sensitive data in logs
  • reworked the entire build system and CI

Note

In this release we include a fix for missing artifacts in our Web release. The faulty release process affects all 8.x versions. Therefore, instead of migrating from any version < 8.x to 8.x, directly migrate to this version.

Breaking Changes

  • Removed support for migrating CoreCrypto database to version 1.

    Affected platforms: Web

    Databases saved by CoreCrypto versions older than 2.0 cannot be migrated anymore.

  • Removed proteusCryptoboxMigrate.

    Affected platforms: all

    Support for Cryptobox migration has been removed.

    Migration: remove all calls to proteusCryptoboxMigrate.

  • Renamed CoreCryptoContext.proteusDecrypt to CoreCryptoContext.proteusDecryptSafe(...).

    Affected platforms: Android

    It used to be the case that the Kotlin bindings hid the actual behavior of proteusDecrypt by adding a higher-level behavior, trading away some efficiency for ease-of-use. With this change, we have exposed the low-level behavior of proteusDecrypt, enabling for more efficient uses when decrypting many proteus messages at once. The old higher-level behavior of proteusDecrypt is now exposed as proteusDecryptSafe.

    Migration: replace all calls to proteusDecrypt with calls to proteusDecryptSafe.

  • Eliminated wrapper class E2EIEnrollment in favor of generated class E2eiEnrollment.

    Affected platforms: Android

    We’ve brought the uniffi-generated code to very near parity with the older high-level bindings. The following breaking changes were necessary to eliminate the old binding class:

    Name changes:

    These methods have had their names changed. To migrate, simply rename all calls to these functions.

    • accountResponse -> newAccountResponse
    • authzResponse -> newAuthzResponse
    • dpopChallengeResponse -> newDpopChallengeResponse
    • contextOidcChallengeResponse -> newOidcChallengeResponse
  • Eliminated (hand-written) wrapper class CoreCryptoContext in favor of (uniffi-generated) class CoreCryptoContext.

    Affected platforms: Android

    We’ve brought the uniffi-generated code to very near parity with the older high-level bindings. The following breaking changes were necessar to eliminate the old binding class:

    Name changes:

    These methods have had their names changed. To migrate, simply rename all calls to these functions.

    • getPublicKey -> clientPublicKey
    • generateKeyPackages -> clientKeypackages
    • validKeyPackageCount -> clientValidKeypackagesCount
    • addMember -> addClientsToConversation
    • removeMember -> removeClientsFromConversation
    • members -> getClientIds
    • deriveAvsSecret -> exportSecretKey
    • proteusGetLocalFingerprint -> proteusFingerprint
    • proteusGetRemoteFingerprint -> proteusFingerprintRemote
    • proteusGetPrekeyFingerprint -> proteusFingerprintPrekeyBundle
    • proteusDoesSessionExist -> proteusSessionExists
    • proteusCreateSession -> proteusSessionFromPrekey
    • proteusDeleteSession -> proteusSessionDelete

    Parameter order changes:

    These methods have had the order of their parameters changed. To migrate, either name the arguments in the caller or reorder the parameters appropriately.

    • generateKeyPackages / clientKeypackages: amount is now the final parameter, not the first
    • joinByExternalCommit: credentialType and configuration have swapped positions
    • e2eiNewEnrollment: team now appears after handle and before expirySec
    • e2eiNewActivationEnrollment: team now appears after handle and before expirySec
    • e2eiNewRotateEnrollment: new param order: (displayName, handle, team, expirySec, ciphersuite)
    • proteusCreateSession / proteusSessionFromPrekey: params swapped
    • proteusDecrypt: params swapped
    • proteusEncrypt: params swapped

    Other Parameter changes:

    These methods have had the set of their parameters changed. To migrate, see instructions for each changed method.

    • createConversation: accepts (ConversationId, CredentialType, ConversationConfiguration). Conversation configuration must be constructed externally.

    Removed Methods:

    These methods no longer exist.

    • proteusNewPrekeys: similar to from.until(from + count).map { cc.proteusNewPrekey(it.toUShort()) }
    • proteusNewLastPrekey: similar to cc.proteusLastResortPrekey()
    • proteusEncryptWithPreKey: similar to:
      cc.proteusSessionFromPrekey(sessionId, preKey)
      val encryptedMessage = cc.proteusEncrypt(sessionId, message)
      cc.proteusSessionSave(sessionId)
      return encryptedMessage
      
  • Stopped duplicating generated code in kotlin bindings.

    Affected platforms: Android

    Hand-written wrappers have largely been removed. The following items have been renamed:

    • Ciphersuites.DEFAULT -> CIPHERSUITES_DEFAULT
    • Ciphersuite.DEFAULT -> CIPHERSUITE_DEFAULT
    • CredentialType.Basic -> CredentialType.BASIC
    • MLSGroupId -> ConversationId
    • MLSKeyPackage -> KeyPackage
    • DeviceStatus.Valid -> DeviceStatus.VALID
    • DeviceStatus.Expired -> DeviceStatus.EXPIRED
    • DeviceStatus.Revoked -> DeviceStatus.REVOKED
    • E2eiConversationState.Verified -> E2eiConversationState.VERIFIED
    • E2eiConversationState.NotVerified -> E2eiConversationState.NOT_VERIFIED
    • E2eiConversationState.NotEnabled -> E2eiConversationState.NOT_ENABLED
  • Changed exposed error type structures

    Affected platforms: iOS

    Migration

    • When pattern-matching the affected error types, add argument labels
    • When accessing inner error values, add field names

Features

  • In our Swift bindings we are now protecting against concurrent access from multiple core crypto instances.
  • In the decode tool we add support for listing members or identities present in a group info.

CoreCrypto 8

v8.0.3 - 2025-08-12

This is only relevant for Kotlin.

Fixes page size alignment for all supported linkers.

Adds ClientId.copyBytes().

Adds ClientId.toString().

Changes ClientId.value from ByteArray to the generated FFI type com.wire.crypto.uniffi.ClientId

v8.0.2 - 2025-07-23

This is only relevant for Kotlin.

Adds MLSKeyPackage.copyBytes().

v8.0.1 - 2025-07-23

This release is relevant only for Kotlin. It adds several pseudo-constructors and accessors for newtypes around byte vectors.

For other platforms, no relevant changes are included.

v8.0.0 - 2025-07-17

This release contains the complete API necessary for history sharing in conversations. We’ve improved the generated types in bindings to be more typesafe, and we’ve added the feature to rotate the key used for the core crypto database.

Breaking changes

  • Removed canClose(), and isLocked().

    Affected platforms: Web

    Migration: Only needed if you were relying on the output of canClose() before calling close():

    Call close() directly. Instead of handling the false case of canClose(), catch the error that close() may throw: Cannot close as multiple strong refs exist, then try again.

    The behavior of close() was adjusted, so that it waits for any running transaction to finish, instead of throwing an error.

  • Removed mlsInitWithClientId, mlsGenerateKeypairs, e2eiDumpPKIEnv, deleteKeypackages, getCredentialInUse

    Affected platforms: Web, Android, iOS

    Migration: not needed, no client is using these functions.

  • Changed the location of the Wasm bytecode file

    Affected platforms: Web

    The Wasm bytecode file, core-crypto-ffi_bg.wasm, has been moved to a subdirectory named autogenerated. While this is an internal change and should normally not be breaking, in reality it may break the Web client, which assumes the location of that internal file.

    Migration: update relevant paths in the Web client to point to the new location, under autogenerated, including in any calls to initWasmModule.

  • Added a parameter refreshToken to newOidcChallengeRequest

    Affected platforms: Web

    This fixes an inconsistency between Web and other platforms.

    Migration: pass the refresh token received from the identity provider when calling newOidcChallengeRequest.

  • Added a parameter context to newOidcChallengeResponse

    Affected platforms: Web

    Migration: pass the transaction context when calling newOidcChallengeResponse.

  • Added encryptedMessage field to CommitBundle

    Affected platforms: Web, Android, iOS

    This field is used to bundle encrypted history secrets with a commit that adds a new history client.

    Migration: update any pattern-matching or other code that depends on the structure of MlsCommitBundle to include the new field. Also, make sure to update your implementation of the MlsTransport protocol/interface to include this field in the payload sent to the Delivery Service.

  • ClientId is a newtype, not a bare byte array.

    Affected platforms: Web, Swift

    Migration: call new ClientId(id) to construct a ClientId, and id.copyBytes() to get a byte array out.

  • ClientId wrapper accepts a byte array, not a string.

    Affected platforms: Android

    Migration: call .toByteArray() on the input.

  • Ciphersuite is an exported public enum, not an integer

    Affected platforms: all

    Migration: use the relevant enum variant instead of an integer.

  • SecretKey, ExternalSenderKey, GroupInfo, ConversationId, KeyPackage, Welcome are now newtypes

    Affected items:

    • CoreCryptoContext.exportSecretKey (aka CoreCryptoContext.deriveAvsSecret) now returns a SecretKey
    • (kotlin) AvsSecret newtype removed in favor of SecretKey
    • CoreCryptoContext.getExternalSender now returns an ExternalSenderKey
    • ConversationConfiguration::external_senders now accepts ExternalSenderKeys
    • CoreCryptoContext.joinByExternalCommit now accepts a GroupInfo
    • GroupInfoBundle now contains a GroupInfo
    • Many CoreCryptoContext methods now accept a ConversationId newtype instead of a byte array
    • HistoryObserver and EpochObserver now produce ConversationId instances instead of byte arrays
    • CoreCryptoContext.clientKeypackages now produces KeyPackages
    • CoreCryptoContext.addClientsToConversation now accepts KeyPackages
    • CommitBundle now might contain a Welcome
    • CoreCryptoContext.processWelcomeMessage now accepts a Welcome

    Affected platforms: all

    Migration: call .copyBytes() on the newtype to get access to the raw byte vector. To construct the newtype from a byte array, just use the appropriate constructor.

    In the past, Android (but only Android) had newtypes in these instances; other clients needed to work with a raw byte vector. We’ve decided to expand the use of newtypes around byte vectors in the FFI interface. This has several benefits:

    • Increased consistency between client FFI libraries
    • Reduced thickness of the high-level FFI wrappers
    • In some cases, we can avoid bidirectional data transfers across the FFI boundary, and just move pointers around instead.
  • Removed PlaintextMessage, MlsMessage and SignaturePublicKey newtypes in favor of ByteArray

    Affected platforms: Android

    The Message newtypes were only used in CoreCryptoContext.encryptMessage and CoreCryptoContext.decryptMessage. SignaturePublicKey was used only for the return value of fun getPublicKey. The only usage we found was an immediate access of the byte vector.

    These types appear to provide no type safety benefits, instead only adding a bit of friction.

Features

  • Support Android environments with 16k page size
  • Added a module-level function updateDatabaseKey, to update the key of an existing CoreCrypto database
  • Support for history sharing which can be enabled by calling enableHistorySharing() and disabled again by calling disableHistorySharing().

CoreCrypto 7

v7.0.2 - 2025-07-07

Upgrade OpenMLS to fix a bug where the ratchet tree would sometimes become corrupt leading to broken MLS groups.

Bug Fixes

  • update openmls [WPB-18569] (7ca7ba7)

v7.0.1 - 2025-06-02

Bug Fixes

  • initWasm was being called with the wrong property field. (ca1706d)
  • allow registering epoch observer before calling mls_init (3f0605a)

v7.0.0 - 2025-05-21

Breaking changes

The typescript bindings no longer implicitly load the wasm module when importing the core crypto module. To replace this behaviour the async initWasmModule() function has been added, which must be called before any other core crypto function.

Features

  • remove top level await and expose async init method instead (ce6e566)
  • expose historyClient constructor to swift (22d98de)
  • expose historyClient constructor to kotlin (b16839d)
  • expose historyClient constructor to wasm (3c2531a)
  • expose history_client constructor publicly (c24fcc3)
  • restore history secrets (5643e87)
  • add fn history_client(HistorySecret) -> CoreCrypto (8b7b7d4)
  • add fn generate_history_secret (a998dec)

Bug Fixes

  • crypto-ffi: fix naming and attributes of WelcomeBundle fields on Wasm (1b2e88b)
  • prevent cancellations during transactions (Kotlin) (75217d5)
  • prevent cancellations during transactions (Kotlin) (ae621b7)

CoreCrypto 6

v6.0.1 - 2025-05-07

Bug Fixes

  • swift publishing CI action (d1030e1)

v6.0.0 - 2025-05-07

  • Changed the core crypto database key format, to enable validation of the same and ensure consistency between platforms
  • Added a function for each platform to migrate from the old to the new key type
  • Several more bug fixes, including prevention of the pending commit error

Breaking changes

  • Changed the core crypto database key format

    Affected platforms: Web, Android, iOS

    Migration: before instantiating this version of core crypto for the first time, call migrateDatabaseKeyTypeToBytes() with the appropiate arguments (old key and your new key) exactly once. Then, instantiate core crypto with the new key.

    Note: Make sure the new key is not based on a string, and provide full 256 bits of entropy.

    Note: Instantiating this version of core crypto will fail before you call the migration function.

v5.4.0 - 2025-05-14

Kotlin bindings only: transactions are now NonCancellable, as required by Uniffi. This prevents a category of bug where Kotlin thinks a transaction has been cancelled, while Rust thinks it is still running.

v5.3.0- 2025-04-29

Bug Fixes

  • re-throw inner error which cancelled the transaction (Swift) (0c282b2)

v5.2.0 - 2025-04-15

Bug Fixes

  • add registerEpochObserver to CoreCryptoProtocol (eadf388)
  • create an interface for ConversationConfiguration (b1e82bf)
  • swift publishing failing due to not running on latest macos runner (dcc1890)

v5.1.0 - 2025-04-03

Bug Fixes

  • broken swift bindings by publishing uniffi framework separately (2b950cc)
  • don’t refer to the internal uniffi EpochObserver type in the public API (7833300)
  • re-expose proteus_reload_session which removed by mistake (36f2b87)

v5.0.0 - 2025-03-21

New Swift bindings which are more ergonomic and allows for better testing by exposing the transaction context as a protocol.

New API for observing epoch changes through a callback API: registerEpochObserver. After adopting this API clients can remove their own epoch observers.

Breaking changes

  • New Swift bindings are replacing the old Swift bindings.

CoreCrypto 4

v4.2.3 - 2025-03-14

  • fix android publishing to maven central

v4.2.2 - 2025-03-14

  • fix publishing to maven central

v4.2.1 - 2025-03-14

  • expose proteusCryptoboxMigrate() [WPB-16549] (682b9fe)

v4.2.0 - 2025-02-28

  • The Android release once again bundles API docs.
  • The Kotlin bindings have received several API fixes in particular:
    • AcmeChallenge was missing the target property.
    • proteusGetPrekeyFingerprint was missing.
  • The Typescript bindings now correctly expose WireIdentity and X509Identity.
  • The code base has migrated to Rust 2024 edition.

v4.1.0 - 2025-02-07

  • Add the capability to handle the case where a proposal-referencing commit arrives before the proposals it references.

(Semi-) Breaking changes

  • For the case mentioned above, the corresponding error type BufferedCommit has been added.
    • Depending on the error model, this can be a breaking change.

v4.0.1 - 2025-02-05

  • support entity derive for tables with hex ids (0bd3676)

v4.0.0 - 2025-01-28

  • All errors crossing the FFI boundary are now logged.
  • An iOS client has been added to internal interop tests, which means we now test the entire FFI stack on iOS.
  • A new interface for MLS transport has been added, allowing for a much simpler and more robust CoreCrypto API.
  • Removal of a number of deprecated and unnecessary functions and types.
  • Completely reworked internal error handling, to allow for more precise errors.
  • A number of improvements to Kotlin and Javascript bindings, making the bindings more consistent.
  • The decode tool gained support for decoding MLS messages.

Breaking changes

  • Deprecated functions on the CoreCrypto type that were automatically creating transactions have been removed.

    Affected platforms: Web, Android, iOS

    Migration: replace calls to functions on CoreCrypto with calls to corresponding functions on CoreCryptoContext, which is created when you explicitly create a transaction. Transactions have to be explicitly created now.

  • The low-level uniffi-generated Kotlin bindings code is no longer publicly available. It should never have been used in application code directly.

    Affected platforms: Android

    Migration: make sure to use the Kotlin high-level API only.

  • The Wasm bytecode generated by wasm-bindgen is now imported directly when importing the corecrypto module. This makes sure that the Wasm module is immediately initialised, without any additional steps. There is no need for the client app to know or handle the path to Wasm bytecode file.

    Additionally, it is now possible to use the same CoreCrypto module in both browser and non-browser contexts.

    Affected platforms: Web

    Migration: drop any references to core-crypto-ffi_bg.wasm and do not set the wasmFilePath argument to the CoreCrypto.init function – it is no longer used. Additionally, make sure there is no special handling or separate hosting of the Wasm bytecode file. CoreCrypto release artifacts should be used without any changes.

  • Validation callbacks, as well as related error variants, have been removed.

    Affected platforms: Web, Android, iOS

    Migration: remove all implementations of authorize, userAuthorize and clientIsExistingGroupUser, as well as calls to CoreCrypto.setCallbacks.

  • The MlsTransport interface has been added. This is another milestone in the effort to simplify the public API and make it more robust.

    All client applications have to provide an implementation of the new interface, which comprises only two functions, sendMessage and sendCommitBundle.

    Affected platforms: Web, Android, iOS

    Migration: implement the MlsTransport interface and call CoreCrypto.provideTransport to make CoreCrypto use your implementation.

  • Functions CoreCrypto.wipe and CoreCrypto.unload have been removed. They were not providing any value.

    Affected platforms: Web, Android, iOS

    Migration: drop calls to wipe and unload. Client applications wishing to make their keys and conversations inaccessible should remove the CoreCrypto database explicitly.

  • The function CoreCrypto.proteusLastErrorCode has been removed. We now have Proteus error codes attached to errors that are emitted by CoreCrypto.

    Affected platforms: Web, Android, iOS

    Migration: drop calls to proteusLastErrorCode and instead check the returned error object.

  • The functions CoreCrypto.buildMetadata and CoreCrypto.version have been moved to the module level. It is no longer required to create a CoreCrypto instance to call them.

    Affected platforms: Web

    Migration: use module-level buildMetadata and version functions.

  • Several changes have been made to the E2EI API.

    The function CoreCrypto.e2eiRotateAll has been removed. Client applications should instead go over each conversation individually and call CoreCrypto.e2eiRotate.

    Two new functions have been added, CoreCrypto.saveX509Credential and CoreCrypto.deleteStaleKeyPackages. The former should be used after getting a new X509 credential, while the latter should be called after generating keypackages for the new credential and replacing the stale ones in the backend.

    Affected platforms: Web, Android, iOS

    Migration: replace calls to e2eiRotateAll with iterations over conversations, calling e2eiRotate on every conversation and checking for errors. Use saveX509Credential and deleteStaleKeyPackages as appropriate (more details in API documentation).

  • The proposal API has been removed, simplifying the public API a great deal. This includes functions like newProposal, newExternalProposal, clearPendingProposal, joinConversation etc.

    Affected platforms: Web, Android, iOS

    Migration: drop all calls to removed functions as they are no longer necessary with the new MLS transport interface.

CoreCrypto 3

v3.1.1 - 2025-04-15

  • This release bumps the version of rusty-jwt-tools to 0.13.0, which includes additional end-to-end identity tests and test markers relevant to Bund.

v3.1.0 - 2025-02-12

  • Add a test case mimicking a real life bug ([WPB-15810]), demonstrating that in some cases it was possible to generate errors by swapping the ordering of two messages.

  • Add a new layer of buffering to handle that situation.

    Note

    Decrypting a message can now potentially return a MlsError::Other variant with the message

    Incoming message is a commit for which we have not yet received all the proposals. Buffering until all proposals have arrived.

    Clients do not need to take any action in response to this message. This error simply indicates that the commit has been buffered, and will be automatically unbuffered when possible.

    If the required proposal is never delivered, however, the client will eventually desync as the commit will never be processed. Clients should be on the lookout for this case and trigger their rejoin protocol in that event.

v3.0.2 - 2025-01-31

  • Fix a bug which could cause certain errors to generate spurious log lines of the form

    Cannot build CoreCryptoError, falling back to standard Error! ctx: Incoming message is from an epoch too far in the future to buffer.

v3.0.1 - 2025-01-27

  • Emit info log with context when buffering, restoring, or clearing buffered messages

v3.0.0 - 2024-12-11

  • Fix the ‘transaction in progress’ error when there was an attempt to perform multiple transactions in parallel. This will no longer throw an error, instead the transactions will be queued and performed serially one after another.

Breaking changes

  • Added the missing MLS error case OrphanWelcome.

CoreCrypto 2

v2.0.0 - 2024-12-02

  • The number of public errors has been reduced and simplified. It’s no longer necessary to use the proteus_last_error_code function, since thrown error should contain all the information.
  • The logger callback now includes an additional context parameter which contains additional context for a log event in the form of a JSON Object string.
  • It’s now possible to change the logger and log level at runtime (see setLogLevel and setLogger).

Breaking changes

  • Dropped support for i686-linux-android target.
  • CoreCryptoLogger takes an additional context parameter.
  • CoreCryptoError and its child errors have been refactored to reduce the amount of error we expose and provide explicit errors for Proteus errors. The errors we have removed will appear under the Other case.
    enum ProteusError {
        SessionNotFound,
        DuplicateMessage,
        RemoteIdentityChanged,
        Other(Int),
    }
    
    pub enum MlsError {
        ConversationAlreadyExists,
        DuplicateMessage,
        BufferedFutureMessage,
        WrongEpoch,
        MessageEpochTooOld,
        SelfCommitIgnored,
        UnmergedPendingGroup,
        StaleProposal,
        StaleCommit,
        Other(String)
    }
    

Release Artifacts

Core-Crypto publishes releases to a variety of platforms.

Typescript

Typescript releases are published on NPM.

JVM / KMP

Java bindings are published on Sonatype.

Android

Android bindings are published on Sonatype.

Swift

Swift bindings are published as a pair of .xcframework.zip files on the Github release.

Rust

We do not publish releases to crates.io. Instead, if you need to include CC as a dependency, add it via the repo:

core-crypto = { version = "10.0.0", tag = "v10.0.0", git = "https://github.com/wireapp/core-crypto" }

CoreCrypto Architecture

block-beta

columns 1

block:system
    Kotlin
    Swift
    TSN["TS Native"]
    TSB["TS Browser"]
end

block:uniffi
    UniFFI
    UBRN
end

RUSTFFI["CoreCrypto FFI"]

block:crypto
    CRYPTO["Crypto<br/>MLS • Proteus"]
    E2EI["E2E Identity"]
end

STORAGE["Storage"]
block:database
    Native["SQLite + SQLCipher"]
    Browser["IndexedDB"]
end

classDef highlighted fill:#969,stroke:#333,stroke-width:3px;
class TSB highlighted
class Browser highlighted

CoreCrypto FFI

Allows other programming languages and platforms to embed and interact with CoreCrypto

  • For iOS and Android, UniFFI is used to produce the relevant Kotlin and Swift bindings
  • For Typescript we additionally use UBRN to create a WASM binary for browser and a native binary for native TS from our UniFFI bindings.

Data Types

RustSwiftKotlinTypeScript
boolBoolBooleanboolean
u8UInt8UBytenumber
u16UInt16UShortnumber
u32UInt32UIntnumber
u64UInt64ULongbigint
i8Int8Bytenumber
i16Int16Shortnumber
i32Int32Intnumber
i64Int64Longbigint
f32FloatFloatnumber
f64DoubleDoublenumber
String / &strStringStringstring
std::time::SystemTimeDatejava.time.InstantDate
std::time::DurationTimeIntervaljava.time.Durationnumber (in milliseconds)
Option<T>Optional<T>Optional<T>T?
Vec<T>Array<T>List<T>Array<T>
HashMap<String, T>Dictionary<String, T>Map<String, T>Record<string, T>
()nilnullnull
Result<T, E>func placeholder() throws E -> TT placeholder() throws Efunction placeholder(): T // @throws E

For more information, see UniFFI Docs and UBRN Docs

CoreCrypto

CoreCrypto provides a unified abstraction layer over MLS and Proteus. It exposes a simplified interface built around application-level entities rather than protocol-specific concepts.

Database

Encrypted Keystore powered by SQLCipher on native platforms. WASM uses an IndexedDB-backed, encrypted store with AES256-GCM. It provides a persistent data storage layer with encryption at-rest.

Native (iOS, Android, Ts-Native)

Pretty much everything is handed off to SQLCipher:

graph LR
    RS[Rusqlite] --> S[SQLCipher]
    SC{AES-256-CBC}
    S -.->|Encrypts| SC
    SC -.->|Decrypts| S
    SC -->|Stores| SF[File]

See SQLCipher design

Summary:

  • SQLCipher’s file page size by default is 4096 bytes
  • When using a passphrase (our case), the provided passphrase is derived using PBKDF2-HMAC-SHA512.
    The salt of this KDF is stored in the 16 first bytes of the file.
    • Note: This cannot be kept as-is on iOS as iOS needs to be able to read the first 16-32 bytes of SQLite databases to “magically” guess they are SQLite databases
      and to allow reading them from the background. This is very useful in the case of background work on iOS such as encrypted data in notifications needing access to the keystore.
  • Each page is encrypted or decrypted on-the-fly using AES256-CBC
    • Provided by OpenSSL -v1.1.1p as of 29/06/22- in our case, but the crypto provider can be changed to NSS, LibTomCrypt or Security.framework
  • Each page is written with a unique, random IV (initialization vector). This IV is regenerated on each page write. This IV is appended at the end of each page.
  • Page ciphertexts are authenticated using an authentication tag using HMAC-SHA512. This tag is also appended at the each of the page.

WASM

  • Backing store: IndexedDB (with the idb crate)
  • Encryption: AES256-GCM Value-Level-Encryption with random, non-reused 96-bits nonces and embedded authentication tag (AAD) of the AEAD
    • Caveat: Primary IDs are not encrypted, as this would compromise lookup and cause whole table
      scans. It is thus not recommended to store sensitive or identifying data in the primary ID.
    • Caveat: Indexed searches do work, in two steps, an optimistic step fetching an unencrypted record,
      and a fallback step iterating on all records, decrypting the targeted field and checking it. Worst case it will run a whole table scan.
  • Crypto primitives provider: RustCrypto - aes-gcm crate
  • PRNG provider: rand crate with getrandom (uses Crypto.getRandomValues under the hood)
graph LR
    direction LR
    B(Keystore Entities)
    C{AES-256-GCM} -->|Stores| I[IndexedDB]
    B -.->|Encrypts| C
    C -.->|Decrypts| B

How the value-level encryption works

  • Consumers of the library are required to provide 32 bytes, generated by a CSPRNG or a hardware RNG, to be used as an AES-256 key
  • Entities (i.e. Models in an ORM environment) dictate which fields are encrypted and with which AAD
    through their implementation of the Entity trait.
  • By default, the AAD is the primary ID of the IndexedDB collection (i.e Table in a SQL database environment)
  • AES256-GCM is used to encrypt the aforementioned fields
    • A random 96-bit (12 bytes) Nonce is generated
    • The AAD is fetched through Entity::aad()
    • Together they are fed to aes-gcm to create a ciphertext with embedded authentication tag
  • The ciphertext is then stored along with its nonce with the following data layout:
    • Cleartext: A buffer of N bytes ([u8; N])
    • Ciphertext: [12 bytes of nonce..., ...ciphertext]
  • When decrypting, the stored nonce is picked apart from the ciphertext, the AAD is also fetched, then the cleartext is
    decrypted and returned
  • Note: All the fields from all entities are zeroed on drop for security reasons

Rust Crypto Dependencies

Cryptographic primitives & spec implementations

CSPRNG

We use rand in combination with rand-chacha to achieve a proper CSPRNG

  • No audits are known of this crate, but it is the de facto for the Rust ecosystem and is used by pretty much any crate needing a randomness source.
  • Note: We use getrandom under the hood to retrieve random data. Other than de facto OS entropy sources, we can inject entropy at will
    • On this topic, the entropy sources per platform are detailed here

CI Builds via GNU make and GitHub Actions

Make-based actions & workflows

Our CI pipeline is heavily driven by make rules (in the root Makefile). To avoid duplicating logic, the CI uses small GitHub composite actions that invoke specific make targets with consistent artifact caching via GitHub artifatcs.

The directory .github/actions/make/ houses:

The workflow file .github/workflows/pipeline.yml orchestrates CI jobs, and uses those make actions in steps.

Generic make action

This action produces an artifact which corresponds to a rule in the root Makefile, or downloads it if any subsequent workflow run already produced the artifact with unchanged prerequisites.

File Hashes of Dependencies

The prerequisites of ceach artifact are defined in the Makefile itself. Each artifact that is produced using this action, has a corresponding variable in the Makefile, called <target-name>-deps, which lists the artifact’s dependencies.

The Makefile contains a wildcard rule that uses this variable to calculate an aggregated sha256sum of all dependencies. This is then appended to the artifact key, identifying an artifact when downloading or uploading.

Artifact Caching

Each caller of the generic make action must provide an artifact key, required to identify an artifact. This is necessary downloading a previously produced artifact or uploading the artifact just produced. The aggregated sha256sum (see above) is then appended to that key to ensure artifacts with different dependencies don’t share a key.

In case an artifact was successfully downloaded, the action touches them, because otherwise the checked out source files would be newer than the downloaded artifacts. This is necessary because another subsequent call of this actions for another rule might require that the downloaded artifact is newer than the source files.

Otherwise, the artifact is produced by calling make <rule argument>.

In both cases (artifact was downloaded or produced), the artifact is uploaded only if it hasn’t been uploaded for this workflow run of pipeline.yml. This is relevant because the make action may be called with the same parameters multiple times in a single workflow run.

Specialized make actions

Since most artifacts (except those produced by a job representing a “leaf” in the make dependency tree) are reused during the pipeline.yml workflow, the generic action is frequently called with the same arguments. The parameters of the make action are the artifact key, the make rule, and the target path. To avoid having to repeat the arguments when an artifact is reused, we’re using sepcialized make actions, located in subfolders of .github/actions/make. These specialized actions are parameterless, except for the github token, which they just forward. The github token is needed to use the gh CLI on the runner machine.

Any reused artifact should have such a specialized action. Whenever a new artifact is introduced in the workflow and it needs to be reused, an action should be added.

Manually publish release artifacts

If one of the publishing jobs fails due to some temporary error, it might be necessary to publish the release artifacts for a platform manually to avoid having to restart the whole release process.

Android / JVM (Kotlin)

Preparation

  • Checkout the release tag
  • Open the Core Libraries vault on 1Password and copy the secrets from Maven Central Publishing and CoreCrypto Sonatype PGP Signing Key
    export ORG_GRADLE_PROJECT_mavenCentralPassword <secret>
    export ORG_GRADLE_PROJECT_mavenCentralUsername <secret>
    export ORG_GRADLE_PROJECT_signingInMemoryKeyId <secret>
    export ORG_GRADLE_PROJECT_signingInMemoryKey <secret>
    export ORG_GRADLE_PROJECT_signingInMemoryKeyPassword <secret>
    

Android

  • Download the android.zip from the release on https://github.com/wireapp/core-crypto/releases
  • Extract the archive and copy the Android artifacts into the root of the core-crypto project
    cp -r ~/downloads/android/* core-crypto
    
  • Publish the project
    cd crypto-ffi/bindings
    ./gradlew android:publishAllPublicationsToMavenCentralRepository --no-configuration-cache
    

JVM

  • Download the jvm.zip from the release on https://github.com/wireapp/core-crypto/releases
  • Extract the archive and copy the JVM artifacts into the root of the core-crypto project
    cp -r ~/downloads/jvm/* core-crypto
    
  • Publish the project
    cd crypto-ffi/bindings
    ./gradlew jvm:publishAllPublicationsToMavenCentralRepository --no-configuration-cache
    

iOS (Swift)

iOS artifacts aren’t distributed through a centralized package manager. If the artifact has been uploaded to the GitHub release, it is considered to be published.

NPM (Typescript)

  • Download the wireapp-core-crypto-x.y.z.tgz from the release on https://github.com/wireapp/core-crypto/releases
  • Publish:
    cd crypto-ffi/bindings/js
    bun publish ~/downloads/wireapp-core-crypto-x.y.z.tgz