blob: 0e903449b135d9f8c072be2867020b7d979871ac [file] [log] [blame]
// Copyright 2020 The Chromium OS 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 "u2fd/webauthn_storage.h"
#include <memory>
#include <optional>
#include <utility>
#include <vector>
#include <base/base64.h>
#include <base/check.h>
#include <base/files/file_enumerator.h>
#include <base/files/file_util.h>
#include <base/files/important_file_writer.h>
#include <base/json/json_reader.h>
#include <base/json/json_string_value_serializer.h>
#include <base/logging.h>
#include <base/strings/string_number_conversions.h>
#include <base/values.h>
#include "brillo/scoped_umask.h"
#include "u2fd/util.h"
namespace u2f {
using base::FilePath;
namespace {
constexpr const char kDaemonStorePath[] = "/run/daemon-store/u2f";
constexpr const char kWebAuthnDirName[] = "webauthn";
constexpr const char kRecordFileNamePrefix[] = "Record_";
// Members of the JSON file
constexpr const char kCredentialIdKey[] = "credential_id";
constexpr const char kSecretKey[] = "secret";
constexpr const char kKeyBlobKey[] = "key_blob";
constexpr const char kRpIdKey[] = "rp_id";
constexpr const char kRpDisplayNameKey[] = "rp_display_name";
constexpr const char kUserIdKey[] = "user_id";
constexpr const char kUserDisplayNameKey[] = "user_display_name";
constexpr const char kCreatedTimestampKey[] = "created";
constexpr const char kIsResidentKeyKey[] = "is_resident_key";
constexpr char kWebAuthnRecordCountMetric[] =
"WebAuthentication.ChromeOS.StartupRecordCount";
constexpr int kMinRecordCount = 0;
constexpr int kMaxRecordCount = 50;
constexpr int kRecordCountBuckets = 50;
} // namespace
WebAuthnStorage::WebAuthnStorage() : root_path_(kDaemonStorePath) {}
WebAuthnStorage::~WebAuthnStorage() = default;
bool WebAuthnStorage::WriteRecord(const WebAuthnRecord& record) {
DCHECK(allow_access_ && !sanitized_user_.empty());
const std::string credential_id_hex =
base::HexEncode(record.credential_id.data(), record.credential_id.size());
if (record.secret.size() != kCredentialSecretSize) {
LOG(ERROR) << "Wrong secret size in record with id " << credential_id_hex;
return false;
}
base::Value record_value(base::Value::Type::DICTIONARY);
record_value.SetStringKey(kCredentialIdKey, credential_id_hex);
record_value.SetStringKey(kSecretKey, base::Base64Encode(record.secret));
record_value.SetStringKey(kKeyBlobKey, base::Base64Encode(record.key_blob));
record_value.SetStringKey(kRpIdKey, record.rp_id);
record_value.SetStringKey(kRpDisplayNameKey, record.rp_display_name);
record_value.SetStringKey(kUserIdKey, base::HexEncode(record.user_id.data(),
record.user_id.size()));
record_value.SetStringKey(kUserDisplayNameKey, record.user_display_name);
record_value.SetDoubleKey(kCreatedTimestampKey, record.timestamp);
record_value.SetBoolKey(kIsResidentKeyKey, record.is_resident_key);
std::string json_string;
JSONStringValueSerializer json_serializer(&json_string);
if (!json_serializer.Serialize(record_value)) {
LOG(ERROR) << "Failed to serialize record with id " << credential_id_hex
<< " to JSON.";
return false;
}
// Use the hash of credential_id for the filename because the hex encode of
// credential_id itself is too long and would cause ENAMETOOLONG.
const std::vector<uint8_t> credential_id_hash =
util::Sha256(record.credential_id);
std::vector<FilePath> paths = {
FilePath(sanitized_user_), FilePath(kWebAuthnDirName),
FilePath(kRecordFileNamePrefix +
base::HexEncode(credential_id_hash.data(),
credential_id_hash.size()))};
FilePath record_storage_filename = root_path_;
for (const auto& path : paths) {
DCHECK(!path.IsAbsolute());
record_storage_filename = record_storage_filename.Append(path);
}
{
brillo::ScopedUmask owner_only_umask(~(0700));
if (!base::CreateDirectory(record_storage_filename.DirName())) {
PLOG(ERROR) << "Cannot create directory: "
<< record_storage_filename.DirName().value() << ".";
return false;
}
}
{
brillo::ScopedUmask owner_only_umask(~(0600));
if (!base::ImportantFileWriter::WriteFileAtomically(record_storage_filename,
json_string)) {
LOG(ERROR) << "Failed to write JSON file: "
<< record_storage_filename.value() << ".";
return false;
}
}
LOG(INFO) << "Done writing record with id " << credential_id_hex
<< " to file successfully. ";
records_.emplace_back(record);
return true;
}
bool WebAuthnStorage::LoadRecords() {
DCHECK(allow_access_ && !sanitized_user_.empty());
FilePath webauthn_path =
root_path_.Append(sanitized_user_).Append(kWebAuthnDirName);
base::FileEnumerator enum_records(webauthn_path, false,
base::FileEnumerator::FILES,
std::string(kRecordFileNamePrefix) + "*");
bool read_all_records_successfully = true;
for (FilePath record_path = enum_records.Next(); !record_path.empty();
record_path = enum_records.Next()) {
std::string json_string;
if (!base::ReadFileToString(record_path, &json_string)) {
LOG(ERROR) << "Failed to read the string from " << record_path.value()
<< ".";
read_all_records_successfully = false;
continue;
}
auto record_value = base::JSONReader::ReadAndReturnValueWithError(
json_string, base::JSON_ALLOW_TRAILING_COMMAS);
if (!record_value.value) {
LOG(ERROR) << "Error in deserializing JSON from path "
<< record_path.value();
LOG_IF(ERROR, !record_value.error_message.empty())
<< "JSON error message: " << record_value.error_message << ".";
read_all_records_successfully = false;
continue;
}
if (!record_value.value->is_dict()) {
LOG(ERROR) << "Value " << record_path.value() << " is not a dictionary.";
read_all_records_successfully = false;
continue;
}
base::Value record_dictionary = std::move(*record_value.value);
const std::string* credential_id_hex =
record_dictionary.FindStringKey(kCredentialIdKey);
std::string credential_id;
if (!credential_id_hex ||
!base::HexStringToString(*credential_id_hex, &credential_id)) {
LOG(ERROR) << "Cannot read credential_id from " << record_path.value()
<< ".";
read_all_records_successfully = false;
continue;
}
const std::string* secret_base64 =
record_dictionary.FindStringKey(kSecretKey);
std::string secret;
if (!secret_base64 || !base::Base64Decode(*secret_base64, &secret)) {
LOG(ERROR) << "Cannot read credential secret from " << record_path.value()
<< ".";
read_all_records_successfully = false;
continue;
}
const std::string* key_blob_base64 =
record_dictionary.FindStringKey(kKeyBlobKey);
std::string key_blob;
// key blob can be empty for backward compatibility. New key blobs generated
// in gsc case are empty strings.
if (key_blob_base64 && !base::Base64Decode(*key_blob_base64, &key_blob)) {
LOG(ERROR) << "Failed to decode credential secret from "
<< record_path.value() << ".";
read_all_records_successfully = false;
continue;
}
const std::string* rp_id = record_dictionary.FindStringKey(kRpIdKey);
if (!rp_id) {
LOG(ERROR) << "Cannot read rp_id from " << record_path.value() << ".";
read_all_records_successfully = false;
continue;
}
const std::string* rp_display_name =
record_dictionary.FindStringKey(kRpDisplayNameKey);
if (!rp_display_name) {
LOG(ERROR) << "Cannot read rp_display_name from " << record_path.value()
<< ".";
read_all_records_successfully = false;
continue;
}
const std::string* user_id_hex =
record_dictionary.FindStringKey(kUserIdKey);
std::string user_id;
if (!user_id_hex) {
LOG(ERROR) << "Cannot read user_id from " << record_path.value() << ".";
read_all_records_successfully = false;
continue;
}
// Empty user_id is allowed:
// https://w3c.github.io/webauthn/#dom-publickeycredentialuserentity-id
if (!user_id_hex->empty() &&
!base::HexStringToString(*user_id_hex, &user_id)) {
LOG(ERROR) << "Cannot parse user_id from " << record_path.value() << ".";
read_all_records_successfully = false;
continue;
}
const std::string* user_display_name =
record_dictionary.FindStringKey(kUserDisplayNameKey);
if (!user_display_name) {
LOG(ERROR) << "Cannot read user_display_name from " << record_path.value()
<< ".";
read_all_records_successfully = false;
continue;
}
const std::optional<double> timestamp =
record_dictionary.FindDoubleKey(kCreatedTimestampKey);
if (!timestamp) {
LOG(ERROR) << "Cannot read timestamp from " << record_path.value() << ".";
read_all_records_successfully = false;
continue;
}
const std::optional<bool> is_resident_key =
record_dictionary.FindBoolKey(kIsResidentKeyKey);
if (!is_resident_key.has_value()) {
LOG(ERROR) << "Cannot read is_resident_key from " << record_path.value()
<< ".";
read_all_records_successfully = false;
continue;
}
records_.emplace_back(WebAuthnRecord{
.credential_id = credential_id,
.secret = brillo::Blob(secret.begin(), secret.end()),
.key_blob = brillo::Blob(key_blob.begin(), key_blob.end()),
.rp_id = *rp_id,
.rp_display_name = *rp_display_name,
.user_id = user_id,
.user_display_name = *user_display_name,
.timestamp = *timestamp,
.is_resident_key = *is_resident_key});
}
LOG(INFO) << "Loaded " << records_.size() << " WebAuthn records to memory.";
return read_all_records_successfully;
}
bool WebAuthnStorage::SendRecordCountToUMA(MetricsLibraryInterface* metrics) {
return metrics->SendToUMA(kWebAuthnRecordCountMetric, records_.size(),
kMinRecordCount, kMaxRecordCount,
kRecordCountBuckets);
}
std::optional<brillo::Blob> WebAuthnStorage::GetSecretByCredentialId(
const std::string& credential_id) {
for (const WebAuthnRecord& record : records_) {
if (record.credential_id == credential_id) {
return record.secret;
}
}
return std::nullopt;
}
bool WebAuthnStorage::GetSecretAndKeyBlobByCredentialId(
const std::string& credential_id,
brillo::Blob* secret,
brillo::Blob* key_blob) {
for (const WebAuthnRecord& record : records_) {
if (record.credential_id == credential_id) {
if (secret) {
*secret = record.secret;
}
if (key_blob) {
*key_blob = record.key_blob;
}
return true;
}
}
return false;
}
std::optional<WebAuthnRecord> WebAuthnStorage::GetRecordByCredentialId(
const std::string& credential_id) {
for (const WebAuthnRecord& record : records_) {
if (record.credential_id == credential_id) {
return record;
}
}
return std::nullopt;
}
int WebAuthnStorage::CountRecordsInTimeRange(int64_t timestamp_min,
int64_t timestamp_max) {
int num_records = 0;
for (const WebAuthnRecord& record : records_) {
if (timestamp_min <= record.timestamp &&
record.timestamp <= timestamp_max) {
num_records++;
}
}
return num_records;
}
int WebAuthnStorage::DeleteRecordsInTimeRange(int64_t timestamp_min,
int64_t timestamp_max) {
size_t original_size = records_.size();
auto remove_begin =
std::remove_if(records_.begin(), records_.end(),
[timestamp_min, timestamp_max](const auto& record) {
return timestamp_min <= record.timestamp &&
record.timestamp <= timestamp_max;
});
for (auto record = remove_begin; record != records_.end(); record++) {
DeleteRecordWithCredentialId(record->credential_id);
}
records_.erase(remove_begin, records_.end());
size_t updated_size = records_.size();
return original_size - updated_size;
}
void WebAuthnStorage::Reset() {
allow_access_ = false;
sanitized_user_.clear();
records_.clear();
}
void WebAuthnStorage::SetRootPathForTesting(const base::FilePath& root_path) {
root_path_ = root_path;
}
bool WebAuthnStorage::DeleteRecordWithCredentialId(
const std::string& credential_id) {
// Use the hash of credential_id for the filename because the hex encode of
// credential_id itself is too long and would cause ENAMETOOLONG.
const std::vector<uint8_t> credential_id_hash = util::Sha256(credential_id);
std::vector<FilePath> paths = {
FilePath(sanitized_user_), FilePath(kWebAuthnDirName),
FilePath(kRecordFileNamePrefix +
base::HexEncode(credential_id_hash.data(),
credential_id_hash.size()))};
FilePath record_storage_filename = root_path_;
for (const auto& path : paths) {
DCHECK(!path.IsAbsolute());
record_storage_filename = record_storage_filename.Append(path);
}
if (!base::DeleteFile(record_storage_filename)) {
LOG(ERROR) << "Failed to delete file: " << record_storage_filename.value()
<< ".";
return false;
}
LOG(INFO) << "Successfully deleted file: " << record_storage_filename.value()
<< ".";
return true;
}
} // namespace u2f