End-to-end encrypted, one-time file transfer powered by AES-256-GCM — zero-knowledge, self-destructing, and serverless-key architecture.
- Project Overview
- Security & Privacy Benefits
- Features & Advantages
- Installation Instructions
- php.ini Configuration
- Usage Guide
- Architecture & Diagrams
- File Structure
- License
xsukax One-Time E2EE File Transfer is a self-hosted, single-file PHP web application that enables truly private file sharing between parties. Files are encrypted entirely in the sender's browser using AES-256-GCM before any data is transmitted to the server. The server never has access to the plaintext file or the encryption key — only opaque ciphertext reaches disk.
Once a recipient enters the 6-character transfer code and downloads the file, it is immediately and permanently deleted from the server. If the file is not downloaded within 30 minutes, it is automatically purged by the cleanup routine. This guarantees a hard, one-time-use delivery model with no residual data left behind.
The entire application ships as a single file — index.php — making it trivially easy to self-host on any PHP-capable web server without external dependencies, databases, or package managers.
Security is the core design principle of this application — not an afterthought. Every layer of the stack has been hardened with deliberate, defense-in-depth choices.
All encryption and decryption is performed exclusively in the sender's and recipient's browser using the Web Crypto API. The server never sees plaintext. The encryption key is derived from the transfer code using PBKDF2 with 200,000 iterations and SHA-256, making brute-force attacks computationally prohibitive.
The transfer code acts as the sole key. The server stores only:
- The AES-256-GCM ciphertext (
.encfile) - The IV (initialization vector) and sanitized filename in a metadata file (
.meta)
The decryption key is never transmitted to, stored on, or derivable by the server. Even a complete server compromise yields only useless ciphertext.
File deletion is enforced using an exclusive file lock (LOCK_EX | LOCK_NB) before serving the download response. The encrypted file and its metadata are deleted from disk before the response is returned to the client, preventing race conditions and ensuring that concurrent download attempts are rejected with a 409 Conflict error.
A cleanup routine runs on every page load, scanning all .meta files and purging any transfers that have exceeded the 30-minute lifetime (FILE_LIFETIME = 1800 seconds). Expired files leave no trace.
Every response includes a comprehensive set of security headers:
| Header | Value |
|---|---|
Content-Security-Policy |
Restricts all resource origins to 'self', preventing XSS and data exfiltration |
X-Frame-Options |
DENY — prevents clickjacking |
X-Content-Type-Options |
nosniff — prevents MIME-sniffing attacks |
X-XSS-Protection |
1; mode=block |
Referrer-Policy |
no-referrer — suppresses referrer information |
Permissions-Policy |
Disables camera, microphone, and geolocation APIs |
A flat-file token bucket rate limiter restricts each IP to 10 requests per 60-second window. IP addresses are stored as SHA-256 hashes — never in plaintext — protecting user privacy even in server logs and rate-limit files.
The transfers/ directory is protected by a server-generated .htaccess file (Options -Indexes / Deny from all) on bootstrap, and the directory itself is created with 0700 permissions. Raw encrypted files are never accessible via direct HTTP requests.
- Transfer codes are validated against a strict regex (
^[A-Z0-9]{6}$) before any file system operation. - Uploaded base64 payloads are validated for character set, length, and post-decode size.
- The AES-GCM IV is validated to be exactly 12 bytes as required by the specification.
- Filenames are sanitized via
basename()and a character allowlist, preventing path traversal. - The transfer code character set deliberately excludes visually ambiguous characters (
I,O,0,1) to prevent transcription errors.
- Single-file deployment — the entire application is
index.php. No frameworks, no Composer, no npm. - No database required — all state is managed on the filesystem using flat files.
- True end-to-end encryption — the server is a dumb ciphertext relay; it cannot decrypt your files.
- One-time download enforcement — atomic file locking prevents replay or double-download.
- Auto-expiry — transfers self-destruct after 30 minutes regardless of download status.
- 200 MB file size support — suitable for documents, archives, images, and more.
- Zero external dependencies — uses only native PHP and the browser's built-in Web Crypto API.
- Responsive UI — clean, GitHub-inspired interface that works on desktop and mobile.
- Privacy-preserving rate limiting — IPs stored as hashes, never plaintext.
- Self-hostable — full control over your data; nothing touches third-party infrastructure.
- PHP 8.1 or higher with the following extensions enabled:
json(usually enabled by default)hash(usually enabled by default)
- A web server: Apache (with
mod_rewrite/.htaccesssupport) or Nginx - Write permissions for the web server user on the application directory
- HTTPS is strongly recommended (required in practice for the Web Crypto API in most browsers)
git clone https://github.com/xsukax/xsukax-One-Time-E2EE-File-Transfer.git
cd xsukax-One-Time-E2EE-File-TransferCopy index.php to your web server's document root or a subdirectory:
cp index.php /var/www/html/transfer/The application will automatically create the transfers/ and transfers/.rate/ directories with appropriate permissions on first run, along with a protective .htaccess file inside transfers/.
Ensure the web server user (commonly www-data on Debian/Ubuntu or apache on RHEL/CentOS) has write access to the application directory:
chown -R www-data:www-data /var/www/html/transfer/
chmod 750 /var/www/html/transfer/If using Apache, ensure .htaccess overrides are allowed in your virtual host:
<VirtualHost *:443>
ServerName transfer.yourdomain.com
DocumentRoot /var/www/html/transfer
<Directory /var/www/html/transfer>
AllowOverride All
Require all granted
</Directory>
SSLEngine on
SSLCertificateFile /etc/ssl/certs/your_cert.pem
SSLCertificateKeyFile /etc/ssl/private/your_key.pem
</VirtualHost>If using Nginx, add a deny rule for the transfers/ directory:
server {
listen 443 ssl;
server_name transfer.yourdomain.com;
root /var/www/html/transfer;
index index.php;
ssl_certificate /etc/ssl/certs/your_cert.pem;
ssl_certificate_key /etc/ssl/private/your_key.pem;
# Block direct access to the transfers directory
location ^~ /transfers/ {
deny all;
return 404;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}Navigate to https://transfer.yourdomain.com/ in your browser. You should see the xsukax One-Time E2EE File Transfer interface with the Send File and Receive File tabs. No further configuration is required.
For optimal performance and to support files up to the 200 MB application limit, the following php.ini directives must be adjusted. These can be set in your global php.ini, a per-directory .user.ini, or via ini_set() in a custom bootstrap if your host allows it.
; Allow uploaded POST bodies up to 200 MB + base64 overhead (~280 MB)
post_max_size = 280M
; Maximum individual file upload size
upload_max_filesize = 200M
; Increase memory limit to handle large file buffers in memory
memory_limit = 256M
; Allow enough time for large file encryption/upload/download cycles
max_execution_time = 120
max_input_time = 120
; Recommended: ensure sessions are not used (this app does not use sessions)
session.auto_start = 0Note: Because the application encrypts client-side and transmits the ciphertext as base64, the effective POST body is approximately 1.37× the original file size. Set
post_max_sizeto at least 280M when allowing 200 MB files.
After modifying php.ini, restart your PHP process manager:
# PHP-FPM
sudo systemctl restart php8.1-fpm
# Apache with mod_php
sudo systemctl restart apache2- Open the application in your browser and ensure you are on the Send File tab.
- Click Choose File and select the file you wish to transfer (up to 200 MB).
- Click 🔒 Encrypt & Upload.
- The browser will:
- Request a unique 6-character transfer code from the server.
- Derive an AES-256-GCM key from that code using PBKDF2 (200,000 iterations).
- Encrypt the file entirely in-browser.
- Upload only the ciphertext to the server.
- A transfer code (e.g.,
A3KX7R) is displayed. Share this code with your recipient via any channel (message, email, phone call, etc.). - The file will expire automatically in 30 minutes if not downloaded.
- Open the application and switch to the Receive File tab.
- Enter the 6-character transfer code provided by the sender.
- Click 🔓 Download & Decrypt.
- The browser will:
- Download the ciphertext from the server.
- Derive the same AES-256-GCM key from the entered code.
- Decrypt the file entirely in-browser.
- Trigger a browser download of the plaintext file.
- The encrypted file is immediately deleted from the server upon successful download.
sequenceDiagram
actor Sender
participant Browser as Sender's Browser<br/>(Web Crypto API)
participant Server as PHP Server<br/>(index.php)
participant FS as Filesystem<br/>(transfers/)
Sender->>Browser: Select file & click "Encrypt & Upload"
Browser->>Server: GET ?action=generate_code
Server-->>Browser: { "code": "A3KX7R" }
Note over Browser: PBKDF2(code, 200k rounds) → AES-256-GCM Key
Note over Browser: crypto.subtle.encrypt(key, file) → ciphertext + IV
Browser->>Server: POST action=upload<br/>code, encrypted_data (base64), iv, filename
Server->>Server: Validate code, base64, IV length, payload size
Server->>FS: Write hash(code).enc (ciphertext)
Server->>FS: Write hash(code).meta (IV, filename, timestamp)
Server-->>Browser: { "status": "uploaded", "code": "A3KX7R" }
Browser-->>Sender: Display transfer code "A3KX7R"
sequenceDiagram
actor Recipient
participant Browser as Recipient's Browser<br/>(Web Crypto API)
participant Server as PHP Server<br/>(index.php)
participant FS as Filesystem<br/>(transfers/)
Recipient->>Browser: Enter code "A3KX7R" & click "Download & Decrypt"
Browser->>Server: GET ?action=check&code=A3KX7R
Server->>FS: Verify .enc and .meta exist
Server-->>Browser: { "exists": true, "filename": "...", "size": ... }
Browser->>Server: GET ?action=download&code=A3KX7R
Server->>FS: Acquire exclusive file lock (LOCK_EX | LOCK_NB)
Server->>FS: Read ciphertext + metadata
Server->>FS: DELETE .enc, .meta, .lock (atomic one-time purge)
Server-->>Browser: { "encrypted_data": "...", "iv": "...", "filename": "..." }
Note over Browser: PBKDF2(code, 200k rounds) → AES-256-GCM Key
Note over Browser: crypto.subtle.decrypt(key, ciphertext, iv) → plaintext
Browser-->>Recipient: Browser file download (plaintext)
flowchart TD
A([Page Request]) --> B[cleanupExpiredFiles runs]
B --> C{Scan transfers/*.meta}
C --> D{Age > 30 minutes?}
D -- Yes --> E[Delete .enc + .meta + .lock]
D -- No --> F[Keep file]
E --> G([Continue handling request])
F --> G
graph LR
subgraph Client ["🖥️ Client (Browser)"]
A[Plaintext File] --> B[Web Crypto API\nAES-256-GCM Encrypt]
B --> C[Ciphertext + IV]
Z[Transfer Code] -.->|PBKDF2 key derivation| B
end
subgraph Network ["🌐 Network (HTTPS)"]
C --> D[Ciphertext transmitted\nPlaintext NEVER leaves browser]
end
subgraph ServerSide ["🖧 Server (PHP)"]
D --> E[Input Validation\nRate Limiting\nSecurity Headers]
E --> F[(transfers/\nhash-named .enc + .meta)]
F -->|One-time atomic delete| G[File Purged After Download]
F -->|30 min TTL| H[Auto-Expiry Cleanup]
end
style Client fill:#dafbe1,stroke:#2da44e
style Network fill:#ddf4ff,stroke:#0969da
style ServerSide fill:#fff8c5,stroke:#d4a72c
After the first request, the repository will produce the following structure on disk:
xsukax-One-Time-E2EE-File-Transfer/
├── index.php # Entire application (single file)
└── transfers/ # Auto-created on first run (mode 0700)
├── .htaccess # Auto-generated: blocks direct HTTP access
├── <sha256_of_code>.enc # Encrypted file ciphertext (temporary)
├── <sha256_of_code>.meta # Transfer metadata: IV, filename, timestamp
├── <sha256_of_code>.lock # Ephemeral exclusive download lock
└── .rate/ # Rate limit token buckets (IP SHA-256 hashes)
└── <sha256_of_ip>.json
This project is licensed under the GNU General Public License v3.0 — see https://www.gnu.org/licenses/gpl-3.0.html for the full license text.