using namespace mozilla;
using namespace mozilla::dom::quota;
// Derived from the cipher's own KeyType so this file stays correct if the // cipher ever switches to a different key length. Matches the canonical // `static_assert(sizeof(NSSCipherStrategy::KeyType) == 32)` in // dom/quota/NSSCipherStrategy.cpp. static constexpr int kKeyBytes = sizeof(IPCStreamCipherStrategy::KeyType);
/* An open file */ struct ObfsFile {
sqlite3_file base; /* IO methods */ constchar* zFName; /* Original name of the file */ bool inCkpt; /* Currently doing a checkpoint */
ObfsFile* pPartner; /* Ptr from WAL to main-db, or from main-db to WAL */ void* pTemp; /* Temporary storage for encoded pages */
IPCStreamCipherStrategy*
encryptCipherStrategy; /* CipherStrategy for encryption */
IPCStreamCipherStrategy*
decryptCipherStrategy; /* CipherStrategy for decryption */ /* For a main DB opened with an explicit URI ?key= whose key is NOT in **lockstore(private-browsingIDB/Cachepassanephemeral&key=),retainthe **rawkeysothekeylessWAL/journalopenedlatercaninherititvia **sqlite3_database_file_object().Thepolicybranchcannotre-deriveakey **lockstoreneverstored.Lockstore-keyedDBsleaveaHasUriKeyfalseand
** re-fetch on demand (nothing cached). Zeroized in obfsClose. */ bool aHasUriKey;
u8 aKey[kKeyBytes];
};
/* **MethodsforObfsFile
*/ staticint obfsClose(sqlite3_file*); staticint obfsRead(sqlite3_file*, void*, int iAmt, sqlite3_int64 iOfst); staticint obfsWrite(sqlite3_file*, constvoid*, int iAmt, sqlite3_int64 iOfst); staticint obfsTruncate(sqlite3_file*, sqlite3_int64 size); staticint obfsSync(sqlite3_file*, int flags); staticint obfsFileSize(sqlite3_file*, sqlite3_int64* pSize); staticint obfsLock(sqlite3_file*, int); staticint obfsUnlock(sqlite3_file*, int); staticint obfsCheckReservedLock(sqlite3_file*, int* pResOut); staticint obfsFileControl(sqlite3_file*, int op, void* pArg); staticint obfsSectorSize(sqlite3_file*); staticint obfsDeviceCharacteristics(sqlite3_file*); staticint obfsShmMap(sqlite3_file*, int iPg, int pgsz, int, voidvolatile**); staticint obfsShmLock(sqlite3_file*, int offset, int n, int flags); staticvoid obfsShmBarrier(sqlite3_file*); staticint obfsShmUnmap(sqlite3_file*, int deleteFlag); staticint obfsFetch(sqlite3_file*, sqlite3_int64 iOfst, int iAmt, void** pp); staticint obfsUnfetch(sqlite3_file*, sqlite3_int64 iOfst, void* p);
static constexpr int kIvBytes = IPCStreamCipherStrategy::BlockPrefixLength; static constexpr int kClearTextPrefixBytesOnFirstPage = 32; static constexpr int kReservedBytes = 32; static constexpr int kBasicBlockSize = IPCStreamCipherStrategy::BasicBlockSize;
static_assert(kClearTextPrefixBytesOnFirstPage % kBasicBlockSize == 0);
static_assert(kReservedBytes % kBasicBlockSize == 0);
// The pager validates that no other connection has modified the database by // reading the 16-byte "file change counter" region at offset 24 of page 1 // directly from the file on every shared-lock acquisition (pager.c, the // "CKVERS" read). It compares those bytes against the value it cached from // page 1 the last time it was read. static constexpr int kChangeCounterOffset = 24; static constexpr int kChangeCounterBytes = 16;
/* Obfuscate a page using p->encryptCipherStrategy. ** **Anewrandomnonceiscreatedandstoredinthelast32bytes **ofthepage.Allotherbytesofthepageareobfuscastedusingthe **CipherStrategy.Except,forpage-1(includingtheSQLite **databaseheader)thefirst32bytesarenotobfuscated ** **Returnapointertotheobfuscatedcontent,whichisheldinthe **p->pTempbuffer.OrreturnaNULLpointerifsomethinggoeswrong. **ErrorsarereportedusingNS_WARNING().
*/ staticvoid* obfsEncode(ObfsFile* p, /* File containing page to be obfuscated */
u8* a, /* database page to be obfuscated */ int nByte /* Bytes of content in a[]. Must be a multiple
of kBasicBlockSize. */
) {
u8 aIv[kIvBytes];
u8* pOut; int i;
static_assert((kIvBytes & (kIvBytes - 1)) == 0);
sqlite3_randomness(kIvBytes, aIv);
pOut = (u8*)p->pTemp; if (pOut == nullptr) {
pOut = static_cast<u8*>(sqlite3_malloc64(nByte)); if (pOut == nullptr) {
NS_WARNING(nsPrintfCString("unable to allocate a buffer in which to" " write obfuscated database content for %s",
p->zFName)
.get()); return nullptr;
}
p->pTemp = pOut;
} if (memcmp(a, "SQLite format 3", 16) == 0) {
i = kClearTextPrefixBytesOnFirstPage; if (a[20] != kReservedBytes) {
NS_WARNING(nsPrintfCString("obfuscated database must have reserved-bytes" " set to %d",
kReservedBytes)
.get()); return nullptr;
}
memcpy(pOut, a, kClearTextPrefixBytesOnFirstPage);
} else {
i = 0;
} constint payloadLength = nByte - kReservedBytes - i;
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)});
memcpy(pOut + nByte - kReservedBytes, aIv, kIvBytes);
return pOut;
}
/* De-obfuscate a page using p->decryptCipherStrategy. ** **Thedeobfuscationisdonein-place. ** **ForpagesthatbeginwiththeSQLiteheadertext,thefirst **32bytesarenotdeobfuscated.
*/ staticvoid obfsDecode(ObfsFile* p, /* File containing page to be obfuscated */
u8* a, /* database page to be obfuscated */ int nByte /* Bytes of content in a[]. Must be a multiple
of kBasicBlockSize. */
) { int i;
if (memcmp(a, "SQLite format 3", 16) == 0) {
i = kClearTextPrefixBytesOnFirstPage;
} else {
i = 0;
} constint payloadLength = nByte - kReservedBytes - i;
MOZ_ASSERT(payloadLength > 0);
p->decryptCipherStrategy->Cipher(
Span{a + nByte - kReservedBytes, kIvBytes},
Span{a + i, static_cast<unsigned>(payloadLength)},
Span{a + i, static_cast<unsigned>(payloadLength)});
memset(a + nByte - kReservedBytes, 0, kIvBytes);
}
// Wipe any retained URI key so a closed slot can't leak to a later reader of // freed pager memory (a no-op for lockstore-keyed files, which never set it).
::memset(p->aKey, 0, sizeof(p->aKey));
/* **Readdatafromanobfuscatedfile. ** **Ifthefileislessthanonefullpageinlength,thenreturn **asubstitute"prototype"page-1.Thisprototypepageone **specifiesadatabaseinWALmodewithan8192-bytepagesize **anda32-bytereserved-bytesvalue.Thosesettingsarenecessary **forobfuscationtofunctioncorrectly.
*/ staticint obfsRead(sqlite3_file* pFile, void* zBuf, int iAmt,
sqlite_int64 iOfst) { int rc;
ObfsFile* p = (ObfsFile*)pFile;
pFile = ORIGFILE(pFile);
// Serve the pager's change-counter validation read (page 1, offset 24, 16 // bytes) from a decoded copy of page 1. The pager caches the decoded value // (pager.c readDbPage), but bytes 32..39 of this region are encrypted on // disk, so a raw read never matches the cache: the pager concludes the file // changed, resets its cache, and -- fatally for an in-progress online // backup -- restarts the copy on every step, so a multi-step backup never // completes. Decoding page 1 here makes both sides of the comparison // plaintext while still reflecting a genuine change (the change counter at // bytes 24..27 lives in the cleartext prefix). Fall through to the raw read // when page 1 is not yet a valid SQLite header (e.g. an empty file), which // is the behaviour the pager already expects. if (!p->inCkpt && iOfst == kChangeCounterOffset &&
iAmt == kChangeCounterBytes) {
u8 aPage1[OBFS_PGSZ];
rc = pFile->pMethods->xRead(pFile, aPage1, OBFS_PGSZ, 0); if (rc == SQLITE_OK && memcmp(aPage1, "SQLite format 3", 16) == 0) {
obfsDecode(p, aPage1, OBFS_PGSZ);
memcpy(zBuf, aPage1 + kChangeCounterOffset, kChangeCounterBytes); return SQLITE_OK;
}
}
rc = pFile->pMethods->xRead(pFile, zBuf, iAmt, iOfst); if (rc == SQLITE_OK) { if ((iAmt == OBFS_PGSZ || iAmt == OBFS_PGSZ + WAL_FRAMEHDRSIZE) &&
!p->inCkpt) {
obfsDecode(p, ((u8*)zBuf) + iAmt - OBFS_PGSZ, OBFS_PGSZ);
}
} elseif (rc == SQLITE_IOERR_SHORT_READ && iOfst == 0 && iAmt >= 100) { staticconstunsignedchar aEmptyDb[] = { // Offset 0, Size 16, The header string: "SQLite format 3\000" 0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x20, 0x33, 0x00, // XXX Add description for other fields 0x20, 0x00, 0x02, 0x02, kReservedBytes, 0x40, 0x20, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Offset 52, Size 4, The page number of the largest root b-tree page // when in auto-vacuum or incremental-vacuum modes, or zero otherwise. 0x00, 0x00, 0x00, 0x01};
/* **Returnthedevicecharacteristicflagssupportedbyanobfuscatedfile.
*/ staticint obfsDeviceCharacteristics(sqlite3_file* pFile) { int dc;
pFile = ORIGFILE(pFile);
dc = pFile->pMethods->xDeviceCharacteristics(pFile); return dc & ~SQLITE_IOCAP_SUBPAGE_READ; /* All except the
SQLITE_IOCAP_SUBPAGE_READ bit */
}
/* Create a shared memory file mapping */ staticint obfsShmMap(sqlite3_file* pFile, int iPg, int pgsz, int bExtend, voidvolatile** pp) {
pFile = ORIGFILE(pFile); return pFile->pMethods->xShmMap(pFile, iPg, pgsz, bExtend, pp);
}
/* Perform locking on a shared-memory segment */ staticint obfsShmLock(sqlite3_file* pFile, int offset, int n, int flags) {
pFile = ORIGFILE(pFile); return pFile->pMethods->xShmLock(pFile, offset, n, flags);
}
// The probe reads the leading header bytes that carry the magic (offsets // 0-15) plus the page-size, file-format, and reserved-bytes fields (16-23) // that distinguish an obfsvfs-encrypted file from a plain SQLite database. static constexpr size_t kOnDiskHeaderProbeBytes = 24;
static OnDiskHeader PeekOnDiskHeader(constchar* zPath) {
FILE* f = fopen(zPath, "rb"); if (!f) { // Distinguish a genuinely absent file (Missing -- the no-CREATE probe // below forwards it to the lower VFS) from any other open failure // (permissions, too many open files, ...). Collapsing the latter to // Missing could forward a plaintext open of a database we merely failed to // inspect, so report Error and let obfsOpen refuse. return errno == ENOENT ? OnDiskHeader::Missing : OnDiskHeader::Error;
} unsignedchar hdr[kOnDiskHeaderProbeBytes] = {0};
size_t n = fread(hdr, 1, sizeof(hdr), f);
fclose(f); if (n != sizeof(hdr)) { return OnDiskHeader::TooShort;
} staticconstunsignedchar kSQLiteMagic[16] = {'S', 'Q', 'L', 'i', 't', 'e', ' ', 'f', 'o', 'r', 'm', 'a', 't', ' ', '3', '\0'}; if (::memcmp(hdr, kSQLiteMagic, sizeof(kSQLiteMagic)) != 0) { // Not a SQLite-shaped file (corrupt, or some other format). No useful // signal for the policy/disk mismatch check; treat as TooShort. return OnDiskHeader::TooShort;
}
uint32_t pageSize = (static_cast<uint32_t>(hdr[16]) << 8) | hdr[17]; if (pageSize == 1) {
pageSize = 65536;
}
uint32_t reservedBytes = hdr[20]; // obfsvfs signature: page_size == OBFS_PGSZ (8192) AND reserved == 32. if (pageSize == OBFS_PGSZ && reservedBytes == 32) { return OnDiskHeader::Encrypted;
} // Any other SQLite-shaped file (a different page size or reserved-bytes // value, i.e. not the obfsvfs signature) is treated as a plain SQLite // database. return OnDiskHeader::Plaintext;
}
/* For a journal/WAL open, strip the SQLite-internal suffix so the policy **lookupiskeyedonthemainDB'spath.SQLite-internalsuffixes: **SQLITE_OPEN_MAIN_JOURNAL--"-journal" **SQLITE_OPEN_WAL--"-wal" **ForSQLITE_OPEN_MAIN_DBtheoriginalzNameisthepolicykey.
*/ static nsAutoCString DeriveMainDbPath(constchar* zName, int flags) {
nsAutoCString p(zName); if (flags & SQLITE_OPEN_MAIN_JOURNAL) {
constexpr auto kJournal = "-journal"_ns; if (StringEndsWith(p, kJournal)) {
p.Truncate(p.Length() - kJournal.Length());
}
} elseif (flags & SQLITE_OPEN_WAL) {
constexpr auto kWal = "-wal"_ns; if (StringEndsWith(p, kWal)) {
p.Truncate(p.Length() - kWal.Length());
}
} return p;
}
/* Pure-string fast bypass for the bootstrap files. Must run BEFORE any **storage-sidecall--inparticularbeforeGetDatabaseEncryptionStatus, **whichacquiressStateMutexviaGetCachedProfilePath.Thebypasspathis **triggeredwhenGetEncryptionKey(alreadyholdingsStateMutex)opens **lockstore.keys.sqliteviaskv->rusqlite->ourobfsvfsxOpen;without **thisfastbypassthesamethreadwouldrecurseintosStateMutexand **MOZ_CRASHonresource-deadlock-avoided. ** **SamereasoningforNSS'sowndatabases:libnss3'sbundledSQLiteshares **theglobalVFSnamespacewithours,soitskey4.db/cert9.dbopens **duringNSS_InitializelandinobfsOpentoo--andthestorage-side **policylookupwouldre-enterNSSvialockstore,deadlockingthestill- **runningNSSinit.BypassingthesenameskeepsNSS'sbootstrapfully **insulatedfromourat-restlayer. ** **Operatesonthepost-suffix-strippedpath,sojournal/WALopens(e.g. **"lockstore.keys.sqlite-wal")matchtoo.
*/ staticbool IsBootstrapBypassPath(const nsACString& aMainDbPath) { return mozilla::storage::IsBootstrapDatabasePath(aMainDbPath);
}
// SQLite opens a connection's rollback journal / WAL without the main DB's // URI query params, so they arrive keyless. Two key sources, in order: // 1. Partner inheritance (below): a main DB opened with an explicit &key= // whose key is NOT in lockstore (private-browsing IDB/Cache) retains it // on its ObfsFile so its keyless WAL/journal can reuse it. // 2. The policy branch: lockstore-keyed in-profile DBs re-derive the same // per-DB key from lockstore on demand (DeriveMainDbPath strips the // -wal/-journal suffix). lockstore stays alive until XPCOMWillShutdown, // so this works even for the final checkpoint at shutdown -- nothing is // cached for these. bool keyReady = false; if (zKey == nullptr &&
(flags & (SQLITE_OPEN_WAL | SQLITE_OPEN_MAIN_JOURNAL))) {
sqlite3_file* pDbFile = sqlite3_database_file_object(zName); if (pDbFile && pDbFile->pMethods == &obfs_io_methods) {
ObfsFile* pPartner = reinterpret_cast<ObfsFile*>(pDbFile); if (pPartner->aHasUriKey) { // Main DB carries an explicit URI key absent from lockstore; inherit // it rather than fall through to a policy lookup that would mint a // different key and corrupt the journal.
::memcpy(aKey, pPartner->aKey, sizeof(aKey));
keyReady = true;
}
}
}
// Owns the lifetime of a policy-derived hex key, when we take that branch. // Declared here so the c_str() returned through zKey stays valid until // the existing hex-validation loop below has finished consuming it.
nsAutoCString policyKey;
if (!keyReady && zKey == nullptr &&
(flags &
(SQLITE_OPEN_MAIN_DB | SQLITE_OPEN_WAL | SQLITE_OPEN_MAIN_JOURNAL))) { // No URI key. With obfsvfs registered as SQLite's default VFS, this is // the path every keyless main/WAL/journal open lands in. Apply the // at-rest encryption *policy*; never silently fall back to plaintext // for an in-profile DB whose policy says it must be encrypted.
mozilla::LogModule* log = mozilla::storage::GetSQLiteEncryptionLog();
nsAutoCString dbPath = DeriveMainDbPath(zName, flags);
// Fast bootstrap bypass -- runs BEFORE any storage-side call so it // never touches sStateMutex. See IsBootstrapBypassPath for why this // matters (would otherwise deadlock when GetEncryptionKey re-enters // through skv, or recurse into NSS_Initialize via lockstore). if (IsBootstrapBypassPath(dbPath)) { return pSubVfs->xOpen(pSubVfs, zName, pFile, flags, pOutFlags);
}
mozilla::storage::EncryptionStatus status =
mozilla::storage::EncryptionStatus::Unset;
nsresult rv = mozilla::storage::GetDatabaseEncryptionStatus(dbPath, status); if (NS_FAILED(rv)) {
MOZ_LOG(log, mozilla::LogLevel::Error,
("obfsOpen: policy lookup failed (0x%" PRIx32 ") for %s; " "refusing open rather than risking plaintext fallback",
static_cast<uint32_t>(rv), zName)); return SQLITE_CANTOPEN;
} if (status == mozilla::storage::EncryptionStatus::Unset) { // Defensive: GetDatabaseEncryptionStatus sets a real value on every // NS_OK path today, so this only fires if a future edit returns success // without deciding. Refuse rather than fall through to a key lookup or a // plaintext forward on an undecided policy.
MOZ_LOG(log, mozilla::LogLevel::Error,
("obfsOpen: policy lookup left status unset for %s; refusing",
zName)); return SQLITE_CANTOPEN;
}
if (disk == OnDiskHeader::Error) { // The file exists but its header could not be read (a non-ENOENT fopen // failure). Never guess encrypted vs plaintext on an ambiguous open // failure -- refuse rather than risk a plaintext write over ciphertext.
MOZ_LOG(log, mozilla::LogLevel::Error,
("obfsOpen: could not inspect on-disk header for %s; refusing",
zName)); return SQLITE_CANTOPEN;
}
// No-CREATE open of a non-existent file (typical of app-services // read-only probes that expect to retry with CREATE on failure): // forward to the lower VFS so SQLite returns its natural // file-not-found error rather than our keystore-derived // SQLITE_CANTOPEN. Without this, the read-only probe would // unnecessarily fire the re-unlock path and hard-error before the // caller's retry-with-CREATE could re-enter and mint the DEK. if (isMainDb && disk == OnDiskHeader::Missing &&
!(flags & SQLITE_OPEN_CREATE)) { return pSubVfs->xOpen(pSubVfs, zName, pFile, flags, pOutFlags);
}
if (status == mozilla::storage::EncryptionStatus::Plaintext) { // Out-of-profile, or the explicit bootstrap bypass for // lockstore.keys.sqlite. Defense in depth: refuse to forward to the // lower VFS plaintext when the file is already encrypted-shaped on // disk, because that means policy regressed and a plaintext write // would silently corrupt the existing ciphertext. if (disk == OnDiskHeader::Encrypted) {
MOZ_LOG(log, mozilla::LogLevel::Error,
("obfsOpen: policy says plaintext but on-disk header is " "encrypted for %s; refusing",
zName)); return SQLITE_CANTOPEN_FULLPATH;
} return pSubVfs->xOpen(pSubVfs, zName, pFile, flags, pOutFlags);
}
// status == Encrypted: derive the DEK from the keystore.
mozilla::storage::OpenIntent intent =
(flags & SQLITE_OPEN_CREATE)
? mozilla::storage::OpenIntent::CreateIfNew
: mozilla::storage::OpenIntent::LoadExisting;
rv = mozilla::storage::GetEncryptionKey(dbPath, intent, policyKey); if (NS_FAILED(rv)) {
MOZ_LOG(log, mozilla::LogLevel::Error,
("obfsOpen: GetEncryptionKey 0x%" PRIx32 " for %s; refusing open (no plaintext fallback)",
static_cast<uint32_t>(rv), zName)); return SQLITE_CANTOPEN;
} if (disk == OnDiskHeader::Plaintext) {
MOZ_LOG(log, mozilla::LogLevel::Error,
("obfsOpen: policy says encrypted but on-disk header is " "plaintext SQLite for %s; refusing rather than write " "ciphertext over an existing plaintext DB",
zName)); return SQLITE_CANTOPEN_FULLPATH;
}
zKey = policyKey.get();
}
if (!keyReady && zKey == nullptr) { return pSubVfs->xOpen(pSubVfs, zName, pFile, flags, pOutFlags);
} if (!keyReady) { for (i = 0;
i < kKeyBytes && isxdigit(zKey[i * 2]) && isxdigit(zKey[i * 2 + 1]);
i++) {
aKey[i] =
(obfsHexToInt(zKey[i * 2]) << 4) | obfsHexToInt(zKey[i * 2 + 1]);
} if (i != kKeyBytes) {
NS_WARNING(
nsPrintfCString("invalid query parameter on %s: key=%s", zName, zKey)
.get()); return SQLITE_CANTOPEN;
}
}
p = (ObfsFile*)pFile;
memset(p, 0, sizeof(*p));
auto encryptCipherStrategy = MakeUnique<IPCStreamCipherStrategy>(); auto decryptCipherStrategy = MakeUnique<IPCStreamCipherStrategy>();
auto resetMethods = MakeScopeExit([pFile] { pFile->pMethods = nullptr; });
if (NS_WARN_IF(NS_FAILED(encryptCipherStrategy->Init(
CipherMode::Encrypt, Span{aKey, sizeof(aKey)},
IPCStreamCipherStrategy::MakeBlockPrefix())))) { return SQLITE_ERROR;
}
if (NS_WARN_IF(NS_FAILED(decryptCipherStrategy->Init(
CipherMode::Decrypt, Span{aKey, sizeof(aKey)})))) { return SQLITE_ERROR;
}
if (keyFromUri) { // Retain the explicit URI key so this main DB's keyless WAL/journal can // inherit it; the policy branch cannot re-derive a non-lockstore key. // Success path only: the SQLITE_ERROR bailouts above leave pFile with null // methods (obfsClose never runs), so a key copied earlier would linger // uncleaned in the pager's ObfsFile memory.
p->aHasUriKey = true;
::memcpy(p->aKey, aKey, sizeof(aKey));
}
#ifdef DEBUG // If the VFS version is higher than the last known one, you should update // this VFS adding appropriate methods for any methods added in the version // change. static constexpr int kLastKnownVfsVersion = 3;
MOZ_ASSERT(pOrig->iVersion <= kLastKnownVfsVersion); #endif
¤ Diese beiden folgenden Angebotsgruppen bietet das Unternehmen0.32Angebot
(Wie Sie bei der Firma Beratungs- und Dienstleistungen beauftragen können 2026-08-25)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.