/* 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 "SSLTokensCache.h" #include "CertVerifier.h" #include "CommonSocketControl.h" #include "TransportSecurityInfo.h" #include "brotli/decode.h" #include "brotli/encode.h" #include "mozIStorageConnection.h" #include "mozIStorageService.h" #include "mozIStorageStatement.h" #include "mozStorageCID.h" #include "mozStorageHelper.h" #include "mozilla/ArrayAlgorithm.h" #include "mozilla/Components.h" #include "mozilla/EndianUtils.h" #include "mozilla/Logging.h" #include "mozilla/Maybe.h" #include "mozilla/OriginAttributes.h" #include "mozilla/Preferences.h" #include "mozilla/Services.h" #include "mozilla/Span.h" #include "mozilla/StaticPrefs_privacy.h" #include "mozilla/dom/Promise.h" #include "mozilla/glean/NetwerkMetrics.h" #include "mozilla/net/SocketProcessChild.h" #include "mozilla/net/SocketProcessParent.h" #include "nsAppDirectoryServiceDefs.h" #include "nsDirectoryServiceUtils.h" #include "nsIEventTarget.h" #include "nsIGlobalObject.h" #include "nsIOService.h" #include "nsIObserverService.h" #include "nsThreadUtils.h" #include "nss.h" #include "prtime.h" #include "ssl.h" #include "sslexp.h" #include "xpcpublic.h" #define SSL_TOKENS_CACHE_DB_FILE "ssl_tokens_cache.sqlite" constexpr int32_t kSSLTokensCacheSchemaVersion = 1; namespace mozilla { namespace net { static LazyLogModule gSSLTokensCacheLog("SSLTokensCache"); // Main thread only: /ssl_tokens_cache.sqlite, or null if no profile. static already_AddRefed ProfileDBFile() { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr file; if (NS_FAILED(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(file)))) { return nullptr; } file->AppendNative(nsLiteralCString(SSL_TOKENS_CACHE_DB_FILE)); return file.forget(); } #undef LOG #define LOG(args) MOZ_LOG(gSSLTokensCacheLog, mozilla::LogLevel::Debug, args) #undef LOG5_ENABLED #define LOG5_ENABLED() \ MOZ_LOG_TEST(mozilla::net::gSSLTokensCacheLog, mozilla::LogLevel::Verbose) class ExpirationComparator { public: bool Equals(SSLTokensCache::TokenCacheRecord* a, SSLTokensCache::TokenCacheRecord* b) const { return a->mExpirationTime == b->mExpirationTime; } bool LessThan(SSLTokensCache::TokenCacheRecord* a, SSLTokensCache::TokenCacheRecord* b) const { return a->mExpirationTime < b->mExpirationTime; } }; static nsTArray> CloneCertChain( const nsTArray>& aSrc) { return TransformIntoNewArray(aSrc, [](const auto& c) { return c.Clone(); }); } SessionCacheInfo SessionCacheInfo::Clone() const { SessionCacheInfo result; result.mEVStatus = mEVStatus; result.mCertificateTransparencyStatus = mCertificateTransparencyStatus; result.mServerCertBytes = mServerCertBytes.Clone(); result.mSucceededCertChainBytes = mSucceededCertChainBytes.map(CloneCertChain); result.mIsBuiltCertChainRootBuiltInRoot = mIsBuiltCertChainRootBuiltInRoot; result.mOverridableErrorCategory = mOverridableErrorCategory; result.mHandshakeCertificatesBytes = mHandshakeCertificatesBytes.map(CloneCertChain); return result; } // Serialized payload layout (all integers little-endian): // [4: token_len][token bytes] // [1: ev_status] // [2: ct_status] // [1: overridable_error] // [1: builtin_root] (0=Nothing, 1=Some(false), 2=Some(true)) // [4: server_cert_len][server_cert bytes] // [1: has_succeeded_chain] if 1: [1: count] then for each [4: len][bytes] // [1: has_handshake_certs] if 1: [1: count] then for each [4: len][bytes] // // Stored compressed with an LE original-size prefix. static constexpr size_t kOriginalLenSize = sizeof(uint32_t); // Sanity bounds on the length fields above. static constexpr uint32_t kMaxPayloadSize = 256 * 1024; static constexpr uint32_t kMaxTokenSize = 256 * 1024; static constexpr uint32_t kMaxCertSize = 0xffff; template static void AppendLE(nsTArray& aBuf, T aVal) { T le = mozilla::NativeEndian::swapToLittleEndian(aVal); aBuf.AppendElements(reinterpret_cast(&le), sizeof(T)); } static nsTArray SerializeRecord(Span aToken, const SessionCacheInfo& aInfo) { nsTArray buf; AppendLE(buf, AssertedCast(aToken.Length())); buf.AppendElements(aToken.Elements(), aToken.Length()); buf.AppendElement(aInfo.mEVStatus == psm::EVStatus::EV ? 1 : 0); AppendLE(buf, aInfo.mCertificateTransparencyStatus); buf.AppendElement(static_cast(aInfo.mOverridableErrorCategory)); if (aInfo.mIsBuiltCertChainRootBuiltInRoot.isNothing()) { buf.AppendElement(0); } else { buf.AppendElement(*aInfo.mIsBuiltCertChainRootBuiltInRoot ? 2 : 1); } AppendLE(buf, AssertedCast(aInfo.mServerCertBytes.Length())); buf.AppendElements(aInfo.mServerCertBytes.Elements(), aInfo.mServerCertBytes.Length()); auto appendChain = [&](const Maybe>>& aChain) { if (aChain.isNothing()) { buf.AppendElement(0); return; } buf.AppendElement(1); MOZ_RELEASE_ASSERT(aChain->Length() <= 0xFF); buf.AppendElement(static_cast(aChain->Length())); for (const auto& cert : *aChain) { AppendLE(buf, AssertedCast(cert.Length())); buf.AppendElements(cert.Elements(), cert.Length()); } }; appendChain(aInfo.mSucceededCertChainBytes); appendChain(aInfo.mHandshakeCertificatesBytes); return buf; } // Records are compressed on the socket thread during the handshake. Quality 1 // encodes ~3.5x faster than 5 for ~5% larger output, and the cache is bounded // in bytes, so that costs cached sessions rather than memory. Quality 2 to 4 // compress this data worse than 1 for no speed gain. static constexpr int kRecordCompressionQuality = 1; // Returns empty array on failure. static nsTArray CompressRecord(Span aPayload) { nsTArray encoded; if (!encoded.SetLength(BrotliEncoderMaxCompressedSize(aPayload.Length()), fallible)) { return {}; } size_t encodedSize = encoded.Length(); if (!BrotliEncoderCompress(kRecordCompressionQuality, BROTLI_DEFAULT_WINDOW, BROTLI_MODE_GENERIC, aPayload.Length(), aPayload.Elements(), &encodedSize, encoded.Elements())) { return {}; } nsTArray result; if (!result.SetCapacity(kOriginalLenSize + encodedSize, fallible)) { return {}; } AppendLE(result, AssertedCast(aPayload.Length())); result.AppendElements(encoded.Elements(), encodedSize); return result; } // Returns empty array on failure. static nsTArray DecompressRecord(Span aCompressed) { if (aCompressed.Length() < kOriginalLenSize) { return {}; } uint32_t originalLen = LittleEndian::readUint32(aCompressed.Elements()); if (originalLen > kMaxPayloadSize) { LOG(("SSLTokensCache: implausible payload originalLen %" PRIu32, originalLen)); return {}; } nsTArray result; if (!result.SetLength(originalLen, fallible)) { return {}; } auto encoded = aCompressed.From(kOriginalLenSize); size_t decodedSize = originalLen; BrotliDecoderResult r = BrotliDecoderDecompress( encoded.Length(), encoded.Elements(), &decodedSize, result.Elements()); if (r != BROTLI_DECODER_RESULT_SUCCESS || decodedSize != originalLen) { return {}; } return result; } struct PayloadReader { Span buf; size_t pos = 0; template bool Read(T& out) { if (buf.Length() - pos < sizeof(T)) return false; if constexpr (sizeof(T) == 1) { out = static_cast(buf[pos]); } else { T le; memcpy(&le, buf.Elements() + pos, sizeof(T)); out = mozilla::NativeEndian::swapFromLittleEndian(le); } pos += sizeof(T); return true; } bool Bytes(nsTArray& out, uint32_t len) { if (buf.Length() - pos < len) return false; if (!out.SetLength(len, fallible)) return false; memcpy(out.Elements(), buf.Elements() + pos, len); pos += len; return true; } bool AtEnd() const { return pos == buf.Length(); } }; static bool DeserializeRecord(Span aBuf, nsTArray& aToken, SessionCacheInfo& aInfo) { PayloadReader r{aBuf}; uint32_t tokenLen; if (!r.Read(tokenLen)) return false; if (tokenLen > kMaxTokenSize) return false; if (!r.Bytes(aToken, tokenLen)) return false; uint8_t evStatus; if (!r.Read(evStatus)) return false; aInfo.mEVStatus = evStatus ? psm::EVStatus::EV : psm::EVStatus::NotEV; uint16_t ctStatus; if (!r.Read(ctStatus)) return false; aInfo.mCertificateTransparencyStatus = ctStatus; uint8_t overridableError; if (!r.Read(overridableError)) return false; aInfo.mOverridableErrorCategory = static_cast( overridableError); uint8_t builtinRoot; if (!r.Read(builtinRoot)) return false; if (builtinRoot == 0) { aInfo.mIsBuiltCertChainRootBuiltInRoot = Nothing(); } else if (builtinRoot == 1) { aInfo.mIsBuiltCertChainRootBuiltInRoot = Some(false); } else if (builtinRoot == 2) { aInfo.mIsBuiltCertChainRootBuiltInRoot = Some(true); } else { return false; } uint32_t serverCertLen; if (!r.Read(serverCertLen)) return false; if (serverCertLen > kMaxCertSize) return false; if (!r.Bytes(aInfo.mServerCertBytes, serverCertLen)) return false; auto readChain = [&](Maybe>>& aChain) -> bool { uint8_t present; if (!r.Read(present)) return false; if (!present) { aChain = Nothing(); return true; } uint8_t count; if (!r.Read(count)) return false; nsTArray> chain; for (uint8_t i = 0; i < count; i++) { uint32_t certLen; if (!r.Read(certLen)) return false; if (certLen > kMaxCertSize) return false; nsTArray cert; if (!r.Bytes(cert, certLen)) return false; chain.AppendElement(std::move(cert)); } aChain = Some(std::move(chain)); return true; }; if (!readChain(aInfo.mSucceededCertChainBytes)) return false; if (!readChain(aInfo.mHandshakeCertificatesBytes)) return false; return r.AtEnd(); } StaticRefPtr SSLTokensCache::gInstance; StaticMutex SSLTokensCache::sLock; uint64_t SSLTokensCache::sRecordId = 0; SSLTokensCache::TokenCacheRecord::~TokenCacheRecord() { if (!gInstance) { return; } gInstance->OnRecordDestroyed(this); } uint32_t SSLTokensCache::TokenCacheRecord::Size() const { return mKey.Length() + mCompressedPayload.Length(); } uint32_t SSLTokensCache::TokenCacheEntry::Size() const { uint32_t size = 0; for (const auto& rec : mRecords) { size += rec->Size(); } return size; } void SSLTokensCache::TokenCacheEntry::AddRecord( UniquePtr&& aRecord, nsTArray& aExpirationArray) { if (mRecords.Length() == StaticPrefs::network_ssl_tokens_cache_records_per_entry()) { aExpirationArray.RemoveElement(mRecords[0].get()); mRecords.RemoveElementAt(0); } aExpirationArray.AppendElement(aRecord.get()); for (int32_t i = mRecords.Length() - 1; i >= 0; --i) { if (aRecord->mExpirationTime > mRecords[i]->mExpirationTime) { mRecords.InsertElementAt(i + 1, std::move(aRecord)); return; } } mRecords.InsertElementAt(0, std::move(aRecord)); } UniquePtr SSLTokensCache::TokenCacheEntry::RemoveWithId(uint64_t aId) { for (int32_t i = mRecords.Length() - 1; i >= 0; --i) { if (mRecords[i]->mId == aId) { UniquePtr record = std::move(mRecords[i]); mRecords.RemoveElementAt(i); return record; } } return nullptr; } const UniquePtr& SSLTokensCache::TokenCacheEntry::Get() { return mRecords[0]; } NS_IMPL_ISUPPORTS(SSLTokensCache, nsIMemoryReporter, nsIObserver, nsIAsyncShutdownBlocker) template void SSLTokensCache::RemoveMatchingLocked(Pred&& aPredicate) { sLock.AssertCurrentThreadOwns(); AutoTArray keysToRemove; for (const auto& entry : mTokenCacheRecords) { if (aPredicate(entry.GetKey())) { keysToRemove.AppendElement(entry.GetKey()); } } for (const auto& key : keysToRemove) { (void)RemoveAllLocked(key); } } void SSLTokensCache::ClearCacheLocked() { sLock.AssertCurrentThreadOwns(); mLoadGeneration++; mExpirationArray.Clear(); mTokenCacheRecords.Clear(); mCacheSize = 0; } // static nsTArray SSLTokensCache::SerializeForIPC() { StaticMutexAutoLock lock(sLock); nsTArray records; if (gInstance) { gInstance->CollectRecordInfosLocked(records, /* aFilterForPersistence */ true); } return records; } void SSLTokensCache::CollectRecordInfosLocked( nsTArray& aOut, bool aFilterForPersistence) const { sLock.AssertCurrentThreadOwns(); for (const auto& entry : mTokenCacheRecords.Values()) { for (const auto& rec : entry->Records()) { if (aFilterForPersistence && !ShouldPersistKey(rec->mKey, rec->mOverridableError)) { continue; } auto info = aOut.AppendElement(); info->key = rec->mKey; info->expirationTime = static_cast(rec->mExpirationTime); info->overridableError = rec->mOverridableError; info->restored = rec->mRestored; info->id = rec->mId; info->compressedPayload = rec->mCompressedPayload.Clone(); } } } // static void SSLTokensCache::GetAllRecords(nsTArray& aOut) { StaticMutexAutoLock lock(sLock); if (!gInstance) { return; } gInstance->CollectRecordInfosLocked(aOut, /* aFilterForPersistence */ false); } // static void SSLTokensCache::ReplaceAllRecords( nsTArray&& aRecords) { StaticMutexAutoLock lock(sLock); if (!gInstance) { return; } gInstance->ClearCacheLocked(); for (auto& info : aRecords) { auto rec = MakeRecord(info.key, static_cast(info.expirationTime), info.overridableError, info.restored, std::move(info.compressedPayload)); gInstance->InsertRecordLocked(std::move(rec)); } } // static bool SSLTokensCache::DecodeCompressedPayload(Span aCompressed, nsTArray& aToken, SessionCacheInfo& aInfo, uint32_t* aDecompressedLength) { nsTArray payload = DecompressRecord(aCompressed); if (payload.IsEmpty()) { return false; } if (aDecompressedLength) { *aDecompressedLength = payload.Length(); } return DeserializeRecord(payload, aToken, aInfo); } // static void SSLTokensCache::DeserializeFromIPC( nsTArray&& aRecords, bool aRestored) { if (aRecords.IsEmpty()) { return; } uint32_t loadGen = 0; { StaticMutexAutoLock lock(sLock); if (!gInstance) { return; } gInstance->ClearCacheLocked(); loadGen = gInstance->mLoadGeneration; } PRTime now = PR_Now(); for (const auto& rec : aRecords) { if (rec.expirationTime <= now) { continue; } PutFromPersisted(rec, loadGen, aRestored); } } // static void SSLTokensCache::DeserializeFromIPCAsync( nsTArray&& aRecords, bool aRestored) { if (aRecords.IsEmpty()) { return; } NS_DispatchBackgroundTask(NS_NewRunnableFunction( "SSLTokensCache::DeserializeFromIPCAsync", [records = std::move(aRecords), aRestored]() mutable { DeserializeFromIPC(std::move(records), aRestored); })); } // static nsDependentCSubstring SSLTokensCache::BasePartFromKey(const nsACString& aKey) { int32_t caretPos = aKey.FindChar('^'); return nsDependentCSubstring( aKey, 0, caretPos == kNotFound ? aKey.Length() : caretPos); } // static // Extracts the host from a key's base part ("host:port" or "prefix:host:port"). // Returns an empty string if no colon is found. nsDependentCSubstring SSLTokensCache::HostFromBasePart( const nsDependentCSubstring& aBasePart) { int32_t lastColon = aBasePart.RFindChar(':'); if (lastColon == kNotFound) { return nsDependentCSubstring(); } return nsDependentCSubstring(aBasePart, 0, lastColon); } // static OriginAttributes SSLTokensCache::OAFromPeerId(const nsACString& aPeerId) { OriginAttributes oa; int32_t caretPos = aPeerId.FindChar('^'); if (caretPos != kNotFound) { // PopulateFromSuffix expects the full OA suffix including the leading '^'. nsAutoCString suffix(Substring(aPeerId, caretPos)); (void)oa.PopulateFromSuffix(suffix); } return oa; } // static bool SSLTokensCache::SetupPersistenceLocked(uint32_t& aLoadGen, bool aStorageServiceAvailable) { sLock.AssertCurrentThreadOwns(); MOZ_ASSERT(gInstance); MOZ_ASSERT(!gInstance->mDBActive); if (!aStorageServiceAvailable) { return false; } if (!gInstance->mDBFile) { nsCOMPtr dbFile = ProfileDBFile(); if (!dbFile) { return false; } gInstance->mDBFile = dbFile; } if (!gInstance->mDBQueue) { nsCOMPtr queue; NS_CreateBackgroundTaskQueue("SSLTokensDB", getter_AddRefs(queue)); if (!queue) { return false; } gInstance->mDBQueue = queue; } gInstance->mDBActive = true; gInstance->mLoadStartTime = TimeStamp::Now(); aLoadGen = gInstance->mLoadGeneration; return true; } // Best-effort removal of the pre-mozStorage backing files. static void RemoveLegacyBackingFilesSync(nsIFile* aDbFile) { nsCOMPtr legacy; if (NS_FAILED(aDbFile->Clone(getter_AddRefs(legacy)))) { return; } legacy->SetLeafName(u"ssl_tokens_cache.bin"_ns); (void)legacy->Remove(false); legacy->SetLeafName(u"ssl_tokens_cache.tmp"_ns); (void)legacy->Remove(false); } // static nsresult SSLTokensCache::Init() { MOZ_ASSERT(NS_IsMainThread()); if (XRE_IsParentProcess()) { if (nsCOMPtr dbFile = ProfileDBFile()) { NS_DispatchBackgroundTask(NS_NewRunnableFunction( "SSLTokensCache::RemoveLegacyBackingFiles", [dbFile]() { RemoveLegacyBackingFilesSync(dbFile); })); } } StaticMutexAutoLock lock(sLock); // The socket process holds the live token cache, the parent process the // persistence layer. Some xpcshell tests use sockets in the parent, too. if (!(XRE_IsSocketProcess() || XRE_IsParentProcess())) { return NS_OK; } MOZ_ASSERT(!gInstance); gInstance = new SSLTokensCache(); RegisterWeakMemoryReporter(gInstance); nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs && XRE_IsParentProcess()) { obs->AddObserver(gInstance, "profile-after-change", false); obs->AddObserver(gInstance, "last-pb-context-exited", false); } if (!StaticPrefs::network_ssl_tokens_cache_persistence()) { return NS_OK; } if (obs) { obs->AddObserver(gInstance, "application-background", false); obs->AddObserver(gInstance, "idle-daily", false); gInstance->mWriteObserversRegistered = true; } return NS_OK; } // static nsresult SSLTokensCache::Shutdown() { MOZ_ASSERT(NS_IsMainThread()); RefPtr instance; bool blockerRegistered = false; { StaticMutexAutoLock lock(sLock); if (!gInstance) { return NS_ERROR_UNEXPECTED; } instance = gInstance; blockerRegistered = gInstance->mShutdownBarrier != nullptr; } // With no blocker, BlockShutdown will never fire: write synchronously // (test environments only), unregister, and clear gInstance now. if (!blockerRegistered) { #ifdef ENABLE_TESTS instance->DoWrite(true); #endif UnregisterFromServices(instance); StaticMutexAutoLock lock(sLock); gInstance = nullptr; } return NS_OK; } // static void SSLTokensCache::UnregisterFromServices(SSLTokensCache* aInstance) { MOZ_ASSERT(NS_IsMainThread()); UnregisterWeakMemoryReporter(aInstance); nsCOMPtr obs = mozilla::services::GetObserverService(); if (!obs) { return; } bool hadWriteObservers; { StaticMutexAutoLock lock(sLock); hadWriteObservers = aInstance->mWriteObserversRegistered; } if (hadWriteObservers) { obs->RemoveObserver(aInstance, "application-background"); obs->RemoveObserver(aInstance, "idle-daily"); } if (XRE_IsParentProcess()) { Preferences::UnregisterCallback(&SSLTokensCache::ReconcilePersistence, "network.ssl_tokens_cache_persistence"); obs->RemoveObserver(aInstance, "profile-after-change"); obs->RemoveObserver(aInstance, "last-pb-context-exited"); } } SSLTokensCache::SSLTokensCache() { LOG(("SSLTokensCache::SSLTokensCache")); } SSLTokensCache::~SSLTokensCache() { LOG(("SSLTokensCache::~SSLTokensCache")); } // static nsresult SSLTokensCache::CreateSchemaOn(mozIStorageConnection* aConn) { nsresult rv = aConn->SetSchemaVersion(kSSLTokensCacheSchemaVersion); NS_ENSURE_SUCCESS(rv, rv); return aConn->ExecuteSimpleSQL( nsLiteralCString("CREATE TABLE ssl_tokens (" "key TEXT NOT NULL," "expiration_time INTEGER NOT NULL," "overridable_error INTEGER NOT NULL," "payload BLOB NOT NULL)")); } // static nsresult SSLTokensCache::EnsureSchema(mozIStorageConnection* aConn) { bool tableExists = false; nsresult rv = aConn->TableExists("ssl_tokens"_ns, &tableExists); NS_ENSURE_SUCCESS(rv, rv); if (!tableExists) { return CreateSchemaOn(aConn); } // An unreadable version is as good as a wrong one: recreate. int32_t dbSchemaVersion = 0; rv = aConn->GetSchemaVersion(&dbSchemaVersion); if (NS_SUCCEEDED(rv) && dbSchemaVersion == kSSLTokensCacheSchemaVersion) { return NS_OK; } mozStorageTransaction transaction(aConn, false); rv = transaction.Start(); NS_ENSURE_SUCCESS(rv, rv); rv = aConn->ExecuteSimpleSQL("DROP TABLE ssl_tokens"_ns); NS_ENSURE_SUCCESS(rv, rv); rv = CreateSchemaOn(aConn); NS_ENSURE_SUCCESS(rv, rv); return transaction.Commit(); } // static nsresult SSLTokensCache::PrepareInsertStatement( mozIStorageConnection* aConn, nsCOMPtr& aStmtInsert) { return aConn->CreateStatement( nsLiteralCString("INSERT INTO ssl_tokens (key, expiration_time, " "overridable_error, payload) VALUES (:key, " ":expiration_time, :overridable_error, :payload)"), getter_AddRefs(aStmtInsert)); } // static nsresult SSLTokensCache::WriteSnapshotTo( mozIStorageConnection* aConn, mozIStorageStatement* aStmtInsert, const nsTArray& aSnapshot) { mozStorageTransaction transaction(aConn, false); nsresult rv = transaction.Start(); NS_ENSURE_SUCCESS(rv, rv); rv = aConn->ExecuteSimpleSQL("DELETE FROM ssl_tokens"_ns); NS_ENSURE_SUCCESS(rv, rv); for (const auto& rec : aSnapshot) { mozStorageStatementScoper scoper(aStmtInsert); rv = aStmtInsert->BindUTF8StringByName("key"_ns, rec.key); NS_ENSURE_SUCCESS(rv, rv); rv = aStmtInsert->BindInt64ByName("expiration_time"_ns, rec.expirationTime); NS_ENSURE_SUCCESS(rv, rv); rv = aStmtInsert->BindInt32ByName("overridable_error"_ns, rec.overridableError); NS_ENSURE_SUCCESS(rv, rv); rv = aStmtInsert->BindBlobByName("payload"_ns, rec.compressedPayload.Elements(), rec.compressedPayload.Length()); NS_ENSURE_SUCCESS(rv, rv); rv = aStmtInsert->Execute(); NS_ENSURE_SUCCESS(rv, rv); } return transaction.Commit(); } // static uint32_t SSLTokensCache::LoadValidRecordsFrom(mozIStorageConnection* aConn, uint32_t aLoadGen, bool* aCorrupted) { nsCOMPtr stmt; nsresult rv = aConn->CreateStatement( nsLiteralCString("SELECT key, expiration_time, overridable_error, " "payload FROM ssl_tokens WHERE expiration_time > " ":now"), getter_AddRefs(stmt)); NS_ENSURE_SUCCESS(rv, 0); rv = stmt->BindInt64ByName("now"_ns, PR_Now()); NS_ENSURE_SUCCESS(rv, 0); uint32_t count = 0; bool hasResult = false; for (;;) { rv = stmt->ExecuteStep(&hasResult); if (NS_FAILED(rv)) { LOG(("SSLTokensCache::LoadValidRecordsFrom: ExecuteStep failed")); if (aCorrupted) { *aCorrupted = true; } break; } if (!hasResult) { break; } SSLTokensCacheRecordInfo rec{}; uint32_t len = 0; const uint8_t* blob = nullptr; if (NS_FAILED(stmt->GetUTF8String(0, rec.key)) || NS_FAILED(stmt->GetSharedBlob(3, &len, &blob))) { // Corruption the open check can't see; don't pass off a partial load. if (aCorrupted) { *aCorrupted = true; } continue; } rec.expirationTime = stmt->AsInt64(1); rec.overridableError = static_cast(stmt->AsInt32(2)); rec.compressedPayload.AppendElements(blob, len); // Loaded from disk, so restored by definition. if (PutFromPersisted(rec, aLoadGen, /* aRestored */ true)) { count++; } } return count; } // static nsresult SSLTokensCache::Put(const nsACString& aKey, const uint8_t* aToken, uint32_t aTokenLen, CommonSocketControl* aSocketControl) { PRTime expirationTime; SSLResumptionTokenInfo tokenInfo; if (SSL_GetResumptionTokenInfo(aToken, aTokenLen, &tokenInfo, sizeof(tokenInfo)) != SECSuccess) { LOG((" cannot get expiration time from the token, NSS error %d", PORT_GetError())); return NS_ERROR_FAILURE; } expirationTime = tokenInfo.expirationTime; SSL_DestroyResumptionTokenInfo(&tokenInfo); return Put(aKey, aToken, aTokenLen, aSocketControl, expirationTime); } // static nsresult SSLTokensCache::Put(const nsACString& aKey, const uint8_t* aToken, uint32_t aTokenLen, CommonSocketControl* aSocketControl, PRTime aExpirationTime) { LOG(("SSLTokensCache::Put [key=%s, tokenLen=%u]", PromiseFlatCString(aKey).get(), aTokenLen)); if (!aSocketControl) { return NS_ERROR_FAILURE; } nsCOMPtr securityInfo; nsresult rv = aSocketControl->GetSecurityInfo(getter_AddRefs(securityInfo)); if (NS_FAILED(rv)) { return rv; } nsCOMPtr cert; securityInfo->GetServerCert(getter_AddRefs(cert)); if (!cert) { return NS_ERROR_FAILURE; } nsTArray certBytes; rv = cert->GetRawDER(certBytes); if (NS_FAILED(rv)) { return rv; } Maybe>> succeededCertChainBytes; nsTArray> succeededCertArray; rv = securityInfo->GetSucceededCertChain(succeededCertArray); if (NS_FAILED(rv)) { return rv; } auto getRawDerAll = [](nsTArray>& aCerts) -> Result>, nsresult> { return TransformIntoNewArrayAbortOnErr( aCerts, [](const RefPtr& aCert) -> Result, nsresult> { nsTArray raw; MOZ_TRY(aCert->GetRawDER(raw)); return std::move(raw); }, fallible); }; Maybe isBuiltCertChainRootBuiltInRoot; if (!succeededCertArray.IsEmpty()) { auto result = getRawDerAll(succeededCertArray); if (result.isErr()) return result.unwrapErr(); succeededCertChainBytes.emplace(result.unwrap()); bool builtInRoot = false; rv = securityInfo->GetIsBuiltCertChainRootBuiltInRoot(&builtInRoot); if (NS_FAILED(rv)) { return rv; } isBuiltCertChainRootBuiltInRoot.emplace(builtInRoot); } bool isEV; rv = securityInfo->GetIsExtendedValidation(&isEV); if (NS_FAILED(rv)) { return rv; } uint16_t certificateTransparencyStatus; rv = securityInfo->GetCertificateTransparencyStatus( &certificateTransparencyStatus); if (NS_FAILED(rv)) { return rv; } nsITransportSecurityInfo::OverridableErrorCategory overridableErrorCategory; rv = securityInfo->GetOverridableErrorCategory(&overridableErrorCategory); if (NS_FAILED(rv)) { return rv; } Maybe>> handshakeCertificatesBytes; nsTArray> handshakeCertificates; rv = securityInfo->GetHandshakeCertificates(handshakeCertificates); if (NS_FAILED(rv)) { return rv; } if (!handshakeCertificates.IsEmpty()) { auto result = getRawDerAll(handshakeCertificates); if (result.isErr()) return result.unwrapErr(); handshakeCertificatesBytes.emplace(result.unwrap()); } SessionCacheInfo info; info.mEVStatus = isEV ? psm::EVStatus::EV : psm::EVStatus::NotEV; info.mCertificateTransparencyStatus = certificateTransparencyStatus; info.mOverridableErrorCategory = overridableErrorCategory; info.mIsBuiltCertChainRootBuiltInRoot = isBuiltCertChainRootBuiltInRoot; info.mServerCertBytes = std::move(certBytes); info.mSucceededCertChainBytes = std::move(succeededCertChainBytes); info.mHandshakeCertificatesBytes = std::move(handshakeCertificatesBytes); nsTArray payload = SerializeRecord({aToken, aTokenLen}, info); nsTArray compressed = CompressRecord(payload); if (compressed.IsEmpty()) { LOG(("SSLTokensCache::Put: compression failed")); return NS_ERROR_FAILURE; } { StaticMutexAutoLock lock(sLock); if (!gInstance) { LOG((" service not initialized")); return NS_ERROR_NOT_INITIALIZED; } auto makeRecord = [&]() MOZ_REQUIRES(sLock) { auto rec = MakeUnique(); rec->mKey = aKey; rec->mExpirationTime = aExpirationTime; rec->mOverridableError = static_cast(overridableErrorCategory); rec->mCompressedPayload = std::move(compressed); return rec; }; gInstance->InsertRecordLocked(makeRecord()); gInstance->LogStats(); } // sLock released return NS_OK; } // static nsresult SSLTokensCache::Get(const nsACString& aKey, nsTArray& aToken, SessionCacheInfo& aResult, uint64_t* aTokenId) { LOG(("SSLTokensCache::Get [key=%s]", PromiseFlatCString(aKey).get())); StaticMutexAutoLock lock(sLock); if (!gInstance) { LOG((" service not initialized")); return NS_ERROR_NOT_INITIALIZED; } UniquePtr owned = gInstance->GetRecordLocked(aKey, aTokenId); if (!owned) { return NS_ERROR_NOT_AVAILABLE; } nsTArray payload = DecompressRecord(owned->mCompressedPayload); if (payload.IsEmpty() || !DeserializeRecord(payload, aToken, aResult)) { return NS_ERROR_FAILURE; } return NS_OK; } UniquePtr SSLTokensCache::GetRecordLocked( const nsACString& aKey, uint64_t* aTokenId) { sLock.AssertCurrentThreadOwns(); if (!mLoadComplete && mDBActive) { LOG(("SSLTokensCache::GetRecordLocked: connection before load complete")); mozilla::glean::network::ssl_token_cache_early_connections.Add(1); } TokenCacheEntry* cacheEntry = nullptr; if (mTokenCacheRecords.Get(aKey, &cacheEntry)) { if (cacheEntry->RecordCount() == 0) { MOZ_ASSERT(false, "Found a cacheEntry with no records"); mTokenCacheRecords.Remove(aKey); return nullptr; } PRTime now = PR_Now(); while (cacheEntry->RecordCount() > 0) { const UniquePtr& rec = cacheEntry->Get(); if (rec->mExpirationTime > now) { uint64_t id = rec->mId; uint32_t size = rec->Size(); UniquePtr owned = cacheEntry->RemoveWithId(id); if (aTokenId) { *aTokenId = id; } mCacheSize -= size; if (cacheEntry->RecordCount() == 0) { mTokenCacheRecords.Remove(aKey); } mozilla::glean::network::ssl_token_cache_hits.Get("hit"_ns).Add(1); LOG(("SSLTokensCache::GetRecordLocked: hit [key=%s, load_complete=%s]", PromiseFlatCString(aKey).get(), mLoadComplete ? "yes" : "no")); return owned; } LOG((" skipping expired token [expirationTime=%" PRId64 ", now=%" PRId64 "]", rec->mExpirationTime, now)); mozilla::glean::network::ssl_token_cache_expired.Add(1); uint64_t expiredId = rec->mId; mCacheSize -= rec->Size(); cacheEntry->RemoveWithId(expiredId); } mTokenCacheRecords.Remove(aKey); } LOG((" token not found")); mozilla::glean::network::ssl_token_cache_hits.Get("miss"_ns).Add(1); return nullptr; } // static nsresult SSLTokensCache::Remove(const nsACString& aKey, uint64_t aId) { StaticMutexAutoLock lock(sLock); LOG(("SSLTokensCache::Remove [key=%s]", PromiseFlatCString(aKey).get())); if (!gInstance) { LOG((" service not initialized")); return NS_ERROR_NOT_INITIALIZED; } return gInstance->RemoveLocked(aKey, aId); } nsresult SSLTokensCache::RemoveLocked(const nsACString& aKey, uint64_t aId) { sLock.AssertCurrentThreadOwns(); LOG(("SSLTokensCache::RemoveLocked [key=%s, id=%" PRIu64 "]", PromiseFlatCString(aKey).get(), aId)); TokenCacheEntry* cacheEntry; if (!mTokenCacheRecords.Get(aKey, &cacheEntry)) { return NS_ERROR_NOT_AVAILABLE; } UniquePtr rec = cacheEntry->RemoveWithId(aId); if (!rec) { return NS_ERROR_NOT_AVAILABLE; } mCacheSize -= rec->Size(); if (cacheEntry->RecordCount() == 0) { mTokenCacheRecords.Remove(aKey); } // Release the record immediately, so mExpirationArray can be also updated. rec = nullptr; LogStats(); return NS_OK; } // static nsresult SSLTokensCache::RemoveAll(const nsACString& aKey) { StaticMutexAutoLock lock(sLock); LOG(("SSLTokensCache::RemoveAll [key=%s]", PromiseFlatCString(aKey).get())); if (!gInstance) { LOG((" service not initialized")); return NS_ERROR_NOT_INITIALIZED; } return gInstance->RemoveAllLocked(aKey); } nsresult SSLTokensCache::RemoveAllLocked(const nsACString& aKey) { sLock.AssertCurrentThreadOwns(); LOG(("SSLTokensCache::RemoveAllLocked [key=%s]", PromiseFlatCString(aKey).get())); UniquePtr cacheEntry; if (!mTokenCacheRecords.Remove(aKey, &cacheEntry)) { return NS_ERROR_NOT_AVAILABLE; } mCacheSize -= cacheEntry->Size(); cacheEntry = nullptr; LogStats(); return NS_OK; } void SSLTokensCache::OnRecordDestroyed(TokenCacheRecord* aRec) { // Always called from destructors of map entries while sLock is held. sLock.AssertCurrentThreadOwns(); mExpirationArray.RemoveElement(aRec); } void SSLTokensCache::EvictIfNecessary() { sLock.AssertCurrentThreadOwns(); // kilobytes to bytes uint32_t capacity = StaticPrefs::network_ssl_tokens_cache_capacity() << 10; if (mCacheSize <= capacity) { return; } LOG(("SSLTokensCache::EvictIfNecessary: evicting")); mExpirationArray.Sort(ExpirationComparator()); PRTime now = PR_Now(); while (mCacheSize > capacity && mExpirationArray.Length() > 0) { auto* rec = mExpirationArray[0]; // Only count evictions of tokens that are still valid; already-expired // tokens would be discarded on lookup anyway and are tracked separately // by ssl_token_cache_expired. if (rec->mExpirationTime > now) { mozilla::glean::network::ssl_token_cache_evictions.Add(1); } DebugOnly rv = RemoveLocked(rec->mKey, rec->mId); MOZ_ASSERT(NS_SUCCEEDED(rv), "mExpirationArray and mTokenCacheRecords are out of sync!"); } } void SSLTokensCache::LogStats() { sLock.AssertCurrentThreadOwns(); if (!LOG5_ENABLED()) { return; } LOG(("SSLTokensCache::LogStats [count=%zu, cacheSize=%u]", mExpirationArray.Length(), mCacheSize)); for (const auto& ent : mTokenCacheRecords.Values()) { const UniquePtr& rec = ent->Get(); LOG((" [key=%s, count=%d]", rec->mKey.get(), ent->RecordCount())); } } size_t SSLTokensCache::SizeOfIncludingThis( mozilla::MallocSizeOf mallocSizeOf) const { size_t n = mallocSizeOf(this); n += mTokenCacheRecords.ShallowSizeOfExcludingThis(mallocSizeOf); n += mExpirationArray.ShallowSizeOfExcludingThis(mallocSizeOf); for (const auto* rec : mExpirationArray) { n += mallocSizeOf(rec); n += rec->mKey.SizeOfExcludingThisIfUnshared(mallocSizeOf); n += rec->mCompressedPayload.ShallowSizeOfExcludingThis(mallocSizeOf); } return n; } MOZ_DEFINE_MALLOC_SIZE_OF(SSLTokensCacheMallocSizeOf) NS_IMETHODIMP SSLTokensCache::CollectReports(nsIHandleReportCallback* aHandleReport, nsISupports* aData, bool aAnonymize) { StaticMutexAutoLock lock(sLock); MOZ_COLLECT_REPORT("explicit/network/ssl-tokens-cache", KIND_HEAP, UNITS_BYTES, SizeOfIncludingThis(SSLTokensCacheMallocSizeOf), "Memory used for the SSL tokens cache."); return NS_OK; } // static void SSLTokensCache::ClearPrivateBrowsing() { LOG(("SSLTokensCache::ClearPrivateBrowsing")); StaticMutexAutoLock lock(sLock); if (!gInstance) { return; } gInstance->RemoveMatchingLocked([](const nsACString& aKey) { // Non-PBM keys have no '^' OA suffix at all — skip full deserialization. if (!aKey.Contains('^')) return false; return OAFromPeerId(aKey).mPrivateBrowsingId != 0; }); } template static void ForwardToSocketProcess(SendFn aSend) { if (!XRE_IsParentProcess()) { return; } if (nsIOService::UseSocketProcess() && gIOService) { gIOService->CallOrWaitForSocketProcess([send = std::move(aSend)]() { RefPtr socketParent = SocketProcessParent::GetSingleton(); if (socketParent) { send(socketParent); } }); } } // static void SSLTokensCache::ForwardClearToSocketProcess() { ForwardToSocketProcess( [](SocketProcessParent* p) { (void)p->SendClearSessionCache(); }); } static void MaybeClearNSSSessionCache() { if (NSS_IsInitialized()) { SSL_ClearSessionCache(); } } // static void SSLTokensCache::ClearSessionCacheAndTokens() { MaybeClearNSSSessionCache(); Clear(); ForwardClearToSocketProcess(); } // static void SSLTokensCache::ForwardClearPrivateBrowsingToSocketProcess() { ForwardToSocketProcess([](SocketProcessParent* p) { (void)p->SendClearPrivateBrowsingSessionCache(); }); } // static void SSLTokensCache::ClearSessionCacheAndPBMTokens() { MOZ_ASSERT(!XRE_IsParentProcess()); MaybeClearNSSSessionCache(); ClearPrivateBrowsing(); } // static void SSLTokensCache::Clear() { LOG(("SSLTokensCache::Clear")); nsCOMPtr queue; { StaticMutexAutoLock lock(sLock); if (!gInstance) { LOG((" service not initialized")); return; } gInstance->ClearCacheLocked(); if (gInstance->mDBActive) { queue = gInstance->mDBQueue; } } if (queue) { // The serial queue guarantees this runs after any pending write. queue->Dispatch( NS_NewRunnableFunction("SSLTokensCache::ClearDB", [] { ClearDB(); }), NS_DISPATCH_EVENT_MAY_BLOCK); } } void SSLTokensCache::DoWrite(bool aSynchronous) { nsCOMPtr queue; { StaticMutexAutoLock lock(sLock); if (!gInstance) { return; } if (mDBActive) { queue = mDBQueue; } } if (!queue) { if (XRE_IsSocketProcess()) { nsTArray snapshot; { StaticMutexAutoLock lock(sLock); if (gInstance) { gInstance->CollectRecordInfosLocked(snapshot, /* aFilterForPersistence */ true); } } if (!snapshot.IsEmpty()) { NS_DispatchToMainThread(NS_NewRunnableFunction( "SSLTokensCache::SendToParent", [records = std::move(snapshot)]() mutable { auto* child = SocketProcessChild::GetSingleton(); if (child && child->CanSend()) { (void)child->SendSSLTokensCacheData(std::move(records)); } })); } } return; } if (aSynchronous) { // Test-only synchronous path; block until write completes on the queue. nsCOMPtr event = NS_NewRunnableFunction("SSLTokensCache::DoWriteSync", [] { CollectAndWriteSnapshotIfLoaded(); }); NS_DispatchAndSpinEventLoopUntilComplete( "SSLTokensCache::DoWrite synchronous test-only write"_ns, queue.get(), event.forget()); return; } queue->Dispatch( NS_NewRunnableFunction("SSLTokensCache::DoWriteAsync", [] { CollectAndWriteSnapshotIfLoaded(); }), NS_DISPATCH_EVENT_MAY_BLOCK); } // static void SSLTokensCache::CollectAndWriteSnapshotIfLoaded() { MOZ_ASSERT(!NS_IsMainThread()); // The serial queue guarantees this runs after any queued OpenAndLoad. nsTArray snapshot; { StaticMutexAutoLock lock(sLock); if (!gInstance || !gInstance->mLoadComplete) { return; } gInstance->CollectRecordInfosLocked(snapshot, /* aFilterForPersistence */ true); } WriteSnapshotToDB(std::move(snapshot)); } // static void SSLTokensCache::DispatchOpenAndLoad(uint32_t aLoadGen) { nsCOMPtr queue; { StaticMutexAutoLock lock(sLock); if (!gInstance || !gInstance->mDBActive) { return; } queue = gInstance->mDBQueue; } queue->Dispatch(NS_NewRunnableFunction("SSLTokensCache::OpenAndLoad", [aLoadGen]() { uint32_t count = LoadFromDB(aLoadGen); OnLoadCompleteNotify(count); }), NS_DISPATCH_EVENT_MAY_BLOCK); } // static void SSLTokensCache::OnLoadCompleteNotify(uint32_t aCount) { mozilla::glean::network::ssl_token_cache_persistence_records_loaded.Add( AssertedCast(aCount)); TimeDuration elapsed; { StaticMutexAutoLock lock(sLock); if (!gInstance) { return; } gInstance->mLoadComplete = true; elapsed = TimeStamp::Now() - gInstance->mLoadStartTime; } mozilla::glean::network::ssl_token_cache_load_time.AccumulateRawDuration( elapsed); LOG(("SSLTokensCache::OnLoadCompleteNotify [records=%u, time=%.1fms]", aCount, elapsed.ToMilliseconds())); // Forward persisted tokens to the socket process. Uses // CallOrWaitForSocketProcess so it fires immediately if the socket process // is already up, or is deferred until it is ready. if (StaticPrefs::network_ssl_tokens_cache_persistence()) { NS_DispatchToMainThread( NS_NewRunnableFunction("SSLTokensCache::ForwardToSocketProcess", []() { if (!gIOService || !nsIOService::UseSocketProcess()) { return; } // Serialize on a background thread, then send on the main thread. // No captures: CallOrWaitForSocketProcess copies its callable when // deferring, so the lambda must remain copyable. gIOService->CallOrWaitForSocketProcess([]() { NS_DispatchBackgroundTask(NS_NewRunnableFunction( "SSLTokensCache::SerializeForSocket", []() { nsTArray records = SSLTokensCache::SerializeForIPC(); if (records.IsEmpty()) { return; } NS_DispatchToMainThread(NS_NewRunnableFunction( "SSLTokensCache::SendToSocket", [records = std::move(records)]() mutable { RefPtr parent = SocketProcessParent::GetSingleton(); if (parent && parent->CanSend()) { (void)parent->SendLoadSSLTokensCache( std::move(records)); } })); })); }); })); } } // static UniquePtr SSLTokensCache::MakeRecord( const nsACString& aKey, PRTime aExpirationTime, uint8_t aOverridableError, bool aRestored, nsTArray&& aCompressedPayload) { auto rec = MakeUnique(); rec->mKey = aKey; rec->mExpirationTime = aExpirationTime; rec->mOverridableError = aOverridableError; rec->mRestored = aRestored; rec->mCompressedPayload = std::move(aCompressedPayload); return rec; } // static bool SSLTokensCache::PutFromPersisted(const SSLTokensCacheRecordInfo& aRec, uint32_t aExpectedGen, bool aRestored) { StaticMutexAutoLock lock(sLock); if (!gInstance || gInstance->mLoadGeneration != aExpectedGen) { return false; } auto rec = MakeRecord(aRec.key, static_cast(aRec.expirationTime), aRec.overridableError, aRestored, aRec.compressedPayload.Clone()); gInstance->InsertRecordLocked(std::move(rec)); return true; } #ifdef DEBUG // static void SSLTokensCache::AssertOnDBThread() { MOZ_ASSERT(!NS_IsMainThread()); nsCOMPtr queue; { StaticMutexAutoLock lock(sLock); if (gInstance) { queue = gInstance->mDBQueue; } } MOZ_ASSERT(!queue || queue->IsOnCurrentThread()); } #endif // static bool SSLTokensCache::IsPersistenceStillActive() { // Re-checked so a task queued before the pref flipped off can't write. if (!StaticPrefs::network_ssl_tokens_cache_persistence()) { return false; } StaticMutexAutoLock lock(sLock); return gInstance && gInstance->mDBActive; } // static already_AddRefed SSLTokensCache::GetCachedDBFile() { StaticMutexAutoLock lock(sLock); if (!gInstance || !gInstance->mDBFile) { return nullptr; } nsCOMPtr dbFile = gInstance->mDBFile; return dbFile.forget(); } // static bool SSLTokensCache::RemoveDBFile(nsIFile* aDbFile) { MOZ_ASSERT(!NS_IsMainThread()); return NS_SUCCEEDED(aDbFile->Remove(false)); } // True if the file itself is unusable, so removal is the only recovery. static bool DBFileIsUnusable(nsresult aRv) { switch (aRv) { case NS_ERROR_STORAGE_BUSY: case NS_ERROR_FILE_IS_LOCKED: case NS_ERROR_FILE_ACCESS_DENIED: case NS_ERROR_FILE_READ_ONLY: case NS_ERROR_OUT_OF_MEMORY: case NS_ERROR_NOT_INITIALIZED: return false; default: return true; } } // Shared by OpenDB and the test-only connection helper below. static nsresult OpenUnsharedDatabaseAt(nsIFile* aFile, mozIStorageConnection** aConn) { nsCOMPtr storageService = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID); if (!storageService) { return NS_ERROR_FAILURE; } return storageService->OpenUnsharedDatabase( aFile, mozIStorageService::CONNECTION_DEFAULT, aConn); } // static bool SSLTokensCache::OpenDB(nsCOMPtr& aConn) { MOZ_ASSERT(!NS_IsMainThread()); #ifdef DEBUG AssertOnDBThread(); #endif MOZ_ASSERT(!aConn); nsCOMPtr dbFile = GetCachedDBFile(); if (!dbFile) { return false; } nsresult rv = OpenUnsharedDatabaseAt(dbFile, getter_AddRefs(aConn)); // Corruption or a missing encryption key never opens again; retry once. if (NS_FAILED(rv) && DBFileIsUnusable(rv)) { LOG(("SSLTokensCache::OpenDB: unopenable (0x%" PRIx32 "), removing", static_cast(rv))); mozilla::glean::network::ssl_token_cache_db_errors.Get("open"_ns).Add(1); aConn = nullptr; RemoveDBFile(dbFile); rv = OpenUnsharedDatabaseAt(dbFile, getter_AddRefs(aConn)); } if (NS_FAILED(rv)) { aConn = nullptr; return false; } // Disposable cache, so skip fsync entirely. (void)NS_WARN_IF( NS_FAILED(aConn->ExecuteSimpleSQL("PRAGMA synchronous = OFF"_ns))); // Tolerate brief contention; matches Places' DATABASE_BUSY_TIMEOUT_MS. (void)NS_WARN_IF( NS_FAILED(aConn->ExecuteSimpleSQL("PRAGMA busy_timeout = 100"_ns))); if (NS_FAILED(EnsureSchema(aConn))) { aConn->Close(); aConn = nullptr; // Opened but unusable; drop it so the next open starts clean. mozilla::glean::network::ssl_token_cache_db_errors.Get("schema"_ns).Add(1); (void)RemoveDBFile(dbFile); return false; } return true; } // static already_AddRefed SSLTokensCache::OpenActiveDB() { MOZ_ASSERT(!NS_IsMainThread()); if (!IsPersistenceStillActive()) { return nullptr; } nsCOMPtr conn; if (!OpenDB(conn)) { return nullptr; } return conn.forget(); } // static uint32_t SSLTokensCache::LoadFromDB(uint32_t aLoadGen) { nsCOMPtr conn = OpenActiveDB(); if (!conn) { return 0; } bool corrupted = false; uint32_t count = LoadValidRecordsFrom(conn, aLoadGen, &corrupted); conn->Close(); if (corrupted) { mozilla::glean::network::ssl_token_cache_db_errors.Get("read"_ns).Add(1); nsCOMPtr dbFile = GetCachedDBFile(); if (dbFile) { RemoveDBFile(dbFile); } } return count; } // static void SSLTokensCache::WriteSnapshotToDB( nsTArray&& aSnapshot) { nsCOMPtr conn = OpenActiveDB(); if (!conn) { return; } nsCOMPtr stmtInsert; nsresult rv = PrepareInsertStatement(conn, stmtInsert); if (NS_SUCCEEDED(rv)) { rv = WriteSnapshotTo(conn, stmtInsert, aSnapshot); stmtInsert = nullptr; } if (NS_WARN_IF(NS_FAILED(rv))) { mozilla::glean::network::ssl_token_cache_db_errors.Get("write"_ns).Add(1); } conn->Close(); } // static void SSLTokensCache::ClearDB() { nsCOMPtr conn = OpenActiveDB(); if (!conn) { return; } (void)NS_WARN_IF( NS_FAILED(conn->ExecuteSimpleSQL("DELETE FROM ssl_tokens"_ns))); conn->Close(); } // static void SSLTokensCache::RemoveDBFileSync() { nsCOMPtr dbFile = GetCachedDBFile(); if (dbFile) { (void)RemoveDBFile(dbFile); } } uint64_t SSLTokensCache::InsertRecordLocked(UniquePtr aRec) { sLock.AssertCurrentThreadOwns(); const uint64_t id = ++sRecordId; aRec->mId = id; // aRec->mKey must be read before AddRecord() moves aRec. TokenCacheEntry* cacheEntry = mTokenCacheRecords.GetOrInsertNew(aRec->mKey); if (cacheEntry->RecordCount() > 0) { mCacheSize -= cacheEntry->Size(); } cacheEntry->AddRecord(std::move(aRec), mExpirationArray); mCacheSize += cacheEntry->Size(); EvictIfNecessary(); return id; } // static bool SSLTokensCache::ShouldPersistKey(const nsACString& aKey, uint8_t aOverridableError) { return aOverridableError == 0 && OAFromPeerId(aKey).mPrivateBrowsingId == 0; } // static void SSLTokensCache::RemoveByMatchAndOAPattern( const nsACString& aValue, const nsACString& aSeparatedValue, const mozilla::OriginAttributesPattern& aPattern) { StaticMutexAutoLock lock(sLock); if (!gInstance) { return; } gInstance->RemoveMatchingLocked( [&aValue, &aSeparatedValue, &aPattern](const nsACString& aKey) { nsDependentCSubstring host = HostFromBasePart(BasePartFromKey(aKey)); return !host.IsEmpty() && (host.Equals(aValue) || StringEndsWith(host, aSeparatedValue)) && aPattern.Matches(OAFromPeerId(aKey)); }); } // static void SSLTokensCache::RemoveByHostAndOAPattern( const nsACString& aHost, const mozilla::OriginAttributesPattern& aPattern) { LOG(("SSLTokensCache::RemoveByHostAndOAPattern")); RemoveByMatchAndOAPattern(aHost, ":"_ns + aHost, aPattern); } // static void SSLTokensCache::ClearSessionCacheAndTokensForHost( const nsACString& aHost, const mozilla::OriginAttributesPattern& aPattern) { LOG(("SSLTokensCache::ClearSessionCacheAndTokensForHost")); MaybeClearNSSSessionCache(); RemoveByHostAndOAPattern(aHost, aPattern); ForwardClearToSocketProcess(); } // static void SSLTokensCache::RemoveBySiteAndOAPattern( const nsACString& aSite, const mozilla::OriginAttributesPattern& aPattern) { LOG(("SSLTokensCache::RemoveBySiteAndOAPattern")); // Three cases for the host part of a key (after HostFromBasePart): // "example.com" — exact, no prefix → Equals(aSite) // "sub.example.com" — subdomain → ends with "."_ns + aSite // "anon:example.com" — connection-type prefix → ends with ":"_ns + aSite // (Prefixed subdomains like "anon:sub.example.com" are caught by dotSite.) nsAutoCString dotSite("."_ns + aSite); nsAutoCString colonSite(":"_ns + aSite); StaticMutexAutoLock lock(sLock); if (!gInstance) { return; } gInstance->RemoveMatchingLocked( [&aSite, &dotSite, &colonSite, &aPattern](const nsACString& aKey) { nsDependentCSubstring host = HostFromBasePart(BasePartFromKey(aKey)); return !host.IsEmpty() && (host.Equals(aSite) || StringEndsWith(host, dotSite) || StringEndsWith(host, colonSite)) && aPattern.Matches(OAFromPeerId(aKey)); }); } #ifdef ENABLE_TESTS // Throwaway connection at aPath, independent of mDBQueue/OpenDB, so tests // can exercise persistence without the pref or observers. static already_AddRefed OpenTestConnection( const nsACString& aPath) { nsCOMPtr file; if (NS_FAILED(NS_NewNativeLocalFile(aPath, getter_AddRefs(file)))) { return nullptr; } nsCOMPtr conn; if (NS_FAILED(OpenUnsharedDatabaseAt(file, getter_AddRefs(conn)))) { return nullptr; } return conn.forget(); } // static void SSLTokensCache::TriggerWriteForTest(const nsACString& aPath) { nsTArray snapshot; { StaticMutexAutoLock lock(sLock); if (gInstance) { gInstance->CollectRecordInfosLocked(snapshot, /* aFilterForPersistence */ true); } } nsCOMPtr conn = OpenTestConnection(aPath); if (!conn) { return; } if (NS_FAILED(EnsureSchema(conn))) { conn->Close(); return; } nsCOMPtr stmt; if (NS_SUCCEEDED(PrepareInsertStatement(conn, stmt))) { (void)NS_WARN_IF(NS_FAILED(WriteSnapshotTo(conn, stmt, snapshot))); stmt = nullptr; } conn->Close(); } // static void SSLTokensCache::LoadForTest(const nsACString& aPath) { uint32_t loadGen = 0; { StaticMutexAutoLock lock(sLock); if (gInstance) { loadGen = gInstance->mLoadGeneration; } } nsCOMPtr conn = OpenTestConnection(aPath); if (!conn) { return; } bool tableExists = false; if (NS_SUCCEEDED(conn->TableExists("ssl_tokens"_ns, &tableExists)) && tableExists) { LoadValidRecordsFrom(conn, loadGen); } conn->Close(); } // static uint32_t SSLTokensCache::CountForTest() { StaticMutexAutoLock lock(sLock); if (!gInstance) { return 0; } return gInstance->mTokenCacheRecords.Count(); } // static uint32_t SSLTokensCache::CacheSizeForTest() { StaticMutexAutoLock lock(sLock); return gInstance ? gInstance->mCacheSize : 0; } // static void SSLTokensCache::PutForTest(const nsACString& aKey) { uint32_t gen = 0; { StaticMutexAutoLock lock(sLock); if (gInstance) { gen = gInstance->mLoadGeneration; } } uint8_t dummyToken[] = {0xDE, 0xAD, 0xBE, 0xEF}; SessionCacheInfo info; nsTArray payload = SerializeRecord({dummyToken, sizeof(dummyToken)}, info); nsTArray compressed = CompressRecord(payload); if (compressed.IsEmpty()) { return; } SSLTokensCacheRecordInfo rec{}; rec.key = aKey; rec.expirationTime = PR_Now() + 3600LL * PR_USEC_PER_SEC; rec.compressedPayload = std::move(compressed); PutFromPersisted(rec, gen, /* aRestored */ false); } #endif // ENABLE_TESTS // static void SSLTokensCache::ReconcilePersistence(const char*, void*) { MOZ_ASSERT(XRE_IsParentProcess()); nsCOMPtr obs = mozilla::services::GetObserverService(); if (!obs) { return; } bool wantPersistence = StaticPrefs::network_ssl_tokens_cache_persistence(); // Checked before sLock; see SetupPersistenceLocked's doc comment. bool storageServiceAvailable = false; if (wantPersistence) { nsCOMPtr storageService = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID); storageServiceAvailable = storageService != nullptr; } bool addObservers = false; bool removeObservers = false; bool loadOnActivation = false; uint32_t loadGen = 0; RefPtr instance; nsCOMPtr queueForRemoval; { StaticMutexAutoLock lock(sLock); instance = gInstance; if (!instance) { return; } bool wasRegistered = instance->mWriteObserversRegistered; instance->mWriteObserversRegistered = wantPersistence; addObservers = wantPersistence && !wasRegistered; removeObservers = !wantPersistence && wasRegistered; if (wantPersistence && !instance->mDBActive && SetupPersistenceLocked(loadGen, storageServiceAvailable)) { // Only the startup activation loads from disk; a later re-enable // resumes writing without reloading a possibly-stale cache. loadOnActivation = !instance->mPrefCallbackRegistered; instance->mLoadComplete = !loadOnActivation; } if (!wantPersistence && instance->mDBActive) { instance->mDBActive = false; // mDBQueue stays alive, only the DB file is removed. queueForRemoval = instance->mDBQueue; instance->ClearCacheLocked(); } } if (!wantPersistence) { // Drop any DB left behind by a session that had persistence on. if (queueForRemoval) { // Ordered after any pending write. queueForRemoval->Dispatch( NS_NewRunnableFunction("SSLTokensCache::RemoveDB", []() { RemoveDBFileSync(); }), NS_DISPATCH_EVENT_MAY_BLOCK); } else if (nsCOMPtr dbFile = ProfileDBFile()) { NS_DispatchBackgroundTask( NS_NewRunnableFunction("SSLTokensCache::RemoveDB", [dbFile]() { (void)RemoveDBFile(dbFile); })); } } if (addObservers) { obs->AddObserver(instance, "application-background", false); obs->AddObserver(instance, "idle-daily", false); } else if (removeObservers) { obs->RemoveObserver(instance, "application-background"); obs->RemoveObserver(instance, "idle-daily"); } if (wantPersistence) { if (loadOnActivation) { DispatchOpenAndLoad(loadGen); } instance->RegisterShutdownBlocker(); } } NS_IMETHODIMP SSLTokensCache::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (!strcmp(aTopic, "application-background") || !strcmp(aTopic, "idle-daily")) { LOG(("SSLTokensCache::Observe [topic=%s]", aTopic)); DoWrite(false); } else if (!strcmp(aTopic, "profile-after-change")) { MOZ_ASSERT(XRE_IsParentProcess()); LOG(("SSLTokensCache::Observe [topic=profile-after-change]")); ReconcilePersistence(); if (!mPrefCallbackRegistered) { // Register for live pref changes now that the JS environment is ready. // Registering earlier (in Init()) would fire the callback during startup // pref-loading before the JS module loader is initialised. Preferences::RegisterCallback(&SSLTokensCache::ReconcilePersistence, "network.ssl_tokens_cache_persistence"); mPrefCallbackRegistered = true; } } else if (!strcmp(aTopic, "last-pb-context-exited")) { MOZ_ASSERT(XRE_IsParentProcess()); LOG(("SSLTokensCache::Observe [topic=last-pb-context-exited]")); // Remove only PBM entries from this process's token cache. ClearPrivateBrowsing(); // Forward a PBM-scoped clear to the socket process. ForwardClearPrivateBrowsingToSocketProcess(); } return NS_OK; } // nsIAsyncShutdownBlocker NS_IMETHODIMP SSLTokensCache::BlockShutdown(nsIAsyncShutdownClient* /* aClient */) { LOG(("SSLTokensCache::BlockShutdown")); MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(XRE_IsParentProcess()); nsCOMPtr queue; { StaticMutexAutoLock lock(sLock); if (mDBActive) { queue = mDBQueue; } } if (!queue) { RemoveShutdownBlocker(); return NS_OK; } // Merge the socket process's tokens, then write the final snapshot. The // blocker keeps the main thread alive until that completes. RefPtr self = this; auto writeAndRelease = [queue, self](nsTArray aRecords) { InvokeAsync(queue.get(), __func__, [records = std::move(aRecords)]() mutable { if (!records.IsEmpty()) { SSLTokensCache::DeserializeFromIPC(std::move(records), /* aRestored */ false); } CollectAndWriteSnapshotIfLoaded(); return GenericPromise::CreateAndResolve(true, __func__); }) ->Then( GetMainThreadSerialEventTarget(), __func__, [self](bool) { self->RemoveShutdownBlocker(); }, [self](nsresult) { self->RemoveShutdownBlocker(); }); }; // If the socket process is alive, flush its token cache first so the // persisted DB reflects the most recent handshake data. RefPtr socketParent = SocketProcessParent::GetSingleton(); if (!socketParent || !socketParent->CanSend()) { writeAndRelease({}); return NS_OK; } socketParent->SendFlushSSLTokensCache()->Then( GetMainThreadSerialEventTarget(), __func__, [writeAndRelease](nsTArray&& aRecords) { writeAndRelease(std::move(aRecords)); }, [writeAndRelease](mozilla::ipc::ResponseRejectReason) { writeAndRelease({}); }); return NS_OK; } NS_IMETHODIMP SSLTokensCache::GetName(nsAString& aName) { aName.AssignLiteral("SSLTokensCache: writing cache to disk"); return NS_OK; } NS_IMETHODIMP SSLTokensCache::GetState(nsIPropertyBag** aState) { *aState = nullptr; return NS_OK; } void SSLTokensCache::RegisterShutdownBlocker() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(XRE_IsParentProcess()); { StaticMutexAutoLock lock(sLock); if (!gInstance || !gInstance->mDBQueue) { return; } if (gInstance->mShutdownBarrier) { return; } } // sLock is intentionally released before the AsyncShutdown calls below: // Service() and AddBlocker may run JS, which mustn't happen under sLock. nsCOMPtr svc = components::AsyncShutdown::Service(); if (!svc) { return; } nsCOMPtr client; svc->GetProfileBeforeChange(getter_AddRefs(client)); if (!client) { return; } LOG(("SSLTokensCache::RegisterShutdownBlocker")); nsresult rv = client->AddBlocker( this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, u""_ns); if (NS_WARN_IF(NS_FAILED(rv))) { // Too late to add a blocker, so don't record one: Shutdown() would // assume it is pending and skip its own cleanup. return; } { StaticMutexAutoLock lock(sLock); mShutdownBarrier = client; } } void SSLTokensCache::RemoveShutdownBlocker() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(XRE_IsParentProcess()); // Shutdown() left gInstance set so BlockShutdown could write, so // unregister here instead. UnregisterFromServices(this); nsCOMPtr barrier; { StaticMutexAutoLock lock(sLock); barrier = std::move(mShutdownBarrier); gInstance = nullptr; } if (barrier) { barrier->RemoveBlocker(this); } } #ifdef ENABLE_TESTS NS_IMPL_ISUPPORTS(SSLTokensCacheService, nsISSLTokensCache, nsISSLTokensCacheTest) #else NS_IMPL_ISUPPORTS(SSLTokensCacheService, nsISSLTokensCache) #endif // Most of nsISSLTokensCache is parent-process-only; the socket process has // no way to serve these requests itself. static nsresult EnsureParentProcess() { MOZ_ASSERT(XRE_IsParentProcess()); return XRE_IsParentProcess() ? NS_OK : NS_ERROR_NOT_AVAILABLE; } NS_IMETHODIMP SSLTokensCacheService::ClearSSLExternalAndInternalSessionCache() { nsresult rv = EnsureParentProcess(); if (NS_FAILED(rv)) { return rv; } SSLTokensCache::ClearSessionCacheAndTokens(); return NS_OK; } NS_IMETHODIMP SSLTokensCacheService::AsyncClearSSLExternalAndInternalSessionCache( JSContext* aCx, mozilla::dom::Promise** aPromise) { nsresult rv = EnsureParentProcess(); if (NS_FAILED(rv)) { return rv; } nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx); if (NS_WARN_IF(!globalObject)) { return NS_ERROR_FAILURE; } ErrorResult result; RefPtr promise = mozilla::dom::Promise::Create(globalObject, result); if (NS_WARN_IF(result.Failed())) { return result.StealNSResult(); } if (nsIOService::UseSocketProcess() && gIOService) { gIOService->CallOrWaitForSocketProcess([p = RefPtr{promise}]() { RefPtr socketParent = SocketProcessParent::GetSingleton(); (void)socketParent->SendClearSessionCache()->Then( GetCurrentSerialEventTarget(), __func__, [promise = RefPtr{p}] { promise->MaybeResolveWithUndefined(); }, [promise = RefPtr{p}] { promise->MaybeReject(NS_ERROR_UNEXPECTED); }); }); } else { promise->MaybeResolveWithUndefined(); } SSLTokensCache::ClearSessionCacheAndTokens(); promise.forget(aPromise); return NS_OK; } template static nsresult WithParsedOAPattern(const nsAString& aPatternJson, F&& aFunc) { nsresult rv = EnsureParentProcess(); if (NS_FAILED(rv)) { return rv; } mozilla::OriginAttributesPattern pattern; if (!pattern.Init(aPatternJson)) { return NS_ERROR_INVALID_ARG; } aFunc(pattern); return NS_OK; } NS_IMETHODIMP SSLTokensCacheService::RemoveSSLTokensByHostAndOriginAttributesPattern( const nsACString& aHost, const nsAString& aPattern) { return WithParsedOAPattern(aPattern, [&aHost](const auto& pattern) { SSLTokensCache::RemoveByHostAndOAPattern(aHost, pattern); }); } NS_IMETHODIMP SSLTokensCacheService::RemoveSSLTokensBySiteAndOriginAttributesPattern( const nsACString& aSite, const nsAString& aPattern) { return WithParsedOAPattern(aPattern, [&aSite](const auto& pattern) { SSLTokensCache::RemoveBySiteAndOAPattern(aSite, pattern); }); } #ifdef ENABLE_TESTS NS_IMETHODIMP SSLTokensCacheService::CountSSLTokens(uint32_t* aCount) { *aCount = SSLTokensCache::CountForTest(); return NS_OK; } NS_IMETHODIMP SSLTokensCacheService::PutSSLTokenForTest(const nsACString& aKey) { SSLTokensCache::PutForTest(aKey); return NS_OK; } #endif // ENABLE_TESTS } // namespace net } // namespace mozilla