Skip to content

aryan0805/Post-Quantum-Encrypted-File-Sharing

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PQCrypt — Post-Quantum Encrypted File Sharing

AES-128-GCM built from first principles · Kyber-512 post-quantum KEM · NIST SP 800-38D validated · 196 tests

A ground-up implementation of hybrid post-quantum file encryption. Every cryptographic primitive — Galois field arithmetic, S-box generation, AES round operations, key expansion, CTR mode, GHASH, and the full GCM authentication tag — is written by hand in pure Python and verified against NIST test vectors. A Kyber-512 KEM layer wraps the AES core into a quantum-resistant hybrid scheme, and a binary file format brings the full stack to disk with a CLI.


Architecture

Four self-contained layers, each building on the one below:

┌─────────────────────────────────────────────────────────────────────┐
│                        CLI  (src/cli.py)                            │
│           keygen · encrypt · decrypt · info                         │
├─────────────────────────────────────────────────────────────────────┤
│                  File Encryption  (src/file_crypto.py)              │
│        PQC\x01 binary format · AAD-bound metadata · disk I/O        │
├─────────────────────────────────────────────────────────────────────┤
│               Hybrid Crypto  (src/hybrid_crypto.py)                 │
│         Kyber-512 key encapsulation + AES-128-GCM payload           │
├──────────────────────────────┬──────────────────────────────────────┤
│   AES-128-GCM (src/aes_gcm)  │  Kyber-512 KEM (src/pqc_wrapper)     │
│   GF(2^8) · S-box · rounds   │  keygen · encaps · decaps            │
│   CTR mode · GHASH · tag     │  800B pub · 1632B priv · 32B secret  │
└──────────────────────────────┴──────────────────────────────────────┘

Hybrid Encryption Design

Encrypting a message for a recipient requires two independent cryptographic operations running in sequence:

SENDER                                         RECIPIENT
──────                                         ─────────

recipient_pk ──────────────────────────────►  (holds secret key)

① Generate random AES-128 session key
   aes_key = urandom(16)

② Encrypt payload with AES-128-GCM
   (aes_ct, auth_tag) = AES_GCM.gcm_encrypt(
       plaintext, aes_key, nonce, aad)

③ Encapsulate via Kyber-512
   (kyber_ct, kyber_ss) = KyberKEM.encapsulate(recipient_pk)

④ Wrap the session key under the Kyber shared secret
   (enc_key, key_tag) = AES_GCM.gcm_encrypt(
       aes_key, kyber_ss[:16], kyber_nonce)

⑤ Transmit ──────────────────────────────────────────────►

   kyber_ct (768B)  |  kyber_nonce (12B)  |  enc_key (32B)
   key_tag (16B)    |  nonce (12B)        |  aes_ct (var)
   auth_tag (16B)

                              ① kyber_ss = KyberKEM.decapsulate(sk, kyber_ct)
                              ② aes_key  = AES_GCM.gcm_decrypt(enc_key, kyber_ss, ...)
                              ③ plaintext = AES_GCM.gcm_decrypt(aes_ct, aes_key, ...)

This two-layer wrapping means: quantum computers that could break classical key exchange cannot recover the session key (Kyber is ML-KEM, NIST-selected PQC standard). Any tampering with any field — at either layer — raises ValueError before a single byte of plaintext is returned.


AES-128-GCM — Implemented from Scratch

Every component is hand-written against NIST FIPS 197 and SP 800-38D:

GF(2^8) Field Arithmetic

galois_multiply(a, b)               Polynomial multiplication mod x^8+x^4+x^3+x+1 (0x11b)
gf_inverse(byte)                    Multiplicative inverse via exhaustive search

S-box Construction

           byte b
              │
              ▼
    ┌─────────────────────┐
    │  GF(2^8) inverse    │    b^(-1)  →  non-linearity
    └──────────┬──────────┘
               │
               ▼
    ┌─────────────────────┐
    │ Affine transformation│    × M + 0x63  →  breaks algebraic structure
    └──────────┬──────────┘
               │
               ▼
          S-box[b]

The 256-entry S-box and its inverse are computed at runtime from these two steps — no hardcoded lookup tables.

AES Round Operations

Operation Forward Inverse
Byte substitution sub_bytes via S-box inv_sub_bytes via inv S-box
Row permutation shift_rows left by row i inv_shift_rows right by i
Column mixing mix_columns GF(2^8) matmul inv_mix_columns
Key mixing add_round_key XOR add_round_key (symmetric)

Key Schedule

128-bit key → 11 round keys (1 initial + 10 rounds) via RotWord, SubWord, and Rcon.

GCM Mode

                           Key K
                             │
                    ┌────────▼────────┐
                    │  E(K, 0^128)    │  ──►  H  (hash subkey)
                    └─────────────────┘

Nonce (96b) ──► J0 = nonce ║ 0x00000001
                     │
           ┌─────────▼─────────────────────────────────┐
           │         CTR Mode Encryption               │
           │  J1=J0+1  J2=J0+2  J3=J0+3  ...          │
           │   E(K,J1)  E(K,J2)  E(K,J3)              │
           │    XOR      XOR      XOR                  │
           │    P[0]     P[1]     P[2]  ...            │
           └──────────────────┬────────────────────────┘
                              │ ciphertext C
                              ▼
           ┌──────────────────────────────────────────┐
           │               GHASH                      │
           │  input: pad(AAD) ║ pad(C) ║ len(A)∥len(C) │ 
           │  process: Σ block_i × H^(n-i) in GF(2^128)│
           └───────────────────┬──────────────────────┘
                               │ GHASH result
                               ▼
                 tag = GHASH ⊕ E(K, J0)    ← binds nonce to tag

File Format

On-disk binary layout for .pqc encrypted files:

Offset   Size    Field
──────   ──────  ──────────────────────────────────────────────────────
0        4 B     Magic bytes: PQC\x01  (format identifier + version)
4        2 B     filename_len  (little-endian uint16)
6        N B     filename  (UTF-8 encoded original filename)
6+N      8 B     timestamp  (little-endian uint64 Unix timestamp)
─── fixed from here ──────────────────────────────────────────────────
         768 B   kyber_ciphertext  (KEM ciphertext for key recovery)
         12 B    kyber_nonce  (AES-GCM nonce for session key wrap)
         32 B    encrypted_aes_key  (session key wrapped under Kyber ss)
         16 B    key_auth_tag  (GCM tag authenticating the wrapped key)
         12 B    nonce  (AES-GCM nonce for payload)
         M  B    aes_ciphertext  (encrypted file payload, M = original size)
         16 B    auth_tag  (GCM tag authenticating the payload)
──────────────────────────────────────────────────────────────────────
         Fixed overhead: 4+2+N+8+768+12+32+16+12+16 = 870+N bytes
         (typically 885 B for short filenames)

The filename and timestamp are bound into the AAD for both AES-GCM layers — any modification to the header fields invalidates the payload authentication tag.


NIST SP 800-38D Compliance

Both ciphertext and authentication tags match the official NIST test vectors:

Test Case Input Ciphertext match Tag match
TC1 K=0, IV=0, PT=empty ✓ (empty)
TC2 K=0, IV=0, PT=0^128 0388dace... ab6e47d4...
TC3 K=feffe992, IV=cafebabe, 64B 42831ec2... 4d5c2af3...

Key design decisions that ensure compliance:

  • Column-major ordering (order='F') for all byte ↔ matrix conversions, matching state[r][c] = bytes[r + 4c]
  • Padded GHASH input: AAD and ciphertext each padded to 16-byte boundaries, followed by a 16-byte block encoding len(AAD) ‖ len(CT) in bits
  • E(K, J0) XOR: final tag XOR'd with encryption of the initial counter J0, binding the nonce into the tag

CLI

Usage: pqcrypt [OPTIONS] COMMAND [ARGS]

Commands:
  keygen   Generate a new Kyber-512 keypair
  encrypt  Encrypt a file using the recipient's public key
  decrypt  Decrypt a file using your private key
  info     Show metadata stored in an encrypted file

Workflow:

# Generate keypairs
pqcrypt keygen --name alice
pqcrypt keygen --name bob

# Alice encrypts a file for Bob
pqcrypt encrypt report.pdf --recipient bob_public.key

# Bob decrypts it
pqcrypt decrypt report.pdf.enc --key bob_private.key

# Inspect an encrypted file (no key needed)
pqcrypt info report.pdf.enc

API

AES-128-GCM

from src.aes_gcm import AES_GCM

aes   = AES_GCM()
key   = AES_GCM.generate_random_key()    # 4x4 numpy uint8 array (128-bit)
nonce = AES_GCM.generate_random_nonce()  # 12-byte random IV

ciphertext, tag = aes.gcm_encrypt(b"Secret payload", key, nonce, aad=b"metadata")

# Raises ValueError on any authentication failure
plaintext = aes.gcm_decrypt(ciphertext, key, nonce, tag, aad=b"metadata")

Kyber-512 KEM

from src.pqc_wrapper import KyberKEM

kem = KyberKEM()
pk, sk = kem.generate_keypair()          # 800 B public, 1632 B secret

ct, ss_sender = kem.encapsulate(pk)      # sender: get ciphertext + shared secret
ss_recv = kem.decapsulate(sk, ct)        # recipient: recover shared secret
assert ss_sender == ss_recv              # both parties hold the same 32-byte key

Hybrid Encryption

from src.hybrid_crypto import HybridCrypto
from src.pqc_wrapper import KyberKEM

kem = KyberKEM()
pk, sk = kem.generate_keypair()

