import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; import { readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js'; import { validatePdfSignatures } from './validate-signature-pdf.js'; import forge from 'node-forge'; import { SignatureValidationResult, ValidateSignatureState } from '@/types'; const state: ValidateSignatureState = { pdfFile: null, pdfBytes: null, results: [], trustedCertFile: null, trustedCert: null, }; function getElement(id: string): T | null { return document.getElementById(id) as T | null; } function resetState(): void { state.pdfFile = null; state.pdfBytes = null; state.results = []; const fileDisplayArea = getElement('file-display-area'); if (fileDisplayArea) fileDisplayArea.innerHTML = ''; const resultsSection = getElement('results-section'); if (resultsSection) resultsSection.classList.add('hidden'); const resultsContainer = getElement('results-container'); if (resultsContainer) resultsContainer.innerHTML = ''; const fileInput = getElement('file-input'); if (fileInput) fileInput.value = ''; const customCertSection = getElement('custom-cert-section'); if (customCertSection) customCertSection.classList.add('hidden'); } function resetCertState(): void { state.trustedCertFile = null; state.trustedCert = null; const certDisplayArea = getElement('cert-display-area'); if (certDisplayArea) certDisplayArea.innerHTML = ''; const certInput = getElement('cert-input'); if (certInput) certInput.value = ''; } function initializePage(): void { createIcons({ icons }); const fileInput = getElement('file-input'); const dropZone = getElement('drop-zone'); const backBtn = getElement('back-to-tools'); const certInput = getElement('cert-input'); const certDropZone = getElement('cert-drop-zone'); if (fileInput) { fileInput.addEventListener('change', handlePdfUpload); fileInput.addEventListener('click', () => { fileInput.value = ''; }); } if (dropZone) { dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('bg-gray-700'); }); dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('bg-gray-700'); }); dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('bg-gray-700'); const droppedFiles = e.dataTransfer?.files; if (droppedFiles && droppedFiles.length > 0) { handlePdfFile(droppedFiles[0]); } }); } if (certInput) { certInput.addEventListener('change', handleCertUpload); certInput.addEventListener('click', () => { certInput.value = ''; }); } if (certDropZone) { certDropZone.addEventListener('dragover', (e) => { e.preventDefault(); certDropZone.classList.add('bg-gray-700'); }); certDropZone.addEventListener('dragleave', () => { certDropZone.classList.remove('bg-gray-700'); }); certDropZone.addEventListener('drop', (e) => { e.preventDefault(); certDropZone.classList.remove('bg-gray-700'); const droppedFiles = e.dataTransfer?.files; if (droppedFiles && droppedFiles.length > 0) { handleCertFile(droppedFiles[0]); } }); } if (backBtn) { backBtn.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; }); } } function handlePdfUpload(e: Event): void { const input = e.target as HTMLInputElement; if (input.files && input.files.length > 0) { handlePdfFile(input.files[0]); } } async function handlePdfFile(file: File): Promise { if ( file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf') ) { showAlert('Invalid File', 'Please select a PDF file.'); return; } resetState(); state.pdfFile = file; state.pdfBytes = new Uint8Array( (await readFileAsArrayBuffer(file)) as ArrayBuffer ); updatePdfDisplay(); const customCertSection = getElement('custom-cert-section'); if (customCertSection) customCertSection.classList.remove('hidden'); createIcons({ icons }); await validateSignatures(); } function updatePdfDisplay(): void { const fileDisplayArea = getElement('file-display-area'); if (!fileDisplayArea || !state.pdfFile) return; fileDisplayArea.innerHTML = ''; const fileDiv = document.createElement('div'); fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; const infoContainer = document.createElement('div'); infoContainer.className = 'flex flex-col flex-1 min-w-0'; const nameSpan = document.createElement('div'); nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; nameSpan.textContent = state.pdfFile.name; const metaSpan = document.createElement('div'); metaSpan.className = 'text-xs text-gray-400'; metaSpan.textContent = formatBytes(state.pdfFile.size); infoContainer.append(nameSpan, metaSpan); const removeBtn = document.createElement('button'); removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.innerHTML = ''; removeBtn.onclick = () => resetState(); fileDiv.append(infoContainer, removeBtn); fileDisplayArea.appendChild(fileDiv); createIcons({ icons }); } function handleCertUpload(e: Event): void { const input = e.target as HTMLInputElement; if (input.files && input.files.length > 0) { handleCertFile(input.files[0]); } } async function handleCertFile(file: File): Promise { const validExtensions = ['.pem', '.crt', '.cer', '.der']; const hasValidExtension = validExtensions.some((ext) => file.name.toLowerCase().endsWith(ext) ); if (!hasValidExtension) { showAlert( 'Invalid Certificate', 'Please select a .pem, .crt, .cer, or .der certificate file.' ); return; } resetCertState(); state.trustedCertFile = file; try { const content = await file.text(); if (content.includes('-----BEGIN CERTIFICATE-----')) { state.trustedCert = forge.pki.certificateFromPem(content); } else { const bytes = new Uint8Array( (await readFileAsArrayBuffer(file)) as ArrayBuffer ); const derString = String.fromCharCode.apply(null, Array.from(bytes)); const asn1 = forge.asn1.fromDer(derString); state.trustedCert = forge.pki.certificateFromAsn1(asn1); } updateCertDisplay(); if (state.pdfBytes) { await validateSignatures(); } } catch (error) { console.error('Error parsing certificate:', error); showAlert('Invalid Certificate', 'Failed to parse the certificate file.'); resetCertState(); } } function updateCertDisplay(): void { const certDisplayArea = getElement('cert-display-area'); if (!certDisplayArea || !state.trustedCertFile || !state.trustedCert) return; certDisplayArea.innerHTML = ''; const certDiv = document.createElement('div'); certDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; const infoContainer = document.createElement('div'); infoContainer.className = 'flex flex-col flex-1 min-w-0'; const nameSpan = document.createElement('div'); nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; const cn = state.trustedCert.subject.getField('CN'); nameSpan.textContent = (cn?.value as string) || state.trustedCertFile.name; const metaSpan = document.createElement('div'); metaSpan.className = 'text-xs text-green-400'; metaSpan.innerHTML = 'Trusted certificate loaded'; infoContainer.append(nameSpan, metaSpan); const removeBtn = document.createElement('button'); removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.innerHTML = ''; removeBtn.onclick = async () => { resetCertState(); if (state.pdfBytes) { await validateSignatures(); } }; certDiv.append(infoContainer, removeBtn); certDisplayArea.appendChild(certDiv); createIcons({ icons }); } async function validateSignatures(): Promise { if (!state.pdfBytes) return; showLoader('Analyzing signatures...'); try { state.results = await validatePdfSignatures( state.pdfBytes, state.trustedCert ?? undefined ); displayResults(); } catch (error) { console.error('Validation error:', error); showAlert( 'Error', 'Failed to validate signatures. The file may be corrupted.' ); } finally { hideLoader(); } } function displayResults(): void { const resultsSection = getElement('results-section'); const resultsContainer = getElement('results-container'); if (!resultsSection || !resultsContainer) return; resultsContainer.innerHTML = ''; resultsSection.classList.remove('hidden'); if (state.results.length === 0) { resultsContainer.innerHTML = `

No Signatures Found

This PDF does not contain any digital signatures.

`; createIcons({ icons }); return; } const summaryDiv = document.createElement('div'); summaryDiv.className = 'mb-4 p-3 bg-gray-700 rounded-lg border border-gray-600'; const validCount = state.results.filter( (r) => r.isValid && !r.isExpired ).length; const trustVerified = state.trustedCert ? state.results.filter((r) => r.isTrusted).length : 0; let summaryHtml = `

${state.results.length} signature${state.results.length > 1 ? 's' : ''} found ${validCount} valid

`; if (state.trustedCert) { summaryHtml += `

Trust verification: ${trustVerified}/${state.results.length} signatures verified against custom certificate

`; } summaryDiv.innerHTML = summaryHtml; resultsContainer.appendChild(summaryDiv); state.results.forEach((result, index) => { const card = createSignatureCard(result, index); resultsContainer.appendChild(card); }); createIcons({ icons }); } function createSignatureCard( result: SignatureValidationResult, index: number ): HTMLElement { const card = document.createElement('div'); card.className = 'bg-gray-700 rounded-lg p-4 border border-gray-600 mb-4'; let statusColor = 'text-green-400'; let statusIcon = 'check-circle'; let statusText = 'Valid Signature'; if (!result.isValid) { if (result.cryptoVerificationStatus === 'unsupported') { statusColor = 'text-yellow-400'; statusIcon = 'alert-triangle'; statusText = 'Unverified — Unsupported Signature Algorithm'; } else { statusColor = 'text-red-400'; statusIcon = 'x-circle'; statusText = result.cryptoVerified === false ? 'Invalid — Cryptographic Verification Failed' : 'Invalid Signature'; } } else if (result.usesInsecureDigest) { statusColor = 'text-red-400'; statusIcon = 'x-circle'; statusText = 'Insecure Digest (MD5 / SHA-1)'; } else if (result.isExpired) { statusColor = 'text-yellow-400'; statusIcon = 'alert-triangle'; statusText = 'Certificate Expired'; } else if (result.isSelfSigned) { statusColor = 'text-yellow-400'; statusIcon = 'alert-triangle'; statusText = 'Self-Signed Certificate'; } const formatDate = (date: Date) => { if (!date || date.getTime() === 0) return 'Unknown'; return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }); }; let trustBadge = ''; if (state.trustedCert) { if (result.isTrusted) { trustBadge = 'Trusted'; } else { trustBadge = 'Not in trust chain'; } } card.innerHTML = `

Signature ${index + 1}

${statusText}

${ result.coverageStatus === 'full' ? 'Full Coverage' : result.coverageStatus === 'partial' ? 'Partial Coverage' : '' }${trustBadge}

Signed By

${escapeHtml(result.signerName)}

${result.signerOrg ? `

${escapeHtml(result.signerOrg)}

` : ''} ${result.signerEmail ? `

${escapeHtml(result.signerEmail)}

` : ''}

Issuer

${escapeHtml(result.issuer)}

${result.issuerOrg ? `

${escapeHtml(result.issuerOrg)}

` : ''}
${ result.signatureDate ? `

Signed On

${formatDate(result.signatureDate)}

` : '' }

Valid From

${formatDate(result.validFrom)}

Valid Until

${formatDate(result.validTo)}

${ result.reason ? `

Reason

${escapeHtml(result.reason)}

` : '' } ${ result.location ? `

Location

${escapeHtml(result.location)}

` : '' }
Technical Details

Serial Number: ${escapeHtml(result.serialNumber)}

Digest Algorithm: ${escapeHtml(result.algorithms.digest)}

Signature Algorithm: ${escapeHtml(result.algorithms.signature)}

${result.errorMessage ? `

Error: ${escapeHtml(result.errorMessage)}

` : ''} ${result.unsupportedAlgorithmReason ? `

${escapeHtml(result.unsupportedAlgorithmReason)}

` : ''}
`; return card; } function escapeHtml(str: string): string { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializePage); } else { initializePage(); }