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 +5 -0
- cyphera/ff1.py +142 -0
- cyphera/ff3.py +127 -0
- cyphera-0.0.1a3.dist-info/METADATA +15 -0
- cyphera-0.0.1a3.dist-info/RECORD +7 -0
- cyphera-0.0.1a3.dist-info/WHEEL +5 -0
- cyphera-0.0.1a3.dist-info/top_level.txt +1 -0
cyphera/__init__.py
ADDED
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 @@
|
|
|
1
|
+
cyphera
|