neozipkit 0.7.2 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +57 -24
  2. package/WHATS_NEW.md +114 -0
  3. package/dist/browser/zstd-browser-shim.d.ts +28 -0
  4. package/dist/browser/zstd-browser-shim.d.ts.map +1 -0
  5. package/dist/browser/zstd-browser-shim.js +66 -0
  6. package/dist/browser/zstd-browser-shim.js.map +1 -0
  7. package/dist/browser-esm/index.js +1786 -41737
  8. package/dist/browser-esm/index.js.map +4 -4
  9. package/dist/browser-umd/index.js +1786 -41737
  10. package/dist/browser-umd/index.js.map +4 -4
  11. package/dist/browser-umd/index.min.js +3 -21
  12. package/dist/core/ZipCompress.d.ts +2 -7
  13. package/dist/core/ZipCompress.d.ts.map +1 -1
  14. package/dist/core/ZipCompress.js +45 -22
  15. package/dist/core/ZipCompress.js.map +1 -1
  16. package/dist/core/ZipDecompress.d.ts +1 -3
  17. package/dist/core/ZipDecompress.d.ts.map +1 -1
  18. package/dist/core/ZipDecompress.js +14 -9
  19. package/dist/core/ZipDecompress.js.map +1 -1
  20. package/dist/core/encryption/AesCrypto.d.ts +55 -11
  21. package/dist/core/encryption/AesCrypto.d.ts.map +1 -1
  22. package/dist/core/encryption/AesCrypto.js +223 -68
  23. package/dist/core/encryption/AesCrypto.js.map +1 -1
  24. package/dist/core/encryption/index.d.ts +1 -1
  25. package/dist/core/encryption/index.d.ts.map +1 -1
  26. package/dist/core/encryption/index.js +3 -1
  27. package/dist/core/encryption/index.js.map +1 -1
  28. package/dist/core/version.js +1 -1
  29. package/dist/node/LegacyZstd.d.ts +67 -0
  30. package/dist/node/LegacyZstd.d.ts.map +1 -0
  31. package/dist/node/LegacyZstd.js +209 -0
  32. package/dist/node/LegacyZstd.js.map +1 -0
  33. package/dist/node/ZipCompressNode.d.ts +9 -7
  34. package/dist/node/ZipCompressNode.d.ts.map +1 -1
  35. package/dist/node/ZipCompressNode.js +85 -46
  36. package/dist/node/ZipCompressNode.js.map +1 -1
  37. package/dist/node/ZipDecompressNode.d.ts +7 -3
  38. package/dist/node/ZipDecompressNode.d.ts.map +1 -1
  39. package/dist/node/ZipDecompressNode.js +51 -55
  40. package/dist/node/ZipDecompressNode.js.map +1 -1
  41. package/dist/node/ZipkitNode.d.ts +14 -0
  42. package/dist/node/ZipkitNode.d.ts.map +1 -1
  43. package/dist/node/ZipkitNode.js +130 -39
  44. package/dist/node/ZipkitNode.js.map +1 -1
  45. package/dist/node/ZstdNode.d.ts +39 -0
  46. package/dist/node/ZstdNode.d.ts.map +1 -0
  47. package/dist/node/ZstdNode.js +171 -0
  48. package/dist/node/ZstdNode.js.map +1 -0
  49. package/dist/node/index.d.ts +3 -0
  50. package/dist/node/index.d.ts.map +1 -1
  51. package/dist/node/index.js +11 -1
  52. package/dist/node/index.js.map +1 -1
  53. package/package.json +10 -10
  54. package/src/browser/zstd-browser-shim.ts +76 -0
  55. package/src/core/ZipCompress.ts +57 -29
  56. package/src/core/ZipDecompress.ts +21 -10
  57. package/src/core/encryption/AesCrypto.ts +265 -86
  58. package/src/core/encryption/index.ts +1 -1
  59. package/src/core/version.ts +1 -1
  60. package/src/node/LegacyZstd.ts +270 -0
  61. package/src/node/ZipCompressNode.ts +107 -55
  62. package/src/node/ZipDecompressNode.ts +80 -66
  63. package/src/node/ZipkitNode.ts +150 -40
  64. package/src/node/ZstdNode.ts +155 -0
  65. package/src/node/index.ts +10 -0
  66. package/dist/core/ZstdManager.d.ts +0 -56
  67. package/dist/core/ZstdManager.d.ts.map +0 -1
  68. package/dist/core/ZstdManager.js +0 -144
  69. package/dist/core/ZstdManager.js.map +0 -1
  70. package/src/core/ZstdManager.ts +0 -165
