Source code for aiorobokassa.utils.signature

"""Signature calculation and verification for RoboKassa."""

import hashlib
from typing import Dict, Optional, Union

from aiorobokassa.enums import SignatureAlgorithm
from aiorobokassa.exceptions import InvalidSignatureAlgorithmError

# Algorithm mapping
ALGORITHMS = {
    SignatureAlgorithm.MD5: hashlib.md5,
    SignatureAlgorithm.SHA256: hashlib.sha256,
    SignatureAlgorithm.SHA512: hashlib.sha512,
}


[docs] def calculate_signature( values: Dict[str, str], password: str, algorithm: Union[str, SignatureAlgorithm] = SignatureAlgorithm.MD5, ) -> str: """ Calculate signature for RoboKassa. Args: values: Dictionary of values to include in signature (sorted by key) password: Password for signature calculation algorithm: Hash algorithm (MD5, SHA256, SHA512) or SignatureAlgorithm enum Returns: Hexadecimal signature string Raises: InvalidSignatureAlgorithmError: If algorithm is not supported """ if isinstance(algorithm, str): try: algorithm = SignatureAlgorithm.from_string(algorithm) except ValueError as e: raise InvalidSignatureAlgorithmError(str(e)) from e sorted_items = sorted(values.items()) signature_string = ":".join(str(value) for _, value in sorted_items) signature_string += f":{password}" hash_func = ALGORITHMS.get(algorithm) if hash_func is None: raise InvalidSignatureAlgorithmError(f"Unsupported algorithm: {algorithm}") hash_obj = hash_func(signature_string.encode("utf-8")) return hash_obj.hexdigest().upper()
[docs] def verify_signature( values: Dict[str, str], password: str, received_signature: str, algorithm: Union[str, SignatureAlgorithm] = SignatureAlgorithm.MD5, ) -> bool: """ Verify signature from RoboKassa. Args: values: Dictionary of values used in signature password: Password for signature verification received_signature: Signature received from RoboKassa algorithm: Hash algorithm (MD5, SHA256, SHA512) Returns: True if signature is valid, False otherwise """ calculated_signature = calculate_signature(values, password, algorithm) return calculated_signature.upper() == received_signature.upper()
[docs] def calculate_payment_signature( merchant_login: str, out_sum: str, inv_id: Optional[str], password: str, algorithm: Union[str, SignatureAlgorithm] = SignatureAlgorithm.MD5, receipt: Optional[str] = None, shp_params: Optional[Dict[str, str]] = None, ) -> str: """ Calculate signature for payment URL. Signature format: MD5(merchant_login:out_sum:inv_id:receipt:Shp_param1:Shp_param2:...:password1) Order is FIXED: MerchantLogin:OutSum:InvId:Receipt:Shp_param1:Shp_param2:...:password1 If InvId is not provided, it must be empty but present (two colons: ::) If receipt is provided, it must be included in signature calculation. Shp_ parameters must be sorted alphabetically by key (without Shp_ prefix). Args: merchant_login: Merchant login out_sum: Payment amount inv_id: Invoice ID (optional) password: Password (password1) algorithm: Hash algorithm receipt: Receipt JSON string for fiscalization (optional) shp_params: Additional Shp_ parameters (without Shp_ prefix) (optional) Returns: Signature string """ if isinstance(algorithm, str): try: algorithm = SignatureAlgorithm.from_string(algorithm) except ValueError as e: raise InvalidSignatureAlgorithmError(str(e)) from e signature_parts = [merchant_login, out_sum] signature_parts.append(inv_id if inv_id else "") if receipt: signature_parts.append(receipt) signature_parts.append(password) if shp_params: sorted_shp = sorted(shp_params.items()) for key, value in sorted_shp: signature_parts.append(f"Shp_{key}={value}") signature_string = ":".join(signature_parts) hash_func = ALGORITHMS.get(algorithm) if hash_func is None: raise InvalidSignatureAlgorithmError(f"Unsupported algorithm: {algorithm}") hash_obj = hash_func(signature_string.encode("utf-8")) return hash_obj.hexdigest().upper()
[docs] def verify_result_url_signature( out_sum: str, inv_id: str, password: str, received_signature: str, algorithm: Union[str, SignatureAlgorithm] = SignatureAlgorithm.MD5, shp_params: Optional[Dict[str, str]] = None, ) -> bool: """ Verify signature from ResultURL notification. Signature format: MD5(OutSum:InvId:Shp_param1:Shp_param2:...:password2) Where Shp_ parameters are sorted alphabetically. Order is FIXED: OutSum:InvId (not sorted alphabetically) Args: out_sum: Payment amount inv_id: Invoice ID password: Password (password2) received_signature: Signature from notification algorithm: Hash algorithm shp_params: Additional Shp_ parameters (without Shp_ prefix) Returns: True if signature is valid """ if isinstance(algorithm, str): try: algorithm = SignatureAlgorithm.from_string(algorithm) except ValueError as e: raise InvalidSignatureAlgorithmError(str(e)) from e signature_parts = [out_sum, inv_id, password] if shp_params: sorted_shp = sorted(shp_params.items()) for key, value in sorted_shp: signature_parts.append(f"Shp_{key}={value}") signature_string = ":".join(signature_parts) hash_func = ALGORITHMS.get(algorithm) if hash_func is None: raise InvalidSignatureAlgorithmError(f"Unsupported algorithm: {algorithm}") hash_obj = hash_func(signature_string.encode("utf-8")) calculated_signature = hash_obj.hexdigest().upper() return calculated_signature == received_signature.upper()
[docs] def verify_success_url_signature( out_sum: str, inv_id: str, password: str, received_signature: str, algorithm: Union[str, SignatureAlgorithm] = SignatureAlgorithm.MD5, shp_params: Optional[Dict[str, str]] = None, ) -> bool: """ Verify signature from SuccessURL redirect. Signature format: MD5(OutSum:InvId:Shp_param1:Shp_param2:...:password1) Where Shp_ parameters are sorted alphabetically. Order is FIXED: OutSum:InvId (not sorted alphabetically) Args: out_sum: Payment amount inv_id: Invoice ID password: Password (password1) received_signature: Signature from redirect algorithm: Hash algorithm shp_params: Additional Shp_ parameters (without Shp_ prefix) Returns: True if signature is valid """ if isinstance(algorithm, str): try: algorithm = SignatureAlgorithm.from_string(algorithm) except ValueError as e: raise InvalidSignatureAlgorithmError(str(e)) from e signature_parts = [out_sum, inv_id, password] if shp_params: sorted_shp = sorted(shp_params.items()) for key, value in sorted_shp: signature_parts.append(f"Shp_{key}={value}") signature_string = ":".join(signature_parts) hash_func = ALGORITHMS.get(algorithm) if hash_func is None: raise InvalidSignatureAlgorithmError(f"Unsupported algorithm: {algorithm}") hash_obj = hash_func(signature_string.encode("utf-8")) calculated_signature = hash_obj.hexdigest().upper() return calculated_signature == received_signature.upper()
[docs] def calculate_split_signature( invoice_json: str, password: str, algorithm: Union[str, SignatureAlgorithm] = SignatureAlgorithm.MD5, ) -> str: """ Calculate signature for split payment. Signature format: MD5(invoice_json + password1) Where invoice_json is the JSON string in "pure" form (not URL-encoded). Args: invoice_json: Invoice JSON string (not URL-encoded) password: Password (password1 of master merchant) algorithm: Hash algorithm Returns: Hexadecimal signature string Raises: InvalidSignatureAlgorithmError: If algorithm is not supported """ if isinstance(algorithm, str): try: algorithm = SignatureAlgorithm.from_string(algorithm) except ValueError as e: raise InvalidSignatureAlgorithmError(str(e)) from e signature_string = invoice_json + password hash_func = ALGORITHMS.get(algorithm) if hash_func is None: raise InvalidSignatureAlgorithmError(f"Unsupported algorithm: {algorithm}") hash_obj = hash_func(signature_string.encode("utf-8")) return hash_obj.hexdigest().lower()