nigig-org/crates/robius-fingerprinting
Arena Bot 33dbece7d8
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
Payment domain and storage / isolated-payment-tests (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
fix(ui): use valid syntax for assigning ids to metric values
2026-07-29 01:56:41 +00:00
..
robius-fingerprinting fix(ui): use valid syntax for assigning ids to metric values 2026-07-29 01:56:41 +00:00
src security(pay): complete phase zero payment containment 2026-07-27 18:17:32 +00:00
Cargo.toml security(pay): complete phase zero payment containment 2026-07-27 18:17:32 +00:00
README.md added missing crates 2026-07-26 21:22:21 +03:00

robius-fingerprinting

Rust abstractions for Android biometric authenticationBiometricPrompt (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

<manifest ...>
  <uses-permission android:name="android.permission.USE_BIOMETRIC" />
  <uses-permission android:name="android.permission.USE_FINGERPRINT" />

  <application ...>
    <!-- No <service> or <receiver> needed — BiometricPrompt is invoked inline. -->
  </application>
</manifest>

Quick start

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.