Skip to content

Commit 3f2a950

Browse files
jhaven-stytchSailor-Saturnnonameplum
authored
Prevent overwriting encryption keys (#603)
* Lazily load encryption key to avoid fresh-install reset race (#601) Delay encryption key initialization until first use, preventing key creation before fresh-install keychain reset during SDK configure. Co-authored-by: Łukasz Śliwiński <lukasz.sliwinski@withintent.com> * Add guardrails around encryption key retrieval to ensure we don't overwrite a key just because the keychain was not yet available * Update test * Lint --------- Co-authored-by: Sailor-Saturn <46728174+Sailor-Saturn@users.noreply.github.com> Co-authored-by: Łukasz Śliwiński <lukasz.sliwinski@withintent.com>
1 parent 9a8b6bf commit 3f2a950

File tree

4 files changed

+46
-12
lines changed

4 files changed

+46
-12
lines changed

Sources/StytchCore/KeychainClient/KeychainClient.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import Foundation
33

44
protocol KeychainClient: AnyObject {
55
var encryptionKey: SymmetricKey? { get }
6+
var didInitializeKeychainData: Bool { get }
67

8+
func getEncryptionKey() throws
79
func getQueryResults(item: KeychainItem) throws -> [KeychainQueryResult]
810
func valueExistsForItem(item: KeychainItem) -> Bool
911
func setValueForItem(value: KeychainItem.Value, item: KeychainItem) throws

Sources/StytchCore/KeychainClient/KeychainClientImplementation.swift

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,39 @@ import Security
44
#if !os(tvOS)
55
import LocalAuthentication
66
#endif
7+
#if os(iOS)
8+
import UIKit
9+
#endif
710

811
let ENCRYPTEDUSERDEFAULTSKEYNAME = "EncryptedUserDefaultsKey"
912

1013
final class KeychainClientImplementation: KeychainClient {
1114
static let shared = KeychainClientImplementation()
1215
private let queue: DispatchQueue
1316
private let queueKey = DispatchSpecificKey<Void>()
14-
var encryptionKey: SymmetricKey?
17+
private var cachedEncryptionKey: SymmetricKey?
18+
var didInitializeKeychainData = false
19+
var encryptionKey: SymmetricKey? {
20+
(try? safelyEnqueue {
21+
if let cachedEncryptionKey {
22+
return cachedEncryptionKey
23+
}
24+
#if os(iOS)
25+
if UIApplication.shared.isProtectedDataAvailable {
26+
try? getEncryptionKey()
27+
didInitializeKeychainData = true
28+
} else {
29+
// For some reason, we are trying to read the encryption key before protected data became available
30+
// Log that this happened (which it hopefully won't?), but leave the behavior up to the caller (EncryptedUserDefaultsClient) to handle a missing key (throw an error)
31+
StytchConsoleLogger.error(message: "Attempted to read encryption key but UIApplication.shared.isProtectedDataAvailable was false")
32+
}
33+
#else
34+
try? getEncryptionKey()
35+
#endif
36+
return cachedEncryptionKey
37+
})
38+
}
39+
1540
private var isOnQueue: Bool {
1641
DispatchQueue.getSpecific(key: queueKey) != nil
1742
}
@@ -25,18 +50,11 @@ final class KeychainClientImplementation: KeychainClient {
2550
private init() {
2651
queue = DispatchQueue(label: "StytchKeychainClientQueue")
2752
queue.setSpecific(key: queueKey, value: ())
28-
loadEncryptionKey()
2953
#if !os(tvOS) && !os(watchOS)
3054
contextWithoutUI.interactionNotAllowed = true
3155
#endif
3256
}
3357

34-
func loadEncryptionKey() {
35-
try? safelyEnqueue {
36-
encryptionKey = try? getEncryptionKey()
37-
}
38-
}
39-
4058
func safelyEnqueue<T>(_ block: () throws -> T) throws -> T {
4159
if isOnQueue {
4260
return try block()
@@ -45,18 +63,19 @@ final class KeychainClientImplementation: KeychainClient {
4563
}
4664
}
4765

48-
private func getEncryptionKey() throws -> SymmetricKey {
66+
func getEncryptionKey() throws {
4967
try safelyEnqueue {
5068
let result = try getFirstQueryResult(KeychainItem.encryptionKey)
5169
guard let result else {
52-
// Key doesn't exist so create it
70+
// At this point, we know that protected data IS available, so if the keychain returned nil, then it means the key TRULY doesn't exist, and so we should create a new one
5371
let data = SymmetricKey(size: .bits256).withUnsafeBytes {
5472
Data(Array($0))
5573
}
5674
try setValueForItem(value: .init(data: data, account: ENCRYPTEDUSERDEFAULTSKEYNAME, label: nil, generic: nil, accessPolicy: nil), item: .encryptionKey)
57-
return SymmetricKey(data: data)
75+
cachedEncryptionKey = SymmetricKey(data: data)
76+
return
5877
}
59-
return SymmetricKey(data: result.data)
78+
cachedEncryptionKey = SymmetricKey(data: result.data)
6079
}
6180
}
6281

Sources/StytchCore/StytchClientCommon.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ extension StytchClientCommonInternal {
6969
}
7070
}
7171
}
72+
NotificationCenter.default.addObserver(forName: UIApplication.protectedDataDidBecomeAvailableNotification, object: nil, queue: nil) { _ in
73+
Task {
74+
if !Current.keychainClient.didInitializeKeychainData {
75+
try? Current.keychainClient.getEncryptionKey()
76+
}
77+
}
78+
}
7279
#endif
7380

7481
Task {

Tests/StytchCoreTests/KeychainClient+Mock.swift

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

88
class KeychainClientMock: KeychainClient {
9+
var didInitializeKeychainData: Bool = true
10+
11+
func getEncryptionKey() throws {
12+
// noop
13+
}
14+
915
var encryptionKey: SymmetricKey? {
1016
do {
1117
return SymmetricKey(data: try Current.cryptoClient.dataWithRandomBytesOfCount(256))

0 commit comments

Comments
 (0)