Antidote/Antidote/KeychainManager.swift

183 lines
5.7 KiB
Swift

// 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
private struct Constants {
static let ActiveAccountDataService = "org.zoxcore.Antidote.KeychainManager.ActiveAccountDataService"
static let toxPasswordForActiveAccount = "toxPasswordForActiveAccount"
static let failedPinAttemptsNumber = "failedPinAttemptsNumber"
}
class KeychainManager {
/// Tox password used to encrypt/decrypt active account.
var toxPasswordForActiveAccount: String? {
get {
return getStringForKey(Constants.toxPasswordForActiveAccount)
}
set {
setString(newValue, forKey: Constants.toxPasswordForActiveAccount)
}
}
/// Number of failed enters of pin by user.
var failedPinAttemptsNumber: Int? {
get {
return getIntForKey(Constants.failedPinAttemptsNumber)
}
set {
setInt(newValue, forKey: Constants.failedPinAttemptsNumber)
}
}
/// Removes all data related to active account.
func deleteActiveAccountData() {
toxPasswordForActiveAccount = nil
failedPinAttemptsNumber = nil
}
}
private extension KeychainManager {
func getIntForKey(_ key: String) -> Int? {
guard let data = getDataForKey(key) else {
return nil
}
guard let number = NSKeyedUnarchiver.unarchiveObject(with: data) as? NSNumber else {
return nil
}
return number.intValue
}
func setInt(_ value: Int?, forKey key: String) {
guard let value = value else {
setData(nil, forKey: key)
return
}
let number = NSNumber(value: value)
let data = NSKeyedArchiver.archivedData(withRootObject: number)
setData(data, forKey: key)
}
func getStringForKey(_ key: String) -> String? {
guard let data = getDataForKey(key) else {
return nil
}
return NSString(data: data, encoding: String.Encoding.utf8.rawValue) as String?
}
func setString(_ string: String?, forKey key: String) {
let data = string?.data(using: String.Encoding.utf8)
setData(data, forKey: key)
}
func getBoolForKey(_ key: String) -> Bool? {
guard let data = getDataForKey(key) else {
return nil
}
return (data as NSData).bytes.bindMemory(to: Int.self, capacity: data.count).pointee == 1
}
func setBool(_ value: Bool?, forKey key: String) {
var data: Data? = nil
if let value = value {
var bytes = value ? 1 : 0
withUnsafePointer(to: &bytes) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
data = Data(bytes: $0, count: MemoryLayout<Int>.size)
}
}
}
setData(data, forKey: key)
}
func getDataForKey(_ key: String) -> Data? {
var query = genericQueryWithKey(key)
query[kSecMatchLimit as String] = kSecMatchLimitOne
query[kSecReturnData as String] = kCFBooleanTrue
var queryResult: AnyObject?
let status = withUnsafeMutablePointer(to: &queryResult) {
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
}
if status == errSecItemNotFound {
return nil
}
guard status == noErr else {
log("Error when getting keychain data for key \(key), status \(status)")
return nil
}
guard let data = queryResult as? Data else {
log("Unexpected data for key \(key)")
return nil
}
return data
}
func setData(_ newData: Data?, forKey key: String) {
let oldData = getDataForKey(key)
switch (oldData, newData) {
case (.some(_), .some(let data)):
// Update
let query = genericQueryWithKey(key)
var attributesToUpdate = [String : AnyObject]()
attributesToUpdate[kSecValueData as String] = data as AnyObject?
let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary)
guard status == noErr else {
log("Error when updating keychain data for key \(key), status \(status)")
return
}
case (.some(_), .none):
// Delete
let query = genericQueryWithKey(key)
let status = SecItemDelete(query as CFDictionary)
guard status == noErr else {
log("Error when updating keychain data for key \(key), status \(status)")
return
}
case (.none, .some(let data)):
// Add
var query = genericQueryWithKey(key)
query[kSecValueData as String] = data as AnyObject?
let status = SecItemAdd(query as CFDictionary, nil)
guard status == noErr else {
log("Error when setting keychain data for key \(key), status \(status)")
return
}
case (.none, .none):
// Nothing to do here, no changes
break
}
}
func genericQueryWithKey(_ key: String) -> [String : AnyObject] {
var query = [String : AnyObject]()
query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrService as String] = Constants.ActiveAccountDataService as AnyObject?
query[kSecAttrAccount as String] = key as AnyObject?
query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
return query
}
}