You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
487 lines
22 KiB
JavaScript
487 lines
22 KiB
JavaScript
/*
|
|
* 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/.
|
|
*/
|
|
|
|
const OpenTimestamps = require('opentimestamps');
|
|
const nwglobal = require('nwglobal');
|
|
const {jsPDF} = window.jspdf;
|
|
|
|
const certificateVersion = 1;
|
|
const pdfPageScale = 3;
|
|
const pdfAssumedDPI = 72;
|
|
var pdfDoc = null;
|
|
var pageNumber = 0;
|
|
var pdfTitle = "";
|
|
|
|
function addPDF() {
|
|
openFileDialog(function (path) {
|
|
var filedata = getFileAsUint8Array(path);
|
|
|
|
// Get filename
|
|
// https://stackoverflow.com/a/424006
|
|
pdfTitle = path.split('\\').pop().split('/').pop();
|
|
|
|
pdfjsLib.getDocument(filedata).promise.then(function (pdfDoc_) {
|
|
pdfDoc = pdfDoc_;
|
|
|
|
pdfDoc.getMetadata().then(function (meta) {
|
|
if (typeof meta.contentDispositionFilename == "string") {
|
|
pdfTitle = meta.contentDispositionFilename;
|
|
}
|
|
}).catch(function (err) {
|
|
console.log('Error getting PDF metadata: ', err);
|
|
});
|
|
|
|
renderAllPages(pdfDoc);
|
|
pdfZoom("fitheight");
|
|
$(".enable-when-doc-open").removeClass("disabled");
|
|
// Initial/first page rendering
|
|
//renderPage(pageNum);
|
|
});
|
|
}, ".pdf");
|
|
}
|
|
|
|
function closePDF(showuserconfirm) {
|
|
if (showuserconfirm && !confirm("Are you sure you want to close? All unsaved changes will be lost.")) {
|
|
return;
|
|
}
|
|
disableGuideBox();
|
|
pageNumber = 0;
|
|
pdfDoc = null;
|
|
$("#page-canvas-container .page-canvas").remove();
|
|
$(".enable-when-doc-open").addClass("disabled");
|
|
}
|
|
|
|
function analyzeSignedPDF() {
|
|
if ($("#page-canvas-container .page-canvas").length > 0 && !confirm("Opening a PDF to analyze will close the open document. Are you sure?")) {
|
|
return;
|
|
}
|
|
closePDF(false);
|
|
openFileDialog(function (path, html5file) {
|
|
var analyze = function (pdf) {
|
|
var splitindex = pdf.indexOf("-----BEGIN PGP MESSAGE-----");
|
|
if (splitindex == -1) {
|
|
showAlert("Selected file does not contain any recognized signature data.");
|
|
return;
|
|
}
|
|
var pdfdata = pdf.slice(0, splitindex);
|
|
var sigdata = pdf.slice(splitindex).toString();
|
|
|
|
var verify = function (pdfhash, reload) {
|
|
loadKeyFromLocalStorage(function () {
|
|
verifyMessage(sigdata, function (msg, fprint) {
|
|
parseAndDisplaySignature(msg, pdfhash, true, fprint);
|
|
}, function (err) {
|
|
console.error(err);
|
|
try {
|
|
readSignatureExternally(sigdata, function (msg, keyprint, signername, verified, ok) {
|
|
if (!ok) {
|
|
showAlert("Error: could not parse signature data.");
|
|
return;
|
|
}
|
|
// If system GPG has the public key, use that.
|
|
// Otherwise, try looking up the fingerprint online and if we get hits
|
|
// add them to the local keyring and try verifying again.
|
|
if (verified) {
|
|
parseAndDisplaySignature(msg, pdfhash, verified, keyprint, signername);
|
|
} else {
|
|
if (typeof reload == 'undefined' || reload == false) {
|
|
lookupPublicKey(keyprint, function (res) {
|
|
if (res == false) {
|
|
parseAndDisplaySignature(msg, pdfhash, verified, keyprint, signername);
|
|
} else {
|
|
importPublicKeysFromRegistry(res, function () {
|
|
verify(pdfhash, true);
|
|
});
|
|
}
|
|
});
|
|
} else {
|
|
parseAndDisplaySignature(msg, pdfhash, verified, keyprint, signername);
|
|
}
|
|
}
|
|
});
|
|
} catch (ex) {
|
|
console.error(ex);
|
|
var base64 = sigdata.split("\n\n", 2)[1].split("\n-----END PGP MESSAGE-----")[0];
|
|
base64 = base64.substring(0, base64.lastIndexOf("\n")).replaceAll("\n", "");
|
|
try {
|
|
var msg = window.atob(base64).split("START", 2)[1].split("END", 2)[0];
|
|
parseAndDisplaySignature(msg, pdfhash, false, null);
|
|
} catch (exx) {
|
|
console.error(exx);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
if (typeof reload == 'undefined' || reload == false) {
|
|
if (typeof nw != 'undefined') {
|
|
pdfjsLib.getDocument(pdf).promise.then(function (pdfDoc_) {
|
|
pdfDoc = pdfDoc_;
|
|
|
|
renderAllPages(pdfDoc);
|
|
pdfZoom("fitheight");
|
|
});
|
|
} else {
|
|
var fileReader = new FileReader();
|
|
fileReader.onload = function () {
|
|
pdfjsLib.getDocument(new Uint8Array(this.result)).promise.then(function (pdfDoc_) {
|
|
pdfDoc = pdfDoc_;
|
|
|
|
renderAllPages(pdfDoc);
|
|
pdfZoom("fitheight");
|
|
});
|
|
};
|
|
fileReader.readAsArrayBuffer(html5file);
|
|
}
|
|
$(".enable-when-doc-open").removeClass("disabled");
|
|
}
|
|
};
|
|
|
|
if (typeof nw != 'undefined') {
|
|
verify(calculateSHA256HashOfString(pdfdata));
|
|
} else {
|
|
window.crypto.subtle.digest("SHA-256", (new TextEncoder()).encode(pdfdata))
|
|
.then(hash => {
|
|
window.hash = hash;
|
|
// here hash is an arrayBuffer,
|
|
// so we'll connvert it to its hex version
|
|
let result = '';
|
|
const view = new DataView(hash);
|
|
for (let i = 0; i < hash.byteLength; i += 4) {
|
|
result += ('00000000' + view.getUint32(i).toString(16)).slice(-8);
|
|
}
|
|
verify(result);
|
|
});
|
|
}
|
|
};
|
|
if (typeof nw != 'undefined') {
|
|
// running in NW.js so we have Node
|
|
analyze(Buffer.from(getFileAsUint8Array(path).buffer));
|
|
} else {
|
|
// no Node :(
|
|
var fileReader = new FileReader();
|
|
fileReader.onload = function (e) {
|
|
analyze(e.target.result);
|
|
}
|
|
fileReader.readAsBinaryString(html5file);
|
|
}
|
|
|
|
}, ".pdf");
|
|
}
|
|
|
|
function parseAndDisplaySignature(msg, pdfhash, verified, fingerprint, signername) {
|
|
var msgparts = {};
|
|
// Decode message contents
|
|
var msglines = msg.split("\n");
|
|
for (var i = 0; i < msglines.length; i++) {
|
|
if (msglines[i].includes(":")) {
|
|
var parts = msglines[i].split(":", 2);
|
|
msgparts[parts[0]] = parts[1];
|
|
}
|
|
}
|
|
if (typeof msgparts["HASH"] == "string") {
|
|
if (msgparts["HASH"] == pdfhash) {
|
|
if (verified) {
|
|
$("#verifyModalStatusMessage").html("<i class=\"fas fa-check-circle\"></i> File contents match signature. File has not been changed since notarization.");
|
|
$("#verifyModalStatusMessage").removeClass();
|
|
$("#verifyModalStatusMessage").addClass(["alert", "alert-success"]);
|
|
} else {
|
|
$("#verifyModalStatusMessage").html("<i class=\"fas fa-question-circle\"></i> File contents match signature; however, \
|
|
could not verify signature authenticity. It's possible the file was changed then re-signed by an unknown person. If you have the \
|
|
public key file for the notary that signed the file, <span class=\"btn btn-secondary text-dark btn-sm\"onclick=\"openPublicKeyFile()\">click here</span> to use it, \
|
|
then run the analyze tool again to prove if it was changed since notarization.");
|
|
$("#verifyModalStatusMessage").removeClass();
|
|
$("#verifyModalStatusMessage").addClass(["alert", "alert-warning"]);
|
|
}
|
|
} else {
|
|
$("#verifyModalStatusMessage").html("<i class=\"fas fa-exclamation-circle\"></i> File contents do not match signature. Document has been modified since notarization.");
|
|
$("#verifyModalStatusMessage").removeClass();
|
|
$("#verifyModalStatusMessage").addClass(["alert", "alert-danger"]);
|
|
}
|
|
} else {
|
|
$("#verifyModalStatusMessage").html("<i class=\"fas fa-exclamation-circle\"></i> No file hash found in document signature. Could not verify document integrity.");
|
|
$("#verifyModalStatusMessage").removeClass();
|
|
$("#verifyModalStatusMessage").addClass(["alert", "alert-danger"]);
|
|
}
|
|
// Add extra data to a list below the big message
|
|
$("#verifyModalDetailedInfoList").html("");
|
|
if (typeof msgparts["DATE"] == "string" && isNaN(msgparts["DATE"]) == false) {
|
|
var datestr = formatTimestamp("F j, Y g:i a", msgparts["DATE"]);
|
|
$("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="far fa-calendar-alt fa-fw"></i> Notarization date/time: ' + datestr + '</li>');
|
|
}
|
|
if (typeof msgparts["NOTARY"] == "string") {
|
|
$("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="fas fa-user fa-fw"></i> Notary: ' + sanitizeHTMLString(msgparts["NOTARY"]) + '</li>');
|
|
}
|
|
if (typeof msgparts["STATE"] == "string") {
|
|
$("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="fas fa-map-marked-alt fa-fw"></i> State: ' + sanitizeHTMLString(msgparts["STATE"]).toUpperCase() + '</li>');
|
|
}
|
|
if (typeof msgparts["OTS"] == "string") {
|
|
try {
|
|
var bytearray = new nwglobal.Array();
|
|
var bytestrarray = msgparts["OTS"].match(/.{1,3}/g);
|
|
for (var i = 0; i < bytestrarray.length; i++) {
|
|
bytearray.push(bytestrarray[i] * 1);
|
|
}
|
|
const fileHashArr = nwglobal.Uint8Array.from(Buffer.from(pdfhash, 'hex'));
|
|
const detached = OpenTimestamps.DetachedTimestampFile.fromHash(new OpenTimestamps.Ops.OpSHA256(), fileHashArr);
|
|
const detachedOts = OpenTimestamps.DetachedTimestampFile.deserialize(bytearray);
|
|
let options = {
|
|
ignoreBitcoinNode: true,
|
|
timeout: 5000
|
|
};
|
|
OpenTimestamps.verify(detachedOts, detached, options).then(verifyResult => {
|
|
if (typeof verifyResult != "undefined") {
|
|
if (typeof verifyResult.bitcoin != undefined) {
|
|
$("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="fab fa-bitcoin fa-fw"></i> '
|
|
+ 'Signing time witnessed by the Bitcoin network, proving it was signed within a few hours of '
|
|
+ formatTimestamp("g A", verifyResult.bitcoin.timestamp) + ' on '
|
|
+ formatTimestamp("F j, Y", verifyResult.bitcoin.timestamp) + '.'
|
|
+ '</li>');
|
|
}
|
|
if (typeof verifyResult.litecoin != undefined) {
|
|
$("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="fab fa-litecoin fa-fw"></i> '
|
|
+ 'Signing time witnessed by the Litecoin network, proving it was signed within a few hours of '
|
|
+ formatTimestamp("g A", verifyResult.litecoin.timestamp) + ' on '
|
|
+ formatTimestamp("F j, Y", verifyResult.litecoin.timestamp) + '.'
|
|
+ '</li>');
|
|
}
|
|
}
|
|
});
|
|
} catch (ex) {
|
|
console.error(ex);
|
|
}
|
|
}
|
|
|
|
$("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="far fa-file fa-fw"></i> Actual file hash: ' + pdfhash + '</li>');
|
|
|
|
if (typeof msgparts["HASH"] == "string") {
|
|
$("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="far fa-file fa-fw"></i> Signed file hash: ' + sanitizeHTMLString(msgparts["HASH"]) + '</li>');
|
|
}
|
|
|
|
if (typeof fingerprint == "string") {
|
|
if (fingerprint.length > 16) {
|
|
var fingerprintstart = fingerprint.substr(0, fingerprint.length - 16);
|
|
var fingerprintend = fingerprint.substr(-16);
|
|
} else {
|
|
var fingerprintstart = "";
|
|
var fingerprintend = fingerprint;
|
|
}
|
|
$("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="fas fa-fingerprint fa-fw"></i> Public key ID: ' + fingerprintstart + '<b>' + fingerprintend + '</b></li>');
|
|
lookupPublicKey(fingerprint, function (result) {
|
|
if (result == false) {
|
|
return;
|
|
}
|
|
if (result.length == 1) {
|
|
$("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="fas fa-globe fa-fw"></i> '
|
|
+ 'The signing notary\'s information was found in an online database.<br>'
|
|
+ '<span class="ms-3 d-block">'
|
|
+ (result[0].name == null ? '' : 'Name: ' + sanitizeHTMLString(result[0].name) + '<br>')
|
|
+ (result[0].email == null ? '' : 'Email: ' + sanitizeHTMLString(result[0].email) + '<br>')
|
|
+ (result[0].state == null ? '' : 'State: ' + sanitizeHTMLString(result[0].state.toUpperCase()) + '<br>')
|
|
+ (result[0].location == null ? '' : 'Location: ' + sanitizeHTMLString(result[0].location) + '<br>')
|
|
+ (result[0].idnumber == null ? '' : 'Commission ID: ' + sanitizeHTMLString(result[0].idnumber) + '<br>')
|
|
+ (result[0].commissionexpires == null ? '' : 'Commission Expires: ' + formatTimestamp("F j, Y", result[0].commissionexpires))
|
|
+ '</span>'
|
|
+ '</li>');
|
|
}
|
|
});
|
|
}
|
|
|
|
if (typeof signername == "string") {
|
|
$("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="fas fa-user-shield fa-fw"></i> Owner of public key: ' + sanitizeHTMLString(signername) + '</li>');
|
|
}
|
|
new bootstrap.Modal(document.getElementById('verifyModal')).show();
|
|
}
|
|
|
|
function generatePDF(callback) {
|
|
var canvases = $("#page-canvas-container .page-canvas");
|
|
var statustextEl = $("#statustext");
|
|
statustextEl.html("<i class='fas fa-spin fa-spinner'></i> Processing document...");
|
|
const pdf = new jsPDF({
|
|
unit: "in",
|
|
compress: true
|
|
});
|
|
// creating a PDF creates a blank page that we don't want to use,
|
|
// as we haven't done the calculations yet
|
|
pdf.deletePage(1);
|
|
|
|
// Render each page in order with a pause in between to keep UI responsive
|
|
var processPage = function (i) {
|
|
if (i < canvases.length) {
|
|
statustextEl.html("<i class='fas fa-spin fa-spinner'></i> Processing page " + (i + 1) + " of " + canvases.length + "...");
|
|
console.log("Processing " + (i + 1));
|
|
var canvas = canvases[i];
|
|
var widthpx = canvas.getContext("2d").canvas.width;
|
|
var heightpx = canvas.getContext("2d").canvas.height;
|
|
var pageWidthInches = widthpx / (pdfAssumedDPI * pdfPageScale);
|
|
var pageHeightInches = heightpx / (pdfAssumedDPI * pdfPageScale);
|
|
console.log(pageWidthInches + " x " + pageHeightInches);
|
|
var pageFormat = [pageWidthInches, pageHeightInches];
|
|
var pageOrientation = (pageWidthInches > pageHeightInches ? "landscape" : "portrait");
|
|
pdf.addPage(pageFormat, pageOrientation);
|
|
pdf.addImage(canvases[i].toDataURL(), 0, 0, pageWidthInches, pageHeightInches);
|
|
i++;
|
|
setTimeout(function () {
|
|
processPage(i)
|
|
}, 100);
|
|
} else {
|
|
statustextEl.html("");
|
|
callback(pdf);
|
|
}
|
|
}
|
|
processPage(0);
|
|
}
|
|
|
|
function getPDFAsByteArray(pdf) {
|
|
return pdf.output("arraybuffer");
|
|
}
|
|
|
|
function makeAndSaveSignedPDF(pdf, savepath, callback) {
|
|
var pdfbuffer = pdf.output("arraybuffer");
|
|
const hashstr = calculateSHA256HashOfBuffer(pdfbuffer);
|
|
|
|
var detached = OpenTimestamps.DetachedTimestampFile.fromHash(new OpenTimestamps.Ops.OpSHA256(), nwglobal.Uint8Array.from(Buffer.from(hashstr, 'hex')));
|
|
var otsbytes = "";
|
|
|
|
var sign = function () {
|
|
var message = "START"
|
|
+ "\nV:" + certificateVersion
|
|
+ "\nHASH:" + hashstr
|
|
+ "\nDATE:" + time()
|
|
+ (otsbytes != "" ? "\nOTS:" + otsbytes : "")
|
|
+ "\nNOTARY:" + getStorage("notary_name")
|
|
+ "\nSTATE:" + getStorage("notary_state")
|
|
+ "\nEND\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
|
|
});
|
|
});
|
|
};
|
|
|
|
OpenTimestamps.stamp(detached).then(() => {
|
|
var bytearray = detached.serializeToBytes();
|
|
var bytestr = "";
|
|
for (var i = 0; i < bytearray.length; i++) {
|
|
bytestr += (bytearray[i] + "").padStart(3, "0");
|
|
}
|
|
otsbytes = bytestr;
|
|
sign();
|
|
}).catch(() => {
|
|
sign();
|
|
});
|
|
}
|
|
|
|
function savePDF() {
|
|
disableGuideBox();
|
|
var statustextEl = $("#statustext");
|
|
loadKeyFromLocalStorage(function (message, ok) {
|
|
if (ok) {
|
|
openSaveFileDialog(function (path) {
|
|
generatePDF(function (pdf) {
|
|
statustextEl.html("<i class='fas fa-spin fa-spinner'></i> Signing document...");
|
|
makeAndSaveSignedPDF(pdf, path, function (result) {
|
|
statustextEl.html("<i class='fas fa-check'></i> Signed and saved!");
|
|
showAlert("<i class='fas fa-check'></i> File signed and saved. SHA256 of file (excluding appended signature): " + result.hash);
|
|
setTimeout(function () {
|
|
statustextEl.html("");
|
|
}, 5000);
|
|
});
|
|
});
|
|
}, "signed.pdf", ".pdf");
|
|
} else {
|
|
statustextEl.html("");
|
|
showAlert("Error: " + message);
|
|
}
|
|
});
|
|
}
|
|
|
|
function pdfZoom(str) {
|
|
disableGuideBox();
|
|
if ($("#page-canvas-container .page-canvas").length == 0) {
|
|
setTimeout(function () {
|
|
pdfZoom(str);
|
|
}, 100);
|
|
return;
|
|
}
|
|
var widthpx = $("#page-canvas-container .page-canvas").css("width").replace("px", "") * 1;
|
|
var zoomstep = 100;
|
|
switch (str) {
|
|
case "out":
|
|
$("#page-canvas-container .page-canvas").css("height", "auto");
|
|
widthpx -= zoomstep;
|
|
$("#page-canvas-container .page-canvas").css("width", widthpx + "px");
|
|
break;
|
|
case "in":
|
|
$("#page-canvas-container .page-canvas").css("height", "auto");
|
|
widthpx += zoomstep;
|
|
$("#page-canvas-container .page-canvas").css("width", widthpx + "px");
|
|
break;
|
|
case "fitwidth":
|
|
$("#page-canvas-container .page-canvas").css("width", "calc(100% - 1rem)");
|
|
$("#page-canvas-container .page-canvas").css("height", "auto");
|
|
break;
|
|
case "fitheight":
|
|
$("#page-canvas-container .page-canvas").css("height", "calc(100% - 1rem)");
|
|
$("#page-canvas-container .page-canvas").css("width", "auto");
|
|
break;
|
|
}
|
|
}
|
|
|
|
function getNewCanvas(pagenumber) {
|
|
var canvas = document.createElement('canvas');
|
|
canvas.id = "pdf-canvas-page-" + pagenumber;
|
|
canvas.className = "page-canvas";
|
|
return canvas;
|
|
}
|
|
|
|
function addPage() {
|
|
if ($("#page-canvas-container .page-canvas").length == 0) {
|
|
showToast("Please open a document first.");
|
|
return;
|
|
}
|
|
pageNumber++;
|
|
var canvas = getNewCanvas(pageNumber);
|
|
var prevPageCanvas = $("#page-canvas-container .page-canvas#pdf-canvas-page-" + (pageNumber - 1))[0];
|
|
canvas.width = prevPageCanvas.getContext("2d").canvas.width;
|
|
canvas.height = prevPageCanvas.getContext("2d").canvas.height;
|
|
var ctx = canvas.getContext('2d');
|
|
ctx.fillStyle = 'white';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
$("#page-canvas-container").append(canvas);
|
|
}
|
|
|
|
function renderAllPages() {
|
|
var startingPageNumber = pageNumber;
|
|
var thisDocPageCount = pdfDoc.numPages;
|
|
for (var i = 1; i <= pdfDoc.numPages; i++) {
|
|
pdfDoc.getPage(i).then(function (page) {
|
|
var viewport = page.getViewport({scale: pdfPageScale});
|
|
var canvas = getNewCanvas(page.pageNumber + startingPageNumber);
|
|
canvas.height = viewport.height;
|
|
canvas.width = viewport.width;
|
|
$("#page-canvas-container").append(canvas);
|
|
|
|
// Render PDF page into canvas context
|
|
var renderContext = {
|
|
canvasContext: canvas.getContext("2d"),
|
|
viewport: viewport
|
|
};
|
|
|
|
page.render(renderContext);
|
|
});
|
|
}
|
|
pageNumber = pageNumber + thisDocPageCount;
|
|
//document.getElementById('page_count').textContent = pageNumber;
|
|
} |