chromium/chrome/browser/webauthn/android/cable_module_android.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/browser/webauthn/android/cable_module_android.h"

#include "base/android/jni_array.h"
#include "base/base64.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/raw_ptr_exclusion.h"
#include "base/no_destructor.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h"
#include "chrome/browser/net/system_network_context_manager.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/sync/device_info_sync_service_factory.h"
#include "chrome/browser/webauthn/android/cable_registration_state.h"
#include "components/cbor/reader.h"
#include "components/cbor/values.h"
#include "components/cbor/writer.h"
#include "components/gcm_driver/instance_id/instance_id_profile_service.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/sync_device_info/device_info.h"
#include "components/sync_device_info/device_info_sync_service.h"
#include "content/public/browser/browser_thread.h"
#include "device/fido/cable/v2_constants.h"
#include "device/fido/cable/v2_handshake.h"
#include "device/fido/cable/v2_registration.h"
#include "device/fido/cbor_extract.h"
#include "device/fido/features.h"
#include "third_party/boringssl/src/include/openssl/bytestring.h"
#include "third_party/boringssl/src/include/openssl/digest.h"
#include "third_party/boringssl/src/include/openssl/ec.h"
#include "third_party/boringssl/src/include/openssl/ec_key.h"
#include "third_party/boringssl/src/include/openssl/hkdf.h"
#include "third_party/boringssl/src/include/openssl/mem.h"
#include "third_party/boringssl/src/include/openssl/obj.h"

// These "headers" actually contains function definitions and thus can only be
// included once across Chromium.
#include "chrome/browser/webauthn/android/jni_headers/CableAuthenticatorModuleProvider_jni.h"
#include "chrome/browser/webauthn/android/jni_headers/PrivacySettingsFragment_jni.h"

using device::cablev2::authenticator::Registration;

namespace webauthn {
namespace authenticator {

namespace {

// kRootSecretPrefName is the name of a string preference that is kept in the
// browser's local state and which stores the base64-encoded root secret for
// the authenticator.
const char kRootSecretPrefName[] = "webauthn.authenticator_root_secret";
const char kSerializedPaaskFieldsName[] = "webauthn.authenticator_info";

const char kWorkProfilePrefName[] = "webauthn.in_work_profile";
// kWorkProfilePrefName wants to be a tristate. Since there's no support for
// that in `PrefService`, it's simulated with a string that is empty if unset,
// and takes one of the following values when set.
const char kInWorkProfile[] = "1";
const char kNotInWorkProfile[] = "0";

// SystemInterface connects a `RegistrationState` to the rest of the system.
// This object is owned by the `RegistrationState`, and that is a singleton
// object. So this object is a singleton too and so can do things like pass a
// pointer to itself to Java functions to route the eventual callback.
class SystemInterface : public RegistrationState::SystemInterface {
 public:
  std::unique_ptr<device::cablev2::authenticator::Registration> NewRegistration(
      device::cablev2::authenticator::Registration::Type type,
      base::OnceCallback<void()> on_ready,
      base::RepeatingCallback<void(
          std::unique_ptr<device::cablev2::authenticator::Registration::Event>)>
          event_callback) override {
    DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
    return device::cablev2::authenticator::Register(
        GetDriver(), type, std::move(on_ready), std::move(event_callback));
  }

  std::string GetRootSecret() override {
    DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
    return g_browser_process->local_state()->GetString(kRootSecretPrefName);
  }

  void SetRootSecret(std::string secret) override {
    DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
    g_browser_process->local_state()->SetString(kRootSecretPrefName,
                                                std::move(secret));
  }

  void CanDeviceSupportCable(base::OnceCallback<void(bool)> callback) override {
    DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
    base::ThreadPool::PostTaskAndReplyWithResult(
        FROM_HERE, {base::TaskPriority::BEST_EFFORT},
        base::BindOnce(
            &SystemInterface::GetCanDeviceSupportCableOnBackgroundSequence),
        std::move(callback));
  }

  void AmInWorkProfile(base::OnceCallback<void(bool)> callback) override {
    DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));

    // Checking whether an app is in a work profile is costly. We assume that a
    // given Chrome profile never moves between being in a work profile or not
    // and thus cache the result on disk.
    const std::string work_profile_state =
        g_browser_process->local_state()->GetString(kWorkProfilePrefName);
    if (work_profile_state == kInWorkProfile) {
      std::move(callback).Run(true);
    } else if (work_profile_state == kNotInWorkProfile) {
      std::move(callback).Run(false);
    } else {
      work_profile_callback_ = std::move(callback);
      // Checking whether this Chrome is in a work profile is sufficiently
      // expensive that doing it at startup impacts benchmarks. (See
      // crbug.com/1459794.) Since startup is an especially contended time, we
      // wait a few minutes before doing this check.
      content::BrowserThread::GetTaskRunnerForThread(content::BrowserThread::UI)
          ->PostDelayedTask(
              FROM_HERE,
              base::BindOnce(&SystemInterface::GetWorkProfileStatus,
                             base::Unretained(this)),
              base::Minutes(3));
    }
  }

