Initial commit

This commit is contained in:
Alex Vanin 2022-10-16 20:05:18 +03:00
parent cd0aadeeda
commit fce4e16333
10 changed files with 515 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
xor-vigenere

38
README.md Normal file
View file

@ -0,0 +1,38 @@
# xor-vigenere
xor-encoded vigenere cypher decoder for ru-ru alphabet.
Decodes uppercase text without white spaces and Ë letter.
## Build
Requires [go1.19](https://go.dev/dl/)
```
$ go build .
```
## Usage
Specify input file and wait for result.
```
$ ./xor-vigenere -i example/cipher.txt -r 2
Cypher text file: example/cipher.txt
Parallel workers: 12 (override with -w flag)
Trying to predict secret key length ... 8 (override with -l flag)
Cypher text sample size to analyze: 2000 (override with -s flag)
Number of letters in permutations: 4 [ОЕАИ] (override with -p flag)
Number of results to show: 2 (override with -r flag)
Analyzing 100% [##################################################] (65536/65536, 36315 keys/s)
Key=АБЫРВАЛГ Sample:ГДЕЖЕВЕСЬМИРВДЕНЬМОЕГОРОЖДЕНИЯГДЕЭЛЕКТРИЧЕСКИЕФО Div:0.145333
Key=АЙЫРВАЛГ Sample:ГМЕЖЕВЕСЬДИРВДЕНЬДОЕГОРОЖМЕНИЯГДЕХЛЕКТРИЧНСКИЕФО Div:0.166576
Execution duration: 1.805705397s
```
`АБЫРВАЛГ` is a valid secret key on top.
To receive better (but slower) results use:
- bigger sample size for analyzing cypher text (`-s 0` for analyzing whole text)
- look through more possible keys (`-r` flag)
- use more frequently used letters to create more permutations (slows **really** hard, `-p` flag)

94
coder.go Normal file
View file

@ -0,0 +1,94 @@
package main
import "math"
var (
alphabet = []rune("АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ")
alphabetLen = len(alphabet)
alphabetFreq = []float64{ // took from https://dpva.ru/Guide/GuideUnitsAlphabets/Alphabets/FrequencyRuLetters/ (without Ë)
0.080159, // А
0.015942, // Б
0.045400, // ...
0.016957,
0.029801,
0.084523,
0.009398,
0.016492,
0.073559,
0.012090,
0.034952,
0.044013,
0.032080,
0.066997,
0.109714,
0.028117,
0.047352,
0.054698,
0.062606,
0.026225,
0.002645,
0.009710,
0.004829,
0.014453,
0.007283,
0.003608,
0.000367,
0.018999,
0.017392,
0.003188,
0.006377,
0.020074,
}
mostFrequest = []rune("ОЕАИНТСРВЛК")
)
type Coder struct {
ToCode map[rune]int
ToRune map[int]rune
RuFreq map[rune]float64
}
func NewCoder() Coder {
coder := Coder{
ToCode: make(map[rune]int, alphabetLen),
ToRune: make(map[int]rune, alphabetLen),
RuFreq: make(map[rune]float64, alphabetLen),
}
for i, r := range alphabet {
coder.ToCode[r] = i
coder.ToRune[i] = r
coder.RuFreq[r] = alphabetFreq[i]
}
return coder
}
// Code with xor encoding.
func (c Coder) Code(text, key []rune) []rune {
res := make([]rune, 0, len(text))
for i, r := range text {
keyIndex := i % len(key)
res = append(res, rune(c.ToRune[c.ToCode[r]^c.ToCode[key[keyIndex]]]))
}
return res
}
func (c Coder) RuneFrequency(text, key []rune) map[rune]int {
res := make(map[rune]int, alphabetLen)
for i, r := range text {
keyIndex := i % len(key)
res[rune(c.ToRune[c.ToCode[r]^c.ToCode[key[keyIndex]]])] += 1
}
return res
}
func (c Coder) AlphabetDivergence(analysis map[rune]int, textLen float64) float64 {
var result float64
for r, freq := range analysis {
result += math.Abs(float64(freq)/textLen - c.RuFreq[r])
}
return result
}

1
example/cipher.txt Normal file

File diff suppressed because one or more lines are too long

13
go.mod Normal file
View file

@ -0,0 +1,13 @@
module xor-vigenere
go 1.19
require github.com/schollz/progressbar/v3 v3.11.0
require (
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/rivo/uniseg v0.4.2 // indirect
golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43 // indirect
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect
)

28
go.sum Normal file
View file

@ -0,0 +1,28 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/schollz/progressbar/v3 v3.11.0 h1:3nIBUF1Zw/pGUaRHP7PZWmARP7ZQbWQ6vL6hwoQiIvU=
github.com/schollz/progressbar/v3 v3.11.0/go.mod h1:R2djRgv58sn00AGysc4fN0ip4piOGd3z88K+zVBjczs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43 h1:OK7RB6t2WQX54srQQYSXMW8dF5C6/8+oA/s5QBmmto4=
golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w=
golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

179
main.go Normal file
View file

@ -0,0 +1,179 @@
package main
import (
"flag"
"fmt"
"os"
"runtime"
"sync"
"time"
"github.com/schollz/progressbar/v3"
)
const (
outputSampleSize = 6
maxSecretKeySize = 12
expectedMinTextLen = 2000
)
func main() {
input := flag.String("i", "", "cypher text")
debug := flag.Bool("debug", false, "print debug info")
secretKeyLen := flag.Int("l", 0, "length of secret key if known")
permLettersLen := flag.Int("p", 4, "number of most frequest russian letters for cartesian permutation")
workers := flag.Int("w", 0, "number of parallel workers (default number of CPU)")
resultLen := flag.Int("r", 50, "number of top results to process")
sampleSize := flag.Int("s", 2000, "size of cypher text sample to analyze, 0 to analyze whole text")
flag.Parse()
start := time.Now()
// Read cypher text.
data, err := os.ReadFile(*input)
if err != nil {
fmt.Printf("reading cypher text: %s\n", err.Error())
os.Exit(1)
}
fmt.Printf("Cypher text file: %s\n", *input)
cypherText := []rune(string(data))
if len(cypherText) < expectedMinTextLen {
fmt.Printf("! Text length %d < %d, frequency analysis may be affected", len(cypherText), expectedMinTextLen)
}
// Set number of parallel workers.
if *workers == 0 {
cpuN := runtime.NumCPU()
workers = &cpuN
}
fmt.Printf("Parallel workers: %d (override with -w flag)\n", *workers)
// Finding secret key length.
if *secretKeyLen == 0 {
fmt.Print("Trying to predict secret key length ...")
prediction := predictSecretKeyLen(cypherText, maxSecretKeySize)
fmt.Printf(" %d (override with -l flag)\n", prediction)
secretKeyLen = &prediction
} else {
fmt.Printf("Secret key length: %d (override with -w flag)\n", *secretKeyLen)
}
// Set sample size to analyze crypto text.
if *sampleSize == 0 {
size := len(cypherText)
sampleSize = &size
}
fmt.Printf("Cypher text sample size to analyze: %d (override with -s flag)\n", *sampleSize)
// Print other parameters.
fmt.Printf("Number of letters in permutations: %d [%s] (override with -p flag)\n", *permLettersLen, string(mostFrequest[:*permLettersLen]))
//
fmt.Printf("Number of results to show: %d (override with -r flag)\n", *resultLen)
// Start decoding.
coder := NewCoder()
groups := groupText(cypherText, *secretKeyLen)
permutations := make([][]rune, *secretKeyLen)
for i := range permutations {
permutations[i] = make([]rune, *permLettersLen)
mostUsedRune := popularRune(groups[i])
for j, r := range mostFrequest[:*permLettersLen] {
permutations[i][j] = rune(coder.ToRune[coder.ToCode[mostUsedRune]^coder.ToCode[r]])
}
}
possibleKeysLen := 1
for _, p := range permutations {
possibleKeysLen *= len(p)
}
result := NewTop(*resultLen)
pb := newProgressBar(possibleKeysLen)
jobs := make(chan []rune)
wg := new(sync.WaitGroup)
wg.Add(possibleKeysLen)
for i := 0; i < *workers; i++ {
go func(jobs <-chan []rune) {
for job := range jobs {
decodedFreq := coder.RuneFrequency(cypherText[:*sampleSize], job)
result.Add(string(job), coder.AlphabetDivergence(decodedFreq, float64(*sampleSize)))
pb.Add(1)
wg.Done()
}
}(jobs)
}
cartesian(permutations, func(r []rune) {
jobs <- r
})
wg.Wait()
fmt.Println()
// Print results.
for _, s := range result.List() {
fmt.Printf("Key=%s Sample:%s Div:%f\n", s, string(coder.Code(cypherText[:outputSampleSize**secretKeyLen], []rune(s))), result.Value(s))
}
fmt.Printf("Execution duration: %s\n", time.Since(start))
if *debug {
PrintMemUsage()
}
}
func newProgressBar(cap int) *progressbar.ProgressBar {
return progressbar.NewOptions(cap,
progressbar.OptionSetDescription("Analyzing"),
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionSetWidth(10),
progressbar.OptionThrottle(100*time.Millisecond),
progressbar.OptionShowCount(),
progressbar.OptionShowIts(),
progressbar.OptionSetItsString("keys"),
progressbar.OptionOnCompletion(func() {
fmt.Fprint(os.Stderr, "\n")
}),
progressbar.OptionSpinnerType(14),
progressbar.OptionSetWidth(50),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "#",
SaucerHead: "#",
SaucerPadding: " ",
BarStart: "[",
BarEnd: "]",
}),
)
}
func predictSecretKeyLen(text []rune, limit int) int {
lim := len(text) / 2
if lim > limit {
lim = limit
}
ln := len(text)
var (
predictedLen int
predictedOverlap int
)
for i := 1; i <= lim; i++ {
overlap := 0
for j := 0; j < ln; j++ {
shiftIndex := (j + i) % ln
if text[j] == text[shiftIndex] {
overlap++
}
}
if overlap > predictedOverlap {
predictedOverlap = overlap
predictedLen = i
}
}
return predictedLen
}

