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.
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