Firefox · DOM
CVE-2026-100798
Logic Error in DOM
Overview
Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/quota/NSSCipherStrategy.cpp |
modified | |
fordom/quota/test/gtest/TestNSSCipherStrategy.cpp |
modified |
Files Changed
dom/quota/DummyCipherStrategy.hdom/quota/EncryptingOutputStream_impl.hdom/quota/NSSCipherStrategy.cppdom/quota/NSSCipherStrategy.hdom/quota/test/gtest/TestNSSCipherStrategy.cppdom/quota/test/gtest/moz.buildstorage/ObfuscatingVFS.cpp
Patch
diff --git a/dom/quota/DummyCipherStrategy.h b/dom/quota/DummyCipherStrategy.h
index ff936577d52..26ddb1e627e 100644
--- a/dom/quota/DummyCipherStrategy.h
+++ b/dom/quota/DummyCipherStrategy.h
@@ -30,8 +30,7 @@ struct DummyCipherStrategy {
static Result<KeyType, nsresult> GenerateKey() { return KeyType{}; }
- nsresult Init(CipherMode aCipherMode, Span<const uint8_t> aKey,
- Span<const uint8_t> aInitialIv = Span<const uint8_t>{}) {
+ nsresult Init(CipherMode aCipherMode, Span<const uint8_t> aKey) {
return NS_OK;
}
diff --git a/dom/quota/EncryptingOutputStream_impl.h b/dom/quota/EncryptingOutputStream_impl.h
index 357fef61c4e..ebf481876a1 100644
--- a/dom/quota/EncryptingOutputStream_impl.h
+++ b/dom/quota/EncryptingOutputStream_impl.h
@@ -27,8 +27,7 @@ EncryptingOutputStream<CipherStrategy>::EncryptingOutputStream(
: EncryptingOutputStreamBase(std::move(aBaseStream), aBlockSize) {
// XXX Move this to a fallible init function.
MOZ_ALWAYS_SUCCEEDS(mCipherStrategy.Init(CipherMode::Encrypt,
- CipherStrategy::SerializeKey(aKey),
- CipherStrategy::MakeBlockPrefix()));
+ CipherStrategy::SerializeKey(aKey)));
MOZ_ASSERT(mBlockSize > 0);
MOZ_ASSERT(mBlockSize % CipherStrategy::BasicBlockSize == 0);
diff --git a/dom/quota/NSSCipherStrategy.cpp b/dom/quota/NSSCipherStrategy.cpp
index 7ecd49ccdd7..20f2fcff77b 100644
--- a/dom/quota/NSSCipherStrategy.cpp
+++ b/dom/quota/NSSCipherStrategy.cpp
@@ -4,7 +4,6 @@
#include "NSSCipherStrategy.h"
-#include <cstring>
#include <utility>
#include "CipherStrategyUtils.h"
@@ -28,16 +27,12 @@ Result<NSSCipherStrategy::KeyType, nsresult> NSSCipherStrategy::GenerateKey() {
}
nsresult NSSCipherStrategy::Init(const CipherMode aMode,
- const Span<const uint8_t> aKey,
- const Span<const uint8_t> aInitialIv) {
- MOZ_ASSERT_IF(CipherMode::Encrypt == aMode, aInitialIv.Length() == 32);
+ const Span<const uint8_t> aKey) {
MOZ_RELEASE_ASSERT(EnsureNSSInitializedChromeOrContent(),
"Could not initialize NSS.");
mMode.init(aMode);
- mIv.AppendElements(aInitialIv);
-
const auto slot = UniquePK11SlotInfo{PK11_GetInternalSlot()};
if (slot == nullptr) {
return NS_ERROR_FAILURE;
@@ -70,11 +65,6 @@ nsresult NSSCipherStrategy::Init(const CipherMode aMode,
nsresult NSSCipherStrategy::Cipher(const Span<uint8_t> aIv,
const Span<const uint8_t> aIn,
const Span<uint8_t> aOut) {
- if (CipherMode::Encrypt == *mMode) {
- MOZ_RELEASE_ASSERT(aIv.Length() == mIv.Length());
- memcpy(aIv.Elements(), mIv.Elements(), aIv.Length());
- }
-
// XXX make tag a separate parameter
constexpr size_t tagLen = 16;
const auto tag = aIv.Last(tagLen);
@@ -83,18 +73,30 @@ nsresult NSSCipherStrategy::Cipher(const Span<uint8_t> aIv,
const auto iv = aIv.First(12);
MOZ_ASSERT(tag.Length() + iv.Length() <= aIv.Length());
+ if (CipherMode::Encrypt == *mMode) {
+ // Draw a fresh random nonce per message. PK11_AEADOp must not derive one:
+ // its counter is private to this PK11Context and restarts at zero, while
+ // callers create a context per SQLite file handle and per reopen under a
+ // single key, so those counters collide and repeat (key, nonce) pairs.
+ //
+ // This covers the whole prefix ahead of the tag, not just the nonce, so
+ // that no part of aIv depends on the caller having initialized it.
+ const auto generated = aIv.First(aIv.Length() - tagLen);
+ if (PK11_GenerateRandom(generated.Elements(),
+ static_cast<int>(generated.Length())) !=
+ SECSuccess) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
int outLen;
// aIn and aOut may not overlap resp. be the same, so we can't do this
// in-place.
const SECStatus rv = PK11_AEADOp(
- mPK11Context->get(), CKG_GENERATE_COUNTER, 0, iv.Elements(), iv.Length(),
+ mPK11Context->get(), CKG_NO_GENERATE, 0, iv.Elements(), iv.Length(),
nullptr, 0, aOut.Elements(), &outLen, aOut.Length(), tag.Elements(),
tag.Length(), aIn.Elements(), aIn.Length());
- if (CipherMode::Encrypt == *mMode) {
- memcpy(mIv.Elements(), aIv.Elements(), aIv.Length());
- }
-
return MapSECStatus(rv);
}
diff --git a/dom/quota/NSSCipherStrategy.h b/dom/quota/NSSCipherStrategy.h
index 7f4ea9922f1..40ae78b8a88 100644
--- a/dom/quota/NSSCipherStrategy.h
+++ b/dom/quota/NSSCipherStrategy.h
@@ -15,7 +15,6 @@
#include "mozilla/InitializedOnce.h"
#include "mozilla/Result.h"
#include "mozilla/Span.h"
-#include "nsTArray.h"
namespace mozilla::dom::quota {
@@ -28,9 +27,13 @@ struct NSSCipherStrategy {
static Result<KeyType, nsresult> GenerateKey();
- nsresult Init(CipherMode aCipherMode, Span<const uint8_t> aKey,
- Span<const uint8_t> aInitialIv = Span<const uint8_t>{});
+ nsresult Init(CipherMode aCipherMode, Span<const uint8_t> aKey);
+ // On encrypt the whole of aIv is written -- a freshly generated nonce in the
+ // leading 12 bytes, the authentication tag in the trailing 16, random filler
+ // in between -- and the caller must store all of it alongside the ciphertext.
+ // Its incoming contents are ignored. On decrypt aIv supplies that same stored
+ // nonce and tag.
nsresult Cipher(Span<uint8_t> aIv, Span<const uint8_t> aIn,
Span<uint8_t> aOut);
@@ -45,7 +48,6 @@ struct NSSCipherStrategy {
// XXX Remove EarlyDestructible, remove moving of the CipherStrategy.
LazyInitializedOnceEarlyDestructible<const CipherMode> mMode;
LazyInitializedOnceEarlyDestructible<const UniquePK11Context> mPK11Context;
- nsTArray<uint8_t> mIv;
};
} // namespace mozilla::dom::quota
diff --git a/storage/ObfuscatingVFS.cpp b/storage/ObfuscatingVFS.cpp
index 156d419c199..d79f5cae16e 100644
--- a/storage/ObfuscatingVFS.cpp
+++ b/storage/ObfuscatingVFS.cpp
@@ -271,9 +271,13 @@ static void* obfsEncode(ObfsFile* p, /* File containing page to be obfuscated */
MOZ_ASSERT(payloadLength > 0);
// XXX I guess this can be done in-place as well, then we don't need the
// temporary page at all, I guess?
- p->encryptCipherStrategy->Cipher(
- Span{aIv}, Span{a + i, static_cast<unsigned>(payloadLength)},
- Span{pOut + i, static_cast<unsigned>(payloadLength)});
+ // On failure pOut still holds uninitialized or previously encoded content,
+ // which obfsWrite would write out under a nonce that never encrypted it.
+ if (NS_WARN_IF(NS_FAILED(p->encryptCipherStrategy->Cipher(
+ Span{aIv}, Span{a + i, static_cast<unsigned>(payloadLength)},
+ Span{pOut + i, static_cast<unsigned>(payloadLength)})))) {
+ return nullptr;
+ }
memcpy(pOut + nByte - kReservedBytes, aIv, kIvBytes);
return pOut;
@@ -851,8 +855,7 @@ static int obfsOpen(sqlite3_vfs* pVfs, const char* zName, sqlite3_file* pFile,
auto resetMethods = MakeScopeExit([pFile] { pFile->pMethods = nullptr; });
if (NS_WARN_IF(NS_FAILED(encryptCipherStrategy->Init(
- CipherMode::Encrypt, Span{aKey, sizeof(aKey)},
- IPCStreamCipherStrategy::MakeBlockPrefix())))) {
+ CipherMode::Encrypt, Span{aKey, sizeof(aKey)})))) {
return SQLITE_ERROR;
}
diff --git a/dom/quota/test/gtest/TestNSSCipherStrategy.cpp b/dom/quota/test/gtest/TestNSSCipherStrategy.cpp
new file mode 100644
index 00000000000..4657c3467a4
--- /dev/null
+++ b/dom/quota/test/gtest/TestNSSCipherStrategy.cpp
@@ -0,0 +1,189 @@
+/* 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 <array>
+#include <cstdint>
+#include <numeric>
+#include <set>
+#include <vector>
+
+#include "NSSCipherStrategy.h"
+#include "gtest/gtest.h"
+#include "mozilla/Span.h"
+
+namespace mozilla::dom::quota::test {
+
+namespace {
+
+constexpr size_t kPayloadLength = 64;
+constexpr size_t kNonceLength = 12;
+
+using IvArray = std::array<uint8_t, NSSCipherStrategy::BlockPrefixLength>;
+using Payload = std::array<uint8_t, kPayloadLength>;
+
+NSSCipherStrategy::KeyType MakeKey(uint8_t aInitialValue = 1) {
+ NSSCipherStrategy::KeyType key;
+ std::iota(key.begin(), key.end(), aInitialValue);
+ return key;
+}
+
+Payload MakePlaintext(uint8_t aInitialValue = 1) {
+ Payload plaintext;
+ std::iota(plaintext.begin(), plaintext.end(), aInitialValue);
+ return plaintext;
+}
+
+std::vector<uint8_t> NonceOf(const IvArray& aIv) {
+ return std::vector<uint8_t>(aIv.begin(), aIv.begin() + kNonceLength);
+}
+
+} // namespace
+
+TEST(NSSCipherStrategyTest, NonceDiffersAcrossCallsInOneContext)
+{
+ const auto key = MakeKey();
+ const auto plaintext = MakePlaintext();
+
+ NSSCipherStrategy encrypt;
+ ASSERT_EQ(NS_OK, encrypt.Init(CipherMode::Encrypt,
+ NSSCipherStrategy::SerializeKey(key)));
+
+ std::set<std::vector<uint8_t>> nonces;
+ for (size_t i = 0; i < 32; ++i) {
+ IvArray iv{};
+ Payload ciphertext{};
+ ASSERT_EQ(NS_OK,
+ encrypt.Cipher(Span{iv}, Span{plaintext}, Span{ciphertext}));
+ EXPECT_TRUE(nonces.insert(NonceOf(iv)).second)
+ << "nonce repeated within a single context on call " << i;
+ }
+}
+
+// Regression test for bug 2055694. ObfuscatingVFS creates a separate encrypt
+// context per SQLite file handle and per reopen, all sharing one key. When the
+// nonce came from a per-context counter starting at zero, those contexts
+// produced identical (key, nonce) pairs, so XOR of two ciphertexts disclosed
+// XOR of their plaintexts.
+TEST(NSSCipherStrategyTest, NonceDiffersAcrossContextsWithTheSameKey)
+{
+ const auto key = MakeKey();
+ const auto plaintext = MakePlaintext();
+
+ const auto encryptOnce = [&key, &plaintext](IvArray& aIv,
+ Payload& aCiphertext) {
+ NSSCipherStrategy encrypt;
+ ASSERT_EQ(NS_OK, encrypt.Init(CipherMode::Encrypt,
+ NSSCipherStrategy::SerializeKey(key)));
+ ASSERT_EQ(NS_OK,
+ encrypt.Cipher(Span{aIv}, Span{plaintext}, Span{aCiphertext}));
+ };
+
+ IvArray firstIv{};
+ Payload firstCiphertext{};
+ encryptOnce(firstIv, firstCiphertext);
+
+ IvArray secondIv{};
+ Payload secondCiphertext{};
+ encryptOnce(secondIv, secondCiphertext);
+
+ EXPECT_NE(NonceOf(firstIv), NonceOf(secondIv));
+ EXPECT_NE(firstCiphertext, secondCiphertext);
+}
+
+// Everything ahead of the tag is generated by Cipher, so a caller that leaves
+// the block prefix uninitialized cannot leak its contents to disk.
+TEST(NSSCipherStrategyTest, OverwritesTheWholePrefixRegardlessOfCallerInput)
+{
+ const auto key = MakeKey();
+ const auto plaintext = MakePlaintext();
+
+ NSSCipherStrategy encrypt;
+ ASSERT_EQ(NS_OK, encrypt.Init(CipherMode::Encrypt,
+ NSSCipherStrategy::SerializeKey(key)));
+
+ constexpr size_t kTagLength = 16;
+ constexpr uint8_t kSentinel = 0xAA;
+
+ std::set<std::vector<uint8_t>> fillers;
+ for (size_t i = 0; i < 32; ++i) {
+ IvArray iv;
+ iv.fill(kSentinel);
+ Payload ciphertext{};
+ ASSERT_EQ(NS_OK,
+ encrypt.Cipher(Span{iv}, Span{plaintext}, Span{ciphertext}));
+
+ fillers.emplace(iv.begin() + kNonceLength, iv.end() - kTagLength);
+ }
+
+ // Had the filler bytes been left to the caller, every sample would still
+ // hold the sentinel and this set would have exactly one entry.
+ EXPECT_GT(fillers.size(), 1u);
+}
+
+TEST(NSSCipherStrategyTest, RoundTripsUsingTheStoredNonce)
+{
+ const auto key = MakeKey();
+ const auto plaintext = MakePlaintext();
+
+ IvArray iv{};
+ Payload ciphertext{};
+ {
+ NSSCipherStrategy encrypt;
+ ASSERT_EQ(NS_OK, encrypt.Init(CipherMode::Encrypt,
+ NSSCipherStrategy::SerializeKey(key)));
+ ASSERT_EQ(NS_OK,
+ encrypt.Cipher(Span{iv}, Span{plaintext}, Span{ciphertext}));
+ }
+ EXPECT_NE(plaintext, ciphertext);
+
+ Payload decrypted{};
+ {
+ NSSCipherStrategy decrypt;
+ ASSERT_EQ(NS_OK, decrypt.Init(CipherMode::Decrypt,
+ NSSCipherStrategy::SerializeKey(key)));
+ ASSERT_EQ(NS_OK,
+ decrypt.Cipher(Span{iv}, Span{ciphertext}, Span{decrypted}));
+ }
+ EXPECT_EQ(plaintext, decrypted);
+}
+
+// Decryption must depend only on the nonce stored alongside the ciphertext,
+// never on a context-local counter. This is what keeps blocks written by older
+// builds -- whose nonces were sequential -- readable.
+TEST(NSSCipherStrategyTest, DecryptionIsIndependentOfBlockOrder)
+{
+ const auto key = MakeKey();
+ const auto firstPlaintext = MakePlaintext(1);
+ const auto secondPlaintext = MakePlaintext(101);
+
+ IvArray firstIv{};
+ Payload firstCiphertext{};
+ IvArray secondIv{};
+ Payload secondCiphertext{};
+ {
+ NSSCipherStrategy encrypt;
+ ASSERT_EQ(NS_OK, encrypt.Init(CipherMode::Encrypt,
+ NSSCipherStrategy::SerializeKey(key)));
+ ASSERT_EQ(NS_OK, encrypt.Cipher(Span{firstIv}, Span{firstPlaintext},
+ Span{firstCiphertext}));
+ ASSERT_EQ(NS_OK, encrypt.Cipher(Span{secondIv}, Span{secondPlaintext},
+ Span{secondCiphertext}));
+ }
+
+ NSSCipherStrategy decrypt;
+ ASSERT_EQ(NS_OK, decrypt.Init(CipherMode::Decrypt,
+ NSSCipherStrategy::SerializeKey(key)));
+
+ Payload secondDecrypted{};
+ ASSERT_EQ(NS_OK, decrypt.Cipher(Span{secondIv}, Span{secondCiphertext},
+ Span{secondDecrypted}));
+ EXPECT_EQ(secondPlaintext, secondDecrypted);
+
+ Payload firstDecrypted{};
+ ASSERT_EQ(NS_OK, decrypt.Cipher(Span{firstIv}, Span{firstCiphertext},
+ Span{firstDecrypted}));
+ EXPECT_EQ(firstPlaintext, firstDecrypted);
+}
+
+} // namespace mozilla::dom::quota::test
diff --git a/dom/quota/test/gtest/moz.build b/dom/quota/test/gtest/moz.build
index 40fc5e90a4b..344a44804d1 100644
--- a/dom/quota/test/gtest/moz.build
+++ b/dom/quota/test/gtest/moz.build
@@ -30,6 +30,7 @@ UNIFIED_SOURCES = [
"TestFlatten.cpp",
"TestForwardDecls.cpp",
"TestMozPromiseUtils.cpp",
+ "TestNSSCipherStrategy.cpp",
"TestNSSRandomAccessCipherStrategy.cpp",
"TestOpenClientDirectoryInfo.cpp",
"TestOriginParser.cpp",
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/dom/quota/test/gtest/TestNSSCipherStrategy.cpp b/dom/quota/test/gtest/TestNSSCipherStrategy.cpp
new file mode 100644
index 00000000000..4657c3467a4
--- /dev/null
+++ b/dom/quota/test/gtest/TestNSSCipherStrategy.cpp
@@ -0,0 +1,189 @@
+/* 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 <array>
+#include <cstdint>
+#include <numeric>
+#include <set>
+#include <vector>
+
+#include "NSSCipherStrategy.h"
+#include "gtest/gtest.h"
+#include "mozilla/Span.h"
+
+namespace mozilla::dom::quota::test {
+
+namespace {
+
+constexpr size_t kPayloadLength = 64;
+constexpr size_t kNonceLength = 12;
+
+using IvArray = std::array<uint8_t, NSSCipherStrategy::BlockPrefixLength>;
+using Payload = std::array<uint8_t, kPayloadLength>;
+
+NSSCipherStrategy::KeyType MakeKey(uint8_t aInitialValue = 1) {
+ NSSCipherStrategy::KeyType key;
+ std::iota(key.begin(), key.end(), aInitialValue);
+ return key;
+}
+
+Payload MakePlaintext(uint8_t aInitialValue = 1) {
+ Payload plaintext;
+ std::iota(plaintext.begin(), plaintext.end(), aInitialValue);
+ return plaintext;
+}
+
+std::vector<uint8_t> NonceOf(const IvArray& aIv) {
+ return std::vector<uint8_t>(aIv.begin(), aIv.begin() + kNonceLength);
+}
+
+} // namespace
+
+TEST(NSSCipherStrategyTest, NonceDiffersAcrossCallsInOneContext)
+{
+ const auto key = MakeKey();
+ const auto plaintext = MakePlaintext();
+
+ NSSCipherStrategy encrypt;
+ ASSERT_EQ(NS_OK, encrypt.Init(CipherMode::Encrypt,
+ NSSCipherStrategy::SerializeKey(key)));
+
+ std::set<std::vector<uint8_t>> nonces;
+ for (size_t i = 0; i < 32; ++i) {
+ IvArray iv{};
+ Payload ciphertext{};
+ ASSERT_EQ(NS_OK,
+ encrypt.Cipher(Span{iv}, Span{plaintext}, Span{ciphertext}));
+ EXPECT_TRUE(nonces.insert(NonceOf(iv)).second)
+ << "nonce repeated within a single context on call " << i;
+ }
+}
+
+// Regression test for bug 2055694. ObfuscatingVFS creates a separate encrypt
+// context per SQLite file handle and per reopen, all sharing one key. When the
+// nonce came from a per-context counter starting at zero, those contexts
+// produced identical (key, nonce) pairs, so XOR of two ciphertexts disclosed
+// XOR of their plaintexts.
+TEST(NSSCipherStrategyTest, NonceDiffersAcrossContextsWithTheSameKey)
+{
+ const auto key = MakeKey();
+ const auto plaintext = MakePlaintext();
+
+ const auto encryptOnce = [&key, &plaintext](IvArray& aIv,
+ Payload& aCiphertext) {
+ NSSCipherStrategy encrypt;
+ ASSERT_EQ(NS_OK, encrypt.Init(CipherMode::Encrypt,
+ NSSCipherStrategy::SerializeKey(key)));
+ ASSERT_EQ(NS_OK,
+ encrypt.Cipher(Span{aIv}, Span{plaintext}, Span{aCiphertext}));
+ };
+
+ IvArray firstIv{};
+ Payload firstCiphertext{};
+ encryptOnce(firstIv, firstCiphertext);
+
+ IvArray secondIv{};
+ Payload secondCiphertext{};
+ encryptOnce(secondIv, secondCiphertext);
+
+ EXPECT_NE(NonceOf(firstIv), NonceOf(secondIv));
+ EXPECT_NE(firstCiphertext, secondCiphertext);
+}
+
+// Everything ahead of the tag is generated by Cipher, so a caller that leaves
+// the block prefix uninitialized cannot leak its contents to disk.
+TEST(NSSCipherStrategyTest, OverwritesTheWholePrefixRegardlessOfCallerInput)
+{
+ const auto key = MakeKey();
+ const auto plaintext = MakePlaintext();
+
+ NSSCipherStrategy encrypt;
+ ASSERT_EQ(NS_OK, encrypt.Init(CipherMode::Encrypt,
+ NSSCipherStrategy::SerializeKey(key)));
+
+ constexpr size_t kTagLength = 16;
+ constexpr uint8_t kSentinel = 0xAA;
+
+ std::set<std::vector<uint8_t>> fillers;
+ for (size_t i = 0; i < 32; ++i) {
+ IvArray iv;
+ iv.fill(kSentinel);
+ Payload ciphertext{};
+ ASSERT_EQ(NS_OK,
+ encrypt.Cipher(Span{iv}, Span{plaintext}, Span{ciphertext}));
+
+ fillers.emplace(iv.begin() + kNonceLength, iv.end() - kTagLength);
+ }
+
+ // Had the filler bytes been left to the caller, every sample would still
+ // hold the sentinel and this set would have exactly one entry.
+ EXPECT_GT(fillers.size(), 1u);
+}
+
+TEST(NSSCipherStrategyTest, RoundTripsUsingTheStoredNonce)
+{
+ const auto key = MakeKey();
+ const auto plaintext = MakePlaintext();
+
+ IvArray iv{};
+ Payload ciphertext{};
+ {
+ NSSCipherStrategy encrypt;
+ ASSERT_EQ(NS_OK, encrypt.Init(CipherMode::Encrypt,
+ NSSCipherStrategy::SerializeKey(key)));
+ ASSERT_EQ(NS_OK,
+ encrypt.Cipher(Span{iv}, Span{plaintext}, Span{ciphertext}));
+ }
+ EXPECT_NE(plaintext, ciphertext);
+
+ Payload decrypted{};
+ {
+ NSSCipherStrategy decrypt;
+ ASSERT_EQ(NS_OK, decrypt.Init(CipherMode::Decrypt,
+ NSSCipherStrategy::SerializeKey(key)));
+ ASSERT_EQ(NS_OK,
+ decrypt.Cipher(Span{iv}, Span{ciphertext}, Span{decrypted}));
+ }
+ EXPECT_EQ(plaintext, decrypted);
+}
+
+// Decryption must depend only on the nonce stored alongside the ciphertext,
+// never on a context-local counter. This is what keeps blocks written by older
+// builds -- whose nonces were sequential -- readable.
+TEST(NSSCipherStrategyTest, DecryptionIsIndependentOfBlockOrder)
+{
+ const auto key = MakeKey();
+ const auto firstPlaintext = MakePlaintext(1);
+ const auto secondPlaintext = MakePlaintext(101);
+
+ IvArray firstIv{};
+ Payload firstCiphertext{};
+ IvArray secondIv{};
+ Payload secondCiphertext{};
+ {
+ NSSCipherStrategy encrypt;
+ ASSERT_EQ(NS_OK, encrypt.Init(CipherMode::Encrypt,
+ NSSCipherStrategy::SerializeKey(key)));
+ ASSERT_EQ(NS_OK, encrypt.Cipher(Span{firstIv}, Span{firstPlaintext},
+ Span{firstCiphertext}));
+ ASSERT_EQ(NS_OK, encrypt.Cipher(Span{secondIv}, Span{secondPlaintext},
+ Span{secondCiphertext}));
+ }
+
+ NSSCipherStrategy decrypt;
+ ASSERT_EQ(NS_OK, decrypt.Init(CipherMode::Decrypt,
+ NSSCipherStrategy::SerializeKey(key)));
+
+ Payload secondDecrypted{};
+ ASSERT_EQ(NS_OK, decrypt.Cipher(Span{secondIv}, Span{secondCiphertext},
+ Span{secondDecrypted}));
+ EXPECT_EQ(secondPlaintext, secondDecrypted);
+
+ Payload firstDecrypted{};
+ ASSERT_EQ(NS_OK, decrypt.Cipher(Span{firstIv}, Span{firstCiphertext},
+ Span{firstDecrypted}));
+ EXPECT_EQ(firstPlaintext, firstDecrypted);
+}
+
+} // namespace mozilla::dom::quota::test
diff --git a/dom/quota/test/gtest/moz.build b/dom/quota/test/gtest/moz.build
index 40fc5e90a4b..344a44804d1 100644
--- a/dom/quota/test/gtest/moz.build
+++ b/dom/quota/test/gtest/moz.build
@@ -30,6 +30,7 @@ UNIFIED_SOURCES = [
"TestFlatten.cpp",
"TestForwardDecls.cpp",
"TestMozPromiseUtils.cpp",
+ "TestNSSCipherStrategy.cpp",
"TestNSSRandomAccessCipherStrategy.cpp",
"TestOpenClientDirectoryInfo.cpp",
"TestOriginParser.cpp",
Loading diff…
References
On This Page