|
| 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 |
0 commit comments