Skip to main content

GUN101 PDF Encryption Protocol

Project description

GUN101 PDF Encryption Protocol

Python 3.9+ License: MIT Version

GUN101 is a production-ready Python library for encrypting PDF files with military-grade AES-256-GCM encryption, augmented by a nanosecond-precision time-based cryptographic challenge system. It protects your documents even when users choose weak passwords.


Table of Contents


What is GUN101?

GUN101 is a layered PDF encryption protocol built around a core insight: even if a password is stolen, the encrypted file should remain uncrackable. It does this by weaving nanosecond-precision timestamps into the encryption key itself.

Two encryption modes are supported:

Mode How it works Best for
GUN101 (password-based) User supplies a password; multi-layer key derivation hardens it Personal files, shared secrets
GUN101-GKP (Ghost Key Protocol) A random AES key is sealed with the recipient's RSA-4096 public key Targeted delivery; no password needed

Security Architecture

GUN101 stacks five independent security layers. Breaking the encryption requires defeating all of them simultaneously.

Layer 1 — Argon2 Password Hashing

The user's password is hashed with Argon2id — a memory-hard, GPU-resistant algorithm. This prevents rainbow table attacks and makes brute-force attempts extremely expensive.

Layer 2 — Dual-Pass PBKDF2 Key Derivation

The raw password is stretched through two sequential PBKDF2 passes:

  • Pass 1: PBKDF2-HMAC-SHA256 at 100,000 iterations
  • Pass 2: PBKDF2-HMAC-SHA512 at 10,000 iterations

This ensures the encryption key is deterministic (same password + salt = same key, every time) while being computationally costly to brute-force.

Layer 3 — GUN101 Time-Based Challenge

At the moment of encryption, a challenge is created from the current time captured at nanosecond precision. This challenge is salted and hashed (SHA-256), then stored alongside the ciphertext. During decryption, the challenge is re-verified — any tampering with the file causes this check to fail.

The timestamp component is also mixed into the final AES key, meaning the key varies with each encryption even if the password is identical. Reverse-engineering this requires knowing the exact nanosecond of encryption, which is impossible after the fact.

Layer 4 — AES-256-GCM Authenticated Encryption

Actual file content is encrypted with AES-256 in GCM mode. GCM provides both confidentiality (the content is hidden) and authenticity (any modification to the ciphertext is detected and rejected).

Layer 5 — Rate Limiting

Decryption attempts are tracked per file (by SHA-256 fingerprint). After 5 failed attempts, the file is locked for 15 minutes. Lockout state persists to disk in .security/lockout_log.json.


Installation

From PyPI (recommended):

pip install gun101

From source:

git clone https://github.com/25f3002130/GUN101.git
cd GUN101
pip install -e .

Requirements: Python 3.9+, cryptography>=42.0.2, argon2-cffi>=23.1.0

Note: The PyPI package name is gun101 (with hyphen), but the Python import name is gun101 (no hyphen). This is standard Python packaging practice — hyphens are not valid in Python identifiers.


Quick Start

Password-Based Encryption

from gun101 import PDFEncryptionHandler

handler = PDFEncryptionHandler()

# Read your PDF
with open("document.pdf", "rb") as f:
    pdf_data = f.read()

# Encrypt
encrypted = handler.encrypt_pdf(pdf_data, password="MySecurePassword!")

# Save encrypted file
with open("document.pdf.encrypted", "wb") as f:
    f.write(encrypted)

# Decrypt later
with open("document.pdf.encrypted", "rb") as f:
    encrypted_data = f.read()

decrypted_pdf, metadata = handler.decrypt_pdf(encrypted_data, password="MySecurePassword!")

with open("document_restored.pdf", "wb") as f:
    f.write(decrypted_pdf)

GUN101-GKP (Ghost Key Protocol)

GUN101-GKP requires no shared password. The sender uses the recipient's public GKP Identity Token; only the recipient's physical device (which holds the matching private key) can decrypt the file.

Step 1 — Recipient generates their GKP identity (one-time setup):

from gun101.identity import IdentityManager

manager = IdentityManager()
token = manager.generate_identity()   # optionally pass a passphrase
print("Share this token with senders:\n", token)

Step 2 — Sender encrypts for the recipient:

from gun101 import PDFEncryptionHandler

handler = PDFEncryptionHandler()

with open("document.pdf", "rb") as f:
    pdf_data = f.read()

recipient_token = "..."   # GKP Identity Token provided by the recipient

encrypted = handler.encrypt_pdf_for_recipient(pdf_data, recipient_token)

with open("document.pdf.encrypted", "wb") as f:
    f.write(encrypted)

