Implement biometric-protected GPG unlock
This commit is contained in:
@@ -478,6 +478,22 @@ fileprivate struct FfiConverterUInt32: FfiConverterPrimitive {
|
||||
}
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
fileprivate struct FfiConverterUInt64: FfiConverterPrimitive {
|
||||
typealias FfiType = UInt64
|
||||
typealias SwiftType = UInt64
|
||||
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UInt64 {
|
||||
return try lift(readInt(&buf))
|
||||
}
|
||||
|
||||
public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
|
||||
writeInt(&buf, lower(value))
|
||||
}
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
@@ -567,6 +583,178 @@ fileprivate struct FfiConverterString: FfiConverter {
|
||||
|
||||
|
||||
|
||||
public protocol MobileAuthenticationProtocol: AnyObject, Sendable {
|
||||
|
||||
func cancel() throws
|
||||
|
||||
func manualLock() throws
|
||||
|
||||
func setBiometricUnlock(enabled: Bool) throws -> MobileAuthenticationState
|
||||
|
||||
func state() throws -> MobileAuthenticationState
|
||||
|
||||
func touchUserActivity() throws
|
||||
|
||||
func unlockEntry(path: String, passphrase: String?) throws -> MobileAuthenticationState
|
||||
|
||||
}
|
||||
open class MobileAuthentication: MobileAuthenticationProtocol, @unchecked Sendable {
|
||||
fileprivate let handle: UInt64
|
||||
|
||||
/// Used to instantiate a [FFIObject] without an actual handle, for fakes in tests, mostly.
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public struct NoHandle {
|
||||
public init() {}
|
||||
}
|
||||
|
||||
// TODO: We'd like this to be `private` but for Swifty reasons,
|
||||
// we can't implement `FfiConverter` without making this `required` and we can't
|
||||
// make it `required` without making it `public`.
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
required public init(unsafeFromHandle handle: UInt64) {
|
||||
self.handle = handle
|
||||
}
|
||||
|
||||
// This constructor can be used to instantiate a fake object.
|
||||
// - Parameter noHandle: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject].
|
||||
//
|
||||
// - Warning:
|
||||
// Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing handle the FFI lower functions will crash.
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public init(noHandle: NoHandle) {
|
||||
self.handle = 0
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func uniffiCloneHandle() -> UInt64 {
|
||||
return try! rustCall { uniffi_ironstorage_apple_fn_clone_mobileauthentication(self.handle, $0) }
|
||||
}
|
||||
// No primary constructor declared for this class.
|
||||
|
||||
deinit {
|
||||
if handle == 0 {
|
||||
// Mock objects have handle=0 don't try to free them
|
||||
return
|
||||
}
|
||||
|
||||
try! rustCall { uniffi_ironstorage_apple_fn_free_mobileauthentication(handle, $0) }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
open func cancel()throws {try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||
uniffiCallStatus in
|
||||
uniffi_ironstorage_apple_fn_method_mobileauthentication_cancel(
|
||||
self.uniffiCloneHandle(),uniffiCallStatus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
open func manualLock()throws {try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||
uniffiCallStatus in
|
||||
uniffi_ironstorage_apple_fn_method_mobileauthentication_manual_lock(
|
||||
self.uniffiCloneHandle(),uniffiCallStatus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
open func setBiometricUnlock(enabled: Bool)throws -> MobileAuthenticationState {
|
||||
return try FfiConverterTypeMobileAuthenticationState_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||
uniffiCallStatus in
|
||||
uniffi_ironstorage_apple_fn_method_mobileauthentication_set_biometric_unlock(
|
||||
self.uniffiCloneHandle(),
|
||||
FfiConverterBool.lower(enabled),uniffiCallStatus
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
open func state()throws -> MobileAuthenticationState {
|
||||
return try FfiConverterTypeMobileAuthenticationState_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||
uniffiCallStatus in
|
||||
uniffi_ironstorage_apple_fn_method_mobileauthentication_state(
|
||||
self.uniffiCloneHandle(),uniffiCallStatus
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
open func touchUserActivity()throws {try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||
uniffiCallStatus in
|
||||
uniffi_ironstorage_apple_fn_method_mobileauthentication_touch_user_activity(
|
||||
self.uniffiCloneHandle(),uniffiCallStatus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
open func unlockEntry(path: String, passphrase: String?)throws -> MobileAuthenticationState {
|
||||
return try FfiConverterTypeMobileAuthenticationState_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||
uniffiCallStatus in
|
||||
uniffi_ironstorage_apple_fn_method_mobileauthentication_unlock_entry(
|
||||
self.uniffiCloneHandle(),
|
||||
FfiConverterString.lower(path),
|
||||
FfiConverterOptionString.lower(passphrase),uniffiCallStatus
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public struct FfiConverterTypeMobileAuthentication: FfiConverter {
|
||||
typealias FfiType = UInt64
|
||||
typealias SwiftType = MobileAuthentication
|
||||
|
||||
public static func lift(_ handle: UInt64) throws -> MobileAuthentication {
|
||||
return MobileAuthentication(unsafeFromHandle: handle)
|
||||
}
|
||||
|
||||
public static func lower(_ value: MobileAuthentication) -> UInt64 {
|
||||
return value.uniffiCloneHandle()
|
||||
}
|
||||
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileAuthentication {
|
||||
let handle: UInt64 = try readInt(&buf)
|
||||
return try lift(handle)
|
||||
}
|
||||
|
||||
public static func write(_ value: MobileAuthentication, into buf: inout [UInt8]) {
|
||||
writeInt(&buf, lower(value))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileAuthentication_lift(_ handle: UInt64) throws -> MobileAuthentication {
|
||||
return try FfiConverterTypeMobileAuthentication.lift(handle)
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileAuthentication_lower(_ value: MobileAuthentication) -> UInt64 {
|
||||
return FfiConverterTypeMobileAuthentication.lower(value)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public protocol MobileHomeOperationProtocol: AnyObject, Sendable {
|
||||
|
||||
func cached() throws -> MobileHomePage
|
||||
@@ -887,6 +1075,64 @@ public func FfiConverterTypeMobileOnboardingOperation_lower(_ value: MobileOnboa
|
||||
|
||||
|
||||
|
||||
public struct MobileAuthenticationState: Equatable, Hashable {
|
||||
public var unlocked: Bool
|
||||
public var biometricUnlockEnabled: Bool
|
||||
public var remainingSeconds: UInt64
|
||||
|
||||
// Default memberwise initializers are never public by default, so we
|
||||
// declare one manually.
|
||||
public init(unlocked: Bool, biometricUnlockEnabled: Bool, remainingSeconds: UInt64) {
|
||||
self.unlocked = unlocked
|
||||
self.biometricUnlockEnabled = biometricUnlockEnabled
|
||||
self.remainingSeconds = remainingSeconds
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
#if compiler(>=6)
|
||||
extension MobileAuthenticationState: Sendable {}
|
||||
#endif
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public struct FfiConverterTypeMobileAuthenticationState: FfiConverterRustBuffer {
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileAuthenticationState {
|
||||
return
|
||||
try MobileAuthenticationState(
|
||||
unlocked: FfiConverterBool.read(from: &buf),
|
||||
biometricUnlockEnabled: FfiConverterBool.read(from: &buf),
|
||||
remainingSeconds: FfiConverterUInt64.read(from: &buf)
|
||||
)
|
||||
}
|
||||
|
||||
public static func write(_ value: MobileAuthenticationState, into buf: inout [UInt8]) {
|
||||
FfiConverterBool.write(value.unlocked, into: &buf)
|
||||
FfiConverterBool.write(value.biometricUnlockEnabled, into: &buf)
|
||||
FfiConverterUInt64.write(value.remainingSeconds, into: &buf)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileAuthenticationState_lift(_ buf: RustBuffer) throws -> MobileAuthenticationState {
|
||||
return try FfiConverterTypeMobileAuthenticationState.lift(buf)
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileAuthenticationState_lower(_ value: MobileAuthenticationState) -> RustBuffer {
|
||||
return FfiConverterTypeMobileAuthenticationState.lower(value)
|
||||
}
|
||||
|
||||
|
||||
public struct MobileHomeChange: Equatable, Hashable {
|
||||
public var id: String
|
||||
public var title: String
|
||||
@@ -1710,6 +1956,200 @@ public func FfiConverterTypeMobileShell_lower(_ value: MobileShell) -> RustBuffe
|
||||
|
||||
|
||||
|
||||
public enum MobileAuthenticationErrorKind: Equatable, Hashable {
|
||||
|
||||
case passphraseRequired
|
||||
case invalidPassphrase
|
||||
case cancelled
|
||||
case biometryUnavailable
|
||||
case configuration
|
||||
case keyMaterial
|
||||
case entry
|
||||
case secureStorage
|
||||
case expired
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
#if compiler(>=6)
|
||||
extension MobileAuthenticationErrorKind: Sendable {}
|
||||
#endif
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public struct FfiConverterTypeMobileAuthenticationErrorKind: FfiConverterRustBuffer {
|
||||
typealias SwiftType = MobileAuthenticationErrorKind
|
||||
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileAuthenticationErrorKind {
|
||||
let variant: Int32 = try readInt(&buf)
|
||||
switch variant {
|
||||
|
||||
case 1: return .passphraseRequired
|
||||
|
||||
case 2: return .invalidPassphrase
|
||||
|
||||
case 3: return .cancelled
|
||||
|
||||
case 4: return .biometryUnavailable
|
||||
|
||||
case 5: return .configuration
|
||||
|
||||
case 6: return .keyMaterial
|
||||
|
||||
case 7: return .entry
|
||||
|
||||
case 8: return .secureStorage
|
||||
|
||||
case 9: return .expired
|
||||
|
||||
default: throw UniffiInternalError.unexpectedEnumCase
|
||||
}
|
||||
}
|
||||
|
||||
public static func write(_ value: MobileAuthenticationErrorKind, into buf: inout [UInt8]) {
|
||||
switch value {
|
||||
|
||||
|
||||
case .passphraseRequired:
|
||||
writeInt(&buf, Int32(1))
|
||||
|
||||
|
||||
case .invalidPassphrase:
|
||||
writeInt(&buf, Int32(2))
|
||||
|
||||
|
||||
case .cancelled:
|
||||
writeInt(&buf, Int32(3))
|
||||
|
||||
|
||||
case .biometryUnavailable:
|
||||
writeInt(&buf, Int32(4))
|
||||
|
||||
|
||||
case .configuration:
|
||||
writeInt(&buf, Int32(5))
|
||||
|
||||
|
||||
case .keyMaterial:
|
||||
writeInt(&buf, Int32(6))
|
||||
|
||||
|
||||
case .entry:
|
||||
writeInt(&buf, Int32(7))
|
||||
|
||||
|
||||
case .secureStorage:
|
||||
writeInt(&buf, Int32(8))
|
||||
|
||||
|
||||
case .expired:
|
||||
writeInt(&buf, Int32(9))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileAuthenticationErrorKind_lift(_ buf: RustBuffer) throws -> MobileAuthenticationErrorKind {
|
||||
return try FfiConverterTypeMobileAuthenticationErrorKind.lift(buf)
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileAuthenticationErrorKind_lower(_ value: MobileAuthenticationErrorKind) -> RustBuffer {
|
||||
return FfiConverterTypeMobileAuthenticationErrorKind.lower(value)
|
||||
}
|
||||
|
||||
|
||||
|
||||
public
|
||||
enum MobileAuthenticationFfiError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
|
||||
|
||||
|
||||
|
||||
case Failed(kind: MobileAuthenticationErrorKind, title: String, detail: String
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public var errorDescription: String? {
|
||||
String(reflecting: self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if compiler(>=6)
|
||||
extension MobileAuthenticationFfiError: Sendable {}
|
||||
#endif
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public struct FfiConverterTypeMobileAuthenticationFfiError: FfiConverterRustBuffer {
|
||||
typealias SwiftType = MobileAuthenticationFfiError
|
||||
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileAuthenticationFfiError {
|
||||
let variant: Int32 = try readInt(&buf)
|
||||
switch variant {
|
||||
|
||||
|
||||
|
||||
|
||||
case 1: return .Failed(
|
||||
kind: try FfiConverterTypeMobileAuthenticationErrorKind.read(from: &buf),
|
||||
title: try FfiConverterString.read(from: &buf),
|
||||
detail: try FfiConverterString.read(from: &buf)
|
||||
)
|
||||
|
||||
default: throw UniffiInternalError.unexpectedEnumCase
|
||||
}
|
||||
}
|
||||
|
||||
public static func write(_ value: MobileAuthenticationFfiError, into buf: inout [UInt8]) {
|
||||
switch value {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
case let .Failed(kind,title,detail):
|
||||
writeInt(&buf, Int32(1))
|
||||
FfiConverterTypeMobileAuthenticationErrorKind.write(kind, into: &buf)
|
||||
FfiConverterString.write(title, into: &buf)
|
||||
FfiConverterString.write(detail, into: &buf)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileAuthenticationFfiError_lift(_ buf: RustBuffer) throws -> MobileAuthenticationFfiError {
|
||||
return try FfiConverterTypeMobileAuthenticationFfiError.lift(buf)
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobileAuthenticationFfiError_lower(_ value: MobileAuthenticationFfiError) -> RustBuffer {
|
||||
return FfiConverterTypeMobileAuthenticationFfiError.lower(value)
|
||||
}
|
||||
|
||||
|
||||
|
||||
public enum MobileHomeChangeKind: Equatable, Hashable {
|
||||
|
||||
case passwordEntry
|
||||
@@ -3198,6 +3638,13 @@ fileprivate struct FfiConverterSequenceTypeMobilePasswordRow: FfiConverterRustBu
|
||||
return seq
|
||||
}
|
||||
}
|
||||
public func mobileAuthentication()throws -> MobileAuthentication {
|
||||
return try FfiConverterTypeMobileAuthentication_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||
uniffiCallStatus in
|
||||
uniffi_ironstorage_apple_fn_func_mobile_authentication(uniffiCallStatus
|
||||
)
|
||||
})
|
||||
}
|
||||
public func mobileHomeOperation() -> MobileHomeOperation {
|
||||
return try! FfiConverterTypeMobileHomeOperation_lift(try! rustCall() {
|
||||
uniffiCallStatus in
|
||||
@@ -3277,6 +3724,9 @@ private let initializationResult: InitializationResult = {
|
||||
if bindings_contract_version != scaffolding_contract_version {
|
||||
return InitializationResult.contractVersionMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_func_mobile_authentication() != 38258) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_func_mobile_home_operation() != 10595) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
@@ -3301,6 +3751,24 @@ private let initializationResult: InitializationResult = {
|
||||
if (uniffi_ironstorage_apple_checksum_func_set_selected_mobile_tab() != 65280) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_cancel() != 6512) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_manual_lock() != 57220) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_biometric_unlock() != 9486) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_state() != 60826) {
|
||||
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_mobilehomeoperation_cached() != 32436) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
|
||||
@@ -242,6 +242,46 @@ typedef struct UniffiForeignFutureResultVoid {
|
||||
typedef void (*UniffiForeignFutureCompleteVoid)(uint64_t, UniffiForeignFutureResultVoid
|
||||
);
|
||||
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILEAUTHENTICATION
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILEAUTHENTICATION
|
||||
uint64_t uniffi_ironstorage_apple_fn_clone_mobileauthentication(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FREE_MOBILEAUTHENTICATION
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FREE_MOBILEAUTHENTICATION
|
||||
void uniffi_ironstorage_apple_fn_free_mobileauthentication(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_CANCEL
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_CANCEL
|
||||
void uniffi_ironstorage_apple_fn_method_mobileauthentication_cancel(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_MANUAL_LOCK
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_MANUAL_LOCK
|
||||
void uniffi_ironstorage_apple_fn_method_mobileauthentication_manual_lock(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_BIOMETRIC_UNLOCK
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_BIOMETRIC_UNLOCK
|
||||
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_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_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
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_UNLOCK_ENTRY
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_UNLOCK_ENTRY
|
||||
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_CLONE_MOBILEHOMEOPERATION
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILEHOMEOPERATION
|
||||
@@ -311,6 +351,12 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobileonboardingoperation_progress
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEONBOARDINGOPERATION_SETUP
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEONBOARDINGOPERATION_SETUP
|
||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileonboardingoperation_setup(uint64_t ptr, RustBuffer branch, int8_t use_existing, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_AUTHENTICATION
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_AUTHENTICATION
|
||||
uint64_t uniffi_ironstorage_apple_fn_func_mobile_authentication(RustCallStatus *_Nonnull out_status
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_HOME_OPERATION
|
||||
@@ -614,6 +660,12 @@ void ffi_ironstorage_apple_rust_future_free_void(uint64_t handle
|
||||
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_APPLE_RUST_FUTURE_COMPLETE_VOID
|
||||
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_APPLE_RUST_FUTURE_COMPLETE_VOID
|
||||
void ffi_ironstorage_apple_rust_future_complete_void(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_AUTHENTICATION
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_AUTHENTICATION
|
||||
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_authentication(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_HOME_OPERATION
|
||||
@@ -662,6 +714,42 @@ uint16_t uniffi_ironstorage_apple_checksum_func_replace_configured_mobile_applic
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_SET_SELECTED_MOBILE_TAB
|
||||
uint16_t uniffi_ironstorage_apple_checksum_func_set_selected_mobile_tab(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_CANCEL
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_CANCEL
|
||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_cancel(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_MANUAL_LOCK
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_MANUAL_LOCK
|
||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_manual_lock(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_BIOMETRIC_UNLOCK
|
||||
#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_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_TOUCH_USER_ACTIVITY
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_TOUCH_USER_ACTIVITY
|
||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_touch_user_activity(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_UNLOCK_ENTRY
|
||||
#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_MOBILEHOMEOPERATION_CACHED
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
<string>1</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Unlock your GPG key for protected password operations.</string>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
|
||||
@@ -4,6 +4,9 @@ extension Notification.Name {
|
||||
static let ironStorageLocalStoreDidChange = Notification.Name(
|
||||
"de.rfc1437.ironstorage.local-store-did-change"
|
||||
)
|
||||
static let ironStorageAuthenticationDidChange = Notification.Name(
|
||||
"de.rfc1437.ironstorage.authentication-did-change"
|
||||
)
|
||||
}
|
||||
|
||||
@main
|
||||
@@ -33,15 +36,27 @@ private protocol MobileTabRoot: AnyObject {
|
||||
@MainActor
|
||||
private final class AppContext: NSObject, UITabBarControllerDelegate {
|
||||
private let tabs = UITabBarController()
|
||||
private let authentication = try? mobileAuthentication()
|
||||
private var navigationControllers: [UINavigationController] = []
|
||||
private var restoreTask: Task<Void, Never>?
|
||||
private var authenticationMonitor: Task<Void, Never>?
|
||||
|
||||
deinit {
|
||||
restoreTask?.cancel()
|
||||
authenticationMonitor?.cancel()
|
||||
}
|
||||
|
||||
func makeRootController() -> UIViewController {
|
||||
let shell = mobileShellFixture(state: .loading)
|
||||
navigationControllers = shell.pages.map { page in
|
||||
let root: UIViewController = page.tab == .passwords
|
||||
? PasswordDirectoryViewController(shellPage: page)
|
||||
: ShellViewController(page: page)
|
||||
let root: UIViewController = switch page.tab {
|
||||
case .passwords:
|
||||
PasswordDirectoryViewController(shellPage: page, authentication: authentication)
|
||||
case .preferences:
|
||||
PreferencesViewController(page: page, authentication: authentication)
|
||||
default:
|
||||
ShellViewController(page: page)
|
||||
}
|
||||
let navigation = UINavigationController(rootViewController: root)
|
||||
navigation.navigationBar.prefersLargeTitles = true
|
||||
navigation.tabBarItem = UITabBarItem(
|
||||
@@ -54,6 +69,7 @@ private final class AppContext: NSObject, UITabBarControllerDelegate {
|
||||
tabs.viewControllers = navigationControllers
|
||||
tabs.delegate = self
|
||||
restoreSelectedTab()
|
||||
monitorAuthentication()
|
||||
return tabs
|
||||
}
|
||||
|
||||
@@ -90,6 +106,29 @@ private final class AppContext: NSObject, UITabBarControllerDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func monitorAuthentication() {
|
||||
authenticationMonitor?.cancel()
|
||||
guard let authentication else { return }
|
||||
authenticationMonitor = Task {
|
||||
var wasUnlocked = (try? authentication.state().unlocked) ?? false
|
||||
while !Task.isCancelled {
|
||||
do {
|
||||
try await Task.sleep(for: .seconds(1))
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
let isUnlocked = (try? authentication.state().unlocked) ?? false
|
||||
if isUnlocked != wasUnlocked {
|
||||
wasUnlocked = isUnlocked
|
||||
NotificationCenter.default.post(
|
||||
name: .ironStorageAuthenticationDidChange,
|
||||
object: authentication
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -614,17 +653,245 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class PreferencesViewController: UITableViewController, MobileTabRoot {
|
||||
fileprivate let shellTab = MobileTab.preferences
|
||||
private var page: MobilePage
|
||||
private let authentication: MobileAuthentication?
|
||||
private var state: MobileAuthenticationState?
|
||||
private var preferenceTask: Task<Void, Never>?
|
||||
private var loadTask: Task<Void, Never>?
|
||||
private var loadGeneration = 0
|
||||
|
||||
init(page: MobilePage, authentication: MobileAuthentication?) {
|
||||
self.page = page
|
||||
self.authentication = authentication
|
||||
super.init(style: .insetGrouped)
|
||||
title = page.title
|
||||
navigationItem.largeTitleDisplayMode = .always
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
title: "Update Token",
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(tokenUpdateRequested)
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(authenticationDidChange),
|
||||
name: .ironStorageAuthenticationDidChange,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) is not supported")
|
||||
}
|
||||
|
||||
deinit {
|
||||
preferenceTask?.cancel()
|
||||
loadTask?.cancel()
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
reloadShell()
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
page.state == .ready ? 2 : 0
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
numberOfRowsInSection section: Int
|
||||
) -> Int {
|
||||
section == 0 ? 1 : 2
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
titleForHeaderInSection section: Int
|
||||
) -> String? {
|
||||
section == 0 ? "Secure Unlock" : "Authentication Session"
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
titleForFooterInSection section: Int
|
||||
) -> String? {
|
||||
if section == 0 {
|
||||
return "When enabled, the GPG passphrase is device-only, requires a device passcode, and is invalidated when enrolled biometrics change."
|
||||
}
|
||||
return "Manual lock and inactivity expiry immediately revoke the shared Rust authentication lease."
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath
|
||||
) -> UITableViewCell {
|
||||
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
|
||||
var content = cell.defaultContentConfiguration()
|
||||
if indexPath.section == 0 {
|
||||
content.image = UIImage(systemName: "faceid")
|
||||
content.text = "Biometric Unlock"
|
||||
content.secondaryText = state?.biometricUnlockEnabled == true
|
||||
? "Protected passphrase enrolled"
|
||||
: "Manual passphrase required"
|
||||
let toggle = UISwitch()
|
||||
toggle.isOn = state?.biometricUnlockEnabled == true
|
||||
toggle.isEnabled = authentication != nil
|
||||
toggle.addTarget(self, action: #selector(biometricToggleChanged(_:)), for: .valueChanged)
|
||||
toggle.accessibilityLabel = "Biometric Unlock"
|
||||
cell.accessoryView = toggle
|
||||
cell.selectionStyle = .none
|
||||
} else if indexPath.row == 0 {
|
||||
let unlocked = state?.unlocked == true
|
||||
content.image = UIImage(systemName: unlocked ? "lock.open.fill" : "lock.fill")
|
||||
content.text = unlocked ? "Unlocked" : "Locked"
|
||||
content.secondaryText = unlocked
|
||||
? "Locks in \(state?.remainingSeconds ?? 0) seconds without activity"
|
||||
: "Protected content is masked"
|
||||
cell.selectionStyle = .none
|
||||
} else {
|
||||
content.image = UIImage(systemName: "lock.fill")
|
||||
content.text = "Lock Now"
|
||||
content.textProperties.color = .systemRed
|
||||
content.secondaryText = "Revoke all active authentication handles"
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
cell.isUserInteractionEnabled = state?.unlocked == true
|
||||
cell.contentView.alpha = state?.unlocked == true ? 1 : 0.45
|
||||
}
|
||||
content.secondaryTextProperties.numberOfLines = 0
|
||||
cell.contentConfiguration = content
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
guard indexPath.section == 1, indexPath.row == 1, let authentication else { return }
|
||||
do {
|
||||
try authentication.manualLock()
|
||||
refreshState()
|
||||
NotificationCenter.default.post(
|
||||
name: .ironStorageAuthenticationDidChange,
|
||||
object: authentication
|
||||
)
|
||||
UIAccessibility.post(notification: .announcement, argument: "IronStorage locked")
|
||||
} catch let error as MobileAuthenticationFfiError {
|
||||
presentAuthenticationFailure(AuthenticationFailure(error))
|
||||
} catch {
|
||||
presentAuthenticationFailure(.unexpected)
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
apply(page)
|
||||
}
|
||||
|
||||
private func reloadShell() {
|
||||
loadGeneration += 1
|
||||
let generation = loadGeneration
|
||||
loadTask?.cancel()
|
||||
loadTask = Task { [weak self] in
|
||||
let shell = await Task.detached(priority: .userInitiated) { mobileShell() }.value
|
||||
guard
|
||||
!Task.isCancelled,
|
||||
let self,
|
||||
generation == loadGeneration,
|
||||
let page = shell.pages.first(where: { $0.tab == .preferences })
|
||||
else { return }
|
||||
apply(page)
|
||||
}
|
||||
}
|
||||
|
||||
private func apply(_ page: MobilePage) {
|
||||
self.page = page
|
||||
title = page.title
|
||||
guard page.state == .ready else {
|
||||
var configuration = UIContentUnavailableConfiguration.empty()
|
||||
configuration.image = UIImage(systemName: "gearshape")
|
||||
configuration.text = page.stateTitle
|
||||
configuration.secondaryText = page.stateDetail
|
||||
contentUnavailableConfiguration = configuration
|
||||
tableView.reloadData()
|
||||
return
|
||||
}
|
||||
contentUnavailableConfiguration = nil
|
||||
refreshState()
|
||||
}
|
||||
|
||||
@objc private func tokenUpdateRequested() {
|
||||
navigationController?.pushViewController(TokenUpdateViewController(), animated: true)
|
||||
}
|
||||
|
||||
@objc private func authenticationDidChange() {
|
||||
refreshState()
|
||||
}
|
||||
|
||||
@objc private func biometricToggleChanged(_ sender: UISwitch) {
|
||||
guard let authentication else {
|
||||
sender.setOn(false, animated: true)
|
||||
presentAuthenticationFailure(.unavailable)
|
||||
return
|
||||
}
|
||||
sender.isEnabled = false
|
||||
let enabled = sender.isOn
|
||||
preferenceTask?.cancel()
|
||||
preferenceTask = Task { [weak self, weak sender] in
|
||||
let result = await Task.detached(priority: .userInitiated) {
|
||||
do {
|
||||
return Result<MobileAuthenticationState, AuthenticationFailure>.success(
|
||||
try authentication.setBiometricUnlock(enabled: enabled)
|
||||
)
|
||||
} catch let error as MobileAuthenticationFfiError {
|
||||
return .failure(AuthenticationFailure(error))
|
||||
} catch {
|
||||
return .failure(.unexpected)
|
||||
}
|
||||
}.value
|
||||
guard !Task.isCancelled, let self else { return }
|
||||
sender?.isEnabled = true
|
||||
switch result {
|
||||
case let .success(state):
|
||||
self.state = state
|
||||
tableView.reloadData()
|
||||
NotificationCenter.default.post(
|
||||
name: .ironStorageAuthenticationDidChange,
|
||||
object: authentication
|
||||
)
|
||||
case let .failure(failure):
|
||||
sender?.setOn(!enabled, animated: true)
|
||||
presentAuthenticationFailure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshState() {
|
||||
state = try? authentication?.state()
|
||||
tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class PasswordDirectoryViewController: UITableViewController, MobileTabRoot {
|
||||
fileprivate let shellTab = MobileTab.passwords
|
||||
private let path: String?
|
||||
private let authentication: MobileAuthentication?
|
||||
private var shellPage: MobilePage?
|
||||
private var directoryPage: MobilePasswordPage?
|
||||
private var loadTask: Task<Void, Never>?
|
||||
private var loadGeneration = 0
|
||||
|
||||
init(shellPage: MobilePage, path: String? = nil) {
|
||||
init(
|
||||
shellPage: MobilePage,
|
||||
authentication: MobileAuthentication?,
|
||||
path: String? = nil
|
||||
) {
|
||||
self.shellPage = shellPage
|
||||
self.authentication = authentication
|
||||
self.path = path
|
||||
super.init(style: .insetGrouped)
|
||||
title = shellPage.title
|
||||
@@ -639,8 +906,9 @@ private final class PasswordDirectoryViewController: UITableViewController, Mobi
|
||||
)
|
||||
}
|
||||
|
||||
private init(path: String, title: String) {
|
||||
private init(path: String, title: String, authentication: MobileAuthentication?) {
|
||||
self.path = path
|
||||
self.authentication = authentication
|
||||
shellPage = nil
|
||||
super.init(style: .insetGrouped)
|
||||
self.title = title
|
||||
@@ -719,9 +987,13 @@ private final class PasswordDirectoryViewController: UITableViewController, Mobi
|
||||
guard let row = directoryPage?.rows[indexPath.row] else { return }
|
||||
let controller: UIViewController = switch row.kind {
|
||||
case .directory:
|
||||
PasswordDirectoryViewController(path: row.path, title: row.title)
|
||||
PasswordDirectoryViewController(
|
||||
path: row.path,
|
||||
title: row.title,
|
||||
authentication: authentication
|
||||
)
|
||||
case .entry:
|
||||
LockedPasswordViewController(entry: row)
|
||||
LockedPasswordViewController(entry: row, authentication: authentication)
|
||||
}
|
||||
navigationController?.pushViewController(controller, animated: true)
|
||||
}
|
||||
@@ -872,9 +1144,17 @@ private struct PasswordFailure: Error, Sendable {
|
||||
@MainActor
|
||||
private final class LockedPasswordViewController: UIViewController {
|
||||
private let entry: MobilePasswordRow
|
||||
private let authentication: MobileAuthentication?
|
||||
private let imageView = UIImageView()
|
||||
private let titleLabel = UILabel()
|
||||
private let detailLabel = UILabel()
|
||||
private let unlockButton = UIButton(type: .system)
|
||||
private var unlockTask: Task<Void, Never>?
|
||||
private var state: MobileAuthenticationState?
|
||||
|
||||
init(entry: MobilePasswordRow) {
|
||||
init(entry: MobilePasswordRow, authentication: MobileAuthentication?) {
|
||||
self.entry = entry
|
||||
self.authentication = authentication
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
title = entry.title
|
||||
navigationItem.largeTitleDisplayMode = .never
|
||||
@@ -888,12 +1168,230 @@ private final class LockedPasswordViewController: UIViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .systemGroupedBackground
|
||||
var configuration = UIContentUnavailableConfiguration.empty()
|
||||
configuration.image = UIImage(systemName: "lock.fill")
|
||||
configuration.text = "Locked Password"
|
||||
configuration.secondaryText = "Authenticate to reveal this password entry."
|
||||
contentUnavailableConfiguration = configuration
|
||||
imageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: 44)
|
||||
imageView.tintColor = .secondaryLabel
|
||||
titleLabel.font = .preferredFont(forTextStyle: .title2)
|
||||
titleLabel.adjustsFontForContentSizeCategory = true
|
||||
titleLabel.textAlignment = .center
|
||||
detailLabel.font = .preferredFont(forTextStyle: .body)
|
||||
detailLabel.adjustsFontForContentSizeCategory = true
|
||||
detailLabel.textAlignment = .center
|
||||
detailLabel.textColor = .secondaryLabel
|
||||
detailLabel.numberOfLines = 0
|
||||
unlockButton.configuration = .filled()
|
||||
unlockButton.configuration?.cornerStyle = .capsule
|
||||
unlockButton.addTarget(self, action: #selector(unlockRequested), for: .touchUpInside)
|
||||
let stack = UIStackView(arrangedSubviews: [imageView, titleLabel, detailLabel, unlockButton])
|
||||
stack.axis = .vertical
|
||||
stack.alignment = .center
|
||||
stack.spacing = 12
|
||||
stack.setCustomSpacing(24, after: detailLabel)
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(stack)
|
||||
NSLayoutConstraint.activate([
|
||||
stack.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
|
||||
stack.leadingAnchor.constraint(greaterThanOrEqualTo: view.layoutMarginsGuide.leadingAnchor),
|
||||
stack.trailingAnchor.constraint(lessThanOrEqualTo: view.layoutMarginsGuide.trailingAnchor),
|
||||
detailLabel.widthAnchor.constraint(lessThanOrEqualToConstant: 360),
|
||||
unlockButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 140),
|
||||
])
|
||||
view.accessibilityIdentifier = entry.id
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(authenticationDidChange),
|
||||
name: .ironStorageAuthenticationDidChange,
|
||||
object: nil
|
||||
)
|
||||
render()
|
||||
}
|
||||
|
||||
deinit {
|
||||
unlockTask?.cancel()
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
refreshState()
|
||||
}
|
||||
|
||||
@objc private func authenticationDidChange() {
|
||||
refreshState()
|
||||
}
|
||||
|
||||
@objc private func unlockRequested() {
|
||||
if state?.unlocked == true, let authentication {
|
||||
do {
|
||||
try authentication.touchUserActivity()
|
||||
refreshState()
|
||||
UIAccessibility.post(notification: .announcement, argument: "Unlock extended")
|
||||
} catch let error as MobileAuthenticationFfiError {
|
||||
presentAuthenticationFailure(AuthenticationFailure(error))
|
||||
} catch {
|
||||
presentAuthenticationFailure(.unexpected)
|
||||
}
|
||||
return
|
||||
}
|
||||
unlock(passphrase: nil)
|
||||
}
|
||||
|
||||
@objc private func lockRequested() {
|
||||
guard let authentication else { return }
|
||||
do {
|
||||
try authentication.manualLock()
|
||||
state = try authentication.state()
|
||||
NotificationCenter.default.post(
|
||||
name: .ironStorageAuthenticationDidChange,
|
||||
object: authentication
|
||||
)
|
||||
render()
|
||||
} catch let error as MobileAuthenticationFfiError {
|
||||
presentAuthenticationFailure(AuthenticationFailure(error))
|
||||
} catch {
|
||||
presentAuthenticationFailure(.unexpected)
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshState() {
|
||||
state = try? authentication?.state()
|
||||
render()
|
||||
}
|
||||
|
||||
private func unlock(passphrase: String?) {
|
||||
guard let authentication else {
|
||||
presentAuthenticationFailure(.unavailable)
|
||||
return
|
||||
}
|
||||
unlockTask?.cancel()
|
||||
unlockButton.isEnabled = false
|
||||
unlockButton.configuration?.showsActivityIndicator = true
|
||||
let path = entry.path
|
||||
unlockTask = Task { [weak self] in
|
||||
let result = await Task.detached(priority: .userInitiated) {
|
||||
do {
|
||||
return Result<MobileAuthenticationState, AuthenticationFailure>.success(
|
||||
try authentication.unlockEntry(path: path, passphrase: passphrase)
|
||||
)
|
||||
} catch let error as MobileAuthenticationFfiError {
|
||||
return .failure(AuthenticationFailure(error))
|
||||
} catch {
|
||||
return .failure(.unexpected)
|
||||
}
|
||||
}.value
|
||||
guard !Task.isCancelled, let self else { return }
|
||||
unlockButton.isEnabled = true
|
||||
unlockButton.configuration?.showsActivityIndicator = false
|
||||
switch result {
|
||||
case let .success(state):
|
||||
self.state = state
|
||||
NotificationCenter.default.post(
|
||||
name: .ironStorageAuthenticationDidChange,
|
||||
object: authentication
|
||||
)
|
||||
render()
|
||||
UIAccessibility.post(notification: .announcement, argument: "Password unlocked")
|
||||
case let .failure(failure):
|
||||
if passphrase == nil,
|
||||
failure.kind == .passphraseRequired || failure.kind == .biometryUnavailable {
|
||||
promptForPassphrase(message: failure.detail)
|
||||
} else if failure.kind != .cancelled {
|
||||
presentAuthenticationFailure(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))
|
||||
alert.addAction(UIAlertAction(title: "Unlock", style: .default) { [weak self, weak alert] _ in
|
||||
guard let field = alert?.textFields?.first, let value = field.text, !value.isEmpty else {
|
||||
return
|
||||
}
|
||||
field.text = nil
|
||||
self?.unlock(passphrase: value)
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func render() {
|
||||
let unlocked = state?.unlocked == true
|
||||
imageView.image = UIImage(systemName: unlocked ? "lock.open.fill" : "lock.fill")
|
||||
titleLabel.text = unlocked ? "Key Unlocked" : "Locked Password"
|
||||
detailLabel.text = if unlocked {
|
||||
"The GPG key is available for protected operations for up to \(state?.remainingSeconds ?? 0) seconds."
|
||||
} else {
|
||||
"Authenticate to unlock this password entry. Browsing remains available while locked."
|
||||
}
|
||||
unlockButton.configuration?.title = unlocked ? "Extend Unlock" : "Unlock"
|
||||
navigationItem.rightBarButtonItem = unlocked
|
||||
? UIBarButtonItem(
|
||||
image: UIImage(systemName: "lock.fill"),
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(lockRequested)
|
||||
)
|
||||
: nil
|
||||
navigationItem.rightBarButtonItem?.accessibilityLabel = "Lock IronStorage"
|
||||
unlockButton.accessibilityHint = unlocked
|
||||
? "Extends access after authentication."
|
||||
: "Requests biometric authentication or the GPG key passphrase."
|
||||
}
|
||||
}
|
||||
|
||||
private struct AuthenticationFailure: Error, Sendable {
|
||||
let kind: MobileAuthenticationErrorKind
|
||||
let title: String
|
||||
let detail: String
|
||||
|
||||
init(_ error: MobileAuthenticationFfiError) {
|
||||
switch error {
|
||||
case let .Failed(kind, title, detail):
|
||||
self.kind = kind
|
||||
self.title = title
|
||||
self.detail = detail
|
||||
}
|
||||
}
|
||||
|
||||
static let unexpected = AuthenticationFailure(
|
||||
kind: .secureStorage,
|
||||
title: "Unlock Failed",
|
||||
detail: "IronStorage could not complete authentication."
|
||||
)
|
||||
|
||||
static let unavailable = AuthenticationFailure(
|
||||
kind: .configuration,
|
||||
title: "Authentication Is Unavailable",
|
||||
detail: "Finish password-store setup before unlocking entries."
|
||||
)
|
||||
|
||||
private init(kind: MobileAuthenticationErrorKind, title: String, detail: String) {
|
||||
self.kind = kind
|
||||
self.title = title
|
||||
self.detail = detail
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private extension UIViewController {
|
||||
func presentAuthenticationFailure(_ failure: AuthenticationFailure) {
|
||||
let alert = UIAlertController(
|
||||
title: failure.title,
|
||||
message: failure.detail,
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ targets:
|
||||
properties:
|
||||
CFBundleDisplayName: IronStorage
|
||||
ITSAppUsesNonExemptEncryption: false
|
||||
NSFaceIDUsageDescription: Unlock your GPG key for protected password operations.
|
||||
UILaunchScreen: {}
|
||||
UISupportedInterfaceOrientations:
|
||||
- UIInterfaceOrientationPortrait
|
||||
|
||||
Reference in New Issue
Block a user