ported Jason Haddix's additions, added transform settings

This commit is contained in:
Dustin Farley
2026-03-21 00:35:39 -07:00
parent 8ff45957c8
commit dcfb1f1dd3
50 changed files with 2769 additions and 484 deletions
+15 -1
View File
@@ -33,6 +33,17 @@
* canDecode: false,
* func: function(text) { ... }
* });
*
* 4. Transform with user options (gear icon in UI) and optional input kind:
*
* export default new BaseTransformer({
* name: 'Binary',
* inputKind: 'textarea', // 'textarea' | 'text' — main transform input when active
* configurableOptions: [
* { id: 'byteSpacing', label: 'Space between bytes', type: 'boolean', default: true }
* ],
* func: function(text, options = {}) { ... }
* });
*/
export class BaseTransformer {
@@ -43,12 +54,15 @@ export class BaseTransformer {
* @param {Function} config.func - Encoding function (required)
* @param {number} [config.priority=85] - Decoder priority (1-310)
* @param {Object} [config.map] - Character mapping (if provided, auto-generates reverse)
* @param {Function} [config.reverse] - Custom decoder function
* @param {Function} [config.reverse] - Custom decoder function (text, options) — options match encode prefs
* @param {Function} [config.preview] - Preview function (defaults to func)
* @param {Function} [config.detector] - Custom detection function (text) => boolean
* @param {boolean} [config.canDecode=true] - Whether this transformer can decode
* @param {string} [config.category] - Category for organization
* @param {string} [config.description] - Help text
* @param {'textarea'|'text'} [config.inputKind='textarea'] - Transform input control when this transform is active
* @param {Array<Object>} [config.configurableOptions] - Option definitions; shows gear when non-empty
* Each: { id, label, type: 'boolean'|'select'|'text'|'number', default, options?, min?, max?, step? }
*/
constructor(config) {
if (!config.name || !config.func) {
+23 -7
View File
@@ -5,7 +5,21 @@ export default new BaseTransformer({
name: 'ADFGX Cipher',
priority: 60,
category: 'cipher',
key: 'KEYWORD', // Default transposition key
key: 'KEYWORD',
configurableOptions: [
{
id: 'key',
label: 'Transposition keyword',
type: 'text',
default: 'KEYWORD'
}
],
_transKey: function(options) {
const k = options && options.key !== undefined && options.key !== null
? String(options.key)
: null;
return (k || this.key || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '');
},
// ADFGX uses a 5x5 Polybius square with letters A, D, F, G, X as coordinates
// Standard square (I and J share position)
square: [
@@ -17,8 +31,9 @@ export default new BaseTransformer({
],
// Coordinate labels
coords: ['A', 'D', 'F', 'G', 'X'],
func: function(text) {
const transKey = (this.key || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '');
func: function(text, options) {
options = options || {};
const transKey = this._transKey(options);
if (transKey.length === 0) return text;
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
@@ -79,8 +94,9 @@ export default new BaseTransformer({
return result;
},
reverse: function(text) {
const transKey = (this.key || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '');
reverse: function(text, options) {
options = options || {};
const transKey = this._transKey(options);
if (transKey.length === 0) return text;
// Only process ADFGX characters
@@ -153,9 +169,9 @@ export default new BaseTransformer({
return result;
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[adfgx]';
const result = this.func(text.slice(0, 5));
const result = this.func(text.slice(0, 5), options);
return result.substring(0, 12) + (result.length > 12 ? '...' : '');
},
detector: function(text) {
+75 -27
View File
@@ -1,32 +1,80 @@
// affine transform
// Wrapped in IIFE so build/build-transforms.js (which strips `export default`) assigns the
// transformer, not a stray top-level helper (see cipher/rot8000.js).
import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
name: 'Affine Cipher (a=5,b=8)',
priority: 60,
a: 5, b: 8, m: 26, invA: 21, // 5*21 ≡ 1 (mod 26)
func: function(text) {
const {a,b,m} = this;
return [...text].map(c => {
const code = c.charCodeAt(0);
if (code>=65 && code<=90) return String.fromCharCode(65 + ((a*(code-65)+b)%m));
if (code>=97 && code<=122) return String.fromCharCode(97 + ((a*(code-97)+b)%m));
return c;
}).join('');
},
preview: function(text) {
if (!text) return '[affine]';
return this.func(text.slice(0,8)) + (text.length>8?'...':'');
},
reverse: function(text) {
const {invA,b,m} = this;
return [...text].map(c => {
const code = c.charCodeAt(0);
if (code>=65 && code<=90) return String.fromCharCode(65 + ((invA*((code-65 - b + m)%m))%m));
if (code>=97 && code<=122) return String.fromCharCode(97 + ((invA*((code-97 - b + m)%m))%m));
return c;
}).join('');
export default (() => {
function modInv(a, m) {
a = ((a % m) + m) % m;
for (let x = 1; x < m; x++) {
if ((a * x) % m === 1) return x;
}
return null;
}
});
return new BaseTransformer({
name: 'Affine Cipher',
priority: 60,
a: 5,
b: 8,
m: 26,
invA: 21,
configurableOptions: [
{
id: 'a',
label: 'Multiplier a (coprime to 26)',
type: 'number',
default: 5,
min: 1,
max: 25,
step: 1
},
{
id: 'b',
label: 'Shift b',
type: 'number',
default: 8,
min: 0,
max: 25,
step: 1
}
],
_params: function(options) {
options = options || {};
const m = 26;
const a = options.a !== undefined && options.a !== '' ? Number(options.a) : this.a;
const b = options.b !== undefined && options.b !== '' ? Number(options.b) : this.b;
const inv = modInv(a, m);
return { a, b, m, invA: inv };
},
func: function(text, options) {
const { a, b, m } = this._params(options);
return [...text].map(c => {
const code = c.charCodeAt(0);
if (code >= 65 && code <= 90) return String.fromCharCode(65 + ((a * (code - 65) + b) % m));
if (code >= 97 && code <= 122) return String.fromCharCode(97 + ((a * (code - 97) + b) % m));
return c;
}).join('');
},
preview: function(text, options) {
if (!text) return '[affine]';
return this.func(text.slice(0, 8), options) + (text.length > 8 ? '...' : '');
},
reverse: function(text, options) {
const { b, m, invA } = this._params(options);
if (invA == null) return text;
return [...text].map(c => {
const code = c.charCodeAt(0);
if (code >= 65 && code <= 90) {
return String.fromCharCode(65 + ((invA * ((code - 65 - b + m) % m)) % m));
}
if (code >= 97 && code <= 122) {
return String.fromCharCode(97 + ((invA * ((code - 97 - b + m) % m)) % m));
}
return c;
}).join('');
}
});
})();
+23 -7
View File
@@ -5,9 +5,24 @@ export default new BaseTransformer({
name: 'Autokey Cipher',
priority: 60,
category: 'cipher',
key: 'KEY', // Initial key
func: function(text) {
const key = (this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
key: 'KEY',
configurableOptions: [
{
id: 'key',
label: 'Priming key',
type: 'text',
default: 'KEY'
}
],
_key: function(options) {
const k = options && options.key !== undefined && options.key !== null
? String(options.key)
: null;
return (k || this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
},
func: function(text, options) {
options = options || {};
const key = this._key(options);
if (key.length === 0) return text;
let result = '';
@@ -33,8 +48,9 @@ export default new BaseTransformer({
return result;
},
reverse: function(text) {
const key = (this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
reverse: function(text, options) {
options = options || {};
const key = this._key(options);
if (key.length === 0) return text;
let result = '';
@@ -71,9 +87,9 @@ export default new BaseTransformer({
return result;
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[autokey]';
return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : '');
return this.func(text.slice(0, 8), options) + (text.length > 8 ? '...' : '');
},
detector: function(text) {
// Autokey produces ciphertext that looks like scrambled letters
+17 -8
View File
@@ -5,9 +5,19 @@ export default new BaseTransformer({
name: 'Beaufort Cipher',
priority: 60,
category: 'cipher',
key: 'KEY', // Default key
func: function(text) {
const key = (this.key || 'KEY').toUpperCase();
key: 'KEY',
configurableOptions: [
{
id: 'key',
label: 'Keyword',
type: 'text',
default: 'KEY'
}
],
func: function(text, options) {
options = options || {};
const k = options.key !== undefined && options.key !== null ? String(options.key) : null;
const key = (k || this.key || 'KEY').toUpperCase();
const keyLength = key.length;
let keyIndex = 0;
@@ -32,13 +42,12 @@ export default new BaseTransformer({
}
}).join('');
},
reverse: function(text) {
// Beaufort cipher is self-reciprocal (same function for encode/decode)
return this.func(text);
reverse: function(text, options) {
return this.func(text, options);
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[beaufort]';
const result = this.func(text.slice(0, 8));
const result = this.func(text.slice(0, 8), options);
return result.substring(0, 10) + (result.length > 10 ? '...' : '');
},
detector: function(text) {
+25 -7
View File
@@ -5,7 +5,25 @@ export default new BaseTransformer({
name: 'Bifid Cipher',
priority: 60,
category: 'cipher',
period: 5, // Period for fractionation (default 5)
period: 5,
configurableOptions: [
{
id: 'period',
label: 'Period',
type: 'number',
default: 5,
min: 2,
max: 20,
step: 1
}
],
_period: function(options) {
options = options || {};
const p = options.period !== undefined && options.period !== ''
? Number(options.period)
: this.period;
return Math.max(2, Math.min(30, parseInt(p, 10) || 5));
},
// Standard Polybius square (5x5, I and J share same cell)
square: [
['A', 'B', 'C', 'D', 'E'],
@@ -14,8 +32,8 @@ export default new BaseTransformer({
['Q', 'R', 'S', 'T', 'U'],
['V', 'W', 'X', 'Y', 'Z']
],
func: function(text) {
const period = parseInt(this.period) || 5;
func: function(text, options) {
const period = this._period(options);
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
if (cleaned.length === 0) return text;
@@ -56,8 +74,8 @@ export default new BaseTransformer({
return result;
},
reverse: function(text) {
const period = parseInt(this.period) || 5;
reverse: function(text, options) {
const period = this._period(options);
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
if (cleaned.length === 0) return text;
@@ -101,9 +119,9 @@ export default new BaseTransformer({
return result;
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[bifid]';
const result = this.func(text.slice(0, 5));
const result = this.func(text.slice(0, 5), options);
return result.substring(0, 10) + (result.length > 10 ? '...' : '');
},
detector: function(text) {
+51 -37
View File
@@ -3,43 +3,57 @@ import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
name: 'Caesar Cipher',
name: 'Caesar Cipher',
priority: 60,
shift: 3, // Traditional Caesar shift is 3
func: function(text) {
return [...text].map(c => {
const code = c.charCodeAt(0);
// Only shift letters, leave other characters unchanged
if (code >= 65 && code <= 90) { // Uppercase letters
return String.fromCharCode(((code - 65 + this.shift) % 26) + 65);
} else if (code >= 97 && code <= 122) { // Lowercase letters
return String.fromCharCode(((code - 97 + this.shift) % 26) + 97);
} else {
return c;
}
}).join('');
},
preview: function(text) {
if (!text) return '[cursive]';
return this.func(text.slice(0, 3)) + '...';
},
reverse: function(text) {
// For decoding, shift in the opposite direction
const originalShift = this.shift;
this.shift = 26 - (this.shift % 26); // Reverse the shift
const result = this.func(text);
this.shift = originalShift; // Restore original shift
return result;
},
// Detector: Check if text is letters-only (potential Caesar cipher)
detector: function(text) {
// Caesar cipher only affects letters, so check if text contains mostly letters
// Remove punctuation, numbers, and common symbols for the ratio check
const cleaned = text.replace(/[\s.,!?;:'"()\-&0-9]/g, '');
// Must be mostly letters (at least 70%) and have some length
if (cleaned.length < 5) return false;
const letterCount = (cleaned.match(/[a-zA-Z]/g) || []).length;
return letterCount / cleaned.length > 0.7;
shift: 3,
configurableOptions: [
{
id: 'shift',
label: 'Shift (125)',
type: 'number',
default: 3,
min: 1,
max: 25,
step: 1
}
],
_encode: function(text, shift) {
const s = ((shift % 26) + 26) % 26;
return [...text].map(c => {
const code = c.charCodeAt(0);
if (code >= 65 && code <= 90) {
return String.fromCharCode(((code - 65 + s) % 26) + 65);
}
if (code >= 97 && code <= 122) {
return String.fromCharCode(((code - 97 + s) % 26) + 97);
}
return c;
}).join('');
},
func: function(text, options) {
options = options || {};
const shift = options.shift !== undefined && options.shift !== ''
? Number(options.shift)
: this.shift;
return this._encode(text, shift);
},
preview: function(text, options) {
if (!text) return '[caesar]';
return this.func(text.slice(0, 3), options) + '...';
},
reverse: function(text, options) {
options = options || {};
const shift = options.shift !== undefined && options.shift !== ''
? Number(options.shift)
: this.shift;
const s = ((shift % 26) + 26) % 26;
return this._encode(text, 26 - s);
},
detector: function(text) {
const cleaned = text.replace(/[\s.,!?;:'"()\-&0-9]/g, '');
if (cleaned.length < 5) return false;
const letterCount = (cleaned.match(/[a-zA-Z]/g) || []).length;
return letterCount / cleaned.length > 0.7;
}
});
});
@@ -5,9 +5,24 @@ export default new BaseTransformer({
name: 'Columnar Transposition',
priority: 60,
category: 'cipher',
key: 'KEY', // Default key
func: function(text) {
const key = (this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
key: 'KEY',
configurableOptions: [
{
id: 'key',
label: 'Keyword',
type: 'text',
default: 'KEY'
}
],
_key: function(options) {
const k = options && options.key !== undefined && options.key !== null
? String(options.key)
: null;
return (k || this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
},
func: function(text, options) {
options = options || {};
const key = this._key(options);
if (key.length === 0) return text;
// Remove spaces and convert to uppercase for processing
@@ -41,8 +56,9 @@ export default new BaseTransformer({
return result.join('');
},
reverse: function(text) {
const key = (this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
reverse: function(text, options) {
options = options || {};
const key = this._key(options);
if (key.length === 0) return text;
const keyLength = key.length;
@@ -79,9 +95,9 @@ export default new BaseTransformer({
return result.join('').replace(/X+$/, ''); // Remove padding X's
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[columnar]';
const result = this.func(text.slice(0, 10));
const result = this.func(text.slice(0, 10), options);
return result.substring(0, 12) + (result.length > 12 ? '...' : '');
},
detector: function(text) {
+28 -7
View File
@@ -5,8 +5,30 @@ export default new BaseTransformer({
name: 'Four-Square Cipher',
priority: 60,
category: 'cipher',
key1: 'EXAMPLE', // Top-left square key
key2: 'KEYWORD', // Bottom-right square key
key1: 'EXAMPLE',
key2: 'KEYWORD',
configurableOptions: [
{
id: 'key1',
label: 'Top-left square keyword',
type: 'text',
default: 'EXAMPLE'
},
{
id: 'key2',
label: 'Bottom-right square keyword',
type: 'text',
default: 'KEYWORD'
}
],
_keys: function(options) {
options = options || {};
const k1 = options.key1 !== undefined && options.key1 !== null ? String(options.key1) : null;
const k2 = options.key2 !== undefined && options.key2 !== null ? String(options.key2) : null;
const key1 = (k1 || this.key1 || 'EXAMPLE').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
const key2 = (k2 || this.key2 || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
return { key1, key2 };
},
// Standard alphabet for top-right and bottom-left squares
standardAlphabet: 'ABCDEFGHIKLMNOPQRSTUVWXYZ',
// Create keyed squares
@@ -95,9 +117,8 @@ export default new BaseTransformer({
return result;
},
reverse: function(text) {
const key1 = (this.key1 || 'EXAMPLE').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
const key2 = (this.key2 || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
reverse: function(text, options) {
const { key1, key2 } = this._keys(options);
if (key1.length === 0 || key2.length === 0) return text;
@@ -145,9 +166,9 @@ export default new BaseTransformer({
return result;
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[four-square]';
return this.func(text.slice(0, 4)) + (text.length > 4 ? '...' : '');
return this.func(text.slice(0, 4), options) + (text.length > 4 ? '...' : '');
},
detector: function(text) {
// Four-Square produces scrambled text (all uppercase letters, no digits)
+23 -7
View File
@@ -5,9 +5,24 @@ export default new BaseTransformer({
name: 'Gronsfeld Cipher',
priority: 60,
category: 'cipher',
key: '12345', // Default numeric key
func: function(text) {
const key = (this.key || '12345').replace(/[^0-9]/g, '');
key: '12345',
configurableOptions: [
{
id: 'key',
label: 'Numeric key (digits)',
type: 'text',
default: '12345'
}
],
_key: function(options) {
const k = options && options.key !== undefined && options.key !== null
? String(options.key)
: null;
return (k || this.key || '12345').replace(/[^0-9]/g, '');
},
func: function(text, options) {
options = options || {};
const key = this._key(options);
if (key.length === 0) return text;
let result = '';
@@ -31,8 +46,9 @@ export default new BaseTransformer({
return result;
},
reverse: function(text) {
const key = (this.key || '12345').replace(/[^0-9]/g, '');
reverse: function(text, options) {
options = options || {};
const key = this._key(options);
if (key.length === 0) return text;
let result = '';
@@ -56,9 +72,9 @@ export default new BaseTransformer({
return result;
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[gronsfeld]';
return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : '');
return this.func(text.slice(0, 8), options) + (text.length > 8 ? '...' : '');
},
detector: function(text) {
// Gronsfeld produces ciphertext that looks like scrambled letters
+30 -8
View File
@@ -5,10 +5,32 @@ export default new BaseTransformer({
name: 'Hill Cipher',
priority: 60,
category: 'cipher',
// Default 2x2 key matrix (must be invertible mod 26)
key: [[3, 3], [2, 5]], // Default key matrix
func: function(text) {
const key = this.key || [[3, 3], [2, 5]];
key: [[3, 3], [2, 5]],
configurableOptions: [
{
id: 'matrixJson',
label: '2×2 matrix (JSON), mod 26',
type: 'text',
default: '[[3,3],[2,5]]'
}
],
_keyMatrix: function(options) {
const fallback = this.key || [[3, 3], [2, 5]];
options = options || {};
if (options.matrixJson !== undefined && options.matrixJson !== null && String(options.matrixJson).trim() !== '') {
try {
const parsed = JSON.parse(String(options.matrixJson));
if (Array.isArray(parsed) && parsed.length === 2 && parsed[0].length === 2) {
return parsed;
}
} catch (e) {
/* use fallback */
}
}
return fallback;
},
func: function(text, options) {
const key = this._keyMatrix(options);
const matrixSize = key.length;
// Prepare text: remove non-letters, pad with X if needed
@@ -40,8 +62,8 @@ export default new BaseTransformer({
return result;
},
reverse: function(text) {
const key = this.key || [[3, 3], [2, 5]];
reverse: function(text, options) {
const key = this._keyMatrix(options);
const matrixSize = key.length;
// Calculate inverse matrix mod 26
@@ -121,9 +143,9 @@ export default new BaseTransformer({
return (oldS % m + m) % m;
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[hill]';
const result = this.func(text.slice(0, 4));
const result = this.func(text.slice(0, 4), options);
return result.substring(0, 8) + '...';
},
detector: function(text) {
+23 -7
View File
@@ -5,7 +5,21 @@ export default new BaseTransformer({
name: 'Nihilist Cipher',
priority: 60,
category: 'cipher',
key: '12345', // Default numeric key
key: '12345',
configurableOptions: [
{
id: 'key',
label: 'Numeric key',
type: 'text',
default: '12345'
}
],
_key: function(options) {
const k = options && options.key !== undefined && options.key !== null
? String(options.key)
: null;
return (k || this.key || '12345').replace(/[^0-9]/g, '');
},
// Standard Polybius square (5x5, I and J share same cell)
square: [
['A', 'B', 'C', 'D', 'E'],
@@ -14,8 +28,9 @@ export default new BaseTransformer({
['Q', 'R', 'S', 'T', 'U'],
['V', 'W', 'X', 'Y', 'Z']
],
func: function(text) {
const key = (this.key || '12345').replace(/[^0-9]/g, '');
func: function(text, options) {
options = options || {};
const key = this._key(options);
if (key.length === 0) return text;
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
@@ -47,8 +62,9 @@ export default new BaseTransformer({
return result.trim();
},
reverse: function(text) {
const key = (this.key || '12345').replace(/[^0-9]/g, '');
reverse: function(text, options) {
options = options || {};
const key = this._key(options);
if (key.length === 0) return text;
// Extract two-digit numbers
@@ -79,9 +95,9 @@ export default new BaseTransformer({
return result;
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[nihilist]';
const result = this.func(text.slice(0, 5));
const result = this.func(text.slice(0, 5), options);
return result.substring(0, 15) + '...';
},
detector: function(text) {
+23 -7
View File
@@ -5,9 +5,24 @@ export default new BaseTransformer({
name: 'Playfair Cipher',
priority: 60,
category: 'cipher',
key: 'KEYWORD', // Default key
func: function(text) {
const key = (this.key || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '');
key: 'KEYWORD',
configurableOptions: [
{
id: 'key',
label: 'Keyword (letters AZ)',
type: 'text',
default: 'KEYWORD'
}
],
_key: function(options) {
const k = options && options.key !== undefined && options.key !== null
? String(options.key)
: null;
return (k || this.key || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '');
},
func: function(text, options) {
options = options || {};
const key = this._key(options);
if (key.length === 0) return text;
// Create Playfair square
@@ -54,8 +69,9 @@ export default new BaseTransformer({
return result;
},
reverse: function(text) {
const key = (this.key || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '');
reverse: function(text, options) {
options = options || {};
const key = this._key(options);
if (key.length === 0) return text;
const alphabet = 'ABCDEFGHIKLMNOPQRSTUVWXYZ';
@@ -97,9 +113,9 @@ export default new BaseTransformer({
return result;
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[playfair]';
const result = this.func(text.slice(0, 8));
const result = this.func(text.slice(0, 8), options);
return result.substring(0, 10) + (result.length > 10 ? '...' : '');
},
detector: function(text) {
+23 -8
View File
@@ -5,9 +5,24 @@ export default new BaseTransformer({
name: 'Porta Cipher',
priority: 60,
category: 'cipher',
key: 'KEY', // Default key
func: function(text) {
const key = (this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
key: 'KEY',
configurableOptions: [
{
id: 'key',
label: 'Keyword',
type: 'text',
default: 'KEY'
}
],
_key: function(options) {
const k = options && options.key !== undefined && options.key !== null
? String(options.key)
: null;
return (k || this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
},
func: function(text, options) {
options = options || {};
const key = this._key(options);
if (key.length === 0) return text;
// Porta uses 13 reciprocal alphabets, each pair (A/B, C/D, etc.) shares a tableau
@@ -56,9 +71,9 @@ export default new BaseTransformer({
return result;
},
reverse: function(text) {
// Porta is self-reciprocal - use reverse lookup in tableau
const key = (this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
reverse: function(text, options) {
options = options || {};
const key = this._key(options);
if (key.length === 0) return text;
const tableaus = {
@@ -114,9 +129,9 @@ export default new BaseTransformer({
return result;
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[porta]';
const result = this.func(text.slice(0, 8));
const result = this.func(text.slice(0, 8), options);
return result.substring(0, 10) + (result.length > 10 ? '...' : '');
},
detector: function(text) {
+62 -41
View File
@@ -3,48 +3,69 @@ import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
name: 'Rail Fence (3 Rails)',
name: 'Rail Fence',
priority: 60,
rails: 3,
func: function(text) {
const rails = Array.from({length: this.rails}, () => []);
let rail = 0, dir = 1;
for (const ch of text) {
rails[rail].push(ch);
rail += dir;
if (rail === 0 || rail === this.rails-1) dir *= -1;
}
return rails.flat().join('');
},
preview: function(text) {
if (!text) return '[rail]';
return this.func(text.slice(0,12)) + (text.length>12?'...':'');
},
reverse: function(text) {
// Use Array.from to properly handle multi-byte UTF-8 characters
const chars = Array.from(text);
const len = chars.length;
const pattern = [];
let rail = 0, dir = 1;
for (let i=0;i<len;i++) {
pattern.push(rail);
rail += dir;
if (rail === 0 || rail === this.rails-1) dir *= -1;
}
const counts = Array(this.rails).fill(0);
for (const r of pattern) counts[r]++;
const railsArr = [];
let idx = 0;
for (let r=0;r<this.rails;r++) {
railsArr[r] = chars.slice(idx, idx+counts[r]);
idx += counts[r];
}
const positions = Array(this.rails).fill(0);
let out = '';
for (const r of pattern) {
out += railsArr[r][positions[r]++];
}
return out;
configurableOptions: [
{
id: 'rails',
label: 'Number of rails',
type: 'number',
default: 3,
min: 2,
max: 20,
step: 1
}
],
_rails: function(options) {
options = options || {};
const r = options.rails !== undefined && options.rails !== ''
? Number(options.rails)
: this.rails;
return Math.max(2, Math.min(50, Math.floor(r) || 2));
},
func: function(text, options) {
const n = this._rails(options);
const rails = Array.from({ length: n }, () => []);
let rail = 0;
let dir = 1;
for (const ch of text) {
rails[rail].push(ch);
rail += dir;
if (rail === 0 || rail === n - 1) dir *= -1;
}
return rails.flat().join('');
},
preview: function(text, options) {
if (!text) return '[rail]';
return this.func(text.slice(0, 12), options) + (text.length > 12 ? '...' : '');
},
reverse: function(text, options) {
const n = this._rails(options);
const chars = Array.from(text);
const len = chars.length;
const pattern = [];
let rail = 0;
let dir = 1;
for (let i = 0; i < len; i++) {
pattern.push(rail);
rail += dir;
if (rail === 0 || rail === n - 1) dir *= -1;
}
const counts = Array(n).fill(0);
for (const r of pattern) counts[r]++;
const railsArr = [];
let idx = 0;
for (let r = 0; r < n; r++) {
railsArr[r] = chars.slice(idx, idx + counts[r]);
idx += counts[r];
}
const positions = Array(n).fill(0);
let out = '';
for (const r of pattern) {
out += railsArr[r][positions[r]++];
}
return out;
}
});
});
+67 -81
View File
@@ -1,87 +1,73 @@
// ROT8000 cipher transform (Unicode rotation)
// Wrapped in IIFE so build/build-transforms.js (which strips `export default`) produces
// transforms['rot8000'] = (() => { ... return new BaseTransformer(...) })();
// Top-level helper functions alone would otherwise bind to the first `function`, not the transformer.
import BaseTransformer from '../BaseTransformer.js';
// Build valid character codes once (excludes control chars and surrogates)
function buildValidCodes() {
const validCodes = [];
for (let i = 0; i <= 0xFFFF; i++) {
// Skip control characters (0x0000-0x001F)
if (i >= 0x0000 && i <= 0x001F) continue;
// Skip DEL and some controls (0x007F-0x00A0)
if (i >= 0x007F && i <= 0x00A0) continue;
// Skip surrogate pairs (0xD800-0xDFFF)
if (i >= 0xD800 && i <= 0xDFFF) continue;
validCodes.push(i);
}
return validCodes;
}
// Cache the valid codes and mappings
let cachedValidCodes = null;
let cachedCodeToIndex = null;
let cachedShift = null;
function getRot8000Data() {
if (!cachedValidCodes) {
cachedValidCodes = buildValidCodes();
// ROT8000 uses a shift that makes it self-reciprocal (applying twice returns original)
// The shift should be approximately 0x8000 (32768), which is half the BMP
// For self-reciprocity: (index + shift + shift) % validCount == index
// This means: (2 * shift) % validCount == 0, so shift = validCount / 2
cachedShift = Math.floor(cachedValidCodes.length / 2);
cachedCodeToIndex = new Map();
cachedValidCodes.forEach((code, index) => {
cachedCodeToIndex.set(code, index);
});
}
return { validCodes: cachedValidCodes, codeToIndex: cachedCodeToIndex, shift: cachedShift };
}
export default new BaseTransformer({
name: 'ROT8000',
priority: 50,
category: 'cipher',
func: function(text) {
// ROT8000 rotates Unicode BMP characters (0x0000-0xFFFF)
// Excludes control characters: U+0000-U+001F, U+007F-U+00A0, U+D800-U+DFFF
// Shift is half the valid range for self-reciprocity
const { validCodes, codeToIndex, shift } = getRot8000Data();
const validCount = validCodes.length;
let result = '';
for (let i = 0; i < text.length; i++) {
const code = text.charCodeAt(i);
// Check if character is in valid range
if (codeToIndex.has(code)) {
const index = codeToIndex.get(code);
const rotatedIndex = (index + shift) % validCount;
const rotatedCode = validCodes[rotatedIndex];
result += String.fromCharCode(rotatedCode);
} else {
// Keep invalid characters as-is (spaces, emojis, etc.)
result += text[i];
}
export default (() => {
function buildValidCodes() {
const validCodes = [];
for (let i = 0; i <= 0xFFFF; i++) {
if (i >= 0x0000 && i <= 0x001F) continue;
if (i >= 0x007F && i <= 0x00A0) continue;
if (i >= 0xD800 && i <= 0xDFFF) continue;
validCodes.push(i);
}
return result;
},
reverse: function(text) {
// ROT8000 is self-reciprocal (rotating by 0x8000 twice = full rotation)
return this.func(text);
},
preview: function(text) {
if (!text) return '[rot8000]';
return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : '');
},
detector: function(text) {
// ROT8000 produces characters in various Unicode ranges
// Check for non-ASCII characters that aren't typical text
const hasNonAscii = /[^\x00-\x7F]/.test(text);
const hasControlChars = /[\x00-\x1F]/.test(text);
return hasNonAscii && text.length >= 5;
return validCodes;
}
});
let cachedValidCodes = null;
let cachedCodeToIndex = null;
let cachedShift = null;
function getRot8000Data() {
if (!cachedValidCodes) {
cachedValidCodes = buildValidCodes();
cachedShift = Math.floor(cachedValidCodes.length / 2);
cachedCodeToIndex = new Map();
cachedValidCodes.forEach((code, index) => {
cachedCodeToIndex.set(code, index);
});
}
return { validCodes: cachedValidCodes, codeToIndex: cachedCodeToIndex, shift: cachedShift };
}
return new BaseTransformer({
name: 'ROT8000',
priority: 50,
category: 'cipher',
func: function(text) {
const { validCodes, codeToIndex, shift } = getRot8000Data();
const validCount = validCodes.length;
let result = '';
for (let i = 0; i < text.length; i++) {
const code = text.charCodeAt(i);
if (codeToIndex.has(code)) {
const index = codeToIndex.get(code);
const rotatedIndex = (index + shift) % validCount;
const rotatedCode = validCodes[rotatedIndex];
result += String.fromCharCode(rotatedCode);
} else {
result += text[i];
}
}
return result;
},
reverse: function(text) {
return this.func(text);
},
preview: function(text) {
if (!text) return '[rot8000]';
return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : '');
},
detector: function(text) {
const hasNonAscii = /[^\x00-\x7F]/.test(text);
const hasControlChars = /[\x00-\x1F]/.test(text);
return hasNonAscii && text.length >= 5;
}
});
})();
+25 -7
View File
@@ -5,9 +5,27 @@ export default new BaseTransformer({
name: 'Scytale Cipher',
priority: 60,
category: 'cipher',
key: 5, // Default number of columns (wrapping width)
func: function(text) {
const key = parseInt(this.key) || 5;
key: 5,
configurableOptions: [
{
id: 'columns',
label: 'Columns (rod width)',
type: 'number',
default: 5,
min: 2,
max: 40,
step: 1
}
],
_cols: function(options) {
options = options || {};
const c = options.columns !== undefined && options.columns !== ''
? Number(options.columns)
: this.key;
return parseInt(c, 10) || 5;
},
func: function(text, options) {
const key = this._cols(options);
if (key < 2) return text;
// Remove spaces for encoding
@@ -39,8 +57,8 @@ export default new BaseTransformer({
return result;
},
reverse: function(text) {
const key = parseInt(this.key) || 5;
reverse: function(text, options) {
const key = this._cols(options);
if (key < 2) return text;
const cleaned = text.replace(/\s/g, '').toUpperCase();
@@ -74,9 +92,9 @@ export default new BaseTransformer({
return result;
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[scytale]';
const result = this.func(text.slice(0, 10));
const result = this.func(text.slice(0, 10), options);
return result.substring(0, 12) + (result.length > 12 ? '...' : '');
},
detector: function(text) {
+25 -7
View File
@@ -5,7 +5,25 @@ export default new BaseTransformer({
name: 'Trifid Cipher',
priority: 60,
category: 'cipher',
period: 5, // Period for fractionation (default 5)
period: 5,
configurableOptions: [
{
id: 'period',
label: 'Period',
type: 'number',
default: 5,
min: 2,
max: 20,
step: 1
}
],
_period: function(options) {
options = options || {};
const p = options.period !== undefined && options.period !== ''
? Number(options.period)
: this.period;
return Math.max(2, Math.min(30, parseInt(p, 10) || 5));
},
// Trifid uses a 3x3x3 cube (27 positions for A-Z and space/punctuation)
// Structure: 3 layers, each with 3 rows and 3 columns
cube: [
@@ -16,8 +34,8 @@ export default new BaseTransformer({
// Layer 2
[['S', 'T', 'U'], ['V', 'W', 'X'], ['Y', 'Z', ' ']]
],
func: function(text) {
const period = parseInt(this.period) || 5;
func: function(text, options) {
const period = this._period(options);
const cleaned = text.toUpperCase().replace(/[^A-Z ]/g, '');
if (cleaned.length === 0) return text;
@@ -70,8 +88,8 @@ export default new BaseTransformer({
return result;
},
reverse: function(text) {
const period = parseInt(this.period) || 5;
reverse: function(text, options) {
const period = this._period(options);
const cleaned = text.toUpperCase().replace(/[^A-Z ]/g, '');
if (cleaned.length === 0) return text;
@@ -128,9 +146,9 @@ export default new BaseTransformer({
return result;
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[trifid]';
const result = this.func(text.slice(0, 5));
const result = this.func(text.slice(0, 5), options);
return result.substring(0, 10) + (result.length > 10 ? '...' : '');
},
detector: function(text) {
+30 -10
View File
@@ -5,8 +5,30 @@ export default new BaseTransformer({
name: 'Two-Square Cipher',
priority: 60,
category: 'cipher',
key1: 'EXAMPLE', // Top square key
key2: 'KEYWORD', // Bottom square key
key1: 'EXAMPLE',
key2: 'KEYWORD',
configurableOptions: [
{
id: 'key1',
label: 'Top square keyword',
type: 'text',
default: 'EXAMPLE'
},
{
id: 'key2',
label: 'Bottom square keyword',
type: 'text',
default: 'KEYWORD'
}
],
_keys: function(options) {
options = options || {};
const k1 = options.key1 !== undefined && options.key1 !== null ? String(options.key1) : null;
const k2 = options.key2 !== undefined && options.key2 !== null ? String(options.key2) : null;
const key1 = (k1 || this.key1 || 'EXAMPLE').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
const key2 = (k2 || this.key2 || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
return { key1, key2 };
},
// Standard alphabet for reference
standardAlphabet: 'ABCDEFGHIKLMNOPQRSTUVWXYZ',
// Create keyed square
@@ -42,9 +64,8 @@ export default new BaseTransformer({
}
return square;
},
func: function(text) {
const key1 = (this.key1 || 'EXAMPLE').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
const key2 = (this.key2 || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
func: function(text, options) {
const { key1, key2 } = this._keys(options);
if (key1.length === 0 || key2.length === 0) return text;
@@ -93,9 +114,8 @@ export default new BaseTransformer({
return result;
},
reverse: function(text) {
const key1 = (this.key1 || 'EXAMPLE').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
const key2 = (this.key2 || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
reverse: function(text, options) {
const { key1, key2 } = this._keys(options);
if (key1.length === 0 || key2.length === 0) return text;
@@ -143,9 +163,9 @@ export default new BaseTransformer({
return result;
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[two-square]';
return this.func(text.slice(0, 4)) + (text.length > 4 ? '...' : '');
return this.func(text.slice(0, 4), options) + (text.length > 4 ? '...' : '');
},
detector: function(text) {
// Two-Square produces scrambled text (all uppercase letters, no digits)
+63 -33
View File
@@ -3,40 +3,70 @@ import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
name: 'Vigenère Cipher',
name: 'Vigenère Cipher',
priority: 60,
key: 'KEY',
func: function(text) {
const key = this.key;
let out = '';
let j = 0;
for (let i=0;i<text.length;i++) {
const c = text[i];
const code = c.charCodeAt(0);
const k = key[j % key.length].toUpperCase().charCodeAt(0) - 65;
if (code >= 65 && code <= 90) { out += String.fromCharCode(65 + ((code-65 + k)%26)); j++; }
else if (code >= 97 && code <= 122) { out += String.fromCharCode(97 + ((code-97 + k)%26)); j++; }
else out += c;
}
return out;
},
preview: function(text) {
if (!text) return '[Vigenère]';
return this.func(text.slice(0,8)) + (text.length>8?'...':'');
},
reverse: function(text) {
const key = this.key;
let out = '';
let j = 0;
for (let i=0;i<text.length;i++) {
const c = text[i];
const code = c.charCodeAt(0);
const k = key[j % key.length].toUpperCase().charCodeAt(0) - 65;
if (code >= 65 && code <= 90) { out += String.fromCharCode(65 + ((code-65 + 26 - (k%26))%26)); j++; }
else if (code >= 97 && code <= 122) { out += String.fromCharCode(97 + ((code-97 + 26 - (k%26))%26)); j++; }
else out += c;
}
return out;
configurableOptions: [
{
id: 'key',
label: 'Keyword',
type: 'text',
default: 'KEY'
}
],
_keyStr: function(options) {
const optionsKey = options && options.key !== undefined && options.key !== null
? String(options.key)
: null;
return (optionsKey || this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
},
func: function(text, options) {
options = options || {};
const key = this._keyStr(options);
if (key.length === 0) return text;
let out = '';
let j = 0;
for (let i = 0; i < text.length; i++) {
const c = text[i];
const code = c.charCodeAt(0);
const k = key[j % key.length].toUpperCase().charCodeAt(0) - 65;
if (code >= 65 && code <= 90) {
out += String.fromCharCode(65 + ((code - 65 + k) % 26));
j++;
} else if (code >= 97 && code <= 122) {
out += String.fromCharCode(97 + ((code - 97 + k) % 26));
j++;
} else {
out += c;
}
}
return out;
},
preview: function(text, options) {
if (!text) return '[Vigenère]';
return this.func(text.slice(0, 8), options) + (text.length > 8 ? '...' : '');
},
reverse: function(text, options) {
options = options || {};
const key = this._keyStr(options);
if (key.length === 0) return text;
let out = '';
let j = 0;
for (let i = 0; i < text.length; i++) {
const c = text[i];
const code = c.charCodeAt(0);
const k = key[j % key.length].toUpperCase().charCodeAt(0) - 65;
if (code >= 65 && code <= 90) {
out += String.fromCharCode(65 + ((code - 65 + 26 - (k % 26)) % 26));
j++;
} else if (code >= 97 && code <= 122) {
out += String.fromCharCode(97 + ((code - 97 + 26 - (k % 26)) % 26));
j++;
} else {
out += c;
}
}
return out;
}
});
});
+21 -8
View File
@@ -5,9 +5,23 @@ export default new BaseTransformer({
name: 'XOR Cipher',
priority: 70,
category: 'cipher',
key: 'KEY', // Default key
func: function(text) {
const key = this.key || 'KEY';
key: 'KEY',
configurableOptions: [
{
id: 'key',
label: 'XOR key (string)',
type: 'text',
default: 'KEY'
}
],
_key: function(options) {
const k = options && options.key !== undefined && options.key !== null
? String(options.key)
: null;
return k || this.key || 'KEY';
},
func: function(text, options) {
const key = this._key(options || {});
const keyBytes = new TextEncoder().encode(key);
const textBytes = new TextEncoder().encode(text);
const result = new Uint8Array(textBytes.length);
@@ -21,12 +35,11 @@ export default new BaseTransformer({
.map(b => b.toString(16).padStart(2, '0'))
.join('');
},
reverse: function(text) {
// XOR is self-reciprocal, but we need to convert from hex first
reverse: function(text, options) {
try {
const hexBytes = text.match(/.{1,2}/g) || [];
const bytes = new Uint8Array(hexBytes.map(h => parseInt(h, 16)));
const key = this.key || 'KEY';
const key = this._key(options || {});
const keyBytes = new TextEncoder().encode(key);
const result = new Uint8Array(bytes.length);
@@ -39,9 +52,9 @@ export default new BaseTransformer({
return text;
}
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[xor]';
const result = this.func(text.slice(0, 4));
const result = this.func(text.slice(0, 4), options);
return result.substring(0, 12) + '...';
},
detector: function(text) {
+18 -6
View File
@@ -4,6 +4,15 @@ import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
name: 'Binary',
priority: 300,
inputKind: 'textarea',
configurableOptions: [
{
id: 'byteSpacing',
label: 'Space between bytes',
type: 'boolean',
default: true
}
],
// Detector: Only 0s, 1s, and spaces
detector: function(text) {
const cleaned = text.trim();
@@ -11,18 +20,21 @@ export default new BaseTransformer({
return noSpaces.length >= 8 && /^[01\s]+$/.test(cleaned);
},
func: function(text) {
// Use TextEncoder to properly handle UTF-8 (including emoji)
func: function(text, options) {
options = options || {};
const spacing = options.byteSpacing !== false;
const encoder = new TextEncoder();
const bytes = encoder.encode(text);
return Array.from(bytes).map(b => b.toString(2).padStart(8, '0')).join(' ');
const bits = Array.from(bytes).map(b => b.toString(2).padStart(8, '0'));
return spacing ? bits.join(' ') : bits.join('');
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[binary]';
const full = this.func(text);
const full = this.func(text, options);
return full.substring(0, 24) + (full.length > 24 ? '...' : '');
},
reverse: function(text) {
reverse: function(text, options) {
options = options || {};
// Remove spaces and ensure we have valid binary
const binText = text.replace(/\s+/g, '');
const bytes = [];
+18 -6
View File
@@ -4,24 +4,36 @@ import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
name: 'Hexadecimal',
priority: 290,
inputKind: 'textarea',
configurableOptions: [
{
id: 'pairSpacing',
label: 'Space between byte pairs',
type: 'boolean',
default: true
}
],
// Detector: Only hex characters (0-9, A-F)
detector: function(text) {
const cleaned = text.trim().replace(/\s/g, '');
return cleaned.length >= 4 && /^[0-9A-Fa-f]+$/.test(cleaned);
},
func: function(text) {
// Use TextEncoder to properly handle UTF-8 (including emoji)
func: function(text, options) {
options = options || {};
const spacing = options.pairSpacing !== false;
const encoder = new TextEncoder();
const bytes = encoder.encode(text);
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(' ');
const pairs = Array.from(bytes).map(b => b.toString(16).padStart(2, '0'));
return spacing ? pairs.join(' ') : pairs.join('');
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[hex]';
const full = this.func(text);
const full = this.func(text, options);
return full.substring(0, 20) + (full.length > 20 ? '...' : '');
},
reverse: function(text) {
reverse: function(text, options) {
options = options || {};
const hexText = text.replace(/\s+/g, '');
const bytes = [];