gateway/tool_security_encrypt_config_value.py

431 lines
17 KiB
Python

#!/usr/bin/env python3
"""
Tool for encrypting configuration values.
This tool allows developers to encrypt secret values for use in configuration files.
It supports both text and JSON values and automatically determines the environment.
It can also encrypt all *_SECRET keys in an environment file at once.
Usage:
# Encrypt a single value
python tool_encrypt_config_value.py --value "my_secret_value" --env dev
python tool_encrypt_config_value.py --file "path/to/file.json" --env prod
# Encrypt all secrets in a file
python tool_encrypt_config_value.py --encrypt-all env_dev.env --env dev
python tool_encrypt_config_value.py --encrypt-all env_prod.env --env prod --dry-run
# Decrypt a value (for testing)
python tool_encrypt_config_value.py --decrypt "DEV_ENC:encrypted_value"
"""
import sys
import os
import json
import argparse
import shutil
from pathlib import Path
from datetime import datetime
# Add the modules directory to the Python path
sys.path.insert(0, str(Path(__file__).parent / 'modules'))
from modules.shared.configuration import encryptValue, decryptValue, isEncryptedValue
def get_env_type_from_file(file_path: Path) -> str:
"""
Read the APP_ENV_TYPE from the environment file.
Args:
file_path: Path to the environment file
Returns:
str: The environment type (dev, int, prod) or 'dev' as default
"""
if not file_path.exists():
return 'dev'
try:
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line.startswith('APP_ENV_TYPE') and '=' in line:
_, value = line.split('=', 1)
return value.strip().lower()
except Exception as e:
print(f"Warning: Could not read APP_ENV_TYPE from {file_path}: {e}")
return 'dev'
def is_any_encrypted_value(value: str) -> bool:
"""
Check if a value has any encryption prefix (DEV_ENC:, INT_ENC:, PROD_ENC:, etc.).
Args:
value: The value to check
Returns:
bool: True if the value has any encryption prefix, False otherwise
"""
if not value or not isinstance(value, str):
return False
# Check for any environment-specific encryption prefixes
return (value.startswith('DEV_ENC:') or
value.startswith('INT_ENC:') or
value.startswith('PROD_ENC:') or
value.startswith('TEST_ENC:') or
value.startswith('STAGING_ENC:'))
def find_secret_keys_in_file(file_path: Path) -> list:
"""
Find all *_SECRET keys in an environment file that are not encrypted.
This function now reads the APP_ENV_TYPE from the file itself and only
processes values that are completely unencrypted (no *_ENC: prefix).
Args:
file_path: Path to the environment file
Returns:
list: List of tuples (line_number, key, value, full_line)
"""
secret_keys = []
if not file_path.exists():
return secret_keys
# Get the environment type from the file itself
file_env_type = get_env_type_from_file(file_path)
print(f"📁 Detected environment type from file: {file_env_type.upper()}")
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
i = 0
while i < len(lines):
line = lines[i].strip()
# Skip empty lines and comments
if not line or line.startswith('#'):
i += 1
continue
# Check if line contains a key-value pair
if '=' in line:
key, value = line.split('=', 1)
key = key.strip()
value = value.strip()
# Check if it's a secret key and not already encrypted with ANY prefix
if key.endswith('_SECRET') and value and not is_any_encrypted_value(value):
# Check if value starts with { (JSON object)
if value.startswith('{'):
# Collect all lines until we find the closing }
json_lines = [value]
start_line = i + 1
i += 1
brace_count = value.count('{') - value.count('}')
while i < len(lines) and brace_count > 0:
json_lines.append(lines[i].rstrip('\n'))
brace_count += lines[i].count('{') - lines[i].count('}')
i += 1
# Join all lines and create the full JSON value
full_json_value = '\n'.join(json_lines)
secret_keys.append((start_line, key, full_json_value, line))
i -= 1 # Adjust for the loop increment
else:
# Single line value
secret_keys.append((i + 1, key, value, line))
# Check if it's a secret key with multiline JSON (value is just "{")
elif key.endswith('_SECRET') and value == '{' and not is_any_encrypted_value(value):
# Collect all lines until we find the closing }
json_lines = [value]
start_line = i + 1
i += 1
brace_count = 1 # We already have one opening brace
while i < len(lines) and brace_count > 0:
json_lines.append(lines[i].rstrip('\n'))
brace_count += lines[i].count('{') - lines[i].count('}')
i += 1
# Join all lines and create the full JSON value
full_json_value = '\n'.join(json_lines)
secret_keys.append((start_line, key, full_json_value, line))
i -= 1 # Adjust for the loop increment
i += 1
except Exception as e:
print(f"Error reading {file_path}: {e}")
return secret_keys
def backup_file(file_path: Path) -> Path:
"""
Create a backup of the file before modification.
Args:
file_path: Path to the file to backup
Returns:
Path: Path to the backup file
"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = file_path.with_suffix(f'.{timestamp}.backup')
shutil.copy2(file_path, backup_path)
return backup_path
def encrypt_all_secrets_in_file(file_path: Path, env_type: str, dry_run: bool = False, create_backup: bool = True) -> dict:
"""
Encrypt all non-encrypted secrets in a file.
The env_type parameter is now ignored - the function reads APP_ENV_TYPE from the file itself.
Args:
file_path: Path to the environment file
env_type: The environment type (ignored - read from file instead)
dry_run: If True, only show what would be changed
create_backup: If True, create a backup before modifying
Returns:
dict: Results of the encryption process
"""
# Get the environment type from the file itself
file_env_type = get_env_type_from_file(file_path)
results = {
'file': str(file_path),
'env_type': file_env_type, # Use the environment type from the file
'secrets_found': 0,
'secrets_encrypted': 0,
'errors': [],
'backup_created': None
}
# Find all secret keys (this function now reads APP_ENV_TYPE from the file)
secret_keys = find_secret_keys_in_file(file_path)
results['secrets_found'] = len(secret_keys)
if not secret_keys:
print(f"\n📁 Processing {file_path.name} ({file_env_type.upper()}):")
print(f" ✅ No unencrypted secrets found - all values already have encryption prefixes")
return results
print(f"\n📁 Processing {file_path.name} ({file_env_type.upper()}):")
print(f" Found {len(secret_keys)} non-encrypted secrets")
if dry_run:
print(" [DRY RUN] Would encrypt the following secrets:")
for line_num, key, value, full_line in secret_keys:
print(f" Line {line_num}: {key} = {value[:50]}{'...' if len(value) > 50 else ''}")
return results
# Create backup if requested
if create_backup:
try:
backup_path = backup_file(file_path)
results['backup_created'] = str(backup_path)
print(f" 📋 Backup created: {backup_path.name}")
except Exception as e:
results['errors'].append(f"Failed to create backup: {e}")
print(f" ⚠️ Warning: Could not create backup: {e}")
# Read the file content
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
except Exception as e:
results['errors'].append(f"Failed to read file: {e}")
return results
# Process each secret key
for line_num, key, value, full_line in secret_keys:
try:
print(f" 🔐 Encrypting {key}...")
# Encrypt the value using the environment type from the file
encrypted_value = encryptValue(value, file_env_type)
# Replace the line in the file content
new_line = f"{key} = {encrypted_value}\n"
lines[line_num - 1] = new_line
# If this was a multiline JSON, we need to remove the remaining lines
if value.startswith('{') and '\n' in value:
# Count how many lines the original JSON spanned
json_lines = value.split('\n')
lines_to_remove = len(json_lines) - 1 # -1 because we already replaced the first line
# Remove the remaining lines
for i in range(line_num, line_num + lines_to_remove):
if i < len(lines):
lines[i] = ""
results['secrets_encrypted'] += 1
print(f" ✓ Encrypted successfully")
except Exception as e:
error_msg = f"Failed to encrypt {key}: {e}"
results['errors'].append(error_msg)
print(f"{error_msg}")
# Write the modified content back to the file
if results['secrets_encrypted'] > 0:
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
print(f" 💾 File updated successfully")
except Exception as e:
results['errors'].append(f"Failed to write file: {e}")
print(f" ✗ Failed to write file: {e}")
return results
def main():
parser = argparse.ArgumentParser(description='Encrypt configuration values')
parser.add_argument('--value', '-v', help='Plain text value to encrypt')
parser.add_argument('--file', '-f', help='File containing the value to encrypt')
parser.add_argument('--env', '-e', choices=['dev', 'int', 'prod'],
help='Environment type (IGNORED - will read APP_ENV_TYPE from file instead)')
parser.add_argument('--decrypt', '-d', help='Decrypt an encrypted value (for testing)')
parser.add_argument('--interactive', '-i', action='store_true',
help='Interactive mode - prompt for value')
parser.add_argument('--encrypt-all', '-a', help='Encrypt all *_SECRET keys in the specified file')
parser.add_argument('--dry-run', action='store_true',
help='Show what would be changed without making changes (for --encrypt-all)')
parser.add_argument('--no-backup', action='store_true',
help='Skip creating backup files (for --encrypt-all)')
args = parser.parse_args()
try:
# Handle encrypt-all functionality
if args.encrypt_all:
file_path = Path(args.encrypt_all)
if not file_path.exists():
print(f"Error: File not found: {file_path}")
return 1
print("🔐 PowerOn Secret Encryption Tool")
print("=" * 50)
print("⚠️ IMPORTANT: The tool will read APP_ENV_TYPE from the file itself")
print("⚠️ Any --env parameter will be IGNORED for safety")
print()
if args.dry_run:
print("🔍 DRY RUN MODE - No changes will be made")
print()
results = encrypt_all_secrets_in_file(
file_path,
args.env or 'ignored', # The env parameter is ignored anyway
dry_run=args.dry_run,
create_backup=not args.no_backup
)
# Summary
print("\n" + "=" * 50)
print("📊 SUMMARY")
print("=" * 50)
print(f"File processed: {file_path.name}")
print(f"Secrets found: {results['secrets_found']}")
if not args.dry_run:
print(f"Secrets encrypted: {results['secrets_encrypted']}")
print(f"Errors: {len(results['errors'])}")
if len(results['errors']) == 0 and results['secrets_encrypted'] > 0:
print("\n🎉 All secrets encrypted successfully!")
elif len(results['errors']) > 0:
print(f"\n⚠️ Completed with {len(results['errors'])} errors")
else:
print("\n✅ No secrets needed encryption")
else:
print(f"Secrets that would be encrypted: {results['secrets_found']}")
# Show backup information
if results['backup_created']:
print(f"\n📋 Backup created: {Path(results['backup_created']).name}")
# Show errors if any
if results['errors']:
print(f"\n❌ Errors encountered:")
for error in results['errors']:
print(f" - {error}")
return 0 if len(results['errors']) == 0 else 1
# Handle decryption
if args.decrypt:
if isEncryptedValue(args.decrypt):
decrypted = decryptValue(args.decrypt)
print(f"Decrypted value: {decrypted}")
else:
print("Error: Value does not appear to be encrypted (missing ENV_ENC: prefix)")
return
# Determine the value to encrypt
value_to_encrypt = None
if args.value:
value_to_encrypt = args.value
elif args.file:
if not os.path.exists(args.file):
print(f"Error: File not found: {args.file}")
return
with open(args.file, 'r', encoding='utf-8') as f:
value_to_encrypt = f.read().strip()
elif args.interactive:
print("Enter the value to encrypt (press Ctrl+D when done):")
try:
value_to_encrypt = sys.stdin.read().strip()
except EOFError:
print("Error: No input provided")
return
else:
# Interactive mode by default
print("Enter the value to encrypt (press Ctrl+D when done):")
try:
value_to_encrypt = sys.stdin.read().strip()
except EOFError:
print("Error: No input provided")
return
if not value_to_encrypt:
print("Error: No value provided to encrypt")
return
# Validate JSON if it looks like JSON
if value_to_encrypt.strip().startswith('{'):
try:
json.loads(value_to_encrypt)
print("✓ Valid JSON detected")
except json.JSONDecodeError as e:
print(f"Warning: Value looks like JSON but is invalid: {e}")
response = input("Continue anyway? (y/N): ")
if response.lower() != 'y':
return
# Encrypt the value
encrypted_value = encryptValue(value_to_encrypt, args.env)
print(f"\n✓ Encryption successful!")
print(f"Environment: {args.env or 'current'}")
print(f"Encrypted value:")
print(f"{encrypted_value}")
print(f"\nCopy the above value to your configuration file.")
# Show usage example
print(f"\nUsage in config file:")
print(f"MY_SECRET_KEY = {encrypted_value}")
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == '__main__':
main()