Source code

Revision control

Copy as Markdown

Other Tools

/* 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 "mozilla/storage/SQLiteEncryption.h"
#include "mozilla/AppShutdown.h"
#include "mozilla/Hex.h"
#include "mozilla/Logging.h"
#include "mozilla/Services.h"
#include "mozilla/StaticMutex.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/StaticPrefs_security.h"
#include "mozilla/SyncRunnable.h"
#include "mozilla/dom/quota/IPCStreamCipherStrategy.h"
#include "mozilla/security/lockstore/lockstore_ffi_generated.h"
#include "ScopedNSSTypes.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsAppRunner.h"
#include "nsCOMPtr.h"
#include "nsDirectoryServiceDefs.h"
#include "nsDirectoryServiceUtils.h"
#include "nsIFile.h"
#include "nsIObserver.h"
#include "nsIObserverService.h"
#include "nsLocalFile.h"
#include "nsString.h"
#include "nsTArray.h"
#include "nsThreadUtils.h"
namespace mozilla::storage {
mozilla::LogModule* GetSQLiteEncryptionLog() {
static mozilla::LazyLogModule sLog("SQLiteEncryption");
return sLog;
}
namespace {
using mozilla::security::lockstore::keystore_close;
using mozilla::security::lockstore::keystore_create_dek;
using mozilla::security::lockstore::keystore_create_kek;
using mozilla::security::lockstore::keystore_get_dek;
using mozilla::security::lockstore::keystore_open;
using mozilla::security::lockstore::KeystoreHandle;
constexpr size_t kDekBytes = 32;
// The lockstore-minted DEK is consumed directly as the
// page-encryption cipher's symmetric key in obfsvfs. The cipher's
// `KeyType` is the load-bearing definition of how many bytes that
// requires; pin `kDekBytes` to it so any cipher migration (or a
// change to lockstore that mints a different-sized key) breaks the
// build instead of producing a silently truncated / padded cipher
// key at runtime. This is the storage-side counterpart to the
// explicit `key_size` argument now threaded through
// `keystore_create_dek`.
static_assert(kDekBytes ==
sizeof(mozilla::dom::quota::IPCStreamCipherStrategy::KeyType),
"kDekBytes must match the page-encryption cipher's KeyType size; "
"update kDekBytes (and the keystore_create_dek call sites that "
"pass it) in lockstep with any cipher key-length change.");
mozilla::StaticMutex sStateMutex;
KeystoreHandle* sHandle MOZ_GUARDED_BY(sStateMutex) = nullptr;
nsString sCachedProfilePath MOZ_GUARDED_BY(sStateMutex);
// Deterministic kek_ref (lockstore::kek::local:sqlite) under which every
// SQLite DEK is wrapped; resolved lazily via create_kek's get-or-create.
nsCString sKekRef MOZ_GUARDED_BY(sStateMutex);
// Set once xpcom-will-shutdown has torn the keystore down, so later calls
// don't re-open it or mint key material that would never be destroyed.
bool sShuttingDown MOZ_GUARDED_BY(sStateMutex) = false;
// Set if writing the EncryptedDatabases marker to compatibility.ini failed.
// While set, GetEncryptionKey refuses to mint a DEK for a NEW database, so we
// never create encrypted data the launch guard cannot later protect. The
// marker write is retried on every startup, so a transient failure self-heals.
bool sMarkerWriteFailed MOZ_GUARDED_BY(sStateMutex) = false;
class ProfileObserver final : public nsIObserver {
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIOBSERVER
private:
~ProfileObserver() = default;
};
mozilla::StaticRefPtr<ProfileObserver> sObserver;
NS_IMPL_ISUPPORTS(ProfileObserver, nsIObserver)
// Resolve the profile directory and cache it. MAIN-THREAD ONLY:
// NS_GetSpecialDirectory -> nsDirectoryService::Get asserts NS_IsMainThread()
// ("Do not call dirsvc::get on non-main threads!") and, in opt builds where
// the assert is compiled out, races its internal hashtable against the main
// thread. Off-main-thread callers must go through
// EnsureProfilePathCachedAnyThread() instead.
void EnsureProfilePathCached() {
MOZ_ASSERT(NS_IsMainThread());
nsCOMPtr<nsIFile> profileDir;
nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
getter_AddRefs(profileDir));
if (NS_FAILED(rv) || !profileDir) {
return;
}
nsString path;
if (NS_FAILED(profileDir->GetPath(path)) || path.IsEmpty()) {
return;
}
StaticMutexAutoLock lock(sStateMutex);
sCachedProfilePath = path;
MOZ_LOG(GetSQLiteEncryptionLog(), LogLevel::Info, ("Profile path cached"));
}
// Ensure the profile path is cached; callable from any thread. Database opens
// run on worker threads (e.g. the QuotaManager / IndexedDB IO threads) where
// the directory service is unavailable, so off the main thread we bounce a
// tiny runnable to the main thread to resolve and cache the path. This is the
// safe direction (worker -> main): the main thread never blocks waiting on a
// storage IO thread, so it cannot deadlock. The fast path avoids the dispatch
// once the path is cached (which, after the first open, it always is).
void EnsureProfilePathCachedAnyThread() {
{
StaticMutexAutoLock lock(sStateMutex);
if (!sCachedProfilePath.IsEmpty()) {
return;
}
}
if (NS_IsMainThread()) {
EnsureProfilePathCached();
return;
}
nsCOMPtr<nsIRunnable> r =
NS_NewRunnableFunction("mozilla::storage::EnsureProfilePathCached",
[]() { EnsureProfilePathCached(); });
mozilla::SyncRunnable::DispatchToThread(GetMainThreadSerialEventTarget(), r);
}
// Snapshot the cached profile path, resolving and caching it on first use.
// Runs on whatever thread opened the database (often a worker such as the
// QuotaManager IO thread, which can open DBs before the main-thread eager
// cache or profile-after-change has populated it -- and under xpcshell
// profile-after-change never fires at all). dirsvc is main-thread only, so
// EnsureProfilePathCachedAnyThread bounces to the main thread when called from
// a worker rather than resolving (and crashing) here. Returns
// NS_ERROR_NOT_INITIALIZED if the path still cannot be resolved.
nsresult GetCachedProfilePath(nsString& aOutPath) {
{
StaticMutexAutoLock lock(sStateMutex);
aOutPath = sCachedProfilePath;
}
if (aOutPath.IsEmpty()) {
EnsureProfilePathCachedAnyThread();
StaticMutexAutoLock lock(sStateMutex);
aOutPath = sCachedProfilePath;
}
if (aOutPath.IsEmpty()) {
MOZ_LOG(GetSQLiteEncryptionLog(), LogLevel::Warning,
("Profile path not yet cached"));
return NS_ERROR_NOT_INITIALIZED;
}
return NS_OK;
}
// If the encryption pref is on, mark the running profile's compatibility.ini
// with EncryptedDatabases=1 (append-only; the writer skips if already
// present), so a later launch can refuse to open the now-encrypted databases
// under a build that would treat them as plaintext. Safe to call on every
// profile-after-change.
void MarkProfileEncryptedIfNeeded() {
MOZ_ASSERT(NS_IsMainThread());
if (!StaticPrefs::security_storage_encryption_sqlite_enabled()) {
return;
}
nsresult rv = mozilla::MarkProfileEncryptedDatabases();
if (NS_FAILED(rv)) {
// The launch guard relies on this marker to refuse opening the profile
// under a build that would treat the now-encrypted databases as plaintext.
// If we cannot write it, refuse to create new encrypted data (see
// sMarkerWriteFailed in GetEncryptionKey) rather than silently produce
// ciphertext the guard can't protect. Retried on every startup.
MOZ_LOG(GetSQLiteEncryptionLog(), LogLevel::Error,
("Failed to write EncryptedDatabases marker (0x%" PRIx32
"); refusing to encrypt new databases this session",
static_cast<uint32_t>(rv)));
StaticMutexAutoLock lock(sStateMutex);
sMarkerWriteFailed = true;
}
}
// When SQLite encryption is on, NSS must be fully initialized on the MAIN
// thread before any in-profile database is opened on a worker (the
// QuotaManager / IndexedDB IO threads). Otherwise the worker's
// EnsureNSSInitializedChromeOrContent() SyncRunnable-dispatches NSS init back
// to the main thread and blocks -- which deadlocks when the main thread is
// itself blocked awaiting that very storage operation (e.g. a synchronous
// LocalStorage/QuotaManager op spinning a nested event loop). Initializing NSS
// here, on the main thread, makes that worker call a cheap no-op (NSS init is
// an idempotent process-wide one-shot). Gated on the profile path being known
// (NSS needs cert9.db/key4.db; forcing it earlier brings NSS up via
// NSS_NoDB_Init and breaks PSM) and MAIN-THREAD ONLY (so this can never itself
// be the deadlocking worker->main dispatch).
void EnsureNSSInitializedForEncryptionIfReady() {
MOZ_ASSERT(NS_IsMainThread());
if (!StaticPrefs::security_storage_encryption_sqlite_enabled()) {
return;
}
{
StaticMutexAutoLock lock(sStateMutex);
if (sCachedProfilePath.IsEmpty()) {
return;
}
}
(void)EnsureNSSInitializedChromeOrContent();
}
NS_IMETHODIMP ProfileObserver::Observe(nsISupports*, const char* aTopic,
const char16_t*) {
if (!strcmp(aTopic, "profile-do-change")) {
// Earliest reliable main-thread point at which the profile (and its cert
// DB) is available, in BOTH the browser and xpcshell -- unlike
// profile-after-change, which does not fire under xpcshell. It also
// strictly precedes any QuotaManager database open (QuotaManager refuses to
// be created before profile-do-change). Pre-initialize NSS here so the
// later worker-thread opens never deadlock dispatching NSS init to a
// blocked main thread.
EnsureProfilePathCached();
EnsureNSSInitializedForEncryptionIfReady();
} else if (!strcmp(aTopic, "profile-after-change")) {
EnsureProfilePathCached();
MarkProfileEncryptedIfNeeded();
} else if (!strcmp(aTopic, "xpcom-will-shutdown")) {
// Tear down lockstore last, at XPCOMWillShutdown. Every in-profile
// encrypted database has already closed by now -- Places at
// profile-before-change, QuotaManager/IndexedDB/DOM-storage at
// profile-before-change-qm -- and nothing writes the profile after
// AppShutdownTelemetry, so the keystore stays available for all of those
// late writes (including each connection's final WAL checkpoint) and only
// then closes. The SQLite WAL checkpoint inside the keystore's own Drop
// still has two phases of headroom before LateWriteChecks activates at
// XPCOMShutdownThreads (default toolkit.shutdown.lateWriteChecksStage = 2).
ShutdownEncryptionKeystore();
}
return NS_OK;
}
} // namespace
void InitEncryptionKeystore() {
MOZ_ASSERT(NS_IsMainThread());
// If we are already at or beyond XPCOMWillShutdown (e.g. a storage service
// created very late in shutdown), don't register a teardown observer that
// would never fire, and refuse to open a keystore that would never be closed.
if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) {
StaticMutexAutoLock lock(sStateMutex);
sShuttingDown = true;
return;
}
// Eagerly try to cache the profile path. If the profile is already
// loaded this succeeds; otherwise we fall through to the observer
// below.
EnsureProfilePathCached();
// If the profile is already available (the common case: the storage service
// is created during NS_InitXPCOM, before profile-do-change), pre-initialize
// NSS now so a later worker-thread database open never deadlocks dispatching
// a synchronous NSS init to a blocked main thread -- e.g. PermissionManager
// opening permissions.sqlite on its IO thread while the main thread holds, or
// is waiting on, the permission-manager monitor. This runs before
// InitializeUserPrefs; nsNSSComponent::InitializeNSS reads security.nocertdb
// live (not via its `once`-mirror) so this early init does not prematurely
// snapshot once-mirrored prefs.
EnsureNSSInitializedForEncryptionIfReady();
// Catch-up for the encrypted-profile marker: a storage service first
// created after profile-after-change has already missed that notification,
// so mark here too. No-op when the pref is off or the profile is not yet
// available (MarkProfileEncryptedDatabases bails without a profile);
// idempotent and append-only otherwise.
MarkProfileEncryptedIfNeeded();
if (sObserver) {
return;
}
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
if (!os) {
return;
}
sObserver = new ProfileObserver();
if (NS_FAILED(os->AddObserver(sObserver, "profile-do-change", false)) ||
NS_FAILED(os->AddObserver(sObserver, "profile-after-change", false)) ||
NS_FAILED(os->AddObserver(sObserver, "xpcom-will-shutdown", false))) {
os->RemoveObserver(sObserver, "profile-do-change");
os->RemoveObserver(sObserver, "profile-after-change");
os->RemoveObserver(sObserver, "xpcom-will-shutdown");
sObserver = nullptr;
}
}
bool IsBootstrapDatabasePath(const nsACString& aPath) {
// Single source of truth for the bootstrap-database name list; obfsvfs's
// IsBootstrapBypassPath delegates here so the two cannot drift.
static constexpr nsLiteralCString kBootstrapNames[] = {
"lockstore.keys.sqlite"_ns, "key4.db"_ns, "cert9.db"_ns, "key3.db"_ns,
"cert8.db"_ns};
// Match both separators: the bootstrap databases reach obfsvfs as native OS
// paths (rusqlite/skv open them directly, bypassing PreparePathForURI's
// forward-slash normalization), so on Windows the basename is delimited by a
// backslash.
const nsDependentCSubstring basename =
Substring(aPath, aPath.RFindCharInSet("/\\") + 1);
for (const auto& name : kBootstrapNames) {
if (basename == name) {
return true;
}
}
return false;
}
nsresult GetDatabaseEncryptionStatus(const nsACString& aDatabasePath,
EncryptionStatus& aStatus) {
// Pref gate. With obfsvfs registered as the SQLite default VFS, every
// keyless sqlite3_open_v2 lands in this function via ObfsOpen; honour the
// master encryption pref here so a turned-off enterprise build still
// returns Plaintext for every path and obfsvfs forwards raw.
if (!StaticPrefs::security_storage_encryption_sqlite_enabled()) {
aStatus = EncryptionStatus::Plaintext;
return NS_OK;
}
nsString profilePath;
nsresult rv = GetCachedProfilePath(profilePath);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIFile> profileDir = new nsLocalFile();
rv = profileDir->InitWithPath(profilePath);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIFile> dbFile = new nsLocalFile();
rv = dbFile->InitWithPath(NS_ConvertUTF8toUTF16(aDatabasePath));
NS_ENSURE_SUCCESS(rv, rv);
// A database under the profile directory is encrypted; anything else (an
// xpcshell temp file, a migration import opened from outside the profile)
// has no stable per-database identifier and is opened as plaintext.
bool isUnder = false;
rv = profileDir->Contains(dbFile, &isUnder);
NS_ENSURE_SUCCESS(rv, rv);
if (!isUnder) {
aStatus = EncryptionStatus::Plaintext;
MOZ_LOG(GetSQLiteEncryptionLog(), LogLevel::Debug,
("Database outside profile; opening unencrypted"));
return NS_OK;
}
// Bootstrap bypass list. These in-profile SQLite databases must stay
// plaintext because they would otherwise re-enter the encryption layer
// during the very initialization that the encryption layer depends on:
//
// - lockstore.keys.sqlite: the keystore itself. It is the source of
// every per-database DEK, so there is no outer key to encrypt it
// with. Encrypting it would require its own key, recursively.
//
// - key4.db / cert9.db (and the legacy key3.db / cert8.db): NSS's own
// softoken databases, opened by libnss3's bundled SQLite during
// NSS_Initialize. Routing them through obfsvfs deadlocks the process
// because GetEncryptionKey -> keystore_open -> nss_rs::init re-enters
// NSS init while NSS_Initialize is still on the stack. NSS manages
// its own at-rest protection for the private-key material in
// key4.db; cert9.db holds public cert data.
if (IsBootstrapDatabasePath(aDatabasePath)) {
aStatus = EncryptionStatus::Plaintext;
return NS_OK;
}
aStatus = EncryptionStatus::Encrypted;
return NS_OK;
}
nsresult GetEncryptionKey(const nsACString& aDatabasePath, OpenIntent aIntent,
nsACString& aOutHexKey) {
// The caller has already established via GetDatabaseEncryptionStatus that
// this database lives under the profile; resolve the profile path again to
// derive the lockstore collection name.
nsString profilePath;
nsresult rv = GetCachedProfilePath(profilePath);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIFile> profileDir = new nsLocalFile();
rv = profileDir->InitWithPath(profilePath);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIFile> dbFile = new nsLocalFile();
rv = dbFile->InitWithPath(NS_ConvertUTF8toUTF16(aDatabasePath));
NS_ENSURE_SUCCESS(rv, rv);
// The collection name is the database's path relative to the profile
// directory (e.g. "places.sqlite", "storage/permanent/.../idb/x.sqlite").
// Unique by construction and human-readable when inspecting the
// lockstore SQLite directly.
nsAutoCString collection;
rv = dbFile->GetRelativePath(profileDir, collection);
NS_ENSURE_SUCCESS(rv, rv);
// Open the lockstore handle (memoised per-path), resolve the shared
// SQLite LocalKey, and read/create this database's DEK -- all while
// holding sStateMutex so ShutdownEncryptionKeystore can't close the
// handle out from under us (mak: avoid use-after-close).
nsTArray<uint8_t> dek;
{
StaticMutexAutoLock lock(sStateMutex);
if (sShuttingDown) {
// After xpcom-will-shutdown the keystore is (being) torn down; don't
// re-open it or mint key material that would never be destroyed
// (mak). Fail the open rather than silently dropping encryption.
MOZ_LOG(GetSQLiteEncryptionLog(), LogLevel::Warning,
("Encryption key requested during shutdown"));
return NS_ERROR_FAILURE;
}
if (!sHandle) {
NS_ConvertUTF16toUTF8 profilePathUtf8(profilePath);
rv = keystore_open(&profilePathUtf8, &sHandle);
if (NS_FAILED(rv)) {
MOZ_LOG(
GetSQLiteEncryptionLog(), LogLevel::Error,
("keystore_open failed: 0x%" PRIx32, static_cast<uint32_t>(rv)));
return rv;
}
MOZ_LOG(GetSQLiteEncryptionLog(), LogLevel::Info, ("Lockstore opened"));
}
// Every SQLite DEK is wrapped under one well-known, deterministic
// LocalKey. create_kek with a fixed identifier is get-or-create: it
// mints lockstore::kek::local:sqlite on first run and recovers it on
// every later run, so we needn't persist the ref ourselves.
if (sKekRef.IsEmpty()) {
const nsCString kekType("local"_ns);
const nsCString kekId("sqlite"_ns);
const nsCString empty;
rv = keystore_create_kek(sHandle, &kekType, &kekId, &empty,
/* cache_timeout_ms */ 0, &sKekRef);
if (NS_FAILED(rv)) {
sKekRef.Truncate();
MOZ_LOG(GetSQLiteEncryptionLog(), LogLevel::Error,
("keystore_create_kek failed: 0x%" PRIx32,
static_cast<uint32_t>(rv)));
return rv;
}
}
rv = keystore_get_dek(sHandle, &collection, &sKekRef, &dek);
if (rv == NS_ERROR_NOT_AVAILABLE && aIntent == OpenIntent::CreateIfNew) {
if (sMarkerWriteFailed) {
// The EncryptedDatabases marker could not be written, so the launch
// guard cannot protect newly-encrypted data; fail closed rather than
// mint a DEK for a new database. Existing databases are unaffected: a
// never-written marker means nothing was encrypted to load.
MOZ_LOG(GetSQLiteEncryptionLog(), LogLevel::Error,
("EncryptedDatabases marker absent; refusing to encrypt %s",
collection.get()));
return NS_ERROR_FAILURE;
}
// First time we see this new (in-profile) database: mint an extractable
// DEK under the shared KEK. A racing thread may have created it first,
// in which case create_dek reports the duplicate as NS_ERROR_FAILURE --
// benign; the get_dek below is the arbiter. LoadExisting never mints a
// key: a missing DEK for an existing database is a hard error (handled
// below), not a cue to create one and make the contents unreadable.
nsresult crv = keystore_create_dek(sHandle, &collection, &sKekRef,
/* extractable */ true,
/* key_size */ kDekBytes);
if (NS_FAILED(crv)) {
MOZ_LOG(GetSQLiteEncryptionLog(), LogLevel::Debug,
("create_dek returned 0x%" PRIx32 "; re-reading",
static_cast<uint32_t>(crv)));
}
rv = keystore_get_dek(sHandle, &collection, &sKekRef, &dek);
}
if (NS_FAILED(rv)) {
// For an existing in-profile database NS_ERROR_NOT_AVAILABLE is the exact
// signal we expect: the DEK is gone for a database we can no longer
// decrypt (corruption / lost keystore) -- a hard dataloss error, distinct
// from a locked or otherwise failing keystore. Either way this database
// is keyable, so the open must fail rather than silently read or write
// plaintext (gcp); remap NS_ERROR_NOT_AVAILABLE so it can never be
// mistaken for a "not encrypted" signal at the call sites.
if (rv == NS_ERROR_NOT_AVAILABLE && aIntent == OpenIntent::LoadExisting) {
MOZ_LOG(GetSQLiteEncryptionLog(), LogLevel::Error,
("missing DEK for an existing encrypted database; failing the "
"open"));
} else {
MOZ_LOG(GetSQLiteEncryptionLog(), LogLevel::Error,
("get_dek failed: 0x%" PRIx32, static_cast<uint32_t>(rv)));
}
return rv == NS_ERROR_NOT_AVAILABLE ? NS_ERROR_FAILURE : rv;
}
}
if (dek.Length() != kDekBytes) {
MOZ_LOG(GetSQLiteEncryptionLog(), LogLevel::Error,
("Unexpected DEK length %zu", dek.Length()));
return NS_ERROR_UNEXPECTED;
}
HexEncode(dek, aOutHexKey);
return NS_OK;
}
void ShutdownEncryptionKeystore() {
// Unregister observer outside the mutex; ObserverService is main-thread
// only and we need to avoid lock-order surprises.
RefPtr<ProfileObserver> observer;
{
StaticMutexAutoLock lock(sStateMutex);
observer = sObserver.forget();
}
if (observer) {
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
if (os) {
os->RemoveObserver(observer, "profile-do-change");
os->RemoveObserver(observer, "profile-after-change");
os->RemoveObserver(observer, "xpcom-will-shutdown");
}
}
StaticMutexAutoLock lock(sStateMutex);
sShuttingDown = true;
if (sHandle) {
MOZ_LOG(GetSQLiteEncryptionLog(), LogLevel::Info,
("Shutting down lockstore"));
(void)keystore_close(sHandle);
sHandle = nullptr;
}
sKekRef.Truncate();
sCachedProfilePath.Truncate();
}
} // namespace mozilla::storage