115 lines
4.3 KiB
Markdown
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.
|