Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/secure random value generation #63

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions envcloak/encryptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand All @@ -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()
)
Expand Down
5 changes: 3 additions & 2 deletions envcloak/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion tests/test_cli_dry_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion tests/test_cli_generate_key.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import secrets
from unittest.mock import patch
from click.testing import CliRunner
import pytest
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion tests/test_cli_rotate_keys.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import secrets
import json
from unittest.mock import patch
from click.testing import CliRunner
Expand All @@ -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"

Expand Down
23 changes: 12 additions & 11 deletions tests/test_dynamic_analysis.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 (
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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)
Expand All @@ -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"

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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"
Expand All @@ -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"
Expand Down
23 changes: 12 additions & 11 deletions tests/test_encryptor.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import secrets
import base64
import json
import pytest
Expand Down Expand Up @@ -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.",
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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):
Expand All @@ -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)

Expand All @@ -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)
Expand All @@ -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)
Expand Down
21 changes: 11 additions & 10 deletions tests/test_exceptions_encryptor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pytest
import os
import secrets
import json
from envcloak.exceptions import (
InvalidSaltException,
Expand All @@ -22,25 +23,25 @@

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)


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")),
)

Expand All @@ -52,15 +53,15 @@ 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)


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,
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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")
Expand Down
Loading