package transforms import ( "math" ) // Caesar shifts A-Z/a-z forward by n, leaving other bytes alone. func Caesar(in []byte, n int) []byte { n = ((n % 26) + 26) % 26 out := make([]byte, len(in)) for i, b := range in { switch { case b >= 'A' && b <= 'Z': out[i] = byte('A' + (int(b-'A')+n)%26) case b >= 'a' && b <= 'z': out[i] = byte('a' + (int(b-'a')+n)%26) default: out[i] = b } } return out } // Atbash maps A..Z -> Z..A (and lowercase equivalently). func Atbash(in []byte) []byte { out := make([]byte, len(in)) for i, b := range in { switch { case b >= 'A' && b <= 'Z': out[i] = 'A' + 'Z' - b case b >= 'a' && b <= 'z': out[i] = 'a' + 'z' - b default: out[i] = b } } return out } // Vigenere applies the Vigenère cipher with an ASCII key. encrypt=true // shifts forward, false shifts backward. Non-letters are preserved and // do not advance the key. func Vigenere(in, key []byte, encrypt bool) []byte { if len(key) == 0 { return append([]byte(nil), in...) } normKey := make([]int, 0, len(key)) for _, k := range key { switch { case k >= 'A' && k <= 'Z': normKey = append(normKey, int(k-'A')) case k >= 'a' && k <= 'z': normKey = append(normKey, int(k-'a')) } } if len(normKey) == 0 { return append([]byte(nil), in...) } out := make([]byte, len(in)) ki := 0 for i, b := range in { var base byte var letter bool if b >= 'A' && b <= 'Z' { base = 'A' letter = true } else if b >= 'a' && b <= 'z' { base = 'a' letter = true } if letter { shift := normKey[ki%len(normKey)] if !encrypt { shift = -shift } out[i] = byte(int(base) + ((int(b-base)+shift)%26+26)%26) ki++ } else { out[i] = b } } return out } // English letter frequency (A..Z) in percent. var englishFreq = [26]float64{ 8.167, 1.492, 2.782, 4.253, 12.702, 2.228, 2.015, 6.094, 6.966, 0.153, 0.772, 4.025, 2.406, 6.749, 7.507, 1.929, 0.095, 5.987, 6.327, 9.056, 2.758, 0.978, 2.360, 0.150, 1.974, 0.074, } // ScoreEnglish returns a heuristic fit to English letter frequencies. // Higher score = closer to English. ASCII-printable bonus added. func ScoreEnglish(in []byte) float64 { if len(in) == 0 { return 0 } var counts [26]int total := 0 printable := 0 for _, b := range in { if b >= 'A' && b <= 'Z' { counts[b-'A']++ total++ } else if b >= 'a' && b <= 'z' { counts[b-'a']++ total++ } if (b >= 0x20 && b <= 0x7e) || b == '\n' || b == '\r' || b == '\t' { printable++ } } printRatio := float64(printable) / float64(len(in)) if total == 0 { return printRatio * 10 // give credit for printability } // Chi-squared inverse-like score chi := 0.0 for i := 0; i < 26; i++ { obs := float64(counts[i]) / float64(total) * 100 exp := englishFreq[i] chi += math.Abs(obs - exp) // L1 distance; simpler than chi^2 for small samples } // Lower chi -> better. Convert to a bounded score. score := (200.0 - chi) + printRatio*50 if score < 0 { score = 0 } return score } // BruteCaesar returns all 26 shifts with scores, best-first. func BruteCaesar(in []byte) []map[string]any { out := make([]map[string]any, 26) for n := 0; n < 26; n++ { dec := Caesar(in, n) out[n] = map[string]any{ "shift": n, "score": ScoreEnglish(dec), "sample": previewText(dec, 120), "text": string(dec), } } // sort by score desc (simple insertion) for i := 1; i < len(out); i++ { for j := i; j > 0 && out[j-1]["score"].(float64) < out[j]["score"].(float64); j-- { out[j-1], out[j] = out[j], out[j-1] } } return out } // BruteXORByte returns scored 1-byte XOR candidates, best-first. func BruteXORByte(in []byte) []map[string]any { out := make([]map[string]any, 256) for k := 0; k < 256; k++ { buf := make([]byte, len(in)) for i, b := range in { buf[i] = b ^ byte(k) } out[k] = map[string]any{ "key": k, "keyHex": byteHex(byte(k)), "score": ScoreEnglish(buf), "sample": previewText(buf, 120), "text": string(buf), } } for i := 1; i < len(out); i++ { for j := i; j > 0 && out[j-1]["score"].(float64) < out[j]["score"].(float64); j-- { out[j-1], out[j] = out[j], out[j-1] } } return out } // RepeatingXORGuess attempts to recover short repeating XOR keys using // normalized Hamming distance for key-size ranking and per-column // single-byte XOR scoring. func RepeatingXORGuess(in []byte, minKey, maxKey, top int) []map[string]any { if minKey <= 0 { minKey = 2 } if maxKey < minKey { maxKey = minKey } if maxKey > 64 { maxKey = 64 } if top <= 0 { top = 5 } type keySizeScore struct { size int score float64 } sizes := []keySizeScore{} for keySize := minKey; keySize <= maxKey; keySize++ { if len(in) < keySize*4 { continue } dist := 0.0 pairs := 0 for block := 0; block < 4; block++ { a := block * keySize b := a + keySize if b+keySize > len(in) { break } dist += float64(hamming(in[a:a+keySize], in[b:b+keySize])) / float64(keySize) pairs++ } if pairs > 0 { sizes = append(sizes, keySizeScore{size: keySize, score: dist / float64(pairs)}) } } for i := 1; i < len(sizes); i++ { for j := i; j > 0 && sizes[j-1].score > sizes[j].score; j-- { sizes[j-1], sizes[j] = sizes[j], sizes[j-1] } } limit := top if limit > len(sizes) { limit = len(sizes) } out := make([]map[string]any, 0, limit) for _, ks := range sizes[:limit] { key := recoverRepeatingXORKey(in, ks.size) dec := XORRepeating(in, key) out = append(out, map[string]any{ "keySize": ks.size, "distance": ks.score, "keyHex": bytesHex(key), "keyText": previewText(key, len(key)), "score": ScoreEnglish(dec), "sample": previewText(dec, 120), "text": string(dec), "normalizedScore": ScoreEnglish(dec) - ks.score, }) } for i := 1; i < len(out); i++ { for j := i; j > 0 && out[j-1]["normalizedScore"].(float64) < out[j]["normalizedScore"].(float64); j-- { out[j-1], out[j] = out[j], out[j-1] } } return out } func recoverRepeatingXORKey(in []byte, keySize int) []byte { key := make([]byte, keySize) for pos := 0; pos < keySize; pos++ { col := make([]byte, 0, len(in)/keySize+1) for i := pos; i < len(in); i += keySize { col = append(col, in[i]) } bestScore := -1.0 bestKey := byte(0) for k := 0; k < 256; k++ { buf := make([]byte, len(col)) for i, b := range col { buf[i] = b ^ byte(k) } score := ScoreEnglish(buf) if score > bestScore { bestScore = score bestKey = byte(k) } } key[pos] = bestKey } return key } func XORRepeating(in, key []byte) []byte { if len(key) == 0 { return append([]byte(nil), in...) } out := make([]byte, len(in)) for i, b := range in { out[i] = b ^ key[i%len(key)] } return out } func hamming(a, b []byte) int { n := len(a) if len(b) < n { n = len(b) } out := 0 for i := 0; i < n; i++ { x := a[i] ^ b[i] for x != 0 { out++ x &= x - 1 } } return out } // TryEverything tries a handful of simple decodes and scores them. func TryEverything(in []byte) []map[string]any { candidates := []map[string]any{ {"name": "input", "text": string(in), "score": ScoreEnglish(in)}, {"name": "rot13", "text": string(Caesar(in, 13)), "score": ScoreEnglish(Caesar(in, 13))}, {"name": "atbash", "text": string(Atbash(in)), "score": ScoreEnglish(Atbash(in))}, } if b, err := Base64Decode(in); err == nil && len(b) > 0 { candidates = append(candidates, map[string]any{"name": "base64.decode", "text": string(b), "score": ScoreEnglish(b)}) } if b, err := HexDecode(in); err == nil && len(b) > 0 { candidates = append(candidates, map[string]any{"name": "hex.decode", "text": string(b), "score": ScoreEnglish(b)}) } if b, err := URLDecode(in); err == nil && len(b) > 0 { candidates = append(candidates, map[string]any{"name": "url.decode", "text": string(b), "score": ScoreEnglish(b)}) } // Top 3 XOR 1-byte candidates xr := BruteXORByte(in) for i := 0; i < 3 && i < len(xr); i++ { candidates = append(candidates, map[string]any{ "name": "xor1:" + xr[i]["keyHex"].(string), "text": xr[i]["text"].(string), "score": xr[i]["score"].(float64), }) } // Top repeating-key XOR candidates. rx := RepeatingXORGuess(in, 2, 40, 3) for i := 0; i < 3 && i < len(rx); i++ { candidates = append(candidates, map[string]any{ "name": "xor-repeating:" + rx[i]["keyHex"].(string), "keyHex": rx[i]["keyHex"].(string), "keyText": rx[i]["keyText"].(string), "text": rx[i]["text"].(string), "score": rx[i]["score"].(float64), }) } // Top 2 caesar cz := BruteCaesar(in) for i := 0; i < 2 && i < len(cz); i++ { candidates = append(candidates, map[string]any{ "name": "caesar:" + byteDec(byte(cz[i]["shift"].(int))), "text": cz[i]["text"].(string), "score": cz[i]["score"].(float64), }) } // Sort for i := 1; i < len(candidates); i++ { for j := i; j > 0 && candidates[j-1]["score"].(float64) < candidates[j]["score"].(float64); j-- { candidates[j-1], candidates[j] = candidates[j], candidates[j-1] } } // Samples for _, c := range candidates { t, _ := c["text"].(string) c["sample"] = previewText([]byte(t), 120) } return candidates } func previewText(buf []byte, n int) string { if n > len(buf) { n = len(buf) } out := make([]byte, n) for i := 0; i < n; i++ { if buf[i] >= 0x20 && buf[i] <= 0x7e { out[i] = buf[i] } else if buf[i] == '\n' { out[i] = ' ' } else { out[i] = '.' } } return string(out) } func byteHex(b byte) string { const h = "0123456789abcdef" return string([]byte{h[b>>4], h[b&0x0f]}) } func bytesHex(buf []byte) string { out := make([]byte, 0, len(buf)*2) for _, b := range buf { out = append(out, byteHex(b)...) } return string(out) } func byteDec(b byte) string { if b == 0 { return "0" } out := [3]byte{} i := 3 for b > 0 { i-- out[i] = '0' + b%10 b /= 10 } return string(out[i:]) }