chromium/device/fido/cros/authenticator.cc

// Copyright 2019 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/cros/authenticator.h"

#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "base/barrier_closure.h"
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/no_destructor.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/dbus/u2f/u2f_client.h"
#include "chromeos/dbus/u2f/u2f_interface.pb.h"
#include "components/cbor/reader.h"
#include "components/device_event_log/device_event_log.h"
#include "device/fido/attestation_statement_formats.h"
#include "device/fido/authenticator_data.h"
#include "device/fido/fido_parsing_utils.h"
#include "device/fido/fido_transport_protocol.h"
#include "device/fido/opaque_attestation_statement.h"
#include "third_party/cros_system_api/dbus/u2f/dbus-constants.h"

namespace device {

ChromeOSAuthenticator::ChromeOSAuthenticator(
    base::RepeatingCallback<std::string()> generate_request_id_callback,
    ChromeOSAuthenticator::Config config)
    : generate_request_id_callback_(std::move(generate_request_id_callback)),
      config_(config),
      weak_factory_(this) {}

ChromeOSAuthenticator::~ChromeOSAuthenticator() {}

AuthenticatorType ChromeOSAuthenticator::GetType() const {
  return AuthenticatorType::kChromeOS;
}

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

namespace {

AuthenticatorSupportedOptions ChromeOSAuthenticatorOptions(bool u2f_enabled) {
  AuthenticatorSupportedOptions options;
  options.is_platform_device =
      AuthenticatorSupportedOptions::PlatformDevice::kYes;
  // TODO(yichengli): change supports_resident_key to true once it's supported.
  options.supports_resident_key = false;
  // Even if the user has no fingerprints enrolled, we will have password
  // as fallback.
  options.user_verification_availability = AuthenticatorSupportedOptions::
      UserVerificationAvailability::kSupportedAndConfigured;
  options.supports_user_presence = true;
  // Enterprise attestation is enabled in the authenticator if its U2F/G2F mode
  // is enabled.
  options.enterprise_attestation = u2f_enabled;
  return options;
}

}  // namespace

const AuthenticatorSupportedOptions& ChromeOSAuthenticator::Options() const {
  static const base::NoDestructor<AuthenticatorSupportedOptions> options(
      ChromeOSAuthenticatorOptions(u2f_enabled_));
  return *options;
}

std::optional<FidoTransportProtocol>
ChromeOSAuthenticator::AuthenticatorTransport() const {
  return FidoTransportProtocol::kInternal;
}

void ChromeOSAuthenticator::InitializeAuthenticator(
    base::OnceClosure callback) {
  auto barrier = base::BarrierClosure(2, std::move(callback));

  u2f::GetAlgorithmsRequest request;
  chromeos::U2FClient::Get()->GetAlgorithms(
      request, base::BindOnce(&ChromeOSAuthenticator::OnGetAlgorithmsResponse,
                              weak_factory_.GetWeakPtr(), barrier));

  IsPowerButtonModeEnabled(
      base::BindOnce(&ChromeOSAuthenticator::OnIsPowerButtonModeEnabled,
                     weak_factory_.GetWeakPtr(), barrier));
}

void ChromeOSAuthenticator::OnGetAlgorithmsResponse(
    base::OnceClosure callback,
    std::optional<u2f::GetAlgorithmsResponse> response) {
  if (response && response->status() ==
                      u2f::GetAlgorithmsResponse_GetAlgorithmsStatus_SUCCESS) {
    supported_algorithms_ = std::vector<int32_t>();
    for (int i = 0; i < response->algorithm_size(); i++) {
      supported_algorithms_->push_back(response->algorithm(i));
    }
  } else {
    // Keep `supported_algorithms_` as nullopt if fetching supported algorithms
    // from u2fd failed, since the caller of `GetAlgorithms` method might want
    // to provide defaults.
    supported_algorithms_ = std::nullopt;
  }

  std::move(callback).Run();
}

void ChromeOSAuthenticator::OnIsPowerButtonModeEnabled(
    base::OnceClosure callback,
    bool enabled) {
  u2f_enabled_ = enabled;
  std::move(callback).Run();
}

std::optional<base::span<const int32_t>>
ChromeOSAuthenticator::GetAlgorithms() {
  if (supported_algorithms_) {
    return base::span<const int32_t>(*supported_algorithms_);
  }
  return std::nullopt;
}

void ChromeOSAuthenticator::MakeCredential(
    CtapMakeCredentialRequest request,
    MakeCredentialOptions request_options,
    MakeCredentialCallback callback) {
  u2f::MakeCredentialRequest req;
  // Only allow skipping user verification if user presence checks via power
  // button press have been configured. This is only the case when running with
  // the "DeviceSecondFactorAuthentication" enterprise policy.
  req.set_verification_type(
      (request.user_verification == UserVerificationRequirement::kDiscouraged &&
       config_.power_button_enabled)
          ? u2f::VERIFICATION_USER_PRESENCE
          : u2f::VERIFICATION_USER_VERIFICATION);
  req.set_rp_id(request.rp.id);
  req.set_client_data_hash(std::string(request.client_data_hash.begin(),
                                       request.client_data_hash.end()));

  // The ChromeOS platform authenticator supports attestation only for
  // credentials created through the legacy, enterprise-policy-controlled power
  // button authenticator. It has two modes, regular U2F attestation and and
  // individually identifying mode called G2F that needs to be explicitly
  // configured in the enterprise policy.
  switch (request.attestation_preference) {
    case AttestationConveyancePreference::kNone:
    case AttestationConveyancePreference::kIndirect:
      req.set_attestation_conveyance_preference(
          u2f::MakeCredentialRequest_AttestationConveyancePreference_NONE);
      break;
    case AttestationConveyancePreference::kDirect:
      req.set_attestation_conveyance_preference(
          u2f::MakeCredentialRequest_AttestationConveyancePreference_U2F);
      break;
    case AttestationConveyancePreference::kEnterpriseIfRPListedOnAuthenticator:
      // There is no separate mechanism for allowing individual RPs to use
      // individual G2F attestation. (Same as with regular U2F authenticators.)
      req.set_attestation_conveyance_preference(
          u2f::MakeCredentialRequest_AttestationConveyancePreference_U2F);
      break;
    case AttestationConveyancePreference::kEnterpriseApprovedByBrowser:
      req.set_attestation_conveyance_preference(
          u2f::MakeCredentialRequest_AttestationConveyancePreference_G2F);
      break;
  }

  req.set_user_id(std::string(request.user.id.begin(), request.user.id.end()));
  if (request.user.display_name.has_value()) {
    req.set_user_display_name(request.user.display_name.value());
  }
  req.set_resident_credential(request.resident_key_required);
  DCHECK(generate_request_id_callback_);
  DCHECK(current_request_id_.empty());
  current_request_id_ = generate_request_id_callback_.Run();
  req.set_request_id_str(current_request_id_);

  for (const PublicKeyCredentialDescriptor& descriptor : request.exclude_list) {
    req.add_excluded_credential_id(
        std::string(descriptor.id.begin(), descriptor.id.end()));
  }
  if (request.app_id_exclude) {
    req.set_app_id_exclude(*request.app_id_exclude);
  }

  chromeos::U2FClient::Get()->MakeCredential(
      req, base::BindOnce(&ChromeOSAuthenticator::OnMakeCredentialResponse,
                          weak_factory_.GetWeakPtr(), std::move(request),
                          std::move(callback)));
}

void ChromeOSAuthenticator::OnMakeCredentialResponse(
    CtapMakeCredentialRequest request,
    MakeCredentialCallback callback,
    std::optional<u2f::MakeCredentialResponse> response) {
  if (!response) {
    FIDO_LOG(ERROR) << "MakeCredential dbus call failed";
    std::move(callback).Run(MakeCredentialStatus::kAuthenticatorResponseInvalid,
                            std::nullopt);
    return;
  }

  FIDO_LOG(DEBUG) << "Make credential status: " << response->status();
  if (response->status() !=
      u2f::MakeCredentialResponse_MakeCredentialStatus_SUCCESS) {
    std::move(callback).Run(MakeCredentialStatus::kUserConsentDenied,
                            std::nullopt);
    return;
  }

  std::optional<AuthenticatorData> authenticator_data =
      AuthenticatorData::DecodeAuthenticatorData(
          base::as_bytes(base::make_span(response->authenticator_data())));
  if (!authenticator_data) {
    FIDO_LOG(ERROR) << "Authenticator data corrupted.";
    std::move(callback).Run(MakeCredentialStatus::kAuthenticatorResponseInvalid,
                            std::nullopt);
    return;
  }

  std::optional<cbor::Value> statement_map = cbor::Reader::Read(
      base::as_bytes(base::make_span(response->attestation_statement())));
  if (!statement_map ||
      statement_map.value().type() != cbor::Value::Type::MAP) {
    FIDO_LOG(ERROR) << "Attestation statement is not a CBOR map.";
    std::move(callback).Run(MakeCredentialStatus::kAuthenticatorResponseInvalid,
                            std::nullopt);
    return;
  }
  auto statement = std::make_unique<OpaqueAttestationStatement>(
      response->attestation_format(), std::move(*statement_map));

  AuthenticatorMakeCredentialResponse fido_response(
      FidoTransportProtocol::kInternal,
      AttestationObject(std::move(*authenticator_data), std::move(statement)));
  fido_response.transports.emplace();
  fido_response.transports->insert(FidoTransportProtocol::kInternal);

  std::move(callback).Run(MakeCredentialStatus::kSuccess,
                          std::move(fido_response));
}

void ChromeOSAuthenticator::GetAssertion(CtapGetAssertionRequest request,
                                         CtapGetAssertionOptions options,
                                         GetAssertionCallback callback) {
  u2f::GetAssertionRequest req;
  // Only allow skipping user verification if user presence checks via power
  // button press have been configured. This is only the case when running with
  // the "DeviceSecondFactorAuthentication" enterprise policy.
  req.set_verification_type(
      (request.user_verification == UserVerificationRequirement::kDiscouraged &&
       config_.power_button_enabled)
          ? u2f::VERIFICATION_USER_PRESENCE
          : u2f::VERIFICATION_USER_VERIFICATION);
  req.set_rp_id(request.rp_id);
  if (request.app_id) {
    req.set_app_id(*request.app_id);
  }
  req.set_client_data_hash(std::string(request.client_data_hash.begin(),
                                       request.client_data_hash.end()));
  DCHECK(generate_request_id_callback_);
  DCHECK(current_request_id_.empty());
  current_request_id_ = generate_request_id_callback_.Run();
  req.set_request_id_str(current_request_id_);

  for (const PublicKeyCredentialDescriptor& descriptor : request.allow_list) {
    req.add_allowed_credential_id(
        std::string(descriptor.id.begin(), descriptor.id.end()));
  }

  chromeos::U2FClient::Get()->GetAssertion(
      req, base::BindOnce(&ChromeOSAuthenticator::OnGetAssertionResponse,
                          weak_factory_.GetWeakPtr(), std::move(request),
                          std::move(callback)));
}

void ChromeOSAuthenticator::GetPlatformCredentialInfoForRequest(
    const CtapGetAssertionRequest& request,
    const CtapGetAssertionOptions& options,
    GetPlatformCredentialInfoForRequestCallback callback) {
  u2f::HasCredentialsRequest req;
  req.set_rp_id(request.rp_id);
  if (request.app_id) {
    req.set_app_id(*request.app_id);
  }

  for (const PublicKeyCredentialDescriptor& descriptor : request.allow_list) {
    req.add_credential_id(
        std::string(descriptor.id.begin(), descriptor.id.end()));
  }

  chromeos::U2FClient::Get()->HasCredentials(
      req, base::BindOnce(
               &ChromeOSAuthenticator::OnHasCredentialInformationForRequest,
               weak_factory_.GetWeakPtr(), std::move(callback)));
}

void ChromeOSAuthenticator::OnHasCredentialInformationForRequest(
    GetPlatformCredentialInfoForRequestCallback callback,
    std::optional<u2f::HasCredentialsResponse> response) {
  std::move(callback).Run(
      /*credentials=*/{},
      response &&
              response->status() ==
                  u2f::HasCredentialsResponse_HasCredentialsStatus_SUCCESS &&
              response->credential_id().size() > 0
          ? FidoRequestHandlerBase::RecognizedCredential::
                kHasRecognizedCredential
          : FidoRequestHandlerBase::RecognizedCredential::
                kNoRecognizedCredential);
}

void ChromeOSAuthenticator::OnGetAssertionResponse(
    CtapGetAssertionRequest request,
    GetAssertionCallback callback,
    std::optional<u2f::GetAssertionResponse> response) {
  if (!response) {
    FIDO_LOG(ERROR) << "GetAssertion dbus call failed";
    std::move(callback).Run(GetAssertionStatus::kAuthenticatorResponseInvalid,
                            {});
    return;
  }

  FIDO_LOG(DEBUG) << "GetAssertion status: " << response->status();
  if (response->status() !=
          u2f::GetAssertionResponse_GetAssertionStatus_SUCCESS ||
      response->assertion_size() < 1) {
    std::move(callback).Run(GetAssertionStatus::kUserConsentDenied, {});
    return;
  }

  u2f::Assertion assertion = response->assertion(0);

  std::optional<AuthenticatorData> authenticator_data =
      AuthenticatorData::DecodeAuthenticatorData(
          base::as_bytes(base::make_span(assertion.authenticator_data())));
  if (!authenticator_data) {
    FIDO_LOG(ERROR) << "Authenticator data corrupted.";
    std::move(callback).Run(GetAssertionStatus::kAuthenticatorResponseInvalid,
                            {});
    return;
  }

  std::vector<uint8_t> signature(assertion.signature().begin(),
                                 assertion.signature().end());
  std::vector<AuthenticatorGetAssertionResponse> authenticator_response;
  authenticator_response.push_back(AuthenticatorGetAssertionResponse(
      std::move(*authenticator_data), std::move(signature),
      FidoTransportProtocol::kInternal));
  const std::string& credential_id = assertion.credential_id();
  authenticator_response.at(0).credential = PublicKeyCredentialDescriptor(
      CredentialType::kPublicKey,
      std::vector<uint8_t>(credential_id.begin(), credential_id.end()));
  std::move(callback).Run(GetAssertionStatus::kSuccess,
                          std::move(authenticator_response));
}

void ChromeOSAuthenticator::HasLegacyU2fCredentialForGetAssertionRequest(
    const CtapGetAssertionRequest& request,
    base::OnceCallback<void(bool has_credential)> callback) {
  u2f::HasCredentialsRequest req;
  req.set_rp_id(request.rp_id);
  if (request.app_id) {
    req.set_app_id(*request.app_id);
  }

  for (const PublicKeyCredentialDescriptor& descriptor : request.allow_list) {
    req.add_credential_id(
        std::string(descriptor.id.begin(), descriptor.id.end()));
  }

  chromeos::U2FClient::Get()->HasLegacyU2FCredentials(
      req,
      base::BindOnce(
          [](base::OnceCallback<void(bool has_credential)> callback,
             std::optional<u2f::HasCredentialsResponse> response) {
            std::move(callback).Run(
                response &&
                response->status() ==
                    u2f::HasCredentialsResponse_HasCredentialsStatus_SUCCESS &&
                response->credential_id().size() > 0);
          },
          std::move(callback)));
}

void ChromeOSAuthenticator::Cancel() {
  if (current_request_id_.empty()) {
    return;
  }

  u2f::CancelWebAuthnFlowRequest req;
  req.set_request_id_str(current_request_id_);
  chromeos::U2FClient::Get()->CancelWebAuthnFlow(
      req, base::BindOnce(&ChromeOSAuthenticator::OnCancelResponse,
                          weak_factory_.GetWeakPtr()));
}

void ChromeOSAuthenticator::OnCancelResponse(
    std::optional<u2f::CancelWebAuthnFlowResponse> response) {
  current_request_id_.clear();

  if (!response) {
    FIDO_LOG(ERROR)
        << "CancelWebAuthnFlow dbus call had no response or timed out";
    return;
  }

  if (!response->canceled()) {
    FIDO_LOG(ERROR) << "Failed to cancel WebAuthn request";
  }
}

void ChromeOSAuthenticator::IsUVPlatformAuthenticatorAvailable(
    base::OnceCallback<void(bool is_uvpaa)> callback) {
  chromeos::U2FClient::IsU2FServiceAvailable(base::BindOnce(
      [](base::OnceCallback<void(bool is_uvpaa)> callback,
         bool is_u2f_service_available) {
        if (!is_u2f_service_available) {
          std::move(callback).Run(false);
          return;
        }

        chromeos::U2FClient::Get()->IsUvpaa(
            u2f::IsUvpaaRequest(),
            base::BindOnce(
                [](base::OnceCallback<void(bool is_available)> callback,
                   std::optional<u2f::IsUvpaaResponse> response) {
                  std::move(callback).Run(response &&
                                          !response->not_available());
                },
                std::move(callback)));
      },
      std::move(callback)));
}

void ChromeOSAuthenticator::IsPowerButtonModeEnabled(
    base::OnceCallback<void(bool is_enabled)> callback) {
  chromeos::U2FClient::Get()->IsU2FEnabled(
      u2f::IsU2fEnabledRequest(),
      base::BindOnce(
          [](base::OnceCallback<void(bool is_enabled)> callback,
             std::optional<u2f::IsU2fEnabledResponse> response) {
            std::move(callback).Run(response && response->enabled());
          },
          std::move(callback)));
}

void ChromeOSAuthenticator::IsLacrosSupported(
    base::OnceCallback<void(bool supported)> callback) {
  chromeos::U2FClient::Get()->GetSupportedFeatures(
      u2f::GetSupportedFeaturesRequest(),
      base::BindOnce(
          [](base::OnceCallback<void(bool is_enabled)> callback,
             std::optional<u2f::GetSupportedFeaturesResponse> response) {
            std::move(callback).Run(response && response->support_lacros());
          },
          std::move(callback)));
}

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

}  // namespace device