chromium/device/fido/mac/fake_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 <vector>

#import <AuthenticationServices/AuthenticationServices.h>
#import <objc/message.h>

#include "base/functional/callback.h"
#include "base/strings/sys_string_conversions.h"
#include "device/fido/fido_parsing_utils.h"
#include "device/fido/mac/fake_icloud_keychain_sys.h"

// A number of AuthenticationServices objects are subclassed so that the
// values of readonly properties can be overridden in tests.

API_AVAILABLE(macos(13.3))
@interface FakeBrowserPlatformPublicKeyCredential
    : ASAuthorizationWebBrowserPlatformPublicKeyCredential
@property(nonatomic, copy) NSData* credentialID;
@property(nonatomic, copy) NSString* name;
@property(nonatomic, copy) NSString* relyingParty;
@property(nonatomic, copy) NSData* userHandle;
@end

@implementation FakeBrowserPlatformPublicKeyCredential
@synthesize credentialID = _credentialID;
@synthesize name = _name;
@synthesize relyingParty = _relyingParty;
@synthesize userHandle = _userHandle;
@end

API_AVAILABLE(macos(13.3))
@interface FakeASAuthorization : ASAuthorization
@property(nonatomic, strong) id<ASAuthorizationCredential> credential;
@end

@implementation FakeASAuthorization
@synthesize credential = _credential;
@end

API_AVAILABLE(macos(13.3))
@interface FakeASAuthorizationPublicKeyCredentialRegistration
    : NSObject <ASAuthorizationPublicKeyCredentialRegistration>
@property(nonatomic, copy) NSData* rawAttestationObject;
@property(nonatomic, copy) NSData* rawClientDataJSON;
@property(nonatomic, copy) NSData* credentialID;
@end

@implementation FakeASAuthorizationPublicKeyCredentialRegistration
@synthesize rawAttestationObject = _rawAttestationObject;
@synthesize rawClientDataJSON = _rawClientDataJSON;
@synthesize credentialID = _credentialID;

+ (BOOL)supportsSecureCoding {
  return NO;
}

- (instancetype)initWithCoder:(NSCoder*)aDecoder {
  NOTREACHED_IN_MIGRATION();
  return self;
}

- (void)encodeWithCoder:(NSCoder*)aCoder {
  NOTREACHED_IN_MIGRATION();
}

- (id)copyWithZone:(NSZone*)zone {
  NOTREACHED_IN_MIGRATION();
  return self;
}
@end

API_AVAILABLE(macos(13.3))
@interface FakeASAuthorizationPublicKeyCredentialAssertion
    : NSObject <ASAuthorizationPublicKeyCredentialAssertion>
@property(nonatomic, copy) NSData* rawAuthenticatorData;
@property(nonatomic, copy) NSData* signature;
@property(nonatomic, copy) NSData* userID;
@property(nonatomic, copy) NSData* credentialID;
@property(nonatomic, copy) NSData* rawClientDataJSON;
@end

@implementation FakeASAuthorizationPublicKeyCredentialAssertion
@synthesize rawAuthenticatorData = _rawAuthenticatorData;
@synthesize signature = _signature;
@synthesize userID = _userID;
@synthesize credentialID = _credentialID;
@synthesize rawClientDataJSON = _rawClientDataJSON;

+ (BOOL)supportsSecureCoding {
  return NO;
}

- (instancetype)initWithCoder:(NSCoder*)aDecoder {
  NOTREACHED_IN_MIGRATION();
  return self;
}

- (void)encodeWithCoder:(NSCoder*)aCoder {
  NOTREACHED_IN_MIGRATION();
}

- (id)copyWithZone:(NSZone*)zone {
  NOTREACHED_IN_MIGRATION();
  return self;
}
@end

