pdfkit/lib/security.js
2018-12-17 21:20:20 +09:00

405 lines
12 KiB
JavaScript

/*
PDFSecurity - represents PDF security settings
By Yang Liu <hi@zesik.com>
*/
import CryptoJS from 'crypto-js';
import saslprep from 'saslprep';
class PDFSecurity {
static generateFileID(info = {}) {
let infoStr = `${info.CreationDate.getTime()}\n`;
for (let key in info) {
if (!info.hasOwnProperty(key)) {
continue;
}
infoStr += `${key}: ${info[key].toString()}\n`;
}
return wordArrayToBuffer(CryptoJS.MD5(infoStr));
}
static generateRandomWordArray(bytes) {
return CryptoJS.lib.WordArray.random(bytes);
}
static create(document, options = {}) {
if (!options.ownerPassword && !options.userPassword) {
return null;
}
return new PDFSecurity(document, options);
}
constructor(document, options = {}) {
if (!options.ownerPassword && !options.userPassword) {
throw new Error('None of owner password and user password is defined.');
}
this.document = document;
this._setupEncryption(options);
}
_setupEncryption(options) {
switch (options.pdfVersion) {
case '1.4':
case '1.5':
this.version = 2;
break;
case '1.6':
case '1.7':
this.version = 4;
break;
case '1.7ext3':
this.version = 5;
break;
default:
this.version = 1;
break;
}
const encDict = {
Filter: 'Standard'
};
switch (this.version) {
case 1:
case 2:
case 4:
this._setupEncryptionV1V2V4(this.version, encDict, options);
break;
case 5:
this._setupEncryptionV5(encDict, options);
break;
}
this.dictionary = this.document.ref(encDict);
}
_setupEncryptionV1V2V4(v, encDict, options) {
let r, permissions;
switch (v) {
case 1:
r = 2;
this.keyBits = 40;
permissions = getPermissionsR2(options.permissions);
break;
case 2:
r = 3;
this.keyBits = 128;
permissions = getPermissionsR3(options.permissions);
break;
case 4:
r = 4;
this.keyBits = 128;
permissions = getPermissionsR3(options.permissions);
break;
}
const paddedUserPassword = processPasswordR2R3R4(options.userPassword);
const paddedOwnerPassword = options.ownerPassword ?
processPasswordR2R3R4(options.ownerPassword) : paddedUserPassword;
const ownerPasswordEntry = getOwnerPasswordR2R3R4(r, this.keyBits, paddedUserPassword, paddedOwnerPassword);
this.encryptionKey = getEncryptionKeyR2R3R4(r, this.keyBits, this.document._id,
paddedUserPassword, ownerPasswordEntry, permissions);
let userPasswordEntry;
if (r === 2) {
userPasswordEntry = getUserPasswordR2(this.encryptionKey);
} else {
userPasswordEntry = getUserPasswordR3R4(this.document._id, this.encryptionKey);
}
encDict.V = v;
if (v >= 2) {
encDict.Length = this.keyBits;
}
if (v === 4) {
encDict.CF = {
StdCF: {
AuthEvent: 'DocOpen',
CFM: 'AESV2',
Length: this.keyBits / 8
}
};
encDict.StmF = 'StdCF';
encDict.StrF = 'StdCF';
}
encDict.R = r;
encDict.O = wordArrayToBuffer(ownerPasswordEntry);
encDict.U = wordArrayToBuffer(userPasswordEntry);
encDict.P = permissions;
}
_setupEncryptionV5(encDict, options) {
this.keyBits = 256;
const permissions = getPermissionsR3(options);
const processedUserPassword = processPasswordR5(options.userPassword);
const processedOwnerPassword = options.ownerPassword ?
processPasswordR5(options.ownerPassword) : processedUserPassword;
this.encryptionKey = getEncryptionKeyR5(PDFSecurity.generateRandomWordArray);
const userPasswordEntry = getUserPasswordR5(processedUserPassword, PDFSecurity.generateRandomWordArray);
const userKeySalt = CryptoJS.lib.WordArray.create(userPasswordEntry.words.slice(10, 12), 8);
const userEncryptionKeyEntry = getUserEncryptionKeyR5(processedUserPassword, userKeySalt, this.encryptionKey);
const ownerPasswordEntry = getOwnerPasswordR5(processedOwnerPassword, userPasswordEntry,
PDFSecurity.generateRandomWordArray);
const ownerKeySalt = CryptoJS.lib.WordArray.create(ownerPasswordEntry.words.slice(10, 12), 8);
const ownerEncryptionKeyEntry = getOwnerEncryptionKeyR5(processedOwnerPassword, ownerKeySalt, userPasswordEntry,
this.encryptionKey);
const permsEntry = getEncryptedPermissionsR5(permissions, this.encryptionKey, PDFSecurity.generateRandomWordArray);
encDict.V = 5;
encDict.Length = this.keyBits;
encDict.CF = {
StdCF: {
AuthEvent: 'DocOpen',
CFM: 'AESV3',
Length: this.keyBits / 8
}
};
encDict.StmF = 'StdCF';
encDict.StrF = 'StdCF';
encDict.R = 5;
encDict.O = wordArrayToBuffer(ownerPasswordEntry);
encDict.OE = wordArrayToBuffer(ownerEncryptionKeyEntry);
encDict.U = wordArrayToBuffer(userPasswordEntry);
encDict.UE = wordArrayToBuffer(userEncryptionKeyEntry);
encDict.P = permissions;
encDict.Perms = wordArrayToBuffer(permsEntry);
}
getEncryptFn(obj, gen) {
let digest;
if (this.version < 5) {
digest = this.encryptionKey.clone().concat(CryptoJS.lib.WordArray.create([
((obj & 0xff) << 24) | ((obj & 0xff00) << 8) | ((obj >> 8) & 0xff00) | (gen & 0xff), (gen & 0xff00) << 16
], 5));
}
if (this.version === 1 || this.version === 2) {
let key = CryptoJS.MD5(digest);
key.sigBytes = Math.min(16, this.keyBits / 8 + 5);
return buffer => wordArrayToBuffer(
CryptoJS.RC4.encrypt(CryptoJS.lib.WordArray.create(buffer), key).ciphertext);
}
let key;
if (this.version === 4) {
key = CryptoJS.MD5(digest.concat(CryptoJS.lib.WordArray.create([0x73416c54], 4)));
} else {
key = this.encryptionKey;
}
const iv = PDFSecurity.generateRandomWordArray(16);
const options = {
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
iv
};
return buffer => wordArrayToBuffer(
iv.clone().concat(CryptoJS.AES.encrypt(CryptoJS.lib.WordArray.create(buffer), key, options).ciphertext));
}
end() {
this.dictionary.end();
}
}
function getPermissionsR2(permissionObject = {}) {
let permissions = 0xffffffc0 >> 0;
if (permissionObject.printing) {
permissions |= 0b00000000010;
}
if (permissionObject.modifying) {
permissions |= 0b000000001000;
}
if (permissionObject.copying) {
permissions |= 0b000000010000;
}
if (permissionObject.annotating) {
permissions |= 0b000000100000;
}
return permissions;
}
function getPermissionsR3(permissionObject = {}) {
let permissions = 0xfffff0c0 >> 0;
if (permissionObject.printing === 'lowResolution') {
permissions |= 0b000000000100;
}
if (permissionObject.printing === 'highResolution') {
permissions |= 0b100000000100;
}
if (permissionObject.modifying) {
permissions |= 0b000000001000;
}
if (permissionObject.copying) {
permissions |= 0b000000010000;
}
if (permissionObject.annotating) {
permissions |= 0b000000100000;
}
if (permissionObject.fillingForms) {
permissions |= 0b000100000000;
}
if (permissionObject.contentAccessibility) {
permissions |= 0b001000000000;
}
if (permissionObject.documentAssembly) {
permissions |= 0b010000000000;
}
return permissions;
}
function getUserPasswordR2(encryptionKey) {
return CryptoJS.RC4.encrypt(processPasswordR2R3R4(), encryptionKey).ciphertext;
}
function getUserPasswordR3R4(documentId, encryptionKey) {
const key = encryptionKey.clone();
let cipher = CryptoJS.MD5(processPasswordR2R3R4().concat(CryptoJS.lib.WordArray.create(documentId)));
for (let i = 0; i < 20; i++) {
const xorRound = Math.ceil(key.sigBytes / 4);
for (let j = 0; j < xorRound; j++) {
key.words[j] = encryptionKey.words[j] ^ (i | (i << 8) | (i << 16) | (i << 24));
}
cipher = CryptoJS.RC4.encrypt(cipher, key).ciphertext;
}
return cipher.concat(CryptoJS.lib.WordArray.create(null, 16));
}
function getOwnerPasswordR2R3R4(r, keyBits, paddedUserPassword, paddedOwnerPassword) {
let digest = paddedOwnerPassword;
let round = r >= 3 ? 51 : 1;
for (let i = 0; i < round; i++) {
digest = CryptoJS.MD5(digest);
}
const key = digest.clone();
key.sigBytes = keyBits / 8;
let cipher = paddedUserPassword;
round = r >= 3 ? 20 : 1;
for (let i = 0; i < round; i++) {
const xorRound = Math.ceil(key.sigBytes / 4);
for (let j = 0; j < xorRound; j++) {
key.words[j] = digest.words[j] ^ (i | (i << 8) | (i << 16) | (i << 24));
}
cipher = CryptoJS.RC4.encrypt(cipher, key).ciphertext;
}
return cipher;
}
function getEncryptionKeyR2R3R4(r, keyBits, documentId, paddedUserPassword, ownerPasswordEntry, permissions) {
let key = paddedUserPassword.clone()
.concat(ownerPasswordEntry)
.concat(CryptoJS.lib.WordArray.create([lsbFirstWord(permissions)], 4))
.concat(CryptoJS.lib.WordArray.create(documentId));
const round = r >= 3 ? 51 : 1;
for (let i = 0; i < round; i++) {
key = CryptoJS.MD5(key);
key.sigBytes = keyBits / 8;
}
return key;
}
function getUserPasswordR5(processedUserPassword, generateRandomWordArray) {
const validationSalt = generateRandomWordArray(8);
const keySalt = generateRandomWordArray(8);
return CryptoJS.SHA256(processedUserPassword.clone().concat(validationSalt))
.concat(validationSalt).concat(keySalt);
}
function getUserEncryptionKeyR5(processedUserPassword, userKeySalt, encryptionKey) {
const key = CryptoJS.SHA256(processedUserPassword.clone().concat(userKeySalt));
const options = {
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.NoPadding,
iv: CryptoJS.lib.WordArray.create(null, 16)
};
return CryptoJS.AES.encrypt(encryptionKey, key, options).ciphertext;
}
function getOwnerPasswordR5(processedOwnerPassword, userPasswordEntry, generateRandomWordArray) {
const validationSalt = generateRandomWordArray(8);
const keySalt = generateRandomWordArray(8);
return CryptoJS.SHA256(processedOwnerPassword.clone().concat(validationSalt).concat(userPasswordEntry))
.concat(validationSalt).concat(keySalt);
}
function getOwnerEncryptionKeyR5(processedOwnerPassword, ownerKeySalt, userPasswordEntry, encryptionKey) {
const key = CryptoJS.SHA256(processedOwnerPassword.clone().concat(ownerKeySalt).concat(userPasswordEntry));
const options = {
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.NoPadding,
iv: CryptoJS.lib.WordArray.create(null, 16)
};
return CryptoJS.AES.encrypt(encryptionKey, key, options).ciphertext;
}
function getEncryptionKeyR5(generateRandomWordArray) {
return generateRandomWordArray(32);
}
function getEncryptedPermissionsR5(permissions, encryptionKey, generateRandomWordArray) {
const cipher = CryptoJS.lib.WordArray.create([lsbFirstWord(permissions), 0xffffffff, 0x54616462], 12)
.concat(generateRandomWordArray(4));
const options = {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.NoPadding
};
return CryptoJS.AES.encrypt(cipher, encryptionKey, options).ciphertext;
}
function processPasswordR2R3R4(password = '') {
const out = new Buffer(32);
const length = password.length;
let index = 0;
while (index < length && index < 32) {
const code = password.charCodeAt(index);
if (code > 0xff) {
throw new Error('Password contains one or more invalid characters.');
}
out[index] = code;
index++;
}
while (index < 32) {
out[index] = PASSWORD_PADDING[index - length];
index++;
}
return CryptoJS.lib.WordArray.create(out);
}
function processPasswordR5(password = '') {
password = unescape(encodeURIComponent(saslprep(password)));
const length = Math.min(127, password.length);
const out = new Buffer(length);
for (let i = 0; i < length; i++) {
out[i] = password.charCodeAt(i);
}
return CryptoJS.lib.WordArray.create(out);
}
function lsbFirstWord(data) {
return ((data & 0xff) << 24) | ((data & 0xff00) << 8) | ((data >> 8) & 0xff00) | ((data >> 24) & 0xff);
}
function wordArrayToBuffer(wordArray) {
const byteArray = [];
for (let i = 0; i < wordArray.sigBytes; i++) {
byteArray.push((wordArray.words[Math.floor(i / 4)] >> (8 * (3 - i % 4))) & 0xff);
}
return Buffer.from(byteArray);
}
const PASSWORD_PADDING = [
0x28, 0xbf, 0x4e, 0x5e, 0x4e, 0x75, 0x8a, 0x41, 0x64, 0x00, 0x4e, 0x56, 0xff, 0xfa, 0x01, 0x08,
0x2e, 0x2e, 0x00, 0xb6, 0xd0, 0x68, 0x3e, 0x80, 0x2f, 0x0c, 0xa9, 0xfe, 0x64, 0x53, 0x69, 0x7a
];
export default PDFSecurity;