archive-codec 1.7.0 → 1.7.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
CHANGED
|
@@ -184,7 +184,7 @@ const baseHash = deriveOfficeRc4BaseHash(password, salt);
|
|
|
184
184
|
const plaintext = decryptOfficeRc4(baseHash, streamOffset, ciphertext);
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
-
`deriveOfficeRc4BaseHash`/`deriveOfficeRc4BlockKey`/`decryptOfficeRc4` implement [MS-OFFCRYPTO] 2.3.6.1/2.3.6.2's RC4 encryption header scheme: a per-workbook base hash derived once from the password and the header's own salt, then a
|
|
187
|
+
`deriveOfficeRc4BaseHash`/`deriveOfficeRc4BlockKey`/`decryptOfficeRc4` implement [MS-OFFCRYPTO] 2.3.6.1/2.3.6.2's RC4 encryption header scheme: a per-workbook base hash derived once from the password and the header's own salt, then a full 128-bit RC4 key re-derived from that base hash at every 1024-byte boundary of the underlying decrypted stream — despite [MS-OFFCRYPTO] 2.3.6.1's own field descriptions twice stating "encrypted using a 40-bit RC4 cipher", which is wrong; the real key is Hfinal in full, confirmed against Apache POI's `BinaryRC4Decryptor` and nolze/msoffcrypto-tool's own doctested test vectors, both cross-checked directly rather than trusted from the spec's own prose — see each constant's own doc comment for the full account, including the truncated-to-5-bytes implementation this package shipped briefly before the cross-check caught it. `decryptOfficeRc4` takes the byte offset a chunk starts at within the whole encrypted stream (not within the chunk itself), so a stream can be decrypted in arbitrary pieces, not only from its own start, and still land on the correct per-block key throughout.
|
|
188
188
|
|
|
189
189
|
This is exactly the piece `xls-codec`'s `FilePass`-record reader needs and nothing more: locating the `FilePass` record, reading its own encryption-header bytes, and verifying the password against `EncryptedVerifier`/`EncryptedVerifierHash` are `xls-codec`'s own concern, not this package's — `archive-codec` carries only the format-agnostic cryptography, never a `.xls`-specific byte layout. `md5`/`rc4` are exported individually too, for `doc-codec`/`ppt-codec`'s own eventual integration against the identical header scheme.
|
|
190
190
|
|
|
@@ -4,10 +4,10 @@ const require_crypto_rc4 = require("./rc4.cjs");
|
|
|
4
4
|
//#region src/crypto/office-rc4.ts
|
|
5
5
|
/** [MS-OFFCRYPTO] 2.3.6.1's own EncryptedVerifier/EncryptedVerifierHash length, and the salt length the same header carries. */
|
|
6
6
|
const OFFICE_RC4_VERIFIER_LENGTH = 16;
|
|
7
|
-
/** The span of the underlying decrypted stream one derived RC4 key covers before the next block's key takes over -- confirmed against Apache POI's
|
|
7
|
+
/** The span of the underlying decrypted stream one derived RC4 key covers before the next block's key takes over -- confirmed against Apache POI's Biff8DecryptingStream.RC4_REKEYING_INTERVAL, since [MS-OFFCRYPTO] 2.3.6.2's own prose does not state it. */
|
|
8
8
|
const OFFICE_RC4_BLOCK_SIZE = 1024;
|
|
9
|
-
/** The
|
|
10
|
-
const
|
|
9
|
+
/** The intermediate H0/H1 truncation [MS-OFFCRYPTO] 2.3.6.2 states explicitly ("H0's own first 5 bytes", "H1's own first 5 bytes") -- distinct from the FINAL per-block key length, which is Hfinal in full (see this file's own top comment) and is never truncated. */
|
|
10
|
+
const INTERMEDIATE_HASH_LENGTH_BYTES = 5;
|
|
11
11
|
function passwordToUtf16LeBytes(password) {
|
|
12
12
|
const bytes = new Uint8Array(password.length * 2);
|
|
13
13
|
for (let i = 0; i < password.length; i += 1) {
|
|
@@ -19,15 +19,15 @@ function passwordToUtf16LeBytes(password) {
|
|
|
19
19
|
}
|
|
20
20
|
/** The per-workbook intermediate hash (H1's own first 5 bytes) every block's key derives from -- computed once per password+salt pair, then reused across every block via deriveOfficeRc4BlockKey, since recomputing H0/H1 per block would be needless repeated work over the identical 336-byte buffer. */
|
|
21
21
|
function deriveOfficeRc4BaseHash(password, salt) {
|
|
22
|
-
const truncated = require_crypto_md5.md5(passwordToUtf16LeBytes(password)).subarray(0,
|
|
22
|
+
const truncated = require_crypto_md5.md5(passwordToUtf16LeBytes(password)).subarray(0, INTERMEDIATE_HASH_LENGTH_BYTES);
|
|
23
23
|
const unit = /* @__PURE__ */ new Uint8Array(21);
|
|
24
24
|
unit.set(truncated, 0);
|
|
25
|
-
unit.set(salt,
|
|
25
|
+
unit.set(salt, INTERMEDIATE_HASH_LENGTH_BYTES);
|
|
26
26
|
const buffer336 = new Uint8Array(unit.length * 16);
|
|
27
27
|
for (let i = 0; i < 16; i += 1) buffer336.set(unit, i * unit.length);
|
|
28
|
-
return require_crypto_md5.md5(buffer336).subarray(0,
|
|
28
|
+
return require_crypto_md5.md5(buffer336).subarray(0, INTERMEDIATE_HASH_LENGTH_BYTES);
|
|
29
29
|
}
|
|
30
|
-
/** The real, block-specific
|
|
30
|
+
/** The real, block-specific RC4 key: MD5(baseHash + the block number as 4 little-endian bytes) in full -- all 16 bytes, never truncated (see this file's own top comment for why that matters). */
|
|
31
31
|
function deriveOfficeRc4BlockKey(baseHash, blockNumber) {
|
|
32
32
|
const input = new Uint8Array(baseHash.length + 4);
|
|
33
33
|
input.set(baseHash, 0);
|
|
@@ -35,7 +35,7 @@ function deriveOfficeRc4BlockKey(baseHash, blockNumber) {
|
|
|
35
35
|
input[baseHash.length + 1] = blockNumber >>> 8 & 255;
|
|
36
36
|
input[baseHash.length + 2] = blockNumber >>> 16 & 255;
|
|
37
37
|
input[baseHash.length + 3] = blockNumber >>> 24 & 255;
|
|
38
|
-
return require_crypto_md5.md5(input)
|
|
38
|
+
return require_crypto_md5.md5(input);
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
41
|
* Decrypts `data` -- a byte range of the underlying OLE stream starting at `streamOffset` bytes from the very start of that stream -- against the RC4 encryption header scheme, re-deriving the block key at every 1024-byte boundary `data` crosses. RC4 is symmetric, so this same function also encrypts; nothing in this package uses it that way, since nothing in this family writes an encrypted legacy binary document.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
//#region src/crypto/office-rc4.d.ts
|
|
2
2
|
/** [MS-OFFCRYPTO] 2.3.6.1's own EncryptedVerifier/EncryptedVerifierHash length, and the salt length the same header carries. */
|
|
3
3
|
declare const OFFICE_RC4_VERIFIER_LENGTH = 16;
|
|
4
|
-
/** The span of the underlying decrypted stream one derived RC4 key covers before the next block's key takes over -- confirmed against Apache POI's
|
|
4
|
+
/** The span of the underlying decrypted stream one derived RC4 key covers before the next block's key takes over -- confirmed against Apache POI's Biff8DecryptingStream.RC4_REKEYING_INTERVAL, since [MS-OFFCRYPTO] 2.3.6.2's own prose does not state it. */
|
|
5
5
|
declare const OFFICE_RC4_BLOCK_SIZE = 1024;
|
|
6
6
|
/** The per-workbook intermediate hash (H1's own first 5 bytes) every block's key derives from -- computed once per password+salt pair, then reused across every block via deriveOfficeRc4BlockKey, since recomputing H0/H1 per block would be needless repeated work over the identical 336-byte buffer. */
|
|
7
7
|
declare function deriveOfficeRc4BaseHash(password: string, salt: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>;
|
|
8
|
-
/** The real, block-specific
|
|
8
|
+
/** The real, block-specific RC4 key: MD5(baseHash + the block number as 4 little-endian bytes) in full -- all 16 bytes, never truncated (see this file's own top comment for why that matters). */
|
|
9
9
|
declare function deriveOfficeRc4BlockKey(baseHash: Uint8Array<ArrayBuffer>, blockNumber: number): Uint8Array<ArrayBuffer>;
|
|
10
10
|
/**
|
|
11
11
|
* Decrypts `data` -- a byte range of the underlying OLE stream starting at `streamOffset` bytes from the very start of that stream -- against the RC4 encryption header scheme, re-deriving the block key at every 1024-byte boundary `data` crosses. RC4 is symmetric, so this same function also encrypts; nothing in this package uses it that way, since nothing in this family writes an encrypted legacy binary document.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
//#region src/crypto/office-rc4.d.ts
|
|
2
2
|
/** [MS-OFFCRYPTO] 2.3.6.1's own EncryptedVerifier/EncryptedVerifierHash length, and the salt length the same header carries. */
|
|
3
3
|
declare const OFFICE_RC4_VERIFIER_LENGTH = 16;
|
|
4
|
-
/** The span of the underlying decrypted stream one derived RC4 key covers before the next block's key takes over -- confirmed against Apache POI's
|
|
4
|
+
/** The span of the underlying decrypted stream one derived RC4 key covers before the next block's key takes over -- confirmed against Apache POI's Biff8DecryptingStream.RC4_REKEYING_INTERVAL, since [MS-OFFCRYPTO] 2.3.6.2's own prose does not state it. */
|
|
5
5
|
declare const OFFICE_RC4_BLOCK_SIZE = 1024;
|
|
6
6
|
/** The per-workbook intermediate hash (H1's own first 5 bytes) every block's key derives from -- computed once per password+salt pair, then reused across every block via deriveOfficeRc4BlockKey, since recomputing H0/H1 per block would be needless repeated work over the identical 336-byte buffer. */
|
|
7
7
|
declare function deriveOfficeRc4BaseHash(password: string, salt: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>;
|
|
8
|
-
/** The real, block-specific
|
|
8
|
+
/** The real, block-specific RC4 key: MD5(baseHash + the block number as 4 little-endian bytes) in full -- all 16 bytes, never truncated (see this file's own top comment for why that matters). */
|
|
9
9
|
declare function deriveOfficeRc4BlockKey(baseHash: Uint8Array<ArrayBuffer>, blockNumber: number): Uint8Array<ArrayBuffer>;
|
|
10
10
|
/**
|
|
11
11
|
* Decrypts `data` -- a byte range of the underlying OLE stream starting at `streamOffset` bytes from the very start of that stream -- against the RC4 encryption header scheme, re-deriving the block key at every 1024-byte boundary `data` crosses. RC4 is symmetric, so this same function also encrypts; nothing in this package uses it that way, since nothing in this family writes an encrypted legacy binary document.
|
|
@@ -3,10 +3,10 @@ import { rc4 } from "./rc4.js";
|
|
|
3
3
|
//#region src/crypto/office-rc4.ts
|
|
4
4
|
/** [MS-OFFCRYPTO] 2.3.6.1's own EncryptedVerifier/EncryptedVerifierHash length, and the salt length the same header carries. */
|
|
5
5
|
const OFFICE_RC4_VERIFIER_LENGTH = 16;
|
|
6
|
-
/** The span of the underlying decrypted stream one derived RC4 key covers before the next block's key takes over -- confirmed against Apache POI's
|
|
6
|
+
/** The span of the underlying decrypted stream one derived RC4 key covers before the next block's key takes over -- confirmed against Apache POI's Biff8DecryptingStream.RC4_REKEYING_INTERVAL, since [MS-OFFCRYPTO] 2.3.6.2's own prose does not state it. */
|
|
7
7
|
const OFFICE_RC4_BLOCK_SIZE = 1024;
|
|
8
|
-
/** The
|
|
9
|
-
const
|
|
8
|
+
/** The intermediate H0/H1 truncation [MS-OFFCRYPTO] 2.3.6.2 states explicitly ("H0's own first 5 bytes", "H1's own first 5 bytes") -- distinct from the FINAL per-block key length, which is Hfinal in full (see this file's own top comment) and is never truncated. */
|
|
9
|
+
const INTERMEDIATE_HASH_LENGTH_BYTES = 5;
|
|
10
10
|
function passwordToUtf16LeBytes(password) {
|
|
11
11
|
const bytes = new Uint8Array(password.length * 2);
|
|
12
12
|
for (let i = 0; i < password.length; i += 1) {
|
|
@@ -18,15 +18,15 @@ function passwordToUtf16LeBytes(password) {
|
|
|
18
18
|
}
|
|
19
19
|
/** The per-workbook intermediate hash (H1's own first 5 bytes) every block's key derives from -- computed once per password+salt pair, then reused across every block via deriveOfficeRc4BlockKey, since recomputing H0/H1 per block would be needless repeated work over the identical 336-byte buffer. */
|
|
20
20
|
function deriveOfficeRc4BaseHash(password, salt) {
|
|
21
|
-
const truncated = md5(passwordToUtf16LeBytes(password)).subarray(0,
|
|
21
|
+
const truncated = md5(passwordToUtf16LeBytes(password)).subarray(0, INTERMEDIATE_HASH_LENGTH_BYTES);
|
|
22
22
|
const unit = /* @__PURE__ */ new Uint8Array(21);
|
|
23
23
|
unit.set(truncated, 0);
|
|
24
|
-
unit.set(salt,
|
|
24
|
+
unit.set(salt, INTERMEDIATE_HASH_LENGTH_BYTES);
|
|
25
25
|
const buffer336 = new Uint8Array(unit.length * 16);
|
|
26
26
|
for (let i = 0; i < 16; i += 1) buffer336.set(unit, i * unit.length);
|
|
27
|
-
return md5(buffer336).subarray(0,
|
|
27
|
+
return md5(buffer336).subarray(0, INTERMEDIATE_HASH_LENGTH_BYTES);
|
|
28
28
|
}
|
|
29
|
-
/** The real, block-specific
|
|
29
|
+
/** The real, block-specific RC4 key: MD5(baseHash + the block number as 4 little-endian bytes) in full -- all 16 bytes, never truncated (see this file's own top comment for why that matters). */
|
|
30
30
|
function deriveOfficeRc4BlockKey(baseHash, blockNumber) {
|
|
31
31
|
const input = new Uint8Array(baseHash.length + 4);
|
|
32
32
|
input.set(baseHash, 0);
|
|
@@ -34,7 +34,7 @@ function deriveOfficeRc4BlockKey(baseHash, blockNumber) {
|
|
|
34
34
|
input[baseHash.length + 1] = blockNumber >>> 8 & 255;
|
|
35
35
|
input[baseHash.length + 2] = blockNumber >>> 16 & 255;
|
|
36
36
|
input[baseHash.length + 3] = blockNumber >>> 24 & 255;
|
|
37
|
-
return md5(input)
|
|
37
|
+
return md5(input);
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
40
|
* Decrypts `data` -- a byte range of the underlying OLE stream starting at `streamOffset` bytes from the very start of that stream -- against the RC4 encryption header scheme, re-deriving the block key at every 1024-byte boundary `data` crosses. RC4 is symmetric, so this same function also encrypts; nothing in this package uses it that way, since nothing in this family writes an encrypted legacy binary document.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archive-codec",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
4
4
|
"description": "ZIP-in-ZIP recursive walking with depth and cumulative decompressed-size guards, bounded classic OLE compound-file ([MS-CFB]) reading and writing, and [MS-OLEPS] Property Set Stream reading and writing - zero document-format knowledge, the archive and container utility package for the documents.js family.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -70,26 +70,23 @@
|
|
|
70
70
|
},
|
|
71
71
|
"packageManager": "pnpm@11.6.0",
|
|
72
72
|
"dependencies": {
|
|
73
|
-
"document-schema.js": "
|
|
74
|
-
"fflate": "
|
|
73
|
+
"document-schema.js": "7.3.1",
|
|
74
|
+
"fflate": "0.8.3"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@arethetypeswrong/cli": "
|
|
78
|
-
"@cloudflare/vitest-pool-workers": "
|
|
79
|
-
"@stryker-mutator/core": "
|
|
80
|
-
"@stryker-mutator/typescript-checker": "
|
|
81
|
-
"@stryker-mutator/vitest-runner": "
|
|
82
|
-
"@types/node": "
|
|
83
|
-
"eslint": "
|
|
84
|
-
"husky": "
|
|
77
|
+
"@arethetypeswrong/cli": "0.18.5",
|
|
78
|
+
"@cloudflare/vitest-pool-workers": "0.21.2",
|
|
79
|
+
"@stryker-mutator/core": "10.0.0",
|
|
80
|
+
"@stryker-mutator/typescript-checker": "10.0.0",
|
|
81
|
+
"@stryker-mutator/vitest-runner": "10.0.0",
|
|
82
|
+
"@types/node": "26.2.0",
|
|
83
|
+
"eslint": "10.8.1",
|
|
84
|
+
"husky": "9.1.7",
|
|
85
85
|
"jiti": "2.7.0",
|
|
86
|
-
"publint": "
|
|
87
|
-
"tsdown": "
|
|
88
|
-
"turbo": "
|
|
89
|
-
"typescript": "
|
|
90
|
-
"vitest": "
|
|
91
|
-
},
|
|
92
|
-
"lint-staged": {
|
|
93
|
-
"*.ts": "eslint --fix"
|
|
86
|
+
"publint": "0.3.22",
|
|
87
|
+
"tsdown": "0.22.14",
|
|
88
|
+
"turbo": "2.10.8",
|
|
89
|
+
"typescript": "6.0.3",
|
|
90
|
+
"vitest": "4.1.11"
|
|
94
91
|
}
|
|
95
92
|
}
|