Bug 1560447 - Add a decryptMany method to crypto-SDR.js for batch decrypting of stored logins. r=keeler
authorJared Wein <jwein@mozilla.com>
Fri, 28 Jun 2019 16:53:11 +0000
changeset 543409 0c2f7d75146b50a03200cc29182ee60905b6e527
parent 543408 496d94e9449892fbed4f6e727b3f438dad9cc795
child 543410 e0311539d0fba60067acf35eef23eba60b26a669
push id2131
push userffxbld-merge
push dateMon, 26 Aug 2019 18:30:20 +0000
treeherdermozilla-release@b19ffb3ca153 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskeeler
bugs1560447
milestone69.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 1560447 - Add a decryptMany method to crypto-SDR.js for batch decrypting of stored logins. r=keeler Differential Revision: https://phabricator.services.mozilla.com/D35879
security/manager/ssl/SecretDecoderRing.cpp
security/manager/ssl/nsISecretDecoderRing.idl
security/manager/ssl/tests/unit/test_sdr.js
toolkit/components/passwordmgr/crypto-SDR.js
toolkit/components/passwordmgr/nsILoginManagerCrypto.idl
--- a/security/manager/ssl/SecretDecoderRing.cpp
+++ b/security/manager/ssl/SecretDecoderRing.cpp
@@ -33,18 +33,17 @@ NS_IMPL_ISUPPORTS(SecretDecoderRing, nsI
 
 void BackgroundSdrEncryptStrings(const nsTArray<nsCString>& plaintexts,
                                  RefPtr<Promise>& aPromise) {
   nsCOMPtr<nsISecretDecoderRing> sdrService =
       do_GetService(NS_SECRETDECODERRING_CONTRACTID);
   InfallibleTArray<nsString> cipherTexts(plaintexts.Length());
 
   nsresult rv = NS_ERROR_FAILURE;
-  for (uint32_t i = 0; i < plaintexts.Length(); ++i) {
-    const nsCString& plaintext = plaintexts[i];
+  for (const auto& plaintext : plaintexts) {
     nsCString cipherText;
     rv = sdrService->EncryptString(plaintext, cipherText);
 
     if (NS_WARN_IF(NS_FAILED(rv))) {
       break;
     }
 
     cipherTexts.AppendElement(NS_ConvertASCIItoUTF16(cipherText));
@@ -58,16 +57,47 @@ void BackgroundSdrEncryptStrings(const n
                                  aPromise->MaybeReject(rv);
                                } else {
                                  aPromise->MaybeResolve(cipherTexts);
                                }
                              }));
   NS_DispatchToMainThread(runnable.forget());
 }
 
