Bug 1426721 - Add async/bulk encryption interface to SDR r=keeler,MattN

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

--HG--
extra : rebase_source : 4677482b4e9b1df7c7ca70a0e817204ef6638cdf
This commit is contained in:
Doug Thayer 2018-01-23 10:06:31 -08:00
parent 4615d385bd
commit ac49bf6b85
6 changed files with 190 additions and 4 deletions

View File

@ -10,6 +10,8 @@
#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"
@ -23,10 +25,41 @@
#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()
{
}
@ -132,6 +165,51 @@ SecretDecoderRing::EncryptString(const nsACString& text,
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)

View File

@ -20,7 +20,7 @@ class SecretDecoderRing : public nsISecretDecoderRing
, public nsNSSShutDownObject
{
public:
NS_DECL_ISUPPORTS
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSISECRETDECODERRING
SecretDecoderRing();

View File

@ -22,6 +22,20 @@ interface nsISecretDecoderRing: nsISupports {
[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

View File

@ -21,7 +21,7 @@ const gTokenPasswordDialogs = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsITokenPasswordDialogs])
};
function run_test() {
add_task(function testEncryptString() {
let sdr = Cc["@mozilla.org/security/sdr;1"]
.getService(Ci.nsISecretDecoderRing);
@ -78,4 +78,42 @@ function run_test() {
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");
}
});

View File

@ -99,6 +99,51 @@ LoginManagerCrypto_SDR.prototype = {
},
/*
* 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
*

View File

@ -28,6 +28,17 @@ interface nsILoginManagerCrypto : nsISupports {
*/
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
*