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¶
- Application lifecycle: retries, restarts, identity changes, recovery and every other call.
- Application delivery: at-least-once delivery and the commit boundary.
- API reference.