Auto-redacts PII (emails, credit cards, phone numbers, API keys) from browser content in real-time with visual overlays
Size
26.8 KB
Version
1.0.1
Created
Jan 8, 2026
Updated
13 days ago
1// ==UserScript==
2// @name ShieldFlow - Real-Time PII Redaction
3// @description Auto-redacts PII (emails, credit cards, phone numbers, API keys) from browser content in real-time with visual overlays
4// @version 1.0.1
5// @match *://*/*
6// @icon https://www.gstatic.com/lamda/images/gemini_sparkle_aurora_33f86dc0c0257da337c63.svg
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 // ============================================
12 // SHIELDFLOW - REAL-TIME PII REDACTION ENGINE
13 // ============================================
14
15 console.log('ShieldFlow: Initializing PII Redaction Engine...');
16
17 // Configuration and State Management
18 const CONFIG = {
19 storageKeys: {
20 enabled: 'shieldflow_enabled',
21 emailsEnabled: 'shieldflow_emails',
22 creditCardsEnabled: 'shieldflow_creditcards',
23 phonesEnabled: 'shieldflow_phones',
24 apiKeysEnabled: 'shieldflow_apikeys',
25 excludedSites: 'shieldflow_excluded_sites'
26 },
27 overlayClass: 'shieldflow-redacted',
28 dataAttributes: {
29 type: 'data-shieldflow-type',
30 original: 'data-shieldflow-original'
31 }
32 };
33
34 // PII Detection Patterns (Optimized Regex)
35 const PATTERNS = {
36 email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
37 creditCard: /\b(?:\d{4}[-\s]?){3}\d{4}\b/g,
38 phone: /\b(?:\+?1[-.\s]?)?\(?([0-9]{3})\)?[-.\s]?([0-9]{3})[-.\s]?([0-9]{4})\b/g,
39 apiKeys: {
40 aws: /\b(AKIA[0-9A-Z]{16})\b/g,
41 openai: /\b(sk-[a-zA-Z0-9]{48})\b/g,
42 stripe: /\b(sk_live_[a-zA-Z0-9]{24,})\b/g,
43 generic: /\b([a-zA-Z0-9_-]{32,})\b/g
44 }
45 };
46
47 // State object to hold current settings
48 let state = {
49 enabled: true,
50 emailsEnabled: true,
51 creditCardsEnabled: true,
52 phonesEnabled: true,
53 apiKeysEnabled: true,
54 excludedSites: [],
55 processedNodes: new WeakSet()
56 };
57
58 // ============================================
59 // STORAGE MANAGEMENT
60 // ============================================
61
62 async function loadSettings() {
63 try {
64 const enabled = await GM.getValue(CONFIG.storageKeys.enabled, true);
65 const emailsEnabled = await GM.getValue(CONFIG.storageKeys.emailsEnabled, true);
66 const creditCardsEnabled = await GM.getValue(CONFIG.storageKeys.creditCardsEnabled, true);
67 const phonesEnabled = await GM.getValue(CONFIG.storageKeys.phonesEnabled, true);
68 const apiKeysEnabled = await GM.getValue(CONFIG.storageKeys.apiKeysEnabled, true);
69 const excludedSites = await GM.getValue(CONFIG.storageKeys.excludedSites, '[]');
70
71 state.enabled = enabled;
72 state.emailsEnabled = emailsEnabled;
73 state.creditCardsEnabled = creditCardsEnabled;
74 state.phonesEnabled = phonesEnabled;
75 state.apiKeysEnabled = apiKeysEnabled;
76 state.excludedSites = JSON.parse(excludedSites);
77
78 console.log('ShieldFlow: Settings loaded', state);
79 } catch (error) {
80 console.error('ShieldFlow: Error loading settings', error);
81 }
82 }
83
84 async function saveSettings() {
85 try {
86 await GM.setValue(CONFIG.storageKeys.enabled, state.enabled);
87 await GM.setValue(CONFIG.storageKeys.emailsEnabled, state.emailsEnabled);
88 await GM.setValue(CONFIG.storageKeys.creditCardsEnabled, state.creditCardsEnabled);
89 await GM.setValue(CONFIG.storageKeys.phonesEnabled, state.phonesEnabled);
90 await GM.setValue(CONFIG.storageKeys.apiKeysEnabled, state.apiKeysEnabled);
91 await GM.setValue(CONFIG.storageKeys.excludedSites, JSON.stringify(state.excludedSites));
92 console.log('ShieldFlow: Settings saved');
93 } catch (error) {
94 console.error('ShieldFlow: Error saving settings', error);
95 }
96 }
97
98 function isCurrentSiteExcluded() {
99 const currentHost = window.location.hostname;
100 return state.excludedSites.some(site => currentHost.includes(site));
101 }
102
103 // ============================================
104 // CSS INJECTION
105 // ============================================
106
107 function injectStyles() {
108 const styles = `
109 .shieldflow-redacted {
110 position: relative;
111 display: inline-block;
112 }
113
114 .shieldflow-redacted::before {
115 content: '';
116 position: absolute;
117 top: -2px;
118 left: -2px;
119 right: -2px;
120 bottom: -2px;
121 background: rgba(0, 0, 0, 0.85);
122 backdrop-filter: blur(8px);
123 -webkit-backdrop-filter: blur(8px);
124 border-radius: 3px;
125 z-index: 999999;
126 pointer-events: none;
127 }
128
129 .shieldflow-redacted::after {
130 content: '🔒';
131 position: absolute;
132 top: 50%;
133 left: 50%;
134 transform: translate(-50%, -50%);
135 font-size: 12px;
136 z-index: 1000000;
137 pointer-events: none;
138 }
139
140 /* Popup UI Styles */
141 .shieldflow-popup {
142 position: fixed;
143 top: 20px;
144 right: 20px;
145 width: 320px;
146 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
147 border-radius: 12px;
148 box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
149 z-index: 10000000;
150 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
151 color: white;
152 padding: 20px;
153 display: none;
154 }
155
156 .shieldflow-popup.visible {
157 display: block;
158 animation: slideIn 0.3s ease-out;
159 }
160
161 @keyframes slideIn {
162 from {
163 transform: translateX(400px);
164 opacity: 0;
165 }
166 to {
167 transform: translateX(0);
168 opacity: 1;
169 }
170 }
171
172 .shieldflow-header {
173 display: flex;
174 justify-content: space-between;
175 align-items: center;
176 margin-bottom: 20px;
177 padding-bottom: 15px;
178 border-bottom: 1px solid rgba(255, 255, 255, 0.2);
179 }
180
181 .shieldflow-title {
182 font-size: 18px;
183 font-weight: 600;
184 display: flex;
185 align-items: center;
186 gap: 8px;
187 }
188
189 .shieldflow-close {
190 background: rgba(255, 255, 255, 0.2);
191 border: none;
192 color: white;
193 width: 28px;
194 height: 28px;
195 border-radius: 50%;
196 cursor: pointer;
197 font-size: 18px;
198 display: flex;
199 align-items: center;
200 justify-content: center;
201 transition: background 0.2s;
202 }
203
204 .shieldflow-close:hover {
205 background: rgba(255, 255, 255, 0.3);
206 }
207
208 .shieldflow-toggle-group {
209 margin-bottom: 15px;
210 }
211
212 .shieldflow-toggle-item {
213 display: flex;
214 justify-content: space-between;
215 align-items: center;
216 padding: 12px;
217 background: rgba(255, 255, 255, 0.1);
218 border-radius: 8px;
219 margin-bottom: 8px;
220 transition: background 0.2s;
221 }
222
223 .shieldflow-toggle-item:hover {
224 background: rgba(255, 255, 255, 0.15);
225 }
226
227 .shieldflow-toggle-label {
228 font-size: 14px;
229 font-weight: 500;
230 }
231
232 .shieldflow-master-toggle {
233 font-size: 16px;
234 font-weight: 600;
235 }
236
237 .shieldflow-switch {
238 position: relative;
239 width: 48px;
240 height: 24px;
241 background: rgba(255, 255, 255, 0.3);
242 border-radius: 12px;
243 cursor: pointer;
244 transition: background 0.3s;
245 }
246
247 .shieldflow-switch.active {
248 background: #4ade80;
249 }
250
251 .shieldflow-switch::after {
252 content: '';
253 position: absolute;
254 top: 2px;
255 left: 2px;
256 width: 20px;
257 height: 20px;
258 background: white;
259 border-radius: 50%;
260 transition: transform 0.3s;
261 }
262
263 .shieldflow-switch.active::after {
264 transform: translateX(24px);
265 }
266
267 .shieldflow-fab {
268 position: fixed;
269 bottom: 30px;
270 right: 30px;
271 width: 56px;
272 height: 56px;
273 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
274 border-radius: 50%;
275 box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4);
276 cursor: pointer;
277 display: flex;
278 align-items: center;
279 justify-content: center;
280 font-size: 24px;
281 z-index: 9999999;
282 transition: transform 0.2s, box-shadow 0.2s;
283 }
284
285 .shieldflow-fab:hover {
286 transform: scale(1.1);
287 box-shadow: 0 6px 30px rgba(102, 126, 234, 0.6);
288 }
289
290 .shieldflow-exclusion {
291 margin-top: 15px;
292 padding-top: 15px;
293 border-top: 1px solid rgba(255, 255, 255, 0.2);
294 }
295
296 .shieldflow-exclusion-title {
297 font-size: 13px;
298 font-weight: 600;
299 margin-bottom: 10px;
300 opacity: 0.9;
301 }
302
303 .shieldflow-exclusion-btn {
304 width: 100%;
305 padding: 10px;
306 background: rgba(255, 255, 255, 0.2);
307 border: none;
308 border-radius: 6px;
309 color: white;
310 font-size: 13px;
311 font-weight: 500;
312 cursor: pointer;
313 transition: background 0.2s;
314 }
315
316 .shieldflow-exclusion-btn:hover {
317 background: rgba(255, 255, 255, 0.3);
318 }
319
320 .shieldflow-exclusion-btn.excluded {
321 background: #ef4444;
322 }
323
324 .shieldflow-stats {
325 margin-top: 15px;
326 padding: 12px;
327 background: rgba(0, 0, 0, 0.2);
328 border-radius: 8px;
329 font-size: 12px;
330 }
331
332 .shieldflow-stat-item {
333 display: flex;
334 justify-content: space-between;
335 margin-bottom: 5px;
336 }
337
338 .shieldflow-stat-item:last-child {
339 margin-bottom: 0;
340 }
341 `;
342
343 TM_addStyle(styles);
344 console.log('ShieldFlow: Styles injected');
345 }
346
347 // ============================================
348 // PII DETECTION AND REDACTION ENGINE
349 // ============================================
350
351 let redactionStats = {
352 emails: 0,
353 creditCards: 0,
354 phones: 0,
355 apiKeys: 0
356 };
357
358 function detectAndRedactPII(node) {
359 if (!state.enabled || isCurrentSiteExcluded()) {
360 return;
361 }
362
363 // Skip if already processed
364 if (state.processedNodes.has(node)) {
365 return;
366 }
367
368 // Skip script, style, and already redacted elements
369 if (node.nodeType !== Node.TEXT_NODE ||
370 !node.textContent.trim() ||
371 node.parentElement?.classList.contains(CONFIG.overlayClass)) {
372 return;
373 }
374
375 const parent = node.parentElement;
376 if (!parent || parent.tagName === 'SCRIPT' || parent.tagName === 'STYLE' || parent.tagName === 'NOSCRIPT') {
377 return;
378 }
379
380 let text = node.textContent;
381 let hasMatch = false;
382 const matches = [];
383
384 // Detect Emails
385 if (state.emailsEnabled) {
386 const emailMatches = [...text.matchAll(PATTERNS.email)];
387 emailMatches.forEach(match => {
388 matches.push({ type: 'email', text: match[0], index: match.index });
389 hasMatch = true;
390 redactionStats.emails++;
391 });
392 }
393
394 // Detect Credit Cards
395 if (state.creditCardsEnabled) {
396 const ccMatches = [...text.matchAll(PATTERNS.creditCard)];
397 ccMatches.forEach(match => {
398 matches.push({ type: 'creditcard', text: match[0], index: match.index });
399 hasMatch = true;
400 redactionStats.creditCards++;
401 });
402 }
403
404 // Detect Phone Numbers
405 if (state.phonesEnabled) {
406 const phoneMatches = [...text.matchAll(PATTERNS.phone)];
407 phoneMatches.forEach(match => {
408 matches.push({ type: 'phone', text: match[0], index: match.index });
409 hasMatch = true;
410 redactionStats.phones++;
411 });
412 }
413
414 // Detect API Keys
415 if (state.apiKeysEnabled) {
416 for (const [keyType, pattern] of Object.entries(PATTERNS.apiKeys)) {
417 const keyMatches = [...text.matchAll(pattern)];
418 keyMatches.forEach(match => {
419 // Filter out common false positives for generic pattern
420 if (keyType === 'generic' && match[0].length < 40) {
421 return;
422 }
423 matches.push({ type: 'apikey', text: match[0], index: match.index });
424 hasMatch = true;
425 redactionStats.apiKeys++;
426 });
427 }
428 }
429
430 if (hasMatch && matches.length > 0) {
431 // Sort matches by index in reverse order to replace from end to start
432 matches.sort((a, b) => b.index - a.index);
433
434 // Create document fragment for efficient DOM manipulation
435 const fragment = document.createDocumentFragment();
436 let lastIndex = text.length;
437
438 // Build the new content with redacted spans
439 for (let i = matches.length - 1; i >= 0; i--) {
440 const match = matches[i];
441 const endIndex = match.index + match.text.length;
442
443 // Add text after this match
444 if (endIndex < lastIndex) {
445 fragment.insertBefore(
446 document.createTextNode(text.substring(endIndex, lastIndex)),
447 fragment.firstChild
448 );
449 }
450
451 // Create redacted span
452 const span = document.createElement('span');
453 span.className = CONFIG.overlayClass;
454 span.setAttribute(CONFIG.dataAttributes.type, match.type);
455 span.setAttribute(CONFIG.dataAttributes.original, match.text);
456 span.textContent = match.text;
457 span.title = `Protected ${match.type} - Click to reveal`;
458
459 // Add click to reveal functionality
460 span.style.cursor = 'pointer';
461 span.addEventListener('click', function(e) {
462 e.stopPropagation();
463 if (confirm('Reveal protected information?')) {
464 this.classList.remove(CONFIG.overlayClass);
465 this.style.cursor = 'default';
466 }
467 });
468
469 fragment.insertBefore(span, fragment.firstChild);
470 lastIndex = match.index;
471 }
472
473 // Add remaining text before first match
474 if (lastIndex > 0) {
475 fragment.insertBefore(
476 document.createTextNode(text.substring(0, lastIndex)),
477 fragment.firstChild
478 );
479 }
480
481 // Replace the text node with the fragment
482 parent.replaceChild(fragment, node);
483 state.processedNodes.add(parent);
484
485 console.log(`ShieldFlow: Redacted ${matches.length} PII items in element`, parent);
486 }
487 }
488
489 function scanDOM(rootNode = document.body) {
490 if (!state.enabled || isCurrentSiteExcluded()) {
491 return;
492 }
493
494 const walker = document.createTreeWalker(
495 rootNode,
496 NodeFilter.SHOW_TEXT,
497 {
498 acceptNode: function(node) {
499 // Skip empty text nodes and already processed parents
500 if (!node.textContent.trim() || state.processedNodes.has(node.parentElement)) {
501 return NodeFilter.FILTER_REJECT;
502 }
503 return NodeFilter.FILTER_ACCEPT;
504 }
505 }
506 );
507
508 const textNodes = [];
509 let currentNode;
510 while (currentNode = walker.nextNode()) {
511 textNodes.push(currentNode);
512 }
513
514 // Process all text nodes
515 textNodes.forEach(node => detectAndRedactPII(node));
516
517 console.log(`ShieldFlow: Scanned ${textNodes.length} text nodes`);
518 }
519
520 // ============================================
521 // MUTATION OBSERVER (Performance Optimized)
522 // ============================================
523
524 let observerTimeout;
525 const DEBOUNCE_DELAY = 300;
526
527 function debounce(func, delay) {
528 return function(...args) {
529 clearTimeout(observerTimeout);
530 observerTimeout = setTimeout(() => func.apply(this, args), delay);
531 };
532 }
533
534 const debouncedScan = debounce((mutations) => {
535 if (!state.enabled || isCurrentSiteExcluded()) {
536 return;
537 }
538
539 const nodesToScan = new Set();
540
541 mutations.forEach(mutation => {
542 mutation.addedNodes.forEach(node => {
543 if (node.nodeType === Node.ELEMENT_NODE) {
544 nodesToScan.add(node);
545 } else if (node.nodeType === Node.TEXT_NODE) {
546 detectAndRedactPII(node);
547 }
548 });
549 });
550
551 // Scan all collected element nodes
552 nodesToScan.forEach(node => scanDOM(node));
553 }, DEBOUNCE_DELAY);
554
555 function initMutationObserver() {
556 const observer = new MutationObserver(debouncedScan);
557
558 observer.observe(document.body, {
559 childList: true,
560 subtree: true,
561 characterData: false,
562 attributes: false
563 });
564
565 console.log('ShieldFlow: MutationObserver initialized');
566 return observer;
567 }
568
569 // ============================================
570 // POPUP UI
571 // ============================================
572
573 function createPopupUI() {
574 // Create FAB button
575 const fab = document.createElement('div');
576 fab.className = 'shieldflow-fab';
577 fab.innerHTML = '🛡️';
578 fab.title = 'ShieldFlow Settings';
579
580 // Create popup
581 const popup = document.createElement('div');
582 popup.className = 'shieldflow-popup';
583 popup.innerHTML = `
584 <div class="shieldflow-header">
585 <div class="shieldflow-title">
586 🛡️ ShieldFlow
587 </div>
588 <button class="shieldflow-close">×</button>
589 </div>
590
591 <div class="shieldflow-toggle-group">
592 <div class="shieldflow-toggle-item">
593 <span class="shieldflow-toggle-label shieldflow-master-toggle">Master Protection</span>
594 <div class="shieldflow-switch ${state.enabled ? 'active' : ''}" data-toggle="master"></div>
595 </div>
596 </div>
597
598 <div class="shieldflow-toggle-group">
599 <div class="shieldflow-toggle-item">
600 <span class="shieldflow-toggle-label">📧 Hide Emails</span>
601 <div class="shieldflow-switch ${state.emailsEnabled ? 'active' : ''}" data-toggle="emails"></div>
602 </div>
603 <div class="shieldflow-toggle-item">
604 <span class="shieldflow-toggle-label">💳 Hide Credit Cards</span>
605 <div class="shieldflow-switch ${state.creditCardsEnabled ? 'active' : ''}" data-toggle="creditcards"></div>
606 </div>
607 <div class="shieldflow-toggle-item">
608 <span class="shieldflow-toggle-label">📱 Hide Phone Numbers</span>
609 <div class="shieldflow-switch ${state.phonesEnabled ? 'active' : ''}" data-toggle="phones"></div>
610 </div>
611 <div class="shieldflow-toggle-item">
612 <span class="shieldflow-toggle-label">🔑 Hide API Keys</span>
613 <div class="shieldflow-switch ${state.apiKeysEnabled ? 'active' : ''}" data-toggle="apikeys"></div>
614 </div>
615 </div>
616
617 <div class="shieldflow-stats">
618 <div class="shieldflow-stat-item">
619 <span>Emails Protected:</span>
620 <span id="stat-emails">0</span>
621 </div>
622 <div class="shieldflow-stat-item">
623 <span>Credit Cards Protected:</span>
624 <span id="stat-creditcards">0</span>
625 </div>
626 <div class="shieldflow-stat-item">
627 <span>Phones Protected:</span>
628 <span id="stat-phones">0</span>
629 </div>
630 <div class="shieldflow-stat-item">
631 <span>API Keys Protected:</span>
632 <span id="stat-apikeys">0</span>
633 </div>
634 </div>
635
636 <div class="shieldflow-exclusion">
637 <div class="shieldflow-exclusion-title">Site Exclusion</div>
638 <button class="shieldflow-exclusion-btn" data-action="exclude">
639 ${isCurrentSiteExcluded() ? '✓ Site Excluded' : 'Exclude This Site'}
640 </button>
641 </div>
642 `;
643
644 document.body.appendChild(fab);
645 document.body.appendChild(popup);
646
647 // Event Listeners
648 fab.addEventListener('click', () => {
649 popup.classList.toggle('visible');
650 updateStatsDisplay();
651 });
652
653 popup.querySelector('.shieldflow-close').addEventListener('click', () => {
654 popup.classList.remove('visible');
655 });
656
657 // Toggle switches
658 popup.querySelectorAll('.shieldflow-switch').forEach(toggle => {
659 toggle.addEventListener('click', async function() {
660 const toggleType = this.getAttribute('data-toggle');
661 this.classList.toggle('active');
662
663 switch(toggleType) {
664 case 'master':
665 state.enabled = !state.enabled;
666 if (state.enabled) {
667 scanDOM();
668 } else {
669 removeAllRedactions();
670 }
671 break;
672 case 'emails':
673 state.emailsEnabled = !state.emailsEnabled;
674 break;
675 case 'creditcards':
676 state.creditCardsEnabled = !state.creditCardsEnabled;
677 break;
678 case 'phones':
679 state.phonesEnabled = !state.phonesEnabled;
680 break;
681 case 'apikeys':
682 state.apiKeysEnabled = !state.apiKeysEnabled;
683 break;
684 }
685
686 await saveSettings();
687
688 // Re-scan if enabling a category
689 if (state.enabled) {
690 removeAllRedactions();
691 redactionStats = { emails: 0, creditCards: 0, phones: 0, apiKeys: 0 };
692 scanDOM();
693 updateStatsDisplay();
694 }
695 });
696 });
697
698 // Exclusion button
699 popup.querySelector('[data-action="exclude"]').addEventListener('click', async function() {
700 const currentHost = window.location.hostname;
701 const isExcluded = isCurrentSiteExcluded();
702
703 if (isExcluded) {
704 state.excludedSites = state.excludedSites.filter(site => !currentHost.includes(site));
705 this.textContent = 'Exclude This Site';
706 this.classList.remove('excluded');
707 scanDOM();
708 } else {
709 state.excludedSites.push(currentHost);
710 this.textContent = '✓ Site Excluded';
711 this.classList.add('excluded');
712 removeAllRedactions();
713 }
714
715 await saveSettings();
716 });
717
718 console.log('ShieldFlow: Popup UI created');
719 }
720
721 function updateStatsDisplay() {
722 document.getElementById('stat-emails').textContent = redactionStats.emails;
723 document.getElementById('stat-creditcards').textContent = redactionStats.creditCards;
724 document.getElementById('stat-phones').textContent = redactionStats.phones;
725 document.getElementById('stat-apikeys').textContent = redactionStats.apiKeys;
726 }
727
728 function removeAllRedactions() {
729 document.querySelectorAll(`.${CONFIG.overlayClass}`).forEach(span => {
730 const text = span.textContent;
731 const textNode = document.createTextNode(text);
732 span.parentNode.replaceChild(textNode, span);
733 });
734 state.processedNodes = new WeakSet();
735 console.log('ShieldFlow: All redactions removed');
736 }
737
738 // ============================================
739 // INITIALIZATION
740 // ============================================
741
742 async function init() {
743 console.log('ShieldFlow: Starting initialization...');
744
745 // Load settings first
746 await loadSettings();
747
748 // Check if site is excluded
749 if (isCurrentSiteExcluded()) {
750 console.log('ShieldFlow: Current site is excluded, skipping initialization');
751 return;
752 }
753
754 // Inject styles
755 injectStyles();
756
757 // Wait for body to be ready
758 TM_runBody(() => {
759 console.log('ShieldFlow: Body ready, starting PII detection...');
760
761 // Initial scan
762 if (state.enabled) {
763 scanDOM();
764 console.log('ShieldFlow: Initial scan complete', redactionStats);
765 }
766
767 // Initialize mutation observer
768 initMutationObserver();
769
770 // Create popup UI
771 createPopupUI();
772
773 console.log('ShieldFlow: Fully initialized and monitoring for PII');
774 });
775 }
776
777 // Start the extension
778 init();
779
780})();