Bug 1996558 - Encrypt SQLite databases from mozStorageService r=simonf,gcp,jari,bbeurdouche,dom-storage-reviewers

This patch is the baseline, follow-up fixes for comments have been added in the child stack.

Differential Revision: https://phabricator.services.mozilla.com/D270165
This commit is contained in:
Nikolas Wipper
2026-06-16 13:00:13 +00:00
committed by bbeurdouche@mozilla.com
parent 07b8152b97
commit eea8f31b75
27 changed files with 1188 additions and 21 deletions
@@ -39,3 +39,7 @@ support-files = [
skip-if = [
"os == 'linux' && os_version == '22.04' && arch == 'x86_64' && display == 'wayland'", # 1924781: Gnome keyring not unlocked
]
# Restores pre-existing unencrypted backup fixture archives; enabling
# SQLite encryption on the restoring profile makes the compatibility
# assertions meaningless.
prefs = ["security.storage.encryption.sqlite.enabled=false"]
+7
View File
@@ -243,6 +243,13 @@ Connection::CreateTable(const char* aTable, const char* aSchema) {
return mBase->CreateTable(aTable, aSchema);
}
NS_IMETHODIMP
Connection::AttachDatabase(const char* aPath, const char* aName,
mozIStorageStatementCallback* aCallback,
mozIStoragePendingStatement** _handle) {
return mBase->AttachDatabase(aPath, aName, aCallback, _handle);
}
NS_IMETHODIMP
Connection::SetGrowthIncrement(int32_t aIncrement,
const nsACString& aDatabase) {
+4
View File
@@ -3,6 +3,10 @@ subsuite = "integration"
tags = "inc-origin-init os_integration"
["test_cacheapi_encryption_PBM.py"]
# This test asserts the PBM-specific encryption layer (QuotaVFS) is the one
# writing ciphertext to disk. Enabling the SQLite-level obfsvfs encryption
# layers a second cipher on top, which breaks the test's file-content checks.
prefs = ["security.storage.encryption.sqlite.enabled=false"]
["test_caches_delete_cleanup_after_shutdown.py"]
skip-if = [
+1 -1
View File
@@ -105,7 +105,7 @@ function fetchOpaqueResponse(url) {
// [3] https://searchfox.org/firefox-main/rev/3aff965e839b7872bf48a9803b80669ca49276ac/security/manager/ssl/TransportSecurityInfo.cpp#114
// [4] https://searchfox.org/firefox-main/rev/3aff965e839b7872bf48a9803b80669ca49276ac/dom/cache/DBSchema.cpp#255
function equalOrOffByOneGrowthChunk(a, b, message) {
if (SpecialPowers.getBoolPref("browser.privatebrowsing.autostart") && a != b) {
if ((SpecialPowers.getBoolPref("browser.privatebrowsing.autostart") || SpecialPowers.getBoolPref("security.storage.encryption.sqlite.enabled")) && a != b) {
b += 32768;
}
is(a, b, message);
@@ -1,5 +1,9 @@
[DEFAULT]
subsuite = "integration"
tags = "inc-origin-init os_integration"
# test_IDB_encryption_PBM verifies the QuotaVFS encryption layer writes
# ciphertext for PBM DBs; stacking obfsvfs on top would double-encrypt and
# break the on-disk checks.
prefs = ["security.storage.encryption.sqlite.enabled=false"]
["test_IDB_encryption_PBM.py"]
+9
View File
@@ -18860,6 +18860,15 @@
value: true
mirror: always
# Enable sqlite database encryption. Mirrored "always" so that browser-chrome
# tests that set the pref via their manifest see the updated value even when
# Firefox was launched with the default. In practice the pref is read during
# mozStorageService::Init and each Connection::initialize, not hot-reloaded.
- name: security.storage.encryption.sqlite.enabled
type: RelaxedAtomicBool
value: false
mirror: always
- name: security.tls13.aes_128_gcm_sha256
type: RelaxedAtomicBool
value: true
+8 -1
View File
@@ -13,7 +13,7 @@ use idna::uts46::ProcessingSuccess;
use idna::uts46::Uts46;
use nserror::*;
use nsstring::*;
use percent_encoding::percent_decode;
use percent_encoding::{percent_decode, percent_encode, AsciiSet, CONTROLS};
/// The URL deny list plus asterisk and double quote.
/// Using AsciiDenyList::URL is https://bugzilla.mozilla.org/show_bug.cgi?id=1815926 .
@@ -276,3 +276,10 @@ pub unsafe extern "C" fn mozilla_net_recover_keyword_from_punycode(
}
}
}
/// Percent-encodes a string.
#[no_mangle]
pub unsafe extern "C" fn mozilla_net_percent_encode(src: &nsACString, dst: &mut nsACString) {
static CHARS: AsciiSet = CONTROLS.add(b'%');
dst.assign(&percent_encode(src, &CHARS).to_string())
}
+333
View File
@@ -0,0 +1,333 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
* 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 "KeyStorage.h"
#include "Persist.h"
#include "Profile.h"
#include "GMPUtils.h"
#include "pk11sdr.h"
#include "nsLocalFile.h"
#include "nsTHashMap.h"
#include "mozilla/Base64.h"
#include "mozilla/Logging.h"
#include "mozilla/StaticPrefs_security.h"
namespace mozilla::storage::key {
mozilla::StaticMutex sKeyMutex;
constinit static nsTHashMap<nsCString, Key> sKeyMap;
constinit static mozilla::UniquePK11SymKey sSystemKey;
nsresult Init() {
if (StaticPrefs::security_storage_encryption_sqlite_enabled()) {
if (!EnsureNSSInitializedChromeOrContent()) {
return NS_ERROR_FAILURE;
}
}
InitObserver();
return NS_OK;
}
void Shutdown() {
mozilla::StaticMutexAutoLock lock(sKeyMutex);
sSystemKey.reset(nullptr);
sKeyMap.Clear();
}
mozilla::LogModule* GetKeyStorageLog() {
static mozilla::LazyLogModule sLog("KeyStorage");
return sLog;
}
/// Create "system" key
/// `keyOut` will contain the SDR encrypted key bytes
/// `SYSTEM_KEY` will the a AES-GCM PK11SymKey created from those bytes
nsresult CreateSystemKey(Key& aKeyOut) {
sKeyMutex.AssertCurrentThreadOwns();
// Every key is 32 bytes long
mozilla::UniqueSECItem key =
mozilla::UniqueSECItem(::SECITEM_AllocItem(nullptr, nullptr, 32));
if (!key) return NS_ERROR_FAILURE;
// Key data is random
SECStatus stat = PK11_GenerateRandom(key->data, key->len);
if (stat != SECSuccess) return MapSECStatus(stat);
mozilla::UniquePK11SlotInfo slot(PK11_GetInternalSlot());
if (!slot) return NS_ERROR_FAILURE;
sSystemKey = mozilla::UniquePK11SymKey(
PK11_ImportSymKey(slot.get(), CKM_AES_GCM, PK11_OriginUnwrap,
CKA_ENCRYPT | CKA_DECRYPT, key.get(), nullptr));
if (!sSystemKey) return NS_ERROR_FAILURE;
// Use the default SDR key
SECItem keyid = {siBuffer, nullptr, 0};
// PK11SDR_EncryptWithMechanism will allocate the needed buffer in SECItem
mozilla::UniqueSECItem encryptedKey(::SECITEM_AllocItem(nullptr, nullptr, 0));
stat = PK11SDR_EncryptWithMechanism(nullptr, &keyid, CKM_AES_CBC, key.get(),
encryptedKey.get(), nullptr);
if (stat != SECSuccess) return MapSECStatus(stat);
aKeyOut =
Key{std::move(encryptedKey),
mozilla::UniqueSECItem(::SECITEM_AllocItem(nullptr, nullptr, 0))};
return NS_OK;
}
CK_GCM_PARAMS MakeGCMParams(const nsACString& aIdentifier,
const mozilla::UniqueSECItem& aIv) {
CK_GCM_PARAMS gcmParams;
gcmParams.pIv = (CK_BYTE_PTR)aIv->data;
gcmParams.ulIvLen = aIv->len;
gcmParams.ulIvBits = aIv->len * 8;
gcmParams.pAAD = (CK_BYTE_PTR)aIdentifier.BeginReading();
gcmParams.ulAADLen = aIdentifier.Length();
gcmParams.ulTagBits = 128;
return gcmParams;
}
/// Create a new key
nsresult CreateDatabaseKey(const nsACString& aIdentifier, Key& aKeyOut) {
// Every key is 32 bytes long
mozilla::UniqueSECItem key =
mozilla::UniqueSECItem(::SECITEM_AllocItem(nullptr, nullptr, 32));
if (!key) return NS_ERROR_FAILURE;
// Key data is random
SECStatus stat = PK11_GenerateRandom(key->data, key->len);
if (stat != SECSuccess) return MapSECStatus(stat);
mozilla::UniqueSECItem iv(::SECITEM_AllocItem(nullptr, nullptr, 12));
if (!iv) return NS_ERROR_FAILURE;
// IV data is also random
stat = PK11_GenerateRandom(iv->data, iv->len);
if (stat != SECSuccess) return MapSECStatus(stat);
CK_GCM_PARAMS gcmParams = MakeGCMParams(aIdentifier, iv);
SECItem gcmItem;
gcmItem.type = siBuffer;
gcmItem.data = (unsigned char*)&gcmParams;
gcmItem.len = sizeof(gcmParams);
// PK11_Encrypt needs an existing buffer
mozilla::UniqueSECItem encryptedKey(
::SECITEM_AllocItem(nullptr, nullptr, 64));
if (!encryptedKey) return NS_ERROR_FAILURE;
unsigned int encryptedLen = 0;
stat =
PK11_Encrypt(sSystemKey.get(), CKM_AES_GCM, &gcmItem, encryptedKey->data,
&encryptedLen, encryptedKey->len, key->data, key->len);
if (stat != SECSuccess) return MapSECStatus(stat);
// Resize buffer to actual length
stat = SECITEM_ReallocItemV2(nullptr, encryptedKey.get(), encryptedLen);
if (stat != SECSuccess) return MapSECStatus(stat);
aKeyOut = Key{std::move(encryptedKey), std::move(iv)};
return NS_OK;
}
nsresult ImportSystemKey(const nsACString& aEncodedKey) {
sKeyMutex.AssertCurrentThreadOwns();
nsCString encryptedKey;
nsresult rv = mozilla::Base64Decode(aEncodedKey, encryptedKey);
NS_ENSURE_SUCCESS(rv, rv);
mozilla::UniqueSECItem wrappedKey(::SECITEM_AllocItem(nullptr, nullptr, 0));
SECStatus stat = SECITEM_MakeItem(nullptr, wrappedKey.get(),
(unsigned char*)encryptedKey.Data(),
encryptedKey.Length());
if (stat != SECSuccess) return MapSECStatus(stat);
mozilla::UniqueSECItem unwrapped(::SECITEM_AllocItem(nullptr, nullptr, 0));
stat = PK11SDR_Decrypt(wrappedKey.get(), unwrapped.get(), nullptr);
if (stat != SECSuccess) return MapSECStatus(stat);
mozilla::UniquePK11SlotInfo slot(PK11_GetInternalSlot());
if (!slot) return NS_ERROR_FAILURE;
sSystemKey = mozilla::UniquePK11SymKey(
PK11_ImportSymKey(slot.get(), CKM_AES_GCM, PK11_OriginUnwrap,
CKA_ENCRYPT | CKA_DECRYPT, unwrapped.get(), nullptr));
if (!sSystemKey) return NS_ERROR_FAILURE;
return NS_OK;
}
/// Import a single, encoded and encrypted key, and encoded IV as `path`
nsresult ImportDatabaseKey(const nsCString& aPath, const nsCString& aEncodedKey,
const nsCString& aEncodedIV) {
sKeyMutex.AssertCurrentThreadOwns();
// Key and IV are encoded Base64 strings
nsCString encryptedKey, stringIV;
nsresult rv = mozilla::Base64Decode(aEncodedKey, encryptedKey);
NS_ENSURE_SUCCESS(rv, rv);
rv = mozilla::Base64Decode(aEncodedIV, stringIV);
NS_ENSURE_SUCCESS(rv, rv);
// Key and IV need to go into heap-allocated SECItems, because they may be
// stored into sKeyMap, which outlives encryptedKey/stringIV
mozilla::UniqueSECItem key(::SECITEM_AllocItem(nullptr, nullptr, 0));
SECStatus stat =
SECITEM_MakeItem(nullptr, key.get(), (unsigned char*)encryptedKey.Data(),
encryptedKey.Length());
if (stat != SECSuccess) return MapSECStatus(stat);
mozilla::UniqueSECItem iv(::SECITEM_AllocItem(nullptr, nullptr, 0));
stat = SECITEM_MakeItem(nullptr, iv.get(), (unsigned char*)stringIV.Data(),
stringIV.Length());
if (stat != SECSuccess) return MapSECStatus(stat);
sKeyMap.InsertOrUpdate(aPath, Key{std::move(key), std::move(iv)});
return NS_OK;
}
// Decrypt key with "system" key
SECStatus DecryptKey(Key& aKey, const nsACString& aIdentifier, SECItem* aData) {
CK_GCM_PARAMS gcmParams = MakeGCMParams(aIdentifier, aKey.iv);
SECItem gcmItem;
gcmItem.type = siBuffer;
gcmItem.data = (unsigned char*)&gcmParams;
gcmItem.len = sizeof(gcmParams);
SECITEM_AllocItem(nullptr, aData, 32);
if (!aData->data) {
PR_SetError(PR_OUT_OF_MEMORY_ERROR, 0);
return SECFailure;
}
unsigned int decryptedLen = 0;
SECStatus stat =
PK11_Decrypt(sSystemKey.get(), CKM_AES_GCM, &gcmItem, aData->data,
&decryptedLen, aData->len, aKey.key->data, aKey.key->len);
aData->len = decryptedLen;
return stat;
}
/// Obtain requested key assuming owned mutex
nsresult FetchOrCreateKey(const nsACString& aIdentifier, SECItem* aData) {
mozilla::StaticMutexAutoLock lock(sKeyMutex);
nsresult rv;
// Load keys if it hasn't happened yet
if (sKeyMap.IsEmpty()) {
MOZ_LOG(GetKeyStorageLog(), mozilla::LogLevel::Debug,
("Reading keys from disk"));
rv = LoadKeysFromDisk();
NS_ENSURE_SUCCESS(rv, rv);
// No keys loaded, so the keystore didn't exist before. Create it!
if (sSystemKey == nullptr) {
MOZ_LOG(GetKeyStorageLog(), mozilla::LogLevel::Debug,
("Initializing key storage"));
nsAutoCString system(SYSTEM_KEY_NAME);
Key key = {0, 0};
nsresult rv = CreateSystemKey(key);
NS_ENSURE_SUCCESS(rv, rv);
rv = WriteKeyToDisk(system, key);
NS_ENSURE_SUCCESS(rv, rv);
}
}
// Key doesn't exist after keys have been loaded. Create it!
if (!sKeyMap.Contains(aIdentifier)) {
Key key = {};
rv = CreateDatabaseKey(aIdentifier, key);
NS_ENSURE_SUCCESS(rv, rv);
MOZ_LOG(GetKeyStorageLog(), mozilla::LogLevel::Debug,
("Writing new key for identifier"));
// Copy key. WriteKeyToDisk may halt to wait for another thread to fetch a
// key, which needs to know that `identifier` already has a key.
sKeyMap.InsertOrUpdate(aIdentifier, Key(key));
rv = WriteKeyToDisk(aIdentifier, key);
NS_ENSURE_SUCCESS(rv, rv);
}
// Decrypt requested key with "system" key
// Must be accessed through visitor, because UniqueSECItems can't be
// copied and nsTHashMap doesn't return references throught Get()
SECStatus stat = sKeyMap.WithEntryHandle(
aIdentifier, [&aIdentifier, &aData](auto entryHandle) {
return DecryptKey(entryHandle.Data(), aIdentifier, aData);
});
return MapSECStatus(stat);
}
nsresult GetKeyByPath(const char* aPath, nsCString& aKey) {
nsCOMPtr<nsIFile> file = new nsLocalFile();
// aPath is UTF-8 (callers pass storage paths from sqlite which are UTF-8).
nsresult rv = file->InitWithPath(NS_ConvertUTF8toUTF16(aPath));
NS_ENSURE_SUCCESS(rv, rv);
return GetKeyByFile(*file, aKey);
}
nsresult GetKeyByFile(nsIFile& aFile, nsCString& aKeyString) {
// Resolve the identifier relative to the current profile. Files outside
// the profile have no stable identifier in this scheme, so the caller
// must handle the failure (typically by falling back to unencrypted).
nsAutoString profilePath;
{
mozilla::StaticMutexAutoLock lock(sKeyMutex);
nsresult rv = GetCurrentProfilePath(profilePath);
NS_ENSURE_SUCCESS(rv, rv);
}
nsCOMPtr<nsIFile> profile = new nsLocalFile();
nsresult rv = profile->InitWithPath(profilePath);
NS_ENSURE_SUCCESS(rv, rv);
bool isUnder = false;
rv = profile->Contains(&aFile, &isUnder);
NS_ENSURE_SUCCESS(rv, rv);
if (!isUnder) {
MOZ_LOG(GetKeyStorageLog(), LogLevel::Debug,
("Refusing to key database outside profile directory"));
return NS_ERROR_NOT_AVAILABLE;
}
nsAutoCString identifier;
rv = aFile.GetRelativePath(profile, identifier);
NS_ENSURE_SUCCESS(rv, rv);
MOZ_LOG(GetKeyStorageLog(), LogLevel::Debug,
("Fetching key for %s", identifier.get()));
UniqueSECItem key(::SECITEM_AllocItem(nullptr, nullptr, 0));
rv = FetchOrCreateKey(identifier, key.get());
NS_ENSURE_SUCCESS(rv, rv);
aKeyString = mozilla::ToHexString(key->data, key->len);
return NS_OK;
}
} // namespace mozilla::storage::key
+71
View File
@@ -0,0 +1,71 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
* 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/. */
#ifndef KeyStorage_h
#define KeyStorage_h
#include "nsIFile.h"
#include "nsString.h"
#include "mozilla/Logging.h"
#include "mozilla/StaticMutex.h"
#include "ScopedNSSTypes.h"
#define SYSTEM_KEY_NAME "system"
namespace mozilla::storage::key {
extern mozilla::StaticMutex sKeyMutex;
struct Key {
mozilla::UniqueSECItem key;
mozilla::UniqueSECItem iv;
Key() = default;
Key(mozilla::UniqueSECItem key, mozilla::UniqueSECItem iv)
: key(std::move(key)), iv(std::move(iv)) {}
Key(const Key& other)
: key(SECITEM_DupItem(other.key.get())),
iv(SECITEM_DupItem(other.iv.get())) {}
Key(Key&&) = default;
Key& operator=(Key&&) = default;
};
/**
* Initialize key storage. Registers a profile observer that tracks the
* current profile directory so per-database keys can be resolved from
* worker threads without touching the main-thread-only directory service.
*
* Must be called on the main thread, before any call to GetKeyByPath or
* GetKeyByFile. Idempotent.
*/
nsresult Init();
/**
* Tear down key storage. Normally invoked by the profile-before-change
* observer; exposed for shutdown and tests.
*/
void Shutdown();
mozilla::LogModule* GetKeyStorageLog();
nsresult ImportSystemKey(const nsACString& aEncodedKey);
nsresult ImportDatabaseKey(const nsCString& aPath, const nsCString& aEncodedKey,
const nsCString& aEncodedIV);
/**
* Fetch (or lazily create) the encryption key for the database at aPath.
* aPath is UTF-8 and must identify a file inside the current profile
* directory. Returns NS_ERROR_NOT_AVAILABLE if the file is not under the
* profile, so callers can fall back to unencrypted storage.
*/
nsresult GetKeyByPath(const char* aPath, nsCString& aKey);
nsresult GetKeyByFile(nsIFile& aFile, nsCString& aKey);
} // namespace mozilla::storage::key
#endif // KeyStorage_h
+164
View File
@@ -0,0 +1,164 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
* 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 "Persist.h"
#include "KeyStorage.h"
#include "Profile.h"
#include "mozilla/Base64.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsCOMPtr.h"
#include "nsIFile.h"
#include "nsNetUtil.h"
#include "nsStreamUtils.h"
#define KEYSTORE_MAGIC "# mozilla secure key storage\n"
#define KEYSTORE_PATH FILE_PATH_SEPARATOR "keystore.db"
namespace mozilla::storage::key {
/// Create path directory for keystore file
nsresult GetKeyStorePath(nsAString& aPath) {
sKeyMutex.AssertCurrentThreadOwns();
nsresult rv = GetCurrentProfilePath(aPath);
NS_ENSURE_SUCCESS(rv, rv);
aPath.Append(NS_LITERAL_STRING_FROM_CSTRING(KEYSTORE_PATH));
return NS_OK;
}
/// Load file contents into string
nsresult LoadFileToString(const nsCOMPtr<nsIFile>& aFile,
nsACString& aContents) {
nsCOMPtr<nsIInputStream> stream;
nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), aFile.get());
// Manually return for fnf error, to avoid having a misleading error message
// in the logs. This is an expected case, that is handled elsewhere
if (rv == NS_ERROR_FILE_NOT_FOUND) return NS_ERROR_FILE_NOT_FOUND;
NS_ENSURE_SUCCESS(rv, rv);
return NS_ConsumeStream(stream, UINT32_MAX, aContents);
}
/// Open file for appending and write string to it
nsresult AppendStringToFile(const nsCOMPtr<nsIFile>& aFile,
nsACString& aContents) {
nsCOMPtr<nsIOutputStream> stream;
nsresult rv = NS_NewLocalFileOutputStream(
getter_AddRefs(stream), aFile.get(),
PR_WRONLY | PR_CREATE_FILE | PR_APPEND, PR_IRUSR | PR_IWUSR);
NS_ENSURE_SUCCESS(rv, rv);
uint32_t count;
rv = stream->Write(aContents.Data(), aContents.Length(), &count);
NS_ENSURE_SUCCESS(rv, rv);
if (count != aContents.Length()) return NS_ERROR_FAILURE;
return NS_OK;
}
/// Load keys from keystore file to memory
nsresult LoadKeysFromDisk() {
sKeyMutex.AssertCurrentThreadOwns();
// Get the file to the key store in the profile and turn it into a file ref
nsAutoString filePath;
nsresult rv = GetKeyStorePath(filePath);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIFile> file;
rv = NS_NewLocalFile(filePath, getter_AddRefs(file));
NS_ENSURE_SUCCESS(rv, rv);
// Load all the file contents into one string
nsCString fileContents;
rv = LoadFileToString(file, fileContents);
if (rv == NS_ERROR_FILE_NOT_FOUND) {
// Non existent keystore is OK and will be handled outside of this
// function
return NS_OK;
}
NS_ENSURE_SUCCESS(rv, rv);
// Verify keystore file
if (!StringBeginsWith(fileContents, nsLiteralCString(KEYSTORE_MAGIC))) {
MOZ_LOG(GetKeyStorageLog(), mozilla::LogLevel::Error,
("Keystore magic mismatch; rejecting file"));
return NS_ERROR_INVALID_SIGNATURE;
}
// Go through each line
for (const auto& line : fileContents.Split('\n')) {
int32_t delimiter1 = line.Find(":"_ns);
int32_t delimiter2 = line.RFind(":"_ns);
// Ignore invalid or incomplete lines
if (delimiter1 == kNotFound || delimiter1 == delimiter2) continue;
// Each line holds a path/identifier, a key and an IV
nsCString path, encodedKey, encodedIV;
path.Assign(line.Data(), delimiter1);
encodedKey.Assign(line.Data() + delimiter1 + 1,
delimiter2 - delimiter1 - 1);
encodedIV.Assign(line.Data() + delimiter2 + 1,
line.Length() - delimiter2 - 1);
if (path.EqualsLiteral(SYSTEM_KEY_NAME)) {
rv = ImportSystemKey(encodedKey);
NS_ENSURE_SUCCESS(rv, rv);
} else {
rv = ImportDatabaseKey(path, encodedKey, encodedIV);
NS_ENSURE_SUCCESS(rv, rv);
}
}
return NS_OK;
}
// Append key to key store file, creating it if needed
nsresult WriteKeyToDisk(const nsACString& aIdentifier, Key& aKey) {
sKeyMutex.AssertCurrentThreadOwns();
// Key and IV need Base64 encoding for disk storage
nsCString encodedKey, encodedIV;
nsresult rv = mozilla::Base64Encode((const char*)aKey.key->data,
aKey.key->len, encodedKey);
NS_ENSURE_SUCCESS(rv, rv);
rv = mozilla::Base64Encode((const char*)aKey.iv->data, aKey.iv->len,
encodedIV);
NS_ENSURE_SUCCESS(rv, rv);
// Get the file to the key store in the profile and turn it into a file ref
nsAutoString filePath;
rv = GetKeyStorePath(filePath);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIFile> file;
rv = NS_NewLocalFile(filePath, getter_AddRefs(file));
NS_ENSURE_SUCCESS(rv, rv);
// If the file doesn't exist, we need to write the magic before anything
// else
bool fileExists;
rv = file->Exists(&fileExists);
NS_ENSURE_SUCCESS(rv, rv);
nsCString fileString;
if (!fileExists) {
fileString.Append(KEYSTORE_MAGIC);
}
nsAutoCString keyEntry =
aIdentifier + ":"_ns + encodedKey + ":"_ns + encodedIV + "\n"_ns;
fileString.Append(keyEntry);
return AppendStringToFile(file, fileString);
}
} // namespace mozilla::storage::key
+18
View File
@@ -0,0 +1,18 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
* 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/. */
#ifndef Persist_h
#define Persist_h
#include "nsString.h"
#include "KeyStorage.h"
namespace mozilla::storage::key {
nsresult LoadKeysFromDisk();
nsresult WriteKeyToDisk(const nsACString& aIdentifier, Key& aKey);
} // namespace mozilla::storage::key
#endif // Persist_h
+97
View File
@@ -0,0 +1,97 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
* 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 "Profile.h"
#include "KeyStorage.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPtr.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsIObserver.h"
#include "nsIObserverService.h"
#include "nsThreadManager.h"
namespace mozilla::storage::key {
constinit static nsString sProfilePath;
class KeyStorageObserver final : public nsIObserver {
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIOBSERVER
private:
~KeyStorageObserver() = default;
};
static mozilla::StaticRefPtr<KeyStorageObserver> sObserver;
NS_IMPL_ISUPPORTS(KeyStorageObserver, nsIObserver)
NS_IMETHODIMP
KeyStorageObserver::Observe(nsISupports*, const char* aTopic, const char16_t*) {
MOZ_ASSERT(NS_IsMainThread());
if (!strcmp(aTopic, "profile-do-change") ||
!strcmp(aTopic, "profile-after-change")) {
mozilla::StaticMutexAutoLock lock(sKeyMutex);
nsCOMPtr<nsIFile> profileDir;
nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
getter_AddRefs(profileDir));
if (NS_FAILED(rv) || !profileDir) {
return NS_OK;
}
nsAutoString path;
rv = profileDir->GetPath(path);
NS_ENSURE_SUCCESS(rv, rv);
sProfilePath = path;
} else if (!strcmp(aTopic, "profile-before-change")) {
Shutdown();
sProfilePath.Truncate();
} else if (!strcmp(aTopic, "xpcom-shutdown")) {
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
if (os) {
os->RemoveObserver(this, "profile-do-change");
os->RemoveObserver(this, "profile-after-change");
os->RemoveObserver(this, "profile-before-change");
os->RemoveObserver(this, "xpcom-shutdown");
}
// The observer service held the last live reference besides sObserver.
// Drop ours so the observer is destroyed cleanly.
sObserver = nullptr;
}
return NS_OK;
}
nsresult InitObserver() {
MOZ_ASSERT(NS_IsMainThread());
if (sObserver) {
return NS_OK;
}
sObserver = new KeyStorageObserver();
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
NS_ENSURE_TRUE(os, NS_ERROR_FAILURE);
os->AddObserver(sObserver, "profile-do-change", false);
os->AddObserver(sObserver, "profile-after-change", false);
os->AddObserver(sObserver, "profile-before-change", false);
os->AddObserver(sObserver, "xpcom-shutdown", false);
// Pick up the current profile if one is already active.
sObserver->Observe(nullptr, "profile-do-change", nullptr);
return NS_OK;
}
nsresult GetCurrentProfilePath(nsAString& aPath) {
sKeyMutex.AssertCurrentThreadOwns();
if (sProfilePath.IsEmpty()) {
return NS_ERROR_NOT_AVAILABLE;
}
aPath = sProfilePath;
return NS_OK;
}
} // namespace mozilla::storage::key
+17
View File
@@ -0,0 +1,17 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
* 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/. */
#ifndef Profile_h
#define Profile_h
#include <nsString.h>
namespace mozilla::storage::key {
nsresult InitObserver();
nsresult GetCurrentProfilePath(nsAString& aPath);
} // namespace mozilla::storage::key
#endif // Profile_h
+37
View File
@@ -0,0 +1,37 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
with Files("**"):
BUG_COMPONENT = ("Core", "Security: PSM")
EXPORTS.mozilla.security += ["KeyStorage.h"]
UNIFIED_SOURCES += [
"KeyStorage.cpp",
"Persist.cpp",
"Profile.cpp",
]
include("/ipc/chromium/chromium-config.mozbuild")
COMPILE_FLAGS["WARNINGS_CXXFLAGS"] += [
"-Wextra",
"-Wunreachable-code",
]
# Gecko headers aren't warning-free enough for us to enable these warnings.
CXXFLAGS += [
"-Wno-unused-parameter",
]
if CONFIG["CC_TYPE"] == "clang-cl":
AllowCompilerWarnings() # workaround for bug 1090497
FINAL_LIBRARY = "xul"
BROWSER_CHROME_MANIFESTS += [
"tests/browser/browser.toml",
]
@@ -0,0 +1,6 @@
[DEFAULT]
prefs = ["security.storage.encryption.sqlite.enabled=true"]
["browser_connect.js"]
["browser_encrypt.js"]
@@ -0,0 +1,67 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that database connections work with enabled keystore encryption.
"use strict";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
});
async function removeIfExists(path) {
if (await IOUtils.exists(path)) {
await IOUtils.remove(path);
}
}
add_task(async function testSecurityEnableEncryption() {
is(
Services.prefs.getBoolPref("security.storage.encryption.sqlite.enabled"),
true,
"security.storage.encryption.sqlite.enabled should be enabled"
);
let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile).path;
let ksPath = profileDir + "/keystore.db";
// Use a unique DB name per task run so test-verify iterations cannot
// collide on stale -wal / -shm sidecars from a previous iteration.
let dbName = `test_encryption_connect_${Date.now()}_${Math.random()
.toString(36)
.slice(2, 8)}.sqlite`;
let dbPath = profileDir + "/" + dbName;
// Belt-and-braces: remove any pre-existing files for this name (and the
// keystore so we can assert it gets re-created).
await removeIfExists(ksPath);
for (let suffix of ["", "-wal", "-shm", "-journal"]) {
await removeIfExists(dbPath + suffix);
}
let conn = await lazy.Sqlite.openConnection({ path: dbName });
is(conn._connectionData._open, true, "Connection should be open");
let res = await conn.execute("SELECT 1;");
is(res[0].getResultByIndex(0), 1, "'SELECT 1;' should return 1");
await conn.execute("CREATE TABLE IF NOT EXISTS test (value TEXT);");
await conn.execute("INSERT INTO test (value) VALUES ('hello');");
await conn.close();
is(await IOUtils.exists(ksPath), true, "keystore.db should exist");
is(await IOUtils.exists(dbPath), true, `${dbName} should exist`);
conn = await lazy.Sqlite.openConnection({ path: dbName });
res = await conn.execute("SELECT value FROM test;");
let values = res.map(row => row.getResultByName("value"));
is(values[0], "hello", "Test `value` should be `'hello'`");
await conn.close();
});
@@ -0,0 +1,67 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that databases are encrypted.
"use strict";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
});
async function removeIfExists(path) {
if (await IOUtils.exists(path)) {
await IOUtils.remove(path);
}
}
add_task(async function testSecurityEnableEncryption() {
is(
Services.prefs.getBoolPref("security.storage.encryption.sqlite.enabled"),
true,
"security.storage.encryption.sqlite.enabled should be enabled"
);
let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile).path;
let ksPath = profileDir + "/keystore.db";
// Use a unique DB name per task run so test-verify iterations cannot
// collide on stale -wal / -shm sidecars from a previous iteration.
let dbName = `test_encryption_encrypt_${Date.now()}_${Math.random()
.toString(36)
.slice(2, 8)}.sqlite`;
let dbPath = profileDir + "/" + dbName;
// Belt-and-braces: remove any pre-existing files for this name (and the
// keystore so we can assert it gets re-created).
await removeIfExists(ksPath);
for (let suffix of ["", "-wal", "-shm", "-journal"]) {
await removeIfExists(dbPath + suffix);
}
let conn = await lazy.Sqlite.openConnection({ path: dbName });
is(conn._connectionData._open, true, "Connection should be open");
let lorem =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin a convallis nisl. Donec tincidunt sodales felis vitae tempus sed. ";
await conn.execute("CREATE TABLE IF NOT EXISTS test (value TEXT);");
await conn.execute("INSERT INTO test (value) VALUES ('" + lorem + "');");
await conn.close();
let contents = await IOUtils.read(dbPath);
// Checking for a substring is easier than checking for a subarray
const decoder = new TextDecoder();
let text = decoder.decode(contents);
is(
text.includes(lorem),
false,
"Encrypted database should not contain plain-text values"
);
});
+1
View File
@@ -8,6 +8,7 @@ with Files("**"):
DIRS += [
"/security/lockstore",
"/security/mls",
"/security/keystore",
]
with Files("generate*.py"):
+18
View File
@@ -413,4 +413,22 @@ interface mozIStorageAsyncConnection : nsISupports {
in mozIStorageCompletionCallback aCallback,
[optional] in unsigned long aPagesPerStep,
[optional] in unsigned long aStepDelayMs);
/**
* Attach another database to this connection.
*
* This has the same effect as if executing ATTACH DATABASE aPath as aName,
* but when database encryption is enabled it opens the encrypted database
* file with the .enc extension and automatically fetches their key and
* decrypts them. See the SQLite documentation for more details.
*
* @param aPath
* The path to the database file that should be attached. May be in
* URI form.
* @param aName
* The name under which the database should be attached.
*/
mozIStoragePendingStatement attachDatabase(in string aPath,
in string aName,
[optional] in mozIStorageStatementCallback aCallback);
};
+175 -3
View File
@@ -4,7 +4,10 @@
#include "BaseVFS.h"
#include "ErrorList.h"
#include "ScopedNSSTypes.h"
#include "nsNetUtil.h"
#include "nsError.h"
#include "nsLocalFile.h"
#include "nsThreadUtils.h"
#include "nsIFile.h"
#include "nsIFileURL.h"
@@ -19,7 +22,9 @@
#include "mozilla/ErrorNames.h"
#include "mozilla/dom/quota/QuotaObject.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/security/KeyStorage.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "mozilla/StaticPrefs_security.h"
#include "mozilla/StaticPrefs_storage.h"
#include "mozIStorageCompletionCallback.h"
@@ -250,6 +255,37 @@ void basicFunctionHelper(sqlite3_context* aCtx, int aArgc,
}
}
void PreparePathForURI(nsACString& aPath) {
#ifdef _WIN32
if (aPath.Find(R"(\\?\)") == 0) {
aPath.Cut(0, 4);
}
aPath.ReplaceChar('\\', '/');
if (std::isalpha(aPath[0]) && aPath[1] == ':') aPath.Insert('/', 0);
#endif
nsAutoCString tmp(aPath);
mozilla_net_percent_encode(&tmp, &aPath);
}
nsresult ExtractURIPathAndQuery(const char* uri, nsCString& path,
nsCString& query) {
if (strstr(uri, "file:") != uri) {
return NS_ERROR_FAILURE;
}
const char* queryDelim = strstr(uri, "?");
// strstr returns nullptr if it can't find "?" or aPath if it is empty
if (!queryDelim || queryDelim == uri) {
return NS_ERROR_FAILURE;
}
query.AssignASCII(
mozilla::Span<const char>(queryDelim + 1, uri + strlen(uri)));
path.AssignASCII(mozilla::Span<const char>(uri + 5, queryDelim));
return NS_OK;
}
RefPtr<QuotaObject> GetQuotaObject(sqlite3_file* aFile, bool obfuscatingVFS) {
return obfuscatingVFS
? mozilla::storage::obfsvfs::GetQuotaObjectForFile(aFile)
@@ -804,6 +840,7 @@ Connection::Connection(Service* aService, int aFlags,
mOpenNotExclusive(aOpenNotExclusive),
mAsyncExecutionThreadShuttingDown(false),
mConnectionClosed(false),
mDatabaseEncrypted(false),
mGrowthChunkSize(0) {
MOZ_ASSERT(!mIgnoreLockingMode || mFlags & SQLITE_OPEN_READONLY,
"Can't ignore locking for a non-readonly connection!");
@@ -1075,6 +1112,48 @@ nsresult Connection::initialize(nsIFile* aDatabaseFile) {
nsresult rv = aDatabaseFile->GetPath(path);
NS_ENSURE_SUCCESS(rv, rv);
// If SQLite encryption is on and this DB is inside the profile, open it
// through obfsvfs with a per-file key.
if (StaticPrefs::security_storage_encryption_sqlite_enabled()) {
// Ensure NSS is up. Safe and idempotent; off-main-thread callers are
// proxied to the main thread by this helper.
if (!EnsureNSSInitializedChromeOrContent()) {
return NS_ERROR_FAILURE;
}
nsCString dbKey;
rv = key::GetKeyByFile(*aDatabaseFile, dbKey);
if (rv == NS_OK) {
mDatabaseEncrypted = true;
nsAutoCString dbPath = NS_ConvertUTF16toUTF8(path);
PreparePathForURI(dbPath);
nsAutoCString dbSpec = "file:"_ns + dbPath + "?key="_ns + dbKey;
int srv =
::sqlite3_open_v2(dbSpec.get(), &mDBConn, mFlags | SQLITE_OPEN_URI,
obfsvfs::GetVFSName());
if (srv != SQLITE_OK) {
::sqlite3_close(mDBConn);
mDBConn = nullptr;
rv = convertResultCode(srv);
RecordOpenStatus(rv);
NS_ENSURE_SUCCESS(rv, rv);
}
rv = initializeInternal();
RecordOpenStatus(rv);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
} else if (rv != NS_ERROR_NOT_AVAILABLE) {
return rv;
}
// NS_ERROR_NOT_AVAILABLE: database is outside the profile (e.g. tests
// using temp files). Fall through to plain open.
MOZ_LOG(key::GetKeyStorageLog(), LogLevel::Debug,
("Database outside profile; opening unencrypted"));
}
bool exclusive =
StaticPrefs::storage_sqlite_exclusiveLock_enabled() && !mOpenNotExclusive;
int srv;
@@ -1098,7 +1177,7 @@ nsresult Connection::initialize(nsIFile* aDatabaseFile) {
mDBConn = nullptr;
rv = convertResultCode(srv);
RecordOpenStatus(rv);
return rv;
NS_ENSURE_SUCCESS(rv, rv);
}
rv = initializeInternal();
@@ -1165,6 +1244,29 @@ nsresult Connection::initialize(nsIFileURL* aFileURL) {
return true;
}));
if (StaticPrefs::security_storage_encryption_sqlite_enabled()) {
if (!EnsureNSSInitializedChromeOrContent()) {
return NS_ERROR_FAILURE;
}
mDatabaseEncrypted = true;
// If a key was manually passed already, assume it is correct
if (!hasKey) {
hasKey = true;
nsCString dbKey;
rv = key::GetKeyByFile(*mDatabaseFile, dbKey);
if (rv == NS_ERROR_NOT_AVAILABLE) {
// Database lives outside the profile directory (e.g. temporary test
// DBs). We cannot key it stably, so open it unencrypted.
mDatabaseEncrypted = false;
} else {
NS_ENSURE_SUCCESS(rv, rv);
hasKey = true;
mDatabaseEncrypted = true;
spec += (query.IsEmpty() ? "?key="_ns : "&key="_ns) + dbKey;
}
}
}
bool exclusive =
StaticPrefs::storage_sqlite_exclusiveLock_enabled() && !mOpenNotExclusive;
@@ -1178,7 +1280,7 @@ nsresult Connection::initialize(nsIFileURL* aFileURL) {
mDBConn = nullptr;
rv = convertResultCode(srv);
RecordOpenStatus(rv);
return rv;
NS_ENSURE_SUCCESS(rv, rv);
}
rv = initializeInternal();
@@ -1961,6 +2063,19 @@ nsresult Connection::initializeClone(Connection* aClone, bool aReadOnly) {
rv = aClone->CreateStatement("ATTACH DATABASE :path AS "_ns + name,
getter_AddRefs(attachStmt));
NS_ENSURE_SUCCESS(rv, rv);
if (mDatabaseEncrypted) {
nsCString aDBKey, query;
rv = key::GetKeyByPath(path.get(), aDBKey);
NS_ENSURE_SUCCESS(rv, rv);
rv = ExtractURIPathAndQuery(path.get(), path, query);
NS_ENSURE_SUCCESS(rv, rv);
PreparePathForURI(path);
// Create a URI to pass the key to obfsvfs
path = nsPrintfCString("file:%s?key=%s", path.get(), aDBKey.get());
}
rv = attachStmt->BindUTF8StringByName("path"_ns, path);
NS_ENSURE_SUCCESS(rv, rv);
rv = attachStmt->Execute();
@@ -2162,7 +2277,10 @@ Connection::AsyncVacuum(mozIStorageCompletionCallback* aCallback,
NS_IMETHODIMP
Connection::GetDefaultPageSize(int32_t* _defaultPageSize) {
*_defaultPageSize = Service::kDefaultPageSize;
if (mDatabaseEncrypted)
*_defaultPageSize = 8192;
else
*_defaultPageSize = Service::kDefaultPageSize;
return NS_OK;
}
@@ -2571,6 +2689,60 @@ Connection::CreateTable(const char* aTableName, const char* aTableSchema) {
return convertResultCode(srv);
}
NS_IMETHODIMP
Connection::AttachDatabase(const char* aPath, const char* aName,
mozIStorageStatementCallback* aCallback,
mozIStoragePendingStatement** _handle) {
nsresult rv;
nsCString uri;
bool encryptionEnabled =
StaticPrefs::security_storage_encryption_sqlite_enabled();
if (encryptionEnabled) {
nsCString dbKey, path, query;
rv = ExtractURIPathAndQuery(aPath, path, query);
if (rv == NS_OK) {
rv = key::GetKeyByPath(path.get(), dbKey);
NS_ENSURE_SUCCESS(rv, rv);
PreparePathForURI(path);
uri = nsPrintfCString("file:%s?%s&key=%s", path.get(), query.get(),
dbKey.get());
} else {
rv = key::GetKeyByPath(aPath, dbKey);
NS_ENSURE_SUCCESS(rv, rv);
nsCString uriString;
uriString.AssignASCII(aPath);
PreparePathForURI(uriString);
uri = nsPrintfCString("file:%s?key=%s", uriString.get(), dbKey.get());
}
} else {
uri = aPath;
}
nsCOMPtr<mozIStorageAsyncStatement> stmt;
rv = CreateAsyncStatement("ATTACH DATABASE :path AS "_ns + nsCString(aName),
getter_AddRefs(stmt));
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->BindUTF8StringByName("path"_ns, uri);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<mozIStoragePendingStatement> pendingStatement;
rv = stmt->ExecuteAsync(aCallback, getter_AddRefs(pendingStatement));
NS_ENSURE_SUCCESS(rv, rv);
pendingStatement.forget(_handle);
return NS_OK;
}
NS_IMETHODIMP
Connection::CreateFunction(const nsACString& aFunctionName,
int32_t aNumArguments,
+15
View File
@@ -8,6 +8,7 @@
#include "nsCOMPtr.h"
#include "mozilla/Atomics.h"
#include "mozilla/Mutex.h"
#include "nsIPrefBranch.h"
#include "nsProxyRelease.h"
#include "nsThreadUtils.h"
#include "nsIInterfaceRequestor.h"
@@ -109,6 +110,15 @@ class Connection final : public mozIStorageConnection,
*/
nsresult initialize(nsIFileURL* aFileURL);
/**
* Creates the connection to the encrypted database.
*
* @param aDatabaseFile
* The nsIFile of the location of the database to open, or create if it
* does not exist.
*/
nsresult initializeSecure(nsIFile* aDatabaseFile);
/**
* Same as initialize, but to be used on the async thread.
*/
@@ -517,6 +527,11 @@ class Connection final : public mozIStorageConnection,
*/
bool mConnectionClosed;
/**
* Set to true if the underlying database file is encrypted on disk.
*/
bool mDatabaseEncrypted;
/**
* Stores the growth increment chunk size, set through SetGrowthIncrement().
*/
+10
View File
@@ -25,6 +25,7 @@
#include "mozilla/StaticPrefs_storage.h"
#include "mozilla/intl/Collator.h"
#include "mozilla/intl/LocaleService.h"
#include "mozilla/security/KeyStorage.h"
#include "sqlite3.h"
#include "mozilla/AutoSQLiteLifetime.h"
@@ -376,6 +377,15 @@ nsresult Service::initialize() {
mozilla::RegisterStorageSQLiteDistinguishedAmount(
StorageSQLiteDistinguishedAmount);
// Always register the key-storage profile observer so that the current
// profile path is known by the time any connection needs a key, even if
// the encryption pref is flipped on later (e.g. by browser-chrome tests
// setting `prefs = [...]` in their manifest).
{
nsresult rv = storage::key::Init();
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
+4
View File
@@ -20,6 +20,7 @@ support-files = [
["test_bug-444233.js"]
["test_cache_size.js"]
prefs = ["security.storage.encryption.sqlite.enabled=false"]
["test_chunk_growth.js"]
# Bug 676981: test fails consistently on Android
@@ -43,6 +44,7 @@ run-if = [
["test_connection_interrupt.js"]
["test_connection_online_backup.js"]
prefs = ["security.storage.encryption.sqlite.enabled=false"]
["test_default_journal_size_limit.js"]
@@ -59,6 +61,7 @@ run-if = [
["test_minimizeMemory.js"]
["test_page_size_is_32k.js"]
prefs = ["security.storage.encryption.sqlite.enabled=false"]
["test_persist_journal.js"]
@@ -67,6 +70,7 @@ run-if = [
["test_retry_on_busy.js"]
["test_sqlite_secure_delete.js"]
prefs = ["security.storage.encryption.sqlite.enabled=false"]
["test_statement_executeAsync.js"]
@@ -468,26 +468,23 @@ void ConcurrentConnection::SetupConnection() {
nsresult ConcurrentConnection::AttachDatabase(const nsString& aFileName,
const nsCString& aSchemaName) {
// No reason to cache this statement, so not using GetStatement here.
nsCOMPtr<mozIStorageAsyncStatement> stmt;
nsresult rv = mConn->CreateAsyncStatement(
"ATTACH DATABASE :path AS "_ns + DATABASE_FAVICONS_SCHEMANAME,
getter_AddRefs(stmt));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIFile> databaseFile =
GetDatabaseFileInProfile(DATABASE_FAVICONS_FILENAME);
NS_ENSURE_SUCCESS(rv, rv);
nsString path;
rv = databaseFile->GetPath(path);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->BindStringByName("path"_ns, path);
nsresult rv = databaseFile->GetPath(path);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<mozIStoragePendingStatement> ps;
nsCOMPtr<mozIStorageStatementCallback> cb = MakeAndAddRef<CallbackOnError>(
this, &ConcurrentConnection::CloseConnection);
rv = stmt->ExecuteAsync(cb, getter_AddRefs(ps));
NS_ConvertUTF16toUTF8 utf8Path(path);
const char* cPath = utf8Path.get();
const char* cSchema = DATABASE_FAVICONS_SCHEMANAME.AsString().get();
rv = mConn->AttachDatabase(cPath, cSchema, cb, getter_AddRefs(ps));
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
+23 -3
View File
@@ -6,13 +6,16 @@
#include "mozilla/ScopeExit.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "mozilla/StaticPrefs_places.h"
#include "mozilla/StaticPrefs_security.h"
#include "mozilla/glean/PlacesMetrics.h"
#include "mozilla/security/KeyStorage.h"
#include "Database.h"
#include "nsIInterfaceRequestorUtils.h"
#include "nsIFile.h"
#include "nsLocalFile.h"
#include "nsNavBookmarks.h"
#include "nsNavHistory.h"
#include "nsPlacesTables.h"
@@ -21,6 +24,7 @@
#include "nsPlacesMacros.h"
#include "nsVariant.h"
#include "SQLFunctions.h"
#include "ScopedNSSTypes.h"
#include "Helpers.h"
#include "nsFaviconService.h"
#include "ConcurrentConnection.h"
@@ -335,12 +339,28 @@ nsresult SetupDurability(nsCOMPtr<mozIStorageConnection>& aDBConn,
nsresult AttachDatabase(nsCOMPtr<mozIStorageConnection>& aDBConn,
const nsACString& aPath, const nsACString& aName) {
nsresult rv;
nsCString path;
path = aPath;
bool encryptionEnabled =
StaticPrefs::security_storage_encryption_sqlite_enabled();
if (encryptionEnabled) {
nsCString dbKey;
rv = storage::key::GetKeyByPath(path.get(), dbKey);
NS_ENSURE_SUCCESS(rv, rv);
path = nsPrintfCString("file:%s?key=%s", path.get(), dbKey.get());
}
nsCOMPtr<mozIStorageStatement> stmt;
nsresult rv = aDBConn->CreateStatement("ATTACH DATABASE :path AS "_ns + aName,
getter_AddRefs(stmt));
rv = aDBConn->CreateStatement("ATTACH DATABASE :path AS "_ns + aName,
getter_AddRefs(stmt));
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->BindUTF8StringByName("path"_ns, aPath);
rv = stmt->BindUTF8StringByName("path"_ns, path);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->Execute();
NS_ENSURE_SUCCESS(rv, rv);
@@ -145,7 +145,7 @@ export class PlacesSemanticHistoryDatabase {
this.#databaseFolderPath,
"places.sqlite"
);
await conn.execute(`ATTACH DATABASE '${placesDbPath}' AS places`);
await conn.attachDatabase(placesDbPath, "places");
return conn;
}
+18
View File
@@ -1284,6 +1284,20 @@ ConnectionData.prototype = Object.freeze({
);
});
},
attachDatabase(path, name) {
return new Promise((resolve, reject) => {
this._dbConn.attachDatabase(path, name, {
handleResult(_result) {},
handleError(error) {
reject(error);
},
handleCompletion(result) {
resolve(result);
},
});
});
},
});
/**
@@ -2170,6 +2184,10 @@ OpenedConnection.prototype = {
stepDelayMs
);
},
attachDatabase(path, name) {
return this._connectionData.attachDatabase(path, name);
},
};
// This is frozen after the prototype has been assigned to allow TypeScript
// identify the properties in the prototype. Ideally we'd change this to be a