From 0304b65fcda7e96cd433cf19f7a2fb8bc64ff375 Mon Sep 17 00:00:00 2001
From: Jonas Jenwald <jonas.jenwald@gmail.com>
Date: Sat, 16 Sep 2023 16:34:24 +0200
Subject: [PATCH] Remove the closure from the `CipherTransformFactory` class

Now that modern JavaScript is fully supported also in the worker-thread we no longer need to keep old closures, which slightly reduces the size of the code.
---
 src/core/crypto.js | 413 ++++++++++++++++++++++-----------------------
 1 file changed, 200 insertions(+), 213 deletions(-)

diff --git a/src/core/crypto.js b/src/core/crypto.js
index 26c22d80b..ee2625c3a 100644
--- a/src/core/crypto.js
+++ b/src/core/crypto.js
@@ -1418,14 +1418,14 @@ class CipherTransform {
   }
 }
 
-const CipherTransformFactory = (function CipherTransformFactoryClosure() {
-  const defaultPasswordBytes = new Uint8Array([
+class CipherTransformFactory {
+  static #defaultPasswordBytes = new Uint8Array([
     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,
   ]);
 
-  function createEncryptionKey20(
+  #createEncryptionKey20(
     revision,
     password,
     ownerPassword,
@@ -1471,7 +1471,7 @@ const CipherTransformFactory = (function CipherTransformFactoryClosure() {
     return null;
   }
 
-  function prepareKeyData(
+  #prepareKeyData(
     fileId,
     password,
     ownerPassword,
@@ -1494,7 +1494,7 @@ const CipherTransformFactory = (function CipherTransformFactoryClosure() {
     }
     j = 0;
     while (i < 32) {
-      hashData[i++] = defaultPasswordBytes[j++];
+      hashData[i++] = CipherTransformFactory.#defaultPasswordBytes[j++];
     }
     // as now the padded password in the hashData[0..i]
     for (j = 0, n = ownerPassword.length; j < n; ++j) {
@@ -1525,7 +1525,7 @@ const CipherTransformFactory = (function CipherTransformFactoryClosure() {
 
     if (revision >= 3) {
       for (i = 0; i < 32; ++i) {
-        hashData[i] = defaultPasswordBytes[i];
+        hashData[i] = CipherTransformFactory.#defaultPasswordBytes[i];
       }
       for (j = 0, n = fileId.length; j < n; ++j) {
         hashData[i++] = fileId[j];
@@ -1548,7 +1548,9 @@ const CipherTransformFactory = (function CipherTransformFactoryClosure() {
       }
     } else {
       cipher = new ARCFourCipher(encryptionKey);
-      checkData = cipher.encryptBlock(defaultPasswordBytes);
+      checkData = cipher.encryptBlock(
+        CipherTransformFactory.#defaultPasswordBytes
+      );
       for (j = 0, n = checkData.length; j < n; ++j) {
         if (userPassword[j] !== checkData[j]) {
           return null;
@@ -1558,7 +1560,7 @@ const CipherTransformFactory = (function CipherTransformFactoryClosure() {
     return encryptionKey;
   }
 
-  function decodeUserPassword(password, ownerPassword, revision, keyLength) {
+  #decodeUserPassword(password, ownerPassword, revision, keyLength) {
     const hashData = new Uint8Array(32);
     let i = 0;
     const n = Math.min(32, password.length);
@@ -1567,7 +1569,7 @@ const CipherTransformFactory = (function CipherTransformFactoryClosure() {
     }
     let j = 0;
     while (i < 32) {
-      hashData[i++] = defaultPasswordBytes[j++];
+      hashData[i++] = CipherTransformFactory.#defaultPasswordBytes[j++];
     }
     let hash = calculateMD5(hashData, 0, i);
     const keyLengthInBytes = keyLength >> 3;
@@ -1595,9 +1597,7 @@ const CipherTransformFactory = (function CipherTransformFactoryClosure() {
     return userPassword;
   }
 
-  const identityName = Name.get("Identity");
-
-  function buildObjectKey(num, gen, encryptionKey, isAes = false) {
+  #buildObjectKey(num, gen, encryptionKey, isAes = false) {
     const key = new Uint8Array(encryptionKey.length + 9);
     const n = encryptionKey.length;
     let i;
@@ -1619,242 +1619,229 @@ const CipherTransformFactory = (function CipherTransformFactoryClosure() {
     return hash.subarray(0, Math.min(encryptionKey.length + 5, 16));
   }
 
-  function buildCipherConstructor(cf, name, num, gen, key) {
+  #buildCipherConstructor(cf, name, num, gen, key) {
     if (!(name instanceof Name)) {
       throw new FormatError("Invalid crypt filter name.");
     }
+    const self = this;
     const cryptFilter = cf.get(name.name);
-    let cfm;
-    if (cryptFilter !== null && cryptFilter !== undefined) {
-      cfm = cryptFilter.get("CFM");
-    }
+    const cfm = cryptFilter?.get("CFM");
+
     if (!cfm || cfm.name === "None") {
-      return function cipherTransformFactoryBuildCipherConstructorNone() {
+      return function () {
         return new NullCipher();
       };
     }
     if (cfm.name === "V2") {
-      return function cipherTransformFactoryBuildCipherConstructorV2() {
+      return function () {
         return new ARCFourCipher(
-          buildObjectKey(num, gen, key, /* isAes = */ false)
+          self.#buildObjectKey(num, gen, key, /* isAes = */ false)
         );
       };
     }
     if (cfm.name === "AESV2") {
-      return function cipherTransformFactoryBuildCipherConstructorAESV2() {
+      return function () {
         return new AES128Cipher(
-          buildObjectKey(num, gen, key, /* isAes = */ true)
+          self.#buildObjectKey(num, gen, key, /* isAes = */ true)
         );
       };
     }
     if (cfm.name === "AESV3") {
-      return function cipherTransformFactoryBuildCipherConstructorAESV3() {
+      return function () {
         return new AES256Cipher(key);
       };
     }
     throw new FormatError("Unknown crypto method");
   }
 
-  // eslint-disable-next-line no-shadow
-  class CipherTransformFactory {
-    constructor(dict, fileId, password) {
-      const filter = dict.get("Filter");
-      if (!isName(filter, "Standard")) {
-        throw new FormatError("unknown encryption method");
-      }
-      this.filterName = filter.name;
-      this.dict = dict;
-      const 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;
-      let 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.
-          const cfDict = dict.get("CF");
-          const streamCryptoName = dict.get("StmF");
-          if (cfDict instanceof Dict && streamCryptoName instanceof Name) {
-            cfDict.suppressEncryption = true; // See comment below.
-            const handlerDict = cfDict.get(streamCryptoName.name);
-            keyLength = 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");
-      }
-
-      const ownerBytes = stringToBytes(dict.get("O")),
-        userBytes = stringToBytes(dict.get("U"));
-      // prepare keys
-      const ownerPassword = ownerBytes.subarray(0, 32);
-      const userPassword = userBytes.subarray(0, 32);
-      const flags = dict.get("P");
-      const revision = dict.get("R");
-      // meaningful when V is 4 or 5
-      const encryptMetadata =
-        (algorithm === 4 || algorithm === 5) &&
-        dict.get("EncryptMetadata") !== false;
-      this.encryptMetadata = encryptMetadata;
-
-      const fileIdBytes = stringToBytes(fileId);
-      let passwordBytes;
-      if (password) {
-        if (revision === 6) {
-          try {
-            password = utf8StringToString(password);
-          } catch {
-            warn(
-              "CipherTransformFactory: Unable to convert UTF8 encoded password."
-            );
-          }
-        }
-        passwordBytes = stringToBytes(password);
-      }
-
-      let encryptionKey;
-      if (algorithm !== 5) {
-        encryptionKey = prepareKeyData(
-          fileIdBytes,
-          passwordBytes,
-          ownerPassword,
-          userPassword,
-          flags,
-          revision,
-          keyLength,
-          encryptMetadata
-        );
+  constructor(dict, fileId, password) {
+    const filter = dict.get("Filter");
+    if (!isName(filter, "Standard")) {
+      throw new FormatError("unknown encryption method");
+    }
+    this.filterName = filter.name;
+    this.dict = dict;
+    const 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;
+    let 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 {
-        const ownerValidationSalt = ownerBytes.subarray(32, 40);
-        const ownerKeySalt = ownerBytes.subarray(40, 48);
-        const uBytes = userBytes.subarray(0, 48);
-        const userValidationSalt = userBytes.subarray(32, 40);
-        const userKeySalt = userBytes.subarray(40, 48);
-        const ownerEncryption = stringToBytes(dict.get("OE"));
-        const userEncryption = stringToBytes(dict.get("UE"));
-        const 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
-        const 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) {
-        const cf = dict.get("CF");
-        if (cf instanceof Dict) {
-          // 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;
+        // Trying to find default handler -- it usually has Length.
+        const cfDict = dict.get("CF");
+        const streamCryptoName = dict.get("StmF");
+        if (cfDict instanceof Dict && streamCryptoName instanceof Name) {
+          cfDict.suppressEncryption = true; // See comment below.
+          const handlerDict = cfDict.get(streamCryptoName.name);
+          keyLength = handlerDict?.get("Length") || 128;
+          if (keyLength < 40) {
+            // Sometimes it's incorrect value of bits, generators specify
+            // bytes.
+            keyLength <<= 3;
+          }
         }
-        this.cf = cf;
-        this.stmf = dict.get("StmF") || identityName;
-        this.strf = dict.get("StrF") || identityName;
-        this.eff = dict.get("EFF") || this.stmf;
       }
     }
+    if (!Number.isInteger(keyLength) || keyLength < 40 || keyLength % 8 !== 0) {
+      throw new FormatError("invalid key length");
+    }
 
-    createCipherTransform(num, gen) {
-      if (this.algorithm === 4 || this.algorithm === 5) {
-        return new CipherTransform(
-          buildCipherConstructor(
-            this.cf,
-            this.strf,
-            num,
-            gen,
-            this.encryptionKey
-          ),
-          buildCipherConstructor(
-            this.cf,
-            this.stmf,
-            num,
-            gen,
-            this.encryptionKey
-          )
-        );
+    const ownerBytes = stringToBytes(dict.get("O")),
+      userBytes = stringToBytes(dict.get("U"));
+    // prepare keys
+    const ownerPassword = ownerBytes.subarray(0, 32);
+    const userPassword = userBytes.subarray(0, 32);
+    const flags = dict.get("P");
+    const revision = dict.get("R");
+    // meaningful when V is 4 or 5
+    const encryptMetadata =
+      (algorithm === 4 || algorithm === 5) &&
+      dict.get("EncryptMetadata") !== false;
+    this.encryptMetadata = encryptMetadata;
+
+    const fileIdBytes = stringToBytes(fileId);
+    let passwordBytes;
+    if (password) {
+      if (revision === 6) {
+        try {
+          password = utf8StringToString(password);
+        } catch {
+          warn(
+            "CipherTransformFactory: Unable to convert UTF8 encoded password."
+          );
+        }
       }
-      // algorithms 1 and 2
-      const key = buildObjectKey(
-        num,
-        gen,
-        this.encryptionKey,
-        /* isAes = */ false
+      passwordBytes = stringToBytes(password);
+    }
+
+    let encryptionKey;
+    if (algorithm !== 5) {
+      encryptionKey = this.#prepareKeyData(
+        fileIdBytes,
+        passwordBytes,
+        ownerPassword,
+        userPassword,
+        flags,
+        revision,
+        keyLength,
+        encryptMetadata
       );
-      const cipherConstructor = function buildCipherCipherConstructor() {
-        return new ARCFourCipher(key);
-      };
-      return new CipherTransform(cipherConstructor, cipherConstructor);
+    } else {
+      const ownerValidationSalt = ownerBytes.subarray(32, 40);
+      const ownerKeySalt = ownerBytes.subarray(40, 48);
+      const uBytes = userBytes.subarray(0, 48);
+      const userValidationSalt = userBytes.subarray(32, 40);
+      const userKeySalt = userBytes.subarray(40, 48);
+      const ownerEncryption = stringToBytes(dict.get("OE"));
+      const userEncryption = stringToBytes(dict.get("UE"));
+      const perms = stringToBytes(dict.get("Perms"));
+      encryptionKey = this.#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
+      const decodedPassword = this.#decodeUserPassword(
+        passwordBytes,
+        ownerPassword,
+        revision,
+        keyLength
+      );
+      encryptionKey = this.#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) {
+      const cf = dict.get("CF");
+      if (cf instanceof Dict) {
+        // 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") || Name.get("Identity");
+      this.strf = dict.get("StrF") || Name.get("Identity");
+      this.eff = dict.get("EFF") || this.stmf;
     }
   }
 
-  return CipherTransformFactory;
-})();
+  createCipherTransform(num, gen) {
+    if (this.algorithm === 4 || this.algorithm === 5) {
+      return new CipherTransform(
+        this.#buildCipherConstructor(
+          this.cf,
+          this.strf,
+          num,
+          gen,
+          this.encryptionKey
+        ),
+        this.#buildCipherConstructor(
+          this.cf,
+          this.stmf,
+          num,
+          gen,
+          this.encryptionKey
+        )
+      );
+    }
+    // algorithms 1 and 2
+    const key = this.#buildObjectKey(
+      num,
+      gen,
+      this.encryptionKey,
+      /* isAes = */ false
+    );
+    const cipherConstructor = function () {
+      return new ARCFourCipher(key);
+    };
+    return new CipherTransform(cipherConstructor, cipherConstructor);
+  }
+}
 
 export {
   AES128Cipher,