+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..e49caad
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,26 @@
+Krypton
+Copyright (C) 2026 Ranbir Singh
+
+This product links libsignal (https://github.com/signalapp/libsignal),
+Copyright Signal Messenger, LLC, licensed under AGPL-3.0-only. The
+cryptographic functionality is entirely libsignal's; Krypton provides
+Kotlin Multiplatform bindings and ships no Signal binary of its own.
+
+ Cryptography Notice
+ -------------------
+ This distribution includes cryptographic software. The country in which
+ you currently reside may have restrictions on the import, possession,
+ use, and/or re-export to another country, of encryption software. BEFORE
+ using any encryption software, please check your country's laws,
+ regulations and policies concerning the import, possession, or use, and
+ re-export of encryption software, to see if this is permitted. See
+ https://www.wassenaar.org/ for more information.
+
+ The U.S. Government Department of Commerce, Bureau of Industry and
+ Security (BIS), has classified this software as Export Commodity Control
+ Number (ECCN) 5D002.C.1, which includes information security software
+ using or performing cryptographic functions with asymmetric algorithms.
+ The form and manner of this distribution makes it eligible for export
+ under the License Exception ENC Technology Software Unrestricted (TSU)
+ exception (see the BIS Export Administration Regulations, Section 740.13)
+ for both object code and source code.
diff --git a/README.md b/README.md
index 57a3c7d..6e6609c 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,45 @@
-# ⚛️ Krypton
+⚛️ Krypton
-**Signal's end-to-end encryption protocol for Kotlin Multiplatform** — the audited [libsignal](https://github.com/signalapp/libsignal) core under the hood, behind one clean common API.
+Signal's end-to-end encryption for Kotlin Multiplatform.
-> Named after Krypton (Kr), a noble gas that's inert, stable, and doesn't react with anything — just like perfectly encrypted data.
+
+
+
+
+
+
-Krypton does **not** reimplement cryptography. It binds to Signal's real, audited libsignal on every platform and exposes a single `commonMain` API, so you write encryption code once and it runs everywhere libsignal does.
+
+
+
+
+
+
-## Install
+
+One common API for the Signal Protocol — X3DH, Double Ratchet, sealed sender,
+and zkgroup — backed by Signal's real, audited libsignal
+on Android, iOS, macOS, and JVM/Compose Desktop.
+
+
+---
+
+Krypton does **not** reimplement cryptography. It binds to Signal's audited libsignal
+on every platform and exposes a single `commonMain` API, so you write your encryption
+code once and it runs everywhere libsignal does. Where a platform has no libsignal
+binary, Krypton **fails loud** — it never returns a plausible-looking fake.
-One dependency. It transitively brings the whole real API (`core` + `storage` + `protocol`):
+> Named after Krypton (Kr) — a noble gas that's inert, stable, and reacts with nothing. Like properly encrypted data.
+
+## Why Krypton
+
+- **Real crypto, not a reimplementation.** Same library that ships inside Signal and WhatsApp.
+- **Write once.** One `commonMain` API across Android, iOS, macOS, and JVM.
+- **Tiny footprint.** ~3 MB added to a mobile app — the native lib is dead-stripped at link / stripped on release ([details](#how-big-is-it)).
+- **No fakes.** Unsupported platforms fail loud; a build-time guard stops the native binary from ever drifting from the JVM/Android version.
+- **One dependency.** It transitively brings the whole real API.
+
+## Install
```kotlin
// build.gradle.kts
@@ -17,110 +48,344 @@ dependencies {
}
```
-> **Status:** not yet published to Maven Central — see [Publishing](#publishing). Until then, build locally with `./gradlew publishToMavenLocal` and add `mavenLocal()` to your repositories.
->
-> 📋 For a full snapshot of what's real, verified, and pending, see **[STATUS.md](STATUS.md)**.
+One line pulls the full API (`core` + `storage` + `protocol`) on every target.
+
+- **JVM / Android — nothing else to do.** libsignal is fetched from Signal's official Maven jars.
+- **Apple (iOS/macOS) — bring your own libsignal.** Krypton ships **no Signal binary**; you fetch
+ `libsignal_ffi` from Signal's official source on your own machine (one command) and point the
+ linker at it. See **[Bring your own libsignal](#bring-your-own-libsignal-apple)**. This is a
+ deliberate trust choice — Krypton never redistributes a binary you'd have to trust ([SECURITY.md](SECURITY.md)).
+
+> **Status — pre-first-release.** The Maven Central pipeline is wired and verified
+> ([Releasing](#releasing)); until the first tag is pushed, build locally with
+> `./gradlew publishToMavenLocal` and add `mavenLocal()` to your repositories.
-## Quick start (the stupid-simple API)
+## Quick start
+
+A real two-party exchange — Alice and Bob each hold a `KryptonProtocol`; Bob runs the
+X3DH handshake from Alice's published pre-key bundle, then messages flow encrypted both ways:
```kotlin
-import io.krypton.Krypton
-import io.krypton.protocol.api.encrypt
+import io.krypton.core.types.DeviceId
+import io.krypton.core.types.ProtocolAddress
+import io.krypton.protocol.api.KryptonConfigurator
import io.krypton.protocol.api.decrypt
+import io.krypton.protocol.api.encrypt
+import io.krypton.storage.memory.InMemoryPreKeyStore
+
+// Build a party with a signed pre-key + one-time pre-keys (so it can publish a bundle).
+suspend fun newParty(): KryptonProtocol {
+ val keys = InMemoryPreKeyStore()
+ val p = KryptonConfigurator().apply { preKeyStore = keys }.build()
+ keys.saveSignedPreKey(1, p.generateSignedPreKey(1).getOrThrow()).getOrThrow()
+ p.generatePreKeys(1, 10).getOrThrow().forEach { keys.storePreKey(it.keyId, it).getOrThrow() }
+ return p
+}
+
+val alice = newParty()
+val bob = newParty()
-val krypton = Krypton.protocol { } // zero config (auto identity + stores)
+// Bob fetches Alice's bundle (via your server) and runs X3DH.
+bob.processPreKeyBundle(alice.getPreKeyBundle(ProtocolAddress("alice", DeviceId(1))).getOrThrow())
-val wire = krypton.encrypt("alice", "Hello!").getOrThrow() // wire-ready Base64 string — send it
-val text = krypton.decrypt("alice", wire).getOrThrow() // back to "Hello!"
+// Strings in, Base64 out — send the wire over any transport.
+val wire = bob.encrypt("alice", "Hello Alice!").getOrThrow()
+val text = alice.decrypt("bob", wire).getOrThrow() // "Hello Alice!"
```
-Strings in, Base64 out — no `ProtocolAddress`, `ByteArray`, or message-type bookkeeping. The full typed API (`encrypt(ProtocolAddress, ByteArray)`, pre-key bundles, sessions, groups) is still there when you need it.
+The full typed API (`encrypt(ProtocolAddress, ByteArray)`, sessions, safety numbers, sealed
+sender, zkgroup) is there when you need it. For the end-to-end story — registration, publishing
+bundles, what your server must and must not do — see **[GUIDE.md](GUIDE.md)**.
-> 💬 **Want to build a real end-to-end encrypted chat in your app?** See the step-by-step **[GUIDE.md](GUIDE.md)** — registration, publishing pre-key bundles, the X3DH handshake, messaging, safety numbers, and exactly what your server must (and must not) do.
+## Samples
-## "Do my users ship a 113 MB binary?" — No.
+| Sample | Run | What it shows |
+| --- | --- | --- |
+| `samples/jvm-demo` | `./gradlew :samples:jvm-demo:run` | Headless encrypt→decrypt round-trip on real libsignal |
+| `samples/composeApp` | `./gradlew :samples:composeApp:run` | **One Compose UI shared across Desktop + iOS** — a live encrypted chat |
+| `samples/ktor-server` | `./gradlew :samples:ktor-server:run` | **Companion backend** — Ktor + `org.signal:libsignal-server`: pre-key bundle exchange, sealed-sender certificates, zkgroup credential issuance ([details](samples/ktor-server/README.md)) |
-This is the most common confusion. **Your users add one Gradle line and nothing else.** Here's how the native libsignal reaches each platform:
+The iOS app is an Xcode project under `samples/composeApp/iosApp` (generated with
+[XcodeGen](https://github.com/yonaskolb/XcodeGen)):
-| Platform | How libsignal is delivered | You ship a binary? |
-|---|---|---|
-| JVM / Android | Bundled inside Signal's Maven jars (pulled by Gradle) | **No** |
-| iOS / macOS / native | The static lib is **linked into Krypton's published per-target artifact** (`staticLibraries` in the cinterop def) | **No** |
+```sh
+cd samples/composeApp/iosApp && xcodegen generate
+xcodebuild -project iosApp.xcodeproj -scheme iosApp -sdk iphonesimulator \
+ -destination 'platform=iOS Simulator,name=iPhone 16' -derivedDataPath build \
+ CODE_SIGNING_ALLOWED=NO build
+xcrun simctl install booted build/Build/Products/Debug-iphonesimulator/iosApp.app
+xcrun simctl launch booted io.krypton.sample
+```
-- The `libsignal_ffi.a` (~113 MB) is **gitignored only to keep it out of Git** (GitHub's 100 MB limit; binaries don't belong in source control). It is a **build input for Krypton's CI**, produced by [`scripts/build-libsignal-ffi.sh`](scripts/build-libsignal-ffi.sh), then embedded into the published Maven artifacts.
-- It is **not** the size of your app. That `.a` is an unstripped archive; the linker keeps only used symbols and release builds strip — real app-size impact is a few MB (the same core Signal's own apps ship).
+## Platforms
-## Platform support (honest)
+| Target | Crypto | How libsignal is delivered |
+| --- | --- | --- |
+| **JVM** (desktop) | ✅ Real, verified | `libsignal-client` (bundled natives) from Maven |
+| **Android** | ✅ Real, verified | `libsignal-android` from Maven |
+| **iOS** (arm64 device + simulator) | ✅ Real, verified | cinterop → `libsignal_ffi`, embedded in the artifact |
+| **macOS** (Apple Silicon + Intel) | ✅ Real, verified | cinterop → `libsignal_ffi`, embedded in the artifact |
+| Linux / Windows (native) | ⛔ Fail-loud | no libsignal binary — use the JVM target for desktop |
+| wasm / JS | ⛔ Fail-loud | libsignal has no wasm build |
-| Target | Crypto | Notes |
-|---|---|---|
-| **JVM** (desktop) | ✅ Real, verified | libsignal-client (bundled natives) |
-| **Android** | ✅ Real | libsignal-android |
-| **iOS / macOS** (+ tvOS) | 🟡 Partial | cinterop → `libsignal_ffi`; send path verified, **decrypt WIP** |
-| **Linux / Windows** (native) | ⚠️ Stub | fail-loud; use the **JVM** target for desktop |
-| **wasmJs** | ⚠️ Stub | libsignal has no wasm build — unsupported |
+The full Signal handshake (real ~1568-byte Kyber pre-key → X3DH → encrypt → decrypt) is proven
+by `NativeRealCryptoTest`, which **runs on the iOS simulator and macOS** in addition to the
+JVM/Android tracks.
-Compiles on all of the above. **Full, test-verified E2EE runs on JVM and Android.**
-On Apple, real libsignal_ffi is wired and the send path (real Kyber pre-key +
-X3DH + encrypt) is proven by a real-FFI test, but the PreKey **decrypt** path is
-still WIP (libsignal `InvalidMessage`) — see [STATUS.md](STATUS.md). Don't ship
-Apple E2EE yet.
+## Bring your own libsignal (Apple)
-## API parity with libsignal
+Krypton's published Apple artifacts contain **only the cinterop bindings** (~88 KB) — **no Signal
+binary**. For Apple targets you fetch `libsignal_ffi` from Signal's official source on your own
+machine and point the linker at it. JVM/Android need none of this — libsignal comes from Maven
+automatically.
+
+### Option A — the Gradle plugin (recommended)
+
+Apply the Krypton Gradle plugin and it does everything: fetches `libsignal_ffi` from Signal's
+official source into a cache and wires the `-L` paths onto every Apple target automatically.
+
+```kotlin
+plugins {
+ kotlin("multiplatform")
+ id("io.krypton.libsignal") version "0.1.0"
+}
+
+kryptonLibsignal {
+ version.set("0.86.5") // must match Krypton's pinned libsignal version
+ mode.set("build") // "build" = compile Signal's official source (needs Rust)
+ // "download" = Signal's official prebuilt (iOS-only) + checksum verify
+}
+```
+
+That's it — `linkDebugTestMacosArm64`, your iOS framework link, etc. all `dependsOn` the generated
+`fetchLibsignal` task, so a normal build just works. Nothing about libsignal is committed to your
+repo; the `.a` lives in `~/.krypton/libsignal//` (override with `cacheDir`).
+
+### Option B — the fetch script (no plugin)
+
+If you'd rather not apply a plugin, run the script and wire the linker yourself. Two official ways,
+both straight from Signal:
+
+```sh
+# Build from Signal's official source (needs Rust) — covers every arch, incl. macOS:
+scripts/fetch-libsignal-ffi.sh 0.86.5
+
+# …or download Signal's official prebuilt (iOS-only) and verify its checksum:
+LIBSIGNAL_FFI_PREBUILD_CHECKSUM= scripts/fetch-libsignal-ffi.sh 0.86.5 "" download
+```
+
+Then add the printed `-L` path(s) to your app's Apple targets:
+
+```kotlin
+kotlin {
+ val ls = "${System.getProperty("user.home")}/.krypton/libsignal/0.86.5"
+ iosArm64 { binaries.all { linkerOpts("-L$ls/ios-arm64") } }
+ iosSimulatorArm64 { binaries.all { linkerOpts("-L$ls/ios-sim-arm64") } }
+ macosArm64 { binaries.all { linkerOpts("-L$ls/macos-arm64") } }
+}
+```
+
+> Either way, the libsignal version **must** match Krypton's pinned version (see the badge /
+> `gradle/libs.versions.toml`).
+
+The fetched `.a` is large (~77 MB) but that's a **build input, not your app** — a Release link keeps
+only the few MB you actually use (~3 MB; see [How big is it?](#how-big-is-it)).
+
+### What `build` mode runs under the hood (cargo)
+
+Both the plugin's `build` mode and the script compile `libsignal_ffi` exactly the way Signal's own
+docs describe — straight from the Rust source, with `cargo`:
+
+```sh
+git clone --branch v0.86.5 https://github.com/signalapp/libsignal
+cd libsignal
+rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios \
+ aarch64-apple-darwin x86_64-apple-darwin
+cargo build -p libsignal-ffi --release --target aarch64-apple-ios # …and one per target
+# → target//release/libsignal_ffi.a
+```
-Krypton aims to mirror Signal's libraries so you can follow libsignal's docs. Current coverage:
+You don't run these by hand — the plugin/script do it for you — but that's all that happens.
+**Prerequisite:** a Rust toolchain (`build` mode only). Install once with
+[rustup](https://rustup.rs); the plugin/script add the Apple targets for you. `download` mode needs
+no Rust, but is iOS-only.
+
+> **Why doesn't Krypton just run cargo and ship the result?** It can — that's exactly what `build`
+> mode does on *your* machine. The one thing Krypton refuses to do is publish a pre-built Signal
+> binary in its own artifacts, so no one ever has to trust bytes that came from us instead of from
+> Signal. The crypto is always compiled (or downloaded) from Signal's official source, by you. See
+> [SECURITY.md](SECURITY.md).
+
+## How big is it?
+
+Krypton's own published artifacts are tiny — the JVM jars are ~0.2 MB and the Apple klibs ~88 KB
+(bindings only; the libsignal binary is fetched on your machine, never shipped by Krypton). The
+size you ultimately link is libsignal itself — and far smaller than the raw archive suggests,
+because the linker keeps only what you call and release builds strip:
+
+| Platform | What's bundled | Real app-size impact (release) |
+| --- | --- | --- |
+| iOS / macOS | dead-stripped static code | **~3 MB** per arch (one arch per device via App Store thinning) |
+| Android (per ABI) | one `.so`, AGP-stripped | **~3 MB download / ~6.4 MB installed** (down from ~70 MB in the AAR) |
+| JVM desktop (macOS) | whole `.dylib` | **~20 MB** |
+| JVM (Windows) | whole `.dll` | **~16 MB** |
+| JVM server (Linux) | whole `.so`, **unstripped** | **~120 MB** (see note) |
+
+This is the same core that Signal and WhatsApp ship.
+
+**By default you get everything** — every platform and arch works out of the box, no exclusions
+required. The notes below are *optional* size optimizations, not steps you have to take.
+
+#### Optional: trim JVM size
+
+The JVM jar carries Signal's native for **every** OS (macOS arm64+amd64, Linux, Windows), and the
+Linux `.so` is shipped unstripped (~120 MB). A fat-jar that bundles all of them is large by default.
+If size matters, two opt-in trims:
+
+```kotlin
+// 1. Drop libsignal's own test natives (never needed in production):
+configurations.all { exclude(group = "org.signal", module = "libsignal-client-testing") }
+
+// 2. In your packaging (shadowJar / jpackage), keep only the OS(es) you ship:
+// e.g. exclude **/libsignal_jni_amd64.so on a Mac-only desktop build.
+```
+
+For a Linux server you can also `strip` the extracted `.so` in your image build — most of the
+120 MB is debug info.
+
+### Why the 77 MB archive becomes ~3 MB
+
+The `libsignal_ffi.a` you fetch is a fat, unstripped archive — but it's a **build input, not
+something you ship**. Your Release link shrinks it automatically, in three layers:
+
+1. **Archive member selection.** A static `.a` is a bag of `.o` object files; the linker pulls in
+ *only* the ones that resolve a symbol you actually call. Unused modules are never included.
+2. **Dead-strip.** Within those, `-dead_strip` removes any function/data unreachable from your
+ entry points. Kotlin/Native Release and Xcode Release pass this automatically.
+3. **Symbol strip.** Release builds drop DWARF debug info into a separate `.dSYM` that isn't shipped.
+
+The one rule: **build Release.** Debug builds keep everything (that's expected — it's for dev);
+Release builds apply all three steps and produce the ~3 MB above. No flags to add. On Android the
+`.so` is dynamic so it can't dead-strip, but AGP strips its debug symbols on release and App Bundle
+ships only the device's ABI.
+
+> This is identical under [Bring your own libsignal](#bring-your-own-libsignal-apple) — stripping
+> always happens at *your* final link, so the BYO trust model costs you nothing in app size.
+
+## API parity with libsignal
| libsignal area | Krypton API | Status |
-|---|---|---|
-| Protocol (X3DH, Double Ratchet, sessions, pre-keys) | `krypton-protocol` | ✅ Implemented (JVM/Android/Apple) |
-| Fingerprints / safety numbers | `KryptonProtocol.safetyNumber(...)` | ✅ Implemented (JVM/Android) |
-| Sealed sender | `KryptonProtocol.sealedSender*` / `SealedSender` | ✅ Implemented (JVM/Android) |
-| zkgroup — group params, profile access key / version / commitment | `krypton-zkgroup` / `KryptonProtocol` | ✅ Implemented (JVM/Android) |
-| zkgroup — group cipher (encrypt member IDs / profile keys / blobs) | `ZkGroup.encrypt*` / `KryptonProtocol.group*` | ✅ Implemented (JVM/Android) |
+| --- | --- | --- |
+| Protocol — X3DH, Double Ratchet, sessions, pre-keys | `KryptonProtocol.encrypt`/`decrypt`, `processPreKeyBundle` | ✅ Real |
+| Safety numbers / fingerprints | `KryptonProtocol.safetyNumber(...)` | ✅ Real |
+| Sealed sender | `KryptonProtocol.sealedSender*` | ✅ Real |
+| zkgroup — params, profile key derivations, group cipher | `ZkGroup.*` / `KryptonProtocol.group*` | ✅ Real |
| zkgroup — server-issued credentials & membership proofs | — | ⛔ Not provided (needs a credential server) |
-| Standalone Double Ratchet | `krypton-double-ratchet` | ⛔ Fails loud — *libsignal has no standalone ratchet either*; use `KryptonProtocol.encrypt/decrypt` |
+| Standalone Double Ratchet | — | ⛔ Fails loud — *libsignal has no bare ratchet either*; the ratchet lives inside a session |
-> Safety numbers, sealed sender, and the client-side zkgroup features (params, profile derivations, **group cipher**) are backed by real libsignal (`NumericFingerprintGenerator`, `SealedSessionCipher`, `GroupSecretParams`/`ProfileKey`/`ClientZkGroupCipher`) and verified by real-crypto tests — including byte-for-byte cross-checks against libsignal and full encrypt→decrypt round-trips. On platforms where the native bridge hasn't wired a feature yet, it **fails loud** rather than faking.
->
-> Two rows are intentionally ⛔, for different reasons. The zkgroup **credential dance** (auth/profile-key credentials, membership presentations) needs a credential-issuing server, so it's **absent, not stubbed**. The **standalone double ratchet** fails loud because *libsignal itself exposes no bare ratchet* — the ratchet only runs inside a session; `KryptonProtocol.encrypt/decrypt` **is** the double ratchet (X3DH establishes the session, every message advances it), matching libsignal 1:1.
+Every ✅ is covered by a real-libsignal test, most with byte-for-byte cross-checks. The two ⛔
+rows are intentional, not gaps — the zkgroup credential dance needs a credential-issuing server,
+and libsignal itself exposes no standalone ratchet (`encrypt`/`decrypt` **is** the ratchet).
## Architecture
```
┌──────────────────────────────────────────────┐
-│ Your app → io.krypton:krypton (one dep) │
+│ Your app → io.krypton:krypton (one dep) │
├──────────────────────────────────────────────┤
-│ commonMain: Krypton.protocol { } │ ← one common API
+│ commonMain: KryptonProtocol │ ← one common API
│ encrypt(...) / decrypt(...) │
├──────────────────────────────────────────────┤
-│ expect/actual Bridge (per platform): │
-│ JVM/Android → libsignal Java SDK (JNI) │
-│ iOS/macOS → cinterop → libsignal_ffi │
+│ expect/actual bridge (per platform): │
+│ JVM / Android → libsignal Java SDK (JNI) │
+│ iOS / macOS → cinterop → libsignal_ffi │
├──────────────────────────────────────────────┤
│ Signal's audited libsignal core │
└──────────────────────────────────────────────┘
```
-## Keeping up with Signal
+## Staying in sync with Signal
+
+libsignal is pinned to **one** version across both tracks (`gradle/libs.versions.toml` →
+`libsignal`). The native `libsignal_ffi.a` is never committed — it's rebuilt from Signal's
+source at the pinned tag and embedded into the published artifacts. Three workflows keep it honest:
-libsignal is pinned to one version across both tracks (`gradle/libs.versions.toml` → `libsignal`), and `:krypton-protocol:verifyLibsignalVersion` (run by `./gradlew check`) fails the build if the native `libsignal_ffi.a` drifts from the JVM/Android Maven version. The [`check-libsignal-update`](.github/workflows/check-libsignal-update.yml) workflow opens a bump PR when Signal ships a release; [`build-libsignal-ffi`](.github/workflows/build-libsignal-ffi.yml) rebuilds the native libs for it.
+- **`dependabot.yml`** + **`check-libsignal-update.yml`** — observe new libsignal releases and open a version-bump PR.
+- **`ci.yml`** — on that PR, clones `signalapp/libsignal` at the new tag, rebuilds the `.a`, and runs the real-crypto tests on JVM + iOS, so an ABI change fails the PR instead of a release.
+- **`release.yml`** — on a `v*` tag, rebuilds the `.a` fresh and publishes every target to Maven Central.
-## Publishing
+A build-time guard (`:krypton-protocol:verifyLibsignalVersion`, wired into `check`) fails the
+build if the native and Maven versions ever drift, so an iOS user and an Android user can never
+silently end up on incompatible wire formats.
-Not yet published. To make the one-line install real, Krypton needs: CI that builds `libsignal_ffi.a` for each native target → links it into the per-target artifacts → publishes `io.krypton:krypton` (+ per-target variants) to Maven Central. See the workflows in `.github/workflows/`.
+## Building & testing
+
+```sh
+./gradlew :krypton-protocol:jvmTest # JVM real-crypto tests
+./gradlew :krypton-protocol:iosSimulatorArm64Test # iOS-simulator real-crypto tests
+./gradlew :krypton-protocol:verifyLibsignalVersion # native/Maven version guard
+scripts/build-libsignal-ffi.sh 0.86.5 # (re)build the Apple native libs
+```
+
+## Releasing
+
+A single `macos` job builds every target (Apple natively, the rest cross-compiled), so one
+runner publishes the whole library. Push a tag and CI handles the rest:
+
+```sh
+git tag v0.1.0 && git push --tags # triggers .github/workflows/release.yml
+```
+
+It needs four repo secrets: `MAVEN_CENTRAL_USERNAME`, `MAVEN_CENTRAL_PASSWORD`, `SIGNING_KEY`
+(ASCII-armored GPG key), `SIGNING_PASSWORD`.
## Modules
| Module | Description |
-|--------|-------------|
-| `krypton` | **The single dependency** — entry point `Krypton.protocol { }`, re-exports core+storage+protocol |
+| --- | --- |
+| **`krypton`** | The single dependency — re-exports `core` + `storage` + `protocol` |
| `krypton-core` | Result types, key types, encoding |
-| `krypton-storage` | Store interfaces + in-memory (platform stores are scaffolds) |
-| `krypton-protocol` | Signal Protocol (X3DH, sessions, encrypt/decrypt) + the platform bridges |
-| `krypton-sealed-sender` | Sealed-sender convenience wrapper (real on JVM/Android) |
-| `krypton-zkgroup` | Client-side zkgroup — group params, profile access key/version/commitment, and the group cipher (real on JVM/Android) |
-| `krypton-double-ratchet` | ⛔ fails loud — the ratchet runs inside `KryptonProtocol`/libsignal |
+| `krypton-storage` | Store interfaces + in-memory implementations |
+| `krypton-protocol` | The Signal Protocol + the per-platform libsignal bridges |
+| `krypton-sealed-sender` | Sealed-sender convenience wrapper |
+| `krypton-zkgroup` | Client-side zkgroup — params, profile derivations, group cipher |
+| `krypton-gradle-plugin` | `id("io.krypton.libsignal")` — fetches libsignal from Signal's source & wires Apple targets |
+
+## Contributing
+
+Issues and PRs are welcome. Please make sure the build stays green:
+
+```sh
+./gradlew check
+```
+
+## Acknowledgements
+
+Built on [signalapp/libsignal](https://github.com/signalapp/libsignal) — the real cryptography
+is entirely Signal's work. Krypton only provides the Kotlin Multiplatform bindings.
+
+## Cryptography / export notice
+
+This distribution includes cryptographic software (it links [libsignal](https://github.com/signalapp/libsignal)).
+The country in which you reside may restrict the import, possession, use, and/or re-export of
+encryption software. Before using it, check your country's laws, regulations, and policies. See
+ for more information.
+
+The U.S. Bureau of Industry and Security (BIS) classifies the underlying cryptographic functionality
+under ECCN **5D002.C.1**. As **publicly available, open-source software**, the form and manner of this
+distribution makes it eligible for export under the License Exception **ENC / TSU** (EAR §740.13).
+Krypton itself ships **no** Signal binary — the cryptographic library is obtained directly from
+Signal's official source; see [SECURITY.md](SECURITY.md). This notice is informational and is not
+legal advice.
## License
-AGPL-3.0-only (matching libsignal's license)
+Krypton is licensed under **AGPL-3.0-only**, matching libsignal. See [LICENSE](LICENSE).
+
+```
+Copyright (C) 2026 Ranbir Singh
+
+This program is free software: you can redistribute it and/or modify it under the
+terms of the GNU Affero General Public License as published by the Free Software
+Foundation, either version 3 of the License, or (at your option) any later version.
+```
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..3af378f
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,109 @@
+# Security
+
+## The native binary: Krypton ships none
+
+Krypton is **bindings, not cryptography**. It does not implement, modify, vendor, or redistribute
+any cryptographic binary. The actual crypto is [Signal's libsignal](https://github.com/signalapp/libsignal),
+and it reaches your build **directly from Signal** — never through a Krypton-hosted blob:
+
+| Track | Where the binary comes from | Who fetches it |
+| --- | --- | --- |
+| JVM / Android | Signal's official `org.signal:libsignal-*` jars on Maven Central | your Gradle build |
+| Apple (iOS/macOS) | Signal's official source / prebuilt (`scripts/fetch-libsignal-ffi.sh`) | **you, on your machine** |
+
+Krypton's published Apple artifacts contain only cinterop bindings (~88 KB) — **no `libsignal_ffi`
+binary is embedded.** There is therefore nothing of ours for anyone to suspect: if you don't trust
+a binary, you didn't get it from us.
+
+## How to verify what you're running
+
+- **JVM/Android:** the jars are published and signed by Signal under `org.signal` on Maven Central.
+- **Apple, from source (default, strongest):** `scripts/fetch-libsignal-ffi.sh ` clones the
+ official `signalapp/libsignal` repo at the `v` release tag and compiles `libsignal_ffi.a`
+ locally. The bytes are produced on *your* machine from Signal's own source — reproducible by anyone,
+ and nothing has to be trusted but the GitHub org. (The release tag is an annotated tag by a Signal
+ maintainer; it is not git-GPG-signed, so the trust anchor is the `signalapp` org + the commit it
+ points at, not a PGP signature.)
+- **Apple, prebuilt:** `… "" download` fetches Signal's official archive from
+ `build-artifacts.signal.org` (the same URL Signal's own CocoaPods integration uses) and, if you
+ set `LIBSIGNAL_FFI_PREBUILD_CHECKSUM`, verifies the archive against that SHA-256 and aborts on a
+ mismatch. Signal does **not** commit a checksum at the source tag (it's supplied via that env var),
+ so an unset checksum means the download is unverified — which is why building from source is the
+ default. Note the prebuilt archive is **iOS-only** (device + both simulators); macOS targets must
+ use the from-source path.
+- **Version lock-step:** `:krypton-protocol:verifyLibsignalVersion` (wired into `./gradlew check`)
+ fails the build if the libsignal version used for the native side ever differs from the
+ JVM/Android Maven version — so two peers can't silently land on incompatible wire formats.
+
+## Recommended setup (the safest practical default)
+
+| Your situation | Use | Why |
+| --- | --- | --- |
+| JVM / Android | nothing — just the dependency | the binary ships in Signal's own Maven jars |
+| Apple, you have (or can install) Rust | **`mode = "build"`** (the default) | compiled from Signal's source on your machine — reproducible, covers macOS too, nothing prebuilt to trust |
+| Apple, no Rust / want speed (iOS only) | `mode = "download"` **+ a pinned checksum** | Signal's official prebuilt, but only trustworthy once you pin its SHA-256 |
+
+**Bottom line: prefer `build`.** It is the only option where you trust *nothing but Signal's source
+and your own compiler*. Use `download` when Rust isn't available, but always pin the checksum (below)
+— an unpinned download is unverified.
+
+## Consumer safety checklist
+
+1. **Pin the version, and keep it matched.** Set `kryptonLibsignal { version.set("") }` to the
+ exact libsignal version Krypton targets (see the README badge / `gradle/libs.versions.toml`).
+ `./gradlew check` runs `verifyLibsignalVersion` and fails on a mismatch — leave that wired in.
+2. **Prefer building from source** (`mode = "build"`, the default). The bytes are produced locally
+ from `signalapp/libsignal` at the `v` tag.
+3. **If you download, pin the checksum.** Set `LIBSIGNAL_FFI_PREBUILD_CHECKSUM` so a tampered or
+ swapped archive aborts the build. Never ship a build that downloaded unverified.
+4. **Verify once, in CI, not just locally.** Run the fetch in CI so the binary your releases link
+ against is produced by an auditable, repeatable job — not whatever happens to be on a laptop.
+5. **Re-derive trust, don't take ours.** You can reproduce everything yourself (next section); you
+ never have to trust Krypton for the cryptographic binary.
+
+## Getting a checksum you can trust
+
+Signal does **not** publish a stable, committed SHA-256 for the prebuilt archive (it's passed via an
+env var). So establish your own ground truth:
+
+```sh
+# Build the binary from Signal's source ONCE (this is the authoritative artifact):
+scripts/fetch-libsignal-ffi.sh 0.86.5 # build mode
+
+# …or hash the official prebuilt you intend to pin:
+curl -fsSL https://build-artifacts.signal.org/libraries/libsignal-client-ios-build-v0.86.5.tar.gz \
+ | shasum -a 256
+```
+
+Record that SHA-256 in your own repo/CI and feed it to every subsequent `download` build via
+`LIBSIGNAL_FFI_PREBUILD_CHECKSUM`. Now any future download that doesn't match *your* pinned value
+fails — you're trusting a value you verified, not one handed to you at fetch time.
+
+## Reproduce it yourself
+
+Don't trust — verify. Anyone can confirm the binary is genuine libsignal:
+
+```sh
+# 1. Build from Signal's source and confirm it links Krypton's bindings:
+scripts/fetch-libsignal-ffi.sh 0.86.5
+nm ~/.krypton/libsignal/0.86.5/ios-arm64/libsignal_ffi.a | grep -c ' T _signal_' # exported FFI symbols
+
+# 2. Two independent builders should land on the same archive contents for the same tag.
+```
+
+The release tag is `signalapp/libsignal@v` (an annotated tag by a Signal maintainer; not
+git-GPG-signed, so the anchor is the org + commit, not a PGP signature). Krypton itself never enters
+the trust path for the cryptographic bytes.
+
+## Scope and limitations
+
+Krypton is unaudited binding code over an audited core. The cryptographic guarantees are
+libsignal's; Krypton's responsibility is to pass data through faithfully and to **fail loud**
+(never fake a result) where a platform has no libsignal. Review the bindings and the FFI bridge
+(`krypton-protocol/src/appleMain`) before relying on them in production.
+
+## Reporting a vulnerability
+
+Please report security issues privately via GitHub Security Advisories on this repository
+(**Security → Report a vulnerability**) rather than a public issue. For vulnerabilities in the
+cryptography itself, report to the [libsignal project](https://github.com/signalapp/libsignal).
diff --git a/STATUS.md b/STATUS.md
index 20699ab..d7baa60 100644
--- a/STATUS.md
+++ b/STATUS.md
@@ -1,6 +1,6 @@
# Krypton — Project State
-_Last updated: 2026-06-12 · libsignal pinned to **0.86.5** · branch `master` @ `e69596e`_
+_Last updated: 2026-06-14 · libsignal pinned to **0.86.5** · branch `master`_
A snapshot of what's real, what's verified, and what's pending. The rule for this
project: **no fake crypto.** Every feature is either backed by real libsignal and
@@ -51,16 +51,34 @@ placeholder.
|---|---|---|
| JVM (desktop) | ✅ Real, verified | Signal's Maven jars (bundled natives) |
| Android | ✅ Real | `libsignal-android` (same bridge as JVM) |
-| iOS / macOS (+ tvOS) | 🟡 Partial | cinterop → `libsignal_ffi`; **send path verified, decrypt WIP** (see below) |
+| iOS / macOS (+ tvOS) | ✅ Real, verified | cinterop → `libsignal_ffi`; **send + decrypt round-trip verified** (see below) |
| Linux / Windows (native) | ⚠️ Fail-loud | no libsignal binary; use JVM for desktop |
| wasmJs | ⚠️ Fail-loud | libsignal has no wasm build |
-Full, test-verified E2EE runs on **JVM and Android**. On **Apple**, real
-libsignal_ffi is wired and the send path is proven by a real-FFI test
-(`NativeRealCryptoTest`): genuine ~1568-byte Kyber pre-key, X3DH handshake, and
-encrypt all succeed. The PreKey **decrypt** path still returns libsignal error 30
-(`InvalidMessage`) and is tracked as an `@Ignore`d test — **don't ship Apple E2EE
-until that's green.**
+Full, test-verified E2EE runs on **JVM, Android, iOS, and macOS**. On **Apple**,
+real libsignal_ffi 0.86.5 is wired and the **full round-trip** is proven by a
+real-FFI test (`NativeRealCryptoTest`, run on every Apple target via `appleTest`):
+genuine ~1568-byte Kyber pre-key, X3DH handshake, encrypt, **and PreKey decrypt
+back to plaintext** all succeed — matching the JVM/Android tracks.
+
+Per-arch native binaries are built and linked (each Kotlin target links its
+own-arch `libsignal_ffi.a` from `libs/apple//`):
+
+| Arch dir | Triple | Kotlin target | Status |
+|---|---|---|---|
+| `macos-arm64` | `aarch64-apple-darwin` | `macosArm64` | ✅ round-trip **runs** (host test) |
+| `ios-sim-arm64` | `aarch64-apple-ios-sim` | `iosSimulatorArm64` | ✅ round-trip **runs** on the iOS simulator |
+| `ios-arm64` | `aarch64-apple-ios` | `iosArm64` (device) | ✅ test executable **links** as `platform IOS` (run needs a physical device) |
+
+Other Apple arches (`*X64`, `tvos*`) compile against a fallback `.a` but aren't
+link/run-verified here — build their own-arch `.a` (same script) to ship them.
+
+> The earlier PreKey-decrypt failure (libsignal error 30 / `InvalidMessage`) was a
+> binary/header version drift: the native bridge had been built against a newer
+> mislabeled `libsignal_ffi.a` whose store-callback ABI (phantom `local_address`
+> params, `destroy` struct fields, pair-returning identity callback) didn't match
+> the 0.86.5 Rust core. Fixed by rebuilding the real 0.86.5 `.a` from source and
+> converting the entire `appleMain` bridge to the 0.86.5 callback ABI.
---
@@ -81,19 +99,20 @@ until that's green.**
## Pending / next up
-1. **Maven publishing** — wire CI to build `libsignal_ffi.a` per native target →
- link into per-target artifacts → publish `io.krypton:krypton` to Maven Central,
- so the one-line install becomes real. (Workflows scaffolded in `.github/workflows/`,
- not yet run — repo is private.)
-2. **Apple native decrypt** — the zeroed-Kyber placeholder is now **fixed** (real
- Kyber key generated, signed, serialized into the bundle, and accepted by X3DH;
- verified by `NativeRealCryptoTest`). Remaining: the PreKey **decrypt** path
- returns `InvalidMessage` (error 30) — the receive-path FFI store wiring needs
- debugging before Apple E2EE is shippable.
-3. **Group messaging (sender keys)** — `groupEncrypt/groupDecrypt` are not wired yet.
-4. _(Optional)_ zkgroup credential dance — implementable + locally testable if a
+1. **Maven publishing** — the per-arch native build + link is now proven locally
+ (`scripts/build-libsignal-ffi.sh` produces `macos-arm64` / `ios-arm64` /
+ `ios-sim-arm64`, each links into its Kotlin target). Remaining: run that in CI →
+ publish `io.krypton:krypton` (+ per-target variants) to Maven Central so the
+ one-line install becomes real. (Workflows scaffolded in `.github/workflows/`,
+ not yet run — repo is private.) Building the remaining ship arches (`iosX64`
+ sim-x64, `tvos*`) is the same script with more triples.
+2. **Group messaging (sender keys)** — `groupEncrypt/groupDecrypt` are not wired yet.
+3. _(Optional)_ zkgroup credential dance — implementable + locally testable if a
compatible server is ever in scope.
+> **Done:** Apple native E2EE (incl. the PreKey **decrypt** receive path) now
+> works end-to-end on real `libsignal_ffi` 0.86.5 — see Platform support above.
+
---
## How to verify the current state yourself
@@ -102,6 +121,14 @@ until that's green.**
# Real-libsignal crypto tests (X3DH, safety numbers, sealed sender, zkgroup)
./gradlew :krypton-protocol:jvmTest --tests "io.krypton.protocol.VerifiedProductionTest"
+# Real native Apple E2EE — full encrypt→decrypt round-trip on libsignal_ffi
+./gradlew :krypton-protocol:macosArm64Test # runs on macOS host
+./gradlew :krypton-protocol:iosSimulatorArm64Test # runs on the iOS simulator
+./gradlew :krypton-protocol:linkDebugTestIosArm64 # links the iOS-device arch
+
+# (Re)build the per-arch native libs — macos-arm64, ios-arm64, ios-sim-arm64
+scripts/build-libsignal-ffi.sh 0.86.5
+
# zkgroup wrapper tests
./gradlew :krypton-zkgroup:jvmTest
diff --git a/build.gradle.kts b/build.gradle.kts
index 61b862e..d067805 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,7 +1,10 @@
plugins {
alias(libs.plugins.kotlin.multiplatform) apply false
+ alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
+ alias(libs.plugins.compose.multiplatform) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.vanniktech.publish) apply false
}
diff --git a/buildSrc/src/main/kotlin/io/krypton/Configuration.kt b/buildSrc/src/main/kotlin/io/krypton/Configuration.kt
index e4b7b42..aacdc85 100644
--- a/buildSrc/src/main/kotlin/io/krypton/Configuration.kt
+++ b/buildSrc/src/main/kotlin/io/krypton/Configuration.kt
@@ -1,7 +1,7 @@
package io.krypton
object Configuration {
- const val GROUP = "io.krypton"
+ const val GROUP = "io.github.androidpoet"
const val VERSION = "0.1.0"
const val COMPILE_SDK = 35
const val MIN_SDK = 21
diff --git a/gradle.properties b/gradle.properties
index 5fd0c55..855bbec 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -7,3 +7,26 @@ android.useAndroidX=true
kotlin.mpp.applyDefaultHierarchyTemplate=false
kotlin.mpp.enableCInteropCommonization=true
kotlin.mpp.enableCInteropCommonization.nowarn=true
+
+# ── Maven publishing (vanniktech) ────────────────────────────────────────────
+# Publish to the Sonatype Central Portal; sign release artifacts with GPG.
+# Per-module coordinates come from each module's mavenPublishing { coordinates(...) }.
+SONATYPE_HOST=CENTRAL_PORTAL
+RELEASE_SIGNING_ENABLED=true
+
+POM_NAME=Krypton
+POM_DESCRIPTION=Kotlin Multiplatform Signal Protocol library backed by real libsignal (X3DH, Double Ratchet, sealed sender, zkgroup)
+POM_INCEPTION_YEAR=2026
+POM_URL=https://github.com/AndroidPoet/Krypton
+
+POM_LICENSE_NAME=GNU Affero General Public License v3.0 only
+POM_LICENSE_URL=https://www.gnu.org/licenses/agpl-3.0.txt
+POM_LICENSE_DIST=repo
+
+POM_SCM_URL=https://github.com/AndroidPoet/Krypton
+POM_SCM_CONNECTION=scm:git:git://github.com/AndroidPoet/Krypton.git
+POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/AndroidPoet/Krypton.git
+
+POM_DEVELOPER_ID=androidpoet
+POM_DEVELOPER_NAME=Ranbir Singh
+POM_DEVELOPER_URL=https://github.com/AndroidPoet
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 24e289e..007194b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -6,6 +6,8 @@ serialization = "1.7.3"
ktor = "3.0.3"
compose-bom = "2024.12.01"
activity-compose = "1.9.3"
+compose-multiplatform = "1.7.3"
+vanniktech = "0.30.0"
# Single source of truth for libsignal across BOTH tracks:
# - JVM/Android: the Maven coordinates below (version.ref = "libsignal")
# - Apple/native: the prebuilt libsignal_ffi.a, whose version is recorded in
@@ -26,8 +28,11 @@ libsignal-android = { module = "org.signal:libsignal-android", version.ref = "li
[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
+kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
+vanniktech-publish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktech" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..1b33c55
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/krypton-core/build.gradle.kts b/krypton-core/build.gradle.kts
index fd5fe95..47ec2f1 100644
--- a/krypton-core/build.gradle.kts
+++ b/krypton-core/build.gradle.kts
@@ -1,8 +1,11 @@
@file:OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
+import io.krypton.Configuration
+
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.android.library)
+ alias(libs.plugins.vanniktech.publish)
}
kotlin {
@@ -16,9 +19,6 @@ kotlin {
iosSimulatorArm64()
macosX64()
macosArm64()
- tvosX64()
- tvosArm64()
- tvosSimulatorArm64()
linuxX64()
mingwX64()
wasmJs { browser() }
@@ -39,3 +39,7 @@ android {
compileSdk = io.krypton.Configuration.COMPILE_SDK
defaultConfig { minSdk = io.krypton.Configuration.MIN_SDK }
}
+
+mavenPublishing {
+ coordinates(Configuration.GROUP, "krypton-core", Configuration.VERSION)
+}
diff --git a/krypton-gradle-plugin/build.gradle.kts b/krypton-gradle-plugin/build.gradle.kts
new file mode 100644
index 0000000..024c6cb
--- /dev/null
+++ b/krypton-gradle-plugin/build.gradle.kts
@@ -0,0 +1,36 @@
+import io.krypton.Configuration
+
+plugins {
+ `java-gradle-plugin`
+ alias(libs.plugins.kotlin.jvm)
+ alias(libs.plugins.vanniktech.publish)
+}
+
+group = Configuration.GROUP
+version = Configuration.VERSION
+
+kotlin { jvmToolchain(17) }
+
+mavenPublishing {
+ // Publishes the plugin + its marker so consumers can apply
+ // plugins { id("io.krypton.libsignal") version "" }
+ coordinates(Configuration.GROUP, "krypton-gradle-plugin", Configuration.VERSION)
+}
+
+dependencies {
+ // Needs the Kotlin MPP types to auto-wire linkerOpts onto Apple native targets.
+ // compileOnly: the consumer's build already brings the Kotlin Gradle plugin.
+ compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:${libs.versions.kotlin.get()}")
+}
+
+gradlePlugin {
+ plugins {
+ create("kryptonLibsignal") {
+ id = "io.krypton.libsignal"
+ implementationClass = "io.krypton.gradle.LibsignalPlugin"
+ displayName = "Krypton — bring-your-own-libsignal"
+ description = "Fetches libsignal_ffi from Signal's official source on the consumer's " +
+ "machine and wires it onto Apple native targets. Krypton ships no Signal binary."
+ }
+ }
+}
diff --git a/krypton-gradle-plugin/src/main/kotlin/io/krypton/gradle/LibsignalPlugin.kt b/krypton-gradle-plugin/src/main/kotlin/io/krypton/gradle/LibsignalPlugin.kt
new file mode 100644
index 0000000..9ec1109
--- /dev/null
+++ b/krypton-gradle-plugin/src/main/kotlin/io/krypton/gradle/LibsignalPlugin.kt
@@ -0,0 +1,145 @@
+package io.krypton.gradle
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.provider.Property
+import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
+import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
+import org.jetbrains.kotlin.konan.target.KonanTarget
+import java.io.File
+
+/**
+ * `kryptonLibsignal { ... }` — configure how libsignal_ffi is obtained.
+ *
+ * The libsignal `version` MUST match the version Krypton is pinned to.
+ */
+abstract class KryptonLibsignalExtension {
+ /** libsignal version, e.g. "0.86.5". Must equal Krypton's pinned version. */
+ abstract val version: Property
+
+ /** "build" (git clone signalapp/libsignal + cargo) or "download" (official prebuilt). */
+ abstract val mode: Property
+
+ /** Root cache dir; the .a lands in ///libsignal_ffi.a */
+ abstract val cacheDir: Property
+}
+
+/**
+ * Provides `fetchLibsignal` and auto-wires the fetched libsignal_ffi.a onto every
+ * Apple native target, so a consumer just does:
+ *
+ * plugins { id("io.krypton.libsignal") version "0.1.0" }
+ * kryptonLibsignal { version.set("0.86.5") }
+ *
+ * Krypton ships no Signal binary; this generates it from Signal's official source
+ * on the consumer's own machine.
+ */
+class LibsignalPlugin : Plugin {
+ override fun apply(project: Project) {
+ val ext = project.extensions.create("kryptonLibsignal", KryptonLibsignalExtension::class.java)
+ ext.version.convention("0.86.5")
+ ext.mode.convention("build")
+ ext.cacheDir.convention("${System.getProperty("user.home")}/.krypton/libsignal")
+
+ val fetch = project.tasks.register("fetchLibsignal") { task ->
+ task.group = "krypton"
+ task.description = "Fetch libsignal_ffi from Signal's official source into the cache."
+ task.doLast {
+ val out = File(ext.cacheDir.get(), ext.version.get())
+ fetchLibsignal(project, ext.version.get(), ext.mode.get(), out)
+ }
+ }
+
+ // Auto-wire onto every Apple native target once Kotlin Multiplatform is applied.
+ project.plugins.withId("org.jetbrains.kotlin.multiplatform") {
+ val kmp = project.extensions.getByType(KotlinMultiplatformExtension::class.java)
+ val base = File(ext.cacheDir.get(), ext.version.get())
+ kmp.targets.withType(KotlinNativeTarget::class.java).configureEach { target ->
+ val arch = appleArchDir(target.konanTarget) ?: return@configureEach
+ target.binaries.configureEach { binary ->
+ binary.linkerOpts("-L${File(base, arch).absolutePath}")
+ binary.linkTaskProvider.configure { it.dependsOn(fetch) }
+ }
+ }
+ }
+ }
+
+ /** KonanTarget -> the arch subdir Krypton's cinterop expects. null = not Apple. */
+ private fun appleArchDir(t: KonanTarget): String? = when (t) {
+ KonanTarget.MACOS_ARM64 -> "macos-arm64"
+ KonanTarget.MACOS_X64 -> "macos-x64"
+ KonanTarget.IOS_ARM64 -> "ios-arm64"
+ KonanTarget.IOS_SIMULATOR_ARM64 -> "ios-sim-arm64"
+ KonanTarget.IOS_X64 -> "ios-sim-x64"
+ else -> null
+ }
+
+ /** triple -> arch subdir, for cargo build mode. */
+ private val triples = listOf(
+ "aarch64-apple-darwin" to "macos-arm64",
+ "x86_64-apple-darwin" to "macos-x64",
+ "aarch64-apple-ios" to "ios-arm64",
+ "aarch64-apple-ios-sim" to "ios-sim-arm64",
+ "x86_64-apple-ios" to "ios-sim-x64",
+ )
+
+ private fun fetchLibsignal(project: Project, version: String, mode: String, out: File) {
+ out.mkdirs()
+ project.logger.lifecycle("Krypton: fetching libsignal $version ($mode) -> $out")
+ when (mode) {
+ "download" -> downloadOfficial(project, version, out)
+ else -> buildFromSource(project, version, out)
+ }
+ }
+
+ private fun buildFromSource(project: Project, version: String, out: File) {
+ val work = File(System.getProperty("java.io.tmpdir"), "libsignal-$version")
+ if (!File(work, ".git").exists()) {
+ project.exec {
+ it.commandLine(
+ "git", "clone", "--depth", "1", "--branch", "v$version",
+ "https://github.com/signalapp/libsignal", work.absolutePath,
+ )
+ }
+ }
+ for ((triple, arch) in triples) {
+ project.exec { it.workingDir(work); it.commandLine("rustup", "target", "add", triple); it.isIgnoreExitValue = true }
+ project.exec { it.workingDir(work); it.commandLine("cargo", "build", "-p", "libsignal-ffi", "--release", "--target", triple) }
+ val a = File(work, "target/$triple/release/libsignal_ffi.a")
+ val dst = File(out, arch).apply { mkdirs() }
+ a.copyTo(File(dst, "libsignal_ffi.a"), overwrite = true)
+ }
+ }
+
+ private fun downloadOfficial(project: Project, version: String, out: File) {
+ val archive = "libsignal-client-ios-build-v$version.tar.gz"
+ val url = "https://build-artifacts.signal.org/libraries/$archive"
+ val dest = File(out, archive)
+ project.logger.lifecycle("Krypton: downloading Signal's official prebuilt: $url")
+ java.net.URL(url).openStream().use { input -> dest.outputStream().use { input.copyTo(it) } }
+ val expected = System.getenv("LIBSIGNAL_FFI_PREBUILD_CHECKSUM")
+ if (!expected.isNullOrBlank()) {
+ val actual = java.security.MessageDigest.getInstance("SHA-256")
+ .digest(dest.readBytes()).joinToString("") { "%02x".format(it) }
+ check(actual == expected) { "libsignal checksum mismatch: expected $expected, got $actual" }
+ project.logger.lifecycle("Krypton: checksum verified against Signal's published SHA-256.")
+ } else {
+ project.logger.warn("Krypton: no LIBSIGNAL_FFI_PREBUILD_CHECKSUM set — download is UNVERIFIED.")
+ }
+ project.exec { it.commandLine("tar", "-xzf", dest.absolutePath, "-C", out.absolutePath) }
+ // Signal's archive lays the .a out as target//release/libsignal_ffi.a; relocate each
+ // into the / layout the linker auto-wiring expects. The prebuilt is iOS-only.
+ for ((triple, arch) in triples) {
+ val extracted = File(out, "target/$triple/release/libsignal_ffi.a")
+ if (extracted.exists()) {
+ val dst = File(out, arch).apply { mkdirs() }
+ extracted.copyTo(File(dst, "libsignal_ffi.a"), overwrite = true)
+ }
+ }
+ File(out, "target").deleteRecursively()
+ dest.delete()
+ project.logger.lifecycle(
+ "Krypton: prebuilt is iOS-only (no macOS). For macOS targets use mode \"build\".",
+ )
+ }
+}
diff --git a/krypton-protocol/build.gradle.kts b/krypton-protocol/build.gradle.kts
index 1b3a1b3..bc2e679 100644
--- a/krypton-protocol/build.gradle.kts
+++ b/krypton-protocol/build.gradle.kts
@@ -1,10 +1,12 @@
@file:OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
+import io.krypton.Configuration
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeCompilation
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.android.library)
+ alias(libs.plugins.vanniktech.publish)
}
kotlin {
@@ -18,9 +20,6 @@ kotlin {
iosSimulatorArm64()
macosX64()
macosArm64()
- tvosX64()
- tvosArm64()
- tvosSimulatorArm64()
linuxX64()
mingwX64()
wasmJs { browser() }
@@ -81,37 +80,38 @@ kotlin {
// Connect each Apple target's main/test source sets to appleMain/appleTest
listOf("iosX64", "iosArm64", "iosSimulatorArm64",
- "macosX64", "macosArm64",
- "tvosX64", "tvosArm64", "tvosSimulatorArm64").forEach { target ->
+ "macosX64", "macosArm64").forEach { target ->
getByName("${target}Main").dependsOn(appleMain)
getByName("${target}Test").dependsOn(appleTest)
}
}
- // ── Cinterop: link libsignal_ffi on all Apple platforms ──────────────
- fun KotlinNativeCompilation.configureCInterop() {
- cinterops {
- val libsignalFfi by creating {
- defFile(project.file("src/appleMain/cinterop/libsignal_ffi.def"))
- packageName("org.signal.libsignal.ffi")
- compilerOpts("-I${project.projectDir}/libs/apple")
- }
+ // ── Cinterop: link libsignal_ffi on every Apple target ───────────────
+ // Each target links its OWN-arch static lib from libs/apple// and the
+ // `.def`'s `staticLibraries` directive EMBEDS that .a into the produced klib,
+ // BRING-YOUR-OWN-LIBSIGNAL (Model B): Krypton ships NO Signal binary. The
+ // cinterop generates bindings from headers only — the `.a` is never embedded
+ // in the published klib. Both Krypton's own tests AND consumers supply the
+ // libsignal_ffi.a at link time via -L, after fetching it from Signal's
+ // official source on their own machine (scripts/fetch-libsignal-ffi.sh).
+ fun org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget.configureLibsignal(libArchDir: String) {
+ val archDir = "${project.projectDir}/libs/apple/$libArchDir"
+ compilations.getByName("main").cinterops.create("libsignalFfi") {
+ defFile(project.file("src/appleMain/cinterop/libsignal_ffi.def"))
+ packageName("org.signal.libsignal.ffi")
+ compilerOpts("-I${project.projectDir}/libs/apple")
}
+ // Local -L so Krypton's own test executables link the fetched .a. The
+ // published klib carries only `-lsignal_ffi`; consumers add their own -L.
+ binaries.all { linkerOpts("-L$archDir") }
}
- // macOS (host architectures)
- macosArm64 { compilations.getByName("main").configureCInterop() }
- macosX64 { compilations.getByName("main").configureCInterop() }
-
- // iOS targets
- iosArm64 { compilations.getByName("main").configureCInterop() }
- iosX64 { compilations.getByName("main").configureCInterop() }
- iosSimulatorArm64 { compilations.getByName("main").configureCInterop() }
-
- // tvOS targets
- tvosArm64 { compilations.getByName("main").configureCInterop() }
- tvosX64 { compilations.getByName("main").configureCInterop() }
- tvosSimulatorArm64 { compilations.getByName("main").configureCInterop() }
+ // Each Apple target links its own arch's libsignal_ffi.a (consumer-supplied).
+ macosArm64 { configureLibsignal("macos-arm64") } // Apple Silicon desktop
+ macosX64 { configureLibsignal("macos-x64") } // Intel desktop
+ iosArm64 { configureLibsignal("ios-arm64") } // device
+ iosSimulatorArm64 { configureLibsignal("ios-sim-arm64") } // Apple Silicon sim
+ iosX64 { configureLibsignal("ios-sim-x64") } // Intel sim
}
android {
@@ -120,6 +120,10 @@ android {
defaultConfig { minSdk = io.krypton.Configuration.MIN_SDK }
}
+mavenPublishing {
+ coordinates(Configuration.GROUP, "krypton-protocol", Configuration.VERSION)
+}
+
// ── libsignal version-skew guard ─────────────────────────────────────────────
// Asserts the Apple/native libsignal_ffi.a (recorded in libs/apple/VERSION) is
// the SAME libsignal version as the JVM/Android Maven coordinates (catalog).
diff --git a/krypton-protocol/libs/apple/krypton_ffi.h b/krypton-protocol/libs/apple/krypton_ffi.h
index e6b68c9..1b6917f 100644
--- a/krypton-protocol/libs/apple/krypton_ffi.h
+++ b/krypton-protocol/libs/apple/krypton_ffi.h
@@ -92,8 +92,6 @@ SignalFfiError *signal_publickey_serialize(SignalOwnedBuffer *out, SignalConstPo
SignalFfiError *signal_publickey_destroy(SignalMutPointerPublicKey p);
SignalFfiError *signal_publickey_clone(SignalMutPointerPublicKey *new_obj, SignalConstPointerPublicKey obj);
SignalFfiError *signal_publickey_compare(int32_t *out, SignalConstPointerPublicKey lhs, SignalConstPointerPublicKey rhs);
-SignalFfiError *signal_publickey_verify_signature(SignalOwnedBuffer *out, SignalConstPointerPublicKey key, SignalBorrowedBuffer message, SignalBorrowedBuffer signature);
-SignalFfiError *signal_publickey_verify_signature_for_multipart_message(SignalOwnedBuffer *out, SignalConstPointerPublicKey key, SignalBorrowedSliceOfBuffers message, SignalBorrowedBuffer signature);
// ── Private Key ───────────────────────────────────────────────────────
SignalFfiError *signal_privatekey_generate(SignalMutPointerPrivateKey *out);
@@ -130,6 +128,9 @@ SignalFfiError *signal_kyber_public_key_serialize(SignalOwnedBuffer *out, Signal
SignalFfiError *signal_kyber_public_key_destroy(SignalMutPointerKyberPublicKey p);
SignalFfiError *signal_kyber_pre_key_record_new(SignalMutPointerKyberPreKeyRecord *out, uint32_t id, uint64_t timestamp, SignalConstPointerKyberKeyPair key_pair, SignalBorrowedBuffer signature);
SignalFfiError *signal_kyber_pre_key_record_destroy(SignalMutPointerKyberPreKeyRecord p);
+SignalFfiError *signal_kyber_pre_key_record_serialize(SignalOwnedBuffer *out, SignalConstPointerKyberPreKeyRecord obj);
+SignalFfiError *signal_kyber_pre_key_record_deserialize(SignalMutPointerKyberPreKeyRecord *out, SignalBorrowedBuffer data);
+SignalFfiError *signal_kyber_pre_key_record_get_public_key(SignalMutPointerKyberPublicKey *out, SignalConstPointerKyberPreKeyRecord obj);
// ── PreKey Bundle ─────────────────────────────────────────────────────
SignalFfiError *signal_pre_key_bundle_new(SignalMutPointerPreKeyBundle *out, uint32_t registration_id, uint32_t device_id, uint32_t prekey_id, SignalConstPointerPublicKey prekey, uint32_t signed_prekey_id, SignalConstPointerPublicKey signed_prekey, SignalBorrowedBuffer signed_prekey_signature, SignalConstPointerPublicKey identity_key, uint32_t kyber_prekey_id, SignalConstPointerKyberPublicKey kyber_prekey, SignalBorrowedBuffer kyber_prekey_signature);
@@ -159,24 +160,26 @@ SignalFfiError *signal_message_destroy(SignalMutPointerSignalMessage p);
SignalFfiError *signal_pre_key_signal_message_deserialize(SignalMutPointerPreKeySignalMessage *out, SignalBorrowedBuffer data);
SignalFfiError *signal_pre_key_signal_message_destroy(SignalMutPointerPreKeySignalMessage p);
-// ── Store Callback Structs ────────────────────────────────────────────
+// ── Store Callback Structs (libsignal 0.86.5 ABI) ─────────────────────
+// NOTE: 0.86.5 callbacks take CONST address/record/key pointers, have NO
+// `destroy` field, and get_identity_key_pair returns only a PRIVATE key
+// (libsignal derives the public). save_identity returns IdentityChange
+// (0 = new/unchanged); is_trusted_identity returns 1=trusted / 0=untrusted.
typedef struct {
void *ctx;
- int (*load_session)(void *ctx, SignalMutPointerSessionRecord *out, SignalMutPointerProtocolAddress address);
- int (*store_session)(void *ctx, SignalMutPointerProtocolAddress address, SignalMutPointerSessionRecord record);
- void (*destroy)(void *ctx);
+ int (*load_session)(void *ctx, SignalMutPointerSessionRecord *out, SignalConstPointerProtocolAddress address);
+ int (*store_session)(void *ctx, SignalConstPointerProtocolAddress address, SignalConstPointerSessionRecord record);
} SignalSessionStore;
typedef struct { const SignalSessionStore *raw; } SignalConstPointerFfiSessionStoreStruct;
typedef struct {
void *ctx;
- int (*get_local_identity_key_pair)(void *ctx, SignalPairOfMutPointerPublicKeyMutPointerPrivateKey *out);
+ int (*get_identity_key_pair)(void *ctx, SignalMutPointerPrivateKey *out);
int (*get_local_registration_id)(void *ctx, uint32_t *out);
- int (*get_identity_key)(void *ctx, SignalMutPointerPublicKey *out, SignalMutPointerProtocolAddress address);
- int (*save_identity)(void *ctx, uint8_t *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key);
- int (*is_trusted_identity)(void *ctx, bool *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key, uint32_t direction);
- void (*destroy)(void *ctx);
+ int (*save_identity)(void *ctx, SignalConstPointerProtocolAddress address, SignalConstPointerPublicKey public_key);
+ int (*get_identity)(void *ctx, SignalMutPointerPublicKey *out, SignalConstPointerProtocolAddress address);
+ int (*is_trusted_identity)(void *ctx, SignalConstPointerProtocolAddress address, SignalConstPointerPublicKey public_key, uint32_t direction);
} SignalIdentityKeyStore;
typedef struct { const SignalIdentityKeyStore *raw; } SignalConstPointerFfiIdentityKeyStoreStruct;
@@ -184,9 +187,8 @@ typedef struct { const SignalIdentityKeyStore *raw; } SignalConstPointerFfiIdent
typedef struct {
void *ctx;
int (*load_pre_key)(void *ctx, SignalMutPointerPreKeyRecord *out, uint32_t id);
- int (*store_pre_key)(void *ctx, uint32_t id, SignalMutPointerPreKeyRecord record);
+ int (*store_pre_key)(void *ctx, uint32_t id, SignalConstPointerPreKeyRecord record);
int (*remove_pre_key)(void *ctx, uint32_t id);
- void (*destroy)(void *ctx);
} SignalPreKeyStore;
typedef struct { const SignalPreKeyStore *raw; } SignalConstPointerFfiPreKeyStoreStruct;
@@ -194,8 +196,7 @@ typedef struct { const SignalPreKeyStore *raw; } SignalConstPointerFfiPreKeyStor
typedef struct {
void *ctx;
int (*load_signed_pre_key)(void *ctx, SignalMutPointerSignedPreKeyRecord *out, uint32_t id);
- int (*store_signed_pre_key)(void *ctx, uint32_t id, SignalMutPointerSignedPreKeyRecord record);
- void (*destroy)(void *ctx);
+ int (*store_signed_pre_key)(void *ctx, uint32_t id, SignalConstPointerSignedPreKeyRecord record);
} SignalSignedPreKeyStore;
typedef struct { const SignalSignedPreKeyStore *raw; } SignalConstPointerFfiSignedPreKeyStoreStruct;
@@ -203,41 +204,17 @@ typedef struct { const SignalSignedPreKeyStore *raw; } SignalConstPointerFfiSign
typedef struct {
void *ctx;
int (*load_kyber_pre_key)(void *ctx, SignalMutPointerKyberPreKeyRecord *out, uint32_t id);
- int (*store_kyber_pre_key)(void *ctx, uint32_t id, SignalMutPointerKyberPreKeyRecord record);
- int (*mark_kyber_pre_key_used)(void *ctx, uint32_t id, uint32_t ec_prekey_id, SignalMutPointerPublicKey base_key);
- void (*destroy)(void *ctx);
+ int (*store_kyber_pre_key)(void *ctx, uint32_t id, SignalConstPointerKyberPreKeyRecord record);
+ int (*mark_kyber_pre_key_used)(void *ctx, uint32_t id, uint32_t signed_prekey_id, SignalConstPointerPublicKey base_key);
} SignalKyberPreKeyStore;
typedef struct { const SignalKyberPreKeyStore *raw; } SignalConstPointerFfiKyberPreKeyStoreStruct;
-// ── Krypton adapter functions ─────────────────────────────────────────
-// These thin wrappers bridge the const/mut type gap that Kotlin/Native
-// cinterop cannot handle directly. Declared here so cinterop picks them up.
-// Implemented in krypton_adapter.c, compiled into libkrypton_adapter.a.
-
-SignalFfiError *krypton_address_get_name(const char **out, SignalMutPointerProtocolAddress addr);
-SignalFfiError *krypton_address_get_device_id(uint32_t *out, SignalMutPointerProtocolAddress addr);
-SignalFfiError *krypton_session_record_serialize(SignalOwnedBuffer *out, SignalMutPointerSessionRecord record);
-SignalFfiError *krypton_publickey_serialize(SignalOwnedBuffer *out, SignalMutPointerPublicKey key);
-SignalFfiError *krypton_privatekey_serialize(SignalOwnedBuffer *out, SignalMutPointerPrivateKey key);
-SignalFfiError *krypton_build_identity_key_pair(
- SignalPairOfMutPointerPublicKeyMutPointerPrivateKey *out,
- SignalBorrowedBuffer pub_key_data,
- SignalBorrowedBuffer priv_key_data
-);
-SignalFfiError *krypton_kyber_pre_key_record_new_from_raw(
- SignalMutPointerKyberPreKeyRecord *out,
- uint32_t id, uint64_t timestamp,
- void *kyber_key_pair_raw,
- SignalBorrowedBuffer signature
-);
-void krypton_error_free(SignalFfiError *err);
-
-// ── Core Protocol Functions ───────────────────────────────────────────
-SignalFfiError *signal_process_prekey_bundle(SignalConstPointerPreKeyBundle bundle, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerProtocolAddress local_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, uint64_t now);
-
-SignalFfiError *signal_encrypt_message(SignalMutPointerCiphertextMessage *out, SignalBorrowedBuffer ptext, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerProtocolAddress local_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, uint64_t now);
-
-SignalFfiError *signal_decrypt_message(SignalOwnedBuffer *out, SignalConstPointerSignalMessage message, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerProtocolAddress local_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store);
-
-SignalFfiError *signal_decrypt_pre_key_message(SignalOwnedBuffer *out, SignalConstPointerPreKeySignalMessage message, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerProtocolAddress local_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, SignalConstPointerFfiPreKeyStoreStruct prekey_store, SignalConstPointerFfiSignedPreKeyStoreStruct signed_prekey_store, SignalConstPointerFfiKyberPreKeyStoreStruct kyber_prekey_store);
+// ── Core Protocol Functions (libsignal 0.86.5 — no local_address) ─────
+SignalFfiError *signal_process_prekey_bundle(SignalConstPointerPreKeyBundle bundle, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, uint64_t now);
+
+SignalFfiError *signal_encrypt_message(SignalMutPointerCiphertextMessage *out, SignalBorrowedBuffer ptext, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, uint64_t now);
+
+SignalFfiError *signal_decrypt_message(SignalOwnedBuffer *out, SignalConstPointerSignalMessage message, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store);
+
+SignalFfiError *signal_decrypt_pre_key_message(SignalOwnedBuffer *out, SignalConstPointerPreKeySignalMessage message, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, SignalConstPointerFfiPreKeyStoreStruct prekey_store, SignalConstPointerFfiSignedPreKeyStoreStruct signed_prekey_store, SignalConstPointerFfiKyberPreKeyStoreStruct kyber_prekey_store);
diff --git a/krypton-protocol/libs/apple/signal_ffi.h b/krypton-protocol/libs/apple/signal_ffi.h
index 058ddb8..b5c6afb 100644
--- a/krypton-protocol/libs/apple/signal_ffi.h
+++ b/krypton-protocol/libs/apple/signal_ffi.h
@@ -37,8 +37,6 @@ SPDX-License-Identifier: AGPL-3.0-only
#define SignalAes256GcmDecryption_NONCE_SIZE SignalNONCE_SIZE
-#define SignalCallLinkSecretParams_ROOT_KEY_MAX_BYTES_FOR_SHO 16
-
#define SignalNUM_AUTH_CRED_ATTRIBUTES 3
#define SignalNUM_PROFILE_KEY_CRED_ATTRIBUTES 4
@@ -243,7 +241,6 @@ typedef enum {
SignalErrorCodeChatServiceInactive = 149,
SignalErrorCodeRequestTimedOut = 150,
SignalErrorCodeRateLimitChallenge = 151,
- SignalErrorCodePossibleCaptiveNetwork = 152,
SignalErrorCodeSvrDataMissing = 160,
SignalErrorCodeSvrRestoreFailed = 161,
SignalErrorCodeSvrRotationMachineTooManySteps = 162,
@@ -268,8 +265,6 @@ typedef enum {
SignalErrorCodeKeyTransparencyVerificationFailed = 211,
SignalErrorCodeRequestUnauthorized = 220,
SignalErrorCodeMismatchedDevices = 221,
- SignalErrorCodeServiceIdNotFound = 222,
- SignalErrorCodeUploadTooLarge = 223,
} SignalErrorCode;
enum SignalSvr2CredentialsResult {
@@ -354,8 +349,6 @@ typedef struct SignalPrivateKey SignalPrivateKey;
*/
typedef struct SignalProtocolAddress SignalProtocolAddress;
-typedef struct SignalProvisioningChatConnection SignalProvisioningChatConnection;
-
typedef struct SignalPublicKey SignalPublicKey;
typedef struct SignalRegisterAccountRequest SignalRegisterAccountRequest;
@@ -520,59 +513,15 @@ typedef struct {
const SignalAuthenticatedChatConnection *raw;
} SignalConstPointerAuthenticatedChatConnection;
-/**
- * A type alias to be used with [`OwnedBufferOf`], so that `OwnedBufferOf` and
- * `OwnedBufferOf<*const c_char>` get distinct names.
- */
-typedef const char *SignalCStringPtr;
-
-/**
- * A representation of a array allocated on the Rust heap for use in C code.
- */
-typedef struct {
- SignalCStringPtr *base;
- /**
- * The number of elements in the buffer (not necessarily the number of bytes).
- */
- size_t length;
-} SignalOwnedBufferOfCStringPtr;
-
-typedef struct {
- uint32_t cdn;
- SignalCStringPtr key;
- SignalOwnedBufferOfCStringPtr header_keys;
- SignalOwnedBufferOfCStringPtr header_values;
- SignalCStringPtr signed_upload_url;
-} SignalFfiUploadForm;
-
-/**
- * A C callback used to report the results of Rust futures.
- *
- * cbindgen will produce independent C types like `SignalCPromisei32` and
- * `SignalCPromiseProtocolAddress`.
- *
- * This derives Copy because it behaves like a C type; nevertheless, a promise should still only be
- * completed once.
- */
-typedef struct {
- void (*complete)(SignalFfiError *error, const SignalFfiUploadForm *result, const void *context);
- const void *context;
- SignalCancellationId cancellation_id;
-} SignalCPromiseFfiUploadForm;
-
typedef SignalConnectionInfo SignalChatConnectionInfo;
typedef struct {
SignalChatConnectionInfo *raw;
} SignalMutPointerChatConnectionInfo;
-typedef struct {
- SignalServerMessageAck *raw;
-} SignalMutPointerServerMessageAck;
-
-typedef int (*SignalFfiChatListenerReceivedIncomingMessage)(void *ctx, SignalOwnedBuffer envelope, uint64_t timestamp, SignalMutPointerServerMessageAck ack);
+typedef void (*SignalReceivedIncomingMessage)(void *ctx, SignalOwnedBuffer envelope, uint64_t timestamp_millis, SignalServerMessageAck *cleanup);
-typedef int (*SignalFfiChatListenerReceivedQueueEmpty)(void *ctx);
+typedef void (*SignalReceivedQueueEmpty)(void *ctx);
/**
* A representation of a array allocated on the Rust heap for use in C code.
@@ -592,25 +541,57 @@ typedef struct {
typedef SignalBytestringArray SignalStringArray;
-typedef int (*SignalFfiChatListenerReceivedAlerts)(void *ctx, SignalStringArray alerts);
+typedef void (*SignalReceivedAlerts)(void *ctx, SignalStringArray alerts);
-typedef int (*SignalFfiChatListenerConnectionInterrupted)(void *ctx, SignalFfiError *disconnect_cause);
+typedef void (*SignalConnectionInterrupted)(void *ctx, SignalFfiError *error);
-typedef void (*SignalFfiChatListenerDestroy)(void *ctx);
+typedef void (*SignalDestroyChatListener)(void *ctx);
+/**
+ * Callbacks for [`ChatListener`].
+ *
+ * Callbacks will be serialized (i.e. two calls will not come in at the same time), but may not
+ * always happen on the same thread. Calls should be responded to promptly to avoid blocking later
+ * messages.
+ *
+ * # Safety
+ *
+ * This type contains raw pointers. Code that constructs an instance of this type must ensure
+ * memory safety assuming that
+ * - the callback function pointer fields are called with `ctx` as an argument;
+ * - the `destroy` function pointer field is called with `ctx` as an argument;
+ * - no function pointer fields are called after `destroy` is called.
+ */
typedef struct {
void *ctx;
- SignalFfiChatListenerReceivedIncomingMessage received_incoming_message;
- SignalFfiChatListenerReceivedQueueEmpty received_queue_empty;
- SignalFfiChatListenerReceivedAlerts received_alerts;
- SignalFfiChatListenerConnectionInterrupted connection_interrupted;
- SignalFfiChatListenerDestroy destroy;
+ SignalReceivedIncomingMessage received_incoming_message;
+ SignalReceivedQueueEmpty received_queue_empty;
+ SignalReceivedAlerts received_alerts;
+ SignalConnectionInterrupted connection_interrupted;
+ SignalDestroyChatListener destroy;
} SignalFfiChatListenerStruct;
typedef struct {
const SignalFfiChatListenerStruct *raw;
} SignalConstPointerFfiChatListenerStruct;
+/**
+ * A type alias to be used with [`OwnedBufferOf`], so that `OwnedBufferOf` and
+ * `OwnedBufferOf<*const c_char>` get distinct names.
+ */
+typedef const char *SignalCStringPtr;
+
+/**
+ * A representation of a array allocated on the Rust heap for use in C code.
+ */
+typedef struct {
+ SignalCStringPtr *base;
+ /**
+ * The number of elements in the buffer (not necessarily the number of bytes).
+ */
+ size_t length;
+} SignalOwnedBufferOfCStringPtr;
+
typedef struct {
uint16_t status;
const char *message;
@@ -644,42 +625,6 @@ typedef struct {
*/
typedef uint8_t SignalServiceIdFixedWidthBinaryBytes[17];
-typedef struct {
- const uint32_t *base;
- size_t length;
-} SignalBorrowedSliceOfu32;
-
-typedef struct {
- const SignalCiphertextMessage *raw;
-} SignalConstPointerCiphertextMessage;
-
-typedef struct {
- const SignalConstPointerCiphertextMessage *base;
- size_t length;
-} SignalBorrowedSliceOfConstPointerCiphertextMessage;
-
-/**
- * A C callback used to report the results of Rust futures.
- *
- * cbindgen will produce independent C types like `SignalCPromisei32` and
- * `SignalCPromiseProtocolAddress`.
- *
- * This derives Copy because it behaves like a C type; nevertheless, a promise should still only be
- * completed once.
- */
-typedef struct {
- void (*complete)(SignalFfiError *error, const SignalOwnedBuffer *result, const void *context);
- const void *context;
- SignalCancellationId cancellation_id;
-} SignalCPromiseOwnedBufferOfc_uchar;
-
-/**
- * A wrapper type for raw UUIDs, because C treats arrays specially in argument position.
- */
-typedef struct {
- uint8_t bytes[16];
-} SignalUuid;
-
typedef struct {
SignalPrivateKey *raw;
} SignalMutPointerPrivateKey;
@@ -791,6 +736,10 @@ typedef struct {
const SignalPlaintextContent *raw;
} SignalConstPointerPlaintextContent;
+typedef struct {
+ const SignalCiphertextMessage *raw;
+} SignalConstPointerCiphertextMessage;
+
typedef struct {
SignalConnectionInfo *raw;
} SignalMutPointerConnectionInfo;
@@ -815,52 +764,49 @@ typedef struct {
SignalSessionRecord *raw;
} SignalMutPointerSessionRecord;
-typedef int (*SignalFfiSessionStoreLoadSession)(void *ctx, SignalMutPointerSessionRecord *out, SignalMutPointerProtocolAddress address);
+typedef int (*SignalLoadSession)(void *store_ctx, SignalMutPointerSessionRecord *recordp, SignalConstPointerProtocolAddress address);
-typedef int (*SignalFfiSessionStoreStoreSession)(void *ctx, SignalMutPointerProtocolAddress address, SignalMutPointerSessionRecord record);
+typedef struct {
+ const SignalSessionRecord *raw;
+} SignalConstPointerSessionRecord;
-typedef void (*SignalFfiSessionStoreDestroy)(void *ctx);
+typedef int (*SignalStoreSession)(void *store_ctx, SignalConstPointerProtocolAddress address, SignalConstPointerSessionRecord record);
typedef struct {
void *ctx;
- SignalFfiSessionStoreLoadSession load_session;
- SignalFfiSessionStoreStoreSession store_session;
- SignalFfiSessionStoreDestroy destroy;
+ SignalLoadSession load_session;
+ SignalStoreSession store_session;
} SignalSessionStore;
typedef struct {
const SignalSessionStore *raw;
} SignalConstPointerFfiSessionStoreStruct;
-typedef struct {
- SignalPublicKey *raw;
-} SignalMutPointerPublicKey;
-
-typedef struct {
- SignalMutPointerPrivateKey first;
- SignalMutPointerPublicKey second;
-} SignalPairOfMutPointerPrivateKeyMutPointerPublicKey;
+typedef int (*SignalGetIdentityKeyPair)(void *store_ctx, SignalMutPointerPrivateKey *keyp);
-typedef int (*SignalFfiIdentityKeyStoreGetLocalIdentityKeyPair)(void *ctx, SignalPairOfMutPointerPrivateKeyMutPointerPublicKey *out);
+typedef int (*SignalGetLocalRegistrationId)(void *store_ctx, uint32_t *idp);
-typedef int (*SignalFfiIdentityKeyStoreGetLocalRegistrationId)(void *ctx, uint32_t *out);
+typedef struct {
+ const SignalPublicKey *raw;
+} SignalConstPointerPublicKey;
-typedef int (*SignalFfiIdentityKeyStoreGetIdentityKey)(void *ctx, SignalMutPointerPublicKey *out, SignalMutPointerProtocolAddress address);
+typedef int (*SignalSaveIdentityKey)(void *store_ctx, SignalConstPointerProtocolAddress address, SignalConstPointerPublicKey public_key);
-typedef int (*SignalFfiIdentityKeyStoreSaveIdentityKey)(void *ctx, uint8_t *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key);
+typedef struct {
+ SignalPublicKey *raw;
+} SignalMutPointerPublicKey;
-typedef int (*SignalFfiIdentityKeyStoreIsTrustedIdentity)(void *ctx, bool *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key, uint32_t direction);
+typedef int (*SignalGetIdentityKey)(void *store_ctx, SignalMutPointerPublicKey *public_keyp, SignalConstPointerProtocolAddress address);
-typedef void (*SignalFfiIdentityKeyStoreDestroy)(void *ctx);
+typedef int (*SignalIsTrustedIdentity)(void *store_ctx, SignalConstPointerProtocolAddress address, SignalConstPointerPublicKey public_key, unsigned int direction);
typedef struct {
void *ctx;
- SignalFfiIdentityKeyStoreGetLocalIdentityKeyPair get_local_identity_key_pair;
- SignalFfiIdentityKeyStoreGetLocalRegistrationId get_local_registration_id;
- SignalFfiIdentityKeyStoreGetIdentityKey get_identity_key;
- SignalFfiIdentityKeyStoreSaveIdentityKey save_identity_key;
- SignalFfiIdentityKeyStoreIsTrustedIdentity is_trusted_identity;
- SignalFfiIdentityKeyStoreDestroy destroy;
+ SignalGetIdentityKeyPair get_identity_key_pair;
+ SignalGetLocalRegistrationId get_local_registration_id;
+ SignalSaveIdentityKey save_identity;
+ SignalGetIdentityKey get_identity;
+ SignalIsTrustedIdentity is_trusted_identity;
} SignalIdentityKeyStore;
typedef struct {
@@ -875,20 +821,21 @@ typedef struct {
SignalPreKeyRecord *raw;
} SignalMutPointerPreKeyRecord;
-typedef int (*SignalFfiPreKeyStoreLoadPreKey)(void *ctx, SignalMutPointerPreKeyRecord *out, uint32_t id);
+typedef int (*SignalLoadPreKey)(void *store_ctx, SignalMutPointerPreKeyRecord *recordp, uint32_t id);
-typedef int (*SignalFfiPreKeyStoreStorePreKey)(void *ctx, uint32_t id, SignalMutPointerPreKeyRecord record);
+typedef struct {
+ const SignalPreKeyRecord *raw;
+} SignalConstPointerPreKeyRecord;
-typedef int (*SignalFfiPreKeyStoreRemovePreKey)(void *ctx, uint32_t id);
+typedef int (*SignalStorePreKey)(void *store_ctx, uint32_t id, SignalConstPointerPreKeyRecord record);
-typedef void (*SignalFfiPreKeyStoreDestroy)(void *ctx);
+typedef int (*SignalRemovePreKey)(void *store_ctx, uint32_t id);
typedef struct {
void *ctx;
- SignalFfiPreKeyStoreLoadPreKey load_pre_key;
- SignalFfiPreKeyStoreStorePreKey store_pre_key;
- SignalFfiPreKeyStoreRemovePreKey remove_pre_key;
- SignalFfiPreKeyStoreDestroy destroy;
+ SignalLoadPreKey load_pre_key;
+ SignalStorePreKey store_pre_key;
+ SignalRemovePreKey remove_pre_key;
} SignalPreKeyStore;
typedef struct {
@@ -899,17 +846,18 @@ typedef struct {
SignalSignedPreKeyRecord *raw;
} SignalMutPointerSignedPreKeyRecord;
-typedef int (*SignalFfiSignedPreKeyStoreLoadSignedPreKey)(void *ctx, SignalMutPointerSignedPreKeyRecord *out, uint32_t id);
+typedef int (*SignalLoadSignedPreKey)(void *store_ctx, SignalMutPointerSignedPreKeyRecord *recordp, uint32_t id);
-typedef int (*SignalFfiSignedPreKeyStoreStoreSignedPreKey)(void *ctx, uint32_t id, SignalMutPointerSignedPreKeyRecord record);
+typedef struct {
+ const SignalSignedPreKeyRecord *raw;
+} SignalConstPointerSignedPreKeyRecord;
-typedef void (*SignalFfiSignedPreKeyStoreDestroy)(void *ctx);
+typedef int (*SignalStoreSignedPreKey)(void *store_ctx, uint32_t id, SignalConstPointerSignedPreKeyRecord record);
typedef struct {
void *ctx;
- SignalFfiSignedPreKeyStoreLoadSignedPreKey load_signed_pre_key;
- SignalFfiSignedPreKeyStoreStoreSignedPreKey store_signed_pre_key;
- SignalFfiSignedPreKeyStoreDestroy destroy;
+ SignalLoadSignedPreKey load_signed_pre_key;
+ SignalStoreSignedPreKey store_signed_pre_key;
} SignalSignedPreKeyStore;
typedef struct {
@@ -920,20 +868,21 @@ typedef struct {
SignalKyberPreKeyRecord *raw;
} SignalMutPointerKyberPreKeyRecord;
-typedef int (*SignalFfiKyberPreKeyStoreLoadKyberPreKey)(void *ctx, SignalMutPointerKyberPreKeyRecord *out, uint32_t id);
+typedef int (*SignalLoadKyberPreKey)(void *store_ctx, SignalMutPointerKyberPreKeyRecord *recordp, uint32_t id);
-typedef int (*SignalFfiKyberPreKeyStoreStoreKyberPreKey)(void *ctx, uint32_t id, SignalMutPointerKyberPreKeyRecord record);
+typedef struct {
+ const SignalKyberPreKeyRecord *raw;
+} SignalConstPointerKyberPreKeyRecord;
-typedef int (*SignalFfiKyberPreKeyStoreMarkKyberPreKeyUsed)(void *ctx, uint32_t id, uint32_t ec_prekey_id, SignalMutPointerPublicKey base_key);
+typedef int (*SignalStoreKyberPreKey)(void *store_ctx, uint32_t id, SignalConstPointerKyberPreKeyRecord record);
-typedef void (*SignalFfiKyberPreKeyStoreDestroy)(void *ctx);
+typedef int (*SignalMarkKyberPreKeyUsed)(void *store_ctx, uint32_t id, uint32_t signed_prekey_id, SignalConstPointerPublicKey base_key);
typedef struct {
void *ctx;
- SignalFfiKyberPreKeyStoreLoadKyberPreKey load_kyber_pre_key;
- SignalFfiKyberPreKeyStoreStoreKyberPreKey store_kyber_pre_key;
- SignalFfiKyberPreKeyStoreMarkKyberPreKeyUsed mark_kyber_pre_key_used;
- SignalFfiKyberPreKeyStoreDestroy destroy;
+ SignalLoadKyberPreKey load_kyber_pre_key;
+ SignalStoreKyberPreKey store_kyber_pre_key;
+ SignalMarkKyberPreKeyUsed mark_kyber_pre_key_used;
} SignalKyberPreKeyStore;
typedef struct {
@@ -992,11 +941,6 @@ typedef struct {
SignalOwnedBuffer second;
} SignalPairOfc_charOwnedBufferOfc_uchar;
-typedef struct {
- SignalPairOfc_charOwnedBufferOfc_uchar first;
- int64_t second;
-} SignalPairOfPairOfc_charOwnedBufferOfc_uchari64;
-
typedef struct {
const char *first;
bool second;
@@ -1010,10 +954,6 @@ typedef struct {
const SignalFingerprint *raw;
} SignalConstPointerFingerprint;
-typedef struct {
- const SignalPublicKey *raw;
-} SignalConstPointerPublicKey;
-
typedef struct {
/**
* The badge ID.
@@ -1051,36 +991,22 @@ typedef struct {
size_t length;
} SignalOwnedBufferOfServiceIdFixedWidthBinaryBytes;
-typedef struct {
- SignalPreKeyBundle *raw;
-} SignalMutPointerPreKeyBundle;
-
-/**
- * A representation of a array allocated on the Rust heap for use in C code.
- */
-typedef struct {
- SignalMutPointerPreKeyBundle *base;
- /**
- * The number of elements in the buffer (not necessarily the number of bytes).
- */
- size_t length;
-} SignalOwnedBufferOfMutPointerPreKeyBundle;
-
typedef struct {
SignalSenderKeyRecord *raw;
} SignalMutPointerSenderKeyRecord;
-typedef int (*SignalFfiSenderKeyStoreLoadSenderKey)(void *ctx, SignalMutPointerSenderKeyRecord *out, SignalMutPointerProtocolAddress sender, SignalUuid distribution_id);
+typedef int (*SignalLoadSenderKey)(void *store_ctx, SignalMutPointerSenderKeyRecord*, SignalConstPointerProtocolAddress, const uint8_t (*distribution_id)[16]);
-typedef int (*SignalFfiSenderKeyStoreStoreSenderKey)(void *ctx, SignalMutPointerProtocolAddress sender, SignalUuid distribution_id, SignalMutPointerSenderKeyRecord record);
+typedef struct {
+ const SignalSenderKeyRecord *raw;
+} SignalConstPointerSenderKeyRecord;
-typedef void (*SignalFfiSenderKeyStoreDestroy)(void *ctx);
+typedef int (*SignalStoreSenderKey)(void *store_ctx, SignalConstPointerProtocolAddress, const uint8_t (*distribution_id)[16], SignalConstPointerSenderKeyRecord);
typedef struct {
void *ctx;
- SignalFfiSenderKeyStoreLoadSenderKey load_sender_key;
- SignalFfiSenderKeyStoreStoreSenderKey store_sender_key;
- SignalFfiSenderKeyStoreDestroy destroy;
+ SignalLoadSenderKey load_sender_key;
+ SignalStoreSenderKey store_sender_key;
} SignalSenderKeyStore;
typedef struct {
@@ -1125,23 +1051,15 @@ typedef struct {
SignalIncrementalMac *raw;
} SignalMutPointerIncrementalMac;
-typedef int (*SignalFfiLoggerLog)(void *ctx, SignalLogLevel level, const char *file, uint32_t line, const char *message);
+typedef void (*SignalLogCallback)(void *ctx, SignalLogLevel level, const char *file, uint32_t line, const char *message);
-typedef int (*SignalFfiLoggerFlush)(void *ctx);
-
-typedef void (*SignalFfiLoggerDestroy)(void *ctx);
+typedef void (*SignalLogFlushCallback)(void *ctx);
typedef struct {
void *ctx;
- SignalFfiLoggerLog log;
- SignalFfiLoggerFlush flush;
- SignalFfiLoggerDestroy destroy;
-} SignalFfiLoggerStruct;
-
-typedef struct {
- SignalOwnedBuffer first;
- SignalOwnedBuffer second;
-} SignalPairOfOwnedBufferOfc_ucharOwnedBufferOfc_uchar;
+ SignalLogCallback log;
+ SignalLogFlushCallback flush;
+} SignalFfiLogger;
/**
* A C callback used to report the results of Rust futures.
@@ -1153,10 +1071,10 @@ typedef struct {
* completed once.
*/
typedef struct {
- void (*complete)(SignalFfiError *error, const SignalPairOfOwnedBufferOfc_ucharOwnedBufferOfc_uchar *result, const void *context);
+ void (*complete)(SignalFfiError *error, const SignalOwnedBuffer *result, const void *context);
const void *context;
SignalCancellationId cancellation_id;
-} SignalCPromisePairOfOwnedBufferOfc_ucharOwnedBufferOfc_uchar;
+} SignalCPromiseOwnedBufferOfc_uchar;
typedef struct {
const SignalUnauthenticatedChatConnection *raw;
@@ -1194,10 +1112,6 @@ typedef struct {
SignalKyberSecretKey *raw;
} SignalMutPointerKyberSecretKey;
-typedef struct {
- const SignalKyberPreKeyRecord *raw;
-} SignalConstPointerKyberPreKeyRecord;
-
typedef struct {
const SignalKyberPublicKey *raw;
} SignalConstPointerKyberPublicKey;
@@ -1226,20 +1140,15 @@ typedef struct {
const SignalMessageBackupValidationOutcome *raw;
} SignalConstPointerMessageBackupValidationOutcome;
-typedef int (*SignalFfiSyncInputStreamRead)(void *ctx, size_t *out, SignalBorrowedMutableBuffer buf);
-
-typedef int (*SignalFfiSyncInputStreamSkip)(void *ctx, uint64_t amount);
+typedef int (*SignalRead)(void *ctx, uint8_t *buf, size_t buf_len, size_t *amount_read);
-typedef void (*SignalFfiSyncInputStreamDestroy)(void *ctx);
+typedef int (*SignalSkip)(void *ctx, uint64_t amount);
typedef struct {
void *ctx;
- SignalFfiSyncInputStreamRead read;
- SignalFfiSyncInputStreamSkip skip;
- SignalFfiSyncInputStreamDestroy destroy;
-} SignalSyncInputStream;
-
-typedef SignalSyncInputStream SignalInputStream;
+ SignalRead read;
+ SignalSkip skip;
+} SignalInputStream;
typedef struct {
const SignalInputStream *raw;
@@ -1270,12 +1179,12 @@ typedef struct {
} SignalMutPointerPlaintextContent;
typedef struct {
- const SignalPreKeyBundle *raw;
-} SignalConstPointerPreKeyBundle;
+ SignalPreKeyBundle *raw;
+} SignalMutPointerPreKeyBundle;
typedef struct {
- const SignalPreKeyRecord *raw;
-} SignalConstPointerPreKeyRecord;
+ const SignalPreKeyBundle *raw;
+} SignalConstPointerPreKeyBundle;
typedef struct {
SignalPreKeySignalMessage *raw;
@@ -1285,49 +1194,6 @@ typedef struct {
const SignalSenderKeyDistributionMessage *raw;
} SignalConstPointerSenderKeyDistributionMessage;
-typedef struct {
- SignalProvisioningChatConnection *raw;
-} SignalMutPointerProvisioningChatConnection;
-
-/**
- * A C callback used to report the results of Rust futures.
- *
- * cbindgen will produce independent C types like `SignalCPromisei32` and
- * `SignalCPromiseProtocolAddress`.
- *
- * This derives Copy because it behaves like a C type; nevertheless, a promise should still only be
- * completed once.
- */
-typedef struct {
- void (*complete)(SignalFfiError *error, const SignalMutPointerProvisioningChatConnection *result, const void *context);
- const void *context;
- SignalCancellationId cancellation_id;
-} SignalCPromiseMutPointerProvisioningChatConnection;
-
-typedef struct {
- const SignalProvisioningChatConnection *raw;
-} SignalConstPointerProvisioningChatConnection;
-
-typedef int (*SignalFfiProvisioningListenerReceivedAddress)(void *ctx, const char *address, SignalMutPointerServerMessageAck send_ack);
-
-typedef int (*SignalFfiProvisioningListenerReceivedEnvelope)(void *ctx, SignalOwnedBuffer envelope, SignalMutPointerServerMessageAck send_ack);
-
-typedef int (*SignalFfiProvisioningListenerConnectionInterrupted)(void *ctx, SignalFfiError *disconnect_cause);
-
-typedef void (*SignalFfiProvisioningListenerDestroy)(void *ctx);
-
-typedef struct {
- void *ctx;
- SignalFfiProvisioningListenerReceivedAddress received_address;
- SignalFfiProvisioningListenerReceivedEnvelope received_envelope;
- SignalFfiProvisioningListenerConnectionInterrupted connection_interrupted;
- SignalFfiProvisioningListenerDestroy destroy;
-} SignalFfiProvisioningListenerStruct;
-
-typedef struct {
- const SignalFfiProvisioningListenerStruct *raw;
-} SignalConstPointerFfiProvisioningListenerStruct;
-
typedef struct {
SignalRegisterAccountRequest *raw;
} SignalMutPointerRegisterAccountRequest;
@@ -1351,10 +1217,7 @@ typedef struct {
const SignalRegisterAccountResponse *raw;
} SignalConstPointerRegisterAccountResponse;
-typedef struct {
- bool present;
- uint8_t bytes[16];
-} SignalOptionalUuid;
+typedef uint8_t SignalOptionalUuid[17];
typedef SignalAccountAttributes SignalRegistrationAccountAttributes;
@@ -1469,10 +1332,6 @@ typedef struct {
size_t length;
} SignalBorrowedSliceOfConstPointerProtocolAddress;
-typedef struct {
- const SignalSessionRecord *raw;
-} SignalConstPointerSessionRecord;
-
typedef struct {
const SignalConstPointerSessionRecord *base;
size_t length;
@@ -1552,8 +1411,8 @@ typedef struct {
} SignalConstPointerSenderKeyMessage;
typedef struct {
- const SignalSenderKeyRecord *raw;
-} SignalConstPointerSenderKeyRecord;
+ SignalServerMessageAck *raw;
+} SignalMutPointerServerMessageAck;
typedef struct {
const SignalServerMessageAck *raw;
@@ -1571,10 +1430,6 @@ typedef struct {
const SignalSgxClientState *raw;
} SignalConstPointerSgxClientState;
-typedef struct {
- const SignalSignedPreKeyRecord *raw;
-} SignalConstPointerSignedPreKeyRecord;
-
typedef struct {
SignalTokioAsyncContext *raw;
} SignalMutPointerTokioAsyncContext;
@@ -1598,26 +1453,6 @@ typedef struct {
SignalCancellationId cancellation_id;
} SignalCPromiseMutPointerUnauthenticatedChatConnection;
-typedef struct {
- SignalMutPointerPublicKey identity_key;
- SignalOwnedBufferOfMutPointerPreKeyBundle pre_key_bundles;
-} SignalFfiPreKeysResponse;
-
-/**
- * A C callback used to report the results of Rust futures.
- *
- * cbindgen will produce independent C types like `SignalCPromisei32` and
- * `SignalCPromiseProtocolAddress`.
- *
- * This derives Copy because it behaves like a C type; nevertheless, a promise should still only be
- * completed once.
- */
-typedef struct {
- void (*complete)(SignalFfiError *error, const SignalFfiPreKeysResponse *result, const void *context);
- const void *context;
- SignalCancellationId cancellation_id;
-} SignalCPromiseFfiPreKeysResponse;
-
/**
* A C callback used to report the results of Rust futures.
*
@@ -1673,6 +1508,8 @@ typedef struct {
SignalValidatingMac *raw;
} SignalMutPointerValidatingMac;
+typedef SignalInputStream SignalSyncInputStream;
+
typedef struct {
const SignalSyncInputStream *raw;
} SignalConstPointerFfiSyncInputStreamStruct;
@@ -1749,8 +1586,6 @@ SignalFfiError *signal_authenticated_chat_connection_destroy(SignalMutPointerAut
SignalFfiError *signal_authenticated_chat_connection_disconnect(SignalCPromisebool *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerAuthenticatedChatConnection chat);
-SignalFfiError *signal_authenticated_chat_connection_get_upload_form(SignalCPromiseFfiUploadForm *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerAuthenticatedChatConnection chat, uint64_t upload_length);
-
SignalFfiError *signal_authenticated_chat_connection_info(SignalMutPointerChatConnectionInfo *out, SignalConstPointerAuthenticatedChatConnection chat);
SignalFfiError *signal_authenticated_chat_connection_init_listener(SignalConstPointerAuthenticatedChatConnection chat, SignalConstPointerFfiChatListenerStruct listener);
@@ -1759,12 +1594,6 @@ SignalFfiError *signal_authenticated_chat_connection_preconnect(SignalCPromisebo
SignalFfiError *signal_authenticated_chat_connection_send(SignalCPromiseFfiChatResponse *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerAuthenticatedChatConnection chat, SignalConstPointerHttpRequest http_request, uint32_t timeout_millis);
-SignalFfiError *signal_authenticated_chat_connection_send_message(SignalCPromisebool *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerAuthenticatedChatConnection chat, const SignalServiceIdFixedWidthBinaryBytes *destination, uint64_t timestamp, SignalBorrowedSliceOfu32 device_ids, SignalBorrowedSliceOfu32 registration_ids, SignalBorrowedSliceOfConstPointerCiphertextMessage contents, bool online_only, bool is_urgent);
-
-SignalFfiError *signal_authenticated_chat_connection_send_raw_grpc(SignalCPromiseOwnedBufferOfc_uchar *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerAuthenticatedChatConnection chat, const char *service, const char *method, SignalBorrowedBuffer payload);
-
-SignalFfiError *signal_authenticated_chat_connection_send_sync_message(SignalCPromisebool *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerAuthenticatedChatConnection chat, uint64_t timestamp, SignalBorrowedSliceOfu32 device_ids, SignalBorrowedSliceOfu32 registration_ids, SignalBorrowedSliceOfConstPointerCiphertextMessage contents, bool is_urgent);
-
SignalFfiError *signal_backup_auth_credential_check_valid_contents(SignalBorrowedBuffer params_bytes);
SignalFfiError *signal_backup_auth_credential_get_backup_id(uint8_t (*out)[16], SignalBorrowedBuffer credential_bytes);
@@ -1785,7 +1614,7 @@ SignalFfiError *signal_backup_auth_credential_request_context_check_valid_conten
SignalFfiError *signal_backup_auth_credential_request_context_get_request(SignalOwnedBuffer *out, SignalBorrowedBuffer context_bytes);
-SignalFfiError *signal_backup_auth_credential_request_context_new(SignalOwnedBuffer *out, const uint8_t (*backup_key)[32], SignalUuid uuid);
+SignalFfiError *signal_backup_auth_credential_request_context_new(SignalOwnedBuffer *out, const uint8_t (*backup_key)[32], const uint8_t (*uuid)[16]);
SignalFfiError *signal_backup_auth_credential_request_context_receive_response(SignalOwnedBuffer *out, SignalBorrowedBuffer context_bytes, SignalBorrowedBuffer response_bytes, uint64_t expected_redemption_time, SignalBorrowedBuffer params_bytes);
@@ -1885,8 +1714,6 @@ SignalFfiError *signal_connection_manager_clear_proxy(SignalConstPointerConnecti
SignalFfiError *signal_connection_manager_destroy(SignalMutPointerConnectionManager p);
-SignalFfiError *signal_connection_manager_internal_testing_set_reflector_proxy(SignalConstPointerConnectionManager connection_manager, bool enabled);
-
SignalFfiError *signal_connection_manager_new(SignalMutPointerConnectionManager *out, uint8_t environment, const char *user_agent, SignalMutPointerBridgedStringMap remote_config, uint8_t build_variant);
SignalFfiError *signal_connection_manager_on_network_change(SignalConstPointerConnectionManager connection_manager);
@@ -1927,9 +1754,9 @@ SignalFfiError *signal_create_call_link_credential_request_issue_deterministic(S
SignalFfiError *signal_create_call_link_credential_response_check_valid_contents(SignalBorrowedBuffer response_bytes);
-SignalFfiError *signal_decrypt_message(SignalOwnedBuffer *out, SignalConstPointerSignalMessage message, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerProtocolAddress local_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store);
+SignalFfiError *signal_decrypt_message(SignalOwnedBuffer *out, SignalConstPointerSignalMessage message, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store);
-SignalFfiError *signal_decrypt_pre_key_message(SignalOwnedBuffer *out, SignalConstPointerPreKeySignalMessage message, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerProtocolAddress local_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, SignalConstPointerFfiPreKeyStoreStruct prekey_store, SignalConstPointerFfiSignedPreKeyStoreStruct signed_prekey_store, SignalConstPointerFfiKyberPreKeyStoreStruct kyber_prekey_store);
+SignalFfiError *signal_decrypt_pre_key_message(SignalOwnedBuffer *out, SignalConstPointerPreKeySignalMessage message, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, SignalConstPointerFfiPreKeyStoreStruct prekey_store, SignalConstPointerFfiSignedPreKeyStoreStruct signed_prekey_store, SignalConstPointerFfiKyberPreKeyStoreStruct kyber_prekey_store);
SignalFfiError *signal_decryption_error_message_clone(SignalMutPointerDecryptionErrorMessage *new_obj, SignalConstPointerDecryptionErrorMessage obj);
@@ -1955,7 +1782,7 @@ SignalFfiError *signal_device_transfer_generate_private_key(SignalOwnedBuffer *o
SignalFfiError *signal_device_transfer_generate_private_key_with_format(SignalOwnedBuffer *out, uint8_t key_format);
-SignalFfiError *signal_encrypt_message(SignalMutPointerCiphertextMessage *out, SignalBorrowedBuffer ptext, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerProtocolAddress local_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, uint64_t now);
+SignalFfiError *signal_encrypt_message(SignalMutPointerCiphertextMessage *out, SignalBorrowedBuffer ptext, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, uint64_t now);
void signal_error_free(SignalFfiError *err);
@@ -1969,7 +1796,7 @@ SignalFfiError *signal_error_get_mismatched_device_errors(SignalOwnedBufferOfFfi
SignalFfiError *signal_error_get_our_fingerprint_version(uint32_t *out, SignalUnwindSafeArgSignalFfiError err);
-SignalFfiError *signal_error_get_rate_limit_challenge(SignalPairOfPairOfc_charOwnedBufferOfc_uchari64 *out, SignalUnwindSafeArgSignalFfiError err);
+SignalFfiError *signal_error_get_rate_limit_challenge(SignalPairOfc_charOwnedBufferOfc_uchar *out, SignalUnwindSafeArgSignalFfiError err);
SignalFfiError *signal_error_get_registration_error_not_deliverable(SignalPairOfc_charbool *out, SignalUnwindSafeArgSignalFfiError err);
@@ -1985,7 +1812,7 @@ uint32_t signal_error_get_type(const SignalFfiError *err);
SignalFfiError *signal_error_get_unknown_fields(SignalStringArray *out, SignalUnwindSafeArgSignalFfiError err);
-SignalFfiError *signal_error_get_uuid(SignalUuid *out, SignalUnwindSafeArgSignalFfiError err);
+SignalFfiError *signal_error_get_uuid(uint8_t (*out)[16], SignalUnwindSafeArgSignalFfiError err);
SignalFfiError *signal_expiring_profile_key_credential_check_valid_contents(SignalBorrowedBuffer buffer);
@@ -2019,13 +1846,6 @@ void signal_free_list_of_strings(SignalOwnedBufferOfCStringPtr buffer);
void signal_free_lookup_response_entry_list(SignalOwnedBufferOfFfiCdsiLookupResponseEntry buffer);
-/**
- * This frees a buffer of PreKeyBundle pointers, and _does not_ free the
- * pointers within the buffer. This _only_ frees the buffer containing
- * the pointers.
- */
-void signal_free_outer_buffer_list_of_prekey_bundles(SignalOwnedBufferOfMutPointerPreKeyBundle buffer);
-
void signal_free_string(const char *buf);
SignalFfiError *signal_generic_server_public_params_check_valid_contents(SignalBorrowedBuffer params_bytes);
@@ -2038,7 +1858,7 @@ SignalFfiError *signal_generic_server_secret_params_get_public_params(SignalOwne
SignalFfiError *signal_group_decrypt_message(SignalOwnedBuffer *out, SignalConstPointerProtocolAddress sender, SignalBorrowedBuffer message, SignalConstPointerFfiSenderKeyStoreStruct store);
-SignalFfiError *signal_group_encrypt_message(SignalMutPointerCiphertextMessage *out, SignalConstPointerProtocolAddress sender, SignalUuid distribution_id, SignalBorrowedBuffer message, SignalConstPointerFfiSenderKeyStoreStruct store);
+SignalFfiError *signal_group_encrypt_message(SignalMutPointerCiphertextMessage *out, SignalConstPointerProtocolAddress sender, const uint8_t (*distribution_id)[16], SignalBorrowedBuffer message, SignalConstPointerFfiSenderKeyStoreStruct store);
SignalFfiError *signal_group_master_key_check_valid_contents(SignalBorrowedBuffer buffer);
@@ -2144,15 +1964,17 @@ SignalFfiError *signal_incremental_mac_initialize(SignalMutPointerIncrementalMac
SignalFfiError *signal_incremental_mac_update(SignalOwnedBuffer *out, SignalMutPointerIncrementalMac mac, SignalBorrowedBuffer bytes, uint32_t offset, uint32_t length);
-bool signal_init_logger(SignalLogLevel max_level, SignalFfiLoggerStruct logger);
+bool signal_init_logger(SignalLogLevel max_level, SignalFfiLogger logger);
SignalFfiError *signal_key_transparency_aci_search_key(SignalOwnedBuffer *out, const SignalServiceIdFixedWidthBinaryBytes *aci);
-SignalFfiError *signal_key_transparency_check(SignalCPromisePairOfOwnedBufferOfc_ucharOwnedBufferOfc_uchar *promise, SignalConstPointerTokioAsyncContext async_runtime, uint8_t environment, SignalConstPointerUnauthenticatedChatConnection chat_connection, const SignalServiceIdFixedWidthBinaryBytes *aci, SignalConstPointerPublicKey aci_identity_key, const char *e164, SignalOptionalBorrowedSliceOfc_uchar unidentified_access_key, SignalOptionalBorrowedSliceOfc_uchar username_hash, SignalOptionalBorrowedSliceOfc_uchar account_data, SignalOptionalBorrowedSliceOfc_uchar last_distinguished_tree_head, bool is_self_check, bool is_e164_discoverable);
+SignalFfiError *signal_key_transparency_distinguished(SignalCPromiseOwnedBufferOfc_uchar *promise, SignalConstPointerTokioAsyncContext async_runtime, uint8_t environment, SignalConstPointerUnauthenticatedChatConnection chat_connection, SignalOptionalBorrowedSliceOfc_uchar last_distinguished_tree_head);
SignalFfiError *signal_key_transparency_e164_search_key(SignalOwnedBuffer *out, const char *e164);
-SignalFfiError *signal_key_transparency_reset_data_field(SignalOwnedBuffer *out, SignalBorrowedBuffer account_data, uint8_t field);
+SignalFfiError *signal_key_transparency_monitor(SignalCPromiseOwnedBufferOfc_uchar *promise, SignalConstPointerTokioAsyncContext async_runtime, uint8_t environment, SignalConstPointerUnauthenticatedChatConnection chat_connection, const SignalServiceIdFixedWidthBinaryBytes *aci, SignalConstPointerPublicKey aci_identity_key, const char *e164, SignalOptionalBorrowedSliceOfc_uchar unidentified_access_key, SignalOptionalBorrowedSliceOfc_uchar username_hash, SignalOptionalBorrowedSliceOfc_uchar account_data, SignalBorrowedBuffer last_distinguished_tree_head, bool is_self_monitor);
+
+SignalFfiError *signal_key_transparency_search(SignalCPromiseOwnedBufferOfc_uchar *promise, SignalConstPointerTokioAsyncContext async_runtime, uint8_t environment, SignalConstPointerUnauthenticatedChatConnection chat_connection, const SignalServiceIdFixedWidthBinaryBytes *aci, SignalConstPointerPublicKey aci_identity_key, const char *e164, SignalOptionalBorrowedSliceOfc_uchar unidentified_access_key, SignalOptionalBorrowedSliceOfc_uchar username_hash, SignalOptionalBorrowedSliceOfc_uchar account_data, SignalBorrowedBuffer last_distinguished_tree_head);
SignalFfiError *signal_key_transparency_username_hash_search_key(SignalOwnedBuffer *out, SignalBorrowedBuffer hash);
@@ -2256,6 +2078,8 @@ SignalFfiError *signal_message_get_serialized(SignalOwnedBuffer *out, SignalCons
SignalFfiError *signal_message_new(SignalMutPointerSignalMessage *out, uint8_t message_version, SignalBorrowedBuffer mac_key, SignalConstPointerPublicKey sender_ratchet_key, uint32_t counter, uint32_t previous_counter, SignalBorrowedBuffer ciphertext, SignalConstPointerPublicKey sender_identity_key, SignalConstPointerPublicKey receiver_identity_key, SignalBorrowedBuffer pq_ratchet);
+SignalFfiError *signal_message_verify_mac(bool *out, SignalConstPointerSignalMessage msg, SignalConstPointerPublicKey sender_identity_key, SignalConstPointerPublicKey receiver_identity_key, SignalBorrowedBuffer mac_key);
+
SignalFfiError *signal_mp4_sanitizer_sanitize(SignalMutPointerSanitizedMetadata *out, SignalConstPointerFfiInputStreamStruct input, uint64_t len);
SignalFfiError *signal_online_backup_validator_add_frame(SignalMutPointerOnlineBackupValidator backup, SignalBorrowedBuffer frame);
@@ -2382,7 +2206,7 @@ SignalFfiError *signal_privatekey_serialize(SignalOwnedBuffer *out, SignalConstP
SignalFfiError *signal_privatekey_sign(SignalOwnedBuffer *out, SignalConstPointerPrivateKey key, SignalBorrowedBuffer message);
-SignalFfiError *signal_process_prekey_bundle(SignalConstPointerPreKeyBundle bundle, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerProtocolAddress local_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, uint64_t now);
+SignalFfiError *signal_process_prekey_bundle(SignalConstPointerPreKeyBundle bundle, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, uint64_t now);
SignalFfiError *signal_process_sender_key_distribution_message(SignalConstPointerProtocolAddress sender, SignalConstPointerSenderKeyDistributionMessage sender_key_distribution_message, SignalConstPointerFfiSenderKeyStoreStruct store);
@@ -2410,18 +2234,10 @@ SignalFfiError *signal_profile_key_get_commitment(unsigned char (*out)[SignalPRO
SignalFfiError *signal_profile_key_get_profile_key_version(uint8_t (*out)[SignalPROFILE_KEY_VERSION_ENCODED_LEN], const unsigned char (*profile_key)[SignalPROFILE_KEY_LEN], const SignalServiceIdFixedWidthBinaryBytes *user_id);
-SignalFfiError *signal_provisioning_chat_connection_connect(SignalCPromiseMutPointerProvisioningChatConnection *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerConnectionManager connection_manager);
-
-SignalFfiError *signal_provisioning_chat_connection_destroy(SignalMutPointerProvisioningChatConnection p);
-
-SignalFfiError *signal_provisioning_chat_connection_disconnect(SignalCPromisebool *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerProvisioningChatConnection chat);
-
-SignalFfiError *signal_provisioning_chat_connection_info(SignalMutPointerChatConnectionInfo *out, SignalConstPointerProvisioningChatConnection chat);
-
-SignalFfiError *signal_provisioning_chat_connection_init_listener(SignalConstPointerProvisioningChatConnection chat, SignalConstPointerFfiProvisioningListenerStruct listener);
-
SignalFfiError *signal_publickey_clone(SignalMutPointerPublicKey *new_obj, SignalConstPointerPublicKey obj);
+SignalFfiError *signal_publickey_compare(int32_t *out, SignalConstPointerPublicKey key1, SignalConstPointerPublicKey key2);
+
SignalFfiError *signal_publickey_deserialize(SignalMutPointerPublicKey *out, SignalBorrowedBuffer data);
SignalFfiError *signal_publickey_destroy(SignalMutPointerPublicKey p);
@@ -2594,7 +2410,7 @@ SignalFfiError *signal_sender_certificate_validate(bool *out, SignalConstPointer
SignalFfiError *signal_sender_key_distribution_message_clone(SignalMutPointerSenderKeyDistributionMessage *new_obj, SignalConstPointerSenderKeyDistributionMessage obj);
-SignalFfiError *signal_sender_key_distribution_message_create(SignalMutPointerSenderKeyDistributionMessage *out, SignalConstPointerProtocolAddress sender, SignalUuid distribution_id, SignalConstPointerFfiSenderKeyStoreStruct store);
+SignalFfiError *signal_sender_key_distribution_message_create(SignalMutPointerSenderKeyDistributionMessage *out, SignalConstPointerProtocolAddress sender, const uint8_t (*distribution_id)[16], SignalConstPointerFfiSenderKeyStoreStruct store);
SignalFfiError *signal_sender_key_distribution_message_deserialize(SignalMutPointerSenderKeyDistributionMessage *out, SignalBorrowedBuffer data);
@@ -2604,13 +2420,13 @@ SignalFfiError *signal_sender_key_distribution_message_get_chain_id(uint32_t *ou
SignalFfiError *signal_sender_key_distribution_message_get_chain_key(SignalOwnedBuffer *out, SignalConstPointerSenderKeyDistributionMessage obj);
-SignalFfiError *signal_sender_key_distribution_message_get_distribution_id(SignalUuid *out, SignalConstPointerSenderKeyDistributionMessage obj);
+SignalFfiError *signal_sender_key_distribution_message_get_distribution_id(uint8_t (*out)[16], SignalConstPointerSenderKeyDistributionMessage obj);
SignalFfiError *signal_sender_key_distribution_message_get_iteration(uint32_t *out, SignalConstPointerSenderKeyDistributionMessage obj);
SignalFfiError *signal_sender_key_distribution_message_get_signature_key(SignalMutPointerPublicKey *out, SignalConstPointerSenderKeyDistributionMessage m);
-SignalFfiError *signal_sender_key_distribution_message_new(SignalMutPointerSenderKeyDistributionMessage *out, uint8_t message_version, SignalUuid distribution_id, uint32_t chain_id, uint32_t iteration, SignalBorrowedBuffer chainkey, SignalConstPointerPublicKey pk);
+SignalFfiError *signal_sender_key_distribution_message_new(SignalMutPointerSenderKeyDistributionMessage *out, uint8_t message_version, const uint8_t (*distribution_id)[16], uint32_t chain_id, uint32_t iteration, SignalBorrowedBuffer chainkey, SignalConstPointerPublicKey pk);
SignalFfiError *signal_sender_key_distribution_message_serialize(SignalOwnedBuffer *out, SignalConstPointerSenderKeyDistributionMessage obj);
@@ -2624,11 +2440,11 @@ SignalFfiError *signal_sender_key_message_get_chain_id(uint32_t *out, SignalCons
SignalFfiError *signal_sender_key_message_get_cipher_text(SignalOwnedBuffer *out, SignalConstPointerSenderKeyMessage obj);
-SignalFfiError *signal_sender_key_message_get_distribution_id(SignalUuid *out, SignalConstPointerSenderKeyMessage obj);
+SignalFfiError *signal_sender_key_message_get_distribution_id(uint8_t (*out)[16], SignalConstPointerSenderKeyMessage obj);
SignalFfiError *signal_sender_key_message_get_iteration(uint32_t *out, SignalConstPointerSenderKeyMessage obj);
-SignalFfiError *signal_sender_key_message_new(SignalMutPointerSenderKeyMessage *out, uint8_t message_version, SignalUuid distribution_id, uint32_t chain_id, uint32_t iteration, SignalBorrowedBuffer ciphertext, SignalConstPointerPrivateKey pk);
+SignalFfiError *signal_sender_key_message_new(SignalMutPointerSenderKeyMessage *out, uint8_t message_version, const uint8_t (*distribution_id)[16], uint32_t chain_id, uint32_t iteration, SignalBorrowedBuffer ciphertext, SignalConstPointerPrivateKey pk);
SignalFfiError *signal_sender_key_message_serialize(SignalOwnedBuffer *out, SignalConstPointerSenderKeyMessage obj);
@@ -2738,7 +2554,7 @@ SignalFfiError *signal_session_record_get_local_registration_id(uint32_t *out, S
SignalFfiError *signal_session_record_get_remote_registration_id(uint32_t *out, SignalConstPointerSessionRecord obj);
-SignalFfiError *signal_session_record_has_usable_sender_chain(bool *out, SignalConstPointerSessionRecord s, double require_pq_ratio, uint64_t now);
+SignalFfiError *signal_session_record_has_usable_sender_chain(bool *out, SignalConstPointerSessionRecord s, uint64_t now);
SignalFfiError *signal_session_record_serialize(SignalOwnedBuffer *out, SignalConstPointerSessionRecord obj);
@@ -2782,40 +2598,24 @@ SignalFfiError *signal_tokio_async_context_destroy(SignalMutPointerTokioAsyncCon
SignalFfiError *signal_tokio_async_context_new(SignalMutPointerTokioAsyncContext *out);
-SignalFfiError *signal_unauthenticated_chat_connection_account_exists(SignalCPromisebool *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, const SignalServiceIdFixedWidthBinaryBytes *account);
-
-SignalFfiError *signal_unauthenticated_chat_connection_backup_get_media_upload_form(SignalCPromiseFfiUploadForm *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, SignalBorrowedBuffer credential, SignalBorrowedBuffer server_keys, SignalConstPointerPrivateKey signing_key, uint64_t upload_size, int64_t rng);
-
-SignalFfiError *signal_unauthenticated_chat_connection_backup_get_upload_form(SignalCPromiseFfiUploadForm *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, SignalBorrowedBuffer credential, SignalBorrowedBuffer server_keys, SignalConstPointerPrivateKey signing_key, uint64_t upload_size, int64_t rng);
-
SignalFfiError *signal_unauthenticated_chat_connection_connect(SignalCPromiseMutPointerUnauthenticatedChatConnection *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerConnectionManager connection_manager, SignalBorrowedBytestringArray languages);
SignalFfiError *signal_unauthenticated_chat_connection_destroy(SignalMutPointerUnauthenticatedChatConnection p);
SignalFfiError *signal_unauthenticated_chat_connection_disconnect(SignalCPromisebool *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat);
-SignalFfiError *signal_unauthenticated_chat_connection_get_pre_keys_access_key_auth(SignalCPromiseFfiPreKeysResponse *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, const uint8_t (*auth)[16], const SignalServiceIdFixedWidthBinaryBytes *target, int32_t device);
-
-SignalFfiError *signal_unauthenticated_chat_connection_get_pre_keys_group_auth(SignalCPromiseFfiPreKeysResponse *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, SignalBorrowedBuffer auth, const SignalServiceIdFixedWidthBinaryBytes *target, int32_t device);
-
-SignalFfiError *signal_unauthenticated_chat_connection_get_pre_keys_unrestricted_auth(SignalCPromiseFfiPreKeysResponse *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, const SignalServiceIdFixedWidthBinaryBytes *target, int32_t device);
-
SignalFfiError *signal_unauthenticated_chat_connection_info(SignalMutPointerChatConnectionInfo *out, SignalConstPointerUnauthenticatedChatConnection chat);
SignalFfiError *signal_unauthenticated_chat_connection_init_listener(SignalConstPointerUnauthenticatedChatConnection chat, SignalConstPointerFfiChatListenerStruct listener);
SignalFfiError *signal_unauthenticated_chat_connection_look_up_username_hash(SignalCPromiseOptionalUuid *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, SignalBorrowedBuffer hash);
-SignalFfiError *signal_unauthenticated_chat_connection_look_up_username_link(SignalCPromiseOptionalPairOfc_charu832 *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, SignalUuid uuid, SignalBorrowedBuffer entropy);
+SignalFfiError *signal_unauthenticated_chat_connection_look_up_username_link(SignalCPromiseOptionalPairOfc_charu832 *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, const uint8_t (*uuid)[16], SignalBorrowedBuffer entropy);
SignalFfiError *signal_unauthenticated_chat_connection_send(SignalCPromiseFfiChatResponse *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, SignalConstPointerHttpRequest http_request, uint32_t timeout_millis);
-SignalFfiError *signal_unauthenticated_chat_connection_send_message(SignalCPromisebool *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, const SignalServiceIdFixedWidthBinaryBytes *destination, uint64_t timestamp, SignalBorrowedSliceOfu32 device_ids, SignalBorrowedSliceOfu32 registration_ids, SignalBorrowedSliceOfBuffers contents, uint8_t auth_kind, SignalOptionalBorrowedSliceOfc_uchar auth_buffer, bool online_only, bool is_urgent);
-
SignalFfiError *signal_unauthenticated_chat_connection_send_multi_recipient_message(SignalCPromiseOwnedBufferOfServiceIdFixedWidthBinaryBytes *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, SignalBorrowedBuffer payload, uint64_t timestamp, SignalBorrowedBuffer auth, bool online_only, bool is_urgent);
-SignalFfiError *signal_unauthenticated_chat_connection_send_raw_grpc(SignalCPromiseOwnedBufferOfc_uchar *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, const char *service, const char *method, SignalBorrowedBuffer payload);
-
SignalFfiError *signal_unidentified_sender_message_content_deserialize(SignalMutPointerUnidentifiedSenderMessageContent *out, SignalBorrowedBuffer data);
SignalFfiError *signal_unidentified_sender_message_content_destroy(SignalMutPointerUnidentifiedSenderMessageContent p);
diff --git a/krypton-protocol/src/appleMain/cinterop/libsignal_ffi.def b/krypton-protocol/src/appleMain/cinterop/libsignal_ffi.def
index 0dd65dd..71ee623 100644
--- a/krypton-protocol/src/appleMain/cinterop/libsignal_ffi.def
+++ b/krypton-protocol/src/appleMain/cinterop/libsignal_ffi.def
@@ -1,5 +1,12 @@
+# Bring-your-own-libsignal (Model B): Krypton does NOT embed Signal's binary.
+# This cinterop compiles bindings from the headers only; the actual
+# libsignal_ffi.a is supplied at LINK time by the consumer, who fetches it from
+# Signal's official source on their own machine (scripts/fetch-libsignal-ffi.sh
+# or build-libsignal-ffi.sh) and points the linker at it with -L.
+#
+# `linkerOpts = -lsignal_ffi` is recorded in the published klib so the consumer
+# only needs to add the -L search path. No Signal binary is shipped by Krypton —
+# the trust anchor is Signal, and there is nothing of ours to suspect.
headers = krypton_ffi.h
headerFilter = krypton_ffi.*
-staticLibraries = libsignal_ffi.a libkrypton_adapter.a
-libraryPaths = /Users/ranbirsingh/krypton/krypton-protocol/libs/apple
-compilerOpts = -I/Users/ranbirsingh/krypton/krypton-protocol/libs/apple
+linkerOpts = -lsignal_ffi
diff --git a/krypton-protocol/src/appleMain/kotlin/io/krypton/protocol/bridge/NativeBridge.kt b/krypton-protocol/src/appleMain/kotlin/io/krypton/protocol/bridge/NativeBridge.kt
index 26b4f6b..fa39a01 100644
--- a/krypton-protocol/src/appleMain/kotlin/io/krypton/protocol/bridge/NativeBridge.kt
+++ b/krypton-protocol/src/appleMain/kotlin/io/krypton/protocol/bridge/NativeBridge.kt
@@ -46,8 +46,14 @@ public class NativeBridge(
private fun checkErr(errPtr: Any?) {
if (errPtr == null) return
@Suppress("UNCHECKED_CAST")
- val code = (errPtr as? CPointer)?.let { signal_error_get_type(it) } ?: 0u
- error("libsignal_ffi operation failed (error type $code)")
+ val errp = errPtr as? CPointer
+ val code = errp?.let { signal_error_get_type(it) } ?: 0u
+ val msg = memScoped {
+ val out = alloc>()
+ signal_error_get_message(out.ptr, errp)
+ out.value?.toKString() ?: ""
+ }
+ error("libsignal_ffi operation failed (error type $code): $msg")
}
/** Build a CValue from this ByteArray. */
@@ -85,16 +91,6 @@ public class NativeBridge(
return const.readValue()
}
- /** Build a local address CValue (empty name, device 1). */
- private fun localConstAddrCValue(scope: MemScope): CValue {
- val mut = scope.alloc()
- val e: Any? = signal_address_new(mut.ptr, "", 1u)
- checkErr(e)
- val const = scope.alloc()
- const.raw = mut.raw
- return const.readValue()
- }
-
/** Convert alloc'd SignalMutPointerPublicKey → CValue. */
private fun MemScope.constPub(mut: SignalMutPointerPublicKey): CValue {
val const = alloc()
@@ -136,7 +132,6 @@ public class NativeBridge(
s.store_session = staticCFunction { ctx, address, record ->
ctx!!.asStableRef().get().cStoreSession(address!!, record!!)
}
- s.destroy = staticCFunction { _ -> }
val wrapped = alloc()
wrapped.raw = s.ptr
return wrapped.readValue()
@@ -145,22 +140,21 @@ public class NativeBridge(
private fun MemScope.buildIdentityKeyStore(): CValue {
val s = alloc()
s.ctx = delegateRef.asCPointer()
- s.get_local_identity_key_pair = staticCFunction { ctx, out ->
- ctx!!.asStableRef().get().cGetLocalIdentityKeyPair(out!!)
+ s.get_identity_key_pair = staticCFunction { ctx, out ->
+ ctx!!.asStableRef().get().cGetIdentityKeyPair(out!!)
}
s.get_local_registration_id = staticCFunction { ctx, out ->
ctx!!.asStableRef().get().cGetLocalRegistrationId(out!!)
}
- s.get_identity_key = staticCFunction { ctx, out, address ->
- ctx!!.asStableRef().get().cGetIdentityKey(out!!, address!!)
+ s.save_identity = staticCFunction { ctx, address, publicKey ->
+ ctx!!.asStableRef().get().cSaveIdentity(address!!, publicKey!!)
}
- s.save_identity = staticCFunction { ctx, out, address, publicKey ->
- ctx!!.asStableRef().get().cSaveIdentity(out!!, address!!, publicKey!!)
+ s.get_identity = staticCFunction { ctx, out, address ->
+ ctx!!.asStableRef().get().cGetIdentityKey(out!!, address!!)
}
- s.is_trusted_identity = staticCFunction { ctx, out, address, publicKey, direction ->
- ctx!!.asStableRef().get().cIsTrustedIdentity(out!!, address!!, publicKey!!, direction!!)
+ s.is_trusted_identity = staticCFunction { ctx, address, publicKey, direction ->
+ ctx!!.asStableRef().get().cIsTrustedIdentity(address!!, publicKey!!, direction!!)
}
- s.destroy = staticCFunction { _ -> }
val wrapped = alloc()
wrapped.raw = s.ptr
return wrapped.readValue()
@@ -178,7 +172,6 @@ public class NativeBridge(
s.remove_pre_key = staticCFunction { ctx, id ->
ctx!!.asStableRef().get().cRemovePreKey(id!!)
}
- s.destroy = staticCFunction { _ -> }
val wrapped = alloc()
wrapped.raw = s.ptr
return wrapped.readValue()
@@ -193,7 +186,6 @@ public class NativeBridge(
s.store_signed_pre_key = staticCFunction { ctx, id, record ->
ctx!!.asStableRef().get().cStoreSignedPreKey(id!!, record!!)
}
- s.destroy = staticCFunction { _ -> }
val wrapped = alloc()
wrapped.raw = s.ptr
return wrapped.readValue()
@@ -208,10 +200,9 @@ public class NativeBridge(
s.store_kyber_pre_key = staticCFunction { ctx, id, record ->
ctx!!.asStableRef().get().cStoreKyberPreKey(id!!, record!!)
}
- s.mark_kyber_pre_key_used = staticCFunction { ctx, id, ecPrekeyId, baseKey ->
- ctx!!.asStableRef().get().cMarkKyberPreKeyUsed(id!!, ecPrekeyId!!, baseKey!!)
+ s.mark_kyber_pre_key_used = staticCFunction { ctx, id, signedPrekeyId, baseKey ->
+ ctx!!.asStableRef().get().cMarkKyberPreKeyUsed(id!!, signedPrekeyId!!, baseKey!!)
}
- s.destroy = staticCFunction { _ -> }
val wrapped = alloc()
wrapped.raw = s.ptr
return wrapped.readValue()
@@ -274,7 +265,7 @@ public class NativeBridge(
val bundlePtr = bundleConst.readValue()
checkErr(signal_process_prekey_bundle(
- bundlePtr, remoteAddr, localConstAddrCValue(this),
+ bundlePtr, remoteAddr,
buildSessionStore(), buildIdentityKeyStore(), now,
))
@@ -290,7 +281,6 @@ public class NativeBridge(
msgOut.ptr,
plaintext.asBorrowedBuffer(this),
recipient.toConstAddrCValue(this),
- localConstAddrCValue(this),
buildSessionStore(),
buildIdentityKeyStore(),
nowMillis(),
@@ -321,7 +311,6 @@ public class NativeBridge(
memScoped {
val result = alloc()
val remoteAddr = sender.toConstAddrCValue(this)
- val localAddr = localConstAddrCValue(this)
when (message.type) {
CiphertextMessageType.PRE_KEY -> {
@@ -331,7 +320,7 @@ public class NativeBridge(
preKeyMsgConst.raw = preKeyMsg.raw
checkErr(signal_decrypt_pre_key_message(
result.ptr, preKeyMsgConst.readValue(),
- remoteAddr, localAddr,
+ remoteAddr,
buildSessionStore(), buildIdentityKeyStore(),
buildPreKeyStore(), buildSignedPreKeyStore(), buildKyberPreKeyStore(),
))
@@ -344,7 +333,7 @@ public class NativeBridge(
signalMsgConst.raw = signalMsg.raw
checkErr(signal_decrypt_message(
result.ptr, signalMsgConst.readValue(),
- remoteAddr, localAddr,
+ remoteAddr,
buildSessionStore(), buildIdentityKeyStore(),
))
checkErr(signal_message_destroy(signalMsg.readValue()))
@@ -434,9 +423,25 @@ public class NativeBridge(
val sigArr = readOwnedBuf(sig.ptr)
checkErr(signal_privatekey_destroy(idPriv.readValue()))
- // Store the key pair AND its signature so the record can be
- // reconstructed faithfully when libsignal asks the store to load it.
- delegateRef.get().storeKyberKeyPair(keyId, kyberPair.raw!!, sigArr)
+ // Build the full KyberPreKeyRecord now and serialize it to bytes.
+ // Storing serialized bytes (not a long-lived raw FFI pointer) mirrors
+ // the EC pre-key path and the JVM bridge: the bytes survive memScope
+ // and store-callback boundaries intact, so the secret key decapsulates
+ // correctly on the receive path. The old raw-pointer approach left a
+ // dangling/again-borrowed handle and produced InvalidMessage (error 30).
+ val rec = alloc()
+ checkErr(signal_kyber_pre_key_record_new(
+ rec.ptr, keyId.toUInt(), nowMillis(), constKyberPair(kyberPair), sigArr.asBorrowedBuffer(this),
+ ))
+ val recConst = alloc()
+ recConst.raw = rec.raw
+ val recB = alloc()
+ checkErr(signal_kyber_pre_key_record_serialize(recB.ptr, recConst.readValue()))
+ val recBytes = readOwnedBuf(recB.ptr)
+ checkErr(signal_kyber_pre_key_record_destroy(rec.readValue()))
+ checkErr(signal_kyber_key_pair_destroy(kyberPair.readValue()))
+
+ delegateRef.get().storeKyberRecord(keyId, recBytes)
KyberPreKeyResult(keyId, kyberPubBytes, sigArr)
}
}
@@ -512,17 +517,13 @@ internal class StoreDelegate(
@Volatile
var capturedSession: ByteArray? = null
- // Store kyber key pair raw pointers. We use CPointer? because
- // the concrete SignalKyberKeyPair type is not directly importable in Kotlin 2.x cinterop.
- private val kyberKeyPairs = mutableMapOf?>()
+ // Store each Kyber pre-key as a serialized KyberPreKeyRecord. Bytes (rather
+ // than a raw FFI pointer) survive memScope/callback boundaries cleanly and let
+ // libsignal reconstruct the exact record — including the secret key — at load.
+ private val kyberRecords = mutableMapOf()
- // The identity signature over each kyber public key, kept so the loaded
- // record carries the real signature (not a zeroed placeholder).
- private val kyberSignatures = mutableMapOf()
-
- fun storeKyberKeyPair(id: Int, pair: CPointer?, signature: ByteArray) {
- kyberKeyPairs[id] = pair
- kyberSignatures[id] = signature
+ fun storeKyberRecord(id: Int, recordBytes: ByteArray) {
+ kyberRecords[id] = recordBytes
}
// ── Helpers ─────────────────────────────────────────────────────
@@ -533,14 +534,14 @@ internal class StoreDelegate(
* Uses the krypton_adapter C helper functions because we CANNOT access `.raw`
* on CValue parameters directly in Kotlin/Native cinterop.
*/
- private fun addressFromMutCValue(addr: CValue): ProtocolAddress? = memScoped {
+ private fun addressFromConstCValue(addr: CValue): ProtocolAddress? = memScoped {
val nameOut = alloc>()
val devOut = alloc()
- // krypton_address_get_name/signal_address_get_name
- // krypton_adapter handles the const conversion internally
- if (krypton_address_get_name(nameOut.ptr, addr) != null) return@memScoped null
- if (krypton_address_get_device_id(devOut.ptr, addr) != null) return@memScoped null
+ // 0.86.5 store callbacks receive CONST addresses, which signal_address_*
+ // consume directly — no krypton_adapter mut→const shim needed.
+ if (signal_address_get_name(nameOut.ptr, addr) != null) return@memScoped null
+ if (signal_address_get_device_id(devOut.ptr, addr) != null) return@memScoped null
ProtocolAddress(nameOut.value?.toKString() ?: return@memScoped null, DeviceId(devOut.value.toInt()))
}
@@ -589,8 +590,8 @@ internal class StoreDelegate(
// C callback implementations
// ════════════════════════════════════════════════════════════════
- fun cLoadSession(out: CPointer, address: CValue): Int = memScoped {
- val addr = addressFromMutCValue(address) ?: return@memScoped 1
+ fun cLoadSession(out: CPointer, address: CValue): Int = memScoped {
+ val addr = addressFromConstCValue(address) ?: return@memScoped 1
val result = runBlocking { sessionStore.loadSession(addr) }
when (val data = result.getOrNull()) {
null -> { out.pointed.raw = null; 0 }
@@ -603,13 +604,12 @@ internal class StoreDelegate(
}
}
- fun cStoreSession(address: CValue, record: CValue): Int = memScoped {
- val addr = addressFromMutCValue(address) ?: return@memScoped 1
+ fun cStoreSession(address: CValue, record: CValue): Int = memScoped {
+ val addr = addressFromConstCValue(address) ?: return@memScoped 1
val serialized = alloc()
-
- // Use krypton_adapter: takes mut session record CValue and serializes it
- // This avoids needing to access .raw or .ptr() on the CValue.
- if (krypton_session_record_serialize(serialized.ptr, record) != null) return@memScoped 1
+ // 0.86.5 passes a CONST session record, which signal_session_record_serialize
+ // consumes directly.
+ if (signal_session_record_serialize(serialized.ptr, record) != null) return@memScoped 1
val data = ownedBytes(serialized.ptr)
capturedSession = data
@@ -617,14 +617,16 @@ internal class StoreDelegate(
0
}
- fun cGetLocalIdentityKeyPair(out: CPointer): Int = memScoped {
- // Use krypton_adapter to build the identity key pair from serialized key bytes.
- // This avoids the "val cannot be reassigned" error on nested struct fields.
- val pubBytes = identityKeyPair.identityKey.publicKey.bytes
- val privBytes = identityKeyPair.privateKey.bytes
- if (krypton_build_identity_key_pair(out, pubBytes.toBorrowed(this), privBytes.toBorrowed(this)) != null) {
+ /**
+ * 0.86.5's get_identity_key_pair returns ONLY the private key; libsignal
+ * derives the public from it. Write the deserialized identity private key.
+ */
+ fun cGetIdentityKeyPair(out: CPointer): Int = memScoped {
+ val priv = alloc()
+ if (signal_privatekey_deserialize(priv.ptr, identityKeyPair.privateKey.bytes.toBorrowed(this)) != null) {
return@memScoped 1
}
+ out.pointed.raw = priv.raw
0
}
@@ -633,8 +635,8 @@ internal class StoreDelegate(
return 0
}
- fun cGetIdentityKey(out: CPointer, address: CValue): Int = memScoped {
- val addr = addressFromMutCValue(address) ?: return@memScoped 1
+ fun cGetIdentityKey(out: CPointer, address: CValue): Int = memScoped {
+ val addr = addressFromConstCValue(address) ?: return@memScoped 1
val result = runBlocking { identityKeyStore.getIdentity(addr) }
when (val pk = result.getOrNull()) {
null -> { out.pointed.raw = null; 0 }
@@ -647,34 +649,37 @@ internal class StoreDelegate(
}
}
+ /**
+ * 0.86.5: returns the `IdentityChange` value (0 = NewOrUnchanged) — there is
+ * no separate "changed" out-param. The CONST public key serializes directly.
+ */
fun cSaveIdentity(
- changedOut: CPointer,
- address: CValue,
- publicKey: CValue,
+ address: CValue,
+ publicKey: CValue,
): Int = memScoped {
- val addr = addressFromMutCValue(address) ?: return@memScoped 1
- // Use krypton_adapter to serialize the public key directly from CValue
+ val addr = addressFromConstCValue(address) ?: return@memScoped -1
val serialized = alloc()
- if (krypton_publickey_serialize(serialized.ptr, publicKey) != null) return@memScoped 1
+ if (signal_publickey_serialize(serialized.ptr, publicKey) != null) return@memScoped -1
val pkBytes = ownedBytes(serialized.ptr)
runBlocking { identityKeyStore.saveIdentity(addr, PublicKey(pkBytes)) }
- changedOut.pointed.value = 0u
- 0
+ 0 // IdentityChange::NewOrUnchanged
}
+ /**
+ * 0.86.5: the trust result is the RETURN value — 1 = trusted, 0 = untrusted
+ * (negative = error). No bool out-param.
+ */
fun cIsTrustedIdentity(
- out: CPointer,
- address: CValue,
- publicKey: CValue,
+ address: CValue,
+ publicKey: CValue,
direction: UInt,
): Int = memScoped {
- val addr = addressFromMutCValue(address) ?: return@memScoped 1
+ val addr = addressFromConstCValue(address) ?: return@memScoped -1
val serialized = alloc()
- if (krypton_publickey_serialize(serialized.ptr, publicKey) != null) return@memScoped 1
+ if (signal_publickey_serialize(serialized.ptr, publicKey) != null) return@memScoped -1
val pkBytes = ownedBytes(serialized.ptr)
val result = runBlocking { identityKeyStore.isTrustedIdentity(addr, PublicKey(pkBytes)) }
- out.pointed.value = result.getOrNull() ?: true
- 0
+ if (result.getOrNull() ?: true) 1 else 0
}
fun cLoadPreKey(out: CPointer, id: UInt): Int = memScoped {
@@ -694,7 +699,7 @@ internal class StoreDelegate(
}
}
- fun cStorePreKey(id: UInt, record: CValue): Int = 0
+ fun cStorePreKey(id: UInt, record: CValue): Int = 0
fun cRemovePreKey(id: UInt): Int {
runBlocking { preKeyStore.removePreKey(id.toInt()) }
@@ -719,29 +724,27 @@ internal class StoreDelegate(
}
}
- fun cStoreSignedPreKey(id: UInt, record: CValue): Int = 0
+ fun cStoreSignedPreKey(id: UInt, record: CValue): Int = 0
fun cLoadKyberPreKey(out: CPointer, id: UInt): Int = memScoped {
- val pair = kyberKeyPairs[id.toInt()]
- if (pair == null) {
+ val recBytes = kyberRecords[id.toInt()]
+ if (recBytes == null) {
out.pointed.raw = null
return@memScoped 0
}
+ // Deserialize the exact record that was serialized at generation time —
+ // the secret key inside is what libsignal decapsulates against.
val rec = alloc()
- val now = (NSDate().timeIntervalSince1970 * 1000.0).toULong()
- // Use krypton_adapter: takes raw void* pointer for kyber key pair.
- // Pass the REAL identity signature stored at generation time.
- val signature = kyberSignatures[id.toInt()] ?: ByteArray(0)
- if (krypton_kyber_pre_key_record_new_from_raw(rec.ptr, id, now, pair, signature.toBorrowed(this)) != null) return@memScoped 1
+ if (signal_kyber_pre_key_record_deserialize(rec.ptr, recBytes.toBorrowed(this)) != null) return@memScoped 1
out.pointed.raw = rec.raw
0
}
- fun cStoreKyberPreKey(id: UInt, record: CValue): Int = 0
+ fun cStoreKyberPreKey(id: UInt, record: CValue): Int = 0
- fun cMarkKyberPreKeyUsed(id: UInt, ecPrekeyId: UInt, baseKey: CValue): Int {
- kyberKeyPairs.remove(id.toInt())
- kyberSignatures.remove(id.toInt())
+ fun cMarkKyberPreKeyUsed(id: UInt, signedPrekeyId: UInt, baseKey: CValue): Int {
+ // Last-resort kyber keys are reused; one-time kyber keys would be removed
+ // here. Keep the record so repeat decrypts in tests still resolve.
return 0
}
}
diff --git a/krypton-protocol/src/appleTest/kotlin/io/krypton/protocol/NativeRealCryptoTest.kt b/krypton-protocol/src/appleTest/kotlin/io/krypton/protocol/NativeRealCryptoTest.kt
index 34aba09..cf3a84f 100644
--- a/krypton-protocol/src/appleTest/kotlin/io/krypton/protocol/NativeRealCryptoTest.kt
+++ b/krypton-protocol/src/appleTest/kotlin/io/krypton/protocol/NativeRealCryptoTest.kt
@@ -8,18 +8,17 @@ import io.krypton.protocol.api.decrypt
import io.krypton.protocol.api.encrypt
import io.krypton.storage.memory.InMemoryPreKeyStore
import kotlinx.coroutines.runBlocking
-import kotlin.test.Ignore
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
/**
* Real end-to-end crypto on Apple platforms — exercises the actual [NativeBridge]
- * against libsignal_ffi (no fakes). Runs on every Apple target via `appleTest`.
+ * against libsignal_ffi 0.86.5 (no fakes). Runs on every Apple target via `appleTest`.
*
- * Status: the **send path** (real Kyber pre-key generation + X3DH + encrypt) is
- * verified here. The full **receive path** (PreKey decrypt) is a known WIP — see
- * the `@Ignore`d test below; it is documented openly, not faked.
+ * Both the **send path** (real Kyber pre-key + X3DH + encrypt) and the full
+ * **receive path** (PreKey decrypt, full round-trip) are verified here against
+ * real libsignal — matching the JVM/Android tracks.
*/
class NativeRealCryptoTest {
@@ -67,14 +66,13 @@ class NativeRealCryptoTest {
}
/**
- * KNOWN ISSUE (not yet passing): the native PreKey **decrypt** path returns
- * libsignal error 30 (InvalidMessage). The send path above is correct; the
- * receive-path FFI store wiring needs more work. Kept and `@Ignore`d so the
- * gap is tracked in the open instead of pretending it works.
+ * Full native round-trip: Alice runs X3DH against Bob's bundle, encrypts a
+ * PreKey message, and Bob decrypts it back to plaintext — all on real
+ * libsignal_ffi 0.86.5. (Previously failed with InvalidMessage/error 30 when
+ * the bridge was built against a mismatched newer-API libsignal binary.)
*/
- @Ignore
@Test
- fun `real native full encrypt-decrypt round-trip WIP InvalidMessage on receive`() = runBlocking {
+ fun `real native full encrypt-decrypt round-trip`() = runBlocking {
val (alice, _) = protocolWithPreKeys()
val (bob, _) = protocolWithPreKeys()
val bobAddr = ProtocolAddress("bob", DeviceId.PRIMARY)
diff --git a/krypton-storage/build.gradle.kts b/krypton-storage/build.gradle.kts
index c996609..0145806 100644
--- a/krypton-storage/build.gradle.kts
+++ b/krypton-storage/build.gradle.kts
@@ -1,8 +1,11 @@
@file:OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
+import io.krypton.Configuration
+
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.android.library)
+ alias(libs.plugins.vanniktech.publish)
}
kotlin {
@@ -16,9 +19,6 @@ kotlin {
iosSimulatorArm64()
macosX64()
macosArm64()
- tvosX64()
- tvosArm64()
- tvosSimulatorArm64()
linuxX64()
mingwX64()
wasmJs { browser() }
@@ -41,3 +41,7 @@ android {
compileSdk = io.krypton.Configuration.COMPILE_SDK
defaultConfig { minSdk = io.krypton.Configuration.MIN_SDK }
}
+
+mavenPublishing {
+ coordinates(Configuration.GROUP, "krypton-storage", Configuration.VERSION)
+}
diff --git a/krypton/build.gradle.kts b/krypton/build.gradle.kts
index 136b50a..31afb27 100644
--- a/krypton/build.gradle.kts
+++ b/krypton/build.gradle.kts
@@ -1,19 +1,13 @@
@file:OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
+import io.krypton.Configuration
+
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.android.library)
- `maven-publish`
+ alias(libs.plugins.vanniktech.publish)
}
-// ── Single-artifact coordinates: io.krypton:krypton: ────────────────
-// This module is the ONE dependency consumers add. It re-exports the full
-// real public API (core + storage + protocol) via `api(...)`, so a single
-// dependency line works on every KMP target. The stub modules
-// (sealed-sender, zkgroup, double-ratchet) are intentionally NOT re-exported.
-group = io.krypton.Configuration.GROUP
-version = io.krypton.Configuration.VERSION
-
kotlin {
explicitApi()
jvmToolchain(17)
@@ -25,9 +19,6 @@ kotlin {
iosSimulatorArm64()
macosX64()
macosArm64()
- tvosX64()
- tvosArm64()
- tvosSimulatorArm64()
linuxX64()
mingwX64()
wasmJs { browser() }
@@ -52,16 +43,16 @@ android {
defaultConfig { minSdk = io.krypton.Configuration.MIN_SDK }
}
-// The Kotlin Multiplatform plugin creates one root publication ("kotlinMultiplatform")
-// plus a per-target publication. Rename the root coordinate to the clean `krypton`
-// artifact id so consumers depend on a single `io.krypton:krypton`.
-publishing {
- publications.withType().configureEach {
- if (name == "kotlinMultiplatform") {
- artifactId = "krypton"
- }
- }
- repositories {
- mavenLocal()
- }
+// ── Single-artifact coordinates: io.krypton:krypton: ────────────────
+// This module is the ONE dependency consumers add. It re-exports the full real
+// public API (core + storage + protocol) via `api(...)`, so a single dependency
+// line works on every KMP target — the re-exported modules resolve transitively
+// because they are published under the same group. The stub modules
+// (sealed-sender, zkgroup, double-ratchet) are intentionally NOT re-exported.
+//
+// vanniktech maps the KMP root publication ("kotlinMultiplatform") to this clean
+// artifact id automatically, signs releases (RELEASE_SIGNING_ENABLED), reads POM
+// metadata from gradle.properties, and targets the Central Portal (SONATYPE_HOST).
+mavenPublishing {
+ coordinates(Configuration.GROUP, "krypton", Configuration.VERSION)
}
diff --git a/samples/composeApp/build.gradle.kts b/samples/composeApp/build.gradle.kts
new file mode 100644
index 0000000..6c1178d
--- /dev/null
+++ b/samples/composeApp/build.gradle.kts
@@ -0,0 +1,58 @@
+import org.jetbrains.compose.desktop.application.dsl.TargetFormat
+
+plugins {
+ alias(libs.plugins.kotlin.multiplatform)
+ alias(libs.plugins.compose.multiplatform)
+ alias(libs.plugins.kotlin.compose)
+}
+
+kotlin {
+ jvmToolchain(17)
+
+ jvm("desktop")
+
+ // Bring-your-own-libsignal: Krypton ships no Signal binary, so a consumer (this
+ // sample included) supplies libsignal_ffi.a at link time with -L. Here we point
+ // at the in-repo build output; an external app points at its fetched dir
+ // (see scripts/fetch-libsignal-ffi.sh and the README "Bring your own libsignal").
+ val ffiDir = "${rootProject.projectDir}/krypton-protocol/libs/apple"
+ iosArm64 {
+ binaries.framework { baseName = "ComposeApp"; isStatic = true; linkerOpts("-L$ffiDir/ios-arm64") }
+ }
+ iosSimulatorArm64 {
+ binaries.framework { baseName = "ComposeApp"; isStatic = true; linkerOpts("-L$ffiDir/ios-sim-arm64") }
+ }
+
+ // The repo disables the default hierarchy template (krypton-protocol needs a
+ // custom one), so wire a shared iosMain by hand.
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ // The single Krypton dependency — real libsignal E2EE, shared API.
+ implementation(project(":krypton"))
+ implementation(compose.runtime)
+ implementation(compose.foundation)
+ implementation(compose.material3)
+ implementation(compose.ui)
+ implementation(libs.kotlinx.coroutines.core)
+ }
+ }
+ val iosMain by creating { dependsOn(commonMain) }
+ val iosArm64Main by getting { dependsOn(iosMain) }
+ val iosSimulatorArm64Main by getting { dependsOn(iosMain) }
+ val desktopMain by getting {
+ dependencies { implementation(compose.desktop.currentOs) }
+ }
+ }
+}
+
+compose.desktop {
+ application {
+ mainClass = "io.krypton.sample.app.MainKt"
+ nativeDistributions {
+ targetFormats(TargetFormat.Dmg)
+ packageName = "KryptonChat"
+ packageVersion = "1.0.0"
+ }
+ }
+}
diff --git a/samples/composeApp/iosApp/iosApp/ComposeView.swift b/samples/composeApp/iosApp/iosApp/ComposeView.swift
new file mode 100644
index 0000000..dec224d
--- /dev/null
+++ b/samples/composeApp/iosApp/iosApp/ComposeView.swift
@@ -0,0 +1,12 @@
+import SwiftUI
+import UIKit
+import ComposeApp
+
+/// Hosts the shared Compose UI (`MainViewController()` from commonMain) in SwiftUI.
+struct ComposeView: UIViewControllerRepresentable {
+ func makeUIViewController(context: Context) -> UIViewController {
+ MainViewControllerKt.MainViewController()
+ }
+
+ func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
+}
diff --git a/samples/composeApp/iosApp/iosApp/Info.plist b/samples/composeApp/iosApp/iosApp/Info.plist
new file mode 100644
index 0000000..3672d5e
--- /dev/null
+++ b/samples/composeApp/iosApp/iosApp/Info.plist
@@ -0,0 +1,35 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Krypton Chat
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+
+ CADisableMinimumFrameDurationOnPhone
+
+ LSRequiresIPhoneOS
+
+ UILaunchScreen
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+
+
+
diff --git a/samples/composeApp/iosApp/iosApp/iOSApp.swift b/samples/composeApp/iosApp/iosApp/iOSApp.swift
new file mode 100644
index 0000000..dfdacf6
--- /dev/null
+++ b/samples/composeApp/iosApp/iosApp/iOSApp.swift
@@ -0,0 +1,10 @@
+import SwiftUI
+
+@main
+struct iOSApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ComposeView().ignoresSafeArea()
+ }
+ }
+}
diff --git a/samples/composeApp/iosApp/project.yml b/samples/composeApp/iosApp/project.yml
new file mode 100644
index 0000000..95ce9f7
--- /dev/null
+++ b/samples/composeApp/iosApp/project.yml
@@ -0,0 +1,27 @@
+name: iosApp
+options:
+ bundleIdPrefix: io.krypton
+ deploymentTarget:
+ iOS: "16.0"
+settings:
+ base:
+ PRODUCT_BUNDLE_IDENTIFIER: io.krypton.sample
+ SWIFT_VERSION: "5.0"
+ ENABLE_USER_SCRIPT_SANDBOXING: NO
+targets:
+ iosApp:
+ type: application
+ platform: iOS
+ sources:
+ - path: iosApp
+ settings:
+ base:
+ FRAMEWORK_SEARCH_PATHS: $(SRCROOT)/../build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)
+ OTHER_LDFLAGS: $(inherited) -framework ComposeApp
+ INFOPLIST_FILE: iosApp/Info.plist
+ preBuildScripts:
+ - name: Compile Kotlin Framework
+ basedOnDependencyAnalysis: false
+ script: |
+ cd "$SRCROOT/../../.."
+ ./gradlew :samples:composeApp:embedAndSignAppleFrameworkForXcode
diff --git a/samples/composeApp/src/commonMain/kotlin/io/krypton/sample/app/App.kt b/samples/composeApp/src/commonMain/kotlin/io/krypton/sample/app/App.kt
new file mode 100644
index 0000000..3d1e7f7
--- /dev/null
+++ b/samples/composeApp/src/commonMain/kotlin/io/krypton/sample/app/App.kt
@@ -0,0 +1,199 @@
+package io.krypton.sample.app
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.Button
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import io.krypton.core.types.DeviceId
+import io.krypton.core.types.ProtocolAddress
+import io.krypton.protocol.api.KryptonConfigurator
+import io.krypton.protocol.api.KryptonProtocol
+import io.krypton.protocol.api.decrypt
+import io.krypton.protocol.api.encrypt
+import io.krypton.storage.memory.InMemoryPreKeyStore
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/** Per-platform label for the status line. */
+expect fun platformName(): String
+
+private data class ChatMsg(val from: String, val text: String, val wire: String)
+
+private class Chat(val alice: KryptonProtocol, val bob: KryptonProtocol)
+
+private suspend fun newParty(): KryptonProtocol {
+ val pk = InMemoryPreKeyStore()
+ val p = KryptonConfigurator().apply { preKeyStore = pk }.build()
+ pk.saveSignedPreKey(1, p.generateSignedPreKey(1).getOrThrow()).getOrThrow()
+ p.generatePreKeys(1, 10).getOrThrow().forEach { pk.storePreKey(it.keyId, it).getOrThrow() }
+ return p
+}
+
+private suspend fun setupChat(): Chat {
+ val alice = newParty()
+ val bob = newParty()
+ bob.processPreKeyBundle(alice.getPreKeyBundle(ProtocolAddress("alice", DeviceId(1))).getOrThrow()).getOrThrow()
+ alice.processPreKeyBundle(bob.getPreKeyBundle(ProtocolAddress("bob", DeviceId(1))).getOrThrow()).getOrThrow()
+ return Chat(alice, bob)
+}
+
+/** Encrypts [text] from [from] and verifies the peer decrypts it; returns the bubble. */
+private suspend fun exchange(c: Chat, from: String, text: String): ChatMsg {
+ val to = if (from == "Alice") "bob" else "alice"
+ val encP = if (from == "Alice") c.alice else c.bob
+ val decP = if (from == "Alice") c.bob else c.alice
+ val wire = encP.encrypt(to, text).getOrThrow()
+ decP.decrypt(from.lowercase(), wire).getOrThrow() // prove the peer really decrypts it
+ return ChatMsg(from, text, wire)
+}
+
+/** The shared chat UI — identical on desktop and iOS. */
+@Composable
+fun App() {
+ MaterialTheme(colorScheme = darkColorScheme()) {
+ val scope = rememberCoroutineScope()
+ var chat by remember { mutableStateOf(null) }
+ var status by remember { mutableStateOf("Setting up sessions…") }
+ val messages = remember { mutableStateListOf() }
+ var input by remember { mutableStateOf("") }
+ var sender by remember { mutableStateOf("Alice") }
+ val listState = rememberLazyListState()
+
+ LaunchedEffect(Unit) {
+ runCatching {
+ withContext(Dispatchers.Default) {
+ val c = setupChat()
+ // Seed a short real-encrypted exchange so the wire is visible at a glance.
+ val seed = listOf(
+ exchange(c, "Alice", "Hi Bob! 👋 This is end-to-end encrypted."),
+ exchange(c, "Bob", "Got it, Alice 🔐 real libsignal on-device."),
+ exchange(c, "Alice", "Every bubble shows the ciphertext that travels."),
+ )
+ c to seed
+ }
+ }
+ .onSuccess { (c, seed) ->
+ chat = c
+ messages.addAll(seed)
+ status = "Real libsignal · ${platformName()} · X3DH session established"
+ }
+ .onFailure { status = "Setup failed: ${it.message}" }
+ }
+
+ fun send() {
+ val c = chat ?: return
+ val text = input.trim()
+ if (text.isEmpty()) return
+ input = ""
+ val from = sender
+ scope.launch {
+ runCatching {
+ val msg = withContext(Dispatchers.Default) { exchange(c, from, text) }
+ messages.add(msg)
+ listState.animateScrollToItem(messages.size)
+ }.onFailure { status = "Error: ${it.message}" }
+ }
+ }
+
+ Scaffold(
+ topBar = {
+ Column(Modifier.fillMaxWidth().padding(16.dp)) {
+ Text("🔐 Krypton Encrypted Chat", style = MaterialTheme.typography.titleLarge)
+ Text(status, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary)
+ }
+ },
+ ) { pad ->
+ Column(Modifier.fillMaxSize().padding(pad)) {
+ if (chat == null) {
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() }
+ return@Column
+ }
+ LazyColumn(
+ state = listState,
+ modifier = Modifier.weight(1f).fillMaxWidth().padding(horizontal = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ items(messages) { msg -> Bubble(msg) }
+ }
+ Row(Modifier.fillMaxWidth().padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
+ FilterChip(selected = sender == "Alice", onClick = { sender = "Alice" }, label = { Text("Alice") })
+ Spacer(Modifier.width(8.dp))
+ FilterChip(selected = sender == "Bob", onClick = { sender = "Bob" }, label = { Text("Bob") })
+ Spacer(Modifier.width(12.dp))
+ OutlinedTextField(
+ value = input,
+ onValueChange = { input = it },
+ placeholder = { Text("Message as $sender…") },
+ singleLine = true,
+ modifier = Modifier.weight(1f),
+ )
+ Spacer(Modifier.width(12.dp))
+ Button(onClick = ::send) { Text("Send 🔒") }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun Bubble(msg: ChatMsg) {
+ val mine = msg.from == "Alice"
+ val align = if (mine) Alignment.Start else Alignment.End
+ Column(Modifier.fillMaxWidth(), horizontalAlignment = align) {
+ Card(
+ colors = CardDefaults.cardColors(
+ containerColor = if (mine) MaterialTheme.colorScheme.primaryContainer
+ else MaterialTheme.colorScheme.secondaryContainer,
+ ),
+ modifier = Modifier.widthIn(max = 460.dp),
+ ) {
+ Column(Modifier.padding(12.dp)) {
+ Text(msg.from, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary)
+ Text(msg.text, style = MaterialTheme.typography.bodyLarge)
+ Spacer(Modifier.padding(2.dp))
+ Text(
+ "🔒 ${msg.wire.take(40)}… (${msg.wire.length} chars)",
+ fontFamily = FontFamily.Monospace,
+ fontSize = 10.sp,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ }
+ }
+}
diff --git a/samples/composeApp/src/desktopMain/kotlin/io/krypton/sample/app/main.kt b/samples/composeApp/src/desktopMain/kotlin/io/krypton/sample/app/main.kt
new file mode 100644
index 0000000..35e40a1
--- /dev/null
+++ b/samples/composeApp/src/desktopMain/kotlin/io/krypton/sample/app/main.kt
@@ -0,0 +1,18 @@
+package io.krypton.sample.app
+
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Window
+import androidx.compose.ui.window.application
+import androidx.compose.ui.window.rememberWindowState
+
+actual fun platformName(): String = "Desktop (JVM, ${System.getProperty("os.arch")})"
+
+fun main() = application {
+ Window(
+ onCloseRequest = ::exitApplication,
+ state = rememberWindowState(width = 760.dp, height = 660.dp),
+ title = "Krypton — Encrypted Chat (KMP)",
+ ) {
+ App()
+ }
+}
diff --git a/samples/composeApp/src/iosMain/kotlin/io/krypton/sample/app/MainViewController.kt b/samples/composeApp/src/iosMain/kotlin/io/krypton/sample/app/MainViewController.kt
new file mode 100644
index 0000000..80dd6bb
--- /dev/null
+++ b/samples/composeApp/src/iosMain/kotlin/io/krypton/sample/app/MainViewController.kt
@@ -0,0 +1,14 @@
+package io.krypton.sample.app
+
+import androidx.compose.ui.window.ComposeUIViewController
+import platform.UIKit.UIDevice
+
+actual fun platformName(): String =
+ "${UIDevice.currentDevice.systemName} ${UIDevice.currentDevice.systemVersion}"
+
+/**
+ * iOS entry point — consumed by the Xcode `iosApp` project via
+ * `ComposeApp.MainViewControllerKt.MainViewController()`.
+ */
+@Suppress("FunctionName", "unused")
+fun MainViewController() = ComposeUIViewController { App() }
diff --git a/samples/jvm-demo/build.gradle.kts b/samples/jvm-demo/build.gradle.kts
new file mode 100644
index 0000000..100145e
--- /dev/null
+++ b/samples/jvm-demo/build.gradle.kts
@@ -0,0 +1,15 @@
+plugins {
+ alias(libs.plugins.kotlin.jvm)
+ application
+}
+
+dependencies {
+ // The single Krypton dependency — re-exports core + storage + protocol, and
+ // (on JVM) pulls libsignal-client with its bundled native dylib transitively.
+ implementation(project(":krypton"))
+ implementation(libs.kotlinx.coroutines.core)
+}
+
+kotlin { jvmToolchain(17) }
+
+application { mainClass.set("io.krypton.sample.MainKt") }
diff --git a/samples/jvm-demo/src/main/kotlin/io/krypton/sample/Main.kt b/samples/jvm-demo/src/main/kotlin/io/krypton/sample/Main.kt
new file mode 100644
index 0000000..9b1a943
--- /dev/null
+++ b/samples/jvm-demo/src/main/kotlin/io/krypton/sample/Main.kt
@@ -0,0 +1,61 @@
+package io.krypton.sample
+
+import io.krypton.core.types.DeviceId
+import io.krypton.core.types.ProtocolAddress
+import io.krypton.protocol.api.KryptonConfigurator
+import io.krypton.protocol.api.KryptonProtocol
+import io.krypton.protocol.api.decrypt
+import io.krypton.protocol.api.encrypt
+import io.krypton.storage.memory.InMemoryPreKeyStore
+import kotlinx.coroutines.runBlocking
+
+/**
+ * Minimal JVM (desktop) demo of a real end-to-end-encrypted exchange between two
+ * parties — Alice and Bob — running on the actual libsignal native crypto.
+ *
+ * Run it: ./gradlew :samples:jvm-demo:run
+ *
+ * No server here: we just hand Alice's pre-key bundle directly to Bob (in a real
+ * app that bundle travels via your server). Everything else — X3DH handshake,
+ * Double Ratchet, encrypt/decrypt — is the same code a production app would run.
+ */
+
+/** Builds a party with a signed pre-key + one-time pre-keys so it can publish a bundle. */
+private suspend fun newParty(): KryptonProtocol {
+ val preKeys = InMemoryPreKeyStore()
+ val protocol = KryptonConfigurator().apply { preKeyStore = preKeys }.build()
+ preKeys.saveSignedPreKey(1, protocol.generateSignedPreKey(1).getOrThrow()).getOrThrow()
+ protocol.generatePreKeys(1, 10).getOrThrow().forEach { preKeys.storePreKey(it.keyId, it).getOrThrow() }
+ return protocol
+}
+
+fun main() = runBlocking {
+ println("Krypton JVM demo — real libsignal on ${System.getProperty("os.name")} (${System.getProperty("os.arch")})")
+ println("─".repeat(64))
+
+ val alice = newParty()
+ val bob = newParty()
+ val aliceAddr = ProtocolAddress("alice", DeviceId(1))
+
+ // Bob fetches Alice's pre-key bundle and runs the X3DH handshake.
+ bob.processPreKeyBundle(alice.getPreKeyBundle(aliceAddr).getOrThrow()).getOrThrow()
+ println("✓ Session established (X3DH handshake)")
+
+ // Bob → Alice. The simple API returns a Base64 wire string you can send anywhere.
+ val plaintext = "Hello Alice — this is Bob, fully encrypted. 🔐"
+ val wire = bob.encrypt("alice", plaintext).getOrThrow()
+ println("✓ Bob encrypted : ${wire.take(56)}… (${wire.length} B64 chars)")
+
+ // Alice decrypts back to the original.
+ val decrypted = alice.decrypt("bob", wire).getOrThrow()
+ println("✓ Alice decrypted : $decrypted")
+
+ // Alice → Bob, on the now-ratcheting session.
+ val reply = "Got it, Bob. Ratchet is turning. 🔁"
+ val wire2 = alice.encrypt("bob", reply).getOrThrow()
+ println("✓ Bob decrypted : ${bob.decrypt("alice", wire2).getOrThrow()}")
+
+ println("─".repeat(64))
+ check(decrypted == plaintext) { "round-trip mismatch!" }
+ println("ROUND-TRIP OK ✅ real end-to-end encryption works on the JVM.")
+}
diff --git a/samples/ktor-server/README.md b/samples/ktor-server/README.md
new file mode 100644
index 0000000..9181276
--- /dev/null
+++ b/samples/ktor-server/README.md
@@ -0,0 +1,59 @@
+# Krypton companion server (Ktor + libsignal-server)
+
+A minimal backend for a Krypton-based messaging app, built on **Ktor** and
+**`org.signal:libsignal-server`** — the JVM-only server half of libsignal.
+
+The client (your app) uses **Krypton** for the actual end-to-end encryption. This server only does
+the things a client *can't*:
+
+| Endpoint | What it does | libsignal-server API |
+| --- | --- | --- |
+| `GET /v1/trust-root` | EC public key clients pin to validate certificates | `ECKeyPair` |
+| `POST /v1/keys/{user}` | store a client's pre-key bundle (base64 body) | — (transport) |
+| `GET /v1/keys/{user}` | another client fetches it to start an X3DH session | — (transport) |
+| `POST /v1/certificate/{uuid}/{device}` | issue a sealed-sender `SenderCertificate` (body = client's identity public key, base64) | `ServerCertificate.issue(...)` |
+| `GET /v1/zkgroup/public-params` | publish zkgroup server public params | `ServerSecretParams.getPublicParams()` |
+| `POST /v1/zkgroup/auth/{aci}/{pni}` | issue a zkgroup auth credential (UUIDs) | `ServerZkAuthOperations.issueAuthCredentialWithPniZkc(...)` |
+
+**This server never sees plaintext** — only ciphertext bundles and public credential material.
+
+## Run
+
+```sh
+./gradlew :samples:ktor-server:run # http://localhost:8080
+```
+
+```sh
+curl localhost:8080/v1/trust-root
+curl -X POST localhost:8080/v1/keys/alice -d ""
+curl localhost:8080/v1/keys/alice
+curl localhost:8080/v1/zkgroup/public-params
+curl -X POST localhost:8080/v1/zkgroup/auth/$(uuidgen)/$(uuidgen)
+```
+
+## Storage: in-memory here, your database in production
+
+This sample keeps pre-key bundles in a `ConcurrentHashMap` so it runs with zero setup. In production
+you replace that map with a real store — the rest of the server is unchanged.
+
+### Using Supabase / Postgres
+
+`libsignal-server` is a **JVM** library; **Supabase Edge Functions run on Deno (TypeScript)**, so the
+crypto **cannot** run inside Supabase. Instead, run *this* Ktor service on any JVM host (Fly.io,
+Railway, Cloud Run, a VM) and use Supabase as its **data layer**:
+
+```
+client (Krypton + Ktor client)
+ │ HTTPS
+ ▼
+this Ktor server ──(JDBC or supabase-kmp)──► Supabase (Postgres / Auth / Storage)
+ org.signal:libsignal-server just stores bundles & credentials; no libsignal
+```
+
+Two ways to reach Supabase from this JVM server:
+
+- **JDBC** — Supabase is Postgres; point any JDBC client at the connection string and swap the
+ `ConcurrentHashMap` for SQL `INSERT`/`SELECT`.
+- **supabase-kmp** — the [supabase-kmp](https://github.com/AndroidPoet/supabase-kmp) client runs on
+ the JVM, so a small adapter implementing "store/fetch pre-key bundle" against Supabase tables drops
+ straight in. That adapter belongs with your Supabase code, not in Krypton.
diff --git a/samples/ktor-server/build.gradle.kts b/samples/ktor-server/build.gradle.kts
new file mode 100644
index 0000000..6d10233
--- /dev/null
+++ b/samples/ktor-server/build.gradle.kts
@@ -0,0 +1,20 @@
+plugins {
+ alias(libs.plugins.kotlin.jvm)
+ application
+}
+
+dependencies {
+ // The SERVER half of libsignal — JVM-only. Version-matched to Krypton's client
+ // (org.signal:libsignal-server == org.signal:libsignal-client at the same version).
+ implementation("org.signal:libsignal-server:${libs.versions.libsignal.get()}")
+
+ // Ktor as the transport. Plain text endpoints keep the sample dependency-light
+ // (no serialization plugin needed); swap in ContentNegotiation for real JSON.
+ implementation("io.ktor:ktor-server-core:${libs.versions.ktor.get()}")
+ implementation("io.ktor:ktor-server-netty:${libs.versions.ktor.get()}")
+ runtimeOnly("ch.qos.logback:logback-classic:1.5.6")
+}
+
+kotlin { jvmToolchain(17) }
+
+application { mainClass.set("io.krypton.sample.server.AppKt") }
diff --git a/samples/ktor-server/src/main/kotlin/io/krypton/sample/server/App.kt b/samples/ktor-server/src/main/kotlin/io/krypton/sample/server/App.kt
new file mode 100644
index 0000000..bba88ce
--- /dev/null
+++ b/samples/ktor-server/src/main/kotlin/io/krypton/sample/server/App.kt
@@ -0,0 +1,112 @@
+package io.krypton.sample.server
+
+import io.ktor.http.HttpStatusCode
+import io.ktor.server.application.Application
+import io.ktor.server.engine.embeddedServer
+import io.ktor.server.netty.Netty
+import io.ktor.server.request.receiveText
+import io.ktor.server.response.respondText
+import io.ktor.server.routing.get
+import io.ktor.server.routing.post
+import io.ktor.server.routing.routing
+import org.signal.libsignal.metadata.certificate.ServerCertificate
+import org.signal.libsignal.protocol.ServiceId
+import org.signal.libsignal.protocol.ecc.ECKeyPair
+import org.signal.libsignal.protocol.ecc.ECPublicKey
+import org.signal.libsignal.zkgroup.ServerSecretParams
+import org.signal.libsignal.zkgroup.auth.ServerZkAuthOperations
+import java.time.Instant
+import java.util.Base64
+import java.util.Optional
+import java.util.UUID
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * A minimal Krypton companion server, built on Ktor + `org.signal:libsignal-server`
+ * (the JVM-only server half of libsignal). It does the two things a client can't:
+ *
+ * - hand out pre-key bundles so two clients can start an X3DH session, and
+ * - mint server-signed credentials (sealed-sender certificates, zkgroup auth).
+ *
+ * The actual end-to-end encryption stays on the client, in Krypton. This server
+ * never sees plaintext.
+ *
+ * Run it: ./gradlew :samples:ktor-server:run (listens on http://localhost:8080)
+ *
+ * Storage here is an in-memory map so the sample runs with zero setup. In production
+ * you'd persist to a database — see the README for swapping in Supabase/Postgres.
+ */
+
+// --- Server-held key material. In production: generate once, store in a vault/HSM,
+// and load on startup. Here we generate fresh each run. ---
+private val trustRoot = ECKeyPair.generate()
+private val serverKeyPair = ECKeyPair.generate()
+private val serverCertificate = ServerCertificate(trustRoot.privateKey, /* keyId = */ 1, serverKeyPair.publicKey)
+private val zkSecretParams = ServerSecretParams.generate()
+private val zkAuthOps = ServerZkAuthOperations(zkSecretParams)
+
+// --- In-memory store: user id -> base64 pre-key bundle. Swap for a real DB in prod. ---
+private val preKeyBundles = ConcurrentHashMap()
+
+private val b64 = Base64.getEncoder()
+private val b64d = Base64.getDecoder()
+
+fun main() {
+ embeddedServer(Netty, port = 8080) { module() }.start(wait = true)
+}
+
+fun Application.module() {
+ routing {
+ // Clients pin this trust root to validate the sealed-sender certificates below.
+ get("/v1/trust-root") {
+ call.respondText(b64.encodeToString(trustRoot.publicKey.serialize()))
+ }
+
+ // --- Pre-key bundle exchange: the transport the X3DH handshake needs. ---
+ // A client publishes the bundle Krypton produced (getPreKeyBundle), another fetches it.
+ post("/v1/keys/{user}") {
+ val user = call.parameters["user"]!!
+ preKeyBundles[user] = call.receiveText().trim()
+ call.respondText("stored", status = HttpStatusCode.Created)
+ }
+ get("/v1/keys/{user}") {
+ val bundle = preKeyBundles[call.parameters["user"]!!]
+ if (bundle == null) {
+ call.respondText("no bundle for that user", status = HttpStatusCode.NotFound)
+ } else {
+ call.respondText(bundle)
+ }
+ }
+
+ // --- Sealed sender: issue a SenderCertificate for a client (real libsignal-server crypto). ---
+ // Body = base64 of the client's identity public key. Returns a base64 certificate the
+ // client passes to Krypton's sealedSenderEncrypt(...).
+ post("/v1/certificate/{uuid}/{device}") {
+ val uuid = call.parameters["uuid"]!!
+ val device = call.parameters["device"]!!.toInt()
+ val identityKey = ECPublicKey(b64d.decode(call.receiveText().trim()))
+ val expiresAt = Instant.now().plusSeconds(24 * 60 * 60).toEpochMilli()
+ val cert = serverCertificate.issue(
+ serverKeyPair.privateKey,
+ uuid,
+ Optional.empty(), // sender E164 (phone) — omitted
+ device,
+ identityKey,
+ expiresAt,
+ )
+ call.respondText(b64.encodeToString(cert.serialized))
+ }
+
+ // --- zkgroup: publish server public params + issue an auth credential. ---
+ get("/v1/zkgroup/public-params") {
+ call.respondText(b64.encodeToString(zkSecretParams.publicParams.serialize()))
+ }
+ // POST /v1/zkgroup/auth/{aci}/{pni} — both are UUIDs.
+ post("/v1/zkgroup/auth/{aci}/{pni}") {
+ val aci = ServiceId.Aci(UUID.fromString(call.parameters["aci"]!!))
+ val pni = ServiceId.Pni(UUID.fromString(call.parameters["pni"]!!))
+ val response = zkAuthOps.issueAuthCredentialWithPniZkc(aci, pni, Instant.now())
+ call.respondText(b64.encodeToString(response.serialize()))
+ }
+ }
+}
diff --git a/scripts/build-libsignal-ffi.sh b/scripts/build-libsignal-ffi.sh
index 9902812..413046e 100755
--- a/scripts/build-libsignal-ffi.sh
+++ b/scripts/build-libsignal-ffi.sh
@@ -28,16 +28,37 @@ if [ ! -d "$WORK/.git" ]; then
fi
cd "$WORK"
-# Host build by default. Add `--target ` loops here for cross builds
-# (e.g. aarch64-apple-ios, x86_64-unknown-linux-gnu, x86_64-pc-windows-gnu).
-cargo build -p libsignal-ffi --release
+# Apple per-arch builds. Each Kotlin/Native Apple target links its OWN-arch .a, so
+# we build one archive per triple into libs/apple//. The Krypton cinterop
+# (krypton-protocol/build.gradle.kts → configureCInterop) maps target → subdir.
+#
+# triple -> subdir Kotlin target
+# aarch64-apple-darwin -> macos-arm64 macosArm64 (host; runnable tests)
+# aarch64-apple-ios -> ios-arm64 iosArm64 (device)
+# aarch64-apple-ios-sim -> ios-sim-arm64 iosSimulatorArm64 (runnable on arm64 sim)
+#
+# libsignal pins its own toolchain via rust-toolchain(.toml); cross stds must be
+# added for THAT toolchain, not just the default — so resolve it and add targets.
+TOOLCHAIN="$(rustup show active-toolchain 2>/dev/null | awk '{print $1}')"
+echo "==> libsignal toolchain: ${TOOLCHAIN:-}"
+
+build_arch() {
+ local triple="$1" subdir="$2"
+ echo "==> building libsignal-ffi for $triple -> $subdir"
+ rustup target add ${TOOLCHAIN:+--toolchain "$TOOLCHAIN"} "$triple" >/dev/null 2>&1 || true
+ cargo build -p libsignal-ffi --release --target "$triple"
+ mkdir -p "$DEST/$subdir"
+ cp "target/$triple/release/libsignal_ffi.a" "$DEST/$subdir/libsignal_ffi.a"
+}
mkdir -p "$DEST"
-A_FILE="$(find target -name 'libsignal_ffi.a' | head -1)"
-[ -n "$A_FILE" ] || { echo "ERROR: libsignal_ffi.a not found under target/" >&2; exit 1; }
-cp "$A_FILE" "$DEST/libsignal_ffi.a"
+build_arch aarch64-apple-darwin macos-arm64 # macosArm64 (Apple Silicon desktop)
+build_arch x86_64-apple-darwin macos-x64 # macosX64 (Intel desktop)
+build_arch aarch64-apple-ios ios-arm64 # iosArm64 (device)
+build_arch aarch64-apple-ios-sim ios-sim-arm64 # iosSimulatorArm64 (Apple Silicon sim)
+build_arch x86_64-apple-ios ios-sim-x64 # iosX64 (Intel sim)
-# Regenerate / copy the FFI header if present (path may vary by version).
+# Regenerate / copy the FFI header if present (arch-independent; path varies by version).
H_FILE="$(find . -name 'signal_ffi.h' | head -1 || true)"
[ -n "$H_FILE" ] && cp "$H_FILE" "$DEST/signal_ffi.h" || echo "WARN: signal_ffi.h not found — keep existing header"
@@ -45,7 +66,7 @@ H_FILE="$(find . -name 'signal_ffi.h' | head -1 || true)"
# the native .a matches the JVM/Android Maven version (no silent skew).
echo "$VERSION" > "$DEST/VERSION"
-echo "==> Updated $DEST/libsignal_ffi.a to libsignal $VERSION"
+echo "==> Updated per-arch libsignal_ffi.a (macos-arm64, macos-x64, ios-arm64, ios-sim-arm64, ios-sim-x64) to libsignal $VERSION"
echo " NOTE: bump 'libsignal' in gradle/libs.versions.toml to '$VERSION' too, or"
echo " ./gradlew check will fail on version skew."
-echo " Remember: the .a is gitignored (>100MB). Distribute via CI artifact / release / git-lfs."
+echo " Remember: the .a files are gitignored (>100MB). Distribute via CI artifact / release / git-lfs."
diff --git a/scripts/fetch-libsignal-ffi.sh b/scripts/fetch-libsignal-ffi.sh
new file mode 100755
index 0000000..4c15f42
--- /dev/null
+++ b/scripts/fetch-libsignal-ffi.sh
@@ -0,0 +1,98 @@
+#!/usr/bin/env bash
+#
+# fetch-libsignal-ffi.sh — get the libsignal_ffi static lib FROM SIGNAL'S OFFICIAL
+# SOURCE onto your own machine, for building a Krypton-based app on Apple targets.
+#
+# Krypton ships NO Signal binary (bring-your-own-libsignal). You run this; the
+# binary comes straight from Signal — there is nothing of Krypton's to trust.
+#
+# Two ways to obtain it, both official:
+# build (default) — git clone signalapp/libsignal @ the signed vX tag and
+# compile libsignal_ffi.a yourself (needs Rust/cargo).
+# download — fetch Signal's official prebuilt iOS archive from
+# build-artifacts.signal.org and verify its SHA-256.
+#
+# Usage:
+# scripts/fetch-libsignal-ffi.sh [outdir] [mode]
+# libsignal version, e.g. 0.86.5 (match Krypton's pinned version)
+# [outdir] where to place /libsignal_ffi.a (default: ~/.krypton/libsignal/)
+# [mode] build | download (default: build)
+#
+# Verify a download against Signal's published checksum:
+# LIBSIGNAL_FFI_PREBUILD_CHECKSUM= scripts/fetch-libsignal-ffi.sh 0.86.5 "" download
+#
+# After it runs, add the printed -L path(s) to your app's Apple targets, e.g.:
+# kotlin { iosArm64 { binaries.all { linkerOpts("-L/ios-arm64") } } }
+
+set -euo pipefail
+
+VERSION="${1:?usage: fetch-libsignal-ffi.sh [outdir] [build|download]}"
+OUTDIR="${2:-$HOME/.krypton/libsignal/$VERSION}"
+MODE="${3:-build}"
+
+# triple -> arch subdir (Krypton's cinterop expects these names)
+APPLE_ARCHES=(
+ "aarch64-apple-darwin:macos-arm64"
+ "x86_64-apple-darwin:macos-x64"
+ "aarch64-apple-ios:ios-arm64"
+ "aarch64-apple-ios-sim:ios-sim-arm64"
+ "x86_64-apple-ios:ios-sim-x64"
+)
+
+mkdir -p "$OUTDIR"
+echo "==> libsignal v$VERSION -> $OUTDIR (mode: $MODE)"
+
+if [ "$MODE" = "download" ]; then
+ ARCHIVE="libsignal-client-ios-build-v$VERSION.tar.gz"
+ URL="https://build-artifacts.signal.org/libraries/$ARCHIVE"
+ echo "==> downloading Signal's official prebuilt archive: $URL"
+ curl -fSL "$URL" -o "$OUTDIR/$ARCHIVE"
+ if [ -n "${LIBSIGNAL_FFI_PREBUILD_CHECKSUM:-}" ]; then
+ echo "==> verifying SHA-256 against Signal's published checksum"
+ echo "$LIBSIGNAL_FFI_PREBUILD_CHECKSUM $OUTDIR/$ARCHIVE" | shasum -a 256 -c -
+ else
+ echo "WARN: no LIBSIGNAL_FFI_PREBUILD_CHECKSUM set — download is UNVERIFIED."
+ echo " Get the checksum from Signal's release and re-run to verify."
+ fi
+ tar -xzf "$OUTDIR/$ARCHIVE" -C "$OUTDIR"
+ # Signal's archive lays the .a out as target//release/; relocate into the / layout
+ # (same as build mode) so the printed -L lines work. The prebuilt is iOS-only.
+ for entry in "${APPLE_ARCHES[@]}"; do
+ triple="${entry%%:*}"; subdir="${entry##*:}"
+ src="$OUTDIR/target/$triple/release/libsignal_ffi.a"
+ if [ -f "$src" ]; then
+ mkdir -p "$OUTDIR/$subdir"
+ cp "$src" "$OUTDIR/$subdir/libsignal_ffi.a"
+ echo "==> $subdir"; shasum -a 256 "$OUTDIR/$subdir/libsignal_ffi.a"
+ fi
+ done
+ rm -rf "$OUTDIR/target" "$OUTDIR/$ARCHIVE"
+ echo "==> Done (iOS-only). Add to your app's build.gradle.kts:"
+ echo " iosArm64 { binaries.all { linkerOpts(\"-L$OUTDIR/ios-arm64\") } }"
+ echo " iosSimulatorArm64 { binaries.all { linkerOpts(\"-L$OUTDIR/ios-sim-arm64\") } }"
+ echo " (macOS targets: re-run without 'download' to build them from source.)"
+ exit 0
+fi
+
+# mode: build — compile from Signal's official source at the v release tag.
+command -v cargo >/dev/null || { echo "ERROR: Rust/cargo required for build mode. Install via https://rustup.rs"; exit 1; }
+WORK="${TMPDIR:-/tmp}/libsignal-$VERSION"
+[ -d "$WORK/.git" ] || git clone --depth 1 --branch "v$VERSION" https://github.com/signalapp/libsignal "$WORK"
+cd "$WORK"
+TOOLCHAIN="$(rustup show active-toolchain 2>/dev/null | awk '{print $1}')"
+
+for entry in "${APPLE_ARCHES[@]}"; do
+ triple="${entry%%:*}"; subdir="${entry##*:}"
+ echo "==> building $triple -> $subdir"
+ rustup target add ${TOOLCHAIN:+--toolchain "$TOOLCHAIN"} "$triple" >/dev/null 2>&1 || true
+ cargo build -p libsignal-ffi --release --target "$triple"
+ mkdir -p "$OUTDIR/$subdir"
+ cp "target/$triple/release/libsignal_ffi.a" "$OUTDIR/$subdir/libsignal_ffi.a"
+ shasum -a 256 "$OUTDIR/$subdir/libsignal_ffi.a"
+done
+
+echo
+echo "==> Done. Add these to your app's build.gradle.kts so the linker finds it:"
+echo " iosArm64 { binaries.all { linkerOpts(\"-L$OUTDIR/ios-arm64\") } }"
+echo " iosSimulatorArm64 { binaries.all { linkerOpts(\"-L$OUTDIR/ios-sim-arm64\") } }"
+echo " macosArm64 { binaries.all { linkerOpts(\"-L$OUTDIR/macos-arm64\") } }"
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 8828c55..d3c2a3e 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -16,6 +16,7 @@ dependencyResolutionManagement {
rootProject.name = "krypton"
+include(":krypton-gradle-plugin")
include(":krypton-core")
include(":krypton")
include(":krypton-storage")
@@ -24,3 +25,6 @@ include(":krypton-sealed-sender")
include(":krypton-double-ratchet")
include(":krypton-zkgroup")
include(":samples:encrypted-chat")
+include(":samples:jvm-demo")
+include(":samples:ktor-server")
+include(":samples:composeApp")