910e62b5创建于 1月15日历史提交
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/common/credential_provider/passkey_keychain_provider_bridge.h"

#import "base/apple/foundation_util.h"
#import "base/containers/to_vector.h"
#import "base/functional/callback.h"
#import "components/sync/protocol/webauthn_credential_specifics.pb.h"
#import "components/webauthn/core/browser/passkey_model_utils.h"
#import "ios/chrome/common/credential_provider/archivable_credential+passkey.h"

typedef void (^CheckEnrolledCompletionBlock)(BOOL is_enrolled, NSError* error);
typedef void (^ErrorCompletionBlock)(NSError* error);
typedef void (^FetchKeysCompletionBlock)(
    const webauthn::SharedKeyList& key_list);

namespace {

// Returns an array of security domain secrets from the vault keys.
NSArray<NSData*>* GetSecurityDomainSecret(const webauthn::SharedKeyList keys) {
  NSMutableArray<NSData*>* security_domain_secrets =
      [NSMutableArray arrayWithCapacity:keys.size()];
  for (const auto& key : keys) {
    [security_domain_secrets addObject:[NSData dataWithBytes:key.data()
                                                      length:key.size()]];
  }
  return security_domain_secrets;
}

// Returns whether there's at least one valid key in the keys array.
bool ContainsValidKey(const webauthn::SharedKeyList keys,
                      id<Credential> credential) {
  for (NSData* security_domain_secret in GetSecurityDomainSecret(keys)) {
    sync_pb::WebauthnCredentialSpecifics_Encrypted credential_secrets;
    if (webauthn::passkey_model_utils::DecryptWebauthnCredentialSpecificsData(
            base::ToVector(base::apple::NSDataToSpan(security_domain_secret)),
            PasskeyFromCredential(credential), &credential_secrets)) {
      return true;
    }
  }

  return false;
}

}  // namespace

@implementation PasskeyKeychainProviderBridge {
  // Provider that manages passkey vault keys.
  std::unique_ptr<PasskeyKeychainProvider> _passkeyKeychainProvider;

  // Navigation controller needed by `_passkeyKeychainProvider` to display some
  // UI to the user.
  UINavigationController* _navigationController;

  // The branded navigation item title view to use in the navigation
  // controller's UIs.
  UIView* _navigationItemTitleView;
}

- (instancetype)initWithEnableLogging:(BOOL)enableLogging
                 navigationController:
                     (UINavigationController*)navigationController
              navigationItemTitleView:(UIView*)navigationItemTitleView {
  self = [super init];
  if (self) {
    _passkeyKeychainProvider =
        std::make_unique<PasskeyKeychainProvider>(enableLogging);
    _navigationController = navigationController;
    _navigationItemTitleView = navigationItemTitleView;
  }
  return self;
}

- (void)dealloc {
  if (_passkeyKeychainProvider) {
    _passkeyKeychainProvider.reset();
  }
}

- (void)
    fetchSecurityDomainSecretForGaia:(NSString*)gaia
                          credential:(id<Credential>)credential
                             purpose:(webauthn::ReauthenticatePurpose)purpose
                          completion:(FetchSecurityDomainSecretCompletionBlock)
                                         fetchSecurityDomainSecretCompletion {
  if (_navigationController) {
    __weak __typeof(self) weakSelf = self;
    auto checkEnrolledCompletion = ^(BOOL is_enrolled, NSError* error) {
      [weakSelf onIsEnrolledForGaia:gaia
                         credential:credential
                            purpose:purpose
                         completion:fetchSecurityDomainSecretCompletion
                         isEnrolled:is_enrolled
                              error:error];
    };
    [self checkEnrolledForGaia:gaia completion:checkEnrolledCompletion];
  } else {
    // If there's no valid navigation controller to show the enrollment UI, it
    // won't be possible to enroll, so only attempt to fetch keys.
    [self fetchKeysForGaia:gaia
                credential:credential
        canMarkKeysAsStale:YES
                   purpose:purpose
         canReauthenticate:YES
                completion:fetchSecurityDomainSecretCompletion
                     error:nil];
  }
}

#pragma mark - Private

// Marks the security domain secret vault keys as stale and calls the completion
// block.
- (void)markKeysAsStaleForGaia:(NSString*)gaia
                    completion:(ProceduralBlock)completion {
  _passkeyKeychainProvider->MarkKeysAsStale(gaia, base::BindOnce(^() {
                                              completion();
                                            }));
}

// Checks if the account associated with the provided gaia ID is enrolled and
// calls the completion block.
- (void)checkEnrolledForGaia:(NSString*)gaia
                  completion:(CheckEnrolledCompletionBlock)completion {
  _passkeyKeychainProvider->CheckEnrolled(
      gaia, base::BindOnce(^(BOOL is_enrolled, NSError* error) {
        completion(is_enrolled, error);
      }));
}

// Handles the enrollment status of the account associated with the provided
// gaia ID. If enrolled, fetches the keys for that account. If not, enrolls the
// account.
- (void)onIsEnrolledForGaia:(NSString*)gaia
                 credential:(id<Credential>)credential
                    purpose:(webauthn::ReauthenticatePurpose)purpose
                 completion:(FetchSecurityDomainSecretCompletionBlock)
                                fetchSecurityDomainSecretCompletion
                 isEnrolled:(BOOL)isEnrolled
                      error:(NSError*)error {
  if (isEnrolled) {
    if (error != nil) {
      // Skip fetching keys if there was an error.
      fetchSecurityDomainSecretCompletion(nil);
      return;
    }

    [self fetchKeysForGaia:gaia
                credential:credential
        canMarkKeysAsStale:YES
                   purpose:purpose
         canReauthenticate:YES
                completion:fetchSecurityDomainSecretCompletion
                     error:nil];
  } else {
    __weak __typeof(self) weakSelf = self;
    auto enrollCompletion = ^(NSError* enroll_error) {
      [weakSelf fetchKeysForGaia:gaia
                      credential:credential
              canMarkKeysAsStale:YES
                         purpose:purpose
               canReauthenticate:NO
                      completion:fetchSecurityDomainSecretCompletion
                           error:enroll_error];
    };
    [self.delegate showEnrollmentWelcomeScreen:^{
      [weakSelf enrollForGaia:gaia completion:enrollCompletion];
    }];
  }
}

// Starts the enrollment process for the account associated with the provided
// gaia ID and calls the completion block.
- (void)enrollForGaia:(NSString*)gaia
           completion:(ErrorCompletionBlock)completion {
  _passkeyKeychainProvider->Enroll(gaia, _navigationController,
                                   _navigationItemTitleView,
                                   base::BindOnce(^(NSError* error) {
                                     completion(error);
                                   }));
}

// Attempts to fetch the keys for the account associated with the provided gaia
// ID if no error occurred at the previous stage. `canReauthenticate` indicates
// whether the user can be asked to reauthenticate by entering their GPM PIN.
// This argument should only be set to `NO` if the user has already been asked
// to reauthenticate.
- (void)fetchKeysForGaia:(NSString*)gaia
              credential:(id<Credential>)credential
      canMarkKeysAsStale:(BOOL)canMarkKeysAsStale
                 purpose:(webauthn::ReauthenticatePurpose)purpose
       canReauthenticate:(BOOL)canReauthenticate
              completion:(FetchSecurityDomainSecretCompletionBlock)
                             fetchSecurityDomainSecretCompletion
                   error:(NSError*)error {
  if (error != nil) {
    // Skip fetching keys if there was an error.
    fetchSecurityDomainSecretCompletion(nil);
    return;
  }

  __weak __typeof(self) weakSelf = self;
  auto fetchKeysCompletion = ^(const webauthn::SharedKeyList& key_list) {
    [weakSelf onKeysFetchedForGaia:gaia
                        credential:credential
                canMarkKeysAsStale:canMarkKeysAsStale
                           purpose:purpose
                        completion:fetchSecurityDomainSecretCompletion
                           keyList:key_list
                 canReauthenticate:canReauthenticate];
  };
  [self fetchKeysForGaia:gaia purpose:purpose completion:fetchKeysCompletion];
}

// Fetches the security domain secret vault keys for the account associated with
// the provided gaia ID and calls the completion block.
- (void)fetchKeysForGaia:(NSString*)gaia
                 purpose:(webauthn::ReauthenticatePurpose)purpose
              completion:(FetchKeysCompletionBlock)completion {
  _passkeyKeychainProvider->FetchKeys(
      gaia, purpose, base::BindOnce(^(const webauthn::SharedKeyList& key_list) {
        completion(key_list);
      }));
}

