240 lines
8.3 KiB
Python
Executable File
240 lines
8.3 KiB
Python
Executable File
#!/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()
|
|
|