  void CalculateIdentityKey(
      const std::array<uint8_t, 32>& secret,
      base::OnceCallback<void(bssl::UniquePtr<EC_KEY>)> callback) override {
    DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
    base::ThreadPool::PostTaskAndReplyWithResult(
        FROM_HERE, {base::TaskPriority::BEST_EFFORT},
        base::BindOnce(
            &SystemInterface::CalculateIdentityKeyOnBackgroundSequence, secret),
        std::move(callback));
  }

  void GetPrelinkFromPlayServices(
      base::OnceCallback<void(std::optional<std::vector<uint8_t>>)> callback)
      override {
    DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
    DCHECK(!prelink_callback_);
    prelink_callback_ = std::move(callback);
    base::ThreadPool::PostTask(
        FROM_HERE, {base::TaskPriority::BEST_EFFORT},
        base::BindOnce(
            &SystemInterface::GetPrelinkFromPlayServicesOnBackgroundSequence,
            // Passing this pointer is reasonable because this object is owned
            // by a singleton.
            reinterpret_cast<uintptr_t>(this)));
  }

  void OnCloudMessage(std::vector<uint8_t> serialized,
                      bool is_make_credential) override {
    DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
    JNIEnv* const env = base::android::AttachCurrentThread();
    Java_CableAuthenticatorModuleProvider_onCloudMessage(
        env, base::android::ToJavaByteArray(env, serialized),
        is_make_credential);
  }

  void RefreshLocalDeviceInfo() override {
    DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
    DeviceInfoSyncServiceFactory::GetForProfile(
        ProfileManager::GetPrimaryUserProfile())
        ->RefreshLocalDeviceInfo();
  }

  // Called when the Java code has finished getting linking information from
  // Play Services.
  void OnHavePlayServicesLinkingInformation(
      std::optional<std::vector<uint8_t>> cbor) {
    DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
    std::move(prelink_callback_).Run(std::move(cbor));
  }

  // Called when the Java code has finished checking if we're running in a work
  // profile.
  void OnHaveWorkProfileResult(bool in_work_profile) {
    DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));

    g_browser_process->local_state()->SetString(
        kWorkProfilePrefName,
        in_work_profile ? kInWorkProfile : kNotInWorkProfile);

    std::move(work_profile_callback_).Run(in_work_profile);
  }

 private:
  static instance_id::InstanceIDDriver* GetDriver() {
    return instance_id::InstanceIDProfileServiceFactory::GetForProfile(
               g_browser_process->profile_manager()->GetPrimaryUserProfile())
        ->driver();
  }

  static bool GetCanDeviceSupportCableOnBackgroundSequence() {
    // This runs on a worker thread because this Java function can take a
    // little while and it shouldn't block the UI thread.
    return Java_CableAuthenticatorModuleProvider_canDeviceSupportCable(
        base::android::AttachCurrentThread());
  }

  static bssl::UniquePtr<EC_KEY> CalculateIdentityKeyOnBackgroundSequence(
      std::array<uint8_t, 32> secret) {
    // This runs on a worker thread because the scalar multiplication takes a
    // few milliseconds on slower devices.
    return device::cablev2::IdentityKey(secret);
  }

  static void GetPrelinkFromPlayServicesOnBackgroundSequence(
      uintptr_t this_pointer) {
    // This runs on a worker thread because this Java function can take a
    // little while and it shouldn't block the UI thread.
    Java_CableAuthenticatorModuleProvider_getLinkingInformation(
        base::android::AttachCurrentThread(), this_pointer);
  }

  void GetWorkProfileStatus() {
    // This Java function must run on the UI thread, but that's ok because it
    // defers work to a worker thread itself. It returns its result by calling
    // `OnHaveWorkProfileResult` on this object.
    Java_CableAuthenticatorModuleProvider_amInWorkProfile(
        base::android::AttachCurrentThread(),
        reinterpret_cast<uintptr_t>(this));
  }

  base::OnceCallback<void(std::optional<std::vector<uint8_t>>)>
      prelink_callback_;
  base::OnceCallback<void(bool)> work_profile_callback_;
};

