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
« 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#
6import os
7from typing import Dict
8from abc import abstractmethod
10from pass_import import clean
11from pass_import.audit import Audit
12from pass_import.core import Asset, Cap
15class PasswordManager(Asset):
16 """Interface for all password managers.
18 **Manager metadata**
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.
26 **Set by reading settings**
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.
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 ]
46 def __init__(self, prefix=None, settings=None):
47 settings = {} if settings is None else settings
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)
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 ''
67 @classmethod
68 def description(cls) -> str:
69 """Get password manager description."""
70 return cls.__doc__.split('\n', maxsplit=1)[0]
73class PasswordImporter(PasswordManager):
74 """Interface for all password managers that support importing passwords.
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.
84 """
85 cap = Cap.IMPORT
87 @abstractmethod
88 def parse(self):
89 """Parse the password manager repository and retrieve passwords."""
91 def invkeys(self) -> Dict[str, str]:
92 """Return the invert of ``keys``."""
93 return {v: k for k, v in self.keys.items()}
95 def _sortgroup(self, folders: Dict[str, Dict[str, str]]):
96 """Order groups in ``data``.
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', ''))
110 for entry in self.data:
111 groupid = entry.get('group', '')
112 entry['group'] = folders.get(groupid, {}).get('group', '')
115class PasswordExporter(PasswordManager):
116 """Interface for all password managers that support exporting passwords.
118 **Set by reading settings**
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``
124 """
125 cap = Cap.EXPORT
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)
133 @abstractmethod
134 def insert(self, entry: Dict[str, str]):
135 """Insert a password entry into the password repository.
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 """
142 def clean(self, cmdclean: bool, convert: bool):
143 """Clean data before export.
145 **Features:**
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.
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.
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)
164 clean.dpaths(self.data, cmdclean, convert)
165 clean.dpaths(self.data, cmdclean, convert)
166 clean.duplicate(self.data)
167 clean.otp(self.data)
169 def audit(self, hibp: bool = False):
170 """Audit the parsed password for vulnerable passwords.
172 **Features:**
174 1. Look for breached password from haveibeenpwned.com,
175 2. Check for duplicated passwords,
176 3. Check password strength estimaton using zxcvbn.
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.
182 """
183 audit = Audit(self.data)
184 if hibp:
185 audit.password()
186 audit.zxcvbn()
187 audit.duplicates()
188 return audit.report