feat: pin code
This commit is contained in:
parent
2c8fc0a8a5
commit
181f83b1cb
|
|
@ -81,7 +81,7 @@ let package = Package(
|
|||
.target(name: "OTPSwiftView", dependencies: ["TIUIElements"], path: "OTPSwiftView/Sources"),
|
||||
.target(name: "TITransitions", path: "TITransitions/Sources"),
|
||||
.target(name: "TIPagination", dependencies: ["Cursors", "TISwiftUtils"], path: "TIPagination/Sources"),
|
||||
.target(name: "TIAuth", dependencies: ["TIFoundationUtils"], path: "TIAuth/Sources"),
|
||||
.target(name: "TIAuth", dependencies: ["TIFoundationUtils", "TIUIKitCore", "KeychainAccess"], path: "TIAuth/Sources"),
|
||||
|
||||
//MARK: - Skolkovo
|
||||
.target(name: "TIEcommerce", dependencies: ["TIFoundationUtils", "TISwiftUtils", "TINetworking"], path: "TIEcommerce/Sources"),
|
||||
|
|
|
|||
|
|
@ -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 TIUIKitCore
|
||||
|
||||
@MainActor
|
||||
protocol CodeConfirmPresenter {
|
||||
protocol CodeConfirmPresenter: LifecyclePresenter {
|
||||
// MARK: - User actions handling
|
||||
|
||||
func inputChanged(newInput: String?)
|
||||
func refreshCode()
|
||||
|
||||
// MARK: - View lifecycle handling
|
||||
|
||||
func viewDidPresented()
|
||||
|
||||
// MARK: - Autofill
|
||||
|
||||
func autofill(code: String, with codeId: String?)
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
public protocol CodeConfirmStateStorage: AnyObject {
|
||||
var currentUserInput: String? { get set }
|
||||
|
|
|
|||
|
|
@ -77,6 +77,8 @@ open class DefaultCodeConfirmPresenter<ConfirmResponse: CodeConfirmResponse,
|
|||
private let codeRefreshTimer = TITimer(mode: .activeAndBackground)
|
||||
private let codeLifetimeTimer = TITimer(mode: .activeAndBackground)
|
||||
|
||||
private var executingTask: Cancellable?
|
||||
|
||||
public var output: Output
|
||||
public var requests: Requests
|
||||
public weak var stateStorage: CodeConfirmStateStorage?
|
||||
|
|
@ -190,7 +192,7 @@ open class DefaultCodeConfirmPresenter<ConfirmResponse: CodeConfirmResponse,
|
|||
if let code = newInput, code.count >= config.codeLength {
|
||||
stateStorage?.isExecutingRequest = true
|
||||
|
||||
Task {
|
||||
executingTask = Task {
|
||||
await confirm(code: code)
|
||||
}
|
||||
}
|
||||
|
|
@ -200,7 +202,7 @@ open class DefaultCodeConfirmPresenter<ConfirmResponse: CodeConfirmResponse,
|
|||
stateStorage?.canRequestNewCode = false
|
||||
stateStorage?.canRefreshCodeAfter = nil
|
||||
|
||||
Task {
|
||||
executingTask = Task {
|
||||
await refreshCode()
|
||||
}
|
||||
}
|
||||
|
|
@ -213,6 +215,10 @@ open class DefaultCodeConfirmPresenter<ConfirmResponse: CodeConfirmResponse,
|
|||
for: currentCodeResponse)
|
||||
}
|
||||
|
||||
open func viewWillDestroy() {
|
||||
executingTask?.cancel()
|
||||
}
|
||||
|
||||
// MARK: - Autofill
|
||||
|
||||
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.dependency 'TIFoundationUtils', s.version.to_s
|
||||
s.dependency 'TIUIKitCore', s.version.to_s
|
||||
s.dependency 'KeychainAccess', "~> 4.2"
|
||||
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|
|
||||
s.name = 'TISwiftUICore'
|
||||
s.version = '1.26.0'
|
||||
s.summary = 'Core UI elements: protocols, views and helpers..'
|
||||
s.summary = 'Core UI elements: protocols, views and helpers.'
|
||||
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
open class DefaultUIViewPresenter<View: AnyObject>: ReusableUIViewPresenter{
|
||||
open class DefaultUIViewPresenter<View: AnyObject>: ReusableUIViewPresenter {
|
||||
public private(set) weak var view: View?
|
||||
|
||||
public init() {}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
//
|
||||
|
||||
@MainActor
|
||||
public protocol ReusableUIViewPresenter: UIViewPresenter {
|
||||
func willReuse(view: View)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
@MainActor
|
||||
public protocol UIViewPresenter {
|
||||
associatedtype View: AnyObject // should be stored weakly
|
||||
|
||||
Loading…
Reference in New Issue