PhishDestroy
deploy: Production non-AI clean release with valid HF metadata
57aaf09
Raw
History Blame Contribute Delete
73.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PDF Redaction Auditor — Taylor Wessing / Valve GDPR Leak Forensic Suite</title>
<!-- Powerful SEO & Metadata Safeguards -->
<meta name="description" content="Forensic auditing utility to expose and unmask failed visual-only PDF redactions created by Taylor Wessing LLP for Valve Corporation. Verify GDPR/DSGVO compliance and extract hidden text layers offline.">
<meta name="keywords" content="Taylor Wessing, Taylor Wessing LLP, GDPR data breach, DSGVO Datenleck, Valve Corporation, Steam data leak, Aspose.PDF redaction, redaction fail, unmask PDF, recover redacted text, HmbBfDI, Article 15 GDPR, subject access request leak, forensic PDF auditor">
<meta name="robots" content="index, follow">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:title" content="PDF Redaction Auditor & Sanitizer — GDPR Forensic Tool">
<meta property="og:description" content="Expose failed visual-only PDF redactions by Taylor Wessing LLP. Audits Aspose.PDF structures and recovers unredacted PII text offline.">
<meta property="og:image" content="https://destroyico.pages.dev/assets/logo.png">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:title" content="PDF Redaction Auditor — Taylor Wessing / Valve GDPR Leak Utility">
<meta property="twitter:description" content="Expose and sanitize insecure visual PDF redactions. Designed to audit data exposures caused by Taylor Wessing LLP.">
<!-- Branded Assets & Icons -->
<link rel="shortcut icon" href="https://destroyico.pages.dev/assets/favicon.ico" type="image/x-icon">
<link rel="icon" type="image/png" sizes="32x32" href="https://destroyico.pages.dev/assets/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="https://destroyico.pages.dev/assets/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="https://destroyico.pages.dev/assets/apple-touch-icon.png">
<link rel="manifest" href="https://destroyico.pages.dev/assets/site.webmanifest">
<!-- PDF.js & PDF-Lib Library Loader -->
<!-- We try local paths first to enable 100% internet-free offline auditing. -->
<!-- If those are missing, we dynamically fall back to CDNs. -->
<script src="lib/pdf.min.js"></script>
<script src="lib/pdf-lib.min.js"></script>
<script>
// Fallback loader to load from CDN if local files are missing
function loadFallbackScript(src, checkVar, callback) {
if (window[checkVar] === undefined) {
console.warn(`Local file/variable [${checkVar}] not found, trying CDN: ${src}`);
const script = document.createElement('script');
script.src = src;
if (callback) script.onload = callback;
document.head.appendChild(script);
} else if (callback) {
callback();
}
}
// Trigger CDNs if the local library files did not load
loadFallbackScript('https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.min.js', 'pdfjsLib', () => {
// Setup worker fallback
if (typeof pdfjsLib !== 'undefined' && !pdfjsLib.GlobalWorkerOptions.workerSrc) {
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.worker.min.js';
}
});
loadFallbackScript('https://unpkg.com/pdf-lib@1.17.1/dist/pdf-lib.min.js', 'PDFLib');
</script>
<style>
:root {
--bg-main: #000000;
--bg-surface: #0a0a0c;
--bg-card: #121214;
--border-primary: #232326;
--border-subtle: #17171a;
--accent-blue: #3b82f6;
--accent-blue-hover: #2563eb;
--accent-red: #ff4757;
--accent-green: #22c55e;
--accent-amber: #f59e0b;
--text-primary: #ffffff;
--text-secondary: #94a3b8;
--text-muted: #4b5563;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg-main);
color: var(--text-primary);
padding: 40px 20px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
.container {
max-width: 1500px;
margin: 0 auto;
}
/* Investigative Report Header Style */
header {
display: flex;
justify-content: space-between;
align-items: flex-start;
border-bottom: 1px solid var(--border-primary);
padding-bottom: 25px;
margin-bottom: 35px;
}
.brand-section {
display: flex;
align-items: center;
gap: 15px;
}
.logo-mark {
width: 38px;
height: 38px;
background-color: var(--accent-red);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
color: white;
font-size: 1.1rem;
letter-spacing: -1px;
box-shadow: 0 0 15px rgba(255, 71, 87, 0.2);
}
h1 {
font-size: 1.5rem;
font-weight: 800;
letter-spacing: -0.75px;
color: var(--text-primary);
text-transform: uppercase;
}
.tagline {
font-size: 0.8rem;
color: var(--text-secondary);
font-family: monospace;
text-transform: uppercase;
letter-spacing: 1px;
margin-top: 2px;
}
.security-badge {
background-color: rgba(34, 197, 94, 0.05);
color: var(--accent-green);
border: 1px solid rgba(34, 197, 94, 0.2);
padding: 6px 14px;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Dashboard Grid Layout */
.dashboard-grid {
display: grid;
grid-template-columns: 1fr;
gap: 30px;
}
@media (min-width: 1050px) {
.dashboard-grid {
grid-template-columns: 450px 1fr;
}
}
/* Sidebar Panels */
.sidebar {
display: flex;
flex-direction: column;
gap: 25px;
}
.card {
background-color: var(--bg-surface);
border: 1px solid var(--border-primary);
border-radius: 6px;
padding: 24px;
}
.card-header {
font-size: 0.8rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-secondary);
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border-subtle);
padding-bottom: 10px;
}
/* Upload Drop Zone */
.drop-zone {
border: 1px dashed var(--border-primary);
border-radius: 4px;
padding: 35px 20px;
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
background-color: rgba(255, 255, 255, 0.005);
}
.drop-zone:hover {
border-color: var(--accent-red);
background-color: rgba(255, 71, 87, 0.01);
}
.drop-zone-icon {
font-size: 1.5rem;
margin-bottom: 8px;
color: var(--accent-red);
}
.drop-zone p {
font-size: 0.85rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 3px;
}
.drop-zone span {
font-size: 0.7rem;
color: var(--text-muted);
}
/* Metadata & Info Items */
.info-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 8px 0;
border-bottom: 1px solid var(--border-subtle);
font-size: 0.75rem;
}
.info-label {
color: var(--text-secondary);
text-transform: uppercase;
font-weight: 600;
white-space: nowrap;
}
.info-value {
font-weight: 600;
color: var(--text-primary);
max-width: 65%;
text-align: right;
word-break: break-word;
font-family: "JetBrains Mono", "Fira Code", Consolas, monospace;
}
/* Alert Callouts */
.alert-box {
padding: 16px;
border-radius: 4px;
border: 1px solid var(--border-primary);
font-size: 0.75rem;
margin-top: 20px;
line-height: 1.5;
}
.alert-danger {
background-color: rgba(255, 71, 87, 0.02);
border-color: rgba(255, 71, 87, 0.2);
color: #ffcccc;
}
.alert-danger h4 {
color: var(--accent-red);
font-weight: 800;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.alert-safe {
background-color: rgba(34, 197, 94, 0.02);
border-color: rgba(34, 197, 94, 0.15);
color: #d1fae5;
}
.alert-safe h4 {
color: var(--accent-green);
font-weight: 800;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Document Viewer Container */
.viewer-card {
display: flex;
flex-direction: column;
align-items: center;
min-height: 600px;
background-color: var(--bg-surface);
border: 1px solid var(--border-primary);
border-radius: 6px;
padding: 24px;
width: 100%;
}
.viewer-toolbar {
display: flex;
justify-content: space-between;
width: 100%;
align-items: center;
margin-bottom: 20px;
border-bottom: 1px solid var(--border-subtle);
padding-bottom: 15px;
flex-wrap: wrap;
gap: 15px;
}
.toolbar-group {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.page-indicator {
font-size: 0.8rem;
font-weight: 700;
font-family: monospace;
color: var(--text-secondary);
}
/* Canvas Wrapper & Layering */
.canvas-container {
position: relative;
box-shadow: 0 15px 35px rgba(0,0,0,0.8);
border: 1px solid var(--border-primary);
max-width: 100%;
overflow: auto;
border-radius: 4px;
}
#pdfCanvas {
display: block;
}
/* Precision Bounding-Box Overlay Layer */
.overlay-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.overlay-box {
position: absolute;
color: var(--accent-red);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-weight: 700;
white-space: nowrap;
background-color: rgba(255, 71, 87, 0.05);
border: 1px solid rgba(255, 71, 87, 0.35);
font-size: 10px;
transform-origin: left top;
letter-spacing: -0.2px;
text-shadow: 0 0 2px #000000;
transition: all 0.1s ease;
}
/* When black bars are visually stripped, text styling adapts */
.overlay-box.unmasked-text {
color: #1e293b !important;
background-color: #ffffff !important;
border: none !important;
text-shadow: none !important;
box-shadow: 0 0 0 1px #e2e8f0;
pointer-events: auto;
}
/* Utility Buttons */
.btn-ui {
background-color: transparent;
border: 1px solid var(--border-primary);
color: var(--text-primary);
padding: 8px 16px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 700;
cursor: pointer;
transition: all 0.15s ease;
font-family: inherit;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-ui:hover:not(:disabled) {
border-color: var(--accent-blue);
color: var(--accent-blue);
}
.btn-ui:disabled {
border-color: var(--border-subtle);
color: var(--text-muted);
cursor: not-allowed;
}
.btn-action {
background-color: var(--accent-red);
border-color: var(--accent-red);
color: white;
}
.btn-action:hover:not(:disabled) {
background-color: #ff384a;
border-color: #ff384a;
box-shadow: 0 0 15px rgba(255, 71, 87, 0.3);
}
/* Form Toggles */
.toggle {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.75rem;
font-weight: 700;
color: var(--text-secondary);
cursor: pointer;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.toggle input {
cursor: pointer;
width: 14px;
height: 14px;
accent-color: var(--accent-red);
}
/* Reports Section */
.report-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.report-desc {
font-size: 0.75rem;
color: var(--text-secondary);
line-height: 1.5;
}
#welcomePanel {
color: var(--text-secondary);
margin: auto;
text-align: center;
padding: 100px 20px;
}
#welcomePanel h2 {
color: var(--text-primary);
font-size: 1rem;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
}
#welcomePanel p {
font-size: 0.75rem;
}
/* Report Templates Panels */
.report-tabs {
display: flex;
gap: 10px;
margin-bottom: 15px;
border-bottom: 1px solid var(--border-subtle);
padding-bottom: 10px;
}
.tab-btn {
background: none;
border: none;
color: var(--text-secondary);
font-size: 0.75rem;
font-weight: 700;
cursor: pointer;
padding: 5px 10px;
border-radius: 4px;
text-transform: uppercase;
}
.tab-btn.active {
background-color: rgba(59, 130, 246, 0.1);
color: var(--accent-blue);
border: 1px solid rgba(59, 130, 246, 0.2);
}
.template-viewer {
font-family: "JetBrains Mono", "Fira Code", Consolas, monospace;
background-color: #020203;
border: 1px solid var(--border-primary);
border-radius: 4px;
padding: 15px;
color: var(--text-secondary);
font-size: 0.7rem;
line-height: 1.4;
max-height: 250px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-word;
}
.copy-success {
color: var(--accent-green);
font-size: 0.7rem;
margin-top: 8px;
display: none;
font-weight: bold;
}
/* Guide Content Styles */
.guide-tab-content {
padding: 10px 0;
width: 100%;
}
.guide-heading {
font-size: 0.95rem;
color: var(--text-primary);
font-weight: 700;
margin-top: 15px;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
text-align: left;
}
.guide-text {
font-size: 0.8rem;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 12px;
text-align: left;
}
.guide-list {
margin-left: 20px;
margin-bottom: 15px;
text-align: left;
font-size: 0.8rem;
color: var(--text-secondary);
}
.guide-list li {
margin-bottom: 6px;
}
.code-block {
font-family: "JetBrains Mono", "Fira Code", Consolas, monospace;
background-color: #050507;
border: 1px solid var(--border-primary);
border-radius: 6px;
padding: 18px;
color: #cbd5e1;
font-size: 0.7rem;
line-height: 1.5;
overflow-x: auto;
white-space: pre;
margin: 15px 0;
text-align: left;
max-height: 350px;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.6);
}
.code-keyword { color: #f43f5e; font-weight: bold; }
.code-comment { color: #64748b; font-style: italic; }
.code-string { color: #10b981; }
.code-function { color: #3b82f6; }
.code-class { color: #eab308; }
</style>
</head>
<body>
<div class="container">
<header>
<div class="brand-section">
<img src="https://destroyico.pages.dev/assets/logo.png" alt="PhishDestroy Logo" style="height: 42px; width: auto; border-radius: 4px; box-shadow: 0 0 15px rgba(255, 71, 87, 0.25);">
<div>
<h1>PDF Redaction Auditor</h1>
<div class="tagline">PhishDestroy Intelligence Division • Document Sanitization Suite</div>
</div>
</div>
<div class="security-badge">🔒 100% Client-Side Engine</div>
</header>
<div class="dashboard-grid">
<!-- Sidebar: Upload, Technical Analysis, Reports -->
<div class="sidebar">
<!-- 1. Drop Zone & Input -->
<div class="card">
<div class="card-header">Document Ingest</div>
<div class="drop-zone" id="dropZone">
<div class="drop-zone-icon">📂</div>
<p>Load PDF File</p>
<span>Drag & drop or click to browse</span>
</div>
<input type="file" id="fileInput" accept=".pdf" style="display: none;">
</div>
<!-- 2. Technical Profile -->
<div class="card" id="auditPanel" style="display: none;">
<div class="card-header">Technical Profile</div>
<div class="info-list" id="metaArea"></div>
<div id="verdictBox"></div>
</div>
<!-- 3. GDPR Exposure Reports Generator -->
<div class="card" id="reportsGeneratorCard" style="display: none;">
<div class="card-header">Breach Notification Drafts</div>
<div class="report-section">
<p class="report-desc">Generate legally binding GDPR/DSGVO notifications based on the metadata of this compromised document.</p>
<div class="report-tabs">
<button class="tab-btn active" id="tabTW">Taylor Wessing</button>
<button class="tab-btn" id="tabReg">Regulator (HmbBfDI)</button>
</div>
<div class="template-viewer" id="templateViewer">Loading template...</div>
<button class="btn-ui" style="width: 100%; margin-top: 10px;" id="copyTemplateBtn">Copy Draft Text</button>
<div class="copy-success" id="copySuccessMsg">✓ Copied to clipboard!</div>
</div>
</div>
<!-- 4. Export & Utilities -->
<div class="card" id="exportPanel" style="display: none;">
<div class="card-header">Export & Sanitization</div>
<div class="report-section">
<!-- CLIENT SIDE DECENSOR DOWNLOAD -->
<p class="report-desc"><strong>Download Sanitized PDF:</strong> Strip visual black redaction shapes from this file completely inside your browser and download the unmasked version.</p>
<button class="btn-ui btn-action btn-danger" style="width: 100%; margin-bottom: 12px;" id="browserDecensorBtn">Strip & Download Clean PDF</button>
<p class="report-desc" style="border-top: 1px solid var(--border-subtle); padding-top: 12px;">Alternatively, download our offline forensic toolpack to process files via CLI on your machine.</p>
<a href="pdf-redaction-auditor.zip" id="downloadZipLink" style="text-decoration: none; width: 100%;">
<button class="btn-ui" style="width: 100%;">Download Forensic Toolpack (ZIP)</button>
</a>
<button class="btn-ui" style="width: 100%; margin-top: 10px;" id="exportReportBtn">Export Audit Log (JSON)</button>
</div>
</div>
<!-- 5. GDPR & Legal Activism Safeguard -->
<div class="card" style="border-color: rgba(255, 71, 87, 0.35);">
<div class="card-header" style="color: var(--accent-red)">⚖️ Audit & Whistleblowing Safeguard</div>
<div class="report-section">
<p class="report-desc"><strong>Forensic Audit Target:</strong> This suite is specifically compiled to audit and sanitize the systemic GDPR Article 32 personal data leaks caused by <strong>Taylor Wessing LLP</strong> and <strong>Valve Corporation (Steam)</strong>.</p>
<p class="report-desc" style="color: #ffcccc; font-weight: bold; font-size: 0.72rem; margin-top: 6px; text-transform: uppercase;">⚠️ CENSORSHIP & COVER-UP CLAUSE:</p>
<p class="report-desc" style="font-size: 0.72rem; line-height: 1.4; color: var(--text-secondary);">
Any legal complaints, DMCA takedowns, or cease-and-desist notices issued by Taylor Wessing LLP, Valve, or their legal agents to suppress this tool
<strong>will be treated as direct public evidence of attempting to hide their own technical and legal incompetence</strong>.
Such hostile actions will trigger immediate, automated mirroring across decentralized filesystems and formal complaints to the European Data Protection Board.
</p>
</div>
</div>
</div>
<!-- Main Display: Document Player & Coordinates -->
<div class="viewer-card">
<div class="viewer-toolbar" id="viewerToolbar" style="display: none;">
<div class="toolbar-group">
<button class="btn-ui" id="prevPage">Prev</button>
<span class="page-indicator" id="pageIndicator">Page 0 / 0</span>
<button class="btn-ui" id="nextPage">Next</button>
</div>
<div class="toolbar-group">
<label class="toggle" style="margin-right: 15px;">
<input type="checkbox" id="toggleOverlay" checked>
<span>Highlight Text Layer</span>
</label>
<label class="toggle">
<input type="checkbox" id="toggleVisualDecensor">
<span style="color: var(--accent-red)">Visual Decensor (Hide Black Bars)</span>
</label>
</div>
</div>
<div id="viewerSplitScreen" style="display: none; width: 100%;">
<div style="display: grid; grid-template-columns: 1fr; gap: 24px; width: 100%;" id="splitScreenGrid">
<!-- Column 1: Page Render Visualizer -->
<div style="display: flex; flex-direction: column; align-items: center; width: 100%;">
<div class="card-header" style="width: 100%; margin-bottom: 12px; font-size: 0.75rem;">Page Render Visualizer</div>
<div class="canvas-container" id="canvasContainer" style="width: 100%;">
<canvas id="pdfCanvas"></canvas>
<!-- Bounding-box layer rendering text nodes over canvas -->
<div class="overlay-layer" id="overlayLayer"></div>
</div>
</div>
<!-- Column 2: X-Ray Raw Text Extractor -->
<div style="display: flex; flex-direction: column; width: 100%;" id="xrayPane">
<div class="card-header" style="width: 100%; margin-bottom: 12px; font-size: 0.75rem; color: var(--accent-green);">📡 X-Ray Raw Text Extractor</div>
<div class="template-viewer" id="xrayTextContainer" style="flex-grow: 1; height: 100%; min-height: 500px; max-height: 800px; background-color: #050507; border-color: rgba(34, 197, 94, 0.2); color: #22c55e; white-space: pre-wrap; font-size: 0.75rem; font-family: 'JetBrains Mono', monospace; padding: 20px;">
Extracting text layer...
</div>
</div>
</div>
</div>
<div id="welcomePanel" style="width: 100%;">
<!-- SaaS Hero Section -->
<div class="welcome-header" style="text-align: center; margin-bottom: 40px;">
<h2 style="font-size: 1.8rem; font-weight: 900; color: var(--text-primary); text-transform: uppercase; letter-spacing: -0.5px; margin-bottom: 12px; line-height: 1.2;">
Stop Leaking Your Secrets.<br><span style="color: var(--accent-red);">Audit Your PDFs.</span>
</h2>
<p style="font-size: 0.9rem; color: var(--text-secondary); max-width: 650px; margin: 0 auto; margin-bottom: 20px; line-height: 1.6;">
Upload any "redacted" PDF to check if your data is actually safe, or if you just drew a fake visual black box over it.
</p>
<div style="background-color: rgba(34, 197, 94, 0.05); border: 1px solid rgba(34, 197, 94, 0.2); border-radius: 4px; padding: 8px 16px; display: inline-block; font-size: 0.7rem; font-weight: 700; color: var(--accent-green); text-transform: uppercase; letter-spacing: 1px;">
🔒 100% Secure & Local: Processing happens entirely in your browser. Files never leave your computer.
</div>
</div>
<!-- Two Auditing Modes Visual Breakdown -->
<div style="display: grid; grid-template-columns: 1fr; gap: 20px; margin-bottom: 40px;" id="welcomeGrid">
<div class="card" style="padding: 20px; text-align: left; background-color: #0d0d10;">
<h4 style="color: var(--accent-blue); font-size: 0.8rem; text-transform: uppercase; font-weight: 800; margin-bottom: 8px;">📡 Mode 1: X-Ray Scanner (Live Extraction)</h4>
<p style="font-size: 0.75rem; color: var(--text-secondary); line-height: 1.5;">
Splits your screen into a dual-pane workspace. On the left is the rendered PDF showing black redaction boxes, and on the right is a synchronized text terminal dumping the raw, unredacted text underneath.
</p>
</div>
<div class="card" style="padding: 20px; text-align: left; background-color: #0d0d10;">
<h4 style="color: var(--accent-red); font-size: 0.8rem; text-transform: uppercase; font-weight: 800; margin-bottom: 8px;">✂️ Mode 2: Layer Stripper (PDF-Lib Native)</h4>
<p style="font-size: 0.75rem; color: var(--text-secondary); line-height: 1.5;">
Surgically traverses and scans all raw indirect PDF streams inside your browser. It swaps visual vector-drawing commands with non-paint operators, completely stripping the black masks to let you download a clean, naked document.
</p>
</div>
</div>
<!-- Prominent High-SEO Case Study Card -->
<div class="card" style="border-color: rgba(245, 158, 11, 0.2); background: linear-gradient(180deg, rgba(245, 158, 11, 0.01) 0%, rgba(245, 158, 11, 0.03) 100%); text-align: left; padding: 24px; margin-bottom: 40px;">
<span style="background-color: rgba(245, 158, 11, 0.1); color: var(--accent-amber); font-weight: 800; font-size: 0.65rem; padding: 3px 8px; border-radius: 3px; text-transform: uppercase; letter-spacing: 1px; display: inline-block; margin-bottom: 12px;">Case Study #1</span>
<h3 style="font-size: 1.05rem; font-weight: 800; color: var(--text-primary); text-transform: uppercase; margin-bottom: 10px; letter-spacing: 0.5px;">The Taylor Wessing / Valve Catastrophe</h3>
<p style="font-size: 0.78rem; color: var(--text-secondary); line-height: 1.6; margin-bottom: 12px;">
Why did we build this? In October 2023, elite international law firm <strong>Taylor Wessing LLP</strong> attempted to redact sensitive GDPR documents for <strong>Valve Corporation</strong> using outdated, automated pipeline software (<em>Aspose.PDF 20.8</em>). They failed.
</p>
<p style="font-size: 0.78rem; color: var(--text-secondary); line-height: 1.6; margin-bottom: 12px;">
Instead of scrubbing the characters, they merely drew cosmetic black vector-graphic bars on top of the text. They created 830 pages of fake visual redactions, leaking thousands of unredacted private Steam account logins, emails, security logs, and telemetry directly to the public.
</p>
<p style="font-size: 0.78rem; color: var(--accent-red); line-height: 1.6; font-weight: 700; margin-bottom: 12px;">
⚠️ DIALOGUE WITH DR. PATRICK: We formally contacted Dr. Patrick (DPO/Partner) at Taylor Wessing LLP regarding this catastrophic leak. Their response confirmed that the firm is completely inadequate and has absolutely no intention of notifying affected data subjects, taking accountability, or warning the public. Therefore, the global community must proactively audit and sanitize their own files immediately. We highly advise against contacting or doing business with these clowns who are systematically unable to redact basic PDFs.
</p>
<p style="font-size: 0.78rem; color: var(--text-secondary); line-height: 1.6; font-weight: 700; margin-bottom: 15px;">
Don't make a multi-million dollar mistake like Taylor Wessing. Read our full forensic writeups and audit your files locally:
</p>
<div style="display: flex; flex-direction: column; gap: 8px;">
<a href="https://phishdestroy.medium.com/my-dog-vs-elite-gdpr-lawyers-the-valve-data-breach-nobody-is-talking-about-f6f7683d813d" target="_blank" style="color: var(--accent-amber); font-weight: 800; font-size: 0.75rem; text-decoration: underline; text-transform: uppercase; letter-spacing: 0.5px;">
📖 Part 1: Exposing the Valve Data Breach & Corporate GDPR Negligence (Medium Writeup) →
</a>
<a href="https://phishdestroy.medium.com/my-dog-vs-elite-lawyers-part-2-the-5-year-pdf-vulnerability-exposing-global-corporations-81cdad269253" target="_blank" style="color: var(--accent-amber); font-weight: 800; font-size: 0.75rem; text-decoration: underline; text-transform: uppercase; letter-spacing: 0.5px;">
📖 Part 2: Exposing the 5-Year PDF Redaction Vulnerability inside Global Corporations (Medium Writeup) →
</a>
<a href="https://phishdestroy.io/valve-profits-from-stolen-accounts" target="_blank" style="color: var(--accent-amber); font-weight: 800; font-size: 0.75rem; text-decoration: underline; text-transform: uppercase; letter-spacing: 0.5px;">
🔬 Full Case Study: Valve's Profits From Stolen Steam Accounts →
</a>
</div>
</div>
<!-- Tabs for Local / Offline Guide -->
<div class="report-tabs" style="justify-content: center; margin-bottom: 25px;">
<button class="tab-btn active" id="tabGuideIntro" onclick="switchGuideTab('intro')">Methodology</button>
<button class="tab-btn" id="tabGuidePython" onclick="switchGuideTab('python')">Offline Python CLI</button>
<button class="tab-btn" id="tabGuideJS" onclick="switchGuideTab('js')">Browser Sanitizer JS</button>
</div>
<!-- 1. Methodology Panel -->
<div id="guideIntro" class="guide-tab-content" style="text-align: left;">
<h3 class="guide-heading">Visual Masking Vulnerability Explainer</h3>
<p class="guide-text">
Modern document redaction requires the <strong>complete destruction</strong> of sensitive character sequences within the PDF's internal content stream.
</p>
<p class="guide-text">
However, automated generators and manual editors often make a critical architectural error. Instead of deleting the target characters, they programmatically query their coordinates and draw a <strong>vector rectangle</strong> filled with solid black ink (using the <code>re</code> and <code>f</code>/<code>F</code>/<code>b</code>/<code>B</code> operators) on top of the text.
</p>
<p class="guide-text">
Because PDF text extractors and search engines parse raw character streams sequentially and ignore visual drawing layers, the "redacted" information remains 100% accessible. Anyone can copy the text, search it, or run simple tools like <code>pdftotext</code> to recover the hidden data instantly.
</p>
<h3 class="guide-heading">Audit Methodology</h3>
<ol class="guide-list">
<li><strong>Ingest File:</strong> Drag and drop your target PDF into the upload card.</li>
<li><strong>Metadata Scan:</strong> We scan the document headers. Red-flag signatures (like <em>Aspose.PDF</em> or <em>Taylor Wessing LLP</em> in the producer tags) will trigger immediate warnings.</li>
<li><strong>Text-Overlay Inspection:</strong> The browser renders the PDF page canvas and overlays the transparent, searchable text nodes exactly where they are situated in the document. By checking the highlighted layer, you can see if text is hidden beneath the visual black boxes.</li>
</ol>
</div>
<!-- 2. Python CLI Panel -->
<div id="guidePython" class="guide-tab-content" style="display: none;">
<h3 class="guide-heading">Local Python CLI Auditing Tool</h3>
<p class="guide-text">
We provide a professional, offline-capable Python utility (<code>decensor.py</code>) that lets security researchers scan and clean documents locally on their own systems. No internet connection is ever used.
</p>
<h3 class="guide-heading">Setup Instructions</h3>
<ol class="guide-list">
<li>Ensure you have Python 3 installed.</li>
<li>Install PyMuPDF: <code style="background:#121214; padding:3px 6px; border-radius:3px; color:var(--accent-blue); font-family: monospace;">pip install pymupdf</code></li>
<li>Execute the tool: <code style="background:#121214; padding:3px 6px; border-radius:3px; color:var(--accent-blue); font-family: monospace;">python decensor.py -i compromised.pdf -o unmasked.pdf</code></li>
</ol>
<h3 class="guide-heading">Core Python Implementation Code</h3>
<div class="code-block"><span class="code-keyword">import</span> re, fitz
<span class="code-keyword">def</span> <span class="code-function">strip_black_bars</span>(input_path, output_path):
<span class="code-comment"># Open the compromised document offline</span>
doc = fitz.open(input_path)
<span class="code-keyword">for</span> page <span class="code-keyword">in</span> doc:
<span class="code-comment"># Iterate through each drawing content stream on the page</span>
<span class="code-keyword">for</span> stream_id <span class="code-keyword">in</span> page.get_contents():
stream_data = doc.xref_stream(stream_id)
<span class="code-comment"># Decode in Latin-1 to safely preserve any binary stream markers</span>
text = stream_data.decode(<span class="code-string">'latin-1'</span>)
<span class="code-comment"># Locate rect drawing "re" followed by fill/stroke operator (f/F/b/B)</span>
<span class="code-comment"># and swap with "n" (new path / no-fill). Preserves exact spacing/newlines!</span>
modified_text, count = re.subn(
r<span class="code-string">'\bre\s+([fFbB]\*?)(?=\s|$)'</span>,
<span class="code-keyword">lambda</span> m: <span class="code-string">f"re{m.group(0)[2:-len(m.group(1))]}n"</span>,
text
)
<span class="code-keyword">if</span> count &gt; <span class="code-string">0</span>:
doc.update_stream(stream_id, modified_text.encode(<span class="code-string">'latin-1'</span>))
<span class="code-comment"># Write output with stream deflation and structural optimization</span>
doc.save(output_path, garbage=<span class="code-string">4</span>, deflate=<span class="code-keyword">True</span>, clean=<span class="code-keyword">True</span>)
doc.close()</div>
</div>
<!-- 3. JS Panel -->
<div id="guideJS" class="guide-tab-content" style="display: none;">
<h3 class="guide-heading">Browser-Based 100% Client-Side Engine</h3>
<p class="guide-text">
The web version performs the exact same structural sanitization directly in your browser. Using the <code>pdf-lib</code> library, the file is parsed, sanitized, and serialized in-memory. Because this is executed in your client sandbox, your files never leave your computer.
</p>
<h3 class="guide-heading">Core JavaScript Sanitizer Code</h3>
<div class="code-block"><span class="code-keyword">async function</span> <span class="code-function">sanitizePdfClientSide</span>(rawPdfBytes) {
<span class="code-comment">// Load document from local byte array into pdf-lib</span>
<span class="code-keyword">const</span> { PDFDocument, PDFName, decodePDFRawStream } = PDFLib;
<span class="code-keyword">const</span> pdfDocInstance = <span class="code-keyword">await</span> PDFDocument.load(rawPdfBytes);
<span class="code-keyword">const</span> context = pdfDocInstance.context;
<span class="code-keyword">const</span> indirectObjects = context.enumerateIndirectObjects();
<span class="code-comment">// Enumerate and scan ALL indirect streams in the document context (Page Contents, Form XObjects, patterns)</span>
<span class="code-keyword">for</span> (<span class="code-keyword">let</span> i = 0; i &lt; indirectObjects.length; i++) {
<span class="code-keyword">const</span> [ref, pdfObject] = indirectObjects[i];
// Duck-typing check: Is this object a stream? (survives JS minification/obfuscation)
<span class="code-keyword">if</span> (pdfObject &amp;&amp; <span class="code-keyword">typeof</span> pdfObject.getContents === <span class="code-string">'function'</span> &amp;&amp; pdfObject.dict) {
<span class="code-keyword">const</span> dict = pdfObject.dict;
<span class="code-keyword">const</span> type = dict.get(PDFName.of(<span class="code-string">'Type'</span>));
<span class="code-keyword">const</span> subtype = dict.get(PDFName.of(<span class="code-string">'Subtype'</span>));
<span class="code-comment">// Skip binary streams like Fonts, Images, and Halftones to prevent overhead</span>
<span class="code-keyword">if</span> (type === PDFName.of(<span class="code-string">'Font'</span>) || subtype === PDFName.of(<span class="code-string">'Image'</span>) || type === PDFName.of(<span class="code-string">'Halftone'</span>)) {
<span class="code-keyword">continue</span>;
}
<span class="code-keyword">try</span> {
<span class="code-comment">// Decompress and read raw bytes using the official decodePDFRawStream decoder</span>
<span class="code-comment">const rawData = decodePDFRawStream(pdfObject).decode();</span>
<span class="code-comment">// Decode losslessly using Latin-1 byte mapping</span>
<span class="code-comment">const text = Array.from(rawData, byte =&gt; String.fromCharCode(byte)).join('');</span>
<span class="code-comment">// Locate "re" (rectangle) drawing instructions followed by filled operators (f, F, b, B, f*, F*, b*, B*)</span>
// and replace the filling operator with "n" (new path / no-fill), preserving exact separator spacing/newlines</span>
<span class="code-keyword">const</span> modifiedText = text.replace(<span class="code-string">/re(\s+)([fFbB]\*?)(?=\s|$)/g</span>, <span class="code-string">'re$1n'</span>);
<span class="code-keyword">if</span> (text !== modifiedText) {
<span class="code-comment">// Losslessly re-encode Latin-1 string back to Uint8Array bytes</span>
<span class="code-keyword">const</span> modifiedData = <span class="code-keyword">new</span> <span class="code-class">Uint8Array</span>(modifiedText.length);
<span class="code-keyword">for</span> (<span class="code-keyword">let</span> k = 0; k &lt; modifiedText.length; k++) {
modifiedData[k] = modifiedText.charCodeAt(k) &amp; <span class="code-string">0xff</span>;
}
<span class="code-comment">// Create a new compressed stream</span>
<span class="code-keyword">const</span> newStreamObj = context.flateStream(modifiedData);
<span class="code-comment">// Preserve original stream dictionary keys (like Subtype, BBox, Resources etc. for Form XObjects)</span>
<span class="code-keyword">const</span> keys = dict.keys();
<span class="code-keyword">for</span> (<span class="code-keyword">let</span> k = 0; k &lt; keys.length; k++) {
<span class="code-keyword">const</span> key = keys[k];
if (key !== PDFName.of('Filter') && key !== PDFName.of('Length')) {
newStreamObj.dict.set(key, dict.get(key));
}
}
// Assign the new stream back to the original reference in-place
context.assign(ref, newStreamObj);
}
} catch (err) {
continue;
}
}
}
<span class="code-comment">// Recompress stream structures and serialize back to bytes</span>
<span class="code-keyword">return await</span> pdfDocInstance.save();
}</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Tab switching for the local guide walkthrough
function switchGuideTab(tabId) {
document.getElementById('tabGuideIntro').classList.toggle('active', tabId === 'intro');
document.getElementById('tabGuidePython').classList.toggle('active', tabId === 'python');
document.getElementById('tabGuideJS').classList.toggle('active', tabId === 'js');
document.getElementById('guideIntro').style.display = tabId === 'intro' ? 'block' : 'none';
document.getElementById('guidePython').style.display = tabId === 'python' ? 'block' : 'none';
document.getElementById('guideJS').style.display = tabId === 'js' ? 'block' : 'none';
}
// Set worker source path offline-first. PDF.js will automatically fall back to threadless fake-worker mode if worker files fail CORS checks in local file:// browser access.
if (typeof pdfjsLib !== 'undefined') {
pdfjsLib.GlobalWorkerOptions.workerSrc = 'lib/pdf.worker.min.js';
}
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const auditPanel = document.getElementById('auditPanel');
const exportPanel = document.getElementById('exportPanel');
const reportsGeneratorCard = document.getElementById('reportsGeneratorCard');
const metaArea = document.getElementById('metaArea');
const verdictBox = document.getElementById('verdictBox');
const viewerToolbar = document.getElementById('viewerToolbar');
const canvasContainer = document.getElementById('canvasContainer');
const pdfCanvas = document.getElementById('pdfCanvas');
const overlayLayer = document.getElementById('overlayLayer');
const welcomePanel = document.getElementById('welcomePanel');
const pageIndicator = document.getElementById('pageIndicator');
const prevPageBtn = document.getElementById('prevPage');
const nextPageBtn = document.getElementById('nextPage');
const toggleOverlay = document.getElementById('toggleOverlay');
const toggleVisualDecensor = document.getElementById('toggleVisualDecensor');
const exportReportBtn = document.getElementById('exportReportBtn');
const browserDecensorBtn = document.getElementById('browserDecensorBtn');
const tabTW = document.getElementById('tabTW');
const tabReg = document.getElementById('tabReg');
const templateViewer = document.getElementById('templateViewer');
const copyTemplateBtn = document.getElementById('copyTemplateBtn');
const copySuccessMsg = document.getElementById('copySuccessMsg');
let pdfDoc = null;
let rawPdfBytes = null;
let fileName = "";
let pageNum = 1;
let pageRendering = false;
let pageNumPending = null;
let auditLog = {};
let activeTab = "TW"; // Default template tab
const ctx = pdfCanvas.getContext('2d');
// PDF Date Parser Helper (Converts D:YYYYMMDDHHmmss to Normal String)
function formatPDFDate(pdfDate) {
if (!pdfDate) return "N/A";
// Remove D: if present
let dateStr = pdfDate.startsWith("D:") ? pdfDate.substring(2) : pdfDate;
// Clean symbols like single quotes
dateStr = dateStr.replace(/[']/g, "");
if (dateStr.length < 8) return pdfDate;
const year = dateStr.substring(0, 4);
const monthNum = dateStr.substring(4, 6);
const day = dateStr.substring(6, 8);
let timeStr = "";
let tzStr = "";
if (dateStr.length >= 14) {
const hour = dateStr.substring(8, 10);
const min = dateStr.substring(10, 12);
const sec = dateStr.substring(12, 14);
timeStr = `, ${hour}:${min}:${sec}`;
// Handle timezone offset (e.g. +0200)
if (dateStr.length > 14) {
const tzSign = dateStr.substring(14, 15);
if (tzSign === "+" || tzSign === "-") {
const tzHours = dateStr.substring(15, 17);
const tzMins = dateStr.substring(17, 19) || "00";
tzStr = ` (UTC${tzSign}${tzHours}:${tzMins})`;
}
}
}
const months = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
const monthName = months[parseInt(monthNum, 10) - 1] || monthNum;
return `${monthName} ${parseInt(day, 10)}, ${year}${timeStr}${tzStr}`;
}
// Parse raw timestamp to Date object for delta calculation
function pdfDateToJSDate(pdfDate) {
if (!pdfDate) return null;
let dateStr = pdfDate.startsWith("D:") ? pdfDate.substring(2) : pdfDate;
if (dateStr.length < 14) return null;
const year = parseInt(dateStr.substring(0, 4), 10);
const month = parseInt(dateStr.substring(4, 6), 10) - 1;
const day = parseInt(dateStr.substring(6, 8), 10);
const hour = parseInt(dateStr.substring(8, 10), 10);
const min = parseInt(dateStr.substring(10, 12), 10);
const sec = parseInt(dateStr.substring(12, 14), 10);
return new Date(Date.UTC(year, month, day, hour, min, sec));
}
// Format delta between two dates in human readable form
function formatTimeDelta(creationDateStr, modDateStr) {
const d1 = pdfDateToJSDate(creationDateStr);
const d2 = pdfDateToJSDate(modDateStr);
if (!d1 || !d2) return null;
const diffMs = Math.abs(d2 - d1);
const diffSecs = Math.floor(diffMs / 1000);
if (diffSecs < 60) return `${diffSecs} seconds`;
const diffMins = Math.floor(diffSecs / 60);
const remainingSecs = diffSecs % 60;
return `${diffMins} min ${remainingSecs} sec`;
}
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.style.borderColor = 'var(--accent-red)';
});
dropZone.addEventListener('dragleave', () => {
dropZone.style.borderColor = 'var(--border-primary)';
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.style.borderColor = 'var(--border-primary)';
if (e.dataTransfer.files.length) {
handleFileLoad(e.dataTransfer.files[0]);
}
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length) {
handleFileLoad(e.target.files[0]);
}
});
function handleFileLoad(file) {
welcomePanel.style.display = 'none';
auditPanel.style.display = 'block';
exportPanel.style.display = 'block';
reportsGeneratorCard.style.display = 'block';
metaArea.innerHTML = '<span style="font-size:0.75rem;color:var(--text-secondary)">Parsing document index...</span>';
verdictBox.innerHTML = '';
viewerToolbar.style.display = 'none';
document.getElementById('viewerSplitScreen').style.display = 'none';
fileName = file.name;
const reader = new FileReader();
reader.onload = function() {
rawPdfBytes = new Uint8Array(this.result);
pdfjsLib.getDocument({data: rawPdfBytes}).promise.then(function(pdf) {
pdfDoc = pdf;
pageNum = 1;
pdf.getMetadata().then(function(meta) {
processMetadata(meta, pdf.numPages);
});
viewerToolbar.style.display = 'flex';
document.getElementById('viewerSplitScreen').style.display = 'block';
renderDocumentPage(pageNum);
}).catch(function(err) {
metaArea.innerHTML = `<span style="color: var(--accent-red); font-size:0.75rem;">Parser error: ${err.message}</span>`;
});
};
reader.readAsArrayBuffer(file);
}
function processMetadata(meta, totalPages) {
const info = meta.info || {};
const rawCreation = info.CreationDate;
const rawMod = info.ModDate;
const formattedCreation = formatPDFDate(rawCreation);
const formattedMod = formatPDFDate(rawMod);
const timeGap = formatTimeDelta(rawCreation, rawMod);
let html = `
<div class="info-item">
<div class="info-label">Filename</div>
<div class="info-value" style="color:var(--accent-blue)">${fileName}</div>
</div>
<div class="info-item">
<div class="info-label">Page Count</div>
<div class="info-value">${totalPages}</div>
</div>
<div class="info-item">
<div class="info-label">Creator</div>
<div class="info-value">${info.Creator || 'N/A'}</div>
</div>
<div class="info-item">
<div class="info-label">Producer</div>
<div class="info-value" style="color:var(--accent-blue)">${info.Producer || 'N/A'}</div>
</div>
<div class="info-item">
<div class="info-label">Created</div>
<div class="info-value">${formattedCreation}</div>
</div>
`;
if (formattedMod && formattedMod !== "N/A" && formattedMod !== formattedCreation) {
html += `
<div class="info-item">
<div class="info-label">Modified</div>
<div class="info-value">${formattedMod}</div>
</div>
`;
if (timeGap) {
html += `
<div class="info-item">
<div class="info-label">Processing Time</div>
<div class="info-value" style="color:var(--accent-amber)">${timeGap} (Batch Pipeline)</div>
</div>
`;
}
}
metaArea.innerHTML = html;
// Populate global audit log
auditLog = {
filename: fileName,
pageCount: totalPages,
metadata: {
creator: info.Creator || null,
producer: info.Producer || null,
creationDate: formattedCreation,
modificationDate: formattedMod,
processingTime: timeGap || "Single-session export"
},
threatAnalysis: {}
};
const producer = (info.Producer || '').toLowerCase();
if (producer.includes('aspose.pdf') || producer.includes('aspose')) {
auditLog.threatAnalysis = {
rating: 'HIGH',
issue: 'Insecure Exporter Signature',
details: 'Document generated by Aspose.PDF. High probability of visual-only redaction overlays.'
};
verdictBox.innerHTML = `
<div class="alert-box alert-danger">
<h4>⚠️ CRITICAL EXPOSURE RISK</h4>
<p>Insecure Exporter Signature detected (<b>${info.Producer}</b>).</p>
<p style="margin-top: 6px;">Visual redactions from this exporter are frequently layered as vector shapes, leaving character arrays fully copyable and extractable.</p>
</div>
`;
} else {
auditLog.threatAnalysis = {
rating: 'UNKNOWN',
issue: 'Signature Untracked',
details: 'Exporter signature is not recognized. Full visual inspection required.'
};
verdictBox.innerHTML = `
<div class="alert-box alert-safe" style="background-color:rgba(148,163,184,0.01);border-color:var(--border-primary);color:var(--text-secondary);">
<h4 style="color:var(--text-primary)">🔍 SIGNATURE UNTRACKED</h4>
<p>Exporter: <b>${info.Producer || 'Unknown'}</b>.</p>
<p style="margin-top: 6px;">No known automatic vulnerabilities for this signature. Proceed with manual verification using the text highlight overlay.</p>
</div>
`;
}
// Generate the breach notification drafts
updateReportTemplates(info, formattedCreation, totalPages);
}
// Generate Formal Breach Notification Draft Letters
function updateReportTemplates(info, formattedCreation, totalPages) {
const d_producer = info.Producer || "Aspose.PDF for .NET 20.8";
const d_creator = info.Creator || "Aspose Ltd.";
const twTemplate = `To: Taylor Wessing LLP (DPO / Compliance Team)
Subject: URGENT: NOTICE OF PERSONAL DATA BREACH (GDPR ART. 33 / ART. 34)
Dear Data Protection Officer,
We are writing to formally notify your firm of a critical security exposure and Personal Data Breach (under GDPR Article 4(12)) identified in a document processed and dispatched by your firm.
TECHNICAL DETAILS OF DISCLOSED DOCUMENT:
- File Audited: ${fileName}
- Page Count: ${totalPages}
- Software Signature (Producer): ${d_producer}
- Software Signature (Creator): ${d_creator}
- Creation Timestamp: ${formattedCreation}
NATURE OF SECURITY VIOLATION:
Forensic analysis of the page content streams reveals a total "Redaction Failure" (visual-only masking). Your system applied vector rectangles (re & f PDF operators) over sensitive text blocks instead of sanitizing the underlying character stream. As a result, 100% of the redacted Personally Identifiable Information (PII)—including private names, accounts, telemetry, and emails—remains completely intact in the raw text layer, searchable, and extractable via simple text-extraction utilities.
LEGAL LIABILITY AND CONSEQUENCES:
This constitutes an active data leak under GDPR Article 32 (Security of Processing), failing to implement appropriate technical measures. Given that these documents contain sensitive telemetry and identifiers of third parties, this exposure triggers mandatory notification to the supervisory authority under GDPR Article 33 and poses severe civil liability risks under GDPR Article 82.
We demand that you immediately:
1. Cease the distribution of all documents processed via this insecure Aspose pipeline.
2. Formally notify the competent supervisory authority (HmbBfDI) of this breach.
3. Review and permanently sanitize all outstanding files using structural content destruction.
Sincerely,
PhishDestroy Intelligence Suite / Independent Audit Team`;
const regTemplate = `To: Hamburg Commissioner for Data Protection and Freedom of Information (HmbBfDI)
Subject: COMPLAINT UNDER GDPR ARTICLE 77 — SYSTEMIC DATA BREACH BY VALVE CORP & TAYLOR WESSING
Dear Commissioner,
We are formally submitting a data protection complaint under GDPR Article 77 regarding a systemic, severe personal data breach resulting from insecure processing practices (GDPR Article 32) by Valve Corporation (Steam) and their legal counsel, Taylor Wessing LLP.
COMPROMISED DOCUMENT METADATA:
- Source File: ${fileName}
- Page Count: ${totalPages}
- Generating Application (Producer): ${d_producer}
- Primary Author (Creator): ${d_creator}
- Date of Origin: ${formattedCreation}
EVIDENCE OF TECHNICAL FAILURE:
The respondents dispatched an 830-page PDF document to an individual in response to a GDPR Article 15 Subject Access Request. In an attempt to protect third-party privacy, the respondents attempted to redact sensitive data (names, login credentials, emails, and logs).
However, forensic analysis of the file confirms a catastrophic "Redaction Fail": the respondents used visual masking (overlaying black vector shapes) instead of structural sanitization. The raw text layer underneath is 100% intact, readable, and extractable.
This failure has compromised the private data of thousands of Steam users, exposing them to active de-anonymization and targeted security threats.
CORRECTIVE ACTIONS REQUESTED:
We request that the HmbBfDI initiates an immediate investigation under GDPR Article 58 to:
1. Order Valve Corp and Taylor Wessing to suspend the processing and dispatch of all PDF documents generated through this insecure Aspose.PDF workflow.
2. Impose corrective administrative sanctions under GDPR Article 83 for systematic failure to implement privacy-by-design (GDPR Article 25).
Sincerely,
PhishDestroy Intelligence Suite / Independent Audit Team`;
// Associate templates to elements
tabTW.onclick = () => {
activeTab = "TW";
tabTW.className = "tab-btn active";
tabReg.className = "tab-btn";
templateViewer.innerText = twTemplate;
};
tabReg.onclick = () => {
activeTab = "Reg";
tabReg.className = "tab-btn active";
tabTW.className = "tab-btn";
templateViewer.innerText = regTemplate;
};
// Initialize active tab text
if (activeTab === "TW") {
templateViewer.innerText = twTemplate;
} else {
templateViewer.innerText = regTemplate;
}
}
// Copy template to clipboard
copyTemplateBtn.addEventListener('click', () => {
navigator.clipboard.writeText(templateViewer.innerText).then(() => {
copySuccessMsg.style.display = 'block';
setTimeout(() => {
copySuccessMsg.style.display = 'none';
}, 2500);
});
});
function renderDocumentPage(num) {
pageRendering = true;
pageIndicator.textContent = `Page ${num} / ${pdfDoc.numPages}`;
prevPageBtn.disabled = (num <= 1);
nextPageBtn.disabled = (num >= pdfDoc.numPages);
pdfDoc.getPage(num).then(function(page) {
const viewport = page.getViewport({scale: 1.5});
pdfCanvas.height = viewport.height;
pdfCanvas.width = viewport.width;
const renderContext = {
canvasContext: ctx,
viewport: viewport
};
const renderTask = page.render(renderContext);
renderTask.promise.then(function() {
pageRendering = false;
if (pageNumPending !== null) {
renderDocumentPage(pageNumPending);
pageNumPending = null;
}
overlayTextLayer(page, viewport);
});
});
}
function overlayTextLayer(page, viewport) {
overlayLayer.innerHTML = '';
page.getTextContent().then(function(textContent) {
const items = textContent.items;
const isVisualDecensorActive = toggleVisualDecensor.checked;
// X-Ray Text Accumulators
let xrayText = "";
let lastY = -1;
items.forEach(function(item) {
const tx = pdfjsLib.Util.transform(
pdfjsLib.Util.transform(viewport.transform, item.transform),
[1, 0, 0, -1, 0, 0]
);
const el = document.createElement('div');
el.className = 'overlay-box';
el.innerText = item.str;
el.style.left = `${tx[4]}px`;
el.style.top = `${tx[5] - item.height}px`;
const fontSize = Math.sqrt(tx[0]*tx[0] + tx[1]*tx[1]);
el.style.fontSize = `${fontSize}px`;
// If visual decensor is active, we style the boxes to act as whiteouts on the canvas
if (isVisualDecensorActive) {
el.classList.add('unmasked-text');
// Compute approximate dimensions to whiteout the canvas area
const widthApprox = item.width * viewport.scale;
// Draw a white block directly onto the canvas at these coordinates to cover up the black vector fill!
ctx.fillStyle = "#ffffff";
// Map transform coordinates back to canvas dimensions
ctx.fillRect(tx[4], tx[5] - fontSize, widthApprox, fontSize * 1.3);
}
overlayLayer.appendChild(el);
// Track line coordinates for X-Ray plain text rendering
const yCoord = tx[5];
if (lastY !== -1 && Math.abs(yCoord - lastY) > 5) {
xrayText += "\n";
}
xrayText += item.str + " ";
lastY = yCoord;
});
// Display raw text in X-Ray Pane
const xrayTextContainer = document.getElementById('xrayTextContainer');
if (xrayText.trim()) {
xrayTextContainer.innerText = xrayText;
xrayTextContainer.style.color = "var(--accent-green)";
} else {
xrayTextContainer.innerText = "[EMPTY PAGE - NO TEXT DETECTED UNDERNEATH LAYERS]";
xrayTextContainer.style.color = "var(--text-muted)";
}
setOverlayVisibility();
});
}
function setOverlayVisibility() {
overlayLayer.style.display = toggleOverlay.checked ? 'block' : 'none';
}
toggleOverlay.addEventListener('change', setOverlayVisibility);
// Toggle Visual Decensor (redraw page to apply canvas fills)
toggleVisualDecensor.addEventListener('change', () => {
if (pdfDoc) {
renderDocumentPage(pageNum);
}
});
function queueRender(num) {
if (pageRendering) {
pageNumPending = num;
} else {
renderDocumentPage(num);
}
}
prevPageBtn.addEventListener('click', () => {
if (pageNum <= 1) return;
pageNum--;
queueRender(pageNum);
});
nextPageBtn.addEventListener('click', () => {
if (pageNum >= pdfDoc.numPages) return;
pageNum++;
queueRender(pageNum);
});
// JSON Export
exportReportBtn.addEventListener('click', function() {
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(auditLog, null, 4));
const a = document.createElement('a');
a.setAttribute("href", dataStr);
a.setAttribute("download", `${fileName.replace(/\.[^/.]+$/, "")}_audit_report.json`);
document.body.appendChild(a);
a.click();
a.remove();
});
// ==========================================
// PDF-LIB DECENSOR (100% Client-Side File Sanitizer)
// ==========================================
browserDecensorBtn.addEventListener('click', async function() {
if (!rawPdfBytes) return;
browserDecensorBtn.innerText = "Processing File Layers...";
browserDecensorBtn.disabled = true;
try {
// Load the raw PDF document into pdf-lib
const { PDFDocument, PDFName, decodePDFRawStream } = PDFLib;
const pdfDocInstance = await PDFDocument.load(rawPdfBytes);
const context = pdfDocInstance.context;
const indirectObjects = context.enumerateIndirectObjects();
let strippedCount = 0;
// Enumerate and scan ALL indirect streams in the document context (Page Contents, Form XObjects, patterns)
for (let i = 0; i < indirectObjects.length; i++) {
const [ref, pdfObject] = indirectObjects[i];
// Duck-typing check: Is this object a stream? (has getContents and dict, survives minification)
if (pdfObject && typeof pdfObject.getContents === 'function' && pdfObject.dict) {
const dict = pdfObject.dict;
const type = dict.get(PDFName.of('Type'));
const subtype = dict.get(PDFName.of('Subtype'));
// Skip binary streams like Fonts, Images, and Halftones to prevent overhead
if (type === PDFName.of('Font') || subtype === PDFName.of('Image') || type === PDFName.of('Halftone')) {
continue;
}
try {
// Decompress and read raw bytes using the official decodePDFRawStream decoder
const decodedStream = decodePDFRawStream(pdfObject);
const rawData = decodedStream.decode();
// Decode losslessly using Latin-1 byte mapping
const text = Array.from(rawData, byte => String.fromCharCode(byte)).join('');
// Locate "re" (rectangle) drawing instructions followed by filled operators (f, F, b, B, f*, F*, b*, B*)
// and replace the filling operator with "n" (new path / no-fill), preserving exact separator spacing/newlines
const modifiedText = text.replace(/re(\s+)([fFbB]\*?)(?=\s|$)/g, 're$1n');
if (text !== modifiedText) {
// Losslessly re-encode Latin-1 string back to Uint8Array bytes
const modifiedData = new Uint8Array(modifiedText.length);
for (let k = 0; k < modifiedText.length; k++) {
modifiedData[k] = modifiedText.charCodeAt(k) & 0xff;
}
// Create a new compressed stream
const newStreamObj = context.flateStream(modifiedData);
// Preserve original stream dictionary keys (like Subtype, BBox, Resources etc. for Form XObjects)
const keys = dict.keys();
for (let k = 0; k < keys.length; k++) {
const key = keys[k];
if (key !== PDFName.of('Filter') && key !== PDFName.of('Length')) {
newStreamObj.dict.set(key, dict.get(key));
}
}
// Assign the new stream back to the original reference in-place
context.assign(ref, newStreamObj);
strippedCount++;
}
} catch (err) {
// Safe skip if stream decompress/parse fails
continue;
}
}
}
console.log(`[+] Stripped content and Form XObject streams across ${strippedCount} elements.`);
// Serialize the modified PDF to bytes (automatically recompresses and recalculates xref offsets!)
const modifiedPdfBytes = await pdfDocInstance.save();
// Trigger file download in browser
const blob = new Blob([modifiedPdfBytes], {type: 'application/pdf'});
const downloadUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
let name = fileName || "document.pdf";
let strippedName = name.replace(/\.[^/.]+$/, "") + "_unmasked.pdf";
a.download = strippedName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(downloadUrl);
browserDecensorBtn.innerText = "Strip & Download Clean PDF";
browserDecensorBtn.disabled = false;
} catch (error) {
console.error("[-] Client-side decensor error:", error);
browserDecensorBtn.innerText = "Error: Strip Failed";
browserDecensorBtn.disabled = false;
alert("Failed to sanitize PDF client-side: " + error.message + "\nPlease use the offline Python CLI tool instead.");
}
});
</script>
</body>
</html>