RegistrationState* GetRegistrationState() {
  static base::NoDestructor<RegistrationState> state(
      std::make_unique<SystemInterface>());
  return state.get();
}

using device::cbor_extract::IntKey;
using device::cbor_extract::Is;
using device::cbor_extract::Map;
using device::cbor_extract::StepOrByte;
using device::cbor_extract::Stop;

// PreLinkInfo reflects the linking information provided by Play Services.
struct PreLinkInfo {
  // RAW_PTR_EXCLUSION: cbor_extract.cc would cast the raw_ptr<T> to a void*,
  // skipping an AddRef() call and causing a ref-counting mismatch.
  RAW_PTR_EXCLUSION const std::vector<uint8_t>* contact_id;
  RAW_PTR_EXCLUSION const std::vector<uint8_t>* pairing_id;
  RAW_PTR_EXCLUSION const std::vector<uint8_t>* secret;
  RAW_PTR_EXCLUSION const std::vector<uint8_t>* peer_public_key_x962;
};

// kPreLinkInfoSteps contains parsing instructions for cbor_extract to convert
// the CBOR-encoded data from Play Services into a `PreLinkInfo`. The format
// that Play Services uses mostly follows the "linking map" structure defined in
// https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#hybrid-qr-initiated
static constexpr StepOrByte<PreLinkInfo> kPreLinkInfoSteps[] = {
    // clang-format off
    ELEMENT(Is::kRequired, PreLinkInfo, contact_id),
    IntKey<PreLinkInfo>(1),
    ELEMENT(Is::kRequired, PreLinkInfo, pairing_id),
    IntKey<PreLinkInfo>(2),
    ELEMENT(Is::kRequired, PreLinkInfo, secret),
    IntKey<PreLinkInfo>(3),
    ELEMENT(Is::kRequired, PreLinkInfo, peer_public_key_x962),
    IntKey<PreLinkInfo>(4),

    Stop<PreLinkInfo>(),
    // clang-format on
};

syncer::DeviceInfo::PhoneAsASecurityKeyInfo::StatusOrInfo
GetSyncDataIfRegisteredInternal() {
  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));

  RegistrationState* state = GetRegistrationState();
  if (!state->have_data_for_sync()) {
    // Not yet ready to provide sync data. When the data is ready,
    // |state| will signal to Sync that something changed and this
    // function will be called again.
    state->SignalSyncWhenReady();
    return syncer::DeviceInfo::PhoneAsASecurityKeyInfo::NotReady();
  }

  if (state->am_in_work_profile()) {
    // Never publish pre-linking information when in a work profile, instead
    // route hybrid requests into the main profile.
    return syncer::DeviceInfo::PhoneAsASecurityKeyInfo::NoSupport();
  }

  if (state->link_data_from_play_services()) {
    std::optional<syncer::DeviceInfo::PhoneAsASecurityKeyInfo> paask_info =
        internal::PaaskInfoFromCBOR(*state->link_data_from_play_services());
    if (paask_info) {
      return *paask_info;
    } else {
      LOG(ERROR)
          << "Failed to parse PaaSK prelink information from Play Services";
    }
  }

  if (!base::FeatureList::IsEnabled(
          device::kWebAuthnEnableAndroidCableAuthenticator) ||
      !state->device_supports_cable()) {
    return syncer::DeviceInfo::PhoneAsASecurityKeyInfo::NoSupport();
  }

  syncer::DeviceInfo::PhoneAsASecurityKeyInfo paask_info;
  paask_info.tunnel_server_domain = device::cablev2::kTunnelServer.value();
  paask_info.contact_id = *state->sync_registration()->contact_id();
  const uint32_t pairing_id = device::cablev2::sync::IDNow();
  paask_info.id = pairing_id;

  std::array<uint8_t, device::cablev2::kPairingIDSize> pairing_id_bytes = {0};
  static_assert(sizeof(pairing_id) <= EXTENT(pairing_id_bytes), "");
  memcpy(pairing_id_bytes.data(), &pairing_id, sizeof(pairing_id));

  paask_info.secret = device::cablev2::Derive<EXTENT(paask_info.secret)>(
      state->secret(), pairing_id_bytes,
      device::cablev2::DerivedValueType::kPairedSecret);

  CHECK_EQ(paask_info.peer_public_key_x962.size(),
           EC_POINT_point2oct(EC_KEY_get0_group(state->identity_key()),
                              EC_KEY_get0_public_key(state->identity_key()),
                              POINT_CONVERSION_UNCOMPRESSED,
                              paask_info.peer_public_key_x962.data(),
                              paask_info.peer_public_key_x962.size(),
                              /*ctx=*/nullptr));

  return paask_info;
}

