콘텐츠로 이동

spakky-cryptography

암호화 utility와 auth snapshot/password provider API입니다.

패키지 루트

Cryptography provider plugin public API.

AuthContextSnapshotVerificationResult(*, decision, auth_context=None) dataclass

Decision plus optional AuthContext produced by snapshot verification.

decision instance-attribute

ALLOW, CHALLENGE, or ERROR decision for the verification attempt.

auth_context = None class-attribute instance-attribute

Verified auth context when decision is ALLOW.

CryptographyAuthProvider(config)

Bases: IAuthContextSnapshotSigner, IAuthContextSnapshotVerifier, IPasswordHasher, IPasswordVerifier

Cryptography-backed provider for snapshot and password auth capabilities.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/auth_provider.py
def __init__(
    self,
    config: CryptographyAuthProviderConfig,
) -> None:
    self._config = config

sign_snapshot(request)

Create a signed AuthContextSnapshot envelope.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/auth_provider.py
@override
def sign_snapshot(self, request: SnapshotSignRequest) -> AuthContextSnapshot:
    """Create a signed AuthContextSnapshot envelope."""
    issued_at = self._aware_datetime(self._config.clock())
    tenant = (
        request.tenant
        if request.tenant is not None
        else request.auth_context.tenant
    )
    unsigned_payload = self._unsigned_payload(
        auth_context=request.auth_context,
        tenant=tenant,
        issued_at=issued_at,
        expires_at=issued_at + self._config.snapshot_ttl,
    )
    signature = HMAC.sign_text(
        self._config.snapshot_key,
        HMACType.HS256,
        self._canonical_json(unsigned_payload),
        url_safe=True,
    )
    return AuthContextSnapshot(
        subject=request.auth_context.subject,
        issuer=request.auth_context.issuer,
        issued_at=issued_at,
        expires_at=issued_at + self._config.snapshot_ttl,
        signature=AuthContextSnapshotSignature(
            key_id=self._config.snapshot_key_id,
            algorithm=SNAPSHOT_SIGNATURE_ALGORITHM,
            signature=signature,
        ),
        tenant=tenant,
        roles=request.auth_context.roles,
        scopes=request.auth_context.scopes,
        selected_claims=request.auth_context.claims,
    )

verify_snapshot(snapshot_envelope, invocation)

Verify a signed snapshot envelope and return its AuthContext.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/auth_provider.py
@override
def verify_snapshot(
    self,
    snapshot_envelope: str,
    invocation: AuthInvocation,
) -> AuthContext:
    """Verify a signed snapshot envelope and return its AuthContext."""
    if not self._config.verification_available:
        raise AuthVerificationProviderUnavailableError()
    if snapshot_envelope == "":
        raise MissingAuthContextSnapshotError()
    payload = self._decode_envelope(snapshot_envelope)
    self._verify_payload_signature(payload)
    expires_at = self._datetime_value(payload, "expires_at")
    if expires_at < self._aware_datetime(self._config.clock()):
        raise ExpiredAuthContextSnapshotError()
    return self._auth_context_from_payload(payload)

verify_snapshot_result(snapshot_envelope, invocation)

Verify a snapshot envelope and map auth errors to decisions.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/auth_provider.py
def verify_snapshot_result(
    self,
    snapshot_envelope: str,
    invocation: AuthInvocation,
) -> AuthContextSnapshotVerificationResult:
    """Verify a snapshot envelope and map auth errors to decisions."""
    try:
        auth_context = self.verify_snapshot(snapshot_envelope, invocation)
    except MissingAuthContextSnapshotError:
        return AuthContextSnapshotVerificationResult(
            decision=AuthorizationDecision.challenge(
                AuthorizationReasonCode.SNAPSHOT_MISSING
            )
        )
    except InvalidAuthContextSnapshotError:
        return AuthContextSnapshotVerificationResult(
            decision=AuthorizationDecision.challenge(
                AuthorizationReasonCode.SNAPSHOT_INVALID
            )
        )
    except ExpiredAuthContextSnapshotError:
        return AuthContextSnapshotVerificationResult(
            decision=AuthorizationDecision.challenge(
                AuthorizationReasonCode.SNAPSHOT_EXPIRED
            )
        )
    except AuthVerificationProviderUnavailableError:
        return AuthContextSnapshotVerificationResult(
            decision=AuthorizationDecision.error(
                AuthorizationReasonCode.VERIFICATION_PROVIDER_UNAVAILABLE
            )
        )
    return AuthContextSnapshotVerificationResult(
        decision=AuthorizationDecision.allow(),
        auth_context=auth_context,
    )

hash_password(password)

Hash plaintext password material for storage.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/auth_provider.py
@override
def hash_password(self, password: AuthPasswordPlaintext) -> AuthPasswordHash:
    """Hash plaintext password material for storage."""
    if not self._config.password_available:
        raise AuthVerificationProviderUnavailableError()
    return BcryptPasswordEncoder(password=password).encode()

verify_password(password, password_hash)

Verify plaintext password material against a retained password hash.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/auth_provider.py
@override
def verify_password(
    self,
    password: AuthPasswordPlaintext,
    password_hash: AuthPasswordHash,
) -> AuthorizationDecision:
    """Verify plaintext password material against a retained password hash."""
    if not self._config.password_available:
        return AuthorizationDecision.error(
            AuthorizationReasonCode.VERIFICATION_PROVIDER_UNAVAILABLE
        )
    try:
        if self._password_encoder(password_hash).challenge(password):
            return AuthorizationDecision.allow()
    except Exception:
        return AuthorizationDecision.challenge(
            AuthorizationReasonCode.INVALID_CREDENTIAL
        )
    return AuthorizationDecision.challenge(
        AuthorizationReasonCode.INVALID_CREDENTIAL
    )

CryptographyAuthProviderConfig()

Bases: BaseSettings

Runtime config for cryptography auth provider capabilities.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/auth_provider.py
def __init__(self) -> None:
    super().__init__()

snapshot_key = Field(default_factory=(lambda: Key(size=32))) class-attribute instance-attribute

HMAC key used to sign and verify AuthContextSnapshot envelopes.

snapshot_key_id = 'spakky-cryptography:default' class-attribute instance-attribute

Identifier carried in signed snapshot envelopes.

snapshot_ttl = timedelta(minutes=5) class-attribute instance-attribute

Validity window for newly signed snapshots.

clock = _utc_now class-attribute instance-attribute

Clock used for signing and expiration validation.

verification_available = True class-attribute instance-attribute

Whether snapshot verification provider dependencies are available.

password_available = True class-attribute instance-attribute

Whether password hashing provider dependencies are available.

Aes(key, url_safe=False)

Bases: ICryptor

AES-CBC encryption/decryption implementation.

Uses 256-bit keys (32 bytes) with automatic PKCS7 padding and random IV generation for each encryption operation.

Initialize AES encryptor.

Parameters:

Name Type Description Default
key Key

256-bit (32-byte) encryption key.

required
url_safe bool

Use URL-safe Base64 encoding for cipher text.

False

Raises:

Type Description
KeySizeError

If key is not 32 bytes.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/aes.py
def __init__(self, key: Key, url_safe: bool = False) -> None:
    """Initialize AES encryptor.

    Args:
        key: 256-bit (32-byte) encryption key.
        url_safe: Use URL-safe Base64 encoding for cipher text.

    Raises:
        KeySizeError: If key is not 32 bytes.
    """
    if key.length != self.KEY_SIZE:
        raise KeySizeError
    self.url_safe = url_safe
    self.__key = key

encrypt(message)

Encrypt a message using AES-CBC.

Parameters:

Name Type Description Default
message str

Plain text message to encrypt.

required

Returns:

Type Description
str

Encrypted cipher text in format "iv:cipher" (Base64 encoded).

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/aes.py
@override
def encrypt(self, message: str) -> str:
    """Encrypt a message using AES-CBC.

    Args:
        message: Plain text message to encrypt.

    Returns:
        Encrypted cipher text in format "iv:cipher" (Base64 encoded).
    """
    plain_bytes: bytes = pad(message.encode(), AES.block_size)
    iv: Key = Key(size=16)
    cryptor = AES.new(  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
        key=self.__key.binary,
        mode=AES.MODE_CBC,
        iv=iv.binary,
    )
    cipher_bytes: bytes = cryptor.encrypt(plain_bytes)
    return f"{iv.b64_urlsafe if self.url_safe else iv.b64}:{Base64Encoder.from_bytes(cipher_bytes, self.url_safe)}"

decrypt(cipher)

Decrypt a cipher text using AES-CBC.

Parameters:

Name Type Description Default
cipher str

Cipher text in format "iv:cipher" (Base64 encoded).

required

Returns:

Type Description
str

Decrypted plain text message.

Raises:

Type Description
DecryptionFailedError

If decryption fails.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/aes.py
@override
def decrypt(self, cipher: str) -> str:
    """Decrypt a cipher text using AES-CBC.

    Args:
        cipher: Cipher text in format "iv:cipher" (Base64 encoded).

    Returns:
        Decrypted plain text message.

    Raises:
        DecryptionFailedError: If decryption fails.
    """
    try:
        [iv, cipher] = cipher.split(":")
        iv_bytes: bytes = Base64Encoder.get_bytes(iv, self.url_safe)
        cipher_bytes: bytes = Base64Encoder.get_bytes(cipher, self.url_safe)
        cryptor = AES.new(  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
            key=self.__key.binary,
            mode=AES.MODE_CBC,
            iv=iv_bytes,
        )
        plain_bytes: bytes = cryptor.decrypt(cipher_bytes)
        return unpad(plain_bytes, AES.block_size).decode()
    except Exception as e:
        raise DecryptionFailedError from e

Gcm(key, url_safe=False)

Bases: ICryptor

AES-GCM authenticated encryption/decryption implementation.

Uses 256-bit keys (32 bytes) with automatic PKCS7 padding, random IV, and AAD generation for authenticated encryption operations.

Initialize AES-GCM encryptor.

Parameters:

Name Type Description Default
key Key

256-bit (32-byte) encryption key.

required
url_safe bool

Use URL-safe Base64 encoding for cipher text.

False

Raises:

Type Description
KeySizeError

If key is not 32 bytes.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/gcm.py
def __init__(self, key: Key, url_safe: bool = False) -> None:
    """Initialize AES-GCM encryptor.

    Args:
        key: 256-bit (32-byte) encryption key.
        url_safe: Use URL-safe Base64 encoding for cipher text.

    Raises:
        KeySizeError: If key is not 32 bytes.
    """
    if key.length != self.KEY_SIZE:
        raise KeySizeError
    self.url_safe = url_safe
    self.__key = key

encrypt(message)

Encrypt a message using AES-GCM.

Parameters:

Name Type Description Default
message str

Plain text message to encrypt.

required

Returns:

Type Description
str

