FIRAppAttestAPIService.m 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. /*
  2. * Copyright 2021 Google LLC
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAPIService.h"
  17. #if __has_include(<FBLPromises/FBLPromises.h>)
  18. #import <FBLPromises/FBLPromises.h>
  19. #else
  20. #import "FBLPromises.h"
  21. #endif
  22. #import "FirebaseAppCheck/Sources/AppAttestProvider/API/FIRAppAttestAttestationResponse.h"
  23. #import "FirebaseAppCheck/Sources/Core/APIService/FIRAppCheckAPIService.h"
  24. #import <GoogleUtilities/GULURLSessionDataResponse.h>
  25. #import "FirebaseAppCheck/Sources/Core/Errors/FIRAppCheckErrorUtil.h"
  26. NS_ASSUME_NONNULL_BEGIN
  27. static NSString *const kRequestFieldArtifact = @"artifact";
  28. static NSString *const kRequestFieldAssertion = @"assertion";
  29. static NSString *const kRequestFieldAttestation = @"attestation_statement";
  30. static NSString *const kRequestFieldChallenge = @"challenge";
  31. static NSString *const kRequestFieldKeyID = @"key_id";
  32. static NSString *const kExchangeAppAttestAssertionEndpoint = @"exchangeAppAttestAssertion";
  33. static NSString *const kExchangeAppAttestAttestationEndpoint = @"exchangeAppAttestAttestation";
  34. static NSString *const kGenerateAppAttestChallengeEndpoint = @"generateAppAttestChallenge";
  35. static NSString *const kContentTypeKey = @"Content-Type";
  36. static NSString *const kJSONContentType = @"application/json";
  37. static NSString *const kHTTPMethodPost = @"POST";
  38. @interface FIRAppAttestAPIService ()
  39. @property(nonatomic, readonly) id<FIRAppCheckAPIServiceProtocol> APIService;
  40. @property(nonatomic, readonly) NSString *projectID;
  41. @property(nonatomic, readonly) NSString *appID;
  42. @end
  43. @implementation FIRAppAttestAPIService
  44. - (instancetype)initWithAPIService:(id<FIRAppCheckAPIServiceProtocol>)APIService
  45. projectID:(NSString *)projectID
  46. appID:(NSString *)appID {
  47. self = [super init];
  48. if (self) {
  49. _APIService = APIService;
  50. _projectID = projectID;
  51. _appID = appID;
  52. }
  53. return self;
  54. }
  55. #pragma mark - Assertion request
  56. - (FBLPromise<FIRAppCheckToken *> *)getAppCheckTokenWithArtifact:(NSData *)artifact
  57. challenge:(NSData *)challenge
  58. assertion:(NSData *)assertion {
  59. NSURL *URL = [self URLForEndpoint:kExchangeAppAttestAssertionEndpoint];
  60. return [self HTTPBodyWithArtifact:artifact challenge:challenge assertion:assertion]
  61. .then(^FBLPromise<GULURLSessionDataResponse *> *(NSData *HTTPBody) {
  62. return [self.APIService sendRequestWithURL:URL
  63. HTTPMethod:kHTTPMethodPost
  64. body:HTTPBody
  65. additionalHeaders:@{kContentTypeKey : kJSONContentType}];
  66. })
  67. .then(^id _Nullable(GULURLSessionDataResponse *_Nullable response) {
  68. return [self.APIService appCheckTokenWithAPIResponse:response];
  69. });
  70. }
  71. #pragma mark - Random Challenge
  72. - (nonnull FBLPromise<NSData *> *)getRandomChallenge {
  73. NSURL *URL = [self URLForEndpoint:kGenerateAppAttestChallengeEndpoint];
  74. return [FBLPromise onQueue:[self backgroundQueue]
  75. do:^id _Nullable {
  76. return [self.APIService sendRequestWithURL:URL
  77. HTTPMethod:kHTTPMethodPost
  78. body:nil
  79. additionalHeaders:nil];
  80. }]
  81. .then(^id _Nullable(GULURLSessionDataResponse *_Nullable response) {
  82. return [self randomChallengeWithAPIResponse:response];
  83. });
  84. }
  85. #pragma mark - Challenge response parsing
  86. - (FBLPromise<NSData *> *)randomChallengeWithAPIResponse:(GULURLSessionDataResponse *)response {
  87. return [FBLPromise onQueue:[self backgroundQueue]
  88. do:^id _Nullable {
  89. NSError *error;
  90. NSData *randomChallenge =
  91. [self randomChallengeFromResponseBody:response.HTTPBody
  92. error:&error];
  93. return randomChallenge ?: error;
  94. }];
  95. }
  96. - (nullable NSData *)randomChallengeFromResponseBody:(NSData *)response error:(NSError **)outError {
  97. if (response.length <= 0) {
  98. FIRAppCheckSetErrorToPointer(
  99. [FIRAppCheckErrorUtil errorWithFailureReason:@"Empty server response body."], outError);
  100. return nil;
  101. }
  102. NSError *JSONError;
  103. NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:response
  104. options:0
  105. error:&JSONError];
  106. if (![responseDict isKindOfClass:[NSDictionary class]]) {
  107. FIRAppCheckSetErrorToPointer([FIRAppCheckErrorUtil JSONSerializationError:JSONError], outError);
  108. return nil;
  109. }
  110. NSString *challenge = responseDict[@"challenge"];
  111. if (![challenge isKindOfClass:[NSString class]]) {
  112. FIRAppCheckSetErrorToPointer(
  113. [FIRAppCheckErrorUtil appCheckTokenResponseErrorWithMissingField:@"challenge"], outError);
  114. return nil;
  115. }
  116. NSData *randomChallenge = [[NSData alloc] initWithBase64EncodedString:challenge options:0];
  117. return randomChallenge;
  118. }
  119. #pragma mark - Attestation request
  120. - (FBLPromise<FIRAppAttestAttestationResponse *> *)attestKeyWithAttestation:(NSData *)attestation
  121. keyID:(NSString *)keyID
  122. challenge:(NSData *)challenge {
  123. NSURL *URL = [self URLForEndpoint:kExchangeAppAttestAttestationEndpoint];
  124. return [self HTTPBodyWithAttestation:attestation keyID:keyID challenge:challenge]
  125. .then(^FBLPromise<GULURLSessionDataResponse *> *(NSData *HTTPBody) {
  126. return [self.APIService sendRequestWithURL:URL
  127. HTTPMethod:kHTTPMethodPost
  128. body:HTTPBody
  129. additionalHeaders:@{kContentTypeKey : kJSONContentType}];
  130. })
  131. .thenOn(
  132. [self backgroundQueue], ^id _Nullable(GULURLSessionDataResponse *_Nullable URLResponse) {
  133. NSError *error;
  134. __auto_type response =
  135. [[FIRAppAttestAttestationResponse alloc] initWithResponseData:URLResponse.HTTPBody
  136. requestDate:[NSDate date]
  137. error:&error];
  138. return response ?: error;
  139. });
  140. }
  141. #pragma mark - Request HTTP Body
  142. - (FBLPromise<NSData *> *)HTTPBodyWithArtifact:(NSData *)artifact
  143. challenge:(NSData *)challenge
  144. assertion:(NSData *)assertion {
  145. if (artifact.length <= 0 || challenge.length <= 0 || assertion.length <= 0) {
  146. FBLPromise *rejectedPromise = [FBLPromise pendingPromise];
  147. [rejectedPromise reject:[FIRAppCheckErrorUtil
  148. errorWithFailureReason:@"Missing or empty request parameter."]];
  149. return rejectedPromise;
  150. }
  151. return [FBLPromise onQueue:[self backgroundQueue]
  152. do:^id {
  153. id JSONObject = @{
  154. kRequestFieldArtifact : [self base64StringWithData:artifact],
  155. kRequestFieldChallenge : [self base64StringWithData:challenge],
  156. kRequestFieldAssertion : [self base64StringWithData:assertion]
  157. };
  158. return [self HTTPBodyWithJSONObject:JSONObject];
  159. }];
  160. }
  161. - (FBLPromise<NSData *> *)HTTPBodyWithAttestation:(NSData *)attestation
  162. keyID:(NSString *)keyID
  163. challenge:(NSData *)challenge {
  164. if (attestation.length <= 0 || keyID.length <= 0 || challenge.length <= 0) {
  165. FBLPromise *rejectedPromise = [FBLPromise pendingPromise];
  166. [rejectedPromise reject:[FIRAppCheckErrorUtil
  167. errorWithFailureReason:@"Missing or empty request parameter."]];
  168. return rejectedPromise;
  169. }
  170. return [FBLPromise onQueue:[self backgroundQueue]
  171. do:^id {
  172. id JSONObject = @{
  173. kRequestFieldKeyID : keyID,
  174. kRequestFieldAttestation : [self base64StringWithData:attestation],
  175. kRequestFieldChallenge : [self base64StringWithData:challenge]
  176. };
  177. return [self HTTPBodyWithJSONObject:JSONObject];
  178. }];
  179. }
  180. - (FBLPromise<NSData *> *)HTTPBodyWithJSONObject:(nonnull id)JSONObject {
  181. NSError *encodingError;
  182. NSData *payloadJSON = [NSJSONSerialization dataWithJSONObject:JSONObject
  183. options:0
  184. error:&encodingError];
  185. FBLPromise<NSData *> *HTTPBodyPromise = [FBLPromise pendingPromise];
  186. if (payloadJSON) {
  187. [HTTPBodyPromise fulfill:payloadJSON];
  188. } else {
  189. [HTTPBodyPromise reject:[FIRAppCheckErrorUtil JSONSerializationError:encodingError]];
  190. }
  191. return HTTPBodyPromise;
  192. }
  193. #pragma mark - Helpers
  194. - (NSString *)base64StringWithData:(NSData *)data {
  195. return [data base64EncodedStringWithOptions:0];
  196. }
  197. - (NSURL *)URLForEndpoint:(NSString *)endpoint {
  198. NSString *URL = [[self class] URLWithBaseURL:self.APIService.baseURL
  199. projectID:self.projectID
  200. appID:self.appID];
  201. return [NSURL URLWithString:[NSString stringWithFormat:@"%@:%@", URL, endpoint]];
  202. }
  203. + (NSString *)URLWithBaseURL:(NSString *)baseURL
  204. projectID:(NSString *)projectID
  205. appID:(NSString *)appID {
  206. return [NSString stringWithFormat:@"%@/projects/%@/apps/%@", baseURL, projectID, appID];
  207. }
  208. - (dispatch_queue_t)backgroundQueue {
  209. return dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
  210. }
  211. @end
  212. NS_ASSUME_NONNULL_END