ppt-codec 1.2.12 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -3
- package/dist/encryption.cjs +82 -0
- package/dist/encryption.d.cts +20 -0
- package/dist/encryption.d.ts +20 -0
- package/dist/encryption.js +80 -0
- package/dist/read.cjs +15 -9
- package/dist/read.d.cts +3 -3
- package/dist/read.d.ts +3 -3
- package/dist/read.js +15 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -57,7 +57,30 @@ for (const slide of slides) {
|
|
|
57
57
|
}
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
`readPptStreams(currentUserStream, powerPointDocumentStream)` is the same read one level down, for a caller that already holds the two streams — the compound file beneath them is `archive-codec`'s business, and separating the two is what lets every record-level behaviour be tested without a container around it.
|
|
60
|
+
`readPptStreams(currentUserStream, powerPointDocumentStream, password)` is the same read one level down, for a caller that already holds the two streams — the compound file beneath them is `archive-codec`'s business, and separating the two is what lets every record-level behaviour be tested without a container around it.
|
|
61
|
+
|
|
62
|
+
## Encryption
|
|
63
|
+
|
|
64
|
+
A `.ppt` protected with a password to open uses [MS-OFFCRYPTO] 2.3.5 "RC4 CryptoAPI Encryption" — genuinely different from the MD5-based scheme `xls-codec` and `doc-codec` share (ExaDev/documents.js#1108/#1113): SHA-1-based key derivation with no intermediate-hash iteration, and re-keying per persist object rather than at a fixed byte interval. `readPpt`/`readPptContent`/`readPptStreams` take an optional `password`, ignored for an unencrypted presentation; a missing or incorrect password against an encrypted one throws `PptEncryptedError` rather than returning a partial or garbled document, and so does an encryption shape this package does not implement (anything other than RC4 CryptoAPI — [MS-PPT] itself never specifies any other scheme for the binary format).
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { readPptContent } from "ppt-codec";
|
|
68
|
+
|
|
69
|
+
const { metadata, slides } = readPptContent(
|
|
70
|
+
pptBytes,
|
|
71
|
+
"correct horse battery staple",
|
|
72
|
+
);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`archive-codec`'s `crypto/office-rc4-cryptoapi` module (ExaDev/documents.js#1116) carries the key derivation and password verification; this package's own `src/encryption.ts` carries the container-specific pieces, which differ from both xls-codec's and doc-codec's shared scheme in three real ways:
|
|
76
|
+
|
|
77
|
+
- **Location.** There is no fixed-offset header at all. The `DocumentEncryptionAtom` (`RT_CryptSession10Container`, [MS-PPT]'s own name for record type 0x2F14) is just another persist object, reached only by walking the current `UserEditAtom.encryptSessionPersistIdRef` through the same persist directory every other record uses.
|
|
78
|
+
- **Re-keying granularity.** Each top-level persist object gets its own RC4 key, derived from its own persist ID as the "block number" — not a running byte offset within one continuous stream, the way both `xls-codec`'s FilePass scheme and `doc-codec`'s own EncryptionHeader scheme re-key.
|
|
79
|
+
- **Encrypted headers.** A persist object's own 8-byte record header is encrypted along with its data, unlike the never-encrypted headers the shared xls/doc scheme leaves alone — `decryptPptDocumentStream` decrypts a peek of those 8 bytes first, under the object's own key, to learn its real length before decrypting the object in full.
|
|
80
|
+
|
|
81
|
+
Pictures in a separate `Pictures` stream are also RC4 CryptoAPI-encrypted per [MS-PPT], but this package does not read the `Pictures` stream at all today (see [Images, tables, and OLE embeddings](#what-it-does-not-read-yet)), so decrypting it is out of scope until something needs to.
|
|
82
|
+
|
|
83
|
+
`writePptContent` never encrypts.
|
|
61
84
|
|
|
62
85
|
## Writing a document
|
|
63
86
|
|
|
@@ -84,6 +107,7 @@ The whole path from a file's first byte to a slide's text, record by record:
|
|
|
84
107
|
| Container | The `Current User` and `PowerPoint Document` streams, read through `archive-codec`'s bounded [MS-CFB] reader. |
|
|
85
108
|
| Record framing | The generic 8-byte `RecordHeader`, the container/atom distinction, sibling sequences, child walks, and typed-descendant search — shared with [MS-ODRAW]'s records, which carry the identical header. |
|
|
86
109
|
| Edit resolution | `CurrentUserAtom` (including its encrypted/plaintext `headerToken`), the `UserEditAtom` chain, `PersistDirectoryAtom`/`PersistDirectoryEntry`'s packed 20-bit/12-bit run form, and the oldest-first directory construction whose later entries supersede earlier ones — [MS-PPT] 2.1.2's own "live record" process, Part 1. |
|
|
110
|
+
| Encryption | `DocumentEncryptionAtom` (`UserEditAtom.encryptSessionPersistIdRef` → the persist directory), [MS-OFFCRYPTO] 2.3.5's RC4 CryptoAPI scheme — see [Encryption](#encryption). |
|
|
87
111
|
| Document | `DocumentContainer` → `DocumentAtom` (slide size, in master units), `DocumentTextInfoContainer`'s `FontCollectionContainer`/`FontEntityAtom` typeface names, and `SlideListWithTextContainer` (distinguished from the master and notes lists by `recInstance`, which does not run in the order the names suggest). |
|
|
88
112
|
| Slides | `SlidePersistAtom` → the persist directory → each `SlideContainer`, and the placeholder texts the slide list carries for it. |
|
|
89
113
|
| Speaker notes | `NotesListWithTextContainer` (the third of the three containers sharing `RT_SlideListWithText`) → `NotesPersistAtom` → the persist directory → each `NotesContainer`, and the `NotesAtom.slideIdRef` naming the presentation slide those notes belong to. The text comes from the notes slide's own drawing, since the notes list — unlike the slide list — carries no texts for an `OutlineTextRefAtom` to reach into. |
|
|
@@ -97,7 +121,6 @@ Geometry is converted from master units (1/576 inch) to points on the way out, s
|
|
|
97
121
|
|
|
98
122
|
Each of these is a real construct of the format that this package currently ignores or cannot represent — not a claim that it does not exist:
|
|
99
123
|
|
|
100
|
-
- **Encrypted documents.** Recognised and refused by name (`PptEncryptedError`) rather than misparsed, but not decrypted.
|
|
101
124
|
- **`DocumentSummaryInformation`'s extended and user-defined properties** (company, manager, custom properties) — a genuinely different stream from the one [Metadata](#metadata) covers, not attempted at all.
|
|
102
125
|
- **Master and layout inheritance.** A run that states no size, typeface, or weight inherits it from the master's `TextMasterStyleAtom`; this reader reports such a property as absent rather than resolving the cascade, so a run's formatting is what the slide itself states and no more.
|
|
103
126
|
- **Scheme colours.** A `ColorIndexStruct` naming a colour-scheme slot (rather than a literal sRGB value) yields no colour, because resolving it needs the slide's `SlideSchemeColorSchemeAtom`.
|
|
@@ -198,6 +221,7 @@ import { readStyleTextPropAtom } from "ppt-codec/text/style";
|
|
|
198
221
|
| `stream/current-user-write` | Writes a real `CurrentUserAtom` pointing at the single edit this writer always produces. |
|
|
199
222
|
| `stream/persist` | `UserEditAtom`, `PersistDirectoryAtom`, and the persist directory the edit chain builds. |
|
|
200
223
|
| `stream/persist-write` | Writes a single-edit `UserEditAtom`/`PersistDirectoryAtom` pair covering the document container and every slide container. |
|
|
224
|
+
| `encryption` | `readDocumentEncryptionAtom`, `decryptPptDocumentStream` — [MS-OFFCRYPTO] 2.3.5 RC4 CryptoAPI decryption, wired against `archive-codec`'s own key derivation (see [Encryption](#encryption)). |
|
|
201
225
|
| `document/document-atom` | `DocumentAtom`: slide and notes sizes, master persist references. |
|
|
202
226
|
| `document/document-atom-write` | Writes a `DocumentAtom` for the one slide size every slide must share. |
|
|
203
227
|
| `document/fonts` | The font collection, resolved to typeface names a `FontIndexRef` indexes. |
|
|
@@ -221,7 +245,7 @@ import { readStyleTextPropAtom } from "ppt-codec/text/style";
|
|
|
221
245
|
| `read` | The whole read pipeline, and the `readPpt`/`readPptContent`/`readPptStreams` surface. |
|
|
222
246
|
| `write` | The whole write pipeline, and the `writePpt`/`writePptContent`/`writePptStreams` surface. |
|
|
223
247
|
| `units` | Master units to points, and points to master units. |
|
|
224
|
-
| `errors` | `PptFormatError` for malformed input, `PptEncryptedError` for
|
|
248
|
+
| `errors` | `PptFormatError` for malformed input, `PptEncryptedError` for encrypted input given no password, an incorrect one, or an encryption scheme this package does not implement (anything other than RC4 CryptoAPI), `PptUnsupportedContentError` for well-formed content this package's writer cannot express. |
|
|
225
249
|
|
|
226
250
|
### Every fixture is built from the specification, not captured
|
|
227
251
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_errors = require("./errors.cjs");
|
|
3
|
+
require("./record/header.cjs");
|
|
4
|
+
const require_record_tree = require("./record/tree.cjs");
|
|
5
|
+
const require_record_types = require("./record/types.cjs");
|
|
6
|
+
let archive_codec = require("archive-codec");
|
|
7
|
+
//#region src/encryption.ts
|
|
8
|
+
const ALG_ID_RC4 = 26625;
|
|
9
|
+
const ALG_ID_HASH_SHA1 = 32772;
|
|
10
|
+
const CRYPTOAPI_VERSION_MINOR = 2;
|
|
11
|
+
const CRYPTOAPI_VERSION_MAJOR_MIN = 2;
|
|
12
|
+
const CRYPTOAPI_VERSION_MAJOR_MAX = 4;
|
|
13
|
+
const VERSION_MAJOR_OFFSET = 0;
|
|
14
|
+
const VERSION_MINOR_OFFSET = 2;
|
|
15
|
+
const HEADER_SIZE_FIELD_OFFSET = 8;
|
|
16
|
+
const HEADER_START = 12;
|
|
17
|
+
const DEFAULT_KEY_SIZE_BITS = 40;
|
|
18
|
+
const MIN_HEADER_FIELDS_LENGTH = 32;
|
|
19
|
+
/** Parses a DocumentEncryptionAtom's own fields -- never encrypted, since it is what a decryptor needs before it can decrypt anything else. */
|
|
20
|
+
function readDocumentEncryptionAtom(record) {
|
|
21
|
+
if (record.header.recType !== 12052) throw new require_errors.PptFormatError(`expected the DocumentEncryptionAtom's own record type (0x${require_record_types.RT_CryptSession10Container.toString(16)}) at offset ${record.offset}, found 0x${record.header.recType.toString(16)}`);
|
|
22
|
+
const { data } = record;
|
|
23
|
+
if (data.length < HEADER_START) throw new require_errors.PptFormatError(`DocumentEncryptionAtom at offset ${record.offset} carries ${data.length} bytes, fewer than the ${HEADER_START}-byte fixed portion (version, encryptionFlags, headerSize) it requires`);
|
|
24
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
25
|
+
const versionMajor = view.getUint16(VERSION_MAJOR_OFFSET, true);
|
|
26
|
+
const versionMinor = view.getUint16(VERSION_MINOR_OFFSET, true);
|
|
27
|
+
if (versionMajor < CRYPTOAPI_VERSION_MAJOR_MIN || versionMajor > CRYPTOAPI_VERSION_MAJOR_MAX || versionMinor !== CRYPTOAPI_VERSION_MINOR) throw new require_errors.PptEncryptedError(`DocumentEncryptionAtom declares version ${versionMajor}.${versionMinor}, outside [MS-OFFCRYPTO] 2.3.5.1's RC4 CryptoAPI range (major 2-4, minor 2); this package only decrypts RC4 CryptoAPI-encrypted presentations`);
|
|
28
|
+
const headerSize = view.getUint32(HEADER_SIZE_FIELD_OFFSET, true);
|
|
29
|
+
const headerEnd = HEADER_START + headerSize;
|
|
30
|
+
if (headerSize < MIN_HEADER_FIELDS_LENGTH || headerEnd > data.length) throw new require_errors.PptFormatError(`DocumentEncryptionAtom at offset ${record.offset} declares a ${headerSize}-byte EncryptionHeader, which is either shorter than the ${MIN_HEADER_FIELDS_LENGTH} fixed fields require or runs past the atom's own ${data.length} bytes`);
|
|
31
|
+
const algId = view.getUint32(20, true);
|
|
32
|
+
if (algId !== ALG_ID_RC4) throw new require_errors.PptEncryptedError(`DocumentEncryptionAtom's EncryptionHeader names cipher algorithm 0x${algId.toString(16)}, not RC4 (0x${ALG_ID_RC4.toString(16)}); this package only decrypts RC4 CryptoAPI-encrypted presentations`);
|
|
33
|
+
const algIdHash = view.getUint32(24, true);
|
|
34
|
+
if (algIdHash !== ALG_ID_HASH_SHA1) throw new require_errors.PptEncryptedError(`DocumentEncryptionAtom's EncryptionHeader names hash algorithm 0x${algIdHash.toString(16)}, not SHA-1 (0x${ALG_ID_HASH_SHA1.toString(16)}) as [MS-OFFCRYPTO] 2.3.5.1 requires of RC4 CryptoAPI`);
|
|
35
|
+
const rawKeySize = view.getUint32(28, true);
|
|
36
|
+
const keySizeBits = rawKeySize === 0 ? DEFAULT_KEY_SIZE_BITS : rawKeySize;
|
|
37
|
+
if (headerEnd + 4 > data.length) throw new require_errors.PptFormatError(`DocumentEncryptionAtom at offset ${record.offset} has no room for its EncryptionVerifier's saltSize field after the ${headerSize}-byte header`);
|
|
38
|
+
const saltSize = view.getUint32(headerEnd, true);
|
|
39
|
+
if (saltSize !== archive_codec.RC4_CRYPTOAPI_SALT_LENGTH) throw new require_errors.PptFormatError(`DocumentEncryptionAtom's EncryptionVerifier declares saltSize ${saltSize}, not the mandated ${archive_codec.RC4_CRYPTOAPI_SALT_LENGTH}`);
|
|
40
|
+
const saltStart = headerEnd + 4;
|
|
41
|
+
const encryptedVerifierStart = saltStart + archive_codec.RC4_CRYPTOAPI_SALT_LENGTH;
|
|
42
|
+
const verifierHashSizeFieldStart = encryptedVerifierStart + archive_codec.RC4_CRYPTOAPI_VERIFIER_LENGTH;
|
|
43
|
+
const encryptedVerifierHashStart = verifierHashSizeFieldStart + 4;
|
|
44
|
+
const encryptedVerifierHashEnd = encryptedVerifierHashStart + archive_codec.RC4_CRYPTOAPI_VERIFIER_HASH_LENGTH;
|
|
45
|
+
if (encryptedVerifierHashEnd > data.length) throw new require_errors.PptFormatError(`DocumentEncryptionAtom at offset ${record.offset} carries ${data.length} bytes, too few for its EncryptionVerifier's salt/encryptedVerifier/encryptedVerifierHash fields`);
|
|
46
|
+
return {
|
|
47
|
+
keySizeBits,
|
|
48
|
+
salt: data.subarray(saltStart, encryptedVerifierStart),
|
|
49
|
+
encryptedVerifier: data.subarray(encryptedVerifierStart, verifierHashSizeFieldStart),
|
|
50
|
+
encryptedVerifierHash: data.subarray(encryptedVerifierHashStart, encryptedVerifierHashEnd)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Decrypts a PowerPoint Document stream protected by [MS-OFFCRYPTO] 2.3.5 RC4 CryptoAPI, given the password and the persist directory/`encryptSessionPersistIdRef` `buildPersistDirectory` already resolved -- both come from parsing the stream's own never-encrypted UserEditAtom/PersistDirectoryAtom chain, which is why this module needs them handed in rather than deriving them itself.
|
|
55
|
+
*
|
|
56
|
+
* Returns a new, fully decrypted copy of the stream: every persist object in `directory` except the DocumentEncryptionAtom itself gets its own key, derived from its own persist ID as the RC4 CryptoAPI "block number" (see this file's own top comment), and is decrypted as one continuous keystream covering its header and data together -- the header is encrypted too here, unlike the shared xls/doc RC4 scheme, so each object's own recLen has to be learned by decrypting its header first.
|
|
57
|
+
*
|
|
58
|
+
* Throws `PptEncryptedError` for a missing password, an incorrect one, or an encryption shape this module does not implement (anything other than RC4 CryptoAPI) -- there is no partial or best-effort result to return in any of those cases.
|
|
59
|
+
*/
|
|
60
|
+
function decryptPptDocumentStream(streamBytes, directory, encryptSessionPersistIdRef, password) {
|
|
61
|
+
const encryptionAtomOffset = directory.get(encryptSessionPersistIdRef);
|
|
62
|
+
if (encryptionAtomOffset === void 0) throw new require_errors.PptFormatError(`UserEditAtom.encryptSessionPersistIdRef ${encryptSessionPersistIdRef} references persist object ${encryptSessionPersistIdRef}, which the persist directory does not contain`);
|
|
63
|
+
const info = readDocumentEncryptionAtom(require_record_tree.readRecordAt(streamBytes, encryptionAtomOffset));
|
|
64
|
+
if (!(0, archive_codec.verifyRc4CryptoApiPassword)(password, info.salt, info.keySizeBits, info.encryptedVerifier, info.encryptedVerifierHash)) throw new require_errors.PptEncryptedError("incorrect password for RC4 CryptoAPI-encrypted presentation");
|
|
65
|
+
const decrypted = new Uint8Array(streamBytes.length);
|
|
66
|
+
decrypted.set(streamBytes);
|
|
67
|
+
for (const [persistId, offset] of directory) {
|
|
68
|
+
if (persistId === encryptSessionPersistIdRef) continue;
|
|
69
|
+
if (offset + 8 > streamBytes.length) throw new require_errors.PptFormatError(`persist object ${persistId} at offset ${offset} needs 8 bytes for its own record header, but only ${streamBytes.length - offset} remain in the stream`);
|
|
70
|
+
const key = (0, archive_codec.deriveRc4CryptoApiBlockKey)(password, info.salt, persistId, info.keySizeBits);
|
|
71
|
+
const headerPlaintext = (0, archive_codec.rc4)(key, streamBytes.subarray(offset, offset + 8));
|
|
72
|
+
const recLen = new DataView(headerPlaintext.buffer, headerPlaintext.byteOffset, headerPlaintext.byteLength).getUint32(4, true);
|
|
73
|
+
const objectEnd = offset + 8 + recLen;
|
|
74
|
+
if (objectEnd > streamBytes.length) throw new require_errors.PptFormatError(`persist object ${persistId} at offset ${offset} decrypts to a ${recLen}-byte record, which runs past the stream's own ${streamBytes.length} bytes`);
|
|
75
|
+
const plaintext = (0, archive_codec.rc4)(key, streamBytes.subarray(offset, objectEnd));
|
|
76
|
+
decrypted.set(plaintext, offset);
|
|
77
|
+
}
|
|
78
|
+
return decrypted;
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
exports.decryptPptDocumentStream = decryptPptDocumentStream;
|
|
82
|
+
exports.readDocumentEncryptionAtom = readDocumentEncryptionAtom;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { t as PptRecord } from "./tree-Du_LXAF0.cjs";
|
|
2
|
+
//#region src/encryption.d.ts
|
|
3
|
+
interface PptEncryptionInfo {
|
|
4
|
+
readonly keySizeBits: number;
|
|
5
|
+
readonly salt: Uint8Array<ArrayBuffer>;
|
|
6
|
+
readonly encryptedVerifier: Uint8Array<ArrayBuffer>;
|
|
7
|
+
readonly encryptedVerifierHash: Uint8Array<ArrayBuffer>;
|
|
8
|
+
}
|
|
9
|
+
/** Parses a DocumentEncryptionAtom's own fields -- never encrypted, since it is what a decryptor needs before it can decrypt anything else. */
|
|
10
|
+
declare function readDocumentEncryptionAtom(record: PptRecord): PptEncryptionInfo;
|
|
11
|
+
/**
|
|
12
|
+
* Decrypts a PowerPoint Document stream protected by [MS-OFFCRYPTO] 2.3.5 RC4 CryptoAPI, given the password and the persist directory/`encryptSessionPersistIdRef` `buildPersistDirectory` already resolved -- both come from parsing the stream's own never-encrypted UserEditAtom/PersistDirectoryAtom chain, which is why this module needs them handed in rather than deriving them itself.
|
|
13
|
+
*
|
|
14
|
+
* Returns a new, fully decrypted copy of the stream: every persist object in `directory` except the DocumentEncryptionAtom itself gets its own key, derived from its own persist ID as the RC4 CryptoAPI "block number" (see this file's own top comment), and is decrypted as one continuous keystream covering its header and data together -- the header is encrypted too here, unlike the shared xls/doc RC4 scheme, so each object's own recLen has to be learned by decrypting its header first.
|
|
15
|
+
*
|
|
16
|
+
* Throws `PptEncryptedError` for a missing password, an incorrect one, or an encryption shape this module does not implement (anything other than RC4 CryptoAPI) -- there is no partial or best-effort result to return in any of those cases.
|
|
17
|
+
*/
|
|
18
|
+
declare function decryptPptDocumentStream(streamBytes: Uint8Array<ArrayBuffer>, directory: ReadonlyMap<number, number>, encryptSessionPersistIdRef: number, password: string): Uint8Array<ArrayBuffer>;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { PptEncryptionInfo, decryptPptDocumentStream, readDocumentEncryptionAtom };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { t as PptRecord } from "./tree-PMcPNgd-.js";
|
|
2
|
+
//#region src/encryption.d.ts
|
|
3
|
+
interface PptEncryptionInfo {
|
|
4
|
+
readonly keySizeBits: number;
|
|
5
|
+
readonly salt: Uint8Array<ArrayBuffer>;
|
|
6
|
+
readonly encryptedVerifier: Uint8Array<ArrayBuffer>;
|
|
7
|
+
readonly encryptedVerifierHash: Uint8Array<ArrayBuffer>;
|
|
8
|
+
}
|
|
9
|
+
/** Parses a DocumentEncryptionAtom's own fields -- never encrypted, since it is what a decryptor needs before it can decrypt anything else. */
|
|
10
|
+
declare function readDocumentEncryptionAtom(record: PptRecord): PptEncryptionInfo;
|
|
11
|
+
/**
|
|
12
|
+
* Decrypts a PowerPoint Document stream protected by [MS-OFFCRYPTO] 2.3.5 RC4 CryptoAPI, given the password and the persist directory/`encryptSessionPersistIdRef` `buildPersistDirectory` already resolved -- both come from parsing the stream's own never-encrypted UserEditAtom/PersistDirectoryAtom chain, which is why this module needs them handed in rather than deriving them itself.
|
|
13
|
+
*
|
|
14
|
+
* Returns a new, fully decrypted copy of the stream: every persist object in `directory` except the DocumentEncryptionAtom itself gets its own key, derived from its own persist ID as the RC4 CryptoAPI "block number" (see this file's own top comment), and is decrypted as one continuous keystream covering its header and data together -- the header is encrypted too here, unlike the shared xls/doc RC4 scheme, so each object's own recLen has to be learned by decrypting its header first.
|
|
15
|
+
*
|
|
16
|
+
* Throws `PptEncryptedError` for a missing password, an incorrect one, or an encryption shape this module does not implement (anything other than RC4 CryptoAPI) -- there is no partial or best-effort result to return in any of those cases.
|
|
17
|
+
*/
|
|
18
|
+
declare function decryptPptDocumentStream(streamBytes: Uint8Array<ArrayBuffer>, directory: ReadonlyMap<number, number>, encryptSessionPersistIdRef: number, password: string): Uint8Array<ArrayBuffer>;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { PptEncryptionInfo, decryptPptDocumentStream, readDocumentEncryptionAtom };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { PptEncryptedError, PptFormatError } from "./errors.js";
|
|
2
|
+
import "./record/header.js";
|
|
3
|
+
import { readRecordAt } from "./record/tree.js";
|
|
4
|
+
import { RT_CryptSession10Container } from "./record/types.js";
|
|
5
|
+
import { RC4_CRYPTOAPI_SALT_LENGTH, RC4_CRYPTOAPI_VERIFIER_HASH_LENGTH, RC4_CRYPTOAPI_VERIFIER_LENGTH, deriveRc4CryptoApiBlockKey, rc4, verifyRc4CryptoApiPassword } from "archive-codec";
|
|
6
|
+
//#region src/encryption.ts
|
|
7
|
+
const ALG_ID_RC4 = 26625;
|
|
8
|
+
const ALG_ID_HASH_SHA1 = 32772;
|
|
9
|
+
const CRYPTOAPI_VERSION_MINOR = 2;
|
|
10
|
+
const CRYPTOAPI_VERSION_MAJOR_MIN = 2;
|
|
11
|
+
const CRYPTOAPI_VERSION_MAJOR_MAX = 4;
|
|
12
|
+
const VERSION_MAJOR_OFFSET = 0;
|
|
13
|
+
const VERSION_MINOR_OFFSET = 2;
|
|
14
|
+
const HEADER_SIZE_FIELD_OFFSET = 8;
|
|
15
|
+
const HEADER_START = 12;
|
|
16
|
+
const DEFAULT_KEY_SIZE_BITS = 40;
|
|
17
|
+
const MIN_HEADER_FIELDS_LENGTH = 32;
|
|
18
|
+
/** Parses a DocumentEncryptionAtom's own fields -- never encrypted, since it is what a decryptor needs before it can decrypt anything else. */
|
|
19
|
+
function readDocumentEncryptionAtom(record) {
|
|
20
|
+
if (record.header.recType !== 12052) throw new PptFormatError(`expected the DocumentEncryptionAtom's own record type (0x${RT_CryptSession10Container.toString(16)}) at offset ${record.offset}, found 0x${record.header.recType.toString(16)}`);
|
|
21
|
+
const { data } = record;
|
|
22
|
+
if (data.length < HEADER_START) throw new PptFormatError(`DocumentEncryptionAtom at offset ${record.offset} carries ${data.length} bytes, fewer than the ${HEADER_START}-byte fixed portion (version, encryptionFlags, headerSize) it requires`);
|
|
23
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
24
|
+
const versionMajor = view.getUint16(VERSION_MAJOR_OFFSET, true);
|
|
25
|
+
const versionMinor = view.getUint16(VERSION_MINOR_OFFSET, true);
|
|
26
|
+
if (versionMajor < CRYPTOAPI_VERSION_MAJOR_MIN || versionMajor > CRYPTOAPI_VERSION_MAJOR_MAX || versionMinor !== CRYPTOAPI_VERSION_MINOR) throw new PptEncryptedError(`DocumentEncryptionAtom declares version ${versionMajor}.${versionMinor}, outside [MS-OFFCRYPTO] 2.3.5.1's RC4 CryptoAPI range (major 2-4, minor 2); this package only decrypts RC4 CryptoAPI-encrypted presentations`);
|
|
27
|
+
const headerSize = view.getUint32(HEADER_SIZE_FIELD_OFFSET, true);
|
|
28
|
+
const headerEnd = HEADER_START + headerSize;
|
|
29
|
+
if (headerSize < MIN_HEADER_FIELDS_LENGTH || headerEnd > data.length) throw new PptFormatError(`DocumentEncryptionAtom at offset ${record.offset} declares a ${headerSize}-byte EncryptionHeader, which is either shorter than the ${MIN_HEADER_FIELDS_LENGTH} fixed fields require or runs past the atom's own ${data.length} bytes`);
|
|
30
|
+
const algId = view.getUint32(20, true);
|
|
31
|
+
if (algId !== ALG_ID_RC4) throw new PptEncryptedError(`DocumentEncryptionAtom's EncryptionHeader names cipher algorithm 0x${algId.toString(16)}, not RC4 (0x${ALG_ID_RC4.toString(16)}); this package only decrypts RC4 CryptoAPI-encrypted presentations`);
|
|
32
|
+
const algIdHash = view.getUint32(24, true);
|
|
33
|
+
if (algIdHash !== ALG_ID_HASH_SHA1) throw new PptEncryptedError(`DocumentEncryptionAtom's EncryptionHeader names hash algorithm 0x${algIdHash.toString(16)}, not SHA-1 (0x${ALG_ID_HASH_SHA1.toString(16)}) as [MS-OFFCRYPTO] 2.3.5.1 requires of RC4 CryptoAPI`);
|
|
34
|
+
const rawKeySize = view.getUint32(28, true);
|
|
35
|
+
const keySizeBits = rawKeySize === 0 ? DEFAULT_KEY_SIZE_BITS : rawKeySize;
|
|
36
|
+
if (headerEnd + 4 > data.length) throw new PptFormatError(`DocumentEncryptionAtom at offset ${record.offset} has no room for its EncryptionVerifier's saltSize field after the ${headerSize}-byte header`);
|
|
37
|
+
const saltSize = view.getUint32(headerEnd, true);
|
|
38
|
+
if (saltSize !== RC4_CRYPTOAPI_SALT_LENGTH) throw new PptFormatError(`DocumentEncryptionAtom's EncryptionVerifier declares saltSize ${saltSize}, not the mandated ${RC4_CRYPTOAPI_SALT_LENGTH}`);
|
|
39
|
+
const saltStart = headerEnd + 4;
|
|
40
|
+
const encryptedVerifierStart = saltStart + RC4_CRYPTOAPI_SALT_LENGTH;
|
|
41
|
+
const verifierHashSizeFieldStart = encryptedVerifierStart + RC4_CRYPTOAPI_VERIFIER_LENGTH;
|
|
42
|
+
const encryptedVerifierHashStart = verifierHashSizeFieldStart + 4;
|
|
43
|
+
const encryptedVerifierHashEnd = encryptedVerifierHashStart + RC4_CRYPTOAPI_VERIFIER_HASH_LENGTH;
|
|
44
|
+
if (encryptedVerifierHashEnd > data.length) throw new PptFormatError(`DocumentEncryptionAtom at offset ${record.offset} carries ${data.length} bytes, too few for its EncryptionVerifier's salt/encryptedVerifier/encryptedVerifierHash fields`);
|
|
45
|
+
return {
|
|
46
|
+
keySizeBits,
|
|
47
|
+
salt: data.subarray(saltStart, encryptedVerifierStart),
|
|
48
|
+
encryptedVerifier: data.subarray(encryptedVerifierStart, verifierHashSizeFieldStart),
|
|
49
|
+
encryptedVerifierHash: data.subarray(encryptedVerifierHashStart, encryptedVerifierHashEnd)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Decrypts a PowerPoint Document stream protected by [MS-OFFCRYPTO] 2.3.5 RC4 CryptoAPI, given the password and the persist directory/`encryptSessionPersistIdRef` `buildPersistDirectory` already resolved -- both come from parsing the stream's own never-encrypted UserEditAtom/PersistDirectoryAtom chain, which is why this module needs them handed in rather than deriving them itself.
|
|
54
|
+
*
|
|
55
|
+
* Returns a new, fully decrypted copy of the stream: every persist object in `directory` except the DocumentEncryptionAtom itself gets its own key, derived from its own persist ID as the RC4 CryptoAPI "block number" (see this file's own top comment), and is decrypted as one continuous keystream covering its header and data together -- the header is encrypted too here, unlike the shared xls/doc RC4 scheme, so each object's own recLen has to be learned by decrypting its header first.
|
|
56
|
+
*
|
|
57
|
+
* Throws `PptEncryptedError` for a missing password, an incorrect one, or an encryption shape this module does not implement (anything other than RC4 CryptoAPI) -- there is no partial or best-effort result to return in any of those cases.
|
|
58
|
+
*/
|
|
59
|
+
function decryptPptDocumentStream(streamBytes, directory, encryptSessionPersistIdRef, password) {
|
|
60
|
+
const encryptionAtomOffset = directory.get(encryptSessionPersistIdRef);
|
|
61
|
+
if (encryptionAtomOffset === void 0) throw new PptFormatError(`UserEditAtom.encryptSessionPersistIdRef ${encryptSessionPersistIdRef} references persist object ${encryptSessionPersistIdRef}, which the persist directory does not contain`);
|
|
62
|
+
const info = readDocumentEncryptionAtom(readRecordAt(streamBytes, encryptionAtomOffset));
|
|
63
|
+
if (!verifyRc4CryptoApiPassword(password, info.salt, info.keySizeBits, info.encryptedVerifier, info.encryptedVerifierHash)) throw new PptEncryptedError("incorrect password for RC4 CryptoAPI-encrypted presentation");
|
|
64
|
+
const decrypted = new Uint8Array(streamBytes.length);
|
|
65
|
+
decrypted.set(streamBytes);
|
|
66
|
+
for (const [persistId, offset] of directory) {
|
|
67
|
+
if (persistId === encryptSessionPersistIdRef) continue;
|
|
68
|
+
if (offset + 8 > streamBytes.length) throw new PptFormatError(`persist object ${persistId} at offset ${offset} needs 8 bytes for its own record header, but only ${streamBytes.length - offset} remain in the stream`);
|
|
69
|
+
const key = deriveRc4CryptoApiBlockKey(password, info.salt, persistId, info.keySizeBits);
|
|
70
|
+
const headerPlaintext = rc4(key, streamBytes.subarray(offset, offset + 8));
|
|
71
|
+
const recLen = new DataView(headerPlaintext.buffer, headerPlaintext.byteOffset, headerPlaintext.byteLength).getUint32(4, true);
|
|
72
|
+
const objectEnd = offset + 8 + recLen;
|
|
73
|
+
if (objectEnd > streamBytes.length) throw new PptFormatError(`persist object ${persistId} at offset ${offset} decrypts to a ${recLen}-byte record, which runs past the stream's own ${streamBytes.length} bytes`);
|
|
74
|
+
const plaintext = rc4(key, streamBytes.subarray(offset, objectEnd));
|
|
75
|
+
decrypted.set(plaintext, offset);
|
|
76
|
+
}
|
|
77
|
+
return decrypted;
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
export { decryptPptDocumentStream, readDocumentEncryptionAtom };
|
package/dist/read.cjs
CHANGED
|
@@ -6,6 +6,7 @@ const require_text_style = require("./text/style.cjs");
|
|
|
6
6
|
const require_text_atoms = require("./text/atoms.cjs");
|
|
7
7
|
const require_units = require("./units.cjs");
|
|
8
8
|
const require_content = require("./content.cjs");
|
|
9
|
+
const require_encryption = require("./encryption.cjs");
|
|
9
10
|
const require_document_document_atom = require("./document/document-atom.cjs");
|
|
10
11
|
const require_document_fonts = require("./document/fonts.cjs");
|
|
11
12
|
const require_document_notes_list = require("./document/notes-list.cjs");
|
|
@@ -89,11 +90,16 @@ function readSlide(streamBytes, directory, persist, size, fontNames, notes) {
|
|
|
89
90
|
notes
|
|
90
91
|
};
|
|
91
92
|
}
|
|
92
|
-
function readPptStreams(currentUserStream, powerPointDocumentStream) {
|
|
93
|
+
function readPptStreams(currentUserStream, powerPointDocumentStream, password) {
|
|
93
94
|
const currentUser = require_stream_current_user.readCurrentUserAtom(currentUserStream);
|
|
94
|
-
if (currentUser.encrypted) throw new require_errors.PptEncryptedError("the CurrentUserAtom's headerToken marks this document as encrypted, and this package does not implement [MS-PPT]'s encryption");
|
|
95
95
|
const { directory, currentEdit } = require_stream_persist.buildPersistDirectory(powerPointDocumentStream, currentUser.offsetToCurrentEdit);
|
|
96
|
-
|
|
96
|
+
let streamBytes = powerPointDocumentStream;
|
|
97
|
+
if (currentUser.encrypted) {
|
|
98
|
+
if (password === void 0) throw new require_errors.PptEncryptedError("the CurrentUserAtom's headerToken marks this document as RC4 CryptoAPI-encrypted ([MS-OFFCRYPTO] 2.3.5); call readPptStreams with a password to decrypt it");
|
|
99
|
+
if (currentEdit.encryptSessionPersistIdRef === void 0) throw new require_errors.PptFormatError("the CurrentUserAtom marks this document as encrypted, but its current UserEditAtom carries no encryptSessionPersistIdRef");
|
|
100
|
+
streamBytes = require_encryption.decryptPptDocumentStream(powerPointDocumentStream, directory, currentEdit.encryptSessionPersistIdRef, password);
|
|
101
|
+
}
|
|
102
|
+
const documentContainer = require_stream_persist.resolvePersistObject(streamBytes, directory, currentEdit.docPersistIdRef, "UserEditAtom.docPersistIdRef");
|
|
97
103
|
if (documentContainer.header.recType !== 1e3) throw new require_errors.PptFormatError(`the document persist object is record type 0x${documentContainer.header.recType.toString(16)}, not RT_Document (0x${require_record_types.RT_Document.toString(16)})`);
|
|
98
104
|
const children = require_record_tree.childRecords(documentContainer);
|
|
99
105
|
const documentAtomRecord = require_record_tree.findChild(children, require_record_types.RT_DocumentAtom);
|
|
@@ -108,15 +114,15 @@ function readPptStreams(currentUserStream, powerPointDocumentStream) {
|
|
|
108
114
|
const listWithInstance = (instance) => children.find((record) => record.header.recType === 4080 && record.header.recInstance === instance);
|
|
109
115
|
const slideList = listWithInstance(0);
|
|
110
116
|
const persists = slideList === void 0 ? [] : require_document_slide_list.readSlideListWithText(slideList);
|
|
111
|
-
const notesBySlideId = readNotesBySlideId(
|
|
117
|
+
const notesBySlideId = readNotesBySlideId(streamBytes, directory, listWithInstance(2));
|
|
112
118
|
return {
|
|
113
119
|
metadata: {},
|
|
114
|
-
slides: persists.map((persist) => readSlide(
|
|
120
|
+
slides: persists.map((persist) => readSlide(streamBytes, directory, persist, size, fontNames, notesBySlideId.get(persist.slideId) ?? ""))
|
|
115
121
|
};
|
|
116
122
|
}
|
|
117
|
-
function readPptContent(bytes) {
|
|
123
|
+
function readPptContent(bytes, password) {
|
|
118
124
|
const streams = (0, archive_codec.readCompoundFile)(bytes);
|
|
119
|
-
const document = readPptStreams(requireStream(streams, CURRENT_USER_STREAM), requireStream(streams, POWERPOINT_DOCUMENT_STREAM));
|
|
125
|
+
const document = readPptStreams(requireStream(streams, CURRENT_USER_STREAM), requireStream(streams, POWERPOINT_DOCUMENT_STREAM), password);
|
|
120
126
|
const metadataStream = streams.find((stream) => stream.path === SUMMARY_INFORMATION_STREAM);
|
|
121
127
|
if (metadataStream === void 0) return document;
|
|
122
128
|
return {
|
|
@@ -124,8 +130,8 @@ function readPptContent(bytes) {
|
|
|
124
130
|
metadata: (0, archive_codec.summaryInformationToLayoutMetadata)((0, archive_codec.readSummaryInformation)(metadataStream.bytes))
|
|
125
131
|
};
|
|
126
132
|
}
|
|
127
|
-
function readPpt(bytes) {
|
|
128
|
-
const { metadata, slides } = readPptContent(bytes);
|
|
133
|
+
function readPpt(bytes, password) {
|
|
134
|
+
const { metadata, slides } = readPptContent(bytes, password);
|
|
129
135
|
const document = {
|
|
130
136
|
kind: "presentation",
|
|
131
137
|
metadata,
|
package/dist/read.d.cts
CHANGED
|
@@ -10,8 +10,8 @@ interface PptDocument {
|
|
|
10
10
|
readonly metadata: LayoutMetadata;
|
|
11
11
|
readonly slides: readonly ContentSlide[];
|
|
12
12
|
}
|
|
13
|
-
declare function readPptStreams(currentUserStream: Uint8Array<ArrayBuffer>, powerPointDocumentStream: Uint8Array<ArrayBuffer
|
|
14
|
-
declare function readPptContent(bytes: Uint8Array<ArrayBuffer
|
|
15
|
-
declare function readPpt(bytes: Uint8Array<ArrayBuffer
|
|
13
|
+
declare function readPptStreams(currentUserStream: Uint8Array<ArrayBuffer>, powerPointDocumentStream: Uint8Array<ArrayBuffer>, password?: string): PptDocument;
|
|
14
|
+
declare function readPptContent(bytes: Uint8Array<ArrayBuffer>, password?: string): PptDocument;
|
|
15
|
+
declare function readPpt(bytes: Uint8Array<ArrayBuffer>, password?: string): DocumentTree;
|
|
16
16
|
//#endregion
|
|
17
17
|
export { CURRENT_USER_STREAM, DEFAULT_INSET_LEFT_RIGHT_PT, DEFAULT_INSET_TOP_BOTTOM_PT, POWERPOINT_DOCUMENT_STREAM, PptDocument, SUMMARY_INFORMATION_STREAM, readPpt, readPptContent, readPptStreams };
|
package/dist/read.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ interface PptDocument {
|
|
|
10
10
|
readonly metadata: LayoutMetadata;
|
|
11
11
|
readonly slides: readonly ContentSlide[];
|
|
12
12
|
}
|
|
13
|
-
declare function readPptStreams(currentUserStream: Uint8Array<ArrayBuffer>, powerPointDocumentStream: Uint8Array<ArrayBuffer
|
|
14
|
-
declare function readPptContent(bytes: Uint8Array<ArrayBuffer
|
|
15
|
-
declare function readPpt(bytes: Uint8Array<ArrayBuffer
|
|
13
|
+
declare function readPptStreams(currentUserStream: Uint8Array<ArrayBuffer>, powerPointDocumentStream: Uint8Array<ArrayBuffer>, password?: string): PptDocument;
|
|
14
|
+
declare function readPptContent(bytes: Uint8Array<ArrayBuffer>, password?: string): PptDocument;
|
|
15
|
+
declare function readPpt(bytes: Uint8Array<ArrayBuffer>, password?: string): DocumentTree;
|
|
16
16
|
//#endregion
|
|
17
17
|
export { CURRENT_USER_STREAM, DEFAULT_INSET_LEFT_RIGHT_PT, DEFAULT_INSET_TOP_BOTTOM_PT, POWERPOINT_DOCUMENT_STREAM, PptDocument, SUMMARY_INFORMATION_STREAM, readPpt, readPptContent, readPptStreams };
|
package/dist/read.js
CHANGED
|
@@ -5,6 +5,7 @@ import { readStyleTextPropAtom } from "./text/style.js";
|
|
|
5
5
|
import { characterCountOf, readTextBody } from "./text/atoms.js";
|
|
6
6
|
import { masterUnitsToPoints } from "./units.js";
|
|
7
7
|
import { buildParagraphs } from "./content.js";
|
|
8
|
+
import { decryptPptDocumentStream } from "./encryption.js";
|
|
8
9
|
import { readDocumentAtom } from "./document/document-atom.js";
|
|
9
10
|
import { readFontNames } from "./document/fonts.js";
|
|
10
11
|
import { readNotesListWithText } from "./document/notes-list.js";
|
|
@@ -88,11 +89,16 @@ function readSlide(streamBytes, directory, persist, size, fontNames, notes) {
|
|
|
88
89
|
notes
|
|
89
90
|
};
|
|
90
91
|
}
|
|
91
|
-
function readPptStreams(currentUserStream, powerPointDocumentStream) {
|
|
92
|
+
function readPptStreams(currentUserStream, powerPointDocumentStream, password) {
|
|
92
93
|
const currentUser = readCurrentUserAtom(currentUserStream);
|
|
93
|
-
if (currentUser.encrypted) throw new PptEncryptedError("the CurrentUserAtom's headerToken marks this document as encrypted, and this package does not implement [MS-PPT]'s encryption");
|
|
94
94
|
const { directory, currentEdit } = buildPersistDirectory(powerPointDocumentStream, currentUser.offsetToCurrentEdit);
|
|
95
|
-
|
|
95
|
+
let streamBytes = powerPointDocumentStream;
|
|
96
|
+
if (currentUser.encrypted) {
|
|
97
|
+
if (password === void 0) throw new PptEncryptedError("the CurrentUserAtom's headerToken marks this document as RC4 CryptoAPI-encrypted ([MS-OFFCRYPTO] 2.3.5); call readPptStreams with a password to decrypt it");
|
|
98
|
+
if (currentEdit.encryptSessionPersistIdRef === void 0) throw new PptFormatError("the CurrentUserAtom marks this document as encrypted, but its current UserEditAtom carries no encryptSessionPersistIdRef");
|
|
99
|
+
streamBytes = decryptPptDocumentStream(powerPointDocumentStream, directory, currentEdit.encryptSessionPersistIdRef, password);
|
|
100
|
+
}
|
|
101
|
+
const documentContainer = resolvePersistObject(streamBytes, directory, currentEdit.docPersistIdRef, "UserEditAtom.docPersistIdRef");
|
|
96
102
|
if (documentContainer.header.recType !== 1e3) throw new PptFormatError(`the document persist object is record type 0x${documentContainer.header.recType.toString(16)}, not RT_Document (0x${RT_Document.toString(16)})`);
|
|
97
103
|
const children = childRecords(documentContainer);
|
|
98
104
|
const documentAtomRecord = findChild(children, RT_DocumentAtom);
|
|
@@ -107,15 +113,15 @@ function readPptStreams(currentUserStream, powerPointDocumentStream) {
|
|
|
107
113
|
const listWithInstance = (instance) => children.find((record) => record.header.recType === 4080 && record.header.recInstance === instance);
|
|
108
114
|
const slideList = listWithInstance(0);
|
|
109
115
|
const persists = slideList === void 0 ? [] : readSlideListWithText(slideList);
|
|
110
|
-
const notesBySlideId = readNotesBySlideId(
|
|
116
|
+
const notesBySlideId = readNotesBySlideId(streamBytes, directory, listWithInstance(2));
|
|
111
117
|
return {
|
|
112
118
|
metadata: {},
|
|
113
|
-
slides: persists.map((persist) => readSlide(
|
|
119
|
+
slides: persists.map((persist) => readSlide(streamBytes, directory, persist, size, fontNames, notesBySlideId.get(persist.slideId) ?? ""))
|
|
114
120
|
};
|
|
115
121
|
}
|
|
116
|
-
function readPptContent(bytes) {
|
|
122
|
+
function readPptContent(bytes, password) {
|
|
117
123
|
const streams = readCompoundFile(bytes);
|
|
118
|
-
const document = readPptStreams(requireStream(streams, CURRENT_USER_STREAM), requireStream(streams, POWERPOINT_DOCUMENT_STREAM));
|
|
124
|
+
const document = readPptStreams(requireStream(streams, CURRENT_USER_STREAM), requireStream(streams, POWERPOINT_DOCUMENT_STREAM), password);
|
|
119
125
|
const metadataStream = streams.find((stream) => stream.path === SUMMARY_INFORMATION_STREAM);
|
|
120
126
|
if (metadataStream === void 0) return document;
|
|
121
127
|
return {
|
|
@@ -123,8 +129,8 @@ function readPptContent(bytes) {
|
|
|
123
129
|
metadata: summaryInformationToLayoutMetadata(readSummaryInformation(metadataStream.bytes))
|
|
124
130
|
};
|
|
125
131
|
}
|
|
126
|
-
function readPpt(bytes) {
|
|
127
|
-
const { metadata, slides } = readPptContent(bytes);
|
|
132
|
+
function readPpt(bytes, password) {
|
|
133
|
+
const { metadata, slides } = readPptContent(bytes, password);
|
|
128
134
|
const document = {
|
|
129
135
|
kind: "presentation",
|
|
130
136
|
metadata,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ppt-codec",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Hand-written PowerPoint 97-2003 binary (.ppt, [MS-PPT]) reader and writer against the shared document-schema.js content pivot.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"license": "MIT",
|
|
79
79
|
"packageManager": "pnpm@11.6.0",
|
|
80
80
|
"dependencies": {
|
|
81
|
-
"archive-codec": "1.
|
|
81
|
+
"archive-codec": "1.9.0",
|
|
82
82
|
"document-schema.js": "7.3.1"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|