mirror of
https://github.com/mozilla-firefox/firefox
synced 2026-08-11 12:19:28 +00:00
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:
committed by
bbeurdouche@mozilla.com
parent
07b8152b97
commit
eea8f31b75
@@ -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"]
|
||||
Vendored
+7
@@ -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
@@ -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
@@ -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"]
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
@@ -8,6 +8,7 @@ with Files("**"):
|
||||
DIRS += [
|
||||
"/security/lockstore",
|
||||
"/security/mls",
|
||||
"/security/keystore",
|
||||
]
|
||||
|
||||
with Files("generate*.py"):
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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().
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user