Skip to content

Commit

Permalink
Implement encryption
Browse files Browse the repository at this point in the history
Based on PhakornKiong/pdf-lib#dev/DocEncrypt
  • Loading branch information
programmarchy committed May 23, 2024
1 parent 61c640a commit ac7c7cd
Show file tree
Hide file tree
Showing 15 changed files with 1,033 additions and 7 deletions.
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
"Brent McSharry (https://github.com/mcshaz)",
"Tim Knapp (https://github.com/duffyd)",
"Ching Chang (https://github.com/ChingChang9)",
"François Billioud (https://github.com/Sharcoux)"
"François Billioud (https://github.com/Sharcoux)",
"Phakorn Kiong (https://github.com/PhakornKiong)",
"Donald Ness (https://github.com/programmarchy)"
],
"scripts": {
"release": "yarn release:prep && release-it",
Expand Down Expand Up @@ -98,6 +100,7 @@
"@pdf-lib/standard-fonts": "^1.0.0",
"@pdf-lib/upng": "^1.0.1",
"color": "^4.2.3",
"crypto-js": "^4.2.0",
"node-html-better-parser": "^1.4.0",
"pako": "^1.0.11"
},
Expand All @@ -108,6 +111,7 @@
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"@types/color": "^3.0.1",
"@types/crypto-js": "^4.2.2",
"@types/jest": "^29.5.12",
"@types/node-fetch": "^2.5.7",
"@types/pako": "^1.0.1",
Expand Down
5 changes: 5 additions & 0 deletions src/api/PDFDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import PDFJavaScript from './PDFJavaScript';
import JavaScriptEmbedder from '../core/embedders/JavaScriptEmbedder';
import { CipherTransformFactory } from '../core/crypto';
import PDFSvg from './PDFSvg';
import PDFSecurity, { SecurityOptions } from '../core/security/PDFSecurity';

