@patrickdk77/office-defuse 0.1.0 → 0.1.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 +113 -26
- package/dist/biff.d.ts +62 -0
- package/dist/biff.js +95 -0
- package/dist/cfb.d.ts +58 -0
- package/dist/cfb.js +456 -0
- package/dist/context.d.ts +40 -12
- package/dist/context.js +46 -25
- package/dist/crc32.d.ts +8 -2
- package/dist/crc32.js +44 -4
- package/dist/engine.js +75 -26
- package/dist/fields.d.ts +29 -0
- package/dist/fields.js +97 -0
- package/dist/findings.js +28 -9
- package/dist/msg.d.ts +7 -0
- package/dist/msg.js +97 -0
- package/dist/objects.d.ts +42 -0
- package/dist/objects.js +134 -0
- package/dist/odf.d.ts +2 -2
- package/dist/offcrypto.d.ts +31 -0
- package/dist/offcrypto.js +408 -0
- package/dist/ole.d.ts +12 -0
- package/dist/ole.js +127 -0
- package/dist/oleobj.d.ts +74 -0
- package/dist/oleobj.js +198 -0
- package/dist/ooxml.d.ts +2 -2
- package/dist/ooxml.js +23 -71
- package/dist/package.d.ts +3 -0
- package/dist/package.js +21 -0
- package/dist/ppt.d.ts +8 -0
- package/dist/ppt.js +241 -0
- package/dist/rtf.d.ts +8 -0
- package/dist/rtf.js +539 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.js +19 -0
- package/dist/word.d.ts +9 -0
- package/dist/word.js +160 -0
- package/dist/xls.d.ts +27 -0
- package/dist/xls.js +174 -0
- package/package.json +3 -3
package/dist/objects.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isEquationClass = void 0;
|
|
4
|
+
exports.nestedNotes = nestedNotes;
|
|
5
|
+
exports.compoundRules = compoundRules;
|
|
6
|
+
exports.describeObject = describeObject;
|
|
7
|
+
exports.linkedObject = linkedObject;
|
|
8
|
+
exports.packageData = packageData;
|
|
9
|
+
const cfb_1 = require("./cfb");
|
|
10
|
+
const context_1 = require("./context");
|
|
11
|
+
const oleobj_1 = require("./oleobj");
|
|
12
|
+
const types_1 = require("./types");
|
|
13
|
+
/** The class ids of Equation Editor 3.0 and its kin, which CVE-2017-11882 and CVE-2018-0802 exploit, as oletools lists them. */
|
|
14
|
+
const EQUATION_CLSIDS = new Set(['0002CE02-0000-0000-C000-000000000046', '00021700-0000-0000-C000-000000000046', '0003000B-0000-0000-C000-000000000046', '0004A6B0-0000-0000-C000-000000000046']);
|
|
15
|
+
/** Equation Editor 3.0's ProgID, which rtfobj matches as a prefix. */
|
|
16
|
+
const isEquationClass = (name) => name.trim().toLowerCase().startsWith('equation.3');
|
|
17
|
+
exports.isEquationClass = isEquationClass;
|
|
18
|
+
/** Streams that only an OLE object's storage holds. */
|
|
19
|
+
const OBJECT_STREAMS = new Set(['\x01OLE', '\x01COMPOBJ', '\x01OLE10NATIVE', '\x03OBJINFO']);
|
|
20
|
+
/** Streams that only an ActiveX control's storage holds. */
|
|
21
|
+
const CONTROL_STREAMS = new Set(['\x03OCXNAME', '\x03OCXDATA']);
|
|
22
|
+
/** A CompObj, OLE or ObjInfo stream is small. More is not read. */
|
|
23
|
+
const SMALL = 65536;
|
|
24
|
+
/** Packager's header is short, and its names are at most 1,024 bytes each. */
|
|
25
|
+
const PACKAGE_HEAD = 4096;
|
|
26
|
+
const upper = (e) => e.name.toUpperCase();
|
|
27
|
+
/** Reports what the reader of a compound file inside this one noted, at the place it sits. */
|
|
28
|
+
function nestedNotes(ctx, cfb, location) {
|
|
29
|
+
for (const n of cfb.notes)
|
|
30
|
+
ctx.report(types_1.OfficeCategory.Container, n.detail, location, { bytes: n.bytes });
|
|
31
|
+
}
|
|
32
|
+
/** Where a finding about an entry points: `prefix`, then the entry's path. */
|
|
33
|
+
const at = (prefix, e) => [prefix, (0, cfb_1.entryPath)(e)].filter(Boolean).join(' > ');
|
|
34
|
+
/**
|
|
35
|
+
* The rules every compound file gets: VBA projects, embedded objects and controls, wherever they sit in the tree.
|
|
36
|
+
* `prefix` names where the file sits when it is inside another, and `rootIsObject` says that its root is itself an
|
|
37
|
+
* OLE object's storage, as an object in a presentation or an RTF file is.
|
|
38
|
+
*/
|
|
39
|
+
async function compoundRules(ctx, cfb, prefix, rootIsObject) {
|
|
40
|
+
const projects = new Set();
|
|
41
|
+
for (const e of cfb.entries) {
|
|
42
|
+
if (e.kind === 'stream')
|
|
43
|
+
continue;
|
|
44
|
+
const child = (name) => e.children.find(c => upper(c) === name);
|
|
45
|
+
// A VBA project: a VBA storage with a dir stream, which names the modules (MS-OVBA 2.2).
|
|
46
|
+
const vba = child('VBA');
|
|
47
|
+
if (vba?.kind === 'storage' && vba.children.some(c => c.kind === 'stream' && upper(c) === 'DIR')) {
|
|
48
|
+
projects.add(e);
|
|
49
|
+
ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, at(prefix, vba), { storage: at(prefix, vba) });
|
|
50
|
+
}
|
|
51
|
+
if (e.kind === 'storage' && (upper(e) === '_VBA_PROJECT_CUR' || upper(e) === 'MACROS') && e.children.length && !projects.has(e)) {
|
|
52
|
+
// Excel's and Word's project storages, without the dir stream a whole project has.
|
|
53
|
+
ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, at(prefix, e), { storage: at(prefix, e) });
|
|
54
|
+
}
|
|
55
|
+
const names = new Set(e.children.map(upper));
|
|
56
|
+
const parent = e.parent ? upper(e.parent) : '';
|
|
57
|
+
const isObject = e.kind === 'root'
|
|
58
|
+
? rootIsObject
|
|
59
|
+
: [...names].some(n => OBJECT_STREAMS.has(n) || CONTROL_STREAMS.has(n)) ||
|
|
60
|
+
(parent === 'OBJECTPOOL' && e.parent?.parent?.kind === 'root') ||
|
|
61
|
+
(e.parent?.kind === 'root' && /^MBD[0-9A-F]{8}$/i.test(e.name));
|
|
62
|
+
if (isObject)
|
|
63
|
+
await objectRules(ctx, cfb, e, at(prefix, e));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Reads an OLE object's storage: its class, its CompObj and OLE streams, and Packager's native data. */
|
|
67
|
+
async function describeObject(cfb, e, _location) {
|
|
68
|
+
const child = (name) => e.children.find(c => c.kind === 'stream' && upper(c) === name);
|
|
69
|
+
const small = async (name) => {
|
|
70
|
+
const s = child(name);
|
|
71
|
+
return s && s.size <= SMALL ? cfb.bytes(s) : undefined;
|
|
72
|
+
};
|
|
73
|
+
const clsid = e.clsid === '00000000-0000-0000-0000-000000000000' ? '' : e.clsid;
|
|
74
|
+
const compObjBytes = await small('\x01COMPOBJ');
|
|
75
|
+
const comp = compObjBytes ? (0, oleobj_1.parseCompObj)(compObjBytes) : {};
|
|
76
|
+
const info = { clsid, progId: comp.progId, userType: comp.userType, control: false, linked: false, target: '', url: false, equation: false };
|
|
77
|
+
info.equation = EQUATION_CLSIDS.has(clsid) || (0, exports.isEquationClass)(comp.progId ?? '');
|
|
78
|
+
if ([...CONTROL_STREAMS].some(n => child(n))) {
|
|
79
|
+
const name = await small('\x03OCXNAME');
|
|
80
|
+
info.control = true;
|
|
81
|
+
if (name)
|
|
82
|
+
info.controlName = Buffer.from(name).toString('utf16le').replace(/\0+$/, '');
|
|
83
|
+
return info;
|
|
84
|
+
}
|
|
85
|
+
const oleBytes = await small('\x01OLE');
|
|
86
|
+
const ole = oleBytes ? (0, oleobj_1.parseOleStream)(oleBytes) : { linked: false, target: '', url: false };
|
|
87
|
+
Object.assign(info, ole);
|
|
88
|
+
const native = child('\x01OLE10NATIVE');
|
|
89
|
+
if (native) {
|
|
90
|
+
// The stream starts with the native data's size, then Packager's data.
|
|
91
|
+
info.pkg = (0, oleobj_1.parsePackage)(await cfb.source(native).read(4, PACKAGE_HEAD));
|
|
92
|
+
}
|
|
93
|
+
return info;
|
|
94
|
+
}
|
|
95
|
+
/** One OLE object's storage: a control, a linked object, or an embedded one, with what it holds. */
|
|
96
|
+
async function objectRules(ctx, cfb, e, location) {
|
|
97
|
+
const d = await describeObject(cfb, e, location);
|
|
98
|
+
const data = { storage: location };
|
|
99
|
+
if (d.clsid)
|
|
100
|
+
data.clsid = d.clsid;
|
|
101
|
+
if (d.progId)
|
|
102
|
+
data.progId = (0, context_1.clip)(d.progId);
|
|
103
|
+
if (d.userType)
|
|
104
|
+
data.className = (0, context_1.clip)(d.userType);
|
|
105
|
+
if (d.control) {
|
|
106
|
+
ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, location, d.controlName ? { ...data, name: (0, context_1.clip)(d.controlName) } : data);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (d.pkg) {
|
|
110
|
+
Object.assign(data, packageData(d.pkg));
|
|
111
|
+
if (!d.pkg.embedded && d.pkg.sourcePath)
|
|
112
|
+
ctx.external(types_1.OfficeDetail.Object, d.pkg.sourcePath, location, { storage: location }, 'package link');
|
|
113
|
+
}
|
|
114
|
+
if (d.linked)
|
|
115
|
+
linkedObject(ctx, d, location, { storage: location, ...(d.clsid ? { clsid: d.clsid } : {}) });
|
|
116
|
+
else
|
|
117
|
+
ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, location, data);
|
|
118
|
+
if (d.equation)
|
|
119
|
+
ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Equation, location, data);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Reports a linked object. A URL moniker's target loads from outside the file whatever its text looks like: a
|
|
123
|
+
* CVE-2017-0199 sample puts a character with a stray high byte where the colon of https: goes.
|
|
124
|
+
*/
|
|
125
|
+
function linkedObject(ctx, d, location, data) {
|
|
126
|
+
if (d.target && !d.url)
|
|
127
|
+
ctx.external(types_1.OfficeDetail.Object, d.target, location, data, 'linked object');
|
|
128
|
+
else
|
|
129
|
+
ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Object, location, d.target ? { ...data, target: (0, context_1.clip)(d.target, 2000) } : data);
|
|
130
|
+
}
|
|
131
|
+
/** What a finding shows of a Packager object's file: its name and paths, and its size when the object holds it. */
|
|
132
|
+
function packageData(p) {
|
|
133
|
+
return { fileName: (0, context_1.clip)(p.fileName), sourcePath: (0, context_1.clip)(p.sourcePath, 2000), tempPath: (0, context_1.clip)(p.tempPath, 2000), ...(p.embedded ? { size: p.size } : {}) };
|
|
134
|
+
}
|
package/dist/odf.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ZipContext } from './context';
|
|
2
2
|
/** Inspects an OpenDocument package. */
|
|
3
|
-
export declare function inspectOdf(ctx:
|
|
3
|
+
export declare function inspectOdf(ctx: ZipContext): Promise<void>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { BiffRecord } from './biff';
|
|
2
|
+
import type { CfbEntry, CompoundFile } from './cfb';
|
|
3
|
+
import type { RunContext } from './context';
|
|
4
|
+
import { type ByteSource } from './types';
|
|
5
|
+
import type { Decrypt } from './xls';
|
|
6
|
+
/** The password Excel opens a workbook with when nobody set one to open it (MS-OFFCRYPTO 2.4.3). */
|
|
7
|
+
export declare const DEFAULT_PASSWORD = "VelvetSweatshop";
|
|
8
|
+
/** RC4, which OpenSSL 3 leaves out of its default provider. */
|
|
9
|
+
export declare class Rc4 {
|
|
10
|
+
private readonly s;
|
|
11
|
+
private i;
|
|
12
|
+
private j;
|
|
13
|
+
constructor(key: Uint8Array);
|
|
14
|
+
/** XORs the next keystream bytes into a copy of `data`. */
|
|
15
|
+
crypt(data: Uint8Array): Uint8Array;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Opens a workbook that its FILEPASS record (MS-XLS 2.4.117) encrypts with RC4 or RC4 CryptoAPI, with the caller's
|
|
19
|
+
* password or VelvetSweatshop. The records after FILEPASS are encrypted in 1024-byte blocks of the stream, each with its
|
|
20
|
+
* own key. Their headers are not, but take keystream bytes, and some records are never encrypted.
|
|
21
|
+
*/
|
|
22
|
+
export declare function unlockWorkbook(ctx: RunContext, filePass: BiffRecord, stream: string): Decrypt;
|
|
23
|
+
/**
|
|
24
|
+
* Inspects an encrypted OOXML package: EncryptionInfo (MS-OFFCRYPTO 2.3.4) names the cipher, and the caller's password
|
|
25
|
+
* or VelvetSweatshop gives the key. The package it decrypts goes through the OOXML rules, read in place: Agile
|
|
26
|
+
* encryption works in 4,096-byte segments and Standard in 16-byte blocks, so nothing is decrypted that is not read.
|
|
27
|
+
*/
|
|
28
|
+
export declare function openPackage(ctx: RunContext, cfb: CompoundFile, info: CfbEntry, pkg: CfbEntry): Promise<{
|
|
29
|
+
source: ByteSource;
|
|
30
|
+
size: number;
|
|
31
|
+
}>;
|
|
@@ -0,0 +1,408 @@
|
|
|
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.DEFAULT_PASSWORD = void 0;
|
|
37
|
+
exports.unlockWorkbook = unlockWorkbook;
|
|
38
|
+
exports.openPackage = openPackage;
|
|
39
|
+
const crypto = __importStar(require("node:crypto"));
|
|
40
|
+
const errors_1 = require("./errors");
|
|
41
|
+
const oleobj_1 = require("./oleobj");
|
|
42
|
+
const types_1 = require("./types");
|
|
43
|
+
/** The password Excel opens a workbook with when nobody set one to open it (MS-OFFCRYPTO 2.4.3). */
|
|
44
|
+
exports.DEFAULT_PASSWORD = 'VelvetSweatshop';
|
|
45
|
+
/** MS-OFFCRYPTO allows a spin count of at most 10,000,000. */
|
|
46
|
+
const MAX_SPIN = 10_000_000;
|
|
47
|
+
/** More of a Standard EncryptionInfo than its header and verifier take, which are a few hundred bytes. */
|
|
48
|
+
const STANDARD_INFO = 4096;
|
|
49
|
+
/** A source's bytes from `from` to `to`, in pieces. */
|
|
50
|
+
async function* chunks(source, from, to) {
|
|
51
|
+
for (let at = from; at < to; at += 65536)
|
|
52
|
+
yield source.read(at, Math.min(65536, to - at));
|
|
53
|
+
}
|
|
54
|
+
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 });
|
|
55
|
+
/** A file encrypted with an algorithm this reader does not decrypt. It never reaches OpenSSL. */
|
|
56
|
+
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 });
|
|
57
|
+
const HASHES = new Map([
|
|
58
|
+
['SHA1', 'sha1'],
|
|
59
|
+
['SHA256', 'sha256'],
|
|
60
|
+
['SHA384', 'sha384'],
|
|
61
|
+
['SHA512', 'sha512'],
|
|
62
|
+
]);
|
|
63
|
+
const AES = new Map([
|
|
64
|
+
[128, { cbc: 'aes-128-cbc', ecb: 'aes-128-ecb', detail: types_1.OfficeDetail.Aes128, bits: 128 }],
|
|
65
|
+
[192, { cbc: 'aes-192-cbc', ecb: 'aes-192-ecb', detail: types_1.OfficeDetail.Aes192, bits: 192 }],
|
|
66
|
+
[256, { cbc: 'aes-256-cbc', ecb: 'aes-256-ecb', detail: types_1.OfficeDetail.Aes256, bits: 256 }],
|
|
67
|
+
]);
|
|
68
|
+
/** CALG_AES_128, CALG_AES_192 and CALG_AES_256, by key size. */
|
|
69
|
+
const STANDARD_AES = new Map([
|
|
70
|
+
[128, 0x660e],
|
|
71
|
+
[192, 0x660f],
|
|
72
|
+
[256, 0x6610],
|
|
73
|
+
]);
|
|
74
|
+
/** A value from the file for a message, cut short. */
|
|
75
|
+
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 }];
|
|
80
|
+
}
|
|
81
|
+
/** 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 });
|
|
85
|
+
}
|
|
86
|
+
const utf16le = (s) => Buffer.from(s, 'utf16le');
|
|
87
|
+
const le32 = (n) => {
|
|
88
|
+
const b = Buffer.alloc(4);
|
|
89
|
+
b.writeUInt32LE(n >>> 0);
|
|
90
|
+
return b;
|
|
91
|
+
};
|
|
92
|
+
const hash = (algorithm, ...parts) => {
|
|
93
|
+
const h = crypto.createHash(algorithm);
|
|
94
|
+
for (const p of parts)
|
|
95
|
+
h.update(p);
|
|
96
|
+
return h.digest();
|
|
97
|
+
};
|
|
98
|
+
/** RC4, which OpenSSL 3 leaves out of its default provider. */
|
|
99
|
+
class Rc4 {
|
|
100
|
+
s = new Uint8Array(256);
|
|
101
|
+
i = 0;
|
|
102
|
+
j = 0;
|
|
103
|
+
constructor(key) {
|
|
104
|
+
for (let k = 0; k < 256; k++)
|
|
105
|
+
this.s[k] = k;
|
|
106
|
+
for (let k = 0, j = 0; k < 256; k++) {
|
|
107
|
+
j = (j + this.s[k] + key[k % key.length]) & 255;
|
|
108
|
+
[this.s[k], this.s[j]] = [this.s[j], this.s[k]];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** XORs the next keystream bytes into a copy of `data`. */
|
|
112
|
+
crypt(data) {
|
|
113
|
+
const out = new Uint8Array(data.length);
|
|
114
|
+
for (let k = 0; k < data.length; k++) {
|
|
115
|
+
this.i = (this.i + 1) & 255;
|
|
116
|
+
this.j = (this.j + this.s[this.i]) & 255;
|
|
117
|
+
[this.s[this.i], this.s[this.j]] = [this.s[this.j], this.s[this.i]];
|
|
118
|
+
out[k] = data[k] ^ this.s[(this.s[this.i] + this.s[this.j]) & 255];
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.Rc4 = Rc4;
|
|
124
|
+
/** Office binary document RC4 (MS-OFFCRYPTO 2.3.6): MD5, a 40-bit truncation and the salt. */
|
|
125
|
+
function rc4Key(password, salt) {
|
|
126
|
+
const h0 = hash('md5', utf16le(password)).subarray(0, 5);
|
|
127
|
+
const buffer = Buffer.concat(Array.from({ length: 16 }, () => Buffer.concat([h0, salt])));
|
|
128
|
+
const h1 = hash('md5', buffer).subarray(0, 5);
|
|
129
|
+
return block => hash('md5', h1, le32(block));
|
|
130
|
+
}
|
|
131
|
+
/** 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. */
|
|
132
|
+
function cryptoApiKey(password, salt, bits) {
|
|
133
|
+
const h0 = hash('sha1', salt, utf16le(password));
|
|
134
|
+
return block => {
|
|
135
|
+
const h = hash('sha1', h0, le32(block));
|
|
136
|
+
return bits === 40 ? Buffer.concat([h.subarray(0, 5), Buffer.alloc(11)]) : h.subarray(0, bits / 8);
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Opens a workbook that its FILEPASS record (MS-XLS 2.4.117) encrypts with RC4 or RC4 CryptoAPI, with the caller's
|
|
141
|
+
* 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.
|
|
143
|
+
*/
|
|
144
|
+
function unlockWorkbook(ctx, filePass, stream) {
|
|
145
|
+
const r = new oleobj_1.ByteReader(filePass.body);
|
|
146
|
+
let key;
|
|
147
|
+
let cipher = types_1.OfficeDetail.Rc4_40;
|
|
148
|
+
let bits = 40;
|
|
149
|
+
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));
|
|
185
|
+
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
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else
|
|
196
|
+
throw unsupported(`encryption version ${major}.${minor}`, { stream });
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
if (e instanceof oleobj_1.Truncated)
|
|
200
|
+
throw required('a FILEPASS record cut short', { stream });
|
|
201
|
+
throw e;
|
|
202
|
+
}
|
|
203
|
+
if (!key)
|
|
204
|
+
throw required('the workbook is encrypted with a password that is not VelvetSweatshop', { stream });
|
|
205
|
+
return biffDecrypt(key);
|
|
206
|
+
}
|
|
207
|
+
/** Records MS-XLS 2.2.10 never encrypts: BOF, FILEPASS, UsrExcl, FileLock, InterfaceHdr, RRDInfo and RRDHead. */
|
|
208
|
+
const PLAIN = new Set([0x0809, 0x002f, 0x0194, 0x0195, 0x00e1, 0x0196, 0x0138]);
|
|
209
|
+
/** Only the records after FILEPASS reach the function it returns. */
|
|
210
|
+
function biffDecrypt(key) {
|
|
211
|
+
let block = -1;
|
|
212
|
+
let stream = new Uint8Array(0);
|
|
213
|
+
const keystream = (b) => {
|
|
214
|
+
if (b !== block) {
|
|
215
|
+
block = b;
|
|
216
|
+
stream = new Rc4(key(b)).crypt(new Uint8Array(1024));
|
|
217
|
+
}
|
|
218
|
+
return stream;
|
|
219
|
+
};
|
|
220
|
+
return r => {
|
|
221
|
+
if (PLAIN.has(r.type))
|
|
222
|
+
return r.body;
|
|
223
|
+
const out = new Uint8Array(r.body);
|
|
224
|
+
// BoundSheet8's lbPlyPos stays plain, so a reader can find each sheet without the key.
|
|
225
|
+
const from = r.type === 0x0085 ? 4 : 0;
|
|
226
|
+
for (let k = from; k < out.length; k++) {
|
|
227
|
+
const at = r.offset + 4 + k;
|
|
228
|
+
out[k] ^= keystream(Math.floor(at / 1024))[at % 1024];
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Inspects an encrypted OOXML package: EncryptionInfo (MS-OFFCRYPTO 2.3.4) names the cipher, and the caller's password
|
|
235
|
+
* or VelvetSweatshop gives the key. The package it decrypts goes through the OOXML rules, read in place: Agile
|
|
236
|
+
* encryption works in 4,096-byte segments and Standard in 16-byte blocks, so nothing is decrypted that is not read.
|
|
237
|
+
*/
|
|
238
|
+
async function openPackage(ctx, cfb, info, pkg) {
|
|
239
|
+
const source = cfb.source(info);
|
|
240
|
+
const version = await source.read(0, 8);
|
|
241
|
+
const major = version.length >= 4 ? version[0] | (version[1] << 8) : 0;
|
|
242
|
+
const minor = version.length >= 4 ? version[2] | (version[3] << 8) : 0;
|
|
243
|
+
let cipher;
|
|
244
|
+
// Agile's XML goes to the tokenizer in pieces. Standard's binary header is a few hundred bytes.
|
|
245
|
+
if (major === 4 && minor === 4)
|
|
246
|
+
cipher = await agile(ctx, chunks(source, 8, info.size));
|
|
247
|
+
else if ((major === 3 || major === 4) && minor === 2)
|
|
248
|
+
cipher = standard(await source.read(8, STANDARD_INFO));
|
|
249
|
+
else
|
|
250
|
+
throw unsupported(`encryption version ${major}.${minor}`, { stream: info.name });
|
|
251
|
+
const head = await cfb.source(pkg).read(0, 8);
|
|
252
|
+
if (head.length < 8)
|
|
253
|
+
throw required('an EncryptedPackage stream cut short', { stream: pkg.name });
|
|
254
|
+
const size = Number(Buffer.from(head).readBigUInt64LE(0));
|
|
255
|
+
if (size > pkg.size - 8)
|
|
256
|
+
throw required('an EncryptedPackage stream shorter than the size it gives', { stream: pkg.name });
|
|
257
|
+
for (const p of passwords(ctx)) {
|
|
258
|
+
(0, errors_1.checkTime)(ctx.settings.deadline);
|
|
259
|
+
const key = cipher.key(p.password);
|
|
260
|
+
if (!key)
|
|
261
|
+
continue;
|
|
262
|
+
opened(ctx, p.supplied, cipher.cipher.detail, pkg.name, cipher.cipher.bits);
|
|
263
|
+
const data = cfb.source(pkg);
|
|
264
|
+
return { source: cipher.source((offset, length) => data.read(8 + offset, length), size, key), size };
|
|
265
|
+
}
|
|
266
|
+
throw required('the package is encrypted with a password that is not VelvetSweatshop', { stream: pkg.name });
|
|
267
|
+
}
|
|
268
|
+
/** AES without padding over whole blocks: ECB, or CBC from `iv`. */
|
|
269
|
+
function aes(cipher, key, data, iv) {
|
|
270
|
+
const d = crypto.createDecipheriv(iv ? cipher.cbc : cipher.ecb, key, iv ?? null);
|
|
271
|
+
d.setAutoPadding(false);
|
|
272
|
+
const whole = data.subarray(0, data.length - (data.length % 16));
|
|
273
|
+
return Buffer.concat([d.update(whole), d.final()]);
|
|
274
|
+
}
|
|
275
|
+
/** Standard encryption (MS-OFFCRYPTO 2.3.4.5): AES-ECB with a key from SHA-1 over 50,000 rounds. */
|
|
276
|
+
function standard(b) {
|
|
277
|
+
const r = new oleobj_1.ByteReader(b);
|
|
278
|
+
try {
|
|
279
|
+
const headerSize = r.u32();
|
|
280
|
+
const header = new oleobj_1.ByteReader(r.bytes(headerSize));
|
|
281
|
+
header.skip(8);
|
|
282
|
+
const algorithm = header.u32();
|
|
283
|
+
const hashAlgorithm = header.u32();
|
|
284
|
+
const bits = header.u32();
|
|
285
|
+
// CALG_AES_128, _192 and _256, which name their key size, with SHA-1.
|
|
286
|
+
const cipher = AES.get(bits);
|
|
287
|
+
if (!cipher || algorithm !== STANDARD_AES.get(bits) || (hashAlgorithm !== 0x8004 && hashAlgorithm !== 0))
|
|
288
|
+
throw unsupported(`Standard encryption with algorithm 0x${algorithm.toString(16)}, hash 0x${hashAlgorithm.toString(16)} and a ${bits}-bit key`);
|
|
289
|
+
r.skip(4);
|
|
290
|
+
const salt = r.bytes(16);
|
|
291
|
+
const verifier = r.bytes(16);
|
|
292
|
+
r.skip(4);
|
|
293
|
+
const verifierHash = r.bytes(32);
|
|
294
|
+
const keyOf = (password) => {
|
|
295
|
+
let h = hash('sha1', salt, utf16le(password));
|
|
296
|
+
for (let i = 0; i < 50000; i++)
|
|
297
|
+
h = hash('sha1', le32(i), h);
|
|
298
|
+
const final = hash('sha1', h, le32(0));
|
|
299
|
+
const x = (fill) => {
|
|
300
|
+
const buf = Buffer.alloc(64, fill);
|
|
301
|
+
for (let i = 0; i < final.length; i++)
|
|
302
|
+
buf[i] ^= final[i];
|
|
303
|
+
return hash('sha1', buf);
|
|
304
|
+
};
|
|
305
|
+
return Buffer.concat([x(0x36), x(0x5c)]).subarray(0, bits / 8);
|
|
306
|
+
};
|
|
307
|
+
return {
|
|
308
|
+
cipher,
|
|
309
|
+
key(password) {
|
|
310
|
+
const key = keyOf(password);
|
|
311
|
+
const v = aes(cipher, key, verifier);
|
|
312
|
+
const vh = aes(cipher, key, verifierHash);
|
|
313
|
+
return hash('sha1', v).equals(Buffer.from(vh.subarray(0, 20))) ? key : undefined;
|
|
314
|
+
},
|
|
315
|
+
source: (read, size, key) => blockSource(size, 16, async (index, count) => aes(cipher, key, await read(index * 16, count * 16))),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
catch (e) {
|
|
319
|
+
if (e instanceof oleobj_1.Truncated)
|
|
320
|
+
throw required('a Standard EncryptionInfo cut short');
|
|
321
|
+
throw e;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** Agile encryption (MS-OFFCRYPTO 2.3.4.10): an XML descriptor, a password key encryptor, and AES-CBC in 4,096-byte segments. */
|
|
325
|
+
async function agile(ctx, xml) {
|
|
326
|
+
const found = {};
|
|
327
|
+
const ENCRYPTION = 'http://schemas.microsoft.com/office/2006/encryption';
|
|
328
|
+
const PASSWORD = 'http://schemas.microsoft.com/office/2006/keyEncryptor/password';
|
|
329
|
+
await ctx.parseXml('EncryptionInfo', xml, {
|
|
330
|
+
start(el) {
|
|
331
|
+
const all = Object.fromEntries(el.attributes.filter(a => a.ns === '').map(a => [a.local, a.value]));
|
|
332
|
+
if (el.ns === ENCRYPTION && el.local === 'keyData')
|
|
333
|
+
found.keyData ??= all;
|
|
334
|
+
else if (el.ns === PASSWORD && el.local === 'encryptedKey')
|
|
335
|
+
found.encryptedKey ??= all;
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
const kd = found.keyData;
|
|
339
|
+
const ek = found.encryptedKey;
|
|
340
|
+
if (!kd || !ek)
|
|
341
|
+
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. */
|
|
343
|
+
const algorithms = (a) => {
|
|
344
|
+
const h = HASHES.get(a.hashAlgorithm);
|
|
345
|
+
const c = AES.get(Number(a.keyBits));
|
|
346
|
+
if (!h || !c || a.cipherAlgorithm !== 'AES' || a.cipherChaining !== 'ChainingModeCBC' || Number(a.blockSize) !== 16)
|
|
347
|
+
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 };
|
|
349
|
+
};
|
|
350
|
+
const { h: dh, c: dc } = algorithms(kd);
|
|
351
|
+
const { h: eh, c: ec } = algorithms(ek);
|
|
352
|
+
const spin = Number(ek.spinCount);
|
|
353
|
+
if (!Number.isInteger(spin) || spin < 0 || spin > MAX_SPIN)
|
|
354
|
+
throw required('an Agile spin count past 10,000,000');
|
|
355
|
+
const b64 = (s) => Buffer.from(s ?? '', 'base64');
|
|
356
|
+
const pad = (b, n, fill) => (b.length >= n ? b.subarray(0, n) : Buffer.concat([b, Buffer.alloc(n - b.length, fill)]));
|
|
357
|
+
const salt = b64(ek.saltValue);
|
|
358
|
+
const BLOCKS = { input: Buffer.from('fea7d2763b4b9e79', 'hex'), value: Buffer.from('d7aa0f6d3061344e', 'hex'), key: Buffer.from('146e0be7abacd0d6', 'hex') };
|
|
359
|
+
const dsalt = b64(kd.saltValue);
|
|
360
|
+
return {
|
|
361
|
+
cipher: dc,
|
|
362
|
+
key(password) {
|
|
363
|
+
let h = hash(eh, salt, utf16le(password));
|
|
364
|
+
for (let i = 0; i < spin; i++) {
|
|
365
|
+
if (i % 65536 === 0)
|
|
366
|
+
(0, errors_1.checkTime)(ctx.settings.deadline);
|
|
367
|
+
h = hash(eh, le32(i), h);
|
|
368
|
+
}
|
|
369
|
+
const derive = (block) => pad(hash(eh, h, block), ec.bits / 8, 0x36);
|
|
370
|
+
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);
|
|
373
|
+
const got = hash(eh, input);
|
|
374
|
+
if (!got.equals(Buffer.from(expected.subarray(0, got.length))))
|
|
375
|
+
return undefined;
|
|
376
|
+
return aes(ec, derive(BLOCKS.key), b64(ek.encryptedKeyValue), iv).subarray(0, dc.bits / 8);
|
|
377
|
+
},
|
|
378
|
+
source: (read, size, key) => blockSource(size, 4096, async (index, count) => {
|
|
379
|
+
const raw = await read(index * 4096, count * 4096);
|
|
380
|
+
const out = [];
|
|
381
|
+
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)));
|
|
383
|
+
return Buffer.concat(out);
|
|
384
|
+
}),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
/** A source over decrypted data, decrypting the whole blocks each read covers, and keeping the last ones decrypted. */
|
|
388
|
+
function blockSource(size, unit, decrypt) {
|
|
389
|
+
let cached;
|
|
390
|
+
return {
|
|
391
|
+
size: async () => size,
|
|
392
|
+
async read(offset, length) {
|
|
393
|
+
const end = Math.min(size, offset + length);
|
|
394
|
+
const first = Math.floor(offset / unit);
|
|
395
|
+
const last = Math.floor((end - 1) / unit);
|
|
396
|
+
let bytes;
|
|
397
|
+
if (cached && cached.index <= first && cached.index + cached.bytes.length / unit > last)
|
|
398
|
+
bytes = cached.bytes.subarray((first - cached.index) * unit);
|
|
399
|
+
else {
|
|
400
|
+
// A small read decrypts 64 KiB from where it starts, since the ZIP reader reads on from there.
|
|
401
|
+
const count = Math.max(last - first + 1, Math.ceil(65536 / unit));
|
|
402
|
+
bytes = await decrypt(first, count);
|
|
403
|
+
cached = { index: first, bytes };
|
|
404
|
+
}
|
|
405
|
+
return bytes.subarray(offset - first * unit, end - first * unit);
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
}
|
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 {};
|