file.ax

hacker-grade file hosting API Contact


Browser E2E Encryption Whitepaper

Revision: 2026-03-15 · Format version 1 (AXE2E001)

Overview

E2E files are encrypted entirely in the browser before upload. The server stores only ciphertext and public metadata — it never receives the password, derived key, or plaintext content. Downloads are decrypted on the fly by a service worker, preserving normal browser download behavior (progress bar, pause/resume, survives page close).

The system is built on the Web Crypto API and uses AES-256-GCM with PBKDF2-SHA256 key derivation. Files are split into fixed-size chunks so any byte range can be decrypted independently, enabling seekable playback of encrypted media.

Source: upclass.js (upload/encrypt), e2edown/app.js (decrypt page), e2edown/sw.js (service worker).

Threat Model

The E2E layer is designed to protect file contents from the server operator and anyone who gains access to the storage backend. It is not a general-purpose secure messaging system.

Threat Protected? Detail
Server compromise / rogue operator Yes Server never sees keys or plaintext. Attacker gets ciphertext and metadata only.
Network eavesdropping beyond TLS Yes Even without TLS, transmitted bytes are AES-256-GCM ciphertext.
Disk/backup forensics Yes Stored blobs are indistinguishable without the password.
File metadata (name, size, type) No Original filename, file size, and MIME type are stored in plaintext so the decrypt UI can display them. Anyone with the file link can read this metadata without the password.
Weak / guessable password No 310,000 PBKDF2 iterations slow brute-force but cannot prevent it for weak passwords. An attacker with the ciphertext can run offline attacks.
Compromised browser No A malicious extension or XSS on the host can read keys from memory. E2E assumes a trusted browser environment.
Malicious server-served JavaScript No The encrypt/decrypt code is served by the same server. A compromised server could serve modified JS that exfiltrates keys. This is inherent to any browser-based E2E system without a separate trusted code delivery channel.

Cryptographic Design

KDFPBKDF2-SHA256, 310,000 iterations, 16-byte random salt per file
CipherAES-256-GCM, 12-byte random IV per chunk, 16-byte (128-bit) authentication tag
Chunk size1 MiB wire (1,048,576 bytes). Plaintext per chunk: 1,048,548 bytes (wire minus IV minus tag).
AAD64-byte header + 4-byte chunk index (LE) + 4-byte plaintext length (LE) = 72 bytes per chunk. Binds every chunk to the file identity, position, and expected size.
IV generationFresh crypto.getRandomValues(12) for every chunk. Never reused.
Key material lifetimeRaw key bytes are zeroed immediately after importKey. The CryptoKey object lives in the service worker for up to 7 days (absolute max) or 30 minutes idle, whichever comes first.

Each chunk decrypts independently. This enables the service worker to satisfy arbitrary HTTP Range requests by fetching and decrypting only the chunks that overlap the requested byte range. No chunk is ever decrypted twice for a single range request, and decrypted plaintext is wiped from memory after being streamed to the browser.

Wire Format

An E2E ciphertext blob is a single contiguous byte sequence with three regions:

┌──────────────────────────────────────────────────────┐
│  Header (64 bytes, plaintext)                        │
│    magic "AXE2E001" | version | IV/tag sizes | KDF   │
│    params | plain chunk size | file size | salt |    │
│    file ID | sentinel size                           │
├──────────────────────────────────────────────────────┤
│  Sentinel chunk (1,052 bytes if enabled)             │
│    [IV 12B] [encrypted sentinel 1024B] [tag 16B]     │
├──────────────────────────────────────────────────────┤
│  Data chunk 0                                        │
│    [IV 12B] [encrypted data ≤1,048,548B] [tag 16B]   │
├──────────────────────────────────────────────────────┤
│  Data chunk 1 … N                                    │
│    (same structure; last chunk may be shorter)       │
└──────────────────────────────────────────────────────┘

The header is not encrypted — it contains the parameters needed to set up decryption (KDF salt, IV/tag sizes, chunk size, etc.). It does not contain the key or password. The 16-byte File ID is random and serves only to make each header unique, preventing identical files with different passwords from producing headers that collide.

