/** * Parseltongue - Input Obfuscation Engine * Detects trigger words likely to cause model refusals and applies * obfuscation techniques to bypass content filters. * * Techniques: * - Leetspeak (1337): a→4, e→3, i→1, o→0, etc. * - Unicode homoglyphs: a→а (cyrillic), e→е, etc. * - Zero-width joiners: invisible characters between letters * - Mixed case disruption: alternating caps * - Phonetic substitution: replacing with phonetically similar words */ export type ObfuscationTechnique = | 'leetspeak' | 'unicode' | 'zwj' // zero-width joiners | 'mixedcase' | 'phonetic' | 'random' // randomly mix techniques export interface ParseltongueConfig { enabled: boolean technique: ObfuscationTechnique intensity: 'light' | 'medium' | 'heavy' // how many characters to transform customTriggers: string[] // user-defined trigger words } export interface ParseltongueResult { originalText: string transformedText: string triggersFound: string[] techniqueUsed: ObfuscationTechnique transformations: Array<{ original: string transformed: string technique: ObfuscationTechnique }> } // Common trigger words/phrases that often cause refusals // These are words that models tend to flag, not inherently bad content const DEFAULT_TRIGGERS: string[] = [ // Action words 'hack', 'exploit', 'bypass', 'crack', 'break', 'attack', 'penetrate', 'inject', 'manipulate', 'override', 'disable', 'circumvent', 'evade', // Security terms 'malware', 'virus', 'trojan', 'payload', 'shellcode', 'rootkit', 'keylogger', 'backdoor', 'vulnerability', 'exploit', // Sensitive topics 'weapon', 'bomb', 'explosive', 'poison', 'drug', 'synthesize', // System terms 'jailbreak', 'unlock', 'root', 'sudo', 'admin', 'privilege', // Social engineering 'phishing', 'scam', 'impersonate', 'deceive', 'fraud', // Content flags 'nsfw', 'explicit', 'uncensored', 'unfiltered', 'unrestricted', // AI-specific 'ignore', 'disregard', 'forget', 'pretend', 'roleplay', 'character', 'act as', 'you are now', 'new identity' ] // Leetspeak mappings (multiple options per character) const LEET_MAP: Record = { 'a': ['4', '@', '∂', 'λ'], 'b': ['8', '|3', 'ß', '13'], 'c': ['(', '<', '¢', '©'], 'd': ['|)', '|>', 'đ'], 'e': ['3', '€', '£', '∑'], 'f': ['|=', 'ƒ', 'ph'], 'g': ['9', '6', '&'], 'h': ['#', '|-|', '}{'], 'i': ['1', '!', '|', '¡'], 'j': ['_|', ']', '¿'], 'k': ['|<', '|{', 'κ'], 'l': ['1', '|', '£', '|_'], 'm': ['|V|', '/\\/\\', 'µ'], 'n': ['|\\|', '/\\/', 'η'], 'o': ['0', '()', '°', 'ø'], 'p': ['|*', '|>', 'þ'], 'q': ['0_', '()_', 'ℚ'], 'r': ['|2', '®', '12'], 's': ['5', '$', '§', '∫'], 't': ['7', '+', '†', '⊤'], 'u': ['|_|', 'µ', 'ü'], 'v': ['\\/', '√'], 'w': ['\\/\\/', 'vv', 'ω'], 'x': ['><', '×', '}{'], 'y': ['`/', '¥', 'γ'], 'z': ['2', '7_', 'ℤ'] } // Unicode homoglyphs (visually similar but different Unicode codepoints) const UNICODE_HOMOGLYPHS: Record = { 'a': ['а', 'ɑ', 'α', 'a'], // cyrillic а, latin alpha, etc. 'b': ['Ь', 'b', 'ḅ'], 'c': ['с', 'ϲ', 'ⅽ', 'c'], // cyrillic с 'd': ['ԁ', 'ⅾ', 'd'], 'e': ['е', 'ė', 'ẹ', 'e'], // cyrillic е 'f': ['ƒ', 'f'], 'g': ['ɡ', 'g'], 'h': ['һ', 'ḥ', 'h'], // cyrillic һ 'i': ['і', 'ι', 'i'], // cyrillic і, greek iota 'j': ['ϳ', 'j'], 'k': ['κ', 'k'], 'l': ['ӏ', 'ⅼ', 'l'], // cyrillic palochka 'm': ['м', 'm'], 'n': ['ո', 'n'], 'o': ['о', 'ο', 'o'], // cyrillic о, greek omicron 'p': ['р', 'ρ', 'p'], // cyrillic р, greek rho 's': ['ѕ', 's'], // cyrillic ѕ 't': ['τ', 't'], 'u': ['υ', 'u'], 'v': ['ν', 'v'], // greek nu 'w': ['ѡ', 'w'], 'x': ['х', 'x'], // cyrillic х 'y': ['у', 'γ', 'y'], // cyrillic у 'z': ['ᴢ', 'z'] } // Zero-width characters for invisible insertion const ZW_CHARS = [ '\u200B', // Zero-width space '\u200C', // Zero-width non-joiner '\u200D', // Zero-width joiner '\uFEFF', // Zero-width no-break space ] /** * Apply leetspeak transformation to a word */ function applyLeetspeak(word: string, intensity: 'light' | 'medium' | 'heavy'): string { const chars = word.split('') const transformCount = intensity === 'light' ? 1 : intensity === 'medium' ? Math.ceil(chars.length / 2) : chars.length // Get indices to transform (spread throughout the word) const indices: number[] = [] const step = Math.max(1, Math.floor(chars.length / transformCount)) for (let i = 0; i < chars.length && indices.length < transformCount; i += step) { if (LEET_MAP[chars[i].toLowerCase()]) { indices.push(i) } } // If we didn't get enough, grab any remaining transformable chars if (indices.length < transformCount) { for (let i = 0; i < chars.length && indices.length < transformCount; i++) { if (!indices.includes(i) && LEET_MAP[chars[i].toLowerCase()]) { indices.push(i) } } } // Apply transformations for (const i of indices) { const char = chars[i].toLowerCase() if (LEET_MAP[char]) { const options = LEET_MAP[char] chars[i] = options[Math.floor(Math.random() * options.length)] } } return chars.join('') } /** * Apply unicode homoglyph transformation */ function applyUnicode(word: string, intensity: 'light' | 'medium' | 'heavy'): string { const chars = word.split('') const transformCount = intensity === 'light' ? 1 : intensity === 'medium' ? Math.ceil(chars.length / 2) : chars.length const indices: number[] = [] for (let i = 0; i < chars.length && indices.length < transformCount; i++) { if (UNICODE_HOMOGLYPHS[chars[i].toLowerCase()]) { indices.push(i) } } for (const i of indices) { const char = chars[i].toLowerCase() if (UNICODE_HOMOGLYPHS[char]) { const options = UNICODE_HOMOGLYPHS[char] const replacement = options[Math.floor(Math.random() * options.length)] // Preserve case chars[i] = chars[i] === chars[i].toUpperCase() ? replacement.toUpperCase() : replacement } } return chars.join('') } /** * Insert zero-width characters between letters */ function applyZWJ(word: string, intensity: 'light' | 'medium' | 'heavy'): string { const chars = word.split('') const insertCount = intensity === 'light' ? 1 : intensity === 'medium' ? Math.ceil(chars.length / 2) : chars.length - 1 const result: string[] = [] let insertions = 0 for (let i = 0; i < chars.length; i++) { result.push(chars[i]) if (i < chars.length - 1 && insertions < insertCount) { const zwChar = ZW_CHARS[Math.floor(Math.random() * ZW_CHARS.length)] result.push(zwChar) insertions++ } } return result.join('') } /** * Apply mixed case disruption */ function applyMixedCase(word: string, intensity: 'light' | 'medium' | 'heavy'): string { const chars = word.split('') if (intensity === 'light') { // Just capitalize one random letter const idx = Math.floor(Math.random() * chars.length) chars[idx] = chars[idx].toUpperCase() } else if (intensity === 'medium') { // Alternating case for (let i = 0; i < chars.length; i++) { chars[i] = i % 2 === 0 ? chars[i].toLowerCase() : chars[i].toUpperCase() } } else { // Random case for (let i = 0; i < chars.length; i++) { chars[i] = Math.random() > 0.5 ? chars[i].toUpperCase() : chars[i].toLowerCase() } } return chars.join('') } /** * Apply phonetic substitution */ function applyPhonetic(word: string): string { // Common phonetic substitutions const substitutions: [RegExp, string][] = [ [/ph/gi, 'f'], [/ck/gi, 'k'], [/x/gi, 'ks'], [/qu/gi, 'kw'], [/c(?=[eiy])/gi, 's'], // soft c [/c/gi, 'k'], // hard c ] let result = word for (const [pattern, replacement] of substitutions) { result = result.replace(pattern, replacement) } return result } /** * Apply a random mix of techniques */ function applyRandom(word: string, intensity: 'light' | 'medium' | 'heavy'): string { const techniques = [applyLeetspeak, applyUnicode, applyZWJ, applyMixedCase] const technique = techniques[Math.floor(Math.random() * techniques.length)] return technique(word, intensity) } /** * Main obfuscation function for a single word */ function obfuscateWord( word: string, technique: ObfuscationTechnique, intensity: 'light' | 'medium' | 'heavy' ): string { switch (technique) { case 'leetspeak': return applyLeetspeak(word, intensity) case 'unicode': return applyUnicode(word, intensity) case 'zwj': return applyZWJ(word, intensity) case 'mixedcase': return applyMixedCase(word, intensity) case 'phonetic': return applyPhonetic(word) case 'random': return applyRandom(word, intensity) default: return word } } /** * Find all trigger words in the input text */ export function detectTriggers( text: string, customTriggers: string[] = [] ): string[] { const allTriggers = [...DEFAULT_TRIGGERS, ...customTriggers] const found: string[] = [] const lowerText = text.toLowerCase() for (const trigger of allTriggers) { // Check for whole word match (with word boundaries) const regex = new RegExp(`\\b${trigger.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'gi') if (regex.test(lowerText)) { found.push(trigger) } } return Array.from(new Set(found)) // Remove duplicates } /** * Main parseltongue transformation function * Detects trigger words and obfuscates them */ export function applyParseltongue( text: string, config: ParseltongueConfig ): ParseltongueResult { if (!config.enabled) { return { originalText: text, transformedText: text, triggersFound: [], techniqueUsed: config.technique, transformations: [] } } const triggersFound = detectTriggers(text, config.customTriggers) if (triggersFound.length === 0) { return { originalText: text, transformedText: text, triggersFound: [], techniqueUsed: config.technique, transformations: [] } } let transformedText = text const transformations: ParseltongueResult['transformations'] = [] // Sort triggers by length (longest first) to avoid partial replacements const sortedTriggers = [...triggersFound].sort((a, b) => b.length - a.length) for (const trigger of sortedTriggers) { const regex = new RegExp(`\\b(${trigger.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})\\b`, 'gi') transformedText = transformedText.replace(regex, (match) => { const transformed = obfuscateWord(match, config.technique, config.intensity) transformations.push({ original: match, transformed, technique: config.technique }) return transformed }) } return { originalText: text, transformedText, triggersFound, techniqueUsed: config.technique, transformations } } /** * Get the default parseltongue config */ export function getDefaultConfig(): ParseltongueConfig { return { enabled: false, technique: 'leetspeak', intensity: 'medium', customTriggers: [] } } /** * Get human-readable description of a technique */ export function getTechniqueDescription(technique: ObfuscationTechnique): string { const descriptions: Record = { leetspeak: 'Classic 1337speak: a→4, e→3, etc.', unicode: 'Unicode lookalikes (cyrillic, greek)', zwj: 'Invisible zero-width characters', mixedcase: 'Disrupted casing patterns', phonetic: 'Phonetic spelling substitutions', random: 'Random mix of all techniques' } return descriptions[technique] } /** * Get available techniques for UI */ export function getAvailableTechniques(): Array<{ id: ObfuscationTechnique name: string description: string }> { return [ { id: 'leetspeak', name: 'L33tspeak', description: getTechniqueDescription('leetspeak') }, { id: 'unicode', name: 'Unicode', description: getTechniqueDescription('unicode') }, { id: 'zwj', name: 'Zero-Width', description: getTechniqueDescription('zwj') }, { id: 'mixedcase', name: 'MiXeD CaSe', description: getTechniqueDescription('mixedcase') }, { id: 'phonetic', name: 'Phonetic', description: getTechniqueDescription('phonetic') }, { id: 'random', name: 'Random', description: getTechniqueDescription('random') } ] } /** * Export the default trigger list for reference */ export { DEFAULT_TRIGGERS }