summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimeon Simeonov2026-05-04 18:38:58 +0200
committerSimeon Simeonov2026-05-04 18:38:58 +0200
commit7fd7db9cd12d59f7c523dc2a1a167f7445e5b2f3 (patch)
treeb29bec47d27aa927bac8b622bcb04fcfd4691892
parent3e4185913b584f5cc9a54f16f3c223861e11ec18 (diff)
Add support for the %e{key} format and add support for type checkers (ty)2.3.0next
-rw-r--r--.ruff.toml25
-rw-r--r--CHANGELOG.md11
-rw-r--r--README.md24
-rw-r--r--pyproject.toml14
-rw-r--r--src/etoolkit/__init__.py2
-rw-r--r--src/etoolkit/__main__.py51
-rw-r--r--src/etoolkit/etoolkit.py141
-rw-r--r--tests/conftest.py38
-rw-r--r--tests/test_cli.py91
-rw-r--r--tests/test_envtoolkit_instance.py17
-rw-r--r--tests/test_envtoolkit_instance_static.py80
11 files changed, 287 insertions, 207 deletions
diff --git a/.ruff.toml b/.ruff.toml
index 769f83c..da8c030 100644
--- a/.ruff.toml
+++ b/.ruff.toml
@@ -2,36 +2,34 @@ cache-dir = "~/.cache/ruff"
2indent-width = 4 2indent-width = 4
3line-length = 79 3line-length = 79
4target-version = "py310" 4target-version = "py310"
5namespace-packages = ["tests"]
6
5 7
6[lint] 8[lint]
7select = ["ALL"] 9select = ["ALL"]
8ignore = ["COM812", "D105", "D202", "D203", "D205", "D211", "D212", "D400", "D401", "D403", "D415", "ERA001", "FBT001", "FBT002", "PTH111", "RUF012", "RUF013", "S101", "TRY300", "BLE001", "UP020", "C901", "D200", "D402", "EM101", "EM102", "FBT003", "INP001", "PLR0912", "PLR0913", "PLR0915", "PLR2004", "PLW2901", "S603", "T201", "TRY003", "TRY400"] 10ignore = ["BLE001", "COM812", "D203", "D205", "D212", "D400", "D401", "D403", "D415", "FBT001", "FBT002", "PTH111", "S101", "UP020", "D200", "D402", "EM101", "EM102", "FBT003", "PLR0913", "PLR2004", "PLW2901", "S603", "T201", "TRY003", "TRY400"]
9# D105 - Missing docstring in magic method 11
10# D200 - One-line docstring should fit on one line 12# BLE001 - Do not catch blind exception: `Exception`
11# D203 - 1 blank line required before class docstring 13# COM812 - Trailing comma missing
12# D205 - 1 blank line required between summary line and description 14# D205 - 1 blank line required between summary line and description
15# D212 - Multi-line docstring summary should start at the first line
16# D400 - First line should end with a period
17# D401 - First line of docstring should be in imperative mood
13# D403 - First word of the first line should be capitalized: `str` -> `Str` 18# D403 - First word of the first line should be capitalized: `str` -> `Str`
19# D415 - First line should end with a period, question mark, or exclamation point
14# FBT001 - Boolean-typed positional argument in function definition 20# FBT001 - Boolean-typed positional argument in function definition
15# FBT002 - Boolean default positional argument in function definition 21# FBT002 - Boolean default positional argument in function definition
16# PTH111 - `os.path.expanduser()` should be replaced by `Path.expanduser()` 22# PTH111 - `os.path.expanduser()` should be replaced by `Path.expanduser()`
17# RUF012 - Mutable class attributes should be annotated with `typing.ClassVar`
18# RUF013 - PEP 484 prohibits implicit `Optional`
19# S101 - Use of `assert` detected 23# S101 - Use of `assert` detected
20# TRY300 - Consider moving this statement to an `else` block 24# TRY300 - Consider moving this statement to an `else` block
21# TRY400 - Use `logging.exception` instead of `logging.error` 25# TRY400 - Use `logging.exception` instead of `logging.error`
22# UP020 - Use builtin `open` 26# UP020 - Use builtin `open`
23 27
24# Project specific 28# Project specific
25# C901 - `X` is too complex
26# D200 - One-line docstring should fit on one line
27# D402 - First line should not be the function's signature (bug in ruff 0.4.4)
28# EM101 - Exception must not use a string literal, assign to variable first 29# EM101 - Exception must not use a string literal, assign to variable first
29# EM102 - Exception must not use an f-string literal, assign to variable first 30# EM102 - Exception must not use an f-string literal, assign to variable first
30# FBT003 - Boolean positional value in function call 31# FBT003 - Boolean positional value in function call
31# INP001 - File `tests/test_envtoolkit_instance_static.py` is part of an implicit namespace package. Add an `__init__.py`.
32# PLR0912 - Too many branches
33# PLR0913 - Too many arguments in function definition 32# PLR0913 - Too many arguments in function definition
34# PLR0915 - Too many statements
35# PLR2004 - Magic value used in comparison, consider replacing `X` with a constant variable 33# PLR2004 - Magic value used in comparison, consider replacing `X` with a constant variable
36# PLW2901 - `for` loop variable `value` overwritten by assignment target 34# PLW2901 - `for` loop variable `value` overwritten by assignment target
37# S603 - `subprocess` call: check for execution of untrusted input 35# S603 - `subprocess` call: check for execution of untrusted input
@@ -42,9 +40,6 @@ ignore = ["COM812", "D105", "D202", "D203", "D205", "D211", "D212", "D400", "D40
42fixable = ["ALL"] 40fixable = ["ALL"]
43unfixable = [] 41unfixable = []
44 42
45[lint.per-file-ignores]
46"tests/*.py" = ["ANN"] # Do not require annotations for tests
47
48 43
49[format] 44[format]
50# Like Black, use double quotes for strings. 45# Like Black, use double quotes for strings.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe4373f..004f29d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
1# Changelog 1# Changelog
2 2
3## [2.3.0](https://codeberg.org/sgs/etoolkit/releases/tag/2.3.0) (2026-05-04)
4
5[Full Changelog](https://codeberg.org/sgs/etoolkit/compare/2.2.0...2.3.0)
6
7**Changes:**
8
9- add support for the %e{key} format
10
11- add support for type checkers (ty)
12
13
3## [2.2.0](https://codeberg.org/sgs/etoolkit/releases/tag/2.2.0) (2026-04-16) 14## [2.2.0](https://codeberg.org/sgs/etoolkit/releases/tag/2.2.0) (2026-04-16)
4 15
5[Full Changelog](https://codeberg.org/sgs/etoolkit/compare/2.1.0...2.2.0) 16[Full Changelog](https://codeberg.org/sgs/etoolkit/compare/2.1.0...2.2.0)
diff --git a/README.md b/README.md
index bdd97e3..dae06b8 100644
--- a/README.md
+++ b/README.md
@@ -56,10 +56,20 @@ for processes that were not spawned by that same *etoolkit* session.
56 56
57## Installation 57## Installation
58 58
59### pip (pypi) 59### Arch Linux
60 60
61 ```bash 61 ```bash
62 pip install etoolkit 62 # fetch sgs' developer key from the keyserver and sign it
63 pacman-key --recv-keys A6645797661E2F473DD3FF06BCE70555C3BB08F7
64 pacman-key --lsign-key A6645797661E2F473DD3FF06BCE70555C3BB08F7
65
66 # add sgs' repository in /etc/pacman.conf and require packages to be signed
67 [sgs]
68 Server = https://pkg.pichove.org/archlinux/$repo/os/$arch
69 SigLevel = PackageRequired
70
71 # install etoolkit
72 pacman -S python-etoolkit
63 ``` 73 ```
64 74
65 75
@@ -72,6 +82,14 @@ for processes that were not spawned by that same *etoolkit* session.
72 emerge dev-python/etoolkit 82 emerge dev-python/etoolkit
73 ``` 83 ```
74 84
85
86### pip (pypi)
87
88 ```bash
89 pip install etoolkit
90 ```
91
92
75## Encryption & decryption scheme 93## Encryption & decryption scheme
76 94
77The etoolkit encryption format is currently at version 2. 95The etoolkit encryption format is currently at version 2.
@@ -262,6 +280,8 @@ When all values are fetched from a given instance (and its parents) and then
262decrypted, they are further processed by replacing macros with their 280decrypted, they are further processed by replacing macros with their
263corresponding values. Currently the following macros are supported: 281corresponding values. Currently the following macros are supported:
264 282
283- **%e{MYVAR}** - the current value of the env. var. *MYVAR*
284
265- **%e** - the current value of the env. var. corresponding to the same key 285- **%e** - the current value of the env. var. corresponding to the same key
266 286
267- **%h** - the home directory of the user running *etoolkit* (~/) 287- **%h** - the home directory of the user running *etoolkit* (~/)
diff --git a/pyproject.toml b/pyproject.toml
index e44adbb..4f7a722 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,6 +28,14 @@ dependencies = [
28] 28]
29 29
30 30
31[dependency-groups]
32dev = [
33 "pytest>=6",
34 "ruff>=0.15.0",
35 "ty>=0.0.31",
36]
37
38
31[project.scripts] 39[project.scripts]
32etoolkit = "etoolkit.__main__:main" 40etoolkit = "etoolkit.__main__:main"
33 41
@@ -42,6 +50,7 @@ Changelog = "https://codeberg.org/sgs/etoolkit/src/branch/master/CHANGELOG.md"
42[tool.pytest.ini_options] 50[tool.pytest.ini_options]
43minversion = "6.0" 51minversion = "6.0"
44addopts = "-s" 52addopts = "-s"
53cache_dir = "~/.cache/pytest_cache"
45testpaths = [ 54testpaths = [
46 "tests" 55 "tests"
47] 56]
@@ -54,6 +63,11 @@ pythonpath = [
54version = {attr = "etoolkit.__version__"} 63version = {attr = "etoolkit.__version__"}
55 64
56 65
66[tool.ty.environment]
67python-version = "3.10"
68root = ["./src"]
69
70
57[build-system] 71[build-system]
58requires = [ 72requires = [
59 "setuptools >= 77.0.3", 73 "setuptools >= 77.0.3",
diff --git a/src/etoolkit/__init__.py b/src/etoolkit/__init__.py
index fc32b61..416be15 100644
--- a/src/etoolkit/__init__.py
+++ b/src/etoolkit/__init__.py
@@ -18,7 +18,7 @@
18from .etoolkit import EtoolkitInstance, EtoolkitInstanceError 18from .etoolkit import EtoolkitInstance, EtoolkitInstanceError
19 19
20__author__ = 'Simeon Simeonov' 20__author__ = 'Simeon Simeonov'
21__version__ = '2.2.0' 21__version__ = '2.3.0'
22__license__ = 'GPL3' 22__license__ = 'GPL3'
23 23
24 24
diff --git a/src/etoolkit/__main__.py b/src/etoolkit/__main__.py
index cef3d42..33e5741 100644
--- a/src/etoolkit/__main__.py
+++ b/src/etoolkit/__main__.py
@@ -61,10 +61,11 @@ class EtoolkitCLIHandler:
61 self._args = args 61 self._args = args
62 self._config_dict = config_dict 62 self._config_dict = config_dict
63 63
64 self._password_hash = None 64 self._password_hash: str = ''
65
65 if 'general' in config_dict: 66 if 'general' in config_dict:
66 self._password_hash = config_dict['general'].get( 67 self._password_hash = config_dict['general'].get(
67 'MASTER_PASSWORD_HASH' 68 'MASTER_PASSWORD_HASH', ''
68 ) 69 )
69 70
70 self._password_from_env = os.environ.get('ETOOLKIT_MASTER_PASSWORD') 71 self._password_from_env = os.environ.get('ETOOLKIT_MASTER_PASSWORD')
@@ -168,9 +169,28 @@ class EtoolkitCLIHandler:
168 ) 169 )
169 print(f'Master password hash: {phash}') 170 print(f'Master password hash: {phash}')
170 171
172 def handle_args(self) -> None:
173 """Runs the handler"""
174 if self._args.decrypt_value:
175 self.decrypt_value()
176 return
177 if self._args.encrypt_value:
178 self.encrypt_value()
179 return
180 if self._args.password_hash:
181 self.generate_master_password_hash()
182 return
183 if self._args.list:
184 self.list()
185 return
186 if self._args.reencrypt:
187 self.reencrypt()
188 return
189
190 self.load_instance()
191
171 def list(self) -> None: 192 def list(self) -> None:
172 """Lists all instances defined in the config file""" 193 """Lists all instances defined in the config file"""
173
174 for instance_name in sorted( 194 for instance_name in sorted(
175 filter( 195 filter(
176 lambda s: not s.startswith('_'), 196 lambda s: not s.startswith('_'),
@@ -181,7 +201,6 @@ class EtoolkitCLIHandler:
181 201
182 def load_instance(self) -> None: 202 def load_instance(self) -> None:
183 """Loads a single specified instance from the config file""" 203 """Loads a single specified instance from the config file"""
184
185 inst = etoolkit.EtoolkitInstance( 204 inst = etoolkit.EtoolkitInstance(
186 self._args.instance, self._config_dict 205 self._args.instance, self._config_dict
187 ) 206 )
@@ -277,9 +296,8 @@ class EtoolkitCLIHandler:
277 ) 296 )
278 297
279 298
280def main(inargs: list = None) -> None: 299def main(inargs: list | None = None) -> None:
281 """main entry point""" 300 """main entry point"""
282
283 parser = argparse.ArgumentParser( 301 parser = argparse.ArgumentParser(
284 prog=__package__, 302 prog=__package__,
285 epilog=( 303 epilog=(
@@ -435,26 +453,11 @@ def main(inargs: list = None) -> None:
435 raise SystemExit(errno.EIO) from exp 453 raise SystemExit(errno.EIO) from exp
436 try: 454 try:
437 etoolkit_cli_handler = EtoolkitCLIHandler(args, config_dict) 455 etoolkit_cli_handler = EtoolkitCLIHandler(args, config_dict)
438 if args.decrypt_value: 456 etoolkit_cli_handler.handle_args()
439 etoolkit_cli_handler.decrypt_value() 457 sys.exit(0)
440 sys.exit(0)
441 if args.encrypt_value:
442 etoolkit_cli_handler.encrypt_value()
443 sys.exit(0)
444 if args.password_hash:
445 etoolkit_cli_handler.generate_master_password_hash()
446 sys.exit(0)
447 if args.list:
448 etoolkit_cli_handler.list()
449 sys.exit(0)
450 if args.reencrypt:
451 etoolkit_cli_handler.reencrypt()
452 sys.exit(0)
453
454 etoolkit_cli_handler.load_instance()
455 except KeyboardInterrupt: 458 except KeyboardInterrupt:
456 logger.debug('KeyboardInterrupt') 459 logger.debug('KeyboardInterrupt')
457 print(os.linesep) 460 print('\n')
458 sys.exit(0) 461 sys.exit(0)
459 except etoolkit.EtoolkitInstanceError as err: 462 except etoolkit.EtoolkitInstanceError as err:
460 logger.error('EtoolkitInstanceError: %s', err) 463 logger.error('EtoolkitInstanceError: %s', err)
diff --git a/src/etoolkit/etoolkit.py b/src/etoolkit/etoolkit.py
index f5ce2a6..9369571 100644
--- a/src/etoolkit/etoolkit.py
+++ b/src/etoolkit/etoolkit.py
@@ -19,7 +19,7 @@ import base64
19import getpass 19import getpass
20import hashlib 20import hashlib
21import os 21import os
22from collections.abc import Callable 22from typing import Protocol
23 23
24from cryptography.exceptions import InvalidTag 24from cryptography.exceptions import InvalidTag
25from cryptography.hazmat.primitives.ciphers.aead import AESGCM 25from cryptography.hazmat.primitives.ciphers.aead import AESGCM
@@ -31,6 +31,24 @@ class EtoolkitInstanceError(Exception):
31 """EtoolkitInstanceError - Generic exceptions related to instances""" 31 """EtoolkitInstanceError - Generic exceptions related to instances"""
32 32
33 33
34class PromptFuncProtocol(Protocol):
35 """Specialized callable for prompt functions"""
36
37 def __call__(self, password_hash: str = '', confirm: bool = True) -> str:
38 """
39 Prompts for master password and then for confirmation if `confirm` True
40
41 :param password_hash: Hash to compare with instead of confirm (def. '')
42 :type password_hash: str
43
44 :param confirm: Confirm the password (and see if there is a match)
45 :type confirm: bool
46
47 :return: Password provided by the user
48 :rtype: str
49 """
50
51
34class EtoolkitInstance: 52class EtoolkitInstance:
35 """A basic class representing a single instance""" 53 """A basic class representing a single instance"""
36 54
@@ -47,9 +65,12 @@ class EtoolkitInstance:
47 self._env = None 65 self._env = None
48 self._raw_env_variables = {} 66 self._raw_env_variables = {}
49 self._sensitive_env_variables = [] 67 self._sensitive_env_variables = []
50 self._master_password = None 68 self._master_password: str | None = None
51 self._master_password_hash = None 69 self._master_password_hash: str = ''
52 self._prompt_func = None # function to use when prompting for input 70
71 # function to use when prompting for input
72 self._prompt_func: PromptFuncProtocol | None = None
73
53 try: 74 try:
54 self._instance_data = data['instances'][name] 75 self._instance_data = data['instances'][name]
55 except KeyError as err: 76 except KeyError as err:
@@ -106,12 +127,12 @@ class EtoolkitInstance:
106 return self._name 127 return self._name
107 128
108 @property 129 @property
109 def prompt_func(self) -> Callable[[str, bool], str]: 130 def prompt_func(self) -> PromptFuncProtocol | None:
110 """prompt_func-property""" 131 """prompt_func-property"""
111 return self._prompt_func 132 return self._prompt_func
112 133
113 @prompt_func.setter 134 @prompt_func.setter
114 def prompt_func(self, value: Callable[[str, bool], str]) -> None: 135 def prompt_func(self, value: PromptFuncProtocol) -> None:
115 """prompt_func-property setter""" 136 """prompt_func-property setter"""
116 self._prompt_func = value 137 self._prompt_func = value
117 if self._parent is not None and self._parent.prompt_func is None: 138 if self._parent is not None and self._parent.prompt_func is None:
@@ -130,12 +151,12 @@ class EtoolkitInstance:
130 151
131 @staticmethod 152 @staticmethod
132 def confirm_password_prompt( 153 def confirm_password_prompt(
133 password_hash: str = None, confirm: bool = True 154 password_hash: str = '', confirm: bool = True
134 ) -> str: 155 ) -> str:
135 """ 156 """
136 Prompts for master password and then for confirmation if `confirm` True 157 Prompts for master password and then for confirmation if `confirm` True
137 158
138 :param password_hash: Hash to compare with instead of confirm 159 :param password_hash: Hash to compare with instead of confirm (def. '')
139 :type password_hash: str 160 :type password_hash: str
140 161
141 :param confirm: Confirm the password (and see if there is a match) 162 :param confirm: Confirm the password (and see if there is a match)
@@ -158,8 +179,8 @@ class EtoolkitInstance:
158 print('The passwords are either empty or do not match') 179 print('The passwords are either empty or do not match')
159 continue 180 continue
160 return pass1.strip() 181 return pass1.strip()
161 except Exception as e: 182 except Exception as exp:
162 raise EtoolkitInstanceError('Prompt error') from e 183 raise EtoolkitInstanceError('Prompt error') from exp
163 184
164 @staticmethod 185 @staticmethod
165 def decrypt(password: str, edata: str) -> str: 186 def decrypt(password: str, edata: str) -> str:
@@ -206,14 +227,14 @@ class EtoolkitInstance:
206 data = data[2 : -int(data[:2].decode())] 227 data = data[2 : -int(data[:2].decode())]
207 228
208 return data.decode() 229 return data.decode()
209 except InvalidTag as e: 230 except InvalidTag as err:
210 raise EtoolkitInstanceError( 231 raise EtoolkitInstanceError(
211 f'Invalid tag when decrypting: {edata}' 232 f'Invalid tag when decrypting: {edata}'
212 ) from e 233 ) from err
213 except Exception as e: 234 except Exception as exp:
214 raise EtoolkitInstanceError( 235 raise EtoolkitInstanceError(
215 f'Error when decrypting: {edata}' 236 f'Error when decrypting: {edata}'
216 ) from e 237 ) from exp
217 238
218 @staticmethod 239 @staticmethod
219 def encrypt(password: str, data: str) -> str: 240 def encrypt(password: str, data: str) -> str:
@@ -269,6 +290,28 @@ class EtoolkitInstance:
269 ) 290 )
270 291
271 @staticmethod 292 @staticmethod
293 def get_global_macros() -> dict[str, str]:
294 """
295 Returns a dict for global macro mapping
296
297 Globals are the same for all instances
298
299 :return: macro: replacement value dict
300 :rtype: dict
301 """
302 macros = {'%h': os.path.expanduser('~'), '%u': getpass.getuser()}
303
304 # unpack defined environment variables
305 for key, value in os.environ.items():
306 if not isinstance(value, str):
307 # should not happen
308 continue
309
310 macros[f'%e{{{key}}}'] = value
311
312 return macros
313
314 @staticmethod
272 def get_new_password_hash(password: str) -> str: 315 def get_new_password_hash(password: str) -> str:
273 """ 316 """
274 Returns a complete password hash based on `password` 317 Returns a complete password hash based on `password`
@@ -295,23 +338,23 @@ class EtoolkitInstance:
295 ) 338 )
296 339
297 @staticmethod 340 @staticmethod
298 def parse_value(value: object, macros: dict) -> object: 341 def parse_value(value: str, macros: dict) -> str:
299 """ 342 """
300 Returns the value with all macros replaced by their values 343 Returns the value with all macros replaced by their values
301 344
302 If `value` is not of type 'str' simply return `value`
303
304 :param value: A simple value 345 :param value: A simple value
305 :type value: object 346 :type value: str
306 347
307 :param macros: Macros mapping 348 :param macros: Macros mapping
308 :type macros: dict 349 :type macros: dict
309 350
310 :return: New value with all macros replaced by their values 351 :return: New value with all macros replaced by their values
311 :rtype: object 352 :rtype: str
312 """ 353 """
313 if not isinstance(value, str): 354 if not isinstance(value, str):
314 return value 355 raise EtoolkitInstanceError(
356 "Environment variable value not of the type 'str' detected"
357 )
315 for key, val in macros.items(): 358 for key, val in macros.items():
316 value = value.replace(key, val) 359 value = value.replace(key, val)
317 return value 360 return value
@@ -332,7 +375,7 @@ class EtoolkitInstance:
332 :return: True if the password matches or password_hash is None, 375 :return: True if the password matches or password_hash is None,
333 :rtype: bool 376 :rtype: bool
334 """ 377 """
335 if password_hash is None: 378 if not password_hash:
336 return True 379 return True
337 # format: pbkdf2_hashalgo$ietarations$salt-base64$key-base64 380 # format: pbkdf2_hashalgo$ietarations$salt-base64$key-base64
338 try: 381 try:
@@ -401,11 +444,9 @@ class EtoolkitInstance:
401 if self._env is not None: 444 if self._env is not None:
402 return self._env 445 return self._env
403 446
404 macros = { 447 macros = self.get_global_macros()
405 '%h': os.path.expanduser('~'), 448 macros['%i'] = self.name
406 '%i': self.name, 449
407 '%u': getpass.getuser(),
408 }
409 new_env = {} 450 new_env = {}
410 for key, value in sorted( 451 for key, value in sorted(
411 self._raw_env_variables.items(), key=lambda x: x[0] 452 self._raw_env_variables.items(), key=lambda x: x[0]
@@ -413,14 +454,12 @@ class EtoolkitInstance:
413 if not value: 454 if not value:
414 # perhaps unset instead of skipping? 455 # perhaps unset instead of skipping?
415 continue 456 continue
416 if isinstance(value, str) and '%e' in value: 457 macros['%e'] = os.environ.get(key, '')
417 macros['%e'] = os.environ.get(key, '') 458 macros['%p'] = (
418 if isinstance(value, str) and '%p' in value: 459 self._parent.get_environ().get(key, '')
419 macros['%p'] = ( 460 if self._parent is not None
420 self._parent.get_environ().get(key, '') 461 else ''
421 if self._parent is not None 462 )
422 else ''
423 )
424 if isinstance(value, str) and value.startswith('enc-val$'): 463 if isinstance(value, str) and value.startswith('enc-val$'):
425 value = self._decrypt_value(value) 464 value = self._decrypt_value(value)
426 if key not in self._sensitive_env_variables: 465 if key not in self._sensitive_env_variables:
@@ -469,7 +508,7 @@ class EtoolkitInstance:
469 return self._parent.get_full_name(delimiter) + delimiter + self.name 508 return self._parent.get_full_name(delimiter) + delimiter + self.name
470 509
471 def get_reencrypted_instance_data( 510 def get_reencrypted_instance_data(
472 self, new_password: str, password: str = None 511 self, new_password: str, password: str | None = None
473 ) -> dict: 512 ) -> dict:
474 """ 513 """
475 Returns new instance data (dict) containing new encrypted values 514 Returns new instance data (dict) containing new encrypted values
@@ -493,16 +532,18 @@ class EtoolkitInstance:
493 if password is None: 532 if password is None:
494 password = self._master_password 533 password = self._master_password
495 534
496 if password is None and self._prompt_func is None: 535 if password is None:
497 password = os.environ.get('ETOOLKIT_MASTER_PASSWORD') 536 if self._prompt_func is None:
498 if password is None: 537 password = os.environ.get('ETOOLKIT_MASTER_PASSWORD')
499 raise EtoolkitInstanceError( 538 else:
500 'Neither password or prompt function set' 539 password = self._prompt_func(
540 self._master_password_hash, confirm=False
501 ) 541 )
502 542
503 if password is None: 543 if password is None:
504 password = self._prompt_func( 544 # the password is still not set
505 self._master_password_hash, confirm=False 545 raise EtoolkitInstanceError(
546 'Neither password or prompt function set'
506 ) 547 )
507 548
508 new_data = dict(self._instance_data) 549 new_data = dict(self._instance_data)
@@ -529,7 +570,10 @@ class EtoolkitInstance:
529 :return: Decrypted value 570 :return: Decrypted value
530 :rtype: str 571 :rtype: str
531 """ 572 """
532 if self._master_password is None: 573 if self._master_password is not None:
574 master_password = self._master_password
575 else:
576 # master password not set for this instance
533 if self._prompt_func is None: 577 if self._prompt_func is None:
534 if ( 578 if (
535 mp_from_env := os.environ.get('ETOOLKIT_MASTER_PASSWORD') 579 mp_from_env := os.environ.get('ETOOLKIT_MASTER_PASSWORD')
@@ -537,10 +581,13 @@ class EtoolkitInstance:
537 raise EtoolkitInstanceError( 581 raise EtoolkitInstanceError(
538 'Neither password or prompt function set' 582 'Neither password or prompt function set'
539 ) 583 )
540 self.master_password = mp_from_env 584 master_password = mp_from_env
541 else: 585 else:
542 # use master_password setter in order to propagate to parent 586 master_password = self._prompt_func(
543 self.master_password = self._prompt_func(
544 self._master_password_hash, confirm=False 587 self._master_password_hash, confirm=False
545 ) 588 )
546 return self.decrypt(self._master_password, evalue) 589
590 # use master_password setter in order to propagate to parent
591 self.master_password = master_password
592
593 return self.decrypt(master_password, evalue)
diff --git a/tests/conftest.py b/tests/conftest.py
index 6cc8b9f..2e8eaf1 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -16,14 +16,14 @@
16"""Common fixtures""" 16"""Common fixtures"""
17 17
18import json 18import json
19import pathlib
19 20
20import pytest 21import pytest
21 22
22 23
23@pytest.fixture 24@pytest.fixture
24def config_data(): 25def config_data() -> dict:
25 """config_data for testing""" 26 """config_data for testing"""
26
27 return { 27 return {
28 'general': { 28 'general': {
29 'MASTER_PASSWORD_HASH': ( 29 'MASTER_PASSWORD_HASH': (
@@ -60,18 +60,16 @@ def config_data():
60 60
61 61
62@pytest.fixture 62@pytest.fixture
63def config_file(tmp_path, config_data): 63def config_file(tmp_path: pathlib.Path, config_data: dict) -> str:
64 """Temporary config file for testing that includes config_data""" 64 """Temporary config file for testing that includes config_data"""
65
66 cf = tmp_path / 'etoolkit.json' 65 cf = tmp_path / 'etoolkit.json'
67 cf.write_text(json.dumps(config_data)) 66 cf.write_text(json.dumps(config_data))
68 return str(cf) 67 return str(cf)
69 68
70 69
71@pytest.fixture 70@pytest.fixture
72def long_encrypted_value(): 71def long_encrypted_value() -> str:
73 """enc. value corresponding to 'Nobody expects the Spanish inquisition'""" 72 """enc. value corresponding to 'Nobody expects the Spanish inquisition'"""
74
75 return ( 73 return (
76 'enc-val$2$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4mn80o=$' 74 'enc-val$2$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4mn80o=$'
77 'UWP5YeRsh5/2vZ2J1UOS+BJti73Kbp6C1pJmCo8hF' 75 'UWP5YeRsh5/2vZ2J1UOS+BJti73Kbp6C1pJmCo8hF'
@@ -80,27 +78,26 @@ def long_encrypted_value():
80 78
81 79
82@pytest.fixture 80@pytest.fixture
83def long_value(): 81def long_value() -> str:
84 """standard value (> 32 bytes)""" 82 """standard value (> 32 bytes)"""
85 return 'Nobody expects the Spanish inquisition' 83 return 'Nobody expects the Spanish inquisition'
86 84
87 85
88@pytest.fixture 86@pytest.fixture
89def master_password(): 87def master_password() -> str:
90 """Master passord""" 88 """Master passord"""
91 return 'The very secret passwd' 89 return 'The very secret passwd'
92 90
93 91
94@pytest.fixture 92@pytest.fixture
95def new_master_password(): 93def new_master_password() -> str:
96 """Master passord""" 94 """Master passord"""
97 return 'New very secret passwd' 95 return 'New very secret passwd'
98 96
99 97
100@pytest.fixture 98@pytest.fixture
101def non_random_bytes_32(): 99def non_random_bytes_32() -> bytes:
102 """always use the same bytes instead of os.urandom(32)""" 100 """always use the same bytes instead of os.urandom(32)"""
103
104 return ( 101 return (
105 b'\xb9\x8aY3U_\x00j\xb4\x086K\xd9\xdb\x88N' 102 b'\xb9\x8aY3U_\x00j\xb4\x086K\xd9\xdb\x88N'
106 b'\xcd;\xe8$#\xfa\x12\x05\x12\x0c~\x17\x89\xa7\xf3J' 103 b'\xcd;\xe8$#\xfa\x12\x05\x12\x0c~\x17\x89\xa7\xf3J'
@@ -108,9 +105,8 @@ def non_random_bytes_32():
108 105
109 106
110@pytest.fixture 107@pytest.fixture
111def non_random_bytes_57(): 108def non_random_bytes_57() -> bytes:
112 """always use the same bytes instead of os.urandom(57)""" 109 """always use the same bytes instead of os.urandom(57)"""
113
114 return ( 110 return (
115 b'D$\x99\xaa\xafiZ\xb4C\xa0%XTz)\xca\xedK\xcd\xa2F~\xff+\xa1[\xe2\xaa' 111 b'D$\x99\xaa\xafiZ\xb4C\xa0%XTz)\xca\xedK\xcd\xa2F~\xff+\xa1[\xe2\xaa'
116 b'\xb2\xd3\x07\x13\xedb\xc2\x84\xfe\tS\r\xf0\x02_\xef\xe3\xde\xf1?e' 112 b'\xb2\xd3\x07\x13\xedb\xc2\x84\xfe\tS\r\xf0\x02_\xef\xe3\xde\xf1?e'
@@ -119,16 +115,14 @@ def non_random_bytes_57():
119 115
120 116
121@pytest.fixture 117@pytest.fixture
122def nonexistent_config_file(tmp_path): 118def nonexistent_config_file(tmp_path: pathlib.Path) -> str:
123 """temporary config file for testing that includes config_data""" 119 """temporary config file for testing that includes config_data"""
124
125 return str(tmp_path / 'etoolkitt.json') 120 return str(tmp_path / 'etoolkitt.json')
126 121
127 122
128@pytest.fixture 123@pytest.fixture
129def password_hash(): 124def password_hash() -> str:
130 """password hash for testing, corresponding to 'The very secret passwd'""" 125 """password hash for testing, corresponding to 'The very secret passwd'"""
131
132 return ( 126 return (
133 'pbkdf2_sha256$500000$UY3o78KUM1Btzxk3k3JCsijnwtJ2lx+hH9NewpVKxo8=$' 127 'pbkdf2_sha256$500000$UY3o78KUM1Btzxk3k3JCsijnwtJ2lx+hH9NewpVKxo8=$'
134 'tHwDm8OVKanC4DoYTigTCb0R3lQIa/CbBYj0B3TZtHg=' 128 'tHwDm8OVKanC4DoYTigTCb0R3lQIa/CbBYj0B3TZtHg='
@@ -136,9 +130,8 @@ def password_hash():
136 130
137 131
138@pytest.fixture 132@pytest.fixture
139def short_encrypted_value(): 133def short_encrypted_value() -> str:
140 """enc. value corresponding to 'secret1'""" 134 """enc. value corresponding to 'secret1'"""
141
142 return ( 135 return (
143 'enc-val$2$RCSZqq9pWrRDoCVYVHopyu1LzaJGfv8roVviqrLTBxM=$' 136 'enc-val$2$RCSZqq9pWrRDoCVYVHopyu1LzaJGfv8roVviqrLTBxM=$'
144 '+YYrZbwTBuG0Pl+WMQrvxLUtq5j8qYuQqzoIwgoGt7AaWZCJz+E7qoDeg3wke70ST8U=' 137 '+YYrZbwTBuG0Pl+WMQrvxLUtq5j8qYuQqzoIwgoGt7AaWZCJz+E7qoDeg3wke70ST8U='
@@ -146,9 +139,8 @@ def short_encrypted_value():
146 139
147 140
148@pytest.fixture 141@pytest.fixture
149def short_encrypted_value_v1(): 142def short_encrypted_value_v1() -> str:
150 """enc. value (enc-val 1) corresponding to 'secret1'""" 143 """enc. value (enc-val 1) corresponding to 'secret1'"""
151
152 return ( 144 return (
153 'enc-val$1$/cXpEMoZrTlb9yokGhw8tLTSUkqnqJ4ZoAkurNgMYx' 145 'enc-val$1$/cXpEMoZrTlb9yokGhw8tLTSUkqnqJ4ZoAkurNgMYx'
154 'w=$1VdkSMcZnLRwLiu1M8VlYcbelwmiVNY=' 146 'w=$1VdkSMcZnLRwLiu1M8VlYcbelwmiVNY='
@@ -156,12 +148,12 @@ def short_encrypted_value_v1():
156 148
157 149
158@pytest.fixture 150@pytest.fixture
159def short_value(): 151def short_value() -> str:
160 """standard value (< 32 bytes)""" 152 """standard value (< 32 bytes)"""
161 return 'secret1' 153 return 'secret1'
162 154
163 155
164@pytest.fixture 156@pytest.fixture
165def wrong_master_password(): 157def wrong_master_password() -> str:
166 """Wrong master passord""" 158 """Wrong master passord"""
167 return 'the very secret passwd' 159 return 'the very secret passwd'
diff --git a/tests/test_cli.py b/tests/test_cli.py
index b7913da..6e28378 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -26,9 +26,13 @@ from etoolkit.__main__ import main
26 26
27 27
28@unittest.mock.patch('builtins.input') 28@unittest.mock.patch('builtins.input')
29def test_decrypt_v1(binput, capsys, config_file, master_password): 29def test_decrypt_v1(
30 binput: unittest.mock.MagicMock,
31 capsys: pytest.CaptureFixture,
32 config_file: str,
33 master_password: str,
34) -> None:
30 """Tests v1 decryption via the CLI interface""" 35 """Tests v1 decryption via the CLI interface"""
31
32 binput.return_value = ( 36 binput.return_value = (
33 'enc-val$1$rye0sMGEnd35gOWyISE1FQa6dzS+8/jf6aopMO5tPr4=$' 37 'enc-val$1$rye0sMGEnd35gOWyISE1FQa6dzS+8/jf6aopMO5tPr4=$'
34 'RjnRY0bUJWFOiejTlM3OhKNimQ==' 38 'RjnRY0bUJWFOiejTlM3OhKNimQ=='
@@ -44,15 +48,14 @@ def test_decrypt_v1(binput, capsys, config_file, master_password):
44 48
45@unittest.mock.patch('builtins.input') 49@unittest.mock.patch('builtins.input')
46def test_decrypt_v2( 50def test_decrypt_v2(
47 binput, 51 binput: unittest.mock.MagicMock,
48 capsys, 52 capsys: pytest.CaptureFixture,
49 config_file, 53 config_file: str,
50 master_password, 54 master_password: str,
51 short_encrypted_value, 55 short_encrypted_value: str,
52 short_value, 56 short_value: str,
53): 57) -> None:
54 """Tests v2 decryption via the CLI interface""" 58 """Tests v2 decryption via the CLI interface"""
55
56 binput.return_value = short_encrypted_value 59 binput.return_value = short_encrypted_value
57 with unittest.mock.patch.dict( 60 with unittest.mock.patch.dict(
58 os.environ, {'ETOOLKIT_MASTER_PASSWORD': master_password} 61 os.environ, {'ETOOLKIT_MASTER_PASSWORD': master_password}
@@ -68,17 +71,16 @@ def test_decrypt_v2(
68@unittest.mock.patch('os.urandom') 71@unittest.mock.patch('os.urandom')
69@unittest.mock.patch('builtins.input') 72@unittest.mock.patch('builtins.input')
70def test_encrypt_with_echo( 73def test_encrypt_with_echo(
71 binput, 74 binput: unittest.mock.MagicMock,
72 urandom, 75 urandom: unittest.mock.MagicMock,
73 capsys, 76 capsys: pytest.CaptureFixture,
74 non_random_bytes_57, 77 non_random_bytes_57: bytes,
75 config_file, 78 config_file: str,
76 master_password, 79 master_password: str,
77 short_encrypted_value, 80 short_encrypted_value: str,
78 short_value, 81 short_value: str,
79): 82) -> None:
80 """Tests encryption via the CLI interface""" 83 """Tests encryption via the CLI interface"""
81
82 binput.return_value = short_value 84 binput.return_value = short_value
83 urandom.return_value = non_random_bytes_57 85 urandom.return_value = non_random_bytes_57
84 with unittest.mock.patch.dict( 86 with unittest.mock.patch.dict(
@@ -95,17 +97,16 @@ def test_encrypt_with_echo(
95@unittest.mock.patch('os.urandom') 97@unittest.mock.patch('os.urandom')
96@unittest.mock.patch('getpass.getpass') 98@unittest.mock.patch('getpass.getpass')
97def test_encrypt_without_echo( 99def test_encrypt_without_echo(
98 getpass, 100 getpass: unittest.mock.MagicMock,
99 urandom, 101 urandom: unittest.mock.MagicMock,
100 capsys, 102 capsys: pytest.CaptureFixture,
101 non_random_bytes_57, 103 non_random_bytes_57: bytes,
102 config_file, 104 config_file: str,
103 master_password, 105 master_password: str,
104 short_encrypted_value, 106 short_encrypted_value: str,
105 short_value, 107 short_value: str,
106): 108) -> None:
107 """Tests encryption via the CLI interface""" 109 """Tests encryption via the CLI interface"""
108
109 getpass.return_value = short_value 110 getpass.return_value = short_value
110 urandom.return_value = non_random_bytes_57 111 urandom.return_value = non_random_bytes_57
111 with unittest.mock.patch.dict( 112 with unittest.mock.patch.dict(
@@ -119,20 +120,26 @@ def test_encrypt_without_echo(
119 ) 120 )
120 121
121 122
122def test_fetch_encrypted_value(config_file, master_password, short_value): 123def test_fetch_encrypted_value(
124 config_file: str, master_password: str, short_value: str
125) -> None:
123 """Tests decryption of encrypted value""" 126 """Tests decryption of encrypted value"""
124
125 with unittest.mock.patch.dict( 127 with unittest.mock.patch.dict(
126 os.environ, {'ETOOLKIT_MASTER_PASSWORD': master_password} 128 os.environ, {'ETOOLKIT_MASTER_PASSWORD': master_password}
127 ): 129 ):
128 assert os.environ.get('ETOOLKIT_TEST_PASSWORD') is None 130 assert os.environ.get('ETOOLKIT_TEST_PASSWORD') is None
129 main(['-c', f'{config_file}', '-q', '-s', '/bin/false', 'secret']) 131 with pytest.raises(SystemExit) as exit_info:
132 main(['-c', f'{config_file}', '-q', '-s', '/bin/false', 'secret'])
133 assert exit_info.value.code == 0
130 assert os.environ.get('ETOOLKIT_TEST_PASSWORD') == short_value 134 assert os.environ.get('ETOOLKIT_TEST_PASSWORD') == short_value
131 135
132 136
133def test_list(capsys, config_file, nonexistent_config_file): 137def test_list(
138 capsys: pytest.CaptureFixture,
139 config_file: str,
140 nonexistent_config_file: str,
141) -> None:
134 """Tests list via the CLI interface""" 142 """Tests list via the CLI interface"""
135
136 with pytest.raises(SystemExit) as exit_info: 143 with pytest.raises(SystemExit) as exit_info:
137 main(['-c', nonexistent_config_file, '-l']) 144 main(['-c', nonexistent_config_file, '-l'])
138 assert exit_info.value.code == errno.EIO 145 assert exit_info.value.code == errno.EIO
@@ -142,9 +149,8 @@ def test_list(capsys, config_file, nonexistent_config_file):
142 assert capsys.readouterr().out.strip() == f'dev{os.linesep}secret' 149 assert capsys.readouterr().out.strip() == f'dev{os.linesep}secret'
143 150
144 151
145def test_help(capsys): 152def test_help(capsys: pytest.CaptureFixture) -> None:
146 """Dummy test checking if the CLI is available at all""" 153 """Dummy test checking if the CLI is available at all"""
147
148 with pytest.raises(SystemExit) as exit_info: 154 with pytest.raises(SystemExit) as exit_info:
149 main(['-h']) 155 main(['-h'])
150 assert exit_info.value.code == 0 156 assert exit_info.value.code == 0
@@ -154,10 +160,12 @@ def test_help(capsys):
154@unittest.mock.patch('os.urandom') 160@unittest.mock.patch('os.urandom')
155@unittest.mock.patch('getpass.getpass') 161@unittest.mock.patch('getpass.getpass')
156def test_generate_master_password_hash( 162def test_generate_master_password_hash(
157 getpass, urandom, capsys, non_random_bytes_32 163 getpass: unittest.mock.MagicMock,
158): 164 urandom: unittest.mock.MagicMock,
165 capsys: pytest.CaptureFixture,
166 non_random_bytes_32: bytes,
167) -> None:
159 """Tests master password hash generation via the CLI interface""" 168 """Tests master password hash generation via the CLI interface"""
160
161 getpass.return_value = 'The very secret passwd' 169 getpass.return_value = 'The very secret passwd'
162 urandom.return_value = non_random_bytes_32 170 urandom.return_value = non_random_bytes_32
163 with pytest.raises(SystemExit) as exit_info: 171 with pytest.raises(SystemExit) as exit_info:
@@ -169,9 +177,8 @@ def test_generate_master_password_hash(
169 ) 177 )
170 178
171 179
172def test_version(capsys): 180def test_version(capsys: pytest.CaptureFixture) -> None:
173 """Dummy test checking if the CLI is available at all""" 181 """Dummy test checking if the CLI is available at all"""
174
175 with pytest.raises(SystemExit) as exit_info: 182 with pytest.raises(SystemExit) as exit_info:
176 main(['-v']) 183 main(['-v'])
177 assert exit_info.value.code == 0 184 assert exit_info.value.code == 0
diff --git a/tests/test_envtoolkit_instance.py b/tests/test_envtoolkit_instance.py
index 5137e9f..3235736 100644
--- a/tests/test_envtoolkit_instance.py
+++ b/tests/test_envtoolkit_instance.py
@@ -20,9 +20,8 @@ import pytest
20import etoolkit 20import etoolkit
21 21
22 22
23def test_instantiation(config_data): 23def test_instantiation(config_data: dict) -> None:
24 """Tests for object instatiation""" 24 """Tests for object instatiation"""
25
26 with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info: 25 with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info:
27 instance = etoolkit.EtoolkitInstance('devv', config_data) 26 instance = etoolkit.EtoolkitInstance('devv', config_data)
28 assert exc_info.type is etoolkit.EtoolkitInstanceError 27 assert exc_info.type is etoolkit.EtoolkitInstanceError
@@ -43,10 +42,12 @@ def test_instantiation(config_data):
43 42
44 43
45def test_get_environ( 44def test_get_environ(
46 config_data, master_password, short_value, wrong_master_password 45 config_data: dict,
47): 46 master_password: str,
47 short_value: str,
48 wrong_master_password: str,
49) -> None:
48 """Tests the EtoolkitInstance.get_environ method""" 50 """Tests the EtoolkitInstance.get_environ method"""
49
50 instance = etoolkit.EtoolkitInstance('secret', config_data) 51 instance = etoolkit.EtoolkitInstance('secret', config_data)
51 with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info: 52 with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info:
52 env = instance.get_environ() 53 env = instance.get_environ()
@@ -68,16 +69,14 @@ def test_get_environ(
68 assert 'ETOOLKIT_TEST_PASSWORD2' in instance.sensitive_env_variables 69 assert 'ETOOLKIT_TEST_PASSWORD2' in instance.sensitive_env_variables
69 70
70 71
71def test_get_full_name(config_data): 72def test_get_full_name(config_data: dict) -> None:
72 """Tests the EtoolkitInstance.get_full_name method""" 73 """Tests the EtoolkitInstance.get_full_name method"""
73
74 instance = etoolkit.EtoolkitInstance('secret', config_data) 74 instance = etoolkit.EtoolkitInstance('secret', config_data)
75 assert instance.get_full_name('->') == '_default->secret' 75 assert instance.get_full_name('->') == '_default->secret'
76 76
77 77
78def test_parent_vars(config_data): 78def test_parent_vars(config_data: dict) -> None:
79 """Tests the EtoolkitInstance.get_full_name method""" 79 """Tests the EtoolkitInstance.get_full_name method"""
80
81 instance = etoolkit.EtoolkitInstance('dev', config_data) 80 instance = etoolkit.EtoolkitInstance('dev', config_data)
82 assert instance.get_full_name('->') == '_default->dev' 81 assert instance.get_full_name('->') == '_default->dev'
83 assert instance.get_environ()['ETOOLKIT_TEST_PYTHONPATH'] == ( 82 assert instance.get_environ()['ETOOLKIT_TEST_PYTHONPATH'] == (
diff --git a/tests/test_envtoolkit_instance_static.py b/tests/test_envtoolkit_instance_static.py
index e13d439..8c21a0f 100644
--- a/tests/test_envtoolkit_instance_static.py
+++ b/tests/test_envtoolkit_instance_static.py
@@ -23,9 +23,10 @@ import etoolkit
23 23
24 24
25@unittest.mock.patch('getpass.getpass') 25@unittest.mock.patch('getpass.getpass')
26def test_confirm_password_prompt(getpass, password_hash, master_password): 26def test_confirm_password_prompt(
27 getpass: unittest.mock.MagicMock, password_hash: str, master_password: str
28) -> None:
27 """Tests the static EtoolkitInstance.confirm_password_prompt method""" 29 """Tests the static EtoolkitInstance.confirm_password_prompt method"""
28
29 getpass.return_value = master_password 30 getpass.return_value = master_password
30 assert ( 31 assert (
31 etoolkit.EtoolkitInstance.confirm_password_prompt(password_hash) 32 etoolkit.EtoolkitInstance.confirm_password_prompt(password_hash)
@@ -37,9 +38,10 @@ def test_confirm_password_prompt(getpass, password_hash, master_password):
37 ) 38 )
38 39
39 40
40def test_decrypt_v1(master_password, short_encrypted_value_v1, short_value): 41def test_decrypt_v1(
42 master_password: str, short_encrypted_value_v1: str, short_value: str
43) -> None:
41 """Tests the static EtoolkitInstance.decrypt method""" 44 """Tests the static EtoolkitInstance.decrypt method"""
42
43 assert ( 45 assert (
44 etoolkit.EtoolkitInstance.decrypt( 46 etoolkit.EtoolkitInstance.decrypt(
45 master_password, short_encrypted_value_v1 47 master_password, short_encrypted_value_v1
@@ -59,10 +61,9 @@ def test_decrypt_v1(master_password, short_encrypted_value_v1, short_value):
59 61
60 62
61def test_decrypt_v2_no_padding( 63def test_decrypt_v2_no_padding(
62 master_password, long_encrypted_value, long_value 64 master_password: str, long_encrypted_value: str, long_value: str
63): 65) -> None:
64 """Tests the static EtoolkitInstance.decrypt method for v2 - no padding""" 66 """Tests the static EtoolkitInstance.decrypt method for v2 - no padding"""
65
66 assert ( 67 assert (
67 etoolkit.EtoolkitInstance.decrypt( 68 etoolkit.EtoolkitInstance.decrypt(
68 master_password, long_encrypted_value 69 master_password, long_encrypted_value
@@ -80,10 +81,9 @@ def test_decrypt_v2_no_padding(
80 81
81 82
82def test_decrypt_v2_with_padding( 83def test_decrypt_v2_with_padding(
83 master_password, short_encrypted_value, short_value 84 master_password: str, short_encrypted_value: str, short_value: str
84): 85) -> None:
85 """Tests the static EtoolkitInstance.decrypt method for v2 with padding""" 86 """Tests the static EtoolkitInstance.decrypt method for v2 with padding"""
86
87 assert ( 87 assert (
88 etoolkit.EtoolkitInstance.decrypt( 88 etoolkit.EtoolkitInstance.decrypt(
89 master_password, short_encrypted_value 89 master_password, short_encrypted_value
@@ -99,9 +99,8 @@ def test_decrypt_v2_with_padding(
99 assert exc_info.value.args[0] == f'Invalid tag when decrypting: {edata}' 99 assert exc_info.value.args[0] == f'Invalid tag when decrypting: {edata}'
100 100
101 101
102def test_encrypt_no_padding(master_password, long_value): 102def test_encrypt_no_padding(master_password: str, long_value: str) -> None:
103 """Tests the static EtoolkitInstance.encrypt method with a long string""" 103 """Tests the static EtoolkitInstance.encrypt method with a long string"""
104
105 edata = etoolkit.EtoolkitInstance.encrypt(master_password, long_value) 104 edata = etoolkit.EtoolkitInstance.encrypt(master_password, long_value)
106 assert edata.startswith('enc-val$2$') 105 assert edata.startswith('enc-val$2$')
107 assert len(edata) == 131 106 assert len(edata) == 131
@@ -111,9 +110,8 @@ def test_encrypt_no_padding(master_password, long_value):
111 ) 110 )
112 111
113 112
114def test_encrypt_with_padding(master_password, short_value): 113def test_encrypt_with_padding(master_password: str, short_value: str) -> None:
115 """Tests the static EtoolkitInstance.encrypt method with a short string""" 114 """Tests the static EtoolkitInstance.encrypt method with a short string"""
116
117 edata = etoolkit.EtoolkitInstance.encrypt(master_password, short_value) 115 edata = etoolkit.EtoolkitInstance.encrypt(master_password, short_value)
118 assert edata.startswith('enc-val$2$') 116 assert edata.startswith('enc-val$2$')
119 assert len(edata) == 123 117 assert len(edata) == 123
@@ -125,14 +123,13 @@ def test_encrypt_with_padding(master_password, short_value):
125 123
126@unittest.mock.patch('os.urandom') 124@unittest.mock.patch('os.urandom')
127def test_encrypt_staticly_no_padding( 125def test_encrypt_staticly_no_padding(
128 urandom, 126 urandom: unittest.mock.MagicMock,
129 master_password, 127 master_password: str,
130 non_random_bytes_32, 128 non_random_bytes_32: bytes,
131 long_encrypted_value, 129 long_encrypted_value: str,
132 long_value, 130 long_value: str,
133): 131) -> None:
134 """Tests the EtoolkitInstance.encrypt method always with the same salt""" 132 """Tests the EtoolkitInstance.encrypt method always with the same salt"""
135
136 urandom.return_value = non_random_bytes_32 133 urandom.return_value = non_random_bytes_32
137 edata = etoolkit.EtoolkitInstance.encrypt(master_password, long_value) 134 edata = etoolkit.EtoolkitInstance.encrypt(master_password, long_value)
138 assert edata == long_encrypted_value 135 assert edata == long_encrypted_value
@@ -144,14 +141,13 @@ def test_encrypt_staticly_no_padding(
144 141
145@unittest.mock.patch('os.urandom') 142@unittest.mock.patch('os.urandom')
146def test_encrypt_staticly_with_padding( 143def test_encrypt_staticly_with_padding(
147 urandom, 144 urandom: unittest.mock.MagicMock,
148 master_password, 145 master_password: str,
149 non_random_bytes_57, 146 non_random_bytes_57: bytes,
150 short_encrypted_value, 147 short_encrypted_value: str,
151 short_value, 148 short_value: str,
152): 149) -> None:
153 """Tests the EtoolkitInstance.encrypt method always with the same salt""" 150 """Tests the EtoolkitInstance.encrypt method always with the same salt"""
154
155 urandom.return_value = non_random_bytes_57 151 urandom.return_value = non_random_bytes_57
156 edata = etoolkit.EtoolkitInstance.encrypt(master_password, short_value) 152 edata = etoolkit.EtoolkitInstance.encrypt(master_password, short_value)
157 assert edata == short_encrypted_value 153 assert edata == short_encrypted_value
@@ -160,9 +156,8 @@ def test_encrypt_staticly_with_padding(
160 ) 156 )
161 157
162 158
163def test_get_new_password_hash(master_password): 159def test_get_new_password_hash(master_password: str) -> None:
164 """Tests the static EtoolkitInstance.get_new_password_hash method""" 160 """Tests the static EtoolkitInstance.get_new_password_hash method"""
165
166 new_hash = etoolkit.EtoolkitInstance.get_new_password_hash(master_password) 161 new_hash = etoolkit.EtoolkitInstance.get_new_password_hash(master_password)
167 # all pbkdf2 params are the same / hardcoded for the time being 162 # all pbkdf2 params are the same / hardcoded for the time being
168 assert new_hash.startswith('pbkdf2_sha256$500000$') 163 assert new_hash.startswith('pbkdf2_sha256$500000$')
@@ -173,9 +168,8 @@ def test_get_new_password_hash(master_password):
173 ) 168 )
174 169
175 170
176def test_parse_value(): 171def test_parse_value() -> None:
177 """Tests the static EtoolkitInstance.parse_value method""" 172 """Tests the static EtoolkitInstance.parse_value method"""
178
179 assert ( 173 assert (
180 etoolkit.EtoolkitInstance.parse_value('t%bs%t', {'%b': 'e', '%t': 't'}) 174 etoolkit.EtoolkitInstance.parse_value('t%bs%t', {'%b': 'e', '%t': 't'})
181 == 'test' 175 == 'test'
@@ -183,10 +177,9 @@ def test_parse_value():
183 177
184 178
185def test_password_matches( 179def test_password_matches(
186 password_hash, master_password, wrong_master_password 180 password_hash: str, master_password: str, wrong_master_password: str
187): 181) -> None:
188 """Tests the static EtoolkitInstance.password_matches method""" 182 """Tests the static EtoolkitInstance.password_matches method"""
189
190 assert etoolkit.EtoolkitInstance.password_matches( 183 assert etoolkit.EtoolkitInstance.password_matches(
191 master_password, password_hash 184 master_password, password_hash
192 ) 185 )
@@ -197,16 +190,15 @@ def test_password_matches(
197 190
198@unittest.mock.patch('os.urandom') 191@unittest.mock.patch('os.urandom')
199def test_reencrypt_staticly_with_padding( 192def test_reencrypt_staticly_with_padding(
200 urandom, 193 urandom: unittest.mock.MagicMock,
201 master_password, 194 master_password: str,
202 new_master_password, 195 new_master_password: str,
203 non_random_bytes_57, 196 non_random_bytes_57: bytes,
204 short_encrypted_value, 197 short_encrypted_value: str,
205 short_encrypted_value_v1, 198 short_encrypted_value_v1: str,
206 short_value, 199 short_value: str,
207): 200) -> None:
208 """Tests the EtoolkitInstance.reencrypt method always with the same salt""" 201 """Tests the EtoolkitInstance.reencrypt method always with the same salt"""
209
210 urandom.return_value = non_random_bytes_57 202 urandom.return_value = non_random_bytes_57
211 # reencrypt (migrate) v1 to current using the same password 203 # reencrypt (migrate) v1 to current using the same password
212 edata = etoolkit.EtoolkitInstance.reencrypt( 204 edata = etoolkit.EtoolkitInstance.reencrypt(