Harden Apple Watch snapshot synchronization
This commit is contained in:
@@ -563,6 +563,8 @@ fileprivate struct FfiConverterData: FfiConverterRustBuffer {
|
|||||||
|
|
||||||
public protocol WatchCoreProtocol: AnyObject, Sendable {
|
public protocol WatchCoreProtocol: AnyObject, Sendable {
|
||||||
|
|
||||||
|
func applyAuthoritativeSnapshot(snapshot: Data) throws -> WatchSnapshotUpdate
|
||||||
|
|
||||||
func applySnapshot(snapshot: Data) throws -> WatchSnapshotUpdate
|
func applySnapshot(snapshot: Data) throws -> WatchSnapshotUpdate
|
||||||
|
|
||||||
func noPersistedSnapshot() throws
|
func noPersistedSnapshot() throws
|
||||||
@@ -633,6 +635,16 @@ open class WatchCore: WatchCoreProtocol, @unchecked Sendable {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
open func applyAuthoritativeSnapshot(snapshot: Data)throws -> WatchSnapshotUpdate {
|
||||||
|
return try FfiConverterTypeWatchSnapshotUpdate_lift(try rustCallWithError(FfiConverterTypeWatchFfiError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_watch_fn_method_watchcore_apply_authoritative_snapshot(
|
||||||
|
self.uniffiCloneHandle(),
|
||||||
|
FfiConverterData.lower(snapshot),uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
open func applySnapshot(snapshot: Data)throws -> WatchSnapshotUpdate {
|
open func applySnapshot(snapshot: Data)throws -> WatchSnapshotUpdate {
|
||||||
return try FfiConverterTypeWatchSnapshotUpdate_lift(try rustCallWithError(FfiConverterTypeWatchFfiError_lift) {
|
return try FfiConverterTypeWatchSnapshotUpdate_lift(try rustCallWithError(FfiConverterTypeWatchFfiError_lift) {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
@@ -1384,6 +1396,9 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_ironstorage_watch_checksum_func_watch_core() != 47212) {
|
if (uniffi_ironstorage_watch_checksum_func_watch_core() != 47212) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_ironstorage_watch_checksum_method_watchcore_apply_authoritative_snapshot() != 29816) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_ironstorage_watch_checksum_method_watchcore_apply_snapshot() != 6162) {
|
if (uniffi_ironstorage_watch_checksum_method_watchcore_apply_snapshot() != 6162) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -253,6 +253,11 @@ uint64_t uniffi_ironstorage_watch_fn_clone_watchcore(uint64_t handle, RustCallSt
|
|||||||
void uniffi_ironstorage_watch_fn_free_watchcore(uint64_t handle, RustCallStatus *_Nonnull out_status
|
void uniffi_ironstorage_watch_fn_free_watchcore(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_APPLY_AUTHORITATIVE_SNAPSHOT
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_APPLY_AUTHORITATIVE_SNAPSHOT
|
||||||
|
RustBuffer uniffi_ironstorage_watch_fn_method_watchcore_apply_authoritative_snapshot(uint64_t ptr, RustBuffer snapshot, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_APPLY_SNAPSHOT
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_APPLY_SNAPSHOT
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_APPLY_SNAPSHOT
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_APPLY_SNAPSHOT
|
||||||
RustBuffer uniffi_ironstorage_watch_fn_method_watchcore_apply_snapshot(uint64_t ptr, RustBuffer snapshot, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_watch_fn_method_watchcore_apply_snapshot(uint64_t ptr, RustBuffer snapshot, RustCallStatus *_Nonnull out_status
|
||||||
@@ -563,6 +568,12 @@ void ffi_ironstorage_watch_rust_future_complete_void(uint64_t handle, RustCallSt
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_FUNC_WATCH_CORE
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_FUNC_WATCH_CORE
|
||||||
uint16_t uniffi_ironstorage_watch_checksum_func_watch_core(void
|
uint16_t uniffi_ironstorage_watch_checksum_func_watch_core(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_APPLY_AUTHORITATIVE_SNAPSHOT
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_APPLY_AUTHORITATIVE_SNAPSHOT
|
||||||
|
uint16_t uniffi_ironstorage_watch_checksum_method_watchcore_apply_authoritative_snapshot(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_APPLY_SNAPSHOT
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_APPLY_SNAPSHOT
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ private final class WatchSnapshotCoordinator: NSObject, WCSessionDelegate {
|
|||||||
Notification.Name.ironStorageWatchSnapshotDidChange,
|
Notification.Name.ironStorageWatchSnapshotDidChange,
|
||||||
.ironStorageLocalStoreDidChange,
|
.ironStorageLocalStoreDidChange,
|
||||||
.ironStorageAuthenticationDidChange,
|
.ironStorageAuthenticationDidChange,
|
||||||
|
UIApplication.didBecomeActiveNotification,
|
||||||
] {
|
] {
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
self,
|
self,
|
||||||
@@ -103,21 +104,7 @@ private final class WatchSnapshotCoordinator: NSObject, WCSessionDelegate {
|
|||||||
Self.snapshotKey: snapshot,
|
Self.snapshotKey: snapshot,
|
||||||
Self.deliveredReceiptKey: Data(transfer.deliveredReceipt),
|
Self.deliveredReceiptKey: Data(transfer.deliveredReceipt),
|
||||||
]
|
]
|
||||||
do {
|
publish(payload, through: session)
|
||||||
try session.updateApplicationContext(payload)
|
|
||||||
if session.isReachable {
|
|
||||||
session.sendMessage(payload, replyHandler: nil) { _ in
|
|
||||||
session.transferUserInfo(payload)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
session.transferUserInfo(payload)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
_ = try? authentication.failWatchSnapshot(
|
|
||||||
revision: transfer.revision,
|
|
||||||
detail: "The latest Apple Watch snapshot could not be queued."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
notifyStatusChanged()
|
notifyStatusChanged()
|
||||||
case .failure:
|
case .failure:
|
||||||
// A locked GPG key leaves the previous replacement pending. Authentication
|
// A locked GPG key leaves the previous replacement pending. Authentication
|
||||||
@@ -127,6 +114,18 @@ private final class WatchSnapshotCoordinator: NSObject, WCSessionDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func publish(_ payload: [String: Any], through session: WCSession) {
|
||||||
|
for transfer in session.outstandingUserInfoTransfers
|
||||||
|
where transfer.userInfo[Self.snapshotKey] != nil {
|
||||||
|
transfer.cancel()
|
||||||
|
}
|
||||||
|
try? session.updateApplicationContext(payload)
|
||||||
|
session.transferUserInfo(payload)
|
||||||
|
if session.isReachable {
|
||||||
|
session.sendMessage(payload, replyHandler: nil) { _ in }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func recordUnavailable(unpaired: Bool, detail: String) {
|
private func recordUnavailable(unpaired: Bool, detail: String) {
|
||||||
guard let authentication else { return }
|
guard let authentication else { return }
|
||||||
_ = try? authentication.setWatchSnapshotUnavailable(unpaired: unpaired, detail: detail)
|
_ = try? authentication.setWatchSnapshotUnavailable(unpaired: unpaired, detail: detail)
|
||||||
|
|||||||
@@ -85,6 +85,12 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
|
|||||||
|
|
||||||
func sceneBecameActive() {
|
func sceneBecameActive() {
|
||||||
restoreSnapshot()
|
restoreSnapshot()
|
||||||
|
guard WCSession.isSupported() else { return }
|
||||||
|
let session = WCSession.default
|
||||||
|
session.activate()
|
||||||
|
if session.activationState == .activated {
|
||||||
|
receive(session.receivedApplicationContext, session: session, authoritative: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sceneBecameInactive() {
|
func sceneBecameInactive() {
|
||||||
@@ -108,7 +114,11 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func receive(_ applicationContext: [String: Any], session: WCSession) {
|
private func receive(
|
||||||
|
_ applicationContext: [String: Any],
|
||||||
|
session: WCSession,
|
||||||
|
authoritative: Bool = false
|
||||||
|
) {
|
||||||
try? core.syncStarted()
|
try? core.syncStarted()
|
||||||
guard var snapshot = applicationContext[Self.snapshotKey] as? Data else {
|
guard var snapshot = applicationContext[Self.snapshotKey] as? Data else {
|
||||||
try? core.syncFinished()
|
try? core.syncFinished()
|
||||||
@@ -117,7 +127,11 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
|
|||||||
}
|
}
|
||||||
defer { snapshot.resetBytes(in: 0..<snapshot.count) }
|
defer { snapshot.resetBytes(in: 0..<snapshot.count) }
|
||||||
do {
|
do {
|
||||||
let update = try core.applySnapshot(snapshot: snapshot)
|
let update = if authoritative {
|
||||||
|
try core.applyAuthoritativeSnapshot(snapshot: snapshot)
|
||||||
|
} else {
|
||||||
|
try core.applySnapshot(snapshot: snapshot)
|
||||||
|
}
|
||||||
switch update.persistence {
|
switch update.persistence {
|
||||||
case .keep:
|
case .keep:
|
||||||
break
|
break
|
||||||
@@ -128,20 +142,25 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
|
|||||||
}
|
}
|
||||||
refreshPresentation()
|
refreshPresentation()
|
||||||
guard !update.receipt.isEmpty else { return }
|
guard !update.receipt.isEmpty else { return }
|
||||||
let acknowledgement = [Self.receiptKey: update.receipt]
|
acknowledge(update.receipt, through: session)
|
||||||
if session.isReachable {
|
|
||||||
session.sendMessage(acknowledgement, replyHandler: nil) { _ in
|
|
||||||
session.transferUserInfo(acknowledgement)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
session.transferUserInfo(acknowledgement)
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
try? core.syncFailed()
|
try? core.syncFailed()
|
||||||
refreshPresentation()
|
refreshPresentation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func acknowledge(_ receipt: Data, through session: WCSession) {
|
||||||
|
let acknowledgement = [Self.receiptKey: receipt]
|
||||||
|
for transfer in session.outstandingUserInfoTransfers
|
||||||
|
where transfer.userInfo[Self.receiptKey] != nil {
|
||||||
|
transfer.cancel()
|
||||||
|
}
|
||||||
|
session.transferUserInfo(acknowledgement)
|
||||||
|
if session.isReachable {
|
||||||
|
session.sendMessage(acknowledgement, replyHandler: nil) { _ in }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func refreshPresentation(at date: Date = .now) {
|
func refreshPresentation(at date: Date = .now) {
|
||||||
presentation = try? core.presentationAt(
|
presentation = try? core.presentationAt(
|
||||||
unixSeconds: UInt64(date.timeIntervalSince1970)
|
unixSeconds: UInt64(date.timeIntervalSince1970)
|
||||||
@@ -160,7 +179,11 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
|
|||||||
self.refreshPresentation()
|
self.refreshPresentation()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.receive(session.receivedApplicationContext, session: session)
|
self.receive(
|
||||||
|
session.receivedApplicationContext,
|
||||||
|
session: session,
|
||||||
|
authoritative: true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +192,7 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
|
|||||||
didReceiveApplicationContext applicationContext: [String: Any]
|
didReceiveApplicationContext applicationContext: [String: Any]
|
||||||
) {
|
) {
|
||||||
Task { @MainActor [weak self] in
|
Task { @MainActor [weak self] in
|
||||||
self?.receive(applicationContext, session: session)
|
self?.receive(applicationContext, session: session, authoritative: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ use data_encoding::HEXLOWER;
|
|||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
use data_encoding::HEXLOWER_PERMISSIVE;
|
use data_encoding::HEXLOWER_PERMISSIVE;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
|
use rand::{RngCore as _, rngs::OsRng};
|
||||||
|
#[cfg(feature = "full")]
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::{Digest as _, Sha256};
|
use sha2::{Digest as _, Sha256};
|
||||||
|
|
||||||
@@ -221,30 +223,17 @@ struct AcceptedSnapshot {
|
|||||||
|
|
||||||
impl WatchSnapshotReceiver {
|
impl WatchSnapshotReceiver {
|
||||||
pub fn apply(&mut self, bytes: SecretBytes) -> Result<WatchSnapshotApply, WatchSnapshotError> {
|
pub fn apply(&mut self, bytes: SecretBytes) -> Result<WatchSnapshotApply, WatchSnapshotError> {
|
||||||
let snapshot = match decode_snapshot(bytes.expose()) {
|
let snapshot = decode_snapshot(bytes.expose())?;
|
||||||
Ok(snapshot) => snapshot,
|
if let Some(accepted) = self.accepted {
|
||||||
Err(error) => {
|
if snapshot.pairing != accepted.pairing || snapshot.revision < accepted.revision {
|
||||||
self.current = None;
|
|
||||||
return Err(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let mut pairing_changed = false;
|
|
||||||
// A valid revocation can only remove secrets, so it must survive a sender reset.
|
|
||||||
if !snapshot.is_revocation()
|
|
||||||
&& let Some(accepted) = self.accepted
|
|
||||||
{
|
|
||||||
if snapshot.revision < accepted.revision {
|
|
||||||
return Ok(WatchSnapshotApply::Stale);
|
return Ok(WatchSnapshotApply::Stale);
|
||||||
} else if snapshot.revision == accepted.revision {
|
} else if snapshot.revision == accepted.revision {
|
||||||
if snapshot.pairing != accepted.pairing || snapshot.digest != accepted.digest {
|
if snapshot.digest != accepted.digest {
|
||||||
self.current = None;
|
|
||||||
return Err(WatchSnapshotError::RevisionConflict);
|
return Err(WatchSnapshotError::RevisionConflict);
|
||||||
}
|
}
|
||||||
if self.current.is_some() {
|
if self.current.is_some() {
|
||||||
return Ok(WatchSnapshotApply::Duplicate);
|
return Ok(WatchSnapshotApply::Duplicate);
|
||||||
}
|
}
|
||||||
} else if accepted.pairing != snapshot.pairing {
|
|
||||||
pairing_changed = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let accepted = AcceptedSnapshot {
|
let accepted = AcceptedSnapshot {
|
||||||
@@ -252,6 +241,37 @@ impl WatchSnapshotReceiver {
|
|||||||
revision: snapshot.revision,
|
revision: snapshot.revision,
|
||||||
digest: snapshot.digest,
|
digest: snapshot.digest,
|
||||||
};
|
};
|
||||||
|
let result = if snapshot.is_revocation() {
|
||||||
|
WatchSnapshotApply::Revoked
|
||||||
|
} else {
|
||||||
|
WatchSnapshotApply::Replaced
|
||||||
|
};
|
||||||
|
self.accepted = Some(accepted);
|
||||||
|
self.current = Some(snapshot);
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn replace_authoritative(
|
||||||
|
&mut self,
|
||||||
|
bytes: SecretBytes,
|
||||||
|
) -> Result<WatchSnapshotApply, WatchSnapshotError> {
|
||||||
|
let snapshot = decode_snapshot(bytes.expose())?;
|
||||||
|
let pairing_changed = self
|
||||||
|
.accepted
|
||||||
|
.is_some_and(|accepted| accepted.pairing != snapshot.pairing);
|
||||||
|
if let Some(accepted) = self.accepted
|
||||||
|
&& !pairing_changed
|
||||||
|
{
|
||||||
|
if snapshot.revision < accepted.revision {
|
||||||
|
return Ok(WatchSnapshotApply::Stale);
|
||||||
|
}
|
||||||
|
if accepted.revision == snapshot.revision
|
||||||
|
&& accepted.digest == snapshot.digest
|
||||||
|
&& self.current.is_some()
|
||||||
|
{
|
||||||
|
return Ok(WatchSnapshotApply::Duplicate);
|
||||||
|
}
|
||||||
|
}
|
||||||
let result = if snapshot.is_revocation() {
|
let result = if snapshot.is_revocation() {
|
||||||
WatchSnapshotApply::Revoked
|
WatchSnapshotApply::Revoked
|
||||||
} else if pairing_changed {
|
} else if pairing_changed {
|
||||||
@@ -259,7 +279,11 @@ impl WatchSnapshotReceiver {
|
|||||||
} else {
|
} else {
|
||||||
WatchSnapshotApply::Replaced
|
WatchSnapshotApply::Replaced
|
||||||
};
|
};
|
||||||
self.accepted = Some(accepted);
|
self.accepted = Some(AcceptedSnapshot {
|
||||||
|
pairing: snapshot.pairing,
|
||||||
|
revision: snapshot.revision,
|
||||||
|
digest: snapshot.digest,
|
||||||
|
});
|
||||||
self.current = Some(snapshot);
|
self.current = Some(snapshot);
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
@@ -432,14 +456,36 @@ impl WatchRuntime {
|
|||||||
let apply = match self.receiver.apply(SecretBytes::new(bytes)) {
|
let apply = match self.receiver.apply(SecretBytes::new(bytes)) {
|
||||||
Ok(apply) => apply,
|
Ok(apply) => apply,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
self.presentation_state = WatchPresentationState::Error;
|
self.sync_failed();
|
||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Ok(self.finish_apply(apply))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_authoritative_snapshot(
|
||||||
|
&mut self,
|
||||||
|
bytes: Vec<u8>,
|
||||||
|
) -> Result<WatchSnapshotUpdate, WatchSnapshotError> {
|
||||||
|
let apply = match self.receiver.replace_authoritative(SecretBytes::new(bytes)) {
|
||||||
|
Ok(apply) => apply,
|
||||||
|
Err(error) => {
|
||||||
|
self.sync_failed();
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(self.finish_apply(apply))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_apply(&mut self, apply: WatchSnapshotApply) -> WatchSnapshotUpdate {
|
||||||
self.protected_data_available = true;
|
self.protected_data_available = true;
|
||||||
self.presentation_state = match apply {
|
self.presentation_state = match apply {
|
||||||
WatchSnapshotApply::Revoked => WatchPresentationState::Empty,
|
WatchSnapshotApply::Revoked => WatchPresentationState::Empty,
|
||||||
WatchSnapshotApply::Stale => WatchPresentationState::Stale,
|
WatchSnapshotApply::Stale => match self.receiver.current() {
|
||||||
|
Some(snapshot) if snapshot.is_revocation() => WatchPresentationState::Empty,
|
||||||
|
Some(_) => WatchPresentationState::Ready,
|
||||||
|
None => WatchPresentationState::Stale,
|
||||||
|
},
|
||||||
WatchSnapshotApply::Replaced
|
WatchSnapshotApply::Replaced
|
||||||
| WatchSnapshotApply::Duplicate
|
| WatchSnapshotApply::Duplicate
|
||||||
| WatchSnapshotApply::PairingChanged => WatchPresentationState::Ready,
|
| WatchSnapshotApply::PairingChanged => WatchPresentationState::Ready,
|
||||||
@@ -454,7 +500,7 @@ impl WatchRuntime {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let current = self.receiver.current();
|
let current = self.receiver.current();
|
||||||
Ok(WatchSnapshotUpdate {
|
WatchSnapshotUpdate {
|
||||||
apply,
|
apply,
|
||||||
persistence,
|
persistence,
|
||||||
revision: current.map(WatchSnapshot::revision),
|
revision: current.map(WatchSnapshot::revision),
|
||||||
@@ -462,7 +508,7 @@ impl WatchRuntime {
|
|||||||
.map(|snapshot| u32::try_from(snapshot.entries().len()).unwrap_or(u32::MAX))
|
.map(|snapshot| u32::try_from(snapshot.entries().len()).unwrap_or(u32::MAX))
|
||||||
.unwrap_or(0),
|
.unwrap_or(0),
|
||||||
receipt: self.receiver.current_receipt().unwrap_or_default(),
|
receipt: self.receiver.current_receipt().unwrap_or_default(),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn records_at(
|
pub fn records_at(
|
||||||
@@ -573,8 +619,11 @@ impl WatchRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn sync_failed(&mut self) {
|
pub fn sync_failed(&mut self) {
|
||||||
self.receiver.clear_secrets();
|
self.presentation_state = if self.receiver.current().is_some() {
|
||||||
self.presentation_state = WatchPresentationState::Error;
|
WatchPresentationState::Stale
|
||||||
|
} else {
|
||||||
|
WatchPresentationState::Error
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn protected_data_unavailable(&mut self) {
|
pub fn protected_data_unavailable(&mut self) {
|
||||||
@@ -620,6 +669,8 @@ impl WatchSnapshotSender {
|
|||||||
) -> Result<(), WatchSnapshotError> {
|
) -> Result<(), WatchSnapshotError> {
|
||||||
if unpaired {
|
if unpaired {
|
||||||
self.journal.pairing = None;
|
self.journal.pairing = None;
|
||||||
|
self.journal.generation = None;
|
||||||
|
self.journal.revision = 0;
|
||||||
self.journal.digest = None;
|
self.journal.digest = None;
|
||||||
self.journal.snapshot_digest = None;
|
self.journal.snapshot_digest = None;
|
||||||
self.journal.delivered_revision = None;
|
self.journal.delivered_revision = None;
|
||||||
@@ -646,29 +697,44 @@ impl WatchSnapshotSender {
|
|||||||
if platform_pairing_identity.trim().is_empty() {
|
if platform_pairing_identity.trim().is_empty() {
|
||||||
return Err(WatchSnapshotError::InvalidPairing);
|
return Err(WatchSnapshotError::InvalidPairing);
|
||||||
}
|
}
|
||||||
let pairing = digest(platform_pairing_identity.as_bytes());
|
let platform_pairing = digest(platform_pairing_identity.as_bytes());
|
||||||
let content = encode_entries(&entries)?;
|
let content = encode_entries(&entries)?;
|
||||||
let content_digest = digest(&content);
|
let content_digest = digest(&content);
|
||||||
let pairing_text = HEXLOWER.encode(&pairing);
|
let pairing_text = HEXLOWER.encode(&platform_pairing);
|
||||||
let digest_text = HEXLOWER.encode(&content_digest);
|
let digest_text = HEXLOWER.encode(&content_digest);
|
||||||
if self.journal.pairing.as_deref() != Some(&pairing_text)
|
if self.journal.pairing.as_deref() != Some(&pairing_text)
|
||||||
|| self.journal.digest.as_deref() != Some(&digest_text)
|
|| self.journal.generation.is_none()
|
||||||
{
|
{
|
||||||
|
let mut generation = [0_u8; 32];
|
||||||
|
OsRng.fill_bytes(&mut generation);
|
||||||
|
self.journal.pairing = Some(pairing_text);
|
||||||
|
self.journal.generation = Some(HEXLOWER.encode(&generation));
|
||||||
|
self.journal.revision = 0;
|
||||||
|
self.journal.digest = None;
|
||||||
|
self.journal.snapshot_digest = None;
|
||||||
|
self.journal.delivered_revision = None;
|
||||||
|
self.journal.current_revision = None;
|
||||||
|
}
|
||||||
|
if self.journal.digest.as_deref() != Some(&digest_text) {
|
||||||
self.journal.revision = self
|
self.journal.revision = self
|
||||||
.journal
|
.journal
|
||||||
.revision
|
.revision
|
||||||
.checked_add(1)
|
.checked_add(1)
|
||||||
.ok_or(WatchSnapshotError::RevisionOverflow)?;
|
.ok_or(WatchSnapshotError::RevisionOverflow)?;
|
||||||
self.journal.pairing = Some(pairing_text);
|
|
||||||
self.journal.digest = Some(digest_text);
|
self.journal.digest = Some(digest_text);
|
||||||
self.journal.delivered_revision = None;
|
self.journal.delivered_revision = None;
|
||||||
self.journal.current_revision = None;
|
self.journal.current_revision = None;
|
||||||
save_journal(&self.path, &self.journal)?;
|
|
||||||
}
|
}
|
||||||
if self.journal.revision == 0 {
|
if self.journal.revision == 0 {
|
||||||
self.journal.revision = 1;
|
self.journal.revision = 1;
|
||||||
save_journal(&self.path, &self.journal)?;
|
|
||||||
}
|
}
|
||||||
|
let generation = decode_hash(
|
||||||
|
self.journal
|
||||||
|
.generation
|
||||||
|
.as_deref()
|
||||||
|
.ok_or(WatchSnapshotError::InvalidJournal)?,
|
||||||
|
)?;
|
||||||
|
let pairing = snapshot_pairing(platform_pairing, generation);
|
||||||
let snapshot = encode_snapshot(pairing, self.journal.revision, &content)?;
|
let snapshot = encode_snapshot(pairing, self.journal.revision, &content)?;
|
||||||
let snapshot_digest = digest(snapshot.expose());
|
let snapshot_digest = digest(snapshot.expose());
|
||||||
self.journal.snapshot_digest = Some(HEXLOWER.encode(&snapshot_digest));
|
self.journal.snapshot_digest = Some(HEXLOWER.encode(&snapshot_digest));
|
||||||
@@ -709,12 +775,19 @@ impl WatchSnapshotSender {
|
|||||||
receipt: &[u8],
|
receipt: &[u8],
|
||||||
) -> Result<MobileWatchSnapshotStatus, WatchSnapshotError> {
|
) -> Result<MobileWatchSnapshotStatus, WatchSnapshotError> {
|
||||||
let receipt = decode_receipt(receipt)?;
|
let receipt = decode_receipt(receipt)?;
|
||||||
let pairing = decode_hash(
|
let platform_pairing = decode_hash(
|
||||||
self.journal
|
self.journal
|
||||||
.pairing
|
.pairing
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.ok_or(WatchSnapshotError::NoPendingSnapshot)?,
|
.ok_or(WatchSnapshotError::NoPendingSnapshot)?,
|
||||||
)?;
|
)?;
|
||||||
|
let generation = decode_hash(
|
||||||
|
self.journal
|
||||||
|
.generation
|
||||||
|
.as_deref()
|
||||||
|
.ok_or(WatchSnapshotError::NoPendingSnapshot)?,
|
||||||
|
)?;
|
||||||
|
let pairing = snapshot_pairing(platform_pairing, generation);
|
||||||
if receipt.pairing != pairing || receipt.revision != self.journal.revision {
|
if receipt.pairing != pairing || receipt.revision != self.journal.revision {
|
||||||
return Ok(self.status());
|
return Ok(self.status());
|
||||||
}
|
}
|
||||||
@@ -762,6 +835,8 @@ struct SenderJournal {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pairing: Option<String>,
|
pairing: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
generation: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
revision: u64,
|
revision: u64,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
digest: Option<String>,
|
digest: Option<String>,
|
||||||
@@ -779,6 +854,7 @@ impl Default for SenderJournal {
|
|||||||
Self {
|
Self {
|
||||||
version: JOURNAL_VERSION,
|
version: JOURNAL_VERSION,
|
||||||
pairing: None,
|
pairing: None,
|
||||||
|
generation: None,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
digest: None,
|
digest: None,
|
||||||
snapshot_digest: None,
|
snapshot_digest: None,
|
||||||
@@ -1085,6 +1161,15 @@ fn set_private_permissions(_temporary: &TempFile<'_>) -> Result<(), WatchSnapsho
|
|||||||
fn digest(bytes: &[u8]) -> [u8; 32] {
|
fn digest(bytes: &[u8]) -> [u8; 32] {
|
||||||
Sha256::digest(bytes).into()
|
Sha256::digest(bytes).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "full")]
|
||||||
|
fn snapshot_pairing(platform_pairing: [u8; 32], generation: [u8; 32]) -> [u8; 32] {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(b"IronStorage Watch snapshot generation\0");
|
||||||
|
hasher.update(platform_pairing);
|
||||||
|
hasher.update(generation);
|
||||||
|
hasher.finalize().into()
|
||||||
|
}
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
fn decode_hash(text: &str) -> Result<[u8; 32], WatchSnapshotError> {
|
fn decode_hash(text: &str) -> Result<[u8; 32], WatchSnapshotError> {
|
||||||
let bytes = HEXLOWER_PERMISSIVE
|
let bytes = HEXLOWER_PERMISSIVE
|
||||||
|
|||||||
@@ -62,8 +62,28 @@ fn apple_sources_preserve_the_mobile_security_boundary() {
|
|||||||
assert!(watch.contains("SecItemCopyMatching"));
|
assert!(watch.contains("SecItemCopyMatching"));
|
||||||
assert!(app.contains("session.sendMessage(payload"));
|
assert!(app.contains("session.sendMessage(payload"));
|
||||||
assert!(app.contains("session.transferUserInfo(payload)"));
|
assert!(app.contains("session.transferUserInfo(payload)"));
|
||||||
|
assert!(app.contains("session.outstandingUserInfoTransfers"));
|
||||||
|
assert!(
|
||||||
|
app.split_once("@main")
|
||||||
|
.expect("iPhone app delegate")
|
||||||
|
.0
|
||||||
|
.contains("UIApplication.didBecomeActiveNotification")
|
||||||
|
);
|
||||||
assert!(watch.contains("didReceiveMessage message"));
|
assert!(watch.contains("didReceiveMessage message"));
|
||||||
assert!(watch.contains("didReceiveUserInfo userInfo"));
|
assert!(watch.contains("didReceiveUserInfo userInfo"));
|
||||||
|
assert!(watch.contains("session.outstandingUserInfoTransfers"));
|
||||||
|
assert!(watch.contains("session.transferUserInfo(acknowledgement)"));
|
||||||
|
assert!(
|
||||||
|
watch
|
||||||
|
.split_once("func sceneBecameActive()")
|
||||||
|
.expect("Watch foreground handler")
|
||||||
|
.1
|
||||||
|
.split_once("func sceneBecameInactive()")
|
||||||
|
.expect("Watch inactive handler")
|
||||||
|
.0
|
||||||
|
.contains("receivedApplicationContext")
|
||||||
|
);
|
||||||
|
assert!(watch.contains("applyAuthoritativeSnapshot"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -112,6 +112,12 @@ fn replacement_snapshots_reject_replays_conflicts_and_pairing_changes() -> TestR
|
|||||||
receiver.apply(SecretBytes::new(
|
receiver.apply(SecretBytes::new(
|
||||||
reset_revocation.snapshot().expose().to_vec()
|
reset_revocation.snapshot().expose().to_vec()
|
||||||
))?,
|
))?,
|
||||||
|
WatchSnapshotApply::Stale
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
receiver.replace_authoritative(SecretBytes::new(
|
||||||
|
reset_revocation.snapshot().expose().to_vec()
|
||||||
|
))?,
|
||||||
WatchSnapshotApply::Revoked
|
WatchSnapshotApply::Revoked
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -134,12 +140,17 @@ fn replacement_snapshots_reject_replays_conflicts_and_pairing_changes() -> TestR
|
|||||||
"paired-watch-b",
|
"paired-watch-b",
|
||||||
vec![entry("otp/carol", "Acme", "carol", b"third-secret")],
|
vec![entry("otp/carol", "Acme", "carol", b"third-secret")],
|
||||||
)?;
|
)?;
|
||||||
assert_eq!(changed_watch.revision(), 4);
|
assert_eq!(changed_watch.revision(), 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
receiver.apply(SecretBytes::new(changed_watch.snapshot().expose().to_vec()))?,
|
receiver.apply(SecretBytes::new(changed_watch.snapshot().expose().to_vec()))?,
|
||||||
|
WatchSnapshotApply::Stale
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
receiver
|
||||||
|
.replace_authoritative(SecretBytes::new(changed_watch.snapshot().expose().to_vec()))?,
|
||||||
WatchSnapshotApply::PairingChanged
|
WatchSnapshotApply::PairingChanged
|
||||||
);
|
);
|
||||||
assert_eq!(receiver.current().expect("new pairing").revision(), 4);
|
assert_eq!(receiver.current().expect("new pairing").revision(), 1);
|
||||||
let mut fresh_watch = WatchSnapshotReceiver::default();
|
let mut fresh_watch = WatchSnapshotReceiver::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fresh_watch.apply(SecretBytes::new(changed_watch.snapshot().expose().to_vec()))?,
|
fresh_watch.apply(SecretBytes::new(changed_watch.snapshot().expose().to_vec()))?,
|
||||||
@@ -149,14 +160,21 @@ fn replacement_snapshots_reject_replays_conflicts_and_pairing_changes() -> TestR
|
|||||||
let mut damaged = changed_watch.snapshot().expose().to_vec();
|
let mut damaged = changed_watch.snapshot().expose().to_vec();
|
||||||
damaged[20] ^= 0x55;
|
damaged[20] ^= 0x55;
|
||||||
assert!(fresh_watch.apply(SecretBytes::new(damaged)).is_err());
|
assert!(fresh_watch.apply(SecretBytes::new(damaged)).is_err());
|
||||||
assert!(fresh_watch.current().is_none());
|
assert_eq!(
|
||||||
|
fresh_watch
|
||||||
|
.current()
|
||||||
|
.expect("last valid snapshot")
|
||||||
|
.entries()[0]
|
||||||
|
.account(),
|
||||||
|
"carol"
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fresh_watch.apply(SecretBytes::new(first.snapshot().expose().to_vec()))?,
|
fresh_watch.apply(SecretBytes::new(first.snapshot().expose().to_vec()))?,
|
||||||
WatchSnapshotApply::Stale
|
WatchSnapshotApply::Stale
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fresh_watch.apply(SecretBytes::new(changed_watch.snapshot().expose().to_vec()))?,
|
fresh_watch.apply(SecretBytes::new(changed_watch.snapshot().expose().to_vec()))?,
|
||||||
WatchSnapshotApply::Replaced
|
WatchSnapshotApply::Duplicate
|
||||||
);
|
);
|
||||||
|
|
||||||
let persisted = fs::read_to_string(journal)?;
|
let persisted = fs::read_to_string(journal)?;
|
||||||
@@ -193,6 +211,131 @@ fn journal_keeps_revisions_monotonic_across_sender_reloads() -> TestResult {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn journal_upgrade_starts_a_new_generation_at_revision_one() -> TestResult {
|
||||||
|
let directory = tempfile::tempdir()?;
|
||||||
|
let journal = directory.path().join("watch-snapshot.toml");
|
||||||
|
let entries = || vec![entry("otp/alice", "Acme", "alice", b"secret")];
|
||||||
|
WatchSnapshotSender::load(journal.clone()).prepare("paired-watch", entries())?;
|
||||||
|
|
||||||
|
let legacy = fs::read_to_string(&journal)?
|
||||||
|
.lines()
|
||||||
|
.filter(|line| !line.starts_with("generation = "))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
.replace("revision = 1", "revision = 42");
|
||||||
|
fs::write(&journal, legacy)?;
|
||||||
|
|
||||||
|
let reset = WatchSnapshotSender::load(journal.clone()).prepare("paired-watch", entries())?;
|
||||||
|
assert_eq!(reset.revision(), 1);
|
||||||
|
let reloaded = WatchSnapshotSender::load(journal).prepare("paired-watch", entries())?;
|
||||||
|
assert_eq!(reloaded.revision(), 1);
|
||||||
|
assert_eq!(reset.snapshot().expose(), reloaded.snapshot().expose());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duplicate_delivery_after_watch_restart_recovers_a_lost_acknowledgement() -> TestResult {
|
||||||
|
let directory = tempfile::tempdir()?;
|
||||||
|
let mut sender = WatchSnapshotSender::load(directory.path().join("watch-snapshot.toml"));
|
||||||
|
let transfer = sender.prepare(
|
||||||
|
"paired-watch",
|
||||||
|
vec![entry("otp/alice", "Acme", "alice", b"secret")],
|
||||||
|
)?;
|
||||||
|
let persisted_snapshot = transfer.snapshot().expose().to_vec();
|
||||||
|
|
||||||
|
let mut interrupted_runtime = WatchRuntime::default();
|
||||||
|
let applied = interrupted_runtime.apply_snapshot(persisted_snapshot.clone())?;
|
||||||
|
assert_eq!(applied.persistence(), WatchPersistenceAction::Replace);
|
||||||
|
assert_eq!(sender.status().state(), MobileWatchSnapshotState::Pending);
|
||||||
|
|
||||||
|
let mut restarted_runtime = WatchRuntime::default();
|
||||||
|
restarted_runtime.apply_snapshot(persisted_snapshot.clone())?;
|
||||||
|
let redelivered = restarted_runtime.apply_snapshot(persisted_snapshot)?;
|
||||||
|
assert_eq!(redelivered.apply(), WatchSnapshotApply::Duplicate);
|
||||||
|
assert_eq!(redelivered.persistence(), WatchPersistenceAction::Keep);
|
||||||
|
assert_eq!(
|
||||||
|
sender.acknowledge(redelivered.receipt())?.state(),
|
||||||
|
MobileWatchSnapshotState::Current
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn authoritative_context_switches_sender_generation_and_rejects_delayed_packets() -> TestResult {
|
||||||
|
let directory = tempfile::tempdir()?;
|
||||||
|
let mut old_sender = WatchSnapshotSender::load(directory.path().join("old.toml"));
|
||||||
|
old_sender.prepare(
|
||||||
|
"paired-watch",
|
||||||
|
vec![entry("otp/alice", "Acme", "alice", b"old-secret")],
|
||||||
|
)?;
|
||||||
|
for revision in 2..5 {
|
||||||
|
old_sender.prepare(
|
||||||
|
"paired-watch",
|
||||||
|
vec![entry(
|
||||||
|
"otp/alice",
|
||||||
|
"Acme",
|
||||||
|
&format!("alice-{revision}"),
|
||||||
|
b"old-secret",
|
||||||
|
)],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
let old = old_sender.prepare(
|
||||||
|
"paired-watch",
|
||||||
|
vec![entry("otp/alice", "Acme", "alice-5", b"old-secret")],
|
||||||
|
)?;
|
||||||
|
assert_eq!(old.revision(), 5);
|
||||||
|
|
||||||
|
let mut new_sender = WatchSnapshotSender::load(directory.path().join("new.toml"));
|
||||||
|
let new = new_sender.prepare(
|
||||||
|
"paired-watch",
|
||||||
|
vec![entry("otp/bob", "Acme", "bob", b"new-secret")],
|
||||||
|
)?;
|
||||||
|
assert_eq!(new.revision(), 1);
|
||||||
|
|
||||||
|
let mut runtime = WatchRuntime::default();
|
||||||
|
runtime.apply_snapshot(old.snapshot().expose().to_vec())?;
|
||||||
|
let queued_new = runtime.apply_snapshot(new.snapshot().expose().to_vec())?;
|
||||||
|
assert_eq!(queued_new.apply(), WatchSnapshotApply::Stale);
|
||||||
|
assert_eq!(
|
||||||
|
runtime.presentation_at(59)?.state(),
|
||||||
|
WatchPresentationState::Ready
|
||||||
|
);
|
||||||
|
assert_eq!(runtime.records_at(59)?[0].account(), "alice-5");
|
||||||
|
|
||||||
|
let replaced = runtime.apply_authoritative_snapshot(new.snapshot().expose().to_vec())?;
|
||||||
|
assert_eq!(replaced.apply(), WatchSnapshotApply::PairingChanged);
|
||||||
|
assert_eq!(replaced.persistence(), WatchPersistenceAction::Replace);
|
||||||
|
assert_eq!(runtime.records_at(59)?[0].account(), "bob");
|
||||||
|
assert_eq!(
|
||||||
|
new_sender.acknowledge(replaced.receipt())?.state(),
|
||||||
|
MobileWatchSnapshotState::Current
|
||||||
|
);
|
||||||
|
|
||||||
|
let delayed_old = runtime.apply_snapshot(old.snapshot().expose().to_vec())?;
|
||||||
|
assert_eq!(delayed_old.apply(), WatchSnapshotApply::Stale);
|
||||||
|
assert_eq!(runtime.records_at(59)?[0].account(), "bob");
|
||||||
|
|
||||||
|
let newer = new_sender.prepare(
|
||||||
|
"paired-watch",
|
||||||
|
vec![entry("otp/carol", "Acme", "carol", b"newer-secret")],
|
||||||
|
)?;
|
||||||
|
runtime.apply_authoritative_snapshot(newer.snapshot().expose().to_vec())?;
|
||||||
|
let delayed_context = runtime.apply_authoritative_snapshot(new.snapshot().expose().to_vec())?;
|
||||||
|
assert_eq!(delayed_context.apply(), WatchSnapshotApply::Stale);
|
||||||
|
assert_eq!(runtime.records_at(59)?[0].account(), "carol");
|
||||||
|
|
||||||
|
assert!(runtime.apply_snapshot(vec![0; 64]).is_err());
|
||||||
|
assert_eq!(runtime.records_at(59)?[0].account(), "carol");
|
||||||
|
assert_eq!(
|
||||||
|
runtime.presentation_at(59)?.state(),
|
||||||
|
WatchPresentationState::Stale
|
||||||
|
);
|
||||||
|
runtime.sync_unavailable();
|
||||||
|
assert_eq!(runtime.records_at(59)?[0].account(), "carol");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn watch_runtime_generates_view_ready_totp_and_clears_secrets_when_locked() -> TestResult {
|
fn watch_runtime_generates_view_ready_totp_and_clears_secrets_when_locked() -> TestResult {
|
||||||
let directory = tempfile::tempdir()?;
|
let directory = tempfile::tempdir()?;
|
||||||
@@ -266,12 +409,12 @@ fn watch_runtime_generates_view_ready_totp_and_clears_secrets_when_locked() -> T
|
|||||||
runtime.apply_snapshot(snapshot.snapshot().expose().to_vec())?;
|
runtime.apply_snapshot(snapshot.snapshot().expose().to_vec())?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
runtime.presentation_at(59)?.state(),
|
runtime.presentation_at(59)?.state(),
|
||||||
WatchPresentationState::Stale
|
WatchPresentationState::Empty
|
||||||
);
|
);
|
||||||
assert!(runtime.apply_snapshot(vec![0; 64]).is_err());
|
assert!(runtime.apply_snapshot(vec![0; 64]).is_err());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
runtime.presentation_at(59)?.state(),
|
runtime.presentation_at(59)?.state(),
|
||||||
WatchPresentationState::Error
|
WatchPresentationState::Stale
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,6 +185,18 @@ impl WatchCore {
|
|||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn apply_authoritative_snapshot(
|
||||||
|
&self,
|
||||||
|
snapshot: Vec<u8>,
|
||||||
|
) -> Result<WatchSnapshotUpdate, WatchFfiError> {
|
||||||
|
self.runtime
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| lock_error())?
|
||||||
|
.apply_authoritative_snapshot(snapshot)
|
||||||
|
.map(Into::into)
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn presentation_at(&self, unix_seconds: u64) -> Result<WatchPresentation, WatchFfiError> {
|
pub fn presentation_at(&self, unix_seconds: u64) -> Result<WatchPresentation, WatchFfiError> {
|
||||||
self.runtime
|
self.runtime
|
||||||
.lock()
|
.lock()
|
||||||
|
|||||||
Reference in New Issue
Block a user