Skip to content

Commit fa70fb4

Browse files
authored
refactor: implement Keychain and drop KeychainAccess dependency (#403)
1 parent 569f445 commit fa70fb4

File tree

4 files changed

+232
-26
lines changed

4 files changed

+232
-26
lines changed

Package.resolved

Lines changed: 0 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ let package = Package(
2727
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"),
2828
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.8.1"),
2929
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0"),
30-
.package(url: "https://github.com/kishikawakatsumi/KeychainAccess", from: "4.2.2"),
3130
],
3231
targets: [
3332
.target(
@@ -48,13 +47,6 @@ let package = Package(
4847
dependencies: [
4948
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
5049
.product(name: "Crypto", package: "swift-crypto"),
51-
.product(
52-
name: "KeychainAccess",
53-
package: "KeychainAccess",
54-
condition: .when(
55-
platforms: [.macOS, .iOS, .macCatalyst, .visionOS, .tvOS, .watchOS]
56-
)
57-
),
5850
"_Helpers",
5951
]
6052
),
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
#if !os(Windows) && !os(Linux)
2+
import Foundation
3+
import Security
4+
5+
struct Keychain {
6+
let service: String
7+
let accessGroup: String?
8+
9+
init(
10+
service: String,
11+
accessGroup: String? = nil
12+
) {
13+
self.service = service
14+
self.accessGroup = accessGroup
15+
}
16+
17+
private func assertSuccess(forStatus status: OSStatus) throws {
18+
if status != errSecSuccess {
19+
throw KeychainError(code: KeychainError.Code(rawValue: status))
20+
}
21+
}
22+
23+
func data(forKey key: String) throws -> Data {
24+
let query = getOneQuery(byKey: key)
25+
var result: AnyObject?
26+
try assertSuccess(forStatus: SecItemCopyMatching(query as CFDictionary, &result))
27+
28+
guard let data = result as? Data else {
29+
let message = "Unable to cast the retrieved item to a Data value"
30+
throw KeychainError(code: KeychainError.Code.unknown(message: message))
31+
}
32+
33+
return data
34+
}
35+
36+
func set(_ data: Data, forKey key: String) throws {
37+
let addItemQuery = setQuery(forKey: key, data: data)
38+
let addStatus = SecItemAdd(addItemQuery as CFDictionary, nil)
39+
40+
if addStatus == KeychainError.duplicateItem.status {
41+
let updateQuery = baseQuery(withKey: key)
42+
let updateAttributes: [String: Any] = [kSecValueData as String: data]
43+
let updateStatus = SecItemUpdate(updateQuery as CFDictionary, updateAttributes as CFDictionary)
44+
try assertSuccess(forStatus: updateStatus)
45+
} else {
46+
try assertSuccess(forStatus: addStatus)
47+
}
48+
}
49+
50+
func deleteItem(forKey key: String) throws {
51+
let query = baseQuery(withKey: key)
52+
try assertSuccess(forStatus: SecItemDelete(query as CFDictionary))
53+
}
54+
55+
private func baseQuery(withKey key: String? = nil, data: Data? = nil) -> [String: Any] {
56+
var query: [String: Any] = [:]
57+
query[kSecClass as String] = kSecClassGenericPassword
58+
query[kSecAttrService as String] = service
59+
60+
if let key {
61+
query[kSecAttrAccount as String] = key
62+
}
63+
if let data {
64+
query[kSecValueData as String] = data
65+
}
66+
if let accessGroup {
67+
query[kSecAttrAccessGroup as String] = accessGroup
68+
}
69+
70+
return query
71+
}
72+
73+
func getOneQuery(byKey key: String) -> [String: Any] {
74+
var query = baseQuery(withKey: key)
75+
query[kSecReturnData as String] = kCFBooleanTrue
76+
query[kSecMatchLimit as String] = kSecMatchLimitOne
77+
return query
78+
}
79+
80+
func setQuery(forKey key: String, data: Data) -> [String: Any] {
81+
var query = baseQuery(withKey: key, data: data)
82+
83+
query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
84+
85+
return query
86+
}
87+
}
88+
89+
struct KeychainError: LocalizedError, CustomDebugStringConvertible {
90+
enum Code: RawRepresentable, Equatable {
91+
case operationNotImplemented
92+
case invalidParameters
93+
case userCanceled
94+
case itemNotAvailable
95+
case authFailed
96+
case duplicateItem
97+
case itemNotFound
98+
case interactionNotAllowed
99+
case decodeFailed
100+
case other(status: OSStatus)
101+
case unknown(message: String)
102+
103+
init(rawValue: OSStatus) {
104+
switch rawValue {
105+
case errSecUnimplemented: self = .operationNotImplemented
106+
case errSecParam: self = .invalidParameters
107+
case errSecUserCanceled: self = .userCanceled
108+
case errSecNotAvailable: self = .itemNotAvailable
109+
case errSecAuthFailed: self = .authFailed
110+
case errSecDuplicateItem: self = .duplicateItem
111+
case errSecItemNotFound: self = .itemNotFound
112+
case errSecInteractionNotAllowed: self = .interactionNotAllowed
113+
case errSecDecode: self = .decodeFailed
114+
default: self = .other(status: rawValue)
115+
}
116+
}
117+
118+
var rawValue: OSStatus {
119+
switch self {
120+
case .operationNotImplemented: errSecUnimplemented
121+
case .invalidParameters: errSecParam
122+
case .userCanceled: errSecUserCanceled
123+
case .itemNotAvailable: errSecNotAvailable
124+
case .authFailed: errSecAuthFailed
125+
case .duplicateItem: errSecDuplicateItem
126+
case .itemNotFound: errSecItemNotFound
127+
case .interactionNotAllowed: errSecInteractionNotAllowed
128+
case .decodeFailed: errSecDecode
129+
case let .other(status): status
130+
case .unknown: errSecSuccess // This is not a Keychain error
131+
}
132+
}
133+
}
134+
135+
let code: Code
136+
137+
init(code: Code) {
138+
self.code = code
139+
}
140+
141+
var status: OSStatus {
142+
code.rawValue
143+
}
144+
145+
var localizedDescription: String { debugDescription }
146+
147+
var errorDescription: String? { debugDescription }
148+
149+
var debugDescription: String {
150+
switch code {
151+
case .operationNotImplemented:
152+
"errSecUnimplemented: A function or operation is not implemented."
153+
case .invalidParameters:
154+
"errSecParam: One or more parameters passed to the function are not valid."
155+
case .userCanceled:
156+
"errSecUserCanceled: User canceled the operation."
157+
case .itemNotAvailable:
158+
"errSecNotAvailable: No trust results are available."
159+
case .authFailed:
160+
"errSecAuthFailed: Authorization and/or authentication failed."
161+
case .duplicateItem:
162+
"errSecDuplicateItem: The item already exists."
163+
case .itemNotFound:
164+
"errSecItemNotFound: The item cannot be found."
165+
case .interactionNotAllowed:
166+
"errSecInteractionNotAllowed: Interaction with the Security Server is not allowed."
167+
case .decodeFailed:
168+
"errSecDecode: Unable to decode the provided data."
169+
case .other:
170+
"Unspecified Keychain error: \(status)."
171+
case let .unknown(message):
172+
"Unknown error: \(message)."
173+
}
174+
}
175+
176+
// MARK: - Error Cases
177+
178+
/// A function or operation is not implemented.
179+
/// See [errSecUnimplemented](https://developer.apple.com/documentation/security/errsecunimplemented).
180+
static let operationNotImplemented: KeychainError = .init(code: .operationNotImplemented)
181+
182+
/// One or more parameters passed to the function are not valid.
183+
/// See [errSecParam](https://developer.apple.com/documentation/security/errsecparam).
184+
static let invalidParameters: KeychainError = .init(code: .invalidParameters)
185+
186+
/// User canceled the operation.
187+
/// See [errSecUserCanceled](https://developer.apple.com/documentation/security/errsecusercanceled).
188+
static let userCanceled: KeychainError = .init(code: .userCanceled)
189+
190+
/// No trust results are available.
191+
/// See [errSecNotAvailable](https://developer.apple.com/documentation/security/errsecnotavailable).
192+
static let itemNotAvailable: KeychainError = .init(code: .itemNotAvailable)
193+
194+
/// Authorization and/or authentication failed.
195+
/// See [errSecAuthFailed](https://developer.apple.com/documentation/security/errsecauthfailed).
196+
static let authFailed: KeychainError = .init(code: .authFailed)
197+
198+
/// The item already exists.
199+
/// See [errSecDuplicateItem](https://developer.apple.com/documentation/security/errsecduplicateitem).
200+
static let duplicateItem: KeychainError = .init(code: .duplicateItem)
201+
202+
/// The item cannot be found.
203+
/// See [errSecItemNotFound](https://developer.apple.com/documentation/security/errsecitemnotfound).
204+
static let itemNotFound: KeychainError = .init(code: .itemNotFound)
205+
206+
/// Interaction with the Security Server is not allowed.
207+
/// See [errSecInteractionNotAllowed](https://developer.apple.com/documentation/security/errsecinteractionnotallowed).
208+
static let interactionNotAllowed: KeychainError = .init(code: .interactionNotAllowed)
209+
210+
/// Unable to decode the provided data.
211+
/// See [errSecDecode](https://developer.apple.com/documentation/security/errsecdecode).
212+
static let decodeFailed: KeychainError = .init(code: .decodeFailed)
213+
214+
/// Other Keychain error.
215+
/// The `OSStatus` of the Keychain operation can be accessed via the ``status`` property.
216+
static let other: KeychainError = .init(code: .other(status: 0))
217+
218+
/// Unknown error. This is not a Keychain error but a Keychain failure. For example, being unable to cast the
219+
/// retrieved item.
220+
static let unknown: KeychainError = .init(code: .unknown(message: ""))
221+
}
222+
223+
extension KeychainError: Equatable {
224+
static func == (lhs: KeychainError, rhs: KeychainError) -> Bool {
225+
lhs.code == rhs.code && lhs.localizedDescription == rhs.localizedDescription
226+
}
227+
}
228+
#endif
Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,23 @@
11
#if !os(Windows) && !os(Linux)
22
import Foundation
3-
@preconcurrency import KeychainAccess
43

54
public struct KeychainLocalStorage: AuthLocalStorage {
65
private let keychain: Keychain
76

87
public init(service: String, accessGroup: String?) {
9-
if let accessGroup {
10-
keychain = Keychain(service: service, accessGroup: accessGroup)
11-
} else {
12-
keychain = Keychain(service: service)
13-
}
8+
keychain = Keychain(service: service, accessGroup: accessGroup)
149
}
1510

1611
public func store(key: String, value: Data) throws {
17-
try keychain.set(value, key: key)
12+
try keychain.set(value, forKey: key)
1813
}
1914

2015
public func retrieve(key: String) throws -> Data? {
21-
try keychain.getData(key)
16+
try keychain.data(forKey: key)
2217
}
2318

2419
public func remove(key: String) throws {
25-
try keychain.remove(key)
20+
try keychain.deleteItem(forKey: key)
2621
}
2722
}
2823
#endif

0 commit comments

Comments
 (0)