├── .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 | 4 | 5 | 6 | 7 | zerowidth API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module zerowidth

23 |
24 |
25 |

Class handling the Steganography.

26 |
27 | 28 | Expand source code 29 | 30 |
"""Class handling the Steganography."""
 31 | 
 32 | from random import randint
 33 | from enum import Enum
 34 | 
 35 | 
 36 | class Position(Enum):
 37 |     """
 38 |     Enum class handling the positioning of the hidden text.
 39 | 
 40 |         TOP: Before the first line of the text
 41 |         BOTTOM: After the first line of the text
 42 |         RANDOM: In a random line in the text
 43 |         NTHLINES: At the end of every nth-line, as specified by the parameter k
 44 |         RANDOMINLINE: In every line at a random position
 45 |     """
 46 | 
 47 |     TOP = 0
 48 |     BOTTOM = 1
 49 |     RANDOM = 2
 50 |     NTHLINES = 3
 51 |     RANDOMINLINE = 4
 52 | 
 53 | 
 54 | class ZeroWidth:
 55 |     """Class handling the steganography embedding in files and strings."""
 56 | 
 57 |     def __init__(self) -> None:
 58 |         """Initialize a ZeroWidth instance."""
 59 |         self._version = "1.1"
 60 |         # maps bits to spaces
 61 |         self._character_map = {
 62 |             "0": "\u200B",  # ZERO WIDTH SPACE
 63 |             "1": "\uFEFF",  # ZERO WIDTH NO-BREAK SPACE
 64 |         }
 65 | 
 66 |         # reverses the "character_map" dict so we can map spaces to bits
 67 |         self._space_map = {v: k for k, v in self._character_map.items()}
 68 |         # get the special hidden characters
 69 |         self._hidden_characters = set(self._space_map.keys())
 70 | 
 71 |     def _spaceEncode(self, clear: str) -> str:
 72 |         """Encode the string into spaces.
 73 | 
 74 |         Args:
 75 |             clear (str): clear string to encode
 76 | 
 77 |         Returns:
 78 |             str
 79 |         """
 80 |         if len(clear) == 0:
 81 |             return ""
 82 | 
 83 |         binary = "".join(format(ord(c), "08b") for c in clear)
 84 |         return "".join(self._character_map[b] for b in binary)
 85 | 
 86 |     def _spaceDecode(self, encoded: str) -> str:
 87 |         """Decode the string from spaces.
 88 | 
 89 |         Args:
 90 |             encoded (str): encoded string
 91 | 
 92 |         Returns:
 93 |             str
 94 |         """
 95 |         if len(encoded) == 0:
 96 |             return ""
 97 | 
 98 |         binary = "".join(self._space_map[e] for e in encoded)
 99 |         decoded = "".join(
100 |             chr(int(binary[x : x + 8], 2)) for x in range(0, len(encoded), 8)
101 |         )
102 | 
103 |         return decoded
104 | 
105 |     def zeroEncode(
106 |         self, source: str, clear: str, position: Position, k: int = 1
107 |     ) -> str:
108 |         """Encode clear string and hides into the source string in position according \
109 |         to the parameter.
110 | 
111 |         Args:
112 |             source (str): text that will contain the hidden strings
113 |             clear (str): string to hide in the text
114 |             position (Position): position of the hidden strings.
115 |             k (int, optional): specifies:
116 |                 The occurrences of the encoded string in the clear text if Position is Random.
117 |                 The number of clear lines between hidden lines if Position is NTHLINE.
118 |             Defaults to 1.
119 | 
120 |         Returns:
121 |             str: string containing the encoded text
122 |         """
123 |         encoded = self._spaceEncode(clear)
124 | 
125 |         match position:
126 |             case position.TOP:
127 |                 embedded = encoded + source
128 |             case position.BOTTOM:
129 |                 embedded = source + encoded
130 |             case position.RANDOM:
131 |                 count = 0
132 |                 while count < k:
133 |                     k = randint(0, len(source) - 1)
134 |                     if source[k] in self._hidden_characters:
135 |                         continue
136 |                     embedded = source[:k] + encoded + source[k:]
137 |                     count += 1
138 |             case position.NTHLINES:
139 |                 lines = source.split("\n")
140 |                 for x in range(0, len(lines), k):
141 |                     lines[x] += encoded
142 |                 embedded = "\n".join(lines)
143 |             case position.RANDOMINLINE:
144 |                 lines = source.split("\n")
145 |                 for x in range(0, len(lines), k):
146 |                     k = randint(0, len(lines[x]) - 1)
147 |                     lines[x] = lines[x][:k] + encoded + lines[x][k:]
148 |                 embedded = "\n".join(lines)
149 | 
150 |         return embedded
151 | 
152 |     def zeroDecode(self, source: str) -> str:
153 |         """Decode an hidden string.
154 | 
155 |         Args:
156 |             source (str): text containing hidden strings
157 | 
158 |         Returns:
159 |             str
160 |         """
161 |         encoded = "".join(s for s in source if s in self._hidden_characters)
162 |         return self._spaceDecode(encoded)
163 | 
164 |     def zeroEncodeFile(
165 |         self, source_path: str, clear: str, position: Position, k: int = 1
166 |     ) -> None:
167 |         """Encode string using a file for source.
168 | 
169 |         Args:
170 |             source_path (str): path of source file
171 |             clear (str): string to hide in the text
172 |             position (Position): position of the hidden strings.
173 |             k (int, optional): specifies:
174 |                 The occurrences of the encoded string in the clear text if Position is Random.
175 |                 The number of clear lines between hidden lines if Position is NTHLINE.
176 |             Defaults to 1.
177 |         """
178 |         with open(source_path, "r") as f:
179 |             source = f.read()
180 | 
181 |         return self.zeroEncode(source=source, clear=clear, position=position, k=k)
182 | 
183 |     def zeroDecodeFile(self, source_path: str) -> str:
184 |         """Decode an hidden string in a file.
185 | 
186 |         Args:
187 |             source_path (str): path of the file containing the hidden string
188 | 
189 |         Returns:
190 |             str: decoded string
191 |         """
192 |         with open(source_path, "r") as f:
193 |             source = f.read()
194 | 
195 |         return self.zeroDecode(source)
196 | 
197 |     def cleanString(self, source: str) -> str:
198 |         """Clean a string from hidden characters.
199 | 
200 |         Args:
201 |             source (str): string containing hidden characters
202 | 
203 |         Returns:
204 |             str
205 |         """
206 |         return "".join(s for s in source if s not in self._hidden_characters)
207 | 
208 |     def cleanFile(self, source_path: str) -> str:
209 |         """Clean a string from hidden characters, using a file as source.
210 | 
211 |         Args:
212 |             source_path (str): path of the file
213 | 
214 |         Returns:
215 |             str
216 |         """
217 |         with open(source_path, "r") as f:
218 |             source = f.read()
219 | 
220 |         return self.cleanString(source)
221 | 
222 |     @property
223 |     def version(self) -> str:
224 |         """Return current version.
225 | 
226 |         Returns:
227 |             str
228 |         """
229 |         return self._version
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |

