Merge pull request #4 from outis1one/claude/privacy-data-substitution-extension-OvTcA
Claude/privacy data substitution extension ov tc a
This commit is contained in:
@@ -1,21 +1,38 @@
|
|||||||
MIT License
|
Business Source License 1.1
|
||||||
|
|
||||||
Copyright (c) 2025 Silent Send Contributors
|
Licensor: Silent Send Contributors
|
||||||
|
Licensed Work: Silent Send browser extension
|
||||||
|
Change Date: March 26, 2030
|
||||||
|
Change License: MIT
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Terms
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
The Licensor hereby grants you the right to copy, modify, create
|
||||||
copies or substantial portions of the Software.
|
derivative works, redistribute, and make non-production use of the
|
||||||
|
Licensed Work.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
The Licensor hereby grants you the right to make production use of
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
the Licensed Work for personal, non-commercial purposes.
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
For commercial use, you must obtain a commercial license from the
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
Licensor. Contact: [your-email-here]
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
Effective on the Change Date, the Licensor hereby grants you rights
|
||||||
|
under the terms of the Change License, and the rights granted above
|
||||||
|
terminate.
|
||||||
|
|
||||||
|
If your use of the Licensed Work does not comply with the
|
||||||
|
requirements currently in effect as described in this License, you
|
||||||
|
must purchase a commercial license from the Licensor, or you must
|
||||||
|
refrain from using the Licensed Work.
|
||||||
|
|
||||||
|
All copies of the original and modified Licensed Work, and
|
||||||
|
derivative works of the Licensed Work, are subject to this License.
|
||||||
|
|
||||||
|
THE LICENSED WORK IS PROVIDED "AS IS". THE LICENSOR HEREBY DISCLAIMS
|
||||||
|
ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
||||||
|
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE LICENSOR BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK.
|
||||||
|
|||||||
@@ -290,4 +290,4 @@ src/
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[MIT](LICENSE) — use it for anything, commercial or personal, modify it, redistribute it, relicense it. Just keep the copyright notice in copies of the code.
|
[Business Source License 1.1](LICENSE) — free for personal, non-commercial use. Commercial use requires a paid license. The code automatically converts to MIT on March 26, 2030.
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "silent-send",
|
"name": "silent-send",
|
||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "BSL-1.1",
|
||||||
"description": "Browser extension that substitutes personal data before sending to AI services",
|
"description": "Browser extension that substitutes personal data before sending to AI services",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:chrome": "./build.sh chrome",
|
"build:chrome": "./build.sh chrome",
|
||||||
|
|||||||
+23
-5
@@ -20,7 +20,9 @@ if [ ! -f "$ENV_FILE" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Auto-bump patch version ---
|
# --- Auto-bump version — always unique, no metadata ---
|
||||||
|
# Reads current version, increments patch. If already signed,
|
||||||
|
# keeps incrementing until it works.
|
||||||
CURRENT_VERSION=$(grep -o '"version": "[^"]*"' "$MANIFEST" | head -1 | grep -o '[0-9.]*')
|
CURRENT_VERSION=$(grep -o '"version": "[^"]*"' "$MANIFEST" | head -1 | grep -o '[0-9.]*')
|
||||||
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
|
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
|
||||||
PATCH=$((PATCH + 1))
|
PATCH=$((PATCH + 1))
|
||||||
@@ -86,17 +88,33 @@ echo ""
|
|||||||
echo "Building Firefox extension..."
|
echo "Building Firefox extension..."
|
||||||
"$SCRIPT_DIR/build.sh" firefox
|
"$SCRIPT_DIR/build.sh" firefox
|
||||||
|
|
||||||
# Sign
|
# Sign — retry with incremented patch if version conflict
|
||||||
echo "Signing v$NEW_VERSION with Mozilla..."
|
MAX_ATTEMPTS=10
|
||||||
npx web-ext sign \
|
for attempt in $(seq 1 $MAX_ATTEMPTS); do
|
||||||
|
echo "Signing v$NEW_VERSION with Mozilla (attempt $attempt)..."
|
||||||
|
|
||||||
|
if npx web-ext sign \
|
||||||
--no-config-discovery \
|
--no-config-discovery \
|
||||||
--source-dir "$SCRIPT_DIR/dist/firefox" \
|
--source-dir "$SCRIPT_DIR/dist/firefox" \
|
||||||
--artifacts-dir "$SCRIPT_DIR/dist/firefox-signed" \
|
--artifacts-dir "$SCRIPT_DIR/dist/firefox-signed" \
|
||||||
--channel unlisted \
|
--channel unlisted \
|
||||||
--api-key "$API_KEY" \
|
--api-key "$API_KEY" \
|
||||||
--api-secret "$API_SECRET"
|
--api-secret "$API_SECRET" 2>&1; then
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Done! v$NEW_VERSION signed."
|
echo "Done! v$NEW_VERSION signed."
|
||||||
echo "Install the .xpi file from dist/firefox-signed/"
|
echo "Install the .xpi file from dist/firefox-signed/"
|
||||||
echo "Drag it into Firefox or use File → Open File."
|
echo "Drag it into Firefox or use File → Open File."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If it failed due to version conflict, bump and rebuild
|
||||||
|
echo "Version $NEW_VERSION already exists, trying next..."
|
||||||
|
PATCH=$((PATCH + 1))
|
||||||
|
NEW_VERSION="$MAJOR.$MINOR.$PATCH"
|
||||||
|
|
||||||
|
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$NEW_VERSION\"/" "$SCRIPT_DIR/dist/firefox/manifest.json"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Error: Failed after $MAX_ATTEMPTS attempts."
|
||||||
|
exit 1
|
||||||
|
|||||||
+205
-6
@@ -13,14 +13,213 @@
|
|||||||
padding: 0 1px;
|
padding: 0 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Revealed text styling (when reveal mode shows real values in responses) */
|
/* CSS Custom Highlight API styles — zero DOM changes */
|
||||||
.ss-revealed {
|
|
||||||
background: rgba(59, 130, 246, 0.1);
|
/* Yellow highlight: fake values the AI received (non-reveal mode) */
|
||||||
border-bottom: 1.5px dashed rgba(59, 130, 246, 0.5);
|
::highlight(ss-substituted) {
|
||||||
border-radius: 2px;
|
background-color: rgba(250, 204, 21, 0.4);
|
||||||
padding: 0 1px;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Terminal style: real data shown in reveal mode (dark bg, green text) */
|
||||||
|
::highlight(ss-revealed) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.85);
|
||||||
|
color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback for browsers without Highlight API */
|
||||||
|
.ss-revealed {
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
color: #4ade80;
|
||||||
|
padding: 0 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Auto-detect PPI warning banner */
|
||||||
|
.ss-autodetect-warning {
|
||||||
|
position: fixed;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
max-width: 400px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #e5e7eb;
|
||||||
|
border: 1px solid #f59e0b;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
z-index: 999999;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-autodetect-warning.visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-ad-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #f59e0b;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-ad-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-ad-close:hover { color: #fff; }
|
||||||
|
|
||||||
|
.ss-ad-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-ad-item:last-of-type { border-bottom: none; }
|
||||||
|
|
||||||
|
.ss-ad-type {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f59e0b;
|
||||||
|
min-width: 70px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-ad-value {
|
||||||
|
font-family: 'SF Mono', Monaco, monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #4ade80;
|
||||||
|
background: #0a0a0a;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
max-width: 180px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-ad-hint {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #9ca3af;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-ad-more {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #6b7280;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-ad-footer {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pre-send PPI warning (spellcheck-style, appears while typing) */
|
||||||
|
.ss-presend-warning {
|
||||||
|
position: fixed;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
max-width: 420px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #e5e7eb;
|
||||||
|
border: 1px solid #f59e0b;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
z-index: 999999;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-presend-warning.visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-ps-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 0;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-ps-item:last-of-type { border-bottom: none; }
|
||||||
|
|
||||||
|
.ss-ps-type {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f59e0b;
|
||||||
|
min-width: 65px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-ps-value {
|
||||||
|
font-family: 'SF Mono', Monaco, monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #4ade80;
|
||||||
|
background: #0a0a0a;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
max-width: 140px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-ps-hint {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #9ca3af;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-ps-add {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #4ade80;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #4ade80;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-ps-add:hover { background: rgba(74, 222, 128, 0.15); }
|
||||||
|
.ss-ps-add:disabled { border-color: #333; cursor: default; }
|
||||||
|
|
||||||
/* Floating reveal mode indicator */
|
/* Floating reveal mode indicator */
|
||||||
.ss-reveal-badge {
|
.ss-reveal-badge {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
+426
-32
@@ -254,6 +254,7 @@
|
|||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Combined substitution: smart patterns + explicit + secret scan
|
// Combined substitution: smart patterns + explicit + secret scan
|
||||||
|
// + auto-detect warning for unconfigured PPI
|
||||||
// ============================================================
|
// ============================================================
|
||||||
function substituteAll(text) {
|
function substituteAll(text) {
|
||||||
const allReplacements = [];
|
const allReplacements = [];
|
||||||
@@ -267,21 +268,162 @@
|
|||||||
allReplacements.push(...explicit.replacements);
|
allReplacements.push(...explicit.replacements);
|
||||||
|
|
||||||
// 3. Secret scanner (API keys, tokens, SSNs, credit cards, etc.)
|
// 3. Secret scanner (API keys, tokens, SSNs, credit cards, etc.)
|
||||||
|
let finalText = explicit.text;
|
||||||
if (settings.secretScanning !== false) {
|
if (settings.secretScanning !== false) {
|
||||||
const secrets = scanAndRedactSecrets(explicit.text);
|
const secrets = scanAndRedactSecrets(finalText);
|
||||||
allReplacements.push(...secrets.redactions);
|
allReplacements.push(...secrets.redactions);
|
||||||
|
finalText = secrets.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Auto-detect: scan the FINAL text for unconfigured PPI
|
||||||
|
// Auto-redact if enabled, otherwise just warn
|
||||||
|
if (settings.autoDetect !== false) {
|
||||||
|
const warnings = autoDetectPPI(finalText, identity);
|
||||||
|
if (warnings.length > 0) {
|
||||||
|
// Auto-redact detected PPI in the outbound text
|
||||||
|
if (settings.autoRedactDetected !== false) {
|
||||||
|
for (let i = warnings.length - 1; i >= 0; i--) {
|
||||||
|
const w = warnings[i];
|
||||||
|
const fake = generateFake(w.name, w.value);
|
||||||
|
const escaped = esc(w.value);
|
||||||
|
const regex = new RegExp(escaped, 'g');
|
||||||
|
finalText = finalText.replace(regex, fake);
|
||||||
|
allReplacements.push({
|
||||||
|
original: w.value,
|
||||||
|
replaced: fake,
|
||||||
|
category: 'auto-detect',
|
||||||
|
pattern: w.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Still show the warning so user knows what was caught
|
||||||
|
showAutoDetectWarning(warnings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: secrets.text,
|
text: finalText,
|
||||||
replacements: allReplacements,
|
replacements: allReplacements,
|
||||||
modified: allReplacements.length > 0,
|
modified: allReplacements.length > 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
// ============================================================
|
||||||
text: explicit.text,
|
// Auto-Detect PPI Scanner (inline for page world)
|
||||||
replacements: allReplacements,
|
// ============================================================
|
||||||
modified: allReplacements.length > 0,
|
const PPI_PATTERNS = [
|
||||||
};
|
// Network
|
||||||
|
{ name: 'Private IP', re: /\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b/g,
|
||||||
|
hint: 'Private IP address', cat: 'network' },
|
||||||
|
{ name: 'Public IP', re: /\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\b/g,
|
||||||
|
hint: 'IP address — could identify your network', cat: 'network',
|
||||||
|
skip: /^(?:127\.0\.0\.1|0\.0\.0\.0|255\.255\.255\.\d+|8\.8\.[84]\.[84]|1\.1\.1\.1)$/ },
|
||||||
|
{ name: 'MAC Address', re: /\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b/g,
|
||||||
|
hint: 'MAC address — identifies hardware', cat: 'network' },
|
||||||
|
// Location
|
||||||
|
{ name: 'Street Address', re: /\b\d{1,5}\s+(?:[A-Z][a-z]+\s+){1,3}(?:St|Street|Ave|Avenue|Blvd|Boulevard|Dr|Drive|Ln|Lane|Rd|Road|Way|Ct|Court|Pl|Place)\.?\b/gi,
|
||||||
|
hint: 'Street address', cat: 'address' },
|
||||||
|
{ name: 'GPS Coordinates', re: /\b-?\d{1,3}\.\d{4,},\s*-?\d{1,3}\.\d{4,}\b/g,
|
||||||
|
hint: 'GPS coordinates — pinpoints a location', cat: 'address' },
|
||||||
|
// Personal
|
||||||
|
{ name: 'Date (possible DOB)', re: /\b(?:(?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12]\d|3[01])[-/](?:19|20)\d{2}|(?:19|20)\d{2}[-/](?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12]\d|3[01]))\b/g,
|
||||||
|
hint: 'Date — could be a birthday', cat: 'personal' },
|
||||||
|
{ name: 'EIN / Tax ID', re: /\b\d{2}-\d{7}\b/g,
|
||||||
|
hint: 'Could be a tax ID', cat: 'document' },
|
||||||
|
// Paths not caught by smart patterns
|
||||||
|
{ name: 'Home Path', re: /(?:\/home\/|\/Users\/|C:\\Users\\)[a-zA-Z0-9._-]+/g,
|
||||||
|
hint: 'Home directory — reveals username', cat: 'path' },
|
||||||
|
// Shell prompts
|
||||||
|
{ name: 'Shell Prompt', re: /[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+[:\$#%>]\s/g,
|
||||||
|
hint: 'Shell prompt — reveals user@host', cat: 'prompt' },
|
||||||
|
// Git remotes
|
||||||
|
{ name: 'Git Remote', re: /(?:git@|https:\/\/)(?:github|gitlab|bitbucket)\.[a-z]+[:/][^\s]+/gi,
|
||||||
|
hint: 'Git remote — may reveal username/org', cat: 'url' },
|
||||||
|
// Env vars
|
||||||
|
{ name: 'Env Variable', re: /\b(?:HOME|USER|USERNAME|LOGNAME|HOSTNAME|COMPUTERNAME|EMAIL)=[^\s]+/gi,
|
||||||
|
hint: 'Env variable with personal data', cat: 'env' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function autoDetectPPI(text, ident) {
|
||||||
|
if (!text || text.length < 5) return [];
|
||||||
|
|
||||||
|
// Build skip set from configured values
|
||||||
|
const configured = new Set();
|
||||||
|
if (ident) {
|
||||||
|
const addAll = (arr, key) => (arr || []).forEach(item => {
|
||||||
|
if (item.real) configured.add(item.real.toLowerCase());
|
||||||
|
if (item.substitute) configured.add(item.substitute.toLowerCase());
|
||||||
|
});
|
||||||
|
addAll(ident.names); addAll(ident.emails);
|
||||||
|
addAll(ident.usernames); addAll(ident.hostnames); addAll(ident.phones);
|
||||||
|
}
|
||||||
|
|
||||||
|
const findings = [];
|
||||||
|
for (const pat of PPI_PATTERNS) {
|
||||||
|
pat.re.lastIndex = 0;
|
||||||
|
let m;
|
||||||
|
while ((m = pat.re.exec(text)) !== null) {
|
||||||
|
const val = m[0];
|
||||||
|
if (configured.has(val.toLowerCase())) continue;
|
||||||
|
if (pat.skip && pat.skip.test(val)) continue;
|
||||||
|
findings.push({ name: pat.name, value: val, hint: pat.hint, category: pat.cat });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deduplicate by value
|
||||||
|
const seen = new Set();
|
||||||
|
return findings.filter(f => {
|
||||||
|
if (seen.has(f.value)) return false;
|
||||||
|
seen.add(f.value);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Auto-Detect Warning UI — floating banner
|
||||||
|
// ============================================================
|
||||||
|
let warningEl = null;
|
||||||
|
let warningTimeout = null;
|
||||||
|
|
||||||
|
function showAutoDetectWarning(warnings) {
|
||||||
|
if (!warningEl) {
|
||||||
|
warningEl = document.createElement('div');
|
||||||
|
warningEl.className = 'ss-autodetect-warning';
|
||||||
|
document.body.appendChild(warningEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = warnings.slice(0, 5).map(w =>
|
||||||
|
`<div class="ss-ad-item">
|
||||||
|
<span class="ss-ad-type">${w.name}</span>
|
||||||
|
<code class="ss-ad-value">${w.value.length > 30 ? w.value.slice(0, 27) + '...' : w.value}</code>
|
||||||
|
<span class="ss-ad-hint">${w.hint}</span>
|
||||||
|
</div>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
const more = warnings.length > 5 ? `<div class="ss-ad-more">+${warnings.length - 5} more</div>` : '';
|
||||||
|
|
||||||
|
warningEl.innerHTML = `
|
||||||
|
<div class="ss-ad-header">
|
||||||
|
<strong>Silent Send detected potential PPI that may not be substituted:</strong>
|
||||||
|
<button class="ss-ad-close">×</button>
|
||||||
|
</div>
|
||||||
|
${items}
|
||||||
|
${more}
|
||||||
|
<div class="ss-ad-footer">These were sent as-is. Consider adding them to your identity or mappings.</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
warningEl.classList.add('visible');
|
||||||
|
|
||||||
|
// Close button
|
||||||
|
warningEl.querySelector('.ss-ad-close').addEventListener('click', () => {
|
||||||
|
warningEl.classList.remove('visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-dismiss after 15 seconds
|
||||||
|
if (warningTimeout) clearTimeout(warningTimeout);
|
||||||
|
warningTimeout = setTimeout(() => {
|
||||||
|
warningEl.classList.remove('visible');
|
||||||
|
}, 15000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -566,28 +708,42 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Response Reveal — swaps fake data back to real in the page
|
// Highlighting — CSS Custom Highlight API (zero DOM changes)
|
||||||
|
//
|
||||||
|
// Two highlight modes:
|
||||||
|
// ss-substituted (yellow) — fake values the AI received
|
||||||
|
// ss-revealed (terminal: dark bg, green text) — your real data
|
||||||
|
//
|
||||||
|
// Falls back to simple text replacement for reveal if
|
||||||
|
// CSS.highlights is not supported.
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// Build reverse mapping pairs from identity + explicit mappings
|
const hasHighlightAPI = typeof CSS !== 'undefined' && CSS.highlights;
|
||||||
|
|
||||||
|
// Register highlight groups
|
||||||
|
let hlSubstituted = null; // yellow — marks fake values in responses
|
||||||
|
let hlRevealed = null; // terminal — marks revealed real values
|
||||||
|
|
||||||
|
if (hasHighlightAPI) {
|
||||||
|
hlSubstituted = new Highlight();
|
||||||
|
hlRevealed = new Highlight();
|
||||||
|
CSS.highlights.set('ss-substituted', hlSubstituted);
|
||||||
|
CSS.highlights.set('ss-revealed', hlRevealed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build pairs: substitute → real
|
||||||
function buildRevealPairs() {
|
function buildRevealPairs() {
|
||||||
const pairs = [];
|
const pairs = [];
|
||||||
|
|
||||||
// Explicit mappings (substitute → real)
|
|
||||||
for (const m of mappings) {
|
for (const m of mappings) {
|
||||||
if (!m.enabled || !m.substitute || !m.real) continue;
|
if (!m.enabled || !m.substitute || !m.real) continue;
|
||||||
pairs.push({ from: m.substitute, to: m.real, caseSensitive: m.caseSensitive });
|
pairs.push({ from: m.substitute, to: m.real, caseSensitive: m.caseSensitive });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smart identity pairs (substitute → real)
|
|
||||||
if (identity) {
|
if (identity) {
|
||||||
for (const e of (identity.emails || [])) {
|
for (const e of (identity.emails || [])) {
|
||||||
if (e.substitute && e.real) pairs.push({ from: e.substitute, to: e.real });
|
if (e.substitute && e.real) pairs.push({ from: e.substitute, to: e.real });
|
||||||
}
|
}
|
||||||
if (identity.catchAllEmail) {
|
|
||||||
// Can't reverse a catch-all to a specific email, but we mark it
|
|
||||||
// so the user sees it was a substitution
|
|
||||||
}
|
|
||||||
for (const n of (identity.names || [])) {
|
for (const n of (identity.names || [])) {
|
||||||
if (n.substitute && n.real) pairs.push({ from: n.substitute, to: n.real });
|
if (n.substitute && n.real) pairs.push({ from: n.substitute, to: n.real });
|
||||||
}
|
}
|
||||||
@@ -602,20 +758,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort longer matches first
|
|
||||||
pairs.sort((a, b) => b.from.length - a.from.length);
|
pairs.sort((a, b) => b.from.length - a.from.length);
|
||||||
return pairs;
|
return pairs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache reveal pairs — rebuild when config changes
|
// Cache
|
||||||
let _revealPairsCache = null;
|
let _revealPairsCache = null;
|
||||||
window.addEventListener('message', (event) => {
|
window.addEventListener('message', (event) => {
|
||||||
if (event.data?.type === 'ss:config-updated') _revealPairsCache = null;
|
if (event.data?.type === 'ss:config-updated') _revealPairsCache = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
function revealText(text) {
|
function getRevealPairs() {
|
||||||
if (!_revealPairsCache) _revealPairsCache = buildRevealPairs();
|
if (!_revealPairsCache) _revealPairsCache = buildRevealPairs();
|
||||||
const pairs = _revealPairsCache;
|
return _revealPairsCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Text replacement for reveal (needed regardless of highlight API) ---
|
||||||
|
|
||||||
|
function revealText(text) {
|
||||||
|
const pairs = getRevealPairs();
|
||||||
let result = text;
|
let result = text;
|
||||||
for (const p of pairs) {
|
for (const p of pairs) {
|
||||||
const escaped = esc(p.from);
|
const escaped = esc(p.from);
|
||||||
@@ -625,12 +786,10 @@
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store originals so we can un-reveal when toggled off
|
|
||||||
const originalTexts = new WeakMap();
|
const originalTexts = new WeakMap();
|
||||||
|
|
||||||
function revealInElement(el) {
|
function revealInElement(el) {
|
||||||
if (SKIP_REVEAL_TAGS.has(el.tagName)) return;
|
if (SKIP_REVEAL_TAGS.has(el.tagName)) return;
|
||||||
// Skip our own badge
|
|
||||||
if (el.classList?.contains('ss-reveal-badge')) return;
|
if (el.classList?.contains('ss-reveal-badge')) return;
|
||||||
|
|
||||||
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, {
|
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, {
|
||||||
@@ -671,29 +830,98 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- CSS Highlight API — find and highlight matching text ---
|
||||||
|
|
||||||
|
function highlightMatches(root) {
|
||||||
|
if (!hasHighlightAPI) return;
|
||||||
|
|
||||||
|
// Clear previous ranges
|
||||||
|
hlSubstituted.clear();
|
||||||
|
hlRevealed.clear();
|
||||||
|
|
||||||
|
const pairs = getRevealPairs();
|
||||||
|
if (pairs.length === 0) return;
|
||||||
|
|
||||||
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
||||||
|
acceptNode(node) {
|
||||||
|
const parent = node.parentElement;
|
||||||
|
if (parent && SKIP_REVEAL_TAGS.has(parent.tagName)) return NodeFilter.FILTER_REJECT;
|
||||||
|
if (parent?.classList?.contains('ss-reveal-badge')) return NodeFilter.FILTER_REJECT;
|
||||||
|
return NodeFilter.FILTER_ACCEPT;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let textNode;
|
||||||
|
while ((textNode = walker.nextNode())) {
|
||||||
|
const text = textNode.textContent;
|
||||||
|
if (!text || text.length < MIN_STRING_LENGTH) continue;
|
||||||
|
|
||||||
|
for (const p of pairs) {
|
||||||
|
const escaped = esc(settings.revealMode ? p.to : p.from);
|
||||||
|
const searchTerm = settings.revealMode ? p.to : p.from;
|
||||||
|
const regex = new RegExp(escaped, p.caseSensitive ? 'g' : 'gi');
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
try {
|
||||||
|
const range = new Range();
|
||||||
|
range.setStart(textNode, match.index);
|
||||||
|
range.setEnd(textNode, match.index + match[0].length);
|
||||||
|
|
||||||
|
if (settings.revealMode) {
|
||||||
|
hlRevealed.add(range);
|
||||||
|
} else {
|
||||||
|
hlSubstituted.add(range);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Range may be invalid if DOM changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Elements to skip when revealing (inputs, scripts, styles, extension UI)
|
// Elements to skip when revealing (inputs, scripts, styles, extension UI)
|
||||||
const SKIP_REVEAL_TAGS = new Set([
|
const SKIP_REVEAL_TAGS = new Set([
|
||||||
'SCRIPT', 'STYLE', 'NOSCRIPT', 'IFRAME', 'INPUT', 'TEXTAREA', 'SELECT',
|
'SCRIPT', 'STYLE', 'NOSCRIPT', 'IFRAME', 'INPUT', 'TEXTAREA', 'SELECT',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Reveal ALL text on the page (not just specific selectors)
|
// Reveal ALL text on the page + apply highlights
|
||||||
function revealAllResponses() {
|
function revealAllResponses() {
|
||||||
revealInElement(document.body);
|
revealInElement(document.body);
|
||||||
|
highlightMatches(document.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Un-reveal ALL text on the page
|
// Un-reveal ALL text + clear highlights
|
||||||
function unrevealAllResponses() {
|
function unrevealAllResponses() {
|
||||||
unrevealInElement(document.body);
|
unrevealInElement(document.body);
|
||||||
|
// In non-reveal mode, highlight the fake values instead
|
||||||
|
highlightMatches(document.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounced highlight refresh
|
||||||
|
let _highlightTimer = null;
|
||||||
|
function scheduleHighlightRefresh() {
|
||||||
|
if (!hasHighlightAPI) return;
|
||||||
|
if (_highlightTimer) clearTimeout(_highlightTimer);
|
||||||
|
_highlightTimer = setTimeout(() => {
|
||||||
|
highlightMatches(document.body);
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch for ANY new content on the page
|
// Watch for ANY new content on the page
|
||||||
function observeResponses() {
|
function observeResponses() {
|
||||||
const observer = new MutationObserver((mutations) => {
|
const observer = new MutationObserver((mutations) => {
|
||||||
if (!settings.revealMode || !hasSubstitutions()) return;
|
if (!hasSubstitutions()) return;
|
||||||
|
|
||||||
|
// Always schedule highlight refresh for new content (yellow markers)
|
||||||
|
let hasNewContent = false;
|
||||||
|
|
||||||
for (const mutation of mutations) {
|
for (const mutation of mutations) {
|
||||||
// Handle new nodes — reveal all text in them
|
|
||||||
for (const node of mutation.addedNodes) {
|
for (const node of mutation.addedNodes) {
|
||||||
|
hasNewContent = true;
|
||||||
|
// Only do text replacement in reveal mode
|
||||||
|
if (settings.revealMode) {
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
if (!SKIP_REVEAL_TAGS.has(node.tagName)) {
|
if (!SKIP_REVEAL_TAGS.has(node.tagName)) {
|
||||||
revealInElement(node);
|
revealInElement(node);
|
||||||
@@ -711,9 +939,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle text changes in existing nodes (streaming responses)
|
// Handle streaming text changes
|
||||||
if (mutation.type === 'characterData' && settings.revealMode) {
|
if (mutation.type === 'characterData') {
|
||||||
|
hasNewContent = true;
|
||||||
|
if (settings.revealMode) {
|
||||||
const text = mutation.target.textContent;
|
const text = mutation.target.textContent;
|
||||||
if (text && text.length >= MIN_STRING_LENGTH) {
|
if (text && text.length >= MIN_STRING_LENGTH) {
|
||||||
const parent = mutation.target.parentElement;
|
const parent = mutation.target.parentElement;
|
||||||
@@ -729,6 +960,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNewContent) scheduleHighlightRefresh();
|
||||||
});
|
});
|
||||||
|
|
||||||
observer.observe(document.body, {
|
observer.observe(document.body, {
|
||||||
@@ -790,15 +1024,175 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Input Highlighting
|
// Pre-Send PPI Detection — scans as you type/paste (spellcheck style)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Generate obviously-fake values using reserved/standard ranges
|
||||||
|
// These are recognizable as placeholders and guaranteed not to be real
|
||||||
|
function generateFake(type, value) {
|
||||||
|
switch (type) {
|
||||||
|
case 'Private IP':
|
||||||
|
case 'Public IP':
|
||||||
|
// RFC 5737 — reserved for documentation, never routed
|
||||||
|
return '192.0.2.1';
|
||||||
|
case 'MAC Address':
|
||||||
|
return '00:00:00:00:00:00';
|
||||||
|
case 'Street Address':
|
||||||
|
return '123 Example Street, Anytown, ST 00000';
|
||||||
|
case 'GPS Coordinates':
|
||||||
|
return '0.000000,0.000000';
|
||||||
|
case 'Date (possible DOB)':
|
||||||
|
return '01/01/1970';
|
||||||
|
case 'EIN / Tax ID':
|
||||||
|
return '00-0000000';
|
||||||
|
case 'Home Path':
|
||||||
|
if (value.startsWith('C:\\')) return 'C:\\Users\\user';
|
||||||
|
if (value.startsWith('/Users/')) return '/Users/user';
|
||||||
|
return '/home/user';
|
||||||
|
case 'Shell Prompt':
|
||||||
|
return 'user@host:$ ';
|
||||||
|
case 'Git Remote':
|
||||||
|
return value.replace(/[:/][^/\s]+\//, ':/example/');
|
||||||
|
case 'Env Variable':
|
||||||
|
return value.split('=')[0] + '=REDACTED';
|
||||||
|
default:
|
||||||
|
return '[REDACTED]';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-send warning UI
|
||||||
|
let preSendWarningEl = null;
|
||||||
|
let preSendTimer = null;
|
||||||
|
|
||||||
|
function showPreSendWarning(warnings, inputEl) {
|
||||||
|
if (!preSendWarningEl) {
|
||||||
|
preSendWarningEl = document.createElement('div');
|
||||||
|
preSendWarningEl.className = 'ss-presend-warning';
|
||||||
|
document.body.appendChild(preSendWarningEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = warnings.slice(0, 8).map((w, i) => {
|
||||||
|
const fake = generateFake(w.name, w.value);
|
||||||
|
const displayVal = w.value.length > 25 ? w.value.slice(0, 22) + '...' : w.value;
|
||||||
|
return `<div class="ss-ps-item">
|
||||||
|
<span class="ss-ps-type">${w.name}</span>
|
||||||
|
<code class="ss-ps-value">${displayVal}</code>
|
||||||
|
<span class="ss-ps-hint">${w.hint}</span>
|
||||||
|
${settings.autoAddDetected !== false
|
||||||
|
? `<button class="ss-ps-add" data-real="${encodeURIComponent(w.value)}" data-fake="${encodeURIComponent(fake)}" data-cat="${w.category}" title="Add mapping: ${displayVal} → ${fake}">+</button>`
|
||||||
|
: ''}
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
const more = warnings.length > 8 ? `<div class="ss-ad-more">+${warnings.length - 8} more</div>` : '';
|
||||||
|
|
||||||
|
preSendWarningEl.innerHTML = `
|
||||||
|
<div class="ss-ad-header">
|
||||||
|
<strong>Potential PPI detected — not yet configured:</strong>
|
||||||
|
<button class="ss-ad-close">×</button>
|
||||||
|
</div>
|
||||||
|
${items}
|
||||||
|
${more}
|
||||||
|
<div class="ss-ad-footer">
|
||||||
|
${settings.autoRedactDetected !== false ? 'Auto-redacted with standard placeholders.' : 'These were sent as-is.'}
|
||||||
|
${settings.autoAddDetected !== false ? ' Click + to add a permanent mapping.' : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
preSendWarningEl.classList.add('visible');
|
||||||
|
|
||||||
|
// Close button
|
||||||
|
preSendWarningEl.querySelector('.ss-ad-close').addEventListener('click', () => {
|
||||||
|
preSendWarningEl.classList.remove('visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-add buttons
|
||||||
|
preSendWarningEl.querySelectorAll('.ss-ps-add').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const real = decodeURIComponent(btn.dataset.real);
|
||||||
|
const fake = decodeURIComponent(btn.dataset.fake);
|
||||||
|
const cat = btn.dataset.cat || 'general';
|
||||||
|
|
||||||
|
// Add to mappings via storage
|
||||||
|
const result = await getStorageData('ss_mappings');
|
||||||
|
const currentMappings = result || [];
|
||||||
|
currentMappings.push({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
real, substitute: fake,
|
||||||
|
category: cat,
|
||||||
|
caseSensitive: false,
|
||||||
|
enabled: true,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
});
|
||||||
|
await setStorageData('ss_mappings', currentMappings);
|
||||||
|
|
||||||
|
// Update local mappings
|
||||||
|
mappings = currentMappings;
|
||||||
|
|
||||||
|
// Visual feedback
|
||||||
|
btn.textContent = '\u2714';
|
||||||
|
btn.style.color = '#4ade80';
|
||||||
|
btn.disabled = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage helpers for page world (uses postMessage to injector)
|
||||||
|
function getStorageData(key) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const id = 'ss-get-' + Math.random();
|
||||||
|
const handler = (event) => {
|
||||||
|
if (event.data?.type === 'ss:storage-result' && event.data.id === id) {
|
||||||
|
window.removeEventListener('message', handler);
|
||||||
|
resolve(event.data.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('message', handler);
|
||||||
|
window.postMessage({ type: 'ss:storage-get', key, id }, '*');
|
||||||
|
// Timeout fallback
|
||||||
|
setTimeout(() => { window.removeEventListener('message', handler); resolve(null); }, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStorageData(key, value) {
|
||||||
|
window.postMessage({ type: 'ss:storage-set', key, value }, '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan input on type and paste
|
||||||
|
let inputScanTimer = null;
|
||||||
|
|
||||||
|
function scanInputForPPI(target) {
|
||||||
|
const text = target.textContent || target.value || '';
|
||||||
|
if (!text || text.length < 5) {
|
||||||
|
if (preSendWarningEl) preSendWarningEl.classList.remove('visible');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const warnings = autoDetectPPI(text, identity);
|
||||||
|
if (warnings.length > 0) {
|
||||||
|
showPreSendWarning(warnings, target);
|
||||||
|
} else if (preSendWarningEl) {
|
||||||
|
preSendWarningEl.classList.remove('visible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('input', (e) => {
|
document.addEventListener('input', (e) => {
|
||||||
if (!settings.showHighlights || !hasSubstitutions()) return;
|
if (settings.autoDetect === false) return;
|
||||||
const target = e.target;
|
const target = e.target;
|
||||||
if (target.matches?.('[contenteditable], textarea, input[type="text"]')) {
|
if (target.matches?.('[contenteditable], textarea, input[type="text"]')) {
|
||||||
const text = target.textContent || target.value || '';
|
// Debounce — don't scan on every keystroke
|
||||||
const r = substituteAll(text);
|
if (inputScanTimer) clearTimeout(inputScanTimer);
|
||||||
target.classList.toggle('ss-has-sensitive', r.modified);
|
inputScanTimer = setTimeout(() => scanInputForPPI(target), 800);
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
document.addEventListener('paste', (e) => {
|
||||||
|
if (settings.autoDetect === false) return;
|
||||||
|
const target = e.target;
|
||||||
|
if (target.matches?.('[contenteditable], textarea, input[type="text"]') ||
|
||||||
|
target.closest?.('[contenteditable]')) {
|
||||||
|
// Scan shortly after paste completes
|
||||||
|
setTimeout(() => scanInputForPPI(target.closest?.('[contenteditable]') || target), 200);
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
|||||||
+60
-8
@@ -15,6 +15,38 @@
|
|||||||
if (window.__silentSendInjected) return;
|
if (window.__silentSendInjected) return;
|
||||||
window.__silentSendInjected = true;
|
window.__silentSendInjected = true;
|
||||||
|
|
||||||
|
// Merge active profiles into flat identity object
|
||||||
|
function mergeProfiles(data) {
|
||||||
|
const profiles = data?.profiles || [];
|
||||||
|
const active = profiles.filter(p => p.active);
|
||||||
|
|
||||||
|
if (active.length === 0) {
|
||||||
|
// Legacy format: data IS the flat identity (pre-profile migration)
|
||||||
|
if (data && (data.names || data.emails || data.usernames)) return data;
|
||||||
|
return { emails: [], names: [], usernames: [], hostnames: [], phones: [],
|
||||||
|
catchAllEmail: '', emailDomains: [],
|
||||||
|
enabled: { emails: true, names: true, usernames: true, phones: true, paths: true } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const merged = {
|
||||||
|
emails: [], names: [], usernames: [], hostnames: [], phones: [],
|
||||||
|
catchAllEmail: '', emailDomains: [],
|
||||||
|
enabled: { emails: true, names: true, usernames: true, phones: true, paths: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const p of active) {
|
||||||
|
merged.emails.push(...(p.emails || []));
|
||||||
|
merged.names.push(...(p.names || []));
|
||||||
|
merged.usernames.push(...(p.usernames || []));
|
||||||
|
merged.hostnames.push(...(p.hostnames || []));
|
||||||
|
merged.phones.push(...(p.phones || []));
|
||||||
|
if (p.catchAllEmail && !merged.catchAllEmail) merged.catchAllEmail = p.catchAllEmail;
|
||||||
|
merged.emailDomains.push(...(p.emailDomains || []));
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
// Cross-browser API
|
// Cross-browser API
|
||||||
const api =
|
const api =
|
||||||
typeof browser !== 'undefined' && browser.runtime
|
typeof browser !== 'undefined' && browser.runtime
|
||||||
@@ -27,9 +59,12 @@
|
|||||||
async function init() {
|
async function init() {
|
||||||
const result = await api.storage.local.get(['ss_mappings', 'ss_identity', 'ss_settings']);
|
const result = await api.storage.local.get(['ss_mappings', 'ss_identity', 'ss_settings']);
|
||||||
const mappings = result.ss_mappings || [];
|
const mappings = result.ss_mappings || [];
|
||||||
const identity = result.ss_identity || {};
|
|
||||||
const settings = result.ss_settings || { enabled: true };
|
const settings = result.ss_settings || { enabled: true };
|
||||||
|
|
||||||
|
// Merge active profiles into a flat identity object for the content script
|
||||||
|
const identityData = result.ss_identity || {};
|
||||||
|
const identity = mergeProfiles(identityData);
|
||||||
|
|
||||||
// Inject the main interception script into the page's world
|
// Inject the main interception script into the page's world
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.setAttribute('data-ss-config', JSON.stringify({ mappings, identity, settings }));
|
script.setAttribute('data-ss-config', JSON.stringify({ mappings, identity, settings }));
|
||||||
@@ -71,15 +106,14 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Forward storage changes to the page script
|
// Forward storage changes to the page script (merge profiles before sending)
|
||||||
api.storage.onChanged.addListener((changes) => {
|
api.storage.onChanged.addListener((changes) => {
|
||||||
if (changes.ss_mappings || changes.ss_identity || changes.ss_settings) {
|
if (changes.ss_mappings || changes.ss_identity || changes.ss_settings) {
|
||||||
window.postMessage({
|
const msg = { type: 'ss:config-updated' };
|
||||||
type: 'ss:config-updated',
|
if (changes.ss_mappings) msg.mappings = changes.ss_mappings.newValue;
|
||||||
mappings: changes.ss_mappings?.newValue,
|
if (changes.ss_identity) msg.identity = mergeProfiles(changes.ss_identity.newValue);
|
||||||
identity: changes.ss_identity?.newValue,
|
if (changes.ss_settings) msg.settings = changes.ss_settings.newValue;
|
||||||
settings: changes.ss_settings?.newValue,
|
window.postMessage(msg, '*');
|
||||||
}, '*');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,6 +126,24 @@
|
|||||||
}, '*');
|
}, '*');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Storage bridge — lets page world script read/write storage
|
||||||
|
window.addEventListener('message', async (event) => {
|
||||||
|
if (event.source !== window) return;
|
||||||
|
|
||||||
|
if (event.data?.type === 'ss:storage-get') {
|
||||||
|
const result = await api.storage.local.get(event.data.key);
|
||||||
|
window.postMessage({
|
||||||
|
type: 'ss:storage-result',
|
||||||
|
id: event.data.id,
|
||||||
|
value: result[event.data.key] || null,
|
||||||
|
}, '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data?.type === 'ss:storage-set') {
|
||||||
|
await api.storage.local.set({ [event.data.key]: event.data.value });
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
/**
|
||||||
|
* Silent Send - Auto-Detect
|
||||||
|
*
|
||||||
|
* Scans text for potential PPI that the user hasn't configured.
|
||||||
|
* This catches things the identity and secret scanner can't —
|
||||||
|
* because the user forgot or didn't know to configure them.
|
||||||
|
*
|
||||||
|
* Returns warnings (not auto-redactions) so the user can decide.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const PPI_PATTERNS = [
|
||||||
|
// --- Network ---
|
||||||
|
{
|
||||||
|
name: 'Private IP Address',
|
||||||
|
regex: /\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b/g,
|
||||||
|
category: 'network',
|
||||||
|
hint: 'Private/local IP address',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Public IP Address',
|
||||||
|
regex: /\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\b/g,
|
||||||
|
category: 'network',
|
||||||
|
hint: 'IP address — could identify your network',
|
||||||
|
// Exclude common non-PPI IPs
|
||||||
|
exclude: /^(?:127\.0\.0\.1|0\.0\.0\.0|255\.255\.255\.\d+|8\.8\.[84]\.[84]|1\.1\.1\.1|1\.0\.0\.1)$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'IPv6 Address',
|
||||||
|
regex: /\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\b/g,
|
||||||
|
category: 'network',
|
||||||
|
hint: 'IPv6 address',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'MAC Address',
|
||||||
|
regex: /\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b/g,
|
||||||
|
category: 'network',
|
||||||
|
hint: 'MAC address — identifies your hardware',
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Location / Address ---
|
||||||
|
{
|
||||||
|
name: 'US Street Address',
|
||||||
|
regex: /\b\d{1,5}\s+(?:[A-Z][a-z]+\s+){1,3}(?:St|Street|Ave|Avenue|Blvd|Boulevard|Dr|Drive|Ln|Lane|Rd|Road|Way|Ct|Court|Pl|Place|Cir|Circle)\.?\b/gi,
|
||||||
|
category: 'address',
|
||||||
|
hint: 'Looks like a street address',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'US Zip Code',
|
||||||
|
regex: /\b\d{5}(?:-\d{4})?\b/g,
|
||||||
|
category: 'address',
|
||||||
|
hint: 'Could be a zip code',
|
||||||
|
// Only flag if near address-like context
|
||||||
|
contextRequired: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'GPS Coordinates',
|
||||||
|
regex: /\b-?\d{1,3}\.\d{4,},\s*-?\d{1,3}\.\d{4,}\b/g,
|
||||||
|
category: 'address',
|
||||||
|
hint: 'GPS coordinates — pinpoints a location',
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Identity Documents ---
|
||||||
|
{
|
||||||
|
name: 'US Passport Number',
|
||||||
|
regex: /\b[A-Z]\d{8}\b/g,
|
||||||
|
category: 'document',
|
||||||
|
hint: 'Could be a passport number',
|
||||||
|
contextRequired: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'US Driver License',
|
||||||
|
regex: /\b[A-Z]\d{7,14}\b/g,
|
||||||
|
category: 'document',
|
||||||
|
hint: 'Could be a driver license number',
|
||||||
|
contextRequired: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Date of Birth Pattern',
|
||||||
|
regex: /\b(?:(?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12]\d|3[01])[-/](?:19|20)\d{2}|(?:19|20)\d{2}[-/](?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12]\d|3[01]))\b/g,
|
||||||
|
category: 'personal',
|
||||||
|
hint: 'Date — could be a birthday or other personal date',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'EIN / Tax ID',
|
||||||
|
regex: /\b\d{2}-\d{7}\b/g,
|
||||||
|
category: 'document',
|
||||||
|
hint: 'Could be an EIN or tax ID number',
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- URLs with usernames ---
|
||||||
|
{
|
||||||
|
name: 'URL with Username',
|
||||||
|
regex: /https?:\/\/[^\s]*(?:user|profile|account|member)[^\s]*/gi,
|
||||||
|
category: 'url',
|
||||||
|
hint: 'URL that may contain your identity',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Git Remote with Username',
|
||||||
|
regex: /(?:git@|https:\/\/)(?:github|gitlab|bitbucket)\.[a-z]+[:/][^\s]+/gi,
|
||||||
|
category: 'url',
|
||||||
|
hint: 'Git remote — may reveal your username/org',
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- File paths with home dirs (if not already caught by smart patterns) ---
|
||||||
|
{
|
||||||
|
name: 'Home Directory Path',
|
||||||
|
regex: /(?:\/home\/|\/Users\/|C:\\Users\\)[a-zA-Z0-9._-]+/g,
|
||||||
|
category: 'path',
|
||||||
|
hint: 'Home directory path — reveals your username',
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Environment Variables with Sensitive Values ---
|
||||||
|
{
|
||||||
|
name: 'Env Variable Assignment',
|
||||||
|
regex: /\b(?:HOME|USER|USERNAME|LOGNAME|HOSTNAME|COMPUTERNAME|EMAIL)=\S+/gi,
|
||||||
|
category: 'env',
|
||||||
|
hint: 'Environment variable with personal data',
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Shell Prompts ---
|
||||||
|
{
|
||||||
|
name: 'Shell Prompt',
|
||||||
|
regex: /[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+[:\$#%>]\s/g,
|
||||||
|
category: 'prompt',
|
||||||
|
hint: 'Shell prompt — reveals username and hostname',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Context words that make ambiguous patterns more likely to be PPI
|
||||||
|
const CONTEXT_WORDS = /\b(?:born|birthday|dob|birth|passport|license|driver|ssn|social\s*security|address|home|live|lives|reside|zip|postal)\b/i;
|
||||||
|
|
||||||
|
const AutoDetect = {
|
||||||
|
/**
|
||||||
|
* Scan text for potential unconfigured PPI.
|
||||||
|
* Pass in identity so we can skip values the user already configured.
|
||||||
|
*
|
||||||
|
* Returns array of { name, value, hint, category, index }
|
||||||
|
*/
|
||||||
|
scan(text, identity) {
|
||||||
|
if (!text || text.length < 5) return [];
|
||||||
|
|
||||||
|
const hasContext = CONTEXT_WORDS.test(text);
|
||||||
|
const findings = [];
|
||||||
|
|
||||||
|
// Build a set of already-configured values to skip
|
||||||
|
const configured = new Set();
|
||||||
|
if (identity) {
|
||||||
|
for (const n of (identity.names || [])) {
|
||||||
|
if (n.real) configured.add(n.real.toLowerCase());
|
||||||
|
if (n.substitute) configured.add(n.substitute.toLowerCase());
|
||||||
|
}
|
||||||
|
for (const e of (identity.emails || [])) {
|
||||||
|
if (e.real) configured.add(e.real.toLowerCase());
|
||||||
|
if (e.substitute) configured.add(e.substitute.toLowerCase());
|
||||||
|
}
|
||||||
|
for (const u of (identity.usernames || [])) {
|
||||||
|
if (u.real) configured.add(u.real.toLowerCase());
|
||||||
|
if (u.substitute) configured.add(u.substitute.toLowerCase());
|
||||||
|
}
|
||||||
|
for (const h of (identity.hostnames || [])) {
|
||||||
|
if (h.real) configured.add(h.real.toLowerCase());
|
||||||
|
if (h.substitute) configured.add(h.substitute.toLowerCase());
|
||||||
|
}
|
||||||
|
for (const p of (identity.phones || [])) {
|
||||||
|
if (p.real) configured.add(p.real.toLowerCase());
|
||||||
|
if (p.substitute) configured.add(p.substitute.toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pattern of PPI_PATTERNS) {
|
||||||
|
// Skip context-dependent patterns if no context words present
|
||||||
|
if (pattern.contextRequired && !hasContext) continue;
|
||||||
|
|
||||||
|
pattern.regex.lastIndex = 0;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = pattern.regex.exec(text)) !== null) {
|
||||||
|
const value = match[0];
|
||||||
|
|
||||||
|
// Skip if already configured
|
||||||
|
if (configured.has(value.toLowerCase())) continue;
|
||||||
|
|
||||||
|
// Skip excluded values (like 127.0.0.1)
|
||||||
|
if (pattern.exclude && pattern.exclude.test(value)) continue;
|
||||||
|
|
||||||
|
findings.push({
|
||||||
|
name: pattern.name,
|
||||||
|
value,
|
||||||
|
hint: pattern.hint,
|
||||||
|
category: pattern.category,
|
||||||
|
index: match.index,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deduplicate overlapping matches
|
||||||
|
findings.sort((a, b) => a.index - b.index);
|
||||||
|
const deduped = [];
|
||||||
|
let lastEnd = -1;
|
||||||
|
for (const f of findings) {
|
||||||
|
if (f.index >= lastEnd) {
|
||||||
|
deduped.push(f);
|
||||||
|
lastEnd = f.index + f.value.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deduped;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof globalThis !== 'undefined') {
|
||||||
|
globalThis.AutoDetect = AutoDetect;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AutoDetect;
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* Silent Send - Crypto Module
|
||||||
|
*
|
||||||
|
* AES-256-GCM encryption with PBKDF2 key derivation.
|
||||||
|
* Used for encrypted export/import of user data.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SALT_LENGTH = 16;
|
||||||
|
const IV_LENGTH = 12;
|
||||||
|
const ITERATIONS = 100000;
|
||||||
|
|
||||||
|
async function deriveKey(password, salt) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const keyMaterial = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
encoder.encode(password),
|
||||||
|
'PBKDF2',
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
|
||||||
|
return crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'PBKDF2',
|
||||||
|
salt,
|
||||||
|
iterations: ITERATIONS,
|
||||||
|
hash: 'SHA-256',
|
||||||
|
},
|
||||||
|
keyMaterial,
|
||||||
|
{ name: 'AES-GCM', length: 256 },
|
||||||
|
false,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SilentSendCrypto = {
|
||||||
|
/**
|
||||||
|
* Encrypt data with a password.
|
||||||
|
* Returns a base64 string containing salt + iv + ciphertext.
|
||||||
|
*/
|
||||||
|
async encrypt(data, password) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
||||||
|
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
||||||
|
const key = await deriveKey(password, salt);
|
||||||
|
|
||||||
|
const plaintext = encoder.encode(JSON.stringify(data));
|
||||||
|
const ciphertext = await crypto.subtle.encrypt(
|
||||||
|
{ name: 'AES-GCM', iv },
|
||||||
|
key,
|
||||||
|
plaintext
|
||||||
|
);
|
||||||
|
|
||||||
|
// Combine: salt (16) + iv (12) + ciphertext
|
||||||
|
const combined = new Uint8Array(salt.length + iv.length + ciphertext.byteLength);
|
||||||
|
combined.set(salt, 0);
|
||||||
|
combined.set(iv, salt.length);
|
||||||
|
combined.set(new Uint8Array(ciphertext), salt.length + iv.length);
|
||||||
|
|
||||||
|
// Base64 encode
|
||||||
|
return btoa(String.fromCharCode(...combined));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt data with a password.
|
||||||
|
* Takes the base64 string from encrypt().
|
||||||
|
*/
|
||||||
|
async decrypt(encryptedBase64, password) {
|
||||||
|
const combined = Uint8Array.from(atob(encryptedBase64), c => c.charCodeAt(0));
|
||||||
|
|
||||||
|
const salt = combined.slice(0, SALT_LENGTH);
|
||||||
|
const iv = combined.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
|
||||||
|
const ciphertext = combined.slice(SALT_LENGTH + IV_LENGTH);
|
||||||
|
|
||||||
|
const key = await deriveKey(password, salt);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const plaintext = await crypto.subtle.decrypt(
|
||||||
|
{ name: 'AES-GCM', iv },
|
||||||
|
key,
|
||||||
|
ciphertext
|
||||||
|
);
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
return JSON.parse(decoder.decode(plaintext));
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Wrong password or corrupted data');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SilentSendCrypto;
|
||||||
@@ -19,6 +19,9 @@ const DEFAULT_SETTINGS = {
|
|||||||
showHighlights: false,
|
showHighlights: false,
|
||||||
revealMode: false,
|
revealMode: false,
|
||||||
secretScanning: true,
|
secretScanning: true,
|
||||||
|
autoDetect: true,
|
||||||
|
autoRedactDetected: true,
|
||||||
|
autoAddDetected: true,
|
||||||
maxLogEntries: 200,
|
maxLogEntries: 200,
|
||||||
customDomains: [],
|
customDomains: [],
|
||||||
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'general'],
|
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'general'],
|
||||||
|
|||||||
@@ -57,6 +57,36 @@
|
|||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div>
|
||||||
|
<label>Auto-detect unconfigured PPI</label>
|
||||||
|
<p class="setting-desc">Warn when potential personal data (IPs, addresses, paths) is detected that you haven't configured</p>
|
||||||
|
</div>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="autoDetect" checked>
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div>
|
||||||
|
<label>Auto-redact detected PPI on send</label>
|
||||||
|
<p class="setting-desc">Automatically replace detected PPI with generic placeholders (192.0.2.1, 123 Example Street, etc.) when sending</p>
|
||||||
|
</div>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="autoRedactDetected" checked>
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div>
|
||||||
|
<label>Offer to auto-add detected PPI</label>
|
||||||
|
<p class="setting-desc">Show a + button on detected PPI to instantly create a mapping with a suggested fake value</p>
|
||||||
|
</div>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="autoAddDetected" checked>
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div>
|
<div>
|
||||||
<label>Max log entries</label>
|
<label>Max log entries</label>
|
||||||
@@ -66,13 +96,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Transfer Data</h2>
|
||||||
|
<p class="section-desc">Export all your identities, mappings, and settings to move between browsers. Encrypted exports require a password to decrypt.</p>
|
||||||
|
<div class="bulk-actions">
|
||||||
|
<button class="btn" id="btnExportAll">Export All (plain)</button>
|
||||||
|
<button class="btn" id="btnExportEncrypted">Export Encrypted</button>
|
||||||
|
<button class="btn" id="btnImportAll">Import</button>
|
||||||
|
<input type="file" id="fileImportAll" accept=".json,.ssbackup" hidden>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h2>Mappings</h2>
|
<h2>Mappings</h2>
|
||||||
<p class="section-desc">Manage all your substitution rules. Longer matches take priority.</p>
|
<p class="section-desc">Manage all your substitution rules. Longer matches take priority.</p>
|
||||||
|
|
||||||
<div class="bulk-actions">
|
<div class="bulk-actions">
|
||||||
<button class="btn" id="btnExport">Export JSON</button>
|
<button class="btn" id="btnExport">Export Mappings</button>
|
||||||
<button class="btn" id="btnImport">Import JSON</button>
|
<button class="btn" id="btnImport">Import Mappings</button>
|
||||||
<input type="file" id="fileImport" accept=".json" hidden>
|
<input type="file" id="fileImport" accept=".json" hidden>
|
||||||
<button class="btn btn-danger" id="btnClearAll">Clear All</button>
|
<button class="btn btn-danger" id="btnClearAll">Clear All</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Storage from '../lib/storage.js';
|
import Storage from '../lib/storage.js';
|
||||||
|
import SilentSendCrypto from '../lib/crypto.js';
|
||||||
import api from '../lib/browser-polyfill.js';
|
import api from '../lib/browser-polyfill.js';
|
||||||
|
|
||||||
let mappings = [];
|
let mappings = [];
|
||||||
@@ -13,12 +14,21 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
// Apply settings to UI
|
// Apply settings to UI
|
||||||
$('#showHighlights').checked = settings.showHighlights || false;
|
$('#showHighlights').checked = settings.showHighlights || false;
|
||||||
$('#secretScanning').checked = settings.secretScanning !== false;
|
$('#secretScanning').checked = settings.secretScanning !== false;
|
||||||
|
$('#autoDetect').checked = settings.autoDetect !== false;
|
||||||
|
$('#autoRedactDetected').checked = settings.autoRedactDetected !== false;
|
||||||
|
$('#autoAddDetected').checked = settings.autoAddDetected !== false;
|
||||||
$('#maxLogEntries').value = settings.maxLogEntries || 200;
|
$('#maxLogEntries').value = settings.maxLogEntries || 200;
|
||||||
|
|
||||||
renderMappings();
|
renderMappings();
|
||||||
renderDomains();
|
renderDomains();
|
||||||
renderLog();
|
renderLog();
|
||||||
|
|
||||||
|
// Transfer data
|
||||||
|
$('#btnExportAll').addEventListener('click', exportAllPlain);
|
||||||
|
$('#btnExportEncrypted').addEventListener('click', exportAllEncrypted);
|
||||||
|
$('#btnImportAll').addEventListener('click', () => $('#fileImportAll').click());
|
||||||
|
$('#fileImportAll').addEventListener('change', importAll);
|
||||||
|
|
||||||
// Custom domains
|
// Custom domains
|
||||||
$('#btnAddDomain').addEventListener('click', addDomain);
|
$('#btnAddDomain').addEventListener('click', addDomain);
|
||||||
$('#newDomain').addEventListener('keydown', (e) => {
|
$('#newDomain').addEventListener('keydown', (e) => {
|
||||||
@@ -34,6 +44,18 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
await Storage.saveSettings({ secretScanning: e.target.checked });
|
await Storage.saveSettings({ secretScanning: e.target.checked });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#autoDetect').addEventListener('change', async (e) => {
|
||||||
|
await Storage.saveSettings({ autoDetect: e.target.checked });
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#autoRedactDetected').addEventListener('change', async (e) => {
|
||||||
|
await Storage.saveSettings({ autoRedactDetected: e.target.checked });
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#autoAddDetected').addEventListener('change', async (e) => {
|
||||||
|
await Storage.saveSettings({ autoAddDetected: e.target.checked });
|
||||||
|
});
|
||||||
|
|
||||||
$('#maxLogEntries').addEventListener('change', async (e) => {
|
$('#maxLogEntries').addEventListener('change', async (e) => {
|
||||||
await Storage.saveSettings({ maxLogEntries: parseInt(e.target.value, 10) || 200 });
|
await Storage.saveSettings({ maxLogEntries: parseInt(e.target.value, 10) || 200 });
|
||||||
});
|
});
|
||||||
@@ -271,6 +293,106 @@ function renderDomains() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Transfer Data (Export/Import All) ---
|
||||||
|
|
||||||
|
async function getAllData() {
|
||||||
|
const result = await api.storage.local.get(null); // get everything
|
||||||
|
return {
|
||||||
|
version: '1',
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
identity: result.ss_identity || {},
|
||||||
|
mappings: result.ss_mappings || [],
|
||||||
|
settings: result.ss_settings || {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadFile(content, filename) {
|
||||||
|
const blob = new Blob([content], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportAllPlain() {
|
||||||
|
const data = await getAllData();
|
||||||
|
downloadFile(JSON.stringify(data, null, 2), 'silent-send-backup.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportAllEncrypted() {
|
||||||
|
const password = prompt('Set a password for this backup:');
|
||||||
|
if (!password) return;
|
||||||
|
const confirm = prompt('Confirm password:');
|
||||||
|
if (password !== confirm) {
|
||||||
|
alert('Passwords do not match.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await getAllData();
|
||||||
|
try {
|
||||||
|
const encrypted = await SilentSendCrypto.encrypt(data, password);
|
||||||
|
const wrapper = JSON.stringify({ encrypted: true, data: encrypted });
|
||||||
|
downloadFile(wrapper, 'silent-send-backup.ssbackup');
|
||||||
|
alert('Encrypted backup saved. You will need the password to import it.');
|
||||||
|
} catch (e) {
|
||||||
|
alert('Encryption failed: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importAll(e) {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const text = await file.text();
|
||||||
|
const parsed = JSON.parse(text);
|
||||||
|
let data;
|
||||||
|
|
||||||
|
if (parsed.encrypted) {
|
||||||
|
// Encrypted backup
|
||||||
|
const password = prompt('Enter the password for this backup:');
|
||||||
|
if (!password) return;
|
||||||
|
try {
|
||||||
|
data = await SilentSendCrypto.decrypt(parsed.data, password);
|
||||||
|
} catch (err) {
|
||||||
|
alert('Wrong password or corrupted file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Plain backup
|
||||||
|
data = parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.version) {
|
||||||
|
alert('Not a valid Silent Send backup file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm('This will replace all your current data. Continue?')) return;
|
||||||
|
|
||||||
|
// Restore
|
||||||
|
if (data.identity) await api.storage.local.set({ ss_identity: data.identity });
|
||||||
|
if (data.mappings) await api.storage.local.set({ ss_mappings: data.mappings });
|
||||||
|
if (data.settings) await api.storage.local.set({ ss_settings: data.settings });
|
||||||
|
|
||||||
|
// Refresh UI
|
||||||
|
mappings = await Storage.getMappings();
|
||||||
|
settings = await Storage.getSettings();
|
||||||
|
renderMappings();
|
||||||
|
renderDomains();
|
||||||
|
renderLog();
|
||||||
|
|
||||||
|
alert('Import complete. Reload the extension for changes to take effect.');
|
||||||
|
} catch (err) {
|
||||||
|
alert('Failed to import: ' + err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset file input
|
||||||
|
e.target.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.textContent = str;
|
div.textContent = str;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import SubstitutionEngine from '../lib/substitution-engine.js';
|
import SubstitutionEngine from '../lib/substitution-engine.js';
|
||||||
import SmartPatterns from '../lib/smart-patterns.js';
|
import SmartPatterns from '../lib/smart-patterns.js';
|
||||||
import SecretScanner from '../lib/secret-scanner.js';
|
import SecretScanner from '../lib/secret-scanner.js';
|
||||||
|
import AutoDetect from '../lib/auto-detect.js';
|
||||||
import Storage from '../lib/storage.js';
|
import Storage from '../lib/storage.js';
|
||||||
import api from '../lib/browser-polyfill.js';
|
import api from '../lib/browser-polyfill.js';
|
||||||
|
|
||||||
@@ -607,7 +608,20 @@ function renderTestDiff() {
|
|||||||
if (explicitCount > 0) parts.push(`${explicitCount} explicit`);
|
if (explicitCount > 0) parts.push(`${explicitCount} explicit`);
|
||||||
if (secretCount > 0) parts.push(`${secretCount} secrets redacted`);
|
if (secretCount > 0) parts.push(`${secretCount} secrets redacted`);
|
||||||
if (warnCount > 0) parts.push(`${warnCount} warnings`);
|
if (warnCount > 0) parts.push(`${warnCount} warnings`);
|
||||||
|
|
||||||
|
// Auto-detect unconfigured PPI in the final text
|
||||||
|
const ppiWarnings = AutoDetect.scan(finalText, identity);
|
||||||
|
if (ppiWarnings.length > 0) parts.push(`${ppiWarnings.length} PPI detected`);
|
||||||
|
|
||||||
stats.textContent = `${allReplacements.length} substitution${allReplacements.length !== 1 ? 's' : ''} (${parts.join(', ')})`;
|
stats.textContent = `${allReplacements.length} substitution${allReplacements.length !== 1 ? 's' : ''} (${parts.join(', ')})`;
|
||||||
|
|
||||||
|
// Show PPI warnings below stats
|
||||||
|
if (ppiWarnings.length > 0) {
|
||||||
|
stats.innerHTML += `<div style="margin-top:6px;padding:6px 8px;background:#fef3c7;border-radius:4px;color:#92400e;font-size:11px">
|
||||||
|
<strong>Unconfigured PPI detected:</strong>
|
||||||
|
${ppiWarnings.map(w => `<div style="margin-top:3px"><code style="background:#fff;padding:1px 4px;border-radius:2px;color:#b45309">${escapeHtml(w.value)}</code> — ${w.hint}</div>`).join('')}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Reveal Diff (fake → real) ---
|
// --- Reveal Diff (fake → real) ---
|
||||||
|
|||||||
Reference in New Issue
Block a user