Implement Apple Watch TOTP interface (#57)
This commit is contained in:
@@ -327,6 +327,7 @@ pub struct WatchTotpRecord {
|
||||
code: SecretBytes,
|
||||
period: u64,
|
||||
valid_until: u64,
|
||||
remaining: u64,
|
||||
}
|
||||
|
||||
impl WatchTotpRecord {
|
||||
@@ -348,6 +349,9 @@ impl WatchTotpRecord {
|
||||
pub fn valid_until(&self) -> u64 {
|
||||
self.valid_until
|
||||
}
|
||||
pub fn remaining(&self) -> u64 {
|
||||
self.remaining
|
||||
}
|
||||
pub fn remaining_at(&self, unix_seconds: u64) -> u64 {
|
||||
self.valid_until.saturating_sub(unix_seconds)
|
||||
}
|
||||
@@ -363,14 +367,58 @@ impl fmt::Debug for WatchTotpRecord {
|
||||
.field("code", &"[REDACTED]")
|
||||
.field("period", &self.period)
|
||||
.field("valid_until", &self.valid_until)
|
||||
.field("remaining", &self.remaining)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum WatchPresentationState {
|
||||
Ready,
|
||||
Empty,
|
||||
Syncing,
|
||||
Stale,
|
||||
Locked,
|
||||
Unavailable,
|
||||
Error,
|
||||
}
|
||||
|
||||
pub struct WatchPresentation {
|
||||
state: WatchPresentationState,
|
||||
title: String,
|
||||
detail: String,
|
||||
records: Vec<WatchTotpRecord>,
|
||||
}
|
||||
|
||||
impl WatchPresentation {
|
||||
pub fn state(&self) -> WatchPresentationState {
|
||||
self.state
|
||||
}
|
||||
pub fn title(&self) -> &str {
|
||||
&self.title
|
||||
}
|
||||
pub fn detail(&self) -> &str {
|
||||
&self.detail
|
||||
}
|
||||
pub fn records(&self) -> &[WatchTotpRecord] {
|
||||
&self.records
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WatchRuntime {
|
||||
receiver: WatchSnapshotReceiver,
|
||||
protected_data_available: bool,
|
||||
presentation_state: WatchPresentationState,
|
||||
}
|
||||
|
||||
impl Default for WatchRuntime {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
receiver: WatchSnapshotReceiver::default(),
|
||||
protected_data_available: true,
|
||||
presentation_state: WatchPresentationState::Syncing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WatchRuntime {
|
||||
@@ -378,8 +426,21 @@ impl WatchRuntime {
|
||||
&mut self,
|
||||
bytes: Vec<u8>,
|
||||
) -> Result<WatchSnapshotUpdate, WatchSnapshotError> {
|
||||
let apply = self.receiver.apply(SecretBytes::new(bytes))?;
|
||||
let apply = match self.receiver.apply(SecretBytes::new(bytes)) {
|
||||
Ok(apply) => apply,
|
||||
Err(error) => {
|
||||
self.presentation_state = WatchPresentationState::Error;
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
self.protected_data_available = true;
|
||||
self.presentation_state = match apply {
|
||||
WatchSnapshotApply::Revoked => WatchPresentationState::Empty,
|
||||
WatchSnapshotApply::Stale => WatchPresentationState::Stale,
|
||||
WatchSnapshotApply::Replaced
|
||||
| WatchSnapshotApply::Duplicate
|
||||
| WatchSnapshotApply::PairingChanged => WatchPresentationState::Ready,
|
||||
};
|
||||
let persistence = match apply {
|
||||
WatchSnapshotApply::Replaced | WatchSnapshotApply::PairingChanged => {
|
||||
WatchPersistenceAction::Replace
|
||||
@@ -434,19 +495,95 @@ impl WatchRuntime {
|
||||
code,
|
||||
period: entry.period,
|
||||
valid_until,
|
||||
remaining: valid_until.saturating_sub(unix_seconds),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn presentation_at(
|
||||
&self,
|
||||
unix_seconds: u64,
|
||||
) -> Result<WatchPresentation, WatchSnapshotError> {
|
||||
let records = if matches!(
|
||||
self.presentation_state,
|
||||
WatchPresentationState::Ready | WatchPresentationState::Stale
|
||||
) {
|
||||
self.records_at(unix_seconds)?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let (title, detail) = match self.presentation_state {
|
||||
WatchPresentationState::Ready => ("TOTP Codes", "Select an entry to view its code."),
|
||||
WatchPresentationState::Empty => (
|
||||
"No TOTP Codes",
|
||||
"Select TOTP entries in IronStorage on your iPhone.",
|
||||
),
|
||||
WatchPresentationState::Syncing => (
|
||||
"Syncing",
|
||||
"Checking for TOTP entries shared by your iPhone.",
|
||||
),
|
||||
WatchPresentationState::Stale => (
|
||||
"Update Delayed",
|
||||
"Showing saved codes; the received update was older.",
|
||||
),
|
||||
WatchPresentationState::Locked => (
|
||||
"Watch Locked",
|
||||
"Unlock your Watch to access shared TOTP codes.",
|
||||
),
|
||||
WatchPresentationState::Unavailable => (
|
||||
"Sync Unavailable",
|
||||
"Open IronStorage on your iPhone to set up Watch sync.",
|
||||
),
|
||||
WatchPresentationState::Error => (
|
||||
"Codes Unavailable",
|
||||
"Open IronStorage on your iPhone and share the entries again.",
|
||||
),
|
||||
};
|
||||
Ok(WatchPresentation {
|
||||
state: self.presentation_state,
|
||||
title: title.to_owned(),
|
||||
detail: detail.to_owned(),
|
||||
records,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sync_started(&mut self) {
|
||||
if self.receiver.current().is_none() && self.protected_data_available {
|
||||
self.presentation_state = WatchPresentationState::Syncing;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sync_finished(&mut self) {
|
||||
if self.receiver.current().is_none()
|
||||
&& self.protected_data_available
|
||||
&& self.presentation_state == WatchPresentationState::Syncing
|
||||
{
|
||||
self.presentation_state = WatchPresentationState::Empty;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sync_unavailable(&mut self) {
|
||||
if self.receiver.current().is_none() && self.protected_data_available {
|
||||
self.presentation_state = WatchPresentationState::Unavailable;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sync_failed(&mut self) {
|
||||
self.receiver.clear_secrets();
|
||||
self.presentation_state = WatchPresentationState::Error;
|
||||
}
|
||||
|
||||
pub fn protected_data_unavailable(&mut self) {
|
||||
self.receiver.clear_secrets();
|
||||
self.protected_data_available = false;
|
||||
self.presentation_state = WatchPresentationState::Locked;
|
||||
}
|
||||
|
||||
pub fn no_persisted_snapshot(&mut self) {
|
||||
self.receiver.revoke();
|
||||
self.protected_data_available = true;
|
||||
self.presentation_state = WatchPresentationState::Empty;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ use std::fs;
|
||||
|
||||
use ironstorage::{
|
||||
mobile_watch::{
|
||||
MobileWatchSnapshotState, WatchPersistenceAction, WatchRuntime, WatchSnapshotApply,
|
||||
WatchSnapshotEntry, WatchSnapshotReceiver, WatchSnapshotSender,
|
||||
MobileWatchSnapshotState, WatchPersistenceAction, WatchPresentationState, WatchRuntime,
|
||||
WatchSnapshotApply, WatchSnapshotEntry, WatchSnapshotReceiver, WatchSnapshotSender,
|
||||
},
|
||||
otp::OtpAlgorithm,
|
||||
repository::{EntryPath, SecretBytes},
|
||||
@@ -184,6 +184,22 @@ fn watch_runtime_generates_view_ready_totp_and_clears_secrets_when_locked() -> T
|
||||
)?;
|
||||
|
||||
let mut runtime = WatchRuntime::default();
|
||||
assert_eq!(
|
||||
runtime.presentation_at(59)?.state(),
|
||||
WatchPresentationState::Syncing
|
||||
);
|
||||
runtime.sync_finished();
|
||||
assert_eq!(
|
||||
runtime.presentation_at(59)?.state(),
|
||||
WatchPresentationState::Empty
|
||||
);
|
||||
runtime.sync_started();
|
||||
runtime.sync_unavailable();
|
||||
assert_eq!(
|
||||
runtime.presentation_at(59)?.state(),
|
||||
WatchPresentationState::Unavailable
|
||||
);
|
||||
runtime.sync_started();
|
||||
let update = runtime.apply_snapshot(snapshot.snapshot().expose().to_vec())?;
|
||||
assert_eq!(update.apply(), WatchSnapshotApply::Replaced);
|
||||
assert_eq!(update.persistence(), WatchPersistenceAction::Replace);
|
||||
@@ -196,9 +212,16 @@ fn watch_runtime_generates_view_ready_totp_and_clears_secrets_when_locked() -> T
|
||||
assert_eq!(records[0].code().expose(), b"94287082");
|
||||
assert_eq!(records[0].valid_until(), 60);
|
||||
assert_eq!(records[0].remaining_at(59), 1);
|
||||
let presentation = runtime.presentation_at(59)?;
|
||||
assert_eq!(presentation.state(), WatchPresentationState::Ready);
|
||||
assert_eq!(presentation.records()[0].remaining(), 1);
|
||||
|
||||
runtime.protected_data_unavailable();
|
||||
assert!(runtime.records_at(59).is_err());
|
||||
assert_eq!(
|
||||
runtime.presentation_at(59)?.state(),
|
||||
WatchPresentationState::Locked
|
||||
);
|
||||
|
||||
let restored = runtime.apply_snapshot(snapshot.snapshot().expose().to_vec())?;
|
||||
assert_eq!(restored.persistence(), WatchPersistenceAction::Replace);
|
||||
@@ -209,5 +232,19 @@ fn watch_runtime_generates_view_ready_totp_and_clears_secrets_when_locked() -> T
|
||||
assert_eq!(revoked.apply(), WatchSnapshotApply::Revoked);
|
||||
assert_eq!(revoked.persistence(), WatchPersistenceAction::Delete);
|
||||
assert!(runtime.records_at(59)?.is_empty());
|
||||
assert_eq!(
|
||||
runtime.presentation_at(59)?.state(),
|
||||
WatchPresentationState::Empty
|
||||
);
|
||||
runtime.apply_snapshot(snapshot.snapshot().expose().to_vec())?;
|
||||
assert_eq!(
|
||||
runtime.presentation_at(59)?.state(),
|
||||
WatchPresentationState::Stale
|
||||
);
|
||||
assert!(runtime.apply_snapshot(vec![0; 64]).is_err());
|
||||
assert_eq!(
|
||||
runtime.presentation_at(59)?.state(),
|
||||
WatchPresentationState::Error
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user