Step 3 — Recipient decrypts on their device:

from gun101 import PDFEncryptionHandler

handler = PDFEncryptionHandler()

with open("document.pdf.encrypted", "rb") as f:
    encrypted_data = f.read()

decrypted_pdf, metadata = handler.decrypt_pdf_identity(encrypted_data)

with open("document_restored.pdf", "wb") as f:
    f.write(decrypted_pdf)

CLI Usage

GUN101 ships with a gun101 command-line tool installed automatically with the package.

gun101 help                                           Show all commands
gun101 encrypt <file>                                 Encrypt a PDF with a password (GUN101)
gun101 encrypt <file> --recipient <TOKEN>             Encrypt for a specific recipient (GUN101-GKP)
gun101 decrypt <file>                                 Decrypt (auto-detects GUN101 vs GKP)
gun101 generate-identity                              Generate your GKP Identity Token
gun101 show-identity                                  Display your GKP Identity Token
gun101 reset-identity                                 Delete your GKP identity (irreversible)

Options:

Flag Description
-o, --output PATH Custom output file path
--recipient TOKEN Recipient's GKP Identity Token (enables GUN101-GKP mode)

Examples:

# Password-based (GUN101) — prompted to enter and confirm the password
gun101 encrypt report.pdf
gun101 decrypt report.pdf.encrypted

# GUN101-GKP — no password needed
gun101 generate-identity
gun101 show-identity                          # copy the token and send it to the sender
gun101 encrypt report.pdf --recipient <GKP_TOKEN>
gun101 decrypt report.pdf.encrypted          # auto-detects GKP mode

Python API Reference

PDFEncryptionHandler

The main entry point for all encryption and decryption operations.

from gun101 import PDFEncryptionHandler
handler = PDFEncryptionHandler()

encrypt_pdf(pdf_data, password, metadata=None) → bytes

Encrypts a PDF using password-based GUN101. Raises ValueError on wrong password, challenge failure, tampering, or lockout.

Parameter Type Description
pdf_data bytes Raw PDF file content
password str User password
metadata dict, optional Arbitrary key-value metadata to encrypt alongside the PDF

Returns the encrypted container as bytes.

decrypt_pdf(encrypted_package, password) → tuple[bytes, dict | None]

Decrypts a password-locked GUN101 PDF.

Parameter Type Description
encrypted_package bytes Output of encrypt_pdf
password str User password

Returns (pdf_bytes, metadata_dict_or_None).

encrypt_pdf_for_recipient(pdf_data, recipient_token, metadata=None) → bytes

Encrypts a PDF using GUN101-GKP for a specific recipient. No password required. A random AES-256 key is generated and sealed using the recipient's RSA-4096 GKP Identity Token.

Parameter Type Description
pdf_data bytes Raw PDF file content
recipient_token str Recipient's GKP Identity Token
metadata dict, optional Arbitrary key-value metadata to encrypt alongside the PDF

decrypt_pdf_identity(encrypted_package, passphrase=None) → tuple[bytes, dict | None]

Decrypts a GKP-locked PDF using this device's GKP private key.

Parameter Type Description
encrypted_package bytes Output of encrypt_pdf_for_recipient
passphrase str, optional Passphrase if the GKP private key was protected at generation time

IdentityManager

Manages RSA-4096 keypairs and GKP Identity Tokens.

from gun101.identity import IdentityManager
manager = IdentityManager()
Method Description
generate_identity(passphrase=None) → str Generate a new GKP keypair; returns the public Identity Token
get_identity_token() → str Retrieve the stored GKP Identity Token
get_identity_fingerprint(token=None) → str Get a human-readable colon-separated fingerprint
has_identity() → bool Check if a GKP identity exists on this device
reset_identity() Delete the keypair from disk (irreversible)
encrypt_symmetric_key(key, token) → bytes RSA-OAEP encrypt an AES key with a recipient's GKP token
decrypt_symmetric_key(encrypted_key, passphrase=None) → bytes RSA-OAEP decrypt using local GKP private key

GKP private keys are stored at ~/.gun101/private_key.pem with 0o600 permissions (owner read/write only).


CryptoEngine

Low-level AES-256-GCM encryption primitives.

from gun101 import CryptoEngine
engine = CryptoEngine()
Method Description
encrypt(data, key, aad=None) → (nonce, ciphertext, tag) Encrypt; returns separate components
decrypt(nonce, ciphertext, tag, key, aad=None) → bytes Decrypt and verify
encrypt_to_bytes(data, key, aad=None) → bytes Encrypt to single byte string (nonce + ciphertext + tag)
decrypt_from_bytes(encrypted, key, aad=None) → bytes Decrypt from single byte string

