Commit 8f5c2474 authored by Nina Satragno's avatar Nina Satragno Committed by Commit Bot

[fido] Large blob encryption & decryption

Implement encrypting and decrypting large blobs, and add a test for
updating blobs which exercises the new code path.

Bug: 1114875
Change-Id: Ief91f642fa5ccc34f7ef2d00c90467fbcb90bc5e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2415218
Commit-Queue: Nina Satragno <nsatragno@chromium.org>
Auto-Submit: Nina Satragno <nsatragno@chromium.org>
Reviewed-by: default avatarMartin Kreichgauer <martinkr@google.com>
Cr-Commit-Position: refs/heads/master@{#808134}
parent 1fd148fc
......@@ -15,6 +15,7 @@
#include "device/fido/test_callback_receiver.h"
#include "device/fido/virtual_ctap2_device.h"
#include "device/fido/virtual_fido_device.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace device {
......@@ -27,8 +28,11 @@ using ReadCallback = device::test::StatusAndValueCallbackReceiver<
CtapDeviceResponseCode,
base::Optional<std::vector<std::pair<LargeBlobKey, std::vector<uint8_t>>>>>;
constexpr LargeBlobKey kDummyKey = {{0x01}};
constexpr std::array<uint8_t, 4> kSmallBlob = {'l', 'u', 'm', 'a'};
constexpr LargeBlobKey kDummyKey1 = {{0x01}};
constexpr LargeBlobKey kDummyKey2 = {{0x02}};
constexpr std::array<uint8_t, 4> kSmallBlob1 = {'r', 'o', 's', 'a'};
constexpr std::array<uint8_t, 4> kSmallBlob2 = {'l', 'u', 'm', 'a'};
constexpr std::array<uint8_t, 4> kSmallBlob3 = {'s', 't', 'a', 'r'};
constexpr size_t kMaxStorageSize = 4096;
class FidoDeviceAuthenticatorTest : public testing::Test {
......@@ -66,7 +70,7 @@ class FidoDeviceAuthenticatorTest : public testing::Test {
TEST_F(FidoDeviceAuthenticatorTest, TestReadEmptyLargeBlob) {
ReadCallback callback;
authenticator_->ReadLargeBlob({kDummyKey}, base::nullopt,
authenticator_->ReadLargeBlob({kDummyKey1}, base::nullopt,
callback.callback());
callback.WaitForCallback();
......@@ -77,7 +81,7 @@ TEST_F(FidoDeviceAuthenticatorTest, TestReadEmptyLargeBlob) {
TEST_F(FidoDeviceAuthenticatorTest, TestReadInvalidLargeBlob) {
authenticator_state_->large_blob[0] += 1;
ReadCallback callback;
authenticator_->ReadLargeBlob({kDummyKey}, base::nullopt,
authenticator_->ReadLargeBlob({kDummyKey1}, base::nullopt,
callback.callback());
callback.WaitForCallback();
......@@ -88,23 +92,24 @@ TEST_F(FidoDeviceAuthenticatorTest, TestReadInvalidLargeBlob) {
// Test reading and writing a blob that fits in a single fragment.
TEST_F(FidoDeviceAuthenticatorTest, TestWriteSmallBlob) {
std::vector<uint8_t> small_blob = fido_parsing_utils::Materialize(kSmallBlob);
std::vector<uint8_t> small_blob =
fido_parsing_utils::Materialize(kSmallBlob1);
WriteCallback write_callback;
authenticator_->WriteLargeBlob(small_blob, {kDummyKey}, base::nullopt,
authenticator_->WriteLargeBlob(small_blob, {kDummyKey1}, base::nullopt,
write_callback.callback());
write_callback.WaitForCallback();
ASSERT_EQ(CtapDeviceResponseCode::kSuccess, write_callback.value());
ReadCallback read_callback;
authenticator_->ReadLargeBlob({kDummyKey}, base::nullopt,
authenticator_->ReadLargeBlob({kDummyKey1}, base::nullopt,
read_callback.callback());
read_callback.WaitForCallback();
ASSERT_EQ(CtapDeviceResponseCode::kSuccess, read_callback.status());
auto large_blob_array = read_callback.value();
ASSERT_TRUE(large_blob_array);
ASSERT_EQ(1u, large_blob_array->size());
EXPECT_EQ(kDummyKey, large_blob_array->at(0).first);
EXPECT_EQ(kDummyKey1, large_blob_array->at(0).first);
EXPECT_EQ(small_blob, large_blob_array->at(0).second);
}
......@@ -117,24 +122,63 @@ TEST_F(FidoDeviceAuthenticatorTest, TestWriteLargeBlob) {
}
WriteCallback write_callback;
authenticator_->WriteLargeBlob(large_blob, {kDummyKey}, base::nullopt,
authenticator_->WriteLargeBlob(large_blob, {kDummyKey1}, base::nullopt,
write_callback.callback());
write_callback.WaitForCallback();
ASSERT_EQ(CtapDeviceResponseCode::kSuccess, write_callback.value());
ReadCallback read_callback;
authenticator_->ReadLargeBlob({kDummyKey}, base::nullopt,
authenticator_->ReadLargeBlob({kDummyKey1}, base::nullopt,
read_callback.callback());
read_callback.WaitForCallback();
ASSERT_EQ(CtapDeviceResponseCode::kSuccess, read_callback.status());
auto large_blob_array = read_callback.value();
ASSERT_TRUE(large_blob_array);
ASSERT_EQ(1u, large_blob_array->size());
EXPECT_EQ(kDummyKey, large_blob_array->at(0).first);
EXPECT_EQ(kDummyKey1, large_blob_array->at(0).first);
EXPECT_EQ(large_blob, large_blob_array->at(0).second);
}
// Test updating a large blob in an array with multiple entries corresponding to
// other keys.
TEST_F(FidoDeviceAuthenticatorTest, TestUpdateLargeBlob) {
WriteCallback write_callback1;
authenticator_->WriteLargeBlob(fido_parsing_utils::Materialize(kSmallBlob1),
{kDummyKey1}, base::nullopt,
write_callback1.callback());
write_callback1.WaitForCallback();
ASSERT_EQ(CtapDeviceResponseCode::kSuccess, write_callback1.value());
WriteCallback write_callback2;
std::vector<uint8_t> small_blob2 =
fido_parsing_utils::Materialize(kSmallBlob2);
authenticator_->WriteLargeBlob(small_blob2, {kDummyKey2}, base::nullopt,
write_callback2.callback());
write_callback2.WaitForCallback();
ASSERT_EQ(CtapDeviceResponseCode::kSuccess, write_callback2.value());
// Update the first entry.
WriteCallback write_callback3;
std::vector<uint8_t> small_blob3 =
fido_parsing_utils::Materialize(kSmallBlob3);
authenticator_->WriteLargeBlob(small_blob3, {kDummyKey1}, base::nullopt,
write_callback3.callback());
write_callback3.WaitForCallback();
ASSERT_EQ(CtapDeviceResponseCode::kSuccess, write_callback3.value());
ReadCallback read_callback;
authenticator_->ReadLargeBlob({kDummyKey1, kDummyKey2}, base::nullopt,
read_callback.callback());
read_callback.WaitForCallback();
ASSERT_EQ(CtapDeviceResponseCode::kSuccess, read_callback.status());
auto large_blob_array = read_callback.value();
ASSERT_TRUE(large_blob_array);
EXPECT_THAT(*large_blob_array, testing::UnorderedElementsAre(
std::make_pair(kDummyKey1, small_blob3),
std::make_pair(kDummyKey2, small_blob2)));
}
} // namespace
} // namespace device
......@@ -139,6 +139,17 @@ bool CopyCBORBytestring(std::array<uint8_t, N>* out,
return ExtractArray(bytestring, /*pos=*/0, out);
}
constexpr std::array<uint8_t, 4> Uint32LittleEndian(uint32_t value) {
return {value & 0xFF, value >> 8 & 0xFF, value >> 16 & 0xFF,
value >> 24 & 0xFF};
}
constexpr std::array<uint8_t, 8> Uint64LittleEndian(uint64_t value) {
return {value & 0xFF, value >> 8 & 0xFF, value >> 16 & 0xFF,
value >> 24 & 0xFF, value >> 32 & 0xFF, value >> 40 & 0xFF,
value >> 48 & 0xFF, value >> 56 & 0xFF};
}
} // namespace fido_parsing_utils
} // namespace device
......
......@@ -6,6 +6,8 @@
#include "base/containers/span.h"
#include "components/cbor/reader.h"
#include "components/cbor/writer.h"
#include "crypto/aead.h"
#include "crypto/random.h"
#include "crypto/sha2.h"
#include "device/fido/fido_parsing_utils.h"
#include "device/fido/pin.h"
......@@ -15,6 +17,21 @@ namespace device {
namespace {
// The number of bytes the large blob validation hash is truncated to.
constexpr size_t kTruncatedHashBytes = 16;
constexpr std::array<uint8_t, 4> kLargeBlobADPrefix = {'b', 'l', 'o', 'b'};
constexpr size_t kAssociatedDataLength = kLargeBlobADPrefix.size() + 8;
std::array<uint8_t, kAssociatedDataLength> GenerateLargeBlobAdditionalData(
size_t size) {
std::array<uint8_t, kAssociatedDataLength> additional_data;
const std::array<uint8_t, 8>& size_array =
fido_parsing_utils::Uint64LittleEndian(size);
std::copy(kLargeBlobADPrefix.begin(), kLargeBlobADPrefix.end(),
additional_data.begin());
std::copy(size_array.begin(), size_array.end(),
additional_data.begin() + kLargeBlobADPrefix.size());
return additional_data;
}
} // namespace
LargeBlobArrayFragment::LargeBlobArrayFragment(const std::vector<uint8_t> bytes,
......@@ -150,7 +167,8 @@ base::Optional<LargeBlobData> LargeBlobData::Parse(const cbor::Value& value) {
}
auto nonce_it =
map.find(cbor::Value(static_cast<int>(LargeBlobDataKeys::kNonce)));
if (nonce_it == map.end() || !nonce_it->second.is_bytestring()) {
if (nonce_it == map.end() || !nonce_it->second.is_bytestring() ||
nonce_it->second.GetBytestring().size() != kLargeBlobArrayNonceLength) {
return base::nullopt;
}
auto orig_size_it =
......@@ -159,21 +177,25 @@ base::Optional<LargeBlobData> LargeBlobData::Parse(const cbor::Value& value) {
return base::nullopt;
}
return LargeBlobData(ciphertext_it->second.GetBytestring(),
nonce_it->second.GetBytestring(),
base::make_span<kLargeBlobArrayNonceLength>(
nonce_it->second.GetBytestring()),
orig_size_it->second.GetUnsigned());
}
LargeBlobData::LargeBlobData(std::vector<uint8_t> ciphertext,
std::vector<uint8_t> nonce,
int64_t orig_size)
: ciphertext_(std::move(ciphertext)),
nonce_(std::move(nonce)),
orig_size_(std::move(orig_size)) {}
LargeBlobData::LargeBlobData(
std::vector<uint8_t> ciphertext,
base::span<const uint8_t, kLargeBlobArrayNonceLength> nonce,
int64_t orig_size)
: ciphertext_(std::move(ciphertext)), orig_size_(std::move(orig_size)) {
std::copy(nonce.begin(), nonce.end(), nonce_.begin());
}
LargeBlobData::LargeBlobData(LargeBlobKey key, std::vector<uint8_t> blob) {
// TODO(nsatragno): implement encrypting the data. For now, just store and
// return the blob as plaintext.
orig_size_ = blob.size();
ciphertext_ = blob;
crypto::Aead aead(crypto::Aead::AeadAlgorithm::AES_256_GCM);
aead.Init(key);
crypto::RandBytes(nonce_);
ciphertext_ =
aead.Seal(blob, nonce_, GenerateLargeBlobAdditionalData(orig_size_));
}
LargeBlobData::LargeBlobData(LargeBlobData&&) = default;
LargeBlobData& LargeBlobData::operator=(LargeBlobData&&) = default;
......@@ -186,9 +208,10 @@ bool LargeBlobData::operator==(const LargeBlobData& other) const {
base::Optional<std::vector<uint8_t>> LargeBlobData::Decrypt(
LargeBlobKey key) const {
// TODO(nsatragno): implement decrypting the data. For now, store and return
// the blob as plaintext.
return ciphertext_;
crypto::Aead aead(crypto::Aead::AeadAlgorithm::AES_256_GCM);
aead.Init(key);
return aead.Open(ciphertext_, nonce_,
GenerateLargeBlobAdditionalData(orig_size_));
}
cbor::Value::MapValue LargeBlobData::AsCBOR() const {
......
......@@ -47,6 +47,7 @@ using LargeBlobKey = std::array<uint8_t, kLargeBlobKeyLength>;
constexpr size_t kLargeBlobDefaultMaxFragmentLength = 960;
constexpr size_t kLargeBlobReadEncodingOverhead = 64;
constexpr size_t kLargeBlobArrayNonceLength = 12;
struct COMPONENT_EXPORT(DEVICE_FIDO) LargeBlobArrayFragment {
LargeBlobArrayFragment(std::vector<uint8_t> bytes, size_t offset);
......@@ -130,10 +131,10 @@ class COMPONENT_EXPORT(DEVICE_FIDO) LargeBlobData {
private:
LargeBlobData(std::vector<uint8_t> ciphertext,
std::vector<uint8_t> nonce,
base::span<const uint8_t, kLargeBlobArrayNonceLength> nonce,
int64_t orig_size);
std::vector<uint8_t> ciphertext_;
std::vector<uint8_t> nonce_;
std::array<uint8_t, kLargeBlobArrayNonceLength> nonce_;
int64_t orig_size_;
};
......
......@@ -27,10 +27,11 @@ const std::array<uint8_t, 17> kInvalidLargeBlobArray = {
// An "valid" CBOR large blob array with two entries. The first entry is not a
// valid large blob map structure. The second entry is valid.
const std::array<uint8_t, 35> kValidLargeBlobArray = {
const std::array<uint8_t, 45> kValidLargeBlobArray = {
0x82, 0xA2, 0x02, 0x42, 0x11, 0x11, 0x03, 0x02, 0xA3, 0x01, 0x42, 0x22,
0x22, 0x02, 0x42, 0x33, 0x33, 0x03, 0x02, 0x53, 0x5b, 0xaa, 0xd2, 0x7a,
0x26, 0x68, 0x34, 0x9e, 0xc3, 0x90, 0xd3, 0x9a, 0x1c, 0x0a, 0xae};
0x22, 0x02, 0x4C, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
0x30, 0x31, 0x32, 0x03, 0x02, 0x9b, 0x33, 0x75, 0x6c, 0x0a, 0x84, 0xdf,
0x32, 0xcc, 0xd0, 0xc8, 0x96, 0xea, 0xa7, 0x99, 0x13};
TEST_F(FidoLargeBlobTest, VerifyLargeBlobArrayIntegrityValid) {
std::vector<uint8_t> large_blob_array =
......@@ -113,7 +114,7 @@ TEST_F(FidoLargeBlobTest, LargeBlobArrayWriter_PopUnevenly) {
// Test popping the large blob array in a fragment size that evenly divides the
// length of the array.
TEST_F(FidoLargeBlobTest, LargeBlobArrayFragments_PopEvenly) {
const size_t fragment_size = 7;
const size_t fragment_size = 9;
const size_t expected_fragments = kValidLargeBlobArray.size() / fragment_size;
size_t fragments = 0;
ASSERT_EQ(0u, kValidLargeBlobArray.size() % fragment_size);
......
......@@ -70,11 +70,6 @@ constexpr uint8_t kSupportedPermissionsMask =
static_cast<uint8_t>(pin::Permissions::kCredentialManagement) |
static_cast<uint8_t>(pin::Permissions::kBioEnrollment);
constexpr std::array<uint8_t, 4> Uint32LittleEndian(int64_t value) {
return {value & 0xFF, value >> 8 & 0xFF, value >> 16 & 0xFF,
value >> 24 & 0xFF};
}
struct PinUvAuthTokenPermissions {
uint8_t permissions;
base::Optional<std::string> rp_id;
......@@ -2199,7 +2194,7 @@ CtapDeviceResponseCode VirtualCtap2Device::OnLargeBlobs(
if (offset_it == request_map.end() || !offset_it->second.is_unsigned()) {
return CtapDeviceResponseCode::kCtap1ErrInvalidParameter;
}
const size_t offset = offset_it->second.GetUnsigned();
const uint64_t offset = offset_it->second.GetUnsigned();
const auto get_it = request_map.find(
cbor::Value(static_cast<uint8_t>(LargeBlobsRequestKey::kGet)));
......@@ -2221,7 +2216,7 @@ CtapDeviceResponseCode VirtualCtap2Device::OnLargeBlobs(
if (length_it != request_map.end()) {
return CtapDeviceResponseCode::kCtap1ErrInvalidParameter;
}
const size_t get = get_it->second.GetUnsigned();
const uint64_t get = get_it->second.GetUnsigned();
if (get > max_fragment_length) {
return CtapDeviceResponseCode::kCtap1ErrInvalidLength;
}
......@@ -2246,7 +2241,7 @@ CtapDeviceResponseCode VirtualCtap2Device::OnLargeBlobs(
if (length_it == request_map.end() || !length_it->second.is_unsigned()) {
return CtapDeviceResponseCode::kCtap1ErrInvalidParameter;
}
const size_t length = length_it->second.GetUnsigned();
const uint64_t length = length_it->second.GetUnsigned();
if (length > config_.available_large_blob_storage) {
return CtapDeviceResponseCode::kCtap2ErrLargeBlobStorageFull;
}
......@@ -2302,7 +2297,7 @@ CtapDeviceResponseCode VirtualCtap2Device::OnLargeBlobs(
kPinUvAuthTokenSafetyPadding.begin(),
kPinUvAuthTokenSafetyPadding.end());
pinauth_bytes.insert(pinauth_bytes.end(), {0x0c, 0x00});
auto offset_vec = Uint32LittleEndian(offset);
auto offset_vec = fido_parsing_utils::Uint32LittleEndian(offset);
pinauth_bytes.insert(pinauth_bytes.end(), offset_vec.begin(),
offset_vec.end());
pinauth_bytes.insert(pinauth_bytes.end(), set.begin(), set.end());
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment