nigig-org/crates/robius-fingerprinting/README.md
2026-07-26 21:22:21 +03:00

115 lines
4.3 KiB
Markdown

# 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
<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
```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.