/* 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 "CacheCrypto.h" #include "CacheFileIOManager.h" #include "CacheIOThread.h" #include "CacheLog.h" #include "CacheObserver.h" #include "LockstoreService.h" #include "ScopedNSSTypes.h" #include "mozilla/Atomics.h" #include "mozilla/StaticMutex.h" #include "mozilla/StaticPrefs_browser.h" #include "mozilla/StaticPtr.h" #include "nsTArray.h" #include "nsThreadUtils.h" #include "pk11pub.h" #include "pkcs11t.h" #include "secitem.h" namespace mozilla { namespace net { using mozilla::security::lockstore::LockstoreService; // Name of the disk cache's data encryption key in the profile keystore, and // the identifier of the "local" KEK wrapping it. static constexpr auto kDekName = "httpcache"_ns; static constexpr auto kKekIdentifier = "profileEncryption"_ns; // Written on the cache I/O thread when the key load finishes, and on the main // thread at lifecycle boundaries (InitForTesting/Shutdown), so every access // goes through gCacheCryptoMutex. The object is threadsafe-refcounted, so // GetInstanceOrNull() hands out a strong reference that keeps it alive while // in use even if Shutdown() drops this one; the mutex is not held across any // cipher operation. static StaticMutex gCacheCryptoMutex; static StaticRefPtr gCacheCrypto MOZ_GUARDED_BY(gCacheCryptoMutex); // Mirrors "a usable gCacheCrypto exists" so callers can cheaply test whether // encryption is active without taking a strong reference or the mutex. Kept in // sync with gCacheCrypto by Publish()/Shutdown(). static Atomic gCacheCryptoActive(false); // Whether disk cache encryption is enabled. IsEnabled() caches the pref value // here on its first call (on whatever thread), so the value is stable for the // session ("takes effect on restart") and can be read from the cache I/O thread // without touching libpref. Distinct from gCacheCryptoActive: the pref can be // on while no usable cipher could be loaded. gCacheCryptoEnabledInited guards // the one-time capture. static Atomic gCacheCryptoEnabled(false); static Atomic gCacheCryptoEnabledInited(false); // Overwrites a buffer with zeros in a way the compiler may not optimize away, // used to clear key material from memory. static void SecureZero(void* aBuf, size_t aLen) { volatile unsigned char* p = static_cast(aBuf); while (aLen--) { *p++ = 0; } } // Runs an AES-256-GCM operation (encrypt or decrypt) for the given block. The // 64-bit block number, followed by any caller-supplied aAad, is bound as // additional authenticated data so a block cannot be silently moved to a // different position and the extra context cannot be tampered with. On encrypt, // aIn is the plaintext and aOut receives ciphertext||tag (aInLen + // kBlockTagLength); on decrypt, aIn is ciphertext||tag and aOut receives the // plaintext. static nsresult AesGcmOp(const uint8_t* aKey, uint64_t aBlockNumber, const uint8_t* aNonce, bool aEncrypt, const uint8_t* aIn, uint32_t aInLen, uint8_t* aOut, uint32_t aOutMax, const uint8_t* aExtraAad, uint32_t aExtraAadLen) { UniquePK11SlotInfo slot(PK11_GetInternalSlot()); if (!slot) { return NS_ERROR_FAILURE; } SECItem keyItem = {siBuffer, const_cast(aKey), CacheCrypto::kKeyLength}; UniquePK11SymKey symKey(PK11_ImportSymKey(slot.get(), CKM_AES_GCM, PK11_OriginUnwrap, CKA_ENCRYPT, &keyItem, nullptr)); if (!symKey) { return NS_ERROR_FAILURE; } // AAD = block number || caller-supplied extra AAD. nsTArray aad; aad.AppendElements(reinterpret_cast(&aBlockNumber), sizeof(aBlockNumber)); if (aExtraAad && aExtraAadLen) { aad.AppendElements(aExtraAad, aExtraAadLen); } CK_GCM_PARAMS gcmParams = {}; gcmParams.pIv = const_cast(aNonce); gcmParams.ulIvLen = CacheCrypto::kBlockNonceLength; gcmParams.ulIvBits = CacheCrypto::kBlockNonceLength * 8; gcmParams.pAAD = aad.Elements(); gcmParams.ulAADLen = aad.Length(); gcmParams.ulTagBits = CacheCrypto::kBlockTagLength * 8; SECItem params = {siBuffer, reinterpret_cast(&gcmParams), sizeof(gcmParams)}; unsigned int outLen = 0; SECStatus rv = aEncrypt ? PK11_Encrypt(symKey.get(), CKM_AES_GCM, ¶ms, aOut, &outLen, aOutMax, aIn, aInLen) : PK11_Decrypt(symKey.get(), CKM_AES_GCM, ¶ms, aOut, &outLen, aOutMax, aIn, aInLen); if (rv != SECSuccess) { return NS_ERROR_FAILURE; } return NS_OK; } // static void CacheCrypto::Init() { MOZ_ASSERT(NS_IsMainThread()); if (IsActive() || !IsEnabled()) { LOG(("CacheCrypto::Init() - nothing to load, disk cache encryption %s", IsActive() ? "already initialized" : "disabled")); return; } // The cipher needs NSS, here and later on the cache I/O thread. if (!EnsureNSSInitializedChromeOrContent()) { LOG(("CacheCrypto::Init() - NSS not available")); return; } // Resolve the service on the main thread even though it is used off it: // creating it is what caches its profile path, which LockstoreService only // does on the main thread. We are inside profile-do-change, so the profile // is available by now. RefPtr lockstore = LockstoreService::GetSingleton(); RefPtr ioThread = CacheFileIOManager::IOThread(); if (!lockstore || !ioThread) { LOG(("CacheCrypto::Init() - no keystore or no cache I/O thread")); return; } // The load runs on the cache I/O thread because Lockstore's synchronous tier // must not run on the main thread, and it both loads and publishes there so // that the key is in place before anything that consults it. // // OPEN_PRIORITY is what orders this against the rest of the cache. It is the // highest level queue, and this is dispatched before OnProfile() queues any // work, so every consumer of the key runs after it: // - entry opens go to OPEN_PRIORITY/OPEN (CacheFileIOManager::OpenFile), // and CacheFile::SetupEncryption only runs from their completions; // - the index rebuild, which reads entry metadata via SyncReadMetadata, // goes to the INDEX level. // Events dispatched to the thread's XPCOM level do jump ahead of the level // queues (see CacheIOThread::ThreadFunc), but the only ones queued this // early are index writes, which need IsEnabled() -- a pref read -- and not // the key. nsresult rv = ioThread->Dispatch( NS_NewRunnableFunction( "CacheCrypto::LoadFromKeystore", [lockstore]() { Publish(LoadFromKeystore(lockstore)); }), CacheIOThread::OPEN_PRIORITY); if (NS_FAILED(rv)) { LOG(("CacheCrypto::Init() - failed to dispatch the key load")); } } // static void CacheCrypto::InitForTesting() { MOZ_ASSERT(NS_IsMainThread()); if (IsActive()) { return; } if (!EnsureNSSInitializedChromeOrContent()) { LOG(("CacheCrypto::InitForTesting() - NSS not available")); return; } // A throwaway key, so gtests need neither the enabled pref nor a keystore. RefPtr crypto = new CacheCrypto(); if (PK11_GenerateRandom(crypto->mKeyBytes, kKeyLength) != SECSuccess) { LOG(("CacheCrypto::InitForTesting() - key generation failed")); return; } crypto->mUsable = true; Publish(crypto.forget()); } // static already_AddRefed CacheCrypto::LoadFromKeystore( LockstoreService* aLockstore) { MOZ_ASSERT(!NS_IsMainThread()); // A non-empty identifier makes this a deterministic get-or-create, so the // kekRef does not have to be persisted anywhere. auto kekRef = aLockstore->DoCreateKek("local"_ns, kKekIdentifier, ""_ns, 0); if (kekRef.isErr()) { LOG( ("CacheCrypto::LoadFromKeystore() - could not obtain the KEK " "[rv=%" PRIx32 "]", static_cast(kekRef.unwrapErr()))); return nullptr; } // Mint the DEK on first use only: asking for one that already exists is an // error the keystore logs, and after the first run that would be every // startup. auto deks = aLockstore->DoListDeks(); if (deks.isErr()) { LOG(("CacheCrypto::LoadFromKeystore() - could not list DEKs [rv=%" PRIx32 "]", static_cast(deks.unwrapErr()))); return nullptr; } if (!deks.inspect().Contains(kDekName)) { // Extractable: the cache does its own AES-GCM per block, binding the block // number as AAD, which the keystore's blob-oriented encrypt() cannot // express. nsresult rv = aLockstore->DoCreateDek(kDekName, kekRef.inspect(), /* aExtractable */ true, kKeyLength); if (NS_FAILED(rv)) { LOG( ("CacheCrypto::LoadFromKeystore() - could not mint the DEK " "[rv=%" PRIx32 "]", static_cast(rv))); return nullptr; } } auto dek = aLockstore->DoGetDek(kDekName, kekRef.inspect()); if (dek.isErr()) { LOG(("CacheCrypto::LoadFromKeystore() - could not read the DEK [rv=%" PRIx32 "]", static_cast(dek.unwrapErr()))); return nullptr; } nsTArray keyBytes = dek.unwrap(); if (keyBytes.Length() != kKeyLength) { LOG(("CacheCrypto::LoadFromKeystore() - DEK is %zu bytes, expected %u", keyBytes.Length(), kKeyLength)); SecureZero(keyBytes.Elements(), keyBytes.Length()); return nullptr; } RefPtr crypto = new CacheCrypto(); memcpy(crypto->mKeyBytes, keyBytes.Elements(), kKeyLength); SecureZero(keyBytes.Elements(), keyBytes.Length()); crypto->mUsable = true; return crypto.forget(); } // static void CacheCrypto::Publish(already_AddRefed aCrypto) { RefPtr crypto = aCrypto; if (!crypto) { // Encryption stays inactive. CacheFile::SetupEncryption() then fails new // entries closed rather than writing them as plaintext, because // IsEnabled() is still true. LOG(("CacheCrypto::Publish() - no cipher, disk cache encryption inactive")); return; } { StaticMutexAutoLock lock(gCacheCryptoMutex); gCacheCrypto = crypto.forget(); // Set after gCacheCrypto so that IsActive() never reports a cipher that // GetInstanceOrNull() cannot yet hand out. gCacheCryptoActive = true; } LOG(("CacheCrypto::Publish() - disk cache encryption ready")); } // static void CacheCrypto::Shutdown() { MOZ_ASSERT(NS_IsMainThread()); { StaticMutexAutoLock lock(gCacheCryptoMutex); gCacheCryptoActive = false; gCacheCrypto = nullptr; } // gCacheCryptoEnabled is intentionally left cached: it reflects the pref as // of the first IsEnabled() call and is meant to be stable for the process. } // static already_AddRefed CacheCrypto::GetInstanceOrNull() { StaticMutexAutoLock lock(gCacheCryptoMutex); RefPtr crypto = gCacheCrypto; if (crypto && crypto->mUsable) { return crypto.forget(); } return nullptr; } // static bool CacheCrypto::IsActive() { return gCacheCryptoActive; } // static bool CacheCrypto::IsEnabled() { // Capture the pref value on the first call (on whatever thread) and reuse it // afterwards, so the encryption decision is stable for the session. The pref // is RelaxedAtomicBool/mirror:always, so the StaticPrefs read is itself // thread-safe. The race between two first-callers is benign: the pref value // doesn't change between them, so both cache the same value. if (!gCacheCryptoEnabledInited) { gCacheCryptoEnabled = StaticPrefs::browser_cache_disk_encryption_enabled(); gCacheCryptoEnabledInited = true; } return gCacheCryptoEnabled; } CacheCrypto::~CacheCrypto() { SecureZero(mKeyBytes, sizeof(mKeyBytes)); } nsresult CacheCrypto::EncryptBlock(uint64_t aBlockNumber, const uint8_t* aPlaintext, uint32_t aLen, uint8_t* aOut, const uint8_t* aAad, uint32_t aAadLen) { if (!mUsable) { return NS_ERROR_NOT_AVAILABLE; } // Layout: [ciphertext(aLen)][tag(kBlockTagLength)][nonce(kBlockNonceLength)]. // ciphertext||tag are written contiguously by PK11_Encrypt; the nonce // follows. uint8_t* nonce = aOut + aLen + kBlockTagLength; if (PK11_GenerateRandom(nonce, kBlockNonceLength) != SECSuccess) { return NS_ERROR_FAILURE; } return AesGcmOp(mKeyBytes, aBlockNumber, nonce, /* aEncrypt */ true, aPlaintext, aLen, aOut, aLen + kBlockTagLength, aAad, aAadLen); } nsresult CacheCrypto::DecryptBlock(uint64_t aBlockNumber, uint8_t* aIn, uint32_t aLen, uint8_t* aOut, const uint8_t* aAad, uint32_t aAadLen) { if (!mUsable) { return NS_ERROR_NOT_AVAILABLE; } // aIn is [ciphertext(aLen)][tag(kBlockTagLength)][nonce(kBlockNonceLength)]. const uint8_t* nonce = aIn + aLen + kBlockTagLength; return AesGcmOp(mKeyBytes, aBlockNumber, nonce, /* aEncrypt */ false, aIn, aLen + kBlockTagLength, aOut, aLen, aAad, aAadLen); } } // namespace net } // namespace mozilla