Added functions of setting protection to a pdf.

pull/2/head
zboris12 2022-10-02 21:45:06 +09:00
parent 8fbedd2c8a
commit 8c2a614a08
4 changed files with 1084 additions and 74 deletions

View File

@ -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<number>|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<Blob>}
*/
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<Blob>}
*/
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<Zga.Crypto.Permission> :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'). <ins>Not supported yet.</ins>
* __c__: string :point_right: A public-key certificate
* __p__: Array<Zga.Crypto.Permission> :point_right: (Optional) Permissions
## Thanks
* The module of setting protection was migrated from [TCPDF](http://www.tcpdf.org).
## License

View File

@ -46,3 +46,58 @@ var SignDrawInfo;
* }}
*/
var SignOption;
/**
* @typedef
* {{
* c: string,
* p: (Array<Zga.Crypto.Permission>|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<Zga.Crypto.Permission>|undefined),
* userpwd: (string|undefined),
* ownerpwd: (string|undefined),
* pubkeys: (Array<PubKeyInfo>|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<number>,
* }}
*/
var RC4LastInfo;

812
zgapdfcryptor.js Normal file
View File

@ -0,0 +1,812 @@
'use strict';
// This module was migrated from [TCPDF](http://www.tcpdf.org)
/**
* @param {Object<string, *>} z
*/
function supplyZgaCryptor(z){
/**
* @param {PDFLib.PDFDocument|Array<number>|Uint8Array|ArrayBuffer|string} pdf
* @return {Promise<PDFLib.PDFDocument>}
*/
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<string>} */
var arr = [];
for(var i=0; i<uarr.length; i++){
arr.push(String.fromCharCode(uarr[i]));
}
return arr.join("");
};
/**
* @param {string} raw
* @return {Uint8Array}
*/
z.rawToU8arr = function(raw){
var arr = new Uint8Array(raw.length);
for(var i=0; i<raw.length; i++){
arr[i] = raw.charCodeAt(i);
}
return arr;
};
z.Crypto = {
/**
* @enum {number}
*/
Mode: {
RC4_40: 0,
RC4_128: 1,
AES_128: 2,
AES_256: 3,
},
/**
* @enum {number}
*/
Permission: {
"owner": 2, // bit 2 -- inverted logic: cleared by default
"print": 4, // bit 3
"modify": 8, // bit 4
"copy": 16, // bit 5
"annot-forms": 32, // bit 6
"fill-forms": 256, // bit 9
"extract": 512, // bit 10
"assemble": 1024,// bit 11
"print-high": 2048 // bit 12
},
/** @type {string} */
EncPadding: "\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",
/**
* Add "\" before "\", "(" and ")", and chr(13) => '\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<s.length; i++){
var c = s.charAt(i);
if(c == "\r"){
arr.push("\\r");
}else{
if(CHARS.indexOf(c) >= 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<number>} */
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<rc4.length; i++){
rc4[i] = i;
}
for(i=0; i<rc4.length; i++){
t = rc4[i];
j = (j + t + k.charCodeAt(i)) % 256;
rc4[i] = rc4[j];
rc4[j] = t;
}
lastinf.enckey = key;
lastinf.enckeyc = [].concat(rc4);
}else{
rc4 = [].concat(lastinf.enckeyc);
}
var len = txt.length;
var a = 0;
var b = 0;
var out = "";
for(i=0; i<len; i++){
a = (a + 1) % 256;
t = rc4[a];
b = (b + t) % 256;
rc4[a] = rc4[b];
rc4[b] = t;
k = rc4[(rc4[a] + rc4[b]) % 256];
out += String.fromCharCode(txt.charCodeAt(i) ^ k);
}
return out;
},
/**
* Return the permission code used on encryption (P value).
*
* @param {Array<Zga.Crypto.Permission>=} 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<PubKeyInfo>} */
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<string>} */
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<number>|Uint8Array|ArrayBuffer|string} pdf
* @param {boolean=} reload
* @return {Promise<PDFLib.PDFDocument>}
*
* 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<len; j++){
ek += String.fromCharCode(this.key.charCodeAt(j) ^ i);
}
enc = z.Crypto._RC4(ek, enc, this.rc4inf);
}
enc += String.fromCharCode(0).repeat(16);
return enc.substr(0, 32);
}else if(this.mode == z.Crypto.Mode.AES_256){
var seed = z.Crypto._md5_16(z.Crypto.getRandomSeed());
// User Validation Salt
this.UVS = seed.substr(0, 8);
// User Key Salt
this.UKS = seed.substr(8, 16);
var md = forge.md.sha256.create();
md.update(this.userpwd + this.UVS);
return md.digest().getBytes() + this.UVS + this.UKS;
}
}
/**
* Compute UE value (used for encryption)
* @private
* @return {string} UE value
*/
_UEvalue(){
var md = forge.md.sha256.create();
md.update(this.userpwd + this.UKS);
return z.Crypto._AESnopad(md.digest().getBytes(), this.key);
}
/**
* Compute O value (used for encryption)
* @private
* @return {string} O value
*/
_Ovalue(){
if(this.mode < z.Crypto.Mode.AES_256){ // RC4-40, RC4-128, AES-128
var tmp = z.Crypto._md5_16(this.ownerpwd);
if(this.mode > 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<len; j++){
ek += String.fromCharCode(owner_key.charCodeAt(j) ^ i);
}
enc = z.Crypto._RC4(ek, enc, this.rc4inf);
}
}
return enc;
}else if(this.mode == z.Crypto.Mode.AES_256){
var seed = z.Crypto._md5_16(z.Crypto.getRandomSeed());
// Owner Validation Salt
this.OVS = seed.substr(0, 8);
// Owner Key Salt
this.OKS = seed.substr(8, 16);
var md = forge.md.sha256.create();
md.update(this.ownerpwd + this.OVS + this.U);
return md.digest().getBytes() + this.OVS + this.OKS;
}
}
/**
* Compute OE value (used for encryption)
* @private
* @return {string} OE value
*/
_OEvalue(){
var md = forge.md.sha256.create();
md.update(this.ownerpwd + this.OKS + this.U);
return z.Crypto._AESnopad(md.digest().getBytes(), this.key);
}
/**
* Convert password for AES-256 encryption mode
* @private
* @param {string} pwd password
* @return {string} password
*/
_fixAES256Password(pwd) {
return pwd.substr(0, 127);
}
/**
* Compute encryption key
* @private
*/
_generateencryptionkey(){
var keybytelen = this.Length / 8;
// standard mode
if(!this.pubkeys){
if(this.mode == z.Crypto.Mode.AES_256){
// generate 256 bit random key
var md = forge.md.sha256.create();
md.update(z.Crypto.getRandomSeed());
this.key = md.digest().getBytes().substr(0, keybytelen);
// truncate passwords
this.userpwd = this._fixAES256Password(this.userpwd);
this.ownerpwd = this._fixAES256Password(this.ownerpwd);
// Compute U value
this.U = this._Uvalue();
// Compute UE value
this.UE = this._UEvalue();
// Compute O value
this.O = this._Ovalue();
// Compute OE value
this.OE = this._OEvalue();
// Compute P value
this.P = this.protection;
// Computing the encryption dictionary's Perms (permissions) value
var perms = z.Crypto.getEncPermissionsString(this.protection); // bytes 0-3
perms += String.fromCharCode(255).repeat(4); // bytes 4-7
if(typeof this.CF.EncryptMetadata == "boolean" && !this.CF.EncryptMetadata){ // byte 8
perms += "F";
}else{
perms += "T";
}
perms += "adb"; // bytes 9-11
perms += "nick"; // bytes 12-15
this.perms = z.Crypto._AESnopad(this.key, perms);
}else{ // RC4-40, RC4-128, AES-128
// Pad passwords
this.userpwd = (this.userpwd + z.Crypto.EncPadding).substr(0, 32);
this.ownerpwd = (this.ownerpwd + z.Crypto.EncPadding).substr(0, 32);
// Compute O value
this.O = this._Ovalue();
// get default permissions (reverse byte order)
var permissions = z.Crypto.getEncPermissionsString(this.protection);
// Compute encryption key
var tmp = z.Crypto._md5_16(this.userpwd + this.O + permissions + this.fileid);
if(this.mode > 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);

View File

@ -1,28 +1,37 @@
'use strict';
globalThis.Zga = {
/**
* @param {Object<string, *>} z
*/
function supplyZgaSigner(z){
/** @type {Object<string, TsaServiceInfo>} */
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<number>|Uint8Array|ArrayBuffer|string} pdf
* @param {EncryptOption=} cypopt
* @return {Promise<Uint8Array>}
*/
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<Uint8Array>}
*/
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<string, *>} */
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<string>} */
var arr = [];
for(var i=0; i<uarr.length; i++){
arr.push(String.fromCharCode(uarr[i]));
}
return arr.join("");
},
/**
* @param {string} raw
* @return {Uint8Array}
*/
rawToU8arr: function(raw){
var arr = new Uint8Array(raw.length);
for(var i=0; i<raw.length; i++){
arr[i] = raw.charCodeAt(i);
}
return arr;
},
};
}
if(!globalThis.Zga){
globalThis.Zga = {};
}
supplyZgaSigner(globalThis.Zga);