|
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
|
||
|---|---|---|
| .. | ||
| robius-fingerprinting | ||
| src | ||
| Cargo.toml | ||
| README.md | ||
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_activityfor JNIEnv access (same as robius-sms).dalvik.system.InMemoryDexClassLoaderto load the bundledclasses.dexat runtime — no APK modification needed.register_native_methodswires three Java→Rust callbacks:rustOnAuthSucceeded(byte[]),rustOnAuthFailed(),rustOnAuthError(int, String).- The Java side uses
android.hardware.fingerprint.FingerprintManager(API 23+, stable public SDK) rather thanandroid.hardware.biometrics. BiometricPrompt. Reason: the platformBiometricPrompt.PromptInfois partially hidden behind@SystemApiand is not consistently available in theandroid.jarthatandroid-buildresolves.FingerprintManagerworks against any android.jar. - Caveat:
FingerprintManagerdoes NOT render a system UI. The consuming Makepad app must show its own prompt overlay (e.g. the PaySheetstatus_labelshows "Place your finger to confirm…"). - Migration path: to use the system-rendered BiometricPrompt UI on
Android 10+, ship the
androidx.biometricaar 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.