Skip to content

Secure ID Serialization

By default, Graphinate uses a fast and simple ID serialization mechanism based on Python's repr() and base64. While convenient, this approach relies on ast.literal_eval(), which, although safer than eval(), might not meet the security requirements of high-risk environments (e.g., public-facing APIs where ID tampering is a concern).

This recipe demonstrates how to implement a Signed ID Converter using HMAC-SHA256 to ensure that IDs cannot be tampered with.

The Recipe

You can monkey-patch or wrap the default converters to add a cryptographic signature.

import ast
import base64
import hashlib
import hmac
import os
from typing import Any

import graphinate.converters

# 1. Define your secret key (load from env in production)
SECRET_KEY = os.environ.get("MY_APP_SECRET", "change-me-in-prod").encode()

def sign(payload: bytes) -> bytes:
    """Generate HMAC-SHA256 signature."""
    return hmac.new(SECRET_KEY, payload, hashlib.sha256).digest()

def secure_encode(value: Any, encoding: str = 'utf-8') -> str:
    """Encodes an object into a signed, Base64 string."""
    # 1. Serialize payload
    obj_s = repr(value)
    obj_b = obj_s.encode(encoding)

    # 2. Sign payload
    signature = sign(obj_b)

    # 3. Pack (Signature + Payload)
    packet = signature + obj_b

    # 4. Base64 Encode
    return base64.urlsafe_b64encode(packet).decode(encoding)

def secure_decode(value: str, encoding: str = 'utf-8') -> Any:
    """Decodes and verifies a signed ID."""
    try:
        packet = base64.urlsafe_b64decode(value.encode(encoding))
    except Exception:
        raise ValueError("Invalid Base64")

    if len(packet) < 32:
        raise ValueError("Token too short")

    # 1. Unpack
    signature = packet[:32]
    payload_b = packet[32:]

    # 2. Verify Signature
    expected_signature = sign(payload_b)
    if not hmac.compare_digest(signature, expected_signature):
        raise ValueError("Invalid Signature - ID tampered with!")

    # 3. Deserialize
    obj_s = payload_b.decode(encoding)
    return ast.literal_eval(obj_s)

# 4. Apply the patch
# Note: In a real application, you might want to subclass the Builder 
# or inject these functions rather than monkey-patching.
graphinate.converters.encode = secure_encode
graphinate.converters.decode = secure_decode

# Now all IDs generated by Graphinate will be signed!

How it Works

  1. Serialization: The object is converted to a string using repr().
  2. Signing: An HMAC-SHA256 hash is calculated for that string using your secret key.
  3. Packing: The signature (32 bytes) is prepended to the payload.
  4. Encoding: The combined packet is Base64 encoded to make it URL-safe.

Verification

When decoding:

  1. The Base64 string is decoded back to bytes.
  2. The signature is extracted.
  3. The signature is re-calculated based on the payload.
  4. If the calculated signature matches the extracted one, the payload is trusted and passed to ast.literal_eval().

Performance Impact

  • Size: Adds ~44 characters to the ID string.
  • CPU: Negligible overhead for HMAC calculation.