@patrickdk77/office-defuse 0.1.0 → 0.2.0

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 (88) hide show
  1. package/README.md +278 -36
  2. package/dist/biff.d.ts +62 -0
  3. package/dist/biff.js +95 -0
  4. package/dist/cfb-writer.d.ts +25 -0
  5. package/dist/cfb-writer.js +314 -0
  6. package/dist/cfb.d.ts +58 -0
  7. package/dist/cfb.js +456 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +357 -0
  10. package/dist/contained.d.ts +46 -0
  11. package/dist/contained.js +152 -0
  12. package/dist/context.d.ts +140 -17
  13. package/dist/context.js +135 -37
  14. package/dist/crc32.d.ts +8 -2
  15. package/dist/crc32.js +44 -4
  16. package/dist/engine.d.ts +12 -1
  17. package/dist/engine.js +336 -42
  18. package/dist/errors.d.ts +17 -0
  19. package/dist/errors.js +27 -1
  20. package/dist/fields.d.ts +36 -0
  21. package/dist/fields.js +139 -0
  22. package/dist/findings.d.ts +2 -1
  23. package/dist/findings.js +44 -9
  24. package/dist/flat.d.ts +33 -0
  25. package/dist/flat.js +486 -0
  26. package/dist/formats.d.ts +7 -0
  27. package/dist/formats.js +5 -1
  28. package/dist/formulas.d.ts +27 -0
  29. package/dist/formulas.js +157 -1
  30. package/dist/index.d.ts +3 -2
  31. package/dist/index.js +8 -1
  32. package/dist/index.mjs +1 -1
  33. package/dist/io.d.ts +62 -1
  34. package/dist/io.js +245 -1
  35. package/dist/links.d.ts +27 -0
  36. package/dist/links.js +106 -1
  37. package/dist/msg.d.ts +8 -0
  38. package/dist/msg.js +251 -0
  39. package/dist/objects.d.ts +45 -0
  40. package/dist/objects.js +204 -0
  41. package/dist/odf.d.ts +66 -2
  42. package/dist/odf.js +181 -64
  43. package/dist/offcrypto.d.ts +54 -0
  44. package/dist/offcrypto.js +647 -0
  45. package/dist/ole.d.ts +12 -0
  46. package/dist/ole.js +139 -0
  47. package/dist/oleobj.d.ts +85 -0
  48. package/dist/oleobj.js +215 -0
  49. package/dist/ooxml.d.ts +83 -2
  50. package/dist/ooxml.js +358 -145
  51. package/dist/package.d.ts +3 -0
  52. package/dist/package.js +21 -0
  53. package/dist/plan.d.ts +104 -0
  54. package/dist/plan.js +73 -0
  55. package/dist/plugins.d.ts +8 -0
  56. package/dist/plugins.js +60 -0
  57. package/dist/ppt.d.ts +8 -0
  58. package/dist/ppt.js +281 -0
  59. package/dist/rewrite-cfb.d.ts +14 -0
  60. package/dist/rewrite-cfb.js +52 -0
  61. package/dist/rewrite-rtf.d.ts +9 -0
  62. package/dist/rewrite-rtf.js +45 -0
  63. package/dist/rewrite-xml.d.ts +36 -0
  64. package/dist/rewrite-xml.js +341 -0
  65. package/dist/rewrite-zip.d.ts +8 -0
  66. package/dist/rewrite-zip.js +498 -0
  67. package/dist/rtf.d.ts +8 -0
  68. package/dist/rtf.js +712 -0
  69. package/dist/rtfcomp.d.ts +7 -0
  70. package/dist/rtfcomp.js +80 -0
  71. package/dist/score.js +1 -1
  72. package/dist/sniff.d.ts +5 -0
  73. package/dist/sniff.js +38 -0
  74. package/dist/types.d.ts +66 -2
  75. package/dist/types.js +33 -0
  76. package/dist/vsd.d.ts +13 -0
  77. package/dist/vsd.js +689 -0
  78. package/dist/word.d.ts +9 -0
  79. package/dist/word.js +194 -0
  80. package/dist/xls.d.ts +27 -0
  81. package/dist/xls.js +208 -0
  82. package/dist/xml.d.ts +30 -0
  83. package/dist/xml.js +45 -23
  84. package/dist/zip-writer.d.ts +4 -0
  85. package/dist/zip-writer.js +20 -7
  86. package/dist/zip.d.ts +9 -2
  87. package/dist/zip.js +61 -4
  88. package/package.json +6 -3
