SwiftGUI_Encryption 0.0.1__tar.gz → 0.0.2__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.
- {swiftgui_encryption-0.0.1 → swiftgui_encryption-0.0.2}/PKG-INFO +4 -2
- {swiftgui_encryption-0.0.1 → swiftgui_encryption-0.0.2}/pyproject.toml +6 -3
- swiftgui_encryption-0.0.2/src/SwiftGUI_Encryption/Advanced/__init__.py +3 -0
- swiftgui_encryption-0.0.2/src/SwiftGUI_Encryption/Advanced/low_level.py +87 -0
- swiftgui_encryption-0.0.2/src/SwiftGUI_Encryption/__init__.py +5 -0
- swiftgui_encryption-0.0.2/src/SwiftGUI_Encryption/basics.py +35 -0
- swiftgui_encryption-0.0.2/src/SwiftGUI_Encryption/key_files.py +176 -0
- swiftgui_encryption-0.0.1/src/SwiftGUI_Encryption/__init__.py +0 -4
- {swiftgui_encryption-0.0.1 → swiftgui_encryption-0.0.2}/LICENSE +0 -0
- {swiftgui_encryption-0.0.1 → swiftgui_encryption-0.0.2}/README.md +0 -0
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SwiftGUI_Encryption
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: Useful encryption-features for SwiftGUI-applications based on PyCryptoDome
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Author: Eric aka CheesecakeTV
|
|
8
|
-
Author-email:
|
|
8
|
+
Author-email: eric@swiftgui.de
|
|
9
9
|
Requires-Python: >=3.10
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Dist: PyCryptoDome
|
|
13
|
+
Requires-Dist: argon2pure
|
|
12
14
|
Project-URL: Documentation, https://github.com/CheesecakeTV/SwiftGUI-Docs
|
|
13
15
|
Project-URL: Repository, https://github.com/CheesecakeTV/SwiftGUI-Encryption
|
|
14
16
|
Project-URL: issues, https://github.com/CheesecakeTV/SwiftGUI/issues
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "SwiftGUI_Encryption"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.2"
|
|
4
4
|
packages = [
|
|
5
5
|
{ include = "SwiftGUI_Encryption", from = "src" }
|
|
6
6
|
]
|
|
7
7
|
authors = [
|
|
8
|
-
{ name="Eric aka CheesecakeTV", email="
|
|
8
|
+
{ name="Eric aka CheesecakeTV", email="eric@swiftgui.de" },
|
|
9
9
|
]
|
|
10
10
|
description = "Useful encryption-features for SwiftGUI-applications based on PyCryptoDome"
|
|
11
11
|
readme = "README.md"
|
|
@@ -16,7 +16,10 @@ classifiers = [
|
|
|
16
16
|
]
|
|
17
17
|
license = "Apache-2.0"
|
|
18
18
|
license-files = ["LICEN[CS]E*"]
|
|
19
|
-
dependencies = [
|
|
19
|
+
dependencies = [
|
|
20
|
+
"PyCryptoDome",
|
|
21
|
+
"argon2pure",
|
|
22
|
+
]
|
|
20
23
|
[project.urls]
|
|
21
24
|
Repository = "https://github.com/CheesecakeTV/SwiftGUI-Encryption"
|
|
22
25
|
Documentation = "https://github.com/CheesecakeTV/SwiftGUI-Docs"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import argon2pure
|
|
2
|
+
from Crypto.Cipher import AES
|
|
3
|
+
import os
|
|
4
|
+
import hashlib
|
|
5
|
+
|
|
6
|
+
def random_key(n: int = 32) -> bytes:
|
|
7
|
+
"""
|
|
8
|
+
Generate a new random key
|
|
9
|
+
:param n: Length of the key in bytes
|
|
10
|
+
:return:
|
|
11
|
+
"""
|
|
12
|
+
return os.urandom(n)
|
|
13
|
+
|
|
14
|
+
def readable_hash(data: bytes, n: int = 6) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Create a humanly-readable string that can be used to compare data without knowing the data.
|
|
17
|
+
Only use it if the user himself needs to read/compare this
|
|
18
|
+
|
|
19
|
+
:param data:
|
|
20
|
+
:param n: Length of the checksum
|
|
21
|
+
:return:
|
|
22
|
+
"""
|
|
23
|
+
return hashlib.sha256(data).hexdigest()[:n].upper()
|
|
24
|
+
|
|
25
|
+
def make_hash(data: bytes) -> bytes:
|
|
26
|
+
"""
|
|
27
|
+
Generate the hash-value of some data
|
|
28
|
+
Raises a value-error if no text was supplied
|
|
29
|
+
:param data:
|
|
30
|
+
:return:
|
|
31
|
+
"""
|
|
32
|
+
return hashlib.sha256(data).digest()
|
|
33
|
+
|
|
34
|
+
def argon2_key_derivation(derive_from: bytes, salt: bytes, multiplier: int = 1, n: int = 32) -> bytes:
|
|
35
|
+
"""
|
|
36
|
+
If you don't know what key-derivation is, don't use this function.
|
|
37
|
+
|
|
38
|
+
:param derive_from:
|
|
39
|
+
:param salt:
|
|
40
|
+
:param multiplier: You may increase this to increase calculation-time and therefore security
|
|
41
|
+
:param n: How many characters the generated key should have
|
|
42
|
+
:return:
|
|
43
|
+
"""
|
|
44
|
+
salt = salt[:16] # clip it to the needed length
|
|
45
|
+
|
|
46
|
+
return argon2pure.argon2(derive_from, salt, multiplier, 8 * multiplier, parallelism=1, tag_length=n)
|
|
47
|
+
|
|
48
|
+
def encrypt(data: bytes, key: bytes, nonce: bytes, mac_len: int = 8) -> bytes:
|
|
49
|
+
"""
|
|
50
|
+
Encrypt some data.
|
|
51
|
+
The tag is appended to the end, fitting the decryption-function.
|
|
52
|
+
|
|
53
|
+
:param data:
|
|
54
|
+
:param key:
|
|
55
|
+
:param nonce: A random number, which you should definetly remember
|
|
56
|
+
:param mac_len:
|
|
57
|
+
:return: Encrypted
|
|
58
|
+
"""
|
|
59
|
+
crypter = AES.new(key, AES.MODE_GCM, mac_len=mac_len, nonce=nonce)
|
|
60
|
+
|
|
61
|
+
enc_data, tag = crypter.encrypt_and_digest(data) # tag is always 16 bytes
|
|
62
|
+
|
|
63
|
+
return tag + enc_data
|
|
64
|
+
|
|
65
|
+
def decrypt(enc_data:bytes, key:bytes, nonce:bytes, mac_len: int = 8) -> bytes:
|
|
66
|
+
"""
|
|
67
|
+
Decrypt some data.
|
|
68
|
+
The tag needs to be appended to the data.
|
|
69
|
+
|
|
70
|
+
Raises a value-error if the data was manipulated (tag is invalid)
|
|
71
|
+
|
|
72
|
+
:param mac_len: This needs to be the same as with the encryption
|
|
73
|
+
:param enc_data: Encrypted data
|
|
74
|
+
:param key: This needs to be the same as with the encryption
|
|
75
|
+
:param nonce: This needs to be the same as with the encryption
|
|
76
|
+
:return:
|
|
77
|
+
"""
|
|
78
|
+
tag = enc_data[:mac_len]
|
|
79
|
+
enc_data = enc_data[mac_len:]
|
|
80
|
+
crypter = AES.new(key, AES.MODE_GCM, nonce=nonce, mac_len=mac_len)
|
|
81
|
+
|
|
82
|
+
data = crypter.decrypt(enc_data)
|
|
83
|
+
|
|
84
|
+
crypter.verify(tag)
|
|
85
|
+
|
|
86
|
+
return data
|
|
87
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import argon2pure
|
|
2
|
+
from Crypto.Cipher import AES
|
|
3
|
+
import os
|
|
4
|
+
import hashlib
|
|
5
|
+
|
|
6
|
+
from SwiftGUI_Encryption import Advanced as adv
|
|
7
|
+
|
|
8
|
+
NONCE_LEN = 32 # This should not be chanced, but who am I to judge
|
|
9
|
+
|
|
10
|
+
def encrypt_full(data: bytes, key: bytes) -> bytes:
|
|
11
|
+
"""
|
|
12
|
+
Encrypt some data
|
|
13
|
+
|
|
14
|
+
:param key:
|
|
15
|
+
:param data:
|
|
16
|
+
:return:
|
|
17
|
+
"""
|
|
18
|
+
nonce = adv.random_key(NONCE_LEN)
|
|
19
|
+
|
|
20
|
+
return nonce + adv.encrypt(data, key, nonce)
|
|
21
|
+
|
|
22
|
+
def decrypt_full(data: bytes, key: bytes) -> bytes:
|
|
23
|
+
"""
|
|
24
|
+
Decrypt some data with its key
|
|
25
|
+
|
|
26
|
+
:param data:
|
|
27
|
+
:param key:
|
|
28
|
+
:return:
|
|
29
|
+
"""
|
|
30
|
+
nonce = data[:NONCE_LEN]
|
|
31
|
+
data = data[NONCE_LEN:]
|
|
32
|
+
|
|
33
|
+
return adv.decrypt(data, key, nonce)
|
|
34
|
+
|
|
35
|
+
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from os import PathLike
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from SwiftGUI_Encryption import Advanced as adv
|
|
6
|
+
from SwiftGUI_Encryption import encrypt_full, decrypt_full
|
|
7
|
+
|
|
8
|
+
SALT_LEN = 16
|
|
9
|
+
SECURITY_MULTIPLIER = 8
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseKeyFile:
|
|
13
|
+
|
|
14
|
+
def __init__(self, file_password: str = None, file_key: bytes = None, saved_key: bytes = None):
|
|
15
|
+
"""
|
|
16
|
+
Abstract base class so you could implement a different read/write-method.
|
|
17
|
+
|
|
18
|
+
Create/read a single file containing a key.
|
|
19
|
+
The file can be encrypted by a password, or a key directly
|
|
20
|
+
|
|
21
|
+
:param file_password: Password to unlock the file
|
|
22
|
+
:param file_key: Key to unlock the file
|
|
23
|
+
:param saved_key: The key that is to be stored inside the file. Leave empty for random key
|
|
24
|
+
"""
|
|
25
|
+
# Create folders containing this file
|
|
26
|
+
assert file_key or file_password, "You need to specify either a file_password, or a file_key for each KeyFile!"
|
|
27
|
+
|
|
28
|
+
self._salt: bytes = adv.random_key(SALT_LEN) # Placeholder if a key is used instead of a password
|
|
29
|
+
self._key: bytes | None = saved_key # Key stored in the file
|
|
30
|
+
self._file_password: str | None = file_password
|
|
31
|
+
self._file_key: bytes | None = file_key # Key to unlock the file
|
|
32
|
+
|
|
33
|
+
if self.exists():
|
|
34
|
+
self._init_exists()
|
|
35
|
+
else:
|
|
36
|
+
self._init_not_exists()
|
|
37
|
+
|
|
38
|
+
def _do_key_derivation(self):
|
|
39
|
+
"""
|
|
40
|
+
Generate or re-generate the file-key
|
|
41
|
+
"""
|
|
42
|
+
if self._file_password is None:
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
self._salt = adv.random_key(SALT_LEN)
|
|
46
|
+
file_key = adv.argon2_key_derivation(self._file_password.encode(), self._salt, multiplier=SECURITY_MULTIPLIER)
|
|
47
|
+
|
|
48
|
+
self._file_key = file_key
|
|
49
|
+
|
|
50
|
+
def _init_exists(self):
|
|
51
|
+
"""init if the file already exists"""
|
|
52
|
+
raw = self._read()
|
|
53
|
+
|
|
54
|
+
if self._file_key is None:
|
|
55
|
+
self._salt = raw[:SALT_LEN]
|
|
56
|
+
self._file_key = adv.argon2_key_derivation(self._file_password.encode(), self._salt, multiplier=SECURITY_MULTIPLIER)
|
|
57
|
+
|
|
58
|
+
assert self._key is None, "The file already exists, yet you tried to define its secret key"
|
|
59
|
+
# if self._key is not None:
|
|
60
|
+
# self.save()
|
|
61
|
+
# return
|
|
62
|
+
|
|
63
|
+
raw = raw[SALT_LEN:]
|
|
64
|
+
self._key = decrypt_full(raw, self._file_key)
|
|
65
|
+
|
|
66
|
+
def _init_not_exists(self):
|
|
67
|
+
"""init if the file doesn't already exist"""
|
|
68
|
+
self._do_key_derivation()
|
|
69
|
+
|
|
70
|
+
if self._key is None:
|
|
71
|
+
self._key = adv.random_key()
|
|
72
|
+
|
|
73
|
+
self.save()
|
|
74
|
+
|
|
75
|
+
def save(self):
|
|
76
|
+
"""
|
|
77
|
+
Save the content of the "file" to whereever
|
|
78
|
+
|
|
79
|
+
:return: Self (Not typehinted for compatability-reasons)
|
|
80
|
+
"""
|
|
81
|
+
raw = encrypt_full(self._key, self._file_key)
|
|
82
|
+
self._write(self._salt + raw)
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def _read(self) -> bytes:
|
|
87
|
+
"""Pure read from whereever"""
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def _write(self, data: bytes):
|
|
92
|
+
"""Pure write the data to whereever"""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def exists(self) -> bool:
|
|
97
|
+
"""True, if the file (or whatever) exists"""
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def key(self) -> bytes:
|
|
102
|
+
return self._key
|
|
103
|
+
|
|
104
|
+
@key.setter
|
|
105
|
+
def key(self, val):
|
|
106
|
+
self.change_key(val)
|
|
107
|
+
|
|
108
|
+
def change_key(self, new_key: bytes = None):
|
|
109
|
+
"""
|
|
110
|
+
Overwrite the saved key.
|
|
111
|
+
:param new_key:
|
|
112
|
+
:return:
|
|
113
|
+
"""
|
|
114
|
+
if new_key is None:
|
|
115
|
+
new_key = adv.random_key()
|
|
116
|
+
|
|
117
|
+
self._do_key_derivation()
|
|
118
|
+
|
|
119
|
+
self._key = new_key
|
|
120
|
+
self.save()
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
def change_file_key(self, new_password: str | None = None, new_key: bytes | None = None):
|
|
124
|
+
"""
|
|
125
|
+
Change the key/password which unlocks the file
|
|
126
|
+
|
|
127
|
+
:param new_password:
|
|
128
|
+
:param new_key:
|
|
129
|
+
:return:
|
|
130
|
+
"""
|
|
131
|
+
self._file_password = new_password
|
|
132
|
+
self._file_key = new_key
|
|
133
|
+
self._do_key_derivation()
|
|
134
|
+
self.save()
|
|
135
|
+
|
|
136
|
+
class KeyFile(BaseKeyFile):
|
|
137
|
+
|
|
138
|
+
def __init__(self, path: str | PathLike | Path, file_password: str = None, file_key: bytes = None,
|
|
139
|
+
saved_key: bytes = None):
|
|
140
|
+
"""
|
|
141
|
+
Create/read a single file containing a key.
|
|
142
|
+
The file can be encrypted by a password, or a key directly
|
|
143
|
+
|
|
144
|
+
:param path: Path to the file
|
|
145
|
+
:param file_password: Password to unlock the file
|
|
146
|
+
:param file_key: Key to unlock the file
|
|
147
|
+
:param saved_key: The key that is to be stored inside the file. Leave empty for random key
|
|
148
|
+
"""
|
|
149
|
+
# Create folders containing this file
|
|
150
|
+
self.path = Path(path)
|
|
151
|
+
|
|
152
|
+
super().__init__(file_password=file_password, file_key=file_key, saved_key=saved_key)
|
|
153
|
+
|
|
154
|
+
def _init_not_exists(self, *args, **kwargs):
|
|
155
|
+
self.path.parent.mkdir(exist_ok=True, parents=True)
|
|
156
|
+
super()._init_not_exists(*args, **kwargs)
|
|
157
|
+
|
|
158
|
+
def _read(self) -> bytes:
|
|
159
|
+
"""Pure read"""
|
|
160
|
+
return self.path.read_bytes()
|
|
161
|
+
|
|
162
|
+
def _write(self, data: bytes):
|
|
163
|
+
"""Pure write"""
|
|
164
|
+
self.path.write_bytes(data)
|
|
165
|
+
|
|
166
|
+
def exists(self) -> bool:
|
|
167
|
+
"""True, if the file (or whatever) exists"""
|
|
168
|
+
return self.path.exists()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class KeyHandler:
|
|
172
|
+
|
|
173
|
+
def __init__(self):
|
|
174
|
+
...
|
|
175
|
+
|
|
176
|
+
|
|
File without changes
|
|
File without changes
|