tl;dr
I wrote a script to help convert exported JSON Bitwarden vaults to KeePassXC compatible CSVs.
- View the GitHub repo
- Read the limitations here.
Introduction
Let’s say you use Bitwarden as your password manager. It’s convenient, cloud native, has a free tier and is open source. As far as password managers go, this is the one I recommend to most people.
But what if you want to switch to a completely offline password manager, like KeePassXC? Unfortunately it’s not as simple as exporting your vault from Bitwarden, and then importing into a new password database in KeePassXC.
A Tale of Two Formats
Bitwarden supports exporting vaults in JSON and CSV format. But CSV exports have a limitation, per their docs:
Bitwarden .csv files will only handle logins and secure notes. If you need to import or export identities and cards as well, use JSON.
Here’s an example:
{
"folders": [
{
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"name": "Folder Name"
},
...
],
"items": [
{
"passwordHistory": [
{
"lastUsedDate": "YYYY-MM-00T00:00:00.000Z",
"password": "passwordValue"
}
],
"revisionDate": "YYYY-MM-00T00:00:00.000Z",
"creationDate": "YYYY-MM-00T00:00:00.000Z",
"deletedDate": null,
"id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
"organizationId": null,
"folderId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"type": 1,
"reprompt": 0,
"name": "My Gmail Login",
"notes": "This is my gmail login for import.",
"favorite": false,
"fields": [
{
"name": "custom-field-1",
"value": "custom-field-value",
"type": 0
},
...
],
"login": {
"uris": [
{
"match": null,
"uri": "https://mail.google.com"
}
],
"username": "myaccount@gmail.com",
"password": "myaccountpassword",
"totp": otpauth://totp/my-secret-key
},
"collectionIds": null
},
...
]
}
KeePassXC does not support import and export of JSON files (though it is in the queue). It supports importing as a CSV, like this:
"Group","Title","Username","Password","URL","Notes","TOTP","Icon","Last Modified","Created"
"Root","My Password","test","password","https://example.com","","","0","2024-02-20T20:45:40Z","2024-02-20T20:45:22Z"
This is pretty straightforward. Below is how we will map the fields:
Bitwarden Field | KeePassXC Field |
---|---|
folderId (mapped via folders dict) | Group |
name | Title |
login.username | Username |
login.password | Password |
login.uris[0].uri | URL |
notes , fields , card | Notes |
A couple things:
- Notice that the bitwarden fields
notes
,fields
, andcard
are mapped to theNotes
KeePassXC field TOTP
fields aren’t handled- These fields will require some additional work and are outside the current scope.1
Implementation
At a high level, convert.py
script works as follows:
- Takes an exported Bitwarden JSON file as an argument and reads it using Python’s
json.load()
. - Loads folder mappings from the JSON to a Python dictionary with
load_folders()
. - Iterates over items, processing each into CSV format using
process_item()
, leveraging Python’sdict.get()
for field extraction. - Writes the processed data to a new CSV file using
csv.writer()
Here it is:
convert.py
import json
import csv
import sys
def load_folders(folder_data):
folders = {folder["id"]: folder["name"] for folder in folder_data}
folders[None] = "No Folder"
return folders
def process_item(item, folders):
group = folders.get(item.get("folderId"), "Imported")
title = item.get("name", "")
username = (
item.get("login", {}).get("username", "") if item.get("type") == 1 else ""
)
password = (
item.get("login", {}).get("password", "") if item.get("type") == 1 else ""
)
url = (
item.get("login", {}).get("uris", [{}])[0].get("uri", "")
if item.get("type") == 1 and item.get("login", {}).get("uris")
else ""
)
## If the item has additional fields, we add them to the `Notes` field.
## IMPORTANT: If the additional field was of type "hidden", it will be shown in plain text inside the `Notes` field!
## TODO: Map to KeePassXC entry's 'Advanced' > 'Additional Attributes' field (with "protect" flag set to "true" for hidden fields)
notes = item.get("notes", "") or ""
if item.get("fields"):
for field in item["fields"]:
notes += f"\n{field['name']}: {field.get('value', '')}"
if item.get("type") == 2:
notes += "\nSecure Note"
if item.get("type") == 3:
card = item.get("card", {})
notes += f"\nCard Number: {card.get('number', '')}\nExpiry: {card.get('expMonth', '')}/{card.get('expYear', '')}"
return [group, title, username, password, url, notes]
def convert(bw_file):
csv_path = bw_file.rsplit('.', 1)[0] + '_keepassxc.csv'
with open(bw_file, "r", encoding="utf-8") as json_file:
data = json.load(json_file)
folders = load_folders(data.get("folders", []))
items = data.get("items", [])
with open(csv_path, "w", newline="", encoding="utf-8") as csv_file:
csv_writer = csv.writer(csv_file)
csv_writer.writerow(
["Group", "Title", "Username", "Password", "URL", "Notes"]
)
for item in items:
csv_writer.writerow(process_item(item, folders))
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python json2csv.py bitwarden_exported_json_file.json")
sys.exit(1)
bw_file = sys.argv[1]
convert(bw_file)
print(f"Conversion complete. KeePassXC CSV file created at {bw_file.rsplit('.', 1)[0] + '_keepassxc.csv'}")
It’s basic with no frills, but solves my problem.
Conclusion
The convert.py
script is a practical tool for converting Bitwarden JSON vaults to KeePassXC compatible CSVs. It handles essential fields for a basic password manager migration, but does not cover all fields like TOTP
and any Custom Fields defined for Bitwarden entries.
The script is a good starting point for those transitioning from Bitwarden to KeePassXC. Future enhancements could include handling more complex fields and scenarios.
Feel free to contact me or contribute!
Ideally we’d map each Bitwarden Custom Fields to the corresponding KeePassXC entry’s ‘Advanced’ > ‘Additional Attributes’ field. This might work for each Custom Field type
Text
,Boolean
,Hidden
, and maybeLinked
. ↩︎