|
1 | 1 | import CryptoKit |
2 | 2 | import Foundation |
3 | 3 |
|
4 | | -class EncryptedUserDefaultsClientImplementation: EncryptedUserDefaultsClient { |
5 | | - private let keychainClient: KeychainClient |
| 4 | +final class EncryptedUserDefaultsClientImplementation: EncryptedUserDefaultsClient { |
| 5 | + static let shared = EncryptedUserDefaultsClientImplementation() |
| 6 | + @Dependency(\.keychainClient) private var keychainClient |
| 7 | + private let queue: DispatchQueue |
| 8 | + private let queueKey = DispatchSpecificKey<Void>() |
| 9 | + |
| 10 | + private var isOnQueue: Bool { |
| 11 | + DispatchQueue.getSpecific(key: queueKey) != nil |
| 12 | + } |
6 | 13 |
|
7 | 14 | internal let defaults: UserDefaults = .init(suiteName: "StytchEncryptedUserDefaults") ?? .standard |
8 | 15 |
|
9 | | - init(keychainClient: KeychainClient) { |
10 | | - self.keychainClient = keychainClient |
| 16 | + private init() { |
| 17 | + queue = DispatchQueue(label: "StytchEncryptedUserDefaultsClientQueue") |
| 18 | + queue.setSpecific(key: queueKey, value: ()) |
11 | 19 | } |
12 | 20 |
|
13 | | - func getItem(item: EncryptedUserDefaultsItem) throws -> EncryptedUserDefaultsItemResult? { |
14 | | - let userDefaultsData = defaults.data(forKey: item.name) |
15 | | - guard let decrypted = decryptData(encryptedData: userDefaultsData) else { |
16 | | - return nil |
| 21 | + private func safelyEnqueue<T>(_ block: () throws -> T) throws -> T { |
| 22 | + if isOnQueue { |
| 23 | + return try block() |
| 24 | + } else { |
| 25 | + return try queue.sync { try block() } |
17 | 26 | } |
18 | | - guard let data = decrypted.data(using: .utf8) else { |
19 | | - return nil |
| 27 | + } |
| 28 | + |
| 29 | + func getItem(item: EncryptedUserDefaultsItem) throws -> EncryptedUserDefaultsItemResult? { |
| 30 | + try safelyEnqueue { |
| 31 | + let userDefaultsData = defaults.data(forKey: item.name) |
| 32 | + guard let decrypted = decryptData(encryptedData: userDefaultsData) else { |
| 33 | + return nil |
| 34 | + } |
| 35 | + guard let data = decrypted.data(using: .utf8) else { |
| 36 | + return nil |
| 37 | + } |
| 38 | + return .init(data: data) |
20 | 39 | } |
21 | | - return .init(data: data) |
22 | 40 | } |
23 | 41 |
|
24 | 42 | func itemExists(item: EncryptedUserDefaultsItem) -> Bool { |
25 | | - let encryptedData = defaults.data(forKey: item.name) |
26 | | - return encryptedData != nil |
| 43 | + let result = try? safelyEnqueue { |
| 44 | + let encryptedData = defaults.data(forKey: item.name) |
| 45 | + return encryptedData != nil |
| 46 | + } |
| 47 | + return result ?? false |
27 | 48 | } |
28 | 49 |
|
29 | 50 | func setValueForItem(value: String?, item: EncryptedUserDefaultsItem) throws { |
30 | | - guard let valueString = value else { |
31 | | - return removeItem(item: item) |
| 51 | + try safelyEnqueue { |
| 52 | + guard let valueString = value else { |
| 53 | + return removeItem(item: item) |
| 54 | + } |
| 55 | + let encryptedText = try encryptString(plainText: valueString) |
| 56 | + defaults.set(encryptedText, forKey: item.name) |
| 57 | + let encryptedDate = try encryptString(plainText: Date().asJson(encoder: Current.jsonEncoder)) |
| 58 | + defaults.set(encryptedDate, forKey: EncryptedUserDefaultsItem.lastValidatedAtDate(item.name).name) |
32 | 59 | } |
33 | | - let encryptedText = try encryptString(plainText: valueString) |
34 | | - defaults.set(encryptedText, forKey: item.name) |
35 | | - let encryptedDate = try encryptString(plainText: Date().asJson(encoder: Current.jsonEncoder)) |
36 | | - defaults.set(encryptedDate, forKey: EncryptedUserDefaultsItem.lastValidatedAtDate(item.name).name) |
37 | 60 | } |
38 | 61 |
|
39 | 62 | func removeItem(item: EncryptedUserDefaultsItem) { |
40 | | - defaults.removeObject(forKey: item.name) |
41 | | - defaults.removeObject(forKey: EncryptedUserDefaultsItem.lastValidatedAtDate(item.name).name) |
| 63 | + try? safelyEnqueue { |
| 64 | + defaults.removeObject(forKey: item.name) |
| 65 | + defaults.removeObject(forKey: EncryptedUserDefaultsItem.lastValidatedAtDate(item.name).name) |
| 66 | + } |
42 | 67 | } |
43 | 68 |
|
44 | 69 | private func encryptString(plainText: String) throws -> Data? { |
45 | | - guard let encryptionKey = keychainClient.encryptionKey else { return nil } |
46 | | - let sealedBox = try AES.GCM.seal( |
47 | | - Data(plainText.utf8), |
48 | | - using: encryptionKey |
49 | | - ) |
50 | | - return sealedBox.combined |
| 70 | + try safelyEnqueue { |
| 71 | + guard let encryptionKey = keychainClient.encryptionKey else { return nil } |
| 72 | + let sealedBox = try AES.GCM.seal( |
| 73 | + Data(plainText.utf8), |
| 74 | + using: encryptionKey |
| 75 | + ) |
| 76 | + return sealedBox.combined |
| 77 | + } |
51 | 78 | } |
52 | 79 |
|
53 | 80 | private func decryptData(encryptedData: Data?) -> String? { |
54 | | - guard let encryptionKey = keychainClient.encryptionKey, let encryptedData else { return nil } |
55 | | - do { |
56 | | - let sealedBox = try AES.GCM.SealedBox(combined: encryptedData) |
57 | | - let decryptedData = try AES.GCM.open(sealedBox, using: encryptionKey) |
58 | | - return String(data: decryptedData, encoding: .utf8) |
59 | | - } catch { |
60 | | - return nil |
| 81 | + try? safelyEnqueue { |
| 82 | + guard let encryptionKey = keychainClient.encryptionKey, let encryptedData else { return nil } |
| 83 | + do { |
| 84 | + let sealedBox = try AES.GCM.SealedBox(combined: encryptedData) |
| 85 | + let decryptedData = try AES.GCM.open(sealedBox, using: encryptionKey) |
| 86 | + return String(data: decryptedData, encoding: .utf8) |
| 87 | + } catch { |
| 88 | + return nil |
| 89 | + } |
61 | 90 | } |
62 | 91 | } |
63 | 92 | } |
0 commit comments