v3 candidate
This commit is contained in:
@@ -0,0 +1,402 @@
|
||||
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:])
|
||||
}
|
||||
Reference in New Issue
Block a user