1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "AntiTrackingLog.h"
8 #include "ContentBlockingLog.h"
9 
10 #include "nsITrackingDBService.h"
11 #include "nsServiceManagerUtils.h"
12 #include "nsTArray.h"
13 #include "mozilla/HashFunctions.h"
14 #include "mozilla/Preferences.h"
15 #include "mozilla/RandomNum.h"
16 #include "mozilla/ReverseIterator.h"
17 #include "mozilla/StaticPrefs_browser.h"
18 #include "mozilla/StaticPrefs_privacy.h"
19 #include "mozilla/StaticPrefs_telemetry.h"
20 #include "mozilla/Telemetry.h"
21 #include "mozilla/XorShift128PlusRNG.h"
22 
23 namespace mozilla {
24 
25 using ipc::AutoIPCStream;
26 
27 typedef Telemetry::OriginMetricID OriginMetricID;
28 
29 // sync with TelemetryOriginData.inc
30 const nsLiteralCString ContentBlockingLog::kDummyOriginHash = "PAGELOAD"_ns;
31 
32 // randomly choose 1% users included in the content blocking measurement
33 // based on their client id.
34 static constexpr double kRatioReportUser = 0.01;
35 
36 // randomly choose 0.14% documents when the page is unload.
37 static constexpr double kRatioReportDocument = 0.0014;
38 
IsReportingPerUserEnabled()39 static bool IsReportingPerUserEnabled() {
40   MOZ_ASSERT(NS_IsMainThread());
41 
42   static Maybe<bool> sIsReportingEnabled;
43 
44   if (sIsReportingEnabled.isSome()) {
45     return sIsReportingEnabled.value();
46   }
47 
48   nsAutoCString cachedClientId;
49   if (NS_FAILED(Preferences::GetCString("toolkit.telemetry.cachedClientID",
50                                         cachedClientId))) {
51     return false;
52   }
53 
54   nsID clientId;
55   if (!clientId.Parse(cachedClientId.get())) {
56     return false;
57   }
58 
59   /**
60    * UUID might not be uniform-distributed (although some part of it could be).
61    * In order to generate more random result, usually we use a hash function,
62    * but here we hope it's fast and doesn't have to be cryptographic-safe.
63    * |XorShift128PlusRNG| looks like a good alternative because it takes a
64    * 128-bit data as its seed and always generate identical sequence if the
65    * initial seed is the same.
66    */
67   static_assert(sizeof(nsID) == 16, "nsID is 128-bit");
68   uint64_t* init = reinterpret_cast<uint64_t*>(&clientId);
69   non_crypto::XorShift128PlusRNG rng(init[0], init[1]);
70   sIsReportingEnabled.emplace(rng.nextDouble() <= kRatioReportUser);
71 
72   return sIsReportingEnabled.value();
73 }
74 
IsReportingPerDocumentEnabled()75 static bool IsReportingPerDocumentEnabled() {
76   constexpr double boundary =
77       kRatioReportDocument * double(std::numeric_limits<uint64_t>::max());
78   Maybe<uint64_t> randomNum = RandomUint64();
79   return randomNum.isSome() && randomNum.value() <= boundary;
80 }
81 
IsReportingEnabled()82 static bool IsReportingEnabled() {
83   if (StaticPrefs::telemetry_origin_telemetry_test_mode_enabled()) {
84     return true;
85   } else if (!StaticPrefs::
86                  privacy_trackingprotection_origin_telemetry_enabled()) {
87     return false;
88   }
89 
90   return IsReportingPerUserEnabled() && IsReportingPerDocumentEnabled();
91 }
92 
ReportOriginSingleHash(OriginMetricID aId,const nsACString & aOrigin)93 static void ReportOriginSingleHash(OriginMetricID aId,
94                                    const nsACString& aOrigin) {
95   MOZ_ASSERT(XRE_IsParentProcess());
96   MOZ_ASSERT(NS_IsMainThread());
97 
98   LOG(("ReportOriginSingleHash metric=%s",
99        Telemetry::MetricIDToString[static_cast<uint32_t>(aId)]));
100   LOG(("ReportOriginSingleHash origin=%s", PromiseFlatCString(aOrigin).get()));
101 
102   Telemetry::RecordOrigin(aId, aOrigin);
103 }
104 
RecordLogParent(const nsACString & aOrigin,uint32_t aType,bool aBlocked,const Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason> & aReason,const nsTArray<nsCString> & aTrackingFullHashes)105 Maybe<uint32_t> ContentBlockingLog::RecordLogParent(
106     const nsACString& aOrigin, uint32_t aType, bool aBlocked,
107     const Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason>&
108         aReason,
109     const nsTArray<nsCString>& aTrackingFullHashes) {
110   MOZ_ASSERT(XRE_IsParentProcess());
111 
112   uint32_t events = GetContentBlockingEventsInLog();
113 
114   bool blockedValue = aBlocked;
115   bool unblocked = false;
116 
117   switch (aType) {
118     case nsIWebProgressListener::STATE_COOKIES_LOADED:
119       MOZ_ASSERT(!aBlocked,
120                  "We don't expected to see blocked STATE_COOKIES_LOADED");
121       [[fallthrough]];
122 
123     case nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER:
124       MOZ_ASSERT(
125           !aBlocked,
126           "We don't expected to see blocked STATE_COOKIES_LOADED_TRACKER");
127       [[fallthrough]];
128 
129     case nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER:
130       MOZ_ASSERT(!aBlocked,
131                  "We don't expected to see blocked "
132                  "STATE_COOKIES_LOADED_SOCIALTRACKER");
133       // Note that the logic in these branches are the logical negation of the
134       // logic in other branches, since the Document API we have is phrased
135       // in "loaded" terms as opposed to "blocked" terms.
136       blockedValue = !aBlocked;
137       [[fallthrough]];
138 
139     case nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT:
140     case nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT:
141     case nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT:
142     case nsIWebProgressListener::STATE_BLOCKED_FINGERPRINTING_CONTENT:
143     case nsIWebProgressListener::STATE_LOADED_FINGERPRINTING_CONTENT:
144     case nsIWebProgressListener::STATE_BLOCKED_CRYPTOMINING_CONTENT:
145     case nsIWebProgressListener::STATE_LOADED_CRYPTOMINING_CONTENT:
146     case nsIWebProgressListener::STATE_BLOCKED_SOCIALTRACKING_CONTENT:
147     case nsIWebProgressListener::STATE_LOADED_SOCIALTRACKING_CONTENT:
148     case nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION:
149     case nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL:
150     case nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN:
151       RecordLogInternal(aOrigin, aType, blockedValue);
152       break;
153 
154     case nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER:
155     case nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER:
156       RecordLogInternal(aOrigin, aType, blockedValue, aReason,
157                         aTrackingFullHashes);
158       break;
159 
160     case nsIWebProgressListener::STATE_REPLACED_TRACKING_CONTENT:
161     case nsIWebProgressListener::STATE_ALLOWED_TRACKING_CONTENT:
162       RecordLogInternal(aOrigin, aType, blockedValue);
163       break;
164 
165     default:
166       // Ignore nsIWebProgressListener::STATE_BLOCKED_UNSAFE_CONTENT;
167       break;
168   }
169 
170   if (!aBlocked) {
171     unblocked = (events & aType) != 0;
172   }
173 
174   const uint32_t oldEvents = events;
175   if (blockedValue) {
176     events |= aType;
177   } else if (unblocked) {
178     events &= ~aType;
179   }
180 
181   if (events == oldEvents
182 #ifdef ANDROID
183       // GeckoView always needs to notify about blocked trackers,
184       // since the GeckoView API always needs to report the URI and
185       // type of any blocked tracker. We use a platform-dependent code
186       // path here because reporting this notification on desktop
187       // platforms isn't necessary and doing so can have a big
188       // performance cost.
189       && aType != nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT
190 #endif
191   ) {
192     // Avoid dispatching repeated notifications when nothing has
193     // changed
194     return Nothing();
195   }
196 
197   return Some(events);
198 }
199 
ReportLog(nsIPrincipal * aFirstPartyPrincipal)200 void ContentBlockingLog::ReportLog(nsIPrincipal* aFirstPartyPrincipal) {
201   MOZ_ASSERT(XRE_IsParentProcess());
202   MOZ_ASSERT(NS_IsMainThread());
203   MOZ_ASSERT(aFirstPartyPrincipal);
204 
205   if (!StaticPrefs::browser_contentblocking_database_enabled()) {
206     return;
207   }
208 
209   if (mLog.IsEmpty()) {
210     return;
211   }
212 
213   nsCOMPtr<nsITrackingDBService> trackingDBService =
214       do_GetService("@mozilla.org/tracking-db-service;1");
215   if (NS_WARN_IF(!trackingDBService)) {
216     return;
217   }
218 
219   trackingDBService->RecordContentBlockingLog(Stringify());
220 }
221 
ReportOrigins()222 void ContentBlockingLog::ReportOrigins() {
223   if (!IsReportingEnabled()) {
224     return;
225   }
226   LOG(("ContentBlockingLog::ReportOrigins [this=%p]", this));
227   const bool testMode =
228       StaticPrefs::telemetry_origin_telemetry_test_mode_enabled();
229   OriginMetricID metricId =
230       testMode ? OriginMetricID::ContentBlocking_Blocked_TestOnly
231                : OriginMetricID::ContentBlocking_Blocked;
232   ReportOriginSingleHash(metricId, kDummyOriginHash);
233 
234   nsTArray<HashNumber> lookupTable;
235   for (const auto& originEntry : mLog) {
236     if (!originEntry.mData) {
237       continue;
238     }
239 
240     for (const auto& logEntry : Reversed(originEntry.mData->mLogs)) {
241       if ((logEntry.mType !=
242                nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER &&
243            logEntry.mType !=
244                nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER) ||
245           logEntry.mTrackingFullHashes.IsEmpty()) {
246         continue;
247       }
248 
249       const bool isBlocked = logEntry.mBlocked;
250       Maybe<StorageAccessPermissionGrantedReason> reason = logEntry.mReason;
251 
252       metricId = testMode ? OriginMetricID::ContentBlocking_Blocked_TestOnly
253                           : OriginMetricID::ContentBlocking_Blocked;
254       if (!isBlocked) {
255         MOZ_ASSERT(reason.isSome());
256         switch (reason.value()) {
257           case StorageAccessPermissionGrantedReason::eStorageAccessAPI:
258             metricId =
259                 testMode
260                     ? OriginMetricID::
261                           ContentBlocking_StorageAccessAPIExempt_TestOnly
262                     : OriginMetricID::ContentBlocking_StorageAccessAPIExempt;
263             break;
264           case StorageAccessPermissionGrantedReason::
265               eOpenerAfterUserInteraction:
266             metricId =
267                 testMode
268                     ? OriginMetricID::
269                           ContentBlocking_OpenerAfterUserInteractionExempt_TestOnly
270                     : OriginMetricID::
271                           ContentBlocking_OpenerAfterUserInteractionExempt;
272             break;
273           case StorageAccessPermissionGrantedReason::eOpener:
274             metricId =
275                 testMode ? OriginMetricID::ContentBlocking_OpenerExempt_TestOnly
276                          : OriginMetricID::ContentBlocking_OpenerExempt;
277             break;
278           default:
279             MOZ_ASSERT_UNREACHABLE(
280                 "Unknown StorageAccessPermissionGrantedReason");
281         }
282       }
283 
284       for (const auto& hash : logEntry.mTrackingFullHashes) {
285         HashNumber key = AddToHash(HashString(hash.get(), hash.Length()),
286                                    static_cast<uint32_t>(metricId));
287         if (lookupTable.Contains(key)) {
288           continue;
289         }
290         lookupTable.AppendElement(key);
291         ReportOriginSingleHash(metricId, hash);
292       }
293       break;
294     }
295   }
296 }
297 
298 }  // namespace mozilla
299