feat: pin code

This commit is contained in:
Ivan Smolin 2022-07-25 11:47:23 +03:00
parent 2c8fc0a8a5
commit 181f83b1cb
47 changed files with 2215 additions and 12 deletions

View File

@ -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"),

View File

@ -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)
}
}

View File

@ -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 }
}

View File

@ -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
}
}

View File

@ -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?)

View File

@ -20,8 +20,6 @@
// THE SOFTWARE.
//
import Foundation
@MainActor
public protocol CodeConfirmStateStorage: AnyObject {
var currentUserInput: String? { get set }

View File

@ -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) {

View File

@ -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)
}
}

View File

@ -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>
}

View File

@ -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)
}

View File

@ -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))
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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>
}

View File

@ -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
}

View File

@ -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>
}

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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()
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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)
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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 }
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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>
}

View File

@ -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 }
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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>
}

View File

@ -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)
}
}

View File

@ -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' }

View File

@ -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() {}

View File

@ -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()
}

View File

@ -20,6 +20,7 @@
// THE SOFTWARE.
//
@MainActor
public protocol ReusableUIViewPresenter: UIViewPresenter {
func willReuse(view: View)
}

View File

@ -20,6 +20,7 @@
// THE SOFTWARE.
//
@MainActor
public protocol UIViewPresenter {
associatedtype View: AnyObject // should be stored weakly