browsers.ts raw
1 import { Palette } from './theme';
2 import { PRIVACY_TEST_SCORES, type PrivacyTestScore } from './privacy-scores';
3
4 /**
5 * Tier system: a coarse, curated stack rank of inherent privacy strength.
6 * Independent of intent-deliverability (covered separately by
7 * nativePrivateWorks / cctPrivateWorks). Used for default ordering on the
8 * Privacy tab and for the small badge under the browser name.
9 *
10 * - 'highest' (reserved — currently unused)
11 * - 'very-high' Default-on hardening + RFP-grade fingerprint defenses
12 * AND tracker/content blocking out of the box (IronFox)
13 * - 'high' Strong in one or more axes but missing tracker blocking
14 * by default — Brave (shields on, fingerprint farbling),
15 * Vanadium (hardened Chromium, no tracker blocker by default),
16 * Tor Browser (network anonymity, but recommends against
17 * content blockers), Focus (always-private)
18 * - 'medium-high' Always-private OR good in some dimensions only (DDG)
19 * - 'medium' Mainstream browser; private mode works
20 * - 'low' Mainstream browser; we can't request a private session
21 */
22 export type PrivacyTier = 'highest' | 'very-high' | 'high' | 'medium-high' | 'medium' | 'low';
23
24 const TIER_ORDER: Record<PrivacyTier, number> = {
25 highest: 0,
26 'very-high': 1,
27 high: 2,
28 'medium-high': 3,
29 medium: 4,
30 low: 5,
31 };
32
33 export const TIER_LABEL: Record<PrivacyTier, string> = {
34 highest: 'Highest',
35 'very-high': 'Very high',
36 high: 'High',
37 'medium-high': 'Mid-high',
38 medium: 'Medium',
39 low: 'Low',
40 };
41
42 export function tierWeight(t: PrivacyTier): number {
43 return TIER_ORDER[t];
44 }
45
46 /**
47 * Strategy that ATTEMPTS private launch. Whether the chosen browser actually
48 * honors it is encoded separately via nativePrivateWorks / cctPrivateWorks
49 * below.
50 * - 'chromium-class' : sets component = IncognitoTabLauncher (Chrome's class)
51 * - 'chromium-extra' : standard VIEW + EXTRA_OPEN_NEW_INCOGNITO_TAB
52 * - 'firefox' : standard VIEW + private_browsing_mode / legacy hints
53 * - null : no private path at all
54 */
55 export type PrivateMode = 'chromium-class' | 'chromium-extra' | 'firefox' | null;
56
57 export type Browser = {
58 id: string;
59 name: string;
60 pkg: string;
61 tint: string;
62 tagline?: string;
63 privateMode: PrivateMode;
64 nativePrivateWorks: boolean;
65 cctPrivateWorks: boolean;
66 alwaysPrivate?: boolean;
67 privacyTier: PrivacyTier;
68 /**
69 * Browser's own internal-flags / config URL — `brave://flags`,
70 * `chrome://flags`, `about:config`, etc. Used by the "Browser config"
71 * preset, where each Flow launches its browser's own settings page
72 * rather than a single shared URL. Undefined → that browser doesn't
73 * expose a config URL via external intent and the Flow is disabled
74 * in this mode.
75 */
76 configUrl?: string;
77 };
78
79 /**
80 * Privacy continuum: normal -> mini -> private -> max (most strict).
81 */
82 export type PrivacyMode = 'normal' | 'mini' | 'private' | 'max';
83
84 export const PRIVACY_MODES: readonly PrivacyMode[] = ['normal', 'mini', 'private', 'max'];
85
86 export function supportedModes(b: Browser): PrivacyMode[] {
87 if (b.alwaysPrivate) return ['normal', 'mini', 'private', 'max'];
88 const m: PrivacyMode[] = ['normal', 'mini'];
89 if (b.nativePrivateWorks) m.push('private');
90 if (b.cctPrivateWorks) m.push('max');
91 return m;
92 }
93
94 /** True if the browser can deliver any kind of private session. */
95 export function canDoPrivate(b: Browser): boolean {
96 return !!b.alwaysPrivate || b.nativePrivateWorks || b.cctPrivateWorks;
97 }
98
99 /** Strongest privacy mode the browser can actually deliver. */
100 export function maxPrivateMode(b: Browser): PrivacyMode | null {
101 if (b.alwaysPrivate) return 'normal'; // already private regardless
102 if (b.cctPrivateWorks) return 'max';
103 if (b.nativePrivateWorks) return 'private';
104 return null;
105 }
106
107 /**
108 * Plain-English explanation of HOW a browser achieves each privacy mode.
109 * Derived entirely from the same flags that drive the launch code in
110 * modules/default-browser, so the docs can't drift from the actual
111 * behavior.
112 */
113 export type ModeExplanation = {
114 mode: PrivacyMode;
115 supported: boolean;
116 description: string;
117 };
118 export type BrowserPrivacyExplanation = {
119 summary: string;
120 protections: string[];
121 testScore?: PrivacyTestScore;
122 modes: ModeExplanation[];
123 };
124
125 export function explainPrivacy(b: Browser): BrowserPrivacyExplanation {
126 const protections = INTRINSIC_PROTECTIONS[b.id] ?? [];
127 const testScore = PRIVACY_TEST_SCORES[b.pkg];
128 // Prefer the curated one-line description when we have one — it's
129 // calibrated for the browser's actual character. Falls back to the
130 // dynamic mode-based summary otherwise.
131 const curated = BROWSER_DESCRIPTIONS[b.id];
132
133 if (b.alwaysPrivate) {
134 return {
135 summary:
136 curated ??
137 `${b.name} is private by design. Every tab is an isolated session ` +
138 `regardless of what intent we send.`,
139 protections,
140 testScore,
141 modes: PRIVACY_MODES.map((m) => ({
142 mode: m,
143 supported: true,
144 description:
145 m === 'normal' || m === 'mini'
146 ? 'Standard launch. Private by the browser itself.'
147 : `${b.name} ignores our hints but is already operating in ` +
148 `private mode.`,
149 })),
150 };
151 }
152
153 const intentFor = (m: PrivacyMode): string => {
154 switch (m) {
155 case 'normal':
156 return 'ACTION_VIEW with the URL — the regular launch.';
157 case 'mini':
158 return (
159 'ACTION_VIEW with a CCT_SESSION marker bundle — renders as a ' +
160 'Custom Tab. Same browser session, minimal toolbar.'
161 );
162 case 'private': {
163 switch (b.privateMode) {
164 case 'firefox':
165 return (
166 'ACTION_VIEW + the "private_browsing_mode = true" extra. ' +
167 'Fenix-based builds (Firefox 112+) honor it for any caller ' +
168 'and open a real private window.'
169 );
170 case 'chromium-extra':
171 return (
172 'ACTION_VIEW + EXTRA_OPEN_NEW_INCOGNITO_TAB on the plain ' +
173 'launcher.' +
174 (b.nativePrivateWorks
175 ? ''
176 : ' This build does not honor the extra on the plain ' +
177 'launcher — current Chromium gates it the same way ' +
178 'Chrome does.')
179 );
180 case 'chromium-class':
181 return (
182 'Targets the IncognitoTabLauncher activity component. ' +
183 'Chrome 79+ blocks non-Google-signed callers via ' +
184 'ExternalAuthUtils.isGoogleSigned() — so for us this ' +
185 'silently opens a regular tab.'
186 );
187 default:
188 return 'No third-party private path is exposed by this browser.';
189 }
190 }
191 case 'max':
192 return b.cctPrivateWorks
193 ? (b.privateMode === 'chromium-class'
194 ? 'Custom Tab + ENABLE_EPHEMERAL_BROWSING extra. Chrome 137+ ' +
195 'honors this as a true ephemeral session — no history, ' +
196 'cookies, or cache persisted.'
197 : 'Custom Tab + EXTRA_OPEN_NEW_INCOGNITO_TAB. Brave-style ' +
198 'forks render this as a private custom tab.')
199 : 'This browser ignores both the ephemeral and incognito hints ' +
200 'on CCT requests; would degrade to a regular Custom Tab.';
201 }
202 };
203
204 const isSupported = (m: PrivacyMode): boolean => {
205 if (m === 'normal' || m === 'mini') return true;
206 if (m === 'private') return b.nativePrivateWorks;
207 if (m === 'max') return b.cctPrivateWorks;
208 return false;
209 };
210
211 const top = maxPrivateMode(b);
212 const summary =
213 curated ??
214 (top === 'max'
215 ? `${b.name} delivers strongest privacy via ephemeral / private ` +
216 `Custom Tab.`
217 : top === 'private'
218 ? `${b.name} delivers strongest privacy via a native private window.`
219 : `${b.name} has no third-party private path; only regular browsing ` +
220 `works from external intents.`);
221
222 return {
223 summary,
224 protections,
225 testScore,
226 modes: PRIVACY_MODES.map((m) => ({
227 mode: m,
228 supported: isSupported(m),
229 description: intentFor(m),
230 })),
231 };
232 }
233
234 /**
235 * Curated default order, privacy-first then by general preference. Users
236 * can reorder in-app; browsers not in the saved order are appended in this
237 * default order.
238 */
239 export const BROWSERS: Browser[] = [
240 // ─── Tier: very-high (hardened + tracker blocking by default) ───────
241 { id: 'ironfox', name: 'IronFox', pkg: 'org.ironfoxoss.ironfox', tint: Palette.accentBright, tagline: 'Hardened Firefox',
242 privateMode: 'firefox', nativePrivateWorks: true, cctPrivateWorks: false, privacyTier: 'very-high',
243 configUrl: 'about:config' },
244
245 // ─── Tier: very-high (default tracker blocking + fingerprint defenses) ─
246 // Brave Shields on by default — tracker / ad / script blocking and
247 // per-session canvas/audio/font farbling. Web-privacy posture sits
248 // alongside IronFox, ahead of Vanadium/Helium which have lighter
249 // defaults on the tracker side.
250 { id: 'brave', name: 'Brave', pkg: 'com.brave.browser', tint: '#fb542b', tagline: 'Privacy shield',
251 privateMode: 'chromium-extra', nativePrivateWorks: false, cctPrivateWorks: true, privacyTier: 'very-high',
252 configUrl: 'brave://flags' },
253
254 // ─── Tier: medium-high (security-first, lighter web-tracking defaults) ─
255 // Vanadium ships EasyList+EasyPrivacy and a hardened posture but no
256 // fingerprint randomization; its strength is OS-level security on
257 // GrapheneOS, not web-tracking defaults. Accepts EXTRA_OPEN_NEW_INCOGNITO_TAB
258 // on plain VIEW intents and honors the ephemeral-CCT extra. User setting
259 // "always open external links in incognito" forces every intent into a
260 // private session — fall-through hardening for users who flip it on.
261 { id: 'vanadium', name: 'Vanadium', pkg: 'app.vanadium.browser', tint: '#5e9bd6', tagline: 'GrapheneOS hardened Chromium',
262 privateMode: 'chromium-extra', nativePrivateWorks: true, cctPrivateWorks: true, privacyTier: 'high',
263 configUrl: 'chrome://flags' },
264 // Helium — Chromium fork that ports Vanadium hardening patches on top of
265 // the off-device Helium codebase. WebRTC IP shielding on by default,
266 // Manifest V2 so uBlock Origin can be *installed* — but no tracker
267 // blocker out of the box. No MTE / hardened_malloc / JIT-off defaults
268 // either (those live in GrapheneOS). Same private-launch shape as the
269 // other chromium-extra browsers.
270 { id: 'helium', name: 'Helium', pkg: 'io.github.jqssun.helium', tint: '#7aa2f7', tagline: 'Vanadium-patched Chromium',
271 privateMode: 'chromium-extra', nativePrivateWorks: true, cctPrivateWorks: true, privacyTier: 'high',
272 configUrl: 'chrome://flags' },
273 // Tor Browser provides network-level anonymity but the Tor Project advises
274 // against installing extensions / content blockers — so by default it has
275 // no tracker blocker the way IronFox does.
276 { id: 'tor', name: 'Tor Browser', pkg: 'org.torproject.torbrowser', tint: '#7d4698', tagline: 'Onion routing',
277 privateMode: null, nativePrivateWorks: false, cctPrivateWorks: false, alwaysPrivate: true, privacyTier: 'highest',
278 configUrl: 'about:config' },
279 { id: 'focus', name: 'Firefox Focus', pkg: 'org.mozilla.focus', tint: '#a4007c', tagline: 'Always-private Firefox',
280 privateMode: null, nativePrivateWorks: false, cctPrivateWorks: false, alwaysPrivate: true, privacyTier: 'high' },
281
282 // ─── Tier: medium-high (always-private but uneven scores) ───────────
283 { id: 'ddg', name: 'DuckDuckGo', pkg: 'com.duckduckgo.mobile.android', tint: '#de5833', tagline: 'Private search',
284 privateMode: null, nativePrivateWorks: false, cctPrivateWorks: false, alwaysPrivate: true, privacyTier: 'high' },
285
286 // ─── Tier: medium (private mode works, default state weak) ──────────
287 { id: 'firefox', name: 'Firefox', pkg: 'org.mozilla.firefox', tint: '#ff7139', tagline: 'Mozilla',
288 privateMode: 'firefox', nativePrivateWorks: true, cctPrivateWorks: false, privacyTier: 'medium-high',
289 configUrl: 'about:config' },
290 // Felice — Fennec F-Droid fork with Mozilla extension signing disabled.
291 // Same Fenix runtime as Firefox stable, identical private-launch shape.
292 { id: 'felice', name: 'Felice', pkg: 'org.mozilla.felice', tint: '#c4a7e7', tagline: 'Fennec fork, no addon signing',
293 privateMode: 'firefox', nativePrivateWorks: true, cctPrivateWorks: false, privacyTier: 'medium-high',
294 configUrl: 'about:config' },
295 // Beta/Nightly intentionally tier below stable: same engine but extra
296 // pre-release telemetry channels. Stable's privacy score is pulled down
297 // by privacytests data; Beta/Nightly have no measured data, so we
298 // anchor them with a slightly lower tier so the family hierarchy
299 // (stable > Beta > Nightly) stays right after the weighted average.
300 { id: 'firefox-beta', name: 'Firefox Beta', pkg: 'org.mozilla.firefox_beta', tint: '#ff7139', tagline: 'Mozilla Beta',
301 privateMode: 'firefox', nativePrivateWorks: true, cctPrivateWorks: false, privacyTier: 'medium',
302 configUrl: 'about:config' },
303 { id: 'firefox-nightly', name: 'Firefox Nightly', pkg: 'org.mozilla.fenix', tint: '#0250bb', tagline: 'Mozilla Nightly',
304 privateMode: 'firefox', nativePrivateWorks: true, cctPrivateWorks: false, privacyTier: 'medium',
305 configUrl: 'about:config' },
306
307 // ─── Tier: medium (Chrome — ephemeral CCT salvages a single tab) ────
308 { id: 'chrome', name: 'Chrome', pkg: 'com.android.chrome', tint: '#4285F4', tagline: 'Google',
309 privateMode: 'chromium-class', nativePrivateWorks: false, cctPrivateWorks: true, privacyTier: 'low',
310 configUrl: 'chrome://flags' },
311 ];
312
313 /**
314 * Intrinsic browser-level defenses, curated. Surfaced in the privacy
315 * explanation modal alongside the per-mode intent details. Reflects what
316 * the browser does on its own, not what we request from it.
317 */
318 /**
319 * Plain-English one-line summary per browser. Surfaced at the top of the
320 * Privacy Features modal so the user gets a calibrated take before
321 * reading the bullet list of intrinsic protections.
322 */
323 export const BROWSER_DESCRIPTIONS: Partial<Record<string /* browser id */, string>> = {
324 vanadium:
325 "GrapheneOS-hardened Chromium: MTE, CFI, hardened_malloc, JIT off by default. No built-in tracker blocker — relies on system-level controls.",
326 brave:
327 "Sturdy fully open-source Chromium fork. Brave Shields (tracker / ad / script blocking) and per-session canvas/audio/font randomisation on by default.",
328 chrome:
329 "Canonical Chromium build with Google services. Fastest update cadence and best site isolation, but the weakest default-privacy posture of the group.",
330 ironfox:
331 "Hardened Firefox fork. uBlock Origin pre-installed, RFP-grade fingerprint defenses, JIT off, telemetry / WebGL / WebRTC stripped. Pair with Orbot (Tor) or a VPN to add network-layer anonymity — approaches Tor-level privacy without the relay-latency cost.",
332 helium:
333 "Experimental Chromium fork combining Helium and GrapheneOS Vanadium patches. WebRTC IP shielded by default, Manifest V2 so uBlock Origin installs from the Web Store. No OS-level hardening — devs themselves recommend GrapheneOS + Vanadium for serious threat models.",
334 tor:
335 "Onion-routed Firefox-based browser. Three-hop relay network gives network-layer anonymity; same fingerprint for every user. No tracker blocker by design — content filters would distinguish you.",
336 focus:
337 "Single-tab always-private Firefox. Every session starts fresh; one-tap erase. No add-ons, no syncing, no history. Smaller attack surface than full Firefox.",
338 ddg:
339 "Wraps the system Chromium WebView. Always-private session, DuckDuckGo Tracker Radar blocks third-party trackers, one-tap fire button burns state.",
340 firefox:
341 "Mozilla Firefox stable. Open source under MPL 2.0, Enhanced Tracking Protection (Standard) and Total Cookie Protection on by default.",
342 felice:
343 "Fennec F-Droid fork. Mozilla's mandatory extension signing disabled — install any .xpi. Same Fenix runtime as Firefox, identical privacy posture.",
344 'firefox-beta':
345 "Firefox pre-release channel. Same Gecko engine as stable, features land ~2 weeks earlier. Updates more often.",
346 'firefox-nightly':
347 "Firefox bleeding-edge channel. Daily builds from upstream Fenix; instability expected.",
348 };
349
350 export const INTRINSIC_PROTECTIONS: Partial<Record<string /* browser id */, string[]>> = {
351 tor: [
352 'Onion routing through three-hop encrypted relay (Tor network)',
353 'Resist Fingerprinting (RFP) — all users return the same fingerprint',
354 'Letterboxing — resists window-size fingerprinting',
355 'Security levels: Standard / Safer / Safest. Safest disables JS globally.',
356 'HTTPS-Only Mode (Firefox feature) — HTTPS Everywhere was retired 2022',
357 'First-party isolation (Total Cookie Protection) built-in',
358 'Supports browser extensions — the Tor Project advises against installing them (de-anonymisation risk), but the capability exists for NoScript / password managers',
359 ],
360 ironfox: [
361 'Strict Enhanced Tracking Protection (ETP Strict) by default',
362 'uBlock Origin pre-installed and pre-configured',
363 'RFP-grade fingerprinting protection (canvas / audio / font / locale spoofed)',
364 'Bundled fonts at build-time — consistent font fingerprint',
365 'HTTPS-Only Mode by default',
366 'DNS over HTTPS (Quad9) Max Protection — no fallback',
367 'Clears history / cache / open tabs on exit',
368 'JIT disabled by default (toggle available)',
369 'Network connectivity monitoring + ACCESS_NETWORK_STATE removed',
370 'Global Privacy Control on by default',
371 'Stack with Orbot (Tor) or a VPN for network-layer anonymity — closes the IP-leak gap that Tor Browser covers natively',
372 'Supports browser extensions (uBlock Origin bundled; add password managers, NoScript, etc.)',
373 ],
374 vanadium: [
375 'Hardware Memory Tagging (MTE), CFI, strong stack protector, zero-init',
376 'Strict site isolation by default',
377 'JavaScript JIT disabled by default (per-site toggle)',
378 'WebGPU disabled — attack-surface + fingerprint reduction',
379 'WebRTC IP handling set to most-private value by default',
380 'High-performance content filtering (EasyList + EasyPrivacy) built in',
381 'Third-party cookies disabled by default',
382 'DoNotTrack enabled by default',
383 'Battery API spoofed (always charging, 100%)',
384 'Reduced User-Agent + client hints — placeholder values for OS / device / browser version',
385 'Hybrid post-quantum TLS by default',
386 'Connects only to GrapheneOS servers by default (no Google services)',
387 'Honors EXTRA_OPEN_NEW_INCOGNITO_TAB for per-link private launches',
388 'User setting "always open external links in incognito" — when on, ' +
389 'every external intent we send opens in a private session regardless',
390 ],
391 helium: [
392 'WebRTC IP handling shielded by default — local IPs not leaked to STUN',
393 'Vanadium security patches ported on top of upstream Chromium',
394 'Manifest V2 extension support — uBlock Origin installable from Chrome Web Store',
395 'Chromium site isolation inherited from upstream',
396 'No telemetry, no Google services integration',
397 'Open source under GPLv2; GitHub-release distribution',
398 ],
399 brave: [
400 'Brave Shields: tracker + ad + script blocking by default',
401 'Farbling: per-session canvas / audio / font randomization',
402 'Aggressive shield mode available per-site',
403 'HTTPS upgrades + cookie blocking by default',
404 'Strips known tracking parameters from URLs',
405 ],
406 focus: [
407 'Always-private: every session starts fresh',
408 'Tracking Protection on by default',
409 'Erase button clears all state in one tap',
410 'Single-tab design — no cross-tab state to leak',
411 'Supports browser extensions — Fenix runtime, so uBlock Origin / password managers installable',
412 ],
413 ddg: [
414 'DuckDuckGo Tracker Radar blocks 100+ trackers',
415 'Fire button burns all open tabs and stored data',
416 'Email Protection for hiding addresses',
417 'Smarter Encryption upgrades HTTP to HTTPS',
418 ],
419 firefox: [
420 'Enhanced Tracking Protection (Standard) on by default',
421 'Total Cookie Protection (cookie jar per site)',
422 'Private window opens a fresh session',
423 'Supports browser extensions (uBlock Origin, password managers, etc.)',
424 ],
425 felice: [
426 'Same as Firefox — Enhanced Tracking Protection + Total Cookie Protection',
427 'Mozilla extension signing disabled — install any .xpi without approval',
428 'Private window opens a fresh session',
429 'Supports browser extensions without restriction',
430 ],
431 'firefox-beta': [
432 'Same as Firefox + pre-release features',
433 'Supports browser extensions (uBlock Origin, password managers, etc.)',
434 ],
435 'firefox-nightly': [
436 'Same as Firefox + nightly experiments',
437 'Supports browser extensions (uBlock Origin, password managers, etc.)',
438 ],
439 chrome: [
440 'Safe Browsing on by default',
441 'Incognito mode (only reachable via Chrome\'s own UI for our caller)',
442 'Ephemeral Custom Tab on Chrome 137+ (truly stateless when requested)',
443 ],
444 };
445
446 /** Privacy-tier-aware default sort. Lower tier weight = more private. */
447 export function sortByTier(browsers: Browser[]): Browser[] {
448 return [...browsers].sort((a, b) => {
449 const t = tierWeight(a.privacyTier) - tierWeight(b.privacyTier);
450 if (t !== 0) return t;
451 // stable within tier — preserve BROWSERS order
452 return BROWSERS.indexOf(a) - BROWSERS.indexOf(b);
453 });
454 }
455
456 /** Lookup privacytests.org score for a package (may be undefined). */
457 export function getPrivacyTestScore(pkg: string): PrivacyTestScore | undefined {
458 return PRIVACY_TEST_SCORES[pkg];
459 }
460