61
text.go Normal file
View file

@ -0,0 +1,61 @@
package main
func groupText(text []rune, period int) []map[rune]int {
res := make([]map[rune]int, period)
for i := range res {
res[i] = make(map[rune]int, alphabetLen)
}
for i, r := range text {
index := i % period
res[index][r] += 1
}
return res
}
func popularRune(m map[rune]int) rune {
var (
res rune
max = 0
)
for l, counter := range m {
if counter > max {
res = l
max = counter
}
}
return res
}
func cartesian(runes [][]rune, processCombination func([]rune)) {
c := 1
for _, a := range runes {
c *= len(a)
}
if c == 0 {
return
}
b := make([]rune, c*len(runes))
n := make([]int, len(runes))
s := 0
for i := 0; i < c; i++ {
e := s + len(runes)
pi := b[s:e]
s = e
for j, n := range n {
pi[j] = runes[j][n]
}
for j := len(n) - 1; j >= 0; j-- {
n[j]++
if n[j] < len(runes[j]) {
break
}
n[j] = 0
}
processCombination(pi)
}
}

82
top.go Normal file
View file

@ -0,0 +1,82 @@
package main
import (
"sort"
"sync"
)
type Top struct {
mu sync.Mutex
capacity int
positions map[string]float64
threshold float64
thresholdKey string
}
func NewTop(capacity int) Top {
return Top{
capacity: capacity,
positions: make(map[string]float64, capacity),
}
}
func (t *Top) Add(key string, divergence float64) {
t.mu.Lock()
defer t.mu.Unlock()
// When structure is not filled, add all values.
if len(t.positions) < t.capacity {
t.positions[key] = divergence
if divergence > t.threshold {
t.threshold = divergence
t.thresholdKey = key
}
return
}
// When structure is filled, do not add values outside of the threshold.
if divergence > t.threshold {
return
}
// To add new value, remove threshold value.
delete(t.positions, t.thresholdKey)
t.positions[key] = divergence
// Find new threshold value in updated map.
var (
newThreshold float64
newThresholdVal string
)
for key, divergence := range t.positions {
if divergence > newThreshold {
newThreshold = divergence
newThresholdVal = key
}
}
t.threshold = newThreshold
t.thresholdKey = newThresholdVal
}
func (t *Top) List() []string {
t.mu.Lock()
defer t.mu.Unlock()
res := make([]string, 0, len(t.positions))
for key := range t.positions {
res = append(res, key)
}
sort.Slice(res, func(i, j int) bool {
return t.positions[res[i]] < t.positions[res[j]]
})
return res
}
func (t *Top) Value(key string) float64 {
t.mu.Lock()
defer t.mu.Unlock()
return t.positions[key]
}

18
util.go Normal file
View file

@ -0,0 +1,18 @@
package main
import (
"fmt"
"runtime"
)
func PrintMemUsage() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Alloc = %v MiB", bToMb(m.Alloc))
fmt.Printf("\tSys = %v MiB", bToMb(m.Sys))
fmt.Printf("\tNumGC = %v\n", m.NumGC)
}
func bToMb(b uint64) uint64 {
return b / 1024 / 1024
}