Repository: Veinar/envcloak

Issue:
Introduce more secure way of generating random values (replace `os.urandom`)
Goal is to improve generating of random values. I assume that package `secrets` will handle.
Steps:
* introduce new package `secrets`
* replace all occurances of `os.urandom(xxx)`

It's good first issue cause it needs small modification in code. Use Python standard package `secrets`.

Discussion:
@BavyaMittal: Hello my name is Bavya and I am a software engineering student looking for a good first issue. I would like to try resolving this issue. Can I work on this if no one else is assigned yet? @Veinar: Hi Bavya, sure, thanks for interest in improving this package.@Veinar: Hi, @BavyaMittal  👋

When can I expect a solution?
If you don't want to perform it let me know.@BavyaMittal: Hi @Veinar! 
Sorry about the delay I am working on it and should have it ready by end of day today. @Veinar: Hi, @BavyaMittal 👋

Any update?@BavyaMittal: Hi @Veinar please see the pull request: https://github.com/Veinar/envcloak/pull/63 .

Solution patch:
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)


Test patch:
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")