Coverage for pass_import/managers/bitwarden.py: 99%

83 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-02-26 12:11 +0000

1# -*- encoding: utf-8 -*- 

2# pass import - Passwords importer swiss army knife 

3# Copyright (C) 2017-2024 Alexandre PUJOL <alexandre@pujol.io>. 

4# 

5 

6import json 

7 

8from pass_import.core import register_managers 

9from pass_import.formats.csv import CSV 

10from pass_import.formats.json import JSON 

11 

12 

13class BitwardenCSV(CSV): 

14 """Importer for Bitwarden in CSV format.""" 

15 name = 'bitwarden' 

16 url = 'https://bitwarden.com' 

17 hexport = 'Tools> Export Vault> File Format: .csv' 

18 himport = 'pass import bitwarden file.csv' 

19 keys = { 

20 'title': 'name', 

21 'password': 'login_password', 

22 'login': 'login_username', 

23 'url': 'login_uri', 

24 'comments': 'notes', 

25 'group': 'folder', 

26 'otpauth': 'login_totp', 

27 } 

28 

29 

30class BitwardenOrgCSV(BitwardenCSV): 

31 """Importer for Bitwarden in CSV format.""" 

32 default = False 

33 keys = { 

34 'title': 'name', 

35 'password': 'login_password', 

36 'login': 'login_username', 

37 'url': 'login_uri', 

38 'comments': 'notes', 

39 'group': 'collections', 

40 'otpauth': 'login_totp', 

41 } 

42 

43 

44class BitwardenJSON(JSON): 

45 """Importer for Bitwarden in JSON format.""" 

46 name = 'bitwarden' 

47 default = False 

48 url = 'https://bitwarden.com' 

49 hexport = 'Tools> Export Vault> File Format: .json' 

50 himport = 'pass import bitwarden file.json' 

51 ignore = { 

52 'login', 'id', 'folderId', 'collectionIds', 'organizationId', 'type', 

53 'favorite', 'secureNote' 

54 } 

55 nesting_keys = {'card', 'identity'} 

56 key_group = 'folders' 

57 key_group_id = 'folderId' 

58 keys = { 

59 'title': 'name', 

60 'password': 'password', 

61 'login': 'username', 

62 'url': 'uris', 

63 'otpauth': 'totp', 

64 'comments': 'notes', 

65 } 

66 json_header = { 

67 'encrypted': False, 

68 'folders': [{ 

69 'id': str, 

70 'name': str 

71 }], 

72 'items': [{ 

73 'id': str, 

74 'folderId': str, 

75 'type': int, 

76 'name': str, 

77 'favorite': bool, 

78 }], 

79 } 

80 

81 def _sortgroup(self, folders): 

82 for entry in self.data: 

83 groupid = entry.get('group', '') 

84 entry['group'] = folders.get(groupid, '') 

85 

86 def parse(self): 

87 """Parse Bitwarden JSON file.""" 

88 jsons = json.loads(self.file.read()) 

89 keys = self.invkeys() 

90 folders = {} 

91 for item in jsons.get(self.key_group, {}): 

92 key = item.get('id', '') 

93 folders[key] = item.get('name', '') 

94 

95 for item in jsons.get('items', {}): 

96 entry = {} 

97 if 'folder' in self.key_group_id: 

98 entry['group'] = item.get(self.key_group_id, '') 

99 else: 

100 entry['group'] = item.get(self.key_group_id, [''])[0] 

101 logins = item.get('login', {}) 

102 item.update(logins) 

103 for key, value in item.items(): 

104 if key in self.ignore: 

105 continue 

106 

107 if key == 'fields': 

108 self._parse_custom_fields(entry, value) 

109 elif key in self.nesting_keys: 

110 self._parse_nested(entry, value) 

111 else: 

112 if value: 

113 entry[keys.get(key, key)] = value 

114 

115 urls = entry.get('url') 

116 if urls and isinstance(urls, list) and urls[0]: 

117 entry['url'] = urls[0]['uri'] 

118 

119 if len(urls) > 1: 

120 index = 2 

121 for url in urls[1:]: 

122 entry[f'url{index}'] = url['uri'] 

123 index += 1 

124 

125 self.data.append(entry) 

126 self._sortgroup(folders) 

127 

128 @staticmethod 

129 def _parse_nested(destination_entry, nesting_source): 

130 for key, value in nesting_source.items(): 

131 if key in destination_entry.keys(): 

132 key = f'{key}_' 

133 if value: 

134 destination_entry[key] = value 

135 

136 @staticmethod 

137 def _parse_custom_fields(destination_entry, custom_fields): 

138 for field in custom_fields: 

139 name = field['name'] 

140 value = field['value'] 

141 if name in destination_entry.keys(): 

142 name = f'{name}_' 

143 if value: 

144 destination_entry[name] = value 

145 

146 

147class BitwardenOrgJSON(BitwardenJSON): 

148 """Importer for Bitwarden Organisation in JSON format.""" 

149 key_group = 'collections' 

150 key_group_id = 'collectionIds' 

151 json_header = { 

152 'encrypted': False, 

153 'collections': list, 

154 'items': [{ 

155 'id': str, 

156 'type': int, 

157 'name': str, 

158 'favorite': bool, 

159 'collectionIds': list, 

160 }], 

161 } 

162 

163 

164register_managers(BitwardenCSV, BitwardenJSON, 

165 BitwardenOrgCSV, BitwardenOrgJSON)