Backported from enterprise-firefox 16d9ea9e4e79. Threads an explicit key_size through keystore_create_dek (LockstoreService, lockstore_ffi, keystore.rs, nsILockstore.idl); SQLiteEncryption passes kDekBytes and static_asserts kDekBytes == IPCStreamCipherStrategy::KeyType size. Also updates the lockstore gtest call sites (TestLockstoreKeystore.cpp, TestLockstoreDatastore.cpp, TestLockstoreService.cpp) to the new 5-arg arity; the enterprise commit updated only TestLockstoreService.cpp, leaving the other two on the old signature (a latent build break). Differential Revision: https://phabricator.services.mozilla.com/D304993
620 lines
22 KiB
C++
620 lines
22 KiB
C++
/* 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/. */
|
|
|
|
#include <atomic>
|
|
#include <set>
|
|
|
|
#include "gtest/gtest.h"
|
|
#include "mozilla/SpinEventLoopUntil.h"
|
|
#include "mozilla/gtest/MozAssertions.h"
|
|
#include "LockstoreService.h"
|
|
#include "nsString.h"
|
|
#include "nsTArray.h"
|
|
#include "nsThreadUtils.h"
|
|
|
|
using namespace mozilla;
|
|
using namespace mozilla::security::lockstore;
|
|
|
|
namespace {
|
|
|
|
nsCString UniqueCollection(const char* aPrefix) {
|
|
// Service-level state is a process-wide singleton bound to the test
|
|
// profile, so we suffix every collection with a counter to keep tests
|
|
// independent within the same binary run.
|
|
static uint32_t sCounter = 0;
|
|
nsCString out;
|
|
out.AppendASCII(aPrefix);
|
|
out.AppendLiteral("-");
|
|
out.AppendInt(++sCounter);
|
|
return out;
|
|
}
|
|
|
|
// Runs `aFn` on a background task and spins the main loop until it
|
|
// completes. Used to drive the off-main-thread sync `Do*` methods from
|
|
// gtest, which runs on the main thread.
|
|
template <typename Fn>
|
|
void RunOnBackground(Fn&& aFn) {
|
|
// `done` is touched only on the main thread (set by the completion
|
|
// runnable, read by the predicate), so it needs no synchronization.
|
|
bool done = false;
|
|
MOZ_ALWAYS_SUCCEEDS(NS_DispatchBackgroundTask(NS_NewRunnableFunction(
|
|
"TestLockstoreService::RunOnBackground",
|
|
[&done, fn = std::forward<Fn>(aFn)]() mutable {
|
|
fn();
|
|
// The predicate is satisfied from this background task, so post
|
|
// completion back to the main thread. This wakes the blocked
|
|
// NS_ProcessNextEvent inside SpinEventLoopUntil; otherwise the spin
|
|
// only re-checks `done` when unrelated event-loop traffic happens to
|
|
// wake the main thread, and those incidental wakeups grow sparse as
|
|
// the process quiesces -- stretching each wait until it trips the
|
|
// gtest no-output watchdog.
|
|
NS_DispatchToMainThread(NS_NewRunnableFunction(
|
|
"TestLockstoreService::RunOnBackground::Done",
|
|
[&done] { done = true; }));
|
|
})));
|
|
MOZ_ALWAYS_TRUE(
|
|
SpinEventLoopUntil("RunOnBackground"_ns, [&done]() { return done; }));
|
|
}
|
|
|
|
nsTArray<uint8_t> Bytes(const char* aLiteral) {
|
|
nsTArray<uint8_t> out;
|
|
out.AppendElements(reinterpret_cast<const uint8_t*>(aLiteral),
|
|
strlen(aLiteral));
|
|
return out;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
class LockstoreServiceTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {
|
|
mService = LockstoreService::GetSingleton();
|
|
ASSERT_TRUE(mService)
|
|
<< "LockstoreService singleton must be obtainable";
|
|
|
|
// Mint two fresh LocalKey kek_refs for this test. Every test gets
|
|
// independent kek_refs so collection lifetimes don't interfere
|
|
// across tests sharing the process-wide service singleton, and
|
|
// tests that need "a different KEK" (wrong-kek decrypt, addKek)
|
|
// have a second one to reach for.
|
|
RunOnBackground([&]() {
|
|
auto k1 = mService->DoCreateKek("local"_ns, /*identifier=*/""_ns, ""_ns,
|
|
/*cacheTimeoutMs=*/0);
|
|
ASSERT_TRUE(k1.isOk())
|
|
<< "DoCreateKek(local) must succeed";
|
|
mLocalKek = k1.unwrap();
|
|
ASSERT_FALSE(mLocalKek.IsEmpty())
|
|
<< "DoCreateKek(local) must mint a non-empty kek_ref";
|
|
|
|
auto k2 = mService->DoCreateKek("local"_ns, /*identifier=*/""_ns, ""_ns,
|
|
/*cacheTimeoutMs=*/0);
|
|
ASSERT_TRUE(k2.isOk())
|
|
<< "DoCreateKek(local) must succeed";
|
|
mOtherKek = k2.unwrap();
|
|
ASSERT_FALSE(mOtherKek.IsEmpty())
|
|
<< "DoCreateKek(local) must mint a non-empty kek_ref";
|
|
});
|
|
}
|
|
|
|
void TearDown() override {
|
|
// The service singleton persists across the gtest binary, so any KEKs
|
|
// left behind accumulate in the store. Drop the per-test KEKs to keep
|
|
// each test's footprint bounded. Best-effort: a test that already
|
|
// deleted its KEKs (or never finished SetUp) sees the second call no-op.
|
|
if (mLocalKek.IsEmpty() && mOtherKek.IsEmpty()) {
|
|
return;
|
|
}
|
|
RunOnBackground([&]() {
|
|
if (!mLocalKek.IsEmpty()) {
|
|
mService->DoDeleteKek(mLocalKek);
|
|
}
|
|
if (!mOtherKek.IsEmpty()) {
|
|
mService->DoDeleteKek(mOtherKek);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Fresh-per-test LocalKey kek_refs.
|
|
nsCString mLocalKek;
|
|
nsCString mOtherKek;
|
|
RefPtr<LockstoreService> mService;
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Singleton / lifecycle
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_F(LockstoreServiceTest, SingletonIdentity) {
|
|
RefPtr<LockstoreService> a = LockstoreService::GetSingleton();
|
|
RefPtr<LockstoreService> b = LockstoreService::GetSingleton();
|
|
EXPECT_EQ(a.get(), b.get()) << "GetSingleton must return the same instance";
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// createDek / listCollections / deleteDek
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_F(LockstoreServiceTest, CreateAndDeleteDek) {
|
|
nsCString coll = UniqueCollection("create-delete");
|
|
|
|
RunOnBackground([&]() {
|
|
EXPECT_NS_SUCCEEDED(mService->DoCreateDek(
|
|
coll, mLocalKek, /*extractable=*/false, /*keySize=*/32));
|
|
|
|
auto collectionsResult = mService->DoListDeks();
|
|
ASSERT_TRUE(collectionsResult.isOk());
|
|
auto collections = collectionsResult.unwrap();
|
|
bool found = false;
|
|
for (const auto& c : collections) {
|
|
if (c == coll) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
EXPECT_TRUE(found) << "Created collection should appear in listCollections";
|
|
|
|
EXPECT_NS_SUCCEEDED(mService->DoDeleteDek(coll));
|
|
|
|
// Second call rejects since the DEK is gone.
|
|
EXPECT_EQ(mService->DoDeleteDek(coll), NS_ERROR_NOT_AVAILABLE);
|
|
});
|
|
}
|
|
|
|
TEST_F(LockstoreServiceTest, CreateDek_DuplicateRejects) {
|
|
nsCString coll = UniqueCollection("dup");
|
|
RunOnBackground([&]() {
|
|
EXPECT_NS_SUCCEEDED(mService->DoCreateDek(
|
|
coll, mLocalKek, /*extractable=*/false, /*keySize=*/32));
|
|
|
|
EXPECT_EQ(mService->DoCreateDek(coll, mLocalKek, /*extractable=*/false,
|
|
/*keySize=*/32),
|
|
NS_ERROR_FAILURE)
|
|
<< "createDek on an existing collection must reject";
|
|
|
|
mService->DoDeleteDek(coll);
|
|
});
|
|
}
|
|
|
|
TEST_F(LockstoreServiceTest, CreateDek_RejectsEmptyCollection) {
|
|
RunOnBackground([&]() {
|
|
EXPECT_EQ(mService->DoCreateDek(""_ns, mLocalKek, /*extractable=*/false,
|
|
/*keySize=*/32),
|
|
NS_ERROR_INVALID_ARG);
|
|
});
|
|
}
|
|
|
|
TEST_F(LockstoreServiceTest, CreateDek_RejectsEmptyKekRef) {
|
|
nsCString coll = UniqueCollection("empty-kek");
|
|
RunOnBackground([&]() {
|
|
EXPECT_EQ(mService->DoCreateDek(coll, ""_ns, /*extractable=*/false,
|
|
/*keySize=*/32),
|
|
NS_ERROR_INVALID_ARG);
|
|
});
|
|
}
|
|
|
|
TEST_F(LockstoreServiceTest, DeleteDek_RejectsEmptyArg) {
|
|
RunOnBackground(
|
|
[&]() { EXPECT_EQ(mService->DoDeleteDek(""_ns), NS_ERROR_INVALID_ARG); });
|
|
}
|
|
|
|
TEST_F(LockstoreServiceTest, ListDeks_ContainsCreated) {
|
|
// Create three uniquely-named collections; listCollections must
|
|
// include all three. The list may also include collections from
|
|
// unrelated tests, so we only assert subset, not equality.
|
|
nsCString a = UniqueCollection("list-a");
|
|
nsCString b = UniqueCollection("list-b");
|
|
nsCString c = UniqueCollection("list-c");
|
|
RunOnBackground([&]() {
|
|
EXPECT_NS_SUCCEEDED(mService->DoCreateDek(
|
|
a, mLocalKek, /*extractable=*/false, /*keySize=*/32));
|
|
EXPECT_NS_SUCCEEDED(mService->DoCreateDek(
|
|
b, mLocalKek, /*extractable=*/false, /*keySize=*/32));
|
|
EXPECT_NS_SUCCEEDED(mService->DoCreateDek(
|
|
c, mLocalKek, /*extractable=*/false, /*keySize=*/32));
|
|
|
|
auto listResult = mService->DoListDeks();
|
|
ASSERT_TRUE(listResult.isOk());
|
|
auto list = listResult.unwrap();
|
|
std::set<nsCString> names;
|
|
for (const auto& n : list) {
|
|
names.insert(n);
|
|
}
|
|
EXPECT_TRUE(names.count(a)) << "listCollections missing " << a.get();
|
|
EXPECT_TRUE(names.count(b)) << "listCollections missing " << b.get();
|
|
EXPECT_TRUE(names.count(c)) << "listCollections missing " << c.get();
|
|
|
|
mService->DoDeleteDek(a);
|
|
mService->DoDeleteDek(b);
|
|
mService->DoDeleteDek(c);
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// listKeks
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_F(LockstoreServiceTest, ListKeks_ReflectsCreateDek) {
|
|
nsCString coll = UniqueCollection("keks-create");
|
|
RunOnBackground([&]() {
|
|
EXPECT_NS_SUCCEEDED(mService->DoCreateDek(
|
|
coll, mLocalKek, /*extractable=*/false, /*keySize=*/32));
|
|
|
|
auto refsResult = mService->DoListKeks(coll);
|
|
ASSERT_TRUE(refsResult.isOk());
|
|
auto refs = refsResult.unwrap();
|
|
ASSERT_EQ(refs.Length(), 1u);
|
|
EXPECT_EQ(refs[0], mLocalKek);
|
|
|
|
mService->DoDeleteDek(coll);
|
|
});
|
|
}
|
|
|
|
TEST_F(LockstoreServiceTest, ListKeks_RejectsNoDek) {
|
|
nsCString coll = UniqueCollection("keks-missing");
|
|
RunOnBackground([&]() {
|
|
auto refsResult = mService->DoListKeks(coll);
|
|
EXPECT_TRUE(refsResult.isErr());
|
|
EXPECT_EQ(refsResult.unwrapErr(), NS_ERROR_NOT_AVAILABLE);
|
|
});
|
|
}
|
|
|
|
TEST_F(LockstoreServiceTest, ListKeks_RejectsEmptyCollection) {
|
|
RunOnBackground([&]() {
|
|
auto refsResult = mService->DoListKeks(""_ns);
|
|
EXPECT_TRUE(refsResult.isErr());
|
|
// Empty / unknown collection surfaces as NS_ERROR_NOT_AVAILABLE: the
|
|
// keystore layer rejects the lookup with `NotFound`, which the FFI
|
|
// maps via `error_to_nsresult`. The FFI no longer pre-rejects empty
|
|
// strings at the boundary.
|
|
EXPECT_EQ(refsResult.unwrapErr(), NS_ERROR_NOT_AVAILABLE);
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// encrypt / decrypt
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_F(LockstoreServiceTest, EncryptDecryptRoundtrip) {
|
|
nsCString coll = UniqueCollection("roundtrip");
|
|
RunOnBackground([&]() {
|
|
EXPECT_NS_SUCCEEDED(mService->DoCreateDek(
|
|
coll, mLocalKek, /*extractable=*/false, /*keySize=*/32));
|
|
|
|
auto ctResult = mService->DoEncrypt(coll, mLocalKek, Bytes("hello world"));
|
|
ASSERT_TRUE(ctResult.isOk());
|
|
auto ciphertext = ctResult.unwrap();
|
|
EXPECT_GT(ciphertext.Length(), 0u);
|
|
|
|
auto ptResult = mService->DoDecrypt(coll, mLocalKek, ciphertext);
|
|
ASSERT_TRUE(ptResult.isOk());
|
|
auto plaintext = ptResult.unwrap();
|
|
nsCString joined;
|
|
for (uint8_t b : plaintext) {
|
|
joined.Append(static_cast<char>(b));
|
|
}
|
|
EXPECT_STREQ(joined.get(), "hello world");
|
|
|
|
mService->DoDeleteDek(coll);
|
|
});
|
|
}
|
|
|
|
TEST_F(LockstoreServiceTest, Encrypt_YieldsUniqueCiphertexts) {
|
|
nsCString coll = UniqueCollection("nonce");
|
|
RunOnBackground([&]() {
|
|
EXPECT_NS_SUCCEEDED(mService->DoCreateDek(
|
|
coll, mLocalKek, /*extractable=*/false, /*keySize=*/32));
|
|
|
|
auto a = mService->DoEncrypt(coll, mLocalKek, Bytes("same"));
|
|
auto b = mService->DoEncrypt(coll, mLocalKek, Bytes("same"));
|
|
ASSERT_TRUE(a.isOk());
|
|
ASSERT_TRUE(b.isOk());
|
|
auto ctA = a.unwrap();
|
|
auto ctB = b.unwrap();
|
|
EXPECT_NE(ctA, ctB) << "Repeated encrypts of the same plaintext must "
|
|
"yield distinct ciphertexts (random nonce)";
|
|
|
|
mService->DoDeleteDek(coll);
|
|
});
|
|
}
|
|
|
|
TEST_F(LockstoreServiceTest, Decrypt_CorruptedCiphertextRejects) {
|
|
nsCString coll = UniqueCollection("corrupt");
|
|
RunOnBackground([&]() {
|
|
EXPECT_NS_SUCCEEDED(mService->DoCreateDek(
|
|
coll, mLocalKek, /*extractable=*/false, /*keySize=*/32));
|
|
|
|
auto ctResult = mService->DoEncrypt(coll, mLocalKek, Bytes("payload"));
|
|
ASSERT_TRUE(ctResult.isOk());
|
|
auto ct = ctResult.unwrap();
|
|
// Flip a byte in the middle to corrupt the AEAD tag.
|
|
if (ct.Length() > 0) {
|
|
ct[ct.Length() / 2] ^= 0xff;
|
|
}
|
|
|
|
auto ptResult = mService->DoDecrypt(coll, mLocalKek, ct);
|
|
EXPECT_TRUE(ptResult.isErr());
|
|
|
|
mService->DoDeleteDek(coll);
|
|
});
|
|
}
|
|
|
|
TEST_F(LockstoreServiceTest, Decrypt_TruncatedCiphertextRejects) {
|
|
nsCString coll = UniqueCollection("trunc");
|
|
RunOnBackground([&]() {
|
|
EXPECT_NS_SUCCEEDED(mService->DoCreateDek(
|
|
coll, mLocalKek, /*extractable=*/false, /*keySize=*/32));
|
|
|
|
auto ctResult = mService->DoEncrypt(coll, mLocalKek, Bytes("payload"));
|
|
ASSERT_TRUE(ctResult.isOk());
|
|
auto ct = ctResult.unwrap();
|
|
if (ct.Length() > 8) {
|
|
ct.SetLength(ct.Length() / 2);
|
|
}
|
|
|
|
auto ptResult = mService->DoDecrypt(coll, mLocalKek, ct);
|
|
EXPECT_TRUE(ptResult.isErr());
|
|
|
|
mService->DoDeleteDek(coll);
|
|
});
|
|
}
|
|
|
|
TEST_F(LockstoreServiceTest, Decrypt_WrongKekRejects) {
|
|
nsCString coll = UniqueCollection("wrong-kek");
|
|
RunOnBackground([&]() {
|
|
EXPECT_NS_SUCCEEDED(mService->DoCreateDek(
|
|
coll, mLocalKek, /*extractable=*/false, /*keySize=*/32));
|
|
|
|
auto ctResult = mService->DoEncrypt(coll, mLocalKek, Bytes("payload"));
|
|
ASSERT_TRUE(ctResult.isOk());
|
|
auto ct = ctResult.unwrap();
|
|
|
|
auto ptResult = mService->DoDecrypt(coll, mOtherKek, ct);
|
|
EXPECT_TRUE(ptResult.isErr());
|
|
|
|
mService->DoDeleteDek(coll);
|
|
});
|
|
}
|
|
|
|
TEST_F(LockstoreServiceTest, Encrypt_NoDekRejects) {
|
|
nsCString coll = UniqueCollection("no-dek");
|
|
RunOnBackground([&]() {
|
|
auto ctResult = mService->DoEncrypt(coll, mLocalKek, Bytes("payload"));
|
|
EXPECT_TRUE(ctResult.isErr());
|
|
});
|
|
}
|
|
|
|
TEST_F(LockstoreServiceTest, Encrypt_RejectsEmptyArgs) {
|
|
nsCString coll = UniqueCollection("empty-enc");
|
|
RunOnBackground([&]() {
|
|
EXPECT_NS_SUCCEEDED(mService->DoCreateDek(
|
|
coll, mLocalKek, /*extractable=*/false, /*keySize=*/32));
|
|
|
|
EXPECT_TRUE(mService->DoEncrypt(""_ns, mLocalKek, Bytes("x")).isErr());
|
|
EXPECT_TRUE(mService->DoEncrypt(coll, ""_ns, Bytes("x")).isErr());
|
|
|
|
mService->DoDeleteDek(coll);
|
|
});
|
|
}
|
|
|
|
TEST_F(LockstoreServiceTest, Decrypt_NoDekRejects) {
|
|
nsCString coll = UniqueCollection("no-dek-dec");
|
|
RunOnBackground([&]() {
|
|
nsTArray<uint8_t> bogus;
|
|
bogus.AppendElements(static_cast<const uint8_t*>(
|
|
reinterpret_cast<const uint8_t*>("\0\0\0\0\0\0")),
|
|
6);
|
|
auto ptResult = mService->DoDecrypt(coll, mLocalKek, bogus);
|
|
EXPECT_TRUE(ptResult.isErr());
|
|
});
|
|
}
|
|
|
|
TEST_F(LockstoreServiceTest, Decrypt_RejectsEmptyArgs) {
|
|
nsCString coll = UniqueCollection("empty-dec");
|
|
RunOnBackground([&]() {
|
|
EXPECT_NS_SUCCEEDED(mService->DoCreateDek(
|
|
coll, mLocalKek, /*extractable=*/false, /*keySize=*/32));
|
|
|
|
auto ctResult = mService->DoEncrypt(coll, mLocalKek, Bytes("x"));
|
|
ASSERT_TRUE(ctResult.isOk());
|
|
auto ct = ctResult.unwrap();
|
|
|
|
EXPECT_TRUE(mService->DoDecrypt(""_ns, mLocalKek, ct).isErr());
|
|
EXPECT_TRUE(mService->DoDecrypt(coll, ""_ns, ct).isErr());
|
|
|
|
mService->DoDeleteDek(coll);
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// addKek / removeKek
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_F(LockstoreServiceTest, AddKek_RejectsMissingCollection) {
|
|
nsCString coll = UniqueCollection("addkek-missing");
|
|
RunOnBackground([&]() {
|
|
EXPECT_EQ(mService->DoAddKek(coll, mLocalKek, mOtherKek),
|
|
NS_ERROR_NOT_AVAILABLE);
|
|
});
|
|
}
|
|
|
|
TEST_F(LockstoreServiceTest, AddKek_RejectsEmptyArgs) {
|
|
nsCString coll = UniqueCollection("addkek-empty");
|
|
RunOnBackground([&]() {
|
|
EXPECT_NS_SUCCEEDED(mService->DoCreateDek(
|
|
coll, mLocalKek, /*extractable=*/false, /*keySize=*/32));
|
|
|
|
EXPECT_EQ(mService->DoAddKek(""_ns, mLocalKek, mOtherKek),
|
|
NS_ERROR_INVALID_ARG);
|
|
EXPECT_EQ(mService->DoAddKek(coll, ""_ns, mOtherKek), NS_ERROR_INVALID_ARG);
|
|
EXPECT_EQ(mService->DoAddKek(coll, mLocalKek, ""_ns), NS_ERROR_INVALID_ARG);
|
|
|
|
mService->DoDeleteDek(coll);
|
|
});
|
|
}
|
|
|
|
TEST_F(LockstoreServiceTest, RemoveKek_RejectsEmptyArgs) {
|
|
nsCString coll = UniqueCollection("rmkek-empty");
|
|
RunOnBackground([&]() {
|
|
EXPECT_NS_SUCCEEDED(mService->DoCreateDek(
|
|
coll, mLocalKek, /*extractable=*/false, /*keySize=*/32));
|
|
|
|
EXPECT_EQ(mService->DoRemoveKek(""_ns, mLocalKek), NS_ERROR_INVALID_ARG);
|
|
EXPECT_EQ(mService->DoRemoveKek(coll, ""_ns), NS_ERROR_INVALID_ARG);
|
|
|
|
mService->DoDeleteDek(coll);
|
|
});
|
|
}
|
|
|
|
TEST_F(LockstoreServiceTest, RemoveKek_LastWrappingRejects) {
|
|
nsCString coll = UniqueCollection("rmkek-last");
|
|
RunOnBackground([&]() {
|
|
EXPECT_NS_SUCCEEDED(mService->DoCreateDek(
|
|
coll, mLocalKek, /*extractable=*/false, /*keySize=*/32));
|
|
|
|
// Removing the last KEK wrapping must be rejected — otherwise the
|
|
// DEK would become unrecoverable.
|
|
EXPECT_EQ(mService->DoRemoveKek(coll, mLocalKek), NS_ERROR_FAILURE);
|
|
|
|
mService->DoDeleteDek(coll);
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Threading: concurrent dispatches all complete with `mMutex`
|
|
// serialising under the hood.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_F(LockstoreServiceTest, ConcurrentEncryptsAllResolveUnique) {
|
|
// Dispatch N encrypts in parallel on independent background tasks.
|
|
// The service's `mMutex` guarantees they execute one at a time;
|
|
// each must produce a distinct ciphertext (random nonce).
|
|
nsCString coll = UniqueCollection("concurrent");
|
|
RunOnBackground([&]() {
|
|
EXPECT_NS_SUCCEEDED(mService->DoCreateDek(
|
|
coll, mLocalKek, /*extractable=*/false, /*keySize=*/32));
|
|
});
|
|
|
|
constexpr size_t N = 8;
|
|
nsTArray<nsTArray<uint8_t>> results;
|
|
results.SetLength(N);
|
|
std::atomic<size_t> doneCount{0};
|
|
|
|
for (size_t i = 0; i < N; ++i) {
|
|
MOZ_ALWAYS_SUCCEEDS(NS_DispatchBackgroundTask(NS_NewRunnableFunction(
|
|
"ConcurrentEncryptsAllResolveUnique::worker",
|
|
[this, &coll, &results, &doneCount, i]() {
|
|
auto r = mService->DoEncrypt(coll, mLocalKek, Bytes("same-input"));
|
|
if (r.isOk()) {
|
|
results[i] = r.unwrap();
|
|
}
|
|
// Record completion on the main thread so the spin loop is woken
|
|
// promptly (see RunOnBackground).
|
|
NS_DispatchToMainThread(
|
|
NS_NewRunnableFunction("ConcurrentEncryptsAllResolveUnique::done",
|
|
[&doneCount]() { ++doneCount; }));
|
|
})));
|
|
}
|
|
MOZ_ALWAYS_TRUE(
|
|
SpinEventLoopUntil("ConcurrentEncryptsAllResolveUnique"_ns,
|
|
[&doneCount]() { return doneCount.load() == N; }));
|
|
|
|
std::set<nsCString> seen;
|
|
for (const auto& ct : results) {
|
|
EXPECT_GT(ct.Length(), 0u);
|
|
nsCString joined;
|
|
for (uint8_t b : ct) {
|
|
joined.AppendInt(static_cast<uint32_t>(b));
|
|
joined.AppendLiteral(",");
|
|
}
|
|
seen.insert(std::move(joined));
|
|
}
|
|
EXPECT_EQ(seen.size(), N)
|
|
<< "Every concurrent encrypt must produce a unique ciphertext";
|
|
|
|
RunOnBackground([&]() { mService->DoDeleteDek(coll); });
|
|
}
|
|
|
|
TEST_F(LockstoreServiceTest, ConcurrentMixedOpsAllComplete) {
|
|
// Mix createDek / encrypt-decrypt / deleteDek across multiple
|
|
// collections concurrently. All ops must complete without deadlock,
|
|
// and listCollections at the end must reflect the post-cleanup state.
|
|
constexpr size_t N = 4;
|
|
nsTArray<nsCString> colls;
|
|
for (size_t i = 0; i < N; ++i) {
|
|
colls.AppendElement(UniqueCollection("mix"));
|
|
}
|
|
|
|
// Concurrent creates.
|
|
std::atomic<size_t> createDone{0};
|
|
for (size_t i = 0; i < N; ++i) {
|
|
const nsCString& c = colls[i];
|
|
MOZ_ALWAYS_SUCCEEDS(NS_DispatchBackgroundTask(NS_NewRunnableFunction(
|
|
"ConcurrentMixedOps::create", [this, &c, &createDone]() {
|
|
EXPECT_NS_SUCCEEDED(mService->DoCreateDek(
|
|
c, mLocalKek, /*extractable=*/false, /*keySize=*/32));
|
|
NS_DispatchToMainThread(
|
|
NS_NewRunnableFunction("ConcurrentMixedOps::create-done",
|
|
[&createDone]() { ++createDone; }));
|
|
})));
|
|
}
|
|
MOZ_ALWAYS_TRUE(
|
|
SpinEventLoopUntil("ConcurrentMixedOps::create-wait"_ns,
|
|
[&createDone]() { return createDone.load() == N; }));
|
|
|
|
// Concurrent encrypt + decrypt round-trips per collection.
|
|
std::atomic<size_t> roundtripDone{0};
|
|
for (size_t i = 0; i < N; ++i) {
|
|
const nsCString& c = colls[i];
|
|
MOZ_ALWAYS_SUCCEEDS(NS_DispatchBackgroundTask(NS_NewRunnableFunction(
|
|
"ConcurrentMixedOps::roundtrip", [this, &c, &roundtripDone]() {
|
|
auto ctResult = mService->DoEncrypt(c, mLocalKek, Bytes("payload"));
|
|
EXPECT_TRUE(ctResult.isOk());
|
|
if (ctResult.isOk()) {
|
|
auto ct = ctResult.unwrap();
|
|
EXPECT_GT(ct.Length(), 0u);
|
|
auto ptResult = mService->DoDecrypt(c, mLocalKek, ct);
|
|
EXPECT_TRUE(ptResult.isOk());
|
|
if (ptResult.isOk()) {
|
|
EXPECT_EQ(ptResult.unwrap().Length(), strlen("payload"));
|
|
}
|
|
}
|
|
NS_DispatchToMainThread(
|
|
NS_NewRunnableFunction("ConcurrentMixedOps::roundtrip-done",
|
|
[&roundtripDone]() { ++roundtripDone; }));
|
|
})));
|
|
}
|
|
MOZ_ALWAYS_TRUE(SpinEventLoopUntil(
|
|
"ConcurrentMixedOps::roundtrip-wait"_ns,
|
|
[&roundtripDone]() { return roundtripDone.load() == N; }));
|
|
|
|
// Concurrent deletes.
|
|
std::atomic<size_t> deleteDone{0};
|
|
for (size_t i = 0; i < N; ++i) {
|
|
const nsCString& c = colls[i];
|
|
MOZ_ALWAYS_SUCCEEDS(NS_DispatchBackgroundTask(NS_NewRunnableFunction(
|
|
"ConcurrentMixedOps::delete", [this, &c, &deleteDone]() {
|
|
EXPECT_NS_SUCCEEDED(mService->DoDeleteDek(c));
|
|
NS_DispatchToMainThread(
|
|
NS_NewRunnableFunction("ConcurrentMixedOps::delete-done",
|
|
[&deleteDone]() { ++deleteDone; }));
|
|
})));
|
|
}
|
|
MOZ_ALWAYS_TRUE(
|
|
SpinEventLoopUntil("ConcurrentMixedOps::delete-wait"_ns,
|
|
[&deleteDone]() { return deleteDone.load() == N; }));
|
|
|
|
// Verify listCollections no longer contains any of them.
|
|
RunOnBackground([&]() {
|
|
auto remainingResult = mService->DoListDeks();
|
|
ASSERT_TRUE(remainingResult.isOk());
|
|
auto remaining = remainingResult.unwrap();
|
|
for (const auto& c : colls) {
|
|
for (const auto& r : remaining) {
|
|
EXPECT_NE(r, c) << "Collection " << c.get()
|
|
<< " should be gone but is still listed";
|
|
}
|
|
}
|
|
});
|
|
}
|