Bug 1641391 Protect login export behind Master Password and/or OS Reauthentication r=MattN,fluent-reviewers,flod
authorAndrei Cristian Petcu <petcuandrei@protonmail.com>
Wed, 03 Jun 2020 06:22:24 +0000
changeset 597750 c6b1d2548c4eb3e5a8816a62a0e40b67f99dcc72
parent 597749 d77619f078aaa11a1fd4eb1977b896c0927b2c85
child 597751 308d2f7a4a13f1e6cce49ed303063f1fb637c239
push id13310
push userffxbld-merge
push dateMon, 29 Jun 2020 14:50:06 +0000
treeherdermozilla-beta@15a59a0afa5c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN, fluent-reviewers, flod
bugs1641391
milestone79.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1641391 Protect login export behind Master Password and/or OS Reauthentication r=MattN,fluent-reviewers,flod Differential Revision: https://phabricator.services.mozilla.com/D77593
browser/components/aboutlogins/AboutLoginsParent.jsm
browser/components/aboutlogins/content/aboutLogins.html
browser/components/aboutlogins/content/aboutLoginsUtils.js
browser/components/aboutlogins/content/components/login-item.js
browser/components/aboutlogins/tests/browser/browser_openExport.js
browser/components/aboutlogins/tests/chrome/test_menu_button.html
browser/locales/en-US/browser/aboutLogins.ftl
toolkit/components/telemetry/Events.yaml
--- a/browser/components/aboutlogins/AboutLoginsParent.jsm
+++ b/browser/components/aboutlogins/AboutLoginsParent.jsm
@@ -460,16 +460,52 @@ class AboutLoginsParent extends JSWindow
         try {
           Services.logins.modifyLogin(logins[0], modifiedLogin);
         } catch (error) {
           this.handleLoginStorageErrors(modifiedLogin, error, message);
         }
         break;
       }
       case "AboutLogins:ExportPasswords": {
+        let messageText = { value: "NOT SUPPORTED" };
+        let captionText = { value: "" };
+
+        // This feature is only supported on Windows and macOS
+        // but we still call in to OSKeyStore on Linux to get
+        // the proper auth_details for Telemetry.
+        // See bug 1614874 for Linux support.
+        if (OSKeyStore.canReauth()) {
+          let messageId =
+            "about-logins-export-password-os-auth-dialog-message-" +
+            AppConstants.platform;
+          [messageText, captionText] = await AboutLoginsL10n.formatMessages([
+            {
+              id: messageId,
+            },
+            {
+              id: "about-logins-os-auth-dialog-caption",
+            },
+          ]);
+        }
+
+        let { isAuthorized, telemetryEvent } = await LoginHelper.requestReauth(
+          this.browsingContext.embedderElement,
+          true,
+          null, // Prompt regardless of a recent prompt
+          messageText.value,
+          captionText.value
+        );
+
+        let { method, object, extra = {}, value = null } = telemetryEvent;
+        Services.telemetry.recordEvent("pwmgr", method, object, value, extra);
+
+        if (!isAuthorized) {
+          return;
+        }
+
         let fp = Cc["@mozilla.org/filepicker;1"].createInstance(
           Ci.nsIFilePicker
         );
         let fpCallback = function fpCallback_done(aResult) {
           if (aResult != Ci.nsIFilePicker.returnCancel) {
             LoginExport.exportAsCSV(fp.file.path);
             Services.telemetry.recordEvent(
               "pwmgr",
--- a/browser/components/aboutlogins/content/aboutLogins.html
+++ b/browser/components/aboutlogins/content/aboutLogins.html
@@ -266,17 +266,17 @@
 
     <template id="menu-button-template">
       <link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/menu-button.css">
       <button class="menu-button ghost-button" data-l10n-id="menu"></button>
       <ul class="menu" role="menu" hidden>
         <button role="menuitem" class="menuitem-button menuitem-import ghost-button" hidden data-supported-platforms="Win32,MacIntel" data-event-name="AboutLoginsImport" data-l10n-id="about-logins-menu-menuitem-import-from-another-browser"></button>
-        <button role="menuitem" class="menuitem-button menuitem-export ghost-button" data-event-name="AboutLoginsExportPasswordsDialog" data-l10n-id="about-logins-menu-menuitem-export-logins" hidden></button>
+        <button role="menuitem" class="menuitem-button menuitem-export ghost-button" data-event-name="AboutLoginsExportPasswordsDialog" data-l10n-id="about-logins-menu-menuitem-export-logins"></button>
         <hr role="separator" class="menuitem-separator"></hr>
         <button role="menuitem" class="menuitem-button menuitem-preferences ghost-button" data-event-name="AboutLoginsOpenPreferences" data-l10n-id="menu-menuitem-preferences"></button>
         <hr role="separator" class="menuitem-separator"></hr>
         <button role="menuitem" class="menuitem-button menuitem-help ghost-button" data-event-name="AboutLoginsGetHelp" data-l10n-id="about-logins-menu-menuitem-help"></button>
         <hr role="separator" class="menuitem-separator"></hr>
         <button role="menuitem" class="menuitem-button menuitem-mobile menuitem-mobile-android ghost-button" data-event-name="AboutLoginsOpenMobileAndroid" data-l10n-id="menu-menuitem-android-app"></button>
         <button role="menuitem" class="menuitem-button menuitem-mobile menuitem-mobile-ios ghost-button" data-event-name="AboutLoginsOpenMobileIos" data-l10n-id="menu-menuitem-iphone-app"></button>
       </ul>
--- a/browser/components/aboutlogins/content/aboutLoginsUtils.js
+++ b/browser/components/aboutlogins/content/aboutLoginsUtils.js
@@ -45,8 +45,14 @@ export function setKeyboardAccessForNonD
     } else if (el.dataset.oldTabIndex) {
       el.tabIndex = el.dataset.oldTabIndex;
       delete el.dataset.oldTabIndex;
     } else {
       el.removeAttribute("tabindex");
     }
   });
 }
