// 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/credential_provider_extension/passkey_util.h"

#import "base/apple/foundation_util.h"
#import "base/containers/span.h"
#import "base/strings/string_number_conversions.h"
#import "base/strings/sys_string_conversions.h"
#import "base/time/time.h"
#import "components/sync/protocol/webauthn_credential_specifics.pb.h"
#import "components/webauthn/core/browser/gpm_user_verification_policy.h"
#import "components/webauthn/core/browser/passkey_model_utils.h"
#import "device/fido/fido_user_verification_requirement.h"
#import "device/fido/public/fido_types.h"
#import "ios/chrome/common/app_group/app_group_constants.h"
#import "ios/chrome/common/credential_provider/ASPasskeyCredentialIdentity+credential.h"
#import "ios/chrome/common/credential_provider/archivable_credential+passkey.h"
#import "ios/chrome/common/credential_provider/constants.h"
#import "ios/chrome/common/credential_provider/credential_provider_migration_notifier.h"
#import "ios/chrome/common/credential_provider/user_defaults_credential_store.h"

using base::SysNSStringToUTF8;

namespace {

// Appends "data" at the end of "container".
void Append(std::vector<uint8_t>& container, NSData* data) {
  container.append_range(base::apple::NSDataToSpan(data));
}

// Creates an ExtensionInputData structure from the prf inputs provided in the
// passkey request.
webauthn::passkey_model_utils::ExtensionInputData
ExtensionInputDataFromPRFInputs(NSArray<NSData*>* prf_inputs) {
  if ([prf_inputs count] > 0) {
    return webauthn::passkey_model_utils::ExtensionInputData(
        {base::apple::NSDataToSpan(prf_inputs[0]),
         ([prf_inputs count] > 1)
             ? std::optional<base::span<const uint8_t>>(
                   base::apple::NSDataToSpan(prf_inputs[1]))
             : std::nullopt});
  }
  return webauthn::passkey_model_utils::ExtensionInputData();
}

// Reads the PRF's result into 1 or 2 PRF outputs if the passkey request had
// provided PRF input data. Returns nil otherwise.
NSMutableArray<NSData*>* PRFOutputsFromExtensionOutputData(
    const webauthn::passkey_model_utils::ExtensionOutputData&
        extension_output_data) {
  static constexpr size_t kPRFOutputSize = 32u;

  auto span = base::span(extension_output_data.prf_result);

  // The PRF result can be empty, have exactly 1 output or exactly 2 outputs.
  CHECK_EQ(span.size() % kPRFOutputSize, 0u)
      << "Invalid PRF result size: " << span.size();
  CHECK_LE(span.size() / kPRFOutputSize, 2u)
      << "Invalid PRF result size: " << span.size();

  if (span.empty()) {
    return nil;
  }

  NSMutableArray<NSData*>* result = [NSMutableArray array];
  while (span.size() >= kPRFOutputSize) {
    auto [head, rest] = span.split_at<kPRFOutputSize>();

    [result addObject:[[NSData alloc] initWithBytes:head.data()
                                             length:head.size()]];

    span = rest;
  }

  return result;
}

// Wrapper around passkey_model_utils's MakeAuthenticatorDataForAssertion
// function.
NSData* MakeAuthenticatorDataForAssertion(NSString* rp_id,
                                          bool did_complete_uv) {
  std::vector<uint8_t> authenticator_data =
      webauthn::passkey_model_utils::MakeAuthenticatorDataForAssertion(
          SysNSStringToUTF8(rp_id), did_complete_uv);
  return [NSData dataWithBytes:authenticator_data.data()
                        length:authenticator_data.size()];
}

// Generates the signature during the passkey assertion process by decrypting
// the passkey using the trusted vault key and then using the decrypted
// passkey to call passkey_model_utils's GenerateEcSignature function.
NSData* GenerateSignature(NSData* authenticator_data,
                          NSData* client_data_hash,
                          const std::string& private_key) {
  // Prepare the signed data.
  std::vector<uint8_t> signed_over_data;
  Append(signed_over_data, authenticator_data);
  Append(signed_over_data, client_data_hash);

  // Compute signature.
  std::optional<std::vector<uint8_t>> signature =
      webauthn::passkey_model_utils::GenerateEcSignature(
          base::as_byte_span(private_key), signed_over_data);
  if (!signature) {
    return nil;
  }

  return [NSData dataWithBytes:signature->data() length:signature->size()];
}

void SaveToIdentityStore(id<Credential> credential,
                         ProceduralBlock completion) {
  auto stateCompletion = ^(ASCredentialIdentityStoreState* state) {
    if (!state.enabled) {
      completion();
      return;
    }

    NSArray<id<ASCredentialIdentity>>* storeIdentities =
        @[ [[ASPasskeyCredentialIdentity alloc]
            cr_initWithCredential:credential] ];
    void (^storeCompletion)(BOOL, NSError*) = ^(BOOL success, NSError* error) {
      completion();
    };

    if (credential.hidden) {
      [ASCredentialIdentityStore.sharedStore
          removeCredentialIdentityEntries:storeIdentities
                               completion:storeCompletion];
    } else {
      [ASCredentialIdentityStore.sharedStore
          saveCredentialIdentityEntries:storeIdentities
                             completion:storeCompletion];
    }
  };
  [ASCredentialIdentityStore.sharedStore
      getCredentialIdentityStoreStateWithCompletion:stateCompletion];
}

}  // namespace