@@ -0,0 +1,647 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Rc4 = exports.POWERPOINT_PASSWORD = exports.DEFAULT_PASSWORD = void 0;
37
+ exports.unlockWorkbook = unlockWorkbook;
38
+ exports.unlockDocument = unlockDocument;
39
+ exports.unlockPresentation = unlockPresentation;
40
+ exports.openPackage = openPackage;
41
+ const crypto = __importStar(require("node:crypto"));
42
+ const errors_1 = require("./errors");
43
+ const oleobj_1 = require("./oleobj");
44
+ const types_1 = require("./types");
45
+ /** The password Excel opens a workbook with when nobody set one to open it (MS-OFFCRYPTO 2.4.3). */
46
+ exports.DEFAULT_PASSWORD = 'VelvetSweatshop';
47
+ /** The password PowerPoint 97 to 2003 opens a presentation with when nobody set one to open it. */
48
+ exports.POWERPOINT_PASSWORD = '/01Hannes Ruescher/01';
49
+ const EXCEL = { password: exports.DEFAULT_PASSWORD, application: 'Excel' };
50
+ const POWERPOINT = { password: exports.POWERPOINT_PASSWORD, application: 'PowerPoint' };
51
+ /** MS-OFFCRYPTO allows a spin count of at most 10,000,000. */
52
+ const MAX_SPIN = 10_000_000;
53
+ /** More of a Standard EncryptionInfo than its header and verifier take, which are a few hundred bytes. */
54
+ const STANDARD_INFO = 4096;
55
+ /** A source's bytes from `from` to `to`, in pieces. */
56
+ async function* chunks(source, from, to) {
57
+ for (let at = from; at < to; at += 65536)
58
+ yield source.read(at, Math.min(65536, to - at));
59
+ }
60
+ const required = (reason, data = {}) => new errors_1.StopError(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.PasswordRequired, `No password at hand opens the file: ${reason}`, { ...data, reason });
61
+ /** A file encrypted with an algorithm this reader does not decrypt. It never reaches OpenSSL. */
62
+ const unsupported = (algorithm, data = {}) => new errors_1.StopError(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.UnsupportedAlgorithm, `The file is encrypted with ${algorithm}, which this reader does not decrypt`, { ...data, algorithm });
63
+ const HASHES = new Map([
64
+ ['SHA1', 'sha1'],
65
+ ['SHA256', 'sha256'],
66
+ ['SHA384', 'sha384'],
67
+ ['SHA512', 'sha512'],
68
+ ]);
69
+ const AES = new Map([
70
+ [128, { cbc: 'aes-128-cbc', ecb: 'aes-128-ecb', detail: types_1.OfficeDetail.Aes128, bits: 128 }],
71
+ [192, { cbc: 'aes-192-cbc', ecb: 'aes-192-ecb', detail: types_1.OfficeDetail.Aes192, bits: 192 }],
72
+ [256, { cbc: 'aes-256-cbc', ecb: 'aes-256-ecb', detail: types_1.OfficeDetail.Aes256, bits: 256 }],
73
+ ]);
74
+ /** CALG_AES_128, CALG_AES_192 and CALG_AES_256, by key size. */
75
+ const STANDARD_AES = new Map([
76
+ [128, 0x660e],
77
+ [192, 0x660f],
78
+ [256, 0x6610],
79
+ ]);
80
+ /** A value from the file for a message, cut short. */
81
+ const shown = (v) => (v ?? 'none').slice(0, 40);
82
+ /**
83
+ * The passwords tried, in order: the caller's, each once, then the application's default, which Word has none of.
84
+ * One upload can hold files with different passwords, so a password meant for another file must not keep the default
85
+ * from opening this one.
86
+ */
87
+ function passwords(ctx, fallback = EXCEL) {
88
+ const given = [...new Set(ctx.settings.passwords)];
89
+ const out = given.map(password => ({ password }));
90
+ return !fallback || given.includes(fallback.password) ? out : [...out, { password: fallback.password, default: fallback }];
91
+ }
92
+ /** Which password opened the file, and the cipher, as findings. */
93
+ function opened(ctx, tried, cipher, location, bits) {
94
+ const d = tried.default;
95
+ ctx.report(types_1.OfficeCategory.Encrypted, d ? types_1.OfficeDetail.DefaultPassword : types_1.OfficeDetail.UserPassword, location, d ? { password: d.password, application: d.application } : {});
96
+ ctx.report(types_1.OfficeCategory.Encrypted, cipher, location, bits === undefined ? {} : { bits });
97
+ }
98
+ const utf16le = (s) => Buffer.from(s, 'utf16le');
99
+ const le32 = (n) => {
100
+ const b = Buffer.alloc(4);
101
+ b.writeUInt32LE(n >>> 0);
102
+ return b;
103
+ };
104
+ const hash = (algorithm, ...parts) => {
105
+ const h = crypto.createHash(algorithm);
106
+ for (const p of parts)
107
+ h.update(p);
108
+ return h.digest();
109
+ };
110
+ /** RC4, which OpenSSL 3 leaves out of its default provider. */
111
+ class Rc4 {
112
+ s = new Uint8Array(256);
113
+ i = 0;
114
+ j = 0;
115
+ constructor(key) {
116
+ for (let k = 0; k < 256; k++)
117
+ this.s[k] = k;
118
+ for (let k = 0, j = 0; k < 256; k++) {
119
+ j = (j + this.s[k] + key[k % key.length]) & 255;
120
+ [this.s[k], this.s[j]] = [this.s[j], this.s[k]];
121
+ }
122
+ }
123
+ /** XORs the next keystream bytes into a copy of `data`. */
124
+ crypt(data) {
125
+ const out = new Uint8Array(data.length);
126
+ for (let k = 0; k < data.length; k++) {
127
+ this.i = (this.i + 1) & 255;
128
+ this.j = (this.j + this.s[this.i]) & 255;
129
+ [this.s[this.i], this.s[this.j]] = [this.s[this.j], this.s[this.i]];
130
+ out[k] = data[k] ^ this.s[(this.s[this.i] + this.s[this.j]) & 255];
131
+ }
132
+ return out;
133
+ }
134
+ }
135
+ exports.Rc4 = Rc4;
136
+ /** Office binary document RC4 (MS-OFFCRYPTO 2.3.6): MD5, a 40-bit truncation and the salt. */
137
+ function rc4Key(password, salt) {
138
+ const h0 = hash('md5', utf16le(password)).subarray(0, 5);
139
+ const buffer = Buffer.concat(Array.from({ length: 16 }, () => Buffer.concat([h0, salt])));
140
+ const h1 = hash('md5', buffer).subarray(0, 5);
141
+ return block => hash('md5', h1, le32(block));
142
+ }
143
+ /** RC4 CryptoAPI (MS-OFFCRYPTO 2.3.5.2): SHA-1 of the salt and password, then of the block number. A 40-bit key is padded to 128 bits. */
144
+ function cryptoApiKey(password, salt, bits) {
145
+ const h0 = hash('sha1', salt, utf16le(password));
146
+ return block => {
147
+ const h = hash('sha1', h0, le32(block));
148
+ return bits === 40 ? Buffer.concat([h.subarray(0, 5), Buffer.alloc(11)]) : h.subarray(0, bits / 8);
149
+ };
150
+ }
151
+ /**
152
+ * The key of a binary document encrypted with RC4 (MS-OFFCRYPTO 2.3.6) or RC4 CryptoAPI (2.3.5), from its
153
+ * EncryptionHeader, which `r` reads from its version on. Returns undefined when no password at hand opens it.
154
+ * `cryptoApiOnly` refuses plain RC4, as PowerPoint does.
155
+ */
156
+ function binaryKey(ctx, r, stream, fallback, cryptoApiOnly = false) {
157
+ const major = r.u16();
158
+ const minor = r.u16();
159
+ if (major === 1 && minor === 1 && !cryptoApiOnly) {
160
+ const salt = r.bytes(16);
161
+ const verifier = r.bytes(16);
162
+ const verifierHash = r.bytes(16);
163
+ for (const p of passwords(ctx, fallback)) {
164
+ const k = rc4Key(p.password, salt);
165
+ const plain = new Rc4(k(0)).crypt(Buffer.concat([verifier, verifierHash]));
166
+ if (hash('md5', plain.subarray(0, 16)).equals(Buffer.from(plain.subarray(16, 32)))) {
167
+ opened(ctx, p, types_1.OfficeDetail.Rc4_40, stream, 40);
168
+ return k;
169
+ }
170
+ }
171
+ return undefined;
172
+ }
173
+ if ((major === 2 || major === 3 || major === 4) && minor === 2) {
174
+ r.skip(4);
175
+ const headerSize = r.u32();
176
+ const header = new oleobj_1.ByteReader(r.bytes(headerSize));
177
+ header.skip(8);
178
+ const algorithm = header.u32();
179
+ header.skip(4);
180
+ const bits = header.u32() || 40;
181
+ if (algorithm !== 0x6801 || bits < 40 || bits > 128 || bits % 8)
182
+ throw unsupported(`CryptoAPI algorithm 0x${algorithm.toString(16)} with a ${bits}-bit key`, { stream });
183
+ const cipher = bits === 40 ? types_1.OfficeDetail.Rc4_40 : bits === 128 ? types_1.OfficeDetail.Rc4_128 : types_1.OfficeDetail.Rc4Other;
184
+ const saltSize = r.u32();
185
+ const salt = r.bytes(saltSize);
186
+ const verifier = r.bytes(16);
187
+ const hashSize = r.u32();
188
+ const verifierHash = r.bytes(Math.min(hashSize, r.left));
189
+ for (const p of passwords(ctx, fallback)) {
190
+ const k = cryptoApiKey(p.password, salt, bits);
191
+ const plain = new Rc4(k(0)).crypt(Buffer.concat([verifier, verifierHash]));
192
+ if (hash('sha1', plain.subarray(0, 16)).equals(Buffer.from(plain.subarray(16, 36)))) {
193
+ opened(ctx, p, cipher, stream, bits);
194
+ return k;
195
+ }
196
+ }
197
+ return undefined;
198
+ }
199
+ throw unsupported(`encryption version ${major}.${minor}`, { stream });
200
+ }
201
+ /**
202
+ * Opens a workbook that its FILEPASS record (MS-XLS 2.4.117) encrypts with RC4 or RC4 CryptoAPI, with the caller's
203
+ * password or VelvetSweatshop. The records after FILEPASS are encrypted in 1024-byte blocks of the stream, each with its
204
+ * own key. Their headers are not, but take keystream bytes, and some records are never encrypted. XOR obfuscation,
205
+ * the only kind an Excel 5.0 or 95 workbook has, is undone the same way record by record.
206
+ */
207
+ function unlockWorkbook(ctx, filePass, stream) {
208
+ const r = new oleobj_1.ByteReader(filePass.body);
209
+ try {
210
+ // A BIFF5 FILEPASS holds only the XOR key and verifier. BIFF8 names the method first.
211
+ const xor = filePass.body.length === 4 || r.u16() === 0;
212
+ if (xor) {
213
+ const key = r.u16();
214
+ const verifier = r.u16();
215
+ for (const p of passwords(ctx)) {
216
+ const pw = xorPassword(p.password);
217
+ if (!pw.length || xorKey1(pw) !== key || verifier1(pw) !== verifier)
218
+ continue;
219
+ opened(ctx, p, types_1.OfficeDetail.XorObfuscation, stream);
220
+ return xorDecrypt(xorArray1(pw));
221
+ }
222
+ throw required('the workbook is obfuscated with a password that is not VelvetSweatshop', { stream });
223
+ }
224
+ const key = binaryKey(ctx, r, stream, EXCEL);
225
+ if (!key)
226
+ throw required('the workbook is encrypted with a password that is not VelvetSweatshop', { stream });
227
+ return biffDecrypt(key);
228
+ }
229
+ catch (e) {
230
+ if (e instanceof oleobj_1.Truncated)
231
+ throw required('a FILEPASS record cut short', { stream });
232
+ throw e;
233
+ }
234
+ }
235
+ /** XOR obfuscation method 1 tables (MS-OFFCRYPTO 2.3.7.2). */
236
+ const PAD = [0xbb, 0xff, 0xff, 0xba, 0xff, 0xff, 0xb9, 0x80, 0x00, 0xbe, 0x0f, 0x00, 0xbf, 0x0f, 0x00];
237
+ const INITIAL_CODE = [0xe1f0, 0x1d0f, 0xcc9c, 0x84c0, 0x110c, 0x0e10, 0xf1ce, 0x313e, 0x1872, 0xe139, 0xd40f, 0x84f9, 0x280c, 0xa96a, 0x4ec3];
238
+ // biome-ignore format: the table as the specification prints it
239
+ const XOR_MATRIX = [
240
+ 0xaefc, 0x4dd9, 0x9bb2, 0x2745, 0x4e8a, 0x9d14, 0x2a09, 0x7b61, 0xf6c2, 0xfda5, 0xeb6b, 0xc6f7, 0x9dcf, 0x2bbf, 0x4563, 0x8ac6,
241
+ 0x05ad, 0x0b5a, 0x16b4, 0x2d68, 0x5ad0, 0x0375, 0x06ea, 0x0dd4, 0x1ba8, 0x3750, 0x6ea0, 0xdd40, 0xd849, 0xa0b3, 0x5147, 0xa28e,
242
+ 0x553d, 0xaa7a, 0x44d5, 0x6f45, 0xde8a, 0xad35, 0x4a4b, 0x9496, 0x390d, 0x721a, 0xeb23, 0xc667, 0x9cef, 0x29ff, 0x53fe, 0xa7fc,
243
+ 0x5fd9, 0x47d3, 0x8fa6, 0x0f6d, 0x1eda, 0x3db4, 0x7b68, 0xf6d0, 0xb861, 0x60e3, 0xc1c6, 0x93ad, 0x377b, 0x6ef6, 0xddec, 0x45a0,
244
+ 0x8b40, 0x06a1, 0x0d42, 0x1a84, 0x3508, 0x6a10, 0xaa51, 0x4483, 0x8906, 0x022d, 0x045a, 0x08b4, 0x1168, 0x76b4, 0xed68, 0xcaf1,
245
+ 0x85c3, 0x1ba7, 0x374e, 0x6e9c, 0x3730, 0x6e60, 0xdcc0, 0xa9a1, 0x4363, 0x86c6, 0x1dad, 0x3331, 0x6662, 0xccc4, 0x89a9, 0x0373,
246
+ 0x06e6, 0x0dcc, 0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48c4,
247
+ ];
248
+ /**
249
+ * A password as the XOR methods read it (MS-OFFCRYPTO 2.3.7.4): each character's low byte, or its high byte when the
250
+ * low one is 0, at most 15 of them.
251
+ */
252
+ function xorPassword(password) {
253
+ const out = [];
254
+ for (let i = 0; i < password.length && out.length < 15; i++) {
255
+ const c = password.charCodeAt(i);
256
+ out.push(c & 0xff ? c & 0xff : (c >> 8) & 0xff);
257
+ }
258
+ return Uint8Array.from(out);
259
+ }
260
+ /** CreateXorKey_Method1 (MS-OFFCRYPTO 2.3.7.2). */
261
+ function xorKey1(pw) {
262
+ let key = INITIAL_CODE[pw.length - 1];
263
+ let element = 0x68;
264
+ for (let i = pw.length - 1; i >= 0; i--) {
265
+ let c = pw[i];
266
+ for (let k = 0; k < 7; k++) {
267
+ if (c & 0x40)
268
+ key ^= XOR_MATRIX[element];
269
+ c *= 2;
270
+ element--;
271
+ }
272
+ }
273
+ return key & 0xffff;
274
+ }
275
+ /** CreatePasswordVerifier_Method1 (MS-OFFCRYPTO 2.3.7.1). */
276
+ function verifier1(pw) {
277
+ let v = 0;
278
+ for (const b of [...pw].reverse().concat(pw.length))
279
+ v = (((v & 0x4000) >> 14) | ((v << 1) & 0x7fff)) ^ b;
280
+ return (v ^ 0xce4b) & 0xffff;
281
+ }
282
+ /** One right rotation of a byte. */
283
+ const ror = (b) => ((b >> 1) | (b << 7)) & 0xff;
284
+ /** CreateXorArray_Method1 (MS-OFFCRYPTO 2.3.7.2). */
285
+ function xorArray1(pw) {
286
+ const key = xorKey1(pw);
287
+ const hi = key >> 8;
288
+ const lo = key & 0xff;
289
+ const out = new Uint8Array(16);
290
+ let i = pw.length;
291
+ if (i % 2 === 1) {
292
+ out[i] = ror(PAD[0] ^ hi);
293
+ i--;
294
+ out[i] = ror(pw[pw.length - 1] ^ lo);
295
+ }
296
+ while (i > 0) {
297
+ i--;
298
+ out[i] = ror(pw[i] ^ hi);
299
+ i--;
300
+ out[i] = ror(pw[i] ^ lo);
301
+ }
302
+ let at = 15;
303
+ for (let pad = 15 - pw.length; pad > 0;) {
304
+ out[at--] = ror(PAD[pad--] ^ hi);
305
+ out[at--] = ror((PAD[pad--] ?? 0) ^ lo);
306
+ }
307
+ return out;
308
+ }
309
+ /** CreateXorArray_Method2 (MS-OFFCRYPTO 2.3.7.5), which Word uses. */
310
+ function xorArray2(pw) {
311
+ const key = xorKey1(pw);
312
+ const out = new Uint8Array(16);
313
+ for (let i = 0; i < 16; i++)
314
+ out[i] = i < pw.length ? pw[i] : PAD[i - pw.length];
315
+ for (let i = 0; i < 16; i += 2) {
316
+ out[i] = ror(out[i] ^ (key & 0xff));
317
+ out[i + 1] = ror(out[i + 1] ^ (key >> 8));
318
+ }
319
+ return out;
320
+ }
321
+ /**
322
+ * Undoes XOR obfuscation method 1 in each record after FILEPASS (MS-OFFCRYPTO 2.3.7.3). The array index of a record's
323
+ * first byte is its stream offset plus its size, as Excel and LibreOffice count it, and each byte is XORed and then
324
+ * rotated right by 5 bits. The records MS-XLS leaves plain, and BoundSheet8's lbPlyPos, stay as they are.
325
+ */
326
+ function xorDecrypt(array) {
327
+ return r => {
328
+ if (PLAIN.has(r.type))
329
+ return r.body;
330
+ const out = new Uint8Array(r.body);
331
+ const start = r.offset + 4;
332
+ for (let k = r.type === 0x0085 ? 4 : 0; k < out.length; k++) {
333
+ const v = out[k] ^ array[(start + r.body.length + k) % 16];
334
+ out[k] = ((v >> 5) | (v << 3)) & 0xff;
335
+ }
336
+ return out;
337
+ };
338
+ }
339
+ /** RC4 keyed per 512-byte block, as Word encrypts its streams, applied from `plain` on. */
340
+ function blockStream(key, plain) {
341
+ const cache = new Map();
342
+ const keystream = (b) => {
343
+ let ks = cache.get(b);
344
+ if (!ks) {
345
+ if (cache.size > 64)
346
+ cache.clear();
347
+ ks = new Rc4(key(b)).crypt(new Uint8Array(512));
348
+ cache.set(b, ks);
349
+ }
350
+ return ks;
351
+ };
352
+ return (offset, bytes) => {
353
+ const out = new Uint8Array(bytes);
354
+ for (let k = Math.max(0, plain - offset); k < out.length; k++) {
355
+ const at = offset + k;
356
+ out[k] ^= keystream(Math.floor(at / 512))[at % 512];
357
+ }
358
+ return out;
359
+ };
360
+ }
361
+ /** XOR obfuscation method 2 over a stream (MS-OFFCRYPTO 2.3.7.6), from `plain` on. */
362
+ function xorStream(array, plain) {
363
+ return (offset, bytes) => {
364
+ const out = new Uint8Array(bytes);
365
+ for (let k = Math.max(0, plain - offset); k < out.length; k++) {
366
+ const a = array[(offset + k) % 16];
367
+ // A byte that was 0x00 or equal to its array element was left as it was.
368
+ if (out[k] !== 0 && out[k] !== a)
369
+ out[k] ^= a;
370
+ }
371
+ return out;
372
+ };
373
+ }
374
+ /**
375
+ * Opens an encrypted Word 97 to 2003 document (MS-DOC 2.2.6) with the caller's passwords. XOR obfuscation keeps its
376
+ * verifier in FibBase.lKey. RC4 and RC4 CryptoAPI keep their EncryptionHeader in the table stream's first lKey bytes.
377
+ * The WordDocument stream stays plain for its first 68 bytes, and Word has no default password.
378
+ */
379
+ async function unlockDocument(ctx, fib, table, stream) {
380
+ const flags = fib[10] | (fib[11] << 8);
381
+ const lKey = (fib[14] | (fib[15] << 8) | (fib[16] << 16) | (fib[17] << 24)) >>> 0;
382
+ if (flags & 0x8000) {
383
+ for (const p of passwords(ctx, undefined)) {
384
+ const pw = xorPassword(p.password);
385
+ if (!pw.length || ((xorKey1(pw) << 16) | verifier1(pw)) >>> 0 !== lKey)
386
+ continue;
387
+ opened(ctx, p, types_1.OfficeDetail.XorObfuscation, stream);
388
+ const array = xorArray2(pw);
389
+ return { word: xorStream(array, 68), table: xorStream(array, 0), data: xorStream(array, 0) };
390
+ }
391
+ throw required('the document is obfuscated with a password not at hand', { stream });
392
+ }
393
+ if (lKey < 8 || lKey > 65536)
394
+ throw required('an EncryptionHeader of an impossible size', { stream });
395
+ try {
396
+ const key = binaryKey(ctx, new oleobj_1.ByteReader(await table.read(0, lKey)), stream, undefined);
397
+ if (!key)
398
+ throw required('the document is encrypted with a password not at hand', { stream });
399
+ return { word: blockStream(key, 68), table: blockStream(key, lKey), data: blockStream(key, 0) };
400
+ }
401
+ catch (e) {
402
+ if (e instanceof oleobj_1.Truncated)
403
+ throw required('an EncryptionHeader cut short', { stream });
404
+ throw e;
405
+ }
406
+ }
407
+ /**
408
+ * Opens an encrypted PowerPoint 97 to 2003 presentation (MS-PPT 2.3.7): `session` is the CryptSession10Container's
409
+ * data, an RC4 CryptoAPI EncryptionHeader. Returns the key of each persist object, whose block number is its persist
410
+ * id, after the caller's passwords and PowerPoint's own default have been tried.
411
+ */
412
+ function unlockPresentation(ctx, session, stream) {
413
+ try {
414
+ const key = binaryKey(ctx, new oleobj_1.ByteReader(session), stream, POWERPOINT, true);
415
+ if (!key)
416
+ throw required('the presentation is encrypted with a password not at hand', { stream });
417
+ return id => new Rc4(key(id));
418
+ }
419
+ catch (e) {
420
+ if (e instanceof oleobj_1.Truncated)
421
+ throw required('a CryptSession10Container cut short', { stream });
422
+ throw e;
423
+ }
424
+ }
425
+ /** Records MS-XLS 2.2.10 never encrypts: BOF, FILEPASS, UsrExcl, FileLock, InterfaceHdr, RRDInfo and RRDHead. */
426
+ const PLAIN = new Set([0x0809, 0x002f, 0x0194, 0x0195, 0x00e1, 0x0196, 0x0138]);
427
+ /** Only the records after FILEPASS reach the function it returns. */
428
+ function biffDecrypt(key) {
429
+ let block = -1;
430
+ let stream = new Uint8Array(0);
431
+ const keystream = (b) => {
432
+ if (b !== block) {
433
+ block = b;
434
+ stream = new Rc4(key(b)).crypt(new Uint8Array(1024));
435
+ }
436
+ return stream;
437
+ };
438
+ return r => {
439
+ if (PLAIN.has(r.type))
440
+ return r.body;
441
+ const out = new Uint8Array(r.body);
442
+ // BoundSheet8's lbPlyPos stays plain, so a reader can find each sheet without the key.
443
+ const from = r.type === 0x0085 ? 4 : 0;
444
+ for (let k = from; k < out.length; k++) {
445
+ const at = r.offset + 4 + k;
446
+ out[k] ^= keystream(Math.floor(at / 1024))[at % 1024];
447
+ }
448
+ return out;
449
+ };
450
+ }
451
+ /**
452
+ * Inspects an encrypted OOXML package: EncryptionInfo (MS-OFFCRYPTO 2.3.4) names the cipher, and the caller's password
453
+ * or VelvetSweatshop gives the key. The package it decrypts goes through the OOXML rules, read in place: Agile
454
+ * encryption works in 4,096-byte segments and Standard in 16-byte blocks, so nothing is decrypted that is not read.
455
+ */
456
+ async function openPackage(ctx, cfb, info, pkg) {
457
+ const source = cfb.source(info);
458
+ const version = await source.read(0, 8);
459
+ const major = version.length >= 4 ? version[0] | (version[1] << 8) : 0;
460
+ const minor = version.length >= 4 ? version[2] | (version[3] << 8) : 0;
461
+ let cipher;
462
+ // Agile's XML goes to the tokenizer in pieces. Standard's binary header is a few hundred bytes.
463
+ if (major === 4 && minor === 4)
464
+ cipher = await agile(ctx, chunks(source, 8, info.size));
465
+ else if ((major === 3 || major === 4) && minor === 2)
466
+ cipher = standard(await source.read(8, STANDARD_INFO));
467
+ else
468
+ throw unsupported(`encryption version ${major}.${minor}`, { stream: info.name });
469
+ const head = await cfb.source(pkg).read(0, 8);
470
+ if (head.length < 8)
471
+ throw required('an EncryptedPackage stream cut short', { stream: pkg.name });
472
+ const size = Number(Buffer.from(head).readBigUInt64LE(0));
473
+ if (size > pkg.size - 8)
474
+ throw required('an EncryptedPackage stream shorter than the size it gives', { stream: pkg.name });
475
+ for (const p of passwords(ctx)) {
476
+ (0, errors_1.checkTime)(ctx.settings.deadline);
477
+ const key = cipher.key(p.password);
478
+ if (!key)
479
+ continue;
480
+ opened(ctx, p, cipher.cipher.detail, pkg.name, cipher.cipher.bits);
481
+ const data = cfb.source(pkg);
482
+ return { source: cipher.source((offset, length) => data.read(8 + offset, length), size, key), size };
483
+ }
484
+ throw required('the package is encrypted with a password that is not VelvetSweatshop', { stream: pkg.name });
485
+ }
486
+ /** AES without padding over whole blocks: ECB, or CBC from `iv`. */
487
+ function aes(cipher, key, data, iv) {
488
+ const d = crypto.createDecipheriv(iv ? cipher.cbc : cipher.ecb, key, iv ?? null);
489
+ d.setAutoPadding(false);
490
+ const whole = data.subarray(0, data.length - (data.length % 16));
491
+ return Buffer.concat([d.update(whole), d.final()]);
492
+ }
493
+ /**
494
+ * AES in CFB with an 8-bit window, which Agile's ChainingModeCFB names, built on AES-ECB. Each byte is XORed with the
495
+ * first byte of the encryption of the 16 ciphertext bytes before it, the IV standing in front. Decryption knows every
496
+ * ciphertext byte, so all those blocks go through ECB in one call.
497
+ */
498
+ function aesCfb8(cipher, key, data, iv) {
499
+ const feed = Buffer.concat([iv.subarray(0, 16), data]);
500
+ const blocks = Buffer.alloc(data.length * 16);
501
+ for (let i = 0; i < data.length; i++)
502
+ feed.copy(blocks, i * 16, i, i + 16);
503
+ const e = crypto.createCipheriv(cipher.ecb, key, null);
504
+ e.setAutoPadding(false);
505
+ const stream = Buffer.concat([e.update(blocks), e.final()]);
506
+ const out = new Uint8Array(data.length);
507
+ for (let i = 0; i < data.length; i++)
508
+ out[i] = data[i] ^ stream[i * 16];
509
+ return out;
510
+ }
511
+ /** Decrypts with the chaining an Agile element names. */
512
+ const agileDecrypt = (cipher, cfb, key, data, iv) => (cfb ? aesCfb8(cipher, key, data, iv) : aes(cipher, key, data, iv));
513
+ /** Standard encryption (MS-OFFCRYPTO 2.3.4.5): AES-ECB with a key from SHA-1 over 50,000 rounds. */
514
+ function standard(b) {
515
+ const r = new oleobj_1.ByteReader(b);
516
+ try {
517
+ const headerSize = r.u32();
518
+ const header = new oleobj_1.ByteReader(r.bytes(headerSize));
519
+ header.skip(8);
520
+ const algorithm = header.u32();
521
+ const hashAlgorithm = header.u32();
522
+ const bits = header.u32();
523
+ // CALG_AES_128, _192 and _256, which name their key size, with SHA-1.
524
+ const cipher = AES.get(bits);
525
+ if (!cipher || algorithm !== STANDARD_AES.get(bits) || (hashAlgorithm !== 0x8004 && hashAlgorithm !== 0))
526
+ throw unsupported(`Standard encryption with algorithm 0x${algorithm.toString(16)}, hash 0x${hashAlgorithm.toString(16)} and a ${bits}-bit key`);
527
+ r.skip(4);
528
+ const salt = r.bytes(16);
529
+ const verifier = r.bytes(16);
530
+ r.skip(4);
531
+ const verifierHash = r.bytes(32);
532
+ const keyOf = (password) => {
533
+ let h = hash('sha1', salt, utf16le(password));
534
+ for (let i = 0; i < 50000; i++)
535
+ h = hash('sha1', le32(i), h);
536
+ const final = hash('sha1', h, le32(0));
537
+ const x = (fill) => {
538
+ const buf = Buffer.alloc(64, fill);
539
+ for (let i = 0; i < final.length; i++)
540
+ buf[i] ^= final[i];
541
+ return hash('sha1', buf);
542
+ };
543
+ return Buffer.concat([x(0x36), x(0x5c)]).subarray(0, bits / 8);
544
+ };
545
+ return {
546
+ cipher,
547
+ key(password) {
548
+ const key = keyOf(password);
549
+ const v = aes(cipher, key, verifier);
550
+ const vh = aes(cipher, key, verifierHash);
551
+ return hash('sha1', v).equals(Buffer.from(vh.subarray(0, 20))) ? key : undefined;
552
+ },
553
+ source: (read, size, key) => blockSource(size, 16, async (index, count) => aes(cipher, key, await read(index * 16, count * 16))),
554
+ };
555
+ }
556
+ catch (e) {
557
+ if (e instanceof oleobj_1.Truncated)
558
+ throw required('a Standard EncryptionInfo cut short');
559
+ throw e;
560
+ }
561
+ }
562
+ /** Agile encryption (MS-OFFCRYPTO 2.3.4.10): an XML descriptor, a password key encryptor, and AES-CBC in 4,096-byte segments. */
563
+ async function agile(ctx, xml) {
564
+ const found = {};
565
+ const ENCRYPTION = 'http://schemas.microsoft.com/office/2006/encryption';
566
+ const PASSWORD = 'http://schemas.microsoft.com/office/2006/keyEncryptor/password';
567
+ await ctx.parseXml('EncryptionInfo', xml, {
568
+ start(el) {
569
+ const all = Object.fromEntries(el.attributes.filter(a => a.ns === '').map(a => [a.local, a.value]));
570
+ if (el.ns === ENCRYPTION && el.local === 'keyData')
571
+ found.keyData ??= all;
572
+ else if (el.ns === PASSWORD && el.local === 'encryptedKey')
573
+ found.encryptedKey ??= all;
574
+ },
575
+ });
576
+ const kd = found.keyData;
577
+ const ek = found.encryptedKey;
578
+ if (!kd || !ek)
579
+ throw required('an Agile descriptor without key data or a password key');
580
+ /** The hash, cipher and chaining a key data or key encryptor element names, when all are on the allowlist. */
581
+ const algorithms = (a) => {
582
+ const h = HASHES.get(a.hashAlgorithm);
583
+ const c = AES.get(Number(a.keyBits));
584
+ const chaining = a.cipherChaining;
585
+ if (!h || !c || a.cipherAlgorithm !== 'AES' || (chaining !== 'ChainingModeCBC' && chaining !== 'ChainingModeCFB') || Number(a.blockSize) !== 16)
586
+ throw unsupported(`Agile encryption with ${shown(a.cipherAlgorithm)} in ${shown(a.cipherChaining)}, a ${shown(a.keyBits)}-bit key and ${shown(a.hashAlgorithm)}`);
587
+ return { h, c, cfb: chaining === 'ChainingModeCFB' };
588
+ };
589
+ const { h: dh, c: dc, cfb: dcfb } = algorithms(kd);
590
+ const { h: eh, c: ec, cfb: ecfb } = algorithms(ek);
591
+ const spin = Number(ek.spinCount);
592
+ if (!Number.isInteger(spin) || spin < 0 || spin > MAX_SPIN)
593
+ throw required('an Agile spin count past 10,000,000');
594
+ const b64 = (s) => Buffer.from(s ?? '', 'base64');
595
+ const pad = (b, n, fill) => (b.length >= n ? b.subarray(0, n) : Buffer.concat([b, Buffer.alloc(n - b.length, fill)]));
596
+ const salt = b64(ek.saltValue);
597
+ const BLOCKS = { input: Buffer.from('fea7d2763b4b9e79', 'hex'), value: Buffer.from('d7aa0f6d3061344e', 'hex'), key: Buffer.from('146e0be7abacd0d6', 'hex') };
598
+ const dsalt = b64(kd.saltValue);
599
+ return {
600
+ cipher: dc,
601
+ key(password) {
602
+ let h = hash(eh, salt, utf16le(password));
603
+ for (let i = 0; i < spin; i++) {
604
+ if (i % 65536 === 0)
605
+ (0, errors_1.checkTime)(ctx.settings.deadline);
606
+ h = hash(eh, le32(i), h);
607
+ }
608
+ const derive = (block) => pad(hash(eh, h, block), ec.bits / 8, 0x36);
609
+ const iv = pad(salt, 16, 0x36);
610
+ const input = agileDecrypt(ec, ecfb, derive(BLOCKS.input), b64(ek.encryptedVerifierHashInput), iv).subarray(0, Number(ek.saltSize) || 16);
611
+ const expected = agileDecrypt(ec, ecfb, derive(BLOCKS.value), b64(ek.encryptedVerifierHashValue), iv);
612
+ const got = hash(eh, input);
613
+ if (!got.equals(Buffer.from(expected.subarray(0, got.length))))
614
+ return undefined;
615
+ return agileDecrypt(ec, ecfb, derive(BLOCKS.key), b64(ek.encryptedKeyValue), iv).subarray(0, dc.bits / 8);
616
+ },
617
+ source: (read, size, key) => blockSource(size, 4096, async (index, count) => {
618
+ const raw = await read(index * 4096, count * 4096);
619
+ const out = [];
620
+ for (let k = 0; k * 4096 < raw.length; k++)
621
+ out.push(agileDecrypt(dc, dcfb, key, raw.subarray(k * 4096, (k + 1) * 4096), pad(hash(dh, dsalt, le32(index + k)), 16, 0x36)));
622
+ return Buffer.concat(out);
623
+ }),
624
+ };
625
+ }
626
+ /** A source over decrypted data, decrypting the whole blocks each read covers, and keeping the last ones decrypted. */
627
+ function blockSource(size, unit, decrypt) {
628
+ let cached;
629
+ return {
630
+ size: async () => size,
631
+ async read(offset, length) {
632
+ const end = Math.min(size, offset + length);
633
+ const first = Math.floor(offset / unit);
634
+ const last = Math.floor((end - 1) / unit);
635
+ let bytes;
636
+ if (cached && cached.index <= first && cached.index + cached.bytes.length / unit > last)
637
+ bytes = cached.bytes.subarray((first - cached.index) * unit);
638
+ else {
639
+ // A small read decrypts 64 KiB from where it starts, since the ZIP reader reads on from there.
640
+ const count = Math.max(last - first + 1, Math.ceil(65536 / unit));
641
+ bytes = await decrypt(first, count);
642
+ cached = { index: first, bytes };
643
+ }
644
+ return bytes.subarray(offset - first * unit, end - first * unit);
645
+ },
646
+ };
647
+ }
package/dist/ole.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { CompoundFile } from './cfb';
2
+ import type { RunContext } from './context';
3
+ /** The families a compound file can hold, by the stream or storage that marks each. */
4
+ type Family = 'word' | 'excel' | 'powerpoint' | 'visio' | 'outlook' | 'publisher' | 'project' | 'encrypted';
5
+ /** What the root of a compound file holds, read by the names of its children, ignoring case as MS-CFB does. */
6
+ export declare function families(cfb: CompoundFile): Family[];
7
+ /**
8
+ * Inspects a compound file: the rules every compound file gets, over its whole tree, and then those of the format
9
+ * its root names.
10
+ */
11
+ export declare function inspectCompound(ctx: RunContext, cfb: CompoundFile): Promise<void>;
12
+ export {};