- Rust 100%
| src | ||
| tests | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| LICENSE | ||
| README.md | ||
import_passkeys_p2b
Migrate your Proton Pass vault — including passkeys — into Bitwarden, safely and verifiably.
Bitwarden's importers can ingest a Proton Pass JSON export, but they silently drop passkeys — Proton stores them in a format Bitwarden's import path doesn't understand. This tool decodes Proton's passkey key material, re-encodes it into Bitwarden's FIDO2 credential schema, and produces a Bitwarden-ready JSON file. It can either build a complete vault from scratch or graft passkeys onto an export you already trust.
Every write is checked by an independent verification pass before it hits disk, and a detailed report tells you exactly what happened to each passkey.
Table of Contents
- Features
- How it works
- Installation
- Usage
- CLI reference
- The migration report
- What gets converted
- Safety guarantees
- Testing
- Development
- License
Features
- Passkey migration — decodes Proton Pass passkeys (ES256 / P-256) and re-encodes them as Bitwarden FIDO2 credentials, the one thing stock importers throw away.
- Full vault conversion — logins, secure notes, credit cards, identities and custom items are mapped to their native Bitwarden types; Proton vaults become Bitwarden folders.
- Graft mode — attach passkeys to an existing Bitwarden export, matching them to the right login item by relying-party ID and username.
- Detailed reporting — a per-passkey report (plain-text or JSON) records every attach, duplicate, skip and failure with a reason.
- Independent verification — generated and patched vaults are re-validated before they're written; the tool refuses to emit a file that doesn't match its own intent.
- Dry-run mode — preview every action without writing the output vault.
- Strict mode — exit non-zero if any passkey needs manual review, ideal for scripting and CI.
- Single static binary — no runtime dependencies, pinned crate versions for reproducible builds.
How it works
┌───────────────────────────┐
Proton Pass │ import_passkeys_p2b │ Bitwarden JSON
JSON export ──▶│ ├─ decode passkeys │──▶ (importable)
│ ├─ map item types │
(optional) │ ├─ attach / duplicate │ report.txt / .json
Bitwarden ───▶│ └─ verify before write │──▶ (what happened)
export └───────────────────────────┘
Two modes are available:
| Mode | When to use | What it produces |
|---|---|---|
| Direct | You want Bitwarden to receive everything from Proton. | A complete Bitwarden vault built from the Proton export. |
| Graft | You already imported into Bitwarden and only need the missing passkeys. | A copy of your Bitwarden export with passkeys attached to matching items. |
Installation
Requires a Rust toolchain (stable).
# Build a release binary
cargo build --release
# ./target/release/import_passkeys_p2b
# Or install it onto your PATH
cargo install --path .
Usage
Direct conversion (recommended)
Convert a Proton Pass export into a ready-to-import Bitwarden vault. Omit --bitwarden to select direct mode.
import_passkeys_p2b \
--proton proton_export.json \
--out bitwarden_vault.json \
--report report.txt
Then import bitwarden_vault.json in Bitwarden via Tools → Import data → Bitwarden (json).
Graft mode
Already have a Bitwarden export and just want the passkeys added? Pass --bitwarden and the tool will patch a copy of it, matching each passkey to the right login item.
import_passkeys_p2b \
--proton proton_export.json \
--bitwarden existing_bitwarden_export.json \
--out bitwarden_with_passkeys.json \
--report report.json
The Bitwarden input must be an unencrypted JSON export containing an items array.
Dry runs
See exactly what would happen without writing the output vault. The report is still generated.
import_passkeys_p2b --proton proton_export.json --dry-run --report preview.txt
For automation, combine --strict so the process exits non-zero when any passkey needs manual attention:
import_passkeys_p2b --proton proton_export.json --strict || echo "Manual review required"
CLI reference
| Flag | Default | Description |
|---|---|---|
--proton <PROTON_JSON> |
(required) | Path to the Proton Pass JSON export. |
--bitwarden <BITWARDEN_JSON> |
(none) | Existing Bitwarden export to patch. Omit for direct conversion. |
--out <OUTPUT_JSON> |
bitwarden_with_passkeys.json |
Where to write the resulting Bitwarden vault. |
--report <REPORT> |
report.txt |
Report path. A .json extension emits JSON; anything else emits plain text. |
--dry-run |
off | Compute and report all actions but do not write the output vault. |
--strict |
off | Exit non-zero if any passkey could not be migrated cleanly. |
--no-create-duplicates |
off | Don't create duplicate items for extra passkeys on a single login. |
--replace-existing-passkeys |
off | (Graft mode) overwrite a passkey already present on a matched item. |
Exit codes: 0 success · 2 invalid/malformed input · 3 strict-mode failures · 1 other errors.
The migration report
Every run writes a report describing what happened to each passkey. Actions include ATTACH, CREATE_DUPLICATE, REPLACE_EXISTING_PASSKEY, their DRY_RUN_* variants, and review states such as UNMATCHED, AMBIGUOUS, UNSUPPORTED, CONFLICTED and DECODE_FAILURE.
Plain-text example:
Passkey migration report
Total passkeys: 3
Strict failures: 1
Actions:
ATTACH: 2
UNMATCHED: 1
ATTACH "GitHub" -> github.com (octocat) -> Bitwarden item "GitHub"
ATTACH "Google" -> google.com (you@example.com): attached passkey to login item
UNMATCHED "Old Service" -> oldservice.io (legacy): no Bitwarden login matched this relying party
Use a .json report path to get the same data as structured JSON for further processing.
What gets converted
In direct mode, Proton item types are mapped to their Bitwarden equivalents:
| Proton type | Bitwarden type |
|---|---|
login |
Login (with URIs, TOTP and passkeys) |
note |
Secure note |
creditCard |
Card (cardholder, number, expiry, CVV, PIN) |
identity |
Identity (name, address, contact, work details) |
custom |
Secure note with custom fields preserved |
| anything else | Secure note containing the original Proton JSON (nothing is lost) |
Proton vaults become Bitwarden folders, custom/extra fields are preserved (hidden fields stay hidden), and pinned items are marked as favorites. When a single login carries more than one passkey, additional ones are split into clearly-named duplicate items (disable with --no-create-duplicates).
Safety guarantees
- Verify-before-write: generated FIDO2 credentials are re-parsed and the P-256 private key is round-tripped; in graft mode the patched vault is diffed against the original to confirm only the intended items changed.
- Non-destructive: your input files are never modified. Output is always written to a separate file.
- No silent failures: anything that can't be migrated cleanly is surfaced in the report (and, under
--strict, in the exit code) rather than being dropped. - Encrypted exports rejected: the tool refuses encrypted Bitwarden input so it never produces a half-decoded vault.
Testing
cargo test
The suite covers item-type mapping, passkey decoding/encoding and the verification logic (21 tests).
Development
cargo build # debug build
cargo test # run the test suite
cargo clippy # lint
cargo fmt # format
The project is a small, dependency-light Rust crate. Source layout:
| File | Responsibility |
|---|---|
src/main.rs |
CLI parsing and orchestration of the two modes. |
src/direct.rs |
Build a full Bitwarden vault from a Proton export. |
src/proton.rs |
Parse Proton items and decode passkeys. |
src/bitwarden.rs |
Match and graft passkeys onto an existing Bitwarden export. |
src/crypto.rs |
Base64 / P-256 key-material helpers. |
src/verify.rs |
Independent verification of generated and patched vaults. |
src/report.rs |
Report and error types. |
License
Released under the BSD 2-Clause License. © 2026 Atri Hegde.