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

+95
View File
@@ -61,8 +61,20 @@ fn manifest_text(source: &str) -> Result<(), Box<dyn Error>> {
"de.rfc1437.ironstorage.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION",
"android.permission.INTERNET",
"android.permission.CAMERA",
"android.permission.QUERY_ALL_PACKAGES",
];
for node in root.children().filter(|node| node.is_element()) {
if node.attribute((ANDROID, "name")) == Some("android.permission.QUERY_ALL_PACKAGES")
&& !application.children().any(|service| {
service.has_tag_name("service")
&& service.attribute((ANDROID, "name"))
== Some("de.rfc1437.ironstorage.providers.IronAutofillService")
&& service.attribute((ANDROID, "permission"))
== Some("android.permission.BIND_AUTOFILL_SERVICE")
})
{
return Err("package visibility requires the reviewed autofill provider".into());
}
if node.tag_name().name().starts_with("uses-permission")
&& !allowed.contains(&node.attribute((ANDROID, "name")).unwrap_or(""))
{
@@ -109,6 +121,46 @@ fn manifest_text(source: &str) -> Result<(), Box<dyn Error>> {
{
return Err("unreviewed launcher intent".into());
}
} else if node.has_tag_name("service")
&& matches!(
node.attribute((ANDROID, "name")),
Some(
"de.rfc1437.ironstorage.providers.IronAutofillService"
| "de.rfc1437.ironstorage.providers.IronCredentialProviderService"
)
)
{
let autofill = node.attribute((ANDROID, "name"))
== Some("de.rfc1437.ironstorage.providers.IronAutofillService");
let (permission, action, metadata) = if autofill {
(
"android.permission.BIND_AUTOFILL_SERVICE",
"android.service.autofill.AutofillService",
"android.autofill",
)
} else {
(
"android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE",
"android.service.credentials.CredentialProviderService",
"android.credentials.provider",
)
};
let actions: Vec<_> = node
.descendants()
.filter(|node| node.has_tag_name("action") || node.has_tag_name("category"))
.filter_map(|node| node.attribute((ANDROID, "name")))
.collect();
if node.attribute((ANDROID, "exported")) != Some("true")
|| node.attribute((ANDROID, "permission")) != Some(permission)
|| actions != [action]
|| !node.children().any(|node| {
node.has_tag_name("meta-data")
&& node.attribute((ANDROID, "name")) == Some(metadata)
&& node.attribute((ANDROID, "resource")).is_some()
})
{
return Err("credential services must retain their system-only binding permissions and reviewed actions".into());
}
} else if node.has_tag_name("receiver")
&& node.attribute((ANDROID, "name"))
== Some("androidx.profileinstaller.ProfileInstallReceiver")
@@ -442,6 +494,10 @@ mod tests {
r#"<manifest xmlns:android="{ANDROID}" package="de.rfc1437.ironstorage"><application android:allowBackup="false" android:usesCleartextTraffic="false" android:fullBackupContent="@xml/backup_rules" android:dataExtractionRules="@xml/data_extraction_rules"><activity android:name="de.rfc1437.ironstorage.MainActivity" android:exported="true"><intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter></activity><provider android:name="fixture" android:exported="false"/></application></manifest>"#
);
assert!(manifest_text(&source).is_ok());
assert!(manifest_text(&source.replace(
"<application",
"<uses-permission android:name=\"android.permission.QUERY_ALL_PACKAGES\"/><application",
)).is_err());
assert!(
manifest_text(&source.replace(
"<application",
@@ -486,6 +542,45 @@ mod tests {
assert!(manifest_text(&launcher.replace("INSTALL_PROFILE", "UNREVIEWED")).is_err());
}
#[test]
fn provider_services_require_exact_system_binding_permissions() {
let source = format!(
r#"<manifest xmlns:android="{ANDROID}" package="de.rfc1437.ironstorage"><application android:allowBackup="false" android:usesCleartextTraffic="false" android:fullBackupContent="@xml/backup_rules" android:dataExtractionRules="@xml/data_extraction_rules"><activity android:name="de.rfc1437.ironstorage.MainActivity" android:exported="true"><intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter></activity><service android:name="de.rfc1437.ironstorage.providers.IronAutofillService" android:exported="true" android:permission="android.permission.BIND_AUTOFILL_SERVICE"><intent-filter><action android:name="android.service.autofill.AutofillService"/></intent-filter><meta-data android:name="android.autofill" android:resource="@xml/autofill_service"/></service></application></manifest>"#
);
assert!(manifest_text(&source).is_ok());
let visible = source.replace(
"<application",
"<uses-permission android:name=\"android.permission.QUERY_ALL_PACKAGES\"/><application",
);
assert!(manifest_text(&visible).is_ok());
assert!(
manifest_text(&visible.replace("IronAutofillService", "UnreviewedService")).is_err()
);
assert!(
manifest_text(&source.replace(
"android.permission.BIND_AUTOFILL_SERVICE",
"android.permission.INTERNET"
))
.is_err()
);
assert!(
manifest_text(&source.replace(
"android.service.autofill.AutofillService",
"unreviewed.action"
))
.is_err()
);
let credentials = source
.replace("IronAutofillService", "IronCredentialProviderService")
.replace("BIND_AUTOFILL_SERVICE", "BIND_CREDENTIAL_PROVIDER_SERVICE")
.replace(
"android.service.autofill.AutofillService",
"android.service.credentials.CredentialProviderService",
)
.replace("android.autofill", "android.credentials.provider");
assert!(manifest_text(&credentials).is_ok());
}
#[test]
fn backup_requires_both_sections_all_domains_and_no_include() {
let exclusions = DOMAINS
+1
View File
@@ -143,6 +143,7 @@ fn check(root: &Path) -> Result<(), Box<dyn Error>> {
for (module, allowed) in [
("app", &["features", "core", "platform"][..]),
("performance", &["app"][..]),
("provider-client", &[][..]),
("features", &["core"][..]),
("core", &["rust-bridge"][..]),
("platform", &["core"][..]),