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 @@
+
+⚠️ 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!"


+



@@ -57,6 +66,9 @@ envcloak generate-key-from-password --password "YourTopSecretPassword" --output
# From random password and salt
envcloak generate-key --output secretkey.key
```
+
+
+
> **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
```
+
+
+
> **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
```
+
+
+
> **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")