void SetPrefIfDifferent(PrefService* state,
                        const char* pref_name,
                        const std::string& value) {
  const std::string existing_value = state->GetString(pref_name);
  if (existing_value != value) {
    state->SetString(pref_name, value);
  }
}

}  // namespace

namespace internal {

std::optional<syncer::DeviceInfo::PhoneAsASecurityKeyInfo> PaaskInfoFromCBOR(
    base::span<const uint8_t> cbor) {
  std::optional<cbor::Value> value = cbor::Reader::Read(cbor);
  if (!value || !value->is_map()) {
    return std::nullopt;
  }

  PreLinkInfo info;
  uint64_t pairing_id;
  std::array<uint8_t, 32> secret;
  std::array<uint8_t, 65> peer_public_key_x962;
  if (!device::cbor_extract::Extract<PreLinkInfo>(&info, kPreLinkInfoSteps,
                                                  value->GetMap()) ||
      info.pairing_id->size() != sizeof(pairing_id) ||
      info.secret->size() != secret.size() ||
      info.peer_public_key_x962->size() != peer_public_key_x962.size()) {
    return std::nullopt;
  }
  memcpy(&pairing_id, info.pairing_id->data(), sizeof(pairing_id));

  memcpy(secret.data(), info.secret->data(), secret.size());
  memcpy(peer_public_key_x962.data(), info.peer_public_key_x962->data(),
         peer_public_key_x962.size());

  syncer::DeviceInfo::PhoneAsASecurityKeyInfo paask_info;
  paask_info.tunnel_server_domain = device::cablev2::kTunnelServer.value();
  paask_info.contact_id = std::move(*info.contact_id);
  if (pairing_id > std::numeric_limits<uint32_t>::max()) {
    return std::nullopt;
  }
  paask_info.id = static_cast<uint32_t>(pairing_id);
  paask_info.secret = secret;
  paask_info.peer_public_key_x962 = peer_public_key_x962;
  return paask_info;
}

std::vector<uint8_t> CBORFromPaaskInfo(
    const syncer::DeviceInfo::PhoneAsASecurityKeyInfo& paask_info) {
  cbor::Value::MapValue map;

  map.emplace(1, paask_info.contact_id);

  const uint64_t pairing_id = paask_info.id;
  uint8_t pairing_id_bytes[sizeof(pairing_id)];
  memcpy(pairing_id_bytes, &pairing_id, sizeof(pairing_id));
  map.emplace(2, std::vector<uint8_t>(std::begin(pairing_id_bytes),
                                      std::end(pairing_id_bytes)));

  map.emplace(3, paask_info.secret);

  map.emplace(4,
              std::vector<uint8_t>(std::begin(paask_info.peer_public_key_x962),
                                   std::end(paask_info.peer_public_key_x962)));

  return cbor::Writer::Write(cbor::Value(std::move(map))).value();
}

syncer::DeviceInfo::PhoneAsASecurityKeyInfo::StatusOrInfo CacheResult(
    syncer::DeviceInfo::PhoneAsASecurityKeyInfo::StatusOrInfo result,
    PrefService* state) {
  // kNoSupportString indicates that there is no support for PaaSK. It is
  // distinct from all base64-encoded values so is distinguishable from an
  // encoded `PhoneAsASecurityKeyInfo`.
  constexpr char kNoSupportString[] = ",";

  if (absl::get_if<syncer::DeviceInfo::PhoneAsASecurityKeyInfo::NotReady>(
          &result)) {
    const std::string previous_result_serialized_b64 =
        state->GetString(kSerializedPaaskFieldsName);
    if (previous_result_serialized_b64 == kNoSupportString) {
      return syncer::DeviceInfo::PhoneAsASecurityKeyInfo::NoSupport();
    }

    std::string previous_result_serialized;
    if (previous_result_serialized_b64.empty() ||
        !base::Base64Decode(previous_result_serialized_b64,
                            &previous_result_serialized)) {
      return result;
    }

    std::optional<syncer::DeviceInfo::PhoneAsASecurityKeyInfo> paask_info =
        internal::PaaskInfoFromCBOR(base::as_bytes(
            base::span<const char>(previous_result_serialized.begin(),
                                   previous_result_serialized.end())));
    if (!paask_info) {
      return result;
    }
    return *paask_info;
  } else if (auto* paask_info =
                 absl::get_if<syncer::DeviceInfo::PhoneAsASecurityKeyInfo>(
                     &result)) {
    SetPrefIfDifferent(
        state, kSerializedPaaskFieldsName,
        base::Base64Encode(internal::CBORFromPaaskInfo(*paask_info)));
    return result;
  } else if (absl::get_if<
                 syncer::DeviceInfo::PhoneAsASecurityKeyInfo::NoSupport>(
                 &result)) {
    SetPrefIfDifferent(state, kSerializedPaaskFieldsName, kNoSupportString);
    return result;
  }

  NOTREACHED();
}

}  // namespace internal

