@@ -2,9 +2,7 @@ import { WORKER_CONCURRENCY } from '#consts/concurrency';
22import { HKDF_IV_STR , HKDF_SALT_STR } from '#consts/encryption' ;
33import DecryptWorker from '#workers/decrypt.worker?worker' ;
44import EncryptWorker from '#workers/encrypt.worker?worker' ;
5-
65import { ZipWriter } from '@zip.js/zip.js' ;
7-
86import {
97 CHUNK_SIZE ,
108 argon2Derive ,
@@ -16,17 +14,14 @@ import {
1614} from './encryption' ;
1715
1816const usedNames = new Map < string , number > ( ) ;
19-
2017const makeUnique = ( name : string ) => {
2118 if ( ! usedNames . has ( name ) ) {
2219 usedNames . set ( name , 1 ) ;
2320 return name ;
2421 }
25-
2622 const count = usedNames . get ( name ) || 1 ;
2723 usedNames . set ( name , count + 1 ) ;
2824
29- // Preserve extension when adding suffix
3025 const lastDot = name . lastIndexOf ( '.' ) ;
3126 if ( lastDot > 0 ) {
3227 const base = name . slice ( 0 , lastDot ) ;
@@ -37,24 +32,20 @@ const makeUnique = (name: string) => {
3732} ;
3833
3934async function deriveSecrets ( ikm : Uint8Array , password ?: string ) {
40- // Derive deterministic salt from IKM
4135 const enc = new TextEncoder ( ) ;
4236 const derivedSalt = await crypto . subtle . digest (
4337 'SHA-256' ,
4438 new Uint8Array ( [ ...ikm , ...enc . encode ( HKDF_SALT_STR ) ] )
4539 ) ;
46-
4740 let finalIKM = ikm ;
4841
49- // Mix in password if provided
5042 if ( password && password . length > 0 ) {
5143 const saltBytes = new Uint8Array ( derivedSalt ) . slice ( 0 , 16 ) ;
5244 const passwordBytes = new TextEncoder ( ) . encode ( password ) ;
5345 const pb = await argon2Derive ( passwordBytes , saltBytes , 32 , 16384 , 32 , 1 ) ;
5446 finalIKM = xorBytes ( ikm , pb ) ;
5547 }
5648
57- // Derive AES key and base IV
5849 const hkdfSalt = new Uint8Array (
5950 await crypto . subtle . digest ( 'SHA-256' , new Uint8Array ( [ ...finalIKM , ...enc . encode ( 'aes-key' ) ] ) )
6051 ) . slice ( 0 , 16 ) ;
@@ -102,7 +93,6 @@ async function handleWorkerEncryptedMessage(ctx: EncryptionContext, data: any):
10293 ctx . processedTotal += sz ;
10394 if ( ctx . onProgress ) ctx . onProgress ( ctx . processedTotal , ctx . originalSize ) ;
10495 }
105-
10696 if ( ctx . streamEnded && ctx . pendingCount === 0 ) {
10797 if ( ctx . allDoneResolve ) ctx . allDoneResolve ( ) ;
10898 if ( ctx . onProgress ) ctx . onProgress ( ctx . originalSize ?? ctx . processedTotal , ctx . originalSize ) ;
@@ -120,14 +110,28 @@ async function initializeEncryptionWorkers(
120110) : Promise < void > {
121111 try {
122112 const keyRaw = await crypto . subtle . exportKey ( 'raw' , aesKey ) ;
113+ const readyPromises : Promise < void > [ ] = [ ] ;
114+
123115 for ( let i = 0 ; i < concurrency ; i ++ ) {
124116 const w = new EncryptWorker ( ) ;
125- w . onmessage = ( ev ) => handleWorkerEncryptedMessage ( ctx , ev . data ) ;
117+ const readyPromise = new Promise < void > ( ( resolve , reject ) => {
118+ w . onmessage = ( ev ) => {
119+ if ( ev . data ?. type === 'ready' ) {
120+ resolve ( ) ;
121+ } else {
122+ handleWorkerEncryptedMessage ( ctx , ev . data ) ;
123+ }
124+ } ;
125+ w . onerror = ( ) => reject ( new Error ( 'Worker failed during initialization' ) ) ;
126+ } ) ;
127+ readyPromises . push ( readyPromise ) ;
128+
126129 ctx . workers . push ( w ) ;
127130 const keyCopy = keyRaw . slice ( 0 ) ;
128131 const ivCopy = baseIv . buffer . slice ( 0 ) ;
129132 w . postMessage ( { type : 'init' , keyRaw : keyCopy , baseIv : ivCopy } , [ keyCopy , ivCopy ] ) ;
130133 }
134+ await Promise . all ( readyPromises ) ;
131135 } catch ( e ) {
132136 ctx . workers . length = 0 ;
133137 await handleEncryptionError ( ctx , e ) ;
@@ -231,7 +235,6 @@ async function handleWorkerDecryptedMessage(ctx: DecryptionContext, data: any):
231235 ctx . decryptedMap . delete ( ctx . nextToEnqueue ) ;
232236 ctx . controllerRef ! . enqueue ( arr ) ;
233237 ctx . nextToEnqueue ++ ;
234-
235238 ctx . processedTotal += arr . byteLength ;
236239 if ( ctx . onProgress ) ctx . onProgress ( ctx . processedTotal , ctx . originalSize ) ;
237240 }
@@ -254,14 +257,28 @@ async function initializeDecryptionWorkers(
254257) : Promise < void > {
255258 try {
256259 const keyRaw = await crypto . subtle . exportKey ( 'raw' , aesKey ) ;
260+ const readyPromises : Promise < void > [ ] = [ ] ;
261+
257262 for ( let i = 0 ; i < concurrency ; i ++ ) {
258263 const w = new DecryptWorker ( ) ;
259- w . onmessage = ( ev ) => handleWorkerDecryptedMessage ( ctx , ev . data ) ;
264+ const readyPromise = new Promise < void > ( ( resolve , reject ) => {
265+ w . onmessage = ( ev ) => {
266+ if ( ev . data ?. type === 'ready' ) {
267+ resolve ( ) ;
268+ } else {
269+ handleWorkerDecryptedMessage ( ctx , ev . data ) ;
270+ }
271+ } ;
272+ w . onerror = ( ) => reject ( new Error ( 'Worker failed during initialization' ) ) ;
273+ } ) ;
274+ readyPromises . push ( readyPromise ) ;
275+
260276 ctx . workers . push ( w ) ;
261277 const keyCopy = keyRaw . slice ( 0 ) ;
262278 const ivCopy = baseIv . buffer . slice ( 0 ) ;
263279 w . postMessage ( { type : 'init' , keyRaw : keyCopy , baseIv : ivCopy } , [ keyCopy , ivCopy ] ) ;
264280 }
281+ await Promise . all ( readyPromises ) ;
265282 } catch ( e ) {
266283 ctx . workers . length = 0 ;
267284 await handleDecryptionError ( ctx , e ) ;
@@ -303,7 +320,6 @@ async function decryptChunkFallback(
303320 ctx . decryptedMap . delete ( ctx . nextToEnqueue ) ;
304321 ctx . controllerRef . enqueue ( arr ) ;
305322 ctx . nextToEnqueue ++ ;
306-
307323 ctx . processedTotal += arr . byteLength ;
308324 if ( ctx . onProgress ) ctx . onProgress ( ctx . processedTotal , ctx . originalSize ) ;
309325 }
@@ -342,17 +358,14 @@ async function writeZipFiles(
342358 for ( const file of files ) {
343359 let filename = ( file as any ) . relativePath || file . name ;
344360 filename = makeUnique ( filename ) ;
345-
346361 try {
347362 await zipWriter . add ( filename , file . stream ( ) , {
348363 password : password ?. length ? password : undefined ,
349- // prefer strongest WinZip AES (1..3 => 128,192,256). Use 3 for AES-256 compatibility.
350364 encryptionStrength : password ?. length ? 3 : undefined ,
351365 level : 9 ,
352366 signal
353367 } ) ;
354368 } catch ( err : any ) {
355- // If the writer reports an existing file, try to generate another unique name and retry once
356369 const msg = String ( err ?. message || err || '' ) ;
357370 if ( msg . includes ( 'File already exists' ) || msg . includes ( 'already exists' ) ) {
358371 const altName = makeUnique ( ( file as any ) . relativePath || file . name ) ;
@@ -367,7 +380,6 @@ async function writeZipFiles(
367380 }
368381 }
369382 }
370-
371383 await zipWriter . close ( ) ;
372384 } catch ( error ) {
373385 console . error ( 'Error creating zip stream:' , error ) ;
@@ -389,11 +401,10 @@ export async function createZipStream(
389401 bufferedWrite : true ,
390402 useCompressionStream : true
391403 } ) ;
392-
393404 writeZipFiles ( zipWriter , writable , files , password , signal ) ;
394-
395405 return readable ;
396406}
407+
397408export async function createEncryptedStream (
398409 inputStream : ReadableStream < Uint8Array > ,
399410 password ?: string ,
@@ -403,7 +414,6 @@ export async function createEncryptedStream(
403414) {
404415 const ikm = ikm_override ?? crypto . getRandomValues ( new Uint8Array ( 32 ) ) ;
405416 const { aesKey, baseIv } = await deriveSecrets ( ikm , password ) ;
406-
407417 const chunks : Uint8Array [ ] = [ ] ;
408418 let bufferedBytes = 0 ;
409419 let chunkIndex = 0 ;
@@ -432,31 +442,26 @@ export async function createEncryptedStream(
432442 while ( offset < size ) {
433443 const first = chunks [ 0 ] ;
434444 const take = Math . min ( first . length , size - offset ) ;
435-
436445 out . set ( first . subarray ( 0 , take ) , offset ) ;
437446
438447 if ( take === first . length ) {
439448 chunks . shift ( ) ;
440449 } else {
441450 chunks [ 0 ] = first . subarray ( take ) ;
442451 }
443-
444452 offset += take ;
445453 bufferedBytes -= take ;
446454 }
447-
448455 return out ;
449456 }
450457
451458 const transformer = new TransformStream < Uint8Array , Uint8Array > ( {
452459 async start ( controller ) {
453460 ctx . controllerRef = controller ;
454-
455461 allDonePromise = new Promise < void > ( ( res , rej ) => {
456462 ctx . allDoneResolve = res ;
457463 ctx . allDoneReject = rej ;
458464 } ) ;
459-
460465 await initializeEncryptionWorkers ( ctx , aesKey , baseIv , WORKER_CONCURRENCY ) ;
461466 } ,
462467
@@ -467,9 +472,7 @@ export async function createEncryptedStream(
467472 while ( bufferedBytes >= CHUNK_SIZE ) {
468473 const chunkData = readChunk ( CHUNK_SIZE ) ;
469474 const index = chunkIndex ++ ;
470-
471475 ctx . chunkSizes . set ( index , chunkData . byteLength ) ;
472-
473476 await assignEncryptionChunk ( ctx , index , chunkData , aesKey , baseIv ) ;
474477 }
475478 } ,
@@ -478,18 +481,13 @@ export async function createEncryptedStream(
478481 if ( bufferedBytes > 0 || chunkIndex === 0 ) {
479482 const chunkData = readChunk ( bufferedBytes ) ;
480483 const index = chunkIndex ++ ;
481-
482484 ctx . chunkSizes . set ( index , chunkData . byteLength ) ;
483-
484485 await assignEncryptionChunk ( ctx , index , chunkData , aesKey , baseIv ) ;
485486 }
486-
487487 ctx . streamEnded = true ;
488-
489488 if ( ctx . pendingCount > 0 ) {
490489 await allDonePromise ;
491490 }
492-
493491 try {
494492 for ( const w of ctx . workers ) w . terminate ( ) ;
495493 } catch { }
@@ -511,7 +509,6 @@ export async function createDecryptedStream(
511509) {
512510 const ikm = base64urlToBytes ( keySecret ) ;
513511 const { aesKey, baseIv } = await deriveSecrets ( ikm , password ) ;
514-
515512 const reader = inputStream . getReader ( ) ;
516513 let buffer = new Uint8Array ( 0 ) ;
517514
@@ -584,7 +581,6 @@ export async function createDecryptedStream(
584581 ctx . decryptedMap . delete ( ctx . nextToEnqueue ) ;
585582 controller . enqueue ( arr ) ;
586583 ctx . nextToEnqueue ++ ;
587-
588584 ctx . processedTotal += arr . byteLength ;
589585 if ( ctx . onProgress ) ctx . onProgress ( ctx . processedTotal , ctx . originalSize ) ;
590586 }
@@ -619,8 +615,6 @@ export function createMultipartStream(
619615 fileStream : ReadableStream < Uint8Array >
620616) : ReadableStream < Uint8Array > {
621617 const encoder = new TextEncoder ( ) ;
622-
623- // Construct preamble
624618 const preAmbleParts : Uint8Array [ ] = [ ] ;
625619 for ( const [ key , value ] of Object . entries ( fields ) ) {
626620 preAmbleParts . push ( encoder . encode ( `--${ boundary } \r\n` ) ) ;
@@ -648,7 +642,7 @@ export function createMultipartStream(
648642 if ( preambleIndex < preAmbleParts . length ) {
649643 controller . enqueue ( preAmbleParts [ preambleIndex ] ) ;
650644 preambleIndex ++ ;
651- return ; // Yield to event loop
645+ return ;
652646 } else {
653647 state = 'file' ;
654648 fileReader = fileStream . getReader ( ) ;
0 commit comments