Save signed PDFs (close #2)
This commit is contained in:
parent
f5c56af2d2
commit
d892fe81ed
@ -99,10 +99,6 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form style="display: none; height: 0; width: 0; margin: 0; padding: 0;">
|
|
||||||
<input type="file" id="open-file-input" accept=".pdf"/>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div id="svgtrimbox"></div>
|
<div id="svgtrimbox"></div>
|
||||||
|
|
||||||
|
|
||||||
@ -111,10 +107,13 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|||||||
<script src="node_modules/pdfjs-dist/build/pdf.min.js"></script>
|
<script src="node_modules/pdfjs-dist/build/pdf.min.js"></script>
|
||||||
<script src="node_modules/jspdf/dist/jspdf.umd.min.js"></script>
|
<script src="node_modules/jspdf/dist/jspdf.umd.min.js"></script>
|
||||||
<script src="node_modules/signature_pad/dist/signature_pad.umd.min.js"></script>
|
<script src="node_modules/signature_pad/dist/signature_pad.umd.min.js"></script>
|
||||||
|
<script src="node_modules/jshashes/hashes.min.js"></script>
|
||||||
|
<script src="js/kbpgp-2.1.15.js"></script>
|
||||||
<script src="js/svg-to-image.js"></script>
|
<script src="js/svg-to-image.js"></script>
|
||||||
<script src="js/util.js"></script>
|
<script src="js/util.js"></script>
|
||||||
<script src="js/storage.js"></script>
|
<script src="js/storage.js"></script>
|
||||||
<script src="js/filesystem.js"></script>
|
<script src="js/filesystem.js"></script>
|
||||||
|
<script src="js/crypto.js"></script>
|
||||||
<script src="js/drawtools.js"></script>
|
<script src="js/drawtools.js"></script>
|
||||||
<script src="js/pdf.js"></script>
|
<script src="js/pdf.js"></script>
|
||||||
<script src="js/main.js"></script>
|
<script src="js/main.js"></script>
|
124
js/crypto.js
Normal file
124
js/crypto.js
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 Netsyms Technologies.
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var keymgr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load and unlock the private key in localstorage, prompting user as needed. If there is no key, generates, saves, and loads a new one.
|
||||||
|
* @param {function} callback Passed two arguments: message for user, and boolean true if OK false if error.
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
function loadKeyFromLocalStorage(callback) {
|
||||||
|
if (!inStorage("signingkey") || getStorage("signingkey") == "undefined") {
|
||||||
|
var pass = prompt("Generating a new signing key. Enter a password to protect it. You'll need to save this password somewhere safe; it cannot be recovered.");
|
||||||
|
generatePrivateKey(getStorage("notary_name") + " <null@null.com>", pass, function (key) {
|
||||||
|
if (typeof key == "undefined") {
|
||||||
|
callback("Could not generate key.", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
keymgr = key;
|
||||||
|
setStorage("signingkey", keymgr.armored_pgp_private);
|
||||||
|
callback("Signing key generated.", true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var pass = prompt("Enter password to unlock signing key.");
|
||||||
|
loadPrivateKey(getStorage("signingkey"), pass, function (key) {
|
||||||
|
if (typeof key == "undefined") {
|
||||||
|
callback("Could not unlock key. Password is probably incorrect.", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
keymgr = key;
|
||||||
|
callback("Signing key unlocked.", true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a private key.
|
||||||
|
* @param {string} armoredkey PGP private key
|
||||||
|
* @param {string} pass key password
|
||||||
|
* @param {function} callback Passed a new keymanager for the key.
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
function loadPrivateKey(armoredkey, pass, callback) {
|
||||||
|
kbpgp.KeyManager.import_from_armored_pgp({
|
||||||
|
armored: armoredkey
|
||||||
|
}, function (err, key) {
|
||||||
|
if (!err) {
|
||||||
|
if (key.is_pgp_locked()) {
|
||||||
|
key.unlock_pgp({
|
||||||
|
passphrase: pass
|
||||||
|
}, function (err) {
|
||||||
|
if (!err) {
|
||||||
|
console.log("Loaded private key with passphrase");
|
||||||
|
callback(key);
|
||||||
|
} else {
|
||||||
|
console.error(err);
|
||||||
|
callback(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("Loaded private key w/o passphrase");
|
||||||
|
callback(key);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(err);
|
||||||
|
callback(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign a message with a key and return the signed message in the callback.
|
||||||
|
* @param {type} text
|
||||||
|
* @param {type} key
|
||||||
|
* @param {type} callback
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
function signMessage(text, key, callback) {
|
||||||
|
var params = {
|
||||||
|
msg: text,
|
||||||
|
sign_with: key
|
||||||
|
};
|
||||||
|
kbpgp.box(params, function (err, result_string, result_buffer) {
|
||||||
|
//console.log(err, result_string, result_buffer);
|
||||||
|
callback(result_string);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a new private key.
|
||||||
|
* @param {string} userid Something like "Test User <test@netsyms.com>"
|
||||||
|
* @param {string} passphrase protects the key
|
||||||
|
* @param {function} callback Passed the keymanager for the new key
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
function generatePrivateKey(userid, passphrase, callback) {
|
||||||
|
var F = kbpgp["const"].openpgp;
|
||||||
|
|
||||||
|
var opts = {
|
||||||
|
userid: userid,
|
||||||
|
primary: {
|
||||||
|
nbits: 2048,
|
||||||
|
flags: F.certify_keys | F.sign_data | F.auth | F.encrypt_comm | F.encrypt_storage,
|
||||||
|
expire_in: 0 // never expire
|
||||||
|
},
|
||||||
|
subkeys: []
|
||||||
|
};
|
||||||
|
|
||||||
|
kbpgp.KeyManager.generate(opts, function (err, alice) {
|
||||||
|
if (!err) {
|
||||||
|
alice.sign({}, function (err) {
|
||||||
|
alice.export_pgp_private({
|
||||||
|
passphrase: passphrase
|
||||||
|
}, function (err, pgp_private) {
|
||||||
|
callback(alice);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -3,16 +3,64 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
//
|
||||||
|
//function openFileDialog(callback) {
|
||||||
|
// $("#open-file-input").off("change");
|
||||||
|
// if (typeof callback != "undefined") {
|
||||||
|
// $("#open-file-input").on("change", function () {
|
||||||
|
// callback($("#open-file-input").val());
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// $("#open-file-input").click();
|
||||||
|
//}
|
||||||
|
|
||||||
function openFileDialog(callback) {
|
/**
|
||||||
$("#open-file-input").off("change");
|
* Open a save file dialog and passes the file path back.
|
||||||
if (typeof callback != "undefined") {
|
* @param {function} callback
|
||||||
$("#open-file-input").on("change", function () {
|
* @param {string} accept HTML5 accept="thisvar" (optional)
|
||||||
callback($("#open-file-input").val());
|
* @returns {undefined}
|
||||||
});
|
*/
|
||||||
|
function openFileDialog(callback, accept) {
|
||||||
|
var dialog = document.createElement("input");
|
||||||
|
dialog.setAttribute("type", "file");
|
||||||
|
if (typeof accept != "undefined") {
|
||||||
|
dialog.setAttribute("accept", accept);
|
||||||
|
}
|
||||||
|
dialog.onchange = function () {
|
||||||
|
callback(dialog.value);
|
||||||
|
}
|
||||||
|
dialog.dispatchEvent(new MouseEvent("click", {
|
||||||
|
"view": window,
|
||||||
|
"bubbles": false,
|
||||||
|
"cancelable": false
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
$("#open-file-input").click();
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a save file dialog and passes the file path back.
|
||||||
|
* @param {function} callback
|
||||||
|
* @param {string} defaultfilename something like file.pdf (optional)
|
||||||
|
* @param {string} accept HTML5 accept="thisvar" (optional)
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
function openSaveFileDialog(callback, defaultfilename, accept) {
|
||||||
|
var dialog = document.createElement("input");
|
||||||
|
dialog.setAttribute("type", "file");
|
||||||
|
if (typeof defaultfilename == "undefined") {
|
||||||
|
defaultfilename = "";
|
||||||
|
}
|
||||||
|
dialog.setAttribute("nwsaveas", defaultfilename);
|
||||||
|
if (typeof accept != "undefined") {
|
||||||
|
dialog.setAttribute("accept", accept);
|
||||||
|
}
|
||||||
|
dialog.onchange = function () {
|
||||||
|
callback(dialog.value);
|
||||||
|
}
|
||||||
|
dialog.dispatchEvent(new MouseEvent("click", {
|
||||||
|
"view": window,
|
||||||
|
"bubbles": false,
|
||||||
|
"cancelable": false
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFileAsString(path) {
|
function getFileAsString(path) {
|
||||||
@ -25,21 +73,26 @@ function getFileAsUint8Array(path) {
|
|||||||
return fs.readFileSync(path, null);
|
return fs.readFileSync(path, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeStringToFile(path, text) {
|
function writeToFile(path, data) {
|
||||||
const fs = require("fs");
|
|
||||||
fs.writeFileSync(path, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeDataToFile(path, data) {
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
fs.writeFileSync(path, data);
|
fs.writeFileSync(path, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendToFile(path, data) {
|
||||||
|
const fs = require("fs");
|
||||||
|
fs.appendFileSync(path, data);
|
||||||
|
}
|
||||||
|
|
||||||
function copyFile(source, dest) {
|
function copyFile(source, dest) {
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
fs.copyFileSync(source, dest);
|
fs.copyFileSync(source, dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deleteFile(path) {
|
||||||
|
const fs = require('fs');
|
||||||
|
fs.unlinkSync(path);
|
||||||
|
}
|
||||||
|
|
||||||
function getBasename(fullpath) {
|
function getBasename(fullpath) {
|
||||||
var path = require("path");
|
var path = require("path");
|
||||||
return path.basename(fullpath);
|
return path.basename(fullpath);
|
||||||
|
65428
js/kbpgp-2.1.15.js
Normal file
65428
js/kbpgp-2.1.15.js
Normal file
File diff suppressed because one or more lines are too long
50
js/pdf.js
50
js/pdf.js
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
const {jsPDF} = window.jspdf;
|
const {jsPDF} = window.jspdf;
|
||||||
|
|
||||||
var pdfPageScale = 4;
|
var pdfPageScale = 3;
|
||||||
var pdfAssumedDPI = 72;
|
var pdfAssumedDPI = 72;
|
||||||
var pdfDoc = null;
|
var pdfDoc = null;
|
||||||
var pageNumber = 0;
|
var pageNumber = 0;
|
||||||
@ -27,10 +27,10 @@ function addPDF() {
|
|||||||
// Initial/first page rendering
|
// Initial/first page rendering
|
||||||
//renderPage(pageNum);
|
//renderPage(pageNum);
|
||||||
});
|
});
|
||||||
});
|
}, ".pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
function savePDF() {
|
function generatePDF() {
|
||||||
var canvases = $("#page-canvas-container .page-canvas");
|
var canvases = $("#page-canvas-container .page-canvas");
|
||||||
const pdf = new jsPDF({
|
const pdf = new jsPDF({
|
||||||
unit: "in",
|
unit: "in",
|
||||||
@ -51,7 +51,49 @@ function savePDF() {
|
|||||||
pdf.addPage(pageFormat, pageOrientation);
|
pdf.addPage(pageFormat, pageOrientation);
|
||||||
pdf.addImage($("#page-canvas-container .page-canvas")[i].toDataURL(), 0, 0, pageWidthInches, pageHeightInches);
|
pdf.addImage($("#page-canvas-container .page-canvas")[i].toDataURL(), 0, 0, pageWidthInches, pageHeightInches);
|
||||||
}
|
}
|
||||||
pdf.save("signed.pdf");
|
return pdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPDFAsByteArray(pdf) {
|
||||||
|
return pdf.output("arraybuffer");
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeAndSaveSignedPDF(pdf, savepath, callback) {
|
||||||
|
const hasha = require('hasha');
|
||||||
|
|
||||||
|
var pdfbuffer = pdf.output("arraybuffer");
|
||||||
|
var hashstr = hasha(Buffer.from(pdfbuffer), {algorithm: 'sha256'});
|
||||||
|
|
||||||
|
var message = "HASH:" + hashstr
|
||||||
|
+ "\nNOTARY:" + getStorage("notary_name")
|
||||||
|
+ "\nSTATE:" + getStorage("notary_state")
|
||||||
|
+ "\n";
|
||||||
|
signMessage(message, keymgr, function (sig) {
|
||||||
|
writeToFile(savepath, Buffer.from(pdfbuffer));
|
||||||
|
appendToFile(savepath, sig);
|
||||||
|
|
||||||
|
//writeToFile(savepath + ".notsigned.pdf", Buffer.from(pdfbuffer));
|
||||||
|
writeToFile(savepath + ".sig", sig);
|
||||||
|
callback({
|
||||||
|
signature: sig,
|
||||||
|
hash: hashstr
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePDF() {
|
||||||
|
loadKeyFromLocalStorage(function (message, ok) {
|
||||||
|
if (ok) {
|
||||||
|
openSaveFileDialog(function (path) {
|
||||||
|
var pdf = generatePDF();
|
||||||
|
makeAndSaveSignedPDF(pdf, path, function (result) {
|
||||||
|
alert("File signed and saved.\n SHA256 of file (without signature): " + result.hash);
|
||||||
|
});
|
||||||
|
}, "signed.pdf", ".pdf");
|
||||||
|
} else {
|
||||||
|
alert("Error: " + message);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function pdfZoom(str) {
|
function pdfZoom(str) {
|
||||||
|
19
package-lock.json
generated
19
package-lock.json
generated
@ -108,6 +108,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
|
||||||
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="
|
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="
|
||||||
},
|
},
|
||||||
|
"hasha": {
|
||||||
|
"version": "5.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz",
|
||||||
|
"integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==",
|
||||||
|
"requires": {
|
||||||
|
"is-stream": "^2.0.0",
|
||||||
|
"type-fest": "^0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"html2canvas": {
|
"html2canvas": {
|
||||||
"version": "1.0.0-rc.7",
|
"version": "1.0.0-rc.7",
|
||||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-rc.7.tgz",
|
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-rc.7.tgz",
|
||||||
@ -117,6 +126,11 @@
|
|||||||
"css-line-break": "1.1.1"
|
"css-line-break": "1.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"is-stream": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw=="
|
||||||
|
},
|
||||||
"jquery": {
|
"jquery": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
|
||||||
@ -205,6 +219,11 @@
|
|||||||
"version": "1.14.1",
|
"version": "1.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||||
|
},
|
||||||
|
"type-fest": {
|
||||||
|
"version": "0.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
|
||||||
|
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^5.15.3",
|
"@fortawesome/fontawesome-free": "^5.15.3",
|
||||||
"bootstrap": "^5.0.2",
|
"bootstrap": "^5.0.2",
|
||||||
|
"hasha": "^5.2.2",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
"jspdf": "^2.3.1",
|
"jspdf": "^2.3.1",
|
||||||
"pdf-lib": "^1.16.0",
|
"pdf-lib": "^1.16.0",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user