mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-15 14:25:52 +00:00
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:
parent
4615d385bd
commit
ac49bf6b85
@ -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)
|
||||
|
@ -20,7 +20,7 @@ class SecretDecoderRing : public nsISecretDecoderRing
|
||||
, public nsNSSShutDownObject
|
||||
{
|
||||
public:
|
||||
NS_DECL_ISUPPORTS
|
||||
NS_DECL_THREADSAFE_ISUPPORTS
|
||||
NS_DECL_NSISECRETDECODERRING
|
||||
|
||||
SecretDecoderRing();
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
}
|
||||
});
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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
|
||||
*
|
||||
|
Loading…
Reference in New Issue
Block a user