// Handles the outcome of the key fetch process.
// If `sharedKeys` is empty, triggers the reauthentication process.
// If not, triggers the `completion`.
- (void)onKeysFetchedForGaia:(NSString*)gaia
                  credential:(id<Credential>)credential
          canMarkKeysAsStale:(BOOL)canMarkKeysAsStale
                     purpose:(webauthn::ReauthenticatePurpose)purpose
                  completion:
                      (FetchSecurityDomainSecretCompletionBlock)completion
                     keyList:(const webauthn::SharedKeyList&)keyList
           canReauthenticate:(BOOL)canReauthenticate {
  __weak __typeof(self) weakSelf = self;
  if (!keyList.empty()) {
    if (purpose == webauthn::ReauthenticatePurpose::kDecrypt &&
        canMarkKeysAsStale && credential &&
        !ContainsValidKey(keyList, credential)) {
      // Mark keys as stale and try again. `canMarkKeysAsStale` is set to `NO`
      // to avoid getting into an infinite loop.
      [self markKeysAsStaleForGaia:gaia
                        completion:^() {
                          [weakSelf fetchKeysForGaia:gaia
                                          credential:credential
                                  canMarkKeysAsStale:NO
                                             purpose:purpose
                                   canReauthenticate:canReauthenticate
                                          completion:completion
                                               error:nil];
                        }];
      return;
    }

    const webauthn::SharedKeyList keys = std::move(keyList);
    // On success, check degraded recoverability.
    auto degradedRecoverabilityCompletion = ^(NSError* error) {
      if (error) {
        completion(nil);
      } else {
        [weakSelf
            performUserVerificationIfNeededAndCallCompletionWithKeys:std::move(
                                                                         keys)
                                                          completion:
                                                              completion];
      }
    };
    [self checkDegradedRecoverabilityForGaia:gaia
                                  completion:degradedRecoverabilityCompletion];
  } else {
    if (_navigationController && canReauthenticate) {
      // A valid navigation controller is needed to show the reauthentication
      // UI. Otherwise, it won't be possible to perform reauthentication.
      [self.delegate showReauthenticationWelcomeScreen:^{
        [weakSelf reauthenticateForGaia:gaia
                             credential:credential
                     canMarkKeysAsStale:canMarkKeysAsStale
                                purpose:purpose
                             completion:completion];
      }];
    } else {
      completion(nil);
    }
  }
}

// Starts the reauthentication process for the account associated with the
// provided gaia ID and calls the completion block.
- (void)reauthenticateForGaia:(NSString*)gaia
                   credential:(id<Credential>)credential
           canMarkKeysAsStale:(BOOL)canMarkKeysAsStale
                      purpose:(webauthn::ReauthenticatePurpose)purpose
                   completion:
                       (FetchSecurityDomainSecretCompletionBlock)completion {
  __weak __typeof(self) weakSelf = self;
  _passkeyKeychainProvider->Reauthenticate(
      gaia, _navigationController, _navigationItemTitleView, purpose,
      base::BindOnce(^(const webauthn::SharedKeyList& key_list) {
        // If we got nonempty keys, that means the reauthentication was a
        // success. Report this back to the delegate.
        if (!key_list.empty()) {
          [weakSelf.delegate providerDidCompleteReauthentication];
        }

        [weakSelf onKeysFetchedForGaia:gaia
                            credential:credential
                    canMarkKeysAsStale:canMarkKeysAsStale
                               purpose:purpose
                            completion:completion
                               keyList:key_list
                     canReauthenticate:NO];
      }));
}

// Checks if the account associated with the provided gaia ID is in degraded
// recoverability and calls the completion block.
- (void)checkDegradedRecoverabilityForGaia:(NSString*)gaia
                                completion:(ErrorCompletionBlock)completion {
  __weak __typeof(self) weakSelf = self;
  _passkeyKeychainProvider->CheckDegradedRecoverability(
      gaia, base::BindOnce(^(BOOL inDegradedRecoverability, NSError* error) {
        if (weakSelf.navigationController && inDegradedRecoverability) {
          // A valid navigation controller is needed to show the "fix degraded
          // recoverability state" UI. Otherwise, it won't be possible to
          // perform the GPM pin creation required to fix the degraded
          // recoverability state.
          [weakSelf.delegate showFixDegradedRecoverabilityWelcomeScreen:^{
            [weakSelf fixDegradedRecoverabilityForGaia:gaia
                                            completion:completion];
          }];
        } else {
          completion(error);
        }
      }));
}

// Fixes the degraded recoverability state for the account associated with the
// provided gaia ID and calls the completion block.
- (void)fixDegradedRecoverabilityForGaia:(NSString*)gaia
                              completion:(ErrorCompletionBlock)completion {
  _passkeyKeychainProvider->FixDegradedRecoverability(
      gaia, _navigationController, _navigationItemTitleView,
      base::BindOnce(^(NSError* error) {
        completion(error);
      }));
}

// Private accessor for the `_navigationController` ivar.
- (UINavigationController*)navigationController {
  return _navigationController;
}

// Asks the delegate to perform a user verification if needed and calls the
// completion block.
- (void)
    performUserVerificationIfNeededAndCallCompletionWithKeys:
        (const webauthn::SharedKeyList)keys
                                                  completion:
                                                      (FetchSecurityDomainSecretCompletionBlock)
                                                          completion {
  [self.delegate performUserVerificationIfNeeded:^{
    completion(GetSecurityDomainSecret(std::move(keys)));
  }];
}

@end