From c31fa58c85e866b3a5ab04882c7aff655f0b5477 Mon Sep 17 00:00:00 2001
From: Simeon Simeonov
Date: Tue, 30 Apr 2024 11:39:20 +0200
Subject: Implement etoolkit encryption protocol v2
---
README.md | 4 +-
pyproject.toml | 3 +
setup.cfg | 5 +-
src/etoolkit/__init__.py | 5 +-
src/etoolkit/__main__.py | 23 ++++---
src/etoolkit/etoolkit.py | 80 +++++++++++++++-------
tests/conftest.py | 35 ++++++++--
tests/test_cli.py | 60 ++++++++++------
tests/test_envtoolkit_instance.py | 8 ++-
tests/test_envtoolkit_instance_static.py | 114 ++++++++++++++++++++++++++-----
10 files changed, 248 insertions(+), 89 deletions(-)
mode change 100755 => 100644 src/etoolkit/etoolkit.py
diff --git a/README.md b/README.md
index ae52d21..6c58bdd 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ goals were set:
## Requirements
-Apart from Python >= 3.7, the only requirement is
+Apart from Python >= 3.8, the only requirement is
[cryptography](https://pypi.org/project/cryptography/)
@@ -317,7 +317,7 @@ Simeon Simeonov - sgs @ LiberaChat
## [License](https://github.com/blackm0re/etoolkit/blob/master/LICENSE)
-Copyright (C) 2021-2022 Simeon Simeonov
+Copyright (C) 2021-2024 Simeon Simeonov
All rights reserved.
[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"
testpaths = [
"tests"
]
+pythonpath = [
+ "src"
+]
diff --git a/setup.cfg b/setup.cfg
index c218df8..4985370 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -15,11 +15,12 @@ classifiers =
Intended Audience :: System Administrators
License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Programming Language :: Python :: 3
- Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
+ Programming Language :: Python :: 3.12
+ Programming Language :: Python :: 3.13
Operating System :: POSIX
Topic :: Security :: Cryptography
@@ -31,7 +32,7 @@ project_urls =
package_dir =
= src
packages = find:
-python_requires = >=3.7
+python_requires = >=3.8
install_requires =
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 @@
# etoolkit
-# Copyright (C) 2021-2022 Simeon Simeonov
+# Copyright (C) 2021-2024 Simeon Simeonov
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -14,10 +14,11 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""A simple toolkit for setting environment variables in a flexible way"""
+
from .etoolkit import EtoolkitInstance, EtoolkitInstanceError
__author__ = 'Simeon Simeonov'
-__version__ = '1.2.0'
+__version__ = '1.3.0'
__license__ = 'GPL3'
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 @@
# etoolkit
-# Copyright (C) 2021-2022 Simeon Simeonov
+# Copyright (C) 2021-2024 Simeon Simeonov
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -16,11 +16,14 @@
"""
CLI entry point for the etoolkit package
-Examples:
+Examples
+--------
python -m etoolkit -h
python -m etoolkit -p
+
"""
+
import argparse
import errno
import getpass
@@ -33,7 +36,7 @@ import sys
import etoolkit
-DEFAULT_LOG_FORMAT = "%(levelname)s: %(message)s"
+DEFAULT_LOG_FORMAT = '%(levelname)s: %(message)s'
DEFAULT_LOG_LEVEL = logging.WARNING
logger = logging.getLogger(__name__)
@@ -269,7 +272,7 @@ def main(inargs=None):
)
args = parser.parse_args(inargs)
try:
- with io.open(args.config_file, 'r', encoding='utf-8') as fp:
+ with io.open(args.config_file, encoding='utf-8') as fp:
config_dict = json.load(fp)
except FileNotFoundError as e:
# do not raise exception if config-file is missing for:
@@ -278,16 +281,16 @@ def main(inargs=None):
# - password hash generation
if args.password_hash or args.decrypt_value or args.encrypt_value:
logger.warning(
- "Configuration file %s is missing, although not required "
- "by the provided parameters",
+ 'Configuration file %s is missing, although not required '
+ 'by the provided parameters',
args.config_file,
)
config_dict = {}
else:
- logger.error("Configuration file %s is missing", args.config_file)
+ logger.error('Configuration file %s is missing', args.config_file)
raise SystemExit(errno.EIO) from e
except Exception as e:
- logger.error("Unable to parse %r: %s", args.config_file, e)
+ logger.exception('Unable to parse %r', args.config_file)
raise SystemExit(errno.EIO) from e
try:
if args.decrypt_value:
@@ -338,8 +341,8 @@ def main(inargs=None):
except subprocess.CalledProcessError as e:
logger.error('Unable to spawn shell process: %s', e)
sys.exit(1)
- except Exception as e:
- logger.error('Unexpected exception: %s', e)
+ except Exception:
+ logger.exception('Unexpected exception')
sys.exit(1)
diff --git a/src/etoolkit/etoolkit.py b/src/etoolkit/etoolkit.py
old mode 100755
new mode 100644
index aab42a0..870a4be
--- a/src/etoolkit/etoolkit.py
+++ b/src/etoolkit/etoolkit.py
@@ -1,5 +1,5 @@
# etoolkit
-# Copyright (C) 2021-2022 Simeon Simeonov
+# Copyright (C) 2021-2024 Simeon Simeonov
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -14,6 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""The main module of the etoolkit package"""
+
import base64
import getpass
import hashlib
@@ -23,6 +24,9 @@ from cryptography.exceptions import InvalidTag
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
+MIN_ENCRYPTED_VALUE_LENGTH = 32
+
+
class EtoolkitInstanceError(Exception):
"""EtoolkitInstanceError - Generic exceptions related to instances"""
@@ -175,24 +179,33 @@ class EtoolkitInstance:
:rtype: str
"""
# check for supported versions
- if not edata.startswith('enc-val$1$'):
+ if not edata.startswith(('enc-val$1$', 'enc-val$2$')):
raise EtoolkitInstanceError(
f'Unsupported encryption format: {edata}'
)
try:
- salt, data = [base64.b64decode(t) for t in edata[10:].split('$')]
+ salt, data = (base64.b64decode(t) for t in edata[10:].split('$'))
nonce = salt[:12]
aesgcm = AESGCM(
hashlib.scrypt(
- password.encode('utf-8'),
- salt=salt,
- n=2**14,
- r=8,
- p=1,
- dklen=32,
+ password.encode(), salt=salt, n=2**14, r=8, p=1, dklen=32
)
)
- return aesgcm.decrypt(nonce, data, salt).decode()
+
+ # decrypt
+ data = aesgcm.decrypt(nonce, data, salt)
+
+ if edata.startswith('enc-val$2$'):
+ # exclusively for the v2 data format:
+ # padding_length_bytes(2 bytes) data padding (between 0 and 32)
+
+ # extract padding_length_bytes
+ if data[:2] == b'--':
+ data = data[2:]
+ else:
+ data = data[2 : -int(data[:2].decode())]
+
+ return data.decode()
except InvalidTag as e:
raise EtoolkitInstanceError(
f'Invalid tag when decrypting: {edata}'
@@ -207,6 +220,8 @@ class EtoolkitInstance:
"""
Encrypts `data` using `password`.
+ Version 2 of the etoolkit encryption format
+
The output string is in the following format:
enc-val$`version-num`$`bas64-salt`$`base64-encrypted_data`
@@ -219,21 +234,37 @@ class EtoolkitInstance:
:return: The output string
:rtype: str
"""
- salt = os.urandom(32)
- aesgcm = AESGCM(
- hashlib.scrypt(
- password.encode('utf-8'),
- salt=salt,
- n=2**14,
- r=8,
- p=1,
- dklen=32,
+ data_bytes = data.encode()
+ if len(data_bytes) < MIN_ENCRYPTED_VALUE_LENGTH:
+ padding_length = MIN_ENCRYPTED_VALUE_LENGTH - len(data_bytes)
+ rnd_bytes = os.urandom(32 + padding_length)
+ salt = rnd_bytes[:32]
+ aesgcm = AESGCM(
+ hashlib.scrypt(
+ password.encode(), salt=salt, n=2**14, r=8, p=1, dklen=32
+ )
+ )
+ nonce = rnd_bytes[:12]
+ padding_bytes = rnd_bytes[32:]
+ # padding_length_bytes is always 2 bytes
+ padding_length_bytes = f'{padding_length:02d}'.encode()
+ edata = aesgcm.encrypt(
+ nonce, padding_length_bytes + data_bytes + padding_bytes, salt
+ )
+ else:
+ salt = os.urandom(32)
+ aesgcm = AESGCM(
+ hashlib.scrypt(
+ password.encode(), salt=salt, n=2**14, r=8, p=1, dklen=32
+ )
+ )
+ nonce = salt[:12]
+ padding_length_bytes = b'--' # no padding used 2 bytes "sign"
+ edata = aesgcm.encrypt(
+ nonce, padding_length_bytes + data_bytes, salt
)
- )
- nonce = salt[:12]
- edata = aesgcm.encrypt(nonce, data.encode('utf-8'), salt)
return (
- f'enc-val$1${base64.b64encode(salt).decode()}$'
+ f'enc-val$2${base64.b64encode(salt).decode()}$'
f'{base64.b64encode(edata).decode()}'
)
@@ -252,7 +283,7 @@ class EtoolkitInstance:
:rtype: str
"""
hash_algo = 'sha256'
- iterations = 100000
+ iterations = 500000
salt = os.urandom(32)
key = hashlib.pbkdf2_hmac(
hash_algo, password.encode('utf-8'), salt, iterations
@@ -339,7 +370,6 @@ class EtoolkitInstance:
macros = {
'%h': os.path.expanduser('~'),
'%i': self.name,
- # '%f': self.get_full_name(),
'%u': getpass.getuser(),
}
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 @@
# etoolkit
-# Copyright (C) 2021-2022 Simeon Simeonov
+# Copyright (C) 2021-2024 Simeon Simeonov
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -14,14 +14,16 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Common fixtures"""
+
import json
import pytest
-@pytest.fixture
+@pytest.fixture()
def config_data():
"""config_data for testing"""
+
return {
'general': {
'MASTER_PASSWORD_HASH': (
@@ -51,26 +53,47 @@ def config_data():
}
-@pytest.fixture
+@pytest.fixture()
def config_file(tmp_path, config_data):
"""temporary config file for testing that includes config_data"""
- cf = tmp_path / "etoolkit.json"
+
+ cf = tmp_path / 'etoolkit.json'
cf.write_text(json.dumps(config_data))
return str(cf)
-@pytest.fixture
+@pytest.fixture()
def non_random_bytes_32():
"""always use the same bytes instead of os.urandom(32)"""
+
return (
b'\xb9\x8aY3U_\x00j\xb4\x086K\xd9\xdb\x88N'
b'\xcd;\xe8$#\xfa\x12\x05\x12\x0c~\x17\x89\xa7\xf3J'
)
-@pytest.fixture
+@pytest.fixture()
+def non_random_bytes_61():
+ """always use the same bytes instead of os.urandom(61)"""
+
+ return (
+ b'D$\x99\xaa\xafiZ\xb4C\xa0%XTz)\xca\xedK\xcd\xa2F~\xff+\xa1[\xe2\xaa'
+ b'\xb2\xd3\x07\x13\xedb\xc2\x84\xfe\tS\r\xf0\x02_\xef\xe3\xde\xf1?e'
+ b'\xa4s(Q\x04\xcd\xc7T\x01_D\xb1'
+ )
+
+
+@pytest.fixture()
+def nonexistent_config_file(tmp_path):
+ """temporary config file for testing that includes config_data"""
+
+ return str(tmp_path / 'etoolkitt.json')
+
+
+@pytest.fixture()
def password_hash():
"""password hash for testing, corresponding to 'The very secret passwd'"""
+
return (
'pbkdf2_sha256$100000$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4mn80o=$h3'
'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 @@
# etoolkit
-# Copyright (C) 2021-2022 Simeon Simeonov
+# Copyright (C) 2021-2024 Simeon Simeonov
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -14,6 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Tests for the CLI (etoolkit.__main__"""
+
import errno
import os
import unittest.mock
@@ -24,11 +25,9 @@ import etoolkit
from etoolkit.__main__ import main
-@unittest.mock.patch('os.urandom')
@unittest.mock.patch('builtins.input')
-def test_decrypt(binput, urandom, capsys, non_random_bytes_32, config_file):
- """Tests encryption via the CLI interface"""
- urandom.return_value = non_random_bytes_32
+def test_decrypt_v1(binput, capsys, config_file):
+ """Tests v1 decryption via the CLI interface"""
binput.return_value = (
'enc-val$1$uYpZM1VfAGq0CDZL2duITs076CQj+'
'hIFEgx+F4mn80o=$xdF/1S+R2MGlEQMCOLG6OjEuzw=='
@@ -43,11 +42,29 @@ def test_decrypt(binput, urandom, capsys, non_random_bytes_32, config_file):
assert capsys.readouterr().out.strip() == 'Decrypted value: bar'
+@unittest.mock.patch('builtins.input')
+def test_decrypt_v2(binput, capsys, config_file):
+ """Tests v2 decryption via the CLI interface"""
+ binput.return_value = (
+ 'enc-val$2$RCSZqq9pWrRDoCVYVHopyu1LzaJGfv8roVviq'
+ 'rLTBxM=$VW3UZ6l12yDtyaqWHb7i0QEDiS9s9np'
+ '7huAACK54BtZVV7RZoIhbu4K6zZuz+LRCyio='
+ )
+ with unittest.mock.patch.dict(
+ os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'}
+ ):
+ with pytest.raises(SystemExit) as exit_info:
+ main(['-c', f'{config_file}', '-d'])
+ assert exit_info.type == SystemExit
+ assert exit_info.value.code == 0
+ assert capsys.readouterr().out.strip() == 'Decrypted value: bar'
+
+
@unittest.mock.patch('os.urandom')
@unittest.mock.patch('builtins.input', lambda *args: 'bar')
-def test_encrypt_with_echo(urandom, capsys, non_random_bytes_32, config_file):
+def test_encrypt_with_echo(urandom, capsys, non_random_bytes_61, config_file):
"""Tests encryption via the CLI interface"""
- urandom.return_value = non_random_bytes_32
+ urandom.return_value = non_random_bytes_61
with unittest.mock.patch.dict(
os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'}
):
@@ -56,16 +73,17 @@ def test_encrypt_with_echo(urandom, capsys, non_random_bytes_32, config_file):
assert exit_info.type == SystemExit
assert exit_info.value.code == 0
assert capsys.readouterr().out.strip() == (
- 'Encrypted value: enc-val$1$uYpZM1VfAGq0CDZL2duITs076CQj+'
- 'hIFEgx+F4mn80o=$xdF/1S+R2MGlEQMCOLG6OjEuzw=='
+ 'Encrypted value: enc-val$2$RCSZqq9pWrRDoCVYVHopyu1LzaJGfv8roVviq'
+ 'rLTBxM=$VW3UZ6l12yDtyaqWHb7i0QEDiS9s9np'
+ '7huAACK54BtZVV7RZoIhbu4K6zZuz+LRCyio='
)
@unittest.mock.patch('os.urandom')
@unittest.mock.patch('getpass.getpass', lambda *args: 'bar')
-def test_encrypt_without_echo(gpass, capsys, non_random_bytes_32, config_file):
+def test_encrypt_without_echo(gpass, capsys, non_random_bytes_61, config_file):
"""Tests encryption via the CLI interface"""
- gpass.return_value = non_random_bytes_32
+ gpass.return_value = non_random_bytes_61
with unittest.mock.patch.dict(
os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'}
):
@@ -74,19 +92,20 @@ def test_encrypt_without_echo(gpass, capsys, non_random_bytes_32, config_file):
assert exit_info.type == SystemExit
assert exit_info.value.code == 0
assert capsys.readouterr().out.strip() == (
- 'Encrypted value: enc-val$1$uYpZM1VfAGq0CDZL2duITs076CQj+'
- 'hIFEgx+F4mn80o=$xdF/1S+R2MGlEQMCOLG6OjEuzw=='
+ 'Encrypted value: enc-val$2$RCSZqq9pWrRDoCVYVHopyu1LzaJGfv8roVviq'
+ 'rLTBxM=$VW3UZ6l12yDtyaqWHb7i0QEDiS9s9np'
+ '7huAACK54BtZVV7RZoIhbu4K6zZuz+LRCyio='
)
-def test_list(capsys, config_file):
+def test_list(capsys, config_file, nonexistent_config_file):
"""Tests list via the CLI interface"""
with pytest.raises(SystemExit) as exit_info:
- main(['-l'])
+ main(['-c', nonexistent_config_file, '-l'])
assert exit_info.type == SystemExit
assert exit_info.value.code == errno.EIO
with pytest.raises(SystemExit) as exit_info:
- main(['-c', f'{config_file}', '-l'])
+ main(['-c', config_file, '-l'])
assert exit_info.type == SystemExit
assert exit_info.value.code == 0
assert capsys.readouterr().out.strip() == f'dev{os.linesep}secret'
@@ -104,10 +123,7 @@ def test_help(capsys):
@unittest.mock.patch('os.urandom')
@unittest.mock.patch('getpass.getpass')
def test_generate_master_password_hash(
- gpass,
- urandom,
- capsys,
- non_random_bytes_32,
+ gpass, urandom, capsys, non_random_bytes_32
):
"""Tests master password hash generation via the CLI interface"""
urandom.return_value = non_random_bytes_32
@@ -117,8 +133,8 @@ def test_generate_master_password_hash(
assert exit_info.type == SystemExit
assert exit_info.value.code == 0
assert capsys.readouterr().out.strip() == (
- 'Master password hash: pbkdf2_sha256$100000$uYpZM1VfAGq0CDZL2duITs076'
- 'CQj+hIFEgx+F4mn80o=$h3PSPLCd37fP15zKdW4CBGn7CXE+q5UiydaF3vbeZHo='
+ 'Master password hash: pbkdf2_sha256$500000$uYpZM1VfAGq0CDZL2duITs076'
+ 'CQj+hIFEgx+F4mn80o=$Msl8/5nOBj0TRchykMzXmCXR8VQVyBqUPHe1PDWeJi8='
)
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 @@
# etoolkit
-# Copyright (C) 2021-2022 Simeon Simeonov
+# Copyright (C) 2021-2024 Simeon Simeonov
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -14,6 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Tests for etoolkit.EtoolkitInstance"""
+
import pytest
import etoolkit
@@ -32,8 +33,9 @@ def test_instantiation(config_data):
assert 'DB_CONNECTION' not in instance.sensitive_env_variables
assert 'PASSWORD' in instance.sensitive_env_variables
assert instance.name == 'secret'
- assert instance.master_password_hash == (
- config_data['general']['MASTER_PASSWORD_HASH']
+ assert (
+ instance.master_password_hash
+ == (config_data['general']['MASTER_PASSWORD_HASH'])
)
assert instance.master_password is None
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 @@
# etoolkit
-# Copyright (C) 2021-2022 Simeon Simeonov
+# Copyright (C) 2021-2024 Simeon Simeonov
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -14,6 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Tests for etoolkit.EtoolkitInstance static methods"""
+
import unittest.mock
import pytest
@@ -35,7 +36,7 @@ def test_confirm_password_prompt(getpass, password_hash):
)
-def test_decrypt():
+def test_decrypt_v1():
"""Tests the static EtoolkitInstance.decrypt method"""
assert (
etoolkit.EtoolkitInstance.decrypt(
@@ -49,39 +50,118 @@ def test_decrypt():
)
# now test with modified edata
+ edata = (
+ 'enc-val$1$/cXpEMoZrTlb9yokGhw8tLTSUkqnqJ5ZoAkurNgMYx'
+ 'w=$1VdkSMcZnLRwLiu1M8VlYcbelwmiVNY='
+ )
with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info:
- edata = (
- 'enc-val$1$/cXpEMoZrTlb9yokGhw8tLTSUkqnqJ5ZoAkurNgMYx'
- 'w=$1VdkSMcZnLRwLiu1M8VlYcbelwmiVNY='
+ etoolkit.EtoolkitInstance.decrypt('The very secret passwd', edata)
+ assert exc_info.type is etoolkit.EtoolkitInstanceError
+ assert exc_info.value.args[0] == f'Invalid tag when decrypting: {edata}'
+
+
+def test_decrypt_v2_no_padding():
+ """Tests the static EtoolkitInstance.decrypt method for v2 - no padding"""
+ assert (
+ etoolkit.EtoolkitInstance.decrypt(
+ 'The very secret passwd',
+ (
+ 'enc-val$2$Wer5lECGyeZhhYS58N18WVx5Zzy+rrC+BPlq3Dw89wQ=$'
+ 'SQc0ox6Emf2m5rrumsiptpIZEujdpXXSR/'
+ '1VcfEZeBz4+KDSagr9ID+bkc4R2yFdxHnhig1eqQ8='
+ ),
)
+ == 'Nobody expects the Spanish inquisition'
+ )
+
+ # now test with modified edata
+ edata = (
+ 'enc-val$2$Wer5lECGyeZhhYS58N18WVx5Zzy+rrC+BPlq3Dw89wQ=$'
+ 'SQc0ox6Emf2m4rrumsiptpIZEujdpXXSR/'
+ '1VcfEZeBz4+KDSagr9ID+bkc4R2yFdxHnhig1eqQ8='
+ )
+ with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info:
+ etoolkit.EtoolkitInstance.decrypt('The very secret passwd', edata)
+ assert exc_info.type is etoolkit.EtoolkitInstanceError
+ assert exc_info.value.args[0] == f'Invalid tag when decrypting: {edata}'
+
+
+def test_decrypt_v2_with_padding():
+ """Tests the static EtoolkitInstance.decrypt method for v2 with padding"""
+ assert (
etoolkit.EtoolkitInstance.decrypt(
- 'The very secret passwd', edata
- ) == 'secret1'
+ 'The very secret passwd',
+ (
+ 'enc-val$2$//kzyUbDEWNoPC5dyukhB8de8+IVaLR2ngx2HwkfOuM=$'
+ 'rhRona4wP9nhnXjcHqwkjFDsiVVVjYanAs'
+ 'N4kknNkgC0ix4RtJQHYDeTzw1rrR1vb2w='
+ ),
+ )
+ == 'secret1'
+ )
+
+ # now test with modified edata
+ edata = (
+ 'enc-val$2$//kzyUbDEWNoPC5dyukhB8de8+IVaLR2ngx2HwkfOuM=$'
+ 'rhRona4wP8nhnXjcHqwkjFDsiVVVjYanAsN4kknNkgC0ix4RtJQHYDeTzw1rrR1vb2w='
+ )
+ with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info:
+ etoolkit.EtoolkitInstance.decrypt('The very secret passwd', edata)
assert exc_info.type is etoolkit.EtoolkitInstanceError
assert exc_info.value.args[0] == f'Invalid tag when decrypting: {edata}'
-def test_encrypt():
- """Tests the static EtoolkitInstance.encrypt method"""
+def test_encrypt_no_padding():
+ """Tests the static EtoolkitInstance.encrypt method with a long string"""
+ edata = etoolkit.EtoolkitInstance.encrypt(
+ 'foo', 'Nobody expects the Spanish inquisition'
+ )
+ assert edata.startswith('enc-val$2$')
+ assert len(edata) == 131
+ # the edata should always be different because of random salting
+ assert edata != etoolkit.EtoolkitInstance.encrypt(
+ 'foo', 'Nobody expects the Spanish inquisition'
+ )
+
+
+def test_encrypt_with_padding():
+ """Tests the static EtoolkitInstance.encrypt method with a short string"""
edata = etoolkit.EtoolkitInstance.encrypt('foo', 'bar')
- assert edata.startswith('enc-val$')
- assert len(edata) == 83
+ assert edata.startswith('enc-val$2$')
+ assert len(edata) == 123
# the edata should always be different because of random salting
assert edata != etoolkit.EtoolkitInstance.encrypt('foo', 'bar')
@unittest.mock.patch('os.urandom')
-def test_encrypt_staticly(urandom, non_random_bytes_32):
+def test_encrypt_staticly_no_padding(urandom, non_random_bytes_32):
"""Tests the EtoolkitInstance.encrypt method always with the same salt"""
urandom.return_value = non_random_bytes_32
+ edata = etoolkit.EtoolkitInstance.encrypt(
+ 'The very secret passwd', 'Nobody expects the Spanish inquisition'
+ )
+ assert edata == (
+ 'enc-val$2$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4mn80o=$'
+ 'UX/5YeRsh5/2vZ2J1UOS+BJti73Kbp6C1pJmC'
+ 'o8hFSujpe35X/XpzAiYv4BV1LNwnSYECsotsgs='
+ )
+ assert len(edata) == 131
+ assert edata == etoolkit.EtoolkitInstance.encrypt(
+ 'The very secret passwd', 'Nobody expects the Spanish inquisition'
+ )
+
+
+@unittest.mock.patch('os.urandom')
+def test_encrypt_staticly_with_padding(urandom, non_random_bytes_61):
+ """Tests the EtoolkitInstance.encrypt method always with the same salt"""
+ urandom.return_value = non_random_bytes_61
edata = etoolkit.EtoolkitInstance.encrypt('The very secret passwd', 'bar')
assert edata == (
- 'enc-val$1$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4mn80'
- 'o=$HjPFNv6xC5hbMrFc0L5lSkWdfQ=='
+ 'enc-val$2$RCSZqq9pWrRDoCVYVHopyu1LzaJGfv8roVviqrLTBxM=$'
+ '+Yo6Ya2MAVcBLTQHuATkyFc+dzYsL/ESvA6ofOUDsiKZvIff35cUHAmoNxVuGG+MXv4='
)
assert edata == etoolkit.EtoolkitInstance.encrypt(
- 'The very secret passwd',
- 'bar',
+ 'The very secret passwd', 'bar'
)
@@ -91,7 +171,7 @@ def test_get_new_password_hash():
'The very secret passwd'
)
# all pbkdf2 params are the same / hardcoded for the time being
- assert new_hash.startswith('pbkdf2_sha256$100000$')
+ assert new_hash.startswith('pbkdf2_sha256$500000$')
assert len(new_hash) == 110
# the hash should always be different because of random salting
assert new_hash != etoolkit.EtoolkitInstance.get_new_password_hash(
--
cgit v1.3