From 8c2a614a08da7be9fb47d75ed07316d15d95b830 Mon Sep 17 00:00:00 2001 From: zboris12 Date: Sun, 2 Oct 2022 21:45:06 +0900 Subject: [PATCH] Added functions of setting protection to a pdf. --- README.md | 81 ++++- closure/zb-externs.js | 55 +++ zgapdfcryptor.js | 812 ++++++++++++++++++++++++++++++++++++++++++ zgapdfsigner.js | 210 +++++++---- 4 files changed, 1084 insertions(+), 74 deletions(-) create mode 100644 zgapdfcryptor.js diff --git a/README.md b/README.md index b42e36d..449e684 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # ZgaPdfSigner -A javascript tool to sign a pdf in web browser. +A javascript tool to sign a pdf or set protection of a pdf in web browser. And it also can be used in Google Apps Script. PS: __ZGA__ is the abbreviation of my father's name. @@ -156,6 +156,85 @@ fld.createFile(Utilities.newBlob(u8arr, "application/pdf").setName("signed_test. * __text__: string :point_right: (Optional) A text drawing on signature, __not implemented yet__ * __fontData__: PDFLib.StandardFonts|Array|Uint8Array|ArrayBuffer|string :point_right: (Optional) The font's data for drawing text, __not implemented yet__ +## Let's protect the pdf + +Set protection to the pdf. + +```js +/** + * @param {ArrayBuffer} pdf + * @param {string} upwd + * @param {string} opwd + * @return {Promise} + */ +async function protect1(pdf, upwd, opwd){ + /** @type {EncryptOption} */ + var eopt = { + mode: Zga.Crypto.Mode.AES_256, + permissions: ["modify", "annot-forms", "fill-forms", "extract", "assemble"], + userpwd: upwd, + ownerpwd: opwd, + }; + var cyptor = new Zga.PdfCryptor(eopt); + var pdfdoc = await cyptor.encryptPdf(pdf); + u8arr = await pdfdoc.save({"useObjectStreams": false}); + return new Blob([u8arr], {"type" : "application/pdf"}); +} +``` + +Sign and set protection. + +```js +/** + * @param {ArrayBuffer} pdf + * @param {ArrayBuffer} cert + * @param {string} pwd + * @param {string} opwd + * @return {Promise} + */ +async function sign1(pdf, cert, pwd, opwd){ + /** @type {SignOption} */ + var sopt = { + p12cert: cert, + pwd: pwd, + }; + /** @type {EncryptOption} */ + var eopt = { + mode: Zga.Crypto.Mode.RC4_128, + permissions: ["modify", "annot-forms", "fill-forms", "extract", "assemble"], + ownerpwd: opwd, + }; + var signer = new Zga.PdfSigner(sopt); + var u8arr = await signer.sign(pdf, eopt); + return new Blob([u8arr], {"type" : "application/pdf"}); +} +``` + +## Detail of EncryptOption + +* __mode__: Zga.Crypto.Mode :point_right: The values of Zga.Crypto.Mode + * RC4_40: 40bit RC4 Encryption + * RC4_128: 128bit RC4 Encryption + * AES_128: 128bit AES Encryption + * AES_256: 256bit AES Encryption +* __permissions__: Array :point_right: (Optional) The set of permissions you want to block + * "print": Print the document; + * "modify": Modify the contents of the document by operations other than those controlled by 'fill-forms', 'extract' and 'assemble'; + * "copy": Copy or otherwise extract text and graphics from the document; + * "annot-forms": Add or modify text annotations, fill in interactive form fields, and, if 'modify' is also set, create or modify interactive form fields (including signature fields); + * "fill-forms": Fill in existing interactive form fields (including signature fields), even if 'annot-forms' is not specified; + * "extract": Extract text and graphics (in support of accessibility to users with disabilities or for other purposes); + * "assemble": Assemble the document (insert, rotate, or delete pages and create bookmarks or thumbnail images), even if 'modify' is not set; + * "print-high": Print the document to a representation from which a faithful digital copy of the PDF content could be generated. When this is not set, printing is limited to a low-level representation of the appearance, possibly of degraded quality. + * "owner": (inverted logic - only for public-key) when set permits change of encryption and enables all other permissions. +* __userpwd__: string :point_right: (Optional) User password. Used when opening the pdf. +* __ownerpwd__: string :point_right: (Optional) Owner password. If not specified, a random value is used. +* __pubkeys__: Array<_PubKeyInfo_> :point_right: (Optional) Array of recipients containing public-key certificates ('c') and permissions ('p'). Not supported yet. + * __c__: string :point_right: A public-key certificate + * __p__: Array :point_right: (Optional) Permissions + +## Thanks +* The module of setting protection was migrated from [TCPDF](http://www.tcpdf.org). ## License diff --git a/closure/zb-externs.js b/closure/zb-externs.js index 2c3cbcb..87f6907 100644 --- a/closure/zb-externs.js +++ b/closure/zb-externs.js @@ -46,3 +46,58 @@ var SignDrawInfo; * }} */ var SignOption; + +/** + * @typedef + * {{ + * c: string, + * p: (Array|undefined), + * }} + */ +var PubKeyInfo; +/** + * permissions: The set of permissions (specify the ones you want to block): + * print : Print the document; + * modify : Modify the contents of the document by operations other than those controlled by 'fill-forms', 'extract' and 'assemble'; + * copy : Copy or otherwise extract text and graphics from the document; + * annot-forms : Add or modify text annotations, fill in interactive form fields, and, if 'modify' is also set, create or modify interactive form fields (including signature fields); + * fill-forms : Fill in existing interactive form fields (including signature fields), even if 'annot-forms' is not specified; + * extract : Extract text and graphics (in support of accessibility to users with disabilities or for other purposes); + * assemble : Assemble the document (insert, rotate, or delete pages and create bookmarks or thumbnail images), even if 'modify' is not set; + * print-high : Print the document to a representation from which a faithful digital copy of the PDF content could be generated. When this is not set, printing is limited to a low-level representation of the appearance, possibly of degraded quality. + * owner : (inverted logic - only for public-key) when set permits change of encryption and enables all other permissions. + * + * ownerpwd: Owner password If not specified, a random value is used. + * + * pubkeys: Array of recipients containing public-key certificates ('c') and permissions ('p'). + * + * @typedef + * {{ + * mode: Zga.Crypto.Mode, + * permissions: (Array|undefined), + * userpwd: (string|undefined), + * ownerpwd: (string|undefined), + * pubkeys: (Array|undefined), + * }} + */ +var EncryptOption; +/** + * @typedef + * {{ + * CFM: string, + * Length: (number|undefined), + * EncryptMetadata: (boolean|undefined), + * AuthEvent: (string|undefined), + * }} + */ +var CFType; +/** + * enckey: Last RC4 key encrypted. + * enckeyc: Last RC4 computed key. + * @typedef + * {{ + * enckey: string, + * enckeyc: Array, + * }} + */ +var RC4LastInfo; diff --git a/zgapdfcryptor.js b/zgapdfcryptor.js new file mode 100644 index 0000000..f1eee6b --- /dev/null +++ b/zgapdfcryptor.js @@ -0,0 +1,812 @@ +'use strict'; + +// This module was migrated from [TCPDF](http://www.tcpdf.org) +/** + * @param {Object} z + */ +function supplyZgaCryptor(z){ + +/** + * @param {PDFLib.PDFDocument|Array|Uint8Array|ArrayBuffer|string} pdf + * @return {Promise} + */ +z.loadPdf = async function(pdf){ + /** @type {PDFLib.PDFDocument} */ + var pdfdoc = null; + if(pdf.addPage){ + pdfdoc = pdf; + }else if(Array.isArray(pdf)){ + pdfdoc = await PDFLib.PDFDocument.load(new Uint8Array(pdf)); + }else{ + pdfdoc = await PDFLib.PDFDocument.load(pdf); + } + return pdfdoc; +}; + +/** + * @param {Uint8Array} uarr + * @return {string} + */ +z.u8arrToRaw = function(uarr){ + /** @type {Array} */ + var arr = []; + for(var i=0; i '\r' + * @param {string} s string to escape. + * @return {string} escaped string. + */ + _escape: function(s){ + if(!s){ + return s; + } + const CHARS = "\\()".split(""); + var arr = []; + for(var i=0; i= 0){ + arr.push("\\"); + } + arr.push(c); + } + } + return arr.join(""); + }, + + /** + * Returns a string containing random data to be used as a seed for encryption methods. + * @param {string=} seed starting seed value + * @return {string} containing random data + */ + getRandomSeed: function(seed){ + var ret = forge.random.getBytesSync(256); + if(seed){ + ret += seed; + } + ret += new Date().getTime(); + return ret; + }, + + /** + * Returns the input text encrypted using RC4 algorithm and the specified key. + * RC4 is the standard encryption algorithm used in PDF format + * @param {string} key Encryption key. + * @param {string} txt Input text to be encrypted. + * @param {RC4LastInfo} lastinf Last RC4 information. + * @return {string} encrypted text + */ + _RC4: function(key, txt, lastinf){ + /** @type {Array} */ + var rc4 = null; + var i = 0; + var j = 0; + var t = 0; + if(lastinf.enckey != key){ + var k = key.repeat(256 / key.length + 1); + rc4 = new Array(256); + // Initialize rc4 + for(i=0; i=} permissions the set of permissions (specify the ones you want to block). + * @param {Zga.Crypto.Mode=} mode + * @return {number} + */ + getUserPermissionCode: function(permissions, mode){ + var protection = 2147422012; // 32 bit: (01111111 11111111 00001111 00111100) + if(permissions){ + permissions.forEach(function(a_itm){ + var a_p = z.Crypto.Permission[a_itm]; + if(a_p){ + if(mode > 0 || a_p <= 32){ + // set only valid permissions + if(a_p == 2){ + // the logic for bit 2 is inverted (cleared by default) + protection += a_p; + }else{ + protection -= a_p; + } + } + } + }); + } + return protection; + }, + + /** + * Convert encryption P value to a string of bytes, low-order byte first. + * @param {number} protection 32bit encryption permission value (P value) + * @return {string} + */ + getEncPermissionsString: function(protection){ + var buff = new forge.util.ByteStringBuffer(); + buff.putInt32Le(protection); + return buff.getBytes(); + }, + + /** + * Encrypts a string using MD5 and returns it's value as a binary string. + * @param {string} str input string + * @return {string} MD5 encrypted binary string + */ + _md5_16: function(str){ + var md = forge.md.md5.create(); + md.update(str); + return md.digest().getBytes(); + }, + + /** + * Returns the input text encrypted using AES algorithm and the specified key. + * Text is padded to 16bytes blocks + * @param {string} key encryption key + * @param {string} txt input text to be encrypted + * @return {string} encrypted text + */ + _AES: function(key, txt){ + // padding (RFC 2898, PKCS #5: Password-Based Cryptography Specification Version 2.0) + /** @type {string} */ + var padding = 16 - (txt.length % 16); + var buff = forge.util.createBuffer(txt); + buff.fillWithByte(padding, padding); + + var iv = forge.random.getBytesSync(16); + var key2 = forge.util.createBuffer(key); + var cipher = forge.cipher.createCipher("AES-CBC", key2); + cipher.start({iv: iv}); + cipher.update(buff); + cipher.finish(); + return iv + cipher.output.truncate(16).getBytes(); + }, + + /** + * Returns the input text encrypted using AES algorithm and the specified key. + * Text is not padded + * @param {string} key encryption key + * @param {string} txt input text to be encrypted + * @return {string} encrypted text + */ + _AESnopad: function(key, txt) { + var buff = forge.util.createBuffer(txt); + var iv = String.fromCharCode(0).repeat(16); + var key2 = forge.util.createBuffer(key); + var cipher = forge.cipher.createCipher("AES-CBC", key2); + cipher.start({iv: iv}); + cipher.update(buff); + cipher.finish(); + return cipher.output.truncate(16).getBytes(); + }, + +}; + +z.PdfCryptor = class{ + /** + * @constructor + * @param {EncryptOption} encopt + */ + constructor(encopt){ + /** @private @type {string} */ + this.fileid = ""; + /** @private @type {string} */ + this.key = null; + /** @private @type {Array} */ + this.pubkeys = encopt.pubkeys; + /** @private @type {number} */ + this.mode = encopt.mode; + /** @private @type {number} */ + this.protection = 0; + /** @private @type {string} */ + this.userpwd = ""; + /** @private @type {string} */ + this.ownerpwd = ""; + + /** @private @type {string} */ + this.Filter = "Standard"; + /** @private @type {string} */ + this.StmF = "StdCF"; + /** @private @type {string} */ + this.StrF = "StdCF"; + + /** @private @type {number} */ + this.V = 1; + /** @private @type {number} */ + this.Length = 0; + /** @private @type {CFType} */ + this.CF = null; + /** @private @type {string} */ + this.SubFilter = ""; + /** @private @type {Array} */ + this.Recipients = null; + + /** @private @type {string} */// User Validation Salt + this.UVS = ""; + /** @private @type {string} */// User Key Salt + this.UKS = ""; + /** @private @type {string} */// U value + this.U = ""; + /** @private @type {string} */// UE value + this.UE = ""; + + /** @private @type {string} */// Owner Validation Salt + this.OVS = ""; + /** @private @type {string} */// Owner Key Salt + this.OKS = ""; + /** @private @type {string} */// O value + this.O = ""; + /** @private @type {string} */// OE value + this.OE = ""; + /** @private @type {number} */// P value + this.P = 0; + /** @private @type {string} */ + this.perms = ""; + + /** @private @type {RC4LastInfo} */ + this.rc4inf = { + enckey: "", + enckeyc: null, + }; + + if(this.pubkeys){ + throw new Error("Public key mode is not supported yet."); + if(this.mode == z.Crypto.Mode.RC4_40){ + // public-Key Security requires at least 128 bit + this.mode = z.Crypto.Mode.RC4_128; + } + this.Filter = "Adobe.PubSec"; + this.StmF = "DefaultCryptFilter"; + this.StrF = "DefaultCryptFilter"; + } + + if(encopt.userpwd){ + this.userpwd = encopt.userpwd; + } + if(encopt.ownerpwd){ + this.ownerpwd = encopt.ownerpwd; + }else{ + var md = forge.md.md5.create(); + md.update(z.Crypto.getRandomSeed()); + this.ownerpwd = md.digest().toHex(); + } + + switch(this.mode){ + case z.Crypto.Mode.RC4_40: + this.V = 1; + this.Length = 40; + this.CF = {CFM: "V2"}; + break; + case z.Crypto.Mode.RC4_128: + this.V = 2; + this.Length = 128; + this.CF = {CFM: "V2"}; + if(this.pubkeys){ + this.SubFilter = "adbe.pkcs7.s4"; + this.Recipients = []; + } + break; + case z.Crypto.Mode.AES_128: + this.V = 4; + this.Length = 128; + this.CF = {CFM: "AESV2", Length: 128}; + if(this.pubkeys){ + this.SubFilter = "adbe.pkcs7.s5"; + this.Recipients = []; + } + break; + case z.Crypto.Mode.AES_256: + this.V = 5; + this.Length = 256; + this.CF = {CFM: "AESV3", Length: 256}; + if(this.pubkeys){ + this.SubFilter = "adbe.pkcs7.s5"; + this.Recipients = []; + } + break; + } + + this.protection = z.Crypto.getUserPermissionCode(encopt.permissions, this.mode); + } + + /** + * @public + * @param {PDFLib.PDFDocument|Array|Uint8Array|ArrayBuffer|string} pdf + * @param {boolean=} reload + * @return {Promise} + * + * If the parameter of pdf is PDFLib.PDFDocument, and some embedded contents have been added to it, + * then the parameter of reload needs to be true. Because before the encryption, all changes must be applied. + * And if reload is true, the return value is a new pdf document, else is pdf itself. + */ + async encryptPdf(pdf, reload){ + /** @type {PDFLib.PDFDocument} */ + var pdfdoc = await z.loadPdf(pdf); + if(pdfdoc === pdf && reload){ + // Temporaryly save the pdf and reload it to apply all changes. + /** @type {Uint8Array} */ + var newpdf = await pdfdoc.save({"useObjectStreams": false}); + pdfdoc = await PDFLib.PDFDocument.load(newpdf); + } + + /** @type {PDFLib.PDFContext} */ + var pdfcont = pdfdoc.context; + /** @type {PDFLib.PDFObject} */ + var trobj = this.prepareEncrypt(pdfcont); + + /** + * @param {number} a_num + * @param {*} a_val + */ + var func = function(a_num, a_val){ + if(a_val instanceof PDFLib.PDFStream){ + if(a_val.contents){ + a_val.contents = this.encryptU8arr(a_num, a_val.contents); + } + }else if(a_val instanceof PDFLib.PDFHexString){ + if(a_val.value){ + a_val.value = this.encryptHexstr(a_num, a_val.value); + } + }else if(a_val instanceof PDFLib.PDFString){ + if(a_val.value){ + a_val.value = z.Crypto._escape(this._encrypt_data(a_num, a_val.value)); + } + } + if(a_val.dict instanceof Map){ + /** @type {Iterator} */ + var a_es = a_val.dict.entries(); + /** @type {IteratorResult} */ + var a_res = a_es.next(); + while(!a_res.done){ + func(a_num, a_res.value[1]); + a_res = a_es.next(); + } + } + }.bind(this); + pdfcont.enumerateIndirectObjects().forEach(function(a_arr){ + func(a_arr[0].objectNumber, a_arr[1]); + }); + + pdfcont.trailerInfo.Encrypt = pdfcont.register(trobj); + + return pdfdoc; + } + + /** + * Prepare for encryption and create the object for saving in trailer. + * + * @private + * @param {PDFLib.PDFContext} pdfcont + * @return {PDFLib.PDFObject} + */ + prepareEncrypt(pdfcont){ + if(!pdfcont.trailerInfo.ID){ + var md = forge.md.md5.create(); + md.update(z.Crypto.getRandomSeed()); + var res = md.digest(); + var idhex = res.toHex(); + this.fileid = res.getBytes(); + + var trIds = new PDFLib.PDFArray(pdfcont); + trIds.push(PDFLib.PDFHexString.of(idhex)); + trIds.push(PDFLib.PDFHexString.of(idhex)); + pdfcont.trailerInfo.ID = trIds; + + }else{ + this.fileid = forge.util.hexToBytes(pdfcont.trailerInfo.ID.get(0).value); + } + this._generateencryptionkey(); + + var obj = {}; + obj.Filter = this.Filter; + if(this.SubFilter){ + obj.SubFilter = this.SubFilter; + } + // V is a code specifying the algorithm to be used in encrypting and decrypting the document + obj.V = this.V; + // The length of the encryption key, in bits. The value shall be a multiple of 8, in the range 40 to 256 + obj.Length = this.Length; + if(this.V >= 4){ + // A dictionary whose keys shall be crypt filter names and whose values shall be the corresponding crypt filter dictionaries. + if(this.CF){ + var objStmF = { + Type: "CryptFilter", + }; + // The method used + if(this.CF.CFM){ + objStmF.CFM = this.CF.CFM; + if(this.pubkeys){ + /** @type {PDFLib.PDFArray} */ + var recps = new PDFLib.PDFArray(pdfcont); + this.Recipients.forEach(function(a_ele){ + recps.push(PDFLib.PDFHexString.of(a_ele)); + }); + objStmF.Recipients = recps; + if(typeof this.CF.EncryptMetadata == "boolean" && !this.CF.EncryptMetadata){ + objStmF.EncryptMetadata = false; + }else{ + objStmF.EncryptMetadata = true; + } + } + }else{ + objStmF.CFM = "None"; + } + // The event to be used to trigger the authorization that is required to access encryption keys used by this filter. + if(this.CF.AuthEvent){ + objStmF.AuthEvent = this.CF.AuthEvent; + }else{ + objStmF.AuthEvent = "DocOpen"; + } + // The bit length of the encryption key. + if(this.CF.Length){ + objStmF.Length = this.CF.Length; + } + + var objCF = { + [this.StmF]: pdfcont.obj(objStmF), + }; + obj.CF = pdfcont.obj(objCF); + } + // The name of the crypt filter that shall be used by default when decrypting streams. + obj.StmF = this.StmF; + // The name of the crypt filter that shall be used when decrypting all strings in the document. + obj.StrF = this.StrF; + } + // Additional encryption dictionary entries for the standard security handler + if(this.pubkeys){ + if(this.V < 4 && this.Recipients && this.Recipients.length > 0){ + /** @type {PDFLib.PDFArray} */ + var recps = new PDFLib.PDFArray(pdfcont); + this.Recipients.forEach(function(a_ele){ + recps.push(PDFLib.PDFHexString.of(a_ele)); + }); + obj.Recipients = recps; + } + }else{ + if(this.V == 5){ // AES-256 + obj.R = 5; + obj.OE = PDFLib.PDFString.of(z.Crypto._escape(this.OE)); + obj.UE = PDFLib.PDFString.of(z.Crypto._escape(this.UE)); + obj.Perms = PDFLib.PDFString.of(z.Crypto._escape(this.perms)); + }else if(this.V == 4){ // AES-128 + obj.R = 4; + }else if(this.V < 2){ // RC-40 + obj.R = 2; + }else{ // RC-128 + obj.R = 3; + } + obj.O = PDFLib.PDFString.of(z.Crypto._escape(this.O)); + obj.U = PDFLib.PDFString.of(z.Crypto._escape(this.U)); + obj.P = this.P; + if(typeof this.EncryptMetadata == "boolean" && !this.EncryptMetadata){ + obj.EncryptMetadata = false; + }else{ + obj.EncryptMetadata = true; + } + } + return pdfcont.obj(obj); + } + + /** + * @private + * @param {number} num + * @param {Uint8Array} dat + * @return {Uint8Array} + */ + encryptU8arr(num, dat){ + var str = z.u8arrToRaw(dat); + var enc = this._encrypt_data(num, str); + return z.rawToU8arr(enc); + } + + /** + * @private + * @param {number} num + * @param {string} dat + * @return {string} + */ + encryptHexstr(num, dat){ + var str = forge.util.hexToBytes(dat); + var enc = this._encrypt_data(num, str); + return forge.util.createBuffer(enc).toHex(); + } + + /** + * Compute encryption key depending on object number where the encrypted data is stored. + * This is used for all strings and streams without crypt filter specifier. + * + * @private + * @param {number} n object number + * @return {string} object key + */ + _objectkey(n){ + var buff = forge.util.createBuffer(this.key); + //pack('VXxx', $n) + buff.putInt24Le(n); + buff.putBytes(String.fromCharCode(0) + String.fromCharCode(0)); + if (this.mode == z.Crypto.Mode.AES_128) { + // AES padding + buff.putBytes("sAlT"); + } + + var md = forge.md.md5.create(); + md.update(buff.getBytes()); + var ret = md.digest(); + return ret.getBytes().substr(0, Math.min(16, (this.Length / 8) + 5)); + } + + /** + * Encrypt the input string. + * + * @private + * @param {number} n object number + * @param {string} s data string to encrypt + * @return {string} encrypted string + */ + _encrypt_data(n, s){ + switch(this.mode){ + case z.Crypto.Mode.RC4_40: + case z.Crypto.Mode.RC4_128: + s = z.Crypto._RC4(this._objectkey(n), s, this.rc4inf); + break; + case z.Crypto.Mode.AES_128: + s = z.Crypto._AES(this._objectkey(n), s); + break; + case z.Crypto.Mode.AES_256: + s = z.Crypto._AES(this.key, s); + break; + } + return s; + } + + /** + * Compute U value (used for encryption) + * @private + * @return {string} U value + */ + _Uvalue(){ + if(this.mode == z.Crypto.Mode.RC4_40){ + return z.Crypto._RC4(this.key, z.Crypto.EncPadding, this.rc4inf); + }else if(this.mode < z.Crypto.Mode.AES_256) { // RC4-128, AES-128 + var tmp = z.Crypto._md5_16(z.Crypto.EncPadding + this.fileid); + var enc = z.Crypto._RC4(this.key, tmp, this.rc4inf); + var len = tmp.length; + for(var i=1; i<=19; i++){ + var ek = ""; + for(var j=0; j z.Crypto.Mode.RC4_40){ + for(var i=0; i<50; i++){ + tmp = z.Crypto._md5_16(tmp); + } + } + var owner_key = tmp.substr(0, this.Length / 8); + var enc = z.Crypto._RC4(owner_key, this.userpwd, this.rc4inf); + if(this.mode > z.Crypto.Mode.RC4_40){ + var len = owner_key.length; + for(var i=1; i<=19; i++){ + var ek = ""; + for(var j=0; j z.Crypto.Mode.RC4_40) { + for(var i=0; i<50; i++){ + tmp = z.Crypto._md5_16(tmp.substr(0, keybytelen)); + } + } + this.key = tmp.substr(0, keybytelen); + // Compute U value + this.U = this._Uvalue(); + // Compute P value + this.P = this.protection; + } + }else{ // Public-Key mode + //TODO + } + } +}; + +} + +if(!globalThis.Zga){ + globalThis.Zga = {}; +} +supplyZgaCryptor(globalThis.Zga); diff --git a/zgapdfsigner.js b/zgapdfsigner.js index 203b757..d039552 100644 --- a/zgapdfsigner.js +++ b/zgapdfsigner.js @@ -1,28 +1,37 @@ 'use strict'; -globalThis.Zga = { +/** + * @param {Object} z + */ +function supplyZgaSigner(z){ /** @type {Object} */ -TSAURLS: { - "1": {url: "http://ts.ssl.com", len: 15100}, - "2": {url: "http://timestamp.digicert.com", len: 14900}, - "3": {url: "http://timestamp.sectigo.com", len: 12900}, - "4": {url: "http://timestamp.entrust.net/TSS/RFC3161sha2TS", len: 13900}, - "5": {url: "http://timestamp.apple.com/ts01", len: 11600}, - "6": {url: "http://www.langedge.jp/tsa", len: 8700}, - "7": {url: "https://freetsa.org/tsr", len: 14000}, -}, +z.TSAURLS = { + "1": {url: "http://ts.ssl.com", len: 15600}, + "2": {url: "http://timestamp.digicert.com", len: 15400}, + "3": {url: "http://timestamp.sectigo.com", len: 13400}, + "4": {url: "http://timestamp.entrust.net/TSS/RFC3161sha2TS", len: 14400}, + "5": {url: "http://timestamp.apple.com/ts01", len: 12100}, + "6": {url: "http://www.langedge.jp/tsa", len: 9200}, + "7": {url: "https://freetsa.org/tsr", len: 14500}, +}; -PdfSigner: class { +z.PdfSigner = class{ /** * @constructor * @param {SignOption} signopt */ constructor(signopt){ + /** @private @const {string} */ + this.DEFAULT_BYTE_RANGE_PLACEHOLDER = "**********"; /** @private @type {SignOption} */ this.opt = signopt; /** @private @type {TsaServiceInfo} */ this.tsainf = null; + /** @private @type {number} */ + this.siglen = 0; + /** @private @type {PDFLib.PDFHexString} */ + this.sigContents = null; /** @private @type {boolean} */ this.debug = false; @@ -45,8 +54,8 @@ PdfSigner: class { if(!globalThis.UrlFetchApp){ throw new Error("Because of the CORS security restrictions, signing with TSA is not supported in web browser."); } - if(Zga.TSAURLS[this.tsainf.url]){ - Object.assign(this.tsainf, Zga.TSAURLS[this.tsainf.url]); + if(z.TSAURLS[this.tsainf.url]){ + Object.assign(this.tsainf, z.TSAURLS[this.tsainf.url]); }else if(!(new RegExp("^https?://")).test(this.tsainf.url)){ throw new Error("Unknown tsa data. " + JSON.stringify(this.tsainf)); } @@ -64,19 +73,17 @@ PdfSigner: class { /** * @public * @param {PDFLib.PDFDocument|Array|Uint8Array|ArrayBuffer|string} pdf + * @param {EncryptOption=} cypopt * @return {Promise} */ - async sign(pdf){ - /** @type {PDFLib.PDFDocument} */ - var pdfdoc = null; - if(pdf.addPage){ - pdfdoc = pdf; - }else if(Array.isArray(pdf)){ - pdfdoc = await PDFLib.PDFDocument.load(new Uint8Array(pdf)); - }else{ - pdfdoc = await PDFLib.PDFDocument.load(pdf); + async sign(pdf, cypopt){ + if(cypopt && !z.PdfCryptor){ + throw new Error("ZgaPdfCryptor is not imported."); } + /** @type {PDFLib.PDFDocument} */ + var pdfdoc = await z.loadPdf(pdf); + if(this.opt.drawinf && this.opt.drawinf.imgData && !this.opt.drawinf.img){ /** @type {Uint8Array|ArrayBuffer|string} */ var imgData2 = null; @@ -95,29 +102,56 @@ PdfSigner: class { } this.addSignHolder(pdfdoc); - var uarr = await pdfdoc.save({"useObjectStreams": false}); - var pdfstr = Zga.u8arrToRaw(uarr); - this.log("A signature holder has been added to the pdf."); + if(cypopt){ + /** @type {Zga.PdfCryptor} */ + var cypt = new z.PdfCryptor(cypopt); + pdfdoc = await cypt.encryptPdf(pdfdoc, true); + // Because pdfdoc has been changed, so this.sigContents need to be found again. + this.sigContents = null; + this.log("Pdf data has been encrypted."); + } + /** @type {Uint8Array} */ - var ret = this.signPdf(pdfstr); - this.log("Signing pdf accomplished."); + var ret = await this.saveAndSign(pdfdoc); + if(!ret){ + this.log("Change size of signature's placeholder and retry."); + if(!this.sigContents){ + this.sigContents = this.findSigContents(pdfdoc); + } + this.sigContents.value = "0".repeat(this.siglen); + ret = await this.saveAndSign(pdfdoc); + } + if(ret){ + this.log("Signing pdf accomplished."); + }else{ + throw new Error("Failed to sign the pdf."); + } + return ret; } + /** + * @private + * @param {PDFLib.PDFDocument} pdfdoc + * @return {Promise} + */ + async saveAndSign(pdfdoc){ + /** @type {Uint8Array} */ + var uarr = await pdfdoc.save({"useObjectStreams": false}); + /** @type {string} */ + var pdfstr = z.u8arrToRaw(uarr); + return this.signPdf(pdfstr); + } + /** * @private * @param {PDFLib.PDFDocument} pdfdoc */ addSignHolder(pdfdoc){ - /** @const {string} */ - const DEFAULT_BYTE_RANGE_PLACEHOLDER = "**********"; - /** @const {number} */ - const SIGNATURE_LENGTH = this.tsainf ? this.tsainf.len : 3322; - /** @const {VisualSignature} */ - const visign = new Zga.VisualSignature(this.opt.drawinf); + const visign = new z.VisualSignature(this.opt.drawinf); /** @const {PDFLib.PDFRef} */ const strmRef = visign.createStream(pdfdoc, this.opt.signame); /** @const {PDFLib.PDFPage} */ @@ -132,9 +166,12 @@ PdfSigner: class { /** @type {PDFLib.PDFArray} */ var bytrng = new PDFLib.PDFArray(pdfdoc.context); bytrng.push(PDFLib.PDFNumber.of(0)); - bytrng.push(PDFLib.PDFName.of(DEFAULT_BYTE_RANGE_PLACEHOLDER)); - bytrng.push(PDFLib.PDFName.of(DEFAULT_BYTE_RANGE_PLACEHOLDER)); - bytrng.push(PDFLib.PDFName.of(DEFAULT_BYTE_RANGE_PLACEHOLDER)); + bytrng.push(PDFLib.PDFName.of(this.DEFAULT_BYTE_RANGE_PLACEHOLDER)); + bytrng.push(PDFLib.PDFName.of(this.DEFAULT_BYTE_RANGE_PLACEHOLDER)); + bytrng.push(PDFLib.PDFName.of(this.DEFAULT_BYTE_RANGE_PLACEHOLDER)); + + this.siglen = this.tsainf ? this.tsainf.len : 3322; + this.sigContents = PDFLib.PDFHexString.of("0".repeat(this.siglen)); /** @type {Object} */ var signObj = { @@ -142,7 +179,7 @@ PdfSigner: class { "Filter": "Adobe.PPKLite", "SubFilter": "adbe.pkcs7.detached", "ByteRange": bytrng, - "Contents": PDFLib.PDFHexString.of("0".repeat(SIGNATURE_LENGTH)), + "Contents": this.sigContents, "M": PDFLib.PDFString.fromDate(signdate), "Prop_Build": pdfdoc.context.obj({ "App": pdfdoc.context.obj({ @@ -192,6 +229,51 @@ PdfSigner: class { ); } + /** + * @private + * @param {PDFLib.PDFDocument} pdfdoc + * @return {PDFLib.PDFHexString} + */ + findSigContents(pdfdoc){ + /** @type {boolean} */ + var istgt = false; + /** @type {PDFLib.PDFHexString} */ + var sigContents = null; + /** @type {Array<*>} */ + var objarr = pdfdoc.context.enumerateIndirectObjects(); + for(var i=objarr.length - 1; i>= 0; i--){ + if(objarr[i][1].dict instanceof Map){ + /** @type {Iterator} */ + var es = objarr[i][1].dict.entries(); + /** @type {IteratorResult} */ + var res = es.next(); + istgt = false; + sigContents = null; + while(!res.done){ + if(res.value[0].encodedName == "/ByteRange"){ + if(res.value[1].array && + res.value[1].array.length == 4 && + res.value[1].array[0].numberValue == 0 && + res.value[1].array[1].encodedName == "/" + this.DEFAULT_BYTE_RANGE_PLACEHOLDER && + res.value[1].array[2].encodedName == res.value[1].array[1].encodedName && + res.value[1].array[3].encodedName == res.value[1].array[1].encodedName){ + istgt = true; + } + }else if(res.value[0].encodedName == "/Contents"){ + if(res.value[1] instanceof PDFLib.PDFHexString){ + sigContents = res.value[1]; + } + } + if(istgt && sigContents){ + return sigContents; + }else{ + res = es.next(); + } + } + } + } + } + /** * @private * @param {string} pdfstr @@ -207,8 +289,8 @@ PdfSigner: class { // Finds ByteRange information within a given PDF Buffer if one exists var byteRangeStrings = pdfstr.match(/\/ByteRange\s*\[{1}\s*(?:(?:\d*|\/\*{10})\s+){3}(?:\d+|\/\*{10}){1}\s*]{1}/g); var byteRangePlaceholder = byteRangeStrings.find(function(a_str){ - return a_str.includes("/**********"); - }); + return a_str.includes("/"+this.DEFAULT_BYTE_RANGE_PLACEHOLDER); + }.bind(this)); if(!byteRangePlaceholder){ throw new Error("no signature placeholder"); } @@ -231,7 +313,7 @@ PdfSigner: class { pdfstr = pdfstr.slice(0, byteRange[1]) + pdfstr.slice(byteRange[2], byteRange[2] + byteRange[3]); if(typeof this.opt.p12cert !== "string"){ - this.opt.p12cert = Zga.u8arrToRaw(new Uint8Array(this.opt.p12cert)); + this.opt.p12cert = z.u8arrToRaw(new Uint8Array(this.opt.p12cert)); } // Convert Buffer P12 to a forge implementation. var p12Asn1 = forge.asn1.fromDer(this.opt.p12cert); @@ -323,7 +405,9 @@ PdfSigner: class { // checking the actual lengths. this.log("Size of signature is " + sighex.length + "/" + placeholderLength); if(sighex.length > placeholderLength){ - throw new Error("Signature is too big. Needs: " + sighex.length); + // throw new Error("Signature is too big. Needs: " + sighex.length); + this.siglen = sighex.length; + return null; }else{ // Pad the signature with zeroes so the it is the same length as the placeholder sighex += "0".repeat(placeholderLength - sighex.length); @@ -331,7 +415,7 @@ PdfSigner: class { // Place it in the document. pdfstr = pdfstr.slice(0, byteRange[1]) + "<" + sighex + ">" + pdfstr.slice(byteRange[1]); - return Zga.rawToU8arr(pdfstr); + return z.rawToU8arr(pdfstr); } /** @@ -432,14 +516,14 @@ PdfSigner: class { */ queryTsa(signature){ var tsr = this.genTsrData(signature); - var tu8s = Zga.rawToU8arr(tsr); + var tu8s = z.rawToU8arr(tsr); var options = { "method": "POST", "headers": {"Content-Type": "application/timestamp-query"}, "payload": tu8s, }; var tblob = UrlFetchApp.fetch(this.tsainf.url, options).getBlob(); - var tstr = Zga.u8arrToRaw(new Uint8Array(tblob.getBytes())); + var tstr = z.u8arrToRaw(new Uint8Array(tblob.getBytes())); var token = forge.asn1.fromDer(tstr).value[1]; return token; } @@ -453,9 +537,9 @@ PdfSigner: class { console.log(msg); } } -}, +}; -VisualSignature: class { +z.VisualSignature = class{ /** * @constructor * @param {SignDrawInfo=} drawinf @@ -667,31 +751,11 @@ VisualSignature: class { } return ret; } -}, - -/** - * @param {Uint8Array} uarr - * @return {string} - */ -u8arrToRaw: function(uarr){ - /** @type {Array} */ - var arr = []; - for(var i=0; i