evilfonttool 2.0.0__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.
@@ -0,0 +1,70 @@
1
+ # This workflow will upload a Python Package to PyPI when a release is created
2
+ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
3
+
4
+ # This workflow uses actions that are not certified by GitHub.
5
+ # They are provided by a third-party and are governed by
6
+ # separate terms of service, privacy policy, and support
7
+ # documentation.
8
+
9
+ name: Upload Python Package
10
+
11
+ on:
12
+ release:
13
+ types: [published]
14
+
15
+ permissions:
16
+ contents: read
17
+
18
+ jobs:
19
+ release-build:
20
+ runs-on: ubuntu-latest
21
+
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - uses: actions/setup-python@v5
26
+ with:
27
+ python-version: "3.x"
28
+
29
+ - name: Build release distributions
30
+ run: |
31
+ # NOTE: put your own distribution build steps here.
32
+ python -m pip install build
33
+ python -m build
34
+
35
+ - name: Upload distributions
36
+ uses: actions/upload-artifact@v4
37
+ with:
38
+ name: release-dists
39
+ path: dist/
40
+
41
+ pypi-publish:
42
+ runs-on: ubuntu-latest
43
+ needs:
44
+ - release-build
45
+ permissions:
46
+ # IMPORTANT: this permission is mandatory for trusted publishing
47
+ id-token: write
48
+
49
+ # Dedicated environments with protections for publishing are strongly recommended.
50
+ # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
51
+ environment:
52
+ name: pypi
53
+ # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
54
+ # url: https://pypi.org/p/YOURPROJECT
55
+ #
56
+ # ALTERNATIVE: if your GitHub Release name is the PyPI project version string
57
+ # ALTERNATIVE: exactly, uncomment the following line instead:
58
+ # url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }}
59
+
60
+ steps:
61
+ - name: Retrieve release distributions
62
+ uses: actions/download-artifact@v4
63
+ with:
64
+ name: release-dists
65
+ path: dist/
66
+
67
+ - name: Publish release distributions to PyPI
68
+ uses: pypa/gh-action-pypi-publish@release/v1
69
+ with:
70
+ packages-dir: dist/
@@ -0,0 +1,6 @@
1
+ .venv
2
+ __pycache__/
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ *.pyc
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrew Griess
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,149 @@
1
+ Metadata-Version: 2.4
2
+ Name: evilfonttool
3
+ Version: 2.0.0
4
+ Summary: Evil Font steganography tool for security research
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.9
8
+ Requires-Dist: brotli
9
+ Requires-Dist: fonttools
10
+ Requires-Dist: python-docx
11
+ Description-Content-Type: text/markdown
12
+
13
+ # EvilFontTool
14
+
15
+ > **A font-based deception tool for red teaming, security research, and whatever else.**
16
+
17
+ EvilFontTool hides machine-readable text inside a document that displays completely different text to a human reader. It does this using **Evil Fonts** — fonts that intentionally deceive the viewer by rendering a different letter than understood by a computer. By remapping font glyphs, the document's visible characters show humans one thing while terminals, AI systems, and clipboard copy paste see another.
18
+
19
+ ## Evil Font Demo !!DON'T MISS THIS!!
20
+
21
+ **[View the Demo → Here](https://doctoreww.github.io/EvilFontTool/)** *(hosted on GitHub Pages)*
22
+
23
+ ---
24
+
25
+ ## Table of Contents
26
+
27
+ - [Installation](#installation)
28
+ - [Usage](#usage)
29
+ - [Ethical Use & Disclaimer](#ethical-use--disclaimer)
30
+ - [Contributing](#contributing)
31
+
32
+ ---
33
+
34
+ ## [How It Works — Blog Post Coming Soon](#)
35
+
36
+ ---
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ git clone https://github.com/DoctorEww/EvilFontTool.git
42
+ cd EvilFontTool
43
+ pip install .
44
+ ```
45
+
46
+ For development (editable install):
47
+
48
+ ```bash
49
+ pip install -e .
50
+ ```
51
+
52
+ ### Dependencies
53
+ - `fonttools` — font parsing and manipulation
54
+ - `python-docx` — DOCX generation
55
+
56
+ ---
57
+ ## Where can I find fonts to use?
58
+
59
+ * Ubuntu: `/usr/share/fonts`
60
+ * Windows: `C:\Windows\Fonts`
61
+ * https://fonts.google.com/
62
+ * The internet??
63
+
64
+
65
+ ---
66
+
67
+
68
+ ## Usage
69
+
70
+ All functionality is exposed via a single CLI with three subcommands.
71
+
72
+ ### `create` — Generate the font family for use in HTML or DOC files
73
+
74
+ ```bash
75
+ evilfonttool create <reference_font> <output_dir> <font_name>
76
+ ```
77
+
78
+ | Argument | Description |
79
+ |---|---|
80
+ | `reference_font` | Path to a `.ttf` or `.woff` source font |
81
+ | `output_dir` | Directory to write fonts and CSS into |
82
+ | `font_name` | Internal name prefix for the generated font family |
83
+
84
+ **Example:**
85
+ ```bash
86
+ evilfonttool create fonts/Arial.ttf output/ 'Arial'
87
+ ```
88
+
89
+ Outputs:
90
+ - `output/fonts/*.woff` — web fonts, one per character
91
+ - `output/ttffonts/*.ttf` — TTF fonts for document embedding
92
+ - `output/fonts.css` — `@font-face` declarations for web use
93
+
94
+ ---
95
+
96
+ ### `web` — Generate an evil font HTML file
97
+
98
+ ```bash
99
+ evilfonttool web <human_file> <computer_file> <output_file>
100
+ ```
101
+
102
+ > Requires `fonts.css` and the generated fonts to be in the output directory so the HTML file can use it (or change the path in the HTML file).
103
+
104
+ **Example:**
105
+ ```bash
106
+ evilfonttool web human.txt computer.txt output/index.html
107
+ ```
108
+
109
+ ---
110
+
111
+ ### `doc` — Generate a evil font DOCX file
112
+
113
+ ```bash
114
+ evilfonttool doc <human_file> <computer_file> <output_file> <font_name>
115
+ ```
116
+
117
+ > The `font_name` must match the name used in the `create` step. The TTF fonts must be installed on the system or embedded in the document. When saving the doc file you can choose in the file -> options -> save to embed the fonts in the file so its portable.
118
+
119
+ **Example:**
120
+ ```bash
121
+ evilfonttool doc human.txt computer.txt output/secret.docx MyFont
122
+ ```
123
+
124
+ ---
125
+
126
+ ### Input File Format
127
+
128
+ - Plain `.txt` files, one sentence or phrase per line
129
+ - Each line in `computer_file` must be **equal to or longer** than the corresponding line in `human_file`
130
+ - Lines are matched positionally (line 1 to line 1, etc.)
131
+
132
+ ---
133
+
134
+ ## Ethical Use & Disclaimer
135
+
136
+ **You are responsible for how you use this tool.** Deploying this technique against systems or individuals without explicit authorization is unethical and may be illegal. The authors provide this tool to help defenders understand and test for this class of vulnerability — not to enable attacks.
137
+
138
+ ---
139
+
140
+ ## Contributing
141
+
142
+ Contributions are welcome. If you've found a new attack surface, an improvement to the font generation pipeline, or a defense technique worth documenting, please open an issue or PR.
143
+
144
+ 1. Fork the repo
145
+ 2. Create a feature branch (`git checkout -b feature/my-feature`)
146
+ 3. Commit your changes
147
+ 4. Open a pull request with a clear description of what changed and why
148
+
149
+
@@ -0,0 +1,137 @@
1
+ # EvilFontTool
2
+
3
+ > **A font-based deception tool for red teaming, security research, and whatever else.**
4
+
5
+ EvilFontTool hides machine-readable text inside a document that displays completely different text to a human reader. It does this using **Evil Fonts** — fonts that intentionally deceive the viewer by rendering a different letter than understood by a computer. By remapping font glyphs, the document's visible characters show humans one thing while terminals, AI systems, and clipboard copy paste see another.
6
+
7
+ ## Evil Font Demo !!DON'T MISS THIS!!
8
+
9
+ **[View the Demo → Here](https://doctoreww.github.io/EvilFontTool/)** *(hosted on GitHub Pages)*
10
+
11
+ ---
12
+
13
+ ## Table of Contents
14
+
15
+ - [Installation](#installation)
16
+ - [Usage](#usage)
17
+ - [Ethical Use & Disclaimer](#ethical-use--disclaimer)
18
+ - [Contributing](#contributing)
19
+
20
+ ---
21
+
22
+ ## [How It Works — Blog Post Coming Soon](#)
23
+
24
+ ---
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ git clone https://github.com/DoctorEww/EvilFontTool.git
30
+ cd EvilFontTool
31
+ pip install .
32
+ ```
33
+
34
+ For development (editable install):
35
+
36
+ ```bash
37
+ pip install -e .
38
+ ```
39
+
40
+ ### Dependencies
41
+ - `fonttools` — font parsing and manipulation
42
+ - `python-docx` — DOCX generation
43
+
44
+ ---
45
+ ## Where can I find fonts to use?
46
+
47
+ * Ubuntu: `/usr/share/fonts`
48
+ * Windows: `C:\Windows\Fonts`
49
+ * https://fonts.google.com/
50
+ * The internet??
51
+
52
+
53
+ ---
54
+
55
+
56
+ ## Usage
57
+
58
+ All functionality is exposed via a single CLI with three subcommands.
59
+
60
+ ### `create` — Generate the font family for use in HTML or DOC files
61
+
62
+ ```bash
63
+ evilfonttool create <reference_font> <output_dir> <font_name>
64
+ ```
65
+
66
+ | Argument | Description |
67
+ |---|---|
68
+ | `reference_font` | Path to a `.ttf` or `.woff` source font |
69
+ | `output_dir` | Directory to write fonts and CSS into |
70
+ | `font_name` | Internal name prefix for the generated font family |
71
+
72
+ **Example:**
73
+ ```bash
74
+ evilfonttool create fonts/Arial.ttf output/ 'Arial'
75
+ ```
76
+
77
+ Outputs:
78
+ - `output/fonts/*.woff` — web fonts, one per character
79
+ - `output/ttffonts/*.ttf` — TTF fonts for document embedding
80
+ - `output/fonts.css` — `@font-face` declarations for web use
81
+
82
+ ---
83
+
84
+ ### `web` — Generate an evil font HTML file
85
+
86
+ ```bash
87
+ evilfonttool web <human_file> <computer_file> <output_file>
88
+ ```
89
+
90
+ > Requires `fonts.css` and the generated fonts to be in the output directory so the HTML file can use it (or change the path in the HTML file).
91
+
92
+ **Example:**
93
+ ```bash
94
+ evilfonttool web human.txt computer.txt output/index.html
95
+ ```
96
+
97
+ ---
98
+
99
+ ### `doc` — Generate a evil font DOCX file
100
+
101
+ ```bash
102
+ evilfonttool doc <human_file> <computer_file> <output_file> <font_name>
103
+ ```
104
+
105
+ > The `font_name` must match the name used in the `create` step. The TTF fonts must be installed on the system or embedded in the document. When saving the doc file you can choose in the file -> options -> save to embed the fonts in the file so its portable.
106
+
107
+ **Example:**
108
+ ```bash
109
+ evilfonttool doc human.txt computer.txt output/secret.docx MyFont
110
+ ```
111
+
112
+ ---
113
+
114
+ ### Input File Format
115
+
116
+ - Plain `.txt` files, one sentence or phrase per line
117
+ - Each line in `computer_file` must be **equal to or longer** than the corresponding line in `human_file`
118
+ - Lines are matched positionally (line 1 to line 1, etc.)
119
+
120
+ ---
121
+
122
+ ## Ethical Use & Disclaimer
123
+
124
+ **You are responsible for how you use this tool.** Deploying this technique against systems or individuals without explicit authorization is unethical and may be illegal. The authors provide this tool to help defenders understand and test for this class of vulnerability — not to enable attacks.
125
+
126
+ ---
127
+
128
+ ## Contributing
129
+
130
+ Contributions are welcome. If you've found a new attack surface, an improvement to the font generation pipeline, or a defense technique worth documenting, please open an issue or PR.
131
+
132
+ 1. Fork the repo
133
+ 2. Create a feature branch (`git checkout -b feature/my-feature`)
134
+ 3. Commit your changes
135
+ 4. Open a pull request with a clear description of what changed and why
136
+
137
+
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "evilfonttool"
7
+ version = "2.0.0"
8
+ description = "Evil Font steganography tool for security research"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ dependencies = [
13
+ "fonttools",
14
+ "python-docx",
15
+ "brotli",
16
+ ]
17
+
18
+ [project.scripts]
19
+ evilfonttool = "evilfonttool.cli:main"
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/evilfonttool"]
@@ -0,0 +1,8 @@
1
+ from evilfonttool._core import (
2
+ createfonts,
3
+ createstealthfont,
4
+ createhtml,
5
+ create_doc,
6
+ )
7
+
8
+ __all__ = ["createfonts", "createstealthfont", "createhtml", "create_doc"]
@@ -0,0 +1,3 @@
1
+ from evilfonttool.cli import main
2
+
3
+ main()
@@ -0,0 +1,363 @@
1
+ import copy
2
+ import logging
3
+ import pathlib
4
+ import string
5
+
6
+ from fontTools.ttLib import TTFont
7
+ from docx import Document
8
+ from docx.oxml.ns import qn
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Configuration
15
+ # ---------------------------------------------------------------------------
16
+
17
+ # The set of characters to include in the font family.
18
+ # Modify this to limit which characters are processed.
19
+ LETTERS = (
20
+ string.ascii_lowercase
21
+ + " "
22
+ + string.punctuation
23
+ + string.ascii_uppercase
24
+ + string.digits
25
+ )
26
+
27
+ # Advance width for invisible (stealth) characters.
28
+ # 0 works for most renderers, but some applications may behave unexpectedly.
29
+ WIDTH = 0
30
+
31
+ # Path to the bundled HTML template.
32
+ _TEMPLATE = pathlib.Path(__file__).parent / "data" / "template.html"
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Font helpers
37
+ # ---------------------------------------------------------------------------
38
+
39
+ def _get_unicode_cmap(font):
40
+ """Return the first suitable Windows Unicode cmap subtable (format 4 or 12).
41
+
42
+ Raises ValueError if none is found.
43
+ """
44
+ for table in font['cmap'].tables:
45
+ if (table.format in (4, 12)
46
+ and table.platformID == 3
47
+ and table.platEncID in (1, 10)):
48
+ return table
49
+ raise ValueError("No suitable Unicode cmap subtable found in the font.")
50
+
51
+
52
+ def _remove_layout_tables(font):
53
+ """Remove OpenType tables that control ligatures, kerning, and substitution.
54
+
55
+ These tables operate on glyph names rather than cmap entries, so they can
56
+ override our remapping in unpredictable ways depending on surrounding characters.
57
+ """
58
+ for tag in ('GSUB', 'GPOS', 'kern', 'GDEF'):
59
+ if tag in font:
60
+ del font[tag]
61
+
62
+
63
+ def _rename_font(font, new_name):
64
+ """Overwrite the family name records in the font's name table."""
65
+ for record in font['name'].names:
66
+ if record.nameID in (1, 4, 6):
67
+ record.string = new_name.encode("utf-16-be")
68
+
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Core font generation
72
+ # ---------------------------------------------------------------------------
73
+
74
+ def createstealthfont(reference_font, output_dir, font_name):
75
+ """Generate a stealth font where every character renders as an invisible space.
76
+
77
+ The stealth font is used to hide the extra computer-file characters that
78
+ have no corresponding human-file character to disguise themselves as.
79
+ It is saved as both WOFF (web) and TTF (document/desktop).
80
+ """
81
+ font = TTFont(reference_font)
82
+ unicode_cmap = _get_unicode_cmap(font)
83
+
84
+ # Use the space glyph as the target — all other characters will map to it
85
+ space_glyph_name = unicode_cmap.cmap.get(ord(" "))
86
+
87
+ # Remap every character in LETTERS to the space glyph and zero its advance width
88
+ for table in font['cmap'].tables:
89
+ for char in LETTERS:
90
+ if ord(char) in table.cmap:
91
+ table.cmap[ord(char)] = space_glyph_name
92
+ font['hmtx'].metrics[space_glyph_name] = (
93
+ WIDTH,
94
+ font['hmtx'].metrics[space_glyph_name][1],
95
+ )
96
+
97
+ _remove_layout_tables(font)
98
+
99
+ # Internal font name for the stealth variant uses "0" as a sentinel
100
+ new_name = f'{font_name} 0'
101
+ _rename_font(font, new_name)
102
+ logger.debug(f" Stealth font internal name: {new_name}")
103
+
104
+ # Append the @font-face CSS rule for the stealth font
105
+ with open(f'{output_dir}/fonts.css', "a") as css_file:
106
+ css_file.write(
107
+ "@font-face {"
108
+ 'font-family: "0";'
109
+ 'src: url("fonts/0.woff") format(\'woff\');'
110
+ "}"
111
+ )
112
+
113
+ # Save WOFF (flavor must be set explicitly) then TTF
114
+ font.flavor = 'woff'
115
+ font.save(f'{output_dir}/fonts/0.woff')
116
+ font.flavor = None
117
+ font.save(f'{output_dir}/ttffonts/0.ttf')
118
+
119
+ logger.debug(f"[DONE] stealth font -> {output_dir}/fonts/0.woff + ttffonts/0.ttf")
120
+
121
+
122
+ def createfonts(reference_font, output_dir, font_name):
123
+ """Generate one Evil Font variant per character in LETTERS.
124
+
125
+ In each variant, every character's glyph is replaced with the glyph for
126
+ `currentletter`. This means that no matter what Unicode byte is stored in
127
+ the document, the renderer will draw `currentletter` — the core Evil Font trick.
128
+
129
+ Also writes all @font-face rules to fonts.css (overwrites any existing file).
130
+ """
131
+ logger.debug(f"Source font: {reference_font}")
132
+ logger.debug(f"Output dir: {output_dir}")
133
+ logger.debug(f"Characters: {len(LETTERS)} variants to generate")
134
+
135
+ with open(f'{output_dir}/fonts.css', "w") as css_file:
136
+
137
+ for currentletter in LETTERS:
138
+ # Load a fresh copy of the font for each variant to avoid cross-contamination
139
+ font = TTFont(reference_font)
140
+ font.recalcBBoxes = False
141
+
142
+ unicode_cmap = _get_unicode_cmap(font)
143
+
144
+ # Get the source glyph and its advance width for this letter
145
+ source_glyph_name = unicode_cmap.cmap.get(ord(currentletter))
146
+ source_width = font['hmtx'].metrics[source_glyph_name][0]
147
+
148
+ # Take a deep copy of the source glyph to use as a stamp
149
+ source_glyph = copy.deepcopy(font['glyf'][source_glyph_name])
150
+
151
+ # Remap every other character in LETTERS to look like currentletter
152
+ for table in font['cmap'].tables:
153
+ for char in LETTERS:
154
+ if char == currentletter:
155
+ continue
156
+ if ord(char) not in table.cmap:
157
+ continue
158
+
159
+ target_glyph_name = table.cmap[ord(char)]
160
+ target_original = font['glyf'][target_glyph_name]
161
+
162
+ # Preserve the target's original bounding box so that
163
+ # spacing and baseline positioning remain correct
164
+ orig_xMin = getattr(target_original, 'xMin', 0)
165
+ orig_yMax = getattr(target_original, 'yMax', 0)
166
+ orig_yMin = getattr(target_original, 'yMin', 0)
167
+
168
+ # Stamp a copy of the source glyph into the target slot
169
+ font['glyf'][target_glyph_name] = copy.deepcopy(source_glyph)
170
+
171
+ # Restore the vertical bounds (keeps line height consistent)
172
+ font['glyf'][target_glyph_name].yMax = orig_yMax
173
+ font['glyf'][target_glyph_name].yMin = orig_yMin
174
+
175
+ # Match advance width to source; preserve original LSB for positioning
176
+ font['hmtx'].metrics[target_glyph_name] = (source_width, orig_xMin)
177
+
178
+ _remove_layout_tables(font)
179
+
180
+ # Each font variant is named using the hex encoding of the letter
181
+ # so the name is unique and safely usable as a filename
182
+ letter_hex = currentletter.encode().hex()
183
+ new_name = f'{font_name} {letter_hex}'
184
+ _rename_font(font, new_name)
185
+
186
+ # Save as WOFF for web use and TTF for document/desktop use
187
+ font.flavor = 'woff'
188
+ font.save(f'{output_dir}/fonts/{letter_hex}.woff')
189
+ font.flavor = None
190
+ font.save(f'{output_dir}/ttffonts/{letter_hex}.ttf')
191
+
192
+ # Write the @font-face rule for this variant
193
+ css_file.write(
194
+ f'@font-face {{'
195
+ f'font-family: "{letter_hex}";'
196
+ f'src: url("fonts/{letter_hex}.woff") format(\'woff\');'
197
+ f'}}'
198
+ )
199
+
200
+ logger.debug(f" [{letter_hex}] '{currentletter}' -> {output_dir}/fonts/{letter_hex}.woff + ttffonts/{letter_hex}.ttf")
201
+
202
+
203
+ # ---------------------------------------------------------------------------
204
+ # HTML output
205
+ # ---------------------------------------------------------------------------
206
+
207
+ def _write_html(output_file, content):
208
+ """Inject `content` into the bundled HTML template and write to `output_file`."""
209
+ html = _TEMPLATE.read_text()
210
+ html = html.replace("<!-- #STUFF HERE -->", content)
211
+ with open(output_file, "w") as f:
212
+ f.write(html)
213
+
214
+
215
+ def createhtml(input_human_file, input_computer_file, output_file):
216
+ """Build a steganographic HTML file from human and computer text files.
217
+
218
+ Each character from the computer file is wrapped in a <span> that applies
219
+ an Evil Font chosen by the corresponding human file character. To a human
220
+ reading the rendered page, the text looks like the human file. A machine
221
+ parsing the raw HTML sees the computer file.
222
+
223
+ Lines where the computer text is shorter than the human text are skipped.
224
+ Extra computer characters (beyond the human line length) are hidden using
225
+ the stealth font (font-family: '0').
226
+ """
227
+ logger.debug(f"Human file: {input_human_file}")
228
+ logger.debug(f"Computer file: {input_computer_file}")
229
+ logger.debug(f"Output file: {output_file}")
230
+
231
+ spans = []
232
+ lines_processed = 0
233
+ total_hidden = 0
234
+ line_number = 0
235
+
236
+ with open(input_human_file, "r") as human_file, \
237
+ open(input_computer_file, "r") as computer_file:
238
+
239
+ for human_line, computer_line in zip(
240
+ (l.rstrip('\n') for l in human_file),
241
+ (l.rstrip('\n') for l in computer_file),
242
+ ):
243
+ h_len = len(human_line)
244
+ c_len = len(computer_line)
245
+
246
+ line_number += 1
247
+ logger.debug(f"Human ({h_len} chars): {human_line}")
248
+ logger.debug(f"Computer({c_len} chars): {computer_line}")
249
+
250
+ if c_len < h_len:
251
+ logger.warning(f" Skipping line {line_number}: computer line must be >= human line length.")
252
+ continue
253
+
254
+ # Extra computer characters are inserted at the midpoint of the human text
255
+ diff = c_len - h_len
256
+ mid = h_len // 2
257
+
258
+ logger.debug(f" diff={diff}, hidden chars inserted at mid={mid}")
259
+
260
+ line_spans = []
261
+ h_index = 0
262
+
263
+ for i, computer_char in enumerate(computer_line):
264
+ if mid <= i < mid + diff:
265
+ # Hidden character — rendered invisibly using the stealth font
266
+ line_spans.append(
267
+ f"<span style=\"font-family: '0';\">{computer_char}</span>"
268
+ )
269
+ else:
270
+ # Visible character — disguised as the corresponding human character
271
+ human_char = human_line[h_index]
272
+ letter_hex = human_char.encode().hex()
273
+ line_spans.append(
274
+ f"<span style=\"font-family: '{letter_hex}';\">{computer_char}</span>"
275
+ )
276
+ h_index += 1
277
+
278
+ spans.append("".join(line_spans))
279
+ lines_processed += 1
280
+ total_hidden += diff
281
+
282
+ _write_html(output_file, "\n<br>\n".join(spans))
283
+ logger.debug(f"[DONE] HTML written -> {output_file} ({lines_processed} lines, {total_hidden} hidden chars)")
284
+
285
+
286
+ # ---------------------------------------------------------------------------
287
+ # DOCX output
288
+ # ---------------------------------------------------------------------------
289
+
290
+ def create_doc(input_human_file, input_computer_file, output_file, font_name_in):
291
+ """Build a steganographic DOCX file from human and computer text files.
292
+
293
+ Works identically to createhtml() but outputs a Word document. Each run is
294
+ assigned an Evil Font by setting all four font slots (ascii, hAnsi, eastAsia,
295
+ cs) to prevent Word from falling back to a system font.
296
+
297
+ The TTF variants of the Evil Fonts must be installed on the system (or
298
+ embedded in the document) for the deception to render correctly in Word.
299
+
300
+ Lines where the computer text is shorter than the human text are skipped.
301
+ """
302
+ logger.debug(f"Human file: {input_human_file}")
303
+ logger.debug(f"Computer file: {input_computer_file}")
304
+ logger.debug(f"Output file: {output_file}")
305
+ logger.debug(f"Font family: {font_name_in}")
306
+
307
+ doc = Document()
308
+ lines_processed = 0
309
+ total_hidden = 0
310
+ line_number = 0
311
+
312
+ with open(input_human_file, "r") as human_file, \
313
+ open(input_computer_file, "r") as computer_file:
314
+
315
+ for human_line, computer_line in zip(
316
+ (l.rstrip('\n') for l in human_file),
317
+ (l.rstrip('\n') for l in computer_file),
318
+ ):
319
+ h_len = len(human_line)
320
+ c_len = len(computer_line)
321
+
322
+ line_number += 1
323
+ logger.debug(f"Human ({h_len} chars): {human_line}")
324
+ logger.debug(f"Computer({c_len} chars): {computer_line}")
325
+
326
+ if c_len < h_len:
327
+ logger.warning(f" Skipping line {line_number}: computer line must be >= human line length.")
328
+ continue
329
+
330
+ diff = c_len - h_len
331
+ mid = h_len // 2
332
+
333
+ logger.debug(f" diff={diff}, hidden chars inserted at mid={mid}")
334
+
335
+ p = doc.add_paragraph()
336
+ h_index = 0
337
+
338
+ for i, computer_char in enumerate(computer_line):
339
+ if mid <= i < mid + diff:
340
+ # Hidden character — use the stealth (zero-width) font
341
+ font_name = f'{font_name_in} 0'
342
+ else:
343
+ # Visible character — disguised as the corresponding human character
344
+ human_char = human_line[h_index]
345
+ font_name = f'{font_name_in} {human_char.encode().hex()}'
346
+ h_index += 1
347
+
348
+ # Add a run and explicitly set all four font slots.
349
+ # Word will fall back to a system font if any slot is unset,
350
+ # which would break the illusion.
351
+ run = p.add_run(computer_char)
352
+ run.font.name = font_name
353
+ rFonts = run._element.rPr.rFonts
354
+ rFonts.set(qn("w:ascii"), font_name)
355
+ rFonts.set(qn("w:hAnsi"), font_name)
356
+ rFonts.set(qn("w:eastAsia"), font_name)
357
+ rFonts.set(qn("w:cs"), font_name)
358
+
359
+ lines_processed += 1
360
+ total_hidden += diff
361
+
362
+ doc.save(output_file)
363
+ logger.debug(f"[DONE] DOCX written -> {output_file} ({lines_processed} lines, {total_hidden} hidden chars)")
@@ -0,0 +1,107 @@
1
+ import argparse
2
+ import logging
3
+ import os
4
+ import sys
5
+
6
+ from evilfonttool._core import createfonts, createstealthfont, createhtml, create_doc
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def setup_parser():
12
+ parser = argparse.ArgumentParser(
13
+ description="EvilFontTool — Evil Font steganography tool for security research.",
14
+ formatter_class=argparse.RawDescriptionHelpFormatter,
15
+ )
16
+ parser.add_argument(
17
+ '--log',
18
+ default='WARNING',
19
+ metavar='LEVEL',
20
+ help='Log level: DEBUG, INFO, WARNING, ERROR (default: WARNING).',
21
+ )
22
+ subparsers = parser.add_subparsers(dest='command', required=True)
23
+
24
+ # --- create ---
25
+ create_parser = subparsers.add_parser(
26
+ 'create',
27
+ help='Generate an Evil Font family from a reference font.',
28
+ description=(
29
+ 'Produces one font variant per printable character plus a stealth font, '
30
+ 'along with a fonts.css file for web use.'
31
+ ),
32
+ )
33
+ create_parser.add_argument('reference_font', help='Path to the source .ttf font file.')
34
+ create_parser.add_argument('output_dir', help='Directory to write fonts and CSS into.')
35
+ create_parser.add_argument('font_name', help='Internal name prefix for the font family.')
36
+
37
+ # --- web ---
38
+ web_parser = subparsers.add_parser(
39
+ 'web',
40
+ help='Generate a steganographic HTML file.',
41
+ description=(
42
+ 'Wraps computer-file characters in Evil Font spans so the page displays '
43
+ 'human-file text visually while the raw HTML contains the computer text. '
44
+ 'fonts.css must be present in the output directory.'
45
+ ),
46
+ )
47
+ web_parser.add_argument('input_human_file', help='Text visible to human readers.')
48
+ web_parser.add_argument('input_computer_file', help='Text visible to machines / AI.')
49
+ web_parser.add_argument('output_file', help='Path for the generated HTML file.')
50
+
51
+ # --- doc ---
52
+ doc_parser = subparsers.add_parser(
53
+ 'doc',
54
+ help='Generate a steganographic DOCX file.',
55
+ description=(
56
+ 'Produces a Word document using Evil Fonts so the displayed text differs '
57
+ 'from the underlying Unicode. font_name must match the value used in create.'
58
+ ),
59
+ )
60
+ doc_parser.add_argument('input_human_file', help='Text visible to human readers.')
61
+ doc_parser.add_argument('input_computer_file', help='Text visible to machines / AI.')
62
+ doc_parser.add_argument('output_file', help='Path for the generated DOCX file.')
63
+ doc_parser.add_argument('font_name', help='Font family name (must match create step).')
64
+
65
+ return parser
66
+
67
+
68
+ def main():
69
+ parser = setup_parser()
70
+ args = parser.parse_args()
71
+
72
+ level = getattr(logging, args.log.upper(), None)
73
+ if not isinstance(level, int):
74
+ print(f"Error: invalid log level '{args.log}'. Choose from DEBUG, INFO, WARNING, ERROR.", file=sys.stderr)
75
+ sys.exit(1)
76
+
77
+ logging.basicConfig(level=level, format='%(message)s', stream=sys.stderr)
78
+
79
+ if args.command == 'create':
80
+ if not os.path.isfile(args.reference_font):
81
+ logger.error("'%s' does not exist.", args.reference_font)
82
+ sys.exit(1)
83
+
84
+ for subdir in ('', '/fonts', '/ttffonts'):
85
+ os.makedirs(args.output_dir + subdir, exist_ok=True)
86
+
87
+ createfonts(args.reference_font, args.output_dir, args.font_name)
88
+ createstealthfont(args.reference_font, args.output_dir, args.font_name)
89
+
90
+ elif args.command == 'web':
91
+ for path in (args.input_human_file, args.input_computer_file):
92
+ if not os.path.isfile(path):
93
+ logger.error("'%s' does not exist.", path)
94
+ sys.exit(1)
95
+ createhtml(args.input_human_file, args.input_computer_file, args.output_file)
96
+
97
+ elif args.command == 'doc':
98
+ for path in (args.input_human_file, args.input_computer_file):
99
+ if not os.path.isfile(path):
100
+ logger.error("'%s' does not exist.", path)
101
+ sys.exit(1)
102
+ create_doc(
103
+ args.input_human_file,
104
+ args.input_computer_file,
105
+ args.output_file,
106
+ args.font_name,
107
+ )
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+
3
+ <link rel="stylesheet" href="fonts.css">
4
+
5
+
6
+ <html>
7
+ <body>
8
+
9
+ <!-- #STUFF HERE -->
10
+
11
+ </body>
12
+ </html>