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..6daea81 --- /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 +# 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 +# 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/NOTICE b/NOTICE new file mode 100644 index 0000000..e49caad --- /dev/null +++ b/NOTICE @@ -0,0 +1,26 @@ +Krypton +Copyright (C) 2026 Ranbir Singh + +This product links libsignal (https://github.com/signalapp/libsignal), +Copyright Signal Messenger, LLC, licensed under AGPL-3.0-only. The +cryptographic functionality is entirely libsignal's; Krypton provides +Kotlin Multiplatform bindings and ships no Signal binary of its own. + + Cryptography Notice + ------------------- + This distribution includes cryptographic software. The country in which + you currently reside may have restrictions on the import, possession, + use, and/or re-export to another country, of encryption software. BEFORE + using any encryption software, please check your country's laws, + regulations and policies concerning the import, possession, or use, and + re-export of encryption software, to see if this is permitted. See + https://www.wassenaar.org/ for more information. + + The U.S. Government Department of Commerce, Bureau of Industry and + Security (BIS), has classified this software as Export Commodity Control + Number (ECCN) 5D002.C.1, which includes information security software + using or performing cryptographic functions with asymmetric algorithms. + The form and manner of this distribution makes it eligible for export + under the License Exception ENC Technology Software Unrestricted (TSU) + exception (see the BIS Export Administration Regulations, Section 740.13) + for both object code and source code. diff --git a/README.md b/README.md index 57a3c7d..6e6609c 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,45 @@ -# ⚛️ Krypton +

⚛️ Krypton

-**Signal's end-to-end encryption protocol for Kotlin Multiplatform** — the audited [libsignal](https://github.com/signalapp/libsignal) core under the hood, behind one clean common API. +

Signal's end-to-end encryption for Kotlin Multiplatform.

-> Named after Krypton (Kr), a noble gas that's inert, stable, and doesn't react with anything — just like perfectly encrypted data. +

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