Merge pull request #661 from Ciphey/bee-xor

Implement xortool in Ciphey and better testing
This commit is contained in:
Brandon 2021-07-03 09:05:24 +01:00 committed by GitHub
commit 5b4244fbae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 184 additions and 219 deletions

View File

@ -19,28 +19,29 @@ class What(Checker[str]):
def check(self, ctext: T) -> Optional[str]:
logging.debug("Trying PyWhat checker")
returned_regexes = self.id.identify(ctext, api=True)
if len(returned_regexes["Regexes"]) > 0:
matched_regex = returned_regexes["Regexes"][0]["Regex Pattern"]
returned_regexes = self.id.identify(ctext)
if returned_regexes["Regexes"]:
matched_regex = returned_regexes["Regexes"]['text'][0]["Regex Pattern"]
ret = f'The plaintext is a [yellow]{matched_regex["Name"]}[/yellow]'
human = f'\nI think the plaintext is a [yellow]{matched_regex["Name"]}[/yellow]'
human = (
f'\nI think the plaintext is a [yellow]{matched_regex["Name"]}[/yellow]'
)
if "Description" in matched_regex and matched_regex["Description"]:
s = matched_regex['Description']
s = matched_regex["Description"]
# lowercases first letter so it doesn't look weird
s = f", which is {s[0].lower() + s[1:]}\n"
ret += s
human += s
# if URL is attached, include that too.
if "URL" in matched_regex:
link = matched_regex['URL'] + ctext.replace(' ', '')
if "URL" in matched_regex and matched_regex["URL"]:
link = matched_regex["URL"] + ctext.replace(" ", "")
ret += f"\nClick here to view in browser [#CAE4F1][link={link}]{link}[/link][/#CAE4F1]\n"
# If greppable mode is on, don't print this
if self.config.verbosity > 0:
if self.config.verbosity >= 0:
# Print with full stop
console.print(human)
return ret

View File

@ -6,6 +6,5 @@ from . import (
rot47,
soundex,
vigenere,
xor_single,
xorcrypt,
xortool,
)

View File

@ -1,144 +0,0 @@
"""
© Brandon Skerritt
Github: brandonskerritt
"""
import base64
from typing import Dict, List, Optional
import cipheycore
import logging
from rich.logging import RichHandler
from ciphey.iface import Config, Cracker, CrackInfo, CrackResult, ParamSpec, registry
@registry.register
class XorCrypt(Cracker[bytes]):
def getInfo(self, ctext: bytes) -> CrackInfo:
if self.keysize is not None:
analysis = self.cache.get_or_update(
ctext,
f"xorcrypt::{self.keysize}",
lambda: cipheycore.analyse_string(ctext, self.keysize, self.group),
)
return CrackInfo(
success_likelihood=cipheycore.xorcrypt_detect(analysis, self.expected),
# TODO: actually calculate runtimes
success_runtime=1e-4,
failure_runtime=1e-4,
)
keysize = self.cache.get_or_update(
ctext,
"xorcrypt::likely_lens",
lambda: cipheycore.xorcrypt_guess_len(ctext),
)
if keysize == 1:
return CrackInfo(
success_likelihood=0,
# TODO: actually calculate runtimes
success_runtime=2e-3,
failure_runtime=2e-2,
)
return CrackInfo(
success_likelihood=0.9, # Dunno, but it's quite likely
# TODO: actually calculate runtimes
success_runtime=2e-3,
failure_runtime=2e-2,
)
@staticmethod
def getTarget() -> str:
return "xorcrypt"
def crackOne(
self, ctext: bytes, analysis: cipheycore.windowed_analysis_res
) -> List[CrackResult]:
possible_keys = cipheycore.xorcrypt_crack(analysis, self.expected, self.p_value)
logging.debug(
f"xorcrypt crack got keys: {[[i for i in candidate.key] for candidate in possible_keys]}"
)
return [
CrackResult(
value=cipheycore.xorcrypt_decrypt(ctext, candidate.key),
key_info="0x" + "".join(["{:02x}".format(i) for i in candidate.key]),
)
for candidate in possible_keys[: min(len(possible_keys), 10)]
]
def attemptCrack(self, ctext: bytes) -> List[CrackResult]:
logging.info(f"Trying xorcrypt cipher on {base64.b64encode(ctext)}")
# Analysis must be done here, where we know the case for the cache
if self.keysize is not None:
return self.crackOne(
ctext,
self.cache.get_or_update(
ctext,
f"xorcrypt::{self.keysize}",
lambda: cipheycore.analyse_bytes(ctext, self.keysize),
),
)
len = self.cache.get_or_update(
ctext,
"xorcrypt::likely_lens",
lambda: cipheycore.xorcrypt_guess_len(ctext),
)
logging.debug(f"Got possible length {len}")
if len < 2:
return []
ret = []
# Fuzz around
for i in range(min(len - 2, 2), len + 2):
ret += self.crackOne(
ctext,
self.cache.get_or_update(
ctext,
f"xorcrypt::{len}",
lambda: cipheycore.analyse_bytes(ctext, len),
),
)
return ret
@staticmethod
def getParams() -> Optional[Dict[str, ParamSpec]]:
return {
"expected": ParamSpec(
desc="The expected distribution of the plaintext",
req=False,
config_ref=["default_dist"],
),
"keysize": ParamSpec(
desc="A key size that should be used. If not given, will attempt to work it out",
req=False,
),
"p_value": ParamSpec(
desc="The p-value to use for windowed frequency analysis",
req=False,
default=0.001,
),
}
def __init__(self, config: Config):
super().__init__(config)
self.expected = config.get_resource(self._params()["expected"])
self.cache = config.cache
self.keysize = self._params().get("keysize")
if self.keysize is not None:
self.keysize = int(self.keysize)
self.p_value = self._params()["p_value"]
self.max_key_length = 16

