Coverage for pass_import/manager.py: 100%

72 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 os 

7from typing import Dict 

8from abc import abstractmethod 

9 

10from pass_import import clean 

11from pass_import.audit import Audit 

12from pass_import.core import Asset, Cap 

13 

14 

15class PasswordManager(Asset): 

16 """Interface for all password managers. 

17 

18 **Manager metadata** 

19 

20 :param str url: Public website of the password manager. 

21 :param str hexport: How to export data from the password manager. 

22 :param str himport: How to import data from the password manager. 

23 :param bool secure: A flag, to set to ``False`` if the password manager is 

24 considered not secure. 

25 

26 **Set by reading settings** 

27 

28 :param Action action: The current action for what the object is used. 

29 :param str root: Internal root where to import the passwords inside the pm. 

30 :param str delimiter: CSV delimiter character. Default: ``,`` 

31 :param str cols: String that shows the list of CSV expected 

32 columns to map columns to credential attributes. Only used for the CSV 

33 generic importer. 

34 

35 """ 

36 url = '' 

37 hexport = '' 

38 himport = '' 

39 secure = True 

40 keys: Dict[str, str] = {} 

41 keyslist = [ 

42 'title', 'password', 'login', 'email', 'url', 'comments', 'otpauth', 

43 'group' 

44 ] 

45 

46 def __init__(self, prefix=None, settings=None): 

47 settings = {} if settings is None else settings 

48 

49 self.data: Dict[str, str] = [] 

50 self.root = settings.get('root', '') 

51 self.cols = settings.get('cols', '') 

52 self.action = settings.get('action', Cap.IMPORT) 

53 self.delimiter = str(settings.get('delimiter', ',')) 

54 super().__init__(prefix) 

55 

56 @classmethod 

57 def usage(cls) -> str: 

58 """Get password manager usage.""" 

59 res = '\n'.join(cls.__doc__.split('\n')[1:-1]) 

60 if ':usage:' in res: 

61 res = res.split(':usage:')[1] 

62 while ' ' in res: 

63 res = res.replace(' ', ' ') 

64 return res 

65 return '' 

66 

67 @classmethod 

68 def description(cls) -> str: 

69 """Get password manager description.""" 

70 return cls.__doc__.split('\n', maxsplit=1)[0] 

71 

72 

73class PasswordImporter(PasswordManager): 

74 """Interface for all password managers that support importing passwords. 

75 

76 :param list[dict] data: The list of password entries imported by the parse 

77 method. Each password entry is a dictionary. 

78 :param list keyslist: The list of core key that will be present into the 

79 password entry, even without the extra option. 

80 :param dict keys: Correspondence dictionary between the password-store key 

81 name (``password``, ``title``, ``login``...), and the key name from the 

82 password manager considered. 

83 

84 """ 

85 cap = Cap.IMPORT 

86 

87 @abstractmethod 

88 def parse(self): 

89 """Parse the password manager repository and retrieve passwords.""" 

90 

91 def invkeys(self) -> Dict[str, str]: 

92 """Return the invert of ``keys``.""" 

93 return {v: k for k, v in self.keys.items()} 

94 

95 def _sortgroup(self, folders: Dict[str, Dict[str, str]]): 

96 """Order groups in ``data``. 

97 

98 :param dict folders: The group structure, it must be generated 

99 as follow: 

100 folders['<group-id>'] = { 

101 'group': '<name>', 

102 'parent': '<parent-id>' 

103 } 

104 """ 

105 for folder in folders.values(): 

106 parentid = folder.get('parent', '') 

107 parentname = folders.get(parentid, {}).get('group', '') 

108 folder['group'] = os.path.join(parentname, folder.get('group', '')) 

109 

110 for entry in self.data: 

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

112 entry['group'] = folders.get(groupid, {}).get('group', '') 

113 

114 

115class PasswordExporter(PasswordManager): 

116 """Interface for all password managers that support exporting passwords. 

117 

118 **Set by reading settings** 

119 

120 :param bool all: Ethier or not import all the data. Default: ``False`` 

121 :param bool force: Either or not to force the insert if the path already 

122 exist. Default: ``False`` 

123 

124 """ 

125 cap = Cap.EXPORT 

126 

127 def __init__(self, prefix=None, settings=None): 

128 settings = {} if settings is None else settings 

129 self.all = settings.get('all', False) 

130 self.force = settings.get('force', False) 

131 super().__init__(prefix, settings) 

132 

133 @abstractmethod 

134 def insert(self, entry: Dict[str, str]): 

135 """Insert a password entry into the password repository. 

136 

137 :param dict entry: The password entry to insert. 

138 :raises PMError: If the entry already exists or in case of 

139 a password manager error. 

140 """ 

141 

142 def clean(self, cmdclean: bool, convert: bool): 

143 """Clean data before export. 

144 

145 **Features:** 

146 

147 1. Remove unused keys and empty values. 

148 2. Clean the protocol's name in the title. 

149 3. Clean group from unwanted values in Unix or Windows paths. 

150 4. Duplicate paths. 

151 5. Format the One-Time Password (OTP) url. 

152 

153 :param bool cmdclean: 

154 If ``True``, make the paths more command line friendly. 

155 :param bool convert: 

156 If ``True``, convert the invalid characters present in the paths. 

157 

158 """ 

159 for entry in self.data: 

160 entry = clean.unused(entry) 

161 path = clean.group(clean.protocol(entry.pop('group', ''))) 

162 entry['path'] = clean.cpath(entry, path, cmdclean, convert) 

163 

164 clean.dpaths(self.data, cmdclean, convert) 

165 clean.dpaths(self.data, cmdclean, convert) 

166 clean.duplicate(self.data) 

167 clean.otp(self.data) 

168 

169 def audit(self, hibp: bool = False): 

170 """Audit the parsed password for vulnerable passwords. 

171 

172 **Features:** 

173 

174 1. Look for breached password from haveibeenpwned.com, 

175 2. Check for duplicated passwords, 

176 3. Check password strength estimaton using zxcvbn. 

177 

178 :param bool hibp: A flag, to set to ``True`` to look for breached 

179 password from haveibeenpwned.com 

180 :returns dict: A report dict. 

181 

182 """ 

183 audit = Audit(self.data) 

184 if hibp: 

185 audit.password() 

186 audit.zxcvbn() 

187 audit.duplicates() 

188 return audit.report