function g(id){return document.getElementById(id)} function status(t){g('status').textContent=t} const params = new URLSearchParams(location.search) const shareId = params.get('id') || location.pathname.split('/').filter(Boolean).pop() const pAuto = params.get('p') let meta=null let fileKey=null let sdebug=[] function slog(){ const t=new Date().toISOString(); const msg=[t].concat([].slice.call(arguments)).join(' '); sdebug.push(msg); if(sdebug.length>200) sdebug.shift(); const el=g('shareDebug'); if(el) el.textContent=sdebug.join('\n') } const CLIENT_VER = 'sharejs-2025-11-25-02' slog('client.ver', CLIENT_VER) slog('share.id', shareId) slog('location', location.href) async function deriveShareKey(pwd, salt){ const material = await crypto.subtle.importKey('raw', new TextEncoder().encode(pwd), 'PBKDF2', false, ['deriveKey']) return crypto.subtle.deriveKey({ name:'PBKDF2', hash:'SHA-256', salt, iterations:200000 }, material, { name:'AES-GCM', length:256 }, false, ['encrypt','decrypt']) } async function unwrapFileKey(wrappedB64, shareKey, ivB64){ const iv = new Uint8Array([...atob(ivB64)].map(c=>c.charCodeAt(0))) const ct = new Uint8Array([...atob(wrappedB64)].map(c=>c.charCodeAt(0))) const pt = await crypto.subtle.decrypt({ name:'AES-GCM', iv }, shareKey, ct) return new Uint8Array(pt) } async function deriveFileKeyFromBytes(bytes){ return crypto.subtle.importKey('raw', bytes, { name:'AES-GCM', length:256 }, false, ['decrypt']) } async function ivForChunk(saltBytes, index){ const data = new Uint8Array(saltBytes.length+4) data.set(saltBytes,0) data.set(new Uint8Array([index>>>24,(index>>>16)&255,(index>>>8)&255,index&255]), saltBytes.length) const d = await crypto.subtle.digest('SHA-256', data) return new Uint8Array(d).slice(0,12) } async function openShare(){ const pwd = g('sharePwd').value slog('open.pwd', pwd) if(!pwd){status('请输入分享密码');return} status('验证中...') const started = performance.now() const r = await fetch(`/api/psc/share/${shareId}/init`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ password: pwd }) }) let j=null try { j = await r.json() } catch(e){ j={ error:'bad_json' } } slog('open.status', r.status); slog('open.json', JSON.stringify(j)); if(j && j.error) slog('open.json.error', j.error) if(j && j.expected) slog('open.hash.expected', j.expected) if(j && j.actual) slog('open.hash.actual', j.actual) if(j && j.shareSaltB64) slog('open.shareSaltB64.len', (j.shareSaltB64||'').length) slog('open.ms', (performance.now()-started).toFixed(2)) if(!r.ok){ status('密码错误或分享不存在'); return } meta = j g('fname').textContent = j.name g('fsize').textContent = (j.size/1048576).toFixed(2)+' MB' const shareSalt = new Uint8Array([...atob(j.shareSaltB64)].map(c=>c.charCodeAt(0))) const sKey = await deriveShareKey(pwd, shareSalt) const fkBytes = await unwrapFileKey(j.wrappedKeyB64, sKey, j.ivB64) fileKey = await deriveFileKeyFromBytes(fkBytes) const fkHash = new Uint8Array(await crypto.subtle.digest('SHA-256', fkBytes)) slog('fk.hash', btoa(String.fromCharCode(...fkHash))) slog('meta.fileId', j.fileId) slog('meta.chunkCount', j.chunkCount) slog('unwrap.len', fkBytes.length) g('info').classList.remove('hidden') status('验证通过') } async function download(){ if(!meta || !fileKey){ await openShare(); if(!meta || !fileKey){ status('请先验证分享'); return } } status('开始下载') const fileSalt = new Uint8Array([...atob(meta.fileSalt)].map(c=>c.charCodeAt(0))) const chunks = new Array(meta.chunkCount) let active=0, next=0, processed=0 const maxParallel=4 async function runOne(i){ try { active++ slog('dl.req', i) const r = await fetch(`/api/psc/share/${shareId}/chunk?index=${i}`) slog('dl.http', r.status) if(!r.ok){ throw new Error('http '+r.status) } const buf = new Uint8Array(await r.arrayBuffer()) slog('dl.buf.len', buf.length) const iv = await ivForChunk(fileSalt,i) slog('dl.iv.b64', btoa(String.fromCharCode(...iv))) const pt = new Uint8Array(await crypto.subtle.decrypt({ name:'AES-GCM', iv }, fileKey, buf)) chunks[i]=pt; processed+=pt.length; status(`已下载 ${(processed/meta.size*100).toFixed(2)}%`) slog('dl.ok', i, pt.length) } catch(e){ slog('dl.err', i, e && (e.message||String(e))); status('分片下载失败') } finally { active--; schedule() } } function schedule(){ slog('dl.schedule', 'active', active, 'next', next) while(active=meta.chunkCount){ finish() } } function finish(){ const out=new Uint8Array(meta.size); let off=0; for(let i=0;iURL.revokeObjectURL(a.href), 30000); status('下载完成') } schedule() } g('btnOpen').onclick=openShare g('btnDownload').onclick=download if(pAuto){ g('sharePwd').value = pAuto; slog('auto.p', pAuto); openShare() } const btnCopy = document.getElementById('btnCopyShareDebug'); if(btnCopy){ btnCopy.onclick = async()=>{ try{ await navigator.clipboard.writeText((g('shareDebug')?.textContent)||''); status('调试信息已复制') } catch{ status('请手动复制调试信息') } } } document.addEventListener('visibilitychange', ()=>{ if(document.visibilityState==='visible'){ const inputs = Array.from(document.querySelectorAll('input,textarea')); inputs.forEach(el=>{ if(el.type==='checkbox' || el.type==='radio'){ el.checked=false } else { el.value='' } }) } })