|
7 | 7 |
|
8 | 8 | import Foundation |
9 | 9 | import os |
10 | | -import Security |
11 | 10 |
|
12 | 11 | /// Service for persisting database connections |
13 | 12 | final class ConnectionStorage { |
@@ -162,223 +161,70 @@ final class ConnectionStorage { |
162 | 161 | // - ConnectionFormView — single-item lookup during form population (negligible latency) |
163 | 162 | // No async wrapper is needed; adding one would add complexity without measurable benefit. |
164 | 163 |
|
165 | | - /// Upsert a value into the Keychain: tries SecItemAdd first, falls back to SecItemUpdate |
166 | | - /// on duplicate. Returns true on success. |
167 | | - @discardableResult |
168 | | - private func keychainUpsert(key: String, data: Data) -> Bool { |
169 | | - let baseQuery: [String: Any] = [ |
170 | | - kSecClass as String: kSecClassGenericPassword, |
171 | | - kSecAttrService as String: "com.TablePro", |
172 | | - kSecAttrAccount as String: key, |
173 | | - ] |
174 | | - |
175 | | - let addQuery = baseQuery.merging([ |
176 | | - kSecValueData as String: data, |
177 | | - kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly, |
178 | | - ]) { _, new in new } |
179 | | - |
180 | | - let addStatus = SecItemAdd(addQuery as CFDictionary, nil) |
181 | | - |
182 | | - if addStatus == errSecDuplicateItem { |
183 | | - // Item already exists — update it |
184 | | - let updateAttrs: [String: Any] = [kSecValueData as String: data] |
185 | | - let updateStatus = SecItemUpdate(baseQuery as CFDictionary, updateAttrs as CFDictionary) |
186 | | - if updateStatus != errSecSuccess { |
187 | | - Self.logger.error("Failed to update Keychain item '\(key)': OSStatus \(updateStatus)") |
188 | | - return false |
189 | | - } |
190 | | - return true |
191 | | - } else if addStatus != errSecSuccess { |
192 | | - Self.logger.error("Failed to add Keychain item '\(key)': OSStatus \(addStatus)") |
193 | | - return false |
194 | | - } |
195 | | - return true |
196 | | - } |
197 | | - |
198 | | - /// Save password to Keychain |
199 | 164 | func savePassword(_ password: String, for connectionId: UUID) { |
200 | 165 | let key = "com.TablePro.password.\(connectionId.uuidString)" |
201 | | - guard let data = password.data(using: .utf8) else { return } |
202 | | - keychainUpsert(key: key, data: data) |
| 166 | + KeychainHelper.shared.saveString(password, forKey: key) |
203 | 167 | } |
204 | 168 |
|
205 | | - /// Load password from Keychain |
206 | 169 | func loadPassword(for connectionId: UUID) -> String? { |
207 | 170 | let key = "com.TablePro.password.\(connectionId.uuidString)" |
208 | | - |
209 | | - let query: [String: Any] = [ |
210 | | - kSecClass as String: kSecClassGenericPassword, |
211 | | - kSecAttrService as String: "com.TablePro", |
212 | | - kSecAttrAccount as String: key, |
213 | | - kSecReturnData as String: true, |
214 | | - kSecMatchLimit as String: kSecMatchLimitOne, |
215 | | - ] |
216 | | - |
217 | | - var result: AnyObject? |
218 | | - let status = SecItemCopyMatching(query as CFDictionary, &result) |
219 | | - |
220 | | - guard status == errSecSuccess, |
221 | | - let data = result as? Data, |
222 | | - let password = String(data: data, encoding: .utf8) |
223 | | - else { |
224 | | - return nil |
225 | | - } |
226 | | - |
227 | | - return password |
| 171 | + return KeychainHelper.shared.loadString(forKey: key) |
228 | 172 | } |
229 | 173 |
|
230 | | - /// Delete password from Keychain |
231 | 174 | func deletePassword(for connectionId: UUID) { |
232 | 175 | let key = "com.TablePro.password.\(connectionId.uuidString)" |
233 | | - |
234 | | - let query: [String: Any] = [ |
235 | | - kSecClass as String: kSecClassGenericPassword, |
236 | | - kSecAttrService as String: "com.TablePro", |
237 | | - kSecAttrAccount as String: key, |
238 | | - ] |
239 | | - |
240 | | - SecItemDelete(query as CFDictionary) |
| 176 | + KeychainHelper.shared.delete(key: key) |
241 | 177 | } |
242 | 178 |
|
243 | 179 | // MARK: - SSH Password Storage |
244 | 180 |
|
245 | | - /// Save SSH password to Keychain |
246 | 181 | func saveSSHPassword(_ password: String, for connectionId: UUID) { |
247 | 182 | let key = "com.TablePro.sshpassword.\(connectionId.uuidString)" |
248 | | - guard let data = password.data(using: .utf8) else { return } |
249 | | - keychainUpsert(key: key, data: data) |
| 183 | + KeychainHelper.shared.saveString(password, forKey: key) |
250 | 184 | } |
251 | 185 |
|
252 | | - /// Load SSH password from Keychain |
253 | 186 | func loadSSHPassword(for connectionId: UUID) -> String? { |
254 | 187 | let key = "com.TablePro.sshpassword.\(connectionId.uuidString)" |
255 | | - |
256 | | - let query: [String: Any] = [ |
257 | | - kSecClass as String: kSecClassGenericPassword, |
258 | | - kSecAttrService as String: "com.TablePro", |
259 | | - kSecAttrAccount as String: key, |
260 | | - kSecReturnData as String: true, |
261 | | - kSecMatchLimit as String: kSecMatchLimitOne, |
262 | | - ] |
263 | | - |
264 | | - var result: AnyObject? |
265 | | - let status = SecItemCopyMatching(query as CFDictionary, &result) |
266 | | - |
267 | | - guard status == errSecSuccess, |
268 | | - let data = result as? Data, |
269 | | - let password = String(data: data, encoding: .utf8) |
270 | | - else { |
271 | | - return nil |
272 | | - } |
273 | | - |
274 | | - return password |
| 188 | + return KeychainHelper.shared.loadString(forKey: key) |
275 | 189 | } |
276 | 190 |
|
277 | | - /// Delete SSH password from Keychain |
278 | 191 | func deleteSSHPassword(for connectionId: UUID) { |
279 | 192 | let key = "com.TablePro.sshpassword.\(connectionId.uuidString)" |
280 | | - |
281 | | - let query: [String: Any] = [ |
282 | | - kSecClass as String: kSecClassGenericPassword, |
283 | | - kSecAttrService as String: "com.TablePro", |
284 | | - kSecAttrAccount as String: key, |
285 | | - ] |
286 | | - |
287 | | - SecItemDelete(query as CFDictionary) |
| 193 | + KeychainHelper.shared.delete(key: key) |
288 | 194 | } |
289 | 195 |
|
290 | 196 | // MARK: - Key Passphrase Storage |
291 | 197 |
|
292 | | - /// Save private key passphrase to Keychain |
293 | 198 | func saveKeyPassphrase(_ passphrase: String, for connectionId: UUID) { |
294 | 199 | let key = "com.TablePro.keypassphrase.\(connectionId.uuidString)" |
295 | | - guard let data = passphrase.data(using: .utf8) else { return } |
296 | | - keychainUpsert(key: key, data: data) |
| 200 | + KeychainHelper.shared.saveString(passphrase, forKey: key) |
297 | 201 | } |
298 | 202 |
|
299 | | - /// Load private key passphrase from Keychain |
300 | 203 | func loadKeyPassphrase(for connectionId: UUID) -> String? { |
301 | 204 | let key = "com.TablePro.keypassphrase.\(connectionId.uuidString)" |
302 | | - |
303 | | - let query: [String: Any] = [ |
304 | | - kSecClass as String: kSecClassGenericPassword, |
305 | | - kSecAttrService as String: "com.TablePro", |
306 | | - kSecAttrAccount as String: key, |
307 | | - kSecReturnData as String: true, |
308 | | - kSecMatchLimit as String: kSecMatchLimitOne, |
309 | | - ] |
310 | | - |
311 | | - var result: AnyObject? |
312 | | - let status = SecItemCopyMatching(query as CFDictionary, &result) |
313 | | - |
314 | | - guard status == errSecSuccess, |
315 | | - let data = result as? Data, |
316 | | - let passphrase = String(data: data, encoding: .utf8) |
317 | | - else { |
318 | | - return nil |
319 | | - } |
320 | | - |
321 | | - return passphrase |
| 205 | + return KeychainHelper.shared.loadString(forKey: key) |
322 | 206 | } |
323 | 207 |
|
324 | | - /// Delete private key passphrase from Keychain |
325 | 208 | func deleteKeyPassphrase(for connectionId: UUID) { |
326 | 209 | let key = "com.TablePro.keypassphrase.\(connectionId.uuidString)" |
327 | | - |
328 | | - let query: [String: Any] = [ |
329 | | - kSecClass as String: kSecClassGenericPassword, |
330 | | - kSecAttrService as String: "com.TablePro", |
331 | | - kSecAttrAccount as String: key, |
332 | | - ] |
333 | | - |
334 | | - SecItemDelete(query as CFDictionary) |
| 210 | + KeychainHelper.shared.delete(key: key) |
335 | 211 | } |
336 | 212 |
|
337 | 213 | // MARK: - TOTP Secret Storage |
338 | 214 |
|
339 | | - /// Save TOTP secret to Keychain |
340 | 215 | func saveTOTPSecret(_ secret: String, for connectionId: UUID) { |
341 | 216 | let key = "com.TablePro.totpsecret.\(connectionId.uuidString)" |
342 | | - guard let data = secret.data(using: .utf8) else { return } |
343 | | - keychainUpsert(key: key, data: data) |
| 217 | + KeychainHelper.shared.saveString(secret, forKey: key) |
344 | 218 | } |
345 | 219 |
|
346 | | - /// Load TOTP secret from Keychain |
347 | 220 | func loadTOTPSecret(for connectionId: UUID) -> String? { |
348 | 221 | let key = "com.TablePro.totpsecret.\(connectionId.uuidString)" |
349 | | - |
350 | | - let query: [String: Any] = [ |
351 | | - kSecClass as String: kSecClassGenericPassword, |
352 | | - kSecAttrService as String: "com.TablePro", |
353 | | - kSecAttrAccount as String: key, |
354 | | - kSecReturnData as String: true, |
355 | | - kSecMatchLimit as String: kSecMatchLimitOne, |
356 | | - ] |
357 | | - |
358 | | - var result: AnyObject? |
359 | | - let status = SecItemCopyMatching(query as CFDictionary, &result) |
360 | | - |
361 | | - guard status == errSecSuccess, |
362 | | - let data = result as? Data, |
363 | | - let secret = String(data: data, encoding: .utf8) |
364 | | - else { |
365 | | - return nil |
366 | | - } |
367 | | - |
368 | | - return secret |
| 222 | + return KeychainHelper.shared.loadString(forKey: key) |
369 | 223 | } |
370 | 224 |
|
371 | | - /// Delete TOTP secret from Keychain |
372 | 225 | func deleteTOTPSecret(for connectionId: UUID) { |
373 | 226 | let key = "com.TablePro.totpsecret.\(connectionId.uuidString)" |
374 | | - |
375 | | - let query: [String: Any] = [ |
376 | | - kSecClass as String: kSecClassGenericPassword, |
377 | | - kSecAttrService as String: "com.TablePro", |
378 | | - kSecAttrAccount as String: key, |
379 | | - ] |
380 | | - |
381 | | - SecItemDelete(query as CFDictionary) |
| 227 | + KeychainHelper.shared.delete(key: key) |
382 | 228 | } |
383 | 229 | } |
384 | 230 |
|
|
0 commit comments