chromium/device/fido/mac/icloud_keychain_sys.mm

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "device/fido/mac/icloud_keychain_sys.h"

#import <AuthenticationServices/AuthenticationServices.h>

#include "base/functional/callback.h"
#include "base/memory/scoped_refptr.h"
#include "base/no_destructor.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "components/device_event_log/device_event_log.h"
#include "device/fido/mac/icloud_keychain_internals.h"

namespace {

// This function is needed by the interfaces below, but interfaces must be
// in the top-level namespace.
NSData* ToNSData(base::span<const uint8_t> data) {
  return [NSData dataWithBytes:data.data() length:data.size()];
}

}  // namespace

// ICloudKeychainPresentationDelegate simply returns an `NSWindow` when asked by
// an `ASAuthorizationController`.
API_AVAILABLE(macos(13.3))
@interface ICloudKeychainPresentationDelegate
    : NSObject <ASAuthorizationControllerPresentationContextProviding>
@property(nonatomic, strong) NSWindow* window;
@end

@implementation ICloudKeychainPresentationDelegate
@synthesize window = _window;

- (ASPresentationAnchor)presentationAnchorForAuthorizationController:
    (ASAuthorizationController*)controller {
  return _window;
}
@end

@interface ASAuthorizationPlatformPublicKeyCredentialAssertionRequest (Extras)
@property(nonatomic) BOOL shouldShowHybridTransport;
@end

// ICloudKeychainDelegate receives callbacks when an `ASAuthorizationController`
// operation completes (successfully or otherwise) and bridges to a
// `OnceCallback`.
API_AVAILABLE(macos(13.3))
@interface ICloudKeychainDelegate : NSObject <ASAuthorizationControllerDelegate>
- (void)setCallback:
    (base::OnceCallback<void(ASAuthorization*, NSError*)>)callback;
- (void)setCleanupCallback:(base::OnceClosure)callback;
@end

@implementation ICloudKeychainDelegate {
  base::OnceCallback<void(ASAuthorization*, NSError*)> _callback;
  base::OnceClosure _cleanupCallback;
}

- (void)setCallback:
    (base::OnceCallback<void(ASAuthorization*, NSError*)>)callback {
  _callback = std::move(callback);
}

- (void)setCleanupCallback:(base::OnceClosure)callback {
  _cleanupCallback = std::move(callback);
}

- (void)authorizationController:(ASAuthorizationController*)controller
    didCompleteWithAuthorization:(ASAuthorization*)authorization {
  std::move(_callback).Run(authorization, nullptr);
  std::move(_cleanupCallback).Run();
}

- (void)authorizationController:(ASAuthorizationController*)controller
           didCompleteWithError:(NSError*)error {
  std::move(_callback).Run(nullptr, error);
  std::move(_cleanupCallback).Run();
}
@end

// ICloudKeychainCreateController overrides `_requestContextWithRequests` from
// `ASAuthorizationController` so that various extra parameters, which browsers
// need to set, can be specified.
API_AVAILABLE(macos(13.3))
@interface ICloudKeychainCreateController : ASAuthorizationController
@end

@implementation ICloudKeychainCreateController {
  std::optional<device::CtapMakeCredentialRequest> request_;
}

- (void)setRequest:(device::CtapMakeCredentialRequest)request {
  request_ = std::move(request);
}

- (id<ASCCredentialRequestContext>)
    _requestContextWithRequests:(NSArray<ASAuthorizationRequest*>*)requests
                          error:(NSError**)outError {
  id<ASCCredentialRequestContext> context =
      [super _requestContextWithRequests:requests error:outError];

  id<ASCPublicKeyCredentialCreationOptions> registrationOptions =
      context.platformKeyCredentialCreationOptions;
  registrationOptions.clientDataHash = ToNSData(request_->client_data_hash);
  registrationOptions.challenge = nil;

  NSMutableArray<NSNumber*>* supported_algos = [[NSMutableArray alloc] init];
  for (const device::PublicKeyCredentialParams::CredentialInfo& param :
       request_->public_key_credential_params.public_key_credential_params()) {
    if (param.type == device::CredentialType::kPublicKey) {
      [supported_algos addObject:[NSNumber numberWithInt:base::strict_cast<int>(
                                                             param.algorithm)]];
    }
  }
  if ([supported_algos count] > 0) {
    registrationOptions.supportedAlgorithmIdentifiers = supported_algos;
  }

  registrationOptions.shouldRequireResidentKey =
      request_->resident_key_required;

  const Class descriptor_class =
      NSClassFromString(@"ASCPublicKeyCredentialDescriptor");
  NSMutableArray<ASCPublicKeyCredentialDescriptor*>* exclude_list =
      [[NSMutableArray alloc] init];
  for (const auto& cred : request_->exclude_list) {
    if (cred.credential_type != device::CredentialType::kPublicKey) {
      continue;
    }
    NSMutableArray<NSString*>* transports = [[NSMutableArray alloc] init];
    for (const auto transport : cred.transports) {
      [transports addObject:base::SysUTF8ToNSString(ToString(transport))];
    }
    ASCPublicKeyCredentialDescriptor* descriptor =
        [[descriptor_class alloc] initWithCredentialID:ToNSData(cred.id)
                                            transports:transports];
    [exclude_list addObject:descriptor];
  }
  if ([exclude_list count] > 0) {
    registrationOptions.excludedCredentials = exclude_list;
  }

  return context;
}
@end

