# robius-fingerprinting Rust abstractions for **Android biometric authentication** — `BiometricPrompt` (API 28+) and the legacy `FingerprintManager` — with transaction-signing `CryptoObject` support. This crate is the biometric counterpart to [`robius-sms`] and [`robius-ussd`]. It mirrors the same architecture: pure-Rust abstraction, no Makepad deps, `robius_android_env::with_activity` + `jni` on Android, in-memory DEX loading for the Java side. ## Why this crate exists The consuming app (`nigig-mpesa`) requires **fingerprint authentication before any money transaction**. The user picks "Send Money", enters the amount, hits "Pay via M-Pesa", and BEFORE we dispatch the `*334#` USSD call we present a BiometricPrompt. Only on `AuthenticationSucceeded` do we call `robius_ussd::begin_transaction`. This layers a biometric gate ON TOP of the M-Pesa PIN — a stolen unlocked phone still cannot send money without the owner's fingerprint. ## Architecture * `robius_android_env::with_activity` for JNIEnv access (same as robius-sms). * `dalvik.system.InMemoryDexClassLoader` to load the bundled `classes.dex` at runtime — no APK modification needed. * `register_native_methods` wires three Java→Rust callbacks: `rustOnAuthSucceeded(byte[])`, `rustOnAuthFailed()`, `rustOnAuthError(int, String)`. * The Java side uses **`android.hardware.fingerprint.FingerprintManager`** (API 23+, stable public SDK) rather than `android.hardware.biometrics. BiometricPrompt`. Reason: the platform `BiometricPrompt.PromptInfo` is partially hidden behind `@SystemApi` and is not consistently available in the `android.jar` that `android-build` resolves. `FingerprintManager` works against any android.jar. * **Caveat**: `FingerprintManager` does NOT render a system UI. The consuming Makepad app must show its own prompt overlay (e.g. the PaySheet `status_label` shows "Place your finger to confirm…"). * **Migration path**: to use the system-rendered BiometricPrompt UI on Android 10+, ship the `androidx.biometric` aar with the host APK and swap the Java implementation. The Rust API surface (`AuthPrompt`, `AuthResult`, `can_authenticate`, `authenticate`) does not need to change. ## AndroidManifest additions ```xml ``` ## Quick start ```rust use robius_fingerprinting::{ init, can_authenticate, has_permission, request_permissions, authenticate, next_event, AuthPrompt, AuthResult, BiometricStrength, Permission, }; use robius_ussd::{begin_transaction, TransactionKind, UssdTransactionRequest}; fn pay_via_mpesa(amount: u64) -> anyhow::Result<()> { // 1. Initialise & check availability. robius_fingerprinting::init()?; robius_ussd::init()?; request_permissions(&[Permission::UseBiometric])?; if !has_permission(Permission::UseBiometric)? { anyhow::bail!("USE_BIOMETRIC not granted"); } if !can_authenticate(BiometricStrength::Strong)? { anyhow::bail!("No biometric enrolled"); } // 2. Build the USSD request — but DO NOT dispatch yet. let ussd_request = UssdTransactionRequest { kind: Some(TransactionKind::SendMoney), amount, pin: "1234".into(), phone: "0712345678".into(), ..Default::default() }; // 3. OFFLINE-FIRST: write `ussd_request` to local store with status `Pending`. // 4. Present biometric prompt. let prompt = AuthPrompt::for_transaction(amount, "Send Money"); authenticate(&prompt)?; // 5. Drain events — only proceed on Succeeded. loop { match next_event() { Some(AuthResult::Succeeded { .. }) => { begin_transaction(&ussd_request)?; break; } Some(AuthResult::Cancelled | AuthResult::Error(_)) => { // Update local record: Pending → Failed. return Ok(()); } Some(AuthResult::Failed) => continue, // user can retry None => std::thread::sleep(std::time::Duration::from_millis(50)), } } Ok(()) } ``` ## License MIT, same as the rest of the Project Robius ecosystem.