Encrypted cipher text in format "aad:tag:iv:cipher" (Base64 encoded).

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/gcm.py
@override
def encrypt(self, message: str) -> str:
    """Encrypt a message using AES-GCM.

    Args:
        message: Plain text message to encrypt.

    Returns:
        Encrypted cipher text in format "aad:tag:iv:cipher" (Base64 encoded).
    """
    plain_bytes: bytes = pad(message.encode(), AES.block_size)
    aad: Key = Key(size=16)
    iv: Key = Key(size=12)
    cryptor = AES.new(  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
        key=self.__key.binary,
        mode=AES.MODE_GCM,
        nonce=iv.binary,
    )
    cryptor.update(aad.binary)
    cipher_bytes, tag_bytes = cryptor.encrypt_and_digest(plain_bytes)
    return f"{aad.b64_urlsafe if self.url_safe else aad.b64}:{Base64Encoder.from_bytes(tag_bytes, self.url_safe)}:{iv.b64_urlsafe if self.url_safe else iv.b64}:{Base64Encoder.from_bytes(cipher_bytes, self.url_safe)}"

decrypt(cipher)

Decrypt a cipher text using AES-GCM.

Parameters:

Name Type Description Default
cipher str

Cipher text in format "aad:tag:iv:cipher" (Base64 encoded).

required

Returns:

Type Description
str

Decrypted plain text message.

Raises:

Type Description
DecryptionFailedError

If decryption or authentication fails.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/gcm.py
@override
def decrypt(self, cipher: str) -> str:
    """Decrypt a cipher text using AES-GCM.

    Args:
        cipher: Cipher text in format "aad:tag:iv:cipher" (Base64 encoded).

    Returns:
        Decrypted plain text message.

    Raises:
        DecryptionFailedError: If decryption or authentication fails.
    """
    try:
        [aad, tag, iv, cipher] = cipher.split(":")
        aad_bytes: bytes = Base64Encoder.get_bytes(aad, self.url_safe)
        tag_bytes: bytes = Base64Encoder.get_bytes(tag, self.url_safe)
        iv_bytes: bytes = Base64Encoder.get_bytes(iv, self.url_safe)
        cipher_bytes: bytes = Base64Encoder.get_bytes(cipher, self.url_safe)
        cryptor = AES.new(  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
            key=self.__key.binary,
            mode=AES.MODE_GCM,
            nonce=iv_bytes,
        )
        cryptor.update(aad_bytes)
        plain_bytes: bytes = cryptor.decrypt_and_verify(cipher_bytes, tag_bytes)
        return unpad(plain_bytes, AES.block_size).decode()
    except Exception as e:
        raise DecryptionFailedError from e

ICryptor

Bases: ABC

Interface for encryption and decryption operations.

ISigner

Bases: ABC

Interface for digital signature operations.

AsymmetricKey(key=None, size=None, passphrase=None)

AsymmetricKey(*, key: str, passphrase: str | None = None)
AsymmetricKey(*, key: bytes, passphrase: str | None = None)
AsymmetricKey(*, size: int, passphrase: str | None = None)

RSA asymmetric key pair.

Manages RSA public/private key pairs with support for key generation, import/export, and passphrase protection. Supports 1024, 2048, 4096, and 8192-bit keys.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/rsa.py
def __init__(
    self,
    key: str | bytes | None = None,
    size: int | None = None,
    passphrase: str | None = None,
) -> None:
    if (
        key is None and size is None
    ):  # pragma: no cover - overloads prevent this at type-check time
        raise AsymmetricKeyRequiredError
    if key is not None:
        try:
            imported_key = RSA.import_key(key, passphrase)
            if (key_size := imported_key.size_in_bits()) not in self.KEY_SIZES:
                raise KeySizeError(key_size * 8)
            self.__key = imported_key
        except (ValueError, IndexError, TypeError) as e:
            raise CannotImportAsymmetricKeyError from e
    if size is not None:
        if size not in self.KEY_SIZES:
            raise KeySizeError(size)
        self.__key = RSA.generate(size)
    if self.__key.has_private():
        self.__private_key = Key(
            binary=self.__key.export_key(passphrase=passphrase)
        )
    self.__public_key = Key(binary=self.__key.public_key().export_key())

is_private property

Check if this key pair includes a private key.

private_key property

Get the private key, or None if this is a public key only.

public_key property

Get the public key.

Rsa(key, url_safe=False)

Bases: ICryptor, ISigner

RSA encryption/decryption and signing/verification.

Provides PKCS1_OAEP encryption/decryption and PKCS1_v1_5 signing/verification using RSA asymmetric keys. Encryption uses the public key, decryption and signing require the private key.

Initialize RSA cryptor/signer.

Parameters:

Name Type Description Default
key AsymmetricKey

RSA asymmetric key pair.

required
url_safe bool

Use URL-safe Base64 encoding for cipher/signature.

False
Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/rsa.py
def __init__(self, key: AsymmetricKey, url_safe: bool = False) -> None:
    """Initialize RSA cryptor/signer.

    Args:
        key: RSA asymmetric key pair.
        url_safe: Use URL-safe Base64 encoding for cipher/signature.
    """
    self.url_safe = url_safe
    self.__key = key

encrypt(message)

Encrypt a message using RSA public key.

Parameters:

Name Type Description Default
message str

Plain text message to encrypt.

required

Returns:

Type Description
str

Encrypted cipher text (Base64 encoded).

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/rsa.py
@override
def encrypt(self, message: str) -> str:
    """Encrypt a message using RSA public key.

    Args:
        message: Plain text message to encrypt.

    Returns:
        Encrypted cipher text (Base64 encoded).
    """
    cryptor = PKCS1_OAEP.new(RSA.import_key(self.__key.public_key.binary))
    cipher_bytes: bytes = cryptor.encrypt(message.encode())
    return Base64Encoder.from_bytes(cipher_bytes, self.url_safe)

decrypt(cipher)

Decrypt a cipher text using RSA private key.

Parameters:

Name Type Description Default
cipher str

Cipher text to decrypt (Base64 encoded).

required

Returns:

Type Description
str

Decrypted plain text message.

Raises:

Type Description
PrivateKeyRequiredError

If key pair has no private key.

DecryptionFailedError

If decryption fails.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/rsa.py
@override
def decrypt(self, cipher: str) -> str:
    """Decrypt a cipher text using RSA private key.

    Args:
        cipher: Cipher text to decrypt (Base64 encoded).

    Returns:
        Decrypted plain text message.

    Raises:
        PrivateKeyRequiredError: If key pair has no private key.
        DecryptionFailedError: If decryption fails.
    """
    if self.__key.private_key is None:
        raise PrivateKeyRequiredError
    try:
        cipher_bytes: bytes = Base64Encoder.get_bytes(cipher, self.url_safe)
        cryptor = PKCS1_OAEP.new(RSA.import_key(self.__key.private_key.binary))
        return cryptor.decrypt(cipher_bytes).decode()
    except Exception as e:
        raise DecryptionFailedError from e

sign(message, hash_type=HashType.SHA256)

Sign a message using RSA private key.

Parameters:

Name Type Description Default
message str

Message to sign.

required
hash_type HashType

Hash algorithm to use for signing.

SHA256

Returns:

Type Description
str

Digital signature (Base64 encoded).

Raises:

Type Description
PrivateKeyRequiredError

If key pair has no private key.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/rsa.py
@override
def sign(self, message: str, hash_type: HashType = HashType.SHA256) -> str:
    """Sign a message using RSA private key.

    Args:
        message: Message to sign.
        hash_type: Hash algorithm to use for signing.

    Returns:
        Digital signature (Base64 encoded).

    Raises:
        PrivateKeyRequiredError: If key pair has no private key.
    """
    if self.__key.private_key is None:
        raise PrivateKeyRequiredError
    signer = PKCS1_v1_5.new(RSA.import_key(self.__key.private_key.binary))
    signature_bytes: bytes = signer.sign(Hash(message, hash_type))
    return Base64Encoder.from_bytes(signature_bytes, self.url_safe)

verify(message, signature, hash_type=HashType.SHA256)

Verify a signature using RSA public key.

Parameters:

Name Type Description Default
message str

Original message that was signed.

required
signature str

Digital signature to verify (Base64 encoded).

required
hash_type HashType

Hash algorithm used for signing.

SHA256

Returns:

Type Description
bool

True if signature is valid, False otherwise.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/rsa.py
@override
def verify(
    self, message: str, signature: str, hash_type: HashType = HashType.SHA256
) -> bool:
    """Verify a signature using RSA public key.

    Args:
        message: Original message that was signed.
        signature: Digital signature to verify (Base64 encoded).
        hash_type: Hash algorithm used for signing.

    Returns:
        True if signature is valid, False otherwise.
    """
    signature_bytes: bytes = Base64Encoder.get_bytes(signature, self.url_safe)
    signer = PKCS1_v1_5.new(RSA.import_key(self.__key.public_key.binary))
    return signer.verify(Hash(message, hash_type), signature_bytes)

Base64Encoder

Utility class for Base64 encoding and decoding operations.

encode(utf8, url_safe=False) staticmethod

Encode a UTF-8 string to Base64.

Parameters:

Name Type Description Default
utf8 str

The string to encode.

required
url_safe bool

Use URL-safe Base64 encoding without padding.

False

Returns:

Type Description
str

The Base64-encoded string.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/encoding.py
@staticmethod
def encode(utf8: str, url_safe: bool = False) -> str:
    """Encode a UTF-8 string to Base64.

    Args:
        utf8: The string to encode.
        url_safe: Use URL-safe Base64 encoding without padding.

    Returns:
        The Base64-encoded string.
    """
    if url_safe:
        return (
            base64.urlsafe_b64encode(utf8.encode("UTF-8"))
            .decode("UTF-8")
            .rstrip("=")
        )
    return base64.b64encode(utf8.encode("UTF-8")).decode("UTF-8")

decode(b64, url_safe=False) staticmethod

Decode a Base64 string to UTF-8.

Parameters:

Name Type Description Default
b64 str

The Base64-encoded string to decode.

required
url_safe bool

Use URL-safe Base64 decoding with padding restoration.

False

Returns:

Type Description
str

The decoded UTF-8 string.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/encoding.py
@staticmethod
def decode(b64: str, url_safe: bool = False) -> str:
    """Decode a Base64 string to UTF-8.

    Args:
        b64: The Base64-encoded string to decode.
        url_safe: Use URL-safe Base64 decoding with padding restoration.

    Returns:
        The decoded UTF-8 string.
    """
    if url_safe:
        return base64.urlsafe_b64decode(
            (b64 + ("=" * (4 - (len(b64) % 4)))).encode("UTF-8")
        ).decode("UTF-8")
    return base64.b64decode(b64.encode("UTF-8")).decode("UTF-8")

from_bytes(binary, url_safe=False) staticmethod

Encode binary data to Base64 string.

Parameters:

Name Type Description Default
binary bytes

The binary data to encode.

required
url_safe bool

Use URL-safe Base64 encoding without padding.

False

Returns:

Type Description
str

The Base64-encoded string.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/encoding.py
@staticmethod
def from_bytes(binary: bytes, url_safe: bool = False) -> str:
    """Encode binary data to Base64 string.

    Args:
        binary: The binary data to encode.
        url_safe: Use URL-safe Base64 encoding without padding.

    Returns:
        The Base64-encoded string.
    """
    if url_safe:
        return base64.urlsafe_b64encode(binary).decode("UTF-8").rstrip("=")
    return base64.b64encode(binary).decode("UTF-8")

get_bytes(b64, url_safe=False) staticmethod

Decode a Base64 string to binary data.

Parameters:

Name Type Description Default
b64 str

The Base64-encoded string to decode.

required
url_safe bool

Use URL-safe Base64 decoding with padding restoration.

