Last active
April 14, 2024 03:57
-
-
Save mohamedadaly/d145835080ff46f3288f00f1c537d78a to your computer and use it in GitHub Desktop.
Export Bitwarden to KeePass 2 XML format
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python | |
from __future__ import print_function | |
import base64 | |
import commands | |
import json | |
import sys | |
import uuid | |
import xmltodict | |
""" | |
Exports a Bitwraden database into an XML file conforming to KeePass 2 XML Format. | |
The advantage of the XML format, is that it supports importing custom fields from | |
Bitwarden into their own custom fields in KeePass 2, which is not currently supported | |
in the Bitwarden CSV import function. | |
Usage: | |
# 1. log into bw | |
$ bw login | |
# 2. export xml | |
$ python bw_export_kp.py > passwords.xml | |
# 3. import the passwords.xml file into KeePass 2 (or other KeePass clones that | |
# support importing KeePass2 XML formats) | |
# 4. delete passwords.xml | |
References: | |
- Bitwarden CLI: https://help.bitwarden.com/article/cli/ | |
- KeePass 2 XML: https://github.com/keepassxreboot/keepassxc-specs/blob/master/kdbx-xml/rfc.txt | |
""" | |
def get_uuid(name): | |
""" | |
Computes the UUID of the given string as required by KeePass XML standard | |
https://github.com/keepassxreboot/keepassxc-specs/blob/master/kdbx-xml/rfc.txt | |
""" | |
name = name.encode('ascii', 'ignore') | |
uid = uuid.uuid5(uuid.NAMESPACE_DNS, name) | |
return base64.b64encode(uid.bytes) | |
def get_folder(f): | |
""" | |
Returns a dict of the input folder JSON structure returned by Bitwarden. | |
""" | |
return dict(UUID=get_uuid(f['name']), | |
Name=f['name']) | |
def get_protected_value(v): | |
""" | |
Returns a Value element that is "memory protected" in KeePass | |
(useful for Passwords and sensitive custom fields/strings). | |
""" | |
return {'#text': v, '@ProtectInMemory': 'True'} | |
def get_fields(subitem, protected=[]): | |
""" | |
Returns the components of subitem as a fields array, | |
protecting the items in protected list | |
""" | |
fields = [] | |
for k, v in subitem.iteritems(): | |
# check if it's protected | |
if k in protected: | |
v = get_protected_value(v) | |
fields.append(dict(Key=k, Value=v)) | |
return fields | |
def get_entry(e): | |
""" | |
Returns a dict of the input entry (item from Bitwarden) | |
Parses the title, username, password, urls, notes, and custom fields. | |
""" | |
# Parse custom fields, protecting as necessary | |
fields = [] | |
if 'fields' in e: | |
for f in e['fields']: | |
if f['name'] is not None: | |
# get value | |
value = f['value'] | |
# if protected? | |
if f['type'] == 1: | |
value = get_protected_value(value) | |
# put together | |
fields.append(dict(Key=f['name'], Value=value)) | |
# default values | |
urls = '' | |
username, password = '', '' | |
notes = e['notes'] if e['notes'] is not None else '' | |
# read username, password, and url if a login item | |
if 'login' in e: | |
login = e['login'] | |
if 'uris' in login: | |
urls = [u['uri'] for u in login['uris']] | |
urls = ','.join(urls) | |
# get username and password | |
username = login['username'] | |
password = login['password'] | |
# add totop to fields as protected | |
fields.append(dict(Key='totp', | |
Value=get_protected_value(login['totp']))) | |
# Parse Card items | |
if 'card' in e: | |
# Make number a protected field | |
fields.extend(get_fields(e['card'], protected=['number'])) | |
# Parse Identity items | |
if 'identity' in e: | |
fields.extend(get_fields(e['identity'])) | |
# Check it's not None | |
username = username or '' | |
password = password or '' | |
# assemble the entry into a dict with a UUID | |
entry = dict(UUID=get_uuid(e['name']), | |
String=[dict(Key='Title', Value=e['name']), | |
dict(Key='UserName', Value=username), | |
dict(Key='Password', Value=get_protected_value(password)), | |
dict(Key='URL', Value=urls), | |
dict(Key='Notes', Value=notes) | |
] + fields) | |
return entry | |
def get_cmd_output(cmd): | |
""" | |
Returns the output of the given command | |
""" | |
status, output = commands.getstatusoutput(cmd) | |
if status != 0: | |
print("Error running command:", cmd) | |
sys.exit(1) | |
return output | |
def get_bw_data(): | |
""" | |
Gets the folders and items from Bitwarden CLI | |
""" | |
# get folders | |
cmd = 'bw list folders' | |
folders = json.loads(get_cmd_output(cmd)) | |
# get items | |
cmd = 'bw list items' | |
items = json.loads(get_cmd_output(cmd)) | |
return folders, items | |
def main(): | |
""" | |
Main function | |
""" | |
# get data from bw | |
bw_folders, bw_items = get_bw_data() | |
# parse all entries | |
entries = [get_entry(e) for e in bw_items] | |
# Meta element | |
meta = dict() | |
# loop over folders | |
# bw_folders = d['folders'] | |
folders = [] | |
root_entries = [] | |
for f in bw_folders: | |
# parse the folder | |
folder = get_folder(f) | |
folder_id = f['id'] | |
# loop on entries in this folder | |
folder_entries = [] | |
for entry, item in zip(entries, bw_items): | |
if item['folderId'] == folder_id: | |
folder_entries.append(entry) | |
# NoFolder (with None id) | |
if folder_id is None: | |
root_entries = folder_entries | |
# Normal folder | |
else: | |
if len(folder_entries) > 0: | |
folder['Entry'] = folder_entries | |
# add to output folder | |
folders.append(folder) | |
# Root group | |
root_group = get_folder(dict(name='Root')) | |
root_group['Group'] = folders | |
# add items to root folder | |
if len(root_entries) > 0: | |
root_group['Entry'] = root_entries | |
# Root element | |
root=dict(Group=root_group) | |
# xml document contents | |
xml = dict(KeePassFile=dict(Meta=meta, Root=root)) | |
# write XML document to stdout | |
print(xmltodict.unparse(xml, pretty=True)) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
FYI, an opposite script to translate a KeePass KDBX database to a bitwarden JSON file: https://gist.github.com/monperrus/578395c30667581677d1ec20b7d445de