Settings: Introduce requestCredentialDetails and make note optional

This CL introduces requestCredentialDetails
which returns a PasswordUiEntry. The API is similar to
requestCredentialDetails. Since the requestCredentialDetails API is
still used (when the Password View subpage is disabled), I am keeping as
it is.

PasswordUiEntry now has the note field as optional, as the followup CLs
will put the note behind authentication.

Bug: 1345899
Change-Id: I94066bf4dcd5d474cdc4d51c880fb1d32710cbd6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3784370
Reviewed-by: Demetrios Papadopoulos <dpapad@chromium.org>
Reviewed-by: Istiaque Ahmed <lazyboy@chromium.org>
Reviewed-by: Viktor Semeniuk <vsemeniuk@google.com>
Commit-Queue: Adem Derinel <derinel@google.com>
Cr-Commit-Position: refs/heads/main@{#1032467}
diff --git a/chrome/browser/extensions/api/passwords_private/passwords_private_api.cc b/chrome/browser/extensions/api/passwords_private/passwords_private_api.cc
index 9f7d8aa..6bd15382 100644
--- a/chrome/browser/extensions/api/passwords_private/passwords_private_api.cc
+++ b/chrome/browser/extensions/api/passwords_private/passwords_private_api.cc
@@ -124,6 +124,40 @@
           ->id)));
 }
 
+// PasswordsPrivateRequestCredentialDetailsFunction
+ResponseAction PasswordsPrivateRequestCredentialDetailsFunction::Run() {
+  auto parameters =
+      api::passwords_private::RequestCredentialDetails::Params::Create(args());
+  EXTENSION_FUNCTION_VALIDATE(parameters);
+
+  GetDelegate(browser_context())
+      ->RequestCredentialDetails(
+          parameters->id,
+          base::BindOnce(&PasswordsPrivateRequestCredentialDetailsFunction::
+                             GotPasswordUiEntry,
+                         this),
+          GetSenderWebContents());
+
+  // GotPasswordUiEntry() might have responded before we reach this point.
+  return did_respond() ? AlreadyResponded() : RespondLater();
+}
+
+void PasswordsPrivateRequestCredentialDetailsFunction::GotPasswordUiEntry(
+    absl::optional<api::passwords_private::PasswordUiEntry> password_ui_entry) {
+  if (password_ui_entry) {
+    Respond(ArgumentList(
+        api::passwords_private::RequestCredentialDetails::Results::Create(
+            std::move(*password_ui_entry))));
+    return;
+  }
+
+  Respond(Error(base::StringPrintf(
+      "Could not obtain password entry. Either the user is not "
+      "authenticated or no credential with id = %d could be found.",
+      api::passwords_private::RequestCredentialDetails::Params::Create(args())
+          ->id)));
+}
+
 // PasswordsPrivateGetSavedPasswordListFunction
 ResponseAction PasswordsPrivateGetSavedPasswordListFunction::Run() {
   // GetList() can immediately call GotList() (which would Respond() before
diff --git a/chrome/browser/extensions/api/passwords_private/passwords_private_api.h b/chrome/browser/extensions/api/passwords_private/passwords_private_api.h
index 64e668cf..7a4b1bb 100644
--- a/chrome/browser/extensions/api/passwords_private/passwords_private_api.h
+++ b/chrome/browser/extensions/api/passwords_private/passwords_private_api.h
@@ -97,6 +97,23 @@
   void GotPassword(absl::optional<std::u16string> password);
 };
 