False

Returns:

Type Description
bytes

The decoded binary data.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/encoding.py
@staticmethod
def get_bytes(b64: str, url_safe: bool = False) -> bytes:
    """Decode a Base64 string to binary data.

    Args:
        b64: The Base64-encoded string to decode.
        url_safe: Use URL-safe Base64 decoding with padding restoration.

    Returns:
        The decoded binary data.
    """
    if url_safe:
        return base64.urlsafe_b64decode(
            (b64 + ("=" * (4 - (len(b64) % 4)))).encode("UTF-8")
        )
    return base64.b64decode(b64.encode("UTF-8"))

Hash(data, hash_type=HashType.SHA256)

Cryptographic hash computation utility.

Computes cryptographic hashes of strings or file streams using various hash algorithms. Supports multiple output formats including hex, Base64, and binary.

Initialize a hash computation.

Parameters:

Name Type Description Default
data str | BufferedReader

The data to hash (string or file stream).

required
hash_type HashType

The hash algorithm to use.

SHA256
Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/hash.py
def __init__(
    self, data: str | BufferedReader, hash_type: HashType = HashType.SHA256
) -> None:
    """Initialize a hash computation.

    Args:
        data: The data to hash (string or file stream).
        hash_type: The hash algorithm to use.
    """
    self.__hash_type = hash_type
    match self.__hash_type:
        case HashType.MD5:
            self.__hash = MD5.new()
        case HashType.SHA1:
            self.__hash = SHA1.new()  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
        case HashType.SHA224:
            self.__hash = SHA224.new()  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
        case HashType.SHA256:
            self.__hash = SHA256.new()  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
        case HashType.SHA384:
            self.__hash = SHA384.new()  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
        case HashType.SHA512:  # pragma: no cover - exhaustive StrEnum
            self.__hash = SHA512.new()  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
    if isinstance(data, str):
        self.__hash.update(data.encode("UTF-8"))
    if isinstance(data, BufferedReader):
        while True:
            buffer: bytes = data.read(65536)
            if not any(buffer):
                break
            self.__hash.update(buffer)

hex property

Get hash as uppercase hexadecimal string.

b64 property

Get hash as Base64-encoded string.

b64_urlsafe property

Get hash as URL-safe Base64-encoded string.

binary property

Get hash as binary data.

oid property

Get the OID (Object Identifier) of the hash algorithm.

digest()

Compute and return the hash digest as binary data.

Returns:

Type Description
bytes

The hash digest as bytes.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/hash.py
def digest(self) -> bytes:
    """Compute and return the hash digest as binary data.

    Returns:
        The hash digest as bytes.
    """
    return self.__hash.digest()

HashType

Bases: StrEnum

Supported cryptographic hash algorithms.

HMAC

HMAC signing and verification utility.

Provides static methods for creating and verifying HMAC signatures using various hash algorithms.

sign_text(key, hmac_type, content, url_safe=False) staticmethod

Sign text content with HMAC.

Parameters:

Name Type Description Default
key Key

The cryptographic key to use for signing.

required
hmac_type HMACType

The HMAC hash algorithm to use.

required
content str

The text content to sign.

required
url_safe bool

Use URL-safe Base64 encoding for the signature.

False

Returns:

Type Description
str

The HMAC signature as a Base64-encoded string.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/hmac_signer.py
@staticmethod
def sign_text(
    key: Key,
    hmac_type: HMACType,
    content: str,
    url_safe: bool = False,
) -> str:
    """Sign text content with HMAC.

    Args:
        key: The cryptographic key to use for signing.
        hmac_type: The HMAC hash algorithm to use.
        content: The text content to sign.
        url_safe: Use URL-safe Base64 encoding for the signature.

    Returns:
        The HMAC signature as a Base64-encoded string.
    """
    key_bytes: bytes = key.binary
    hash_function: Callable[..., object]
    match hmac_type:
        case HMACType.HS224:
            hash_function = hashlib.sha224
        case HMACType.HS256:
            hash_function = hashlib.sha256
        case HMACType.HS384:
            hash_function = hashlib.sha384
        case HMACType.HS512:
            hash_function = (
                hashlib.sha512
            )  # pragma: no cover - exhaustive HMACType match
    return Base64Encoder.from_bytes(
        hmac.new(
            key_bytes,
            content.encode("UTF-8"),
            hash_function,  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
        ).digest(),
        url_safe,
    )

verify(key, hmac_type, content, signature, url_safe=False) staticmethod

Verify HMAC signature of text content.

Parameters:

Name Type Description Default
key Key

The cryptographic key used for verification.

required
hmac_type HMACType

The HMAC hash algorithm to use.

required
content str

The text content to verify.

required
signature str

The expected HMAC signature as a Base64 string.

required
url_safe bool

Whether the signature uses URL-safe Base64 encoding.

False

Returns:

Type Description
bool

True if the signature is valid, False otherwise.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/hmac_signer.py
@staticmethod
def verify(
    key: Key,
    hmac_type: HMACType,
    content: str,
    signature: str,
    url_safe: bool = False,
) -> bool:
    """Verify HMAC signature of text content.

    Args:
        key: The cryptographic key used for verification.
        hmac_type: The HMAC hash algorithm to use.
        content: The text content to verify.
        signature: The expected HMAC signature as a Base64 string.
        url_safe: Whether the signature uses URL-safe Base64 encoding.

    Returns:
        True if the signature is valid, False otherwise.
    """
    key_bytes: bytes = key.binary
    hash_function: Callable[..., object]
    match hmac_type:
        case HMACType.HS224:
            hash_function = hashlib.sha224
        case HMACType.HS256:
            hash_function = hashlib.sha256
        case HMACType.HS384:
            hash_function = hashlib.sha384
        case HMACType.HS512:
            hash_function = (
                hashlib.sha512
            )  # pragma: no cover - exhaustive HMACType match
    return (
        Base64Encoder.from_bytes(
            hmac.new(
                key_bytes,
                content.encode("UTF-8"),
                hash_function,  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
            ).digest(),
            url_safe,
        )
        == signature
    )

HMACType

Bases: StrEnum

Supported HMAC hash algorithms.

Key(size=None, binary=None, base64=None, url_safe=False)

Key(*, size: int)
Key(*, binary: bytes)
Key(*, base64: str, url_safe: bool = False)

Cryptographic key wrapper with format conversion utilities.

Supports creating keys from random generation, binary data, or Base64 encoding. Provides properties for converting keys to different formats.

Initialize a cryptographic key.

Parameters:

Name Type Description Default
size int | None

Generate a random key of specified byte size.

None
binary bytes | None

Create key from binary data.

None
base64 str | None

Create key from Base64-encoded string.

None
url_safe bool

Use URL-safe Base64 decoding when base64 is provided.

False

Raises:

Type Description
ValueError

If no valid initialization method is provided.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/key.py
def __init__(
    self,
    size: int | None = None,
    binary: bytes | None = None,
    base64: str | None = None,
    url_safe: bool = False,
) -> None:
    """Initialize a cryptographic key.

    Args:
        size: Generate a random key of specified byte size.
        binary: Create key from binary data.
        base64: Create key from Base64-encoded string.
        url_safe: Use URL-safe Base64 decoding when base64 is provided.

    Raises:
        ValueError: If no valid initialization method is provided.
    """
    if size is not None:
        self.__binary = secrets.token_bytes(size)
        return
    if binary is not None:
        self.__binary = binary
        return
    if base64 is not None:
        self.__binary = Base64Encoder.get_bytes(base64, url_safe=url_safe)
        return
    raise InvalidKeyConstructorCallError

binary property

Get the key as binary data.

length property

Get the key length in bytes.

b64 property

Get the key as Base64-encoded string.

b64_urlsafe property

Get the key as URL-safe Base64-encoded string.

hex property

Get the key as uppercase hexadecimal string.

Argon2PasswordEncoder(*, password_hash=None, password=None, salt=None, time_cost=3, memory_cost=65536, parallelism=4, hash_len=32, url_safe=False)

Argon2PasswordEncoder(
    *, password_hash: str, url_safe: bool = False
)
Argon2PasswordEncoder(
    *,
    password: str,
    salt: Key | None = None,
    time_cost: int = 3,
    memory_cost: int = 65536,
    parallelism: int = 4,
    hash_len: int = 32,
    url_safe: bool = False,
)

Bases: IPasswordEncoder

Argon2 password encoder.

Uses the Argon2 key derivation function for secure password hashing with configurable computational complexity parameters.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/argon2.py
def __init__(
    self,
    *,
    password_hash: str | None = None,
    password: str | None = None,
    salt: Key | None = None,
    time_cost: int = 3,
    memory_cost: int = 65536,
    parallelism: int = 4,
    hash_len: int = 32,
    url_safe: bool = False,
) -> None:
    self.__url_safe = url_safe
    if password_hash is not None:
        parts: list[str] = password_hash.split(":")
        parts.pop(0)
        self.__salt = Key(
            binary=Base64Encoder.get_bytes(parts[0], url_safe=self.__url_safe)
        )
        self.__time_cost = int(parts[1])
        self.__memory_cost = int(parts[2])
        self.__parallelism = int(parts[3])
        self.__hash_len = int(parts[4])
        self.__salt_len = self.__salt.length
        self.__hash = parts[5]
    else:
        if password is None:
            raise PasswordRequiredError
        if salt is None:
            salt = Key(size=self.SALT_SIZE)
        self.__salt = salt
        self.__time_cost = time_cost
        self.__memory_cost = memory_cost
        self.__parallelism = parallelism
        self.__hash_len = hash_len
        self.__salt_len = salt.length
        self.__hash = Base64Encoder.from_bytes(
            binary=PasswordHasher(
                time_cost=self.__time_cost,
                memory_cost=self.__memory_cost,
                parallelism=self.__parallelism,
                hash_len=self.__hash_len,
                salt_len=self.__salt_len,
            )
            .hash(password.encode("UTF-8"), salt=self.__salt.binary)
            .encode("UTF-8"),
            url_safe=self.__url_safe,
        )

encode()

Encode password hash as a string.

Returns:

Type Description
str

Encoded password hash string with algorithm and parameters.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/argon2.py
@override
def encode(self) -> str:
    """Encode password hash as a string.

    Returns:
        Encoded password hash string with algorithm and parameters.
    """
    return f"{self.ALGORITHM_TYPE}:{self.__salt.b64_urlsafe if self.__url_safe else self.__salt.b64}:{self.__time_cost}:{self.__memory_cost}:{self.__parallelism}:{self.__hash_len}:{self.__hash}"

challenge(password)

Verify a password against the stored hash.

Parameters:

Name Type Description Default
password str

Password to verify.

required

Returns:

Type Description
bool

True if password matches, False otherwise.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/argon2.py
@override
def challenge(self, password: str) -> bool:
    """Verify a password against the stored hash.

    Args:
        password: Password to verify.

    Returns:
        True if password matches, False otherwise.
    """
    return PasswordHasher(
        time_cost=self.__time_cost,
        memory_cost=self.__memory_cost,
        parallelism=self.__parallelism,
        hash_len=self.__hash_len,
        salt_len=self.__salt_len,
    ).verify(
        Base64Encoder.get_bytes(self.__hash, self.__url_safe),
        password.encode("UTF-8"),
    )

