Skip to content

Commit

Permalink
🗓 Apr 16, 2024 8:25:44 PM
Browse files Browse the repository at this point in the history
🧪 tests added/updated
✨ to/from base45
✨ to/from base92
📔 docs added/updated
  • Loading branch information
securisec committed Apr 17, 2024
1 parent d35cf9f commit b49356e
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 7 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/tests_multi_os.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ jobs:
- "3.12"

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
Expand Down Expand Up @@ -106,9 +106,9 @@ jobs:
needs: test
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: setup
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.7"
- name: build
Expand Down
2 changes: 1 addition & 1 deletion chepy/chepy_plugins
46 changes: 45 additions & 1 deletion chepy/modules/dataformat.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
Rotate,
Uint1Array,
UUEncoderDecoder,
Base92,
Base45,
)

yaml = lazy_import.lazy_module("yaml")
Expand Down Expand Up @@ -340,6 +342,46 @@ def from_base32(self, remove_whitespace: bool = True) -> DataFormatT:
self.state = base64.b32decode(self.state)
return self

@ChepyDecorators.call_stack
def to_base92(self) -> DataFormatT:
"""Encode to Base92
Returns:
Chepy: The Chepy object.
"""
self.state = Base92.b92encode(self._convert_to_bytes())
return self

@ChepyDecorators.call_stack
def from_base92(self) -> DataFormatT:
"""Decode from Base92
Returns:
Chepy: The Chepy object.
"""
self.state = Base92.b92decode(self._convert_to_str())
return self

@ChepyDecorators.call_stack
def to_base45(self) -> DataFormatT:
"""Encode to Base45
Returns:
Chepy: The Chepy object.
"""
self.state = Base45().b45encode(self._convert_to_bytes())
return self

@ChepyDecorators.call_stack
def from_base45(self) -> DataFormatT:
"""Decode from Base45
Returns:
Chepy: The Chepy object.
"""
self.state = Base45().b45decode(self._convert_to_bytes())
return self

@ChepyDecorators.call_stack
def to_base91(self) -> DataFormatT: # pragma: no cover
"""Base91 encode
Expand Down Expand Up @@ -490,7 +532,9 @@ def to_base64(self, custom: str = None, url_safe: bool = False) -> DataFormatT:
return self

@ChepyDecorators.call_stack
def from_base64(self, custom: str = None, url_safe: bool = False, remove_whitespace: bool = True) -> DataFormatT:
def from_base64(
self, custom: str = None, url_safe: bool = False, remove_whitespace: bool = True
) -> DataFormatT:
"""Decode as Base64
Base64 is a notation for encoding arbitrary byte data using a
Expand Down
4 changes: 4 additions & 0 deletions chepy/modules/dataformat.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ class DataFormat(ChepyCore):
def to_leetcode(self: DataFormatT, replace_space: str=...) -> DataFormatT: ...
def substitute(self: DataFormatT, x: str=..., y: str=...) -> DataFormatT: ...
def remove_nonprintable(self: DataFormatT, replace_with: Union[str, bytes] = ...): ...
def to_base92(self: DataFormatT) -> DataFormatT: ...
def from_base92(self: DataFormatT) -> DataFormatT: ...
def to_base45(self: DataFormatT) -> DataFormatT: ...
def from_base45(self: DataFormatT) -> DataFormatT: ...
def to_base91(self: DataFormatT) -> DataFormatT: ...
def from_base91(self: DataFormatT) -> DataFormatT: ...
def swap_endianness(self: DataFormatT, word_length: int=...) -> DataFormatT: ...
Expand Down
142 changes: 141 additions & 1 deletion chepy/modules/internal/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,146 @@
import binascii


class Base45:
# reference: https://github.com/kirei/python-base45/blob/main/base45/__init__.py
def __init__(self) -> None:
self.BASE45_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"
self.BASE45_DICT = {v: i for i, v in enumerate(self.BASE45_CHARSET)}

def b45encode(self, buf: bytes) -> bytes:
"""Convert bytes to base45-encoded string"""
res = ""
buflen = len(buf)
for i in range(0, buflen & ~1, 2):
x = (buf[i] << 8) + buf[i + 1]
e, x = divmod(x, 45 * 45)
d, c = divmod(x, 45)
res += (
self.BASE45_CHARSET[c] + self.BASE45_CHARSET[d] + self.BASE45_CHARSET[e]
)
if buflen & 1:
d, c = divmod(buf[-1], 45)
res += self.BASE45_CHARSET[c] + self.BASE45_CHARSET[d]
return res.encode()

def b45decode(self, s: Union[bytes, str]) -> bytes:
"""Decode base45-encoded string to bytes"""
try:
if isinstance(s, str): # pragma: no cover
buf = [self.BASE45_DICT[c] for c in s.rstrip("\n")]
elif isinstance(s, bytes):
buf = [self.BASE45_DICT[c] for c in s.decode()]
else: # pragma: no cover
raise TypeError("Type must be 'str' or 'bytes'")

buflen = len(buf)
if buflen % 3 == 1: # pragma: no cover
raise ValueError("Invalid base45 string")

res = []
for i in range(0, buflen, 3):
if buflen - i >= 3:
x = buf[i] + buf[i + 1] * 45 + buf[i + 2] * 45 * 45
if x > 0xFFFF: # pragma: no cover
raise ValueError
res.extend(divmod(x, 256))
else:
x = buf[i] + buf[i + 1] * 45
if x > 0xFF: # pragma: no cover
raise ValueError
res.append(x)
return bytes(res)
except (ValueError, KeyError, AttributeError): # pragma: no cover
raise ValueError("Invalid base45 string")


