feistel-py 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.
- feistel-py-0.1.0/LICENSE +21 -0
- feistel-py-0.1.0/MANIFEST.in +5 -0
- feistel-py-0.1.0/PKG-INFO +182 -0
- feistel-py-0.1.0/README.md +144 -0
- feistel-py-0.1.0/pyproject.toml +36 -0
- feistel-py-0.1.0/setup.cfg +4 -0
- feistel-py-0.1.0/src/feistel/__init__.py +3 -0
- feistel-py-0.1.0/src/feistel/__main__.py +76 -0
- feistel-py-0.1.0/src/feistel/cipher.py +63 -0
- feistel-py-0.1.0/src/feistel/custom.py +61 -0
- feistel-py-0.1.0/src/feistel/fpe.py +168 -0
- feistel-py-0.1.0/src/feistel/utils/base256.py +27 -0
- feistel-py-0.1.0/src/feistel/utils/bytearray.py +44 -0
- feistel-py-0.1.0/src/feistel/utils/hash.py +42 -0
- feistel-py-0.1.0/src/feistel/utils/padding.py +14 -0
- feistel-py-0.1.0/src/feistel/utils/strings.py +35 -0
- feistel-py-0.1.0/src/feistel/utils/xor.py +21 -0
- feistel-py-0.1.0/src/feistel_py.egg-info/PKG-INFO +182 -0
- feistel-py-0.1.0/src/feistel_py.egg-info/SOURCES.txt +25 -0
- feistel-py-0.1.0/src/feistel_py.egg-info/dependency_links.txt +1 -0
- feistel-py-0.1.0/src/feistel_py.egg-info/entry_points.txt +2 -0
- feistel-py-0.1.0/src/feistel_py.egg-info/requires.txt +2 -0
- feistel-py-0.1.0/src/feistel_py.egg-info/top_level.txt +1 -0
- feistel-py-0.1.0/tests/test_cipher.py +26 -0
- feistel-py-0.1.0/tests/test_custom.py +48 -0
- feistel-py-0.1.0/tests/test_fpe.py +75 -0
- feistel-py-0.1.0/tests/test_utils.py +101 -0
feistel-py-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Cyril Dever
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: feistel-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Feistel cipher implementation in Python for format-preserving encryption
|
|
5
|
+
Author-email: Cyril Dever <cdever@pep-s.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 Cyril Dever
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/cyrildever/feistel-py
|
|
29
|
+
Keywords: feistel,cipher,format-preserving,encryption,fpe
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Operating System :: OS Independent
|
|
33
|
+
Requires-Python: >=3.10.2
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Requires-Dist: pycryptodome>=3.20.0
|
|
37
|
+
Requires-Dist: py-utls>=0.1.6
|
|
38
|
+
|
|
39
|
+
# feistel-py
|
|
40
|
+
_Feistel cipher implementation in Python for format-preserving encryption_
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+

|
|
45
|
+

|
|
46
|
+

