Port Chromium browsers to Firefox. FoxPort scans your installed Chromium-family browsers (Chrome, Brave, Edge, Vivaldi, Opera, Arc, Thorium, Yandex, ...), decrypts your saved passwords, packages up your bookmarks, and maps your Chrome extensions to their Firefox equivalents on addons.mozilla.org — all in one click.
The source browser is never modified. FoxPort writes Firefox-native import files into an output folder; you import them through the target browser's normal UI.
The wizard walks through Source → Target → Items → Preview → Run/Done with a left-rail step indicator and Catppuccin Mocha dark theme:
Research-driven correctness pass — fixes for three P0 bugs identified in the archived research plan plus a basket of P1/P2 trust + polish wins.
- Correct
places.sqlite— schema bumped to Firefox tip (v86), newblock_until_ms/block_pages_until_mscolumns onmoz_origins,url_hashnow uses a Mozillamfbt::HashStringport (newcrypto/mozhash.py) so AwesomeBar dedup actually works. - Working open-tabs SNSS extractor — proper Pickle-aware parser that
reads both
Sessions/Session_*ANDSessions/Tabs_*files. Verified live: previous extractor returned 0 URLs; new one returns 12 from the same Chrome profile. - 48-test pytest suite —
tests/tree with synthetic fixtures for Chromium Bookmarks JSON, History SQLite, SNSS Tabs files. CI runspytestcross-platform. - HIBP password scan (opt-in, free k-anonymity API). Produces
compromised-passwords.txtwith URL+username (no plaintext). - Drag-and-drop "Manual source" tile now works — promotes the
dropped folder into a synthetic
ChromiumProfileand the wizard proceeds with it. - Quick wins —
cookies.sqliteupdateTimecolumn;favicons.sqlitebacked up instead of deleted;chrome:///about:/edge://URLs filtered from bookmarks + history + open-tabs by default;diffCLI refuses ambiguous profile matches; master-password retry loop (3 attempts); password preview masks values by default with a Show-all toggle; already-installed extensions fold into a collapsed<details>block inextensions.html; reverse-extensionsAMO_GUID_TO_CHROMEcleaned up;check_curated_map.py --strict-stale. - History time-range filter — Last 7 / 30 / 90 days, last 12 months, custom range. Surfaced via "Customize…" on the History row.
- Settings page (File → Settings…) persisted to per-platform JSON
(
%APPDATA%/FoxPort/config.json,~/Library/Application Support/...,$XDG_CONFIG_HOME/FoxPort/...). - Path-traversal hardening on
make_export_dir— slug-safe characters, resolved-path bounds check. formhistory.sqlitev5 schema withmoz_sources+moz_history_to_sourcestables.NSSSession.decrypt()method centralizes the previously-inlinePK11SDR_Decryptctypes call.- Reverse-map harvester (
scripts/harvest_reverse_map.py) auto- populatesAMO_GUID_TO_CHROMEfrom the forward curated map via the AMO detail API.
- GUI direction toggle — A segmented Chromium → Firefox / Firefox → Chromium selector on the Source step; the Source and Target pages swap their tile lists when you flip it. Items step disables the categories not yet supported in reverse mode.
- Master-password prompt — Reverse mode auto-prompts via a Qt password dialog when the Firefox source has one set, then retries NSS with the entered string. Cancel aborts the migration cleanly.
- Direct-write cookies + history — Sibling checkboxes to the existing
direct-write-passwords one on the Items step. Each backs up the
pre-existing file in the target profile to a timestamped sibling,
refuses on locked profile, and for history additionally deletes
favicons.sqliteso Firefox rebuilds it from the imported visits. - Open tabs migration — Reads the most recent Chromium
Sessions/Session_<n>SNSS binary, scans for UTF-16LE-encoded URLs (RFC 3986 char class to avoid bleed between fields), and emits a Firefox-compatiblerecovery.jsonlz4(mozLz40\0magic + lz4-block- compressed JSON). Optional direct-write tosessionstore-backups/recovery.jsonlz4. - Profile diff — New CLI
diffsubcommand reports passwords/bookmarks/extensions that exist in source but not in target, with samples. Useful before committing a migration:python -m foxport.cli diff --source "Brave/Default" --target "Firefox/default-release". - Curated-map auditor —
scripts/check_curated_map.pyhits AMO for every slug in the curated map, flags 404s / disabled / stale entries, exits non-zero on broken results. Designed for monthly CI runs.
- Reverse direction (Firefox → Chromium) is now supported through the
CLI as
python -m foxport.cli migrate-reverse --source "Firefox/default- release" --items passwords,bookmarks,extensions. - Firefox-side readers (
browsers/firefox_read.py) — NSS-decryptlogins.json, walkplaces.sqlitefor bookmarks, parseextensions.jsonfor installed AMO add-ons. - Reverse migrators in
foxport/migrate_reverse/:passwords.pywrites Chrome'sname,url,username,password,noteCSV.bookmarks.pywrites a Netscape HTML with the Firefox toolbar root promoted to Chrome'sPERSONAL_TOOLBAR_FOLDER.extensions.pyinverts the curated map and falls back to a Chrome Web Store text-search URL for unmapped AMO GUIDs.
- Form autofill migration — Chromium
Web Data.autofillbecomes a Firefox v4-schemaformhistory.sqlitewith properfirstUsed/lastUsedµs conversion and base64-encodedguids. - Saved cards CSV export — Decrypt credit card numbers from
Web Data.credit_cardsand write a 1Password-importable CSV. Firefox has no native card store, so the CSV is informational. - Search engines export — Read
Web Data.keywordsand emit a machine-readablesearch-engines.jsoninventory plus a per-engine OpenSearch XML descriptor (search-engines/<slug>.xml) the user can drag-install in Firefox. Chromium-specific URL tokens ({google:baseURL}etc.) stripped during emission. - All new categories work cross-platform (Windows/macOS/Linux) and honor dry-run mode.
- macOS support — Chromium-family profiles under
~/Library/Application Support/<vendor>, Firefox-family profiles under~/Library/Application Support/<vendor>/profiles.ini. Master keys recovered from the Keychain (security find-generic-password -s "<Browser> Safe Storage") + PBKDF2-SHA1 with 1003 iterations, AES-128-CBC decrypt forv10blobs. - Linux support — Chromium-family profiles under
~/.config/<vendor>(or$XDG_CONFIG_HOME), Firefox-family under~/.mozilla/firefox/,~/.librewolf/,~/.floorp/,~/.waterfox/,~/.zen/, etc. Master key recovered viasecret-tool→kwallet-query→"peanuts"plaintext fallback, PBKDF2-SHA1 with 1 iteration. NSS lookup covers/usr/lib/firefox/libnss3.so, distro-package locations, Flatpak, Snap. - Browser-running detection is now Linux/macOS-aware (
ps -axo comm=instead oftasklist). - All five migration paths (passwords, bookmarks, extensions, cookies, history) work identically across all three platforms.
- CLI mode —
python -m foxport.cli listenumerates detected profiles;python -m foxport.cli migrate --source "Brave/Default" --target "Firefox/default-release" --all --dry-runruns a full migration without the GUI. Substring matching on profile names. - Per-folder bookmark filter — A new "Customize…" button on the Items
step opens a tree dialog where you untick branches you don't want in
bookmarks.html. Selections persist across re-opens. - Password preview & per-row filter — Sibling "Customize…" button decrypts everything in a searchable table; tick rows in/out and the CSV export honors the selection. Per-row checkboxes default to "include all".
- NSS direct write — Opt-in checkbox. When the target Firefox profile is
selected and closed, FoxPort loads the target install's
nss3.dllvia ctypes, encrypts each login withPK11SDR_Encrypt, and writes directly intologins.json(+logins-backup.json). The previouslogins.jsonis backed up to a timestamped sibling. CSV is still emitted alongside for audit/safety. - Conflict-safe direct write — Deterministic GUIDs mean an existing matching entry is skipped, not duplicated.
- Cookies migration — Decrypt all cookies with the AES-GCM key (including
the SHA-256 HOST_KEY prefix Chrome 130+ silently added) and emit a fresh
Firefox v17-schema
cookies.sqlite. Swap into a closed Firefox profile. - History migration — Read Chromium
History(URLs + visits) and write a fresh Firefox v77-schemaplaces.sqlitewithmoz_origins,moz_places(frecency=-1,recalc_frecency=1, scheme-taggedurl_hash), andmoz_historyvisits. Bookmarks tree left empty (HTML import flow handles those). - Dry-run mode — A checkbox on the Items step runs detection + decryption tests and produces no on-disk artifacts. Useful for ABE/credentialed troubleshooting.
- App-Bound Encryption recovery —
foxport_abe.exesidecar intools/abe_sidecar/calls the per-browserIElevatorCOM interface (UAC-prompted) and returns the AES master key. Hooked intoload_master_keyso passwords/cookies on Chrome 127+ / Brave 1.86+ profiles decrypt automatically once the sidecar is built and dropped intofoxport/data/. - Two new action buttons on the Done screen — Reveal cookies.sqlite / places.sqlite in Explorer, since these aren't files you double-click.
- Five-step wizard UI — Source → Target → Items → Preview → Run/Done, with a left-rail step indicator, tile-based pickers, drag-and-drop support, and a sample-rich preview pane before commit.
- Smarter extension matching — A 63-entry curated map plus a manifest-
gecko.idprobe against the AMO detail endpoint plus permission-overlap confidence scoring on every match. - Already-installed detection — FoxPort reads the target Firefox profile's
extensions.jsonand strikes through extensions you already have so a click-through install doesn't re-tap them. - App-Bound Encryption awareness — Detects Chrome 127+ ABE on the source and reports clearly. Bookmarks and extensions still migrate; passwords on ABE-only profiles are flagged for v0.3.
- Opera Stable / GX flat-profile layout — Now detected correctly (no
Default/subdir). - Browser-running banner — Surfaces when a source browser is running so you know the data may be stale.
- Deterministic password GUIDs — Re-running the migration produces stable GUIDs so Firefox dedupes instead of inserting duplicates.
| Item | Source | Destination format | How to import |
|---|---|---|---|
| Passwords | Login Data SQLite + DPAPI key (+ ABE sidecar for Chrome 127+) |
Firefox CSV (url,username,password,...) |
about:logins → menu → Import from a File |
| Bookmarks | Bookmarks JSON |
Netscape HTML | Library (Ctrl+Shift+O) → Import Bookmarks from HTML |
| Extensions | Profile Extensions/<id>/<ver>/manifest.json |
HTML page of AMO install links | Open the HTML in Firefox, click each link |
| Extension settings | Allowlisted storage for uBO, Stylus, Bitwarden | extension-settings.json |
Opt in per extension; import through each add-on's own UI |
| Cookies | Network/Cookies SQLite + DPAPI key |
Firefox cookies.sqlite (v17) |
Close Firefox, back up existing, drop in |
| History | History SQLite (urls + visits) |
Firefox places.sqlite (v77; includes matching download annotations when paired with history direct-write) |
Close Firefox, back up existing, drop in |
| Form autofill | Web Data.autofill SQLite |
Firefox formhistory.sqlite |
Close Firefox, back up existing, drop in |
| Saved cards | Web Data.credit_cards + AES key |
CSV (1Password import shape) | Firefox has no native store — import into a password manager |
| Search engines | Web Data.keywords SQLite |
OpenSearch XML per engine + JSON inventory | Open each XML in Firefox → Settings → Search → Add |
| Downloads | History.downloads + downloads_url_chains |
Portable CSV, plus Firefox moz_annos download metadata when history direct-write is applied |
CSV is a reference artifact; direct-write makes matching rows visible in Firefox history/download surfaces |
| Passkeys | Known/likely WebAuthn stores | Inventory only (passkeys inventory) |
No export yet; counts only until CXF/CXP support exists |
Google Chrome (stable / Beta / Canary), Chromium, Brave (stable / Beta / Nightly), Microsoft Edge (stable / Beta / Dev), Vivaldi, Opera, Opera GX, Yandex, Arc, Thorium.
Any browser that follows the Chrome User Data\<profile> layout will be picked up automatically. Each browser's individual profiles (Default, Profile 1, Profile 2, ...) are detected separately.
Firefox (stable / Nightly / ESR), LibreWolf, Waterfox, Floorp, Mullvad Browser, Tor Browser, Zen Browser.
Any Gecko-based browser that ships a profiles.ini.
Requires Python 3.11+. Windows-first, but the runtime and CI matrix
cover Windows / macOS / Linux — same install steps everywhere; activate
the venv with .venv\Scripts\activate on Windows or . .venv/bin/activate
on macOS / Linux.
git clone https://github.com/SysAdminDoc/FoxPort.git
cd FoxPort
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt
python -m foxportChromium (since v80) wraps the AES-256 master key for the password store with Windows DPAPI and stores it in Local State under os_crypt.encrypted_key. Each saved password is an AES-256-GCM blob in the Login Data SQLite database, tagged with a v10/v11 prefix, nonce, ciphertext, and auth tag.
FoxPort:
- Loads
Local State, base64-decodes the wrapped key, strips theDPAPIprefix, and callsCryptUnprotectData. - Copies
Login Datato a temp directory (so it works even while the browser is running and holds a write-lock), opens it read-only, and decrypts each entry withcryptography.AESGCM. - Converts each row's Chromium WebKit timestamps (microseconds since 1601-01-01 UTC) to Firefox milliseconds since 1970-01-01 UTC, generates a fresh GUID, and writes the result as a CSV that
about:loginsnatively imports.
DPAPI only works on the same Windows user account that originally encrypted the data. Running FoxPort against a copy of someone else's profile won't decrypt their passwords — by design.
The Bookmarks file is plain JSON. FoxPort walks the bookmark_bar, other, and synced roots, converting them to the Netscape Bookmark HTML format that Firefox (and every other browser) has imported for two decades. The bookmark bar is tagged with PERSONAL_TOOLBAR_FOLDER="true" so it lands in Firefox's Bookmarks Toolbar.
Chrome and Firefox both speak WebExtensions, but Chrome's MV3 lockdown means extensions are not byte-for-byte portable. FoxPort instead resolves the identity of each Chrome extension through four progressively-less-confident stages:
- Curated map (
foxport/data/curated_extension_map.json) — 56 hand-verified Chrome ID → AMO slug pairs across 14 categories (ad blockers, password managers, userscripts, dev tools, AI assistants, ...). Zero network. Open the JSON file to contribute additions. The weeklycurated-map-audit.ymlworkflow re-verifies every slug against AMO and files a GitHub issue on broken or removed entries. The_meta.entry_countfield is asserted by the auditor so the docs can't drift again silently — see_meta.descriptionfor the dead-link policy. Advanced users/CI can pointFOXPORT_CURATED_MAP_PATHat a replacement JSON; FoxPort reloads the active map for every run. - Gecko ID probe — If the Chromium manifest declares
browser_specific_settings.gecko.id, FoxPort hits AMO's detail endpoint with that GUID for a 100%-confidence match. Duplicate GUIDs share one in-run AMO cache. - AMO name search — Otherwise, the localized extension name is queried against
addons.mozilla.org/api/v5/addons/search/and the top hit is taken, ranked by exact-name + prefix overlap, filtered to public + non-disabled. Duplicate names share the same in-run AMO cache. - Permission overlap — For non-curated matches, FoxPort compares Chrome's declared permissions to the candidate's AMO permissions and downgrades confidence when overlap is poor (
amo-search-lowif < 30%).
The resulting extensions.html shows match confidence, permission requests, user count, AMO rating, and whether the equivalent is already installed in your target Firefox profile (struck through). Offline runs (uncheck "Allow online AMO lookup") still produce a usable page from the curated table.
Extension settings are separate and opt-in. FoxPort currently allowlists
uBlock Origin filter/user rules, Stylus user styles, and Bitwarden self-hosted
server URLs. It writes extension-settings.json only with those fields; raw
extension storage, auth tokens, cached vault data, and other private keys are
not copied.
%USERPROFILE%\Documents\FoxPort\
└── 20260523-114205_Brave_-_Default__to__Firefox_-_default-release\
├── passwords.csv # for about:logins
├── bookmarks.html # for Library import
├── extensions.html # one-click AMO install page
├── extensions.json # machine-readable mapping
├── extension-settings.json # opt-in allowlisted settings
└── README.txt # step-by-step import instructions
The output folder is configurable in the UI.
FOXPORT_NSS_PATH— absolute path to a Firefoxnss3.dll/libnss3.dylib/libnss3.so. Lets NSS direct-write target portable Firefox installs that aren't in the standard search paths.
- Source profile read-only. FoxPort copies SQLite files to a temp dir before reading; it never writes to
Login Data,Bookmarks, or anything else in the source profile. - Local-only by default. Decryption happens on your machine. Four optional network calls exist and all can be turned off:
- AMO lookup for extension names + GUIDs (the "Allow online Add-ons lookup" checkbox /
--no-onlineflag). - The HIBP
https://api.pwnedpasswords.com/range/<prefix>breach scan, opt-in via the Items step or--hibp. Uses k-anonymity — only the first five hex chars of eachSHA-1(password)go over the wire and theAdd-Padding: trueheader is set. Plaintext never leaves the box. - Glean migration telemetry to
https://incoming.telemetry.mozilla.org, opt-in via Settings or--telemetry. It sends only selected category slugs, aggregate counts, direction, dry-run/direct-write flags, and outcome. It never sends paths, profile labels, URLs, hostnames, usernames, filenames, or secrets. Seedocs/telemetry.md. - Sentry crash reporting, opt-in via Settings or CLI
--crash-reportingand only active whenFOXPORT_SENTRY_DSNorSENTRY_DSNis configured. It disables locals/source context, strips paths before send, and does not enable Sentry's default argument/log/module integrations. Seedocs/crash-reporting.md.
- AMO lookup for extension names + GUIDs (the "Allow online Add-ons lookup" checkbox /
- Output files contain plaintext passwords. Treat
passwords.csvandsaved-cards.csvlike secrets. Delete them after importing. manifest.jsonper run. Every non-dry-run migration writes a schema-versioned manifest next toREADME.txtlisting every emitted artifact with its SHA-256, size, sensitivity label, and (when applicable) the absolute path of any direct-write backup. Plaintext secret values never appear in the manifest, only metadata about the files that contain them.- Direct-write into Firefox only via atomic replace. When you opt in to writing
logins.json/cookies.sqlite/places.sqlite/recovery.jsonlz4straight into a closed target profile, FoxPort stages the file in a temp dir, fsyncs, and swaps it atomically — an interrupted write can never leave a half-written file in your profile. Backups of the previous file are kept under timestamped names so a regret-undo is always possible. Cookies/history can use themergepolicy to preserve the target DB and add only source cookies absent by host/path/name or source history visits absent by URL+visit timestamp. - DPAPI scoping. Decryption only succeeds when running as the Windows user who originally saved the passwords.
- App-Bound Encryption (Chrome 127+, Brave) is detected and surfaced clearly; a full ABE bypass via the
foxport_abe.exesidecar ships when the signed-release pipeline lands. - NSS version guard. Before any direct-write into
logins.jsonFoxPort checks the version reported by the loadednss3.dll. Below NSS 3.x we refuse — setFOXPORT_NSS_FORCE=1to override only if you know what you're doing. - SBOM + signed build provenance. Each release publishes a CycloneDX SBOM of the bundled Python dependencies plus a SLSA build-provenance attestation signed via GitHub OIDC + Sigstore. Verify with
gh attestation verify FoxPort-<tag>-windows-x64.zip --owner SysAdminDoc --repo SysAdminDoc/FoxPort. See docs/supply-chain.md.
See ROADMAP.md.
- ROADMAP.md — active to-do list.
- COMPLETED.md — shipped roadmap history.
- RESEARCH_REPORT.md — research summary and archive index.
- CHANGELOG.md — release-level details.
See CHANGELOG.md.
MIT — see LICENSE.




