diff options
| author | Simeon Simeonov | 2024-04-30 11:39:20 +0200 |
|---|---|---|
| committer | Simeon Simeonov | 2024-04-30 11:39:20 +0200 |
| commit | c31fa58c85e866b3a5ab04882c7aff655f0b5477 (patch) | |
| tree | 84532e1b6ce24c07267d4a8d8e8f810ba657500c | |
| parent | 8ca7a0d9666ede2d9d270e2a469e4b8dda492e69 (diff) | |
Implement etoolkit encryption protocol v2
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | pyproject.toml | 3 | ||||
| -rw-r--r-- | setup.cfg | 5 | ||||
| -rw-r--r-- | src/etoolkit/__init__.py | 5 | ||||
| -rw-r--r-- | src/etoolkit/__main__.py | 23 | ||||
| -rw-r--r--[-rwxr-xr-x] | src/etoolkit/etoolkit.py | 80 | ||||
| -rw-r--r-- | tests/conftest.py | 35 | ||||
| -rw-r--r-- | tests/test_cli.py | 60 | ||||
| -rw-r--r-- | tests/test_envtoolkit_instance.py | 8 | ||||
| -rw-r--r-- | tests/test_envtoolkit_instance_static.py | 114 |
10 files changed, 248 insertions, 89 deletions
| @@ -21,7 +21,7 @@ goals were set: | |||
| 21 | 21 | ||
| 22 | ## Requirements | 22 | ## Requirements |
| 23 | 23 | ||
| 24 | Apart from Python >= 3.7, the only requirement is | 24 | Apart from Python >= 3.8, the only requirement is |
| 25 | [cryptography](https://pypi.org/project/cryptography/) | 25 | [cryptography](https://pypi.org/project/cryptography/) |
| 26 | 26 | ||
| 27 | 27 | ||
| @@ -317,7 +317,7 @@ Simeon Simeonov - sgs @ LiberaChat | |||
| 317 | 317 | ||
| 318 | ## [License](https://github.com/blackm0re/etoolkit/blob/master/LICENSE) | 318 | ## [License](https://github.com/blackm0re/etoolkit/blob/master/LICENSE) |
| 319 | 319 | ||
| 320 | Copyright (C) 2021-2022 Simeon Simeonov | 320 | Copyright (C) 2021-2024 Simeon Simeonov |
| 321 | All rights reserved. | 321 | All rights reserved. |
| 322 | 322 | ||
| 323 | [Licensed](https://github.com/blackm0re/etoolkit/blob/master/LICENSE) under the | 323 | [Licensed](https://github.com/blackm0re/etoolkit/blob/master/LICENSE) under the |
diff --git a/pyproject.toml b/pyproject.toml index 1173069..86a1527 100644 --- a/pyproject.toml +++ b/pyproject.toml | |||
| @@ -13,3 +13,6 @@ addopts = "-s" | |||
| 13 | testpaths = [ | 13 | testpaths = [ |
| 14 | "tests" | 14 | "tests" |
| 15 | ] | 15 | ] |
| 16 | pythonpath = [ | ||
| 17 | "src" | ||
| 18 | ] | ||
| @@ -15,11 +15,12 @@ classifiers = | |||
| 15 | Intended Audience :: System Administrators | 15 | Intended Audience :: System Administrators |
| 16 | License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) | 16 | License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) |
| 17 | Programming Language :: Python :: 3 | 17 | Programming Language :: Python :: 3 |
| 18 | Programming Language :: Python :: 3.7 | ||
| 19 | Programming Language :: Python :: 3.8 | 18 | Programming Language :: Python :: 3.8 |
| 20 | Programming Language :: Python :: 3.9 | 19 | Programming Language :: Python :: 3.9 |
| 21 | Programming Language :: Python :: 3.10 | 20 | Programming Language :: Python :: 3.10 |
| 22 | Programming Language :: Python :: 3.11 | 21 | Programming Language :: Python :: 3.11 |
| 22 | Programming Language :: Python :: 3.12 | ||
| 23 | Programming Language :: Python :: 3.13 | ||
| 23 | Operating System :: POSIX | 24 | Operating System :: POSIX |
| 24 | Topic :: Security :: Cryptography | 25 | Topic :: Security :: Cryptography |
| 25 | 26 | ||
| @@ -31,7 +32,7 @@ project_urls = | |||
| 31 | package_dir = | 32 | package_dir = |
| 32 | = src | 33 | = src |
| 33 | packages = find: | 34 | packages = find: |
| 34 | python_requires = >=3.7 | 35 | python_requires = >=3.8 |
| 35 | 36 | ||
| 36 | install_requires = | 37 | install_requires = |
| 37 | cryptography>=3.2 | 38 | cryptography>=3.2 |
diff --git a/src/etoolkit/__init__.py b/src/etoolkit/__init__.py index b49bbcf..0ef5957 100644 --- a/src/etoolkit/__init__.py +++ b/src/etoolkit/__init__.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | # etoolkit | 1 | # etoolkit |
| 2 | # Copyright (C) 2021-2022 Simeon Simeonov | 2 | # Copyright (C) 2021-2024 Simeon Simeonov |
| 3 | 3 | ||
| 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU General Public License as published by | 5 | # it under the terms of the GNU General Public License as published by |
| @@ -14,10 +14,11 @@ | |||
| 14 | # You should have received a copy of the GNU General Public License | 14 | # You should have received a copy of the GNU General Public License |
| 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | """A simple toolkit for setting environment variables in a flexible way""" | 16 | """A simple toolkit for setting environment variables in a flexible way""" |
| 17 | |||
| 17 | from .etoolkit import EtoolkitInstance, EtoolkitInstanceError | 18 | from .etoolkit import EtoolkitInstance, EtoolkitInstanceError |
| 18 | 19 | ||
| 19 | __author__ = 'Simeon Simeonov' | 20 | __author__ = 'Simeon Simeonov' |
| 20 | __version__ = '1.2.0' | 21 | __version__ = '1.3.0' |
| 21 | __license__ = 'GPL3' | 22 | __license__ = 'GPL3' |
| 22 | 23 | ||
| 23 | 24 | ||
diff --git a/src/etoolkit/__main__.py b/src/etoolkit/__main__.py index a200453..fba9dc7 100644 --- a/src/etoolkit/__main__.py +++ b/src/etoolkit/__main__.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | # etoolkit | 1 | # etoolkit |
| 2 | # Copyright (C) 2021-2022 Simeon Simeonov | 2 | # Copyright (C) 2021-2024 Simeon Simeonov |
| 3 | 3 | ||
| 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU General Public License as published by | 5 | # it under the terms of the GNU General Public License as published by |
| @@ -16,11 +16,14 @@ | |||
| 16 | """ | 16 | """ |
| 17 | CLI entry point for the etoolkit package | 17 | CLI entry point for the etoolkit package |
| 18 | 18 | ||
| 19 | Examples: | 19 | Examples |
| 20 | -------- | ||
| 20 | python -m etoolkit -h | 21 | python -m etoolkit -h |
| 21 | 22 | ||
| 22 | python -m etoolkit -p | 23 | python -m etoolkit -p |
| 24 | |||
| 23 | """ | 25 | """ |
| 26 | |||
| 24 | import argparse | 27 | import argparse |
| 25 | import errno | 28 | import errno |
| 26 | import getpass | 29 | import getpass |
| @@ -33,7 +36,7 @@ import sys | |||
| 33 | 36 | ||
| 34 | import etoolkit | 37 | import etoolkit |
| 35 | 38 | ||
| 36 | DEFAULT_LOG_FORMAT = "%(levelname)s: %(message)s" | 39 | DEFAULT_LOG_FORMAT = '%(levelname)s: %(message)s' |
| 37 | DEFAULT_LOG_LEVEL = logging.WARNING | 40 | DEFAULT_LOG_LEVEL = logging.WARNING |
| 38 | 41 | ||
| 39 | logger = logging.getLogger(__name__) | 42 | logger = logging.getLogger(__name__) |
| @@ -269,7 +272,7 @@ def main(inargs=None): | |||
| 269 | ) | 272 | ) |
| 270 | args = parser.parse_args(inargs) | 273 | args = parser.parse_args(inargs) |
| 271 | try: | 274 | try: |
| 272 | with io.open(args.config_file, 'r', encoding='utf-8') as fp: | 275 | with io.open(args.config_file, encoding='utf-8') as fp: |
| 273 | config_dict = json.load(fp) | 276 | config_dict = json.load(fp) |
| 274 | except FileNotFoundError as e: | 277 | except FileNotFoundError as e: |
| 275 | # do not raise exception if config-file is missing for: | 278 | # do not raise exception if config-file is missing for: |
| @@ -278,16 +281,16 @@ def main(inargs=None): | |||
| 278 | # - password hash generation | 281 | # - password hash generation |
| 279 | if args.password_hash or args.decrypt_value or args.encrypt_value: | 282 | if args.password_hash or args.decrypt_value or args.encrypt_value: |
| 280 | logger.warning( | 283 | logger.warning( |
| 281 | "Configuration file %s is missing, although not required " | 284 | 'Configuration file %s is missing, although not required ' |
| 282 | "by the provided parameters", | 285 | 'by the provided parameters', |
| 283 | args.config_file, | 286 | args.config_file, |
| 284 | ) | 287 | ) |
| 285 | config_dict = {} | 288 | config_dict = {} |
| 286 | else: | 289 | else: |
| 287 | logger.error("Configuration file %s is missing", args.config_file) | 290 | logger.error('Configuration file %s is missing', args.config_file) |
| 288 | raise SystemExit(errno.EIO) from e | 291 | raise SystemExit(errno.EIO) from e |
| 289 | except Exception as e: | 292 | except Exception as e: |
| 290 | logger.error("Unable to parse %r: %s", args.config_file, e) | 293 | logger.exception('Unable to parse %r', args.config_file) |
| 291 | raise SystemExit(errno.EIO) from e | 294 | raise SystemExit(errno.EIO) from e |
| 292 | try: | 295 | try: |
| 293 | if args.decrypt_value: | 296 | if args.decrypt_value: |
| @@ -338,8 +341,8 @@ def main(inargs=None): | |||
| 338 | except subprocess.CalledProcessError as e: | 341 | except subprocess.CalledProcessError as e: |
| 339 | logger.error('Unable to spawn shell process: %s', e) | 342 | logger.error('Unable to spawn shell process: %s', e) |
| 340 | sys.exit(1) | 343 | sys.exit(1) |
| 341 | except Exception as e: | 344 | except Exception: |
| 342 | logger.error('Unexpected exception: %s', e) | 345 | logger.exception('Unexpected exception') |
| 343 | sys.exit(1) | 346 | sys.exit(1) |
| 344 | 347 | ||
| 345 | 348 | ||
diff --git a/src/etoolkit/etoolkit.py b/src/etoolkit/etoolkit.py index aab42a0..870a4be 100755..100644 --- a/src/etoolkit/etoolkit.py +++ b/src/etoolkit/etoolkit.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | # etoolkit | 1 | # etoolkit |
| 2 | # Copyright (C) 2021-2022 Simeon Simeonov | 2 | # Copyright (C) 2021-2024 Simeon Simeonov |
| 3 | 3 | ||
| 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU General Public License as published by | 5 | # it under the terms of the GNU General Public License as published by |
| @@ -14,6 +14,7 @@ | |||
| 14 | # You should have received a copy of the GNU General Public License | 14 | # You should have received a copy of the GNU General Public License |
| 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | """The main module of the etoolkit package""" | 16 | """The main module of the etoolkit package""" |
| 17 | |||
| 17 | import base64 | 18 | import base64 |
| 18 | import getpass | 19 | import getpass |
| 19 | import hashlib | 20 | import hashlib |
| @@ -23,6 +24,9 @@ from cryptography.exceptions import InvalidTag | |||
| 23 | from cryptography.hazmat.primitives.ciphers.aead import AESGCM | 24 | from cryptography.hazmat.primitives.ciphers.aead import AESGCM |
| 24 | 25 | ||
| 25 | 26 | ||
| 27 | MIN_ENCRYPTED_VALUE_LENGTH = 32 | ||
| 28 | |||
| 29 | |||
| 26 | class EtoolkitInstanceError(Exception): | 30 | class EtoolkitInstanceError(Exception): |
| 27 | """EtoolkitInstanceError - Generic exceptions related to instances""" | 31 | """EtoolkitInstanceError - Generic exceptions related to instances""" |
| 28 | 32 | ||
| @@ -175,24 +179,33 @@ class EtoolkitInstance: | |||
| 175 | :rtype: str | 179 | :rtype: str |
| 176 | """ | 180 | """ |
| 177 | # check for supported versions | 181 | # check for supported versions |
| 178 | if not edata.startswith('enc-val$1$'): | 182 | if not edata.startswith(('enc-val$1$', 'enc-val$2$')): |
| 179 | raise EtoolkitInstanceError( | 183 | raise EtoolkitInstanceError( |
| 180 | f'Unsupported encryption format: {edata}' | 184 | f'Unsupported encryption format: {edata}' |
| 181 | ) | 185 | ) |
| 182 | try: | 186 | try: |
| 183 | salt, data = [base64.b64decode(t) for t in edata[10:].split('$')] | 187 | salt, data = (base64.b64decode(t) for t in edata[10:].split('$')) |
| 184 | nonce = salt[:12] | 188 | nonce = salt[:12] |
| 185 | aesgcm = AESGCM( | 189 | aesgcm = AESGCM( |
| 186 | hashlib.scrypt( | 190 | hashlib.scrypt( |
| 187 | password.encode('utf-8'), | 191 | password.encode(), salt=salt, n=2**14, r=8, p=1, dklen=32 |
| 188 | salt=salt, | ||
| 189 | n=2**14, | ||
| 190 | r=8, | ||
| 191 | p=1, | ||
| 192 | dklen=32, | ||
| 193 | ) | 192 | ) |
| 194 | ) | 193 | ) |
| 195 | return aesgcm.decrypt(nonce, data, salt).decode() | 194 | |
| 195 | # decrypt | ||
| 196 | data = aesgcm.decrypt(nonce, data, salt) | ||
| 197 | |||
| 198 | if edata.startswith('enc-val$2$'): | ||
| 199 | # exclusively for the v2 data format: | ||
| 200 | # padding_length_bytes(2 bytes) data padding (between 0 and 32) | ||
| 201 | |||
| 202 | # extract padding_length_bytes | ||
| 203 | if data[:2] == b'--': | ||
| 204 | data = data[2:] | ||
| 205 | else: | ||
| 206 | data = data[2 : -int(data[:2].decode())] | ||
| 207 | |||
| 208 | return data.decode() | ||
| 196 | except InvalidTag as e: | 209 | except InvalidTag as e: |
| 197 | raise EtoolkitInstanceError( | 210 | raise EtoolkitInstanceError( |
| 198 | f'Invalid tag when decrypting: {edata}' | 211 | f'Invalid tag when decrypting: {edata}' |
| @@ -207,6 +220,8 @@ class EtoolkitInstance: | |||
| 207 | """ | 220 | """ |
| 208 | Encrypts `data` using `password`. | 221 | Encrypts `data` using `password`. |
| 209 | 222 | ||
| 223 | Version 2 of the etoolkit encryption format | ||
| 224 | |||
| 210 | The output string is in the following format: | 225 | The output string is in the following format: |
| 211 | enc-val$`version-num`$`bas64-salt`$`base64-encrypted_data` | 226 | enc-val$`version-num`$`bas64-salt`$`base64-encrypted_data` |
| 212 | 227 | ||
| @@ -219,21 +234,37 @@ class EtoolkitInstance: | |||
| 219 | :return: The output string | 234 | :return: The output string |
| 220 | :rtype: str | 235 | :rtype: str |
| 221 | """ | 236 | """ |
| 222 | salt = os.urandom(32) | 237 | data_bytes = data.encode() |
| 223 | aesgcm = AESGCM( | 238 | if len(data_bytes) < MIN_ENCRYPTED_VALUE_LENGTH: |
| 224 | hashlib.scrypt( | 239 | padding_length = MIN_ENCRYPTED_VALUE_LENGTH - len(data_bytes) |
| 225 | password.encode('utf-8'), | 240 | rnd_bytes = os.urandom(32 + padding_length) |
| 226 | salt=salt, | 241 | salt = rnd_bytes[:32] |
| 227 | n=2**14, | 242 | aesgcm = AESGCM( |
| 228 | r=8, | 243 | hashlib.scrypt( |
| 229 | p=1, | 244 | password.encode(), salt=salt, n=2**14, r=8, p=1, dklen=32 |
| 230 | dklen=32, | 245 | ) |
| 246 | ) | ||
| 247 | nonce = rnd_bytes[:12] | ||
| 248 | padding_bytes = rnd_bytes[32:] | ||
| 249 | # padding_length_bytes is always 2 bytes | ||
| 250 | padding_length_bytes = f'{padding_length:02d}'.encode() | ||
| 251 | edata = aesgcm.encrypt( | ||
| 252 | nonce, padding_length_bytes + data_bytes + padding_bytes, salt | ||
| 253 | ) | ||
| 254 | else: | ||
| 255 | salt = os.urandom(32) | ||
| 256 | aesgcm = AESGCM( | ||
| 257 | hashlib.scrypt( | ||
| 258 | password.encode(), salt=salt, n=2**14, r=8, p=1, dklen=32 | ||
| 259 | ) | ||
| 260 | ) | ||
| 261 | nonce = salt[:12] | ||
| 262 | padding_length_bytes = b'--' # no padding used 2 bytes "sign" | ||
| 263 | edata = aesgcm.encrypt( | ||
| 264 | nonce, padding_length_bytes + data_bytes, salt | ||
| 231 | ) | 265 | ) |
| 232 | ) | ||
| 233 | nonce = salt[:12] | ||
| 234 | edata = aesgcm.encrypt(nonce, data.encode('utf-8'), salt) | ||
| 235 | return ( | 266 | return ( |
| 236 | f'enc-val$1${base64.b64encode(salt).decode()}$' | 267 | f'enc-val$2${base64.b64encode(salt).decode()}$' |
| 237 | f'{base64.b64encode(edata).decode()}' | 268 | f'{base64.b64encode(edata).decode()}' |
| 238 | ) | 269 | ) |
| 239 | 270 | ||
| @@ -252,7 +283,7 @@ class EtoolkitInstance: | |||
| 252 | :rtype: str | 283 | :rtype: str |
| 253 | """ | 284 | """ |
| 254 | hash_algo = 'sha256' | 285 | hash_algo = 'sha256' |
| 255 | iterations = 100000 | 286 | iterations = 500000 |
| 256 | salt = os.urandom(32) | 287 | salt = os.urandom(32) |
| 257 | key = hashlib.pbkdf2_hmac( | 288 | key = hashlib.pbkdf2_hmac( |
| 258 | hash_algo, password.encode('utf-8'), salt, iterations | 289 | hash_algo, password.encode('utf-8'), salt, iterations |
| @@ -339,7 +370,6 @@ class EtoolkitInstance: | |||
| 339 | macros = { | 370 | macros = { |
| 340 | '%h': os.path.expanduser('~'), | 371 | '%h': os.path.expanduser('~'), |
| 341 | '%i': self.name, | 372 | '%i': self.name, |
| 342 | # '%f': self.get_full_name(), | ||
| 343 | '%u': getpass.getuser(), | 373 | '%u': getpass.getuser(), |
| 344 | } | 374 | } |
| 345 | new_env = {} | 375 | new_env = {} |
diff --git a/tests/conftest.py b/tests/conftest.py index 5dcc456..e555a49 100644 --- a/tests/conftest.py +++ b/tests/conftest.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | # etoolkit | 1 | # etoolkit |
| 2 | # Copyright (C) 2021-2022 Simeon Simeonov | 2 | # Copyright (C) 2021-2024 Simeon Simeonov |
| 3 | 3 | ||
| 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU General Public License as published by | 5 | # it under the terms of the GNU General Public License as published by |
| @@ -14,14 +14,16 @@ | |||
| 14 | # You should have received a copy of the GNU General Public License | 14 | # You should have received a copy of the GNU General Public License |
| 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | """Common fixtures""" | 16 | """Common fixtures""" |
| 17 | |||
| 17 | import json | 18 | import json |
| 18 | 19 | ||
| 19 | import pytest | 20 | import pytest |
| 20 | 21 | ||
| 21 | 22 | ||
| 22 | @pytest.fixture | 23 | @pytest.fixture() |
| 23 | def config_data(): | 24 | def config_data(): |
| 24 | """config_data for testing""" | 25 | """config_data for testing""" |
| 26 | |||
| 25 | return { | 27 | return { |
| 26 | 'general': { | 28 | 'general': { |
| 27 | 'MASTER_PASSWORD_HASH': ( | 29 | 'MASTER_PASSWORD_HASH': ( |
| @@ -51,26 +53,47 @@ def config_data(): | |||
| 51 | } | 53 | } |
| 52 | 54 | ||
| 53 | 55 | ||
| 54 | @pytest.fixture | 56 | @pytest.fixture() |
| 55 | def config_file(tmp_path, config_data): | 57 | def config_file(tmp_path, config_data): |
| 56 | """temporary config file for testing that includes config_data""" | 58 | """temporary config file for testing that includes config_data""" |
| 57 | cf = tmp_path / "etoolkit.json" | 59 | |
| 60 | cf = tmp_path / 'etoolkit.json' | ||
| 58 | cf.write_text(json.dumps(config_data)) | 61 | cf.write_text(json.dumps(config_data)) |
| 59 | return str(cf) | 62 | return str(cf) |
| 60 | 63 | ||
| 61 | 64 | ||
| 62 | @pytest.fixture | 65 | @pytest.fixture() |
| 63 | def non_random_bytes_32(): | 66 | def non_random_bytes_32(): |
| 64 | """always use the same bytes instead of os.urandom(32)""" | 67 | """always use the same bytes instead of os.urandom(32)""" |
| 68 | |||
| 65 | return ( | 69 | return ( |
| 66 | b'\xb9\x8aY3U_\x00j\xb4\x086K\xd9\xdb\x88N' | 70 | b'\xb9\x8aY3U_\x00j\xb4\x086K\xd9\xdb\x88N' |
| 67 | b'\xcd;\xe8$#\xfa\x12\x05\x12\x0c~\x17\x89\xa7\xf3J' | 71 | b'\xcd;\xe8$#\xfa\x12\x05\x12\x0c~\x17\x89\xa7\xf3J' |
| 68 | ) | 72 | ) |
| 69 | 73 | ||
| 70 | 74 | ||
| 71 | @pytest.fixture | 75 | @pytest.fixture() |
| 76 | def non_random_bytes_61(): | ||
| 77 | """always use the same bytes instead of os.urandom(61)""" | ||
| 78 | |||
| 79 | return ( | ||
| 80 | b'D$\x99\xaa\xafiZ\xb4C\xa0%XTz)\xca\xedK\xcd\xa2F~\xff+\xa1[\xe2\xaa' | ||
| 81 | b'\xb2\xd3\x07\x13\xedb\xc2\x84\xfe\tS\r\xf0\x02_\xef\xe3\xde\xf1?e' | ||
| 82 | b'\xa4s(Q\x04\xcd\xc7T\x01_D\xb1' | ||
| 83 | ) | ||
| 84 | |||
| 85 | |||
| 86 | @pytest.fixture() | ||
| 87 | def nonexistent_config_file(tmp_path): | ||
| 88 | """temporary config file for testing that includes config_data""" | ||
| 89 | |||
| 90 | return str(tmp_path / 'etoolkitt.json') | ||
| 91 | |||
| 92 | |||
| 93 | @pytest.fixture() | ||
| 72 | def password_hash(): | 94 | def password_hash(): |
| 73 | """password hash for testing, corresponding to 'The very secret passwd'""" | 95 | """password hash for testing, corresponding to 'The very secret passwd'""" |
| 96 | |||
| 74 | return ( | 97 | return ( |
| 75 | 'pbkdf2_sha256$100000$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4mn80o=$h3' | 98 | 'pbkdf2_sha256$100000$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4mn80o=$h3' |
| 76 | 'PSPLCd37fP15zKdW4CBGn7CXE+q5UiydaF3vbeZHo=' | 99 | 'PSPLCd37fP15zKdW4CBGn7CXE+q5UiydaF3vbeZHo=' |
diff --git a/tests/test_cli.py b/tests/test_cli.py index 684c2e7..e5db244 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | # etoolkit | 1 | # etoolkit |
| 2 | # Copyright (C) 2021-2022 Simeon Simeonov | 2 | # Copyright (C) 2021-2024 Simeon Simeonov |
| 3 | 3 | ||
| 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU General Public License as published by | 5 | # it under the terms of the GNU General Public License as published by |
| @@ -14,6 +14,7 @@ | |||
| 14 | # You should have received a copy of the GNU General Public License | 14 | # You should have received a copy of the GNU General Public License |
| 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | """Tests for the CLI (etoolkit.__main__""" | 16 | """Tests for the CLI (etoolkit.__main__""" |
| 17 | |||
| 17 | import errno | 18 | import errno |
| 18 | import os | 19 | import os |
| 19 | import unittest.mock | 20 | import unittest.mock |
| @@ -24,11 +25,9 @@ import etoolkit | |||
| 24 | from etoolkit.__main__ import main | 25 | from etoolkit.__main__ import main |
| 25 | 26 | ||
| 26 | 27 | ||
| 27 | @unittest.mock.patch('os.urandom') | ||
| 28 | @unittest.mock.patch('builtins.input') | 28 | @unittest.mock.patch('builtins.input') |
| 29 | def test_decrypt(binput, urandom, capsys, non_random_bytes_32, config_file): | 29 | def test_decrypt_v1(binput, capsys, config_file): |
| 30 | """Tests encryption via the CLI interface""" | 30 | """Tests v1 decryption via the CLI interface""" |
| 31 | urandom.return_value = non_random_bytes_32 | ||
| 32 | binput.return_value = ( | 31 | binput.return_value = ( |
| 33 | 'enc-val$1$uYpZM1VfAGq0CDZL2duITs076CQj+' | 32 | 'enc-val$1$uYpZM1VfAGq0CDZL2duITs076CQj+' |
| 34 | 'hIFEgx+F4mn80o=$xdF/1S+R2MGlEQMCOLG6OjEuzw==' | 33 | 'hIFEgx+F4mn80o=$xdF/1S+R2MGlEQMCOLG6OjEuzw==' |
| @@ -43,11 +42,29 @@ def test_decrypt(binput, urandom, capsys, non_random_bytes_32, config_file): | |||
| 43 | assert capsys.readouterr().out.strip() == 'Decrypted value: bar' | 42 | assert capsys.readouterr().out.strip() == 'Decrypted value: bar' |
| 44 | 43 | ||
| 45 | 44 | ||
| 45 | @unittest.mock.patch('builtins.input') | ||
| 46 | def test_decrypt_v2(binput, capsys, config_file): | ||
| 47 | """Tests v2 decryption via the CLI interface""" | ||
| 48 | binput.return_value = ( | ||
| 49 | 'enc-val$2$RCSZqq9pWrRDoCVYVHopyu1LzaJGfv8roVviq' | ||
| 50 | 'rLTBxM=$VW3UZ6l12yDtyaqWHb7i0QEDiS9s9np' | ||
| 51 | '7huAACK54BtZVV7RZoIhbu4K6zZuz+LRCyio=' | ||
| 52 | ) | ||
| 53 | with unittest.mock.patch.dict( | ||
| 54 | os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'} | ||
| 55 | ): | ||
| 56 | with pytest.raises(SystemExit) as exit_info: | ||
| 57 | main(['-c', f'{config_file}', '-d']) | ||
| 58 | assert exit_info.type == SystemExit | ||
| 59 | assert exit_info.value.code == 0 | ||
| 60 | assert capsys.readouterr().out.strip() == 'Decrypted value: bar' | ||
| 61 | |||
| 62 | |||
| 46 | @unittest.mock.patch('os.urandom') | 63 | @unittest.mock.patch('os.urandom') |
| 47 | @unittest.mock.patch('builtins.input', lambda *args: 'bar') | 64 | @unittest.mock.patch('builtins.input', lambda *args: 'bar') |
| 48 | def test_encrypt_with_echo(urandom, capsys, non_random_bytes_32, config_file): | 65 | def test_encrypt_with_echo(urandom, capsys, non_random_bytes_61, config_file): |
| 49 | """Tests encryption via the CLI interface""" | 66 | """Tests encryption via the CLI interface""" |
| 50 | urandom.return_value = non_random_bytes_32 | 67 | urandom.return_value = non_random_bytes_61 |
| 51 | with unittest.mock.patch.dict( | 68 | with unittest.mock.patch.dict( |
| 52 | os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'} | 69 | os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'} |
| 53 | ): | 70 | ): |
| @@ -56,16 +73,17 @@ def test_encrypt_with_echo(urandom, capsys, non_random_bytes_32, config_file): | |||
| 56 | assert exit_info.type == SystemExit | 73 | assert exit_info.type == SystemExit |
| 57 | assert exit_info.value.code == 0 | 74 | assert exit_info.value.code == 0 |
| 58 | assert capsys.readouterr().out.strip() == ( | 75 | assert capsys.readouterr().out.strip() == ( |
| 59 | 'Encrypted value: enc-val$1$uYpZM1VfAGq0CDZL2duITs076CQj+' | 76 | 'Encrypted value: enc-val$2$RCSZqq9pWrRDoCVYVHopyu1LzaJGfv8roVviq' |
| 60 | 'hIFEgx+F4mn80o=$xdF/1S+R2MGlEQMCOLG6OjEuzw==' | 77 | 'rLTBxM=$VW3UZ6l12yDtyaqWHb7i0QEDiS9s9np' |
| 78 | '7huAACK54BtZVV7RZoIhbu4K6zZuz+LRCyio=' | ||
| 61 | ) | 79 | ) |
| 62 | 80 | ||
| 63 | 81 | ||
| 64 | @unittest.mock.patch('os.urandom') | 82 | @unittest.mock.patch('os.urandom') |
| 65 | @unittest.mock.patch('getpass.getpass', lambda *args: 'bar') | 83 | @unittest.mock.patch('getpass.getpass', lambda *args: 'bar') |
| 66 | def test_encrypt_without_echo(gpass, capsys, non_random_bytes_32, config_file): | 84 | def test_encrypt_without_echo(gpass, capsys, non_random_bytes_61, config_file): |
| 67 | """Tests encryption via the CLI interface""" | 85 | """Tests encryption via the CLI interface""" |
| 68 | gpass.return_value = non_random_bytes_32 | 86 | gpass.return_value = non_random_bytes_61 |
| 69 | with unittest.mock.patch.dict( | 87 | with unittest.mock.patch.dict( |
| 70 | os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'} | 88 | os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'} |
| 71 | ): | 89 | ): |
| @@ -74,19 +92,20 @@ def test_encrypt_without_echo(gpass, capsys, non_random_bytes_32, config_file): | |||
| 74 | assert exit_info.type == SystemExit | 92 | assert exit_info.type == SystemExit |
| 75 | assert exit_info.value.code == 0 | 93 | assert exit_info.value.code == 0 |
| 76 | assert capsys.readouterr().out.strip() == ( | 94 | assert capsys.readouterr().out.strip() == ( |
| 77 | 'Encrypted value: enc-val$1$uYpZM1VfAGq0CDZL2duITs076CQj+' | 95 | 'Encrypted value: enc-val$2$RCSZqq9pWrRDoCVYVHopyu1LzaJGfv8roVviq' |
| 78 | 'hIFEgx+F4mn80o=$xdF/1S+R2MGlEQMCOLG6OjEuzw==' | 96 | 'rLTBxM=$VW3UZ6l12yDtyaqWHb7i0QEDiS9s9np' |
| 97 | '7huAACK54BtZVV7RZoIhbu4K6zZuz+LRCyio=' | ||
| 79 | ) | 98 | ) |
| 80 | 99 | ||
| 81 | 100 | ||
| 82 | def test_list(capsys, config_file): | 101 | def test_list(capsys, config_file, nonexistent_config_file): |
| 83 | """Tests list via the CLI interface""" | 102 | """Tests list via the CLI interface""" |
| 84 | with pytest.raises(SystemExit) as exit_info: | 103 | with pytest.raises(SystemExit) as exit_info: |
| 85 | main(['-l']) | 104 | main(['-c', nonexistent_config_file, '-l']) |
| 86 | assert exit_info.type == SystemExit | 105 | assert exit_info.type == SystemExit |
| 87 | assert exit_info.value.code == errno.EIO | 106 | assert exit_info.value.code == errno.EIO |
| 88 | with pytest.raises(SystemExit) as exit_info: | 107 | with pytest.raises(SystemExit) as exit_info: |
| 89 | main(['-c', f'{config_file}', '-l']) | 108 | main(['-c', config_file, '-l']) |
| 90 | assert exit_info.type == SystemExit | 109 | assert exit_info.type == SystemExit |
| 91 | assert exit_info.value.code == 0 | 110 | assert exit_info.value.code == 0 |
| 92 | assert capsys.readouterr().out.strip() == f'dev{os.linesep}secret' | 111 | assert capsys.readouterr().out.strip() == f'dev{os.linesep}secret' |
| @@ -104,10 +123,7 @@ def test_help(capsys): | |||
| 104 | @unittest.mock.patch('os.urandom') | 123 | @unittest.mock.patch('os.urandom') |
| 105 | @unittest.mock.patch('getpass.getpass') | 124 | @unittest.mock.patch('getpass.getpass') |
| 106 | def test_generate_master_password_hash( | 125 | def test_generate_master_password_hash( |
| 107 | gpass, | 126 | gpass, urandom, capsys, non_random_bytes_32 |
| 108 | urandom, | ||
| 109 | capsys, | ||
| 110 | non_random_bytes_32, | ||
| 111 | ): | 127 | ): |
| 112 | """Tests master password hash generation via the CLI interface""" | 128 | """Tests master password hash generation via the CLI interface""" |
| 113 | urandom.return_value = non_random_bytes_32 | 129 | urandom.return_value = non_random_bytes_32 |
| @@ -117,8 +133,8 @@ def test_generate_master_password_hash( | |||
| 117 | assert exit_info.type == SystemExit | 133 | assert exit_info.type == SystemExit |
| 118 | assert exit_info.value.code == 0 | 134 | assert exit_info.value.code == 0 |
| 119 | assert capsys.readouterr().out.strip() == ( | 135 | assert capsys.readouterr().out.strip() == ( |
| 120 | 'Master password hash: pbkdf2_sha256$100000$uYpZM1VfAGq0CDZL2duITs076' | 136 | 'Master password hash: pbkdf2_sha256$500000$uYpZM1VfAGq0CDZL2duITs076' |
| 121 | 'CQj+hIFEgx+F4mn80o=$h3PSPLCd37fP15zKdW4CBGn7CXE+q5UiydaF3vbeZHo=' | 137 | 'CQj+hIFEgx+F4mn80o=$Msl8/5nOBj0TRchykMzXmCXR8VQVyBqUPHe1PDWeJi8=' |
| 122 | ) | 138 | ) |
| 123 | 139 | ||
| 124 | 140 | ||
diff --git a/tests/test_envtoolkit_instance.py b/tests/test_envtoolkit_instance.py index 09baa31..73faed6 100644 --- a/tests/test_envtoolkit_instance.py +++ b/tests/test_envtoolkit_instance.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | # etoolkit | 1 | # etoolkit |
| 2 | # Copyright (C) 2021-2022 Simeon Simeonov | 2 | # Copyright (C) 2021-2024 Simeon Simeonov |
| 3 | 3 | ||
| 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU General Public License as published by | 5 | # it under the terms of the GNU General Public License as published by |
| @@ -14,6 +14,7 @@ | |||
| 14 | # You should have received a copy of the GNU General Public License | 14 | # You should have received a copy of the GNU General Public License |
| 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | """Tests for etoolkit.EtoolkitInstance""" | 16 | """Tests for etoolkit.EtoolkitInstance""" |
| 17 | |||
| 17 | import pytest | 18 | import pytest |
| 18 | 19 | ||
| 19 | import etoolkit | 20 | import etoolkit |
| @@ -32,8 +33,9 @@ def test_instantiation(config_data): | |||
| 32 | assert 'DB_CONNECTION' not in instance.sensitive_env_variables | 33 | assert 'DB_CONNECTION' not in instance.sensitive_env_variables |
| 33 | assert 'PASSWORD' in instance.sensitive_env_variables | 34 | assert 'PASSWORD' in instance.sensitive_env_variables |
| 34 | assert instance.name == 'secret' | 35 | assert instance.name == 'secret' |
| 35 | assert instance.master_password_hash == ( | 36 | assert ( |
| 36 | config_data['general']['MASTER_PASSWORD_HASH'] | 37 | instance.master_password_hash |
| 38 | == (config_data['general']['MASTER_PASSWORD_HASH']) | ||
| 37 | ) | 39 | ) |
| 38 | assert instance.master_password is None | 40 | assert instance.master_password is None |
| 39 | 41 | ||
diff --git a/tests/test_envtoolkit_instance_static.py b/tests/test_envtoolkit_instance_static.py index 73a5f36..5fb9451 100644 --- a/tests/test_envtoolkit_instance_static.py +++ b/tests/test_envtoolkit_instance_static.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | # etoolkit | 1 | # etoolkit |
| 2 | # Copyright (C) 2021-2022 Simeon Simeonov | 2 | # Copyright (C) 2021-2024 Simeon Simeonov |
| 3 | 3 | ||
| 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU General Public License as published by | 5 | # it under the terms of the GNU General Public License as published by |
| @@ -14,6 +14,7 @@ | |||
| 14 | # You should have received a copy of the GNU General Public License | 14 | # You should have received a copy of the GNU General Public License |
| 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | """Tests for etoolkit.EtoolkitInstance static methods""" | 16 | """Tests for etoolkit.EtoolkitInstance static methods""" |
| 17 | |||
| 17 | import unittest.mock | 18 | import unittest.mock |
| 18 | 19 | ||
| 19 | import pytest | 20 | import pytest |
| @@ -35,7 +36,7 @@ def test_confirm_password_prompt(getpass, password_hash): | |||
| 35 | ) | 36 | ) |
| 36 | 37 | ||
| 37 | 38 | ||
| 38 | def test_decrypt(): | 39 | def test_decrypt_v1(): |
| 39 | """Tests the static EtoolkitInstance.decrypt method""" | 40 | """Tests the static EtoolkitInstance.decrypt method""" |
| 40 | assert ( | 41 | assert ( |
| 41 | etoolkit.EtoolkitInstance.decrypt( | 42 | etoolkit.EtoolkitInstance.decrypt( |
| @@ -49,39 +50,118 @@ def test_decrypt(): | |||
| 49 | ) | 50 | ) |
| 50 | 51 | ||
| 51 | # now test with modified edata | 52 | # now test with modified edata |
| 53 | edata = ( | ||
| 54 | 'enc-val$1$/cXpEMoZrTlb9yokGhw8tLTSUkqnqJ5ZoAkurNgMYx' | ||
| 55 | 'w=$1VdkSMcZnLRwLiu1M8VlYcbelwmiVNY=' | ||
| 56 | ) | ||
| 52 | with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info: | 57 | with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info: |
| 53 | edata = ( | 58 | etoolkit.EtoolkitInstance.decrypt('The very secret passwd', edata) |
| 54 | 'enc-val$1$/cXpEMoZrTlb9yokGhw8tLTSUkqnqJ5ZoAkurNgMYx' | 59 | assert exc_info.type is etoolkit.EtoolkitInstanceError |
| 55 | 'w=$1VdkSMcZnLRwLiu1M8VlYcbelwmiVNY=' | 60 | assert exc_info.value.args[0] == f'Invalid tag when decrypting: {edata}' |
| 61 | |||
| 62 | |||
| 63 | def test_decrypt_v2_no_padding(): | ||
| 64 | """Tests the static EtoolkitInstance.decrypt method for v2 - no padding""" | ||
| 65 | assert ( | ||
| 66 | etoolkit.EtoolkitInstance.decrypt( | ||
| 67 | 'The very secret passwd', | ||
| 68 | ( | ||
| 69 | 'enc-val$2$Wer5lECGyeZhhYS58N18WVx5Zzy+rrC+BPlq3Dw89wQ=$' | ||
| 70 | 'SQc0ox6Emf2m5rrumsiptpIZEujdpXXSR/' | ||
| 71 | '1VcfEZeBz4+KDSagr9ID+bkc4R2yFdxHnhig1eqQ8=' | ||
| 72 | ), | ||
| 56 | ) | 73 | ) |
| 74 | == 'Nobody expects the Spanish inquisition' | ||
| 75 | ) | ||
| 76 | |||
| 77 | # now test with modified edata | ||
| 78 | edata = ( | ||
| 79 | 'enc-val$2$Wer5lECGyeZhhYS58N18WVx5Zzy+rrC+BPlq3Dw89wQ=$' | ||
| 80 | 'SQc0ox6Emf2m4rrumsiptpIZEujdpXXSR/' | ||
| 81 | '1VcfEZeBz4+KDSagr9ID+bkc4R2yFdxHnhig1eqQ8=' | ||
| 82 | ) | ||
| 83 | with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info: | ||
| 84 | etoolkit.EtoolkitInstance.decrypt('The very secret passwd', edata) | ||
| 85 | assert exc_info.type is etoolkit.EtoolkitInstanceError | ||
| 86 | assert exc_info.value.args[0] == f'Invalid tag when decrypting: {edata}' | ||
| 87 | |||
| 88 | |||
| 89 | def test_decrypt_v2_with_padding(): | ||
| 90 | """Tests the static EtoolkitInstance.decrypt method for v2 with padding""" | ||
| 91 | assert ( | ||
| 57 | etoolkit.EtoolkitInstance.decrypt( | 92 | etoolkit.EtoolkitInstance.decrypt( |
| 58 | 'The very secret passwd', edata | 93 | 'The very secret passwd', |
| 59 | ) == 'secret1' | 94 | ( |
| 95 | 'enc-val$2$//kzyUbDEWNoPC5dyukhB8de8+IVaLR2ngx2HwkfOuM=$' | ||
| 96 | 'rhRona4wP9nhnXjcHqwkjFDsiVVVjYanAs' | ||
| 97 | 'N4kknNkgC0ix4RtJQHYDeTzw1rrR1vb2w=' | ||
| 98 | ), | ||
| 99 | ) | ||
| 100 | == 'secret1' | ||
| 101 | ) | ||
| 102 | |||
| 103 | # now test with modified edata | ||
| 104 | edata = ( | ||
| 105 | 'enc-val$2$//kzyUbDEWNoPC5dyukhB8de8+IVaLR2ngx2HwkfOuM=$' | ||
| 106 | 'rhRona4wP8nhnXjcHqwkjFDsiVVVjYanAsN4kknNkgC0ix4RtJQHYDeTzw1rrR1vb2w=' | ||
| 107 | ) | ||
| 108 | with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info: | ||
| 109 | etoolkit.EtoolkitInstance.decrypt('The very secret passwd', edata) | ||
| 60 | assert exc_info.type is etoolkit.EtoolkitInstanceError | 110 | assert exc_info.type is etoolkit.EtoolkitInstanceError |
| 61 | assert exc_info.value.args[0] == f'Invalid tag when decrypting: {edata}' | 111 | assert exc_info.value.args[0] == f'Invalid tag when decrypting: {edata}' |
| 62 | 112 | ||
| 63 | 113 | ||
| 64 | def test_encrypt(): | 114 | def test_encrypt_no_padding(): |
| 65 | """Tests the static EtoolkitInstance.encrypt method""" | 115 | """Tests the static EtoolkitInstance.encrypt method with a long string""" |
| 116 | edata = etoolkit.EtoolkitInstance.encrypt( | ||
| 117 | 'foo', 'Nobody expects the Spanish inquisition' | ||
| 118 | ) | ||
| 119 | assert edata.startswith('enc-val$2$') | ||
| 120 | assert len(edata) == 131 | ||
| 121 | # the edata should always be different because of random salting | ||
| 122 | assert edata != etoolkit.EtoolkitInstance.encrypt( | ||
| 123 | 'foo', 'Nobody expects the Spanish inquisition' | ||
| 124 | ) | ||
| 125 | |||
| 126 | |||
| 127 | def test_encrypt_with_padding(): | ||
| 128 | """Tests the static EtoolkitInstance.encrypt method with a short string""" | ||
| 66 | edata = etoolkit.EtoolkitInstance.encrypt('foo', 'bar') | 129 | edata = etoolkit.EtoolkitInstance.encrypt('foo', 'bar') |
| 67 | assert edata.startswith('enc-val$') | 130 | assert edata.startswith('enc-val$2$') |
| 68 | assert len(edata) == 83 | 131 | assert len(edata) == 123 |
| 69 | # the edata should always be different because of random salting | 132 | # the edata should always be different because of random salting |
| 70 | assert edata != etoolkit.EtoolkitInstance.encrypt('foo', 'bar') | 133 | assert edata != etoolkit.EtoolkitInstance.encrypt('foo', 'bar') |
| 71 | 134 | ||
| 72 | 135 | ||
| 73 | @unittest.mock.patch('os.urandom') | 136 | @unittest.mock.patch('os.urandom') |
| 74 | def test_encrypt_staticly(urandom, non_random_bytes_32): | 137 | def test_encrypt_staticly_no_padding(urandom, non_random_bytes_32): |
| 75 | """Tests the EtoolkitInstance.encrypt method always with the same salt""" | 138 | """Tests the EtoolkitInstance.encrypt method always with the same salt""" |
| 76 | urandom.return_value = non_random_bytes_32 | 139 | urandom.return_value = non_random_bytes_32 |
| 140 | edata = etoolkit.EtoolkitInstance.encrypt( | ||
| 141 | 'The very secret passwd', 'Nobody expects the Spanish inquisition' | ||
| 142 | ) | ||
| 143 | assert edata == ( | ||
| 144 | 'enc-val$2$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4mn80o=$' | ||
| 145 | 'UX/5YeRsh5/2vZ2J1UOS+BJti73Kbp6C1pJmC' | ||
| 146 | 'o8hFSujpe35X/XpzAiYv4BV1LNwnSYECsotsgs=' | ||
| 147 | ) | ||
| 148 | assert len(edata) == 131 | ||
| 149 | assert edata == etoolkit.EtoolkitInstance.encrypt( | ||
| 150 | 'The very secret passwd', 'Nobody expects the Spanish inquisition' | ||
| 151 | ) | ||
| 152 | |||
| 153 | |||
| 154 | @unittest.mock.patch('os.urandom') | ||
| 155 | def test_encrypt_staticly_with_padding(urandom, non_random_bytes_61): | ||
| 156 | """Tests the EtoolkitInstance.encrypt method always with the same salt""" | ||
| 157 | urandom.return_value = non_random_bytes_61 | ||
| 77 | edata = etoolkit.EtoolkitInstance.encrypt('The very secret passwd', 'bar') | 158 | edata = etoolkit.EtoolkitInstance.encrypt('The very secret passwd', 'bar') |
| 78 | assert edata == ( | 159 | assert edata == ( |
| 79 | 'enc-val$1$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4mn80' | 160 | 'enc-val$2$RCSZqq9pWrRDoCVYVHopyu1LzaJGfv8roVviqrLTBxM=$' |
| 80 | 'o=$HjPFNv6xC5hbMrFc0L5lSkWdfQ==' | 161 | '+Yo6Ya2MAVcBLTQHuATkyFc+dzYsL/ESvA6ofOUDsiKZvIff35cUHAmoNxVuGG+MXv4=' |
| 81 | ) | 162 | ) |
| 82 | assert edata == etoolkit.EtoolkitInstance.encrypt( | 163 | assert edata == etoolkit.EtoolkitInstance.encrypt( |
| 83 | 'The very secret passwd', | 164 | 'The very secret passwd', 'bar' |
| 84 | 'bar', | ||
| 85 | ) | 165 | ) |
| 86 | 166 | ||
| 87 | 167 | ||
| @@ -91,7 +171,7 @@ def test_get_new_password_hash(): | |||
| 91 | 'The very secret passwd' | 171 | 'The very secret passwd' |
| 92 | ) | 172 | ) |
| 93 | # all pbkdf2 params are the same / hardcoded for the time being | 173 | # all pbkdf2 params are the same / hardcoded for the time being |
| 94 | assert new_hash.startswith('pbkdf2_sha256$100000$') | 174 | assert new_hash.startswith('pbkdf2_sha256$500000$') |
| 95 | assert len(new_hash) == 110 | 175 | assert len(new_hash) == 110 |
| 96 | # the hash should always be different because of random salting | 176 | # the hash should always be different because of random salting |
| 97 | assert new_hash != etoolkit.EtoolkitInstance.get_new_password_hash( | 177 | assert new_hash != etoolkit.EtoolkitInstance.get_new_password_hash( |
