Implement iPhone TOTP tab and Watch sharing

This commit is contained in:
hugo committed 2026-08-11 20:54:34 +02:00
1 parent ae64ce47a3
commit b01cc8bb6d
10 files changed
+1980 -5

No files matched your search

@@ -15,6 +15,7 @@ use crate::{
MobileEntryEditorInput, MobileEntryEditorPage, MobileEntryEditorSession, MobileEntryPage,
MobileEntryValueError, field_value,
},
mobile_totp::{MobileTotpDetail, MobileTotpError, MobileTotpPage, MobileTotpService},
recipient::RecipientPolicyManager,
repository::{
DirectoryPath, EncryptedEntry, EntryPath, Repository, RepositoryError, SecretBytes,
@@ -161,6 +162,7 @@ struct MobileAuthenticationStatus {
active: Option<ActiveMobileLease>,
next_editor_id: u64,
editors: BTreeMap<u64, MobileEntryDraft>,
watch_shared_totp_entries: std::collections::BTreeSet<EntryPath>,
}
/// One process-wide mobile authentication lease shared by every tab and viewer.
@@ -194,6 +196,7 @@ impl MobileAuthentication {
active: None,
next_editor_id: 0,
editors: BTreeMap::new(),
watch_shared_totp_entries: config.watch_shared_totp_entries().clone(),
}),
config,
repository,
@@ -396,6 +399,95 @@ impl MobileAuthentication {
})
}
pub fn unlock_totp(
&self,
passphrase: Option<SecretBytes>,
) -> Result<MobileAuthenticationState, MobileAuthenticationError> {
let path = self
.repository
.snapshot()
.map_err(entry_error)?
.entries()
.next()
.map(|entry| entry.path().clone())
.ok_or_else(|| entry_detail("TOTP Is Unavailable", "the password store is empty"))?;
let ciphertext = self.repository.read_entry(&path).map_err(entry_error)?;
self.unlock_ciphertext(&ciphertext, passphrase)
}
pub fn totp_page(&self) -> Result<MobileTotpPage, MobileAuthenticationError> {
self.ensure_active()?;
let (handle, key, shared) = {
let status = self.status()?;
let active = status.active.as_ref().ok_or_else(locked_error)?;
(
active.handle.clone(),
active.key.clone(),
status.watch_shared_totp_entries.clone(),
)
};
let mut provider = KeyOnlyProvider::new(handle, &key);
MobileTotpService::new(&self.repository, &self.keys)
.page(&shared, &mut provider)
.map_err(totp_error)
}
pub fn totp_detail(
&self,
path: &str,
unix_seconds: u64,
) -> Result<MobileTotpDetail, MobileAuthenticationError> {
self.ensure_active()?;
let (handle, key, shared) = {
let status = self.status()?;
let active = status.active.as_ref().ok_or_else(locked_error)?;
(
active.handle.clone(),
active.key.clone(),
status.watch_shared_totp_entries.clone(),
)
};
let mut provider = KeyOnlyProvider::new(handle, &key);
MobileTotpService::new(&self.repository, &self.keys)
.detail(path, unix_seconds, &shared, &mut provider)
.map_err(totp_error)
}
pub fn copy_totp_code(
&self,
path: &str,
unix_seconds: u64,
) -> Result<MobileEntryCopy, MobileAuthenticationError> {
let detail = self.totp_detail(path, unix_seconds)?;
let value = String::from_utf8(detail.code().expose().to_vec())
.map_err(|_| entry_detail("TOTP Code Is Unavailable", "the code is not UTF-8"))?;
Ok(MobileEntryCopy {
value,
timeout_seconds: self.config.clipboard_timeout().duration().as_secs(),
})
}
pub fn set_totp_watch_shared(
&self,
path: &str,
shared: bool,
unix_seconds: u64,
) -> Result<MobileTotpDetail, MobileAuthenticationError> {
self.totp_detail(path, unix_seconds)?;
let entry = EntryPath::parse(path).map_err(entry_error)?;
let mut selected = self.status()?.watch_shared_totp_entries.clone();
if shared {
selected.insert(entry);
} else {
selected.remove(&entry);
}
self.config
.update_watch_shared_totp_entries(&selected)
.map_err(config_error)?;
self.status()?.watch_shared_totp_entries = selected;
self.totp_detail(path, unix_seconds)
}
pub fn replace_entry_field(
&self,
path: &str,
@@ -786,6 +878,10 @@ fn editor_error(error: MobileEntryEditorError) -> MobileAuthenticationError {
entry_detail("Entry Draft Is Invalid", error)
}
fn totp_error(error: MobileTotpError) -> MobileAuthenticationError {
entry_detail("TOTP Is Unavailable", error)
}
fn editor_missing() -> MobileAuthenticationError {
entry_detail(
"Entry Draft Is Unavailable",