std::optional<sync_pb::WebauthnCredentialSpecifics_Encrypted>
DecryptCredentialSecrets(id<Credential> credential,
                         webauthn::SharedKeyList trusted_vault_keys) {
  if (trusted_vault_keys.empty()) {
    return std::nullopt;
  }

  // Decrypt the private key using the trusted vault key.
  sync_pb::WebauthnCredentialSpecifics credential_specifics;
  if ([credential.privateKey length] > 0) {
    credential_specifics.set_private_key(credential.privateKey.bytes,
                                         credential.privateKey.length);
  } else if ([credential.encrypted length] > 0) {
    credential_specifics.set_encrypted(credential.encrypted.bytes,
                                       credential.encrypted.length);
  } else {
    return std::nullopt;
  }

  for (const webauthn::SharedKey& trusted_vault_key : trusted_vault_keys) {
    sync_pb::WebauthnCredentialSpecifics_Encrypted credential_secrets;
    if (webauthn::passkey_model_utils::DecryptWebauthnCredentialSpecificsData(
            trusted_vault_key, credential_specifics, &credential_secrets)) {
      return credential_secrets;
    }
  }

  return std::nullopt;
}

PasskeyCreationOutput PerformPasskeyCreation(
    NSData* client_data_hash,
    NSString* rp_id,
    NSString* user_name,
    NSData* user_handle,
    NSString* gaia,
    webauthn::SharedKeyList trusted_vault_keys,
    NSArray<NSData*>* prf_inputs,
    bool did_complete_uv) {
  if (trusted_vault_keys.empty()) {
    return {};
  }

  // Convert input arguments to std equivalents for use in functions below.
  std::vector<uint8_t> user_id;
  Append(user_id, user_handle);
  std::string rp_id_str = SysNSStringToUTF8(rp_id);
  std::string user_name_str = SysNSStringToUTF8(user_name);

  webauthn::passkey_model_utils::ExtensionInputData extension_input_data =
      ExtensionInputDataFromPRFInputs(prf_inputs);
  webauthn::passkey_model_utils::ExtensionOutputData extension_output_data;

  // Generate a key pair containing the webauthn specifics and the public key.
  auto [passkey, public_key_spki_der] =
      webauthn::passkey_model_utils::GeneratePasskeyAndEncryptSecrets(
          rp_id_str,
          webauthn::PasskeyModel::UserEntity(user_id, user_name_str,
                                             user_name_str),
          std::move(trusted_vault_keys[0]), /*trusted_vault_key_version=*/0,
          extension_input_data, &extension_output_data);

  if (!webauthn::passkey_model_utils::IsPasskeyValid(passkey)) {
    return {};
  }

  base::span<const uint8_t> cred_id =
      base::as_byte_span(passkey.credential_id());
  webauthn::passkey_model_utils::SerializedAttestationObject
      serialized_attestation_object =
          webauthn::passkey_model_utils::MakeAttestationObjectForCreation(
              rp_id_str, did_complete_uv, cred_id, public_key_spki_der);

  SavePasskeyCredential([[ArchivableCredential alloc] initWithFavicon:nil
                                                                 gaia:gaia
                                                              passkey:passkey]);

  NSData* credential_id = [NSData dataWithBytes:cred_id.data()
                                         length:cred_id.size()];
  NSData* attestation_object = [NSData
      dataWithBytes:serialized_attestation_object.attestation_object.data()
             length:serialized_attestation_object.attestation_object.size()];
  return {[ASPasskeyRegistrationCredential
              credentialWithRelyingParty:rp_id
                          clientDataHash:client_data_hash
                            credentialID:credential_id
                       attestationObject:attestation_object],
          PRFOutputsFromExtensionOutputData(extension_output_data)};
}

