Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1612822
initial build
paulb777 Aug 20, 2026
1af23f2
warnings fixed
paulb777 Aug 20, 2026
a9b41b9
checkpoint: antigravity thinks it builds
paulb777 Aug 20, 2026
2132be6
Rename GAC
paulb777 Aug 20, 2026
0f7c337
tests build
paulb777 Aug 20, 2026
b019a29
Tests pass
paulb777 Aug 20, 2026
c4b7e9c
antigravity claiming victory
paulb777 Aug 20, 2026
6136fd4
Missing AppCheckCoreAppAttestProviderTests tests added
paulb777 Aug 20, 2026
7e45028
warnings and tests fixed according to agy
paulb777 Aug 20, 2026
7a93b7f
style
paulb777 Aug 20, 2026
3bcd072
copyrights
paulb777 Aug 20, 2026
cfaeba3
ObjC symbol fixes
paulb777 Aug 20, 2026
8eaba5d
FirebaseAppCheck unit tests pass
paulb777 Aug 20, 2026
bf61a5b
CocoaPods import and CocoaPods-specific test fixes
paulb777 Aug 20, 2026
8eb6bd0
CocoaPods test fixes
paulb777 Aug 20, 2026
ce216c8
fixes
paulb777 Aug 21, 2026
1094456
watchos fixes
paulb777 Aug 21, 2026
eb97cbf
fixes and style
paulb777 Aug 21, 2026
760417b
review and style
paulb777 Aug 21, 2026
28b16f5
Apply code review findings: Fix unnecessary async/sync overhead in Ap…
paulb777 Aug 21, 2026
3db6fb8
Apply code review findings: Embrace native Swift paradigms
paulb777 Aug 21, 2026
2098274
Apply code review findings: Fix critical data race in AppCheckCoreTok…
paulb777 Aug 21, 2026
db745e5
Fix AppCheckCore concurrency race in Keychain when testing
paulb777 Aug 21, 2026
1b743a7
Ignore errSecDuplicateItem during token cache write to fix test flakes
paulb777 Aug 21, 2026
89e505d
watchos should stay at 7
paulb777 Aug 21, 2026
fc4a7a0
Fix bridging crash for requestHooks arrays by accepting Any and mappi…
paulb777 Aug 21, 2026
fd62410
Address code review feedback for AppCheckCore rewrite
paulb777 Aug 21, 2026
81dd9c9
Address code review findings for AppAttestProvider and DeviceCheckPro…
paulb777 Aug 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 4 additions & 22 deletions AppCheckCore.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'AppCheckCore'
s.version = '11.3.1'
s.version = '12.0.0'
s.summary = 'App Check Core SDK.'

s.description = <<-DESC
Expand All @@ -17,7 +17,7 @@ Pod::Spec.new do |s|
}
s.social_media_url = 'https://twitter.com/Firebase'

ios_deployment_target = '12.0'
ios_deployment_target = '13.0'
osx_deployment_target = '10.15'
tvos_deployment_target = '13.0'
watchos_deployment_target = '7.0'
Expand All @@ -35,7 +35,7 @@ Pod::Spec.new do |s|
base_dir = "AppCheckCore/"

s.source_files = [
base_dir + 'Sources/**/*.[mh]',
base_dir + 'Sources/**/*.{h,m,swift}',
]
s.ios.source_files = [
'AppCheckRecaptchaProvider/Sources/**/*.swift',
Expand All @@ -45,9 +45,6 @@ Pod::Spec.new do |s|
s.ios.weak_framework = 'DeviceCheck'
s.osx.weak_framework = 'DeviceCheck'
s.tvos.weak_framework = 'DeviceCheck'

s.dependency 'PromisesObjC', '~> 2.4'
s.dependency 'PromisesSwift', '~> 2.4'
s.dependency 'GoogleUtilities/Environment', '~> 8.0'
s.dependency 'GoogleUtilities/UserDefaults', '~> 8.0'
s.ios.dependency 'RecaptchaInterop', '~> 101.0'
Expand All @@ -64,28 +61,13 @@ Pod::Spec.new do |s|
:tvos => tvos_deployment_target
}
unit_tests.source_files = [
base_dir + 'Tests/Unit/**/*.[mh]',
base_dir + 'Tests/Utils/**/*.[mh]',
base_dir + 'Tests/Unit/**/*.swift',
]

unit_tests.resources = base_dir + 'Tests/Fixture/**/*'
unit_tests.requires_app_host = true
end

