From 6b4c4f80e40c1ff2651905ff0b74cf1e9f496d12 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Fri, 26 Feb 2021 15:51:45 +0100 Subject: [PATCH] Convert code in `src/core/crypto.js` to use "normal" classes All of this code predates the existence of native JS classes, however we can now clean this up a bit. This patch thus let us remove some variable "shadowing" from the code. --- src/core/crypto.js | 842 ++++++++++++++++++++++----------------------- 1 file changed, 407 insertions(+), 435 deletions(-) diff --git a/src/core/crypto.js b/src/core/crypto.js index 62439e27c..752e5e79d 100644 --- a/src/core/crypto.js +++ b/src/core/crypto.js @@ -28,9 +28,8 @@ import { import { isDict, isName, Name } from "./primitives.js"; import { DecryptStream } from "./stream.js"; -var ARCFourCipher = (function ARCFourCipherClosure() { - // eslint-disable-next-line no-shadow - function ARCFourCipher(key) { +class ARCFourCipher { + constructor(key) { this.a = 0; this.b = 0; var s = new Uint8Array(256); @@ -50,35 +49,37 @@ var ARCFourCipher = (function ARCFourCipherClosure() { this.s = s; } - ARCFourCipher.prototype = { - encryptBlock: function ARCFourCipher_encryptBlock(data) { - var i, - n = data.length, - tmp, - tmp2; - var a = this.a, - b = this.b, - s = this.s; - var output = new Uint8Array(n); - for (i = 0; i < n; ++i) { - a = (a + 1) & 0xff; - tmp = s[a]; - b = (b + tmp) & 0xff; - tmp2 = s[b]; - s[a] = tmp2; - s[b] = tmp; - output[i] = data[i] ^ s[(tmp + tmp2) & 0xff]; - } - this.a = a; - this.b = b; - return output; - }, - }; - ARCFourCipher.prototype.decryptBlock = ARCFourCipher.prototype.encryptBlock; - ARCFourCipher.prototype.encrypt = ARCFourCipher.prototype.encryptBlock; + encryptBlock(data) { + var i, + n = data.length, + tmp, + tmp2; + var a = this.a, + b = this.b, + s = this.s; + var output = new Uint8Array(n); + for (i = 0; i < n; ++i) { + a = (a + 1) & 0xff; + tmp = s[a]; + b = (b + tmp) & 0xff; + tmp2 = s[b]; + s[a] = tmp2; + s[b] = tmp; + output[i] = data[i] ^ s[(tmp + tmp2) & 0xff]; + } + this.a = a; + this.b = b; + return output; + } - return ARCFourCipher; -})(); + decryptBlock(data) { + return this.encryptBlock(data); + } + + encrypt(data) { + return this.encryptBlock(data); + } +} var calculateMD5 = (function calculateMD5Closure() { // prettier-ignore @@ -180,94 +181,93 @@ var calculateMD5 = (function calculateMD5Closure() { return hash; })(); -var Word64 = (function Word64Closure() { - // eslint-disable-next-line no-shadow - function Word64(highInteger, lowInteger) { + +class Word64 { + constructor(highInteger, lowInteger) { this.high = highInteger | 0; this.low = lowInteger | 0; } - Word64.prototype = { - and: function Word64_and(word) { - this.high &= word.high; - this.low &= word.low; - }, - xor: function Word64_xor(word) { - this.high ^= word.high; - this.low ^= word.low; - }, - or: function Word64_or(word) { - this.high |= word.high; - this.low |= word.low; - }, + and(word) { + this.high &= word.high; + this.low &= word.low; + } - shiftRight: function Word64_shiftRight(places) { - if (places >= 32) { - this.low = (this.high >>> (places - 32)) | 0; - this.high = 0; - } else { - this.low = (this.low >>> places) | (this.high << (32 - places)); - this.high = (this.high >>> places) | 0; - } - }, + xor(word) { + this.high ^= word.high; + this.low ^= word.low; + } - shiftLeft: function Word64_shiftLeft(places) { - if (places >= 32) { - this.high = this.low << (places - 32); - this.low = 0; - } else { - this.high = (this.high << places) | (this.low >>> (32 - places)); - this.low = this.low << places; - } - }, + or(word) { + this.high |= word.high; + this.low |= word.low; + } - rotateRight: function Word64_rotateRight(places) { - var low, high; - if (places & 32) { - high = this.low; - low = this.high; - } else { - low = this.low; - high = this.high; - } - places &= 31; - this.low = (low >>> places) | (high << (32 - places)); - this.high = (high >>> places) | (low << (32 - places)); - }, + shiftRight(places) { + if (places >= 32) { + this.low = (this.high >>> (places - 32)) | 0; + this.high = 0; + } else { + this.low = (this.low >>> places) | (this.high << (32 - places)); + this.high = (this.high >>> places) | 0; + } + } - not: function Word64_not() { - this.high = ~this.high; - this.low = ~this.low; - }, + shiftLeft(places) { + if (places >= 32) { + this.high = this.low << (places - 32); + this.low = 0; + } else { + this.high = (this.high << places) | (this.low >>> (32 - places)); + this.low = this.low << places; + } + } - add: function Word64_add(word) { - var lowAdd = (this.low >>> 0) + (word.low >>> 0); - var highAdd = (this.high >>> 0) + (word.high >>> 0); - if (lowAdd > 0xffffffff) { - highAdd += 1; - } - this.low = lowAdd | 0; - this.high = highAdd | 0; - }, + rotateRight(places) { + var low, high; + if (places & 32) { + high = this.low; + low = this.high; + } else { + low = this.low; + high = this.high; + } + places &= 31; + this.low = (low >>> places) | (high << (32 - places)); + this.high = (high >>> places) | (low << (32 - places)); + } - copyTo: function Word64_copyTo(bytes, offset) { - bytes[offset] = (this.high >>> 24) & 0xff; - bytes[offset + 1] = (this.high >> 16) & 0xff; - bytes[offset + 2] = (this.high >> 8) & 0xff; - bytes[offset + 3] = this.high & 0xff; - bytes[offset + 4] = (this.low >>> 24) & 0xff; - bytes[offset + 5] = (this.low >> 16) & 0xff; - bytes[offset + 6] = (this.low >> 8) & 0xff; - bytes[offset + 7] = this.low & 0xff; - }, + not() { + this.high = ~this.high; + this.low = ~this.low; + } - assign: function Word64_assign(word) { - this.high = word.high; - this.low = word.low; - }, - }; - return Word64; -})(); + add(word) { + var lowAdd = (this.low >>> 0) + (word.low >>> 0); + var highAdd = (this.high >>> 0) + (word.high >>> 0); + if (lowAdd > 0xffffffff) { + highAdd += 1; + } + this.low = lowAdd | 0; + this.high = highAdd | 0; + } + + copyTo(bytes, offset) { + bytes[offset] = (this.high >>> 24) & 0xff; + bytes[offset + 1] = (this.high >> 16) & 0xff; + bytes[offset + 2] = (this.high >> 8) & 0xff; + bytes[offset + 3] = this.high & 0xff; + bytes[offset + 4] = (this.low >>> 24) & 0xff; + bytes[offset + 5] = (this.low >> 16) & 0xff; + bytes[offset + 6] = (this.low >> 8) & 0xff; + bytes[offset + 7] = this.low & 0xff; + } + + assign(word) { + this.high = word.high; + this.low = word.low; + } +} var calculateSHA256 = (function calculateSHA256Closure() { function rotr(x, n) { @@ -521,8 +521,7 @@ var calculateSHA512 = (function calculateSHA512Closure() { new Word64(0x4cc5d4be, 0xcb3e42b6), new Word64(0x597f299c, 0xfc657e2a), new Word64(0x5fcb6fab, 0x3ad6faec), new Word64(0x6c44198c, 0x4a475817)]; - function hash(data, offset, length, mode384) { - mode384 = !!mode384; + function hash(data, offset, length, mode384 = false) { // initial hash values var h0, h1, h2, h3, h4, h5, h6, h7; if (!mode384) { @@ -687,28 +686,20 @@ var calculateSHA512 = (function calculateSHA512Closure() { return hash; })(); -var calculateSHA384 = (function calculateSHA384Closure() { - function hash(data, offset, length) { - return calculateSHA512(data, offset, length, true); + +function calculateSHA384(data, offset, length) { + return calculateSHA512(data, offset, length, /* mode384 = */ true); +} + +class NullCipher { + decryptBlock(data) { + return data; } - return hash; -})(); -var NullCipher = (function NullCipherClosure() { - // eslint-disable-next-line no-shadow - function NullCipher() {} - - NullCipher.prototype = { - decryptBlock: function NullCipher_decryptBlock(data) { - return data; - }, - encrypt: function NullCipher_encrypt(data) { - return data; - }, - }; - - return NullCipher; -})(); + encrypt(data) { + return data; + } +} class AESBaseCipher { constructor() { @@ -1262,65 +1253,44 @@ class AES256Cipher extends AESBaseCipher { } } -var PDF17 = (function PDF17Closure() { - // eslint-disable-next-line no-shadow - function PDF17() {} +class PDF17 { + checkOwnerPassword(password, ownerValidationSalt, userBytes, ownerPassword) { + var hashData = new Uint8Array(password.length + 56); + hashData.set(password, 0); + hashData.set(ownerValidationSalt, password.length); + hashData.set(userBytes, password.length + ownerValidationSalt.length); + var result = calculateSHA256(hashData, 0, hashData.length); + return isArrayEqual(result, ownerPassword); + } - PDF17.prototype = { - checkOwnerPassword: function PDF17_checkOwnerPassword( - password, - ownerValidationSalt, - userBytes, - ownerPassword - ) { - var hashData = new Uint8Array(password.length + 56); - hashData.set(password, 0); - hashData.set(ownerValidationSalt, password.length); - hashData.set(userBytes, password.length + ownerValidationSalt.length); - var result = calculateSHA256(hashData, 0, hashData.length); - return isArrayEqual(result, ownerPassword); - }, - checkUserPassword: function PDF17_checkUserPassword( - password, - userValidationSalt, - userPassword - ) { - var hashData = new Uint8Array(password.length + 8); - hashData.set(password, 0); - hashData.set(userValidationSalt, password.length); - var result = calculateSHA256(hashData, 0, hashData.length); - return isArrayEqual(result, userPassword); - }, - getOwnerKey: function PDF17_getOwnerKey( - password, - ownerKeySalt, - userBytes, - ownerEncryption - ) { - var hashData = new Uint8Array(password.length + 56); - hashData.set(password, 0); - hashData.set(ownerKeySalt, password.length); - hashData.set(userBytes, password.length + ownerKeySalt.length); - var key = calculateSHA256(hashData, 0, hashData.length); - var cipher = new AES256Cipher(key); - return cipher.decryptBlock(ownerEncryption, false, new Uint8Array(16)); - }, - getUserKey: function PDF17_getUserKey( - password, - userKeySalt, - userEncryption - ) { - var hashData = new Uint8Array(password.length + 8); - hashData.set(password, 0); - hashData.set(userKeySalt, password.length); - // `key` is the decryption key for the UE string. - var key = calculateSHA256(hashData, 0, hashData.length); - var cipher = new AES256Cipher(key); - return cipher.decryptBlock(userEncryption, false, new Uint8Array(16)); - }, - }; - return PDF17; -})(); + checkUserPassword(password, userValidationSalt, userPassword) { + var hashData = new Uint8Array(password.length + 8); + hashData.set(password, 0); + hashData.set(userValidationSalt, password.length); + var result = calculateSHA256(hashData, 0, hashData.length); + return isArrayEqual(result, userPassword); + } + + getOwnerKey(password, ownerKeySalt, userBytes, ownerEncryption) { + var hashData = new Uint8Array(password.length + 56); + hashData.set(password, 0); + hashData.set(ownerKeySalt, password.length); + hashData.set(userBytes, password.length + ownerKeySalt.length); + var key = calculateSHA256(hashData, 0, hashData.length); + var cipher = new AES256Cipher(key); + return cipher.decryptBlock(ownerEncryption, false, new Uint8Array(16)); + } + + getUserKey(password, userKeySalt, userEncryption) { + var hashData = new Uint8Array(password.length + 8); + hashData.set(password, 0); + hashData.set(userKeySalt, password.length); + // `key` is the decryption key for the UE string. + var key = calculateSHA256(hashData, 0, hashData.length); + var cipher = new AES256Cipher(key); + return cipher.decryptBlock(userEncryption, false, new Uint8Array(16)); + } +} var PDF20 = (function PDF20Closure() { function calculatePDF20Hash(password, input, userBytes) { @@ -1370,13 +1340,12 @@ var PDF20 = (function PDF20Closure() { } // eslint-disable-next-line no-shadow - function PDF20() {} - - PDF20.prototype = { - hash: function PDF20_hash(password, concatBytes, userBytes) { + class PDF20 { + hash(password, concatBytes, userBytes) { return calculatePDF20Hash(password, concatBytes, userBytes); - }, - checkOwnerPassword: function PDF20_checkOwnerPassword( + } + + checkOwnerPassword( password, ownerValidationSalt, userBytes, @@ -1388,24 +1357,17 @@ var PDF20 = (function PDF20Closure() { hashData.set(userBytes, password.length + ownerValidationSalt.length); var result = calculatePDF20Hash(password, hashData, userBytes); return isArrayEqual(result, ownerPassword); - }, - checkUserPassword: function PDF20_checkUserPassword( - password, - userValidationSalt, - userPassword - ) { + } + + checkUserPassword(password, userValidationSalt, userPassword) { var hashData = new Uint8Array(password.length + 8); hashData.set(password, 0); hashData.set(userValidationSalt, password.length); var result = calculatePDF20Hash(password, hashData, []); return isArrayEqual(result, userPassword); - }, - getOwnerKey: function PDF20_getOwnerKey( - password, - ownerKeySalt, - userBytes, - ownerEncryption - ) { + } + + getOwnerKey(password, ownerKeySalt, userBytes, ownerEncryption) { var hashData = new Uint8Array(password.length + 56); hashData.set(password, 0); hashData.set(ownerKeySalt, password.length); @@ -1413,12 +1375,9 @@ var PDF20 = (function PDF20Closure() { var key = calculatePDF20Hash(password, hashData, userBytes); var cipher = new AES256Cipher(key); return cipher.decryptBlock(ownerEncryption, false, new Uint8Array(16)); - }, - getUserKey: function PDF20_getUserKey( - password, - userKeySalt, - userEncryption - ) { + } + + getUserKey(password, userKeySalt, userEncryption) { var hashData = new Uint8Array(password.length + 8); hashData.set(password, 0); hashData.set(userKeySalt, password.length); @@ -1426,74 +1385,73 @@ var PDF20 = (function PDF20Closure() { var key = calculatePDF20Hash(password, hashData, []); var cipher = new AES256Cipher(key); return cipher.decryptBlock(userEncryption, false, new Uint8Array(16)); - }, - }; + } + } + return PDF20; })(); -var CipherTransform = (function CipherTransformClosure() { - // eslint-disable-next-line no-shadow - function CipherTransform(stringCipherConstructor, streamCipherConstructor) { +class CipherTransform { + constructor(stringCipherConstructor, streamCipherConstructor) { this.StringCipherConstructor = stringCipherConstructor; this.StreamCipherConstructor = streamCipherConstructor; } - CipherTransform.prototype = { - createStream: function CipherTransform_createStream(stream, length) { - var cipher = new this.StreamCipherConstructor(); - return new DecryptStream( - stream, - length, - function cipherTransformDecryptStream(data, finalize) { - return cipher.decryptBlock(data, finalize); + createStream(stream, length) { + var cipher = new this.StreamCipherConstructor(); + return new DecryptStream( + stream, + length, + function cipherTransformDecryptStream(data, finalize) { + return cipher.decryptBlock(data, finalize); + } + ); + } + + decryptString(s) { + var cipher = new this.StringCipherConstructor(); + var data = stringToBytes(s); + data = cipher.decryptBlock(data, true); + return bytesToString(data); + } + + encryptString(s) { + const cipher = new this.StringCipherConstructor(); + if (cipher instanceof AESBaseCipher) { + // Append some chars equal to "16 - (M mod 16)" + // where M is the string length (see section 7.6.2 in PDF specification) + // to have a final string where the length is a multiple of 16. + const strLen = s.length; + const pad = 16 - (strLen % 16); + if (pad !== 16) { + s = s.padEnd(16 * Math.ceil(strLen / 16), String.fromCharCode(pad)); + } + + // Generate an initialization vector + const iv = new Uint8Array(16); + if (typeof crypto !== "undefined") { + crypto.getRandomValues(iv); + } else { + for (let i = 0; i < 16; i++) { + iv[i] = Math.floor(256 * Math.random()); } - ); - }, - decryptString: function CipherTransform_decryptString(s) { - var cipher = new this.StringCipherConstructor(); - var data = stringToBytes(s); - data = cipher.decryptBlock(data, true); - return bytesToString(data); - }, - encryptString: function CipherTransform_encryptString(s) { - const cipher = new this.StringCipherConstructor(); - if (cipher instanceof AESBaseCipher) { - // Append some chars equal to "16 - (M mod 16)" - // where M is the string length (see section 7.6.2 in PDF specification) - // to have a final string where the length is a multiple of 16. - const strLen = s.length; - const pad = 16 - (strLen % 16); - if (pad !== 16) { - s = s.padEnd(16 * Math.ceil(strLen / 16), String.fromCharCode(pad)); - } - - // Generate an initialization vector - const iv = new Uint8Array(16); - if (typeof crypto !== "undefined") { - crypto.getRandomValues(iv); - } else { - for (let i = 0; i < 16; i++) { - iv[i] = Math.floor(256 * Math.random()); - } - } - - let data = stringToBytes(s); - data = cipher.encrypt(data, iv); - - const buf = new Uint8Array(16 + data.length); - buf.set(iv); - buf.set(data, 16); - - return bytesToString(buf); } let data = stringToBytes(s); - data = cipher.encrypt(data); - return bytesToString(data); - }, - }; - return CipherTransform; -})(); + data = cipher.encrypt(data, iv); + + const buf = new Uint8Array(16 + data.length); + buf.set(iv); + buf.set(data, 16); + + return bytesToString(buf); + } + + let data = stringToBytes(s); + data = cipher.encrypt(data); + return bytesToString(data); + } +} var CipherTransformFactory = (function CipherTransformFactoryClosure() { // prettier-ignore @@ -1684,161 +1642,7 @@ var CipherTransformFactory = (function CipherTransformFactoryClosure() { var identityName = Name.get("Identity"); - // eslint-disable-next-line no-shadow - function CipherTransformFactory(dict, fileId, password) { - var filter = dict.get("Filter"); - if (!isName(filter, "Standard")) { - throw new FormatError("unknown encryption method"); - } - this.dict = dict; - var algorithm = dict.get("V"); - if ( - !Number.isInteger(algorithm) || - (algorithm !== 1 && algorithm !== 2 && algorithm !== 4 && algorithm !== 5) - ) { - throw new FormatError("unsupported encryption algorithm"); - } - this.algorithm = algorithm; - var keyLength = dict.get("Length"); - if (!keyLength) { - // Spec asks to rely on encryption dictionary's Length entry, however - // some PDFs don't have it. Trying to recover. - if (algorithm <= 3) { - // For 1 and 2 it's fixed to 40-bit, for 3 40-bit is a minimal value. - keyLength = 40; - } else { - // Trying to find default handler -- it usually has Length. - var cfDict = dict.get("CF"); - var streamCryptoName = dict.get("StmF"); - if (isDict(cfDict) && isName(streamCryptoName)) { - cfDict.suppressEncryption = true; // See comment below. - var handlerDict = cfDict.get(streamCryptoName.name); - keyLength = (handlerDict && handlerDict.get("Length")) || 128; - if (keyLength < 40) { - // Sometimes it's incorrect value of bits, generators specify bytes. - keyLength <<= 3; - } - } - } - } - if (!Number.isInteger(keyLength) || keyLength < 40 || keyLength % 8 !== 0) { - throw new FormatError("invalid key length"); - } - - // prepare keys - var ownerPassword = stringToBytes(dict.get("O")).subarray(0, 32); - var userPassword = stringToBytes(dict.get("U")).subarray(0, 32); - var flags = dict.get("P"); - var revision = dict.get("R"); - // meaningful when V is 4 or 5 - var encryptMetadata = - (algorithm === 4 || algorithm === 5) && - dict.get("EncryptMetadata") !== false; - this.encryptMetadata = encryptMetadata; - - var fileIdBytes = stringToBytes(fileId); - var passwordBytes; - if (password) { - if (revision === 6) { - try { - password = utf8StringToString(password); - } catch (ex) { - warn( - "CipherTransformFactory: " + - "Unable to convert UTF8 encoded password." - ); - } - } - passwordBytes = stringToBytes(password); - } - - var encryptionKey; - if (algorithm !== 5) { - encryptionKey = prepareKeyData( - fileIdBytes, - passwordBytes, - ownerPassword, - userPassword, - flags, - revision, - keyLength, - encryptMetadata - ); - } else { - var ownerValidationSalt = stringToBytes(dict.get("O")).subarray(32, 40); - var ownerKeySalt = stringToBytes(dict.get("O")).subarray(40, 48); - var uBytes = stringToBytes(dict.get("U")).subarray(0, 48); - var userValidationSalt = stringToBytes(dict.get("U")).subarray(32, 40); - var userKeySalt = stringToBytes(dict.get("U")).subarray(40, 48); - var ownerEncryption = stringToBytes(dict.get("OE")); - var userEncryption = stringToBytes(dict.get("UE")); - var perms = stringToBytes(dict.get("Perms")); - encryptionKey = createEncryptionKey20( - revision, - passwordBytes, - ownerPassword, - ownerValidationSalt, - ownerKeySalt, - uBytes, - userPassword, - userValidationSalt, - userKeySalt, - ownerEncryption, - userEncryption, - perms - ); - } - if (!encryptionKey && !password) { - throw new PasswordException( - "No password given", - PasswordResponses.NEED_PASSWORD - ); - } else if (!encryptionKey && password) { - // Attempting use the password as an owner password - var decodedPassword = decodeUserPassword( - passwordBytes, - ownerPassword, - revision, - keyLength - ); - encryptionKey = prepareKeyData( - fileIdBytes, - decodedPassword, - ownerPassword, - userPassword, - flags, - revision, - keyLength, - encryptMetadata - ); - } - - if (!encryptionKey) { - throw new PasswordException( - "Incorrect Password", - PasswordResponses.INCORRECT_PASSWORD - ); - } - - this.encryptionKey = encryptionKey; - - if (algorithm >= 4) { - var cf = dict.get("CF"); - if (isDict(cf)) { - // The 'CF' dictionary itself should not be encrypted, and by setting - // `suppressEncryption` we can prevent an infinite loop inside of - // `XRef_fetchUncompressed` if the dictionary contains indirect objects - // (fixes issue7665.pdf). - cf.suppressEncryption = true; - } - this.cf = cf; - this.stmf = dict.get("StmF") || identityName; - this.strf = dict.get("StrF") || identityName; - this.eff = dict.get("EFF") || this.stmf; - } - } - - function buildObjectKey(num, gen, encryptionKey, isAes) { + function buildObjectKey(num, gen, encryptionKey, isAes = false) { var key = new Uint8Array(encryptionKey.length + 9), i, n; @@ -1876,12 +1680,16 @@ var CipherTransformFactory = (function CipherTransformFactoryClosure() { } if (cfm.name === "V2") { return function cipherTransformFactoryBuildCipherConstructorV2() { - return new ARCFourCipher(buildObjectKey(num, gen, key, false)); + return new ARCFourCipher( + buildObjectKey(num, gen, key, /* isAes = */ false) + ); }; } if (cfm.name === "AESV2") { return function cipherTransformFactoryBuildCipherConstructorAESV2() { - return new AES128Cipher(buildObjectKey(num, gen, key, true)); + return new AES128Cipher( + buildObjectKey(num, gen, key, /* isAes = */ true) + ); }; } if (cfm.name === "AESV3") { @@ -1892,11 +1700,170 @@ var CipherTransformFactory = (function CipherTransformFactoryClosure() { throw new FormatError("Unknown crypto method"); } - CipherTransformFactory.prototype = { - createCipherTransform: function CipherTransformFactory_createCipherTransform( - num, - gen - ) { + // eslint-disable-next-line no-shadow + class CipherTransformFactory { + constructor(dict, fileId, password) { + var filter = dict.get("Filter"); + if (!isName(filter, "Standard")) { + throw new FormatError("unknown encryption method"); + } + this.dict = dict; + var algorithm = dict.get("V"); + if ( + !Number.isInteger(algorithm) || + (algorithm !== 1 && + algorithm !== 2 && + algorithm !== 4 && + algorithm !== 5) + ) { + throw new FormatError("unsupported encryption algorithm"); + } + this.algorithm = algorithm; + var keyLength = dict.get("Length"); + if (!keyLength) { + // Spec asks to rely on encryption dictionary's Length entry, however + // some PDFs don't have it. Trying to recover. + if (algorithm <= 3) { + // For 1 and 2 it's fixed to 40-bit, for 3 40-bit is a minimal value. + keyLength = 40; + } else { + // Trying to find default handler -- it usually has Length. + var cfDict = dict.get("CF"); + var streamCryptoName = dict.get("StmF"); + if (isDict(cfDict) && isName(streamCryptoName)) { + cfDict.suppressEncryption = true; // See comment below. + var handlerDict = cfDict.get(streamCryptoName.name); + keyLength = (handlerDict && handlerDict.get("Length")) || 128; + if (keyLength < 40) { + // Sometimes it's incorrect value of bits, generators specify + // bytes. + keyLength <<= 3; + } + } + } + } + if ( + !Number.isInteger(keyLength) || + keyLength < 40 || + keyLength % 8 !== 0 + ) { + throw new FormatError("invalid key length"); + } + + // prepare keys + var ownerPassword = stringToBytes(dict.get("O")).subarray(0, 32); + var userPassword = stringToBytes(dict.get("U")).subarray(0, 32); + var flags = dict.get("P"); + var revision = dict.get("R"); + // meaningful when V is 4 or 5 + var encryptMetadata = + (algorithm === 4 || algorithm === 5) && + dict.get("EncryptMetadata") !== false; + this.encryptMetadata = encryptMetadata; + + var fileIdBytes = stringToBytes(fileId); + var passwordBytes; + if (password) { + if (revision === 6) { + try { + password = utf8StringToString(password); + } catch (ex) { + warn( + "CipherTransformFactory: " + + "Unable to convert UTF8 encoded password." + ); + } + } + passwordBytes = stringToBytes(password); + } + + var encryptionKey; + if (algorithm !== 5) { + encryptionKey = prepareKeyData( + fileIdBytes, + passwordBytes, + ownerPassword, + userPassword, + flags, + revision, + keyLength, + encryptMetadata + ); + } else { + var ownerValidationSalt = stringToBytes(dict.get("O")).subarray(32, 40); + var ownerKeySalt = stringToBytes(dict.get("O")).subarray(40, 48); + var uBytes = stringToBytes(dict.get("U")).subarray(0, 48); + var userValidationSalt = stringToBytes(dict.get("U")).subarray(32, 40); + var userKeySalt = stringToBytes(dict.get("U")).subarray(40, 48); + var ownerEncryption = stringToBytes(dict.get("OE")); + var userEncryption = stringToBytes(dict.get("UE")); + var perms = stringToBytes(dict.get("Perms")); + encryptionKey = createEncryptionKey20( + revision, + passwordBytes, + ownerPassword, + ownerValidationSalt, + ownerKeySalt, + uBytes, + userPassword, + userValidationSalt, + userKeySalt, + ownerEncryption, + userEncryption, + perms + ); + } + if (!encryptionKey && !password) { + throw new PasswordException( + "No password given", + PasswordResponses.NEED_PASSWORD + ); + } else if (!encryptionKey && password) { + // Attempting use the password as an owner password + var decodedPassword = decodeUserPassword( + passwordBytes, + ownerPassword, + revision, + keyLength + ); + encryptionKey = prepareKeyData( + fileIdBytes, + decodedPassword, + ownerPassword, + userPassword, + flags, + revision, + keyLength, + encryptMetadata + ); + } + + if (!encryptionKey) { + throw new PasswordException( + "Incorrect Password", + PasswordResponses.INCORRECT_PASSWORD + ); + } + + this.encryptionKey = encryptionKey; + + if (algorithm >= 4) { + var cf = dict.get("CF"); + if (isDict(cf)) { + // The 'CF' dictionary itself should not be encrypted, and by setting + // `suppressEncryption` we can prevent an infinite loop inside of + // `XRef_fetchUncompressed` if the dictionary contains indirect + // objects (fixes issue7665.pdf). + cf.suppressEncryption = true; + } + this.cf = cf; + this.stmf = dict.get("StmF") || identityName; + this.strf = dict.get("StrF") || identityName; + this.eff = dict.get("EFF") || this.stmf; + } + } + + createCipherTransform(num, gen) { if (this.algorithm === 4 || this.algorithm === 5) { return new CipherTransform( buildCipherConstructor( @@ -1916,13 +1883,18 @@ var CipherTransformFactory = (function CipherTransformFactoryClosure() { ); } // algorithms 1 and 2 - var key = buildObjectKey(num, gen, this.encryptionKey, false); + var key = buildObjectKey( + num, + gen, + this.encryptionKey, + /* isAes = */ false + ); var cipherConstructor = function buildCipherCipherConstructor() { return new ARCFourCipher(key); }; return new CipherTransform(cipherConstructor, cipherConstructor); - }, - }; + } + } return CipherTransformFactory; })();