From e02898ad516bdd3d52339caf91f064e190480563 Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Sun, 15 Dec 2024 18:06:17 -0600 Subject: [PATCH 01/10] Only clear cover if app in active --- ios/Cove/CoveApp.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ios/Cove/CoveApp.swift b/ios/Cove/CoveApp.swift index f18c4c2..47258c0 100644 --- a/ios/Cove/CoveApp.swift +++ b/ios/Cove/CoveApp.swift @@ -443,7 +443,7 @@ struct CoveApp: App { // prevent getting stuck on show cover coverClearTask = Task { try? await Task.sleep(for: .milliseconds(200)) - showCover = false + if phase == .active { showCover = false } } app.lockState = .locked @@ -452,6 +452,7 @@ struct CoveApp: App { // close all open sheets when going into the background if app.isAuthEnabled, oldPhase == .inactive, newPhase == .background { + coverClearTask?.cancel() lockedAt = Date.now UIApplication.shared.connectedScenes From 7ff576e6810be164fe9e510f815153cfeb566a58 Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Mon, 16 Dec 2024 09:44:49 -0600 Subject: [PATCH 02/10] Remove unnecessary use auth toggle --- ios/Cove/HomeScreens/SettingsScreen.swift | 82 +++++++++++------------ ios/Cove/SettingsScreen/WipePin.swift | 67 ++++++++++++++++++ rust/src/app.rs | 17 +---- 3 files changed, 107 insertions(+), 59 deletions(-) create mode 100644 ios/Cove/SettingsScreen/WipePin.swift diff --git a/ios/Cove/HomeScreens/SettingsScreen.swift b/ios/Cove/HomeScreens/SettingsScreen.swift index e06aef3..af03052 100644 --- a/ios/Cove/HomeScreens/SettingsScreen.swift +++ b/ios/Cove/HomeScreens/SettingsScreen.swift @@ -23,36 +23,33 @@ struct SettingsScreen: View { return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) } - var useAuth: Binding { + var toggleBiometric: Binding { Binding( - get: { app.isAuthEnabled }, + get: { app.authType == AuthType.both || app.authType == AuthType.biometric }, set: { enable in - if enable { return sheetState = .init(.enableAuth) } - - switch app.authType { - case .none: Log.error("Trying to disable auth when auth is not enabled") - default: sheetState = .init(.disableAuth) + if enable { + sheetState = .init(.enableBiometric) + } else { + sheetState = .init(.disableBiometric) } } ) } - var useBiometric: Binding { + var togglePin: Binding { Binding( - get: { app.authType == AuthType.both || app.authType == AuthType.biometric }, + get: { app.authType == AuthType.both || app.authType == AuthType.pin }, set: { enable in - if enable { sheetState = .init(.enableBiometric) } - else { sheetState = .init(.disableBiometric) } + if enable { sheetState = .init(.newPin) } else { sheetState = .init(.removePin) } } ) } - var usePin: Binding { + var toggleWipeMePin: Binding { Binding( get: { app.authType == AuthType.both || app.authType == AuthType.pin }, set: { enable in - if enable { sheetState = .init(.newPin) } - else { sheetState = .init(.removePin) } + if enable { sheetState = .init(.newPin) } else { sheetState = .init(.removePin) } } ) } @@ -106,25 +103,23 @@ struct SettingsScreen: View { NodeSelectionView() Section("Security") { - Toggle(isOn: useAuth) { - Label("Require Authentication", systemImage: "lock.shield") + if canUseBiometrics() { + Toggle(isOn: toggleBiometric) { + Label("Enable Face ID", systemImage: "faceid") + } } - if app.isAuthEnabled { - if canUseBiometrics() { - Toggle(isOn: useBiometric) { - Label("Enable Face ID", systemImage: "faceid") - } - } + Toggle(isOn: togglePin) { + Label("Enable PIN", systemImage: "lock") + } - Toggle(isOn: usePin) { - Label("Enable PIN", systemImage: "lock.fill") + if togglePin.wrappedValue { + Button(action: { sheetState = .init(.changePin) }) { + Label("Change PIN", systemImage: "lock.open.rotation") } - if usePin.wrappedValue { - Button(action: { sheetState = .init(.changePin) }) { - Label("Change PIN", systemImage: "lock.open.rotation") - } + Toggle(isOn: Binding.constant(false)) { + Label("Enable Wipe Data PIN", systemImage: "trash.slash") } } } @@ -217,21 +212,22 @@ struct SettingsScreen: View { private func SheetContent(_ state: TaggedItem) -> some View { switch state.item { case .enableAuth: - LockView( - lockType: .both, - isPinCorrect: { _ in true }, - onUnlock: { pin in - app.dispatch(action: .enableBiometric) - - if !pin.isEmpty { - app.dispatch(action: .setPin(pin)) - } - - sheetState = .none - }, - backAction: { sheetState = .none }, - content: { EmptyView() } - ) + if canUseBiometrics() { + LockView( + lockType: .biometric, + isPinCorrect: { _ in true }, + onUnlock: { pin in + app.dispatch(action: .enableBiometric) + if !pin.isEmpty { app.dispatch(action: .setPin(pin)) } + + sheetState = .none + }, + backAction: { sheetState = .none }, + content: { EmptyView() } + ) + } else { + NewPinView(onComplete: setPin, backAction: { sheetState = .none }) + } case .newPin: NewPinView(onComplete: setPin, backAction: { sheetState = .none }) diff --git a/ios/Cove/SettingsScreen/WipePin.swift b/ios/Cove/SettingsScreen/WipePin.swift new file mode 100644 index 0000000..c57c070 --- /dev/null +++ b/ios/Cove/SettingsScreen/WipePin.swift @@ -0,0 +1,67 @@ +// +// WipePin.swift +// Cove +// +// Created by Praveen Perera on 12/15/24. +// + +import SwiftUI + +private enum PinState { + case pin, new, confirm(String) +} + +struct WipePin: View { + @Environment(AppManager.self) var app + + /// args + var onComplete: (String) -> Void + var backAction: () -> Void + + /// private + @State private var pinState: PinState = .new + + var body: some View { + Group { + switch pinState { + case .pin: + NumberPadPinView( + title: "Enter Current PIN", + isPinCorrect: app.checkPin, + showPin: false, + backAction: backAction, + onUnlock: { _ in + withAnimation { + pinState = .new + } + } + ) + case .new: + NumberPadPinView( + title: "Enter Wipe Me PIN", + isPinCorrect: { _ in true }, + showPin: true, + backAction: backAction, + onUnlock: { enteredPin in + withAnimation { + pinState = .confirm(enteredPin) + } + } + ) + case let .confirm(pinToConfirm): + NumberPadPinView( + title: "Confirm Wipe Me PIN", + isPinCorrect: { $0 == pinToConfirm }, + showPin: true, + backAction: backAction, + onUnlock: onComplete + ) + } + } + } +} + +#Preview { + WipePin(onComplete: { _ in }, backAction: {}) + .environment(AppManager()) +} diff --git a/rust/src/app.rs b/rust/src/app.rs index 212c94d..963b866 100644 --- a/rust/src/app.rs +++ b/rust/src/app.rs @@ -54,8 +54,6 @@ pub enum AppAction { UpdateFiatPrices, UpdateFees, UpdateAuthType(AuthType), - EnableAuth, - DisableAuth, EnableBiometric, DisableBiometric, SetPin(String), @@ -193,23 +191,10 @@ impl App { } AppAction::UpdateAuthType(auth_type) => { - debug!("authType changed, NEW: {auth_type:?}"); + debug!("authType changed, new: {auth_type:?}"); set_auth_type(auth_type); } - AppAction::EnableAuth => { - debug!("enable auth"); - let current_auth_type = FfiApp::global().auth_type(); - if current_auth_type == AuthType::None { - set_auth_type(AuthType::Biometric); - } - } - - AppAction::DisableAuth => { - debug!("disable auth"); - set_auth_type(AuthType::None); - } - AppAction::EnableBiometric => { debug!("enable biometric"); From 999060d84a541e7598ca6321f2f346ab040b8ecd Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Mon, 16 Dec 2024 11:58:17 -0600 Subject: [PATCH 03/10] Refactor all auth concerns into AuthManager --- ios/Cove/AppManager.swift | 3 - ios/Cove/AuthManager.swift | 41 ++ ios/Cove/Cove.swift | 581 +++++++++++++++++++--- ios/Cove/CoveApp.swift | 56 ++- ios/Cove/HomeScreens/SettingsScreen.swift | 132 +++-- ios/Cove/Views/LockView.swift | 35 +- rust/src/app.rs | 81 +-- rust/src/database/global_flag.rs | 5 + rust/src/manager.rs | 1 + rust/src/manager/auth.rs | 169 +++++++ 10 files changed, 884 insertions(+), 220 deletions(-) create mode 100644 ios/Cove/AuthManager.swift create mode 100644 rust/src/manager/auth.rs diff --git a/ios/Cove/AppManager.swift b/ios/Cove/AppManager.swift index b934485..715e182 100644 --- a/ios/Cove/AppManager.swift +++ b/ios/Cove/AppManager.swift @@ -27,9 +27,6 @@ import SwiftUI // changed when route is reset, to clear lifecycle view state var routeId = UUID() - @MainActor - var isUsingBiometrics: Bool = false - @ObservationIgnored weak var walletManager: WalletManager? diff --git a/ios/Cove/AuthManager.swift b/ios/Cove/AuthManager.swift new file mode 100644 index 0000000..9e06ea4 --- /dev/null +++ b/ios/Cove/AuthManager.swift @@ -0,0 +1,41 @@ +import SwiftUI + +@Observable class AuthManager: AuthManagerReconciler { + private let logger = Log(id: "AuthManager") + var rust: RustAuthManager + var authType = Database().globalConfig().authType() + + @MainActor + var isUsingBiometrics: Bool = false + + public init() { + rust = RustAuthManager() + rust.listenForUpdates(reconciler: self) + } + + public var isAuthEnabled: Bool { + authType != AuthType.none + } + + public func checkPin(_ pin: String) -> Bool { + AuthPin().check(pin: pin) + } + + func reconcile(message: AuthManagerReconcileMessage) { + logger.debug("reconcile: \(message)") + + Task { + await MainActor.run { + switch message { + case let .authTypeChanged(authType): + self.authType = authType + } + } + } + } + + public func dispatch(action: AuthManagerAction) { + logger.debug("dispatch: \(action)") + rust.dispatch(action: action) + } +} diff --git a/ios/Cove/Cove.swift b/ios/Cove/Cove.swift index 5f5aabd..f4263fa 100644 --- a/ios/Cove/Cove.swift +++ b/ios/Cove/Cove.swift @@ -3975,7 +3975,7 @@ public func FfiConverterTypeFeeRateOptionsWithTotalFee_lower(_ value: FeeRateOpt /** - * Representation of our app over FFI. Essentially a wrapper of [`App`]. + * Representation of our app over FFI. Essenially a wrapper of [`App`]. */ public protocol FfiAppProtocol : AnyObject { @@ -4048,7 +4048,7 @@ public protocol FfiAppProtocol : AnyObject { } /** - * Representation of our app over FFI. Essentially a wrapper of [`App`]. + * Representation of our app over FFI. Essenially a wrapper of [`App`]. */ open class FfiApp: FfiAppProtocol { @@ -5268,6 +5268,8 @@ public protocol GlobalFlagTableProtocol : AnyObject { func get(key: GlobalFlagKey) throws -> Bool + func getBoolConfig(key: GlobalFlagKey) -> Bool + func set(key: GlobalFlagKey, value: Bool) throws func toggleBoolConfig(key: GlobalFlagKey) throws @@ -5332,6 +5334,14 @@ open func get(key: GlobalFlagKey)throws -> Bool { }) } +open func getBoolConfig(key: GlobalFlagKey) -> Bool { + return try! FfiConverterBool.lift(try! rustCall() { + uniffi_cove_fn_method_globalflagtable_get_bool_config(self.uniffiClonePointer(), + FfiConverterTypeGlobalFlagKey.lower(key),$0 + ) +}) +} + open func set(key: GlobalFlagKey, value: Bool)throws {try rustCallWithError(FfiConverterTypeDatabaseError.lift) { uniffi_cove_fn_method_globalflagtable_set(self.uniffiClonePointer(), FfiConverterTypeGlobalFlagKey.lower(key), @@ -7277,6 +7287,181 @@ public func FfiConverterTypeRouteFactory_lower(_ value: RouteFactory) -> UnsafeM +public protocol RustAuthManagerProtocol : AnyObject { + + /** + * Get the auth type for the app + */ + func authType() -> AuthType + + /** + * Action from the frontend to change the state of the view model + */ + func dispatch(action: AuthManagerAction) + + func listenForUpdates(reconciler: AuthManagerReconciler) + + func send(message: AuthManagerReconcileMessage) + + func setAuthType(authType: AuthType) + +} + +open class RustAuthManager: + RustAuthManagerProtocol { + fileprivate let pointer: UnsafeMutableRawPointer! + + /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public struct NoPointer { + 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`. + required public init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + // This constructor can be used to instantiate a fake object. + // - Parameter noPointer: 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 [Pointer] the FFI lower functions will crash. +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public init(noPointer: NoPointer) { + self.pointer = nil + } + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public func uniffiClonePointer() -> UnsafeMutableRawPointer { + return try! rustCall { uniffi_cove_fn_clone_rustauthmanager(self.pointer, $0) } + } +public convenience init() { + let pointer = + try! rustCall() { + uniffi_cove_fn_constructor_rustauthmanager_new($0 + ) +} + self.init(unsafeFromRawPointer: pointer) +} + + deinit { + guard let pointer = pointer else { + return + } + + try! rustCall { uniffi_cove_fn_free_rustauthmanager(pointer, $0) } + } + + + + + /** + * Get the auth type for the app + */ +open func authType() -> AuthType { + return try! FfiConverterTypeAuthType.lift(try! rustCall() { + uniffi_cove_fn_method_rustauthmanager_auth_type(self.uniffiClonePointer(),$0 + ) +}) +} + + /** + * Action from the frontend to change the state of the view model + */ +open func dispatch(action: AuthManagerAction) {try! rustCall() { + uniffi_cove_fn_method_rustauthmanager_dispatch(self.uniffiClonePointer(), + FfiConverterTypeAuthManagerAction.lower(action),$0 + ) +} +} + +open func listenForUpdates(reconciler: AuthManagerReconciler) {try! rustCall() { + uniffi_cove_fn_method_rustauthmanager_listen_for_updates(self.uniffiClonePointer(), + FfiConverterCallbackInterfaceAuthManagerReconciler.lower(reconciler),$0 + ) +} +} + +open func send(message: AuthManagerReconcileMessage) {try! rustCall() { + uniffi_cove_fn_method_rustauthmanager_send(self.uniffiClonePointer(), + FfiConverterTypeAuthManagerReconcileMessage.lower(message),$0 + ) +} +} + +open func setAuthType(authType: AuthType) {try! rustCall() { + uniffi_cove_fn_method_rustauthmanager_set_auth_type(self.uniffiClonePointer(), + FfiConverterTypeAuthType.lower(authType),$0 + ) +} +} + + +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeRustAuthManager: FfiConverter { + + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = RustAuthManager + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> RustAuthManager { + return RustAuthManager(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: RustAuthManager) -> UnsafeMutableRawPointer { + return value.uniffiClonePointer() + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> RustAuthManager { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if (ptr == nil) { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: RustAuthManager, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } +} + + + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeRustAuthManager_lift(_ pointer: UnsafeMutableRawPointer) throws -> RustAuthManager { + return try FfiConverterTypeRustAuthManager.lift(pointer) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeRustAuthManager_lower(_ value: RustAuthManager) -> UnsafeMutableRawPointer { + return FfiConverterTypeRustAuthManager.lower(value) +} + + + + public protocol RustImportWalletManagerProtocol : AnyObject { /** @@ -10693,6 +10878,55 @@ public func FfiConverterTypeAppState_lower(_ value: AppState) -> RustBuffer { } +public struct AuthManagerState { + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init() { + } +} + + + +extension AuthManagerState: Equatable, Hashable { + public static func ==(lhs: AuthManagerState, rhs: AuthManagerState) -> Bool { + return true + } + + public func hash(into hasher: inout Hasher) { + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeAuthManagerState: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> AuthManagerState { + return + AuthManagerState() + } + + public static func write(_ value: AuthManagerState, into buf: inout [UInt8]) { + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeAuthManagerState_lift(_ buf: RustBuffer) throws -> AuthManagerState { + return try FfiConverterTypeAuthManagerState.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeAuthManagerState_lower(_ value: AuthManagerState) -> RustBuffer { + return FfiConverterTypeAuthManagerState.lower(value) +} + + public struct Balance { public var immature: Amount public var trustedPending: Amount @@ -12562,15 +12796,6 @@ public enum AppAction { ) case updateFiatPrices case updateFees - case updateAuthType(AuthType - ) - case enableAuth - case disableAuth - case enableBiometric - case disableBiometric - case setPin(String - ) - case disablePin } @@ -12600,22 +12825,6 @@ public struct FfiConverterTypeAppAction: FfiConverterRustBuffer { case 6: return .updateFees - case 7: return .updateAuthType(try FfiConverterTypeAuthType.read(from: &buf) - ) - - case 8: return .enableAuth - - case 9: return .disableAuth - - case 10: return .enableBiometric - - case 11: return .disableBiometric - - case 12: return .setPin(try FfiConverterString.read(from: &buf) - ) - - case 13: return .disablePin - default: throw UniffiInternalError.unexpectedEnumCase } } @@ -12651,36 +12860,6 @@ public struct FfiConverterTypeAppAction: FfiConverterRustBuffer { case .updateFees: writeInt(&buf, Int32(6)) - - case let .updateAuthType(v1): - writeInt(&buf, Int32(7)) - FfiConverterTypeAuthType.write(v1, into: &buf) - - - case .enableAuth: - writeInt(&buf, Int32(8)) - - - case .disableAuth: - writeInt(&buf, Int32(9)) - - - case .enableBiometric: - writeInt(&buf, Int32(10)) - - - case .disableBiometric: - writeInt(&buf, Int32(11)) - - - case let .setPin(v1): - writeInt(&buf, Int32(12)) - FfiConverterString.write(v1, into: &buf) - - - case .disablePin: - writeInt(&buf, Int32(13)) - } } } @@ -13013,6 +13192,157 @@ extension AuthError: Foundation.LocalizedError { } } +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum AuthManagerAction { + + case updateAuthType(AuthType + ) + case enableBiometric + case disableBiometric + case setPin(String + ) + case disablePin +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeAuthManagerAction: FfiConverterRustBuffer { + typealias SwiftType = AuthManagerAction + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> AuthManagerAction { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .updateAuthType(try FfiConverterTypeAuthType.read(from: &buf) + ) + + case 2: return .enableBiometric + + case 3: return .disableBiometric + + case 4: return .setPin(try FfiConverterString.read(from: &buf) + ) + + case 5: return .disablePin + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: AuthManagerAction, into buf: inout [UInt8]) { + switch value { + + + case let .updateAuthType(v1): + writeInt(&buf, Int32(1)) + FfiConverterTypeAuthType.write(v1, into: &buf) + + + case .enableBiometric: + writeInt(&buf, Int32(2)) + + + case .disableBiometric: + writeInt(&buf, Int32(3)) + + + case let .setPin(v1): + writeInt(&buf, Int32(4)) + FfiConverterString.write(v1, into: &buf) + + + case .disablePin: + writeInt(&buf, Int32(5)) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeAuthManagerAction_lift(_ buf: RustBuffer) throws -> AuthManagerAction { + return try FfiConverterTypeAuthManagerAction.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeAuthManagerAction_lower(_ value: AuthManagerAction) -> RustBuffer { + return FfiConverterTypeAuthManagerAction.lower(value) +} + + + +extension AuthManagerAction: Equatable, Hashable {} + + + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum AuthManagerReconcileMessage { + + case authTypeChanged(AuthType + ) +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeAuthManagerReconcileMessage: FfiConverterRustBuffer { + typealias SwiftType = AuthManagerReconcileMessage + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> AuthManagerReconcileMessage { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .authTypeChanged(try FfiConverterTypeAuthType.read(from: &buf) + ) + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: AuthManagerReconcileMessage, into buf: inout [UInt8]) { + switch value { + + + case let .authTypeChanged(v1): + writeInt(&buf, Int32(1)) + FfiConverterTypeAuthType.write(v1, into: &buf) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeAuthManagerReconcileMessage_lift(_ buf: RustBuffer) throws -> AuthManagerReconcileMessage { + return try FfiConverterTypeAuthManagerReconcileMessage.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeAuthManagerReconcileMessage_lower(_ value: AuthManagerReconcileMessage) -> RustBuffer { + return FfiConverterTypeAuthManagerReconcileMessage.lower(value) +} + + + +extension AuthManagerReconcileMessage: Equatable, Hashable {} + + + // Note that we don't yet support `indirect` for enums. // See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. @@ -14725,6 +15055,7 @@ extension GlobalConfigTableError: Foundation.LocalizedError { public enum GlobalFlagKey { case completedOnboarding + case wipeMePinEnabled } @@ -14740,6 +15071,8 @@ public struct FfiConverterTypeGlobalFlagKey: FfiConverterRustBuffer { case 1: return .completedOnboarding + case 2: return .wipeMePinEnabled + default: throw UniffiInternalError.unexpectedEnumCase } } @@ -14751,6 +15084,10 @@ public struct FfiConverterTypeGlobalFlagKey: FfiConverterRustBuffer { case .completedOnboarding: writeInt(&buf, Int32(1)) + + case .wipeMePinEnabled: + writeInt(&buf, Int32(2)) + } } } @@ -19574,6 +19911,111 @@ extension XpubError: Foundation.LocalizedError { +public protocol AuthManagerReconciler : AnyObject { + + /** + * Tells the frontend to reconcile the manager changes + */ + func reconcile(message: AuthManagerReconcileMessage) + +} + + + +// Put the implementation in a struct so we don't pollute the top-level namespace +fileprivate struct UniffiCallbackInterfaceAuthManagerReconciler { + + // Create the VTable using a series of closures. + // Swift automatically converts these into C callback functions. + // + // This creates 1-element array, since this seems to be the only way to construct a const + // pointer that we can pass to the Rust code. + static let vtable: [UniffiVTableCallbackInterfaceAuthManagerReconciler] = [UniffiVTableCallbackInterfaceAuthManagerReconciler( + reconcile: { ( + uniffiHandle: UInt64, + message: RustBuffer, + uniffiOutReturn: UnsafeMutableRawPointer, + uniffiCallStatus: UnsafeMutablePointer + ) in + let makeCall = { + () throws -> () in + guard let uniffiObj = try? FfiConverterCallbackInterfaceAuthManagerReconciler.handleMap.get(handle: uniffiHandle) else { + throw UniffiInternalError.unexpectedStaleHandle + } + return uniffiObj.reconcile( + message: try FfiConverterTypeAuthManagerReconcileMessage.lift(message) + ) + } + + + let writeReturn = { () } + uniffiTraitInterfaceCall( + callStatus: uniffiCallStatus, + makeCall: makeCall, + writeReturn: writeReturn + ) + }, + uniffiFree: { (uniffiHandle: UInt64) -> () in + let result = try? FfiConverterCallbackInterfaceAuthManagerReconciler.handleMap.remove(handle: uniffiHandle) + if result == nil { + print("Uniffi callback interface AuthManagerReconciler: handle missing in uniffiFree") + } + } + )] +} + +private func uniffiCallbackInitAuthManagerReconciler() { + uniffi_cove_fn_init_callback_vtable_authmanagerreconciler(UniffiCallbackInterfaceAuthManagerReconciler.vtable) +} + +// FfiConverter protocol for callback interfaces +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterCallbackInterfaceAuthManagerReconciler { + fileprivate static let handleMap = UniffiHandleMap() +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +extension FfiConverterCallbackInterfaceAuthManagerReconciler : FfiConverter { + typealias SwiftType = AuthManagerReconciler + typealias FfiType = UInt64 + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public static func lift(_ handle: UInt64) throws -> SwiftType { + try handleMap.get(handle: handle) + } + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + let handle: UInt64 = try readInt(&buf) + return try lift(handle) + } + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public static func lower(_ v: SwiftType) -> UInt64 { + return handleMap.insert(obj: v) + } + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public static func write(_ v: SwiftType, into buf: inout [UInt8]) { + writeInt(&buf, lower(v)) + } +} + + + + public protocol DeviceAccess : AnyObject { func timezone() -> String @@ -21926,6 +22368,9 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_globalflagtable_get() != 42810) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_method_globalflagtable_get_bool_config() != 34785) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_method_globalflagtable_set() != 23016) { return InitializationResult.apiChecksumMismatch } @@ -22052,6 +22497,21 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_routefactory_send_set_amount() != 33578) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_method_rustauthmanager_auth_type() != 13301) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_cove_checksum_method_rustauthmanager_dispatch() != 58198) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_cove_checksum_method_rustauthmanager_listen_for_updates() != 6029) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_cove_checksum_method_rustauthmanager_send() != 55296) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_cove_checksum_method_rustauthmanager_set_auth_type() != 20435) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_method_rustimportwalletmanager_dispatch() != 61781) { return InitializationResult.apiChecksumMismatch } @@ -22451,6 +22911,9 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_constructor_routefactory_new() != 4959) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_constructor_rustauthmanager_new() != 30134) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_constructor_rustimportwalletmanager_new() != 63844) { return InitializationResult.apiChecksumMismatch } @@ -22502,6 +22965,9 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_constructor_wordvalidator_preview() != 53831) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_method_authmanagerreconciler_reconcile() != 44010) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_method_deviceaccess_timezone() != 16696) { return InitializationResult.apiChecksumMismatch } @@ -22528,6 +22994,7 @@ private let initializationResult: InitializationResult = { } uniffiCallbackInitAutoComplete() + uniffiCallbackInitAuthManagerReconciler() uniffiCallbackInitDeviceAccess() uniffiCallbackInitFfiReconcile() uniffiCallbackInitImportWalletManagerReconciler() diff --git a/ios/Cove/CoveApp.swift b/ios/Cove/CoveApp.swift index 47258c0..5c08c33 100644 --- a/ios/Cove/CoveApp.swift +++ b/ios/Cove/CoveApp.swift @@ -35,8 +35,8 @@ struct SafeAreaInsetsKey: EnvironmentKey { } } -public extension EnvironmentValues { - var safeAreaInsets: EdgeInsets { +extension EnvironmentValues { + public var safeAreaInsets: EdgeInsets { self[SafeAreaInsetsKey.self] } } @@ -48,6 +48,8 @@ struct CoveApp: App { @AppStorage("lockedAt") var lockedAt: Date = .init() @State var app: AppManager + @State var auth: AuthManager + @State var id = UUID() @State var showCover: Bool = true @@ -77,7 +79,7 @@ struct CoveApp: App { ): "The address \(address) is on the wrong network. You are on \(currentNetwork), and the address was for \(network)." case let .noWalletSelected(address), - let .foundAddress(address, _): + let .foundAddress(address, _): String(address) case .noCameraPermission: "Please allow camera access in Settings to use this feature." @@ -99,11 +101,11 @@ struct CoveApp: App { try? app.rust.selectWallet(id: walletId) } case .invalidWordGroup, - .errorImportingHotWallet, - .importedSuccessfully, - .unableToSelectWallet, - .errorImportingHardwareWallet, - .invalidFileFormat: + .errorImportingHotWallet, + .importedSuccessfully, + .unableToSelectWallet, + .errorImportingHardwareWallet, + .invalidFileFormat: Button("OK") { app.alertState = .none } @@ -160,7 +162,10 @@ struct CoveApp: App { _ = Device(device: DeviceAccesor()) let app = AppManager() + let auth = AuthManager() + self.app = app + self.auth = auth } private var showingAlert: Binding { @@ -390,6 +395,7 @@ struct CoveApp: App { } } .environment(app) + .environment(auth) } var routeToTint: Color { @@ -431,11 +437,11 @@ struct CoveApp: App { func handleScenePhaseChange(_ oldPhase: ScenePhase, _ newPhase: ScenePhase) { Log.debug( - "[SCENE PHASE]: \(oldPhase) --> \(newPhase) && using biometrics: \(app.isUsingBiometrics)" + "[SCENE PHASE]: \(oldPhase) --> \(newPhase) && using biometrics: \(auth.isUsingBiometrics)" ) - if app.isAuthEnabled, !app.isUsingBiometrics, oldPhase == .active, - newPhase == .inactive + if auth.isAuthEnabled, !auth.isUsingBiometrics, oldPhase == .active, + newPhase == .inactive { coverClearTask?.cancel() showCover = true @@ -502,20 +508,20 @@ struct CoveApp: App { .gesture( app.router.routes.isEmpty ? DragGesture() - .onChanged { gesture in - if gesture.startLocation.x < 25, gesture.translation.width > 100 { - withAnimation(.spring()) { - app.isSidebarVisible = true + .onChanged { gesture in + if gesture.startLocation.x < 25, gesture.translation.width > 100 { + withAnimation(.spring()) { + app.isSidebarVisible = true + } } } - } - .onEnded { gesture in - if gesture.startLocation.x < 20, gesture.translation.width > 50 { - withAnimation(.spring()) { - app.isSidebarVisible = true + .onEnded { gesture in + if gesture.startLocation.x < 20, gesture.translation.width > 50 { + withAnimation(.spring()) { + app.isSidebarVisible = true + } } - } - } : nil + } : nil ) .task { await app.rust.initOnStart() @@ -524,7 +530,11 @@ struct CoveApp: App { .onOpenURL(perform: handleFileOpen) .onChange(of: phase, initial: true, handleScenePhaseChange) .onAppear { - if app.isAuthEnabled { app.lockState = .locked } else { app.lockState = .unlocked } + if app.isAuthEnabled { + app.lockState = .locked + } else { + app.lockState = .unlocked + } } } } diff --git a/ios/Cove/HomeScreens/SettingsScreen.swift b/ios/Cove/HomeScreens/SettingsScreen.swift index af03052..d2a51c0 100644 --- a/ios/Cove/HomeScreens/SettingsScreen.swift +++ b/ios/Cove/HomeScreens/SettingsScreen.swift @@ -2,18 +2,24 @@ import LocalAuthentication import SwiftUI private enum SheetState: Equatable { - case newPin, removePin, changePin, disableAuth, disableBiometric, enableAuth, enableBiometric + case newPin, removePin, changePin, disableBiometric, enableAuth, enableBiometric +} + +private enum AlertState: Equatable { + case networkChanged(Network) + case confirmEnableWipeMePin } struct SettingsScreen: View { @Environment(AppManager.self) private var app + @Environment(AuthManager.self) private var auth @Environment(\.dismiss) private var dismiss @State private var notificationFrequency = 1 @State private var networkChanged = false - @State private var showConfirmationAlert = false @State private var sheetState: TaggedItem? = nil + @State private var alertState: TaggedItem? = nil let themes = allColorSchemes() @@ -47,9 +53,9 @@ struct SettingsScreen: View { var toggleWipeMePin: Binding { Binding( - get: { app.authType == AuthType.both || app.authType == AuthType.pin }, + get: { false }, set: { enable in - if enable { sheetState = .init(.newPin) } else { sheetState = .init(.removePin) } + if enable { alertState = .init(.confirmEnableWipeMePin) } } ) } @@ -118,7 +124,7 @@ struct SettingsScreen: View { Label("Change PIN", systemImage: "lock.open.rotation") } - Toggle(isOn: Binding.constant(false)) { + Toggle(isOn: toggleWipeMePin) { Label("Enable Wipe Data PIN", systemImage: "trash.slash") } } @@ -131,7 +137,7 @@ struct SettingsScreen: View { ? ToolbarItem(placement: .navigationBarLeading) { Button(action: { if networkChanged { - showConfirmationAlert = true + alertState = .init(.networkChanged(app.selectedNetwork)) } else { dismiss() } @@ -147,18 +153,14 @@ struct SettingsScreen: View { } } : nil } - .alert(isPresented: $showConfirmationAlert) { - Alert( - title: Text("⚠️ Network Changed ⚠️"), - message: Text("You've changed your network to \(app.selectedNetwork)"), - primaryButton: .destructive(Text("Yes, Change Network")) { - app.resetRoute(to: .listWallets) - dismiss() - }, - secondaryButton: .cancel(Text("Cancel")) - ) - } .fullScreenCover(item: $sheetState, content: SheetContent) + .alert( + alertTitle, + isPresented: showingAlert, + presenting: alertState, + actions: { MyAlert($0).actions }, + message: { MyAlert($0).message } + ) .preferredColorScheme(app.colorScheme) .gesture( networkChanged @@ -166,14 +168,14 @@ struct SettingsScreen: View { .onChanged { gesture in if gesture.startLocation.x < 25, gesture.translation.width > 100 { withAnimation(.spring()) { - showConfirmationAlert = true + alertState = .init(.networkChanged(app.selectedNetwork)) } } } .onEnded { gesture in if gesture.startLocation.x < 20, gesture.translation.width > 50 { withAnimation(.spring()) { - showConfirmationAlert = true + alertState = .init(.networkChanged(app.selectedNetwork)) } } } : nil @@ -181,14 +183,10 @@ struct SettingsScreen: View { } func setPin(_ pin: String) { - app.dispatch(action: .setPin(pin)) + auth.dispatch(action: .setPin(pin)) sheetState = .none } - func checkPin(_ pin: String) -> Bool { - AuthPin().check(pin: pin) - } - @ViewBuilder private func CancelView(_ content: () -> some View) -> some View { VStack { @@ -208,6 +206,62 @@ struct SettingsScreen: View { .background(.midnightBlue) } + // MARK: Alerts + + private var showingAlert: Binding { + Binding( + get: { alertState != nil }, + set: { if !$0 { alertState = .none } } + ) + } + + private var alertTitle: String { + guard let alertState else { return "Error" } + return MyAlert(alertState).title + } + + private func MyAlert(_ alert: TaggedItem) -> some AlertBuilderProtocol { + switch alert.item { + case let .networkChanged(network): + return AlertBuilder( + title: "⚠️ Network Changed ⚠️", + message: "You've changed your network to \(network)", + actions: { + Button("Yes, Change Network") { + app.resetRoute(to: .listWallets) + dismiss() + } + Button("Cancel", role: .cancel) {} + } + ) + + case .confirmEnableWipeMePin: + return AlertBuilder( + title: "Are you sure?", + message: + """ + + Enabling the Wipe Data PIN will let you chose a PIN that if entered will wipe all Cove wallet data on this device. + + If you wipe the data without having a back up of your wallet, you will lose the bitcoin in that wallet. + + Please make sure you have a backup of your wallet before enabling this. + """, + actions: { + Button("Yes, Enable Wipe Data PIN") { + // app.dispatch(action: .enableWipeMePin) + dismiss() + } + Button("Cancel", role: .cancel) { + alertState = .none + } + } + ) + } + } + + // MARK: Sheets + @ViewBuilder private func SheetContent(_ state: TaggedItem) -> some View { switch state.item { @@ -217,8 +271,8 @@ struct SettingsScreen: View { lockType: .biometric, isPinCorrect: { _ in true }, onUnlock: { pin in - app.dispatch(action: .enableBiometric) - if !pin.isEmpty { app.dispatch(action: .setPin(pin)) } + auth.dispatch(action: .enableBiometric) + if !pin.isEmpty { auth.dispatch(action: .setPin(pin)) } sheetState = .none }, @@ -235,39 +289,27 @@ struct SettingsScreen: View { case .removePin: NumberPadPinView( title: "Enter Current PIN", - isPinCorrect: checkPin, + isPinCorrect: auth.checkPin, backAction: { sheetState = .none }, onUnlock: { _ in - app.dispatch(action: .disablePin) + auth.dispatch(action: .disablePin) sheetState = .none } ) case .changePin: ChangePinView( - isPinCorrect: checkPin, + isPinCorrect: auth.checkPin, backAction: { sheetState = .none }, onComplete: setPin ) - case .disableAuth: - LockView( - lockType: app.authType == .biometric ? .biometric : .pin, - isPinCorrect: checkPin, - onUnlock: { _ in - app.dispatch(action: .disableAuth) - sheetState = .none - }, - backAction: { sheetState = .none }, - content: { EmptyView() } - ) - case .disableBiometric: LockView( - lockType: app.authType, - isPinCorrect: checkPin, + lockType: auth.authType, + isPinCorrect: app.checkPin, onUnlock: { _ in - app.dispatch(action: .disableBiometric) + auth.dispatch(action: .disableBiometric) sheetState = .none }, backAction: { sheetState = .none }, @@ -279,7 +321,7 @@ struct SettingsScreen: View { lockType: .biometric, isPinCorrect: { _ in true }, onUnlock: { _ in - app.dispatch(action: .enableBiometric) + auth.dispatch(action: .enableBiometric) sheetState = .none }, backAction: { sheetState = .none }, diff --git a/ios/Cove/Views/LockView.swift b/ios/Cove/Views/LockView.swift index c10899c..9692885 100644 --- a/ios/Cove/Views/LockView.swift +++ b/ios/Cove/Views/LockView.swift @@ -18,6 +18,7 @@ enum LockState: Equatable { struct LockView: View { @Environment(AppManager.self) var app + @Environment(AuthManager.self) var auth /// Args: Lock Properties var lockType: AuthType @@ -73,9 +74,10 @@ struct LockView: View { self.content = content() // back - let backEnabled = if backAction != nil { true } else { - lockType == .both && isBiometricAvailable - } + let backEnabled = + if backAction != nil { true } else { + lockType == .both && isBiometricAvailable + } self.backEnabled = backEnabled _backAction = backAction @@ -100,10 +102,11 @@ struct LockView: View { } private var lockState: Binding { - lockStateBinding ?? Binding( - get: { innerLockState }, - set: { innerLockState = $0 } - ) + lockStateBinding + ?? Binding( + get: { innerLockState }, + set: { innerLockState = $0 } + ) } var body: some View { @@ -173,10 +176,12 @@ struct LockView: View { @ViewBuilder var PermissionsNeeded: some View { VStack(spacing: 20) { - Text("Cove needs permissions to FaceID to unlock your wallet. Please open settings and enable FaceID.") - .font(.callout) - .multilineTextAlignment(.center) - .padding(.horizontal, 50) + Text( + "Cove needs permissions to FaceID to unlock your wallet. Please open settings and enable FaceID." + ) + .font(.callout) + .multilineTextAlignment(.center) + .padding(.horizontal, 50) Button("Open Settings") { let url = URL(string: UIApplication.openSettingsURLString)! @@ -230,26 +235,26 @@ struct LockView: View { private func tryUnlockingView() { guard lockType == .biometric || lockType == .both else { return } - guard !app.isUsingBiometrics else { return } + guard !auth.isUsingBiometrics else { return } guard isBiometricAvailable else { return } guard lockState.wrappedValue == .locked else { return } /// Checking and Unlocking View Task { /// Requesting Biometric Unlock - app.isUsingBiometrics = true + auth.isUsingBiometrics = true if await (try? bioMetricUnlock()) ?? false { await MainActor.run { withAnimation(.snappy, completionCriteria: .logicallyComplete) { lockState.wrappedValue = .unlocked } completion: { - app.isUsingBiometrics = false + auth.isUsingBiometrics = false onUnlock("") } } } else { - await MainActor.run { app.isUsingBiometrics = false } + await MainActor.run { auth.isUsingBiometrics = false } } } } diff --git a/rust/src/app.rs b/rust/src/app.rs index 963b866..a20e125 100644 --- a/rust/src/app.rs +++ b/rust/src/app.rs @@ -5,7 +5,7 @@ pub mod reconcile; use std::{sync::Arc, time::Duration}; use crate::{ - auth::{AuthPin, AuthType}, + auth::AuthType, color_scheme::ColorSchemeSelection, database::{error::DatabaseError, Database}, fiat::client::{PriceResponse, FIAT_CLIENT}, @@ -53,11 +53,6 @@ pub enum AppAction { SetSelectedNode(Node), UpdateFiatPrices, UpdateFees, - UpdateAuthType(AuthType), - EnableBiometric, - DisableBiometric, - SetPin(String), - DisablePin, } #[derive( @@ -143,7 +138,7 @@ impl App { } AppAction::ChangeColorScheme(color_scheme) => { - debug!("color scheme change, NEW: {:?}", color_scheme); + debug!("color scheme change, new: {:?}", color_scheme); Database::global() .global_config @@ -152,7 +147,7 @@ impl App { } AppAction::SetSelectedNode(node) => { - debug!("selected node change, NEW: {:?}", node); + debug!("selected node change, new: {:?}", node); match Database::global().global_config.set_selected_node(&node) { Ok(_) => {} @@ -189,63 +184,6 @@ impl App { } }); } - - AppAction::UpdateAuthType(auth_type) => { - debug!("authType changed, new: {auth_type:?}"); - set_auth_type(auth_type); - } - - AppAction::EnableBiometric => { - debug!("enable biometric"); - - let current_auth_type = FfiApp::global().auth_type(); - match current_auth_type { - AuthType::None => set_auth_type(AuthType::Biometric), - AuthType::Pin => set_auth_type(AuthType::Both), - _ => {} - }; - } - - AppAction::DisableBiometric => { - debug!("disable biometric"); - - let current_auth_type = FfiApp::global().auth_type(); - match current_auth_type { - AuthType::Biometric => set_auth_type(AuthType::None), - AuthType::Both => set_auth_type(AuthType::Pin), - _ => {} - }; - } - - AppAction::SetPin(pin) => { - debug!("set pin"); - - if let Err(err) = AuthPin::new().set(pin) { - return error!("unable to set pin: {err:?}"); - } - - let current_auth_type = FfiApp::global().auth_type(); - match current_auth_type { - AuthType::None => set_auth_type(AuthType::Pin), - AuthType::Biometric => set_auth_type(AuthType::Both), - _ => {} - } - } - - AppAction::DisablePin => { - debug!("disable pin"); - - if let Err(err) = AuthPin::new().delete() { - return error!("unable to delete pin: {err:?}"); - } - - let current_auth_type = FfiApp::global().auth_type(); - match current_auth_type { - AuthType::Pin => set_auth_type(AuthType::None), - AuthType::Both => set_auth_type(AuthType::Biometric), - _ => {} - } - } } } @@ -264,18 +202,7 @@ impl App { } } -fn set_auth_type(auth_type: AuthType) { - match Database::global().global_config.set_auth_type(auth_type) { - Ok(_) => { - Updater::send_update(AppMessage::AuthTypeChanged(auth_type)); - } - Err(error) => { - error!("unable to set auth type: {error:?}"); - } - } -} - -/// Representation of our app over FFI. Essentially a wrapper of [`App`]. +/// Representation of our app over FFI. Essenially a wrapper of [`App`]. #[derive(Debug, Clone, Hash, Eq, PartialEq, uniffi::Object)] pub struct FfiApp; diff --git a/rust/src/database/global_flag.rs b/rust/src/database/global_flag.rs index d4c3861..5134738 100644 --- a/rust/src/database/global_flag.rs +++ b/rust/src/database/global_flag.rs @@ -11,6 +11,7 @@ pub const TABLE: TableDefinition<&'static str, bool> = TableDefinition::new("glo #[derive(Debug, Clone, Copy, strum::IntoStaticStr, uniffi::Enum)] pub enum GlobalFlagKey { CompletedOnboarding, + WipeMePinEnabled, } #[derive(Debug, Clone, uniffi::Object)] @@ -84,6 +85,10 @@ impl GlobalFlagTable { Ok(()) } + pub fn get_bool_config(&self, key: GlobalFlagKey) -> bool { + self.get(key).unwrap_or(false) + } + pub fn toggle_bool_config(&self, key: GlobalFlagKey) -> Result<(), Error> { let value = self.get(key)?; diff --git a/rust/src/manager.rs b/rust/src/manager.rs index 0ff9c0c..6029a39 100644 --- a/rust/src/manager.rs +++ b/rust/src/manager.rs @@ -1,3 +1,4 @@ +pub mod auth; pub mod import_wallet; pub mod pending_wallet; pub mod wallet; diff --git a/rust/src/manager/auth.rs b/rust/src/manager/auth.rs new file mode 100644 index 0000000..1ce2f11 --- /dev/null +++ b/rust/src/manager/auth.rs @@ -0,0 +1,169 @@ +use std::sync::Arc; + +use crossbeam::channel::{Receiver, Sender}; +use macros::impl_default_for; +use parking_lot::RwLock; +use tap::TapFallible as _; +use tracing::{debug, error}; + +use crate::{ + auth::{AuthPin, AuthType}, + database::Database, +}; + +type Message = AuthManagerReconcileMessage; + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, uniffi::Enum)] +pub enum AuthManagerReconcileMessage { + AuthTypeChanged(AuthType), +} + +#[uniffi::export(callback_interface)] +pub trait AuthManagerReconciler: Send + Sync + std::fmt::Debug + 'static { + /// Tells the frontend to reconcile the manager changes + fn reconcile(&self, message: AuthManagerReconcileMessage); +} + +impl_default_for!(RustAuthManager); + +#[derive(Clone, Debug, uniffi::Object)] +pub struct RustAuthManager { + #[allow(dead_code)] + pub state: Arc>, + pub reconciler: Sender, + pub reconcile_receiver: Arc>, +} + +#[derive(Clone, Debug, uniffi::Record)] +pub struct AuthManagerState {} + +type Action = AuthManagerAction; + +#[derive(Debug, Clone, Hash, Eq, PartialEq, uniffi::Enum)] +pub enum AuthManagerAction { + UpdateAuthType(AuthType), + EnableBiometric, + DisableBiometric, + SetPin(String), + DisablePin, +} + +#[uniffi::export] +impl RustAuthManager { + #[uniffi::constructor] + pub fn new() -> Self { + let (sender, receiver) = crossbeam::channel::bounded(1000); + + Self { + state: Arc::new(RwLock::new(AuthManagerState::new())), + reconciler: sender, + reconcile_receiver: Arc::new(receiver), + } + } + + #[uniffi::method] + pub fn listen_for_updates(&self, reconciler: Box) { + let reconcile_receiver = self.reconcile_receiver.clone(); + + std::thread::spawn(move || { + while let Ok(field) = reconcile_receiver.recv() { + // call the reconcile method on the frontend + reconciler.reconcile(field); + } + }); + } + + /// Get the auth type for the app + pub fn auth_type(&self) -> AuthType { + Database::global() + .global_config + .auth_type() + .tap_err(|error| { + error!("unable to get auth type: {error:?}"); + }) + .unwrap_or_default() + } + + fn send(&self, message: Message) { + if let Err(error) = self.reconciler.send(message) { + error!("unable to send message: {error:?}"); + } + } + + fn set_auth_type(&self, auth_type: AuthType) { + match Database::global().global_config.set_auth_type(auth_type) { + Ok(_) => { + self.send(Message::AuthTypeChanged(auth_type)); + } + Err(error) => { + error!("unable to set auth type: {error:?}"); + } + } + } + + /// Action from the frontend to change the state of the view model + #[uniffi::method] + pub fn dispatch(&self, action: AuthManagerAction) { + match action { + Action::UpdateAuthType(auth_type) => { + debug!("authType changed, new: {auth_type:?}"); + self.set_auth_type(auth_type); + } + + Action::EnableBiometric => { + debug!("enable biometric"); + + match self.auth_type() { + AuthType::None => self.set_auth_type(AuthType::Biometric), + AuthType::Pin => self.set_auth_type(AuthType::Both), + _ => {} + }; + } + + Action::DisableBiometric => { + debug!("disable biometric"); + + match self.auth_type() { + AuthType::Biometric => self.set_auth_type(AuthType::None), + AuthType::Both => self.set_auth_type(AuthType::Pin), + _ => {} + }; + } + + Action::SetPin(pin) => { + debug!("set pin"); + + if let Err(err) = AuthPin::new().set(pin) { + return error!("unable to set pin: {err:?}"); + } + + match self.auth_type() { + AuthType::None => self.set_auth_type(AuthType::Pin), + AuthType::Biometric => self.set_auth_type(AuthType::Both), + _ => {} + } + } + + Action::DisablePin => { + debug!("disable pin"); + + if let Err(err) = AuthPin::new().delete() { + return error!("unable to delete pin: {err:?}"); + } + + match self.auth_type() { + AuthType::Pin => self.set_auth_type(AuthType::None), + AuthType::Both => self.set_auth_type(AuthType::Biometric), + _ => {} + } + } + } + } +} + +impl_default_for!(AuthManagerState); +impl AuthManagerState { + pub fn new() -> Self { + Self {} + } +} From c2d60493a5fa11272ee722040819c8916728371e Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Mon, 16 Dec 2024 16:04:18 -0600 Subject: [PATCH 04/10] Add ability to check, set and delete wipe data pin --- ios/Cove/AuthManager.swift | 6 +++++- rust/src/app/reconcile.rs | 1 + rust/src/database/global_config.rs | 10 ++++----- rust/src/database/global_flag.rs | 1 - rust/src/database/macros.rs | 23 ++++++++++---------- rust/src/manager/auth.rs | 34 ++++++++++++++++++++++++++---- 6 files changed, 51 insertions(+), 24 deletions(-) diff --git a/ios/Cove/AuthManager.swift b/ios/Cove/AuthManager.swift index 9e06ea4..0f4127c 100644 --- a/ios/Cove/AuthManager.swift +++ b/ios/Cove/AuthManager.swift @@ -21,7 +21,11 @@ import SwiftUI AuthPin().check(pin: pin) } - func reconcile(message: AuthManagerReconcileMessage) { + public func checkWipeMePin(_ pin: String) -> Bool { + AuthPin().check(pin: pin) + } + + func reconcile(message: Aut hManagerReconcileMessage) { logger.debug("reconcile: \(message)") Task { diff --git a/rust/src/app/reconcile.rs b/rust/src/app/reconcile.rs index b50eb42..b48b5ff 100644 --- a/rust/src/app/reconcile.rs +++ b/rust/src/app/reconcile.rs @@ -20,6 +20,7 @@ pub enum AppStateReconcileMessage { FeesChanged(FeeResponse), AuthTypeChanged(AuthType), HashedPinCodeChanged(String), + WipeDataPinChanged(String), } // alias for easier imports on the rust side diff --git a/rust/src/database/global_config.rs b/rust/src/database/global_config.rs index 8673217..030a593 100644 --- a/rust/src/database/global_config.rs +++ b/rust/src/database/global_config.rs @@ -26,6 +26,7 @@ pub enum GlobalConfigKey { ColorScheme, AuthType, HashedPinCode, + WipeDataPin, } impl From for &'static str { @@ -38,6 +39,7 @@ impl From for &'static str { GlobalConfigKey::ColorScheme => "color_scheme", GlobalConfigKey::AuthType => "auth_type", GlobalConfigKey::HashedPinCode => "hashed_pin_code", + GlobalConfigKey::WipeDataPin => "wipe_data_pin", } } } @@ -83,12 +85,8 @@ impl GlobalConfigTable { Update::ColorSchemeChanged ); - string_config_accessor!( - priv_hashed_pin_code, - GlobalConfigKey::HashedPinCode, - String, - Update::HashedPinCodeChanged - ); + string_config_accessor!(pub wipe_data_pin, GlobalConfigKey::WipeDataPin, String); + string_config_accessor!(priv_hashed_pin_code, GlobalConfigKey::HashedPinCode, String); } #[uniffi::export] diff --git a/rust/src/database/global_flag.rs b/rust/src/database/global_flag.rs index 5134738..399d81b 100644 --- a/rust/src/database/global_flag.rs +++ b/rust/src/database/global_flag.rs @@ -11,7 +11,6 @@ pub const TABLE: TableDefinition<&'static str, bool> = TableDefinition::new("glo #[derive(Debug, Clone, Copy, strum::IntoStaticStr, uniffi::Enum)] pub enum GlobalFlagKey { CompletedOnboarding, - WipeMePinEnabled, } #[derive(Debug, Clone, uniffi::Object)] diff --git a/rust/src/database/macros.rs b/rust/src/database/macros.rs index 70b8991..de434f5 100644 --- a/rust/src/database/macros.rs +++ b/rust/src/database/macros.rs @@ -1,7 +1,6 @@ #[macro_export] macro_rules! string_config_accessor { - // Define internal macro for the implementation - (@impl $vis:vis, $fn_name:ident, $key:expr, $return_type:ty, $update_variant:path) => { + (@impl $vis:vis, $fn_name:ident, $key:expr, $return_type:ty, $($update_variant:expr)?) => { $vis fn $fn_name(&self) -> Result<$return_type, Error> { use std::str::FromStr as _; @@ -21,10 +20,12 @@ macro_rules! string_config_accessor { paste::paste! { $vis fn [](&self, value: $return_type) -> Result<(), Error> { - let value_to_send = value.clone(); - let value = value.to_string(); - self.set($key, value)?; - Updater::send_update($update_variant(value_to_send)); + let value_str = value.to_string(); + self.set($key, value_str)?; + + $( + Updater::send_update($update_variant(value)); + )? Ok(()) } @@ -39,13 +40,11 @@ macro_rules! string_config_accessor { } }; - // Public interface - (pub $fn_name:ident, $key:expr, $return_type:ty, $update_variant:path) => { - string_config_accessor!(@impl pub, $fn_name, $key, $return_type, $update_variant); + (pub $fn_name:ident, $key:expr, $return_type:ty $(, $update_variant:expr)?) => { + string_config_accessor!(@impl pub, $fn_name, $key, $return_type, $($update_variant)?); }; - // Private interface - ($fn_name:ident, $key:expr, $return_type:ty, $update_variant:path) => { - string_config_accessor!(@impl, $fn_name, $key, $return_type, $update_variant); + ($fn_name:ident, $key:expr, $return_type:ty $(, $update_variant:expr)?) => { + string_config_accessor!(@impl, $fn_name, $key, $return_type, $($update_variant)?); }; } diff --git a/rust/src/manager/auth.rs b/rust/src/manager/auth.rs index 1ce2f11..993d59f 100644 --- a/rust/src/manager/auth.rs +++ b/rust/src/manager/auth.rs @@ -13,7 +13,7 @@ use crate::{ type Message = AuthManagerReconcileMessage; -#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, uniffi::Enum)] +#[derive(Debug, Clone, Hash, Eq, PartialEq, uniffi::Enum)] pub enum AuthManagerReconcileMessage { AuthTypeChanged(AuthType), } @@ -46,6 +46,7 @@ pub enum AuthManagerAction { DisableBiometric, SetPin(String), DisablePin, + SetWipeDataPin(String), } #[uniffi::export] @@ -84,12 +85,24 @@ impl RustAuthManager { .unwrap_or_default() } - fn send(&self, message: Message) { - if let Err(error) = self.reconciler.send(message) { - error!("unable to send message: {error:?}"); + /// Check if the PIN matches the wipe data pin + pub fn check_wipe_data_pin(&self, pin: String) -> bool { + let wipe_data_pin = Database::global() + .global_config + .wipe_data_pin() + .unwrap_or_default(); + + pin == wipe_data_pin + } + + /// Delete the wipe data pin + pub fn delete_wipe_data_pin(&self) { + if let Err(error) = Database::global().global_config.delete_wipe_data_pin() { + error!("unable to delete wipe data pin: {error:?}"); } } + // private fn set_auth_type(&self, auth_type: AuthType) { match Database::global().global_config.set_auth_type(auth_type) { Ok(_) => { @@ -101,6 +114,12 @@ impl RustAuthManager { } } + fn send(&self, message: Message) { + if let Err(error) = self.reconciler.send(message) { + error!("unable to send message: {error:?}"); + } + } + /// Action from the frontend to change the state of the view model #[uniffi::method] pub fn dispatch(&self, action: AuthManagerAction) { @@ -157,6 +176,13 @@ impl RustAuthManager { _ => {} } } + + Action::SetWipeDataPin(pin) => { + debug!("set wipe data pin"); + if let Err(error) = Database::global().global_config.set_wipe_data_pin(pin) { + error!("unable to set wipe data pin: {error:?}"); + } + } } } } From a0b5af336ed948e734d0dad2593bc0d6714c13ce Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Mon, 16 Dec 2024 19:58:33 -0600 Subject: [PATCH 05/10] Move lock state from AppManager to AuthManager --- ios/Cove/AppManager.swift | 21 ----- ios/Cove/AuthManager.swift | 24 ++++-- ios/Cove/Cove.swift | 80 ++++++++++++------- ios/Cove/CoveApp.swift | 22 ++--- .../SecretWordsScreen.swift | 3 +- ios/Cove/HomeScreens/SettingsScreen.swift | 40 +++++----- ios/Cove/SettingsScreen/WipePin.swift | 3 +- rust/src/app/reconcile.rs | 7 +- rust/src/database/global_config.rs | 3 +- 9 files changed, 111 insertions(+), 92 deletions(-) diff --git a/ios/Cove/AppManager.swift b/ios/Cove/AppManager.swift index 715e182..dde1325 100644 --- a/ios/Cove/AppManager.swift +++ b/ios/Cove/AppManager.swift @@ -15,14 +15,12 @@ import SwiftUI var colorSchemeSelection = Database().globalConfig().colorScheme() var selectedNode = Database().globalConfig().selectedNode() - var authType = Database().globalConfig().authType() var nfcReader = NFCReader() var prices: PriceResponse? var fees: FeeResponse? - var lockState: LockState = .locked // changed when route is reset, to clear lifecycle view state var routeId = UUID() @@ -76,19 +74,6 @@ import SwiftUI walletManager = vm } - public func lock() { - guard isAuthEnabled else { return } - lockState = .locked - } - - public func checkPin(_ pin: String) -> Bool { - AuthPin().check(pin: pin) - } - - var isAuthEnabled: Bool { - authType != AuthType.none - } - var currentRoute: Route { router.routes.last ?? router.default } @@ -164,12 +149,6 @@ import SwiftUI case let .feesChanged(fees): self.fees = fees - - case let .authTypeChanged(authType): - self.authType = authType - - case .hashedPinCodeChanged: - () } } } diff --git a/ios/Cove/AuthManager.swift b/ios/Cove/AuthManager.swift index 0f4127c..bea5a66 100644 --- a/ios/Cove/AuthManager.swift +++ b/ios/Cove/AuthManager.swift @@ -4,6 +4,7 @@ import SwiftUI private let logger = Log(id: "AuthManager") var rust: RustAuthManager var authType = Database().globalConfig().authType() + var lockState: LockState = .locked @MainActor var isUsingBiometrics: Bool = false @@ -12,20 +13,33 @@ import SwiftUI rust = RustAuthManager() rust.listenForUpdates(reconciler: self) } + + public func lock() { + guard isAuthEnabled else { return } + lockState = .locked + } public var isAuthEnabled: Bool { authType != AuthType.none } public func checkPin(_ pin: String) -> Bool { - AuthPin().check(pin: pin) + if AuthPin().check(pin: pin) { + return true + } + + if self.checkWipeDataPin(pin) { + // TODO: delete all data + } + + return false } - public func checkWipeMePin(_ pin: String) -> Bool { - AuthPin().check(pin: pin) + public func checkWipeDataPin(_ pin: String) -> Bool { + rust.checkWipeDataPin(pin: pin) } - - func reconcile(message: Aut hManagerReconcileMessage) { + + func reconcile(message: AuthManagerReconcileMessage) { logger.debug("reconcile: \(message)") Task { diff --git a/ios/Cove/Cove.swift b/ios/Cove/Cove.swift index f4263fa..37b0970 100644 --- a/ios/Cove/Cove.swift +++ b/ios/Cove/Cove.swift @@ -7294,6 +7294,16 @@ public protocol RustAuthManagerProtocol : AnyObject { */ func authType() -> AuthType + /** + * Check if the PIN matches the wipe data pin + */ + func checkWipeDataPin(pin: String) -> Bool + + /** + * Delete the wipe data pin + */ + func deleteWipeDataPin() + /** * Action from the frontend to change the state of the view model */ @@ -7372,6 +7382,26 @@ open func authType() -> AuthType { uniffi_cove_fn_method_rustauthmanager_auth_type(self.uniffiClonePointer(),$0 ) }) +} + + /** + * Check if the PIN matches the wipe data pin + */ +open func checkWipeDataPin(pin: String) -> Bool { + return try! FfiConverterBool.lift(try! rustCall() { + uniffi_cove_fn_method_rustauthmanager_check_wipe_data_pin(self.uniffiClonePointer(), + FfiConverterString.lower(pin),$0 + ) +}) +} + + /** + * Delete the wipe data pin + */ +open func deleteWipeDataPin() {try! rustCall() { + uniffi_cove_fn_method_rustauthmanager_delete_wipe_data_pin(self.uniffiClonePointer(),$0 + ) +} } /** @@ -12965,10 +12995,6 @@ public enum AppStateReconcileMessage { ) case feesChanged(FeeResponse ) - case authTypeChanged(AuthType - ) - case hashedPinCodeChanged(String - ) } @@ -13002,12 +13028,6 @@ public struct FfiConverterTypeAppStateReconcileMessage: FfiConverterRustBuffer { case 7: return .feesChanged(try FfiConverterTypeFeeResponse.read(from: &buf) ) - case 8: return .authTypeChanged(try FfiConverterTypeAuthType.read(from: &buf) - ) - - case 9: return .hashedPinCodeChanged(try FfiConverterString.read(from: &buf) - ) - default: throw UniffiInternalError.unexpectedEnumCase } } @@ -13050,16 +13070,6 @@ public struct FfiConverterTypeAppStateReconcileMessage: FfiConverterRustBuffer { writeInt(&buf, Int32(7)) FfiConverterTypeFeeResponse.write(v1, into: &buf) - - case let .authTypeChanged(v1): - writeInt(&buf, Int32(8)) - FfiConverterTypeAuthType.write(v1, into: &buf) - - - case let .hashedPinCodeChanged(v1): - writeInt(&buf, Int32(9)) - FfiConverterString.write(v1, into: &buf) - } } } @@ -13204,6 +13214,8 @@ public enum AuthManagerAction { case setPin(String ) case disablePin + case setWipeDataPin(String + ) } @@ -13229,6 +13241,9 @@ public struct FfiConverterTypeAuthManagerAction: FfiConverterRustBuffer { case 5: return .disablePin + case 6: return .setWipeDataPin(try FfiConverterString.read(from: &buf) + ) + default: throw UniffiInternalError.unexpectedEnumCase } } @@ -13258,6 +13273,11 @@ public struct FfiConverterTypeAuthManagerAction: FfiConverterRustBuffer { case .disablePin: writeInt(&buf, Int32(5)) + + case let .setWipeDataPin(v1): + writeInt(&buf, Int32(6)) + FfiConverterString.write(v1, into: &buf) + } } } @@ -14895,6 +14915,7 @@ public enum GlobalConfigKey { case colorScheme case authType case hashedPinCode + case wipeDataPin } @@ -14921,6 +14942,8 @@ public struct FfiConverterTypeGlobalConfigKey: FfiConverterRustBuffer { case 6: return .hashedPinCode + case 7: return .wipeDataPin + default: throw UniffiInternalError.unexpectedEnumCase } } @@ -14953,6 +14976,10 @@ public struct FfiConverterTypeGlobalConfigKey: FfiConverterRustBuffer { case .hashedPinCode: writeInt(&buf, Int32(6)) + + case .wipeDataPin: + writeInt(&buf, Int32(7)) + } } } @@ -15055,7 +15082,6 @@ extension GlobalConfigTableError: Foundation.LocalizedError { public enum GlobalFlagKey { case completedOnboarding - case wipeMePinEnabled } @@ -15071,8 +15097,6 @@ public struct FfiConverterTypeGlobalFlagKey: FfiConverterRustBuffer { case 1: return .completedOnboarding - case 2: return .wipeMePinEnabled - default: throw UniffiInternalError.unexpectedEnumCase } } @@ -15084,10 +15108,6 @@ public struct FfiConverterTypeGlobalFlagKey: FfiConverterRustBuffer { case .completedOnboarding: writeInt(&buf, Int32(1)) - - case .wipeMePinEnabled: - writeInt(&buf, Int32(2)) - } } } @@ -22500,6 +22520,12 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_rustauthmanager_auth_type() != 13301) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_method_rustauthmanager_check_wipe_data_pin() != 49775) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_cove_checksum_method_rustauthmanager_delete_wipe_data_pin() != 30374) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_method_rustauthmanager_dispatch() != 58198) { return InitializationResult.apiChecksumMismatch } diff --git a/ios/Cove/CoveApp.swift b/ios/Cove/CoveApp.swift index 5c08c33..1f55738 100644 --- a/ios/Cove/CoveApp.swift +++ b/ios/Cove/CoveApp.swift @@ -359,10 +359,10 @@ struct CoveApp: App { CoverView() } else { LockView( - lockType: app.authType, - isPinCorrect: app.checkPin, + lockType: auth.authType, + isPinCorrect: auth.checkPin, showPin: false, - lockState: $app.lockState, + lockState: $auth.lockState, onUnlock: { _ in showCover = false } ) { SidebarContainer { @@ -452,12 +452,12 @@ struct CoveApp: App { if phase == .active { showCover = false } } - app.lockState = .locked + auth.lockState = .locked lockedAt = Date.now } // close all open sheets when going into the background - if app.isAuthEnabled, oldPhase == .inactive, newPhase == .background { + if auth.isAuthEnabled, oldPhase == .inactive, newPhase == .background { coverClearTask?.cancel() lockedAt = Date.now @@ -469,13 +469,13 @@ struct CoveApp: App { } } - if app.isAuthEnabled, oldPhase == .inactive, newPhase == .active { + if auth.isAuthEnabled, oldPhase == .inactive, newPhase == .active { showCover = false // less than 15 seconds, auto unlock if PIN only // TODO: make this configurable and put in DB - if app.authType == .pin, Date.now.timeIntervalSince(lockedAt) < 15 { - app.lockState = .locked + if auth.authType == .pin, Date.now.timeIntervalSince(lockedAt) < 15 { + auth.lockState = .locked } } } @@ -530,10 +530,10 @@ struct CoveApp: App { .onOpenURL(perform: handleFileOpen) .onChange(of: phase, initial: true, handleScenePhaseChange) .onAppear { - if app.isAuthEnabled { - app.lockState = .locked + if auth.isAuthEnabled { + auth.lockState = .locked } else { - app.lockState = .unlocked + auth.lockState = .unlocked } } } diff --git a/ios/Cove/Flows/SelectedWalletFlow/SecretWordsScreen.swift b/ios/Cove/Flows/SelectedWalletFlow/SecretWordsScreen.swift index ffeb4a1..1e22e20 100644 --- a/ios/Cove/Flows/SelectedWalletFlow/SecretWordsScreen.swift +++ b/ios/Cove/Flows/SelectedWalletFlow/SecretWordsScreen.swift @@ -9,6 +9,7 @@ import SwiftUI struct SecretWordsScreen: View { @Environment(AppManager.self) private var app + @Environment(AuthManager.self) private var auth let id: WalletId @@ -105,7 +106,7 @@ struct SecretWordsScreen: View { } .padding() .onAppear { - app.lock() + auth.lock() guard words == nil else { return } do { words = try Mnemonic(id: id) } catch { errorMessage = error.localizedDescription } diff --git a/ios/Cove/HomeScreens/SettingsScreen.swift b/ios/Cove/HomeScreens/SettingsScreen.swift index d2a51c0..e39c6c3 100644 --- a/ios/Cove/HomeScreens/SettingsScreen.swift +++ b/ios/Cove/HomeScreens/SettingsScreen.swift @@ -31,7 +31,7 @@ struct SettingsScreen: View { var toggleBiometric: Binding { Binding( - get: { app.authType == AuthType.both || app.authType == AuthType.biometric }, + get: { auth.authType == AuthType.both || auth.authType == AuthType.biometric }, set: { enable in if enable { sheetState = .init(.enableBiometric) @@ -44,7 +44,7 @@ struct SettingsScreen: View { var togglePin: Binding { Binding( - get: { app.authType == AuthType.both || app.authType == AuthType.pin }, + get: { auth.authType == AuthType.both || auth.authType == AuthType.pin }, set: { enable in if enable { sheetState = .init(.newPin) } else { sheetState = .init(.removePin) } } @@ -165,20 +165,20 @@ struct SettingsScreen: View { .gesture( networkChanged ? DragGesture() - .onChanged { gesture in - if gesture.startLocation.x < 25, gesture.translation.width > 100 { - withAnimation(.spring()) { - alertState = .init(.networkChanged(app.selectedNetwork)) + .onChanged { gesture in + if gesture.startLocation.x < 25, gesture.translation.width > 100 { + withAnimation(.spring()) { + alertState = .init(.networkChanged(app.selectedNetwork)) + } } } - } - .onEnded { gesture in - if gesture.startLocation.x < 20, gesture.translation.width > 50 { - withAnimation(.spring()) { - alertState = .init(.networkChanged(app.selectedNetwork)) + .onEnded { gesture in + if gesture.startLocation.x < 20, gesture.translation.width > 50 { + withAnimation(.spring()) { + alertState = .init(.networkChanged(app.selectedNetwork)) + } } - } - } : nil + } : nil ) } @@ -239,14 +239,16 @@ struct SettingsScreen: View { return AlertBuilder( title: "Are you sure?", message: - """ + """ + + Enabling the Wipe Data PIN will let you chose a PIN that if entered will wipe all Cove wallet data on this device. - Enabling the Wipe Data PIN will let you chose a PIN that if entered will wipe all Cove wallet data on this device. + If you wipe the data without having a back up of your wallet, you will lose the bitcoin in that wallet. - If you wipe the data without having a back up of your wallet, you will lose the bitcoin in that wallet. + Please make sure you have a backup of your wallet before enabling this. - Please make sure you have a backup of your wallet before enabling this. - """, + Note: Enabling the Wipe Data PIN will disable FaceID auth if its enabled. + """, actions: { Button("Yes, Enable Wipe Data PIN") { // app.dispatch(action: .enableWipeMePin) @@ -307,7 +309,7 @@ struct SettingsScreen: View { case .disableBiometric: LockView( lockType: auth.authType, - isPinCorrect: app.checkPin, + isPinCorrect: auth.checkPin, onUnlock: { _ in auth.dispatch(action: .disableBiometric) sheetState = .none diff --git a/ios/Cove/SettingsScreen/WipePin.swift b/ios/Cove/SettingsScreen/WipePin.swift index c57c070..e6cd187 100644 --- a/ios/Cove/SettingsScreen/WipePin.swift +++ b/ios/Cove/SettingsScreen/WipePin.swift @@ -13,6 +13,7 @@ private enum PinState { struct WipePin: View { @Environment(AppManager.self) var app + @Environment(AuthManager.self) var auth /// args var onComplete: (String) -> Void @@ -27,7 +28,7 @@ struct WipePin: View { case .pin: NumberPadPinView( title: "Enter Current PIN", - isPinCorrect: app.checkPin, + isPinCorrect: auth.checkPin, showPin: false, backAction: backAction, onUnlock: { _ in diff --git a/rust/src/app/reconcile.rs b/rust/src/app/reconcile.rs index b48b5ff..eee2d31 100644 --- a/rust/src/app/reconcile.rs +++ b/rust/src/app/reconcile.rs @@ -4,8 +4,8 @@ use crossbeam::channel::Sender; use once_cell::sync::OnceCell; use crate::{ - auth::AuthType, color_scheme::ColorSchemeSelection, fiat::client::PriceResponse, node::Node, - router::Route, transaction::fees::client::FeeResponse, + color_scheme::ColorSchemeSelection, fiat::client::PriceResponse, node::Node, router::Route, + transaction::fees::client::FeeResponse, }; #[derive(uniffi::Enum)] @@ -18,9 +18,6 @@ pub enum AppStateReconcileMessage { SelectedNodeChanged(Node), FiatPricesChanged(PriceResponse), FeesChanged(FeeResponse), - AuthTypeChanged(AuthType), - HashedPinCodeChanged(String), - WipeDataPinChanged(String), } // alias for easier imports on the rust side diff --git a/rust/src/database/global_config.rs b/rust/src/database/global_config.rs index 030a593..c8a8a6f 100644 --- a/rust/src/database/global_config.rs +++ b/rust/src/database/global_config.rs @@ -74,8 +74,7 @@ impl GlobalConfigTable { string_config_accessor!( pub auth_type, GlobalConfigKey::AuthType, - AuthType, - Update::AuthTypeChanged + AuthType ); string_config_accessor!( From 8ecc214e099cd7cb123e2b0116128fc36092c5dd Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Mon, 16 Dec 2024 20:09:18 -0600 Subject: [PATCH 06/10] Create function to get list of unverified wallet ids --- rust/src/app.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/rust/src/app.rs b/rust/src/app.rs index a20e125..4b759c0 100644 --- a/rust/src/app.rs +++ b/rust/src/app.rs @@ -13,7 +13,7 @@ use crate::{ node::Node, router::{Route, RouteFactory, Router}, transaction::fees::client::{FeeResponse, FEE_CLIENT}, - wallet::metadata::WalletId, + wallet::metadata::{WalletId, WalletType}, }; use crossbeam::channel::{Receiver, Sender}; use macros::impl_default_for; @@ -269,6 +269,17 @@ impl FfiApp { Database::global().wallets().len(network).unwrap_or(0) } + /// Get wallets that have not been backed up and verified + pub fn unverified_wallet_ids(&self) -> Vec { + let all_wallets = Database::global().wallets().all().unwrap_or_default(); + + all_wallets + .into_iter() + .filter(|wallet| wallet.wallet_type == WalletType::Hot && !wallet.verified) + .map(|wallet| wallet.id) + .collect::>() + } + /// Load and reset the default route after 800ms delay pub fn load_and_reset_default_route(&self, route: Route) { self.load_and_reset_default_route_after(route, 800); From fc9b8a3c18418f9d2d02a4196b57afd21a35c27e Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Tue, 17 Dec 2024 11:24:17 -0600 Subject: [PATCH 07/10] Make AuthManager a singleton --- rust/src/manager/auth.rs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/rust/src/manager/auth.rs b/rust/src/manager/auth.rs index 993d59f..1e3f7de 100644 --- a/rust/src/manager/auth.rs +++ b/rust/src/manager/auth.rs @@ -1,7 +1,8 @@ -use std::sync::Arc; +use std::sync::{Arc, LazyLock}; use crossbeam::channel::{Receiver, Sender}; use macros::impl_default_for; +use once_cell::sync::OnceCell; use parking_lot::RwLock; use tap::TapFallible as _; use tracing::{debug, error}; @@ -13,6 +14,8 @@ use crate::{ type Message = AuthManagerReconcileMessage; +pub static AUTH_MANAGER: LazyLock> = LazyLock::new(RustAuthManager::init); + #[derive(Debug, Clone, Hash, Eq, PartialEq, uniffi::Enum)] pub enum AuthManagerReconcileMessage { AuthTypeChanged(AuthType), @@ -47,12 +50,11 @@ pub enum AuthManagerAction { SetPin(String), DisablePin, SetWipeDataPin(String), + DisableWipeDataPin, } -#[uniffi::export] impl RustAuthManager { - #[uniffi::constructor] - pub fn new() -> Self { + fn init() -> Arc { let (sender, receiver) = crossbeam::channel::bounded(1000); Self { @@ -60,6 +62,15 @@ impl RustAuthManager { reconciler: sender, reconcile_receiver: Arc::new(receiver), } + .into() + } +} + +#[uniffi::export] +impl RustAuthManager { + #[uniffi::constructor] + pub fn new() -> Arc { + AUTH_MANAGER.clone() } #[uniffi::method] @@ -183,6 +194,13 @@ impl RustAuthManager { error!("unable to set wipe data pin: {error:?}"); } } + + Action::DisableWipeDataPin => { + debug!("disable wipe data pin"); + if let Err(error) = Database::global().global_config.delete_wipe_data_pin() { + error!("unable to delete wipe data pin: {error:?}"); + } + } } } } From 09f92897af5468fc10cc26c97e01cb32b9aad138 Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Tue, 17 Dec 2024 16:09:20 -0600 Subject: [PATCH 08/10] Complete setting wipe data pin with validations --- ios/Cove/AlertBuilder.swift | 16 + ios/Cove/AuthManager.swift | 31 +- ios/Cove/Cove.swift | 296 ++++++++++++++++-- ios/Cove/CoveApp.swift | 51 +-- ios/Cove/Extention/Error+Ext.swift | 12 + ios/Cove/HomeScreens/SettingsScreen.swift | 206 +++++++++--- ios/Cove/SettingsScreen/WipeDataPinView.swift | 54 ++++ ios/Cove/Views/NumberPadPinView.swift | 4 +- rust/src/cove_nfc/parser.rs | 1 - rust/src/manager/auth.rs | 97 ++++-- 10 files changed, 658 insertions(+), 110 deletions(-) create mode 100644 ios/Cove/Extention/Error+Ext.swift create mode 100644 ios/Cove/SettingsScreen/WipeDataPinView.swift diff --git a/ios/Cove/AlertBuilder.swift b/ios/Cove/AlertBuilder.swift index f2defed..d67bd3a 100644 --- a/ios/Cove/AlertBuilder.swift +++ b/ios/Cove/AlertBuilder.swift @@ -6,6 +6,18 @@ // import SwiftUI +struct AnyAlertBuilder: AlertBuilderProtocol { + let title: String + let message: AnyView + let actions: AnyView + + init(_ alert: some AlertBuilderProtocol) { + title = alert.title + message = AnyView(alert.message) + actions = AnyView(alert.actions) + } +} + protocol AlertBuilderProtocol { associatedtype Message: View associatedtype Actions: View @@ -39,4 +51,8 @@ struct AlertBuilder: AlertBuilderProtocol { self.message = Text(message) self.actions = actions() } + + func eraseToAny() -> AnyAlertBuilder { + AnyAlertBuilder(self) + } } diff --git a/ios/Cove/AuthManager.swift b/ios/Cove/AuthManager.swift index bea5a66..c49a994 100644 --- a/ios/Cove/AuthManager.swift +++ b/ios/Cove/AuthManager.swift @@ -3,24 +3,28 @@ import SwiftUI @Observable class AuthManager: AuthManagerReconciler { private let logger = Log(id: "AuthManager") var rust: RustAuthManager - var authType = Database().globalConfig().authType() + var type = Database().globalConfig().authType() var lockState: LockState = .locked + var isWipeDataPinEnabled: Bool @MainActor var isUsingBiometrics: Bool = false public init() { - rust = RustAuthManager() + let rust = RustAuthManager() + self.rust = rust + isWipeDataPinEnabled = rust.isWipeDataPinEnabled() + rust.listenForUpdates(reconciler: self) } - + public func lock() { guard isAuthEnabled else { return } lockState = .locked } public var isAuthEnabled: Bool { - authType != AuthType.none + type != AuthType.none } public func checkPin(_ pin: String) -> Bool { @@ -28,8 +32,18 @@ import SwiftUI return true } - if self.checkWipeDataPin(pin) { - // TODO: delete all data + if checkWipeDataPin(pin) { + AppManager().rust.dangerousWipeAllData() + + // reset auth maanger + rust = RustAuthManager() + lockState = .unlocked + type = .none + + // reset app manager + AppManager().reset() + + return true } return false @@ -46,7 +60,10 @@ import SwiftUI await MainActor.run { switch message { case let .authTypeChanged(authType): - self.authType = authType + self.type = authType + + case .wipeDataPinChanged: + self.isWipeDataPinEnabled = self.rust.isWipeDataPinEnabled() } } } diff --git a/ios/Cove/Cove.swift b/ios/Cove/Cove.swift index 37b0970..2e715a9 100644 --- a/ios/Cove/Cove.swift +++ b/ios/Cove/Cove.swift @@ -2944,6 +2944,8 @@ public func FfiConverterTypeConfirmedTransaction_lower(_ value: ConfirmedTransac public protocol DatabaseProtocol : AnyObject { + func dangerousResetAllData() + func globalConfig() -> GlobalConfigTable func unsignedTransactions() -> UnsignedTransactionsTable @@ -3009,6 +3011,12 @@ public convenience init() { +open func dangerousResetAllData() {try! rustCall() { + uniffi_cove_fn_method_database_dangerous_reset_all_data(self.uniffiClonePointer(),$0 + ) +} +} + open func globalConfig() -> GlobalConfigTable { return try! FfiConverterTypeGlobalConfigTable.lift(try! rustCall() { uniffi_cove_fn_method_database_global_config(self.uniffiClonePointer(),$0 @@ -3984,6 +3992,11 @@ public protocol FfiAppProtocol : AnyObject { */ func authType() -> AuthType + /** + * DANGER: This will wipe all wallet data on this device + */ + func dangerousWipeAllData() + /** * Frontend calls this method to send events to the rust application logic */ @@ -4045,6 +4058,11 @@ public protocol FfiAppProtocol : AnyObject { func state() -> AppState + /** + * Get wallets that have not been backed up and verified + */ + func unverifiedWalletIds() -> [WalletId] + } /** @@ -4118,6 +4136,15 @@ open func authType() -> AuthType { uniffi_cove_fn_method_ffiapp_auth_type(self.uniffiClonePointer(),$0 ) }) +} + + /** + * DANGER: This will wipe all wallet data on this device + */ +open func dangerousWipeAllData() {try! rustCall() { + uniffi_cove_fn_method_ffiapp_dangerous_wipe_all_data(self.uniffiClonePointer(),$0 + ) +} } /** @@ -4290,6 +4317,16 @@ open func state() -> AppState { }) } + /** + * Get wallets that have not been backed up and verified + */ +open func unverifiedWalletIds() -> [WalletId] { + return try! FfiConverterSequenceTypeWalletId.lift(try! rustCall() { + uniffi_cove_fn_method_ffiapp_unverified_wallet_ids(self.uniffiClonePointer(),$0 + ) +}) +} + } @@ -7295,7 +7332,7 @@ public protocol RustAuthManagerProtocol : AnyObject { func authType() -> AuthType /** - * Check if the PIN matches the wipe data pin + * Check to see if the passed in PIN matches the wipe data PIN */ func checkWipeDataPin(pin: String) -> Bool @@ -7309,12 +7346,22 @@ public protocol RustAuthManagerProtocol : AnyObject { */ func dispatch(action: AuthManagerAction) + /** + * Check if the wipe data pin is enabled + */ + func isWipeDataPinEnabled() -> Bool + func listenForUpdates(reconciler: AuthManagerReconciler) func send(message: AuthManagerReconcileMessage) func setAuthType(authType: AuthType) + /** + * Set the wipe data pin + */ + func setWipeDataPin(pin: String) throws + } open class RustAuthManager: @@ -7385,7 +7432,7 @@ open func authType() -> AuthType { } /** - * Check if the PIN matches the wipe data pin + * Check to see if the passed in PIN matches the wipe data PIN */ open func checkWipeDataPin(pin: String) -> Bool { return try! FfiConverterBool.lift(try! rustCall() { @@ -7412,6 +7459,16 @@ open func dispatch(action: AuthManagerAction) {try! rustCall() { FfiConverterTypeAuthManagerAction.lower(action),$0 ) } +} + + /** + * Check if the wipe data pin is enabled + */ +open func isWipeDataPinEnabled() -> Bool { + return try! FfiConverterBool.lift(try! rustCall() { + uniffi_cove_fn_method_rustauthmanager_is_wipe_data_pin_enabled(self.uniffiClonePointer(),$0 + ) +}) } open func listenForUpdates(reconciler: AuthManagerReconciler) {try! rustCall() { @@ -7435,6 +7492,16 @@ open func setAuthType(authType: AuthType) {try! rustCall() { } } + /** + * Set the wipe data pin + */ +open func setWipeDataPin(pin: String)throws {try rustCallWithError(FfiConverterTypeAuthManagerError.lift) { + uniffi_cove_fn_method_rustauthmanager_set_wipe_data_pin(self.uniffiClonePointer(), + FfiConverterString.lower(pin),$0 + ) +} +} + } @@ -13211,11 +13278,10 @@ public enum AuthManagerAction { ) case enableBiometric case disableBiometric - case setPin(String - ) case disablePin - case setWipeDataPin(String + case setPin(String ) + case disableWipeDataPin } @@ -13236,14 +13302,13 @@ public struct FfiConverterTypeAuthManagerAction: FfiConverterRustBuffer { case 3: return .disableBiometric - case 4: return .setPin(try FfiConverterString.read(from: &buf) - ) - - case 5: return .disablePin + case 4: return .disablePin - case 6: return .setWipeDataPin(try FfiConverterString.read(from: &buf) + case 5: return .setPin(try FfiConverterString.read(from: &buf) ) + case 6: return .disableWipeDataPin + default: throw UniffiInternalError.unexpectedEnumCase } } @@ -13265,19 +13330,18 @@ public struct FfiConverterTypeAuthManagerAction: FfiConverterRustBuffer { writeInt(&buf, Int32(3)) - case let .setPin(v1): - writeInt(&buf, Int32(4)) - FfiConverterString.write(v1, into: &buf) - - case .disablePin: - writeInt(&buf, Int32(5)) + writeInt(&buf, Int32(4)) - case let .setWipeDataPin(v1): - writeInt(&buf, Int32(6)) + case let .setPin(v1): + writeInt(&buf, Int32(5)) FfiConverterString.write(v1, into: &buf) + + case .disableWipeDataPin: + writeInt(&buf, Int32(6)) + } } } @@ -13303,6 +13367,71 @@ extension AuthManagerAction: Equatable, Hashable {} + +public enum AuthManagerError { + + + + case WipeDataSet(WipeDataPinError + ) + case DatabaseError(DatabaseError + ) +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeAuthManagerError: FfiConverterRustBuffer { + typealias SwiftType = AuthManagerError + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> AuthManagerError { + let variant: Int32 = try readInt(&buf) + switch variant { + + + + + case 1: return .WipeDataSet( + try FfiConverterTypeWipeDataPinError.read(from: &buf) + ) + case 2: return .DatabaseError( + try FfiConverterTypeDatabaseError.read(from: &buf) + ) + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: AuthManagerError, into buf: inout [UInt8]) { + switch value { + + + + + + case let .WipeDataSet(v1): + writeInt(&buf, Int32(1)) + FfiConverterTypeWipeDataPinError.write(v1, into: &buf) + + + case let .DatabaseError(v1): + writeInt(&buf, Int32(2)) + FfiConverterTypeDatabaseError.write(v1, into: &buf) + + } + } +} + + +extension AuthManagerError: Equatable, Hashable {} + +extension AuthManagerError: Foundation.LocalizedError { + public var errorDescription: String? { + String(reflecting: self) + } +} + // Note that we don't yet support `indirect` for enums. // See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. @@ -13310,6 +13439,7 @@ public enum AuthManagerReconcileMessage { case authTypeChanged(AuthType ) + case wipeDataPinChanged } @@ -13326,6 +13456,8 @@ public struct FfiConverterTypeAuthManagerReconcileMessage: FfiConverterRustBuffe case 1: return .authTypeChanged(try FfiConverterTypeAuthType.read(from: &buf) ) + case 2: return .wipeDataPinChanged + default: throw UniffiInternalError.unexpectedEnumCase } } @@ -13338,6 +13470,10 @@ public struct FfiConverterTypeAuthManagerReconcileMessage: FfiConverterRustBuffe writeInt(&buf, Int32(1)) FfiConverterTypeAuthType.write(v1, into: &buf) + + case .wipeDataPinChanged: + writeInt(&buf, Int32(2)) + } } } @@ -19842,6 +19978,78 @@ extension WalletType: Equatable, Hashable {} +public enum WipeDataPinError { + + + + /** + * Unable to set wipe data pin, because PIN is not enabled + */ + case PinNotEnabled + /** + * Unable to set wipe data pin, because its the same as the current pin + */ + case SameAsCurrentPin + /** + * Unable to set wipe data pin, because biometrics is enabled + */ + case BiometricsEnabled +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeWipeDataPinError: FfiConverterRustBuffer { + typealias SwiftType = WipeDataPinError + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> WipeDataPinError { + let variant: Int32 = try readInt(&buf) + switch variant { + + + + + case 1: return .PinNotEnabled + case 2: return .SameAsCurrentPin + case 3: return .BiometricsEnabled + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: WipeDataPinError, into buf: inout [UInt8]) { + switch value { + + + + + + case .PinNotEnabled: + writeInt(&buf, Int32(1)) + + + case .SameAsCurrentPin: + writeInt(&buf, Int32(2)) + + + case .BiometricsEnabled: + writeInt(&buf, Int32(3)) + + } + } +} + + +extension WipeDataPinError: Equatable, Hashable {} + +extension WipeDataPinError: Foundation.LocalizedError { + public var errorDescription: String? { + String(reflecting: self) + } +} + + public enum XpubError { @@ -21541,6 +21749,31 @@ fileprivate struct FfiConverterSequenceSequenceTypeGroupedWord: FfiConverterRust } } +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceTypeWalletId: FfiConverterRustBuffer { + typealias SwiftType = [WalletId] + + public static func write(_ value: [WalletId], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeWalletId.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [WalletId] { + let len: Int32 = try readInt(&buf) + var seq = [WalletId]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeWalletId.read(from: &buf)) + } + return seq + } +} + /** * Typealias from the type name used in the UDL file to the builtin type. This @@ -21720,6 +21953,13 @@ public func allUnits() -> [Unit] { ) }) } +public func authManagerErrorToString(error: AuthManagerError) -> String { + return try! FfiConverterString.lift(try! rustCall() { + uniffi_cove_fn_func_auth_manager_error_to_string( + FfiConverterTypeAuthManagerError.lower(error),$0 + ) +}) +} public func balanceZero() -> Balance { return try! FfiConverterTypeBalance.lift(try! rustCall() { uniffi_cove_fn_func_balance_zero($0 @@ -21953,6 +22193,9 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_func_all_units() != 36925) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_func_auth_manager_error_to_string() != 12061) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_func_balance_zero() != 63807) { return InitializationResult.apiChecksumMismatch } @@ -22193,6 +22436,9 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_confirmedtransaction_sent_and_received() != 3525) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_method_database_dangerous_reset_all_data() != 31513) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_method_database_global_config() != 4476) { return InitializationResult.apiChecksumMismatch } @@ -22265,6 +22511,9 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_ffiapp_auth_type() != 34438) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_method_ffiapp_dangerous_wipe_all_data() != 63122) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_method_ffiapp_dispatch() != 48712) { return InitializationResult.apiChecksumMismatch } @@ -22310,6 +22559,9 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_ffiapp_state() != 19551) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_method_ffiapp_unverified_wallet_ids() != 20710) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_method_ffinfcreader_data_from_records() != 32962) { return InitializationResult.apiChecksumMismatch } @@ -22520,7 +22772,7 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_rustauthmanager_auth_type() != 13301) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cove_checksum_method_rustauthmanager_check_wipe_data_pin() != 49775) { + if (uniffi_cove_checksum_method_rustauthmanager_check_wipe_data_pin() != 50482) { return InitializationResult.apiChecksumMismatch } if (uniffi_cove_checksum_method_rustauthmanager_delete_wipe_data_pin() != 30374) { @@ -22529,6 +22781,9 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_rustauthmanager_dispatch() != 58198) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_method_rustauthmanager_is_wipe_data_pin_enabled() != 29022) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_method_rustauthmanager_listen_for_updates() != 6029) { return InitializationResult.apiChecksumMismatch } @@ -22538,6 +22793,9 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_rustauthmanager_set_auth_type() != 20435) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_method_rustauthmanager_set_wipe_data_pin() != 20226) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_method_rustimportwalletmanager_dispatch() != 61781) { return InitializationResult.apiChecksumMismatch } diff --git a/ios/Cove/CoveApp.swift b/ios/Cove/CoveApp.swift index 1f55738..28c5549 100644 --- a/ios/Cove/CoveApp.swift +++ b/ios/Cove/CoveApp.swift @@ -35,8 +35,8 @@ struct SafeAreaInsetsKey: EnvironmentKey { } } -extension EnvironmentValues { - public var safeAreaInsets: EdgeInsets { +public extension EnvironmentValues { + var safeAreaInsets: EdgeInsets { self[SafeAreaInsetsKey.self] } } @@ -79,7 +79,7 @@ struct CoveApp: App { ): "The address \(address) is on the wrong network. You are on \(currentNetwork), and the address was for \(network)." case let .noWalletSelected(address), - let .foundAddress(address, _): + let .foundAddress(address, _): String(address) case .noCameraPermission: "Please allow camera access in Settings to use this feature." @@ -101,11 +101,11 @@ struct CoveApp: App { try? app.rust.selectWallet(id: walletId) } case .invalidWordGroup, - .errorImportingHotWallet, - .importedSuccessfully, - .unableToSelectWallet, - .errorImportingHardwareWallet, - .invalidFileFormat: + .errorImportingHotWallet, + .importedSuccessfully, + .unableToSelectWallet, + .errorImportingHardwareWallet, + .invalidFileFormat: Button("OK") { app.alertState = .none } @@ -359,7 +359,7 @@ struct CoveApp: App { CoverView() } else { LockView( - lockType: auth.authType, + lockType: auth.type, isPinCorrect: auth.checkPin, showPin: false, lockState: $auth.lockState, @@ -440,8 +440,11 @@ struct CoveApp: App { "[SCENE PHASE]: \(oldPhase) --> \(newPhase) && using biometrics: \(auth.isUsingBiometrics)" ) + if !auth.isAuthEnabled { showCover = false } + if newPhase == .active { showCover = false } + if auth.isAuthEnabled, !auth.isUsingBiometrics, oldPhase == .active, - newPhase == .inactive + newPhase == .inactive { coverClearTask?.cancel() showCover = true @@ -472,10 +475,10 @@ struct CoveApp: App { if auth.isAuthEnabled, oldPhase == .inactive, newPhase == .active { showCover = false - // less than 15 seconds, auto unlock if PIN only + // less than 1 seconds, auto unlock if PIN only // TODO: make this configurable and put in DB - if auth.authType == .pin, Date.now.timeIntervalSince(lockedAt) < 15 { - auth.lockState = .locked + if auth.type == .pin, Date.now.timeIntervalSince(lockedAt) < 1 { + auth.lockState = .unlocked } } } @@ -508,20 +511,20 @@ struct CoveApp: App { .gesture( app.router.routes.isEmpty ? DragGesture() - .onChanged { gesture in - if gesture.startLocation.x < 25, gesture.translation.width > 100 { - withAnimation(.spring()) { - app.isSidebarVisible = true - } + .onChanged { gesture in + if gesture.startLocation.x < 25, gesture.translation.width > 100 { + withAnimation(.spring()) { + app.isSidebarVisible = true } } - .onEnded { gesture in - if gesture.startLocation.x < 20, gesture.translation.width > 50 { - withAnimation(.spring()) { - app.isSidebarVisible = true - } + } + .onEnded { gesture in + if gesture.startLocation.x < 20, gesture.translation.width > 50 { + withAnimation(.spring()) { + app.isSidebarVisible = true } - } : nil + } + } : nil ) .task { await app.rust.initOnStart() diff --git a/ios/Cove/Extention/Error+Ext.swift b/ios/Cove/Extention/Error+Ext.swift new file mode 100644 index 0000000..295d5fa --- /dev/null +++ b/ios/Cove/Extention/Error+Ext.swift @@ -0,0 +1,12 @@ +// +// Error+Ext.swift +// Cove +// +// Created by Praveen Perera on 12/17/24. +// + +extension AuthManagerError { + var describe: String { + authManagerErrorToString(error: self) + } +} diff --git a/ios/Cove/HomeScreens/SettingsScreen.swift b/ios/Cove/HomeScreens/SettingsScreen.swift index e39c6c3..55754a2 100644 --- a/ios/Cove/HomeScreens/SettingsScreen.swift +++ b/ios/Cove/HomeScreens/SettingsScreen.swift @@ -2,12 +2,23 @@ import LocalAuthentication import SwiftUI private enum SheetState: Equatable { - case newPin, removePin, changePin, disableBiometric, enableAuth, enableBiometric + case newPin, + removePin, + changePin, + disableBiometric, + enableAuth, + enableBiometric, + enableWipeDataPin } private enum AlertState: Equatable { case networkChanged(Network) + case unverifiedWallets(WalletId) case confirmEnableWipeMePin + case noteNoFaceIdWhenWipeMePin + case notePinRequired + case noteFaceIdDisabling + case wipeDataSetPinError(String) } struct SettingsScreen: View { @@ -31,12 +42,19 @@ struct SettingsScreen: View { var toggleBiometric: Binding { Binding( - get: { auth.authType == AuthType.both || auth.authType == AuthType.biometric }, + get: { auth.type == AuthType.both || auth.type == AuthType.biometric }, set: { enable in - if enable { - sheetState = .init(.enableBiometric) - } else { + // disable + if !enable { sheetState = .init(.disableBiometric) + return + } + + // enable + if auth.isWipeDataPinEnabled { + alertState = .init(.noteNoFaceIdWhenWipeMePin) + } else { + sheetState = .init(.enableBiometric) } } ) @@ -44,7 +62,7 @@ struct SettingsScreen: View { var togglePin: Binding { Binding( - get: { auth.authType == AuthType.both || auth.authType == AuthType.pin }, + get: { auth.type == AuthType.both || auth.type == AuthType.pin }, set: { enable in if enable { sheetState = .init(.newPin) } else { sheetState = .init(.removePin) } } @@ -53,9 +71,31 @@ struct SettingsScreen: View { var toggleWipeMePin: Binding { Binding( - get: { false }, + get: { auth.isWipeDataPinEnabled }, set: { enable in - if enable { alertState = .init(.confirmEnableWipeMePin) } + // enable + if enable { + if !app.rust.unverifiedWalletIds().isEmpty { + alertState = .init( + .unverifiedWallets(app.rust.unverifiedWalletIds().first!)) + return + } + + if auth.type == .biometric { + alertState = .init(.notePinRequired) + return + } + + if auth.type == .both { + alertState = .init(.noteFaceIdDisabling) + return + } + + alertState = .init(.confirmEnableWipeMePin) + } + + // disable + if !enable { auth.dispatch(action: .disableWipeDataPin) } } ) } @@ -131,6 +171,7 @@ struct SettingsScreen: View { } } .navigationTitle("Settings") + .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden(networkChanged) .toolbar { networkChanged @@ -165,20 +206,20 @@ struct SettingsScreen: View { .gesture( networkChanged ? DragGesture() - .onChanged { gesture in - if gesture.startLocation.x < 25, gesture.translation.width > 100 { - withAnimation(.spring()) { - alertState = .init(.networkChanged(app.selectedNetwork)) - } + .onChanged { gesture in + if gesture.startLocation.x < 25, gesture.translation.width > 100 { + withAnimation(.spring()) { + alertState = .init(.networkChanged(app.selectedNetwork)) } } - .onEnded { gesture in - if gesture.startLocation.x < 20, gesture.translation.width > 50 { - withAnimation(.spring()) { - alertState = .init(.networkChanged(app.selectedNetwork)) - } + } + .onEnded { gesture in + if gesture.startLocation.x < 20, gesture.translation.width > 50 { + withAnimation(.spring()) { + alertState = .init(.networkChanged(app.selectedNetwork)) } - } : nil + } + } : nil ) } @@ -220,10 +261,10 @@ struct SettingsScreen: View { return MyAlert(alertState).title } - private func MyAlert(_ alert: TaggedItem) -> some AlertBuilderProtocol { + private func MyAlert(_ alert: TaggedItem) -> AnyAlertBuilder { switch alert.item { case let .networkChanged(network): - return AlertBuilder( + AlertBuilder( title: "⚠️ Network Changed ⚠️", message: "You've changed your network to \(network)", actions: { @@ -231,34 +272,96 @@ struct SettingsScreen: View { app.resetRoute(to: .listWallets) dismiss() } - Button("Cancel", role: .cancel) {} + Button("Cancel", role: .cancel) { alertState = .none } } - ) + ).eraseToAny() + + case let .unverifiedWallets(walletId): + AlertBuilder( + title: "Can't Enable Wipe Data PIN", + message: """ + You have wallets that have not been backed up. Please back up your wallets before enabling the Wipe Data PIN.\ + If you wipe the data without having a back up of your wallet, you will lose the bitcoin in that wallet. + """, + actions: { + Button("Go To Wallet") { + try? app.rust.selectWallet(id: walletId) + } + + Button("Cancel", role: .cancel) { alertState = .none } + } + ).eraseToAny() case .confirmEnableWipeMePin: - return AlertBuilder( + AlertBuilder( title: "Are you sure?", message: - """ + """ - Enabling the Wipe Data PIN will let you chose a PIN that if entered will wipe all Cove wallet data on this device. + Enabling the Wipe Data PIN will let you chose a PIN that if entered will wipe all Cove wallet data on this device. - If you wipe the data without having a back up of your wallet, you will lose the bitcoin in that wallet. + If you wipe the data without having a back up of your wallet, you will lose the bitcoin in that wallet. - Please make sure you have a backup of your wallet before enabling this. - - Note: Enabling the Wipe Data PIN will disable FaceID auth if its enabled. - """, + Please make sure you have a backup of your wallet before enabling this. + """, actions: { Button("Yes, Enable Wipe Data PIN") { - // app.dispatch(action: .enableWipeMePin) - dismiss() - } - Button("Cancel", role: .cancel) { alertState = .none + sheetState = .init(.enableWipeDataPin) } + Button("Cancel", role: .cancel) { alertState = .none } } + ).eraseToAny() + + case .notePinRequired: + AlertBuilder( + title: "PIN is required", + message: "Setting a PIN is required to have a wipe data PIN", + actions: { Button("OK") { alertState = .none } } + ).eraseToAny() + + case .noteFaceIdDisabling: + AlertBuilder( + title: "Disable FaceID Unlock?", + message: """ + + Enabling the wipe data PIN will disable FaceID unlock for Cove. + + Going forward, you will have to use your PIN to unlock Cove. + """, + actions: { + Button("Disable FaceID", role: .destructive) { + auth.dispatch(action: .disableBiometric) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.350) { + alertState = .init(.confirmEnableWipeMePin) + } + } + Button("Cancel", role: .cancel) { alertState = .none } + } + ).eraseToAny() + + case .noteNoFaceIdWhenWipeMePin: + AlertBuilder( + title: "Can't do that", + message: "You can't have both Wipe Data PIN and FaceID active at the same time", + actions: { + Button("Cancel", role: .cancel) { alertState = .none } + Button("Disable Wipe Data PIN", role: .destructive) { + auth.dispatch(action: .disableWipeDataPin) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.350) { + auth.dispatch(action: .enableBiometric) + } + } + } + ).eraseToAny() + + case let .wipeDataSetPinError(error): + AlertBuilder( + title: "Something went wrong!", + message: error, + actions: { Button("OK") { alertState = .none } } ) + .eraseToAny() } } @@ -295,6 +398,7 @@ struct SettingsScreen: View { backAction: { sheetState = .none }, onUnlock: { _ in auth.dispatch(action: .disablePin) + auth.dispatch(action: .disableWipeDataPin) sheetState = .none } ) @@ -303,12 +407,22 @@ struct SettingsScreen: View { ChangePinView( isPinCorrect: auth.checkPin, backAction: { sheetState = .none }, - onComplete: setPin + onComplete: { pin in + sheetState = .none + + if auth.checkWipeDataPin(pin) { + return alertState = .init( + .wipeDataSetPinError("Can't update PIN because its the same as your wipe data PIN") + ) + } + + setPin(pin) + } ) case .disableBiometric: LockView( - lockType: auth.authType, + lockType: auth.type, isPinCorrect: auth.checkPin, onUnlock: { _ in auth.dispatch(action: .disableBiometric) @@ -329,6 +443,24 @@ struct SettingsScreen: View { backAction: { sheetState = .none }, content: { EmptyView() } ) + + case .enableWipeDataPin: + WipeDataPinView( + onComplete: setWipeDataPin, + backAction: { + sheetState = .none + } + ) + } + } + + func setWipeDataPin(_ pin: String) { + sheetState = .none + + do { try auth.rust.setWipeDataPin(pin: pin) } + catch { + let error = error as! AuthManagerError + alertState = .init(.wipeDataSetPinError(error.describe)) } } } diff --git a/ios/Cove/SettingsScreen/WipeDataPinView.swift b/ios/Cove/SettingsScreen/WipeDataPinView.swift new file mode 100644 index 0000000..55a7a2e --- /dev/null +++ b/ios/Cove/SettingsScreen/WipeDataPinView.swift @@ -0,0 +1,54 @@ +// +// WipeDataPinView.swift +// Cove +// +// Created by Praveen Perera on 12/17/24. +// + +import SwiftUI + +private enum PinState { + case new + case confirm(String) +} + +struct WipeDataPinView: View { + /// args + var onComplete: (String) -> Void + var backAction: () -> Void + + /// private + @State private var pinState: PinState = .new + + var body: some View { + Group { + switch pinState { + case .new: + NumberPadPinView( + title: "Enter Wipe Data PIN", + isPinCorrect: { _ in true }, + showPin: false, + backAction: backAction, + onUnlock: { enteredPin in + withAnimation { + pinState = .confirm(enteredPin) + } + } + ) + case let .confirm(pinToConfirm): + NumberPadPinView( + title: "Confirm Wipe Data PIN", + isPinCorrect: { $0 == pinToConfirm }, + showPin: false, + backAction: backAction, + onUnlock: onComplete + ) + } + } + } +} + +#Preview { + WipeDataPinView(onComplete: { _ in }, backAction: {}) + .environment(AuthManager()) +} diff --git a/ios/Cove/Views/NumberPadPinView.swift b/ios/Cove/Views/NumberPadPinView.swift index dc4163f..e4d33a1 100644 --- a/ios/Cove/Views/NumberPadPinView.swift +++ b/ios/Cove/Views/NumberPadPinView.swift @@ -179,13 +179,15 @@ struct NumberPadPinView: View { .padding(.vertical) .onChange(of: pin) { _, newValue in if newValue.count == pinLength { + let pin = newValue + /// Validate Pin if isPinCorrect(pin) { withAnimation(.snappy, completionCriteria: .logicallyComplete) { lockState = .unlocked } completion: { onUnlock(pin) - pin = "" + self.pin = "" } } else { animateField.toggle() diff --git a/rust/src/cove_nfc/parser.rs b/rust/src/cove_nfc/parser.rs index 0d64b79..dd55a3a 100644 --- a/rust/src/cove_nfc/parser.rs +++ b/rust/src/cove_nfc/parser.rs @@ -434,7 +434,6 @@ mod tests { let mut descriptor = *DESCRIPTOR; let message = parse_ndef_message(&mut descriptor).unwrap(); - println!("{message:?}"); assert_eq!(message.len(), 1); let record = &message[0]; diff --git a/rust/src/manager/auth.rs b/rust/src/manager/auth.rs index 1e3f7de..46a9e81 100644 --- a/rust/src/manager/auth.rs +++ b/rust/src/manager/auth.rs @@ -2,14 +2,13 @@ use std::sync::{Arc, LazyLock}; use crossbeam::channel::{Receiver, Sender}; use macros::impl_default_for; -use once_cell::sync::OnceCell; use parking_lot::RwLock; use tap::TapFallible as _; use tracing::{debug, error}; use crate::{ auth::{AuthPin, AuthType}, - database::Database, + database::{self, Database}, }; type Message = AuthManagerReconcileMessage; @@ -19,6 +18,7 @@ pub static AUTH_MANAGER: LazyLock> = LazyLock::new(RustAuth #[derive(Debug, Clone, Hash, Eq, PartialEq, uniffi::Enum)] pub enum AuthManagerReconcileMessage { AuthTypeChanged(AuthType), + WipeDataPinChanged, } #[uniffi::export(callback_interface)] @@ -27,8 +27,6 @@ pub trait AuthManagerReconciler: Send + Sync + std::fmt::Debug + 'static { fn reconcile(&self, message: AuthManagerReconcileMessage); } -impl_default_for!(RustAuthManager); - #[derive(Clone, Debug, uniffi::Object)] pub struct RustAuthManager { #[allow(dead_code)] @@ -47,12 +45,43 @@ pub enum AuthManagerAction { UpdateAuthType(AuthType), EnableBiometric, DisableBiometric, - SetPin(String), DisablePin, - SetWipeDataPin(String), + SetPin(String), DisableWipeDataPin, } +type Result = std::result::Result; +type Error = AuthManagerError; + +#[derive(Debug, Clone, Hash, Eq, PartialEq, uniffi::Error, thiserror::Error)] +pub enum AuthManagerError { + #[error("Unable to set the wipe data PIN, because {0}")] + WipeDataSet(#[from] WipeDataPinError), + + #[error("There was a database error: {0}")] + DatabaseError(#[from] database::Error), +} + +#[uniffi::export] +fn auth_manager_error_to_string(error: AuthManagerError) -> String { + error.to_string() +} + +#[derive(Debug, Clone, Hash, Eq, PartialEq, uniffi::Error, thiserror::Error)] +pub enum WipeDataPinError { + /// Unable to set wipe data pin, because PIN is not enabled + #[error("PIN is not enabled")] + PinNotEnabled, + + /// Unable to set wipe data pin, because its the same as the current pin + #[error("its is the same as the current pin")] + SameAsCurrentPin, + + /// Unable to set wipe data pin, because biometrics is enabled + #[error("biometrics is enabled")] + BiometricsEnabled, +} + impl RustAuthManager { fn init() -> Arc { let (sender, receiver) = crossbeam::channel::bounded(1000); @@ -73,7 +102,6 @@ impl RustAuthManager { AUTH_MANAGER.clone() } - #[uniffi::method] pub fn listen_for_updates(&self, reconciler: Box) { let reconcile_receiver = self.reconcile_receiver.clone(); @@ -96,8 +124,45 @@ impl RustAuthManager { .unwrap_or_default() } - /// Check if the PIN matches the wipe data pin + /// Check if the wipe data pin is enabled + pub fn is_wipe_data_pin_enabled(&self) -> bool { + let pin = Database::global() + .global_config + .wipe_data_pin() + .unwrap_or_default(); + + !pin.is_empty() + } + + /// Set the wipe data pin + pub fn set_wipe_data_pin(&self, pin: String) -> Result<()> { + let auth_type = self.auth_type(); + + if auth_type == AuthType::None { + return Err(WipeDataPinError::PinNotEnabled.into()); + } + + if auth_type == AuthType::Biometric || auth_type == AuthType::Both { + return Err(WipeDataPinError::BiometricsEnabled.into()); + } + + if AuthPin::new().check(&pin) { + return Err(WipeDataPinError::SameAsCurrentPin.into()); + } + + // set the pin + Database::global().global_config.set_wipe_data_pin(pin)?; + self.send(Message::WipeDataPinChanged); + + Ok(()) + } + + /// Check to see if the passed in PIN matches the wipe data PIN pub fn check_wipe_data_pin(&self, pin: String) -> bool { + if pin.is_empty() { + return false; + } + let wipe_data_pin = Database::global() .global_config .wipe_data_pin() @@ -111,6 +176,8 @@ impl RustAuthManager { if let Err(error) = Database::global().global_config.delete_wipe_data_pin() { error!("unable to delete wipe data pin: {error:?}"); } + + self.send(Message::WipeDataPinChanged); } // private @@ -188,19 +255,7 @@ impl RustAuthManager { } } - Action::SetWipeDataPin(pin) => { - debug!("set wipe data pin"); - if let Err(error) = Database::global().global_config.set_wipe_data_pin(pin) { - error!("unable to set wipe data pin: {error:?}"); - } - } - - Action::DisableWipeDataPin => { - debug!("disable wipe data pin"); - if let Err(error) = Database::global().global_config.delete_wipe_data_pin() { - error!("unable to delete wipe data pin: {error:?}"); - } - } + Action::DisableWipeDataPin => self.delete_wipe_data_pin(), } } } From b1372c20a09f6f034765e439583cd170448460b0 Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Tue, 17 Dec 2024 16:09:34 -0600 Subject: [PATCH 09/10] Add ability to wipe database on wipe data pin --- ios/Cove/AppManager.swift | 10 ++++- rust/src/app.rs | 31 +++++++++++++++ rust/src/auth.rs | 24 +++++++----- rust/src/database.rs | 80 +++++++++++++++++++++++---------------- 4 files changed, 101 insertions(+), 44 deletions(-) diff --git a/ios/Cove/AppManager.swift b/ios/Cove/AppManager.swift index dde1325..4eeb91c 100644 --- a/ios/Cove/AppManager.swift +++ b/ios/Cove/AppManager.swift @@ -21,7 +21,6 @@ import SwiftUI var prices: PriceResponse? var fees: FeeResponse? - // changed when route is reset, to clear lifecycle view state var routeId = UUID() @@ -74,6 +73,15 @@ import SwiftUI walletManager = vm } + /// Reset the manager state + public func reset() { + rust = FfiApp() + database = Database() + + let state = rust.state() + router = state.router + } + var currentRoute: Route { router.routes.last ?? router.default } diff --git a/rust/src/app.rs b/rust/src/app.rs index 4b759c0..4f9202a 100644 --- a/rust/src/app.rs +++ b/rust/src/app.rs @@ -9,6 +9,7 @@ use crate::{ color_scheme::ColorSchemeSelection, database::{error::DatabaseError, Database}, fiat::client::{PriceResponse, FIAT_CLIENT}, + keychain::Keychain, network::Network, node::Node, router::{Route, RouteFactory, Router}, @@ -359,6 +360,36 @@ impl FfiApp { Ok(fees) } + /// DANGER: This will wipe all wallet data on this device + pub fn dangerous_wipe_all_data(&self) { + let database = Database::global(); + let keychain = Keychain::global(); + + let wallets = Database::global().wallets().all().unwrap_or_default(); + + for wallet in wallets { + let wallet_id = &wallet.id; + + // delete the wallet from the database + if let Err(error) = database.wallets.delete(wallet_id) { + error!("Unable to delete wallet from database: {error}"); + } + + // delete the secret key from the keychain + keychain.delete_wallet_key(wallet_id); + + // delete the xpub from keychain + keychain.delete_wallet_xpub(wallet_id); + + // delete the wallet persisted bdk data + if let Err(error) = crate::wallet::delete_data_path(&wallet_id) { + error!("Unable to delete wallet persisted bdk data: {error}"); + } + } + + database.dangerous_reset_all_data(); + } + /// Frontend calls this method to send events to the rust application logic pub fn dispatch(&self, action: AppAction) { self.inner().handle_action(action); diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 68c2ee2..f258f83 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -86,10 +86,19 @@ impl AuthPin { .map_err(AuthError::DatabaseSaveError) } - pub fn verify(&self, pin: String, hashed_pin: String) -> Result<()> { + pub fn check(&self, pin: &str) -> bool { + let hashed_pin = Database::global() + .global_config + .hashed_pin_code() + .unwrap_or_default(); + + self.verify(&pin, &hashed_pin).is_ok() + } + + pub fn verify(&self, pin: &str, hashed_pin: &str) -> Result<()> { let argon2 = Argon2::default(); - let parsed_hash = PasswordHash::new(&hashed_pin).map_err(|error| { + let parsed_hash = PasswordHash::new(hashed_pin).map_err(|error| { AuthError::ParseHashedPinError(format!("unable to parse hashed pin: {error}")) })?; @@ -106,14 +115,9 @@ impl AuthPin { Self {} } - #[uniffi::method] - pub fn check(&self, pin: String) -> bool { - let hashed_pin = Database::global() - .global_config - .hashed_pin_code() - .unwrap_or_default(); - - self.verify(pin, hashed_pin).is_ok() + #[uniffi::method(name = "check")] + pub fn _check(&self, pin: String) -> bool { + self.check(&pin) } } diff --git a/rust/src/database.rs b/rust/src/database.rs index c897409..2c568d2 100644 --- a/rust/src/database.rs +++ b/rust/src/database.rs @@ -12,6 +12,7 @@ pub mod wallet_data; use std::{path::PathBuf, sync::Arc}; +use arc_swap::ArcSwap; use global_cache::GlobalCacheTable; use global_config::GlobalConfigTable; use global_flag::GlobalFlagTable; @@ -23,7 +24,7 @@ use tracing::{error, info}; use crate::consts::ROOT_DATA_DIR; -pub static DATABASE: OnceCell = OnceCell::new(); +pub static DATABASE: OnceCell> = OnceCell::new(); pub type Error = error::DatabaseError; @@ -37,16 +38,10 @@ pub struct Database { pub unsigned_transactions: UnsignedTransactionsTable, } -impl Default for Database { - fn default() -> Self { - Self::new() - } -} - #[uniffi::export] impl Database { #[uniffi::constructor(name = "new")] - pub fn new() -> Self { + pub fn new() -> Arc { Self::global().clone() } @@ -61,34 +56,53 @@ impl Database { pub fn unsigned_transactions(&self) -> UnsignedTransactionsTable { self.unsigned_transactions.clone() } + + pub fn dangerous_reset_all_data(&self) { + let current_location = ROOT_DATA_DIR.join("cove.db"); + + if let Err(error) = std::fs::remove_file(¤t_location) { + error!("unable to delete database: {error}"); + return; + } + + DATABASE + .get() + .expect("database not initialized") + .swap(Self::init()); + } } impl Database { - pub fn global() -> &'static Database { - DATABASE.get_or_init(|| { - let db = get_or_create_database(); - - let write_txn = db.begin_write().expect("failed to begin write transaction"); - let db = Arc::new(db); - - let wallets = WalletsTable::new(db.clone(), &write_txn); - let global_flag = GlobalFlagTable::new(db.clone(), &write_txn); - let global_config = GlobalConfigTable::new(db.clone(), &write_txn); - let global_cache = GlobalCacheTable::new(db.clone(), &write_txn); - let unsigned_transactions = UnsignedTransactionsTable::new(db.clone(), &write_txn); - - write_txn - .commit() - .expect("failed to commit write transaction"); - - Database { - wallets, - global_flag, - global_config, - global_cache, - unsigned_transactions, - } - }) + pub fn global() -> Arc { + let db = DATABASE.get_or_init(|| ArcSwap::new(Self::init())).load(); + Arc::clone(&db) + } + + fn init() -> Arc { + let db = get_or_create_database(); + + let write_txn = db.begin_write().expect("failed to begin write transaction"); + let db = Arc::new(db); + + let wallets = WalletsTable::new(db.clone(), &write_txn); + let global_flag = GlobalFlagTable::new(db.clone(), &write_txn); + let global_config = GlobalConfigTable::new(db.clone(), &write_txn); + let global_cache = GlobalCacheTable::new(db.clone(), &write_txn); + let unsigned_transactions = UnsignedTransactionsTable::new(db.clone(), &write_txn); + + write_txn + .commit() + .expect("failed to commit write transaction"); + + let db = Database { + wallets, + global_flag, + global_config, + global_cache, + unsigned_transactions, + }; + + Arc::new(db) } } From ee499e9ebe5ba129fdd4712cd1e4308a32732a9d Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Tue, 17 Dec 2024 16:12:30 -0600 Subject: [PATCH 10/10] Fix clippy lints --- rust/src/app.rs | 2 +- rust/src/auth.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rust/src/app.rs b/rust/src/app.rs index 4f9202a..6a1efc6 100644 --- a/rust/src/app.rs +++ b/rust/src/app.rs @@ -382,7 +382,7 @@ impl FfiApp { keychain.delete_wallet_xpub(wallet_id); // delete the wallet persisted bdk data - if let Err(error) = crate::wallet::delete_data_path(&wallet_id) { + if let Err(error) = crate::wallet::delete_data_path(wallet_id) { error!("Unable to delete wallet persisted bdk data: {error}"); } } diff --git a/rust/src/auth.rs b/rust/src/auth.rs index f258f83..1816af5 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -92,7 +92,7 @@ impl AuthPin { .hashed_pin_code() .unwrap_or_default(); - self.verify(&pin, &hashed_pin).is_ok() + self.verify(pin, &hashed_pin).is_ok() } pub fn verify(&self, pin: &str, hashed_pin: &str) -> Result<()> { @@ -136,7 +136,7 @@ mod tests { fn test_verify_pin() { let auth = AuthPin::new(); let hashed = auth.hash("123456".to_string()).unwrap(); - let result = auth.verify("123456".to_string(), hashed); + let result = auth.verify("123456", &hashed); assert!(result.is_ok()); } }