From fd3bf075be9e45647daf8e045145ed7f99c8feb4 Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Tue, 10 Dec 2024 14:45:52 -0600 Subject: [PATCH 01/16] Complete PIN unlock view --- ios/Cove/Cove.entitlements | 7 + ios/Cove/CoveApp.swift | 20 ++- ios/Cove/Views/LockView.swift | 297 ++++++++++++++++++++++++++++++++++ 3 files changed, 320 insertions(+), 4 deletions(-) create mode 100644 ios/Cove/Views/LockView.swift diff --git a/ios/Cove/Cove.entitlements b/ios/Cove/Cove.entitlements index 2bb4dee1..a88cb249 100644 --- a/ios/Cove/Cove.entitlements +++ b/ios/Cove/Cove.entitlements @@ -2,9 +2,16 @@ + com.apple.security.app-sandbox + + + com.apple.security.files.user-selected.read-only + + com.apple.developer.nfc.readersession.formats TAG + diff --git a/ios/Cove/CoveApp.swift b/ios/Cove/CoveApp.swift index eb0a5267..c889ba6d 100644 --- a/ios/Cove/CoveApp.swift +++ b/ios/Cove/CoveApp.swift @@ -43,7 +43,8 @@ public extension EnvironmentValues { @main struct CoveApp: App { - @Environment(\.colorScheme) var colorScheme + @Environment(\.colorScheme) private var colorScheme + @Environment(\.scenePhase) private var phase @State var manager: AppManager @State var id = UUID() @@ -459,11 +460,22 @@ struct CoveApp: App { ) .task { await manager.rust.initOnStart() - await MainActor.run { - manager.asyncRuntimeReady = true - } + await MainActor.run { manager.asyncRuntimeReady = true } } .onOpenURL(perform: handleFileOpen) + .onChange(of: phase) { oldPhase, newPhase in + Log.debug("[SCENE PHASE]: \(oldPhase) --> \(newPhase)") + + /// TODO: only do this if PIN and/or Biometric is enabledA + if newPhase == .background { + UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .flatMap(\.windows) + .forEach { window in + window.rootViewController?.dismiss(animated: false) + } + } + } } } } diff --git a/ios/Cove/Views/LockView.swift b/ios/Cove/Views/LockView.swift new file mode 100644 index 00000000..2930ddee --- /dev/null +++ b/ios/Cove/Views/LockView.swift @@ -0,0 +1,297 @@ +// +// LockView.swift +// Cove +// +// Created by Praveen Perera on 12/10/24. +// + +import LocalAuthentication +import SwiftUI + +struct LockView: View { + /// Args: Lock Properties + var lockType: LockType + var isPinCorrect: (String) -> Bool + var isEnabled: Bool + var lockWhenBackground: Bool = true + var bioMetricUnlockMessage: String = "Unlock your wallet" + @ViewBuilder var content: Content + + /// View Properties + @State private var pin: String = "" + @State private var animateField: Bool = false + @State private var isUnlocked: Bool = false + @State private var noBiometricAccess: Bool = false + + /// private consts + private let pinLength = 6 + + /// Scene Phase + @Environment(\.scenePhase) private var phase + + var body: some View { + GeometryReader { + let size = $0.size + + content + .frame(width: size.width, height: size.height) + + if isEnabled, !isUnlocked { + ZStack { + Rectangle() + .fill(.black) + .ignoresSafeArea() + + if (lockType == .both && !noBiometricAccess) || lockType == .biometric { + Group { + if noBiometricAccess { + Text("Enable biometric authentication in Settings to unlock the view.") + .font(.callout) + .multilineTextAlignment(.center) + .padding(50) + } else { + /// Bio Metric / Pin Unlock + VStack(spacing: 12) { + VStack(spacing: 6) { + Image(systemName: "faceid") + .font(.largeTitle) + + Text("Tap to Unlock") + .font(.caption2) + .foregroundStyle(.gray) + } + .frame(width: 100, height: 100) + .background(.ultraThinMaterial, in: .rect(cornerRadius: 10)) + .contentShape(.rect) + .onTapGesture { + unlockView() + } + + if lockType == .both { + Text("Enter Pin") + .frame(width: 100, height: 40) + .background(.ultraThinMaterial, + in: .rect(cornerRadius: 10)) + .contentShape(.rect) + .onTapGesture { + noBiometricAccess = true + } + } + } + } + } + } else { + /// Custom Number Pad to type View Lock Pin + NumberPadPinView() + } + } + .environment(\.colorScheme, .dark) + .transition(.offset(y: size.height + 100)) + } + } + .onChange(of: isEnabled, initial: true) { _, newValue in + if newValue { + unlockView() + } + } + /// Locking When App Goes Background + .onChange(of: phase) { _, newValue in + if newValue != .active, lockWhenBackground { + isUnlocked = false + pin = "" + } + } + } + + private func bioMetricUnlock() async throws -> Bool { + /// Lock Context + let context = LAContext() + + return try await context.evaluatePolicy( + .deviceOwnerAuthenticationWithBiometrics, + localizedReason: bioMetricUnlockMessage) + } + + private func unlockView() { + /// Checking and Unlocking View + Task { + guard isBiometricAvailable, lockType != .number else { + /// No Bio Metric Permission || Lock Type Must be Set as Keypad + /// Updating Biometric Status + await MainActor.run { noBiometricAccess = !isBiometricAvailable } + return + } + + /// Requesting Biometric Unlock + if await (try? bioMetricUnlock()) ?? false { + await MainActor.run { + withAnimation( + .snappy, + completionCriteria: .logicallyComplete) + { + isUnlocked = true + } completion: { pin = "" } + } + } + } + } + + private var isBiometricAvailable: Bool { + /// Lock Context + let context = LAContext() + return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) + } + + /// Numberpad Pin View + @ViewBuilder + private func NumberPadPinView() -> some View { + VStack(spacing: 15) { + Text("Enter Pin") + .font(.title.bold()) + .frame(maxWidth: .infinity) + .overlay(alignment: .leading) { + /// Back button only for Both Lock Type + if lockType == .both, isBiometricAvailable { + Button(action: { + pin = "" + noBiometricAccess = false + }, label: { + Image(systemName: "arrow.left") + .font(.title3) + .contentShape(.rect) + }) + .tint(.white) + .padding(.leading) + } + } + + /// Adding Wiggling Animation for Wrong Password With Keyframe Animator + HStack(spacing: 10) { + ForEach(0 ..< pinLength, id: \.self) { index in + RoundedRectangle(cornerRadius: 10) + .frame(width: 40, height: 45) + /// Showing Pin at each box with the help of Index + .overlay { + /// Safe Check + if pin.count > index { + let index = pin.index(pin.startIndex, offsetBy: index) + let string = String(pin[index]) + + Text(string) + .font(.title.bold()) + .foregroundStyle(.black) + } + } + } + } + .keyframeAnimator( + initialValue: CGFloat.zero, + trigger: animateField, + content: { content, value in + content + .offset(x: value) + }, + keyframes: { _ in + KeyframeTrack { + CubicKeyframe(30, duration: 0.07) + CubicKeyframe(-30, duration: 0.07) + CubicKeyframe(20, duration: 0.07) + CubicKeyframe(-20, duration: 0.07) + CubicKeyframe(10, duration: 0.07) + CubicKeyframe(-10, duration: 0.07) + CubicKeyframe(0, duration: 0.07) + } + }) + .padding(.top, 15) + .frame(maxHeight: .infinity) + + /// Custom Number Pad + GeometryReader { _ in + LazyVGrid(columns: Array(repeating: GridItem(), count: 3), content: { + ForEach(1 ... 9, id: \.self) { number in + Button(action: { + guard pin.count < pinLength else { return } + pin.append(String(number)) + }, label: { + Text(String(number)) + .font(.title) + .frame(maxWidth: .infinity) + .padding(.vertical, 20) + .contentShape(.rect) + }) + .tint(.white) + } + + /// 0 and Back Button + Button(action: { + if !pin.isEmpty { pin.removeLast() } + }, label: { + Image(systemName: "delete.backward") + .font(.title) + .frame(maxWidth: .infinity) + .padding(.vertical, 20) + .contentShape(.rect) + }) + .tint(.white) + + Button(action: { + guard pin.count < pinLength else { return } + pin.append("0") + }, label: { + Text("0") + .font(.title) + .frame(maxWidth: .infinity) + .padding(.vertical, 20) + .contentShape(.rect) + }) + .tint(.white) + }) + .frame(maxHeight: .infinity, alignment: .bottom) + } + .onChange(of: pin) { _, newValue in + if newValue.count == pinLength { + /// Validate Pin + if isPinCorrect(pin) { + withAnimation(.snappy, completionCriteria: .logicallyComplete) { + isUnlocked = true + } completion: { + pin = "" + noBiometricAccess = !isBiometricAvailable + } + } else { + pin = "" + animateField.toggle() + } + } + } + } + .padding() + .environment(\.colorScheme, .dark) + } +} + +/// Lock Type +enum LockType { + case biometric + case number + case both + + var description: String { + switch self { + case .biometric: + "Bio Metric Auth" + case .number: + "Custom Number Lock" + case .both: + "First preference will be biometric, and if it's not available, it will go for number lock." + } + } +} + +#Preview { + LockView(lockType: .both, isPinCorrect: { $0 == "111111" }, isEnabled: true) { + VStack { + Text("Hello World") + } + } +} From 2bea68c835cf392f418f7c97c696032380c21d9a Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Tue, 10 Dec 2024 15:11:04 -0600 Subject: [PATCH 02/16] Enable saving `LockType` in our database --- ios/Cove/Cove.swift | 109 +++++++++++++++++++++++++++++ ios/Cove/Views/LockView.swift | 20 +----- rust/src/database/global_config.rs | 25 ++++++- rust/src/lib.rs | 1 + rust/src/lock.rs | 24 +++++++ 5 files changed, 159 insertions(+), 20 deletions(-) create mode 100644 rust/src/lock.rs diff --git a/ios/Cove/Cove.swift b/ios/Cove/Cove.swift index b5df8500..204f00f0 100644 --- a/ios/Cove/Cove.swift +++ b/ios/Cove/Cove.swift @@ -4876,6 +4876,8 @@ public protocol GlobalConfigTableProtocol : AnyObject { func get(key: GlobalConfigKey) throws -> String? + func getLockType() throws -> LockType + func selectWallet(id: WalletId) throws func selectedNetwork() -> Network @@ -4888,6 +4890,8 @@ public protocol GlobalConfigTableProtocol : AnyObject { func setColorScheme(colorScheme: ColorSchemeSelection) throws + func setLockType(lockType: LockType) throws + func setSelectedNetwork(network: Network) throws func setSelectedNode(node: Node) throws @@ -4972,6 +4976,13 @@ open func get(key: GlobalConfigKey)throws -> String? { }) } +open func getLockType()throws -> LockType { + return try FfiConverterTypeLockType.lift(try rustCallWithError(FfiConverterTypeDatabaseError.lift) { + uniffi_cove_fn_method_globalconfigtable_get_lock_type(self.uniffiClonePointer(),$0 + ) +}) +} + open func selectWallet(id: WalletId)throws {try rustCallWithError(FfiConverterTypeDatabaseError.lift) { uniffi_cove_fn_method_globalconfigtable_select_wallet(self.uniffiClonePointer(), FfiConverterTypeWalletId.lower(id),$0 @@ -5015,6 +5026,13 @@ open func setColorScheme(colorScheme: ColorSchemeSelection)throws {try rustCal } } +open func setLockType(lockType: LockType)throws {try rustCallWithError(FfiConverterTypeDatabaseError.lift) { + uniffi_cove_fn_method_globalconfigtable_set_lock_type(self.uniffiClonePointer(), + FfiConverterTypeLockType.lower(lockType),$0 + ) +} +} + open func setSelectedNetwork(network: Network)throws {try rustCallWithError(FfiConverterTypeDatabaseError.lift) { uniffi_cove_fn_method_globalconfigtable_set_selected_network(self.uniffiClonePointer(), FfiConverterTypeNetwork.lower(network),$0 @@ -14122,6 +14140,7 @@ public enum GlobalConfigKey { case selectedNode(Network ) case colorScheme + case lockType } @@ -14144,6 +14163,8 @@ public struct FfiConverterTypeGlobalConfigKey: FfiConverterRustBuffer { case 4: return .colorScheme + case 5: return .lockType + default: throw UniffiInternalError.unexpectedEnumCase } } @@ -14168,6 +14189,10 @@ public struct FfiConverterTypeGlobalConfigKey: FfiConverterRustBuffer { case .colorScheme: writeInt(&buf, Int32(4)) + + case .lockType: + writeInt(&buf, Int32(5)) + } } } @@ -14845,6 +14870,84 @@ extension KeychainError: 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 LockType { + + case pin + case biometric + case both + case none +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeLockType: FfiConverterRustBuffer { + typealias SwiftType = LockType + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> LockType { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .pin + + case 2: return .biometric + + case 3: return .both + + case 4: return .none + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: LockType, into buf: inout [UInt8]) { + switch value { + + + case .pin: + writeInt(&buf, Int32(1)) + + + case .biometric: + writeInt(&buf, Int32(2)) + + + case .both: + writeInt(&buf, Int32(3)) + + + case .none: + writeInt(&buf, Int32(4)) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeLockType_lift(_ buf: RustBuffer) throws -> LockType { + return try FfiConverterTypeLockType.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeLockType_lower(_ value: LockType) -> RustBuffer { + return FfiConverterTypeLockType.lower(value) +} + + + +extension LockType: Equatable, Hashable {} + + + public enum MnemonicError { @@ -21420,6 +21523,9 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_globalconfigtable_get() != 52128) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_method_globalconfigtable_get_lock_type() != 16233) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_method_globalconfigtable_select_wallet() != 52001) { return InitializationResult.apiChecksumMismatch } @@ -21438,6 +21544,9 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_globalconfigtable_set_color_scheme() != 24086) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_method_globalconfigtable_set_lock_type() != 26312) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_method_globalconfigtable_set_selected_network() != 34312) { return InitializationResult.apiChecksumMismatch } diff --git a/ios/Cove/Views/LockView.swift b/ios/Cove/Views/LockView.swift index 2930ddee..72537254 100644 --- a/ios/Cove/Views/LockView.swift +++ b/ios/Cove/Views/LockView.swift @@ -115,7 +115,7 @@ struct LockView: View { private func unlockView() { /// Checking and Unlocking View Task { - guard isBiometricAvailable, lockType != .number else { + guard isBiometricAvailable, lockType != .pin else { /// No Bio Metric Permission || Lock Type Must be Set as Keypad /// Updating Biometric Status await MainActor.run { noBiometricAccess = !isBiometricAvailable } @@ -270,24 +270,6 @@ struct LockView: View { } } -/// Lock Type -enum LockType { - case biometric - case number - case both - - var description: String { - switch self { - case .biometric: - "Bio Metric Auth" - case .number: - "Custom Number Lock" - case .both: - "First preference will be biometric, and if it's not available, it will go for number lock." - } - } -} - #Preview { LockView(lockType: .both, isPinCorrect: { $0 == "111111" }, isEnabled: true) { VStack { diff --git a/rust/src/database/global_config.rs b/rust/src/database/global_config.rs index f936a5bd..fb379538 100644 --- a/rust/src/database/global_config.rs +++ b/rust/src/database/global_config.rs @@ -1,10 +1,11 @@ -use std::sync::Arc; +use std::{str::FromStr as _, sync::Arc}; use redb::TableDefinition; use crate::{ app::reconcile::{Update, Updater}, color_scheme::ColorSchemeSelection, + lock::LockType, network::Network, node::Node, wallet::metadata::WalletId, @@ -20,6 +21,7 @@ pub enum GlobalConfigKey { SelectedNetwork, SelectedNode(Network), ColorScheme, + LockType, } impl From for &'static str { @@ -30,6 +32,7 @@ impl From for &'static str { GlobalConfigKey::SelectedNode(Network::Bitcoin) => "selected_node_bitcoin", GlobalConfigKey::SelectedNode(Network::Testnet) => "selected_node_testnet", GlobalConfigKey::ColorScheme => "color_scheme", + GlobalConfigKey::LockType => "lock_type", } } } @@ -147,6 +150,26 @@ impl GlobalConfigTable { Ok(()) } + pub fn get_lock_type(&self) -> Result { + let Some(lock_type) = self + .get(GlobalConfigKey::LockType) + .map_err(|error| Error::DatabaseAccess(error.to_string()))? + else { + return Ok(LockType::None); + }; + + let lock_type = LockType::from_str(&lock_type) + .map_err(|_| GlobalConfigTableError::Read("unable to parse lock type".to_string()))?; + + Ok(lock_type) + } + + pub fn set_lock_type(&self, lock_type: LockType) -> Result<(), Error> { + let lock_type = lock_type.to_string(); + self.set(GlobalConfigKey::LockType, lock_type)?; + Ok(()) + } + pub fn get(&self, key: GlobalConfigKey) -> Result, Error> { let read_txn = self .db diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 3ec20136..403cee8e 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -19,6 +19,7 @@ pub(crate) mod hardware_export; pub(crate) mod header_icon_presenter; pub(crate) mod keychain; pub(crate) mod keys; +pub(crate) mod lock; pub(crate) mod manager; pub(crate) mod mnemonic; pub(crate) mod multi_format; diff --git a/rust/src/lock.rs b/rust/src/lock.rs new file mode 100644 index 00000000..a02d4fb6 --- /dev/null +++ b/rust/src/lock.rs @@ -0,0 +1,24 @@ +use derive_more::{Display, FromStr}; +use serde::{Deserialize, Serialize}; + +#[derive( + Debug, + Clone, + Hash, + Eq, + PartialEq, + uniffi::Enum, + Serialize, + Deserialize, + Default, + Display, + FromStr, +)] +pub enum LockType { + Pin, + Biometric, + Both, + + #[default] + None, +} From 65403fd6fc8ebc4c0e8a91042adfa8002915d75e Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Tue, 10 Dec 2024 16:23:20 -0600 Subject: [PATCH 03/16] Add ability to toggle auth type in settings --- ios/Cove/AppManager.swift | 8 + ios/Cove/Cove.swift | 275 ++++++++++++++-------- ios/Cove/CoveApp.swift | 4 +- ios/Cove/HomeScreens/SettingsScreen.swift | 41 +++- ios/Cove/Info.plist | 5 + ios/Cove/Views/LockView.swift | 50 ++-- rust/src/app.rs | 96 ++++++-- rust/src/app/reconcile.rs | 7 +- rust/src/{lock.rs => auth.rs} | 3 +- rust/src/database/global_config.rs | 24 +- rust/src/lib.rs | 2 +- 11 files changed, 353 insertions(+), 162 deletions(-) rename rust/src/{lock.rs => auth.rs} (90%) diff --git a/ios/Cove/AppManager.swift b/ios/Cove/AppManager.swift index 932fbf8b..a3ec0a85 100644 --- a/ios/Cove/AppManager.swift +++ b/ios/Cove/AppManager.swift @@ -15,6 +15,7 @@ import SwiftUI var colorSchemeSelection = Database().globalConfig().colorScheme() var selectedNode = Database().globalConfig().selectedNode() + var authType = try? Database().globalConfig().authType() ?? AuthType.none var nfcReader = NFCReader() @@ -73,6 +74,10 @@ import SwiftUI walletManager = vm } + var isAuthEnabled: Bool { + authType != AuthType.none + } + var currentRoute: Route { router.routes.last ?? router.default } @@ -148,6 +153,9 @@ import SwiftUI case let .feesChanged(fees): self.fees = fees + + case let .authTypeChanged(authType): + self.authType = authType } } } diff --git a/ios/Cove/Cove.swift b/ios/Cove/Cove.swift index 204f00f0..09354925 100644 --- a/ios/Cove/Cove.swift +++ b/ios/Cove/Cove.swift @@ -3851,6 +3851,11 @@ public func FfiConverterTypeFeeRateOptionsWithTotalFee_lower(_ value: FeeRateOpt */ public protocol FfiAppProtocol : AnyObject { + /** + * Get the auth type for the app + */ + func authType() -> AuthType + /** * Frontend calls this method to send events to the rust application logic */ @@ -3977,6 +3982,16 @@ public convenience init() { + /** + * Get the auth type for the app + */ +open func authType() -> AuthType { + return try! FfiConverterTypeAuthType.lift(try! rustCall() { + uniffi_cove_fn_method_ffiapp_auth_type(self.uniffiClonePointer(),$0 + ) +}) +} + /** * Frontend calls this method to send events to the rust application logic */ @@ -4868,6 +4883,8 @@ public func FfiConverterTypeFoundJson_lower(_ value: FoundJson) -> UnsafeMutable public protocol GlobalConfigTableProtocol : AnyObject { + func authType() throws -> AuthType + func clearSelectedWallet() throws func colorScheme() -> ColorSchemeSelection @@ -4876,8 +4893,6 @@ public protocol GlobalConfigTableProtocol : AnyObject { func get(key: GlobalConfigKey) throws -> String? - func getLockType() throws -> LockType - func selectWallet(id: WalletId) throws func selectedNetwork() -> Network @@ -4888,9 +4903,9 @@ public protocol GlobalConfigTableProtocol : AnyObject { func set(key: GlobalConfigKey, value: String) throws - func setColorScheme(colorScheme: ColorSchemeSelection) throws + func setAuthType(authType: AuthType) throws - func setLockType(lockType: LockType) throws + func setColorScheme(colorScheme: ColorSchemeSelection) throws func setSelectedNetwork(network: Network) throws @@ -4948,6 +4963,13 @@ open class GlobalConfigTable: +open func authType()throws -> AuthType { + return try FfiConverterTypeAuthType.lift(try rustCallWithError(FfiConverterTypeDatabaseError.lift) { + uniffi_cove_fn_method_globalconfigtable_auth_type(self.uniffiClonePointer(),$0 + ) +}) +} + open func clearSelectedWallet()throws {try rustCallWithError(FfiConverterTypeDatabaseError.lift) { uniffi_cove_fn_method_globalconfigtable_clear_selected_wallet(self.uniffiClonePointer(),$0 ) @@ -4976,13 +4998,6 @@ open func get(key: GlobalConfigKey)throws -> String? { }) } -open func getLockType()throws -> LockType { - return try FfiConverterTypeLockType.lift(try rustCallWithError(FfiConverterTypeDatabaseError.lift) { - uniffi_cove_fn_method_globalconfigtable_get_lock_type(self.uniffiClonePointer(),$0 - ) -}) -} - open func selectWallet(id: WalletId)throws {try rustCallWithError(FfiConverterTypeDatabaseError.lift) { uniffi_cove_fn_method_globalconfigtable_select_wallet(self.uniffiClonePointer(), FfiConverterTypeWalletId.lower(id),$0 @@ -5019,16 +5034,16 @@ open func set(key: GlobalConfigKey, value: String)throws {try rustCallWithErro } } -open func setColorScheme(colorScheme: ColorSchemeSelection)throws {try rustCallWithError(FfiConverterTypeDatabaseError.lift) { - uniffi_cove_fn_method_globalconfigtable_set_color_scheme(self.uniffiClonePointer(), - FfiConverterTypeColorSchemeSelection.lower(colorScheme),$0 +open func setAuthType(authType: AuthType)throws {try rustCallWithError(FfiConverterTypeDatabaseError.lift) { + uniffi_cove_fn_method_globalconfigtable_set_auth_type(self.uniffiClonePointer(), + FfiConverterTypeAuthType.lower(authType),$0 ) } } -open func setLockType(lockType: LockType)throws {try rustCallWithError(FfiConverterTypeDatabaseError.lift) { - uniffi_cove_fn_method_globalconfigtable_set_lock_type(self.uniffiClonePointer(), - FfiConverterTypeLockType.lower(lockType),$0 +open func setColorScheme(colorScheme: ColorSchemeSelection)throws {try rustCallWithError(FfiConverterTypeDatabaseError.lift) { + uniffi_cove_fn_method_globalconfigtable_set_color_scheme(self.uniffiClonePointer(), + FfiConverterTypeColorSchemeSelection.lower(colorScheme),$0 ) } } @@ -12402,6 +12417,13 @@ public enum AppAction { ) case updateFiatPrices case updateFees + case updateAuthType(AuthType + ) + case toggleAuth + case toggleBiometric + case setPin(String + ) + case disablePin } @@ -12431,6 +12453,18 @@ public struct FfiConverterTypeAppAction: FfiConverterRustBuffer { case 6: return .updateFees + case 7: return .updateAuthType(try FfiConverterTypeAuthType.read(from: &buf) + ) + + case 8: return .toggleAuth + + case 9: return .toggleBiometric + + case 10: return .setPin(try FfiConverterString.read(from: &buf) + ) + + case 11: return .disablePin + default: throw UniffiInternalError.unexpectedEnumCase } } @@ -12466,6 +12500,28 @@ public struct FfiConverterTypeAppAction: FfiConverterRustBuffer { case .updateFees: writeInt(&buf, Int32(6)) + + case let .updateAuthType(v1): + writeInt(&buf, Int32(7)) + FfiConverterTypeAuthType.write(v1, into: &buf) + + + case .toggleAuth: + writeInt(&buf, Int32(8)) + + + case .toggleBiometric: + writeInt(&buf, Int32(9)) + + + case let .setPin(v1): + writeInt(&buf, Int32(10)) + FfiConverterString.write(v1, into: &buf) + + + case .disablePin: + writeInt(&buf, Int32(11)) + } } } @@ -12571,6 +12627,8 @@ public enum AppStateReconcileMessage { ) case feesChanged(FeeResponse ) + case authTypeChanged(AuthType + ) } @@ -12604,6 +12662,9 @@ public struct FfiConverterTypeAppStateReconcileMessage: FfiConverterRustBuffer { case 7: return .feesChanged(try FfiConverterTypeFeeResponse.read(from: &buf) ) + case 8: return .authTypeChanged(try FfiConverterTypeAuthType.read(from: &buf) + ) + default: throw UniffiInternalError.unexpectedEnumCase } } @@ -12646,6 +12707,11 @@ 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) + } } } @@ -12668,6 +12734,84 @@ public func FfiConverterTypeAppStateReconcileMessage_lower(_ value: AppStateReco +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum AuthType { + + case pin + case biometric + case both + case none +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeAuthType: FfiConverterRustBuffer { + typealias SwiftType = AuthType + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> AuthType { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .pin + + case 2: return .biometric + + case 3: return .both + + case 4: return .none + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: AuthType, into buf: inout [UInt8]) { + switch value { + + + case .pin: + writeInt(&buf, Int32(1)) + + + case .biometric: + writeInt(&buf, Int32(2)) + + + case .both: + writeInt(&buf, Int32(3)) + + + case .none: + writeInt(&buf, Int32(4)) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeAuthType_lift(_ buf: RustBuffer) throws -> AuthType { + return try FfiConverterTypeAuthType.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeAuthType_lower(_ value: AuthType) -> RustBuffer { + return FfiConverterTypeAuthType.lower(value) +} + + + +extension AuthType: Equatable, Hashable {} + + + public enum Bip39Error { @@ -14140,7 +14284,7 @@ public enum GlobalConfigKey { case selectedNode(Network ) case colorScheme - case lockType + case authType } @@ -14163,7 +14307,7 @@ public struct FfiConverterTypeGlobalConfigKey: FfiConverterRustBuffer { case 4: return .colorScheme - case 5: return .lockType + case 5: return .authType default: throw UniffiInternalError.unexpectedEnumCase } @@ -14190,7 +14334,7 @@ public struct FfiConverterTypeGlobalConfigKey: FfiConverterRustBuffer { writeInt(&buf, Int32(4)) - case .lockType: + case .authType: writeInt(&buf, Int32(5)) } @@ -14870,84 +15014,6 @@ extension KeychainError: 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 LockType { - - case pin - case biometric - case both - case none -} - - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public struct FfiConverterTypeLockType: FfiConverterRustBuffer { - typealias SwiftType = LockType - - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> LockType { - let variant: Int32 = try readInt(&buf) - switch variant { - - case 1: return .pin - - case 2: return .biometric - - case 3: return .both - - case 4: return .none - - default: throw UniffiInternalError.unexpectedEnumCase - } - } - - public static func write(_ value: LockType, into buf: inout [UInt8]) { - switch value { - - - case .pin: - writeInt(&buf, Int32(1)) - - - case .biometric: - writeInt(&buf, Int32(2)) - - - case .both: - writeInt(&buf, Int32(3)) - - - case .none: - writeInt(&buf, Int32(4)) - - } - } -} - - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeLockType_lift(_ buf: RustBuffer) throws -> LockType { - return try FfiConverterTypeLockType.lift(buf) -} - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeLockType_lower(_ value: LockType) -> RustBuffer { - return FfiConverterTypeLockType.lower(value) -} - - - -extension LockType: Equatable, Hashable {} - - - public enum MnemonicError { @@ -21439,6 +21505,9 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_feerateoptionswithtotalfee_slow() != 1762) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_method_ffiapp_auth_type() != 34438) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_method_ffiapp_dispatch() != 48712) { return InitializationResult.apiChecksumMismatch } @@ -21511,6 +21580,9 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_fingerprint_as_uppercase() != 11522) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_method_globalconfigtable_auth_type() != 32553) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_method_globalconfigtable_clear_selected_wallet() != 22146) { return InitializationResult.apiChecksumMismatch } @@ -21523,9 +21595,6 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_globalconfigtable_get() != 52128) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cove_checksum_method_globalconfigtable_get_lock_type() != 16233) { - return InitializationResult.apiChecksumMismatch - } if (uniffi_cove_checksum_method_globalconfigtable_select_wallet() != 52001) { return InitializationResult.apiChecksumMismatch } @@ -21541,10 +21610,10 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_globalconfigtable_set() != 31033) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cove_checksum_method_globalconfigtable_set_color_scheme() != 24086) { + if (uniffi_cove_checksum_method_globalconfigtable_set_auth_type() != 48884) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cove_checksum_method_globalconfigtable_set_lock_type() != 26312) { + if (uniffi_cove_checksum_method_globalconfigtable_set_color_scheme() != 24086) { return InitializationResult.apiChecksumMismatch } if (uniffi_cove_checksum_method_globalconfigtable_set_selected_network() != 34312) { diff --git a/ios/Cove/CoveApp.swift b/ios/Cove/CoveApp.swift index c889ba6d..8b71c26e 100644 --- a/ios/Cove/CoveApp.swift +++ b/ios/Cove/CoveApp.swift @@ -465,8 +465,8 @@ struct CoveApp: App { .onOpenURL(perform: handleFileOpen) .onChange(of: phase) { oldPhase, newPhase in Log.debug("[SCENE PHASE]: \(oldPhase) --> \(newPhase)") - - /// TODO: only do this if PIN and/or Biometric is enabledA + + // TODO: only do this if PIN and/or Biometric is enabledA if newPhase == .background { UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } diff --git a/ios/Cove/HomeScreens/SettingsScreen.swift b/ios/Cove/HomeScreens/SettingsScreen.swift index 41dfd969..83c3a489 100644 --- a/ios/Cove/HomeScreens/SettingsScreen.swift +++ b/ios/Cove/HomeScreens/SettingsScreen.swift @@ -1,3 +1,4 @@ +import LocalAuthentication import SwiftUI struct SettingsScreen: View { @@ -10,6 +11,26 @@ struct SettingsScreen: View { let themes = allColorSchemes() + private func canUseBiometrics() -> Bool { + let context = LAContext() + var error: NSError? + return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) + } + + var useAuth: Binding { + Binding( + get: { app.isAuthEnabled }, + set: { app.dispatch(action: .toggleAuth) } + ) + } + + var useBiometric: Binding { + Binding( + get: { app.authType == .both || app.authType == .biometric }, + set: { app.dispatch(action: .toggleBiometric) } + ) + } + var body: some View { Form { Section(header: Text("Network")) { @@ -49,6 +70,24 @@ struct SettingsScreen: View { NodeSelectionView() + Section("Security") { + Toggle(isOn: useAuth) { + Label("Require Authentication", systemImage: "lock.shield") + } + + if useAuth { + if canUseBiometrics() { + Toggle(isOn: $useBiometric) { + Label("Enable Face ID", systemImage: "faceid") + } + } + + Toggle(isOn: $usePIN) { + Label("Enable PIN", systemImage: "key.fill") + } + } + } + Section(header: Text("About")) { HStack { Text("Version") @@ -57,8 +96,8 @@ struct SettingsScreen: View { .foregroundColor(.secondary) } } - .navigationTitle("Settings") } + .navigationTitle("Settings") .navigationBarBackButtonHidden(networkChanged) .toolbar { networkChanged diff --git a/ios/Cove/Info.plist b/ios/Cove/Info.plist index 7ff72e3a..3c1c4eb0 100644 --- a/ios/Cove/Info.plist +++ b/ios/Cove/Info.plist @@ -2,8 +2,12 @@ + NSFaceIDUsageDescription + Allow Face ID to securely authenticate access to your wallets + LSSupportsOpeningDocumentsInPlace + CFBundleDocumentTypes @@ -64,5 +68,6 @@ + diff --git a/ios/Cove/Views/LockView.swift b/ios/Cove/Views/LockView.swift index 72537254..20b5f99f 100644 --- a/ios/Cove/Views/LockView.swift +++ b/ios/Cove/Views/LockView.swift @@ -16,32 +16,32 @@ struct LockView: View { var lockWhenBackground: Bool = true var bioMetricUnlockMessage: String = "Unlock your wallet" @ViewBuilder var content: Content - + /// View Properties @State private var pin: String = "" @State private var animateField: Bool = false @State private var isUnlocked: Bool = false @State private var noBiometricAccess: Bool = false - + /// private consts private let pinLength = 6 - + /// Scene Phase @Environment(\.scenePhase) private var phase - + var body: some View { GeometryReader { let size = $0.size - + content .frame(width: size.width, height: size.height) - + if isEnabled, !isUnlocked { ZStack { Rectangle() .fill(.black) .ignoresSafeArea() - + if (lockType == .both && !noBiometricAccess) || lockType == .biometric { Group { if noBiometricAccess { @@ -55,7 +55,7 @@ struct LockView: View { VStack(spacing: 6) { Image(systemName: "faceid") .font(.largeTitle) - + Text("Tap to Unlock") .font(.caption2) .foregroundStyle(.gray) @@ -66,7 +66,7 @@ struct LockView: View { .onTapGesture { unlockView() } - + if lockType == .both { Text("Enter Pin") .frame(width: 100, height: 40) @@ -102,16 +102,17 @@ struct LockView: View { } } } - + private func bioMetricUnlock() async throws -> Bool { /// Lock Context let context = LAContext() - + return try await context.evaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, - localizedReason: bioMetricUnlockMessage) + localizedReason: bioMetricUnlockMessage + ) } - + private func unlockView() { /// Checking and Unlocking View Task { @@ -121,27 +122,27 @@ struct LockView: View { await MainActor.run { noBiometricAccess = !isBiometricAvailable } return } - + /// Requesting Biometric Unlock if await (try? bioMetricUnlock()) ?? false { await MainActor.run { withAnimation( .snappy, - completionCriteria: .logicallyComplete) - { + completionCriteria: .logicallyComplete + ) { isUnlocked = true } completion: { pin = "" } } } } } - + private var isBiometricAvailable: Bool { /// Lock Context let context = LAContext() return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) } - + /// Numberpad Pin View @ViewBuilder private func NumberPadPinView() -> some View { @@ -164,7 +165,7 @@ struct LockView: View { .padding(.leading) } } - + /// Adding Wiggling Animation for Wrong Password With Keyframe Animator HStack(spacing: 10) { ForEach(0 ..< pinLength, id: \.self) { index in @@ -176,7 +177,7 @@ struct LockView: View { if pin.count > index { let index = pin.index(pin.startIndex, offsetBy: index) let string = String(pin[index]) - + Text(string) .font(.title.bold()) .foregroundStyle(.black) @@ -201,10 +202,11 @@ struct LockView: View { CubicKeyframe(-10, duration: 0.07) CubicKeyframe(0, duration: 0.07) } - }) + } + ) .padding(.top, 15) .frame(maxHeight: .infinity) - + /// Custom Number Pad GeometryReader { _ in LazyVGrid(columns: Array(repeating: GridItem(), count: 3), content: { @@ -221,7 +223,7 @@ struct LockView: View { }) .tint(.white) } - + /// 0 and Back Button Button(action: { if !pin.isEmpty { pin.removeLast() } @@ -233,7 +235,7 @@ struct LockView: View { .contentShape(.rect) }) .tint(.white) - + Button(action: { guard pin.count < pinLength else { return } pin.append("0") diff --git a/rust/src/app.rs b/rust/src/app.rs index 52d0080e..57e4f9a2 100644 --- a/rust/src/app.rs +++ b/rust/src/app.rs @@ -5,6 +5,7 @@ pub mod reconcile; use std::{sync::Arc, time::Duration}; use crate::{ + auth::AuthType, color_scheme::ColorSchemeSelection, database::{error::DatabaseError, Database}, fiat::client::{PriceResponse, FIAT_CLIENT}, @@ -18,7 +19,8 @@ use crossbeam::channel::{Receiver, Sender}; use macros::impl_default_for; use once_cell::sync::OnceCell; use parking_lot::RwLock; -use reconcile::{AppStateReconcileMessage, FfiReconcile, Updater}; +use reconcile::{AppStateReconcileMessage as AppMessage, FfiReconcile, Updater}; +use tap::TapFallible as _; use tracing::{debug, error, warn}; pub static APP: OnceCell = OnceCell::new(); @@ -40,7 +42,7 @@ impl AppState { #[derive(Clone)] pub struct App { state: Arc>, - update_receiver: Arc>, + update_receiver: Arc>, } #[derive(Debug, Clone, Hash, Eq, PartialEq, uniffi::Enum)] @@ -51,6 +53,11 @@ pub enum AppAction { SetSelectedNode(Node), UpdateFiatPrices, UpdateFees, + UpdateAuthType(AuthType), + ToggleAuth, + ToggleBiometric, + SetPin(String), + DisablePin, } #[derive( @@ -73,10 +80,8 @@ impl App { crate::logging::init(); // Set up the updater channel - let (sender, receiver): ( - Sender, - Receiver, - ) = crossbeam::channel::bounded(1000); + let (sender, receiver): (Sender, Receiver) = + crossbeam::channel::bounded(1000); Updater::init(sender); let state = Arc::new(RwLock::new(AppState::new())); @@ -162,9 +167,7 @@ impl App { crate::task::spawn(async move { match FIAT_CLIENT.get_prices().await { - Ok(prices) => Updater::send_update( - AppStateReconcileMessage::FiatPricesChanged(prices), - ), + Ok(prices) => Updater::send_update(AppMessage::FiatPricesChanged(prices)), Err(error) => { error!("unable to update prices: {error:?}"); } @@ -178,7 +181,7 @@ impl App { crate::task::spawn(async move { match FEE_CLIENT.get_fees().await { Ok(fees) => { - Updater::send_update(AppStateReconcileMessage::FeesChanged(fees)); + Updater::send_update(AppMessage::FeesChanged(fees)); } Err(error) => { error!("unable to get fees: {error:?}"); @@ -186,6 +189,47 @@ impl App { } }); } + + AppAction::UpdateAuthType(auth_type) => { + debug!("AuthType changed, NEW: {auth_type:?}"); + set_auth_type(auth_type); + } + + AppAction::ToggleAuth => { + let current_auth_type = FfiApp::global().auth_type(); + let auth_type = if current_auth_type == AuthType::None { + AuthType::Biometric + } else { + AuthType::None + }; + + set_auth_type(auth_type); + } + + AppAction::ToggleBiometric => { + let current_auth_type = FfiApp::global().auth_type(); + let auth_type = match current_auth_type { + AuthType::None => AuthType::Biometric, + AuthType::Biometric => AuthType::None, + AuthType::Pin => AuthType::Biometric, + AuthType::Both => AuthType::Pin, + }; + + set_auth_type(auth_type); + } + + AppAction::SetPin(pin) => { + todo!("HASH and SAVE PIN {pin}",); + } + + AppAction::DisablePin => { + 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), + AuthType::None | AuthType::Biometric => {} + } + } } } @@ -204,6 +248,17 @@ 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`]. #[derive(Debug, Clone, Hash, Eq, PartialEq, uniffi::Object)] pub struct FfiApp; @@ -239,6 +294,17 @@ impl FfiApp { Ok(()) } + /// 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() + } + /// Get the selected wallet pub fn go_to_selected_wallet(&self) -> Option { let selected_wallet = Database::global().global_config.selected_wallet()?; @@ -281,7 +347,7 @@ impl FfiApp { .router .reset_nested_routes_to(default_route.clone(), nested_routes.clone()); - Updater::send_update(AppStateReconcileMessage::DefaultRouteChanged( + Updater::send_update(AppMessage::DefaultRouteChanged( default_route, nested_routes, )); @@ -308,7 +374,7 @@ impl FfiApp { .router .reset_routes_to(route.clone()); - Updater::send_update(AppStateReconcileMessage::DefaultRouteChanged(route, vec![])); + Updater::send_update(AppMessage::DefaultRouteChanged(route, vec![])); } pub fn state(&self) -> AppState { @@ -359,7 +425,7 @@ impl FfiApp { if crate::fiat::client::init_prices().await.is_ok() { let prices = FIAT_CLIENT.get_prices().await; if let Ok(prices) = prices { - Updater::send_update(AppStateReconcileMessage::FiatPricesChanged(prices)); + Updater::send_update(AppMessage::FiatPricesChanged(prices)); } return; @@ -384,7 +450,7 @@ impl FfiApp { let prices = FIAT_CLIENT.get_prices().await; if let Ok(prices) = prices { - Updater::send_update(AppStateReconcileMessage::FiatPricesChanged(prices)); + Updater::send_update(AppMessage::FiatPricesChanged(prices)); } } }); @@ -395,7 +461,7 @@ impl FfiApp { let fees = FEE_CLIENT.get_fees().await; if let Ok(fees) = fees { - Updater::send_update(AppStateReconcileMessage::FeesChanged(fees)); + Updater::send_update(AppMessage::FeesChanged(fees)); } }); } diff --git a/rust/src/app/reconcile.rs b/rust/src/app/reconcile.rs index 94dd62ff..60eff044 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::{ - color_scheme::ColorSchemeSelection, fiat::client::PriceResponse, node::Node, router::Route, - transaction::fees::client::FeeResponse, + auth::AuthType, color_scheme::ColorSchemeSelection, fiat::client::PriceResponse, node::Node, + router::Route, transaction::fees::client::FeeResponse, }; #[derive(uniffi::Enum)] @@ -18,9 +18,10 @@ pub enum AppStateReconcileMessage { SelectedNodeChanged(Node), FiatPricesChanged(PriceResponse), FeesChanged(FeeResponse), + AuthTypeChanged(AuthType), } -// alais for easier imports on the rust side +// alias for easier imports on the rust side pub type Update = AppStateReconcileMessage; pub static UPDATER: OnceCell = OnceCell::new(); diff --git a/rust/src/lock.rs b/rust/src/auth.rs similarity index 90% rename from rust/src/lock.rs rename to rust/src/auth.rs index a02d4fb6..d73666a2 100644 --- a/rust/src/lock.rs +++ b/rust/src/auth.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; #[derive( Debug, + Copy, Clone, Hash, Eq, @@ -14,7 +15,7 @@ use serde::{Deserialize, Serialize}; Display, FromStr, )] -pub enum LockType { +pub enum AuthType { Pin, Biometric, Both, diff --git a/rust/src/database/global_config.rs b/rust/src/database/global_config.rs index fb379538..c3f860e5 100644 --- a/rust/src/database/global_config.rs +++ b/rust/src/database/global_config.rs @@ -4,8 +4,8 @@ use redb::TableDefinition; use crate::{ app::reconcile::{Update, Updater}, + auth::AuthType, color_scheme::ColorSchemeSelection, - lock::LockType, network::Network, node::Node, wallet::metadata::WalletId, @@ -21,7 +21,7 @@ pub enum GlobalConfigKey { SelectedNetwork, SelectedNode(Network), ColorScheme, - LockType, + AuthType, } impl From for &'static str { @@ -32,7 +32,7 @@ impl From for &'static str { GlobalConfigKey::SelectedNode(Network::Bitcoin) => "selected_node_bitcoin", GlobalConfigKey::SelectedNode(Network::Testnet) => "selected_node_testnet", GlobalConfigKey::ColorScheme => "color_scheme", - GlobalConfigKey::LockType => "lock_type", + GlobalConfigKey::AuthType => "auth_type", } } } @@ -150,23 +150,23 @@ impl GlobalConfigTable { Ok(()) } - pub fn get_lock_type(&self) -> Result { - let Some(lock_type) = self - .get(GlobalConfigKey::LockType) + pub fn auth_type(&self) -> Result { + let Some(auth_type) = self + .get(GlobalConfigKey::AuthType) .map_err(|error| Error::DatabaseAccess(error.to_string()))? else { - return Ok(LockType::None); + return Ok(AuthType::None); }; - let lock_type = LockType::from_str(&lock_type) + let auth_type = AuthType::from_str(&auth_type) .map_err(|_| GlobalConfigTableError::Read("unable to parse lock type".to_string()))?; - Ok(lock_type) + Ok(auth_type) } - pub fn set_lock_type(&self, lock_type: LockType) -> Result<(), Error> { - let lock_type = lock_type.to_string(); - self.set(GlobalConfigKey::LockType, lock_type)?; + pub fn set_auth_type(&self, auth_type: AuthType) -> Result<(), Error> { + let auth_type = auth_type.to_string(); + self.set(GlobalConfigKey::AuthType, auth_type)?; Ok(()) } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 403cee8e..0e1818f5 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -4,6 +4,7 @@ pub(crate) mod app; pub(crate) mod logging; pub(crate) mod router; +pub(crate) mod auth; pub(crate) mod autocomplete; pub(crate) mod bip39; pub(crate) mod color; @@ -19,7 +20,6 @@ pub(crate) mod hardware_export; pub(crate) mod header_icon_presenter; pub(crate) mod keychain; pub(crate) mod keys; -pub(crate) mod lock; pub(crate) mod manager; pub(crate) mod mnemonic; pub(crate) mod multi_format; From 7aca1166d3f4273b26f6a3ee066f63c022c42773 Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Wed, 11 Dec 2024 10:04:55 -0600 Subject: [PATCH 04/16] Create macro to create accessors for string configs --- rust/Cargo.lock | 43 ++++++++++++++- rust/Cargo.toml | 12 +++- rust/src/app/reconcile.rs | 1 + rust/src/auth.rs | 36 ++++++++++++ rust/src/color_scheme.rs | 25 ++++++--- rust/src/database.rs | 1 + rust/src/database/global_config.rs | 88 +++++++++++++++--------------- rust/src/database/macros.rs | 37 +++++++++++++ 8 files changed, 185 insertions(+), 58 deletions(-) create mode 100644 rust/src/database/macros.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index a8244c08..97bd68dc 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "act-zero" @@ -128,6 +128,18 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -213,6 +225,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "basic-toml" version = "0.1.9" @@ -418,6 +436,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -596,6 +623,7 @@ dependencies = [ "act-zero", "ahash 0.8.11", "arc-swap", + "argon2", "async-trait", "bbqr", "bdk_chain", @@ -625,6 +653,7 @@ dependencies = [ "numfmt", "once_cell", "parking_lot", + "paste", "pubport", "rand", "redb", @@ -766,6 +795,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -1829,6 +1859,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 923f8017..1b9b651c 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -23,6 +23,7 @@ bdk_wallet = { version = "1.0.0-beta.5", features = [ "keys-bip39", "file_store", ], git = "https://github.com/bitcoinppl/bdk" } + bip39 = { version = "2.0.0", features = ["zeroize"] } # bitcoin nodes @@ -31,6 +32,7 @@ bdk_esplora = { version = "0.19", git = "https://github.com/bitcoinppl/bdk", fea "async-https", "tokio", ] } + bdk_electrum = { version = "0.19", features = [ "use-rustls-ring", ], default-features = false, git = "https://github.com/bitcoinppl/bdk" } @@ -59,6 +61,10 @@ tryhard = "0.5" rand = "0.8.5" zeroize = "1.8.1" +# hashing +sha2 = "0.10.8" +argon2 = "0.5.0" + # concurrency crossbeam = "0.8.4" parking_lot = { version = "0.12.1", features = ["deadlock_detection"] } @@ -108,9 +114,6 @@ serde_json = "1.0" # bindings uniffi = { git = "https://github.com/mozilla/uniffi-rs", features = ["tokio"] } -# hashing -sha2 = "0.10.8" - # bit manipulation num-bigint = "0.4" bitvec = "1.0" @@ -145,6 +148,9 @@ winnow = "0.6.0" # sync arc-swap = "1.7" +# macros +paste = "1.0" + [build-dependencies] uniffi = { git = "https://github.com/mozilla/uniffi-rs", features = ["build"] } diff --git a/rust/src/app/reconcile.rs b/rust/src/app/reconcile.rs index 60eff044..df3732d7 100644 --- a/rust/src/app/reconcile.rs +++ b/rust/src/app/reconcile.rs @@ -19,6 +19,7 @@ pub enum AppStateReconcileMessage { FiatPricesChanged(PriceResponse), FeesChanged(FeeResponse), AuthTypeChanged(AuthType), + PinCodeChanged(String), } // alias for easier imports on the rust side diff --git a/rust/src/auth.rs b/rust/src/auth.rs index d73666a2..f7110779 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -1,6 +1,8 @@ use derive_more::{Display, FromStr}; use serde::{Deserialize, Serialize}; +use crate::database; + #[derive( Debug, Copy, @@ -23,3 +25,37 @@ pub enum AuthType { #[default] None, } + +type Result = std::result::Result; + +#[derive( + Debug, Clone, Hash, Eq, PartialEq, uniffi::Error, thiserror::Error, derive_more::Display, +)] +pub enum AuthError { + /// Unable to save pin to database {0:?} + DatabaseSaveError(database::Error), + + /// Unable to get pin from database {0:?} + DatabaseGetError(database::Error), +} + +#[derive(Debug, Clone, Hash, Eq, PartialEq, uniffi::Object)] +pub struct AuthPin {} + +#[uniffi::export] +impl AuthPin { + #[uniffi::constructor] + pub fn new() -> Self { + Self {} + } + + #[uniffi::method] + pub fn check(&self, pin: String) -> bool { + todo!("CHECK PIN {pin}"); + } + + #[uniffi::method] + pub fn set(&self, pin: String) -> Result<()> { + todo!("SET PIN {pin}"); + } +} diff --git a/rust/src/color_scheme.rs b/rust/src/color_scheme.rs index 4d1847ba..b3724848 100644 --- a/rust/src/color_scheme.rs +++ b/rust/src/color_scheme.rs @@ -1,4 +1,4 @@ -use std::fmt::Display; +use std::{fmt::Display, str::FromStr}; #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, uniffi::Enum, strum::EnumIter)] pub enum FfiColorScheme { @@ -6,19 +6,14 @@ pub enum FfiColorScheme { Dark, } -#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, uniffi::Enum, strum::EnumIter)] +#[derive(Default, Debug, Copy, Clone, Hash, Eq, PartialEq, uniffi::Enum, strum::EnumIter)] pub enum ColorSchemeSelection { Light, Dark, + #[default] System, } -impl Default for ColorSchemeSelection { - fn default() -> Self { - Self::System - } -} - impl ColorSchemeSelection { pub fn as_capitalized_string(&self) -> &'static str { match self { @@ -29,13 +24,25 @@ impl ColorSchemeSelection { } } +impl FromStr for ColorSchemeSelection { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(ColorSchemeSelection::from(s)) + } +} + impl From<&str> for ColorSchemeSelection { fn from(value: &str) -> Self { match value { "Light" | "light" => Self::Light, "Dark" | "dark" => Self::Dark, "System" | "system" => Self::System, - _ => Self::System, + other => match other.to_lowercase().as_str() { + "light" => Self::Light, + "dark" => Self::Dark, + _ => Self::System, + }, } } } diff --git a/rust/src/database.rs b/rust/src/database.rs index be7d5d00..c8974093 100644 --- a/rust/src/database.rs +++ b/rust/src/database.rs @@ -5,6 +5,7 @@ pub mod error; pub mod global_cache; pub mod global_config; pub mod global_flag; +pub mod macros; pub mod unsigned_transactions; pub mod wallet; pub mod wallet_data; diff --git a/rust/src/database/global_config.rs b/rust/src/database/global_config.rs index c3f860e5..df7cbb5f 100644 --- a/rust/src/database/global_config.rs +++ b/rust/src/database/global_config.rs @@ -1,4 +1,4 @@ -use std::{str::FromStr as _, sync::Arc}; +use std::sync::Arc; use redb::TableDefinition; @@ -12,9 +12,12 @@ use crate::{ }; use super::{error::SerdeError, Error}; +use crate::string_config_accessor; pub const TABLE: TableDefinition<&'static str, String> = TableDefinition::new("global_config"); +type Result = std::result::Result; + #[derive(Debug, Clone, Copy, uniffi::Enum)] pub enum GlobalConfigKey { SelectedWalletId, @@ -22,6 +25,7 @@ pub enum GlobalConfigKey { SelectedNode(Network), ColorScheme, AuthType, + HashedPinCode, } impl From for &'static str { @@ -33,6 +37,7 @@ impl From for &'static str { GlobalConfigKey::SelectedNode(Network::Testnet) => "selected_node_testnet", GlobalConfigKey::ColorScheme => "color_scheme", GlobalConfigKey::AuthType => "auth_type", + GlobalConfigKey::HashedPinCode => "hashed_pin_code", } } } @@ -58,11 +63,30 @@ pub enum GlobalConfigTableError { #[error("failed to get global config: {0}")] Read(String), + + #[error("pin code must be hashed before saving")] + PinCodeMustBeHashed, +} + +impl GlobalConfigTable { + string_config_accessor!( + auth_type, + GlobalConfigKey::AuthType, + AuthType, + Update::AuthTypeChanged + ); + + string_config_accessor!( + color_scheme, + GlobalConfigKey::ColorScheme, + ColorSchemeSelection, + Update::ColorSchemeChanged + ); } #[uniffi::export] impl GlobalConfigTable { - pub fn select_wallet(&self, id: WalletId) -> Result<(), Error> { + pub fn select_wallet(&self, id: WalletId) -> Result<()> { self.set(GlobalConfigKey::SelectedWalletId, id.to_string())?; Ok(()) @@ -78,7 +102,7 @@ impl GlobalConfigTable { Some(wallet_id) } - pub fn clear_selected_wallet(&self) -> Result<(), Error> { + pub fn clear_selected_wallet(&self) -> Result<()> { self.delete(GlobalConfigKey::SelectedWalletId)?; Ok(()) @@ -103,6 +127,12 @@ impl GlobalConfigTable { network } + pub fn set_selected_network(&self, network: Network) -> Result<()> { + self.set(GlobalConfigKey::SelectedNetwork, network.to_string())?; + + Ok(()) + } + pub fn selected_node(&self) -> Node { let network = self.selected_network(); let selected_node_key = GlobalConfigKey::SelectedNode(network); @@ -115,7 +145,7 @@ impl GlobalConfigTable { serde_json::from_str(&node_json).unwrap_or_else(|_| Node::default(network)) } - pub fn set_selected_node(&self, node: &Node) -> Result<(), Error> { + pub fn set_selected_node(&self, node: &Node) -> Result<()> { let network = node.network; let node_json = serde_json::to_string(node) .map_err(|error| SerdeError::SerializationError(error.to_string()))?; @@ -128,49 +158,17 @@ impl GlobalConfigTable { Ok(()) } - pub fn color_scheme(&self) -> ColorSchemeSelection { - let color_scheme = self - .get(GlobalConfigKey::ColorScheme) - .unwrap_or(None) - .unwrap_or("system".to_string()); - - ColorSchemeSelection::from(color_scheme) + #[uniffi::method(name = "colorScheme")] + pub fn _color_scheme(&self) -> Result { + self.color_scheme() } - pub fn set_color_scheme(&self, color_scheme: ColorSchemeSelection) -> Result<(), Error> { - self.set(GlobalConfigKey::ColorScheme, color_scheme.to_string())?; - Updater::send_update(Update::ColorSchemeChanged(color_scheme)); - - Ok(()) - } - - pub fn set_selected_network(&self, network: Network) -> Result<(), Error> { - self.set(GlobalConfigKey::SelectedNetwork, network.to_string())?; - - Ok(()) - } - - pub fn auth_type(&self) -> Result { - let Some(auth_type) = self - .get(GlobalConfigKey::AuthType) - .map_err(|error| Error::DatabaseAccess(error.to_string()))? - else { - return Ok(AuthType::None); - }; - - let auth_type = AuthType::from_str(&auth_type) - .map_err(|_| GlobalConfigTableError::Read("unable to parse lock type".to_string()))?; - - Ok(auth_type) - } - - pub fn set_auth_type(&self, auth_type: AuthType) -> Result<(), Error> { - let auth_type = auth_type.to_string(); - self.set(GlobalConfigKey::AuthType, auth_type)?; - Ok(()) + #[uniffi::method(name = "setColorScheme")] + pub fn _set_color_scheme(&self, color_scheme: ColorSchemeSelection) -> Result<()> { + self.set_color_scheme(color_scheme) } - pub fn get(&self, key: GlobalConfigKey) -> Result, Error> { + fn get(&self, key: GlobalConfigKey) -> Result> { let read_txn = self .db .begin_read() @@ -189,7 +187,7 @@ impl GlobalConfigTable { Ok(value) } - pub fn set(&self, key: GlobalConfigKey, value: String) -> Result<(), Error> { + fn set(&self, key: GlobalConfigKey, value: String) -> Result<()> { let write_txn = self .db .begin_write() @@ -215,7 +213,7 @@ impl GlobalConfigTable { Ok(()) } - pub fn delete(&self, key: GlobalConfigKey) -> Result<(), Error> { + pub fn delete(&self, key: GlobalConfigKey) -> Result<()> { let write_txn = self .db .begin_write() diff --git a/rust/src/database/macros.rs b/rust/src/database/macros.rs new file mode 100644 index 00000000..a631be03 --- /dev/null +++ b/rust/src/database/macros.rs @@ -0,0 +1,37 @@ +#[macro_export] +macro_rules! string_config_accessor { + ($fn_name:ident, $key:expr, $return_type:ty, $update_variant:path) => { + pub fn $fn_name(&self) -> Result<$return_type, Error> { + use std::str::FromStr as _; + + let Some(value) = self + .get($key) + .map_err(|error| Error::DatabaseAccess(error.to_string()))? + else { + return Ok(Default::default()); + }; + + let parsed = <$return_type>::from_str(&value).map_err(|_| { + GlobalConfigTableError::Read(format!("unable to parse {}", stringify!($fn_name))) + })?; + + Ok(parsed) + } + + paste::paste! { + pub fn [](&self, value: $return_type) -> Result<(), Error> { + let value_to_send = if std::mem::needs_drop::<$return_type>() { + value.clone() + } else { + value + }; + + let value = value.to_string(); + self.set($key, value)?; + Updater::send_update($update_variant(value_to_send)); + + Ok(()) + } + } + }; +} From 37408505994a26a5c9cac6269834f4abcc335544 Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Wed, 11 Dec 2024 12:15:05 -0600 Subject: [PATCH 05/16] Complete functions to set, get and verify pin --- rust/Cargo.toml | 2 +- rust/src/app/reconcile.rs | 2 +- rust/src/auth.rs | 54 ++++++++++++++++++++++++++++-- rust/src/database/global_config.rs | 7 ++++ rust/src/database/macros.rs | 7 +--- 5 files changed, 62 insertions(+), 10 deletions(-) diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 1b9b651c..0fcfffc7 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -63,7 +63,7 @@ zeroize = "1.8.1" # hashing sha2 = "0.10.8" -argon2 = "0.5.0" +argon2 = { version = "0.5.0", features = ["password-hash"] } # concurrency crossbeam = "0.8.4" diff --git a/rust/src/app/reconcile.rs b/rust/src/app/reconcile.rs index df3732d7..b50eb426 100644 --- a/rust/src/app/reconcile.rs +++ b/rust/src/app/reconcile.rs @@ -19,7 +19,7 @@ pub enum AppStateReconcileMessage { FiatPricesChanged(PriceResponse), FeesChanged(FeeResponse), AuthTypeChanged(AuthType), - PinCodeChanged(String), + HashedPinCodeChanged(String), } // alias for easier imports on the rust side diff --git a/rust/src/auth.rs b/rust/src/auth.rs index f7110779..77b889a0 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -1,8 +1,14 @@ +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + Argon2, +}; use derive_more::{Display, FromStr}; use serde::{Deserialize, Serialize}; use crate::database; +use self::database::Database; + #[derive( Debug, Copy, @@ -37,6 +43,15 @@ pub enum AuthError { /// Unable to get pin from database {0:?} DatabaseGetError(database::Error), + + /// Unable to hash pin {0} + HashError(String), + + /// Unable to parse hashed pin {0} + ParseHashedPinError(String), + + /// Verification failed {0} + VerificationFailed(String), } #[derive(Debug, Clone, Hash, Eq, PartialEq, uniffi::Object)] @@ -51,11 +66,46 @@ impl AuthPin { #[uniffi::method] pub fn check(&self, pin: String) -> bool { - todo!("CHECK PIN {pin}"); + let hashed_pin = Database::global() + .global_config + .hashed_pin_code() + .unwrap_or_default(); + + self.verify(pin, hashed_pin).is_ok() } #[uniffi::method] pub fn set(&self, pin: String) -> Result<()> { - todo!("SET PIN {pin}"); + let hashed = self.hash(pin)?; + Database::global() + .global_config + .set_hashed_pin_code(hashed) + .map_err(|error| AuthError::DatabaseSaveError(error)) + } + + #[uniffi::method] + pub fn hash(&self, pin: String) -> Result { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + + let pin_hash = argon2 + .hash_password(pin.as_bytes(), &salt) + .map_err(|error| AuthError::HashError(format!("unable to hash pin: {error}")))? + .to_string(); + + Ok(pin_hash) + } + + #[uniffi::method] + pub fn verify(&self, pin: String, hashed_pin: String) -> Result<()> { + let argon2 = Argon2::default(); + + let parsed_hash = PasswordHash::new(&hashed_pin).map_err(|error| { + AuthError::ParseHashedPinError(format!("unable to parse hashed pin: {error}")) + })?; + + argon2 + .verify_password(pin.as_bytes(), &parsed_hash) + .map_err(|error| AuthError::VerificationFailed(format!("{error:?}"))) } } diff --git a/rust/src/database/global_config.rs b/rust/src/database/global_config.rs index df7cbb5f..c20f2ffa 100644 --- a/rust/src/database/global_config.rs +++ b/rust/src/database/global_config.rs @@ -82,6 +82,13 @@ impl GlobalConfigTable { ColorSchemeSelection, Update::ColorSchemeChanged ); + + string_config_accessor!( + hashed_pin_code, + GlobalConfigKey::HashedPinCode, + String, + Update::HashedPinCodeChanged + ); } #[uniffi::export] diff --git a/rust/src/database/macros.rs b/rust/src/database/macros.rs index a631be03..0377ac0a 100644 --- a/rust/src/database/macros.rs +++ b/rust/src/database/macros.rs @@ -20,12 +20,7 @@ macro_rules! string_config_accessor { paste::paste! { pub fn [](&self, value: $return_type) -> Result<(), Error> { - let value_to_send = if std::mem::needs_drop::<$return_type>() { - value.clone() - } else { - value - }; - + let value_to_send = value.clone(); let value = value.to_string(); self.set($key, value)?; Updater::send_update($update_variant(value_to_send)); From 56a5e864e5058a3397c2b17afbd424f71d4989d3 Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Wed, 11 Dec 2024 12:17:37 -0600 Subject: [PATCH 06/16] Add function to delete pin --- rust/src/auth.rs | 10 +++++++++- rust/src/database/macros.rs | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 77b889a0..44596f5d 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -80,7 +80,7 @@ impl AuthPin { Database::global() .global_config .set_hashed_pin_code(hashed) - .map_err(|error| AuthError::DatabaseSaveError(error)) + .map_err(AuthError::DatabaseSaveError) } #[uniffi::method] @@ -96,6 +96,14 @@ impl AuthPin { Ok(pin_hash) } + #[uniffi::method] + pub fn delete(&self) -> Result<()> { + Database::global() + .global_config + .delete_hashed_pin_code() + .map_err(AuthError::DatabaseSaveError) + } + #[uniffi::method] pub fn verify(&self, pin: String, hashed_pin: String) -> Result<()> { let argon2 = Argon2::default(); diff --git a/rust/src/database/macros.rs b/rust/src/database/macros.rs index 0377ac0a..5a5320ef 100644 --- a/rust/src/database/macros.rs +++ b/rust/src/database/macros.rs @@ -28,5 +28,13 @@ macro_rules! string_config_accessor { Ok(()) } } + + paste::paste! { + #[allow(dead_code)] + pub fn [](&self) -> Result<(), Error> { + self.delete($key)?; + Ok(()) + } + } }; } From 42b91c0bfb2b4faaa205c31e5075c8f4dfbeb576 Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Wed, 11 Dec 2024 14:22:27 -0600 Subject: [PATCH 07/16] Change macro to be able to create private accessors --- rust/src/auth.rs | 20 ++++++++++++++++++++ rust/src/database/global_config.rs | 26 +++++++++++++++++++++++--- rust/src/database/macros.rs | 19 +++++++++++++++---- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 44596f5d..261b7406 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -117,3 +117,23 @@ impl AuthPin { .map_err(|error| AuthError::VerificationFailed(format!("{error:?}"))) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_hash_pin() { + let auth = AuthPin::new(); + let hashed = auth.hash("123456".to_string()).unwrap(); + assert!(hashed.starts_with("$argon2id")); + } + + #[test] + fn test_verify_pin() { + let auth = AuthPin::new(); + let hashed = auth.hash("123456".to_string()).unwrap(); + let result = auth.verify("123456".to_string(), hashed); + assert!(result.is_ok()); + } +} diff --git a/rust/src/database/global_config.rs b/rust/src/database/global_config.rs index c20f2ffa..ce17410a 100644 --- a/rust/src/database/global_config.rs +++ b/rust/src/database/global_config.rs @@ -70,21 +70,21 @@ pub enum GlobalConfigTableError { impl GlobalConfigTable { string_config_accessor!( - auth_type, + pub auth_type, GlobalConfigKey::AuthType, AuthType, Update::AuthTypeChanged ); string_config_accessor!( - color_scheme, + pub color_scheme, GlobalConfigKey::ColorScheme, ColorSchemeSelection, Update::ColorSchemeChanged ); string_config_accessor!( - hashed_pin_code, + priv_hashed_pin_code, GlobalConfigKey::HashedPinCode, String, Update::HashedPinCodeChanged @@ -175,6 +175,26 @@ impl GlobalConfigTable { self.set_color_scheme(color_scheme) } + pub fn hashed_pin_code(&self) -> Result { + self.priv_hashed_pin_code() + } + + pub fn delete_hashed_pin_code(&self) -> Result<()> { + self.delete_priv_hashed_pin_code() + } + + pub fn set_hashed_pin_code(&self, hashed_pin_code: String) -> Result<()> { + if hashed_pin_code.is_empty() { + return Err(GlobalConfigTableError::PinCodeMustBeHashed.into()); + } + + if hashed_pin_code.len() <= 6 { + return Err(GlobalConfigTableError::PinCodeMustBeHashed.into()); + } + + self.set_priv_hashed_pin_code(hashed_pin_code) + } + fn get(&self, key: GlobalConfigKey) -> Result> { let read_txn = self .db diff --git a/rust/src/database/macros.rs b/rust/src/database/macros.rs index 5a5320ef..7bb29da2 100644 --- a/rust/src/database/macros.rs +++ b/rust/src/database/macros.rs @@ -1,7 +1,8 @@ #[macro_export] macro_rules! string_config_accessor { - ($fn_name:ident, $key:expr, $return_type:ty, $update_variant:path) => { - pub fn $fn_name(&self) -> Result<$return_type, Error> { + // Define internal macro for the implementation + (@impl $vis:vis, $fn_name:ident, $key:expr, $return_type:ty, $update_variant:path) => { + $vis fn $fn_name(&self) -> Result<$return_type, Error> { use std::str::FromStr as _; let Some(value) = self @@ -19,7 +20,7 @@ macro_rules! string_config_accessor { } paste::paste! { - pub fn [](&self, value: $return_type) -> Result<(), Error> { + $vis fn [](&self, value: $return_type) -> Result<(), Error> { let value_to_send = value.clone(); let value = value.to_string(); self.set($key, value)?; @@ -31,10 +32,20 @@ macro_rules! string_config_accessor { paste::paste! { #[allow(dead_code)] - pub fn [](&self) -> Result<(), Error> { + $vis fn [](&self) -> Result<(), Error> { self.delete($key)?; Ok(()) } } }; + + // 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); + }; + + // Private interface + ($fn_name:ident, $key:expr, $return_type:ty, $update_variant:path) => { + string_config_accessor!(@impl , $fn_name, $key, $return_type, $update_variant); + }; } From df8643c6c6b2b7b87b3bf34353cf5a3fa5c78ce3 Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Wed, 11 Dec 2024 19:13:51 -0600 Subject: [PATCH 08/16] Get Settings screen ready for using PIN view to set PIN --- ios/Cove/AppManager.swift | 5 +- ios/Cove/Cove.swift | 373 +++++++++++++++++++++- ios/Cove/HomeScreens/SettingsScreen.swift | 63 +++- rust/src/app.rs | 10 +- rust/src/auth.rs | 2 +- rust/src/database/global_config.rs | 9 +- rust/src/database/macros.rs | 2 +- 7 files changed, 424 insertions(+), 40 deletions(-) diff --git a/ios/Cove/AppManager.swift b/ios/Cove/AppManager.swift index a3ec0a85..072ead03 100644 --- a/ios/Cove/AppManager.swift +++ b/ios/Cove/AppManager.swift @@ -15,7 +15,7 @@ import SwiftUI var colorSchemeSelection = Database().globalConfig().colorScheme() var selectedNode = Database().globalConfig().selectedNode() - var authType = try? Database().globalConfig().authType() ?? AuthType.none + var authType = Database().globalConfig().authType() var nfcReader = NFCReader() @@ -156,6 +156,9 @@ import SwiftUI case let .authTypeChanged(authType): self.authType = authType + + case .hashedPinCodeChanged: + () } } } diff --git a/ios/Cove/Cove.swift b/ios/Cove/Cove.swift index 09354925..8d8bc0ff 100644 --- a/ios/Cove/Cove.swift +++ b/ios/Cove/Cove.swift @@ -1232,6 +1232,171 @@ public func FfiConverterTypeAmount_lower(_ value: Amount) -> UnsafeMutableRawPoi +public protocol AuthPinProtocol : AnyObject { + + func check(pin: String) -> Bool + + func delete() throws + + func hash(pin: String) throws -> String + + func set(pin: String) throws + + func verify(pin: String, hashedPin: String) throws + +} + +open class AuthPin: + AuthPinProtocol { + 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_authpin(self.pointer, $0) } + } +public convenience init() { + let pointer = + try! rustCall() { + uniffi_cove_fn_constructor_authpin_new($0 + ) +} + self.init(unsafeFromRawPointer: pointer) +} + + deinit { + guard let pointer = pointer else { + return + } + + try! rustCall { uniffi_cove_fn_free_authpin(pointer, $0) } + } + + + + +open func check(pin: String) -> Bool { + return try! FfiConverterBool.lift(try! rustCall() { + uniffi_cove_fn_method_authpin_check(self.uniffiClonePointer(), + FfiConverterString.lower(pin),$0 + ) +}) +} + +open func delete()throws {try rustCallWithError(FfiConverterTypeAuthError.lift) { + uniffi_cove_fn_method_authpin_delete(self.uniffiClonePointer(),$0 + ) +} +} + +open func hash(pin: String)throws -> String { + return try FfiConverterString.lift(try rustCallWithError(FfiConverterTypeAuthError.lift) { + uniffi_cove_fn_method_authpin_hash(self.uniffiClonePointer(), + FfiConverterString.lower(pin),$0 + ) +}) +} + +open func set(pin: String)throws {try rustCallWithError(FfiConverterTypeAuthError.lift) { + uniffi_cove_fn_method_authpin_set(self.uniffiClonePointer(), + FfiConverterString.lower(pin),$0 + ) +} +} + +open func verify(pin: String, hashedPin: String)throws {try rustCallWithError(FfiConverterTypeAuthError.lift) { + uniffi_cove_fn_method_authpin_verify(self.uniffiClonePointer(), + FfiConverterString.lower(pin), + FfiConverterString.lower(hashedPin),$0 + ) +} +} + + +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeAuthPin: FfiConverter { + + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = AuthPin + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> AuthPin { + return AuthPin(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: AuthPin) -> UnsafeMutableRawPointer { + return value.uniffiClonePointer() + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> AuthPin { + 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: AuthPin, 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 FfiConverterTypeAuthPin_lift(_ pointer: UnsafeMutableRawPointer) throws -> AuthPin { + return try FfiConverterTypeAuthPin.lift(pointer) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeAuthPin_lower(_ value: AuthPin) -> UnsafeMutableRawPointer { + return FfiConverterTypeAuthPin.lower(value) +} + + + + public protocol AutoComplete : AnyObject { func autocomplete(word: String) -> [String] @@ -4883,7 +5048,7 @@ public func FfiConverterTypeFoundJson_lower(_ value: FoundJson) -> UnsafeMutable public protocol GlobalConfigTableProtocol : AnyObject { - func authType() throws -> AuthType + func authType() -> AuthType func clearSelectedWallet() throws @@ -4891,8 +5056,12 @@ public protocol GlobalConfigTableProtocol : AnyObject { func delete(key: GlobalConfigKey) throws + func deleteHashedPinCode() throws + func get(key: GlobalConfigKey) throws -> String? + func hashedPinCode() throws -> String + func selectWallet(id: WalletId) throws func selectedNetwork() -> Network @@ -4903,10 +5072,10 @@ public protocol GlobalConfigTableProtocol : AnyObject { func set(key: GlobalConfigKey, value: String) throws - func setAuthType(authType: AuthType) throws - func setColorScheme(colorScheme: ColorSchemeSelection) throws + func setHashedPinCode(hashedPinCode: String) throws + func setSelectedNetwork(network: Network) throws func setSelectedNode(node: Node) throws @@ -4963,9 +5132,9 @@ open class GlobalConfigTable: -open func authType()throws -> AuthType { - return try FfiConverterTypeAuthType.lift(try rustCallWithError(FfiConverterTypeDatabaseError.lift) { - uniffi_cove_fn_method_globalconfigtable_auth_type(self.uniffiClonePointer(),$0 +open func authType() -> AuthType { + return try! FfiConverterTypeAuthType.lift(try! rustCall() { + uniffi_cove_fn_method_globalconfigtable_authtype(self.uniffiClonePointer(),$0 ) }) } @@ -4978,7 +5147,7 @@ open func clearSelectedWallet()throws {try rustCallWithError(FfiConverterTypeD open func colorScheme() -> ColorSchemeSelection { return try! FfiConverterTypeColorSchemeSelection.lift(try! rustCall() { - uniffi_cove_fn_method_globalconfigtable_color_scheme(self.uniffiClonePointer(),$0 + uniffi_cove_fn_method_globalconfigtable_colorscheme(self.uniffiClonePointer(),$0 ) }) } @@ -4990,6 +5159,12 @@ open func delete(key: GlobalConfigKey)throws {try rustCallWithError(FfiConvert } } +open func deleteHashedPinCode()throws {try rustCallWithError(FfiConverterTypeDatabaseError.lift) { + uniffi_cove_fn_method_globalconfigtable_delete_hashed_pin_code(self.uniffiClonePointer(),$0 + ) +} +} + open func get(key: GlobalConfigKey)throws -> String? { return try FfiConverterOptionString.lift(try rustCallWithError(FfiConverterTypeDatabaseError.lift) { uniffi_cove_fn_method_globalconfigtable_get(self.uniffiClonePointer(), @@ -4998,6 +5173,13 @@ open func get(key: GlobalConfigKey)throws -> String? { }) } +open func hashedPinCode()throws -> String { + return try FfiConverterString.lift(try rustCallWithError(FfiConverterTypeDatabaseError.lift) { + uniffi_cove_fn_method_globalconfigtable_hashed_pin_code(self.uniffiClonePointer(),$0 + ) +}) +} + open func selectWallet(id: WalletId)throws {try rustCallWithError(FfiConverterTypeDatabaseError.lift) { uniffi_cove_fn_method_globalconfigtable_select_wallet(self.uniffiClonePointer(), FfiConverterTypeWalletId.lower(id),$0 @@ -5034,16 +5216,16 @@ open func set(key: GlobalConfigKey, value: String)throws {try rustCallWithErro } } -open func setAuthType(authType: AuthType)throws {try rustCallWithError(FfiConverterTypeDatabaseError.lift) { - uniffi_cove_fn_method_globalconfigtable_set_auth_type(self.uniffiClonePointer(), - FfiConverterTypeAuthType.lower(authType),$0 +open func setColorScheme(colorScheme: ColorSchemeSelection)throws {try rustCallWithError(FfiConverterTypeDatabaseError.lift) { + uniffi_cove_fn_method_globalconfigtable_setcolorscheme(self.uniffiClonePointer(), + FfiConverterTypeColorSchemeSelection.lower(colorScheme),$0 ) } } -open func setColorScheme(colorScheme: ColorSchemeSelection)throws {try rustCallWithError(FfiConverterTypeDatabaseError.lift) { - uniffi_cove_fn_method_globalconfigtable_set_color_scheme(self.uniffiClonePointer(), - FfiConverterTypeColorSchemeSelection.lower(colorScheme),$0 +open func setHashedPinCode(hashedPinCode: String)throws {try rustCallWithError(FfiConverterTypeDatabaseError.lift) { + uniffi_cove_fn_method_globalconfigtable_set_hashed_pin_code(self.uniffiClonePointer(), + FfiConverterString.lower(hashedPinCode),$0 ) } } @@ -12629,6 +12811,8 @@ public enum AppStateReconcileMessage { ) case authTypeChanged(AuthType ) + case hashedPinCodeChanged(String + ) } @@ -12665,6 +12849,9 @@ public struct FfiConverterTypeAppStateReconcileMessage: FfiConverterRustBuffer { case 8: return .authTypeChanged(try FfiConverterTypeAuthType.read(from: &buf) ) + case 9: return .hashedPinCodeChanged(try FfiConverterString.read(from: &buf) + ) + default: throw UniffiInternalError.unexpectedEnumCase } } @@ -12712,6 +12899,11 @@ public struct FfiConverterTypeAppStateReconcileMessage: FfiConverterRustBuffer { writeInt(&buf, Int32(8)) FfiConverterTypeAuthType.write(v1, into: &buf) + + case let .hashedPinCodeChanged(v1): + writeInt(&buf, Int32(9)) + FfiConverterString.write(v1, into: &buf) + } } } @@ -12734,6 +12926,116 @@ public func FfiConverterTypeAppStateReconcileMessage_lower(_ value: AppStateReco + +public enum AuthError { + + + + /** + * Unable to save pin to database {0:?} + */ + case DatabaseSaveError(DatabaseError + ) + /** + * Unable to get pin from database {0:?} + */ + case DatabaseGetError(DatabaseError + ) + /** + * Unable to hash pin {0} + */ + case HashError(String + ) + /** + * Unable to parse hashed pin {0} + */ + case ParseHashedPinError(String + ) + /** + * Verification failed {0} + */ + case VerificationFailed(String + ) +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeAuthError: FfiConverterRustBuffer { + typealias SwiftType = AuthError + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> AuthError { + let variant: Int32 = try readInt(&buf) + switch variant { + + + + + case 1: return .DatabaseSaveError( + try FfiConverterTypeDatabaseError.read(from: &buf) + ) + case 2: return .DatabaseGetError( + try FfiConverterTypeDatabaseError.read(from: &buf) + ) + case 3: return .HashError( + try FfiConverterString.read(from: &buf) + ) + case 4: return .ParseHashedPinError( + try FfiConverterString.read(from: &buf) + ) + case 5: return .VerificationFailed( + try FfiConverterString.read(from: &buf) + ) + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: AuthError, into buf: inout [UInt8]) { + switch value { + + + + + + case let .DatabaseSaveError(v1): + writeInt(&buf, Int32(1)) + FfiConverterTypeDatabaseError.write(v1, into: &buf) + + + case let .DatabaseGetError(v1): + writeInt(&buf, Int32(2)) + FfiConverterTypeDatabaseError.write(v1, into: &buf) + + + case let .HashError(v1): + writeInt(&buf, Int32(3)) + FfiConverterString.write(v1, into: &buf) + + + case let .ParseHashedPinError(v1): + writeInt(&buf, Int32(4)) + FfiConverterString.write(v1, into: &buf) + + + case let .VerificationFailed(v1): + writeInt(&buf, Int32(5)) + FfiConverterString.write(v1, into: &buf) + + } + } +} + + +extension AuthError: Equatable, Hashable {} + +extension AuthError: 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. @@ -14285,6 +14587,7 @@ public enum GlobalConfigKey { ) case colorScheme case authType + case hashedPinCode } @@ -14309,6 +14612,8 @@ public struct FfiConverterTypeGlobalConfigKey: FfiConverterRustBuffer { case 5: return .authType + case 6: return .hashedPinCode + default: throw UniffiInternalError.unexpectedEnumCase } } @@ -14337,6 +14642,10 @@ public struct FfiConverterTypeGlobalConfigKey: FfiConverterRustBuffer { case .authType: writeInt(&buf, Int32(5)) + + case .hashedPinCode: + writeInt(&buf, Int32(6)) + } } } @@ -14371,6 +14680,7 @@ public enum GlobalConfigTableError { ) case Read(String ) + case PinCodeMustBeHashed } @@ -14393,6 +14703,7 @@ public struct FfiConverterTypeGlobalConfigTableError: FfiConverterRustBuffer { case 2: return .Read( try FfiConverterString.read(from: &buf) ) + case 3: return .PinCodeMustBeHashed default: throw UniffiInternalError.unexpectedEnumCase } @@ -14414,6 +14725,10 @@ public struct FfiConverterTypeGlobalConfigTableError: FfiConverterRustBuffer { writeInt(&buf, Int32(2)) FfiConverterString.write(v1, into: &buf) + + case .PinCodeMustBeHashed: + writeInt(&buf, Int32(3)) + } } } @@ -21325,6 +21640,21 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_amount_sats_string_with_unit() != 34409) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_method_authpin_check() != 17948) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_cove_checksum_method_authpin_delete() != 15788) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_cove_checksum_method_authpin_hash() != 13652) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_cove_checksum_method_authpin_set() != 63469) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_cove_checksum_method_authpin_verify() != 9856) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_method_autocomplete_autocomplete() != 4748) { return InitializationResult.apiChecksumMismatch } @@ -21580,21 +21910,27 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_fingerprint_as_uppercase() != 11522) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cove_checksum_method_globalconfigtable_auth_type() != 32553) { + if (uniffi_cove_checksum_method_globalconfigtable_authtype() != 38700) { return InitializationResult.apiChecksumMismatch } if (uniffi_cove_checksum_method_globalconfigtable_clear_selected_wallet() != 22146) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cove_checksum_method_globalconfigtable_color_scheme() != 18859) { + if (uniffi_cove_checksum_method_globalconfigtable_colorscheme() != 12515) { return InitializationResult.apiChecksumMismatch } if (uniffi_cove_checksum_method_globalconfigtable_delete() != 13364) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_method_globalconfigtable_delete_hashed_pin_code() != 4238) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_method_globalconfigtable_get() != 52128) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_method_globalconfigtable_hashed_pin_code() != 15707) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_method_globalconfigtable_select_wallet() != 52001) { return InitializationResult.apiChecksumMismatch } @@ -21610,10 +21946,10 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_globalconfigtable_set() != 31033) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cove_checksum_method_globalconfigtable_set_auth_type() != 48884) { + if (uniffi_cove_checksum_method_globalconfigtable_setcolorscheme() != 57216) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cove_checksum_method_globalconfigtable_set_color_scheme() != 24086) { + if (uniffi_cove_checksum_method_globalconfigtable_set_hashed_pin_code() != 36127) { return InitializationResult.apiChecksumMismatch } if (uniffi_cove_checksum_method_globalconfigtable_set_selected_network() != 34312) { @@ -22063,6 +22399,9 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_constructor_amount_one_sat() != 58118) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_constructor_authpin_new() != 39860) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_constructor_bip39autocomplete_new() != 41839) { return InitializationResult.apiChecksumMismatch } diff --git a/ios/Cove/HomeScreens/SettingsScreen.swift b/ios/Cove/HomeScreens/SettingsScreen.swift index 83c3a489..a333afcd 100644 --- a/ios/Cove/HomeScreens/SettingsScreen.swift +++ b/ios/Cove/HomeScreens/SettingsScreen.swift @@ -1,6 +1,11 @@ import LocalAuthentication import SwiftUI +private enum SheetState: Equatable { + case newPin + case removePin +} + struct SettingsScreen: View { @Environment(AppManager.self) private var app @Environment(\.dismiss) private var dismiss @@ -9,6 +14,8 @@ struct SettingsScreen: View { @State private var networkChanged = false @State private var showConfirmationAlert = false + @State private var sheetState: TaggedItem? = nil + let themes = allColorSchemes() private func canUseBiometrics() -> Bool { @@ -20,19 +27,37 @@ struct SettingsScreen: View { var useAuth: Binding { Binding( get: { app.isAuthEnabled }, - set: { app.dispatch(action: .toggleAuth) } + set: { _ in app.dispatch(action: .toggleAuth) } ) } var useBiometric: Binding { Binding( - get: { app.authType == .both || app.authType == .biometric }, - set: { app.dispatch(action: .toggleBiometric) } + get: { app.authType == AuthType.both || app.authType == AuthType.biometric }, + set: { _ in app.dispatch(action: .toggleBiometric) } + ) + } + + var usePin: Binding { + Binding( + get: { app.authType == AuthType.both || app.authType == AuthType.pin }, + set: { enabled in + if enabled { sheetState = .init(.removePin) } else { sheetState = .init(.newPin) } + } ) } var body: some View { Form { + Section(header: Text("About")) { + HStack { + Text("Version") + Spacer() + Text("0.0.0") + .foregroundColor(.secondary) + } + } + Section(header: Text("Network")) { Picker( "Network", @@ -75,25 +100,22 @@ struct SettingsScreen: View { Label("Require Authentication", systemImage: "lock.shield") } - if useAuth { + if app.isAuthEnabled { if canUseBiometrics() { - Toggle(isOn: $useBiometric) { + Toggle(isOn: useBiometric) { Label("Enable Face ID", systemImage: "faceid") } } - Toggle(isOn: $usePIN) { - Label("Enable PIN", systemImage: "key.fill") + Toggle(isOn: usePin) { + Label("Enable PIN", systemImage: "lock.fill") } - } - } - Section(header: Text("About")) { - HStack { - Text("Version") - Spacer() - Text("0.0.0") - .foregroundColor(.secondary) + if usePin.wrappedValue { + Button(action: {}) { + Label("Change PIN", systemImage: "lock.open.rotation") + } + } } } } @@ -131,6 +153,7 @@ struct SettingsScreen: View { secondaryButton: .cancel(Text("Cancel")) ) } + .fullScreenCover(item: $sheetState, content: SheetContent) .preferredColorScheme(app.colorScheme) .gesture( networkChanged @@ -151,6 +174,16 @@ struct SettingsScreen: View { } : nil ) } + + @ViewBuilder + private func SheetContent(_ state: TaggedItem) -> some View { + switch state.item { + case .newPin: + EmptyView() + case .removePin: + EmptyView() + } + } } #Preview { diff --git a/rust/src/app.rs b/rust/src/app.rs index 57e4f9a2..545012f9 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::AuthType, + auth::{AuthPin, AuthType}, color_scheme::ColorSchemeSelection, database::{error::DatabaseError, Database}, fiat::client::{PriceResponse, FIAT_CLIENT}, @@ -219,7 +219,9 @@ impl App { } AppAction::SetPin(pin) => { - todo!("HASH and SAVE PIN {pin}",); + if let Err(err) = AuthPin::new().set(pin) { + error!("unable to set pin: {err:?}"); + } } AppAction::DisablePin => { @@ -227,7 +229,9 @@ impl App { match current_auth_type { AuthType::Pin => set_auth_type(AuthType::None), AuthType::Both => set_auth_type(AuthType::Biometric), - AuthType::None | AuthType::Biometric => {} + AuthType::None | AuthType::Biometric => { + AuthPin::new().delete().unwrap_or_default(); + } } } } diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 261b7406..0ccf66b8 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -55,7 +55,7 @@ pub enum AuthError { } #[derive(Debug, Clone, Hash, Eq, PartialEq, uniffi::Object)] -pub struct AuthPin {} +pub struct AuthPin; #[uniffi::export] impl AuthPin { diff --git a/rust/src/database/global_config.rs b/rust/src/database/global_config.rs index ce17410a..86732174 100644 --- a/rust/src/database/global_config.rs +++ b/rust/src/database/global_config.rs @@ -165,9 +165,14 @@ impl GlobalConfigTable { Ok(()) } + #[uniffi::method(name = "authType")] + pub fn _auth_type(&self) -> AuthType { + self.auth_type().unwrap_or_default() + } + #[uniffi::method(name = "colorScheme")] - pub fn _color_scheme(&self) -> Result { - self.color_scheme() + pub fn _color_scheme(&self) -> ColorSchemeSelection { + self.color_scheme().unwrap_or_default() } #[uniffi::method(name = "setColorScheme")] diff --git a/rust/src/database/macros.rs b/rust/src/database/macros.rs index 7bb29da2..70b89914 100644 --- a/rust/src/database/macros.rs +++ b/rust/src/database/macros.rs @@ -46,6 +46,6 @@ macro_rules! string_config_accessor { // Private interface ($fn_name:ident, $key:expr, $return_type:ty, $update_variant:path) => { - string_config_accessor!(@impl , $fn_name, $key, $return_type, $update_variant); + string_config_accessor!(@impl, $fn_name, $key, $return_type, $update_variant); }; } From 09a1e32fbb2b05503ca6d7eee933abe9d2e1e090 Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Wed, 11 Dec 2024 19:14:24 -0600 Subject: [PATCH 09/16] Extract out NumberPadPinView as a separate View --- ios/Cove/Views/LockView.swift | 140 ++------------------- ios/Cove/Views/NumberPadPinView.swift | 175 ++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 131 deletions(-) create mode 100644 ios/Cove/Views/NumberPadPinView.swift diff --git a/ios/Cove/Views/LockView.swift b/ios/Cove/Views/LockView.swift index 20b5f99f..c8d7f471 100644 --- a/ios/Cove/Views/LockView.swift +++ b/ios/Cove/Views/LockView.swift @@ -10,7 +10,7 @@ import SwiftUI struct LockView: View { /// Args: Lock Properties - var lockType: LockType + var lockType: AuthType var isPinCorrect: (String) -> Bool var isEnabled: Bool var lockWhenBackground: Bool = true @@ -81,8 +81,14 @@ struct LockView: View { } } } else { - /// Custom Number Pad to type View Lock Pin - NumberPadPinView() + NumberPadPinView( + pin: $pin, + isUnlocked: $isUnlocked, + noBiometricAccess: $noBiometricAccess, + isPinCorrect: isPinCorrect, + lockType: lockType, + pinLength: pinLength + ) } } .environment(\.colorScheme, .dark) @@ -142,134 +148,6 @@ struct LockView: View { let context = LAContext() return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) } - - /// Numberpad Pin View - @ViewBuilder - private func NumberPadPinView() -> some View { - VStack(spacing: 15) { - Text("Enter Pin") - .font(.title.bold()) - .frame(maxWidth: .infinity) - .overlay(alignment: .leading) { - /// Back button only for Both Lock Type - if lockType == .both, isBiometricAvailable { - Button(action: { - pin = "" - noBiometricAccess = false - }, label: { - Image(systemName: "arrow.left") - .font(.title3) - .contentShape(.rect) - }) - .tint(.white) - .padding(.leading) - } - } - - /// Adding Wiggling Animation for Wrong Password With Keyframe Animator - HStack(spacing: 10) { - ForEach(0 ..< pinLength, id: \.self) { index in - RoundedRectangle(cornerRadius: 10) - .frame(width: 40, height: 45) - /// Showing Pin at each box with the help of Index - .overlay { - /// Safe Check - if pin.count > index { - let index = pin.index(pin.startIndex, offsetBy: index) - let string = String(pin[index]) - - Text(string) - .font(.title.bold()) - .foregroundStyle(.black) - } - } - } - } - .keyframeAnimator( - initialValue: CGFloat.zero, - trigger: animateField, - content: { content, value in - content - .offset(x: value) - }, - keyframes: { _ in - KeyframeTrack { - CubicKeyframe(30, duration: 0.07) - CubicKeyframe(-30, duration: 0.07) - CubicKeyframe(20, duration: 0.07) - CubicKeyframe(-20, duration: 0.07) - CubicKeyframe(10, duration: 0.07) - CubicKeyframe(-10, duration: 0.07) - CubicKeyframe(0, duration: 0.07) - } - } - ) - .padding(.top, 15) - .frame(maxHeight: .infinity) - - /// Custom Number Pad - GeometryReader { _ in - LazyVGrid(columns: Array(repeating: GridItem(), count: 3), content: { - ForEach(1 ... 9, id: \.self) { number in - Button(action: { - guard pin.count < pinLength else { return } - pin.append(String(number)) - }, label: { - Text(String(number)) - .font(.title) - .frame(maxWidth: .infinity) - .padding(.vertical, 20) - .contentShape(.rect) - }) - .tint(.white) - } - - /// 0 and Back Button - Button(action: { - if !pin.isEmpty { pin.removeLast() } - }, label: { - Image(systemName: "delete.backward") - .font(.title) - .frame(maxWidth: .infinity) - .padding(.vertical, 20) - .contentShape(.rect) - }) - .tint(.white) - - Button(action: { - guard pin.count < pinLength else { return } - pin.append("0") - }, label: { - Text("0") - .font(.title) - .frame(maxWidth: .infinity) - .padding(.vertical, 20) - .contentShape(.rect) - }) - .tint(.white) - }) - .frame(maxHeight: .infinity, alignment: .bottom) - } - .onChange(of: pin) { _, newValue in - if newValue.count == pinLength { - /// Validate Pin - if isPinCorrect(pin) { - withAnimation(.snappy, completionCriteria: .logicallyComplete) { - isUnlocked = true - } completion: { - pin = "" - noBiometricAccess = !isBiometricAvailable - } - } else { - pin = "" - animateField.toggle() - } - } - } - } - .padding() - .environment(\.colorScheme, .dark) - } } #Preview { diff --git a/ios/Cove/Views/NumberPadPinView.swift b/ios/Cove/Views/NumberPadPinView.swift new file mode 100644 index 00000000..4c67bebe --- /dev/null +++ b/ios/Cove/Views/NumberPadPinView.swift @@ -0,0 +1,175 @@ +// +// NumberPadPinView.swift +// Cove +// +// Created by Praveen Perera on 12/11/24. +// +import LocalAuthentication +import SwiftUI + +struct NumberPadPinView: View { + /// args + @Binding var pin: String + @Binding var isUnlocked: Bool + @Binding var noBiometricAccess: Bool + + let isPinCorrect: (String) -> Bool + let lockType: AuthType + let pinLength: Int + + /// private view properties + @State private var animateField: Bool = false + + private var isBiometricAvailable: Bool { + /// Lock Context + let context = LAContext() + return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) + } + + var body: some View { + VStack(spacing: 15) { + Text("Enter Pin") + .font(.title.bold()) + .frame(maxWidth: .infinity) + .overlay(alignment: .leading) { + /// Back button only for Both Lock Type + if lockType == .both, isBiometricAvailable { + Button(action: { + pin = "" + noBiometricAccess = false + }, label: { + Image(systemName: "arrow.left") + .font(.title3) + .contentShape(.rect) + }) + .tint(.white) + .padding(.leading) + } + } + + /// Adding Wiggling Animation for Wrong Password With Keyframe Animator + HStack(spacing: 10) { + ForEach(0 ..< pinLength, id: \.self) { index in + RoundedRectangle(cornerRadius: 10) + .frame(width: 40, height: 45) + /// Showing Pin at each box with the help of Index + .overlay { + /// Safe Check + if pin.count > index { + let index = pin.index(pin.startIndex, offsetBy: index) + let string = String(pin[index]) + + Text(string) + .font(.title.bold()) + .foregroundStyle(.black) + } + } + } + } + .keyframeAnimator( + initialValue: CGFloat.zero, + trigger: animateField, + content: { content, value in + content + .offset(x: value) + }, + keyframes: { _ in + KeyframeTrack { + CubicKeyframe(30, duration: 0.07) + CubicKeyframe(-30, duration: 0.07) + CubicKeyframe(20, duration: 0.07) + CubicKeyframe(-20, duration: 0.07) + CubicKeyframe(10, duration: 0.07) + CubicKeyframe(-10, duration: 0.07) + CubicKeyframe(0, duration: 0.07) + } + } + ) + .padding(.top, 15) + .frame(maxHeight: .infinity) + + /// Custom Number Pad + GeometryReader { _ in + LazyVGrid(columns: Array(repeating: GridItem(), count: 3), content: { + ForEach(1 ... 9, id: \.self) { number in + Button(action: { + guard pin.count < pinLength else { return } + pin.append(String(number)) + }, label: { + Text(String(number)) + .font(.title) + .frame(maxWidth: .infinity) + .padding(.vertical, 20) + .contentShape(.rect) + }) + .tint(.white) + } + + /// 0 and Back Button + Button(action: { + if !pin.isEmpty { pin.removeLast() } + }, label: { + Image(systemName: "delete.backward") + .font(.title) + .frame(maxWidth: .infinity) + .padding(.vertical, 20) + .contentShape(.rect) + }) + .tint(.white) + + Button(action: { + guard pin.count < pinLength else { return } + pin.append("0") + }, label: { + Text("0") + .font(.title) + .frame(maxWidth: .infinity) + .padding(.vertical, 20) + .contentShape(.rect) + }) + .tint(.white) + }) + .frame(maxHeight: .infinity, alignment: .bottom) + } + .onChange(of: pin) { _, newValue in + if newValue.count == pinLength { + /// Validate Pin + if isPinCorrect(pin) { + withAnimation(.snappy, completionCriteria: .logicallyComplete) { + isUnlocked = true + } completion: { + pin = "" + noBiometricAccess = !isBiometricAvailable + } + } else { + pin = "" + animateField.toggle() + } + } + } + } + .padding() + .environment(\.colorScheme, .dark) + } +} + +#Preview { + struct Container: View { + @State var pin = "" + @State var noBiometricAccess = true + @State var isUnlocked = false + + var body: some View { + NumberPadPinView( + pin: $pin, + isUnlocked: $isUnlocked, + noBiometricAccess: $noBiometricAccess, + isPinCorrect: { $0 == "000000" }, + lockType: .pin, + pinLength: 6 + ) + } + } + + return Container() +} From a620bd204366c1831aadc66e96a6b9bba6020110 Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Thu, 12 Dec 2024 11:16:00 -0600 Subject: [PATCH 10/16] Add success and failure callbacks for lock and pin views --- ios/Cove/Views/LockView.swift | 22 +++++++++++++++++----- ios/Cove/Views/NumberPadPinView.swift | 12 ++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/ios/Cove/Views/LockView.swift b/ios/Cove/Views/LockView.swift index c8d7f471..6c414dbb 100644 --- a/ios/Cove/Views/LockView.swift +++ b/ios/Cove/Views/LockView.swift @@ -15,6 +15,11 @@ struct LockView: View { var isEnabled: Bool var lockWhenBackground: Bool = true var bioMetricUnlockMessage: String = "Unlock your wallet" + + /// default calllbacks on success and failure + let onUnlock: () -> Void = {} + let onWrongPin: () -> Void = {} + @ViewBuilder var content: Content /// View Properties @@ -87,7 +92,12 @@ struct LockView: View { noBiometricAccess: $noBiometricAccess, isPinCorrect: isPinCorrect, lockType: lockType, - pinLength: pinLength + pinLength: pinLength, + onUnlock: { + unlockView() + onUnlock() + }, + onWrongPin: onWrongPin ) } } @@ -96,9 +106,7 @@ struct LockView: View { } } .onChange(of: isEnabled, initial: true) { _, newValue in - if newValue { - unlockView() - } + if newValue { unlockView() } } /// Locking When App Goes Background .onChange(of: phase) { _, newValue in @@ -151,7 +159,11 @@ struct LockView: View { } #Preview { - LockView(lockType: .both, isPinCorrect: { $0 == "111111" }, isEnabled: true) { + LockView( + lockType: .both, + isPinCorrect: { $0 == "111111" }, + isEnabled: true + ) { VStack { Text("Hello World") } diff --git a/ios/Cove/Views/NumberPadPinView.swift b/ios/Cove/Views/NumberPadPinView.swift index 4c67bebe..696e05be 100644 --- a/ios/Cove/Views/NumberPadPinView.swift +++ b/ios/Cove/Views/NumberPadPinView.swift @@ -17,6 +17,10 @@ struct NumberPadPinView: View { let lockType: AuthType let pinLength: Int + /// default calllbacks on success and failure + let onUnlock: () -> Void = {} + let onWrongPin: () -> Void = {} + /// private view properties @State private var animateField: Bool = false @@ -85,6 +89,13 @@ struct NumberPadPinView: View { } } ) + /// run onEnd call back after keyframe animation + .onChange(of: animateField) { _, _ in + let totalDuration = 7 * 0.07 + DispatchQueue.main.asyncAfter(deadline: .now() + totalDuration) { + onWrongPin() + } + } .padding(.top, 15) .frame(maxHeight: .infinity) @@ -140,6 +151,7 @@ struct NumberPadPinView: View { } completion: { pin = "" noBiometricAccess = !isBiometricAvailable + onUnlock() } } else { pin = "" From 871bfa488c6e555406765e1b26c282330e792ce6 Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Thu, 12 Dec 2024 11:37:28 -0600 Subject: [PATCH 11/16] Refactor LockView make it cleaner less nesting --- ios/Cove/Views/LockView.swift | 112 ++++++++++++++------------ ios/Cove/Views/NumberPadPinView.swift | 4 +- 2 files changed, 61 insertions(+), 55 deletions(-) diff --git a/ios/Cove/Views/LockView.swift b/ios/Cove/Views/LockView.swift index 6c414dbb..c8113415 100644 --- a/ios/Cove/Views/LockView.swift +++ b/ios/Cove/Views/LockView.swift @@ -48,57 +48,9 @@ struct LockView: View { .ignoresSafeArea() if (lockType == .both && !noBiometricAccess) || lockType == .biometric { - Group { - if noBiometricAccess { - Text("Enable biometric authentication in Settings to unlock the view.") - .font(.callout) - .multilineTextAlignment(.center) - .padding(50) - } else { - /// Bio Metric / Pin Unlock - VStack(spacing: 12) { - VStack(spacing: 6) { - Image(systemName: "faceid") - .font(.largeTitle) - - Text("Tap to Unlock") - .font(.caption2) - .foregroundStyle(.gray) - } - .frame(width: 100, height: 100) - .background(.ultraThinMaterial, in: .rect(cornerRadius: 10)) - .contentShape(.rect) - .onTapGesture { - unlockView() - } - - if lockType == .both { - Text("Enter Pin") - .frame(width: 100, height: 40) - .background(.ultraThinMaterial, - in: .rect(cornerRadius: 10)) - .contentShape(.rect) - .onTapGesture { - noBiometricAccess = true - } - } - } - } - } + PinOrBioMetric } else { - NumberPadPinView( - pin: $pin, - isUnlocked: $isUnlocked, - noBiometricAccess: $noBiometricAccess, - isPinCorrect: isPinCorrect, - lockType: lockType, - pinLength: pinLength, - onUnlock: { - unlockView() - onUnlock() - }, - onWrongPin: onWrongPin - ) + numberPadPinView } } .environment(\.colorScheme, .dark) @@ -106,7 +58,7 @@ struct LockView: View { } } .onChange(of: isEnabled, initial: true) { _, newValue in - if newValue { unlockView() } + if newValue { tryUnlockingView() } } /// Locking When App Goes Background .onChange(of: phase) { _, newValue in @@ -117,6 +69,58 @@ struct LockView: View { } } + var numberPadPinView: NumberPadPinView { + NumberPadPinView( + pin: $pin, + isUnlocked: $isUnlocked, + noBiometricAccess: $noBiometricAccess, + isPinCorrect: isPinCorrect, + lockType: lockType, + pinLength: pinLength, + onUnlock: onUnlock, + onWrongPin: onWrongPin + ) + } + + @ViewBuilder + var PinOrBioMetric: some View { + Group { + if noBiometricAccess { + Text("Enable biometric authentication in Settings to unlock the view.") + .font(.callout) + .multilineTextAlignment(.center) + .padding(50) + } else { + /// Bio Metric / Pin Unlock + VStack(spacing: 12) { + VStack(spacing: 6) { + Image(systemName: "faceid") + .font(.largeTitle) + + Text("Tap to Unlock") + .font(.caption2) + .foregroundStyle(.gray) + } + .frame(width: 100, height: 100) + .background(.ultraThinMaterial, in: .rect(cornerRadius: 10)) + .contentShape(.rect) + .onTapGesture { tryUnlockingView() } + + if lockType == .both { + Text("Enter Pin") + .frame(width: 100, height: 40) + .background( + .ultraThinMaterial, + in: .rect(cornerRadius: 10) + ) + .contentShape(.rect) + .onTapGesture { noBiometricAccess = true } + } + } + } + } + } + private func bioMetricUnlock() async throws -> Bool { /// Lock Context let context = LAContext() @@ -127,7 +131,7 @@ struct LockView: View { ) } - private func unlockView() { + private func tryUnlockingView() { /// Checking and Unlocking View Task { guard isBiometricAvailable, lockType != .pin else { @@ -145,7 +149,9 @@ struct LockView: View { completionCriteria: .logicallyComplete ) { isUnlocked = true - } completion: { pin = "" } + } completion: { + pin = "" + } } } } diff --git a/ios/Cove/Views/NumberPadPinView.swift b/ios/Cove/Views/NumberPadPinView.swift index 696e05be..ecf62ea3 100644 --- a/ios/Cove/Views/NumberPadPinView.swift +++ b/ios/Cove/Views/NumberPadPinView.swift @@ -18,8 +18,8 @@ struct NumberPadPinView: View { let pinLength: Int /// default calllbacks on success and failure - let onUnlock: () -> Void = {} - let onWrongPin: () -> Void = {} + var onUnlock: () -> Void = {} + var onWrongPin: () -> Void = {} /// private view properties @State private var animateField: Bool = false From 07e8945f857f4be1dca377bbc0bad7784538f6cc Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Thu, 12 Dec 2024 12:08:18 -0600 Subject: [PATCH 12/16] Complete `NewPinView` --- ios/Cove/SettingsScreen/NewPinView.swift | 56 ++++++++++++++++++++++++ ios/Cove/Views/LockView.swift | 6 +-- ios/Cove/Views/NumberPadPinView.swift | 42 ++++++------------ 3 files changed, 71 insertions(+), 33 deletions(-) create mode 100644 ios/Cove/SettingsScreen/NewPinView.swift diff --git a/ios/Cove/SettingsScreen/NewPinView.swift b/ios/Cove/SettingsScreen/NewPinView.swift new file mode 100644 index 00000000..eeb88120 --- /dev/null +++ b/ios/Cove/SettingsScreen/NewPinView.swift @@ -0,0 +1,56 @@ +// +// NewPinView.swift +// Cove +// +// Created by Praveen Perera on 12/12/24. +// + +import SwiftUI + +enum PinState { + case new, confirm(String) +} + +struct NewPinView: View { + /// args + var onComplete: (String) -> Void = { _ in } + + /// private + @State private var pin = "" + @State private var pinState: PinState = .new + + var body: some View { + Group { + switch pinState { + case .new: + NumberPadPinView( + pin: $pin, + isUnlocked: Binding.constant(false), + isPinCorrect: { _ in true }, + onUnlock: { enteredPin in + withAnimation { + pinState = .confirm(enteredPin) + pin = "" + } + } + ) + case .confirm(let pinToConfirm): + NumberPadPinView( + title: "Confirm Pin", + pin: $pin, + isUnlocked: .constant(false), + isPinCorrect: { + $0 == pinToConfirm + }, + onUnlock: { _ in + onComplete(pinToConfirm) + } + ) + } + } + } +} + +#Preview { + NewPinView() +} diff --git a/ios/Cove/Views/LockView.swift b/ios/Cove/Views/LockView.swift index c8113415..865318b7 100644 --- a/ios/Cove/Views/LockView.swift +++ b/ios/Cove/Views/LockView.swift @@ -17,8 +17,8 @@ struct LockView: View { var bioMetricUnlockMessage: String = "Unlock your wallet" /// default calllbacks on success and failure - let onUnlock: () -> Void = {} - let onWrongPin: () -> Void = {} + let onUnlock: (String) -> Void = { _ in } + let onWrongPin: (String) -> Void = { _ in } @ViewBuilder var content: Content @@ -73,9 +73,7 @@ struct LockView: View { NumberPadPinView( pin: $pin, isUnlocked: $isUnlocked, - noBiometricAccess: $noBiometricAccess, isPinCorrect: isPinCorrect, - lockType: lockType, pinLength: pinLength, onUnlock: onUnlock, onWrongPin: onWrongPin diff --git a/ios/Cove/Views/NumberPadPinView.swift b/ios/Cove/Views/NumberPadPinView.swift index ecf62ea3..945a77c1 100644 --- a/ios/Cove/Views/NumberPadPinView.swift +++ b/ios/Cove/Views/NumberPadPinView.swift @@ -9,17 +9,16 @@ import SwiftUI struct NumberPadPinView: View { /// args + var title: String = "Enter Pin" @Binding var pin: String @Binding var isUnlocked: Bool - @Binding var noBiometricAccess: Bool let isPinCorrect: (String) -> Bool - let lockType: AuthType - let pinLength: Int + var pinLength: Int = 6 /// default calllbacks on success and failure - var onUnlock: () -> Void = {} - var onWrongPin: () -> Void = {} + var onUnlock: (String) -> Void = { _ in } + var onWrongPin: (String) -> Void = { _ in } /// private view properties @State private var animateField: Bool = false @@ -32,30 +31,17 @@ struct NumberPadPinView: View { var body: some View { VStack(spacing: 15) { - Text("Enter Pin") + Text(title) .font(.title.bold()) .frame(maxWidth: .infinity) - .overlay(alignment: .leading) { - /// Back button only for Both Lock Type - if lockType == .both, isBiometricAvailable { - Button(action: { - pin = "" - noBiometricAccess = false - }, label: { - Image(systemName: "arrow.left") - .font(.title3) - .contentShape(.rect) - }) - .tint(.white) - .padding(.leading) - } - } + .foregroundStyle(.white) /// Adding Wiggling Animation for Wrong Password With Keyframe Animator HStack(spacing: 10) { ForEach(0 ..< pinLength, id: \.self) { index in RoundedRectangle(cornerRadius: 10) .frame(width: 40, height: 45) + .foregroundStyle(.white) /// Showing Pin at each box with the help of Index .overlay { /// Safe Check @@ -91,9 +77,12 @@ struct NumberPadPinView: View { ) /// run onEnd call back after keyframe animation .onChange(of: animateField) { _, _ in + let pin = pin + self.pin = "" + let totalDuration = 7 * 0.07 DispatchQueue.main.asyncAfter(deadline: .now() + totalDuration) { - onWrongPin() + onWrongPin(pin) } } .padding(.top, 15) @@ -149,35 +138,30 @@ struct NumberPadPinView: View { withAnimation(.snappy, completionCriteria: .logicallyComplete) { isUnlocked = true } completion: { + onUnlock(pin) pin = "" - noBiometricAccess = !isBiometricAvailable - onUnlock() } } else { - pin = "" animateField.toggle() } } } } .padding() - .environment(\.colorScheme, .dark) + .background(.midnightBlue) } } #Preview { struct Container: View { @State var pin = "" - @State var noBiometricAccess = true @State var isUnlocked = false var body: some View { NumberPadPinView( pin: $pin, isUnlocked: $isUnlocked, - noBiometricAccess: $noBiometricAccess, isPinCorrect: { $0 == "000000" }, - lockType: .pin, pinLength: 6 ) } From 1e49df737837ad1b615673c1c44e2ce731f91272 Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Thu, 12 Dec 2024 12:11:35 -0600 Subject: [PATCH 13/16] Handle tracking PIN internally --- ios/Cove/SettingsScreen/NewPinView.swift | 4 ---- ios/Cove/Views/LockView.swift | 10 +--------- ios/Cove/Views/NumberPadPinView.swift | 3 +-- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/ios/Cove/SettingsScreen/NewPinView.swift b/ios/Cove/SettingsScreen/NewPinView.swift index eeb88120..aa76a200 100644 --- a/ios/Cove/SettingsScreen/NewPinView.swift +++ b/ios/Cove/SettingsScreen/NewPinView.swift @@ -16,7 +16,6 @@ struct NewPinView: View { var onComplete: (String) -> Void = { _ in } /// private - @State private var pin = "" @State private var pinState: PinState = .new var body: some View { @@ -24,20 +23,17 @@ struct NewPinView: View { switch pinState { case .new: NumberPadPinView( - pin: $pin, isUnlocked: Binding.constant(false), isPinCorrect: { _ in true }, onUnlock: { enteredPin in withAnimation { pinState = .confirm(enteredPin) - pin = "" } } ) case .confirm(let pinToConfirm): NumberPadPinView( title: "Confirm Pin", - pin: $pin, isUnlocked: .constant(false), isPinCorrect: { $0 == pinToConfirm diff --git a/ios/Cove/Views/LockView.swift b/ios/Cove/Views/LockView.swift index 865318b7..b15c5f15 100644 --- a/ios/Cove/Views/LockView.swift +++ b/ios/Cove/Views/LockView.swift @@ -23,7 +23,6 @@ struct LockView: View { @ViewBuilder var content: Content /// View Properties - @State private var pin: String = "" @State private var animateField: Bool = false @State private var isUnlocked: Bool = false @State private var noBiometricAccess: Bool = false @@ -64,14 +63,12 @@ struct LockView: View { .onChange(of: phase) { _, newValue in if newValue != .active, lockWhenBackground { isUnlocked = false - pin = "" } } } var numberPadPinView: NumberPadPinView { NumberPadPinView( - pin: $pin, isUnlocked: $isUnlocked, isPinCorrect: isPinCorrect, pinLength: pinLength, @@ -142,13 +139,8 @@ struct LockView: View { /// Requesting Biometric Unlock if await (try? bioMetricUnlock()) ?? false { await MainActor.run { - withAnimation( - .snappy, - completionCriteria: .logicallyComplete - ) { + withAnimation(.snappy) { isUnlocked = true - } completion: { - pin = "" } } } diff --git a/ios/Cove/Views/NumberPadPinView.swift b/ios/Cove/Views/NumberPadPinView.swift index 945a77c1..a8b48f01 100644 --- a/ios/Cove/Views/NumberPadPinView.swift +++ b/ios/Cove/Views/NumberPadPinView.swift @@ -10,7 +10,6 @@ import SwiftUI struct NumberPadPinView: View { /// args var title: String = "Enter Pin" - @Binding var pin: String @Binding var isUnlocked: Bool let isPinCorrect: (String) -> Bool @@ -21,6 +20,7 @@ struct NumberPadPinView: View { var onWrongPin: (String) -> Void = { _ in } /// private view properties + @State private var pin: String = "" @State private var animateField: Bool = false private var isBiometricAvailable: Bool { @@ -159,7 +159,6 @@ struct NumberPadPinView: View { var body: some View { NumberPadPinView( - pin: $pin, isUnlocked: $isUnlocked, isPinCorrect: { $0 == "000000" }, pinLength: 6 From ce2dcf1665a86219535086bc72d1a8df90199d28 Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Thu, 12 Dec 2024 15:02:19 -0600 Subject: [PATCH 14/16] Complete enabling and disabling auth from settings --- ios/Cove/Cove.swift | 83 +++++---------- ios/Cove/HomeScreens/SettingsScreen.swift | 110 ++++++++++++++++++-- ios/Cove/SettingsScreen/ChangePinView.swift | 66 ++++++++++++ ios/Cove/SettingsScreen/NewPinView.swift | 22 ++-- ios/Cove/Views/LockView.swift | 56 ++++++++-- ios/Cove/Views/NumberPadPinView.swift | 50 +++++++-- rust/src/app.rs | 57 ++++++---- rust/src/auth.rs | 39 ++++--- 8 files changed, 348 insertions(+), 135 deletions(-) create mode 100644 ios/Cove/SettingsScreen/ChangePinView.swift diff --git a/ios/Cove/Cove.swift b/ios/Cove/Cove.swift index 8d8bc0ff..5f5aabd9 100644 --- a/ios/Cove/Cove.swift +++ b/ios/Cove/Cove.swift @@ -1236,14 +1236,6 @@ public protocol AuthPinProtocol : AnyObject { func check(pin: String) -> Bool - func delete() throws - - func hash(pin: String) throws -> String - - func set(pin: String) throws - - func verify(pin: String, hashedPin: String) throws - } open class AuthPin: @@ -1311,35 +1303,6 @@ open func check(pin: String) -> Bool { }) } -open func delete()throws {try rustCallWithError(FfiConverterTypeAuthError.lift) { - uniffi_cove_fn_method_authpin_delete(self.uniffiClonePointer(),$0 - ) -} -} - -open func hash(pin: String)throws -> String { - return try FfiConverterString.lift(try rustCallWithError(FfiConverterTypeAuthError.lift) { - uniffi_cove_fn_method_authpin_hash(self.uniffiClonePointer(), - FfiConverterString.lower(pin),$0 - ) -}) -} - -open func set(pin: String)throws {try rustCallWithError(FfiConverterTypeAuthError.lift) { - uniffi_cove_fn_method_authpin_set(self.uniffiClonePointer(), - FfiConverterString.lower(pin),$0 - ) -} -} - -open func verify(pin: String, hashedPin: String)throws {try rustCallWithError(FfiConverterTypeAuthError.lift) { - uniffi_cove_fn_method_authpin_verify(self.uniffiClonePointer(), - FfiConverterString.lower(pin), - FfiConverterString.lower(hashedPin),$0 - ) -} -} - } @@ -12601,8 +12564,10 @@ public enum AppAction { case updateFees case updateAuthType(AuthType ) - case toggleAuth - case toggleBiometric + case enableAuth + case disableAuth + case enableBiometric + case disableBiometric case setPin(String ) case disablePin @@ -12638,14 +12603,18 @@ public struct FfiConverterTypeAppAction: FfiConverterRustBuffer { case 7: return .updateAuthType(try FfiConverterTypeAuthType.read(from: &buf) ) - case 8: return .toggleAuth + case 8: return .enableAuth + + case 9: return .disableAuth + + case 10: return .enableBiometric - case 9: return .toggleBiometric + case 11: return .disableBiometric - case 10: return .setPin(try FfiConverterString.read(from: &buf) + case 12: return .setPin(try FfiConverterString.read(from: &buf) ) - case 11: return .disablePin + case 13: return .disablePin default: throw UniffiInternalError.unexpectedEnumCase } @@ -12688,21 +12657,29 @@ public struct FfiConverterTypeAppAction: FfiConverterRustBuffer { FfiConverterTypeAuthType.write(v1, into: &buf) - case .toggleAuth: + case .enableAuth: writeInt(&buf, Int32(8)) - case .toggleBiometric: + case .disableAuth: writeInt(&buf, Int32(9)) - case let .setPin(v1): + 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(11)) + writeInt(&buf, Int32(13)) } } @@ -21643,18 +21620,6 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_authpin_check() != 17948) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cove_checksum_method_authpin_delete() != 15788) { - return InitializationResult.apiChecksumMismatch - } - if (uniffi_cove_checksum_method_authpin_hash() != 13652) { - return InitializationResult.apiChecksumMismatch - } - if (uniffi_cove_checksum_method_authpin_set() != 63469) { - return InitializationResult.apiChecksumMismatch - } - if (uniffi_cove_checksum_method_authpin_verify() != 9856) { - return InitializationResult.apiChecksumMismatch - } if (uniffi_cove_checksum_method_autocomplete_autocomplete() != 4748) { return InitializationResult.apiChecksumMismatch } diff --git a/ios/Cove/HomeScreens/SettingsScreen.swift b/ios/Cove/HomeScreens/SettingsScreen.swift index a333afcd..e459a802 100644 --- a/ios/Cove/HomeScreens/SettingsScreen.swift +++ b/ios/Cove/HomeScreens/SettingsScreen.swift @@ -2,8 +2,7 @@ import LocalAuthentication import SwiftUI private enum SheetState: Equatable { - case newPin - case removePin + case newPin, removePin, changePin, disableAuth, disableBiometric, enableAuth } struct SettingsScreen: View { @@ -27,22 +26,30 @@ struct SettingsScreen: View { var useAuth: Binding { Binding( get: { app.isAuthEnabled }, - set: { _ in app.dispatch(action: .toggleAuth) } + set: { enable in + if enable { return sheetState = .init(.enableAuth) } + + switch app.authType { + case .both, .pin: sheetState = .init(.removePin) + case .biometric: sheetState = .init(.disableAuth) + case .none: Log.error("Trying to disable auth when auth is not enabled") + } + } ) } var useBiometric: Binding { Binding( get: { app.authType == AuthType.both || app.authType == AuthType.biometric }, - set: { _ in app.dispatch(action: .toggleBiometric) } + set: { _ in sheetState = .init(.disableBiometric) } ) } var usePin: Binding { Binding( get: { app.authType == AuthType.both || app.authType == AuthType.pin }, - set: { enabled in - if enabled { sheetState = .init(.removePin) } else { sheetState = .init(.newPin) } + set: { turnOn in + if turnOn { sheetState = .init(.newPin) } else { sheetState = .init(.removePin) } } ) } @@ -112,7 +119,7 @@ struct SettingsScreen: View { } if usePin.wrappedValue { - Button(action: {}) { + Button(action: { sheetState = .init(.changePin) }) { Label("Change PIN", systemImage: "lock.open.rotation") } } @@ -175,13 +182,98 @@ struct SettingsScreen: View { ) } + func setPin(_ pin: String) { + app.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 { + HStack { + Spacer() + + Button("Cancel") { + sheetState = .none + } + .foregroundStyle(.white) + .font(.headline) + } + .padding() + + content() + } + .background(.midnightBlue) + } + @ViewBuilder 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() } + ) + case .newPin: - EmptyView() + NewPinView(onComplete: setPin, backAction: { sheetState = .none }) + case .removePin: - EmptyView() + NumberPadPinView( + title: "Enter Current PIN", + isPinCorrect: checkPin, + backAction: { sheetState = .none }, + onUnlock: { _ in + app.dispatch(action: .disablePin) + sheetState = .none + } + ) + + case .changePin: + ChangePinView( + isPinCorrect: checkPin, + backAction: { sheetState = .none }, + onComplete: setPin + ) + + case .disableAuth: + LockView( + lockType: app.authType, + isPinCorrect: checkPin, + onUnlock: { _ in + app.dispatch(action: .disableAuth) + sheetState = .none + }, + backAction: { sheetState = .none }, + content: { EmptyView() } + ) + + case .disableBiometric: + LockView( + lockType: app.authType, + isPinCorrect: checkPin, + onUnlock: { _ in + app.dispatch(action: .disableBiometric) + sheetState = .none + }, + backAction: { sheetState = .none }, + content: { EmptyView() } + ) } } } diff --git a/ios/Cove/SettingsScreen/ChangePinView.swift b/ios/Cove/SettingsScreen/ChangePinView.swift new file mode 100644 index 00000000..01efc4f7 --- /dev/null +++ b/ios/Cove/SettingsScreen/ChangePinView.swift @@ -0,0 +1,66 @@ +// +// ChangePinView.swift +// Cove +// +// Created by Praveen Perera on 12/12/24. +// + +import SwiftUI + +private enum PinState { + case current, new, confirm(String) +} + +struct ChangePinView: View { + /// args + var isPinCorrect: (String) -> Bool + var backAction: () -> Void + var onComplete: (String) -> Void + + /// private + @State private var pinState: PinState = .current + + var body: some View { + Group { + switch pinState { + case .current: + NumberPadPinView( + title: "Enter Current PIN", + backAction: backAction, + onUnlock: { _ in + withAnimation { + pinState = .new + } + } + ) + + case .new: + NumberPadPinView( + title: "Enter new PIN", + backAction: backAction, + onUnlock: { enteredPin in + withAnimation { + pinState = .confirm(enteredPin) + } + } + ) + + case .confirm(let pinToConfirm): + NumberPadPinView( + title: "Confirm New PIN", + isPinCorrect: { $0 == pinToConfirm }, + backAction: backAction, + onUnlock: onComplete + ) + } + } + } +} + +#Preview { + ChangePinView( + isPinCorrect: { pin in pin == "111111" }, + backAction: {}, + onComplete: { _ in } + ) +} diff --git a/ios/Cove/SettingsScreen/NewPinView.swift b/ios/Cove/SettingsScreen/NewPinView.swift index aa76a200..56421c25 100644 --- a/ios/Cove/SettingsScreen/NewPinView.swift +++ b/ios/Cove/SettingsScreen/NewPinView.swift @@ -7,13 +7,14 @@ import SwiftUI -enum PinState { +private enum PinState { case new, confirm(String) } struct NewPinView: View { /// args - var onComplete: (String) -> Void = { _ in } + var onComplete: (String) -> Void + var backAction: () -> Void /// private @State private var pinState: PinState = .new @@ -23,8 +24,9 @@ struct NewPinView: View { switch pinState { case .new: NumberPadPinView( - isUnlocked: Binding.constant(false), + title: "Enter New PIN", isPinCorrect: { _ in true }, + backAction: backAction, onUnlock: { enteredPin in withAnimation { pinState = .confirm(enteredPin) @@ -33,14 +35,10 @@ struct NewPinView: View { ) case .confirm(let pinToConfirm): NumberPadPinView( - title: "Confirm Pin", - isUnlocked: .constant(false), - isPinCorrect: { - $0 == pinToConfirm - }, - onUnlock: { _ in - onComplete(pinToConfirm) - } + title: "Confirm New PIN", + isPinCorrect: { $0 == pinToConfirm }, + backAction: backAction, + onUnlock: onComplete ) } } @@ -48,5 +46,5 @@ struct NewPinView: View { } #Preview { - NewPinView() + NewPinView(onComplete: { _ in }, backAction: {}) } diff --git a/ios/Cove/Views/LockView.swift b/ios/Cove/Views/LockView.swift index b15c5f15..280f50e4 100644 --- a/ios/Cove/Views/LockView.swift +++ b/ios/Cove/Views/LockView.swift @@ -13,26 +13,61 @@ struct LockView: View { var lockType: AuthType var isPinCorrect: (String) -> Bool var isEnabled: Bool - var lockWhenBackground: Bool = true - var bioMetricUnlockMessage: String = "Unlock your wallet" + var lockWhenBackground: Bool + var bioMetricUnlockMessage: String /// default calllbacks on success and failure - let onUnlock: (String) -> Void = { _ in } - let onWrongPin: (String) -> Void = { _ in } + var onUnlock: (String) -> Void + var onWrongPin: (String) -> Void @ViewBuilder var content: Content + /// back button + private var backEnabled: Bool + var backAction: () -> Void + /// View Properties - @State private var animateField: Bool = false - @State private var isUnlocked: Bool = false - @State private var noBiometricAccess: Bool = false + @State private var animateField: Bool + @State private var isUnlocked: Bool + @State private var noBiometricAccess: Bool /// private consts - private let pinLength = 6 + private let pinLength: Int /// Scene Phase @Environment(\.scenePhase) private var phase + init( + lockType: AuthType, + isPinCorrect: @escaping (String) -> Bool, + isEnabled: Bool = true, + lockWhenBackground: Bool = true, + bioMetricUnlockMessage: String = "Unlock your wallet", + onUnlock: @escaping (String) -> Void = { _ in }, + onWrongPin: @escaping (String) -> Void = { _ in }, + backAction: (() -> Void)? = nil, + @ViewBuilder content: () -> Content + ) { + self.lockType = lockType + self.isPinCorrect = isPinCorrect + self.isEnabled = isEnabled + self.lockWhenBackground = lockWhenBackground + self.bioMetricUnlockMessage = bioMetricUnlockMessage + self.onUnlock = onUnlock + self.onWrongPin = onWrongPin + self.content = content() + + // back + self.backEnabled = backAction != nil + self.backAction = backAction ?? {} + + // private + self.animateField = false + self.isUnlocked = false + self.noBiometricAccess = false + self.pinLength = 6 + } + var body: some View { GeometryReader { let size = $0.size @@ -72,6 +107,7 @@ struct LockView: View { isUnlocked: $isUnlocked, isPinCorrect: isPinCorrect, pinLength: pinLength, + backAction: backEnabled ? backAction : nil, onUnlock: onUnlock, onWrongPin: onWrongPin ) @@ -139,8 +175,10 @@ struct LockView: View { /// Requesting Biometric Unlock if await (try? bioMetricUnlock()) ?? false { await MainActor.run { - withAnimation(.snappy) { + withAnimation(.snappy, completionCriteria: .logicallyComplete) { isUnlocked = true + } completion: { + onUnlock("") } } } diff --git a/ios/Cove/Views/NumberPadPinView.swift b/ios/Cove/Views/NumberPadPinView.swift index a8b48f01..4869e775 100644 --- a/ios/Cove/Views/NumberPadPinView.swift +++ b/ios/Cove/Views/NumberPadPinView.swift @@ -9,20 +9,46 @@ import SwiftUI struct NumberPadPinView: View { /// args - var title: String = "Enter Pin" + var title: String @Binding var isUnlocked: Bool let isPinCorrect: (String) -> Bool - var pinLength: Int = 6 + var pinLength: Int + + // back button + private var backEnabled: Bool + var backAction: () -> Void /// default calllbacks on success and failure - var onUnlock: (String) -> Void = { _ in } - var onWrongPin: (String) -> Void = { _ in } + var onUnlock: (String) -> Void + var onWrongPin: (String) -> Void /// private view properties - @State private var pin: String = "" - @State private var animateField: Bool = false + @State private var pin: String + @State private var animateField: Bool + public init( + title: String = "Enter Pin", + isUnlocked: Binding = .constant(false), + isPinCorrect: @escaping (String) -> Bool = { _ in true }, + pinLength: Int = 6, + backAction: (() -> Void)? = nil, + onUnlock: @escaping (String) -> Void = { _ in }, + onWrongPin: @escaping (String) -> Void = { _ in } + ) { + self.title = title + self._isUnlocked = isUnlocked + self.isPinCorrect = isPinCorrect + self.pinLength = pinLength + self.backEnabled = backAction != nil + self.backAction = backAction ?? {} + self.onUnlock = onUnlock + self.onWrongPin = onWrongPin + + self.pin = "" + self.animateField = false + } + private var isBiometricAvailable: Bool { /// Lock Context let context = LAContext() @@ -31,6 +57,18 @@ struct NumberPadPinView: View { var body: some View { VStack(spacing: 15) { + if backEnabled { + HStack { + Spacer() + Button(action: backAction) { + Text("Cancel") + } + .font(.headline.bold()) + .foregroundStyle(.white) + } + .padding(.bottom, 10) + } + Text(title) .font(.title.bold()) .frame(maxWidth: .infinity) diff --git a/rust/src/app.rs b/rust/src/app.rs index 545012f9..c5a20823 100644 --- a/rust/src/app.rs +++ b/rust/src/app.rs @@ -54,8 +54,10 @@ pub enum AppAction { UpdateFiatPrices, UpdateFees, UpdateAuthType(AuthType), - ToggleAuth, - ToggleBiometric, + EnableAuth, + DisableAuth, + EnableBiometric, + DisableBiometric, SetPin(String), DisablePin, } @@ -195,43 +197,58 @@ impl App { set_auth_type(auth_type); } - AppAction::ToggleAuth => { + AppAction::EnableAuth => { let current_auth_type = FfiApp::global().auth_type(); - let auth_type = if current_auth_type == AuthType::None { - AuthType::Biometric - } else { - AuthType::None - }; + if current_auth_type == AuthType::None { + set_auth_type(AuthType::Biometric); + } + } - set_auth_type(auth_type); + AppAction::DisableAuth => { + set_auth_type(AuthType::None); } - AppAction::ToggleBiometric => { + AppAction::EnableBiometric => { let current_auth_type = FfiApp::global().auth_type(); - let auth_type = match current_auth_type { - AuthType::None => AuthType::Biometric, - AuthType::Biometric => AuthType::None, - AuthType::Pin => AuthType::Biometric, - AuthType::Both => AuthType::Pin, + match current_auth_type { + AuthType::None => set_auth_type(AuthType::Biometric), + AuthType::Pin => set_auth_type(AuthType::Both), + _ => {} }; + } - set_auth_type(auth_type); + AppAction::DisableBiometric => { + 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::Biometric), + _ => {} + }; } AppAction::SetPin(pin) => { if let Err(err) = AuthPin::new().set(pin) { - error!("unable to set pin: {err:?}"); + 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 => { + 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), - AuthType::None | AuthType::Biometric => { - AuthPin::new().delete().unwrap_or_default(); - } + _ => {} } } } diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 0ccf66b8..68c2ee24 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -57,33 +57,16 @@ pub enum AuthError { #[derive(Debug, Clone, Hash, Eq, PartialEq, uniffi::Object)] pub struct AuthPin; -#[uniffi::export] impl AuthPin { - #[uniffi::constructor] - pub fn new() -> Self { - 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] pub fn set(&self, pin: String) -> Result<()> { let hashed = self.hash(pin)?; + Database::global() .global_config .set_hashed_pin_code(hashed) .map_err(AuthError::DatabaseSaveError) } - #[uniffi::method] pub fn hash(&self, pin: String) -> Result { let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default(); @@ -96,7 +79,6 @@ impl AuthPin { Ok(pin_hash) } - #[uniffi::method] pub fn delete(&self) -> Result<()> { Database::global() .global_config @@ -104,7 +86,6 @@ impl AuthPin { .map_err(AuthError::DatabaseSaveError) } - #[uniffi::method] pub fn verify(&self, pin: String, hashed_pin: String) -> Result<()> { let argon2 = Argon2::default(); @@ -118,6 +99,24 @@ impl AuthPin { } } +#[uniffi::export] +impl AuthPin { + #[uniffi::constructor] + pub fn new() -> Self { + 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() + } +} + #[cfg(test)] mod tests { use super::*; From fa3d24fb234776dabc76587ef34fc401099453df Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Thu, 12 Dec 2024 15:07:16 -0600 Subject: [PATCH 15/16] Require passing in `isPinCorrect` function --- ios/Cove/SettingsScreen/ChangePinView.swift | 2 ++ ios/Cove/Views/NumberPadPinView.swift | 17 ++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/ios/Cove/SettingsScreen/ChangePinView.swift b/ios/Cove/SettingsScreen/ChangePinView.swift index 01efc4f7..d6631f72 100644 --- a/ios/Cove/SettingsScreen/ChangePinView.swift +++ b/ios/Cove/SettingsScreen/ChangePinView.swift @@ -26,6 +26,7 @@ struct ChangePinView: View { case .current: NumberPadPinView( title: "Enter Current PIN", + isPinCorrect: isPinCorrect, backAction: backAction, onUnlock: { _ in withAnimation { @@ -37,6 +38,7 @@ struct ChangePinView: View { case .new: NumberPadPinView( title: "Enter new PIN", + isPinCorrect: { _ in true }, backAction: backAction, onUnlock: { enteredPin in withAnimation { diff --git a/ios/Cove/Views/NumberPadPinView.swift b/ios/Cove/Views/NumberPadPinView.swift index 4869e775..0fbdc2eb 100644 --- a/ios/Cove/Views/NumberPadPinView.swift +++ b/ios/Cove/Views/NumberPadPinView.swift @@ -30,7 +30,7 @@ struct NumberPadPinView: View { public init( title: String = "Enter Pin", isUnlocked: Binding = .constant(false), - isPinCorrect: @escaping (String) -> Bool = { _ in true }, + isPinCorrect: @escaping (String) -> Bool, pinLength: Int = 6, backAction: (() -> Void)? = nil, onUnlock: @escaping (String) -> Void = { _ in }, @@ -143,11 +143,14 @@ struct NumberPadPinView: View { .tint(.white) } - /// 0 and Back Button + // take up space + Button(action: {}) {} + Button(action: { - if !pin.isEmpty { pin.removeLast() } + guard pin.count < pinLength else { return } + pin.append("0") }, label: { - Image(systemName: "delete.backward") + Text("0") .font(.title) .frame(maxWidth: .infinity) .padding(.vertical, 20) @@ -155,11 +158,11 @@ struct NumberPadPinView: View { }) .tint(.white) + /// 0 and Back Button Button(action: { - guard pin.count < pinLength else { return } - pin.append("0") + if !pin.isEmpty { pin.removeLast() } }, label: { - Text("0") + Image(systemName: "delete.backward") .font(.title) .frame(maxWidth: .infinity) .padding(.vertical, 20) From 52abc59de37f41b7b975b54fff38ee9957194be3 Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Thu, 12 Dec 2024 15:08:22 -0600 Subject: [PATCH 16/16] Run swiftformat --- ios/Cove/SettingsScreen/ChangePinView.swift | 2 +- ios/Cove/SettingsScreen/NewPinView.swift | 2 +- ios/Cove/Views/LockView.swift | 10 +++--- ios/Cove/Views/NumberPadPinView.swift | 36 ++++++++++----------- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/ios/Cove/SettingsScreen/ChangePinView.swift b/ios/Cove/SettingsScreen/ChangePinView.swift index d6631f72..224ddc74 100644 --- a/ios/Cove/SettingsScreen/ChangePinView.swift +++ b/ios/Cove/SettingsScreen/ChangePinView.swift @@ -47,7 +47,7 @@ struct ChangePinView: View { } ) - case .confirm(let pinToConfirm): + case let .confirm(pinToConfirm): NumberPadPinView( title: "Confirm New PIN", isPinCorrect: { $0 == pinToConfirm }, diff --git a/ios/Cove/SettingsScreen/NewPinView.swift b/ios/Cove/SettingsScreen/NewPinView.swift index 56421c25..4ed6530b 100644 --- a/ios/Cove/SettingsScreen/NewPinView.swift +++ b/ios/Cove/SettingsScreen/NewPinView.swift @@ -33,7 +33,7 @@ struct NewPinView: View { } } ) - case .confirm(let pinToConfirm): + case let .confirm(pinToConfirm): NumberPadPinView( title: "Confirm New PIN", isPinCorrect: { $0 == pinToConfirm }, diff --git a/ios/Cove/Views/LockView.swift b/ios/Cove/Views/LockView.swift index 280f50e4..12fe2990 100644 --- a/ios/Cove/Views/LockView.swift +++ b/ios/Cove/Views/LockView.swift @@ -58,14 +58,14 @@ struct LockView: View { self.content = content() // back - self.backEnabled = backAction != nil + backEnabled = backAction != nil self.backAction = backAction ?? {} // private - self.animateField = false - self.isUnlocked = false - self.noBiometricAccess = false - self.pinLength = 6 + animateField = false + isUnlocked = false + noBiometricAccess = false + pinLength = 6 } var body: some View { diff --git a/ios/Cove/Views/NumberPadPinView.swift b/ios/Cove/Views/NumberPadPinView.swift index 0fbdc2eb..16368530 100644 --- a/ios/Cove/Views/NumberPadPinView.swift +++ b/ios/Cove/Views/NumberPadPinView.swift @@ -11,10 +11,10 @@ struct NumberPadPinView: View { /// args var title: String @Binding var isUnlocked: Bool - + let isPinCorrect: (String) -> Bool var pinLength: Int - + // back button private var backEnabled: Bool var backAction: () -> Void @@ -26,7 +26,7 @@ struct NumberPadPinView: View { /// private view properties @State private var pin: String @State private var animateField: Bool - + public init( title: String = "Enter Pin", isUnlocked: Binding = .constant(false), @@ -37,18 +37,18 @@ struct NumberPadPinView: View { onWrongPin: @escaping (String) -> Void = { _ in } ) { self.title = title - self._isUnlocked = isUnlocked + _isUnlocked = isUnlocked self.isPinCorrect = isPinCorrect self.pinLength = pinLength - self.backEnabled = backAction != nil + backEnabled = backAction != nil self.backAction = backAction ?? {} self.onUnlock = onUnlock self.onWrongPin = onWrongPin - - self.pin = "" - self.animateField = false + + pin = "" + animateField = false } - + private var isBiometricAvailable: Bool { /// Lock Context let context = LAContext() @@ -73,7 +73,7 @@ struct NumberPadPinView: View { .font(.title.bold()) .frame(maxWidth: .infinity) .foregroundStyle(.white) - + /// Adding Wiggling Animation for Wrong Password With Keyframe Animator HStack(spacing: 10) { ForEach(0 ..< pinLength, id: \.self) { index in @@ -86,7 +86,7 @@ struct NumberPadPinView: View { if pin.count > index { let index = pin.index(pin.startIndex, offsetBy: index) let string = String(pin[index]) - + Text(string) .font(.title.bold()) .foregroundStyle(.black) @@ -117,7 +117,7 @@ struct NumberPadPinView: View { .onChange(of: animateField) { _, _ in let pin = pin self.pin = "" - + let totalDuration = 7 * 0.07 DispatchQueue.main.asyncAfter(deadline: .now() + totalDuration) { onWrongPin(pin) @@ -125,7 +125,7 @@ struct NumberPadPinView: View { } .padding(.top, 15) .frame(maxHeight: .infinity) - + /// Custom Number Pad GeometryReader { _ in LazyVGrid(columns: Array(repeating: GridItem(), count: 3), content: { @@ -142,10 +142,10 @@ struct NumberPadPinView: View { }) .tint(.white) } - + // take up space Button(action: {}) {} - + Button(action: { guard pin.count < pinLength else { return } pin.append("0") @@ -157,7 +157,7 @@ struct NumberPadPinView: View { .contentShape(.rect) }) .tint(.white) - + /// 0 and Back Button Button(action: { if !pin.isEmpty { pin.removeLast() } @@ -197,7 +197,7 @@ struct NumberPadPinView: View { struct Container: View { @State var pin = "" @State var isUnlocked = false - + var body: some View { NumberPadPinView( isUnlocked: $isUnlocked, @@ -206,6 +206,6 @@ struct NumberPadPinView: View { ) } } - + return Container() }