Compare commits
1 Commits
master
...
feature/pi
| Author | SHA1 | Date |
|---|---|---|
|
|
181f83b1cb |
|
|
@ -81,7 +81,7 @@ let package = Package(
|
||||||
.target(name: "OTPSwiftView", dependencies: ["TIUIElements"], path: "OTPSwiftView/Sources"),
|
.target(name: "OTPSwiftView", dependencies: ["TIUIElements"], path: "OTPSwiftView/Sources"),
|
||||||
.target(name: "TITransitions", path: "TITransitions/Sources"),
|
.target(name: "TITransitions", path: "TITransitions/Sources"),
|
||||||
.target(name: "TIPagination", dependencies: ["Cursors", "TISwiftUtils"], path: "TIPagination/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
|
//MARK: - Skolkovo
|
||||||
.target(name: "TIEcommerce", dependencies: ["TIFoundationUtils", "TISwiftUtils", "TINetworking"], path: "TIEcommerce/Sources"),
|
.target(name: "TIEcommerce", dependencies: ["TIFoundationUtils", "TISwiftUtils", "TINetworking"], path: "TIEcommerce/Sources"),
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,18 +21,15 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import TIUIKitCore
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
protocol CodeConfirmPresenter {
|
protocol CodeConfirmPresenter: LifecyclePresenter {
|
||||||
// MARK: - User actions handling
|
// MARK: - User actions handling
|
||||||
|
|
||||||
func inputChanged(newInput: String?)
|
func inputChanged(newInput: String?)
|
||||||
func refreshCode()
|
func refreshCode()
|
||||||
|
|
||||||
// MARK: - View lifecycle handling
|
|
||||||
|
|
||||||
func viewDidPresented()
|
|
||||||
|
|
||||||
// MARK: - Autofill
|
// MARK: - Autofill
|
||||||
|
|
||||||
func autofill(code: String, with codeId: String?)
|
func autofill(code: String, with codeId: String?)
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,6 @@
|
||||||
// THE SOFTWARE.
|
// THE SOFTWARE.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public protocol CodeConfirmStateStorage: AnyObject {
|
public protocol CodeConfirmStateStorage: AnyObject {
|
||||||
var currentUserInput: String? { get set }
|
var currentUserInput: String? { get set }
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,8 @@ open class DefaultCodeConfirmPresenter<ConfirmResponse: CodeConfirmResponse,
|
||||||
private let codeRefreshTimer = TITimer(mode: .activeAndBackground)
|
private let codeRefreshTimer = TITimer(mode: .activeAndBackground)
|
||||||
private let codeLifetimeTimer = TITimer(mode: .activeAndBackground)
|
private let codeLifetimeTimer = TITimer(mode: .activeAndBackground)
|
||||||
|
|
||||||
|
private var executingTask: Cancellable?
|
||||||
|
|
||||||
public var output: Output
|
public var output: Output
|
||||||
public var requests: Requests
|
public var requests: Requests
|
||||||
public weak var stateStorage: CodeConfirmStateStorage?
|
public weak var stateStorage: CodeConfirmStateStorage?
|
||||||
|
|
@ -190,7 +192,7 @@ open class DefaultCodeConfirmPresenter<ConfirmResponse: CodeConfirmResponse,
|
||||||
if let code = newInput, code.count >= config.codeLength {
|
if let code = newInput, code.count >= config.codeLength {
|
||||||
stateStorage?.isExecutingRequest = true
|
stateStorage?.isExecutingRequest = true
|
||||||
|
|
||||||
Task {
|
executingTask = Task {
|
||||||
await confirm(code: code)
|
await confirm(code: code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -200,7 +202,7 @@ open class DefaultCodeConfirmPresenter<ConfirmResponse: CodeConfirmResponse,
|
||||||
stateStorage?.canRequestNewCode = false
|
stateStorage?.canRequestNewCode = false
|
||||||
stateStorage?.canRefreshCodeAfter = nil
|
stateStorage?.canRefreshCodeAfter = nil
|
||||||
|
|
||||||
Task {
|
executingTask = Task {
|
||||||
await refreshCode()
|
await refreshCode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -213,6 +215,10 @@ open class DefaultCodeConfirmPresenter<ConfirmResponse: CodeConfirmResponse,
|
||||||
for: currentCodeResponse)
|
for: currentCodeResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func viewWillDestroy() {
|
||||||
|
executingTask?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Autofill
|
// MARK: - Autofill
|
||||||
|
|
||||||
open func autofill(code: String, with codeId: String? = nil) {
|
open func autofill(code: String, with codeId: String? = nil) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
//
|
||||||
|
// 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 AESCipher: Cipher {
|
||||||
|
public struct CryptError: Error {
|
||||||
|
public let ccCryptorStatus: Int32
|
||||||
|
public let key: Data
|
||||||
|
public let iv: Data
|
||||||
|
}
|
||||||
|
|
||||||
|
public var iv: Data
|
||||||
|
public var key: Data
|
||||||
|
|
||||||
|
public init(iv: Data, key: Data) {
|
||||||
|
self.iv = iv
|
||||||
|
self.key = key
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Cipher
|
||||||
|
|
||||||
|
public func encrypt(data: Data) -> Result<Data, CipherError> {
|
||||||
|
crypt(data: data, operation: CCOperation(kCCEncrypt))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func decrypt(data: Data) -> Result<Data, CipherError> {
|
||||||
|
crypt(data: data, operation: CCOperation(kCCDecrypt))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func crypt(data: Data, operation: CCOperation) -> Result<Data, CipherError> {
|
||||||
|
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..<cryptData.count)
|
||||||
|
return .success(cryptData)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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 Cipher {
|
||||||
|
func encrypt(data: Data) -> Result<Data, CipherError>
|
||||||
|
func decrypt(data: Data) -> Result<Data, CipherError>
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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<Data, CipherError> {
|
||||||
|
var failureResult: Result<Data, CipherError>?
|
||||||
|
|
||||||
|
let derivedKeyBytes = Array<UInt8>(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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<UInt8>(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<Data, CipherError> {
|
||||||
|
passwordDerivator.derive(password: password,
|
||||||
|
salt: saltPreprocessor.preprocess(salt: salt))
|
||||||
|
}
|
||||||
|
|
||||||
|
open func encrypt(token: Data, using password: String) -> Result<StringEncryptionResult, CipherError> {
|
||||||
|
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<Data, CipherError> {
|
||||||
|
createCipher(iv: token.iv, key: key)
|
||||||
|
.decrypt(data: token.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func decrypt(token: StringEncryptionResult, using password: String) -> Result<Data, CipherError> {
|
||||||
|
passwordDerivator.derive(password: password,
|
||||||
|
salt: saltPreprocessor.preprocess(salt: token.salt)).flatMap {
|
||||||
|
createCipher(iv: token.iv,
|
||||||
|
key: $0)
|
||||||
|
.decrypt(data: token.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Data, CipherError>
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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<Data, CipherError>
|
||||||
|
|
||||||
|
func encrypt(token: Data, using password: String) -> Result<StringEncryptionResult, CipherError>
|
||||||
|
func decrypt(token: StringEncryptionResult, using key: Data) -> Result<Data, CipherError>
|
||||||
|
func decrypt(token: StringEncryptionResult, using password: String) -> Result<Data, CipherError>
|
||||||
|
}
|
||||||
|
|
@ -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<DefaultViolation> {
|
||||||
|
let violations: [DefaultViolation] = [
|
||||||
|
.equalDigits(minEqualDigits: 3),
|
||||||
|
.orderedDigits(ascending: true, minLength: 3),
|
||||||
|
.orderedDigits(ascending: false, minLength: 3)
|
||||||
|
]
|
||||||
|
|
||||||
|
return DefaultInputValidator<DefaultViolation>(violations: violations) {
|
||||||
|
$0.defaultValidationRule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var codeLength: Int
|
||||||
|
public var defaultInputValidator: DefaultInputValidator<DefaultViolation>
|
||||||
|
|
||||||
|
public init(codeLength: Int = Defaults.codeLength,
|
||||||
|
defaultInputValidator: DefaultInputValidator<DefaultViolation> = 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<DefaultViolation>) {
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -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<SuccessResult> = Result<SuccessResult, PinCodeScenarioError>
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
open class BasePinCodeScenario<SuccessResult>: PinCodeScenario, PinCodePresenterDelegate, AsyncRunExecutable {
|
||||||
|
public typealias ScenarioResult = DefaultPinCodeScenarioResult<SuccessResult>
|
||||||
|
|
||||||
|
@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<StringEncryptionResult> = DefaultEncryptedTokenStorage()
|
||||||
|
public var encryptedTokenKeyStorage: SingleValueAuthKeychainStorage<Data> = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<StringEncryptionResult> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Data> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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<Void> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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<Violation: Hashable>: 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<Violation> {
|
||||||
|
Set(rules.filter { !$0.value.validate(input: input) }.keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Violation>
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Data> {
|
||||||
|
open class Defaults: SingleValueAuthKeychainStorage<StringEncryptionResult>.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<StringEncryptionResult> {
|
||||||
|
open class Defaults: SingleValueAuthKeychainStorage<StringEncryptionResult>.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<ValueType>: SingleValueStorage {
|
||||||
|
open class Defaults {
|
||||||
|
public static var keychainServiceIdentifier: String {
|
||||||
|
Bundle.main.bundleIdentifier ?? "ru.touchin.TIAuth"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public typealias GetValueClosure = (Keychain, String) -> Result<ValueType, StorageError>
|
||||||
|
public typealias SetValueClosure = (Keychain, ValueType, String) -> Result<Void, StorageError>
|
||||||
|
|
||||||
|
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<Void, StorageError> {
|
||||||
|
return setValueClosure(keychain, value, storageKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func getValue() -> Result<ValueType, StorageError> {
|
||||||
|
guard !settingsStorage.shouldResetStoredData else {
|
||||||
|
let result: Result<ValueType, StorageError>
|
||||||
|
|
||||||
|
do {
|
||||||
|
try keychain.remove(storageKey)
|
||||||
|
settingsStorage.shouldResetStoredData = false
|
||||||
|
|
||||||
|
result = .failure(.valueNotFound)
|
||||||
|
} catch {
|
||||||
|
result = .failure(.unableToWriteData(underlyingError: error))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return getValueClosure(keychain, storageKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Int> {
|
||||||
|
.zero..<CryptoConstants.saltLength
|
||||||
|
}
|
||||||
|
|
||||||
|
private static var ivRange: Range<Int> {
|
||||||
|
saltRange.endIndex..<CryptoConstants.saltLength + CryptoConstants.ivLength
|
||||||
|
}
|
||||||
|
|
||||||
|
private static var valueRange: PartialRangeFrom<Int> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,4 +13,6 @@ Pod::Spec.new do |s|
|
||||||
s.source_files = s.name + '/Sources/**/*'
|
s.source_files = s.name + '/Sources/**/*'
|
||||||
|
|
||||||
s.dependency 'TIFoundationUtils', s.version.to_s
|
s.dependency 'TIFoundationUtils', s.version.to_s
|
||||||
|
s.dependency 'TIUIKitCore', s.version.to_s
|
||||||
|
s.dependency 'KeychainAccess', "~> 4.2"
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Void, ErrorType>
|
||||||
|
func getValue() -> Result<ValueType, ErrorType>
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
Pod::Spec.new do |s|
|
Pod::Spec.new do |s|
|
||||||
s.name = 'TISwiftUICore'
|
s.name = 'TISwiftUICore'
|
||||||
s.version = '1.26.0'
|
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.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
// THE SOFTWARE.
|
// THE SOFTWARE.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
@MainActor
|
||||||
public protocol ReusableUIViewPresenter: UIViewPresenter {
|
public protocol ReusableUIViewPresenter: UIViewPresenter {
|
||||||
func willReuse(view: View)
|
func willReuse(view: View)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
// THE SOFTWARE.
|
// THE SOFTWARE.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
@MainActor
|
||||||
public protocol UIViewPresenter {
|
public protocol UIViewPresenter {
|
||||||
associatedtype View: AnyObject // should be stored weakly
|
associatedtype View: AnyObject // should be stored weakly
|
||||||
|
|
||||||
Loading…
Reference in New Issue