Skip to content

Commit 70ccce8

Browse files
Add a DispatchQueue to EncryptedUserDefaults the same way we did to the KeychainClient; Don't trigger the startup flow (or attempt to retrieve the encryption key) until we are sure that protectedData is available (#506)
1 parent 196a993 commit 70ccce8

File tree

6 files changed

+94
-44
lines changed

6 files changed

+94
-44
lines changed
Lines changed: 63 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,92 @@
11
import CryptoKit
22
import Foundation
33

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+
}
613

714
internal let defaults: UserDefaults = .init(suiteName: "StytchEncryptedUserDefaults") ?? .standard
815

9-
init(keychainClient: KeychainClient) {
10-
self.keychainClient = keychainClient
16+
private init() {
17+
queue = DispatchQueue(label: "StytchEncryptedUserDefaultsClientQueue")
18+
queue.setSpecific(key: queueKey, value: ())
1119
}
1220

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() }
1726
}
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)
2039
}
21-
return .init(data: data)
2240
}
2341

2442
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
2748
}
2849

2950
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)
3259
}
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)
3760
}
3861

3962
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+
}
4267
}
4368

4469
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+
}
5178
}
5279

5380
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+
}
6190
}
6291
}
6392
}

Sources/StytchCore/Environment.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ struct Environment {
7171

7272
var keychainClient: KeychainClient = KeychainClientImplementation.shared
7373

74-
var userDefaultsClient: EncryptedUserDefaultsClient = EncryptedUserDefaultsClientImplementation(keychainClient: KeychainClientImplementation.shared)
74+
var userDefaultsClient: EncryptedUserDefaultsClient = EncryptedUserDefaultsClientImplementation.shared
7575

7676
var networkMonitor: NetworkMonitor = .init()
7777

Sources/StytchCore/KeychainClient/KeychainClient.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ protocol KeychainClient: AnyObject {
88
func valueExistsForItem(item: KeychainItem) -> Bool
99
func setValueForItem(value: KeychainItem.Value, item: KeychainItem) throws
1010
func removeItem(item: KeychainItem) throws
11+
func onProtectedDataDidBecomeAvailable()
1112
}
1213

1314
extension KeychainClient {

Sources/StytchCore/KeychainClient/KeychainClientImplementation.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ final class KeychainClientImplementation: KeychainClient {
1919
private init() {
2020
queue = DispatchQueue(label: "StytchKeychainClientQueue")
2121
queue.setSpecific(key: queueKey, value: ())
22-
encryptionKey = try? getEncryptionKey()
22+
}
23+
24+
func onProtectedDataDidBecomeAvailable() {
25+
try? safelyEnqueue {
26+
encryptionKey = try? getEncryptionKey()
27+
}
2328
}
2429

2530
func safelyEnqueue<T>(_ block: () throws -> T) throws -> T {

Sources/StytchCore/StytchClientType.swift

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,18 @@ extension StytchClientType {
8282
}
8383
}
8484
}
85-
#endif
86-
87-
Task {
88-
do {
89-
try await StartupClient.start(clientType: Self.clientType)
90-
try? await EventsClient.logEvent(parameters: .init(eventName: "client_initialization_success"))
91-
} catch {
92-
try? await EventsClient.logEvent(parameters: .init(eventName: "client_initialization_failure"))
85+
if UIApplication.shared.isProtectedDataAvailable {
86+
keychainClient.onProtectedDataDidBecomeAvailable()
87+
defaultStartupFlow()
88+
} else {
89+
NotificationCenter.default.addObserver(forName: UIApplication.protectedDataDidBecomeAvailableNotification, object: nil, queue: nil) { _ in
90+
keychainClient.onProtectedDataDidBecomeAvailable()
91+
defaultStartupFlow()
9392
}
9493
}
94+
#else
95+
defaultStartupFlow()
96+
#endif
9597
}
9698

9799
// swiftlint:disable:next identifier_name large_tuple
@@ -139,4 +141,15 @@ extension StytchClientType {
139141
}
140142
}
141143
}
144+
145+
private func defaultStartupFlow() {
146+
Task {
147+
do {
148+
try await StartupClient.start(clientType: Self.clientType)
149+
try? await EventsClient.logEvent(parameters: .init(eventName: "client_initialization_success"))
150+
} catch {
151+
try? await EventsClient.logEvent(parameters: .init(eventName: "client_initialization_failure"))
152+
}
153+
}
154+
}
142155
}

Tests/StytchCoreTests/KeychainClient+Mock.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import Foundation
66
public var keychainDateCreatedOffsetInMinutes = 0
77

88
class KeychainClientMock: KeychainClient {
9+
func onProtectedDataDidBecomeAvailable() {}
10+
911
var encryptionKey: SymmetricKey? {
1012
do {
1113
return SymmetricKey(data: try Current.cryptoClient.dataWithRandomBytesOfCount(256))

0 commit comments

Comments
 (0)