class Base92(object):
"""
Reference: https://github.com/Gu-f/py3base92/tree/master
"""

CHARACTER_SET = r"!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_abcdefghijklmnopqrstuvwxyz{|}"

@classmethod
def base92_chr(cls, val):
if val < 0 or val >= 91: # pragma: no cover
raise ValueError("val must be in [0, 91)")
if val == 0:
return "!" # pragma: no cover
elif val <= 61:
return chr(ord("#") + val - 1)
else:
return chr(ord("a") + val - 62)

@classmethod
def base92_ord(cls, val):
num = ord(val)
if val == "!":
return 0 # pragma: no cover
elif ord("#") <= num and num <= ord("_"):
return num - ord("#") + 1
elif ord("a") <= num and num <= ord("}"):
return num - ord("a") + 62
else: # pragma: no cover
raise ValueError("val is not a base92 character")

@classmethod
def b92encode(cls, byt: bytes) -> str:
if not isinstance(byt, bytes): # pragma: no cover
raise TypeError(f"a bytes-like object is required, not '{type(byt)}'")
if not byt:
return "~"
if not isinstance(byt, str):
byt = "".join([chr(b) for b in byt])
bitstr = ""
while len(bitstr) < 13 and byt:
bitstr += "{:08b}".format(ord(byt[0]))
byt = byt[1:]
resstr = ""
while len(bitstr) > 13 or byt:
i = int(bitstr[:13], 2)
resstr += cls.base92_chr(i // 91)
resstr += cls.base92_chr(i % 91)
bitstr = bitstr[13:]
while len(bitstr) < 13 and byt:
bitstr += "{:08b}".format(ord(byt[0]))
byt = byt[1:]

if bitstr:
if len(bitstr) < 7:
bitstr += "0" * (6 - len(bitstr))
resstr += cls.base92_chr(int(bitstr, 2))
else: # pragma: no cover
bitstr += "0" * (13 - len(bitstr))
i = int(bitstr, 2)
resstr += cls.base92_chr(i // 91)
resstr += cls.base92_chr(i % 91)
return resstr

@classmethod
def b92decode(cls, bstr: str) -> bytes:
if not isinstance(bstr, str): # pragma: no cover
raise TypeError(f"a str object is required, not '{type(bstr)}'")
bitstr = ""
resstr = ""
if bstr == "~":
return "".encode(encoding="latin-1")

for i in range(len(bstr) // 2):
x = cls.base92_ord(bstr[2 * i]) * 91 + cls.base92_ord(bstr[2 * i + 1])
bitstr += "{:013b}".format(x)
while 8 <= len(bitstr):
resstr += chr(int(bitstr[0:8], 2))
bitstr = bitstr[8:]
if len(bstr) % 2 == 1:
x = cls.base92_ord(bstr[-1])
bitstr += "{:06b}".format(x)
while 8 <= len(bitstr):
resstr += chr(int(bitstr[0:8], 2))
bitstr = bitstr[8:]
return resstr.encode(encoding="latin-1")


class LZ77Compressor:
"""
Class containing compress and decompress methods using LZ77 compression algorithm.
Expand Down Expand Up @@ -168,7 +308,7 @@ def detect_delimiter(
"""
if default_delimiter:
return default_delimiter

is_bytes = False
if isinstance(data, bytes): # pragma: no cover
delimiters = [d.encode() for d in delimiters]
Expand Down
22 changes: 22 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,28 @@ print(c)

# CTF

#### Incognito 5.0 - Marathon
```py
from chepy import Chepy

data = '3430203633203639203538203664203763203334203731203366203362203562203731203262203332203366203231203366203564203662203362203365203538203635203330203435203634203739203239203236203530203634203566203363203333203331203532203564203530203265203634203239203533203632203539203535203763203634203632203462203362203533203731203261203733203333203563203430203436203435203461203238203262203466203733203434203539203533203634203663203536203331203334203233203539203565203233203365203537203535203634203438203261203362203635203336203363203634203464203664203666203436203332203236203431203238203436203338203737203533203363203263203461203335203737203239203535203663203463203665203233203333203732203361203236203363203364203436203764203331203731203233203331203530203562203538203439203532203438203366203539203330203632203331203738203261203530203361203231203631203734203536203537203535203637203362203264203531203365203533203633203334203430203733203762203565203639203263203365203439203737203430203236203736203331203639203733203330203665203334203436203536203363203239203337203631203633203339203566203439203265203265203764203432'
c = (
Chepy(data)
.from_hex()
.from_hex()
.from_base92()
.from_base64()
.from_base62()
.from_base58()
.from_base45()
.from_base32()
.rot_13()
)

print(c)
ictf{D3c0d3_4ll_7h3_W@y}
```

#### HTB Business 2022 - Perseverence
```py
import base64
Expand Down
13 changes: 13 additions & 0 deletions tests/test_dataformat.py
Original file line number Diff line number Diff line change
Expand Up @@ -770,3 +770,16 @@ def test_rison():
data = {"b": True, "c": {"d": [1, 2]}}
assert Chepy(data).to_rison().o == b"(b:!t,c:(d:!(1,2)))"
assert Chepy("(b:!t,c:(d:!(1,2)))").from_rison().o == data


def test_base92():
data = "hello"
assert Chepy(data).to_base92().o == b"Fc_$aOB"
assert Chepy("").to_base92().o == b"~"
assert Chepy(b"Fc_$aOB").from_base92().o == b"hello"
assert Chepy("~").from_base92().o == b""


def test_base45():
assert Chepy("+8D VDL2").from_base45().o == b"hello"
assert Chepy("hello").to_base45().o == b"+8D VDL2"

0 comments on commit b49356e

Please sign in to comment.