Skip to content

Encryption Service

File: lenzeye_encryption_service.py (21,000+ lines)


Responsibilities

This module is the single source of truth for all cryptographic operations in Lenzeye:

  • Master key loading and validation
  • Per-user AES-256 key generation, encryption, decryption, rotation
  • File stream encryption and decryption (AES-256-CTR)
  • Multipart chunk encryption at correct byte offset
  • HMAC-SHA256 accumulation and verification
  • Stateless upload session token creation and decryption

Master Key

python def has_master_key() -> bool def get_master_key() -> bytes # 32-byte key from MASTER_KEY env var (hex-encoded)

  • Loaded from MASTER_KEY environment variable
  • Must be 64 hex characters (32 bytes = 256 bits)
  • Never stored in code or database
  • Used only to wrap/unwrap per-user keys via AES-256-GCM

Per-User Key Management

python def get_or_create_user_key(user) -> tuple[bytes, str]

  • Queries UserEncryptionKey for is_active=True row for this user
  • If none exists, generates a new 32-byte key (os.urandom(32))
  • Encrypts the key with master key using AES-256-GCM
  • Stores encrypted key in DB, returns (raw_key_bytes, key_version)
  • key_version is SHA-256 hash of the raw key (stored in S3 metadata for future decryption routing)

python def rotate_user_key(user) -> str

  • Sets existing active key to is_active=False
  • Generates a new key, stores it as active
  • Does NOT re-encrypt existing files (old key is retained for decryption)

Session Token (Stateless Upload)

python def create_upload_token(key: bytes, iv: bytes, part_size: int) -> str def decrypt_upload_token(token: str) -> tuple[bytes, bytes, int]

Token layout (plaintext, before GCM seal): 32 bytes user_key 16 bytes iv 4 bytes part_size (big-endian uint32) ────────────────── 52 bytes plaintext → ~80 bytes after AES-256-GCM (nonce + tag + ciphertext)

  • Sealed with AESGCM(master_key) — authenticated encryption
  • Nonce is prepended to output
  • Browser holds token opaquely; server decrypts with one AES-GCM operation per part
  • Zero DB reads per upload part

Multipart Chunk Encryption

python def encrypt_multipart_chunk(plaintext: bytes, key: bytes, iv: bytes, byte_offset: int) -> bytes

  • Computes the correct AES-CTR counter value for byte_offset
  • CTR counter = byte_offset // 16 (16 bytes per AES block)
  • Creates an AES-256-CTR cipher starting at this counter
  • Encrypts the chunk independently — no dependency on previous chunks
  • Returns ciphertext of same length as input

Why this matters: Part 3 can be encrypted before parts 1 and 2 are uploaded. Full parallelism.


Stream Encryption / Decryption

python def encrypt_file_stream(plaintext_stream, key: bytes) -> generator def decrypt_file_stream(ciphertext_stream, key: bytes, expected_hmac: bytes) -> generator

  • Used for single-part (non-multipart) file operations
  • Yields chunks for streaming — full file never in memory at once
  • decrypt_file_stream reads HMAC from first bytes, verifies after full stream, raises InvalidSignature on mismatch

HMAC Registry

python _hmac_registry: dict # upload_id → {"h": HMAC object, "iv": bytes} _hmac_registry_lock: threading.Lock

  • Module-level dict shared across all threads within one Gunicorn worker
  • Keyed by S3 upload_id
  • HMAC object accumulated per chunk during upload-part phase
  • On complete, HMAC is finalized and stored in S3 metadata
  • Entry is cleaned up after complete

Registry is in-process memory

The _hmac_registry only works within a single Gunicorn worker process. If workers are increased to >1, HMAC accumulation across requests breaks. This is the primary reason for the --workers=1 constraint.


TL;DR

What it does: All crypto in one file. Master key wraps per-user keys (AES-GCM). Per-user keys encrypt files (AES-256-CTR per chunk at offset). HMAC-SHA256 accumulated per chunk, verified on download.

Key techniques: AES-256-CTR (seekable, chunk-independent), AES-256-GCM (session token + key wrapping), HMAC-SHA256 (Encrypt-then-MAC), stateless session token (zero DB reads per part), in-process HMAC registry (requires single worker).