@patrickdk77/office-defuse 0.1.1 → 0.2.1

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 (82) hide show
  1. package/README.md +211 -43
  2. package/dist/cfb-writer.d.ts +25 -0
  3. package/dist/cfb-writer.js +314 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +357 -0
  6. package/dist/contained.d.ts +46 -0
  7. package/dist/contained.js +152 -0
  8. package/dist/context.d.ts +107 -12
  9. package/dist/context.js +101 -24
  10. package/dist/detect.d.ts +13 -0
  11. package/dist/detect.js +83 -0
  12. package/dist/engine.d.ts +66 -1
  13. package/dist/engine.js +290 -35
  14. package/dist/errors.d.ts +17 -0
  15. package/dist/errors.js +27 -1
  16. package/dist/fields.d.ts +20 -13
  17. package/dist/fields.js +65 -23
  18. package/dist/findings.d.ts +2 -1
  19. package/dist/findings.js +17 -1
  20. package/dist/flat.d.ts +33 -0
  21. package/dist/flat.js +555 -0
  22. package/dist/formats.d.ts +9 -0
  23. package/dist/formats.js +7 -1
  24. package/dist/formulas.d.ts +27 -0
  25. package/dist/formulas.js +157 -1
  26. package/dist/index.d.ts +4 -2
  27. package/dist/index.js +11 -1
  28. package/dist/index.mjs +1 -1
  29. package/dist/io.d.ts +62 -1
  30. package/dist/io.js +245 -1
  31. package/dist/links.d.ts +27 -0
  32. package/dist/links.js +106 -1
  33. package/dist/msg.d.ts +3 -2
  34. package/dist/msg.js +172 -18
  35. package/dist/objects.d.ts +8 -5
  36. package/dist/objects.js +90 -20
  37. package/dist/odf.d.ts +87 -1
  38. package/dist/odf.js +204 -76
  39. package/dist/offcrypto.d.ts +24 -1
  40. package/dist/offcrypto.js +308 -69
  41. package/dist/ole.d.ts +19 -2
  42. package/dist/ole.js +98 -58
  43. package/dist/oleobj.d.ts +11 -0
  44. package/dist/oleobj.js +20 -3
  45. package/dist/ooxml.d.ts +108 -2
  46. package/dist/ooxml.js +392 -109
  47. package/dist/package.d.ts +14 -0
  48. package/dist/package.js +23 -7
  49. package/dist/plan.d.ts +104 -0
  50. package/dist/plan.js +73 -0
  51. package/dist/plugins.d.ts +8 -0
  52. package/dist/plugins.js +35 -0
  53. package/dist/ppt.js +45 -5
  54. package/dist/rewrite-cfb.d.ts +14 -0
  55. package/dist/rewrite-cfb.js +52 -0
  56. package/dist/rewrite-rtf.d.ts +9 -0
  57. package/dist/rewrite-rtf.js +45 -0
  58. package/dist/rewrite-xml.d.ts +36 -0
  59. package/dist/rewrite-xml.js +341 -0
  60. package/dist/rewrite-zip.d.ts +8 -0
  61. package/dist/rewrite-zip.js +498 -0
  62. package/dist/rtf.js +216 -42
  63. package/dist/rtfcomp.d.ts +7 -0
  64. package/dist/rtfcomp.js +80 -0
  65. package/dist/score.js +1 -1
  66. package/dist/sniff.d.ts +5 -0
  67. package/dist/sniff.js +38 -0
  68. package/dist/types.d.ts +66 -6
  69. package/dist/types.js +14 -0
  70. package/dist/vsd.d.ts +13 -0
  71. package/dist/vsd.js +689 -0
  72. package/dist/word.d.ts +4 -4
  73. package/dist/word.js +70 -23
  74. package/dist/xls.d.ts +10 -4
  75. package/dist/xls.js +58 -10
  76. package/dist/xml.d.ts +30 -0
  77. package/dist/xml.js +45 -23
  78. package/dist/zip-writer.d.ts +4 -0
  79. package/dist/zip-writer.js +20 -7
  80. package/dist/zip.d.ts +11 -2
  81. package/dist/zip.js +67 -6
  82. package/package.json +4 -1
package/dist/offcrypto.js CHANGED
@@ -33,8 +33,10 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.Rc4 = exports.DEFAULT_PASSWORD = void 0;
36
+ exports.Rc4 = exports.POWERPOINT_PASSWORD = exports.DEFAULT_PASSWORD = void 0;
37
37
  exports.unlockWorkbook = unlockWorkbook;
