================================================================================ OXYSecure Transfer — In-Depth Project Document ================================================================================ Product name: OXYSecure Transfer (oxy-secure-transfer) Description: Secure file encryption and transfer with AES-256-GCM, X25519, and Signal-style transport. Supports web app, CLI, and server upload/download. ================================================================================ 1. OVERVIEW ================================================================================ OXYSecure Transfer allows users to: - Generate a receiver key pair (32-byte X25519: receiver.key, receiver.pub). - Encrypt files for a receiver using their public key; only the holder of the private key can decrypt. - Upload encrypted files to a server (optional) and download/decrypt them with the receiver private key. - Perform all operations via a Next.js web app or a Node.js CLI. Design principles: - Receiver-based: encryption is bound to one receiver's public key. - No server-side key storage: server stores only opaque encrypted blobs. - Same crypto and wire format for web, CLI, and server flows. ================================================================================ 2. ARCHITECTURE ================================================================================ High-level layout: [Sender] [Server] [Receiver] | | | | plain file + | | | receiver.pub | | |------------------------->| | | | encrypt + store | | storage key | (local disk or S3) | |<-------------------------| | | | | | (share storage key out-of-band with receiver) | | | | | | key + receiver.key | | |<-----------------------------| | | decrypt, return file | | |----------------------------->| Components: - Backend (Node.js): Express server, crypto (AES, X25519, HKDF), file encrypt/decrypt, Signal-style transport, optional S3 or local disk storage. - Frontend (Next.js): Upload, Download/Decrypt, Create keys (Web Crypto X25519). OxyVault-inspired dark UI. - CLI (Node.js): keygen (with optional --timestamp for backups), encrypt-file, decrypt-file, upload, download, backup (time-based key + encrypt; optional upload), retrieve (fetch and decrypt a backup by storage key). Uses same backend crypto when run from backend directory; upload/download call backend HTTP API. Monorepo (npm workspaces): - Root: package.json (workspaces: backend, frontend). - backend/: server, CLI, crypto, file, signal, models, utils, tests. - frontend/: Next.js app (pages, components, styles). ================================================================================ 3. CRYPTOGRAPHY ================================================================================ 3.1 Algorithms -------------- - Symmetric: AES-256-GCM (12-byte IV, 16-byte auth tag). - Key agreement: X25519 (Curve25519). Raw 32-byte public and private keys. - Key derivation: HKDF-SHA256 (no salt; info string for domain separation). - Hashing: SHA-256 for file integrity (stored in package). 3.2 Key format -------------- - receiver.key: 32 bytes raw X25519 private key (binary). Optional: base64 when pasted in UI; backend/CLI accept 32-byte file or base64. - receiver.pub: 32 bytes raw X25519 public key (binary). Same acceptance. 3.3 Encryption flow (per file) ------------------------------- 1. Generate random 32-byte AES key. 2. (Optional) Hash plaintext with SHA-256; store in package for verification. 3. Encrypt file: - If size <= 4 MB: single AES-GCM (IV + tag + ciphertext). - If size > 4 MB: chunked AES-GCM (4 MB chunks, each with own IV/tag). 4. ECDH: generate ephemeral X25519 key pair; compute shared secret with receiver's public key. 5. KEK = HKDF(shared_secret, "file-key-encryption", 32). 6. Encrypt the AES key with KEK (AES-GCM); store ephemeral public key, encrypted key, IV, tag in package. 7. Encrypt metadata (filename, mimeType) with the file AES key; append to package. 8. Serialize package to binary (see EncryptedPackage layout below). 9. Transport wrap: generate another ephemeral key pair; shared secret with receiver public; transport key = HKDF(secret, "signal-transport", 32); encrypt serialized package with transport key (AES-GCM). Wire format: ephemeral_public(32) || iv(12) || tag(16) || ciphertext. 3.4 Decryption flow ------------------- 1. Unwrap transport: parse wire → ephemeral public, iv, tag, ciphertext; shared secret = X25519(receiver_private, ephemeral_public); transport key = HKDF(secret, "signal-transport", 32); decrypt → serialized package. 2. Deserialize package. 3. Recover AES key: shared secret = X25519(receiver_private, sender_ephemeral_public); KEK = HKDF(secret, "file-key-encryption", 32); decrypt encrypted_aes_key with KEK. 4. Decrypt file (single block or chunked) with AES key. 5. If file hash present: verify SHA-256 in constant time; reject if wrong. 6. Decrypt metadata with AES key; set filename/mimeType for response. ================================================================================ 4. BACKEND ================================================================================ 4.1 Directory structure ----------------------- backend/ server.js Express app, CORS, routes, storage dispatch cli.js CLI entry (keygen, backup, retrieve, encrypt-file, decrypt-file, upload, download) crypto/ aes.js AES-256-GCM encrypt/decrypt, constant-time tag compare curve25519.js X25519 key pair (Node), shared secret (@noble/curves) hkdf.js HKDF-SHA256 random.js CSPRNG, zeroize file/ encrypt.js encryptFile() — AES key, ECDH, KEK, package build decrypt.js decryptFile() — unwrap KEK, decrypt file, verify hash chunking.js 4 MB chunk encrypt/decrypt signal/ session.js encryptTransport, decryptTransport (transport wrap) transport.js wrapForTransport, unwrapFromTransport models/ encrypted_package.js serialize(), deserialize() — binary package layout utils/ encoding.js toBase64, fromBase64 hashing.js sha256 test/ Node test runner (*.test.js) uploads/ Local storage root (when USE_LOCAL_STORAGE) 4.2 Server API -------------- Base URL: default http://localhost:3001 (configurable via PORT). POST /api/upload Content-Type: multipart/form-data Body: file (file), receiverPublicKey (string, base64 of 32-byte public key) Behavior: Encrypts file for receiver, wraps for transport, stores blob. Storage key = "uploads/" + timestamp + "-" + sanitized filename. Response: JSON { key, size }. key is the storage identifier. POST /api/download Content-Type: application/json Body: { key: string, receiverPrivateKeyB64: string } Behavior: Loads blob by key, unwraps transport, decrypts, returns file with Content-Disposition and Content-Type from metadata. Response: Binary (decrypted file). POST /api/decrypt Content-Type: multipart/form-data Body: encryptedFile (file), receiverPrivateKeyB64 (string) Behavior: Same decryption as download but payload is the uploaded file (or base64-decoded if body looks like base64). Returns decrypted file. Response: Binary (decrypted file). GET /api/health Response: JSON { ok: true }. Limits - Max upload size (per request): 500 MB (multer limit for /api/upload, /api/store, /api/decrypt). JSON request body limit 1 MB (for /api/download, /api/retrieve). - Encryption: files larger than 4 MB are encrypted in 4 MB chunks (each with own IV and auth tag). Package format supports up to 1,000,000 chunks (sanity cap). - Backup: intended for smaller files (e.g. SQL dumps). Avoid very large files; full file and encrypted output are held in memory (no streaming). POST /api/store Content-Type: multipart/form-data Body: file (pre-encrypted blob, e.g. from CLI backup) Behavior: Stores blob under backups/-. No server-side decryption. Used by CLI backup --upload. Response: JSON { key, size }. key is the storage identifier (e.g. backups/...). POST /api/retrieve Content-Type: application/json Body: { key: string } (key must start with "backups/") Behavior: Returns raw stored blob (no decryption). Used by CLI retrieve. Response: Binary (encrypted blob). 4.3 Storage ----------- - Local: If USE_LOCAL_STORAGE=1 or AWS_ACCESS_KEY_ID is unset, files are written under LOCAL_STORAGE_DIR (default backend/uploads/). Key format "uploads/..." for uploads; "backups/..." for CLI backup --upload (e.g. backups/1234567890-foo.txt.encrypted). - S3: Otherwise AWS S3 (or S3-compatible endpoint via S3_ENDPOINT). Bucket from S3_BUCKET (default secure-file-transfer). Key same as above. 4.4 CLI ------- Run from backend directory: node cli.js [args]. Or from root: npm run cli -- [args] (if cli script points to backend/cli.js). keygen [prefix] [--timestamp] Generates prefix.key and prefix.pub (32 bytes each). Default prefix: receiver. With --timestamp, prefix becomes prefix-YYYYMMDD-HHMMSS (e.g. backup-20250311-143022) for time-based backup keys. backup [--out dir] [--upload] [--server URL] Generates a timestamped key pair (backup-YYYYMMDD-HHMMSS), encrypts file with that public key, writes .key, .pub, and .encrypted to cwd or --out. With --upload, POSTs encrypted blob to /api/store and prints the storage key. Use retrieve to fetch and decrypt later. Note: Not for whole-disk backups. Intended for smaller files (e.g. SQL dumps, typically under ~100 MB); large files are loaded fully into memory. Server accepts up to 500 MB per upload; keep backup files small for performance. retrieve [output_file] POSTs { key } to /api/retrieve, gets raw blob, unwraps transport and decrypts with receiver.key. Writes to output_file or stdout. Key must be a backups/... key (e.g. from backup --upload). encrypt-file Reads file, encrypts for receiver, wraps for transport, outputs base64 to stdout. decrypt-file [output_file] Reads encrypted data (file or stdin if "-"), unwraps and decrypts. Writes to output_file or stdout. Input can be raw binary or base64. upload [--server URL] Sends plain file and receiver public key to server; server encrypts and stores. Prints storage key. API_URL env or --server. download [output_file] POSTs key and receiver private key (base64) to server; server decrypts and returns file. Writes to output_file or stdout. ================================================================================ 5. FRONTEND ================================================================================ 5.1 Stack --------- Next.js 14 (pages router), React 18, lucide-react. No Tailwind; global CSS with OxyVault-inspired theme (dark panels, blue accent, slate borders). 5.2 Structure ------------- frontend/ pages/ _app.js Imports globals.css, renders Component index.js Home: hero + nav cards (Upload, Download, Create keys) upload.js Upload: file drop, receiver public key (file or paste), POST /api/upload download.js Download/Decrypt: source = server (storage key) or local encrypted file; receiver private key; POST /api/download or /api/decrypt; show text or download keys.js Create keys: Web Crypto X25519 key pair, download receiver.key / receiver.pub (private exported as PKCS#8 then raw 32 bytes extracted in JS) components/ Layout.js App shell: logo "OXY Secure Transfer", nav pills (UPLOAD, DOWNLOAD, CREATE KEYS) styles/ globals.css Variables, layout, panels, forms, buttons, upload zone 5.3 Key generation (browser) ---------------------------- Keys page uses Web Crypto API: crypto.subtle.generateKey({ name: "X25519" }, true, ["deriveBits"]). Public key exported as "raw" (32 bytes). Private key exported as "pkcs8" then parsed to extract 32-byte raw (same layout as backend) so downloaded receiver.key is compatible with backend and CLI. 5.4 Environment ---------------- NEXT_PUBLIC_API_URL: backend base URL (default http://localhost:3001). Used for /api/upload, /api/download, /api/decrypt. ================================================================================ 6. ENCRYPTED PACKAGE (BINARY LAYOUT) ================================================================================ After transport unwrap, the serialized package has this layout (see models/encrypted_package.js): version(1) | timestamp(8) | sender_ephemeral_public(32) | encrypted_aes_key_len(4) | encrypted_aes_key | aes_key_iv(12) | aes_key_tag(16) | file_iv(12) | file_tag(16) | file_size(8) | file_hash(32) | meta_len(4) | encrypted_metadata? | chunked(1) if chunked: chunk_count(4) | for each chunk: iv(12) tag(16) ct_len(4) ct else: ciphertext_len(4) | ciphertext All multi-byte integers little-endian. Version = 1. ================================================================================ 7. CONFIGURATION AND ENVIRONMENT ================================================================================ Backend ------- PORT Server port (default 3001) USE_LOCAL_STORAGE "1" to force local disk storage AWS_ACCESS_KEY_ID If set, use S3 instead of local (unless forced) AWS_REGION S3 region (default us-east-1) S3_BUCKET Bucket name (default secure-file-transfer) S3_ENDPOINT Optional S3-compatible endpoint LOCAL_STORAGE_DIR Directory for local uploads (default backend/uploads) CORS_ORIGIN Access-Control-Allow-Origin (default *) Frontend -------- NEXT_PUBLIC_API_URL Backend URL for API calls (default http://localhost:3001) CLI --- API_URL Backend URL for upload/download (default http://localhost:3001). Overridable with --server. ================================================================================ 8. TESTING ================================================================================ Backend tests (Node test runner): cd backend && npm test (runs test/**/*.test.js) Files: - crypto.aes.test.js AES-GCM encrypt/decrypt, constant-time compare - crypto.curve25519.test.js Key pair, shared secret - crypto.hkdf.test.js HKDF output length and domain separation - package.serialization.test.js serialize/deserialize roundtrip - file.encrypt.test.js encryptFile/decryptFile roundtrip, wrong key rejection ================================================================================ 9. SECURITY CONSIDERATIONS ================================================================================ - Keys: Receiver private key must never be sent to the server except in POST /api/download and POST /api/decrypt (in request body). Server does not persist it. - Integrity: File hash (SHA-256) is verified on decrypt with constant-time compare; GCM tags verified with constant-time compare. - Forward secrecy: Each encryption uses a new ephemeral key pair; only the receiver’s long-term key is reused. - Key zeroization: Ephemeral private keys and derived keys are zeroized after use in backend code where possible. - Transport: Optional double encryption (package + transport wrap) so stored blobs are encrypted for the receiver even if package format is known. ================================================================================ 10. QUICK START (RECAP) ================================================================================ From repo root: npm install npm run dev:backend # Terminal 1: backend on 3001 npm run dev:frontend # Terminal 2: Next.js on 3000 Create keys (one of): - Web: Open http://localhost:3000/keys → Generate → Download .key and .pub - CLI: cd backend && node cli.js keygen receiver Upload (web): Open /upload, choose file, add receiver.pub (file or paste), click Upload. Note the storage key. Upload (CLI): node cli.js upload receiver.pub Download (web): Open /download, enter storage key (or choose encrypted file), add receiver.key, click Decrypt. View or download result. Download (CLI): node cli.js download "" receiver.key [out] Timed backup (CLI): node cli.js backup myfile.txt --upload prints a backups/... key; later: node cli.js retrieve "backups/..." backup-*.key out. Local-only (no server): CLI encrypt-file → stdout base64; decrypt-file with that input and receiver.key. ================================================================================ End of document ================================================================================