BcryptPasswordEncoder(password_hash=None, password=None, url_safe=False, rounds=None)

BcryptPasswordEncoder(
    *, password_hash: str, url_safe: bool = False
)
BcryptPasswordEncoder(
    *,
    password: str,
    url_safe: bool = False,
    rounds: int | None = None,
)

Bases: IPasswordEncoder

Bcrypt password encoder.

Uses the Bcrypt adaptive hash function for secure password hashing with automatic salt generation.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/bcrypt.py
def __init__(
    self,
    password_hash: str | None = None,
    password: str | None = None,
    url_safe: bool = False,
    rounds: int | None = None,
) -> None:
    self.__url_safe = url_safe
    if password_hash is not None:
        parts: list[str] = password_hash.split(":")
        parts.pop(0)
        self.__salt = Key(
            binary=Base64Encoder.get_bytes(parts[0], url_safe=self.__url_safe)
        )
        self.__hash = parts[1]
    else:
        if password is None:
            raise PasswordRequiredError
        effective_rounds = rounds if rounds is not None else self.DEFAULT_ROUNDS
        self.__salt = Key(binary=bcrypt.gensalt(rounds=effective_rounds))
        self.__hash = Base64Encoder.from_bytes(
            bcrypt.hashpw(
                password.encode("UTF-8"),
                self.__salt.binary,
            ),
            url_safe=self.__url_safe,
        )

encode()

Encode password hash as a string.

Returns:

Type Description
str

Encoded password hash string with algorithm and salt.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/bcrypt.py
@override
def encode(self) -> str:
    """Encode password hash as a string.

    Returns:
        Encoded password hash string with algorithm and salt.
    """
    return f"{self.ALGORITHM_TYPE}:{self.__salt.b64_urlsafe if self.__url_safe else self.__salt.b64}:{self.__hash}"

challenge(password)

Verify a password against the stored hash.

Parameters:

Name Type Description Default
password str

Password to verify.

required

Returns:

Type Description
bool

True if password matches, False otherwise.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/bcrypt.py
@override
def challenge(self, password: str) -> bool:
    """Verify a password against the stored hash.

    Args:
        password: Password to verify.

    Returns:
        True if password matches, False otherwise.
    """
    return bcrypt.checkpw(
        password.encode("UTF-8"),
        Base64Encoder.get_bytes(self.__hash, url_safe=self.__url_safe),
    )

IPasswordEncoder

Bases: IEquatable, IRepresentable, ABC

Interface for password hashing and verification operations.

Pbkdf2PasswordEncoder(*, password_hash=None, password=None, salt=None, hash_type=HashType.SHA256, iteration=100000, url_safe=False)

Pbkdf2PasswordEncoder(
    *, password_hash: str, url_safe: bool = False
)
Pbkdf2PasswordEncoder(
    *,
    password: str,
    salt: Key | None = None,
    hash_type: HashType = HashType.SHA256,
    iteration: int = 100000,
    url_safe: bool = False,
)

Bases: IPasswordEncoder

PBKDF2 password encoder.

Uses the PBKDF2 key derivation function for secure password hashing with configurable iteration count and hash algorithm.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/pbkdf2.py
def __init__(
    self,
    *,
    password_hash: str | None = None,
    password: str | None = None,
    salt: Key | None = None,
    hash_type: HashType = HashType.SHA256,
    iteration: int = 100000,
    url_safe: bool = False,
) -> None:
    self.__url_safe = url_safe
    if password_hash is not None:
        parts: list[str] = password_hash.split(":")
        parts.pop(0)
        self.__hash_type = HashType(parts[0].upper())
        self.__iteration = int(parts[1])
        self.__salt = Key(
            binary=Base64Encoder.get_bytes(parts[2], url_safe=self.__url_safe)
        )
        self.__hash = parts[3]
    else:
        if password is None:
            raise PasswordRequiredError
        if salt is None:
            salt = Key(size=self.SALT_SIZE)
        self.__salt = salt
        self.__hash_type = hash_type
        self.__iteration = iteration
        self.__hash: str = Base64Encoder.from_bytes(
            hashlib.pbkdf2_hmac(
                self.__hash_type,
                password.encode("UTF-8"),
                self.__salt.binary,
                self.__iteration,
            ),
            url_safe=self.__url_safe,
        )

encode()

Encode password hash as a string.

Returns:

Type Description
str

Encoded password hash string with algorithm, hash type, and parameters.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/pbkdf2.py
@override
def encode(self) -> str:
    """Encode password hash as a string.

    Returns:
        Encoded password hash string with algorithm, hash type, and parameters.
    """
    return f"{self.ALGORITHM_TYPE}:{self.__hash_type.lower()}:{self.__iteration}:{self.__salt.b64_urlsafe if self.__url_safe else self.__salt.b64}:{self.__hash}"

challenge(password)

Verify a password against the stored hash.

Parameters:

Name Type Description Default
password str

Password to verify.

required

Returns:

Type Description
bool

True if password matches, False otherwise.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/pbkdf2.py
@override
def challenge(self, password: str) -> bool:
    """Verify a password against the stored hash.

    Args:
        password: Password to verify.

    Returns:
        True if password matches, False otherwise.
    """
    new_password: Pbkdf2PasswordEncoder = Pbkdf2PasswordEncoder(
        password=password,
        salt=self.__salt,
        hash_type=self.__hash_type,
        iteration=self.__iteration,
    )
    return self == new_password

ScryptPasswordEncoder(*, password_hash=None, password=None, salt=None, n=2 ** 14, r=8, p=1, maxmem=0, dklen=32, url_safe=False)

ScryptPasswordEncoder(
    *, password_hash: str, url_safe: bool = False
)
ScryptPasswordEncoder(
    *,
    password: str,
    salt: Key | None = None,
    n: int = 2**14,
    r: int = 8,
    p: int = 1,
    maxmem: int = 0,
    dklen: int = 32,
    url_safe: bool = False,
)

Bases: IPasswordEncoder

Scrypt password encoder.

Uses the Scrypt key derivation function for secure password hashing with configurable CPU/memory cost parameters for enhanced security.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/scrypt.py
def __init__(
    self,
    *,
    password_hash: str | None = None,
    password: str | None = None,
    salt: Key | None = None,
    n: int = 2**14,
    r: int = 8,
    p: int = 1,
    maxmem: int = 0,
    dklen: int = 32,
    url_safe: bool = False,
) -> None:
    self.__url_safe = url_safe
    if password_hash is not None:
        parts: list[str] = password_hash.split(":")
        parts.pop(0)
        self.__salt = Key(
            binary=Base64Encoder.get_bytes(parts[0], url_safe=self.__url_safe)
        )
        self.__n = int(parts[1])
        self.__r = int(parts[2])
        self.__p = int(parts[3])
        self.__maxmem = int(parts[4])
        self.__dklen = int(parts[5])
        self.__hash = parts[6]
    else:
        if password is None:
            raise PasswordRequiredError
        if salt is None:
            salt = Key(size=self.SALT_SIZE)
        self.__salt = salt
        self.__n = n
        self.__r = r
        self.__p = p
        self.__maxmem = maxmem
        self.__dklen = dklen

        self.__hash = Base64Encoder.from_bytes(
            scrypt(
                password.encode("UTF-8"),
                salt=self.__salt.binary,
                n=self.__n,
                r=self.__r,
                p=self.__p,
                maxmem=self.__maxmem,
                dklen=self.__dklen,
            ),
            url_safe=self.__url_safe,
        )

encode()

Encode password hash as a string.

Returns:

Type Description
str

Encoded password hash string with algorithm and parameters.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/scrypt.py
@override
def encode(self) -> str:
    """Encode password hash as a string.

    Returns:
        Encoded password hash string with algorithm and parameters.
    """
    return f"{self.ALGORITHM_TYPE}:{self.__salt.b64_urlsafe if self.__url_safe else self.__salt.b64}:{self.__n}:{self.__r}:{self.__p}:{self.__maxmem}:{self.__dklen}:{self.__hash}"

challenge(password)

Verify a password against the stored hash.

Parameters:

Name Type Description Default
password str

Password to verify.

required

Returns:

Type Description
bool

True if password matches, False otherwise.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/scrypt.py
@override
def challenge(self, password: str) -> bool:
    """Verify a password against the stored hash.

    Args:
        password: Password to verify.

    Returns:
        True if password matches, False otherwise.
    """
    return self.__hash == Base64Encoder.from_bytes(
        scrypt(
            password.encode("UTF-8"),
            salt=self.__salt.binary,
            n=self.__n,
            r=self.__r,
            p=self.__p,
            maxmem=self.__maxmem,
            dklen=self.__dklen,
        ),
        url_safe=self.__url_safe,
    )

플러그인 진입점

Plugin initialization for cryptography utilities and auth provider.

initialize(app)

Register cryptography config, auth provider, and auth port bindings.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/main.py
def initialize(app: SpakkyApplication) -> None:
    """Register cryptography config, auth provider, and auth port bindings."""
    app.add(CryptographyAuthProviderConfig)
    app.add(CryptographyAuthProvider)
    app.container.bind_to_type(IAuthContextSnapshotSigner, CryptographyAuthProvider)
    app.container.bind_to_type(IAuthContextSnapshotVerifier, CryptographyAuthProvider)
    app.container.bind_to_type(IPasswordHasher, CryptographyAuthProvider)
    app.container.bind_to_type(IPasswordVerifier, CryptographyAuthProvider)

Auth Provider

Auth snapshot and password capabilities backed by cryptographic utilities.

CRYPTOGRAPHY_AUTH_PROVIDER_ID = 'provider:spakky-cryptography' module-attribute

Stable auth provider id advertised by spakky-cryptography.

SNAPSHOT_SIGNATURE_ALGORITHM = 'HS256' module-attribute

Snapshot envelope signature algorithm used by this provider.

CryptographyAuthProviderConfig()

Bases: BaseSettings

Runtime config for cryptography auth provider capabilities.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/auth_provider.py
def __init__(self) -> None:
    super().__init__()

snapshot_key = Field(default_factory=(lambda: Key(size=32))) class-attribute instance-attribute

HMAC key used to sign and verify AuthContextSnapshot envelopes.

snapshot_key_id = 'spakky-cryptography:default' class-attribute instance-attribute

Identifier carried in signed snapshot envelopes.

snapshot_ttl = timedelta(minutes=5) class-attribute instance-attribute

Validity window for newly signed snapshots.

clock = _utc_now class-attribute instance-attribute

Clock used for signing and expiration validation.

verification_available = True class-attribute instance-attribute

Whether snapshot verification provider dependencies are available.

password_available = True class-attribute instance-attribute

Whether password hashing provider dependencies are available.

AuthContextSnapshotVerificationResult(*, decision, auth_context=None) dataclass

Decision plus optional AuthContext produced by snapshot verification.

decision instance-attribute

ALLOW, CHALLENGE, or ERROR decision for the verification attempt.

auth_context = None class-attribute instance-attribute

Verified auth context when decision is ALLOW.

CryptographyAuthProvider(config)

Bases: IAuthContextSnapshotSigner, IAuthContextSnapshotVerifier, IPasswordHasher, IPasswordVerifier

Cryptography-backed provider for snapshot and password auth capabilities.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/auth_provider.py
def __init__(
    self,
    config: CryptographyAuthProviderConfig,
) -> None:
    self._config = config

