Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ dist
build
MANIFEST
TODO

__pycache__
.ruff*
xortool_out
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13.2
106 changes: 54 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,75 +1,79 @@
xortool.py
====================
# xortool.py

A tool to do some xor analysis:

- guess the key length (based on count of equal chars)
- guess the key (base on knowledge of most frequent char)
- guess the key length (based on count of equal chars)
- guess the key (base on knowledge of most frequent char)

**Notice:** xortool is now only running on Python 3. The old Python 2 version is accessible at the `py2` branch. The **pip** package has been updated.
**Notice:** xortool is now only running on Python 3. (And Update with `rye` project support and maximum support for Python 3.9-3.13+)

## Installation

```bash
$ pip3 install xortool
pip3 install xortool
```

For development or building this repository, [poetry](https://python-poetry.org/) is needed.


```bash
poetry build
pip install dist/xortool*.whl
```

Usage
---------------------
## Usage

```text
$ xortool --help

Usage: xortool [OPTIONS] [FILENAME]

A tool to do some xor analysis:
- guess the key length (based on count of equal chars)
- guess the key (base on knowledge of most frequent char)

╭─ Arguments ───────────────────────────────────────────────────────────────────────────────╮
│ filename [FILENAME] Input file (or stdin if omitted) │
╰───────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────────────────────────────────────────────╮
│ --hex -x Input is hex-encoded str │
│ --key-length -l INTEGER Length of the key [default: None] │
│ --max-keylen -m INTEGER Maximum key length to probe [default: 65] │
│ --char -c TEXT Most frequent char (one char or hex code) │
│ [default: None] │
│ --brute-chars -b Brute force all possible most frequent chars │
│ --brute-printable -o Same as -b but will only check printable chars │
│ --filter-output -f Filter outputs based on the charset │
│ --text-charset -t TEXT Target text character set │
│ [default: │
│ 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOP… │
│ ] │
│ --known-plaintext -p TEXT Use known plaintext for decoding [default: None] │
│ --version Show version and exit │
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────────────────────────────────────────────╯

```
xortool
A tool to do some xor analysis:
- guess the key length (based on count of equal chars)
- guess the key (base on knowledge of most frequent char)

Usage:
xortool [-x] [-m MAX-LEN] [-f] [-t CHARSET] [FILE]
xortool [-x] [-l LEN] [-c CHAR | -b | -o] [-f] [-t CHARSET] [-p PLAIN] [FILE]
xortool [-x] [-m MAX-LEN| -l LEN] [-c CHAR | -b | -o] [-f] [-t CHARSET] [-p PLAIN] [FILE]
xortool [-h | --help]
xortool --version

Options:
-x --hex input is hex-encoded str
-l LEN, --key-length=LEN length of the key
-m MAX-LEN, --max-keylen=MAX-LEN maximum key length to probe [default: 65]
-c CHAR, --char=CHAR most frequent char (one char or hex code)
-b --brute-chars brute force all possible most frequent chars
-o --brute-printable same as -b but will only check printable chars
-f --filter-output filter outputs based on the charset
-t CHARSET --text-charset=CHARSET target text character set [default: printable]
-p PLAIN --known-plaintext=PLAIN use known plaintext for decoding
-h --help show this help

Notes:
Text character set:
* Pre-defined sets: printable, base32, base64
* Custom sets:
- a: lowercase chars
- A: uppercase chars
- 1: digits
- !: special chars
- *: printable chars
Text character set:

- Pre-defined sets: printable, base32, base64
- Custom sets:
- a: lowercase chars
- A: uppercase chars
- 1: digits
- !: special chars
- \*: printable chars

Examples:
xortool file.bin
xortool -l 11 -c 20 file.bin
xortool -x -c ' ' file.hex
xortool -b -f -l 23 -t base64 message.enc
xortool -b -p "xctf{" message.enc
```

Example 1
---------------------
- `xortool file.bin`
- `xortool -l 11 -c 20 file.bin`
- `xortool -x -c ' ' file.hex`
- `xortool -b -f -l 23 -t base64 message.enc`
- `xortool -b -p "xctf{" message.enc`

## Example 1

```bash
# xor is xortool/xortool-xor
Expand Down Expand Up @@ -149,8 +153,7 @@ So, if automated decryption fails, you can calibrate:
- (`-l`) selected length to see some interesting keys
- (`-c`) the most frequent char to produce right plaintext

Example 2
---------------------
## Example 2

We are given a message in encoded in Base64 and XORed with an unknown key.

Expand Down Expand Up @@ -188,8 +191,7 @@ See files filename-key.csv, filename-char_used-perc_valid.csv

By filtering the outputs on the character set of Base64, we directly keep the only solution.

Information
---------------------
## Information

Author: hellman

Expand Down
66 changes: 0 additions & 66 deletions poetry.lock

This file was deleted.

31 changes: 17 additions & 14 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[tool.poetry]
[project]
name = "xortool"
version = "1.0.2"
version = "1.1.0"
description = "A tool to analyze multi-byte xor cipher"
authors = ["hellman"]
authors = [{ name = "hellman", email = "[email protected]" }]
license = "MIT"
readme = "README.md"
keywords = ["xor", "xortool", "cryptanalysis"]
Expand All @@ -11,22 +11,25 @@ classifiers = [
'Intended Audience :: Science/Research',
'Topic :: Security :: Cryptography',
]
requires-python = ">=3.6,<4.0"
dependencies = ["typer>=0.15.2", "rich>=14.0.0"]

[tool.poetry.scripts]
[project.scripts]
xortool = 'xortool.tool_main:main'
xortool-xor = 'xortool.tool_xor:main'

[tool.poetry.urls]
[project.urls]
homepage = "http://github.com/hellman/xortool"

[tool.poetry.dependencies]
python = ">=3.6,<4.0"
docopt = "^0.6.2"
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"

[tool.poetry.dev-dependencies]
docopt = "^0.6.2"
importlib_metadata = "^4.8"
[tool.rye]
managed = true
dev-dependencies = []
# dev-dependencies = ["importlib_metadata ^4.8"]

[build-system]
requires = ["poetry-core>=1.0.0a5"]
build-backend = "poetry.core.masonry.api"
# [build-system]
# requires = ["rye>=0.21.1"]
# build-backend = "rye.masonry.api"
29 changes: 29 additions & 0 deletions requirements-dev.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: false
# with-sources: false
# generate-hashes: false
# universal: false

-e file:.
click==8.1.8
# via typer
markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
pygments==2.19.1
# via rich
rich==14.0.0
# via typer
# via xortool
shellingham==1.5.4
# via typer
typer==0.15.2
# via xortool
typing-extensions==4.13.2
# via typer
29 changes: 29 additions & 0 deletions requirements.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: false
# with-sources: false
# generate-hashes: false
# universal: false

-e file:.
click==8.1.8
# via typer
markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
pygments==2.19.1
# via rich
rich==14.0.0
# via typer
# via xortool
shellingham==1.5.4
# via typer
typer==0.15.2
# via xortool
typing-extensions==4.13.2
# via typer
4 changes: 2 additions & 2 deletions xortool/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from importlib_metadata import version
from importlib.metadata import version

__version__ = version(__package__)
__version__ = version(__package__ or "xortool")
26 changes: 0 additions & 26 deletions xortool/args.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
from docopt import docopt

from xortool.charset import get_charset


class ArgError(Exception):
pass

Expand All @@ -29,24 +24,3 @@ def parse_int(i):
if i is None:
return None
return int(i)


def parse_parameters(doc, version):
p = docopt(doc, version=version)
p = {k.lstrip("-"): v for k, v in p.items()}
try:
return {
"brute_chars": bool(p["brute-chars"]),
"brute_printable": bool(p["brute-printable"]),
"filename": p["FILE"] if p["FILE"] else "-", # stdin by default
"filter_output": bool(p["filter-output"]),
"frequency_spread": 0, # to be removed
"input_is_hex": bool(p["hex"]),
"known_key_length": parse_int(p["key-length"]),
"max_key_length": parse_int(p["max-keylen"]),
"most_frequent_char": parse_char(p["char"]),
"text_charset": get_charset(p["text-charset"]),
"known_plain": p["known-plaintext"].encode() if p["known-plaintext"] else False,
}
except ValueError as err:
raise ArgError(str(err))
7 changes: 4 additions & 3 deletions xortool/charset.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import string
from typing import Optional


class CharsetError(Exception):
Expand All @@ -14,13 +15,13 @@ class CharsetError(Exception):
}

PREDEFINED_CHARSETS = {
"base32": CHARSETS["A"] + "234567=",
"base64": CHARSETS["a"] + CHARSETS["A"] + CHARSETS["1"] + "/+=",
"base32": CHARSETS["A"] + "234567=",
"base64": CHARSETS["a"] + CHARSETS["A"] + CHARSETS["1"] + "/+=",
"printable": CHARSETS["*"],
}


def get_charset(charset):
def get_charset(charset: Optional[str]):
charset = charset or "printable"
if charset in PREDEFINED_CHARSETS:
return PREDEFINED_CHARSETS[charset].encode("ascii")
Expand Down
Loading