/ *
* 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 > F i l e c o n t e n t s m a t c h s i g n a t u r e ; h o w e v e r , \
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 < / s p a n > t o u s e i t , \
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;
}