// ICloudKeychainGetController overrides `_requestContextWithRequests` from
// `ASAuthorizationController` so that various extra parameters, which browsers
// need to set, can be specified.
API_AVAILABLE(macos(13.3))
@interface ICloudKeychainGetController : ASAuthorizationController
@end

@implementation ICloudKeychainGetController {
  std::optional<device::CtapGetAssertionRequest> request_;
}

- (void)setRequest:(device::CtapGetAssertionRequest)request {
  request_ = std::move(request);
}

- (id<ASCCredentialRequestContext>)
    _requestContextWithRequests:(NSArray<ASAuthorizationRequest*>*)requests
                          error:(NSError**)outError {
  id<ASCCredentialRequestContext> context =
      [super _requestContextWithRequests:requests error:outError];

  id<ASCPublicKeyCredentialAssertionOptions> assertionOptions =
      context.platformKeyCredentialAssertionOptions;
  assertionOptions.clientDataHash = ToNSData(request_->client_data_hash);
  context.platformKeyCredentialAssertionOptions =
      [assertionOptions copyWithZone:nil];
  return context;
}
@end

namespace device::fido::icloud_keychain {
namespace {

API_AVAILABLE(macos(13.3))
ASAuthorizationWebBrowserPublicKeyCredentialManager* GetManager() {
  return [[ASAuthorizationWebBrowserPublicKeyCredentialManager alloc] init];
}

bool ProcessHasEntitlement() {
  base::apple::ScopedCFTypeRef<SecTaskRef> task(SecTaskCreateFromSelf(nullptr));
  if (!task) {
    return false;
  }

  base::apple::ScopedCFTypeRef<CFTypeRef> entitlement_value_cftype(
      SecTaskCopyValueForEntitlement(
          task.get(),
          CFSTR("com.apple.developer.web-browser.public-key-credential"),
          nullptr));
  return !!entitlement_value_cftype;
}

API_AVAILABLE(macos(13.3))
ASAuthorizationPublicKeyCredentialAttestationKind Convert(
    AttestationConveyancePreference preference) {
  // If attestation is requested then the request immediately fails, so
  // all types are mapped to `none`.
  return ASAuthorizationPublicKeyCredentialAttestationKindNone;
}

API_AVAILABLE(macos(13.3))
ASAuthorizationPublicKeyCredentialUserVerificationPreference Convert(
    UserVerificationRequirement uv) {
  switch (uv) {
    case UserVerificationRequirement::kDiscouraged:
      return ASAuthorizationPublicKeyCredentialUserVerificationPreferenceDiscouraged;
    case UserVerificationRequirement::kPreferred:
      return ASAuthorizationPublicKeyCredentialUserVerificationPreferencePreferred;
    case UserVerificationRequirement::kRequired:
      return ASAuthorizationPublicKeyCredentialUserVerificationPreferenceRequired;
  }
}

class API_AVAILABLE(macos(13.3)) NativeSystemInterface
    : public SystemInterface {
 public:
  bool IsAvailable() const override {
    static bool available = ProcessHasEntitlement();
    return available;
  }

  AuthState GetAuthState() override {
    return GetManager().authorizationStateForPlatformCredentials;
  }

  void AuthorizeAndContinue(base::OnceCallback<void()> callback) override {
    auto task_runner = base::SequencedTaskRunner::GetCurrentDefault();
    __block auto internal_callback = std::move(callback);
    [GetManager()
        requestAuthorizationForPublicKeyCredentials:^(AuthState state) {
          task_runner->PostTask(FROM_HERE,
                                base::BindOnce(std::move(internal_callback)));
        }];
  }

  void GetPlatformCredentials(
      const std::string& rp_id,
      void (^handler)(
          NSArray<ASAuthorizationWebBrowserPlatformPublicKeyCredential*>*))
      override {
    [GetManager()
        platformCredentialsForRelyingParty:base::SysUTF8ToNSString(rp_id)
                         completionHandler:handler];
  }

  void MakeCredential(
      NSWindow* window,
      CtapMakeCredentialRequest request,
      base::OnceCallback<void(ASAuthorization*, NSError*)> callback) override {
    DCHECK(!create_controller_);
    DCHECK(!get_controller_);
    DCHECK(!delegate_);
    DCHECK(!presentation_delegate_);

    ASAuthorizationPlatformPublicKeyCredentialProvider* provider =
        [[ASAuthorizationPlatformPublicKeyCredentialProvider alloc]
            initWithRelyingPartyIdentifier:base::SysUTF8ToNSString(
                                               request.rp.id)];
    NSData* challenge = ToNSData(request.client_data_hash);
    NSData* user_id = ToNSData(request.user.id);
    NSString* name = base::SysUTF8ToNSString(request.user.name.value_or(""));
    ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest*
        create_request =
            [provider createCredentialRegistrationRequestWithChallenge:challenge
                                                                  name:name
                                                                userID:user_id];
    create_request.attestationPreference =
        Convert(request.attestation_preference);
    create_request.userVerificationPreference =
        Convert(request.user_verification);
    if (request.user.display_name) {
      create_request.displayName =
          base::SysUTF8ToNSString(*request.user.display_name);
    }

    create_controller_ = [[ICloudKeychainCreateController alloc]
        initWithAuthorizationRequests:@[ create_request ]];
    [create_controller_ setRequest:std::move(request)];
    delegate_ = [[ICloudKeychainDelegate alloc] init];
    [delegate_ setCallback:std::move(callback)];
    [delegate_ setCleanupCallback:base::BindOnce(
                                      &NativeSystemInterface::Cleanup, this)];
    create_controller_.delegate = delegate_;
    presentation_delegate_ = [[ICloudKeychainPresentationDelegate alloc] init];
    presentation_delegate_.window = window;
    create_controller_.presentationContextProvider = presentation_delegate_;

    [create_controller_ performRequests];
  }

  void GetAssertion(
      NSWindow* window,
      CtapGetAssertionRequest request,
      base::OnceCallback<void(ASAuthorization*, NSError*)> callback) override {
    DCHECK(!create_controller_);
    DCHECK(!get_controller_);
    DCHECK(!delegate_);
    DCHECK(!presentation_delegate_);

    ASAuthorizationPlatformPublicKeyCredentialProvider* provider =
        [[ASAuthorizationPlatformPublicKeyCredentialProvider alloc]
            initWithRelyingPartyIdentifier:base::SysUTF8ToNSString(
                                               request.rp_id)];

    NSData* challenge = ToNSData(request.client_data_hash);
    ASAuthorizationPlatformPublicKeyCredentialAssertionRequest* get_request =
        [provider createCredentialAssertionRequestWithChallenge:challenge];
    NSMutableArray* allowedCredentials = [[NSMutableArray alloc] init];
    for (const auto& cred : request.allow_list) {
      // All credentials are assumed to be platform credentials because we don't
      // wish to trigger macOS's handling of security keys.
      [allowedCredentials
          addObject:[[ASAuthorizationPlatformPublicKeyCredentialDescriptor
                        alloc] initWithCredentialID:ToNSData(cred.id)]];
    }
    get_request.allowedCredentials = allowedCredentials;
    [get_request setShouldShowHybridTransport:false];
    get_request.userVerificationPreference = Convert(request.user_verification);
    get_controller_ = [[ICloudKeychainGetController alloc]
        initWithAuthorizationRequests:@[ get_request ]];
    [get_controller_ setRequest:std::move(request)];
    delegate_ = [[ICloudKeychainDelegate alloc] init];
    [delegate_ setCallback:std::move(callback)];
    [delegate_ setCleanupCallback:base::BindOnce(
                                      &NativeSystemInterface::Cleanup, this)];
    get_controller_.delegate = delegate_;
    presentation_delegate_ = [[ICloudKeychainPresentationDelegate alloc] init];
    presentation_delegate_.window = window;
    get_controller_.presentationContextProvider = presentation_delegate_;

    [get_controller_ performRequests];
  }

  void Cancel() override {
    // Sending `cancel` will cause the controller to resolve the delegate with
    // an error. That will end up calling `Cleanup` to drop these references.
    if (create_controller_) {
      [create_controller_ cancel];
    }
    if (get_controller_) {
      [get_controller_ cancel];
    }
  }

 protected:
  ~NativeSystemInterface() override = default;

  void Cleanup() {
    create_controller_ = nullptr;
    get_controller_ = nullptr;
    delegate_ = nullptr;
    presentation_delegate_ = nullptr;
  }

  ICloudKeychainCreateController* __strong create_controller_;
  ICloudKeychainGetController* __strong get_controller_;
  ICloudKeychainDelegate* __strong delegate_;
  ICloudKeychainPresentationDelegate* __strong presentation_delegate_;
};

API_AVAILABLE(macos(13.3))
scoped_refptr<SystemInterface> GetNativeSystemInterface() {
  static base::NoDestructor<scoped_refptr<SystemInterface>>
      native_sys_interface(base::MakeRefCounted<NativeSystemInterface>());
  return *native_sys_interface;
}

API_AVAILABLE(macos(13.3))
scoped_refptr<SystemInterface>& GetTestInterface() {
  static base::NoDestructor<scoped_refptr<SystemInterface>> test_interface;
  return *test_interface;
}

}  // namespace

SystemInterface::~SystemInterface() = default;

API_AVAILABLE(macos(13.3))
scoped_refptr<SystemInterface> GetSystemInterface() {
  scoped_refptr<SystemInterface>& test_interface = GetTestInterface();
  if (test_interface) {
    return test_interface;
  }
  return GetNativeSystemInterface();
}

API_AVAILABLE(macos(13.3))
void SetSystemInterfaceForTesting(  // IN-TEST
    scoped_refptr<SystemInterface> sys_interface) {
  GetTestInterface() = sys_interface;
}

}  // namespace device::fido::icloud_keychain