A Python port of the Invisible Internet Project (I2P)
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()