|
|
47
|
+
|
|
48
|
+
This is a Python library implementing the Feistel cipher for Format-Preserving Encryption (FPE).
|
|
49
|
+
|
|
50
|
+
### Motivation
|
|
51
|
+
|
|
52
|
+
The main objective of this library is not to provide a secure encryption scheme but rather a safe obfuscation tool.
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
### Formal description
|
|
56
|
+
|
|
57
|
+
This library operates on the concept of the Feistel cipher described in [Wikipedia](https://en.wikipedia.org/wiki/Feistel_cipher) as:
|
|
58
|
+
> A Feistel network is subdivided into several rounds or steps. In its balanced version, the network processes the data in two parts of identical size. On each round, the two blocks are exchanged, then one of the blocks is combined with a transformed version of the other block.
|
|
59
|
+
> Half of the data is encoded with the key, then the result of this operation is added using an XOR operation to the other half of the data.
|
|
60
|
+
> Then in the next round, we reverse: it is the turn of the last half to be encrypted and then to be xored to the first half, except that we use the data previously encrypted.
|
|
61
|
+
> The diagram below shows the data flow (the  represents the XOR operation). Each round uses an intermediate key, usually taken from the main key via a generation called key schedule. The operations performed during encryption with these intermediate keys are specific to each algorithm.
|
|
62
|
+
|
|
63
|
+

|
|
64
|
+
|
|
65
|
+
The algorithmic description (provided by Wikipedia) of the encryption is as follows:
|
|
66
|
+
* Let  be the number of steps,  the keys associated with each step and  a function of the ) space to the  space.
|
|
67
|
+
* For each step ![$i{\in}[0;n]$](https://render.githubusercontent.com/render/math?math=i\in[0%3Bn]), note the encrypted word in step :
|
|
68
|
+
* 
|
|
69
|
+
* )
|
|
70
|
+
*  is the unciphered text,  is the ciphered word.
|
|
71
|
+
|
|
72
|
+
There is no restriction on the  function other than the XOR operation must be possible. For simplicity, we will choose  of the same size as  and the function  shall transform a word of length  into a word of length  (and this for all ).
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
### Usage
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
pip install feistel-py
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
To get an obfuscated string from a source data using the SHA-256 hashing function at each round, first instantiate a `Cipher` object, passing it a key and a number of rounds. Then, use the `encrypt()` method with the source data as argument. The result will be a byte array. To ensure maximum security, I recommend you use a 256-bit key or longer and a minimum of 10 rounds.
|
|
82
|
+
|
|
83
|
+
The decryption process uses the obfuscated buffered data and pass it to the `decrypt()` method of the `Cipher`.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from feistel.cipher import Cipher
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
source = "my-source-data"
|
|
90
|
+
|
|
91
|
+
# Encrypt
|
|
92
|
+
cipher = Cipher("some-32-byte-long-key-to-be-safe", 10)
|
|
93
|
+
obfuscated = cipher.encrypt(source)
|
|
94
|
+
|
|
95
|
+
# Decrypt
|
|
96
|
+
deciphered = cipher.decrypt(obfuscated)
|
|
97
|
+
|
|
98
|
+
assert deciphered == source, "deciphered should be 'my-source-data'"
|
|
99
|
+
```
|
|
100
|
+
_NB: This is the exact replica of my other implementations (see below)._
|
|
101
|
+
|
|
102
|
+
You may also use your own set of keys through a `CustomCipher` instance, eg.
|
|
103
|
+
```python
|
|
104
|
+
from feistel.custom import CustomCipher
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
keys = [
|
|
108
|
+
"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
|
109
|
+
"9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba",
|
|
110
|
+
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
|
|
111
|
+
]
|
|
112
|
+
cipher = CustomCipher(keys)
|
|
113
|
+
```
|
|
114
|
+
In that case, the number of rounds depends on the number of provided keys.
|
|
115
|
+
|
|
116
|
+
Finally, you might want to use the latest cipher, providing true format-preserving encryption for strings:
|
|
117
|
+
```python
|
|
118
|
+
from feistel.fpe import FPECipher
|
|
119
|
+
from feistel.utils.hash import SHA_256
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
cipher = FPECipher(SHA_256, "some-32-byte-long-key-to-be-safe", 128)
|
|
123
|
+
obfuscated = cipher.encrypt(source)
|
|
124
|
+
|
|
125
|
+
assert len(obfuscated) == len(source)
|
|
126
|
+
```
|
|
127
|
+
_NB: For stability and security purposes, the number `0` always returns itself._
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
You might also want to use it with the command line:
|
|
131
|
+
```
|
|
132
|
+
usage: python3 -m feistel [-h] [-c CIPHER] [-e ENGINE] [-k KEY] [-r ROUNDS] [-o OPERATION] input
|
|
133
|
+
|
|
134
|
+
positional arguments:
|
|
135
|
+
input The string to obfuscate (watch for quotes)
|
|
136
|
+
|
|
137
|
+
options:
|
|
138
|
+
-h, --help show this help message and exit
|
|
139
|
+
-c CIPHER, --cipher CIPHER
|
|
140
|
+
The type of cipher: feistel [default] | custom | fpe
|
|
141
|
+
-e ENGINE, --engine ENGINE
|
|
142
|
+
The hashing engine [default sha-256]
|
|
143
|
+
-k KEY, --key KEY The key(s) to use
|
|
144
|
+
-r ROUNDS, --rounds ROUNDS
|
|
145
|
+
The (optional) number of rounds [default 10]
|
|
146
|
+
-o OPERATION, --operation OPERATION
|
|
147
|
+
The operation to process : cipher | decipher
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
### Dependencies
|
|
152
|
+
|
|
153
|
+
The following libraries are necessary:
|
|
154
|
+
- `pycryptodome`;
|
|
155
|
+
- `py-utls`.
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
### Tests
|
|
159
|
+
|
|
160
|
+
```console
|
|
161
|
+
$ python3 -m unittest discover
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
### Other implementations
|
|
166
|
+
|
|
167
|
+
For those interested, I also made two other implementations of these ciphers:
|
|
168
|
+
* In [Golang](https://github.com/cyrildever/feistel) as an executable;
|
|
169
|
+
* In [Typescript](https://github.com/cyrildever/feistel-cipher) for the browser;
|
|
170
|
+
* In [Scala](https://github.com/cyrildever/feistel-jar) for the JVM.
|
|
171
|
+
|
|
172
|
+
I also created a special library for redacting classified documents using the new FPE cipher. Feel free to [contact me](mailto:cdever@edgewhere.fr) about it.
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
### License
|
|
176
|
+
|
|
177
|
+
This module is distributed under a MIT license. \
|
|
178
|
+
See the [LICENSE](LICENSE) file.
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
<hr />
|
|
182
|
+
© 2024 Cyril Dever. All rights reserved.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# feistel-py
|
|
2
|
+
_Feistel cipher implementation in Python for format-preserving encryption_
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
This is a Python library implementing the Feistel cipher for Format-Preserving Encryption (FPE).
|
|
11
|
+
|
|
12
|
+
### Motivation
|
|
13
|
+
|
|
14
|
+
The main objective of this library is not to provide a secure encryption scheme but rather a safe obfuscation tool.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Formal description
|
|
18
|
+
|
|
19
|
+
This library operates on the concept of the Feistel cipher described in [Wikipedia](https://en.wikipedia.org/wiki/Feistel_cipher) as:
|
|
20
|
+
> A Feistel network is subdivided into several rounds or steps. In its balanced version, the network processes the data in two parts of identical size. On each round, the two blocks are exchanged, then one of the blocks is combined with a transformed version of the other block.
|
|
21
|
+
> Half of the data is encoded with the key, then the result of this operation is added using an XOR operation to the other half of the data.
|
|
22
|
+
> Then in the next round, we reverse: it is the turn of the last half to be encrypted and then to be xored to the first half, except that we use the data previously encrypted.
|
|
23
|
+
> The diagram below shows the data flow (the  represents the XOR operation). Each round uses an intermediate key, usually taken from the main key via a generation called key schedule. The operations performed during encryption with these intermediate keys are specific to each algorithm.
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
The algorithmic description (provided by Wikipedia) of the encryption is as follows:
|
|
28
|
+
* Let  be the number of steps,  the keys associated with each step and  a function of the ) space to the  space.
|
|
29
|
+
* For each step ![$i{\in}[0;n]$](https://render.githubusercontent.com/render/math?math=i\in[0%3Bn]), note the encrypted word in step :
|
|
30
|
+
* 
|
|
31
|
+
* )
|
|
32
|
+
*  is the unciphered text,  is the ciphered word.
|
|
33
|
+
|
|
34
|
+
There is no restriction on the  function other than the XOR operation must be possible. For simplicity, we will choose  of the same size as  and the function  shall transform a word of length  into a word of length  (and this for all ).
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
### Usage
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
pip install feistel-py
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
To get an obfuscated string from a source data using the SHA-256 hashing function at each round, first instantiate a `Cipher` object, passing it a key and a number of rounds. Then, use the `encrypt()` method with the source data as argument. The result will be a byte array. To ensure maximum security, I recommend you use a 256-bit key or longer and a minimum of 10 rounds.
|
|
44
|
+
|
|
45
|
+
The decryption process uses the obfuscated buffered data and pass it to the `decrypt()` method of the `Cipher`.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from feistel.cipher import Cipher
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
source = "my-source-data"
|
|
52
|
+
|
|
53
|
+
# Encrypt
|
|
54
|
+
cipher = Cipher("some-32-byte-long-key-to-be-safe", 10)
|
|
55
|
+
obfuscated = cipher.encrypt(source)
|
|
56
|
+
|
|
57
|
+
# Decrypt
|
|
58
|
+
deciphered = cipher.decrypt(obfuscated)
|
|
59
|
+
|
|
60
|
+
assert deciphered == source, "deciphered should be 'my-source-data'"
|
|
61
|
+
```
|
|
62
|
+
_NB: This is the exact replica of my other implementations (see below)._
|
|
63
|
+
|
|
64
|
+
You may also use your own set of keys through a `CustomCipher` instance, eg.
|
|
65
|
+
```python
|
|
66
|
+
from feistel.custom import CustomCipher
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
keys = [
|
|
70
|
+
"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
|
71
|
+
"9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba",
|
|
72
|
+
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
|
|
73
|
+
]
|
|
74
|
+
cipher = CustomCipher(keys)
|
|
75
|
+
```
|
|
76
|
+
In that case, the number of rounds depends on the number of provided keys.
|
|
77
|
+
|
|
78
|
+
Finally, you might want to use the latest cipher, providing true format-preserving encryption for strings:
|
|
79
|
+
```python
|
|
80
|
+
from feistel.fpe import FPECipher
|
|
81
|
+
from feistel.utils.hash import SHA_256
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
cipher = FPECipher(SHA_256, "some-32-byte-long-key-to-be-safe", 128)
|
|
85
|
+
obfuscated = cipher.encrypt(source)
|
|
86
|
+
|
|
87
|
+
assert len(obfuscated) == len(source)
|
|
88
|
+
```
|
|
89
|
+
_NB: For stability and security purposes, the number `0` always returns itself._
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
You might also want to use it with the command line:
|
|
93
|
+
```
|
|
94
|
+
usage: python3 -m feistel [-h] [-c CIPHER] [-e ENGINE] [-k KEY] [-r ROUNDS] [-o OPERATION] input
|
|
95
|
+
|
|
96
|
+
positional arguments:
|
|
97
|
+
input The string to obfuscate (watch for quotes)
|
|
98
|
+
|
|
99
|
+
options:
|
|
100
|
+
-h, --help show this help message and exit
|
|
101
|
+
-c CIPHER, --cipher CIPHER
|
|
102
|
+
The type of cipher: feistel [default] | custom | fpe
|
|
103
|
+
-e ENGINE, --engine ENGINE
|
|
104
|
+
The hashing engine [default sha-256]
|
|
105
|
+
-k KEY, --key KEY The key(s) to use
|
|
106
|
+
-r ROUNDS, --rounds ROUNDS
|
|
107
|
+
The (optional) number of rounds [default 10]
|
|
108
|
+
-o OPERATION, --operation OPERATION
|
|
109
|
+
The operation to process : cipher | decipher
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
### Dependencies
|
|
114
|
+
|
|
115
|
+
The following libraries are necessary:
|
|
116
|
+
- `pycryptodome`;
|
|
117
|
+
- `py-utls`.
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
### Tests
|
|
121
|
+
|
|
122
|
+
```console
|
|
123
|
+
$ python3 -m unittest discover
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
### Other implementations
|
|
128
|
+
|
|
129
|
+
For those interested, I also made two other implementations of these ciphers:
|
|
130
|
+
* In [Golang](https://github.com/cyrildever/feistel) as an executable;
|
|
131
|
+
* In [Typescript](https://github.com/cyrildever/feistel-cipher) for the browser;
|
|
132
|
+
* In [Scala](https://github.com/cyrildever/feistel-jar) for the JVM.
|
|
133
|
+
|
|
134
|
+
I also created a special library for redacting classified documents using the new FPE cipher. Feel free to [contact me](mailto:cdever@edgewhere.fr) about it.
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
### License
|
|
138
|
+
|
|
139
|
+
This module is distributed under a MIT license. \
|
|
140
|
+
See the [LICENSE](LICENSE) file.
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
<hr />
|
|
144
|
+
© 2024 Cyril Dever. All rights reserved.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# pyproject.toml
|
|
2
|
+
|
|
3
|
+
[build-system]
|
|
4
|
+
requires = ["setuptools>=58.1.0", "wheel"]
|
|
5
|
+
build-backend = "setuptools.build_meta"
|
|
6
|
+
|
|
7
|
+
[project]
|
|
8
|
+
name = "feistel-py"
|
|
9
|
+
version = "0.1.0"
|
|
10
|
+
description = "Feistel cipher implementation in Python for format-preserving encryption"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
authors = [{ name = "Cyril Dever", email = "cdever@pep-s.com" }]
|
|
13
|
+
license = { file = "LICENSE" }
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
keywords = [
|
|
20
|
+
"feistel",
|
|
21
|
+
"cipher",
|
|
22
|
+
"format-preserving",
|
|
23
|
+
"encryption",
|
|
24
|
+
"fpe",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"pycryptodome >= 3.20.0",
|
|
28
|
+
"py-utls >= 0.1.6",
|
|
29
|
+
]
|
|
30
|
+
requires-python = ">=3.10.2"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/cyrildever/feistel-py"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
feistel-py = "feistel.__main__:main"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import ast
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from feistel.cipher import Cipher
|
|
6
|
+
from feistel.custom import CustomCipher
|
|
7
|
+
from feistel.fpe import Engine, FPECipher
|
|
8
|
+
from feistel.utils.hash import SHA_256, is_available_engine
|
|
9
|
+
|
|
10
|
+
CUSTOM = "custom"
|
|
11
|
+
FEISTEL = "feistel"
|
|
12
|
+
FPE = "fpe"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main(args):
|
|
16
|
+
if not args.input or not args.operation:
|
|
17
|
+
raise Exception("Missing mandatory parameters")
|
|
18
|
+
data = str(args.input)
|
|
19
|
+
operation = str(args.operation)
|
|
20
|
+
if operation != "cipher" and operation != "decipher":
|
|
21
|
+
raise Exception("Invalid operation")
|
|
22
|
+
|
|
23
|
+
cipher_type = (
|
|
24
|
+
args.cipher
|
|
25
|
+
if args.cipher and args.cipher in [FEISTEL, CUSTOM, FPE]
|
|
26
|
+
else FEISTEL
|
|
27
|
+
)
|
|
28
|
+
if cipher_type == FEISTEL:
|
|
29
|
+
if operation == "decipher" and data.startswith("b'") and data.endswith("'"):
|
|
30
|
+
data = ast.literal_eval(args.input)
|
|
31
|
+
key = str(args.key)
|
|
32
|
+
if not key:
|
|
33
|
+
raise Exception("missing mandatory key")
|
|
34
|
+
rounds = int(args.rounds) if args.rounds else 10
|
|
35
|
+
cipher = Cipher(key, rounds)
|
|
36
|
+
elif cipher_type == CUSTOM:
|
|
37
|
+
if operation == "decipher" and data.startswith("b'") and data.endswith("'"):
|
|
38
|
+
data = ast.literal_eval(args.input)
|
|
39
|
+
keys = str(args.key).split(",")
|
|
40
|
+
if len(keys) == 0:
|
|
41
|
+
raise Exception("missing mandatory keys")
|
|
42
|
+
cipher = CustomCipher(keys)
|
|
43
|
+
else:
|
|
44
|
+
key = str(args.key)
|
|
45
|
+
if not key:
|
|
46
|
+
raise Exception("missing mandatory key")
|
|
47
|
+
if not is_available_engine(args.engine):
|
|
48
|
+
engine = SHA_256
|
|
49
|
+
else:
|
|
50
|
+
engine = Engine(args.engine)
|
|
51
|
+
rounds = int(args.rounds) if args.rounds else 10
|
|
52
|
+
cipher = FPECipher(engine, key, rounds)
|
|
53
|
+
|
|
54
|
+
if operation == "cipher":
|
|
55
|
+
print(cipher.encrypt(data))
|
|
56
|
+
else:
|
|
57
|
+
print(cipher.decrypt(data))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
parser = argparse.ArgumentParser()
|
|
62
|
+
parser.add_argument("input", help="The string to obfuscate (watch for quotes)")
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"-c", "--cipher", help="The type of cipher: feistel [default] | custom | fpe"
|
|
65
|
+
)
|
|
66
|
+
parser.add_argument("-e", "--engine", help="The hashing engine [default sha-256]")
|
|
67
|
+
parser.add_argument("-k", "--key", help="The key(s) to use")
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"-r", "--rounds", help="The (optional) number of rounds [default 10]"
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"-o", "--operation", help="The operation to process : cipher | decipher"
|
|
73
|
+
)
|
|
74
|
+
args = parser.parse_args()
|
|
75
|
+
|
|
76
|
+
main(args)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from feistel.utils.hash import hash
|
|
2
|
+
from feistel.utils.padding import pad, unpad
|
|
3
|
+
from feistel.utils.strings import add, extract, split, string2bytearray
|
|
4
|
+
from feistel.utils.xor import xor
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Cipher:
|
|
8
|
+
def __init__(self, key: str, rounds: int):
|
|
9
|
+
"""
|
|
10
|
+
The Cipher class is the main entry point to the Feistel cipher if you want to use the SHA-256 hash function at each round.
|
|
11
|
+
You should instantiate it with the base key you want to use and the number of rounds to apply.
|
|
12
|
+
For better security, you should choose a 256-bit key or longer, and 10 rounds is a good start.
|
|
13
|
+
Once instantiated, use the encrypt() or decrypt() methods on the Cipher instance with the appropriate data.
|
|
14
|
+
"""
|
|
15
|
+
assert key and rounds >= 2, "CipherError: wrong arguments"
|
|
16
|
+
self.key = key
|
|
17
|
+
self.rounds = rounds
|
|
18
|
+
|
|
19
|
+
def encrypt(self, data: str) -> bytearray:
|
|
20
|
+
"""
|
|
21
|
+
Obfuscate the passed data
|
|
22
|
+
"""
|
|
23
|
+
if len(data) == 0:
|
|
24
|
+
return bytearray()
|
|
25
|
+
|
|
26
|
+
if len(data) % 2 == 1:
|
|
27
|
+
data = pad(data)
|
|
28
|
+
|
|
29
|
+
# Apply the balanced Feistel cipher
|
|
30
|
+
left, right = split(data)
|
|
31
|
+
if len(left) != len(right):
|
|
32
|
+
raise Exception("invalid string: unable to split")
|
|
33
|
+
|
|
34
|
+
parts = [left, right]
|
|
35
|
+
for i in range(0, self.rounds):
|
|
36
|
+
tmp = xor(parts[0], self._round(parts[1], i))
|
|
37
|
+
parts = [parts[1], tmp]
|
|
38
|
+
|
|
39
|
+
return string2bytearray(parts[0] + parts[1])
|
|
40
|
+
|
|
41
|
+
def decrypt(self, obfuscated: bytes | bytearray) -> str:
|
|
42
|
+
"""
|
|
43
|
+
Deobfuscate the passed data
|
|
44
|
+
"""
|
|
45
|
+
assert len(obfuscated) % 2 == 0, "CipherError: invalid obfuscated data"
|
|
46
|
+
if len(obfuscated) == 0:
|
|
47
|
+
return ""
|
|
48
|
+
|
|
49
|
+
o = obfuscated.decode()
|
|
50
|
+
|
|
51
|
+
# Apply the balanced Feistel cipher
|
|
52
|
+
b, a = split(o)
|
|
53
|
+
for i in range(0, self.rounds):
|
|
54
|
+
tmp = xor(a, self._round(b, self.rounds - i - 1))
|
|
55
|
+
a = b
|
|
56
|
+
b = tmp
|
|
57
|
+
|
|
58
|
+
return unpad(b + a)
|
|
59
|
+
|
|
60
|
+
def _round(self, item: str, idx: int) -> str:
|
|
61
|
+
addition = add(item, extract(self.key, idx, len(item)))
|
|
62
|
+
hex_hashed = hash(string2bytearray(addition)).hex()
|
|
63
|
+
return extract(hex_hashed, idx, len(item))
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from feistel.utils.hash import hash
|
|
2
|
+
from feistel.utils.padding import pad, unpad
|
|
3
|
+
from feistel.utils.strings import add, extract, split, string2bytearray
|
|
4
|
+
from feistel.utils.xor import xor
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CustomCipher:
|
|
8
|
+
def __init__(self, keys: list[str]):
|
|
9
|
+
"""
|
|
10
|
+
The CustomCipher uses custom keys instead of the SHA-256 hashing function to provide a new key at each round.
|
|
11
|
+
The number of rounds is then determined by the number of keys provided.
|
|
12
|
+
NB: There must be at least two keys.
|
|
13
|
+
"""
|
|
14
|
+
assert len(keys) >= 2, "CustomCipherError: wrong arguments"
|
|
15
|
+
self.keys = keys
|
|
16
|
+
|
|
17
|
+
def encrypt(self, data: str) -> bytearray:
|
|
18
|
+
"""
|
|
19
|
+
Obfuscate the passed data
|
|
20
|
+
"""
|
|
21
|
+
if len(data) == 0:
|
|
22
|
+
return bytearray()
|
|
23
|
+
|
|
24
|
+
if len(data) % 2 == 1:
|
|
25
|
+
data = pad(data)
|
|
26
|
+
|
|
27
|
+
# Apply the balanced Feistel cipher
|
|
28
|
+
left, right = split(data)
|
|
29
|
+
if len(left) != len(right):
|
|
30
|
+
raise Exception("invalid string: unable to split")
|
|
31
|
+
|
|
32
|
+
parts = [left, right]
|
|
33
|
+
for i in range(0, len(self.keys)):
|
|
34
|
+
tmp = xor(parts[0], self._round(parts[1], i))
|
|
35
|
+
parts = [parts[1], tmp]
|
|
36
|
+
|
|
37
|
+
return string2bytearray(parts[0] + parts[1])
|
|
38
|
+
|
|
39
|
+
def decrypt(self, obfuscated: bytes | bytearray) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Deobfuscate the passed data
|
|
42
|
+
"""
|
|
43
|
+
assert len(obfuscated) % 2 == 0, "CipherError: invalid obfuscated data"
|
|
44
|
+
if len(obfuscated) == 0:
|
|
45
|
+
return ""
|
|
46
|
+
|
|
47
|
+
o = obfuscated.decode()
|
|
48
|
+
|
|
49
|
+
# Apply the balanced Feistel cipher
|
|
50
|
+
b, a = split(o)
|
|
51
|
+
for i in range(0, len(self.keys)):
|
|
52
|
+
tmp = xor(a, self._round(b, len(self.keys) - i - 1))
|
|
53
|
+
a = b
|
|
54
|
+
b = tmp
|
|
55
|
+
|
|
56
|
+
return unpad(b + a)
|
|
57
|
+
|
|
58
|
+
def _round(self, item: str, idx: int) -> str:
|
|
59
|
+
addition = add(item, extract(self.keys[idx], idx, len(item)))
|
|
60
|
+
hex_hashed = hash(string2bytearray(addition)).hex()
|
|
61
|
+
return extract(hex_hashed, idx, len(item))
|