blob: a6a6c2575f8fee88ad5f9d32067cc19f55f8ffc9 [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 "chromeos/services/device_sync/cryptauth_metadata_syncer_impl.h"
#include <utility>
#include "base/bind.h"
#include "base/memory/ptr_util.h"
#include "base/no_destructor.h"
#include "base/stl_util.h"
#include "chromeos/components/multidevice/logging/logging.h"
#include "chromeos/services/device_sync/async_execution_time_metrics_logger.h"
#include "chromeos/services/device_sync/cryptauth_client.h"
#include "chromeos/services/device_sync/cryptauth_ecies_encryptor_impl.h"
#include "chromeos/services/device_sync/cryptauth_key_creator_impl.h"
#include "chromeos/services/device_sync/value_string_encoding.h"
namespace chromeos {
namespace device_sync {
namespace {
const cryptauthv2::KeyType kGroupKeyType = cryptauthv2::KeyType::P256;
// Timeout values for asynchronous operations.
// TODO(https://crbug.com/933656): Use async execution time metrics to tune
// these timeout values. For now, set these timeouts to the max execution time
// recorded by the metrics.
constexpr base::TimeDelta kWaitingForGroupKeyCreationTimeout =
kMaxAsyncExecutionTime;
constexpr base::TimeDelta kWaitingForLocalDeviceMetadataEncryptionTimeout =
kMaxAsyncExecutionTime;
constexpr base::TimeDelta kWaitingForFirstSyncMetadataResponseTimeout =
kMaxAsyncExecutionTime;
constexpr base::TimeDelta kWaitingForSecondSyncMetadataResponseTimeout =
kMaxAsyncExecutionTime;
CryptAuthDeviceSyncResult::ResultCode
SyncMetadataNetworkRequestErrorToResultCode(NetworkRequestError error) {
switch (error) {
case NetworkRequestError::kOffline:
return CryptAuthDeviceSyncResult::ResultCode::
kErrorSyncMetadataApiCallOffline;
case NetworkRequestError::kEndpointNotFound:
return CryptAuthDeviceSyncResult::ResultCode::
kErrorSyncMetadataApiCallEndpointNotFound;
case NetworkRequestError::kAuthenticationError:
return CryptAuthDeviceSyncResult::ResultCode::
kErrorSyncMetadataApiCallAuthenticationError;
case NetworkRequestError::kBadRequest:
return CryptAuthDeviceSyncResult::ResultCode::
kErrorSyncMetadataApiCallBadRequest;
case NetworkRequestError::kResponseMalformed:
return CryptAuthDeviceSyncResult::ResultCode::
kErrorSyncMetadataApiCallResponseMalformed;
case NetworkRequestError::kInternalServerError:
return CryptAuthDeviceSyncResult::ResultCode::
kErrorSyncMetadataApiCallInternalServerError;
case NetworkRequestError::kUnknown:
return CryptAuthDeviceSyncResult::ResultCode::
kErrorSyncMetadataApiCallUnknownError;
}
}
void RecordGroupKeyCreationMetrics(const base::TimeDelta& execution_time) {
LogAsyncExecutionTimeMetric(
"CryptAuth.DeviceSyncV2.MetadataSyncer.ExecutionTime.GroupKeyCreation",
execution_time);
}
void RecordLocalDeviceMetadataEncryptionMetrics(
const base::TimeDelta& execution_time) {
LogAsyncExecutionTimeMetric(
"CryptAuth.DeviceSyncV2.MetadataSyncer.ExecutionTime."
"LocalDeviceMetadataEncryption",
execution_time);
}
void RecordFirstSyncMetadataMetrics(const base::TimeDelta& execution_time) {
LogAsyncExecutionTimeMetric(
"CryptAuth.DeviceSyncV2.MetadataSyncer.ExecutionTime.FirstSyncMetadata",
execution_time);
}
void RecordSecondSyncMetadataMetrics(const base::TimeDelta& execution_time) {
LogAsyncExecutionTimeMetric(
"CryptAuth.DeviceSyncV2.MetadataSyncer.ExecutionTime.SecondSyncMetadata",
execution_time);
}
} // namespace
// static
CryptAuthMetadataSyncerImpl::Factory*
CryptAuthMetadataSyncerImpl::Factory::test_factory_ = nullptr;
// static
CryptAuthMetadataSyncerImpl::Factory*
CryptAuthMetadataSyncerImpl::Factory::Get() {
if (test_factory_)
return test_factory_;
static base::NoDestructor<CryptAuthMetadataSyncerImpl::Factory> factory;
return factory.get();
}
// static
void CryptAuthMetadataSyncerImpl::Factory::SetFactoryForTesting(
Factory* test_factory) {
test_factory_ = test_factory;
}
CryptAuthMetadataSyncerImpl::Factory::~Factory() = default;
std::unique_ptr<CryptAuthMetadataSyncer>
CryptAuthMetadataSyncerImpl::Factory::BuildInstance(
CryptAuthClientFactory* client_factory,
std::unique_ptr<base::OneShotTimer> timer) {
return base::WrapUnique(
new CryptAuthMetadataSyncerImpl(client_factory, std::move(timer)));
}
CryptAuthMetadataSyncerImpl::CryptAuthMetadataSyncerImpl(
CryptAuthClientFactory* client_factory,
std::unique_ptr<base::OneShotTimer> timer)
: client_factory_(client_factory), timer_(std::move(timer)) {
DCHECK(client_factory);
}
CryptAuthMetadataSyncerImpl::~CryptAuthMetadataSyncerImpl() = default;
// static
base::Optional<base::TimeDelta> CryptAuthMetadataSyncerImpl::GetTimeoutForState(
State state) {
switch (state) {
case State::kWaitingForGroupKeyCreation:
return kWaitingForGroupKeyCreationTimeout;
case State::kWaitingForLocalDeviceMetadataEncryption:
return kWaitingForLocalDeviceMetadataEncryptionTimeout;
case State::kWaitingForFirstSyncMetadataResponse:
return kWaitingForFirstSyncMetadataResponseTimeout;
case State::kWaitingForSecondSyncMetadataResponse:
return kWaitingForSecondSyncMetadataResponseTimeout;
default:
// Signifies that there should not be a timeout.
return base::nullopt;
}
}
// static
base::Optional<CryptAuthDeviceSyncResult::ResultCode>
CryptAuthMetadataSyncerImpl::ResultCodeErrorFromTimeoutDuringState(
State state) {
switch (state) {
case State::kWaitingForGroupKeyCreation:
return CryptAuthDeviceSyncResult::ResultCode::
kErrorTimeoutWaitingForGroupKeyCreation;
case State::kWaitingForLocalDeviceMetadataEncryption:
return CryptAuthDeviceSyncResult::ResultCode::
kErrorTimeoutWaitingForLocalDeviceMetadataEncryption;
case State::kWaitingForFirstSyncMetadataResponse:
return CryptAuthDeviceSyncResult::ResultCode::
kErrorTimeoutWaitingForFirstSyncMetadataResponse;
case State::kWaitingForSecondSyncMetadataResponse:
return CryptAuthDeviceSyncResult::ResultCode::
kErrorTimeoutWaitingForSecondSyncMetadataResponse;
default:
return base::nullopt;
}
}
void CryptAuthMetadataSyncerImpl::OnAttemptStarted(
const cryptauthv2::RequestContext& request_context,
const cryptauthv2::BetterTogetherDeviceMetadata& local_device_metadata,
const CryptAuthKey* initial_group_key) {
DCHECK_EQ(State::kNotStarted, state_);
request_context_ = request_context;
local_device_metadata_ = local_device_metadata;
initial_group_key_ = initial_group_key;
AttemptNextStep();
}
void CryptAuthMetadataSyncerImpl::SetState(State state) {
timer_->Stop();
PA_LOG(INFO) << "Transitioning from " << state_ << " to " << state;
state_ = state;
last_state_change_timestamp_ = base::TimeTicks::Now();
base::Optional<base::TimeDelta> timeout_for_state = GetTimeoutForState(state);
if (!timeout_for_state)
return;
// TODO(https://crbug.com/936273): Add metrics to track failure rates due to
// async timeouts.
timer_->Start(FROM_HERE, *timeout_for_state,
base::BindOnce(&CryptAuthMetadataSyncerImpl::OnTimeout,
base::Unretained(this)));
}
void CryptAuthMetadataSyncerImpl::OnTimeout() {
// If there's a timeout specified, there should be a corresponding error code.
base::Optional<CryptAuthDeviceSyncResult::ResultCode> error_code =
ResultCodeErrorFromTimeoutDuringState(state_);
DCHECK(error_code);
base::TimeDelta execution_time =
base::TimeTicks::Now() - last_state_change_timestamp_;
switch (state_) {
case State::kWaitingForGroupKeyCreation:
RecordGroupKeyCreationMetrics(execution_time);
break;
case State::kWaitingForLocalDeviceMetadataEncryption:
RecordLocalDeviceMetadataEncryptionMetrics(execution_time);
break;
case State::kWaitingForFirstSyncMetadataResponse:
RecordFirstSyncMetadataMetrics(execution_time);
break;
case State::kWaitingForSecondSyncMetadataResponse:
RecordSecondSyncMetadataMetrics(execution_time);
break;
default:
NOTREACHED();
}
FinishAttempt(*error_code);
}
const CryptAuthKey* CryptAuthMetadataSyncerImpl::GetGroupKey() {
if (new_group_key_)
return new_group_key_.get();
return initial_group_key_;
}
CryptAuthMetadataSyncerImpl::GroupPublicKeyState
CryptAuthMetadataSyncerImpl::GetGroupPublicKeyState() {
const CryptAuthKey* group_key = GetGroupKey();
if (!group_key)
return GroupPublicKeyState::kNewKeyNeedsToBeCreated;
if (!sync_metadata_response_)
return GroupPublicKeyState::kKeyExistsButNotConfirmedWithCryptAuth;
if (sync_metadata_response_->group_public_key().empty())
return GroupPublicKeyState::kNewKeyNeedsToBeCreated;
if (group_key->public_key() != sync_metadata_response_->group_public_key())
return GroupPublicKeyState::kNewKeyReceivedFromCryptAuth;
return GroupPublicKeyState::kEstablished;
}
void CryptAuthMetadataSyncerImpl::AttemptNextStep() {
switch (state_) {
case State::kNotStarted:
switch (GetGroupPublicKeyState()) {
case GroupPublicKeyState::kNewKeyNeedsToBeCreated:
CreateGroupKey();
return;
case GroupPublicKeyState::kKeyExistsButNotConfirmedWithCryptAuth:
EncryptLocalDeviceMetadata();
return;
default:
NOTREACHED();
return;
}
case State::kWaitingForGroupKeyCreation:
EncryptLocalDeviceMetadata();
return;
case State::kWaitingForLocalDeviceMetadataEncryption:
MakeSyncMetadataCall();
return;
case State::kWaitingForFirstSyncMetadataResponse:
switch (GetGroupPublicKeyState()) {
case GroupPublicKeyState::kNewKeyNeedsToBeCreated:
CreateGroupKey();
return;
case GroupPublicKeyState::kNewKeyReceivedFromCryptAuth:
new_group_key_ = std::make_unique<CryptAuthKey>(
sync_metadata_response_->group_public_key(),
std::string() /* private_key */, CryptAuthKey::Status::kActive,
kGroupKeyType);
EncryptLocalDeviceMetadata();
return;
case GroupPublicKeyState::kEstablished:
FilterMetadataAndFinishAttempt();
return;
default:
NOTREACHED();
return;
}
case State::kWaitingForSecondSyncMetadataResponse:
// No more than two SyncMetadata requests should be necessary in the v2
// DeviceSync protocol to establish the group public key.
switch (GetGroupPublicKeyState()) {
case GroupPublicKeyState::kEstablished:
FilterMetadataAndFinishAttempt();
return;
default:
FinishAttempt(CryptAuthDeviceSyncResult::ResultCode::
kErrorEstablishingGroupPublicKey);
return;
}
case State::kFinished:
NOTREACHED();
return;
}
}
void CryptAuthMetadataSyncerImpl::EncryptLocalDeviceMetadata() {
SetState(State::kWaitingForLocalDeviceMetadataEncryption);
encryptor_ = CryptAuthEciesEncryptorImpl::Factory::Get()->BuildInstance();
encryptor_->Encrypt(
local_device_metadata_.SerializeAsString(), GetGroupKey()->public_key(),
base::BindOnce(
&CryptAuthMetadataSyncerImpl::OnLocalDeviceMetadataEncrypted,
base::Unretained(this)));
}
void CryptAuthMetadataSyncerImpl::OnLocalDeviceMetadataEncrypted(
const base::Optional<std::string>& encrypted_metadata) {
DCHECK_EQ(State::kWaitingForLocalDeviceMetadataEncryption, state_);
RecordLocalDeviceMetadataEncryptionMetrics(base::TimeTicks::Now() -
last_state_change_timestamp_);
if (!encrypted_metadata) {
FinishAttempt(
CryptAuthDeviceSyncResult::ResultCode::kErrorEncryptingDeviceMetadata);
return;
}
encrypted_local_device_metadata_ = encrypted_metadata;
AttemptNextStep();
}
void CryptAuthMetadataSyncerImpl::CreateGroupKey() {
SetState(State::kWaitingForGroupKeyCreation);
key_creator_ = CryptAuthKeyCreatorImpl::Factory::Get()->BuildInstance();
key_creator_->CreateKeys(
{{CryptAuthKeyBundle::Name::kDeviceSyncBetterTogetherGroupKey,
CryptAuthKeyCreator::CreateKeyData(CryptAuthKey::Status::kActive,
kGroupKeyType)}},
base::nullopt /* server_ephemeral_dh */,
base::BindOnce(&CryptAuthMetadataSyncerImpl::OnGroupKeyCreated,
base::Unretained(this)));
}
void CryptAuthMetadataSyncerImpl::OnGroupKeyCreated(
const base::flat_map<CryptAuthKeyBundle::Name, CryptAuthKey>& new_keys,
const base::Optional<CryptAuthKey>& client_ephemeral_dh) {
DCHECK_EQ(State::kWaitingForGroupKeyCreation, state_);
RecordGroupKeyCreationMetrics(base::TimeTicks::Now() -
last_state_change_timestamp_);
const auto it = new_keys.find(
CryptAuthKeyBundle::Name::kDeviceSyncBetterTogetherGroupKey);
DCHECK(it != new_keys.end());
new_group_key_ = std::make_unique<CryptAuthKey>(it->second);
AttemptNextStep();
}
void CryptAuthMetadataSyncerImpl::MakeSyncMetadataCall() {
DCHECK(encrypted_local_device_metadata_);
const CryptAuthKey* group_key = GetGroupKey();
DCHECK(group_key);
cryptauthv2::SyncMetadataRequest request;
request.mutable_context()->CopyFrom(request_context_);
request.set_group_public_key(group_key->public_key());
request.set_encrypted_metadata(*encrypted_local_device_metadata_);
request.set_need_group_private_key(group_key->private_key().empty());
++num_sync_metadata_calls_;
switch (num_sync_metadata_calls_) {
case 1:
SetState(State::kWaitingForFirstSyncMetadataResponse);
break;
case 2:
SetState(State::kWaitingForSecondSyncMetadataResponse);
break;
default:
// AttemptNextStep() ensures that no more than two calls are made.
NOTREACHED();
return;
}
cryptauth_client_ = client_factory_->CreateInstance();
cryptauth_client_->SyncMetadata(
request,
base::Bind(&CryptAuthMetadataSyncerImpl::OnSyncMetadataSuccess,
base::Unretained(this)),
base::Bind(&CryptAuthMetadataSyncerImpl::OnSyncMetadataFailure,
base::Unretained(this)));
}
void CryptAuthMetadataSyncerImpl::OnSyncMetadataSuccess(
const cryptauthv2::SyncMetadataResponse& response) {
base::TimeDelta execution_time =
base::TimeTicks::Now() - last_state_change_timestamp_;
if (state_ == State::kWaitingForFirstSyncMetadataResponse)
RecordFirstSyncMetadataMetrics(execution_time);
else if (state_ == State::kWaitingForSecondSyncMetadataResponse)
RecordSecondSyncMetadataMetrics(execution_time);
else
NOTREACHED();
sync_metadata_response_ = response;
AttemptNextStep();
}
void CryptAuthMetadataSyncerImpl::OnSyncMetadataFailure(
NetworkRequestError error) {
base::TimeDelta execution_time =
base::TimeTicks::Now() - last_state_change_timestamp_;
if (state_ == State::kWaitingForFirstSyncMetadataResponse)
RecordFirstSyncMetadataMetrics(execution_time);
else if (state_ == State::kWaitingForSecondSyncMetadataResponse)
RecordSecondSyncMetadataMetrics(execution_time);
else
NOTREACHED();
FinishAttempt(SyncMetadataNetworkRequestErrorToResultCode(error));
}
void CryptAuthMetadataSyncerImpl::FilterMetadataAndFinishAttempt() {
DCHECK_EQ(GroupPublicKeyState::kEstablished, GetGroupPublicKeyState());
DCHECK(sync_metadata_response_);
// At minimum, the local device's metadata should be present in the
// SyncMetadataResponse.
if (sync_metadata_response_->encrypted_metadata().empty()) {
FinishAttempt(
CryptAuthDeviceSyncResult::ResultCode::kErrorNoMetadataInResponse);
return;
}
bool did_non_fatal_error_occur = false;
for (const cryptauthv2::DeviceMetadataPacket& metadata :
sync_metadata_response_->encrypted_metadata()) {
// TODO(https://crbug.com/936273): Log metrics for invalid
// DeviceMetadataPackets.
bool is_device_metadata_packet_valid =
!metadata.device_id().empty() && !metadata.device_name().empty() &&
!metadata.device_public_key().empty();
if (!is_device_metadata_packet_valid) {
PA_LOG(ERROR) << "Invalid DeviceMetadataPacket: device_id = "
<< metadata.device_id() << ", device_public_key = "
<< util::EncodeAsString(metadata.device_public_key())
<< ", device_name empty? "
<< (metadata.device_name().empty() ? "yes" : "no") << ".";
did_non_fatal_error_occur = true;
continue;
}
// TODO(https://crbug.com/936273): Log metrics for duplicate device IDs.
bool is_duplicate_id =
base::Contains(id_to_device_metadata_packet_map_, metadata.device_id());
if (is_duplicate_id) {
PA_LOG(ERROR) << "Duplicate device IDs (" << metadata.device_id()
<< ") in SyncMetadata response.";
did_non_fatal_error_occur = true;
continue;
}
id_to_device_metadata_packet_map_[metadata.device_id()] = metadata;
}
// Finish attempt if DeviceMetadataPackets were sent but none were valid.
if (id_to_device_metadata_packet_map_.empty()) {
FinishAttempt(CryptAuthDeviceSyncResult::ResultCode::
kErrorAllResponseMetadataInvalid);
return;
}
// We require that the local device's metadata is returned in the response.
if (!base::Contains(id_to_device_metadata_packet_map_,
request_context_.device_id())) {
PA_LOG(ERROR) << "Metadata for local device (Instance ID: "
<< request_context_.device_id()
<< ") not in SyncMetadata response.";
FinishAttempt(CryptAuthDeviceSyncResult::ResultCode::
kErrorNoLocalDeviceMetadataInResponse);
return;
}
CryptAuthDeviceSyncResult::ResultCode result_code =
did_non_fatal_error_occur
? CryptAuthDeviceSyncResult::ResultCode::kFinishedWithNonFatalErrors
: CryptAuthDeviceSyncResult::ResultCode::kSuccess;
FinishAttempt(result_code);
}
void CryptAuthMetadataSyncerImpl::FinishAttempt(
CryptAuthDeviceSyncResult::ResultCode result_code) {
cryptauth_client_.reset();
key_creator_.reset();
encryptor_.reset();
base::Optional<cryptauthv2::ClientDirective> new_client_directive;
base::Optional<cryptauthv2::EncryptedGroupPrivateKey>
encrypted_group_private_key;
if (sync_metadata_response_) {
if (sync_metadata_response_->has_client_directive())
new_client_directive = sync_metadata_response_->client_directive();
if (sync_metadata_response_->has_encrypted_group_private_key()) {
encrypted_group_private_key =
sync_metadata_response_->encrypted_group_private_key();
}
}
SetState(State::kFinished);
OnAttemptFinished(id_to_device_metadata_packet_map_,
std::move(new_group_key_), encrypted_group_private_key,
new_client_directive, result_code);
}
std::ostream& operator<<(std::ostream& stream,
const CryptAuthMetadataSyncerImpl::State& state) {
switch (state) {
case CryptAuthMetadataSyncerImpl::State::kNotStarted:
stream << "[MetadataSyncer state: Not started]";
break;
case CryptAuthMetadataSyncerImpl::State::kWaitingForGroupKeyCreation:
stream << "[MetadataSyncer state: Waiting for group key pair creation]";
break;
case CryptAuthMetadataSyncerImpl::State::
kWaitingForLocalDeviceMetadataEncryption:
stream << "[MetadataSyncer state: Waiting for local device metadata "
<< "encryption]";
break;
case CryptAuthMetadataSyncerImpl::State::
kWaitingForFirstSyncMetadataResponse:
stream << "[MetadataSyncer state: Waiting for first SyncMetadata "
<< "response]";
break;
case CryptAuthMetadataSyncerImpl::State::
kWaitingForSecondSyncMetadataResponse:
stream << "[MetadataSyncer state: Waiting for second SyncMetadata "
<< "response]";
break;
case CryptAuthMetadataSyncerImpl::State::kFinished:
stream << "[MetadataSyncer state: Finished]";
break;
}
return stream;
}
} // namespace device_sync
} // namespace chromeos