38
+ exports.unlockDocument = unlockDocument;
39
+ exports.unlockPresentation = unlockPresentation;
38
40
  exports.openPackage = openPackage;
39
41
  const crypto = __importStar(require("node:crypto"));
40
42
  const errors_1 = require("./errors");
@@ -42,6 +44,10 @@ const oleobj_1 = require("./oleobj");
42
44
  const types_1 = require("./types");
43
45
  /** The password Excel opens a workbook with when nobody set one to open it (MS-OFFCRYPTO 2.4.3). */
44
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' };
45
51
  /** MS-OFFCRYPTO allows a spin count of at most 10,000,000. */
46
52
  const MAX_SPIN = 10_000_000;
47
53
  /** More of a Standard EncryptionInfo than its header and verifier take, which are a few hundred bytes. */
@@ -73,15 +79,21 @@ const STANDARD_AES = new Map([
73
79
  ]);
74
80
  /** A value from the file for a message, cut short. */
75
81
  const shown = (v) => (v ?? 'none').slice(0, 40);
76
- /** The passwords tried, in order: the caller's, then Excel's default. */
77
- function passwords(ctx) {
78
- const out = ctx.settings.password !== undefined ? [{ password: ctx.settings.password, supplied: true }] : [];
79
- return [...out, { password: exports.DEFAULT_PASSWORD, supplied: false }];
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 }];
80
91
  }
81
92
  /** Which password opened the file, and the cipher, as findings. */
82
- function opened(ctx, supplied, cipher, location, bits) {
83
- ctx.report(types_1.OfficeCategory.Encrypted, supplied ? types_1.OfficeDetail.UserPassword : types_1.OfficeDetail.DefaultPassword, location, supplied ? {} : { password: exports.DEFAULT_PASSWORD });
84
- ctx.report(types_1.OfficeCategory.Encrypted, cipher, location, { bits });
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 });
85
97
  }
86
98
  const utf16le = (s) => Buffer.from(s, 'utf16le');
87
99
  const le32 = (n) => {
@@ -136,73 +148,279 @@ function cryptoApiKey(password, salt, bits) {
136
148
  return bits === 40 ? Buffer.concat([h.subarray(0, 5), Buffer.alloc(11)]) : h.subarray(0, bits / 8);
137
149
  };
138
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
+ }
139
201
  /**
140
202
  * Opens a workbook that its FILEPASS record (MS-XLS 2.4.117) encrypts with RC4 or RC4 CryptoAPI, with the caller's
141
203
  * password or VelvetSweatshop. The records after FILEPASS are encrypted in 1024-byte blocks of the stream, each with its
142
- * own key. Their headers are not, but take keystream bytes, and some records are never encrypted.
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.
143
206
  */
144
207
  function unlockWorkbook(ctx, filePass, stream) {
145
208
  const r = new oleobj_1.ByteReader(filePass.body);
146
- let key;
147
- let cipher = types_1.OfficeDetail.Rc4_40;
148
- let bits = 40;
149
209
  try {
150
- const type = r.u16();
151
- if (type !== 1)
152
- throw unsupported('XOR obfuscation', { stream });
153
- const major = r.u16();
154
- const minor = r.u16();
155
- if (major === 1 && minor === 1) {
156
- const salt = r.bytes(16);
157
- const verifier = r.bytes(16);
158
- const verifierHash = r.bytes(16);
159
- for (const p of passwords(ctx)) {
160
- const k = rc4Key(p.password, salt);
161
- const plain = new Rc4(k(0)).crypt(Buffer.concat([verifier, verifierHash]));
162
- if (hash('md5', plain.subarray(0, 16)).equals(Buffer.from(plain.subarray(16, 32)))) {
163
- key = k;
164
- opened(ctx, p.supplied, types_1.OfficeDetail.Rc4_40, stream, 40);
165
- break;
166
- }
167
- }
168
- }
169
- else if ((major === 2 || major === 3 || major === 4) && minor === 2) {
170
- r.skip(4);
171
- const headerSize = r.u32();
172
- const header = new oleobj_1.ByteReader(r.bytes(headerSize));
173
- header.skip(8);
174
- const algorithm = header.u32();
175
- header.skip(4);
176
- bits = header.u32() || 40;
177
- if (algorithm !== 0x6801 || bits < 40 || bits > 128 || bits % 8)
178
- throw unsupported(`CryptoAPI algorithm 0x${algorithm.toString(16)} with a ${bits}-bit key`, { stream });
179
- cipher = bits === 40 ? types_1.OfficeDetail.Rc4_40 : bits === 128 ? types_1.OfficeDetail.Rc4_128 : types_1.OfficeDetail.Rc4Other;
180
- const saltSize = r.u32();
181
- const salt = r.bytes(saltSize);
182
- const verifier = r.bytes(16);
183
- const hashSize = r.u32();
184
- const verifierHash = r.bytes(Math.min(hashSize, r.left));
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();
185
215
  for (const p of passwords(ctx)) {
186
- const k = cryptoApiKey(p.password, salt, bits);
187
- const plain = new Rc4(k(0)).crypt(Buffer.concat([verifier, verifierHash]));
188
- if (hash('sha1', plain.subarray(0, 16)).equals(Buffer.from(plain.subarray(16, 36)))) {
189
- key = k;
190
- opened(ctx, p.supplied, cipher, stream, bits);
191
- break;
192
- }
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));
193
221
  }
222
+ throw required('the workbook is obfuscated with a password that is not VelvetSweatshop', { stream });
194
223
  }
