[webauthn] Do not skip UV after iCloud recovery

When a user recovers their security domain secret through MagicArch,
they had to enter some form of user verification, so we don't prompt
them for a second UV to satisfy the user verification requirement.

This logic was erroneously applying to iCloud keychain recovery keys.

Fixed: 367771121
Change-Id: I4dc03f8568285cdc2ca14dbc65df0f4724cdac10
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5873366
Reviewed-by: Adam Langley <agl@chromium.org>
Commit-Queue: Nina Satragno <nsatragno@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1358217}
diff --git a/chrome/browser/webauthn/enclave_authenticator_browsertest.cc b/chrome/browser/webauthn/enclave_authenticator_browsertest.cc
index cbe7d6fd..bf8e4d7 100644
--- a/chrome/browser/webauthn/enclave_authenticator_browsertest.cc
+++ b/chrome/browser/webauthn/enclave_authenticator_browsertest.cc
@@ -2952,11 +2952,19 @@
     // factor that should have been added.
     trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
         registration_state_result;
+    const auto pin_member = std::ranges::find_if(
+        security_domain_service_->members(),
+        [](const trusted_vault_pb::SecurityDomainMember& member) {
+          return member.member_type() ==
+                 trusted_vault_pb::SecurityDomainMember::
+                     MEMBER_TYPE_GOOGLE_PASSWORD_MANAGER_PIN;
+        });
+    const auto& pin_metadata =
+        pin_member->member_metadata().google_password_manager_pin_metadata();
     registration_state_result.gpm_pin_metadata = trusted_vault::GpmPinMetadata(
-        "public key",
-        EnclaveManager::MakeWrappedPINForTesting(kSecurityDomainSecret,
-                                                 "123456"),
-        /*expiry=*/base::Time::Now() + base::Seconds(10000));
+        pin_member->public_key(), pin_metadata.encrypted_pin_hash(),
+        base::Time::FromSecondsSinceUnixEpoch(
+            pin_metadata.expiration_time().seconds()));
     registration_state_result.state =
         trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult::
             State::kRecoverable;
@@ -2990,7 +2998,7 @@
     content::WebContents* web_contents =
         browser()->tab_strip_model()->GetActiveWebContents();
     content::DOMMessageQueue message_queue(web_contents);
-    content::ExecuteScriptAsync(web_contents, kMakeCredentialUvDiscouraged);
+    content::ExecuteScriptAsync(web_contents, kMakeCredentialUvRequired);
     delegate_observer()->WaitForUI();
 
     EXPECT_EQ(
@@ -3000,11 +3008,17 @@
                   ->enclave_controller_for_testing()
                   ->account_state_for_testing(),
               GPMEnclaveController::AccountState::kRecoverable);
+
+    // User verification must not be skipped when recovering from an iCloud key.
+    model_observer()->SetStepToObserve(
+        AuthenticatorRequestDialogModel::Step::kGPMEnterPin);
     dialog_model()->OnTrustThisComputer();
+    model_observer()->WaitForStep();
+    dialog_model()->OnGPMPinEntered(u"123456");
 
     std::string script_result;
     ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
-    EXPECT_EQ(script_result, "\"webauthn: OK\"");
+    EXPECT_EQ(script_result, "\"webauthn: uv=true\"");
 
     delegate_observer()->WaitForDelegateDestruction();
 