s.test_spec 'integration' do |integration_tests|
integration_tests.platforms = {
:ios => ios_deployment_target,
:osx => osx_deployment_target,
:tvos => tvos_deployment_target
}
integration_tests.source_files = [
base_dir + 'Tests/Integration/**/*.[mh]',
base_dir + 'Tests/Integration/**/*.[mh]',
]
integration_tests.resources = base_dir + 'Tests/Fixture/**/*'
integration_tests.requires_app_host = true
end

s.test_spec 'swift-unit' do |swift_unit_tests|
swift_unit_tests.platforms = {
:ios => ios_deployment_target,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

private let kGenerateAppAttestChallengeEndpoint = "generateAppAttestChallenge"
private let kExchangeAppAttestAttestationEndpoint = "exchangeAppAttestAttestation"
private let kExchangeAppAttestAssertionEndpoint = "exchangeAppAttestAssertion"

private let kRequestFieldArtifact = "artifact"
private let kRequestFieldAssertion = "assertion"
private let kRequestFieldChallenge = "challenge"
private let kRequestFieldKeyID = "key_id"
private let kRequestFieldAttestation = "attestation_statement"
private let kRequestFieldLimitedUse = "limited_use"
private let kContentTypeKey = "Content-Type"
private let kJSONContentType = "application/json"
private let kHTTPMethodPost = "POST"

@objc(GACAppAttestAPIServiceProtocol)
public protocol AppCheckCoreAppAttestAPIServiceProtocol: NSObjectProtocol {
@objc func getRandomChallenge() async throws -> Data

@objc
func attestKey(withAttestation attestation: Data, keyID: String, challenge: Data,
limitedUse: Bool) async throws -> AppCheckCoreAppAttestAttestationResponse

@objc
func getAppCheckToken(withArtifact artifact: Data, challenge: Data, assertion: Data,
limitedUse: Bool) async throws -> AppCheckCoreToken
}

@objc(GACAppAttestAPIService)
public class AppCheckCoreAppAttestAPIService: NSObject, AppCheckCoreAppAttestAPIServiceProtocol {
private let apiService: AppCheckCoreAPIServiceProtocol
private let resourceName: String

@objc(initWithAPIService:resourceName:)
public init(apiService: AppCheckCoreAPIServiceProtocol, resourceName: String) {
self.apiService = apiService
self.resourceName = resourceName
super.init()
}

// MARK: - API Calls

@objc
public func getRandomChallenge() async throws -> Data {
let url = urlForEndpoint(kGenerateAppAttestChallengeEndpoint)
let response = try await apiService.sendRequest(
withURL: url,
httpMethod: kHTTPMethodPost,
body: nil,
additionalHeaders: nil
)
return try randomChallengeWithAPIResponse(response)
}

@objc
public func attestKey(withAttestation attestation: Data, keyID: String, challenge: Data,
limitedUse: Bool) async throws -> AppCheckCoreAppAttestAttestationResponse {
let url = urlForEndpoint(kExchangeAppAttestAttestationEndpoint)
let body = try httpBody(
withAttestation: attestation,
keyID: keyID,
challenge: challenge,
limitedUse: limitedUse
)

let urlResponse = try await apiService.sendRequest(
withURL: url,
httpMethod: kHTTPMethodPost,
body: body,
additionalHeaders: [kContentTypeKey: kJSONContentType]
)

guard let responseData = urlResponse.httpBody else {
throw AppCheckCoreErrorUtil.error(withFailureReason: "Invalid or missing response data.")
}
let response = try AppCheckCoreAppAttestAttestationResponse(
responseData: responseData,
requestDate: Date()
)

return response
}

@objc
public func getAppCheckToken(withArtifact artifact: Data, challenge: Data, assertion: Data,
limitedUse: Bool) async throws -> AppCheckCoreToken {
let url = urlForEndpoint(kExchangeAppAttestAssertionEndpoint)
let body = try httpBody(
withArtifact: artifact,
challenge: challenge,
assertion: assertion,
limitedUse: limitedUse
)

let urlResponse = try await apiService.sendRequest(
withURL: url,
httpMethod: kHTTPMethodPost,
body: body,
additionalHeaders: [kContentTypeKey: kJSONContentType]
)

let token = try await apiService.appCheckToken(withAPIResponse: urlResponse)
return token
}

// MARK: - Challenge parsing

private func randomChallengeWithAPIResponse(_ response: AppCheckCoreURLSessionDataResponse) throws
-> Data {
guard let responseData = response.httpBody else {
throw AppCheckCoreErrorUtil.error(withFailureReason: "Empty server response body.")
}

if responseData.isEmpty {
throw AppCheckCoreErrorUtil.error(withFailureReason: "Empty server response body.")
}

guard let responseDict = try? JSONSerialization
.jsonObject(with: responseData, options: []) as? [String: Any] else {
throw AppCheckCoreErrorUtil.jsonSerializationError(NSError(
domain: NSCocoaErrorDomain,
code: 0,
userInfo: nil
))
}

guard let challengeBase64 = responseDict["challenge"] as? String else {
throw AppCheckCoreErrorUtil.appCheckTokenResponseError(withMissingField: "challenge")
}

guard let challenge = Data(base64Encoded: challengeBase64) else {
throw AppCheckCoreErrorUtil.error(withFailureReason: "Invalid base64 string for challenge.")
}

return challenge
}

// MARK: - Body Builders

private func httpBody(withAttestation attestation: Data, keyID: String, challenge: Data,
limitedUse: Bool) throws -> Data {
if attestation.isEmpty || keyID.isEmpty || challenge.isEmpty {
throw AppCheckCoreErrorUtil.error(withFailureReason: "Missing or empty request parameter.")
}

let jsonObject: [String: Any] = [
kRequestFieldKeyID: keyID,
kRequestFieldAttestation: attestation.base64EncodedString(),
kRequestFieldChallenge: challenge.base64EncodedString(),
kRequestFieldLimitedUse: limitedUse,
]

return try httpBody(withJSONObject: jsonObject)
}

private func httpBody(withArtifact artifact: Data, challenge: Data, assertion: Data,
limitedUse: Bool) throws -> Data {
if artifact.isEmpty || challenge.isEmpty || assertion.isEmpty {
throw AppCheckCoreErrorUtil.error(withFailureReason: "Missing or empty request parameter.")
}

let jsonObject: [String: Any] = [
kRequestFieldArtifact: artifact.base64EncodedString(),
kRequestFieldChallenge: challenge.base64EncodedString(),
kRequestFieldAssertion: assertion.base64EncodedString(),
kRequestFieldLimitedUse: limitedUse,
]

return try httpBody(withJSONObject: jsonObject)
}

private func httpBody(withJSONObject jsonObject: Any) throws -> Data {
do {
return try JSONSerialization.data(withJSONObject: jsonObject, options: [])
} catch {
throw AppCheckCoreErrorUtil.jsonSerializationError(error as NSError)
}
}

// MARK: - URL Helpers

private func urlForEndpoint(_ endpoint: String) -> URL {
let urlString = "\(apiService.baseURL)/\(resourceName):\(endpoint)"
return URL(string: urlString)!
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

private let kResponseFieldAppCheckTokenDict = "appCheckToken"
private let kResponseFieldArtifact = "artifact"

@objc(GACAppAttestAttestationResponse)
public class AppCheckCoreAppAttestAttestationResponse: NSObject {
@objc public let artifact: Data
@objc public let token: AppCheckCoreToken

@objc(initWithArtifact:token:)
public init(artifact: Data, token: AppCheckCoreToken) {
self.artifact = artifact
self.token = token
super.init()
}

@objc(initWithResponseData:requestDate:error:)
public init(responseData: Data, requestDate: Date) throws {
if responseData.isEmpty {
throw AppCheckCoreErrorUtil
.error(
withFailureReason: "Failed to parse the initial handshake response. Empty server response body."
)
}

let responseDict = try JSONSerialization
.jsonObject(with: responseData, options: []) as? [String: Any]

guard let responseDict = responseDict else {
throw AppCheckCoreErrorUtil.jsonSerializationError(NSError(
domain: NSCocoaErrorDomain,
code: 0,
userInfo: nil
))
}

guard let artifactBase64String = responseDict[kResponseFieldArtifact] as? String,
let artifactData = Data(base64Encoded: artifactBase64String) else {
throw AppCheckCoreErrorUtil
.appAttestAttestationResponseError(withMissingField: kResponseFieldArtifact)
}

guard let appCheckTokenDict = responseDict[kResponseFieldAppCheckTokenDict] as? [String: Any]
else {
throw AppCheckCoreErrorUtil
.appAttestAttestationResponseError(withMissingField: kResponseFieldAppCheckTokenDict)
}

let appCheckToken = try AppCheckCoreToken(
responseDict: appCheckTokenDict,
requestDate: requestDate
)

artifact = artifactData
token = appCheckToken
super.init()
}
}
Loading
Loading