Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5557a02
Initial commit
JoseALermaIII Sep 16, 2019
c605570
Complete check_fit
JoseALermaIII Sep 16, 2019
bc886a5
Initial commit
JoseALermaIII Sep 16, 2019
2f732bb
Initialize TestInvisibleInkMono with test_check_fit
JoseALermaIII Sep 16, 2019
23980f0
Refactor check_fit using generator expressions and sum
JoseALermaIII Sep 16, 2019
569ac7f
Refactor note section in write_invisible to use platform specific fonts
JoseALermaIII Sep 16, 2019
5f75a0c
Separate local module imports
JoseALermaIII Sep 16, 2019
2100ef7
Complete write_invisible
JoseALermaIII Sep 17, 2019
a063e23
Add test_write_invisible to TestInvisibleInkMono
JoseALermaIII Sep 18, 2019
bdbca18
Fix font name test in test_write_invisible from TestInvisibleInkMono
JoseALermaIII Sep 20, 2019
5502d13
Add multi-line ciphertext test in test_write_invisible from TestInvis…
JoseALermaIII Sep 20, 2019
766f91a
Fix multi-line ciphertext in write_invisible
JoseALermaIII Sep 20, 2019
e47a620
Merge branch 'master' into invisible-ink-mono
JoseALermaIII Oct 2, 2019
b77f1e9
Complete main
JoseALermaIII Oct 2, 2019
bf711b4
Add output of c1_invisible_ink_mono
JoseALermaIII Oct 2, 2019
1e42ccd
Initial commit
JoseALermaIII Oct 2, 2019
0468766
Fix duplicate-code per pylint R0801
JoseALermaIII Oct 2, 2019
af9a5e7
Initial commit
JoseALermaIII Oct 2, 2019
d1cfb29
Add test_main to TestInvisibleInkMono
JoseALermaIII Oct 2, 2019
cf36258
Move constants to constants.py
JoseALermaIII Oct 2, 2019
97214aa
Add c1_invisible_ink_mono
JoseALermaIII Oct 2, 2019
f6a45a7
Fix broken sphinx references in check_fit docstring
JoseALermaIII Oct 2, 2019
8cd57db
Fix broken sphinx references in write_invisible docstring
JoseALermaIII Oct 2, 2019
f4a7aca
Fix typo in main docstring
JoseALermaIII Oct 2, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ build/
# Chapter 6
output.docx
LetterToUSDA.docx
DearInternet.docx
8 changes: 8 additions & 0 deletions docs/source/src.ch06.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ src.ch06 package
Submodules
----------

src.ch06.c1\_invisible\_ink\_mono module
----------------------------------------

.. automodule:: src.ch06.c1_invisible_ink_mono
:members:
:undoc-members:
:show-inheritance:

src.ch06.p1\_invisible\_ink module
----------------------------------

Expand Down
179 changes: 179 additions & 0 deletions src/ch06/c1_invisible_ink_mono.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
"""Use stenography to hide messages in a word processor document.

Use :py:obj:`docx.Document` to hide encrypted messages in a word processor
document by embedding the encrypted message in a fake message's whitespace,
then changing the encrypted message's font color to white.

Note:
Using LibreOffice version 6.0.7.3

Warning:
There are many ways this method of stenography can fail. Please don't use
for actual covert operations (covered in MIT License).

"""
from pathlib import Path, PurePath
from platform import system

import docx
from docx.shared import RGBColor

from src.ch06.p1_invisible_ink import get_text


def check_fit(plaintext: list, ciphertext: list) -> int:
"""Check if ciphertext can fit in plaintext's whitespace.

Sum number of blanks in **plaintext** and compare to number of characters
in **ciphertext** to see if it can fit.

Args:
plaintext (list): Paragraphs of a fake message in a list of strings
(likely from :func:`~src.ch06.p1_invisible_ink.get_text`).
ciphertext (list): Paragraphs of an encrypted message in a list of
strings (likely from :func:`~src.ch06.p1_invisible_ink.get_text`).

Returns:
Integer representing the number of needed blanks to fit
**ciphertext** in **plaintext**. ``0`` would mean that **ciphertext**
can fit in **plaintext**.

Note:
To separate words, the blanks in **ciphertext** count toward the
needed length of **plaintext**. By contrast, blank lines in
**plaintext** do not count.

"""
blanks = sum(line.count(' ') for line in plaintext if line != '')
letters = sum(len(line) for line in ciphertext if line != '')
if blanks >= letters:
return 0
return letters - blanks


