From eea8f31b75751cc8a6c8124dd49c488805149e0d Mon Sep 17 00:00:00 2001 From: Nikolas Wipper Date: Tue, 16 Jun 2026 12:34:24 +0000 Subject: [PATCH] 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 --- .../backup/tests/marionette/manifest.toml | 4 + dom/cache/Connection.cpp | 7 + dom/cache/test/marionette/manifest.toml | 4 + .../test/mochitest/test_cache_padding.html | 2 +- dom/indexedDB/test/marionette/manifest.toml | 4 + modules/libpref/init/StaticPrefList.yaml | 9 + netwerk/base/idna_glue/src/lib.rs | 9 +- security/keystore/KeyStorage.cpp | 333 ++++++++++++++++++ security/keystore/KeyStorage.h | 71 ++++ security/keystore/Persist.cpp | 164 +++++++++ security/keystore/Persist.h | 18 + security/keystore/Profile.cpp | 97 +++++ security/keystore/Profile.h | 17 + security/keystore/moz.build | 37 ++ security/keystore/tests/browser/browser.toml | 6 + .../keystore/tests/browser/browser_connect.js | 67 ++++ .../keystore/tests/browser/browser_encrypt.js | 67 ++++ security/moz.build | 1 + storage/mozIStorageAsyncConnection.idl | 18 + storage/mozStorageConnection.cpp | 178 +++++++++- storage/mozStorageConnection.h | 15 + storage/mozStorageService.cpp | 10 + storage/test/unit/xpcshell.toml | 4 + .../places/ConcurrentConnection.cpp | 21 +- toolkit/components/places/Database.cpp | 26 +- .../PlacesSemanticHistoryDatabase.sys.mjs | 2 +- toolkit/modules/Sqlite.sys.mjs | 18 + 27 files changed, 1188 insertions(+), 21 deletions(-) create mode 100644 security/keystore/KeyStorage.cpp create mode 100644 security/keystore/KeyStorage.h create mode 100644 security/keystore/Persist.cpp create mode 100644 security/keystore/Persist.h create mode 100644 security/keystore/Profile.cpp create mode 100644 security/keystore/Profile.h create mode 100644 security/keystore/moz.build create mode 100644 security/keystore/tests/browser/browser.toml create mode 100644 security/keystore/tests/browser/browser_connect.js create mode 100644 security/keystore/tests/browser/browser_encrypt.js diff --git a/browser/components/backup/tests/marionette/manifest.toml b/browser/components/backup/tests/marionette/manifest.toml index 21e4e489b4c9..06e765a2c8b8 100644 --- a/browser/components/backup/tests/marionette/manifest.toml +++ b/browser/components/backup/tests/marionette/manifest.toml @@ -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"] diff --git a/dom/cache/Connection.cpp b/dom/cache/Connection.cpp index 1de9b159ff67..85a29e461a9f 100644 --- a/dom/cache/Connection.cpp +++ b/dom/cache/Connection.cpp @@ -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) { diff --git a/dom/cache/test/marionette/manifest.toml b/dom/cache/test/marionette/manifest.toml index 990570382043..41cf962e2274 100644 --- a/dom/cache/test/marionette/manifest.toml +++ b/dom/cache/test/marionette/manifest.toml @@ -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 = [ diff --git a/dom/cache/test/mochitest/test_cache_padding.html b/dom/cache/test/mochitest/test_cache_padding.html index adcdea9c30c5..927271e56346 100644 --- a/dom/cache/test/mochitest/test_cache_padding.html +++ b/dom/cache/test/mochitest/test_cache_padding.html @@ -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); diff --git a/dom/indexedDB/test/marionette/manifest.toml b/dom/indexedDB/test/marionette/manifest.toml index 075a2760a18b..b05b51684f3f 100644 --- a/dom/indexedDB/test/marionette/manifest.toml +++ b/dom/indexedDB/test/marionette/manifest.toml @@ -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"] diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml index 7b2199acff8c..7aca64730289 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml @@ -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 diff --git a/netwerk/base/idna_glue/src/lib.rs b/netwerk/base/idna_glue/src/lib.rs index e5c395870b6c..336a12cecaf8 100644 --- a/netwerk/base/idna_glue/src/lib.rs +++ b/netwerk/base/idna_glue/src/lib.rs @@ -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()) +} diff --git a/security/keystore/KeyStorage.cpp b/security/keystore/KeyStorage.cpp new file mode 100644 index 000000000000..1d60daa691ef --- /dev/null +++ b/security/keystore/KeyStorage.cpp @@ -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 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 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 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 diff --git a/security/keystore/KeyStorage.h b/security/keystore/KeyStorage.h new file mode 100644 index 000000000000..7fe7e7ddbe07 --- /dev/null +++ b/security/keystore/KeyStorage.h @@ -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 diff --git a/security/keystore/Persist.cpp b/security/keystore/Persist.cpp new file mode 100644 index 000000000000..1d8a7e21e1af --- /dev/null +++ b/security/keystore/Persist.cpp @@ -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& aFile, + nsACString& aContents) { + nsCOMPtr 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& aFile, + nsACString& aContents) { + nsCOMPtr 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 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 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 diff --git a/security/keystore/Persist.h b/security/keystore/Persist.h new file mode 100644 index 000000000000..d4bbe100470c --- /dev/null +++ b/security/keystore/Persist.h @@ -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 diff --git a/security/keystore/Profile.cpp b/security/keystore/Profile.cpp new file mode 100644 index 000000000000..24387516759d --- /dev/null +++ b/security/keystore/Profile.cpp @@ -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 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 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 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 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 diff --git a/security/keystore/Profile.h b/security/keystore/Profile.h new file mode 100644 index 000000000000..b6160d82d328 --- /dev/null +++ b/security/keystore/Profile.h @@ -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 + +namespace mozilla::storage::key { +nsresult InitObserver(); +nsresult GetCurrentProfilePath(nsAString& aPath); +} // namespace mozilla::storage::key + +#endif // Profile_h diff --git a/security/keystore/moz.build b/security/keystore/moz.build new file mode 100644 index 000000000000..4afa66354367 --- /dev/null +++ b/security/keystore/moz.build @@ -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", +] diff --git a/security/keystore/tests/browser/browser.toml b/security/keystore/tests/browser/browser.toml new file mode 100644 index 000000000000..fdbd8760c6e3 --- /dev/null +++ b/security/keystore/tests/browser/browser.toml @@ -0,0 +1,6 @@ +[DEFAULT] +prefs = ["security.storage.encryption.sqlite.enabled=true"] + +["browser_connect.js"] + +["browser_encrypt.js"] diff --git a/security/keystore/tests/browser/browser_connect.js b/security/keystore/tests/browser/browser_connect.js new file mode 100644 index 000000000000..fdd90b685bad --- /dev/null +++ b/security/keystore/tests/browser/browser_connect.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(); +}); diff --git a/security/keystore/tests/browser/browser_encrypt.js b/security/keystore/tests/browser/browser_encrypt.js new file mode 100644 index 000000000000..4b3ba5a33c20 --- /dev/null +++ b/security/keystore/tests/browser/browser_encrypt.js @@ -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" + ); +}); diff --git a/security/moz.build b/security/moz.build index 5f3bce2b37b0..fb49533df0c2 100644 --- a/security/moz.build +++ b/security/moz.build @@ -8,6 +8,7 @@ with Files("**"): DIRS += [ "/security/lockstore", "/security/mls", + "/security/keystore", ] with Files("generate*.py"): diff --git a/storage/mozIStorageAsyncConnection.idl b/storage/mozIStorageAsyncConnection.idl index 15155e564753..54cf3b5024a8 100644 --- a/storage/mozIStorageAsyncConnection.idl +++ b/storage/mozIStorageAsyncConnection.idl @@ -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); }; diff --git a/storage/mozStorageConnection.cpp b/storage/mozStorageConnection.cpp index bc0d67b5ed6b..d8960a70cf84 100644 --- a/storage/mozStorageConnection.cpp +++ b/storage/mozStorageConnection.cpp @@ -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(queryDelim + 1, uri + strlen(uri))); + path.AssignASCII(mozilla::Span(uri + 5, queryDelim)); + + return NS_OK; +} + RefPtr 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 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 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, diff --git a/storage/mozStorageConnection.h b/storage/mozStorageConnection.h index 4d5c7c04fcf7..f1a87c54f6e1 100644 --- a/storage/mozStorageConnection.h +++ b/storage/mozStorageConnection.h @@ -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(). */ diff --git a/storage/mozStorageService.cpp b/storage/mozStorageService.cpp index c77217409496..d54bfdf12497 100644 --- a/storage/mozStorageService.cpp +++ b/storage/mozStorageService.cpp @@ -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; } diff --git a/storage/test/unit/xpcshell.toml b/storage/test/unit/xpcshell.toml index 030c5f10e619..241df7d00964 100644 --- a/storage/test/unit/xpcshell.toml +++ b/storage/test/unit/xpcshell.toml @@ -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"] diff --git a/toolkit/components/places/ConcurrentConnection.cpp b/toolkit/components/places/ConcurrentConnection.cpp index 94bc39afd734..6ca2a17507b3 100644 --- a/toolkit/components/places/ConcurrentConnection.cpp +++ b/toolkit/components/places/ConcurrentConnection.cpp @@ -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 stmt; - nsresult rv = mConn->CreateAsyncStatement( - "ATTACH DATABASE :path AS "_ns + DATABASE_FAVICONS_SCHEMANAME, - getter_AddRefs(stmt)); - NS_ENSURE_SUCCESS(rv, rv); - nsCOMPtr 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 ps; nsCOMPtr cb = MakeAndAddRef( 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; diff --git a/toolkit/components/places/Database.cpp b/toolkit/components/places/Database.cpp index f565a999b7f8..286969d87131 100644 --- a/toolkit/components/places/Database.cpp +++ b/toolkit/components/places/Database.cpp @@ -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& aDBConn, nsresult AttachDatabase(nsCOMPtr& 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 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); diff --git a/toolkit/components/places/PlacesSemanticHistoryDatabase.sys.mjs b/toolkit/components/places/PlacesSemanticHistoryDatabase.sys.mjs index 4dab391039da..63845d19ccbf 100644 --- a/toolkit/components/places/PlacesSemanticHistoryDatabase.sys.mjs +++ b/toolkit/components/places/PlacesSemanticHistoryDatabase.sys.mjs @@ -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; } diff --git a/toolkit/modules/Sqlite.sys.mjs b/toolkit/modules/Sqlite.sys.mjs index 530fd14f0317..734561ea7a37 100644 --- a/toolkit/modules/Sqlite.sys.mjs +++ b/toolkit/modules/Sqlite.sys.mjs @@ -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