chromium/device/fido/win/authenticator.cc

// Copyright 2018 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/win/authenticator.h"

#include <windows.h>

#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "base/functional/bind.h"
#include "base/notreached.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_util_win.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "components/device_event_log/device_event_log.h"
#include "device/fido/authenticator_supported_options.h"
#include "device/fido/ctap_get_assertion_request.h"
#include "device/fido/ctap_make_credential_request.h"
#include "device/fido/discoverable_credential_metadata.h"
#include "device/fido/fido_constants.h"
#include "device/fido/fido_request_handler_base.h"
#include "device/fido/fido_transport_protocol.h"
#include "device/fido/public_key_credential_descriptor.h"
#include "device/fido/win/type_conversions.h"
#include "device/fido/win/webauthn_api.h"
#include "third_party/microsoft_webauthn/webauthn.h"

namespace device {

namespace {

AuthenticatorSupportedOptions WinWebAuthnApiOptions(int api_version) {
  AuthenticatorSupportedOptions options;
  options.is_platform_device =
      AuthenticatorSupportedOptions::PlatformDevice::kBoth;
  options.supports_resident_key = true;
  options.user_verification_availability = AuthenticatorSupportedOptions::
      UserVerificationAvailability::kSupportedAndConfigured;
  options.supports_user_presence = true;
  options.supports_cred_protect = api_version >= WEBAUTHN_API_VERSION_2;
  options.enterprise_attestation = api_version >= WEBAUTHN_API_VERSION_3;
  if (api_version >= WEBAUTHN_API_VERSION_3) {
    options.large_blob_type = LargeBlobSupportType::kKey;
  }
  options.supports_min_pin_length_extension =
      api_version >= WEBAUTHN_API_VERSION_3;
  if (api_version >= WEBAUTHN_API_VERSION_3) {
    // The 256-byte maximum length here is an arbitrary sanity limit. Blobs are
    // generally 32 bytes so this is intended to be larger than any reasonable
    // request.
    options.max_cred_blob_length = 256;
  }
  options.supports_hmac_secret = true;
  return options;
}

bool MayHaveWindowsHelloCredentials(
    std::vector<PublicKeyCredentialDescriptor> allow_list) {
  return allow_list.empty() ||
         base::ranges::any_of(allow_list, [](const auto& credential) {
           return credential.transports.empty() ||
                  base::Contains(credential.transports,
                                 FidoTransportProtocol::kInternal);
         });
}

// Filters credentials from |found_creds| that are not present in
// |allow_list_creds|.
void FilterFoundCredentials(
    std::vector<DiscoverableCredentialMetadata>* found_creds,
    const std::vector<PublicKeyCredentialDescriptor>& allow_list_creds) {
  auto remove_it = base::ranges::remove_if(
      *found_creds, [&allow_list_creds](const auto& found_cred) {
        return base::ranges::none_of(
            allow_list_creds, [&found_cred](const auto& allow_list_cred) {
              return allow_list_cred.id == found_cred.cred_id;
            });
      });
  found_creds->erase(remove_it, found_creds->end());
}

}  // namespace

// static
void WinWebAuthnApiAuthenticator::IsUserVerifyingPlatformAuthenticatorAvailable(
    WinWebAuthnApi* api,
    base::OnceCallback<void(bool is_available)> callback) {
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE,
      {base::TaskPriority::USER_VISIBLE, base::MayBlock(),
       base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
      base::BindOnce(
          [](WinWebAuthnApi* api) {
            BOOL result;
            if (!api || !api->IsAvailable()) {
              return false;
            }
            return api->IsUserVerifyingPlatformAuthenticatorAvailable(
                       &result) == S_OK &&
                   result == TRUE;
          },
          api),
      std::move(callback));
}

// static
void WinWebAuthnApiAuthenticator::IsConditionalMediationAvailable(
    WinWebAuthnApi* api,
    base::OnceCallback<void(bool is_available)> callback) {
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE,
      {base::TaskPriority::USER_VISIBLE, base::MayBlock(),
       base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
      base::BindOnce(
          [](WinWebAuthnApi* api) {
            return api && api->IsAvailable() && api->SupportsSilentDiscovery();
          },
          api),
      std::move(callback));
}

// static
void WinWebAuthnApiAuthenticator::EnumeratePlatformCredentials(
    WinWebAuthnApi* api,
    base::OnceCallback<
        void(std::vector<device::DiscoverableCredentialMetadata>)> callback) {
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE,
      {base::TaskPriority::USER_VISIBLE, base::MayBlock(),
       base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
      base::BindOnce(AuthenticatorEnumerateCredentialsBlocking, api,
                     /*rp_id=*/std::u16string_view(),
                     /*is_incognito=*/false),
      base::BindOnce(
          [](base::OnceCallback<void(
                 std::vector<device::DiscoverableCredentialMetadata>)> callback,
             std::pair<bool, std::vector<DiscoverableCredentialMetadata>>
                 result) { std::move(callback).Run(std::move(result.second)); },
          std::move(callback)));
}

// static
void WinWebAuthnApiAuthenticator::DeletePlatformCredential(
    WinWebAuthnApi* api,
    base::span<const uint8_t> credential_id,
    base::OnceCallback<void(bool)> callback) {
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE,
      {base::TaskPriority::USER_VISIBLE, base::MayBlock(),
       base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
      base::BindOnce(
          [](WinWebAuthnApi* api, std::vector<uint8_t> credential_id) -> bool {
            return api && api->IsAvailable() &&
                   api->SupportsSilentDiscovery() &&
                   api->DeletePlatformCredential(credential_id) == S_OK;
          },
          api,
          std::vector<uint8_t>(credential_id.begin(), credential_id.end())),
      std::move(callback));
}

WinWebAuthnApiAuthenticator::WinWebAuthnApiAuthenticator(
    HWND current_window,
    WinWebAuthnApi* win_api)
    : options_(WinWebAuthnApiOptions(win_api->Version())),
      current_window_(current_window),
      win_api_(win_api) {
  CHECK(win_api_->IsAvailable());
  CoCreateGuid(&cancellation_id_);
}

WinWebAuthnApiAuthenticator::~WinWebAuthnApiAuthenticator() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  Cancel();
}

