# libs/pdfsecurity.js

    /**
     * @license
     * Licensed under the MIT License.
     * http://opensource.org/licenses/mit-license
     * Author: Owen Leong (@owenl131)
     * Date: 15 Oct 2020
     * References:
     * https://www.cs.cmu.edu/~dst/Adobe/Gallery/anon21jul01-pdf-encryption.txt
     * https://github.com/foliojs/pdfkit/blob/master/lib/security.js
     * http://www.fpdf.org/en/script/script37.php
     */

    import { md5Bin } from "./md5.js";
    import { rc4 } from "./rc4.js";

    var permissionOptions = {
      print: 4,
      modify: 8,
      copy: 16,
      "annot-forms": 32
    };

    /**
     * Initializes encryption settings
     *
     * @name constructor
     * @function
     * @param {Array} permissions Permissions allowed for user, "print", "modify", "copy" and "annot-forms".
     * @param {String} userPassword Permissions apply to this user. Leaving this empty means the document
     *                              is not password protected but viewer has the above permissions.
     * @param {String} ownerPassword Owner has full functionalities to the file.
     * @param {String} fileId As hex string, should be same as the file ID in the trailer.
     * @example
     * var security = new PDFSecurity(["print"])
     */
    function PDFSecurity(permissions, userPassword, ownerPassword, fileId) {
      this.v = 1; // algorithm 1, future work can add in more recent encryption schemes
      this.r = 2; // revision 2

      // set flags for what functionalities the user can access
      let protection = 192;
      permissions.forEach(function(perm) {
        if (typeof permissionOptions.perm !== "undefined") {
          throw new Error("Invalid permission: " + perm);
        }
        protection += permissionOptions[perm];
      });

      // padding is used to pad the passwords to 32 bytes, also is hashed and stored in the final PDF
      this.padding =
        "\x28\xBF\x4E\x5E\x4E\x75\x8A\x41\x64\x00\x4E\x56\xFF\xFA\x01\x08" +
        "\x2E\x2E\x00\xB6\xD0\x68\x3E\x80\x2F\x0C\xA9\xFE\x64\x53\x69\x7A";
      let paddedUserPassword = (userPassword + this.padding).substr(0, 32);
      let paddedOwnerPassword = (ownerPassword + this.padding).substr(0, 32);

      this.O = this.processOwnerPassword(paddedUserPassword, paddedOwnerPassword);
      this.P = -((protection ^ 255) + 1);
      this.encryptionKey = md5Bin(
        paddedUserPassword +
          this.O +
          this.lsbFirstWord(this.P) +
          this.hexToBytes(fileId)
      ).substr(0, 5);
      this.U = rc4(this.encryptionKey, this.padding);
    }

    /**
     * Breaks down a 4-byte number into its individual bytes, with the least significant bit first
     *
     * @name lsbFirstWord
     * @function
     * @param {number} data 32-bit number
     * @returns {Array}
     */
    PDFSecurity.prototype.lsbFirstWord = function(data) {
      return String.fromCharCode(
        (data >> 0) & 0xff,
        (data >> 8) & 0xff,
        (data >> 16) & 0xff,
        (data >> 24) & 0xff
      );
    };

    /**
     * Converts a byte string to a hex string
     *
     * @name toHexString
     * @function
     * @param {String} byteString Byte string
     * @returns {String}
     */
    PDFSecurity.prototype.toHexString = function(byteString) {
      return byteString
        .split("")
        .map(function(byte) {
          return ("0" + (byte.charCodeAt(0) & 0xff).toString(16)).slice(-2);
        })
        .join("");
    };

    /**
     * Converts a hex string to a byte string
     *
     * @name hexToBytes
     * @function
     * @param {String} hex Hex string
     * @returns {String}
     */
    PDFSecurity.prototype.hexToBytes = function(hex) {
      for (var bytes = [], c = 0; c < hex.length; c += 2)
        bytes.push(String.fromCharCode(parseInt(hex.substr(c, 2), 16)));
      return bytes.join("");
    };

    /**
     * Computes the 'O' field in the encryption dictionary
     *
     * @name processOwnerPassword
     * @function
     * @param {String} paddedUserPassword Byte string of padded user password
     * @param {String} paddedOwnerPassword Byte string of padded owner password
     * @returns {String}
     */
    PDFSecurity.prototype.processOwnerPassword = function(
      paddedUserPassword,
      paddedOwnerPassword
    ) {
      let key = md5Bin(paddedOwnerPassword).substr(0, 5);
      return rc4(key, paddedUserPassword);
    };

    /**
     * Returns an encryptor function which can take in a byte string and returns the encrypted version
     *
     * @name encryptor
     * @function
     * @param {number} objectId
     * @param {number} generation Not sure what this is for, you can set it to 0
     * @returns {Function}
     * @example
     * out("stream");
     * encryptor = security.encryptor(object.id, 0);
     * out(encryptor(data));
     * out("endstream");
     */
    PDFSecurity.prototype.encryptor = function(objectId, generation) {
      let key = md5Bin(
        this.encryptionKey +
          String.fromCharCode(
            objectId & 0xff,
            (objectId >> 8) & 0xff,
            (objectId >> 16) & 0xff,
            generation & 0xff,
            (generation >> 8) & 0xff
          )
      ).substr(0, 10);
      return function(data) {
        return rc4(key, data);
      };
    };

    export { PDFSecurity };