Implement Android Credential Manager and Autofill providers (#109)
Android ARM verification / rust (push) Failing after 49m12s
Android ARM verification / android (push) Failing after 8s
Dependency security audit / rustsec (push) Failing after 21s

This commit is contained in:
hugo committed 2026-10-12 19:18:10 +02:00
1 parent 316a875d63
commit 0c8366d34f
48 files changed
+4138 -78

No files matched your search

+210
View File
@@ -0,0 +1,210 @@
//! Android provider bindings; all credential decisions remain in storage.
use crate::{MobileAuthentication, MobileAuthenticationFfiError};
use ironstorage::mobile_credentials as storage;
#[derive(Clone, Debug, uniffi::Record)]
pub struct MobileCredentialCaller {
pub package: String,
pub certificates: Vec<String>,
pub origin: Option<String>,
}
impl From<MobileCredentialCaller> for storage::CredentialCaller {
fn from(value: MobileCredentialCaller) -> Self {
Self {
package: value.package,
certificates: value.certificates,
origin: value.origin,
}
}
}
#[derive(Clone, Debug, uniffi::Record)]
pub struct MobileCredentialAttribute {
pub name: String,
pub value: String,
}
#[derive(Clone, Debug, uniffi::Record)]
pub struct MobileCredentialField {
pub index: u32,
pub hints: Vec<String>,
pub html_attributes: Vec<MobileCredentialAttribute>,
pub input_type: u32,
pub focused: bool,
pub editable: bool,
}
#[derive(Clone, Debug, uniffi::Record)]
pub struct MobileCredentialForm {
pub username: u32,
pub password: u32,
}
#[derive(Clone, Debug, uniffi::Record)]
pub struct MobileCredentialCandidate {
pub path: String,
pub matched: bool,
}
#[derive(Clone, Debug, uniffi::Record)]
pub struct MobileProviderRequest {
pub token: String,
pub title: String,
pub revision: String,
pub candidates: Vec<MobileCredentialCandidate>,
}
impl From<storage::CredentialRequest> for MobileProviderRequest {
fn from(value: storage::CredentialRequest) -> Self {
Self {
token: value.token,
title: value.identity.title,
revision: value.revision,
candidates: value
.candidates
.into_iter()
.map(|candidate| MobileCredentialCandidate {
path: candidate.path,
matched: candidate.matched,
})
.collect(),
}
}
}
#[derive(uniffi::Record)]
pub struct MobileCredentialValue {
pub username: String,
pub password: String,
}
impl std::fmt::Debug for MobileCredentialValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("MobileCredentialValue([REDACTED])")
}
}
fn rejected(detail: &str) -> MobileAuthenticationFfiError {
MobileAuthenticationFfiError::Failed {
kind: crate::MobileAuthenticationErrorKind::Entry,
title: "Credential Request Rejected".into(),
detail: detail.into(),
}
}
#[uniffi::export]
pub fn mobile_credential_form(
fields: Vec<MobileCredentialField>,
) -> Result<MobileCredentialForm, MobileAuthenticationFfiError> {
let fields: Vec<_> = fields
.into_iter()
.map(|field| storage::CredentialField {
index: field.index,
hints: field.hints,
html_attributes: field
.html_attributes
.into_iter()
.map(|attribute| (attribute.name, attribute.value))
.collect(),
input_type: field.input_type,
focused: field.focused,
editable: field.editable,
})
.collect();
let form = storage::credential_form(&fields).map_err(rejected)?;
Ok(MobileCredentialForm {
username: form.username,
password: form.password,
})
}
#[uniffi::export]
impl MobileAuthentication {
pub fn expire_credential_requests(&self) -> Result<(), MobileAuthenticationFfiError> {
self.authentication
.expire_credential_requests()
.map_err(Into::into)
}
pub fn begin_credential_request(
&self,
caller: MobileCredentialCaller,
) -> Result<MobileProviderRequest, MobileAuthenticationFfiError> {
Ok(self
.authentication
.begin_credential_request(caller.into())?
.into())
}
pub fn credential_request(
&self,
token: String,
caller: MobileCredentialCaller,
query: String,
) -> Result<MobileProviderRequest, MobileAuthenticationFfiError> {
Ok(self
.authentication
.search_credential_request(&token, &caller.into(), &query)?
.into())
}
pub fn cancel_credential_request(
&self,
token: String,
) -> Result<(), MobileAuthenticationFfiError> {
self.authentication
.cancel_credential_request(&token)
.map_err(Into::into)
}
pub fn stage_credential_save(
&self,
token: String,
caller: MobileCredentialCaller,
username: String,
password: String,
) -> Result<(), MobileAuthenticationFfiError> {
self.authentication
.stage_credential_save(
&token,
&caller.into(),
ironstorage::repository::SecretBytes::new(username.into_bytes()),
ironstorage::repository::SecretBytes::new(password.into_bytes()),
)
.map_err(Into::into)
}
pub fn save_staged_credential(
&self,
token: String,
caller: MobileCredentialCaller,
path: String,
consent: bool,
overwrite: bool,
passphrase: Option<String>,
) -> Result<(), MobileAuthenticationFfiError> {
self.authentication
.save_staged_credential(
&token,
&caller.into(),
&path,
consent,
overwrite,
passphrase.map(|text| ironstorage::repository::SecretBytes::new(text.into_bytes())),
)
.map_err(Into::into)
}
pub fn fill_credential(
&self,
token: String,
caller: MobileCredentialCaller,
path: String,
consent: bool,
passphrase: Option<String>,
allowed_usernames: Vec<String>,
) -> Result<MobileCredentialValue, MobileAuthenticationFfiError> {
let value = self.authentication.fill_credential(
&token,
&caller.into(),
&path,
consent,
passphrase.map(|text| ironstorage::repository::SecretBytes::new(text.into_bytes())),
&allowed_usernames,
)?;
Ok(MobileCredentialValue {
username: String::from_utf8(value.username.expose().to_vec())
.map_err(|_| rejected("Invalid username text."))?,
password: String::from_utf8(value.password.expose().to_vec())
.map_err(|_| rejected("Invalid password text."))?,
})
}
}
#[uniffi::export]
pub fn mobile_credential_privileged_callers() -> String {
storage::privileged_callers_json()
}
+3
View File
@@ -3021,3 +3021,6 @@ impl MobileAuthentication {
.map_err(Into::into)
}
}
mod credentials;
pub use credentials::*;