#!/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
)