Classes

240 |
241 |
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
488 |
489 |

Instance variables

490 |
491 |
var version : str
492 |
493 |

Return current version.

494 |

Returns

495 |

str

496 |
497 | 498 | Expand source code 499 | 500 |
@property
501 | def version(self) -> str:
502 |     """Return current version.
503 | 
504 |     Returns:
505 |         str
506 |     """
507 |     return self._version
508 |
509 |
510 |
511 |

Methods

512 |
513 |
514 | def cleanFile(self, source_path: str) ‑> str 515 |
516 |
517 |

Clean a string from hidden characters, using a file as source.

518 |

Args

519 |
520 |
source_path : str
521 |
path of the file
522 |
523 |

Returns

524 |

str

525 |
526 | 527 | Expand source code 528 | 529 |
def cleanFile(self, source_path: str) -> str:
530 |     """Clean a string from hidden characters, using a file as source.
531 | 
532 |     Args:
533 |         source_path (str): path of the file
534 | 
535 |     Returns:
536 |         str
537 |     """
538 |     with open(source_path, "r") as f:
539 |         source = f.read()
540 | 
541 |     return self.cleanString(source)
542 |
543 |
544 |
545 | def cleanString(self, source: str) ‑> str 546 |
547 |
548 |

Clean a string from hidden characters.

549 |

Args

550 |
551 |
source : str
552 |
string containing hidden characters
553 |
554 |

Returns

555 |

str

556 |
557 | 558 | Expand source code 559 | 560 |
def cleanString(self, source: str) -> str:
561 |     """Clean a string from hidden characters.
562 | 
563 |     Args:
564 |         source (str): string containing hidden characters
565 | 
566 |     Returns:
567 |         str
568 |     """
569 |     return "".join(s for s in source if s not in self._hidden_characters)
570 |
571 |
572 |
573 | def zeroDecode(self, source: str) ‑> str 574 |
575 |
576 |

Decode an hidden string.

577 |

Args

578 |
579 |
source : str
580 |
text containing hidden strings
581 |
582 |

Returns

583 |

str

584 |
585 | 586 | Expand source code 587 | 588 |
def zeroDecode(self, source: str) -> str:
589 |     """Decode an hidden string.
590 | 
591 |     Args:
592 |         source (str): text containing hidden strings
593 | 
594 |     Returns:
595 |         str
596 |     """
597 |     encoded = "".join(s for s in source if s in self._hidden_characters)
598 |     return self._spaceDecode(encoded)
599 |
600 |
601 |
602 | def zeroDecodeFile(self, source_path: str) ‑> str 603 |
604 |
605 |

Decode an hidden string in a file.

606 |

Args

607 |
608 |
source_path : str
609 |
path of the file containing the hidden string
610 |
611 |

Returns

612 |
613 |
str
614 |
decoded string
615 |
616 |
617 | 618 | Expand source code 619 | 620 |
def zeroDecodeFile(self, source_path: str) -> str:
621 |     """Decode an hidden string in a file.
622 | 
623 |     Args:
624 |         source_path (str): path of the file containing the hidden string
625 | 
626 |     Returns:
627 |         str: decoded string
628 |     """
629 |     with open(source_path, "r") as f:
630 |         source = f.read()
631 | 
632 |     return self.zeroDecode(source)
633 |
634 |
635 |
636 | def zeroEncode(self, source: str, clear: str, position: Position, k: int = 1) ‑> str 637 |
638 |
639 |

Encode clear string and hides into the source string in position according 640 | to the parameter.

641 |

Args

642 |
643 |
source : str
644 |
text that will contain the hidden strings
645 |
clear : str
646 |
string to hide in the text
647 |
position : Position
648 |
position of the hidden strings.
649 |
k : int, optional
650 |
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
710 |
711 |
712 |
713 | def zeroEncodeFile(self, source_path: str, clear: str, position: Position, k: int = 1) ‑> None 714 |
715 |
716 |

Encode string using a file for source.

717 |

Args

718 |
719 |
source_path : str
720 |
path of source file
721 |
clear : str
722 |
string to hide in the text
723 |
position : Position
724 |
position of the hidden strings.
725 |
k : int, optional
726 |
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)
753 |
754 |
755 |
756 |
757 |
758 |
759 |
760 | 794 |
795 | 798 | 799 | --------------------------------------------------------------------------------