Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 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
7abb6b9
Fix hardcoded Error Domain and code in AppAttestRejectionError
paulb777 Aug 22, 2026
ac9774a
Fix string interpolation in RecaptchaTokenGenerator
paulb777 Aug 22, 2026
21be62d
Remove leftover Promises imports from unit tests
paulb777 Aug 22, 2026
8463a0f
Bump to iOS 15 etc
paulb777 Sep 4, 2026
c8d78bb
fix(storage): restore legacy userdefaults keys and suites for v11 com…
ncooke3 Sep 11, 2026
7ac8730
test(storage): verify v11 storage compatibility with binary fixtures …
ncooke3 Sep 14, 2026
f87e6c1
refactor(interop): audit and replace objcMembers with granular objc a…
ncooke3 Sep 15, 2026
20ce274
refactor(storage): mark storage types final and add isysroot to fixtu…
ncooke3 Sep 15, 2026
c0a8566
fix: port google/app-check#113 to swift (#121)
ncooke3 Sep 16, 2026
4c49af0
test(api): add Objective-C API build test and verify Swift parity (#120)
ncooke3 Sep 16, 2026
bf6cea4
build: bump minimum macOS and watchOS deployment targets (#122)
ncooke3 Sep 16, 2026
0775b8f
build: bump swift-tools-version to 6.0 and clean up visionOS workarou…
ncooke3 Sep 16, 2026
d8f3e74
style: format Package.swift with style.sh
ncooke3 Sep 16, 2026
a3ce962
merge main into pb-swift (#124)
ncooke3 Sep 18, 2026
95e2554
Merge branch 'main' into pb-swift
ncooke3 Sep 18, 2026
9d5eaf1
Nc.swift.review parity (#125)
ncooke3 Sep 22, 2026
25510ac
Fix: Replace #if !NDEBUG with #if DEBUG in AppCheckCoreAPIService
paulb777 Sep 22, 2026
f84acc2
Fix: Add fallback for URLSession.data(for:) on macOS 11
paulb777 Sep 22, 2026
5bbe081
Fix: Add CustomNSError conformance to AppCheckCoreErrorCode and updat…
paulb777 Sep 22, 2026
4824864
Fix: Introduce SafeContinuation to prevent trapping when third-party …
paulb777 Sep 22, 2026
ec3dfde
Fix: Restore AppCheckCore integration tests and include ObjC API test…
paulb777 Sep 22, 2026
b78f085
Fix: Replace Task.sleep with XCTestExpectation in AppCheckCoreTests t…
paulb777 Sep 22, 2026
3af92af
Fix: Correct SafeContinuation type parameters and mark as Sendable
paulb777 Sep 22, 2026
cb07668
new test fix
paulb777 Sep 22, 2026
a4759e4
Fix: Reduce public API surface (M7) by removing public modifiers from…
paulb777 Sep 22, 2026
79be899
Fix: Avoid SIGBUS crash in tests by providing a valid HTTPURLResponse…
paulb777 Sep 22, 2026
bda1a6b
Fix: Address M6 by converting AppCheckCoreBackoffWrapper to a generic…
paulb777 Sep 22, 2026
ca6a2cf
Fix: Address M3 by extracting the main-thread delivery helper to appl…
paulb777 Sep 22, 2026
4b93df6
Feat: Leverage 'package' visibility to replace @_spi(FirebaseInternal…
paulb777 Sep 22, 2026
d068bca
style
paulb777 Sep 22, 2026
d8f8147
Fix: Route ad-hoc error domains through AppCheckCoreErrorUtil
paulb777 Sep 22, 2026
fded7ee
Address a few nits
paulb777 Sep 22, 2026
392b70e
Refactor: Remove unnecessary 'async' from appCheckToken(withAPIRespon…
paulb777 Sep 22, 2026
1b6142e
build fixes
paulb777 Sep 22, 2026
f60acf9
Fix: Address M4 residuals, CocoaPods Concurrency build errors, and ma…
paulb777 Sep 22, 2026
d54f872
Fix: Replace force unwrapped URL with safe error throwing in AppAttes…
paulb777 Sep 22, 2026
aa4f740
Fix: Demote AppCheckCoreStorage, backoff types, TokenRefreshResult, a…
paulb777 Sep 22, 2026
f872172
Fix: Restore strongly-typed AppCheckCoreAPIRequestHook in Provider ar…
paulb777 Sep 22, 2026
968afec
Fix: Mark AppCheckCoreTokenResult as @unchecked Sendable and enforce …
paulb777 Sep 22, 2026
af0bc65
Package to public to make CocoaPods happy
paulb777 Sep 22, 2026
5080ca5
Fix: Recover ObjC blocks passed as [Any]? in requestHooks
paulb777 Sep 22, 2026
3153076
Refactor requestHooks bridging recovery: tightening and hermetic test
paulb777 Sep 23, 2026
2e26413
Refactor: Restore internal v11 ObjC names and pin APIService designat…
paulb777 Sep 23, 2026
d224ae1
chore: Swift migration wrap-up (v11 parity, CHANGELOG, public footprint)
paulb777 Sep 23, 2026
e521711
fix(requesthooks): cover reCAPTCHA and pin the NSBlock filter (#126)
ncooke3 Sep 23, 2026
5a901b0
fix(recaptcha): address review items for requestHooks bridging (#127)
ncooke3 Sep 23, 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
5 changes: 2 additions & 3 deletions .github/workflows/app_check_core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@ jobs:
pod_lib_lint:
strategy:
matrix:
# TODO: macos tests are blocked by https://github.com/erikdoe/ocmock/pull/532
target: [ios, tvos, macos --skip-tests, watchos]
target: [ios, tvos, macos, watchos]
os: [macos-15, macos-26]
include:
- os: macos-15
xcode: Xcode_16.4
xcode: Xcode_26.2
- os: macos-26
xcode: Xcode_26.2
runs-on: ${{ matrix.os }}
Expand Down
20 changes: 15 additions & 5 deletions .github/workflows/spm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,8 @@ jobs:
strategy:
matrix:
os: [macos-15]
xcode: [Xcode_16.4]
xcode: [Xcode_26.2]
platform: [iOS, tvOS, macOS, catalyst]
include:
- os: macos-14
xcode: Xcode_16.2
platform: iOS
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
Expand All @@ -36,3 +32,17 @@ jobs:
run: xcodebuild -list
- name: iOS Unit Tests
run: scripts/third_party/travis/retry.sh scripts/build.sh AppCheck-Package ${{ matrix.platform }} spm

# SwiftPM emits Swift diagnostics but still exits 0, so warnings scroll past
# unnoticed in the job above. `pod lib lint` escalates the same warnings to
# fatal, which previously made CocoaPods the only gate able to reject them --
# a ~5 minute round-trip, and an accidental dependency rather than a designed
# one. This job makes the cheap gate as strict as the expensive one.
warnings-as-errors:
runs-on: macos-15
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Xcode
run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
- name: Build sources and tests with warnings as errors
run: swift build --build-tests -Xswiftc -warnings-as-errors
25 changes: 11 additions & 14 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.2'
s.version = '12.0.0'
s.summary = 'App Check Core SDK.'

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

ios_deployment_target = '12.0'
osx_deployment_target = '10.15'
tvos_deployment_target = '13.0'
watchos_deployment_target = '7.0'
ios_deployment_target = '15.0'
osx_deployment_target = '11.0'
tvos_deployment_target = '15.0'
watchos_deployment_target = '8.0'

s.swift_version = '5.5'
s.swift_version = '5.9'

s.ios.deployment_target = ios_deployment_target
s.osx.deployment_target = osx_deployment_target
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,8 +61,8 @@ 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',
base_dir + 'Tests/Unit/ObjC/**/*.m',
]

unit_tests.resources = base_dir + 'Tests/Fixture/**/*'
Expand All @@ -79,13 +76,13 @@ Pod::Spec.new do |s|
:tvos => tvos_deployment_target
}
integration_tests.source_files = [
base_dir + 'Tests/Integration/**/*.[mh]',
base_dir + 'Tests/Integration/**/*.[mh]',
base_dir + 'Tests/Integration/**/*.swift',
]
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,196 @@
// 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"

protocol AppCheckCoreAppAttestAPIServiceProtocol: NSObjectProtocol {
func getRandomChallenge() async throws -> Data

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

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

class AppCheckCoreAppAttestAPIService: NSObject, AppCheckCoreAppAttestAPIServiceProtocol {
private let apiService: AppCheckCoreAPIServiceProtocol
private let resourceName: String

init(apiService: AppCheckCoreAPIServiceProtocol, resourceName: String) {
self.apiService = apiService
self.resourceName = resourceName
super.init()
}

// MARK: - API Calls

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

func attestKey(withAttestation attestation: Data, keyID: String, challenge: Data,
limitedUse: Bool) async throws -> AppCheckCoreAppAttestAttestationResponse {
let url = try 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: urlResponse.requestDate
)

return response
}

func getAppCheckToken(withArtifact artifact: Data, challenge: Data, assertion: Data,
limitedUse: Bool) async throws -> AppCheckCoreToken {
let url = try 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 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) throws -> URL {
let urlString = "\(apiService.baseURL)/\(resourceName):\(endpoint)"
guard let url = URL(string: urlString) else {
throw AppCheckCoreErrorUtil.error(withFailureReason: "Invalid URL.")
}
return url
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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"

class AppCheckCoreAppAttestAttestationResponse: NSObject {
let artifact: Data
let token: AppCheckCoreToken

init(artifact: Data, token: AppCheckCoreToken) {
self.artifact = artifact
self.token = token
super.init()
}

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