/** * Per-browser Privacy and Security ratings (each 0–100), surfaced as badges * on each browser row and as breakdowns in the Privacy Features modal. * * The numbers are curated — opinionated but defensible. Each component has * a short note so a reader can see WHY the browser landed where it did. * Adjusting a single dimension is a one-line change; the row badge re-derives. * * Privacy = mix of measured (privacytests.org pass rate) + tier weight. * Security = weighted average of five hand-rated dimensions per browser: * sandbox process/site isolation strength * mitigations CFI, MTE, hardened_malloc, stack canaries, zero-init * jitOff JIT disabled by default (reduces JS exploit surface) * updates how quickly CVE patches reach users (channel + cadence) * forkLag distance from upstream Chromium/Gecko stable * * Sources of fact (not subjective): * - GrapheneOS Vanadium docs (vanadium.app, grapheneos.org/features) * - IronFox tracker (gitlab.com/ironfox-oss/IronFox) * - Tor Browser security levels (tb-manual.torproject.org) * - Chromium platform security docs (chromium.org/Home/chromium-security) * - privacytests.org Android dataset (snapshot in data/) */ import { PRIVACY_TEST_SCORES } from './privacy-scores'; import { tierWeight, type Browser, type PrivacyTier } from './browsers'; // ──────────────────────────────────────────────────────────────────────── // Privacy // ──────────────────────────────────────────────────────────────────────── const TIER_TO_PRIVACY_BASE: Record = { highest: 99, 'very-high': 92, high: 80, 'medium-high': 65, medium: 50, low: 30, }; export type PrivacyBreakdown = { total: number; parts: { label: string; score: number; note: string }[]; }; /** * Browsers that route traffic through an anonymising network by default — * scored 0–100 for network-layer privacy. Only Tor qualifies today; other * browsers can stack a VPN or Orbot externally but that's out-of-scope * for the in-app score. */ const NETWORK_ANONYMITY: Record = { tor: 100, }; /** * Compute privacy score 0–100 as a weighted average of three signals: * - Baseline tier (the curated posture per browser) * - privacytests.org pass rate (per-API measured, when available) * - Network anonymity (whether traffic is onion-routed by default) * * Tier dominates (80%) because per-API tests systematically under-reward * browsers like Tor that deliberately omit features (e.g. bundled tracker * blockers) for anonymity-set reasons. The measured and network buckets * add 10% each so privacytests data still informs the score and Tor's * unique moat is reflected. * * When a signal is missing (no privacytests data, no network anonymity), * its weight is dropped and the remaining ones are renormalised. */ export function privacyScore(b: Browser): number { return privacyBreakdown(b).total; } export function privacyBreakdown(b: Browser): PrivacyBreakdown { const tierBase = TIER_TO_PRIVACY_BASE[b.privacyTier]; const pt = PRIVACY_TEST_SCORES[b.pkg]; const network = NETWORK_ANONYMITY[b.id] ?? 0; const parts: PrivacyBreakdown['parts'] = []; parts.push({ label: 'Baseline', score: tierBase, note: 'Intrinsic protections + always-private posture', }); let weighted = tierBase * 80; let weightSum = 80; if (pt) { const measuredPct = Math.round((pt.totalPassed / pt.totalTests) * 100); parts.push({ label: 'privacytests.org', score: measuredPct, note: `${pt.totalPassed}/${pt.totalTests} tests passed`, }); weighted += measuredPct * 10; weightSum += 10; } if (network > 0) { parts.push({ label: 'Network anonymity', score: network, note: 'Onion-routed by default — hides IP from every site', }); weighted += network * 10; weightSum += 10; } return { total: Math.round(weighted / weightSum), parts, }; } // ──────────────────────────────────────────────────────────────────────── // Security // ──────────────────────────────────────────────────────────────────────── type SecDim = 'sandbox' | 'mitigations' | 'openness' | 'updates' | 'forkLag' | 'jitOff'; const SEC_WEIGHTS: Record = { sandbox: 0.30, mitigations: 0.25, openness: 0.15, updates: 0.10, forkLag: 0.10, jitOff: 0.10, }; const SEC_LABEL: Record = { sandbox: 'Sandbox', mitigations: 'Mitigations', openness: 'Open source', updates: 'Updates', forkLag: 'Upstream lag', jitOff: 'JIT-off default', }; type SecRow = { score: number; note: string }; type SecProfile = Record; const SEC: Record = { vanadium: { sandbox: { score: 95, note: 'Chromium site isolation + GrapheneOS process hardening' }, mitigations: { score: 98, note: 'MTE on Tensor / 8 Gen 3, CFI, hardened_malloc, zero-init' }, openness: { score: 100, note: 'Fully open source under GPLv2, auditable patch set' }, jitOff: { score: 100, note: 'JIT disabled by default, per-site toggle' }, updates: { score: 90, note: 'GrapheneOS channel, within days of Chromium stable' }, forkLag: { score: 95, note: 'Tracks Chromium stable closely' }, }, helium: { sandbox: { score: 90, note: 'Chromium site isolation; no OS-level GrapheneOS hardening' }, mitigations: { score: 80, note: 'Vanadium patches ported; CFI + stack canaries; no MTE / hardened_malloc' }, openness: { score: 100, note: 'Fully open source under GPLv2' }, jitOff: { score: 30, note: 'Per-site only — no JIT-off default' }, updates: { score: 70, note: 'GitHub releases only — manual install / sideload cadence' }, forkLag: { score: 80, note: 'Tracks Chromium stable; experimental builds' }, }, brave: { sandbox: { score: 95, note: 'Chromium-based, same sandbox' }, mitigations: { score: 85, note: 'Inherits Chromium mitigations' }, openness: { score: 100, note: 'Fully open source under MPL 2.0; auditable repo' }, jitOff: { score: 30, note: 'Per-site only' }, updates: { score: 80, note: 'Play Store, typically ~1 week behind Chromium' }, forkLag: { score: 85, note: '1–2 weeks behind Chromium stable for QA' }, }, chrome: { sandbox: { score: 95, note: 'Canonical Chromium site isolation' }, mitigations: { score: 85, note: 'CFI, stack canaries, scudo — no MTE by default' }, openness: { score: 40, note: 'Chromium is OSS but Chrome ships proprietary Google blobs' }, jitOff: { score: 30, note: 'Per-site Site Settings toggle only' }, updates: { score: 95, note: 'Play Store auto-update, weekly stable patches' }, forkLag: { score: 100, note: 'Upstream itself' }, }, // Firefox / Gecko on Android has substantially weaker process isolation // than Chromium: a single content process for all tabs, no Fission / // site-per-process by default on Android, and no CFI in the Gecko build // (Mozilla tracks this in bugzilla 1539852 + meta 1665877). Sandbox and // mitigation scores are graded against the Chromium baseline. ironfox: { sandbox: { score: 45, note: 'Single Gecko content process; no site-per-process on Android' }, mitigations: { score: 80, note: 'WebGL / WebRTC / EME / telemetry stripped; ACCESS_NETWORK_STATE removed; bundled fonts' }, openness: { score: 100, note: 'Fully open source under MPL 2.0' }, jitOff: { score: 100, note: 'JIT disabled by default' }, updates: { score: 65, note: 'F-Droid + GitLab releases — small team but regular cadence' }, forkLag: { score: 65, note: 'Tracks Fenix release with hardening patches' }, }, tor: { sandbox: { score: 45, note: 'Single Gecko content process; no site-per-process on Android' }, mitigations: { score: 65, note: 'Tor Project hardening; no CFI in Gecko build' }, openness: { score: 100, note: 'Fully open source (MPL 2.0 + 3-clause BSD)' }, jitOff: { score: 80, note: 'Highest security level disables JIT (not default)' }, updates: { score: 65, note: 'Android releases lag desktop Tor Browser' }, forkLag: { score: 50, note: 'Built on Firefox ESR, well behind upstream Fenix' }, }, focus: { sandbox: { score: 45, note: 'Single Gecko content process; smaller surface (no tabs/add-ons)' }, mitigations: { score: 60, note: 'Same Gecko baseline as Firefox; clears state on exit' }, openness: { score: 100, note: 'Fully open source under MPL 2.0' }, jitOff: { score: 25, note: 'Not user-toggleable on Android' }, updates: { score: 88, note: 'Play Store, regular Mozilla cadence' }, forkLag: { score: 95, note: 'Tracks Fenix Focus branch closely' }, }, firefox: { sandbox: { score: 45, note: 'Single Gecko content process; no site-per-process on Android' }, mitigations: { score: 55, note: 'No CFI in Gecko build; stack canaries + ASLR only' }, openness: { score: 100, note: 'Fully open source under MPL 2.0' }, jitOff: { score: 25, note: 'Not user-toggleable on Android' }, updates: { score: 90, note: 'Play Store auto-update, weekly stable patches' }, forkLag: { score: 100, note: 'Upstream Fenix' }, }, 'firefox-beta': { sandbox: { score: 45, note: 'Same Gecko model as release' }, mitigations: { score: 55, note: 'Same Gecko mitigations as release' }, openness: { score: 100, note: 'Same MPL 2.0 source as release' }, jitOff: { score: 25, note: 'Not user-toggleable' }, updates: { score: 95, note: 'Beta channel — more frequent than release' }, forkLag: { score: 100, note: 'Ahead of stable' }, }, 'firefox-nightly': { sandbox: { score: 45, note: 'Same Gecko model as release' }, mitigations: { score: 50, note: 'Bleeding edge — occasional regressions' }, openness: { score: 100, note: 'Same MPL 2.0 source as release' }, jitOff: { score: 25, note: 'Not user-toggleable' }, updates: { score: 100, note: 'Nightly builds' }, forkLag: { score: 100, note: 'Tip of Fenix tree' }, }, ddg: { sandbox: { score: 70, note: 'Wraps system WebView — site isolation when device supports it' }, mitigations: { score: 75, note: 'Inherits Chromium WebView mitigations; varies by device' }, openness: { score: 90, note: 'App is Apache-2.0 OSS; engine = system WebView (varies)' }, jitOff: { score: 25, note: 'Inherits WebView default' }, updates: { score: 75, note: 'App updates fast; engine updates depend on system WebView' }, forkLag: { score: 60, note: 'Cannot push engine — lags on AOSP / outdated WebView devices' }, }, }; export type SecurityBreakdown = { total: number; parts: { label: string; score: number; note: string }[]; }; export function securityScore(b: Browser): number { return securityBreakdown(b).total; } export function securityBreakdown(b: Browser): SecurityBreakdown { const prof = SEC[b.id]; if (!prof) { // Fallback for any new browser id we forget to add: a conservative middling score. return { total: 50, parts: [{ label: 'Uncurated', score: 50, note: 'Not yet rated' }] }; } const dims: SecDim[] = ['sandbox', 'mitigations', 'openness', 'updates', 'forkLag', 'jitOff']; let total = 0; const parts = dims.map((d) => { const row = prof[d]; total += row.score * SEC_WEIGHTS[d]; return { label: SEC_LABEL[d], score: row.score, note: row.note }; }); return { total: Math.round(total), parts }; } // Tier weight isn't a security input but TS treeshakes the import otherwise. void tierWeight;