sign_snapshot(request)

Create a signed AuthContextSnapshot envelope.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/auth_provider.py
@override
def sign_snapshot(self, request: SnapshotSignRequest) -> AuthContextSnapshot:
    """Create a signed AuthContextSnapshot envelope."""
    issued_at = self._aware_datetime(self._config.clock())
    tenant = (
        request.tenant
        if request.tenant is not None
        else request.auth_context.tenant
    )
    unsigned_payload = self._unsigned_payload(
        auth_context=request.auth_context,
        tenant=tenant,
        issued_at=issued_at,
        expires_at=issued_at + self._config.snapshot_ttl,
    )
    signature = HMAC.sign_text(
        self._config.snapshot_key,
        HMACType.HS256,
        self._canonical_json(unsigned_payload),
        url_safe=True,
    )
    return AuthContextSnapshot(
        subject=request.auth_context.subject,
        issuer=request.auth_context.issuer,
        issued_at=issued_at,
        expires_at=issued_at + self._config.snapshot_ttl,
        signature=AuthContextSnapshotSignature(
            key_id=self._config.snapshot_key_id,
            algorithm=SNAPSHOT_SIGNATURE_ALGORITHM,
            signature=signature,
        ),
        tenant=tenant,
        roles=request.auth_context.roles,
        scopes=request.auth_context.scopes,
        selected_claims=request.auth_context.claims,
    )

verify_snapshot(snapshot_envelope, invocation)

Verify a signed snapshot envelope and return its AuthContext.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/auth_provider.py
@override
def verify_snapshot(
    self,
    snapshot_envelope: str,
    invocation: AuthInvocation,
) -> AuthContext:
    """Verify a signed snapshot envelope and return its AuthContext."""
    if not self._config.verification_available:
        raise AuthVerificationProviderUnavailableError()
    if snapshot_envelope == "":
        raise MissingAuthContextSnapshotError()
    payload = self._decode_envelope(snapshot_envelope)
    self._verify_payload_signature(payload)
    expires_at = self._datetime_value(payload, "expires_at")
    if expires_at < self._aware_datetime(self._config.clock()):
        raise ExpiredAuthContextSnapshotError()
    return self._auth_context_from_payload(payload)

verify_snapshot_result(snapshot_envelope, invocation)

Verify a snapshot envelope and map auth errors to decisions.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/auth_provider.py
def verify_snapshot_result(
    self,
    snapshot_envelope: str,
    invocation: AuthInvocation,
) -> AuthContextSnapshotVerificationResult:
    """Verify a snapshot envelope and map auth errors to decisions."""
    try:
        auth_context = self.verify_snapshot(snapshot_envelope, invocation)
    except MissingAuthContextSnapshotError:
        return AuthContextSnapshotVerificationResult(
            decision=AuthorizationDecision.challenge(
                AuthorizationReasonCode.SNAPSHOT_MISSING
            )
        )
    except InvalidAuthContextSnapshotError:
        return AuthContextSnapshotVerificationResult(
            decision=AuthorizationDecision.challenge(
                AuthorizationReasonCode.SNAPSHOT_INVALID
            )
        )
    except ExpiredAuthContextSnapshotError:
        return AuthContextSnapshotVerificationResult(
            decision=AuthorizationDecision.challenge(
                AuthorizationReasonCode.SNAPSHOT_EXPIRED
            )
        )
    except AuthVerificationProviderUnavailableError:
        return AuthContextSnapshotVerificationResult(
            decision=AuthorizationDecision.error(
                AuthorizationReasonCode.VERIFICATION_PROVIDER_UNAVAILABLE
            )
        )
    return AuthContextSnapshotVerificationResult(
        decision=AuthorizationDecision.allow(),
        auth_context=auth_context,
    )

hash_password(password)

Hash plaintext password material for storage.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/auth_provider.py
@override
def hash_password(self, password: AuthPasswordPlaintext) -> AuthPasswordHash:
    """Hash plaintext password material for storage."""
    if not self._config.password_available:
        raise AuthVerificationProviderUnavailableError()
    return BcryptPasswordEncoder(password=password).encode()

verify_password(password, password_hash)

Verify plaintext password material against a retained password hash.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/auth_provider.py
@override
def verify_password(
    self,
    password: AuthPasswordPlaintext,
    password_hash: AuthPasswordHash,
) -> AuthorizationDecision:
    """Verify plaintext password material against a retained password hash."""
    if not self._config.password_available:
        return AuthorizationDecision.error(
            AuthorizationReasonCode.VERIFICATION_PROVIDER_UNAVAILABLE
        )
    try:
        if self._password_encoder(password_hash).challenge(password):
            return AuthorizationDecision.allow()
    except Exception:
        return AuthorizationDecision.challenge(
            AuthorizationReasonCode.INVALID_CREDENTIAL
        )
    return AuthorizationDecision.challenge(
        AuthorizationReasonCode.INVALID_CREDENTIAL
    )

cryptography_auth_provider_contribution()

Return the auth capabilities contributed by spakky-cryptography.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/auth_provider.py
@Pod(name="spakky_cryptography_auth_provider_contribution")
def cryptography_auth_provider_contribution() -> AuthProviderContribution:
    """Return the auth capabilities contributed by spakky-cryptography."""
    return AuthProviderContribution(
        provider_id=CRYPTOGRAPHY_AUTH_PROVIDER_ID,
        capabilities=frozenset(
            {
                AuthCapability.SNAPSHOT_SIGN,
                AuthCapability.SNAPSHOT_VERIFY,
                AuthCapability.PASSWORD_HASH,
                AuthCapability.PASSWORD_VERIFY,
            }
        ),
    )

Key / Encoding / Hash / HMAC

Cryptographic key management utilities.

Provides utilities for generating, storing, and converting cryptographic keys in various formats including binary, Base64, and hexadecimal.

Key(size=None, binary=None, base64=None, url_safe=False)

Key(*, size: int)
Key(*, binary: bytes)
Key(*, base64: str, url_safe: bool = False)

Cryptographic key wrapper with format conversion utilities.

Supports creating keys from random generation, binary data, or Base64 encoding. Provides properties for converting keys to different formats.

Initialize a cryptographic key.

Parameters:

Name Type Description Default
size int | None

Generate a random key of specified byte size.

None
binary bytes | None

Create key from binary data.

None
base64 str | None

Create key from Base64-encoded string.

None
url_safe bool

Use URL-safe Base64 decoding when base64 is provided.

False

Raises:

Type Description
ValueError

If no valid initialization method is provided.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/key.py
def __init__(
    self,
    size: int | None = None,
    binary: bytes | None = None,
    base64: str | None = None,
    url_safe: bool = False,
) -> None:
    """Initialize a cryptographic key.

    Args:
        size: Generate a random key of specified byte size.
        binary: Create key from binary data.
        base64: Create key from Base64-encoded string.
        url_safe: Use URL-safe Base64 decoding when base64 is provided.

    Raises:
        ValueError: If no valid initialization method is provided.
    """
    if size is not None:
        self.__binary = secrets.token_bytes(size)
        return
    if binary is not None:
        self.__binary = binary
        return
    if base64 is not None:
        self.__binary = Base64Encoder.get_bytes(base64, url_safe=url_safe)
        return
    raise InvalidKeyConstructorCallError

binary property

Get the key as binary data.

length property

Get the key length in bytes.

b64 property

Get the key as Base64-encoded string.

b64_urlsafe property

Get the key as URL-safe Base64-encoded string.

hex property

Get the key as uppercase hexadecimal string.

Base64 encoding and decoding utilities.

Provides utilities for encoding and decoding data in Base64 format with support for URL-safe encoding and direct bytes conversion.

Base64Encoder

Utility class for Base64 encoding and decoding operations.

encode(utf8, url_safe=False) staticmethod

Encode a UTF-8 string to Base64.

Parameters:

Name Type Description Default
utf8 str

The string to encode.

required
url_safe bool

Use URL-safe Base64 encoding without padding.

False

Returns:

Type Description
str

The Base64-encoded string.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/encoding.py
@staticmethod
def encode(utf8: str, url_safe: bool = False) -> str:
    """Encode a UTF-8 string to Base64.

    Args:
        utf8: The string to encode.
        url_safe: Use URL-safe Base64 encoding without padding.

    Returns:
        The Base64-encoded string.
    """
    if url_safe:
        return (
            base64.urlsafe_b64encode(utf8.encode("UTF-8"))
            .decode("UTF-8")
            .rstrip("=")
        )
    return base64.b64encode(utf8.encode("UTF-8")).decode("UTF-8")

decode(b64, url_safe=False) staticmethod

Decode a Base64 string to UTF-8.

Parameters:

Name Type Description Default
b64 str

The Base64-encoded string to decode.

required
url_safe bool

Use URL-safe Base64 decoding with padding restoration.

False

Returns:

Type Description
str

The decoded UTF-8 string.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/encoding.py
@staticmethod
def decode(b64: str, url_safe: bool = False) -> str:
    """Decode a Base64 string to UTF-8.

    Args:
        b64: The Base64-encoded string to decode.
        url_safe: Use URL-safe Base64 decoding with padding restoration.

    Returns:
        The decoded UTF-8 string.
    """
    if url_safe:
        return base64.urlsafe_b64decode(
            (b64 + ("=" * (4 - (len(b64) % 4)))).encode("UTF-8")
        ).decode("UTF-8")
    return base64.b64decode(b64.encode("UTF-8")).decode("UTF-8")

from_bytes(binary, url_safe=False) staticmethod

Encode binary data to Base64 string.

Parameters:

Name Type Description Default
binary bytes

The binary data to encode.

required
url_safe bool

Use URL-safe Base64 encoding without padding.

False

Returns:

Type Description
str

The Base64-encoded string.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/encoding.py
@staticmethod
def from_bytes(binary: bytes, url_safe: bool = False) -> str:
    """Encode binary data to Base64 string.

    Args:
        binary: The binary data to encode.
        url_safe: Use URL-safe Base64 encoding without padding.

    Returns:
        The Base64-encoded string.
    """
    if url_safe:
        return base64.urlsafe_b64encode(binary).decode("UTF-8").rstrip("=")
    return base64.b64encode(binary).decode("UTF-8")

get_bytes(b64, url_safe=False) staticmethod

Decode a Base64 string to binary data.

Parameters:

Name Type Description Default
b64 str

The Base64-encoded string to decode.

required
url_safe bool

Use URL-safe Base64 decoding with padding restoration.

False

Returns:

Type Description
bytes

The decoded binary data.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/encoding.py
@staticmethod
def get_bytes(b64: str, url_safe: bool = False) -> bytes:
    """Decode a Base64 string to binary data.

    Args:
        b64: The Base64-encoded string to decode.
        url_safe: Use URL-safe Base64 decoding with padding restoration.

    Returns:
        The decoded binary data.
    """
    if url_safe:
        return base64.urlsafe_b64decode(
            (b64 + ("=" * (4 - (len(b64) % 4)))).encode("UTF-8")
        )
    return base64.b64decode(b64.encode("UTF-8"))

Cryptographic hash utilities.

Provides utilities for computing cryptographic hashes using various algorithms including MD5, SHA1, SHA224, SHA256, SHA384, and SHA512.

HashType

Bases: StrEnum