PasskeyAssertionOutput PerformPasskeyAssertion(
    id<Credential> credential,
    NSData* client_data_hash,
    NSArray<NSData*>* allowed_credentials,
    webauthn::SharedKeyList trusted_vault_keys,
    NSArray<NSData*>* prf_inputs,
    bool did_complete_uv) {
  if (trusted_vault_keys.empty()) {
    return {};
  }

  // If the array is empty, then the relying party accepts any passkey
  // credential.
  if (allowed_credentials.count > 0 &&
      ![allowed_credentials containsObject:credential.credentialId]) {
    return {};
  }

  std::optional<sync_pb::WebauthnCredentialSpecifics_Encrypted>
      credential_secrets =
          DecryptCredentialSecrets(credential, std::move(trusted_vault_keys));
  if (!credential_secrets) {
    return {};
  }

  webauthn::passkey_model_utils::ExtensionInputData extension_input_data =
      ExtensionInputDataFromPRFInputs(prf_inputs);
  NSData* authenticatorData =
      MakeAuthenticatorDataForAssertion(credential.rpId, did_complete_uv);
  NSData* signature = GenerateSignature(authenticatorData, client_data_hash,
                                        credential_secrets->private_key());

  if (!signature) {
    return {};
  }

  // Update the credential's last used time.
  credential.lastUsedTime =
      base::Time::Now().ToDeltaSinceWindowsEpoch().InMicroseconds();
  SavePasskeyCredential(credential);

  return {[ASPasskeyAssertionCredential
              credentialWithUserHandle:credential.userId
                          relyingParty:credential.rpId
                             signature:signature
                        clientDataHash:client_data_hash
                     authenticatorData:authenticatorData
                          credentialID:credential.credentialId],
          PRFOutputsFromExtensionOutputData(
              extension_input_data.ToOutputData(*credential_secrets))};
}

BOOL ShouldPerformUserVerificationForPreference(
    ASAuthorizationPublicKeyCredentialUserVerificationPreference
        user_verification_preference_string,
    BOOL is_biometric_authentication_enabled,
    BOOL is_conditional_create) {
  if (is_conditional_create) {
    return NO;
  }

  // Fall back to the `kPreferred` UV requirement as per the WebAuthn spec.
  std::string user_verification_requirement_string =
      SysNSStringToUTF8(user_verification_preference_string);
  return webauthn::GpmWillDoUserVerification(
      device::ConvertToUserVerificationRequirement(
          user_verification_requirement_string)
          .value_or(device::UserVerificationRequirement::kPreferred),
      is_biometric_authentication_enabled);
}

void SavePasskeyCredential(id<Credential> credential) {
  NSString* key = AppGroupUserDefaultsCredentialProviderNewCredentials();
  UserDefaultsCredentialStore* store = [[UserDefaultsCredentialStore alloc]
      initWithUserDefaults:app_group::GetGroupUserDefaults()
                       key:key];

  if ([store credentialWithRecordIdentifier:credential.recordIdentifier]) {
    [store updateCredential:credential];
  } else {
    [store addCredential:credential];
  }

  [store saveDataWithCompletion:^(NSError* error) {
    if (error != nil) {
      return;
    }

    SaveToIdentityStore(credential, ^{
      [CredentialProviderMigrationNotifier notifyMigrationNeeded];
    });
  }];
}
