cyphera 0.0.1a3__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.
cyphera/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from cyphera.ff1 import FF1
2
+ from cyphera.ff3 import FF3
3
+
4
+ __all__ = ["FF1", "FF3"]
5
+ __version__ = "0.1.0"
cyphera/ff1.py ADDED
@@ -0,0 +1,142 @@
1
+ """FF1 Format-Preserving Encryption (NIST SP 800-38G)."""
2
+
3
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
4
+ import math
5
+
6
+ DIGITS = "0123456789"
7
+ ALPHANUMERIC = "0123456789abcdefghijklmnopqrstuvwxyz"
8
+
9
+
10
+ class FF1:
11
+ def __init__(self, key: bytes, tweak: bytes, alphabet: str = ALPHANUMERIC):
12
+ if len(key) not in (16, 24, 32):
13
+ raise ValueError(f"Key must be 16, 24, or 32 bytes, got {len(key)}")
14
+ if len(alphabet) < 2:
15
+ raise ValueError("Alphabet must have >= 2 characters")
16
+ self._key = key
17
+ self._tweak = tweak
18
+ self._alphabet = alphabet
19
+ self._radix = len(alphabet)
20
+ self._char_to_int = {c: i for i, c in enumerate(alphabet)}
21
+
22
+ def encrypt(self, plaintext: str) -> str:
23
+ digits = self._to_digits(plaintext)
24
+ result = self._ff1_encrypt(digits, self._tweak)
25
+ return self._from_digits(result)
26
+
27
+ def decrypt(self, ciphertext: str) -> str:
28
+ digits = self._to_digits(ciphertext)
29
+ result = self._ff1_decrypt(digits, self._tweak)
30
+ return self._from_digits(result)
31
+
32
+ def _to_digits(self, s: str) -> list[int]:
33
+ return [self._char_to_int[c] for c in s]
34
+
35
+ def _from_digits(self, d: list[int]) -> str:
36
+ return "".join(self._alphabet[i] for i in d)
37
+
38
+ def _aes_ecb(self, block: bytes) -> bytes:
39
+ cipher = Cipher(algorithms.AES(self._key), modes.ECB())
40
+ enc = cipher.encryptor()
41
+ return enc.update(block) + enc.finalize()
42
+
43
+ def _prf(self, data: bytes) -> bytes:
44
+ y = b"\x00" * 16
45
+ for i in range(0, len(data), 16):
46
+ block = bytes(a ^ b for a, b in zip(y, data[i : i + 16]))
47
+ y = self._aes_ecb(block)
48
+ return y
49
+
50
+ def _expand_s(self, r: bytes, d: int) -> bytes:
51
+ blocks = (d + 15) // 16
52
+ out = bytearray(r)
53
+ for j in range(1, blocks):
54
+ x = j.to_bytes(16, "big")
55
+ # XOR with R (not previous block) per NIST SP 800-38G
56
+ x = bytes(a ^ b for a, b in zip(x, r))
57
+ enc = self._aes_ecb(x)
58
+ out.extend(enc)
59
+ return bytes(out[:d])
60
+
61
+ def _num(self, digits: list[int]) -> int:
62
+ result = 0
63
+ for d in digits:
64
+ result = result * self._radix + d
65
+ return result
66
+
67
+ def _str(self, num: int, length: int) -> list[int]:
68
+ result = [0] * length
69
+ for i in range(length - 1, -1, -1):
70
+ result[i] = num % self._radix
71
+ num //= self._radix
72
+ return result
73
+
74
+ def _compute_b(self, v: int) -> int:
75
+ return math.ceil(math.ceil(v * math.log2(self._radix)) / 8)
76
+
77
+ def _build_p(self, u: int, n: int, t: int) -> bytes:
78
+ return bytes(
79
+ [1, 2, 1, (self._radix >> 16) & 0xFF, (self._radix >> 8) & 0xFF, self._radix & 0xFF, 10, u]
80
+ + list(n.to_bytes(4, "big"))
81
+ + list(t.to_bytes(4, "big"))
82
+ )
83
+
84
+ def _build_q(self, T: bytes, i: int, num_bytes: bytes, b: int) -> bytes:
85
+ pad = (16 - ((len(T) + 1 + b) % 16)) % 16
86
+ q = bytearray(T)
87
+ q.extend(b"\x00" * pad)
88
+ q.append(i)
89
+ if len(num_bytes) < b:
90
+ q.extend(b"\x00" * (b - len(num_bytes)))
91
+ start = max(0, len(num_bytes) - b)
92
+ q.extend(num_bytes[start:])
93
+ return bytes(q)
94
+
95
+ def _ff1_encrypt(self, pt: list[int], T: bytes) -> list[int]:
96
+ n = len(pt)
97
+ u, v = n // 2, n - n // 2
98
+ A, B = pt[:u], pt[u:]
99
+
100
+ b = self._compute_b(v)
101
+ d = 4 * ((b + 3) // 4) + 4
102
+ P = self._build_p(u, n, len(T))
103
+
104
+ for i in range(10):
105
+ num_b = self._num(B).to_bytes(max(b, 1), "big")
106
+ if len(num_b) > b:
107
+ num_b = num_b[-b:] if b > 0 else b""
108
+ Q = self._build_q(T, i, num_b, b)
109
+ R = self._prf(P + Q)
110
+ S = self._expand_s(R, d)
111
+ y = int.from_bytes(S, "big")
112
+
113
+ m = u if i % 2 == 0 else v
114
+ c = (self._num(A) + y) % (self._radix ** m)
115
+ A, B = B, self._str(c, m)
116
+
117
+ return A + B
118
+
119
+ def _ff1_decrypt(self, ct: list[int], T: bytes) -> list[int]:
120
+ n = len(ct)
121
+ u, v = n // 2, n - n // 2
122
+ A, B = ct[:u], ct[u:]
123
+
124
+ b = self._compute_b(v)
125
+ d = 4 * ((b + 3) // 4) + 4
126
+ P = self._build_p(u, n, len(T))
127
+
128
+ for i in range(9, -1, -1):
129
+ num_a = self._num(A).to_bytes(max(b, 1), "big")
130
+ if len(num_a) > b:
131
+ num_a = num_a[-b:] if b > 0 else b""
132
+ Q = self._build_q(T, i, num_a, b)
133
+ R = self._prf(P + Q)
134
+ S = self._expand_s(R, d)
135
+ y = int.from_bytes(S, "big")
136
+
137
+ m = u if i % 2 == 0 else v
138
+ mod = self._radix ** m
139
+ c = (self._num(B) - y) % mod
140
+ B, A = A, self._str(c, m)
141
+
142
+ return A + B
cyphera/ff3.py ADDED
@@ -0,0 +1,127 @@
1
+ """FF3-1 Format-Preserving Encryption (NIST SP 800-38G Rev 1)."""
2
+
3
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
4
+
5
+ DIGITS = "0123456789"
6
+ ALPHANUMERIC = "0123456789abcdefghijklmnopqrstuvwxyz"
7
+
8
+
9
+ class FF3:
10
+ def __init__(self, key: bytes, tweak: bytes, alphabet: str = ALPHANUMERIC):
11
+ if len(key) not in (16, 24, 32):
12
+ raise ValueError(f"Key must be 16, 24, or 32 bytes, got {len(key)}")
13
+ if len(tweak) != 8:
14
+ raise ValueError(f"Tweak must be exactly 8 bytes, got {len(tweak)}")
15
+ if len(alphabet) < 2:
16
+ raise ValueError("Alphabet must have >= 2 characters")
17
+ # FF3 reverses the key
18
+ self._key = key[::-1]
19
+ self._tweak = tweak
20
+ self._alphabet = alphabet
21
+ self._radix = len(alphabet)
22
+ self._char_to_int = {c: i for i, c in enumerate(alphabet)}
23
+
24
+ def encrypt(self, plaintext: str) -> str:
25
+ digits = self._to_digits(plaintext)
26
+ result = self._ff3_encrypt(digits)
27
+ return self._from_digits(result)
28
+
29
+ def decrypt(self, ciphertext: str) -> str:
30
+ digits = self._to_digits(ciphertext)
31
+ result = self._ff3_decrypt(digits)
32
+ return self._from_digits(result)
33
+
34
+ def _to_digits(self, s: str) -> list[int]:
35
+ return [self._char_to_int[c] for c in s]
36
+
37
+ def _from_digits(self, d: list[int]) -> str:
38
+ return "".join(self._alphabet[i] for i in d)
39
+
40
+ def _aes_ecb(self, block: bytes) -> bytes:
41
+ cipher = Cipher(algorithms.AES(self._key), modes.ECB())
42
+ enc = cipher.encryptor()
43
+ return enc.update(block) + enc.finalize()
44
+
45
+ def _num(self, digits: list[int]) -> int:
46
+ result = 0
47
+ for d in digits:
48
+ result = result * self._radix + d
49
+ return result
50
+
51
+ def _str(self, num: int, length: int) -> list[int]:
52
+ result = [0] * length
53
+ for i in range(length - 1, -1, -1):
54
+ result[i] = num % self._radix
55
+ num //= self._radix
56
+ return result
57
+
58
+ def _calc_p(self, round_num: int, w: bytes, half: list[int]) -> int:
59
+ inp = bytearray(16)
60
+ inp[0:4] = w
61
+ inp[3] ^= round_num
62
+
63
+ rev_half = list(reversed(half))
64
+ half_num = self._num(rev_half)
65
+ half_bytes = half_num.to_bytes(max(1, (half_num.bit_length() + 7) // 8), "big") if half_num > 0 else b"\x00"
66
+
67
+ if len(half_bytes) <= 12:
68
+ inp[16 - len(half_bytes) : 16] = half_bytes
69
+ else:
70
+ inp[4:16] = half_bytes[-12:]
71
+
72
+ rev_inp = bytes(reversed(inp))
73
+ aes_out = self._aes_ecb(rev_inp)
74
+ rev_out = bytes(reversed(aes_out))
75
+ return int.from_bytes(rev_out, "big")
76
+
77
+ def _ff3_encrypt(self, pt: list[int]) -> list[int]:
78
+ n = len(pt)
79
+ u = (n + 1) // 2
80
+ v = n - u
81
+ A, B = pt[:u], pt[u:]
82
+
83
+ for i in range(8):
84
+ if i % 2 == 0:
85
+ w = self._tweak[4:8]
86
+ p = self._calc_p(i, w, B)
87
+ m = self._radix ** u
88
+ a_num = self._num(list(reversed(A)))
89
+ y = (a_num + p) % m
90
+ new = self._str(y, u)
91
+ A = list(reversed(new))
92
+ else:
93
+ w = self._tweak[0:4]
94
+ p = self._calc_p(i, w, A)
95
+ m = self._radix ** v
96
+ b_num = self._num(list(reversed(B)))
97
+ y = (b_num + p) % m
98
+ new = self._str(y, v)
99
+ B = list(reversed(new))
100
+
101
+ return A + B
102
+
103
+ def _ff3_decrypt(self, ct: list[int]) -> list[int]:
104
+ n = len(ct)
105
+ u = (n + 1) // 2
106
+ v = n - u
107
+ A, B = ct[:u], ct[u:]
108
+
109
+ for i in range(7, -1, -1):
110
+ if i % 2 == 0:
111
+ w = self._tweak[4:8]
112
+ p = self._calc_p(i, w, B)
113
+ m = self._radix ** u
114
+ a_num = self._num(list(reversed(A)))
115
+ y = (a_num - p) % m
116
+ new = self._str(y, u)
117
+ A = list(reversed(new))
118
+ else:
119
+ w = self._tweak[0:4]
120
+ p = self._calc_p(i, w, A)
121
+ m = self._radix ** v
122
+ b_num = self._num(list(reversed(B)))
123
+ y = (b_num - p) % m
124
+ new = self._str(y, v)
125
+ B = list(reversed(new))
126
+
127
+ return A + B
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: cyphera
3
+ Version: 0.0.1a3
4
+ Summary: Data protection SDK — format-preserving encryption (FF1/FF3), AES-GCM, data masking, and hashing.
5
+ Author-email: Leslie Gutschow <leslie.gutschow@horizondigital.dev>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://cyphera.io
8
+ Project-URL: Repository, https://github.com/cyphera-labs/cyphera-python
9
+ Project-URL: Issues, https://github.com/cyphera-labs/cyphera-python/issues
10
+ Keywords: encryption,fpe,format-preserving,data-protection,masking
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Security :: Cryptography
14
+ Requires-Python: >=3.9
15
+ Requires-Dist: cryptography>=41.0
@@ -0,0 +1,7 @@
1
+ cyphera/__init__.py,sha256=065ZzXa-Mbi1sDAhmbcfTv14ks4_X4syH-BMyknU2UI,104
2
+ cyphera/ff1.py,sha256=cKoZmyJBV6Gtm_AU1u_8ncYkuHz_pDHIjx66Ezq18UI,4807
3
+ cyphera/ff3.py,sha256=tPPnXoVyf8IEfSm1hBCSWb4f7p19VYLHCalxJJShgpA,4295
4
+ cyphera-0.0.1a3.dist-info/METADATA,sha256=sOi6qeydw9_02nOkrala6SxUsAt2BJLVDK_6qgO41-4,711
5
+ cyphera-0.0.1a3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ cyphera-0.0.1a3.dist-info/top_level.txt,sha256=U_YqVTaTo9hWD43o4-D7T5nWBaVMB0jB-6wD8I1OgO4,8
7
+ cyphera-0.0.1a3.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ cyphera