blob: 318b8f4c6502a7cc0c75e5c31c06fb3212211cde [file] [log] [blame]
Yicheng Li1090c902020-11-10 11:31:43 -08001// 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 Li30b6abc2020-11-13 14:51:15 -08007#include <memory>
Yicheng Li1090c902020-11-10 11:31:43 -08008#include <utility>
9#include <vector>
10
11#include <base/base64.h>
Qijiang Fan713061e2021-03-08 15:45:12 +090012#include <base/check.h>
Yicheng Li1090c902020-11-10 11:31:43 -080013#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 Lic3ae78d2020-11-25 15:32:20 -080022#include "u2fd/util.h"
Yicheng Li1090c902020-11-10 11:31:43 -080023
24namespace u2f {
25
26using base::FilePath;
27
28namespace {
29
30constexpr const char kDaemonStorePath[] = "/run/daemon-store/u2f";
31constexpr const char kWebAuthnDirName[] = "webauthn";
32constexpr const char kRecordFileNamePrefix[] = "Record_";
Yicheng Li30b6abc2020-11-13 14:51:15 -080033constexpr const char kAuthTimeSecretHashFileName[] = "AuthTimeSecretHash";
Yicheng Li1090c902020-11-10 11:31:43 -080034
35// Members of the JSON file
36constexpr const char kCredentialIdKey[] = "credential_id";
37constexpr const char kSecretKey[] = "secret";
38constexpr const char kRpIdKey[] = "rp_id";
Yicheng Lid73908d2021-02-17 09:58:11 -080039constexpr const char kRpDisplayNameKey[] = "rp_display_name";
Yicheng Li1090c902020-11-10 11:31:43 -080040constexpr const char kUserIdKey[] = "user_id";
Yicheng Liaeb3d682020-11-19 11:07:57 -080041constexpr const char kUserDisplayNameKey[] = "user_display_name";
Yicheng Li1090c902020-11-10 11:31:43 -080042constexpr const char kCreatedTimestampKey[] = "created";
Yicheng Lid73908d2021-02-17 09:58:11 -080043constexpr const char kIsResidentKeyKey[] = "is_resident_key";
Yicheng Li1090c902020-11-10 11:31:43 -080044
Yicheng Lie7a26252021-02-03 12:18:37 -080045constexpr char kWebAuthnRecordCountMetric[] =
46 "WebAuthentication.ChromeOS.StartupRecordCount";
47constexpr int kMinRecordCount = 0;
48constexpr int kMaxRecordCount = 50;
49constexpr int kRecordCountBuckets = 50;
50
Yicheng Li1090c902020-11-10 11:31:43 -080051} // namespace
52
53WebAuthnStorage::WebAuthnStorage() : root_path_(kDaemonStorePath) {}
54WebAuthnStorage::~WebAuthnStorage() = default;
55
56bool 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 Lid73908d2021-02-17 09:58:11 -080071 record_value.SetStringKey(kRpDisplayNameKey, record.rp_display_name);
Yicheng Liaeb3d682020-11-19 11:07:57 -080072 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 Li1090c902020-11-10 11:31:43 -080075 record_value.SetDoubleKey(kCreatedTimestampKey, record.timestamp);
Yicheng Lid73908d2021-02-17 09:58:11 -080076 record_value.SetBoolKey(kIsResidentKeyKey, record.is_resident_key);
Yicheng Li1090c902020-11-10 11:31:43 -080077
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 Lic3ae78d2020-11-25 15:32:20 -080086 // 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 Li1090c902020-11-10 11:31:43 -080090 std::vector<FilePath> paths = {
91 FilePath(sanitized_user_), FilePath(kWebAuthnDirName),
Yicheng Lic3ae78d2020-11-25 15:32:20 -080092 FilePath(kRecordFileNamePrefix +
93 base::HexEncode(credential_id_hash.data(),
94 credential_id_hash.size()))};
Yicheng Li1090c902020-11-10 11:31:43 -080095
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
130bool 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) {
hscham9737f252020-12-11 16:28:57 +0900153 LOG(ERROR) << "Error in deserializing JSON from path "
154 << record_path.value();
Yicheng Li1090c902020-11-10 11:31:43 -0800155 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 Li1090c902020-11-10 11:31:43 -0800188
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 Lid73908d2021-02-17 09:58:11 -0800196 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 Liaeb3d682020-11-19 11:07:57 -0800205 const std::string* user_id_hex =
206 record_dictionary.FindStringKey(kUserIdKey);
207 std::string user_id;
208 if (!user_id_hex) {
Yicheng Li1090c902020-11-10 11:31:43 -0800209 LOG(ERROR) << "Cannot read user_id from " << record_path.value() << ".";
210 read_all_records_successfully = false;
211 continue;
212 }
Yicheng Liaeb3d682020-11-19 11:07:57 -0800213 // 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 Li1090c902020-11-10 11:31:43 -0800230
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 Lid73908d2021-02-17 09:58:11 -0800239 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 Li4d27fa72020-12-10 11:09:21 -0800248 records_.emplace_back(WebAuthnRecord{
249 credential_id, brillo::Blob(secret.begin(), secret.end()), *rp_id,
Yicheng Lid73908d2021-02-17 09:58:11 -0800250 *rp_display_name, user_id, *user_display_name, *timestamp,
251 *is_resident_key});
Yicheng Li1090c902020-11-10 11:31:43 -0800252 }
253 LOG(INFO) << "Loaded " << records_.size() << " WebAuthn records to memory.";
254 return read_all_records_successfully;
255}
256
Yicheng Lie7a26252021-02-03 12:18:37 -0800257bool WebAuthnStorage::SendRecordCountToUMA(MetricsLibraryInterface* metrics) {
258 return metrics->SendToUMA(kWebAuthnRecordCountMetric, records_.size(),
259 kMinRecordCount, kMaxRecordCount,
260 kRecordCountBuckets);
261}
262
Yicheng Li4d27fa72020-12-10 11:09:21 -0800263base::Optional<brillo::Blob> WebAuthnStorage::GetSecretByCredentialId(
Yicheng Li1090c902020-11-10 11:31:43 -0800264 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
273base::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 Li30b6abc2020-11-13 14:51:15 -0800283bool 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
312std::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 Li1090c902020-11-10 11:31:43 -0800330void WebAuthnStorage::Reset() {
331 allow_access_ = false;
332 sanitized_user_.clear();
333 records_.clear();
334}
335
336void WebAuthnStorage::SetRootPathForTesting(const base::FilePath& root_path) {
337 root_path_ = root_path;
338}
339
340} // namespace u2f