diff --git a/Packages/WalletFoundation/Sources/Core/Keychain/KeychainProtocol.swift b/Packages/WalletFoundation/Sources/Core/Keychain/KeychainProtocol.swift new file mode 100644 index 00000000..401df362 --- /dev/null +++ b/Packages/WalletFoundation/Sources/Core/Keychain/KeychainProtocol.swift @@ -0,0 +1,26 @@ +// +// KeychainProtocol.swift +// +// +// Created by Nut.Tech on 16.02.2023. +// + +import Foundation + +public typealias Key = CommonKey + +public protocol KeychainProtocol { + + /// Checks if a given common key have a password-saved value + /// - Parameter key: Common Key + /// - Returns: is value exists + func exist(_ key: Key) -> Bool + /// Checks if a given common key have a biometric-saved value + /// - Parameter key: Common Key + /// - Returns: is value exists + func bioExist(_ key: Key) -> Bool + /// Access to common key value by biometric authenfication + subscript(biometric key: Key) -> String? { get } + /// Access to common key value by password authenfication + subscript(_ key: Key, password password: String) -> String? { get set } +} diff --git a/Packages/WalletFoundation/Sources/Core/Keychain/WalletKeychain.swift b/Packages/WalletFoundation/Sources/Core/Keychain/WalletKeychain.swift index 672d7ce3..a9e42d59 100644 --- a/Packages/WalletFoundation/Sources/Core/Keychain/WalletKeychain.swift +++ b/Packages/WalletFoundation/Sources/Core/Keychain/WalletKeychain.swift @@ -5,18 +5,15 @@ // Created by Juraldinio on 11/27/22. // -import Foundation import LocalAuthentication -extension String { - fileprivate static let password = "PWD" - fileprivate static let biometric = "BIO" -} +final public class WalletKeychain: KeychainProtocol { + + private enum KeychainLocals { + public static let password = "PWD" + public static let biometric = "BIO" + } -final public class WalletKeychain { - - public typealias Key = CommonKey - public static let instance = WalletKeychain() // MARK: - Init @@ -25,17 +22,17 @@ final public class WalletKeychain { // MARK: - Interface - public func exist(_ key: Key) -> Bool { self.checkProtectedExist(key: key.with(.password)) } + public func exist(_ key: Key) -> Bool { self.checkProtectedExist(key: key.with(KeychainLocals.password)) } - public func bioExist(_ key: Key) -> Bool { self.checkProtectedExist(key: key.with(.biometric) ) } + public func bioExist(_ key: Key) -> Bool { self.checkProtectedExist(key: key.with(KeychainLocals.biometric) ) } public subscript(biometric key: Key) -> String? { - self.loadBiometricProtected(key: key.with(.biometric)) + self.loadBiometricProtected(key: key.with(KeychainLocals.biometric)) .map({ String(data: $0, encoding: .utf8) }) ?? nil } public subscript(_ key: Key, password password: String) -> String? { - get { loadPassProtected(key: key.with(.password), password: password).map { String(data: $0, encoding: .utf8) } ?? nil } + get { loadPassProtected(key: key.with(KeychainLocals.password), password: password).map { String(data: $0, encoding: .utf8) } ?? nil } set { update(key, password: password, newValue: newValue) } } @@ -152,12 +149,13 @@ final public class WalletKeychain { // MARK: - private func update(_ key: Key, password: String, newValue: String?) { + //TODO: Refactor later - updation of biometric entry only when it is available and needed if let value = newValue { - setPassProtected(key: key.with(.password), data: value, password: password) - setBiometricEntry(key: key.with(.biometric), data: value) + setPassProtected(key: key.with(KeychainLocals.password), data: value, password: password) + setBiometricEntry(key: key.with(KeychainLocals.biometric), data: value) } else { - removeProtected(key: key.with(.password)) - removeProtected(key: key.with(.biometric)) + removeProtected(key: key.with(KeychainLocals.password)) + removeProtected(key: key.with(KeychainLocals.biometric)) } } diff --git a/Packages/WalletFoundation/Sources/Extensions/Data+Extension.swift b/Packages/WalletFoundation/Sources/Extensions/Data+Extension.swift index 578a8e53..1bb9eac4 100644 --- a/Packages/WalletFoundation/Sources/Extensions/Data+Extension.swift +++ b/Packages/WalletFoundation/Sources/Extensions/Data+Extension.swift @@ -9,7 +9,17 @@ import Foundation public extension Data { - func jsonDecoded(type: T.Type) -> T? { try? JSONDecoder().decode(type, from: self) } + func jsonDecoded(type: T.Type, userInfo: [CodingUserInfoKey: Any]? = nil) -> T? { + try? self.makeDecoder(userInfo: userInfo).decode(type, from: self) + } - func jsonDecoded(type: T.Type) -> [T]? { try? JSONDecoder().decode([T].self, from: self) } + func jsonDecoded(type: T.Type, userInfo: [CodingUserInfoKey: Any]? = nil) -> [T]? { + try? self.makeDecoder(userInfo: userInfo).decode([T].self, from: self) + } + + private func makeDecoder(userInfo: [CodingUserInfoKey: Any]?) -> JSONDecoder { + let jsonDecoder = JSONDecoder() + userInfo >>- { jsonDecoder.userInfo = $0 } + return jsonDecoder + } } diff --git a/Packages/WalletKit/Package.swift b/Packages/WalletKit/Package.swift index 833dd6e7..a09550dd 100644 --- a/Packages/WalletKit/Package.swift +++ b/Packages/WalletKit/Package.swift @@ -16,14 +16,15 @@ let package = Package( .package(name: "eosswift", path: "../../Vendors/spm/eos-swift"), .package(name: "KeyChainAccess", path: "../../Vendors/spm/KeyChainAccess"), .package(name: "WalletFoundation", path: "../WalletFoundation"), - .package(name: "WalletNetwork", path: "../WalletNetwork") + .package(name: "WalletNetwork", path: "../WalletNetwork"), + .package(name: "CryptoSwift", path: "../../Vendors/spm/CryptoSwift-1.5.1") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "WalletKit", - dependencies: ["eosswift", "KeyChainAccess", "WalletFoundation", "WalletNetwork"], + dependencies: ["CryptoSwift", "eosswift", "KeyChainAccess", "WalletFoundation", "WalletNetwork"], path: "./Sources"), .testTarget( name: "WalletKitTests", diff --git a/Packages/WalletKit/Sources/Village/Village.swift b/Packages/WalletKit/Sources/Village/Village.swift index 98950ab2..ff5a1986 100644 --- a/Packages/WalletKit/Sources/Village/Village.swift +++ b/Packages/WalletKit/Sources/Village/Village.swift @@ -9,16 +9,9 @@ import Foundation import WalletFoundation import WalletNetwork -public enum VillageError: Error { - case passwordNotMatch -} - final public class Village { - private enum Constants { - static let passwordKey = CommonKey("Account.Service.password") - } - + private static var keychainKeeper = PrivacyKeeper(keychain: WalletKeychain.instance) private static var villages = [Village]() private let environment: NetworkEnvironment @@ -41,6 +34,7 @@ final public class Village { /// Create instance of Village. public static func get(with environment: NetworkEnvironment) -> Village { + if let village = Self.villages.first(where: { $0.environment.isEquals(other: environment) }) { return village } @@ -99,17 +93,19 @@ final public class Village { } /// Create bank that contain Wallets. - public func getBank(with password: String, on device: Device) -> Bank { + public func getBank(with password: String, on device: Device) throws -> Bank { - if let bank = self.houses.first(where: { $0.isEquals(password: password, device: device, environment: self.environment) }) { + guard Self.keychainKeeper.accept(password: password) else { + throw WalletError.passwordNotMatch + } + + if let bank = self.houses.first(where: { $0.isEquals(device: device, environment: self.environment, password: password) }) { return bank } - - if Self.password != password { - Self.password = password - } - - let house = VillageHouse.create(for: password, on: device, environment: self.environment) + let house = VillageHouse.create(password: password, + keeper: Self.keychainKeeper, + on: device, + environment: self.environment) self.houses.append(house) return house @@ -117,31 +113,24 @@ final public class Village { public static func reset() { VillageHouse.removeAllVillagers() - Self.password = nil + Self.keychainKeeper.reset() } - public static var isPasswordExists: Bool { WalletKeychain.instance.exist(Constants.passwordKey) } + // Keychain static methods - public static func getPassword(password: String?) -> String? { - if let password { - return WalletKeychain.instance[Constants.passwordKey, password: password] - } else { - return WalletKeychain.instance[biometric: Constants.passwordKey] - } + public static func accept(password: String) -> Bool { + Self.keychainKeeper.accept(password: password) } - // TODO: Remove this function after refactoring passwords - public static func updateCommonPassword(password: String, old: String) throws { - if WalletKeychain.instance[Constants.passwordKey, password: old] != nil { - Self.password = password - } else { - throw VillageError.passwordNotMatch - } + public static func passwordViaBiometrics() -> String? { + Self.keychainKeeper.passwordByBiometrics() } - private static var password: String? { - get { WalletKeychain.instance[biometric: Constants.passwordKey] } - set { WalletKeychain.instance[Constants.passwordKey, password: newValue ?? ""] = newValue } + public static var isPasswordExists: Bool { + Self.keychainKeeper.isPasswordExists } + public static func set(password: String, old: String? = nil) throws { + try Self.keychainKeeper.update(password: password, old: old) + } } diff --git a/Packages/WalletKit/Sources/Village/internal/PrivacyKeeper.swift b/Packages/WalletKit/Sources/Village/internal/PrivacyKeeper.swift new file mode 100644 index 00000000..b7dfffde --- /dev/null +++ b/Packages/WalletKit/Sources/Village/internal/PrivacyKeeper.swift @@ -0,0 +1,230 @@ +// +// PrivacyKeeper.swift +// +// +// Created by Nut.Tech on 07.02.2023. +// + +import WalletFoundation +import Foundation +import CryptoSwift + +final class PrivacyKeeper { + private typealias CipherData = (key: String, iv: String) + private enum Constants { + static let oldPasswordKey = CommonKey("PrivacyKeeper.password") + static let passwordKey = "PrivacyKeeper.password" + static let cipherKey = "PrivacyKeeper.ckey" + static let ivKey = "PrivacyKeeper.ivkey" + } + + var isPasswordExists: Bool { + if let encryptedPasswordKey { + return self.keychain.exist(encryptedPasswordKey) + } + return false + } + + private var encryptedPasswordKey: CommonKey? + private var cipherData: CipherData + private var keychain: KeychainProtocol + + init(keychain: KeychainProtocol) { + self.keychain = keychain + self.cipherData = Self.initCipherData() + self.encryptedPasswordKey = Self.makeEncryptedPasswordCommonKey(cipherData: self.cipherData) + } + + /// Cheks if password correct + func accept(password: String) -> Bool { + self.getPassword(password) == password + } + + /// Get password via biometry + func passwordByBiometrics() -> String? { + guard let encryptedPasswordKey else { return nil } + return self.keychain.bioExist(encryptedPasswordKey) ? + self.keychain[biometric: encryptedPasswordKey] : nil + } + + /// Get private key from keychain + /// - Parameters: + /// - key: common key in keychain + /// - password: current password + /// - Returns: private key (if exists) + func privateKey(for key: String, password: String?) -> String? { + let pwd = password ?? self.passwordByBiometrics() + if let pwd { + return self.keychain[self.encryptPK(commonKey: key), password: pwd] + } else { + return nil + } + } + + /// Update password to new one. + func update(password: String, old: String? = nil) throws { + guard self.isPasswordExists else { + try self.setPassword(password) + return + } + guard let old, self.accept(password: old) else { + throw WalletError.passwordNotMatch + } + + try self.setPassword(password) + } + + /// Update private key for current account. If you are using biometrics, pass nil in password. + /// - Parameters: + /// - privateKey: new private key + /// - key: common key in keychain + /// - password: current password + func update(privateKey: String?, for key: String, password: String?) { + let pwd = password ?? self.passwordByBiometrics() + guard let pwd, self.accept(password: pwd) else { return } + self.keychain[self.encryptPK(commonKey: key), password: pwd] = privateKey + } + + /// Migrates pasword and private key storage in keychain to new style + /// - Parameters: + /// - oldKey: old private key common key for keychain + /// - newKey: new private key common key for keychain + /// - password: current password + func migrate(oldKey: CommonKey, newKey: CommonKey, password: String) { + self.migratePassword(password: password) + guard self.accept(password: password) else { return } + self.migratePrivateKey(oldKey: oldKey, newKey: newKey, password: password) + self.migrateToEncrypted(commonKey: newKey, password: password) + } + + /// Resets all data by making all old data inaccessible + func reset() { + self.cipherData = Self.createCipherData() + self.encryptedPasswordKey = Self.makeEncryptedPasswordCommonKey(cipherData: self.cipherData) + } + + /// Gets password if exists. Pass nil to get biometrics password + /// - Parameter password: current password + /// - Returns: password string (if exists) + private func getPassword(_ password: String? = nil) -> String? { + if let password { + guard let encryptedPasswordKey else { return nil } + return self.keychain[encryptedPasswordKey, password: password] + } else { + return self.passwordByBiometrics() + } + } + + /// Set password + /// - Parameter password: new password + private func setPassword(_ password: String) throws { + guard let encryptedPasswordKey else { + throw WalletError.passwordKeyNotExist + } + self.keychain[encryptedPasswordKey, password: password] = password + } + + /// Migrate private key in keychain from old to new for old versions + /// - Parameters: + /// - oldKey: old-style key + /// - newKey: new-style key + /// - password: current password + private func migratePrivateKey(oldKey: CommonKey, newKey: CommonKey, password: String) { + guard let privateKey = self.keychain[oldKey, password: password] else { return } + self.keychain[oldKey, password: password] = nil + self.keychain[newKey, password: password] = privateKey + } + + /// Migrate to encrypted keys for keychain + /// - Parameters: + /// - commonKey: old-style common key, without encryption + /// - password: current password + private func migrateToEncrypted(commonKey: CommonKey, password: String) { + guard let privateKey = self.keychain[commonKey, password: password] else { return } + self.keychain[commonKey, password: password] = nil + self.keychain[self.encryptPK(commonKey: commonKey.rawValue), password: password] = privateKey + } + + /// Migrating from an old keychain password key to a new one + /// - Parameter password: current password + private func migratePassword(password: String) { + guard self.keychain[Constants.oldPasswordKey, password: password] != nil, + let encryptedPasswordKey else { return } + self.keychain[Constants.oldPasswordKey, password: password] = nil + self.keychain[encryptedPasswordKey, password: password] = password + } + + /// Makes encrypted password key for keychain + private static func makeEncryptedPasswordCommonKey(cipherData: CipherData) -> CommonKey? { + guard let encryptedCKeyString = try? Self.aesEncrypt(string: Constants.passwordKey, + key: cipherData.key, + iv: cipherData.iv) + else { return nil } + return CommonKey(encryptedCKeyString) + } + + /// Encrypt key for use in keychain + /// - Parameter commonKey: common key, e.g. "user@wallet.privatekey" + /// - Returns: encrypted CommonKey object + private func encryptPK(commonKey: String) -> CommonKey { + let encryptedCKeyString = try? Self.aesEncrypt(string: commonKey, + key: self.cipherData.key, + iv: self.cipherData.iv) + return CommonKey(encryptedCKeyString ?? commonKey) + } + + /// Generates data for encoding keys + private static func makeCipherData() -> CipherData { + let ckey = UUID().uuidString.replacingOccurrences(of:"-", + with: "", + options: .literal) + let iv = String(UUID().uuidString.replacingOccurrences(of:"-", + with: "", + options: .literal) + .dropLast(16)) + return CipherData(key: ckey, iv: iv) + } + + /// Tries to load cipher data and creates it, if doesn't exist + private static func initCipherData() -> CipherData { + if let ckey = UserDefaults.standard.string(forKey: Constants.cipherKey), + let iv = UserDefaults.standard.string(forKey: Constants.ivKey) { + return CipherData(key: ckey, iv: iv) + } else { + return Self.createCipherData() + } + } + + /// Creates new data for encoding keychain keys and saves it to defaults + private static func createCipherData() -> CipherData { + let cipherData = Self.makeCipherData() + UserDefaults.standard.set(cipherData.key, forKey: Constants.cipherKey) + UserDefaults.standard.set(cipherData.iv, forKey: Constants.ivKey) + return cipherData + } + + static func aesEncrypt(string: String, key: String, iv: String) throws -> String { + guard let data = string.data(using: .utf8) else { + throw WalletError.cannotEncryptNonUTF8Data + } + let encrypted = try AES(key: Array(key.utf8), + blockMode: CBC(iv: Array(iv.utf8))) + .encrypt([UInt8](data)) + let encryptedData = Data(encrypted) + return encryptedData.base64EncodedString() + } + + static func aesDecrypt(string: String, key: String, iv: String) throws -> String { + guard let data = Data(base64Encoded: string) else { + throw WalletError.cannotDecryptNonUTF8Data + } + let decrypted = try AES(key: Array(key.utf8), + blockMode: CBC(iv: Array(iv.utf8))) + .decrypt([UInt8](data)) + let decryptedData = Data(decrypted) + guard let decryptedString = String(bytes: decryptedData.bytes, encoding: .utf8) else { + throw WalletError.cannotDecryptData + } + return decryptedString + } +} diff --git a/Packages/WalletKit/Sources/Village/internal/VillageHouse.swift b/Packages/WalletKit/Sources/Village/internal/VillageHouse.swift index 006d2ee8..6ba3a754 100644 --- a/Packages/WalletKit/Sources/Village/internal/VillageHouse.swift +++ b/Packages/WalletKit/Sources/Village/internal/VillageHouse.swift @@ -11,7 +11,6 @@ import WalletFoundation import WalletNetwork final class VillageHouse: Bank { - private enum Constants { static let oldKey = "Account.Service.collection" static let key = "Wallet.bank.service" @@ -24,10 +23,9 @@ final class VillageHouse: Bank { // MARK: - Properties - private var password: String private let device: Device private let environment: NetworkEnvironment - + private let keychainKeeper: PrivacyKeeper private let activeSubject: CurrentValueSubject private let villagersSubject: CurrentValueSubject<[Villager], Never> @@ -35,16 +33,17 @@ final class VillageHouse: Bank { // MARK: - Init - private init(password: String, device: Device, environment: NetworkEnvironment) { + private init(password: String, keychain: PrivacyKeeper, device: Device, environment: NetworkEnvironment) { - self.password = password self.device = device self.environment = environment + self.keychainKeeper = keychain - let collection = Self.restore() + let collection = Self.restore(keychain: keychain) + Self.migrateIfNeeded(collection: collection, password: password) self.villagersSubject = CurrentValueSubject(collection) - let active = Self.active(in: collection) + let active = Self.active(in: collection, keychain: keychain) self.activeSubject = CurrentValueSubject(active) self.villagersSubject @@ -52,8 +51,6 @@ final class VillageHouse: Bank { self?.save(villagers: villagers) } .store(in: &self.cancellables) - - self.migrate(using: self.password) } // MARK: - Bank @@ -65,8 +62,6 @@ final class VillageHouse: Bank { lazy var activePublisher: AnyPublisher = self.activeSubject.map { $0 }.eraseToAnyPublisher() lazy var walletsPublisher: AnyPublisher<[Wallet], Never> = self.villagersSubject.map { $0 }.eraseToAnyPublisher() - - func accept(password: String) -> Bool { self.password == password } func remove(wallet: Wallet) throws { @@ -107,31 +102,29 @@ final class VillageHouse: Bank { self.activeSubject.value = villager } - func add(using walletCase: WalletCase) async throws -> Wallet { - let villager = try await Villager.create(walletCase: walletCase, on: self.device, using: self.environment) - villager.updatePrivateKey(using: self.password) - - let villagers = self.villagersSubject.value - self.villagersSubject.value = villagers + [villager] - + func add(using walletCase: WalletCase, password: String) async throws -> Wallet { + let villager = try await Villager.create(walletCase: walletCase, + on: self.device, + using: self.environment, + keychain: self.keychainKeeper) + self.add(wallets: [villager], password: password) return villager } - func add(using purses: [Purse]) throws { + func add(using purses: [Purse], password: String) throws { + guard self.keychainKeeper.accept(password: password) else { + throw WalletError.passwordNotMatch + } let wallets = purses - .filter { $0.bank?.isEquals(other: self) ?? false } + .filter { $0.bank?.isEquals(other: self, password: password) ?? false } .filter { purse in !self.wallets.contains(where: { $0.name == purse.name && $0.keyType.rawValue == purse.permission.permName }) } - .map { Villager.create(purse: $0) } + .map { Villager.create(purse: $0, + keeper: self.keychainKeeper) } - wallets - .filter { $0.key.privateKey.isExist } - .forEach { $0.updatePrivateKey(using: self.password) } - - let villagers = self.villagersSubject.value - self.villagersSubject.value = villagers + wallets + self.add(wallets: wallets, password: password) } func restore(using keys: WalletKey) async throws -> PurseHolder { @@ -176,22 +169,21 @@ final class VillageHouse: Bank { } } - func isEquals(other: Bank) -> Bool { - guard let house = other as? VillageHouse else { return false } - return self.password == house.password && - self.device.isEquals(other: house.device) && - self.environment.isEquals(other: house.environment) + func accept(password: String) -> Bool { + self.keychainKeeper.accept(password: password) } - func switchPassword(_ password: String, old: String) throws { - - guard self.accept(password: old) else { - throw BankError.passwordNotMatch - } - - self.password = password - - old >>- { old in self.villagersSubject.value.forEach { $0.update(password: password, old: old) } } + func isEquals(other: Bank, password: String) -> Bool { + guard let house = other as? VillageHouse else { return false } + return self.device.isEquals(other: house.device) && + self.environment.isEquals(other: house.environment) && + self.keychainKeeper.accept(password: password) && + other.accept(password: password) + } + + func switchPassword(_ new: String, old: String) throws { + try self.keychainKeeper.update(password: new, old: old) + self.wallets.forEach { $0.updatePrivateKeyEncryption(password: new, old: old) } } func update(_ keyUpdates: [WalletKeyUpdate], using password: String) async throws -> [WalletKeyUpdateResult] { @@ -216,32 +208,48 @@ final class VillageHouse: Bank { // MARK: - Internal - func isEquals(password: String, device: Device, environment: NetworkEnvironment) -> Bool { - return self.password == password && - self.device.isEquals(other: device) && - self.environment.isEquals(other: environment) + func isEquals(device: Device, environment: NetworkEnvironment, password: String) -> Bool { + return self.device.isEquals(other: device) && + self.environment.isEquals(other: environment) && + self.accept(password: password) } // MARK: - Private + private func add(wallets: [Villager], password: String) { + wallets + .filter { $0.key.privateKey.isExist } + .forEach { $0.updateKeychain(key: $0.key, using: password) } + + let villagers = self.villagersSubject.value + self.villagersSubject.value = villagers + wallets + } + private func save(villagers: [Villager]) { Settings.shared[Constants.Keys.collection] = villagers.jsonData() } - private static func restore() -> [Villager] { + private static func restore(keychain: PrivacyKeeper) -> [Villager] { if let data: Data = Settings.shared[Constants.Keys.collection], - let villagers: [Villager] = data.jsonDecoded(type: Villager.self) { + let userInfoKey = Villager.keychainUserInfoKey, + let villagers: [Villager] = data.jsonDecoded(type: Villager.self, + userInfo: [userInfoKey: keychain]) { return villagers } return [] } - private func migrate(using password: String) { + private static func migrateIfNeeded(collection: [Villager], password: String) { + guard collection.count > 0 else { return } + collection.forEach { $0.migrate(password: password) } + } + + private func migrate() { let migrateWallets: [Villager] if let data = UserDefaults.standard.value(forKey: Constants.oldKey) as? Data, let collection = try? JSONDecoder().decode([Villager.OldVillager].self, from: data) { - migrateWallets = collection.compactMap { $0.covert(password: password) } + migrateWallets = collection.compactMap { $0.covert(keeper: self.keychainKeeper) } } else { migrateWallets = [] } @@ -263,14 +271,20 @@ final class VillageHouse: Bank { // MARK: - Static - static func create(for password: String, on device: Device, environment: NetworkEnvironment) -> VillageHouse { - VillageHouse(password: password, device: device, environment: environment) + public static func create(password: String, keeper: PrivacyKeeper, on device: Device, environment: NetworkEnvironment) -> VillageHouse { + VillageHouse(password: password, keychain: keeper, device: device, environment: environment) } - private static func active(in collection: [Villager]) -> Villager? { + public static func removeAllVillagers() { + Settings.shared[Constants.Keys.collection] = Data() + } + + private static func active(in collection: [Villager], keychain: PrivacyKeeper) -> Villager? { if let data: Data = Settings.shared[Constants.Keys.current], - let villager: Villager = data.jsonDecoded(type: Villager.self), + let userInfoKey = Villager.keychainUserInfoKey, + let villager: Villager = data.jsonDecoded(type: Villager.self, + userInfo: [userInfoKey: keychain]), collection.contains(where: { $0 == villager }) { return villager } @@ -283,8 +297,4 @@ final class VillageHouse: Bank { return collection.first(where: { $0.name == name && $0.keyType == keyType }) } - - static func removeAllVillagers() { - Settings.shared[Constants.Keys.collection] = Data() - } } diff --git a/Packages/WalletKit/Sources/Village/internal/Villager.swift b/Packages/WalletKit/Sources/Village/internal/Villager.swift index c02d05e5..9090a6af 100644 --- a/Packages/WalletKit/Sources/Village/internal/Villager.swift +++ b/Packages/WalletKit/Sources/Village/internal/Villager.swift @@ -21,13 +21,17 @@ final class Villager: Wallet, Codable, CustomStringConvertible { var publicKey: String var keyType: String - func covert(password: String) -> Villager? { + func covert(keeper: PrivacyKeeper) -> Villager? { guard let keyType = WalletKeyType(rawValue: self.keyType), - let key = try? WalletKey.restore(using: self.username, type: keyType, publicKey: self.publicKey, password: password) else { + let key = try? WalletKey.restore(using: self.username, type: keyType, publicKey: self.publicKey) else { return nil } - return Villager(name: self.username, key: key, keyType: keyType, state: .accepted) + return Villager(name: self.username, + key: key, + keyType: keyType, + state: .accepted, + keychain: keeper) } } @@ -35,17 +39,26 @@ final class Villager: Wallet, Codable, CustomStringConvertible { // MARK: - Init - private init(walletCase: WalletCase, keyType: WalletKeyType, state: WalletState) { + private init(walletCase: WalletCase, + keyType: WalletKeyType, + state: WalletState, + keychain: PrivacyKeeper) { self.name = walletCase.name self.key = walletCase.key self.keyType = keyType + self.keychain = keychain self.stateSubject = CurrentValueSubject(state) } - private init(name: String, key: WalletKey, keyType: WalletKeyType, state: WalletState) { + private init(name: String, + key: WalletKey, + keyType: WalletKeyType, + state: WalletState, + keychain: PrivacyKeeper) { self.name = name self.key = key self.keyType = keyType + self.keychain = keychain self.stateSubject = CurrentValueSubject(state) } @@ -62,6 +75,10 @@ final class Villager: Wallet, Codable, CustomStringConvertible { case state } + static var keychainUserInfoKey: CodingUserInfoKey? { + return CodingUserInfoKey(rawValue: "keychain") + } + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -77,6 +94,11 @@ final class Villager: Wallet, Codable, CustomStringConvertible { let state = try container.decode(WalletState.self, forKey: .state) self.stateSubject = CurrentValueSubject(state) + guard let keychainKey = Self.keychainUserInfoKey, + let keeper = decoder.userInfo[keychainKey] as? PrivacyKeeper else { + throw WalletError.decodingWithoutKeychainContext + } + self.keychain = keeper // If first version we hold key on flat structure! if let key = try? WalletKey(from: decoder) { @@ -84,6 +106,7 @@ final class Villager: Wallet, Codable, CustomStringConvertible { } else { self.key = try container.decode(WalletKey.self, forKey: .key) } + // TODO: - NEED COMPLETETASK // self.key = try WalletKey.restore(using: self.name, type: self.keyType, publicKey: publicKey, password: "") } @@ -102,6 +125,7 @@ final class Villager: Wallet, Codable, CustomStringConvertible { private(set) var key: WalletKey let keyType: WalletKeyType var state: WalletState { self.stateSubject.value } + private let keychain: PrivacyKeeper lazy var statePublisher: AnyPublisher = self.stateSubject.eraseToAnyPublisher() @@ -143,52 +167,65 @@ final class Villager: Wallet, Codable, CustomStringConvertible { self.stateSubject.value = state } - func update(key: WalletKey, using passrod: String) -> Wallet { - self.key = key - self.updatePrivateKey(using: passrod) + @discardableResult + func updateKeychain(key: WalletKey, using password: String) -> Wallet { + if self.keychain.accept(password: password) { + self.key = key + self.updatePrivateKey(password: password) + } return self } - func privateKey(_ value: String?) -> String? { - let privateKey: String? - if let password = value { - privateKey = WalletKeychain.instance[.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey), password: password] - } else { - privateKey = WalletKeychain.instance[biometric: .key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey)] - } - return privateKey + func privateKey(password: String?) -> String? { + guard let password, self.keychain.accept(password: password) else { return nil } + let key = CommonKey.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey) + return self.keychain.privateKey(for: key.rawValue, password: password) } - func updatePrivateKey(using password: String) { + private func updatePrivateKey(password: String) { guard let privateKey = self.key.privateKey else { return } - WalletKeychain.instance[.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey), - password: password] = privateKey + let key = CommonKey.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey) + self.keychain.update(privateKey: privateKey, for: key.rawValue, password: password) } // TODO: - Need review func migrate(password: String) { - let privateKey = WalletKeychain.instance[.key(self.name, suffix: .privateKey), password: password] - WalletKeychain.instance[.key(self.name, suffix: .privateKey), password: ""] = nil - WalletKeychain.instance[.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey), password: password] = privateKey + let oldKey = CommonKey.key(self.name, suffix: .privateKey) + let newKey = CommonKey.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey) + self.keychain.migrate(oldKey: oldKey, newKey: newKey, password: password) } - func update(password: String, old: String) { - WalletKeychain.instance[.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey), - password: password] = self.privateKey(old) + /// Updates storage of private key in keychain for new password + /// - Parameters: + /// - password: new password + /// - old: old password (which was stored with private key earlier) + func updatePrivateKeyEncryption(password: String, old: String) { + let key = CommonKey.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey) + let privateKey = self.privateKey(password: old) + self.keychain.update(privateKey: nil, for: key.rawValue, password: old) + self.keychain.update(privateKey: privateKey, for: key.rawValue, password: password) } func clear() { - WalletKeychain.instance[.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey), password: ""] = nil + let key = CommonKey.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey) + self.keychain.update(privateKey: nil, for: key.rawValue, password: "") } // MARK: - Static - static func create(purse: Purse) -> Villager { - Villager(name: purse.name, key: purse.key, keyType: purse.keyType, state: .accepted) + static func create(purse: Purse, keeper: PrivacyKeeper) -> Villager { + Villager(name: purse.name, + key: purse.key, + keyType: purse.keyType, + state: .accepted, + keychain: keeper) } - static func create(walletCase: WalletCase, on device: Device, using environment: NetworkEnvironment) async throws -> Villager { + static func create(walletCase: WalletCase, + on device: Device, + using environment: NetworkEnvironment, + keychain: PrivacyKeeper) async throws -> Villager { let service = AccountService(environment: environment) do { @@ -205,12 +242,18 @@ final class Villager: Wallet, Codable, CustomStringConvertible { case .completed: state = .accepted } - return Villager(walletCase: walletCase, keyType: .owner, state: state) + return Villager(walletCase: walletCase, + keyType: .owner, + state: state, + keychain: keychain) } catch let NetworkServiceError.gqlApplication(error) { if ApplicationSettings.ignoreCreateAccountSatus { - return Villager(walletCase: walletCase, keyType: .owner, state: .creating("HELLO")) + return Villager(walletCase: walletCase, + keyType: .owner, + state: .creating("HELLO"), + keychain: keychain) } let walletError: WalletError diff --git a/Packages/WalletKit/Sources/Village/public/Bank.swift b/Packages/WalletKit/Sources/Village/public/Bank.swift index e0349c64..ee6e042c 100644 --- a/Packages/WalletKit/Sources/Village/public/Bank.swift +++ b/Packages/WalletKit/Sources/Village/public/Bank.swift @@ -12,7 +12,6 @@ import WalletNetwork public enum BankError: Error { case empty case notOwned - case passwordNotMatch } /// Bank that contain wallets. @@ -25,14 +24,12 @@ public protocol Bank: AnyObject { var wallets: [Wallet] { get } /// Publisher for observe wallet changes. var walletsPublisher: AnyPublisher<[Wallet], Never> { get } - /// Check password - func accept(password: String) -> Bool /// Activate wallet func activate(wallet: Wallet?) throws /// Create new wallet in bank. - func add(using walletCase: WalletCase) async throws -> Wallet + func add(using walletCase: WalletCase, password: String) async throws -> Wallet /// Add Purse to bank with converting to Wallet - func add(using purses: [Purse]) throws + func add(using purses: [Purse], password: String) throws /// Restore wallet by key. func restore(using keys: WalletKey) async throws -> PurseHolder /// Remove wallet from bank. @@ -42,7 +39,9 @@ public protocol Bank: AnyObject { /// Update WalletKey for Wallet func update(_ keyUpdates: [WalletKeyUpdate], using password: String) async throws -> [WalletKeyUpdateResult] /// Compare two banks - func isEquals(other: Bank) -> Bool + func isEquals(other: Bank, password: String) -> Bool /// Switch password func switchPassword(_ password: String, old: String) throws + /// Check if password valid for this bank + func accept(password: String) -> Bool } diff --git a/Packages/WalletKit/Sources/Village/public/Wallet.swift b/Packages/WalletKit/Sources/Village/public/Wallet.swift index 1304d2b9..c07973f7 100644 --- a/Packages/WalletKit/Sources/Village/public/Wallet.swift +++ b/Packages/WalletKit/Sources/Village/public/Wallet.swift @@ -33,6 +33,18 @@ public enum WalletError: Error { case privateKeyNotExists /// Wallet with such name already exists. case exists + /// Input password doesn't match keychain password + case passwordNotMatch + /// Error creating access key for password in keychain + case passwordKeyNotExist + /// Failed encryption access key for keychain + case cannotEncryptNonUTF8Data + /// Failed decryption access key for keychain + case cannotDecryptNonUTF8Data + /// Common decrypt error + case cannotDecryptData + /// Trying to decode Wallet-protocol object without keychain data + case decodingWithoutKeychainContext } /// Wallet state @@ -127,12 +139,13 @@ public protocol Wallet: AnyObject { var keyType: WalletKeyType { get } /// State var state: WalletState { get } + var statePublisher: AnyPublisher { get } @discardableResult - func update(key: WalletKey, using passrod: String) -> Wallet - - func privateKey(_ value: String?) -> String? + func updateKeychain(key: WalletKey, using password: String) -> Wallet + func privateKey(password: String?) -> String? + func updatePrivateKeyEncryption(password: String, old: String) /// Compare two Wallets func isEquals(other: Wallet) -> Bool diff --git a/Packages/WalletKit/Sources/Village/public/WalletKey.swift b/Packages/WalletKit/Sources/Village/public/WalletKey.swift index 5fd8f90e..ce176db3 100644 --- a/Packages/WalletKit/Sources/Village/public/WalletKey.swift +++ b/Packages/WalletKit/Sources/Village/public/WalletKey.swift @@ -135,7 +135,7 @@ public enum WalletKey: Codable, Equatable { // MARK: - Internal static - static func restore(using name: String, type: WalletKeyType, publicKey: String, password: String) throws -> WalletKey { + static func restore(using name: String, type: WalletKeyType, publicKey: String) throws -> WalletKey { throw WalletKeyError.unlock } diff --git a/Packages/WalletKit/Tests/JSONParsingTests.swift b/Packages/WalletKit/Tests/JSONParsingTests.swift new file mode 100644 index 00000000..3fd6346e --- /dev/null +++ b/Packages/WalletKit/Tests/JSONParsingTests.swift @@ -0,0 +1,99 @@ +// +// JSONParsingTests.swift +// +// +// Created by Nut.Tech on 17.02.2023. +// + +import XCTest +@testable import WalletKit + +fileprivate struct User: Decodable { + let id: Int + let name: String + var employer: String? + static var employerUserInfoKey: CodingUserInfoKey? { + return CodingUserInfoKey(rawValue: "employer") + } + + private enum CodingKeys: String, CodingKey { + case id + case name + } + + init(from decoder: Decoder) throws { + let keyedContainer = try decoder.container(keyedBy: CodingKeys.self) + self.id = try keyedContainer.decode(Int.self, forKey: .id) + self.name = try keyedContainer.decode(String.self, forKey: .name) + + guard let employerKey = Self.employerUserInfoKey, + let employer = decoder.userInfo[employerKey] as? String else { + return + } + self.employer = employer + } +} + +final class JSONParsingTests: XCTestCase { + + private enum Locals { + static let username1 = "Username1" + static let username2 = "Username2" + static let userId1 = 1 + static let userId2 = 2 + static let companyName = "Company Name" + } + + func testJsonDecoded() { + let jsonData = """ + {"id": \(Locals.userId1), "name": "\(Locals.username1)"} + """.data(using: .utf8)! + + guard let user: User = jsonData.jsonDecoded(type: User.self) else { + XCTFail("Error decoding data") + return + } + + XCTAssertEqual(user.id, Locals.userId1) + XCTAssertEqual(user.name, Locals.username1) + } + + func testJsonDecodedArray() { + let jsonData = """ + [{"id": \(Locals.userId1), "name": "\(Locals.username1)"}, {"id": \(Locals.userId2), "name": "\(Locals.username2)"}] + """.data(using: .utf8)! + + guard let users: [User] = jsonData.jsonDecoded(type: User.self) else { + XCTFail("Error decoding data") + return + } + + XCTAssertEqual(users.count, 2) + XCTAssertEqual(users[0].id, Locals.userId1) + XCTAssertEqual(users[0].name, Locals.username1) + XCTAssertEqual(users[1].id, Locals.userId2) + XCTAssertEqual(users[1].name, Locals.username2) + } + + func testJsonDecodedWithUserInfo() { + let jsonData = """ + {"id": \(Locals.userId1), "name": "\(Locals.username1)"} + """.data(using: .utf8)! + + guard let key = CodingUserInfoKey(rawValue: "employer") else { + XCTFail("Error creating user key") + return + } + + let userInfo = [key: Locals.companyName] + + guard let user: User = jsonData.jsonDecoded(type: User.self, userInfo: userInfo) else { + XCTFail("Error decoding data") + return + } + + XCTAssertEqual(user.id, Locals.userId1) + XCTAssertEqual(user.name, Locals.username1) + XCTAssertEqual(user.employer, Locals.companyName) + } +} diff --git a/Packages/WalletKit/Tests/KeychainMock.swift b/Packages/WalletKit/Tests/KeychainMock.swift new file mode 100644 index 00000000..7e0e23ed --- /dev/null +++ b/Packages/WalletKit/Tests/KeychainMock.swift @@ -0,0 +1,75 @@ +// +// KeychainMock.swift +// +// +// Created by NUT.TECH on 16.02.2023. +// + +import Foundation +import WalletFoundation + +final class KeychainMock: KeychainProtocol { + + private enum KeychainMockLocals { + static let password = "PWD" + static let biometric = "BIO" + } + + typealias Value = (value: String, password: String) + + private var storage: [String: Value] + + init(storage: [String: Value]? = nil) { + self.storage = storage ?? [:] + } + + func exist(_ key: Key) -> Bool { + let commonKey = key.with(KeychainMockLocals.password) + return self.storage[commonKey.rawValue].isExist + } + + func bioExist(_ key: Key) -> Bool { + let commonKey = key.with(KeychainMockLocals.biometric) + return self.storage[commonKey.rawValue] != nil + } + + subscript(biometric key: Key) -> String? { + let commonKey = key.with(KeychainMockLocals.biometric).rawValue + let object = self.storage[commonKey] + return object?.value + } + + subscript(key: Key, password password: String) -> String? { + get { + let commonKey = key.with(KeychainMockLocals.password).rawValue + let object = self.storage[commonKey] + return object?.password == password ? object?.value : nil + } + set { + self.update(key, password: password, newValue: newValue) + } + } + + private func update(_ key: Key, password: String, newValue: String?) { + if let value = newValue { + self.updateStorage(key: key.with(KeychainMockLocals.password), password: password, value: value) + self.updateStorage(key: key.with(KeychainMockLocals.biometric), password: password, value: value) + } else { + self.updateStorage(key: key.with(KeychainMockLocals.password), password: password, value: nil) + self.updateStorage(key: key.with(KeychainMockLocals.biometric), password: password, value: nil) + } + } + + private func updateStorage(key: Key, password: String, value: String?) { + let stringKey = key.rawValue + if self.storage[stringKey] != nil, self.storage[stringKey]?.password == password { + if let value { + self.storage[stringKey]?.value = value + } else { + self.storage.removeValue(forKey: stringKey) + } + } else if let value { + self.storage[stringKey] = Value(value: value, password: password) + } + } +} diff --git a/Packages/WalletKit/Tests/PrivacyKeeperTests.swift b/Packages/WalletKit/Tests/PrivacyKeeperTests.swift new file mode 100644 index 00000000..19ee5d09 --- /dev/null +++ b/Packages/WalletKit/Tests/PrivacyKeeperTests.swift @@ -0,0 +1,103 @@ +// +// PrivacyKeeperTests.swift +// +// +// Created by user on 16.02.2023. +// + +import XCTest +@testable import WalletKit +@testable import WalletFoundation + +final class PrivacyKeeperTests: XCTestCase { + + func testAES() throws { + let commonKey = "PrivacyKeeper.password" + let encryptedKey = "mdHBVh0lxLT8VBJnL9MYuM/pxWi/QmTCXmW8YZQO8Cs=" + let key = "96080AE5042241D9BFF30330C9625201" + let iv = "7C515B5233314B42" + do { + let encryptedCommonKey = try PrivacyKeeper.aesEncrypt(string: commonKey, key: key, iv: iv) + + let decryptedCommonKey = try PrivacyKeeper.aesDecrypt(string: encryptedCommonKey, key: key, iv: iv) + + XCTAssertEqual(encryptedCommonKey, encryptedKey, "Incorrect encryption result") + XCTAssertEqual(decryptedCommonKey, commonKey, "Incorrect decryption result") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testPassword() { + let password = "0000" + let keychainMock = KeychainMock() + let keeper = PrivacyKeeper(keychain: keychainMock) + do { + XCTAssertFalse(keeper.isPasswordExists, "Incorrect isPasswordExists result") + + try keeper.update(password: password) + XCTAssertTrue(keeper.isPasswordExists, "Incorrect isPasswordExists result") + XCTAssertTrue(keeper.accept(password: password), "Incorrect password") + + let newPassword = "1111" + try keeper.update(password: newPassword, old: password) + XCTAssertTrue(keeper.accept(password: newPassword), "Incorrect password") + + let incorrectPassword = "2222" + XCTAssertFalse(keeper.accept(password: incorrectPassword), "Incorrect password check") + + keeper.reset() + XCTAssertFalse(keeper.accept(password: newPassword), "Incorrect password reset") + XCTAssertFalse(keeper.isPasswordExists, "Incorrect isPasswordExists result") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testPrivateKey() { + let password = "0000" + let privateKey = "privateKey1111" + let commonKey = "accountKey@malinka.privateKey" + let keychainMock = KeychainMock() + let keeper = PrivacyKeeper(keychain: keychainMock) + + do { + try keeper.update(password: password) + XCTAssertTrue(keeper.accept(password: password), "Incorrect password") + + keeper.update(privateKey: privateKey, + for: commonKey, + password: password) + let readPrivateKey = keeper.privateKey(for: commonKey, password: password) + XCTAssertNotNil(readPrivateKey, "Error setting private key to keychain") + XCTAssertEqual(readPrivateKey, privateKey, "Incorrect private key stored in keychain") + + keeper.reset() + let nilPrivateKey = keeper.privateKey(for: commonKey, password: password) + XCTAssertNil(nilPrivateKey, "Incorrect private key reset") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testMigration() { + let password = "0000" + let privateKey = "privateKey1111" + let privateKey2 = "privateKey2222" + let migratedCommonKey = "accountName@owner.privateKey" + let keyToMigrate = CommonKey("accountKey2@malinka.privateKey") + let oldKey = CommonKey.key("accountName", suffix: "privateKey") + let newKey = CommonKey.key("accountName@owner", suffix: "privateKey") + let storage = ["accountName.privateKey.PWD": KeychainMock.Value(value: privateKey, password: password), + "PrivacyKeeper.password.PWD": KeychainMock.Value(value: password, password: password), + "accountKey2@malinka.privateKey.PWD": KeychainMock.Value(value: privateKey2, password: password)] + let keychainMock = KeychainMock(storage: storage) + let keeper = PrivacyKeeper(keychain: keychainMock) + keeper.migrate(oldKey: oldKey, newKey: newKey, password: password) + XCTAssertNil(keeper.privateKey(for: oldKey.rawValue, password: password), "Incorrect private key") + XCTAssertEqual(keeper.privateKey(for: migratedCommonKey, password: password), privateKey, "Incorrect private key") + + keeper.migrate(oldKey: keyToMigrate, newKey: keyToMigrate, password: password) + XCTAssertEqual(keeper.privateKey(for: keyToMigrate.rawValue, password: password), privateKey2, "Incorrect private key") + } +} diff --git a/iOS/Wallet/Sources/Account/Controller/AccountControllerManual.swift b/iOS/Wallet/Sources/Account/Controller/AccountControllerManual.swift index 3befa872..03422ecf 100644 --- a/iOS/Wallet/Sources/Account/Controller/AccountControllerManual.swift +++ b/iOS/Wallet/Sources/Account/Controller/AccountControllerManual.swift @@ -205,7 +205,9 @@ final class AccountControllerManual: UIViewController { for (_, accounts) in accountsToAdd { do { - try Accounts().addAccounts(purses: accounts.map(\.purse)) + if let password = Account.Service.Authorize.shared.password { + try Accounts().addAccounts(purses: accounts.map(\.purse), password: password) + } Loader.hide(in: self) } catch { Loader.hide(in: self) diff --git a/iOS/Wallet/Sources/Account/Controller/SettingsViewController/SettingsViewController.swift b/iOS/Wallet/Sources/Account/Controller/SettingsViewController/SettingsViewController.swift index c025541b..6192402f 100644 --- a/iOS/Wallet/Sources/Account/Controller/SettingsViewController/SettingsViewController.swift +++ b/iOS/Wallet/Sources/Account/Controller/SettingsViewController/SettingsViewController.swift @@ -73,28 +73,26 @@ final class SettingsViewController: UIViewController { let ctrl = UINavigationController(rootViewController: accountControollerPin) accountControollerPin.type = .old accountControollerPin.viewModel.validatePin = { [weak ctrl, weak self] (pin: String) -> Bool in - guard let password = Account.Service.getPassword(password: pin), - !password.isEmpty, password == pin, password.count == 4 else { + guard Account.Service.check(password: pin), !pin.isEmpty, pin.count == 4 else { Account.Service.Authorize.shared.savePinTry() return false } ctrl?.dismiss(animated: false) - Account.Service.Authorize.shared.update(password: password) + Account.Service.Authorize.shared.update(password: pin) let accountControollerPin = WalletPinController.instantiate() let ctrl = UINavigationController(rootViewController: accountControollerPin) accountControollerPin.type = .new - accountControollerPin.viewModel.validatePin = { [weak ctrl] (pin: String) -> Bool in - // TODO: It seems that there needs to place if Accounts().bank.accept(password:) instead of Account.Service.getPassword(password:), but compromise solution is to refactor it after 1.4.0 - if let oldPassword = Account.Service.getPassword(password: password) { + accountControollerPin.viewModel.validatePin = { [weak ctrl] (newPin: String) -> Bool in + if Account.Service.check(password: pin) { do { - try Accounts().update(password: pin, old: oldPassword) + try Accounts().update(password: newPin, old: pin) } catch { Alert.error(text: L10n.Account.Password.currentWrong) return false } - Account.Service.Authorize.shared.update(password: pin) + Account.Service.Authorize.shared.update(password: newPin) ctrl?.dismiss(animated: true) return true } else { @@ -143,7 +141,7 @@ final class SettingsViewController: UIViewController { let ctrl = UINavigationController(rootViewController: accountControollerPin) accountControollerPin.type = .new accountControollerPin.viewModel.validatePin = { [weak ctrl] (pin: String) -> Bool in - if let oldPassword = Account.Service.getPassword(password: oldPassword) { + if Account.Service.check(password: oldPassword) { do { try Accounts().update(password: pin, old: oldPassword) UserDefaults.standard.setValue(!UserDefaults.standard.bool(forKey: "isPinPwdMode"), forKey: "isPinPwdMode") diff --git a/iOS/Wallet/Sources/Account/Service/AccountService.swift b/iOS/Wallet/Sources/Account/Service/AccountService.swift index 9aa25b9c..44bb1f33 100644 --- a/iOS/Wallet/Sources/Account/Service/AccountService.swift +++ b/iOS/Wallet/Sources/Account/Service/AccountService.swift @@ -121,9 +121,9 @@ extension Account { // MARK: - Edit extension Account.Service { - func addAccounts(purses: [Purse]) throws { + func addAccounts(purses: [Purse], password: String) throws { - try self.bank.add(using: purses) + try self.bank.add(using: purses, password: password) if let wallet = self.collection.last, let book = self.messageShelf.book(for: wallet) { @@ -150,7 +150,7 @@ extension Account.Service { $0.name == accountToChange.wallet.name && $0.keyType == accountToChange.wallet.keyType }) else { continue } - account.update(key: accountToChange.key, using: password) + account.updateKeychain(key: accountToChange.key, using: password) } } @@ -158,7 +158,7 @@ extension Account.Service { for accountToChange in accounts { guard let oldPrivateKey = Accounts().collection.first(where: { $0.name == accountToChange.wallet.name - && $0.keyType == accountToChange.keyType })?.privateKey(password) else { continue } + && $0.keyType == accountToChange.keyType })?.privateKey(password: password) else { continue } guard let msgsService = try? CryptoChat.Service.MsgsHistory(username: accountToChange.wallet.name, encryptionKey: oldPrivateKey) else { continue } @@ -215,10 +215,9 @@ extension Account.Service { var isAuthorized: Bool { if UserDefaults.standard.bool(forKey: "isPinPwdMode") { if let pin = Account.Service.Authorize.shared.password, - let password = Self.getPassword(password: pin), - !password.isEmpty, - password == pin, - password.count == 4 { + Self.check(password: pin), + !pin.isEmpty, + pin.count == 4 { return true } else { return false @@ -228,14 +227,13 @@ extension Account.Service { } } - static func getPassword(password: String?) -> String? { Village.getPassword(password: password) } + static var isPasswordExists: Bool { Village.isPasswordExists } static var isBiometricksEnabled: Bool { get { Settings.shared[isBiometricksEnabledKey] } set { Settings.shared[isBiometricksEnabledKey] = newValue } } - static var isPasswordExists: Bool { Village.isPasswordExists } static var isAccountMigrated: Bool { get { Settings.shared[isAccountMigratedKey] } set { Settings.shared[isAccountMigratedKey] = newValue } @@ -255,14 +253,13 @@ extension Account.Service { DispatchQueue.main.async { completion(success) } } } + + static func check(password: String) -> Bool { Village.accept(password: password) } + + static func passwordViaBiometrics() -> String? { Village.passwordViaBiometrics() } func update(password: String, old: String? = nil) throws { - try old >>- { - //Compromise solution for 1.4.0 - //TODO: Refactor this later - try Village.updateCommonPassword(password: password, old: $0) - try self.bank.switchPassword(password, old: $0) - } + try old >>- { try self.bank.switchPassword(password, old: $0) } } func validate(password value: String) -> Bool { value.count >= 4 } diff --git a/iOS/Wallet/Sources/Account/Service/AccountServiceAuthorize.swift b/iOS/Wallet/Sources/Account/Service/AccountServiceAuthorize.swift index 63037cd0..b7e62b5e 100644 --- a/iOS/Wallet/Sources/Account/Service/AccountServiceAuthorize.swift +++ b/iOS/Wallet/Sources/Account/Service/AccountServiceAuthorize.swift @@ -9,6 +9,7 @@ import Foundation import Combine import WalletFoundation +import WalletKit private let pinTriesKey = CommonKey("Account.Service.Authorize.pinTries") @@ -43,7 +44,12 @@ extension Account.Service { func update(password value: String?, type: UpdateType = .change) { switch type { - case .change: self.password = value + case .change: + if self.password == nil, let value { + // TODO: Call only once on initialization, not on update! Needs to refactor in AccountServiceAuthorize task + try? Village.set(password: value) + } + self.password = value case .toggle: self.updateAuthSubject.send(()) case .both: self.password = value diff --git a/iOS/Wallet/Sources/Account/Service/AccountServiceChangeKey.swift b/iOS/Wallet/Sources/Account/Service/AccountServiceChangeKey.swift index 9e6f816b..cfce0b50 100644 --- a/iOS/Wallet/Sources/Account/Service/AccountServiceChangeKey.swift +++ b/iOS/Wallet/Sources/Account/Service/AccountServiceChangeKey.swift @@ -48,7 +48,7 @@ extension Account.Service { _ completion: @escaping Completion) { guard let wallet = accounts.first?.wallet, - let privateKey = wallet.privateKey(password) else { + let privateKey = wallet.privateKey(password: password) else { completion(.success("None")) return } diff --git a/iOS/Wallet/Sources/Account/View/Authorize/AccountViewAuthorize.swift b/iOS/Wallet/Sources/Account/View/Authorize/AccountViewAuthorize.swift index 16480477..1ac759ef 100644 --- a/iOS/Wallet/Sources/Account/View/Authorize/AccountViewAuthorize.swift +++ b/iOS/Wallet/Sources/Account/View/Authorize/AccountViewAuthorize.swift @@ -54,19 +54,13 @@ final class AccountViewAuthorize: CommonViewCustom { static func getPrivateKey() -> String { - if UserDefaults.standard.bool(forKey: "isPinPwdMode") { - if let pin = Account.Service.Authorize.shared.password, - let password = Account.Service.getPassword(password: pin), - !password.isEmpty, - password == pin, - password.count == 4, - let value = Accounts().current?.privateKey(password) { - return value - } - return "" - } else { - return "" - } + if UserDefaults.standard.bool(forKey: "isPinPwdMode"), + let pin = Account.Service.Authorize.shared.password, + Account.Service.check(password: pin), + let value = Accounts().current?.privateKey(password: pin) { + return value + } + return "" } static func showGetPassword(in viewController: UIViewController, @@ -92,14 +86,11 @@ final class AccountViewAuthorize: CommonViewCustom { if UserDefaults.standard.bool(forKey: "isPinPwdMode") { if let pin = Account.Service.Authorize.shared.password, - let password = Account.Service.getPassword(password: pin), - !password.isEmpty, - password == pin, - password.count == 4 { + Account.Service.check(password: pin), + !pin.isEmpty, + pin.count == 4 { - completion(isPasswordRequired - ? password - : Accounts().current?.privateKey(password) ?? "") + completion(isPasswordRequired ? pin : Accounts().current?.privateKey(password: pin) ?? "") return } @@ -107,18 +98,15 @@ final class AccountViewAuthorize: CommonViewCustom { let ctrl = UINavigationController(rootViewController: accountControollerPin) accountControollerPin.showBackButton = showPinBackButton accountControollerPin.viewModel.validatePin = { [weak ctrl] (pin: String) -> Bool in - if let password = Account.Service.getPassword(password: pin), - !password.isEmpty, - password == pin, - password.count == 4 { + if Account.Service.check(password: pin), + !pin.isEmpty, + pin.count == 4 { ctrl?.dismiss(animated: true) - Account.Service.Authorize.shared.update(password: password, type: .both) + Account.Service.Authorize.shared.update(password: pin, type: .both) - completion(isPasswordRequired - ? password - : Accounts().current?.privateKey(password) ?? "") + completion(isPasswordRequired ? pin : Accounts().current?.privateKey(password: pin) ?? "") Account.Service.Authorize.shared.clearPinTries() return true @@ -128,16 +116,16 @@ final class AccountViewAuthorize: CommonViewCustom { } } accountControollerPin.viewModel.submitBiometric = { [weak ctrl] in - if let password = Account.Service.getPassword(password: nil), - !password.isEmpty, - password.count == 4 { + if let pin = Account.Service.passwordViaBiometrics(), + !pin.isEmpty, + pin.count == 4 { - Account.Service.Authorize.shared.update(password: password, type: .both) + Account.Service.Authorize.shared.update(password: pin, type: .both) ctrl?.dismiss(animated: true) { completion(isPasswordRequired - ? password - : Accounts().current?.privateKey(password) ?? "") + ? pin + : Accounts().current?.privateKey(password: pin) ?? "") } } } @@ -167,13 +155,15 @@ final class AccountViewAuthorize: CommonViewCustom { @IBAction private func onSubmit(_ : AnyObject?) { if isGetPassword { - if let password = Account.Service.getPassword(password: textField.lzText ?? ""), !password.isEmpty { - self.didSubmit?(password) + if let pin = textField.lzText, Account.Service.check(password: pin), !pin.isEmpty { + self.didSubmit?(pin) } else { self.textField.error = L10n.Account.Authorize.error } } else { - if let privateKey = Accounts().current?.privateKey(textField.lzText ?? ""), !privateKey.isEmpty { + if let pin = textField.lzText, + let privateKey = Accounts().current?.privateKey(password: pin), + !privateKey.isEmpty { self.didSubmit?(privateKey) } else { self.textField.error = L10n.Account.Authorize.error @@ -183,14 +173,14 @@ final class AccountViewAuthorize: CommonViewCustom { @IBAction private func onFaceId(_ : AnyObject?) { if isGetPassword { - if let password = Account.Service.getPassword(password: nil), + if let password = Account.Service.passwordViaBiometrics(), !password.isEmpty { self.didSubmit?(password) } else { self.textField.error = L10n.Account.Authorize.error } } else { - if let privateKey = Accounts().current?.privateKey(nil), + if let privateKey = Accounts().current?.privateKey(password: nil), !privateKey.isEmpty { self.didSubmit?(privateKey) } else { diff --git a/iOS/Wallet/Sources/Account/View/Password/AccountViewPassword.swift b/iOS/Wallet/Sources/Account/View/Password/AccountViewPassword.swift index 2efef314..3743b8fe 100644 --- a/iOS/Wallet/Sources/Account/View/Password/AccountViewPassword.swift +++ b/iOS/Wallet/Sources/Account/View/Password/AccountViewPassword.swift @@ -77,14 +77,8 @@ final class AccountViewPassword: CommonViewCustom { } @IBAction private func onSubmit(_ : AnyObject?) { - - guard let oldPassword = Account.Service.getPassword(password: passwordTxtFld.value) else { - Alert.error(text: L10n.Account.Password.currentWrong) - return - } - do { - try Accounts().update(password: newPasswordTxtFld.value, old: oldPassword) + try Accounts().update(password: newPasswordTxtFld.value, old: passwordTxtFld.value) didSubmit?() } catch { Alert.error(text: L10n.Account.Password.currentWrong) diff --git a/iOS/Wallet/Sources/Account/View/Settings/AccountViewSettings.swift b/iOS/Wallet/Sources/Account/View/Settings/AccountViewSettings.swift index e915e09c..b046b7d9 100644 --- a/iOS/Wallet/Sources/Account/View/Settings/AccountViewSettings.swift +++ b/iOS/Wallet/Sources/Account/View/Settings/AccountViewSettings.swift @@ -74,5 +74,4 @@ final class AccountViewSettings: CommonViewCustom { @IBAction private func onChangeProtection(_ sender: Any) { self.actionSubject.send(.changeProtection) } - } diff --git a/iOS/Wallet/Sources/Flows/PinFlow/Controller/WalletPinController.swift b/iOS/Wallet/Sources/Flows/PinFlow/Controller/WalletPinController.swift index 3e427f15..ebe71580 100644 --- a/iOS/Wallet/Sources/Flows/PinFlow/Controller/WalletPinController.swift +++ b/iOS/Wallet/Sources/Flows/PinFlow/Controller/WalletPinController.swift @@ -7,6 +7,7 @@ // import UIKit +import WalletKit final class WalletPinController: UIViewController { @@ -216,13 +217,14 @@ final class WalletPinController: UIViewController { if repeatPin == Constants.NoPinDigits, pinCount == Constants.maxPinLength { self.headerLabelRepeatSetup() - self.repeatPin = pin + self.repeatPin = self.pin self.pin = [] } else if repeatPin == Constants.maxPinLength, pinCount == Constants.maxPinLength, self.repeatPin == self.pin { - Account.Service.Authorize.shared.update(password: self.pin.map { "\($0)" }.joined()) - successShow() + let pinCode = self.pin.map { "\($0)" }.joined() + Account.Service.Authorize.shared.update(password: pinCode) + self.successShow() } } } diff --git a/iOS/Wallet/Sources/Flows/WalletCreation/Service/CreateWalletService.swift b/iOS/Wallet/Sources/Flows/WalletCreation/Service/CreateWalletService.swift index a576f2b6..e6e0197f 100644 --- a/iOS/Wallet/Sources/Flows/WalletCreation/Service/CreateWalletService.swift +++ b/iOS/Wallet/Sources/Flows/WalletCreation/Service/CreateWalletService.swift @@ -28,8 +28,8 @@ struct CreateWalletService { // MARK: - Methods func create(for password: String) async throws -> UpdateWalletStateService { - let bank = self.village.getBank(with: password, on: self.device) - let wallet = try await bank.add(using: self.walletCase) + let bank = try self.village.getBank(with: password, on: self.device) + let wallet = try await bank.add(using: self.walletCase, password: password) return UpdateWalletStateService(bank: bank, wallet: wallet) } } diff --git a/iOS/Wallet/Sources/OnBoarding/OnBoardingWalletController.swift b/iOS/Wallet/Sources/OnBoarding/OnBoardingWalletController.swift index ef0cfc5f..de57583b 100644 --- a/iOS/Wallet/Sources/OnBoarding/OnBoardingWalletController.swift +++ b/iOS/Wallet/Sources/OnBoarding/OnBoardingWalletController.swift @@ -152,7 +152,7 @@ final class OnBoardingWalletController: UIViewController { guard let device = try? await village.device() else { throw NSError() } - let bank = village.getBank(with: password, on: device) + let bank = try village.getBank(with: password, on: device) Account.Service.shared = Account.Service(using: bank) self.loadingGroup.notify(queue: DispatchQueue.main) {