Enforce Android lifecycle and presentation security (#100)
Android ARM verification / rust (push) Failing after 47m6s
Android ARM verification / android (push) Failing after 8s
Dependency security audit / rustsec (push) Successful in 23s

This commit is contained in:
hugo committed 2026-10-09 18:59:18 +02:00
1 parent c0ba6d43d8
commit a06f39af23
37 files changed
+1129 -110

No files matched your search

+19
View File
@@ -282,6 +282,8 @@ pub enum MobileLifecycleEvent {
Foreground,
Background,
DeviceLocked,
MemoryPressure,
ProcessRecreated,
UserActivity,
Cancel,
}
@@ -291,6 +293,8 @@ impl From<MobileLifecycleEvent> for storage::MobileLifecycleEvent {
MobileLifecycleEvent::Foreground => Self::Foreground,
MobileLifecycleEvent::Background => Self::Background,
MobileLifecycleEvent::DeviceLocked => Self::DeviceLocked,
MobileLifecycleEvent::MemoryPressure => Self::MemoryPressure,
MobileLifecycleEvent::ProcessRecreated => Self::ProcessRecreated,
MobileLifecycleEvent::UserActivity => Self::UserActivity,
MobileLifecycleEvent::Cancel => Self::Cancel,
}
@@ -404,6 +408,21 @@ pub fn copy_mobile_entry_clearing(
copy_entry(copy, clipboard, true)
}
/// Revoke a copy while the Android window still has foreground clipboard access.
#[uniffi::export]
pub fn clear_mobile_entry_copy(
copy: MobileEntryCopy,
clipboard: Arc<dyn MobileClipboard>,
) -> Result<(), MobilePlatformFfiError> {
presentation::ClipboardManager::new(
ClipboardAdapter(clipboard),
presentation::ClipboardTimeout::default(),
)
.clear_current_copy(&SecretBytes::new(copy.value.into_bytes()))
.map(|_| ())
.map_err(|_| MobilePlatformFfiError::ClipboardFailed)
}
fn copy_entry(
copy: MobileEntryCopy,
clipboard: Arc<dyn MobileClipboard>,
+4 -3
View File
@@ -1609,9 +1609,10 @@ impl MobileAuthentication {
use crate::mobile_platform::MobileLifecycleEvent;
match event {
MobileLifecycleEvent::Foreground => {}
MobileLifecycleEvent::Background | MobileLifecycleEvent::DeviceLocked => {
self.manual_lock()?
}
MobileLifecycleEvent::Background
| MobileLifecycleEvent::DeviceLocked
| MobileLifecycleEvent::MemoryPressure
| MobileLifecycleEvent::ProcessRecreated => self.manual_lock()?,
MobileLifecycleEvent::UserActivity => {
if self.state()?.unlocked() {
self.touch_user_activity()?;
+2
View File
@@ -127,6 +127,8 @@ pub enum MobileLifecycleEvent {
Foreground,
Background,
DeviceLocked,
MemoryPressure,
ProcessRecreated,
UserActivity,
Cancel,
}
+8
View File
@@ -177,6 +177,14 @@ impl<B: ClipboardBackend> ClipboardManager<B> {
Ok(disposition)
}
/// Clear only the current copy, without waiting or retaining previous data.
pub fn clear_current_copy(
&mut self,
copied: &SecretBytes,
) -> Result<ClipboardDisposition, ClipboardError> {
self.cleanup(copied, &ClipboardContent::EmptyOrNonText)
}
fn cleanup(
&mut self,
copied: &SecretBytes,
@@ -85,6 +85,28 @@ fn mobile_context_uses_injected_os_backend_clock_and_lifecycle_without_global_co
authentication.lifecycle(MobileLifecycleEvent::Background)?;
assert!(!authentication.state()?.unlocked());
assert!(authentication.entry_editor(editor.id()).is_err());
for event in [
MobileLifecycleEvent::DeviceLocked,
MobileLifecycleEvent::MemoryPressure,
MobileLifecycleEvent::ProcessRecreated,
] {
authentication.unlock_entry(
"email/personal",
Some(SecretBytes::new(
fixture.key("alice")?.passphrase.as_bytes().to_vec(),
)),
)?;
let editor = authentication.begin_entry_editor("email/personal")?;
clock.advance(Duration::from_secs(5));
assert_eq!(
authentication
.lifecycle(MobileLifecycleEvent::Foreground)?
.remaining_seconds(),
25
);
assert!(!authentication.lifecycle(event)?.unlocked());
assert!(authentication.entry_editor(editor.id()).is_err());
}
let baseline_locks = backend.locks();
drop(authentication);
assert!(
+22
View File
@@ -165,6 +165,28 @@ fn clearing_clipboard_never_reads_or_restores_previous_secrets() -> TestResult {
Ok(())
}
#[test]
fn immediate_copy_revocation_clears_only_matching_content() -> TestResult {
let backend = MemoryClipboard::with_text(b"copied secret");
let mut manager = ClipboardManager::new(backend.clone(), ClipboardTimeout::default());
let secret = SecretBytes::new(b"copied secret".to_vec());
assert_eq!(
manager.clear_current_copy(&secret)?,
ClipboardDisposition::Cleared
);
assert_eq!(backend.text(), None);
backend.set_text(b"new user value");
assert_eq!(
manager.clear_current_copy(&secret)?,
ClipboardDisposition::PreservedNewer
);
assert_eq!(
backend.text().as_deref(),
Some(b"new user value".as_slice())
);
Ok(())
}
#[test]
fn clipboard_validation_and_cleanup_failures_are_typed_and_redacted() -> TestResult {
assert_eq!(