Skip to main content
CoreCrypto ships Swift bindings generated by UniFFI. The API mirrors the Rust interface with camelCase naming throughout.

Adding the framework

Build the framework from source (see Building from source), then:
  1. In Xcode, select your target and open General.
  2. Under Frameworks, Libraries, and Embedded Content, click +.
  3. Choose Add Other → Add Files and select WireCoreCrypto.xcframework.
  4. Set the embed option to Embed & Sign.

Setup

1

Open a database

CoreCrypto stores cryptographic material in an encrypted SQLite database via SQLCipher. Choose a path inside your app’s sandboxed container.
import WireCoreCrypto

// Generate a 32-byte key once and store it in the iOS Keychain.
var rawKey = [UInt8](repeating: 0, count: 32)
_ = SecRandomCopyBytes(kSecRandomDefault, rawKey.count, &rawKey)
// DatabaseKey's initialiser throws if the key is not exactly 32 bytes.
let databaseKey = try DatabaseKey(key: Data(rawKey))

let dbDirectory = FileManager.default
    .urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
    .appendingPathComponent("mls", isDirectory: true)
try FileManager.default.createDirectory(at: dbDirectory, withIntermediateDirectories: true)
let dbPath = dbDirectory.appendingPathComponent("keystore").path

let database = try await Database.open(location: dbPath, key: databaseKey)
Store the DatabaseKey bytes in the iOS Keychain (kSecClassGenericPassword), not in UserDefaults. A mismatched key will cause Database.open to throw.
2

Create a CoreCrypto instance

import WireCoreCrypto

let coreCrypto = try CoreCrypto(database: database)
3

Initialise MLS inside a transaction

All state-mutating operations go through a transaction. The closure receives a CoreCryptoContextProtocol value; if the closure throws, every change made inside it is rolled back.
let clientId = ClientId(bytes: "[email protected]/device-1".data(using: .utf8)!)

try await coreCrypto.transaction { ctx in
    // Attach the MLS transport before initialising.
    try await ctx.mlsInit(clientId: clientId, transport: myMlsTransport)

    // Add a credential so you can generate key packages.
    let credential = try Credential.basic(
        ciphersuite: ciphersuiteDefault(),
        clientId: clientId
    )
    _ = try await ctx.addCredential(credential: credential)
}

Implementing the MlsTransport protocol

CoreCrypto calls your transport implementation whenever it needs to deliver a commit bundle or a plain MLS message to the delivery service.
import WireCoreCrypto

final actor MyMlsTransport: MlsTransport {
    func sendCommitBundle(commitBundle: CommitBundle) async -> MlsTransportResponse {
        // Deliver commit, welcome, and group info to your server.
        await deliveryService.postCommit(commitBundle)
        return .success
    }

    func sendMessage(mlsMessage: Data) async -> MlsTransportResponse {
        await deliveryService.postMessage(mlsMessage)
        return .success
    }

    func prepareForTransport(historySecret: HistorySecret) async -> MlsTransportData {
        // Encode data for the history client (only required when history sharing is enabled).
        return historySecret.clientId.copyBytes()
    }
}

Working with MLS groups

Creating a conversation

let conversationId = ConversationId(bytes: "room-42".data(using: .utf8)!)

try await coreCrypto.transaction { ctx in
    let credentialRef = try await ctx.findCredentials(
        clientId: nil,
        publicKey: nil,
        ciphersuite: ciphersuiteDefault(),
        credentialType: .basic,
        earliestValidity: nil
    ).first!

    try await ctx.createConversation(
        conversationId: conversationId,
        credentialRef: credentialRef,
        externalSender: nil
    )
}

Adding members

Obtain the invitee’s key package, then call addClientsToConversation. The resulting CommitBundle is delivered to the transport automatically.
try await coreCrypto.transaction { ctx in
    // bobKeyPackage is a Keypackage received from the server.
    _ = try await ctx.addClientsToConversation(
        conversationId: conversationId,
        keyPackages: [bobKeyPackage]
    )
}
The invited client processes the welcome:
try await bobCoreCrypto.transaction { ctx in
    // welcomeMessage is the Welcome bytes from the commit bundle.
    let welcomeBundle = try await ctx.processWelcomeMessage(welcomeMessage: welcomeMessage)
    print("Joined conversation: \(welcomeBundle.id)")
}

