#include "device/fido/mac/credential_metadata.h"
#include <ostream>
#include "base/check.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "components/cbor/reader.h"
#include "components/cbor/values.h"
#include "components/cbor/writer.h"
#include "device/fido/public_key_credential_user_entity.h"
#include "third_party/boringssl/src/include/openssl/digest.h"
#include "third_party/boringssl/src/include/openssl/hkdf.h"
#include "third_party/boringssl/src/include/openssl/rand.h"
namespace device {
namespace fido {
namespace mac {
static constexpr size_t kNonceLength = 12;
namespace {
std::vector<uint8_t> MakeAad(CredentialMetadata::Version version,
const std::string& rp_id) {
std::vector<uint8_t> result = {static_cast<uint8_t>(version)};
result.insert(result.end(), rp_id.data(), rp_id.data() + rp_id.size());
return result;
}
class Cryptor {
public:
explicit Cryptor(std::string secret) : secret_(std::move(secret)) {}
Cryptor(Cryptor&&) = default;
Cryptor& operator=(Cryptor&&) = default;
~Cryptor() = default;
enum Algorithm : uint8_t {
kAes256Gcm = 0,
kHmacSha256 = 1,
kAes256GcmSiv = 2,
};
std::vector<uint8_t> Seal(Algorithm alg,
base::span<const uint8_t> nonce,
base::span<const uint8_t> plaintext,
base::span<const uint8_t> authenticated_data) const;
absl::optional<std::vector<uint8_t>> Unseal(
Algorithm alg,
base::span<const uint8_t> nonce,
base::span<const uint8_t> ciphertext,
base::span<const uint8_t> authenticated_data) const;
std::string HmacForStorage(base::StringPiece data) const;
private:
static absl::optional<crypto::Aead::AeadAlgorithm> ToAeadAlgorithm(
Algorithm alg);
std::string DeriveKey(Algorithm alg) const;
Cryptor(const Cryptor&) = delete;
Cryptor& operator=(const Cryptor&) = delete;
std::string secret_;
};
std::vector<uint8_t> Cryptor::Seal(
Algorithm algorithm,
base::span<const uint8_t> nonce,
base::span<const uint8_t> plaintext,
base::span<const uint8_t> authenticated_data) const {
const std::string key = DeriveKey(algorithm);
crypto::Aead aead(*ToAeadAlgorithm(algorithm));
aead.Init(&key);
return aead.Seal(plaintext, nonce, authenticated_data);
}
absl::optional<std::vector<uint8_t>> Cryptor::Unseal(
Algorithm algorithm,
base::span<const uint8_t> nonce,
base::span<const uint8_t> ciphertext,
base::span<const uint8_t> authenticated_data) const {
const std::string key = DeriveKey(algorithm);
crypto::Aead aead(*ToAeadAlgorithm(algorithm));
aead.Init(&key);
return aead.Open(ciphertext, nonce, authenticated_data);
}
std::string Cryptor::HmacForStorage(base::StringPiece data) const {
crypto::HMAC hmac(crypto::HMAC::SHA256);
const std::string key = DeriveKey(Algorithm::kHmacSha256);
std::vector<uint8_t> digest(hmac.DigestLength());
CHECK(hmac.Init(key));
CHECK(hmac.Sign(data, digest.data(), hmac.DigestLength()));
return base::HexEncode(digest.data(), digest.size());
}
absl::optional<crypto::Aead::AeadAlgorithm> Cryptor::ToAeadAlgorithm(
Algorithm alg) {
switch (alg) {
case Algorithm::kAes256Gcm:
return crypto::Aead::AES_256_GCM;
case Algorithm::kAes256GcmSiv:
return crypto::Aead::AES_256_GCM_SIV;
case Algorithm::kHmacSha256:
NOTREACHED() << "invalid AEAD";
return absl::nullopt;
}
}
std::string Cryptor::DeriveKey(Algorithm alg) const {
static constexpr size_t kKeyLength = 32u;
std::string key;
const uint8_t info = static_cast<uint8_t>(alg);
const bool hkdf_init =
::HKDF(reinterpret_cast<uint8_t*>(base::WriteInto(&key, kKeyLength + 1)),
kKeyLength, EVP_sha256(),
reinterpret_cast<const uint8_t*>(secret_.data()), secret_.size(),
nullptr , 0, &info, 1);
DCHECK(hkdf_init);
return key;
}
}
CredentialMetadata::Version CredentialMetadata::CurrentVersion() {
return base::FeatureList::IsEnabled(
kWebAuthnMacPlatformAuthenticatorOptionalUv)
? CredentialMetadata::Version::kV4
: CredentialMetadata::Version::kV3;
}
CredentialMetadata CredentialMetadata::FromPublicKeyCredentialUserEntity(
const PublicKeyCredentialUserEntity& user,
bool is_resident) {
return CredentialMetadata(
CurrentVersion(),
user.id,
user.name.value_or(""),
user.display_name.value_or(""),
is_resident,
CredentialMetadata::SignCounter::kZero);
}
PublicKeyCredentialUserEntity
CredentialMetadata::ToPublicKeyCredentialUserEntity() const {
PublicKeyCredentialUserEntity user_entity(user_id);
if (!user_name.empty()) {
user_entity.name = user_name;
}
if (!user_display_name.empty()) {
user_entity.display_name = user_display_name;
}
return user_entity;
}
CredentialMetadata::CredentialMetadata(Version version,
std::vector<uint8_t> user_id,
std::string user_name,
std::string user_display_name,
bool is_resident,
SignCounter counter_type)
: version(version),
user_id(std::move(user_id)),
user_name(std::move(user_name)),
user_display_name(std::move(user_display_name)),
is_resident(is_resident),
sign_counter_type(counter_type) {}
CredentialMetadata::CredentialMetadata(const CredentialMetadata&) = default;
CredentialMetadata::CredentialMetadata(CredentialMetadata&&) = default;
CredentialMetadata& CredentialMetadata::operator=(const CredentialMetadata&) =
default;
CredentialMetadata& CredentialMetadata::operator=(CredentialMetadata&&) =
default;
CredentialMetadata::~CredentialMetadata() = default;
bool CredentialMetadata::operator==(const CredentialMetadata& other) const {
return version == other.version && user_id == other.user_id &&
user_name == other.user_name &&
user_display_name == other.user_display_name &&
is_resident == other.is_resident &&
sign_counter_type == other.sign_counter_type;
}
std::string GenerateCredentialMetadataSecret() {
static constexpr size_t kSecretSize = 32u;
std::string secret;
RAND_bytes(
reinterpret_cast<uint8_t*>(base::WriteInto(&secret, kSecretSize + 1)),
kSecretSize);
return secret;
}
static std::string MaybeTruncateWithTrailingEllipsis(const std::string& in) {
constexpr size_t kMaxLength = 70u;
if (in.size() <= kMaxLength) {
return in;
}
std::string out;
base::TruncateUTF8ToByteSize(in, kMaxLength - 3, &out);
out += "…";
return out;
}
std::vector<uint8_t> SealCredentialMetadata(
const std::string& secret,
const std::string& rp_id,
const CredentialMetadata& metadata) {
DCHECK_GE(metadata.version, CredentialMetadata::Version::kV3);
cbor::Value::ArrayValue cbor_metadata;
cbor_metadata.emplace_back(cbor::Value(metadata.user_id));
cbor_metadata.emplace_back(
cbor::Value(MaybeTruncateWithTrailingEllipsis(metadata.user_name),
cbor::Value::Type::BYTE_STRING));
cbor_metadata.emplace_back(
cbor::Value(MaybeTruncateWithTrailingEllipsis(metadata.user_display_name),
cbor::Value::Type::BYTE_STRING));
cbor_metadata.emplace_back(cbor::Value(metadata.is_resident));
cbor_metadata.emplace_back(
cbor::Value(static_cast<uint8_t>(metadata.sign_counter_type)));
absl::optional<std::vector<uint8_t>> pt =
cbor::Writer::Write(cbor::Value(std::move(cbor_metadata)));
DCHECK(pt);
std::vector<uint8_t> nonce(kNonceLength);
RAND_bytes(nonce.data(), nonce.size());
const std::vector<uint8_t> ct =
Cryptor(secret).Seal(Cryptor::Algorithm::kAes256Gcm, nonce, *pt,
MakeAad(metadata.version, rp_id));
nonce.insert(nonce.end(), ct.begin(), ct.end());
return nonce;
}
static absl::optional<CredentialMetadata> UnsealLegacyCredentialId(
const std::string& secret,
const std::string& rp_id,
base::span<const uint8_t> credential_id) {
if (credential_id.size() <= 1 + kNonceLength ||
(credential_id[0] !=
static_cast<uint8_t>(CredentialMetadata::Version::kV0) &&
credential_id[0] !=
static_cast<uint8_t>(CredentialMetadata::Version::kV1))) {
return absl::nullopt;
}
auto version = static_cast<CredentialMetadata::Version>(credential_id[0]);
absl::optional<std::vector<uint8_t>> plaintext = Cryptor(secret).Unseal(
Cryptor::Algorithm::kAes256Gcm, credential_id.subspan(1, kNonceLength),
credential_id.subspan(1 + kNonceLength), MakeAad(version, rp_id));
if (!plaintext) {
return absl::nullopt;
}
absl::optional<cbor::Value> maybe_array = cbor::Reader::Read(*plaintext);
if (!maybe_array || !maybe_array->is_array()) {
return absl::nullopt;
}
const cbor::Value::ArrayValue& array = maybe_array->GetArray();
if (array.size() < 3 || !array[0].is_bytestring() ||
!array[1].is_bytestring() || !array[2].is_bytestring()) {
return absl::nullopt;
}
auto user_id = array[0].GetBytestring();
auto user_name = array[1].GetBytestringAsString();
auto user_display_name = array[2].GetBytestringAsString();
bool is_resident = false;
DCHECK(version == CredentialMetadata::Version::kV0 ||
version == CredentialMetadata::Version::kV1);
if (version == CredentialMetadata::Version::kV0 && array.size() != 3) {
return absl::nullopt;
}
if (version == CredentialMetadata::Version::kV1) {
if (array.size() != 4 || !array[3].is_bool()) {
return absl::nullopt;
}
is_resident = array[3].GetBool();
}
return CredentialMetadata(
version,
user_id,
std::string(user_name),
std::string(user_display_name),
is_resident,
CredentialMetadata::SignCounter::kTimestamp);
}
static absl::optional<CredentialMetadata> UnsealV2OrLaterCredentialMetadata(
CredentialMetadata::Version version,
const std::string& secret,
const std::string& rp_id,
base::span<const uint8_t> credential_id) {
DCHECK_GE(version, CredentialMetadata::Version::kV2);
if (credential_id.size() <= kNonceLength) {
return absl::nullopt;
}
absl::optional<std::vector<uint8_t>> plaintext = Cryptor(secret).Unseal(
Cryptor::Algorithm::kAes256Gcm, credential_id.subspan(0, kNonceLength),
credential_id.subspan(kNonceLength), MakeAad(version, rp_id));
if (!plaintext) {
return absl::nullopt;
}
absl::optional<cbor::Value> maybe_array = cbor::Reader::Read(base::make_span(
reinterpret_cast<const uint8_t*>(plaintext->data()), plaintext->size()));
if (!maybe_array || !maybe_array->is_array()) {
return absl::nullopt;
}
const cbor::Value::ArrayValue& array = maybe_array->GetArray();
if (array.size() < 4 || !array[0].is_bytestring() ||
!array[1].is_bytestring() || !array[2].is_bytestring() ||
!array[3].is_bool()) {
return absl::nullopt;
}
if (version == CredentialMetadata::Version::kV2) {
if (array.size() != 4) {
return absl::nullopt;
}
return CredentialMetadata(
CredentialMetadata::Version::kV2, array[0].GetBytestring(),
std::string(array[1].GetBytestringAsString()),
std::string(array[2].GetBytestringAsString()), array[3].GetBool(),
CredentialMetadata::SignCounter::kZero);
}
static_assert(
CredentialMetadata::Version::MAX_VERSION ==
CredentialMetadata::Version::kV4,
"Ensure unsealing code is able to handle added CredentialMetadata "
"versions");
DCHECK_GE(version, CredentialMetadata::Version::kV3);
if (array.size() != 5) {
return absl::nullopt;
}
const int64_t counter_type = array[4].GetUnsigned();
if (counter_type < 1) {
return absl::nullopt;
}
return CredentialMetadata(version, array[0].GetBytestring(),
std::string(array[1].GetBytestringAsString()),
std::string(array[2].GetBytestringAsString()),
array[3].GetBool(),
CredentialMetadata::SignCounter(counter_type));
}
absl::optional<CredentialMetadata> UnsealMetadataFromLegacyCredentialId(
const std::string& secret,
const std::string& rp_id,
base::span<const uint8_t> credential_id) {
absl::optional<CredentialMetadata> credential_metadata =
UnsealV2OrLaterCredentialMetadata(CredentialMetadata::Version::kV2,
secret, rp_id, credential_id);
if (credential_metadata) {
return credential_metadata;
}
return UnsealLegacyCredentialId(secret, rp_id, credential_id);
}
absl::optional<CredentialMetadata> UnsealMetadataFromApplicationTag(
const std::string& secret,
const std::string& rp_id,
base::span<const uint8_t> application_tag) {
static_assert(
CredentialMetadata::Version::MAX_VERSION ==
CredentialMetadata::Version::kV4,
"Ensure unsealing code is able to handle added CredentialMetadata "
"versions");
for (const auto version :
{CredentialMetadata::Version::kV3, CredentialMetadata::Version::kV4}) {
if (absl::optional<CredentialMetadata> metadata =
UnsealV2OrLaterCredentialMetadata(version, secret, rp_id,
application_tag)) {
return metadata;
}
}
return absl::nullopt;
}
std::string EncodeRpIdAndUserIdDeprecated(const std::string& secret,
const std::string& rp_id,
base::span<const uint8_t> user_id) {
const auto* user_id_data = reinterpret_cast<const char*>(user_id.data());
return Cryptor(secret).HmacForStorage(
rp_id + "/" + std::string(user_id_data, user_id_data + user_id.size()));
}
std::string EncodeRpId(const std::string& secret, const std::string& rp_id) {
static constexpr std::array<uint8_t, kNonceLength> fixed_zero_nonce = {};
base::span<const uint8_t> pt(reinterpret_cast<const uint8_t*>(rp_id.data()),
rp_id.size());
const std::vector<uint8_t> ct =
Cryptor(secret).Seal(Cryptor::Algorithm::kAes256GcmSiv, fixed_zero_nonce,
pt, {});
return base::HexEncode(ct.data(), ct.size());
}
absl::optional<std::string> DecodeRpId(const std::string& secret,
const std::string& ciphertext) {
std::vector<uint8_t> ct;
if (!base::HexStringToBytes(ciphertext, &ct)) {
return absl::nullopt;
}
static constexpr std::array<uint8_t, kNonceLength> fixed_zero_nonce = {};
absl::optional<std::vector<uint8_t>> pt = Cryptor(secret).Unseal(
Cryptor::Algorithm::kAes256GcmSiv, fixed_zero_nonce, ct,
{});
if (!pt) {
return absl::nullopt;
}
return std::string(pt->begin(), pt->end());
}
std::vector<uint8_t> SealLegacyCredentialIdForTestingOnly(
CredentialMetadata::Version version,
const std::string& secret,
const std::string& rp_id,
const std::vector<uint8_t>& user_id,
const std::string& user_name,
const std::string& user_display_name,
bool is_resident) {
DCHECK_LT(version, CredentialMetadata::Version::kV3);
std::vector<uint8_t> result;
if (version < CredentialMetadata::Version::kV2) {
result.push_back(static_cast<uint8_t>(version));
}
auto nonce_begin = result.insert(result.end(), 12, 0);
base::span<uint8_t> nonce(nonce_begin, result.end());
DCHECK_EQ(nonce.size(), 12u);
RAND_bytes(nonce.data(), nonce.size());
cbor::Value::ArrayValue cbor_metadata;
cbor_metadata.emplace_back(cbor::Value(user_id));
cbor_metadata.emplace_back(
cbor::Value(user_name, cbor::Value::Type::BYTE_STRING));
cbor_metadata.emplace_back(
cbor::Value(user_display_name, cbor::Value::Type::BYTE_STRING));
DCHECK(version > CredentialMetadata::Version::kV0 || !is_resident);
if (version > CredentialMetadata::Version::kV0) {
cbor_metadata.emplace_back(cbor::Value(is_resident));
}
absl::optional<std::vector<uint8_t>> pt =
cbor::Writer::Write(cbor::Value(std::move(cbor_metadata)));
DCHECK(pt);
std::vector<uint8_t> aad;
aad.push_back(static_cast<uint8_t>(version));
aad.insert(aad.end(), rp_id.data(), rp_id.data() + rp_id.size());
const std::vector<uint8_t> ct =
Cryptor(secret).Seal(Cryptor::Algorithm::kAes256Gcm, nonce, *pt, aad);
result.insert(result.end(), ct.begin(), ct.end());
return result;
}
}
}
}