void RegisterForCloudMessages() {
  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));

  GetRegistrationState()->Register();
}

void RegisterLocalState(PrefRegistrySimple* registry) {
  registry->RegisterStringPref(kRootSecretPrefName, std::string());
  registry->RegisterStringPref(kSerializedPaaskFieldsName, std::string());
  registry->RegisterStringPref(kWorkProfilePrefName, std::string());
}

syncer::DeviceInfo::PhoneAsASecurityKeyInfo::StatusOrInfo
GetSyncDataIfRegistered() {
  return internal::CacheResult(GetSyncDataIfRegisteredInternal(),
                               g_browser_process->local_state());
}

}  // namespace authenticator
}  // namespace webauthn

using webauthn::authenticator::SystemInterface;

// JNI callbacks.

static jlong JNI_CableAuthenticatorModuleProvider_GetSystemNetworkContext(
    JNIEnv* env) {
  static_assert(sizeof(jlong) >= sizeof(uintptr_t),
                "Java longs are too small to contain pointers");
  return static_cast<jlong>(reinterpret_cast<uintptr_t>(
      SystemNetworkContextManager::GetInstance()->GetContext()));
}

static jlong JNI_CableAuthenticatorModuleProvider_GetRegistration(JNIEnv* env) {
  static_assert(sizeof(jlong) >= sizeof(uintptr_t),
                "Java longs are too small to contain pointers");
  return static_cast<jlong>(reinterpret_cast<uintptr_t>(
      webauthn::authenticator::GetRegistrationState()->linking_registration()));
}

static void JNI_CableAuthenticatorModuleProvider_FreeEvent(JNIEnv* env,
                                                           jlong event_long) {
  static_assert(sizeof(jlong) >= sizeof(uintptr_t),
                "Java longs are too small to contain pointers");
  Registration::Event* event =
      reinterpret_cast<Registration::Event*>(event_long);
  delete event;
}

static base::android::ScopedJavaLocalRef<jbyteArray>
JNI_CableAuthenticatorModuleProvider_GetSecret(JNIEnv* env) {
  return base::android::ToJavaByteArray(
      env, webauthn::authenticator::GetRegistrationState()->secret());
}

static void JNI_CableAuthenticatorModuleProvider_OnHaveLinkingInformation(
    JNIEnv* env,
    jlong system_interface_pointer,
    const base::android::JavaParamRef<jbyteArray>& cbor_java) {
  std::optional<std::vector<uint8_t>> optional_cbor;

  if (cbor_java) {
    std::vector<uint8_t> cbor;
    base::android::JavaByteArrayToByteVector(env, cbor_java, &cbor);
    optional_cbor = std::move(cbor);
  }

  content::BrowserThread::GetTaskRunnerForThread(content::BrowserThread::UI)
      ->PostTask(
          FROM_HERE,
          base::BindOnce(&SystemInterface::OnHavePlayServicesLinkingInformation,
                         base::Unretained(reinterpret_cast<SystemInterface*>(
                             static_cast<uintptr_t>(system_interface_pointer))),
                         std::move(optional_cbor)));
}

static void JNI_CableAuthenticatorModuleProvider_OnHaveWorkProfileResult(
    JNIEnv* env,
    jlong system_interface_pointer,
    jboolean in_work_profile) {
  content::BrowserThread::GetTaskRunnerForThread(content::BrowserThread::UI)
      ->PostTask(
          FROM_HERE,
          base::BindOnce(&SystemInterface::OnHaveWorkProfileResult,
                         base::Unretained(reinterpret_cast<SystemInterface*>(
                             static_cast<uintptr_t>(system_interface_pointer))),
                         in_work_profile));
}

static void JNI_PrivacySettingsFragment_RevokeAllLinkedDevices(JNIEnv* env) {
  // Invalidates the current cloud messaging (GCM) token and creates a new one.
  // This causes the tunnel server to reject connection attempts with a 410
  // (Gone) error. Since linking keys are derived from the root secret by using
  // the GCM token, this also invalidates all existing linking keys.
  webauthn::authenticator::GetRegistrationState()
      ->linking_registration()
      ->RotateContactID();
}