purepython-aes 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. purepython_aes/__init__.py +33 -0
  2. purepython_aes/aes/__init__.py +29 -0
  3. purepython_aes/aes/algorithms/__init__.py +5 -0
  4. purepython_aes/aes/algorithms/aes128.py +13 -0
  5. purepython_aes/aes/algorithms/aes192.py +13 -0
  6. purepython_aes/aes/algorithms/aes256.py +13 -0
  7. purepython_aes/aes/core/__init__.py +3 -0
  8. purepython_aes/aes/core/aes.py +54 -0
  9. purepython_aes/aes/core/expansion.py +44 -0
  10. purepython_aes/aes/core/operations.py +64 -0
  11. purepython_aes/aes/core/state.py +210 -0
  12. purepython_aes/aes/interface.py +28 -0
  13. purepython_aes/aes/modes/__init__.py +4 -0
  14. purepython_aes/aes/modes/_base.py +14 -0
  15. purepython_aes/aes/modes/block/__init__.py +4 -0
  16. purepython_aes/aes/modes/block/_base.py +35 -0
  17. purepython_aes/aes/modes/block/ecb.py +21 -0
  18. purepython_aes/aes/modes/operations.py +16 -0
  19. purepython_aes/aes/padding/__init__.py +17 -0
  20. purepython_aes/aes/padding/_base.py +17 -0
  21. purepython_aes/aes/padding/ansix923.py +23 -0
  22. purepython_aes/aes/padding/iso10126.py +20 -0
  23. purepython_aes/aes/padding/iso7816.py +27 -0
  24. purepython_aes/aes/padding/no.py +16 -0
  25. purepython_aes/aes/padding/pkcs7.py +19 -0
  26. purepython_aes/aes/padding/zero.py +22 -0
  27. purepython_aes/const/__init__.py +26 -0
  28. purepython_aes/const/aes.py +40 -0
  29. purepython_aes/const/sbox.py +105 -0
  30. purepython_aes/py.typed +0 -0
  31. purepython_aes/types/__init__.py +5 -0
  32. purepython_aes/types/finite/__init__.py +3 -0
  33. purepython_aes/types/finite/aliases.py +37 -0
  34. purepython_aes-0.1.0.dist-info/METADATA +53 -0
  35. purepython_aes-0.1.0.dist-info/RECORD +39 -0
  36. purepython_aes-0.1.0.dist-info/WHEEL +5 -0
  37. purepython_aes-0.1.0.dist-info/scm_file_list.json +76 -0
  38. purepython_aes-0.1.0.dist-info/scm_version.json +8 -0
  39. purepython_aes-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,33 @@
