Privacy Pass protocol implementation for Flutter and Dart using Rust FFI. Provides native performance for cryptographic operations.
- ✅ Native Performance: Rust-powered cryptography via FFI
- ✅ Modern FFI package: Built and bundled via build hooks (
hook/build.dart) — no platform-specific plugin code, works in Flutter and standalone Dart - ✅ Background Processing: Operations run in isolates (via
Isolate.run) to prevent UI blocking - ✅ Memory Safe: Automatic memory management with
@Nativebindings generated bypackage:ffigen - ✅ Cross-Platform: Supports Android (arm64-v8a, armeabi-v7a, x86_64), iOS (device + simulators), and macOS (universal)
Privacy Pass is a protocol that allows users to prove they're trustworthy without revealing their identity. It uses cryptographic tokens to provide anonymous authentication, implementing:
- VOPRF (Verifiable Oblivious Pseudorandom Function) using Ristretto255 curve
- RFC 9578 Privacy Pass Issuance Protocol
- Batched token issuance for efficiency
Add to your pubspec.yaml:
dependencies:
privacypass_ffi:
path: ../privacypass_ffi # Update with your pathThen run:
flutter pub getRequires Flutter 3.44+ / Dart 3.12+ (matching the pubspec sdk: ^3.12.0 constraint). Prebuilt native libraries are
committed under prebuilt/, so consumers do not need a Rust toolchain.
Use PrivacyPassIsolate to run crypto operations in background isolates:
import 'package:privacypass_ffi/privacypass_ffi.dart';
final client = PrivacyPassIsolate();
try {
// Step 1: Get challenge from origin server
final response = await http.get(Uri.parse('https://origin.com/protected'));
final wwwAuthHeader = response.headers['www-authenticate']!;
// Step 2: Generate token request
final request = await client.generateTokenRequest(
wwwAuthenticateHeader: wwwAuthHeader,
tokenCount: 5,
);
// Step 3: Send to issuer
final issuerResponse = await http.post(
Uri.parse('https://issuer.com/token'),
body: request.tokenRequest,
);
// Step 4: Finalize tokens
final tokens = await client.finalizeTokens(
wwwAuthenticateHeader: wwwAuthHeader,
clientState: request.clientState,
tokenResponse: issuerResponse.body,
);
// Step 5: Use tokens
final protectedResponse = await http.get(
Uri.parse('https://origin.com/protected'),
headers: {'Authorization': 'PrivateToken token=${tokens.first}'},
);
print('Success: ${protectedResponse.body}');
} catch (e) {
print('Error: $e');
}Use PrivacyPassClient for synchronous operations:
import 'package:privacypass_ffi/privacypass_ffi.dart';
final client = PrivacyPassClient();
// Generate token request (blocks current isolate)
final request = client.generateTokenRequest(
wwwAuthenticateHeader: header,
tokenCount: 5,
);
// Finalize tokens
final tokens = client.finalizeTokens(
wwwAuthenticateHeader: header,
clientState: request.clientState,
tokenResponse: serverResponse,
);Background processing client using Dart's built-in Isolate.run.
Future<TokenRequestResult> generateTokenRequest({required String wwwAuthenticateHeader, required int tokenCount})
- Generate a Privacy Pass token request in a background isolate
- Returns:
TokenRequestResultwithclientStateandtokenRequest
Future<List<String>> finalizeTokens({required String wwwAuthenticateHeader, required String clientState, required String tokenResponse})
- Finalize tokens from issuer response in a background isolate
- Returns: List of base64-encoded tokens
Synchronous client (operations block current isolate).
TokenRequestResult generateTokenRequest({required String wwwAuthenticateHeader, required int tokenCount})
- Synchronous token request generation
List<String> finalizeTokens({required String wwwAuthenticateHeader, required String clientState, required String tokenResponse})
- Synchronous token finalization
String get nativeLibraryVersion
- Version of the bundled native library (e.g.
"0.1.0")
TokenRequestResult
class TokenRequestResult {
final String clientState; // Preserve for finalization
final String tokenRequest; // Send to issuer
}PrivacyPassException
- Exception thrown when operations fail
privacypass_ffi/
├── lib/
│ ├── src/
│ │ ├── privacypass_ffi_bindings_generated.dart # ffigen @Native bindings
│ │ ├── privacy_pass_client.dart # Synchronous API
│ │ ├── privacy_pass_isolate.dart # Async isolate API
│ │ └── types.dart # Dart types
│ └── privacypass_ffi.dart # Main export
├── hook/
│ └── build.dart # Build hook: registers prebuilt code assets
├── prebuilt/ # Committed native libraries (per OS/arch)
│ ├── android/{arm64-v8a,armeabi-v7a,x86_64}/libkagipp_ffi.so
│ ├── ios/{iphoneos,iphonesimulator}/<arch>/libkagipp_ffi.dylib
│ └── macos/{arm64,x64}/libkagipp_ffi.dylib # thin per-arch (Flutter lipo-combines)
├── src/
│ └── kagipp_ffi.h # cbindgen-generated header (ffigen input)
├── ffigen.yaml # Bindings generator config
├── rust/ # Rust FFI source (git submodule)
├── scripts/
│ └── update_prebuilt_libs.sh # Rebuild everything under prebuilt/
├── test/ # Unit tests
└── example/ # Demo app
This package follows the Flutter package_ffi
template architecture:
hook/build.dartruns automatically duringflutter build/flutter run/dart testand registers the correct library fromprebuilt/as a code asset for the current target OS, architecture, and iOS SDK.- Flutter/Dart bundles the library (on Apple platforms it is wrapped into
kagipp_ffi.framework) and resolves the@Nativeexternal functions inlib/src/privacypass_ffi_bindings_generated.dartby asset id at runtime. - There are no podspecs, gradle files, or platform folders — and no symbol-stripping workarounds, since the library is a real dynamic library on every platform.
Prerequisites: rustup with the Android/iOS/macOS targets, cargo install cargo-ndk cbindgen,
Android NDK, and Xcode. Then:
git submodule update --init
bash scripts/update_prebuilt_libs.shIf the FFI surface changed (new/changed exported functions), regenerate the Dart bindings afterwards:
dart run ffigen --config ffigen.yamldart testThe build hook runs before tests and loads the prebuilt macOS library, so tests exercise the real native code.
cd example
flutter runCrypto operations are CPU-intensive. Benchmarks on iPhone 14 Pro:
- Token Request (5 tokens): ~15ms
- Token Finalization (5 tokens): ~20ms
Using PrivacyPassIsolate prevents UI jank during these operations.
- Memory Safety: All FFI calls properly manage memory to prevent leaks
- String Lifetime: Input strings are copied; output strings are owned by Rust and freed via
privacy_pass_free_string - Error Handling: All errors propagate as
PrivacyPassException - Thread Safety: No shared state; isolate calls construct their own client
See LICENSE file.
This is a Kagi internal project. For issues or questions, contact the Privacy Pass team.