blob: d1dac22ede569a70e6301aa7f1ce5aca520c320e [file] [log] [blame]
// Copyright 2019 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/ui/webui/settings/settings_security_key_handler.h"
#include <algorithm>
#include <string>
#include <utility>
#include <vector>
#include "base/base64.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/containers/contains.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/webui/settings/settings_page_ui_handler.h"
#include "chrome/browser/ui/webui/settings/settings_security_key_handler.h"
#include "chrome/browser/webauthn/cablev2_devices.h"
#include "chrome/grit/generated_resources.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui.h"
#include "device/fido/cable/cable_discovery_data.h"
#include "device/fido/credential_management.h"
#include "device/fido/fido_constants.h"
#include "device/fido/pin.h"
#include "device/fido/public_key_credential_descriptor.h"
#include "device/fido/public_key_credential_user_entity.h"
#include "device/fido/reset_request_handler.h"
#include "device/fido/set_pin_request_handler.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/icu/source/common/unicode/locid.h"
#include "ui/base/l10n/l10n_util.h"
using content::BrowserThread;
namespace {
base::flat_set<device::FidoTransportProtocol> supported_transports() {
// If we ever support BLE devices then additional thought will be required
// in the UI; therefore don't enable them here. NFC is not supported on
// desktop thus only USB devices remain to be enabled.
return {device::FidoTransportProtocol::kUsbHumanInterfaceDevice};
}
void HandleClose(base::RepeatingClosure close_callback,
const base::Value::List& args) {
DCHECK_EQ(0u, args.size());
close_callback.Run();
}
base::Value::Dict EncodeEnrollment(const std::vector<uint8_t>& id,
const std::string& name) {
base::Value::Dict value;
value.Set("name", name);
value.Set("id", base::HexEncode(id.data(), id.size()));
return value;
}
bool DecodePublicKey(const std::string& value,
std::array<uint8_t, device::kP256X962Length>* out) {
std::string bytes;
if (!base::Base64Decode(value, &bytes) || bytes.size() != out->size()) {
return false;
}
std::copy(bytes.begin(), bytes.end(), out->begin());
return true;
}
} // namespace
namespace settings {
SecurityKeysHandlerBase::SecurityKeysHandlerBase() = default;
SecurityKeysHandlerBase::SecurityKeysHandlerBase(
std::unique_ptr<device::FidoDiscoveryFactory> discovery_factory)
: discovery_factory_(std::move(discovery_factory)) {}
SecurityKeysHandlerBase::~SecurityKeysHandlerBase() = default;
void SecurityKeysHandlerBase::OnJavascriptAllowed() {}
void SecurityKeysHandlerBase::OnJavascriptDisallowed() {
// If Javascript is disallowed, |Close| will invalidate all current WeakPtrs
// and thus drop all pending callbacks. This means that
// |IsJavascriptAllowed| doesn't need to be tested before each callback
// because, if the callback into this object happened, then Javascript is
// allowed.
Close();
}
SecurityKeysPINHandler::SecurityKeysPINHandler() = default;
SecurityKeysPINHandler::~SecurityKeysPINHandler() = default;
void SecurityKeysPINHandler::RegisterMessages() {
web_ui()->RegisterMessageCallback(
"securityKeyStartSetPIN",
base::BindRepeating(&SecurityKeysPINHandler::HandleStartSetPIN,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"securityKeySetPIN",
base::BindRepeating(&SecurityKeysPINHandler::HandleSetPIN,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"securityKeyPINClose",
base::BindRepeating(&HandleClose,
base::BindRepeating(&SecurityKeysPINHandler::Close,
base::Unretained(this))));
}
void SecurityKeysPINHandler::Close() {
// Invalidate all existing WeakPtrs so that no stale callbacks occur.
weak_factory_.InvalidateWeakPtrs();
state_ = State::kNone;
set_pin_.reset();
callback_id_.clear();
}
void SecurityKeysPINHandler::HandleStartSetPIN(const base::Value::List& args) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(State::kNone, state_);
DCHECK_EQ(1u, args.size());
AllowJavascript();
DCHECK(callback_id_.empty());
callback_id_ = args[0].GetString();
state_ = State::kStartSetPIN;
set_pin_ = std::make_unique<device::SetPINRequestHandler>(
supported_transports(),
base::BindOnce(&SecurityKeysPINHandler::OnGatherPIN,
weak_factory_.GetWeakPtr()),
base::BindRepeating(&SecurityKeysPINHandler::OnSetPINComplete,
weak_factory_.GetWeakPtr()));
}
void SecurityKeysPINHandler::OnGatherPIN(uint32_t current_min_pin_length,
uint32_t new_min_pin_length,
absl::optional<int64_t> num_retries) {
DCHECK_EQ(State::kStartSetPIN, state_);
DCHECK_CURRENTLY_ON(BrowserThread::UI);
base::Value::Dict response;
response.Set("done", false);
response.Set("error", base::Value());
response.Set("currentMinPinLength", static_cast<int>(current_min_pin_length));
response.Set("newMinPinLength", static_cast<int>(new_min_pin_length));
if (num_retries) {
state_ = State::kGatherChangePIN;
response.Set("retries", static_cast<int>(*num_retries));
} else {
state_ = State::kGatherNewPIN;
response.Set("retries", base::Value());
}
ResolveJavascriptCallback(base::Value(std::move(callback_id_)),
base::Value(std::move(response)));
}
void SecurityKeysPINHandler::OnSetPINComplete(
device::CtapDeviceResponseCode code) {
DCHECK(state_ == State::kStartSetPIN || state_ == State::kSettingPIN);
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (code == device::CtapDeviceResponseCode::kCtap2ErrPinInvalid) {
// In the event that the old PIN was incorrect, the UI may prompt again.
state_ = State::kGatherChangePIN;
} else {
state_ = State::kNone;
set_pin_.reset();
}
base::Value::Dict response;
response.Set("done", true);
response.Set("error", static_cast<int>(code));
ResolveJavascriptCallback(base::Value(std::move(callback_id_)),
base::Value(std::move(response)));
}
void SecurityKeysPINHandler::HandleSetPIN(const base::Value::List& args) {
DCHECK(state_ == State::kGatherNewPIN || state_ == State::kGatherChangePIN);
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(3u, args.size());
DCHECK(callback_id_.empty());
callback_id_ = args[0].GetString();
const std::string old_pin = args[1].GetString();
const std::string new_pin = args[2].GetString();
DCHECK((state_ == State::kGatherNewPIN) == old_pin.empty());
CHECK_EQ(device::pin::ValidatePIN(new_pin),
device::pin::PINEntryError::kNoError);
state_ = State::kSettingPIN;
set_pin_->ProvidePIN(old_pin, new_pin);
}
SecurityKeysResetHandler::SecurityKeysResetHandler() = default;
SecurityKeysResetHandler::~SecurityKeysResetHandler() = default;
void SecurityKeysResetHandler::RegisterMessages() {
web_ui()->RegisterMessageCallback(
"securityKeyReset",
base::BindRepeating(&SecurityKeysResetHandler::HandleReset,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"securityKeyCompleteReset",
base::BindRepeating(&SecurityKeysResetHandler::HandleCompleteReset,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"securityKeyResetClose",
base::BindRepeating(&HandleClose,
base::BindRepeating(&SecurityKeysResetHandler::Close,
base::Unretained(this))));
}
void SecurityKeysResetHandler::Close() {
// Invalidate all existing WeakPtrs so that no stale callbacks occur.
weak_factory_.InvalidateWeakPtrs();
state_ = State::kNone;
reset_.reset();
callback_id_.clear();
}
void SecurityKeysResetHandler::HandleReset(const base::Value::List& args) {
DCHECK_EQ(State::kNone, state_);
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(1u, args.size());
AllowJavascript();
DCHECK(callback_id_.empty());
callback_id_ = args[0].GetString();
state_ = State::kStartReset;
reset_ = std::make_unique<device::ResetRequestHandler>(
supported_transports(),
base::BindOnce(&SecurityKeysResetHandler::OnResetSent,
weak_factory_.GetWeakPtr()),
base::BindOnce(&SecurityKeysResetHandler::OnResetFinished,
weak_factory_.GetWeakPtr()));
}
void SecurityKeysResetHandler::OnResetSent() {
DCHECK_EQ(State::kStartReset, state_);
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// A reset message has been sent to a security key and it may complete
// before Javascript asks for the result. Therefore |HandleCompleteReset|
// and |OnResetFinished| may be called in either order.
state_ = State::kWaitingForResetNoCallbackYet;
ResolveJavascriptCallback(base::Value(std::move(callback_id_)),
base::Value(0 /* success */));
}
void SecurityKeysResetHandler::HandleCompleteReset(
const base::Value::List& args) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(1u, args.size());
DCHECK(callback_id_.empty());
callback_id_ = args[0].GetString();
switch (state_) {
case State::kWaitingForResetNoCallbackYet:
// The reset operation hasn't completed. |callback_id_| will be used in
// |OnResetFinished| when it does.
state_ = State::kWaitingForResetHaveCallback;
break;
case State::kWaitingForCompleteReset:
// The reset operation has completed and we were waiting for this
// call from Javascript in order to provide the result.
state_ = State::kNone;
ResolveJavascriptCallback(base::Value(std::move(callback_id_)),
base::Value(static_cast<int>(*reset_result_)));
reset_.reset();
break;
default:
NOTREACHED();
}
}
void SecurityKeysResetHandler::OnResetFinished(
device::CtapDeviceResponseCode result) {
switch (state_) {
case State::kWaitingForResetNoCallbackYet:
// The reset operation has completed, but Javascript hasn't called
// |CompleteReset| so we cannot make the callback yet.
state_ = State::kWaitingForCompleteReset;
reset_result_ = result;
break;
case State::kStartReset:
// The reset operation failed immediately, probably because the user
// selected a U2F device. |callback_id_| has been set by |Reset|.
[[fallthrough]];
case State::kWaitingForResetHaveCallback:
// The |CompleteReset| call has already provided |callback_id_| so the
// reset can be completed immediately.
state_ = State::kNone;
ResolveJavascriptCallback(base::Value(std::move(callback_id_)),
base::Value(static_cast<int>(result)));
reset_.reset();
break;
default:
NOTREACHED();
}
}
SecurityKeysCredentialHandler::SecurityKeysCredentialHandler() = default;
SecurityKeysCredentialHandler::SecurityKeysCredentialHandler(
std::unique_ptr<device::FidoDiscoveryFactory> discovery_factory)
: SecurityKeysHandlerBase(std::move(discovery_factory)) {}
SecurityKeysCredentialHandler::~SecurityKeysCredentialHandler() = default;
void SecurityKeysCredentialHandler::HandleStart(const base::Value::List& args) {
DCHECK_EQ(State::kNone, state_);
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(1u, args.size());
DCHECK(!credential_management_);
AllowJavascript();
DCHECK(callback_id_.empty());
callback_id_ = args[0].GetString();
state_ = State::kStart;
credential_management_ =
std::make_unique<device::CredentialManagementHandler>(
discovery_factory(), supported_transports(),
base::BindOnce(
&SecurityKeysCredentialHandler::OnCredentialManagementReady,
weak_factory_.GetWeakPtr()),
base::BindRepeating(&SecurityKeysCredentialHandler::OnGatherPIN,
weak_factory_.GetWeakPtr()),
base::BindOnce(&SecurityKeysCredentialHandler::OnFinished,
weak_factory_.GetWeakPtr()));
}
void SecurityKeysCredentialHandler::RegisterMessages() {
web_ui()->RegisterMessageCallback(
"securityKeyCredentialManagementStart",
base::BindRepeating(&SecurityKeysCredentialHandler::HandleStart,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"securityKeyCredentialManagementPIN",
base::BindRepeating(&SecurityKeysCredentialHandler::HandlePIN,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"securityKeyCredentialManagementEnumerate",
base::BindRepeating(&SecurityKeysCredentialHandler::HandleEnumerate,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"securityKeyCredentialManagementDelete",
base::BindRepeating(&SecurityKeysCredentialHandler::HandleDelete,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"securityKeyCredentialManagementUpdate",
base::BindRepeating(
&SecurityKeysCredentialHandler::HandleUpdateUserInformation,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"securityKeyCredentialManagementClose",
base::BindRepeating(
&HandleClose,
base::BindRepeating(&SecurityKeysCredentialHandler::Close,
base::Unretained(this))));
}
void SecurityKeysCredentialHandler::Close() {
// Invalidate all existing WeakPtrs so that no stale callbacks occur.
weak_factory_.InvalidateWeakPtrs();
state_ = State::kNone;
credential_management_.reset();
callback_id_.clear();
credential_management_provide_pin_cb_.Reset();
DCHECK(!credential_management_provide_pin_cb_);
}
void SecurityKeysCredentialHandler::HandlePIN(const base::Value::List& args) {
DCHECK_EQ(State::kPIN, state_);
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(2u, args.size());
DCHECK(credential_management_);
DCHECK(credential_management_provide_pin_cb_);
DCHECK(callback_id_.empty());
callback_id_ = args[0].GetString();
std::string pin = args[1].GetString();
std::move(credential_management_provide_pin_cb_).Run(pin);
}
void SecurityKeysCredentialHandler::HandleEnumerate(
const base::Value::List& args) {
DCHECK_EQ(state_, State::kReady);
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(1u, args.size());
DCHECK(credential_management_);
DCHECK(callback_id_.empty());
state_ = State::kGettingCredentials;
callback_id_ = args[0].GetString();
credential_management_->GetCredentials(
base::BindOnce(&SecurityKeysCredentialHandler::OnHaveCredentials,
weak_factory_.GetWeakPtr()));
}
void SecurityKeysCredentialHandler::HandleDelete(
const base::Value::List& args) {
DCHECK_EQ(State::kReady, state_);
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(2u, args.size());
DCHECK(credential_management_);
DCHECK(callback_id_.empty());
state_ = State::kDeletingCredentials;
callback_id_ = args[0].GetString();
std::vector<device::PublicKeyCredentialDescriptor> credential_ids;
for (const base::Value& el : args[1].GetList()) {
std::vector<uint8_t> credential_id_bytes;
if (!base::HexStringToBytes(el.GetString(), &credential_id_bytes)) {
NOTREACHED();
continue;
}
device::PublicKeyCredentialDescriptor credential_id(
device::CredentialType::kPublicKey, std::move(credential_id_bytes));
credential_ids.emplace_back(std::move(credential_id));
}
credential_management_->DeleteCredentials(
std::move(credential_ids),
base::BindOnce(&SecurityKeysCredentialHandler::OnCredentialsDeleted,
weak_factory_.GetWeakPtr()));
}
void SecurityKeysCredentialHandler::HandleUpdateUserInformation(
const base::Value::List& args) {
DCHECK_EQ(State::kReady, state_);
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(5u, args.size());
DCHECK(credential_management_);
DCHECK(callback_id_.empty());
state_ = State::kUpdatingUserInformation;
callback_id_ = args[0].GetString();
std::vector<uint8_t> credential_id_bytes;
if (!base::HexStringToBytes(args[1].GetString(), &credential_id_bytes)) {
NOTREACHED();
}
device::PublicKeyCredentialDescriptor credential_id(
device::CredentialType::kPublicKey, credential_id_bytes);
std::vector<uint8_t> user_handle;
if (!base::HexStringToBytes(args[2].GetString(), &user_handle)) {
NOTREACHED();
}
std::string new_username = args[3].GetString();
std::string new_displayname = args[4].GetString();
device::PublicKeyCredentialUserEntity updated_user(
std::move(user_handle), std::move(new_username),
std::move(new_displayname), absl::nullopt);
credential_management_->UpdateUserInformation(
std::move(credential_id), std::move(updated_user),
base::BindOnce(&SecurityKeysCredentialHandler::OnUserInformationUpdated,
weak_factory_.GetWeakPtr()));
}
void SecurityKeysCredentialHandler::OnCredentialManagementReady() {
DCHECK(state_ == State::kStart || state_ == State::kPIN);
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(credential_management_);
DCHECK(!callback_id_.empty());
state_ = State::kReady;
ResolveJavascriptCallback(base::Value(std::move(callback_id_)),
base::Value());
}
void SecurityKeysCredentialHandler::OnHaveCredentials(
device::CtapDeviceResponseCode status,
absl::optional<std::vector<device::AggregatedEnumerateCredentialsResponse>>
responses,
absl::optional<size_t> remaining_credentials) {
DCHECK_EQ(State::kGettingCredentials, state_);
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(credential_management_);
DCHECK(!callback_id_.empty());
if (status == device::CtapDeviceResponseCode::kCtap2ErrNoCredentials) {
OnFinished(device::CredentialManagementStatus::kNoCredentials);
return;
}
if (status != device::CtapDeviceResponseCode::kSuccess) {
OnFinished(
device::CredentialManagementStatus::kAuthenticatorResponseInvalid);
return;
}
DCHECK(responses);
DCHECK(remaining_credentials);
state_ = State::kReady;
base::Value::List credentials;
for (const auto& response : *responses) {
for (const auto& credential : response.credentials) {
base::Value::Dict credential_dict;
std::string credential_id = base::HexEncode(credential.credential_id.id);
if (credential_id.empty()) {
NOTREACHED();
continue;
}
std::string userHandle = base::HexEncode(credential.user.id);
credential_dict.Set("credentialId", std::move(credential_id));
credential_dict.Set("relyingPartyId", response.rp.id);
credential_dict.Set("userHandle", std::move(userHandle));
credential_dict.Set("userName", credential.user.name.value_or(""));
credential_dict.Set("userDisplayName",
credential.user.display_name.value_or(""));
credentials.Append(std::move(credential_dict));
}
}
ResolveJavascriptCallback(base::Value(std::move(callback_id_)),
base::Value(std::move(credentials)));
}
void SecurityKeysCredentialHandler::OnGatherPIN(
device::CredentialManagementHandler::AuthenticatorProperties
authenticator_properties,
base::OnceCallback<void(std::string)> callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(!callback_id_.empty());
DCHECK(!credential_management_provide_pin_cb_);
credential_management_provide_pin_cb_ = std::move(callback);
if (state_ == State::kStart) {
// Resolve the promise to startCredentialManagement().
base::Value::Dict response;
response.Set("minPinLength",
static_cast<int>(authenticator_properties.min_pin_length));
response.Set("supportsUpdateUserInformation",
authenticator_properties.supports_update_user_information);
state_ = State::kPIN;
ResolveJavascriptCallback(base::Value(std::move(callback_id_)),
base::Value(std::move(response)));
return;
}
// Resolve the promise to credentialManagementProvidePIN().
DCHECK_EQ(state_, State::kPIN);
base::Value::List response;
response.Append(static_cast<int>(authenticator_properties.min_pin_length));
response.Append(static_cast<int>(authenticator_properties.pin_retries));
ResolveJavascriptCallback(base::Value(std::move(callback_id_)),
base::Value(std::move(response)));
}
void SecurityKeysCredentialHandler::OnCredentialsDeleted(
device::CtapDeviceResponseCode status) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(State::kDeletingCredentials, state_);
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(credential_management_);
DCHECK(!callback_id_.empty());
state_ = State::kReady;
base::Value::Dict response;
response.Set("success", status == device::CtapDeviceResponseCode::kSuccess);
response.Set(
"message",
l10n_util::GetStringUTF8(
status == device::CtapDeviceResponseCode::kSuccess
? IDS_SETTINGS_SECURITY_KEYS_CREDENTIAL_MANAGEMENT_DELETE_SUCCESS
: IDS_SETTINGS_SECURITY_KEYS_CREDENTIAL_MANAGEMENT_DELETE_FAILED));
ResolveJavascriptCallback(base::Value(std::move(callback_id_)),
base::Value(std::move(response)));
}
void SecurityKeysCredentialHandler::OnUserInformationUpdated(
device::CtapDeviceResponseCode status) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(State::kUpdatingUserInformation, state_);
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(credential_management_);
DCHECK(!callback_id_.empty());
state_ = State::kReady;
base::Value::Dict response;
response.Set("success", status == device::CtapDeviceResponseCode::kSuccess);
response.Set(
"message",
l10n_util::GetStringUTF8(
status == device::CtapDeviceResponseCode::kSuccess
? IDS_SETTINGS_SECURITY_KEYS_CREDENTIAL_MANAGEMENT_UPDATE_SUCCESS
: IDS_SETTINGS_SECURITY_KEYS_CREDENTIAL_MANAGEMENT_UPDATE_FAILED));
ResolveJavascriptCallback(base::Value(std::move(callback_id_)),
base::Value(std::move(response)));
}
void SecurityKeysCredentialHandler::OnFinished(
device::CredentialManagementStatus status) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
int error;
bool requires_pin_change = false;
switch (status) {
case device::CredentialManagementStatus::kSoftPINBlock:
error = IDS_SETTINGS_SECURITY_KEYS_PIN_SOFT_LOCK;
break;
case device::CredentialManagementStatus::kHardPINBlock:
error = IDS_SETTINGS_SECURITY_KEYS_PIN_HARD_LOCK;
break;
case device::CredentialManagementStatus::kNoCredentials:
error = IDS_SETTINGS_SECURITY_KEYS_CREDENTIAL_MANAGEMENT_NO_CREDENTIALS;
break;
case device::CredentialManagementStatus::
kAuthenticatorMissingCredentialManagement:
error = IDS_SETTINGS_SECURITY_KEYS_NO_CREDENTIAL_MANAGEMENT;
break;
case device::CredentialManagementStatus::kNoPINSet:
requires_pin_change = true;
error = IDS_SETTINGS_SECURITY_KEYS_CREDENTIAL_MANAGEMENT_NO_PIN;
break;
case device::CredentialManagementStatus::kAuthenticatorResponseInvalid:
error = IDS_SETTINGS_SECURITY_KEYS_CREDENTIAL_MANAGEMENT_ERROR;
break;
case device::CredentialManagementStatus::kForcePINChange:
requires_pin_change = true;
error = IDS_SETTINGS_SECURITY_KEYS_FORCE_PIN_CHANGE;
break;
case device::CredentialManagementStatus::kSuccess:
error = IDS_SETTINGS_SECURITY_KEYS_CREDENTIAL_MANAGEMENT_REMOVED;
break;
}
FireWebUIListener("security-keys-credential-management-finished",
base::Value(l10n_util::GetStringUTF8(error)),
base::Value(requires_pin_change));
}
SecurityKeysBioEnrollmentHandler::SecurityKeysBioEnrollmentHandler() = default;
SecurityKeysBioEnrollmentHandler::SecurityKeysBioEnrollmentHandler(
std::unique_ptr<device::FidoDiscoveryFactory> discovery_factory)
: SecurityKeysHandlerBase(std::move(discovery_factory)) {}
SecurityKeysBioEnrollmentHandler::~SecurityKeysBioEnrollmentHandler() = default;
void SecurityKeysBioEnrollmentHandler::HandleStart(
const base::Value::List& args) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(state_, State::kNone);
DCHECK_EQ(1u, args.size());
DCHECK(callback_id_.empty());
AllowJavascript();
state_ = State::kStart;
callback_id_ = args[0].GetString();
bio_ = std::make_unique<device::BioEnrollmentHandler>(
supported_transports(),
base::BindOnce(&SecurityKeysBioEnrollmentHandler::OnReady,
weak_factory_.GetWeakPtr()),
base::BindOnce(&SecurityKeysBioEnrollmentHandler::OnError,
weak_factory_.GetWeakPtr()),
base::BindRepeating(&SecurityKeysBioEnrollmentHandler::OnGatherPIN,
weak_factory_.GetWeakPtr()),
discovery_factory());
}
void SecurityKeysBioEnrollmentHandler::RegisterMessages() {
web_ui()->RegisterMessageCallback(
"securityKeyBioEnrollStart",
base::BindRepeating(&SecurityKeysBioEnrollmentHandler::HandleStart,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"securityKeyBioEnrollProvidePIN",
base::BindRepeating(&SecurityKeysBioEnrollmentHandler::HandleProvidePIN,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"securityKeyBioEnrollGetSensorInfo",
base::BindRepeating(
&SecurityKeysBioEnrollmentHandler::HandleGetSensorInfo,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"securityKeyBioEnrollEnumerate",
base::BindRepeating(&SecurityKeysBioEnrollmentHandler::HandleEnumerate,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"securityKeyBioEnrollStartEnrolling",
base::BindRepeating(
&SecurityKeysBioEnrollmentHandler::HandleStartEnrolling,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"securityKeyBioEnrollDelete",
base::BindRepeating(&SecurityKeysBioEnrollmentHandler::HandleDelete,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"securityKeyBioEnrollRename",
base::BindRepeating(&SecurityKeysBioEnrollmentHandler::HandleRename,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"securityKeyBioEnrollCancel",
base::BindRepeating(&SecurityKeysBioEnrollmentHandler::HandleCancel,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"securityKeyBioEnrollClose",
base::BindRepeating(
&HandleClose,
base::BindRepeating(&SecurityKeysBioEnrollmentHandler::Close,
base::Unretained(this))));
}
void SecurityKeysBioEnrollmentHandler::Close() {
weak_factory_.InvalidateWeakPtrs();
state_ = State::kNone;
bio_.reset();
callback_id_.clear();
provide_pin_cb_.Reset();
}
void SecurityKeysBioEnrollmentHandler::OnReady(
device::BioEnrollmentHandler::SensorInfo sensor_info) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(bio_);
DCHECK_EQ(state_, State::kGatherPIN);
DCHECK(!callback_id_.empty());
state_ = State::kReady;
sensor_info_ = std::move(sensor_info);
ResolveJavascriptCallback(base::Value(std::move(callback_id_)),
base::Value());
}
void SecurityKeysBioEnrollmentHandler::OnError(
device::BioEnrollmentHandler::Error error) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
state_ = State::kNone;
int error_message;
bool requires_pin_change = false;
using Error = device::BioEnrollmentHandler::Error;
switch (error) {
case Error::kAuthenticatorRemoved:
error_message = IDS_SETTINGS_SECURITY_KEYS_CREDENTIAL_MANAGEMENT_REMOVED;
break;
case Error::kSoftPINBlock:
error_message = IDS_SETTINGS_SECURITY_KEYS_PIN_SOFT_LOCK;
break;
case Error::kHardPINBlock:
error_message = IDS_SETTINGS_SECURITY_KEYS_PIN_HARD_LOCK;
break;
case Error::kAuthenticatorMissingBioEnrollment:
error_message = IDS_SETTINGS_SECURITY_KEYS_NO_BIOMETRIC_ENROLLMENT;
break;
case Error::kNoPINSet:
requires_pin_change = true;
error_message = IDS_SETTINGS_SECURITY_KEYS_BIO_NO_PIN;
break;
case Error::kAuthenticatorResponseInvalid:
error_message = IDS_SETTINGS_SECURITY_KEYS_CREDENTIAL_MANAGEMENT_ERROR;
break;
case Error::kForcePINChange:
requires_pin_change = true;
error_message = IDS_SETTINGS_SECURITY_KEYS_FORCE_PIN_CHANGE;
break;
}
FireWebUIListener("security-keys-bio-enroll-error",
base::Value(l10n_util::GetStringUTF8(error_message)),
base::Value(requires_pin_change));
// If |callback_id_| is not empty, there is an ongoing operation,
// which means there is an unresolved Promise. Reject it so that
// it isn't leaked.
if (!callback_id_.empty()) {
RejectJavascriptCallback(base::Value(std::move(callback_id_)),
base::Value());
}
}
void SecurityKeysBioEnrollmentHandler::OnGatherPIN(
uint32_t min_pin_length,
int64_t retries,
base::OnceCallback<void(std::string)> cb) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(!callback_id_.empty());
DCHECK(state_ == State::kStart || state_ == State::kGatherPIN);
state_ = State::kGatherPIN;
provide_pin_cb_ = std::move(cb);
base::Value::List response;
response.Append(static_cast<int>(min_pin_length));
response.Append(static_cast<int>(retries));
ResolveJavascriptCallback(base::Value(std::move(callback_id_)),
base::Value(std::move(response)));
}
void SecurityKeysBioEnrollmentHandler::HandleProvidePIN(
const base::Value::List& args) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(2u, args.size());
DCHECK_EQ(state_, State::kGatherPIN);
state_ = State::kGatherPIN;
callback_id_ = args[0].GetString();
std::move(provide_pin_cb_).Run(args[1].GetString());
}
void SecurityKeysBioEnrollmentHandler::HandleGetSensorInfo(
const base::Value::List& args) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(1u, args.size());
DCHECK_EQ(state_, State::kReady);
base::Value::Dict response;
response.Set("maxTemplateFriendlyName",
static_cast<int>(sensor_info_.max_template_friendly_name));
if (sensor_info_.max_samples_for_enroll) {
response.Set("maxSamplesForEnroll", *sensor_info_.max_samples_for_enroll);
}
ResolveJavascriptCallback(base::Value(std::move(args[0].GetString())),
base::Value(std::move(response)));
}
void SecurityKeysBioEnrollmentHandler::HandleEnumerate(
const base::Value::List& args) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(1u, args.size());
DCHECK_EQ(state_, State::kReady);
state_ = State::kEnumerating;
callback_id_ = args[0].GetString();
bio_->EnumerateTemplates(
base::BindOnce(&SecurityKeysBioEnrollmentHandler::OnHaveEnumeration,
weak_factory_.GetWeakPtr()));
}
void SecurityKeysBioEnrollmentHandler::OnHaveEnumeration(
device::CtapDeviceResponseCode code,
absl::optional<std::map<std::vector<uint8_t>, std::string>> enrollments) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(!callback_id_.empty());
DCHECK_EQ(state_, State::kEnumerating);
base::Value::List list;
if (enrollments) {
for (const auto& enrollment : *enrollments) {
list.Append(EncodeEnrollment(enrollment.first, enrollment.second));
}
}
state_ = State::kReady;
ResolveJavascriptCallback(base::Value(std::move(callback_id_)),
base::Value(std::move(list)));
}
void SecurityKeysBioEnrollmentHandler::HandleStartEnrolling(
const base::Value::List& args) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(1u, args.size());
DCHECK_EQ(state_, State::kReady);
state_ = State::kEnrolling;
callback_id_ = args[0].GetString();
bio_->EnrollTemplate(
base::BindRepeating(
&SecurityKeysBioEnrollmentHandler::OnEnrollingResponse,
weak_factory_.GetWeakPtr()),
base::BindOnce(&SecurityKeysBioEnrollmentHandler::OnEnrollmentFinished,
weak_factory_.GetWeakPtr()));
}
void SecurityKeysBioEnrollmentHandler::OnEnrollingResponse(
device::BioEnrollmentSampleStatus status,
uint8_t remaining_samples) {
DCHECK_EQ(state_, State::kEnrolling);
base::Value::Dict d;
d.Set("status", static_cast<int>(status));
d.Set("remaining", static_cast<int>(remaining_samples));
FireWebUIListener("security-keys-bio-enroll-status",
base::Value(std::move(d)));
}
void SecurityKeysBioEnrollmentHandler::OnEnrollmentFinished(
device::CtapDeviceResponseCode code,
std::vector<uint8_t> template_id) {
DCHECK_EQ(state_, State::kEnrolling);
DCHECK(!callback_id_.empty());
if (code == device::CtapDeviceResponseCode::kCtap2ErrKeepAliveCancel ||
code == device::CtapDeviceResponseCode::kCtap2ErrFpDatabaseFull) {
state_ = State::kReady;
base::Value::Dict d;
d.Set("code", static_cast<int>(code));
d.Set("remaining", 0);
ResolveJavascriptCallback(base::Value(std::move(callback_id_)),
base::Value(std::move(d)));
return;
}
if (code != device::CtapDeviceResponseCode::kSuccess) {
OnError(device::BioEnrollmentHandler::Error::kAuthenticatorResponseInvalid);
return;
}
bio_->EnumerateTemplates(base::BindOnce(
&SecurityKeysBioEnrollmentHandler::OnHavePostEnrollmentEnumeration,
weak_factory_.GetWeakPtr(), std::move(template_id)));
}
void SecurityKeysBioEnrollmentHandler::OnHavePostEnrollmentEnumeration(
std::vector<uint8_t> enrolled_template_id,
device::CtapDeviceResponseCode code,
absl::optional<std::map<std::vector<uint8_t>, std::string>> enrollments) {
DCHECK_EQ(state_, State::kEnrolling);
DCHECK(!callback_id_.empty());
state_ = State::kReady;
if (code != device::CtapDeviceResponseCode::kSuccess || !enrollments ||
!base::Contains(*enrollments, enrolled_template_id)) {
OnError(device::BioEnrollmentHandler::Error::kAuthenticatorResponseInvalid);
return;
}
base::Value::Dict d;
d.Set("code", static_cast<int>(code));
d.Set("remaining", 0);
d.Set("enrollment", EncodeEnrollment(enrolled_template_id,
(*enrollments)[enrolled_template_id]));
ResolveJavascriptCallback(base::Value(std::move(callback_id_)),
base::Value(std::move(d)));
}
void SecurityKeysBioEnrollmentHandler::HandleDelete(
const base::Value::List& args) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(2u, args.size());
state_ = State::kDeleting;
callback_id_ = args[0].GetString();
std::vector<uint8_t> template_id;
if (!base::HexStringToBytes(args[1].GetString(), &template_id)) {
NOTREACHED();
return;
}
bio_->DeleteTemplate(
std::move(template_id),
base::BindOnce(&SecurityKeysBioEnrollmentHandler::OnDelete,
weak_factory_.GetWeakPtr()));
}
void SecurityKeysBioEnrollmentHandler::OnDelete(
device::CtapDeviceResponseCode code) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(state_, State::kDeleting);
DCHECK(!callback_id_.empty());
state_ = State::kEnumerating;
bio_->EnumerateTemplates(
base::BindOnce(&SecurityKeysBioEnrollmentHandler::OnHaveEnumeration,
weak_factory_.GetWeakPtr()));
}
void SecurityKeysBioEnrollmentHandler::HandleRename(
const base::Value::List& args) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(args.size(), 3u);
state_ = State::kRenaming;
callback_id_ = args[0].GetString();
std::vector<uint8_t> template_id;
if (!base::HexStringToBytes(args[1].GetString(), &template_id)) {
NOTREACHED();
return;
}
bio_->RenameTemplate(
std::move(template_id), args[2].GetString(),
base::BindOnce(&SecurityKeysBioEnrollmentHandler::OnRename,
weak_factory_.GetWeakPtr()));
}
void SecurityKeysBioEnrollmentHandler::OnRename(
device::CtapDeviceResponseCode code) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(state_, State::kRenaming);
DCHECK(!callback_id_.empty());
state_ = State::kEnumerating;
bio_->EnumerateTemplates(
base::BindOnce(&SecurityKeysBioEnrollmentHandler::OnHaveEnumeration,
weak_factory_.GetWeakPtr()));
}
void SecurityKeysBioEnrollmentHandler::HandleCancel(
const base::Value::List& args) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(state_, State::kEnrolling);
DCHECK_EQ(0u, args.size());
DCHECK(!callback_id_.empty());
// OnEnrollmentFinished() will be invoked once the cancellation is complete.
bio_->CancelEnrollment();
}
SecurityKeysPhonesHandler::SecurityKeysPhonesHandler() = default;
SecurityKeysPhonesHandler::~SecurityKeysPhonesHandler() = default;
void SecurityKeysPhonesHandler::OnJavascriptAllowed() {}
void SecurityKeysPhonesHandler::OnJavascriptDisallowed() {}
void SecurityKeysPhonesHandler::RegisterMessages() {
web_ui()->RegisterMessageCallback(
"securityKeyPhonesEnumerate",
base::BindRepeating(&SecurityKeysPhonesHandler::HandleEnumerate,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"securityKeyPhonesDelete",
base::BindRepeating(&SecurityKeysPhonesHandler::HandleDelete,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"securityKeyPhonesRename",
base::BindRepeating(&SecurityKeysPhonesHandler::HandleRename,
base::Unretained(this)));
}
void SecurityKeysPhonesHandler::HandleEnumerate(const base::Value::List& args) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(1u, args.size());
AllowJavascript();
DoEnumerate(args[0]);
}
void SecurityKeysPhonesHandler::HandleDelete(const base::Value::List& args) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(2u, args.size());
AllowJavascript();
const std::string public_key_base64 = args[1].GetString();
std::array<uint8_t, device::kP256X962Length> public_key;
const bool ok = DecodePublicKey(public_key_base64, &public_key);
DCHECK(ok);
PrefService* const prefs =
Profile::FromBrowserContext(
web_ui()->GetWebContents()->GetBrowserContext())
->GetPrefs();
cablev2::DeletePairingByPublicKey(prefs, public_key);
DoEnumerate(args[0]);
}
void SecurityKeysPhonesHandler::HandleRename(const base::Value::List& args) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(3u, args.size());
AllowJavascript();
const std::string public_key_base64 = args[1].GetString();
const std::string new_name = args[2].GetString();
content::BrowserContext* const browser_ctx =
web_ui()->GetWebContents()->GetBrowserContext();
std::array<uint8_t, device::kP256X962Length> public_key;
const bool ok = DecodePublicKey(public_key_base64, &public_key);
DCHECK(ok);
// `existing_names` is built without calling `cablev2::MergeDevices` because
// that function will discard linked entries with duplicate public keys, which
// can hide some names that we would still like to avoid colliding with.
std::unique_ptr<cablev2::KnownDevices> known_devices =
cablev2::KnownDevices::FromProfile(
Profile::FromBrowserContext(browser_ctx));
// Remove the device that is getting renamed from the set of linked devices.
auto new_end = std::remove_if(
known_devices->linked_devices.begin(),
known_devices->linked_devices.end(),
[&public_key](const std::unique_ptr<device::cablev2::Pairing>& device)
-> bool { return device->peer_public_key_x962 == public_key; });
known_devices->linked_devices.erase(new_end,
known_devices->linked_devices.end());
PrefService* const prefs =
Profile::FromBrowserContext(browser_ctx)->GetPrefs();
cablev2::RenamePairing(prefs, public_key, new_name, known_devices->Names());
ResolveJavascriptCallback(args[0], base::Value());
}
void SecurityKeysPhonesHandler::DoEnumerate(const base::Value& callback_id) {
const std::vector<std::unique_ptr<device::cablev2::Pairing>> pairings =
cablev2::MergeDevices(
cablev2::KnownDevices::FromProfile(Profile::FromBrowserContext(
web_ui()->GetWebContents()->GetBrowserContext())),
&icu::Locale::getDefault());
base::Value::List synced;
base::Value::List linked;
absl::optional<std::string> last_synced_device_name;
for (const auto& pairing : pairings) {
base::Value::Dict dict;
dict.Set("name", pairing->name);
dict.Set("publicKey", base::Base64Encode(pairing->peer_public_key_x962));
if (pairing->from_sync_deviceinfo) {
// Synced devices can have duplicate names. (E.g. if two or more
// channels are syncing from the same phone.) These are deduplicated
// here.
if (!last_synced_device_name ||
*last_synced_device_name != pairing->name) {
synced.Append(std::move(dict));
}
last_synced_device_name = pairing->name;
} else {
linked.Append(std::move(dict));
}
}
base::Value::List result;
result.Append(std::move(synced));
result.Append(std::move(linked));
ResolveJavascriptCallback(callback_id, base::Value(std::move(result)));
}
} // namespace settings