+class PasswordsPrivateRequestCredentialDetailsFunction
+    : public ExtensionFunction {
+ public:
+  DECLARE_EXTENSION_FUNCTION("passwordsPrivate.requestCredentialDetails",
+                             PASSWORDSPRIVATE_REQUESTCREDENTIALDETAILS)
+ protected:
+  ~PasswordsPrivateRequestCredentialDetailsFunction() override = default;
+
+  // ExtensionFunction overrides.
+  ResponseAction Run() override;
+
+ private:
+  void GotPasswordUiEntry(
+      absl::optional<api::passwords_private::PasswordUiEntry>
+          password_ui_entry);
+};
+
 class PasswordsPrivateGetSavedPasswordListFunction : public ExtensionFunction {
  public:
   DECLARE_EXTENSION_FUNCTION("passwordsPrivate.getSavedPasswordList",
diff --git a/chrome/browser/extensions/api/passwords_private/passwords_private_apitest.cc b/chrome/browser/extensions/api/passwords_private/passwords_private_apitest.cc
index cece829..f917abe 100644
--- a/chrome/browser/extensions/api/passwords_private/passwords_private_apitest.cc
+++ b/chrome/browser/extensions/api/passwords_private/passwords_private_apitest.cc
@@ -200,6 +200,15 @@
   EXPECT_TRUE(RunPasswordsSubtest("requestPlaintextPasswordFails")) << message_;
 }
 
+IN_PROC_BROWSER_TEST_F(PasswordsPrivateApiTest, RequestCredentialDetails) {
+  EXPECT_TRUE(RunPasswordsSubtest("requestCredentialDetails")) << message_;
+}
+
+IN_PROC_BROWSER_TEST_F(PasswordsPrivateApiTest, RequestCredentialDetailsFails) {
+  ResetPlaintextPassword();
+  EXPECT_TRUE(RunPasswordsSubtest("requestCredentialDetailsFails")) << message_;
+}
+
 IN_PROC_BROWSER_TEST_F(PasswordsPrivateApiTest, GetSavedPasswordList) {
   EXPECT_TRUE(RunPasswordsSubtest("getSavedPasswordList")) << message_;
 }
diff --git a/chrome/browser/extensions/api/passwords_private/passwords_private_delegate.h b/chrome/browser/extensions/api/passwords_private/passwords_private_delegate.h
index 3079618e..ac1301fa 100644
--- a/chrome/browser/extensions/api/passwords_private/passwords_private_delegate.h
+++ b/chrome/browser/extensions/api/passwords_private/passwords_private_delegate.h
@@ -33,6 +33,8 @@
  public:
   using PlaintextPasswordCallback =
       base::OnceCallback<void(absl::optional<std::u16string>)>;
+  using RequestCredentialDetailsCallback = base::OnceCallback<void(
+      absl::optional<api::passwords_private::PasswordUiEntry>)>;
 
   using RefreshScriptsIfNecessaryCallback = base::OnceClosure;
 
@@ -118,6 +120,20 @@
       PlaintextPasswordCallback callback,
       content::WebContents* web_contents) = 0;
 
+  // Requests the full PasswordUiEntry (with filled password) with the given id.
+  // Returns the full PasswordUiEntry with |callback|. Returns |absl::nullopt|
+  // if no matching credential with |id| is found.
+  // |id| the id created when going over the list of saved passwords.
+  // |reason| The reason why the full PasswordUiEntry is requested.
+  // |callback| The callback that gets invoked with the PasswordUiEntry if it
+  // could be obtained successfully, or absl::nullopt otherwise.
+  // |web_contents| The web content object used as the UI; will be used to show
+  //     an OS-level authentication dialog if necessary.
+  virtual void RequestCredentialDetails(
+      int id,
+      RequestCredentialDetailsCallback callback,
+      content::WebContents* web_contents) = 0;
+
   // Moves a list of passwords currently stored on the device to being stored in
   // the signed-in, non-syncing Google Account. The result of any password is a
   // no-op if any of these is true: |id| is invalid; |id| corresponds to a
diff --git a/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl.cc b/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl.cc
index 4e123a4..cce390f 100644
--- a/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl.cc
+++ b/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl.cc
@@ -149,6 +149,34 @@
   return {};
 }
 
+extensions::api::passwords_private::PasswordUiEntry
+CreatePasswordUiEntryFromCredentialUiEntry(
+    int id,
+    const CredentialUIEntry& credential) {
+  extensions::api::passwords_private::PasswordUiEntry entry;
+  entry.urls = extensions::CreateUrlCollectionFromCredential(credential);
+  entry.username = base::UTF16ToUTF8(credential.username);
+  // TODO(crbug.com/1345899): Fill the note field after authentication in
+  // OnRequestCredentialDetailsAuthResult
+  entry.note =
+      std::make_unique<std::string>(base::UTF16ToUTF8(credential.note.value));
+  entry.id = id;
+  entry.stored_in = extensions::StoreSetFromCredential(credential);
+  entry.is_android_credential =
+      password_manager::IsValidAndroidFacetURI(credential.signon_realm);
+  if (!credential.federation_origin.opaque()) {
+    std::u16string formatted_origin =
+        url_formatter::FormatOriginForSecurityDisplay(
+            credential.federation_origin,
+            url_formatter::SchemeDisplay::OMIT_CRYPTOGRAPHIC);
+
+    entry.federation_text =
+        std::make_unique<std::string>(l10n_util::GetStringFUTF8(
+            IDS_PASSWORDS_VIA_FEDERATION, formatted_origin));
+  }
+  return entry;
+}
+
 }  // namespace
 
 namespace extensions {
@@ -354,6 +382,23 @@
           weak_ptr_factory_.GetWeakPtr(), id, reason, std::move(callback)));
 }
 
