Skip to content
This repository has been archived by the owner. It is now read-only.
Permalink
2.0.0
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import SwiftKeychainWrapper
public let KeychainKeyAuthenticationInfo = "authenticationInfo"
public let AllowedPasscodeFailedAttempts = 3
// Passcode intervals with rawValue in seconds.
public enum PasscodeInterval: Int {
case immediately = 2
case oneMinute = 60
case fiveMinutes = 300
case tenMinutes = 600
case fifteenMinutes = 900
case oneHour = 3600
}
// MARK: - Helper methods for accessing Authentication information from the Keychain
public extension KeychainWrapper {
func authenticationInfo() -> AuthenticationKeychainInfo? {
NSKeyedUnarchiver.setClass(AuthenticationKeychainInfo.self, forClassName: "AuthenticationKeychainInfo")
return object(forKey: KeychainKeyAuthenticationInfo) as? AuthenticationKeychainInfo
}
func setAuthenticationInfo(_ info: AuthenticationKeychainInfo?) {
NSKeyedArchiver.setClassName("AuthenticationKeychainInfo", for: AuthenticationKeychainInfo.self)
if let info = info {
set(info, forKey: KeychainKeyAuthenticationInfo)
} else {
removeObject(forKey: KeychainKeyAuthenticationInfo)
}
}
}
open class AuthenticationKeychainInfo: NSObject, NSCoding {
fileprivate(set) open var lastPasscodeValidationInterval: TimeInterval?
fileprivate(set) open var passcode: String?
fileprivate(set) open var requiredPasscodeInterval: PasscodeInterval?
fileprivate(set) open var lockOutInterval: TimeInterval?
fileprivate(set) open var failedAttempts: Int
open var useTouchID: Bool
// Timeout period before user can retry entering passcodes
open var lockTimeInterval: TimeInterval = 15 * 60
public init(passcode: String) {
self.passcode = passcode
self.requiredPasscodeInterval = .immediately
self.failedAttempts = 0
self.useTouchID = false
}
open func encode(with aCoder: NSCoder) {
if let lastPasscodeValidationInterval = lastPasscodeValidationInterval {
let interval = NSNumber(value: lastPasscodeValidationInterval as Double)
aCoder.encode(interval, forKey: "lastPasscodeValidationInterval")
}
if let lockOutInterval = lockOutInterval, isLocked() {
let interval = NSNumber(value: lockOutInterval as Double)
aCoder.encode(interval, forKey: "lockOutInterval")
}
aCoder.encode(passcode, forKey: "passcode")
aCoder.encode(requiredPasscodeInterval?.rawValue, forKey: "requiredPasscodeInterval")
aCoder.encode(failedAttempts, forKey: "failedAttempts")
aCoder.encode(useTouchID, forKey: "useTouchID")
}
public required init?(coder aDecoder: NSCoder) {
self.lastPasscodeValidationInterval = aDecoder.decodeAsDouble(forKey: "lastPasscodeValidationInterval")
if let lockOutInterval = aDecoder.decodeObject(forKey: "lockOutInterval") as? NSNumber {
self.lockOutInterval = lockOutInterval.doubleValue
}
self.passcode = aDecoder.decodeObject(forKey: "passcode") as? String
self.failedAttempts = aDecoder.decodeAsInt(forKey: "failedAttempts")
self.useTouchID = aDecoder.decodeAsBool(forKey: "useTouchID")
if var interval = aDecoder.decodeObject(forKey: "requiredPasscodeInterval") as? NSNumber {
// We have updated the immediate lockout value to 2 from 0 due to timing issues with systemUptime()
interval = interval == 0 ? 2 : interval
self.requiredPasscodeInterval = PasscodeInterval(rawValue: interval.intValue)
}
}
}
// MARK: - API
public extension AuthenticationKeychainInfo {
private func resetLockoutState() {
self.failedAttempts = 0
self.lockOutInterval = nil
}
func updatePasscode(_ passcode: String) {
self.passcode = passcode
self.lastPasscodeValidationInterval = nil
}
func updateRequiredPasscodeInterval(_ interval: PasscodeInterval) {
self.requiredPasscodeInterval = interval
self.lastPasscodeValidationInterval = nil
}
func recordValidation() {
// Save the timestamp to remember the last time we successfully
// validated and clear out the failed attempts counter.
self.lastPasscodeValidationInterval = SystemUtils.systemUptime()
resetLockoutState()
}
func lockOutUser() {
self.lockOutInterval = SystemUtils.systemUptime()
}
func recordFailedAttempt() {
if self.failedAttempts >= AllowedPasscodeFailedAttempts {
//This is a failed attempt after a lockout period. Reset the lockout state
//This prevents failedAttemps from being higher than 3
self.resetLockoutState()
}
self.failedAttempts += 1
}
func isLocked() -> Bool {
guard let lockOutInterval = self.lockOutInterval else {
return false
}
if SystemUtils.systemUptime() < lockOutInterval {
// Unlock and require passcode input
resetLockoutState()
return false
}
return (SystemUtils.systemUptime() - (self.lockOutInterval ?? 0)) < lockTimeInterval
}
func requiresValidation() -> Bool {
// If there isn't a passcode, don't need validation.
guard let _ = passcode else {
return false
}
// Need to make sure we've validated in the past. If not, its a definite yes.
guard let lastValidationInterval = lastPasscodeValidationInterval,
let requireInterval = requiredPasscodeInterval
else {
return true
}
// We've authenticated before so lets see how long since. If the uptime is less than the last validation stamp,
// we probably restarted which means we should require validation.
return SystemUtils.systemUptime() - lastValidationInterval > Double(requireInterval.rawValue) ||
SystemUtils.systemUptime() < lastValidationInterval
}
}