Supported cryptographic hash algorithms.

Hash(data, hash_type=HashType.SHA256)

Cryptographic hash computation utility.

Computes cryptographic hashes of strings or file streams using various hash algorithms. Supports multiple output formats including hex, Base64, and binary.

Initialize a hash computation.

Parameters:

Name Type Description Default
data str | BufferedReader

The data to hash (string or file stream).

required
hash_type HashType

The hash algorithm to use.

SHA256
Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/hash.py
def __init__(
    self, data: str | BufferedReader, hash_type: HashType = HashType.SHA256
) -> None:
    """Initialize a hash computation.

    Args:
        data: The data to hash (string or file stream).
        hash_type: The hash algorithm to use.
    """
    self.__hash_type = hash_type
    match self.__hash_type:
        case HashType.MD5:
            self.__hash = MD5.new()
        case HashType.SHA1:
            self.__hash = SHA1.new()  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
        case HashType.SHA224:
            self.__hash = SHA224.new()  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
        case HashType.SHA256:
            self.__hash = SHA256.new()  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
        case HashType.SHA384:
            self.__hash = SHA384.new()  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
        case HashType.SHA512:  # pragma: no cover - exhaustive StrEnum
            self.__hash = SHA512.new()  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
    if isinstance(data, str):
        self.__hash.update(data.encode("UTF-8"))
    if isinstance(data, BufferedReader):
        while True:
            buffer: bytes = data.read(65536)
            if not any(buffer):
                break
            self.__hash.update(buffer)

hex property

Get hash as uppercase hexadecimal string.

b64 property

Get hash as Base64-encoded string.

b64_urlsafe property

Get hash as URL-safe Base64-encoded string.

binary property

Get hash as binary data.

oid property

Get the OID (Object Identifier) of the hash algorithm.

digest()

Compute and return the hash digest as binary data.

Returns:

Type Description
bytes

The hash digest as bytes.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/hash.py
def digest(self) -> bytes:
    """Compute and return the hash digest as binary data.

    Returns:
        The hash digest as bytes.
    """
    return self.__hash.digest()

HMAC signing and verification utilities.

Provides utilities for creating and verifying HMAC signatures using various hash algorithms (SHA-224, SHA-256, SHA-384, SHA-512).

HMACType

Bases: StrEnum

Supported HMAC hash algorithms.

HMAC

HMAC signing and verification utility.

Provides static methods for creating and verifying HMAC signatures using various hash algorithms.

sign_text(key, hmac_type, content, url_safe=False) staticmethod

Sign text content with HMAC.

Parameters:

Name Type Description Default
key Key

The cryptographic key to use for signing.

required
hmac_type HMACType

The HMAC hash algorithm to use.

required
content str

The text content to sign.

required
url_safe bool

Use URL-safe Base64 encoding for the signature.

False

Returns:

Type Description
str

The HMAC signature as a Base64-encoded string.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/hmac_signer.py
@staticmethod
def sign_text(
    key: Key,
    hmac_type: HMACType,
    content: str,
    url_safe: bool = False,
) -> str:
    """Sign text content with HMAC.

    Args:
        key: The cryptographic key to use for signing.
        hmac_type: The HMAC hash algorithm to use.
        content: The text content to sign.
        url_safe: Use URL-safe Base64 encoding for the signature.

    Returns:
        The HMAC signature as a Base64-encoded string.
    """
    key_bytes: bytes = key.binary
    hash_function: Callable[..., object]
    match hmac_type:
        case HMACType.HS224:
            hash_function = hashlib.sha224
        case HMACType.HS256:
            hash_function = hashlib.sha256
        case HMACType.HS384:
            hash_function = hashlib.sha384
        case HMACType.HS512:
            hash_function = (
                hashlib.sha512
            )  # pragma: no cover - exhaustive HMACType match
    return Base64Encoder.from_bytes(
        hmac.new(
            key_bytes,
            content.encode("UTF-8"),
            hash_function,  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
        ).digest(),
        url_safe,
    )

verify(key, hmac_type, content, signature, url_safe=False) staticmethod

Verify HMAC signature of text content.

Parameters:

Name Type Description Default
key Key

The cryptographic key used for verification.

required
hmac_type HMACType

The HMAC hash algorithm to use.

required
content str

The text content to verify.

required
signature str

The expected HMAC signature as a Base64 string.

required
url_safe bool

Whether the signature uses URL-safe Base64 encoding.

False

Returns:

Type Description
bool

True if the signature is valid, False otherwise.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/hmac_signer.py
@staticmethod
def verify(
    key: Key,
    hmac_type: HMACType,
    content: str,
    signature: str,
    url_safe: bool = False,
) -> bool:
    """Verify HMAC signature of text content.

    Args:
        key: The cryptographic key used for verification.
        hmac_type: The HMAC hash algorithm to use.
        content: The text content to verify.
        signature: The expected HMAC signature as a Base64 string.
        url_safe: Whether the signature uses URL-safe Base64 encoding.

    Returns:
        True if the signature is valid, False otherwise.
    """
    key_bytes: bytes = key.binary
    hash_function: Callable[..., object]
    match hmac_type:
        case HMACType.HS224:
            hash_function = hashlib.sha224
        case HMACType.HS256:
            hash_function = hashlib.sha256
        case HMACType.HS384:
            hash_function = hashlib.sha384
        case HMACType.HS512:
            hash_function = (
                hashlib.sha512
            )  # pragma: no cover - exhaustive HMACType match
    return (
        Base64Encoder.from_bytes(
            hmac.new(
                key_bytes,
                content.encode("UTF-8"),
                hash_function,  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
            ).digest(),
            url_safe,
        )
        == signature
    )

Symmetric / Asymmetric Cryptography

Cryptography protocol interfaces.

Defines protocol interfaces for encryption/decryption and signing/verification operations used by cryptographic implementations.

ICryptor

Bases: ABC

Interface for encryption and decryption operations.

ISigner

Bases: ABC

Interface for digital signature operations.

AES encryption and decryption utilities.

Provides AES-CBC mode encryption/decryption with automatic padding and IV generation using 256-bit keys.

Aes(key, url_safe=False)

Bases: ICryptor

AES-CBC encryption/decryption implementation.

Uses 256-bit keys (32 bytes) with automatic PKCS7 padding and random IV generation for each encryption operation.

Initialize AES encryptor.

Parameters:

Name Type Description Default
key Key

256-bit (32-byte) encryption key.

required
url_safe bool

Use URL-safe Base64 encoding for cipher text.

False

Raises:

Type Description
KeySizeError

If key is not 32 bytes.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/aes.py
def __init__(self, key: Key, url_safe: bool = False) -> None:
    """Initialize AES encryptor.

    Args:
        key: 256-bit (32-byte) encryption key.
        url_safe: Use URL-safe Base64 encoding for cipher text.

    Raises:
        KeySizeError: If key is not 32 bytes.
    """
    if key.length != self.KEY_SIZE:
        raise KeySizeError
    self.url_safe = url_safe
    self.__key = key

encrypt(message)

Encrypt a message using AES-CBC.

Parameters:

Name Type Description Default
message str

Plain text message to encrypt.

required

Returns:

Type Description
str

Encrypted cipher text in format "iv:cipher" (Base64 encoded).

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/aes.py
@override
def encrypt(self, message: str) -> str:
    """Encrypt a message using AES-CBC.

    Args:
        message: Plain text message to encrypt.

    Returns:
        Encrypted cipher text in format "iv:cipher" (Base64 encoded).
    """
    plain_bytes: bytes = pad(message.encode(), AES.block_size)
    iv: Key = Key(size=16)
    cryptor = AES.new(  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
        key=self.__key.binary,
        mode=AES.MODE_CBC,
        iv=iv.binary,
    )
    cipher_bytes: bytes = cryptor.encrypt(plain_bytes)
    return f"{iv.b64_urlsafe if self.url_safe else iv.b64}:{Base64Encoder.from_bytes(cipher_bytes, self.url_safe)}"

decrypt(cipher)

Decrypt a cipher text using AES-CBC.

Parameters:

Name Type Description Default
cipher str

Cipher text in format "iv:cipher" (Base64 encoded).

required

Returns:

Type Description
str

Decrypted plain text message.

Raises:

Type Description
DecryptionFailedError

If decryption fails.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/aes.py
@override
def decrypt(self, cipher: str) -> str:
    """Decrypt a cipher text using AES-CBC.

    Args:
        cipher: Cipher text in format "iv:cipher" (Base64 encoded).

    Returns:
        Decrypted plain text message.

    Raises:
        DecryptionFailedError: If decryption fails.
    """
    try:
        [iv, cipher] = cipher.split(":")
        iv_bytes: bytes = Base64Encoder.get_bytes(iv, self.url_safe)
        cipher_bytes: bytes = Base64Encoder.get_bytes(cipher, self.url_safe)
        cryptor = AES.new(  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
            key=self.__key.binary,
            mode=AES.MODE_CBC,
            iv=iv_bytes,
        )
        plain_bytes: bytes = cryptor.decrypt(cipher_bytes)
        return unpad(plain_bytes, AES.block_size).decode()
    except Exception as e:
        raise DecryptionFailedError from e

AES-GCM encryption and decryption utilities.

Provides AES-GCM mode authenticated encryption/decryption with automatic padding, IV, and AAD generation using 256-bit keys.

Gcm(key, url_safe=False)

Bases: ICryptor

AES-GCM authenticated encryption/decryption implementation.

Uses 256-bit keys (32 bytes) with automatic PKCS7 padding, random IV, and AAD generation for authenticated encryption operations.

Initialize AES-GCM encryptor.

Parameters:

Name Type Description Default
key Key

256-bit (32-byte) encryption key.

required
url_safe bool

Use URL-safe Base64 encoding for cipher text.

False

Raises:

Type Description
KeySizeError

If key is not 32 bytes.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/gcm.py
def __init__(self, key: Key, url_safe: bool = False) -> None:
    """Initialize AES-GCM encryptor.

    Args:
        key: 256-bit (32-byte) encryption key.
        url_safe: Use URL-safe Base64 encoding for cipher text.

    Raises:
        KeySizeError: If key is not 32 bytes.
    """
    if key.length != self.KEY_SIZE:
        raise KeySizeError
    self.url_safe = url_safe
    self.__key = key

encrypt(message)

Encrypt a message using AES-GCM.

Parameters:

Name Type Description Default
message str

Plain text message to encrypt.

required

Returns:

Type Description
str

Encrypted cipher text in format "aad:tag:iv:cipher" (Base64 encoded).

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/gcm.py
@override
def encrypt(self, message: str) -> str:
    """Encrypt a message using AES-GCM.

    Args:
        message: Plain text message to encrypt.

    Returns:
        Encrypted cipher text in format "aad:tag:iv:cipher" (Base64 encoded).
    """
    plain_bytes: bytes = pad(message.encode(), AES.block_size)
    aad: Key = Key(size=16)
    iv: Key = Key(size=12)
    cryptor = AES.new(  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
        key=self.__key.binary,
        mode=AES.MODE_GCM,
        nonce=iv.binary,
    )
    cryptor.update(aad.binary)
    cipher_bytes, tag_bytes = cryptor.encrypt_and_digest(plain_bytes)
    return f"{aad.b64_urlsafe if self.url_safe else aad.b64}:{Base64Encoder.from_bytes(tag_bytes, self.url_safe)}:{iv.b64_urlsafe if self.url_safe else iv.b64}:{Base64Encoder.from_bytes(cipher_bytes, self.url_safe)}"

