111 lines
3.3 KiB
Swift
111 lines
3.3 KiB
Swift
import Foundation
|
|
import Security
|
|
|
|
class PasswordAttemptStore {
|
|
private let service = "com.mattermost.securepdfviewer.password.attempts"
|
|
private let maxAttempts = 10
|
|
private let restDelay: TimeInterval = 10 * 60
|
|
|
|
func getRemainingAttempts(for fileKey: String) -> Int {
|
|
maybeResetIfExperied(for: fileKey)
|
|
let failed = getAttemptInfo(for: fileKey)?.count ?? 0
|
|
return max(maxAttempts - failed, 0)
|
|
}
|
|
|
|
func registerFailedAttempt(for fileKey: String) -> Int {
|
|
maybeResetIfExperied(for: fileKey)
|
|
|
|
var info = getAttemptInfo(for: fileKey) ?? AttemptInfo(count: 0, timestap: nil)
|
|
info.count += 1
|
|
|
|
if info.count >= maxAttempts {
|
|
info.timestap = Date()
|
|
}
|
|
|
|
saveAttempts(for: fileKey, info: info)
|
|
return max(maxAttempts - info.count, 0)
|
|
}
|
|
|
|
func resetAttempts(for fileKey: String) {
|
|
delete(for: fileKey)
|
|
}
|
|
|
|
func hasExceededLimit(for fileKey: String) -> Bool {
|
|
maybeResetIfExperied(for: fileKey)
|
|
return getAttemptInfo(for: fileKey)?.count ?? 0 >= maxAttempts
|
|
}
|
|
|
|
var maxAllowedAttempts: Int {
|
|
return maxAttempts
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private struct AttemptInfo: Codable {
|
|
var count: Int
|
|
var timestap: Date?
|
|
}
|
|
|
|
private func maybeResetIfExperied(for key: String) {
|
|
guard let info = getAttemptInfo(for: key),
|
|
info.count >= maxAttempts,
|
|
let ts = info.timestap else {
|
|
return
|
|
}
|
|
|
|
if Date().timeIntervalSince(ts) >= restDelay {
|
|
resetAttempts(for: key)
|
|
}
|
|
}
|
|
|
|
private func getAttemptInfo(for key: String) -> AttemptInfo? {
|
|
guard let data = getData(forKey: key),
|
|
let info = try? JSONDecoder().decode(AttemptInfo.self, from: data) else {
|
|
return nil
|
|
}
|
|
return info
|
|
}
|
|
|
|
private func saveAttempts(for key: String, info: AttemptInfo) {
|
|
guard let data = try? JSONEncoder().encode(info) else { return }
|
|
|
|
let query: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: service,
|
|
kSecAttrAccount as String: key
|
|
]
|
|
|
|
let attributesToUpdate = [kSecValueData as String: data]
|
|
let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary)
|
|
|
|
if status == errSecItemNotFound {
|
|
var newItem = query
|
|
newItem[kSecValueData as String] = data
|
|
SecItemAdd(newItem as CFDictionary, nil)
|
|
}
|
|
}
|
|
|
|
private func getData(forKey key: String) -> Data? {
|
|
let query = [
|
|
kSecClass: kSecClassGenericPassword,
|
|
kSecAttrService: service,
|
|
kSecAttrAccount: key,
|
|
kSecReturnData: true,
|
|
kSecMatchLimit: kSecMatchLimitOne
|
|
] as CFDictionary
|
|
|
|
var result: AnyObject?
|
|
let status = SecItemCopyMatching(query, &result)
|
|
return status == errSecSuccess ? result as? Data : nil
|
|
}
|
|
|
|
private func delete(for key: String) {
|
|
let query = [
|
|
kSecClass: kSecClassGenericPassword,
|
|
kSecAttrService: service,
|
|
kSecAttrAccount: key
|
|
] as CFDictionary
|
|
|
|
SecItemDelete(query)
|
|
}
|
|
}
|