Enforce Android lifecycle and presentation security (#100)
This commit is contained in:
1 parent
c0ba6d43d8
commit
a06f39af23
37 files changed
+1129
-110
No files matched your search
@@ -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>,
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -127,6 +127,8 @@ pub enum MobileLifecycleEvent {
|
||||
Foreground,
|
||||
Background,
|
||||
DeviceLocked,
|
||||
MemoryPressure,
|
||||
ProcessRecreated,
|
||||
UserActivity,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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!(
|
||||
|
||||
Reference in new issue
Block a user