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 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()
@@ -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)