"""Blinding API — generate blinding factors and blind/unblind Ed25519 keys. Ported from net.i2p.crypto.Blinding. Used for EncryptedLeaseSet (LS2) where the published signing key is blinded so observers cannot link it to the real destination. """ from __future__ import annotations import hashlib import zlib from datetime import datetime, timezone from i2p_crypto.dsa import SigType from i2p_crypto.eddsa_blinding import EdDSABlinding from i2p_crypto.hkdf import HKDF from i2p_data.key_types import SigningPublicKey, SigningPrivateKey _HKDF = HKDF() class Blinding: """Static methods for I2P destination blinding.""" @staticmethod def generate_alpha( dest_spk: SigningPublicKey, sig_type_in: SigType = SigType.EdDSA_SHA512_Ed25519, sig_type_out: SigType = SigType.RedDSA_SHA512_Ed25519, timestamp_sec: int | None = None, secret: str | None = None, ) -> SigningPrivateKey: """Generate the daily blinding factor (alpha) as a scalar. Parameters ---------- dest_spk: unblinded Ed25519 signing public key sig_type_in: source signature type (usually Ed25519) sig_type_out: blinded signature type (usually RedDSA) timestamp_sec: seconds since epoch (UTC) secret: optional shared secret string """ if timestamp_sec is None: timestamp_sec = int(datetime.now(timezone.utc).timestamp()) # Date string: "yyyyMMdd" UTC dt = datetime.fromtimestamp(timestamp_sec, tz=timezone.utc) date_str = dt.strftime("%Y%m%d").encode("ascii") # HKDF input data = date_str [+ secret] data = date_str if secret: data += secret.encode("utf-8") # Salt = SHA-256("I2PGenerateAlpha" + spk + sig_type_in(2) + sig_type_out(2)) salt_input = ( b"I2PGenerateAlpha" + dest_spk.to_bytes() + sig_type_in.code.to_bytes(2, "big") + sig_type_out.code.to_bytes(2, "big") ) salt = hashlib.sha256(salt_input).digest() # HKDF extract-and-expand to get 64 bytes seed = _HKDF.extract_and_expand(salt, data, b"i2pblinding1", 64) # Reduce 64 bytes to 32-byte scalar alpha = EdDSABlinding.reduce(seed) return SigningPrivateKey(alpha, sig_type=sig_type_out) @staticmethod def blind( spk: SigningPublicKey, alpha: SigningPrivateKey, ) -> SigningPublicKey: """Blind a public key: blinded = spk + alpha*G.""" alpha_pub = EdDSABlinding.scalar_mult_base(alpha.to_bytes()) blinded = EdDSABlinding.blind_public(spk.to_bytes(), alpha_pub) return SigningPublicKey(blinded, sig_type=SigType.RedDSA_SHA512_Ed25519) @staticmethod def unblind( spk_seed: bytes, alpha: SigningPrivateKey, ) -> SigningPrivateKey: """Unblind (derive blinded private key from seed + alpha).""" scalar = EdDSABlinding.seed_to_scalar(spk_seed) blinded_scalar = EdDSABlinding.blind_private(scalar, alpha.to_bytes()) return SigningPrivateKey(blinded_scalar, sig_type=SigType.RedDSA_SHA512_Ed25519) @staticmethod def encode_b32( spk: SigningPublicKey, sig_type_in: SigType = SigType.EdDSA_SHA512_Ed25519, sig_type_out: SigType = SigType.RedDSA_SHA512_Ed25519, secret_required: bool = False, per_client_auth: bool = False, ) -> str: """Encode a blinded destination as a .b32.i2p address. Format: flag(1) + sig_type_in(1) + sig_type_out(1) + spk(32) First 3 bytes XORed with CRC32 of bytes[3:35] for obfuscation. """ flags = 0x00 if secret_required: flags |= 0x02 if per_client_auth: flags |= 0x04 raw = bytearray(35) raw[0] = flags raw[1] = sig_type_in.code & 0xFF raw[2] = sig_type_out.code & 0xFF raw[3:35] = spk.to_bytes()[:32] # CRC32 of the key portion crc = zlib.crc32(bytes(raw[3:35])) & 0xFFFFFFFF crc_bytes = crc.to_bytes(4, "big") # XOR first 3 bytes with CRC bytes[1:4] raw[0] ^= crc_bytes[1] raw[1] ^= crc_bytes[2] raw[2] ^= crc_bytes[3] import base64 b32 = base64.b32encode(bytes(raw)).rstrip(b"=").decode("ascii").lower() return f"{b32}.b32.i2p" @staticmethod def decode_b32(address: str) -> tuple[SigType, SigType, SigningPublicKey, int]: """Decode a blinded .b32.i2p address. Returns (sig_type_in, sig_type_out, spk, flags). """ import base64 hostname = address.removesuffix(".b32.i2p") # Add padding padding = (8 - len(hostname) % 8) % 8 b32_padded = hostname.upper() + "=" * padding raw = bytearray(base64.b32decode(b32_padded)) if len(raw) < 35: raise ValueError(f"Blinded b32 too short: {len(raw)} bytes") # Reverse CRC XOR crc = zlib.crc32(bytes(raw[3:35])) & 0xFFFFFFFF crc_bytes = crc.to_bytes(4, "big") raw[0] ^= crc_bytes[1] raw[1] ^= crc_bytes[2] raw[2] ^= crc_bytes[3] flags = raw[0] sig_type_in = SigType.by_code(raw[1]) sig_type_out = SigType.by_code(raw[2]) spk = SigningPublicKey(bytes(raw[3:35]), sig_type=sig_type_in) assert sig_type_in is not None and sig_type_out is not None return sig_type_in, sig_type_out, spk, flags