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
|
import Foundation
import Security
/// Thin wrapper over the Keychain for storing API keys. Service-scoped so
/// values never collide with other apps.
enum Keychain {
static let service = "co.soryu.makima"
enum Error: Swift.Error, Equatable {
case unhandled(OSStatus)
case invalidData
}
@discardableResult
static func set(_ value: String, for account: String) throws -> Bool {
let data = Data(value.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account
]
let attrs: [String: Any] = [
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
]
// Try update first
let updateStatus = SecItemUpdate(query as CFDictionary, attrs as CFDictionary)
if updateStatus == errSecSuccess { return true }
if updateStatus != errSecItemNotFound {
throw Error.unhandled(updateStatus)
}
var addQuery = query
for (k, v) in attrs { addQuery[k] = v }
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
guard addStatus == errSecSuccess else {
throw Error.unhandled(addStatus)
}
return true
}
static func get(_ account: String) throws -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
if status == errSecItemNotFound { return nil }
guard status == errSecSuccess else { throw Error.unhandled(status) }
guard let data = item as? Data, let string = String(data: data, encoding: .utf8) else {
throw Error.invalidData
}
return string
}
@discardableResult
static func delete(_ account: String) throws -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account
]
let status = SecItemDelete(query as CFDictionary)
if status == errSecSuccess || status == errSecItemNotFound { return true }
throw Error.unhandled(status)
}
}
|