Synchronize selected TOTP entries to Apple Watch (#55)
This commit is contained in:
@@ -603,6 +603,8 @@ fileprivate struct FfiConverterData: FfiConverterRustBuffer {
|
|||||||
|
|
||||||
public protocol MobileAuthenticationProtocol: AnyObject, Sendable {
|
public protocol MobileAuthenticationProtocol: AnyObject, Sendable {
|
||||||
|
|
||||||
|
func acknowledgeWatchSnapshot(receipt: Data) throws -> MobileWatchSnapshotStatus
|
||||||
|
|
||||||
func addEntryEditorField(editor: UInt64, kind: MobileEntryEditorFieldKind, name: String?, value: String) throws -> MobileEntryEditorPage
|
func addEntryEditorField(editor: UInt64, kind: MobileEntryEditorFieldKind, name: String?, value: String) throws -> MobileEntryEditorPage
|
||||||
|
|
||||||
func beginCreateEntry(directory: String, name: String, passphrase: String?) throws -> MobileEntryEditorSession
|
func beginCreateEntry(directory: String, name: String, passphrase: String?) throws -> MobileEntryEditorSession
|
||||||
@@ -623,6 +625,8 @@ public protocol MobileAuthenticationProtocol: AnyObject, Sendable {
|
|||||||
|
|
||||||
func entryPage(path: String, unixSeconds: UInt64) throws -> MobileEntryPresentation
|
func entryPage(path: String, unixSeconds: UInt64) throws -> MobileEntryPresentation
|
||||||
|
|
||||||
|
func failWatchSnapshot(revision: UInt64, detail: String) throws -> MobileWatchSnapshotStatus
|
||||||
|
|
||||||
func generateEntryEditorPassword(editor: UInt64, length: UInt32?, noSymbols: Bool) throws -> MobileEntryEditorPage
|
func generateEntryEditorPassword(editor: UInt64, length: UInt32?, noSymbols: Bool) throws -> MobileEntryEditorPage
|
||||||
|
|
||||||
func gitIdentity() throws -> MobileGitIdentity
|
func gitIdentity() throws -> MobileGitIdentity
|
||||||
@@ -637,6 +641,8 @@ public protocol MobileAuthenticationProtocol: AnyObject, Sendable {
|
|||||||
|
|
||||||
func prepareEntryMutation(path: String, action: MobileMutationAction) throws -> MobileMutationPlan
|
func prepareEntryMutation(path: String, action: MobileMutationAction) throws -> MobileMutationPlan
|
||||||
|
|
||||||
|
func prepareWatchSnapshot(platformPairingIdentity: String) throws -> MobileWatchSnapshotTransfer
|
||||||
|
|
||||||
func removeApplicationToken() throws
|
func removeApplicationToken() throws
|
||||||
|
|
||||||
func removeEntryEditorField(editor: UInt64, field: UInt64) throws -> MobileEntryEditorPage
|
func removeEntryEditorField(editor: UInt64, field: UInt64) throws -> MobileEntryEditorPage
|
||||||
@@ -661,6 +667,8 @@ public protocol MobileAuthenticationProtocol: AnyObject, Sendable {
|
|||||||
|
|
||||||
func setTotpWatchShared(path: String, shared: Bool, unixSeconds: UInt64) throws -> MobileTotpDetail
|
func setTotpWatchShared(path: String, shared: Bool, unixSeconds: UInt64) throws -> MobileTotpDetail
|
||||||
|
|
||||||
|
func setWatchSnapshotUnavailable(unpaired: Bool, detail: String) throws -> MobileWatchSnapshotStatus
|
||||||
|
|
||||||
func state() throws -> MobileAuthenticationState
|
func state() throws -> MobileAuthenticationState
|
||||||
|
|
||||||
func totpDetail(path: String, unixSeconds: UInt64) throws -> MobileTotpDetail
|
func totpDetail(path: String, unixSeconds: UInt64) throws -> MobileTotpDetail
|
||||||
@@ -675,6 +683,8 @@ public protocol MobileAuthenticationProtocol: AnyObject, Sendable {
|
|||||||
|
|
||||||
func updateEntryEditor(editor: UInt64, fields: [MobileEntryEditorInput]) throws -> MobileEntryEditorPage
|
func updateEntryEditor(editor: UInt64, fields: [MobileEntryEditorInput]) throws -> MobileEntryEditorPage
|
||||||
|
|
||||||
|
func watchSnapshotStatus() throws -> MobileWatchSnapshotStatus
|
||||||
|
|
||||||
}
|
}
|
||||||
open class MobileAuthentication: MobileAuthenticationProtocol, @unchecked Sendable {
|
open class MobileAuthentication: MobileAuthenticationProtocol, @unchecked Sendable {
|
||||||
fileprivate let handle: UInt64
|
fileprivate let handle: UInt64
|
||||||
@@ -729,6 +739,16 @@ open class MobileAuthentication: MobileAuthenticationProtocol, @unchecked Sendab
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
open func acknowledgeWatchSnapshot(receipt: Data)throws -> MobileWatchSnapshotStatus {
|
||||||
|
return try FfiConverterTypeMobileWatchSnapshotStatus_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_method_mobileauthentication_acknowledge_watch_snapshot(
|
||||||
|
self.uniffiCloneHandle(),
|
||||||
|
FfiConverterData.lower(receipt),uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
open func addEntryEditorField(editor: UInt64, kind: MobileEntryEditorFieldKind, name: String?, value: String)throws -> MobileEntryEditorPage {
|
open func addEntryEditorField(editor: UInt64, kind: MobileEntryEditorFieldKind, name: String?, value: String)throws -> MobileEntryEditorPage {
|
||||||
return try FfiConverterTypeMobileEntryEditorPage_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
return try FfiConverterTypeMobileEntryEditorPage_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
@@ -833,6 +853,17 @@ open func entryPage(path: String, unixSeconds: UInt64)throws -> MobileEntryPres
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func failWatchSnapshot(revision: UInt64, detail: String)throws -> MobileWatchSnapshotStatus {
|
||||||
|
return try FfiConverterTypeMobileWatchSnapshotStatus_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_method_mobileauthentication_fail_watch_snapshot(
|
||||||
|
self.uniffiCloneHandle(),
|
||||||
|
FfiConverterUInt64.lower(revision),
|
||||||
|
FfiConverterString.lower(detail),uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
open func generateEntryEditorPassword(editor: UInt64, length: UInt32?, noSymbols: Bool)throws -> MobileEntryEditorPage {
|
open func generateEntryEditorPassword(editor: UInt64, length: UInt32?, noSymbols: Bool)throws -> MobileEntryEditorPage {
|
||||||
return try FfiConverterTypeMobileEntryEditorPage_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
return try FfiConverterTypeMobileEntryEditorPage_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
@@ -904,6 +935,16 @@ open func prepareEntryMutation(path: String, action: MobileMutationAction)throws
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func prepareWatchSnapshot(platformPairingIdentity: String)throws -> MobileWatchSnapshotTransfer {
|
||||||
|
return try FfiConverterTypeMobileWatchSnapshotTransfer_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_method_mobileauthentication_prepare_watch_snapshot(
|
||||||
|
self.uniffiCloneHandle(),
|
||||||
|
FfiConverterString.lower(platformPairingIdentity),uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
open func removeApplicationToken()throws {try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
open func removeApplicationToken()throws {try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
uniffi_ironstorage_apple_fn_method_mobileauthentication_remove_application_token(
|
uniffi_ironstorage_apple_fn_method_mobileauthentication_remove_application_token(
|
||||||
@@ -1030,6 +1071,17 @@ open func setTotpWatchShared(path: String, shared: Bool, unixSeconds: UInt64)thr
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func setWatchSnapshotUnavailable(unpaired: Bool, detail: String)throws -> MobileWatchSnapshotStatus {
|
||||||
|
return try FfiConverterTypeMobileWatchSnapshotStatus_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_method_mobileauthentication_set_watch_snapshot_unavailable(
|
||||||
|
self.uniffiCloneHandle(),
|
||||||
|
FfiConverterBool.lower(unpaired),
|
||||||
|
FfiConverterString.lower(detail),uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
open func state()throws -> MobileAuthenticationState {
|
open func state()throws -> MobileAuthenticationState {
|
||||||
return try FfiConverterTypeMobileAuthenticationState_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
return try FfiConverterTypeMobileAuthenticationState_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
@@ -1100,6 +1152,15 @@ open func updateEntryEditor(editor: UInt64, fields: [MobileEntryEditorInput])thr
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func watchSnapshotStatus()throws -> MobileWatchSnapshotStatus {
|
||||||
|
return try FfiConverterTypeMobileWatchSnapshotStatus_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_method_mobileauthentication_watch_snapshot_status(
|
||||||
|
self.uniffiCloneHandle(),uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4453,12 +4514,14 @@ public func FfiConverterTypeMobileTotpRow_lower(_ value: MobileTotpRow) -> RustB
|
|||||||
|
|
||||||
public struct MobileWatchSnapshotStatus: Equatable, Hashable {
|
public struct MobileWatchSnapshotStatus: Equatable, Hashable {
|
||||||
public var state: MobileWatchSnapshotState
|
public var state: MobileWatchSnapshotState
|
||||||
|
public var revision: UInt64?
|
||||||
public var detail: String
|
public var detail: String
|
||||||
|
|
||||||
// Default memberwise initializers are never public by default, so we
|
// Default memberwise initializers are never public by default, so we
|
||||||
// declare one manually.
|
// declare one manually.
|
||||||
public init(state: MobileWatchSnapshotState, detail: String) {
|
public init(state: MobileWatchSnapshotState, revision: UInt64?, detail: String) {
|
||||||
self.state = state
|
self.state = state
|
||||||
|
self.revision = revision
|
||||||
self.detail = detail
|
self.detail = detail
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4479,12 +4542,14 @@ public struct FfiConverterTypeMobileWatchSnapshotStatus: FfiConverterRustBuffer
|
|||||||
return
|
return
|
||||||
try MobileWatchSnapshotStatus(
|
try MobileWatchSnapshotStatus(
|
||||||
state: FfiConverterTypeMobileWatchSnapshotState.read(from: &buf),
|
state: FfiConverterTypeMobileWatchSnapshotState.read(from: &buf),
|
||||||
|
revision: FfiConverterOptionUInt64.read(from: &buf),
|
||||||
detail: FfiConverterString.read(from: &buf)
|
detail: FfiConverterString.read(from: &buf)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func write(_ value: MobileWatchSnapshotStatus, into buf: inout [UInt8]) {
|
public static func write(_ value: MobileWatchSnapshotStatus, into buf: inout [UInt8]) {
|
||||||
FfiConverterTypeMobileWatchSnapshotState.write(value.state, into: &buf)
|
FfiConverterTypeMobileWatchSnapshotState.write(value.state, into: &buf)
|
||||||
|
FfiConverterOptionUInt64.write(value.revision, into: &buf)
|
||||||
FfiConverterString.write(value.detail, into: &buf)
|
FfiConverterString.write(value.detail, into: &buf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4505,6 +4570,68 @@ public func FfiConverterTypeMobileWatchSnapshotStatus_lower(_ value: MobileWatch
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct MobileWatchSnapshotTransfer: Equatable, Hashable {
|
||||||
|
public var revision: UInt64
|
||||||
|
public var selectedEntries: UInt32
|
||||||
|
public var snapshot: Data
|
||||||
|
public var deliveredReceipt: Data
|
||||||
|
|
||||||
|
// Default memberwise initializers are never public by default, so we
|
||||||
|
// declare one manually.
|
||||||
|
public init(revision: UInt64, selectedEntries: UInt32, snapshot: Data, deliveredReceipt: Data) {
|
||||||
|
self.revision = revision
|
||||||
|
self.selectedEntries = selectedEntries
|
||||||
|
self.snapshot = snapshot
|
||||||
|
self.deliveredReceipt = deliveredReceipt
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobileWatchSnapshotTransfer: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobileWatchSnapshotTransfer: FfiConverterRustBuffer {
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileWatchSnapshotTransfer {
|
||||||
|
return
|
||||||
|
try MobileWatchSnapshotTransfer(
|
||||||
|
revision: FfiConverterUInt64.read(from: &buf),
|
||||||
|
selectedEntries: FfiConverterUInt32.read(from: &buf),
|
||||||
|
snapshot: FfiConverterData.read(from: &buf),
|
||||||
|
deliveredReceipt: FfiConverterData.read(from: &buf)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobileWatchSnapshotTransfer, into buf: inout [UInt8]) {
|
||||||
|
FfiConverterUInt64.write(value.revision, into: &buf)
|
||||||
|
FfiConverterUInt32.write(value.selectedEntries, into: &buf)
|
||||||
|
FfiConverterData.write(value.snapshot, into: &buf)
|
||||||
|
FfiConverterData.write(value.deliveredReceipt, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileWatchSnapshotTransfer_lift(_ buf: RustBuffer) throws -> MobileWatchSnapshotTransfer {
|
||||||
|
return try FfiConverterTypeMobileWatchSnapshotTransfer.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileWatchSnapshotTransfer_lower(_ value: MobileWatchSnapshotTransfer) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobileWatchSnapshotTransfer.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public enum MobileAppearance: Equatable, Hashable {
|
public enum MobileAppearance: Equatable, Hashable {
|
||||||
|
|
||||||
@@ -6618,6 +6745,9 @@ public enum MobileWatchPreferenceState: Equatable, Hashable {
|
|||||||
case appNotInstalled
|
case appNotInstalled
|
||||||
case ready
|
case ready
|
||||||
case pending
|
case pending
|
||||||
|
case delivered
|
||||||
|
case current
|
||||||
|
case failed
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -6649,6 +6779,12 @@ public struct FfiConverterTypeMobileWatchPreferenceState: FfiConverterRustBuffer
|
|||||||
|
|
||||||
case 5: return .pending
|
case 5: return .pending
|
||||||
|
|
||||||
|
case 6: return .delivered
|
||||||
|
|
||||||
|
case 7: return .current
|
||||||
|
|
||||||
|
case 8: return .failed
|
||||||
|
|
||||||
default: throw UniffiInternalError.unexpectedEnumCase
|
default: throw UniffiInternalError.unexpectedEnumCase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6676,6 +6812,18 @@ public struct FfiConverterTypeMobileWatchPreferenceState: FfiConverterRustBuffer
|
|||||||
case .pending:
|
case .pending:
|
||||||
writeInt(&buf, Int32(5))
|
writeInt(&buf, Int32(5))
|
||||||
|
|
||||||
|
|
||||||
|
case .delivered:
|
||||||
|
writeInt(&buf, Int32(6))
|
||||||
|
|
||||||
|
|
||||||
|
case .current:
|
||||||
|
writeInt(&buf, Int32(7))
|
||||||
|
|
||||||
|
|
||||||
|
case .failed:
|
||||||
|
writeInt(&buf, Int32(8))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6701,7 +6849,11 @@ public func FfiConverterTypeMobileWatchPreferenceState_lower(_ value: MobileWatc
|
|||||||
public enum MobileWatchSnapshotState: Equatable, Hashable {
|
public enum MobileWatchSnapshotState: Equatable, Hashable {
|
||||||
|
|
||||||
case unavailable
|
case unavailable
|
||||||
|
case unpaired
|
||||||
case pending
|
case pending
|
||||||
|
case delivered
|
||||||
|
case current
|
||||||
|
case failed
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -6725,7 +6877,15 @@ public struct FfiConverterTypeMobileWatchSnapshotState: FfiConverterRustBuffer {
|
|||||||
|
|
||||||
case 1: return .unavailable
|
case 1: return .unavailable
|
||||||
|
|
||||||
case 2: return .pending
|
case 2: return .unpaired
|
||||||
|
|
||||||
|
case 3: return .pending
|
||||||
|
|
||||||
|
case 4: return .delivered
|
||||||
|
|
||||||
|
case 5: return .current
|
||||||
|
|
||||||
|
case 6: return .failed
|
||||||
|
|
||||||
default: throw UniffiInternalError.unexpectedEnumCase
|
default: throw UniffiInternalError.unexpectedEnumCase
|
||||||
}
|
}
|
||||||
@@ -6739,9 +6899,25 @@ public struct FfiConverterTypeMobileWatchSnapshotState: FfiConverterRustBuffer {
|
|||||||
writeInt(&buf, Int32(1))
|
writeInt(&buf, Int32(1))
|
||||||
|
|
||||||
|
|
||||||
case .pending:
|
case .unpaired:
|
||||||
writeInt(&buf, Int32(2))
|
writeInt(&buf, Int32(2))
|
||||||
|
|
||||||
|
|
||||||
|
case .pending:
|
||||||
|
writeInt(&buf, Int32(3))
|
||||||
|
|
||||||
|
|
||||||
|
case .delivered:
|
||||||
|
writeInt(&buf, Int32(4))
|
||||||
|
|
||||||
|
|
||||||
|
case .current:
|
||||||
|
writeInt(&buf, Int32(5))
|
||||||
|
|
||||||
|
|
||||||
|
case .failed:
|
||||||
|
writeInt(&buf, Int32(6))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6786,6 +6962,30 @@ fileprivate struct FfiConverterOptionUInt32: FfiConverterRustBuffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
fileprivate struct FfiConverterOptionUInt64: FfiConverterRustBuffer {
|
||||||
|
typealias SwiftType = UInt64?
|
||||||
|
|
||||||
|
public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
|
||||||
|
guard let value = value else {
|
||||||
|
writeInt(&buf, Int8(0))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeInt(&buf, Int8(1))
|
||||||
|
FfiConverterUInt64.write(value, into: &buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType {
|
||||||
|
switch try readInt(&buf) as Int8 {
|
||||||
|
case 0: return nil
|
||||||
|
case 1: return try FfiConverterUInt64.read(from: &buf)
|
||||||
|
default: throw UniffiInternalError.unexpectedOptionalTag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if swift(>=5.8)
|
#if swift(>=5.8)
|
||||||
@_documentation(visibility: private)
|
@_documentation(visibility: private)
|
||||||
#endif
|
#endif
|
||||||
@@ -7497,6 +7697,9 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_ironstorage_apple_checksum_func_set_selected_mobile_tab() != 65280) {
|
if (uniffi_ironstorage_apple_checksum_func_set_selected_mobile_tab() != 65280) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_acknowledge_watch_snapshot() != 12885) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_add_entry_editor_field() != 58790) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_add_entry_editor_field() != 58790) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
@@ -7527,6 +7730,9 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_entry_page() != 9073) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_entry_page() != 9073) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_fail_watch_snapshot() != 56313) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_generate_entry_editor_password() != 34289) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_generate_entry_editor_password() != 34289) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
@@ -7548,6 +7754,9 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_prepare_entry_mutation() != 6234) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_prepare_entry_mutation() != 6234) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_prepare_watch_snapshot() != 14176) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_remove_application_token() != 11452) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_remove_application_token() != 11452) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
@@ -7584,6 +7793,9 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_totp_watch_shared() != 57472) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_totp_watch_shared() != 57472) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_watch_snapshot_unavailable() != 49094) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_state() != 60826) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_state() != 60826) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
@@ -7605,6 +7817,9 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_update_entry_editor() != 30139) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_update_entry_editor() != 30139) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_watch_snapshot_status() != 17344) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_cached() != 32436) {
|
if (uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_cached() != 32436) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -253,6 +253,11 @@ uint64_t uniffi_ironstorage_apple_fn_clone_mobileauthentication(uint64_t handle,
|
|||||||
void uniffi_ironstorage_apple_fn_free_mobileauthentication(uint64_t handle, RustCallStatus *_Nonnull out_status
|
void uniffi_ironstorage_apple_fn_free_mobileauthentication(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_ACKNOWLEDGE_WATCH_SNAPSHOT
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_ACKNOWLEDGE_WATCH_SNAPSHOT
|
||||||
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_acknowledge_watch_snapshot(uint64_t ptr, RustBuffer receipt, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_ADD_ENTRY_EDITOR_FIELD
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_ADD_ENTRY_EDITOR_FIELD
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_ADD_ENTRY_EDITOR_FIELD
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_ADD_ENTRY_EDITOR_FIELD
|
||||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_add_entry_editor_field(uint64_t ptr, uint64_t editor, RustBuffer kind, RustBuffer name, RustBuffer value, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_add_entry_editor_field(uint64_t ptr, uint64_t editor, RustBuffer kind, RustBuffer name, RustBuffer value, RustCallStatus *_Nonnull out_status
|
||||||
@@ -303,6 +308,11 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_entry_editor(
|
|||||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_entry_page(uint64_t ptr, RustBuffer path, uint64_t unix_seconds, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_entry_page(uint64_t ptr, RustBuffer path, uint64_t unix_seconds, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_FAIL_WATCH_SNAPSHOT
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_FAIL_WATCH_SNAPSHOT
|
||||||
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_fail_watch_snapshot(uint64_t ptr, uint64_t revision, RustBuffer detail, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_GENERATE_ENTRY_EDITOR_PASSWORD
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_GENERATE_ENTRY_EDITOR_PASSWORD
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_GENERATE_ENTRY_EDITOR_PASSWORD
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_GENERATE_ENTRY_EDITOR_PASSWORD
|
||||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_generate_entry_editor_password(uint64_t ptr, uint64_t editor, RustBuffer length, int8_t no_symbols, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_generate_entry_editor_password(uint64_t ptr, uint64_t editor, RustBuffer length, int8_t no_symbols, RustCallStatus *_Nonnull out_status
|
||||||
@@ -338,6 +348,11 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_preferences(u
|
|||||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_prepare_entry_mutation(uint64_t ptr, RustBuffer path, RustBuffer action, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_prepare_entry_mutation(uint64_t ptr, RustBuffer path, RustBuffer action, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_PREPARE_WATCH_SNAPSHOT
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_PREPARE_WATCH_SNAPSHOT
|
||||||
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_prepare_watch_snapshot(uint64_t ptr, RustBuffer platform_pairing_identity, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_REMOVE_APPLICATION_TOKEN
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_REMOVE_APPLICATION_TOKEN
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_REMOVE_APPLICATION_TOKEN
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_REMOVE_APPLICATION_TOKEN
|
||||||
void uniffi_ironstorage_apple_fn_method_mobileauthentication_remove_application_token(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
void uniffi_ironstorage_apple_fn_method_mobileauthentication_remove_application_token(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
||||||
@@ -398,6 +413,11 @@ void uniffi_ironstorage_apple_fn_method_mobileauthentication_set_mobile_appearan
|
|||||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_totp_watch_shared(uint64_t ptr, RustBuffer path, int8_t shared, uint64_t unix_seconds, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_totp_watch_shared(uint64_t ptr, RustBuffer path, int8_t shared, uint64_t unix_seconds, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_WATCH_SNAPSHOT_UNAVAILABLE
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_WATCH_SNAPSHOT_UNAVAILABLE
|
||||||
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_watch_snapshot_unavailable(uint64_t ptr, int8_t unpaired, RustBuffer detail, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_STATE
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_STATE
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_STATE
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_STATE
|
||||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_state(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_state(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
||||||
@@ -433,6 +453,11 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_unlock_totp(u
|
|||||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_update_entry_editor(uint64_t ptr, uint64_t editor, RustBuffer fields, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_update_entry_editor(uint64_t ptr, uint64_t editor, RustBuffer fields, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_WATCH_SNAPSHOT_STATUS
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_WATCH_SNAPSHOT_STATUS
|
||||||
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_watch_snapshot_status(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILEHOMEOPERATION
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILEHOMEOPERATION
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILEHOMEOPERATION
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILEHOMEOPERATION
|
||||||
uint64_t uniffi_ironstorage_apple_fn_clone_mobilehomeoperation(uint64_t handle, RustCallStatus *_Nonnull out_status
|
uint64_t uniffi_ironstorage_apple_fn_clone_mobilehomeoperation(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||||
@@ -973,6 +998,12 @@ uint16_t uniffi_ironstorage_apple_checksum_func_replace_configured_mobile_applic
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_SET_SELECTED_MOBILE_TAB
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_SET_SELECTED_MOBILE_TAB
|
||||||
uint16_t uniffi_ironstorage_apple_checksum_func_set_selected_mobile_tab(void
|
uint16_t uniffi_ironstorage_apple_checksum_func_set_selected_mobile_tab(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_ACKNOWLEDGE_WATCH_SNAPSHOT
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_ACKNOWLEDGE_WATCH_SNAPSHOT
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_acknowledge_watch_snapshot(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_ADD_ENTRY_EDITOR_FIELD
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_ADD_ENTRY_EDITOR_FIELD
|
||||||
@@ -1033,6 +1064,12 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_entry_edi
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_ENTRY_PAGE
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_ENTRY_PAGE
|
||||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_entry_page(void
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_entry_page(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_FAIL_WATCH_SNAPSHOT
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_FAIL_WATCH_SNAPSHOT
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_fail_watch_snapshot(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_GENERATE_ENTRY_EDITOR_PASSWORD
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_GENERATE_ENTRY_EDITOR_PASSWORD
|
||||||
@@ -1075,6 +1112,12 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_preferenc
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_PREPARE_ENTRY_MUTATION
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_PREPARE_ENTRY_MUTATION
|
||||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_prepare_entry_mutation(void
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_prepare_entry_mutation(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_PREPARE_WATCH_SNAPSHOT
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_PREPARE_WATCH_SNAPSHOT
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_prepare_watch_snapshot(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_REMOVE_APPLICATION_TOKEN
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_REMOVE_APPLICATION_TOKEN
|
||||||
@@ -1147,6 +1190,12 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_mobil
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_TOTP_WATCH_SHARED
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_TOTP_WATCH_SHARED
|
||||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_totp_watch_shared(void
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_totp_watch_shared(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_WATCH_SNAPSHOT_UNAVAILABLE
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_WATCH_SNAPSHOT_UNAVAILABLE
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_watch_snapshot_unavailable(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_STATE
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_STATE
|
||||||
@@ -1189,6 +1238,12 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_unlock_to
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_UPDATE_ENTRY_EDITOR
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_UPDATE_ENTRY_EDITOR
|
||||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_update_entry_editor(void
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_update_entry_editor(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_WATCH_SNAPSHOT_STATUS
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_WATCH_SNAPSHOT_STATUS
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_watch_snapshot_status(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEHOMEOPERATION_CACHED
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEHOMEOPERATION_CACHED
|
||||||
|
|||||||
@@ -14,11 +14,161 @@ extension Notification.Name {
|
|||||||
static let ironStorageWatchSnapshotDidChange = Notification.Name(
|
static let ironStorageWatchSnapshotDidChange = Notification.Name(
|
||||||
"de.rfc1437.ironstorage.watch-snapshot-did-change"
|
"de.rfc1437.ironstorage.watch-snapshot-did-change"
|
||||||
)
|
)
|
||||||
|
static let ironStorageWatchStatusDidChange = Notification.Name(
|
||||||
|
"de.rfc1437.ironstorage.watch-status-did-change"
|
||||||
|
)
|
||||||
static let ironStorageKeyMaterialDidChange = Notification.Name(
|
static let ironStorageKeyMaterialDidChange = Notification.Name(
|
||||||
"de.rfc1437.ironstorage.key-material-did-change"
|
"de.rfc1437.ironstorage.key-material-did-change"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private final class WatchSnapshotCoordinator: NSObject, WCSessionDelegate {
|
||||||
|
private static let snapshotKey = "de.rfc1437.ironstorage.watch.snapshot"
|
||||||
|
private static let deliveredReceiptKey = "de.rfc1437.ironstorage.watch.delivered"
|
||||||
|
|
||||||
|
private let authentication: MobileAuthentication?
|
||||||
|
private var session: WCSession?
|
||||||
|
private var syncTask: Task<Void, Never>?
|
||||||
|
private var generation = 0
|
||||||
|
|
||||||
|
var isSupported: Bool { WCSession.isSupported() }
|
||||||
|
var isPaired: Bool { session?.isPaired ?? false }
|
||||||
|
var isWatchAppInstalled: Bool { session?.isWatchAppInstalled ?? false }
|
||||||
|
|
||||||
|
init(authentication: MobileAuthentication?) {
|
||||||
|
self.authentication = authentication
|
||||||
|
super.init()
|
||||||
|
for name in [
|
||||||
|
Notification.Name.ironStorageWatchSnapshotDidChange,
|
||||||
|
.ironStorageLocalStoreDidChange,
|
||||||
|
.ironStorageAuthenticationDidChange,
|
||||||
|
] {
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(synchronize),
|
||||||
|
name: name,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
guard WCSession.isSupported() else {
|
||||||
|
recordUnavailable(unpaired: false, detail: "Apple Watch connectivity is unavailable on this device.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let session = WCSession.default
|
||||||
|
self.session = session
|
||||||
|
session.delegate = self
|
||||||
|
session.activate()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
syncTask?.cancel()
|
||||||
|
session?.delegate = nil
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func synchronize() {
|
||||||
|
generation += 1
|
||||||
|
syncTask?.cancel()
|
||||||
|
guard let authentication else { return }
|
||||||
|
guard let session, session.activationState == .activated else { return }
|
||||||
|
guard session.isPaired else {
|
||||||
|
try? session.updateApplicationContext([:])
|
||||||
|
recordUnavailable(unpaired: true, detail: "Pair an Apple Watch to synchronize selected TOTP entries.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard session.isWatchAppInstalled, let directory = session.watchDirectoryURL else {
|
||||||
|
try? session.updateApplicationContext([:])
|
||||||
|
recordUnavailable(unpaired: false, detail: "Install IronStorage on the paired Apple Watch.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let pairingIdentity = directory.lastPathComponent
|
||||||
|
let current = generation
|
||||||
|
syncTask = Task { [weak self] in
|
||||||
|
let result = await Task.detached(priority: .utility) {
|
||||||
|
Result { try authentication.prepareWatchSnapshot(platformPairingIdentity: pairingIdentity) }
|
||||||
|
}.value
|
||||||
|
guard !Task.isCancelled, let self, current == generation else { return }
|
||||||
|
switch result {
|
||||||
|
case var .success(transfer):
|
||||||
|
var snapshot = Data(transfer.snapshot)
|
||||||
|
defer {
|
||||||
|
snapshot.resetBytes(in: 0..<snapshot.count)
|
||||||
|
transfer.snapshot.removeAll(keepingCapacity: false)
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
try session.updateApplicationContext([
|
||||||
|
Self.snapshotKey: snapshot,
|
||||||
|
Self.deliveredReceiptKey: Data(transfer.deliveredReceipt),
|
||||||
|
])
|
||||||
|
} catch {
|
||||||
|
_ = try? authentication.failWatchSnapshot(
|
||||||
|
revision: transfer.revision,
|
||||||
|
detail: "The latest Apple Watch snapshot could not be queued."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
notifyStatusChanged()
|
||||||
|
case .failure:
|
||||||
|
// A locked GPG key leaves the previous replacement pending. Authentication
|
||||||
|
// changes trigger a fresh attempt without persisting snapshot bytes in Swift.
|
||||||
|
notifyStatusChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func recordUnavailable(unpaired: Bool, detail: String) {
|
||||||
|
guard let authentication else { return }
|
||||||
|
_ = try? authentication.setWatchSnapshotUnavailable(unpaired: unpaired, detail: detail)
|
||||||
|
notifyStatusChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func notifyStatusChanged() {
|
||||||
|
NotificationCenter.default.post(name: .ironStorageWatchStatusDidChange, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated func session(
|
||||||
|
_ session: WCSession,
|
||||||
|
activationDidCompleteWith activationState: WCSessionActivationState,
|
||||||
|
error: Error?
|
||||||
|
) {
|
||||||
|
Task { @MainActor [weak self] in self?.synchronize() }
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated func sessionDidBecomeInactive(_ session: WCSession) {
|
||||||
|
Task { @MainActor [weak self] in
|
||||||
|
try? session.updateApplicationContext([:])
|
||||||
|
self?.recordUnavailable(unpaired: true, detail: "The active Apple Watch changed; a fresh snapshot is required.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated func sessionDidDeactivate(_ session: WCSession) {
|
||||||
|
session.activate()
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated func sessionWatchStateDidChange(_ session: WCSession) {
|
||||||
|
Task { @MainActor [weak self] in self?.synchronize() }
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated private func acknowledge(_ userInfo: [String: Any]) {
|
||||||
|
guard
|
||||||
|
let receipt = userInfo["de.rfc1437.ironstorage.watch.delivered"] as? Data
|
||||||
|
else { return }
|
||||||
|
Task { @MainActor [weak self] in
|
||||||
|
guard let self, let authentication else { return }
|
||||||
|
_ = try? authentication.acknowledgeWatchSnapshot(receipt: receipt)
|
||||||
|
notifyStatusChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
|
||||||
|
acknowledge(userInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
|
||||||
|
acknowledge(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@main
|
@main
|
||||||
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
@@ -73,6 +223,7 @@ private protocol MobileTabRoot: AnyObject {
|
|||||||
private final class AppContext: NSObject, UITabBarControllerDelegate {
|
private final class AppContext: NSObject, UITabBarControllerDelegate {
|
||||||
private let tabs = UITabBarController()
|
private let tabs = UITabBarController()
|
||||||
private let authentication = try? mobileAuthentication()
|
private let authentication = try? mobileAuthentication()
|
||||||
|
private lazy var watchSnapshot = WatchSnapshotCoordinator(authentication: authentication)
|
||||||
private var navigationControllers: [UINavigationController] = []
|
private var navigationControllers: [UINavigationController] = []
|
||||||
private var restoreTask: Task<Void, Never>?
|
private var restoreTask: Task<Void, Never>?
|
||||||
private var authenticationMonitor: Task<Void, Never>?
|
private var authenticationMonitor: Task<Void, Never>?
|
||||||
@@ -83,6 +234,7 @@ private final class AppContext: NSObject, UITabBarControllerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func makeRootController() -> UIViewController {
|
func makeRootController() -> UIViewController {
|
||||||
|
watchSnapshot.synchronize()
|
||||||
let shell = mobileShellFixture(state: .loading)
|
let shell = mobileShellFixture(state: .loading)
|
||||||
navigationControllers = shell.pages.map { page in
|
navigationControllers = shell.pages.map { page in
|
||||||
let root: UIViewController = switch page.tab {
|
let root: UIViewController = switch page.tab {
|
||||||
@@ -93,7 +245,11 @@ private final class AppContext: NSObject, UITabBarControllerDelegate {
|
|||||||
case .totp:
|
case .totp:
|
||||||
TotpListViewController(shellPage: page, authentication: authentication)
|
TotpListViewController(shellPage: page, authentication: authentication)
|
||||||
case .preferences:
|
case .preferences:
|
||||||
PreferencesViewController(page: page, authentication: authentication)
|
PreferencesViewController(
|
||||||
|
page: page,
|
||||||
|
authentication: authentication,
|
||||||
|
watchSnapshot: watchSnapshot
|
||||||
|
)
|
||||||
case .home:
|
case .home:
|
||||||
ShellViewController(page: page, authentication: authentication)
|
ShellViewController(page: page, authentication: authentication)
|
||||||
}
|
}
|
||||||
@@ -852,9 +1008,7 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private final class PreferencesViewController: UITableViewController, MobileTabRoot,
|
private final class PreferencesViewController: UITableViewController, MobileTabRoot {
|
||||||
WCSessionDelegate
|
|
||||||
{
|
|
||||||
fileprivate let shellTab = MobileTab.preferences
|
fileprivate let shellTab = MobileTab.preferences
|
||||||
private var page: MobilePage
|
private var page: MobilePage
|
||||||
private let authentication: MobileAuthentication?
|
private let authentication: MobileAuthentication?
|
||||||
@@ -862,21 +1016,20 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
private var state: MobileAuthenticationState?
|
private var state: MobileAuthenticationState?
|
||||||
private var preferences: MobilePreferences?
|
private var preferences: MobilePreferences?
|
||||||
private var gitIdentity: MobileGitIdentity?
|
private var gitIdentity: MobileGitIdentity?
|
||||||
private var watchSession: WCSession?
|
private let watchSnapshot: WatchSnapshotCoordinator
|
||||||
private var preferenceTask: Task<Void, Never>?
|
private var preferenceTask: Task<Void, Never>?
|
||||||
private var loadTask: Task<Void, Never>?
|
private var loadTask: Task<Void, Never>?
|
||||||
private var loadGeneration = 0
|
private var loadGeneration = 0
|
||||||
|
|
||||||
init(page: MobilePage, authentication: MobileAuthentication?) {
|
init(
|
||||||
|
page: MobilePage,
|
||||||
|
authentication: MobileAuthentication?,
|
||||||
|
watchSnapshot: WatchSnapshotCoordinator
|
||||||
|
) {
|
||||||
self.page = page
|
self.page = page
|
||||||
self.authentication = authentication
|
self.authentication = authentication
|
||||||
|
self.watchSnapshot = watchSnapshot
|
||||||
super.init(style: .insetGrouped)
|
super.init(style: .insetGrouped)
|
||||||
if WCSession.isSupported() {
|
|
||||||
let session = WCSession.default
|
|
||||||
watchSession = session
|
|
||||||
session.delegate = self
|
|
||||||
session.activate()
|
|
||||||
}
|
|
||||||
title = page.title
|
title = page.title
|
||||||
navigationItem.largeTitleDisplayMode = .always
|
navigationItem.largeTitleDisplayMode = .always
|
||||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||||
@@ -891,6 +1044,12 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
name: .ironStorageAuthenticationDidChange,
|
name: .ironStorageAuthenticationDidChange,
|
||||||
object: nil
|
object: nil
|
||||||
)
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(authenticationDidChange),
|
||||||
|
name: .ironStorageWatchStatusDidChange,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
@@ -899,7 +1058,6 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
watchSession?.delegate = nil
|
|
||||||
preferenceTask?.cancel()
|
preferenceTask?.cancel()
|
||||||
loadTask?.cancel()
|
loadTask?.cancel()
|
||||||
NotificationCenter.default.removeObserver(self)
|
NotificationCenter.default.removeObserver(self)
|
||||||
@@ -1380,8 +1538,19 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
|
|
||||||
private func openWatchPreference() {
|
private func openWatchPreference() {
|
||||||
switch preferences?.watchState {
|
switch preferences?.watchState {
|
||||||
case .ready, .pending:
|
case .ready, .pending, .delivered, .current:
|
||||||
selectTotpTab()
|
selectTotpTab()
|
||||||
|
case .failed:
|
||||||
|
let alert = UIAlertController(
|
||||||
|
title: "Apple Watch Synchronization Failed",
|
||||||
|
message: preferences?.watchDetail ?? "The latest snapshot could not be synchronized.",
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||||
|
alert.addAction(UIAlertAction(title: "Retry", style: .default) { [weak self] _ in
|
||||||
|
self?.watchSnapshot.synchronize()
|
||||||
|
})
|
||||||
|
present(alert, animated: true)
|
||||||
case .notPaired:
|
case .notPaired:
|
||||||
presentWatchGuidance(
|
presentWatchGuidance(
|
||||||
title: "Pair an Apple Watch",
|
title: "Pair an Apple Watch",
|
||||||
@@ -1429,6 +1598,9 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
switch state {
|
switch state {
|
||||||
case .ready: "applewatch.radiowaves.left.and.right"
|
case .ready: "applewatch.radiowaves.left.and.right"
|
||||||
case .pending: "arrow.triangle.2.circlepath"
|
case .pending: "arrow.triangle.2.circlepath"
|
||||||
|
case .delivered: "arrow.down.circle"
|
||||||
|
case .current: "checkmark.circle.fill"
|
||||||
|
case .failed: "exclamationmark.triangle"
|
||||||
case .notPaired, .appNotInstalled: "applewatch.slash"
|
case .notPaired, .appNotInstalled: "applewatch.slash"
|
||||||
default: "applewatch"
|
default: "applewatch"
|
||||||
}
|
}
|
||||||
@@ -1480,9 +1652,9 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
state = try? authentication?.state()
|
state = try? authentication?.state()
|
||||||
gitIdentity = try? authentication?.gitIdentity()
|
gitIdentity = try? authentication?.gitIdentity()
|
||||||
preferences = try? authentication?.preferences(
|
preferences = try? authentication?.preferences(
|
||||||
watchSupported: WCSession.isSupported(),
|
watchSupported: watchSnapshot.isSupported,
|
||||||
watchPaired: watchSession?.isPaired ?? false,
|
watchPaired: watchSnapshot.isPaired,
|
||||||
watchAppInstalled: watchSession?.isWatchAppInstalled ?? false
|
watchAppInstalled: watchSnapshot.isWatchAppInstalled
|
||||||
)
|
)
|
||||||
if let appearance = preferences?.appearance {
|
if let appearance = preferences?.appearance {
|
||||||
tabBarController?.overrideUserInterfaceStyle = switch appearance {
|
tabBarController?.overrideUserInterfaceStyle = switch appearance {
|
||||||
@@ -1494,23 +1666,6 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
tableView.reloadData()
|
tableView.reloadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
nonisolated func session(
|
|
||||||
_ session: WCSession,
|
|
||||||
activationDidCompleteWith activationState: WCSessionActivationState,
|
|
||||||
error: Error?
|
|
||||||
) {
|
|
||||||
Task { @MainActor [weak self] in self?.refreshState() }
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated func sessionDidBecomeInactive(_ session: WCSession) {}
|
|
||||||
|
|
||||||
nonisolated func sessionDidDeactivate(_ session: WCSession) {
|
|
||||||
session.activate()
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated func sessionWatchStateDidChange(_ session: WCSession) {
|
|
||||||
Task { @MainActor [weak self] in self?.refreshState() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -2220,6 +2375,12 @@ private final class TotpListViewController: UITableViewController, MobileTabRoot
|
|||||||
name: .ironStorageWatchSnapshotDidChange,
|
name: .ironStorageWatchSnapshotDidChange,
|
||||||
object: nil
|
object: nil
|
||||||
)
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(localStoreDidChange),
|
||||||
|
name: .ironStorageWatchStatusDidChange,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
refreshState()
|
refreshState()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2691,8 +2852,6 @@ private final class TotpListViewController: UITableViewController, MobileTabRoot
|
|||||||
object: authentication
|
object: authentication
|
||||||
)
|
)
|
||||||
refreshState()
|
refreshState()
|
||||||
} else {
|
|
||||||
showUnavailable(title: failure.title, detail: failure.detail, image: "exclamationmark.triangle")
|
|
||||||
}
|
}
|
||||||
presentAuthenticationFailure(failure)
|
presentAuthenticationFailure(failure)
|
||||||
}
|
}
|
||||||
@@ -2870,6 +3029,12 @@ private final class TotpDetailViewController: UITableViewController {
|
|||||||
name: .ironStorageLocalStoreDidChange,
|
name: .ironStorageLocalStoreDidChange,
|
||||||
object: nil
|
object: nil
|
||||||
)
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(entryDidChange),
|
||||||
|
name: .ironStorageWatchStatusDidChange,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
apply(detail)
|
apply(detail)
|
||||||
startTimer()
|
startTimer()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,54 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import WatchConnectivity
|
||||||
|
|
||||||
|
private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessionDelegate {
|
||||||
|
private static let snapshotKey = "de.rfc1437.ironstorage.watch.snapshot"
|
||||||
|
private static let deliveredReceiptKey = "de.rfc1437.ironstorage.watch.delivered"
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
guard WCSession.isSupported() else { return }
|
||||||
|
let session = WCSession.default
|
||||||
|
session.delegate = self
|
||||||
|
session.activate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func receive(_ applicationContext: [String: Any], session: WCSession) {
|
||||||
|
guard
|
||||||
|
applicationContext[Self.snapshotKey] is Data,
|
||||||
|
let receipt = applicationContext[Self.deliveredReceiptKey] as? Data
|
||||||
|
else { return }
|
||||||
|
let acknowledgement = [Self.deliveredReceiptKey: receipt]
|
||||||
|
if session.isReachable {
|
||||||
|
session.sendMessage(acknowledgement, replyHandler: nil) { _ in
|
||||||
|
session.transferUserInfo(acknowledgement)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
session.transferUserInfo(acknowledgement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func session(
|
||||||
|
_ session: WCSession,
|
||||||
|
activationDidCompleteWith activationState: WCSessionActivationState,
|
||||||
|
error: Error?
|
||||||
|
) {
|
||||||
|
guard activationState == .activated, error == nil else { return }
|
||||||
|
receive(session.receivedApplicationContext, session: session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func session(
|
||||||
|
_ session: WCSession,
|
||||||
|
didReceiveApplicationContext applicationContext: [String: Any]
|
||||||
|
) {
|
||||||
|
receive(applicationContext, session: session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct IronStorageWatchApp: App {
|
struct IronStorageWatchApp: App {
|
||||||
|
@StateObject private var transport = WatchSnapshotTransport()
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentUnavailableView("No TOTP Codes", systemImage: "timer")
|
ContentUnavailableView("No TOTP Codes", systemImage: "timer")
|
||||||
|
|||||||
@@ -59,8 +59,11 @@ use ironstorage::{
|
|||||||
MobileTotpDetail as StorageTotpDetail,
|
MobileTotpDetail as StorageTotpDetail,
|
||||||
MobileTotpDiscoveryPhase as StorageTotpDiscoveryPhase,
|
MobileTotpDiscoveryPhase as StorageTotpDiscoveryPhase,
|
||||||
MobileTotpOperation as StorageTotpOperation, MobileTotpPage as StorageTotpPage,
|
MobileTotpOperation as StorageTotpOperation, MobileTotpPage as StorageTotpPage,
|
||||||
|
},
|
||||||
|
mobile_watch::{
|
||||||
MobileWatchSnapshotState as StorageWatchSnapshotState,
|
MobileWatchSnapshotState as StorageWatchSnapshotState,
|
||||||
MobileWatchSnapshotStatus as StorageWatchSnapshotStatus,
|
MobileWatchSnapshotStatus as StorageWatchSnapshotStatus,
|
||||||
|
WatchSnapshotTransfer as StorageWatchSnapshotTransfer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -659,6 +662,9 @@ pub enum MobileWatchPreferenceState {
|
|||||||
AppNotInstalled,
|
AppNotInstalled,
|
||||||
Ready,
|
Ready,
|
||||||
Pending,
|
Pending,
|
||||||
|
Delivered,
|
||||||
|
Current,
|
||||||
|
Failed,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<StorageWatchPreferenceState> for MobileWatchPreferenceState {
|
impl From<StorageWatchPreferenceState> for MobileWatchPreferenceState {
|
||||||
@@ -669,6 +675,9 @@ impl From<StorageWatchPreferenceState> for MobileWatchPreferenceState {
|
|||||||
StorageWatchPreferenceState::AppNotInstalled => Self::AppNotInstalled,
|
StorageWatchPreferenceState::AppNotInstalled => Self::AppNotInstalled,
|
||||||
StorageWatchPreferenceState::Ready => Self::Ready,
|
StorageWatchPreferenceState::Ready => Self::Ready,
|
||||||
StorageWatchPreferenceState::Pending => Self::Pending,
|
StorageWatchPreferenceState::Pending => Self::Pending,
|
||||||
|
StorageWatchPreferenceState::Delivered => Self::Delivered,
|
||||||
|
StorageWatchPreferenceState::Current => Self::Current,
|
||||||
|
StorageWatchPreferenceState::Failed => Self::Failed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -922,14 +931,22 @@ pub struct MobileEntryCopy {
|
|||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
|
||||||
pub enum MobileWatchSnapshotState {
|
pub enum MobileWatchSnapshotState {
|
||||||
Unavailable,
|
Unavailable,
|
||||||
|
Unpaired,
|
||||||
Pending,
|
Pending,
|
||||||
|
Delivered,
|
||||||
|
Current,
|
||||||
|
Failed,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<StorageWatchSnapshotState> for MobileWatchSnapshotState {
|
impl From<StorageWatchSnapshotState> for MobileWatchSnapshotState {
|
||||||
fn from(state: StorageWatchSnapshotState) -> Self {
|
fn from(state: StorageWatchSnapshotState) -> Self {
|
||||||
match state {
|
match state {
|
||||||
StorageWatchSnapshotState::Unavailable => Self::Unavailable,
|
StorageWatchSnapshotState::Unavailable => Self::Unavailable,
|
||||||
|
StorageWatchSnapshotState::Unpaired => Self::Unpaired,
|
||||||
StorageWatchSnapshotState::Pending => Self::Pending,
|
StorageWatchSnapshotState::Pending => Self::Pending,
|
||||||
|
StorageWatchSnapshotState::Delivered => Self::Delivered,
|
||||||
|
StorageWatchSnapshotState::Current => Self::Current,
|
||||||
|
StorageWatchSnapshotState::Failed => Self::Failed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -937,6 +954,7 @@ impl From<StorageWatchSnapshotState> for MobileWatchSnapshotState {
|
|||||||
#[derive(Clone, uniffi::Record)]
|
#[derive(Clone, uniffi::Record)]
|
||||||
pub struct MobileWatchSnapshotStatus {
|
pub struct MobileWatchSnapshotStatus {
|
||||||
pub state: MobileWatchSnapshotState,
|
pub state: MobileWatchSnapshotState,
|
||||||
|
pub revision: Option<u64>,
|
||||||
pub detail: String,
|
pub detail: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -944,11 +962,31 @@ impl From<&StorageWatchSnapshotStatus> for MobileWatchSnapshotStatus {
|
|||||||
fn from(status: &StorageWatchSnapshotStatus) -> Self {
|
fn from(status: &StorageWatchSnapshotStatus) -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: status.state().into(),
|
state: status.state().into(),
|
||||||
|
revision: status.revision(),
|
||||||
detail: status.detail().to_owned(),
|
detail: status.detail().to_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, uniffi::Record)]
|
||||||
|
pub struct MobileWatchSnapshotTransfer {
|
||||||
|
pub revision: u64,
|
||||||
|
pub selected_entries: u32,
|
||||||
|
pub snapshot: Vec<u8>,
|
||||||
|
pub delivered_receipt: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StorageWatchSnapshotTransfer> for MobileWatchSnapshotTransfer {
|
||||||
|
fn from(transfer: StorageWatchSnapshotTransfer) -> Self {
|
||||||
|
Self {
|
||||||
|
revision: transfer.revision(),
|
||||||
|
selected_entries: transfer.selected_entries(),
|
||||||
|
snapshot: transfer.snapshot().expose().to_vec(),
|
||||||
|
delivered_receipt: transfer.delivered_receipt().to_vec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, uniffi::Record)]
|
#[derive(Clone, uniffi::Record)]
|
||||||
pub struct MobileTotpRow {
|
pub struct MobileTotpRow {
|
||||||
pub path: String,
|
pub path: String,
|
||||||
@@ -1724,6 +1762,57 @@ impl MobileAuthentication {
|
|||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn watch_snapshot_status(
|
||||||
|
&self,
|
||||||
|
) -> Result<MobileWatchSnapshotStatus, MobileAuthenticationFfiError> {
|
||||||
|
self.authentication
|
||||||
|
.watch_snapshot_status()
|
||||||
|
.map(|status| (&status).into())
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prepare_watch_snapshot(
|
||||||
|
&self,
|
||||||
|
platform_pairing_identity: String,
|
||||||
|
) -> Result<MobileWatchSnapshotTransfer, MobileAuthenticationFfiError> {
|
||||||
|
self.authentication
|
||||||
|
.prepare_watch_snapshot(&platform_pairing_identity)
|
||||||
|
.map(Into::into)
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_watch_snapshot_unavailable(
|
||||||
|
&self,
|
||||||
|
unpaired: bool,
|
||||||
|
detail: String,
|
||||||
|
) -> Result<MobileWatchSnapshotStatus, MobileAuthenticationFfiError> {
|
||||||
|
self.authentication
|
||||||
|
.set_watch_snapshot_unavailable(unpaired, detail)
|
||||||
|
.map(|status| (&status).into())
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fail_watch_snapshot(
|
||||||
|
&self,
|
||||||
|
revision: u64,
|
||||||
|
detail: String,
|
||||||
|
) -> Result<MobileWatchSnapshotStatus, MobileAuthenticationFfiError> {
|
||||||
|
self.authentication
|
||||||
|
.fail_watch_snapshot(revision, detail)
|
||||||
|
.map(|status| (&status).into())
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn acknowledge_watch_snapshot(
|
||||||
|
&self,
|
||||||
|
receipt: Vec<u8>,
|
||||||
|
) -> Result<MobileWatchSnapshotStatus, MobileAuthenticationFfiError> {
|
||||||
|
self.authentication
|
||||||
|
.acknowledge_watch_snapshot(&receipt)
|
||||||
|
.map(|status| (&status).into())
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn begin_create_entry(
|
pub fn begin_create_entry(
|
||||||
&self,
|
&self,
|
||||||
directory: String,
|
directory: String,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ pub mod mobile_mutation;
|
|||||||
pub mod mobile_onboarding;
|
pub mod mobile_onboarding;
|
||||||
pub mod mobile_passwords;
|
pub mod mobile_passwords;
|
||||||
pub mod mobile_totp;
|
pub mod mobile_totp;
|
||||||
|
pub mod mobile_watch;
|
||||||
pub mod mutation;
|
pub mod mutation;
|
||||||
pub mod otp;
|
pub mod otp;
|
||||||
pub mod presentation;
|
pub mod presentation;
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ use crate::{
|
|||||||
mobile_totp::{
|
mobile_totp::{
|
||||||
MobileTotpDetail, MobileTotpError, MobileTotpOperation, MobileTotpPage, MobileTotpService,
|
MobileTotpDetail, MobileTotpError, MobileTotpOperation, MobileTotpPage, MobileTotpService,
|
||||||
},
|
},
|
||||||
|
mobile_watch::{
|
||||||
|
MobileWatchSnapshotState, MobileWatchSnapshotStatus, WatchSnapshotError,
|
||||||
|
WatchSnapshotSender, WatchSnapshotTransfer,
|
||||||
|
},
|
||||||
otp::OtpError,
|
otp::OtpError,
|
||||||
recipient::RecipientPolicyManager,
|
recipient::RecipientPolicyManager,
|
||||||
repository::{
|
repository::{
|
||||||
@@ -139,6 +143,9 @@ pub enum MobileWatchPreferenceState {
|
|||||||
AppNotInstalled,
|
AppNotInstalled,
|
||||||
Ready,
|
Ready,
|
||||||
Pending,
|
Pending,
|
||||||
|
Delivered,
|
||||||
|
Current,
|
||||||
|
Failed,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
@@ -282,6 +289,7 @@ struct MobileAuthenticationStatus {
|
|||||||
mutation_active: bool,
|
mutation_active: bool,
|
||||||
repository_operation_active: bool,
|
repository_operation_active: bool,
|
||||||
watch_shared_totp_entries: std::collections::BTreeSet<EntryPath>,
|
watch_shared_totp_entries: std::collections::BTreeSet<EntryPath>,
|
||||||
|
watch_snapshot: WatchSnapshotSender,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
@@ -338,6 +346,8 @@ impl MobileAuthentication {
|
|||||||
config.authentication_timeout(),
|
config.authentication_timeout(),
|
||||||
)
|
)
|
||||||
.map_err(MobileAuthenticationError::authentication)?;
|
.map_err(MobileAuthenticationError::authentication)?;
|
||||||
|
let watch_snapshot =
|
||||||
|
WatchSnapshotSender::load(config.source().with_file_name("watch-snapshot.toml"));
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
status: Mutex::new(MobileAuthenticationStatus {
|
status: Mutex::new(MobileAuthenticationStatus {
|
||||||
biometric_unlock_enabled: config.biometric_unlock_enabled(),
|
biometric_unlock_enabled: config.biometric_unlock_enabled(),
|
||||||
@@ -350,6 +360,7 @@ impl MobileAuthentication {
|
|||||||
mutation_active: false,
|
mutation_active: false,
|
||||||
repository_operation_active: false,
|
repository_operation_active: false,
|
||||||
watch_shared_totp_entries: config.watch_shared_totp_entries().clone(),
|
watch_shared_totp_entries: config.watch_shared_totp_entries().clone(),
|
||||||
|
watch_snapshot,
|
||||||
}),
|
}),
|
||||||
config,
|
config,
|
||||||
repository,
|
repository,
|
||||||
@@ -564,8 +575,14 @@ impl MobileAuthentication {
|
|||||||
.ok_or_else(|| config_detail("The default GPG key is unavailable."))?;
|
.ok_or_else(|| config_detail("The default GPG key is unavailable."))?;
|
||||||
let status = self.status()?;
|
let status = self.status()?;
|
||||||
let selected = status.watch_shared_totp_entries.len();
|
let selected = status.watch_shared_totp_entries.len();
|
||||||
let (watch_state, watch_title, watch_detail) =
|
let snapshot = status.watch_snapshot.status();
|
||||||
watch_preference(watch_supported, watch_paired, watch_app_installed, selected);
|
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 authentication_timeout_seconds = status.authentication_timeout.duration().as_secs();
|
||||||
let biometric_unlock_enabled = status.biometric_unlock_enabled;
|
let biometric_unlock_enabled = status.biometric_unlock_enabled;
|
||||||
let appearance = status.appearance;
|
let appearance = status.appearance;
|
||||||
@@ -681,7 +698,10 @@ impl MobileAuthentication {
|
|||||||
&cache_path,
|
&cache_path,
|
||||||
)
|
)
|
||||||
.map_err(totp_error)?;
|
.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 {
|
Ok(MobileEntryPresentation {
|
||||||
page: MobileEntryPage::from_document(&document),
|
page: MobileEntryPage::from_document(&document),
|
||||||
totp,
|
totp,
|
||||||
@@ -808,9 +828,11 @@ impl MobileAuthentication {
|
|||||||
};
|
};
|
||||||
let cache_path = self.config.source().with_file_name("totp-catalog.toml");
|
let cache_path = self.config.source().with_file_name("totp-catalog.toml");
|
||||||
let mut provider = KeyOnlyProvider::new(handle, &key);
|
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)
|
.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> {
|
pub fn cached_totp_page(&self) -> Result<Option<MobileTotpPage>, MobileAuthenticationError> {
|
||||||
@@ -823,13 +845,15 @@ impl MobileAuthentication {
|
|||||||
) -> Result<Option<MobileTotpPage>, MobileAuthenticationError> {
|
) -> Result<Option<MobileTotpPage>, MobileAuthenticationError> {
|
||||||
let shared = self.status()?.watch_shared_totp_entries.clone();
|
let shared = self.status()?.watch_shared_totp_entries.clone();
|
||||||
let cache_path = self.config.source().with_file_name("totp-catalog.toml");
|
let cache_path = self.config.source().with_file_name("totp-catalog.toml");
|
||||||
Ok(
|
let mut page = MobileTotpService::new(&self.repository, &self.keys).search_cached_page(
|
||||||
MobileTotpService::new(&self.repository, &self.keys).search_cached_page(
|
&shared,
|
||||||
&shared,
|
&cache_path,
|
||||||
&cache_path,
|
query,
|
||||||
query,
|
);
|
||||||
),
|
if let Some(page) = &mut page {
|
||||||
)
|
page.set_watch(self.watch_snapshot_status()?);
|
||||||
|
}
|
||||||
|
Ok(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn totp_detail(
|
pub fn totp_detail(
|
||||||
@@ -848,9 +872,11 @@ impl MobileAuthentication {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
let mut provider = KeyOnlyProvider::new(handle, &key);
|
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)
|
.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(
|
pub fn copy_totp_code(
|
||||||
@@ -888,6 +914,85 @@ impl MobileAuthentication {
|
|||||||
self.totp_detail(path, unix_seconds)
|
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(
|
pub fn replace_entry_field(
|
||||||
&self,
|
&self,
|
||||||
path: &str,
|
path: &str,
|
||||||
@@ -1430,6 +1535,7 @@ fn watch_preference(
|
|||||||
paired: bool,
|
paired: bool,
|
||||||
app_installed: bool,
|
app_installed: bool,
|
||||||
selected: usize,
|
selected: usize,
|
||||||
|
snapshot: &MobileWatchSnapshotStatus,
|
||||||
) -> (MobileWatchPreferenceState, String, String) {
|
) -> (MobileWatchPreferenceState, String, String) {
|
||||||
if !supported {
|
if !supported {
|
||||||
return (
|
return (
|
||||||
@@ -1452,25 +1558,50 @@ fn watch_preference(
|
|||||||
"Install the IronStorage companion on the paired Apple Watch.".to_owned(),
|
"Install the IronStorage companion on the paired Apple Watch.".to_owned(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if selected == 0 {
|
match snapshot.state() {
|
||||||
return (
|
MobileWatchSnapshotState::Pending => (
|
||||||
MobileWatchPreferenceState::Ready,
|
MobileWatchPreferenceState::Pending,
|
||||||
"Ready".to_owned(),
|
"Synchronization Pending".to_owned(),
|
||||||
"No TOTP codes are selected for Apple Watch.".to_owned(),
|
snapshot.detail().to_owned(),
|
||||||
);
|
|
||||||
}
|
|
||||||
(
|
|
||||||
MobileWatchPreferenceState::Pending,
|
|
||||||
"Synchronization Pending".to_owned(),
|
|
||||||
format!(
|
|
||||||
"{selected} selected TOTP {} pending Apple Watch synchronization.",
|
|
||||||
if selected == 1 {
|
|
||||||
"code is"
|
|
||||||
} else {
|
|
||||||
"codes are"
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
)
|
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 {
|
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 {
|
fn editor_missing() -> MobileAuthenticationError {
|
||||||
entry_detail(
|
entry_detail(
|
||||||
"Entry Draft Is Unavailable",
|
"Entry Draft Is Unavailable",
|
||||||
@@ -1584,7 +1723,8 @@ fn entry_detail(title: &str, error: impl fmt::Display) -> MobileAuthenticationEr
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
MobileRepositoryOperation, MobileRepositoryOperationError, MobileWatchPreferenceState,
|
MobileRepositoryOperation, MobileRepositoryOperationError, MobileWatchPreferenceState,
|
||||||
repository_operation_conflict, watch_preference,
|
MobileWatchSnapshotState, MobileWatchSnapshotStatus, repository_operation_conflict,
|
||||||
|
watch_preference,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1613,15 +1753,20 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn watch_preferences_render_platform_and_selection_state() {
|
fn watch_preferences_render_platform_and_selection_state() {
|
||||||
|
let unavailable = MobileWatchSnapshotStatus {
|
||||||
|
state: MobileWatchSnapshotState::Unavailable,
|
||||||
|
revision: None,
|
||||||
|
detail: "not started".to_owned(),
|
||||||
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
watch_preference(true, false, false, 2).0,
|
watch_preference(true, false, false, 2, &unavailable).0,
|
||||||
MobileWatchPreferenceState::NotPaired
|
MobileWatchPreferenceState::NotPaired
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
watch_preference(true, true, true, 0).0,
|
watch_preference(true, true, true, 0, &unavailable).0,
|
||||||
MobileWatchPreferenceState::Ready
|
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_eq!(pending.0, MobileWatchPreferenceState::Pending);
|
||||||
assert!(pending.2.contains("2 selected TOTP codes"));
|
assert!(pending.2.contains("2 selected TOTP codes"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use sha2::{Digest as _, Sha256};
|
|||||||
use crate::{
|
use crate::{
|
||||||
crypto::{KeyStore, SecretProvider},
|
crypto::{KeyStore, SecretProvider},
|
||||||
document::EntryDocument,
|
document::EntryDocument,
|
||||||
|
mobile_watch::{MobileWatchSnapshotState, MobileWatchSnapshotStatus, WatchSnapshotEntry},
|
||||||
otp::{OtpError, OtpKind, OtpService},
|
otp::{OtpError, OtpKind, OtpService},
|
||||||
repository::{EncryptedEntry, EntryPath, Repository, RepositoryError, SecretBytes},
|
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)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct MobileTotpRow {
|
pub struct MobileTotpRow {
|
||||||
path: String,
|
path: String,
|
||||||
@@ -206,6 +185,10 @@ impl MobileTotpPage {
|
|||||||
pub fn watch(&self) -> &MobileWatchSnapshotStatus {
|
pub fn watch(&self) -> &MobileWatchSnapshotStatus {
|
||||||
&self.watch
|
&self.watch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_watch(&mut self, watch: MobileWatchSnapshotStatus) {
|
||||||
|
self.watch = watch;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MobileTotpDetail {
|
pub struct MobileTotpDetail {
|
||||||
@@ -251,6 +234,21 @@ impl MobileTotpDetail {
|
|||||||
pub fn watch(&self) -> &MobileWatchSnapshotStatus {
|
pub fn watch(&self) -> &MobileWatchSnapshotStatus {
|
||||||
&self.watch
|
&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 {
|
impl fmt::Debug for MobileTotpDetail {
|
||||||
@@ -500,6 +498,40 @@ impl<'a> MobileTotpService<'a> {
|
|||||||
}
|
}
|
||||||
detail_for_uri(&path, uri, unix_seconds, shared)
|
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(
|
fn detail_for_uri(
|
||||||
@@ -755,11 +787,13 @@ fn snapshot_status(selected: usize) -> MobileWatchSnapshotStatus {
|
|||||||
if selected == 0 {
|
if selected == 0 {
|
||||||
MobileWatchSnapshotStatus {
|
MobileWatchSnapshotStatus {
|
||||||
state: MobileWatchSnapshotState::Unavailable,
|
state: MobileWatchSnapshotState::Unavailable,
|
||||||
|
revision: None,
|
||||||
detail: "No TOTP codes are selected for Apple Watch.".to_owned(),
|
detail: "No TOTP codes are selected for Apple Watch.".to_owned(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
MobileWatchSnapshotStatus {
|
MobileWatchSnapshotStatus {
|
||||||
state: MobileWatchSnapshotState::Pending,
|
state: MobileWatchSnapshotState::Pending,
|
||||||
|
revision: None,
|
||||||
detail: format!(
|
detail: format!(
|
||||||
"{selected} selected TOTP {} pending Apple Watch synchronization.",
|
"{selected} selected TOTP {} pending Apple Watch synchronization.",
|
||||||
if selected == 1 {
|
if selected == 1 {
|
||||||
|
|||||||
868
crates/storage/src/mobile_watch.rs
Normal file
868
crates/storage/src/mobile_watch.rs
Normal file
@@ -0,0 +1,868 @@
|
|||||||
|
//! Versioned, replacement-only Apple Watch TOTP snapshots and sender state.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
fmt, fs,
|
||||||
|
io::Write as _,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use cap_std::{ambient_authority, fs::Dir};
|
||||||
|
use cap_tempfile::TempFile;
|
||||||
|
use data_encoding::{HEXLOWER, HEXLOWER_PERMISSIVE};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest as _, Sha256};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
otp::OtpAlgorithm,
|
||||||
|
repository::{EntryPath, SecretBytes},
|
||||||
|
};
|
||||||
|
|
||||||
|
const SNAPSHOT_MAGIC: &[u8; 4] = b"ISWS";
|
||||||
|
const RECEIPT_MAGIC: &[u8; 4] = b"ISWR";
|
||||||
|
const SNAPSHOT_VERSION: u16 = 1;
|
||||||
|
const JOURNAL_VERSION: u32 = 1;
|
||||||
|
const MAX_SNAPSHOT_BYTES: usize = 256 * 1024;
|
||||||
|
const MAX_ENTRIES: usize = 256;
|
||||||
|
const MAX_TEXT_BYTES: usize = 4096;
|
||||||
|
const MAX_SECRET_BYTES: usize = 1024;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum MobileWatchSnapshotState {
|
||||||
|
Unavailable,
|
||||||
|
Unpaired,
|
||||||
|
Pending,
|
||||||
|
Delivered,
|
||||||
|
Current,
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct MobileWatchSnapshotStatus {
|
||||||
|
pub(crate) state: MobileWatchSnapshotState,
|
||||||
|
pub(crate) revision: Option<u64>,
|
||||||
|
pub(crate) detail: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MobileWatchSnapshotStatus {
|
||||||
|
pub fn state(&self) -> MobileWatchSnapshotState {
|
||||||
|
self.state
|
||||||
|
}
|
||||||
|
pub fn revision(&self) -> Option<u64> {
|
||||||
|
self.revision
|
||||||
|
}
|
||||||
|
pub fn detail(&self) -> &str {
|
||||||
|
&self.detail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WatchSnapshotEntry {
|
||||||
|
path: EntryPath,
|
||||||
|
issuer: Option<String>,
|
||||||
|
account: String,
|
||||||
|
algorithm: OtpAlgorithm,
|
||||||
|
digits: u32,
|
||||||
|
period: u64,
|
||||||
|
secret: SecretBytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WatchSnapshotEntry {
|
||||||
|
pub fn new(
|
||||||
|
path: EntryPath,
|
||||||
|
issuer: Option<String>,
|
||||||
|
account: String,
|
||||||
|
algorithm: OtpAlgorithm,
|
||||||
|
digits: u32,
|
||||||
|
period: u64,
|
||||||
|
secret: SecretBytes,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
path,
|
||||||
|
issuer,
|
||||||
|
account,
|
||||||
|
algorithm,
|
||||||
|
digits,
|
||||||
|
period,
|
||||||
|
secret,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &EntryPath {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
pub fn issuer(&self) -> Option<&str> {
|
||||||
|
self.issuer.as_deref()
|
||||||
|
}
|
||||||
|
pub fn account(&self) -> &str {
|
||||||
|
&self.account
|
||||||
|
}
|
||||||
|
pub fn algorithm(&self) -> OtpAlgorithm {
|
||||||
|
self.algorithm
|
||||||
|
}
|
||||||
|
pub fn digits(&self) -> u32 {
|
||||||
|
self.digits
|
||||||
|
}
|
||||||
|
pub fn period(&self) -> u64 {
|
||||||
|
self.period
|
||||||
|
}
|
||||||
|
pub fn secret(&self) -> &SecretBytes {
|
||||||
|
&self.secret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for WatchSnapshotEntry {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
formatter
|
||||||
|
.debug_struct("WatchSnapshotEntry")
|
||||||
|
.field("path", &self.path)
|
||||||
|
.field("issuer", &self.issuer)
|
||||||
|
.field("account", &self.account)
|
||||||
|
.field("algorithm", &self.algorithm)
|
||||||
|
.field("digits", &self.digits)
|
||||||
|
.field("period", &self.period)
|
||||||
|
.field("secret", &"[REDACTED]")
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WatchSnapshot {
|
||||||
|
pairing: [u8; 32],
|
||||||
|
revision: u64,
|
||||||
|
entries: Vec<WatchSnapshotEntry>,
|
||||||
|
digest: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WatchSnapshot {
|
||||||
|
pub fn revision(&self) -> u64 {
|
||||||
|
self.revision
|
||||||
|
}
|
||||||
|
pub fn entries(&self) -> &[WatchSnapshotEntry] {
|
||||||
|
&self.entries
|
||||||
|
}
|
||||||
|
pub fn is_revocation(&self) -> bool {
|
||||||
|
self.entries.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for WatchSnapshot {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
formatter
|
||||||
|
.debug_struct("WatchSnapshot")
|
||||||
|
.field("pairing", &HEXLOWER.encode(&self.pairing))
|
||||||
|
.field("revision", &self.revision)
|
||||||
|
.field("entries", &self.entries)
|
||||||
|
.field("digest", &HEXLOWER.encode(&self.digest))
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WatchSnapshotTransfer {
|
||||||
|
revision: u64,
|
||||||
|
selected_entries: u32,
|
||||||
|
snapshot: SecretBytes,
|
||||||
|
delivered_receipt: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WatchSnapshotTransfer {
|
||||||
|
pub fn revision(&self) -> u64 {
|
||||||
|
self.revision
|
||||||
|
}
|
||||||
|
pub fn selected_entries(&self) -> u32 {
|
||||||
|
self.selected_entries
|
||||||
|
}
|
||||||
|
pub fn snapshot(&self) -> &SecretBytes {
|
||||||
|
&self.snapshot
|
||||||
|
}
|
||||||
|
pub fn delivered_receipt(&self) -> &[u8] {
|
||||||
|
&self.delivered_receipt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for WatchSnapshotTransfer {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
formatter
|
||||||
|
.debug_struct("WatchSnapshotTransfer")
|
||||||
|
.field("revision", &self.revision)
|
||||||
|
.field("selected_entries", &self.selected_entries)
|
||||||
|
.field("snapshot", &"[REDACTED]")
|
||||||
|
.field("delivered_receipt", &"[OPAQUE]")
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum WatchSnapshotApply {
|
||||||
|
Replaced,
|
||||||
|
Revoked,
|
||||||
|
Duplicate,
|
||||||
|
Stale,
|
||||||
|
PairingChanged,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct WatchSnapshotReceiver {
|
||||||
|
current: Option<WatchSnapshot>,
|
||||||
|
accepted: Option<AcceptedSnapshot>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct AcceptedSnapshot {
|
||||||
|
pairing: [u8; 32],
|
||||||
|
revision: u64,
|
||||||
|
digest: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WatchSnapshotReceiver {
|
||||||
|
pub fn apply(&mut self, bytes: SecretBytes) -> Result<WatchSnapshotApply, WatchSnapshotError> {
|
||||||
|
let snapshot = match decode_snapshot(bytes.expose()) {
|
||||||
|
Ok(snapshot) => snapshot,
|
||||||
|
Err(error) => {
|
||||||
|
self.current = None;
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut pairing_changed = false;
|
||||||
|
if let Some(accepted) = self.accepted {
|
||||||
|
if snapshot.revision < accepted.revision {
|
||||||
|
return Ok(WatchSnapshotApply::Stale);
|
||||||
|
} else if snapshot.revision == accepted.revision {
|
||||||
|
if snapshot.pairing != accepted.pairing || snapshot.digest != accepted.digest {
|
||||||
|
self.current = None;
|
||||||
|
return Err(WatchSnapshotError::RevisionConflict);
|
||||||
|
}
|
||||||
|
if self.current.is_some() {
|
||||||
|
return Ok(WatchSnapshotApply::Duplicate);
|
||||||
|
}
|
||||||
|
} else if accepted.pairing != snapshot.pairing {
|
||||||
|
pairing_changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let accepted = AcceptedSnapshot {
|
||||||
|
pairing: snapshot.pairing,
|
||||||
|
revision: snapshot.revision,
|
||||||
|
digest: snapshot.digest,
|
||||||
|
};
|
||||||
|
let result = if pairing_changed {
|
||||||
|
WatchSnapshotApply::PairingChanged
|
||||||
|
} else if snapshot.is_revocation() {
|
||||||
|
WatchSnapshotApply::Revoked
|
||||||
|
} else {
|
||||||
|
WatchSnapshotApply::Replaced
|
||||||
|
};
|
||||||
|
self.accepted = Some(accepted);
|
||||||
|
self.current = Some(snapshot);
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current(&self) -> Option<&WatchSnapshot> {
|
||||||
|
self.current.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_receipt(&self) -> Option<Vec<u8>> {
|
||||||
|
self.current.as_ref().map(|snapshot| {
|
||||||
|
encode_receipt(
|
||||||
|
ReceiptKind::Current,
|
||||||
|
snapshot.pairing,
|
||||||
|
snapshot.revision,
|
||||||
|
snapshot.digest,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn revoke(&mut self) {
|
||||||
|
self.current = None;
|
||||||
|
self.accepted = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WatchSnapshotSender {
|
||||||
|
path: PathBuf,
|
||||||
|
journal: SenderJournal,
|
||||||
|
status: MobileWatchSnapshotStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WatchSnapshotSender {
|
||||||
|
pub fn load(path: PathBuf) -> Self {
|
||||||
|
let journal = load_journal(&path).unwrap_or_default();
|
||||||
|
let status = status_from_journal(&journal);
|
||||||
|
Self {
|
||||||
|
path,
|
||||||
|
journal,
|
||||||
|
status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn status(&self) -> MobileWatchSnapshotStatus {
|
||||||
|
self.status.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unavailable(
|
||||||
|
&mut self,
|
||||||
|
unpaired: bool,
|
||||||
|
detail: impl Into<String>,
|
||||||
|
) -> Result<(), WatchSnapshotError> {
|
||||||
|
if unpaired {
|
||||||
|
self.journal.pairing = None;
|
||||||
|
self.journal.digest = None;
|
||||||
|
self.journal.snapshot_digest = None;
|
||||||
|
self.journal.delivered_revision = None;
|
||||||
|
self.journal.current_revision = None;
|
||||||
|
save_journal(&self.path, &self.journal)?;
|
||||||
|
}
|
||||||
|
self.status = MobileWatchSnapshotStatus {
|
||||||
|
state: if unpaired {
|
||||||
|
MobileWatchSnapshotState::Unpaired
|
||||||
|
} else {
|
||||||
|
MobileWatchSnapshotState::Unavailable
|
||||||
|
},
|
||||||
|
revision: None,
|
||||||
|
detail: detail.into(),
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prepare(
|
||||||
|
&mut self,
|
||||||
|
platform_pairing_identity: &str,
|
||||||
|
entries: Vec<WatchSnapshotEntry>,
|
||||||
|
) -> Result<WatchSnapshotTransfer, WatchSnapshotError> {
|
||||||
|
if platform_pairing_identity.trim().is_empty() {
|
||||||
|
return Err(WatchSnapshotError::InvalidPairing);
|
||||||
|
}
|
||||||
|
let pairing = digest(platform_pairing_identity.as_bytes());
|
||||||
|
let content = encode_entries(&entries)?;
|
||||||
|
let content_digest = digest(&content);
|
||||||
|
let pairing_text = HEXLOWER.encode(&pairing);
|
||||||
|
let digest_text = HEXLOWER.encode(&content_digest);
|
||||||
|
if self.journal.pairing.as_deref() != Some(&pairing_text)
|
||||||
|
|| self.journal.digest.as_deref() != Some(&digest_text)
|
||||||
|
{
|
||||||
|
self.journal.revision = self
|
||||||
|
.journal
|
||||||
|
.revision
|
||||||
|
.checked_add(1)
|
||||||
|
.ok_or(WatchSnapshotError::RevisionOverflow)?;
|
||||||
|
self.journal.pairing = Some(pairing_text);
|
||||||
|
self.journal.digest = Some(digest_text);
|
||||||
|
self.journal.delivered_revision = None;
|
||||||
|
self.journal.current_revision = None;
|
||||||
|
save_journal(&self.path, &self.journal)?;
|
||||||
|
}
|
||||||
|
if self.journal.revision == 0 {
|
||||||
|
self.journal.revision = 1;
|
||||||
|
save_journal(&self.path, &self.journal)?;
|
||||||
|
}
|
||||||
|
let snapshot = encode_snapshot(pairing, self.journal.revision, &content)?;
|
||||||
|
let snapshot_digest = digest(snapshot.expose());
|
||||||
|
self.journal.snapshot_digest = Some(HEXLOWER.encode(&snapshot_digest));
|
||||||
|
save_journal(&self.path, &self.journal)?;
|
||||||
|
let receipt = encode_receipt(
|
||||||
|
ReceiptKind::Delivered,
|
||||||
|
pairing,
|
||||||
|
self.journal.revision,
|
||||||
|
snapshot_digest,
|
||||||
|
);
|
||||||
|
let selected_entries =
|
||||||
|
u32::try_from(entries.len()).map_err(|_| WatchSnapshotError::TooManyEntries)?;
|
||||||
|
self.status = MobileWatchSnapshotStatus {
|
||||||
|
state: MobileWatchSnapshotState::Pending,
|
||||||
|
revision: Some(self.journal.revision),
|
||||||
|
detail: snapshot_detail("pending delivery", selected_entries, self.journal.revision),
|
||||||
|
};
|
||||||
|
Ok(WatchSnapshotTransfer {
|
||||||
|
revision: self.journal.revision,
|
||||||
|
selected_entries,
|
||||||
|
snapshot,
|
||||||
|
delivered_receipt: receipt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn failed(&mut self, revision: u64, detail: impl Into<String>) {
|
||||||
|
if revision == self.journal.revision {
|
||||||
|
self.status = MobileWatchSnapshotStatus {
|
||||||
|
state: MobileWatchSnapshotState::Failed,
|
||||||
|
revision: Some(revision),
|
||||||
|
detail: detail.into(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn acknowledge(
|
||||||
|
&mut self,
|
||||||
|
receipt: &[u8],
|
||||||
|
) -> Result<MobileWatchSnapshotStatus, WatchSnapshotError> {
|
||||||
|
let receipt = decode_receipt(receipt)?;
|
||||||
|
let pairing = decode_hash(
|
||||||
|
self.journal
|
||||||
|
.pairing
|
||||||
|
.as_deref()
|
||||||
|
.ok_or(WatchSnapshotError::NoPendingSnapshot)?,
|
||||||
|
)?;
|
||||||
|
if receipt.pairing != pairing || receipt.revision != self.journal.revision {
|
||||||
|
return Ok(self.status());
|
||||||
|
}
|
||||||
|
let expected_snapshot_digest = decode_hash(
|
||||||
|
self.journal
|
||||||
|
.snapshot_digest
|
||||||
|
.as_deref()
|
||||||
|
.ok_or(WatchSnapshotError::NoPendingSnapshot)?,
|
||||||
|
)?;
|
||||||
|
if receipt._digest != expected_snapshot_digest {
|
||||||
|
return Err(WatchSnapshotError::InvalidReceipt);
|
||||||
|
}
|
||||||
|
if receipt.kind == ReceiptKind::Delivered {
|
||||||
|
self.journal.delivered_revision = Some(receipt.revision);
|
||||||
|
self.status = MobileWatchSnapshotStatus {
|
||||||
|
state: MobileWatchSnapshotState::Delivered,
|
||||||
|
revision: Some(receipt.revision),
|
||||||
|
detail: format!(
|
||||||
|
"Snapshot revision {} reached the paired Apple Watch.",
|
||||||
|
receipt.revision
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
self.journal.current_revision = Some(receipt.revision);
|
||||||
|
self.status = MobileWatchSnapshotStatus {
|
||||||
|
state: MobileWatchSnapshotState::Current,
|
||||||
|
revision: Some(receipt.revision),
|
||||||
|
detail: format!(
|
||||||
|
"Apple Watch is current at snapshot revision {}.",
|
||||||
|
receipt.revision
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
save_journal(&self.path, &self.journal)?;
|
||||||
|
Ok(self.status())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct SenderJournal {
|
||||||
|
#[serde(default = "journal_version")]
|
||||||
|
version: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pairing: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
revision: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
digest: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
snapshot_digest: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
delivered_revision: Option<u64>,
|
||||||
|
#[serde(default)]
|
||||||
|
current_revision: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SenderJournal {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
version: JOURNAL_VERSION,
|
||||||
|
pairing: None,
|
||||||
|
revision: 0,
|
||||||
|
digest: None,
|
||||||
|
snapshot_digest: None,
|
||||||
|
delivered_revision: None,
|
||||||
|
current_revision: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn journal_version() -> u32 {
|
||||||
|
JOURNAL_VERSION
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
enum ReceiptKind {
|
||||||
|
Delivered = 1,
|
||||||
|
Current = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Receipt {
|
||||||
|
kind: ReceiptKind,
|
||||||
|
pairing: [u8; 32],
|
||||||
|
revision: u64,
|
||||||
|
_digest: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_entries(entries: &[WatchSnapshotEntry]) -> Result<Vec<u8>, WatchSnapshotError> {
|
||||||
|
if entries.len() > MAX_ENTRIES {
|
||||||
|
return Err(WatchSnapshotError::TooManyEntries);
|
||||||
|
}
|
||||||
|
let mut output = Vec::new();
|
||||||
|
put_u32(
|
||||||
|
&mut output,
|
||||||
|
u32::try_from(entries.len()).map_err(|_| WatchSnapshotError::TooManyEntries)?,
|
||||||
|
);
|
||||||
|
for entry in entries {
|
||||||
|
put_text(&mut output, &entry.path.to_string())?;
|
||||||
|
match &entry.issuer {
|
||||||
|
Some(issuer) => {
|
||||||
|
output.push(1);
|
||||||
|
put_text(&mut output, issuer)?;
|
||||||
|
}
|
||||||
|
None => output.push(0),
|
||||||
|
}
|
||||||
|
put_text(&mut output, &entry.account)?;
|
||||||
|
output.push(match entry.algorithm {
|
||||||
|
OtpAlgorithm::Sha1 => 1,
|
||||||
|
OtpAlgorithm::Sha256 => 2,
|
||||||
|
OtpAlgorithm::Sha512 => 3,
|
||||||
|
});
|
||||||
|
output.push(u8::try_from(entry.digits).map_err(|_| WatchSnapshotError::InvalidEntry)?);
|
||||||
|
put_u64(&mut output, entry.period);
|
||||||
|
put_bytes(&mut output, entry.secret.expose(), MAX_SECRET_BYTES)?;
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_snapshot(
|
||||||
|
pairing: [u8; 32],
|
||||||
|
revision: u64,
|
||||||
|
content: &[u8],
|
||||||
|
) -> Result<SecretBytes, WatchSnapshotError> {
|
||||||
|
let mut output = Vec::with_capacity(4 + 2 + 32 + 8 + content.len() + 32);
|
||||||
|
output.extend_from_slice(SNAPSHOT_MAGIC);
|
||||||
|
output.extend_from_slice(&SNAPSHOT_VERSION.to_be_bytes());
|
||||||
|
output.extend_from_slice(&pairing);
|
||||||
|
put_u64(&mut output, revision);
|
||||||
|
output.extend_from_slice(content);
|
||||||
|
let checksum = digest(&output);
|
||||||
|
output.extend_from_slice(&checksum);
|
||||||
|
if output.len() > MAX_SNAPSHOT_BYTES {
|
||||||
|
return Err(WatchSnapshotError::SnapshotTooLarge);
|
||||||
|
}
|
||||||
|
Ok(SecretBytes::new(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_snapshot(bytes: &[u8]) -> Result<WatchSnapshot, WatchSnapshotError> {
|
||||||
|
if bytes.len() > MAX_SNAPSHOT_BYTES || bytes.len() < 4 + 2 + 32 + 8 + 4 + 32 {
|
||||||
|
return Err(WatchSnapshotError::InvalidSnapshot);
|
||||||
|
}
|
||||||
|
let (payload, checksum) = bytes.split_at(bytes.len() - 32);
|
||||||
|
if digest(payload).as_slice() != checksum {
|
||||||
|
return Err(WatchSnapshotError::InvalidChecksum);
|
||||||
|
}
|
||||||
|
let mut input = payload;
|
||||||
|
if take(&mut input, 4)? != SNAPSHOT_MAGIC || take_u16(&mut input)? != SNAPSHOT_VERSION {
|
||||||
|
return Err(WatchSnapshotError::UnsupportedVersion);
|
||||||
|
}
|
||||||
|
let pairing: [u8; 32] = take(&mut input, 32)?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| WatchSnapshotError::InvalidSnapshot)?;
|
||||||
|
let revision = take_u64(&mut input)?;
|
||||||
|
if revision == 0 {
|
||||||
|
return Err(WatchSnapshotError::InvalidSnapshot);
|
||||||
|
}
|
||||||
|
let count =
|
||||||
|
usize::try_from(take_u32(&mut input)?).map_err(|_| WatchSnapshotError::TooManyEntries)?;
|
||||||
|
if count > MAX_ENTRIES {
|
||||||
|
return Err(WatchSnapshotError::TooManyEntries);
|
||||||
|
}
|
||||||
|
let mut entries = Vec::with_capacity(count);
|
||||||
|
for _ in 0..count {
|
||||||
|
let path = EntryPath::parse(&take_text(&mut input)?)
|
||||||
|
.map_err(|_| WatchSnapshotError::InvalidEntry)?;
|
||||||
|
let issuer = match take_u8(&mut input)? {
|
||||||
|
0 => None,
|
||||||
|
1 => Some(take_text(&mut input)?),
|
||||||
|
_ => return Err(WatchSnapshotError::InvalidEntry),
|
||||||
|
};
|
||||||
|
let account = take_text(&mut input)?;
|
||||||
|
if account.is_empty() {
|
||||||
|
return Err(WatchSnapshotError::InvalidEntry);
|
||||||
|
}
|
||||||
|
let algorithm = match take_u8(&mut input)? {
|
||||||
|
1 => OtpAlgorithm::Sha1,
|
||||||
|
2 => OtpAlgorithm::Sha256,
|
||||||
|
3 => OtpAlgorithm::Sha512,
|
||||||
|
_ => return Err(WatchSnapshotError::InvalidEntry),
|
||||||
|
};
|
||||||
|
let digits = u32::from(take_u8(&mut input)?);
|
||||||
|
if !matches!(digits, 6 | 8) {
|
||||||
|
return Err(WatchSnapshotError::InvalidEntry);
|
||||||
|
}
|
||||||
|
let period = take_u64(&mut input)?;
|
||||||
|
if period == 0 {
|
||||||
|
return Err(WatchSnapshotError::InvalidEntry);
|
||||||
|
}
|
||||||
|
let secret = take_bytes(&mut input, MAX_SECRET_BYTES)?;
|
||||||
|
if secret.is_empty() {
|
||||||
|
return Err(WatchSnapshotError::InvalidEntry);
|
||||||
|
}
|
||||||
|
entries.push(WatchSnapshotEntry::new(
|
||||||
|
path,
|
||||||
|
issuer,
|
||||||
|
account,
|
||||||
|
algorithm,
|
||||||
|
digits,
|
||||||
|
period,
|
||||||
|
SecretBytes::new(secret),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !input.is_empty() {
|
||||||
|
return Err(WatchSnapshotError::InvalidSnapshot);
|
||||||
|
}
|
||||||
|
Ok(WatchSnapshot {
|
||||||
|
pairing,
|
||||||
|
revision,
|
||||||
|
entries,
|
||||||
|
digest: digest(bytes),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_receipt(
|
||||||
|
kind: ReceiptKind,
|
||||||
|
pairing: [u8; 32],
|
||||||
|
revision: u64,
|
||||||
|
snapshot_digest: [u8; 32],
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut output = Vec::with_capacity(79);
|
||||||
|
output.extend_from_slice(RECEIPT_MAGIC);
|
||||||
|
output.extend_from_slice(&SNAPSHOT_VERSION.to_be_bytes());
|
||||||
|
output.push(kind as u8);
|
||||||
|
output.extend_from_slice(&pairing);
|
||||||
|
put_u64(&mut output, revision);
|
||||||
|
output.extend_from_slice(&snapshot_digest);
|
||||||
|
let checksum = digest(&output);
|
||||||
|
output.extend_from_slice(&checksum);
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_receipt(bytes: &[u8]) -> Result<Receipt, WatchSnapshotError> {
|
||||||
|
if bytes.len() != 4 + 2 + 1 + 32 + 8 + 32 + 32 {
|
||||||
|
return Err(WatchSnapshotError::InvalidReceipt);
|
||||||
|
}
|
||||||
|
let (payload, checksum) = bytes.split_at(bytes.len() - 32);
|
||||||
|
if digest(payload).as_slice() != checksum {
|
||||||
|
return Err(WatchSnapshotError::InvalidReceipt);
|
||||||
|
}
|
||||||
|
let mut input = payload;
|
||||||
|
if take(&mut input, 4)? != RECEIPT_MAGIC || take_u16(&mut input)? != SNAPSHOT_VERSION {
|
||||||
|
return Err(WatchSnapshotError::InvalidReceipt);
|
||||||
|
}
|
||||||
|
let kind = match take_u8(&mut input)? {
|
||||||
|
1 => ReceiptKind::Delivered,
|
||||||
|
2 => ReceiptKind::Current,
|
||||||
|
_ => return Err(WatchSnapshotError::InvalidReceipt),
|
||||||
|
};
|
||||||
|
let pairing = take(&mut input, 32)?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| WatchSnapshotError::InvalidReceipt)?;
|
||||||
|
let revision = take_u64(&mut input)?;
|
||||||
|
let digest = take(&mut input, 32)?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| WatchSnapshotError::InvalidReceipt)?;
|
||||||
|
Ok(Receipt {
|
||||||
|
kind,
|
||||||
|
pairing,
|
||||||
|
revision,
|
||||||
|
_digest: digest,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status_from_journal(journal: &SenderJournal) -> MobileWatchSnapshotStatus {
|
||||||
|
let (state, revision, detail) = if let Some(revision) = journal.current_revision {
|
||||||
|
(
|
||||||
|
MobileWatchSnapshotState::Current,
|
||||||
|
Some(revision),
|
||||||
|
format!("Apple Watch is current at snapshot revision {revision}."),
|
||||||
|
)
|
||||||
|
} else if let Some(revision) = journal.delivered_revision {
|
||||||
|
(
|
||||||
|
MobileWatchSnapshotState::Delivered,
|
||||||
|
Some(revision),
|
||||||
|
format!("Snapshot revision {revision} reached the paired Apple Watch."),
|
||||||
|
)
|
||||||
|
} else if journal.pairing.is_some() && journal.revision > 0 {
|
||||||
|
(
|
||||||
|
MobileWatchSnapshotState::Pending,
|
||||||
|
Some(journal.revision),
|
||||||
|
format!(
|
||||||
|
"Snapshot revision {} is pending Apple Watch delivery.",
|
||||||
|
journal.revision
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
MobileWatchSnapshotState::Unavailable,
|
||||||
|
None,
|
||||||
|
"Apple Watch synchronization has not started.".to_owned(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
MobileWatchSnapshotStatus {
|
||||||
|
state,
|
||||||
|
revision,
|
||||||
|
detail,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot_detail(state: &str, count: u32, revision: u64) -> String {
|
||||||
|
format!(
|
||||||
|
"Snapshot revision {revision} with {count} selected TOTP {} is {state}.",
|
||||||
|
if count == 1 { "entry" } else { "entries" }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_journal(path: &Path) -> Option<SenderJournal> {
|
||||||
|
let metadata = fs::symlink_metadata(path).ok()?;
|
||||||
|
if metadata.file_type().is_symlink() || !metadata.is_file() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let journal = toml::from_str::<SenderJournal>(&fs::read_to_string(path).ok()?).ok()?;
|
||||||
|
(journal.version == JOURNAL_VERSION).then_some(journal)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_journal(path: &Path, journal: &SenderJournal) -> Result<(), WatchSnapshotError> {
|
||||||
|
let parent = path.parent().ok_or(WatchSnapshotError::JournalWrite)?;
|
||||||
|
fs::create_dir_all(parent).map_err(|_| WatchSnapshotError::JournalWrite)?;
|
||||||
|
let directory = Dir::open_ambient_dir(parent, ambient_authority())
|
||||||
|
.map_err(|_| WatchSnapshotError::JournalWrite)?;
|
||||||
|
let name = path.file_name().ok_or(WatchSnapshotError::JournalWrite)?;
|
||||||
|
if let Ok(metadata) = directory.symlink_metadata(name)
|
||||||
|
&& (metadata.file_type().is_symlink() || !metadata.is_file())
|
||||||
|
{
|
||||||
|
return Err(WatchSnapshotError::JournalWrite);
|
||||||
|
}
|
||||||
|
let serialized = toml::to_string(journal).map_err(|_| WatchSnapshotError::JournalWrite)?;
|
||||||
|
let mut temporary = TempFile::new(&directory).map_err(|_| WatchSnapshotError::JournalWrite)?;
|
||||||
|
set_private_permissions(&temporary)?;
|
||||||
|
temporary
|
||||||
|
.write_all(serialized.as_bytes())
|
||||||
|
.and_then(|()| temporary.as_file().sync_all())
|
||||||
|
.and_then(|()| temporary.replace(name))
|
||||||
|
.and_then(|()| directory.open(".").and_then(|file| file.sync_all()))
|
||||||
|
.map_err(|_| WatchSnapshotError::JournalWrite)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn set_private_permissions(temporary: &TempFile<'_>) -> Result<(), WatchSnapshotError> {
|
||||||
|
use cap_std::fs::{Permissions, PermissionsExt as _};
|
||||||
|
temporary
|
||||||
|
.as_file()
|
||||||
|
.set_permissions(Permissions::from_mode(0o600))
|
||||||
|
.map_err(|_| WatchSnapshotError::JournalWrite)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
fn set_private_permissions(_temporary: &TempFile<'_>) -> Result<(), WatchSnapshotError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn digest(bytes: &[u8]) -> [u8; 32] {
|
||||||
|
Sha256::digest(bytes).into()
|
||||||
|
}
|
||||||
|
fn decode_hash(text: &str) -> Result<[u8; 32], WatchSnapshotError> {
|
||||||
|
let bytes = HEXLOWER_PERMISSIVE
|
||||||
|
.decode(text.as_bytes())
|
||||||
|
.map_err(|_| WatchSnapshotError::InvalidJournal)?;
|
||||||
|
bytes
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| WatchSnapshotError::InvalidJournal)
|
||||||
|
}
|
||||||
|
fn put_u32(output: &mut Vec<u8>, value: u32) {
|
||||||
|
output.extend_from_slice(&value.to_be_bytes());
|
||||||
|
}
|
||||||
|
fn put_u64(output: &mut Vec<u8>, value: u64) {
|
||||||
|
output.extend_from_slice(&value.to_be_bytes());
|
||||||
|
}
|
||||||
|
fn put_text(output: &mut Vec<u8>, value: &str) -> Result<(), WatchSnapshotError> {
|
||||||
|
put_bytes(output, value.as_bytes(), MAX_TEXT_BYTES)
|
||||||
|
}
|
||||||
|
fn put_bytes(output: &mut Vec<u8>, value: &[u8], max: usize) -> Result<(), WatchSnapshotError> {
|
||||||
|
if value.len() > max {
|
||||||
|
return Err(WatchSnapshotError::InvalidEntry);
|
||||||
|
}
|
||||||
|
put_u32(
|
||||||
|
output,
|
||||||
|
u32::try_from(value.len()).map_err(|_| WatchSnapshotError::InvalidEntry)?,
|
||||||
|
);
|
||||||
|
output.extend_from_slice(value);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn take<'a>(input: &mut &'a [u8], count: usize) -> Result<&'a [u8], WatchSnapshotError> {
|
||||||
|
if input.len() < count {
|
||||||
|
return Err(WatchSnapshotError::InvalidSnapshot);
|
||||||
|
}
|
||||||
|
let (value, remainder) = input.split_at(count);
|
||||||
|
*input = remainder;
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
fn take_u8(input: &mut &[u8]) -> Result<u8, WatchSnapshotError> {
|
||||||
|
Ok(take(input, 1)?[0])
|
||||||
|
}
|
||||||
|
fn take_u16(input: &mut &[u8]) -> Result<u16, WatchSnapshotError> {
|
||||||
|
Ok(u16::from_be_bytes(
|
||||||
|
take(input, 2)?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| WatchSnapshotError::InvalidSnapshot)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
fn take_u32(input: &mut &[u8]) -> Result<u32, WatchSnapshotError> {
|
||||||
|
Ok(u32::from_be_bytes(
|
||||||
|
take(input, 4)?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| WatchSnapshotError::InvalidSnapshot)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
fn take_u64(input: &mut &[u8]) -> Result<u64, WatchSnapshotError> {
|
||||||
|
Ok(u64::from_be_bytes(
|
||||||
|
take(input, 8)?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| WatchSnapshotError::InvalidSnapshot)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
fn take_bytes(input: &mut &[u8], max: usize) -> Result<Vec<u8>, WatchSnapshotError> {
|
||||||
|
let count = usize::try_from(take_u32(input)?).map_err(|_| WatchSnapshotError::InvalidEntry)?;
|
||||||
|
if count > max {
|
||||||
|
return Err(WatchSnapshotError::InvalidEntry);
|
||||||
|
}
|
||||||
|
Ok(take(input, count)?.to_vec())
|
||||||
|
}
|
||||||
|
fn take_text(input: &mut &[u8]) -> Result<String, WatchSnapshotError> {
|
||||||
|
String::from_utf8(take_bytes(input, MAX_TEXT_BYTES)?)
|
||||||
|
.map_err(|_| WatchSnapshotError::InvalidEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum WatchSnapshotError {
|
||||||
|
InvalidPairing,
|
||||||
|
TooManyEntries,
|
||||||
|
InvalidEntry,
|
||||||
|
SnapshotTooLarge,
|
||||||
|
InvalidSnapshot,
|
||||||
|
InvalidChecksum,
|
||||||
|
UnsupportedVersion,
|
||||||
|
RevisionConflict,
|
||||||
|
RevisionOverflow,
|
||||||
|
InvalidReceipt,
|
||||||
|
NoPendingSnapshot,
|
||||||
|
InvalidJournal,
|
||||||
|
JournalWrite,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for WatchSnapshotError {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
formatter.write_str(match self {
|
||||||
|
Self::InvalidPairing => "Apple Watch pairing identity is invalid",
|
||||||
|
Self::TooManyEntries => "too many TOTP entries are selected for Apple Watch",
|
||||||
|
Self::InvalidEntry => "an Apple Watch TOTP entry is invalid",
|
||||||
|
Self::SnapshotTooLarge => "the Apple Watch snapshot is too large",
|
||||||
|
Self::InvalidSnapshot => "the Apple Watch snapshot is invalid",
|
||||||
|
Self::InvalidChecksum => "the Apple Watch snapshot is damaged",
|
||||||
|
Self::UnsupportedVersion => "the Apple Watch snapshot version is unsupported",
|
||||||
|
Self::RevisionConflict => {
|
||||||
|
"the Apple Watch snapshot revision conflicts with different content"
|
||||||
|
}
|
||||||
|
Self::RevisionOverflow => "the Apple Watch snapshot revision is exhausted",
|
||||||
|
Self::InvalidReceipt => "the Apple Watch delivery receipt is invalid",
|
||||||
|
Self::NoPendingSnapshot => "there is no pending Apple Watch snapshot",
|
||||||
|
Self::InvalidJournal => "the Apple Watch synchronization journal is invalid",
|
||||||
|
Self::JournalWrite => "the Apple Watch synchronization journal could not be saved",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for WatchSnapshotError {}
|
||||||
@@ -229,6 +229,10 @@ impl OtpUri {
|
|||||||
self.period
|
self.period
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn watch_secret(&self) -> SecretBytes {
|
||||||
|
SecretBytes::new(self.secret.expose().to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn counter(&self) -> Option<u64> {
|
pub fn counter(&self) -> Option<u64> {
|
||||||
self.counter
|
self.counter
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ use ironstorage::{
|
|||||||
document::EntryDocumentService,
|
document::EntryDocumentService,
|
||||||
mobile_totp::{
|
mobile_totp::{
|
||||||
MobileTotpDiscoveryPhase, MobileTotpError, MobileTotpOperation, MobileTotpService,
|
MobileTotpDiscoveryPhase, MobileTotpError, MobileTotpOperation, MobileTotpService,
|
||||||
MobileWatchSnapshotState,
|
|
||||||
},
|
},
|
||||||
|
mobile_watch::MobileWatchSnapshotState,
|
||||||
otp::OtpError,
|
otp::OtpError,
|
||||||
recipient::RecipientPolicyManager,
|
recipient::RecipientPolicyManager,
|
||||||
repository::{EncryptedEntry, EntryPath, Repository, SecretBytes},
|
repository::{EncryptedEntry, EntryPath, Repository, SecretBytes},
|
||||||
|
|||||||
167
crates/storage/tests/mobile_watch.rs
Normal file
167
crates/storage/tests/mobile_watch.rs
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use ironstorage::{
|
||||||
|
mobile_watch::{
|
||||||
|
MobileWatchSnapshotState, WatchSnapshotApply, WatchSnapshotEntry, WatchSnapshotReceiver,
|
||||||
|
WatchSnapshotSender,
|
||||||
|
},
|
||||||
|
otp::OtpAlgorithm,
|
||||||
|
repository::{EntryPath, SecretBytes},
|
||||||
|
};
|
||||||
|
|
||||||
|
type TestResult = Result<(), Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
fn entry(path: &str, issuer: &str, account: &str, secret: &[u8]) -> WatchSnapshotEntry {
|
||||||
|
WatchSnapshotEntry::new(
|
||||||
|
EntryPath::parse(path).expect("fixture path"),
|
||||||
|
Some(issuer.to_owned()),
|
||||||
|
account.to_owned(),
|
||||||
|
OtpAlgorithm::Sha256,
|
||||||
|
8,
|
||||||
|
30,
|
||||||
|
SecretBytes::new(secret.to_vec()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replacement_snapshots_reject_replays_conflicts_and_pairing_changes() -> TestResult {
|
||||||
|
let directory = tempfile::tempdir()?;
|
||||||
|
let journal = directory.path().join("watch-snapshot.toml");
|
||||||
|
let mut sender = WatchSnapshotSender::load(journal.clone());
|
||||||
|
let first = sender.prepare(
|
||||||
|
"paired-watch-a",
|
||||||
|
vec![entry("otp/alice", "Acme", "alice", b"first-secret")],
|
||||||
|
)?;
|
||||||
|
assert_eq!(first.revision(), 1);
|
||||||
|
assert_eq!(sender.status().state(), MobileWatchSnapshotState::Pending);
|
||||||
|
|
||||||
|
let mut receiver = WatchSnapshotReceiver::default();
|
||||||
|
assert_eq!(
|
||||||
|
receiver.apply(SecretBytes::new(first.snapshot().expose().to_vec()))?,
|
||||||
|
WatchSnapshotApply::Replaced
|
||||||
|
);
|
||||||
|
assert_eq!(receiver.current().expect("snapshot").entries().len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
sender.acknowledge(first.delivered_receipt())?.state(),
|
||||||
|
MobileWatchSnapshotState::Delivered
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
sender
|
||||||
|
.acknowledge(&receiver.current_receipt().expect("accepted receipt"))?
|
||||||
|
.state(),
|
||||||
|
MobileWatchSnapshotState::Current
|
||||||
|
);
|
||||||
|
|
||||||
|
let duplicate = sender.prepare(
|
||||||
|
"paired-watch-a",
|
||||||
|
vec![entry("otp/alice", "Acme", "alice", b"first-secret")],
|
||||||
|
)?;
|
||||||
|
assert_eq!(duplicate.revision(), 1);
|
||||||
|
assert_eq!(duplicate.snapshot().expose(), first.snapshot().expose());
|
||||||
|
assert_eq!(
|
||||||
|
receiver.apply(SecretBytes::new(duplicate.snapshot().expose().to_vec()))?,
|
||||||
|
WatchSnapshotApply::Duplicate
|
||||||
|
);
|
||||||
|
|
||||||
|
let replacement = sender.prepare(
|
||||||
|
"paired-watch-a",
|
||||||
|
vec![entry("otp/bob", "Acme", "bob", b"second-secret")],
|
||||||
|
)?;
|
||||||
|
assert_eq!(replacement.revision(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
receiver.apply(SecretBytes::new(replacement.snapshot().expose().to_vec()))?,
|
||||||
|
WatchSnapshotApply::Replaced
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
receiver.current().expect("replacement").entries()[0].account(),
|
||||||
|
"bob"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
receiver.apply(SecretBytes::new(first.snapshot().expose().to_vec()))?,
|
||||||
|
WatchSnapshotApply::Stale
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
receiver.current().expect("stale ignored").entries()[0].account(),
|
||||||
|
"bob"
|
||||||
|
);
|
||||||
|
|
||||||
|
let revoked = sender.prepare("paired-watch-a", Vec::new())?;
|
||||||
|
assert_eq!(revoked.revision(), 3);
|
||||||
|
assert_eq!(
|
||||||
|
receiver.apply(SecretBytes::new(revoked.snapshot().expose().to_vec()))?,
|
||||||
|
WatchSnapshotApply::Revoked
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
receiver
|
||||||
|
.current()
|
||||||
|
.expect("revocation marker")
|
||||||
|
.is_revocation()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
receiver.apply(SecretBytes::new(replacement.snapshot().expose().to_vec()))?,
|
||||||
|
WatchSnapshotApply::Stale
|
||||||
|
);
|
||||||
|
|
||||||
|
let changed_watch = sender.prepare(
|
||||||
|
"paired-watch-b",
|
||||||
|
vec![entry("otp/carol", "Acme", "carol", b"third-secret")],
|
||||||
|
)?;
|
||||||
|
assert_eq!(changed_watch.revision(), 4);
|
||||||
|
assert_eq!(
|
||||||
|
receiver.apply(SecretBytes::new(changed_watch.snapshot().expose().to_vec()))?,
|
||||||
|
WatchSnapshotApply::PairingChanged
|
||||||
|
);
|
||||||
|
assert_eq!(receiver.current().expect("new pairing").revision(), 4);
|
||||||
|
let mut fresh_watch = WatchSnapshotReceiver::default();
|
||||||
|
assert_eq!(
|
||||||
|
fresh_watch.apply(SecretBytes::new(changed_watch.snapshot().expose().to_vec()))?,
|
||||||
|
WatchSnapshotApply::Replaced
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut damaged = changed_watch.snapshot().expose().to_vec();
|
||||||
|
damaged[20] ^= 0x55;
|
||||||
|
assert!(fresh_watch.apply(SecretBytes::new(damaged)).is_err());
|
||||||
|
assert!(fresh_watch.current().is_none());
|
||||||
|
assert_eq!(
|
||||||
|
fresh_watch.apply(SecretBytes::new(first.snapshot().expose().to_vec()))?,
|
||||||
|
WatchSnapshotApply::Stale
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
fresh_watch.apply(SecretBytes::new(changed_watch.snapshot().expose().to_vec()))?,
|
||||||
|
WatchSnapshotApply::Replaced
|
||||||
|
);
|
||||||
|
|
||||||
|
let persisted = fs::read_to_string(journal)?;
|
||||||
|
for forbidden in [
|
||||||
|
"first-secret",
|
||||||
|
"second-secret",
|
||||||
|
"third-secret",
|
||||||
|
"otpauth://",
|
||||||
|
"94287082",
|
||||||
|
] {
|
||||||
|
assert!(!persisted.contains(forbidden), "journal leaked {forbidden}");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn journal_keeps_revisions_monotonic_across_sender_reloads() -> TestResult {
|
||||||
|
let directory = tempfile::tempdir()?;
|
||||||
|
let journal = directory.path().join("watch-snapshot.toml");
|
||||||
|
let first = WatchSnapshotSender::load(journal.clone()).prepare(
|
||||||
|
"paired-watch",
|
||||||
|
vec![entry("otp/alice", "Acme", "alice", b"secret")],
|
||||||
|
)?;
|
||||||
|
assert_eq!(first.revision(), 1);
|
||||||
|
|
||||||
|
let same = WatchSnapshotSender::load(journal.clone()).prepare(
|
||||||
|
"paired-watch",
|
||||||
|
vec![entry("otp/alice", "Acme", "alice", b"secret")],
|
||||||
|
)?;
|
||||||
|
assert_eq!(same.revision(), 1);
|
||||||
|
let changed = WatchSnapshotSender::load(journal).prepare("paired-watch", Vec::new())?;
|
||||||
|
assert_eq!(changed.revision(), 2);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user