@@ -14,7 +14,7 @@
14
14
  //
15
15
 
16
16
  const pako = require('pako');
17
- import { ZstdManager } from './ZstdManager';
17
+ import { ZstdNode } from '../node/ZstdNode';
18
18
  import Zipkit from './Zipkit';
19
19
  import { Logger } from './components/Logger';
20
20
  import ZipEntry from './ZipEntry';
@@ -275,13 +275,8 @@ export class ZipCompress {
275
275
  }
276
276
 
277
277
  /**
278
- * Compress data using Zstandard (zstd) algorithm
279
- * @param input - Input data to compress (Buffer or chunked reader)
280
- * @param options - Compression options
281
- * @param bufferSize - Buffer size for chunked processing
282
- * @param entry - ZIP entry being compressed
283
- * @param onOutputBuffer - Optional callback for streaming output
284
- * @returns Buffer containing compressed data
278
+ * Compress data using Zstandard via Node native zlib chunked streaming.
279
+ * Not available in browser builds (throws).
285
280
  */
286
281
  async zstdCompress(
287
282
  input: Buffer | { totalSize: number, readChunk: (position: number, size: number) => Buffer },
@@ -291,33 +286,66 @@ export class ZipCompress {
291
286
  onOutputBuffer?: (data: Buffer) => Promise<void>
292
287
  ): Promise<Buffer> {
293
288
  this.log(`zstdCompress() called - entry: ${entry?.filename ?? 'unknown'}, bufferSize: ${bufferSize}, level: ${options?.level ?? 6}`);
294
-
289
+
295
290
  const effectiveBufferSize = bufferSize || this.zipkit.getBufferSize();
296
291
  const level = options?.level ?? 6;
297
-
298
- // Handle chunked reader
299
- if (typeof input === 'object' && 'totalSize' in input && 'readChunk' in input) {
300
- // Chunked reader mode - not implemented in simplified version
301
- throw new Error('Chunked reader mode not supported in ZipCompress');
302
- }
303
-
304
- // Validate input
305
- if (!input || input.length === 0) {
292
+ const isBuffer = Buffer.isBuffer(input);
293
+ const totalSize = isBuffer ? input.length : input.totalSize;
294
+
295
+ if (!totalSize) {
306
296
  throw new Error('ZSTD compression: empty input buffer');
307
297
  }
308
-
309
- // Convert Buffer to Uint8Array for WASM module
310
- const inputArray = new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
311
-
312
- // Use global ZstdManager for compression (handles queuing and initialization)
313
- const compressed = await ZstdManager.compress(inputArray, level);
314
- const compressedBuffer = Buffer.from(compressed);
315
-
298
+
299
+ if (options?.level === 0) {
300
+ if (isBuffer) {
301
+ if (onOutputBuffer) {
302
+ await onOutputBuffer(input);
303
+ }
304
+ return input;
305
+ }
306
+ const chunks: Buffer[] = [];
307
+ let position = 0;
308
+ while (position < totalSize) {
309
+ const size = Math.min(effectiveBufferSize, totalSize - position);
310
+ const chunk = input.readChunk(position, size);
311
+ if (onOutputBuffer) {
312
+ await onOutputBuffer(chunk);
313
+ } else {
314
+ chunks.push(chunk);
315
+ }
316
+ position += chunk.length;
317
+ }
318
+ return onOutputBuffer ? Buffer.alloc(0) : Buffer.concat(chunks);
319
+ }
320
+
321
+ const readChunk = isBuffer
322
+ ? (position: number, size: number) => input.subarray(position, position + size)
323
+ : input.readChunk.bind(input);
324
+
325
+ const collected: Buffer[] = [];
326
+ const compressedSize = await ZstdNode.compressChunks(
327
+ readChunk,
328
+ totalSize,
329
+ effectiveBufferSize,
330
+ level,
331
+ undefined,
332
+ async (compressed) => {
333
+ if (onOutputBuffer) {
334
+ await onOutputBuffer(compressed);
335
+ } else {
336
+ collected.push(compressed);
337
+ }
338
+ }
339
+ );
340
+
341
+ if (entry) {
342
+ entry.compressedSize = compressedSize;
343
+ }
344
+
316
345
  if (onOutputBuffer) {
317
- await onOutputBuffer(compressedBuffer);
346
+ return Buffer.alloc(0);
318
347
  }
319
-
320
- return compressedBuffer;
348
+ return Buffer.concat(collected);
321
349
  }
322
350
 
323
351
  /**
@@ -14,7 +14,8 @@
14
14
  //
15
15
 
16
16
  const pako = require('pako');
17
- import { ZstdManager } from './ZstdManager';
17
+ import { ZstdNode } from '../node/ZstdNode';
18
+ import { repairLegacyWasmZstdPlaintext } from '../node/LegacyZstd';
18
19
  import Zipkit from './Zipkit';
19
20
  import { Logger } from './components/Logger';
20
21
  import ZipEntry from './ZipEntry';
@@ -128,18 +129,28 @@ class ZipDecompress {
128
129
 
129
130
 
130
131
  /**
131
- * Synchronous zstd decompress method for in-memory mode
132
- * ZSTD codec is guaranteed to be initialized via factory method
133
- * Internal method only
132
+ * Zstd decompress via Node native zlib, repairing legacy WASM +18 padding when present.
134
133
  */
135
- private async zstdDecompressSync(data: Buffer): Promise<Buffer> {
134
+ private async zstdDecompressSync(data: Buffer, entry?: ZipEntry): Promise<Buffer> {
136
135
  this.log(`zstdDecompressSync() called with ${data.length} bytes`);
137
-
136
+
138
137
  try {
139
- // Use global ZstdManager for decompression (handles queuing and initialization)
140
- const decompressed = await ZstdManager.decompress(data);
138
+ let decompressed = await ZstdNode.decompress(data);
139
+ if (entry && entry.uncompressedSize > 0) {
140
+ const { repaired, wasLegacy } = repairLegacyWasmZstdPlaintext(
141
+ decompressed,
142
+ entry.uncompressedSize,
143
+ entry.crc
144
+ );
145
+ if (wasLegacy) {
146
+ this.log(
147
+ `Legacy WASM zstd padding repaired: ${decompressed.length} -> ${repaired.length} bytes`
148
+ );
149
+ }
150
+ decompressed = repaired;
151
+ }
141
152
  this.log(`ZSTD decompression successful: ${data.length} bytes -> ${decompressed.length} bytes`);
142
- return Buffer.from(decompressed);
153
+ return decompressed;
143
154
  } catch (error) {
144
155
  this.log(`ZSTD decompression failed: ${error}`);
145
156
  throw new Error(`ZSTD decompression failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -173,7 +184,7 @@ class ZipDecompress {
173
184
  } else if (method === CMP_METHOD.DEFLATED) {
174
185
  outBuf = this.inflate(compressedData);
175
186
  } else if (method === CMP_METHOD.ZSTD) {
176
- outBuf = await this.zstdDecompressSync(compressedData);
187
+ outBuf = await this.zstdDecompressSync(compressedData, entry);
177
188
  } else {
178
189
  throw new Error(`Unsupported compression method: ${method}`);
179
190
  }
@@ -12,6 +12,9 @@
12
12
  //
13
13
  // Extra field 0x9901 (11 bytes):
14
14
  // [header ID (2)] [data size (2)] [vendor version (2)] [vendor ID (2)] [strength (1)] [real method (2)]
15
+ //
16
+ // Streaming helpers (AesEncryptor / AesDecryptor) keep only a 16-byte CTR block in
17
+ // flight so Node create/extract can chunk large AE-1/AE-2 entries.
15
18
 
16
19
  import * as crypto from 'crypto';
17
20
  import ZipEntry from '../ZipEntry';
@@ -21,8 +24,8 @@ import {
21
24
  AES256_PWD_VERIFY_SIZE,
22
25
  AES_AUTH_CODE_SIZE,
23
26
  AES_EXTRA_FIELD_SIZE,
27
+ AES_OVERHEAD,
24
28
  HDR_ID,
25
- CMP_METHOD,
26
29
  } from '../constants/Headers';
27
30
 
28
31
  const COMPOSITE_KEY_LENGTH = 2 * AES256_KEY_SIZE + AES256_PWD_VERIFY_SIZE; // 66 bytes
@@ -35,6 +38,215 @@ interface DerivedKeys {
35
38
  passwordVerifier: Buffer;
36
39
  }
37
40
 
41
+ /** Little-endian AES-CTR keystream (WinZip counter starts at 1). */
42
+ class AesCtrEngine {
43
+ private readonly aesKey: Buffer;
44
+ private readonly counter: Buffer;
45
+ private keystream: Buffer;
46
+ private ksPos: number;
47
+
48
+ constructor(aesKey: Buffer) {
49
+ this.aesKey = aesKey;
50
+ this.counter = Buffer.alloc(AES_BLOCK_SIZE);
51
+ this.counter[0] = 1;
52
+ this.keystream = this.encryptCounter();
53
+ this.ksPos = 0;
54
+ }
55
+
56
+ process(data: Buffer): Buffer {
57
+ if (data.length === 0) {
58
+ return Buffer.alloc(0);
59
+ }
60
+ const result = Buffer.alloc(data.length);
61
+ for (let i = 0; i < data.length; i++) {
62
+ if (this.ksPos >= AES_BLOCK_SIZE) {
63
+ AesCtrEngine.incrementCounterLE(this.counter);
64
+ this.keystream = this.encryptCounter();
65
+ this.ksPos = 0;
66
+ }
67
+ result[i] = data[i] ^ this.keystream[this.ksPos++];
68
+ }
69
+ return result;
70
+ }
71
+
72
+ private encryptCounter(): Buffer {
73
+ const cipher = crypto.createCipheriv('aes-256-ecb', this.aesKey, null);
74
+ cipher.setAutoPadding(false);
75
+ return cipher.update(this.counter);
76
+ }
77
+
78
+ static incrementCounterLE(counter: Buffer): void {
79
+ for (let i = 0; i < AES_BLOCK_SIZE; i++) {
80
+ if (counter[i] === 255) {
81
+ counter[i] = 0;
82
+ } else {
83
+ counter[i]++;
84
+ break;
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Streaming AE-1/AE-2 encryptor for ZIP entry payloads.
92
+ *
93
+ * Usage: `header()` once → `update(compressedChunk)` repeatedly → `finish()` for HMAC.
94
+ */
95
+ export class AesEncryptor {
96
+ private readonly keys: DerivedKeys;
97
+ private readonly salt: Buffer;
98
+ private readonly ctr: AesCtrEngine;
99
+ private readonly hmac: crypto.Hmac;
100
+ private headerSent = false;
101
+ private finished = false;
102
+
103
+ constructor(password: string) {
104
+ this.salt = crypto.randomBytes(AES256_SALT_SIZE);
105
+ this.keys = AesCrypto.deriveKeys(password, this.salt);
106
+ this.ctr = new AesCtrEngine(this.keys.aesKey);
107
+ this.hmac = crypto.createHmac('sha1', this.keys.hmacKey);
108
+ }
109
+
110
+ /** salt (16) + password verifier (2). Write before any ciphertext. */
111
+ header(): Buffer {
112
+ if (this.finished) {
113
+ throw new Error('AesEncryptor already finished');
114
+ }
115
+ this.headerSent = true;
116
+ return Buffer.concat([this.salt, this.keys.passwordVerifier]);
117
+ }
118
+
119
+ /** Encrypt one compressed-data chunk; updates HMAC over ciphertext. */
120
+ update(plain: Buffer): Buffer {
121
+ if (this.finished) {
122
+ throw new Error('AesEncryptor already finished');
123
+ }
124
+ if (!this.headerSent) {
125
+ throw new Error('AesEncryptor.header() must be called before update()');
126
+ }
127
+ if (plain.length === 0) {
128
+ return Buffer.alloc(0);
129
+ }
130
+ const cipher = this.ctr.process(plain);
131
+ this.hmac.update(cipher);
132
+ return cipher;
133
+ }
134
+
135
+ /** Final 10-byte authentication code. */
136
+ finish(): Buffer {
137
+ if (this.finished) {
138
+ throw new Error('AesEncryptor already finished');
139
+ }
140
+ if (!this.headerSent) {
141
+ throw new Error('AesEncryptor.header() must be called before finish()');
142
+ }
143
+ this.finished = true;
144
+ return this.hmac.digest().subarray(0, AES_AUTH_CODE_SIZE);
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Streaming AE-1/AE-2 decryptor. `compressedSize` is the full ZIP payload length
150
+ * (salt + verifier + ciphertext + auth), as stored in the local/central header.
151
+ */
152
+ export class AesDecryptor {
153
+ private readonly password: string;
154
+ private readonly cipherLen: number;
155
+ private remainingCipher: number;
156
+ private keys: DerivedKeys | null = null;
157
+ private ctr: AesCtrEngine | null = null;
158
+ private hmac: crypto.Hmac | null = null;
159
+ private headerBuf = Buffer.alloc(0);
160
+ private authBuf = Buffer.alloc(0);
161
+ private headerDone = false;
162
+ private verified = false;
163
+
164
+ constructor(password: string, compressedSize: number) {
165
+ if (compressedSize < AES_OVERHEAD) {
166
+ throw new Error('AES decryption failed: payload too short');
167
+ }
168
+ this.password = password;
169
+ this.cipherLen = compressedSize - AES_OVERHEAD;
170
+ this.remainingCipher = this.cipherLen;
171
+ }
172
+
173
+ /**
174
+ * Feed the next archive chunk. Returns decrypted compressed bytes
175
+ * (empty while still reading salt/verifier).
176
+ */
177
+ push(chunk: Buffer): Buffer {
178
+ let data = chunk;
179
+ if (!this.headerDone) {
180
+ this.headerBuf = Buffer.concat([this.headerBuf, data]);
181
+ const need = AES256_SALT_SIZE + AES256_PWD_VERIFY_SIZE;
182
+ if (this.headerBuf.length < need) {
183
+ return Buffer.alloc(0);
184
+ }
185
+ const salt = this.headerBuf.subarray(0, AES256_SALT_SIZE);
186
+ const storedVerifier = this.headerBuf.subarray(AES256_SALT_SIZE, need);
187
+ this.keys = AesCrypto.deriveKeys(this.password, salt);
188
+ if (!this.keys.passwordVerifier.equals(storedVerifier)) {
189
+ throw new Error('AES decryption failed: wrong password');
190
+ }
191
+ this.ctr = new AesCtrEngine(this.keys.aesKey);
192
+ this.hmac = crypto.createHmac('sha1', this.keys.hmacKey);
193
+ this.headerDone = true;
194
+ data = this.headerBuf.subarray(need);
195
+ this.headerBuf = Buffer.alloc(0);
196
+ }
197
+
198
+ if (data.length === 0) {
199
+ return Buffer.alloc(0);
200
+ }
201
+
202
+ const cipherTake = Math.min(data.length, this.remainingCipher);
203
+ const cipherPart = data.subarray(0, cipherTake);
204
+ const rest = data.subarray(cipherTake);
205
+ if (rest.length > 0) {
206
+ this.authBuf = Buffer.concat([this.authBuf, rest]);
207
+ }
208
+
209
+ if (cipherPart.length === 0) {
210
+ this.tryVerify();
211
+ return Buffer.alloc(0);
212
+ }
213
+
214
+ this.hmac!.update(cipherPart);
215
+ const plain = this.ctr!.process(cipherPart);
216
+ this.remainingCipher -= cipherPart.length;
217
+ this.tryVerify();
218
+ return plain;
219
+ }
220
+
221
+ /** Ensure ciphertext is complete and HMAC verified. */
222
+ end(): void {
223
+ if (this.remainingCipher > 0) {
224
+ throw new Error('AES decryption failed: truncated ciphertext');
225
+ }
226
+ this.tryVerify();
227
+ if (!this.verified) {
228
+ throw new Error('AES decryption failed: missing authentication code');
229
+ }
230
+ }
231
+
232
+ private tryVerify(): void {
233
+ if (this.verified || this.remainingCipher > 0 || !this.hmac) {
234
+ return;
235
+ }
236
+ if (this.authBuf.length < AES_AUTH_CODE_SIZE) {
237
+ return;
238
+ }
239
+ const stored = this.authBuf.subarray(0, AES_AUTH_CODE_SIZE);
240
+ const calculated = this.hmac.digest().subarray(0, AES_AUTH_CODE_SIZE);
241
+ if (!calculated.equals(stored)) {
242
+ throw new Error(
243
+ 'AES decryption failed: authentication code mismatch (data corrupted or tampered)'
244
+ );
245
+ }
246
+ this.verified = true;
247
+ }
248
+ }
249
+
38
250
  /**
39
251
  * WinZip AES-256 encryption/decryption for ZIP files.
40
252
  *
@@ -42,14 +254,19 @@ interface DerivedKeys {
42
254
  * a little-endian counter (WinZip convention), and HMAC-SHA1 for authentication.
43
255
  */
44
256
  export class AesCrypto {
45
-
46
257
  /**
47
258
  * Derive encryption key, HMAC key, and password verification value
48
259
  * using PBKDF2 with HMAC-SHA1.
49
260
  */
50
261
  static deriveKeys(password: string | Buffer, salt: Buffer): DerivedKeys {
51
262
  const key = Buffer.isBuffer(password) ? password : Buffer.from(password);
52
- const compositeKey = crypto.pbkdf2Sync(key, salt, PBKDF2_ITERATIONS, COMPOSITE_KEY_LENGTH, 'sha1');
263
+ const compositeKey = crypto.pbkdf2Sync(
264
+ key,
265
+ salt,
266
+ PBKDF2_ITERATIONS,
267
+ COMPOSITE_KEY_LENGTH,
268
+ 'sha1'
269
+ );
53
270
  return {
54
271
  aesKey: compositeKey.subarray(0, AES256_KEY_SIZE),
55
272
  hmacKey: compositeKey.subarray(AES256_KEY_SIZE, 2 * AES256_KEY_SIZE),
@@ -57,22 +274,33 @@ export class AesCrypto {
57
274
  };
58
275
  }
59
276
 
277
+ /** Create a streaming AE-1/AE-2 encryptor. */
278
+ static createEncryptor(password: string): AesEncryptor {
279
+ return new AesEncryptor(password);
280
+ }
281
+
282
+ /**
283
+ * Create a streaming AE-1/AE-2 decryptor.
284
+ * @param compressedSize Full payload size including salt, verifier, and auth code.
285
+ */
286
+ static createDecryptor(password: string, compressedSize: number): AesDecryptor {
287
+ return new AesDecryptor(password, compressedSize);
288
+ }
289
+
60
290
  /**
61
291
  * Encrypt compressed data using WinZip AES-256.
62
292
  *
63
293
  * @returns Buffer: salt(16) + verifier(2) + encryptedData + hmac(10)
64
294
  */
65
295
  static encryptBuffer(entry: ZipEntry, compressedData: Buffer, password: string): Buffer {
66
- const salt = crypto.randomBytes(AES256_SALT_SIZE);
67
- const keys = AesCrypto.deriveKeys(password, salt);
68
-
69
- const encrypted = AesCrypto.aesCtrProcess(keys.aesKey, compressedData);
70
-
71
- const hmac = crypto.createHmac('sha1', keys.hmacKey);
72
- hmac.update(encrypted);
73
- const authCode = hmac.digest().subarray(0, AES_AUTH_CODE_SIZE);
74
-
75
- return Buffer.concat([salt, keys.passwordVerifier, encrypted, authCode]);
296
+ void entry;
297
+ const enc = AesCrypto.createEncryptor(password);
298
+ const parts = [enc.header()];
299
+ if (compressedData.length > 0) {
300
+ parts.push(enc.update(compressedData));
301
+ }
302
+ parts.push(enc.finish());
303
+ return Buffer.concat(parts);
76
304
  }
77
305
 
78
306
  /**
@@ -84,78 +312,30 @@ export class AesCrypto {
84
312
  * @returns Decrypted (compressed) data
85
313
  */
86
314
  static decryptBuffer(entry: ZipEntry, fullPayload: Buffer, password: string): Buffer {
87
- const salt = fullPayload.subarray(0, AES256_SALT_SIZE);
88
- const storedVerifier = fullPayload.subarray(AES256_SALT_SIZE, AES256_SALT_SIZE + AES256_PWD_VERIFY_SIZE);
89
- const encryptedData = fullPayload.subarray(
90
- AES256_SALT_SIZE + AES256_PWD_VERIFY_SIZE,
91
- fullPayload.length - AES_AUTH_CODE_SIZE
92
- );
93
- const storedAuthCode = fullPayload.subarray(fullPayload.length - AES_AUTH_CODE_SIZE);
94
-
95
- const keys = AesCrypto.deriveKeys(password, salt);
96
-
97
- if (!keys.passwordVerifier.equals(storedVerifier)) {
98
- throw new Error('AES decryption failed: wrong password');
99
- }
100
-
101
- const hmac = crypto.createHmac('sha1', keys.hmacKey);
102
- hmac.update(encryptedData);
103
- const calculatedAuthCode = hmac.digest().subarray(0, AES_AUTH_CODE_SIZE);
104
-
105
- if (!calculatedAuthCode.equals(storedAuthCode)) {
106
- throw new Error('AES decryption failed: authentication code mismatch (data corrupted or tampered)');
107
- }
108
-
109
- return AesCrypto.aesCtrProcess(keys.aesKey, encryptedData);
110
- }
111
-
112
- /**
113
- * AES-CTR encrypt/decrypt with WinZip's little-endian counter.
114
- *
115
- * WinZip uses a counter that starts at 1 and increments in little-endian byte order.
116
- * Node's built-in aes-256-ctr uses big-endian counter increment, so we process
117
- * block-by-block with an explicit LE counter to match the WinZip format.
118
- */
119
- private static aesCtrProcess(aesKey: Buffer, data: Buffer): Buffer {
120
- if (data.length === 0) {
121
- return Buffer.alloc(0);
122
- }
123
-
124
- const result = Buffer.alloc(data.length);
125
- const counter = Buffer.alloc(AES_BLOCK_SIZE);
126
- counter[0] = 1; // LE counter starts at 1
127
-
128
- let offset = 0;
129
- while (offset < data.length) {
130
- // Encrypt the counter block to get keystream
131
- const cipher = crypto.createCipheriv('aes-256-ecb', aesKey, null);
132
- cipher.setAutoPadding(false);
133
- const keystream = cipher.update(counter);
134
-
135
- const blockLen = Math.min(AES_BLOCK_SIZE, data.length - offset);
136
- for (let i = 0; i < blockLen; i++) {
137
- result[offset + i] = data[offset + i] ^ keystream[i];
138
- }
139
-
140
- offset += blockLen;
141
- AesCrypto.incrementCounterLE(counter);
142
- }
143
-
144
- return result;
315
+ void entry;
316
+ const dec = AesCrypto.createDecryptor(password, fullPayload.length);
317
+ const out = dec.push(fullPayload);
318
+ dec.end();
319
+ return out;
145
320
  }
146
321
 
147
322
  /**
148
- * Increment a 16-byte counter in little-endian order.
323
+ * Decrypt an async chunk stream (Node extract path).
324
+ * Yields decrypted compressed chunks; verifies HMAC when the stream ends.
149
325
  */
150
- private static incrementCounterLE(counter: Buffer): void {
151
- for (let i = 0; i < AES_BLOCK_SIZE; i++) {
152
- if (counter[i] === 255) {
153
- counter[i] = 0;
154
- } else {
155
- counter[i]++;
156
- break;
326
+ static async *decryptStream(
327
+ password: string,
328
+ compressedSize: number,
329
+ encryptedStream: AsyncIterable<Buffer>
330
+ ): AsyncGenerator<Buffer> {
331
+ const dec = AesCrypto.createDecryptor(password, compressedSize);
332
+ for await (const chunk of encryptedStream) {
333
+ const plain = dec.push(chunk);
334
+ if (plain.length > 0) {
335
+ yield plain;
157
336
  }
158
337
  }
338
+ dec.end();
159
339
  }
160
340
 
161
341
  /**
@@ -167,12 +347,12 @@ export class AesCrypto {
167
347
  */
168
348
  static buildAesExtraField(realMethod: number, vendorVersion: number = 1): Buffer {
169
349
  const buf = Buffer.alloc(AES_EXTRA_FIELD_SIZE);
170
- buf.writeUInt16LE(HDR_ID.AES, 0); // Extra field header ID (0x9901)
171
- buf.writeUInt16LE(7, 2); // Data size (always 7)
172
- buf.writeUInt16LE(vendorVersion, 4); // AE-1 (0x0001) or AE-2 (0x0002)
173
- buf.writeUInt16LE(0x4541, 6); // Vendor ID: "AE" (little-endian)
174
- buf.writeUInt8(0x03, 8); // Encryption strength: AES-256
175
- buf.writeUInt16LE(realMethod, 9); // Actual compression method
350
+ buf.writeUInt16LE(HDR_ID.AES, 0);
351
+ buf.writeUInt16LE(7, 2);
352
+ buf.writeUInt16LE(vendorVersion, 4);
353
+ buf.writeUInt16LE(0x4541, 6); // "AE"
354
+ buf.writeUInt8(0x03, 8); // AES-256
355
+ buf.writeUInt16LE(realMethod, 9);
176
356
  return buf;
177
357
  }
178
358
 
@@ -180,7 +360,6 @@ export class AesCrypto {
180
360
  * Parse the AES extra data field (0x9901) from a buffer.
181
361
  *
182
362
  * @param data - 7-byte data portion of the extra field (without header ID and size)
183
- * @returns Parsed AES extra field values
184
363
  */
185
364
  static parseAesExtraField(data: Buffer): {
186
365
  vendorVersion: number;
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  export { ZipCrypto, crc32, crc32update } from './ZipCrypto';
8
- export { AesCrypto } from './AesCrypto';
8
+ export { AesCrypto, AesEncryptor, AesDecryptor } from './AesCrypto';
9
9
  export { NeoCrypto, NEO_CRYPTO_ALGORITHM_AES256_V1 } from './NeoCrypto';
10
10
  export { EncryptionManager } from './Manager';
11
11
  export {
@@ -28,6 +28,6 @@ const currentDate = new Date();
28
28
  const releaseDate = `${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(currentDate.getDate()).padStart(2, '0')}-${currentDate.getFullYear()}`;
29
29
 
30
30
  export const VERSION = {
31
- number: '0.7.0', // packageJson.version
31
+ number: '1.0.2', // packageJson.version
32
32
  date: releaseDate
33
33
  };