195
- else
196
- throw unsupported(`encryption version ${major}.${minor}`, { stream });
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);
197
228
  }
198
229
  catch (e) {
199
230
  if (e instanceof oleobj_1.Truncated)
200
231
  throw required('a FILEPASS record cut short', { stream });
201
232
  throw e;
202
233
  }
203
- if (!key)
204
- throw required('the workbook is encrypted with a password that is not VelvetSweatshop', { stream });
205
- return biffDecrypt(key);
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
+ }
206
424
  }
207
425
  /** Records MS-XLS 2.2.10 never encrypts: BOF, FILEPASS, UsrExcl, FileLock, InterfaceHdr, RRDInfo and RRDHead. */
208
426
  const PLAIN = new Set([0x0809, 0x002f, 0x0194, 0x0195, 0x00e1, 0x0196, 0x0138]);
@@ -259,7 +477,7 @@ async function openPackage(ctx, cfb, info, pkg) {
259
477
  const key = cipher.key(p.password);
260
478
  if (!key)
261
479
  continue;
262
- opened(ctx, p.supplied, cipher.cipher.detail, pkg.name, cipher.cipher.bits);
480
+ opened(ctx, p, cipher.cipher.detail, pkg.name, cipher.cipher.bits);
263
481
  const data = cfb.source(pkg);
264
482
  return { source: cipher.source((offset, length) => data.read(8 + offset, length), size, key), size };
265
483
  }
@@ -272,6 +490,26 @@ function aes(cipher, key, data, iv) {
272
490
  const whole = data.subarray(0, data.length - (data.length % 16));
273
491
  return Buffer.concat([d.update(whole), d.final()]);
274
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));
275
513
  /** Standard encryption (MS-OFFCRYPTO 2.3.4.5): AES-ECB with a key from SHA-1 over 50,000 rounds. */
