mirror of
https://github.com/google/app-check
synced 2026-11-09 22:05:52 +05:45
Signed-off-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Co-authored-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Co-authored-by: Nick Cooke <nickcooke@google.com>
197 lines
6.9 KiB
Swift
197 lines
6.9 KiB
Swift
// 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
|
|
}
|
|
}
|