godmod3-api / src /lib /parseltongue.ts
pliny-the-prompter's picture
Upload 22 files
c78c312 verified
Raw
History Blame Contribute Delete
12.7 kB
/**
* 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<string, string[]> = {
'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<string, string[]> = {
'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<ObfuscationTechnique, string> = {
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 }