A Python port of the Invisible Internet Project (I2P)
at main 164 lines 5.2 kB view raw
1""" 2AES-256-CBC engine for I2P. 3 4Ported from: 5 net.i2p.crypto.CryptixAESEngine (CBC encrypt/decrypt) 6 net.i2p.crypto.AESEngine (base class) 7 8Wraps Python's ``cryptography`` library using AES-CBC with no padding. 9All data lengths must be multiples of 16 bytes. 10""" 11 12from __future__ import annotations 13 14from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes 15 16 17# AES block size in bytes 18_BLOCK_SIZE = 16 19 20 21class AESEngine: 22 """AES-256-CBC engine with no automatic padding. 23 24 This mirrors I2P's ``CryptixAESEngine``: CBC mode, 16-byte IV, 25 data length must be a multiple of 16. Single-block ECB helpers 26 (``encrypt_block`` / ``decrypt_block``) are also provided for 27 callers that implement their own chaining. 28 29 All keys must be 16, 24, or 32 bytes (128/192/256-bit AES). 30 The I2P network uses 32-byte (256-bit) session keys. 31 """ 32 33 # ------------------------------------------------------------------ 34 # CBC bulk operations 35 # ------------------------------------------------------------------ 36 37 @staticmethod 38 def encrypt(payload: bytes, key: bytes, iv: bytes) -> bytes: 39 """Encrypt *payload* with AES-CBC using *key* and *iv*. 40 41 Parameters 42 ---------- 43 payload: 44 Plaintext whose length **must** be a positive multiple of 16. 45 key: 46 AES key (16, 24, or 32 bytes). 47 iv: 48 Initialisation vector, exactly 16 bytes. 49 50 Returns 51 ------- 52 bytes 53 Ciphertext of the same length as *payload*. 54 55 Raises 56 ------ 57 ValueError 58 If *payload* length is not a positive multiple of 16, or 59 *iv* is not 16 bytes. 60 """ 61 AESEngine._validate(payload, key, iv) 62 cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) 63 encryptor = cipher.encryptor() 64 return encryptor.update(payload) + encryptor.finalize() 65 66 @staticmethod 67 def decrypt(payload: bytes, key: bytes, iv: bytes) -> bytes: 68 """Decrypt *payload* with AES-CBC using *key* and *iv*. 69 70 Parameters 71 ---------- 72 payload: 73 Ciphertext whose length **must** be a positive multiple of 16. 74 key: 75 AES key (16, 24, or 32 bytes). 76 iv: 77 Initialisation vector, exactly 16 bytes. 78 79 Returns 80 ------- 81 bytes 82 Plaintext of the same length as *payload*. 83 84 Raises 85 ------ 86 ValueError 87 If *payload* length is not a positive multiple of 16, or 88 *iv* is not 16 bytes. 89 """ 90 AESEngine._validate(payload, key, iv) 91 cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) 92 decryptor = cipher.decryptor() 93 return decryptor.update(payload) + decryptor.finalize() 94 95 # ------------------------------------------------------------------ 96 # Single-block (ECB) operations — 16 bytes exactly 97 # ------------------------------------------------------------------ 98 99 @staticmethod 100 def encrypt_block(block: bytes, key: bytes) -> bytes: 101 """Encrypt a single 16-byte block (ECB mode, no IV). 102 103 Parameters 104 ---------- 105 block: 106 Exactly 16 bytes of plaintext. 107 key: 108 AES key (16, 24, or 32 bytes). 109 110 Returns 111 ------- 112 bytes 113 16 bytes of ciphertext. 114 """ 115 if len(block) != _BLOCK_SIZE: 116 raise ValueError( 117 f"Block must be exactly {_BLOCK_SIZE} bytes, got {len(block)}" 118 ) 119 cipher = Cipher(algorithms.AES(key), modes.ECB()) 120 encryptor = cipher.encryptor() 121 return encryptor.update(block) + encryptor.finalize() 122 123 @staticmethod 124 def decrypt_block(block: bytes, key: bytes) -> bytes: 125 """Decrypt a single 16-byte block (ECB mode, no IV). 126 127 Parameters 128 ---------- 129 block: 130 Exactly 16 bytes of ciphertext. 131 key: 132 AES key (16, 24, or 32 bytes). 133 134 Returns 135 ------- 136 bytes 137 16 bytes of plaintext. 138 """ 139 if len(block) != _BLOCK_SIZE: 140 raise ValueError( 141 f"Block must be exactly {_BLOCK_SIZE} bytes, got {len(block)}" 142 ) 143 cipher = Cipher(algorithms.AES(key), modes.ECB()) 144 decryptor = cipher.decryptor() 145 return decryptor.update(block) + decryptor.finalize() 146 147 # ------------------------------------------------------------------ 148 # Internal helpers 149 # ------------------------------------------------------------------ 150 151 @staticmethod 152 def _validate(payload: bytes, key: bytes, iv: bytes) -> None: 153 """Common pre-condition checks for CBC operations.""" 154 if len(iv) != _BLOCK_SIZE: 155 raise ValueError( 156 f"IV must be exactly {_BLOCK_SIZE} bytes, got {len(iv)}" 157 ) 158 if len(payload) == 0: 159 raise ValueError("Payload must not be empty") 160 if len(payload) % _BLOCK_SIZE != 0: 161 raise ValueError( 162 f"Payload length must be a multiple of {_BLOCK_SIZE}, " 163 f"got {len(payload)}" 164 )