Skip to main content
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

1

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

Create a CoreCrypto instance

import com.wire.crypto.CoreCrypto

val coreCrypto = CoreCrypto(database)
3

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

ClassWhat it is
CoreCryptoLong-lived handle to the encrypted database. Acquired via CoreCrypto(database).
CoreCryptoContextShort-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.
coreCrypto.close()

Naming conventions

All Rust snake_case methods are mapped to camelCase in Kotlin. Rust numeric types map as follows:
RustKotlin
u8UByte
u32UInt
u64ULong
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/.

Build docs developers (and LLMs) love