Proton Pass's three exports
The PGP passphrase encrypts the vault but not the attachments packed beside it.
Checked against Proton Pass 1.40.2 exports Sources: Proton: How to export data from Proton Pass, ProtonMail/WebClients, RFC 4880: OpenPGP Message Format
Proton Pass offers three exports, and two of them are the same ZIP: plaintext JSON inside one, and that JSON sealed in an OpenPGP message inside the other. The third is a CSV, thinner than either and still holding the items you put in the trash.
| Form | File | Encrypted |
|---|---|---|
| ZIP One folder holding data.json: every vault and every item, in plaintext. | Proton Pass_export_*.zip |
No |
| PGP-encrypted ZIP The same ZIP with data.json swapped for data.pgp, an OpenPGP message sealed with a passphrase. Decrypts to the same JSON document. | Proton Pass_export_*.zip |
Yes |
| CSV Twelve columns. Cards and identities ride inside the note column as JSON, and trashed items come along with nothing marking them. | Proton Pass_export_*.csv |
No |
We exported one vault all three ways:
Either ZIP carries the whole vault. Take the encrypted one if the file will sit anywhere you do not control, and leave the CSV for tools that read nothing else.
How to export
Open Settings from the gear icon, which sits in the bottom left of the web app and behind the ☰ menu in the browser extension and the desktop app. Then open the Export tab. The mobile apps cannot export.
Pick the PGP-encrypted ZIP, the unencrypted ZIP or the CSV. Choosing encryption brings up a
passphrase field, and that passphrase is all that protects data.pgp, for reasons
covered below.
Either ZIP has a switch for including file attachments. Turning it on warns you, in so many words, that the attachments will not be encrypted. That applies to the PGP export too.
Before exporting a CSV, restore anything in the trash you want to keep and empty the rest. The ZIPs mark trashed items and the CSV does not, so anything left in there arrives looking live.
JSON format
Archive layout
The ZIP holds one folder, named after the app:
Proton Pass/
data.json the unencrypted export
data.pgp the encrypted export, in place of data.json
files/ attachments, when the export includes them
The folder is what identifies the archive. An importer matching on data.json alone
also claims a Bitwarden ZIP, which keeps a data.json of its own at the root.
Safari on macOS opens downloaded ZIPs by default, which leaves a loose Proton Pass
folder in Downloads instead of the archive. Buddy reads data.json or
data.pgp on its own, so the file inside that folder still imports. Hand it the archive
rather than the file when you can, though. Attachments live outside the document and come across
only when Buddy has the ZIP to read them out of.
Document layout
Vaults are keyed by their share id, and every item sits inside the vault that holds it:
{
"version": "1.40.2",
"userId": "k3Jx8vQzW0pN5tYbR2mL...",
"vaults": {
"Hq7cT1nZ4wKe9sVdA6fU...": {
"name": "Personal",
"description": "Personal vault",
"display": { "color": 0, "icon": 0 },
"items": [ ... ]
}
}
}
version is the Proton Pass release that wrote the file. userId is your
Proton account's id, so the file identifies you to anyone you hand it to.
Items
An item keeps what you typed in a data object and Proton's bookkeeping around it. A
login:
{
"itemId": "Pw4rN8xKc2mQ7tLz...",
"shareId": "Hq7cT1nZ4wKe9sVdA6fU...",
"data": {
"metadata": { "name": "GitHub", "note": "Personal account", "itemUuid": "4f2a91c7" },
"extraFields": [],
"type": "login",
"content": {
"itemEmail": "octocat@example.com",
"itemUsername": "octocat",
"password": "correct-horse-battery",
"totpUri": "otpauth://totp/GitHub:octocat?secret=JBSWY3DPEHPK3PXP&issuer=GitHub",
"urls": ["https://github.com/login"],
"autofillUrls": [{ "url": "https://github.com/login", "mode": 0 }],
"passkeys": []
}
},
"state": 1,
"aliasEmail": null,
"contentFormatVersion": 8,
"createTime": 1789426882,
"modifyTime": 1789426882,
"pinned": false,
"shareCount": 0,
"files": []
}
A login has two identity fields, itemUsername and itemEmail, and Proton's
form fills in either or both. Buddy uses the username when there is one and keeps the email beside
it as a field. When the email is all there is, it becomes the username.
urls and autofillUrls hold the same addresses, the second with each one's
autofill mode attached. The first becomes the entry's URL and the rest become URL fields.
Item types
data.type says which shape content takes:
login itemUsername, itemEmail, password, urls, totpUri, passkeys
alias empty: the address is aliasEmail, one level up
note empty: the text is metadata.note
creditCard cardholderName, number, verificationNumber, expirationDate, pin
identity 37 keys across personal, address, contact and work details
sshKey privateKey, publicKey, sections
wifi ssid, password, security, sections
custom sections, and the template's fields in extraFields
A card's expirationDate is written year first, as 2028-03, and becomes
03/28. Its pin imports as a secret in the card section.
An identity fills Buddy's address section from streetAddress, city,
stateOrProvince, zipOrPostalCode and countryOrRegion.
Everything else it holds becomes a field under Proton's own label, with the social security,
passport and license numbers concealed.
custom covers more than its name suggests. Proton's new-item menu offers a bank account, a
passport, a driver's license, a medical record, a membership, a reward program, an API credential, a
database, a server and several more, and every one of them is written out as custom.
What distinguishes them is a set of extraFields the template prefilled, whose labels you
can rename like any other. Nothing in the file records which template you started from. By the time
they reach the export, a passport and a scratch item built by hand are the same kind of thing.
Aliases
An alias is a forwarding address that Proton runs. The address is the only thing it holds, and it is
not in content:
{
"data": {
"metadata": { "name": "Newsletters", "note": "", "itemUuid": "84b7b598" },
"extraFields": [],
"type": "alias",
"content": {}
},
"aliasEmail": "shop.x7k2@passmail.net"
}
content is an empty object and aliasEmail sits a level up beside
state and createTime, so an importer that reads only content
brings every alias across as a blank entry. It is null on every other item type.
Buddy imports the address as the entry's username. The forwarding stays on Proton's servers, which means an alias delivers mail only while the Proton account behind it exists.
Custom fields
extraFields holds the fields you added and the ones a template put there for you. Each
one's type says which key its value is under:
"extraFields": [
{ "fieldName": "Employee ID", "type": "text", "data": { "content": "12345" } },
{ "fieldName": "Recovery code", "type": "hidden", "data": { "content": "X7K2-9QPL-M4TR" } },
{ "fieldName": "Backup 2FA", "type": "totp", "data": { "totpUri": "JBSWY3DPEHPK3PXP" } },
{ "fieldName": "Expires", "type": "timestamp", "data": { "timestamp": "2026-09-24" } }
]
textimports as a text field.hiddenbecomes a secret field.totpis the entry's own code when the item has none, and a secret field when it does.timestampis a plainYYYY-MM-DDdate, and an empty string when unset.
totpUri is named for the otpauth:// URI Proton writes when you scan a QR
code, but the key holds whatever you pasted, down to a bare base32 secret with no URI around it.
Buddy reads both shapes, and keeps anything it cannot parse as a code in a secret field rather than
dropping it.
SSH key, Wi-Fi and custom items add sections, each a sectionName and a list
of fields in this same shape. Identities do it through extraSections and four
extra…Details lists. Buddy imports every field in them, without the section names.
Vaults
Vaults are how Proton Pass groups items, and there is no folder level beneath them. Buddy turns the vault name into a tag when the export holds more than one vault with items in it. A single-vault export gains no tags, since a tag on every entry distinguishes nothing.
pinned has no equivalent and is dropped.
Trash
state is 1 for a live item and 2 for one in the trash. Both
ZIPs include trashed items marked this way, and Buddy leaves them behind.
Passkeys
A login carries a passkeys array. The passkeys stay behind, and the login gains a
Passkey (not imported) field naming the site in their place. Those fields add up to a
list of the accounts to enroll again by hand.
Attachments
Attachments are opt-in, and the switch for them sits next to the format. With it on, the files land
under Proton Pass/files/ and the item that owns them lists their names:
"files": ["Screenshot 2026-09-09 at 7.17.02 PM.426d8f9a45a37a6a.png"]
The name in files is the name of the entry in the archive, hash and all. Buddy
matches the two without guessing and imports the file onto the entry that owns it. The match is
byte-exact: a macOS screenshot carries its narrow no-break space through both, which is why nothing
here tidies the name up first.
Switch attachments off and the item still lists them, which leaves a name with no file behind it.
Buddy keeps that as an Attachment (not in export) field rather than a reference to
nothing, and does the same for a loose data.json, which has no archive around it to
hold the bytes.
Whichever ZIP you pick, these files are written in the clear. The PGP export seals
data.json and nothing else, so an encrypted export carrying attachments hands them to
anyone who has the archive. Treat that file as if the attachments were loose, because they are.
PGP-encrypted format
The encrypted export is the same ZIP with data.json replaced by data.pgp,
and with files/ untouched. That one file is a standard OpenPGP message sealed with a
passphrase rather than a key, so GnuPG opens it without any Proton software:
gpg --decrypt "Proton Pass/data.pgp" > data.json
Decrypted, it is the same JSON document the unencrypted ZIP holds, and the two import to identical entries, attachments included. The passphrase buys you the vault rather than the archive.
The armor
The file is ASCII-armored text in 60-character lines, with no header lines and a CRC-24 checksum at the end:
-----BEGIN PGP MESSAGE-----
wy4ECQMIDlfqBn8OVjD/knP/MLXFj0rRPXNyg0+hDROAQ6Z6LIY8V9p0f8tX
kFfb0s9tAZrl48gklzPQDI4OOlOkxKlB4JxWqV1dXIe8jYe4aD6wnj+jEE7p
...
=7Lzf
-----END PGP MESSAGE-----
Packets
Decoded, the message is two packets. The first says how to get a key from your passphrase, and the second is the vault. From a test export:
c3 2e tag 3: symmetric-key encrypted session key, 46 bytes
04 version 4
09 AES-256
03 08 iterated and salted S2K, SHA-256
0e 57 ea 06 7f 0e 56 30 salt
ff count: 65,011,712 bytes
(33 bytes) the session key, encrypted
d2 cf 6d tag 18: encrypted, integrity-protected data, 4,141 bytes
01 version 1
(4,140 bytes) the payload
Iterated and salted S2K runs the salt and passphrase, repeated end to end, through SHA-256 until
65,011,712 bytes have gone in. That count byte, ff, is the highest the format can
express. The hash unwraps a random 32-byte session key, and the session key decrypts the payload with
AES-256 in OpenPGP's CFB mode.
Inside the payload is one literal data packet holding data.json, uncompressed. After it
comes a modification detection code, a SHA-1 of everything before it, which is how a version 1
packet checks its integrity from before OpenPGP had authenticated encryption.
Passphrase strength
S2K is not memory-hard. Each guess at your passphrase costs 65 megabytes of SHA-256 and nothing more,
which a GPU gets through far faster than the Argon2d guarding a
Dashlane .dash. The
length of your passphrase is the whole of this file's protection. Make it long and random.
Wrong passphrases
A wrong passphrase shows itself before any JSON is parsed, through three checks of rising cost. First, the session key unwraps to noise whose first byte names AES-256 only once in 256 tries. Past that, the payload opens with 16 random bytes and repeats their last two, which noise matches once in 65,536. Anything that gets through both fails the SHA-1 at the end.
CSV format
Example file
type,name,url,autofillUrls,email,username,password,note,totp,createTime,modifyTime,vault
login,GitHub,https://github.com/login,"[{""url"":""https://github.com/login"",""mode"":0}]",octocat@example.com,octocat,correct-horse-battery,Personal account,otpauth://totp/GitHub:octocat?secret=JBSWY3DPEHPK3PXP&issuer=GitHub,1789426882,1789426882,Personal
creditCard,Travel card,,,,,,"{""cardholderName"":""A Person"",""cardType"":1,""number"":""4111111111111111"",""verificationNumber"":""123"",""expirationDate"":""2028-03"",""pin"":""1234"",""note"":""travel only""}",,1789426920,1789426920,Personal
alias,Newsletters,,,shop.x7k2@passmail.net,,,,,1789426950,1789426950,Personal
custom,Office door code,,,,,,,,1789427001,1789427001,Personal
login,Old login,https://example.com/,"[{""url"":""https://example.com/"",""mode"":0}]",,olduser,old-password,,,1789427044,1789427050,Personal
An alias puts its address in email and leaves username empty. The
custom row is what every item type the CSV has no columns for looks like: a name, a
vault, two timestamps, and nothing of what you stored in it. The last row is in the trash, and
nothing in the file says so.
Cards and identities
A card or identity row has no columns of its own. Proton writes the item's whole
content into note as JSON, with the real note tucked inside under a
note key. Read that column as text and every card arrives as a note full of JSON. Parse
it and the card is complete, PIN included.
What the CSV leaves out
Custom fields never reach the file, and a FIXME in Proton's exporter says as much.
Logins, aliases, cards, identities and notes have their content written out. SSH key and custom
items keep their name and note and nothing else, and a Wi-Fi item keeps its password but loses its
network name.
That takes a TOTP code with it. The totp column carries a login's own code, but a code
you added as a custom field is a custom field, and it is gone. If any of your two-factor secrets live
on anything other than a login, the CSV is the wrong file to leave with.
url holds only the addresses in Proton's default autofill mode, joined with a comma and a
space. autofillUrls holds every one of them as JSON, and is the column to read.
createTime and modifyTime are Unix seconds, so a CSV import keeps each
entry's real age. vault names the vault, and becomes a tag when the file spans more than
one.
Buddy field mapping
ZIP and PGP-encrypted ZIP
CSV
Buddy is a desktop password manager for macOS and Windows that imports these files. If your export looks different from what this page describes, tell us. Other managers are on the export formats page.