1
+ from purepython_aes.aes import (
2
+ Aes,
3
+ Aes128,
4
+ Aes192,
5
+ Aes256,
6
+ AesMode,
7
+ AnsiX923Padding,
8
+ BasePadding,
9
+ BlockCipherMode,
10
+ EcbMode,
11
+ Iso7816Padding,
12
+ Iso10126Padding,
13
+ NoPadding,
14
+ Pkcs7Padding,
15
+ ZeroPadding,
16
+ )
17
+
18
+ __all__: list[str] = [
19
+ 'Aes',
20
+ 'Aes128',
21
+ 'Aes192',
22
+ 'Aes256',
23
+ 'AesMode',
24
+ 'AnsiX923Padding',
25
+ 'BasePadding',
26
+ 'BlockCipherMode',
27
+ 'EcbMode',
28
+ 'Iso7816Padding',
29
+ 'Iso10126Padding',
30
+ 'NoPadding',
31
+ 'Pkcs7Padding',
32
+ 'ZeroPadding',
33
+ ]
@@ -0,0 +1,29 @@
1
+ from purepython_aes.aes.algorithms import Aes128, Aes192, Aes256
2
+ from purepython_aes.aes.interface import Aes
3
+ from purepython_aes.aes.modes import AesMode, BlockCipherMode, EcbMode
4
+ from purepython_aes.aes.padding import (
5
+ AnsiX923Padding,
6
+ BasePadding,
7
+ Iso7816Padding,
8
+ Iso10126Padding,
9
+ NoPadding,
10
+ Pkcs7Padding,
11
+ ZeroPadding,
12
+ )
13
+
14
+ __all__: list[str] = [
15
+ 'Aes128',
16
+ 'Aes192',
17
+ 'Aes256',
18
+ 'Aes',
19
+ 'AesMode',
20
+ 'BlockCipherMode',
21
+ 'EcbMode',
22
+ 'AnsiX923Padding',
23
+ 'BasePadding',
24
+ 'Iso7816Padding',
25
+ 'Iso10126Padding',
26
+ 'NoPadding',
27
+ 'Pkcs7Padding',
28
+ 'ZeroPadding',
29
+ ]
@@ -0,0 +1,5 @@
1
+ from purepython_aes.aes.algorithms.aes128 import Aes128
2
+ from purepython_aes.aes.algorithms.aes192 import Aes192
3
+ from purepython_aes.aes.algorithms.aes256 import Aes256
4
+
5
+ __all__: list[str] = ['Aes128', 'Aes192', 'Aes256']
@@ -0,0 +1,13 @@
1
+ from dataclasses import dataclass
2
+ from typing import ClassVar
3
+
4
+ from purepython_aes.aes.core import AesCore
5
+ from purepython_aes.const import AES_128_KEY_SIZE, AES_128_ROUND_COUNT
6
+
7
+
8
+ @dataclass(init=False, slots=True)
9
+ class Aes128(AesCore):
10
+ """AES-128 encryption algorithm."""
11
+
12
+ __key_size__: ClassVar[int] = AES_128_KEY_SIZE
13
+ __round_count__: ClassVar[int] = AES_128_ROUND_COUNT
@@ -0,0 +1,13 @@
1
+ from dataclasses import dataclass
2
+ from typing import ClassVar
3
+
4
+ from purepython_aes.aes.core import AesCore
5
+ from purepython_aes.const import AES_192_KEY_SIZE, AES_192_ROUND_COUNT
6
+
7
+
8
+ @dataclass(init=False, slots=True)
9
+ class Aes192(AesCore):
10
+ """AES-192 encryption algorithm."""
11
+
12
+ __key_size__: ClassVar[int] = AES_192_KEY_SIZE
13
+ __round_count__: ClassVar[int] = AES_192_ROUND_COUNT
@@ -0,0 +1,13 @@
1
+ from dataclasses import dataclass
2
+ from typing import ClassVar
3
+
4
+ from purepython_aes.aes.core import AesCore
5
+ from purepython_aes.const import AES_256_KEY_SIZE, AES_256_ROUND_COUNT
6
+
7
+
8
+ @dataclass(init=False, slots=True)
9
+ class Aes256(AesCore):
10
+ """AES-256 encryption algorithm."""
11
+
12
+ __key_size__: ClassVar[int] = AES_256_KEY_SIZE
13
+ __round_count__: ClassVar[int] = AES_256_ROUND_COUNT
@@ -0,0 +1,3 @@
1
+ from purepython_aes.aes.core.aes import AesCore
2
+
3
+ __all__: list[str] = ['AesCore']
@@ -0,0 +1,54 @@
1
+ from dataclasses import dataclass
2
+
3
+ from purepython_aes.aes.core.expansion import expand_key
4
+ from purepython_aes.aes.core.state import AesState
5
+ from purepython_aes.aes.interface import Aes
6
+ from purepython_aes.const import AES_BLOCK_SIZE
7
+
8
+
9
+ @dataclass(init=False, slots=True)
10
+ class AesCore(Aes):
11
+ """Implement `encrypt_block` and `decrypt_block` methods."""
12
+
13
+ __round_keys: tuple[bytes, ...]
14
+
15
+ def __init__(self, key: bytes) -> None:
16
+ if len(key) != self.__key_size__:
17
+ raise ValueError(
18
+ f'expected len(key) == {self.__key_size__}, got {len(key)}'
19
+ )
20
+ self.__round_keys = expand_key(key, self.__round_count__)
21
+
22
+ def encrypt_block(self, plaintext: bytes) -> bytes:
23
+ if len(plaintext) != AES_BLOCK_SIZE:
24
+ raise ValueError(
25
+ f'Expected len(plaintext) == {AES_BLOCK_SIZE}, got {len(plaintext)}'
26
+ )
27
+ state: AesState = AesState.from_bytes(plaintext)
28
+ state.add_round_key(self.__round_keys[0])
29
+ for round_index in range(1, self.__round_count__):
30
+ state.subs_bytes()
31
+ state.shift_rows()
32
+ state.mix_columns()
33
+ state.add_round_key(self.__round_keys[round_index])
34
+ state.subs_bytes()
35
+ state.shift_rows()
36
+ state.add_round_key(self.__round_keys[self.__round_count__])
37
+ return state.to_bytes()
38
+
39
+ def decrypt_block(self, ciphertext: bytes) -> bytes:
40
+ if len(ciphertext) != AES_BLOCK_SIZE:
41
+ raise ValueError(
42
+ f'Expected len(ciphertext) == {AES_BLOCK_SIZE}, got {len(ciphertext)}'
43
+ )
44
+ state: AesState = AesState.from_bytes(ciphertext)
45
+ state.add_round_key(self.__round_keys[self.__round_count__])
46
+ for round_index in range(self.__round_count__ - 1, 0, -1):
47
+ state.inverse_shift_rows()
48
+ state.inverse_subs_bytes()
49
+ state.add_round_key(self.__round_keys[round_index])
50
+ state.inverse_mix_columns()
51
+ state.inverse_shift_rows()
52
+ state.inverse_subs_bytes()
53
+ state.add_round_key(self.__round_keys[0])
54
+ return state.to_bytes()
@@ -0,0 +1,44 @@
1
+ from purepython_aes.const import (
2
+ AES_256_KEY_SIZE,
3
+ AES_BLOCK_SIZE,
4
+ AES_WORD_SIZE,
5
+ ROUND_CONSTANTS,
6
+ SBOX,
7
+ )
8
+
9
+
10
+ def expand_key(key: bytes, round_count: int) -> tuple[bytes, ...]:
11
+ """Expand an AES key into one 16-byte key for every cipher round."""
12
+
13
+ key_word_count: int = len(key) // AES_WORD_SIZE
14
+ expanded_word_count: int = (AES_BLOCK_SIZE // AES_WORD_SIZE) * (round_count + 1)
15
+ expanded_key: bytearray = bytearray(expanded_word_count * AES_WORD_SIZE)
16
+ expanded_key[0 : len(key)] = key
17
+ for word_index in range(key_word_count, expanded_word_count):
18
+ previous_offset: int = (word_index - 1) * AES_WORD_SIZE
19
+ b0: int = expanded_key[previous_offset]
20
+ b1: int = expanded_key[previous_offset + 1]
21
+ b2: int = expanded_key[previous_offset + 2]
22
+ b3: int = expanded_key[previous_offset + 3]
23
+ position_in_key: int = word_index % key_word_count
24
+ if position_in_key == 0:
25
+ round_constant_index: int = word_index // key_word_count
26
+ b0, b1, b2, b3 = (
27
+ SBOX[b1] ^ ROUND_CONSTANTS[round_constant_index],
28
+ SBOX[b2],
29
+ SBOX[b3],
30
+ SBOX[b0],
31
+ )
32
+ else:
33
+ if len(key) == AES_256_KEY_SIZE and position_in_key == 4:
34
+ b0, b1, b2, b3 = SBOX[b0], SBOX[b1], SBOX[b2], SBOX[b3]
35
+ source_offset: int = (word_index - key_word_count) * AES_WORD_SIZE
36
+ destination_offset: int = word_index * AES_WORD_SIZE
37
+ expanded_key[destination_offset] = expanded_key[source_offset] ^ b0
38
+ expanded_key[destination_offset + 1] = expanded_key[source_offset + 1] ^ b1
39
+ expanded_key[destination_offset + 2] = expanded_key[source_offset + 2] ^ b2
40
+ expanded_key[destination_offset + 3] = expanded_key[source_offset + 3] ^ b3
41
+ return tuple(
42
+ bytes(expanded_key[offset : offset + AES_BLOCK_SIZE])
43
+ for offset in range(0, len(expanded_key), AES_BLOCK_SIZE)
44
+ )
@@ -0,0 +1,64 @@
1
+ def galois256_multiply(left: int, right: int) -> int:
2
+ """Multiply two bytes in the GF(2^8) finite field."""
3
+
4
+ result: int = 0
5
+ for _ in range(8):
6
+ if right & 1:
7
+ result ^= left
8
+ is_high_bit_set: bool = bool(left & 0x80)
9
+ left = (left << 1) & 0xFF
10
+ if is_high_bit_set:
11
+ left ^= 0x1B
12
+ right >>= 1
13
+ return result
14
+
15
+
16
+ def xor_bytes(*bs: int) -> int:
17
+ """XOR the supplied bytes."""
18
+
19
+ result: int = 0
20
+ for b in bs:
21
+ result ^= b
22
+ return result
23
+
24
+
25
+ def mix_column(b0: int, b1: int, b2: int, b3: int) -> tuple[int, int, int, int]:
26
+ """Apply `MixColumns` to one AES state column."""
27
+
28
+ return (
29
+ xor_bytes(galois256_multiply(0x02, b0), galois256_multiply(0x03, b1), b2, b3),
30
+ xor_bytes(b0, galois256_multiply(0x02, b1), galois256_multiply(0x03, b2), b3),
31
+ xor_bytes(b0, b1, galois256_multiply(0x02, b2), galois256_multiply(0x03, b3)),
32
+ xor_bytes(galois256_multiply(0x03, b0), b1, b2, galois256_multiply(0x02, b3)),
33
+ )
34
+
35
+
36
+ def inverse_mix_column(b0: int, b1: int, b2: int, b3: int) -> tuple[int, int, int, int]:
37
+ """Apply `InvMixColumns` to one AES state column."""
38
+
39
+ return (
40
+ xor_bytes(
41
+ galois256_multiply(0x0E, b0),
42
+ galois256_multiply(0x0B, b1),
43
+ galois256_multiply(0x0D, b2),
44
+ galois256_multiply(0x09, b3),
45
+ ),
46
+ xor_bytes(
47
+ galois256_multiply(0x09, b0),
48
+ galois256_multiply(0x0E, b1),
49
+ galois256_multiply(0x0B, b2),
50
+ galois256_multiply(0x0D, b3),
51
+ ),
52
+ xor_bytes(
53
+ galois256_multiply(0x0D, b0),
54
+ galois256_multiply(0x09, b1),
55
+ galois256_multiply(0x0E, b2),
56
+ galois256_multiply(0x0B, b3),
57
+ ),
58
+ xor_bytes(
59
+ galois256_multiply(0x0B, b0),
60
+ galois256_multiply(0x0D, b1),
61
+ galois256_multiply(0x09, b2),
62
+ galois256_multiply(0x0E, b3),
63
+ ),
64
+ )
@@ -0,0 +1,210 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import ClassVar, final
5
+
6
+ from purepython_aes.aes.core.operations import inverse_mix_column, mix_column
7
+ from purepython_aes.const import AES_BLOCK_SIZE, INVERSE_SBOX, SBOX
8
+
9
+
10
+ @dataclass(slots=True)
11
+ @final
12
+ class AesState:
13
+ __constructor_key: ClassVar[object] = object()
14
+ __b: list[int]
15
+
16
+ @classmethod
17
+ def from_bytes(cls, block: bytes) -> AesState:
18
+ """Create `AesState` from a 16-byte block."""
19
+
20
+ return cls(cls.__constructor_key, values=list(block))
21
+
22
+ def to_bytes(self) -> bytes:
23
+ """Serialize the AES state in column-major order."""
24
+
25
+ return bytes(self.__b)
26
+
27
+ def add_round_key(self, key: bytes) -> None:
28
+ """XOR a round key into this state."""
29
+
30
+ if len(key) != AES_BLOCK_SIZE:
31
+ raise ValueError(f'expected len(key) == {AES_BLOCK_SIZE}, got {len(key)}')
32
+ for index, key_byte in enumerate(key):
33
+ self.__b[index] ^= key_byte
34
+
35
+ def subs_bytes(self) -> None:
36
+ """Apply the AES S-box to every state byte."""
37
+
38
+ for index in range(AES_BLOCK_SIZE):
39
+ self.__b[index] = SBOX[self.__b[index]]
40
+
41
+ def inverse_subs_bytes(self) -> None:
42
+ """Apply the inverse AES S-box to every state byte."""
43
+
44
+ for index in range(AES_BLOCK_SIZE):
45
+ self.__b[index] = INVERSE_SBOX[self.__b[index]]
46
+
47
+ def shift_rows(self) -> None:
48
+ """Cyclically shift each AES state row to the left."""
49
+
50
+ (
51
+ self.__b[1],
52
+ self.__b[5],
53
+ self.__b[9],
54
+ self.__b[13],
55
+ self.__b[2],
56
+ self.__b[6],
57
+ self.__b[10],
58
+ self.__b[14],
59
+ self.__b[3],
60
+ self.__b[7],
61
+ self.__b[11],
62
+ self.__b[15],
63
+ ) = (
64
+ self.__b[5],
65
+ self.__b[9],
66
+ self.__b[13],
67
+ self.__b[1],
68
+ self.__b[10],
69
+ self.__b[14],
70
+ self.__b[2],
71
+ self.__b[6],
72
+ self.__b[15],
73
+ self.__b[3],
74
+ self.__b[7],
75
+ self.__b[11],
76
+ )
77
+
78
+ def inverse_shift_rows(self) -> None:
79
+ """Cyclically shift each AES state row to the right."""
80
+
81
+ (
82
+ self.__b[1],
83
+ self.__b[5],
84
+ self.__b[9],
85
+ self.__b[13],
86
+ self.__b[2],
87
+ self.__b[6],
88
+ self.__b[10],
89
+ self.__b[14],
90
+ self.__b[3],
91
+ self.__b[7],
92
+ self.__b[11],
93
+ self.__b[15],
94
+ ) = (
95
+ self.__b[13],
96
+ self.__b[1],
97
+ self.__b[5],
98
+ self.__b[9],
99
+ self.__b[10],
100
+ self.__b[14],
101
+ self.__b[2],
102
+ self.__b[6],
103
+ self.__b[7],
104
+ self.__b[11],
105
+ self.__b[15],
106
+ self.__b[3],
107
+ )
108
+
109
+ def mix_columns(self) -> None:
110
+ """Apply the AES `MixColumns` transformation."""
111
+
112
+ (
113
+ self.__b[0],
114
+ self.__b[1],
115
+ self.__b[2],
116
+ self.__b[3],
117
+ ) = mix_column(
118
+ self.__b[0],
119
+ self.__b[1],
120
+ self.__b[2],
121
+ self.__b[3],
122
+ )
123
+ (
124
+ self.__b[4],
125
+ self.__b[5],
126
+ self.__b[6],
127
+ self.__b[7],
128
+ ) = mix_column(
129
+ self.__b[4],
130
+ self.__b[5],
131
+ self.__b[6],
132
+ self.__b[7],
133
+ )
134
+ (
135
+ self.__b[8],
136
+ self.__b[9],
137
+ self.__b[10],
138
+ self.__b[11],
139
+ ) = mix_column(
140
+ self.__b[8],
141
+ self.__b[9],
142
+ self.__b[10],
143
+ self.__b[11],
144
+ )
145
+ (
146
+ self.__b[12],
147
+ self.__b[13],
148
+ self.__b[14],
149
+ self.__b[15],
150
+ ) = mix_column(
151
+ self.__b[12],
152
+ self.__b[13],
153
+ self.__b[14],
154
+ self.__b[15],
155
+ )
156
+
157
+ def inverse_mix_columns(self) -> None:
158
+ """Apply the AES `InvMixColumns` transformation."""
159
+
160
+ (
161
+ self.__b[0],
162
+ self.__b[1],
163
+ self.__b[2],
164
+ self.__b[3],
165
+ ) = inverse_mix_column(
166
+ self.__b[0],
167
+ self.__b[1],
168
+ self.__b[2],
169
+ self.__b[3],
170
+ )
171
+ (
172
+ self.__b[4],
173
+ self.__b[5],
174
+ self.__b[6],
175
+ self.__b[7],
176
+ ) = inverse_mix_column(
177
+ self.__b[4],
178
+ self.__b[5],
179
+ self.__b[6],
180
+ self.__b[7],
181
+ )
182
+ (
183
+ self.__b[8],
184
+ self.__b[9],
185
+ self.__b[10],
186
+ self.__b[11],
187
+ ) = inverse_mix_column(
188
+ self.__b[8],
189
+ self.__b[9],
190
+ self.__b[10],
191
+ self.__b[11],
192
+ )
193
+ (
194
+ self.__b[12],
195
+ self.__b[13],
196
+ self.__b[14],
197
+ self.__b[15],
198
+ ) = inverse_mix_column(
199
+ self.__b[12],
200
+ self.__b[13],
201
+ self.__b[14],
202
+ self.__b[15],
203
+ )
204
+
205
+ def __init__(self, __constructor_sentinel: object, /, values: list[int]) -> None:
206
+ if __constructor_sentinel is not AesState.__constructor_key:
207
+ raise ValueError(f'{AesState.__name__}() must not be called externally')
208
+ if len(values) != AES_BLOCK_SIZE:
209
+ raise ValueError(f'expected {AES_BLOCK_SIZE} values, got {len(values)}')
210
+ self.__b = values
@@ -0,0 +1,28 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import ClassVar
3
+
4
+
5
+ class Aes(ABC):
6
+ """Base class for AES-128, AES-192, and AES-256 algorithms."""
7
+
8
+ __key_size__: ClassVar[int]
9
+ """AES key length in bytes."""
10
+
11
+ __round_count__: ClassVar[int]
12
+ """Number of AES rounds."""
13
+
14
+ @abstractmethod
15
+ def encrypt_block(self, plaintext: bytes) -> bytes:
16
+ """Encrypt one 16-byte AES block."""
17
+
18
+ raise NotImplementedError
19
+
20
+ @abstractmethod
21
+ def decrypt_block(self, ciphertext: bytes) -> bytes:
22
+ """Decrypt one 16-byte AES block."""
23
+
24
+ raise NotImplementedError
25
+
26
+ @abstractmethod
27
+ def __init__(self, key: bytes) -> None:
28
+ raise NotImplementedError
@@ -0,0 +1,4 @@
1
+ from purepython_aes.aes.modes._base import AesMode
2
+ from purepython_aes.aes.modes.block import BlockCipherMode, EcbMode
3
+
4
+ __all__: list[str] = ['AesMode', 'BlockCipherMode', 'EcbMode']
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC
4
+ from dataclasses import dataclass
5
+
6
+ from purepython_aes.aes.interface import Aes
7
+
8
+
9
+ @dataclass
10
+ class AesMode(ABC):
11
+ """Base class for all AES modes of operation."""
12
+
13
+ algorithm: Aes
14
+ """Either AES-128, AES-192, or AES-256."""
@@ -0,0 +1,4 @@
1
+ from purepython_aes.aes.modes.block._base import BlockCipherMode
2
+ from purepython_aes.aes.modes.block.ecb import EcbMode
3
+
4
+ __all__: list[str] = ['BlockCipherMode', 'EcbMode']
@@ -0,0 +1,35 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass
3
+
4
+ from purepython_aes.aes.modes._base import AesMode
5
+ from purepython_aes.aes.padding import BasePadding
6
+ from purepython_aes.const import AES_BLOCK_SIZE
7
+
8
+
9
+ @dataclass(slots=True)
10
+ class BlockCipherMode(AesMode, ABC):
11
+ padding: BasePadding
12
+
13
+ @abstractmethod
14
+ def encrypt_blocks(self, padded_plaintext: bytes) -> bytes:
15
+ """Encrypt plaintext containing complete blocks."""
16
+
17
+ raise NotImplementedError
18
+
19
+ @abstractmethod
20
+ def decrypt_blocks(self, ciphertext: bytes) -> bytes:
21
+ """Decrypt ciphertext containing complete blocks."""
22
+
23
+ raise NotImplementedError
24
+
25
+ def encrypt(self, plaintext: bytes) -> bytes:
26
+ """Pad and encrypt a byte string."""
27
+
28
+ return self.encrypt_blocks(self.padding.pad(plaintext))
29
+
30
+ def decrypt(self, ciphertext: bytes) -> bytes:
31
+ """Decrypt and unpad a byte string."""
32
+
33
+ if len(ciphertext) % AES_BLOCK_SIZE != 0:
34
+ raise ValueError(f'len(ciphertext) % {AES_BLOCK_SIZE} must be 0')
35
+ return self.padding.unpad(self.decrypt_blocks(ciphertext))
@@ -0,0 +1,21 @@
1
+ from dataclasses import dataclass
2
+ from typing import final
3
+
4
+ from purepython_aes.aes.modes.block._base import BlockCipherMode
5
+ from purepython_aes.aes.modes.operations import split_into_blocks
6
+
7
+
8
+ @final
9
+ @dataclass(slots=True)
10
+ class EcbMode(BlockCipherMode):
11
+ """Electronic Codebook mode of operation."""
12
+
13
+ def encrypt_blocks(self, padded_plaintext: bytes) -> bytes:
14
+ return bytes(0).join(
15
+ map(self.algorithm.encrypt_block, split_into_blocks(padded_plaintext))
16
+ )
17
+
18
+ def decrypt_blocks(self, ciphertext: bytes) -> bytes:
19
+ return bytes(0).join(
20
+ map(self.algorithm.decrypt_block, split_into_blocks(ciphertext))
21
+ )
@@ -0,0 +1,16 @@
1
+ from purepython_aes.const import AES_BLOCK_SIZE
2
+
3
+
4
+ def xor(left: bytes, right: bytes) -> bytes:
5
+ """XOR two equally sized byte strings."""
6
+
7
+ return bytes(b0 ^ b1 for b0, b1 in zip(left, right, strict=True))
8
+
9
+
10
+ def split_into_blocks(data: bytes) -> tuple[bytes, ...]:
11
+ """Split data into 16-byte blocks."""
12
+
13
+ return tuple(
14
+ data[block_start : (block_start + AES_BLOCK_SIZE)]
15
+ for block_start in range(0, len(data), AES_BLOCK_SIZE)
16
+ )
@@ -0,0 +1,17 @@
1
+ from purepython_aes.aes.padding._base import BasePadding
2
+ from purepython_aes.aes.padding.ansix923 import AnsiX923Padding
3
+ from purepython_aes.aes.padding.iso7816 import Iso7816Padding
4
+ from purepython_aes.aes.padding.iso10126 import Iso10126Padding
5
+ from purepython_aes.aes.padding.no import NoPadding
6
+ from purepython_aes.aes.padding.pkcs7 import Pkcs7Padding
7
+ from purepython_aes.aes.padding.zero import ZeroPadding
8
+
9
+ __all__: list[str] = [
10
+ 'AnsiX923Padding',
11
+ 'BasePadding',
12
+ 'Iso10126Padding',
13
+ 'Iso7816Padding',
14
+ 'NoPadding',
15
+ 'Pkcs7Padding',
16
+ 'ZeroPadding',
17
+ ]
@@ -0,0 +1,17 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class BasePadding(ABC):
5
+ """Reversible block-padding algorithm."""
6
+
7
+ @abstractmethod
8
+ def pad(self, data: bytes) -> bytes:
9
+ """Pad data to a multiple of 16 bytes."""
10
+
11
+ raise NotImplementedError
12
+
13
+ @abstractmethod
14
+ def unpad(self, data: bytes) -> bytes:
15
+ """Remove padding from block-aligned data."""
16
+
17
+ raise NotImplementedError
@@ -0,0 +1,23 @@
1
+ from purepython_aes.aes.padding._base import BasePadding
2
+ from purepython_aes.const import AES_BLOCK_SIZE
3
+
4
+
5
+ class AnsiX923Padding(BasePadding):
6
+ """Implement deterministic ANSI X9.23 block padding."""
7
+
8
+ def pad(self, data: bytes) -> bytes:
9
+ padding_size: int = AES_BLOCK_SIZE - len(data) % AES_BLOCK_SIZE
10
+ zero_padding: bytes = bytes(padding_size - 1)
11
+ length_byte: bytes = bytes([padding_size])
12
+ return data + zero_padding + length_byte
13
+
14
+ def unpad(self, data: bytes) -> bytes:
15
+ if len(data) == 0:
16
+ raise ValueError('ANSI X9.23 padding expects non-empty data')
17
+ padding_size: int = data[-1]
18
+ if padding_size < 1 or padding_size > AES_BLOCK_SIZE:
19
+ raise ValueError('invalid ANSI X9.23 padding size')
20
+ zero_padding: bytes = data[(-padding_size):(-1)]
21
+ if zero_padding != bytes(padding_size - 1):
22
+ raise ValueError('invalid ANSI X9.23 padding bytes')
23
+ return data[:(-padding_size)]
@@ -0,0 +1,20 @@
1
+ from secrets import token_bytes
2
+
3
+ from purepython_aes.aes.padding._base import BasePadding
4
+ from purepython_aes.const import AES_BLOCK_SIZE
5
+
6
+
7
+ class Iso10126Padding(BasePadding):
8
+ """Implement legacy ISO 10126 block padding."""
9
+
10
+ def pad(self, data: bytes) -> bytes:
11
+ padding_size: int = AES_BLOCK_SIZE - len(data) % AES_BLOCK_SIZE
12
+ random_padding: bytes = token_bytes(padding_size - 1)
13
+ length_byte: bytes = bytes([padding_size])
14
+ return data + random_padding + length_byte
15
+
16
+ def unpad(self, data: bytes) -> bytes:
17
+ padding_size: int = data[-1]
18
+ if padding_size < 1 or padding_size > AES_BLOCK_SIZE:
19
+ raise ValueError('invalid ISO 10126 padding size')
20
+ return data[:(-padding_size)]
@@ -0,0 +1,27 @@
1
+ from typing import Final
2
+
3
+ from purepython_aes.aes.padding._base import BasePadding
4
+ from purepython_aes.const import AES_BLOCK_SIZE
5
+
6
+ PADDING_START_BYTE: Final[int] = 0x80
7
+
8
+
9
+ class Iso7816Padding(BasePadding):
10
+ """Implement ISO/IEC 7816-4 block padding."""
11
+
12
+ def pad(self, data: bytes) -> bytes:
13
+ padding_size: int = AES_BLOCK_SIZE - len(data) % AES_BLOCK_SIZE
14
+ zero_padding: bytes = bytes(padding_size - 1)
15
+ return data + bytes([PADDING_START_BYTE]) + zero_padding
16
+
17
+ def unpad(self, data: bytes) -> bytes:
18
+ padding_start_index: int = len(data) - 1
19
+ while padding_start_index >= 0 and data[padding_start_index] == 0:
20
+ padding_start_index -= 1
21
+ if padding_start_index < 0:
22
+ raise ValueError('ISO/IEC 7816-4 padding marker not found')
23
+ if data[padding_start_index] != PADDING_START_BYTE:
24
+ raise ValueError('invalid ISO/IEC 7816-4 padding marker')
25
+ if (len(data) - padding_start_index) > AES_BLOCK_SIZE:
26
+ raise ValueError('invalid ISO/IEC 7816-4 padding size')
27
+ return data[:padding_start_index]
@@ -0,0 +1,16 @@
1
+ from purepython_aes.aes.padding._base import BasePadding
2
+ from purepython_aes.const import AES_BLOCK_SIZE
3
+
4
+
5
+ class NoPadding(BasePadding):
6
+ """Require callers to provide block-aligned data."""
7
+
8
+ def pad(self, data: bytes) -> bytes:
9
+ if len(data) % AES_BLOCK_SIZE != 0:
10
+ raise ValueError(
11
+ f'expected len(data) % {AES_BLOCK_SIZE} == 0, got {len(data)}'
12
+ )
13
+ return data
14
+
15
+ def unpad(self, data: bytes) -> bytes:
16
+ return data
@@ -0,0 +1,19 @@
1
+ from purepython_aes.aes.padding._base import BasePadding
2
+ from purepython_aes.const import AES_BLOCK_SIZE
3
+
4
+
5
+ class Pkcs7Padding(BasePadding):
6
+ """Implement PKCS#7/CMS block padding."""
7
+
8
+ def pad(self, data: bytes) -> bytes:
9
+ padding_size: int = AES_BLOCK_SIZE - len(data) % AES_BLOCK_SIZE
10
+ padding: bytes = bytes([padding_size]) * padding_size
11
+ return data + padding
12
+
13
+ def unpad(self, data: bytes) -> bytes:
14
+ padding_size: int = data[-1]
15
+ if padding_size < 1 or padding_size > AES_BLOCK_SIZE:
16
+ raise ValueError('invalid PKCS#7 padding size')
17
+ if not data.endswith(bytes([padding_size]) * padding_size):
18
+ raise ValueError('invalid PKCS#7 padding bytes')
19
+ return data[:(-padding_size)]
@@ -0,0 +1,22 @@
1
+ from purepython_aes.aes.padding._base import BasePadding
2
+ from purepython_aes.const import AES_BLOCK_SIZE
3
+
4
+
5
+ class ZeroPadding(BasePadding):
6
+ """Implement ambiguous zero-byte padding."""
7
+
8
+ def pad(self, data: bytes) -> bytes:
9
+ remainder_size: int = len(data) % AES_BLOCK_SIZE
10
+ if remainder_size == 0:
11
+ return data
12
+ return data + bytes(AES_BLOCK_SIZE - remainder_size)
13
+
14
+ def unpad(self, data: bytes) -> bytes:
15
+ if len(data) % AES_BLOCK_SIZE != 0:
16
+ raise ValueError(
17
+ f'expected len(data) % {AES_BLOCK_SIZE} == 0, got {len(data)}'
18
+ )
19
+ unpadded_size: int = len(data)
20
+ while unpadded_size > 0 and data[unpadded_size - 1] == 0:
21
+ unpadded_size -= 1
22
+ return data[:unpadded_size]
@@ -0,0 +1,26 @@
1
+ from purepython_aes.const.aes import (
2
+ AES_128_KEY_SIZE,
3
+ AES_128_ROUND_COUNT,
4
+ AES_192_KEY_SIZE,
5
+ AES_192_ROUND_COUNT,
6
+ AES_256_KEY_SIZE,
7
+ AES_256_ROUND_COUNT,
8
+ AES_BLOCK_SIZE,
9
+ AES_WORD_SIZE,
10
+ ROUND_CONSTANTS,
11
+ )
12
+ from purepython_aes.const.sbox import INVERSE_SBOX, SBOX
13
+
14
+ __all__: list[str] = [
15
+ 'AES_128_KEY_SIZE',
16
+ 'AES_128_ROUND_COUNT',
17
+ 'AES_192_KEY_SIZE',
18
+ 'AES_192_ROUND_COUNT',
19
+ 'AES_256_KEY_SIZE',
20
+ 'AES_256_ROUND_COUNT',
21
+ 'AES_BLOCK_SIZE',
22
+ 'AES_WORD_SIZE',
23
+ 'ROUND_CONSTANTS',
24
+ 'INVERSE_SBOX',
25
+ 'SBOX',
26
+ ]
@@ -0,0 +1,40 @@
1
+ from typing import Final
2
+
3
+ AES_BLOCK_SIZE: Final[int] = 16
4
+ """AES block size in bytes."""
5
+
6
+ AES_WORD_SIZE: Final[int] = 4
7
+ """Word size in bytes."""
8
+
9
+ AES_128_KEY_SIZE: Final[int] = 16
10
+ """Key length in bytes for AES-128."""
11
+
12
+ AES_128_ROUND_COUNT: Final[int] = 10
13
+ """Number of rounds for AES-128."""
14
+
15
+ AES_192_KEY_SIZE: Final[int] = 24
16
+ """Key length in bytes for AES-192."""
17
+
18
+ AES_192_ROUND_COUNT: Final[int] = 12
19
+ """Number of rounds for AES-192."""
20
+
21
+ AES_256_KEY_SIZE: Final[int] = 32
22
+ """Key length in bytes for AES-256."""
23
+
24
+ AES_256_ROUND_COUNT: Final[int] = 14
25
+ """Number of rounds for AES-256."""
26
+
27
+ ROUND_CONSTANTS: Final[tuple[int, ...]] = (
28
+ 0x00,
29
+ 0x01,
30
+ 0x02,
31
+ 0x04,
32
+ 0x08,
33
+ 0x10,
34
+ 0x20,
35
+ 0x40,
36
+ 0x80,
37
+ 0x1B,
38
+ 0x36,
39
+ )
40
+ """Predefined values used during the AES key expansion."""
@@ -0,0 +1,105 @@
1
+ from typing import Final
2
+
3
+ from purepython_aes.types import IntLookupTable256
4
+
5
+ SBOX: Final[IntLookupTable256] = (
6
+ 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, # fmt: skip
7
+ 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76, # fmt: skip
8
+ 0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, # fmt: skip
9
+ 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0, # fmt: skip
10
+ 0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, # fmt: skip
11
+ 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15, # fmt: skip
12
+ 0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, # fmt: skip
13
+ 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75, # fmt: skip
14
+ 0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, # fmt: skip
15
+ 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84, # fmt: skip
16
+ 0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, # fmt: skip
17
+ 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF, # fmt: skip
18
+ 0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, # fmt: skip
19
+ 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8, # fmt: skip
20
+ 0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, # fmt: skip
21
+ 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, # fmt: skip
22
+ 0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, # fmt: skip
23
+ 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73, # fmt: skip
24
+ 0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, # fmt: skip
25
+ 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB, # fmt: skip
26
+ 0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, # fmt: skip
27
+ 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, # fmt: skip
28
+ 0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, # fmt: skip
29
+ 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08, # fmt: skip
30
+ 0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, # fmt: skip
31
+ 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A, # fmt: skip
32
+ 0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, # fmt: skip
33
+ 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E, # fmt: skip
34
+ 0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, # fmt: skip
35
+ 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF, # fmt: skip
36
+ 0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, # fmt: skip
37
+ 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16, # fmt: skip
38
+ )
39
+ """
40
+ Forward AES substitution box (S-box).
41
+
42
+ This table implements the nonlinear substitution used by the AES `SubBytes`
43
+ round operation and the `SubWord` operation during key expansion.
44
+
45
+ Internally, the AES S-box is constructed by:
46
+
47
+ 1. Computing the multiplicative inverse of the input byte in the finite field
48
+ GF(2^8), treating `0x00` as a special case.
49
+ 2. Applying the affine transformation defined by the AES specification.
50
+
51
+ The resulting mapping is a permutation of all 256 possible byte values, meaning
52
+ every byte appears exactly once as an output. Its inverse permutation is stored
53
+ in `INVERSE_SBOX`.
54
+ """
55
+
56
+
57
+ INVERSE_SBOX: Final[IntLookupTable256] = (
58
+ 0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, # fmt: skip
59
+ 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB, # fmt: skip
60
+ 0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, # fmt: skip
61
+ 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB, # fmt: skip
62
+ 0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, # fmt: skip
63
+ 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E, # fmt: skip
64
+ 0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, # fmt: skip
65
+ 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25, # fmt: skip
66
+ 0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, # fmt: skip
67
+ 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92, # fmt: skip
68
+ 0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, # fmt: skip
69
+ 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84, # fmt: skip
70
+ 0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, # fmt: skip
71
+ 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06, # fmt: skip
72
+ 0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, # fmt: skip
73
+ 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B, # fmt: skip
74
+ 0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, # fmt: skip
75
+ 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73, # fmt: skip
76
+ 0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, # fmt: skip
77
+ 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E, # fmt: skip
78
+ 0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, # fmt: skip
79
+ 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B, # fmt: skip
80
+ 0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, # fmt: skip
81
+ 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4, # fmt: skip
82
+ 0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, # fmt: skip
83
+ 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F, # fmt: skip
84
+ 0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, # fmt: skip
85
+ 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF, # fmt: skip
86
+ 0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, # fmt: skip
87
+ 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61, # fmt: skip
88
+ 0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, # fmt: skip
89
+ 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D, # fmt: skip
90
+ )
91
+ """
92
+ Inverse AES substitution box (Inverse S-box).
93
+
94
+ This table implements the inverse substitution transformation used by the
95
+ AES `InvSubBytes` round operation during decryption.
96
+
97
+ `INVERSE_SBOX` is the exact inverse permutation of `SBOX`. For every
98
+ possible byte value `n`:
99
+
100
+ INVERSE_SBOX[SBOX[n]] == n
101
+ SBOX[INVERSE_SBOX[n]] == n
102
+
103
+ Like the forward S-box, this table contains exactly 256 entries and represents
104
+ a permutation of every possible byte value.
105
+ """
File without changes
@@ -0,0 +1,5 @@
1
+ # noqa: A005
2
+
3
+ from purepython_aes.types.finite import IntLookupTable256
4
+
5
+ __all__: list[str] = ['IntLookupTable256']
@@ -0,0 +1,3 @@
1
+ from purepython_aes.types.finite.aliases import IntLookupTable256
2
+
3
+ __all__: list[str] = ['IntLookupTable256']
@@ -0,0 +1,37 @@
1
+ from typing import TypeAlias
2
+
3
+ IntLookupTable256: TypeAlias = tuple[
4
+ int, int, int, int, int, int, int, int, # fmt: skip
5
+ int, int, int, int, int, int, int, int, # fmt: skip
6
+ int, int, int, int, int, int, int, int, # fmt: skip
7
+ int, int, int, int, int, int, int, int, # fmt: skip
8
+ int, int, int, int, int, int, int, int, # fmt: skip
9
+ int, int, int, int, int, int, int, int, # fmt: skip
10
+ int, int, int, int, int, int, int, int, # fmt: skip
11
+ int, int, int, int, int, int, int, int, # fmt: skip
12
+ int, int, int, int, int, int, int, int, # fmt: skip
13
+ int, int, int, int, int, int, int, int, # fmt: skip
14
+ int, int, int, int, int, int, int, int, # fmt: skip
15
+ int, int, int, int, int, int, int, int, # fmt: skip
16
+ int, int, int, int, int, int, int, int, # fmt: skip
17
+ int, int, int, int, int, int, int, int, # fmt: skip
18
+ int, int, int, int, int, int, int, int, # fmt: skip
19
+ int, int, int, int, int, int, int, int, # fmt: skip
20
+ int, int, int, int, int, int, int, int, # fmt: skip
21
+ int, int, int, int, int, int, int, int, # fmt: skip
22
+ int, int, int, int, int, int, int, int, # fmt: skip
23
+ int, int, int, int, int, int, int, int, # fmt: skip
24
+ int, int, int, int, int, int, int, int, # fmt: skip
25
+ int, int, int, int, int, int, int, int, # fmt: skip
26
+ int, int, int, int, int, int, int, int, # fmt: skip
27
+ int, int, int, int, int, int, int, int, # fmt: skip
28
+ int, int, int, int, int, int, int, int, # fmt: skip
29
+ int, int, int, int, int, int, int, int, # fmt: skip
30
+ int, int, int, int, int, int, int, int, # fmt: skip
31
+ int, int, int, int, int, int, int, int, # fmt: skip
32
+ int, int, int, int, int, int, int, int, # fmt: skip
33
+ int, int, int, int, int, int, int, int, # fmt: skip
34
+ int, int, int, int, int, int, int, int, # fmt: skip
35
+ int, int, int, int, int, int, int, int, # fmt: skip
36
+ ]
37
+ """Type alias describing a fixed-length tuple containing exactly 256 integer values."""
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: purepython-aes
3
+ Version: 0.1.0
4
+ Summary: Pure-Python implementation of AES encryption algorithm.
5
+ Author-email: Emelianov Artem <penguin5726@proton.me>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Pukhosos/purepython-aes
8
+ Project-URL: Repository, https://github.com/Pukhosos/purepython-aes
9
+ Project-URL: Issues, https://github.com/Pukhosos/purepython-aes/issues
10
+ Classifier: Development Status :: 1 - Planning
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Security :: Cryptography
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ Provides-Extra: dev
24
+ Requires-Dist: isort; extra == "dev"
25
+ Requires-Dist: black; extra == "dev"
26
+ Requires-Dist: flake8; extra == "dev"
27
+ Requires-Dist: flake8-bugbear; extra == "dev"
28
+ Requires-Dist: flake8-builtins; extra == "dev"
29
+ Requires-Dist: flake8-comprehensions; extra == "dev"
30
+ Requires-Dist: flake8-fixme; extra == "dev"
31
+ Requires-Dist: flake8-pyproject; extra == "dev"
32
+ Requires-Dist: flake8-quotes; extra == "dev"
33
+ Requires-Dist: flake8-simplify; extra == "dev"
34
+ Requires-Dist: pep8-naming; extra == "dev"
35
+ Requires-Dist: mypy; extra == "dev"
36
+ Requires-Dist: pre-commit; extra == "dev"
37
+ Provides-Extra: test
38
+ Requires-Dist: pytest>=9.0; extra == "test"
39
+ Requires-Dist: pytest-env; extra == "test"
40
+ Requires-Dist: pytest-randomly; extra == "test"
41
+ Requires-Dist: pytest-xdist; extra == "test"
42
+ Requires-Dist: hypothesis; extra == "test"
43
+ Provides-Extra: fuzz
44
+ Requires-Dist: hypofuzz; extra == "fuzz"
45
+ Provides-Extra: build
46
+ Requires-Dist: build; extra == "build"
47
+ Requires-Dist: twine; extra == "build"
48
+
49
+ # purepython-aes
50
+
51
+ ## License
52
+
53
+ `purepython-aes` is a free, open-source software distributed under the [MIT License](LICENSE.txt)
@@ -0,0 +1,39 @@
1
+ purepython_aes/__init__.py,sha256=REYvoTsB3HL3wD6AR0T8NJZKfD51VX8aH4bcxUC_xp0,531
2
+ purepython_aes/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ purepython_aes/aes/__init__.py,sha256=lv_RO-dBRo9IPFtRY0csjkycsyUH7DXW41SZMCaHLV0,628
4
+ purepython_aes/aes/interface.py,sha256=OzNhRtS2xu5pOXFF7sAiHkNdSuMHLwaYSRm-0Ldz0Sg,699
5
+ purepython_aes/aes/algorithms/__init__.py,sha256=L21rL4gaCdoRmV2wyFJyuzFpBI4jXry1uQSi0b0eqds,221
6
+ purepython_aes/aes/algorithms/aes128.py,sha256=rmv-LJ2dcHIYmTfSTg2xAn-M94ank89ULYZKgpFuoVs,387
7
+ purepython_aes/aes/algorithms/aes192.py,sha256=Acjoel_K3vHzEuD4gv2idPJdiXWrSMyfHC_6SW_lOG4,387
8
+ purepython_aes/aes/algorithms/aes256.py,sha256=eeTW6Anl_F0b1RhFx__GZ5Uddl6d_5T2endcbBHmlBw,387
9
+ purepython_aes/aes/core/__init__.py,sha256=rCKHARdFJ8Q4e1QTTWSkzejbfIwcuHdo2lpC5QpJpzA,82
10
+ purepython_aes/aes/core/aes.py,sha256=xU1K9cNIek7moVeu-ZDrv3TIQOHTb6SoJsswXX10V3c,2141
11
+ purepython_aes/aes/core/expansion.py,sha256=JmNxuHIV3EzFFDFX2RAnbwKECimovBLrL4O3mfE8UOA,1979
12
+ purepython_aes/aes/core/operations.py,sha256=4jGJZ3xwRb3z0BLMorF-QCbTdPKAWUVPEvxkUe-5JjU,1989
13
+ purepython_aes/aes/core/state.py,sha256=j79LDXRabfGAkF2qx8iVKgrSDfEUfFnfKU1XQ-5OidM,5488
14
+ purepython_aes/aes/modes/__init__.py,sha256=fwEckEnJ9PGVSWNAXIG9M0KVhSSM7onKeFHwFkcvQ18,183
15
+ purepython_aes/aes/modes/_base.py,sha256=L81chTROXrIgNL5GJ1AT9icmixbTT7okfcZ0QyeVaiA,289
16
+ purepython_aes/aes/modes/operations.py,sha256=VYnoYB0Bb9bfwnApE3tL-2koLK6dDRotit1Pc1cAfhA,460
17
+ purepython_aes/aes/modes/block/__init__.py,sha256=TZuLX83Kkf1jhyJztqTIsUsULN8Q56fuUksm6eep_E8,173
18
+ purepython_aes/aes/modes/block/_base.py,sha256=TH6oekojIZluInw8ZLMFpPNuolYQOcxI0QOgsGIWekc,1119
19
+ purepython_aes/aes/modes/block/ecb.py,sha256=lL1gyIjq768Xe_V6QdWeG9xn5w2G-uTQqOX_cs9MYH4,668
20
+ purepython_aes/aes/padding/__init__.py,sha256=X_JqBtvuLm1edPoE7B-DnENBHIgwpemhB7d37ZRDQEY,582
21
+ purepython_aes/aes/padding/_base.py,sha256=dnuNv8OVOBDxAXSUGEeJu5uy9bSFkY2AKkzrK0If8so,408
22
+ purepython_aes/aes/padding/ansix923.py,sha256=xWLzlk85vQn1pPZm5x0wJy3aK46bvtK1YHNCLRC0yfA,991
23
+ purepython_aes/aes/padding/iso10126.py,sha256=OHIywjjWAXFwWcmNASnkGNZNY6xKUm5Y7A5rqETZ0MQ,750
24
+ purepython_aes/aes/padding/iso7816.py,sha256=I_ssvJVOnwriArkPlQlhn9fuRjNwEVR-sdGhJbD9Lyw,1106
25
+ purepython_aes/aes/padding/no.py,sha256=H73EYvOegwxqfpQ_7Xqb02Y4Zo2M3eL_-GlZLE98H4A,487
26
+ purepython_aes/aes/padding/pkcs7.py,sha256=XJk7K48bonCX7E9_UENF_Vva_cgXmhPxyH9BOnmbAjU,762
27
+ purepython_aes/aes/padding/zero.py,sha256=bFStfbwhquROKcZKzlWbK01De6kZb11UEP7Bj_HNZdM,787
28
+ purepython_aes/const/__init__.py,sha256=z5r00PDY5EvyWJGgTd1J4_mPTYwCzVbxWfQzvR3ZWRE,576
29
+ purepython_aes/const/aes.py,sha256=Vhk45-Tf3RXnoQ9qSNz6KLvQ5gkkOHOLakWQdegQMPA,806
30
+ purepython_aes/const/sbox.py,sha256=RaSYzs2mjUKZjv0fbEwCD3YLLkoz7emJhjEbU-RH09o,5399
31
+ purepython_aes/types/__init__.py,sha256=QiuMSx2DaC2lCXoK16RGeEBrfoX4d3jozkx4QV0kFjc,116
32
+ purepython_aes/types/finite/__init__.py,sha256=2cYu5Zxdp5NKAwYbV-MD2qamSaJxfqRYp0jOt657-YY,110
33
+ purepython_aes/types/finite/aliases.py,sha256=gKO8ETbMhV5FX6l3enF8wt246FpMPohreTZ3BPDThBM,1982
34
+ purepython_aes-0.1.0.dist-info/METADATA,sha256=EtqJAtIEueHuNorxAbaDbPVE6OV9B1bW4ho8pqVgMXs,2112
35
+ purepython_aes-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
36
+ purepython_aes-0.1.0.dist-info/scm_file_list.json,sha256=kfQDdACHcGFPsm09ncINh7o1yMzqwz4yaugdCuK7RFk,3068
37
+ purepython_aes-0.1.0.dist-info/scm_version.json,sha256=M8rto4eBaxUN90pVC3CaDYQm6j_D1U-WaRS4PcJkfOM,160
38
+ purepython_aes-0.1.0.dist-info/top_level.txt,sha256=IiTUB8OvHhwNXxf6OXKCfUADF7HcQdCRUMfPJC6TS5k,15
39
+ purepython_aes-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,76 @@
1
+ {
2
+ "files": [
3
+ ".pre-commit-config.yaml",
4
+ "README.md",
5
+ "pyproject.toml",
6
+ ".gitignore",
7
+ "LISENCE.txt",
8
+ "scripts/check-test-marks.sh",
9
+ "src/purepython_aes/py.typed",
10
+ "src/purepython_aes/__init__.py",
11
+ "src/purepython_aes/types/__init__.py",
12
+ "src/purepython_aes/types/finite/aliases.py",
13
+ "src/purepython_aes/types/finite/__init__.py",
14
+ "src/purepython_aes/const/__init__.py",
15
+ "src/purepython_aes/const/aes.py",
16
+ "src/purepython_aes/const/sbox.py",
17
+ "src/purepython_aes/aes/__init__.py",
18
+ "src/purepython_aes/aes/interface.py",
19
+ "src/purepython_aes/aes/core/__init__.py",
20
+ "src/purepython_aes/aes/core/aes.py",
21
+ "src/purepython_aes/aes/core/state.py",
22
+ "src/purepython_aes/aes/core/expansion.py",
23
+ "src/purepython_aes/aes/core/operations.py",
24
+ "src/purepython_aes/aes/padding/_base.py",
25
+ "src/purepython_aes/aes/padding/no.py",
26
+ "src/purepython_aes/aes/padding/pkcs7.py",
27
+ "src/purepython_aes/aes/padding/__init__.py",
28
+ "src/purepython_aes/aes/padding/zero.py",
29
+ "src/purepython_aes/aes/padding/iso10126.py",
30
+ "src/purepython_aes/aes/padding/iso7816.py",
31
+ "src/purepython_aes/aes/padding/ansix923.py",
32
+ "src/purepython_aes/aes/modes/_base.py",
33
+ "src/purepython_aes/aes/modes/__init__.py",
34
+ "src/purepython_aes/aes/modes/operations.py",
35
+ "src/purepython_aes/aes/modes/block/_base.py",
36
+ "src/purepython_aes/aes/modes/block/__init__.py",
37
+ "src/purepython_aes/aes/modes/block/ecb.py",
38
+ "src/purepython_aes/aes/algorithms/aes128.py",
39
+ "src/purepython_aes/aes/algorithms/__init__.py",
40
+ "src/purepython_aes/aes/algorithms/aes256.py",
41
+ "src/purepython_aes/aes/algorithms/aes192.py",
42
+ "tests/__init__.py",
43
+ "tests/test_initial.py",
44
+ "tests/unit/__init__.py",
45
+ "tests/unit/const/__init__.py",
46
+ "tests/unit/const/test_sbox.py",
47
+ "tests/unit/aes/__init__.py",
48
+ "tests/unit/aes/strategies.py",
49
+ "tests/unit/aes/core/test_operations.py",
50
+ "tests/unit/aes/core/__init__.py",
51
+ "tests/unit/aes/core/strategies.py",
52
+ "tests/unit/aes/core/test_state.py",
53
+ "tests/unit/aes/core/test_aes.py",
54
+ "tests/unit/aes/core/test_expansion.py",
55
+ "tests/unit/aes/padding/__init__.py",
56
+ "tests/unit/aes/padding/strategies.py",
57
+ "tests/unit/aes/padding/test_pkcs7.py",
58
+ "tests/unit/aes/padding/test_ansix923.py",
59
+ "tests/unit/aes/padding/test_iso10126.py",
60
+ "tests/unit/aes/padding/test_zero.py",
61
+ "tests/unit/aes/padding/conftest.py",
62
+ "tests/unit/aes/padding/test_iso7816.py",
63
+ "tests/unit/aes/padding/test_no.py",
64
+ "tests/unit/aes/modes/test_operations.py",
65
+ "tests/unit/aes/modes/__init__.py",
66
+ "tests/unit/aes/modes/block/__init__.py",
67
+ "tests/unit/aes/modes/block/test_ecb.py",
68
+ "tests/unit/aes/algorithms/test_aes256.py",
69
+ "tests/unit/aes/algorithms/__init__.py",
70
+ "tests/unit/aes/algorithms/test_aes128.py",
71
+ "tests/unit/aes/algorithms/test_aes192.py",
72
+ "tests/unit/aes/algorithms/conftest.py",
73
+ ".github/workflows/release.yml",
74
+ ".github/workflows/pre-commit.yml"
75
+ ]
76
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.1.0",
3
+ "distance": 0,
4
+ "node": "g18cff6ba62f4743b0f5af672a133edba13f2b244",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-13"
8
+ }
@@ -0,0 +1 @@
1
+ purepython_aes