From d1eb655f60c31e85381cc65d0f98b6f7a975c0d7 Mon Sep 17 00:00:00 2001 From: Ranbir Singh Date: Sat, 20 Jun 2026 13:44:40 +0530 Subject: [PATCH 1/7] Add Maven Central publishing, KMP samples, and libsignal CI/CD Publishing - Wire vanniktech maven-publish into krypton-core/storage/protocol/krypton so one dependency (io.krypton:krypton) resolves all modules transitively; POM metadata + AGPL-3.0 in gradle.properties. Native targets - Build real per-arch libsignal_ffi.a for all Apple targets (add macos-x64, ios-sim-x64); cinterop links each target's own arch, no cross-arch stand-ins. - Drop tvOS (Rust tier-3, unused) rather than ship broken artifacts. CI/CD - release.yml: single macOS job builds the .a fresh from signalapp/libsignal at the pinned tag and publishes every target to Maven Central. - ci.yml: rebuild + real-crypto tests (JVM + iOS sim) on every PR, so a libsignal bump is validated before release. - dependabot.yml + check-libsignal-update.yml: observe new libsignal releases. Samples - jvm-demo: headless encrypt/decrypt round-trip. - composeApp: one Compose UI shared across Desktop + iOS (XcodeGen iosApp). Housekeeping - Add AGPL-3.0 LICENSE; rewrite README to OSS standard. - Fix .gitignore: track gradle-wrapper.jar (was ignored, broke ./gradlew on clone); keep the 101 MB vendored libsignal jar out. --- .github/dependabot.yml | 26 + .github/workflows/ci.yml | 70 ++ .github/workflows/release.yml | 70 ++ .gitignore | 12 +- LICENSE | 661 ++++++++++++++++++ README.md | 252 +++++-- STATUS.md | 65 +- build.gradle.kts | 3 + gradle.properties | 23 + gradle/libs.versions.toml | 5 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes krypton-core/build.gradle.kts | 10 +- krypton-protocol/build.gradle.kts | 41 +- krypton-protocol/libs/apple/krypton_ffi.h | 75 +- krypton-protocol/libs/apple/signal_ffi.h | 492 ++++--------- .../src/appleMain/cinterop/libsignal_ffi.def | 8 +- .../krypton/protocol/bridge/NativeBridge.kt | 191 ++--- .../krypton/protocol/NativeRealCryptoTest.kt | 20 +- krypton-storage/build.gradle.kts | 10 +- krypton/build.gradle.kts | 39 +- samples/composeApp/build.gradle.kts | 56 ++ .../iosApp/iosApp/ComposeView.swift | 12 + samples/composeApp/iosApp/iosApp/Info.plist | 35 + samples/composeApp/iosApp/iosApp/iOSApp.swift | 10 + samples/composeApp/iosApp/project.yml | 27 + .../kotlin/io/krypton/sample/app/App.kt | 199 ++++++ .../kotlin/io/krypton/sample/app/main.kt | 18 + .../krypton/sample/app/MainViewController.kt | 14 + samples/jvm-demo/build.gradle.kts | 15 + .../src/main/kotlin/io/krypton/sample/Main.kt | 61 ++ scripts/build-libsignal-ffi.sh | 39 +- settings.gradle.kts | 2 + 32 files changed, 1911 insertions(+), 650 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 LICENSE create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 samples/composeApp/build.gradle.kts create mode 100644 samples/composeApp/iosApp/iosApp/ComposeView.swift create mode 100644 samples/composeApp/iosApp/iosApp/Info.plist create mode 100644 samples/composeApp/iosApp/iosApp/iOSApp.swift create mode 100644 samples/composeApp/iosApp/project.yml create mode 100644 samples/composeApp/src/commonMain/kotlin/io/krypton/sample/app/App.kt create mode 100644 samples/composeApp/src/desktopMain/kotlin/io/krypton/sample/app/main.kt create mode 100644 samples/composeApp/src/iosMain/kotlin/io/krypton/sample/app/MainViewController.kt create mode 100644 samples/jvm-demo/build.gradle.kts create mode 100644 samples/jvm-demo/src/main/kotlin/io/krypton/sample/Main.kt diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ef62ff9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,26 @@ +version: 2 + +# Observe libsignal releases automatically. Dependabot watches Maven Central for +# new org.signal:libsignal-* versions and opens a PR bumping the single +# `libsignal` ref in gradle/libs.versions.toml. Because libsignal-client and +# libsignal-android both use version.ref = "libsignal", that one bump moves BOTH +# the JVM/Android Maven coords and (via ci.yml rebuilding the .a from source) the +# Apple native track in lockstep. The PR then triggers ci.yml, which clones +# signalapp/libsignal at the new tag, rebuilds libsignal_ffi.a, and runs the +# real-crypto tests on JVM + iOS — so a bad/ABI-changing version goes red on the +# PR instead of in a release. +updates: + - package-ecosystem: gradle + directory: "/" + schedule: + interval: daily + # Only libsignal must stay in lockstep with the native binary; let the rest + # be bumped manually so unrelated deps don't churn the (heavy) native CI. + allow: + - dependency-name: "org.signal:libsignal-client" + - dependency-name: "org.signal:libsignal-android" + commit-message: + prefix: chore + labels: + - libsignal-bump + open-pull-requests-limit: 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d88cca9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,70 @@ +name: CI + +# Validates every PR / push — and crucially, proves a libsignal version bump +# actually works end-to-end BEFORE it is merged and tagged for release. +# +# The native libsignal_ffi.a is never committed: this job rebuilds it FRESH from +# Signal's source at the pinned version (cached so it only rebuilds when the +# version changes), embeds it into the Apple klib, and runs the real-crypto +# tests on JVM and the iOS simulator. If Signal ships a version whose FFI ABI +# changed, this job goes red here instead of breaking a published release. +on: + pull_request: + push: + branches: [main, master] + workflow_dispatch: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + verify: + runs-on: macos-14 # builds Apple natively + cross-compiles jvm/android/linux/wasm + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + + - uses: gradle/actions/setup-gradle@v4 + + - uses: dtolnay/rust-toolchain@stable + + # Single source of truth — the catalog version drives BOTH tracks. + - name: Resolve libsignal version + id: ls + run: | + v=$(grep -oE '^libsignal = "[^"]+"' gradle/libs.versions.toml | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') + echo "version=$v" >> "$GITHUB_OUTPUT" + echo "libsignal=$v" + + # Cache the built .a per version: a normal PR restores instantly; the first + # PR after a version bump pays the Rust build once, then everyone benefits. + - name: Cache libsignal_ffi.a + id: ffi-cache + uses: actions/cache@v4 + with: + path: krypton-protocol/libs/apple + key: libsignal-ffi-${{ steps.ls.outputs.version }}-apple-v1 + + - name: Build fresh libsignal_ffi.a from Signal source (cache miss) + if: steps.ffi-cache.outputs.cache-hit != 'true' + run: scripts/build-libsignal-ffi.sh ${{ steps.ls.outputs.version }} + + # Hard gate: native .a version must equal the JVM/Android Maven version. + - name: Verify native/Maven libsignal versions match + run: ./gradlew :krypton-protocol:verifyLibsignalVersion + + # Prove the (possibly new) libsignal works on both tracks. + - name: JVM real-crypto tests + run: ./gradlew :krypton-protocol:jvmTest + + - name: iOS simulator real-crypto tests + run: ./gradlew :krypton-protocol:iosSimulatorArm64Test + + # Make sure every published target still compiles against this version. + - name: Compile all published targets + run: ./gradlew :krypton:assemble diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a42a805 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,70 @@ +name: Release + +# Publishes the whole Krypton KMP library to Maven Central. +# +# Why a single macOS runner: it can build EVERY Kotlin target — all Apple arches +# natively, and it cross-compiles JVM, Android, Linux, Windows and wasm. The only +# native binaries Krypton needs are the Apple `libsignal_ffi.a` files; JVM/Android +# pull libsignal from Maven, and Linux/Windows/wasm are fail-loud (no binary). So +# one job builds the .a FRESH from libsignal's pinned source, embeds them into the +# klibs (cinterop `staticLibraries`), and publishes — consumers add one dependency +# and the native binary travels inside the artifact. +# +# Trigger: push a tag like `v0.1.0`, or run manually. Set these repo secrets: +# MAVEN_CENTRAL_USERNAME / MAVEN_CENTRAL_PASSWORD — Central Portal token +# SIGNING_KEY — ASCII-armored GPG private key (in-memory signing) +# SIGNING_PASSWORD — passphrase for that key +on: + push: + tags: ['v*'] + workflow_dispatch: + +jobs: + publish: + runs-on: macos-14 # Apple Silicon: native Apple builds + cross-compiles the rest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + + - uses: gradle/actions/setup-gradle@v4 + + - uses: dtolnay/rust-toolchain@stable + + # libsignal version is the single source of truth in the catalog; the native + # .a MUST match it (verifyLibsignalVersion enforces this below). + - name: Resolve libsignal version + id: ls + run: | + v=$(grep -oE '^libsignal = "[^"]+"' gradle/libs.versions.toml | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') + echo "version=$v" >> "$GITHUB_OUTPUT" + echo "libsignal=$v" + + # The Rust build of libsignal is slow; cache the produced .a per version so it + # only rebuilds when the libsignal version actually changes. + - name: Cache libsignal_ffi.a + id: ffi-cache + uses: actions/cache@v4 + with: + path: krypton-protocol/libs/apple + key: libsignal-ffi-${{ steps.ls.outputs.version }}-apple-v1 + + - name: Build fresh libsignal_ffi.a (cache miss) + if: steps.ffi-cache.outputs.cache-hit != 'true' + run: scripts/build-libsignal-ffi.sh ${{ steps.ls.outputs.version }} + + # Guard: native .a version must equal the JVM/Android Maven version, or an + # iOS user and an Android user could silently fail to talk to each other. + - name: Verify native/Maven libsignal versions match + run: ./gradlew :krypton-protocol:verifyLibsignalVersion + + - name: Publish + release to Maven Central + env: + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} + run: ./gradlew publishAndReleaseToMavenCentral --no-configuration-cache --stacktrace diff --git a/.gitignore b/.gitignore index 1ee7497..2b374bf 100644 --- a/.gitignore +++ b/.gitignore @@ -4,14 +4,22 @@ build/ local.properties .idea *.class -*.jar *.log .DS_Store .cxx captures .externalNativeBuild -# Prebuilt native crypto binaries (build artifacts — exceed GitHub size limits) +# Prebuilt native crypto binaries (build artifacts — rebuilt fresh in CI, +# exceed GitHub's size limits, and don't belong in source control). *.a *.o +# Vendored libsignal jars (~100 MB) — these come from Maven, never commit them. krypton-protocol/libs/**/*.jar + +# Generated Xcode project — the source of truth is project.yml. +# Run `xcodegen generate` in samples/composeApp/iosApp to recreate it. +samples/composeApp/iosApp/iosApp.xcodeproj/ + +# NOTE: the Gradle wrapper jar (gradle/wrapper/gradle-wrapper.jar) is intentionally +# committed so `./gradlew` works on a fresh clone — do not add a blanket *.jar rule. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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/README.md b/README.md index 57a3c7d..fbb6f7a 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. +

