Skip to content

Commit

Permalink
Merge pull request #14 from nronzel/more-tests
Browse files Browse the repository at this point in the history
More tests
  • Loading branch information
nronzel authored Feb 12, 2024
2 parents af95c02 + 54bb58b commit 1e8d088
Show file tree
Hide file tree
Showing 14 changed files with 194 additions and 125 deletions.
59 changes: 30 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,16 @@

# XORacle

XORacle is a tool designed to decrypt data encoded with a repeating-key XOR
cipher. Utilizing brute-force methods alongside transposition and frequency
analysis techniques, XORacle will attempt to deduce the key size and the key
itself.
XORacle is a simple tool aimed at decrypting data that's been encrypted using
a repeating-key XOR cipher. It combines a brute-force approach with transposition
and frequency analysis to try and figure out the encryption key's size, the
key itself, and attempts to decrypt the data with the derived key(s).

This project is dockerized and gets deployed to Google Cloud Run.
[View Hosted Site](https://xoracle-uzphfx7uwa-ue.a.run.app)

## ToDo

- [x] Clear the output on each request
- [x] Rate limiting
- [ ] Better HTMX errors
- [ ] More tests
- [x] Use the new ServeMux in Go 1.22 to replace Chi
- [x] Semver versioning
> This project is dockerized and gets deployed to Google Cloud Run.
>
> [View Hosted Site](https://xoracle-uzphfx7uwa-ue.a.run.app)
>
> _See the [Usage](#usage) section for examples to test it out._
## Features

Expand Down Expand Up @@ -92,19 +86,19 @@ Go 1.22

### Steps

1. Clone the repository:
**1. Clone the repository:**

```sh
git clone https://github.com/nronzel/xoracle.git
```

2. Navigate to the project directory:
**2. Navigate to the project directory:**

```sh
cd xoracle
```

3. Install dependencies:
**3. Install dependencies:**

- golang.org/x/time

Expand All @@ -114,24 +108,31 @@ Install dependencies with the command:
go mod tidy
```

4. Build and run the project:
**4. Build and run the project:**

Linux & MacOS:

```sh
go build -o xoracle && ./xoracle
```

> There is also a script located in `scripts/buildprod.sh` that you can use if
> you are running Linux that will build the executable with the production build
> flags. If you are running another OS you will need to modify the build flags for
> your OS, or just build from the command line as normal.
Windows:

```sh
go build -o xoracle.exe && .\xoracle.exe
```

> The script `scripts/buildprod.sh` is included and used to build the binary
> that gets deployed to Cloud Run. You can use this script if you're planning
> on running the binary on a Linux amd64 machine.
>
> The flags used in `buildprod.sh` are:
>
> ```sh
> CGO_ENABLED=0 GOOS=linux GOARCH=amd64
> ```
5. Open your browser and navigate to:
**5. Open your browser and navigate to:**
```text
localhost:8080/
Expand All @@ -141,13 +142,13 @@ localhost:8080/

If you'd like to run this in a Docker container:

Pull the image:
**Pull the image:**

```sh
docker pull sutats/xoracle:latest
```

Run the image:
**Run the image:**

```sh
docker run -p 8080:8080 xoracle
Expand All @@ -160,13 +161,13 @@ the image in a container.

While in the root of the project directory:

Build the image:
**Build the image:**

```sh
docker build . -t xoracle:latest
```

Run the image in a container:
**Run the image in a container:**

```sh
docker run -p 8080:8080 xoracle
Expand Down Expand Up @@ -211,7 +212,7 @@ Begin by identifying potential key sizes. XORacle employs a heuristic based on
the Hamming distance (the number of differing bits) between the blocks of
ciphertext. By analyzing the distances between blocks of various sizes, we can
make educated guesses about the most probable key sizes. The assumption is that
the correct key size with result in the smallest average normalized Hamming
the correct key size will result in the smallest average normalized Hamming
distance because correctly sized blocks aligned with the repeating key will have
more similar bit patterns.

Expand Down
10 changes: 2 additions & 8 deletions pkg/decryption/blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,15 @@ package decryption
// ciphers, where you want to analyze each nth byte under the assumption they
// were XOR'd with the same byte of the key.
func transposeBlocks(blocks [][]byte, keySize int) [][]byte {
// Initialize a slice of byte slices with length equal to the keySize.
// This will hold the transposed bytes. Each index i of 'transposed'
// will contain the ith byte from each of the input blocks.
transposed := make([][]byte, keySize)

for i := 0; i < keySize; i++ {
for _, block := range blocks {
// Check if the current block has enough bytes to include an ith
// byte. This is necessary because the last block might be shorter
// than the others.
// byte. The last block might be shorter than the others.
if i < len(block) {
// If the current block has an ith byte, append that byte to
// the i'th position in the 'transposed' slice. This effectively
// groups all nth bytes together in the same slice, facilitating
// further analysis or manipulation based on those bytes alone.
// the i'th position in the 'transposed' slice.
transposed[i] = append(transposed[i], block[i])
}
}
Expand Down
3 changes: 1 addition & 2 deletions pkg/decryption/blocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"testing"
)

// TestTransposeBlocks tests the transposeBlocks function with various input scenarios.
func TestTransposeBlocks(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -71,7 +70,7 @@ func TestTransposeBlocks(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
got := transposeBlocks(tt.blocks, tt.keySize)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("transposeBlocks() = %v, want %v", got, tt.want)
t.Errorf("want: %v, got: %v", tt.want, got)
}
})
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/decryption/dc_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import "github.com/nronzel/xoracle/utils"
// Scores the resulting decrypted data after attempting to decrypt with the guessed
// key and keySize. Higher score means it is more likely English text and it will
// return that result, removing the false positives.
//
// If the scoring results in a tie; the first result will be returned as the best.
func ScoreResults(results []DecryptionResult) DecryptionResult {
var highScore float64
var best DecryptionResult
Expand Down
20 changes: 10 additions & 10 deletions pkg/decryption/dc_util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,38 @@ import (

func TestScoreResults(t *testing.T) {
tests := []struct {
name string
results []DecryptionResult
expected DecryptionResult
name string
results []DecryptionResult
want DecryptionResult
}{
{
name: "A should win",
results: []DecryptionResult{
{KeySize: 4, Key: []byte("A"), DecryptedData: "This is valid English"},
{KeySize: 4, Key: []byte("A"), DecryptedData: "asldkjfaiocnlajeizpg"},
},
expected: DecryptionResult{KeySize: 4, Key: []byte("A"), DecryptedData: "This is valid English"},
want: DecryptionResult{KeySize: 4, Key: []byte("A"), DecryptedData: "This is valid English"},
},
{
name: "multiple same score, first one wins",
results: []DecryptionResult{
{KeySize: 3, Key: []byte{1, 2, 3}, DecryptedData: "excellent"},
{KeySize: 5, Key: []byte{2, 3, 4}, DecryptedData: "excellent"},
},
expected: DecryptionResult{KeySize: 3, Key: []byte{1, 2, 3}, DecryptedData: "excellent"},
want: DecryptionResult{KeySize: 3, Key: []byte{1, 2, 3}, DecryptedData: "excellent"},
},
{
name: "empty input",
results: []DecryptionResult{},
expected: DecryptionResult{},
name: "empty input",
results: []DecryptionResult{},
want: DecryptionResult{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ScoreResults(tt.results)
if got.DecryptedData != tt.expected.DecryptedData {
t.Fatalf("Expected: %v, got: %v", tt.expected.DecryptedData, got.DecryptedData)
if got.DecryptedData != tt.want.DecryptedData {
t.Fatalf("want: %v, got: %v", tt.want.DecryptedData, got.DecryptedData)
}
})
}
Expand Down
25 changes: 8 additions & 17 deletions pkg/decryption/decryption.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func singleByteXORCipher(encoded []byte) (byte, []byte) {
score := utils.ScoreText(decoded)

// If the current message's score is higher than the highest score found
// so far, update maxScore, key, and message with the current values.
// so far - update maxScore, key, and message with the current values.
if score > maxScore {
maxScore = score
key = byte(k)
Expand All @@ -44,11 +44,10 @@ func singleByteXORCipher(encoded []byte) (byte, []byte) {
return key, message
}

// guessKeySizes attempts to guess the key size used in an encryption algorithm
// based on the average Hamming distance between blocks of bytes in the
// encrypted data. It returns a slice of the top candidate key sizes that have
// the lowest average Hamming distances, suggesting these sizes are likely
// candidates for the actual key size.
// guessKeySizes attempts to guess the key size based on the average Hamming
// distance between blocks of bytes in the encrypted data. It returns a slice
// of the top candidate key sizes that have the lowest average Hamming
// distances, suggesting these sizes are likely candidates for the actual key size.
func GuessKeySizes(data []byte) ([]int, error) {
const maxKeySize = 40 // The maximum key size to test.
const maxKeysToCompare = 2 // The number of top key sizes to return.
Expand All @@ -60,7 +59,6 @@ func GuessKeySizes(data []byte) ([]int, error) {

var scores []keySizeScore

// Loop through each possible key size from 2 to maxKeySize (inclusive).
for keySize := 2; keySize <= maxKeySize; keySize++ {
// Skip keySize if keySize*4 exceeds the length of the data. Not able
// to make a meaningful comparison.
Expand Down Expand Up @@ -91,21 +89,14 @@ func GuessKeySizes(data []byte) ([]int, error) {
return topKeySizes, nil
}

type DecryptionResult struct {
KeySize int
Key []byte
DecryptedData string
}

// ProcessKeySizes attempts to decrypt the provided byte slice (data) for each
// ProcessKeySizes attempts to decrypt the provided bytes for each
// of the top key sizes found. It attempts to break a repeating-key XOR cipher
// without directly knowing the key.
func ProcessKeySizes(topKeySizes []int, data []byte) []DecryptionResult {
var results []DecryptionResult
for _, keySize := range topKeySizes {
// Break the ciphertext into blocks of KEYSIZE length to manage the
// analysis in chunks. This approach is critical for transposing the
// bytes correctly in a later step.
// Break the ciphertext into blocks of keySize length to manage the
// analysis in chunks.
blocks := make([][]byte, int(math.Ceil(float64(len(data))/float64(keySize))))
for i := 0; i < len(blocks); i++ {
start := i * keySize
Expand Down
8 changes: 4 additions & 4 deletions pkg/decryption/decryption_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ func TestProcessKeySizes(t *testing.T) {
name string
topKeySizes []int
data []byte
expected []DecryptionResult
want []DecryptionResult
}{
{name: "Base64 Decoded - Should Decrypt",
topKeySizes: []int{2},
data: decodedBase64,
expected: []DecryptionResult{
want: []DecryptionResult{
{KeySize: 2,
Key: []byte("AB"),
DecryptedData: "secret text dudee",
Expand All @@ -29,7 +29,7 @@ func TestProcessKeySizes(t *testing.T) {
{name: "Base64 Decoded - Should Decrypt",
topKeySizes: []int{2},
data: decodedHex,
expected: []DecryptionResult{
want: []DecryptionResult{
{KeySize: 2,
Key: []byte("AB"),
DecryptedData: "secret text dudee",
Expand All @@ -41,7 +41,7 @@ func TestProcessKeySizes(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ProcessKeySizes(tt.topKeySizes, tt.data)
if !reflect.DeepEqual(got[0], tt.expected[0]) {
if !reflect.DeepEqual(got[0], tt.want[0]) {
t.Errorf("did not receive expected result: %v", got[0])
}
})
Expand Down
2 changes: 1 addition & 1 deletion pkg/decryption/hamming.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (

// averageHammingDistance calculates the average Hamming distance
// between multiple pairs of blocks of bytes, where each block is of size
// 'keySize'. Helps in estimating the size of the key used in encryption
// keySize. Helps in estimating the size of the key used in encryption
// algorithms that operate in block modes.
func averageHammingDistance(data []byte, keySize int) (float64, error) {
// At least 4 blocks of 'keySize' are needed to make a meaningful comparison.
Expand Down
Loading

0 comments on commit 1e8d088

Please sign in to comment.