summaryrefslogtreecommitdiff
path: root/redaction-checker.html
diff options
context:
space:
mode:
authorTadas Vilkeliskis <vilkeliskis.t@gmail.com>2026-02-08 17:02:29 -0500
committerTadas Vilkeliskis <vilkeliskis.t@gmail.com>2026-02-08 17:02:29 -0500
commit5307d1c45fcee63602b8d079956c4dc44d3ac08b (patch)
tree33c12839d47f5030e0f4ff8d69a39ec8f583769f /redaction-checker.html
parent8d752518ef6942e6fe2f1b4fc97229266f1ca114 (diff)
add redaction checker
Diffstat (limited to 'redaction-checker.html')
-rw-r--r--redaction-checker.html374
1 files changed, 374 insertions, 0 deletions
diff --git a/redaction-checker.html b/redaction-checker.html
new file mode 100644
index 0000000..9ae0f25
--- /dev/null
+++ b/redaction-checker.html
@@ -0,0 +1,374 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Redaction Checker | Just Redact</title>
+ <meta name="description" content="Check if your PDF is truly redacted. Detect hidden text layers, metadata, and unflattened elements. 100% local and private.">
+
+ <script src="https://cdn.tailwindcss.com"></script>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
+
+ <style>
+ @import url('https://fonts.googleapis.com/css2?family=Crimson+Pro:ital,wght@0,400;0,600;0,700;1,400&family=Inter:wght@400;500;600&display=swap');
+
+ body {
+ font-family: 'Inter', sans-serif;
+ color: #1f2937;
+ background-color: #f9fafb;
+ -webkit-font-smoothing: antialiased;
+ }
+
+ h1, h2, h3, h4 {
+ font-family: 'Crimson Pro', serif;
+ }
+
+ .drop-zone--over {
+ border-color: #10b981 !important;
+ background-color: #ecfdf5 !important;
+ }
+ </style>
+</head>
+<body class="flex flex-col min-h-screen">
+
+ <!-- Header -->
+ <header class="max-w-5xl mx-auto w-full px-6 py-8 flex justify-between items-center">
+ <a href="index.html" class="text-xl font-bold tracking-tight text-gray-900 font-serif flex items-center gap-2">
+ Just Redact.
+ </a>
+ <div class="flex gap-4">
+ <a href="redact.html" class="text-sm font-medium text-white bg-gray-900 hover:bg-gray-800 px-4 py-2 rounded-lg transition">Securely Redact Now</a>
+ </div>
+ </header>
+
+ <main class="flex-grow max-w-4xl mx-auto w-full px-6 pb-24">
+
+ <section class="text-center mt-8 mb-12">
+ <h1 class="text-4xl md:text-5xl font-semibold text-gray-900 mb-4">
+ Is your PDF actually redacted?
+ </h1>
+ <p class="text-lg text-gray-600 max-w-2xl mx-auto">
+ Hidden data often lurks beneath black boxes. Use our local checker to find searchable text, metadata, and unflattened layers.
+ </p>
+ </section>
+
+ <!-- Upload Zone -->
+ <div id="drop-zone" class="border-2 border-dashed border-gray-300 rounded-2xl p-12 text-center transition-all bg-white cursor-pointer hover:border-gray-400">
+ <input type="file" id="file-input" class="hidden" accept="application/pdf">
+ <div class="flex flex-col items-center">
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-gray-400 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
+ </svg>
+ <p class="text-xl font-medium text-gray-900 mb-1">Drop your PDF here</p>
+ <p class="text-sm text-gray-500">or click to browse from your device</p>
+ <p class="mt-4 text-xs text-gray-400 uppercase tracking-widest font-semibold">100% Local • No Uploads</p>
+ </div>
+ </div>
+
+ <!-- Analysis State -->
+ <div id="loading-state" class="hidden mt-12 text-center">
+ <div class="inline-block animate-spin rounded-full h-8 w-8 border-4 border-gray-300 border-t-gray-900 mb-4"></div>
+ <p class="text-gray-600 font-medium">Analyzing document safety...</p>
+ </div>
+
+ <!-- Results Dashboard -->
+ <div id="results-dashboard" class="hidden mt-12 animate-in fade-in duration-500">
+
+ <div class="grid md:grid-cols-3 gap-8">
+
+ <!-- Verdict Card -->
+ <div class="md:col-span-1 bg-white p-8 rounded-2xl shadow-sm border border-gray-100 flex flex-col items-center justify-center text-center">
+ <h2 class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-6">Security Verdict</h2>
+ <div id="verdict-icon" class="mb-4">
+ <!-- Icon injected by JS -->
+ </div>
+ <p id="verdict-text" class="text-3xl font-bold mb-2"></p>
+ <p id="health-score" class="text-sm text-gray-500"></p>
+ </div>
+
+ <!-- Details Column -->
+ <div class="md:col-span-2 space-y-6">
+
+ <!-- Searchable Text -->
+ <div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
+ <div class="flex justify-between items-center mb-4">
+ <h3 class="font-bold text-gray-900 flex items-center gap-2">
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7" />
+ </svg>
+ Searchable Text Layer
+ </h3>
+ <span id="text-badge" class="px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider"></span>
+ </div>
+ <p id="text-description" class="text-sm text-gray-600 leading-relaxed">
+ Searching for hidden text that remains selectable or searchable even if covered by black boxes.
+ </p>
+ </div>
+
+ <!-- Metadata -->
+ <div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
+ <div class="flex justify-between items-center mb-4">
+ <h3 class="font-bold text-gray-900 flex items-center gap-2">
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
+ </svg>
+ Document Metadata
+ </h3>
+ <span id="metadata-badge" class="px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider"></span>
+ </div>
+ <div id="metadata-list" class="space-y-2 mb-4 hidden">
+ <!-- Table injected by JS -->
+ </div>
+ <p id="metadata-description" class="text-sm text-gray-600 leading-relaxed">
+ Checking for hidden properties like Author, Producer, or Tool used that can leak identity or workflow details.
+ </p>
+ </div>
+
+ <!-- Annotations -->
+ <div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
+ <div class="flex justify-between items-center mb-4">
+ <h3 class="font-bold text-gray-900 flex items-center gap-2">
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
+ </svg>
+ Unflattened Annotations
+ </h3>
+ <span id="annotations-badge" class="px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider"></span>
+ </div>
+ <p id="annotations-description" class="text-sm text-gray-600 leading-relaxed">
+ Detecting shapes, highlights, or comments that are "objects" on top of text rather than being part of the image.
+ </p>
+ </div>
+
+ </div>
+ </div>
+
+ <!-- Call to Action -->
+ <div id="cta-unsafe" class="mt-12 p-8 bg-gray-900 rounded-3xl text-center text-white hidden">
+ <h3 class="text-2xl font-bold mb-4 font-serif">Hidden data detected. Fix it now.</h3>
+ <p class="text-gray-400 mb-8 max-w-xl mx-auto">
+ Your PDF isn't fully redacted. Use Just Redact to permanently flatten this document into a secure image.
+ </p>
+ <a href="redact.html" class="inline-block bg-green-600 hover:bg-green-700 text-white font-bold py-4 px-10 rounded-full shadow-lg transition-transform transform hover:-translate-y-0.5">
+ Launch Secure Redactor
+ </a>
+ </div>
+
+ <div id="cta-safe" class="mt-12 p-8 bg-green-50 border border-green-100 rounded-3xl text-center hidden">
+ <h3 class="text-2xl font-bold text-green-900 mb-4 font-serif">This document is secure.</h3>
+ <p class="text-green-700 mb-0">
+ No text layers or hidden metadata were found. This PDF appears to be properly flattened.
+ </p>
+ </div>
+
+ </div>
+
+ </main>
+
+ <!-- Footer -->
+ <footer class="bg-white py-12 border-t border-gray-100">
+ <div class="max-w-4xl mx-auto px-6 text-center text-sm text-gray-500">
+ &copy; 2026 Just Redact. All processing happens 100% in your browser.
+ </div>
+ </footer>
+
+ <script>
+ pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
+
+ const fileInput = document.getElementById('file-input');
+ const dropZone = document.getElementById('drop-zone');
+ const resultsDashboard = document.getElementById('results-dashboard');
+ const loadingState = document.getElementById('loading-state');
+
+ // UI Elements for Results
+ const verdictIcon = document.getElementById('verdict-icon');
+ const verdictText = document.getElementById('verdict-text');
+ const healthScore = document.getElementById('health-score');
+ const textBadge = document.getElementById('text-badge');
+ const textDescription = document.getElementById('text-description');
+ const metadataBadge = document.getElementById('metadata-badge');
+ const metadataList = document.getElementById('metadata-list');
+ const metadataDescription = document.getElementById('metadata-description');
+ const annotationsBadge = document.getElementById('annotations-badge');
+ const annotationsDescription = document.getElementById('annotations-description');
+ const ctaUnsafe = document.getElementById('cta-unsafe');
+ const ctaSafe = document.getElementById('cta-safe');
+
+ dropZone.addEventListener('click', () => fileInput.click());
+
+ dropZone.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ dropZone.classList.add('drop-zone--over');
+ });
+
+ ['dragleave', 'dragend'].forEach(type => {
+ dropZone.addEventListener(type, () => {
+ dropZone.classList.remove('drop-zone--over');
+ });
+ });
+
+ dropZone.addEventListener('drop', (e) => {
+ e.preventDefault();
+ dropZone.classList.remove('drop-zone--over');
+ if (e.dataTransfer.files.length) {
+ fileInput.files = e.dataTransfer.files;
+ handleFile(e.dataTransfer.files[0]);
+ }
+ });
+
+ fileInput.addEventListener('change', (e) => {
+ if (fileInput.files.length) {
+ handleFile(fileInput.files[0]);
+ }
+ });
+
+ async function handleFile(file) {
+ if (file.type !== 'application/pdf') {
+ alert('Please upload a PDF file.');
+ return;
+ }
+
+ // UI Transition
+ resultsDashboard.classList.add('hidden');
+ loadingState.classList.remove('hidden');
+ dropZone.classList.add('opacity-50', 'pointer-events-none');
+
+ const reader = new FileReader();
+ reader.onload = async function() {
+ const typedarray = new Uint8Array(this.result);
+ await analyzePDF(typedarray);
+ };
+ reader.readAsArrayBuffer(file);
+ }
+
+ async function analyzePDF(data) {
+ try {
+ const pdfDoc = await pdfjsLib.getDocument(data).promise;
+
+ let findings = {
+ wordCount: 0,
+ annotationCount: 0,
+ metadata: {},
+ metadataFound: false
+ };
+
+ // 1. Metadata Check
+ const { info, metadata } = await pdfDoc.getMetadata();
+ const sensitiveKeys = ['Author', 'Creator', 'Producer', 'Title', 'Keywords'];
+ sensitiveKeys.forEach(key => {
+ if (info[key] && info[key].trim().length > 0) {
+ findings.metadata[key] = info[key];
+ findings.metadataFound = true;
+ }
+ });
+
+ // 2. Page Analysis
+ for (let i = 1; i <= pdfDoc.numPages; i++) {
+ const page = await pdfDoc.getPage(i);
+
+ // Text Layer
+ const textContent = await page.getTextContent();
+ textContent.items.forEach(item => {
+ if (item.str && item.str.trim().length > 0) {
+ findings.wordCount += item.str.trim().split(/\s+/).length;
+ }
+ });
+
+ // Annotations
+ const annotations = await page.getAnnotations();
+ findings.annotationCount += annotations.length;
+ }
+
+ renderResults(findings);
+
+ } catch (err) {
+ console.error("Analysis failed:", err);
+ alert("Could not analyze PDF. It might be password protected or corrupted.");
+ loadingState.classList.add('hidden');
+ dropZone.classList.remove('opacity-50', 'pointer-events-none');
+ }
+ }
+
+ function renderResults(findings) {
+ loadingState.classList.add('hidden');
+ resultsDashboard.classList.remove('hidden');
+ dropZone.classList.remove('opacity-50', 'pointer-events-none');
+
+ let score = 100;
+ if (findings.wordCount > 0) score -= 50;
+
+ // Only penalize for personally identifiable metadata
+ const piiKeys = ['Author', 'Title', 'Keywords'];
+ const hasPII = piiKeys.some(key => findings.metadata[key] && findings.metadata[key].trim().length > 0);
+ if (hasPII) score -= 20;
+
+ if (findings.annotationCount > 0) score -= 30;
+
+ // Limit score
+ score = Math.max(0, score);
+
+ const isSafe = score === 100;
+
+ // 1. Verdict UI
+ verdictText.innerText = isSafe ? 'Safe' : 'Unsafe';
+ verdictText.className = `text-3xl font-bold mb-2 ${isSafe ? 'text-green-600' : 'text-red-600'}`;
+ healthScore.innerText = `Redaction Health Score: ${score}/100`;
+
+ verdictIcon.innerHTML = isSafe
+ ? `<div class="bg-green-100 p-4 rounded-full"><svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" /></svg></div>`
+ : `<div class="bg-red-100 p-4 rounded-full"><svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg></div>`;
+
+ // 2. Text Layer UI
+ if (findings.wordCount > 0) {
+ textBadge.innerText = 'Leaking';
+ textBadge.className = 'px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-red-100 text-red-700';
+ textDescription.innerHTML = `<strong>Danger:</strong> Found <span class="text-red-600 font-bold">${findings.wordCount}</span> searchable words. This document has not been flattened.`;
+ } else {
+ textBadge.innerText = 'Clean';
+ textBadge.className = 'px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-green-100 text-green-700';
+ textDescription.innerText = 'No searchable text found. The document is likely flattened into an image.';
+ }
+
+ // 3. Metadata UI
+ if (findings.metadataFound) {
+ metadataBadge.innerText = 'Visible';
+ metadataBadge.className = 'px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-yellow-100 text-yellow-700';
+ metadataList.classList.remove('hidden');
+
+ let listHtml = `<table class="w-full text-sm text-left border-collapse">`;
+ for (const [key, value] of Object.entries(findings.metadata)) {
+ listHtml += `<tr class="border-b border-gray-50"><td class="py-2 font-medium text-gray-500 w-24">${key}</td><td class="py-2 text-gray-900 truncate max-w-xs">${value}</td></tr>`;
+ }
+ listHtml += `</table>`;
+ metadataList.innerHTML = listHtml;
+ metadataDescription.innerHTML = `<strong>Warning:</strong> Document properties contain potentially identifying information.`;
+ } else {
+ metadataBadge.innerText = 'Hidden';
+ metadataBadge.className = 'px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-green-100 text-green-700';
+ metadataList.classList.add('hidden');
+ metadataDescription.innerText = 'No common sensitive metadata keys were found.';
+ }
+
+ // 4. Annotations UI
+ if (findings.annotationCount > 0) {
+ annotationsBadge.innerText = 'Found';
+ annotationsBadge.className = 'px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-yellow-100 text-yellow-700';
+ annotationsDescription.innerHTML = `<strong>Warning:</strong> <span class="font-bold">${findings.annotationCount}</span> unflattened objects found. These might be shapes or highlights used for redaction.`;
+ } else {
+ annotationsBadge.innerText = 'None';
+ annotationsBadge.className = 'px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-green-100 text-green-700';
+ annotationsDescription.innerText = 'No unflattened annotations or interactive elements detected.';
+ }
+
+ // 5. CTA Visibility
+ if (isSafe) {
+ ctaSafe.classList.remove('hidden');
+ ctaUnsafe.classList.add('hidden');
+ } else {
+ ctaSafe.classList.add('hidden');
+ ctaUnsafe.classList.remove('hidden');
+ }
+ }
+ </script>
+</body>
+</html>