2021-06-30 02:32:55 -06:00
/ *
* 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 { jsPDF } = window . jspdf ;
2021-07-08 19:53:19 -06:00
const certificateVersion = 1 ;
const pdfPageScale = 3 ;
const pdfAssumedDPI = 72 ;
2021-06-30 02:32:55 -06:00
var pdfDoc = null ;
var pageNumber = 0 ;
2021-07-05 17:36:36 -06:00
var pdfTitle = "" ;
2021-06-30 02:32:55 -06:00
function addPDF ( ) {
openFileDialog ( function ( path ) {
var filedata = getFileAsUint8Array ( path ) ;
2021-07-05 17:36:36 -06:00
// Get filename
// https://stackoverflow.com/a/424006
pdfTitle = path . split ( '\\' ) . pop ( ) . split ( '/' ) . pop ( ) ;
2021-06-30 02:32:55 -06:00
pdfjsLib . getDocument ( filedata ) . promise . then ( function ( pdfDoc _ ) {
pdfDoc = pdfDoc _ ;
2021-07-05 17:36:36 -06:00
pdfDoc . getMetadata ( ) . then ( function ( meta ) {
if ( typeof meta . contentDispositionFilename == "string" ) {
pdfTitle = meta . contentDispositionFilename ;
}
} ) . catch ( function ( err ) {
console . log ( 'Error getting PDF metadata: ' , err ) ;
} ) ;
2021-06-30 02:32:55 -06:00
renderAllPages ( pdfDoc ) ;
pdfZoom ( "fitwidth" ) ;
// Initial/first page rendering
//renderPage(pageNum);
} ) ;
2021-07-01 23:18:13 -06:00
} , ".pdf" ) ;
2021-06-30 02:32:55 -06:00
}
2021-07-02 00:55:28 -06:00
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 ( ) ;
}
2021-07-04 22:29:55 -06:00
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 ) {
var pdf = Buffer . from ( getFileAsUint8Array ( path ) . buffer ) ;
var splitindex = pdf . indexOf ( "-----BEGIN PGP MESSAGE-----" ) ;
if ( splitindex == - 1 ) {
alert ( "Selected file does not contain any recognized signature data." ) ;
return ;
}
var pdfdata = pdf . slice ( 0 , splitindex ) ;
var sigdata = pdf . slice ( splitindex ) . toString ( ) ;
var pdfhash = calculateSHA256HashOfString ( pdfdata ) ;
2021-07-05 23:28:52 -06:00
loadKeyFromLocalStorage ( function ( ) {
verifyMessage ( sigdata , function ( msg , fprint ) {
parseAndDisplaySignature ( msg , pdfhash , true , fprint ) ;
} , function ( err ) {
console . error ( err ) ;
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 ( ex ) {
console . error ( ex ) ;
alert ( "Error: could not parse signature data." ) ;
}
} ) ;
2021-07-04 22:29:55 -06:00
} ) ;
pdfjsLib . getDocument ( pdf ) . promise . then ( function ( pdfDoc _ ) {
pdfDoc = pdfDoc _ ;
renderAllPages ( pdfDoc ) ;
pdfZoom ( "fitheight" ) ;
} ) ;
} , ".pdf" ) ;
}
function parseAndDisplaySignature ( msg , pdfhash , verified , fingerprint ) {
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-outline-secondary btn-sm\"onclick=\"openPublicKeyFile()\" > click here < / s p a n > t o u s e i t , \
2021-07-05 23:28:52 -06:00
then run the analyze tool again to prove if it was changed since notarization . " ) ;
2021-07-04 22:29:55 -06:00
$ ( "#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>' ) ;
}
2021-07-05 23:28:52 -06:00
if ( typeof msgparts [ "OTS" ] == "string" ) {
var bytearray = [ ] ;
var bytestrarray = msgparts [ "OTS" ] . match ( /.{1,3}/g ) ;
for ( var i = 0 ; i < bytestrarray . length ; i ++ ) {
bytearray . push ( bytestrarray [ i ] * 1 ) ;
}
const detached = OpenTimestamps . DetachedTimestampFile . fromHash ( new OpenTimestamps . Ops . OpSHA256 ( ) , Uint8Array . from ( Buffer . from ( pdfhash , 'hex' ) ) ) ;
const detachedOts = OpenTimestamps . DetachedTimestampFile . deserialize ( bytearray ) ;
let options = { } ;
OpenTimestamps . verify ( detachedOts , detached , options ) . then ( verifyResult => {
console . log ( verifyResult ) ;
if ( typeof verifyResult != "undefined" ) {
if ( typeof verifyResult . bitcoin != undefined ) {
$ ( "#verifyModalDetailedInfoList" ) . append ( '<li class="list-group-item"><i class="fas fa-clock fa-fw"></i> Signing time independently verified on the Bitcoin blockchain. Signed at ' + formatTimestamp ( "F j, Y g:i a" , verifyResult . bitcoin . timestamp ) + '</li>' ) ;
}
if ( typeof verifyResult . litecoin != undefined ) {
$ ( "#verifyModalDetailedInfoList" ) . append ( '<li class="list-group-item"><i class="fas fa-clock fa-fw"></i> Signing time independently verified on the Litecoin blockchain. Signed at ' + formatTimestamp ( "F j, Y g:i a" , verifyResult . bitcoin . timestamp ) + '</li>' ) ;
}
}
} ) ;
}
$ ( "#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>' ) ;
}
2021-07-04 22:29:55 -06:00
if ( typeof fingerprint == "string" ) {
$ ( "#verifyModalDetailedInfoList" ) . append ( '<li class="list-group-item"><i class="fas fa-fingerprint fa-fw"></i> Signature fingerprint: ' + fingerprint + '</li>' ) ;
}
new bootstrap . Modal ( document . getElementById ( 'verifyModal' ) ) . show ( ) ;
}
2021-07-02 02:43:47 -06:00
function generatePDF ( callback ) {
2021-06-30 02:32:55 -06:00
var canvases = $ ( "#page-canvas-container .page-canvas" ) ;
2021-07-02 02:43:47 -06:00
var statustextEl = $ ( "#statustext" ) ;
statustextEl . html ( "<i class='fas fa-spin fa-spinner'></i> Processing document..." ) ;
2021-06-30 02:32:55 -06:00
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 ) ;
2021-07-02 02:43:47 -06:00
// 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 ) ;
}
2021-06-30 02:32:55 -06:00
}
2021-07-02 02:43:47 -06:00
processPage ( 0 ) ;
2021-07-01 23:18:13 -06:00
}
function getPDFAsByteArray ( pdf ) {
return pdf . output ( "arraybuffer" ) ;
}
function makeAndSaveSignedPDF ( pdf , savepath , callback ) {
var pdfbuffer = pdf . output ( "arraybuffer" ) ;
2021-07-05 23:28:52 -06:00
const hashstr = calculateSHA256HashOfBuffer ( pdfbuffer ) ;
var detached = OpenTimestamps . DetachedTimestampFile . fromHash ( new OpenTimestamps . Ops . OpSHA256 ( ) , Uint8Array . from ( Buffer . from ( hashstr , 'hex' ) ) ) ;
var otsbytes = "" ;
var sign = function ( ) {
var message = "START"
2021-07-08 19:53:19 -06:00
+ "\nV:" + certificateVersion
2021-07-05 23:28:52 -06:00
+ "\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
} ) ;
2021-07-01 23:18:13 -06:00
} ) ;
2021-07-05 23:28:52 -06:00
} ;
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 ( ) ;
2021-07-01 23:18:13 -06:00
} ) ;
}
function savePDF ( ) {
2021-07-02 00:55:28 -06:00
disableGuideBox ( ) ;
2021-07-02 02:43:47 -06:00
var statustextEl = $ ( "#statustext" ) ;
2021-07-01 23:18:13 -06:00
loadKeyFromLocalStorage ( function ( message , ok ) {
if ( ok ) {
openSaveFileDialog ( function ( path ) {
2021-07-02 02:43:47 -06:00
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!" ) ;
alert ( "File signed and saved.\nSHA256 of file (without signature): " + result . hash ) ;
setTimeout ( function ( ) {
statustextEl . html ( "" ) ;
} , 5000 ) ;
} ) ;
2021-07-01 23:18:13 -06:00
} ) ;
} , "signed.pdf" , ".pdf" ) ;
} else {
2021-07-02 02:43:47 -06:00
statustextEl . html ( "" ) ;
2021-07-01 23:18:13 -06:00
alert ( "Error: " + message ) ;
}
} ) ;
2021-06-30 02:32:55 -06:00
}
function pdfZoom ( str ) {
disableGuideBox ( ) ;
var widthpx = $ ( "#page-canvas-container .page-canvas" ) . css ( "width" ) . replace ( "px" , "" ) * 1 ;
var zoomstep = 100 ;
console . log ( widthpx ) ;
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" , "100%" ) ;
$ ( "#page-canvas-container .page-canvas" ) . css ( "height" , "auto" ) ;
break ;
case "fitheight" :
$ ( "#page-canvas-container .page-canvas" ) . css ( "height" , "100%" ) ;
$ ( "#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 ( ) {
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 ;
$ ( "#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 ;
2021-07-02 00:38:28 -06:00
//document.getElementById('page_count').textContent = pageNumber;
2021-06-30 02:32:55 -06:00
}