-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbw_export_kp.py
266 lines (210 loc) · 7.34 KB
/
bw_export_kp.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
#!/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
# Or export a json file from bitwarden and then export xml using
$ python bw_export_kp.py <path/to/json/file> > 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=[], prefix=''):
"""
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)
# add prefix
k = prefix + k
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']))
# Parse Password History
if 'passwordHistory' in e:
hists = e['passwordHistory']
# loop on the list
for i, hist in enumerate(hists):
prefix = 'Old #%d ' % (i + 1)
fields.extend(get_fields(hist,
protected=['password'],
prefix=prefix))
# # Add the password
# key = prefix
# val = get_protected_value(hist['password'])
# fields.append(dict(Key=key, Value=val))
# # Add the date
# key = prefix + ' Date'
# val = hist['lastUsedDate']
# fields.append(dict(Key=key, Value=val))
# 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: '%s'" % cmd)
sys.exit(1)
return output
def get_bw_data(file_name=None):
"""
Gets the folders and items from Bitwarden CLI
"""
if file_name is None:
# 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))
else:
# load the contents of the json file
data = json.load(open(file_name, 'r'))
folders = data['folders']
items = data['items']
# Add null folder if not there
nullFolder = [f for f in folders if f['id'] is None]
if len(nullFolder) == 0:
folders.append({u'id': None, u'name': u'Root'})
# print(folders)
return folders, items
def main():
"""
Main function
"""
# The name of the json file
file_name = None
if len(sys.argv) > 1: file_name = sys.argv[1]
# get data from bw
bw_folders, bw_items = get_bw_data(file_name)
# 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()