#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ SQL generator for creating Guacamole user with custom password Uses same hashing algorithm as Guacamole: - SHA-256(password_bytes + salt_bytes) - Random 32-byte salt Usage: python generate_guacamole_user.py --username admin --password MySecurePass123 python generate_guacamole_user.py --username admin --password MySecurePass123 --admin """ import hashlib import secrets import argparse import sys import io # Fix Windows encoding issues if sys.platform == 'win32': sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') def generate_guacamole_password_hash(password: str) -> tuple[bytes, bytes]: """ Generate hash and salt for Guacamole password CORRECT ALGORITHM (verified 2025-10-29): Guacamole uses: SHA-256(password_string + salt_hex_string) IMPORTANT: Salt converted to HEX string BEFORE hashing! Args: password: Password in plain text Returns: Tuple (password_hash, password_salt) as bytes for PostgreSQL """ # Generate random 32-byte salt salt_bytes = secrets.token_bytes(32) # CRITICAL: Convert salt to HEX STRING (uppercase) # Guacamole hashes: password + hex(salt), NOT password + binary(salt)! salt_hex_string = salt_bytes.hex().upper() # Compute SHA-256(password_string + salt_hex_string) # Concatenate password STRING + salt HEX STRING, then encode to UTF-8 hash_input = password + salt_hex_string password_hash = hashlib.sha256(hash_input.encode('utf-8')).digest() return password_hash, salt_bytes def bytes_to_postgres_hex(data: bytes) -> str: """ Convert bytes to PostgreSQL hex format for decode() Args: data: Bytes to convert Returns: String in 'HEXSTRING' format for use in decode('...', 'hex') """ return data.hex().upper() def generate_sql(username: str, password: str, is_admin: bool = False) -> str: """ Generate SQL for creating Guacamole user Args: username: Username password: Password is_admin: If True, grant full administrator privileges Returns: SQL script to execute """ password_hash, password_salt = generate_guacamole_password_hash(password) hash_hex = bytes_to_postgres_hex(password_hash) salt_hex = bytes_to_postgres_hex(password_salt) sql = f"""-- Generated Guacamole user creation SQL -- Username: {username} -- Password: {'*' * len(password)} (length: {len(password)}) -- Generated with: generate_guacamole_user.py -- Create user entity INSERT INTO guacamole_entity (name, type) VALUES ('{username}', 'USER'); -- Create user with password hash INSERT INTO guacamole_user (entity_id, password_hash, password_salt, password_date) SELECT entity_id, decode('{hash_hex}', 'hex'), decode('{salt_hex}', 'hex'), CURRENT_TIMESTAMP FROM guacamole_entity WHERE name = '{username}' AND guacamole_entity.type = 'USER'; """ if is_admin: sql += f""" -- Grant all system permissions (administrator) INSERT INTO guacamole_system_permission (entity_id, permission) SELECT entity_id, permission::guacamole_system_permission_type FROM ( VALUES ('{username}', 'CREATE_CONNECTION'), ('{username}', 'CREATE_CONNECTION_GROUP'), ('{username}', 'CREATE_SHARING_PROFILE'), ('{username}', 'CREATE_USER'), ('{username}', 'CREATE_USER_GROUP'), ('{username}', 'ADMINISTER') ) permissions (username, permission) JOIN guacamole_entity ON permissions.username = guacamole_entity.name AND guacamole_entity.type = 'USER'; -- Grant permission to read/update/administer self INSERT INTO guacamole_user_permission (entity_id, affected_user_id, permission) SELECT guacamole_entity.entity_id, guacamole_user.user_id, permission::guacamole_object_permission_type FROM ( VALUES ('{username}', '{username}', 'READ'), ('{username}', '{username}', 'UPDATE'), ('{username}', '{username}', 'ADMINISTER') ) permissions (username, affected_username, permission) JOIN guacamole_entity ON permissions.username = guacamole_entity.name AND guacamole_entity.type = 'USER' JOIN guacamole_entity affected ON permissions.affected_username = affected.name AND guacamole_entity.type = 'USER' JOIN guacamole_user ON guacamole_user.entity_id = affected.entity_id; """ return sql def main(): parser = argparse.ArgumentParser( description='Generate SQL for creating Guacamole user with custom password', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Create regular user python generate_guacamole_user.py --username john --password SecurePass123 # Create administrator user python generate_guacamole_user.py --username admin --password AdminPass456 --admin # Save to file python generate_guacamole_user.py --username admin --password AdminPass456 --admin > 002-custom-admin.sql # Apply directly to running database python generate_guacamole_user.py --username admin --password AdminPass456 --admin | \\ docker compose exec -T postgres psql -U guacamole_user -d guacamole_db SECURITY NOTES: - Never commit generated SQL files with passwords to git! - Use strong passwords (minimum 16 characters, mixed case, numbers, symbols) - Change default passwords immediately after deployment - Store passwords securely (password manager, secrets vault) """ ) parser.add_argument( '--username', required=True, help='Username for the new Guacamole user' ) parser.add_argument( '--password', required=True, help='Password for the new user (plain text)' ) parser.add_argument( '--admin', action='store_true', help='Grant administrator privileges (ADMINISTER system permission)' ) parser.add_argument( '--verify', action='store_true', help='Verify password by generating hash twice' ) args = parser.parse_args() # Validate password strength if len(args.password) < 8: print("[WARNING] Password is too short (< 8 characters)", file=sys.stderr) print(" Recommended: minimum 16 characters with mixed case, numbers, symbols", file=sys.stderr) response = input("Continue anyway? (y/N): ") if response.lower() != 'y': sys.exit(1) # Verify if requested if args.verify: print("[VERIFY] Verifying hash generation...", file=sys.stderr) hash1, salt1 = generate_guacamole_password_hash(args.password) hash2, salt2 = generate_guacamole_password_hash(args.password) # Salts should be different (random) if salt1 == salt2: print("[ERROR] Salt generation not random!", file=sys.stderr) sys.exit(1) # But if we use same salt, hash should be same # Use correct algorithm: SHA256(password_string + salt_hex_string) salt_hex_string = salt1.hex().upper() hash_test = hashlib.sha256((args.password + salt_hex_string).encode('utf-8')).digest() if hash_test == hash1: print("[OK] Hash generation verified", file=sys.stderr) else: print("[ERROR] Hash generation mismatch!", file=sys.stderr) sys.exit(1) # Generate SQL sql = generate_sql(args.username, args.password, args.admin) # Output print(sql) # Print info to stderr (so it doesn't interfere with piping SQL) role = "Administrator" if args.admin else "Regular User" print(f"\n[OK] SQL generated successfully!", file=sys.stderr) print(f" Username: {args.username}", file=sys.stderr) print(f" Role: {role}", file=sys.stderr) print(f" Password length: {len(args.password)} characters", file=sys.stderr) print(f"\n[INFO] To apply this SQL:", file=sys.stderr) print(f" docker compose exec -T postgres psql -U guacamole_user -d guacamole_db < output.sql", file=sys.stderr) if __name__ == '__main__': main()