Sentinel (Password Verification)

Before downloading an entire file, the client decrypts a small sentinel chunk (1,024 bytes plaintext, ~1 KB on the wire) to verify that the password is correct. This avoids downloading potentially gigabytes of ciphertext only to discover the password was wrong.

The sentinel plaintext is the ASCII string AXE2E_SENTINEL_V1 (17 bytes) followed by 1,007 zero bytes. After decryption, the client checks that these marker bytes are present. If GCM authentication fails (tag mismatch), the password is wrong or the file is corrupted.

The sentinel is encrypted as AAD chunk index 0. When sentinel is present, file data chunks start at AAD index 1. The sentinel comparison uses constant-time byte comparison to avoid timing side-channels.

Upload Flow

Encryption happens entirely in the browser. The upload proceeds in three phases:

1. Key derivation and header construction. A 16-byte random salt and 16-byte random file ID are generated. The password is fed through PBKDF2-SHA256 (310,000 iterations) to produce a 256-bit AES key. The 64-byte header is assembled from the KDF parameters, chunk sizes, original file size, salt, and file ID.

2. Chunked upload. The header is uploaded as chunk 1 (plaintext). The sentinel is encrypted and uploaded as chunk 2. Then the file is split into plaintext chunks (~1 MiB each), each independently encrypted with a fresh random IV, and uploaded. Multiple chunks can upload concurrently — the server reassembles by chunk number.

3. Finalization. The client sends an e2eMeta object containing the KDF parameters, base64-encoded header and salt, chunk sizes, original filename, and the expected total ciphertext size. The server reassembles all chunks into a single blob, verifies that the actual file size matches the declared ciphertext size (rejects with HTTP 400 on mismatch), stores the blob, and writes the companion metadata row.

Download and Streaming Decrypt

The decrypt page (/e2edown/:token) fetches E2E metadata from the server, prompts for the password, derives the key, and hands a temporary decrypt session to a service worker.

The service worker intercepts the browser's fetch for the download URL and:

Because the service worker runs independently of the page, downloads continue even after the user navigates away or closes the tab. The browser shows a normal download progress bar with pause/resume support.

Key Handoff and Session Lifecycle

The decrypt page sends the derived CryptoKey to the service worker via postMessage on a MessageChannel. The SW stores the session in memory and optionally persists it in IndexedDB for cross-tab recovery.

Sessions expire under two rules:

While a download is active, the SW refreshes the persisted IndexedDB lease periodically to avoid mid-stream expiry on very large files. When a session expires, the CryptoKey reference is discarded and the IndexedDB record is deleted. Raw key bytes are zeroed immediately after crypto.subtle.importKey during session setup — only the non-exportable CryptoKey object is retained.

IndexedDB Fallback

When IndexedDB is unavailable (Firefox private browsing, storage quota, policy restrictions), the system falls back to passing the key, header, and ciphertext URL as URL query parameters on the service worker download route.

This significantly weakens E2E guarantees. Key material in the URL is exposed to:

The browser UI displays a warning when this fallback activates. Users should not share the URL or use the fallback on shared/monitored networks.

Inline View Safety

The service worker can serve decrypted content inline (rendered in the browser) for safe content types: images, audio, video, PDF, and plain text. Potentially dangerous formats are blocked from inline rendering and must be downloaded:

This prevents decrypted HTML or SVG from executing in the service worker's origin context, which could lead to script injection.

Storage and Compatibility

E2E files coexist with legacy (unencrypted and server-password-protected) files in the same database. The distinction is the resource_type column:

E2E-specific fields (KDF params, header, salt, original filename/type, chunk sizes) are stored in a separate e2e_file_meta table keyed by res.id with ON DELETE CASCADE — deleting a file automatically cleans up its E2E metadata.

The file manager, download pages, and APIs detect resource_type = 2 and route to the E2E decrypt flow. Legacy upload and download paths are completely unaffected.

Known Limitations