|
| 1 | +// |
| 2 | +// KeychainItem.swift |
| 3 | +// Twift_SwiftUI |
| 4 | +// |
| 5 | +// Created by Daniel Eden on 10/08/2022. |
| 6 | +// |
| 7 | + |
| 8 | +import Foundation |
| 9 | +import Security |
| 10 | + |
| 11 | +private func throwIfNotZero(_ status: OSStatus) throws { |
| 12 | + guard status != 0 else { return } |
| 13 | + throw KeychainError.keychainError(status: status) |
| 14 | +} |
| 15 | + |
| 16 | +public enum KeychainError: Error { |
| 17 | + case invalidData |
| 18 | + case keychainError(status: OSStatus) |
| 19 | +} |
| 20 | + |
| 21 | +extension Dictionary { |
| 22 | + func adding(key: Key, value: Value) -> Dictionary { |
| 23 | + var copy = self |
| 24 | + copy[key] = value |
| 25 | + return copy |
| 26 | + } |
| 27 | +} |
| 28 | + |
| 29 | +@propertyWrapper |
| 30 | +/// This class is for demonstrative purposes only to illustrate how an encoded OAuth2User can be encoded and stored to the user's keychain. |
| 31 | +final public class KeychainItem { |
| 32 | + private let account: String |
| 33 | + private let accessGroup: String? |
| 34 | + |
| 35 | + public init(account: String) { |
| 36 | + self.account = account |
| 37 | + self.accessGroup = nil |
| 38 | + } |
| 39 | + |
| 40 | + public init(account: String, accessGroup: String) { |
| 41 | + self.account = account |
| 42 | + self.accessGroup = accessGroup |
| 43 | + } |
| 44 | + |
| 45 | + private var baseDictionary: [String:AnyObject] { |
| 46 | + let base = [ |
| 47 | + kSecClass as String: kSecClassGenericPassword, |
| 48 | + kSecAttrAccount as String: account as AnyObject, |
| 49 | + kSecAttrSynchronizable as String: kCFBooleanTrue! |
| 50 | + ] |
| 51 | + |
| 52 | + return accessGroup == nil |
| 53 | + ? base |
| 54 | + : base.adding(key: kSecAttrAccessGroup as String, value: accessGroup as AnyObject) |
| 55 | + } |
| 56 | + |
| 57 | + private var query: [String:AnyObject] { |
| 58 | + return baseDictionary.adding(key: kSecMatchLimit as String, value: kSecMatchLimitOne) |
| 59 | + } |
| 60 | + |
| 61 | + public var wrappedValue: String? { |
| 62 | + get { |
| 63 | + try? read() |
| 64 | + } |
| 65 | + set { |
| 66 | + if let v = newValue { |
| 67 | + if let _ = try? read() { |
| 68 | + try! update(v) |
| 69 | + } else { |
| 70 | + try! add(v) |
| 71 | + } |
| 72 | + } else { |
| 73 | + try? delete() |
| 74 | + } |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + private func delete() throws { |
| 79 | + // SecItemDelete seems to fail with errSecItemNotFound if the item does not exist in the keychain. Is this expected behavior? |
| 80 | + let status = SecItemDelete(baseDictionary as CFDictionary) |
| 81 | + guard status != errSecItemNotFound else { return } |
| 82 | + try throwIfNotZero(status) |
| 83 | + } |
| 84 | + |
| 85 | + private func read() throws -> String? { |
| 86 | + let query = self.query.adding(key: kSecReturnData as String, value: true as AnyObject) |
| 87 | + var result: AnyObject? = nil |
| 88 | + let status = SecItemCopyMatching(query as CFDictionary, &result) |
| 89 | + guard status != errSecItemNotFound else { return nil } |
| 90 | + try throwIfNotZero(status) |
| 91 | + guard let data = result as? Data, let string = String(data: data, encoding: .utf8) else { |
| 92 | + throw KeychainError.invalidData |
| 93 | + } |
| 94 | + return string |
| 95 | + } |
| 96 | + |
| 97 | + private func update(_ secret: String) throws { |
| 98 | + let dictionary: [String:AnyObject] = [ |
| 99 | + kSecValueData as String: secret.data(using: String.Encoding.utf8)! as AnyObject |
| 100 | + ] |
| 101 | + try throwIfNotZero(SecItemUpdate(baseDictionary as CFDictionary, dictionary as CFDictionary)) |
| 102 | + } |
| 103 | + |
| 104 | + private func add(_ secret: String) throws { |
| 105 | + let dictionary = baseDictionary.adding(key: kSecValueData as String, value: secret.data(using: .utf8)! as AnyObject) |
| 106 | + try throwIfNotZero(SecItemAdd(dictionary as CFDictionary, nil)) |
| 107 | + } |
| 108 | +} |
0 commit comments