Skip to content

Commit 483643f

Browse files
committed
Added feature for computing codecs chain's strength
1 parent f4ff5c0 commit 483643f

10 files changed

Lines changed: 686 additions & 418 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
[![DOI](https://zenodo.org/badge/236679865.svg)](https://zenodo.org/badge/latestdoi/236679865)
1212
[![License](https://img.shields.io/pypi/l/codext.svg)](https://pypi.python.org/pypi/codext/)
1313

14-
[**CodExt**](https://github.com/dhondta/python-codext) is a library (Python 3.10 or later) that extends the native [`codecs`](https://docs.python.org/3/library/codecs) library (namely for adding new custom encodings and character mappings) and provides **120+ new codecs**, hence its name combining *CODecs EXTension*. It also features a **guess mode** for decoding multiple layers of encoding and **CLI tools** for convenience.
14+
[**CodExt**](https://github.com/dhondta/python-codext) is a library (Python 3.10 or later) that extends the native [`codecs`](https://docs.python.org/3/library/codecs) library (namely for adding new custom encodings and character mappings) and provides **150+ new codecs**, hence its name combining *CODecs EXTension*. It also features a **guess mode** for decoding multiple layers of encoding and **CLI tools** for convenience.
1515

1616
```sh
1717
$ pip install codext

‎docs/pages/cli.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ usage examples:
4747
- echo -en "test" | codext encode upper reverse base32 base64 morse
4848
- echo -en "test" | codext encode base64 gzip | codext guess
4949
- echo -en "test" | codext encode base64 gzip | codext guess gzip -c base
50+
- codext strength base64 gzip
51+
- echo -en "secret" | codext strength base64 rot-13
5052
```
5153

5254
!!! note "Input/output"

‎docs/pages/guessing.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,25 @@ This "textness" depends on the script of the letters. For Latin letters, it is m
181181

182182
This part shall still be improved with better features and/or using machine learning. In particular, the default stop function (`text`) accepts any printable output with a low entropy, hence possibly an intermediate result of a chain of encodings.
183183

184+
### Strength Evaluation
185+
186+
While the guess mode tries to *reverse* an unknown chain of codecs, the strength feature does the opposite: given a chain, it estimates how hard that chain is to reverse. It rates resistance to codext's own guessing heuristic and to casual recognition - not cryptographic strength.
187+
188+
It encodes a sample through the chain, then walks the true decoding path and, at each layer, uses the ranking heuristic (see above) to measure how high the correct codec is ranked. A layer ranked first is an obvious give-away ; a deeply-ranked or non-surfaced ("blind") layer, e.g. one whose codec opts out of guessing, cannot be greedily followed. It also flags an output that still looks like a text (a disguise) and a lossy chain (e.g. a hash) whose plaintext cannot be recovered at all.
189+
190+
The result is a score from 0 (trivially reversible) to 100 (resists the guesser) with a letter grade, the per-layer ranks and notes pointing at the weakest layer:
191+
192+
```python
193+
>>> codext.strength(["base64"])
194+
{'chain': ['base64'], 'reversible': True, 'length': 1, 'score': 12, 'grade': 'F', 'blind_layers': 0,
195+
'worst_rank': 1, 'disguised': False, 'correct_ranks': [1],
196+
'notes': ['every layer is ranked first by the guesser: trivially peelable']}
197+
>>> codext.strength(["gzip", "xor-173", "base64"])["grade"]
198+
'C'
199+
```
200+
201+
A compatible sample is chosen automatically ; a specific one can be given with the `sample` keyword-argument. The `include`/`exclude` arguments scope the codecs the guesser may use, exactly like for the guess mode. With the [CLI tool](cli.md), the chain is given as arguments and the sample is read from stdin or a file (a default is used otherwise): `codext strength base64 gzip`.
202+
203+
!!! note "Not cryptographic strength"
204+
205+
This measures resistance to codext's guessing/ranking and to casual inspection, not cryptographic security. The per-layer ranking is used on purpose instead of the full recursive guess, which is combinatorially expensive on some chains.

‎src/codext/VERSION.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.0.0
1+
2.0.1

‎src/codext/__common__.py‎

Lines changed: 3 additions & 415 deletions
Large diffs are not rendered by default.

‎src/codext/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
44
"""
55
from .__common__ import *
6+
from .guess import * # the guess/rank/stopfunc feature (wires codecs.guess and codecs.rank on import)
7+
from .strength import * # the strength feature (wires codecs.strength on import)
68
from .__info__ import __author__, __copyright__, __email__, __license__, __source__, __version__
79

810

‎src/codext/__main__.py‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import sys
77

88
from .__common__ import *
9+
from .guess import stopfunc
910
from .__info__ import __author__, __copyright__, __email__, __license__, __source__, __version__
1011

1112

@@ -151,6 +152,14 @@ def _format_action_invocation(self, action):
151152
help="categories, codecs and encodings to be explicitely used ;\n "
152153
"format: [category|codec|encoding] OR depth:[category|codec|encoding]")
153154
rank.add_argument("-l", "--limit", type=int, default=10, help="limit the number of displayed results")
155+
strength = sparsers.add_parser("strength", help="evaluate the strength of a chain of codecs", **kw)
156+
strength.add_argument("encoding", nargs="+", help="list of encodings forming the chain to evaluate")
157+
strength.add_argument("-e", "--exclude", nargs="*", action="extend", metavar="CAT|COD|ENC",
158+
help="categories, codecs and encodings the guessing adversary may not use")
159+
strength.add_argument("-f", "--stop-function", default="text", metavar="FUNC",
160+
help="stop function for the guessing adversary (default: text)")
161+
strength.add_argument("-i", "--include", nargs="*", action="extend", metavar="CAT|COD|ENC",
162+
help="categories, codecs and encodings the guessing adversary may use")
154163
search = sparsers.add_parser("search", help="search for codecs")
155164
search.add_argument("pattern", nargs="+", help="encoding pattern to search")
156165
listi = sparsers.add_parser("list", help="list items")
@@ -164,7 +173,7 @@ def _format_action_invocation(self, action):
164173
remm = sparsers.add_parser("remove-macro", help="remove a macro from the registry")
165174
remm.add_argument("name", help="macro's name")
166175
args = parser.parse_args()
167-
if args.command in ["guess", "rank"]:
176+
if args.command in ["guess", "rank", "strength"]:
168177
args.include, args.exclude = __format_list(args.include), __format_list(args.exclude, False)
169178
try:
170179
# if a search pattern is given, only handle it
@@ -181,6 +190,19 @@ def _format_action_invocation(self, action):
181190
elif args.command == "remove-macro":
182191
remove_macro(args.name)
183192
return 0
193+
# evaluate the strength of a chain of codecs (sample from a file, from a pipe, or a default one)
194+
elif args.command == "strength":
195+
sample = None
196+
if args.infile:
197+
sample = _input(args.infile)
198+
elif not sys.stdin.isatty():
199+
sample = _input(None)
200+
if sample is not None:
201+
sample = (sample.rstrip("\r\n") if isinstance(sample, str) else sample.rstrip(b"\r\n")) or None
202+
print(codecs.strength_report(args.encoding, sample=sample,
203+
stop_func=getattr(stopfunc, args.stop_function, stopfunc.default),
204+
include=args.include, exclude=args.exclude))
205+
return 0
184206
# list encodings or macros
185207
elif args.command == "list":
186208
if args.type == "encodings":

0 commit comments

Comments
 (0)