decrypt(cipher)

Decrypt a cipher text using AES-GCM.

Parameters:

Name Type Description Default
cipher str

Cipher text in format "aad:tag:iv:cipher" (Base64 encoded).

required

Returns:

Type Description
str

Decrypted plain text message.

Raises:

Type Description
DecryptionFailedError

If decryption or authentication fails.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/gcm.py
@override
def decrypt(self, cipher: str) -> str:
    """Decrypt a cipher text using AES-GCM.

    Args:
        cipher: Cipher text in format "aad:tag:iv:cipher" (Base64 encoded).

    Returns:
        Decrypted plain text message.

    Raises:
        DecryptionFailedError: If decryption or authentication fails.
    """
    try:
        [aad, tag, iv, cipher] = cipher.split(":")
        aad_bytes: bytes = Base64Encoder.get_bytes(aad, self.url_safe)
        tag_bytes: bytes = Base64Encoder.get_bytes(tag, self.url_safe)
        iv_bytes: bytes = Base64Encoder.get_bytes(iv, self.url_safe)
        cipher_bytes: bytes = Base64Encoder.get_bytes(cipher, self.url_safe)
        cryptor = AES.new(  # type: ignore[no-untyped-call] - PyCryptodome 타입 스텁 미제공
            key=self.__key.binary,
            mode=AES.MODE_GCM,
            nonce=iv_bytes,
        )
        cryptor.update(aad_bytes)
        plain_bytes: bytes = cryptor.decrypt_and_verify(cipher_bytes, tag_bytes)
        return unpad(plain_bytes, AES.block_size).decode()
    except Exception as e:
        raise DecryptionFailedError from e

RSA encryption, decryption, and signing utilities.

Provides RSA asymmetric cryptography operations including key generation, encryption/decryption with PKCS1_OAEP, and signing/verification with PKCS1_v1_5.

AsymmetricKey(key=None, size=None, passphrase=None)

AsymmetricKey(*, key: str, passphrase: str | None = None)
AsymmetricKey(*, key: bytes, passphrase: str | None = None)
AsymmetricKey(*, size: int, passphrase: str | None = None)

RSA asymmetric key pair.

Manages RSA public/private key pairs with support for key generation, import/export, and passphrase protection. Supports 1024, 2048, 4096, and 8192-bit keys.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/rsa.py
def __init__(
    self,
    key: str | bytes | None = None,
    size: int | None = None,
    passphrase: str | None = None,
) -> None:
    if (
        key is None and size is None
    ):  # pragma: no cover - overloads prevent this at type-check time
        raise AsymmetricKeyRequiredError
    if key is not None:
        try:
            imported_key = RSA.import_key(key, passphrase)
            if (key_size := imported_key.size_in_bits()) not in self.KEY_SIZES:
                raise KeySizeError(key_size * 8)
            self.__key = imported_key
        except (ValueError, IndexError, TypeError) as e:
            raise CannotImportAsymmetricKeyError from e
    if size is not None:
        if size not in self.KEY_SIZES:
            raise KeySizeError(size)
        self.__key = RSA.generate(size)
    if self.__key.has_private():
        self.__private_key = Key(
            binary=self.__key.export_key(passphrase=passphrase)
        )
    self.__public_key = Key(binary=self.__key.public_key().export_key())

is_private property

Check if this key pair includes a private key.

private_key property

Get the private key, or None if this is a public key only.

public_key property

Get the public key.

Rsa(key, url_safe=False)

Bases: ICryptor, ISigner

RSA encryption/decryption and signing/verification.

Provides PKCS1_OAEP encryption/decryption and PKCS1_v1_5 signing/verification using RSA asymmetric keys. Encryption uses the public key, decryption and signing require the private key.

Initialize RSA cryptor/signer.

Parameters:

Name Type Description Default
key AsymmetricKey

RSA asymmetric key pair.

required
url_safe bool

Use URL-safe Base64 encoding for cipher/signature.

False
Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/rsa.py
def __init__(self, key: AsymmetricKey, url_safe: bool = False) -> None:
    """Initialize RSA cryptor/signer.

    Args:
        key: RSA asymmetric key pair.
        url_safe: Use URL-safe Base64 encoding for cipher/signature.
    """
    self.url_safe = url_safe
    self.__key = key

encrypt(message)

Encrypt a message using RSA public key.

Parameters:

Name Type Description Default
message str

Plain text message to encrypt.

required

Returns:

Type Description
str

Encrypted cipher text (Base64 encoded).

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/rsa.py
@override
def encrypt(self, message: str) -> str:
    """Encrypt a message using RSA public key.

    Args:
        message: Plain text message to encrypt.

    Returns:
        Encrypted cipher text (Base64 encoded).
    """
    cryptor = PKCS1_OAEP.new(RSA.import_key(self.__key.public_key.binary))
    cipher_bytes: bytes = cryptor.encrypt(message.encode())
    return Base64Encoder.from_bytes(cipher_bytes, self.url_safe)

decrypt(cipher)

Decrypt a cipher text using RSA private key.

Parameters:

Name Type Description Default
cipher str

Cipher text to decrypt (Base64 encoded).

required

Returns:

Type Description
str

Decrypted plain text message.

Raises:

Type Description
PrivateKeyRequiredError

If key pair has no private key.

DecryptionFailedError

If decryption fails.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/rsa.py
@override
def decrypt(self, cipher: str) -> str:
    """Decrypt a cipher text using RSA private key.

    Args:
        cipher: Cipher text to decrypt (Base64 encoded).

    Returns:
        Decrypted plain text message.

    Raises:
        PrivateKeyRequiredError: If key pair has no private key.
        DecryptionFailedError: If decryption fails.
    """
    if self.__key.private_key is None:
        raise PrivateKeyRequiredError
    try:
        cipher_bytes: bytes = Base64Encoder.get_bytes(cipher, self.url_safe)
        cryptor = PKCS1_OAEP.new(RSA.import_key(self.__key.private_key.binary))
        return cryptor.decrypt(cipher_bytes).decode()
    except Exception as e:
        raise DecryptionFailedError from e

sign(message, hash_type=HashType.SHA256)

Sign a message using RSA private key.

Parameters:

Name Type Description Default
message str

Message to sign.

required
hash_type HashType

Hash algorithm to use for signing.

SHA256

Returns:

Type Description
str

Digital signature (Base64 encoded).

Raises:

Type Description
PrivateKeyRequiredError

If key pair has no private key.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/rsa.py
@override
def sign(self, message: str, hash_type: HashType = HashType.SHA256) -> str:
    """Sign a message using RSA private key.

    Args:
        message: Message to sign.
        hash_type: Hash algorithm to use for signing.

    Returns:
        Digital signature (Base64 encoded).

    Raises:
        PrivateKeyRequiredError: If key pair has no private key.
    """
    if self.__key.private_key is None:
        raise PrivateKeyRequiredError
    signer = PKCS1_v1_5.new(RSA.import_key(self.__key.private_key.binary))
    signature_bytes: bytes = signer.sign(Hash(message, hash_type))
    return Base64Encoder.from_bytes(signature_bytes, self.url_safe)

verify(message, signature, hash_type=HashType.SHA256)

Verify a signature using RSA public key.

Parameters:

Name Type Description Default
message str

Original message that was signed.

required
signature str

Digital signature to verify (Base64 encoded).

required
hash_type HashType

Hash algorithm used for signing.

SHA256

Returns:

Type Description
bool

True if signature is valid, False otherwise.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/cryptography/rsa.py
@override
def verify(
    self, message: str, signature: str, hash_type: HashType = HashType.SHA256
) -> bool:
    """Verify a signature using RSA public key.

    Args:
        message: Original message that was signed.
        signature: Digital signature to verify (Base64 encoded).
        hash_type: Hash algorithm used for signing.

    Returns:
        True if signature is valid, False otherwise.
    """
    signature_bytes: bytes = Base64Encoder.get_bytes(signature, self.url_safe)
    signer = PKCS1_v1_5.new(RSA.import_key(self.__key.public_key.binary))
    return signer.verify(Hash(message, hash_type), signature_bytes)

Password Encoders

Password encoding protocol interface.

Defines the protocol interface for password hashing implementations used by various password encoding algorithms.

IPasswordEncoder

Bases: IEquatable, IRepresentable, ABC

Interface for password hashing and verification operations.

Argon2 password hashing implementation.

Provides password hashing using the Argon2 algorithm with configurable parameters for time cost, memory cost, parallelism, and hash length.

Argon2PasswordEncoder(*, password_hash=None, password=None, salt=None, time_cost=3, memory_cost=65536, parallelism=4, hash_len=32, url_safe=False)

Argon2PasswordEncoder(
    *, password_hash: str, url_safe: bool = False
)
Argon2PasswordEncoder(
    *,
    password: str,
    salt: Key | None = None,
    time_cost: int = 3,
    memory_cost: int = 65536,
    parallelism: int = 4,
    hash_len: int = 32,
    url_safe: bool = False,
)

Bases: IPasswordEncoder

Argon2 password encoder.

Uses the Argon2 key derivation function for secure password hashing with configurable computational complexity parameters.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/argon2.py
def __init__(
    self,
    *,
    password_hash: str | None = None,
    password: str | None = None,
    salt: Key | None = None,
    time_cost: int = 3,
    memory_cost: int = 65536,
    parallelism: int = 4,
    hash_len: int = 32,
    url_safe: bool = False,
) -> None:
    self.__url_safe = url_safe
    if password_hash is not None:
        parts: list[str] = password_hash.split(":")
        parts.pop(0)
        self.__salt = Key(
            binary=Base64Encoder.get_bytes(parts[0], url_safe=self.__url_safe)
        )
        self.__time_cost = int(parts[1])
        self.__memory_cost = int(parts[2])
        self.__parallelism = int(parts[3])
        self.__hash_len = int(parts[4])
        self.__salt_len = self.__salt.length
        self.__hash = parts[5]
    else:
        if password is None:
            raise PasswordRequiredError
        if salt is None:
            salt = Key(size=self.SALT_SIZE)
        self.__salt = salt
        self.__time_cost = time_cost
        self.__memory_cost = memory_cost
        self.__parallelism = parallelism
        self.__hash_len = hash_len
        self.__salt_len = salt.length
        self.__hash = Base64Encoder.from_bytes(
            binary=PasswordHasher(
                time_cost=self.__time_cost,
                memory_cost=self.__memory_cost,
                parallelism=self.__parallelism,
                hash_len=self.__hash_len,
                salt_len=self.__salt_len,
            )
            .hash(password.encode("UTF-8"), salt=self.__salt.binary)
            .encode("UTF-8"),
            url_safe=self.__url_safe,
        )

encode()

Encode password hash as a string.

Returns:

Type Description
str

Encoded password hash string with algorithm and parameters.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/argon2.py
@override
def encode(self) -> str:
    """Encode password hash as a string.

    Returns:
        Encoded password hash string with algorithm and parameters.
    """
    return f"{self.ALGORITHM_TYPE}:{self.__salt.b64_urlsafe if self.__url_safe else self.__salt.b64}:{self.__time_cost}:{self.__memory_cost}:{self.__parallelism}:{self.__hash_len}:{self.__hash}"