View File

@ -5,68 +5,50 @@
© Brandon Skerritt
Github: brandonskerritt
Github: bee-san
"""
from typing import Dict, List, Optional
import cipheycore
import logging
from rich.logging import RichHandler
from xortool_ciphey import tool_main
from ciphey.iface import Config, Cracker, CrackInfo, CrackResult, ParamSpec, registry
@registry.register
class XorSingle(Cracker[bytes]):
class XorTool(Cracker[str]):
def getInfo(self, ctext: str) -> CrackInfo:
analysis = self.cache.get_or_update(
ctext,
"cipheycore::simple_analysis",
lambda: cipheycore.analyse_bytes(ctext),
)
return CrackInfo(
success_likelihood=cipheycore.xor_single_detect(analysis, self.expected),
success_likelihood=0.1,
# TODO: actually calculate runtimes
success_runtime=1e-5,
failure_runtime=1e-5,
success_runtime=1e-8,
failure_runtime=1e-8,
)
@staticmethod
def getTarget() -> str:
return "xor_single"
return "xortool"
def attemptCrack(self, ctext: bytes) -> List[CrackResult]:
logging.info("Trying xor single cipher")
# TODO: handle different alphabets
def attemptCrack(self, ctext: str) -> List[CrackResult]:
logging.debug("Trying xortool cipher")
# TODO handle different charsets
# TODO allow more config over xortool
logging.debug("Beginning cipheycore simple analysis")
logging.debug(f"{ctext}")
# Hand it off to the core
analysis = self.cache.get_or_update(
ctext,
"cipheycore::simple_analysis",
lambda: cipheycore.analyse_bytes(ctext),
)
logging.debug("Beginning cipheycore::xor_single")
possible_keys = cipheycore.xor_single_crack(
analysis, self.expected, self.p_value
)
# https://github.com/Ciphey/xortool/discussions/4
# for docs on this function
try:
result = tool_main.api(str.encode(ctext))
except:
logging.debug("Xor failed.")
return
n_candidates = len(possible_keys)
logging.info(f"XOR single returned {n_candidates} candidates")
result = CrackResult(value=result[1]["Dexored"], key_info=result[0]["keys"])
candidates = []
for candidate in possible_keys:
translated = cipheycore.xor_single_decrypt(ctext, candidate.key)
logging.debug(f"Candidate {candidate.key} has prob {candidate.p_value}")
candidates.append(CrackResult(value=translated, key_info=candidate.key))
logging.debug(f"{candidates}")
return candidates
return [result]
@staticmethod
def getParams() -> Optional[Dict[str, ParamSpec]]:
@ -80,8 +62,7 @@ class XorSingle(Cracker[bytes]):
desc="The p-value to use for standard frequency analysis",
req=False,
default=0.01,
)
# TODO: add "filter" param
),
}
@staticmethod

View File

@ -273,7 +273,9 @@ class AuSearch(Searcher):
# TODO Cyclic uses some tricky C++ here
# I know because it's sorted the one at the back (the anti-weight)
# is the most likely
edge: Edge = chunk.pop(-1)
# Expand the node
res = edge.route(edge.source.level.result.value)
if res is None:

View File

@ -309,6 +309,7 @@ class Searcher(ConfigurableModule):
def pretty_search_results(res: SearchResult, display_intermediate: bool = False) -> str:
# TODO what is display_intermediate
ret: str = ""
table = Table(show_header=False, box=box.ROUNDED, safe_box=False)
# Only print the checker if we need to. Normal people don't know what
@ -340,12 +341,14 @@ def pretty_search_results(res: SearchResult, display_intermediate: bool = False)
already_broken = True
if not already_broken:
out += "\n"
return out
return out, already_broken
# Skip the 'input' and print in order
already_broken = False
out = ""
for i in res.path[1:]:
out += add_one()
output, already_broken = add_one()
out += output
if out:
if len(out.split("\n")) > 1:
@ -358,8 +361,11 @@ def pretty_search_results(res: SearchResult, display_intermediate: bool = False)
ret = ret[:-1]
# If we didn't show intermediate steps, then print the final result
if not display_intermediate:
if already_broken:
ret += f"""\nPlaintext: [bold green]"{escape(res.path[-1].result.value)}"[bold green]"""
else:
ret += f"""Plaintext: [bold green]"{escape(res.path[-1].result.value)}"[bold green]"""
table.add_row(ret)
return table

View File

@ -136,7 +136,9 @@ class mathsHelper:
highest_key = key
logging.debug(f"Highest key is {highest_key}")
# removes the highest key from the prob table
logging.debug(f"Prob table is {prob_table} and highest key is {highest_key}")
logging.debug(
f"Prob table is {prob_table} and highest key is {highest_key}"
)
logging.debug(f"Removing {prob_table[highest_key]}")
del prob_table[highest_key]
logging.debug(f"Prob table after deletion is {prob_table}")

View File

@ -23,7 +23,8 @@ base91 = "^1.0.1"
pybase62 = "^0.4.3"
click = ">=7.1.2,<9.0.0"
mock = "^4.0.3"
pywhat = ">=0.2.5,<1.3.0"
pywhat = "3.0.0"
xortool-ciphey = "^0.1.16"
[tool.poetry.dev-dependencies]
pytest-cov = "^2.10.1"

View File

@ -0,0 +1,50 @@
import pytest
from click.testing import CliRunner
import mock
import re
from ciphey import decrypt
from ciphey.iface import Config
from ciphey.ciphey import main
from ciphey.basemods.Checkers import human
def test_xor():
res = decrypt(Config().library_default().complete_config(),"Uihr!hr!`!udru!gns!YNS-!hu!hr!sd`mmx!mnof!un!l`jd!rtsd!ui`u!YNSunnm!b`o!fdu!hu/!Bhqidx!*!YNSunnm!hr!bnnm/")
assert re.findall("This is a test for XOR", res)
@pytest.mark.skip("Skipping because it matches on Discover card, this is a PyWhat bug that's being fixed.")
@mock.patch("ciphey.basemods.Checkers.human.HumanChecker.check", return_value = "")
def test_xor_tui_multi_byte(mock_click):
# https://github.com/Ciphey/Ciphey/issues/655
runner = CliRunner()
mock_click.return_value = "y"
result = runner.invoke(main, ['-vvv', '-t', '360w0x11450x114504421611100x0y0545000x06171y1511070145150x110z45081709110y45071y1100423w2z3045120z0x060z450x1145080w170042060z0u1509071w45160w040x45160y0y020v0045001x1107453w2w374y422x0y1111000301450w03450w0y091y4510110x0y05450442160x0x02090745071y110042030z104504420v001y45120z0x060z450x11450003161x42110z42071717110042030z10060042041642110w071700420x16420z0y0v1x4550505342150z11160x000x090y11001149450u1009160x45001x1107450v071x1642060z170901420w04140045160w0z170416030y0111450z0445150w16160y070x0v0x110716450u040v0y0y02420x11420w04100145160z450017101600030w1706074y453z2z37160z0z0v450x1145041500160w08004207000104101100450y114501040y42061703060v42070z160w45110x0y05090042071x160045030y014208100v110x42071x1600453z2z3742000y01171x121100064511071w114x452z0x060042260x120w001y450w0316453z2z37160z0z0v450x0x1100051704160001420x0y160z450y1149420x1142120x0v0945000045111015071745030804180x0y0545040x0145150x090v451012021703010042260x120w001y45110w450707450400090042110z42061703060v42060z0u1509071w453z2z3742000y01171x121100064511071w114x45320z1x450y1645160w0x114511071w1142160z42090z0x025z4227000104101100453z2z37160z0z0v45060w1009060y4216450610040609450x1645120z000y422x450u040107450x1645160z0z171600174x455u4z'])
assert result.exit_code == 0
assert re.findall("This is a string encrypted with multi", str(result.output))
@mock.patch("ciphey.basemods.Checkers.human.HumanChecker.check", return_value = "")
def test_xor_tui(mock_click):
# https://github.com/Ciphey/Ciphey/issues/655
runner = CliRunner()
mock_click.return_value = "y"
result = runner.invoke(main, ['-t', 'Uihr!hr!`!udru!gns!YNS-!hu!hr!sd`mmx!mnof!un!l`jd!rtsd!ui`u!YNSunnm!b`o!fdu!hu/!Bhqidx!*!YNSunnm!hr!bnnm/'])
assert result.exit_code == 0
assert re.findall("This is a test for XOR", str(result.output))
@mock.patch("ciphey.basemods.Checkers.human.HumanChecker.check", return_value = "")
def test_xor_tui_verbose_mode_doesnt_break(mock_click):
# We had a bug where verbose mode broke xor
# https://discord.com/channels/754001738184392704/814565556027654214/853183178104373310
runner = CliRunner()
mock_click.return_value = "y"
result = runner.invoke(main, ['-v', '-t', 'Uihr!hr!`!udru!gns!YNS-!hu!hr!sd`mmx!mnof!un!l`jd!rtsd!ui`u!YNSunnm!b`o!fdu!hu/!Bhqidx!*!YNSunnm!hr!bnnm/'])
assert result.exit_code == 0
assert re.findall("This is a test for XOR", str(result.output))
def test_xor_atbash():
# Frsi!si!{!fwif!tmh!BMH-!sf!si!hw{nnc!nmlu!fm!o{qw!ighw!fr{f!BMHfmmn!y{l!uwf!sf/!Ysjrwc!*!BMHfmmn.si!ymmn/
# This is a test for XOR, it is really long to make sure that XORtool can get it. Ciphey + XORtool/is cool.
# Previously xor only worked on level 1, this test ensures it always works on levels > 1
res = decrypt(Config().library_default().complete_config(),"Frsi!si!{!fwif!tmh!BMH-!sf!si!hw{nnc!nmlu!fm!o{qw!ighw!fr{f!BMHfmmn!y{l!uwf!sf/!Ysjrwc!*!BMHfmmn.si!ymmn/")
assert re.findall("This is a test for XOR", res)

View File

@ -1,14 +1,28 @@
from click.testing import CliRunner
from ciphey.ciphey import main
from ciphey.basemods.Checkers import human
import mock
def test_hello_world():
runner = CliRunner()
result = runner.invoke(main, ['-g', '-t', 'hello'])
assert result.exit_code == 0
assert result.output == 'hello\n'
runner = CliRunner()
result = runner.invoke(main, ["-g", "-t", "hello"])
assert result.exit_code == 0
assert result.output == "hello\n"
def test_ip_address():
runner = CliRunner()
result = runner.invoke(main, ['-g', '-t', 'MTkyLjE2OC4wLjE='])
assert result.exit_code == 0
assert result.output == '192.168.0.1\n'
runner = CliRunner()
result = runner.invoke(main, ["-g", "-t", "MTkyLjE2OC4wLjE="])
assert result.exit_code == 0
assert result.output == "192.168.0.1\n"
@mock.patch("ciphey.basemods.Checkers.human.HumanChecker.check", return_value="")
def test_quick_visual_output(mock_click):
# https://github.com/Ciphey/Ciphey/issues/655
runner = CliRunner()
mock_click.return_value = "y"
result = runner.invoke(main, ["-t", "NB2HI4DTHIXS6Z3PN5TWYZJOMNXW2==="])
assert result.exit_code == 0
assert "base32" in result.output

View File

@ -1,16 +1,22 @@
from click.testing import CliRunner
from ciphey.ciphey import main
from ciphey.basemods.Checkers import human
import mock
import mock
@mock.patch("ciphey.basemods.Checkers.human.HumanChecker.check", return_value = "")
@mock.patch("ciphey.basemods.Checkers.human.HumanChecker.check", return_value="")
def test_fix_for_655(mock_click):
# https://github.com/Ciphey/Ciphey/issues/655
runner = CliRunner()
runner = CliRunner()
mock_click.return_value = "y"
result = runner.invoke(main, ['-t', 'NB2HI4DTHIXS6Z3PN5TWYZJOMNXW2==='])
result = runner.invoke(main, ["-t", "NB2HI4DTHIXS6Z3PN5TWYZJOMNXW2==="])
assert result.exit_code == 0
assert "base32" in result.output
# assert "base32" in result.output
"""
TODO Mock
360d0c11450c114504421611100c0b0545000c06171b1511070145150c110a45081709110b45071b1100423d2a3045120a0c060a450c1145080d170042060a0f1509071d45160d040c45160b0b020e0045001c1107453d2d374b422c0b1111000301450d03450d0b091b4510110c0b05450442160c0c02090745071b110042030a104504420e001b45120a0c060a450c11450003161c42110a42071717110042030a10060042041642110d071700420c16420a0b0e1c4550505342150a11160c000c090b11001149450f1009160c45001c1107450e071c1642060a170901420d04140045160d0a170416030b0111450a0445150d16160b070c0e0c110716450f040e0b0b02420c11420d04100145160a450017101600030d1706074b453a2a37160a0a0e450c1145041500160d08004207000104101100450b114501040b42061703060e42070a160d45110c0b05090042071c160045030b014208100e110c42071c1600453a2a3742000b01171c121100064511071d114c452a0c060042260c120d001b450d0316453a2a37160a0a0e450c0c1100051704160001420c0b160a450b1149420c1142120c0e0945000045111015071745030804180c0b0545040c0145150c090e451012021703010042260c120d001b45110d450707450400090042110a42061703060e42060a0f1509071d453a2a3742000b01171c121100064511071d114c45320a1c450b1645160d0c114511071d1142160a42090a0c025a4227000104101100453a2a37160a0a0e45060d1009060b4216450610040609450c1645120a000b422c450f040107450c1645160a0a171600174c455f4a
As it passes as a discover card
"""

View File

@ -112,7 +112,9 @@ def test_binary():
assert res == answer_str
@pytest.mark.skip("Can't decode base64 + caesar https://github.com/Ciphey/Ciphey/issues/606")
@pytest.mark.skip(
"Can't decode base64 + caesar https://github.com/Ciphey/Ciphey/issues/606"
)
def test_binary_base64_caesar():
res = decrypt(
Config().library_default().complete_config(),

43
tests/test_quick.py Normal file
View File

@ -0,0 +1,43 @@
import pytest
from ciphey import decrypt
from ciphey.iface import Config
from click.testing import CliRunner
from ciphey.ciphey import main
from ciphey.basemods.Checkers import human
import mock
answer_str = "Hello my name is bee and I like dog and apple and tree"
def test_quick_base32():
res = decrypt(
Config().library_default().complete_config(),
"JBSWY3DPEBWXSIDOMFWWKIDJOMQGEZLFEBQW4ZBAJEQGY2LLMUQGI33HEBQW4ZBAMFYHA3DFEBQW4ZBAORZGKZI=",
)
assert res.lower() == answer_str.lower()
def test_quick_base58_ripple():
res = decrypt(
Config().library_default().complete_config(),
"aqY64A1PhaM8hgyagyw4C1Mmp5cwxGEwag8EjVm9F6YHebyfPZmsvt65XxS7ffteQgTEGbHNT8",
)
assert res.lower() == answer_str.lower()
def test_quick_greppable_works_with_ip_address():
runner = CliRunner()
result = runner.invoke(main, ["-g", "-t", "MTkyLjE2OC4wLjE="])
assert result.exit_code == 0
assert result.output == "192.168.0.1\n"
@mock.patch("ciphey.basemods.Checkers.human.HumanChecker.check", return_value="")
def test_quick_visual_output(mock_click):
# https://github.com/Ciphey/Ciphey/issues/655
runner = CliRunner()
mock_click.return_value = "y"
result = runner.invoke(main, ["-t", "NB2HI4DTHIXS6Z3PN5TWYZJOMNXW2==="])
assert result.exit_code == 0
assert "base32" in result.output

View File

@ -11,6 +11,7 @@ def test_regex_ip():
)
assert res == "192.160.0.1"
def test_regex_domain():
res = decrypt(
Config().library_default().complete_config(),
@ -18,9 +19,10 @@ def test_regex_domain():
)
assert res == "https://google.com"
def test_regex_bitcoin():
res = decrypt(
Config().library_default().complete_config(),
"M0ZaYmdpMjljcGpxMkdqZHdWOGV5SHVKSm5rTHRrdFpjNQ==",
)
assert res == "3FZbgi29cpjq2GjdwV8eyHuJJnkLtktZc5"
assert res == "3FZbgi29cpjq2GjdwV8eyHuJJnkLtktZc5"