+void PasswordsPrivateDelegateImpl::RequestCredentialDetails(
+    int id,
+    RequestCredentialDetailsCallback callback,
+    content::WebContents* web_contents) {
+  // Save |web_contents| so that it can be used later when OsReauthCall() is
+  // called. Note: This is safe because the |web_contents| is used before
+  // exiting this method.
+  // TODO(crbug.com/495290): Pass the native window directly to the
+  // reauth-handling code.
+  web_contents_ = web_contents;
+  password_access_authenticator_.EnsureUserIsAuthenticated(
+      GetReauthPurpose(api::passwords_private::PLAINTEXT_REASON_VIEW),
+      base::BindOnce(
+          &PasswordsPrivateDelegateImpl::OnRequestCredentialDetailsAuthResult,
+          weak_ptr_factory_.GetWeakPtr(), id, std::move(callback)));
+}
+
 void PasswordsPrivateDelegateImpl::OsReauthCall(
     password_manager::ReauthPurpose purpose,
     password_manager::PasswordAccessAuthenticator::AuthResultCallback
@@ -393,26 +438,8 @@
       current_exception_entry.id = id;
       current_exceptions_.push_back(std::move(current_exception_entry));
     } else {
-      api::passwords_private::PasswordUiEntry entry;
-      entry.urls = CreateUrlCollectionFromCredential(credential);
-      entry.username = base::UTF16ToUTF8(credential.username);
-      entry.note = base::UTF16ToUTF8(credential.note.value);
-      entry.id = id;
-      entry.stored_in = StoreSetFromCredential(credential);
-      entry.is_android_credential =
-          password_manager::IsValidAndroidFacetURI(credential.signon_realm);
-      if (!credential.federation_origin.opaque()) {
-        std::u16string formatted_origin =
-            url_formatter::FormatOriginForSecurityDisplay(
-                credential.federation_origin,
-                url_formatter::SchemeDisplay::OMIT_CRYPTOGRAPHIC);
-
-        entry.federation_text =
-            std::make_unique<std::string>(l10n_util::GetStringFUTF8(
-                IDS_PASSWORDS_VIA_FEDERATION, formatted_origin));
-      }
-
-      current_entries_.push_back(std::move(entry));
+      current_entries_.push_back(
+          CreatePasswordUiEntryFromCredentialUiEntry(id, credential));
     }
   }
 
@@ -648,22 +675,32 @@
   } else {
     std::move(callback).Run(entry->password);
   }
+  EmitHistogramsForCredentialAccess(*entry, reason);
+}
 
-  syncer::SyncService* sync_service = nullptr;
-  if (SyncServiceFactory::HasSyncService(profile_)) {
-    sync_service = SyncServiceFactory::GetForProfile(profile_);
-  }
-  if (password_manager::sync_util::IsSyncAccountCredential(
-          entry->url, entry->username, sync_service,
-          IdentityManagerFactory::GetForProfile(profile_))) {
-    base::RecordAction(
-        base::UserMetricsAction("PasswordManager_SyncCredentialShown"));
+void PasswordsPrivateDelegateImpl::OnRequestCredentialDetailsAuthResult(
+    int id,
+    RequestCredentialDetailsCallback callback,
+    bool authenticated) {
+  if (!authenticated) {
+    std::move(callback).Run(absl::nullopt);
+    return;
   }
 
-  UMA_HISTOGRAM_ENUMERATION(
-      "PasswordManager.AccessPasswordInSettings",
-      ConvertPlaintextReason(reason),
-      password_manager::metrics_util::ACCESS_PASSWORD_COUNT);
+  const CredentialUIEntry* credential = credential_id_generator_.TryGetKey(id);
+  if (!credential) {
+    std::move(callback).Run(absl::nullopt);
+    return;
+  }
+
+  api::passwords_private::PasswordUiEntry password_ui_entry =
+      CreatePasswordUiEntryFromCredentialUiEntry(id, *credential);
+  password_ui_entry.password =
+      std::make_unique<std::string>(base::UTF16ToUTF8(credential->password));
+  std::move(callback).Run(std::move(password_ui_entry));
+
+  EmitHistogramsForCredentialAccess(
+      *credential, api::passwords_private::PLAINTEXT_REASON_VIEW);
 }
 
 void PasswordsPrivateDelegateImpl::OnExportPasswordsAuthResult(
@@ -718,4 +755,24 @@
   pre_initialization_callbacks_.clear();
 }
 
+void PasswordsPrivateDelegateImpl::EmitHistogramsForCredentialAccess(
+    const CredentialUIEntry& entry,
+    api::passwords_private::PlaintextReason reason) {
+  syncer::SyncService* sync_service = nullptr;
+  if (SyncServiceFactory::HasSyncService(profile_)) {
+    sync_service = SyncServiceFactory::GetForProfile(profile_);
+  }
+  if (password_manager::sync_util::IsSyncAccountCredential(
+          entry.url, entry.username, sync_service,
+          IdentityManagerFactory::GetForProfile(profile_))) {
+    base::RecordAction(
+        base::UserMetricsAction("PasswordManager_SyncCredentialShown"));
+  }
+
+  UMA_HISTOGRAM_ENUMERATION(
+      "PasswordManager.AccessPasswordInSettings",
+      ConvertPlaintextReason(reason),
+      password_manager::metrics_util::ACCESS_PASSWORD_COUNT);
+}
+
 }  // namespace extensions
