// 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.
#include "chrome/browser/nearby_sharing/certificates/nearby_share_private_certificate.h"
#include <string_view>
#include <utility>
#include "base/base64url.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/json/values_util.h"
#include "base/rand_util.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/nearby_sharing/certificates/common.h"
#include "chrome/browser/nearby_sharing/certificates/constants.h"
#include "chrome/browser/nearby_sharing/common/nearby_share_switches.h"
#include "chromeos/ash/components/nearby/common/proto/timestamp.pb.h"
#include "components/cross_device/logging/logging.h"
#include "crypto/aead.h"
#include "crypto/ec_private_key.h"
#include "crypto/ec_signature_creator.h"
#include "crypto/encryptor.h"
#include "crypto/hmac.h"
#include "crypto/sha2.h"
#include "crypto/symmetric_key.h"
namespace {
// Dictionary keys used in ToDictionary, FromDictionary
const char kVisibility[] = "visibility";
const char kNotBefore[] = "not_before";
const char kNotAfter[] = "not_after";
const char kKeyPair[] = "key_pair";
const char kSecretKey[] = "secret_key";
const char kMetadataEncryptionKey[] = "metadata_encryption_key";
const char kId[] = "id";
const char kUnencryptedMetadata[] = "unencrypted_metadata";
const char kConsumedSalts[] = "consumed_salts";
// Generates a random validity bound offset in the interval
// [0, kNearbyShareMaxPrivateCertificateValidityBoundOffset).
base::TimeDelta GenerateRandomOffset() {
return base::RandTimeDeltaUpTo(
kNearbyShareMaxPrivateCertificateValidityBoundOffset);
}
// Generates a certificate identifier by hashing the input secret |key|.
std::vector<uint8_t> CreateCertificateIdFromSecretKey(
const crypto::SymmetricKey& key) {
DCHECK_EQ(crypto::kSHA256Length, kNearbyShareNumBytesCertificateId);
std::vector<uint8_t> id(kNearbyShareNumBytesCertificateId);
crypto::SHA256HashString(key.key(), id.data(), id.size());
return id;
}
// Creates an HMAC from |metadata_encryption_key| to be used as a key commitment
// in certificates.
std::optional<std::vector<uint8_t>> CreateMetadataEncryptionKeyTag(
base::span<const uint8_t> metadata_encryption_key) {
// This array of 0x00 is used to conform with the GmsCore implementation.
std::vector<uint8_t> key(kNearbyShareNumBytesMetadataEncryptionKeyTag, 0x00);
std::vector<uint8_t> result(kNearbyShareNumBytesMetadataEncryptionKeyTag);
crypto::HMAC hmac(crypto::HMAC::HashAlgorithm::SHA256);
if (!hmac.Init(key) || !hmac.Sign(metadata_encryption_key, result))
return std::nullopt;
return result;
}
std::string EncodeString(const std::string& unencoded_string) {
std::string encoded_string;
base::Base64UrlEncode(unencoded_string,
base::Base64UrlEncodePolicy::INCLUDE_PADDING,
&encoded_string);
return encoded_string;
}
std::optional<std::string> DecodeString(const std::string* encoded_string) {
if (!encoded_string)
return std::nullopt;
std::string decoded_string;
if (!base::Base64UrlDecode(*encoded_string,
base::Base64UrlDecodePolicy::REQUIRE_PADDING,
&decoded_string)) {
return std::nullopt;
}
return decoded_string;
}
std::string BytesToEncodedString(const std::vector<uint8_t>& bytes) {
return EncodeString(std::string(bytes.begin(), bytes.end()));
}
std::optional<std::vector<uint8_t>> EncodedStringToBytes(
const std::string* str) {
std::optional<std::string> decoded_str = DecodeString(str);
return decoded_str ? std::make_optional<std::vector<uint8_t>>(
decoded_str->begin(), decoded_str->end())
: std::nullopt;
}
std::string SaltsToString(const std::set<std::vector<uint8_t>>& salts) {
std::string str;
str.reserve(salts.size() * 2 * kNearbyShareNumBytesMetadataEncryptionKeySalt);
for (const std::vector<uint8_t>& salt : salts) {
str += base::HexEncode(salt);
}
return str;
}
std::set<std::vector<uint8_t>> StringToSalts(const std::string& str) {
const size_t chars_per_salt =
2 * kNearbyShareNumBytesMetadataEncryptionKeySalt;
DCHECK(str.size() % chars_per_salt == 0);
std::set<std::vector<uint8_t>> salts;
for (size_t i = 0; i < str.size(); i += chars_per_salt) {
std::vector<uint8_t> salt;
base::HexStringToBytes(std::string_view(&str[i], chars_per_salt), &salt);
salts.insert(std::move(salt));
}
return salts;
}
// Check for a command-line override the certificate validity period, otherwise
// return the default |kNearbyShareCertificateValidityPeriod|.
base::TimeDelta GetCertificateValidityPeriod() {
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
if (!command_line->HasSwitch(
switches::kNearbyShareCertificateValidityPeriodHours)) {
return kNearbyShareCertificateValidityPeriod;
}
std::string certificate_validity_period_hours_str =
command_line->GetSwitchValueASCII(
switches::kNearbyShareCertificateValidityPeriodHours);
int certificate_validity_period_hours = 0;
if (!base::StringToInt(certificate_validity_period_hours_str,
&certificate_validity_period_hours) ||
certificate_validity_period_hours < 1) {
CD_LOG(ERROR, Feature::NS)
<< __func__
<< ": Invalid value provided for certificate validity period override.";
return kNearbyShareCertificateValidityPeriod;
}
return base::Hours(certificate_validity_period_hours);
}
} // namespace
NearbySharePrivateCertificate::NearbySharePrivateCertificate(
nearby_share::mojom::Visibility visibility,
base::Time not_before,
nearby::sharing::proto::EncryptedMetadata unencrypted_metadata)
: visibility_(visibility),
not_before_(not_before),
not_after_(not_before_ + GetCertificateValidityPeriod()),
key_pair_(crypto::ECPrivateKey::Create()),
secret_key_(crypto::SymmetricKey::GenerateRandomKey(
crypto::SymmetricKey::Algorithm::AES,
/*key_size_in_bits=*/8 * kNearbyShareNumBytesSecretKey)),
metadata_encryption_key_(
GenerateRandomBytes(kNearbyShareNumBytesMetadataEncryptionKey)),
id_(CreateCertificateIdFromSecretKey(*secret_key_)),
unencrypted_metadata_(std::move(unencrypted_metadata)) {
DCHECK_NE(visibility, nearby_share::mojom::Visibility::kNoOne);
}
NearbySharePrivateCertificate::NearbySharePrivateCertificate(
nearby_share::mojom::Visibility visibility,
base::Time not_before,
base::Time not_after,
std::unique_ptr<crypto::ECPrivateKey> key_pair,
std::unique_ptr<crypto::SymmetricKey> secret_key,
std::vector<uint8_t> metadata_encryption_key,
std::vector<uint8_t> id,
nearby::sharing::proto::EncryptedMetadata unencrypted_metadata,
std::set<std::vector<uint8_t>> consumed_salts)
: visibility_(visibility),
not_before_(not_before),
not_after_(not_after),
key_pair_(std::move(key_pair)),
secret_key_(std::move(secret_key)),
metadata_encryption_key_(std::move(metadata_encryption_key)),
id_(std::move(id)),
unencrypted_metadata_(std::move(unencrypted_metadata)),
consumed_salts_(std::move(consumed_salts)) {
DCHECK_NE(visibility, nearby_share::mojom::Visibility::kNoOne);
}
NearbySharePrivateCertificate::NearbySharePrivateCertificate(
const NearbySharePrivateCertificate& other) {
*this = other;
}
NearbySharePrivateCertificate& NearbySharePrivateCertificate::operator=(
const NearbySharePrivateCertificate& other) {
if (this == &other)
return *this;
visibility_ = other.visibility_;
not_before_ = other.not_before_;
not_after_ = other.not_after_;
key_pair_ = other.key_pair_->Copy();
secret_key_ = crypto::SymmetricKey::Import(
crypto::SymmetricKey::Algorithm::AES, other.secret_key_->key());
metadata_encryption_key_ = other.metadata_encryption_key_;
id_ = other.id_;
unencrypted_metadata_ = other.unencrypted_metadata_;
consumed_salts_ = other.consumed_salts_;
next_salts_for_testing_ = other.next_salts_for_testing_;
offset_for_testing_ = other.offset_for_testing_;
return *this;
}
NearbySharePrivateCertificate::NearbySharePrivateCertificate(
NearbySharePrivateCertificate&& other) = default;
NearbySharePrivateCertificate& NearbySharePrivateCertificate::operator=(
NearbySharePrivateCertificate&& other) = default;
NearbySharePrivateCertificate::~NearbySharePrivateCertificate() = default;
std::optional<NearbyShareEncryptedMetadataKey>
NearbySharePrivateCertificate::EncryptMetadataKey() {
std::optional<std::vector<uint8_t>> salt = GenerateUnusedSalt();
if (!salt) {
CD_LOG(ERROR, Feature::NS)
<< "Encryption failed: Salt generation unsuccessful.";
return std::nullopt;
}
std::unique_ptr<crypto::Encryptor> encryptor =
CreateNearbyShareCtrEncryptor(secret_key_.get(), *salt);
if (!encryptor) {
CD_LOG(ERROR, Feature::NS)
<< "Encryption failed: Could not create CTR encryptor.";
return std::nullopt;
}
DCHECK_EQ(kNearbyShareNumBytesMetadataEncryptionKey,
metadata_encryption_key_.size());
std::vector<uint8_t> encrypted_metadata_key;
if (!encryptor->Encrypt(metadata_encryption_key_, &encrypted_metadata_key)) {
CD_LOG(ERROR, Feature::NS)
<< "Encryption failed: Could not encrypt metadata key.";
return std::nullopt;
}
return NearbyShareEncryptedMetadataKey(*salt, encrypted_metadata_key);
}
std::optional<std::vector<uint8_t>> NearbySharePrivateCertificate::Sign(
base::span<const uint8_t> payload) const {
std::unique_ptr<crypto::ECSignatureCreator> signer(
crypto::ECSignatureCreator::Create(key_pair_.get()));
std::vector<uint8_t> signature;
if (!signer->Sign(payload, &signature)) {
CD_LOG(ERROR, Feature::NS) << "Signing failed.";
return std::nullopt;
}
return signature;
}
std::vector<uint8_t> NearbySharePrivateCertificate::HashAuthenticationToken(
base::span<const uint8_t> authentication_token) const {
return ComputeAuthenticationTokenHash(
authentication_token,
base::as_bytes(base::make_span(secret_key_->key())));
}
std::optional<nearby::sharing::proto::PublicCertificate>
NearbySharePrivateCertificate::ToPublicCertificate() const {
std::vector<uint8_t> public_key;
if (!key_pair_->ExportPublicKey(&public_key)) {
CD_LOG(ERROR, Feature::NS) << "Failed to export public key.";
return std::nullopt;
}
std::optional<std::vector<uint8_t>> encrypted_metadata_bytes =
EncryptMetadata();
if (!encrypted_metadata_bytes) {
CD_LOG(ERROR, Feature::NS) << "Failed to encrypt metadata.";
return std::nullopt;
}
std::optional<std::vector<uint8_t>> metadata_encryption_key_tag =
CreateMetadataEncryptionKeyTag(metadata_encryption_key_);
if (!metadata_encryption_key_tag) {
CD_LOG(ERROR, Feature::NS)
<< "Failed to compute metadata encryption key tag.";
return std::nullopt;
}
base::TimeDelta not_before_offset =
offset_for_testing_.value_or(GenerateRandomOffset());
base::TimeDelta not_after_offset =
offset_for_testing_.value_or(GenerateRandomOffset());
nearby::sharing::proto::PublicCertificate public_certificate;
public_certificate.set_secret_id(std::string(id_.begin(), id_.end()));
public_certificate.set_secret_key(secret_key_->key());
public_certificate.set_public_key(
std::string(public_key.begin(), public_key.end()));
public_certificate.mutable_start_time()->set_seconds(
(not_before_ - not_before_offset).InMillisecondsSinceUnixEpoch() / 1000);
public_certificate.mutable_end_time()->set_seconds(
(not_after_ + not_after_offset).InMillisecondsSinceUnixEpoch() / 1000);
// When `visibility_` is set to kYourDevices, under the hood, the visibility
// is set to Selected Contacts with an empty allowed contact list. The
// NearbyShare server sends a public certificate to all devices logged into
// the same GAIA account as this one when the visibility is kSelectedContacts,
// so if the allowed contact list is empty, then the public certificate is
// sent out to devices logged into the same GAIA account only; this is
// effectively being visible only to the user's own devices.
public_certificate.set_for_selected_contacts(
visibility_ == nearby_share::mojom::Visibility::kSelectedContacts ||
visibility_ == nearby_share::mojom::Visibility::kYourDevices);
public_certificate.set_metadata_encryption_key(std::string(
metadata_encryption_key_.begin(), metadata_encryption_key_.end()));
public_certificate.set_encrypted_metadata_bytes(std::string(
encrypted_metadata_bytes->begin(), encrypted_metadata_bytes->end()));
public_certificate.set_metadata_encryption_key_tag(
std::string(metadata_encryption_key_tag->begin(),
metadata_encryption_key_tag->end()));
// Note: Setting |for_self_share| here will cause the server to silently
// reject the marked certificates. The |for_self_share| field is not set by
// clients but is set by the server for all downloaded public certificates.
// TODO (brandosocarras@ b/291132662): indicate that Your Devices visibility
// public certificates are Your Devices visibility to NS server.
return public_certificate;
}
base::Value::Dict NearbySharePrivateCertificate::ToDictionary() const {
std::vector<uint8_t> key_pair;
key_pair_->ExportPrivateKey(&key_pair);
return base::Value::Dict()
.Set(kVisibility, static_cast<int>(visibility_))
.Set(kNotBefore, base::TimeToValue(not_before_))
.Set(kNotAfter, base::TimeToValue(not_after_))
.Set(kKeyPair, BytesToEncodedString(key_pair))
.Set(kSecretKey, EncodeString(secret_key_->key()))
.Set(kMetadataEncryptionKey,
BytesToEncodedString(metadata_encryption_key_))
.Set(kId, BytesToEncodedString(id_))
.Set(kUnencryptedMetadata,
EncodeString(unencrypted_metadata_.SerializeAsString()))
.Set(kConsumedSalts, SaltsToString(consumed_salts_));
}
std::optional<NearbySharePrivateCertificate>
NearbySharePrivateCertificate::FromDictionary(const base::Value::Dict& dict) {
std::optional<int> int_opt;
const std::string* str_ptr;
std::optional<std::string> str_opt;
std::optional<base::Time> time_opt;
std::optional<std::vector<uint8_t>> bytes_opt;
int_opt = dict.FindInt(kVisibility);
if (!int_opt)
return std::nullopt;
nearby_share::mojom::Visibility visibility =
static_cast<nearby_share::mojom::Visibility>(*int_opt);
time_opt = base::ValueToTime(dict.Find(kNotBefore));
if (!time_opt)
return std::nullopt;
base::Time not_before = *time_opt;
time_opt = base::ValueToTime(dict.Find(kNotAfter));
if (!time_opt)
return std::nullopt;
base::Time not_after = *time_opt;
bytes_opt = EncodedStringToBytes(dict.FindString(kKeyPair));
if (!bytes_opt)
return std::nullopt;
std::unique_ptr<crypto::ECPrivateKey> key_pair =
crypto::ECPrivateKey::CreateFromPrivateKeyInfo(*bytes_opt);
str_opt = DecodeString(dict.FindString(kSecretKey));
if (!str_opt)
return std::nullopt;
std::unique_ptr<crypto::SymmetricKey> secret_key =
crypto::SymmetricKey::Import(crypto::SymmetricKey::Algorithm::AES,
*str_opt);
bytes_opt = EncodedStringToBytes(dict.FindString(kMetadataEncryptionKey));
if (!bytes_opt)
return std::nullopt;
std::vector<uint8_t> metadata_encryption_key = *bytes_opt;
bytes_opt = EncodedStringToBytes(dict.FindString(kId));
if (!bytes_opt)
return std::nullopt;
std::vector<uint8_t> id = *bytes_opt;
str_opt = DecodeString(dict.FindString(kUnencryptedMetadata));
if (!str_opt)
return std::nullopt;
nearby::sharing::proto::EncryptedMetadata unencrypted_metadata;
if (!unencrypted_metadata.ParseFromString(*str_opt))
return std::nullopt;
str_ptr = dict.FindString(kConsumedSalts);
if (!str_ptr)
return std::nullopt;
std::set<std::vector<uint8_t>> consumed_salts = StringToSalts(*str_ptr);
return NearbySharePrivateCertificate(
visibility, not_before, not_after, std::move(key_pair),
std::move(secret_key), std::move(metadata_encryption_key), std::move(id),
std::move(unencrypted_metadata), std::move(consumed_salts));
}
std::optional<std::vector<uint8_t>>
NearbySharePrivateCertificate::GenerateUnusedSalt() {
if (consumed_salts_.size() >= kNearbyShareMaxNumMetadataEncryptionKeySalts) {
CD_LOG(ERROR, Feature::NS) << "All salts exhausted for certificate.";
return std::nullopt;
}
for (size_t attempt = 0;
attempt < kNearbyShareMaxNumMetadataEncryptionKeySaltGenerationRetries;
++attempt) {
std::vector<uint8_t> salt;
if (next_salts_for_testing_.empty()) {
salt = GenerateRandomBytes(2u);
} else {
salt = next_salts_for_testing_.front();
next_salts_for_testing_.pop();
}
DCHECK_EQ(2u, salt.size());
if (!base::Contains(consumed_salts_, salt)) {
consumed_salts_.insert(salt);
return salt;
}
}
CD_LOG(ERROR, Feature::NS)
<< "Salt generation exceeded max number of retries. This is "
"highly improbable.";
return std::nullopt;
}
std::optional<std::vector<uint8_t>>
NearbySharePrivateCertificate::EncryptMetadata() const {
// Init() keeps a reference to the input key, so that reference must outlive
// the lifetime of |aead|.
std::vector<uint8_t> derived_key = DeriveNearbyShareKey(
metadata_encryption_key_, kNearbyShareNumBytesAesGcmKey);
crypto::Aead aead(crypto::Aead::AeadAlgorithm::AES_256_GCM);
aead.Init(derived_key);
std::vector<uint8_t> metadata_array(unencrypted_metadata_.ByteSizeLong());
unencrypted_metadata_.SerializeToArray(metadata_array.data(),
metadata_array.size());
return aead.Seal(
metadata_array,
/*nonce=*/
DeriveNearbyShareKey(base::as_bytes(base::make_span(secret_key_->key())),
kNearbyShareNumBytesAesGcmIv),
/*additional_data=*/base::span<const uint8_t>());
}