/**
* Represents a PDF document.
Expand Down Expand Up @@ -1299,6 +1300,10 @@ export default class PDFDocument {
return embeddedPages;
}

encrypt(options: SecurityOptions) {
this.context.security = PDFSecurity.create(this.context, options).encrypt();
}

/**
* > **NOTE:** You shouldn't need to call this method directly. The [[save]]
* > and [[saveAsBase64]] methods will automatically ensure that all embedded
Expand Down
5 changes: 5 additions & 0 deletions src/core/PDFContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import PDFString from './objects/PDFString';
import PDFOperator from './operators/PDFOperator';
import Ops from './operators/PDFOperatorNames';
import PDFContentStream from './structures/PDFContentStream';
import PDFSecurity from './security/PDFSecurity';
import { typedArrayFor } from '../utils';
import { SimpleRNG } from '../utils/rng';

Expand Down Expand Up @@ -65,6 +66,8 @@ class PDFContext {
};
rng: SimpleRNG;

security?: PDFSecurity;

private readonly indirectObjects: Map<PDFRef, PDFObject>;

private pushGraphicsStateContentStreamRef?: PDFRef;
Expand Down Expand Up @@ -210,6 +213,8 @@ class PDFContext {
return PDFNumber.of(literal);
} else if (typeof literal === 'boolean') {
return literal ? PDFBool.True : PDFBool.False;
} else if (literal instanceof Uint8Array) {
return PDFHexString.fromBytes(literal);
} else if (Array.isArray(literal)) {
const array = PDFArray.withContext(this);
for (let idx = 0, len = literal.length; idx < len; idx++) {
Expand Down
4 changes: 4 additions & 0 deletions src/core/document/PDFHeader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ class PDFHeader {
this.minor = String(minor);
}

getVersionString(): string {
return `${this.major}.${this.minor}`;
}

toString(): string {
const bc = charFromCode(129);
return `%PDF-${this.major}.${this.minor}\n%${bc}${bc}${bc}${bc}`;
Expand Down
2 changes: 2 additions & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export { default as PDFObjectStreamParser } from './parser/PDFObjectStreamParser
export { default as PDFParser } from './parser/PDFParser';
export { default as PDFXRefStreamParser } from './parser/PDFXRefStreamParser';

export { default as PDFSecurity, SecurityOptions } from './security/PDFSecurity';

export { decodePDFRawStream } from './streams/decode';

export * from './annotation';
Expand Down
5 changes: 5 additions & 0 deletions src/core/objects/PDFHexString.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
pdfDocEncodingDecode,
parseDate,
hasUtf16BOM,
byteArrayToHexString,
} from '../../utils';
import { InvalidPDFDateStringError } from '../errors';

Expand All @@ -25,6 +26,10 @@ class PDFHexString extends PDFObject {
return new PDFHexString(hex);
};

static fromBytes = (bytes: Uint8Array) => {
return PDFHexString.of(byteArrayToHexString(bytes));
};

private readonly value: string;

constructor(value: string) {
Expand Down
6 changes: 5 additions & 1 deletion src/core/objects/PDFRawStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class PDFRawStream extends PDFStream {
transform?: CipherTransform,
) => new PDFRawStream(dict, contents, transform);

readonly contents: Uint8Array;
contents: Uint8Array;
readonly transform?: CipherTransform;

private constructor(
Expand Down Expand Up @@ -43,6 +43,10 @@ class PDFRawStream extends PDFStream {
getContentsSize(): number {
return this.contents.length;
}

updateContents(contents: Uint8Array): void {
this.contents = contents;
}
}

export default PDFRawStream;
7 changes: 7 additions & 0 deletions src/core/objects/PDFStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ class PDFStream extends PDFObject {
);
}

updateContents(_contents: Uint8Array): void {
throw new MethodNotImplementedError(
this.constructor.name,
'updateContents',
);
}

updateDict(): void {
const contentsSize = this.getContentsSize();
this.dict.set(PDFName.Length, PDFNumber.of(contentsSize));
Expand Down
209 changes: 209 additions & 0 deletions src/core/security/Encryption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import PDFContext from '../PDFContext';
import PDFObject from '../objects/PDFObject';
import PDFDict from '../objects/PDFDict';
import PDFName from '../objects/PDFName';
import PDFNumber from '../objects/PDFNumber';
import PDFHexString from '../objects/PDFHexString';
import PDFString from '../objects/PDFString';

export default class Encryption {
readonly dict: PDFDict;

static fromDict = (dict: PDFDict): Encryption =>
new Encryption(dict);

protected constructor(dict: PDFDict) {
this.dict = dict;
}

V(): PDFNumber {
return this.dict.lookup(PDFName.of('V'), PDFNumber);
}

R(): PDFNumber {
return this.dict.lookup(PDFName.of('R'), PDFNumber);
}

O(): PDFHexString {
return this.dict.lookup(PDFName.of('O'), PDFHexString);
}

U(): PDFHexString {
return this.dict.lookup(PDFName.of('U'), PDFHexString);
}

P(): PDFNumber {
return this.dict.lookup(PDFName.of('P'), PDFNumber);
}

Filter(): PDFString {
return this.dict.lookup(PDFName.of('Filter'), PDFString);
}

Length(): PDFNumber | undefined {
return this.dict.lookupMaybe(PDFName.of('Length'), PDFNumber);
}

// Only when V === 4
CF(): CryptFilter | undefined {
if (this.dict.has(PDFName.of('CF'))) {
return CryptFilter.fromDict(this.dict.lookup(PDFName.of('CF'), PDFDict));
} else {
return undefined;
}
}

StmF(): PDFString | undefined {
return this.dict.lookupMaybe(PDFName.of('StmF'), PDFString);
}

StrF(): PDFString | undefined {
return this.dict.lookupMaybe(PDFName.of('StrF'), PDFString);
}

OE(): PDFHexString {
return this.dict.lookup(PDFName.of('U'), PDFHexString);
}

UE(): PDFHexString {
return this.dict.lookup(PDFName.of('UE'), PDFHexString);
}

Perms(): PDFHexString {
return this.dict.lookup(PDFName.of('Perms'), PDFHexString);
}

getAlgorithm() {
return this.V().asNumber();
}

setAlgorithm(algorithm: number) {
this.dict.set(PDFName.of('V'), PDFNumber.of(algorithm));
}

getRevision() {
return this.V().asNumber();
}

setRevision(revision: number) {
this.dict.set(PDFName.of('R'), PDFNumber.of(revision));
}

setOwnerPassword(byteArray: Uint8Array) {
this.dict.set(PDFName.of('O'), PDFHexString.fromBytes(byteArray));
}

setUserPassword(byteArray: Uint8Array) {
this.dict.set(PDFName.of('U'), PDFHexString.fromBytes(byteArray));
}

setPermissionFlags(flags: number) {
this.dict.set(PDFName.of('P'), PDFNumber.of(flags));
}

getFilter(): string {
return this.Filter().asString();
}

setFilter(filter: string) {
this.dict.set(PDFName.of('Filter'), PDFString.of(filter));
}

getLength(): number | undefined {
const length = this.Length();
if (!length) return undefined;
return length.asNumber();
}

setLength(length: number) {
this.dict.set(PDFName.of('Length'), PDFNumber.of(length));
}

setCryptFilter(cryptFilter: CryptFilter) {
this.dict.set(PDFName.of('CF'), cryptFilter.dict);
}

setStreamFilter(streamFilter: string) {
this.dict.set(PDFName.of('StmF'), PDFString.of(streamFilter));
}

setStringFilter(stringFilter: string) {
this.dict.set(PDFName.of('StrF'), PDFString.of(stringFilter));
}

setOwnerEncryptionKey(byteArray: Uint8Array) {
this.dict.set(PDFName.of('OE'), PDFHexString.fromBytes(byteArray));
}

setUserEncryptionKey(byteArray: Uint8Array) {
this.dict.set(PDFName.of('UE'), PDFHexString.fromBytes(byteArray));
}

setPermissions(byteArray: Uint8Array) {
this.dict.set(PDFName.of('Perms'), PDFHexString.fromBytes(byteArray));
}
}

type CF = {
StdCF: StdCF;
};

export class CryptFilter {
readonly dict: PDFDict;

static fromDict = (dict: PDFDict): CryptFilter =>
new CryptFilter(dict);

static fromObjectWithContext = (object: CF, context: PDFContext): CryptFilter =>
new CryptFilter(
PDFDict.fromMapWithContext(new Map<PDFName, PDFObject>([
[PDFName.of('StdCF'), StandardCryptFilter.fromObjectWithContext(object.StdCF, context).dict],
]), context),
);

protected constructor(dict: PDFDict) {
this.dict = dict;
}

StdCF(): StandardCryptFilter {
return StandardCryptFilter.fromDict(this.dict.lookup(PDFName.of('StdCF'), PDFDict));
}
}

type StdCF = {
AuthEvent: 'DocOpen';
CFM: 'AESV2' | 'AESV3';
Length: number;
};

export class StandardCryptFilter {
readonly dict: PDFDict;

static fromDict = (dict: PDFDict): StandardCryptFilter =>
new StandardCryptFilter(dict);

static fromObjectWithContext = (object: StdCF, context: PDFContext): StandardCryptFilter =>
new StandardCryptFilter(
PDFDict.fromMapWithContext(new Map<PDFName, PDFObject>([
[PDFName.of('AuthEvent'), PDFString.of(object.AuthEvent)],
[PDFName.of('CFM'), PDFString.of(object.CFM)],
[PDFName.of('Length'), PDFNumber.of(object.Length)],
]), context)
);

protected constructor(dict: PDFDict) {
this.dict = dict;
}

AuthEvent(): PDFString {
return this.dict.lookup(PDFName.of('AuthEvent'), PDFString);
}

CFM(): PDFString {
return this.dict.lookup(PDFName.of('CFM'), PDFString);
}

Length(): PDFNumber {
return this.dict.lookup(PDFName.of('Length'), PDFNumber);
}
}
Loading

0 comments on commit ac7c7cd

Please sign in to comment.