ghostbytes 1.0.0__py3-none-any.whl

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.
ghostbytes/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ __version__ = "1.0.0"
2
+ __license__ = "MIT"
3
+ __link__ = "http://github.com/maxttkam/ghostbytes"
ghostbytes/__main__.py ADDED
@@ -0,0 +1,9 @@
1
+ from .gui import gui
2
+
3
+ def main() -> None:
4
+ app = gui.App()
5
+ app.mainloop()
6
+
7
+
8
+ if __name__ == "__main__":
9
+ main()
@@ -0,0 +1,155 @@
1
+ """
2
+ config.py
3
+
4
+ Defines encryption configuration used by Ghostbytes:
5
+ which cipher, hash function, KDF parameters, and random source to use,
6
+ plus helper functions (import, export, and generate configuration)
7
+ to save / load configurations to / from a config file.
8
+
9
+ See docs/confguration.md
10
+ """
11
+ import base64
12
+ import configparser
13
+ from hashlib import md5
14
+ from Crypto.Hash import SHA3_512, SHA3_256, SHA512, SHA256, BLAKE2b, BLAKE2s
15
+ from ghostbytes.error import invalid_configuration
16
+
17
+ AVAIL_HASH = [SHA3_512, SHA3_256, SHA512, SHA256, BLAKE2b, BLAKE2s, md5]
18
+ AVAIL_HASH_STR = [
19
+ "sha3_512",
20
+ "sha3_256",
21
+ "sha512",
22
+ "sha256",
23
+ "blake2b",
24
+ "blake2s",
25
+ "md5"]
26
+ AVAIL_ALG = ["aes", "rsa-oaep", "extended_oaep", "ML-KEM-768", "ML-KEM-1024"]
27
+ AVAIL_RANDOM_STR = [
28
+ "os.urandom",
29
+ "cryptodome_random",
30
+ "random lib (python)",
31
+ "secrets_random",
32
+ "/dev/urandom (*nux only)",
33
+ "/dev/random (*nux only)",
34
+ "/dev/urandom (dd, out file only) (*nux only)",
35
+ "/dev/random (dd, out file only) (*nux only)",
36
+ ]
37
+ ENCRYPTED_SUFFIX = ".enc"
38
+ RSA_KEY_OUT_FORMAT = ["PEM", "DER", "OpenSSH"]
39
+ COMMON_RSA_SIZE = [2048, 4096, 8192]
40
+ RSA_SIZE_WARNING_THRESHOLD = 4096
41
+
42
+ OVERWRITE_OPTIONS = [
43
+ 'random',
44
+ 'zero',
45
+ 'one',
46
+ 'gutmann'
47
+ ]
48
+
49
+ RANDOM_SALT_SIZE = 64
50
+
51
+
52
+ class CryptoConfig:
53
+ """
54
+ Holds cryptographic parameters used to encrypt / decrypt a file.
55
+
56
+ An instance can be created via `CryptoConfig()`, randomised via
57
+ `CryptoConfig.generate_random_salt()`, or restored from disk via
58
+ `CryptoConfig.import_file(path)`. Use `export_file(path)` to persist it.
59
+
60
+ Fields such as `algorithm`, `store_iv`, `mac_len`, and all kdf parameters must
61
+ remain the identical between encryptor and decryptor to allow ciphertext
62
+ to be successfully decrypted.
63
+
64
+ It is also recommended to use a DIFFERERNT SALT per encryption pair to prevent
65
+ rainbow table attacks. Use `export_file` and `import_file` to ensure the
66
+ cryptographic configurations are the same.
67
+ """
68
+ algorithm = AVAIL_ALG[0]
69
+
70
+ store_iv = "append" # or "prepend"
71
+ mac_len = 16
72
+ kdf_salt = b"CHANGE_ME_F1L3_3NCRYP710N_53CR37_54L7_70_3N5UR3_R4ND0MN355"
73
+ kdf_time_cost = 16
74
+ kdf_memory_cost = 128 * 1024 # 128 MB, times 1024 to get KB
75
+ kdf_parallelism = 8
76
+
77
+ hash_func = AVAIL_HASH[0]
78
+ rand_func = AVAIL_RANDOM_STR[0]
79
+
80
+ def generate_random_salt(self, length=64):
81
+ """Generate and store a new KDF salt using this config's random source."""
82
+
83
+ # Imports random within the function prevents cross-import error
84
+ from ghostbytes.tools.rand import random
85
+
86
+ self.kdf_salt = random(self.rand_func, length)
87
+ return self.kdf_salt
88
+
89
+ def export_file(self, path):
90
+ """Exports the current `CryptoConfig` state to a `.conf` file"""
91
+ parser = configparser.ConfigParser()
92
+ parser["CryptoConfig"] = {
93
+ "algorithm": self.algorithm,
94
+ "store_iv": self.store_iv,
95
+ "mac_len": str(self.mac_len),
96
+ "kdf_salt": base64.b64encode(self.kdf_salt).decode("ascii"),
97
+ "kdf_time_cost": str(self.kdf_time_cost),
98
+ "kdf_memory_cost": str(self.kdf_memory_cost),
99
+ "kdf_parallelism": str(self.kdf_parallelism),
100
+ "hash_func": AVAIL_HASH_STR[AVAIL_HASH.index(self.hash_func)],
101
+ "rand_func": self.rand_func,
102
+ }
103
+ with open(path, "w", encoding="utf-8") as file:
104
+ parser.write(file)
105
+
106
+ @classmethod
107
+ def import_file(cls, path):
108
+ """Imports the previous `CryptoConfig` state through a `.conf` file"""
109
+ parser = configparser.ConfigParser()
110
+ try:
111
+ if not parser.read(
112
+ path,
113
+ encoding="utf-8") or "CryptoConfig" not in parser:
114
+ raise invalid_configuration("missing CryptoConfig section")
115
+ except (OSError, UnicodeDecodeError, configparser.Error) as exc:
116
+ raise invalid_configuration("file could not be read") from exc
117
+
118
+ values = parser["CryptoConfig"]
119
+ try:
120
+ algorithm = values["algorithm"]
121
+ store_iv = values["store_iv"]
122
+ mac_len = values.getint("mac_len")
123
+ kdf_salt = base64.b64decode(values["kdf_salt"], validate=True)
124
+ kdf_time_cost = values.getint("kdf_time_cost")
125
+ kdf_memory_cost = values.getint("kdf_memory_cost")
126
+ kdf_parallelism = values.getint("kdf_parallelism")
127
+ hash_name = values["hash_func"]
128
+ rand_func = values["rand_func"]
129
+ except (KeyError, ValueError) as e:
130
+ raise invalid_configuration(
131
+ "one or more values are malformed") from e
132
+
133
+ if algorithm not in AVAIL_ALG:
134
+ raise invalid_configuration("unsupported algorithm")
135
+ if store_iv not in ("append", "prepend"):
136
+ raise invalid_configuration("invalid IV storage mode")
137
+ if not 4 <= mac_len <= 16:
138
+ raise invalid_configuration(
139
+ "MAC length must be between 4 and 16 bytes")
140
+ if not kdf_salt or kdf_time_cost < 1 or kdf_memory_cost < 1 or kdf_parallelism < 1:
141
+ raise invalid_configuration("invalid KDF settings")
142
+ if hash_name not in AVAIL_HASH_STR or rand_func not in AVAIL_RANDOM_STR:
143
+ raise invalid_configuration("unsupported hash or random function")
144
+
145
+ config = cls()
146
+ config.algorithm = algorithm
147
+ config.store_iv = store_iv
148
+ config.mac_len = mac_len
149
+ config.kdf_salt = kdf_salt
150
+ config.kdf_time_cost = kdf_time_cost
151
+ config.kdf_memory_cost = kdf_memory_cost
152
+ config.kdf_parallelism = kdf_parallelism
153
+ config.hash_func = AVAIL_HASH[AVAIL_HASH_STR.index(hash_name)]
154
+ config.rand_func = rand_func
155
+ return config
@@ -0,0 +1,162 @@
1
+ """
2
+ crypto.py
3
+
4
+ This is the main module provides a unified interface for encryption and decryption
5
+ using symmetric, asymmetric, and post-quantum cryptographic algorithms.
6
+
7
+ Supported algorithms are defined in `./config.py`
8
+ """
9
+
10
+
11
+ import re
12
+ from Crypto.PublicKey import RSA as _rsa
13
+ from ghostbytes.crypto.kyber import kyber_decrypt, kyber_encrypt
14
+ from ghostbytes.crypto.oaep_extension import oaep_extended_decrypt, oaep_extended_encrypt
15
+ from ghostbytes.crypto.config import CryptoConfig
16
+ from ghostbytes.error import algorithm_not_supported, parameter_not_exist, \
17
+ keyfile_cannot_crypt, keyfile_passphrase_incorrect, invalid_keyfile, \
18
+ invalid_configuration_type
19
+ from ghostbytes.crypto.primitives import derive_key, \
20
+ aes_decrypt, aes_encrypt, rsa_oaep_decrypt, rsa_oaep_encrypt
21
+
22
+
23
+ def encrypt(
24
+ config,
25
+ key,
26
+ plaintext,
27
+ rsa_passphrase=None
28
+ ):
29
+ """
30
+ Encrypts plaintext using the specified algorithm in `CryptoConfig`
31
+
32
+ Args:
33
+ config: Cryptographic configuration for the operation, includes the algorithm,
34
+ KDF parameters, and all necessary configuration to complete the operation.
35
+ key: Encryption key or key material. The expected format depends on the algorithm.
36
+ For symmetric encryption, key is typically a password or other secret key material.
37
+ plaintext: ata to encrypt. Must be provided as bytes.
38
+ rsa_passphrase: Optional passphrase used to decrypt a password-protected RSA keyfile
39
+ Must be provided as bytes.
40
+
41
+ Returns:
42
+ bytes: The encrypted ciphertext
43
+
44
+ Raises:
45
+ TypeError: If config args is not an instance of CryptoConfig.
46
+ CryptoError: Cryptographic errors likely raised due to invalid keys, malformed
47
+ ciphertext, invalid cryptographic parameters, or failures during
48
+ the underlying encryption/decryption operation.
49
+ GeneralError: Incorrect Usage of the function that are not cryptography-related.
50
+ """
51
+ _must_be_valid("config", config)
52
+ _must_be_valid("key", key)
53
+ _must_be_valid("plaintext", plaintext)
54
+
55
+ if not isinstance(config, CryptoConfig):
56
+ raise invalid_configuration_type()
57
+
58
+ algorithm = config.algorithm
59
+ if algorithm == "aes":
60
+ raw_key = derive_key(config, key)
61
+ return aes_encrypt(config, raw_key, plaintext)
62
+
63
+ if algorithm == "extended_oaep":
64
+ try:
65
+ rsa_key = _rsa.import_key(key, rsa_passphrase)
66
+ except (ValueError, TypeError, IndexError) as e:
67
+ if rsa_passphrase is not None:
68
+ raise keyfile_passphrase_incorrect() from e
69
+ raise invalid_keyfile() from e
70
+ if not rsa_key.can_encrypt():
71
+ raise keyfile_cannot_crypt("encrypt")
72
+
73
+ return oaep_extended_encrypt(config, rsa_key, plaintext)
74
+
75
+ if algorithm == "rsa-oaep":
76
+ try:
77
+ rsa_key = _rsa.import_key(key, rsa_passphrase)
78
+ except (ValueError, TypeError, IndexError) as e:
79
+ if rsa_passphrase is not None:
80
+ raise keyfile_passphrase_incorrect() from e
81
+ raise invalid_keyfile() from e
82
+ if not rsa_key.can_encrypt():
83
+ raise keyfile_cannot_crypt("encrypt")
84
+
85
+ return rsa_oaep_encrypt(config, rsa_key, plaintext)
86
+ if re.search(r"^ML-KEM-(768|1024)$", algorithm):
87
+ return kyber_encrypt(config, key, plaintext, rsa_passphrase)
88
+ raise algorithm_not_supported()
89
+
90
+
91
+ def decrypt(
92
+ config,
93
+ key,
94
+ ciphertext,
95
+ rsa_passphrase=None
96
+ ):
97
+ """
98
+ Decrypts ciphertext using the specified algorithm in `CryptoConfig`
99
+
100
+ Args:
101
+ config: Cryptographic configuration for the operation, includes the algorithm,
102
+ KDF parameters, and all necessary configuration to complete the operation.
103
+ key: Encryption key or key material. The expected format depends on the algorithm.
104
+ For symmetric encryption, key is typically a password or other secret key material.
105
+ ciphertext: Encrypted data to decrypt. Must be provided as bytes.
106
+ rsa_passphrase: Optional passphrase used to decrypt a password-protected RSA keyfile
107
+ Must be provided as bytes.
108
+
109
+ Returns:
110
+ bytes: The encrypted ciphertext
111
+
112
+ Raises:
113
+ TypeError: If config args is not an instance of CryptoConfig.
114
+ CryptoError: Cryptographic errors likely raised due to invalid keys, malformed
115
+ ciphertext, invalid cryptographic parameters, or failures during
116
+ the underlying encryption/decryption operation.
117
+ GeneralError: Incorrect Usage of the function that are not cryptography-related.
118
+ """
119
+
120
+ if not isinstance(config, CryptoConfig):
121
+ raise invalid_configuration_type()
122
+
123
+ _must_be_valid("config", config)
124
+ _must_be_valid("key", key)
125
+ _must_be_valid("ciphertext", ciphertext)
126
+
127
+ algorithm = config.algorithm
128
+ if algorithm == "aes":
129
+ raw_key = derive_key(config, key)
130
+ return aes_decrypt(config, raw_key, ciphertext)
131
+
132
+ if algorithm == "extended_oaep":
133
+ try:
134
+ rsa_key = _rsa.import_key(key, rsa_passphrase)
135
+ except (ValueError, TypeError, IndexError) as e:
136
+ if rsa_passphrase is not None:
137
+ raise keyfile_passphrase_incorrect() from e
138
+ raise invalid_keyfile() from e
139
+ if not rsa_key.has_private():
140
+ raise keyfile_cannot_crypt("decrypt")
141
+
142
+ return oaep_extended_decrypt(config, rsa_key, ciphertext)
143
+
144
+ if algorithm == "rsa-oaep":
145
+ try:
146
+ rsa_key = _rsa.import_key(key, rsa_passphrase)
147
+ except (ValueError, TypeError, IndexError) as e:
148
+ if rsa_passphrase is not None:
149
+ raise keyfile_passphrase_incorrect() from e
150
+ raise invalid_keyfile() from e
151
+ if not rsa_key.has_private():
152
+ raise keyfile_cannot_crypt("decrypt")
153
+
154
+ return rsa_oaep_decrypt(config, rsa_key, ciphertext)
155
+ if re.search(r"^ML-KEM-(768|1024)$", algorithm):
156
+ return kyber_decrypt(config, key, ciphertext, rsa_passphrase)
157
+ raise algorithm_not_supported()
158
+
159
+
160
+ def _must_be_valid(name, param):
161
+ if param is None:
162
+ raise parameter_not_exist(name)
@@ -0,0 +1,149 @@
1
+ from typing import Literal, TypeAlias
2
+
3
+ from Crypto.PublicKey.RSA import RsaKey
4
+
5
+ from ghostbytes.crypto.config import (
6
+ AVAIL_ALG,
7
+ AVAIL_HASH,
8
+ AVAIL_HASH_STR,
9
+ AVAIL_RANDOM_STR,
10
+ COMMON_RSA_SIZE,
11
+ ENCRYPTED_SUFFIX,
12
+ OVERWRITE_OPTIONS,
13
+ RANDOM_SALT_SIZE,
14
+ RSA_KEY_OUT_FORMAT,
15
+ RSA_SIZE_WARNING_THRESHOLD,
16
+ CryptoConfig,
17
+ )
18
+
19
+
20
+ # Public configuration values
21
+ Algorithm: TypeAlias = Literal[
22
+ "aes",
23
+ "rsa-oaep",
24
+ "extended_oaep",
25
+ "ML-KEM-768",
26
+ "ML-KEM-1024",
27
+ ]
28
+ HashName: TypeAlias = Literal[
29
+ "sha3_512",
30
+ "sha3_256",
31
+ "sha512",
32
+ "sha256",
33
+ "blake2b",
34
+ "blake2s",
35
+ "md5",
36
+ ]
37
+ KeyEncoding: TypeAlias = Literal["PEM", "DER", "OpenSSH"]
38
+ MLKEMEncoding: TypeAlias = Literal["PEM", "DER"]
39
+
40
+
41
+ # crypto.py
42
+ def encrypt(
43
+ config: CryptoConfig,
44
+ key: bytes,
45
+ plaintext: bytes,
46
+ rsa_passphrase: str | bytes | None = None,
47
+ ) -> bytes: ...
48
+
49
+
50
+ def decrypt(
51
+ config: CryptoConfig,
52
+ key: bytes,
53
+ ciphertext: bytes,
54
+ rsa_passphrase: str | bytes | None = None,
55
+ ) -> bytes: ...
56
+
57
+
58
+ # primitives.py
59
+ def aes_encrypt(config: CryptoConfig, key: bytes, plaintext: bytes) -> bytes: ...
60
+
61
+
62
+ def aes_decrypt(config: CryptoConfig, key: bytes, ciphertext: bytes) -> bytes: ...
63
+
64
+
65
+ def derive_key(config: CryptoConfig, secret: bytes) -> bytes: ...
66
+
67
+
68
+ def rsa_oaep_encrypt(
69
+ config: CryptoConfig,
70
+ key: RsaKey,
71
+ plaintext: bytes,
72
+ ) -> bytes: ...
73
+
74
+
75
+ def rsa_oaep_decrypt(
76
+ config: CryptoConfig,
77
+ key: RsaKey,
78
+ ciphertext: bytes,
79
+ ) -> bytes: ...
80
+
81
+
82
+ def genrsa(
83
+ length: int,
84
+ exp: int,
85
+ passphrase: str | bytes | None,
86
+ out_format: KeyEncoding = "PEM",
87
+ ) -> tuple[bytes, bytes]: ...
88
+
89
+
90
+ def verify_rsa(
91
+ public_key: bytes,
92
+ private_key: bytes,
93
+ passphrase: str | bytes | None = None,
94
+ ) -> bool: ...
95
+
96
+
97
+ def detect_key_format(key_bytes: bytes) -> Literal["PEM", "DER"]: ...
98
+
99
+
100
+ def keytype(
101
+ key_bytes: bytes,
102
+ passphrase: str | bytes | None = None,
103
+ ) -> Literal["RSA", "ML-KEM"]: ...
104
+
105
+
106
+ # oaep_extension.py
107
+ def oaep_extended_encrypt(
108
+ config: CryptoConfig,
109
+ rsa: RsaKey,
110
+ plaintext: bytes,
111
+ ) -> bytes: ...
112
+
113
+
114
+ def oaep_extended_decrypt(
115
+ config: CryptoConfig,
116
+ rsa: RsaKey,
117
+ ciphertext: bytes,
118
+ ) -> bytes: ...
119
+
120
+
121
+ # kyber.py
122
+ def genkey(
123
+ alg: Literal["ML-KEM-768", "ML-KEM-1024"],
124
+ encoding: MLKEMEncoding,
125
+ passphrase: bytes | None = None,
126
+ ) -> tuple[bytes, bytes]: ...
127
+
128
+
129
+ def verify_key(
130
+ pubkey: bytes,
131
+ privkey: bytes,
132
+ passphrase: str | bytes | None = None,
133
+ ) -> bool: ...
134
+
135
+
136
+ def kyber_encrypt(
137
+ config: CryptoConfig,
138
+ pubkey: bytes,
139
+ plaintext: bytes,
140
+ passphrase: str | bytes | None = None,
141
+ ) -> bytes: ...
142
+
143
+
144
+ def kyber_decrypt(
145
+ config: CryptoConfig,
146
+ privkey: bytes,
147
+ ciphertext: bytes,
148
+ passphrase: str | bytes | None = None,
149
+ ) -> bytes: ...