+void BackgroundSdrDecryptStrings(const nsTArray<nsCString>& encryptedStrings,
+                                 RefPtr<Promise>& aPromise) {
+  nsCOMPtr<nsISecretDecoderRing> sdrService =
+      do_GetService(NS_SECRETDECODERRING_CONTRACTID);
+  InfallibleTArray<nsString> plainTexts(encryptedStrings.Length());
+
+  nsresult rv = NS_ERROR_FAILURE;
+  for (const auto& encryptedString : encryptedStrings) {
+    nsCString plainText;
+    rv = sdrService->DecryptString(encryptedString, plainText);
+
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      break;
+    }
+
+    plainTexts.AppendElement(NS_ConvertASCIItoUTF16(plainText));
+  }
+
+  nsCOMPtr<nsIRunnable> runnable(
+      NS_NewRunnableFunction("BackgroundSdrDecryptStringsResolve",
+                             [rv, aPromise = std::move(aPromise),
+                              plainTexts = std::move(plainTexts)]() {
+                               if (NS_FAILED(rv)) {
+                                 aPromise->MaybeReject(rv);
+                               } else {
+                                 aPromise->MaybeResolve(plainTexts);
+                               }
+                             }));
+  NS_DispatchToMainThread(runnable.forget());
+}
+
 nsresult SecretDecoderRing::Encrypt(const nsACString& data,
                                     /*out*/ nsACString& result) {
   UniquePK11SlotInfo slot(PK11_GetInternalKeySlot());
   if (!slot) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   /* Make sure token is initialized. */
@@ -142,16 +172,17 @@ SecretDecoderRing::EncryptString(const n
 }
 
 NS_IMETHODIMP
 SecretDecoderRing::AsyncEncryptStrings(const nsTArray<nsCString>& plaintexts,
                                        JSContext* aCx, Promise** aPromise) {
   MOZ_RELEASE_ASSERT(NS_IsMainThread());
   NS_ENSURE_ARG(!plaintexts.IsEmpty());
   NS_ENSURE_ARG_POINTER(aCx);
+  NS_ENSURE_ARG_POINTER(aPromise);
 
   nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
   if (NS_WARN_IF(!globalObject)) {
     return NS_ERROR_UNEXPECTED;
   }
 
   ErrorResult result;
   RefPtr<Promise> promise = Promise::Create(globalObject, result);
@@ -192,16 +223,56 @@ SecretDecoderRing::DecryptString(const n
   if (NS_FAILED(rv)) {
     return rv;
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
+SecretDecoderRing::AsyncDecryptStrings(
+    const nsTArray<nsCString>& encryptedStrings, JSContext* aCx,
+    Promise** aPromise) {
+  MOZ_RELEASE_ASSERT(NS_IsMainThread());
+  NS_ENSURE_ARG(!encryptedStrings.IsEmpty());
+  NS_ENSURE_ARG_POINTER(aCx);
+  NS_ENSURE_ARG_POINTER(aPromise);
+
+  nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
+  if (NS_WARN_IF(!globalObject)) {
+    return NS_ERROR_UNEXPECTED;
+  }
+
+  ErrorResult result;
+  RefPtr<Promise> promise = Promise::Create(globalObject, result);
+  if (NS_WARN_IF(result.Failed())) {
+    return result.StealNSResult();
+  }
+
+  // encryptedStrings are expected to be base64.
+  nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction(
+      "BackgroundSdrDecryptStrings", [promise, encryptedStrings]() mutable {
+        BackgroundSdrDecryptStrings(encryptedStrings, promise);
+      }));
+
+  nsCOMPtr<nsIEventTarget> target(
+      do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID));
+  if (!target) {
+    return NS_ERROR_FAILURE;
+  }
+  nsresult rv = target->Dispatch(runnable, NS_DISPATCH_NORMAL);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  promise.forget(aPromise);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 SecretDecoderRing::ChangePassword() {
   UniquePK11SlotInfo slot(PK11_GetInternalKeySlot());
   if (!slot) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   // nsPK11Token::nsPK11Token takes its own reference to slot, so we pass a
   // non-owning pointer here.
--- a/security/manager/ssl/nsISecretDecoderRing.idl
+++ b/security/manager/ssl/nsISecretDecoderRing.idl
@@ -41,16 +41,27 @@ interface nsISecretDecoderRing: nsISuppo
    *
    * @param encryptedBase64Text Encrypted input text, encoded as Base64.
    * @return The decoded text.
    */
   [must_use]
   ACString decryptString(in ACString encryptedBase64Text);
 
   /**
+   * Run decryptString on multiple strings, asynchronously. This will allow you
+   * to not jank the browser if you need to decrypt a large number of strings
+   * all at once.
+   *
+   * @param encryptedStrings the strings to decrypt, encoded as Base64.
+   * @return A promise that resolves with the list of decrypted strings in Unicode.
+   */
+  [implicit_jscontext, must_use]
+  Promise asyncDecryptStrings(in Array<ACString> encryptedStrings);
+
+  /**
    * Prompt the user to change the password on the SDR key.
    */
   [must_use]
   void changePassword();
 
   /**
    * Logout of the security device that protects the SDR key.
    */
--- a/security/manager/ssl/tests/unit/test_sdr.js
+++ b/security/manager/ssl/tests/unit/test_sdr.js
@@ -112,8 +112,45 @@ add_task(async function testAsyncEncrypt
     } catch (e) {
       ok(false, `encryptString() should have returned Base64: ${e}`);
     }
 
     equal(convertedInput, sdr.decryptString(encrypted),
           "decryptString(encryptString(input)) should return input");
   }
 });
+
+add_task(async function testAsyncDecryptStrings() {
+  let sdr = Cc["@mozilla.org/security/sdr;1"]
+              .getService(Ci.nsISecretDecoderRing);
+
+  // Test valid inputs for encryptString() and decryptString().
+  let testCases = [
+    "",
+    " ", // First printable latin1 character (code point 32).
+    "foo",
+    "1234567890`~!@#$%^&*()-_=+{[}]|\\:;'\",<.>/?",
+    "¡äöüÿ", // Misc + last printable latin1 character (code point 255).
+    "aaa 一二三", // Includes Unicode with code points outside [0, 255].
+  ];
+
+  let convertedTestCases = testCases.map(tc => {
+    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+      .createInstance(Ci.nsIScriptableUnicodeConverter);
+    converter.charset = "UTF-8";
+
+    let convertedInput = converter.ConvertFromUnicode(tc);
+    convertedInput += converter.Finish();
+    return convertedInput;
+  });
+
+  let encryptedStrings = convertedTestCases.map(tc => sdr.encryptString(tc));
+  let decrypteds = await sdr.asyncDecryptStrings(encryptedStrings);
+  for (let i = 0; i < encryptedStrings.length; i++) {
+    let decrypted = decrypteds[i];
+    let expectedDecryptedString = convertedTestCases[i];
+
+    equal(decrypted, expectedDecryptedString,
+          "decrypted string should match expected value");
+    equal(sdr.decryptString(encryptedStrings[i]), decrypted,
+          "decryptString(encryptString(input)) should return the initial decrypted string value");
+  }
+});
--- a/toolkit/components/passwordmgr/crypto-SDR.js
+++ b/toolkit/components/passwordmgr/crypto-SDR.js
@@ -188,16 +188,56 @@ LoginManagerCrypto_SDR.prototype = {
       } else if (canceledMP) {
         this._notifyObservers("passwordmgr-crypto-loginCanceled");
       }
     }
 
     return plainText;
   },
 