276
514
  function standard(b) {
277
515
  const r = new oleobj_1.ByteReader(b);
@@ -339,16 +577,17 @@ async function agile(ctx, xml) {
339
577
  const ek = found.encryptedKey;
340
578
  if (!kd || !ek)
341
579
  throw required('an Agile descriptor without key data or a password key');
342
- /** The hash and cipher a key data or key encryptor element names, when both are on the allowlist. */
580
+ /** The hash, cipher and chaining a key data or key encryptor element names, when all are on the allowlist. */
343
581
  const algorithms = (a) => {
344
582
  const h = HASHES.get(a.hashAlgorithm);
345
583
  const c = AES.get(Number(a.keyBits));
346
- if (!h || !c || a.cipherAlgorithm !== 'AES' || a.cipherChaining !== 'ChainingModeCBC' || Number(a.blockSize) !== 16)
584
+ const chaining = a.cipherChaining;
585
+ if (!h || !c || a.cipherAlgorithm !== 'AES' || (chaining !== 'ChainingModeCBC' && chaining !== 'ChainingModeCFB') || Number(a.blockSize) !== 16)
347
586
  throw unsupported(`Agile encryption with ${shown(a.cipherAlgorithm)} in ${shown(a.cipherChaining)}, a ${shown(a.keyBits)}-bit key and ${shown(a.hashAlgorithm)}`);
348
- return { h, c };
587
+ return { h, c, cfb: chaining === 'ChainingModeCFB' };
349
588
  };
350
- const { h: dh, c: dc } = algorithms(kd);
351
- const { h: eh, c: ec } = algorithms(ek);
589
+ const { h: dh, c: dc, cfb: dcfb } = algorithms(kd);
590
+ const { h: eh, c: ec, cfb: ecfb } = algorithms(ek);
352
591
  const spin = Number(ek.spinCount);
353
592
  if (!Number.isInteger(spin) || spin < 0 || spin > MAX_SPIN)
354
593
  throw required('an Agile spin count past 10,000,000');
@@ -368,18 +607,18 @@ async function agile(ctx, xml) {
368
607
  }
369
608
  const derive = (block) => pad(hash(eh, h, block), ec.bits / 8, 0x36);
370
609
  const iv = pad(salt, 16, 0x36);
371
- const input = aes(ec, derive(BLOCKS.input), b64(ek.encryptedVerifierHashInput), iv).subarray(0, Number(ek.saltSize) || 16);
372
- const expected = aes(ec, derive(BLOCKS.value), b64(ek.encryptedVerifierHashValue), iv);
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);
373
612
  const got = hash(eh, input);
374
613
  if (!got.equals(Buffer.from(expected.subarray(0, got.length))))
375
614
  return undefined;
376
- return aes(ec, derive(BLOCKS.key), b64(ek.encryptedKeyValue), iv).subarray(0, dc.bits / 8);
615
+ return agileDecrypt(ec, ecfb, derive(BLOCKS.key), b64(ek.encryptedKeyValue), iv).subarray(0, dc.bits / 8);
377
616
  },
378
617
  source: (read, size, key) => blockSource(size, 4096, async (index, count) => {
379
618
  const raw = await read(index * 4096, count * 4096);
380
619
  const out = [];
381
620
  for (let k = 0; k * 4096 < raw.length; k++)
382
- out.push(aes(dc, key, raw.subarray(k * 4096, (k + 1) * 4096), pad(hash(dh, dsalt, le32(index + k)), 16, 0x36)));
621
+ out.push(agileDecrypt(dc, dcfb, key, raw.subarray(k * 4096, (k + 1) * 4096), pad(hash(dh, dsalt, le32(index + k)), 16, 0x36)));
383
622
  return Buffer.concat(out);
384
623
  }),
385
624
  };
package/dist/ole.d.ts CHANGED
@@ -1,12 +1,29 @@
1
1
  import type { CompoundFile } from './cfb';
2
2
  import type { RunContext } from './context';
3
+ import type { Format } from './formats';
4
+ import { ZipArchive } from './zip';
3
5
  /** 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';
6
+ export type Family = 'word' | 'excel' | 'powerpoint' | 'visio' | 'outlook' | 'publisher' | 'project' | 'encrypted';
5
7
  /** What the root of a compound file holds, read by the names of its children, ignoring case as MS-CFB does. */
6
8
  export declare function families(cfb: CompoundFile): Family[];
9
+ /**
10
+ * The family whose format a compound file holds, by the streams and storages at its root. Throws for a root that
11
+ * holds no Office document, or the documents of two families.
12
+ */
13
+ export declare function mainFamily(cfb: CompoundFile): Family;
14
+ /**
15
+ * The format of a compound file of `family`. A Word document is a template when its FIB says so, and a workbook a
16
+ * template or an add-in when its globals do; both are read without a password. An encrypted package names no format
17
+ * until it is decrypted, so for one this is undefined.
18
+ */
19
+ export declare function compoundFormat(cfb: CompoundFile, family: Family, deadline?: number): Promise<Format | undefined>;
20
+ /**
21
+ * Decrypts the package of an encrypted compound file with the run's passwords, then VelvetSweatshop, and reads it as
22
+ * a ZIP archive. Throws ENCRYPTED/PASSWORD_REQUIRED when no password opens it.
23
+ */
24
+ export declare function openEncryptedPackage(ctx: RunContext, cfb: CompoundFile): Promise<ZipArchive>;
7
25
  /**
8
26
  * Inspects a compound file: the rules every compound file gets, over its whole tree, and then those of the format
9
27
  * its root names.
10
28
  */
11
29
  export declare function inspectCompound(ctx: RunContext, cfb: CompoundFile): Promise<void>;
12
- export {};