KeyManager

Password hashing and key derivation.

from gun101 import KeyManager
km = KeyManager()
Method Description
derive_key_from_password(password, salt=None) → (key, salt) PBKDF2 dual-pass key derivation
hash_password(password) → str Argon2 hash for storage
verify_password(password, argon2_hash) → bool Verify a password against stored Argon2 hash

SecurityLayer

Rate-limiting and brute-force protection.

from gun101 import SecurityLayer
layer = SecurityLayer(max_attempts=5)
Method Description
record_attempt(identifier, success=False) → (allowed, message) Record a decryption attempt; returns whether allowed to proceed
is_locked(identifier) → bool Check if an identifier is currently locked out
get_status(identifier) → dict Get attempt count, lock status, and remaining attempts
reset_attempts(identifier) Manually reset attempt counter

Configuration

Security parameters are centralized in SecurityConfig and available via the security_config singleton:

from gun101 import security_config

print(security_config.ENCRYPTION_PROTOCOL)    # GUN101
print(security_config.GKP_PROTOCOL)           # GUN101-GKP
print(security_config.ENCRYPTION_ALGORITHM)   # AES-256-GCM
print(security_config.MAX_DECRYPTION_ATTEMPTS) # 5
print(security_config.LOCKOUT_DURATION)        # 15 minutes
Parameter Default Description
ENCRYPTION_PROTOCOL "GUN101" Outer protocol identifier (all containers)
GKP_PROTOCOL "GUN101-GKP" GKP sub-protocol identifier
GKP_LOCK_MODE "GKP" Value stored in container's lock_mode field
KEY_SIZE 32 bytes AES key length (256 bits)
ARGON2_TIME_COST 4 Argon2 iteration count
ARGON2_MEMORY_COST 128 MB Argon2 memory requirement
ARGON2_PARALLELISM 4 Argon2 thread count
KEY_STRETCH_ITERATIONS 100,000 PBKDF2 iterations (first pass)
MAX_DECRYPTION_ATTEMPTS 5 Failed attempts before lockout
LOCKOUT_DURATION 15 minutes Lockout duration
TIMESTAMP_TOLERANCE 300 seconds Tolerance window for timestamp validation

Examples

The examples/ directory contains ready-to-run scripts:

# Basic password-based encryption and decryption
python examples/basic_usage.py

# Advanced configuration and GUN101-GKP (Ghost Key Protocol)
python examples/advanced_config.py

Encrypt a real file in three lines:

from gun101 import PDFEncryptionHandler
handler = PDFEncryptionHandler()
open("out.encrypted","wb").write(handler.encrypt_pdf(open("doc.pdf","rb").read(), "password"))

Attach metadata to an encrypted file:

metadata = {"author": "Alice", "classification": "CONFIDENTIAL"}
encrypted = handler.encrypt_pdf(pdf_data, "password", metadata=metadata)

pdf, meta = handler.decrypt_pdf(encrypted, "password")
print(meta)  # {'author': 'Alice', 'classification': 'CONFIDENTIAL'}

Running Tests

pip install -e ".[dev]"
pytest
pytest --cov=gun101 --cov-report=term-missing   # with coverage

Tests cover encryption/decryption correctness, wrong-password rejection, rate limiting, GKP identity keypair operations, and GUN101 challenge verification.


Security Considerations

  • Lost password: Password-locked files cannot be recovered without the original password. There is no back door.
  • Lost GKP identity: If you delete your GKP identity (or lose the device), any GKP-encrypted files targeting that identity are permanently unrecoverable. Back up ~/.gun101/private_key.pem before resetting.
  • GKP passphrase protection: When generating a GKP identity, supply a passphrase to encrypt the private key at rest on disk.
  • Rate limiting persistence: Lockout state is written to .security/lockout_log.json in the working directory. In server deployments, ensure this path is writable and persistent.
  • Timestamp validation: GUN101 verifies that the challenge timestamp in the encrypted metadata matches the one in the container. Any tampering with either field causes decryption to fail.

Applying the GKP Rename — Files to Update

The GUN101-GKP (Ghost Key Protocol) naming was introduced after the initial release. If you are applying this rename to an existing copy of the codebase, here are every file that needs to change and exactly what to update in each.

Note on backward compatibility: The outer "protocol" field in all encrypted containers stays "GUN101". Only the lock_mode value changes from "identity""GKP". This means any files already encrypted with the old lock_mode: "identity" will fail to auto-detect correctly until pdf_handler.py and cli.py are updated. Re-encrypt important files after the update if backward compatibility matters.


src/gun101/config.py

Add two new constants to the SecurityConfig class:

# GUN101-GKP: Ghost Key Protocol (identity/asymmetric sub-protocol)
GKP_PROTOCOL = "GUN101-GKP"
GKP_LOCK_MODE = "GKP"  # value stored in the container's lock_mode field

This is the single source of truth for both strings. Every other file reads from security_config rather than hardcoding them.


src/gun101/pdf_handler.py

Three changes:

1. Module docstring — update the description of the identity-based mode to say "GUN101-GKP (Ghost Key Protocol)".

2. encrypt_pdf_for_recipient — the container dict currently writes "lock_mode": "identity". Replace with:

"sub_protocol": security_config.GKP_PROTOCOL,  # "GUN101-GKP"
"lock_mode": security_config.GKP_LOCK_MODE,     # "GKP"

Same change applies inside encryption_metadata (the dict that gets encrypted alongside the PDF).

3. decrypt_pdf and decrypt_pdf_identity — both methods check lock_mode to route or reject files. Update the string comparisons:

# decrypt_pdf — reject GKP files
if lock_mode == security_config.GKP_LOCK_MODE:   # was: == "identity"
    raise ValueError("This file is GKP-locked (Ghost Key Protocol)...")

# decrypt_pdf_identity — require GKP files
if lock_mode != security_config.GKP_LOCK_MODE:   # was: != "identity"
    raise ValueError("This file is password-locked (GUN101), not GKP-locked...")

src/gun101/identity.py

No functional changes — this file only needs its docstrings and comments updated. Rename every occurrence of "Identity Manager" / "identity-based" / "identity-locked" to use "GKP" / "Ghost Key Protocol" language. The class name IdentityManager and all method names stay the same.


src/gun101/cli.py

Two changes:

1. Auto-detect block in decrypt_command — currently compares lock_mode == "identity". Update to:

if lock_mode == security_config.GKP_LOCK_MODE:

2. User-facing text — update the banner, help strings, and info/success messages to say "GUN101-GKP" and "Ghost Key Protocol" where they currently say "identity-based" or "Identity Token". No command names change (generate-identity, show-identity, reset-identity stay as-is for CLI backward compatibility).


src/gun101/__init__.py

Add one new public constant:

__gkp_protocol__ = "GUN101-GKP"

Update the module docstring to mention both modes by name.


examples/basic_usage.py and examples/advanced_config.py

Update comments and print() strings that refer to "identity-based encryption" to say "GUN101-GKP (Ghost Key Protocol)". No functional code changes needed in the examples.


Files that do not need changes

File Reason
crypto_engine.py Pure AES-256-GCM logic; protocol-agnostic
key_manager.py Pure key derivation; no protocol references
security_layer.py Pure rate-limiting; no protocol references
utils.py Pure utility functions; no protocol references
gun101_challenge.py The challenge system is shared by both modes; no GKP-specific references
pyproject.toml Package metadata; no protocol strings
tests/ Test logic uses method names, not protocol strings — no changes needed unless your tests assert on the lock_mode string value directly

License

MIT License — see LICENSE for full terms.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gun101-1.2.0.tar.gz (30.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gun101-1.2.0-py3-none-any.whl (27.8 kB view details)

Uploaded Python 3

File details

Details for the file gun101-1.2.0.tar.gz.

File metadata

  • Download URL: gun101-1.2.0.tar.gz
  • Upload date:
  • Size: 30.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for gun101-1.2.0.tar.gz
Algorithm Hash digest
SHA256 1fde4c2c68e7cf8dfb0a51a7f9904a8e698f4c0810670ad29ca98bbfe317f2a2
MD5 181d30ba19d4fd9124c2df89030361e4
BLAKE2b-256 c12a46977939861d08f1f985f1ba64b4a02fc7b22cc1a5b4d9e9116973defe47

See more details on using hashes here.

Provenance

The following attestation bundles were made for gun101-1.2.0.tar.gz:

Publisher: publish.yml on dialga-cmd/GUN101

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file gun101-1.2.0-py3-none-any.whl.

File metadata

  • Download URL: gun101-1.2.0-py3-none-any.whl
  • Upload date:
  • Size: 27.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for gun101-1.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8d621f0741f1cf2fd810a0e365eeeb7652ed08e97921edfd0920de61cb1dec7f
MD5 d71df0b8e21ff249157aa3b422c62761
BLAKE2b-256 70716fbbc89a188a93587c254465560205cfc2f28492460165b9b2ac6dfb3672

See more details on using hashes here.

Provenance

The following attestation bundles were made for gun101-1.2.0-py3-none-any.whl:

Publisher: publish.yml on dialga-cmd/GUN101

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page