SwiftGUI_Encryption 0.0.2__tar.gz → 0.0.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SwiftGUI_Encryption
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: Useful encryption-features for SwiftGUI-applications based on PyCryptoDome
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "SwiftGUI_Encryption"
3
- version = "0.0.2"
3
+ version = "0.0.4"
4
4
  packages = [
5
5
  { include = "SwiftGUI_Encryption", from = "src" }
6
6
  ]
@@ -0,0 +1,14 @@
1
+
2
+ from . import Advanced
3
+ random_key = Advanced.random_key
4
+
5
+ from .basics import decrypt_full, encrypt_full, encrypt_with_password, decrypt_with_password, password_to_key
6
+ from .key_files import KeyFile, KeyHandler, BaseKeyFile
7
+
8
+ try:
9
+ import SwiftGUI
10
+ except ImportError:
11
+ ...
12
+ else:
13
+ from . import sg
14
+
@@ -0,0 +1,85 @@
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
+ # This should not be chanced, it just doesn't feel right to add magic numbers...
9
+ NONCE_LEN = 32
10
+ SALT_LEN = 16
11
+
12
+ def encrypt_full(data: bytes, key: bytes) -> bytes:
13
+ """
14
+ Encrypt some data
15
+
16
+ :param key:
17
+ :param data:
18
+ :return:
19
+ """
20
+ nonce = adv.random_key(NONCE_LEN)
21
+
22
+ return nonce + adv.encrypt(data, key, nonce)
23
+
24
+ def decrypt_full(data: bytes, key: bytes) -> bytes:
25
+ """
26
+ Decrypt some data with its key
27
+
28
+ :param data:
29
+ :param key:
30
+ :return:
31
+ """
32
+ nonce = data[:NONCE_LEN]
33
+ data = data[NONCE_LEN:]
34
+
35
+ return adv.decrypt(data, key, nonce)
36
+
37
+ def encrypt_with_password(data: bytes, password: str, security_multiplier: int = 1) -> bytes:
38
+ """
39
+ IMPORTANT:
40
+ This needs to be decrypted with decrypt_with_password.
41
+ It is not compatible with decrypt_full.
42
+
43
+ But this function is very secure, because each encryption generates its own key.
44
+
45
+ :param data: What to encrypt
46
+ :param password:
47
+ :param security_multiplier:
48
+ :return:
49
+ """
50
+ salt = adv.random_key(SALT_LEN)
51
+ key = adv.argon2_key_derivation(password.encode(), salt, multiplier=security_multiplier)
52
+
53
+ return salt + encrypt_full(data, key)
54
+
55
+ def decrypt_with_password(data: bytes, password: str, security_multiplier: int = 1) -> bytes:
56
+ """
57
+ IMPORTANT:
58
+ This needs data from encrypt_with_password.
59
+ encrypt_full doesn't work on this.
60
+
61
+ :param data: What to decrypt
62
+ :param password:
63
+ :param security_multiplier: Needs to be the same as with the encryption
64
+ :return:
65
+ """
66
+ salt = data[:SALT_LEN]
67
+ key = adv.argon2_key_derivation(password.encode(), salt, multiplier=security_multiplier)
68
+
69
+ return decrypt_full(data[SALT_LEN:], key)
70
+
71
+ def password_to_key(password: str, security_multiplier: int = 1) -> bytes:
72
+ """
73
+ WARNING!
74
+ If anyone finds out what this function returned (the key), he might be able to find your password.
75
+ It's still quite secure, but not against people with supercomputers.
76
+ The attack is called pre-calculation-attack, or rainbow-table-attack.
77
+
78
+ Increasing the security_multiplier helps, but not as much as it does normally.
79
+
80
+ :param password:
81
+ :param security_multiplier: Needs to be the same as with the encryption
82
+ :return:
83
+ """
84
+ return adv.argon2_key_derivation(password.encode(), salt=b"", multiplier=security_multiplier)
85
+
@@ -23,7 +23,7 @@ class BaseKeyFile:
23
23
  :param saved_key: The key that is to be stored inside the file. Leave empty for random key
