1
1
import json
2
2
import logging
3
3
import os
4
+ import re
4
5
import subprocess
5
6
6
7
from argparse import ArgumentParser
8
+ from typing import Dict , List , Optional
7
9
from shlex import quote
8
10
9
11
from pykeepass import PyKeePass , create_database
10
12
from pykeepass .exceptions import CredentialsError
13
+ from pykeepass .group import Group as KPGroup
11
14
15
+ import folder as FolderType
12
16
from item import Item , Types as ItemTypes
13
17
14
18
logging .basicConfig (
17
21
datefmt = '%Y-%m-%d %H:%M:%S' ,
18
22
)
19
23
24
+ kp : Optional [PyKeePass ] = None
20
25
21
26
def bitwarden_to_keepass (args ):
27
+ global kp
22
28
try :
23
29
kp = PyKeePass (args .database_path , password = args .database_password , keyfile = args .database_keyfile )
24
30
except FileNotFoundError :
@@ -30,29 +36,7 @@ def bitwarden_to_keepass(args):
30
36
31
37
folders = subprocess .check_output (f'{ quote (args .bw_path )} list folders --session { quote (args .bw_session )} ' , shell = True , encoding = 'utf8' )
32
38
folders = json .loads (folders )
33
- # sort folders so that in the case of nested folders, the parents would be guaranteed to show up before the children
34
- folders .sort (key = lambda x : x ['name' ])
35
- groups_by_id = {}
36
- groups_by_name = {}
37
- for folder in folders :
38
- # entries not associated with a folder should go under the root group
39
- if folder ['id' ] is None :
40
- groups_by_id [folder ['id' ]] = kp .root_group
41
- continue
42
-
43
- parent_group = kp .root_group
44
- target_name = folder ['name' ]
45
-
46
- # check if this is a nested folder; set appropriate parent group if so
47
- folder_path_split = target_name .rsplit ('/' , maxsplit = 1 )
48
- if len (folder_path_split ) > 1 :
49
- parent_group = groups_by_name [folder_path_split [0 ]]
50
- target_name = folder_path_split [1 ]
51
-
52
- new_group = kp .add_group (parent_group , target_name )
53
-
54
- groups_by_id [folder ['id' ]] = new_group
55
- groups_by_name [folder ['name' ]] = new_group
39
+ groups_by_id = load_folders (folders )
56
40
logging .info (f'Folders done ({ len (groups_by_id )} ).' )
57
41
58
42
items = subprocess .check_output (f'{ quote (args .bw_path )} list items --session { quote (args .bw_session )} ' , shell = True , encoding = 'utf8' )
@@ -114,6 +98,35 @@ def bitwarden_to_keepass(args):
114
98
kp .save ()
115
99
logging .info ('Export completed.' )
116
100
101
+ def load_folders (folders ) -> Dict [str , KPGroup ]:
102
+ # sort folders so that in the case of nested folders, the parents would be guaranteed to show up before the children
103
+ folders .sort (key = lambda x : x ['name' ])
104
+
105
+ # dict to store mapping of Bitwarden folder id to keepass group
106
+ groups_by_id : Dict [str , KPGroup ] = {}
107
+
108
+ # build up folder tree
109
+ folder_root : FolderType .Folder = FolderType .Folder (None )
110
+ folder_root .keepass_group = kp .root_group
111
+ groups_by_id [None ] = kp .root_group
112
+
113
+ for folder in folders :
114
+ if folder ['id' ] is not None :
115
+ new_folder : FolderType .Folder = FolderType .Folder (folder ['id' ])
116
+ # regex lifted from https://github.com/bitwarden/jslib/blob/ecdd08624f61ccff8128b7cb3241f39e664e1c7f/common/src/services/folder.service.ts#L108
117
+ folder_name_parts : List [str ] = re .sub (r'^\/+|\/+$' , '' , folder ['name' ]).split ('/' )
118
+ FolderType .nested_traverse_insert (folder_root , folder_name_parts , new_folder , '/' )
119
+
120
+ # create keepass groups based off folder tree
121
+ def add_keepass_group (folder : FolderType .Folder ):
122
+ parent_group : KPGroup = folder .parent .keepass_group
123
+ new_group : KPGroup = kp .add_group (parent_group , folder .name )
124
+ folder .keepass_group = new_group
125
+ groups_by_id [folder .id ] = new_group
126
+
127
+ FolderType .bfs_traverse_execute (folder_root , add_keepass_group )
128
+
129
+ return groups_by_id
117
130
118
131
def check_args (args ):
119
132
if args .database_keyfile :
0 commit comments