-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathKeychain.swift
More file actions
175 lines (139 loc) · 4.67 KB
/
Copy pathKeychain.swift
File metadata and controls
175 lines (139 loc) · 4.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import Foundation
/// A Keychain wrapper that offers key/value storage with the following features:
///
/// * Takes an identifier to maintain separate stores
/// * Each Keychain offers independent key/value storage
/// * Caches values in memory
/// * Allows reading and writing Data directly
/// * Encodes Strings as UTF-8
/// * Encodes JSONSerialization-compatible types in JSON
/// * Supports "reset" to delete all keys in this Keychain
///
public actor Keychain {
public let identifier: String
private var cache: [String: Data] = [:]
public init(identifier: String) {
self.identifier = identifier
}
// MARK: - Data Operations
public func data(for key: String) throws -> Data? {
// First check the cache
if let data = cache[key] {
return data
}
// That terrible `SecItemCopyMatching` call you all know...
let data = try _data(for: key)
// And cache it for later
cache[key] = data
return data
}
public func set(data: Data, for key: String) throws {
// Set it to the cache and to system keychain
cache[key] = data
try _set(data: data, for: key)
}
public func removeData(for key: String) throws {
// Remove it from the cache and the system keychain
cache[key] = nil
try _removeData(for: key)
}
public func reset() throws {
// Clear the cache and delete all keys for this identifier
cache = [:]
try _reset()
}
// MARK: - String Operations -- Encode as UTF-8
public func string(for key: String) throws -> String? {
guard let data = try data(for: key) else { return nil }
return String(data: data, encoding: .utf8)
}
public func set(string: String, for key: String) throws {
try set(data: Data(string.utf8), for: key)
}
// MARK: - JSONSerialization Operations
public func value(for key: String) throws -> Any? {
guard let data = try data(for: key) else { return nil }
return try JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed])
}
public func set(value: Any, for key: String) throws {
let data = try JSONSerialization.data(withJSONObject: value, options: [.fragmentsAllowed])
try set(data: data, for: key)
}
// MARK: - Public Extensions for Common Types (Bool, Int)
public func bool(for key: String) throws -> Bool? {
try value(for: key) as? Bool
}
public func set(bool: Bool, for key: String) throws {
try set(value: bool, for: key)
}
public func int(for key: String) throws -> Int? {
try value(for: key) as? Int
}
public func set(int: Int, for key: String) throws {
try set(value: int, for: key)
}
// MARK: - All those horrible low-level SecItem... wrappers that I'm not going to bore you with
private func makeParameters(for key: String?) -> [CFString: Any] {
var query: [CFString: Any] = [
kSecAttrGeneric: Data(self.identifier.utf8),
kSecClass: kSecClassGenericPassword,
]
if let key {
query[kSecAttrService] = key
}
return query
}
private func _data(for key: String) throws -> Data? {
var params = makeParameters(for: key)
params[kSecMatchLimit] = kSecMatchLimitOne
params[kSecReturnData] = kCFBooleanTrue
var result: CFTypeRef?
let status = SecItemCopyMatching(
params as CFDictionary,
&result)
if status == errSecItemNotFound {
return nil
}
guard status == errSecSuccess else {
throw KeychainError(status)
}
return result as? Data
}
private func _set(data: Data, for key: String) throws {
var params = makeParameters(for: key)
// Attempt to update the entry
var status = SecItemUpdate(
params as CFDictionary,
[kSecValueData: data] as CFDictionary)
// If it doesn't exist, try adding it
if status == errSecItemNotFound {
params[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
params[kSecValueData] = data
status = SecItemAdd(params as CFDictionary, nil)
}
guard status == errSecSuccess else {
throw KeychainError(status)
}
}
private func _removeData(for key: String) throws {
let params = makeParameters(for: key)
let status = SecItemDelete(params as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeychainError(status)
}
}
private func _reset() throws {
var query = makeParameters(for: nil)
#if os(macOS)
query[kSecMatchLimit] = kSecMatchLimitAll
#endif
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeychainError(status)
}
}
}
public struct KeychainError: Swift.Error {
let status: OSStatus
init(_ status: OSStatus) { self.status = status }
}