Skip to content
This repository was archived by the owner on Apr 12, 2021. It is now read-only.

Commit d5d8e3c

Browse files
authored
Merge pull request foliojs#820 from zesik/master
Implement encryption and access privileges
2 parents 6f0d5ff + c400dfa commit d5d8e3c

26 files changed

Lines changed: 696 additions & 47 deletions

README.md

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ A JavaScript PDF generation library for Node and the browser.
66

77
## Description
88

9-
PDFKit is a PDF document generation library for Node and the browser that makes creating complex, multi-page, printable documents easy.
10-
It's written in CoffeeScript, but you can choose to use the API in plain 'ol JavaScript if you like. The API embraces
11-
chainability, and includes both low level functions as well as abstractions for higher level functionality. The PDFKit API
9+
PDFKit is a PDF document generation library for Node and the browser that makes creating complex, multi-page, printable documents easy.
10+
It's written in CoffeeScript, but you can choose to use the API in plain 'ol JavaScript if you like. The API embraces
11+
chainability, and includes both low level functions as well as abstractions for higher level functionality. The PDFKit API
1212
is designed to be simple, so generating complex documents is often as simple as a few function calls.
1313

1414
Check out some of the [documentation and examples](http://pdfkit.org/docs/getting_started.html) to see for yourself!
@@ -49,15 +49,17 @@ Installation uses the [npm](http://npmjs.org/) package manager. Just type the f
4949
* Underlines
5050
* etc.
5151
* Outlines
52-
52+
* PDF security
53+
* Encryption
54+
* Access privileges (printing, copying, modifying, annotating, form filling, content accessibility, document assembly)
55+
5356
## Coming soon!
5457

5558
* Patterns fills
56-
* PDF Security
5759
* Higher level APIs for creating tables and laying out content
5860
* More performance optimizations
5961
* Even more awesomeness, perhaps written by you! Please fork this repository and send me pull requests.
60-
62+
6163
## Example
6264

6365
```coffeescript
@@ -111,9 +113,9 @@ doc.addPage()
111113
# Finalize PDF file
112114
doc.end()
113115
```
114-
115-
[The PDF output from this example](http://pdfkit.org/demo/out.pdf) (with a few additions) shows the power of PDFKit — producing
116-
complex documents with a very small amount of code. For more, see the `demo` folder and the
116+
117+
[The PDF output from this example](http://pdfkit.org/demo/out.pdf) (with a few additions) shows the power of PDFKit — producing
118+
complex documents with a very small amount of code. For more, see the `demo` folder and the
117119
[PDFKit programming guide](http://pdfkit.org/docs/getting_started.html).
118120

119121
## Browser Usage
@@ -122,13 +124,13 @@ There are two ways to use PDFKit in the browser. The first is to use [Browserif
122124
which is a Node module packager for the browser with the familiar `require` syntax. The second is to use
123125
a prebuilt version of PDFKit, which you can [download from Github](https://github.com/devongovett/pdfkit/releases).
124126

125-
In addition to PDFKit, you'll need somewhere to stream the output to. HTML5 has a
127+
In addition to PDFKit, you'll need somewhere to stream the output to. HTML5 has a
126128
[Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) object which can be used to store binary data, and
127-
get URLs to this data in order to display PDF output inside an iframe, or upload to a server, etc. In order to
129+
get URLs to this data in order to display PDF output inside an iframe, or upload to a server, etc. In order to
128130
get a Blob from the output of PDFKit, you can use the [blob-stream](https://github.com/devongovett/blob-stream)
129131
module.
130132

131-
The following example uses Browserify to load `PDFKit` and `blob-stream`, but if you're not using Browserify,
133+
The following example uses Browserify to load `PDFKit` and `blob-stream`, but if you're not using Browserify,
132134
you can load them in whatever way you'd like (e.g. script tags).
133135

134136
```coffeescript
@@ -157,9 +159,9 @@ stream.on 'finish', ->
157159

158160
You can see an interactive in-browser demo of PDFKit [here](http://pdfkit.org/demo/browser.html).
159161

160-
Note that in order to Browserify a project using PDFKit, you need to install the `brfs` module with npm,
161-
which is used to load built-in font data into the package. It is listed as a `devDependency` in
162-
PDFKit's `package.json`, so it isn't installed by default for Node users.
162+
Note that in order to Browserify a project using PDFKit, you need to install the `brfs` module with npm,
163+
which is used to load built-in font data into the package. It is listed as a `devDependency` in
164+
PDFKit's `package.json`, so it isn't installed by default for Node users.
163165
If you forget to install it, Browserify will print an error message.
164166

165167
## Documentation

docs/getting_started.coffee.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,71 @@ capitalized.
183183
* `CreationDate` - the date the document was created (added automatically by PDFKit)
184184
* `ModDate` - the date the document was last modified
185185
186+
## Encryption and Access Privileges
187+
188+
PDF specification allow you to encrypt the PDF file and require a password when opening the file,
189+
and/or set permissions of what users can do with the PDF file. PDFKit implements standard security
190+
handler in PDF version 1.3 (40-bit RC4), version 1.4 (128-bit RC4), PDF version 1.7 (128-bit AES),
191+
and PDF version 1.7 ExtensionLevel 3 (256-bit AES).
192+
193+
To enable encryption, provide a user password when creating the `PDFDocument` in `options` object.
194+
The PDF file will be encrypted when a user password is provided, and users will be prompted to enter
195+
the password to decrypt the file when opening it.
196+
197+
* `userPassword` - the user password (string value)
198+
199+
To set access privileges for the PDF file, you need to provide an owner password and permission
200+
settings in the `option` object when creating `PDFDocument`. By default, all operations are disallowed.
201+
You need to explicitly allow certain operations.
202+
203+
* `ownerPassword` - the owner password (string value)
204+
* `permissions` - the object specifying PDF file permissions
205+
206+
Following settings are allowed in `permissions` object:
207+
208+
* `printing` - whether printing is allowed. Specify `"lowResolution"` to allow degraded printing, or `"highResolution"` to allow printing with high resolution
209+
* `modifying` - whether modifying the file is allowed. Specify `true` to allow modifying document content
210+
* `copying` - whether copying text or graphics is allowed. Specify `true` to allow copying
211+
* `annotating` - whether annotating, form filling is allowed. Specify `true` to allow annotating and form filling
212+
* `fillingForms` - whether form filling and signing is allowed. Specify `true` to allow filling in form fields and signing
213+
* `contentAccessibility` - whether copying text for accessibility is allowed. Specify `true` to allow copying for accessibility
214+
* `documentAssembly` - whether assembling document is allowed. Specify `true` to allow document assembly
215+
216+
You can specify either user password, owner password or both passwords.
217+
Behavior differs according to passwords you provides:
218+
219+
* When only user password is provided,
220+
users with user password are able to decrypt the file and have full access to the document.
221+
* When only owner password is provided,
222+
users are able to decrypt and open the document without providing any password,
223+
but the access is limited to those operations explicitly permitted.
224+
Users with owner password have full access to the document.
225+
* When both passwords are provided,
226+
users with user password are able to decrypt the file
227+
but only have limited access to the file according to permission settings.
228+
Users with owner password have full access to the document.
229+
230+
Note that PDF file itself cannot enforce access privileges.
231+
When file is decrypted, PDF viewer applications have full access to the file content,
232+
and it is up to viewer applications to respect permission settings.
233+
234+
To choose encryption method, you need to specify PDF version.
235+
PDFKit will choose best encryption method available in the PDF version you specified.
236+
237+
* `pdfVersion` - a string value specifying PDF file version
238+
239+
Available options includes:
240+
241+
* `1.3` - PDF version 1.3 (default), 40-bit RC4 is used
242+
* `1.4` - PDF version 1.4, 128-bit RC4 is used
243+
* `1.5` - PDF version 1.5, 128-bit RC4 is used
244+
* `1.6` - PDF version 1.6, 128-bit AES is used
245+
* `1.7` - PDF version 1.7, 128-bit AES is used
246+
* `1.7ext3` - PDF version 1.7 ExtensionLevel 3, 256-bit AES is used
247+
248+
When using PDF version 1.7 ExtensionLevel 3, password is truncated to 127 bytes of its UTF-8 representation.
249+
In older versions, password is truncated to 32 bytes, and only Latin-1 characters are allowed.
250+
186251
### Adding content
187252

188253
Once you've created a `PDFDocument` instance, you can add content to the

lib/document.js

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import fs from 'fs';
88
import PDFObject from './object';
99
import PDFReference from './reference';
1010
import PDFPage from './page';
11+
import PDFSecurity from './security';
1112
import ColorMixin from './mixins/color';
1213
import VectorMixin from './mixins/vector';
1314
import FontsMixin from './mixins/fonts';
@@ -22,7 +23,24 @@ class PDFDocument extends stream.Readable {
2223
this.options = options;
2324

2425
// PDF version
25-
this.version = 1.3;
26+
switch (options.pdfVersion) {
27+
case '1.4':
28+
this.version = 1.4;
29+
break;
30+
case '1.5':
31+
this.version = 1.5;
32+
break;
33+
case '1.6':
34+
this.version = 1.6;
35+
break;
36+
case '1.7':
37+
case '1.7ext3':
38+
this.version = 1.7;
39+
break;
40+
default:
41+
this.version = 1.3;
42+
break;
43+
}
2644

2745
// Whether streams should be compressed
2846
this.compress = this.options.compress != null ? this.options.compress : true;
@@ -82,6 +100,12 @@ class PDFDocument extends stream.Readable {
82100
}
83101
}
84102

103+
// Generate file ID
104+
this._id = PDFSecurity.generateFileID(this.info);
105+
106+
// Initialize security settings
107+
this._security = PDFSecurity.create(this, options);
108+
85109
// Write the header
86110
// PDF version
87111
this._write(`%PDF-${this.version}`);
@@ -213,7 +237,10 @@ Please pipe the document into a Node stream.\
213237
val = new String(val);
214238
}
215239

216-
this._info.data[key] = val;
240+
let entry = this.ref(val);
241+
entry.end();
242+
243+
this._info.data[key] = entry;
217244
}
218245

219246
this._info.end();
@@ -224,10 +251,14 @@ Please pipe the document into a Node stream.\
224251
}
225252

226253
this.endOutline();
227-
254+
228255
this._root.end();
229256
this._root.data.Pages.end();
230257

258+
if (this._security) {
259+
this._security.end();
260+
}
261+
231262
if (this._waiting === 0) {
232263
return this._finalize();
233264
} else {
@@ -248,13 +279,18 @@ Please pipe the document into a Node stream.\
248279
}
249280

250281
// trailer
251-
this._write('trailer');
252-
this._write(PDFObject.convert({
282+
const trailer = {
253283
Size: this._offsets.length + 1,
254284
Root: this._root,
255-
Info: this._info
256-
})
257-
);
285+
Info: this._info,
286+
ID: [this._id, this._id]
287+
};
288+
if (this._security) {
289+
trailer.Encrypt = this._security.dictionary;
290+
}
291+
292+
this._write('trailer');
293+
this._write(PDFObject.convert(trailer));
258294

259295
this._write('startxref');
260296
this._write(`${xRefOffset}`);
@@ -270,7 +306,7 @@ Please pipe the document into a Node stream.\
270306
};
271307

272308
const mixin = methods => {
273-
Object.assign(PDFDocument.prototype, methods);
309+
Object.assign(PDFDocument.prototype, methods);
274310
};
275311

276312
mixin(ColorMixin);

lib/object.js

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ By Devon Govett
66
import PDFAbstractReference from './abstract_reference';
77

88
const pad = (str, length) => (Array(length + 1).join('0') + str).slice(-length);
9-
9+
1010
const escapableRe = /[\n\r\t\b\f\(\)\\]/g;
1111
const escapable = {
1212
'\n': '\\n',
@@ -36,7 +36,7 @@ const swapBytes = function(buff) {
3636
};
3737

3838
class PDFObject {
39-
static convert(object) {
39+
static convert(object, encryptFn = null) {
4040
// String literals are converted to the PDF name type
4141
if (typeof object === 'string') {
4242
return `/${object}`;
@@ -54,8 +54,18 @@ class PDFObject {
5454
}
5555

5656
// If so, encode it as big endian UTF-16
57+
let stringBuffer;
5758
if (isUnicode) {
58-
string = swapBytes(new Buffer(`\ufeff${string}`, 'utf16le')).toString('binary');
59+
stringBuffer = swapBytes(new Buffer(`\ufeff${string}`, 'utf16le'));
60+
} else {
61+
stringBuffer = new Buffer(string, 'ascii');
62+
}
63+
64+
// Encrypt the string when necessary
65+
if (encryptFn) {
66+
string = encryptFn(stringBuffer).toString('binary');
67+
} else {
68+
string = stringBuffer.toString('binary');
5969
}
6070

6171
// Escape characters as required by the spec
@@ -71,23 +81,32 @@ class PDFObject {
7181
return object.toString();
7282

7383
} else if (object instanceof Date) {
74-
return `(D:${pad(object.getUTCFullYear(), 4)}` +
75-
pad(object.getUTCMonth() + 1, 2) +
76-
pad(object.getUTCDate(), 2) +
77-
pad(object.getUTCHours(), 2) +
78-
pad(object.getUTCMinutes(), 2) +
79-
pad(object.getUTCSeconds(), 2) +
80-
'Z)';
84+
let string = `D:${pad(object.getUTCFullYear(), 4)}` +
85+
pad(object.getUTCMonth() + 1, 2) +
86+
pad(object.getUTCDate(), 2) +
87+
pad(object.getUTCHours(), 2) +
88+
pad(object.getUTCMinutes(), 2) +
89+
pad(object.getUTCSeconds(), 2) + 'Z';
90+
91+
// Encrypt the string when necessary
92+
if (encryptFn) {
93+
string = encryptFn(new Buffer(string, 'ascii')).toString('binary');
94+
95+
// Escape characters as required by the spec
96+
string = string.replace(escapableRe, c => escapable[c]);
97+
}
98+
99+
return `(${string})`;
81100

82101
} else if (Array.isArray(object)) {
83-
const items = (object.map((e) => PDFObject.convert(e))).join(' ');
102+
const items = (object.map((e) => PDFObject.convert(e, encryptFn))).join(' ');
84103
return `[${items}]`;
85104

86105
} else if ({}.toString.call(object) === '[object Object]') {
87106
const out = ['<<'];
88107
for (let key in object) {
89108
const val = object[key];
90-
out.push(`/${key} ${PDFObject.convert(val)}`);
109+
out.push(`/${key} ${PDFObject.convert(val, encryptFn)}`);
91110
}
92111

93112
out.push('>>');

lib/reference.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import PDFObject from './object';
99

1010
class PDFReference extends PDFAbstractReference {
1111
constructor(document, id, data) {
12-
super();
12+
super();
1313
this.document = document;
1414
this.id = id;
1515
if (data == null) { data = {}; }
@@ -45,15 +45,25 @@ class PDFReference extends PDFAbstractReference {
4545
return setTimeout(() => {
4646
this.offset = this.document._offset;
4747

48-
this.document._write(`${this.id} ${this.gen} obj`);
49-
this.document._write(PDFObject.convert(this.data));
48+
const encryptFn = this.document._security ? this.document._security.getEncryptFn(this.id, this.gen) : null;
5049

5150
if (this.buffer.length) {
5251
this.buffer = Buffer.concat(this.buffer);
5352
if (this.compress) {
5453
this.buffer = zlib.deflateSync(this.buffer);
55-
this.data.Length = this.buffer.length;
5654
}
55+
56+
if (encryptFn) {
57+
this.buffer = encryptFn(this.buffer);
58+
}
59+
60+
this.data.Length = this.buffer.length;
61+
}
62+
63+
this.document._write(`${this.id} ${this.gen} obj`);
64+
this.document._write(PDFObject.convert(this.data, encryptFn));
65+
66+
if (this.buffer.length) {
5767
this.document._write('stream');
5868
this.document._write(this.buffer);
5969

0 commit comments

Comments
 (0)