void WinWebAuthnApiAuthenticator::InitializeAuthenticator(
    base::OnceClosure callback) {
  std::move(callback).Run();
}

void WinWebAuthnApiAuthenticator::MakeCredential(
    CtapMakeCredentialRequest request,
    MakeCredentialOptions options,
    MakeCredentialCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (is_pending_) {
    NOTREACHED_IN_MIGRATION();
    return;
  }
  is_pending_ = true;

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::TaskPriority::USER_BLOCKING, base::MayBlock()},
      base::BindOnce(&AuthenticatorMakeCredentialBlocking, win_api_,
                     current_window_, cancellation_id_, std::move(request),
                     std::move(options)),
      base::BindOnce(&WinWebAuthnApiAuthenticator::MakeCredentialDone,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
}

void WinWebAuthnApiAuthenticator::MakeCredentialDone(
    MakeCredentialCallback callback,
    std::pair<MakeCredentialStatus,
              std::optional<AuthenticatorMakeCredentialResponse>> result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(is_pending_);
  is_pending_ = false;
  if (waiting_for_cancellation_) {
    // Don't bother invoking the reply callback if the caller has already
    // cancelled the operation.
    waiting_for_cancellation_ = false;
    return;
  }
  if (result.first != MakeCredentialStatus::kSuccess) {
    std::move(callback).Run(result.first, std::nullopt);
    return;
  }
  CHECK(result.second);
  std::move(callback).Run(result.first, std::move(result.second));
}

