Save signed PDFs (close #2)

This commit is contained in:
Skylar Ittner 2021-07-01 23:18:13 -06:00
parent f5c56af2d2
commit d892fe81ed
7 changed files with 65687 additions and 21 deletions

View File

@ -99,10 +99,6 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
</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>
@ -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/jspdf/dist/jspdf.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/util.js"></script>
<script src="js/storage.js"></script>
<script src="js/filesystem.js"></script>
<script src="js/crypto.js"></script>
<script src="js/drawtools.js"></script>
<script src="js/pdf.js"></script>
<script src="js/main.js"></script>

124
js/crypto.js Normal file
View 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);
});
});
}
});
}

View File

@ -3,16 +3,64 @@
* 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/.
*/
//
//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");
if (typeof callback != "undefined") {
$("#open-file-input").on("change", function () {
callback($("#open-file-input").val());
});
/**
* Open a save file dialog and passes the file path back.
* @param {function} callback
* @param {string} accept HTML5 accept="thisvar" (optional)
* @returns {undefined}
*/
function openFileDialog(callback, accept) {
var dialog = document.createElement("input");
dialog.setAttribute("type", "file");
if (typeof accept != "undefined") {
dialog.setAttribute("accept", accept);
}
$("#open-file-input").click();
dialog.onchange = function () {
callback(dialog.value);
}
dialog.dispatchEvent(new MouseEvent("click", {
"view": window,
"bubbles": false,
"cancelable": false
}));
}
/**
* 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) {
@ -25,21 +73,26 @@ function getFileAsUint8Array(path) {
return fs.readFileSync(path, null);
}
function writeStringToFile(path, text) {
const fs = require("fs");
fs.writeFileSync(path, text);
}
function writeDataToFile(path, data) {
function writeToFile(path, data) {
const fs = require("fs");
fs.writeFileSync(path, data);
}
function appendToFile(path, data) {
const fs = require("fs");
fs.appendFileSync(path, data);
}
function copyFile(source, dest) {
const fs = require("fs");
fs.copyFileSync(source, dest);
}
function deleteFile(path) {
const fs = require('fs');
fs.unlinkSync(path);
}
function getBasename(fullpath) {
var path = require("path");
return path.basename(fullpath);

65428
js/kbpgp-2.1.15.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -7,7 +7,7 @@
const {jsPDF} = window.jspdf;
var pdfPageScale = 4;
var pdfPageScale = 3;
var pdfAssumedDPI = 72;
var pdfDoc = null;
var pageNumber = 0;
@ -27,10 +27,10 @@ function addPDF() {
// Initial/first page rendering
//renderPage(pageNum);
});
});
}, ".pdf");
}
function savePDF() {
function generatePDF() {
var canvases = $("#page-canvas-container .page-canvas");
const pdf = new jsPDF({
unit: "in",
@ -51,7 +51,49 @@ function savePDF() {
pdf.addPage(pageFormat, pageOrientation);
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) {

19
package-lock.json generated
View File

@ -108,6 +108,15 @@
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
"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": {
"version": "1.0.0-rc.7",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-rc.7.tgz",
@ -117,6 +126,11 @@
"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": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
@ -205,6 +219,11 @@
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"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=="
}
}
}

View File

@ -14,6 +14,7 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.3",
"bootstrap": "^5.0.2",
"hasha": "^5.2.2",
"jquery": "^3.6.0",
"jspdf": "^2.3.1",
"pdf-lib": "^1.16.0",