Files
simplexmq/xftp-web/web/crypto-backend.ts
T
2026-06-06 09:04:26 +01:00

141 lines
4.9 KiB
TypeScript

import type {FileHeader} from '../src/crypto/file.js'
import {t} from './i18n.js'
export interface CryptoBackend {
encrypt(data: Uint8Array, fileName: string,
onProgress?: (done: number, total: number) => void
): Promise<EncryptResult>
readChunk(offset: number, size: number): Promise<Uint8Array>
decryptAndStoreChunk(
dhSecret: Uint8Array, nonce: Uint8Array,
body: Uint8Array, digest: Uint8Array, chunkNo: number
): Promise<void>
verifyAndDecrypt(params: {size: number, digest: Uint8Array, key: Uint8Array, nonce: Uint8Array},
onProgress?: (done: number, total: number) => void
): Promise<{header: FileHeader, content: Uint8Array}>
cleanup(): Promise<void>
}
export interface EncryptResult {
digest: Uint8Array
key: Uint8Array
nonce: Uint8Array
chunkSizes: number[]
}
type PendingRequest = {resolve: (value: any) => void, reject: (reason: any) => void}
class WorkerBackend implements CryptoBackend {
private worker: Worker
private pending = new Map<number, PendingRequest>()
private nextId = 1
private progressCb: ((done: number, total: number) => void) | null = null
private ready: Promise<void>
constructor() {
this.worker = new Worker(new URL('./crypto.worker.ts', import.meta.url), {type: 'module'})
let rejectReady: (e: Error) => void
this.ready = new Promise((resolve, reject) => {
rejectReady = reject
this.worker.onmessage = (e) => {
if (e.data?.type === 'ready') {
this.worker.onmessage = (e) => this.handleMessage(e.data)
resolve()
} else {
reject(new Error('Worker: unexpected first message'))
}
}
})
this.worker.onerror = (e) => {
rejectReady(new Error('Worker failed to load: ' + e.message))
for (const p of this.pending.values()) p.reject(new Error('Worker error: ' + e.message))
this.pending.clear()
}
}
private handleMessage(msg: {id: number, type: string, [k: string]: any}) {
if (msg.type === 'progress') {
this.progressCb?.(msg.done, msg.total)
return
}
const p = this.pending.get(msg.id)
if (!p) return
this.pending.delete(msg.id)
if (msg.type === 'error') {
p.reject(new Error(msg.message))
} else {
p.resolve(msg)
}
}
private async send(msg: Record<string, any>, transfer?: Transferable[]): Promise<any> {
await this.ready
const id = this.nextId++
return new Promise((resolve, reject) => {
this.pending.set(id, {resolve, reject})
this.worker.postMessage({...msg, id}, transfer ?? [])
})
}
private toTransferable(data: Uint8Array): ArrayBuffer {
if (data.byteOffset !== 0 || data.byteLength !== data.buffer.byteLength) {
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer
}
return data.buffer as ArrayBuffer
}
async encrypt(data: Uint8Array, fileName: string,
onProgress?: (done: number, total: number) => void): Promise<EncryptResult> {
this.progressCb = onProgress ?? null
const buf = this.toTransferable(data)
const resp = await this.send({type: 'encrypt', data: buf, fileName}, [buf])
this.progressCb = null
return {digest: resp.digest, key: resp.key, nonce: resp.nonce, chunkSizes: resp.chunkSizes}
}
async readChunk(offset: number, size: number): Promise<Uint8Array> {
const resp = await this.send({type: 'readChunk', offset, size})
return new Uint8Array(resp.data)
}
async decryptAndStoreChunk(
dhSecret: Uint8Array, nonce: Uint8Array,
body: Uint8Array, digest: Uint8Array, chunkNo: number
): Promise<void> {
// Copy arrays to ensure clean ArrayBuffer separation before worker transfer
// nonce/dhSecret may be subarrays sharing buffer with body
const dhSecretCopy = new Uint8Array(dhSecret)
const nonceCopy = new Uint8Array(nonce)
const digestCopy = new Uint8Array(digest)
const buf = this.toTransferable(body)
await this.send(
{type: 'decryptAndStoreChunk', dhSecret: dhSecretCopy, nonce: nonceCopy, body: buf, chunkDigest: digestCopy, chunkNo},
[buf]
)
}
async verifyAndDecrypt(params: {size: number, digest: Uint8Array, key: Uint8Array, nonce: Uint8Array},
onProgress?: (done: number, total: number) => void
): Promise<{header: FileHeader, content: Uint8Array}> {
this.progressCb = onProgress ?? null
const resp = await this.send({
type: 'verifyAndDecrypt',
size: params.size, digest: params.digest, key: params.key, nonce: params.nonce
})
this.progressCb = null
return {header: resp.header, content: new Uint8Array(resp.content)}
}
async cleanup(): Promise<void> {
await this.send({type: 'cleanup'})
this.worker.terminate()
}
}
export function createCryptoBackend(): CryptoBackend {
if (typeof Worker === 'undefined') {
throw new Error(t('workersRequired', 'Web Workers required \u2014 update your browser'))
}
return new WorkerBackend()
}