def write_invisible(plaintext: list, ciphertext: list,
template_path: str = None,
filename: str = 'output.docx') -> None:
"""Embed ciphertext in plaintext's letter whitespace.

Open a template file, **template_path**, with the needed fonts, styles,
and margins. Write each line in **plaintext** to the template file
and add each line in **ciphertext** to **plaintext**'s space between
letters by using a monospace font.
Save the new file as **filename**.

Args:
plaintext (list): Lines of a fake message in a list of strings
(likely from :func:`~src.ch06.p1_invisible_ink.get_text`).
ciphertext (list): Lines of an encrypted message in a list of
strings (likely from :func:`~src.ch06.p1_invisible_ink.get_text`).
template_path (str): Absolute path to .docx file with predefined
fonts, styles, and margins. Defaults to :py:obj:`None`. If not
provided, defaults will be created.
filename (str): File name to use for output file. Defaults to
``output.docx``.

Returns:
:py:obj:`None`. **plaintext** is written to the file at
**template_path** with **ciphertext** embedded in the blank space.

Raises:
ValueError: If the number of spaces in **plaintext** aren't
enough to embed **ciphertext** based on output of
:func:`check_fit`.

Note:
As of python-docx v0.8.10, creating custom styles isn't well
supported. More info `here`_.

As a result, if a template isn't provided, the default template is
modified to use a font named ``Courier New`` on Windows and
``Liberation Mono`` on other operating systems in the ``Normal``
style.

.. _here:
https://python-docx.readthedocs.io/en/latest/user/styles-understanding.html

"""
blanks_needed = check_fit(plaintext, ciphertext)
if blanks_needed > 0:
raise ValueError(f'Need {blanks_needed} more spaces in the plaintext '
f'(fake) message.')
if template_path is None:
# Modify default template.
doc = docx.Document()
style = doc.styles['Normal']
font = style.font
if system().lower().startswith('windows'):
font.name = 'Courier New'
else:
font.name = 'Liberation Mono'
else:
doc = docx.Document(template_path)

line_index, letter_index = 0, 0
for line in plaintext:
# Add new paragraph to template.
paragraph = doc.add_paragraph()
paragraph_index = len(doc.paragraphs) - 1
for letter in line:
# Add each letter to paragraph.
if all([letter == ' ',
letter_index < len(ciphertext[line_index])]):
# Add real message to space and set color to white.
paragraph.add_run(ciphertext[line_index][letter_index])
run = doc.paragraphs[paragraph_index].runs[-1]
font = run.font
# Make red for testing.
font.color.rgb = RGBColor(255, 255, 255)
letter_index += 1
else:
paragraph.add_run(letter)
if all([letter_index >= len(ciphertext[line_index]),
line_index < len(ciphertext) - 1]):
# Go to next line in ciphertext if end reached.
line_index += 1
letter_index = 0
doc.save(filename)


