コンテンツにスキップ

pytilpack.crypto

pytilpack.crypto

署名・トークンユーティリティ。

pycrypto.py (AES-GCM暗号化) やsecrets.py (秘密鍵生成) を補完する HMAC署名・タイムスタンプ付きトークン機能。

TimestampSigner(key, purpose='', get_time=time.time)

タイムスタンプ付き署名トークンの生成・検証。

初期化。

引数:

名前 タイプ デスクリプション デフォルト
key str | bytes

署名鍵。

必須
purpose str

用途識別子。同じ鍵でも異なるpurposeのトークンは互換性がない。

''
get_time Callable[[], float]

現在時刻を返す関数。テスト用。

time
ソースコード位置: pytilpack/crypto.py
def __init__(
    self,
    key: str | bytes,
    purpose: str = "",
    get_time: typing.Callable[[], float] = time.time,
) -> None:
    """初期化。

    Args:
        key: 署名鍵。
        purpose: 用途識別子。同じ鍵でも異なるpurposeのトークンは互換性がない。
        get_time: 現在時刻を返す関数。テスト用。
    """
    key_bytes = key.encode() if isinstance(key, str) else key
    # purposeごとに鍵を導出し、用途間の分離を保証
    self.derived_key = hmac.new(key_bytes, purpose.encode(), hashlib.sha256).digest()
    self.get_time = get_time

sign(data)

データに署名してトークンを生成する。

Token format: base64url(version[1] + timestamp[8] + type[1] + data[...] + hmac[32])

ソースコード位置: pytilpack/crypto.py
def sign(self, data: str | bytes | dict) -> str:
    """データに署名してトークンを生成する。

    Token format: base64url(version[1] + timestamp[8] + type[1] + data[...] + hmac[32])
    """
    # データ型に応じてtype byteとペイロードを決定
    type_byte, payload = _encode_payload(data)

    # ヘッダ + ペイロードを構築
    header = struct.pack(">B", _VERSION) + struct.pack(">d", self.get_time())
    message = header + struct.pack(">B", type_byte) + payload

    # HMAC署名を付加
    sig = hmac.new(self.derived_key, message, hashlib.sha256).digest()
    return _b64url_encode(message + sig)

unsign(token, max_age=None)

トークンを検証してデータを取り出す。

発生:

タイプ デスクリプション
ValueError

検証失敗、期限切れ、バージョン不一致の場合。

ソースコード位置: pytilpack/crypto.py
def unsign(self, token: str, max_age: float | None = None) -> str | bytes | dict:
    """トークンを検証してデータを取り出す。

    Raises:
        ValueError: 検証失敗、期限切れ、バージョン不一致の場合。
    """
    raw = _b64url_decode(token)

    # 最小長チェック: version(1) + timestamp(8) + type(1) + hmac(32) = 42
    if len(raw) < 1 + 8 + 1 + _HMAC_SIZE:
        raise ValueError("トークンが短すぎます")

    # 署名を分離して検証
    message = raw[:-_HMAC_SIZE]
    sig = raw[-_HMAC_SIZE:]
    expected = hmac.new(self.derived_key, message, hashlib.sha256).digest()
    if not hmac.compare_digest(sig, expected):
        raise ValueError("署名が一致しません")

    # ヘッダ解析
    version = message[0]
    if version != _VERSION:
        raise ValueError(f"未対応のバージョン: {version}")

    timestamp = struct.unpack(">d", message[1:9])[0]
    type_byte = message[9]
    payload = message[10:]

    # 有効期限チェック
    if max_age is not None:
        age = self.get_time() - timestamp
        if age > max_age:
            raise ValueError(f"トークンが期限切れです (経過: {age:.1f}秒)")

    return _decode_payload(type_byte, payload)

hmac_sign(data, key)

HMAC-SHA256で署名する。

戻り値:

タイプ デスクリプション
str

URL-safe base64エンコードされた署名文字列。

ソースコード位置: pytilpack/crypto.py
def hmac_sign(data: str | bytes, key: str | bytes) -> str:
    """HMAC-SHA256で署名する。

    Returns:
        URL-safe base64エンコードされた署名文字列。
    """
    data_bytes = data.encode() if isinstance(data, str) else data
    key_bytes = key.encode() if isinstance(key, str) else key
    sig = hmac.new(key_bytes, data_bytes, hashlib.sha256).digest()
    return _b64url_encode(sig)

hmac_verify(data, signature, key)

HMAC-SHA256署名を検証する。タイミングセーフな比較を使用。

ソースコード位置: pytilpack/crypto.py
def hmac_verify(data: str | bytes, signature: str, key: str | bytes) -> bool:
    """HMAC-SHA256署名を検証する。タイミングセーフな比較を使用。"""
    expected = hmac_sign(data, key)
    return hmac.compare_digest(expected, signature)