blob: 60d9feafd77999aff219b23b27e2b98ebfc225d2 [file] [log] [blame]
// Copyright 2018 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 "device/fido/mac/credential_metadata.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "components/cbor/cbor_reader.h"
#include "components/cbor/cbor_values.h"
#include "components/cbor/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 {
using cbor::CBORWriter;
using cbor::CBORReader;
using cbor::CBORValue;
// static
std::string CredentialMetadata::GenerateRandomSecret() {
static constexpr size_t kSecretSize = 32u;
std::string secret;
RAND_bytes(
reinterpret_cast<uint8_t*>(base::WriteInto(&secret, kSecretSize + 1)),
kSecretSize);
return secret;
}
CredentialMetadata::CredentialMetadata(const std::string& secret)
: secret_(secret) {}
CredentialMetadata::~CredentialMetadata() = default;
// static
CredentialMetadata::UserEntity
CredentialMetadata::UserEntity::FromPublicKeyCredentialUserEntity(
const PublicKeyCredentialUserEntity& user) {
return CredentialMetadata::UserEntity(user.user_id(),
user.user_name().value_or(""),
user.user_display_name().value_or(""));
}
PublicKeyCredentialUserEntity
CredentialMetadata::UserEntity::ToPublicKeyCredentialUserEntity() {
auto user_entity = PublicKeyCredentialUserEntity(id);
if (!name.empty()) {
user_entity.SetUserName(name);
}
if (!display_name.empty()) {
user_entity.SetDisplayName(display_name);
}
return user_entity;
}
CredentialMetadata::UserEntity::UserEntity(std::vector<uint8_t> id_,
std::string name_,
std::string display_name_)
: id(std::move(id_)),
name(std::move(name_)),
display_name(std::move(display_name_)) {}
CredentialMetadata::UserEntity::UserEntity(
const CredentialMetadata::UserEntity&) = default;
CredentialMetadata::UserEntity::UserEntity(CredentialMetadata::UserEntity&&) =
default;
CredentialMetadata::UserEntity& CredentialMetadata::UserEntity::operator=(
CredentialMetadata::UserEntity&&) = default;
CredentialMetadata::UserEntity::~UserEntity() = default;
static constexpr size_t kNonceLength = 12;
// static
base::Optional<std::vector<uint8_t>> CredentialMetadata::SealCredentialId(
const std::string& secret,
const std::string& rp_id,
const UserEntity& user) {
CredentialMetadata cryptor(secret);
// The first 13 bytes are the version and nonce.
std::vector<uint8_t> result(1 + kNonceLength);
result[0] = kVersion;
// Pick a random nonce. N.B. the nonce is similar to an IV. It needs to be
// distinct (but not necessarily random). Nonce reuse breaks confidentiality
// (in particular, it leaks the XOR of the plaintexts encrypted under the
// same nonce and key).
base::span<uint8_t> nonce(result.data() + 1, kNonceLength);
RAND_bytes(nonce.data(), nonce.size()); // RAND_bytes always returns 1.
// The remaining bytes are the CBOR-encoded UserEntity, encrypted with
// AES-256-GCM and authenticated with the version and RP ID.
CBORValue::ArrayValue cbor_user;
cbor_user.emplace_back(CBORValue(user.id));
cbor_user.emplace_back(CBORValue(user.name, CBORValue::Type::BYTE_STRING));
cbor_user.emplace_back(
CBORValue(user.display_name, CBORValue::Type::BYTE_STRING));
base::Optional<std::vector<uint8_t>> pt =
CBORWriter::Write(CBORValue(std::move(cbor_user)));
if (!pt) {
return base::nullopt;
}
base::Optional<std::string> ciphertext = cryptor.Seal(
CredentialMetadata::Algorithm::kAes256Gcm, nonce, *pt, MakeAad(rp_id));
if (!ciphertext) {
return base::nullopt;
}
base::span<const char> cts(reinterpret_cast<const char*>(ciphertext->data()),
ciphertext->size());
result.insert(result.end(), cts.begin(), cts.end());
return result;
}
// static
base::Optional<CredentialMetadata::UserEntity>
CredentialMetadata::UnsealCredentialId(
const std::string& secret,
const std::string& rp_id,
base::span<const uint8_t> credential_id) {
CredentialMetadata cryptor(secret);
// Recover the nonce and check for the correct version byte. Then try to
// decrypt the remaining bytes.
if (credential_id.size() <= 1 + kNonceLength ||
credential_id[0] != kVersion) {
return base::nullopt;
}
base::Optional<std::string> plaintext =
cryptor.Unseal(CredentialMetadata::Algorithm::kAes256Gcm,
credential_id.subspan(1, kNonceLength),
credential_id.subspan(1 + kNonceLength), MakeAad(rp_id));
if (!plaintext) {
return base::nullopt;
}
// The recovered plaintext should decode into the UserEntity struct.
base::Optional<CBORValue> maybe_array = CBORReader::Read(base::make_span(
reinterpret_cast<const uint8_t*>(plaintext->data()), plaintext->size()));
if (!maybe_array || !maybe_array->is_array()) {
return base::nullopt;
}
const CBORValue::ArrayValue& array = maybe_array->GetArray();
if (array.size() != 3 || !array[0].is_bytestring() ||
!array[1].is_bytestring() || !array[2].is_bytestring()) {
return base::nullopt;
}
return UserEntity(array[0].GetBytestring(),
array[1].GetBytestringAsString().as_string(),
array[2].GetBytestringAsString().as_string());
}
// static
base::Optional<std::string> CredentialMetadata::EncodeRpIdAndUserId(
const std::string& secret,
const std::string& rp_id,
base::span<const uint8_t> user_id) {
// Encoding RP ID along with the user ID hides whether the same user ID was
// reused on different RPs.
const auto* user_id_data = reinterpret_cast<const char*>(user_id.data());
return CredentialMetadata(secret).HmacForStorage(
rp_id + "/" + std::string(user_id_data, user_id_data + user_id.size()));
}
// static
base::Optional<std::string> CredentialMetadata::EncodeRpId(
const std::string& secret,
const std::string& rp_id) {
// Encrypt with a fixed nonce to make the result deterministic while still
// allowing the RP ID to be recovered from the ciphertext later.
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());
std::string empty_ad;
// Using AES-GCM with a fixed nonce would break confidentiality, so this uses
// AES-GCM-SIV instead.
base::Optional<std::string> ct = CredentialMetadata(secret).Seal(
CredentialMetadata::Algorithm::kAes256GcmSiv, fixed_zero_nonce, pt,
empty_ad);
if (!ct) {
return base::nullopt;
}
// The keychain field that stores the encrypted RP ID only accepts NSString
// (not NSData), so we HexEncode to ensure the result is UTF-8-decodable.
return base::HexEncode(ct->data(), ct->size());
}
// static
base::Optional<std::string> CredentialMetadata::DecodeRpId(
const std::string& secret,
const std::string& ciphertext) {
std::vector<uint8_t> ct;
if (!base::HexStringToBytes(ciphertext, &ct)) {
return base::nullopt;
}
static constexpr std::array<uint8_t, kNonceLength> fixed_zero_nonce = {};
std::string empty_ad;
return CredentialMetadata(secret).Unseal(
CredentialMetadata::Algorithm::kAes256GcmSiv, fixed_zero_nonce, ct,
empty_ad);
}
// static
std::string CredentialMetadata::MakeAad(const std::string& rp_id) {
return std::string(1, kVersion) + rp_id;
}
// static
std::string CredentialMetadata::DeriveKey(base::StringPiece secret,
Algorithm alg) {
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 /* salt */, 0, &info, 1);
DCHECK(hkdf_init);
return key;
}
// static
base::Optional<crypto::Aead::AeadAlgorithm> CredentialMetadata::ToAeadAlgorithm(
Algorithm alg) {
switch (alg) {
case CredentialMetadata::Algorithm::kAes256Gcm:
return crypto::Aead::AES_256_GCM;
case CredentialMetadata::Algorithm::kAes256GcmSiv:
return crypto::Aead::AES_256_GCM_SIV;
case CredentialMetadata::Algorithm::kHmacSha256:
NOTREACHED() << "invalid AEAD";
return base::nullopt;
}
}
base::Optional<std::string> CredentialMetadata::Seal(
CredentialMetadata::Algorithm algorithm,
base::span<const uint8_t> nonce,
base::span<const uint8_t> plaintext,
base::StringPiece authenticated_data) const {
auto opt_aead_algorithm = ToAeadAlgorithm(algorithm);
if (!opt_aead_algorithm)
return base::nullopt;
const std::string key = DeriveKey(secret_, algorithm);
crypto::Aead aead(*opt_aead_algorithm);
aead.Init(&key);
std::string ciphertext;
if (!aead.Seal(
base::StringPiece(reinterpret_cast<const char*>(plaintext.data()),
plaintext.size()),
base::StringPiece(reinterpret_cast<const char*>(nonce.data()),
nonce.size()),
authenticated_data, &ciphertext)) {
return base::nullopt;
}
return ciphertext;
}
base::Optional<std::string> CredentialMetadata::Unseal(
CredentialMetadata::Algorithm algorithm,
base::span<const uint8_t> nonce,
base::span<const uint8_t> ciphertext,
base::StringPiece authenticated_data) const {
auto opt_aead_algorithm = ToAeadAlgorithm(algorithm);
if (!opt_aead_algorithm)
return base::nullopt;
const std::string key = DeriveKey(secret_, algorithm);
crypto::Aead aead(*opt_aead_algorithm);
aead.Init(&key);
std::string plaintext;
if (!aead.Open(
base::StringPiece(reinterpret_cast<const char*>(ciphertext.data()),
ciphertext.size()),
base::StringPiece(reinterpret_cast<const char*>(nonce.data()),
nonce.size()),
authenticated_data, &plaintext)) {
return base::nullopt;
}
return plaintext;
}
base::Optional<std::string> CredentialMetadata::HmacForStorage(
base::StringPiece data) const {
crypto::HMAC hmac(crypto::HMAC::SHA256);
const std::string key = DeriveKey(secret_, Algorithm::kHmacSha256);
std::vector<uint8_t> digest(hmac.DigestLength());
if (!hmac.Init(key) || !hmac.Sign(data, digest.data(), hmac.DigestLength())) {
return base::nullopt;
}
// The keychain fields that store RP ID and User ID seem to only accept
// NSString (not NSData), so we HexEncode to ensure the result to be
// UTF-8-decodable.
return base::HexEncode(digest.data(), digest.size());
}
} // namespace mac
} // namespace fido
} // namespace device