Yicheng Li | 1090c90 | 2020-11-10 11:31:43 -0800 | [diff] [blame] | 1 | // Copyright 2020 The Chromium OS Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | #include "u2fd/webauthn_storage.h" |
| 6 | |
Yicheng Li | 30b6abc | 2020-11-13 14:51:15 -0800 | [diff] [blame] | 7 | #include <memory> |
Yicheng Li | 1090c90 | 2020-11-10 11:31:43 -0800 | [diff] [blame] | 8 | #include <utility> |
| 9 | #include <vector> |
| 10 | |
| 11 | #include <base/base64.h> |
Qijiang Fan | 713061e | 2021-03-08 15:45:12 +0900 | [diff] [blame] | 12 | #include <base/check.h> |
Yicheng Li | 1090c90 | 2020-11-10 11:31:43 -0800 | [diff] [blame] | 13 | #include <base/files/file_enumerator.h> |
| 14 | #include <base/files/file_util.h> |
| 15 | #include <base/files/important_file_writer.h> |
| 16 | #include <base/json/json_reader.h> |
| 17 | #include <base/json/json_string_value_serializer.h> |
| 18 | #include <base/strings/string_number_conversions.h> |
| 19 | #include <base/values.h> |
| 20 | |
| 21 | #include "brillo/scoped_umask.h" |
Yicheng Li | c3ae78d | 2020-11-25 15:32:20 -0800 | [diff] [blame] | 22 | #include "u2fd/util.h" |
Yicheng Li | 1090c90 | 2020-11-10 11:31:43 -0800 | [diff] [blame] | 23 | |
| 24 | namespace u2f { |
| 25 | |
| 26 | using base::FilePath; |
| 27 | |
| 28 | namespace { |
| 29 | |
| 30 | constexpr const char kDaemonStorePath[] = "/run/daemon-store/u2f"; |
| 31 | constexpr const char kWebAuthnDirName[] = "webauthn"; |
| 32 | constexpr const char kRecordFileNamePrefix[] = "Record_"; |
Yicheng Li | 30b6abc | 2020-11-13 14:51:15 -0800 | [diff] [blame] | 33 | constexpr const char kAuthTimeSecretHashFileName[] = "AuthTimeSecretHash"; |
Yicheng Li | 1090c90 | 2020-11-10 11:31:43 -0800 | [diff] [blame] | 34 | |
| 35 | // Members of the JSON file |
| 36 | constexpr const char kCredentialIdKey[] = "credential_id"; |
| 37 | constexpr const char kSecretKey[] = "secret"; |
| 38 | constexpr const char kRpIdKey[] = "rp_id"; |
Yicheng Li | d73908d | 2021-02-17 09:58:11 -0800 | [diff] [blame] | 39 | constexpr const char kRpDisplayNameKey[] = "rp_display_name"; |
Yicheng Li | 1090c90 | 2020-11-10 11:31:43 -0800 | [diff] [blame] | 40 | constexpr const char kUserIdKey[] = "user_id"; |
Yicheng Li | aeb3d68 | 2020-11-19 11:07:57 -0800 | [diff] [blame] | 41 | constexpr const char kUserDisplayNameKey[] = "user_display_name"; |
Yicheng Li | 1090c90 | 2020-11-10 11:31:43 -0800 | [diff] [blame] | 42 | constexpr const char kCreatedTimestampKey[] = "created"; |
Yicheng Li | d73908d | 2021-02-17 09:58:11 -0800 | [diff] [blame] | 43 | constexpr const char kIsResidentKeyKey[] = "is_resident_key"; |
Yicheng Li | 1090c90 | 2020-11-10 11:31:43 -0800 | [diff] [blame] | 44 | |
Yicheng Li | e7a2625 | 2021-02-03 12:18:37 -0800 | [diff] [blame] | 45 | constexpr char kWebAuthnRecordCountMetric[] = |
| 46 | "WebAuthentication.ChromeOS.StartupRecordCount"; |
| 47 | constexpr int kMinRecordCount = 0; |
| 48 | constexpr int kMaxRecordCount = 50; |
| 49 | constexpr int kRecordCountBuckets = 50; |
| 50 | |
Yicheng Li | 1090c90 | 2020-11-10 11:31:43 -0800 | [diff] [blame] | 51 | } // namespace |
| 52 | |
| 53 | WebAuthnStorage::WebAuthnStorage() : root_path_(kDaemonStorePath) {} |
| 54 | WebAuthnStorage::~WebAuthnStorage() = default; |
| 55 | |
| 56 | bool WebAuthnStorage::WriteRecord(const WebAuthnRecord& record) { |
| 57 | DCHECK(allow_access_ && !sanitized_user_.empty()); |
| 58 | |
| 59 | const std::string credential_id_hex = |
| 60 | base::HexEncode(record.credential_id.data(), record.credential_id.size()); |
| 61 | |
| 62 | if (record.secret.size() != kCredentialSecretSize) { |
| 63 | LOG(ERROR) << "Wrong secret size in record with id " << credential_id_hex; |
| 64 | return false; |
| 65 | } |
| 66 | |
| 67 | base::Value record_value(base::Value::Type::DICTIONARY); |
| 68 | record_value.SetStringKey(kCredentialIdKey, credential_id_hex); |
| 69 | record_value.SetStringKey(kSecretKey, base::Base64Encode(record.secret)); |
| 70 | record_value.SetStringKey(kRpIdKey, record.rp_id); |
Yicheng Li | d73908d | 2021-02-17 09:58:11 -0800 | [diff] [blame] | 71 | record_value.SetStringKey(kRpDisplayNameKey, record.rp_display_name); |
Yicheng Li | aeb3d68 | 2020-11-19 11:07:57 -0800 | [diff] [blame] | 72 | record_value.SetStringKey(kUserIdKey, base::HexEncode(record.user_id.data(), |
| 73 | record.user_id.size())); |
| 74 | record_value.SetStringKey(kUserDisplayNameKey, record.user_display_name); |
Yicheng Li | 1090c90 | 2020-11-10 11:31:43 -0800 | [diff] [blame] | 75 | record_value.SetDoubleKey(kCreatedTimestampKey, record.timestamp); |
Yicheng Li | d73908d | 2021-02-17 09:58:11 -0800 | [diff] [blame] | 76 | record_value.SetBoolKey(kIsResidentKeyKey, record.is_resident_key); |
Yicheng Li | 1090c90 | 2020-11-10 11:31:43 -0800 | [diff] [blame] | 77 | |
| 78 | std::string json_string; |
| 79 | JSONStringValueSerializer json_serializer(&json_string); |
| 80 | if (!json_serializer.Serialize(record_value)) { |
| 81 | LOG(ERROR) << "Failed to serialize record with id " << credential_id_hex |
| 82 | << " to JSON."; |
| 83 | return false; |
| 84 | } |
| 85 | |
Yicheng Li | c3ae78d | 2020-11-25 15:32:20 -0800 | [diff] [blame] | 86 | // Use the hash of credential_id for the filename because the hex encode of |
| 87 | // credential_id itself is too long and would cause ENAMETOOLONG. |
| 88 | const std::vector<uint8_t> credential_id_hash = |
| 89 | util::Sha256(record.credential_id); |
Yicheng Li | 1090c90 | 2020-11-10 11:31:43 -0800 | [diff] [blame] | 90 | std::vector<FilePath> paths = { |
| 91 | FilePath(sanitized_user_), FilePath(kWebAuthnDirName), |
Yicheng Li | c3ae78d | 2020-11-25 15:32:20 -0800 | [diff] [blame] | 92 | FilePath(kRecordFileNamePrefix + |
| 93 | base::HexEncode(credential_id_hash.data(), |
| 94 | credential_id_hash.size()))}; |
Yicheng Li | 1090c90 | 2020-11-10 11:31:43 -0800 | [diff] [blame] | 95 | |
| 96 | FilePath record_storage_filename = root_path_; |
| 97 | for (const auto& path : paths) { |
| 98 | DCHECK(!path.IsAbsolute()); |
| 99 | record_storage_filename = record_storage_filename.Append(path); |
| 100 | } |
| 101 | |
| 102 | { |
| 103 | brillo::ScopedUmask owner_only_umask(~(0700)); |
| 104 | |
| 105 | if (!base::CreateDirectory(record_storage_filename.DirName())) { |
| 106 | PLOG(ERROR) << "Cannot create directory: " |
| 107 | << record_storage_filename.DirName().value() << "."; |
| 108 | return false; |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | { |
| 113 | brillo::ScopedUmask owner_only_umask(~(0600)); |
| 114 | |
| 115 | if (!base::ImportantFileWriter::WriteFileAtomically(record_storage_filename, |
| 116 | json_string)) { |
| 117 | LOG(ERROR) << "Failed to write JSON file: " |
| 118 | << record_storage_filename.value() << "."; |
| 119 | return false; |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | LOG(INFO) << "Done writing record with id " << credential_id_hex |
| 124 | << " to file successfully. "; |
| 125 | |
| 126 | records_.emplace_back(record); |
| 127 | return true; |
| 128 | } |
| 129 | |
| 130 | bool WebAuthnStorage::LoadRecords() { |
| 131 | DCHECK(allow_access_ && !sanitized_user_.empty()); |
| 132 | |
| 133 | FilePath webauthn_path = |
| 134 | root_path_.Append(sanitized_user_).Append(kWebAuthnDirName); |
| 135 | base::FileEnumerator enum_records(webauthn_path, false, |
| 136 | base::FileEnumerator::FILES, |
| 137 | std::string(kRecordFileNamePrefix) + "*"); |
| 138 | bool read_all_records_successfully = true; |
| 139 | for (FilePath record_path = enum_records.Next(); !record_path.empty(); |
| 140 | record_path = enum_records.Next()) { |
| 141 | std::string json_string; |
| 142 | if (!base::ReadFileToString(record_path, &json_string)) { |
| 143 | LOG(ERROR) << "Failed to read the string from " << record_path.value() |
| 144 | << "."; |
| 145 | read_all_records_successfully = false; |
| 146 | continue; |
| 147 | } |
| 148 | |
| 149 | auto record_value = base::JSONReader::ReadAndReturnValueWithError( |
| 150 | json_string, base::JSON_ALLOW_TRAILING_COMMAS); |
| 151 | |
| 152 | if (!record_value.value) { |
hscham | 9737f25 | 2020-12-11 16:28:57 +0900 | [diff] [blame] | 153 | LOG(ERROR) << "Error in deserializing JSON from path " |
| 154 | << record_path.value(); |
Yicheng Li | 1090c90 | 2020-11-10 11:31:43 -0800 | [diff] [blame] | 155 | LOG_IF(ERROR, !record_value.error_message.empty()) |
| 156 | << "JSON error message: " << record_value.error_message << "."; |
| 157 | read_all_records_successfully = false; |
| 158 | continue; |
| 159 | } |
| 160 | |
| 161 | if (!record_value.value->is_dict()) { |
| 162 | LOG(ERROR) << "Value " << record_path.value() << " is not a dictionary."; |
| 163 | read_all_records_successfully = false; |
| 164 | continue; |
| 165 | } |
| 166 | base::Value record_dictionary = std::move(*record_value.value); |
| 167 | |
| 168 | const std::string* credential_id_hex = |
| 169 | record_dictionary.FindStringKey(kCredentialIdKey); |
| 170 | std::string credential_id; |
| 171 | if (!credential_id_hex || |
| 172 | !base::HexStringToString(*credential_id_hex, &credential_id)) { |
| 173 | LOG(ERROR) << "Cannot read credential_id from " << record_path.value() |
| 174 | << "."; |
| 175 | read_all_records_successfully = false; |
| 176 | continue; |
| 177 | } |
| 178 | |
| 179 | const std::string* secret_base64 = |
| 180 | record_dictionary.FindStringKey(kSecretKey); |
| 181 | std::string secret; |
| 182 | if (!secret_base64 || !base::Base64Decode(*secret_base64, &secret)) { |
| 183 | LOG(ERROR) << "Cannot read credential secret from " << record_path.value() |
| 184 | << "."; |
| 185 | read_all_records_successfully = false; |
| 186 | continue; |
| 187 | } |
Yicheng Li | 1090c90 | 2020-11-10 11:31:43 -0800 | [diff] [blame] | 188 | |
| 189 | const std::string* rp_id = record_dictionary.FindStringKey(kRpIdKey); |
| 190 | if (!rp_id) { |
| 191 | LOG(ERROR) << "Cannot read rp_id from " << record_path.value() << "."; |
| 192 | read_all_records_successfully = false; |
| 193 | continue; |
| 194 | } |
| 195 | |
Yicheng Li | d73908d | 2021-02-17 09:58:11 -0800 | [diff] [blame] | 196 | const std::string* rp_display_name = |
| 197 | record_dictionary.FindStringKey(kRpDisplayNameKey); |
| 198 | if (!rp_display_name) { |
| 199 | LOG(ERROR) << "Cannot read rp_display_name from " << record_path.value() |
| 200 | << "."; |
| 201 | read_all_records_successfully = false; |
| 202 | continue; |
| 203 | } |
| 204 | |
Yicheng Li | aeb3d68 | 2020-11-19 11:07:57 -0800 | [diff] [blame] | 205 | const std::string* user_id_hex = |
| 206 | record_dictionary.FindStringKey(kUserIdKey); |
| 207 | std::string user_id; |
| 208 | if (!user_id_hex) { |
Yicheng Li | 1090c90 | 2020-11-10 11:31:43 -0800 | [diff] [blame] | 209 | LOG(ERROR) << "Cannot read user_id from " << record_path.value() << "."; |
| 210 | read_all_records_successfully = false; |
| 211 | continue; |
| 212 | } |
Yicheng Li | aeb3d68 | 2020-11-19 11:07:57 -0800 | [diff] [blame] | 213 | // Empty user_id is allowed: |
| 214 | // https://w3c.github.io/webauthn/#dom-publickeycredentialuserentity-id |
| 215 | if (!user_id_hex->empty() && |
| 216 | !base::HexStringToString(*user_id_hex, &user_id)) { |
| 217 | LOG(ERROR) << "Cannot parse user_id from " << record_path.value() << "."; |
| 218 | read_all_records_successfully = false; |
| 219 | continue; |
| 220 | } |
| 221 | |
| 222 | const std::string* user_display_name = |
| 223 | record_dictionary.FindStringKey(kUserDisplayNameKey); |
| 224 | if (!user_display_name) { |
| 225 | LOG(ERROR) << "Cannot read user_display_name from " << record_path.value() |
| 226 | << "."; |
| 227 | read_all_records_successfully = false; |
| 228 | continue; |
| 229 | } |
Yicheng Li | 1090c90 | 2020-11-10 11:31:43 -0800 | [diff] [blame] | 230 | |
| 231 | const base::Optional<double> timestamp = |
| 232 | record_dictionary.FindDoubleKey(kCreatedTimestampKey); |
| 233 | if (!timestamp) { |
| 234 | LOG(ERROR) << "Cannot read timestamp from " << record_path.value() << "."; |
| 235 | read_all_records_successfully = false; |
| 236 | continue; |
| 237 | } |
| 238 | |
Yicheng Li | d73908d | 2021-02-17 09:58:11 -0800 | [diff] [blame] | 239 | const base::Optional<bool> is_resident_key = |
| 240 | record_dictionary.FindBoolKey(kIsResidentKeyKey); |
| 241 | if (!is_resident_key.has_value()) { |
| 242 | LOG(ERROR) << "Cannot read is_resident_key from " << record_path.value() |
| 243 | << "."; |
| 244 | read_all_records_successfully = false; |
| 245 | continue; |
| 246 | } |
| 247 | |
Yicheng Li | 4d27fa7 | 2020-12-10 11:09:21 -0800 | [diff] [blame] | 248 | records_.emplace_back(WebAuthnRecord{ |
| 249 | credential_id, brillo::Blob(secret.begin(), secret.end()), *rp_id, |
Yicheng Li | d73908d | 2021-02-17 09:58:11 -0800 | [diff] [blame] | 250 | *rp_display_name, user_id, *user_display_name, *timestamp, |
| 251 | *is_resident_key}); |
Yicheng Li | 1090c90 | 2020-11-10 11:31:43 -0800 | [diff] [blame] | 252 | } |
| 253 | LOG(INFO) << "Loaded " << records_.size() << " WebAuthn records to memory."; |
| 254 | return read_all_records_successfully; |
| 255 | } |
| 256 | |
Yicheng Li | e7a2625 | 2021-02-03 12:18:37 -0800 | [diff] [blame] | 257 | bool WebAuthnStorage::SendRecordCountToUMA(MetricsLibraryInterface* metrics) { |
| 258 | return metrics->SendToUMA(kWebAuthnRecordCountMetric, records_.size(), |
| 259 | kMinRecordCount, kMaxRecordCount, |
| 260 | kRecordCountBuckets); |
| 261 | } |
| 262 | |
Yicheng Li | 4d27fa7 | 2020-12-10 11:09:21 -0800 | [diff] [blame] | 263 | base::Optional<brillo::Blob> WebAuthnStorage::GetSecretByCredentialId( |
Yicheng Li | 1090c90 | 2020-11-10 11:31:43 -0800 | [diff] [blame] | 264 | const std::string& credential_id) { |
| 265 | for (const WebAuthnRecord& record : records_) { |
| 266 | if (record.credential_id == credential_id) { |
| 267 | return record.secret; |
| 268 | } |
| 269 | } |
| 270 | return base::nullopt; |
| 271 | } |
| 272 | |
| 273 | base::Optional<WebAuthnRecord> WebAuthnStorage::GetRecordByCredentialId( |
| 274 | const std::string& credential_id) { |
| 275 | for (const WebAuthnRecord& record : records_) { |
| 276 | if (record.credential_id == credential_id) { |
| 277 | return record; |
| 278 | } |
| 279 | } |
| 280 | return base::nullopt; |
| 281 | } |
| 282 | |
Yicheng Li | 30b6abc | 2020-11-13 14:51:15 -0800 | [diff] [blame] | 283 | bool WebAuthnStorage::PersistAuthTimeSecretHash(const brillo::Blob& hash) { |
| 284 | DCHECK(allow_access_ && !sanitized_user_.empty()); |
| 285 | |
| 286 | FilePath path = FilePath(kDaemonStorePath) |
| 287 | .Append(sanitized_user_) |
| 288 | .Append(kWebAuthnDirName) |
| 289 | .Append(kAuthTimeSecretHashFileName); |
| 290 | |
| 291 | { |
| 292 | brillo::ScopedUmask owner_only_umask(~(0700)); |
| 293 | if (!base::CreateDirectory(path.DirName())) { |
| 294 | LOG(ERROR) << "Cannot create directory: " << path.DirName().value() |
| 295 | << "."; |
| 296 | return false; |
| 297 | } |
| 298 | } |
| 299 | |
| 300 | { |
| 301 | brillo::ScopedUmask owner_only_umask(~(0600)); |
| 302 | if (!base::ImportantFileWriter::WriteFileAtomically( |
| 303 | path, base::Base64Encode(hash))) { |
| 304 | LOG(ERROR) << "Failed to persist auth time secret hash to disk."; |
| 305 | return false; |
| 306 | } |
| 307 | } |
| 308 | |
| 309 | return true; |
| 310 | } |
| 311 | |
| 312 | std::unique_ptr<brillo::Blob> WebAuthnStorage::LoadAuthTimeSecretHash() { |
| 313 | DCHECK(allow_access_ && !sanitized_user_.empty()); |
| 314 | |
| 315 | FilePath path = FilePath(kDaemonStorePath) |
| 316 | .Append(sanitized_user_) |
| 317 | .Append(kWebAuthnDirName) |
| 318 | .Append(kAuthTimeSecretHashFileName); |
| 319 | std::string hash_str_base64; |
| 320 | std::string hash_str; |
| 321 | if (!base::ReadFileToString(path, &hash_str_base64) || |
| 322 | !base::Base64Decode(hash_str_base64, &hash_str)) { |
| 323 | LOG(ERROR) << "Failed to read auth time secret hash from disk."; |
| 324 | return nullptr; |
| 325 | } |
| 326 | |
| 327 | return std::make_unique<brillo::Blob>(hash_str.begin(), hash_str.end()); |
| 328 | } |
| 329 | |
Yicheng Li | 1090c90 | 2020-11-10 11:31:43 -0800 | [diff] [blame] | 330 | void WebAuthnStorage::Reset() { |
| 331 | allow_access_ = false; |
| 332 | sanitized_user_.clear(); |
| 333 | records_.clear(); |
| 334 | } |
| 335 | |
| 336 | void WebAuthnStorage::SetRootPathForTesting(const base::FilePath& root_path) { |
| 337 | root_path_ = root_path; |
| 338 | } |
| 339 | |
| 340 | } // namespace u2f |