Removing members

try await coreCrypto.transaction { ctx in
    try await ctx.removeClientsFromConversation(
        conversationId: conversationId,
        clients: [carolId]
    )
}

Encrypting and decrypting messages

let plaintext = "Hello, MLS!".data(using: .utf8)!

// Sender encrypts.
let ciphertext = try await coreCrypto.transaction { ctx in
    try await ctx.encryptMessage(conversationId: conversationId, message: plaintext)
}

// Receiver decrypts.
let decrypted = try await bobCoreCrypto.transaction { ctx in
    try await ctx.decryptMessage(conversationId: conversationId, payload: ciphertext)
}

if let message = decrypted.message {
    print(String(data: message, encoding: .utf8)!) // "Hello, MLS!"
}

Generating key packages

let keyPackage = try await coreCrypto.transaction { ctx in
    let credentialRef = try await ctx.findCredentials(
        clientId: nil,
        publicKey: nil,
        ciphersuite: ciphersuiteDefault(),
        credentialType: .basic,
        earliestValidity: nil
    ).first!

    return try await ctx.generateKeypackage(credentialRef: credentialRef, lifetime: nil)
}

// Serialise for upload to the key distribution service.
let keyPackageBytes = try keyPackage.serialize()

Swift-specific notes

CoreCrypto vs CoreCryptoContextProtocol

TypeWhat it is
CoreCryptoLong-lived handle to the encrypted database. Acquired via CoreCrypto(database:).
CoreCryptoContextProtocolShort-lived transaction context. Passed to the transaction(_:) closure.
All MLS and Proteus operations are async throws methods on CoreCryptoContextProtocol. The CoreCrypto object itself exposes transaction(_:), observer registration, and a handful of non-mutating helpers.

Swift concurrency

CoreCrypto is @unchecked Sendable, allowing it to be shared across actors. Transactions are serialised internally — concurrent transaction calls on the same instance are queued, not run in parallel. File-based instances also acquire an exclusive file lock for the duration of each transaction, making multi-process use safe.
// Transactions are async — call them with await.
try await coreCrypto.transaction { ctx in
    try await ctx.updateKeyingMaterial(conversationId: conversationId)
}

// Spawn concurrent tasks safely; CoreCrypto serialises them.
await withThrowingTaskGroup(of: Void.self) { group in
    group.addTask { try await coreCrypto.transaction { ctx in /* ... */ } }
    group.addTask { try await coreCrypto.transaction { ctx in /* ... */ } }
    try await group.waitForAll()
}

Epoch and history observers

try await coreCrypto.registerEpochObserver(MyEpochObserver())

try await coreCrypto.registerHistoryObserver(MyHistoryObserver())
Observer methods are dispatched asynchronously via Task { } to prevent the observer from blocking the CoreCrypto runtime.
final actor MyEpochObserver: EpochObserver {
    func epochChanged(conversationId: ConversationId, epoch: UInt64) async throws {
        // Notified after each commit that advances the epoch.
    }
}

final actor MyHistoryObserver: HistoryObserver {
    func historyClientCreated(conversationId: ConversationId, secret: HistorySecret) async throws {
        // Notified when history sharing is enabled for a conversation.
    }
}

Updating the database key

try await database.updateKey(key: newDatabaseKey)
This re-encrypts the SQLite database in place. The CoreCrypto instance that opened the database must be recreated afterwards.

Naming conventions

All Rust snake_case methods are mapped to camelCase in Swift. Rust numeric types map as follows:
RustSwift
u8UInt8
u32UInt32
u64UInt64
Vec<T>[T]
Option<T>T?

Building from source

See the general requirements for Rust toolchain setup. Then:
# Install Xcode and its command-line tools, then add Rust targets:
rustup target add aarch64-apple-ios aarch64-apple-ios-sim

# Build the iOS framework.
make ios

# Package as an XCFramework (required for distribution).
make ios-create-xcframework

# Run Swift tests (macOS only).
make ios-test
The built WireCoreCrypto.xcframework is placed in the repository root’s build output directory after make ios-create-xcframework.

Build docs developers (and LLMs) love