cyphera 0.0.1a3__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.
- cyphera-0.0.1a3/PKG-INFO +15 -0
- cyphera-0.0.1a3/README.md +23 -0
- cyphera-0.0.1a3/cyphera/__init__.py +5 -0
- cyphera-0.0.1a3/cyphera/ff1.py +142 -0
- cyphera-0.0.1a3/cyphera/ff3.py +127 -0
- cyphera-0.0.1a3/cyphera.egg-info/PKG-INFO +15 -0
- cyphera-0.0.1a3/cyphera.egg-info/SOURCES.txt +12 -0
- cyphera-0.0.1a3/cyphera.egg-info/dependency_links.txt +1 -0
- cyphera-0.0.1a3/cyphera.egg-info/requires.txt +1 -0
- cyphera-0.0.1a3/cyphera.egg-info/top_level.txt +1 -0
- cyphera-0.0.1a3/pyproject.toml +28 -0
- cyphera-0.0.1a3/setup.cfg +4 -0
- cyphera-0.0.1a3/tests/test_ff1_nist.py +56 -0
- cyphera-0.0.1a3/tests/test_ff3_nist.py +25 -0
cyphera-0.0.1a3/PKG-INFO
ADDED
|
@@ -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,23 @@
|
|
|
1
|
+
# cyphera
|
|
2
|
+
|
|
3
|
+
Data obfuscation SDK for Python. FPE, AES, masking, hashing.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
pip install cyphera
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from cyphera import FF1
|
|
11
|
+
|
|
12
|
+
cipher = FF1(key, tweak)
|
|
13
|
+
encrypted = cipher.encrypt("0123456789")
|
|
14
|
+
decrypted = cipher.decrypt(encrypted)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Status
|
|
18
|
+
|
|
19
|
+
Early development. FF1 and FF3 engines with all NIST test vectors.
|
|
20
|
+
|
|
21
|
+
## License
|
|
22
|
+
|
|
23
|
+
Apache 2.0
|
|
@@ -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
|
|
@@ -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,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
cyphera/__init__.py
|
|
4
|
+
cyphera/ff1.py
|
|
5
|
+
cyphera/ff3.py
|
|
6
|
+
cyphera.egg-info/PKG-INFO
|
|
7
|
+
cyphera.egg-info/SOURCES.txt
|
|
8
|
+
cyphera.egg-info/dependency_links.txt
|
|
9
|
+
cyphera.egg-info/requires.txt
|
|
10
|
+
cyphera.egg-info/top_level.txt
|
|
11
|
+
tests/test_ff1_nist.py
|
|
12
|
+
tests/test_ff3_nist.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cryptography>=41.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cyphera
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cyphera"
|
|
3
|
+
version = "0.0.1a3"
|
|
4
|
+
description = "Data protection SDK — format-preserving encryption (FF1/FF3), AES-GCM, data masking, and hashing."
|
|
5
|
+
license = "Apache-2.0"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
dependencies = ["cryptography>=41.0"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Leslie Gutschow", email = "leslie.gutschow@horizondigital.dev" },
|
|
10
|
+
]
|
|
11
|
+
keywords = ["encryption", "fpe", "format-preserving", "data-protection", "masking"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Topic :: Security :: Cryptography",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://cyphera.io"
|
|
20
|
+
Repository = "https://github.com/cyphera-labs/cyphera-python"
|
|
21
|
+
Issues = "https://github.com/cyphera-labs/cyphera-python/issues"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["setuptools>=68"]
|
|
25
|
+
build-backend = "setuptools.build_meta"
|
|
26
|
+
|
|
27
|
+
[tool.pytest.ini_options]
|
|
28
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""FF1 NIST SP 800-38G test vectors."""
|
|
2
|
+
from cyphera.ff1 import FF1, DIGITS, ALPHANUMERIC
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_sample_1():
|
|
6
|
+
c = FF1(bytes.fromhex("2B7E151628AED2A6ABF7158809CF4F3C"), b"", DIGITS)
|
|
7
|
+
assert c.encrypt("0123456789") == "2433477484"
|
|
8
|
+
assert c.decrypt("2433477484") == "0123456789"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_sample_2():
|
|
12
|
+
c = FF1(bytes.fromhex("2B7E151628AED2A6ABF7158809CF4F3C"), bytes.fromhex("39383736353433323130"), DIGITS)
|
|
13
|
+
assert c.encrypt("0123456789") == "6124200773"
|
|
14
|
+
assert c.decrypt("6124200773") == "0123456789"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_sample_3():
|
|
18
|
+
c = FF1(bytes.fromhex("2B7E151628AED2A6ABF7158809CF4F3C"), bytes.fromhex("3737373770717273373737"), ALPHANUMERIC)
|
|
19
|
+
assert c.encrypt("0123456789abcdefghi") == "a9tv40mll9kdu509eum"
|
|
20
|
+
assert c.decrypt("a9tv40mll9kdu509eum") == "0123456789abcdefghi"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_sample_4():
|
|
24
|
+
c = FF1(bytes.fromhex("2B7E151628AED2A6ABF7158809CF4F3CEF4359D8D580AA4F"), b"", DIGITS)
|
|
25
|
+
assert c.encrypt("0123456789") == "2830668132"
|
|
26
|
+
assert c.decrypt("2830668132") == "0123456789"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_sample_5():
|
|
30
|
+
c = FF1(bytes.fromhex("2B7E151628AED2A6ABF7158809CF4F3CEF4359D8D580AA4F"), bytes.fromhex("39383736353433323130"), DIGITS)
|
|
31
|
+
assert c.encrypt("0123456789") == "2496655549"
|
|
32
|
+
assert c.decrypt("2496655549") == "0123456789"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_sample_6():
|
|
36
|
+
c = FF1(bytes.fromhex("2B7E151628AED2A6ABF7158809CF4F3CEF4359D8D580AA4F"), bytes.fromhex("3737373770717273373737"), ALPHANUMERIC)
|
|
37
|
+
assert c.encrypt("0123456789abcdefghi") == "xbj3kv35jrawxv32ysr"
|
|
38
|
+
assert c.decrypt("xbj3kv35jrawxv32ysr") == "0123456789abcdefghi"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_sample_7():
|
|
42
|
+
c = FF1(bytes.fromhex("2B7E151628AED2A6ABF7158809CF4F3CEF4359D8D580AA4F7F036D6F04FC6A94"), b"", DIGITS)
|
|
43
|
+
assert c.encrypt("0123456789") == "6657667009"
|
|
44
|
+
assert c.decrypt("6657667009") == "0123456789"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_sample_8():
|
|
48
|
+
c = FF1(bytes.fromhex("2B7E151628AED2A6ABF7158809CF4F3CEF4359D8D580AA4F7F036D6F04FC6A94"), bytes.fromhex("39383736353433323130"), DIGITS)
|
|
49
|
+
assert c.encrypt("0123456789") == "1001623463"
|
|
50
|
+
assert c.decrypt("1001623463") == "0123456789"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_sample_9():
|
|
54
|
+
c = FF1(bytes.fromhex("2B7E151628AED2A6ABF7158809CF4F3CEF4359D8D580AA4F7F036D6F04FC6A94"), bytes.fromhex("3737373770717273373737"), ALPHANUMERIC)
|
|
55
|
+
assert c.encrypt("0123456789abcdefghi") == "xs8a0azh2avyalyzuwd"
|
|
56
|
+
assert c.decrypt("xs8a0azh2avyalyzuwd") == "0123456789abcdefghi"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""FF3 NIST SP 800-38G test vectors (all 15)."""
|
|
2
|
+
from cyphera.ff3 import FF3, DIGITS
|
|
3
|
+
|
|
4
|
+
R26 = "0123456789abcdefghijklmnop"
|
|
5
|
+
|
|
6
|
+
def _t(key_hex, tweak_hex, alphabet, pt, ct):
|
|
7
|
+
c = FF3(bytes.fromhex(key_hex), bytes.fromhex(tweak_hex), alphabet)
|
|
8
|
+
assert c.encrypt(pt) == ct, f"encrypt({pt}) != {ct}"
|
|
9
|
+
assert c.decrypt(ct) == pt, f"decrypt({ct}) != {pt}"
|
|
10
|
+
|
|
11
|
+
def test_s01(): _t("EF4359D8D580AA4F7F036D6F04FC6A94", "D8E7920AFA330A73", DIGITS, "890121234567890000", "750918814058654607")
|
|
12
|
+
def test_s02(): _t("EF4359D8D580AA4F7F036D6F04FC6A94", "9A768A92F60E12D8", DIGITS, "890121234567890000", "018989839189395384")
|
|
13
|
+
def test_s03(): _t("EF4359D8D580AA4F7F036D6F04FC6A94", "D8E7920AFA330A73", DIGITS, "89012123456789000000789000000", "48598367162252569629397416226")
|
|
14
|
+
def test_s04(): _t("EF4359D8D580AA4F7F036D6F04FC6A94", "0000000000000000", DIGITS, "89012123456789000000789000000", "34695224821734535122613701434")
|
|
15
|
+
def test_s05(): _t("EF4359D8D580AA4F7F036D6F04FC6A94", "9A768A92F60E12D8", R26, "0123456789abcdefghi", "g2pk40i992fn20cjakb")
|
|
16
|
+
def test_s06(): _t("EF4359D8D580AA4F7F036D6F04FC6A942B7E151628AED2A6", "D8E7920AFA330A73", DIGITS, "890121234567890000", "646965393875028755")
|
|
17
|
+
def test_s07(): _t("EF4359D8D580AA4F7F036D6F04FC6A942B7E151628AED2A6", "9A768A92F60E12D8", DIGITS, "890121234567890000", "961610514491424446")
|
|
18
|
+
def test_s08(): _t("EF4359D8D580AA4F7F036D6F04FC6A942B7E151628AED2A6", "D8E7920AFA330A73", DIGITS, "89012123456789000000789000000", "53048884065350204541786380807")
|
|
19
|
+
def test_s09(): _t("EF4359D8D580AA4F7F036D6F04FC6A942B7E151628AED2A6", "0000000000000000", DIGITS, "89012123456789000000789000000", "98083802678820389295041483512")
|
|
20
|
+
def test_s10(): _t("EF4359D8D580AA4F7F036D6F04FC6A942B7E151628AED2A6", "9A768A92F60E12D8", R26, "0123456789abcdefghi", "i0ihe2jfj7a9opf9p88")
|
|
21
|
+
def test_s11(): _t("EF4359D8D580AA4F7F036D6F04FC6A942B7E151628AED2A6ABF7158809CF4F3C", "D8E7920AFA330A73", DIGITS, "890121234567890000", "922011205562777495")
|
|
22
|
+
def test_s12(): _t("EF4359D8D580AA4F7F036D6F04FC6A942B7E151628AED2A6ABF7158809CF4F3C", "9A768A92F60E12D8", DIGITS, "890121234567890000", "504149865578056140")
|
|
23
|
+
def test_s13(): _t("EF4359D8D580AA4F7F036D6F04FC6A942B7E151628AED2A6ABF7158809CF4F3C", "D8E7920AFA330A73", DIGITS, "89012123456789000000789000000", "04344343235792599165734622699")
|
|
24
|
+
def test_s14(): _t("EF4359D8D580AA4F7F036D6F04FC6A942B7E151628AED2A6ABF7158809CF4F3C", "0000000000000000", DIGITS, "89012123456789000000789000000", "30859239999374053872365555822")
|
|
25
|
+
def test_s15(): _t("EF4359D8D580AA4F7F036D6F04FC6A942B7E151628AED2A6ABF7158809CF4F3C", "9A768A92F60E12D8", R26, "0123456789abcdefghi", "p0b2godfja9bhb7bk38")
|