Skip to content

Instantly share code, notes, and snippets.

@zboralski
Last active December 11, 2025 16:19
Show Gist options
  • Select an option

  • Save zboralski/dac7b9ab2f97ac4fa2b4a1a65faf82e5 to your computer and use it in GitHub Desktop.

Select an option

Save zboralski/dac7b9ab2f97ac4fa2b4a1a65faf82e5 to your computer and use it in GitHub Desktop.
unxor.go
// unxor - XOR key recovery and decryption for encrypted assets
//
// Uses Kasiski examination to find key length, then known-plaintext attack
// with common file headers to recover the full key.
//
// Usage:
//
// unxor analyze <file> # Find key length and recover key
// unxor decrypt <file> -k <key> # Decrypt with known key
// unxor decrypt <file> -a # Auto-detect key and decrypt
//
// Examples:
// unxor analyze main.98555.js
// unxor decrypt main.98555.js -k c0f0205080b0e0104070a0d000306090
// unxor decrypt main.98555.js -a -o decrypted.js
// unxor string "SGVsbG8gV29ybGQ=" -k 00
// unxor string "4a6f686e" --hex -b
// unxor scan app.smali -k 69
// unxor scan decoded_apk/smali -r -k 69`)
package main
import (
"bytes"
"encoding/base64"
"encoding/hex"
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
// Common Cocos Creator file headers for known-plaintext attack
var knownHeaders = [][]byte{
[]byte("window.boot = function () {\n var settings"), // main.js bootstrap (long)
[]byte("window.boot = function () {"), // main.js bootstrap
[]byte("window.__require"), // bundled modules
[]byte("cc._RF.push(module"), // resource files
[]byte("System.register(["), // SystemJS modules
[]byte("(function(r,e,"), // browserify IIFE
[]byte("(function(){\"use strict\""), // strict IIFE
[]byte("\"use strict\";"), // strict mode
[]byte("var cc = cc || {};"), // Cocos variable
[]byte("<!DOCTYPE html>"), // HTML files
[]byte("<html lang="), // HTML files
[]byte("{\"launchScene\":"), // settings.json
[]byte("window._CCSettings = "), // settings.js
[]byte("cc.game.on(cc.game."), // game events
}
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run(args []string) error {
if len(args) == 0 {
printUsage(os.Stdout)
return errors.New("no command specified")
}
switch args[0] {
case "analyze":
return analyzeCmd(args[1:])
case "decrypt":
return decryptCmd(args[1:])
case "string":
return stringCmd(args[1:])
case "scan":
return scanCmd(args[1:])
case "help", "-h", "--help":
printUsage(os.Stdout)
return nil
default:
printUsage(os.Stderr)
return fmt.Errorf("unknown command %q", args[0])
}
}
func printUsage(w io.Writer) {
fmt.Fprintln(w, `unxor - XOR key recovery and decryption
Usage:
unxor analyze <file> Analyze file to find XOR key
unxor decrypt <file> [options] Decrypt file
unxor string <string> [options] Decrypt base64/hex encoded XOR string
unxor scan <file> [options] Scan file for encrypted strings
Decrypt Options:
-k, --key <hex> XOR key in hex (e.g., c0f0205080b0e010)
-a, --auto Auto-detect key using known headers
-o, --output <f> Output file (default: <input>.dec)
-w, --overwrite Overwrite input file with decrypted content
String Options:
-k, --key <hex> XOR key in hex (required, or use -b for brute force)
-b, --brute Brute force single-byte XOR keys (0x00-0xff)
--base64 Input is base64 encoded (default: auto-detect base64/hex)
--hex Input is hex encoded
Note: Input encoding is auto-detected (tries base64 first, then hex)
Scan Options:
-k, --key <hex> XOR key to try
-b, --brute Try all single-byte XOR keys
-r, --recursive Recursively scan directories for .smali files
--min <n> Minimum string length (default: 4)
Examples:
unxor analyze main.98555.js
unxor decrypt main.98555.js -k c0f0205080b0e0104070a0d000306090
unxor decrypt main.98555.js -a -o decrypted.js
unxor string "SGVsbG8gV29ybGQ=" -k 00
unxor string "4a6f686e" --hex -b
unxor scan app.smali -k 69
unxor scan decoded_apk/smali -r -k 69`)
}
func analyzeCmd(args []string) error {
fs := newFlagSet("analyze", "Usage: unxor analyze <file>")
if err := fs.Parse(args); err != nil {
return err
}
if fs.NArg() < 1 {
fs.Usage()
return errors.New("analyze requires a file path")
}
file := fs.Arg(0)
data, err := os.ReadFile(file)
if err != nil {
return fmt.Errorf("read %s: %w", file, err)
}
fmt.Printf("Analyzing %s (%d bytes)\n\n", file, len(data))
// Step 1: Find repeated sequences (Kasiski examination)
fmt.Println("Kasiski examination...")
keyLen := findKeyLength(data)
if keyLen == 0 {
fmt.Println(" No repeated patterns found, trying frequency analysis...")
keyLen = frequencyKeyLength(data, 32)
}
fmt.Printf(" Key length: %d bytes\n\n", keyLen)
// Step 2: Try known-plaintext attack
fmt.Println("Known-plaintext attack...")
key := recoverKey(data, keyLen)
if key != nil {
fmt.Printf(" Key: %s\n", hex.EncodeToString(key))
analyzeKeyPattern(key)
// Show sample decryption
fmt.Println("\nSample decryption (first 200 bytes):")
fmt.Println("---")
decrypted := xorDecrypt(data[:min(200, len(data))], key)
fmt.Println(sanitizeOutput(decrypted))
fmt.Println("---")
} else {
fmt.Println(" Could not recover key with known headers")
fmt.Println(" Try manual analysis or provide a known plaintext")
}
return nil
}
func decryptCmd(args []string) error {
fs := newFlagSet("decrypt", "Usage: unxor decrypt <file> [-k key | -a] [-o output]")
keyHex := fs.String("k", "", "XOR key in hex")
fs.StringVar(keyHex, "key", "", "XOR key in hex")
auto := fs.Bool("a", false, "Auto-detect key")
fs.BoolVar(auto, "auto", false, "Auto-detect key")
output := fs.String("o", "", "Output file")
fs.StringVar(output, "output", "", "Output file")
overwrite := fs.Bool("w", false, "Overwrite input file")
fs.BoolVar(overwrite, "overwrite", false, "Overwrite input file")
if err := fs.Parse(args); err != nil {
return err
}
if fs.NArg() < 1 {
fs.Usage()
return errors.New("decrypt requires at least one file")
}
expandedFiles, err := expandGlobs(fs.Args())
if err != nil {
return err
}
if len(expandedFiles) == 0 {
return errors.New("no files matched")
}
key, err := decodeKey(*keyHex)
if err != nil {
return err
}
var encounteredError bool
for _, file := range expandedFiles {
if err := decryptFile(file, key, *auto, *overwrite, *output); err != nil {
encounteredError = true
fmt.Fprintln(os.Stderr, err)
}
}
if encounteredError {
return errors.New("one or more files failed to decrypt")
}
return nil
}
func decryptFile(file string, key []byte, auto bool, overwrite bool, output string) error {
data, err := os.ReadFile(file)
if err != nil {
return fmt.Errorf("read %s: %w", file, err)
}
fileKey := key
if auto || fileKey == nil {
fileKey = autoDetectKey(data)
if fileKey == nil {
return fmt.Errorf("[%s] could not auto-detect key", file)
}
fmt.Printf("[%s] Auto-detected key: %s\n", file, hex.EncodeToString(fileKey))
}
decrypted := xorDecrypt(data, fileKey)
outPath := output
if outPath == "" {
if overwrite {
outPath = file
} else {
outPath = file + ".dec"
}
}
if err := os.WriteFile(outPath, decrypted, 0o644); err != nil {
return fmt.Errorf("write %s: %w", outPath, err)
}
fmt.Printf("[%s] -> %s (%d bytes)\n", file, outPath, len(decrypted))
return nil
}
func autoDetectKey(data []byte) []byte {
keyLen := findKeyLength(data)
if keyLen == 0 {
keyLen = frequencyKeyLength(data, 32)
}
return recoverKey(data, keyLen)
}
// stringCmd handles decryption of base64/hex encoded XOR strings
func stringCmd(args []string) error {
fs := newFlagSet("string", "Usage: unxor string <encoded-string> [-k key | -b]")
keyHex := fs.String("k", "", "XOR key in hex")
fs.StringVar(keyHex, "key", "", "XOR key in hex")
brute := fs.Bool("b", false, "Brute force single-byte keys")
fs.BoolVar(brute, "brute", false, "Brute force single-byte keys")
isBase64 := fs.Bool("base64", false, "Input is base64 encoded")
isHex := fs.Bool("hex", false, "Input is hex encoded")
if err := fs.Parse(args); err != nil {
return err
}
if fs.NArg() < 1 {
fs.Usage()
return errors.New("string requires an encoded string argument")
}
input := fs.Arg(0)
// Decode the input
var data []byte
var err error
var encoding string
if *isHex {
data, err = hex.DecodeString(input)
encoding = "hex"
} else if *isBase64 {
data, err = base64.StdEncoding.DecodeString(input)
encoding = "base64"
} else {
// Auto-detect: try base64 first, then hex
data, err = base64.StdEncoding.DecodeString(input)
if err == nil {
encoding = "base64"
} else {
data, err = hex.DecodeString(input)
if err == nil {
encoding = "hex"
} else {
// Assume raw bytes
data = []byte(input)
encoding = "raw"
}
}
}
if err != nil {
return fmt.Errorf("decode input: %w", err)
}
fmt.Printf("Input: %s (%d bytes, %s)\n", input, len(data), encoding)
if *brute {
// Brute force single-byte XOR
fmt.Println("\nBrute forcing single-byte XOR keys...")
fmt.Println("---")
found := 0
for key := 0; key <= 0xff; key++ {
decrypted := xorDecrypt(data, []byte{byte(key)})
if looksLikeText(decrypted) {
fmt.Printf("0x%02x: %s\n", key, sanitizeOutput(decrypted))
found++
}
}
if found == 0 {
fmt.Println("No readable results found")
}
fmt.Println("---")
return nil
}
if *keyHex == "" {
return errors.New("specify -k <key> or -b for brute force")
}
key, err := hex.DecodeString(*keyHex)
if err != nil {
return fmt.Errorf("invalid hex key: %w", err)
}
decrypted := xorDecrypt(data, key)
fmt.Printf("Key: %s\n", hex.EncodeToString(key))
fmt.Printf("Decrypted: %s\n", sanitizeOutput(decrypted))
return nil
}
// Regex patterns for finding encoded strings
var (
// Base64: at least 8 chars, valid base64 alphabet, optional padding
base64Pattern = regexp.MustCompile(`[A-Za-z0-9+/]{8,}={0,2}`)
// Hex: even number of hex chars, at least 8
hexPattern = regexp.MustCompile(`(?:0x)?([0-9a-fA-F]{2}){4,}`)
// Smali array: 0xNNt bytes
smaliArrayPattern = regexp.MustCompile(`0x([0-9a-fA-F]{1,2})t`)
)
// scanCmd scans files for base64/hex encoded strings and tries to decrypt them
func scanCmd(args []string) error {
fs := newFlagSet("scan", "Usage: unxor scan <path> [-k key] [-b] [-r]")
keyHex := fs.String("k", "", "XOR key in hex")
fs.StringVar(keyHex, "key", "", "XOR key in hex")
brute := fs.Bool("b", false, "Brute force single-byte keys")
fs.BoolVar(brute, "brute", false, "Brute force single-byte keys")
minLen := fs.Int("min", 4, "Minimum decoded string length")
recursive := fs.Bool("r", false, "Recursively scan directories")
fs.BoolVar(recursive, "recursive", false, "Recursively scan directories")
if err := fs.Parse(args); err != nil {
return err
}
if fs.NArg() < 1 {
fs.Usage()
return errors.New("scan requires a file or directory path")
}
var key []byte
var err error
if *keyHex != "" {
key, err = hex.DecodeString(*keyHex)
if err != nil {
return fmt.Errorf("invalid hex key: %w", err)
}
}
path := fs.Arg(0)
info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("stat %s: %w", path, err)
}
opts := &scanOptions{
key: key,
brute: *brute,
minLen: *minLen,
recursive: *recursive,
}
if info.IsDir() {
return scanDirectory(path, opts)
}
return scanFile(path, opts)
}
type scanOptions struct {
key []byte
brute bool
minLen int
recursive bool
}
// scanDirectory recursively scans a directory for smali files
func scanDirectory(dir string, opts *scanOptions) error {
var files []string
// Get absolute path of base directory for computing relative paths
absDir, err := filepath.Abs(dir)
if err != nil {
absDir = dir
}
walkFn := func(path string, d os.DirEntry, err error) error {
if err != nil {
return nil // Skip files we can't access
}
if d.IsDir() {
return nil
}
// Only scan smali files in directories
if strings.HasSuffix(path, ".smali") {
files = append(files, path)
}
return nil
}
if opts.recursive {
if err := filepath.WalkDir(dir, walkFn); err != nil {
return fmt.Errorf("walk %s: %w", dir, err)
}
} else {
entries, err := os.ReadDir(dir)
if err != nil {
return fmt.Errorf("read dir %s: %w", dir, err)
}
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".smali") {
files = append(files, filepath.Join(dir, e.Name()))
}
}
}
if len(files) == 0 {
fmt.Printf("No smali files found in %s\n", dir)
return nil
}
fmt.Printf("Scanning %d smali files in %s\n\n", len(files), dir)
totalFound := 0
for _, file := range files {
// Compute relative path for display
relPath, err := filepath.Rel(absDir, file)
if err != nil {
relPath = file
}
found, err := scanFileWithResults(file, relPath, opts)
if err != nil {
fmt.Fprintf(os.Stderr, "Error scanning %s: %v\n", relPath, err)
continue
}
totalFound += found
}
fmt.Printf("\nTotal: %d decrypted strings\n", totalFound)
return nil
}
// scanFile scans a single file and prints results
func scanFile(path string, opts *scanOptions) error {
content, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read %s: %w", path, err)
}
text := string(content)
fmt.Printf("Scanning %s (%d bytes)\n\n", path, len(content))
// Check if this looks like a smali file
if strings.Contains(text, ".array-data") || strings.Contains(text, "xor-int/lit8") {
return scanSmaliFileWithPath(path, text, opts)
}
// Scan for base64 strings
found := 0
seen := make(map[string]bool)
fmt.Println("=== Base64 Strings ===")
lines := strings.Split(text, "\n")
for lineNum, line := range lines {
for _, match := range base64Pattern.FindAllString(line, -1) {
if seen[match] || len(match) < opts.minLen {
continue
}
seen[match] = true
decoded, decErr := base64.StdEncoding.DecodeString(match)
if decErr != nil || len(decoded) < 4 {
continue
}
results := tryDecrypt(decoded, opts.key, opts.brute)
for _, r := range results {
fmt.Printf("%s:%d: %s\n", path, lineNum+1, match)
fmt.Printf(" -> [0x%02x] %s\n", r.key, r.plaintext)
found++
}
}
}
fmt.Printf("\nFound %d decodable strings\n", found)
return nil
}
// scanFileWithResults scans a file and returns count of found strings
func scanFileWithResults(path string, displayPath string, opts *scanOptions) (int, error) {
content, err := os.ReadFile(path)
if err != nil {
return 0, err
}
text := string(content)
// Check if this looks like a smali file
if strings.Contains(text, ".array-data") || strings.Contains(text, "xor-int/lit8") {
return scanSmaliFileCount(displayPath, text, opts)
}
return 0, nil
}
// scanSmaliFile handles smali files with .array-data sections (legacy, for tests)
func scanSmaliFile(text string, key []byte, brute bool, minLen int) error {
opts := &scanOptions{key: key, brute: brute, minLen: minLen}
return scanSmaliFileWithPath("", text, opts)
}
// scanSmaliFileWithPath scans a smali file with path for line number output
func scanSmaliFileWithPath(path string, text string, opts *scanOptions) error {
fmt.Println("Detected smali file, scanning .array-data sections...")
key := opts.key
// Find xor key used in smali if not specified
if key == nil && !opts.brute {
xorKeyPattern := regexp.MustCompile(`xor-int/lit8\s+v\d+,\s*v\d+,\s*0x([0-9a-fA-F]+)`)
if matches := xorKeyPattern.FindStringSubmatch(text); len(matches) > 1 {
keyByte, _ := hex.DecodeString(matches[1])
if len(keyByte) > 0 {
key = keyByte
fmt.Printf("Auto-detected XOR key from smali: 0x%s\n", matches[1])
}
}
}
// Find array data sections with their line numbers
lines := strings.Split(text, "\n")
arrayStartPattern := regexp.MustCompile(`^\s*:array_([0-9a-f]+)\s*$`)
arrayDataPattern := regexp.MustCompile(`^\s*\.array-data\s+1\s*$`)
type arrayInfo struct {
name string
lineNum int
data []byte
hexBytes string
}
var arrays []arrayInfo
for i := 0; i < len(lines); i++ {
// Look for :array_N label
if matches := arrayStartPattern.FindStringSubmatch(lines[i]); matches != nil {
arrayName := matches[1]
arrayLine := i + 1 // 1-indexed
// Next line should be .array-data 1
if i+1 < len(lines) && arrayDataPattern.MatchString(lines[i+1]) {
// Collect bytes until .end array-data
var byteValues []byte
j := i + 2
for j < len(lines) && !strings.Contains(lines[j], ".end array-data") {
byteMatches := smaliArrayPattern.FindAllStringSubmatch(lines[j], -1)
for _, b := range byteMatches {
val, _ := hex.DecodeString(fmt.Sprintf("%02s", b[1]))
if len(val) > 0 {
byteValues = append(byteValues, val[0])
}
}
j++
}
if len(byteValues) >= opts.minLen {
arrays = append(arrays, arrayInfo{
name: arrayName,
lineNum: arrayLine,
data: byteValues,
hexBytes: hex.EncodeToString(byteValues),
})
}
i = j // Skip to end of array
}
}
}
if len(arrays) == 0 {
fmt.Println("No .array-data sections found")
return nil
}
fmt.Printf("Found %d array sections\n\n", len(arrays))
for _, arr := range arrays {
results := tryDecrypt(arr.data, key, opts.brute)
for _, r := range results {
if path != "" {
fmt.Printf("%s:%d: array_%s: %s\n", path, arr.lineNum, arr.name, arr.hexBytes)
} else {
fmt.Printf("array_%s: %s\n", arr.name, arr.hexBytes)
}
fmt.Printf(" -> [0x%02x] %s\n", r.key, r.plaintext)
}
}
return nil
}
// scanSmaliFileCount scans and returns count (for directory scanning)
func scanSmaliFileCount(path string, text string, opts *scanOptions) (int, error) {
key := opts.key
// Find xor key used in smali if not specified
if key == nil && !opts.brute {
xorKeyPattern := regexp.MustCompile(`xor-int/lit8\s+v\d+,\s*v\d+,\s*0x([0-9a-fA-F]+)`)
if matches := xorKeyPattern.FindStringSubmatch(text); len(matches) > 1 {
keyByte, _ := hex.DecodeString(matches[1])
if len(keyByte) > 0 {
key = keyByte
}
}
}
// Find array data sections with their line numbers
lines := strings.Split(text, "\n")
arrayStartPattern := regexp.MustCompile(`^\s*:array_([0-9a-f]+)\s*$`)
arrayDataPattern := regexp.MustCompile(`^\s*\.array-data\s+1\s*$`)
found := 0
for i := 0; i < len(lines); i++ {
// Look for :array_N label
if matches := arrayStartPattern.FindStringSubmatch(lines[i]); matches != nil {
arrayName := matches[1]
arrayLine := i + 1 // 1-indexed
// Next line should be .array-data 1
if i+1 < len(lines) && arrayDataPattern.MatchString(lines[i+1]) {
// Collect bytes until .end array-data
var byteValues []byte
j := i + 2
for j < len(lines) && !strings.Contains(lines[j], ".end array-data") {
byteMatches := smaliArrayPattern.FindAllStringSubmatch(lines[j], -1)
for _, b := range byteMatches {
val, _ := hex.DecodeString(fmt.Sprintf("%02s", b[1]))
if len(val) > 0 {
byteValues = append(byteValues, val[0])
}
}
j++
}
if len(byteValues) >= opts.minLen {
results := tryDecrypt(byteValues, key, opts.brute)
for _, r := range results {
fmt.Printf("%s:%d: array_%s: %s\n", path, arrayLine, arrayName, hex.EncodeToString(byteValues))
fmt.Printf(" -> [0x%02x] %s\n", r.key, r.plaintext)
found++
}
}
i = j // Skip to end of array
}
}
}
return found, nil
}
type decryptResult struct {
key byte
plaintext string
}
// tryDecrypt attempts to decrypt data with given key or brute force
func tryDecrypt(data []byte, key []byte, brute bool) []decryptResult {
var results []decryptResult
if brute {
for k := 0; k <= 0xff; k++ {
decrypted := xorDecrypt(data, []byte{byte(k)})
if looksLikeText(decrypted) {
results = append(results, decryptResult{
key: byte(k),
plaintext: sanitizeOutput(decrypted),
})
}
}
} else if key != nil {
decrypted := xorDecrypt(data, key)
if looksLikeText(decrypted) {
keyByte := byte(0)
if len(key) > 0 {
keyByte = key[0]
}
results = append(results, decryptResult{
key: keyByte,
plaintext: sanitizeOutput(decrypted),
})
}
}
return results
}
func expandGlobs(files []string) ([]string, error) {
var expanded []string
for _, pattern := range files {
if strings.ContainsAny(pattern, "*?[") {
matches, err := filepath.Glob(pattern)
if err != nil {
return nil, fmt.Errorf("expand %q: %w", pattern, err)
}
expanded = append(expanded, matches...)
continue
}
expanded = append(expanded, pattern)
}
return expanded, nil
}
func decodeKey(keyHex string) ([]byte, error) {
if keyHex == "" {
return nil, nil
}
key, err := hex.DecodeString(keyHex)
if err != nil {
return nil, fmt.Errorf("invalid hex key: %w", err)
}
return key, nil
}
// findKeyLength uses Kasiski examination to find repeated sequences
func findKeyLength(data []byte) int {
if len(data) < 100 {
return 0
}
// Find repeated sequences of length 6-16 bytes
distances := make(map[int]int)
minSeqLen := 6
maxSeqLen := 16
for seqLen := minSeqLen; seqLen <= maxSeqLen; seqLen++ {
seen := make(map[string][]int)
for i := 0; i <= len(data)-seqLen; i++ {
seq := string(data[i : i+seqLen])
seen[seq] = append(seen[seq], i)
}
for _, positions := range seen {
if len(positions) < 2 {
continue
}
for i := 1; i < len(positions); i++ {
dist := positions[i] - positions[i-1]
if dist > 0 && dist < 10000 {
distances[dist]++
}
}
}
}
if len(distances) == 0 {
return 0
}
// Find GCD of most common distances
type distCount struct {
dist int
count int
}
var dcs []distCount
for d, c := range distances {
dcs = append(dcs, distCount{d, c})
}
sort.Slice(dcs, func(i, j int) bool {
return dcs[i].count > dcs[j].count
})
// Take top distances and find their GCD
topN := min(20, len(dcs))
result := dcs[0].dist
for i := 1; i < topN; i++ {
result = gcd(result, dcs[i].dist)
}
// Common key lengths
validLengths := []int{4, 8, 12, 16, 20, 24, 32}
for _, vl := range validLengths {
if result%vl == 0 || vl%result == 0 {
// Prefer the valid length closest to our GCD
if result >= vl {
return vl
}
}
}
// If GCD is reasonable, use it
if result >= 4 && result <= 64 {
return result
}
return 16 // Default fallback
}
// frequencyKeyLength uses index of coincidence to estimate key length
func frequencyKeyLength(data []byte, maxLen int) int {
bestLen := 1
bestIC := 0.0
for keyLen := 1; keyLen <= maxLen; keyLen++ {
var totalIC float64
for offset := 0; offset < keyLen; offset++ {
// Extract every keyLen-th byte starting at offset
var slice []byte
for i := offset; i < len(data); i += keyLen {
slice = append(slice, data[i])
}
totalIC += indexOfCoincidence(slice)
}
avgIC := totalIC / float64(keyLen)
// English text has IC ~0.065, random ~0.038
// Higher IC suggests correct key length
if avgIC > bestIC {
bestIC = avgIC
bestLen = keyLen
}
}
return bestLen
}
func indexOfCoincidence(data []byte) float64 {
if len(data) < 2 {
return 0
}
freq := make([]int, 256)
for _, b := range data {
freq[b]++
}
var sum float64
n := float64(len(data))
for _, f := range freq {
sum += float64(f) * float64(f-1)
}
return sum / (n * (n - 1))
}
// recoverKey attempts to recover the key using known plaintext headers
func recoverKey(data []byte, keyLen int) []byte {
if keyLen == 0 || len(data) < keyLen {
return nil
}
// Try multiple key lengths (multiples of detected length)
tryLengths := []int{keyLen}
for mult := 2; mult <= 8; mult++ {
if keyLen*mult <= 64 {
tryLengths = append(tryLengths, keyLen*mult)
}
}
for _, kl := range tryLengths {
// Try each known header
for _, header := range knownHeaders {
if len(header) < kl {
// Skip headers that are too short to derive a full key of length kl
continue
}
// XOR ciphertext with expected plaintext to get key
key := make([]byte, kl)
for i := 0; i < kl; i++ {
key[i] = data[i] ^ header[i]
}
// Validate: decrypt and check if result looks like text
decrypted := xorDecrypt(data[:min(500, len(data))], key)
if looksLikeText(decrypted) {
return key
}
}
}
return nil
}
// xorDecrypt decrypts data with repeating XOR key
func xorDecrypt(data, key []byte) []byte {
if len(key) == 0 {
return data
}
result := make([]byte, len(data))
for i, b := range data {
result[i] = b ^ key[i%len(key)]
}
return result
}
// looksLikeText checks if data appears to be readable text/code
func looksLikeText(data []byte) bool {
if len(data) == 0 {
return false
}
printable := 0
for _, b := range data {
if (b >= 0x20 && b <= 0x7e) || b == '\n' || b == '\r' || b == '\t' {
printable++
}
}
ratio := float64(printable) / float64(len(data))
return ratio > 0.85
}
// analyzeKeyPattern looks for patterns in the recovered key
func analyzeKeyPattern(key []byte) {
if len(key) < 2 {
return
}
// Check for arithmetic progression
diff := int(key[1]) - int(key[0])
isArithmetic := true
for i := 2; i < len(key); i++ {
if int(key[i])-int(key[i-1]) != diff {
isArithmetic = false
break
}
}
if isArithmetic {
fmt.Printf(" Pattern: arithmetic progression (diff = 0x%02x)\n", byte(diff)&0xff)
}
// Check for repeating pattern
for patLen := 1; patLen <= len(key)/2; patLen++ {
if len(key)%patLen != 0 {
continue
}
isRepeating := true
pattern := key[:patLen]
for i := patLen; i < len(key); i++ {
if key[i] != pattern[i%patLen] {
isRepeating = false
break
}
}
if isRepeating {
fmt.Printf(" Pattern: repeating %d-byte sequence: %s\n", patLen, hex.EncodeToString(pattern))
break
}
}
}
func sanitizeOutput(data []byte) string {
var buf bytes.Buffer
for _, b := range data {
if b >= 0x20 && b <= 0x7e {
buf.WriteByte(b)
} else if b == '\n' || b == '\r' || b == '\t' {
buf.WriteByte(b)
} else {
buf.WriteString(fmt.Sprintf("\\x%02x", b))
}
}
return buf.String()
}
func gcd(a, b int) int {
for b != 0 {
a, b = b, a%b
}
return a
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func readStdin() ([]byte, error) {
return io.ReadAll(os.Stdin)
}
func newFlagSet(name, usage string) *flag.FlagSet {
fs := flag.NewFlagSet(name, flag.ContinueOnError)
fs.SetOutput(io.Discard)
fs.Usage = func() {
fmt.Fprintln(os.Stderr, usage)
}
return fs
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment