Skip to content

Instantly share code, notes, and snippets.

@ignlg
Forked from mpalet/bw_export_kp.py
Last active March 31, 2020 18:20
Show Gist options
  • Save ignlg/0a349712a973cb94be67c8c4a3e3a196 to your computer and use it in GitHub Desktop.
Save ignlg/0a349712a973cb94be67c8c4a3e3a196 to your computer and use it in GitHub Desktop.
Export Bitwarden to KeePass 2 XML format, with custom banned fields to clean things up
#!/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 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_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 into protected values
fields = []
if 'fields' in e:
for f in e['fields']:
if f['name'] is not None:
fields.append(dict(Key=f['name'],
Value=get_protected_value(f['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:
if 'uris' in e['login']:
urls = [u['uri'] for u in e['login']['uris']]
urls = ','.join(urls)
username = e['login']['username']
password = e['login']['password']
# 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)
# print(entry)
return entry
def main():
"""
Main function
"""
# get output from bw CLI by listing all folders and items
# and returning a JSON object with
# 'folders': list of folders
# 'items': list of items
cmd = "(bw list folders | jq '{folders: .}' ; bw list items | jq '{items: .}') | cat | jq -s '. | add'"
status, output = commands.getstatusoutput(cmd)
# read stdin
# stdin = sys.stdin.read()
if status != 0:
print("Error ...")
print(output)
sys.exit(1)
# parse input json to a dict structure
d = json.loads(output)
#print(d['folders'][0]['name'])
# print(d['items'][0])
# print(get_entry(d['items'][0]))
# parse all entries
in_entries = [get_entry(e) for e in d['items']]
# Meta element
meta = dict()
# loop over folders
in_folders = d['folders']
out_folders = []
for f in in_folders:
# parse the folder
folder = get_folder(f)
folder_id = f['id']
# loop on entries in this folder
entries = []
for entry, item in zip(in_entries, d['items']):
if item['folderId'] == folder_id:
entries.append(entry)
# add if there is something
if len(entries) > 0:
folder['Entry'] = entries
# add to output folder
out_folders.append(folder)
# Root group
root_group = get_folder(dict(name='Root'))
root_group['Group'] = out_folders
# root_group['Entry'] = 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