+ Maven Central + Kotlin + License + libsignal +

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

+ badge-android + badge-ios + badge-macos + badge-jvm +

-## 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. + +> Named after Krypton (Kr) — a noble gas that's inert, stable, and reacts with nothing. Like properly encrypted data. + +## Why Krypton -One dependency. It transitively brings the whole real API (`core` + `storage` + `protocol`): +- **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,197 @@ 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. On JVM/Android, +libsignal is fetched from Maven; on Apple, the native `libsignal_ffi.a` is embedded inside the +published artifact — **your users add nothing else.** + +> **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 -## Quick start (the stupid-simple API) +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 krypton = Krypton.protocol { } // zero config (auto identity + stores) +val alice = newParty() +val bob = newParty() -val wire = krypton.encrypt("alice", "Hello!").getOrThrow() // wire-ready Base64 string — send it -val text = krypton.decrypt("alice", wire).getOrThrow() // back to "Hello!" +// Bob fetches Alice's bundle (via your server) and runs X3DH. +bob.processPreKeyBundle(alice.getPreKeyBundle(ProtocolAddress("alice", DeviceId(1))).getOrThrow()) + +// 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 | -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. +## How big is it? -## API parity with libsignal +Krypton's own code is negligible (~0.2 MB). The size is libsignal — and far smaller than the +raw artifact suggests, because the linker keeps only what you call and release builds strip: + +| Platform | Real app-size impact (release) | +| --- | --- | +| iOS / macOS | **~3 MB** (static lib, dead-stripped at link) | +| Android (per ABI) | **~3 MB download / ~6.4 MB installed** (AGP strips `.so` on release) | +| JVM desktop | ~20 MB per OS | -Krypton aims to mirror Signal's libraries so you can follow libsignal's docs. Current coverage: +This is the same core that Signal and WhatsApp ship. + +## 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`), 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. +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: -## Publishing +- **`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. -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/`. +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. + +## 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 | + +## 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. ## 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/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/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 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 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 + "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() { + // ── 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, + // so it ships inside the published artifact — consumers link it with no setup. + // The per-arch .a files are built fresh in CI (scripts/build-libsignal-ffi.sh, + // run by .github/workflows/release.yml); they are gitignored, never committed. + fun KotlinNativeCompilation.configureCInterop(libArchDir: String) { 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") + extraOpts("-libraryPath", "${project.projectDir}/libs/apple/$libArchDir") } } } - // 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() } + // Every Apple target links its own real per-arch .a (no cross-arch stand-ins). + macosArm64 { compilations.getByName("main").configureCInterop("macos-arm64") } // Apple Silicon desktop + macosX64 { compilations.getByName("main").configureCInterop("macos-x64") } // Intel desktop + iosArm64 { compilations.getByName("main").configureCInterop("ios-arm64") } // device + iosSimulatorArm64 { compilations.getByName("main").configureCInterop("ios-sim-arm64") } // Apple Silicon sim + iosX64 { compilations.getByName("main").configureCInterop("ios-sim-x64") } // Intel sim } android { @@ -120,6 +117,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..9f14424 100644 --- a/krypton-protocol/src/appleMain/cinterop/libsignal_ffi.def +++ b/krypton-protocol/src/appleMain/cinterop/libsignal_ffi.def @@ -1,5 +1,7 @@ +# libraryPaths is supplied per-target by the Gradle build (configureCInterop) so +# each Apple target links its OWN-arch libsignal_ffi.a (macos-arm64 / ios-arm64 / +# ios-sim-arm64). compilerOpts uses a project-relative -I so the header resolves +# regardless of checkout path. 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 +staticLibraries = libsignal_ffi.a 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..510d18b --- /dev/null +++ b/samples/composeApp/build.gradle.kts @@ -0,0 +1,56 @@ +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") + + // device + Apple-Silicon simulator. Each links Krypton's per-arch libsignal_ffi. + iosArm64() + iosSimulatorArm64() + listOf(iosArm64(), iosSimulatorArm64()).forEach { target -> + target.binaries.framework { + baseName = "ComposeApp" + isStatic = true + } + } + + // 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/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/settings.gradle.kts b/settings.gradle.kts index 8828c55..efb9796 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,3 +24,5 @@ include(":krypton-sealed-sender") include(":krypton-double-ratchet") include(":krypton-zkgroup") include(":samples:encrypted-chat") +include(":samples:jvm-demo") +include(":samples:composeApp") From 124808cf492aca75910bfaead01855ed81599379 Mon Sep 17 00:00:00 2001 From: Ranbir Singh Date: Sat, 20 Jun 2026 19:06:24 +0530 Subject: [PATCH 2/7] Switch Apple targets to bring-your-own-libsignal + add Gradle plugin Krypton's published Apple artifacts now ship only the cinterop bindings (~88 KB klib) and no Signal binary. Consumers fetch libsignal_ffi from Signal's official source on their own machine, so there is nothing of ours for anyone to suspect having tampered with. - cinterop .def is headers-only with linkerOpts = -lsignal_ffi (no staticLibraries); the .a is supplied at the consumer's link step - krypton-gradle-plugin: id("io.krypton.libsignal") registers fetchLibsignal (clone signalapp/libsignal at the release tag + cargo, or download the official prebuilt + SHA-256 check) and auto-wires the -L paths onto every Apple native target - scripts/fetch-libsignal-ffi.sh: standalone fetch for non-plugin builds - SECURITY.md: documents the trust model (build from source is default) - README: Bring-your-own-libsignal section (plugin first, script second) Verified against libsignal v0.86.5: the from-source build and the official prebuilt both produce a real libsignal_ffi.a in which all of Krypton's cinterop header symbols resolve. The release tag is annotated by a Signal maintainer but not GPG-signed, so docs anchor trust on the org + commit, not a signature. The official prebuilt is iOS-only; download mode warns and points macOS at the source build. --- .github/workflows/release.yml | 10 +- README.md | 95 +++++++++++- SECURITY.md | 49 ++++++ krypton-gradle-plugin/build.gradle.kts | 36 +++++ .../io/krypton/gradle/LibsignalPlugin.kt | 144 ++++++++++++++++++ krypton-protocol/build.gradle.kts | 37 ++--- .../src/appleMain/cinterop/libsignal_ffi.def | 15 +- samples/composeApp/build.gradle.kts | 18 ++- scripts/fetch-libsignal-ffi.sh | 98 ++++++++++++ settings.gradle.kts | 1 + 10 files changed, 463 insertions(+), 40 deletions(-) create mode 100644 SECURITY.md create mode 100644 krypton-gradle-plugin/build.gradle.kts create mode 100644 krypton-gradle-plugin/src/main/kotlin/io/krypton/gradle/LibsignalPlugin.kt create mode 100755 scripts/fetch-libsignal-ffi.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a42a805..6daea81 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,11 +4,11 @@ name: Release # # Why a single macOS runner: it can build EVERY Kotlin target — all Apple arches # natively, and it cross-compiles JVM, Android, Linux, Windows and wasm. The only -# native binaries Krypton needs are the Apple `libsignal_ffi.a` files; JVM/Android -# pull libsignal from Maven, and Linux/Windows/wasm are fail-loud (no binary). So -# one job builds the .a FRESH from libsignal's pinned source, embeds them into the -# klibs (cinterop `staticLibraries`), and publishes — consumers add one dependency -# and the native binary travels inside the artifact. +# Krypton publishes only cinterop bindings (no Signal binary embedded — see +# SECURITY.md / "Bring your own libsignal"). This job builds the Apple .a FRESH +# from libsignal's pinned source ONLY to run the real-crypto tests before +# publishing; the published Apple klibs stay binding-only and Apple consumers +# fetch libsignal themselves. JVM/Android libsignal comes from Maven. # # Trigger: push a tag like `v0.1.0`, or run manually. Set these repo secrets: # MAVEN_CENTRAL_USERNAME / MAVEN_CENTRAL_PASSWORD — Central Portal token diff --git a/README.md b/README.md index fbb6f7a..954407a 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,13 @@ dependencies { } ``` -One line pulls the full API (`core` + `storage` + `protocol`) on every target. On JVM/Android, -libsignal is fetched from Maven; on Apple, the native `libsignal_ffi.a` is embedded inside the -published artifact — **your users add nothing else.** +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 @@ -127,10 +131,71 @@ The full Signal handshake (real ~1568-byte Kyber pre-key → X3DH → encrypt by `NativeRealCryptoTest`, which **runs on the iOS simulator and macOS** in addition to the JVM/Android tracks. +## Bring your own libsignal (Apple) + +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)). + ## How big is it? -Krypton's own code is negligible (~0.2 MB). The size is libsignal — and far smaller than the -raw artifact suggests, because the linker keeps only what you call and release builds strip: +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 | Real app-size impact (release) | | --- | --- | @@ -140,6 +205,25 @@ raw artifact suggests, because the linker keeps only what you call and release b This is the same core that Signal and WhatsApp ship. +### 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 | @@ -217,6 +301,7 @@ It needs four repo secrets: `MAVEN_CENTRAL_USERNAME`, `MAVEN_CENTRAL_PASSWORD`, | `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 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..b368767 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,49 @@ +# 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. + +## 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/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..d7fa990 --- /dev/null +++ b/krypton-gradle-plugin/src/main/kotlin/io/krypton/gradle/LibsignalPlugin.kt @@ -0,0 +1,144 @@ +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() + 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 9335d1d..bc2e679 100644 --- a/krypton-protocol/build.gradle.kts +++ b/krypton-protocol/build.gradle.kts @@ -89,26 +89,29 @@ kotlin { // ── 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, - // so it ships inside the published artifact — consumers link it with no setup. - // The per-arch .a files are built fresh in CI (scripts/build-libsignal-ffi.sh, - // run by .github/workflows/release.yml); they are gitignored, never committed. - fun KotlinNativeCompilation.configureCInterop(libArchDir: String) { - 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") - extraOpts("-libraryPath", "${project.projectDir}/libs/apple/$libArchDir") - } + // 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") } } - // Every Apple target links its own real per-arch .a (no cross-arch stand-ins). - macosArm64 { compilations.getByName("main").configureCInterop("macos-arm64") } // Apple Silicon desktop - macosX64 { compilations.getByName("main").configureCInterop("macos-x64") } // Intel desktop - iosArm64 { compilations.getByName("main").configureCInterop("ios-arm64") } // device - iosSimulatorArm64 { compilations.getByName("main").configureCInterop("ios-sim-arm64") } // Apple Silicon sim - iosX64 { compilations.getByName("main").configureCInterop("ios-sim-x64") } // Intel sim + // 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 { diff --git a/krypton-protocol/src/appleMain/cinterop/libsignal_ffi.def b/krypton-protocol/src/appleMain/cinterop/libsignal_ffi.def index 9f14424..71ee623 100644 --- a/krypton-protocol/src/appleMain/cinterop/libsignal_ffi.def +++ b/krypton-protocol/src/appleMain/cinterop/libsignal_ffi.def @@ -1,7 +1,12 @@ -# libraryPaths is supplied per-target by the Gradle build (configureCInterop) so -# each Apple target links its OWN-arch libsignal_ffi.a (macos-arm64 / ios-arm64 / -# ios-sim-arm64). compilerOpts uses a project-relative -I so the header resolves -# regardless of checkout path. +# 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 +linkerOpts = -lsignal_ffi diff --git a/samples/composeApp/build.gradle.kts b/samples/composeApp/build.gradle.kts index 510d18b..6c1178d 100644 --- a/samples/composeApp/build.gradle.kts +++ b/samples/composeApp/build.gradle.kts @@ -11,14 +11,16 @@ kotlin { jvm("desktop") - // device + Apple-Silicon simulator. Each links Krypton's per-arch libsignal_ffi. - iosArm64() - iosSimulatorArm64() - listOf(iosArm64(), iosSimulatorArm64()).forEach { target -> - target.binaries.framework { - baseName = "ComposeApp" - isStatic = true - } + // 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 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 efb9796..bf14b00 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") From 1823ec7348993bd75be6e294259ccf407fddd327 Mon Sep 17 00:00:00 2001 From: Ranbir Singh Date: Sat, 20 Jun 2026 19:26:21 +0530 Subject: [PATCH 3/7] Switch group to io.github.androidpoet; clean up download cache - GROUP -> io.github.androidpoet (GitHub-auto-verified on Central Portal; matches the passkeys library, no domain ownership needed) - plugin download mode deletes the archive after extracting Verified end-to-end locally: a consumer pulling io.github.androidpoet:krypton from mavenLocal, applying id("io.krypton.libsignal") in download mode, links an iosArm64 framework against Signal's official prebuilt libsignal_ffi.a. --- buildSrc/src/main/kotlin/io/krypton/Configuration.kt | 2 +- .../src/main/kotlin/io/krypton/gradle/LibsignalPlugin.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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/krypton-gradle-plugin/src/main/kotlin/io/krypton/gradle/LibsignalPlugin.kt b/krypton-gradle-plugin/src/main/kotlin/io/krypton/gradle/LibsignalPlugin.kt index d7fa990..9ec1109 100644 --- a/krypton-gradle-plugin/src/main/kotlin/io/krypton/gradle/LibsignalPlugin.kt +++ b/krypton-gradle-plugin/src/main/kotlin/io/krypton/gradle/LibsignalPlugin.kt @@ -137,6 +137,7 @@ class LibsignalPlugin : Plugin { } } File(out, "target").deleteRecursively() + dest.delete() project.logger.lifecycle( "Krypton: prebuilt is iOS-only (no macOS). For macOS targets use mode \"build\".", ) From 027799c55b4b334238d51a8bb35a6537f3baaaff Mon Sep 17 00:00:00 2001 From: Ranbir Singh Date: Sat, 20 Jun 2026 19:53:08 +0530 Subject: [PATCH 4/7] Document cargo build path; add cryptography/export notice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README: "What build mode runs under the hood (cargo)" — shows the exact git clone + rustup + cargo build steps, the Rust prerequisite, and why Krypton compiles/downloads from Signal's source instead of shipping a pre-built binary - README: Cryptography / export notice (ECCN 5D002, ENC/TSU exception) - NOTICE: standalone export + attribution notice --- NOTICE | 26 ++++++++++++++++++++++++++ README.md | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 NOTICE 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 954407a..0cc601f 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,31 @@ kotlin { 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 +``` + +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 @@ -316,6 +341,20 @@ Issues and PRs are welcome. Please make sure the build stays green: 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 Krypton is licensed under **AGPL-3.0-only**, matching libsignal. See [LICENSE](LICENSE). From 6954c2fee26c3ef35f9c898bae86eec3106cd032 Mon Sep 17 00:00:00 2001 From: Ranbir Singh Date: Sat, 20 Jun 2026 19:55:16 +0530 Subject: [PATCH 5/7] Expand SECURITY.md into a full consumer safety guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Recommended setup table (prefer build-from-source; download needs a pinned checksum) - Consumer safety checklist (pin version, verify in CI, re-derive trust) - "Getting a checksum you can trust" — since Signal commits none, establish your own ground truth - "Reproduce it yourself" — verify the binary is genuine libsignal --- SECURITY.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index b368767..3af378f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -35,6 +35,66 @@ a binary, you didn't get it from us. 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 From 0641b485ca4f720189f644e928b1fc53c19f2c6d Mon Sep 17 00:00:00 2001 From: Ranbir Singh Date: Sat, 20 Jun 2026 20:26:28 +0530 Subject: [PATCH 6/7] Document measured binary sizes; keep all platforms by default - "How big is it?" table now has real per-platform figures incl. the JVM Linux ~120 MB unstripped caveat and Windows ~16 MB - Default ships everything (all platforms/arches work out of the box); add optional, opt-in trims: exclude libsignal test natives and keep only the OS native(s) you actually ship --- README.md | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0cc601f..1828ff7 100644 --- a/README.md +++ b/README.md @@ -222,14 +222,36 @@ Krypton's own published artifacts are tiny — the JVM jars are ~0.2 MB and 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 | Real app-size impact (release) | -| --- | --- | -| iOS / macOS | **~3 MB** (static lib, dead-stripped at link) | -| Android (per ABI) | **~3 MB download / ~6.4 MB installed** (AGP strips `.so` on release) | -| JVM desktop | ~20 MB per OS | +| 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 From 294a6d479b428d8f301c73f223c57f437598e84b Mon Sep 17 00:00:00 2001 From: Ranbir Singh Date: Sat, 20 Jun 2026 21:32:26 +0530 Subject: [PATCH 7/7] Add Ktor + libsignal-server companion server sample MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit samples/ktor-server: a minimal JVM backend for a Krypton client, built on Ktor and org.signal:libsignal-server (the JVM-only server half of libsignal). - pre-key bundle exchange (the transport X3DH needs) - sealed-sender SenderCertificate issuance (ServerCertificate.issue) - zkgroup server public params + auth credential issuance (ServerSecretParams / ServerZkAuthOperations) In-memory storage so it runs with zero setup; README documents swapping in Supabase/Postgres as the data layer (libsignal-server stays on the JVM since Supabase Edge Functions are Deno/TS — the crypto can't run inside Supabase). Verified: server boots and all endpoints return real libsignal-server output (900-char zkgroup public params, real AuthCredentialWithPniResponse). --- README.md | 1 + samples/ktor-server/README.md | 59 +++++++++ samples/ktor-server/build.gradle.kts | 20 ++++ .../kotlin/io/krypton/sample/server/App.kt | 112 ++++++++++++++++++ settings.gradle.kts | 1 + 5 files changed, 193 insertions(+) create mode 100644 samples/ktor-server/README.md create mode 100644 samples/ktor-server/build.gradle.kts create mode 100644 samples/ktor-server/src/main/kotlin/io/krypton/sample/server/App.kt diff --git a/README.md b/README.md index 1828ff7..6e6609c 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ bundles, what your server must and must not do — see **[GUIDE.md](GUIDE.md)**. | --- | --- | --- | | `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)) | The iOS app is an Xcode project under `samples/composeApp/iosApp` (generated with [XcodeGen](https://github.com/yonaskolb/XcodeGen)): 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/settings.gradle.kts b/settings.gradle.kts index bf14b00..d3c2a3e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,4 +26,5 @@ include(":krypton-double-ratchet") include(":krypton-zkgroup") include(":samples:encrypted-chat") include(":samples:jvm-demo") +include(":samples:ktor-server") include(":samples:composeApp")