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.
- package/README.md +57 -24
- package/WHATS_NEW.md +114 -0
- package/dist/browser/zstd-browser-shim.d.ts +28 -0
- package/dist/browser/zstd-browser-shim.d.ts.map +1 -0
- package/dist/browser/zstd-browser-shim.js +66 -0
- package/dist/browser/zstd-browser-shim.js.map +1 -0
- package/dist/browser-esm/index.js +1786 -41737
- package/dist/browser-esm/index.js.map +4 -4
- package/dist/browser-umd/index.js +1786 -41737
- package/dist/browser-umd/index.js.map +4 -4
- package/dist/browser-umd/index.min.js +3 -21
- package/dist/core/ZipCompress.d.ts +2 -7
- package/dist/core/ZipCompress.d.ts.map +1 -1
- package/dist/core/ZipCompress.js +45 -22
- package/dist/core/ZipCompress.js.map +1 -1
- package/dist/core/ZipDecompress.d.ts +1 -3
- package/dist/core/ZipDecompress.d.ts.map +1 -1
- package/dist/core/ZipDecompress.js +14 -9
- package/dist/core/ZipDecompress.js.map +1 -1
- package/dist/core/encryption/AesCrypto.d.ts +55 -11
- package/dist/core/encryption/AesCrypto.d.ts.map +1 -1
- package/dist/core/encryption/AesCrypto.js +223 -68
- package/dist/core/encryption/AesCrypto.js.map +1 -1
- package/dist/core/encryption/index.d.ts +1 -1
- package/dist/core/encryption/index.d.ts.map +1 -1
- package/dist/core/encryption/index.js +3 -1
- package/dist/core/encryption/index.js.map +1 -1
- package/dist/core/version.js +1 -1
- package/dist/node/LegacyZstd.d.ts +67 -0
- package/dist/node/LegacyZstd.d.ts.map +1 -0
- package/dist/node/LegacyZstd.js +209 -0
- package/dist/node/LegacyZstd.js.map +1 -0
- package/dist/node/ZipCompressNode.d.ts +9 -7
- package/dist/node/ZipCompressNode.d.ts.map +1 -1
- package/dist/node/ZipCompressNode.js +85 -46
- package/dist/node/ZipCompressNode.js.map +1 -1
- package/dist/node/ZipDecompressNode.d.ts +7 -3
- package/dist/node/ZipDecompressNode.d.ts.map +1 -1
- package/dist/node/ZipDecompressNode.js +51 -55
- package/dist/node/ZipDecompressNode.js.map +1 -1
- package/dist/node/ZipkitNode.d.ts +14 -0
- package/dist/node/ZipkitNode.d.ts.map +1 -1
- package/dist/node/ZipkitNode.js +130 -39
- package/dist/node/ZipkitNode.js.map +1 -1
- package/dist/node/ZstdNode.d.ts +39 -0
- package/dist/node/ZstdNode.d.ts.map +1 -0
- package/dist/node/ZstdNode.js +171 -0
- package/dist/node/ZstdNode.js.map +1 -0
- package/dist/node/index.d.ts +3 -0
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +11 -1
- package/dist/node/index.js.map +1 -1
- package/package.json +10 -10
- package/src/browser/zstd-browser-shim.ts +76 -0
- package/src/core/ZipCompress.ts +57 -29
- package/src/core/ZipDecompress.ts +21 -10
- package/src/core/encryption/AesCrypto.ts +265 -86
- package/src/core/encryption/index.ts +1 -1
- package/src/core/version.ts +1 -1
- package/src/node/LegacyZstd.ts +270 -0
- package/src/node/ZipCompressNode.ts +107 -55
- package/src/node/ZipDecompressNode.ts +80 -66
- package/src/node/ZipkitNode.ts +150 -40
- package/src/node/ZstdNode.ts +155 -0
- package/src/node/index.ts +10 -0
- package/dist/core/ZstdManager.d.ts +0 -56
- package/dist/core/ZstdManager.d.ts.map +0 -1
- package/dist/core/ZstdManager.js +0 -144
- package/dist/core/ZstdManager.js.map +0 -1
- package/src/core/ZstdManager.ts +0 -165
package/src/core/ZipCompress.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
//
|
|
15
15
|
|
|
16
16
|
const pako = require('pako');
|
|
17
|
-
import {
|
|
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
|
|
279
|
-
*
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
*
|
|
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
|
-
|
|
140
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
*
|
|
323
|
+
* Decrypt an async chunk stream (Node extract path).
|
|
324
|
+
* Yields decrypted compressed chunks; verifies HMAC when the stream ends.
|
|
149
325
|
*/
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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);
|
|
171
|
-
buf.writeUInt16LE(7, 2);
|
|
172
|
-
buf.writeUInt16LE(vendorVersion, 4);
|
|
173
|
-
buf.writeUInt16LE(0x4541, 6);
|
|
174
|
-
buf.writeUInt8(0x03, 8);
|
|
175
|
-
buf.writeUInt16LE(realMethod, 9);
|
|
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 {
|
package/src/core/version.ts
CHANGED
|
@@ -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.
|
|
31
|
+
number: '1.0.2', // packageJson.version
|
|
32
32
|
date: releaseDate
|
|
33
33
|
};
|