Implement iPhone TOTP tab and Watch sharing

This commit is contained in:
2026-08-11 20:54:34 +02:00
parent ae64ce47a3
commit b01cc8bb6d
10 changed files with 1980 additions and 5 deletions

View File

@@ -595,6 +595,8 @@ public protocol MobileAuthenticationProtocol: AnyObject, Sendable {
func copyEntryField(path: String, field: UInt64) throws -> MobileEntryCopy
func copyTotpCode(path: String, unixSeconds: UInt64) throws -> MobileEntryCopy
func discardEntryEditor(editor: UInt64) throws
func entryEditor(editor: UInt64) throws -> MobileEntryEditorPage
@@ -617,12 +619,20 @@ public protocol MobileAuthenticationProtocol: AnyObject, Sendable {
func setBiometricUnlock(enabled: Bool) throws -> MobileAuthenticationState
func setTotpWatchShared(path: String, shared: Bool, unixSeconds: UInt64) throws -> MobileTotpDetail
func state() throws -> MobileAuthenticationState
func totpDetail(path: String, unixSeconds: UInt64) throws -> MobileTotpDetail
func totpPage() throws -> MobileTotpPage
func touchUserActivity() throws
func unlockEntry(path: String, passphrase: String?) throws -> MobileAuthenticationState
func unlockTotp(passphrase: String?) throws -> MobileAuthenticationState
func updateEntryEditor(editor: UInt64, fields: [MobileEntryEditorInput]) throws -> MobileEntryEditorPage
}
@@ -733,6 +743,17 @@ open func copyEntryField(path: String, field: UInt64)throws -> MobileEntryCopy
})
}
open func copyTotpCode(path: String, unixSeconds: UInt64)throws -> MobileEntryCopy {
return try FfiConverterTypeMobileEntryCopy_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobileauthentication_copy_totp_code(
self.uniffiCloneHandle(),
FfiConverterString.lower(path),
FfiConverterUInt64.lower(unixSeconds),uniffiCallStatus
)
})
}
open func discardEntryEditor(editor: UInt64)throws {try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobileauthentication_discard_entry_editor(
@@ -849,6 +870,18 @@ open func setBiometricUnlock(enabled: Bool)throws -> MobileAuthenticationState
})
}
open func setTotpWatchShared(path: String, shared: Bool, unixSeconds: UInt64)throws -> MobileTotpDetail {
return try FfiConverterTypeMobileTotpDetail_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobileauthentication_set_totp_watch_shared(
self.uniffiCloneHandle(),
FfiConverterString.lower(path),
FfiConverterBool.lower(shared),
FfiConverterUInt64.lower(unixSeconds),uniffiCallStatus
)
})
}
open func state()throws -> MobileAuthenticationState {
return try FfiConverterTypeMobileAuthenticationState_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
@@ -858,6 +891,26 @@ open func state()throws -> MobileAuthenticationState {
})
}
open func totpDetail(path: String, unixSeconds: UInt64)throws -> MobileTotpDetail {
return try FfiConverterTypeMobileTotpDetail_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobileauthentication_totp_detail(
self.uniffiCloneHandle(),
FfiConverterString.lower(path),
FfiConverterUInt64.lower(unixSeconds),uniffiCallStatus
)
})
}
open func totpPage()throws -> MobileTotpPage {
return try FfiConverterTypeMobileTotpPage_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobileauthentication_totp_page(
self.uniffiCloneHandle(),uniffiCallStatus
)
})
}
open func touchUserActivity()throws {try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobileauthentication_touch_user_activity(
@@ -877,6 +930,16 @@ open func unlockEntry(path: String, passphrase: String?)throws -> MobileAuthent
})
}
open func unlockTotp(passphrase: String?)throws -> MobileAuthenticationState {
return try FfiConverterTypeMobileAuthenticationState_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobileauthentication_unlock_totp(
self.uniffiCloneHandle(),
FfiConverterOptionString.lower(passphrase),uniffiCallStatus
)
})
}
open func updateEntryEditor(editor: UInt64, fields: [MobileEntryEditorInput])throws -> MobileEntryEditorPage {
return try FfiConverterTypeMobileEntryEditorPage_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
@@ -2682,6 +2745,266 @@ public func FfiConverterTypeMobileShell_lower(_ value: MobileShell) -> RustBuffe
}
public struct MobileTotpDetail: Equatable, Hashable {
public var path: String
public var issuer: String?
public var account: String
public var code: String
public var validUntil: UInt64
public var period: UInt64
public var sharedWithWatch: Bool
public var watch: MobileWatchSnapshotStatus
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(path: String, issuer: String?, account: String, code: String, validUntil: UInt64, period: UInt64, sharedWithWatch: Bool, watch: MobileWatchSnapshotStatus) {
self.path = path
self.issuer = issuer
self.account = account
self.code = code
self.validUntil = validUntil
self.period = period
self.sharedWithWatch = sharedWithWatch
self.watch = watch
}
}
#if compiler(>=6)
extension MobileTotpDetail: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobileTotpDetail: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileTotpDetail {
return
try MobileTotpDetail(
path: FfiConverterString.read(from: &buf),
issuer: FfiConverterOptionString.read(from: &buf),
account: FfiConverterString.read(from: &buf),
code: FfiConverterString.read(from: &buf),
validUntil: FfiConverterUInt64.read(from: &buf),
period: FfiConverterUInt64.read(from: &buf),
sharedWithWatch: FfiConverterBool.read(from: &buf),
watch: FfiConverterTypeMobileWatchSnapshotStatus.read(from: &buf)
)
}
public static func write(_ value: MobileTotpDetail, into buf: inout [UInt8]) {
FfiConverterString.write(value.path, into: &buf)
FfiConverterOptionString.write(value.issuer, into: &buf)
FfiConverterString.write(value.account, into: &buf)
FfiConverterString.write(value.code, into: &buf)
FfiConverterUInt64.write(value.validUntil, into: &buf)
FfiConverterUInt64.write(value.period, into: &buf)
FfiConverterBool.write(value.sharedWithWatch, into: &buf)
FfiConverterTypeMobileWatchSnapshotStatus.write(value.watch, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileTotpDetail_lift(_ buf: RustBuffer) throws -> MobileTotpDetail {
return try FfiConverterTypeMobileTotpDetail.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileTotpDetail_lower(_ value: MobileTotpDetail) -> RustBuffer {
return FfiConverterTypeMobileTotpDetail.lower(value)
}
public struct MobileTotpPage: Equatable, Hashable {
public var rows: [MobileTotpRow]
public var unavailableEntries: UInt32
public var watch: MobileWatchSnapshotStatus
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(rows: [MobileTotpRow], unavailableEntries: UInt32, watch: MobileWatchSnapshotStatus) {
self.rows = rows
self.unavailableEntries = unavailableEntries
self.watch = watch
}
}
#if compiler(>=6)
extension MobileTotpPage: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobileTotpPage: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileTotpPage {
return
try MobileTotpPage(
rows: FfiConverterSequenceTypeMobileTotpRow.read(from: &buf),
unavailableEntries: FfiConverterUInt32.read(from: &buf),
watch: FfiConverterTypeMobileWatchSnapshotStatus.read(from: &buf)
)
}
public static func write(_ value: MobileTotpPage, into buf: inout [UInt8]) {
FfiConverterSequenceTypeMobileTotpRow.write(value.rows, into: &buf)
FfiConverterUInt32.write(value.unavailableEntries, into: &buf)
FfiConverterTypeMobileWatchSnapshotStatus.write(value.watch, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileTotpPage_lift(_ buf: RustBuffer) throws -> MobileTotpPage {
return try FfiConverterTypeMobileTotpPage.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileTotpPage_lower(_ value: MobileTotpPage) -> RustBuffer {
return FfiConverterTypeMobileTotpPage.lower(value)
}
public struct MobileTotpRow: Equatable, Hashable {
public var path: String
public var issuer: String?
public var account: String
public var title: String
public var detail: String
public var sharedWithWatch: Bool
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(path: String, issuer: String?, account: String, title: String, detail: String, sharedWithWatch: Bool) {
self.path = path
self.issuer = issuer
self.account = account
self.title = title
self.detail = detail
self.sharedWithWatch = sharedWithWatch
}
}
#if compiler(>=6)
extension MobileTotpRow: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobileTotpRow: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileTotpRow {
return
try MobileTotpRow(
path: FfiConverterString.read(from: &buf),
issuer: FfiConverterOptionString.read(from: &buf),
account: FfiConverterString.read(from: &buf),
title: FfiConverterString.read(from: &buf),
detail: FfiConverterString.read(from: &buf),
sharedWithWatch: FfiConverterBool.read(from: &buf)
)
}
public static func write(_ value: MobileTotpRow, into buf: inout [UInt8]) {
FfiConverterString.write(value.path, into: &buf)
FfiConverterOptionString.write(value.issuer, into: &buf)
FfiConverterString.write(value.account, into: &buf)
FfiConverterString.write(value.title, into: &buf)
FfiConverterString.write(value.detail, into: &buf)
FfiConverterBool.write(value.sharedWithWatch, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileTotpRow_lift(_ buf: RustBuffer) throws -> MobileTotpRow {
return try FfiConverterTypeMobileTotpRow.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileTotpRow_lower(_ value: MobileTotpRow) -> RustBuffer {
return FfiConverterTypeMobileTotpRow.lower(value)
}
public struct MobileWatchSnapshotStatus: Equatable, Hashable {
public var state: MobileWatchSnapshotState
public var detail: String
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(state: MobileWatchSnapshotState, detail: String) {
self.state = state
self.detail = detail
}
}
#if compiler(>=6)
extension MobileWatchSnapshotStatus: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobileWatchSnapshotStatus: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileWatchSnapshotStatus {
return
try MobileWatchSnapshotStatus(
state: FfiConverterTypeMobileWatchSnapshotState.read(from: &buf),
detail: FfiConverterString.read(from: &buf)
)
}
public static func write(_ value: MobileWatchSnapshotStatus, into buf: inout [UInt8]) {
FfiConverterTypeMobileWatchSnapshotState.write(value.state, into: &buf)
FfiConverterString.write(value.detail, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileWatchSnapshotStatus_lift(_ buf: RustBuffer) throws -> MobileWatchSnapshotStatus {
return try FfiConverterTypeMobileWatchSnapshotStatus.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileWatchSnapshotStatus_lower(_ value: MobileWatchSnapshotStatus) -> RustBuffer {
return FfiConverterTypeMobileWatchSnapshotStatus.lower(value)
}
public enum MobileAuthenticationErrorKind: Equatable, Hashable {
@@ -4311,6 +4634,72 @@ public func FfiConverterTypeMobileTab_lower(_ value: MobileTab) -> RustBuffer {
}
public enum MobileWatchSnapshotState: Equatable, Hashable {
case unavailable
case pending
}
#if compiler(>=6)
extension MobileWatchSnapshotState: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobileWatchSnapshotState: FfiConverterRustBuffer {
typealias SwiftType = MobileWatchSnapshotState
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileWatchSnapshotState {
let variant: Int32 = try readInt(&buf)
switch variant {
case 1: return .unavailable
case 2: return .pending
default: throw UniffiInternalError.unexpectedEnumCase
}
}
public static func write(_ value: MobileWatchSnapshotState, into buf: inout [UInt8]) {
switch value {
case .unavailable:
writeInt(&buf, Int32(1))
case .pending:
writeInt(&buf, Int32(2))
}
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileWatchSnapshotState_lift(_ buf: RustBuffer) throws -> MobileWatchSnapshotState {
return try FfiConverterTypeMobileWatchSnapshotState.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileWatchSnapshotState_lower(_ value: MobileWatchSnapshotState) -> RustBuffer {
return FfiConverterTypeMobileWatchSnapshotState.lower(value)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
@@ -4656,6 +5045,31 @@ fileprivate struct FfiConverterSequenceTypeMobilePasswordRow: FfiConverterRustBu
return seq
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
fileprivate struct FfiConverterSequenceTypeMobileTotpRow: FfiConverterRustBuffer {
typealias SwiftType = [MobileTotpRow]
public static func write(_ value: [MobileTotpRow], into buf: inout [UInt8]) {
let len = Int32(value.count)
writeInt(&buf, len)
for item in value {
FfiConverterTypeMobileTotpRow.write(item, into: &buf)
}
}
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [MobileTotpRow] {
let len: Int32 = try readInt(&buf)
var seq = [MobileTotpRow]()
seq.reserveCapacity(Int(len))
for _ in 0 ..< len {
seq.append(try FfiConverterTypeMobileTotpRow.read(from: &buf))
}
return seq
}
}
public func mobileAuthentication()throws -> MobileAuthentication {
return try FfiConverterTypeMobileAuthentication_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
@@ -4784,6 +5198,9 @@ private let initializationResult: InitializationResult = {
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_copy_entry_field() != 17773) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_copy_totp_code() != 8344) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_discard_entry_editor() != 28195) {
return InitializationResult.apiChecksumMismatch
}
@@ -4817,15 +5234,27 @@ private let initializationResult: InitializationResult = {
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_biometric_unlock() != 9486) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_totp_watch_shared() != 57472) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_state() != 60826) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_totp_detail() != 42762) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_totp_page() != 18529) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_touch_user_activity() != 18402) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_unlock_entry() != 28139) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_unlock_totp() != 816) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_update_entry_editor() != 30139) {
return InitializationResult.apiChecksumMismatch
}

View File

@@ -278,6 +278,11 @@ void uniffi_ironstorage_apple_fn_method_mobileauthentication_cancel(uint64_t ptr
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_copy_entry_field(uint64_t ptr, RustBuffer path, uint64_t field, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_COPY_TOTP_CODE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_COPY_TOTP_CODE
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_copy_totp_code(uint64_t ptr, RustBuffer path, uint64_t unix_seconds, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_DISCARD_ENTRY_EDITOR
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_DISCARD_ENTRY_EDITOR
void uniffi_ironstorage_apple_fn_method_mobileauthentication_discard_entry_editor(uint64_t ptr, uint64_t editor, RustCallStatus *_Nonnull out_status
@@ -333,11 +338,26 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_save_entry_ed
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_biometric_unlock(uint64_t ptr, int8_t enabled, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_TOTP_WATCH_SHARED
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_TOTP_WATCH_SHARED
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
#ifndef 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
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_TOTP_DETAIL
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_TOTP_DETAIL
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_totp_detail(uint64_t ptr, RustBuffer path, uint64_t unix_seconds, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_TOTP_PAGE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_TOTP_PAGE
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_totp_page(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_TOUCH_USER_ACTIVITY
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_TOUCH_USER_ACTIVITY
void uniffi_ironstorage_apple_fn_method_mobileauthentication_touch_user_activity(uint64_t ptr, RustCallStatus *_Nonnull out_status
@@ -348,6 +368,11 @@ void uniffi_ironstorage_apple_fn_method_mobileauthentication_touch_user_activity
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_unlock_entry(uint64_t ptr, RustBuffer path, RustBuffer passphrase, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_UNLOCK_TOTP
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_UNLOCK_TOTP
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_unlock_totp(uint64_t ptr, RustBuffer passphrase, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_UPDATE_ENTRY_EDITOR
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_UPDATE_ENTRY_EDITOR
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_update_entry_editor(uint64_t ptr, uint64_t editor, RustBuffer fields, RustCallStatus *_Nonnull out_status
@@ -814,6 +839,12 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_cancel(vo
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_COPY_ENTRY_FIELD
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_copy_entry_field(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_COPY_TOTP_CODE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_COPY_TOTP_CODE
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_copy_totp_code(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_DISCARD_ENTRY_EDITOR
@@ -880,12 +911,30 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_save_entr
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_BIOMETRIC_UNLOCK
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_biometric_unlock(void
);
#endif
#ifndef 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
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_STATE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_STATE
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_state(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_TOTP_DETAIL
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_TOTP_DETAIL
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_totp_detail(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_TOTP_PAGE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_TOTP_PAGE
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_totp_page(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_TOUCH_USER_ACTIVITY
@@ -898,6 +947,12 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_touch_use
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_UNLOCK_ENTRY
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_unlock_entry(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_UNLOCK_TOTP
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_UNLOCK_TOTP
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_unlock_totp(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_UPDATE_ENTRY_EDITOR

View File

@@ -7,6 +7,9 @@ extension Notification.Name {
static let ironStorageAuthenticationDidChange = Notification.Name(
"de.rfc1437.ironstorage.authentication-did-change"
)
static let ironStorageWatchSnapshotDidChange = Notification.Name(
"de.rfc1437.ironstorage.watch-snapshot-did-change"
)
}
@main
@@ -56,6 +59,8 @@ private final class AppContext: NSObject, UITabBarControllerDelegate {
let root: UIViewController = switch page.tab {
case .passwords:
PasswordDirectoryViewController(shellPage: page, authentication: authentication)
case .totp:
TotpListViewController(shellPage: page, authentication: authentication)
case .preferences:
PreferencesViewController(page: page, authentication: authentication)
default:
@@ -888,6 +893,738 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
}
}
@MainActor
private final class TotpListViewController: UITableViewController, MobileTabRoot {
fileprivate let shellTab = MobileTab.totp
private let authentication: MobileAuthentication?
private var shellPage: MobilePage
private var page: MobileTotpPage?
private var loadTask: Task<Void, Never>?
private var unlockTask: Task<Void, Never>?
init(shellPage: MobilePage, authentication: MobileAuthentication?) {
self.shellPage = shellPage
self.authentication = authentication
super.init(style: .insetGrouped)
title = shellPage.title
navigationItem.largeTitleDisplayMode = .always
refreshControl = UIRefreshControl()
refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported")
}
deinit {
loadTask?.cancel()
unlockTask?.cancel()
NotificationCenter.default.removeObserver(self)
}
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(
self,
selector: #selector(authenticationDidChange),
name: .ironStorageAuthenticationDidChange,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(localStoreDidChange),
name: .ironStorageLocalStoreDidChange,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(localStoreDidChange),
name: .ironStorageWatchSnapshotDidChange,
object: nil
)
refreshState()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
refreshState()
}
override func numberOfSections(in tableView: UITableView) -> Int { 1 }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
page?.rows.count ?? 0
}
override func tableView(
_ tableView: UITableView,
titleForFooterInSection section: Int
) -> String? {
guard let page else { return nil }
let unavailable = page.unavailableEntries == 0
? ""
: " \(page.unavailableEntries) entries could not be inspected with the active key."
return page.watch.detail + unavailable
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let row = page?.rows[indexPath.row]
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
var content = cell.defaultContentConfiguration()
content.image = UIImage(systemName: "timer")
content.text = row?.title
content.secondaryText = row?.detail
content.secondaryTextProperties.numberOfLines = 2
if row?.sharedWithWatch == true {
content.secondaryText = [row?.detail, "Apple Watch selected"]
.compactMap { $0 }
.joined(separator: "")
}
cell.contentConfiguration = content
cell.accessoryType = .disclosureIndicator
cell.accessibilityLabel = [row?.title, row?.detail].compactMap { $0 }.joined(separator: ", ")
cell.accessibilityValue = row?.sharedWithWatch == true ? "Selected for Apple Watch" : nil
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard let row = page?.rows[indexPath.row], let authentication else { return }
loadTask?.cancel()
loadTask = Task { [weak self] in
let result = await Task.detached(priority: .userInitiated) {
do {
try authentication.touchUserActivity()
return Result<MobileTotpDetail, AuthenticationFailure>.success(
try authentication.totpDetail(
path: row.path,
unixSeconds: currentUnixSeconds()
)
)
} catch let error as MobileAuthenticationFfiError {
return .failure(AuthenticationFailure(error))
} catch {
return .failure(.unexpected)
}
}.value
guard !Task.isCancelled, let self else { return }
switch result {
case let .success(detail):
navigationController?.pushViewController(
TotpDetailViewController(authentication: authentication, detail: detail),
animated: true
)
case let .failure(failure):
handle(failure)
}
}
}
@objc private func refreshRequested() {
refreshState(force: true)
}
@objc private func authenticationDidChange() {
refreshState()
}
@objc private func localStoreDidChange() {
guard (try? authentication?.state().unlocked) == true else { return }
loadPage()
}
@objc private func unlockRequested() {
unlock(passphrase: nil)
}
@objc private func lockRequested() {
guard let authentication else { return }
do {
try authentication.manualLock()
NotificationCenter.default.post(
name: .ironStorageAuthenticationDidChange,
object: authentication
)
} catch let error as MobileAuthenticationFfiError {
presentAuthenticationFailure(AuthenticationFailure(error))
} catch {
presentAuthenticationFailure(.unexpected)
}
}
private func refreshState(force: Bool = false) {
guard shellPage.state == .ready else {
page = nil
showUnavailable(
title: shellPage.stateTitle,
detail: shellPage.stateDetail,
image: shellPage.systemImage
)
return
}
if (try? authentication?.state().unlocked) == true {
if page == nil || force { loadPage() }
} else {
loadTask?.cancel()
page = nil
tableView.reloadData()
navigationItem.rightBarButtonItem = nil
var configuration = UIContentUnavailableConfiguration.empty()
configuration.image = UIImage(systemName: "lock.fill")
configuration.text = "TOTP Is Locked"
configuration.secondaryText =
"Authenticate to scan password entries for time-based one-time passwords."
configuration.button = .filled()
configuration.button.title = "Unlock"
configuration.buttonProperties.primaryAction = UIAction { [weak self] _ in
self?.unlockRequested()
}
contentUnavailableConfiguration = configuration
refreshControl?.endRefreshing()
}
}
private func unlock(passphrase: String?) {
guard let authentication else {
presentAuthenticationFailure(.unavailable)
return
}
unlockTask?.cancel()
showLoading("Unlocking TOTP")
unlockTask = Task { [weak self] in
let result = await Task.detached(priority: .userInitiated) {
do {
return Result<MobileAuthenticationState, AuthenticationFailure>.success(
try authentication.unlockTotp(passphrase: passphrase)
)
} catch let error as MobileAuthenticationFfiError {
return .failure(AuthenticationFailure(error))
} catch {
return .failure(.unexpected)
}
}.value
guard !Task.isCancelled, let self else { return }
switch result {
case .success:
NotificationCenter.default.post(
name: .ironStorageAuthenticationDidChange,
object: authentication
)
loadPage()
UIAccessibility.post(notification: .announcement, argument: "TOTP unlocked")
case let .failure(failure)
where passphrase == nil
&& (failure.kind == .passphraseRequired
|| failure.kind == .biometryUnavailable):
promptForPassphrase(message: failure.detail)
case let .failure(failure):
if failure.kind != .cancelled { handle(failure) }
}
}
}
private func promptForPassphrase(message: String) {
let alert = UIAlertController(
title: "GPG Key Passphrase",
message: message,
preferredStyle: .alert
)
alert.addTextField { field in
field.isSecureTextEntry = true
field.textContentType = .password
field.placeholder = "Passphrase"
field.returnKeyType = .go
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { [weak self] _ in
self?.refreshState()
})
alert.addAction(UIAlertAction(title: "Unlock", style: .default) { [weak self, weak alert] _ in
guard let value = alert?.textFields?.first?.text, !value.isEmpty else { return }
alert?.textFields?.first?.text = nil
self?.unlock(passphrase: value)
})
present(alert, animated: true)
}
private func loadPage() {
guard let authentication else {
presentAuthenticationFailure(.unavailable)
return
}
loadTask?.cancel()
showLoading("Loading TOTP")
loadTask = Task { [weak self] in
let result = await Task.detached(priority: .userInitiated) {
do {
return Result<MobileTotpPage, AuthenticationFailure>.success(
try authentication.totpPage()
)
} catch let error as MobileAuthenticationFfiError {
return .failure(AuthenticationFailure(error))
} catch {
return .failure(.unexpected)
}
}.value
guard !Task.isCancelled, let self else { return }
refreshControl?.endRefreshing()
switch result {
case let .success(page):
self.page = page
navigationItem.rightBarButtonItem = UIBarButtonItem(
image: UIImage(systemName: "lock.fill"),
style: .plain,
target: self,
action: #selector(lockRequested)
)
navigationItem.rightBarButtonItem?.accessibilityLabel = "Lock IronStorage"
tableView.reloadData()
if page.rows.isEmpty {
showUnavailable(
title: "No TOTP Codes",
detail: "No valid time-based OTP entries were found in the password store.",
image: "timer"
)
} else {
contentUnavailableConfiguration = nil
}
case let .failure(failure):
handle(failure)
}
}
}
private func handle(_ failure: AuthenticationFailure) {
if failure.kind == .expired {
NotificationCenter.default.post(
name: .ironStorageAuthenticationDidChange,
object: authentication
)
refreshState()
} else {
showUnavailable(title: failure.title, detail: failure.detail, image: "exclamationmark.triangle")
}
presentAuthenticationFailure(failure)
}
private func showLoading(_ title: String) {
var configuration = UIContentUnavailableConfiguration.loading()
configuration.text = title
configuration.secondaryText = "Reading OTP metadata in secure storage."
contentUnavailableConfiguration = configuration
}
private func showUnavailable(title: String, detail: String, image: String) {
var configuration = UIContentUnavailableConfiguration.empty()
configuration.image = UIImage(systemName: image)
configuration.text = title
configuration.secondaryText = detail
contentUnavailableConfiguration = configuration
refreshControl?.endRefreshing()
}
}
@MainActor
private final class TotpDetailViewController: UITableViewController {
private let authentication: MobileAuthentication
private var detail: MobileTotpDetail
private let codeContainer = UIView()
private let issuerLabel = UILabel()
private let accountLabel = UILabel()
private let codeLabel = UILabel()
private let countdownLabel = UILabel()
private let progress = UIProgressView(progressViewStyle: .default)
private let watchSwitch = UISwitch()
private var timerTask: Task<Void, Never>?
private var loadTask: Task<Void, Never>?
private var clipboardTask: Task<Void, Never>?
private var copiedValue: String?
init(authentication: MobileAuthentication, detail: MobileTotpDetail) {
self.authentication = authentication
self.detail = detail
super.init(style: .insetGrouped)
navigationItem.largeTitleDisplayMode = .never
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported")
}
deinit {
timerTask?.cancel()
loadTask?.cancel()
clipboardTask?.cancel()
NotificationCenter.default.removeObserver(self)
}
override func viewDidLoad() {
super.viewDidLoad()
configureHeader()
navigationItem.rightBarButtonItem = UIBarButtonItem(
image: UIImage(systemName: "doc.on.doc"),
style: .plain,
target: self,
action: #selector(copyRequested)
)
navigationItem.rightBarButtonItem?.accessibilityLabel = "Copy current TOTP code"
watchSwitch.addTarget(self, action: #selector(watchSwitchChanged(_:)), for: .valueChanged)
NotificationCenter.default.addObserver(
self,
selector: #selector(authenticationDidChange),
name: .ironStorageAuthenticationDidChange,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(entryDidChange),
name: .ironStorageLocalStoreDidChange,
object: nil
)
apply(detail)
startTimer()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard let header = tableView.tableHeaderView else { return }
let height = header.systemLayoutSizeFitting(
CGSize(width: tableView.bounds.width, height: 0),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel
).height
guard abs(header.frame.height - height) > 0.5 else { return }
header.frame.size.height = height
tableView.tableHeaderView = header
}
override func numberOfSections(in tableView: UITableView) -> Int { 1 }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 1 }
override func tableView(
_ tableView: UITableView,
titleForHeaderInSection section: Int
) -> String? {
"Apple Watch"
}
override func tableView(
_ tableView: UITableView,
titleForFooterInSection section: Int
) -> String? {
detail.watch.detail
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
var content = cell.defaultContentConfiguration()
content.text = "Share with Apple Watch"
content.secondaryText = "Time-based codes only"
cell.contentConfiguration = content
cell.accessoryView = watchSwitch
cell.selectionStyle = .none
return cell
}
@objc private func authenticationDidChange() {
guard (try? authentication.state().unlocked) == true else {
lockDetail()
return
}
}
@objc private func entryDidChange() {
refreshCode()
}
@objc private func copyRequested() {
loadTask?.cancel()
let authentication = authentication
let path = detail.path
loadTask = Task { [weak self] in
let result = await Task.detached(priority: .userInitiated) {
do {
try authentication.touchUserActivity()
return Result<MobileEntryCopy, AuthenticationFailure>.success(
try authentication.copyTotpCode(
path: path,
unixSeconds: currentUnixSeconds()
)
)
} catch let error as MobileAuthenticationFfiError {
return .failure(AuthenticationFailure(error))
} catch {
return .failure(.unexpected)
}
}.value
guard !Task.isCancelled, let self else { return }
loadTask = nil
switch result {
case let .success(copy):
UIPasteboard.general.string = copy.value
copiedValue = copy.value
flashCopied()
clipboardTask?.cancel()
clipboardTask = Task { @MainActor in
do {
try await Task.sleep(for: .seconds(copy.timeoutSeconds))
} catch {
return
}
if UIPasteboard.general.string == copy.value {
UIPasteboard.general.items = []
}
self.copiedValue = nil
}
case let .failure(failure):
handle(failure)
}
}
}
@objc private func watchSwitchChanged(_ sender: UISwitch) {
let requested = sender.isOn
sender.isEnabled = false
loadTask?.cancel()
let authentication = authentication
let path = detail.path
loadTask = Task { [weak self, weak sender] in
let result = await Task.detached(priority: .userInitiated) {
do {
try authentication.touchUserActivity()
return Result<MobileTotpDetail, AuthenticationFailure>.success(
try authentication.setTotpWatchShared(
path: path,
shared: requested,
unixSeconds: currentUnixSeconds()
)
)
} catch let error as MobileAuthenticationFfiError {
return .failure(AuthenticationFailure(error))
} catch {
return .failure(.unexpected)
}
}.value
guard !Task.isCancelled, let self else { return }
loadTask = nil
sender?.isEnabled = true
switch result {
case let .success(detail):
apply(detail)
UINotificationFeedbackGenerator().notificationOccurred(.success)
NotificationCenter.default.post(
name: .ironStorageWatchSnapshotDidChange,
object: nil
)
case let .failure(failure):
sender?.setOn(!requested, animated: true)
handle(failure)
}
}
}
private func configureHeader() {
issuerLabel.font = .preferredFont(forTextStyle: .title2)
issuerLabel.adjustsFontForContentSizeCategory = true
issuerLabel.textAlignment = .center
accountLabel.font = .preferredFont(forTextStyle: .body)
accountLabel.textColor = .secondaryLabel
accountLabel.adjustsFontForContentSizeCategory = true
accountLabel.textAlignment = .center
accountLabel.numberOfLines = 0
codeLabel.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(
for: .monospacedDigitSystemFont(ofSize: 48, weight: .semibold)
)
codeLabel.adjustsFontForContentSizeCategory = true
codeLabel.textAlignment = .center
codeLabel.minimumScaleFactor = 0.55
codeLabel.adjustsFontSizeToFitWidth = true
codeLabel.layer.cornerRadius = 12
codeLabel.layer.masksToBounds = true
codeLabel.isAccessibilityElement = true
codeLabel.isUserInteractionEnabled = true
codeLabel.accessibilityTraits.insert(.button)
codeLabel.accessibilityHint = "Copies the current code"
countdownLabel.font = .preferredFont(forTextStyle: .footnote)
countdownLabel.textColor = .secondaryLabel
countdownLabel.adjustsFontForContentSizeCategory = true
countdownLabel.textAlignment = .center
let stack = UIStackView(arrangedSubviews: [issuerLabel, accountLabel, codeLabel, progress, countdownLabel])
stack.axis = .vertical
stack.spacing = 12
stack.translatesAutoresizingMaskIntoConstraints = false
codeContainer.addSubview(stack)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: codeContainer.layoutMarginsGuide.leadingAnchor),
stack.trailingAnchor.constraint(equalTo: codeContainer.layoutMarginsGuide.trailingAnchor),
stack.topAnchor.constraint(equalTo: codeContainer.topAnchor, constant: 20),
stack.bottomAnchor.constraint(equalTo: codeContainer.bottomAnchor, constant: -20),
])
codeContainer.frame = CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 1)
tableView.tableHeaderView = codeContainer
codeLabel.addGestureRecognizer(
UITapGestureRecognizer(target: self, action: #selector(copyRequested))
)
}
private func apply(_ detail: MobileTotpDetail) {
self.detail = detail
title = detail.issuer ?? detail.account
issuerLabel.text = detail.issuer ?? "TOTP"
accountLabel.text = detail.account
codeLabel.text = groupedCode(detail.code)
codeLabel.accessibilityLabel = "Current code \(detail.code.map(String.init).joined(separator: " "))"
watchSwitch.setOn(detail.sharedWithWatch, animated: false)
tableView.reloadData()
updateCountdown()
}
private func startTimer() {
timerTask?.cancel()
timerTask = Task { [weak self] in
while !Task.isCancelled {
do {
try await Task.sleep(for: .seconds(1))
} catch {
return
}
guard let self else { return }
if currentUnixSeconds() >= detail.validUntil {
refreshCode()
} else {
updateCountdown()
}
}
}
}
private func updateCountdown() {
let remaining = detail.validUntil.saturatingSubtracting(currentUnixSeconds())
let fraction = detail.period == 0 ? 0 : Float(remaining) / Float(detail.period)
progress.setProgress(min(max(fraction, 0), 1), animated: true)
countdownLabel.text = "\(remaining) seconds remaining"
progress.accessibilityLabel = "Code validity"
progress.accessibilityValue = countdownLabel.text
}
private func refreshCode() {
guard loadTask == nil else { return }
let authentication = authentication
let path = detail.path
loadTask = Task { [weak self] in
let result = await Task.detached(priority: .userInitiated) {
do {
return Result<MobileTotpDetail, AuthenticationFailure>.success(
try authentication.totpDetail(
path: path,
unixSeconds: currentUnixSeconds()
)
)
} catch let error as MobileAuthenticationFfiError {
return .failure(AuthenticationFailure(error))
} catch {
return .failure(.unexpected)
}
}.value
guard let self else { return }
loadTask = nil
guard !Task.isCancelled else { return }
switch result {
case let .success(detail): apply(detail)
case let .failure(failure): showDetailUnavailable(failure)
}
}
}
private func flashCopied() {
UINotificationFeedbackGenerator().notificationOccurred(.success)
UIAccessibility.post(notification: .announcement, argument: "TOTP code copied")
UIView.animate(withDuration: 0.12, animations: {
self.codeLabel.backgroundColor = .systemGreen.withAlphaComponent(0.28)
}) { _ in
UIView.animate(withDuration: 0.55) { self.codeLabel.backgroundColor = .clear }
}
}
private func handle(_ failure: AuthenticationFailure) {
if failure.kind == .expired {
lockDetail()
}
presentAuthenticationFailure(failure)
}
private func showDetailUnavailable(_ failure: AuthenticationFailure) {
if failure.kind == .expired {
lockDetail()
} else {
timerTask?.cancel()
detail.code = ""
codeLabel.text = nil
navigationItem.rightBarButtonItem = nil
watchSwitch.isEnabled = false
var configuration = UIContentUnavailableConfiguration.empty()
configuration.image = UIImage(systemName: "exclamationmark.triangle")
configuration.text = failure.title
configuration.secondaryText = failure.detail
configuration.button = .plain()
configuration.button.title = "Back to TOTP"
configuration.buttonProperties.primaryAction = UIAction { [weak self] _ in
self?.navigationController?.popViewController(animated: true)
}
contentUnavailableConfiguration = configuration
}
presentAuthenticationFailure(failure)
}
private func lockDetail() {
timerTask?.cancel()
loadTask?.cancel()
clipboardTask?.cancel()
detail.code = ""
codeLabel.text = nil
if UIPasteboard.general.string == copiedValue {
UIPasteboard.general.items = []
}
copiedValue = nil
navigationItem.rightBarButtonItem = nil
watchSwitch.isEnabled = false
var configuration = UIContentUnavailableConfiguration.empty()
configuration.image = UIImage(systemName: "lock.fill")
configuration.text = "TOTP Is Locked"
configuration.secondaryText = "The code was removed when IronStorage locked."
configuration.button = .plain()
configuration.button.title = "Back to TOTP"
configuration.buttonProperties.primaryAction = UIAction { [weak self] _ in
self?.navigationController?.popViewController(animated: true)
}
contentUnavailableConfiguration = configuration
UIAccessibility.post(notification: .announcement, argument: "TOTP locked")
}
}
private func currentUnixSeconds() -> UInt64 {
UInt64(max(Date().timeIntervalSince1970, 0))
}
private func groupedCode(_ code: String) -> String {
let midpoint = code.index(code.startIndex, offsetBy: code.count / 2)
return String(code[..<midpoint]) + " " + String(code[midpoint...])
}
private extension UInt64 {
func saturatingSubtracting(_ value: UInt64) -> UInt64 {
self > value ? self - value : 0
}
}
@MainActor
private final class PasswordDirectoryViewController: UITableViewController, MobileTabRoot {
fileprivate let shellTab = MobileTab.passwords