From 16dd9f6eace51cf72bd894660b51ec7125636f3b Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 11 Sep 2026 16:54:23 -0400 Subject: [PATCH] fix(storage): restore legacy userdefaults keys and suites for v11 compatibility Restore GACAppAttestKeyIDStorage suite name and GACAppCheckDebugToken keys to prevent data loss on app upgrade from App Check 11 or lower. Add immutable contract assertion tests and roundtrip compatibility tests to prevent future regressions. --- .../AppCheckCoreAppAttestKeyIDStorage.swift | 9 ++- .../AppCheckCoreDebugProvider.swift | 18 +++-- ...pCheckCoreAppAttestKeyIDStorageTests.swift | 40 +++++++++++ .../AppCheckCoreDebugProviderTests.swift | 66 ++++++++++++++++++- 4 files changed, 123 insertions(+), 10 deletions(-) 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"