24
24
  """
25
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!"
26
+ assert file_key or file_password, "You need to specify either a file_password, or a file_key for every KeyFile!"
27
27
 
28
28
  self._salt: bytes = adv.random_key(SALT_LEN) # Placeholder if a key is used instead of a password
29
29
  self._key: bytes | None = saved_key # Key stored in the file
@@ -0,0 +1,5 @@
1
+
2
+ # SwiftGUI-Addons
3
+ from .dictFile import EncryptedJSONDictFile, PasswordJSONDictFile
4
+ from .popup_create_password import popup_create_password
5
+
@@ -0,0 +1,164 @@
1
+ from pathlib import Path
2
+ import json
3
+
4
+ import SwiftGUI.Files as files
5
+ from SwiftGUI_Encryption import encrypt_full, decrypt_full, random_key
6
+ from SwiftGUI_Encryption import Advanced as adv
7
+
8
+ NONCE_LEN = 32
9
+ SALT_LEN = 16
10
+
11
+ class EncryptedJSONDictFile(files.BaseDictFile):
12
+ """
13
+ An encrypted json-dictfile
14
+ """
15
+
16
+ def __init__(
17
+ self,
18
+ path: str | Path,
19
+ file_key: bytes,
20
+ *,
21
+ defaults: dict = None,
22
+ add_defaults_to_values: bool = None,
23
+ auto_save: bool = None,
24
+ **kwargs
25
+ ):
26
+ assert not isinstance(file_key, str), "Keys are always in the byte-format.\nIf you tired to pass a password, use PasswordJSONDictFile instead."
27
+
28
+ self._filekey = file_key # Key to encrypt the file with
29
+
30
+ super().__init__(
31
+ path=path,
32
+ defaults=defaults,
33
+ add_defaults_to_values=add_defaults_to_values,
34
+ auto_save=auto_save,
35
+ **kwargs
36
+ )
37
+
38
+ def change_key(self, new_key: bytes):
39
+ """
40
+
41
+ :param new_key:
42
+ :return:
43
+ """
44
+ self._filekey = new_key
45
+ self._do_auto_save()
46
+
47
+ return self
48
+
49
+ @property
50
+ def key(self) -> bytes:
51
+ return self._filekey
52
+
53
+ def _save_to_file(
54
+ self,
55
+ values: dict,
56
+ path: Path,
57
+ ):
58
+ raw = json.dumps(values)
59
+
60
+ path.write_bytes(
61
+ encrypt_full(raw.encode(), self._filekey)
62
+ )
63
+
64
+ def _load_from_file(
65
+ self,
66
+ path: Path
67
+ ) -> dict:
68
+ raw = path.read_bytes()
69
+
70
+ raw = decrypt_full(raw, self._filekey).decode()
71
+ return json.loads(raw)
72
+
73
+ class PasswordJSONDictFile(EncryptedJSONDictFile):
74
+ """
75
+ An encrypted JSON-dictfile that requires a password instead of a key
76
+ """
77
+ def __init__(
78
+ self,
79
+ path: str | Path,
80
+ password: str,
81
+ *,
82
+ defaults: dict = None,
83
+ add_defaults_to_values: bool = None,
84
+ auto_save: bool = None,
85
+ **kwargs
86
+ ):
87
+ path = Path(path)
88
+
89
+ exists = path.exists()
90
+ self._regenerated_key = not exists # Regenerate the key on next save, if it already exists
91
+ if exists:
92
+ salt = path.read_bytes()[:SALT_LEN]
93
+ else:
94
+ salt = random_key(SALT_LEN)
95
+
96
+ self._salt = salt
97
+ self._password = password
98
+
99
+ self._filekey = adv.argon2_key_derivation(password.encode(), salt) # Key to encrypt the file with
100
+
101
+ super(EncryptedJSONDictFile, self).__init__(
102
+ path=path,
103
+ #file_key=self._filekey,
104
+ defaults=defaults,
105
+ add_defaults_to_values=add_defaults_to_values,
106
+ auto_save=auto_save,
107
+ **kwargs
108
+ )
109
+
110
+ def _regenerate_key(self, new_salt = True):
111
+ if new_salt:
112
+ self._salt = random_key(SALT_LEN)
113
+
114
+ self._filekey = adv.argon2_key_derivation(self._password.encode(), self._salt)
115
+
116
+ def change_key(self, new_key: bytes):
117
+ """
118
+ NOT IMPLEMENTED!
119
+
120
+ :param new_key:
121
+ :return:
122
+ """
123
+ raise NotImplementedError("Setting a key directly on a Password-file is not possible. Use .change_password instead!")
124
+
125
+ def change_password(self, new_password: str):
126
+ """
127
+ Specify a new password for this file
128
+
129
+ :param new_password:
130
+ :return:
131
+ """
132
+ self._password = new_password
133
+ self._regenerate_key()
134
+
135
+ def _save_to_file(
136
+ self,
137
+ values: dict,
138
+ path: Path,
139
+ ):
140
+ raw = json.dumps(values)
141
+
142
+ if not self._regenerated_key:
143
+ self._regenerate_key()
144
+ self._regenerated_key = False
145
+
146
+ path.write_bytes(
147
+ self._salt + encrypt_full(raw.encode(), self._filekey)
148
+ )
149
+
150
+ def _load_from_file(
151
+ self,
152
+ path: Path
153
+ ) -> dict:
154
+ raw = path.read_bytes()
155
+
156
+ salt = raw[:SALT_LEN]
157
+ if salt != self._salt:
158
+ self._salt = salt
159
+ self._regenerate_key(new_salt=False)
160
+
161
+ raw = decrypt_full(raw[SALT_LEN:], self._filekey).decode()
162
+ return json.loads(raw)
163
+
164
+
@@ -0,0 +1,168 @@
1
+ from typing import Hashable, Callable
2
+ from string import ascii_uppercase, ascii_lowercase, digits, ascii_letters, punctuation
3
+
4
+ ascii_letters: set = set(ascii_letters)
5
+ ascii_uppercase: set = set(ascii_uppercase)
6
+ ascii_lowercase: set = set(ascii_lowercase)
7
+ digits: set = set(digits)
8
+ punctuation: set = set(punctuation)
9
+
10
+ import SwiftGUI as sg
11
+ from SwiftGUI import ValueDict
12
+
13
+ # This class was originally created as an example for new SwiftGUI users.
14
+ # That's why it has so many comments.
15
+ class popup_create_password(sg.BasePopup, str):
16
+ def __init__(
17
+ self,
18
+ title: str = "Create your password",
19
+ min_length: int = None,
20
+ must_include_upper: bool = None,
21
+ must_include_lower: bool = None,
22
+ must_include_special: bool = None,
23
+ must_include_digits: bool = None,
24
+ additional_check_function: Callable = None,
25
+ additional_check_text: str = None,
26
+ wrong_password_color: str | sg.Color = "#A52A2A",
27
+ **kwargs
28
+ ):
29
+ self.min_length = min_length
30
+ self.must_include_upper = must_include_upper
31
+ self.must_include_lower = must_include_lower
32
+ self.must_include_special = must_include_special
33
+ self.must_include_digits = must_include_digits
34
+
35
+ self.additional_check_function = additional_check_function
36
+ self.additional_check_text = additional_check_text
37
+
38
+ self.wrong_password_color = wrong_password_color
39
+
40
+ layout = [
41
+ [
42
+ sg.T("Password:", width= 10),
43
+ password := sg.In(
44
+ key= "PW",
45
+ default_event= True,
46
+ pass_char= "*", # Hidden characters
47
+ ).bind_event(
48
+ sg.Event.KeyEnter,
49
+ key_function= lambda w:w["Confirm"].set_focus() # Jump to next input-element
50
+ ),
51
+ sg.Spacer(width=5),
52
+ sg.Checkbox(
53
+ "Show password",
54
+ default_event= True,
55
+ key_function= lambda val: password.update(pass_char = "" if val else "*"), # If the box is checked, reveal characters. Else hide them
56
+ takefocus= False, # Pressing tab should ignore this element
57
+ )
58
+ ],[
59
+ sg.T("Confirm:", width= 10),
60
+ confirm := sg.In(
61
+ key= "Confirm",
62
+ default_event= True,
63
+ pass_char= "*", # Also hidden characters
64
+ ).bind_event(
65
+ sg.Event.KeyEnter, # Same as clicking "Confirm"
66
+ key= "Done",
67
+ ),
68
+ sg.T(expand=True)
69
+ ],[
70
+ sg.HSep(),
71
+ ], [
72
+ sg.Button(
73
+ "Confirm",
74
+ key= "Done",
75
+ ),
76
+ sg.Button(
77
+ "Cancel",
78
+ key_function= lambda :self.done() # Call self.done() so the popup "returns" None
79
+ )
80
+ ]
81
+ ] + self._additional_layout()
82
+ self.password = password # Save these two for later
83
+ self.confirm = confirm
84
+
85
+ super().__init__(layout, title=title, **kwargs)
86
+ password.set_focus() # Start with the focus on the password-input-field
87
+
88
+ def _additional_layout(self) -> list[list[sg.BaseElement]]:
89
+ new_texts = []
90
+
91
+ if self.additional_check_text:
92
+ new_texts.append(self.additional_check_text)
93
+
94
+ if self.min_length:
95
+ new_texts.append(f"Must be at least {self.min_length} characters long")
96
+
97
+ if self.must_include_upper:
98
+ new_texts.append(f"Must include uppercase letters")
99
+
100
+ if self.must_include_lower:
101
+ new_texts.append(f"Must include lowercase letters")
102
+
103
+ if self.must_include_special:
104
+ new_texts.append(f"Must include special characters")
105
+
106
+ if self.must_include_digits:
107
+ new_texts.append(f"Must include digits")
108
+
109
+ new_layout = []
110
+ if new_texts:
111
+ new_layout.append([sg.HSep()])
112
+ new_layout.append([sg.T("The password...", expand=True)])
113
+
114
+ new_layout += [
115
+ [sg.T(text)] for text in new_texts
116
+ ]
117
+
118
+ return new_layout
119
+
120
+ def _is_valid_password(self) -> bool:
121
+ """Check if the entered password follows the rules"""
122
+
123
+ current_pw = self.password.value
124
+
125
+ if self.additional_check_function and not self.additional_check_function(current_pw):
126
+ return False
127
+
128
+ if self.min_length and len(current_pw) < self.min_length:
129
+ return False
130
+
131
+ current_pw: set = set(current_pw)
132
+
133
+ if self.must_include_upper and not (current_pw & ascii_uppercase):
134
+ return False
135
+
136
+ if self.must_include_lower and not (current_pw & ascii_lowercase):
137
+ return False
138
+
139
+ if self.must_include_special and not (current_pw & punctuation):
140
+ return False
141
+
142
+ if self.must_include_digits and not (current_pw & digits):
143
+ return False
144
+
145
+ return True
146
+
147
+ def _event_loop(self, e: Hashable, v: sg.ValueDict):
148
+ pw_match = self.password.value == self.confirm.value
149
+
150
+ if e == "Done" and pw_match and self._is_valid_password():
151
+ self.done(self.password.value) # "Return" the password
152
+
153
+ if self._is_valid_password():
154
+ self.password.update_to_default_value("background_color")
155
+ else:
156
+ self.password.update(background_color=self.wrong_password_color)
157
+
158
+ # If any other key happened, check if the two input-values match
159
+ if pw_match:
160
+ # If they do, use the default background-color for the confirm-field
161
+ self.confirm.update_to_default_value("background_color")
162
+ else:
163
+ # Else, set it to red
164
+ self.confirm.update(background_color=self.wrong_password_color)
165
+
166
+
167
+
168
+
@@ -1,5 +0,0 @@
1
-
2
- from . import Advanced
3
-
4
- from .basics import decrypt_full, encrypt_full
5
- from .key_files import KeyFile, KeyHandler, BaseKeyFile
@@ -1,35 +0,0 @@
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
-