diff --git a/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl.h b/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl.h
index fe616f8..37c48e7d 100644
--- a/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl.h
+++ b/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl.h
@@ -25,6 +25,7 @@
 #include "components/password_manager/core/browser/password_access_authenticator.h"
 #include "components/password_manager/core/browser/password_account_storage_settings_watcher.h"
 #include "components/password_manager/core/browser/reauth_purpose.h"
+#include "components/password_manager/core/browser/ui/credential_ui_entry.h"
 #include "components/password_manager/core/browser/ui/export_progress_status.h"
 #include "components/password_manager/core/browser/ui/saved_passwords_presenter.h"
 #include "extensions/browser/extension_function.h"
@@ -75,6 +76,9 @@
                                 api::passwords_private::PlaintextReason reason,
                                 PlaintextPasswordCallback callback,
                                 content::WebContents* web_contents) override;
+  void RequestCredentialDetails(int id,
+                                RequestCredentialDetailsCallback callback,
+                                content::WebContents* web_contents) override;
   void MovePasswordsToAccount(const std::vector<int>& ids,
                               content::WebContents* web_contents) override;
   void ImportPasswords(content::WebContents* web_contents) override;
@@ -162,6 +166,12 @@
       PlaintextPasswordCallback callback,
       bool authenticated);
 
+  // Callback for RequestCredentialDetails() after authentication check.
+  void OnRequestCredentialDetailsAuthResult(
+      int id,
+      RequestCredentialDetailsCallback callback,
+      bool authenticated);
+
   // Callback for ExportPasswords() after authentication check.
   void OnExportPasswordsAuthResult(
       base::OnceCallback<void(const std::string&)> accepted_callback,
@@ -179,6 +189,11 @@
       password_manager::PasswordAccessAuthenticator::AuthResultCallback
           callback);
 
+  // Records user action and emits histogram values for retrieving |entry|.
+  void EmitHistogramsForCredentialAccess(
+      const password_manager::CredentialUIEntry& entry,
+      api::passwords_private::PlaintextReason reason);
+
   // Not owned by this class.
   raw_ptr<Profile> profile_;
 
diff --git a/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl_unittest.cc b/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl_unittest.cc
index 5511fc4b..a9dc3d4 100644
--- a/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl_unittest.cc
+++ b/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl_unittest.cc
@@ -68,6 +68,8 @@
 
 using MockPlaintextPasswordCallback =
     base::MockCallback<PasswordsPrivateDelegate::PlaintextPasswordCallback>;
