Source code for gntplib.keys

#!/usr/bin/env python3

# File: gntplib/keys.py
# Author: Hadi Cahyadi <cumulus13@gmail.com>
# Date: 2025-12-25
# Description: Authentication and key management for GNTP messages.
# License: MIT

"""Authentication and key management for GNTP messages.

This module provides secure key generation and hashing for GNTP authentication.
Supports MD5, SHA1, SHA256, and SHA512 hashing algorithms.
"""

import binascii
import hashlib
import secrets
from typing import Optional

from .constants import random_bytes

__all__ = ['MD5', 'SHA1', 'SHA256', 'SHA512', 'Key', 'Algorithm']


# Hashing algorithm constraints
MIN_SALT_BYTES: int = 8  # Increased from 4 for better security
MAX_SALT_BYTES: int = 16
DEFAULT_SALT_BYTES: int = 16


[docs] class Algorithm: """Factory class for creating authentication keys with specific hash algorithms. This class represents a hashing algorithm configuration that can be used to create Key instances. Attributes: algorithm_id: String identifier for the algorithm (e.g., 'SHA256') key_size: Size of the hash output in bytes """
[docs] def __init__(self, algorithm_id: str, key_size: int): """Initialize algorithm configuration. Args: algorithm_id: Algorithm identifier (MD5, SHA1, SHA256, SHA512) key_size: Expected hash output size in bytes """ self.algorithm_id = algorithm_id self.key_size = key_size
[docs] def key(self, password: str, salt: Optional[bytes] = None) -> 'Key': """Create a Key instance using this algorithm. Args: password: Password for authentication salt: Optional salt bytes. If None, random salt is generated Returns: Key instance configured with this algorithm Example: >>> algo = SHA256 >>> key = algo.key('mypassword') """ return Key(password, self.algorithm_id, salt)
[docs] def __repr__(self) -> str: """Return string representation of algorithm.""" return f"Algorithm(id={self.algorithm_id}, key_size={self.key_size})"
# Pre-configured algorithm instances MD5 = Algorithm('MD5', 16) # 128-bit, 16 bytes, 32 chars hex SHA1 = Algorithm('SHA1', 20) # 160-bit, 20 bytes, 40 chars hex SHA256 = Algorithm('SHA256', 32) # 256-bit, 32 bytes, 64 chars hex (recommended) SHA512 = Algorithm('SHA512', 64) # 512-bit, 64 bytes, 128 chars hex def random_salt(num_bytes: int = DEFAULT_SALT_BYTES) -> bytes: """Generate cryptographically secure random salt. Args: num_bytes: Number of salt bytes to generate (default: 16) Returns: Random salt bytes Raises: ValueError: If num_bytes is outside valid range Example: >>> salt = random_salt(16) >>> len(salt) 16 """ if not MIN_SALT_BYTES <= num_bytes <= MAX_SALT_BYTES: raise ValueError( f"Salt size must be between {MIN_SALT_BYTES} and {MAX_SALT_BYTES} bytes" ) return random_bytes(num_bytes)
[docs] class Key: """Authentication key for GNTP messages. This class handles password-based authentication by generating secure hashes using a salt. The key is used for both authentication and optional encryption of GNTP messages. Attributes: password: Original password (stored as UTF-8 bytes) algorithm_id: Hash algorithm identifier salt: Random salt used in key derivation key: Derived key from password and salt key_hash: Hash of the derived key """
[docs] def __init__( self, password: str, algorithm_id: str = 'SHA256', salt: Optional[bytes] = None ): """Initialize authentication key. Args: password: Password for authentication algorithm_id: Hash algorithm (MD5, SHA1, SHA256, SHA512) salt: Optional salt. If None, random salt is generated Raises: ValueError: If algorithm_id is not supported Example: >>> key = Key('mypassword', 'SHA256') >>> key.key_hash_hex b'...' """ if algorithm_id not in ['MD5', 'SHA1', 'SHA256', 'SHA512']: raise ValueError( f"Unsupported algorithm: {algorithm_id}. " f"Use MD5, SHA1, SHA256, or SHA512" ) self.password = password.encode('utf-8') self.algorithm_id = algorithm_id self.salt = salt if salt is not None else random_salt() # Get hash function hash_func = getattr(hashlib, algorithm_id.lower()) # Derive key from password and salt key_basis = self.password + self.salt self.key = hash_func(key_basis).digest() # Create hash of the key for verification self.key_hash = hash_func(self.key).digest()
@property def salt_hex(self) -> bytes: """Get hex-encoded salt. Returns: Hex-encoded salt as bytes """ return binascii.hexlify(self.salt) @property def key_hex(self) -> bytes: """Get hex-encoded key. Returns: Hex-encoded key as bytes """ return binascii.hexlify(self.key) @property def key_hash_hex(self) -> bytes: """Get hex-encoded key hash. Returns: Hex-encoded key hash as bytes """ return binascii.hexlify(self.key_hash)
[docs] def verify_password(self, password: str) -> bool: """Verify if a password matches this key. Args: password: Password to verify Returns: True if password matches, False otherwise Example: >>> key = Key('secret', 'SHA256') >>> key.verify_password('secret') True >>> key.verify_password('wrong') False """ # Create temporary key with same salt temp_key = Key(password, self.algorithm_id, self.salt) return temp_key.key == self.key
[docs] def __repr__(self) -> str: """Return string representation of key.""" return ( f"Key(algorithm={self.algorithm_id}, " f"salt_length={len(self.salt)})" )
[docs] def __eq__(self, other) -> bool: """Check equality with another Key instance.""" if not isinstance(other, Key): return False return ( self.algorithm_id == other.algorithm_id and self.salt == other.salt and self.key == other.key )