Bug 1426721 - Add async/bulk encryption interface to SDR r?keeler
Since encryption can be somewhat CPU intensive, if we're encrypting
a large number of strings we want to be able to do so in a background
thread. This will be consumed by the profile migrators when importing
logins.
MozReview-Commit-ID: JoJGOgMzZ4u
--- a/security/manager/ssl/SecretDecoderRing.cpp
+++ b/security/manager/ssl/SecretDecoderRing.cpp
@@ -5,33 +5,66 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "SecretDecoderRing.h"
#include "ScopedNSSTypes.h"
#include "mozilla/Base64.h"
#include "mozilla/Casting.h"
#include "mozilla/Services.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/Promise.h"
#include "nsCOMPtr.h"
#include "nsIInterfaceRequestor.h"
#include "nsIInterfaceRequestorUtils.h"
#include "nsIObserverService.h"
#include "nsIServiceManager.h"
#include "nsITokenPasswordDialogs.h"
#include "nsNSSComponent.h"
#include "nsNSSHelper.h"
#include "pk11func.h"
#include "pk11sdr.h" // For PK11SDR_Encrypt, PK11SDR_Decrypt
#include "ssl.h" // For SSL_ClearSessionCache
using namespace mozilla;
+using dom::Promise;
-// NOTE: Should these be the thread-safe versions?
NS_IMPL_ISUPPORTS(SecretDecoderRing, nsISecretDecoderRing)
+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];
+ nsCString cipherText;
+ rv = sdrService->EncryptString(plaintext, cipherText);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ break;
+ }
+
+ cipherTexts.AppendElement(NS_ConvertASCIItoUTF16(cipherText));
+ }
+
+ nsCOMPtr<nsIRunnable> runnable(
+ NS_NewRunnableFunction("BackgroundSdrEncryptStringsResolve",
+ [rv, aPromise = Move(aPromise), cipherTexts = Move(cipherTexts)]() {
+ if (NS_FAILED(rv)) {
+ aPromise->MaybeReject(rv);
+ } else {
+ aPromise->MaybeResolve(cipherTexts);
+ }
+ }));
+ NS_DispatchToMainThread(runnable);
+}
+
SecretDecoderRing::SecretDecoderRing()
{
}
SecretDecoderRing::~SecretDecoderRing()
{
nsNSSShutDownPreventionLock locker;
if (isAlreadyShutDown()) {
@@ -128,16 +161,61 @@ SecretDecoderRing::EncryptString(const n
if (NS_FAILED(rv)) {
return rv;
}
return NS_OK;
}
NS_IMETHODIMP
+SecretDecoderRing::AsyncEncryptStrings(uint32_t plaintextsCount,
+ const char16_t** plaintexts,
+ JSContext* aCx,
+ nsISupports** aPromise) {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ NS_ENSURE_ARG(plaintextsCount);
+ NS_ENSURE_ARG_POINTER(plaintexts);
+ NS_ENSURE_ARG_POINTER(aCx);
+
+ nsIGlobalObject* globalObject =
+ xpc::NativeGlobal(JS::CurrentGlobalOrNull(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();
+ }
+
+ InfallibleTArray<nsCString> plaintextsUtf8(plaintextsCount);
+ for (uint32_t i = 0; i < plaintextsCount; ++i) {
+ plaintextsUtf8.AppendElement(NS_ConvertUTF16toUTF8(plaintexts[i]));
+ }
+ nsCOMPtr<nsIRunnable> runnable(
+ NS_NewRunnableFunction("BackgroundSdrEncryptStrings",
+ [promise, plaintextsUtf8 = Move(plaintextsUtf8)]() mutable {
+ BackgroundSdrEncryptStrings(plaintextsUtf8, promise);
+ }));
+
+ nsCOMPtr<nsIThread> encryptionThread;
+ nsresult rv = NS_NewNamedThread("AsyncSDRThread",
+ getter_AddRefs(encryptionThread),
+ runnable);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
SecretDecoderRing::DecryptString(const nsACString& encryptedBase64Text,
/*out*/ nsACString& decryptedText)
{
nsAutoCString encryptedText;
nsresult rv = Base64Decode(encryptedBase64Text, encryptedText);
if (NS_FAILED(rv)) {
return rv;
}
--- a/security/manager/ssl/SecretDecoderRing.h
+++ b/security/manager/ssl/SecretDecoderRing.h
@@ -15,17 +15,17 @@
#define NS_SECRETDECODERRING_CID \
{ 0x0c4f1ddc, 0x1dd2, 0x11b2, { 0x9d, 0x95, 0xf2, 0xfd, 0xf1, 0x13, 0x04, 0x4b } }
class SecretDecoderRing : public nsISecretDecoderRing
, public nsNSSShutDownObject
{
public:
- NS_DECL_ISUPPORTS
+ NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSISECRETDECODERRING
SecretDecoderRing();
// Nothing to release.
virtual void virtualDestroyNSSReference() override {}
protected:
--- a/security/manager/ssl/nsISecretDecoderRing.idl
+++ b/security/manager/ssl/nsISecretDecoderRing.idl
@@ -18,16 +18,30 @@ interface nsISecretDecoderRing: nsISuppo
*
* @param text The text to encrypt.
* @return The encrypted text, encoded as Base64.
*/
[must_use]
ACString encryptString(in ACString text);
/**
+ * Run encryptString on multiple strings, asynchronously. This will allow you
+ * to not jank the browser if you need to encrypt a large number of strings
+ * all at once. This method accepts an array of wstrings which it will convert
+ * to UTF-8 internally before encrypting.
+ *
+ * @param plaintextsCount the number of strings to encrypt.
+ * @param plaintexts the strings to encrypt.
+ * @return A promise for the list of encrypted strings, encoded as Base64.
+ */
+ [implicit_jscontext, must_use]
+ nsISupports asyncEncryptStrings(in unsigned long plaintextsCount,
+ [array, size_is(plaintextsCount)] in wstring plaintexts);
+
+ /**
* Decrypt Base64 input.
* See the encryptString() documentation - this method has basically the same
* limitations.
*
* @param encryptedBase64Text Encrypted input text, encoded as Base64.
* @return The decoded text.
*/
[must_use]
--- a/security/manager/ssl/tests/unit/test_sdr.js
+++ b/security/manager/ssl/tests/unit/test_sdr.js
@@ -16,17 +16,17 @@ const gTokenPasswordDialogs = {
info(`setPassword() called; shown ${gSetPasswordShownCount} times`);
info(`tokenName: ${tokenName}`);
return false; // Returning false means "the user didn't cancel".
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsITokenPasswordDialogs])
};
-function run_test() {
+add_task(function testEncryptString() {
let sdr = Cc["@mozilla.org/security/sdr;1"]
.getService(Ci.nsISecretDecoderRing);
// Test valid inputs for encryptString() and decryptString().
let inputs = [
"",
" ", // First printable latin1 character (code point 32).
"foo",
@@ -73,9 +73,47 @@ function run_test() {
});
equal(gSetPasswordShownCount, 0,
"changePassword() dialog should have been shown zero times");
sdr.changePassword();
equal(gSetPasswordShownCount, 1,
"changePassword() dialog should have been shown exactly once");
}
-}
+});
+
+add_task(async function testAsyncEncryptStrings() {
+ let sdr = Cc["@mozilla.org/security/sdr;1"]
+ .getService(Ci.nsISecretDecoderRing);
+
+ // Test valid inputs for encryptString() and decryptString().
+ let inputs = [
+ "",
+ " ", // 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 encrypteds = await sdr.asyncEncryptStrings(inputs.length, inputs);
+ for (let i = 0; i < inputs.length; i++) {
+ let encrypted = encrypteds[i];
+ let input = inputs[i];
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+
+ let convertedInput = converter.ConvertFromUnicode(input);
+ convertedInput += converter.Finish();
+ notEqual(convertedInput, encrypted,
+ "Encrypted input should not just be the input itself");
+
+ try {
+ atob(encrypted);
+ } catch (e) {
+ ok(false, `encryptString() should have returned Base64: ${e}`);
+ }
+
+ equal(convertedInput, sdr.decryptString(encrypted),
+ "decryptString(encryptString(input)) should return input");
+ }
+});
--- a/toolkit/components/passwordmgr/crypto-SDR.js
+++ b/toolkit/components/passwordmgr/crypto-SDR.js
@@ -95,16 +95,61 @@ LoginManagerCrypto_SDR.prototype = {
else if (canceledMP)
this._notifyObservers("passwordmgr-crypto-loginCanceled");
}
return cipherText;
},
/*
+ * encryptMany
+ *
+ * Encrypts the specified strings, using the SecretDecoderRing.
+ *
+ * Returns a promise which resolves with the the encrypted strings,
+ * or throws/rejects with an error if there was a problem.
+ */
+ async encryptMany(plaintexts) {
+ if (!Array.isArray(plaintexts) || !plaintexts.length) {
+ throw Components.Exception("Need at least one plaintext to encrypt",
+ Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ let cipherTexts;
+
+ let wasLoggedIn = this.isLoggedIn;
+ let canceledMP = false;
+
+ this._uiBusy = true;
+ try {
+ cipherTexts = await this._decoderRing.asyncEncryptStrings(plaintexts.length, plaintexts);
+ } catch (e) {
+ this.log("Failed to encrypt strings. (" + e.name + ")");
+ // If the user clicks Cancel, we get NS_ERROR_FAILURE.
+ // (unlike decrypting, which gets NS_ERROR_NOT_AVAILABLE).
+ if (e.result == Cr.NS_ERROR_FAILURE) {
+ canceledMP = true;
+ throw Components.Exception("User canceled master password entry", Cr.NS_ERROR_ABORT);
+ } else {
+ throw Components.Exception("Couldn't encrypt strings", 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 cipherTexts;
+ },
+
+
+ /*
* decrypt
*
* Decrypts the specified string, using the SecretDecoderRing.
*
* Returns the decrypted string, or throws an exception if there was a
* problem.
*/
decrypt(cipherText) {
--- a/toolkit/components/passwordmgr/nsILoginManagerCrypto.idl
+++ b/toolkit/components/passwordmgr/nsILoginManagerCrypto.idl
@@ -23,16 +23,27 @@ interface nsILoginManagerCrypto : nsISup
* NOTE: The current implemention of this inferface simply uses NSS/PSM's
* "Secret Decoder Ring" service. It is not recommended for general
* purpose encryption/decryption.
*
* Can throw if the user cancels entry of their master password.
*/
AString encrypt(in AString plainText);
+ /*
+ * encryptMany
+ *
+ * @param plainTexts
+ * The strings to be encrypted.
+ *
+ * Encrypts the specified strings, similar to encrypt, but returning a promise
+ * which resolves with the the encrypted strings.
+ */
+ jsval encryptMany(in jsval plainTexts);
+
/**
* decrypt
*
* @param cipherText
* The string to be decrypted.
*
* Decrypts the specified string, returning the plaintext value.
*