diff --git a/envcloak/encryptor.py b/envcloak/encryptor.py index fb37475..2219836 100644 --- a/envcloak/encryptor.py +++ b/envcloak/encryptor.py @@ -14,6 +14,7 @@ from cryptography.hazmat.primitives import hashes from cryptography.hazmat.backends import default_backend import click +import secrets from click import style from envcloak.exceptions import ( InvalidSaltException, @@ -58,7 +59,7 @@ def generate_salt() -> bytes: :return: Randomly generated salt (16 bytes). """ try: - return os.urandom(SALT_SIZE) + return secrets.token_bytes(SALT_SIZE) except Exception as e: raise EncryptionException(details=f"Failed to generate salt: {str(e)}") from e @@ -72,7 +73,7 @@ def encrypt(data: str, key: bytes) -> dict: :return: Dictionary with encrypted data, nonce, and associated metadata. """ try: - nonce = os.urandom(NONCE_SIZE) # Generate a secure random nonce + nonce = secrets.token_bytes(NONCE_SIZE) # Generate a secure random nonce cipher = Cipher( algorithms.AES(key), modes.GCM(nonce), backend=default_backend() ) diff --git a/envcloak/generator.py b/envcloak/generator.py index 1ac55e2..fdeaf27 100644 --- a/envcloak/generator.py +++ b/envcloak/generator.py @@ -9,13 +9,14 @@ import os from pathlib import Path from .encryptor import derive_key +import secrets def generate_key_file(output_path: Path): """ Generate a secure random encryption key, save it to a file. """ - key = os.urandom(32) # Generate a 256-bit random key + key = secrets.token_bytes(32) # Generate a 256-bit random key output_path.parent.mkdir(parents=True, exist_ok=True) # Ensure directory exists with open(output_path, "wb") as key_file: key_file.write(key) @@ -36,7 +37,7 @@ def generate_key_from_password_file(password: str, output_path: Path, salt: str raise ValueError("Salt must be 16 bytes (32 hex characters).") salt_bytes = bytes.fromhex(salt) else: - salt_bytes = os.urandom(16) # Generate a random 16-byte salt + salt_bytes = secrets.token_bytes(16) # Generate a random 16-byte salt # Derive the key key = derive_key(password, salt_bytes) diff --git a/tests/test_cli_dry_run.py b/tests/test_cli_dry_run.py index 0b650eb..734e6a4 100644 --- a/tests/test_cli_dry_run.py +++ b/tests/test_cli_dry_run.py @@ -3,10 +3,12 @@ import pytest import shutil import tempfile +import secrets from click.testing import CliRunner from envcloak.cli import main + @pytest.fixture def mock_files(isolated_mock_files): """ @@ -148,7 +150,7 @@ def test_rotate_keys_dry_run(runner, mock_files): """ _, encrypted_file, key_file, directory = mock_files new_key_file = directory / "newkey.key" - new_key_file.write_bytes(os.urandom(32)) + new_key_file.write_bytes(secrets.token_bytes(32)) output_file = str(encrypted_file).replace(".enc", ".rotated") result = runner.invoke( diff --git a/tests/test_cli_generate_key.py b/tests/test_cli_generate_key.py index 3df89bc..2008646 100644 --- a/tests/test_cli_generate_key.py +++ b/tests/test_cli_generate_key.py @@ -1,4 +1,5 @@ import os +import secrets from unittest.mock import patch from click.testing import CliRunner import pytest @@ -173,7 +174,7 @@ def mock_create_key_from_password(password, output_path, salt): temp_key_file.unlink() -@patch("envcloak.generator.os.urandom") +@patch("envcloak.generator.secrets.token_bytes") @patch("envcloak.generator.derive_key") def test_generate_key_from_password_random_salt( mock_derive_key, diff --git a/tests/test_cli_rotate_keys.py b/tests/test_cli_rotate_keys.py index 7ff051a..71e8762 100644 --- a/tests/test_cli_rotate_keys.py +++ b/tests/test_cli_rotate_keys.py @@ -1,4 +1,5 @@ import os +import secrets import json from unittest.mock import patch from click.testing import CliRunner @@ -19,7 +20,7 @@ def test_rotate_keys(mock_encrypt_file, mock_decrypt_file, runner, isolated_mock temp_decrypted_file = isolated_mock_files / "temp_variables.decrypted" key_file = isolated_mock_files / "mykey.key" temp_new_key_file = key_file.with_name("temp_newkey.key") - temp_new_key_file.write_bytes(os.urandom(32)) + temp_new_key_file.write_bytes(secrets.token_bytes(32)) tmp_file = str(temp_decrypted_file) + ".tmp" diff --git a/tests/test_dynamic_analysis.py b/tests/test_dynamic_analysis.py index 8d0d9f6..2e5b0ca 100644 --- a/tests/test_dynamic_analysis.py +++ b/tests/test_dynamic_analysis.py @@ -1,4 +1,5 @@ import os +import secrets from hypothesis import given, strategies as st from envcloak.encryptor import encrypt, decrypt, encrypt_file, decrypt_file, derive_key from envcloak.loader import load_encrypted_env @@ -10,7 +11,7 @@ # Test Large Inputs for Encryption and Decryption @given(st.text(min_size=5, max_size=1000)) def test_large_input_encryption_decryption(large_text): - key = os.urandom(32) # Use a valid 32-byte key + key = secrets.token_bytes(32) # Use a valid 32-byte key encrypted = encrypt(large_text, key) decrypted = decrypt(encrypted, key) assert ( @@ -20,7 +21,7 @@ def test_large_input_encryption_decryption(large_text): # Test Empty Input for Encryption def test_empty_input_encryption(): - key = os.urandom(32) # Use a valid 32-byte key + key = secrets.token_bytes(32) # Use a valid 32-byte key try: encrypted = encrypt("", key) assert encrypted, "Empty input should still be encrypted successfully" @@ -51,7 +52,7 @@ def test_invalid_key_decryption(invalid_key): # Test Malformed Encrypted Input for Decryption @given(st.binary()) def test_malformed_encrypted_input(binary_data): - key = os.urandom(32) # Use a valid 32-byte key + key = secrets.token_bytes(32) # Use a valid 32-byte key try: decrypt(binary_data, key) assert False, "Decryption should fail for malformed input" @@ -62,7 +63,7 @@ def test_malformed_encrypted_input(binary_data): # Stress Test: Multiple Encryption-Decryption Cycles @given(st.text(min_size=10, max_size=100)) def test_multiple_encryption_decryption_cycles(plain_text): - key = os.urandom(32) # Use a valid 32-byte key + key = secrets.token_bytes(32) # Use a valid 32-byte key for _ in range(100): # Stress test with 100 cycles encrypted = encrypt(plain_text, key) plain_text = decrypt(encrypted, key) @@ -73,7 +74,7 @@ def test_multiple_encryption_decryption_cycles(plain_text): # Test Loading Encrypted Environment Variables def test_load_encrypted_env(): # Prepare mock files - key = os.urandom(32) # Use a valid 32-byte key + key = secrets.token_bytes(32) # Use a valid 32-byte key encrypted_file = "mock_variables.env.enc" key_file = "mock_key.key" @@ -113,7 +114,7 @@ def test_load_encrypted_env(): ) ) def test_randomized_env_file_content(env_data): - key = os.urandom(32) # Use a valid 32-byte key + key = secrets.token_bytes(32) # Use a valid 32-byte key encrypted_file = "random_env_file.enc" decrypted_file = "random_env_file_decrypted.env" input_file = "random_env_file.env" @@ -157,7 +158,7 @@ def test_randomized_env_file_content(env_data): ) ) def test_special_characters_in_env(env_data): - key = os.urandom(32) # Use a valid 32-byte key + key = secrets.token_bytes(32) # Use a valid 32-byte key encrypted_file = "special_env_file.enc" decrypted_file = "special_env_file_decrypted.env" input_file = "special_env_file.env" @@ -207,7 +208,7 @@ def test_key_derivation_from_password(password, salt): # Use a different password or salt different_password_key = derive_key(password + "1", salt) - different_salt_key = derive_key(password, os.urandom(16)) + different_salt_key = derive_key(password, secrets.token_bytes(16)) assert ( key1 != different_password_key @@ -217,7 +218,7 @@ def test_key_derivation_from_password(password, salt): @given(st.text(min_size=5, max_size=20)) def test_invalid_file_paths(file_name): - key = os.urandom(32) # Use a valid 32-byte key + key = secrets.token_bytes(32) # Use a valid 32-byte key try: load_encrypted_env(file_name, "nonexistent_key.key") assert False, "Loading should fail with nonexistent files" @@ -226,8 +227,8 @@ def test_invalid_file_paths(file_name): def test_key_rotation(): - key_old = os.urandom(32) - key_new = os.urandom(32) + key_old = secrets.token_bytes(32) + key_new = secrets.token_bytes(32) input_file = "key_rotation_test.env" encrypted_file_old = "key_rotation_test_old.enc" encrypted_file_new = "key_rotation_test_new.enc" diff --git a/tests/test_encryptor.py b/tests/test_encryptor.py index cc9c5ed..8c70ddd 100644 --- a/tests/test_encryptor.py +++ b/tests/test_encryptor.py @@ -1,4 +1,5 @@ import os +import secrets import base64 import json import pytest @@ -40,7 +41,7 @@ def test_derive_key_invalid_salt(read_variable): Test that derive_key raises an InvalidSaltException for invalid salt sizes. """ password = read_variable("pass6") - invalid_salt = os.urandom(SALT_SIZE - 1) # Smaller than expected + invalid_salt = secrets.token_bytes(SALT_SIZE - 1) # Smaller than expected with pytest.raises( InvalidSaltException, match=f"Expected salt of size {SALT_SIZE}, got {SALT_SIZE - 1} bytes.", @@ -52,7 +53,7 @@ def test_encrypt_and_decrypt(): """ Test that encrypting and decrypting a string works as expected. """ - key = os.urandom(KEY_SIZE) + key = secrets.token_bytes(KEY_SIZE) plaintext = "This is a test message." # Encrypt the data @@ -70,8 +71,8 @@ def test_encrypt_and_decrypt_invalid_key(): """ Test that decrypting with an incorrect key raises an error. """ - key = os.urandom(KEY_SIZE) - wrong_key = os.urandom(KEY_SIZE) + key = secrets.token_bytes(KEY_SIZE) + wrong_key = secrets.token_bytes(KEY_SIZE) plaintext = "This is a test message." encrypted_data = encrypt(plaintext, key) @@ -84,12 +85,12 @@ def test_encrypt_and_decrypt_invalid_data(): """ Test that decrypting with invalid encrypted data raises an error. """ - key = os.urandom(KEY_SIZE) + key = secrets.token_bytes(KEY_SIZE) invalid_data = { "ciphertext": base64.b64encode(b"invalid").decode(), - "nonce": base64.b64encode(os.urandom(NONCE_SIZE)).decode(), - "tag": base64.b64encode(os.urandom(16)).decode(), + "nonce": base64.b64encode(secrets.token_bytes(NONCE_SIZE)).decode(), + "tag": base64.b64encode(secrets.token_bytes(16)).decode(), } with pytest.raises(Exception): @@ -114,7 +115,7 @@ def test_encrypt_file(tmp_files): Test encrypting a file. """ plaintext_file, encrypted_file, _ = tmp_files - key = os.urandom(KEY_SIZE) + key = secrets.token_bytes(KEY_SIZE) encrypt_file(plaintext_file, encrypted_file, key) @@ -133,7 +134,7 @@ def test_decrypt_file(tmp_files): Test decrypting a file. """ plaintext_file, encrypted_file, decrypted_file = tmp_files - key = os.urandom(KEY_SIZE) + key = secrets.token_bytes(KEY_SIZE) # Encrypt and then decrypt the file encrypt_file(plaintext_file, encrypted_file, key) @@ -151,8 +152,8 @@ def test_encrypt_and_decrypt_file_invalid_key(tmp_files): Test decrypting a file with an invalid key. """ plaintext_file, encrypted_file, decrypted_file = tmp_files - key = os.urandom(KEY_SIZE) - wrong_key = os.urandom(KEY_SIZE) + key = secrets.token_bytes(KEY_SIZE) + wrong_key = secrets.token_bytes(KEY_SIZE) # Encrypt the file encrypt_file(plaintext_file, encrypted_file, key) diff --git a/tests/test_exceptions_encryptor.py b/tests/test_exceptions_encryptor.py index e398e52..6fa494f 100644 --- a/tests/test_exceptions_encryptor.py +++ b/tests/test_exceptions_encryptor.py @@ -1,5 +1,6 @@ import pytest import os +import secrets import json from envcloak.exceptions import ( InvalidSaltException, @@ -22,7 +23,7 @@ def test_derive_key_invalid_salt(read_variable): password = read_variable("pass1") - invalid_salt = os.urandom(SALT_SIZE - 1) # Invalid salt size + invalid_salt = secrets.token_bytes(SALT_SIZE - 1) # Invalid salt size with pytest.raises(InvalidSaltException, match="Expected salt of size"): derive_key(password, invalid_salt) @@ -30,17 +31,17 @@ def test_derive_key_invalid_salt(read_variable): def test_derive_key_invalid_password(): invalid_password = None # Password must be a string - salt = os.urandom(SALT_SIZE) + salt = secrets.token_bytes(SALT_SIZE) with pytest.raises(InvalidKeyException, match="object has no attribute 'encode'"): derive_key(invalid_password, salt) def test_generate_salt_error(monkeypatch): - # Simulate os.urandom throwing an exception + # Simulate secrets.token_bytes throwing an exception monkeypatch.setattr( - os, - "urandom", + secrets, + "token_bytes", lambda _: (_ for _ in ()).throw(OSError("Random generation error")), ) @@ -52,7 +53,7 @@ def test_generate_salt_error(monkeypatch): def test_encrypt_invalid_key(): data = "Sensitive data" - invalid_key = os.urandom(KEY_SIZE - 1) # Key must be 32 bytes + invalid_key = secrets.token_bytes(KEY_SIZE - 1) # Key must be 32 bytes with pytest.raises(EncryptionException, match="Invalid key size"): encrypt(data, invalid_key) @@ -60,7 +61,7 @@ def test_encrypt_invalid_key(): def test_decrypt_invalid_data(): invalid_encrypted_data = {"ciphertext": "wrong", "nonce": "wrong", "tag": "wrong"} - key = os.urandom(KEY_SIZE) + key = secrets.token_bytes(KEY_SIZE) with pytest.raises( DecryptionException, @@ -72,7 +73,7 @@ def test_decrypt_invalid_data(): def test_encrypt_file_error(tmp_path): input_file = tmp_path / "nonexistent.txt" # File does not exist output_file = tmp_path / "output.enc" - key = os.urandom(KEY_SIZE) + key = secrets.token_bytes(KEY_SIZE) with pytest.raises(FileEncryptionException, match="No such file or directory"): encrypt_file(str(input_file), str(output_file), key) @@ -81,7 +82,7 @@ def test_encrypt_file_error(tmp_path): def test_decrypt_file_error(tmp_path): input_file = tmp_path / "nonexistent.enc" # File does not exist output_file = tmp_path / "output.txt" - key = os.urandom(KEY_SIZE) + key = secrets.token_bytes(KEY_SIZE) with pytest.raises(FileDecryptionException, match="No such file or directory"): decrypt_file(str(input_file), str(output_file), key) @@ -90,7 +91,7 @@ def test_decrypt_file_error(tmp_path): def test_decrypt_file_invalid_content(tmp_path): input_file = tmp_path / "invalid.enc" output_file = tmp_path / "output.txt" - key = os.urandom(KEY_SIZE) + key = secrets.token_bytes(KEY_SIZE) # Write invalid encrypted content to the input file input_file.write_text("not a valid encrypted file", encoding="utf-8")