+
+export function promptForMasterPassword(messageId) {
+  return new Promise(resolve => {
+    window.AboutLoginsUtils.promptForMasterPassword(resolve, messageId);
+  });
+}
--- a/browser/components/aboutlogins/content/components/login-item.js
+++ b/browser/components/aboutlogins/content/components/login-item.js
@@ -1,13 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-import { recordTelemetryEvent } from "../aboutLoginsUtils.js";
+import {
+  recordTelemetryEvent,
+  promptForMasterPassword,
+} from "../aboutLoginsUtils.js";
 
 export default class LoginItem extends HTMLElement {
   /**
    * The number of milliseconds to display the "Copied" success message
    * before reverting to the normal "Copy" button.
    */
   static get COPY_BUTTON_RESET_TIMEOUT() {
     return 5000;
@@ -344,22 +347,19 @@ export default class LoginItem extends H
         }
         break;
       }
       case "click": {
         let classList = event.currentTarget.classList;
         if (classList.contains("reveal-password-checkbox")) {
           // We prompt for the master password when entering edit mode already.
           if (this._revealCheckbox.checked && !this.dataset.editing) {
-            let masterPasswordAuth = await new Promise(resolve => {
-              window.AboutLoginsUtils.promptForMasterPassword(
-                resolve,
-                "about-logins-reveal-password-os-auth-dialog-message"
-              );
-            });
+            let masterPasswordAuth = await promptForMasterPassword(
+              "about-logins-reveal-password-os-auth-dialog-message"
+            );
             if (!masterPasswordAuth) {
               this._revealCheckbox.checked = false;
               return;
             }
           }
           this._updatePasswordRevealState();
 
           let method = this._revealCheckbox.checked ? "show" : "hide";
@@ -397,22 +397,19 @@ export default class LoginItem extends H
           classList.contains("copy-username-button")
         ) {
           let copyButton = event.currentTarget;
           let otherCopyButton =
             copyButton == this._copyPasswordButton
               ? this._copyUsernameButton
               : this._copyPasswordButton;
           if (copyButton.dataset.copyLoginProperty == "password") {
-            let masterPasswordAuth = await new Promise(resolve => {
-              window.AboutLoginsUtils.promptForMasterPassword(
-                resolve,
-                "about-logins-copy-password-os-auth-dialog-message"
-              );
-            });
+            let masterPasswordAuth = await promptForMasterPassword(
+              "about-logins-copy-password-os-auth-dialog-message"
+            );
             if (!masterPasswordAuth) {
               return;
             }
           }
 
           copyButton.disabled = true;
           copyButton.dataset.copied = true;
           let propertyToCopy = this._login[
@@ -451,22 +448,19 @@ export default class LoginItem extends H
                 bubbles: true,
                 detail: this._login,
               })
             );
           });
           return;
         }
         if (classList.contains("edit-button")) {
-          let masterPasswordAuth = await new Promise(resolve => {
-            window.AboutLoginsUtils.promptForMasterPassword(
-              resolve,
-              "about-logins-edit-login-os-auth-dialog-message"
-            );
-          });
+          let masterPasswordAuth = await promptForMasterPassword(
+            "about-logins-edit-login-os-auth-dialog-message"
+          );
           if (!masterPasswordAuth) {
             return;
           }
 
           this._toggleEditing();
           this.render();
 
           this._recordTelemetryEvent({
--- a/browser/components/aboutlogins/tests/browser/browser_openExport.js
+++ b/browser/components/aboutlogins/tests/browser/browser_openExport.js
@@ -3,16 +3,19 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 /**
  * Test the export logins file picker appears.
  */
 
+let { OSKeyStore } = ChromeUtils.import(
+  "resource://gre/modules/OSKeyStore.jsm"
+);
 let { TelemetryTestUtils } = ChromeUtils.import(
   "resource://testing-common/TelemetryTestUtils.jsm"
 );
 
 let { MockFilePicker } = SpecialPowers;
 
 add_task(async function setup() {
   await TestUtils.waitForCondition(() => {
@@ -60,18 +63,16 @@ add_task(async function test_open_export
         }, "waiting for menu to open");
       });
 
       function getExportMenuItem() {
         let menuButton = window.document.querySelector("menu-button");
         let exportButton = menuButton.shadowRoot.querySelector(
           ".menuitem-export"
         );
-        // Force the menu item to be visible for the test.
-        exportButton.hidden = false;
         return exportButton;
       }
 
       await BrowserTestUtils.synthesizeMouseAtCenter(
         getExportMenuItem,
         {},
         browser
       );
@@ -80,36 +81,70 @@ add_task(async function test_open_export
       await LoginTestUtils.telemetry.waitForEventCount(2);
       TelemetryTestUtils.assertEvents(
         [["pwmgr", "mgmt_menu_item_used", "export"]],
         { category: "pwmgr", method: "mgmt_menu_item_used" },
         { process: "content" }
       );
 
       info("Clicking confirm button");
+      let osReAuthPromise = null;
+
+      if (
+        OSKeyStore.canReauth() &&
+        !OSKeyStoreTestUtils.canTestOSKeyStoreLogin()
+      ) {
+        todo(
+          OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+          "Cannot test OS key store login in this build."
+        );
+        return;
+      }
+
+      if (OSKeyStore.canReauth()) {
+        osReAuthPromise = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+      }
       let filePicker = waitForFilePicker();
       await BrowserTestUtils.synthesizeMouseAtCenter(
         () => {
           let confirmExportDialog = window.document.querySelector(
             "confirmation-dialog"
           );
           return confirmExportDialog.shadowRoot.querySelector(
             ".confirm-button"
           );
         },
         {},
         browser
       );
 
+      if (osReAuthPromise) {
+        ok(osReAuthPromise, "Waiting for OS re-auth promise");
+        await osReAuthPromise;
+      }
+
       info("waiting for Export file picker to get opened");
       await filePicker;
       ok(true, "Export file picker opened");
 
       info("Waiting for the export to complete");
-      await LoginTestUtils.telemetry.waitForEventCount(1, "parent");
+      let expectedEvents = [
+        [
+          "pwmgr",
+          "reauthenticate",
+          "os_auth",
+          osReAuthPromise ? "success" : "success_unsupported_platform",
+        ],
+        ["pwmgr", "mgmt_menu_item_used", "export_complete"],
+      ];
+      await LoginTestUtils.telemetry.waitForEventCount(
+        expectedEvents.length,
+        "parent"
+      );
+
       TelemetryTestUtils.assertEvents(
-        [["pwmgr", "mgmt_menu_item_used", "export_complete"]],
-        { category: "pwmgr", method: "mgmt_menu_item_used" },
+        expectedEvents,
+        { category: "pwmgr", method: /(reauthenticate|mgmt_menu_item_used)/ },
         { process: "parent" }
       );
     }
   );
 });
--- a/browser/components/aboutlogins/tests/chrome/test_menu_button.html
+++ b/browser/components/aboutlogins/tests/chrome/test_menu_button.html
@@ -68,16 +68,17 @@ add_task(async function test_menu_open_c
   synthesizeKey("VK_TAB", { shiftKey: true });
   await SimpleTest.promiseWaitForCondition(() => !firstVisibleItem.matches(":focus"),
     "waiting for firstVisibleItem to lose focus");
   ok(!firstVisibleItem.matches(":focus"), "firstVisibleItem should lose focus after tabbing away from it");
   sendKey("TAB");
   await SimpleTest.promiseWaitForCondition(() => firstVisibleItem.matches(":focus"),
     "waiting for firstVisibleItem to get focus again");
   ok(firstVisibleItem.matches(":focus"), "firstVisibleItem should be focused after tabbing to it again");
+  sendKey("TAB"); // Export
 
   if (navigator.platform == "Win32" || navigator.platform == "MacIntel") {
     // The Import menuitem is only visible on Windows/macOS, where we will need another Tab
     // press to get to the Preferences item.
     let preferencesItem = gMenuButton.shadowRoot.querySelector(".menuitem-preferences");
     sendKey("DOWN");
     await SimpleTest.promiseWaitForCondition(() => preferencesItem.matches(":focus"),
       "waiting for preferencesItem to gain focus");
@@ -117,16 +118,17 @@ add_task(async function test_menu_keyboa
     );
   }
 
   function getFocusedMenuItem() {
     return gMenuButton.shadowRoot.querySelector(".menuitem-button:focus");
   }
 
   let allItems = [
+    "menuitem-export",
     "menuitem-preferences",
     "menuitem-help",
     "menuitem-mobile-android",
     "menuitem-mobile-ios",
   ];
   if (navigator.platform == "Win32" || navigator.platform == "MacIntel") {
     allItems = ["menuitem-import", ...allItems];
   }
--- a/browser/locales/en-US/browser/aboutLogins.ftl
+++ b/browser/locales/en-US/browser/aboutLogins.ftl
@@ -123,16 +123,22 @@ about-logins-reveal-password-os-auth-dia
 about-logins-reveal-password-os-auth-dialog-message-macosx = reveal the saved password
 
 # This message can be seen when attempting to copy a password in about:logins on Windows.
 about-logins-copy-password-os-auth-dialog-message-win = To copy your password, enter your Windows login credentials. This helps protect the security of your accounts.
 # This message can be seen when attempting to copy a password in about:logins
 # On MacOS, only provide the reason that account verification is needed. Do not put a complete sentence here.
 about-logins-copy-password-os-auth-dialog-message-macosx = copy the saved password
 
+# This message can be seen when attempting to export a password in about:logins on Windows.
+about-logins-export-password-os-auth-dialog-message-win = To export your logins, enter your Windows login credentials. This helps protect the security of your accounts.
+# This message can be seen when attempting to export a password in about:logins
+# On MacOS, only provide the reason that account verification is needed. Do not put a complete sentence here.
+about-logins-export-password-os-auth-dialog-message-macosx = export saved logins and passwords
+
 ## Master Password notification
 
 master-password-notification-message = Please enter your master password to view saved logins & passwords
 master-password-reload-button =
   .label = Log in
   .accesskey = L
 
 ## Password Sync notification
--- a/toolkit/components/telemetry/Events.yaml
+++ b/toolkit/components/telemetry/Events.yaml
@@ -658,17 +658,17 @@ pwmgr:
       - 1623745
       - 1636729
       - 1642267
     expiry_version: never
     notification_emails: ["loines@mozilla.com", "passwords-dev@mozilla.org", "jaws@mozilla.com"]
     release_channel_collection: opt-out
     products:
       - "firefox"
-    record_in_processes: [content]
+    record_in_processes: [main, content]
   mgmt_interaction:
     description: >
       These events record interactions on the about:logins page.
     extra_keys:
       breached: >
         Whether the login is marked as breached or not. If a login is both breached and vulnerable, it will only be reported as breached.
       vulnerable: >
         Whether the login is marked as vulnerable or not. If a login is both breached and vulnerable, it will only be reported as breached.