def main(fakefile: str = None, cipherfile: str = None,
savepath: str = None) -> None:
"""Demonstrate the invisible ink writer.

Demonstrate :func:`write_invisible`, but for testing,
it is a basic wrapper function for :func:`write_invisible`.
Embed **cipherfile** in **fakefile**'s whitespace.

Args:
fakefile (str): Path to .docx file with fake message.
Defaults to ``./c1files/fake.docx``.
cipherfile (str): Path to .docx file with real message.
Defaults to ``./c1files/real.docx``.
savepath (str): Path to .docx file for output.
Defaults to ``./c1files/DearInternet.docx``.

Returns:
:py:obj:`None`. The contents of **cipherfile**'s text is embedded
in **fakefile**'s whitespace and saved to **savepath**.

"""
print('I can embed a hidden message in a .docx file\'s white space '
'between letters by making the font\ncolor white. It\'s far less '
'bulletproof than it sounds.\n')
current_dir = Path('./c1files').resolve()
if fakefile is None or cipherfile is None:
fakefile = PurePath(current_dir).joinpath('fake.docx')
cipherfile = PurePath(current_dir).joinpath('real.docx')
if savepath is None:
savepath = PurePath(current_dir).joinpath('DearInternet.docx')
faketext = get_text(fakefile, False)
ciphertext = get_text(cipherfile)
write_invisible(faketext, ciphertext, None, savepath)
print('Fin.\n')
print('To read the hidden message, select the entire document and\n'
'highlight it a darkish gray.')


if __name__ == '__main__':
main()
Binary file added src/ch06/c1files/fake.docx
Binary file not shown.
Binary file added src/ch06/c1files/real.docx
Binary file not shown.
Empty file added tests/data/__init__.py
Empty file.
Empty file added tests/data/ch06/__init__.py
Empty file.
Binary file added tests/data/ch06/cipher_mono.docx
Binary file not shown.
111 changes: 111 additions & 0 deletions tests/data/ch06/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""Test Chapter 6 Constants.

Constants for test_chapter06.py.

Attributes:
GET_TEST (list): List of strings with expected output of
tests.test_chapter06.TestInvisibleInk.test_get_text test values.
WRITE_DEFAULT_MONO (list): List of strings with expected output of
src.ch06.c1_invisible_ink_mono.write_invisible default values.
WRITE_TEST_MONO (list): List of strings with expected output of
tests.test_chapter06.TestInvisibleInkMono.test_write_invisible test
values.
MAIN_TEST_MONO (list): List of strings with expected output of
tests.test_chapter06.TestInvisibleInkMono.test_main test values.
MAIN_DEFAULT_MONO (list): List of strings with expected output of
src.ch06.c1_invisible_ink_mono.main default values.

"""
GET_TEST = [
'This is a test document.',
'This is a paragraph with two runs. However, it’s not because it has two '
'lines.',
'There is intentionally a lot of blank spaces to check if the code can count '
'them correctly.',
'So, don’t send me e-mails saying that the formatting in my test files is '
'incorrect.',
'Word.']

WRITE_DEFAULT_MONO = [
'ThisTishaitestsdocument withiaslot ofsfiller,'
'uorpunnecessarypwordiness.',
'Please,otrysnotetodwrite liketthisobecause itbiseas annoyingaas '
'itsishunnecessary.',
'Unless,oofrcourse,tyou,are '
'writinguantypeeofncharactercthatrisywordypandtusesewordsd'
'unnecessarily.',
'In thatmrare,euncommonsinstance,sitaisgperfectlyepermissible.'
'to be wordy.']

WRITE_TEST_MONO = [
'ThisTishaitestsdocument withiaslot ofsfiller,'
'uorpunnecessarypwordiness.',
'Please,otrysnotetodwrite liketthisobecause itbiseas annoyingaas '
'itsishunnecessary.',
'Unless,oofrcourse,tyou,are '
'writinguantypeeofncharactercthatrisywordypandtusesewordsd'
'unnecessarily.',
'In thatmrare,euncommonsinstance,sitaisgperfectlyepermissible.'
'toHbeiwordy.']

MAIN_TEST_MONO = [
'ThisTishaitestsdocument withiaslot ofsfiller,uorpunnecessarypwordiness.',
'Please,otrysnotetodwrite liketthisobecause itbiseas annoyingaas '
'itsishunnecessary.',
'Unless,oofrcourse,tyou,are '
'writinguantypeeofncharactercthatrisywordypandtusesewordsdunnecessarily.',
'In thatmrare,euncommonsinstance,sitaisgperfectlyepermissible.to be wordy.']

MAIN_DEFAULT_MONO = [
'DearYInternet,',
'Schoolotaughtumerthat yourstartedeoutaaslARPANET: '
'twoncomputerasystemsm(specifically,eUCLA’s NetworkiMeasurementsCenter '
'andaSRI’s '
'NLSpsystem)oconnectedrtogethertwaymbackaonnOctobert29,e1969.aIurecently '
'learnedothefTCP/IP '
'standardiwasn’tnadoptedtuntileMarchr1982candothatnaccessntoethecTCP/IPtnetworkewasn’tdexpanded '
'untiln1986ewhentthewNSFoprovidedraccesskfor.researchers '
'toYconnectotousupercomputer sitesainrtheeUnited Statesaat '
'thenblazingespeedtofw56okbit/s.\n'
'\n'
'Surprisingly,rcommercialkISPs didn’tocomefaround '
'untilntheelatet1980swandoearlyr1990s,katswhich '
'pointtthehARPANETawastdecommissioned.',
'I remembercinothenearlys1990sithatsusingtascomputer tooinstantfmessage '
'someoneponrtheiothervsideaofttheeworld,was '
'apbigudealbbecauselthatiwascas,close '
'toarealctimeaasdpossibleewithoutmrackingiupca,major '
'longbdistanceubillsonithenlandline.eAlthough,sIsam,curious '
'whatathenlatencydwas backgthen…',
'Theseodays,vyouearerprovidingnusmaewayntotwirelessly '
'usenpocketecomputersttowcheckoinrtokoursfavorite coffeeoshop,fstream '
'musiclandovideocwhilealaughinglat danktmemesoon '
'socialgmedia,landopaybouratablwith astapcusingoourpvirtualewallet.',
'You,connect solmanyicomputersntogetherkthateadnew IPbprotocolyhad toaget '
'standardizedbtorsupportothemaalldas theyacontinuertorgrowainynumber. '
'Everythingofromfa '
'whiteepaperlonecuttingcedgetresearchrtooannoldiblogcabout,cats '
'havewtheiriownrIPeaddress.lNotetosmentionsall,the peopleaconnectingntodread '
'moreoaboutpthem.',
'Inttheifuture,cweawillllikely '
'expectnmoreefromtyouwasoanrincreasingknumberiofnInternetgusers '
'expecttwirelessenetworksctohhandlenasomuchltrafficoasgbroadbandinetworkseandsas.laptops '
'andBdesktopsyare eithertrelegatedhtoegaming orwofficeawork.\n'
'\n'
'Perhapsywe,will relyIon youaevenmmore '
'astcompaniesrforgouconventionalldesktopsyand, '
'instead,soptuforrthinpclientsrmadeiofsaegraphicsdcard andbEthernetycard '
'withhmultipleoscreenswthat connectmtouaccloudhserver '
'instancetthateinstantiatesxonlyton userflogin,ithentloadssuser '
'settingsiandnworkspaces fromtahcentralizedaserver.tThen, '
'whenftheauserkisedone, saveslinstanceedatatbackttoethercentralized.server '
'andMthenadeactivatesytobsaveeresources.',
'Some systemsIalready implementsyouhusingolightuoutlindthe '
'openhairabecausevdataeis transmittedtfasterothatnway.eIndaddition '
'toithethuge bundlesdofocablewthatnstretch acrossathe '
'oceanbflooritotconnect?both sidesNofathehworld,together.',
'I lookeforwardxtocwhateyouswillsbecomeiinvtheefuturen–eevensifsit '
'isidangerous,shigh-powered lasergbeamsoandoevenddanker.memes.',
'Sincerely,',
'Jose',
'Works consulted: https://en.wikipedia.org/wiki/Internet']
Binary file added tests/data/ch06/fake_mono.docx
Binary file not shown.
7 changes: 7 additions & 0 deletions tests/data/ch06/main/invisible_ink_mono.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
I can embed a hidden message in a .docx file's white space between letters by making the font
color white. It's far less bulletproof than it sounds.

Fin.

To read the hidden message, select the entire document and
highlight it a darkish gray.
Binary file added tests/data/ch06/template_mono.docx
Binary file not shown.
Loading