libs/pdfsecurity.js

  1. /**
  2. * @license
  3. * Licensed under the MIT License.
  4. * http://opensource.org/licenses/mit-license
  5. * Author: Owen Leong (@owenl131)
  6. * Date: 15 Oct 2020
  7. * References:
  8. * https://www.cs.cmu.edu/~dst/Adobe/Gallery/anon21jul01-pdf-encryption.txt
  9. * https://github.com/foliojs/pdfkit/blob/master/lib/security.js
  10. * http://www.fpdf.org/en/script/script37.php
  11. */
  12. import { md5Bin } from "./md5.js";
  13. import { rc4 } from "./rc4.js";
  14. var permissionOptions = {
  15. print: 4,
  16. modify: 8,
  17. copy: 16,
  18. "annot-forms": 32
  19. };
  20. /**
  21. * Initializes encryption settings
  22. *
  23. * @name constructor
  24. * @function
  25. * @param {Array} permissions Permissions allowed for user, "print", "modify", "copy" and "annot-forms".
  26. * @param {String} userPassword Permissions apply to this user. Leaving this empty means the document
  27. * is not password protected but viewer has the above permissions.
  28. * @param {String} ownerPassword Owner has full functionalities to the file.
  29. * @param {String} fileId As hex string, should be same as the file ID in the trailer.
  30. * @example
  31. * var security = new PDFSecurity(["print"])
  32. */
  33. function PDFSecurity(permissions, userPassword, ownerPassword, fileId) {
  34. this.v = 1; // algorithm 1, future work can add in more recent encryption schemes
  35. this.r = 2; // revision 2
  36. // set flags for what functionalities the user can access
  37. let protection = 192;
  38. permissions.forEach(function(perm) {
  39. if (typeof permissionOptions.perm !== "undefined") {
  40. throw new Error("Invalid permission: " + perm);
  41. }
  42. protection += permissionOptions[perm];
  43. });
  44. // padding is used to pad the passwords to 32 bytes, also is hashed and stored in the final PDF
  45. this.padding =
  46. "\x28\xBF\x4E\x5E\x4E\x75\x8A\x41\x64\x00\x4E\x56\xFF\xFA\x01\x08" +
  47. "\x2E\x2E\x00\xB6\xD0\x68\x3E\x80\x2F\x0C\xA9\xFE\x64\x53\x69\x7A";
  48. let paddedUserPassword = (userPassword + this.padding).substr(0, 32);
  49. let paddedOwnerPassword = (ownerPassword + this.padding).substr(0, 32);
  50. this.O = this.processOwnerPassword(paddedUserPassword, paddedOwnerPassword);
  51. this.P = -((protection ^ 255) + 1);
  52. this.encryptionKey = md5Bin(
  53. paddedUserPassword +
  54. this.O +
  55. this.lsbFirstWord(this.P) +
  56. this.hexToBytes(fileId)
  57. ).substr(0, 5);
  58. this.U = rc4(this.encryptionKey, this.padding);
  59. }
  60. /**
  61. * Breaks down a 4-byte number into its individual bytes, with the least significant bit first
  62. *
  63. * @name lsbFirstWord
  64. * @function
  65. * @param {number} data 32-bit number
  66. * @returns {Array}
  67. */
  68. PDFSecurity.prototype.lsbFirstWord = function(data) {
  69. return String.fromCharCode(
  70. (data >> 0) & 0xff,
  71. (data >> 8) & 0xff,
  72. (data >> 16) & 0xff,
  73. (data >> 24) & 0xff
  74. );
  75. };
  76. /**
  77. * Converts a byte string to a hex string
  78. *
  79. * @name toHexString
  80. * @function
  81. * @param {String} byteString Byte string
  82. * @returns {String}
  83. */
  84. PDFSecurity.prototype.toHexString = function(byteString) {
  85. return byteString
  86. .split("")
  87. .map(function(byte) {
  88. return ("0" + (byte.charCodeAt(0) & 0xff).toString(16)).slice(-2);
  89. })
  90. .join("");
  91. };
  92. /**
  93. * Converts a hex string to a byte string
  94. *
  95. * @name hexToBytes
  96. * @function
  97. * @param {String} hex Hex string
  98. * @returns {String}
  99. */
  100. PDFSecurity.prototype.hexToBytes = function(hex) {
  101. for (var bytes = [], c = 0; c < hex.length; c += 2)
  102. bytes.push(String.fromCharCode(parseInt(hex.substr(c, 2), 16)));
  103. return bytes.join("");
  104. };
  105. /**
  106. * Computes the 'O' field in the encryption dictionary
  107. *
  108. * @name processOwnerPassword
  109. * @function
  110. * @param {String} paddedUserPassword Byte string of padded user password
  111. * @param {String} paddedOwnerPassword Byte string of padded owner password
  112. * @returns {String}
  113. */
  114. PDFSecurity.prototype.processOwnerPassword = function(
  115. paddedUserPassword,
  116. paddedOwnerPassword
  117. ) {
  118. let key = md5Bin(paddedOwnerPassword).substr(0, 5);
  119. return rc4(key, paddedUserPassword);
  120. };
  121. /**
  122. * Returns an encryptor function which can take in a byte string and returns the encrypted version
  123. *
  124. * @name encryptor
  125. * @function
  126. * @param {number} objectId
  127. * @param {number} generation Not sure what this is for, you can set it to 0
  128. * @returns {Function}
  129. * @example
  130. * out("stream");
  131. * encryptor = security.encryptor(object.id, 0);
  132. * out(encryptor(data));
  133. * out("endstream");
  134. */
  135. PDFSecurity.prototype.encryptor = function(objectId, generation) {
  136. let key = md5Bin(
  137. this.encryptionKey +
  138. String.fromCharCode(
  139. objectId & 0xff,
  140. (objectId >> 8) & 0xff,
  141. (objectId >> 16) & 0xff,
  142. generation & 0xff,
  143. (generation >> 8) & 0xff
  144. )
  145. ).substr(0, 10);
  146. return function(data) {
  147. return rc4(key, data);
  148. };
  149. };
  150. export { PDFSecurity };