-
-
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
This file contains hidden or 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. | |
| 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