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

#include "third_party/blink/renderer/modules/credentialmanagement/public_key_credential.h"

#include <utility>
#include <variant>

#include "third_party/abseil-cpp/absl/functional/overload.h"
#include "third_party/blink/public/mojom/webauthn/authenticator.mojom-shared.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_all_accepted_credentials_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_authentication_response_js_on.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_current_user_details_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_public_key_credential_creation_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_registration_response_js_on.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_unknown_credential_options.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
#include "third_party/blink/renderer/modules/credentialmanagement/authentication_credentials_container.h"
#include "third_party/blink/renderer/modules/credentialmanagement/credential_manager_proxy.h"
#include "third_party/blink/renderer/modules/credentialmanagement/json.h"
#include "third_party/blink/renderer/modules/credentialmanagement/scoped_promise_resolver.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/wtf/forward.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
#include "third_party/blink/renderer/platform/wtf/text/base64.h"
#include "v8/include/v8-local-handle.h"
#include "v8/include/v8-value.h"

namespace blink {

namespace {

// https://www.w3.org/TR/webauthn/#dom-publickeycredential-type-slot:
constexpr char kPublicKeyCredentialType[] = "public-key";

void OnIsUserVerifyingComplete(ScriptPromiseResolver<IDLBoolean>* resolver,
                               bool available) {
  resolver->Resolve(available);
}

String AuthenticatorAttachmentToString(
    mojom::blink::AuthenticatorAttachment authenticator_attachment) {
  switch (authenticator_attachment) {
    case mojom::blink::AuthenticatorAttachment::PLATFORM:
      return "platform";
    case mojom::blink::AuthenticatorAttachment::CROSS_PLATFORM:
      return "cross-platform";
    case mojom::blink::AuthenticatorAttachment::NO_PREFERENCE:
      return g_null_atom;
  }
}

void OnGetClientCapabilitiesComplete(
    ScriptPromiseResolver<IDLRecord<IDLString, IDLBoolean>>* resolver,
    const Vector<mojom::blink::WebAuthnClientCapabilityPtr> capabilities) {
  Vector<std::pair<String, bool>> results;
  for (const auto& capability : capabilities) {
    results.emplace_back(std::move(capability->name), capability->supported);
  }

  // Extensions are added from the AuthenticationExtensionsClientInputs
  // dictionary defined in authentication_extensions_client_inputs.idl.
  // According to the specification, we should include a key for each
  // extension implemented by the client, formed by prefixing "extension:"
  // to the extension identifier.
  //
  // Excluded extensions: uvm and remoteDesktopClientOverride.
  results.emplace_back("extension:appid", true);
  results.emplace_back("extension:appidExclude", true);
  results.emplace_back("extension:hmacCreateSecret", true);
  results.emplace_back("extension:credentialProtectionPolicy", true);
  results.emplace_back("extension:enforceCredentialProtectionPolicy", true);
  results.emplace_back("extension:minPinLength", true);
  results.emplace_back("extension:credProps", true);
  results.emplace_back("extension:largeBlob", true);
  results.emplace_back("extension:credBlob", true);
  results.emplace_back("extension:getCredBlob", true);
  results.emplace_back(
      "extension:payment",
      RuntimeEnabledFeatures::SecurePaymentConfirmationEnabled());
  results.emplace_back("extension:prf", true);
  results.emplace_back(
      "extension:cmtgKey",
      RuntimeEnabledFeatures::WebAuthenticationCmtgKeyEnabled());
  results.emplace_back(
      "extension:crossDeviceFallbackUrl",
      RuntimeEnabledFeatures::WebAuthenticationCrossDeviceFallbackUrlEnabled(
          resolver->GetExecutionContext()));

  // Results should be sorted lexicographically based on the keys.
  std::sort(
      results.begin(), results.end(),
      [](const std::pair<String, bool>& a, const std::pair<String, bool>& b) {
        return CodeUnitCompare(a.first, b.first) < 0;
      });

  if (!RuntimeEnabledFeatures::WebAuthenticationAmbientEnabled(
          resolver->GetExecutionContext())) {
    for (wtf_size_t i = 0; i < results.size(); ++i) {
      if (results[i].first == "ambientGet") {
        results.EraseAt(i);
        break;
      }
    }
  }
  resolver->Resolve(std::move(results));
}

void OnSignalReportComplete(
    std::unique_ptr<ScopedPromiseResolver> scoped_resolver,
    mojom::AuthenticatorStatus status,
    mojom::blink::WebAuthnDOMExceptionDetailsPtr dom_exception_details) {
  auto* resolver = scoped_resolver->Release()->DowncastTo<IDLUndefined>();
  if (status != mojom::blink::AuthenticatorStatus::SUCCESS) {
    resolver->Reject(
        AuthenticatorStatusToDOMException(status, dom_exception_details));
    return;
  }
  resolver->Resolve();
}

}  // namespace

PublicKeyCredential::PublicKeyCredential(
    const String& id,
    DOMArrayBuffer* raw_id,
    AuthenticatorResponse* response,
    mojom::blink::AuthenticatorAttachment authenticator_attachment,
    const AuthenticationExtensionsClientOutputs* extension_outputs,
    const String& type)
    : Credential(id, type.empty() ? kPublicKeyCredentialType : type),
      raw_id_(raw_id),
      response_(response),
      authenticator_attachment_(
          AuthenticatorAttachmentToString(authenticator_attachment)),
      extension_outputs_(extension_outputs) {}

// static
ScriptPromise<IDLRecord<IDLString, IDLBoolean>>
PublicKeyCredential::getClientCapabilities(ScriptState* script_state) {
  // Ignore calls if the current realm execution context is no longer valid,
  // e.g., because the responsible document was detached.
  if (!script_state->ContextIsValid()) {
    return ScriptPromise<IDLRecord<IDLString, IDLBoolean>>::
        RejectWithDOMException(
            script_state,
            MakeGarbageCollected<DOMException>(
                DOMExceptionCode::kInvalidStateError, "Context is detached"));
  }

  auto* resolver = MakeGarbageCollected<
      ScriptPromiseResolver<IDLRecord<IDLString, IDLBoolean>>>(script_state);
  ScriptPromise promise = resolver->Promise();

  UseCounter::Count(resolver->GetExecutionContext(),
                    WebFeature::kWebAuthnGetClientCapabilities);

  auto* authenticator =
      CredentialManagerProxy::From(script_state)->Authenticator();
  authenticator->GetClientCapabilities(
      BindOnce(&OnGetClientCapabilitiesComplete, WrapPersistent(resolver)));
  return promise;
}

// static
ScriptPromise<IDLBoolean>
PublicKeyCredential::isUserVerifyingPlatformAuthenticatorAvailable(
    ScriptState* script_state) {
  // Ignore calls if the current realm execution context is no longer valid,
  // e.g., because the responsible document was detached.
  if (!script_state->ContextIsValid()) {
    return ScriptPromise<IDLBoolean>::RejectWithDOMException(
        script_state,
        MakeGarbageCollected<DOMException>(DOMExceptionCode::kInvalidStateError,
                                           "Context is detached"));
  }

  auto* resolver =
      MakeGarbageCollected<ScriptPromiseResolver<IDLBoolean>>(script_state);
  auto promise = resolver->Promise();

  UseCounter::Count(
      resolver->GetExecutionContext(),
      WebFeature::
          kCredentialManagerIsUserVerifyingPlatformAuthenticatorAvailable);

  auto* authenticator =
      CredentialManagerProxy::From(script_state)->Authenticator();
  authenticator->IsUserVerifyingPlatformAuthenticatorAvailable(
      BindOnce(&OnIsUserVerifyingComplete, WrapPersistent(resolver)));
  return promise;
}

AuthenticationExtensionsClientOutputs*
PublicKeyCredential::getClientExtensionResults() const {
  return const_cast<AuthenticationExtensionsClientOutputs*>(
      extension_outputs_.Get());
}

// static
// Credential:
ScriptPromise<IDLBoolean> PublicKeyCredential::isConditionalMediationAvailable(
    ScriptState* script_state) {
  auto* resolver =
      MakeGarbageCollected<ScriptPromiseResolver<IDLBoolean>>(script_state);
  auto promise = resolver->Promise();

  // Ignore calls if the current realm execution context is no longer valid,
  // e.g., because the responsible document was detached.
  DCHECK(resolver->GetExecutionContext());
  if (resolver->GetExecutionContext()->IsContextDestroyed()) {
    resolver->Reject();
    return promise;
  }
  UseCounter::Count(
      resolver->GetExecutionContext(),
      WebFeature::kCredentialManagerIsConditionalMediationAvailable);
  auto* authenticator =
      CredentialManagerProxy::From(script_state)->Authenticator();
  authenticator->IsConditionalMediationAvailable(
      BindOnce([](ScriptPromiseResolver<IDLBoolean>* resolver,
                  bool available) { resolver->Resolve(available); },
               WrapPersistent(resolver)));
  return promise;
}

v8::Local<v8::Object> PublicKeyCredential::toJSON(
    ScriptState* script_state) const {
  // PublicKeyCredential.response holds an AuthenticatorAttestationResponse, if
  // it was returned from a create call, or an AuthenticatorAssertionResponse
  // if returned from a get() call. In the former case, the spec wants us to
  // return a RegistrationResponseJSON, and in the latter an
  // AuthenticationResponseJSON.  We can't reflect the type of `response_`
  // though, so we serialize it to JSON first and branch on the result type.
  std::variant<AuthenticatorAssertionResponseJSON*,
               AuthenticatorAttestationResponseJSON*>
      response_json = response_->toJSON();

  v8::Local<v8::Value> result;
  std::visit(
      absl::Overload{
          [&](AuthenticatorAttestationResponseJSON* attestation_response) {
            auto* registration_response = RegistrationResponseJSON::Create();
            registration_response->setId(id());
            registration_response->setRawId(WebAuthnBase64UrlEncode(rawId()));
            registration_response->setResponse(attestation_response);
            if (!authenticator_attachment_.IsNull()) {
              registration_response->setAuthenticatorAttachment(
                  authenticator_attachment_);
            }
            registration_response->setClientExtensionResults(
                AuthenticationExtensionsClientOutputsToJSON(
                    script_state, *extension_outputs_));
            registration_response->setType(type());
            result = registration_response->ToV8(script_state);
          },
          [&](AuthenticatorAssertionResponseJSON* assertion_response) {
            auto* authentication_response =
                AuthenticationResponseJSON::Create();
            authentication_response->setId(id());
            authentication_response->setRawId(WebAuthnBase64UrlEncode(rawId()));
            authentication_response->setResponse(assertion_response);
            if (!authenticator_attachment_.IsNull()) {
              authentication_response->setAuthenticatorAttachment(
                  authenticator_attachment_);
            }
            authentication_response->setClientExtensionResults(
                AuthenticationExtensionsClientOutputsToJSON(
                    script_state, *extension_outputs_));
            authentication_response->setType(type());
            result = authentication_response->ToV8(script_state);
          }},
      response_json);
  CHECK(result->IsObject());
  return result.As<v8::Object>();
}

// static
const PublicKeyCredentialCreationOptions*
PublicKeyCredential::parseCreationOptionsFromJSON(
    ScriptState* script_state,
    const PublicKeyCredentialCreationOptionsJSON* options,
    ExceptionState& exception_state) {
  return PublicKeyCredentialCreationOptionsFromJSON(options, exception_state);
}

// static
const PublicKeyCredentialRequestOptions*
PublicKeyCredential::parseRequestOptionsFromJSON(
    ScriptState* script_state,
    const PublicKeyCredentialRequestOptionsJSON* options,
    ExceptionState& exception_state) {
  return PublicKeyCredentialRequestOptionsFromJSON(options, exception_state);
}

// static
ScriptPromise<IDLUndefined> PublicKeyCredential::signalUnknownCredential(
    ScriptState* script_state,
    const UnknownCredentialOptions* options,
    ExceptionState& exception_state) {
  if (!script_state->ContextIsValid()) {
    return ScriptPromise<IDLUndefined>::RejectWithDOMException(
        script_state,
        MakeGarbageCollected<DOMException>(DOMExceptionCode::kInvalidStateError,
                                           "Context is detached"));
  }
  auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>(
      script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  Vector<uint8_t> decoded_cred_id;
  if (!Base64UnpaddedUrlDecode(options->credentialId(), decoded_cred_id)) {
    resolver->RejectWithTypeError("Invalid base64url string for credentialId.");
    return promise;
  }
  mojom::blink::PublicKeyCredentialReportOptionsPtr mojo_options =
      mojom::blink::PublicKeyCredentialReportOptions::From(*options);
  auto* authenticator =
      CredentialManagerProxy::From(script_state)->Authenticator();
  authenticator->Report(
      std::move(mojo_options),
      BindOnce(&OnSignalReportComplete,
               std::make_unique<ScopedPromiseResolver>(
                   resolver,
                   ScopedPromiseResolver::ConnectionType::kAuthenticator)));
  return promise;
}

// static
ScriptPromise<IDLUndefined> PublicKeyCredential::signalAllAcceptedCredentials(
    ScriptState* script_state,
    const AllAcceptedCredentialsOptions* options,
    ExceptionState& exception_state) {
  if (!script_state->ContextIsValid()) {
    return ScriptPromise<IDLUndefined>::RejectWithDOMException(
        script_state,
        MakeGarbageCollected<DOMException>(DOMExceptionCode::kInvalidStateError,
                                           "Context is detached"));
  }
  auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>(
      script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  for (String credential_id : options->allAcceptedCredentialIds()) {
    Vector<uint8_t> decoded_cred_id;
    if (!Base64UnpaddedUrlDecode(credential_id, decoded_cred_id)) {
      resolver->RejectWithTypeError(
          "Invalid base64url string for allAcceptedCredentialIds.");
      return promise;
    }
  }
  Vector<uint8_t> decoded_user_id;
  if (!Base64UnpaddedUrlDecode(options->userId(), decoded_user_id)) {
    resolver->RejectWithTypeError("Invalid base64url string for userId.");
    return promise;
  }
  mojom::blink::PublicKeyCredentialReportOptionsPtr mojo_options =
      mojom::blink::PublicKeyCredentialReportOptions::From(*options);
  auto* authenticator =
      CredentialManagerProxy::From(script_state)->Authenticator();
  authenticator->Report(
      std::move(mojo_options),
      BindOnce(&OnSignalReportComplete,
               std::make_unique<ScopedPromiseResolver>(
                   resolver,
                   ScopedPromiseResolver::ConnectionType::kAuthenticator)));
  return promise;
}

// static
ScriptPromise<IDLUndefined> PublicKeyCredential::signalCurrentUserDetails(
    ScriptState* script_state,
    const CurrentUserDetailsOptions* options,
    ExceptionState& exception_state) {
  if (!script_state->ContextIsValid()) {
    return ScriptPromise<IDLUndefined>::RejectWithDOMException(
        script_state,
        MakeGarbageCollected<DOMException>(DOMExceptionCode::kInvalidStateError,
                                           "Context is detached"));
  }
  auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>(
      script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  Vector<uint8_t> decoded_user_id;
  if (!Base64UnpaddedUrlDecode(options->userId(), decoded_user_id)) {
    resolver->RejectWithTypeError("Invalid base64url string for userId.");
    return promise;
  }
  mojom::blink::PublicKeyCredentialReportOptionsPtr mojo_options =
      mojom::blink::PublicKeyCredentialReportOptions::From(*options);
  auto* authenticator =
      CredentialManagerProxy::From(script_state)->Authenticator();
  authenticator->Report(
      std::move(mojo_options),
      BindOnce(&OnSignalReportComplete,
               std::make_unique<ScopedPromiseResolver>(
                   resolver,
                   ScopedPromiseResolver::ConnectionType::kAuthenticator)));
  return promise;
}

void PublicKeyCredential::Trace(Visitor* visitor) const {
  visitor->Trace(raw_id_);
  visitor->Trace(response_);
  visitor->Trace(extension_outputs_);
  Credential::Trace(visitor);
}

bool PublicKeyCredential::IsPublicKeyCredential() const {
  return true;
}

}  // namespace blink
