purepython-aes 0.3.1__py3-none-any.whl → 0.4.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.
@@ -8,6 +8,8 @@ from purepython_aes.aes import (
8
8
  BasePadding,
9
9
  BlockCipherMode,
10
10
  CbcMode,
11
+ CfbMode,
12
+ CipherMode,
11
13
  EcbMode,
12
14
  Iso7816Padding,
13
15
  Iso10126Padding,
@@ -17,6 +19,7 @@ from purepython_aes.aes import (
17
19
  ReferenceAes128,
18
20
  ReferenceAes192,
19
21
  ReferenceAes256,
22
+ StreamCipherMode,
20
23
  ZeroPadding,
21
24
  )
22
25
 
@@ -30,6 +33,8 @@ __all__: list[str] = [
30
33
  'BasePadding',
31
34
  'BlockCipherMode',
32
35
  'CbcMode',
36
+ 'CfbMode',
37
+ 'CipherMode',
33
38
  'EcbMode',
34
39
  'Iso7816Padding',
35
40
  'Iso10126Padding',
@@ -39,5 +44,6 @@ __all__: list[str] = [
39
44
  'ReferenceAes128',
40
45
  'ReferenceAes192',
41
46
  'ReferenceAes256',
47
+ 'StreamCipherMode',
42
48
  'ZeroPadding',
43
49
  ]
@@ -11,8 +11,11 @@ from purepython_aes.aes.modes import (
11
11
  AesMode,
12
12
  BlockCipherMode,
13
13
  CbcMode,
14
+ CfbMode,
15
+ CipherMode,
14
16
  EcbMode,
15
17
  PcbcMode,
18
+ StreamCipherMode,
16
19
  )
17
20
  from purepython_aes.aes.padding import (
18
21
  AnsiX923Padding,
@@ -35,8 +38,11 @@ __all__: list[str] = [
35
38
  'AesMode',
36
39
  'BlockCipherMode',
37
40
  'CbcMode',
41
+ 'CfbMode',
42
+ 'CipherMode',
38
43
  'EcbMode',
39
44
  'PcbcMode',
45
+ 'StreamCipherMode',
40
46
  'AnsiX923Padding',
41
47
  'BasePadding',
42
48
  'Iso7816Padding',
@@ -1,4 +1,14 @@
1
- from purepython_aes.aes.modes._base import AesMode
1
+ from purepython_aes.aes.modes._base import AesMode, CipherMode
2
2
  from purepython_aes.aes.modes.block import BlockCipherMode, CbcMode, EcbMode, PcbcMode
3
+ from purepython_aes.aes.modes.stream import CfbMode, StreamCipherMode
3
4
 
4
- __all__: list[str] = ['AesMode', 'BlockCipherMode', 'CbcMode', 'EcbMode', 'PcbcMode']
5
+ __all__: list[str] = [
6
+ 'AesMode',
7
+ 'CipherMode',
8
+ 'BlockCipherMode',
9
+ 'CbcMode',
10
+ 'EcbMode',
11
+ 'PcbcMode',
12
+ 'CfbMode',
13
+ 'StreamCipherMode',
14
+ ]
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from abc import ABC
3
+ from abc import ABC, abstractmethod
4
4
  from dataclasses import dataclass
5
5
 
6
6
  from purepython_aes.aes.core.interface import Aes
@@ -12,3 +12,20 @@ class AesMode(ABC):
12
12
 
13
13
  algorithm: Aes
14
14
  """Either AES-128, AES-192, or AES-256."""
15
+
16
+
17
+ @dataclass(slots=True)
18
+ class CipherMode(AesMode):
19
+ """Base class for all AES Cipher modes of operation."""
20
+
21
+ @abstractmethod
22
+ def encrypt(self, plaintext: bytes) -> bytes:
23
+ """Encrypt a byte string."""
24
+
25
+ raise NotImplementedError
26
+
27
+ @abstractmethod
28
+ def decrypt(self, ciphertext: bytes) -> bytes:
29
+ """Decrypt a byte string."""
30
+
31
+ raise NotImplementedError
@@ -1,13 +1,13 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from dataclasses import dataclass
3
3
 
4
- from purepython_aes.aes.modes._base import AesMode
4
+ from purepython_aes.aes.modes._base import CipherMode
5
5
  from purepython_aes.aes.padding import BasePadding
6
6
  from purepython_aes.const import AES_BLOCK_SIZE
7
7
 
8
8
 
9
9
  @dataclass(slots=True)
10
- class BlockCipherMode(AesMode, ABC):
10
+ class BlockCipherMode(CipherMode, ABC):
11
11
  padding: BasePadding
12
12
 
13
13
  @abstractmethod
@@ -23,13 +23,9 @@ class BlockCipherMode(AesMode, ABC):
23
23
  raise NotImplementedError
24
24
 
25
25
  def encrypt(self, plaintext: bytes) -> bytes:
26
- """Pad and encrypt a byte string."""
27
-
28
26
  return self.__encrypt_blocks__(self.padding.pad(plaintext))
29
27
 
30
28
  def decrypt(self, ciphertext: bytes) -> bytes:
31
- """Decrypt and unpad a byte string."""
32
-
33
29
  if len(ciphertext) % AES_BLOCK_SIZE != 0:
34
30
  raise ValueError(f'len(ciphertext) % {AES_BLOCK_SIZE} must be 0')
35
31
  return self.padding.unpad(self.__decrypt_blocks__(ciphertext))
@@ -0,0 +1,4 @@
1
+ from purepython_aes.aes.modes.stream._base import StreamCipherMode
2
+ from purepython_aes.aes.modes.stream.cfb import CfbMode
3
+
4
+ __all__: list[str] = ['StreamCipherMode', 'CfbMode']
@@ -0,0 +1,40 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass
3
+ from secrets import token_bytes
4
+
5
+ from purepython_aes.aes.modes._base import CipherMode
6
+ from purepython_aes.const import AES_BLOCK_SIZE
7
+
8
+
9
+ @dataclass(slots=True)
10
+ class StreamCipherMode(CipherMode, ABC):
11
+ def encrypt(self, plaintext: bytes) -> bytes:
12
+ iv: bytes = token_bytes(AES_BLOCK_SIZE)
13
+ ciphertext: bytes = self.transform_for_encryption(iv, plaintext)
14
+ return iv + ciphertext
15
+
16
+ def decrypt(self, ciphertext: bytes) -> bytes:
17
+ return self.transform_for_decryption(
18
+ initialization_value=ciphertext[:AES_BLOCK_SIZE],
19
+ ciphertext=ciphertext[AES_BLOCK_SIZE:],
20
+ )
21
+
22
+ @abstractmethod
23
+ def transform_for_encryption(
24
+ self,
25
+ initialization_value: bytes,
26
+ plaintext: bytes,
27
+ ) -> bytes:
28
+ """Transform plaintext into ciphertext."""
29
+
30
+ raise NotImplementedError
31
+
32
+ @abstractmethod
33
+ def transform_for_decryption(
34
+ self,
35
+ initialization_value: bytes,
36
+ ciphertext: bytes,
37
+ ) -> bytes:
38
+ """Transform ciphertext into plaintext."""
39
+
40
+ raise NotImplementedError
@@ -0,0 +1,76 @@
1
+ from dataclasses import dataclass
2
+ from typing import final
3
+
4
+ from purepython_aes.aes.modes.stream._base import StreamCipherMode
5
+ from purepython_aes.const import AES_BLOCK_SIZE
6
+ from purepython_aes.types import CfbSegmentSize
7
+
8
+
9
+ @final
10
+ @dataclass(slots=True)
11
+ class CfbMode(StreamCipherMode):
12
+ """Cipher FeedBack mode of operation."""
13
+
14
+ segment_size: CfbSegmentSize = AES_BLOCK_SIZE # type: ignore[assignment]
15
+ """Feedback segment size in bytes."""
16
+
17
+ def __post_init__(self) -> None:
18
+ if self.segment_size < 1 or self.segment_size > AES_BLOCK_SIZE:
19
+ raise ValueError(
20
+ ', '.join(
21
+ (
22
+ f'expected 1 <= segment_size <= {AES_BLOCK_SIZE}',
23
+ f'got {self.segment_size}',
24
+ )
25
+ )
26
+ )
27
+
28
+ def transform_for_encryption(
29
+ self,
30
+ initialization_value: bytes,
31
+ plaintext: bytes,
32
+ ) -> bytes:
33
+ feedback_register: bytes = initialization_value
34
+ plaintext_size: int = len(plaintext)
35
+ ciphertext: bytearray = bytearray(plaintext_size)
36
+ for segment_start in range(0, plaintext_size, self.segment_size):
37
+ segment_end: int = min(segment_start + self.segment_size, plaintext_size)
38
+ segment_length: int = segment_end - segment_start
39
+ encrypted_register: bytes = self.algorithm.encrypt_block(feedback_register)
40
+ for segment_index in range(segment_length):
41
+ ciphertext[segment_start + segment_index] = (
42
+ plaintext[segment_start + segment_index]
43
+ ^ encrypted_register[segment_index]
44
+ )
45
+ ciphertext_segment: bytes = bytes(ciphertext[segment_start:segment_end])
46
+ feedback_register = (
47
+ feedback_register[segment_length:] + bytes(ciphertext_segment)
48
+ if segment_length != AES_BLOCK_SIZE
49
+ else ciphertext_segment
50
+ )
51
+ return bytes(ciphertext)
52
+
53
+ def transform_for_decryption(
54
+ self,
55
+ initialization_value: bytes,
56
+ ciphertext: bytes,
57
+ ) -> bytes:
58
+ feedback_register: bytes = initialization_value
59
+ ciphertext_size: int = len(ciphertext)
60
+ plaintext: bytearray = bytearray(ciphertext_size)
61
+ for segment_start in range(0, ciphertext_size, self.segment_size):
62
+ segment_end: int = min(segment_start + self.segment_size, ciphertext_size)
63
+ segment_length: int = segment_end - segment_start
64
+ encrypted_register: bytes = self.algorithm.encrypt_block(feedback_register)
65
+ for segment_index in range(segment_length):
66
+ plaintext[segment_start + segment_index] = (
67
+ ciphertext[segment_start + segment_index]
68
+ ^ encrypted_register[segment_index]
69
+ )
70
+ ciphertext_segment: bytes = ciphertext[segment_start:segment_end]
71
+ feedback_register = (
72
+ feedback_register[segment_length:] + ciphertext_segment
73
+ if segment_length != AES_BLOCK_SIZE
74
+ else ciphertext_segment
75
+ )
76
+ return bytes(plaintext)
@@ -1,5 +1,9 @@
1
1
  # noqa: A005
2
+ from purepython_aes.types.finite import (
3
+ CfbSegmentSize,
4
+ IntLookupTable256,
5
+ RoundKey,
6
+ RoundKeys,
7
+ )
2
8
 
3
- from purepython_aes.types.finite import IntLookupTable256, RoundKey, RoundKeys
4
-
5
- __all__: list[str] = ['IntLookupTable256', 'RoundKey', 'RoundKeys']
9
+ __all__: list[str] = ['CfbSegmentSize', 'IntLookupTable256', 'RoundKey', 'RoundKeys']
@@ -1,3 +1,4 @@
1
1
  from purepython_aes.types.finite.aliases import IntLookupTable256, RoundKey, RoundKeys
2
+ from purepython_aes.types.finite.literals import CfbSegmentSize
2
3
 
3
- __all__: list[str] = ['IntLookupTable256', 'RoundKey', 'RoundKeys']
4
+ __all__: list[str] = ['IntLookupTable256', 'RoundKey', 'RoundKeys', 'CfbSegmentSize']
@@ -0,0 +1,6 @@
1
+ from typing import Literal, TypeAlias
2
+
3
+ CfbSegmentSize: TypeAlias = Literal[
4
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
5
+ ]
6
+ """Allowed value for `segment_size` field of CfbMode."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: purepython-aes
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: Pure-Python implementation of AES encryption algorithm.
5
5
  Author-email: Emelianov Artem <penguin5726@proton.me>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
- purepython_aes/__init__.py,sha256=wvlzCvoB3WmSo9S-VEt6ssTDP4qcQcn9-CEVmYf4gAU,721
1
+ purepython_aes/__init__.py,sha256=sFq-h5KnKs8Cc2frwbw5vD4CCRXZLQvrJENiL_BH8W8,829
2
2
  purepython_aes/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- purepython_aes/aes/__init__.py,sha256=pOXG-x2P4ff_vZo5iSB17WHxerTQtQbs9peLglLt8ug,810
3
+ purepython_aes/aes/__init__.py,sha256=t9gw5jUnW-5t3nna3X3skgwMMcM1rSr32YYXR3Jv_sY,918
4
4
  purepython_aes/aes/core/__init__.py,sha256=4GPbzAHsnkR6HqowK6OcKZjGHQlNh2TkAGObaTdzFsc,435
5
5
  purepython_aes/aes/core/interface.py,sha256=OzNhRtS2xu5pOXFF7sAiHkNdSuMHLwaYSRm-0Ldz0Sg,699
6
6
  purepython_aes/aes/core/fast/__init__.py,sha256=Zy5rPai_UmbTWA0NBOby9LWHDrYYq5Mr4R0z_-uFhd4,200
@@ -21,14 +21,17 @@ purepython_aes/aes/core/reference/algorithms/__init__.py,sha256=gbIlFmH8DzKYg0Wo
21
21
  purepython_aes/aes/core/reference/algorithms/aes128.py,sha256=TD4_m3duy5wumE4mi5ybQ5TMPLcKF2JMYeCNeB0bW3o,410
22
22
  purepython_aes/aes/core/reference/algorithms/aes192.py,sha256=ju9q9Av0lm-zgTSqMPZn2c7yrkmahh1mAM_EKyn0evk,410
23
23
  purepython_aes/aes/core/reference/algorithms/aes256.py,sha256=2sUtwdXsIl_SZqEjolfa7_szyEYQ0-wVHfUWeQBRghM,410
24
- purepython_aes/aes/modes/__init__.py,sha256=NMoX0SgSjN3SLjt4PbeBhBSKTFIr0SBkFL7Ots5LVDs,225
25
- purepython_aes/aes/modes/_base.py,sha256=KI6KIhQRweclkbgl3rLuAGxDv6OZ-C6PK7c7-K0oSA8,294
24
+ purepython_aes/aes/modes/__init__.py,sha256=k_A9fbVW85TR4XHg6V8H3urYvnsN7RuByM_3nUCxodo,387
25
+ purepython_aes/aes/modes/_base.py,sha256=fFZFki4G_mu8DmaBxrBpKsHUY0tNdaZ3gtEOQvgPXzY,709
26
26
  purepython_aes/aes/modes/operations.py,sha256=VYnoYB0Bb9bfwnApE3tL-2koLK6dDRotit1Pc1cAfhA,460
27
27
  purepython_aes/aes/modes/block/__init__.py,sha256=V_hnUcEExOXRL91yPjApl7ms9ADPMOOg7JdqXYro24A,308
28
- purepython_aes/aes/modes/block/_base.py,sha256=E6tBpKP02bG66BnTJULxFW6pn7S2biBuVztddQ-vCCg,1135
28
+ purepython_aes/aes/modes/block/_base.py,sha256=TCEUcyeieIfhfpbV-IiK0Ln42-WJcZl4h-7DhW2bOkE,1047
29
29
  purepython_aes/aes/modes/block/cbc.py,sha256=GGh8IMa7NEK87UyL1RDM6jewTmNPLP-WWZQo9-asd9w,1627
30
30
  purepython_aes/aes/modes/block/ecb.py,sha256=iCLPQRuXZQzPJ3rS3Rw6299--iSgTDXXOSXWjvo6dJk,676
31
31
  purepython_aes/aes/modes/block/pcbc.py,sha256=DGWRkNGtIvOiZ6a0qXXWlKCQFT1m01uIs0hvelMEkTA,1674
32
+ purepython_aes/aes/modes/stream/__init__.py,sha256=SzvBUxdfYjZmo6-1xdtPXFW0iUOO-MBI63euZYwZVRA,177
33
+ purepython_aes/aes/modes/stream/_base.py,sha256=4C5uGGtWR8mOg_1H0CPnoqlBoh83NI5NDNPFyjwj4Is,1164
34
+ purepython_aes/aes/modes/stream/cfb.py,sha256=vr-T6ujpYChMI7yy6DwxPV94xbdMRqSt7g7SBM0zngY,3188
32
35
  purepython_aes/aes/padding/__init__.py,sha256=X_JqBtvuLm1edPoE7B-DnENBHIgwpemhB7d37ZRDQEY,582
33
36
  purepython_aes/aes/padding/_base.py,sha256=dnuNv8OVOBDxAXSUGEeJu5uy9bSFkY2AKkzrK0If8so,408
34
37
  purepython_aes/aes/padding/ansix923.py,sha256=xWLzlk85vQn1pPZm5x0wJy3aK46bvtK1YHNCLRC0yfA,991
@@ -40,12 +43,13 @@ purepython_aes/aes/padding/zero.py,sha256=bFStfbwhquROKcZKzlWbK01De6kZb11UEP7Bj_
40
43
  purepython_aes/const/__init__.py,sha256=z5r00PDY5EvyWJGgTd1J4_mPTYwCzVbxWfQzvR3ZWRE,576
41
44
  purepython_aes/const/aes.py,sha256=Vhk45-Tf3RXnoQ9qSNz6KLvQ5gkkOHOLakWQdegQMPA,806
42
45
  purepython_aes/const/sbox.py,sha256=RaSYzs2mjUKZjv0fbEwCD3YLLkoz7emJhjEbU-RH09o,5399
43
- purepython_aes/types/__init__.py,sha256=74jGOz6MIBNG2fU3FYww6M2oj-Cb_O5BXWZeYbtecHk,162
44
- purepython_aes/types/finite/__init__.py,sha256=mZ5jgOMJY0lzEA7_Ybr9UERN-8I19QOWCeKOQmhlV2o,156
46
+ purepython_aes/types/__init__.py,sha256=8U7QcSwJ99rR6vSO5hC7pTD0vwwCPo8kIaOt316h-kA,216
47
+ purepython_aes/types/finite/__init__.py,sha256=zpuVlmlJgGTiuuG5ULFdMnKqFeA0QNCBTBhlav1HjNw,238
45
48
  purepython_aes/types/finite/aliases.py,sha256=qRemfmEzf71H1EvRLBJWW23roZttnlPfVr-2N3V2mJw,2166
46
- purepython_aes-0.3.1.dist-info/METADATA,sha256=y67CMkFboxsrjuBGC1AmdYGIT7SUkl3FbTMurRtPgjs,2112
47
- purepython_aes-0.3.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
48
- purepython_aes-0.3.1.dist-info/scm_file_list.json,sha256=5IKUojOS_t_5M0tdnCisgMdYbriPmdBEo-db085Gf2U,4641
49
- purepython_aes-0.3.1.dist-info/scm_version.json,sha256=c-vOoHPHGYe693ggHwGleLWybUhShUAjW5Y6Ok-a5g0,160
50
- purepython_aes-0.3.1.dist-info/top_level.txt,sha256=IiTUB8OvHhwNXxf6OXKCfUADF7HcQdCRUMfPJC6TS5k,15
51
- purepython_aes-0.3.1.dist-info/RECORD,,
49
+ purepython_aes/types/finite/literals.py,sha256=8ikjbBjjEOJEl_1L9G-kbnw7Hvl6PT5rXDlBu9-_4ts,193
50
+ purepython_aes-0.4.0.dist-info/METADATA,sha256=oLHchCHRQhyhBdAF0aHTJtxJuKro98tTEHetLRSGz6I,2112
51
+ purepython_aes-0.4.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
52
+ purepython_aes-0.4.0.dist-info/scm_file_list.json,sha256=oF5fV6TnUT4bGY4o_HKsZ3hJNQOCRlCyuSeDCRG4vPg,4943
53
+ purepython_aes-0.4.0.dist-info/scm_version.json,sha256=9hIZ_IyMWP0imDxTcdSRcrJb8SJBNBaG_aUwTBWBPRw,160
54
+ purepython_aes-0.4.0.dist-info/top_level.txt,sha256=IiTUB8OvHhwNXxf6OXKCfUADF7HcQdCRUMfPJC6TS5k,15
55
+ purepython_aes-0.4.0.dist-info/RECORD,,
@@ -11,6 +11,7 @@
11
11
  "src/purepython_aes/types/__init__.py",
12
12
  "src/purepython_aes/types/finite/aliases.py",
13
13
  "src/purepython_aes/types/finite/__init__.py",
14
+ "src/purepython_aes/types/finite/literals.py",
14
15
  "src/purepython_aes/const/__init__.py",
15
16
  "src/purepython_aes/const/aes.py",
16
17
  "src/purepython_aes/const/sbox.py",
@@ -46,6 +47,9 @@
46
47
  "src/purepython_aes/aes/modes/_base.py",
47
48
  "src/purepython_aes/aes/modes/__init__.py",
48
49
  "src/purepython_aes/aes/modes/operations.py",
50
+ "src/purepython_aes/aes/modes/stream/_base.py",
51
+ "src/purepython_aes/aes/modes/stream/cfb.py",
52
+ "src/purepython_aes/aes/modes/stream/__init__.py",
49
53
  "src/purepython_aes/aes/modes/block/_base.py",
50
54
  "src/purepython_aes/aes/modes/block/__init__.py",
51
55
  "src/purepython_aes/aes/modes/block/cbc.py",
@@ -92,6 +96,8 @@
92
96
  "tests/unit/aes/padding/test_no.py",
93
97
  "tests/unit/aes/modes/test_operations.py",
94
98
  "tests/unit/aes/modes/__init__.py",
99
+ "tests/unit/aes/modes/stream/test_cfb.py",
100
+ "tests/unit/aes/modes/stream/__init__.py",
95
101
  "tests/unit/aes/modes/block/__init__.py",
96
102
  "tests/unit/aes/modes/block/test_pcbc.py",
97
103
  "tests/unit/aes/modes/block/test_ecb.py",
@@ -1,7 +1,7 @@
1
1
  {
2
- "tag": "0.3.1",
2
+ "tag": "0.4.0",
3
3
  "distance": 0,
4
- "node": "g35d4036fe4f9868f92a439cff80b412c016aa475",
4
+ "node": "ge127baa0ad018fcbb3e51110b1a486ae5ffe9daa",
5
5
  "dirty": false,
6
6
  "branch": "HEAD",
7
7
  "node_date": "2026-07-14"