From 181f83b1cb5aab90e2e65973c48190815ede90d2 Mon Sep 17 00:00:00 2001 From: Ivan Smolin Date: Mon, 25 Jul 2022 11:47:23 +0300 Subject: [PATCH] feat: pin code --- Package.swift | 2 +- TIAuth/Sources/Biometry/BiometryService.swift | 35 +++ .../Biometry/BiometrySettingsStorage.swift | 27 ++ .../DefaultBiometrySettingsStorage.swift | 48 ++++ .../CodeConfirmPresenter.swift | 7 +- .../CodeConfirmStateStorage.swift | 2 - .../DefaultCodeConfirmPresenter.swift | 10 +- TIAuth/Sources/Cryptography/AESCipher.swift | 93 +++++++ TIAuth/Sources/Cryptography/Cipher.swift | 28 ++ TIAuth/Sources/Cryptography/CipherError.swift | 28 ++ .../DefaultPBKDF2PasswordDerivator.swift | 78 ++++++ .../DefaultSaltPreprocessor.swift | 41 +++ .../Cryptography/DefaultTokenCipher.swift | 97 +++++++ .../Cryptography/PasswordDerivator.swift | 27 ++ .../Cryptography/SaltPreprocessor.swift | 27 ++ TIAuth/Sources/Cryptography/TokenCipher.swift | 31 +++ .../DefaultPinCodePresenter.swift | 261 ++++++++++++++++++ .../PinCodePresenter/PinCodeAction.swift | 54 ++++ .../PinCodePresenter/PinCodePresenter.swift | 54 ++++ .../PinCodeScenario/BasePinCodeScenario.swift | 98 +++++++ .../CreatePinCodeScenario.swift | 67 +++++ .../DecryptStoredTokenScenario.swift | 154 +++++++++++ .../PinCodeScenario/PinCodeScenario.swift | 30 ++ .../UseBiometryLoginScenario.swift | 57 ++++ .../EqualDigitsValidationRule.swift | 35 +++ .../OrderedDigitsValidationRule.swift | 39 +++ .../String+PinCodeValidation.swift | 91 ++++++ .../ValidationRules/ValidationRule.swift | 25 ++ .../Validator/DefaultInputValidator.swift | 39 +++ .../Validator/DefaultViolation.swift | 35 +++ .../Validator/InputValidator.swift | 29 ++ .../TokenStorage/AuthSettingsStorage.swift | 28 ++ .../TokenStorage/CryptoConstants.swift | 45 +++ .../DefaultAuthSettingsStorage.swift | 52 ++++ .../DefaultEncryptedTokenKeyStorage.swift | 71 +++++ .../DefaultEncryptedTokenStorage.swift | 67 +++++ .../SingleValueAuthKeychainStorage.swift | 84 ++++++ .../TokenStorage/StringEncryptionResult.swift | 67 +++++ TIAuth/TIAuth.podspec | 2 + .../DataStorage/AppReinstallChecker.swift | 48 ++++ .../DataStorage/SingleValueStorage.swift | 30 ++ .../DefaultFingerprintsSettingsStorage.swift | 51 ++++ TISwiftUICore/TISwiftUICore.podspec | 2 +- .../Presenter/DefaultUIViewPresenter.swift | 2 +- .../Presenter/LifecyclePresenter.swift | 27 ++ .../Presenter/ReusableUIViewPresenter.swift | 1 + ...Presenters.swift => UIViewPresenter.swift} | 1 + 47 files changed, 2215 insertions(+), 12 deletions(-) create mode 100644 TIAuth/Sources/Biometry/BiometryService.swift create mode 100644 TIAuth/Sources/Biometry/BiometrySettingsStorage.swift create mode 100644 TIAuth/Sources/Biometry/DefaultBiometrySettingsStorage.swift create mode 100644 TIAuth/Sources/Cryptography/AESCipher.swift create mode 100644 TIAuth/Sources/Cryptography/Cipher.swift create mode 100644 TIAuth/Sources/Cryptography/CipherError.swift create mode 100644 TIAuth/Sources/Cryptography/DefaultPBKDF2PasswordDerivator.swift create mode 100644 TIAuth/Sources/Cryptography/DefaultSaltPreprocessor.swift create mode 100644 TIAuth/Sources/Cryptography/DefaultTokenCipher.swift create mode 100644 TIAuth/Sources/Cryptography/PasswordDerivator.swift create mode 100644 TIAuth/Sources/Cryptography/SaltPreprocessor.swift create mode 100644 TIAuth/Sources/Cryptography/TokenCipher.swift create mode 100644 TIAuth/Sources/PinCodePresenter/DefaultPinCodePresenter.swift create mode 100644 TIAuth/Sources/PinCodePresenter/PinCodeAction.swift create mode 100644 TIAuth/Sources/PinCodePresenter/PinCodePresenter.swift create mode 100644 TIAuth/Sources/PinCodeScenario/BasePinCodeScenario.swift create mode 100644 TIAuth/Sources/PinCodeScenario/CreatePinCodeScenario.swift create mode 100644 TIAuth/Sources/PinCodeScenario/DecryptStoredTokenScenario.swift create mode 100644 TIAuth/Sources/PinCodeScenario/PinCodeScenario.swift create mode 100644 TIAuth/Sources/PinCodeScenario/UseBiometryLoginScenario.swift create mode 100644 TIAuth/Sources/PinCodeValidation/ValidationRules/EqualDigitsValidationRule.swift create mode 100644 TIAuth/Sources/PinCodeValidation/ValidationRules/OrderedDigitsValidationRule.swift create mode 100644 TIAuth/Sources/PinCodeValidation/ValidationRules/String+PinCodeValidation.swift create mode 100644 TIAuth/Sources/PinCodeValidation/ValidationRules/ValidationRule.swift create mode 100644 TIAuth/Sources/PinCodeValidation/Validator/DefaultInputValidator.swift create mode 100644 TIAuth/Sources/PinCodeValidation/Validator/DefaultViolation.swift create mode 100644 TIAuth/Sources/PinCodeValidation/Validator/InputValidator.swift create mode 100644 TIAuth/Sources/TokenStorage/AuthSettingsStorage.swift create mode 100644 TIAuth/Sources/TokenStorage/CryptoConstants.swift create mode 100644 TIAuth/Sources/TokenStorage/DefaultAuthSettingsStorage.swift create mode 100644 TIAuth/Sources/TokenStorage/DefaultEncryptedTokenKeyStorage.swift create mode 100644 TIAuth/Sources/TokenStorage/DefaultEncryptedTokenStorage.swift create mode 100644 TIAuth/Sources/TokenStorage/SingleValueAuthKeychainStorage.swift create mode 100644 TIAuth/Sources/TokenStorage/StringEncryptionResult.swift create mode 100644 TIFoundationUtils/DataStorage/AppReinstallChecker.swift create mode 100644 TIFoundationUtils/DataStorage/SingleValueStorage.swift create mode 100644 TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/DefaultFingerprintsSettingsStorage.swift create mode 100644 TIUIKitCore/Sources/Presenter/LifecyclePresenter.swift rename TIUIKitCore/Sources/Presenter/{UIViewPresenters.swift => UIViewPresenter.swift} (99%) diff --git a/Package.swift b/Package.swift index 0dc55fe0..b32d430b 100644 --- a/Package.swift +++ b/Package.swift @@ -81,7 +81,7 @@ let package = Package( .target(name: "OTPSwiftView", dependencies: ["TIUIElements"], path: "OTPSwiftView/Sources"), .target(name: "TITransitions", path: "TITransitions/Sources"), .target(name: "TIPagination", dependencies: ["Cursors", "TISwiftUtils"], path: "TIPagination/Sources"), - .target(name: "TIAuth", dependencies: ["TIFoundationUtils"], path: "TIAuth/Sources"), + .target(name: "TIAuth", dependencies: ["TIFoundationUtils", "TIUIKitCore", "KeychainAccess"], path: "TIAuth/Sources"), //MARK: - Skolkovo .target(name: "TIEcommerce", dependencies: ["TIFoundationUtils", "TISwiftUtils", "TINetworking"], path: "TIEcommerce/Sources"), diff --git a/TIAuth/Sources/Biometry/BiometryService.swift b/TIAuth/Sources/Biometry/BiometryService.swift new file mode 100644 index 00000000..112db509 --- /dev/null +++ b/TIAuth/Sources/Biometry/BiometryService.swift @@ -0,0 +1,35 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation +import LocalAuthentication + +public protocol BiometryService { + var isBiometryAuthAvailable: Bool { get } + var biometryType: LABiometryType { get } +} + +extension LAContext: BiometryService { + public var isBiometryAuthAvailable: Bool { + canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) + } +} diff --git a/TIAuth/Sources/Biometry/BiometrySettingsStorage.swift b/TIAuth/Sources/Biometry/BiometrySettingsStorage.swift new file mode 100644 index 00000000..321bf886 --- /dev/null +++ b/TIAuth/Sources/Biometry/BiometrySettingsStorage.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public protocol BiometrySettingsStorage { + var isBiometryAuthEnabled: Bool { get set } +} diff --git a/TIAuth/Sources/Biometry/DefaultBiometrySettingsStorage.swift b/TIAuth/Sources/Biometry/DefaultBiometrySettingsStorage.swift new file mode 100644 index 00000000..195fe064 --- /dev/null +++ b/TIAuth/Sources/Biometry/DefaultBiometrySettingsStorage.swift @@ -0,0 +1,48 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +open class DefaultBiometrySettingsStorage: BiometrySettingsStorage { + public enum StorageKeys { + static var isBiometryAuthEnabledStorageKey: String { + "isBiometryAuthEnabled" + } + } + + public var defaultsStorage: UserDefaults + + // MARK: - BiometrySettingsService + + public var isBiometryAuthEnabled: Bool { + get { + defaultsStorage.bool(forKey: StorageKeys.isBiometryAuthEnabledStorageKey) + } + set { + defaultsStorage.set(newValue, forKey: StorageKeys.isBiometryAuthEnabledStorageKey) + } + } + + public init(defaultsStorage: UserDefaults = .standard) { + self.defaultsStorage = defaultsStorage + } +} diff --git a/TIAuth/Sources/CodeConfirmPresenter/CodeConfirmPresenter.swift b/TIAuth/Sources/CodeConfirmPresenter/CodeConfirmPresenter.swift index 156b0e51..8ad2223d 100644 --- a/TIAuth/Sources/CodeConfirmPresenter/CodeConfirmPresenter.swift +++ b/TIAuth/Sources/CodeConfirmPresenter/CodeConfirmPresenter.swift @@ -21,18 +21,15 @@ // import Foundation +import TIUIKitCore @MainActor -protocol CodeConfirmPresenter { +protocol CodeConfirmPresenter: LifecyclePresenter { // MARK: - User actions handling func inputChanged(newInput: String?) func refreshCode() - // MARK: - View lifecycle handling - - func viewDidPresented() - // MARK: - Autofill func autofill(code: String, with codeId: String?) diff --git a/TIAuth/Sources/CodeConfirmPresenter/CodeConfirmStateStorage.swift b/TIAuth/Sources/CodeConfirmPresenter/CodeConfirmStateStorage.swift index c13a9dc7..b04e949d 100644 --- a/TIAuth/Sources/CodeConfirmPresenter/CodeConfirmStateStorage.swift +++ b/TIAuth/Sources/CodeConfirmPresenter/CodeConfirmStateStorage.swift @@ -20,8 +20,6 @@ // THE SOFTWARE. // -import Foundation - @MainActor public protocol CodeConfirmStateStorage: AnyObject { var currentUserInput: String? { get set } diff --git a/TIAuth/Sources/CodeConfirmPresenter/DefaultCodeConfirmPresenter.swift b/TIAuth/Sources/CodeConfirmPresenter/DefaultCodeConfirmPresenter.swift index bad77a05..11472480 100644 --- a/TIAuth/Sources/CodeConfirmPresenter/DefaultCodeConfirmPresenter.swift +++ b/TIAuth/Sources/CodeConfirmPresenter/DefaultCodeConfirmPresenter.swift @@ -77,6 +77,8 @@ open class DefaultCodeConfirmPresenter= config.codeLength { stateStorage?.isExecutingRequest = true - Task { + executingTask = Task { await confirm(code: code) } } @@ -200,7 +202,7 @@ open class DefaultCodeConfirmPresenter Result { + crypt(data: data, operation: CCOperation(kCCEncrypt)) + } + + public func decrypt(data: Data) -> Result { + crypt(data: data, operation: CCOperation(kCCDecrypt)) + } + + private func crypt(data: Data, operation: CCOperation) -> Result { + let cryptDataLength = data.count + kCCBlockSizeAES128 + var cryptData = Data(count: cryptDataLength) + + var bytesLength = Int.zero + + let status = cryptData.withUnsafeMutableBytes { cryptBytes in + data.withUnsafeBytes { dataBytes in + iv.withUnsafeBytes { ivBytes in + key.withUnsafeBytes { keyBytes in + CCCrypt(operation, + CCAlgorithm(kCCAlgorithmAES), + CCOptions(kCCOptionPKCS7Padding), + keyBytes.baseAddress, + key.count, + ivBytes.baseAddress, + dataBytes.baseAddress, + data.count, + cryptBytes.baseAddress, + cryptDataLength, + &bytesLength) + } + } + } + } + + guard status == kCCSuccess else { + let error = CryptError(ccCryptorStatus: status, + key: key, + iv: iv) + + if operation == kCCEncrypt { + return .failure(.failedToEncrypt(data: data, error: error)) + } else { + return .failure(.failedToDecrypt(encryptedData: data, error: error)) + } + } + + cryptData.removeSubrange(bytesLength.. Result + func decrypt(data: Data) -> Result +} diff --git a/TIAuth/Sources/Cryptography/CipherError.swift b/TIAuth/Sources/Cryptography/CipherError.swift new file mode 100644 index 00000000..f34e750d --- /dev/null +++ b/TIAuth/Sources/Cryptography/CipherError.swift @@ -0,0 +1,28 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public enum CipherError: Error { + case failedToEncrypt(data: Data, error: Error) + case failedToDecrypt(encryptedData: Data, error: Error) +} diff --git a/TIAuth/Sources/Cryptography/DefaultPBKDF2PasswordDerivator.swift b/TIAuth/Sources/Cryptography/DefaultPBKDF2PasswordDerivator.swift new file mode 100644 index 00000000..7cc4bc1c --- /dev/null +++ b/TIAuth/Sources/Cryptography/DefaultPBKDF2PasswordDerivator.swift @@ -0,0 +1,78 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation +import CommonCrypto + +open class DefaultPBKDF2PasswordDerivator: PasswordDerivator { + public struct CryptError: Error { + public let derivationStatus: Int32 + public let password: String + public let salt: Data + + func asCipherError() -> CipherError { + var mutablePassword = password + return .failedToEncrypt(data: mutablePassword.withUTF8 { Data($0) }, + error: self) + } + } + + public func derive(password: String, salt: Data) -> Result { + var failureResult: Result? + + let derivedKeyBytes = Array(unsafeUninitializedCapacity: CryptoConstants.keyLength) { derivedKeyBuffer, initializedCount in + guard let derivedKeyStartAddress = derivedKeyBuffer.baseAddress else { + failureResult = .failure(CryptError(derivationStatus: CCStatus(kCCMemoryFailure), + password: password, + salt: salt) + .asCipherError()) + + initializedCount = .zero + return + } + + let deriviationStatus = salt.withContiguousStorageIfAvailable { saltBytes in + CCKeyDerivationPBKDF( + CCPBKDFAlgorithm(kCCPBKDF2), + password, + password.count, + saltBytes.baseAddress, + salt.count, + CCPBKDFAlgorithm(kCCPRFHmacAlgSHA512), + UInt32(CryptoConstants.pbkdf2NumberOfIterations), + derivedKeyStartAddress, + CryptoConstants.keyLength) + } ?? CCStatus(kCCParamError) + + guard deriviationStatus == kCCSuccess else { + initializedCount = .zero + failureResult = .failure(CryptError(derivationStatus: deriviationStatus, + password: password, + salt: salt) + .asCipherError()) + return + } + } + + return failureResult ?? .success(Data(derivedKeyBytes)) + } +} diff --git a/TIAuth/Sources/Cryptography/DefaultSaltPreprocessor.swift b/TIAuth/Sources/Cryptography/DefaultSaltPreprocessor.swift new file mode 100644 index 00000000..392ec516 --- /dev/null +++ b/TIAuth/Sources/Cryptography/DefaultSaltPreprocessor.swift @@ -0,0 +1,41 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +open class DefaultSaltPreprocessor: SaltPreprocessor { + public typealias DeviceIdProviderClosure = () -> String? + + private let deviceIdProvider: DeviceIdProviderClosure + + public init(deviceIdProvider: @escaping DeviceIdProviderClosure) { + self.deviceIdProvider = deviceIdProvider + } + + // MARK: - SaltPreprocessor + + public func preprocess(salt: Data) -> Data { + deviceIdProvider().map { + salt + Data($0.utf8) + } ?? salt + } +} diff --git a/TIAuth/Sources/Cryptography/DefaultTokenCipher.swift b/TIAuth/Sources/Cryptography/DefaultTokenCipher.swift new file mode 100644 index 00000000..274448af --- /dev/null +++ b/TIAuth/Sources/Cryptography/DefaultTokenCipher.swift @@ -0,0 +1,97 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation +import Security + +open class DefaultTokenCipher: TokenCipher { + public var saltPreprocessor: SaltPreprocessor + public var passwordDerivator: PasswordDerivator + + public init(saltPreprocessor: SaltPreprocessor, passwordDerivator: PasswordDerivator) { + self.saltPreprocessor = saltPreprocessor + self.passwordDerivator = passwordDerivator + } + + open func createCipher(iv: Data, key: Data) -> Cipher { + AESCipher(iv: iv, key: key) + } + + open func generateIV() -> Data { + generateRandomData(count: CryptoConstants.ivLength) + } + + open func generateSalt() -> Data { + generateRandomData(count: CryptoConstants.saltLength) + } + + open func generateRandomData(count: Int) -> Data { + let randomBytes = Array(unsafeUninitializedCapacity: count) { buffer, initializedCount in + guard let startAddress = buffer.baseAddress, + SecRandomCopyBytes(kSecRandomDefault, + count, + startAddress) == errSecSuccess else { + + initializedCount = .zero + return + } + initializedCount = count + } + + return Data(randomBytes) + } + + // MARK: - TokenCipher + + open func derive(password: String, using salt: Data) -> Result { + passwordDerivator.derive(password: password, + salt: saltPreprocessor.preprocess(salt: salt)) + } + + open func encrypt(token: Data, using password: String) -> Result { + let iv = generateIV() + let salt = generateSalt() + + return derive(password: password, using: salt) + .flatMap { + createCipher(iv: iv, key: $0) + .encrypt(data: token) + .map { + StringEncryptionResult(salt: salt, iv: iv, value: $0) + } + } + } + + open func decrypt(token: StringEncryptionResult, using key: Data) -> Result { + createCipher(iv: token.iv, key: key) + .decrypt(data: token.value) + } + + open func decrypt(token: StringEncryptionResult, using password: String) -> Result { + passwordDerivator.derive(password: password, + salt: saltPreprocessor.preprocess(salt: token.salt)).flatMap { + createCipher(iv: token.iv, + key: $0) + .decrypt(data: token.value) + } + } +} diff --git a/TIAuth/Sources/Cryptography/PasswordDerivator.swift b/TIAuth/Sources/Cryptography/PasswordDerivator.swift new file mode 100644 index 00000000..17b97a02 --- /dev/null +++ b/TIAuth/Sources/Cryptography/PasswordDerivator.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public protocol PasswordDerivator { + func derive(password: String, salt: Data) -> Result +} diff --git a/TIAuth/Sources/Cryptography/SaltPreprocessor.swift b/TIAuth/Sources/Cryptography/SaltPreprocessor.swift new file mode 100644 index 00000000..2985c586 --- /dev/null +++ b/TIAuth/Sources/Cryptography/SaltPreprocessor.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public protocol SaltPreprocessor { + func preprocess(salt: Data) -> Data +} diff --git a/TIAuth/Sources/Cryptography/TokenCipher.swift b/TIAuth/Sources/Cryptography/TokenCipher.swift new file mode 100644 index 00000000..955165d9 --- /dev/null +++ b/TIAuth/Sources/Cryptography/TokenCipher.swift @@ -0,0 +1,31 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public protocol TokenCipher { + func derive(password: String, using salt: Data) -> Result + + func encrypt(token: Data, using password: String) -> Result + func decrypt(token: StringEncryptionResult, using key: Data) -> Result + func decrypt(token: StringEncryptionResult, using password: String) -> Result +} diff --git a/TIAuth/Sources/PinCodePresenter/DefaultPinCodePresenter.swift b/TIAuth/Sources/PinCodePresenter/DefaultPinCodePresenter.swift new file mode 100644 index 00000000..b21b3c89 --- /dev/null +++ b/TIAuth/Sources/PinCodePresenter/DefaultPinCodePresenter.swift @@ -0,0 +1,261 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import KeychainAccess +import Foundation + +@MainActor +public protocol PinCodeStateStorage: AnyObject { + var currentCodeInput: String { get set } + var progress: Progress { get } + var orderedPinPadActions: [PinCodeAction] { get set } +} + +open class DefaultPinCodePresenter: PinCodePresenter { + open class Output { + public typealias OnLogoutClosure = () -> Void + public typealias OnRecoverClosure = () -> Void + + public var onLogout: OnLogoutClosure + public var onRecover: OnRecoverClosure + + public init(onLogout: @escaping OnLogoutClosure, + onRecover: @escaping OnRecoverClosure) { + + self.onLogout = onLogout + self.onRecover = onRecover + } + } + + public struct Config { + public enum Defaults { + public static var codeLength: Int { + 4 + } + + public static var validator: DefaultInputValidator { + let violations: [DefaultViolation] = [ + .equalDigits(minEqualDigits: 3), + .orderedDigits(ascending: true, minLength: 3), + .orderedDigits(ascending: false, minLength: 3) + ] + + return DefaultInputValidator(violations: violations) { + $0.defaultValidationRule + } + } + } + + public var codeLength: Int + public var defaultInputValidator: DefaultInputValidator + + public init(codeLength: Int = Defaults.codeLength, + defaultInputValidator: DefaultInputValidator = Defaults.validator) { + + self.codeLength = codeLength + self.defaultInputValidator = defaultInputValidator + } + } + + public var output: Output + + public weak var stateStorage: PinCodeStateStorage? { + didSet { + updateProgress() + updateOrderedPinPadActions() + } + } + + public var config: Config { + didSet { + updateProgress() + } + } + + // MARK: - Scenario properties + + public var filledPinCode: String { + stateStorage?.currentCodeInput ?? String() + } + + public weak var pinCodePresenterDelegate: PinCodePresenterDelegate? + + public var leadingPinPadAction: PinCodeAction? { + didSet { + updateOrderedPinPadActions() + } + } + + public var trailingPinPadAction: PinCodeAction? { + didSet { + updateOrderedPinPadActions() + } + } + + public init(output: Output, + delegate: PinCodePresenterDelegate? = nil, + config: Config = .init(), + stateStorage: PinCodeStateStorage? = nil) { + + self.output = output + self.pinCodePresenterDelegate = delegate + self.config = config + self.stateStorage = stateStorage + } + + // MARK: - LifecyclePresenter + + open func viewDidPresented() { + pinCodePresenterDelegate?.onViewDidPresented() + } + + open func viewWillDestroy() { + // cancel requests (if any) + } + + // MARK: - User actions handling + + open func didPerform(action: PinCodeAction) { + switch action { + case let .digit(digit): + handleDigitAction(digit: digit) + case .backspace: + handleBackspaceAction() + case .biometry: + handleBiometryAction() + case .logout: + handleLogoutAction() + case .recover: + handleRecoverAction() + case let .custom(actionId): + handleCustomAction(actionId: actionId) + } + } + + // MARK: - State management + + open func setCheckingState() { + // disable UI + // display animation + } + + open func setIncorrectCodeState() { + resetState() + + // show error message / animation + } + + open func setValidCodeState() { + // display animation + } + + open func resetState() { + stateStorage?.currentCodeInput = .init() + updateProgress() + } + + // MARK: - Subclass override + + open func handleDigitAction(digit: UInt) { + withStateStorage { + $0.currentCodeInput.append(contentsOf: format(digit: digit)) + + let currentCodeValid = validate(currentCode: $0.currentCodeInput) + + if $0.currentCodeInput.count == config.codeLength && currentCodeValid { + pinCodePresenterDelegate?.onFill(pinCode: $0.currentCodeInput) + } + } + } + + open func validate(currentCode: String) -> Bool { + let violations = config.defaultInputValidator.validate(input: currentCode) + + handleDefaultValidator(violations: violations) + + return violations.isEmpty + } + + open func handleBackspaceAction() { + stateStorage?.currentCodeInput.removeLast() + } + + open func handleBiometryAction() { + pinCodePresenterDelegate?.onBiometryRequest() + } + + open func handleLogoutAction() { + output.onLogout() + } + + open func handleRecoverAction() { + output.onRecover() + } + + open func handleCustomAction(actionId: String) { + // handle in subclass + } + + open func format(digit: UInt) -> String { + String(digit) + } + + open func handleDefaultValidator(violations: Set) { + // handle in subclass + } + + open func update(progress: Progress) { + progress.totalUnitCount = Int64(config.codeLength) + progress.completedUnitCount = Int64(stateStorage?.currentCodeInput.count ?? .zero) + } + + // MARK: - Private + + private func updateProgress() { + withStateStorage { + update(progress: $0.progress) + } + } + + private func withStateStorage(actionClosure: (PinCodeStateStorage) -> Void) { + guard let stateStorage = stateStorage else { + return + } + + actionClosure(stateStorage) + } + + private func updateOrderedPinPadActions() { + let first3RowsActions = (1...9).map(PinCodeAction.digit) + var lastRowActions: [PinCodeAction] = [.digit(.zero)] + + if let leadingPinPadAction = leadingPinPadAction { + lastRowActions.insert(leadingPinPadAction, at: .zero) + } + + if let trailingPinPadAction = trailingPinPadAction { + lastRowActions.append(trailingPinPadAction) + } + + stateStorage?.orderedPinPadActions = first3RowsActions + lastRowActions + } +} diff --git a/TIAuth/Sources/PinCodePresenter/PinCodeAction.swift b/TIAuth/Sources/PinCodePresenter/PinCodeAction.swift new file mode 100644 index 00000000..e26efb20 --- /dev/null +++ b/TIAuth/Sources/PinCodePresenter/PinCodeAction.swift @@ -0,0 +1,54 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public enum PinCodeActionBiometryType { + case touchID + case faceID +} + +public enum PinCodeAction: Identifiable { + case digit(UInt) + case backspace + case biometry(PinCodeActionBiometryType) + case logout + case recover + case custom(String) + + // MARK: - Identifiable + + public var id: String { + switch self { + case let .digit(digit): + return String(digit) + case .backspace: + return "backspace" + case .biometry: + return "biometry" + case .logout: + return "logout" + case .recover: + return "recover" + case let .custom(actionId): + return actionId + } + } +} diff --git a/TIAuth/Sources/PinCodePresenter/PinCodePresenter.swift b/TIAuth/Sources/PinCodePresenter/PinCodePresenter.swift new file mode 100644 index 00000000..1d9ff889 --- /dev/null +++ b/TIAuth/Sources/PinCodePresenter/PinCodePresenter.swift @@ -0,0 +1,54 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TIUIKitCore + +@MainActor +public protocol PinCodePresenterDelegate: AnyObject { + func onViewDidPresented() + func onBiometryRequest() + func onPinChanged() + func onFill(pinCode: String) +} + +@MainActor +public protocol PinCodePresenter: LifecyclePresenter, AnyObject { + // MARK: - Scenario properties + + var pinCodePresenterDelegate: PinCodePresenterDelegate? { get set } + + var filledPinCode: String { get } + + var leadingPinPadAction: PinCodeAction? { get set } + var trailingPinPadAction: PinCodeAction? { get set } + + // MARK: - User actions handling + + func didPerform(action: PinCodeAction) + + // MARK: - State management + + func resetState() + func setCheckingState() + func setIncorrectCodeState() + func setValidCodeState() +} diff --git a/TIAuth/Sources/PinCodeScenario/BasePinCodeScenario.swift b/TIAuth/Sources/PinCodeScenario/BasePinCodeScenario.swift new file mode 100644 index 00000000..25d4835b --- /dev/null +++ b/TIAuth/Sources/PinCodeScenario/BasePinCodeScenario.swift @@ -0,0 +1,98 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TIFoundationUtils +import UIKit.UIDevice +import LocalAuthentication +import Foundation + +public enum PinCodeScenarioError: Error { + case encryptDecryptError(CipherError) + case readWriteError(StorageError) +} + +public typealias DefaultPinCodeScenarioResult = Result + +@MainActor +open class BasePinCodeScenario: PinCodeScenario, PinCodePresenterDelegate, AsyncRunExecutable { + public typealias ScenarioResult = DefaultPinCodeScenarioResult + + @MainActor + public enum Defaults { + public static var tokenCipher: DefaultTokenCipher { + DefaultTokenCipher(saltPreprocessor: DefaultSaltPreprocessor { UIDevice.current.identifierForVendor?.uuidString }, + passwordDerivator: DefaultPBKDF2PasswordDerivator()) + } + } + + public let pinCodePresenter: PinCodePresenter + public var biometryService: BiometryService = LAContext() + public var biometrySettingsService: BiometrySettingsStorage = DefaultBiometrySettingsStorage() + public var encryptedTokenStorage: SingleValueAuthKeychainStorage = DefaultEncryptedTokenStorage() + public var encryptedTokenKeyStorage: SingleValueAuthKeychainStorage = DefaultEncryptedTokenKeyStorage() + public var tokenCipher: TokenCipher = Defaults.tokenCipher + + public private(set) var scenarioCompletion: CompletionClosure? + + public init(pinCodePresenter: PinCodePresenter) { + self.pinCodePresenter = pinCodePresenter + self.pinCodePresenter.pinCodePresenterDelegate = self + } + + // MARK: - PinCodeScenario + + open func run(completion: @escaping CompletionClosure) { + scenarioCompletion = completion + } + + // MARK: - PinCodePresenterDelegate + + open func onViewDidPresented() { + // override in subclass + } + + open func onBiometryRequest() { + // override in subclass + } + + open func onPinChanged() { + // override in subclass + } + + open func onFill(pinCode: String) { + // override in subclass + } +} + +public protocol AsyncRunExecutable: AnyObject {} + +extension AsyncRunExecutable { + func asyncRun(qos: DispatchQoS.QoSClass = .userInitiated, workUnit: @escaping (Self) -> ()) { + DispatchQueue.global(qos: qos).async { [weak self] in + guard let self = self else { + return + } + + workUnit(self) + } + } +} diff --git a/TIAuth/Sources/PinCodeScenario/CreatePinCodeScenario.swift b/TIAuth/Sources/PinCodeScenario/CreatePinCodeScenario.swift new file mode 100644 index 00000000..0fb24f4f --- /dev/null +++ b/TIAuth/Sources/PinCodeScenario/CreatePinCodeScenario.swift @@ -0,0 +1,67 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation +import TIFoundationUtils + +@MainActor +open class CreatePinCodeScenario: BasePinCodeScenario { + private let token: Data + private var firstPinCode: String? + + public init(pinCodePresenter: PinCodePresenter, token: Data) { + self.token = token + + super.init(pinCodePresenter: pinCodePresenter) + } + + open override func onFill(pinCode: String) { + super.onFill(pinCode: pinCode) + + guard firstPinCode != nil else { + firstPinCode = pinCode + + pinCodePresenter.resetState() + + return + } + + guard firstPinCode == pinCode else { + pinCodePresenter.setIncorrectCodeState() + return + } + + asyncRun { scenario in + let result: ScenarioResult = scenario.tokenCipher.encrypt(token: scenario.token, using: pinCode) + .mapError { .encryptDecryptError($0) } + .flatMap { stringEncryptionResult in + scenario.encryptedTokenStorage.store(value: stringEncryptionResult) + .map { stringEncryptionResult } + .mapError { .readWriteError($0) } + } + + DispatchQueue.main.async { + scenario.scenarioCompletion?(result) + } + } + } +} diff --git a/TIAuth/Sources/PinCodeScenario/DecryptStoredTokenScenario.swift b/TIAuth/Sources/PinCodeScenario/DecryptStoredTokenScenario.swift new file mode 100644 index 00000000..8b10d593 --- /dev/null +++ b/TIAuth/Sources/PinCodeScenario/DecryptStoredTokenScenario.swift @@ -0,0 +1,154 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation +import LocalAuthentication +import UIKit +import TIFoundationUtils + +@MainActor +open class DecryptStoredTokenScenario: BasePinCodeScenario { + public typealias TokenValidationClosure = (Data, @MainActor (Bool) -> Void) -> Void + + public var tokenValidation: TokenValidationClosure + + public var canAuthWithBiometry: Bool { + biometrySettingsService.isBiometryAuthEnabled && biometryService.isBiometryAuthAvailable + } + + public init(pinCodePresenter: PinCodePresenter, + tokenValidation: @escaping TokenValidationClosure) { + + self.tokenValidation = tokenValidation + + super.init(pinCodePresenter: pinCodePresenter) + + pinCodePresenter.leadingPinPadAction = .logout + + updateTrailingAction() + } + + // MARK: - PinCodePresenterDelegate + + open override func onViewDidPresented() { + super.onViewDidPresented() + + let canAuthWithBiometry = biometrySettingsService.isBiometryAuthEnabled && biometryService.isBiometryAuthAvailable + let hasStoredKey = encryptedTokenKeyStorage.hasStoredValue() + + if canAuthWithBiometry && hasStoredKey { + startWithBiometry() + } + } + + open override func onBiometryRequest() { + super.onBiometryRequest() + + startWithBiometry() + } + + open override func onPinChanged() { + super.onPinChanged() + + updateTrailingAction() + } + + open override func onFill(pinCode: String) { + super.onFill(pinCode: pinCode) + + pinCodePresenter.setCheckingState() + + asyncRun { scenario in + let scenarioResult: ScenarioResult = scenario.encryptedTokenStorage.getValue() + .mapError { .readWriteError($0) } + .flatMap { + scenario.tokenCipher.decrypt(token: $0, using: pinCode) + .mapError { .encryptDecryptError($0) } + } + + guard case let .success(token) = scenarioResult else { + DispatchQueue.main.async { + scenario.scenarioCompletion?(scenarioResult) + } + + return + } + + scenario.tokenValidation(token) { validationPassed in + if validationPassed { + scenario.pinCodePresenter.setValidCodeState() + scenario.scenarioCompletion?(scenarioResult) + } else { + scenario.pinCodePresenter.setIncorrectCodeState() + } + } + } + } + + // MARK: - Private + + private func updateTrailingAction() { + guard pinCodePresenter.filledPinCode.isEmpty else { + pinCodePresenter.trailingPinPadAction = .backspace + return + } + + if canAuthWithBiometry, let pinBiometryType = biometryService.biometryType.pinCodeBiometryType { + pinCodePresenter.trailingPinPadAction = .biometry(pinBiometryType) + } else { + pinCodePresenter.trailingPinPadAction = .backspace + } + } + + private func startWithBiometry() { + asyncRun { scenario in + let scenarioResult: ScenarioResult = scenario.encryptedTokenKeyStorage.getValue() + .flatMap { key in + scenario.encryptedTokenStorage.getValue().map { + (encryptedToken: $0, key: key) + } + } + .mapError { .readWriteError($0) } + .flatMap { (key, stringEncryptionResult) in + scenario.tokenCipher.decrypt(token: key, using: stringEncryptionResult) + .mapError { .encryptDecryptError($0) } + } + + DispatchQueue.main.async { + scenario.scenarioCompletion?(scenarioResult) + } + } + } +} + +extension LABiometryType { + var pinCodeBiometryType: PinCodeActionBiometryType? { + switch self { + case .none: + return nil + case .touchID: + return .touchID + case .faceID: + return .faceID + } + } +} diff --git a/TIAuth/Sources/PinCodeScenario/PinCodeScenario.swift b/TIAuth/Sources/PinCodeScenario/PinCodeScenario.swift new file mode 100644 index 00000000..f8d8473a --- /dev/null +++ b/TIAuth/Sources/PinCodeScenario/PinCodeScenario.swift @@ -0,0 +1,30 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +@MainActor +public protocol PinCodeScenario { + associatedtype ScenarioResult + + typealias CompletionClosure = (ScenarioResult) -> Void + + func run(completion: @escaping CompletionClosure) +} diff --git a/TIAuth/Sources/PinCodeScenario/UseBiometryLoginScenario.swift b/TIAuth/Sources/PinCodeScenario/UseBiometryLoginScenario.swift new file mode 100644 index 00000000..5b142173 --- /dev/null +++ b/TIAuth/Sources/PinCodeScenario/UseBiometryLoginScenario.swift @@ -0,0 +1,57 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation +import TIFoundationUtils + +@MainActor +open class UseBiometryLoginScenario: BasePinCodeScenario { + public let encryptedToken: StringEncryptionResult + + public init(pinCodePresenter: PinCodePresenter, encryptedToken: StringEncryptionResult) { + self.encryptedToken = encryptedToken + + super.init(pinCodePresenter: pinCodePresenter) + } + + open override func run(completion: @escaping CompletionClosure) { + super.run(completion: completion) + + asyncRun { scenario in + let result: ScenarioResult = scenario.tokenCipher.derive(password: scenario.pinCodePresenter.filledPinCode, + using: scenario.encryptedToken.salt) + .mapError { .encryptDecryptError($0) } + .flatMap { + scenario.encryptedTokenKeyStorage.store(value: $0) + .mapError { .readWriteError($0) } + } + + if case .success = result { + scenario.biometrySettingsService.isBiometryAuthEnabled = true + } + + DispatchQueue.main.async { + completion(result) + } + } + } +} diff --git a/TIAuth/Sources/PinCodeValidation/ValidationRules/EqualDigitsValidationRule.swift b/TIAuth/Sources/PinCodeValidation/ValidationRules/EqualDigitsValidationRule.swift new file mode 100644 index 00000000..87ae1129 --- /dev/null +++ b/TIAuth/Sources/PinCodeValidation/ValidationRules/EqualDigitsValidationRule.swift @@ -0,0 +1,35 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public struct EqualDigitsValidationRule: ValidationRule { + private let minEqualDigits: UInt + + public init(minEqualDigits: UInt) { + self.minEqualDigits = minEqualDigits + } + + // MARK: - ValidationRule + + public func validate(input: String) -> Bool { + !input.containsSequenceOfEqualCharacters(minEqualCharacters: minEqualDigits) + } +} diff --git a/TIAuth/Sources/PinCodeValidation/ValidationRules/OrderedDigitsValidationRule.swift b/TIAuth/Sources/PinCodeValidation/ValidationRules/OrderedDigitsValidationRule.swift new file mode 100644 index 00000000..8180890d --- /dev/null +++ b/TIAuth/Sources/PinCodeValidation/ValidationRules/OrderedDigitsValidationRule.swift @@ -0,0 +1,39 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public struct OrderedDigitsValidationRule: ValidationRule { + private let ascendingSequence: Bool + private let minLength: UInt + + public init(ascendingSequence: Bool, minLength: UInt) { + self.ascendingSequence = ascendingSequence + self.minLength = minLength + } + + // MARK: - ValidationRule + + public func validate(input: String) -> Bool { + ascendingSequence + ? !input.containsAscendingSequence(minLength: minLength) + : !input.containsDescendingSequence(minLength: minLength) + } +} diff --git a/TIAuth/Sources/PinCodeValidation/ValidationRules/String+PinCodeValidation.swift b/TIAuth/Sources/PinCodeValidation/ValidationRules/String+PinCodeValidation.swift new file mode 100644 index 00000000..08e30e1b --- /dev/null +++ b/TIAuth/Sources/PinCodeValidation/ValidationRules/String+PinCodeValidation.swift @@ -0,0 +1,91 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +private extension Substring.SubSequence { + func recursivePairCheck(requiredMatches: UInt, + sequenceRequiredMatches: UInt, + checkClosure: (Character, Character) -> Bool) -> Bool { + + guard sequenceRequiredMatches > 0 else { + return true + } + + guard !isEmpty else { + return false + } + + let tail = dropFirst() + + guard let current = first, let next = tail.first else { + return false + } + + let matched = checkClosure(current, next) + + let reducedMatches = sequenceRequiredMatches - (matched ? 1 : 0) + + let currentSequenceMatch = matched + && tail.recursivePairCheck(requiredMatches: requiredMatches, + sequenceRequiredMatches: reducedMatches, + checkClosure: checkClosure) + + return currentSequenceMatch || tail.recursivePairCheck(requiredMatches: requiredMatches, + sequenceRequiredMatches: requiredMatches, + checkClosure: checkClosure) + } + + func recursivePairCheck(requiredMatches: UInt, checkClosure: (Character, Character) -> Bool) -> Bool { + recursivePairCheck(requiredMatches: requiredMatches, + sequenceRequiredMatches: requiredMatches, + checkClosure: checkClosure) + } + + func containsOrderedSequence(minLength: UInt, orderingClosure: ((Int, Int) -> Bool)) -> Bool { + recursivePairCheck(requiredMatches: minLength - 1) { + guard let current = $0.intValue, let next = $1.intValue else { + return false + } + + return orderingClosure(current, next) + } + } +} + +private extension Character { + var intValue: Int? { + return Int(String(self)) + } +} + +extension String { + func containsSequenceOfEqualCharacters(minEqualCharacters: UInt) -> Bool { + Substring(self).recursivePairCheck(requiredMatches: minEqualCharacters - 1) { $0 == $1 } + } + + func containsAscendingSequence(minLength: UInt) -> Bool { + Substring(self).containsOrderedSequence(minLength: minLength) { $0 + 1 == $1 } + } + + func containsDescendingSequence(minLength: UInt) -> Bool { + Substring(self).containsOrderedSequence(minLength: minLength) { $0 - 1 == $1 } + } +} diff --git a/TIAuth/Sources/PinCodeValidation/ValidationRules/ValidationRule.swift b/TIAuth/Sources/PinCodeValidation/ValidationRules/ValidationRule.swift new file mode 100644 index 00000000..16b78e5d --- /dev/null +++ b/TIAuth/Sources/PinCodeValidation/ValidationRules/ValidationRule.swift @@ -0,0 +1,25 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public protocol ValidationRule { + func validate(input: String) -> Bool +} diff --git a/TIAuth/Sources/PinCodeValidation/Validator/DefaultInputValidator.swift b/TIAuth/Sources/PinCodeValidation/Validator/DefaultInputValidator.swift new file mode 100644 index 00000000..b138b296 --- /dev/null +++ b/TIAuth/Sources/PinCodeValidation/Validator/DefaultInputValidator.swift @@ -0,0 +1,39 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +open class DefaultInputValidator: InputValidator { + public var rules: [Violation: ValidationRule] + + public init(rules: [Violation: ValidationRule]) { + self.rules = rules + } + + convenience init(violations: [Violation], rulesCreator: (Violation) -> ValidationRule) { + self.init(rules: .init(uniqueKeysWithValues: violations.map { ($0, rulesCreator($0)) }) ) + } + + // MARK: - InputValidator + + open func validate(input: String) -> Set { + Set(rules.filter { !$0.value.validate(input: input) }.keys) + } +} diff --git a/TIAuth/Sources/PinCodeValidation/Validator/DefaultViolation.swift b/TIAuth/Sources/PinCodeValidation/Validator/DefaultViolation.swift new file mode 100644 index 00000000..4be586ac --- /dev/null +++ b/TIAuth/Sources/PinCodeValidation/Validator/DefaultViolation.swift @@ -0,0 +1,35 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public enum DefaultViolation: Hashable { + case orderedDigits(ascending: Bool, minLength: UInt) + case equalDigits(minEqualDigits: UInt) + + public var defaultValidationRule: ValidationRule { + switch self { + case let .orderedDigits(ascending, minLength): + return OrderedDigitsValidationRule(ascendingSequence: ascending, minLength: minLength) + case let .equalDigits(minEqualDigits): + return EqualDigitsValidationRule(minEqualDigits: minEqualDigits) + } + } +} diff --git a/TIAuth/Sources/PinCodeValidation/Validator/InputValidator.swift b/TIAuth/Sources/PinCodeValidation/Validator/InputValidator.swift new file mode 100644 index 00000000..baac926d --- /dev/null +++ b/TIAuth/Sources/PinCodeValidation/Validator/InputValidator.swift @@ -0,0 +1,29 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public protocol InputValidator { + associatedtype Violation: Hashable + + var rules: [Violation: ValidationRule] { get set } + + func validate(input: String) -> Set +} diff --git a/TIAuth/Sources/TokenStorage/AuthSettingsStorage.swift b/TIAuth/Sources/TokenStorage/AuthSettingsStorage.swift new file mode 100644 index 00000000..9924544f --- /dev/null +++ b/TIAuth/Sources/TokenStorage/AuthSettingsStorage.swift @@ -0,0 +1,28 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public protocol AuthSettingsStorage: AnyObject { + /// Should be true by default (on app first run) + var shouldResetStoredData: Bool { get set } +} diff --git a/TIAuth/Sources/TokenStorage/CryptoConstants.swift b/TIAuth/Sources/TokenStorage/CryptoConstants.swift new file mode 100644 index 00000000..91795ea6 --- /dev/null +++ b/TIAuth/Sources/TokenStorage/CryptoConstants.swift @@ -0,0 +1,45 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +internal enum CryptoConstants { + static var saltLength: Int { + 32 + } + + static var ivLength: Int { + 16 + } + + static var keyLength: Int { + 32 + } + + static var pbkdf2NumberOfIterations: Int { + 8192 + } + + static var blockSize: Int { + 16 // 128 / 8 + } +} diff --git a/TIAuth/Sources/TokenStorage/DefaultAuthSettingsStorage.swift b/TIAuth/Sources/TokenStorage/DefaultAuthSettingsStorage.swift new file mode 100644 index 00000000..32382e7a --- /dev/null +++ b/TIAuth/Sources/TokenStorage/DefaultAuthSettingsStorage.swift @@ -0,0 +1,52 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TIFoundationUtils +import Foundation + +open class DefaultAuthSettingsStorage: AuthSettingsStorage { + public enum Defaults { + public static var shouldResetDataKey: String { + "shouldResetData" + } + } + + private let reinstallChecker: AppReinstallChecker + + // MARK: - PinCodeSettingsStorage + + open var shouldResetStoredData: Bool { + get { + reinstallChecker.isAppFirstRun + } + set { + reinstallChecker.isAppFirstRun = newValue + } + } + + public init(defaultsStorage: UserDefaults = .standard, + storageKey: String = Defaults.shouldResetDataKey) { + + self.reinstallChecker = AppReinstallChecker(defaultsStorage: defaultsStorage, + storageKey: storageKey) + } +} diff --git a/TIAuth/Sources/TokenStorage/DefaultEncryptedTokenKeyStorage.swift b/TIAuth/Sources/TokenStorage/DefaultEncryptedTokenKeyStorage.swift new file mode 100644 index 00000000..70fd0800 --- /dev/null +++ b/TIAuth/Sources/TokenStorage/DefaultEncryptedTokenKeyStorage.swift @@ -0,0 +1,71 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import KeychainAccess +import Foundation +import LocalAuthentication + +open class DefaultEncryptedTokenKeyStorage: SingleValueAuthKeychainStorage { + open class Defaults: SingleValueAuthKeychainStorage.Defaults { + public static var encryptedTokenKeyStorageKey: String { + keychainServiceIdentifier + ".encryptedTokenKey" + } + + public static var reusableLAContext: LAContext { + let context = LAContext() + context.touchIDAuthenticationAllowableReuseDuration = LATouchIDAuthenticationMaximumAllowableReuseDuration + return context + } + } + + public init(keychain: Keychain = Keychain(service: Defaults.keychainServiceIdentifier), + localAuthContext: LAContext = Defaults.reusableLAContext, + settingsStorage: AuthSettingsStorage = DefaultAuthSettingsStorage(), + encryptedTokenKeyStorageKey: String = Defaults.encryptedTokenKeyStorageKey) { + + let getValueClosure: GetValueClosure = { keychain, storageKey in + do { + guard let value = try keychain.getData(storageKey) else { + return .failure(.valueNotFound) + } + + return .success(value) + } catch { + return .failure(.unableToExtractData(underlyingError: error)) + } + } + + let setValueClosure: SetValueClosure = { keychain, value, storageKey in + do { + return .success(try keychain.set(value, key: storageKey)) + } catch { + return .failure(.unableToWriteData(underlyingError: error)) + } + } + + super.init(keychain: keychain.authenticationContext(localAuthContext), + settingsStorage: settingsStorage, + storageKey: encryptedTokenKeyStorageKey, + getValueClosure: getValueClosure, + setValueClosure: setValueClosure) + } +} diff --git a/TIAuth/Sources/TokenStorage/DefaultEncryptedTokenStorage.swift b/TIAuth/Sources/TokenStorage/DefaultEncryptedTokenStorage.swift new file mode 100644 index 00000000..fc928fc3 --- /dev/null +++ b/TIAuth/Sources/TokenStorage/DefaultEncryptedTokenStorage.swift @@ -0,0 +1,67 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation +import KeychainAccess + +open class DefaultEncryptedTokenStorage: SingleValueAuthKeychainStorage { + open class Defaults: SingleValueAuthKeychainStorage.Defaults { + public static var encryptedTokenStorageKey: String { + keychainServiceIdentifier + ".encryptedToken" + } + } + + public init(keychain: Keychain = Keychain(service: Defaults.keychainServiceIdentifier), + settingsStorage: AuthSettingsStorage = DefaultAuthSettingsStorage(), + encryptedTokenStorageKey: String = Defaults.encryptedTokenStorageKey) { + + let getValueClosure: GetValueClosure = { keychain, storageKey in + do { + guard let value = try keychain.getData(storageKey) else { + return .failure(.valueNotFound) + } + + do { + return .success(try StringEncryptionResult(storableData: value)) + } catch { + return .failure(.unableToDecode(underlyingError: error)) + } + } catch { + return .failure(.unableToExtractData(underlyingError: error)) + } + } + + let setValueClosure: SetValueClosure = { keychain, value, storageKey in + do { + return .success(try keychain.set(value.asStorableData(), key: storageKey)) + } catch { + return .failure(.unableToWriteData(underlyingError: error)) + } + } + + super.init(keychain: keychain, + settingsStorage: settingsStorage, + storageKey: encryptedTokenStorageKey, + getValueClosure: getValueClosure, + setValueClosure: setValueClosure) + } +} diff --git a/TIAuth/Sources/TokenStorage/SingleValueAuthKeychainStorage.swift b/TIAuth/Sources/TokenStorage/SingleValueAuthKeychainStorage.swift new file mode 100644 index 00000000..07bb45a8 --- /dev/null +++ b/TIAuth/Sources/TokenStorage/SingleValueAuthKeychainStorage.swift @@ -0,0 +1,84 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TIFoundationUtils +import KeychainAccess +import Foundation + +open class SingleValueAuthKeychainStorage: SingleValueStorage { + open class Defaults { + public static var keychainServiceIdentifier: String { + Bundle.main.bundleIdentifier ?? "ru.touchin.TIAuth" + } + } + + public typealias GetValueClosure = (Keychain, String) -> Result + public typealias SetValueClosure = (Keychain, ValueType, String) -> Result + + public let keychain: Keychain + public let settingsStorage: AuthSettingsStorage + public let storageKey: String + public let getValueClosure: GetValueClosure + public let setValueClosure: SetValueClosure + + public init(keychain: Keychain = Keychain(service: Defaults.keychainServiceIdentifier), + settingsStorage: AuthSettingsStorage = DefaultAuthSettingsStorage(), + storageKey: String, + getValueClosure: @escaping GetValueClosure, + setValueClosure: @escaping SetValueClosure) { + + self.keychain = keychain + self.settingsStorage = settingsStorage + self.storageKey = storageKey + self.getValueClosure = getValueClosure + self.setValueClosure = setValueClosure + } + + // MARK: - SingleValueStorage + + open func hasStoredValue() -> Bool { + !settingsStorage.shouldResetStoredData && ((try? keychain.contains(storageKey)) ?? false) + } + + open func store(value: ValueType) -> Result { + return setValueClosure(keychain, value, storageKey) + } + + open func getValue() -> Result { + guard !settingsStorage.shouldResetStoredData else { + let result: Result + + do { + try keychain.remove(storageKey) + settingsStorage.shouldResetStoredData = false + + result = .failure(.valueNotFound) + } catch { + result = .failure(.unableToWriteData(underlyingError: error)) + } + + return result + } + + return getValueClosure(keychain, storageKey) + } +} diff --git a/TIAuth/Sources/TokenStorage/StringEncryptionResult.swift b/TIAuth/Sources/TokenStorage/StringEncryptionResult.swift new file mode 100644 index 00000000..f0acb447 --- /dev/null +++ b/TIAuth/Sources/TokenStorage/StringEncryptionResult.swift @@ -0,0 +1,67 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public struct StringEncryptionResult { + public struct DataRangeMismatch: Error { + public let dataLength: Int + public let valueRangeLowerBound: Int + } + + public let salt: Data + public let iv: Data + public let value: Data + + private static var saltRange: Range { + .zero.. { + saltRange.endIndex.. { + ivRange.endIndex... + } + + public init(salt: Data, iv: Data, value: Data) { + self.salt = salt + self.iv = iv + self.value = value + } + + public init(storableData: Data) throws { + guard Self.valueRange.contains(storableData.endIndex) else { + throw DataRangeMismatch(dataLength: storableData.count, + valueRangeLowerBound: Self.valueRange.lowerBound) + } + + self.init(salt: storableData[Self.saltRange], + iv: storableData[Self.ivRange], + value: storableData[Self.valueRange]) + } + + public func asStorableData() -> Data { + salt + iv + value + } +} diff --git a/TIAuth/TIAuth.podspec b/TIAuth/TIAuth.podspec index 82232708..e9f310e2 100644 --- a/TIAuth/TIAuth.podspec +++ b/TIAuth/TIAuth.podspec @@ -13,4 +13,6 @@ Pod::Spec.new do |s| s.source_files = s.name + '/Sources/**/*' s.dependency 'TIFoundationUtils', s.version.to_s + s.dependency 'TIUIKitCore', s.version.to_s + s.dependency 'KeychainAccess', "~> 4.2" end diff --git a/TIFoundationUtils/DataStorage/AppReinstallChecker.swift b/TIFoundationUtils/DataStorage/AppReinstallChecker.swift new file mode 100644 index 00000000..bb2dcee7 --- /dev/null +++ b/TIFoundationUtils/DataStorage/AppReinstallChecker.swift @@ -0,0 +1,48 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +open class AppReinstallChecker { + private let defaultsStorage: UserDefaults + private let storageKey: String + + open var isAppFirstRun: Bool { + get { + guard defaultsStorage.object(forKey: storageKey) != nil else { + return true + } + + return defaultsStorage.bool(forKey: storageKey) + } + set { + defaultsStorage.set(newValue, forKey: storageKey) + } + } + + public init(defaultsStorage: UserDefaults = .standard, + storageKey: String) { + + self.defaultsStorage = defaultsStorage + self.storageKey = storageKey + } +} diff --git a/TIFoundationUtils/DataStorage/SingleValueStorage.swift b/TIFoundationUtils/DataStorage/SingleValueStorage.swift new file mode 100644 index 00000000..4c832e6a --- /dev/null +++ b/TIFoundationUtils/DataStorage/SingleValueStorage.swift @@ -0,0 +1,30 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public protocol SingleValueStorage { + associatedtype ValueType + associatedtype ErrorType: Error + + func hasStoredValue() -> Bool + func store(value: ValueType) -> Result + func getValue() -> Result +} diff --git a/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/DefaultFingerprintsSettingsStorage.swift b/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/DefaultFingerprintsSettingsStorage.swift new file mode 100644 index 00000000..6926f1eb --- /dev/null +++ b/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/DefaultFingerprintsSettingsStorage.swift @@ -0,0 +1,51 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TIFoundationUtils +import Foundation + +open class DefaultFingerprintsSettingsStorage: FingerprintsSettingsStorage { + public enum Defaults { + public static var shouldResetFingerprintsKey: String { + "shouldResetFingerprints" + } + } + + private let reinstallChecker: AppReinstallChecker + + // MARK: - PinCodeSettingsStorage + + open var shouldResetFingerprints: Bool { + get { + reinstallChecker.isAppFirstRun + } + set { + reinstallChecker.isAppFirstRun = newValue + } + } + + public init(defaultsStorage: UserDefaults = .standard, + storageKey: String = Defaults.shouldResetFingerprintsKey) { + + self.reinstallChecker = AppReinstallChecker(defaultsStorage: defaultsStorage, storageKey: storageKey) + } +} diff --git a/TISwiftUICore/TISwiftUICore.podspec b/TISwiftUICore/TISwiftUICore.podspec index f02f9b8e..38ec741f 100644 --- a/TISwiftUICore/TISwiftUICore.podspec +++ b/TISwiftUICore/TISwiftUICore.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = 'TISwiftUICore' s.version = '1.26.0' - s.summary = 'Core UI elements: protocols, views and helpers..' + s.summary = 'Core UI elements: protocols, views and helpers.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } diff --git a/TIUIKitCore/Sources/Presenter/DefaultUIViewPresenter.swift b/TIUIKitCore/Sources/Presenter/DefaultUIViewPresenter.swift index e05d46d3..36ba18b3 100644 --- a/TIUIKitCore/Sources/Presenter/DefaultUIViewPresenter.swift +++ b/TIUIKitCore/Sources/Presenter/DefaultUIViewPresenter.swift @@ -20,7 +20,7 @@ // THE SOFTWARE. // -open class DefaultUIViewPresenter: ReusableUIViewPresenter{ +open class DefaultUIViewPresenter: ReusableUIViewPresenter { public private(set) weak var view: View? public init() {} diff --git a/TIUIKitCore/Sources/Presenter/LifecyclePresenter.swift b/TIUIKitCore/Sources/Presenter/LifecyclePresenter.swift new file mode 100644 index 00000000..35fc2f0d --- /dev/null +++ b/TIUIKitCore/Sources/Presenter/LifecyclePresenter.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +@MainActor +public protocol LifecyclePresenter { + func viewDidPresented() + func viewWillDestroy() +} diff --git a/TIUIKitCore/Sources/Presenter/ReusableUIViewPresenter.swift b/TIUIKitCore/Sources/Presenter/ReusableUIViewPresenter.swift index 37048990..657f96f8 100644 --- a/TIUIKitCore/Sources/Presenter/ReusableUIViewPresenter.swift +++ b/TIUIKitCore/Sources/Presenter/ReusableUIViewPresenter.swift @@ -20,6 +20,7 @@ // THE SOFTWARE. // +@MainActor public protocol ReusableUIViewPresenter: UIViewPresenter { func willReuse(view: View) } diff --git a/TIUIKitCore/Sources/Presenter/UIViewPresenters.swift b/TIUIKitCore/Sources/Presenter/UIViewPresenter.swift similarity index 99% rename from TIUIKitCore/Sources/Presenter/UIViewPresenters.swift rename to TIUIKitCore/Sources/Presenter/UIViewPresenter.swift index 5f911d44..14a96e1a 100644 --- a/TIUIKitCore/Sources/Presenter/UIViewPresenters.swift +++ b/TIUIKitCore/Sources/Presenter/UIViewPresenter.swift @@ -20,6 +20,7 @@ // THE SOFTWARE. // +@MainActor public protocol UIViewPresenter { associatedtype View: AnyObject // should be stored weakly