Skip to content

kagisearch/privacypass_ffi

Repository files navigation

Privacy Pass FFI

Privacy Pass protocol implementation for Flutter and Dart using Rust FFI. Provides native performance for cryptographic operations.

Features

  • 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 @Native bindings generated by package:ffigen
  • Cross-Platform: Supports Android (arm64-v8a, armeabi-v7a, x86_64), iOS (device + simulators), and macOS (universal)

What is Privacy Pass?

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

Installation

Add to your pubspec.yaml:

dependencies:
  privacypass_ffi:
    path: ../privacypass_ffi  # Update with your path

Then run:

flutter pub get

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

Usage

Option 1: Background Processing (Recommended)

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');
}

Option 2: Synchronous (For Simple Cases)

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,
);

API Reference

PrivacyPassIsolate

Background processing client using Dart's built-in Isolate.run.

Methods

Future<TokenRequestResult> generateTokenRequest({required String wwwAuthenticateHeader, required int tokenCount})

  • Generate a Privacy Pass token request in a background isolate
  • Returns: TokenRequestResult with clientState and tokenRequest

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

PrivacyPassClient

Synchronous client (operations block current isolate).

Methods

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")

Types

TokenRequestResult

class TokenRequestResult {
  final String clientState; // Preserve for finalization
  final String tokenRequest; // Send to issuer
}

PrivacyPassException

  • Exception thrown when operations fail

Development

Project Structure

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

How native code is built and bundled

This package follows the Flutter package_ffi template architecture:

  1. hook/build.dart runs automatically during flutter build / flutter run / dart test and registers the correct library from prebuilt/ as a code asset for the current target OS, architecture, and iOS SDK.
  2. Flutter/Dart bundles the library (on Apple platforms it is wrapped into kagipp_ffi.framework) and resolves the @Native external functions in lib/src/privacypass_ffi_bindings_generated.dart by asset id at runtime.
  3. 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.

Rebuilding the native libraries

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

If the FFI surface changed (new/changed exported functions), regenerate the Dart bindings afterwards:

dart run ffigen --config ffigen.yaml

Testing

dart test

The build hook runs before tests and loads the prebuilt macOS library, so tests exercise the real native code.

Running the Example

cd example
flutter run

Performance

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

Security Considerations

  1. Memory Safety: All FFI calls properly manage memory to prevent leaks
  2. String Lifetime: Input strings are copied; output strings are owned by Rust and freed via privacy_pass_free_string
  3. Error Handling: All errors propagate as PrivacyPassException
  4. Thread Safety: No shared state; isolate calls construct their own client

License

See LICENSE file.

Contributing

This is a Kagi internal project. For issues or questions, contact the Privacy Pass team.

References

About

Privacy Pass protocol implementation via Rust FFI

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages