CVE-2025-12910
Overview
Background
- Enclave authenticator
- the Google Password Manager (GPM) passkey backend in
device/fido/enclave/, where Chrome sends CBOR-encoded commands to a cloud enclave over an encrypted channel (transact.cc). - Security domain secret
- the end-to-end encryption secret for the user’s synced passkeys, placed in enclave requests under the
"secret"key (kRequestSecretKey), as the new unit test built withBuildGetAssertionCommand(..., secret())shows. - PRF / largeBlob outputs
- WebAuthn extension results the enclave returns nested under
"ok"→"ok"in an assertion response. PRF outputs are derived key material and largeBlob holds per-credential data the relying party stored. - `FIDO_LOG(EVENT)` / device event log
- Chrome’s in-memory
device_event_log(type"fido", shown onchrome://device-log), which the new browser tests read back withdevice_event_log::GetAsString.
Root Cause Analysis
Before the patch, Transaction in device/fido/enclave/transact.cc logged every enclave command and reply in full with FIDO_LOG(EVENT) << "<- " << cbor::DiagnosticWriter::Write(request_) and FIDO_LOG(EVENT) << "-> " << cbor::DiagnosticWriter::Write(*response). These lines ran right before the plaintext request was encrypted (BuildCommandRequestBody) and right after the response was decrypted, so whatever the end-to-end encrypted channel protected was written in cleartext to the FIDO device event log. That included the security domain "secret", "prf" outputs and "largeBlob" contents. The broken rule is that secret material protected by the enclave’s encryption must never appear in a diagnostic sink with weaker access controls.
The fix adds RedactEnclaveRequest and RedactEnclaveResponse. Each clones the CBOR value with RedactCbor and walks fixed key paths with RedactPath: {"secret"} for requests, and {"ok","ok","largeBlob"} and {"ok","ok","prf"} for responses. The walk applies to every element when it meets an array, and the matched leaf becomes the string "[redacted]" whatever its original type. Only the redacted clone is passed to DiagnosticWriter::Write, so the original request_/*response that the protocol logic uses is unchanged.
"[redacted]".Attack Path
- Passkey operation with the enclave
a user creates or uses a GPM passkey, so
Transactionsends a request containing"secret"and receives a response that may contain"prf"or"largeBlob". - Plaintext logging
before the patch,
FIDO_LOG(EVENT)wrote the full diagnostic CBOR of both messages into the"fido"device event log. - Log exposure
anyone who can read that log can recover these values, either from
chrome://device-logon the device or from a copy of the log that leaves the device, such as a diagnostic or feedback report (inferred; the patch shows only the log write, not where the log goes). - Use of recovered secrets with the security domain secret, an attacker could decrypt the user’s synced passkey material. With PRF outputs, they could obtain keys a relying party derives from them (inferred; the patch only shows that the values were logged).
Impact Assessment
(inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
fordevice/fido/enclave/enclave_protocol_utils.cc |
modified | |
IN_PROC_BROWSER_TEST_Fchrome/browser/webauthn/enclave_authenticator_browsertest.cc |
modified | |
TEST_Fdevice/fido/enclave/enclave_protocol_utils_unittest.cc |
modified |
Files Changed
chrome/browser/webauthn/enclave_authenticator_browsertest.ccchrome/test/BUILD.gndevice/fido/enclave/enclave_protocol_utils.ccdevice/fido/enclave/enclave_protocol_utils.hdevice/fido/enclave/enclave_protocol_utils_unittest.ccdevice/fido/enclave/transact.cc
Audit Directions
- Logging around encryption boundarieslook for
FIDO_LOG,VLOGandDiagnosticWriter::Writecalls placed just before encryption or just after decryption, in the enclave and other FIDO, sync or cryptographic channels. Pay particular attention to messages that carry PIN,wrapped_secret, hmac-secret or credential blob fields. - Denylist redaction completeness
RedactPathredacts only exact key paths and stops quietly when the structure differs. Check that other message shapes do not carry the same secret material under different nesting: error responses, make-credential replies, and requests that wrap a"secret". - Device event log as a sinkaudit what other
device_event_logcategories (Bluetooth, USB, network) record. That log is user-viewable and may be included in diagnostics, so any key, token or PII written there is a similar leak.
Patch
diff --git a/device/fido/enclave/enclave_protocol_utils.cc b/device/fido/enclave/enclave_protocol_utils.cc
index 51883e1..f607cf3 100644
--- a/device/fido/enclave/enclave_protocol_utils.cc
+++ b/device/fido/enclave/enclave_protocol_utils.cc
@@ -232,6 +232,91 @@
return ret;
}
+// Redacts `path` from `cbor` using the semantics described below for
+// `RedactCbor`. Mutates `cbor` in place.
+void RedactPath(cbor::Value* cbor, base::span<const char*> path) {
+ if (cbor->is_array()) {
+ // Mutate all the elements in the array.
+ cbor::Value::ArrayValue& array =
+ const_cast<cbor::Value::ArrayValue&>(cbor->GetArray());
+ for (cbor::Value& value : array) {
+ RedactPath(&value, path);
+ }
+ return;
+ }
+ if (!cbor->is_map()) {
+ // Only maps and arrays are supported.
+ return;
+ }
+ cbor::Value::MapValue& map =
+ const_cast<cbor::Value::MapValue&>(cbor->GetMap());
+ const char* field = path.take_first_elem();
+ const auto it = map.find(cbor::Value(field));
+ if (it == map.end()) {
+ // Could not find some part of the path, bail out.
+ return;
+ }
+ if (path.empty()) {
+ // Found the leaf, replace the map value regardless of its type.
+ it->second = cbor::Value("[redacted]");
+ return;
+ }
+ RedactPath(&it->second, path);
+}
+
+// Redacts `paths_to_redact` from `cbor` by finding the corresponding keys and
+// replacing them by the cbor string "redacted". Nested paths should correspond
+// to nested maps under the same key name. The redaction is applied to all array
+// elements for a matching key.
+// If a path is not found, a clone of `cbor` is returned.
+//
+// Example:
+//
+// Given a `cbor` value...
+// {
+// characters: [
+// {
+// name: "Reimu",
+// occupation: ["Shrine maiden"]
+// },
+// {
+// name: "Marisa",
+// occupation: ["Witch", "Troublemaker"]
+// }
+// ]
+// }
+//
+// ...and a `paths_to_redact` value...
+//
+// [
+// ["characters", "occupation"],
+// ["characters", "date-of-birth"],
+// ]
+//
+// ...the returned cbor will be:
+//
+// {
+// characters: [
+// {
+// name: "Reimu",
+// occupation: "redacted"
+// },
+// {
+// name: "Marisa",
+// occupation: "redacted"
+// }
+// ]
+// }
+cbor::Value RedactCbor(
+ const cbor::Value& cbor,
+ base::span<const std::vector<const char*>> paths_to_redact) {
+ cbor::Value response = cbor.Clone();
+ for (std::vector<const char*> field_to_redact : paths_to_redact) {
+ RedactPath(&response, field_to_redact);
+ }
+ return response;
+}
+
} // namespace
ErrorResponse::ErrorResponse(std::string error)
@@ -667,4 +752,17 @@
std::move(complete_callback)));
}
+cbor::Value RedactEnclaveRequest(const cbor::Value& cbor) {
+ const std::array redacted_fields = {std::vector{"secret"}};
+ return RedactCbor(cbor, redacted_fields);
+}
+
+cbor::Value RedactEnclaveResponse(const cbor::Value& cbor) {
+ const std::array redacted_fields = {
+ std::vector{"ok", "ok", "largeBlob"},
+ std::vector{"ok", "ok", "prf"},
+ };
+ return RedactCbor(cbor, redacted_fields);
+}
+
} // namespace device::enclave
diff --git a/device/fido/enclave/enclave_protocol_utils.h b/device/fido/enclave/enclave_protocol_utils.h
index 41772175..e435f04 100644
--- a/device/fido/enclave/enclave_protocol_utils.h
+++ b/device/fido/enclave/enclave_protocol_utils.h
@@ -125,6 +125,16 @@
base::OnceCallback<void(std::optional<std::vector<uint8_t>>)>
complete_callback);
+// Returns a copy of the enclave `request` with its sensitive information (such
+// as end-to-end encryption secrets) removed.
+cbor::Value COMPONENT_EXPORT(DEVICE_FIDO)
+ RedactEnclaveRequest(const cbor::Value& request);
+
+// Returns a copy of the enclave `response` with its sensitive information (such
+// as prf outputs) removed.
+cbor::Value COMPONENT_EXPORT(DEVICE_FIDO)
+ RedactEnclaveResponse(const cbor::Value& response);
+
} // namespace enclave
} // namespace device
diff --git a/device/fido/enclave/transact.cc b/device/fido/enclave/transact.cc
index 874a04f..f64a4440 100644
--- a/device/fido/enclave/transact.cc
+++ b/device/fido/enclave/transact.cc
@@ -89,7 +89,9 @@
return;
}
- FIDO_LOG(EVENT) << "<- " << cbor::DiagnosticWriter::Write(request_);
+ FIDO_LOG(EVENT) << "<- "
+ << cbor::DiagnosticWriter::Write(
+ RedactEnclaveRequest(request_));
BuildCommandRequestBody(
std::move(request_), std::move(signing_callback_), *handshake_hash_,
base::BindOnce(&Transaction::RequestReady, scoped_refptr(this)));
@@ -111,7 +113,9 @@
break;
}
- FIDO_LOG(EVENT) << "-> " << cbor::DiagnosticWriter::Write(*response);
+ FIDO_LOG(EVENT) << "-> "
+ << cbor::DiagnosticWriter::Write(
+ RedactEnclaveResponse(*response));
if (!response->is_map()) {
RecordTransactionResult(EnclaveTransactionResult::kParseFailure);
std::move(callback_).Run(base::unexpected(TransactError::kOther));
diff --git a/chrome/browser/webauthn/enclave_authenticator_browsertest.cc b/chrome/browser/webauthn/enclave_authenticator_browsertest.cc
index bf2e825..b6cb868 100644
--- a/chrome/browser/webauthn/enclave_authenticator_browsertest.cc
+++ b/chrome/browser/webauthn/enclave_authenticator_browsertest.cc
@@ -60,6 +60,7 @@
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
+#include "components/device_event_log/device_event_log.h"
#include "components/password_manager/core/browser/password_form.h"
#include "components/password_manager/core/browser/password_store/password_store_interface.h"
#include "components/password_manager/core/common/password_manager_pref_names.h"
@@ -604,6 +605,13 @@
}
}
+std::string GetDeviceLog() {
+ return device_event_log::GetAsString(
+ device_event_log::NEWEST_FIRST, /*format=*/"level",
+ /*types=*/"fido",
+ /*max_level=*/device_event_log::LOG_LEVEL_EVENT, /*max_events=*/0);
+}
+
bool IsMechanismEnclaveCredential(
const AuthenticatorRequestDialogModel::Mechanism& mechanism) {
if (std::holds_alternative<
@@ -1013,6 +1021,9 @@
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
+
+ // Ensure the security domain secret is redacted from logs.
+ EXPECT_THAT(GetDeviceLog(), testing::HasSubstr("\"secret\": \"[redacted]\""));
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest, NonWebauthnRequest) {
@@ -1115,6 +1126,9 @@
EXPECT_TRUE(enabled);
EXPECT_EQ(first, "none");
EXPECT_EQ(second, "none");
+
+ // Ensure the PRF is redacted from logs.
+ EXPECT_THAT(GetDeviceLog(), testing::HasSubstr("\"prf\": \"[redacted]\""));
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest, GetAssertionWithPrf) {
@@ -4316,6 +4330,10 @@
histogram_tester.ExpectBucketCount(
"WebAuthentication.GPM.GetAssertion.LargeBlobSucceeded.Read",
/*sample=*/true, /*expected_count=*/1);
+
+ // Ensure the large blob is redacted from logs.
+ EXPECT_THAT(GetDeviceLog(),
+ testing::HasSubstr("\"largeBlob\": \"[redacted]\""));
}
// Disable large blob for GPM feature flag.
diff --git a/device/fido/enclave/enclave_protocol_utils_unittest.cc b/device/fido/enclave/enclave_protocol_utils_unittest.cc
index cb7e04c..bf22f49 100644
--- a/device/fido/enclave/enclave_protocol_utils_unittest.cc
+++ b/device/fido/enclave/enclave_protocol_utils_unittest.cc
@@ -19,9 +19,11 @@
#include "base/values.h"
#include "components/cbor/reader.h"
#include "components/cbor/values.h"
+#include "components/cbor/writer.h"
#include "components/device_event_log/device_event_log.h"
#include "components/sync/protocol/webauthn_credential_specifics.pb.h"
#include "device/fido/ctap_make_credential_request.h"
+#include "device/fido/enclave/constants.h"
#include "device/fido/fido_parsing_utils.h"
#include "device/fido/fido_transport_protocol.h"
#include "device/fido/json_request.h"
@@ -94,6 +96,18 @@
constexpr char kMakeCredentialHexResponse[] =
"81A1626F6BA3677075625F6B657944050607086776657273696F6E0169656E637279707465"
"644401020304";
+
+// An example response with the top-level "ok" key, dummy large blob and PRF
+// values.
+constexpr char kCompleteGetAssertionHexResponse[] =
+ "A1626F6B81A1626F6BA3637072661904D268726573706F6E7365A3697369676E6174757265"
+ "A2646461746184185318691867186E6474797065664275666665726A7573657248616E646C"
+ "65A2646461746182186118626474797065664275666665727161757468656E74696361746F"
+ "7244617461A2646461746198251118941822188D18A818FD18BD18EE18FD1826181B18D718"
+ "B61859185C18FD187018A50D187018C61840187B18CF01183D18E9186D184E18FB1718DE01"
+ "000000183B647479706566427566666572696C61726765426C6F62A264726561641904D264"
+ "73697A6501";
+
constexpr int32_t kWrappedSecretVersion = 952;
struct BadResponseTestCase {
@@ -247,8 +261,11 @@
std::vector<uint8_t> wrapped_secret() { return wrapped_secret_; }
+ std::vector<uint8_t> secret() { return secret_; }
+
private:
const std::vector<uint8_t> wrapped_secret_ = {1, 2, 3, 4, 5};
+ const std::vector<uint8_t> secret_ = {6, 7, 8, 9, 0};
std::vector<uint8_t> device_id_;
std::vector<uint8_t> user_id_;
std::vector<uint8_t> encrypted_passkey_;
@@ -647,5 +664,54 @@
testing::ElementsAre(1, 2, 3));
}
+TEST_F(EnclaveProtocolUtilsTest, RedactEnclaveRequest) {
+ auto entity = PasskeyEntity();
+ entity.set_rp_id(kRpId);
+ std::optional<base::Value> parsed_json =
+ base::JSONReader::Read(kGetAssertionRequestJson);
+ EXPECT_TRUE(parsed_json);
+ auto json_request =
+ base::MakeRefCounted<JSONRequest>(std::move(*parsed_json));
+ cbor::Value request_cbor = BuildGetAssertionCommand(
+ std::move(entity), json_request, kClientDataJson,
+ /*claimed_pin=*/nullptr, /*wrapped_secret=*/std::nullopt, secret());
+ cbor::Value redacted = RedactEnclaveRequest(request_cbor);
+ ASSERT_TRUE(redacted.is_map());
+ const auto& redacted_map = redacted.GetMap();
+ const auto& redacted_value =
+ redacted_map.find(cbor::Value(kRequestSecretKey))->second;
+ EXPECT_EQ(redacted_value.GetString(), "[redacted]");
+}
+
+TEST_F(EnclaveProtocolUtilsTest, RedactErroneousEnclaveRequest) {
+ cbor::Value request = cbor::Value("not a valid request");
+ EXPECT_EQ(cbor::Writer::Write(RedactEnclaveRequest(request)),
+ cbor::Writer::Write(request));
+}
+
+TEST_F(EnclaveProtocolUtilsTest, RedactEnclaveResponse) {
+ std::vector<uint8_t> response_serialized;
+ CHECK(base::HexStringToBytes(kCompleteGetAssertionHexResponse,
+ &response_serialized));
+ cbor::Value response_cbor = cbor::Reader::Read(response_serialized).value();
+
+ const cbor::Value redacted = RedactEnclaveResponse(response_cbor);
+ const cbor::Value::MapValue& redacted_outer_map =
+ redacted.GetMap().find(cbor::Value("ok"))->second.GetArray()[0].GetMap();
+ const cbor::Value::MapValue& redacted_map =
+ redacted_outer_map.find(cbor::Value("ok"))->second.GetMap();
+ const auto& large_blob_value =
+ redacted_map.find(cbor::Value("largeBlob"))->second;
+ EXPECT_EQ(large_blob_value.GetString(), "[redacted]");
+ const auto& prf_value = redacted_map.find(cbor::Value("prf"))->second;
+ EXPECT_EQ(prf_value.GetString(), "[redacted]");
+}
+
+TEST_F(EnclaveProtocolUtilsTest, RedactErroneousEnclaveResponse) {
+ cbor::Value response = cbor::Value("not a valid response");
+ EXPECT_EQ(cbor::Writer::Write(RedactEnclaveResponse(response)),
+ cbor::Writer::Write(response));
+}
+
} // namespace enclave
} // namespace device
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index aba6cb4d..ded37f7 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -2570,6 +2570,7 @@
"//components/cookie_config",
"//components/country_codes",
"//components/data_sharing/public",
+ "//components/device_event_log",
"//components/dom_distiller/content/browser",
"//components/dom_distiller/content/browser:test_support",
"//components/dom_distiller/content/renderer",
Regression Test / PoC
diff --git a/chrome/browser/webauthn/enclave_authenticator_browsertest.cc b/chrome/browser/webauthn/enclave_authenticator_browsertest.cc
index bf2e825..b6cb868 100644
--- a/chrome/browser/webauthn/enclave_authenticator_browsertest.cc
+++ b/chrome/browser/webauthn/enclave_authenticator_browsertest.cc
@@ -60,6 +60,7 @@
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
+#include "components/device_event_log/device_event_log.h"
#include "components/password_manager/core/browser/password_form.h"
#include "components/password_manager/core/browser/password_store/password_store_interface.h"
#include "components/password_manager/core/common/password_manager_pref_names.h"
@@ -604,6 +605,13 @@
}
}
+std::string GetDeviceLog() {
+ return device_event_log::GetAsString(
+ device_event_log::NEWEST_FIRST, /*format=*/"level",
+ /*types=*/"fido",
+ /*max_level=*/device_event_log::LOG_LEVEL_EVENT, /*max_events=*/0);
+}
+
bool IsMechanismEnclaveCredential(
const AuthenticatorRequestDialogModel::Mechanism& mechanism) {
if (std::holds_alternative<
@@ -1013,6 +1021,9 @@
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
+
+ // Ensure the security domain secret is redacted from logs.
+ EXPECT_THAT(GetDeviceLog(), testing::HasSubstr("\"secret\": \"[redacted]\""));
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest, NonWebauthnRequest) {
@@ -1115,6 +1126,9 @@
EXPECT_TRUE(enabled);
EXPECT_EQ(first, "none");
EXPECT_EQ(second, "none");
+
+ // Ensure the PRF is redacted from logs.
+ EXPECT_THAT(GetDeviceLog(), testing::HasSubstr("\"prf\": \"[redacted]\""));
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest, GetAssertionWithPrf) {
@@ -4316,6 +4330,10 @@
histogram_tester.ExpectBucketCount(
"WebAuthentication.GPM.GetAssertion.LargeBlobSucceeded.Read",
/*sample=*/true, /*expected_count=*/1);
+
+ // Ensure the large blob is redacted from logs.
+ EXPECT_THAT(GetDeviceLog(),
+ testing::HasSubstr("\"largeBlob\": \"[redacted]\""));
}
// Disable large blob for GPM feature flag.
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index aba6cb4d..ded37f7 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -2570,6 +2570,7 @@
"//components/cookie_config",
"//components/country_codes",
"//components/data_sharing/public",
+ "//components/device_event_log",
"//components/dom_distiller/content/browser",
"//components/dom_distiller/content/browser:test_support",
"//components/dom_distiller/content/renderer",
diff --git a/device/fido/enclave/enclave_protocol_utils_unittest.cc b/device/fido/enclave/enclave_protocol_utils_unittest.cc
index cb7e04c..bf22f49 100644
--- a/device/fido/enclave/enclave_protocol_utils_unittest.cc
+++ b/device/fido/enclave/enclave_protocol_utils_unittest.cc
@@ -19,9 +19,11 @@
#include "base/values.h"
#include "components/cbor/reader.h"
#include "components/cbor/values.h"
+#include "components/cbor/writer.h"
#include "components/device_event_log/device_event_log.h"
#include "components/sync/protocol/webauthn_credential_specifics.pb.h"
#include "device/fido/ctap_make_credential_request.h"
+#include "device/fido/enclave/constants.h"
#include "device/fido/fido_parsing_utils.h"
#include "device/fido/fido_transport_protocol.h"
#include "device/fido/json_request.h"
@@ -94,6 +96,18 @@
constexpr char kMakeCredentialHexResponse[] =
"81A1626F6BA3677075625F6B657944050607086776657273696F6E0169656E637279707465"
"644401020304";
+
+// An example response with the top-level "ok" key, dummy large blob and PRF
+// values.
+constexpr char kCompleteGetAssertionHexResponse[] =
+ "A1626F6B81A1626F6BA3637072661904D268726573706F6E7365A3697369676E6174757265"
+ "A2646461746184185318691867186E6474797065664275666665726A7573657248616E646C"
+ "65A2646461746182186118626474797065664275666665727161757468656E74696361746F"
+ "7244617461A2646461746198251118941822188D18A818FD18BD18EE18FD1826181B18D718"
+ "B61859185C18FD187018A50D187018C61840187B18CF01183D18E9186D184E18FB1718DE01"
+ "000000183B647479706566427566666572696C61726765426C6F62A264726561641904D264"
+ "73697A6501";
+
constexpr int32_t kWrappedSecretVersion = 952;
struct BadResponseTestCase {
@@ -247,8 +261,11 @@
std::vector<uint8_t> wrapped_secret() { return wrapped_secret_; }
+ std::vector<uint8_t> secret() { return secret_; }
+
private:
const std::vector<uint8_t> wrapped_secret_ = {1, 2, 3, 4, 5};
+ const std::vector<uint8_t> secret_ = {6, 7, 8, 9, 0};
std::vector<uint8_t> device_id_;
std::vector<uint8_t> user_id_;
std::vector<uint8_t> encrypted_passkey_;
@@ -647,5 +664,54 @@
testing::ElementsAre(1, 2, 3));
}
+TEST_F(EnclaveProtocolUtilsTest, RedactEnclaveRequest) {
+ auto entity = PasskeyEntity();
+ entity.set_rp_id(kRpId);
+ std::optional<base::Value> parsed_json =
+ base::JSONReader::Read(kGetAssertionRequestJson);
+ EXPECT_TRUE(parsed_json);
+ auto json_request =
+ base::MakeRefCounted<JSONRequest>(std::move(*parsed_json));
+ cbor::Value request_cbor = BuildGetAssertionCommand(
+ std::move(entity), json_request, kClientDataJson,
+ /*claimed_pin=*/nullptr, /*wrapped_secret=*/std::nullopt, secret());
+ cbor::Value redacted = RedactEnclaveRequest(request_cbor);
+ ASSERT_TRUE(redacted.is_map());
+ const auto& redacted_map = redacted.GetMap();
+ const auto& redacted_value =
+ redacted_map.find(cbor::Value(kRequestSecretKey))->second;
+ EXPECT_EQ(redacted_value.GetString(), "[redacted]");
+}
+
+TEST_F(EnclaveProtocolUtilsTest, RedactErroneousEnclaveRequest) {
+ cbor::Value request = cbor::Value("not a valid request");
+ EXPECT_EQ(cbor::Writer::Write(RedactEnclaveRequest(request)),
+ cbor::Writer::Write(request));
+}
+
+TEST_F(EnclaveProtocolUtilsTest, RedactEnclaveResponse) {
+ std::vector<uint8_t> response_serialized;
+ CHECK(base::HexStringToBytes(kCompleteGetAssertionHexResponse,
+ &response_serialized));
+ cbor::Value response_cbor = cbor::Reader::Read(response_serialized).value();
+
+ const cbor::Value redacted = RedactEnclaveResponse(response_cbor);
+ const cbor::Value::MapValue& redacted_outer_map =
+ redacted.GetMap().find(cbor::Value("ok"))->second.GetArray()[0].GetMap();
+ const cbor::Value::MapValue& redacted_map =
+ redacted_outer_map.find(cbor::Value("ok"))->second.GetMap();
+ const auto& large_blob_value =
+ redacted_map.find(cbor::Value("largeBlob"))->second;
+ EXPECT_EQ(large_blob_value.GetString(), "[redacted]");
+ const auto& prf_value = redacted_map.find(cbor::Value("prf"))->second;
+ EXPECT_EQ(prf_value.GetString(), "[redacted]");
+}
+
+TEST_F(EnclaveProtocolUtilsTest, RedactErroneousEnclaveResponse) {
+ cbor::Value response = cbor::Value("not a valid response");
+ EXPECT_EQ(cbor::Writer::Write(RedactEnclaveResponse(response)),
+ cbor::Writer::Write(response));
+}
+
} // namespace enclave
} // namespace device
Original Bug Report
Master key that encrypts all passkeys is visible in plain text and vulnerable to leakage.
Security Bug
Important: Please do not change the component of this bug manually.
Please READ THIS FAQ before filing a bug: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/faq.md
Please see the following link for instructions on filing security bugs: https://www.chromium.org/Home/chromium-security/reporting-security-bugs
Reports may be eligible for reward payments under the Chrome VRP: https://g.co/chrome/vrp
NOTE: Security bugs are normally made public once a fix has been widely deployed.
VULNERABILITY DETAILS Master key(AKA Security domain key or SDK in chromium) that encrypts all the passkeys is visible in chrome://device-log in the session that user has registered their client with the GPM for storing passkeys. It’s also vulnerable from design perspective in code to be leaked via Chrome feedback tool(On ChromeOS) to Chrome’s own backend servers VERSION Version 138.0.7204.158 (Official Build) (64-bit) Operating System: Windows, Version 138.0.7204.158 (Official Build) (64-bit)
REPRODUCTION CASE
- Create a new google account or use an account with which you have never saved a passkey to GPM.
- Go to Webauthn.io and enter name of a passkey and click “Register” button.
- Select “Google password manager”(GPM) in the mechanism selection dialog , click Create and enter the GPM Pin to set it up.
- Once the passkeys is created, go to chrome://device-log and ensure “FIDO” is selected as one of the types.
- Observe that you can see a log like below where the log like below where secret as a key name has a value shown. This is the unencrypted SDK or the Security domain key(Image attached for a test account)
- Once you go to Chrome->Feedback on ChromeOS the same logs in chrome://device-log are uploaded via DeviceEventLogSource(https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/feedback/system_logs/log_sources/device_event_log_source.cc;bpv=1;bpt=1) class and visible to user as “non-network” section in the feedback logs. The only thing that prevents the SDK in this log to be uploaded to chrome feedback server is the redaction logic in RedactionTool::RedactHashes method. But it seems quite unrelated to the code in Enclave that generates the SDK. If any day someone changes to have a SDK of 40 bytes the SDK would start getting leaked to chrome feedback since RedactHashes only redacts hashes of size 16 to 32 bytes today. Or if some changes happen in the RedactHashes method the raw SDK would start leaking back to chrome feedback and reach chrome server unencrypted which a developer or who ever is looking at feedbacks from chrome’s side can see.
FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION Type of crash: [tab, browser, etc.] Crash State: [see link above: stack trace with symbols, registers, exception record] Client ID (if relevant): [see link above]
CREDIT INFORMATION Externally reported security bugs may appear in Chrome release notes. If this bug is included, how would you like to be credited? Reporter credit: Kamaraj Gandhirajan initially found the issue in chrome://device-log and Anoop Pandey(me) further found vulnerabilities in Chromium feedback and escalated this issue to Chromium owners.
- https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/faq.md
- https://g.co/chrome/vrp
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/feedback/system_logs/log_sources/device_event_log_source.cc;bpv=1;bpt=1
- https://www.chromium.org/Home/chromium-security/reporting-security-bugs