CoreCrypto ships Swift bindings generated by UniFFI. The API mirrors the Rust
interface with camelCase naming throughout.
Adding the framework
XCFramework
Swift Package
Build the framework from source (see Building from source), then:
- In Xcode, select your target and open General.
- Under Frameworks, Libraries, and Embedded Content, click +.
- Choose Add Other → Add Files and select
WireCoreCrypto.xcframework.
- Set the embed option to Embed & Sign.
Reference the WireCoreCrypto product in your Package.swift or via
Xcode’s File → Add Package Dependencies using the
wireapp/core-crypto repository
URL and the tag matching your desired version.
Setup
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.
Create a CoreCrypto instance
import WireCoreCrypto
let coreCrypto = try CoreCrypto(database: database)
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
| Type | What it is |
|---|
CoreCrypto | Long-lived handle to the encrypted database. Acquired via CoreCrypto(database:). |
CoreCryptoContextProtocol | Short-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:
| Rust | Swift |
|---|
u8 | UInt8 |
u32 | UInt32 |
u64 | UInt64 |
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.