CoreCrypto ships Kotlin bindings generated by UniFFI. The API mirrors the Rust
interface with camelCase naming throughout.
Gradle dependency
Add the Maven Central artifact to your module’s build.gradle.kts:
dependencies {
implementation("com.wire:core-crypto-android:9.0.1")
}
The group ID is com.wire and the artifact ID is core-crypto-android.
Published versions follow semantic versioning and are
available on Maven Central.
Setup
Open a database
CoreCrypto stores cryptographic material in an encrypted SQLite database
via SQLCipher. Choose a path inside your app’s files directory so the
operating system protects it with its file-based encryption.import com.wire.crypto.Database
import com.wire.crypto.DatabaseKey
// Generate a 32-byte key once and store it in the Android Keystore.
val rawKey = ByteArray(32).also { java.security.SecureRandom().nextBytes(it) }
val databaseKey = DatabaseKey(rawKey)
val dbPath = context.filesDir.resolve("mls/keystore").also { it.parentFile?.mkdirs() }
val database = Database.open(
location = dbPath.absolutePath,
key = databaseKey
)
Store the DatabaseKey bytes in the Android Keystore (e.g. using
EncryptedSharedPreferences or a KeyStore-backed secret), not in
plaintext SharedPreferences. A mismatched key will throw on open.
Create a CoreCrypto instance
import com.wire.crypto.CoreCrypto
val coreCrypto = CoreCrypto(database)
Initialise MLS inside a transaction
All state-mutating operations go through a transaction. The lambda
receives a CoreCryptoContext and runs under NonCancellable; if it
throws, every change made inside it is rolled back.import com.wire.crypto.*
val clientId = ClientId("[email protected]/device-1".toByteArray())
coreCrypto.transaction { ctx ->
// Attach the MLS transport before initialising.
ctx.mlsInit(clientId, myMlsTransport)
// Add a credential so you can generate key packages.
ctx.addCredential(
Credential.basic(CIPHERSUITE_DEFAULT, clientId)
)
}
Implementing MlsTransport
CoreCrypto calls your transport implementation whenever it needs to deliver a
commit bundle or a plain MLS message to the delivery service.
import com.wire.crypto.*
class MyMlsTransport : MlsTransport {
override suspend fun sendCommitBundle(commitBundle: CommitBundle): MlsTransportResponse {
// Deliver commit, welcome, and group info to your server.
deliveryService.postCommit(commitBundle)
return MlsTransportResponse.Success
}
override suspend fun sendMessage(mlsMessage: ByteArray): MlsTransportResponse {
deliveryService.postMessage(mlsMessage)
return MlsTransportResponse.Success
}
override suspend fun prepareForTransport(historySecret: HistorySecret): 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
val conversationId = ConversationId("room-42".toByteArray())
coreCrypto.transaction { ctx ->
val credentialRef = ctx.findCredentials(
clientId = null,
publicKey = null,
ciphersuite = CIPHERSUITE_DEFAULT,
credentialType = CREDENTIAL_TYPE_DEFAULT,
earliestValidity = null
).last()
ctx.createConversation(
conversationId = conversationId,
credentialRef = credentialRef,
externalSender = null
)
}
Adding members
Obtain the invitee’s key package, then call addClientsToConversation. The
resulting CommitBundle is delivered to the transport automatically.
coreCrypto.transaction { ctx ->
// bobKeyPackage is a Keypackage received from the server.
ctx.addClientsToConversation(
conversationId = conversationId,
keyPackages = listOf(bobKeyPackage)
)
}
The invited client processes the welcome:
bobCoreCrypto.transaction { ctx ->
// welcomeMessage is the Welcome bytes from the commit bundle.
val welcomeBundle = ctx.processWelcomeMessage(welcomeMessage)
println("Joined conversation: ${welcomeBundle.id}")
}
Removing members
coreCrypto.transaction { ctx ->
ctx.removeClientsFromConversation(
conversationId = conversationId,
clients = listOf(carolId)
)
}
Encrypting and decrypting messages
val plaintext = "Hello, MLS!".toByteArray()
// Sender encrypts.
val ciphertext = coreCrypto.transaction { ctx ->
ctx.encryptMessage(conversationId, plaintext)
}
// Receiver decrypts.
val decrypted = bobCoreCrypto.transaction { ctx ->
ctx.decryptMessage(conversationId, ciphertext)
}
decrypted.message?.let { println(String(it)) } // "Hello, MLS!"
Generating key packages
val keyPackage = coreCrypto.transaction { ctx ->
val credentialRef = ctx.findCredentials(
clientId = null,
publicKey = null,
ciphersuite = CIPHERSUITE_DEFAULT,
credentialType = CREDENTIAL_TYPE_DEFAULT,
earliestValidity = null
).last()
ctx.generateKeypackage(credentialRef)
}
// Serialise for upload to the key distribution service.
val keyPackageBytes = keyPackage.serialize()
Kotlin-specific notes
CoreCrypto vs CoreCryptoContext
| Class | What it is |
|---|
CoreCrypto | Long-lived handle to the encrypted database. Acquired via CoreCrypto(database). |
CoreCryptoContext | Short-lived transaction context. Provided to the transaction { } lambda. |
All MLS and Proteus operations are suspend methods on CoreCryptoContext.
Coroutines and suspend functions
CoreCrypto.transaction runs with NonCancellable coroutine context
internally. Cancelling the outer coroutine does not interrupt an in-flight
transaction; the transaction finishes and then the cancellation is observed.
This matches UniFFI’s recommendation for async Rust interop.
// Transactions are suspend functions — call them from a coroutine.
viewModelScope.launch {
coreCrypto.transaction { ctx ->
ctx.updateKeyingMaterial(conversationId)
}
}
Epoch and history observers
Register observers before your first transaction and pass a CoroutineScope
that lives as long as you need the observer:
coreCrypto.registerEpochObserver(lifecycleScope, object : EpochObserver {
override suspend fun epochChanged(conversationId: ConversationId, epoch: ULong) {
// Called after each commit that changes the epoch.
}
})
coreCrypto.registerHistoryObserver(lifecycleScope, object : HistoryObserver {
override suspend fun historyClientCreated(conversationId: ConversationId, secret: HistorySecret) {
// Called when history sharing is enabled for a conversation.
}
})
Closing the instance
Call close() when you no longer need the CoreCrypto instance to release
native memory. After close() the object must not be used again.
Naming conventions
All Rust snake_case methods are mapped to camelCase in Kotlin. Rust
numeric types map as follows:
| Rust | Kotlin |
|---|
u8 | UByte |
u32 | UInt |
u64 | ULong |
Vec<T> | List<T> |
Option<T> | nullable T? |
Building from source
See the general requirements for Rust toolchain
setup. Then:
# Install Android SDK and NDK, then add Rust targets:
rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi
# Build the Android AAR.
make android
# Build and run Android instrumented tests (requires a connected device or emulator).
make android-test
The built AAR is placed under crypto-ffi/bindings/android/build/.