fast-cipher 0.1.0__tar.gz

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.
@@ -0,0 +1,207 @@
1
+ Metadata-Version: 2.3
2
+ Name: fast-cipher
3
+ Version: 0.1.0
4
+ Summary: FAST format-preserving encryption cipher in pure Python
5
+ Requires-Dist: cryptography>=46.0.5
6
+ Requires-Dist: pytest>=9.0.2 ; extra == 'dev'
7
+ Requires-Python: >=3.14
8
+ Provides-Extra: dev
9
+ Description-Content-Type: text/markdown
10
+
11
+ # FAST format-preserving cipher for Python
12
+
13
+ A pure Python implementation of the [FAST cipher](https://github.com/jedisct1/fast),
14
+ a format-preserving encryption (FPE) scheme designed for tokenizing API keys, credentials, and other structured secrets.
15
+
16
+ For prefix-based tokens (GitHub, AWS, Stripe, etc.), encrypted output keeps the exact same format
17
+ (length, prefix, character set) as the originals, so they pass through systems that validate token formats.
18
+ Heuristic tokens (Fastly, AWS secret keys) are wrapped in a tagged marker since they have no distinguishing prefix.
19
+
20
+ Fully interoperable with the [C](https://github.com/jedisct1/c-fast),
21
+ [Zig](https://github.com/jedisct1/zig-fast),
22
+ [Go](https://github.com/jedisct1/go-fast), and
23
+ [JavaScript](https://github.com/nickvdyck/js-fast) implementations.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install fast-cipher
29
+ ```
30
+
31
+ Or with [uv](https://docs.astral.sh/uv/):
32
+
33
+ ```bash
34
+ uv add fast-cipher
35
+ ```
36
+
37
+ ## Quick start
38
+
39
+ The most common use case is encrypting tokens and API keys found inside a block of text.
40
+ `TokenEncryptor` handles scanning, encrypting, and decrypting automatically:
41
+
42
+ ```python
43
+ import os
44
+ from fast_cipher.tokens import TokenEncryptor
45
+
46
+ key = os.urandom(32) # AES-128, AES-192, or AES-256
47
+ encryptor = TokenEncryptor(key)
48
+
49
+ text = """
50
+ Here are the credentials for the staging environment:
51
+ GitHub token: ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij
52
+ AWS access key: AKIAIOSFODNN7EXAMPLE
53
+ Stripe key: sk_live_ABCDEFGHIJKLMNOPQRSTUVWXab
54
+ """
55
+
56
+ encrypted = encryptor.encrypt(text)
57
+ ```
58
+
59
+ For prefix-based tokens the result still looks like valid tokens: same prefixes, same lengths, same
60
+ character sets, but the secret parts have been replaced with ciphertext. Decryption restores the original
61
+ text exactly:
62
+
63
+ ```python
64
+ decrypted = encryptor.decrypt(encrypted)
65
+ assert decrypted == text
66
+ ```
67
+
68
+ ## Tweaks
69
+
70
+ A tweak is optional context data that gets mixed into the encryption.
71
+ The same plaintext encrypted with different tweaks produces different ciphertext,
72
+ which is useful for binding tokens to a specific user, session, or tenant:
73
+
74
+ ```python
75
+ enc_alice = encryptor.encrypt(text, tweak=b"user-alice")
76
+ enc_bob = encryptor.encrypt(text, tweak=b"user-bob")
77
+
78
+ assert enc_alice != enc_bob
79
+
80
+ # Each can only be decrypted with the matching tweak
81
+ assert encryptor.decrypt(enc_alice, tweak=b"user-alice") == text
82
+ assert encryptor.decrypt(enc_bob, tweak=b"user-bob") == text
83
+ ```
84
+
85
+ ## Filtering by token type
86
+
87
+ If you only want to encrypt certain kinds of tokens and leave the rest as-is,
88
+ pass a `types` list with the pattern names you care about:
89
+
90
+ ```python
91
+ text = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij and AKIAIOSFODNN7EXAMPLE"
92
+
93
+ encrypted = encryptor.encrypt(text, types=["github-pat"])
94
+ # The GitHub token is encrypted, but the AWS key is untouched
95
+ ```
96
+
97
+ ## Supported token types
98
+
99
+ The following patterns are detected and encrypted out of the box:
100
+
101
+ | Provider | Pattern name(s) | Prefix |
102
+ | -------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------- |
103
+ | Anthropic | `anthropic` | `sk-ant-api03-` |
104
+ | AWS | `aws-access-key` | `AKIA` |
105
+ | Datadog | `datadog` | `ddapi_` |
106
+ | GitHub | `github-pat`, `github-oauth`, `github-user`, `github-server`, `github-refresh` | `ghp_`, `gho_`, `ghu_`, `ghs_`, `ghr_` |
107
+ | GitLab | `gitlab` | `glpat-` |
108
+ | Google | `google-api` | `AIza` |
109
+ | Grafana | `grafana` | `glc_` |
110
+ | HuggingFace | `huggingface` | `hf_` |
111
+ | npm | `npm` | `npm_` |
112
+ | OpenAI | `openai`, `openai-legacy` | `sk-proj-`, `sk-` |
113
+ | PyPI | `pypi` | `pypi-` |
114
+ | SendGrid | `sendgrid` | `SG.` |
115
+ | Slack | `slack-bot`, `slack-user` | `xoxb-`, `xoxp-` |
116
+ | Stripe | `stripe-secret-live`, `stripe-publish-live`, `stripe-secret-test`, `stripe-publish-test` | `sk_live_`, `pk_live_`, `sk_test_`, `pk_test_` |
117
+ | Supabase | `supabase` | `sbp_` |
118
+ | Twilio | `twilio` | `SK` |
119
+ | Vercel | `vercel` | `vercel_` |
120
+ | Fastly | `fastly` | *(heuristic, no prefix)* |
121
+ | AWS Secret Key | `aws-secret-key` | *(heuristic, no prefix)* |
122
+
123
+ Heuristic patterns don't rely on a prefix. They look for strings with high entropy and mixed character classes, which is how secrets like Fastly tokens and AWS secret keys are typically formatted. Because there is no distinguishing prefix, encrypted output is wrapped in an `[ENCRYPTED:<name>]` marker. `decrypt()` will attempt to unwrap anything matching that marker pattern, so avoid feeding text containing literal `[ENCRYPTED:...]` strings that were not produced by `encrypt()`.
124
+
125
+ ## Custom token patterns
126
+
127
+ You can register your own patterns for tokens that aren't covered by the built-in set.
128
+ A `SimpleTokenPattern` works for anything that has a fixed prefix followed by a body with a known alphabet:
129
+
130
+ ```python
131
+ from fast_cipher.tokens import ALPHANUMERIC, TokenEncryptor
132
+ from fast_cipher.tokens.types import SimpleTokenPattern
133
+
134
+ my_pattern = SimpleTokenPattern(
135
+ name="myapp-api-key",
136
+ prefix="myapp_",
137
+ body_regex="[A-Za-z0-9]{32}",
138
+ body_alphabet=ALPHANUMERIC,
139
+ min_body_length=32,
140
+ )
141
+
142
+ key = os.urandom(32)
143
+ encryptor = TokenEncryptor(key)
144
+ encryptor.register(my_pattern)
145
+
146
+ text = "key: myapp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef"
147
+ encrypted = encryptor.encrypt(text)
148
+ decrypted = encryptor.decrypt(encrypted)
149
+ assert decrypted == text
150
+ ```
151
+
152
+ Registered patterns take priority over built-in ones.
153
+
154
+ The available alphabets are `DIGITS`, `HEX_LOWER`, `ALPHANUMERIC_UPPER`, `ALPHANUMERIC_LOWER`, `ALPHANUMERIC`, `BASE64`, and `BASE64URL`.
155
+ You can also create your own with `Alphabet(name="my-abc", chars="abc...")`.
156
+
157
+ ## Low-level cipher
158
+
159
+ `TokenEncryptor` is built on top of `FastCipher`, which you can use directly when you need format-preserving encryption for arbitrary data. It works on sequences of integers in a given radix (base).
160
+
161
+ For example, to encrypt an 8-digit decimal number:
162
+
163
+ ```python
164
+ from fast_cipher import FastCipher, calculate_recommended_params
165
+
166
+ params = calculate_recommended_params(radix=10, word_length=8)
167
+ key = os.urandom(32)
168
+ cipher = FastCipher(params, key)
169
+
170
+ plaintext = [1, 2, 3, 4, 5, 6, 7, 8]
171
+ ciphertext = cipher.encrypt(plaintext)
172
+
173
+ # Result is still 8 digits, each between 0 and 9
174
+ assert len(ciphertext) == 8
175
+ assert all(0 <= d < 10 for d in ciphertext)
176
+
177
+ decrypted = cipher.decrypt(ciphertext)
178
+ assert decrypted == plaintext
179
+ ```
180
+
181
+ For raw bytes, use radix 256 with the `encrypt_bytes`/`decrypt_bytes` convenience methods:
182
+
183
+ ```python
184
+ params = calculate_recommended_params(radix=256, word_length=16)
185
+ cipher = FastCipher(params, key)
186
+
187
+ ciphertext = cipher.encrypt_bytes(b"sensitive data!!")
188
+ plaintext = cipher.decrypt_bytes(ciphertext)
189
+ assert plaintext == b"sensitive data!!"
190
+ ```
191
+
192
+ ## Cleanup
193
+
194
+ When you're done with an encryptor or cipher, call `destroy()` to invalidate the instance:
195
+
196
+ ```python
197
+ encryptor.destroy()
198
+ ```
199
+
200
+ After `destroy()`, any further calls to `encrypt()` or `decrypt()` will raise a `RuntimeError`.
201
+ The method overwrites key references with zero-filled placeholders and clears internal state,
202
+ but Python's garbage collector may retain copies of the original key bytes in memory.
203
+ For applications that require guaranteed memory scrubbing, use a C-level implementation instead.
204
+
205
+ ## Cross-implementation compatibility
206
+
207
+ Ciphertext produced by any FAST implementation (C, Zig, Go, JavaScript, Python) can be decrypted by any other, as long as the key, radix, word length, and tweak match. This library is tested against the Go test vectors and cross-validated against the JavaScript implementation.
@@ -0,0 +1,197 @@
1
+ # FAST format-preserving cipher for Python
2
+
3
+ A pure Python implementation of the [FAST cipher](https://github.com/jedisct1/fast),
4
+ a format-preserving encryption (FPE) scheme designed for tokenizing API keys, credentials, and other structured secrets.
5
+
6
+ For prefix-based tokens (GitHub, AWS, Stripe, etc.), encrypted output keeps the exact same format
7
+ (length, prefix, character set) as the originals, so they pass through systems that validate token formats.
8
+ Heuristic tokens (Fastly, AWS secret keys) are wrapped in a tagged marker since they have no distinguishing prefix.
9
+
10
+ Fully interoperable with the [C](https://github.com/jedisct1/c-fast),
11
+ [Zig](https://github.com/jedisct1/zig-fast),
12
+ [Go](https://github.com/jedisct1/go-fast), and
13
+ [JavaScript](https://github.com/nickvdyck/js-fast) implementations.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install fast-cipher
19
+ ```
20
+
21
+ Or with [uv](https://docs.astral.sh/uv/):
22
+
23
+ ```bash
24
+ uv add fast-cipher
25
+ ```
26
+
27
+ ## Quick start
28
+
29
+ The most common use case is encrypting tokens and API keys found inside a block of text.
30
+ `TokenEncryptor` handles scanning, encrypting, and decrypting automatically:
31
+
32
+ ```python
33
+ import os
34
+ from fast_cipher.tokens import TokenEncryptor
35
+
36
+ key = os.urandom(32) # AES-128, AES-192, or AES-256
37
+ encryptor = TokenEncryptor(key)
38
+
39
+ text = """
40
+ Here are the credentials for the staging environment:
41
+ GitHub token: ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij
42
+ AWS access key: AKIAIOSFODNN7EXAMPLE
43
+ Stripe key: sk_live_ABCDEFGHIJKLMNOPQRSTUVWXab
44
+ """
45
+
46
+ encrypted = encryptor.encrypt(text)
47
+ ```
48
+
49
+ For prefix-based tokens the result still looks like valid tokens: same prefixes, same lengths, same
50
+ character sets, but the secret parts have been replaced with ciphertext. Decryption restores the original
51
+ text exactly:
52
+
53
+ ```python
54
+ decrypted = encryptor.decrypt(encrypted)
55
+ assert decrypted == text
56
+ ```
57
+
58
+ ## Tweaks
59
+
60
+ A tweak is optional context data that gets mixed into the encryption.
61
+ The same plaintext encrypted with different tweaks produces different ciphertext,
62
+ which is useful for binding tokens to a specific user, session, or tenant:
63
+
64
+ ```python
65
+ enc_alice = encryptor.encrypt(text, tweak=b"user-alice")
66
+ enc_bob = encryptor.encrypt(text, tweak=b"user-bob")
67
+
68
+ assert enc_alice != enc_bob
69
+
70
+ # Each can only be decrypted with the matching tweak
71
+ assert encryptor.decrypt(enc_alice, tweak=b"user-alice") == text
72
+ assert encryptor.decrypt(enc_bob, tweak=b"user-bob") == text
73
+ ```
74
+
75
+ ## Filtering by token type
76
+
77
+ If you only want to encrypt certain kinds of tokens and leave the rest as-is,
78
+ pass a `types` list with the pattern names you care about:
79
+
80
+ ```python
81
+ text = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij and AKIAIOSFODNN7EXAMPLE"
82
+
83
+ encrypted = encryptor.encrypt(text, types=["github-pat"])
84
+ # The GitHub token is encrypted, but the AWS key is untouched
85
+ ```
86
+
87
+ ## Supported token types
88
+
89
+ The following patterns are detected and encrypted out of the box:
90
+
91
+ | Provider | Pattern name(s) | Prefix |
92
+ | -------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------- |
93
+ | Anthropic | `anthropic` | `sk-ant-api03-` |
94
+ | AWS | `aws-access-key` | `AKIA` |
95
+ | Datadog | `datadog` | `ddapi_` |
96
+ | GitHub | `github-pat`, `github-oauth`, `github-user`, `github-server`, `github-refresh` | `ghp_`, `gho_`, `ghu_`, `ghs_`, `ghr_` |
97
+ | GitLab | `gitlab` | `glpat-` |
98
+ | Google | `google-api` | `AIza` |
99
+ | Grafana | `grafana` | `glc_` |
100
+ | HuggingFace | `huggingface` | `hf_` |
101
+ | npm | `npm` | `npm_` |
102
+ | OpenAI | `openai`, `openai-legacy` | `sk-proj-`, `sk-` |
103
+ | PyPI | `pypi` | `pypi-` |
104
+ | SendGrid | `sendgrid` | `SG.` |
105
+ | Slack | `slack-bot`, `slack-user` | `xoxb-`, `xoxp-` |
106
+ | Stripe | `stripe-secret-live`, `stripe-publish-live`, `stripe-secret-test`, `stripe-publish-test` | `sk_live_`, `pk_live_`, `sk_test_`, `pk_test_` |
107
+ | Supabase | `supabase` | `sbp_` |
108
+ | Twilio | `twilio` | `SK` |
109
+ | Vercel | `vercel` | `vercel_` |
110
+ | Fastly | `fastly` | *(heuristic, no prefix)* |
111
+ | AWS Secret Key | `aws-secret-key` | *(heuristic, no prefix)* |
112
+
113
+ Heuristic patterns don't rely on a prefix. They look for strings with high entropy and mixed character classes, which is how secrets like Fastly tokens and AWS secret keys are typically formatted. Because there is no distinguishing prefix, encrypted output is wrapped in an `[ENCRYPTED:<name>]` marker. `decrypt()` will attempt to unwrap anything matching that marker pattern, so avoid feeding text containing literal `[ENCRYPTED:...]` strings that were not produced by `encrypt()`.
114
+
115
+ ## Custom token patterns
116
+
117
+ You can register your own patterns for tokens that aren't covered by the built-in set.
118
+ A `SimpleTokenPattern` works for anything that has a fixed prefix followed by a body with a known alphabet:
119
+
120
+ ```python
121
+ from fast_cipher.tokens import ALPHANUMERIC, TokenEncryptor
122
+ from fast_cipher.tokens.types import SimpleTokenPattern
123
+
124
+ my_pattern = SimpleTokenPattern(
125
+ name="myapp-api-key",
126
+ prefix="myapp_",
127
+ body_regex="[A-Za-z0-9]{32}",
128
+ body_alphabet=ALPHANUMERIC,
129
+ min_body_length=32,
130
+ )
131
+
132
+ key = os.urandom(32)
133
+ encryptor = TokenEncryptor(key)
134
+ encryptor.register(my_pattern)
135
+
136
+ text = "key: myapp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef"
137
+ encrypted = encryptor.encrypt(text)
138
+ decrypted = encryptor.decrypt(encrypted)
139
+ assert decrypted == text
140
+ ```
141
+
142
+ Registered patterns take priority over built-in ones.
143
+
144
+ The available alphabets are `DIGITS`, `HEX_LOWER`, `ALPHANUMERIC_UPPER`, `ALPHANUMERIC_LOWER`, `ALPHANUMERIC`, `BASE64`, and `BASE64URL`.
145
+ You can also create your own with `Alphabet(name="my-abc", chars="abc...")`.
146
+
147
+ ## Low-level cipher
148
+
149
+ `TokenEncryptor` is built on top of `FastCipher`, which you can use directly when you need format-preserving encryption for arbitrary data. It works on sequences of integers in a given radix (base).
150
+
151
+ For example, to encrypt an 8-digit decimal number:
152
+
153
+ ```python
154
+ from fast_cipher import FastCipher, calculate_recommended_params
155
+
156
+ params = calculate_recommended_params(radix=10, word_length=8)
157
+ key = os.urandom(32)
158
+ cipher = FastCipher(params, key)
159
+
160
+ plaintext = [1, 2, 3, 4, 5, 6, 7, 8]
161
+ ciphertext = cipher.encrypt(plaintext)
162
+
163
+ # Result is still 8 digits, each between 0 and 9
164
+ assert len(ciphertext) == 8
165
+ assert all(0 <= d < 10 for d in ciphertext)
166
+
167
+ decrypted = cipher.decrypt(ciphertext)
168
+ assert decrypted == plaintext
169
+ ```
170
+
171
+ For raw bytes, use radix 256 with the `encrypt_bytes`/`decrypt_bytes` convenience methods:
172
+
173
+ ```python
174
+ params = calculate_recommended_params(radix=256, word_length=16)
175
+ cipher = FastCipher(params, key)
176
+
177
+ ciphertext = cipher.encrypt_bytes(b"sensitive data!!")
178
+ plaintext = cipher.decrypt_bytes(ciphertext)
179
+ assert plaintext == b"sensitive data!!"
180
+ ```
181
+
182
+ ## Cleanup
183
+
184
+ When you're done with an encryptor or cipher, call `destroy()` to invalidate the instance:
185
+
186
+ ```python
187
+ encryptor.destroy()
188
+ ```
189
+
190
+ After `destroy()`, any further calls to `encrypt()` or `decrypt()` will raise a `RuntimeError`.
191
+ The method overwrites key references with zero-filled placeholders and clears internal state,
192
+ but Python's garbage collector may retain copies of the original key bytes in memory.
193
+ For applications that require guaranteed memory scrubbing, use a C-level implementation instead.
194
+
195
+ ## Cross-implementation compatibility
196
+
197
+ Ciphertext produced by any FAST implementation (C, Zig, Go, JavaScript, Python) can be decrypted by any other, as long as the key, radix, word length, and tweak match. This library is tested against the Go test vectors and cross-validated against the JavaScript implementation.
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "fast-cipher"
3
+ version = "0.1.0"
4
+ description = "FAST format-preserving encryption cipher in pure Python"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = [
8
+ "cryptography>=46.0.5",
9
+ ]
10
+
11
+ [project.optional-dependencies]
12
+ dev = ["pytest>=9.0.2"]
13
+
14
+ [build-system]
15
+ requires = ["uv_build>=0.10.10,<0.11.0"]
16
+ build-backend = "uv_build"
17
+
18
+ [dependency-groups]
19
+ dev = [
20
+ "pytest>=9.0.2",
21
+ "ruff>=0.15.6",
22
+ ]
@@ -0,0 +1,29 @@
1
+ from .cipher import FastCipher
2
+ from .params import calculate_recommended_params
3
+ from .types import (
4
+ FastError,
5
+ FastParams,
6
+ InvalidBranchDistError,
7
+ InvalidKeyError,
8
+ InvalidLengthError,
9
+ InvalidParametersError,
10
+ InvalidRadixError,
11
+ InvalidSBoxCountError,
12
+ InvalidValueError,
13
+ InvalidWordLengthError,
14
+ )
15
+
16
+ __all__ = [
17
+ "FastCipher",
18
+ "FastError",
19
+ "FastParams",
20
+ "InvalidBranchDistError",
21
+ "InvalidKeyError",
22
+ "InvalidLengthError",
23
+ "InvalidParametersError",
24
+ "InvalidRadixError",
25
+ "InvalidSBoxCountError",
26
+ "InvalidValueError",
27
+ "InvalidWordLengthError",
28
+ "calculate_recommended_params",
29
+ ]
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ from .core import cdec, cenc
4
+ from .encoding import build_setup1_input, build_setup2_input
5
+ from .prf import derive_key
6
+ from .prng import generate_sequence
7
+ from .sbox import generate_sbox_pool
8
+ from .types import (
9
+ FastParams,
10
+ InvalidBranchDistError,
11
+ InvalidKeyError,
12
+ InvalidLengthError,
13
+ InvalidRadixError,
14
+ InvalidSBoxCountError,
15
+ InvalidValueError,
16
+ InvalidWordLengthError,
17
+ )
18
+
19
+ DERIVED_KEY_SIZE = 32
20
+
21
+
22
+ class FastCipher:
23
+ """FAST format-preserving encryption cipher."""
24
+
25
+ def __init__(self, params: FastParams, key: bytes) -> None:
26
+ _validate_params(params, key)
27
+ self.params = params
28
+ self._master_key = bytes(key)
29
+
30
+ pool_key_material = derive_key(
31
+ key, build_setup1_input(params), DERIVED_KEY_SIZE
32
+ )
33
+ self._sboxes = generate_sbox_pool(
34
+ params.radix, params.sbox_count, bytes(pool_key_material)
35
+ )
36
+
37
+ self._cached_tweak: bytes | None = None
38
+ self._cached_seq: list[int] | None = None
39
+ self._destroyed = False
40
+
41
+ def _ensure_sequence(self, tweak: bytes) -> list[int]:
42
+ if self._cached_seq is not None and self._cached_tweak == tweak:
43
+ return self._cached_seq
44
+
45
+ seq_key_material = derive_key(
46
+ self._master_key,
47
+ build_setup2_input(self.params, tweak),
48
+ DERIVED_KEY_SIZE,
49
+ )
50
+ seq = generate_sequence(
51
+ self.params.num_layers,
52
+ self.params.sbox_count,
53
+ bytes(seq_key_material),
54
+ )
55
+ self._cached_tweak = tweak
56
+ self._cached_seq = seq
57
+ return seq
58
+
59
+ def _assert_alive(self) -> None:
60
+ if self._destroyed:
61
+ raise RuntimeError("FastCipher has been destroyed")
62
+
63
+ def _validate_input(self, data: bytes | list[int]) -> list[int]:
64
+ values = list(data)
65
+ if len(values) != self.params.word_length:
66
+ raise InvalidLengthError(
67
+ f"Expected {self.params.word_length} elements, got {len(values)}"
68
+ )
69
+ for v in values:
70
+ if not (0 <= v < self.params.radix):
71
+ raise InvalidValueError(
72
+ f"Value {v} out of range [0, {self.params.radix})"
73
+ )
74
+ return values
75
+
76
+ def encrypt(self, plaintext: bytes | list[int], tweak: bytes = b"") -> list[int]:
77
+ self._assert_alive()
78
+ values = self._validate_input(plaintext)
79
+ seq = self._ensure_sequence(tweak)
80
+ return cenc(self.params, self._sboxes, seq, values)
81
+
82
+ def decrypt(self, ciphertext: bytes | list[int], tweak: bytes = b"") -> list[int]:
83
+ self._assert_alive()
84
+ values = self._validate_input(ciphertext)
85
+ seq = self._ensure_sequence(tweak)
86
+ return cdec(self.params, self._sboxes, seq, values)
87
+
88
+ def encrypt_bytes(self, plaintext: bytes, tweak: bytes = b"") -> bytes:
89
+ return bytes(self.encrypt(plaintext, tweak))
90
+
91
+ def decrypt_bytes(self, ciphertext: bytes, tweak: bytes = b"") -> bytes:
92
+ return bytes(self.decrypt(ciphertext, tweak))
93
+
94
+ def destroy(self) -> None:
95
+ self._destroyed = True
96
+ self._master_key = b"\x00" * len(self._master_key)
97
+ self._sboxes = []
98
+ self._cached_seq = None
99
+ self._cached_tweak = None
100
+
101
+
102
+ def _validate_params(params: FastParams, key: bytes) -> None:
103
+ if params.radix < 4 or params.radix > 256:
104
+ raise InvalidRadixError("Radix must be between 4 and 256")
105
+
106
+ if params.word_length < 1:
107
+ raise InvalidWordLengthError("Word length must be >= 1")
108
+
109
+ if params.num_layers < 1:
110
+ raise InvalidWordLengthError("num_layers must be >= 1")
111
+
112
+ if params.word_length > 1 and params.num_layers % params.word_length != 0:
113
+ raise InvalidWordLengthError("num_layers must be a multiple of word_length")
114
+
115
+ if params.sbox_count < 1:
116
+ raise InvalidSBoxCountError("S-box count must be >= 1")
117
+
118
+ if params.branch_dist1 < 0:
119
+ raise InvalidBranchDistError("branch_dist1 must be >= 0")
120
+
121
+ if params.branch_dist2 < 0:
122
+ raise InvalidBranchDistError("branch_dist2 must be >= 0")
123
+
124
+ if params.word_length > 1:
125
+ if params.branch_dist1 > params.word_length - 2:
126
+ raise InvalidBranchDistError("branch_dist1 must be <= word_length - 2")
127
+ if params.branch_dist2 == 0 or params.branch_dist2 > params.word_length - 1:
128
+ raise InvalidBranchDistError("branch_dist2 is out of valid range")
129
+
130
+ if len(key) not in (16, 24, 32):
131
+ raise InvalidKeyError("Key must be 16, 24, or 32 bytes")
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from .layers import ds_layer, es_layer
4
+ from .sbox import SBox
5
+ from .types import FastParams
6
+
7
+
8
+ def cenc(
9
+ params: FastParams,
10
+ sboxes: list[SBox],
11
+ seq: list[int],
12
+ plaintext: list[int],
13
+ ) -> list[int]:
14
+ """Component encryption: apply all ES layers in forward order."""
15
+ data = list(plaintext)
16
+ if params.word_length == 1:
17
+ for layer in range(params.num_layers):
18
+ data[0] = sboxes[seq[layer]].perm[data[0]]
19
+ return data
20
+ for layer in range(params.num_layers):
21
+ es_layer(params, sboxes[seq[layer]], data)
22
+ return data
23
+
24
+
25
+ def cdec(
26
+ params: FastParams,
27
+ sboxes: list[SBox],
28
+ seq: list[int],
29
+ ciphertext: list[int],
30
+ ) -> list[int]:
31
+ """Component decryption: apply all DS layers in reverse order."""
32
+ data = list(ciphertext)
33
+ if params.word_length == 1:
34
+ for layer in range(params.num_layers - 1, -1, -1):
35
+ data[0] = sboxes[seq[layer]].inv[data[0]]
36
+ return data
37
+ for layer in range(params.num_layers - 1, -1, -1):
38
+ ds_layer(params, sboxes[seq[layer]], data)
39
+ return data
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ import struct
4
+
5
+ from .types import FastParams
6
+
7
+ _LABEL_INSTANCE1 = b"instance1"
8
+ _LABEL_INSTANCE2 = b"instance2"
9
+ _LABEL_FPE_POOL = b"FPE Pool"
10
+ _LABEL_FPE_SEQ = b"FPE SEQ"
11
+ _LABEL_TWEAK = b"tweak"
12
+
13
+
14
+ def _u32be(value: int) -> bytes:
15
+ return struct.pack(">I", value)
16
+
17
+
18
+ def encode_parts(parts: list[bytes]) -> bytes:
19
+ buf = bytearray(_u32be(len(parts)))
20
+ for part in parts:
21
+ buf.extend(_u32be(len(part)))
22
+ buf.extend(part)
23
+ return bytes(buf)
24
+
25
+
26
+ def build_setup1_input(params: FastParams) -> bytes:
27
+ return encode_parts(
28
+ [
29
+ _LABEL_INSTANCE1,
30
+ _u32be(params.radix),
31
+ _u32be(params.sbox_count),
32
+ _LABEL_FPE_POOL,
33
+ ]
34
+ )
35
+
36
+
37
+ def build_setup2_input(params: FastParams, tweak: bytes) -> bytes:
38
+ return encode_parts(
39
+ [
40
+ _LABEL_INSTANCE1,
41
+ _u32be(params.radix),
42
+ _u32be(params.sbox_count),
43
+ _LABEL_INSTANCE2,
44
+ _u32be(params.word_length),
45
+ _u32be(params.num_layers),
46
+ _u32be(params.branch_dist1),
47
+ _u32be(params.branch_dist2),
48
+ _LABEL_FPE_SEQ,
49
+ _LABEL_TWEAK,
50
+ tweak,
51
+ ]
52
+ )