challenge(password)

Verify a password against the stored hash.

Parameters:

Name Type Description Default
password str

Password to verify.

required

Returns:

Type Description
bool

True if password matches, False otherwise.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/argon2.py
@override
def challenge(self, password: str) -> bool:
    """Verify a password against the stored hash.

    Args:
        password: Password to verify.

    Returns:
        True if password matches, False otherwise.
    """
    return PasswordHasher(
        time_cost=self.__time_cost,
        memory_cost=self.__memory_cost,
        parallelism=self.__parallelism,
        hash_len=self.__hash_len,
        salt_len=self.__salt_len,
    ).verify(
        Base64Encoder.get_bytes(self.__hash, self.__url_safe),
        password.encode("UTF-8"),
    )

Bcrypt password hashing implementation.

Provides password hashing using the Bcrypt algorithm with automatic salt generation and configurable work factor.

BcryptPasswordEncoder(password_hash=None, password=None, url_safe=False, rounds=None)

BcryptPasswordEncoder(
    *, password_hash: str, url_safe: bool = False
)
BcryptPasswordEncoder(
    *,
    password: str,
    url_safe: bool = False,
    rounds: int | None = None,
)

Bases: IPasswordEncoder

Bcrypt password encoder.

Uses the Bcrypt adaptive hash function for secure password hashing with automatic salt generation.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/bcrypt.py
def __init__(
    self,
    password_hash: str | None = None,
    password: str | None = None,
    url_safe: bool = False,
    rounds: int | None = None,
) -> None:
    self.__url_safe = url_safe
    if password_hash is not None:
        parts: list[str] = password_hash.split(":")
        parts.pop(0)
        self.__salt = Key(
            binary=Base64Encoder.get_bytes(parts[0], url_safe=self.__url_safe)
        )
        self.__hash = parts[1]
    else:
        if password is None:
            raise PasswordRequiredError
        effective_rounds = rounds if rounds is not None else self.DEFAULT_ROUNDS
        self.__salt = Key(binary=bcrypt.gensalt(rounds=effective_rounds))
        self.__hash = Base64Encoder.from_bytes(
            bcrypt.hashpw(
                password.encode("UTF-8"),
                self.__salt.binary,
            ),
            url_safe=self.__url_safe,
        )

encode()

Encode password hash as a string.

Returns:

Type Description
str

Encoded password hash string with algorithm and salt.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/bcrypt.py
@override
def encode(self) -> str:
    """Encode password hash as a string.

    Returns:
        Encoded password hash string with algorithm and salt.
    """
    return f"{self.ALGORITHM_TYPE}:{self.__salt.b64_urlsafe if self.__url_safe else self.__salt.b64}:{self.__hash}"

challenge(password)

Verify a password against the stored hash.

Parameters:

Name Type Description Default
password str

Password to verify.

required

Returns:

Type Description
bool

True if password matches, False otherwise.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/bcrypt.py
@override
def challenge(self, password: str) -> bool:
    """Verify a password against the stored hash.

    Args:
        password: Password to verify.

    Returns:
        True if password matches, False otherwise.
    """
    return bcrypt.checkpw(
        password.encode("UTF-8"),
        Base64Encoder.get_bytes(self.__hash, url_safe=self.__url_safe),
    )

PBKDF2 password hashing implementation.

Provides password hashing using the PBKDF2 key derivation function with configurable hash algorithm, iteration count, and salt.

Pbkdf2PasswordEncoder(*, password_hash=None, password=None, salt=None, hash_type=HashType.SHA256, iteration=100000, url_safe=False)

Pbkdf2PasswordEncoder(
    *, password_hash: str, url_safe: bool = False
)
Pbkdf2PasswordEncoder(
    *,
    password: str,
    salt: Key | None = None,
    hash_type: HashType = HashType.SHA256,
    iteration: int = 100000,
    url_safe: bool = False,
)

Bases: IPasswordEncoder

PBKDF2 password encoder.

Uses the PBKDF2 key derivation function for secure password hashing with configurable iteration count and hash algorithm.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/pbkdf2.py
def __init__(
    self,
    *,
    password_hash: str | None = None,
    password: str | None = None,
    salt: Key | None = None,
    hash_type: HashType = HashType.SHA256,
    iteration: int = 100000,
    url_safe: bool = False,
) -> None:
    self.__url_safe = url_safe
    if password_hash is not None:
        parts: list[str] = password_hash.split(":")
        parts.pop(0)
        self.__hash_type = HashType(parts[0].upper())
        self.__iteration = int(parts[1])
        self.__salt = Key(
            binary=Base64Encoder.get_bytes(parts[2], url_safe=self.__url_safe)
        )
        self.__hash = parts[3]
    else:
        if password is None:
            raise PasswordRequiredError
        if salt is None:
            salt = Key(size=self.SALT_SIZE)
        self.__salt = salt
        self.__hash_type = hash_type
        self.__iteration = iteration
        self.__hash: str = Base64Encoder.from_bytes(
            hashlib.pbkdf2_hmac(
                self.__hash_type,
                password.encode("UTF-8"),
                self.__salt.binary,
                self.__iteration,
            ),
            url_safe=self.__url_safe,
        )

encode()

Encode password hash as a string.

Returns:

Type Description
str

Encoded password hash string with algorithm, hash type, and parameters.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/pbkdf2.py
@override
def encode(self) -> str:
    """Encode password hash as a string.

    Returns:
        Encoded password hash string with algorithm, hash type, and parameters.
    """
    return f"{self.ALGORITHM_TYPE}:{self.__hash_type.lower()}:{self.__iteration}:{self.__salt.b64_urlsafe if self.__url_safe else self.__salt.b64}:{self.__hash}"

challenge(password)

Verify a password against the stored hash.

Parameters:

Name Type Description Default
password str

Password to verify.

required

Returns:

Type Description
bool

True if password matches, False otherwise.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/pbkdf2.py
@override
def challenge(self, password: str) -> bool:
    """Verify a password against the stored hash.

    Args:
        password: Password to verify.

    Returns:
        True if password matches, False otherwise.
    """
    new_password: Pbkdf2PasswordEncoder = Pbkdf2PasswordEncoder(
        password=password,
        salt=self.__salt,
        hash_type=self.__hash_type,
        iteration=self.__iteration,
    )
    return self == new_password

Scrypt password hashing implementation.

Provides password hashing using the Scrypt key derivation function with configurable CPU and memory cost parameters for resistance against hardware brute-force attacks.

ScryptPasswordEncoder(*, password_hash=None, password=None, salt=None, n=2 ** 14, r=8, p=1, maxmem=0, dklen=32, url_safe=False)

ScryptPasswordEncoder(
    *, password_hash: str, url_safe: bool = False
)
ScryptPasswordEncoder(
    *,
    password: str,
    salt: Key | None = None,
    n: int = 2**14,
    r: int = 8,
    p: int = 1,
    maxmem: int = 0,
    dklen: int = 32,
    url_safe: bool = False,
)

Bases: IPasswordEncoder

Scrypt password encoder.

Uses the Scrypt key derivation function for secure password hashing with configurable CPU/memory cost parameters for enhanced security.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/scrypt.py
def __init__(
    self,
    *,
    password_hash: str | None = None,
    password: str | None = None,
    salt: Key | None = None,
    n: int = 2**14,
    r: int = 8,
    p: int = 1,
    maxmem: int = 0,
    dklen: int = 32,
    url_safe: bool = False,
) -> None:
    self.__url_safe = url_safe
    if password_hash is not None:
        parts: list[str] = password_hash.split(":")
        parts.pop(0)
        self.__salt = Key(
            binary=Base64Encoder.get_bytes(parts[0], url_safe=self.__url_safe)
        )
        self.__n = int(parts[1])
        self.__r = int(parts[2])
        self.__p = int(parts[3])
        self.__maxmem = int(parts[4])
        self.__dklen = int(parts[5])
        self.__hash = parts[6]
    else:
        if password is None:
            raise PasswordRequiredError
        if salt is None:
            salt = Key(size=self.SALT_SIZE)
        self.__salt = salt
        self.__n = n
        self.__r = r
        self.__p = p
        self.__maxmem = maxmem
        self.__dklen = dklen

        self.__hash = Base64Encoder.from_bytes(
            scrypt(
                password.encode("UTF-8"),
                salt=self.__salt.binary,
                n=self.__n,
                r=self.__r,
                p=self.__p,
                maxmem=self.__maxmem,
                dklen=self.__dklen,
            ),
            url_safe=self.__url_safe,
        )

encode()

Encode password hash as a string.

Returns:

Type Description
str

Encoded password hash string with algorithm and parameters.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/scrypt.py
@override
def encode(self) -> str:
    """Encode password hash as a string.

    Returns:
        Encoded password hash string with algorithm and parameters.
    """
    return f"{self.ALGORITHM_TYPE}:{self.__salt.b64_urlsafe if self.__url_safe else self.__salt.b64}:{self.__n}:{self.__r}:{self.__p}:{self.__maxmem}:{self.__dklen}:{self.__hash}"

challenge(password)

Verify a password against the stored hash.

Parameters:

Name Type Description Default
password str

Password to verify.

required

Returns:

Type Description
bool

True if password matches, False otherwise.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/password/scrypt.py
@override
def challenge(self, password: str) -> bool:
    """Verify a password against the stored hash.

    Args:
        password: Password to verify.

    Returns:
        True if password matches, False otherwise.
    """
    return self.__hash == Base64Encoder.from_bytes(
        scrypt(
            password.encode("UTF-8"),
            salt=self.__salt.binary,
            n=self.__n,
            r=self.__r,
            p=self.__p,
            maxmem=self.__maxmem,
            dklen=self.__dklen,
        ),
        url_safe=self.__url_safe,
    )

Errors

Cryptography-related error classes.

Provides specialized exception classes for cryptography and key management.

DecryptionFailedError

Bases: AbstractSpakkyFrameworkError

Raised when decryption fails due to invalid key or corrupted data.

KeySizeError

Bases: AbstractSpakkyFrameworkError

Raised when a cryptographic key has an invalid size.

PrivateKeyRequiredError

Bases: AbstractSpakkyFrameworkError

Raised when a private key is required but not provided.

CannotImportAsymmetricKeyError

Bases: AbstractSpakkyFrameworkError

Raised when an asymmetric key cannot be imported.

InvalidKeyConstructorCallError

Bases: AbstractSpakkyFrameworkError

Raised when Key constructor is called without valid arguments.

IncompatibleKeyTypeError

Bases: AbstractSpakkyFrameworkError

Raised when comparing a Key with an incompatible type.

PasswordRequiredError

Bases: AbstractSpakkyFrameworkError

Raised when password parameter is required but not provided.

AsymmetricKeyRequiredError

Bases: AbstractSpakkyFrameworkError

Raised when key or size parameter is required but not provided.

추가 모듈

Auth feature contribution for the cryptography provider.

initialize(app)

Register cryptography auth capability metadata.

Source code in plugins/spakky-cryptography/src/spakky/plugins/cryptography/contributions/auth.py
def initialize(app: SpakkyApplication) -> None:
    """Register cryptography auth capability metadata."""
    app.add(cryptography_auth_provider_contribution)