Low watch CVSS 6.2 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
6.2
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Passkeys
DescriptionInappropriate implementation in Passkeys
ComponentPasskeys
Sub-componentWebauthn
Bug ClassLogic Error
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker434977743
Fix commit3e819c06ee43 (chromium/src) +199/-2
CWECWE-1295
CVSS vectorCVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedKamaraj Gandhirajan and Anoop Pandey
Reported2025-07-30 (per vendor advisory)
Fix landed2025-07-31 (public git)
Disclosed2025-09-02
Patch gap33 days — fix public in source before the advisory

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 with BuildGetAssertionCommand(..., 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 on chrome://device-log), which the new browser tests read back with device_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.

Key insight
The pre-encryption request and post-decryption response were serialized into a persistent, user-viewable log, which undid the confidentiality the enclave channel provides. The fix logs only a cloned copy where the known secret-bearing CBOR fields have been replaced with "[redacted]".

Attack Path

  1. Passkey operation with the enclave a user creates or uses a GPM passkey, so Transaction sends a request containing "secret" and receives a response that may contain "prf" or "largeBlob".
  2. Plaintext logging before the patch, FIDO_LOG(EVENT) wrote the full diagnostic CBOR of both messages into the "fido" device event log.
  3. Log exposure anyone who can read that log can recover these values, either from chrome://device-log on 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).
  4. 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

This is an information leak from the browser process into its diagnostic event log, not memory corruption. The attacker needs access to the victim’s device-log contents, either local access to the browser or a log that was shared. The vendor rates it low severity as an “Inappropriate implementation in Passkeys”, with no in-the-wild exploitation reported. Any compromise of passkey confidentiality that follows from the leaked secret is (inferred).

Changed Functions

FunctionChangeNotes
for
device/fido/enclave/enclave_protocol_utils.cc
modified
IN_PROC_BROWSER_TEST_F
chrome/browser/webauthn/enclave_authenticator_browsertest.cc
modified
TEST_F
device/fido/enclave/enclave_protocol_utils_unittest.cc
modified

Files Changed

  • chrome/browser/webauthn/enclave_authenticator_browsertest.cc
  • chrome/test/BUILD.gn
  • device/fido/enclave/enclave_protocol_utils.cc
  • device/fido/enclave/enclave_protocol_utils.h
  • device/fido/enclave/enclave_protocol_utils_unittest.cc
  • device/fido/enclave/transact.cc

Audit Directions

  • Logging around encryption boundaries
    look for FIDO_LOG, VLOG and DiagnosticWriter::Write calls 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
    RedactPath redacts 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 sink
    audit what other device_event_log categories (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.
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",
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by an...@microsoft.com

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

  1. Create a new google account or use an account with which you have never saved a passkey to GPM.
  2. Go to Webauthn.io and enter name of a passkey and click “Register” button.
  3. Select “Google password manager”(GPM) in the mechanism selection dialog , click Create and enter the GPM Pin to set it up.
  4. Once the passkeys is created, go to chrome://device-log and ensure “FIDO” is selected as one of the types.
  5. 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)
  6. 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.

View on issue tracker