diff --git a/.github/workflows/mobile-ci.yml b/.github/workflows/mobile-ci.yml index bbfef14..51dd899 100644 --- a/.github/workflows/mobile-ci.yml +++ b/.github/workflows/mobile-ci.yml @@ -38,6 +38,24 @@ jobs: - name: cargo test -p warden-ffi run: cargo test --locked -p warden-ffi + plugin-check: + name: warden_ffi_flutter (analyze + publish dry-run) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: stable + - name: pub get + working-directory: ffi/flutter + run: flutter pub get + - name: analyze + working-directory: ffi/flutter + run: dart analyze + - name: publish dry-run + working-directory: ffi/flutter + run: flutter pub publish --dry-run + build-ios: name: iOS xcframework (build check) runs-on: macos-latest @@ -47,8 +65,15 @@ jobs: with: targets: aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios - uses: Swatinem/rust-cache@v2 - - name: build-mobile.sh ios - run: ffi/build-mobile.sh ios + - name: build-mobile.sh ios-framework (dynamic) + run: ffi/build-mobile.sh ios-framework + - name: Verify warden_* symbols are exported + run: | + bin=$(find dist/mobile/ios/WardenFfi.xcframework -path '*ios-arm64/WardenFfi.framework/WardenFfi' | head -1) + echo "device framework binary: $bin" + n=$(nm -gU "$bin" | grep -c '_warden_' || true) + echo "exported warden_* symbols: $n" + test "$n" -ge 5 build-android: name: Android jniLibs (build check) diff --git a/.github/workflows/mobile-release.yml b/.github/workflows/mobile-release.yml index b87a1f6..1571890 100644 --- a/.github/workflows/mobile-release.yml +++ b/.github/workflows/mobile-release.yml @@ -33,7 +33,13 @@ jobs: targets: aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios - uses: Swatinem/rust-cache@v2 - name: Cross-compile (release, --locked) - run: ffi/build-mobile.sh ios + run: ffi/build-mobile.sh ios-framework + - name: Verify warden_* symbols are exported + run: | + bin=$(find dist/mobile/ios/WardenFfi.xcframework -path '*ios-arm64/WardenFfi.framework/WardenFfi' | head -1) + n=$(nm -gU "$bin" | grep -c '_warden_' || true) + echo "exported warden_* symbols: $n" + test "$n" -ge 5 - name: Zip xcframework # ditto preserves the framework bundle layout/symlinks better than `zip`. run: ditto -c -k --keepParent dist/mobile/ios/WardenFfi.xcframework WardenFfi.xcframework.zip diff --git a/ffi/README.md b/ffi/README.md index bbf2330..e81890e 100644 --- a/ffi/README.md +++ b/ffi/README.md @@ -53,8 +53,12 @@ Each `v*` tag publishes the cross-compiled binaries as **GitHub Release assets** Pick the release whose version matches the `warden_ffi` pub.dev version you depend on — both are cut from the same tag, so they're always compatible. A consuming-app redeploy is irrelevant (the gate -crypto bakes in no on-chain addresses). _(A `warden_ffi_flutter` plugin that does this download -automatically at build time is tracked in [#4](https://github.com/bytesbrains/warden/issues/4).)_ +crypto bakes in no on-chain addresses). + +**Flutter apps: prefer the [`warden_ffi_flutter`](flutter/) plugin** — it does this download +automatically at build time (iOS embeds a dynamic framework, Android downloads jniLibs), so you add +one pub.dev dependency and need neither a manual download nor any Xcode/Gradle wiring. The manual +download above is for non-plugin consumers or for pinning the binary yourself. ### Mobile (build from source — for warden developers) diff --git a/ffi/build-mobile.sh b/ffi/build-mobile.sh index c511def..4b790fb 100755 --- a/ffi/build-mobile.sh +++ b/ffi/build-mobile.sh @@ -35,13 +35,13 @@ set -euo pipefail WARDEN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" LIB="libwarden_ffi" -usage() { echo "usage: $0 [ios|android|all] [--out ]" >&2; } +usage() { echo "usage: $0 [ios|ios-framework|android|all] [--out ]" >&2; } WHAT="" OUT_DIR="" while [ $# -gt 0 ]; do case "$1" in - ios|android|all) WHAT="$1"; shift ;; + ios|ios-framework|android|all) WHAT="$1"; shift ;; --out) [ $# -ge 2 ] || { echo "--out requires a directory" >&2; usage; exit 2; } OUT_DIR="$2"; shift 2 ;; --out=*) OUT_DIR="${1#*=}"; shift ;; @@ -82,6 +82,66 @@ build_ios() { echo "==> iOS: done. Wire it once into ios/Runner per warden/ffi/README.md (§ Mobile → iOS)." } +# Wrap a single dylib slice (device, or universal-simulator) in a .framework bundle. +# The dynamic-framework form is what the warden_ffi_flutter plugin ships: CocoaPods embeds +# + signs it, so dyld loads it at app launch and the warden_* symbols resolve via +# DynamicLibrary.process() with NO -force_load wiring on the consumer. +make_framework() { + local dylib="$1" fwdir="$2" minos="$3" + rm -rf "$fwdir"; mkdir -p "$fwdir" + cp "$dylib" "$fwdir/WardenFfi" + # dyld locates the binary by its framework-relative @rpath install name. + install_name_tool -id @rpath/WardenFfi.framework/WardenFfi "$fwdir/WardenFfi" + cat > "$fwdir/Info.plist" < + + + + CFBundleExecutableWardenFfi + CFBundleIdentifiercom.bytesbrains.WardenFfi + CFBundleInfoDictionaryVersion6.0 + CFBundleNameWardenFfi + CFBundlePackageTypeFMWK + CFBundleShortVersionString1.0 + CFBundleVersion1 + MinimumOSVersion$minos + + +PLIST +} + +build_ios_framework() { + echo "==> iOS (dynamic): cross-compiling $LIB cdylib (device + simulator arm64+x86_64, release, --locked)" + ( cd "$WARDEN_DIR" \ + && cargo build --locked -p warden-ffi --release --target aarch64-apple-ios \ + && cargo build --locked -p warden-ffi --release --target aarch64-apple-ios-sim \ + && cargo build --locked -p warden-ffi --release --target x86_64-apple-ios ) + + local work="$WARDEN_DIR/target/ios-framework" + rm -rf "$work"; mkdir -p "$work/device" "$work/sim" + + # Device: the lone arm64 dylib. Xcode reads the Mach-O platform, so create-xcframework + # files this under the ios-arm64 slice automatically. + make_framework "$WARDEN_DIR/target/aarch64-apple-ios/release/$LIB.dylib" \ + "$work/device/WardenFfi.framework" "12.0" + # Simulator: fuse arm64 + x86_64 so it links on both Apple-Silicon and Intel hosts. + lipo -create \ + "$WARDEN_DIR/target/aarch64-apple-ios-sim/release/$LIB.dylib" \ + "$WARDEN_DIR/target/x86_64-apple-ios/release/$LIB.dylib" \ + -output "$work/sim-universal.dylib" + make_framework "$work/sim-universal.dylib" "$work/sim/WardenFfi.framework" "12.0" + + local out="$MOBILE_DIR/ios/WardenFfi.xcframework" + echo "==> iOS: assembling dynamic $out (ios-arm64 device + universal simulator)" + mkdir -p "$(dirname "$out")" # the --out base may not pre-exist (e.g. default dist/mobile) + rm -rf "$out" + xcodebuild -create-xcframework \ + -framework "$work/device/WardenFfi.framework" \ + -framework "$work/sim/WardenFfi.framework" \ + -output "$out" + echo "==> iOS: done (dynamic framework — CocoaPods embeds it; symbols load at launch, no -force_load)." +} + build_android() { echo "==> Android: cross-compiling $LIB (4 ABIs, release, --locked) via cargo-ndk" local jni="$MOBILE_DIR/android/app/src/main/jniLibs" @@ -92,7 +152,8 @@ build_android() { } case "$WHAT" in - ios) build_ios ;; - android) build_android ;; - all) build_ios; build_android ;; + ios) build_ios ;; # static .a + -force_load (legacy / manual wiring) + ios-framework) build_ios_framework ;; # dynamic framework (warden_ffi_flutter plugin) + android) build_android ;; + all) build_ios; build_android ;; esac diff --git a/ffi/flutter/.gitignore b/ffi/flutter/.gitignore new file mode 100644 index 0000000..9faf236 --- /dev/null +++ b/ffi/flutter/.gitignore @@ -0,0 +1,6 @@ +.dart_tool/ +.packages +pubspec.lock +build/ +.flutter-plugins +.flutter-plugins-dependencies diff --git a/ffi/flutter/CHANGELOG.md b/ffi/flutter/CHANGELOG.md new file mode 100644 index 0000000..caed398 --- /dev/null +++ b/ffi/flutter/CHANGELOG.md @@ -0,0 +1,9 @@ +## 0.1.0-dev.1 + +- Initial pre-release. Flutter plugin that bundles the Warden native FFI library + (the Veil bridge) and re-exports the `warden_ffi` binding. +- iOS: downloads + embeds the prebuilt dynamic `WardenFfi.xcframework` from the + warden GitHub release (`v0.1.0-dev.2`) via the podspec. +- Android: downloads the prebuilt `jniLibs//libwarden_ffi.so` from the same + release via `build.gradle`. +- Preview / Phase-0 PoC: the timing guarantee is not security on the test federation. diff --git a/ffi/flutter/LICENSE b/ffi/flutter/LICENSE new file mode 100644 index 0000000..7d8374d --- /dev/null +++ b/ffi/flutter/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 BytesBrains + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ffi/flutter/README.md b/ffi/flutter/README.md new file mode 100644 index 0000000..001414b --- /dev/null +++ b/ffi/flutter/README.md @@ -0,0 +1,57 @@ +# warden_ffi_flutter + +Flutter plugin that **bundles the Warden native FFI library** (the Veil bridge) so a +Flutter app gets the threshold conditional-decryption gate with **no Rust toolchain and +no native build wiring**. It wraps and re-exports the pure +[`warden_ffi`](https://pub.dev/packages/warden_ffi) binding; its only job is to deliver +the matching native library per platform. + +> ⚠️ Preview / Phase-0 PoC. On a single-operator test federation the *timing* guarantee +> is not security — this gate gives condition-binding only. Not audited. + +## Use + +```yaml +dependencies: + warden_ffi_flutter: 0.1.0-dev.1 +``` + +```dart +import 'package:warden_ffi_flutter/warden_ffi_flutter.dart'; + +// The native library is bundled by this plugin, so no path is needed on device. +final warden = WardenFfi.load(); + +final id = warden.conditionIdentity(conditionJson); +final env = warden.sealGated(conditionJson, masterPubHex, network, blobHex); +// …once the federation releases enough partials for this condition… +final dId = warden.combine(partialsJson, id, fedJson); +final blob = warden.openGated(env, dId); // == the original blobHex +``` + +## How the native library is delivered + +Binaries are large and platform-specific, so they are **not** shipped inside this package. +They're built once per version by warden CI and published as GitHub Release assets; this +plugin downloads the matching set at build time: + +- **iOS** — the [podspec](ios/warden_ffi_flutter.podspec)'s `prepare_command` downloads the + prebuilt **dynamic** `WardenFfi.xcframework` and CocoaPods embeds + signs it. dyld loads + it at app launch, so `WardenFfi.load()` resolves the symbols via + `DynamicLibrary.process()` — no `-force_load`, no Xcode wiring. +- **Android** — [`build.gradle`](android/build.gradle) downloads `jniLibs//libwarden_ffi.so` + and adds it as a `jniLibs` source set; Gradle bundles it into the APK and + `WardenFfi.load()` resolves it via `DynamicLibrary.open('libwarden_ffi.so')`. + +The warden release tag the binaries come from is pinned in both files (`warden_tag` / +`WARDEN_NATIVE_TAG`) and kept in lockstep with this package's version. + +## Desktop / tests + +For host (desktop/CI) use, depend on the pure `warden_ffi` package directly and pass a +dylib path to `WardenFfi.load(path: …)` — see its +[README](https://pub.dev/packages/warden_ffi). This plugin targets iOS + Android. + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/ffi/flutter/analysis_options.yaml b/ffi/flutter/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/ffi/flutter/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/ffi/flutter/android/build.gradle b/ffi/flutter/android/build.gradle new file mode 100644 index 0000000..d0e6fa8 --- /dev/null +++ b/ffi/flutter/android/build.gradle @@ -0,0 +1,76 @@ +// warden_ffi_flutter — Android native delivery for the Warden Veil bridge. +// +// The native .so files are NOT vendored in the published package. This downloads the +// prebuilt jniLibs from the matching warden GitHub release into the build dir and adds +// them as a jniLibs source set, so Gradle bundles libwarden_ffi.so per ABI into the APK. +// DynamicLibrary.open('libwarden_ffi.so') then resolves it at runtime. + +group 'com.bytesbrains.warden_ffi_flutter' +version '1.0' + +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.1.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +// The warden release whose binaries this version pulls. Keep in lockstep with the tag +// the ios/warden_ffi_flutter.podspec downloads (warden_tag). +def WARDEN_NATIVE_TAG = 'v0.1.0-dev.2' +def wardenJni = new File(buildDir, "warden_native") + +// Download + unpack the prebuilt jniLibs once (the zip contains jniLibs//...). +task downloadWardenJni { + outputs.dir wardenJni + doLast { + def marker = new File(wardenJni, "jniLibs/arm64-v8a/libwarden_ffi.so") + if (!marker.exists()) { + wardenJni.mkdirs() + def zip = new File(buildDir, "warden-ffi-android-jniLibs.zip") + def url = "https://github.com/bytesbrains/warden/releases/download/${WARDEN_NATIVE_TAG}/warden-ffi-android-jniLibs.zip" + logger.lifecycle("warden_ffi_flutter: downloading ${url}") + new URL(url).withInputStream { i -> zip.withOutputStream { o -> o << i } } + copy { + from zipTree(zip) + into wardenJni + } + zip.delete() + } + } +} + +android { + namespace 'com.bytesbrains.warden_ffi_flutter' + compileSdk 34 + + defaultConfig { + minSdk 21 + } + + // The zip unpacks to /jniLibs//libwarden_ffi.so. + sourceSets { + main { + jniLibs.srcDirs += ["${wardenJni}/jniLibs"] + } + } +} + +// Ensure the libs are present before the library is assembled. +tasks.configureEach { task -> + if (task.name == 'preBuild') { + task.dependsOn downloadWardenJni + } +} diff --git a/ffi/flutter/ios/warden_ffi_flutter.podspec b/ffi/flutter/ios/warden_ffi_flutter.podspec new file mode 100644 index 0000000..9665de6 --- /dev/null +++ b/ffi/flutter/ios/warden_ffi_flutter.podspec @@ -0,0 +1,38 @@ +# +# warden_ffi_flutter — iOS native delivery for the Warden Veil bridge. +# +# The native library is NOT vendored in the published package (binaries are large and +# platform-specific). Instead `prepare_command` downloads the prebuilt **dynamic** +# WardenFfi.xcframework from the matching warden GitHub release at `pod install` time, +# and CocoaPods embeds + signs it. Because it's a dynamic framework, dyld loads it at +# app launch and the warden_* C symbols resolve via DynamicLibrary.process() with no +# -force_load wiring on the consumer. +# +Pod::Spec.new do |s| + s.name = 'warden_ffi_flutter' + s.version = '0.1.0-dev.1' + s.summary = 'Bundles the Warden native FFI library (Veil bridge) for Flutter iOS.' + s.homepage = 'https://github.com/bytesbrains/warden' + s.license = { :type => 'MIT', :file => '../LICENSE' } + s.author = { 'bytesbrains' => 'contact@bytesbrains.com' } + s.source = { :path => '.' } + s.dependency 'Flutter' + s.platform = :ios, '12.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + + # The warden release whose binaries this version pulls. Keep in lockstep with the + # tag the android/build.gradle downloads (WARDEN_NATIVE_TAG). + warden_tag = 'v0.1.0-dev.2' + s.prepare_command = <<-CMD + set -euo pipefail + if [ ! -d "WardenFfi.xcframework" ]; then + url="https://github.com/bytesbrains/warden/releases/download/#{warden_tag}/WardenFfi.xcframework.zip" + echo "warden_ffi_flutter: downloading $url" + curl -fsSL "$url" -o WardenFfi.xcframework.zip + unzip -q -o WardenFfi.xcframework.zip + rm -f WardenFfi.xcframework.zip + fi + CMD + + s.vendored_frameworks = 'WardenFfi.xcframework' +end diff --git a/ffi/flutter/lib/warden_ffi_flutter.dart b/ffi/flutter/lib/warden_ffi_flutter.dart new file mode 100644 index 0000000..af90686 --- /dev/null +++ b/ffi/flutter/lib/warden_ffi_flutter.dart @@ -0,0 +1,26 @@ +/// Flutter plugin that bundles the Warden native FFI library (the Veil bridge), so a +/// Flutter app gets the threshold conditional-decryption gate with **no Rust toolchain +/// and no native build wiring**. +/// +/// It re-exports the pure [`warden_ffi`](https://pub.dev/packages/warden_ffi) binding; +/// this package's only job is to deliver the matching native library per platform: +/// +/// - **iOS** — an embedded dynamic `WardenFfi.framework` (the podspec downloads it from +/// the warden release and CocoaPods embeds + signs it). dyld loads it at launch, so +/// [WardenFfi.load] resolves the symbols via `DynamicLibrary.process()` — no path, +/// no `-force_load`. +/// - **Android** — `jniLibs//libwarden_ffi.so` (gradle downloads it from the same +/// release). [WardenFfi.load] resolves it via `DynamicLibrary.open('libwarden_ffi.so')`. +/// +/// ```dart +/// import 'package:warden_ffi_flutter/warden_ffi_flutter.dart'; +/// +/// final warden = WardenFfi.load(); // bundled native lib — no path needed on device +/// final id = warden.conditionIdentity(conditionJson); +/// ``` +/// +/// ⚠️ Preview: on a single-operator test federation the *timing* guarantee is not +/// security — this gate gives condition-binding only. +library; + +export 'package:warden_ffi/warden_ffi.dart'; diff --git a/ffi/flutter/pubspec.yaml b/ffi/flutter/pubspec.yaml new file mode 100644 index 0000000..3464731 --- /dev/null +++ b/ffi/flutter/pubspec.yaml @@ -0,0 +1,43 @@ +name: warden_ffi_flutter +description: >- + Flutter plugin that bundles the Warden native FFI library (the Veil bridge) — a + threshold conditional-decryption gate — so an app gets it with no Rust toolchain + and no native build wiring. Wraps the warden_ffi binding. +# Pre-release: Warden is a Phase-0 PoC; the timing guarantee is unproven on a +# production federation. Pinned EXACT, same discipline as warden_ffi. +version: 0.1.0-dev.1 +repository: https://github.com/bytesbrains/warden +issue_tracker: https://github.com/bytesbrains/warden/issues +homepage: https://github.com/bytesbrains/warden +topics: + - ffi + - cryptography + - threshold + - blockchain + +environment: + sdk: ^3.4.0 + flutter: '>=3.10.0' + +dependencies: + flutter: + sdk: flutter + # The pure Dart binding. This plugin only adds the native library + platform wiring. + # A range (not an exact pin) so consumers can co-resolve warden_ffi — pub.dev rejects + # an exact pin on a published library. An end app that wants an exact pin still sets + # one in its own pubspec (it wins in resolution); the ABI is stable within 0.1.x. + warden_ffi: ^0.1.0-dev.1 + +dev_dependencies: + flutter_lints: ^4.0.0 + +# FFI plugin: no Dart plugin class — the native library is bundled per platform +# (iOS: an embedded dynamic framework downloaded by the podspec; Android: jniLibs +# downloaded by build.gradle). DynamicLibrary.process()/open() then resolves it. +flutter: + plugin: + platforms: + android: + ffiPlugin: true + ios: + ffiPlugin: true