Skip to content

Getting started

The shortest path from an empty project to one message sent, received, committed and acknowledged. The Kotlin below is taken verbatim from samples/jvm-e2e, which the release gate compiles and runs against the published artifacts.

Pre-1.0, no comprehensive audit

KSecureMessage is 0.x and has not had a comprehensive security audit. Read the security model and its known residual risks before you depend on it.

Dependencies

Group dev.kreienbuehl.ksecuremessage, one artifact per module named ksecuremessage-<module path>. The current version is 0.1.0 (changelog).

dependencies {
    implementation("dev.kreienbuehl.ksecuremessage:ksecuremessage-client-core:0.1.0")
    implementation("dev.kreienbuehl.ksecuremessage:ksecuremessage-client-ktor:0.1.0")
    implementation("dev.kreienbuehl.ksecuremessage:ksecuremessage-storage-client-sqldelight:0.1.0")
    // one platform key provider:
    implementation("dev.kreienbuehl.ksecuremessage:ksecuremessage-storage-keyprovider-android:0.1.0")
    implementation("dev.kreienbuehl.ksecuremessage:ksecuremessage-storage-keyprovider-apple:0.1.0")
}
dependencies {
    implementation("dev.kreienbuehl.ksecuremessage:ksecuremessage-server-core:0.1.0")
    implementation("dev.kreienbuehl.ksecuremessage:ksecuremessage-server-ktor:0.1.0")
    implementation("dev.kreienbuehl.ksecuremessage:ksecuremessage-storage-server-sqldelight:0.1.0")
}

The full artifact list is in releasing. Snapshots are served from the Maven Central snapshot repository https://central.sonatype.com/repository/maven-snapshots/.

Choose storage and a key provider

Use Client storage Storage key
Production, Android SqlDelightClientStorage.open(driver, keyProvider) AndroidStorageKeyProvider(context)
Production, iOS/macOS SqlDelightClientStorage.open(driver, keyProvider) AppleStorageKeyProvider()
Other platforms SqlDelightClientStorage.open(driver, keyProvider) your own StorageKeyProvider backed by the platform's secret store
Tests and examples InMemoryClientStorage() none (not encrypted, not persistent)

The application creates and owns the SQLDelight driver (SqlDelightClientStorage.Schema). See client storage, storage encryption and storage key providers.

Run a server

The server is a blind relay: it stores public prekeys and queues opaque envelopes. The host owns the SQLite driver and its own user authentication: the registration route asks the host for the caller's principal and the DeviceRegistrationAuthorizer decides with it (the sample's bearer-token table is DEMO ONLY).

val databaseFile = Files.createTempDirectory("ksecuremessage-sample").resolve("server.db")
val driver = JdbcSqliteDriver("jdbc:sqlite:$databaseFile", Properties(), SqlDelightServerStorage.Schema)
val serverStorage = SqlDelightServerStorage.open(driver)
// Which caller may add a device to which user is the host's decision
// (docs/server-authentication.md, "Registration authorization"): the
// host's own authentication establishes a principal for the request, and
// the authorizer compares it with the user the device is registered for.
// DEMO ONLY: a fixed bearer-token table stands in for the application's
// login. Replace it with your application's authenticated principal.
val demoSessions = mapOf(
    "demo-token-alice" to DemoPrincipal(UserId("alice")),
    "demo-token-bob" to DemoPrincipal(UserId("bob")),
    "demo-token-mallory" to DemoPrincipal(UserId("mallory")),
)
val registrationContext = DeviceRegistrationContextExtractor { call ->
    // null = not authenticated: the registration is refused without asking the authorizer.
    call.request.headers[HttpHeaders.Authorization]?.takeIf { it.startsWith("Bearer ") }?.let { demoSessions[it.removePrefix("Bearer ")] }
}
val registrationAuthorizer = DeviceRegistrationAuthorizer<DemoPrincipal> { principal, request ->
    // A principal may add devices to its own user only.
    if (request.address.userId == principal.userId) {
        DeviceRegistrationAuthorizationResult.Authorized
    } else {
        DeviceRegistrationAuthorizationResult.Denied
    }
}
// No RecoveryKeyResetPolicy: delayed recovery key resets are disabled on this server.
val server = SecureMessageServer(serverStorage, Clock.System, registrationAuthorizer, recoveryKeyResetPolicy = null)
val http = embeddedServer(CIO, port = 0, host = "127.0.0.1") {
    install(ServerContentNegotiation) { json() }
    routing { kSecureMessageRoutes(server, registrationContext) }
}.start(wait = false)

Clocks, backups and deployment notes: operating the server.

Create clients

Each device has a DeviceAddress (user + device). The sample keeps storage in memory to stay short; use SqlDelightClientStorage in an application. The transport's HttpClient carries the application's own authentication.

// The application's HTTP client carries the application's own
// authentication (DEMO ONLY: the fixed bearer tokens above).
fun transport(token: String?) = KtorSecureMessageTransport(
    baseUrl,
    HttpClient {
        install(ClientContentNegotiation) { json() }
        if (token != null) defaultRequest { bearerAuth(token) }
    },
)
val aliceAddress = DeviceAddress(UserId("alice"), DeviceId("phone"))
val bobAddress = DeviceAddress(UserId("bob"), DeviceId("laptop"))
val alice = SecureMessageClient(aliceAddress, InMemoryClientStorage(), KodiumProtocolEngine(), transport("demo-token-alice"))
val bob = SecureMessageClient(bobAddress, InMemoryClientStorage(), KodiumProtocolEngine(), transport("demo-token-bob"))

First launch: initialize, register, publish

initialize() creates the local identity, the device authentication key and prekeys (on every launch it tops them up). registerDevice() registers the device authentication key with the server, publishPreKeys() uploads the public prekeys.

for (client in listOf(alice, bob)) {
    client.initialize()
    client.registerDevice()
    client.publishPreKeys()
}

Send

The message stays pending on the sender until the recipient acknowledges it.

val sent = alice.send(bobAddress, "Hello Bob".encodeToByteArray())

Receive and commit

receive() drains the mailbox; decrypt returns a Delivery that is not yet acknowledged. Apply the message durably first, then commit: the commit sends the ACK.

val delivered = bob.receive().map { bob.decrypt(it) }
val delivery = delivered.single() as? ReceiveResult.Delivery ?: error("expected a Delivery, got $delivered")
val commit = bob.commitReceivedMessage(delivery.message)

Next