Complete Android parity and security audit (#111)
This commit is contained in:
1 parent
45d78aa5b3
commit
b54636b7bc
14 files changed
+495
-72
No files matched your search
@@ -222,6 +222,8 @@ def build():
|
|||||||
decoded.write_text(run(analyzer, "resources", "xml", "--file", resource, path, capture=True).stdout)
|
decoded.write_text(run(analyzer, "resources", "xml", "--file", resource, path, capture=True).stdout)
|
||||||
rules.append(decoded)
|
rules.append(decoded)
|
||||||
tool("audit-manifest", manifest, *rules)
|
tool("audit-manifest", manifest, *rules)
|
||||||
|
if variant == "release":
|
||||||
|
tool("audit-release-manifest", manifest)
|
||||||
trees = []
|
trees = []
|
||||||
for target in ("aarch64-linux-android", "armv7-linux-androideabi"):
|
for target in ("aarch64-linux-android", "armv7-linux-androideabi"):
|
||||||
trees.append(run(CARGO, "+1.97.1", "tree", "--locked", "-p", "ironstorage-android", "--target", target, "--edges", "normal", "--prefix", "none", "--format", "{p}|{l}|{r}", capture=True, cwd=ROOT).stdout)
|
trees.append(run(CARGO, "+1.97.1", "tree", "--locked", "-p", "ironstorage-android", "--target", target, "--edges", "normal", "--prefix", "none", "--format", "{p}|{l}|{r}", capture=True, cwd=ROOT).stdout)
|
||||||
|
|||||||
Generated
+27
-19
@@ -1439,9 +1439,9 @@ checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chacha20"
|
name = "chacha20"
|
||||||
version = "0.10.1"
|
version = "0.10.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
|
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cipher 0.5.2",
|
"cipher 0.5.2",
|
||||||
@@ -2271,6 +2271,15 @@ dependencies = [
|
|||||||
"generic-array 0.14.7",
|
"generic-array 0.14.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "defmt"
|
||||||
|
version = "0.3.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad"
|
||||||
|
dependencies = [
|
||||||
|
"defmt 1.1.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "defmt"
|
name = "defmt"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -2883,10 +2892,12 @@ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "faster-hex"
|
name = "faster-hex"
|
||||||
version = "0.10.0"
|
version = "0.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73"
|
checksum = "04839bdf9d8c10f66806fad16b852fc72aab80873aebc3cb69d85b4fa41543ed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"defmt 0.3.100",
|
||||||
"heapless",
|
"heapless",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -3844,7 +3855,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1a791e6620676a875f362f3156ed213e73ca099a09bf992c18812abe65cc37b1"
|
checksum = "1a791e6620676a875f362f3156ed213e73ca099a09bf992c18812abe65cc37b1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bstr",
|
"bstr",
|
||||||
"hashbrown 0.17.1",
|
"hashbrown 0.15.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5588,7 +5599,7 @@ version = "0.2.35"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
|
checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt",
|
"defmt 1.1.1",
|
||||||
"jiff-core",
|
"jiff-core",
|
||||||
"jiff-static",
|
"jiff-static",
|
||||||
"jiff-tzdb-platform",
|
"jiff-tzdb-platform",
|
||||||
@@ -5605,7 +5616,7 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
|
checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt",
|
"defmt 1.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6005,15 +6016,12 @@ name = "lru"
|
|||||||
version = "0.16.4"
|
version = "0.16.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39"
|
checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39"
|
||||||
dependencies = [
|
|
||||||
"hashbrown 0.16.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lru"
|
name = "lru"
|
||||||
version = "0.18.1"
|
version = "0.18.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6"
|
checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown 0.17.1",
|
"hashbrown 0.17.1",
|
||||||
]
|
]
|
||||||
@@ -8147,7 +8155,7 @@ dependencies = [
|
|||||||
"hashbrown 0.17.1",
|
"hashbrown 0.17.1",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"kasuari",
|
"kasuari",
|
||||||
"lru 0.18.1",
|
"lru 0.18.2",
|
||||||
"palette",
|
"palette",
|
||||||
"serde",
|
"serde",
|
||||||
"strum",
|
"strum",
|
||||||
@@ -8523,12 +8531,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rqrr"
|
name = "rqrr"
|
||||||
version = "0.10.1"
|
version = "0.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ffbe87d9e8db95652c25ded2418150e00b08c2fde09e23ec15896d2c470c6631"
|
checksum = "8d084c50c5cfa397a081850da329566c8667b0b6c4e81da16abff1fef27de93b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"g2p",
|
"g2p",
|
||||||
"lru 0.16.4",
|
"lru 0.18.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8755,9 +8763,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.45"
|
version = "0.23.46"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -9886,7 +9894,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.4.3",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.1.4",
|
"rustix 1.1.4",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
|
|||||||
+2
-2
@@ -54,9 +54,9 @@ qrcode = { version = "0.14", default-features = false }
|
|||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
regex = "1.13"
|
regex = "1.13"
|
||||||
reqwest = { version = "0.13", default-features = false, features = ["blocking", "rustls"] }
|
reqwest = { version = "0.13", default-features = false, features = ["blocking", "rustls"] }
|
||||||
rustls = { version = "0.23", default-features = false }
|
rustls = { version = "0.23.46", default-features = false }
|
||||||
rfd = { version = "0.17", default-features = false }
|
rfd = { version = "0.17", default-features = false }
|
||||||
rqrr = { version = "0.10", default-features = false }
|
rqrr = { version = "0.11", default-features = false }
|
||||||
rpassword = "7.5"
|
rpassword = "7.5"
|
||||||
russh = { version = "=0.63.1", default-features = false, features = ["ring", "rsa"] }
|
russh = { version = "=0.63.1", default-features = false, features = ["ring", "rsa"] }
|
||||||
ratatui = { version = "0.30", default-features = false, features = ["crossterm_0_29", "layout-cache", "macros", "underline-color", "unstable-rendered-line-info"] }
|
ratatui = { version = "0.30", default-features = false, features = ["crossterm_0_29", "layout-cache", "macros", "underline-color", "unstable-rendered-line-info"] }
|
||||||
|
|||||||
@@ -86,10 +86,10 @@ class TotpScreenTest {
|
|||||||
compose.onNodeWithText("Unlock").performClick()
|
compose.onNodeWithText("Unlock").performClick()
|
||||||
compose.waitUntil(5000) { fixture.ticks > 0 }
|
compose.waitUntil(5000) { fixture.ticks > 0 }
|
||||||
compose.onNodeWithText("Back to TOTP entries").performClick()
|
compose.onNodeWithText("Back to TOTP entries").performClick()
|
||||||
val stopped = fixture.ticks
|
compose.onNodeWithTag("otp-code").assertDoesNotExist()
|
||||||
|
val stopped = compose.runOnIdle { fixture.ticks }
|
||||||
Thread.sleep(1200)
|
Thread.sleep(1200)
|
||||||
compose.waitForIdle()
|
compose.runOnIdle { assertEquals(stopped, fixture.ticks) }
|
||||||
assertEquals(stopped, fixture.ticks)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun largeTextShowsEveryDigitWithoutClipping() {
|
@Test fun largeTextShowsEveryDigitWithoutClipping() {
|
||||||
|
|||||||
+20
@@ -9,7 +9,9 @@ import java.util.UUID
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@@ -66,6 +68,24 @@ class NativeEntryViewerTest {
|
|||||||
val key = setup.generate("Viewer fixture", password).selectedKey
|
val key = setup.generate("Viewer fixture", password).selectedKey
|
||||||
setup.setupLocal(requireNotNull(key), password)
|
setup.setupLocal(requireNotNull(key), password)
|
||||||
}
|
}
|
||||||
|
repeat(12) {
|
||||||
|
coroutineScope {
|
||||||
|
val onboarding = async(Dispatchers.IO) {
|
||||||
|
session.storage().androidOnboarding(session.backend()).use { it.status() }
|
||||||
|
}
|
||||||
|
val home = async(Dispatchers.IO) {
|
||||||
|
session.storage().androidHomeOperation(
|
||||||
|
session.backend(),
|
||||||
|
object : MobileClock {
|
||||||
|
override fun monotonicMilliseconds(): ULong =
|
||||||
|
android.os.SystemClock.elapsedRealtime().toULong()
|
||||||
|
}
|
||||||
|
).use { it.cached() }
|
||||||
|
}
|
||||||
|
assertTrue(onboarding.await().configured)
|
||||||
|
home.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
for (appearance in Appearance.entries) {
|
for (appearance in Appearance.entries) {
|
||||||
withContext(Dispatchers.Main.immediate) { authentication.setAppearance(appearance) }
|
withContext(Dispatchers.Main.immediate) { authentication.setAppearance(appearance) }
|
||||||
withTimeout(30000) {
|
withTimeout(30000) {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use crate::{
|
|||||||
mobile_authentication::{
|
mobile_authentication::{
|
||||||
MobileAuthentication, MobileRepositoryOperation, MobileRepositoryOperationError,
|
MobileAuthentication, MobileRepositoryOperation, MobileRepositoryOperationError,
|
||||||
},
|
},
|
||||||
mobile_private_storage::{PrivateStorage, PrivateStorageError, exclusive_lock},
|
mobile_private_storage::{PrivateStorage, PrivateStorageError, runtime_lock},
|
||||||
repository::{Repository, RepositoryError},
|
repository::{Repository, RepositoryError},
|
||||||
secret_store::{
|
secret_store::{
|
||||||
NativeSecretBackend, SecretCachePolicy, SecretProtectionPolicy, SecretStore,
|
NativeSecretBackend, SecretCachePolicy, SecretProtectionPolicy, SecretStore,
|
||||||
@@ -582,8 +582,7 @@ impl MobileHomeStorage {
|
|||||||
cap_std::ambient_authority(),
|
cap_std::ambient_authority(),
|
||||||
)
|
)
|
||||||
.map_err(|_| MobileHomeError::from_private(PrivateStorageError::Io))?;
|
.map_err(|_| MobileHomeError::from_private(PrivateStorageError::Io))?;
|
||||||
let lock =
|
let lock = runtime_lock(&root).map_err(MobileHomeError::from_private)?;
|
||||||
exclusive_lock(&root, "runtime.lock").map_err(MobileHomeError::from_private)?;
|
|
||||||
let config = private
|
let config = private
|
||||||
.storage
|
.storage
|
||||||
.configuration()
|
.configuration()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use crate::{
|
|||||||
authentication::SystemAuthenticationClock,
|
authentication::SystemAuthenticationClock,
|
||||||
git::{GitCredential, GitFetchTransport, GitRemoteCredentialProvider, GitSmartHttpTransport},
|
git::{GitCredential, GitFetchTransport, GitRemoteCredentialProvider, GitSmartHttpTransport},
|
||||||
mobile_onboarding::android::tests::{Backend, home_fixture},
|
mobile_onboarding::android::tests::{Backend, home_fixture},
|
||||||
|
mobile_private_storage::exclusive_lock,
|
||||||
repository::SecretBytes,
|
repository::SecretBytes,
|
||||||
};
|
};
|
||||||
use std::{fs, sync::mpsc, thread, time::Duration};
|
use std::{fs, sync::mpsc, thread, time::Duration};
|
||||||
@@ -66,7 +67,7 @@ fn private_home_keeps_transient_private_storage_retry_guidance() {
|
|||||||
)
|
)
|
||||||
.err()
|
.err()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(error.kind(), MobileHomeErrorKind::Configuration);
|
assert_eq!(error.kind(), MobileHomeErrorKind::Conflict);
|
||||||
assert!(error.detail().contains("Retry later"));
|
assert!(error.detail().contains("Retry later"));
|
||||||
assert!(
|
assert!(
|
||||||
!error
|
!error
|
||||||
|
|||||||
@@ -538,7 +538,7 @@ impl AndroidOnboarding {
|
|||||||
}
|
}
|
||||||
fn lock(&self) -> Result<fs::File, MobileOnboardingError> {
|
fn lock(&self) -> Result<fs::File, MobileOnboardingError> {
|
||||||
let dir = Dir::open_ambient_dir(&self.root, ambient_authority()).map_err(io_error)?;
|
let dir = Dir::open_ambient_dir(&self.root, ambient_authority()).map_err(io_error)?;
|
||||||
crate::mobile_private_storage::exclusive_lock(&dir, "runtime.lock").map_err(private_error)
|
crate::mobile_private_storage::runtime_lock(&dir).map_err(private_error)
|
||||||
}
|
}
|
||||||
fn begin(
|
fn begin(
|
||||||
&self,
|
&self,
|
||||||
@@ -878,7 +878,11 @@ fn io_error(error: io::Error) -> MobileOnboardingError {
|
|||||||
fn private_error(error: PrivateStorageError) -> MobileOnboardingError {
|
fn private_error(error: PrivateStorageError) -> MobileOnboardingError {
|
||||||
MobileOnboardingError {
|
MobileOnboardingError {
|
||||||
kind: MobileOnboardingErrorKind::Configuration,
|
kind: MobileOnboardingErrorKind::Configuration,
|
||||||
title: "Setup Recovery Required",
|
title: if error == PrivateStorageError::Busy {
|
||||||
|
"Private Storage Busy"
|
||||||
|
} else {
|
||||||
|
"Setup Recovery Required"
|
||||||
|
},
|
||||||
detail: error.detail().into(),
|
detail: error.detail().into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -912,6 +916,16 @@ pub(crate) mod tests {
|
|||||||
};
|
};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
#[test]
|
||||||
|
fn private_contention_does_not_claim_setup_recovery() {
|
||||||
|
let busy = private_error(PrivateStorageError::Busy);
|
||||||
|
assert_eq!(busy.title(), "Private Storage Busy");
|
||||||
|
assert_eq!(busy.detail(), PrivateStorageError::Busy.detail());
|
||||||
|
assert_eq!(
|
||||||
|
private_error(PrivateStorageError::CorruptLayout).title(),
|
||||||
|
"Setup Recovery Required"
|
||||||
|
);
|
||||||
|
}
|
||||||
const ALICE: &str = "7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30";
|
const ALICE: &str = "7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30";
|
||||||
const BOB: &str = "B37027B56FC406BD3F6A622B2AC03492B992D06F";
|
const BOB: &str = "B37027B56FC406BD3F6A622B2AC03492B992D06F";
|
||||||
fn password() -> Option<SecretBytes> {
|
fn password() -> Option<SecretBytes> {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ impl PrivateStorageError {
|
|||||||
Self::AccessDenied => "Private storage access was denied. Existing data was retained.",
|
Self::AccessDenied => "Private storage access was denied. Existing data was retained.",
|
||||||
Self::ReadOnly => "Private storage is read-only. Existing data was retained.",
|
Self::ReadOnly => "Private storage is read-only. Existing data was retained.",
|
||||||
Self::StorageFull => "Free device storage and retry. Existing data was retained.",
|
Self::StorageFull => "Free device storage and retry. Existing data was retained.",
|
||||||
Self::Busy => "Another process is initializing private storage. Retry later.",
|
Self::Busy => "Another private storage operation is in progress. Retry later.",
|
||||||
Self::Io => "Private storage could not be accessed. Existing data was retained.",
|
Self::Io => "Private storage could not be accessed. Existing data was retained.",
|
||||||
Self::CorruptLayout => {
|
Self::CorruptLayout => {
|
||||||
"Storage layout metadata is damaged. Do not reset the repository."
|
"Storage layout metadata is damaged. Do not reset the repository."
|
||||||
@@ -179,7 +179,7 @@ impl PrivateStorage {
|
|||||||
let os_cache = Dir::open_ambient_dir(&roots.cache, ambient_authority())?;
|
let os_cache = Dir::open_ambient_dir(&roots.cache, ambient_authority())?;
|
||||||
let data = private_directory(&no_backup, "ironstorage")?;
|
let data = private_directory(&no_backup, "ironstorage")?;
|
||||||
let cache = private_directory(&os_cache, "ironstorage")?;
|
let cache = private_directory(&os_cache, "ironstorage")?;
|
||||||
let _initialization = exclusive_lock(&data, "runtime.lock")?;
|
let _initialization = runtime_lock(&data)?;
|
||||||
let layout = match regular_bytes(&data, MARKER, 16 * 1024)? {
|
let layout = match regular_bytes(&data, MARKER, 16 * 1024)? {
|
||||||
Some(bytes) => toml::from_str::<Layout>(
|
Some(bytes) => toml::from_str::<Layout>(
|
||||||
std::str::from_utf8(&bytes).map_err(|_| PrivateStorageError::CorruptLayout)?,
|
std::str::from_utf8(&bytes).map_err(|_| PrivateStorageError::CorruptLayout)?,
|
||||||
@@ -321,12 +321,19 @@ impl PrivateStorage {
|
|||||||
clock: Arc<dyn crate::authentication::AuthenticationClock>,
|
clock: Arc<dyn crate::authentication::AuthenticationClock>,
|
||||||
) -> Result<Arc<MobileAuthentication>, MobileAuthenticationError> {
|
) -> Result<Arc<MobileAuthentication>, MobileAuthenticationError> {
|
||||||
let recovery = |error: PrivateStorageError| {
|
let recovery = |error: PrivateStorageError| {
|
||||||
MobileAuthenticationError::new(
|
let (kind, title) = if error == PrivateStorageError::Busy {
|
||||||
|
(
|
||||||
|
MobileAuthenticationErrorKind::Conflict,
|
||||||
|
"Private Storage Busy",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
MobileAuthenticationErrorKind::Configuration,
|
MobileAuthenticationErrorKind::Configuration,
|
||||||
"Private Storage Recovery Required",
|
"Private Storage Recovery Required",
|
||||||
error.detail(),
|
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
MobileAuthenticationError::new(kind, title, error.detail())
|
||||||
|
};
|
||||||
// Retain the native backend's encrypted transactions across the FFI callback adapter.
|
// Retain the native backend's encrypted transactions across the FFI callback adapter.
|
||||||
let backend: Arc<dyn SecretStoreBackend> = match self
|
let backend: Arc<dyn SecretStoreBackend> = match self
|
||||||
.android_backend
|
.android_backend
|
||||||
@@ -344,7 +351,11 @@ impl PrivateStorage {
|
|||||||
let config = self
|
let config = self
|
||||||
.configuration()
|
.configuration()
|
||||||
.map_err(recovery)?
|
.map_err(recovery)?
|
||||||
.ok_or_else(|| recovery(PrivateStorageError::ConfigurationMissingWithData))?;
|
.ok_or_else(|| MobileAuthenticationError::new(
|
||||||
|
MobileAuthenticationErrorKind::Configuration,
|
||||||
|
"Set Up Password Store",
|
||||||
|
"Complete local or HTTPS password-store setup before opening protected settings or entries.",
|
||||||
|
))?;
|
||||||
if let Some(current) = owner.as_ref() {
|
if let Some(current) = owner.as_ref() {
|
||||||
if current.matches_configuration(&config)? {
|
if current.matches_configuration(&config)? {
|
||||||
return Ok(Arc::clone(current));
|
return Ok(Arc::clone(current));
|
||||||
@@ -468,7 +479,7 @@ impl PrivateStorage {
|
|||||||
&self,
|
&self,
|
||||||
backend: &dyn SecretStoreBackend,
|
backend: &dyn SecretStoreBackend,
|
||||||
) -> Result<(), PrivateStorageError> {
|
) -> Result<(), PrivateStorageError> {
|
||||||
let _lock = exclusive_lock(&self.data, "runtime.lock")?;
|
let _lock = runtime_lock(&self.data)?;
|
||||||
let bytes = regular_bytes(&self.data, MARKER, 16 * 1024)?
|
let bytes = regular_bytes(&self.data, MARKER, 16 * 1024)?
|
||||||
.ok_or(PrivateStorageError::CorruptLayout)?;
|
.ok_or(PrivateStorageError::CorruptLayout)?;
|
||||||
let mut layout: Layout = toml::from_str(
|
let mut layout: Layout = toml::from_str(
|
||||||
@@ -553,7 +564,7 @@ impl PrivateStorage {
|
|||||||
&self,
|
&self,
|
||||||
action: impl FnOnce(&Dir) -> Result<T, PrivateStorageError>,
|
action: impl FnOnce(&Dir) -> Result<T, PrivateStorageError>,
|
||||||
) -> Result<T, PrivateStorageError> {
|
) -> Result<T, PrivateStorageError> {
|
||||||
let initialization = exclusive_lock(&self.data, "runtime.lock")?;
|
let initialization = runtime_lock(&self.data)?;
|
||||||
let scratch = open_directory(&self.data, "scratch")?;
|
let scratch = open_directory(&self.data, "scratch")?;
|
||||||
let name = format!("transaction-{:016x}", rand::random::<u64>());
|
let name = format!("transaction-{:016x}", rand::random::<u64>());
|
||||||
let dir = private_directory(&scratch, &name)?;
|
let dir = private_directory(&scratch, &name)?;
|
||||||
@@ -634,6 +645,19 @@ pub(crate) fn exclusive_lock(parent: &Dir, name: &str) -> Result<fs::File, Priva
|
|||||||
crate::file_lock::try_lock(&file)?;
|
crate::file_lock::try_lock(&file)?;
|
||||||
Ok(file)
|
Ok(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn runtime_lock(parent: &Dir) -> Result<fs::File, PrivateStorageError> {
|
||||||
|
// Startup's binding/setup/Home reads share this gate; absorb brief overlap, not long operations.
|
||||||
|
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(1);
|
||||||
|
loop {
|
||||||
|
match exclusive_lock(parent, "runtime.lock") {
|
||||||
|
Err(PrivateStorageError::Busy) if std::time::Instant::now() < deadline => {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
result => return result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
fn save_layout(parent: &Dir, layout: &Layout) -> Result<(), PrivateStorageError> {
|
fn save_layout(parent: &Dir, layout: &Layout) -> Result<(), PrivateStorageError> {
|
||||||
let bytes = toml::to_string(layout).map_err(|_| PrivateStorageError::CorruptLayout)?;
|
let bytes = toml::to_string(layout).map_err(|_| PrivateStorageError::CorruptLayout)?;
|
||||||
let mut temporary = TempFile::new(parent)?;
|
let mut temporary = TempFile::new(parent)?;
|
||||||
@@ -1066,6 +1090,94 @@ mod tests {
|
|||||||
.ok_or(SecretStoreError::Missing)
|
.ok_or(SecretStoreError::Missing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[test]
|
||||||
|
fn authentication_distinguishes_first_run_from_missing_configuration_with_data() {
|
||||||
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
let storage = PrivateStorage::open(roots(temp.path())).unwrap();
|
||||||
|
let backend = Arc::new(BindingBackend::default());
|
||||||
|
let clock = Arc::new(crate::authentication::SystemAuthenticationClock::default());
|
||||||
|
let error = storage
|
||||||
|
.authentication(backend.clone(), clock.clone())
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(error.kind(), MobileAuthenticationErrorKind::Configuration);
|
||||||
|
assert_eq!(error.title(), "Set Up Password Store");
|
||||||
|
assert!(backend.0.lock().unwrap().is_none());
|
||||||
|
assert_eq!(storage.status().recovery, None);
|
||||||
|
configured(&storage);
|
||||||
|
let ciphertext = fs::read(storage.root.join("vault/mail.gpg")).unwrap();
|
||||||
|
fs::remove_file(storage.root.join("config.toml")).unwrap();
|
||||||
|
let error = storage
|
||||||
|
.authentication(backend.clone(), clock)
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(error.title(), "Private Storage Recovery Required");
|
||||||
|
assert_eq!(
|
||||||
|
error.detail(),
|
||||||
|
PrivateStorageError::ConfigurationMissingWithData.detail()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
storage.status().recovery,
|
||||||
|
Some(PrivateStorageError::ConfigurationMissingWithData)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
fs::read(storage.root.join("vault/mail.gpg")).unwrap(),
|
||||||
|
ciphertext
|
||||||
|
);
|
||||||
|
assert!(backend.0.lock().unwrap().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn startup_waits_for_a_short_lived_runtime_owner_without_losing_data() {
|
||||||
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
let storage = PrivateStorage::open(roots(temp.path())).unwrap();
|
||||||
|
configured(&storage);
|
||||||
|
let ciphertext = fs::read(storage.root.join("vault/mail.gpg")).unwrap();
|
||||||
|
let lock = exclusive_lock(&storage.data, "runtime.lock").unwrap();
|
||||||
|
let release = std::thread::spawn(move || {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||||
|
drop(lock);
|
||||||
|
});
|
||||||
|
let backend = Arc::new(BindingBackend::default());
|
||||||
|
let owner = storage
|
||||||
|
.authentication(
|
||||||
|
backend,
|
||||||
|
Arc::new(crate::authentication::SystemAuthenticationClock::default()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
release.join().unwrap();
|
||||||
|
assert!(!owner.state().unwrap().unlocked());
|
||||||
|
assert_eq!(
|
||||||
|
fs::read(storage.root.join("vault/mail.gpg")).unwrap(),
|
||||||
|
ciphertext
|
||||||
|
);
|
||||||
|
assert_eq!(storage.status().recovery, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn authentication_contention_is_retryable_without_claiming_data_recovery() {
|
||||||
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
let storage = PrivateStorage::open(roots(temp.path())).unwrap();
|
||||||
|
configured(&storage);
|
||||||
|
let backend = Arc::new(BindingBackend::default());
|
||||||
|
let clock = Arc::new(crate::authentication::SystemAuthenticationClock::default());
|
||||||
|
let lock = exclusive_lock(&storage.data, "runtime.lock").unwrap();
|
||||||
|
let error = storage
|
||||||
|
.authentication(backend.clone(), clock.clone())
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(error.kind(), MobileAuthenticationErrorKind::Conflict);
|
||||||
|
assert_eq!(error.title(), "Private Storage Busy");
|
||||||
|
assert_eq!(error.detail(), PrivateStorageError::Busy.detail());
|
||||||
|
assert!(backend.0.lock().unwrap().is_none());
|
||||||
|
assert_eq!(storage.status().recovery, None);
|
||||||
|
drop(lock);
|
||||||
|
let owner = storage.authentication(backend, clock).unwrap();
|
||||||
|
assert!(!owner.state().unwrap().unlocked());
|
||||||
|
assert!(storage.status().configured);
|
||||||
|
assert_eq!(storage.status().recovery, None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn restored_or_mismatched_keystore_binding_is_a_typed_non_destructive_recovery() {
|
fn restored_or_mismatched_keystore_binding_is_a_typed_non_destructive_recovery() {
|
||||||
let temp = tempfile::tempdir().unwrap();
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ At #89 pin a mutually supported stable AGP/Gradle/Kotlin/KSP/Hilt set, JDK,
|
|||||||
SDK build tools and NDK, plus wrapper checksum. The current [AGP 9.4 contract](https://developer.android.com/build/releases/agp-9-4-0-release-notes)
|
SDK build tools and NDK, plus wrapper checksum. The current [AGP 9.4 contract](https://developer.android.com/build/releases/agp-9-4-0-release-notes)
|
||||||
requires Gradle 9.6.0 and JDK 17 and supports API 37. This is a candidate build
|
requires Gradle 9.6.0 and JDK 17 and supports API 37. This is a candidate build
|
||||||
baseline, subject to actual artifact availability and plugin compatibility.
|
baseline, subject to actual artifact availability and plugin compatibility.
|
||||||
The #89 scaffold pins AGP 9.4.1, Gradle 9.8.1, Kotlin 2.4.20, KSP 2.3.12,
|
The #89 scaffold pins AGP 9.4.1, Gradle 9.8.1, Kotlin 2.4.21, KSP 2.3.12,
|
||||||
Hilt 2.60.1 and JDK 25 (JVM output 17); its catalog and build instructions live
|
Hilt 2.60.1 and JDK 25 (JVM output 17); its catalog and build instructions live
|
||||||
in [android/README.md](../../android/README.md). These pins are verified by the
|
in [android/README.md](../../android/README.md). These pins are verified by the
|
||||||
scaffold build gates rather than inferred from minimum-version requirements.
|
scaffold build gates rather than inferred from minimum-version requirements.
|
||||||
@@ -198,9 +198,11 @@ a remote clone. Do not seed app data or commit credentials to prove recovery.
|
|||||||
## Verification and acceptance
|
## Verification and acceptance
|
||||||
|
|
||||||
The [parity matrix](parity.md) assigns every current non-Watch mobile capability
|
The [parity matrix](parity.md) assigns every current non-Watch mobile capability
|
||||||
and every Android issue to a Rust owner and Android API. Its pending rows are
|
and every Android issue to a Rust owner, current Android component and executable
|
||||||
implementation obligations, not accepted gaps. #111 must replace pending status
|
checks. The [#111 audit](audit.md) records current results, runtime limits and the
|
||||||
with evidence and have no unexplained gap before #112 distribution.
|
unpatched/deprecated upstream dependency findings tracked outside this milestone
|
||||||
|
in [#125](https://git.rfc1437.de/hugo/IronStorage/issues/125). This traceability does
|
||||||
|
not claim that cargo-audit is clean or grant unrestricted release approval.
|
||||||
|
|
||||||
Every issue runs the AGENTS.md Rust gates before commit. Android changes also
|
Every issue runs the AGENTS.md Rust gates before commit. Android changes also
|
||||||
run build/lint/unit/instrumented checks appropriate to the issue and computer-use
|
run build/lint/unit/instrumented checks appropriate to the issue and computer-use
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
# Android parity, architecture and security audit (#111)
|
||||||
|
|
||||||
|
Audit date: 2026-10-15. Scope is milestone 23, the current non-Watch iPhone
|
||||||
|
surface and the Android release graph. [parity.md](parity.md) maps every
|
||||||
|
capability to its Rust owner, concrete Android component and executable checks.
|
||||||
|
[architecture.md](architecture.md) remains the support/threat-model contract.
|
||||||
|
A test listed as a coverage pointer is not evidence that an opt-in journey ran.
|
||||||
|
The current iPhone AutoFill extension controller has no implemented selection
|
||||||
|
flow. The Android provider requirement is nevertheless fully implemented through
|
||||||
|
Rust and the native OS services; the matrix does not attribute that capability
|
||||||
|
to the empty iPhone controller.
|
||||||
|
|
||||||
|
## Findings corrected in this audit
|
||||||
|
|
||||||
|
- First-run Preferences previously claimed that configuration had been lost while
|
||||||
|
repository/key data remained. `PrivateStorage::authentication` now distinguishes
|
||||||
|
the validated empty configuration from actual `ConfigurationMissingWithData`.
|
||||||
|
Genuine recovery still retains its typed error and ciphertext. Rust regressions
|
||||||
|
first failed against the old implementation and now pass for both cases.
|
||||||
|
- Startup onboarding, binding verification and Home independently used the same
|
||||||
|
nonblocking runtime gate. Brief legitimate overlap could display a storage
|
||||||
|
failure on a cold start. Rust now tolerates up to one second of contention,
|
||||||
|
checking the same capability-validated lock every 10 ms. Longer operations
|
||||||
|
still fail with retry guidance. Scratch-owner locks remain nonblocking so
|
||||||
|
startup cleanup never deletes active scratch. Tests cover transient release,
|
||||||
|
a retained long-lived lock, unchanged encrypted bytes and successful retry.
|
||||||
|
- Busy authentication is a typed `Conflict` titled `Private Storage Busy`, rather
|
||||||
|
than a claim of corruption/recovery. The Home facade preserves the typed
|
||||||
|
conflict. Setup also reports contention as `Private Storage Busy` while genuine
|
||||||
|
setup corruption retains `Setup Recovery Required`. No frontend derives state
|
||||||
|
from error strings.
|
||||||
|
- API 26 exposed a test synchronization race in the TOTP disposal check: the
|
||||||
|
baseline was read immediately after a click, before Compose removed the code
|
||||||
|
view, and one legitimate pre-disposal tick could land afterward. The regression
|
||||||
|
now asserts that the code node is absent and reads/checks the counter on the UI
|
||||||
|
thread. It retains the full 1.2-second no-further-ticks assertion; no production
|
||||||
|
timer or assertion was disabled to obtain a pass. The corrected focused check
|
||||||
|
passed three isolated API 26 runs, then passed in the full 53-case app runner.
|
||||||
|
The failed original attempt is not counted as passing evidence.
|
||||||
|
- The release artifact gate now rejects `debuggable`, `testOnly` and `profileable`
|
||||||
|
access in the packaged production manifest. Debug/profile/benchmark packages
|
||||||
|
remain separately identifiable test artifacts. Manifest regression tests prove
|
||||||
|
rejection and the release CI path invokes the gate on decoded APK contents.
|
||||||
|
- Updated Android-reachable Rustls 0.23.45 to 0.23.46 (and raised its minimum),
|
||||||
|
faster-hex 0.10.0 to 0.10.1, rqrr 0.10.1 to 0.11.0 / lru 0.18.2, and the yanked
|
||||||
|
chacha20 0.10.1 to 0.10.2. QR fixtures and native decode tests exercise the
|
||||||
|
decoder change; both release ABIs were rebuilt from the resulting lockfile.
|
||||||
|
|
||||||
|
## Boundary and lifetime review
|
||||||
|
|
||||||
|
Reviewed the connected `crates/storage` mobile/authentication/onboarding/Git/QR/
|
||||||
|
credential/secret-store implementations, `crates/mobile-bridge`, `crates/android`,
|
||||||
|
and Android app/core/features/platform/native-loader modules. Rust owns directory
|
||||||
|
capabilities, repository operations, parsing, generation, OTP, QR transfer/decode,
|
||||||
|
secret records, authentication, endpoint and caller/site policy. UniFFI exports
|
||||||
|
mechanical values and OS callbacks. Kotlin maps typed state, schedules blocking
|
||||||
|
calls off the main thread, collects input and presents Android UI/OS services.
|
||||||
|
|
||||||
|
No Android-reachable application component launches a process. Project-authored
|
||||||
|
Rust forbids unsafe code; generated/dependency JNI/JNA/Keystore FFI is outside that
|
||||||
|
claim. `std::process::id()` obtains a transaction owner ID and does not launch a
|
||||||
|
process. Build/release tools legitimately invoke host commands. The desktop CLI's
|
||||||
|
external-editor support is outside the Android runtime graph and this audit; this
|
||||||
|
report does not claim that every unrelated workspace executable avoids processes.
|
||||||
|
|
||||||
|
NativeRuntime's `File`/`ZipFile` checks inspect installed APK/native-library facts,
|
||||||
|
not the repository. Provider package-certificate hashing obtains OS identity facts;
|
||||||
|
Rust compares caller/site identities and decides grants/matches. Kotlin never
|
||||||
|
parses pass documents, `.gpg-id`, GPG packets, OTP payloads or Git transport errors.
|
||||||
|
The source/dependency gate complements this connected-code review.
|
||||||
|
|
||||||
|
Git endpoints are validated typed HTTPS on Android (SSH feature disabled), without
|
||||||
|
embedded account/token userinfo. Repository remote configuration is authoritative.
|
||||||
|
Tokens stay in Rust-owned sealed records with OS wrapping keys. TLS uses the
|
||||||
|
Android platform verifier and redirects cannot forward credentials. There is no
|
||||||
|
cleartext transport or certificate-validation bypass to make fixtures pass.
|
||||||
|
|
||||||
|
Ordinary persistent state contains encrypted entries, encrypted private keys,
|
||||||
|
configuration, encrypted secret envelopes and private TOTP metadata, not decrypted
|
||||||
|
entry fields, passphrases, tokens or OTP seeds. Editor recovery is encrypted and
|
||||||
|
revision/generation-bound. Android saved state retains non-secret selection/query
|
||||||
|
facts only. Logs and reports use bounded typed diagnostics and sanitized metrics;
|
||||||
|
there is no app crash-upload service. Kotlin/Java memory cannot promise complete
|
||||||
|
erasure, and accessibility/IME/clipboard consumers can observe deliberately
|
||||||
|
revealed or handed-off data under OS control.
|
||||||
|
|
||||||
|
## Threat-test coverage and limits
|
||||||
|
|
||||||
|
| Threat | Enforcement and executable evidence | Interpretation |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Keystore record tamper, missing keys, restore/device binding | Rust Android backend/private-storage tests; real `AndroidKeystoreTest.deviceUnlockedRoundtripReplaceDeleteAndTamper` | Failure retains existing encrypted repository; no silent identity/key recreation for bound data |
|
||||||
|
| Biometric cancellation, rejection, invalidation/lock removal | Rust authentication/backend tests; opt-in platform `AndroidKeystoreTest` and `AuthenticationLeaseTest` | OS-interactive checks require separate recorded runs; do not count standard-suite exclusions as passes or change enrollment on the retained app phone |
|
||||||
|
| Backup/transfer and update | Packaged XML reference and all-domain exclusion gates, `BackupPolicyTest`, private-context/update Rust tests | Install over the same ID/signing lineage; emulator data retained. OEM transfer tools remain outside available emulator evidence |
|
||||||
|
| Screen capture/recents, focus loss, logs and plaintext saved state | Secure-window/recents platform wiring; `PresentationSecurityTest`, source audit, generation/lease Rust tests | Computer-use visibility is test observation, not proof that an arbitrary compromised OS cannot capture pixels |
|
||||||
|
| Clipboard and accessibility | OS ownership/expiry adapter, Rust copy policy, `PresentationSecurityTest`; manual hardening evidence | Deliberate reveal/copy is observable by authorized platform consumers. TalkBack focus/activation is verified; speech output and actual external switch scanning are not claimed |
|
||||||
|
| Background, process death, dirty edits and stale callbacks | `AuthenticationLeaseTest`, `PresentationSecurityTest`, `NativeEntryViewerTest`; Rust lease/editor checkpoint/cancellation tests | No stored lease, no plaintext draft restore, no replay of a completed mutation |
|
||||||
|
| Git dirty tree/conflict, TLS/auth/offline/cancellation | Rust `mobile_home::android_tests`, Git transport/embedded tests, Compose Home tests | Injected transports prove failure ordering without sending fixture tokens to external servers; runtime local Git journey is separate from public-server availability |
|
||||||
|
| Camera interruption, malicious/oversized QR/image | `AndroidQrCameraTest`, Rust QR/key-transfer tests and bounded Y-plane adapter | Camera restart and real frame delivery are distinct from decoding known QR vectors in Rust |
|
||||||
|
| Malicious providers, forged/stale/cancelled intents, preauth disclosure | Rust `mobile_credentials`/authentication tests, `ProviderIntentSecurityTest`, `NativeCredentialProviderTest`, system `provider-client` journeys | Paths/candidates may be listed while locked; secret fill requires exact live request, explicit consent/selection and valid lease |
|
||||||
|
| Offline, doze/battery, memory/storage pressure | Rust fault/rollback tests; native environment/hardening checks | No physical disk filling or physical-device coverage is implied by the low-storage broadcast |
|
||||||
|
| Root/debug assumptions | Non-debuggable packaged release gate, private capabilities/backup/Keystore policy | Rooted/fully compromised devices and local hostile timing observers are outside the accepted sandbox threat model; a debug APK is not the production security boundary |
|
||||||
|
|
||||||
|
## Dependency exceptions
|
||||||
|
|
||||||
|
RustSec database: 1,329 advisories, commit
|
||||||
|
`edb58f5974b83eb8302332821cb6d389777aa351`, updated 2026-10-14T22:18:32+02:00.
|
||||||
|
After the updates, the Android normal/build dependency graph has two remaining
|
||||||
|
findings, explicitly tracked outside this milestone in
|
||||||
|
[#125](https://git.rfc1437.de/hugo/IronStorage/issues/125):
|
||||||
|
|
||||||
|
- `pgp -> rsa 0.9.10`: [RUSTSEC-2023-0071](https://rustsec.org/advisories/RUSTSEC-2023-0071.html)
|
||||||
|
has **no patched release**. OpenPGP decryption is local and follows explicit
|
||||||
|
authorization; Git/HTTPS does not expose a remote RSA-decryption endpoint.
|
||||||
|
This limits the ordinary remote oracle but does not establish constant-time
|
||||||
|
RSA or eliminate local timing risk. Retaining existing pass RSA-key compatibility
|
||||||
|
does not justify claiming the advisory fixed. Reassess an upstream-reviewed,
|
||||||
|
compatible pure-Rust mitigation under #125.
|
||||||
|
- `gix-protocol -> bisync 0.3.0`: yanked/deprecated, with no replacement release.
|
||||||
|
Its [upstream notice](https://github.com/JM4ier/bisync) says the author retired
|
||||||
|
the tiny utility to avoid maintenance/supply-chain responsibility. The existing
|
||||||
|
lockfile checksum remains required; track a reviewed transport update removing
|
||||||
|
it under #125 rather than hiding the warning or adding an unreviewed fork.
|
||||||
|
|
||||||
|
Workspace `cargo audit` still exits nonzero: besides RSA it reports host/desktop
|
||||||
|
packages outside the Android graph (including quinn-proto and ruzstd). They were
|
||||||
|
not silently updated as Android work. No clean whole-workspace audit or unrestricted
|
||||||
|
release approval is claimed. License/SBOM policy and vulnerability status are
|
||||||
|
separate checks.
|
||||||
|
|
||||||
|
## Run record
|
||||||
|
|
||||||
|
Required Rust gates passed: formatting, warnings-as-errors workspace/all-target
|
||||||
|
check, Clippy with warnings denied, and the full workspace test run (524 passed,
|
||||||
|
zero failed). The final Android verification/build passed debug/release/AAB,
|
||||||
|
Lint including release, ktlint, JVM tests, both ARM native builds/metadata/16-KB
|
||||||
|
package audits, decoded backup/transfer/manifest checks, separate native symbols,
|
||||||
|
R8 mapping and deterministic CycloneDX/license policy (1,839 inventory components).
|
||||||
|
The CI orchestration self-test run passed all 11 tests.
|
||||||
|
|
||||||
|
The final API 37 / 4-KB ARM64 run passed app/core/bridge/platform instrumentation
|
||||||
|
and five cold starts. The native core suite exercised the new 12-pair concurrent
|
||||||
|
onboarding/Home regression along with real encrypted edit/mutation/OTP/provider
|
||||||
|
and catalog/recovery tests. Native QR analysis was 84 ms, authenticated fill
|
||||||
|
1,249 ms, and 10,000-entry navigation/search were 90/273 ms. Debug cold starts
|
||||||
|
were 840–1,220 ms; five native-load measurements were 40.8–58.4 ms.
|
||||||
|
|
||||||
|
The final API 26 / 4-KB ARM64 run likewise passed all four instrumentation suites
|
||||||
|
and five cold starts after the TOTP test synchronization fix. Native QR analysis
|
||||||
|
was 80 ms, authenticated fill 1,210 ms, and 10,000-entry navigation/search were
|
||||||
|
76/227 ms. Debug cold starts were 210–249 ms. Computer use opened actual first-run
|
||||||
|
Preferences and verified `Set Up Password Store` with local/HTTPS setup guidance,
|
||||||
|
replacing the old false data-recovery claim without creating or deleting a store.
|
||||||
|
|
||||||
|
API 26 release-equivalent Macrobenchmark passed five cold and five warm starts:
|
||||||
|
initial display 149.8–171.0 ms cold and 44.1–95.4 ms warm; full cold display
|
||||||
|
189.7–219.5 ms. The measured artifact retained its app data and baseline profile.
|
||||||
|
|
||||||
|
The explicit `biometricSecurity=true` real-OS fixture passed on the existing
|
||||||
|
API 37 phone fingerprint enrollment. Computer use observed the CryptoObject
|
||||||
|
prompts and cancellation result; emulator sensor events supplied matching/nonmatching
|
||||||
|
synthetic fingerprints. OS counters confirmed one rejected crypto attempt before
|
||||||
|
a matching unlock. The test asserted enrollment, shared reload while unlocked,
|
||||||
|
manual lock, cancellation, real activity background interruption, fresh post-background
|
||||||
|
unlock, immediate memory-pressure relock and biometric disable. Its namespace,
|
||||||
|
public passphrase and generated store were isolated from the retained app. Fixture
|
||||||
|
aliases and runner were cleaned up; device enrollment and app data were retained.
|
||||||
|
|
||||||
|
Strict API 37 / 16-KB ARM64 passed all four suites and five cold starts with
|
||||||
|
both compatibility paths disabled. Native QR analysis was 82 ms, authenticated
|
||||||
|
fill 1,216 ms, and 10,000-entry navigation/search were 82/251 ms. Cached TOTP,
|
||||||
|
warm refresh and provider preparation p95 were 5,349/8,241/4,195 us. Five debug
|
||||||
|
cold starts were 832–971 ms and native loads 66.4–75.8 ms. Computer use verified
|
||||||
|
the actual retained six-commit store and clean worktree loaded after the final
|
||||||
|
cold starts, with no startup busy/recovery failure.
|
||||||
|
|
||||||
|
Strict API 37 release-equivalent Macrobenchmark also passed five cold and five
|
||||||
|
warm starts: initial display 192.3–335.9 ms cold and 77.7–134.5 ms warm; full cold
|
||||||
|
display 399.0–510.1 ms. Computer use completed real framework Autofill and Android
|
||||||
|
Credential Manager selection/consent/unlock against the separate provider client;
|
||||||
|
both returned the exact expected public fixture username/password. The initial
|
||||||
|
requests expired during slow inspection; fresh requests passed within the
|
||||||
|
unchanged request lifetime. Pre-authentication selectors exposed entry paths,
|
||||||
|
not the secret fields.
|
||||||
|
|
||||||
|
Real API 35 ARM64 Keystore enrollment-change invalidation passed in an isolated
|
||||||
|
platform test app: the current-set biometric key became `Invalidated`, while a
|
||||||
|
separate DeviceUnlocked key still decrypted its public token. The first attempt
|
||||||
|
timed out while driving OS setup and is not counted as a pass; the fresh test
|
||||||
|
completed in 124.4 seconds, within the unchanged 180-second fixture window.
|
||||||
|
Computer use observed the encryption prompt, OS enrollment and return to the
|
||||||
|
fixture. Only synthetic enrollment on that dedicated device changed; no ordinary
|
||||||
|
IronStorage app was installed there. Test keys were deleted by the fixture.
|
||||||
|
The device was shut down gracefully afterward. Its existing synthetic PIN was
|
||||||
|
retained; no data or enrollment reset was used to recover the cold boot.
|
||||||
|
|
||||||
|
The updated fold emulator also loaded real first-run setup and Preferences via
|
||||||
|
computer use, with readable adaptive navigation and the corrected setup guidance.
|
||||||
|
The final connected-code review checked the diff, callers, error mappings,
|
||||||
|
fixtures and acceptance matrix. No issue is closed on an expired/failed run.
|
||||||
|
|
||||||
|
After the final setup-contention title correction, all four Rust gates passed
|
||||||
|
again (524 tests) and the full Android build/audit passed again for both ARM ABIs.
|
||||||
|
The final native viewer regression, including concurrent onboarding/Home, passed
|
||||||
|
on API 37 in 54.9 seconds. A fresh app cold launch took 743 ms and computer use
|
||||||
|
verified the retained three-commit vault and clean worktree. ARM64 emulator runtime evidence stays distinct from
|
||||||
|
ARMv7 packaging and historical per-issue evidence.
|
||||||
+30
-30
@@ -1,43 +1,43 @@
|
|||||||
# Android parity and issue ownership
|
# Android parity and issue ownership
|
||||||
|
|
||||||
Contract for [#88](https://git.rfc1437.de/hugo/IronStorage/issues/88), reviewed
|
Contract for [#88](https://git.rfc1437.de/hugo/IronStorage/issues/88), reviewed
|
||||||
2026-10-08 against apple/Sources/App/IronStorageApp.swift,
|
2026-10-15 against apple/Sources/App/IronStorageApp.swift,
|
||||||
apple/Sources/AutoFill/CredentialProviderViewController.swift, crates/apple/src/lib.rs,
|
apple/Sources/AutoFill/CredentialProviderViewController.swift, crates/apple/src/lib.rs,
|
||||||
and crates/storage/src/mobile*.rs. See [architecture.md](architecture.md) for
|
and crates/storage/src/mobile*.rs. See [architecture.md](architecture.md) for
|
||||||
support, dependency, security and update policy.
|
support, dependency, security and update policy.
|
||||||
|
|
||||||
**Status:** #88 records the architecture baseline. The Android shell (#89),
|
**Status:** implementation issues #89–#110 are delivered. The #111 audit
|
||||||
native packaging (#92), shared contract (#91), and private runtime (#90) are
|
maps each capability to current Android components and executable checks below.
|
||||||
implemented; feature presentation rows remain assigned below. Issue comments
|
[audit.md](audit.md) records current results, dependency decisions and explicit
|
||||||
record per-issue validation and outstanding test gaps. #111 must attach the
|
runtime limits. Test names identify coverage; opt-in tests need separate runtime
|
||||||
final screen/component/test evidence to each row before claiming full parity.
|
evidence and are not counted as exercised by a normal suite.
|
||||||
Every issue linked here belongs to milestone 23.
|
Every implementation issue linked here belongs to milestone 23.
|
||||||
|
|
||||||
## Current iPhone and shared-mobile capabilities
|
## Current iPhone and shared-mobile capabilities
|
||||||
|
|
||||||
| Existing capability / source | Owning Rust API/module | Android presentation / approved API | Implementation and required evidence |
|
| Existing capability / source | Owning Rust API/module | Android presentation / approved API | Implementation and required evidence |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Five destinations: Home, Passwords, Search, TOTP, Preferences; loading/empty/ready/locked/error fixtures (AppContext, ShellViewController) | mobile::MobileShell, MobileTab, MobileShellState; config selected tab | Single Activity, Compose Material 3 Adaptive, Navigation 3, ViewModel/StateFlow | #89/#91/#102; shell fixtures, startup, back/navigation and recreation |
|
| Five destinations: Home, Passwords, Search, TOTP, Preferences; loading/empty/ready/locked/error fixtures (AppContext, ShellViewController) | mobile::MobileShell, MobileTab, MobileShellState; config selected tab | `ShellScreen / ShellViewModel / MainActivity` | #89/#91/#102; shell fixtures, startup, back/navigation and recreation; **Checks:** ShellScreenTest, AdaptiveShellTest, Tier2WindowTest, HingeWindowTest |
|
||||||
| Local-first setup, generated/imported key, passphrase confirmation, staged QR import (SetupChoiceViewController, LocalSetupViewController) | mobile_onboarding::MobileOnboardingOperation, crypto; mobile_key_transfer import | Compose forms and core facade; OS input/camera adapters | #95/#106; real local init, key export/import, canceled/failed setup rollback |
|
| Local-first setup, generated/imported key, passphrase confirmation, staged QR import (SetupChoiceViewController, LocalSetupViewController) | mobile_onboarding::MobileOnboardingOperation, crypto; mobile_key_transfer import | `OnboardingScreen / NativeOnboardingRepository` | #95/#106; real local init, key export/import, canceled/failed setup rollback; **Checks:** OnboardingScreenTest; Rust mobile_onboarding::android::tests |
|
||||||
| Optional HTTPS clone, server/account/token, repository/branch discovery; attach remote to local store (OnboardingViewController) | mobile_onboarding::{MobileOnboardingRequest, MobileOnboardingOperation}; git/config/secret_store | Forms, typed incremental progress and cancel | #95; TLS/auth/branch/cancel/offline errors; repository remote authority |
|
| Optional HTTPS clone, server/account/token, repository/branch discovery; attach remote to local store (OnboardingViewController) | mobile_onboarding::{MobileOnboardingRequest, MobileOnboardingOperation}; git/config/secret_store | `OnboardingScreen and SettingsScreen HTTPS actions` | #95; TLS/auth/branch/cancel/offline errors; repository remote authority; **Checks:** OnboardingScreenTest; Rust onboarding and git TLS/auth/remote-authority tests |
|
||||||
| Home history, ahead/behind, local/remote change status, commit activity (ShellViewController, MobileCommitActivityViewController) | mobile_home::MobileHomePage/Operation and git | Compose Home/list/detail | #97; summaries and stale/error state, Git identity, author/history |
|
| Home history, ahead/behind, local/remote change status, commit activity (ShellViewController, MobileCommitActivityViewController) | mobile_home::MobileHomePage/Operation and git | `HomeScreen / NativeHomeRepository` | #97; summaries and stale/error state, Git identity, author/history; **Checks:** HomeScreenTest; Rust mobile_home and tests/git_embedded.rs |
|
||||||
| Commit, Fetch, Pull, Push and pull-then-push Sync | mobile_home::MobileHomeOperation; git sync/merge/conflicts/signing | Typed actions, progress/cancel, conflict presentation | #97; pull failure prevents push, conflict/data-loss cases and signing |
|
| Commit, Fetch, Pull, Push and pull-then-push Sync | mobile_home::MobileHomeOperation; git sync/merge/conflicts/signing | `HomeScreen typed actions and progress` | #97; pull failure prevents push, conflict/data-loss cases and signing; **Checks:** HomeScreenTest; Rust mobile_home::android_tests and Git conflict/cancellation tests |
|
||||||
| Directory hierarchy, breadcrumbs, folder and entry rows (PasswordDirectoryViewController) | mobile_passwords::MobilePasswordPage/Row/RowKind; repository/read | Adaptive list/detail with stable IDs | #96; deep tree, empty/error, keyboard and size changes |
|
| Directory hierarchy, breadcrumbs, folder and entry rows (PasswordDirectoryViewController) | mobile_passwords::MobilePasswordPage/Row/RowKind; repository/read | `PasswordScreen / NativePasswordRepository` | #96; deep tree, empty/error, keyboard and size changes; **Checks:** PasswordScreenTest, NativeEntryViewerTest, NativeTreeMutationTest |
|
||||||
| Password-store name search (PasswordSearchViewController) | mobile_passwords::MobilePasswordPage::search | Search ViewModel, Flow latest-query-wins | #101; large-store responsiveness and canceled/stale results |
|
| Password-store name search (PasswordSearchViewController) | mobile_passwords::MobilePasswordPage::search | `SearchScreen / NativeSearchRepository` | #101; large-store responsiveness and canceled/stale results; **Checks:** SearchScreenTest; Rust mobile_passwords and large-store native instrumentation |
|
||||||
| Unlock entry, explicit user activity, timeout, manual/background lock; shared entry/TOTP/editor lease (LockedPasswordViewController, AppContext) | mobile_authentication::MobileAuthentication; authentication generation/leases | AndroidX BiometricPrompt/CryptoObject, KeyguardManager, lifecycle | #93/#98/#100; wrong passphrase, cancel, expiry, generation race, changed enrollment and process death |
|
| Unlock entry, explicit user activity, timeout, manual/background lock; shared entry/TOTP/editor lease (LockedPasswordViewController, AppContext) | mobile_authentication::MobileAuthentication; authentication generation/leases | `AuthenticationControls / AndroidKeystoreAdapter / MainActivity lifecycle` | #93/#98/#100; wrong passphrase, cancel, expiry, generation race, changed enrollment and process death; **Checks:** AuthenticationScreenTest, opt-in AuthenticationLeaseTest, AndroidKeystoreTest; tests/authentication_leases.rs |
|
||||||
| Structured fields/sections, masked secrets, explicit reveal/select/copy; OTP field diagnostics (MobileEntryFieldCell) | mobile_entry::MobileEntryPage/Field; mobile_authentication::{entry_page,reveal_entry_field,copy_entry_field} | Compose viewer, sensitive controls and accessibility semantics | #99/#100; no pre-unlock values, relock clears all views, copy expiry/ownership |
|
| Structured fields/sections, masked secrets, explicit reveal/select/copy; OTP field diagnostics (MobileEntryFieldCell) | mobile_entry::MobileEntryPage/Field; mobile_authentication::{entry_page,reveal_entry_field,copy_entry_field} | `EntryViewer / NativePasswordRepository` | #99/#100; no pre-unlock values, relock clears all views, copy expiry/ownership; **Checks:** EntryViewerTest, NativeEntryViewerTest, NativeOtpPresentationTest; tests/entry_documents.rs |
|
||||||
| Create entry/folder, structured multiline editing, add/remove/reorder fields, dirty discard confirmation (MobileEntryEditorViewController) | mobile_entry::MobileEntryDraft; mobile_authentication editor session/create/save/discard; write | Compose editor/forms, stable field IDs | #103; actual encrypted saves, conflict tokens, interrupted saves and dirty navigation |
|
| Create entry/folder, structured multiline editing, add/remove/reorder fields, dirty discard confirmation (MobileEntryEditorViewController) | mobile_entry::MobileEntryDraft; mobile_authentication editor session/create/save/discard; write | `EntryEditorScreen / NativeEditorRepository` | #103; actual encrypted saves, conflict tokens, interrupted saves and dirty navigation; **Checks:** EntryEditorScreenTest, NativeEntryViewerTest; Rust editor conflict/checkpoint/atomic-save tests |
|
||||||
| Password length/generation and replacement | generate; mobile_entry::MobileEntryDraft::generate_password | Generator controls call Rust | #103; Rust validation and unbiased generated value; no Kotlin generator |
|
| Password length/generation and replacement | generate; mobile_entry::MobileEntryDraft::generate_password | `EntryEditorScreen generator controls` | #103; Rust validation and unbiased generated value; no Kotlin generator; **Checks:** EntryEditorScreenTest; tests/password_generation.rs |
|
||||||
| OTP URI input, camera/image import, review before Save (OtpImportViewController; TOTP Add action) | mobile_entry::MobileEntryDraft::add_otp_image; document/otp/presentation | CameraX frames or Android photo picker bytes; Rust decode and field construction | #103/#106/#107; URI/image/scan parity, invalid/oversized input, cancel; no payload parsing in Kotlin |
|
| OTP URI input, camera/image import, review before Save (OtpImportViewController; TOTP Add action) | mobile_entry::MobileEntryDraft::add_otp_image; document/otp/presentation | `EntryEditorScreen / QrCameraPane / AndroidQrCamera` | #103/#106/#107; URI/image/scan parity, invalid/oversized input, cancel; no payload parsing in Kotlin; **Checks:** EntryEditorScreenTest, AndroidQrCameraTest; Rust qr and tests/otp.rs |
|
||||||
| Move/copy/delete entries and folders, destination browse, overwrite and dirty-editor handling (PasswordMutationCoordinator) | mobile_mutation::MobileMutationService/Plan/Request; mutation | Compose dialogs/destination picker | #108; revisions, destination conflicts, recipient reencryption and rollback |
|
| Move/copy/delete entries and folders, destination browse, overwrite and dirty-editor handling (PasswordMutationCoordinator) | mobile_mutation::MobileMutationService/Plan/Request; mutation | `MutationDialog / NativePasswordRepository` | #108; revisions, destination conflicts, recipient reencryption and rollback; **Checks:** MutationWorkflowTest, NativeTreeMutationTest; tests/tree_mutations.rs and recipient_policy.rs |
|
||||||
| TOTP discovery catalog, incremental progress, warm cache, refresh, cancel/resume, cached search (TotpListViewController) | mobile_totp discovery/catalog/operation; mobile_authentication cached/search APIs | Compose list/progress/search; private no-backup catalog | #90/#110; [discovery behavior and evidence](totp-discovery.md), cold/warm/change/add/remove/corrupt/wrong-store/concurrent-change tests |
|
| TOTP discovery catalog, incremental progress, warm cache, refresh, cancel/resume, cached search (TotpListViewController) | mobile_totp discovery/catalog/operation; mobile_authentication cached/search APIs | `TotpScreen / NativeTotpRepository` | #90/#110; [discovery behavior and evidence](totp-discovery.md), cold/warm/change/add/remove/corrupt/wrong-store/concurrent-change tests; **Checks:** TotpDiscoveryFixtureTest, NativeTotpDiscoveryTest, NativeTotpPerformanceTest; tests/mobile_totp.rs |
|
||||||
| TOTP rows, large detail code, countdown/expiry, copying (TotpDetailViewController, TotpCodeView) | mobile_totp::MobileOtpCodePresentation; otp; mobile_authentication::{presented_otp_code,copy_presented_otp_code,advance_presented_hotp} | Compose code/detail; OS clock facts, Rust timing | #107/#100; [OTP behavior and evidence](otp.md), RFC vectors, boundary countdown, stale callback/relock and clipboard |
|
| TOTP rows, large detail code, countdown/expiry, copying (TotpDetailViewController, TotpCodeView) | mobile_totp::MobileOtpCodePresentation; otp; mobile_authentication::{presented_otp_code,copy_presented_otp_code,advance_presented_hotp} | `TotpScreen / OtpCodeView / EntryViewer` | #107/#100; [OTP behavior and evidence](otp.md), RFC vectors, boundary countdown, stale callback/relock and clipboard; **Checks:** TotpScreenTest, NativeOtpPresentationTest; Rust OTP boundary/relock/HOTP tests |
|
||||||
| Repository/server identity, local-only/add-remote Settings, token replace/remove (PreferencesViewController, TokenUpdateViewController) | mobile_authentication::MobilePreferences; mobile_onboarding::replace_configured_application_token; config/secret_store | Compose Settings/facade | #104/#95; no token display; protected replacement/removal and offline operation |
|
| Repository/server identity, local-only/add-remote Settings, token replace/remove (PreferencesViewController, TokenUpdateViewController) | mobile_authentication::MobilePreferences; mobile_onboarding::replace_configured_application_token; config/secret_store | `SettingsScreen / NativeSettingsRepository` | #104/#95; no token display; protected replacement/removal and offline operation; **Checks:** SettingsScreenTest; Rust preference/token and Android onboarding tests |
|
||||||
| Key identity/fingerprint, GPG recovery import and public/private QR export (KeyImportScannerViewController, KeyExportListViewController, KeyQrExportViewController) | mobile_key_transfer::{MobileKeyTransferService,MobileKeyTransferImport}; crypto/presentation | CameraX Preview/ImageAnalysis; animated Rust-provided matrices | #106/#104; multi-frame order/duplicates/interruption, bounded frames, private export authorization and camera denial |
|
| Key identity/fingerprint, GPG recovery import and public/private QR export (KeyImportScannerViewController, KeyExportListViewController, KeyQrExportViewController) | mobile_key_transfer::{MobileKeyTransferService,MobileKeyTransferImport}; crypto/presentation | `SettingsScreen key transfer / QrCameraPane / AndroidQrCamera` | #106/#104; multi-frame order/duplicates/interruption, bounded frames, private export authorization and camera denial; **Checks:** SettingsScreenTest, AndroidQrCameraTest; Rust mobile_key_transfer frame/order/cancel/authorization tests |
|
||||||
| Inactivity timeout, biometric enable/disable, Lock Now, appearance and Git commit name/email | mobile_authentication preference setters, git_identity; config | Compose Settings, BiometricPrompt, system/light/dark theme | #98/#104; persistent settings, validation, Keystore enrollment/recovery and lease revocation |
|
| Inactivity timeout, biometric enable/disable, Lock Now, appearance and Git commit name/email | mobile_authentication preference setters, git_identity; config | `SettingsScreen / AuthenticationControls / ApplicationTheme` | #98/#104; persistent settings, validation, Keystore enrollment/recovery and lease revocation; **Checks:** SettingsScreenTest, NativeEntryViewerTest, opt-in AuthenticationLeaseTest; Rust authenticated preference tests |
|
||||||
| iPhone AutoFill credential selection (CredentialProviderViewController) and shared identity/access domain | desktop/read/document/secret_store today; storage-owned provider contract to add | AndroidX CredentialProviderService (API 34+) and framework AutofillService (API 26+) | #109; typed caller/site identity, non-secret candidates, explicit selection/auth, forged/stale requests, permissions and cross-process lifetime |
|
| AutoFill extension boundary (current iPhone controller is empty), shared identity/access domain and Android provider requirement | mobile_credentials; mobile_authentication provider request/select/fill/save APIs | `IronCredentialProviderService / IronAutofillService / CredentialSelectionActivity` | #109; typed caller/site identity, non-secret candidates, explicit selection/auth, forged/stale requests, permissions and cross-process lifetime; **Checks:** ProviderIntentSecurityTest, CredentialSelectionScreenTest, NativeCredentialProviderTest; provider-client OS journeys |
|
||||||
| Camera permissions, private paths, secret-store mechanics, clipboard, scene/process events, screen privacy | Rust repository/config/secret_store/authentication/presentation; platform callbacks contain facts only | Android framework adapters, FLAG_SECURE, backup rules, ClipboardManager | #90/#93/#98/#100; backup inspection, sandbox paths, pressure/update, screen/clipboard/lifecycle tests |
|
| Camera permissions, private paths, secret-store mechanics, clipboard, scene/process events, screen privacy | Rust repository/config/secret_store/authentication/presentation; platform callbacks contain facts only | `AndroidSecretClipboard / ProtectedInput / ProtectedPresentation / MainActivity` | #90/#93/#98/#100; backup inspection, sandbox paths, pressure/update, screen/clipboard/lifecycle tests; **Checks:** PresentationSecurityTest, BackupPolicyTest, AndroidQrCameraTest; Rust secret-store and editor recovery tests |
|
||||||
| Accessible labels/actions, large text, keyboard, responsive layout and localization | Typed Rust state/actions; UI accessibility is Kotlin | Compose semantics, TalkBack, adaptive layout, resources | #105; measured keyboard/TalkBack/large-font/multi-window/RTL/rotation coverage |
|
| Accessible labels/actions, large text, keyboard, responsive layout and localization | Typed Rust state/actions; UI accessibility is Kotlin | `Compose semantics/resources / HingeSafeLayout / HingeSafeAlertDialog` | #105; measured keyboard/TalkBack/large-font/multi-window/RTL/rotation coverage; **Checks:** Tier2WindowTest, HingeWindowTest; hardening.md records manual accessibility and exact performance evidence |
|
||||||
| Watch pairing/install state in Settings; TOTP Watch selection, snapshots, receipts, transfer and watchOS detail | mobile_watch; shared bridge's explicit `watch` feature enabled only by crates/apple | Absent from Android records and UI (#91) | Deliberate Watch-only exclusion; no unsupported placeholder controls or Wear implementation |
|
| Watch pairing/install state in Settings; TOTP Watch selection, snapshots, receipts, transfer and watchOS detail | mobile_watch; shared bridge's explicit `watch` feature enabled only by crates/apple | Absent from Android records and UI (#91) | Deliberate Watch-only exclusion; no unsupported placeholder controls or Wear implementation |
|
||||||
|
|
||||||
Name search matches current mobile behavior; CLI/TUI decrypted grep is not an
|
Name search matches current mobile behavior; CLI/TUI decrypted grep is not an
|
||||||
@@ -49,8 +49,8 @@ an Android workflow invokes them; platform presentation may not replace them.
|
|||||||
Current iPhone camera adapters pass OS-decoded QR bytes. Android #106 supplies bounded Y-plane/stride/orientation facts through the
|
Current iPhone camera adapters pass OS-decoded QR bytes. Android #106 supplies bounded Y-plane/stride/orientation facts through the
|
||||||
shared native contract; storage decodes and reassembles the transfer. See
|
shared native contract; storage decodes and reassembles the transfer. See
|
||||||
[key-transfer.md](key-transfer.md) for interruption/security behavior and evidence.
|
[key-transfer.md](key-transfer.md) for interruption/security behavior and evidence.
|
||||||
The external-credential matching/grant API required by #109 must be implemented
|
The external-credential matching/grant API is implemented in `mobile_credentials`
|
||||||
in storage, rather than copying frontend selection assumptions from AutoFill.
|
and `mobile_authentication`; both Android providers delegate to it.
|
||||||
|
|
||||||
## Complete milestone issue-to-layer map
|
## Complete milestone issue-to-layer map
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,33 @@ pub fn manifest(manifest: &Path, backup: &Path, extraction: &Path) -> Result<(),
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn release_manifest(path: &Path) -> Result<(), Box<dyn Error>> {
|
||||||
|
release_manifest_text(&fs::read_to_string(path)?)?;
|
||||||
|
println!("Verified release cannot be debugged, profiled or installed as test-only");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn release_manifest_text(source: &str) -> Result<(), Box<dyn Error>> {
|
||||||
|
let document = Document::parse(source)?;
|
||||||
|
let application = document
|
||||||
|
.root_element()
|
||||||
|
.children()
|
||||||
|
.find(|node| node.has_tag_name("application"))
|
||||||
|
.ok_or("application missing")?;
|
||||||
|
for name in ["debuggable", "testOnly"] {
|
||||||
|
if !matches!(application.attribute((ANDROID, name)), None | Some("false")) {
|
||||||
|
return Err(format!("unsafe release application flag: {name}").into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if application
|
||||||
|
.children()
|
||||||
|
.any(|node| node.has_tag_name("profileable"))
|
||||||
|
{
|
||||||
|
return Err("profiling is restricted to benchmark/profile builds".into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn manifest_text(source: &str) -> Result<(), Box<dyn Error>> {
|
fn manifest_text(source: &str) -> Result<(), Box<dyn Error>> {
|
||||||
let document = Document::parse(source)?;
|
let document = Document::parse(source)?;
|
||||||
let root = document.root_element();
|
let root = document.root_element();
|
||||||
@@ -489,6 +516,35 @@ fn android_license<'a>(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
|
fn release_rejects_debug_test_and_profile_access() {
|
||||||
|
let source = format!(r#"<manifest xmlns:android="{ANDROID}"><application/></manifest>"#);
|
||||||
|
assert!(release_manifest_text(&source).is_ok());
|
||||||
|
for flag in ["debuggable", "testOnly"] {
|
||||||
|
assert!(
|
||||||
|
release_manifest_text(&source.replace(
|
||||||
|
"<application/>",
|
||||||
|
&format!(r#"<application android:{flag}="false"/>"#)
|
||||||
|
))
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
release_manifest_text(&source.replace(
|
||||||
|
"<application/>",
|
||||||
|
&format!(r#"<application android:{flag}="true"/>"#)
|
||||||
|
))
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
release_manifest_text(&source.replace(
|
||||||
|
"<application/>",
|
||||||
|
r#"<application><profileable android:shell="true"/></application>"#
|
||||||
|
))
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(release_manifest_text("<manifest/>").is_err());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
fn manifest_rejects_backup_permissions_and_exports() {
|
fn manifest_rejects_backup_permissions_and_exports() {
|
||||||
let source = format!(
|
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><provider android:name="fixture" android:exported="false"/></application></manifest>"#
|
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>"#
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
[_, command, directory] if command == "audit-native" => {
|
[_, command, directory] if command == "audit-native" => {
|
||||||
native::audit_directory(Path::new(directory))?
|
native::audit_directory(Path::new(directory))?
|
||||||
}
|
}
|
||||||
|
[_, command, manifest] if command == "audit-release-manifest" => {
|
||||||
|
gates::release_manifest(Path::new(manifest))?;
|
||||||
|
}
|
||||||
[_, command, manifest, backup, extraction] if command == "audit-manifest" => {
|
[_, command, manifest, backup, extraction] if command == "audit-manifest" => {
|
||||||
gates::manifest(
|
gates::manifest(
|
||||||
Path::new(manifest),
|
Path::new(manifest),
|
||||||
@@ -85,7 +88,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(
|
return Err(
|
||||||
"usage: ironstorage-android-build <fixtures output.kt | check android-dir | prepare-bindings input output | package-native input output | audit-elf path abi | audit-native jniLibs | audit-apk-split apk abi | audit-package apk-or-aab | audit-release apk symbols | audit-manifest manifest.xml backup.xml extraction.xml | sbom android.json rust.txt Cargo.lock policy.json output.json | safe-reports input-dir output.json | check-toolchain cargo rust ndk ndk-version cargo-ndk-version>".into(),
|
"usage: ironstorage-android-build <fixtures output.kt | check android-dir | prepare-bindings input output | package-native input output | audit-elf path abi | audit-native jniLibs | audit-apk-split apk abi | audit-package apk-or-aab | audit-release apk symbols | audit-release-manifest manifest.xml | audit-manifest manifest.xml backup.xml extraction.xml | sbom android.json rust.txt Cargo.lock policy.json output.json | safe-reports input-dir output.json | check-toolchain cargo rust ndk ndk-version cargo-ndk-version>".into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user