├── .gitignore ├── LICENSE ├── README.md ├── benchmark.py ├── bsaes.py ├── test.py └── util.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 David Buchanan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Bitsliced AES 2 | 3 | An experimental implementation of bitsliced AES-128-ECB in pure python. Quite possibly the fastest pure-python AES implementation on the planet. 4 | 5 | Bitslice AES logic based on https://github.com/conorpp/bitsliced-aes. Thanks to `someone12469` for the bit packing trick. 6 | 7 | This is currently just a performance test, it has not been rigourously evaluated from a security perspective. 8 | 9 | Benchmark results from an M1 Pro macbook: (python3.9) 10 | 11 | ``` 12 | Size: 0x10 bytes. Speed: 0.00979213948158905 MB/s 13 | Size: 0x100 bytes. Speed: 0.2033569630731888 MB/s 14 | Size: 0x200 bytes. Speed: 0.3661175784327935 MB/s 15 | Size: 0x400 bytes. Speed: 0.7431157773410884 MB/s 16 | Size: 0x800 bytes. Speed: 1.4439644713729343 MB/s 17 | Size: 0x1000 bytes. Speed: 2.6039894565041872 MB/s 18 | Size: 0x10000 bytes. Speed: 8.098923491089558 MB/s 19 | Size: 0x100000 bytes. Speed: 11.94128913542198 MB/s 20 | Size: 0x400000 bytes. Speed: 7.914459860922976 MB/s 21 | Size: 0x1000000 bytes. Speed: 7.02089122658765 MB/s 22 | ``` 23 | 24 | Benchmark results from an AMD 3950x: (python3.8) 25 | 26 | ``` 27 | Size: 0x10 bytes. Speed: 0.010748708616940834 MB/s 28 | Size: 0x100 bytes. Speed: 0.20518680839274817 MB/s 29 | Size: 0x200 bytes. Speed: 0.3675134687165803 MB/s 30 | Size: 0x400 bytes. Speed: 0.7493836462514636 MB/s 31 | Size: 0x800 bytes. Speed: 1.476730332109973 MB/s 32 | Size: 0x1000 bytes. Speed: 2.71460696086907 MB/s 33 | Size: 0x10000 bytes. Speed: 9.777050554537222 MB/s 34 | Size: 0x100000 bytes. Speed: 13.119903169673073 MB/s 35 | Size: 0x400000 bytes. Speed: 7.310395026532739 MB/s 36 | Size: 0x1000000 bytes. Speed: 7.216789286347978 MB/s 37 | ``` 38 | 39 | Benchmark results from an AMD 5800x: (python3.10) 40 | 41 | ``` 42 | Size: 0x10 bytes. Speed: 0.012103095577838383 MB/s 43 | Size: 0x100 bytes. Speed: 0.23881866099628496 MB/s 44 | Size: 0x200 bytes. Speed: 0.43531839039412895 MB/s 45 | Size: 0x400 bytes. Speed: 0.9053066467189118 MB/s 46 | Size: 0x800 bytes. Speed: 1.7895068950475823 MB/s 47 | Size: 0x1000 bytes. Speed: 3.3794519407501507 MB/s 48 | Size: 0x10000 bytes. Speed: 14.013407355512953 MB/s 49 | Size: 0x100000 bytes. Speed: 17.402108711834412 MB/s 50 | Size: 0x400000 bytes. Speed: 13.657595307228183 MB/s 51 | Size: 0x1000000 bytes. Speed: 9.780158139496866 MB/s 52 | ``` 53 | 54 | Obviously, the optimal message length is around 1MB. I'm not quite sure why it slows 55 | down for longer messages, but I believe its happening in the bitslice packing/unpacking process. 56 | Longer messages could be broken up into shorter blocks, for optimal performance. 57 | 58 | For reference, the next fastest pure-python AES implementation I could find was https://github.com/ricmoo/pyaes. 59 | It runs at about 0.5MB/s (or with some of my own optimisations, about 0.75MB/s). 60 | 61 | This bitsliced implementation is an order of magnitude faster (although, only on inputs longer than about 1024 bytes). 62 | -------------------------------------------------------------------------------- /benchmark.py: -------------------------------------------------------------------------------- 1 | from bsaes import BitslicedAES128ECB 2 | import pyaes 3 | from timeit import timeit 4 | import os 5 | 6 | TEST_SIZES = [0x10, 0x100, 0x200, 0x400, 0x800, 0x1000, 0x10000, 0x100000, 0x400000, 0x1000000] 7 | TEST_REPEATS = [100, 100, 100, 100, 100, 100, 10, 4, 2, 1] 8 | 9 | aes = BitslicedAES128ECB(key=bytes(range(0x10))) 10 | 11 | 12 | for size, repeats in zip(TEST_SIZES, TEST_REPEATS): 13 | repeats = 1 14 | msg = os.urandom(size) 15 | #pyaes_aes = pyaes.Encrypter(pyaes.AESModeOfOperationECB(bytes(range(0x10)))) 16 | time = timeit(lambda: aes.encrypt(msg), number=repeats) 17 | #time = timeit(lambda: pyaes_aes.feed(msg) + pyaes_aes.feed(), number=repeats) 18 | 19 | print(f"Size: 0x{size:x} bytes. Speed: {size*repeats/0x100000/time} MB/s") 20 | -------------------------------------------------------------------------------- /bsaes.py: -------------------------------------------------------------------------------- 1 | from util import aes_128_key_expansion 2 | 3 | class BitslicedAES128ECB(): 4 | def __init__(self, key): 5 | if len(key) != 16: 6 | raise Exception("Only 128-bit keys are supported.") 7 | 8 | self._rkeys = aes_128_key_expansion(key) 9 | 10 | 11 | def encrypt(self, message): 12 | if len(message) % 16: 13 | raise Exception("Message length must be a multiple of 16 bytes") 14 | 15 | self._slice(message) 16 | 17 | self._add_round_key(self._rkeys[0]) 18 | for i in range(1, 10): 19 | self._sub_bytes() 20 | self._shift_rows() 21 | self._mix_columns() 22 | self._add_round_key(self._rkeys[i]) 23 | self._sub_bytes() 24 | self._shift_rows() 25 | self._add_round_key(self._rkeys[-1]) 26 | 27 | return self._unslice() 28 | 29 | 30 | def _slice(self, data): 31 | self._byte_length = len(data) 32 | 33 | # we add an extra 1-bit to the zeroes so that bitwise operations against it 34 | # will *hopefully* take the same amount of time as with the ones. The 35 | # ones also have this extra bit, for maximum symmetry. 36 | self._ones = int.from_bytes(b"\xff"*((self._byte_length+127)//128) + b"\x01", "little") 37 | self._zeroes = int.from_bytes(b"\x00"*((self._byte_length+127)//128) + b"\x01", "little") 38 | 39 | n = f"{int.from_bytes(data, 'little'):0{self._byte_length*8}b}" 40 | self._slices = [int(n[127-i::128], 2) for i in range(128)] 41 | 42 | 43 | def _unslice(self): 44 | data = bytearray(self._byte_length*8) 45 | for i in range(128): 46 | data[127-i::128] = f"{self._slices[i]:0{self._byte_length//16}b}"[-self._byte_length//16:].encode() 47 | return int(data, 2).to_bytes(self._byte_length, "little") 48 | 49 | 50 | def _sub_bytes(self): 51 | for i in range(0, 128, 8): 52 | self._slices[i:i+8] = self._sbox(self._slices[i:i+8]) 53 | 54 | 55 | def _mix_columns(self): 56 | for i in range(0, 128, 32): 57 | self._slices[i:i+32] = self._mix_column(self._slices[i:i+32]) 58 | 59 | 60 | def _add_round_key(self, rkey): 61 | for i in range(128): 62 | self._slices[i] ^= [self._zeroes, self._ones][(rkey>>i)&1] # TODO: make contant-timeier 63 | 64 | 65 | # https://github.com/conorpp/bitsliced-aes/blob/master/bs.c 66 | def _sbox(self, U): 67 | T1 = U[7] ^ U[4] 68 | T2 = U[7] ^ U[2] 69 | T3 = U[7] ^ U[1] 70 | T4 = U[4] ^ U[2] 71 | T5 = U[3] ^ U[1] 72 | T6 = T1 ^ T5 73 | T7 = U[6] ^ U[5] 74 | T8 = U[0] ^ T6 75 | T9 = U[0] ^ T7 76 | T10 = T6 ^ T7 77 | T11 = U[6] ^ U[2] 78 | T12 = U[5] ^ U[2] 79 | T13 = T3 ^ T4 80 | T14 = T6 ^ T11 81 | T15 = T5 ^ T11 82 | T16 = T5 ^ T12 83 | T17 = T9 ^ T16 84 | T18 = U[4] ^ U[0] 85 | T19 = T7 ^ T18 86 | T20 = T1 ^ T19 87 | T21 = U[1] ^ U[0] 88 | T22 = T7 ^ T21 89 | T23 = T2 ^ T22 90 | T24 = T2 ^ T10 91 | T25 = T20 ^ T17 92 | T26 = T3 ^ T16 93 | T27 = T1 ^ T12 94 | M1 = T13 & T6 95 | M2 = T23 & T8 96 | M3 = T14 ^ M1 97 | M4 = T19 & U[0] 98 | M5 = M4 ^ M1 99 | M6 = T3 & T16 100 | M7 = T22 & T9 101 | M8 = T26 ^ M6 102 | M9 = T20 & T17 103 | M10 = M9 ^ M6 104 | M11 = T1 & T15 105 | M12 = T4 & T27 106 | M13 = M12 ^ M11 107 | M14 = T2 & T10 108 | M15 = M14 ^ M11 109 | M16 = M3 ^ M2 110 | M17 = M5 ^ T24 111 | M18 = M8 ^ M7 112 | M19 = M10 ^ M15 113 | M20 = M16 ^ M13 114 | M21 = M17 ^ M15 115 | M22 = M18 ^ M13 116 | M23 = M19 ^ T25 117 | M24 = M22 ^ M23 118 | M25 = M22 & M20 119 | M26 = M21 ^ M25 120 | M27 = M20 ^ M21 121 | M28 = M23 ^ M25 122 | M29 = M28 & M27 123 | M30 = M26 & M24 124 | M31 = M20 & M23 125 | M32 = M27 & M31 126 | M33 = M27 ^ M25 127 | M34 = M21 & M22 128 | M35 = M24 & M34 129 | M36 = M24 ^ M25 130 | M37 = M21 ^ M29 131 | M38 = M32 ^ M33 132 | M39 = M23 ^ M30 133 | M40 = M35 ^ M36 134 | M41 = M38 ^ M40 135 | M42 = M37 ^ M39 136 | M43 = M37 ^ M38 137 | M44 = M39 ^ M40 138 | M45 = M42 ^ M41 139 | M46 = M44 & T6 140 | M47 = M40 & T8 141 | M48 = M39 & U[0] 142 | M49 = M43 & T16 143 | M50 = M38 & T9 144 | M51 = M37 & T17 145 | M52 = M42 & T15 146 | M53 = M45 & T27 147 | M54 = M41 & T10 148 | M55 = M44 & T13 149 | M56 = M40 & T23 150 | M57 = M39 & T19 151 | M58 = M43 & T3 152 | M59 = M38 & T22 153 | M60 = M37 & T20 154 | M61 = M42 & T1 155 | M62 = M45 & T4 156 | M63 = M41 & T2 157 | L0 = M61 ^ M62 158 | L1 = M50 ^ M56 159 | L2 = M46 ^ M48 160 | L3 = M47 ^ M55 161 | L4 = M54 ^ M58 162 | L5 = M49 ^ M61 163 | L6 = M62 ^ L5 164 | L7 = M46 ^ L3 165 | L8 = M51 ^ M59 166 | L9 = M52 ^ M53 167 | L10 = M53 ^ L4 168 | L11 = M60 ^ L2 169 | L12 = M48 ^ M51 170 | L13 = M50 ^ L0 171 | L14 = M52 ^ M61 172 | L15 = M55 ^ L1 173 | L16 = M56 ^ L0 174 | L17 = M57 ^ L1 175 | L18 = M58 ^ L8 176 | L19 = M63 ^ L4 177 | L20 = L0 ^ L1 178 | L21 = L1 ^ L7 179 | L22 = L3 ^ L12 180 | L23 = L18 ^ L2 181 | L24 = L15 ^ L9 182 | L25 = L6 ^ L10 183 | L26 = L7 ^ L9 184 | L27 = L8 ^ L10 185 | L28 = L11 ^ L14 186 | L29 = L11 ^ L17 187 | S = [None]*8 188 | S[7] = L6 ^ L24 189 | S[6] = L16 ^ L26 ^ self._ones 190 | S[5] = L19 ^ L28 ^ self._ones 191 | S[4] = L6 ^ L21 192 | S[3] = L20 ^ L22 193 | S[2] = L25 ^ L29 194 | S[1] = L13 ^ L27 ^ self._ones 195 | S[0] = L6 ^ L23 ^ self._ones 196 | return S 197 | 198 | 199 | def _shift_rows(self): 200 | Bp = [None]*128 201 | offsetr0 = 0 202 | offsetr1 = 32 203 | offsetr2 = 64 204 | offsetr3 = 96 205 | B0 = 0 206 | B1 = 32 207 | B2 = 64 208 | B3 = 96 209 | for _ in range(4): 210 | Br0 = self._slices[offsetr0:offsetr0+8] 211 | Br1 = self._slices[offsetr1:offsetr1+8] 212 | Br2 = self._slices[offsetr2:offsetr2+8] 213 | Br3 = self._slices[offsetr3:offsetr3+8] 214 | 215 | Bp[B0 + 0] = Br0[0] 216 | Bp[B0 + 1] = Br0[1] 217 | Bp[B0 + 2] = Br0[2] 218 | Bp[B0 + 3] = Br0[3] 219 | Bp[B0 + 4] = Br0[4] 220 | Bp[B0 + 5] = Br0[5] 221 | Bp[B0 + 6] = Br0[6] 222 | Bp[B0 + 7] = Br0[7] 223 | Bp[B1 + 0] = Br1[0] 224 | Bp[B1 + 1] = Br1[1] 225 | Bp[B1 + 2] = Br1[2] 226 | Bp[B1 + 3] = Br1[3] 227 | Bp[B1 + 4] = Br1[4] 228 | Bp[B1 + 5] = Br1[5] 229 | Bp[B1 + 6] = Br1[6] 230 | Bp[B1 + 7] = Br1[7] 231 | Bp[B2 + 0] = Br2[0] 232 | Bp[B2 + 1] = Br2[1] 233 | Bp[B2 + 2] = Br2[2] 234 | Bp[B2 + 3] = Br2[3] 235 | Bp[B2 + 4] = Br2[4] 236 | Bp[B2 + 5] = Br2[5] 237 | Bp[B2 + 6] = Br2[6] 238 | Bp[B2 + 7] = Br2[7] 239 | Bp[B3 + 0] = Br3[0] 240 | Bp[B3 + 1] = Br3[1] 241 | Bp[B3 + 2] = Br3[2] 242 | Bp[B3 + 3] = Br3[3] 243 | Bp[B3 + 4] = Br3[4] 244 | Bp[B3 + 5] = Br3[5] 245 | Bp[B3 + 6] = Br3[6] 246 | Bp[B3 + 7] = Br3[7] 247 | 248 | offsetr0 = (offsetr0 + 128//16 + 128//4) & 0x7f 249 | offsetr1 = (offsetr1 + 128//16 + 128//4) & 0x7f 250 | offsetr2 = (offsetr2 + 128//16 + 128//4) & 0x7f 251 | offsetr3 = (offsetr3 + 128//16 + 128//4) & 0x7f 252 | 253 | B0 += 8 254 | B1 += 8 255 | B2 += 8 256 | B3 += 8 257 | 258 | self._slices = Bp 259 | 260 | 261 | def _mix_column(self, B): 262 | A0 = 0 263 | A1 = 8 264 | A2 = 16 265 | A3 = 24 266 | 267 | Bp = [None]*32 268 | of = B[A0+7] ^ B[A1+7] 269 | 270 | Bp[A0+0] = B[A1+0] ^ B[A2+0] ^ B[A3+0] ^ of 271 | Bp[A0+1] = B[A0+0] ^ B[A1+0] ^ B[A1+1] ^ B[A2+1] ^ B[A3+1] ^ of 272 | Bp[A0+2] = B[A0+1] ^ B[A1+1] ^ B[A1+2] ^ B[A2+2] ^ B[A3+2] 273 | Bp[A0+3] = B[A0+2] ^ B[A1+2] ^ B[A1+3] ^ B[A2+3] ^ B[A3+3] ^ of 274 | Bp[A0+4] = B[A0+3] ^ B[A1+3] ^ B[A1+4] ^ B[A2+4] ^ B[A3+4] ^ of 275 | Bp[A0+5] = B[A0+4] ^ B[A1+4] ^ B[A1+5] ^ B[A2+5] ^ B[A3+5] 276 | Bp[A0+6] = B[A0+5] ^ B[A1+5] ^ B[A1+6] ^ B[A2+6] ^ B[A3+6] 277 | Bp[A0+7] = B[A0+6] ^ B[A1+6] ^ B[A1+7] ^ B[A2+7] ^ B[A3+7] 278 | 279 | of = B[A1+7] ^ B[A2+7] 280 | 281 | Bp[A1+0] = B[A0+0] ^ B[A2+0] ^ B[A3+0] ^ of 282 | Bp[A1+1] = B[A0+1] ^ B[A1+0] ^ B[A2+0] ^ B[A2+1] ^ B[A3+1] ^ of 283 | Bp[A1+2] = B[A0+2] ^ B[A1+1] ^ B[A2+1] ^ B[A2+2] ^ B[A3+2] 284 | Bp[A1+3] = B[A0+3] ^ B[A1+2] ^ B[A2+2] ^ B[A2+3] ^ B[A3+3] ^ of 285 | Bp[A1+4] = B[A0+4] ^ B[A1+3] ^ B[A2+3] ^ B[A2+4] ^ B[A3+4] ^ of 286 | Bp[A1+5] = B[A0+5] ^ B[A1+4] ^ B[A2+4] ^ B[A2+5] ^ B[A3+5] 287 | Bp[A1+6] = B[A0+6] ^ B[A1+5] ^ B[A2+5] ^ B[A2+6] ^ B[A3+6] 288 | Bp[A1+7] = B[A0+7] ^ B[A1+6] ^ B[A2+6] ^ B[A2+7] ^ B[A3+7] 289 | 290 | of = B[A2+7] ^ B[A3+7] 291 | 292 | Bp[A2+0] = B[A0+0] ^ B[A1+0] ^ B[A3+0] ^ of 293 | Bp[A2+1] = B[A0+1] ^ B[A1+1] ^ B[A2+0] ^ B[A3+0] ^ B[A3+1] ^ of 294 | Bp[A2+2] = B[A0+2] ^ B[A1+2] ^ B[A2+1] ^ B[A3+1] ^ B[A3+2] 295 | Bp[A2+3] = B[A0+3] ^ B[A1+3] ^ B[A2+2] ^ B[A3+2] ^ B[A3+3] ^ of 296 | Bp[A2+4] = B[A0+4] ^ B[A1+4] ^ B[A2+3] ^ B[A3+3] ^ B[A3+4] ^ of 297 | Bp[A2+5] = B[A0+5] ^ B[A1+5] ^ B[A2+4] ^ B[A3+4] ^ B[A3+5] 298 | Bp[A2+6] = B[A0+6] ^ B[A1+6] ^ B[A2+5] ^ B[A3+5] ^ B[A3+6] 299 | Bp[A2+7] = B[A0+7] ^ B[A1+7] ^ B[A2+6] ^ B[A3+6] ^ B[A3+7] 300 | 301 | of = B[A0+7] ^ B[A3+7] 302 | 303 | Bp[A3+0] = B[A0+0] ^ B[A1+0] ^ B[A2+0] ^ of 304 | Bp[A3+1] = B[A0+1] ^ B[A0+0] ^ B[A1+1] ^ B[A2+1] ^ B[A3+0] ^ of 305 | Bp[A3+2] = B[A0+2] ^ B[A0+1] ^ B[A1+2] ^ B[A2+2] ^ B[A3+1] 306 | Bp[A3+3] = B[A0+3] ^ B[A0+2] ^ B[A1+3] ^ B[A2+3] ^ B[A3+2] ^ of 307 | Bp[A3+4] = B[A0+4] ^ B[A0+3] ^ B[A1+4] ^ B[A2+4] ^ B[A3+3] ^ of 308 | Bp[A3+5] = B[A0+5] ^ B[A0+4] ^ B[A1+5] ^ B[A2+5] ^ B[A3+4] 309 | Bp[A3+6] = B[A0+6] ^ B[A0+5] ^ B[A1+6] ^ B[A2+6] ^ B[A3+5] 310 | Bp[A3+7] = B[A0+7] ^ B[A0+6] ^ B[A1+7] ^ B[A2+7] ^ B[A3+6] 311 | 312 | return Bp 313 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from bsaes import BitslicedAES128ECB 2 | from Crypto.Cipher import AES 3 | import os 4 | 5 | def check(key, pt): 6 | bsaes = BitslicedAES128ECB(key=key) 7 | bs_ct = bsaes.encrypt(pt) 8 | print("bs_ct:", bs_ct.hex()) 9 | 10 | ogaes = AES.new(key=key, mode=AES.MODE_ECB) 11 | og_ct = ogaes.encrypt(pt) 12 | print("og_ct:", og_ct.hex()) 13 | 14 | assert(bs_ct == og_ct) 15 | 16 | 17 | KEY = bytes(range(16)) 18 | PLAINTEXT = bytes(range(16)) 19 | 20 | check(KEY, PLAINTEXT) 21 | check(os.urandom(16), os.urandom(128)) 22 | -------------------------------------------------------------------------------- /util.py: -------------------------------------------------------------------------------- 1 | RCON = [ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 ] 2 | SBOX = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 ] 3 | 4 | 5 | def xor_bytes(a, b): 6 | return bytearray([ai^bi for ai, bi in zip(a, b)]) 7 | 8 | 9 | def rot_word(word): 10 | return word[1:] + word[:1] 11 | 12 | 13 | def sub_word(word): 14 | return bytearray(SBOX[x] for x in word) 15 | 16 | 17 | def aes_128_key_expansion(key): 18 | round_keys = [bytearray(key)] 19 | for i in range(1, 10+1): 20 | rkey = bytearray() 21 | temp = sub_word(rot_word(round_keys[-1][-4:])) 22 | temp[0] ^= RCON[i-1] 23 | for j in range(4): 24 | temp = xor_bytes(temp, round_keys[-1][j * 4 : j * 4 + 4]) 25 | rkey += temp 26 | round_keys.append(rkey) 27 | return [int.from_bytes(k, "little") for k in round_keys] 28 | --------------------------------------------------------------------------------