namespace device::fido::icloud_keychain {

namespace {
NSData* ToNSData(base::span<const uint8_t> data) {
  return [NSData dataWithBytes:data.data() length:data.size()];
}
}  // namespace

FakeSystemInterface::FakeSystemInterface() = default;

void FakeSystemInterface::set_auth_state(AuthState auth_state) {
  auth_state_ = auth_state;
}

void FakeSystemInterface::set_next_auth_state(AuthState next_auth_state) {
  next_auth_state_ = next_auth_state;
}

void FakeSystemInterface::SetMakeCredentialResult(
    base::span<const uint8_t> attestation_object_bytes,
    base::span<const uint8_t> credential_id) {
  CHECK(!make_credential_error_code_);
  make_credential_attestation_object_bytes_ =
      fido_parsing_utils::Materialize(attestation_object_bytes);
  make_credential_credential_id_ =
      fido_parsing_utils::Materialize(credential_id);
}

void FakeSystemInterface::SetMakeCredentialError(int code) {
  CHECK(!make_credential_attestation_object_bytes_);
  make_credential_error_code_ = code;
}

void FakeSystemInterface::SetGetAssertionResult(
    base::span<const uint8_t> authenticator_data,
    base::span<const uint8_t> signature,
    base::span<const uint8_t> user_id,
    base::span<const uint8_t> credential_id) {
  get_assertion_authenticator_data_ =
      fido_parsing_utils::Materialize(authenticator_data);
  get_assertion_signature_ = fido_parsing_utils::Materialize(signature);
  get_assertion_user_id_ = fido_parsing_utils::Materialize(user_id);
  get_assertion_credential_id_ = fido_parsing_utils::Materialize(credential_id);
}

void FakeSystemInterface::SetGetAssertionError(int code, std::string msg) {
  get_assertion_error_ = std::make_pair(code, std::move(msg));
}

void FakeSystemInterface::SetCredentials(
    std::vector<DiscoverableCredentialMetadata> creds) {
  creds_ = std::move(creds);
}

bool FakeSystemInterface::IsAvailable() const {
  return true;
}

SystemInterface::AuthState FakeSystemInterface::GetAuthState() {
  return auth_state_;
}

void FakeSystemInterface::AuthorizeAndContinue(
    base::OnceCallback<void()> callback) {
  CHECK(next_auth_state_.has_value());
  auth_state_ = *next_auth_state_;
  std::move(callback).Run();
}

void FakeSystemInterface::GetPlatformCredentials(
    const std::string& rp_id,
    void (^block)(
        NSArray<ASAuthorizationWebBrowserPlatformPublicKeyCredential*>*)) {
  NSMutableArray<ASAuthorizationWebBrowserPlatformPublicKeyCredential*>* ret =
      [[NSMutableArray alloc] init];
  for (const auto& cred_values : creds_) {
    // `init` on `ASAuthorizationWebBrowserPlatformPublicKeyCredential` is
    // marked as `NS_UNAVAILABLE` and so is not called.
    FakeBrowserPlatformPublicKeyCredential* cred =
        [FakeBrowserPlatformPublicKeyCredential alloc];
    cred.credentialID = ToNSData(cred_values.cred_id);
    cred.userHandle = ToNSData(cred_values.user.id);
    cred.relyingParty = base::SysUTF8ToNSString(rp_id);
    cred.name = base::SysUTF8ToNSString(cred_values.user.name.value_or(""));
    [ret addObject:cred];
  }

  block(ret);
}

void FakeSystemInterface::MakeCredential(
    NSWindow* window,
    CtapMakeCredentialRequest request,
    base::OnceCallback<void(ASAuthorization*, NSError*)> callback) {
  auto attestation_object_bytes =
      std::move(make_credential_attestation_object_bytes_);
  make_credential_attestation_object_bytes_.reset();
  auto credential_id = std::move(make_credential_credential_id_);
  make_credential_credential_id_.reset();
  auto error_code = std::move(make_credential_error_code_);
  make_credential_error_code_.reset();

  if (error_code) {
    std::move(callback).Run(nullptr,
                            [[NSError alloc] initWithDomain:@"WKErrorDomain"
                                                       code:error_code.value()
                                                   userInfo:nullptr]);
  } else if (!attestation_object_bytes) {
    // Error code 1001 is a arbitrary number that doesn't trigger any special
    // processing.
    std::move(callback).Run(nullptr, [[NSError alloc] initWithDomain:@""
                                                                code:1001
                                                            userInfo:nullptr]);
  } else {
    FakeASAuthorizationPublicKeyCredentialRegistration* result =
        [[FakeASAuthorizationPublicKeyCredentialRegistration alloc] init];
    result.rawAttestationObject = ToNSData(*attestation_object_bytes);
    result.credentialID = ToNSData(*credential_id);

    FakeASAuthorization* authorization = [FakeASAuthorization alloc];
    authorization.credential = result;

    std::move(callback).Run(authorization, nullptr);
  }
}

void FakeSystemInterface::GetAssertion(
    NSWindow* window,
    CtapGetAssertionRequest request,
    base::OnceCallback<void(ASAuthorization*, NSError*)> callback) {
  if (get_assertion_error_) {
    NSError* error = [[NSError alloc]
        initWithDomain:@""
                  code:get_assertion_error_->first
              userInfo:@{
                NSLocalizedDescriptionKey : base::SysUTF8ToNSString(
                    get_assertion_error_->second.c_str())
              }];
    get_assertion_error_.reset();
    std::move(callback).Run(nullptr, error);
    return;
  }

  if (!get_assertion_authenticator_data_) {
    std::move(callback).Run(nullptr, [[NSError alloc] initWithDomain:@""
                                                                code:1001
                                                            userInfo:nullptr]);
    return;
  }

  FakeASAuthorizationPublicKeyCredentialAssertion* result =
      [[FakeASAuthorizationPublicKeyCredentialAssertion alloc] init];
  result.rawAuthenticatorData = ToNSData(*get_assertion_authenticator_data_);
  result.signature = ToNSData(*get_assertion_signature_);
  result.userID = ToNSData(*get_assertion_user_id_);
  result.credentialID = ToNSData(*get_assertion_credential_id_);

  get_assertion_authenticator_data_.reset();
  get_assertion_signature_.reset();
  get_assertion_user_id_.reset();
  get_assertion_credential_id_.reset();

  FakeASAuthorization* authorization = [FakeASAuthorization alloc];
  authorization.credential = result;

  std::move(callback).Run(authorization, nullptr);
}

void FakeSystemInterface::Cancel() {
  cancel_count_++;
}

FakeSystemInterface::~FakeSystemInterface() = default;

}  // namespace device::fido::icloud_keychain