+  /*
+   * Decrypts the specified strings, using the SecretDecoderRing.
+   *
+   * Returns a promise which resolves with the the decrypted strings,
+   * or throws/rejects with an error if there was a problem.
+   */
+  async decryptMany(cipherTexts) {
+    if (!Array.isArray(cipherTexts) || !cipherTexts.length) {
+      throw Components.Exception("Need at least one ciphertext to decrypt",
+                                 Cr.NS_ERROR_INVALID_ARG);
+    }
+
+    let plainTexts = [];
+
+    let wasLoggedIn = this.isLoggedIn;
+    let canceledMP = false;
+
+    this._uiBusy = true;
+    try {
+      plainTexts = await this._decoderRing.asyncDecryptStrings(cipherTexts);
+    } catch (e) {
+      this.log("Failed to decrypt strings. (" + e.name + ")");
+      // If the user clicks Cancel, we get NS_ERROR_NOT_AVAILABLE.
+      if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
+        canceledMP = true;
+        throw Components.Exception("User canceled master password entry", Cr.NS_ERROR_ABORT);
+      } else {
+        throw Components.Exception("Couldn't decrypt strings: " + e.result, Cr.NS_ERROR_FAILURE);
+      }
+    } finally {
+      this._uiBusy = false;
+      // If we triggered a master password prompt, notify observers.
+      if (!wasLoggedIn && this.isLoggedIn) {
+        this._notifyObservers("passwordmgr-crypto-login");
+      } else if (canceledMP) {
+        this._notifyObservers("passwordmgr-crypto-loginCanceled");
+      }
+    }
+    return plainTexts;
+  },
 
   /*
    * uiBusy
    */
   get uiBusy() {
     return this._uiBusy;
   },
 
--- a/toolkit/components/passwordmgr/nsILoginManagerCrypto.idl
+++ b/toolkit/components/passwordmgr/nsILoginManagerCrypto.idl
@@ -48,16 +48,28 @@ interface nsILoginManagerCrypto : nsISup
    *
    * Can throw if the user cancels entry of their master password, or if the
    * cipherText value can not be successfully decrypted (eg, if it was
    * encrypted with some other key).
    */
   AString decrypt(in AString cipherText);
 
   /**
+   * @param cipherTexts
+   *        The strings to be decrypted.
+   *
+   * Decrypts the specified strings, returning the plaintext values.
+   *
+   * Can throw if the user cancels entry of their master password, or if the
+   * cipherText value can not be successfully decrypted (eg, if it was
+   * encrypted with some other key).
+   */
+  jsval decryptMany(in jsval cipherTexts);
+
+  /**
    * uiBusy
    *
    * True when a master password prompt is being displayed.
    */
   readonly attribute boolean uiBusy;
 
   /**
    * isLoggedIn