From 08a3280b062af83ee50fa139d7827d954907886e Mon Sep 17 00:00:00 2001 From: Simeon Simeonov Date: Mon, 13 May 2024 21:52:13 +0200 Subject: Implement re-encryption support --- tests/conftest.py | 61 ++++++++++++-- tests/test_cli.py | 57 ++++++++----- tests/test_envtoolkit_instance.py | 6 +- tests/test_envtoolkit_instance_static.py | 136 ++++++++++++++++++------------- 4 files changed, 173 insertions(+), 87 deletions(-) (limited to 'tests') diff --git a/tests/conftest.py b/tests/conftest.py index cbf7152..37dd184 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -45,9 +45,9 @@ def config_data(): 'ETOOLKIT_SENSITIVE': ['ETOOLKIT_TEST_PASSWORD'], 'GNUPGHOME': '%h/private/.gnupg', 'ETOOLKIT_TEST_PASSWORD': ( - 'enc-val$2$v6F2M7LeUDbQWNLg6WW5mUcbuYYo7aGynSxzWAENVBI=$' - 'ZcyWzf9Kp0aYI8N+biKMSmu4RGGi199ayq' - 'EYdJl+qdq7b1HSutwYlC7UR2GsSofu4Xo=' + 'enc-val$2$RCSZqq9pWrRDoCVYVHopyu1LzaJGfv8roVviqrLTBxM=$' + '+YYrZbwTBuG0Pl+WMQrvxLUtq5j8qYuQqz' + 'oIwgoGt7AaWZCJz+E7qoDeg3wke70ST8U=' ), }, }, @@ -63,12 +63,35 @@ def config_file(tmp_path, config_data): return str(cf) +@pytest.fixture() +def long_encrypted_value(): + """enc. value corresponding to 'Nobody expects the Spanish inquisition'""" + + return ( + 'enc-val$2$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4mn80o=$' + 'UWP5YeRsh5/2vZ2J1UOS+BJti73Kbp6C1pJmCo8hF' + 'Sujpe35X/XpzBegJJpo86AiCsNsUS6B6JM=' + ) + + +@pytest.fixture() +def long_value(): + """standard value (> 32 bytes)""" + return 'Nobody expects the Spanish inquisition' + + @pytest.fixture() def master_password(): """Master passord""" return 'The very secret passwd' +@pytest.fixture() +def new_master_password(): + """Master passord""" + return 'New very secret passwd' + + @pytest.fixture() def non_random_bytes_32(): """always use the same bytes instead of os.urandom(32)""" @@ -80,13 +103,13 @@ def non_random_bytes_32(): @pytest.fixture() -def non_random_bytes_61(): - """always use the same bytes instead of os.urandom(61)""" +def non_random_bytes_57(): + """always use the same bytes instead of os.urandom(57)""" 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' + b'\xa4s(Q\x04\xcd\xc7T' ) @@ -107,6 +130,32 @@ def password_hash(): ) +@pytest.fixture() +def short_encrypted_value(): + """enc. value corresponding to 'secret1'""" + + return ( + 'enc-val$2$RCSZqq9pWrRDoCVYVHopyu1LzaJGfv8roVviqrLTBxM=$' + '+YYrZbwTBuG0Pl+WMQrvxLUtq5j8qYuQqzoIwgoGt7AaWZCJz+E7qoDeg3wke70ST8U=' + ) + + +@pytest.fixture() +def short_encrypted_value_v1(): + """enc. value (enc-val 1) corresponding to 'secret1'""" + + return ( + 'enc-val$1$/cXpEMoZrTlb9yokGhw8tLTSUkqnqJ4ZoAkurNgMYx' + 'w=$1VdkSMcZnLRwLiu1M8VlYcbelwmiVNY=' + ) + + +@pytest.fixture() +def short_value(): + """standard value (< 32 bytes)""" + return 'secret1' + + @pytest.fixture() def wrong_master_password(): """Wrong master passord""" diff --git a/tests/test_cli.py b/tests/test_cli.py index 1693a2b..953abf4 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -44,14 +44,17 @@ def test_decrypt_v1(binput, capsys, config_file, master_password): @unittest.mock.patch('builtins.input') -def test_decrypt_v2(binput, capsys, config_file, master_password): +def test_decrypt_v2( + binput, + capsys, + config_file, + master_password, + short_encrypted_value, + short_value, +): """Tests v2 decryption via the CLI interface""" - binput.return_value = ( - 'enc-val$2$RCSZqq9pWrRDoCVYVHopyu1LzaJGfv8roVviq' - 'rLTBxM=$+Yo6Ya2MAVcBLTQHuATkyFc+dzYsL/E' - 'SvA6ofOUDsiKZvIff35cUHAmoNxVuGG+MXv4=' - ) + binput.return_value = short_encrypted_value with unittest.mock.patch.dict( os.environ, {'ETOOLKIT_MASTER_PASSWORD': master_password} ): @@ -59,18 +62,27 @@ def test_decrypt_v2(binput, capsys, config_file, master_password): 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' + assert capsys.readouterr().out.strip() == ( + f'Decrypted value: {short_value}' + ) @unittest.mock.patch('os.urandom') @unittest.mock.patch('builtins.input') def test_encrypt_with_echo( - binput, urandom, capsys, non_random_bytes_61, config_file, master_password + binput, + urandom, + capsys, + non_random_bytes_57, + config_file, + master_password, + short_encrypted_value, + short_value, ): """Tests encryption via the CLI interface""" - binput.return_value = 'bar' - urandom.return_value = non_random_bytes_61 + binput.return_value = short_value + urandom.return_value = non_random_bytes_57 with unittest.mock.patch.dict( os.environ, {'ETOOLKIT_MASTER_PASSWORD': master_password} ): @@ -79,21 +91,26 @@ def test_encrypt_with_echo( assert exit_info.type == SystemExit assert exit_info.value.code == 0 assert capsys.readouterr().out.strip() == ( - 'Encrypted value: enc-val$2$RCSZqq9pWrRDoCVYVHopyu1LzaJGfv8roVviq' - 'rLTBxM=$+Yo6Ya2MAVcBLTQHuATkyFc+dzYsL/E' - 'SvA6ofOUDsiKZvIff35cUHAmoNxVuGG+MXv4=' + f'Encrypted value: {short_encrypted_value}' ) @unittest.mock.patch('os.urandom') @unittest.mock.patch('getpass.getpass') def test_encrypt_without_echo( - getpass, urandom, capsys, non_random_bytes_61, config_file, master_password + getpass, + urandom, + capsys, + non_random_bytes_57, + config_file, + master_password, + short_encrypted_value, + short_value, ): """Tests encryption via the CLI interface""" - getpass.return_value = 'bar' - urandom.return_value = non_random_bytes_61 + getpass.return_value = short_value + urandom.return_value = non_random_bytes_57 with unittest.mock.patch.dict( os.environ, {'ETOOLKIT_MASTER_PASSWORD': master_password} ): @@ -102,13 +119,11 @@ def test_encrypt_without_echo( assert exit_info.type == SystemExit assert exit_info.value.code == 0 assert capsys.readouterr().out.strip() == ( - 'Encrypted value: enc-val$2$RCSZqq9pWrRDoCVYVHopyu1LzaJGfv8roVviq' - 'rLTBxM=$+Yo6Ya2MAVcBLTQHuATkyFc+dzYsL/E' - 'SvA6ofOUDsiKZvIff35cUHAmoNxVuGG+MXv4=' + f'Encrypted value: {short_encrypted_value}' ) -def test_fetch_encrypted_value(config_file, master_password): +def test_fetch_encrypted_value(config_file, master_password, short_value): """Tests decryption of encrypted value""" with unittest.mock.patch.dict( @@ -116,7 +131,7 @@ def test_fetch_encrypted_value(config_file, master_password): ): assert os.environ.get('ETOOLKIT_TEST_PASSWORD') is None main(['-c', f'{config_file}', '-q', '-s', '/bin/false', 'secret']) - assert os.environ.get('ETOOLKIT_TEST_PASSWORD') == 'bar' + assert os.environ.get('ETOOLKIT_TEST_PASSWORD') == short_value def test_list(capsys, config_file, nonexistent_config_file): diff --git a/tests/test_envtoolkit_instance.py b/tests/test_envtoolkit_instance.py index 0147c6f..9873d57 100644 --- a/tests/test_envtoolkit_instance.py +++ b/tests/test_envtoolkit_instance.py @@ -41,7 +41,9 @@ def test_instantiation(config_data): assert instance.master_password is None -def test_get_environ(config_data, master_password, wrong_master_password): +def test_get_environ( + config_data, master_password, short_value, wrong_master_password +): """Tests the EtoolkitInstance.get_environ method""" instance = etoolkit.EtoolkitInstance('secret', config_data) @@ -61,7 +63,7 @@ def test_get_environ(config_data, master_password, wrong_master_password): instance.master_password = master_password env = instance.get_environ() assert isinstance(env, dict) - assert env['ETOOLKIT_TEST_PASSWORD'] == 'bar' + assert env['ETOOLKIT_TEST_PASSWORD'] == short_value def test_get_full_name(config_data): diff --git a/tests/test_envtoolkit_instance_static.py b/tests/test_envtoolkit_instance_static.py index b3b24f0..ee5cf9e 100644 --- a/tests/test_envtoolkit_instance_static.py +++ b/tests/test_envtoolkit_instance_static.py @@ -37,18 +37,14 @@ def test_confirm_password_prompt(getpass, password_hash, master_password): ) -def test_decrypt_v1(master_password): +def test_decrypt_v1(master_password, short_encrypted_value_v1, short_value): """Tests the static EtoolkitInstance.decrypt method""" assert ( etoolkit.EtoolkitInstance.decrypt( - master_password, - ( - 'enc-val$1$/cXpEMoZrTlb9yokGhw8tLTSUkqnqJ4ZoAkurNgMYx' - 'w=$1VdkSMcZnLRwLiu1M8VlYcbelwmiVNY=' - ), + master_password, short_encrypted_value_v1 ) - == 'secret1' + == short_value ) # now test with modified edata @@ -62,117 +58,106 @@ def test_decrypt_v1(master_password): assert exc_info.value.args[0] == f'Invalid tag when decrypting: {edata}' -def test_decrypt_v2_no_padding(master_password): +def test_decrypt_v2_no_padding( + master_password, long_encrypted_value, long_value +): """Tests the static EtoolkitInstance.decrypt method for v2 - no padding""" assert ( etoolkit.EtoolkitInstance.decrypt( - master_password, - ( - 'enc-val$2$Wer5lECGyeZhhYS58N18WVx5Zzy+rrC+BPlq3Dw89wQ=$' - 'SQc0ox6Emf2m5rrumsiptpIZEujdpXXSR/' - '1VcfEZeBz4+KDSagr9ID+bkc4R2yFdxHnhig1eqQ8=' - ), + master_password, long_encrypted_value ) - == 'Nobody expects the Spanish inquisition' + == long_value ) - # now test with modified edata - edata = ( - 'enc-val$2$Wer5lECGyeZhhYS58N18WVx5Zzy+rrC+BPlq3Dw89wQ=$' - 'SQc0ox6Emf2m4rrumsiptpIZEujdpXXSR/' - '1VcfEZeBz4+KDSagr9ID+bkc4R2yFdxHnhig1eqQ8=' - ) + # now test with modified encrypted data + edata = long_encrypted_value[:60] + '5' + long_encrypted_value[61:] + with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info: etoolkit.EtoolkitInstance.decrypt(master_password, 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(master_password): +def test_decrypt_v2_with_padding( + master_password, short_encrypted_value, short_value +): """Tests the static EtoolkitInstance.decrypt method for v2 with padding""" assert ( etoolkit.EtoolkitInstance.decrypt( - master_password, - ( - 'enc-val$2$//kzyUbDEWNoPC5dyukhB8de8+IVaLR2ngx2HwkfOuM=$' - 'rhRona4wP9nhnXjcHqwkjFDsiVVVjYanAs' - 'N4kknNkgC0ix4RtJQHYDeTzw1rrR1vb2w=' - ), + master_password, short_encrypted_value ) - == 'secret1' + == short_value ) # now test with modified edata - edata = ( - 'enc-val$2$//kzyUbDEWNoPC5dyukhB8de8+IVaLR2ngx2HwkfOuM=$' - 'rhRona4wP8nhnXjcHqwkjFDsiVVVjYanAsN4kknNkgC0ix4RtJQHYDeTzw1rrR1vb2w=' - ) + edata = short_encrypted_value[:60] + '5' + short_encrypted_value[61:] with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info: etoolkit.EtoolkitInstance.decrypt(master_password, edata) assert exc_info.type is etoolkit.EtoolkitInstanceError assert exc_info.value.args[0] == f'Invalid tag when decrypting: {edata}' -def test_encrypt_no_padding(master_password): +def test_encrypt_no_padding(master_password, long_value): """Tests the static EtoolkitInstance.encrypt method with a long string""" - edata = etoolkit.EtoolkitInstance.encrypt( - master_password, 'Nobody expects the Spanish inquisition' - ) + edata = etoolkit.EtoolkitInstance.encrypt(master_password, long_value) 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( - master_password, 'Nobody expects the Spanish inquisition' + master_password, long_value ) -def test_encrypt_with_padding(master_password): +def test_encrypt_with_padding(master_password, short_value): """Tests the static EtoolkitInstance.encrypt method with a short string""" - edata = etoolkit.EtoolkitInstance.encrypt(master_password, 'bar') + edata = etoolkit.EtoolkitInstance.encrypt(master_password, short_value) 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(master_password, 'bar') + assert edata != etoolkit.EtoolkitInstance.encrypt( + master_password, short_value + ) @unittest.mock.patch('os.urandom') def test_encrypt_staticly_no_padding( - urandom, master_password, non_random_bytes_32 + urandom, + master_password, + non_random_bytes_32, + long_encrypted_value, + long_value, ): """Tests the EtoolkitInstance.encrypt method always with the same salt""" urandom.return_value = non_random_bytes_32 - edata = etoolkit.EtoolkitInstance.encrypt( - master_password, 'Nobody expects the Spanish inquisition' - ) - assert edata == ( - 'enc-val$2$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4mn80o=$' - 'UX/5YeRsh5/2vZ2J1UOS+BJti73Kbp6C1pJmC' - 'o8hFSujpe35X/XpzAiYv4BV1LNwnSYECsotsgs=' - ) + edata = etoolkit.EtoolkitInstance.encrypt(master_password, long_value) + assert edata == long_encrypted_value assert len(edata) == 131 assert edata == etoolkit.EtoolkitInstance.encrypt( - master_password, 'Nobody expects the Spanish inquisition' + master_password, long_value ) @unittest.mock.patch('os.urandom') def test_encrypt_staticly_with_padding( - urandom, master_password, non_random_bytes_61 + urandom, + master_password, + non_random_bytes_57, + short_encrypted_value, + short_value, ): """Tests the EtoolkitInstance.encrypt method always with the same salt""" - urandom.return_value = non_random_bytes_61 - edata = etoolkit.EtoolkitInstance.encrypt(master_password, 'bar') - assert edata == ( - 'enc-val$2$RCSZqq9pWrRDoCVYVHopyu1LzaJGfv8roVviqrLTBxM=$' - '+Yo6Ya2MAVcBLTQHuATkyFc+dzYsL/ESvA6ofOUDsiKZvIff35cUHAmoNxVuGG+MXv4=' + urandom.return_value = non_random_bytes_57 + edata = etoolkit.EtoolkitInstance.encrypt(master_password, short_value) + assert edata == short_encrypted_value + assert edata == etoolkit.EtoolkitInstance.encrypt( + master_password, short_value ) - assert edata == etoolkit.EtoolkitInstance.encrypt(master_password, 'bar') def test_get_new_password_hash(master_password): @@ -208,3 +193,38 @@ def test_password_matches( assert not etoolkit.EtoolkitInstance.password_matches( wrong_master_password, password_hash ) + + +@unittest.mock.patch('os.urandom') +def test_reencrypt_staticly_with_padding( + urandom, + master_password, + new_master_password, + non_random_bytes_57, + short_encrypted_value, + short_encrypted_value_v1, + short_value, +): + """Tests the EtoolkitInstance.reencrypt method always with the same salt""" + + urandom.return_value = non_random_bytes_57 + # reencrypt (migrate) v1 to current using the same password + edata = etoolkit.EtoolkitInstance.reencrypt( + master_password, master_password, short_encrypted_value_v1 + ) + assert edata == short_encrypted_value + + # same version, same salt, same edata + assert edata == etoolkit.EtoolkitInstance.reencrypt( + master_password, master_password, edata + ) + + # use different password + edata = etoolkit.EtoolkitInstance.reencrypt( + master_password, new_master_password, edata + ) + assert edata != short_encrypted_value + assert ( + etoolkit.EtoolkitInstance.decrypt(new_master_password, edata) + == short_value + ) -- cgit v1.3