SwiftGUI_Encryption 0.0.1__tar.gz → 0.0.3__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,14 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SwiftGUI_Encryption
3
- Version: 0.0.1
3
+ Version: 0.0.3
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: cheesecaketv53+pypi@gmail.com
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.1"
3
+ version = "0.0.3"
4
4
  packages = [
5
5
  { include = "SwiftGUI_Encryption", from = "src" }
6
6
  ]
7
7
  authors = [
8
- { name="Eric aka CheesecakeTV", email="cheesecaketv53+pypi@gmail.com" },
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,3 @@
1
+
2
+ from .low_level import encrypt, decrypt, readable_hash, make_hash, random_key, argon2_key_derivation
3
+
@@ -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,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
+
@@ -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 every 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
+
@@ -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,157 @@
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):
111
+ self._salt = random_key(SALT_LEN)
112
+ self._filekey = adv.argon2_key_derivation(self._password.encode(), self._salt)
113
+
114
+ def change_key(self, new_key: bytes):
115
+ """
116
+ NOT IMPLEMENTED!
117
+
118
+ :param new_key:
119
+ :return:
120
+ """
121
+ raise NotImplementedError("Setting a key directly on a Password-file is not possible. Use .change_password instead!")
122
+
123
+ def change_password(self, new_password: str):
124
+ """
125
+ Specify a new password for this file
126
+
127
+ :param new_password:
128
+ :return:
129
+ """
130
+ self._password = new_password
131
+ self._regenerate_key()
132
+
133
+ def _save_to_file(
134
+ self,
135
+ values: dict,
136
+ path: Path,
137
+ ):
138
+ raw = json.dumps(values)
139
+
140
+ if not self._regenerated_key:
141
+ self._regenerate_key()
142
+ self._regenerated_key = False
143
+
144
+ path.write_bytes(
145
+ self._salt + encrypt_full(raw.encode(), self._filekey)
146
+ )
147
+
148
+ def _load_from_file(
149
+ self,
150
+ path: Path
151
+ ) -> dict:
152
+ raw = path.read_bytes()
153
+
154
+ raw = decrypt_full(raw[SALT_LEN:], self._filekey).decode()
155
+ return json.loads(raw)
156
+
157
+
@@ -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
+