diff --git a/AppCheckCore/Sources/AppAttestProvider/Storage/AppCheckCoreAppAttestKeyIDStorage.swift b/AppCheckCore/Sources/AppAttestProvider/Storage/AppCheckCoreAppAttestKeyIDStorage.swift index 3045f3d..cc05726 100644 --- a/AppCheckCore/Sources/AppAttestProvider/Storage/AppCheckCoreAppAttestKeyIDStorage.swift +++ b/AppCheckCore/Sources/AppAttestProvider/Storage/AppCheckCoreAppAttestKeyIDStorage.swift @@ -25,18 +25,21 @@ public protocol AppCheckCoreAppAttestKeyIDStorageProtocol: NSObjectProtocol { @objc func getAppAttestKeyID() async throws -> String? } -private let kKeyIDStorageDefaultsSuiteName = "com.firebase.AppCheckCoreAppAttestKeyIDStorage" - @objc(GACAppAttestKeyIDStorage) public class AppCheckCoreAppAttestKeyIDStorage: NSObject, AppCheckCoreAppAttestKeyIDStorageProtocol { + /// Suite name for the app attest key ID in UserDefaults. + /// Do not rename: retains the "GAC" prefix for compatibility with existing stored data from v11 + /// or lower. + static let keyIDStorageDefaultsSuiteName = "com.firebase.GACAppAttestKeyIDStorage" + private let keySuffix: String private let userDefaults: GULUserDefaults @objc(initWithKeySuffix:) public init(keySuffix: String) { self.keySuffix = keySuffix - userDefaults = GULUserDefaults(suiteName: kKeyIDStorageDefaultsSuiteName) + userDefaults = GULUserDefaults(suiteName: Self.keyIDStorageDefaultsSuiteName) super.init() } diff --git a/AppCheckCore/Sources/DebugProvider/AppCheckCoreDebugProvider.swift b/AppCheckCore/Sources/DebugProvider/AppCheckCoreDebugProvider.swift index e7b9c8e..11edb74 100644 --- a/AppCheckCore/Sources/DebugProvider/AppCheckCoreDebugProvider.swift +++ b/AppCheckCore/Sources/DebugProvider/AppCheckCoreDebugProvider.swift @@ -25,12 +25,20 @@ import Foundation private let kDebugTokenEnvKey = "AppCheckDebugToken" private let kFirebaseDebugTokenEnvKey = "FIRAAppCheckDebugToken" -private let kDebugTokenUserDefaultsKey = "AppCheckCoreDebugToken" -private let kDebugTokenRegisteredUserDefaultsKey = "AppCheckCoreDebugTokenRegistered" @objc(GACAppCheckDebugProvider) @objcMembers public class AppCheckCoreDebugProvider: NSObject, AppCheckCoreProvider { + /// Storage key for the debug token in UserDefaults. + /// Do not rename: retains the "GAC" prefix for compatibility with existing stored data from v11 + /// or lower. + static let debugTokenUserDefaultsKey = "GACAppCheckDebugToken" + + /// Storage key prefix for registered debug tokens in UserDefaults. + /// Do not rename: retains the "GAC" prefix for compatibility with existing stored data from v11 + /// or lower. + static let debugTokenRegisteredUserDefaultsKeyPrefix = "GACAppCheckDebugTokenRegistered" + private let apiService: AppCheckCoreDebugProviderAPIServiceProtocol private let debugTokenEnvValue: String? private let registeredUserDefaultsKey: String @@ -156,11 +164,11 @@ public class AppCheckCoreDebugProvider: NSObject, AppCheckCoreProvider { } private static func localDebugToken() -> String { - if let token = GULUserDefaults.standard().string(forKey: kDebugTokenUserDefaultsKey) { + if let token = GULUserDefaults.standard().string(forKey: debugTokenUserDefaultsKey) { return token } else { let token = UUID().uuidString - GULUserDefaults.standard().setObject(token, forKey: kDebugTokenUserDefaultsKey) + GULUserDefaults.standard().setObject(token, forKey: debugTokenUserDefaultsKey) return token } } @@ -172,7 +180,7 @@ public class AppCheckCoreDebugProvider: NSObject, AppCheckCoreProvider { if safeResourceName.isEmpty { safeResourceName = "default" } - return "\(kDebugTokenRegisteredUserDefaultsKey)_\(safeServiceName)_\(safeResourceName)" + return "\(debugTokenRegisteredUserDefaultsKeyPrefix)_\(safeServiceName)_\(safeResourceName)" } private static func environmentVariableDebugToken(registeredUserDefaultsKey: String, diff --git a/AppCheckCore/Tests/Unit/AppAttestProvider/Storage/AppCheckCoreAppAttestKeyIDStorageTests.swift b/AppCheckCore/Tests/Unit/AppAttestProvider/Storage/AppCheckCoreAppAttestKeyIDStorageTests.swift index f207d49..4fcaefd 100644 --- a/AppCheckCore/Tests/Unit/AppAttestProvider/Storage/AppCheckCoreAppAttestKeyIDStorageTests.swift +++ b/AppCheckCore/Tests/Unit/AppAttestProvider/Storage/AppCheckCoreAppAttestKeyIDStorageTests.swift @@ -54,6 +54,46 @@ class AppCheckCoreAppAttestKeyIDStorageTests: XCTestCase { XCTAssertNil(setKeyID) } + // IMMUTABLE TEST: Do not edit, update, or remove under any circumstance. + // This test locks in an immutable backwards compatibility contract with App Check 11. + // If this test fails, revert changes to the implementation instead of modifying this test. + func testKeyIDStorageDefaultsSuiteName_MatchesContract() { + XCTAssertEqual( + AppCheckCoreAppAttestKeyIDStorage.keyIDStorageDefaultsSuiteName, + "com.firebase.GACAppAttestKeyIDStorage", + "Suite name mismatch breaks App Check 11 migration compatibility." + ) + } + + func testGetAppAttestKeyID_CompatibilityWithLegacyGACStorage() async throws { + let legacySuiteName = AppCheckCoreAppAttestKeyIDStorage.keyIDStorageDefaultsSuiteName + let legacyDefaults = try XCTUnwrap(UserDefaults(suiteName: legacySuiteName)) + let legacyKey = "app_attest_keyID.\(keySuffix!)" + let expectedKeyID = "legacy_test_key_id" + legacyDefaults.set(expectedKeyID, forKey: legacyKey) + defer { + legacyDefaults.removeObject(forKey: legacyKey) + } + + let retrievedKeyID = try await storage.getAppAttestKeyID() + XCTAssertEqual(retrievedKeyID, expectedKeyID) + } + + func testSetAppAttestKeyID_CompatibilityWithLegacyGACStorage() async throws { + let legacySuiteName = AppCheckCoreAppAttestKeyIDStorage.keyIDStorageDefaultsSuiteName + let legacyDefaults = try XCTUnwrap(UserDefaults(suiteName: legacySuiteName)) + let legacyKey = "app_attest_keyID.\(keySuffix!)" + let newKeyID = "new_test_key_id" + defer { + legacyDefaults.removeObject(forKey: legacyKey) + } + + _ = try await storage.setAppAttestKeyID(newKeyID) + + let storedValue = legacyDefaults.string(forKey: legacyKey) + XCTAssertEqual(storedValue, newKeyID) + } + func testGetAppAttestKeyID_WhenAppAttestKeyIDNotFoundError() async { do { _ = try await storage.getAppAttestKeyID() diff --git a/AppCheckCore/Tests/Unit/DebugProvider/AppCheckCoreDebugProviderTests.swift b/AppCheckCore/Tests/Unit/DebugProvider/AppCheckCoreDebugProviderTests.swift index 329e5dc..9c8e056 100644 --- a/AppCheckCore/Tests/Unit/DebugProvider/AppCheckCoreDebugProviderTests.swift +++ b/AppCheckCore/Tests/Unit/DebugProvider/AppCheckCoreDebugProviderTests.swift @@ -47,8 +47,9 @@ class MockAppCheckDebugProviderAPIService: NSObject, AppCheckCoreDebugProviderAP class AppCheckCoreDebugProviderTests: XCTestCase { let kDebugTokenEnvKey = "AppCheckDebugToken" let kFirebaseDebugTokenEnvKey = "FIRAAppCheckDebugToken" - let kDebugTokenUserDefaultsKey = "AppCheckCoreDebugToken" - let kDebugTokenRegisteredUserDefaultsKey = "AppCheckCoreDebugTokenRegistered" + let kDebugTokenUserDefaultsKey = AppCheckCoreDebugProvider.debugTokenUserDefaultsKey + let kDebugTokenRegisteredUserDefaultsKey = AppCheckCoreDebugProvider + .debugTokenRegisteredUserDefaultsKeyPrefix var provider: AppCheckCoreDebugProvider! var fakeAPIService: MockAppCheckDebugProviderAPIService! @@ -71,6 +72,67 @@ class AppCheckCoreDebugProviderTests: XCTestCase { // MARK: - Debug token generating/storing + // IMMUTABLE TEST: Do not edit, update, or remove under any circumstance. + // This test locks in an immutable backwards compatibility contract with App Check 11. + // If this test fails, revert changes to the implementation instead of modifying this test. + func testDebugTokenUserDefaultsKeys_MatchContract() { + XCTAssertEqual( + AppCheckCoreDebugProvider.debugTokenUserDefaultsKey, + "GACAppCheckDebugToken", + "Debug token key mismatch breaks App Check 11 migration compatibility." + ) + XCTAssertEqual( + AppCheckCoreDebugProvider.debugTokenRegisteredUserDefaultsKeyPrefix, + "GACAppCheckDebugTokenRegistered", + "Registered token key prefix mismatch breaks App Check 11 migration compatibility." + ) + } + + func testLegacyDebugTokenCompatibility() { + let legacyKey = kDebugTokenUserDefaultsKey + let legacyToken = "legacy_stored_debug_token" + UserDefaults.standard.set(legacyToken, forKey: legacyKey) + defer { + UserDefaults.standard.removeObject(forKey: legacyKey) + } + + let debugProvider = AppCheckCoreDebugProvider( + apiService: fakeAPIService, + serviceName: "test-service", + resourceName: "projects/test-project/apps/test-app", + environment: [:] + ) + + XCTAssertEqual( + debugProvider.currentDebugToken(), + legacyToken, + "Debug token mismatch breaks App Check 11 migration compatibility." + ) + } + + func testSetDebugToken_CompatibilityWithLegacyGACStorage() { + let legacyKey = kDebugTokenUserDefaultsKey + UserDefaults.standard.removeObject(forKey: legacyKey) + defer { + UserDefaults.standard.removeObject(forKey: legacyKey) + } + + let debugProvider = AppCheckCoreDebugProvider( + apiService: fakeAPIService, + serviceName: "test-service", + resourceName: "projects/test-project/apps/test-app", + environment: [:] + ) + + let generatedToken = debugProvider.currentDebugToken() + let storedLegacyValue = UserDefaults.standard.string(forKey: legacyKey) + XCTAssertEqual( + storedLegacyValue, + generatedToken, + "Generated debug token was not stored under legacy key." + ) + } + func testCurrentTokenWhenEnvironmentVariableSetAndTokenStored() { UserDefaults.standard.set("stored token", forKey: kDebugTokenUserDefaultsKey) let envToken = "env token"