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