gun101 1.2.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.
- gun101/__init__.py +47 -0
- gun101/cli.py +331 -0
- gun101/config.py +43 -0
- gun101/crypto_engine.py +129 -0
- gun101/gun101_challenge.py +182 -0
- gun101/identity.py +280 -0
- gun101/key_manager.py +98 -0
- gun101/pdf_handler.py +451 -0
- gun101/security_layer.py +141 -0
- gun101/utils.py +60 -0
- gun101-1.2.0.dist-info/METADATA +542 -0
- gun101-1.2.0.dist-info/RECORD +15 -0
- gun101-1.2.0.dist-info/WHEEL +4 -0
- gun101-1.2.0.dist-info/entry_points.txt +2 -0
- gun101-1.2.0.dist-info/licenses/LICENSE +21 -0
gun101/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GUN101 PDF Encryption Protocol Suite
|
|
3
|
+
|
|
4
|
+
Two encryption modes:
|
|
5
|
+
|
|
6
|
+
GUN101 (password-based)
|
|
7
|
+
AES-256-GCM with Argon2/PBKDF2 key derivation, time-based cryptographic
|
|
8
|
+
challenge, and rate-limiting brute-force protection.
|
|
9
|
+
|
|
10
|
+
GUN101-GKP (Ghost Key Protocol — identity/asymmetric mode)
|
|
11
|
+
RSA-4096 key wrapping via the recipient's public Identity Token.
|
|
12
|
+
No shared password required. Only the recipient's physical device
|
|
13
|
+
(holding the matching GKP private key) can decrypt the file.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .config import security_config
|
|
17
|
+
from .crypto_engine import CryptoEngine
|
|
18
|
+
from .key_manager import KeyManager
|
|
19
|
+
from .security_layer import SecurityLayer
|
|
20
|
+
from .pdf_handler import PDFEncryptionHandler
|
|
21
|
+
from .identity import IdentityManager
|
|
22
|
+
from .utils import (
|
|
23
|
+
generate_salt,
|
|
24
|
+
generate_nonce,
|
|
25
|
+
get_current_timestamp,
|
|
26
|
+
verify_password_hash,
|
|
27
|
+
create_fingerprint
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__version__ = "1.2.0"
|
|
31
|
+
__author__ = "Security Team"
|
|
32
|
+
__protocol__ = "GUN101"
|
|
33
|
+
__gkp_protocol__ = "GUN101-GKP"
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"PDFEncryptionHandler",
|
|
37
|
+
"IdentityManager",
|
|
38
|
+
"CryptoEngine",
|
|
39
|
+
"KeyManager",
|
|
40
|
+
"SecurityLayer",
|
|
41
|
+
"security_config",
|
|
42
|
+
"generate_salt",
|
|
43
|
+
"generate_nonce",
|
|
44
|
+
"get_current_timestamp",
|
|
45
|
+
"verify_password_hash",
|
|
46
|
+
"create_fingerprint"
|
|
47
|
+
]
|
gun101/cli.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GUN101 Command Line Interface
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
gun101 help Show all available commands
|
|
6
|
+
gun101 encrypt <file> Encrypt a PDF with a password (GUN101)
|
|
7
|
+
gun101 encrypt <file> --recipient <token> Encrypt for a specific recipient (GUN101-GKP)
|
|
8
|
+
gun101 decrypt <file> Decrypt a file (auto-detects GUN101 vs GKP)
|
|
9
|
+
gun101 generate-identity Generate your GKP Identity Token
|
|
10
|
+
gun101 show-identity Display your GKP Identity Token
|
|
11
|
+
gun101 reset-identity Delete your GKP identity (WARNING: irreversible)
|
|
12
|
+
"""
|
|
13
|
+
import argparse
|
|
14
|
+
import sys
|
|
15
|
+
import json
|
|
16
|
+
import getpass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from gun101 import PDFEncryptionHandler
|
|
20
|
+
from gun101.identity import IdentityManager
|
|
21
|
+
from gun101.config import security_config
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ── Styling helpers ──────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
def _banner():
|
|
27
|
+
print()
|
|
28
|
+
print("╔══════════════════════════════════════════════════════════╗")
|
|
29
|
+
print("║ GUN101 PDF Encryption Protocol Suite ║")
|
|
30
|
+
print("║ Password · GKP (Ghost Key Protocol) · AES-256-GCM ║")
|
|
31
|
+
print("╚══════════════════════════════════════════════════════════╝")
|
|
32
|
+
print()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _success(msg):
|
|
36
|
+
print(f" ✓ {msg}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _error(msg):
|
|
40
|
+
print(f" ✗ {msg}", file=sys.stderr)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _info(msg):
|
|
44
|
+
print(f" ℹ {msg}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ── Command implementations ─────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
def help_command(args):
|
|
50
|
+
"""Show detailed help for all GUN101 commands."""
|
|
51
|
+
_banner()
|
|
52
|
+
print("COMMANDS")
|
|
53
|
+
print("────────────────────────────────────────────────────────────")
|
|
54
|
+
print()
|
|
55
|
+
print(" gun101 help")
|
|
56
|
+
print(" Show this help message with all available commands.")
|
|
57
|
+
print()
|
|
58
|
+
print(" gun101 encrypt <file> [-o OUTPUT]")
|
|
59
|
+
print(" Encrypt a PDF file using a password (GUN101 mode).")
|
|
60
|
+
print(" You will be securely prompted to enter and confirm")
|
|
61
|
+
print(" your password (it will not appear on screen).")
|
|
62
|
+
print()
|
|
63
|
+
print(" gun101 encrypt <file> --recipient <IDENTITY_TOKEN> [-o OUTPUT]")
|
|
64
|
+
print(" Encrypt a PDF using GUN101-GKP (Ghost Key Protocol).")
|
|
65
|
+
print(" No password is needed. Only the recipient's physical")
|
|
66
|
+
print(" device, which holds the matching GKP private key,")
|
|
67
|
+
print(" can decrypt the file.")
|
|
68
|
+
print()
|
|
69
|
+
print(" gun101 decrypt <file> [-o OUTPUT]")
|
|
70
|
+
print(" Decrypt a GUN101 or GKP-encrypted file. The tool")
|
|
71
|
+
print(" auto-detects the mode and handles it accordingly.")
|
|
72
|
+
print()
|
|
73
|
+
print(" gun101 generate-identity")
|
|
74
|
+
print(" Generate your device's GKP (Ghost Key Protocol) identity.")
|
|
75
|
+
print(" An RSA-4096 keypair is created. The private key is")
|
|
76
|
+
print(" stored securely on this device and never leaves it.")
|
|
77
|
+
print(" The public Identity Token is displayed for sharing.")
|
|
78
|
+
print()
|
|
79
|
+
print(" gun101 show-identity")
|
|
80
|
+
print(" Display your existing GKP Identity Token and its")
|
|
81
|
+
print(" fingerprint so you can share it with senders.")
|
|
82
|
+
print()
|
|
83
|
+
print(" gun101 reset-identity")
|
|
84
|
+
print(" Delete your GKP identity from this device.")
|
|
85
|
+
print(" WARNING: Any GKP-encrypted files targeting this")
|
|
86
|
+
print(" identity will become PERMANENTLY UNRECOVERABLE.")
|
|
87
|
+
print()
|
|
88
|
+
print("OPTIONS")
|
|
89
|
+
print("────────────────────────────────────────────────────────────")
|
|
90
|
+
print(" -o, --output PATH Custom output file path")
|
|
91
|
+
print(" --recipient TOKEN Recipient's GKP Identity Token (for GKP mode)")
|
|
92
|
+
print()
|
|
93
|
+
print("EXAMPLES")
|
|
94
|
+
print("────────────────────────────────────────────────────────────")
|
|
95
|
+
print(" # Password-based encryption (GUN101)")
|
|
96
|
+
print(" gun101 encrypt report.pdf")
|
|
97
|
+
print(" gun101 decrypt report.pdf.encrypted")
|
|
98
|
+
print()
|
|
99
|
+
print(" # Identity-based encryption (GUN101-GKP — no password needed)")
|
|
100
|
+
print(" gun101 generate-identity")
|
|
101
|
+
print(" gun101 show-identity # share your token with the sender")
|
|
102
|
+
print(" gun101 encrypt report.pdf --recipient <Identity_Token>")
|
|
103
|
+
print(" gun101 decrypt report.pdf.encrypted # run on your device to decrypt")
|
|
104
|
+
print()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def encrypt_command(args):
|
|
108
|
+
"""Encrypt a PDF file."""
|
|
109
|
+
try:
|
|
110
|
+
input_path = Path(args.input)
|
|
111
|
+
if not input_path.exists():
|
|
112
|
+
_error(f"File not found: {args.input}")
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
115
|
+
with open(input_path, "rb") as f:
|
|
116
|
+
data = f.read()
|
|
117
|
+
|
|
118
|
+
handler = PDFEncryptionHandler()
|
|
119
|
+
output_path = args.output if args.output else str(input_path) + ".encrypted"
|
|
120
|
+
|
|
121
|
+
if args.recipient:
|
|
122
|
+
# ── GUN101-GKP (Ghost Key Protocol) ──
|
|
123
|
+
_info("Encrypting with GUN101-GKP (Ghost Key Protocol)...")
|
|
124
|
+
_info("Recipient's device is the only one that can decrypt this file.")
|
|
125
|
+
encrypted_data = handler.encrypt_pdf_for_recipient(data, args.recipient)
|
|
126
|
+
with open(output_path, "wb") as f:
|
|
127
|
+
f.write(encrypted_data)
|
|
128
|
+
_success(f"GKP-locked file saved to {output_path}")
|
|
129
|
+
_info("Only the recipient's physical device can decrypt this file.")
|
|
130
|
+
else:
|
|
131
|
+
# ── GUN101 password mode ──
|
|
132
|
+
password = getpass.getpass(" Enter password for encryption: ")
|
|
133
|
+
confirm = getpass.getpass(" Confirm password: ")
|
|
134
|
+
if password != confirm:
|
|
135
|
+
_error("Passwords do not match.")
|
|
136
|
+
sys.exit(1)
|
|
137
|
+
|
|
138
|
+
encrypted_data = handler.encrypt_pdf(data, password)
|
|
139
|
+
with open(output_path, "wb") as f:
|
|
140
|
+
f.write(encrypted_data)
|
|
141
|
+
_success(f"Password-locked file saved to {output_path}")
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
_error(f"Encryption failed: {e}")
|
|
145
|
+
sys.exit(1)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def decrypt_command(args):
|
|
149
|
+
"""Decrypt a GUN101 or GKP-encrypted file (auto-detects mode)."""
|
|
150
|
+
try:
|
|
151
|
+
input_path = Path(args.input)
|
|
152
|
+
if not input_path.exists():
|
|
153
|
+
_error(f"File not found: {args.input}")
|
|
154
|
+
sys.exit(1)
|
|
155
|
+
|
|
156
|
+
with open(input_path, "rb") as f:
|
|
157
|
+
data = f.read()
|
|
158
|
+
|
|
159
|
+
# Auto-detect lock mode from the container
|
|
160
|
+
try:
|
|
161
|
+
container = json.loads(data.decode("utf-8"))
|
|
162
|
+
lock_mode = container.get("lock_mode", "password")
|
|
163
|
+
except Exception:
|
|
164
|
+
lock_mode = "password"
|
|
165
|
+
|
|
166
|
+
handler = PDFEncryptionHandler()
|
|
167
|
+
output_path = args.output if args.output else str(input_path).replace(
|
|
168
|
+
".encrypted", ".decrypted.pdf"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if lock_mode == security_config.GKP_LOCK_MODE:
|
|
172
|
+
# ── GUN101-GKP decryption ──
|
|
173
|
+
_info("GKP-locked file detected (Ghost Key Protocol). Using device private key...")
|
|
174
|
+
decrypted_data, metadata = handler.decrypt_pdf_identity(data)
|
|
175
|
+
with open(output_path, "wb") as f:
|
|
176
|
+
f.write(decrypted_data)
|
|
177
|
+
_success(f"Decrypted file saved to {output_path}")
|
|
178
|
+
if metadata:
|
|
179
|
+
_info(f"Metadata recovered: {metadata}")
|
|
180
|
+
else:
|
|
181
|
+
# ── GUN101 password decryption ──
|
|
182
|
+
password = getpass.getpass(" Enter password for decryption: ")
|
|
183
|
+
decrypted_data, metadata = handler.decrypt_pdf(data, password)
|
|
184
|
+
with open(output_path, "wb") as f:
|
|
185
|
+
f.write(decrypted_data)
|
|
186
|
+
_success(f"Decrypted file saved to {output_path}")
|
|
187
|
+
if metadata:
|
|
188
|
+
_info(f"Metadata recovered: {metadata}")
|
|
189
|
+
|
|
190
|
+
except ValueError as e:
|
|
191
|
+
_error(f"Decryption failed: {e}")
|
|
192
|
+
sys.exit(1)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
_error(f"An error occurred: {e}")
|
|
195
|
+
sys.exit(1)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def generate_identity_command(args):
|
|
199
|
+
"""Generate a new GKP device Identity Token."""
|
|
200
|
+
_banner()
|
|
201
|
+
try:
|
|
202
|
+
manager = IdentityManager()
|
|
203
|
+
|
|
204
|
+
passphrase = None
|
|
205
|
+
response = input(" Protect GKP private key with a passphrase? (y/N): ").strip().lower()
|
|
206
|
+
if response == "y":
|
|
207
|
+
passphrase = getpass.getpass(" Enter passphrase: ")
|
|
208
|
+
confirm = getpass.getpass(" Confirm passphrase: ")
|
|
209
|
+
if passphrase != confirm:
|
|
210
|
+
_error("Passphrases do not match.")
|
|
211
|
+
sys.exit(1)
|
|
212
|
+
|
|
213
|
+
_info("Generating RSA-4096 GKP keypair... (this may take a moment)")
|
|
214
|
+
token = manager.generate_identity(passphrase)
|
|
215
|
+
fingerprint = manager.get_identity_fingerprint(token)
|
|
216
|
+
|
|
217
|
+
_success("GKP identity generated successfully!")
|
|
218
|
+
print()
|
|
219
|
+
print(" Your GKP Identity Token (share this with senders):")
|
|
220
|
+
print(f" {token[:80]}...")
|
|
221
|
+
print()
|
|
222
|
+
print(f" Fingerprint: {fingerprint}")
|
|
223
|
+
print()
|
|
224
|
+
_info("Your private key is stored securely on this device.")
|
|
225
|
+
_info("Run 'gun101 show-identity' to see the full token anytime.")
|
|
226
|
+
|
|
227
|
+
except FileExistsError as e:
|
|
228
|
+
_error(str(e))
|
|
229
|
+
sys.exit(1)
|
|
230
|
+
except Exception as e:
|
|
231
|
+
_error(f"GKP identity generation failed: {e}")
|
|
232
|
+
sys.exit(1)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def show_identity_command(args):
|
|
236
|
+
"""Display the current device GKP Identity Token."""
|
|
237
|
+
try:
|
|
238
|
+
manager = IdentityManager()
|
|
239
|
+
token = manager.get_identity_token()
|
|
240
|
+
fingerprint = manager.get_identity_fingerprint(token)
|
|
241
|
+
|
|
242
|
+
_banner()
|
|
243
|
+
print(" Your GKP Identity Token:")
|
|
244
|
+
print(f" {token}")
|
|
245
|
+
print()
|
|
246
|
+
print(f" Fingerprint: {fingerprint}")
|
|
247
|
+
print()
|
|
248
|
+
_info("Share the token above with anyone who wants to send you a GKP-encrypted file.")
|
|
249
|
+
|
|
250
|
+
except FileNotFoundError as e:
|
|
251
|
+
_error(str(e))
|
|
252
|
+
sys.exit(1)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def reset_identity_command(args):
|
|
256
|
+
"""Delete the GKP device identity (irreversible)."""
|
|
257
|
+
_banner()
|
|
258
|
+
print(" ⚠ WARNING: This will permanently delete your GKP identity.")
|
|
259
|
+
print(" Any GKP-encrypted files targeting this identity will become")
|
|
260
|
+
print(" PERMANENTLY UNRECOVERABLE.")
|
|
261
|
+
print()
|
|
262
|
+
confirm = input(" Type 'DELETE' to confirm: ").strip()
|
|
263
|
+
if confirm != "DELETE":
|
|
264
|
+
_info("Reset cancelled.")
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
manager = IdentityManager()
|
|
268
|
+
manager.reset_identity()
|
|
269
|
+
_success("GKP identity has been deleted from this device.")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ── CLI entry point ──────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
def main():
|
|
275
|
+
parser = argparse.ArgumentParser(
|
|
276
|
+
prog="gun101",
|
|
277
|
+
description=(
|
|
278
|
+
"GUN101 PDF Encryption Protocol Suite — "
|
|
279
|
+
"Password (GUN101) · Ghost Key Protocol (GKP) · AES-256-GCM"
|
|
280
|
+
),
|
|
281
|
+
add_help=False
|
|
282
|
+
)
|
|
283
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
284
|
+
|
|
285
|
+
# gun101 help
|
|
286
|
+
help_parser = subparsers.add_parser("help", help="Show all available commands and usage")
|
|
287
|
+
help_parser.set_defaults(func=help_command)
|
|
288
|
+
|
|
289
|
+
# gun101 encrypt
|
|
290
|
+
encrypt_parser = subparsers.add_parser("encrypt", help="Encrypt a PDF file")
|
|
291
|
+
encrypt_parser.add_argument("input", help="Path to the PDF file")
|
|
292
|
+
encrypt_parser.add_argument("-o", "--output", help="Custom output file path")
|
|
293
|
+
encrypt_parser.add_argument(
|
|
294
|
+
"--recipient",
|
|
295
|
+
help="Recipient's GKP Identity Token (enables GUN101-GKP mode)",
|
|
296
|
+
default=None
|
|
297
|
+
)
|
|
298
|
+
encrypt_parser.set_defaults(func=encrypt_command)
|
|
299
|
+
|
|
300
|
+
# gun101 decrypt
|
|
301
|
+
decrypt_parser = subparsers.add_parser("decrypt", help="Decrypt a GUN101 or GKP-encrypted file")
|
|
302
|
+
decrypt_parser.add_argument("input", help="Path to the encrypted file")
|
|
303
|
+
decrypt_parser.add_argument("-o", "--output", help="Custom output file path")
|
|
304
|
+
decrypt_parser.set_defaults(func=decrypt_command)
|
|
305
|
+
|
|
306
|
+
# gun101 generate-identity
|
|
307
|
+
gen_parser = subparsers.add_parser(
|
|
308
|
+
"generate-identity", help="Generate your GKP (Ghost Key Protocol) Identity Token"
|
|
309
|
+
)
|
|
310
|
+
gen_parser.set_defaults(func=generate_identity_command)
|
|
311
|
+
|
|
312
|
+
# gun101 show-identity
|
|
313
|
+
show_parser = subparsers.add_parser("show-identity", help="Display your GKP Identity Token")
|
|
314
|
+
show_parser.set_defaults(func=show_identity_command)
|
|
315
|
+
|
|
316
|
+
# gun101 reset-identity
|
|
317
|
+
reset_parser = subparsers.add_parser(
|
|
318
|
+
"reset-identity", help="Delete your GKP identity (irreversible)"
|
|
319
|
+
)
|
|
320
|
+
reset_parser.set_defaults(func=reset_identity_command)
|
|
321
|
+
|
|
322
|
+
args = parser.parse_args()
|
|
323
|
+
|
|
324
|
+
if args.command is None:
|
|
325
|
+
help_command(args)
|
|
326
|
+
else:
|
|
327
|
+
args.func(args)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
if __name__ == "__main__":
|
|
331
|
+
main()
|
gun101/config.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration and security constants for PDF encryption layer
|
|
3
|
+
"""
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SecurityConfig:
|
|
8
|
+
"""Security configuration for PDF encryption"""
|
|
9
|
+
|
|
10
|
+
# GUN101 Protocol: Time-Based Cryptographic Challenge (umbrella protocol)
|
|
11
|
+
ENCRYPTION_PROTOCOL = "GUN101"
|
|
12
|
+
ENCRYPTION_ALGORITHM = "AES-256-GCM"
|
|
13
|
+
KEY_SIZE = 32 # 256 bits
|
|
14
|
+
|
|
15
|
+
# GUN101-GKP: Ghost Key Protocol (identity/asymmetric sub-protocol)
|
|
16
|
+
GKP_PROTOCOL = "GUN101-GKP"
|
|
17
|
+
GKP_LOCK_MODE = "GKP" # value stored in container's lock_mode field
|
|
18
|
+
|
|
19
|
+
# Argon2 key derivation parameters
|
|
20
|
+
ARGON2_TIME_COST = 4 # iterations
|
|
21
|
+
ARGON2_MEMORY_COST = 128 # MB
|
|
22
|
+
ARGON2_PARALLELISM = 4 # threads
|
|
23
|
+
ARGON2_SALT_LENGTH = 16 # bytes
|
|
24
|
+
|
|
25
|
+
# Additional security layers
|
|
26
|
+
NONCE_LENGTH = 12 # bytes for GCM mode
|
|
27
|
+
TAG_LENGTH = 16 # bytes for GCM authentication tag
|
|
28
|
+
|
|
29
|
+
# Rate limiting
|
|
30
|
+
MAX_DECRYPTION_ATTEMPTS = 5
|
|
31
|
+
LOCKOUT_DURATION = timedelta(minutes=15)
|
|
32
|
+
ATTEMPT_RESET_DURATION = timedelta(hours=1)
|
|
33
|
+
|
|
34
|
+
# Timestamp-based security
|
|
35
|
+
ENABLE_TIMESTAMP_VALIDATION = True
|
|
36
|
+
TIMESTAMP_TOLERANCE = 300 # seconds (5 minutes)
|
|
37
|
+
|
|
38
|
+
# Key stretching iterations
|
|
39
|
+
KEY_STRETCH_ITERATIONS = 100000
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Load configuration
|
|
43
|
+
security_config = SecurityConfig()
|
gun101/crypto_engine.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AES-256 GCM encryption engine
|
|
3
|
+
Provides secure encryption and decryption operations
|
|
4
|
+
"""
|
|
5
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
6
|
+
import os
|
|
7
|
+
from typing import Tuple
|
|
8
|
+
from .config import security_config
|
|
9
|
+
from .utils import generate_nonce, generate_salt
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CryptoEngine:
|
|
13
|
+
"""AES-256-GCM encryption engine"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.key_size = security_config.KEY_SIZE
|
|
17
|
+
self.nonce_length = security_config.NONCE_LENGTH
|
|
18
|
+
self.tag_length = security_config.TAG_LENGTH
|
|
19
|
+
|
|
20
|
+
def encrypt(
|
|
21
|
+
self,
|
|
22
|
+
data: bytes,
|
|
23
|
+
encryption_key: bytes,
|
|
24
|
+
associated_data: bytes = None
|
|
25
|
+
) -> Tuple[bytes, bytes, bytes]:
|
|
26
|
+
"""
|
|
27
|
+
Encrypt data using AES-256-GCM
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
data: Data to encrypt
|
|
31
|
+
encryption_key: 256-bit encryption key
|
|
32
|
+
associated_data: Optional additional authenticated data (AAD)
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Tuple of (nonce, ciphertext, tag)
|
|
36
|
+
Full encrypted data format: nonce + ciphertext + tag
|
|
37
|
+
"""
|
|
38
|
+
if len(encryption_key) != self.key_size:
|
|
39
|
+
raise ValueError(f"Key must be {self.key_size} bytes, got {len(encryption_key)}")
|
|
40
|
+
|
|
41
|
+
# Generate random nonce
|
|
42
|
+
nonce = generate_nonce(self.nonce_length)
|
|
43
|
+
|
|
44
|
+
# Create cipher
|
|
45
|
+
cipher = AESGCM(encryption_key)
|
|
46
|
+
|
|
47
|
+
# Encrypt with authentication
|
|
48
|
+
# GCM mode provides both confidentiality and authenticity
|
|
49
|
+
ciphertext = cipher.encrypt(nonce, data, associated_data)
|
|
50
|
+
|
|
51
|
+
# In GCM, the tag is appended to ciphertext by cryptography library
|
|
52
|
+
# We need to separate it for flexibility
|
|
53
|
+
actual_ciphertext = ciphertext[:-self.tag_length]
|
|
54
|
+
tag = ciphertext[-self.tag_length:]
|
|
55
|
+
|
|
56
|
+
return nonce, actual_ciphertext, tag
|
|
57
|
+
|
|
58
|
+
def decrypt(
|
|
59
|
+
self,
|
|
60
|
+
nonce: bytes,
|
|
61
|
+
ciphertext: bytes,
|
|
62
|
+
tag: bytes,
|
|
63
|
+
encryption_key: bytes,
|
|
64
|
+
associated_data: bytes = None
|
|
65
|
+
) -> bytes:
|
|
66
|
+
"""
|
|
67
|
+
Decrypt data using AES-256-GCM
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
nonce: Nonce used during encryption
|
|
71
|
+
ciphertext: Encrypted data (without tag)
|
|
72
|
+
tag: GCM authentication tag
|
|
73
|
+
encryption_key: 256-bit encryption key
|
|
74
|
+
associated_data: Optional additional authenticated data (AAD)
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Decrypted plaintext data
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
cryptography.exceptions.InvalidTag: If authentication fails
|
|
81
|
+
"""
|
|
82
|
+
if len(encryption_key) != self.key_size:
|
|
83
|
+
raise ValueError(f"Key must be {self.key_size} bytes, got {len(encryption_key)}")
|
|
84
|
+
|
|
85
|
+
cipher = AESGCM(encryption_key)
|
|
86
|
+
|
|
87
|
+
# Reconstruct ciphertext with tag for decryption
|
|
88
|
+
ciphertext_with_tag = ciphertext + tag
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
plaintext = cipher.decrypt(nonce, ciphertext_with_tag, associated_data)
|
|
92
|
+
return plaintext
|
|
93
|
+
except Exception as e:
|
|
94
|
+
raise ValueError(f"Decryption failed (possible tampering or wrong key): {str(e)}")
|
|
95
|
+
|
|
96
|
+
def encrypt_to_bytes(
|
|
97
|
+
self,
|
|
98
|
+
data: bytes,
|
|
99
|
+
encryption_key: bytes,
|
|
100
|
+
associated_data: bytes = None
|
|
101
|
+
) -> bytes:
|
|
102
|
+
"""
|
|
103
|
+
Encrypt data and return as single byte string
|
|
104
|
+
Format: nonce (12 bytes) + ciphertext + tag (16 bytes)
|
|
105
|
+
"""
|
|
106
|
+
nonce, ciphertext, tag = self.encrypt(data, encryption_key, associated_data)
|
|
107
|
+
return nonce + ciphertext + tag
|
|
108
|
+
|
|
109
|
+
def decrypt_from_bytes(
|
|
110
|
+
self,
|
|
111
|
+
encrypted_data: bytes,
|
|
112
|
+
encryption_key: bytes,
|
|
113
|
+
associated_data: bytes = None
|
|
114
|
+
) -> bytes:
|
|
115
|
+
"""
|
|
116
|
+
Decrypt data from single byte string
|
|
117
|
+
Expects format: nonce (12 bytes) + ciphertext + tag (16 bytes)
|
|
118
|
+
"""
|
|
119
|
+
nonce_length = self.nonce_length
|
|
120
|
+
tag_length = self.tag_length
|
|
121
|
+
|
|
122
|
+
if len(encrypted_data) < nonce_length + tag_length:
|
|
123
|
+
raise ValueError("Encrypted data too short")
|
|
124
|
+
|
|
125
|
+
nonce = encrypted_data[:nonce_length]
|
|
126
|
+
tag = encrypted_data[-tag_length:]
|
|
127
|
+
ciphertext = encrypted_data[nonce_length:-tag_length]
|
|
128
|
+
|
|
129
|
+
return self.decrypt(nonce, ciphertext, tag, encryption_key, associated_data)
|