A Python port of the Invisible Internet Project (I2P)
at main 88 lines 2.9 kB view raw
1"""ChaCha20 stream cipher. 2 3Ported from net.i2p.crypto.ChaCha20. 4Implements RFC 7539 ChaCha20 encryption/decryption, 5wrapping Python's ``cryptography`` library. 6 7Key: 32 bytes. Nonce: 12 bytes (standard) or 16 bytes 8(4-byte little-endian counter prefix + 12-byte nonce). 9""" 10 11from __future__ import annotations 12 13from cryptography.hazmat.primitives.ciphers import Cipher, algorithms 14 15 16class ChaCha20: 17 """ChaCha20 stream cipher wrapper. Thread-safe, stateless (all static methods).""" 18 19 # RFC 7539 specifies initial counter = 1 for encryption 20 _INITIAL_COUNTER = 1 21 22 def __init__(self) -> None: # pragma: no cover 23 raise TypeError("ChaCha20 should not be instantiated; use static methods") 24 25 @staticmethod 26 def encrypt(key: bytes, nonce: bytes, plaintext: bytes) -> bytes: 27 """Encrypt plaintext with ChaCha20. 28 29 Args: 30 key: 32-byte key. 31 nonce: 12-byte nonce (RFC 7539) or 16-byte nonce 32 (first 4 bytes = little-endian counter, next 12 = nonce). 33 34 Returns: 35 Ciphertext of the same length as plaintext. 36 37 Raises: 38 ValueError: if key or nonce lengths are invalid. 39 """ 40 return ChaCha20._apply(key, nonce, plaintext) 41 42 @staticmethod 43 def decrypt(key: bytes, nonce: bytes, ciphertext: bytes) -> bytes: 44 """Decrypt ciphertext with ChaCha20. 45 46 ChaCha20 is a stream cipher — decryption is identical to encryption. 47 48 Args: 49 key: 32-byte key. 50 nonce: 12-byte nonce (RFC 7539) or 16-byte nonce 51 (first 4 bytes = little-endian counter, next 12 = nonce). 52 53 Returns: 54 Plaintext of the same length as ciphertext. 55 56 Raises: 57 ValueError: if key or nonce lengths are invalid. 58 """ 59 return ChaCha20._apply(key, nonce, ciphertext) 60 61 @staticmethod 62 def _apply(key: bytes, nonce: bytes, data: bytes) -> bytes: 63 """Core ChaCha20 XOR operation (encrypt == decrypt). 64 65 The ``cryptography`` library expects a 16-byte nonce composed of 66 a 4-byte little-endian initial counter followed by the 12-byte 67 RFC 7539 nonce. 68 """ 69 if len(key) < 32: 70 raise ValueError(f"Key must be at least 32 bytes, got {len(key)}") 71 72 k = key[:32] 73 74 if len(nonce) == 12: 75 # Standard 12-byte nonce: prepend counter = 1 (little-endian) 76 full_nonce = ChaCha20._INITIAL_COUNTER.to_bytes(4, "little") + nonce 77 elif len(nonce) == 16: 78 # 16-byte nonce already includes counter prefix 79 full_nonce = nonce 80 else: 81 raise ValueError( 82 f"Nonce must be 12 or 16 bytes, got {len(nonce)}" 83 ) 84 85 algorithm = algorithms.ChaCha20(k, full_nonce) 86 cipher = Cipher(algorithm, mode=None) 87 encryptor = cipher.encryptor() 88 return encryptor.update(data) + encryptor.finalize()