Initial commit
This commit is contained in:
parent
cd0aadeeda
commit
fce4e16333
10 changed files with 515 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
xor-vigenere
|
38
README.md
Normal file
38
README.md
Normal 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
94
coder.go
Normal 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
1
example/cipher.txt
Normal file
File diff suppressed because one or more lines are too long
13
go.mod
Normal file
13
go.mod
Normal 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
28
go.sum
Normal 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
179
main.go
Normal 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
61
text.go
Normal 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
82
top.go
Normal 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
18
util.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue