Source code for tet.util.base64

"""
Base64 and Crockford Base32 encoding utilities.

This module provides encoding utilities including standard Base64 and
Crockford's Base32, which is human-friendly (avoids ambiguous characters
like 0/O and 1/I/L).

Example
-------

Using standard Base64::

    from tet.util.base64 import Base64

    encoded = Base64.encode(b"hello")
    decoded = Base64.decode(encoded)

    # Generate random Base64 characters
    random_str = Base64.generate_characters(16)

Using Crockford Base32::

    from tet.util.base64 import CrockfordBase32

    encoded = CrockfordBase32.encode(b"hello")
    decoded = CrockfordBase32.decode(encoded)

    # Crockford Base32 is case-insensitive and handles ambiguous chars
    CrockfordBase32.decode("O1L")  # Treated as "011"
"""

import base64
import secrets
import string

maketrans = bytes.maketrans


[docs] class BaseCodec: """Base class for encoding codecs.""" #: Number of bits each encoded character carries (log2 of the alphabet #: size). Subclasses with power-of-two alphabets set this explicitly. bits_per_char = None
[docs] @classmethod def generate_characters(cls, length): """Generate a random string of ``length`` characters in this codec's alphabet, drawn from a cryptographically secure source. Random bytes are generated with :func:`secrets.token_bytes`, run through the codec's own :meth:`encode`, and truncated to ``length``. Each encoded character carries ``cls.bits_per_char`` bits, so ``ceil(length * bits_per_char / 8)`` input bytes always encode to at least ``length`` data characters. Padding (if any) only trails the data, so the truncated slice never includes it. """ if length <= 0: return "" n_bytes = (length * cls.bits_per_char + 7) // 8 encoded = cls.encode(secrets.token_bytes(n_bytes)) if isinstance(encoded, bytes): encoded = encoded.decode("ascii") return encoded[:length]
[docs] class Base64(BaseCodec): """Standard Base64 encoding.""" chars = string.ascii_letters + string.digits + "+/" bits_per_char = 6 padding = True
[docs] @classmethod def encode(cls, string): """Encode bytes to Base64.""" return base64.b64encode(string)
[docs] @classmethod def normalize(cls, string): """Normalize input (no-op for standard Base64).""" return string
[docs] @classmethod def decode(cls, string): """Decode Base64 to bytes.""" return base64.b64decode(string)
_std_b32_to_crockford_b32 = maketrans( b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", b"0123456789ABCDEFGHJKMNPQRSTVWXYZ" ) _crockford_b32_to_std_b32 = maketrans( b"0OI1L23456789ABCDEFGHJKMNPQRSTVWXYZ", b"AABBBCDEFGHIJKLMNOPQRSTUVWXYZ234567" ) _normalize_crockford_b32 = maketrans( b"OIL" + string.ascii_lowercase.encode(), b"011" + string.ascii_uppercase.encode() )
[docs] class CrockfordBase32(BaseCodec): """ Crockford's Base32 encoding. Human-friendly encoding that avoids ambiguous characters (0/O, 1/I/L). Case-insensitive and handles common transcription errors. """ chars = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" bits_per_char = 5 padding = False
[docs] @classmethod def encode(cls, string, normalize=True, validate=False): """Encode bytes to Crockford Base32.""" if isinstance(string, str): string = string.encode() return ( base64.b32encode(string).translate(_std_b32_to_crockford_b32, b"=").decode() )
[docs] @classmethod def decode(cls, string, normalize=True, validate=False): """Decode Crockford Base32 to bytes.""" if normalize: string = cls.normalize(string) if isinstance(string, str): string = string.encode() # Ensure the mandatory padding is correct: b32 = string.upper() b32 += b"=" * ((8 - len(b32) % 8) % 8) return base64.b32decode(b32.translate(_crockford_b32_to_std_b32))
[docs] @classmethod def normalize(cls, string): """Normalize input by handling ambiguous characters (O->0, I/L->1).""" if isinstance(string, str): string = string.encode() string = string.translate(_normalize_crockford_b32) return string.decode()