hc = HybridCrypto()
enc = hc.encrypt(b"Secret message", pk, aad=b"optional context")
plaintext = hc.decrypt(enc, sk, aad=b"optional context")

File Encryption

from src.file_crypto import FileEncryptor
from src.pqc_wrapper import KyberKEM

kem = KyberKEM()
pk, sk = kem.generate_keypair()

fe = FileEncryptor()
info = fe.encrypt_file("input.pdf", "output.pqc", pk)
# info: original_size, encrypted_size, overhead, timestamp

info = fe.decrypt_file("output.pqc", "recovered.pdf", sk)
# info: decrypted_size, original_filename, timestamp (ISO), encrypted_size

Test Suite — 196 Tests, 0 Failures

test_aes_gcm.py — 66 tests

Suite Tests Coverage
GF(2^128) Multiply 6 Commutativity, distributivity, identity (1<<127), zero, bounds
NIST Vectors 5 TC1 / TC2 / TC3 ciphertext; TC2 / TC3 authentication tags
Roundtrip 16 1 B to 64 KB; block-aligned; non-aligned; empty; large AAD
Edge Cases 17 Single byte; all-zero/all-FF; full byte range; nonce/key independence
Tamper Detection 17 Ciphertext; AAD; key; nonce; tag truncation/extension/flip
Performance 5 Throughput benchmarks at 16 B → 16 KB

test_hybrid_crypto.py — 75 tests

Suite Tests Coverage
KyberKEM 15 Key sizes; encaps/decaps roundtrip; implicit rejection; type checks
Hybrid Roundtrip 17 All sizes; UTF-8/binary AAD; AAD binding; 64 KB
Edge Cases 13 Empty plaintext; single byte; cross-recipient isolation; 64 KB
Tamper Detection 23 Every field; swapped tags/nonces; truncated/zeroed key; wrong sk
Performance 5 Kyber KEM overhead + hybrid throughput at 64 B → 64 KB

test_file_encryption.py — 55 tests

Suite Tests Coverage
Roundtrip 16 Empty; single byte; block-aligned; 1 MB; all-zeros; full byte range
Metadata 12 All return fields; filename preservation; size tracking; ISO timestamp
Format Validation 8 Magic bytes; wrong magic; random/empty/truncated/zeroed input
Tamper Detection 13 auth_tag; aes_ct; key_auth_tag; kyber_ct; kyber_nonce; wrong key
Error Handling 3 Nonexistent input; plaintext passed to decrypt
Performance 3 1 KB / 64 KB / 1 MB encrypt + decrypt timing

Run the full suite:

source venv/bin/activate
python3 -m unittest discover -s tests -v

Performance

This is a learning implementation — pure Python, no hardware acceleration. The bottleneck is gf128_multiply, which iterates 128 times per 16-byte block. OpenSSL uses CLMUL/AES-NI CPU instructions for the same operation.

AES-128-GCM Throughput

Payload This Implementation OpenSSL (AES-NI) Slowdown
64 B ~5 ms ~0.005 ms ~1,000×
1 KB ~60 ms ~0.050 ms ~1,200×
16 KB ~910 ms ~0.023 ms ~40,000×

Kyber-512 KEM Overhead

Operation Time
Key generation ~1.5 ms
Encapsulate ~2 ms
Decapsulate ~3 ms
Full KEM round-trip ~6 ms

At payload sizes above ~64 KB, AES-GCM dominates and KEM overhead drops below 0.1% of total time.


Project Structure

src/
  aes_gcm.py            Full AES-128-GCM implementation (~430 lines)
  pqc_wrapper.py        KyberKEM — thin wrapper around Kyber-512
  hybrid_crypto.py      HybridCrypto — two-layer Kyber + AES-GCM scheme
  file_crypto.py        FileEncryptor — PQC\x01 binary format, disk I/O
  cli.py                Click CLI — keygen / encrypt / decrypt / info

tests/
  test_aes_gcm.py          66 tests: GF math, NIST vectors, tamper detection
  test_hybrid_crypto.py    75 tests: KEM, hybrid roundtrip, tamper detection
  test_file_encryption.py  55 tests: file I/O, format, tamper detection

examples/
  demo.py               10-section walkthrough of the full stack

docs/
  DESIGN.md             Cryptographic architecture and design decisions
  LEARNINGS.md          Implementation retrospective and key insights

Dependencies

Package Used in Purpose
numpy src/aes_gcm.py 4×4 state matrix operations
kyber-py src/pqc_wrapper.py Pure-Python Kyber-512 implementation
click src/cli.py CLI argument parsing
rich src/cli.py Terminal output formatting
cryptography examples/demo.py OpenSSL baseline for benchmarks only
pip install -r requirements.txt

Repository

GitHub: https://github.com/aryan0805/p1.git

About

Post-quantum file encryption: Kyber-512 KEM + AES-128-GCM built from scratch. NIST validated, 196 tests.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages