text-deobfuscate 0.1.0a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. text_deobfuscate-0.1.0a1/LICENSE +21 -0
  2. text_deobfuscate-0.1.0a1/PKG-INFO +257 -0
  3. text_deobfuscate-0.1.0a1/README.md +227 -0
  4. text_deobfuscate-0.1.0a1/pyproject.toml +52 -0
  5. text_deobfuscate-0.1.0a1/pyproject.toml.orig +41 -0
  6. text_deobfuscate-0.1.0a1/src/text_deobfuscate/__init__.py +13 -0
  7. text_deobfuscate-0.1.0a1/src/text_deobfuscate/__main__.py +5 -0
  8. text_deobfuscate-0.1.0a1/src/text_deobfuscate/cli.py +30 -0
  9. text_deobfuscate-0.1.0a1/src/text_deobfuscate/core.py +21 -0
  10. text_deobfuscate-0.1.0a1/src/text_deobfuscate/data/NOTICE.md +10 -0
  11. text_deobfuscate-0.1.0a1/src/text_deobfuscate/data/SCOWL-COPYRIGHT.txt +248 -0
  12. text_deobfuscate-0.1.0a1/src/text_deobfuscate/data/lookalikes.txt +597 -0
  13. text_deobfuscate-0.1.0a1/src/text_deobfuscate/data/ngrams.txt +33813 -0
  14. text_deobfuscate-0.1.0a1/src/text_deobfuscate/data/scripts.txt +999 -0
  15. text_deobfuscate-0.1.0a1/src/text_deobfuscate/data/words.txt +92459 -0
  16. text_deobfuscate-0.1.0a1/src/text_deobfuscate/engine.py +196 -0
  17. text_deobfuscate-0.1.0a1/src/text_deobfuscate/protect.py +42 -0
  18. text_deobfuscate-0.1.0a1/src/text_deobfuscate/py.typed +0 -0
  19. text_deobfuscate-0.1.0a1/src/text_deobfuscate/resources.py +39 -0
  20. text_deobfuscate-0.1.0a1/src/text_deobfuscate/scorer.py +90 -0
  21. text_deobfuscate-0.1.0a1/src/text_deobfuscate/scripts.py +23 -0
  22. text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/__init__.py +57 -0
  23. text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/base.py +52 -0
  24. text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/base64.py +61 -0
  25. text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/bytes.py +58 -0
  26. text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/caesar.py +203 -0
  27. text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/emoji.py +59 -0
  28. text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/escapes.py +121 -0
  29. text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/invisible.py +70 -0
  30. text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/leet.py +415 -0
  31. text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/lookalike.py +298 -0
  32. text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/mail.py +107 -0
  33. text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/marks.py +87 -0
  34. text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/misspelling.py +113 -0
  35. text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/morse.py +87 -0
  36. text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/repeated.py +73 -0
  37. text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/reverse.py +127 -0
  38. text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/runs.py +55 -0
  39. text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/separator.py +145 -0
  40. text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/shorthand.py +99 -0
  41. text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/styled.py +90 -0
  42. text_deobfuscate-0.1.0a1/src/text_deobfuscate/types.py +33 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lapid Harel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,257 @@
1
+ Metadata-Version: 2.4
2
+ Name: text-deobfuscate
3
+ Version: 0.1.0a1
4
+ Summary: Undo text obfuscation (encodings and visual tricks) and report which transforms were applied.
5
+ Keywords: deobfuscation,text,leetspeak,homoglyph,phishing,unicode,normalization,security
6
+ Author: Lapid Harel
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Information Technology
12
+ Classifier: Natural Language :: English
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Security
21
+ Classifier: Typing :: Typed
22
+ Classifier: Topic :: Text Processing
23
+ Requires-Python: >=3.10
24
+ Project-URL: Homepage, https://github.com/lapid/text-deobfuscate
25
+ Project-URL: Repository, https://github.com/lapid/text-deobfuscate
26
+ Project-URL: Issues, https://github.com/lapid/text-deobfuscate/issues
27
+ Project-URL: Changelog, https://github.com/lapid/text-deobfuscate/blob/master/CHANGELOG.md
28
+ Project-URL: Documentation, https://github.com/lapid/text-deobfuscate/blob/master/docs/architecture.md
29
+ Description-Content-Type: text/markdown
30
+
31
+ # text-deobfuscate
32
+
33
+ Takes a text and returns the deobfuscated text, plus a record of which transforms were applied
34
+ and where.
35
+
36
+ ```python
37
+ >>> from text_deobfuscate import deobfuscate
38
+ >>> deobfuscate("Please confirm your account").text
39
+ 'Please confirm your account'
40
+ >>> deobfuscate("Cl1ck h3r3 to cla1m your pr1ze", strictness="balanced").text
41
+ 'Click here to claim your prize'
42
+ ```
43
+
44
+ **Status: version 0.1.0a1, an alpha.** The library and the command work,
45
+ thirty-two transforms are implemented and measured, and the numbers below say how far to trust
46
+ them: they are mostly from samples our own generators produced, so read "How well it works"
47
+ before relying on it.
48
+
49
+ ## Installing
50
+
51
+ From PyPI. The version is an alpha, so pip needs `--pre`:
52
+
53
+ ```sh
54
+ pip install --pre text-deobfuscate # or: uv add --prerelease allow text-deobfuscate
55
+ ```
56
+
57
+ Or from a clone, to work on it:
58
+
59
+ ```sh
60
+ git clone https://github.com/lapid/text-deobfuscate && cd text-deobfuscate
61
+ uv sync # or: pip install .
62
+ ```
63
+
64
+ It needs **Python 3.10 or newer** and has **no dependencies**.
65
+
66
+ ## Using it
67
+
68
+ As a library. `deobfuscate()` returns the text and an ordered list of steps; each step says what
69
+ changed, where it sat in the text as it stood before that step, and how confident the engine was:
70
+
71
+ ```python
72
+ from text_deobfuscate import deobfuscate
73
+
74
+ result = deobfuscate("Please confirm your acc​ount")
75
+ result.text # 'Please confirm your account'
76
+ [step.transform for step in result.steps] # ['zero-width', 'html-entity']
77
+ result.steps[0].start, result.steps[0].end # 25, 33
78
+ ```
79
+
80
+ As a command, with `--json` for the whole record:
81
+
82
+ ```sh
83
+ $ text-deobfuscate "UGxlYXNlIHNlbmQgdGhlIHJlcG9ydCBiZWZvcmUgRnJpZGF5"
84
+ Please send the report before Friday
85
+
86
+ $ text-deobfuscate --strictness balanced "c o n f i r m y o u r a c c o u n t"
87
+ confirm your account
88
+
89
+ $ text-deobfuscate --json "Thx 4 ur msg" # text plus steps, as JSON
90
+ ```
91
+
92
+ It reads standard input when given no argument, so it pipes. Piped or redirected, it writes the
93
+ text and nothing else — no trailing newline is added, so `text-deobfuscate x > file` holds exactly the
94
+ text. A terminal gets a line ending, so the prompt does not land mid-sentence.
95
+
96
+ ### What is public
97
+
98
+ `deobfuscate`, `Result`, `Step`, `STRICTNESS_LEVELS` and `__version__`, and the `text-deobfuscate`
99
+ command. Everything else in the package (`engine`, `scorer`, `transforms`, `protect`, `scripts` and
100
+ the rest) is internal and may change in any release, even though Python lets you import it. The
101
+ package ships type information (`py.typed`); the public names check cleanly with mypy, and the
102
+ internals still have some warnings.
103
+
104
+ ## Strictness
105
+
106
+ Three levels, because the tricks differ in how certain they are. `conservative` is the default.
107
+
108
+ | Level | Adds | Example |
109
+ |---|---|---|
110
+ | `conservative` | Encodings and escapes, invisible characters, styled and fullwidth letters, ciphers, reversal, Morse — things ordinary English does not contain | `UGxlYXNl…`, `e`, `\141\142`, `ⓒⓞⓝⓕⓘⓡⓜ` |
111
+ | `balanced` | Ambiguous visual tricks: lookalike letters, added accents, overlaid marks, leetspeak, inserted separators | `раssword` with Cyrillic letters, `Cl1ck`, `c o n f i r m` |
112
+ | `aggressive` | A lower acceptance margin, plus informal writing from short lists: stretched words, texting shorthand, common misspellings, emoji for a word | `sooooo`, `l8r`, `recieve`, `📞 me` |
113
+
114
+ Below `aggressive`, informal writing comes back exactly as written.
115
+
116
+ ## How it decides
117
+
118
+ **Changing clean text is treated as the worst mistake the tool can make.** Transforms only
119
+ *propose* rewrites; the engine applies one only when the result scores as more plausible English
120
+ than what it replaces, judged by a word list and a character four-gram table. Layered
121
+ obfuscation is decoded first and judged by what it finally turns into. URLs, paths, hashes and
122
+ other things that must not be touched are shielded. Nothing is ever executed — only decoded,
123
+ within size and depth limits.
124
+
125
+ Details in [`docs/architecture.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/architecture.md) and [`docs/scorer.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/scorer.md).
126
+
127
+ ## How well it works
128
+
129
+ **Read this first.** Most samples were made by our own obfuscation generators, so they test the
130
+ decoders against the people who wrote them. The independent evidence is smaller: 3,000 real
131
+ phishing sentences, 25 sentences disguised by hand, and sets made by someone else's generator
132
+ (below). The development sets have been run and tuned against many times, and the test split was
133
+ used up in October 2026. Treat every figure as an upper bound for text you have not seen.
134
+
135
+ Recovery means the text came back exactly, ignoring capital letters; clean texts changed is
136
+ measured strictly. The tool is judged first on **not changing clean text**.
137
+
138
+ **Development sets, thirty-two transforms (2026-10-29):** 7,850 clean texts, about 9,500
139
+ obfuscated ones, and the 3,000 real phishing sentences (kept outside the repository).
140
+
141
+ | Level | Clean texts changed | Recovered | Made worse | Real phishing | Hand-disguised |
142
+ |---|---|---|---|---|---|
143
+ | `conservative` | 0.01% (1 of 7,850) | 89.3% (5,273 of 5,905) | 1.2% | — | — |
144
+ | `balanced` | 0.01% (1 of 7,850) | 88.1% (10,292 of 11,680) | 0.6% | 90.8% | 15 of 25 |
145
+ | `aggressive` | 0.28% (22 of 7,850) | 86.8% (10,864 of 12,520) | 0.7% | 94.4% | 17 of 25 |
146
+
147
+ Each level is scored on the samples it is meant to undo, so the denominators differ and the rows
148
+ are not comparable with each other. Adding transforms or harder samples lowers a total without
149
+ anything getting worse; judge a trick by its own row in
150
+ [`docs/evaluation.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/evaluation.md).
151
+
152
+ **Sets made by someone else's generator, scored at `balanced`.** The first is 6,000 rows kept
153
+ outside the repository; we tuned against it, so it is optimistic. The second is 3,000 fresh rows
154
+ from unused text of the same source and generator: new rows, but the same style of obfuscation.
155
+ Neither tests a different author's habits.
156
+
157
+ | Trick | Tuned set | Fresh rows |
158
+ |---|---|---|
159
+ | base64, ROT13, zero-width, spacing | 99–100% | 100% |
160
+ | fullwidth, reversal | 96–99% | 98.5–99.6% |
161
+ | homoglyph | 96.5% | 97.8% |
162
+ | a symbol between every character | 99.8% | 99.3% |
163
+ | leetspeak | 59.7% | 57.5% |
164
+ | alternating capitals | 0% (99.8% ignoring capitals) | 0% (100%) |
165
+ | dropped vowels | 0% | 0% |
166
+ | **all rows** | **77.3%** | **77.5%** |
167
+
168
+ Alternating capitals come back in lower case: the tool does not restore capitals. Dropped
169
+ vowels are informal writing, out of scope by design.
170
+
171
+ **Where it does worst** (development sets, `balanced`): a disguised sentence inside plain text is
172
+ the weakest case. Reverse in context recovers 2.5%, ROT47 15%, per-word reversal 17.5%, Atbash
173
+ 45%, Caesar 58%, ROT13 70%. Leetspeak with every letter replaced recovers 66% of whole texts;
174
+ accent substitution 62–71%. Encodings that fill a whole text (base64, hex, base32, escapes) are
175
+ above 90%.
176
+
177
+ **Time:** the median text takes about 2 ms and the 95th percentile about 15 ms. The slowest
178
+ inputs are long and hostile: 100,000 random printable characters take about ten seconds at
179
+ `aggressive` (six at `balanced`), and everything else tested at that size takes under 5. One call
180
+ scans at most one million characters in all.
181
+
182
+ **The test split** was run once, on 2026-10-12, for twenty-three transforms. It showed clean
183
+ texts changed at 3, 13 and 38 of 7,442 for the three levels. Reading the 13 at `balanced` on
184
+ 2026-10-27 led to fixes for leetspeak on names and numbers (`P-51's`, `S4C`, `tty1`), and 5
185
+ remain, so the split no longer measures anything independently.
186
+
187
+ ## Known limits
188
+
189
+ Honest list; each is measured and logged.
190
+
191
+ - **Clean texts changed was higher on test than on dev** (13 against 1 at `balanced`). Reading the
192
+ 13 showed leetspeak applied to names and numbers; that is fixed, and 5 remain, which are
193
+ arguably correct changes. The unseen text of a real user may still be changed in ways our
194
+ samples do not show: report it.
195
+ - **Hand-disguised sentences did not carry over** from dev (15 of 25) to test (5 of 22). Human
196
+ disguises vary more than generated ones.
197
+ - **Most of the evidence comes from generators we wrote or tuned against.** See the top of
198
+ "How well it works".
199
+ - **Leetspeak** is about 58% exact on someone else's generator. Lone `1` and `4` in lightly
200
+ disguised text need a second disguised word nearby, and the wider table (`8`, `9`, `2`) is weak.
201
+ - **Time and size:** inputs over 100,000 characters are returned unchanged, and one call stops
202
+ scanning after a fixed budget, so a very long text may be only partly decoded. The slowest
203
+ input found, 100,000 random printable characters, takes about ten seconds at `aggressive` and
204
+ six at `balanced`; 30,000 take about a second. If you pass untrusted text to it in a service,
205
+ cap its length (a few thousand characters covers messages and emails) and use a timeout.
206
+ - **A text reversed word by word cannot be told from a wholly reversed one** unless a full stop
207
+ settles it, so 51 of 210 such samples come back with the words in the wrong order. 44 of those
208
+ are still perfectly legible; this is a limit of the ambiguity, not a bug to fix.
209
+ - **A lone disguised word** with everyday accents, or a lone Greek "a", is left alone at
210
+ `balanced`: one word is not enough evidence.
211
+ - **A capital hidden behind a leetspeak digit is lost** (`Cl1ck` → `click`). Recovery is measured
212
+ ignoring capitals for this reason.
213
+ - **Shifted or reversed words inside otherwise clean text** recover poorly.
214
+ - **Text in another language** passes through unchanged in nearly every case: 75 short phrases in
215
+ six languages come back untouched at every level. A short phrase used to be reversed ("No pasa
216
+ nada" became "adan asap oN"); texts of fewer than four words and sixteen letters are no longer
217
+ reversed as a whole, which also means a very short reversed text is not recovered. A longer
218
+ foreign text that reads as English backwards would still be reversed.
219
+ - **At `aggressive`, accents are removed from other words** ("Zoë" may become "Zoe"). Normal English
220
+ spellings with accents (`naïve`, `café`, `résumé`, `jalapeño` and about eighty more) are kept.
221
+ - **No recogniser yet for key-like tokens or code**, so a ticket code in base32 may be decoded.
222
+ - **English only.** Other languages and scripts pass through unchanged, by design.
223
+ - **Deferred to version 2:** bidirectional controls, variation-selector payloads, Punycode, part
224
+ of Unicode's invisible characters, and some styled ranges. Samples for them exist and are
225
+ excluded from a normal run.
226
+ - **Wrong-encoding damage** (`é` for `é`) is out of scope and comes back unchanged.
227
+ - Code obfuscation is out of scope.
228
+
229
+ ## Testing and the regression gate
230
+
231
+ ```sh
232
+ uv run python -m unittest discover -s tests # 125 unit tests
233
+ uv run python -m evaluation.gate # the regression gate: pass or fail
234
+ uv run python -m evaluation.run --detail # the full report, per category and per trick
235
+ ```
236
+
237
+ The gate scores the committed sets at all three levels and fails if clean texts changed rises, if
238
+ any trick recovers fewer samples than before, or if the output stops settling when the tool is run
239
+ over its own output. A deliberate trade-off is made by passing `--update` in the same commit and
240
+ giving the reason in the commit message. More in
241
+ [`docs/evaluation.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/evaluation.md).
242
+
243
+ ## Documentation
244
+
245
+ - [`docs/taxonomy.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/taxonomy.md): every trick, its level, its main guard, and what is out of scope.
246
+ - [`docs/architecture.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/architecture.md): how it works inside, and how to add a transform.
247
+ - [`docs/scorer.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/scorer.md): the plausibility scorer and the experiment behind it.
248
+ - [`docs/evaluation.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/evaluation.md): the sets, the measures, and every baseline number.
249
+
250
+ Contributions go through a pull request: the tests and the gate must pass, and a new transform
251
+ comes with samples. The false-change rate on the clean set may not rise.
252
+
253
+ ## Licence
254
+
255
+ MIT for our own code and data; see [`LICENSE`](https://github.com/lapid/text-deobfuscate/blob/master/LICENSE). Third-party data keeps its own licence,
256
+ with its source and terms recorded beside it in [`data/`](https://github.com/lapid/text-deobfuscate/tree/master/data/) and
257
+ [`src/text_deobfuscate/data/NOTICE.md`](https://github.com/lapid/text-deobfuscate/blob/master/src/text_deobfuscate/data/NOTICE.md).
@@ -0,0 +1,227 @@
1
+ # text-deobfuscate
2
+
3
+ Takes a text and returns the deobfuscated text, plus a record of which transforms were applied
4
+ and where.
5
+
6
+ ```python
7
+ >>> from text_deobfuscate import deobfuscate
8
+ >>> deobfuscate("Please confirm your account").text
9
+ 'Please confirm your account'
10
+ >>> deobfuscate("Cl1ck h3r3 to cla1m your pr1ze", strictness="balanced").text
11
+ 'Click here to claim your prize'
12
+ ```
13
+
14
+ **Status: version 0.1.0a1, an alpha.** The library and the command work,
15
+ thirty-two transforms are implemented and measured, and the numbers below say how far to trust
16
+ them: they are mostly from samples our own generators produced, so read "How well it works"
17
+ before relying on it.
18
+
19
+ ## Installing
20
+
21
+ From PyPI. The version is an alpha, so pip needs `--pre`:
22
+
23
+ ```sh
24
+ pip install --pre text-deobfuscate # or: uv add --prerelease allow text-deobfuscate
25
+ ```
26
+
27
+ Or from a clone, to work on it:
28
+
29
+ ```sh
30
+ git clone https://github.com/lapid/text-deobfuscate && cd text-deobfuscate
31
+ uv sync # or: pip install .
32
+ ```
33
+
34
+ It needs **Python 3.10 or newer** and has **no dependencies**.
35
+
36
+ ## Using it
37
+
38
+ As a library. `deobfuscate()` returns the text and an ordered list of steps; each step says what
39
+ changed, where it sat in the text as it stood before that step, and how confident the engine was:
40
+
41
+ ```python
42
+ from text_deobfuscate import deobfuscate
43
+
44
+ result = deobfuscate("Please confirm your acc​ount")
45
+ result.text # 'Please confirm your account'
46
+ [step.transform for step in result.steps] # ['zero-width', 'html-entity']
47
+ result.steps[0].start, result.steps[0].end # 25, 33
48
+ ```
49
+
50
+ As a command, with `--json` for the whole record:
51
+
52
+ ```sh
53
+ $ text-deobfuscate "UGxlYXNlIHNlbmQgdGhlIHJlcG9ydCBiZWZvcmUgRnJpZGF5"
54
+ Please send the report before Friday
55
+
56
+ $ text-deobfuscate --strictness balanced "c o n f i r m y o u r a c c o u n t"
57
+ confirm your account
58
+
59
+ $ text-deobfuscate --json "Thx 4 ur msg" # text plus steps, as JSON
60
+ ```
61
+
62
+ It reads standard input when given no argument, so it pipes. Piped or redirected, it writes the
63
+ text and nothing else — no trailing newline is added, so `text-deobfuscate x > file` holds exactly the
64
+ text. A terminal gets a line ending, so the prompt does not land mid-sentence.
65
+
66
+ ### What is public
67
+
68
+ `deobfuscate`, `Result`, `Step`, `STRICTNESS_LEVELS` and `__version__`, and the `text-deobfuscate`
69
+ command. Everything else in the package (`engine`, `scorer`, `transforms`, `protect`, `scripts` and
70
+ the rest) is internal and may change in any release, even though Python lets you import it. The
71
+ package ships type information (`py.typed`); the public names check cleanly with mypy, and the
72
+ internals still have some warnings.
73
+
74
+ ## Strictness
75
+
76
+ Three levels, because the tricks differ in how certain they are. `conservative` is the default.
77
+
78
+ | Level | Adds | Example |
79
+ |---|---|---|
80
+ | `conservative` | Encodings and escapes, invisible characters, styled and fullwidth letters, ciphers, reversal, Morse — things ordinary English does not contain | `UGxlYXNl…`, `e`, `\141\142`, `ⓒⓞⓝⓕⓘⓡⓜ` |
81
+ | `balanced` | Ambiguous visual tricks: lookalike letters, added accents, overlaid marks, leetspeak, inserted separators | `раssword` with Cyrillic letters, `Cl1ck`, `c o n f i r m` |
82
+ | `aggressive` | A lower acceptance margin, plus informal writing from short lists: stretched words, texting shorthand, common misspellings, emoji for a word | `sooooo`, `l8r`, `recieve`, `📞 me` |
83
+
84
+ Below `aggressive`, informal writing comes back exactly as written.
85
+
86
+ ## How it decides
87
+
88
+ **Changing clean text is treated as the worst mistake the tool can make.** Transforms only
89
+ *propose* rewrites; the engine applies one only when the result scores as more plausible English
90
+ than what it replaces, judged by a word list and a character four-gram table. Layered
91
+ obfuscation is decoded first and judged by what it finally turns into. URLs, paths, hashes and
92
+ other things that must not be touched are shielded. Nothing is ever executed — only decoded,
93
+ within size and depth limits.
94
+
95
+ Details in [`docs/architecture.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/architecture.md) and [`docs/scorer.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/scorer.md).
96
+
97
+ ## How well it works
98
+
99
+ **Read this first.** Most samples were made by our own obfuscation generators, so they test the
100
+ decoders against the people who wrote them. The independent evidence is smaller: 3,000 real
101
+ phishing sentences, 25 sentences disguised by hand, and sets made by someone else's generator
102
+ (below). The development sets have been run and tuned against many times, and the test split was
103
+ used up in October 2026. Treat every figure as an upper bound for text you have not seen.
104
+
105
+ Recovery means the text came back exactly, ignoring capital letters; clean texts changed is
106
+ measured strictly. The tool is judged first on **not changing clean text**.
107
+
108
+ **Development sets, thirty-two transforms (2026-10-29):** 7,850 clean texts, about 9,500
109
+ obfuscated ones, and the 3,000 real phishing sentences (kept outside the repository).
110
+
111
+ | Level | Clean texts changed | Recovered | Made worse | Real phishing | Hand-disguised |
112
+ |---|---|---|---|---|---|
113
+ | `conservative` | 0.01% (1 of 7,850) | 89.3% (5,273 of 5,905) | 1.2% | — | — |
114
+ | `balanced` | 0.01% (1 of 7,850) | 88.1% (10,292 of 11,680) | 0.6% | 90.8% | 15 of 25 |
115
+ | `aggressive` | 0.28% (22 of 7,850) | 86.8% (10,864 of 12,520) | 0.7% | 94.4% | 17 of 25 |
116
+
117
+ Each level is scored on the samples it is meant to undo, so the denominators differ and the rows
118
+ are not comparable with each other. Adding transforms or harder samples lowers a total without
119
+ anything getting worse; judge a trick by its own row in
120
+ [`docs/evaluation.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/evaluation.md).
121
+
122
+ **Sets made by someone else's generator, scored at `balanced`.** The first is 6,000 rows kept
123
+ outside the repository; we tuned against it, so it is optimistic. The second is 3,000 fresh rows
124
+ from unused text of the same source and generator: new rows, but the same style of obfuscation.
125
+ Neither tests a different author's habits.
126
+
127
+ | Trick | Tuned set | Fresh rows |
128
+ |---|---|---|
129
+ | base64, ROT13, zero-width, spacing | 99–100% | 100% |
130
+ | fullwidth, reversal | 96–99% | 98.5–99.6% |
131
+ | homoglyph | 96.5% | 97.8% |
132
+ | a symbol between every character | 99.8% | 99.3% |
133
+ | leetspeak | 59.7% | 57.5% |
134
+ | alternating capitals | 0% (99.8% ignoring capitals) | 0% (100%) |
135
+ | dropped vowels | 0% | 0% |
136
+ | **all rows** | **77.3%** | **77.5%** |
137
+
138
+ Alternating capitals come back in lower case: the tool does not restore capitals. Dropped
139
+ vowels are informal writing, out of scope by design.
140
+
141
+ **Where it does worst** (development sets, `balanced`): a disguised sentence inside plain text is
142
+ the weakest case. Reverse in context recovers 2.5%, ROT47 15%, per-word reversal 17.5%, Atbash
143
+ 45%, Caesar 58%, ROT13 70%. Leetspeak with every letter replaced recovers 66% of whole texts;
144
+ accent substitution 62–71%. Encodings that fill a whole text (base64, hex, base32, escapes) are
145
+ above 90%.
146
+
147
+ **Time:** the median text takes about 2 ms and the 95th percentile about 15 ms. The slowest
148
+ inputs are long and hostile: 100,000 random printable characters take about ten seconds at
149
+ `aggressive` (six at `balanced`), and everything else tested at that size takes under 5. One call
150
+ scans at most one million characters in all.
151
+
152
+ **The test split** was run once, on 2026-10-12, for twenty-three transforms. It showed clean
153
+ texts changed at 3, 13 and 38 of 7,442 for the three levels. Reading the 13 at `balanced` on
154
+ 2026-10-27 led to fixes for leetspeak on names and numbers (`P-51's`, `S4C`, `tty1`), and 5
155
+ remain, so the split no longer measures anything independently.
156
+
157
+ ## Known limits
158
+
159
+ Honest list; each is measured and logged.
160
+
161
+ - **Clean texts changed was higher on test than on dev** (13 against 1 at `balanced`). Reading the
162
+ 13 showed leetspeak applied to names and numbers; that is fixed, and 5 remain, which are
163
+ arguably correct changes. The unseen text of a real user may still be changed in ways our
164
+ samples do not show: report it.
165
+ - **Hand-disguised sentences did not carry over** from dev (15 of 25) to test (5 of 22). Human
166
+ disguises vary more than generated ones.
167
+ - **Most of the evidence comes from generators we wrote or tuned against.** See the top of
168
+ "How well it works".
169
+ - **Leetspeak** is about 58% exact on someone else's generator. Lone `1` and `4` in lightly
170
+ disguised text need a second disguised word nearby, and the wider table (`8`, `9`, `2`) is weak.
171
+ - **Time and size:** inputs over 100,000 characters are returned unchanged, and one call stops
172
+ scanning after a fixed budget, so a very long text may be only partly decoded. The slowest
173
+ input found, 100,000 random printable characters, takes about ten seconds at `aggressive` and
174
+ six at `balanced`; 30,000 take about a second. If you pass untrusted text to it in a service,
175
+ cap its length (a few thousand characters covers messages and emails) and use a timeout.
176
+ - **A text reversed word by word cannot be told from a wholly reversed one** unless a full stop
177
+ settles it, so 51 of 210 such samples come back with the words in the wrong order. 44 of those
178
+ are still perfectly legible; this is a limit of the ambiguity, not a bug to fix.
179
+ - **A lone disguised word** with everyday accents, or a lone Greek "a", is left alone at
180
+ `balanced`: one word is not enough evidence.
181
+ - **A capital hidden behind a leetspeak digit is lost** (`Cl1ck` → `click`). Recovery is measured
182
+ ignoring capitals for this reason.
183
+ - **Shifted or reversed words inside otherwise clean text** recover poorly.
184
+ - **Text in another language** passes through unchanged in nearly every case: 75 short phrases in
185
+ six languages come back untouched at every level. A short phrase used to be reversed ("No pasa
186
+ nada" became "adan asap oN"); texts of fewer than four words and sixteen letters are no longer
187
+ reversed as a whole, which also means a very short reversed text is not recovered. A longer
188
+ foreign text that reads as English backwards would still be reversed.
189
+ - **At `aggressive`, accents are removed from other words** ("Zoë" may become "Zoe"). Normal English
190
+ spellings with accents (`naïve`, `café`, `résumé`, `jalapeño` and about eighty more) are kept.
191
+ - **No recogniser yet for key-like tokens or code**, so a ticket code in base32 may be decoded.
192
+ - **English only.** Other languages and scripts pass through unchanged, by design.
193
+ - **Deferred to version 2:** bidirectional controls, variation-selector payloads, Punycode, part
194
+ of Unicode's invisible characters, and some styled ranges. Samples for them exist and are
195
+ excluded from a normal run.
196
+ - **Wrong-encoding damage** (`é` for `é`) is out of scope and comes back unchanged.
197
+ - Code obfuscation is out of scope.
198
+
199
+ ## Testing and the regression gate
200
+
201
+ ```sh
202
+ uv run python -m unittest discover -s tests # 125 unit tests
203
+ uv run python -m evaluation.gate # the regression gate: pass or fail
204
+ uv run python -m evaluation.run --detail # the full report, per category and per trick
205
+ ```
206
+
207
+ The gate scores the committed sets at all three levels and fails if clean texts changed rises, if
208
+ any trick recovers fewer samples than before, or if the output stops settling when the tool is run
209
+ over its own output. A deliberate trade-off is made by passing `--update` in the same commit and
210
+ giving the reason in the commit message. More in
211
+ [`docs/evaluation.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/evaluation.md).
212
+
213
+ ## Documentation
214
+
215
+ - [`docs/taxonomy.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/taxonomy.md): every trick, its level, its main guard, and what is out of scope.
216
+ - [`docs/architecture.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/architecture.md): how it works inside, and how to add a transform.
217
+ - [`docs/scorer.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/scorer.md): the plausibility scorer and the experiment behind it.
218
+ - [`docs/evaluation.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/evaluation.md): the sets, the measures, and every baseline number.
219
+
220
+ Contributions go through a pull request: the tests and the gate must pass, and a new transform
221
+ comes with samples. The false-change rate on the clean set may not rise.
222
+
223
+ ## Licence
224
+
225
+ MIT for our own code and data; see [`LICENSE`](https://github.com/lapid/text-deobfuscate/blob/master/LICENSE). Third-party data keeps its own licence,
226
+ with its source and terms recorded beside it in [`data/`](https://github.com/lapid/text-deobfuscate/tree/master/data/) and
227
+ [`src/text_deobfuscate/data/NOTICE.md`](https://github.com/lapid/text-deobfuscate/blob/master/src/text_deobfuscate/data/NOTICE.md).
@@ -0,0 +1,52 @@
1
+ [project]
2
+ name = "text-deobfuscate"
3
+ version = "0.1.0a1"
4
+ description = "Undo text obfuscation (encodings and visual tricks) and report which transforms were applied."
5
+ license = "MIT"
6
+ license-files = ["LICENSE"]
7
+ readme = "README.md"
8
+ requires-python = ">=3.10"
9
+ keywords = [
10
+ "deobfuscation",
11
+ "text",
12
+ "leetspeak",
13
+ "homoglyph",
14
+ "phishing",
15
+ "unicode",
16
+ "normalization",
17
+ "security",
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 3 - Alpha",
21
+ "Intended Audience :: Developers",
22
+ "Intended Audience :: Information Technology",
23
+ "Natural Language :: English",
24
+ "Operating System :: OS Independent",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
30
+ "Programming Language :: Python :: 3.14",
31
+ "Topic :: Security",
32
+ "Typing :: Typed",
33
+ "Topic :: Text Processing",
34
+ ]
35
+ dependencies = []
36
+
37
+ [[project.authors]]
38
+ name = "Lapid Harel"
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/lapid/text-deobfuscate"
42
+ Repository = "https://github.com/lapid/text-deobfuscate"
43
+ Issues = "https://github.com/lapid/text-deobfuscate/issues"
44
+ Changelog = "https://github.com/lapid/text-deobfuscate/blob/master/CHANGELOG.md"
45
+ Documentation = "https://github.com/lapid/text-deobfuscate/blob/master/docs/architecture.md"
46
+
47
+ [project.scripts]
48
+ text-deobfuscate = "text_deobfuscate.cli:main"
49
+
50
+ [build-system]
51
+ requires = ["uv_build>=0.9.7,<0.14"]
52
+ build-backend = "uv_build"
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "text-deobfuscate"
3
+ version = "0.1.0a1"
4
+ description = "Undo text obfuscation (encodings and visual tricks) and report which transforms were applied."
5
+ license = "MIT"
6
+ license-files = ["LICENSE"]
7
+ readme = "README.md"
8
+ requires-python = ">=3.10"
9
+ authors = [{ name = "Lapid Harel" }]
10
+ keywords = ["deobfuscation", "text", "leetspeak", "homoglyph", "phishing", "unicode", "normalization", "security"]
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Intended Audience :: Developers",
14
+ "Intended Audience :: Information Technology",
15
+ "Natural Language :: English",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ "Topic :: Security",
24
+ "Typing :: Typed",
25
+ "Topic :: Text Processing",
26
+ ]
27
+ dependencies = []
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/lapid/text-deobfuscate"
31
+ Repository = "https://github.com/lapid/text-deobfuscate"
32
+ Issues = "https://github.com/lapid/text-deobfuscate/issues"
33
+ Changelog = "https://github.com/lapid/text-deobfuscate/blob/master/CHANGELOG.md"
34
+ Documentation = "https://github.com/lapid/text-deobfuscate/blob/master/docs/architecture.md"
35
+
36
+ [project.scripts]
37
+ text-deobfuscate = "text_deobfuscate.cli:main"
38
+
39
+ [build-system]
40
+ requires = ["uv_build>=0.9.7,<0.14"]
41
+ build-backend = "uv_build"
@@ -0,0 +1,13 @@
1
+ """Undo text obfuscation and report which transforms were applied."""
2
+
3
+ from importlib import metadata as _metadata
4
+
5
+ from text_deobfuscate.core import STRICTNESS_LEVELS, deobfuscate
6
+ from text_deobfuscate.types import Result, Step
7
+
8
+ try:
9
+ __version__ = _metadata.version("text-deobfuscate")
10
+ except _metadata.PackageNotFoundError: # running from a source tree that was never installed
11
+ __version__ = "unknown"
12
+
13
+ __all__ = ["STRICTNESS_LEVELS", "Result", "Step", "__version__", "deobfuscate"]
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from text_deobfuscate.cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,30 @@
1
+ import argparse
2
+ import json
3
+ import sys
4
+
5
+ from text_deobfuscate import __version__
6
+ from text_deobfuscate.core import STRICTNESS_LEVELS, deobfuscate
7
+
8
+
9
+ def main(argv: list[str] | None = None) -> int:
10
+ parser = argparse.ArgumentParser(
11
+ prog="text-deobfuscate",
12
+ description="Undo text obfuscation. Reads the text argument, or standard input if none is given.",
13
+ )
14
+ parser.add_argument("text", nargs="?", help="text to deobfuscate; standard input if omitted")
15
+ parser.add_argument("--strictness", choices=STRICTNESS_LEVELS, default="conservative")
16
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
17
+ parser.add_argument("--json", action="store_true", help="print the text and the steps applied as JSON")
18
+ args = parser.parse_args(argv)
19
+
20
+ text = args.text if args.text is not None else sys.stdin.read()
21
+ result = deobfuscate(text, strictness=args.strictness)
22
+ if args.json:
23
+ json.dump(result.to_dict(), sys.stdout, ensure_ascii=False)
24
+ sys.stdout.write("\n")
25
+ else:
26
+ # Nothing is added when the output is piped or redirected: the contract is the
27
+ # text and only the text, so `text-deobfuscate x > file` writes exactly that. A
28
+ # terminal gets a line ending, or the shell prompt lands mid-text.
29
+ sys.stdout.write(result.text + "\n" if sys.stdout.isatty() else result.text)
30
+ return 0