Synchronize selected TOTP entries to Apple Watch (#55)

This commit is contained in:
hugo committed 2026-08-16 13:47:31 +02:00
1 parent affdb55519
commit a055c93b86
12 files changed
+1888 -98

No files matched your search

+56 -22
View File
@@ -21,6 +21,7 @@ use sha2::{Digest as _, Sha256};
use crate::{
crypto::{KeyStore, SecretProvider},
document::EntryDocument,
mobile_watch::{MobileWatchSnapshotState, MobileWatchSnapshotStatus, WatchSnapshotEntry},
otp::{OtpError, OtpKind, OtpService},
repository::{EncryptedEntry, EntryPath, Repository, RepositoryError, SecretBytes},
};
@@ -124,28 +125,6 @@ impl MobileTotpOperation {
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MobileWatchSnapshotState {
Unavailable,
Pending,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MobileWatchSnapshotStatus {
state: MobileWatchSnapshotState,
detail: String,
}
impl MobileWatchSnapshotStatus {
pub fn state(&self) -> MobileWatchSnapshotState {
self.state
}
pub fn detail(&self) -> &str {
&self.detail
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MobileTotpRow {
path: String,
@@ -206,6 +185,10 @@ impl MobileTotpPage {
pub fn watch(&self) -> &MobileWatchSnapshotStatus {
&self.watch
}
pub(crate) fn set_watch(&mut self, watch: MobileWatchSnapshotStatus) {
self.watch = watch;
}
}
pub struct MobileTotpDetail {
@@ -251,6 +234,21 @@ impl MobileTotpDetail {
pub fn watch(&self) -> &MobileWatchSnapshotStatus {
&self.watch
}
pub(crate) fn set_watch(&mut self, watch: MobileWatchSnapshotStatus) {
self.watch = watch;
}
}
pub struct WatchSnapshotSelection {
entries: Vec<WatchSnapshotEntry>,
retained: BTreeSet<EntryPath>,
}
impl WatchSnapshotSelection {
pub fn into_parts(self) -> (Vec<WatchSnapshotEntry>, BTreeSet<EntryPath>) {
(self.entries, self.retained)
}
}
impl fmt::Debug for MobileTotpDetail {
@@ -500,6 +498,40 @@ impl<'a> MobileTotpService<'a> {
}
detail_for_uri(&path, uri, unix_seconds, shared)
}
pub fn watch_snapshot_selection(
&self,
selected: &BTreeSet<EntryPath>,
provider: &mut impl SecretProvider,
) -> Result<WatchSnapshotSelection, MobileTotpError> {
let mut entries = Vec::with_capacity(selected.len());
let mut retained = BTreeSet::new();
for path in selected {
let uri = match OtpService::new(self.repository, self.keys)
.uri(&path.to_string(), provider)
{
Ok(uri) if uri.kind() == OtpKind::Totp => uri,
Ok(_) => continue,
Err(OtpError::Repository(RepositoryError::NotFound { .. }))
| Err(OtpError::MissingUri { .. } | OtpError::AmbiguousUri { .. }) => continue,
Err(OtpError::Crypto(error)) => return Err(OtpError::Crypto(error).into()),
Err(OtpError::Repository(error)) => return Err(error.into()),
Err(_) => continue,
};
let period = uri.period().ok_or(OtpError::NotTotp)?;
retained.insert(path.clone());
entries.push(WatchSnapshotEntry::new(
path.clone(),
uri.issuer().map(str::to_owned),
uri.account().to_owned(),
uri.algorithm(),
uri.digits(),
period,
uri.watch_secret(),
));
}
Ok(WatchSnapshotSelection { entries, retained })
}
}
fn detail_for_uri(
@@ -755,11 +787,13 @@ fn snapshot_status(selected: usize) -> MobileWatchSnapshotStatus {
if selected == 0 {
MobileWatchSnapshotStatus {
state: MobileWatchSnapshotState::Unavailable,
revision: None,
detail: "No TOTP codes are selected for Apple Watch.".to_owned(),
}
} else {
MobileWatchSnapshotStatus {
state: MobileWatchSnapshotState::Pending,
revision: None,
detail: format!(
"{selected} selected TOTP {} pending Apple Watch synchronization.",
if selected == 1 {