void WinWebAuthnApiAuthenticator::GetAssertion(CtapGetAssertionRequest request,
                                               CtapGetAssertionOptions options,
                                               GetAssertionCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(!is_pending_);
  if (is_pending_) {
    return;
  }

  is_pending_ = true;

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::TaskPriority::USER_BLOCKING, base::MayBlock()},
      base::BindOnce(&AuthenticatorGetAssertionBlocking, win_api_,
                     current_window_, cancellation_id_, std::move(request),
                     std::move(options)),
      base::BindOnce(&WinWebAuthnApiAuthenticator::GetAssertionDone,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
}

void WinWebAuthnApiAuthenticator::GetAssertionDone(
    GetAssertionCallback callback,
    std::pair<GetAssertionStatus,
              std::optional<AuthenticatorGetAssertionResponse>> result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(is_pending_);
  is_pending_ = false;
  if (waiting_for_cancellation_) {
    waiting_for_cancellation_ = false;
    return;
  }
  if (result.first != GetAssertionStatus::kSuccess) {
    std::move(callback).Run(result.first, {});
    return;
  }
  CHECK(result.second);
  std::vector<AuthenticatorGetAssertionResponse> responses;
  responses.emplace_back(std::move(*result.second));
  std::move(callback).Run(result.first, std::move(responses));
}

void WinWebAuthnApiAuthenticator::GetPlatformCredentialInfoForRequest(
    const CtapGetAssertionRequest& request,
    const CtapGetAssertionOptions& request_options,
    GetPlatformCredentialInfoForRequestCallback callback) {
  // Handle the special case where a request has an allow list, all the
  // credential descriptors have a transport, and none of have the "internal"
  // transport. These credentials cannot possibly be Windows Hello.
  if (!MayHaveWindowsHelloCredentials(request.allow_list)) {
    std::move(callback).Run(
        /*credentials=*/{},
        FidoRequestHandlerBase::RecognizedCredential::kNoRecognizedCredential);
    return;
  }
  FIDO_LOG(DEBUG) << "Silently discovering credentials for " << request.rp_id;
  auto [success, credentials] = AuthenticatorEnumerateCredentialsBlocking(
      win_api_, base::UTF8ToUTF16(request.rp_id),
      request_options.is_off_the_record_context);
  if (!success) {
    std::move(callback).Run(
        /*credentials=*/{},
        FidoRequestHandlerBase::RecognizedCredential::kUnknown);
    return;
  }
  if (!request.allow_list.empty()) {
    FilterFoundCredentials(&credentials, request.allow_list);
  }
  auto recognized = credentials.empty()
                        ? FidoRequestHandlerBase::RecognizedCredential::
                              kNoRecognizedCredential
                        : FidoRequestHandlerBase::RecognizedCredential::
                              kHasRecognizedCredential;
  std::move(callback).Run(std::move(credentials), recognized);
}

void WinWebAuthnApiAuthenticator::GetTouch(base::OnceClosure callback) {
  NOTREACHED_IN_MIGRATION();
}

void WinWebAuthnApiAuthenticator::Cancel() {
  if (!is_pending_ || waiting_for_cancellation_) {
    return;
  }

  waiting_for_cancellation_ = true;
  // This returns immediately.
  win_api_->CancelCurrentOperation(&cancellation_id_);
}

AuthenticatorType WinWebAuthnApiAuthenticator::GetType() const {
  return AuthenticatorType::kWinNative;
}

std::string WinWebAuthnApiAuthenticator::GetId() const {
  return "WinWebAuthnApiAuthenticator";
}

std::optional<FidoTransportProtocol>
WinWebAuthnApiAuthenticator::AuthenticatorTransport() const {
  // The Windows API could potentially use any external or
  // platform authenticator.
  return std::nullopt;
}

const AuthenticatorSupportedOptions& WinWebAuthnApiAuthenticator::Options()
    const {
  return options_;
}

base::WeakPtr<FidoAuthenticator> WinWebAuthnApiAuthenticator::GetWeakPtr() {
  return weak_factory_.GetWeakPtr();
}

bool WinWebAuthnApiAuthenticator::ShowsPrivacyNotice() const {
  return win_api_->Version() >= WEBAUTHN_API_VERSION_2;
}

}  // namespace device