Skip to main content

fission_core/
platform_biometric.rs

1//! Biometric authentication host capabilities.
2//!
3//! These types describe what the app asks the host to do. The shell owns the
4//! OS-specific authentication prompt and returns only portable results.
5
6use crate::capability::{CapabilityType, OperationCapability};
7use serde::{Deserialize, Serialize};
8
9/// Biometric modality reported by a host.
10#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub enum BiometricKind {
12    Face,
13    Fingerprint,
14    Iris,
15    DeviceCredential,
16    Unknown,
17}
18
19/// Strength requested by an app or reported by a host.
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
21pub enum BiometricStrength {
22    #[default]
23    Any,
24    Weak,
25    Strong,
26}
27
28/// Biometric support state for the active device/session.
29#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
30pub struct BiometricAvailability {
31    pub supported: bool,
32    pub enrolled: bool,
33    pub strong: bool,
34    pub weak: bool,
35    pub device_credential: bool,
36    pub kinds: Vec<BiometricKind>,
37    pub reason: Option<String>,
38}
39
40/// Request to authenticate the current user.
41#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
42pub struct BiometricAuthenticateRequest {
43    pub reason: String,
44    pub title: Option<String>,
45    pub subtitle: Option<String>,
46    pub fallback_title: Option<String>,
47    pub cancel_title: Option<String>,
48    pub allow_device_credential: bool,
49    pub required_strength: BiometricStrength,
50}
51
52impl Default for BiometricAuthenticateRequest {
53    fn default() -> Self {
54        Self {
55            reason: String::new(),
56            title: None,
57            subtitle: None,
58            fallback_title: None,
59            cancel_title: None,
60            allow_device_credential: true,
61            required_strength: BiometricStrength::Any,
62        }
63    }
64}
65
66/// Successful biometric authentication response.
67#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
68pub struct BiometricAuthenticateResult {
69    pub verified: bool,
70    pub kind: Option<BiometricKind>,
71    pub used_device_credential: bool,
72}
73
74/// Portable biometric error payload.
75#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
76pub struct BiometricError {
77    pub code: String,
78    pub message: String,
79}
80
81impl BiometricError {
82    /// Creates a portable biometric error payload.
83    ///
84    /// `code` should be a stable, machine-readable reason such as
85    /// `unsupported`, `permission_denied`, or `timeout`. `message` should be a
86    /// concise human-readable explanation suitable for logs or developer-facing
87    /// diagnostics.
88    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
89        Self {
90            code: code.into(),
91            message: message.into(),
92        }
93    }
94
95    /// Creates the standard unsupported-operation error for this capability.
96    ///
97    /// `operation` should name the attempted biometric operation. Use this
98    /// from hosts that implement the capability contract but cannot provide this
99    /// operation on the current platform or hardware.
100    pub fn unsupported(operation: impl Into<String>) -> Self {
101        Self::new(
102            "unsupported",
103            format!(
104                "biometric operation `{}` is not supported by this host",
105                operation.into()
106            ),
107        )
108    }
109}
110
111pub struct GetBiometricAvailabilityCapability;
112impl OperationCapability for GetBiometricAvailabilityCapability {
113    type Request = ();
114    type Ok = BiometricAvailability;
115    type Err = BiometricError;
116}
117
118pub struct AuthenticateBiometricCapability;
119impl OperationCapability for AuthenticateBiometricCapability {
120    type Request = BiometricAuthenticateRequest;
121    type Ok = BiometricAuthenticateResult;
122    type Err = BiometricError;
123}
124
125pub struct CancelBiometricAuthenticationCapability;
126impl OperationCapability for CancelBiometricAuthenticationCapability {
127    type Request = ();
128    type Ok = ();
129    type Err = BiometricError;
130}
131
132pub const GET_BIOMETRIC_AVAILABILITY: CapabilityType<GetBiometricAvailabilityCapability> =
133    CapabilityType::new("fission.biometric.get_availability");
134pub const AUTHENTICATE_BIOMETRIC: CapabilityType<AuthenticateBiometricCapability> =
135    CapabilityType::new("fission.biometric.authenticate");
136pub const CANCEL_BIOMETRIC_AUTHENTICATION: CapabilityType<CancelBiometricAuthenticationCapability> =
137    CapabilityType::new("fission.biometric.cancel_authentication");
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn biometric_request_round_trips() {
145        let request = BiometricAuthenticateRequest {
146            reason: "Unlock encrypted notes".into(),
147            title: Some("Unlock".into()),
148            subtitle: Some("Confirm it is you".into()),
149            fallback_title: Some("Use passcode".into()),
150            cancel_title: Some("Cancel".into()),
151            allow_device_credential: true,
152            required_strength: BiometricStrength::Strong,
153        };
154
155        let bytes = serde_json::to_vec(&request).unwrap();
156        let decoded: BiometricAuthenticateRequest = serde_json::from_slice(&bytes).unwrap();
157
158        assert_eq!(decoded, request);
159    }
160
161    #[test]
162    fn availability_round_trips() {
163        let availability = BiometricAvailability {
164            supported: true,
165            enrolled: true,
166            strong: true,
167            weak: true,
168            device_credential: true,
169            kinds: vec![BiometricKind::Face, BiometricKind::Fingerprint],
170            reason: None,
171        };
172
173        let bytes = serde_json::to_vec(&availability).unwrap();
174        let decoded: BiometricAvailability = serde_json::from_slice(&bytes).unwrap();
175
176        assert_eq!(decoded, availability);
177    }
178}