blob: 54cfc1f041d750b20abc110abefae64b11ce6ced [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/webauthn/android/cable_module_android.h"
#include "base/android/jni_array.h"
#include "base/base64.h"
#include "base/bind.h"
#include "base/no_destructor.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 "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 "crypto/random.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/features.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/obj.h"
// This "header" actually contains function definitions and thus can only be
// included once across Chromium.
#include "chrome/browser/webauthn/android/jni_headers/CableAuthenticatorModuleProvider_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";
// RegistrationState is a singleton object that loads an install-wide secret at
// startup and holds two FCM registrations. One registration, the "linking"
// registration, is used when the user links with another device by scanning a
// QR code. The second is advertised via Sync for other devices signed into the
// same account. The reason for having two registrations is that the linking
// registration can be rotated if the user wishes to unlink all QR-linked
// devices. But we don't want to break synced peers when that happens. Instead,
// for synced peers we require that they have received a recent sync status from
// this device, i.e. we rotate them automatically.
class RegistrationState {
public:
void Register() {
DCHECK(!linking_registration_);
DCHECK(!sync_registration_);
instance_id::InstanceIDDriver* const driver =
instance_id::InstanceIDProfileServiceFactory::GetForProfile(
g_browser_process->profile_manager()->GetPrimaryUserProfile())
->driver();
linking_registration_ = device::cablev2::authenticator::Register(
driver, device::cablev2::authenticator::Registration::Type::LINKING,
base::BindOnce(&RegistrationState::OnLinkingRegistrationReady,
base::Unretained(this)),
base::BindRepeating(&RegistrationState::OnEvent,
base::Unretained(this)));
sync_registration_ = device::cablev2::authenticator::Register(
driver, device::cablev2::authenticator::Registration::Type::SYNC,
base::BindOnce(&RegistrationState::OnSyncRegistrationReady,
base::Unretained(this)),
base::BindRepeating(&RegistrationState::OnEvent,
base::Unretained(this)));
PrefService* const local_state = g_browser_process->local_state();
std::string secret_base64 = local_state->GetString(kRootSecretPrefName);
if (!secret_base64.empty()) {
std::string secret_str;
if (base::Base64Decode(secret_base64, &secret_str) &&
secret_str.size() == secret_.size()) {
memcpy(secret_.data(), secret_str.data(), secret_.size());
} else {
secret_base64.clear();
}
}
if (secret_base64.empty()) {
crypto::RandBytes(secret_);
local_state->SetString(kRootSecretPrefName, base::Base64Encode(secret_));
}
}
bool is_registered_for_linking() const {
return linking_registration_ != nullptr;
}
bool is_registered_for_sync() const { return sync_registration_ != nullptr; }
Registration* linking_registration() const {
return linking_registration_.get();
}
Registration* sync_registration() const { return sync_registration_.get(); }
const std::array<uint8_t, 32>& secret() const { return secret_; }
// have_data_for_sync returns true if this object has loaded enough state to
// put information into sync's DeviceInfo.
bool have_data_for_sync() const {
return sync_registration_ != nullptr && sync_registration_->contact_id();
}
void SignalSyncWhenReady() {
if (sync_registration_ && !sync_registration_->contact_id()) {
sync_registration_->PrepareContactID();
}
signal_sync_when_ready_ = true;
}
private:
void OnLinkingRegistrationReady() { MaybeFlushPendingEvent(); }
void OnSyncRegistrationReady() { MaybeSignalSync(); }
// OnEvent is called when a GCM message is received.
void OnEvent(std::unique_ptr<Registration::Event> event) {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
pending_event_ = std::move(event);
MaybeFlushPendingEvent();
}
void MaybeFlushPendingEvent() {
if (!pending_event_) {
return;
}
if (pending_event_->source == Registration::Type::LINKING &&
!linking_registration_->contact_id()) {
// This GCM message is from a QR-linked peer so it needs the contact ID to
// be processed, but that contact ID isn't ready yet.
linking_registration_->PrepareContactID();
return;
}
Java_CableAuthenticatorModuleProvider_onCloudMessage(
base::android::AttachCurrentThread(),
static_cast<jlong>(
reinterpret_cast<uintptr_t>(pending_event_.release())));
}
// MaybeSignalSync prompts the Sync system to refresh local-device data if
// the Sync data is now ready and |signal_sync_when_ready_| has been set to
// indicate that the Sync data was not available last time Sync queried it.
void MaybeSignalSync() {
if (!signal_sync_when_ready_ || !have_data_for_sync()) {
return;
}
signal_sync_when_ready_ = false;
DeviceInfoSyncServiceFactory::GetForProfile(
ProfileManager::GetPrimaryUserProfile())
->RefreshLocalDeviceInfo();
}
std::unique_ptr<Registration> linking_registration_;
std::unique_ptr<Registration> sync_registration_;
std::array<uint8_t, 32> secret_;
std::unique_ptr<Registration::Event> pending_event_;
bool signal_sync_when_ready_ = false;
};
RegistrationState* GetRegistrationState() {
static base::NoDestructor<RegistrationState> state;
return state.get();
}
} // namespace
void RegisterForCloudMessages() {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
if (!base::FeatureList::IsEnabled(device::kWebAuthCableSecondFactor) &&
!base::FeatureList::IsEnabled(device::kWebAuthPhoneSupport)) {
return;
}
GetRegistrationState()->Register();
}
void RegisterLocalState(PrefRegistrySimple* registry) {
registry->RegisterStringPref(kRootSecretPrefName, std::string());
}
base::Optional<syncer::DeviceInfo::PhoneAsASecurityKeyInfo>
GetSyncDataIfRegistered() {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
if (!base::FeatureList::IsEnabled(device::kWebAuthCableSecondFactor) ||
!Java_CableAuthenticatorModuleProvider_canDeviceSupportCable(
base::android::AttachCurrentThread())) {
return base::nullopt;
}
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 base::nullopt;
}
syncer::DeviceInfo::PhoneAsASecurityKeyInfo paask_info;
paask_info.tunnel_server_domain = device::cablev2::kTunnelServer;
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);
bssl::UniquePtr<EC_KEY> identity_key =
device::cablev2::IdentityKey(state->secret());
CHECK_EQ(paask_info.peer_public_key_x962.size(),
EC_POINT_point2oct(EC_KEY_get0_group(identity_key.get()),
EC_KEY_get0_public_key(identity_key.get()),
POINT_CONVERSION_UNCOMPRESSED,
paask_info.peer_public_key_x962.data(),
paask_info.peer_public_key_x962.size(),
/*ctx=*/nullptr));
return paask_info;
}
} // namespace authenticator
} // namespace webauthn
// 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());
}