/** * FileShot.io ZKE Uploader * Node.js (native fetch, Blob, FormData) * Credit by zx * Usage: node fileshot-upload.js * Sumber: https://whatsapp.com/channel/0029VbDLqe7EquiSF4STU13o/299 */ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const API = 'https://api.fileshot.io/api'; const SITE = 'https://fileshot.io'; const ZKE_CHUNK = 524288; // 512 KB — ZKE encryption chunk const UP_CHUNK = 2 * 1024 * 1024; // 2 MB — upload transport chunk const b64url = b => Buffer.from(b).toString('base64') .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); function chunkIv(base, idx) { const iv = Buffer.from(base); iv.writeUInt32BE((iv.readUInt32BE(8) + idx) >>> 0, 8); return iv; } function zkeEncrypt(buf, name, mime) { const key = crypto.randomBytes(32); const iv = crypto.randomBytes(12); const hdrObj = { v: 1, magic: 'FSZK', chunkSize: ZKE_CHUNK, fileSize: buf.length, name, mime: mime || 'application/octet-stream', iv: b64url(iv), keyMode: 'raw', kdf: null, createdAt: Date.now() }; const hj = Buffer.from(JSON.stringify(hdrObj), 'utf-8'); const lb = Buffer.alloc(4); lb.writeUInt32BE(hj.length, 0); const parts = [ Buffer.concat([Buffer.from('FSZK', 'ascii'), Buffer.from([1]), lb, hj]) ]; const n = Math.ceil(buf.length / ZKE_CHUNK); for (let i = 0; i < n; i++) { const s = i * ZKE_CHUNK; const e = Math.min(s + ZKE_CHUNK, buf.length); const c = crypto.createCipheriv( 'aes-256-gcm', key, chunkIv(iv, i), { authTagLength: 16 } ); parts.push(Buffer.concat([ c.update(buf.subarray(s, e)), c.final(), c.getAuthTag() // 16-byte GCM tag (sama dgn WebCrypto) ])); } return { blob: Buffer.concat(parts), key: b64url(key) }; } class FileShotUploader { constructor(o = {}) { this.api = o.api || API; this.site = o.site || SITE; this.token = o.authToken || null; } _h(extra = {}) { const h = { ...extra }; if (this.token) h.Authorization = `Bearer ${this.token}`; return h; } async preUpload(meta) { const r = await fetch(`${this.api}/files/pre-upload`, { method: 'POST', headers: this._h({ 'Content-Type': 'application/json' }), body: JSON.stringify(meta) }); if (!r.ok) throw new Error(`Pre-upload ${r.status}: ${await r.text()}`); return r.json(); } async uploadChunk(fid, data, idx, total) { const fd = new FormData(); fd.append('chunk', new Blob([data], { type: 'application/octet-stream' }), 'chunk.bin'); fd.append('totalChunks', String(total)); fd.append('isLastChunk', String(idx === total - 1)); const r = await fetch( `${this.api}/files/upload-chunk/${fid}/${idx}`, { method: 'POST', headers: this._h(), body: fd } ); if (!r.ok) throw new Error(`Chunk ${idx} → ${r.status}: ${await r.text()}`); return r.json(); } async finalize(fid) { const r = await fetch(`${this.api}/files/finalize-upload/${fid}`, { method: 'POST', headers: this._h({ 'Content-Type': 'application/json' }) }); if (!r.ok) throw new Error(`Finalize ${r.status}: ${await r.text()}`); return r.json(); } async status(fid) { const r = await fetch(`${this.api}/files/upload-status/${fid}`, { headers: this._h() }); if (!r.ok) throw new Error(`Status ${r.status}: ${await r.text()}`); return r.json(); } async hint(fid, sent, total) { await fetch(`${this.api}/files/upload-hint/${fid}`, { method: 'POST', headers: this._h({ 'Content-Type': 'application/json' }), body: JSON.stringify({ bytesSent: sent, totalBytes: total }) }).catch(() => {}); } async upload(fp, opts = {}) { const name = path.basename(fp); const buf = fs.readFileSync(fp); const mime = opts.mime || 'application/octet-stream'; console.log(`[*] Encrypting : ${name} (${buf.length} bytes)`); const { blob, key } = zkeEncrypt(buf, name, mime); const total = Math.ceil(blob.length / UP_CHUNK); console.log(`[*] Encrypted : ${blob.length} bytes → ${total} upload chunk(s)`); const pre = await this.preUpload({ fileName: name + '.encrypted', fileSize: blob.length, mimeType: 'application/octet-stream', isZeroKnowledge: 'true', originalFileName: name, originalFileSize: buf.length, originalMimeType: mime, chunkSize: Math.min(blob.length, UP_CHUNK), totalChunks: total, expirationHours: opts.expirationHours ?? 2160 }); const fid = pre.fileId; console.log(`[+] Pre-upload OK fileId=${fid}`); let sent = 0; for (let i = 0; i < total; i++) { const s = i * UP_CHUNK, e = Math.min(s + UP_CHUNK, blob.length); await this.uploadChunk(fid, blob.subarray(s, e), i, total); sent += e - s; await this.hint(fid, sent, blob.length); console.log(`[>] Chunk ${i + 1}/${total} uploaded (${sent} B)`); } await this.finalize(fid); let st; do { await new Promise(r => setTimeout(r, 600)); st = await this.status(fid); } while (st.uploadStatus === 'pending' || st.uploadStatus === 'uploading'); const url = `${this.site}/f/${fid}#k=${key}`; console.log(`[+] Done → ${url}`); return { fileId: fid, url, key }; } } /** const f = process.argv[2]; if (!f) { console.error('Usage: node fileshot-upload.js '); process.exit(1); } new FileShotUploader() .upload(f) .then(r => console.log('\nDownload:', r.url)) .catch(e => { console.error(`[ERROR] ${e.message}`); process.exit(1); }); **/