diff --git a/chrome/browser/webauthn/gpm_enclave_controller.cc b/chrome/browser/webauthn/gpm_enclave_controller.cc
index f4736f9..61b4870 100644
--- a/chrome/browser/webauthn/gpm_enclave_controller.cc
+++ b/chrome/browser/webauthn/gpm_enclave_controller.cc
@@ -313,7 +313,7 @@
 // Pick an enclave user verification method for a specific request.
 EnclaveUserVerificationMethod PickEnclaveUserVerificationMethod(
     device::UserVerificationRequirement uv,
-    bool have_added_device,
+    bool have_entered_pin_for_recovery,
     bool has_pin,
     EnclaveManager::UvKeyState uv_key_state,
     bool platform_has_biometrics,
@@ -324,7 +324,7 @@
   constexpr bool kIsMac = false;
 #endif
 
-  if (have_added_device) {
+  if (have_entered_pin_for_recovery) {
     return EnclaveUserVerificationMethod::kImplicit;
   }
 
@@ -560,7 +560,8 @@
   if (request_type_ == device::FidoRequestType::kGetAssertion) {
     if (enclave_manager_->is_ready()) {
       switch (PickEnclaveUserVerificationMethod(
-          user_verification_requirement_, /*have_added_device=*/false,
+          user_verification_requirement_,
+          /*have_entered_pin_for_recovery=*/false,
           enclave_manager_->has_wrapped_pin(),
           enclave_manager_->uv_key_state(/*platform_has_biometrics=*/false),
           /*platform_has_biometrics=*/false, BrowserIsApp())) {
@@ -781,7 +782,6 @@
     // iCloud keychain recovery.
     device::enclave::RecordEvent(
         device::enclave::Event::kICloudRecoverySuccessful);
-    recovered_with_icloud_keychain_ = false;
   } else if (model_->step() == Step::kRecoverSecurityDomain) {
     // MagicArch recovery.
     webauthn::user_actions::RecordRecoverySucceeded();
@@ -941,16 +941,34 @@
   SetFailedPINAttemptCount(0);
 
   uv_method_ = PickEnclaveUserVerificationMethod(
-      user_verification_requirement_, have_added_device_,
+      user_verification_requirement_,
+      have_added_device_ && !recovered_with_icloud_keychain_,
       enclave_manager_->has_wrapped_pin(),
       enclave_manager_->uv_key_state(*model_->platform_has_biometrics),
       *model_->platform_has_biometrics, BrowserIsApp());
-  // `have_added_device_` is set, which satisfies UV, so we must have picked
-  // "implicit" UV.
-  CHECK_EQ(*uv_method_, EnclaveUserVerificationMethod::kImplicit);
+  switch (*uv_method_) {
+    case EnclaveUserVerificationMethod::kUVKeyWithSystemUI:
+    case EnclaveUserVerificationMethod::kDeferredUVKeyWithSystemUI:
+    case EnclaveUserVerificationMethod::kNone:
+    case EnclaveUserVerificationMethod::kImplicit:
+      model_->DisableUiOrShowLoadingDialog();
+      StartTransaction();
+      break;
 
-  model_->DisableUiOrShowLoadingDialog();
-  StartTransaction();
+    case EnclaveUserVerificationMethod::kUVKeyWithChromeUI:
+      model_->SetStep(Step::kGPMTouchID);
+      break;
+
+    case EnclaveUserVerificationMethod::kUnsatisfiable:
+      // TODO(crbug.com/367985619): it's possible to get to this state if a user
+      // recovers from iCloud keychain and does not have a usable PIN.
+      model_->SetStep(Step::kGPMError);
+      break;
+
+    case EnclaveUserVerificationMethod::kPIN:
+      PromptForPin();
+      break;
+  }
 }
 
 void GPMEnclaveController::SetAccountStateReady() {
@@ -972,7 +990,8 @@
 
     case AccountState::kReady:
       uv_method_ = PickEnclaveUserVerificationMethod(
-          user_verification_requirement_, have_added_device_,
+          user_verification_requirement_,
+          have_added_device_ && !recovered_with_icloud_keychain_,
           enclave_manager_->has_wrapped_pin(),
           enclave_manager_->uv_key_state(*model_->platform_has_biometrics),
           *model_->platform_has_biometrics, BrowserIsApp());
@@ -1025,7 +1044,8 @@
   switch (account_state_) {
     case AccountState::kReady:
       uv_method_ = PickEnclaveUserVerificationMethod(
-          user_verification_requirement_, have_added_device_,
+          user_verification_requirement_,
+          have_added_device_ && !recovered_with_icloud_keychain_,
           enclave_manager_->has_wrapped_pin(),
           enclave_manager_->uv_key_state(*model_->platform_has_biometrics),
           *model_->platform_has_biometrics, BrowserIsApp());