Synchronize selected TOTP entries to Apple Watch (#55)
This commit is contained in:
1 parent
affdb55519
commit
a055c93b86
12 files changed
+1888
-98
No files matched your search
@@ -23,6 +23,10 @@ use crate::{
|
||||
mobile_totp::{
|
||||
MobileTotpDetail, MobileTotpError, MobileTotpOperation, MobileTotpPage, MobileTotpService,
|
||||
},
|
||||
mobile_watch::{
|
||||
MobileWatchSnapshotState, MobileWatchSnapshotStatus, WatchSnapshotError,
|
||||
WatchSnapshotSender, WatchSnapshotTransfer,
|
||||
},
|
||||
otp::OtpError,
|
||||
recipient::RecipientPolicyManager,
|
||||
repository::{
|
||||
@@ -139,6 +143,9 @@ pub enum MobileWatchPreferenceState {
|
||||
AppNotInstalled,
|
||||
Ready,
|
||||
Pending,
|
||||
Delivered,
|
||||
Current,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -282,6 +289,7 @@ struct MobileAuthenticationStatus {
|
||||
mutation_active: bool,
|
||||
repository_operation_active: bool,
|
||||
watch_shared_totp_entries: std::collections::BTreeSet<EntryPath>,
|
||||
watch_snapshot: WatchSnapshotSender,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -338,6 +346,8 @@ impl MobileAuthentication {
|
||||
config.authentication_timeout(),
|
||||
)
|
||||
.map_err(MobileAuthenticationError::authentication)?;
|
||||
let watch_snapshot =
|
||||
WatchSnapshotSender::load(config.source().with_file_name("watch-snapshot.toml"));
|
||||
Ok(Self {
|
||||
status: Mutex::new(MobileAuthenticationStatus {
|
||||
biometric_unlock_enabled: config.biometric_unlock_enabled(),
|
||||
@@ -350,6 +360,7 @@ impl MobileAuthentication {
|
||||
mutation_active: false,
|
||||
repository_operation_active: false,
|
||||
watch_shared_totp_entries: config.watch_shared_totp_entries().clone(),
|
||||
watch_snapshot,
|
||||
}),
|
||||
config,
|
||||
repository,
|
||||
@@ -564,8 +575,14 @@ impl MobileAuthentication {
|
||||
.ok_or_else(|| config_detail("The default GPG key is unavailable."))?;
|
||||
let status = self.status()?;
|
||||
let selected = status.watch_shared_totp_entries.len();
|
||||
let (watch_state, watch_title, watch_detail) =
|
||||
watch_preference(watch_supported, watch_paired, watch_app_installed, selected);
|
||||
let snapshot = status.watch_snapshot.status();
|
||||
let (watch_state, watch_title, watch_detail) = watch_preference(
|
||||
watch_supported,
|
||||
watch_paired,
|
||||
watch_app_installed,
|
||||
selected,
|
||||
&snapshot,
|
||||
);
|
||||
let authentication_timeout_seconds = status.authentication_timeout.duration().as_secs();
|
||||
let biometric_unlock_enabled = status.biometric_unlock_enabled;
|
||||
let appearance = status.appearance;
|
||||
@@ -681,7 +698,10 @@ impl MobileAuthentication {
|
||||
&cache_path,
|
||||
)
|
||||
.map_err(totp_error)?;
|
||||
let (totp, cache_notice) = reconciliation.into_parts();
|
||||
let (mut totp, cache_notice) = reconciliation.into_parts();
|
||||
if let Some(detail) = &mut totp {
|
||||
detail.set_watch(self.watch_snapshot_status()?);
|
||||
}
|
||||
Ok(MobileEntryPresentation {
|
||||
page: MobileEntryPage::from_document(&document),
|
||||
totp,
|
||||
@@ -808,9 +828,11 @@ impl MobileAuthentication {
|
||||
};
|
||||
let cache_path = self.config.source().with_file_name("totp-catalog.toml");
|
||||
let mut provider = KeyOnlyProvider::new(handle, &key);
|
||||
MobileTotpService::new(&self.repository, &self.keys)
|
||||
let mut page = MobileTotpService::new(&self.repository, &self.keys)
|
||||
.discover(&shared, &mut provider, &cache_path, operation)
|
||||
.map_err(totp_error)
|
||||
.map_err(totp_error)?;
|
||||
page.set_watch(self.watch_snapshot_status()?);
|
||||
Ok(page)
|
||||
}
|
||||
|
||||
pub fn cached_totp_page(&self) -> Result<Option<MobileTotpPage>, MobileAuthenticationError> {
|
||||
@@ -823,13 +845,15 @@ impl MobileAuthentication {
|
||||
) -> Result<Option<MobileTotpPage>, MobileAuthenticationError> {
|
||||
let shared = self.status()?.watch_shared_totp_entries.clone();
|
||||
let cache_path = self.config.source().with_file_name("totp-catalog.toml");
|
||||
Ok(
|
||||
MobileTotpService::new(&self.repository, &self.keys).search_cached_page(
|
||||
&shared,
|
||||
&cache_path,
|
||||
query,
|
||||
),
|
||||
)
|
||||
let mut page = MobileTotpService::new(&self.repository, &self.keys).search_cached_page(
|
||||
&shared,
|
||||
&cache_path,
|
||||
query,
|
||||
);
|
||||
if let Some(page) = &mut page {
|
||||
page.set_watch(self.watch_snapshot_status()?);
|
||||
}
|
||||
Ok(page)
|
||||
}
|
||||
|
||||
pub fn totp_detail(
|
||||
@@ -848,9 +872,11 @@ impl MobileAuthentication {
|
||||
)
|
||||
};
|
||||
let mut provider = KeyOnlyProvider::new(handle, &key);
|
||||
MobileTotpService::new(&self.repository, &self.keys)
|
||||
let mut detail = MobileTotpService::new(&self.repository, &self.keys)
|
||||
.detail(path, unix_seconds, &shared, &mut provider)
|
||||
.map_err(totp_error)
|
||||
.map_err(totp_error)?;
|
||||
detail.set_watch(self.watch_snapshot_status()?);
|
||||
Ok(detail)
|
||||
}
|
||||
|
||||
pub fn copy_totp_code(
|
||||
@@ -888,6 +914,85 @@ impl MobileAuthentication {
|
||||
self.totp_detail(path, unix_seconds)
|
||||
}
|
||||
|
||||
pub fn watch_snapshot_status(
|
||||
&self,
|
||||
) -> Result<MobileWatchSnapshotStatus, MobileAuthenticationError> {
|
||||
Ok(self.status()?.watch_snapshot.status())
|
||||
}
|
||||
|
||||
pub fn prepare_watch_snapshot(
|
||||
&self,
|
||||
platform_pairing_identity: &str,
|
||||
) -> Result<WatchSnapshotTransfer, MobileAuthenticationError> {
|
||||
self.ensure_active()?;
|
||||
let (handle, key, selected) = {
|
||||
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);
|
||||
let selection = MobileTotpService::new(&self.repository, &self.keys)
|
||||
.watch_snapshot_selection(&selected, &mut provider)
|
||||
.map_err(totp_error)?;
|
||||
let (entries, retained) = selection.into_parts();
|
||||
let mut status = self.status()?;
|
||||
if status.watch_shared_totp_entries != selected {
|
||||
return Err(entry_detail(
|
||||
"Apple Watch Snapshot Changed",
|
||||
"the selected TOTP entries changed while the snapshot was being prepared",
|
||||
));
|
||||
}
|
||||
if retained != selected {
|
||||
drop(status);
|
||||
self.config
|
||||
.update_watch_shared_totp_entries(&retained)
|
||||
.map_err(config_error)?;
|
||||
status = self.status()?;
|
||||
status.watch_shared_totp_entries = retained;
|
||||
}
|
||||
status
|
||||
.watch_snapshot
|
||||
.prepare(platform_pairing_identity, entries)
|
||||
.map_err(watch_error)
|
||||
}
|
||||
|
||||
pub fn set_watch_snapshot_unavailable(
|
||||
&self,
|
||||
unpaired: bool,
|
||||
detail: String,
|
||||
) -> Result<MobileWatchSnapshotStatus, MobileAuthenticationError> {
|
||||
let mut status = self.status()?;
|
||||
status
|
||||
.watch_snapshot
|
||||
.unavailable(unpaired, detail)
|
||||
.map_err(watch_error)?;
|
||||
Ok(status.watch_snapshot.status())
|
||||
}
|
||||
|
||||
pub fn fail_watch_snapshot(
|
||||
&self,
|
||||
revision: u64,
|
||||
detail: String,
|
||||
) -> Result<MobileWatchSnapshotStatus, MobileAuthenticationError> {
|
||||
let mut status = self.status()?;
|
||||
status.watch_snapshot.failed(revision, detail);
|
||||
Ok(status.watch_snapshot.status())
|
||||
}
|
||||
|
||||
pub fn acknowledge_watch_snapshot(
|
||||
&self,
|
||||
receipt: &[u8],
|
||||
) -> Result<MobileWatchSnapshotStatus, MobileAuthenticationError> {
|
||||
self.status()?
|
||||
.watch_snapshot
|
||||
.acknowledge(receipt)
|
||||
.map_err(watch_error)
|
||||
}
|
||||
|
||||
pub fn replace_entry_field(
|
||||
&self,
|
||||
path: &str,
|
||||
@@ -1430,6 +1535,7 @@ fn watch_preference(
|
||||
paired: bool,
|
||||
app_installed: bool,
|
||||
selected: usize,
|
||||
snapshot: &MobileWatchSnapshotStatus,
|
||||
) -> (MobileWatchPreferenceState, String, String) {
|
||||
if !supported {
|
||||
return (
|
||||
@@ -1452,25 +1558,50 @@ fn watch_preference(
|
||||
"Install the IronStorage companion on the paired Apple Watch.".to_owned(),
|
||||
);
|
||||
}
|
||||
if selected == 0 {
|
||||
return (
|
||||
MobileWatchPreferenceState::Ready,
|
||||
"Ready".to_owned(),
|
||||
"No TOTP codes are selected for Apple Watch.".to_owned(),
|
||||
);
|
||||
}
|
||||
(
|
||||
MobileWatchPreferenceState::Pending,
|
||||
"Synchronization Pending".to_owned(),
|
||||
format!(
|
||||
"{selected} selected TOTP {} pending Apple Watch synchronization.",
|
||||
if selected == 1 {
|
||||
"code is"
|
||||
} else {
|
||||
"codes are"
|
||||
}
|
||||
match snapshot.state() {
|
||||
MobileWatchSnapshotState::Pending => (
|
||||
MobileWatchPreferenceState::Pending,
|
||||
"Synchronization Pending".to_owned(),
|
||||
snapshot.detail().to_owned(),
|
||||
),
|
||||
)
|
||||
MobileWatchSnapshotState::Delivered => (
|
||||
MobileWatchPreferenceState::Delivered,
|
||||
"Delivered".to_owned(),
|
||||
snapshot.detail().to_owned(),
|
||||
),
|
||||
MobileWatchSnapshotState::Current => (
|
||||
MobileWatchPreferenceState::Current,
|
||||
"Current".to_owned(),
|
||||
snapshot.detail().to_owned(),
|
||||
),
|
||||
MobileWatchSnapshotState::Failed => (
|
||||
MobileWatchPreferenceState::Failed,
|
||||
"Synchronization Failed".to_owned(),
|
||||
snapshot.detail().to_owned(),
|
||||
),
|
||||
MobileWatchSnapshotState::Unavailable | MobileWatchSnapshotState::Unpaired => {
|
||||
if selected == 0 {
|
||||
(
|
||||
MobileWatchPreferenceState::Ready,
|
||||
"Ready".to_owned(),
|
||||
"No TOTP codes are selected for Apple Watch.".to_owned(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
MobileWatchPreferenceState::Pending,
|
||||
"Synchronization Pending".to_owned(),
|
||||
format!(
|
||||
"{selected} selected TOTP {} pending Apple Watch synchronization.",
|
||||
if selected == 1 {
|
||||
"code is"
|
||||
} else {
|
||||
"codes are"
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn entry_error(error: RepositoryError) -> MobileAuthenticationError {
|
||||
@@ -1565,6 +1696,14 @@ fn totp_error(error: MobileTotpError) -> MobileAuthenticationError {
|
||||
}
|
||||
}
|
||||
|
||||
fn watch_error(error: WatchSnapshotError) -> MobileAuthenticationError {
|
||||
MobileAuthenticationError::new(
|
||||
MobileAuthenticationErrorKind::Entry,
|
||||
"Apple Watch Synchronization Failed",
|
||||
error.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
fn editor_missing() -> MobileAuthenticationError {
|
||||
entry_detail(
|
||||
"Entry Draft Is Unavailable",
|
||||
@@ -1584,7 +1723,8 @@ fn entry_detail(title: &str, error: impl fmt::Display) -> MobileAuthenticationEr
|
||||
mod tests {
|
||||
use super::{
|
||||
MobileRepositoryOperation, MobileRepositoryOperationError, MobileWatchPreferenceState,
|
||||
repository_operation_conflict, watch_preference,
|
||||
MobileWatchSnapshotState, MobileWatchSnapshotStatus, repository_operation_conflict,
|
||||
watch_preference,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -1613,15 +1753,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn watch_preferences_render_platform_and_selection_state() {
|
||||
let unavailable = MobileWatchSnapshotStatus {
|
||||
state: MobileWatchSnapshotState::Unavailable,
|
||||
revision: None,
|
||||
detail: "not started".to_owned(),
|
||||
};
|
||||
assert_eq!(
|
||||
watch_preference(true, false, false, 2).0,
|
||||
watch_preference(true, false, false, 2, &unavailable).0,
|
||||
MobileWatchPreferenceState::NotPaired
|
||||
);
|
||||
assert_eq!(
|
||||
watch_preference(true, true, true, 0).0,
|
||||
watch_preference(true, true, true, 0, &unavailable).0,
|
||||
MobileWatchPreferenceState::Ready
|
||||
);
|
||||
let pending = watch_preference(true, true, true, 2);
|
||||
let pending = watch_preference(true, true, true, 2, &unavailable);
|
||||
assert_eq!(pending.0, MobileWatchPreferenceState::Pending);
|
||||
assert!(pending.2.contains("2 selected TOTP codes"));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user