#!/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()