Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions .github/workflows/mobile-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/mobile-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions ffi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
71 changes: 66 additions & 5 deletions ffi/build-mobile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <dir>]" >&2; }
usage() { echo "usage: $0 [ios|ios-framework|android|all] [--out <dir>]" >&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 ;;
Expand Down Expand Up @@ -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" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key><string>WardenFfi</string>
<key>CFBundleIdentifier</key><string>com.bytesbrains.WardenFfi</string>
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
<key>CFBundleName</key><string>WardenFfi</string>
<key>CFBundlePackageType</key><string>FMWK</string>
<key>CFBundleShortVersionString</key><string>1.0</string>
<key>CFBundleVersion</key><string>1</string>
<key>MinimumOSVersion</key><string>$minos</string>
</dict>
</plist>
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"
Expand All @@ -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
6 changes: 6 additions & 0 deletions ffi/flutter/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.dart_tool/
.packages
pubspec.lock
build/
.flutter-plugins
.flutter-plugins-dependencies
9 changes: 9 additions & 0 deletions ffi/flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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/<abi>/libwarden_ffi.so` from the same
release via `build.gradle`.
- Preview / Phase-0 PoC: the timing guarantee is not security on the test federation.
21 changes: 21 additions & 0 deletions ffi/flutter/LICENSE
Original file line number Diff line number Diff line change
@@ -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.
57 changes: 57 additions & 0 deletions ffi/flutter/README.md
Original file line number Diff line number Diff line change
@@ -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/<abi>/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).
1 change: 1 addition & 0 deletions ffi/flutter/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml
76 changes: 76 additions & 0 deletions ffi/flutter/android/build.gradle
Original file line number Diff line number Diff line change
@@ -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/<abi>/...).
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 <wardenJni>/jniLibs/<abi>/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
}
}
38 changes: 38 additions & 0 deletions ffi/flutter/ios/warden_ffi_flutter.podspec
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading