375 lines
14 KiB
Python
375 lines
14 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 shared.configuration import encrypt_value, decrypt_value, _is_encrypted_value
|
|
|
|
def find_secret_keys_in_file(file_path: Path) -> list:
|
|
"""
|
|
Find all *_SECRET keys in an environment file that are not encrypted.
|
|
|
|
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
|
|
|
|
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
|
|
if key.endswith('_SECRET') and value and not _is_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_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.
|
|
|
|
Args:
|
|
file_path: Path to the environment file
|
|
env_type: The environment type
|
|
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
|
|
"""
|
|
results = {
|
|
'file': str(file_path),
|
|
'env_type': env_type,
|
|
'secrets_found': 0,
|
|
'secrets_encrypted': 0,
|
|
'errors': [],
|
|
'backup_created': None
|
|
}
|
|
|
|
# Find all secret keys
|
|
secret_keys = find_secret_keys_in_file(file_path)
|
|
results['secrets_found'] = len(secret_keys)
|
|
|
|
if not secret_keys:
|
|
return results
|
|
|
|
print(f"\n📁 Processing {file_path.name} ({env_type}):")
|
|
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 existing function
|
|
encrypted_value = encrypt_value(value, 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 (default: current environment)')
|
|
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
|
|
|
|
if not args.env:
|
|
print("Error: --env is required when using --encrypt-all")
|
|
return 1
|
|
|
|
print("🔐 PowerOn Secret Encryption Tool")
|
|
print("=" * 50)
|
|
|
|
if args.dry_run:
|
|
print("🔍 DRY RUN MODE - No changes will be made")
|
|
print()
|
|
|
|
results = encrypt_all_secrets_in_file(
|
|
file_path,
|
|
args.env,
|
|
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 _is_encrypted_value(args.decrypt):
|
|
decrypted = decrypt_value(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 = encrypt_value(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()
|