diff --git a/CHANGELOG b/CHANGELOG index f262710..c2a872c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,23 @@ # CHANGELOG for EnvCloak +## *[0.3.0]* - xxxx-xx-xx +### Added +- **Information about versatility.** +- Added possibility to not specify output when encrypting single file (using original name with `.enc` suffix). +- Added `--preview` flag for encrypt and decrypt command. +- Introduced `SECURITY.md` policy with guidelines how to securely use this tool. +- Added guidelines for integrating with popular cloud KMS. + +### Changed +- Code refactor to modularize logic. +- Increased randomness when generating salt by introducing `secure` package. (Thanks to @BavyaMittal) +- Applied module docstrings + +## *[0.2.2]* - 2024-11-29 +### Changed + +- Fix critical error with `packaging` not declared in dependencies list. + ## *[0.2.1]* & *[0.2.0]* - 2024-11-27 > First of Beta release ### Added diff --git a/README.md b/README.md index 6999022..f8441c8 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,21 @@ logo

+

+⚠️ IMPORTANT NOTE: EnvCloak is NOT Limited to .env Files!⚠️
+EnvCloak was originally built to secure .env files, but it can encrypt and decrypt any file type.
+Use it for .json, .yaml, .txt, binary files, or any sensitive data.
+
+The name may be misleading, but the tool is far more versatile than it suggests! +

+ # 🔒 EnvCloak > "Because Your Secrets Deserve Better Than Plaintext!" ![GitHub License](https://img.shields.io/github/license/Veinar/envcloak) ![Contrib Welcome](https://img.shields.io/badge/contributions-welcome-blue) +![Looking for](https://img.shields.io/badge/looking%20for-maintainers-228B22) ![Code style](https://img.shields.io/badge/code%20style-black-black) ![CI/CD Pipeline](https://github.com/Veinar/envcloak/actions/workflows/test.yaml/badge.svg) ![Build Pipeline](https://github.com/Veinar/envcloak/actions/workflows/build.yaml/badge.svg) @@ -57,6 +66,9 @@ envcloak generate-key-from-password --password "YourTopSecretPassword" --output # From random password and salt envcloak generate-key --output secretkey.key ``` + +![generate-key-gif](https://veinar.pl/envcloak-generate-key.gif) + > **What it does:** generates your private key used to encrypt and decrypt files. **Appends (or creates if needed) .gitignore as well** as super-hero should! 🎉 > ⚠ **If someone knows your password and salt (option 1) can recreate same `key` - keep those variables safe as `key` itself** ⚠ @@ -66,6 +78,9 @@ envcloak generate-key --output secretkey.key ```bash envcloak encrypt --input .env --output .env.enc --key-file mykey.key ``` + +![encrypt-gif](https://veinar.pl/envcloak-encrypt.gif) + > **What it does:** Encrypts your `.env` file with a specified key, outputting a sparkling `.env.enc` file. ### Decrypting Variables: @@ -73,6 +88,9 @@ envcloak encrypt --input .env --output .env.enc --key-file mykey.key ```bash envcloak decrypt --input .env.enc --output .env --key-file mykey.key ``` + +![decrypt-gif](https://veinar.pl/envcloak-decrypt.gif) + > **What it does:** Decrypts the `.env.enc` file back to `.env` using the same key. Voilà! or you may want to use it ... @@ -105,6 +123,7 @@ load_encrypted_env('.env.enc', key_file='mykey.key').to_os_env() * Works with directories using `--directory` instead of `--input` on `encrypt` and `decrypt`. > ℹ️ EnvCloak process files in batch one-by-one. * Can [recursively](docs/recursive.md) encrypt or decrypt directories. +* Can list files in directory that will be encrypted using `--preview` flag (ℹ️ only for directories and it does not commit the operation!). 🚦 Error Handling diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..68cb222 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,95 @@ +# ⛨ Security Policy + +This document outlines the security policies and practices for the **EnvCloak** project, ensuring the tool is secure and reliable for managing encrypted environment variables. + +## 🔩 Supported Versions + +The following table indicates the versions of **EnvCloak** currently supported with security updates: + +| Version | Supported | +|-----------|--------------------| +| > 0.3 | :white_check_mark: | +| ≤ 0.3 | :x: | + +## 🚨 Reporting a Vulnerability + +If you discover a security vulnerability in **EnvCloak**, please report it to the project author. +Or create issue describing what is wrong. + +## ℹ️ Known Security Risks + +### 1. Key Storage in Plaintext +- **Risk**: Keys are stored as plaintext files (e.g., `.key` extension) and may be exposed if file permissions are weak or the file is mishandled. +- **Mitigation**: + - Store key files in secure locations with restricted permissions (`chmod 600` recommended). + - Use secure directories or storage solutions (e.g., encrypted storage or key management services). + - Avoid committing key files to version control systems. + +### 2. Tampering with Encrypted Files +- **Risk**: Encrypted files could be tampered with, leading to undetected data corruption or malicious injection. +- **Mitigation**: + - **EnvCloak** implements a double SHA-3 verification: + 1. A SHA-3 hash is generated for the encrypted file. + 2. A second SHA-3 hash is generated from the content of the file during encryption. + - **EnvCloak will not decrypt files if SHA validation fails**, ensuring file integrity. To bypass this validation, users must explicitly use the `--skip-sha-validation` flag. + - Use the `envcloak compare` command to verify file integrity. + +### 3. Improper Key Rotation +- **Risk**: Key rotation errors could lead to data being unrecoverable or inconsistencies between environments. +- **Mitigation**: + - Use the `--dry-run` option during key rotation to preview changes before applying them. + - Backup all encrypted files before initiating key rotation. + +### 4. Key Recreation via Password and Salt +- **Risk**: If a key is generated using a password and salt, the same key can be recreated if both the password and salt are known. +- **Mitigation**: + - Use sufficiently long and unique passwords. + - Avoid using predictable or commonly reused salts. + - Consider generating random keys without relying on passwords when possible. + +### 5. Directory Encryption Risks +- **Risk**: Encrypting entire directories without care may include unintended sensitive or system-critical files. +- **Mitigation**: + - Use the `--preview` option to list files before encryption. + - Avoid running **EnvCloak** on system-critical paths without reviewing the target files. + +### 6. Unauthorized Access +- **Risk**: Weak file permissions or mishandled decryption keys could expose sensitive data to unauthorized users. +- **Mitigation**: + - Ensure encrypted files are stored with restricted access (`chmod 600` recommended). + - Do not store decrypted files or plaintext keys in accessible locations. + +### 7. Outdated Algorithms +- **Risk**: Encryption algorithms used by **EnvCloak** may become outdated or insecure over time. +- **Mitigation**: + - **EnvCloak** currently uses **AES-256**, a widely trusted encryption standard. + - Regular audits will ensure algorithms remain up-to-date with industry standards. + - A migration mechanism will be provided if future updates require transitioning to a new algorithm. + +## 🦑 Best Practices for Secure Usage + +1. **Key Management**: + - Store key files securely, with restricted access (`chmod 600`). + - Use extensions like `.key` to clearly differentiate key files from other files. + - Rotate keys periodically using the `envcloak rotate` command. + +2. **Environment File Handling**: + - Do not store plaintext `.env` files in version control systems. + - Encrypt sensitive `.env` files using the `envcloak encrypt` command. + +3. **File Permissions**: + - Restrict access to encrypted files (`chmod 600` on Linux systems). + - Ensure only authorized users have access to the decryption key. + +4. **Tamper Detection**: + - Leverage the double SHA-3 verification feature to detect unauthorized changes to encrypted files. + - Be cautious when using `--skip-sha-validation`, as this bypasses integrity checks. + +5. **Integration Security**: + - Pass sensitive keys or data via environment variables in CI/CD pipelines. + - Avoid logging sensitive data during encryption or decryption processes. + +## Contact + +For any security-related concerns or questions, please contact the project author via the email address listed on their [GitHub profile](https://github.com/Veinar) or Package (pypi) site https://pypi.org/project/envcloak/. +We appreciate your support in keeping **EnvCloak** secure for everyone. 🥳 diff --git a/envcloak/__init__.py b/envcloak/__init__.py index dded953..2e10f97 100644 --- a/envcloak/__init__.py +++ b/envcloak/__init__.py @@ -1,4 +1,4 @@ from .loader import load_encrypted_env -__version__ = "0.2.2" +__version__ = "0.3.0" __all__ = ["load_encrypted_env"] diff --git a/envcloak/cli.py b/envcloak/cli.py index 27627b8..93c6a15 100644 --- a/envcloak/cli.py +++ b/envcloak/cli.py @@ -1,3 +1,10 @@ +""" +cli.py + +This module defines the command-line interface for interacting with the `envcloak` package. +It provides commands for encrypting, decrypting, and managing environment variables. +""" + import click from envcloak.commands.encrypt import encrypt from envcloak.commands.decrypt import decrypt @@ -6,13 +13,14 @@ from envcloak.commands.rotate_keys import rotate_keys from envcloak.commands.compare import compare from envcloak.version_check import warn_if_outdated +from envcloak import __version__ # Warn About Outdated Versions warn_if_outdated() @click.group() -@click.version_option(prog_name="EnvCloak") +@click.version_option(version=__version__, prog_name="EnvCloak") def main(): """ EnvCloak: Securely manage encrypted environment variables. diff --git a/envcloak/commands/compare.py b/envcloak/commands/compare.py index 84d8a31..c116f78 100644 --- a/envcloak/commands/compare.py +++ b/envcloak/commands/compare.py @@ -1,3 +1,9 @@ +""" +compare.py + +This module provides logic for compare command of EnvCloak +""" + import click from click import style from envcloak.comparator import compare_files_or_directories diff --git a/envcloak/commands/decrypt.py b/envcloak/commands/decrypt.py index 8adb0dc..9325781 100644 --- a/envcloak/commands/decrypt.py +++ b/envcloak/commands/decrypt.py @@ -1,28 +1,38 @@ -import os -import shutil -from pathlib import Path +""" +decrypt.py + +This module provides logic for decrypt command of EnvCloak +""" + import click -from click import style -from envcloak.utils import debug_log, calculate_required_space +from envcloak.utils import ( + debug_log, + calculate_required_space, + list_files_to_encrypt, + read_key_file, +) +from envcloak.handlers import ( + handle_directory_preview, + handle_overwrite, + handle_common_exceptions, +) from envcloak.decorators.common_decorators import ( debug_option, dry_run_option, force_option, no_sha_validation_option, - recursion, + recursion_option, + preview_option, ) from envcloak.validation import ( check_file_exists, check_directory_exists, check_directory_not_empty, - check_output_not_exists, check_permissions, check_disk_space, ) from envcloak.encryptor import decrypt_file, traverse_and_process_files from envcloak.exceptions import ( - OutputFileExistsException, - DiskSpaceException, FileDecryptionException, ) @@ -32,7 +42,8 @@ @dry_run_option @force_option @no_sha_validation_option -@recursion +@recursion_option +@preview_option @click.option( "--input", "-i", @@ -64,6 +75,7 @@ def decrypt( debug, skip_sha_validation, recursion, + preview, ): """ Decrypt environment variables from a file or all files in a directory. @@ -71,14 +83,21 @@ def decrypt( try: debug_log("Debug mode is enabled", debug) - # Always perform validation - debug_log("Debug: Validating input and directory parameters.", debug) if not input and not directory: raise click.UsageError("You must provide either --input or --directory.") if input and directory: raise click.UsageError( "You must provide either --input or --directory, not both." ) + + if directory and preview: + handle_directory_preview(directory, recursion, debug, list_files_to_encrypt) + return + + debug_log(f"Debug: Validating key file {key_file}.", debug) + check_file_exists(key_file) + check_permissions(key_file) + if input: debug_log(f"Debug: Validating input file {input}.", debug) check_file_exists(input) @@ -87,32 +106,8 @@ def decrypt( debug_log(f"Debug: Validating directory {directory}.", debug) check_directory_exists(directory) check_directory_not_empty(directory) - debug_log(f"Debug: Validating key file {key_file}.", debug) - check_file_exists(key_file) - check_permissions(key_file) - # Handle overwrite with --force - debug_log("Debug: Handling overwrite logic with force flag.", debug) - if not force: - check_output_not_exists(output) - else: - if os.path.exists(output): - debug_log( - f"Debug: Existing file or directory found at {output}. Overwriting due to --force.", - debug, - ) - click.echo( - style( - f"⚠️ Warning: Overwriting existing file or directory {output} (--force used).", - fg="yellow", - ) - ) - if os.path.isdir(output): - debug_log(f"Debug: Removing existing directory {output}.", debug) - shutil.rmtree(output) # Remove existing directory - else: - debug_log(f"Debug: Removing existing file {output}.", debug) - os.remove(output) # Remove existing file + handle_overwrite(output, force, debug) debug_log( f"Debug: Calculating required space for input {input} or directory {directory}.", @@ -126,10 +121,7 @@ def decrypt( click.echo("Dry-run checks passed successfully.") return - # Actual decryption logic - with open(key_file, "rb") as kf: - key = kf.read() - debug_log(f"Debug: Key file {key_file} read successfully.", debug) + key = read_key_file(key_file, debug) if input: debug_log( @@ -139,6 +131,7 @@ def decrypt( decrypt_file(input, output, key, validate_integrity=not skip_sha_validation) click.echo(f"File {input} decrypted -> {output} using key {key_file}") elif directory: + debug_log(f"Debug: Decrypting files in directory {directory}.", debug) traverse_and_process_files( directory, output, @@ -154,9 +147,13 @@ def decrypt( recursion=recursion, ) click.echo(f"All files in directory {directory} decrypted -> {output}") - except ( - OutputFileExistsException, - DiskSpaceException, - FileDecryptionException, - ) as e: - click.echo(f"Error during decryption: {str(e)}") + except FileDecryptionException as e: + click.echo( + f"Error during decryption: Error: Failed to decrypt the file.\nDetails: {e.details}", + err=True, + ) + except click.UsageError as e: + click.echo(f"Usage Error: {e}", err=True) + except Exception as e: + handle_common_exceptions(e, debug) + raise diff --git a/envcloak/commands/encrypt.py b/envcloak/commands/encrypt.py index 14792a6..c67b427 100644 --- a/envcloak/commands/encrypt.py +++ b/envcloak/commands/encrypt.py @@ -1,27 +1,34 @@ -import os -import shutil -from pathlib import Path +""" +encrypt.py + +This module provides logic for encrypt command of EnvCloak +""" + import click -from click import style -from envcloak.utils import debug_log, calculate_required_space +from envcloak.utils import ( + debug_log, + calculate_required_space, + list_files_to_encrypt, + validate_paths, + read_key_file, +) +from envcloak.handlers import ( + handle_directory_preview, + handle_overwrite, + handle_common_exceptions, +) from envcloak.decorators.common_decorators import ( debug_option, force_option, dry_run_option, - recursion, + recursion_option, + preview_option, ) from envcloak.validation import ( - check_file_exists, - check_directory_exists, - check_directory_not_empty, - check_output_not_exists, - check_permissions, check_disk_space, ) from envcloak.encryptor import encrypt_file, traverse_and_process_files from envcloak.exceptions import ( - OutputFileExistsException, - DiskSpaceException, FileEncryptionException, ) @@ -30,7 +37,8 @@ @debug_option @dry_run_option @force_option -@recursion +@recursion_option +@preview_option @click.option( "--input", "-i", required=False, help="Path to the input file (e.g., .env)." ) @@ -43,67 +51,42 @@ @click.option( "--output", "-o", - required=True, + required=False, help="Path to the output file or directory for encrypted files.", ) @click.option( "--key-file", "-k", required=True, help="Path to the encryption key file." ) -def encrypt(input, directory, output, key_file, dry_run, force, debug, recursion): +def encrypt( + input, directory, output, key_file, dry_run, force, debug, recursion, preview +): """ Encrypt environment variables from a file or all files in a directory. """ try: - # debug mode + # Debug mode debug_log("Debug mode is enabled", debug) - debug_log("Debug: Validating input and directory parameters.", debug) - # Always perform validation - if not input and not directory: - raise click.UsageError("You must provide either --input or --directory.") - if input and directory: + # Raise error if --preview is used with --input + if input and preview: raise click.UsageError( - "You must provide either --input or --directory, not both." + "The --preview option cannot be used with a single file (--input)." ) + + # Handle preview mode for directories + if directory and preview: + handle_directory_preview(directory, recursion, debug, list_files_to_encrypt) + return + + # Validate input, directory, key file, and output + validate_paths(input=input, directory=directory, key_file=key_file, debug=debug) + if input: - debug_log(f"Debug: Validating input file {input}.", debug) - check_file_exists(input) - check_permissions(input) - if directory: - debug_log(f"Debug: Validating directory {directory}.", debug) - check_directory_exists(directory) - check_directory_not_empty(directory) - debug_log(f"Debug: Validating key file {key_file}.", debug) - check_file_exists(key_file) - check_permissions(key_file) + output = output or f"{input}.enc" + debug_log(f"Debug: Output set to {output}.", debug) - # Handle overwrite with --force - debug_log("Debug: Handling overwrite logic with force flag.", debug) - if not force: - check_output_not_exists(output) - else: - if os.path.exists(output): - debug_log( - f"Debug: File or directory {output} exists, proceeding with overwrite.", - debug, - ) - click.echo( - style( - f"⚠️ Warning: Overwriting existing file or directory {output} (--force used).", - fg="yellow", - ) - ) - if os.path.isdir(output): - debug_log(f"Debug: Removing existing directory {output}.", debug) - shutil.rmtree(output) # Remove existing directory - else: - debug_log(f"Debug: Removing existing file {output}.", debug) - os.remove(output) # Remove existing file + handle_overwrite(output, force, debug) - debug_log( - f"Debug: Calculating required space for input {input} and output directory {directory}.", - debug, - ) required_space = calculate_required_space(input, directory) check_disk_space(output, required_space) @@ -115,10 +98,7 @@ def encrypt(input, directory, output, key_file, dry_run, force, debug, recursion click.echo("Dry-run checks passed successfully.") return - # Actual encryption logic - with open(key_file, "rb") as kf: - key = kf.read() - debug_log(f"Debug: Key file {key_file} read successfully.", debug) + key = read_key_file(key_file, debug) if input: debug_log( @@ -128,6 +108,7 @@ def encrypt(input, directory, output, key_file, dry_run, force, debug, recursion encrypt_file(input, output, key) click.echo(f"File {input} encrypted -> {output} using key {key_file}") elif directory: + debug_log(f"Debug: Encrypting files in directory {directory}.", debug) traverse_and_process_files( directory, output, @@ -140,9 +121,13 @@ def encrypt(input, directory, output, key_file, dry_run, force, debug, recursion recursion=recursion, ) click.echo(f"All files in directory {directory} encrypted -> {output}") - except ( - OutputFileExistsException, - DiskSpaceException, - FileEncryptionException, - ) as e: - click.echo(f"Error during encryption: {str(e)}") + except FileEncryptionException as e: + click.echo( + f"Error: An error occurred during file encryption.\nDetails: {e}", + err=True, + ) + except click.UsageError as e: + click.echo(f"Usage Error: {e}", err=True) + except Exception as e: + handle_common_exceptions(e, debug) + raise diff --git a/envcloak/commands/generate_key.py b/envcloak/commands/generate_key.py index b15539a..3d50104 100644 --- a/envcloak/commands/generate_key.py +++ b/envcloak/commands/generate_key.py @@ -1,3 +1,9 @@ +""" +generate_key.py + +This module provides logic for key generation via command of EnvCloak +""" + from pathlib import Path import click from envcloak.validation import check_output_not_exists, check_disk_space diff --git a/envcloak/commands/generate_key_from_password.py b/envcloak/commands/generate_key_from_password.py index a897726..efcfbbe 100644 --- a/envcloak/commands/generate_key_from_password.py +++ b/envcloak/commands/generate_key_from_password.py @@ -1,3 +1,9 @@ +""" +generate_key_from_password.py + +This module provides logic for generating key using password or password and salt via command of EnvCloak +""" + from pathlib import Path import click from envcloak.validation import check_output_not_exists, check_disk_space, validate_salt diff --git a/envcloak/commands/rotate_keys.py b/envcloak/commands/rotate_keys.py index eece152..e8c6214 100644 --- a/envcloak/commands/rotate_keys.py +++ b/envcloak/commands/rotate_keys.py @@ -1,3 +1,9 @@ +""" +rotate_keys.py + +This module provides logic for key rotating using EnvCloak command. +""" + import os import click from envcloak.utils import debug_log @@ -30,7 +36,12 @@ "--new-key-file", "-nk", required=True, help="Path to the new encryption key." ) @click.option("--output", "-o", required=True, help="Path to the re-encrypted file.") -def rotate_keys(input, old_key_file, new_key_file, output, dry_run, debug): +@click.option( + "--preview", + is_flag=True, + help="Preview the key rotation process without making changes.", +) +def rotate_keys(input, old_key_file, new_key_file, output, dry_run, debug, preview): """ Rotate encryption keys by re-encrypting a file with a new key. """ @@ -46,6 +57,18 @@ def rotate_keys(input, old_key_file, new_key_file, output, dry_run, debug): check_output_not_exists(output) check_disk_space(output, required_space=1024 * 1024) + # Handle Preview or Dry-run modes + if preview: + click.secho( + f""" +Preview of Key Rotation: +- Old key: {old_key_file} will no longer be valid for this encrypted file. +- New key: {new_key_file} will be used to decrypt the encrypted file. +- Encrypted file: {input} will be re-encrypted to {output}. + """, + fg="cyan", + ) + return if dry_run: click.echo("Dry-run checks passed successfully.") return diff --git a/envcloak/comparator.py b/envcloak/comparator.py index a93a3ea..e8c99ec 100644 --- a/envcloak/comparator.py +++ b/envcloak/comparator.py @@ -1,3 +1,10 @@ +""" +comparator.py + +This module contains utilities for comparing files, directories, or cryptographic properties. +It ensures integrity and helps detect discrepancies in encrypted data. +""" + import os import tempfile from pathlib import Path diff --git a/envcloak/constants.py b/envcloak/constants.py index 475d1e4..9d46331 100644 --- a/envcloak/constants.py +++ b/envcloak/constants.py @@ -1,3 +1,10 @@ +""" +constants.py + +This module defines constants used throughout the `envcloak` package. +It centralizes configuration and default values to ensure consistency and easier maintenance. +""" + # AES Encryption AES_BLOCK_SIZE = 128 # Block size for AES NONCE_SIZE = 12 # Recommended size for GCM nonce diff --git a/envcloak/decorators/common_decorators.py b/envcloak/decorators/common_decorators.py index 6177df8..30f362e 100644 --- a/envcloak/decorators/common_decorators.py +++ b/envcloak/decorators/common_decorators.py @@ -1,3 +1,9 @@ +""" +common_decorators.py + +This module provides Click options that are common across multiple commands +""" + import click @@ -42,7 +48,7 @@ def no_sha_validation_option(func): )(func) -def recursion(func): +def recursion_option(func): """ Add `--recursion` and `-r` flags to a Click command. """ @@ -52,3 +58,14 @@ def recursion(func): is_flag=True, help="Enable recursion to process files in subdirectories.", )(func) + + +def preview_option(func): + """ + Add `--preview` flag to a Click command. + """ + return click.option( + "--preview", + is_flag=True, + help="List files that will be decrypted (only applicable for directories).", + )(func) diff --git a/envcloak/encryptor.py b/envcloak/encryptor.py index bd3cebd..2219836 100644 --- a/envcloak/encryptor.py +++ b/envcloak/encryptor.py @@ -1,3 +1,10 @@ +""" +encryptor.py + +This module implements core functionality for encrypting and decrypting files. +It handles file traversal, key management, and cryptographic operations to ensure secure data handling. +""" + import os import base64 import json @@ -7,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, @@ -51,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 @@ -65,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() ) @@ -129,16 +137,10 @@ def encrypt_file(input_file: str, output_file: str, key: bytes): # Compute hash of plaintext for integrity encrypted_data["sha"] = compute_sha256(data) - print( - f"Debug: SHA-256 hash of plaintext during encryption: {encrypted_data['sha']}" - ) # Compute hash of the entire encrypted structure file_hash = compute_sha256(json.dumps(encrypted_data, ensure_ascii=False)) encrypted_data["file_sha"] = file_hash # Store this hash in the structure - print( - f"Debug: SHA-256 hash of encrypted structure (file_sha): {encrypted_data['file_sha']}" - ) with open(output_file, "w", encoding="utf-8") as outfile: json.dump(encrypted_data, outfile, ensure_ascii=False) @@ -242,7 +244,11 @@ def traverse_and_process_files( target_path = output_dir / relative_path target_path.parent.mkdir(parents=True, exist_ok=True) + output_file = str(target_path) + if output_file.endswith(".enc"): + output_file = output_file[:-4] # Explicitly handle `.enc` + if not dry_run: - process_file(file_path, target_path, key, debug) + process_file(file_path, output_file, key, debug) else: - debug_log(f"Dry-run: Would process {file_path} -> {target_path}", debug) + debug_log(f"Dry-run: Would process {file_path} -> {output_file}", debug) diff --git a/envcloak/exceptions.py b/envcloak/exceptions.py index f8eeb1f..ae6a81a 100644 --- a/envcloak/exceptions.py +++ b/envcloak/exceptions.py @@ -1,3 +1,11 @@ +""" +exceptions.py + +This module defines custom exception classes for handling errors specific to the `envcloak` package. +It enhances error reporting with meaningful context, aiding debugging and user feedback. +""" + + #### EncryptedEnvLoader Exceptions class EncryptedEnvLoaderException(Exception): """Base exception for EncryptedEnvLoader errors.""" diff --git a/envcloak/generator.py b/envcloak/generator.py index adf05c9..fdeaf27 100644 --- a/envcloak/generator.py +++ b/envcloak/generator.py @@ -1,14 +1,22 @@ +""" +generator.py + +This module provides utilities for generating cryptographic keys and passwords. +It ensures secure and reliable key generation for encryption and decryption tasks. +""" + # import secrets # TODO: implement this 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) @@ -29,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/envcloak/handlers.py b/envcloak/handlers.py new file mode 100644 index 0000000..073cc7c --- /dev/null +++ b/envcloak/handlers.py @@ -0,0 +1,92 @@ +""" +handlers.py + +This module defines handlers for processing application events, errors with/without logging. +It facilitates seamless interaction between components and ensures proper error handling. +""" + +import os +import shutil +import click +from envcloak.utils import debug_log +from envcloak.validation import check_output_not_exists +from envcloak.exceptions import OutputFileExistsException, DiskSpaceException + + +def handle_overwrite(output: str, force: bool, debug: bool): + """Handle overwriting existing files or directories.""" + if not force: + check_output_not_exists(output) + else: + if os.path.exists(output): + if os.path.isdir(output): + debug_log(f"Debug: Removing existing directory {output}.", debug) + click.secho( + f"⚠️ Warning: Overwriting existing directory {output} (--force used).", + fg="yellow", + ) + shutil.rmtree(output) + else: + debug_log(f"Debug: Removing existing file {output}.", debug) + click.secho( + f"⚠️ Warning: Overwriting existing file {output} (--force used).", + fg="yellow", + ) + os.remove(output) + + +def handle_directory_preview(directory, recursion, debug, list_files_func): + """ + Handles listing files in a directory for preview purposes. + + :param directory: Path to the directory. + :param recursion: Whether to include files recursively. + :param debug: Debug flag for verbose logging. + :param list_files_func: Function to list files in the directory. + """ + debug_log(f"Debug: Listing files for preview. Recursive = {recursion}.", debug) + files = list_files_func(directory, recursion) + if not files: + click.secho(f"ℹ️ No files found in directory {directory}.", fg="blue") + else: + click.secho(f"ℹ️ Files to be processed in directory {directory}:", fg="green") + for file in files: + click.echo(file) + return files + + +def handle_common_exceptions(exception, debug): + """ + Handles common exceptions and provides user-friendly error messages. + + This function processes known exceptions and displays appropriate error + messages to the user via `click.echo`. If the exception is not recognized, + it logs the error message (if debug mode is enabled) and re-raises the exception. + + Args: + exception (Exception): The exception to handle. Supported exceptions include: + - OutputFileExistsException: Raised when an output file or directory already exists. + - DiskSpaceException: Raised when there is insufficient disk space. + - click.UsageError: Raised for invalid command-line usage. + debug (bool): If True, debug information is logged for unexpected exceptions. + + Raises: + Exception: Re-raises unexpected exceptions after logging them. + + Outputs: + Prints user-friendly error messages to the console for known exceptions. + """ + if isinstance(exception, OutputFileExistsException): + click.echo( + f"Error: The specified output file or directory already exists.\nDetails: {exception}", + err=True, + ) + elif isinstance(exception, DiskSpaceException): + click.echo( + f"Error: Insufficient disk space for operation.\nDetails: {exception}", + err=True, + ) + elif isinstance(exception, click.UsageError): + click.echo(f"Usage Error: {exception}", err=True) + else: + debug_log(f"Unexpected error occurred: {str(exception)}", debug) diff --git a/envcloak/loader.py b/envcloak/loader.py index ef5e290..b799165 100644 --- a/envcloak/loader.py +++ b/envcloak/loader.py @@ -1,3 +1,10 @@ +""" +loader.py + +This module manages the loading of encrypted environment variables for usage in Python code as imported function. +It decrypts and validates sensitive information and loads it in to OS env if requested. +""" + import os import json from pathlib import Path @@ -15,6 +22,10 @@ class EncryptedEnvLoader: + """ + Class responsible for handling inside from code requests to decrypt env variables. + """ + def __init__(self, file_path: str, key_file: str): """ Initialize the EncryptedEnvLoader with an encrypted file and key file. diff --git a/envcloak/utils.py b/envcloak/utils.py index c75f021..f971611 100644 --- a/envcloak/utils.py +++ b/envcloak/utils.py @@ -1,6 +1,20 @@ +""" +utils.py + +This module provides helper functions and common utilities to support various operations +in the `envcloak` package, such as logging, checksum calculation, and general-purpose tools. +""" + import os import hashlib from pathlib import Path +import click +from envcloak.validation import ( + check_file_exists, + check_directory_exists, + check_permissions, + check_directory_not_empty, +) def add_to_gitignore(directory: str, filename: str): @@ -51,6 +65,47 @@ def calculate_required_space(input=None, directory=None): return 0 +def list_files_to_encrypt(directory, recursion): + """ + List files in a directory that would be encrypted. + + :param directory: Path to the directory to scan. + :param recursion: Whether to scan directories recursively. + :return: List of file paths. + """ + path = Path(directory) + if not path.is_dir(): + raise click.UsageError(f"The specified path {directory} is not a directory.") + + files = [] + if recursion: + files = list(path.rglob("*")) # Recursive glob + else: + files = list(path.glob("*")) # Non-recursive glob + + # Filter only files + files = [str(f) for f in files if f.is_file()] + return files + + +def validate_paths(input=None, directory=None, key_file=None, output=None, debug=False): + """Perform validation for common parameters.""" + if input and directory: + raise click.UsageError( + "You must provide either --input or --directory, not both." + ) + if not input and not directory: + raise click.UsageError("You must provide either --input or --directory.") + if key_file: + debug_log(f"Debug: Validating key file {key_file}.", debug) + check_file_exists(key_file) + check_permissions(key_file) + if directory: + debug_log(f"Debug: Validating directory {directory}.", debug) + check_directory_exists(directory) + check_directory_not_empty(directory) + + def debug_log(message, debug): """ Print message only if debug is true @@ -71,3 +126,20 @@ def compute_sha256(data: str) -> str: :return: SHA-256 hash as a hex string. """ return hashlib.sha3_256(data.encode()).hexdigest() + + +def read_key_file(key_file, debug): + """ + Reads a cryptographic key from a file and logs the operation if debugging is enabled. + + Args: + key_file (str or Path): The path to the file containing the cryptographic key. + debug (bool): If True, logs debugging information about the operation. + + Returns: + bytes: The binary content of the key file. + """ + with open(key_file, "rb") as kf: + key = kf.read() + debug_log(f"Debug: Key file {key_file} read successfully.", debug) + return key diff --git a/envcloak/validation.py b/envcloak/validation.py index 7d081d2..85a3470 100644 --- a/envcloak/validation.py +++ b/envcloak/validation.py @@ -1,3 +1,10 @@ +""" +validation.py + +This module offers robust validation utilities for checking user inputs, file paths, permissions, +and other constraints required for secure and reliable operations. +""" + import os from pathlib import Path import shutil diff --git a/envcloak/version_check.py b/envcloak/version_check.py index 2529987..98fc3f7 100644 --- a/envcloak/version_check.py +++ b/envcloak/version_check.py @@ -1,3 +1,10 @@ +""" +version_check.py + +This module includes functionality to check for the latest version of the `envcloak` package on PyPI. +It helps users stay updated with the newest features, enhancements, and fixes. +""" + import requests import click from packaging.version import Version, InvalidVersion @@ -5,6 +12,19 @@ def get_latest_version(): + """ + Fetches the latest version of the 'envcloak' package from PyPI. + + This function sends a request to the PyPI API to retrieve the latest version + information for the 'envcloak' package. + + Returns: + str: The latest version as a string if successful. + None: If an error occurs during the request. + + Outputs: + Prints error messages to the console if the request fails or times out. + """ url = "https://pypi.org/pypi/envcloak/json" try: # Send a GET request to the PyPI API @@ -28,6 +48,17 @@ def get_latest_version(): def warn_if_outdated(): + """ + Warns the user if the installed version of 'envcloak' is outdated. + + This function compares the installed version of 'envcloak' with the latest version + available on PyPI. If the installed version is older, it displays a warning and + provides instructions to upgrade. + + Outputs: + Prints a warning message with upgrade instructions if a newer version is available. + Prints an error message if version comparison fails or the latest version cannot be determined. + """ latest_version = get_latest_version() current_version = __version__ diff --git a/examples/workflow/README.md b/examples/workflow/README.md index 424ea69..06cc3ae 100644 --- a/examples/workflow/README.md +++ b/examples/workflow/README.md @@ -2,6 +2,8 @@ EnvCloak simplifies the secure management of sensitive environment variables. In CI/CD workflows, the encrypted `.env.enc` file is typically created and committed manually, while the decryption happens automatically during deployment or application startup. This guide focuses on **decrypting variables** in workflows using both the `envcloak` CLI and Python code. +> Examples how to integrate KMS in CI/CD process can be found [here](integration_with_kms.md). 💎 + --- ## Key Handling: Securely Storing `ENVCLOAK_KEY_B64` diff --git a/examples/workflow/integration_with_kms.md b/examples/workflow/integration_with_kms.md new file mode 100644 index 0000000..5f23577 --- /dev/null +++ b/examples/workflow/integration_with_kms.md @@ -0,0 +1,107 @@ +# General Guidelines for usage with KMS providers. + +## 1. HashiCorp Vault + +### Key Storage + +* Install and configure HashiCorp Vault. +* Enable the KV secrets engine: +```bash +vault secrets enable -path=envcloak kv +``` +* Store the key in Vault: +```bash +vault kv put envcloak/key key=$(cat key.txt) +``` + +### Key Retrieval + +* Authenticate with Vault: + * Using a token: + ```bash + export VAULT_TOKEN= + ``` + * Or use a method like AppRole or `Kubernetes` auth for automated systems. + +* Retrieve the key: +```bash +vault kv get -field=key envcloak/key > key.txt +``` + +### Integrating in CI/CD + +* Use Vault's CLI or API to fetch keys during pipeline execution. For example: + +* Add a script in your CI pipeline: +```bash +vault kv get -field=key envcloak/key > key.txt && \ +envcloak decrypt --input .env.enc --output .env --key-file key.txt +``` + +## 2. AWS KMS with Secrets Manager + +### Key Storage + +* Store the key in AWS Secrets Manager: +```bash +aws secretsmanager create-secret \ +--name envcloak/key \ +--secret-string "$(cat key.txt)" +``` + +### Key Retrieval + +* Retrieve the key using AWS CLI: +```bash +aws secretsmanager get-secret-value \ +--secret-id envcloak/key \ +--query SecretString \ +--output text > key.txt +``` + +### Integrating in CI/CD + +* Use an IAM role for the CI/CD system with access to the secret. +* Add a script in your pipeline: +```bash +KEY=$(aws secretsmanager get-secret-value --secret-id envcloak/key \ +--query SecretString --output text) echo "$KEY" > key.txt && \ +envcloak decrypt --input .env.enc --output .env --key-file key.txt +``` + +## 3. Google Cloud KMS with Secret Manager + +### Key Storage + +* Store the key in Google Secret Manager: +```bash +echo -n "$(cat key.txt)" | gcloud secrets create envcloak-key \ +--data-file=- --replication-policy="automatic" +``` + +### Key Retrieval + +* Grant your CI/CD service account the `roles/secretmanager.secretAccessor` role. +* Retrieve the key: +```bash +gcloud secrets versions access latest --secret="envcloak-key" > key.txt +``` + +### Integrating in CI/CD + +* Authenticate with a service account key or use a GCP-managed CI/CD system with a properly scoped service account. +* Add a step in your pipeline: +```bash +gcloud secrets versions access latest --secret="envcloak-key" > key.txt && \ +envcloak decrypt --input .env.enc --output .env --key-file key.txt +``` + +## General Guidelines for Secure Key Management + +* Role-Based Access Control (RBAC): Ensure that only authorized users or services can access the keys. +* Audit Logging: Enable logging for all access to secrets for auditing and compliance. +* Key Rotation: + * Periodically rotate keys in the KMS. + * Re-encrypt the environment files with the new key. +* Automated Integration: + * Use the native SDKs or APIs for these KMS providers in your applications or CI/CD pipelines for seamless integration. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 749f4fd..f7a0c6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "envcloak" -version = "0.2.2" +version = "0.3.0" description = "Securely manage encrypted environment variables with ease." readme = "README.md" license = { file = "LICENSE" } diff --git a/tests/test_cli_decrypt.py b/tests/test_cli_decrypt.py index 89dc32e..4a08bc5 100644 --- a/tests/test_cli_decrypt.py +++ b/tests/test_cli_decrypt.py @@ -21,6 +21,9 @@ def test_decrypt(mock_decrypt_file, runner, mock_files): temp_decrypted_file = decrypted_file.with_name("variables.temp.decrypted") def mock_decrypt(input_path, output_path, key, validate_integrity=True): + print( + f"mock_decrypt called with: {input_path}, {output_path}, {key}, {validate_integrity}" + ) assert os.path.exists(input_path), "Encrypted file does not exist" assert isinstance( validate_integrity, bool @@ -144,26 +147,26 @@ def mock_decrypt(input_path, output_path, key, validate_integrity=True): "--force", "--recursion", # Enable recursion "--skip-sha-validation", + "--debug", ], ) # Check that output mentions overwriting existing files - assert "Overwriting existing file" in result.output + assert "Overwriting existing directory" in result.output # Verify that decrypt_file was called for each input file mock_decrypt_file.assert_any_call( str(directory / "file1.env.enc"), str(output_directory / "file1.env"), b"mock_key", - validate_integrity=False, # Ensure the validate_integrity flag matches + validate_integrity=False, ) mock_decrypt_file.assert_any_call( str(directory / "file2.env.enc"), str(output_directory / "file2.env"), b"mock_key", - validate_integrity=False, # Ensure the validate_integrity flag matches + validate_integrity=False, ) - # Ensure the output is clean and correct assert (output_directory / "file1.env").read_text() == "decrypted content" assert (output_directory / "file2.env").read_text() == "decrypted content" diff --git a/tests/test_cli_dry_run.py b/tests/test_cli_dry_run.py index 3f7dad0..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): """ @@ -40,7 +42,6 @@ def test_encrypt_dry_run_single_file(runner, mock_files): ], ) - assert result.exit_code == 0 assert "Output path already exists" in result.output @@ -149,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_encrypt.py b/tests/test_cli_encrypt.py index 26586d4..53d58a6 100644 --- a/tests/test_cli_encrypt.py +++ b/tests/test_cli_encrypt.py @@ -1,15 +1,10 @@ import os import json -import shutil -from pathlib import Path from unittest.mock import patch from click.testing import CliRunner import pytest from envcloak.cli import main -# Fixtures imported from conftest.py -# `isolated_mock_files` and `runner` - @patch("envcloak.commands.encrypt.encrypt_file") def test_encrypt(mock_encrypt_file, runner, isolated_mock_files): @@ -17,7 +12,7 @@ def test_encrypt(mock_encrypt_file, runner, isolated_mock_files): Test the `encrypt` CLI command. """ input_file = isolated_mock_files / "variables.env" - encrypted_file = isolated_mock_files / "variables.temp.enc" # Use unique temp file + encrypted_file = isolated_mock_files / "variables.temp.enc" key_file = isolated_mock_files / "mykey.key" def mock_encrypt(input_path, output_path, key): @@ -131,7 +126,7 @@ def mock_encrypt(input_path, output_path, key): ], ) - assert "Overwriting existing file" in result.output + assert "Overwriting existing directory" in result.output mock_encrypt_file.assert_any_call( str(directory / "file1.env"), str(output_directory / "file1.env.enc"), diff --git a/tests/test_cli_encrypt_preview.py b/tests/test_cli_encrypt_preview.py new file mode 100644 index 0000000..5ac3270 --- /dev/null +++ b/tests/test_cli_encrypt_preview.py @@ -0,0 +1,63 @@ +import pytest +from click.testing import CliRunner +from envcloak.commands.encrypt import encrypt + + +def test_preview_with_directory(runner, isolated_mock_files): + """ + Test the --preview flag with a directory. + Ensures that files in the directory are listed correctly without errors. + """ + result = runner.invoke( + encrypt, + [ + "--directory", + str(isolated_mock_files), + "--key-file", + str(isolated_mock_files / "mykey.key"), + "--preview", + ], + ) + assert result.exit_code == 0, f"Command failed with output: {result.output}" + assert "Files to be processed in directory" in result.output + + +def test_preview_with_single_file_error(runner, isolated_mock_files): + """ + Test the --preview flag with a single file (using --input). + Ensures that an error is raised when --preview is used with --input. + """ + result = runner.invoke( + encrypt, + [ + "--input", + str(isolated_mock_files / "variables.env"), + "--key-file", + str(isolated_mock_files / "mykey.key"), + "--preview", + ], + ) + assert ( + "The --preview option cannot be used with a single file (--input)." + in result.output + ) + + +def test_preview_with_empty_directory(runner, test_dir): + """ + Test the --preview flag with an empty directory. + Ensures that a message indicating no files are found is displayed. + """ + empty_dir = test_dir / "empty_dir" + empty_dir.mkdir() + result = runner.invoke( + encrypt, + [ + "--directory", + str(empty_dir), + "--key-file", + str(test_dir / "mykey.key"), + "--preview", + ], + ) + assert "ℹ️ No files found in directory" in result.output 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 4a09c9d..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,17 +218,17 @@ 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" - except Exception: - pass # Expected exception + except Exception as e: + print(e) 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")