@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.
- package/README.md +211 -43
- package/dist/cfb-writer.d.ts +25 -0
- package/dist/cfb-writer.js +314 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +357 -0
- package/dist/contained.d.ts +46 -0
- package/dist/contained.js +152 -0
- package/dist/context.d.ts +107 -12
- package/dist/context.js +101 -24
- package/dist/detect.d.ts +13 -0
- package/dist/detect.js +83 -0
- package/dist/engine.d.ts +66 -1
- package/dist/engine.js +290 -35
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +27 -1
- package/dist/fields.d.ts +20 -13
- package/dist/fields.js +65 -23
- package/dist/findings.d.ts +2 -1
- package/dist/findings.js +17 -1
- package/dist/flat.d.ts +33 -0
- package/dist/flat.js +555 -0
- package/dist/formats.d.ts +9 -0
- package/dist/formats.js +7 -1
- package/dist/formulas.d.ts +27 -0
- package/dist/formulas.js +157 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +11 -1
- package/dist/index.mjs +1 -1
- package/dist/io.d.ts +62 -1
- package/dist/io.js +245 -1
- package/dist/links.d.ts +27 -0
- package/dist/links.js +106 -1
- package/dist/msg.d.ts +3 -2
- package/dist/msg.js +172 -18
- package/dist/objects.d.ts +8 -5
- package/dist/objects.js +90 -20
- package/dist/odf.d.ts +87 -1
- package/dist/odf.js +204 -76
- package/dist/offcrypto.d.ts +24 -1
- package/dist/offcrypto.js +308 -69
- package/dist/ole.d.ts +19 -2
- package/dist/ole.js +98 -58
- package/dist/oleobj.d.ts +11 -0
- package/dist/oleobj.js +20 -3
- package/dist/ooxml.d.ts +108 -2
- package/dist/ooxml.js +392 -109
- package/dist/package.d.ts +14 -0
- package/dist/package.js +23 -7
- package/dist/plan.d.ts +104 -0
- package/dist/plan.js +73 -0
- package/dist/plugins.d.ts +8 -0
- package/dist/plugins.js +35 -0
- package/dist/ppt.js +45 -5
- package/dist/rewrite-cfb.d.ts +14 -0
- package/dist/rewrite-cfb.js +52 -0
- package/dist/rewrite-rtf.d.ts +9 -0
- package/dist/rewrite-rtf.js +45 -0
- package/dist/rewrite-xml.d.ts +36 -0
- package/dist/rewrite-xml.js +341 -0
- package/dist/rewrite-zip.d.ts +8 -0
- package/dist/rewrite-zip.js +498 -0
- package/dist/rtf.js +216 -42
- package/dist/rtfcomp.d.ts +7 -0
- package/dist/rtfcomp.js +80 -0
- package/dist/score.js +1 -1
- package/dist/sniff.d.ts +5 -0
- package/dist/sniff.js +38 -0
- package/dist/types.d.ts +66 -6
- package/dist/types.js +14 -0
- package/dist/vsd.d.ts +13 -0
- package/dist/vsd.js +689 -0
- package/dist/word.d.ts +4 -4
- package/dist/word.js +70 -23
- package/dist/xls.d.ts +10 -4
- package/dist/xls.js +58 -10
- package/dist/xml.d.ts +30 -0
- package/dist/xml.js +45 -23
- package/dist/zip-writer.d.ts +4 -0
- package/dist/zip-writer.js +20 -7
- package/dist/zip.d.ts +11 -2
- package/dist/zip.js +67 -6
- 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
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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,
|
|
83
|
-
|
|
84
|
-
ctx.report(types_1.OfficeCategory.Encrypted,
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
372
|
-
const expected =
|
|
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
|
|
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(
|
|
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 {};
|