├── .gitignore
├── TODO.md
├── LICENSE
├── tests.py
├── zero-width-steganography.py
├── README.md
├── zerowidth.py
└── docs
└── zerowidth.html
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | venv
3 | .vscode
4 | .coverage
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | # TODO LIST
2 |
3 | ## CODE
4 | 1. ~~create requirements.txt file~~ **NO NEED TO DO SO**
5 | 2. ~~comment code in a decent way~~ **DONE**
6 | 3. ~~do some testing~~ **DONE**
7 | 4. find some more descriptive names for the methods *STILL THINKING IF I SHOULD CHANGE NAME*
8 | 5. git squash and go public *IT'S NOT READY YET!*
9 | 6. ~~convert function arguments in \*\*kwargs~~ **DONE**
10 |
11 | ## INTERFACE
12 | 1. add colored interface - *is it really necessary?*
13 |
14 | ## DOCS
15 | 1. create some documentation explaining how i did it all *WIP*
16 | 2. write a decent readme *WIP*
17 | 3. video recording of how it works
18 | 4. ~~**A more descriptive name?**~~ **FOUND? I GUESS**
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [2022] [Lorenzo Rossi]
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.
--------------------------------------------------------------------------------
/tests.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from zerowidth import ZeroWidth, Position
3 |
4 | from random import choices, randint
5 | from string import ascii_letters
6 | from os import remove
7 |
8 |
9 | def random_str(k=128) -> str:
10 | return "".join(choices(ascii_letters, k=k))
11 |
12 |
13 | def random_multiline(n=32, k=128) -> str:
14 | return "\n".join(random_str(k) for _ in range(n))
15 |
16 |
17 | TESTS = 100
18 | PATH = "test.log"
19 |
20 |
21 | class TestSteganography(unittest.TestCase):
22 | def test_encode_decode(self):
23 | z = ZeroWidth()
24 |
25 | for _ in range(TESTS):
26 | k = randint(50, 200)
27 | s = random_str(k)
28 | encoded = z._spaceEncode(s)
29 | decoded = z._spaceDecode(encoded)
30 | self.assertEqual(s, decoded)
31 |
32 | def test_embed(self):
33 | z = ZeroWidth()
34 | for position in Position:
35 | for _ in range(TESTS):
36 | num = randint(5, 10)
37 | clear = random_str(32)
38 | source = random_multiline(128)
39 | encoded = z.zeroEncode(
40 | source=source, clear=clear, position=position, k=num
41 | )
42 |
43 | decoded = z.zeroDecode(source=encoded)
44 |
45 | if position in [
46 | Position.NTHLINES,
47 | Position.RANDOMINLINE,
48 | ]:
49 | self.assertEqual(clear, decoded[: len(clear)])
50 | else:
51 | self.assertEqual(clear, decoded)
52 |
53 | cleaned = z.cleanString(encoded)
54 | self.assertEqual(source, cleaned)
55 |
56 | def test_file_encode(self):
57 | z = ZeroWidth()
58 |
59 | for position in Position:
60 | for _ in range(TESTS):
61 | source = random_multiline()
62 | with open(PATH, "w") as f:
63 | f.write(source)
64 |
65 | num = randint(5, 10)
66 | clear = random_str(k=32)
67 | encoded = z.zeroEncodeFile(
68 | source_path=PATH, clear=clear, position=position, k=num
69 | )
70 |
71 | with open(PATH, "w") as f:
72 | f.write(encoded)
73 |
74 | decoded = z.zeroDecodeFile(source_path=PATH)
75 |
76 | if position in [
77 | Position.NTHLINES,
78 | Position.RANDOMINLINE,
79 | ]:
80 | self.assertEqual(clear, decoded[: len(clear)])
81 | else:
82 | self.assertEqual(clear, decoded)
83 |
84 | self.assertEqual(source, z.cleanFile(PATH))
85 |
86 | remove(PATH)
87 |
88 | def test_edge_cases(self):
89 | z = ZeroWidth()
90 |
91 | # empty encoded string
92 | clear = ""
93 | source = random_multiline(128)
94 | encoded = z.zeroEncode(source=source, clear=clear, position=Position.NTHLINES)
95 | decoded = z.zeroDecode(source=encoded)
96 | self.assertEqual(clear, decoded[: len(clear)])
97 |
98 | # single line string
99 | clear = random_str()
100 | source = random_multiline(128)
101 | encoded = z.zeroEncode(source=source, clear=clear, position=Position.NTHLINES)
102 | decoded = z.zeroDecode(source=encoded)
103 | self.assertEqual(clear, decoded[: len(clear)])
104 |
105 |
106 | if __name__ == "__main__":
107 | unittest.main()
108 |
--------------------------------------------------------------------------------
/zero-width-steganography.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import zerowidth
3 |
4 |
5 | def check_args(args: argparse.Namespace) -> bool:
6 | if args.clear_source and args.file_source:
7 | print("Invalid options. Pass either a string source or a file source.")
8 | return False
9 |
10 | if [args.encode, args.decode, args.clean].count(True) != 1:
11 | print("Invalid options. Pass either encode, decode or clear.")
12 | return False
13 |
14 | if not (args.clear_source or args.file_source) and args.encode:
15 | print("Invalid options. Pass one source.")
16 | return False
17 |
18 | if not args.to_hide and args.encode:
19 | print("Invalid options. Pass a string to hide.")
20 | return False
21 |
22 | if args.encode:
23 | if not args.position:
24 | args.position = zerowidth.Position.NTHLINES
25 | else:
26 | try:
27 | args.position = zerowidth.Position(int(args.position))
28 | except ValueError:
29 | print("Invalid position.")
30 | return False
31 |
32 | try:
33 | args.k = int(args.k)
34 | except ValueError:
35 | print("Invalid k.")
36 | return False
37 |
38 | return True
39 |
40 |
41 | def clean(args: argparse.Namespace):
42 | z = zerowidth.ZeroWidth()
43 |
44 | if args.file_source:
45 | cleaned = z.cleanFile(source_path=args.file_source)
46 | else:
47 | cleaned = z.clean(source=args.clear_source)
48 |
49 | if args.output_path:
50 | with open(args.output_path, "w") as f:
51 | f.write(cleaned)
52 | return
53 |
54 | print(cleaned)
55 |
56 |
57 | def encode(args: argparse.Namespace):
58 | z = zerowidth.ZeroWidth()
59 | clear = args.to_hide
60 |
61 | if args.file_source:
62 | encoded = z.zeroEncodeFile(
63 | source_path=args.file_source, clear=clear, position=args.position, k=args.k
64 | )
65 | else:
66 | encoded = z.zeroEncode(
67 | clear=clear, source=args.clear_source, position=args.position, k=args.k
68 | )
69 |
70 | if args.output_path:
71 | with open(args.output_path, "w") as f:
72 | f.write(encoded)
73 | return
74 |
75 | print(encoded)
76 |
77 |
78 | def decode(args: argparse.Namespace):
79 | z = zerowidth.ZeroWidth()
80 |
81 | if args.file_source:
82 | decoded = z.zeroDecodeFile(source_path=args.file_source)
83 | else:
84 | decoded = z.zeroDecode(source=args.clear_source)
85 |
86 | if args.output_path:
87 | with open(args.output_path, "w") as f:
88 | f.write(decoded)
89 | return
90 |
91 | print(decoded)
92 |
93 |
94 | def main():
95 | parser = argparse.ArgumentParser(
96 | description="Hide and recover text in plainsight",
97 | )
98 | parser.add_argument("-V", "--version", help="library version", action="store_true")
99 | parser.add_argument("-E", "--encode", help="encode text", action="store_true")
100 | parser.add_argument("-D", "--decode", help="decode text", action="store_true")
101 | parser.add_argument("-C", "--clean", help="clear file", action="store_true")
102 | parser.add_argument("-t", "--to-hide", help="string to encode")
103 | parser.add_argument("-f", "--file-source", help="file source")
104 | parser.add_argument("-c", "--clear-source", help="clear string")
105 | parser.add_argument("-o", "--output-path", help="output path")
106 | parser.add_argument(
107 | "-p",
108 | "--position",
109 | help="hidden string position. 0 for TOP, 1 for BOTTOM, 2 for RANDOM, "
110 | "3 for NTHLINES, 4 for RANDOMINLINE",
111 | default=3,
112 | )
113 | parser.add_argument("-k", "-position-k", help="position variator", default=1)
114 |
115 | args = parser.parse_args()
116 |
117 | if not check_args(args):
118 | return
119 |
120 | if args.version:
121 | z = zerowidth.ZeroWidth()
122 | print(f"Zero width version: {z.version}")
123 | return
124 |
125 | if args.clean:
126 | clean(args)
127 | return
128 |
129 | if args.encode:
130 | encode(args)
131 | return
132 |
133 | if args.decode:
134 | decode(args)
135 | return
136 |
137 |
138 | if __name__ == "__main__":
139 | main()
140 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Zero Width Steganography
2 |
3 | `Steganography: the practice of concealing a file, message, image, or video within another file, message, image, or video.` ~ [Wikipedia](https://en.wikipedia.org/wiki/Steganography)
4 |
5 | ## Introduction
6 |
7 | Since its inception (happened in 1499 by German abbot *Johannes Trithemius*), steganography has always been regarded as a powerful method of hiding text in plain sight. The clear advantage over encryption it's that, if used correctly, will raise absolutely no suspicion.
8 |
9 |
10 | Steganography is normally used in photos. Nowadays, it takes very little so upload a photo into your favourite social network to sneeringly share some deep secret. But I wanted to take it a bit further.
11 | Why not text? What if i told you that somewhere in this paragraph, there's a secret message hiding? You couldn't tell, right?
12 |
13 | So this is how I got the inspiration. Neat, isn't it?
14 |
15 | ## Looking for the right way
16 |
17 | At this point I had to start tinkering on how I could realize this project.
18 | Snooping through Wikipedia, I started looking for characters that could be hidden inside text files without being too noticeable. Instantly, I was very surprising by discovering that Greek question marks (;) are very similar (if not completely unrecognizable!) to regular semicolons (;). I thought that maybe I could find a way to swap them in some clever way to hide text. But honestly, unless you are working with C/C++ code, how often do you use semicolons? (This similarity can be used to prank a programmer. Just replace every semicolon and watch him *-and his compiler-* go crazy)
19 |
20 | So at this point, I was lost. There are *1,112,064 different characters* encoded by UTF-8. I mean, it's bound to create some very similar symbols, right? Sadly, after scrolling a while (a very long while) through the list of every character it dawned to me that this couldn't be the right path.
21 |
22 | ## The discovery
23 |
24 | Almost ready to give up, my eyes noticed a very strange table named *General Punctuation*. There are some funky characters in there, including parenthesis and asterisks. But the real interesting feature in that table is *Whitespaces*.
25 |
26 | So, basically, *whitespaces* are special character that, as the name suggests, leave some space (usually, white) between characters. I started exploring them, toying with the idea of converting some text in space.
27 | This could actually work, with the small limitation of actually adding some visible spacing inside the "container" text.
28 |
29 | Some Googling later, I discovered that there are 2 different space characters that add no visible space to the file: `U+200B` **(ZERO WIDTH SPACE)** and `U+FEFF` **(ZERO WIDTH NO-BREAK SPACE)**. These characters are actually invisible to the user and don't show up while writing.
30 | I still don't really understand how they could be legitimately used inside a document, but I chose to ride with this.
31 |
32 | ## How I did it
33 |
34 | Now that I had everything, I could start working on the idea.
35 |
36 | The first thing that I had to do was encode the input string into UTF-8, convert it in binary and "translate" it into a "custom" binary system, using those two magic characters as 0 and 1. At this point, it's pretty easy to add the encoded string into a "container" file. This file is visually identical to the "source" file.
37 | To decode an encrypted string, I just need to convert those strange spaces back into binary and decode them from Unicode. Pretty awesome, right?
38 |
39 | ## The code
40 |
41 | I made two Python3 scripts:
42 |
43 | 1. `zerowidth.py` - the main module
44 | 2. `zero-width-steganography.py` - a wrapper, built around the aforementioned module, to quickly encode/decode/clean files
45 |
46 | Those next sections will explain how to use the second script.
47 |
48 | ### Encoding
49 |
50 | You can either provide a file text or a string as a source file. The hidden text will be hidden in here.
51 | Then you must provide some text that will be encrypted and hidden in the source. As for the source file, you can either supply it inline or load from a file.
52 |
53 | You must then provide a destination file that will contain the hidden text.
54 |
55 | The next step is to state where you want to put the hidden string inside the source text. The options are:
56 |
57 | * *TOP* - the hidden string will be put at the very beginning of the text file.
58 | * *BOTTOM* - the hidden string will be put at the end the text file.
59 | * *RANDOM* - the hidden text will be placed in random positions all through the document.
60 | * *NTHLINES* - the hidden text will be placed at the end of every nth line.
61 | * *RANDOMINLINE* - the hidden text will be placed in random positions in the lines of the source.
62 |
63 | If you either chose *RANDOM*, *NTHLINES* or *RANDOMINLINE* you can provide one more parameter (*k*) which represents number of occurrences (for *RANDOM* or *RANDOMINLINE*) or how many lines you want to skip at every iteration (for *NTHLINES*).
64 | Its value will default to 1.
65 |
66 | ### Decoding
67 |
68 | Like in *Encoding*, you must provide a file text as source and you can provide a file destination. If you chose to not do so, the decoded text (if found) will be shown in console.
69 |
70 | ### Cleaning
71 |
72 | The script also has the ability to clean a file containing hidden text. Just provide a source file and a destination. If you omit the destination, the source file will be overwritten.
73 |
74 | ## Examples
75 |
76 | * Hide the string `HELLO THERE, GENERAL KENOBI` at the end of the the file `obiwankenobi.txt` sourcing the text from `beemovie.txt`: `python3 zero-width-steganography.py -E -t "HELLO THERE GENERAL KENOBI" -f beemovie.txt -o obiwankenobi.txt`
77 | * Hide the string `I DON'T LIKE SAND` in 10 different positions inside the `coarse.txt` file and output to console:
78 | `python3 zero-width-steganography.py -E -t "I DON'T LIKE SAND" -f coarse.txt`
79 | * Find the hidden test from the file `shrek.txt` and output to console: `python3 zero-width-steganography.py -D -f shrek.txt`
80 | * Clear the file `cars-3-pixar-final` from all hidden strings: `python3 zero-width-steganography.py -C -f cars-3-pixar-final -o cars-3-pixar-final`
81 |
82 | ## Documentation
83 |
84 | The documentation can be found [here](https://lorossi.github.io/zero-width-steganography/).
85 |
86 | This code doesn't need any particular module. It will work with the pre installed packages.
87 |
--------------------------------------------------------------------------------
/zerowidth.py:
--------------------------------------------------------------------------------
1 | """Class handling the Steganography."""
2 |
3 | from random import randint
4 | from enum import Enum
5 |
6 |
7 | class Position(Enum):
8 | """
9 | Enum class handling the positioning of the hidden text.
10 |
11 | TOP: Before the first line of the text
12 | BOTTOM: After the first line of the text
13 | RANDOM: In a random line in the text
14 | NTHLINES: At the end of every nth-line, as specified by the parameter k
15 | RANDOMINLINE: In every line at a random position
16 | """
17 |
18 | TOP = 0
19 | BOTTOM = 1
20 | RANDOM = 2
21 | NTHLINES = 3
22 | RANDOMINLINE = 4
23 |
24 |
25 | class ZeroWidth:
26 | """Class handling the steganography embedding in files and strings."""
27 |
28 | def __init__(self) -> None:
29 | """Initialize a ZeroWidth instance."""
30 | self._version = "1.1"
31 | # maps bits to spaces
32 | self._character_map = {
33 | "0": "\u200B", # ZERO WIDTH SPACE
34 | "1": "\uFEFF", # ZERO WIDTH NO-BREAK SPACE
35 | }
36 |
37 | # reverses the "character_map" dict so we can map spaces to bits
38 | self._space_map = {v: k for k, v in self._character_map.items()}
39 | # get the special hidden characters
40 | self._hidden_characters = set(self._space_map.keys())
41 |
42 | def _spaceEncode(self, clear: str) -> str:
43 | """Encode the string into spaces.
44 |
45 | Args:
46 | clear (str): clear string to encode
47 |
48 | Returns:
49 | str
50 | """
51 | if len(clear) == 0:
52 | return ""
53 |
54 | binary = "".join(format(ord(c), "08b") for c in clear)
55 | return "".join(self._character_map[b] for b in binary)
56 |
57 | def _spaceDecode(self, encoded: str) -> str:
58 | """Decode the string from spaces.
59 |
60 | Args:
61 | encoded (str): encoded string
62 |
63 | Returns:
64 | str
65 | """
66 | if len(encoded) == 0:
67 | return ""
68 |
69 | binary = "".join(self._space_map[e] for e in encoded)
70 | decoded = "".join(
71 | chr(int(binary[x : x + 8], 2)) for x in range(0, len(encoded), 8)
72 | )
73 |
74 | return decoded
75 |
76 | def zeroEncode(
77 | self, source: str, clear: str, position: Position, k: int = 1
78 | ) -> str:
79 | """Encode clear string and hides into the source string in position according \
80 | to the parameter.
81 |
82 | Args:
83 | source (str): text that will contain the hidden strings
84 | clear (str): string to hide in the text
85 | position (Position): position of the hidden strings.
86 | k (int, optional): specifies:
87 | The occurrences of the encoded string in the clear text if Position is Random.
88 | The number of clear lines between hidden lines if Position is NTHLINE.
89 | Defaults to 1.
90 |
91 | Returns:
92 | str: string containing the encoded text
93 | """
94 | encoded = self._spaceEncode(clear)
95 |
96 | match position:
97 | case position.TOP:
98 | embedded = encoded + source
99 | case position.BOTTOM:
100 | embedded = source + encoded
101 | case position.RANDOM:
102 | count = 0
103 | while count < k:
104 | k = randint(0, len(source) - 1)
105 | if source[k] in self._hidden_characters:
106 | continue
107 | embedded = source[:k] + encoded + source[k:]
108 | count += 1
109 | case position.NTHLINES:
110 | lines = source.split("\n")
111 | for x in range(0, len(lines), k):
112 | lines[x] += encoded
113 | embedded = "\n".join(lines)
114 | case position.RANDOMINLINE:
115 | lines = source.split("\n")
116 | for x in range(0, len(lines), k):
117 | k = randint(0, len(lines[x]) - 1)
118 | lines[x] = lines[x][:k] + encoded + lines[x][k:]
119 | embedded = "\n".join(lines)
120 |
121 | return embedded
122 |
123 | def zeroDecode(self, source: str) -> str:
124 | """Decode an hidden string.
125 |
126 | Args:
127 | source (str): text containing hidden strings
128 |
129 | Returns:
130 | str
131 | """
132 | encoded = "".join(s for s in source if s in self._hidden_characters)
133 | return self._spaceDecode(encoded)
134 |
135 | def zeroEncodeFile(
136 | self, source_path: str, clear: str, position: Position, k: int = 1
137 | ) -> None:
138 | """Encode string using a file for source.
139 |
140 | Args:
141 | source_path (str): path of source file
142 | clear (str): string to hide in the text
143 | position (Position): position of the hidden strings.
144 | k (int, optional): specifies:
145 | The occurrences of the encoded string in the clear text if Position is Random.
146 | The number of clear lines between hidden lines if Position is NTHLINE.
147 | Defaults to 1.
148 | """
149 | with open(source_path, "r") as f:
150 | source = f.read()
151 |
152 | return self.zeroEncode(source=source, clear=clear, position=position, k=k)
153 |
154 | def zeroDecodeFile(self, source_path: str) -> str:
155 | """Decode an hidden string in a file.
156 |
157 | Args:
158 | source_path (str): path of the file containing the hidden string
159 |
160 | Returns:
161 | str: decoded string
162 | """
163 | with open(source_path, "r") as f:
164 | source = f.read()
165 |
166 | return self.zeroDecode(source)
167 |
168 | def cleanString(self, source: str) -> str:
169 | """Clean a string from hidden characters.
170 |
171 | Args:
172 | source (str): string containing hidden characters
173 |
174 | Returns:
175 | str
176 | """
177 | return "".join(s for s in source if s not in self._hidden_characters)
178 |
179 | def cleanFile(self, source_path: str) -> str:
180 | """Clean a string from hidden characters, using a file as source.
181 |
182 | Args:
183 | source_path (str): path of the file
184 |
185 | Returns:
186 | str
187 | """
188 | with open(source_path, "r") as f:
189 | source = f.read()
190 |
191 | return self.cleanString(source)
192 |
193 | @property
194 | def version(self) -> str:
195 | """Return current version.
196 |
197 | Returns:
198 | str
199 | """
200 | return self._version
201 |
--------------------------------------------------------------------------------
/docs/zerowidth.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
242 | class Position
243 | (value, names=None, *, module=None, qualname=None, type=None, start=1)
244 |
245 |
246 |
Enum class handling the positioning of the hidden text.
247 |
TOP: Before the first line of the text
248 | BOTTOM: After the first line of the text
249 | RANDOM: In a random line in the text
250 | NTHLINES: At the end of every nth-line, as specified by the parameter k
251 | RANDOMINLINE: In every line at a random position
252 |
253 |
254 |
255 | Expand source code
256 |
257 |
class Position(Enum):
258 | """
259 | Enum class handling the positioning of the hidden text.
260 |
261 | TOP: Before the first line of the text
262 | BOTTOM: After the first line of the text
263 | RANDOM: In a random line in the text
264 | NTHLINES: At the end of every nth-line, as specified by the parameter k
265 | RANDOMINLINE: In every line at a random position
266 | """
267 |
268 | TOP = 0
269 | BOTTOM = 1
270 | RANDOM = 2
271 | NTHLINES = 3
272 | RANDOMINLINE = 4
273 |
274 |
Ancestors
275 |
276 |
enum.Enum
277 |
278 |
Class variables
279 |
280 |
var BOTTOM
281 |
282 |
283 |
284 |
var NTHLINES
285 |
286 |
287 |
288 |
var RANDOM
289 |
290 |
291 |
292 |
var RANDOMINLINE
293 |
294 |
295 |
296 |
var TOP
297 |
298 |
299 |
300 |
301 |
302 |
303 | class ZeroWidth
304 |
305 |
306 |
Class handling the steganography embedding in files and strings.
307 |
Initialize a ZeroWidth instance.
308 |
309 |
310 | Expand source code
311 |
312 |
class ZeroWidth:
313 | """Class handling the steganography embedding in files and strings."""
314 |
315 | def __init__(self) -> None:
316 | """Initialize a ZeroWidth instance."""
317 | self._version = "1.1"
318 | # maps bits to spaces
319 | self._character_map = {
320 | "0": "\u200B", # ZERO WIDTH SPACE
321 | "1": "\uFEFF", # ZERO WIDTH NO-BREAK SPACE
322 | }
323 |
324 | # reverses the "character_map" dict so we can map spaces to bits
325 | self._space_map = {v: k for k, v in self._character_map.items()}
326 | # get the special hidden characters
327 | self._hidden_characters = set(self._space_map.keys())
328 |
329 | def _spaceEncode(self, clear: str) -> str:
330 | """Encode the string into spaces.
331 |
332 | Args:
333 | clear (str): clear string to encode
334 |
335 | Returns:
336 | str
337 | """
338 | if len(clear) == 0:
339 | return ""
340 |
341 | binary = "".join(format(ord(c), "08b") for c in clear)
342 | return "".join(self._character_map[b] for b in binary)
343 |
344 | def _spaceDecode(self, encoded: str) -> str:
345 | """Decode the string from spaces.
346 |
347 | Args:
348 | encoded (str): encoded string
349 |
350 | Returns:
351 | str
352 | """
353 | if len(encoded) == 0:
354 | return ""
355 |
356 | binary = "".join(self._space_map[e] for e in encoded)
357 | decoded = "".join(
358 | chr(int(binary[x : x + 8], 2)) for x in range(0, len(encoded), 8)
359 | )
360 |
361 | return decoded
362 |
363 | def zeroEncode(
364 | self, source: str, clear: str, position: Position, k: int = 1
365 | ) -> str:
366 | """Encode clear string and hides into the source string in position according \
367 | to the parameter.
368 |
369 | Args:
370 | source (str): text that will contain the hidden strings
371 | clear (str): string to hide in the text
372 | position (Position): position of the hidden strings.
373 | k (int, optional): specifies:
374 | The occurrences of the encoded string in the clear text if Position is Random.
375 | The number of clear lines between hidden lines if Position is NTHLINE.
376 | Defaults to 1.
377 |
378 | Returns:
379 | str: string containing the encoded text
380 | """
381 | encoded = self._spaceEncode(clear)
382 |
383 | match position:
384 | case position.TOP:
385 | embedded = encoded + source
386 | case position.BOTTOM:
387 | embedded = source + encoded
388 | case position.RANDOM:
389 | count = 0
390 | while count < k:
391 | k = randint(0, len(source) - 1)
392 | if source[k] in self._hidden_characters:
393 | continue
394 | embedded = source[:k] + encoded + source[k:]
395 | count += 1
396 | case position.NTHLINES:
397 | lines = source.split("\n")
398 | for x in range(0, len(lines), k):
399 | lines[x] += encoded
400 | embedded = "\n".join(lines)
401 | case position.RANDOMINLINE:
402 | lines = source.split("\n")
403 | for x in range(0, len(lines), k):
404 | k = randint(0, len(lines[x]) - 1)
405 | lines[x] = lines[x][:k] + encoded + lines[x][k:]
406 | embedded = "\n".join(lines)
407 |
408 | return embedded
409 |
410 | def zeroDecode(self, source: str) -> str:
411 | """Decode an hidden string.
412 |
413 | Args:
414 | source (str): text containing hidden strings
415 |
416 | Returns:
417 | str
418 | """
419 | encoded = "".join(s for s in source if s in self._hidden_characters)
420 | return self._spaceDecode(encoded)
421 |
422 | def zeroEncodeFile(
423 | self, source_path: str, clear: str, position: Position, k: int = 1
424 | ) -> None:
425 | """Encode string using a file for source.
426 |
427 | Args:
428 | source_path (str): path of source file
429 | clear (str): string to hide in the text
430 | position (Position): position of the hidden strings.
431 | k (int, optional): specifies:
432 | The occurrences of the encoded string in the clear text if Position is Random.
433 | The number of clear lines between hidden lines if Position is NTHLINE.
434 | Defaults to 1.
435 | """
436 | with open(source_path, "r") as f:
437 | source = f.read()
438 |
439 | return self.zeroEncode(source=source, clear=clear, position=position, k=k)
440 |
441 | def zeroDecodeFile(self, source_path: str) -> str:
442 | """Decode an hidden string in a file.
443 |
444 | Args:
445 | source_path (str): path of the file containing the hidden string
446 |
447 | Returns:
448 | str: decoded string
449 | """
450 | with open(source_path, "r") as f:
451 | source = f.read()
452 |
453 | return self.zeroDecode(source)
454 |
455 | def cleanString(self, source: str) -> str:
456 | """Clean a string from hidden characters.
457 |
458 | Args:
459 | source (str): string containing hidden characters
460 |
461 | Returns:
462 | str
463 | """
464 | return "".join(s for s in source if s not in self._hidden_characters)
465 |
466 | def cleanFile(self, source_path: str) -> str:
467 | """Clean a string from hidden characters, using a file as source.
468 |
469 | Args:
470 | source_path (str): path of the file
471 |
472 | Returns:
473 | str
474 | """
475 | with open(source_path, "r") as f:
476 | source = f.read()
477 |
478 | return self.cleanString(source)
479 |
480 | @property
481 | def version(self) -> str:
482 | """Return current version.
483 |
484 | Returns:
485 | str
486 | """
487 | return self._version
specifies:
651 | The occurrences of the encoded string in the clear text if Position is Random.
652 | The number of clear lines between hidden lines if Position is NTHLINE.
653 |
654 |
Defaults to 1.
655 |
Returns
656 |
657 |
str
658 |
string containing the encoded text
659 |
660 |
661 |
662 | Expand source code
663 |
664 |
def zeroEncode(
665 | self, source: str, clear: str, position: Position, k: int = 1
666 | ) -> str:
667 | """Encode clear string and hides into the source string in position according \
668 | to the parameter.
669 |
670 | Args:
671 | source (str): text that will contain the hidden strings
672 | clear (str): string to hide in the text
673 | position (Position): position of the hidden strings.
674 | k (int, optional): specifies:
675 | The occurrences of the encoded string in the clear text if Position is Random.
676 | The number of clear lines between hidden lines if Position is NTHLINE.
677 | Defaults to 1.
678 |
679 | Returns:
680 | str: string containing the encoded text
681 | """
682 | encoded = self._spaceEncode(clear)
683 |
684 | match position:
685 | case position.TOP:
686 | embedded = encoded + source
687 | case position.BOTTOM:
688 | embedded = source + encoded
689 | case position.RANDOM:
690 | count = 0
691 | while count < k:
692 | k = randint(0, len(source) - 1)
693 | if source[k] in self._hidden_characters:
694 | continue
695 | embedded = source[:k] + encoded + source[k:]
696 | count += 1
697 | case position.NTHLINES:
698 | lines = source.split("\n")
699 | for x in range(0, len(lines), k):
700 | lines[x] += encoded
701 | embedded = "\n".join(lines)
702 | case position.RANDOMINLINE:
703 | lines = source.split("\n")
704 | for x in range(0, len(lines), k):
705 | k = randint(0, len(lines[x]) - 1)
706 | lines[x] = lines[x][:k] + encoded + lines[x][k:]
707 | embedded = "\n".join(lines)
708 |
709 | return embedded
specifies:
727 | The occurrences of the encoded string in the clear text if Position is Random.
728 | The number of clear lines between hidden lines if Position is NTHLINE.
729 |
730 |
Defaults to 1.
731 |
732 |
733 | Expand source code
734 |
735 |
def zeroEncodeFile(
736 | self, source_path: str, clear: str, position: Position, k: int = 1
737 | ) -> None:
738 | """Encode string using a file for source.
739 |
740 | Args:
741 | source_path (str): path of source file
742 | clear (str): string to hide in the text
743 | position (Position): position of the hidden strings.
744 | k (int, optional): specifies:
745 | The occurrences of the encoded string in the clear text if Position is Random.
746 | The number of clear lines between hidden lines if Position is NTHLINE.
747 | Defaults to 1.
748 | """
749 | with open(source_path, "r") as f:
750 | source = f.read()
751 |
752 | return self.zeroEncode(source=source, clear=clear, position=position, k=k)