async non-blocking encryptFileForUpload with 64KB yielding

This commit is contained in:
shum
2026-02-18 16:21:58 +00:00
parent 9e64919a6f
commit 995944cdaf
3 changed files with 51 additions and 5 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ const servers = [
parseXFTPServer("xftp://server2..."),
parseXFTPServer("xftp://server3..."),
]
const encrypted = encryptFileForUpload(fileBytes, "photo.jpg")
const encrypted = await encryptFileForUpload(fileBytes, "photo.jpg")
const {rcvDescriptions, sndDescription, uri} = await uploadFile(agent, servers, encrypted, {
onProgress: (uploaded, total) => console.log(`${uploaded}/${total}`),
})
+8 -4
View File
@@ -4,7 +4,7 @@
// file descriptions, and DEFLATE-compressed URI encoding.
import pako from "pako"
import {encryptFile, encodeFileHeader} from "./crypto/file.js"
import {encryptFileAsync, encodeFileHeader} from "./crypto/file.js"
import {generateEd25519KeyPair, encodePubKeyEd25519, encodePrivKeyEd25519, decodePrivKeyEd25519, ed25519KeyPairFromSeed} from "./crypto/keys.js"
import {sha512Streaming} from "./crypto/digest.js"
import {prepareChunkSizes, prepareChunkSpecs, getChunkDigest, fileSizeLen, authTagSize} from "./protocol/chunks.js"
@@ -79,7 +79,11 @@ export function decodeDescriptionURI(fragment: string): FileDescription {
// -- Upload
export function encryptFileForUpload(source: Uint8Array, fileName: string): EncryptedFileInfo {
export async function encryptFileForUpload(
source: Uint8Array,
fileName: string,
onProgress?: (done: number, total: number) => void
): Promise<EncryptedFileInfo> {
const key = new Uint8Array(32)
const nonce = new Uint8Array(24)
crypto.getRandomValues(key)
@@ -89,7 +93,7 @@ export function encryptFileForUpload(source: Uint8Array, fileName: string): Encr
const payloadSize = Number(fileSize) + fileSizeLen + authTagSize
const chunkSizes = prepareChunkSizes(payloadSize)
const encSize = BigInt(chunkSizes.reduce((a, b) => a + b, 0))
const encData = encryptFile(source, fileHdr, key, nonce, fileSize, encSize)
const encData = await encryptFileAsync(source, fileHdr, key, nonce, fileSize, encSize, onProgress)
const digest = sha512Streaming([encData])
console.log(`[AGENT-DBG] encrypt: encData.len=${encData.length} digest=${_dbgHex(digest, 64)} chunkSizes=[${chunkSizes.join(',')}]`)
return {encData, digest, key, nonce, chunkSizes}
@@ -229,7 +233,7 @@ async function uploadRedirectDescription(
): Promise<FileDescription> {
const yaml = encodeFileDescription(innerFd)
const yamlBytes = new TextEncoder().encode(yaml)
const enc = encryptFileForUpload(yamlBytes, "")
const enc = await encryptFileForUpload(yamlBytes, "")
const specs = prepareChunkSpecs(enc.chunkSizes)
const chunkJobs = specs.map((spec, i) => ({
+42
View File
@@ -69,6 +69,48 @@ export function encryptFile(
return concatBytes(hdr, encSource, encPad, tag)
}
// Async variant: encrypts source in 64KB slices, yielding between each to avoid blocking the main thread.
// Produces identical output to encryptFile.
const ENCRYPT_SLICE = 65536
export async function encryptFileAsync(
source: Uint8Array,
fileHdr: Uint8Array,
key: Uint8Array,
nonce: Uint8Array,
fileSize: bigint,
encSize: bigint,
onProgress?: (done: number, total: number) => void
): Promise<Uint8Array> {
const state = sbInit(key, nonce)
const lenStr = encodeInt64(fileSize)
const padLen = Number(encSize - AUTH_TAG_SIZE - fileSize - 8n)
if (padLen < 0) throw new Error("encryptFile: encSize too small")
const totalOut = Number(encSize)
const out = new Uint8Array(totalOut)
let outOff = 0
// Header (small, no yield needed)
const hdr = sbEncryptChunk(state, concatBytes(lenStr, fileHdr))
out.set(hdr, outOff); outOff += hdr.length
// Source in 64KB slices, yielding between each
for (let off = 0; off < source.length; off += ENCRYPT_SLICE) {
const end = Math.min(off + ENCRYPT_SLICE, source.length)
const enc = sbEncryptChunk(state, source.subarray(off, end))
out.set(enc, outOff); outOff += enc.length
onProgress?.(end, source.length)
await new Promise<void>(r => setTimeout(r, 0))
}
// Padding (small, no yield needed)
const padding = new Uint8Array(padLen)
padding.fill(0x23)
const encPad = sbEncryptChunk(state, padding)
out.set(encPad, outOff); outOff += encPad.length
// Auth tag
const tag = sbAuth(state)
out.set(tag, outOff)
return out
}
// -- Decryption (FileTransfer.Crypto:decryptChunks)
// Decrypt one or more XFTP chunks into a FileHeader and file content.