+using MockRequestCredentialDetailsCallback = base::MockCallback<
+    PasswordsPrivateDelegate::RequestCredentialDetailsCallback>;
 
 class MockPasswordManagerClient : public ChromePasswordManagerClient {
  public:
@@ -197,7 +199,7 @@
 MATCHER_P(PasswordUiEntryDataEquals, expected, "") {
   return testing::Value(expected.get().urls.link, arg.urls.link) &&
          testing::Value(expected.get().username, arg.username) &&
-         testing::Value(expected.get().note, arg.note) &&
+         testing::Value(*expected.get().note, *arg.note) &&
          testing::Value(expected.get().stored_in, arg.stored_in) &&
          testing::Value(expected.get().is_android_credential,
                         arg.is_android_credential);
@@ -381,13 +383,13 @@
   api::passwords_private::PasswordUiEntry expected_entry1;
   expected_entry1.urls.link = "https://example1.com/";
   expected_entry1.username = "username1";
-  expected_entry1.note = "";
+  expected_entry1.note = std::make_unique<std::string>();
   expected_entry1.stored_in =
       api::passwords_private::PASSWORD_STORE_SET_ACCOUNT;
   api::passwords_private::PasswordUiEntry expected_entry2;
   expected_entry2.urls.link = "http://example2.com/login";
   expected_entry2.username = "";
-  expected_entry2.note = "note";
+  expected_entry2.note = std::make_unique<std::string>("note");
   expected_entry2.stored_in = api::passwords_private::PASSWORD_STORE_SET_DEVICE;
   EXPECT_CALL(callback,
               Run(testing::UnorderedElementsAre(
@@ -473,7 +475,7 @@
   EXPECT_CALL(callback, Run(SizeIs(1)))
       .WillOnce([](const PasswordsPrivateDelegate::UiEntries& passwords) {
         EXPECT_EQ("new_user", passwords[0].username);
-        EXPECT_EQ("", passwords[0].note);
+        EXPECT_THAT(passwords[0].note, Pointee(Eq("")));
       });
   delegate.GetSavedPasswordsList(callback.Get());
 }
@@ -499,8 +501,8 @@
       .WillOnce([&](const PasswordsPrivateDelegate::UiEntries& passwords) {
         EXPECT_EQ(sample_form.username_value,
                   base::UTF8ToUTF16(passwords[0].username));
-        EXPECT_EQ(sample_form.notes[1].value,
-                  base::UTF8ToUTF16(passwords[0].note));
+        EXPECT_THAT(passwords[0].note,
+                    Pointee(Eq(base::UTF16ToUTF8(sample_form.notes[1].value))));
       });
   delegate.GetSavedPasswordsList(callback.Get());
   int sample_form_id = delegate.GetIdForCredential(
@@ -527,7 +529,7 @@
   EXPECT_CALL(callback, Run(SizeIs(1)))
       .WillOnce([](const PasswordsPrivateDelegate::UiEntries& passwords) {
         EXPECT_EQ("new_user", passwords[0].username);
-        EXPECT_EQ("new note", passwords[0].note);
+        EXPECT_THAT(passwords[0].note, Pointee(Eq("new note")));
       });
   delegate.GetSavedPasswordsList(callback.Get());
 }
@@ -694,7 +696,7 @@
   delegate.RequestPlaintextPassword(
       0, api::passwords_private::PLAINTEXT_REASON_COPY, password_callback.Get(),
       nullptr);
-  // Clipboard should not be modifiend in case Reauth failed
+  // Clipboard should not be modified in case Reauth failed
   std::u16string result;
   test_clipboard_->ReadText(ui::ClipboardBuffer::kCopyPaste,
                             /* data_dst = */ nullptr, &result);
@@ -732,6 +734,38 @@
       1);
 }
 
+TEST_F(PasswordsPrivateDelegateImplTest,
+       TestPassedReauthOnRequestCredentialDetails) {
+  SetUpPasswordStores({CreateSampleForm()});
+
+  PasswordsPrivateDelegateImpl delegate(&profile_);
+  // Spin the loop to allow PasswordStore tasks posted on the creation of
+  // |delegate| to be completed.
+  base::RunLoop().RunUntilIdle();
+
+  MockReauthCallback callback;
+  delegate.set_os_reauth_call(callback.Get());
+
+  EXPECT_CALL(callback, Run(ReauthPurpose::VIEW_PASSWORD, _))
+      .WillOnce(testing::WithArg<1>(
+          [&](password_manager::PasswordAccessAuthenticator::AuthResultCallback
+                  callback) { std::move(callback).Run(true); }));
+
+  MockRequestCredentialDetailsCallback password_callback;
+  EXPECT_CALL(password_callback, Run)
+      .WillOnce(
+          [&](absl::optional<api::passwords_private::PasswordUiEntry> entry) {
+            EXPECT_THAT(entry->password, Pointee(Eq("test")));
+            EXPECT_THAT(entry->username, Eq("test@gmail.com"));
+          });
+
+  delegate.RequestCredentialDetails(0, password_callback.Get(), nullptr);
+
+  histogram_tester().ExpectUniqueSample(
+      kHistogramName, password_manager::metrics_util::ACCESS_PASSWORD_VIEWED,
+      1);
+}
+
 TEST_F(PasswordsPrivateDelegateImplTest, TestFailedReauthOnView) {
   SetUpPasswordStores({CreateSampleForm()});
 
@@ -758,6 +792,31 @@
   histogram_tester().ExpectTotalCount(kHistogramName, 0);
 }
 
+TEST_F(PasswordsPrivateDelegateImplTest,
+       TestFailedReauthOnRequestCredentialDetails) {
+  SetUpPasswordStores({CreateSampleForm()});
+
+  PasswordsPrivateDelegateImpl delegate(&profile_);
+  // Spin the loop to allow PasswordStore tasks posted on the creation of
+  // |delegate| to be completed.
+  base::RunLoop().RunUntilIdle();
+
+  MockReauthCallback callback;
+  delegate.set_os_reauth_call(callback.Get());
+
+  EXPECT_CALL(callback, Run(ReauthPurpose::VIEW_PASSWORD, _))
+      .WillOnce(testing::WithArg<1>(
+          [&](password_manager::PasswordAccessAuthenticator::AuthResultCallback
+                  callback) { std::move(callback).Run(false); }));
+
+  MockRequestCredentialDetailsCallback password_callback;
+  EXPECT_CALL(password_callback, Run(Eq(absl::nullopt)));
+  delegate.RequestCredentialDetails(0, password_callback.Get(), nullptr);
+
+  // Since Reauth had failed password was not viewed and metric wasn't recorded
+  histogram_tester().ExpectTotalCount(kHistogramName, 0);
+}
+
 TEST_F(PasswordsPrivateDelegateImplTest, TestReauthFailedOnExport) {
   SetUpPasswordStores({CreateSampleForm()});
   StrictMock<base::MockCallback<base::OnceCallback<void(const std::string&)>>>
@@ -899,7 +958,7 @@
   expected_entry.urls.link =
       "https://play.google.com/store/apps/details?id=example.com";
   expected_entry.username = "test@gmail.com";
-  expected_entry.note = "";
+  expected_entry.note = std::make_unique<std::string>();
   expected_entry.is_android_credential = true;
   expected_entry.stored_in = api::passwords_private::PASSWORD_STORE_SET_DEVICE;
   EXPECT_CALL(callback, Run(testing::ElementsAre(PasswordUiEntryDataEquals(
diff --git a/chrome/browser/extensions/api/passwords_private/test_passwords_private_delegate.cc b/chrome/browser/extensions/api/passwords_private/test_passwords_private_delegate.cc
index c883996..d0dcbd7 100644
--- a/chrome/browser/extensions/api/passwords_private/test_passwords_private_delegate.cc
+++ b/chrome/browser/extensions/api/passwords_private/test_passwords_private_delegate.cc
@@ -151,6 +151,20 @@
   std::move(callback).Run(plaintext_password_);
 }
 
+void TestPasswordsPrivateDelegate::RequestCredentialDetails(
+    int id,
+    RequestCredentialDetailsCallback callback,
+    content::WebContents* web_contents) {
+  api::passwords_private::PasswordUiEntry entry = CreateEntry(42);
+  if (plaintext_password_.has_value()) {
+    entry.password = std::make_unique<std::string>(
+        base::UTF16ToUTF8(plaintext_password_.value()));
+    std::move(callback).Run(std::move(entry));
+  } else {
+    std::move(callback).Run(std::move(absl::nullopt));
+  }
+}
+
 void TestPasswordsPrivateDelegate::MovePasswordsToAccount(
     const std::vector<int>& ids,
     content::WebContents* web_contents) {
diff --git a/chrome/browser/extensions/api/passwords_private/test_passwords_private_delegate.h b/chrome/browser/extensions/api/passwords_private/test_passwords_private_delegate.h
index 7894b9a8..6ace7622 100644
--- a/chrome/browser/extensions/api/passwords_private/test_passwords_private_delegate.h
+++ b/chrome/browser/extensions/api/passwords_private/test_passwords_private_delegate.h
@@ -54,6 +54,9 @@
                                 api::passwords_private::PlaintextReason reason,
                                 PlaintextPasswordCallback callback,
                                 content::WebContents* web_contents) override;
+  void RequestCredentialDetails(int id,
+                                RequestCredentialDetailsCallback callback,
+                                content::WebContents* web_contents) override;
   void MovePasswordsToAccount(const std::vector<int>& ids,
                               content::WebContents* web_contents) override;
   void ImportPasswords(content::WebContents* web_contents) override;
diff --git a/chrome/browser/resources/settings/autofill_page/password_edit_dialog.ts b/chrome/browser/resources/settings/autofill_page/password_edit_dialog.ts
index eaa303dc..5386ca7 100644
--- a/chrome/browser/resources/settings/autofill_page/password_edit_dialog.ts
+++ b/chrome/browser/resources/settings/autofill_page/password_edit_dialog.ts
@@ -299,7 +299,7 @@
     if (this.existingEntry) {
       this.websiteUrls_ = this.existingEntry.urls;
       this.username_ = this.existingEntry.username;
-      this.note_ = this.existingEntry.note;
+      this.note_ = this.existingEntry.note || '';
     }
     this.password_ = this.getPassword_();
     if (!this.isInFederatedViewMode_) {
diff --git a/chrome/common/extensions/api/passwords_private.idl b/chrome/common/extensions/api/passwords_private.idl
index 0143ac5..d737948 100644
--- a/chrome/common/extensions/api/passwords_private.idl
+++ b/chrome/common/extensions/api/passwords_private.idl
@@ -170,7 +170,7 @@
     DOMString username;
 
     // The password of the credential. Empty by default, only set if explicitly
-    // requested. Populated exclusively by JS.
+    // requested.
     DOMString? password;
 
     // Text shown if the password was obtained via a federated identity.
@@ -186,7 +186,7 @@
     boolean isAndroidCredential;
 
     // The value of the attached note.
-    DOMString note;
+    DOMString? note;
 
     // The URL where the insecure password can be changed. Might be not set for
     // Android apps.
@@ -268,6 +268,7 @@
   };
 
   callback PlaintextPasswordCallback = void(DOMString password);
+  callback RequestCredentialDetailsCallback = void(PasswordUiEntry entry);
   callback ChangeSavedPasswordCallback = void(long newId);
   callback PasswordListCallback = void(PasswordUiEntry[] entries);
   callback ExceptionListCallback = void(ExceptionEntry[] exceptions);
@@ -323,6 +324,17 @@
         PlaintextReason reason,
         PlaintextPasswordCallback callback);
 
+    // Returns the PasswordUiEntry (with |password| field filled) corresponding
+    // to |id|. Note that on some operating systems, this call may result in an
+    // OS-level reauthentication. Once the PasswordUiEntry has been fetched, it
+    // will be returned via |callback|.
+    // |id|: The id for the password entry being being retrieved.
+    // |callback|: The callback that gets invoked with the retrieved
+    // PasswordUiEntry.
+    [supportPromises] static void requestCredentialDetails(
+        long id,
+        RequestCredentialDetailsCallback callback);
+
     // Returns the list of saved passwords.
     // |callback|: Called with the list of saved passwords.
     [supportsPromises] static void getSavedPasswordList(
diff --git a/chrome/test/data/extensions/api_test/passwords_private/test.js b/chrome/test/data/extensions/api_test/passwords_private/test.js
index e4ab2d7..094ebdb4 100644
--- a/chrome/test/data/extensions/api_test/passwords_private/test.js
+++ b/chrome/test/data/extensions/api_test/passwords_private/test.js
@@ -197,6 +197,27 @@
         });
   },
 
+  function requestCredentialDetails() {
+    chrome.passwordsPrivate.requestCredentialDetails(0, passwordUiEntry => {
+      // Ensure that the callback is invoked without an error state and the
+      // expected plaintext password.
+      chrome.test.assertNoLastError();
+      chrome.test.assertEq('plaintext', passwordUiEntry.password);
+      chrome.test.succeed();
+    });
+  },
+
+  function requestCredentialDetailsFails() {
+    chrome.passwordsPrivate.requestCredentialDetails(123, passwordUiEntry => {
+      // Ensure that the callback is invoked with an error state and the
+      // message contains the right id.
+      chrome.test.assertLastError(
+          'Could not obtain password entry. Either the user is not ' +
+          'authenticated or no credential with id = 123 could be found.');
+      chrome.test.succeed();
+    });
+  },
+
   function getSavedPasswordList() {
     var callback = function(list) {
       chrome.test.assertTrue(!!list);
diff --git a/extensions/browser/extension_function_histogram_value.h b/extensions/browser/extension_function_histogram_value.h
index 5edf1d4d..e95fcd4 100644
--- a/extensions/browser/extension_function_histogram_value.h
+++ b/extensions/browser/extension_function_histogram_value.h
@@ -1757,6 +1757,7 @@
   FILEMANAGERPRIVATE_SHOWDLPRESTRICTIONDETAILS = 1694,
   ACCESSIBILITY_PRIVATE_SILENCESPOKENFEEDBACK = 1695,
   AUTOTESTPRIVATE_SETALLOWEDPREF = 1696,
+  PASSWORDSPRIVATE_REQUESTCREDENTIALDETAILS = 1697,
   // Last entry: Add new entries above, then run:
   // tools/metrics/histograms/update_extension_histograms.py
   ENUM_BOUNDARY
diff --git a/third_party/closure_compiler/externs/passwords_private.js b/third_party/closure_compiler/externs/passwords_private.js
index b96870f..37b0a2aa 100644
--- a/third_party/closure_compiler/externs/passwords_private.js
+++ b/third_party/closure_compiler/externs/passwords_private.js
@@ -143,7 +143,7 @@
  *   id: number,
  *   storedIn: !chrome.passwordsPrivate.PasswordStoreSet,
  *   isAndroidCredential: boolean,
- *   note: string,
+ *   note: (string|undefined),
  *   changePasswordUrl: (string|undefined),
  *   hasStartableScript: boolean,
  *   compromisedInfo: (!chrome.passwordsPrivate.CompromisedInfo|undefined)
@@ -249,6 +249,17 @@
 chrome.passwordsPrivate.requestPlaintextPassword = function(id, reason, callback) {};
 
 /**
+ * Returns the PasswordUiEntry (with |password| field filled) corresponding to
+ * |id|. Note that on some operating systems, this call may result in an
+ * OS-level reauthentication. Once the PasswordUiEntry has been fetched, it will
+ * be returned via |callback|.
+ * @param {number} id The id for the password entry being being retrieved.
+ * @param {function(!chrome.passwordsPrivate.PasswordUiEntry): void} callback
+ *     The callback that gets invoked with the retrieved PasswordUiEntry.
+ */
+chrome.passwordsPrivate.requestCredentialDetails = function(id, callback) {};
+
+/**
  * Returns the list of saved passwords.
  * @param {function(!Array<!chrome.passwordsPrivate.PasswordUiEntry>): void}
  *     callback Called with the list of saved passwords.
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index 7be88e6d..a709061 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -35241,6 +35241,7 @@
   <int value="1694" label="FILEMANAGERPRIVATE_SHOWDLPRESTRICTIONDETAILS"/>
   <int value="1695" label="ACCESSIBILITY_PRIVATE_SILENCESPOKENFEEDBACK"/>
   <int value="1696" label="AUTOTESTPRIVATE_SETALLOWEDPREF"/>
+  <int value="1697" label="PASSWORDSPRIVATE_REQUESTCREDENTIALDETAILS"/>
 </enum>
 
 <enum name="ExtensionIconState">
diff --git a/tools/typescript/definitions/passwords_private.d.ts b/tools/typescript/definitions/passwords_private.d.ts
index ca79875..199b183 100644
--- a/tools/typescript/definitions/passwords_private.d.ts
+++ b/tools/typescript/definitions/passwords_private.d.ts
@@ -105,7 +105,7 @@
         id: number;
         storedIn: PasswordStoreSet;
         isAndroidCredential: boolean;
-        note: string;
+        note?: string;
         changePasswordUrl?: string;
         hasStartableScript: boolean;
         compromisedInfo?: CompromisedInfo;
@@ -146,12 +146,16 @@
       export function changeSavedPassword(
           id: number, params: ChangeSavedPasswordParams,
           callback?: (newId: number) => void): void;
-      export function removeSavedPassword(id: number, fromStores: PasswordStoreSet): void;
+      export function removeSavedPassword(
+          id: number, fromStores: PasswordStoreSet): void;
       export function removePasswordException(id: number): void;
       export function undoRemoveSavedPasswordOrException(): void;
       export function requestPlaintextPassword(
           id: number, reason: PlaintextReason,
           callback: (password: string) => void): void;
+      export function requestCredentialDetails(
+          id: number,
+          callback: (passwordUiEntry: PasswordUiEntry) => void): void;
       export function getSavedPasswordList(
           callback: (entries: Array<PasswordUiEntry>) => void): void;
       export function getPasswordExceptionList(