From eac6b54c653fc5f88d3b13d570670d011f329b9b Mon Sep 17 00:00:00 2001 From: maqp Date: Wed, 23 Oct 2019 07:21:29 +0300 Subject: [PATCH] 1.19.10 --- .travis.yml | 3 +- LICENSE-3RD-PARTY | 8 +- README.md | 18 +- dd.py | 6 +- install.sh | 350 +- install.sh.asc | 26 +- launchers/TFC-Dev.desktop | 2 +- launchers/TFC-Local-test.desktop | 2 +- launchers/TFC-RP-Tails.desktop | 4 +- launchers/TFC-RP.desktop | 2 +- launchers/TFC-RxP.desktop | 2 +- launchers/TFC-TxP.desktop | 2 +- relay.py | 8 +- requirements-dev.txt | 47 +- requirements-relay-tails.txt | 38 + requirements-relay.txt | 19 +- requirements-setuptools.txt | 2 + requirements-venv.txt | 2 +- requirements.txt | 11 +- src/common/crypto.py | 93 +- src/common/db_contacts.py | 5 +- src/common/db_groups.py | 5 +- src/common/db_keys.py | 5 +- src/common/db_logs.py | 11 +- src/common/db_masterkey.py | 59 +- src/common/db_onion.py | 2 +- src/common/db_settings.py | 6 +- src/common/encoding.py | 24 +- src/common/exceptions.py | 2 +- src/common/gateway.py | 17 +- src/common/input.py | 3 +- src/common/misc.py | 15 +- src/common/output.py | 9 +- src/common/reed_solomon.py | 22 +- src/common/statics.py | 172 +- src/common/word_list.py | 7799 +++++++++++++++++++++ src/receiver/commands.py | 15 +- src/receiver/commands_g.py | 4 +- src/receiver/files.py | 8 +- src/receiver/key_exchanges.py | 13 +- src/receiver/messages.py | 11 +- src/receiver/output_loop.py | 60 +- src/receiver/packet.py | 18 +- src/receiver/receiver_loop.py | 4 +- src/receiver/windows.py | 17 +- src/relay/client.py | 29 +- src/relay/commands.py | 11 +- src/relay/onion.py | 55 +- src/relay/server.py | 2 +- src/relay/tcb.py | 14 +- src/transmitter/commands.py | 11 +- src/transmitter/commands_g.py | 14 +- src/transmitter/contact.py | 8 +- src/transmitter/files.py | 3 +- src/transmitter/input_loop.py | 6 +- src/transmitter/key_exchanges.py | 27 +- src/transmitter/packet.py | 33 +- src/transmitter/sender_loop.py | 8 +- src/transmitter/traffic_masking.py | 3 +- src/transmitter/user_input.py | 2 +- src/transmitter/windows.py | 2 +- tests/common/test_crypto.py | 65 +- tests/common/test_db_contacts.py | 11 +- tests/common/test_db_groups.py | 7 +- tests/common/test_db_keys.py | 7 +- tests/common/test_db_logs.py | 34 +- tests/common/test_db_masterkey.py | 29 +- tests/common/test_db_onion.py | 7 +- tests/common/test_db_settings.py | 14 +- tests/common/test_encoding.py | 7 +- tests/common/test_gateway.py | 16 +- tests/common/test_input.py | 4 +- tests/common/test_misc.py | 20 +- tests/common/test_output.py | 8 +- tests/common/test_path.py | 5 + tests/common/test_reed_solomon.py | 13 +- tests/common/test_word_list.py | 39 + tests/mock_classes.py | 6 +- tests/receiver/test_commands.py | 32 +- tests/receiver/test_commands_g.py | 10 +- tests/receiver/test_files.py | 26 +- tests/receiver/test_key_exchanges.py | 35 +- tests/receiver/test_messages.py | 10 +- tests/receiver/test_output_loop.py | 8 +- tests/receiver/test_packet.py | 8 +- tests/receiver/test_receiver_loop.py | 10 +- tests/receiver/test_windows.py | 17 +- tests/relay/test_client.py | 12 +- tests/relay/test_commands.py | 116 +- tests/relay/test_onion.py | 43 +- tests/relay/test_server.py | 2 +- tests/relay/test_tcb.py | 15 +- tests/test_dd.py | 15 +- tests/transmitter/test_commands.py | 52 +- tests/transmitter/test_commands_g.py | 27 +- tests/transmitter/test_contact.py | 24 +- tests/transmitter/test_files.py | 2 + tests/transmitter/test_input_loop.py | 4 +- tests/transmitter/test_key_exchanges.py | 33 +- tests/transmitter/test_packet.py | 39 +- tests/transmitter/test_sender_loop.py | 8 +- tests/transmitter/test_traffic_masking.py | 6 +- tests/transmitter/test_user_input.py | 8 +- tests/transmitter/test_windows.py | 18 +- tests/utils.py | 26 +- tfc.png | Bin 14827 -> 41051 bytes tfc.py | 14 +- tfc.yml | 24 + 108 files changed, 9419 insertions(+), 696 deletions(-) create mode 100644 requirements-relay-tails.txt create mode 100644 requirements-setuptools.txt create mode 100644 src/common/word_list.py create mode 100644 tests/common/test_word_list.py mode change 100755 => 100644 tfc.png create mode 100644 tfc.yml diff --git a/.travis.yml b/.travis.yml index 042462b..1040931 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,8 +16,7 @@ before_install: install: - pip install pytest pytest-cov pyyaml coveralls - - pip install -r requirements.txt --require-hashes - - pip install -r requirements-relay.txt --require-hashes + - pip install -r requirements-dev.txt script: - py.test --cov=src tests/ diff --git a/LICENSE-3RD-PARTY b/LICENSE-3RD-PARTY index e134fb2..4ac851d 100644 --- a/LICENSE-3RD-PARTY +++ b/LICENSE-3RD-PARTY @@ -83,9 +83,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - The Argon2 library, Copyright © 2015, Hynek Schlawack (https://github.com/hynek/argon2_cffi) - - The asn1crypto library, Copyright © 2015-2018, Will Bond - (https://github.com/wbond/asn1crypto) - - The src.common.encoding Base58 implementation, Copyright © 2015, David Keijser (https://github.com/keis/base58) @@ -531,6 +528,11 @@ Public License instead of this License. - The src.relay.onion Tor class, Copyright © 2014-2019, Micah Lee (https://github.com/micahflee/onionshare) + - gnome-terminal, Copyright © Guilherme de S. Pastore , + Havoc Pennington , + Mariano Suárez-Alvarez + (https://gitlab.gnome.org/GNOME/gnome-terminal) + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Preamble diff --git a/README.md b/README.md index 4291215..25bee26 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ - + ### Tinfoil Chat @@ -67,8 +67,8 @@ and schedule of communication, even if the Networked Computer is compromised. ### How it works -![](https://www.cs.helsinki.fi/u/oottela/wiki/readme/how_it_works.png) -[System overview](https://www.cs.helsinki.fi/u/oottela/wiki/readme/how_it_works.png) +![](https://www.cs.helsinki.fi/u/oottela/wiki/readme/how_it_works2.png) +[System overview](https://www.cs.helsinki.fi/u/oottela/wiki/readme/how_it_works2.png) TFC uses three computers per endpoint: Source Computer, Networked Computer, and Destination Computer. @@ -114,8 +114,8 @@ the user. 3. The Networked Computer is assumed to be compromised. All sensitive data that passes through it is encrypted and signed with no exceptions. -![](https://www.cs.helsinki.fi/u/oottela/wiki/readme/attacks.png) -[Exfiltration security](https://www.cs.helsinki.fi/u/oottela/wiki/readme/attacks.png) +![](https://www.cs.helsinki.fi/u/oottela/wiki/readme/attacks2.png) +[Exfiltration security](https://www.cs.helsinki.fi/u/oottela/wiki/readme/attacks2.png) #### Data diode Optical repeater inside the @@ -131,12 +131,14 @@ fundamental laws of physics. #### Source/Destination Computer - Debian 10 -- *buntu 19.04 (or newer) +- PureOS 9.0 +- *buntu 19.10 #### Networked Computer -- Tails (Debian Buster or newer) +- Tails 4.0 - Debian 10 -- *buntu 19.04 (or newer) +- PureOS 9.0 +- *buntu 19.10 ### More information diff --git a/dd.py b/dd.py index d3ab9a3..0a84bed 100644 --- a/dd.py +++ b/dd.py @@ -29,9 +29,9 @@ from typing import Any, Dict, Tuple from src.common.misc import get_terminal_height, get_terminal_width, ignored, monitor_processes from src.common.output import clear_screen -from src.common.statics import DATA_FLOW, DD_ANIMATION_LENGTH, DD_OFFSET_FROM_CENTER, DST_DD_LISTEN_SOCKET -from src.common.statics import DST_LISTEN_SOCKET, EXIT_QUEUE, IDLE, LOCALHOST, NC, NCDCLR, NCDCRL, RP_LISTEN_SOCKET -from src.common.statics import SCNCLR, SCNCRL, SRC_DD_LISTEN_SOCKET +from src.common.statics import (DATA_FLOW, DD_ANIMATION_LENGTH, DD_OFFSET_FROM_CENTER, DST_DD_LISTEN_SOCKET, + DST_LISTEN_SOCKET, EXIT_QUEUE, IDLE, LOCALHOST, NC, NCDCLR, NCDCRL, RP_LISTEN_SOCKET, + SCNCLR, SCNCRL, SRC_DD_LISTEN_SOCKET) def draw_frame(argv: str, # Arguments for the simulator position/orientation diff --git a/install.sh b/install.sh index 04a570a..0cdf6e0 100644 --- a/install.sh +++ b/install.sh @@ -16,6 +16,30 @@ # You should have received a copy of the GNU General Public License # along with TFC. If not, see . +# PIP dependency file names +ARGON2=argon2_cffi-19.1.0-cp34-abi3-manylinux1_x86_64.whl +CERTIFI=certifi-2019.9.11-py2.py3-none-any.whl +CFFI=cffi-1.13.1-cp37-cp37m-manylinux1_x86_64.whl +CHARDET=chardet-3.0.4-py2.py3-none-any.whl +CLICK=Click-7.0-py2.py3-none-any.whl +CRYPTOGRAPHY=cryptography-2.8-cp34-abi3-manylinux1_x86_64.whl +FLASK=Flask-1.1.1-py2.py3-none-any.whl +IDNA=idna-2.8-py2.py3-none-any.whl +ITSDANGEROUS=itsdangerous-1.1.0-py2.py3-none-any.whl +JINJA2=Jinja2-2.10.3-py2.py3-none-any.whl +MARKUPSAFE=MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl +PYCPARSER=pycparser-2.19.tar.gz +PYNACL=PyNaCl-1.3.0-cp34-abi3-manylinux1_x86_64.whl +PYSERIAL=pyserial-3.4-py2.py3-none-any.whl +PYSOCKS=PySocks-1.7.1-py3-none-any.whl +REQUESTS=requests-2.22.0-py2.py3-none-any.whl +SETUPTOOLS=setuptools-41.4.0-py2.py3-none-any.whl +SIX=six-1.12.0-py2.py3-none-any.whl +STEM=stem-1.7.1.tar.gz +URLLIB3=urllib3-1.25.6-py2.py3-none-any.whl +VIRTUALENV=virtualenv-16.7.7-py2.py3-none-any.whl +WERKZEUG=Werkzeug-0.16.0-py2.py3-none-any.whl + function compare_digest { # Compare the SHA512 digest of TFC file against the digest pinned in @@ -30,111 +54,89 @@ function compare_digest { function verify_tcb_requirements_files { -# To minimize the time TCB installer configuration stays online, only -# the requirements files are authenticated between downloads. - compare_digest c3f27d766f2795bf0c87ddb0cec43f1f9919c2cf20db5eff62e818d67436f1520b6001bf9e7649c5508e62b221c02039b6cb29f7393ba1dbacf9a442cb3bb8b2 '' requirements.txt - compare_digest 2c9e865be4231d346504bef99159d987803944b4ed7a1f0dbb7e674d4043e83c45771da34b7c4772f25101b81f41f2bafc75bfd07e58d37ddf7d3dc1aa32da24 '' requirements-venv.txt + # To minimize the time TCB installer configuration stays online, only + # the requirements files are authenticated between downloads. + compare_digest 99912fe2f7240a9b163292ff83c28b6ab41ee1c10bf96cc57f2c066537d3f153b46280e2c769b0f273c6bc36c74badb42d3c66c6fb3d16862dc96ff27319788d '' requirements.txt + compare_digest 97558ed189976ccd54e3a25bcf639f1944aa43f4a4f42ff5ef2cf22349a7b649272e91746041b4e04b2f33adf1fab8818c339b1cc58f9353af3e5ac76cb1ec0b '' requirements-venv.txt } function verify_files { # Verify the authenticity of the rest of the TFC files. - compare_digest e4f81f752001dbd04d46314ea6d8867393c3ad5ed85c2d3e336a8018913446f5855525e0ca03671ab8d26d8af1fe16416c8f5a163cad795867284a726adfeb31 '' dd.py + compare_digest bcb8a7ce1eb2d2f064b560ca5a8e467f84e3a0c3d643771e7782c792e89494600436e52c12f0a8471bf4a1da116f82ed732b8e06783534227a31f576f7adbd6c '' dd.py compare_digest d361e5e8201481c6346ee6a886592c51265112be550d5224f1a7a6e116255c2f1ab8788df579d9b8372ed7bfd19bac4b6e70e00b472642966ab5b319b99a2686 '' LICENSE - compare_digest 651fccf97f1a1a3541078a19191b834448cb0c3256c6c2822989b572d67bc4b16932edea226ecf0cbd792fc6a11f4db841367d3ecd93efa67b27eaee0cc04cb7 '' LICENSE-3RD-PARTY - compare_digest 84a4e5b287ba4f600fc170913f5bdcd3db67c6d75a57804331a04336a9931c7ce9c58257ad874d3f197c097869438bb1d2932f06f5762c44f264617681eab287 '' relay.py - compare_digest 2865708ab24c3ceeaf0a6ec382fb7c331fdee52af55a111c1afb862a336dd757d597f91b94267da009eb74bbc77d01bf78824474fa6f0aa820cd8c62ddb72138 '' requirements-dev.txt - compare_digest 1ab71b773a0451807eda87a1442dd79529de62f617e8087a21c0f3ad77e4fe22218e05a94d19d1660b2967a9908fb722f750a64a75e650b178c818c8536f64db '' requirements-relay.txt - compare_digest 6d93d5513f66389778262031cbba95e1e38138edaec66ced278db2c2897573247d1de749cf85362ec715355c5dfa5c276c8a07a394fd5cf9b45c7a7ae6249a66 '' tfc.png - compare_digest a7b8090855295adfc22528b2f89bed88617b5e990ffe58e3a42142a9a4bea6b1b67c757c9b7d1eafeec22eddee9f9891b44afffa52d31ce5d050f08a1734874d '' tfc.py + compare_digest 7e519d20fef24e25e88ec4a9c03abadf513b084e05038f17c62ca7899c2f9174a953caa0bfbd3b61e455e243513cdab737c22a34d73ebab07b65d3ce99100f0a '' LICENSE-3RD-PARTY + compare_digest 99815d0cfbca7d83409b7317947fe940fe93fd94b50e6099a566563ee6999e33830fd883ff61e5367a040d5fda3f2a43165ef0dc6155e14a573e07dc27eba70d '' relay.py + compare_digest 28d06826a45ca4d64c2b4d06859ee7a0c7152198fe49b85681f7ce6b9c02b1a103fd7f3514b05b24e95e2ec5f48ce02529a2b4f2ea806b333e8141b1650d1257 '' requirements-dev.txt + compare_digest 8a57366899139b9906f0a75272c702575a6cd5c6ca2dd09f0dbd1be9efd5341178f9d3d64fec113af7d1fdccbb5cbdf384133aa3afa3672292e37405f60cf0a8 '' requirements-relay.txt + compare_digest 8ecd5957f3bfbe237549e8772720cba5b5899b51a475063edcbc416ad5f77f614da2c9069aeb31bca6d2bb74ce6f2877d29df178ec3ecf6d5dd05daaff51c6b2 '' requirements-relay-tails.txt + compare_digest 4a44501e21d463ff8569a1665b75c2e4d8de741d445dc3e442479cbb7282646045129233bd7313df4b9c2e64ec86b7615a8196ae2b3350de933731926d39bbda '' requirements-setuptools.txt + compare_digest 79f8272a2ab122a48c60630c965cd9d000dcafabf5ee9d69b1c33c58ec321feb17e4654dbbbf783cc8868ccdfe2777d60c6c3fc9ef16f8264d9fcf43724e83c2 '' tfc.png + compare_digest e4dadae63adcd72108fcfa04401f42a1bae956008303d09f22e849b207ebca699306f2bd4034ee96a5531028719f5e41689205ec8ef12cd1726a86376d3aec3e '' tfc.py + compare_digest 7ae1c2a393d96761843bea90edd569244bfb4e0f9943e68a4549ee46d93180d26d4101c2471c1a37785ccdfaef45eedecf15057c0a9cc6c056460c5f9a69d37b '' tfc.yml compare_digest c6a61b3050624874cabc28cc51e947aa1ba629b0fd62564466b902cc433c08be6ae64d53bb2f33158e198c60ef2eb7c38b0bee1a64ef9659d101dee07557ddc7 '' uninstall.sh compare_digest d4f503df2186db02641f54a545739d90974b6d9d920f76ad7e93fe1a38a68a85c167da6c19f7574d11fbb69e57d563845d174d420c55691bc2cd75a1a72806dc launchers/ terminator-config-local-test - compare_digest fed9f056541afe1822ec41bce315fc6ce94652318088ecc905ddc657a15525d740ca83ff335e5090b9e3844027a706990da72a754ffa3dcd255b21e263597c78 launchers/ TFC-Local-test.desktop - compare_digest 011a64aa7790253b008f0adfe940108dc94c08adec9b3a42efd1ac0d6405a82d828b3d416736ad64235b6b90ef2f6712fc10a70d8e97870670e5b84cf25fe54e launchers/ TFC-RP.desktop - compare_digest 12c86594b28f5dae48e4d216781034917ab80ca46f30cb3d33a0dec615d415830fdc41228f933c8fef651eb51643390fe5521e70f9efc642ea5c8b2e34442e00 launchers/ TFC-RP-Tails.desktop - compare_digest 270153a45e5426db326ba2144ca454bb57ddf8f53731c1948ee8fa33dedcb98e32e19506b0fa923e5a7f6fca9a5b67338216c19417e52289726721efe12abd98 launchers/ TFC-RxP.desktop - compare_digest 691d0cc4f03bd9c2874711ba97e347e705e4eadf53c5b14562e7e1a419ae892a9ce156b6e5a26945bde72c0b3e56ec6ca2d24fb3bf23848fb44808b7681e4141 launchers/ TFC-TxP.desktop + compare_digest a5611269e2f69a452840ae13d888bd80d6f8e5e78fdab0cb666440491d8431e6c326dc57a52df7d9e68ecd139376606c9f6c945207f2427bb21c114fe26c0af7 launchers/ TFC-Local-test.desktop + compare_digest 9263737fca4773672515e0f4708e147b634bd09c8d068966806bb77d3b38dcf60b1f933846f9a649e795760ff141a31dc2b58fad38ef2afbaedb33d2f479a29b launchers/ TFC-RP.desktop + compare_digest 9263737fca4773672515e0f4708e147b634bd09c8d068966806bb77d3b38dcf60b1f933846f9a649e795760ff141a31dc2b58fad38ef2afbaedb33d2f479a29b launchers/ TFC-RP-Tails.desktop + compare_digest 113d1f8f6bc03009ef1ccfe1aed8a90bdecb54e66bd91ed815bbd83cb695419a25c614de8287475d3beab832cfcaf6d549c06832f2ea098d29ff049d7cd91da7 launchers/ TFC-RxP.desktop + compare_digest 1f4d4e216039b63f2579eef17dc18df5e2f1e65f09e619b62adb8dceb128de6ffe5784ea0ff1dc846af21e1ce641bc612df51e37e205fee210f94dd87b86f467 launchers/ TFC-TxP.desktop compare_digest cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e src/ __init__.py compare_digest cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e src/common/ __init__.py - compare_digest 92c7f788c41984cf75ad879e281f73a1a334dd759b858be11f91481a0e754e2db1527c9d3ab369aad7b8cf139492d5809c0c0542e46ff27637ee5b4b655b726f src/common/ crypto.py - compare_digest 0b830ad6705f90dc8e554c8b06f31054d9222d3b35db92470eaf3f9af935aae3107b61142ea68129943e4227a45dfe26a87f23e9dd95a7794ae65c397bd35641 src/common/ db_contacts.py - compare_digest bad54588b3e713caf94aba3216090410f77f92064aecfea004a33487a21f9fffcf5d05782b91104c27ec499e38196881a76d6002ec1149816d8c53560182fba9 src/common/ db_groups.py - compare_digest 46e503af15fb7a1ea8387fa8f22674d0926eda0d879d43f99076ca967295d5e3727a411aa11c4bb4d832783c5daa9f4b7ef4491b90112c1be405910d269abaf4 src/common/ db_keys.py - compare_digest 68b2c7761623ff9e4ce596e170c8e1ce2b6129bbbbb9fc9e48a120974d30db6e471bb5d514f5dc8d7edc8e0abcb0cd0c2b4e5da739f4d98c6fa8c8e07af0d8ef src/common/ db_logs.py - compare_digest 1a3783e6ea5643bc799b9745d0a8abb82f9e814ca01da3414df86faaa007aaa21609a88e07c8947e43567a787f114db57143f10585053e92f96807858adec96a src/common/ db_masterkey.py - compare_digest 5befbe864e2b09125be2b04cdfee8d13e7616715fc20a0fa06da270e34b555602b2df825fd429059056b2beb1497c50dafdc682d59a43a483837445861647e9d src/common/ db_onion.py - compare_digest 328375464a5064829621fb8154c94cf15a1e7564122d99f03c9e578dcd6d6f264a671412b738b4971a6e505062673df40cc89f7b67c7ef2a761236c3f1247e93 src/common/ db_settings.py - compare_digest e3f760b03eb38d3ec97b04123f8cf36825d616e126bfb9fb575927586cbb1911dce448c08cb8cfb26be0635c2e6abaaad8ea94f0513672c288c78892010ab146 src/common/ encoding.py - compare_digest f7ff99c8a5f84b892b959fe391cdfe416d103a5a4d25fcd46842f768583a5b241036ee9c4b6713a938e8234f5af82a770ffe28975292d162b6d5bea69a120ad8 src/common/ exceptions.py - compare_digest 67cbb9e983aeb49d8a0cc8666aa5c1dad4b868e4fc968c72c7fc4290b886fae9a58ef84f5942d8a08b19621cc2fe01b787ac10bff8b63393e6addaa178da4ea7 src/common/ gateway.py - compare_digest 618cbaccb4f50bc6e551cd13570693d4d53cfdfdc00b7ff42ff9fd14f0dadf7a15c9bc32c8e53801aca42348826568a2aa9117bdbf79bbcc4217a07c94c9efd3 src/common/ input.py - compare_digest 14f9a5f06443b50251272939b92eaf287fecfb4e3c9882254f13757259d9d791ac1e66c46fd0475d1e3161ff27f16ad7b4de04ac564500cae60d27a631d42e36 src/common/ misc.py - compare_digest b4efca327d057f9b3304d9f0415d23e95ba88230576a1a8d8be85c3e25364e7420389af36df4b4dacf62b5df11cb62f1ca7d0b6c03aee6b591282d51bbc119e3 src/common/ output.py + compare_digest 580c6453d8649d82973817de1fcfd83c73b0f6bb3e2597c5ffb6c0d3929d10f693c0a3d98dad4f528f1a1b62b220bcb99fa075ccd28e8b53d6cc966697523e25 src/common/ crypto.py + compare_digest 053c431755b4d6a5a869d2f06213bd23a8fb8743644b127d3622ff0223687880e8b1d510fbc976f2112318e48caa0044a1959c9af21272001e3f107fd9f785ba src/common/ db_contacts.py + compare_digest 8e4e8ab2dff2400f3fe72d980c95c1780d26e11f0482c555bb47cfa7513d091b06fd901666f415e0b3db291bd7f161d7182ac530a6d118a61f0b72bb665eead1 src/common/ db_groups.py + compare_digest 712c5422193994a65eff74f0a328107232069784c6c687f700a6823435fe65afb3e31cbd1102dbe07bc1d7e5e1572a4db6e467d477ebd3e4aab6ff3685723ccf src/common/ db_keys.py + compare_digest a2714c8aab538a5fc273f4eb58ea0d039effa4ce64559ab34dbe9eb92c762aff98a242443e2dffe5e55005b8778a5581f5c2ad3c07a0b6eee58e599e44a951b3 src/common/ db_logs.py + compare_digest c6bbb2b75f14447ba20bf1ac214b044d5a79b11a5346a1691a823c5e5ac4db05dd24a8de52856021d8d7f7f8582c1871839945323245521f320714cc72994bdf src/common/ db_masterkey.py + compare_digest 253521b1ed39a73a0fd6108cbcf88bbd1ffadde28be1467c1e7871094392d0a55947032ef5fc19d85727117bacf8516ac1f08f70240be20b9fc2d009a68989ee src/common/ db_onion.py + compare_digest 2e8ff65270e0165e510f5d330fa2cbfdc6ecf8ed953220bbe18d19a8afa6fa2bf852e56ba875c1c361fdb72016d0610e8c9fa1df302fa47f916eaaecde11e423 src/common/ db_settings.py + compare_digest 7a673e6feb7a5b2e3417d2c0eee82a59b3730a5d241938a84fd866dfc838c3cd63d7ef96772d43f62df740a2ba1001456746dd6c86e950484eac3ebabed498ce src/common/ encoding.py + compare_digest 00ad45d8fba1a605817a9f5d64cdfd6aad9c618db66befe682728a2291384c67bf5e80a5257211717a86e4a51c7e8c74f8f7ccccc3b4ac3c6f0f4c4e1b3cc98f src/common/ exceptions.py + compare_digest 5de68b10dc6b6ff98d7f73a2dc89d6b64c529fb4949b4a51d1b2fb4491006266bf7200b717102987347add666f12d7cfa9afc43a540304290f9a41fe48545078 src/common/ gateway.py + compare_digest 604893a2814219b2ed4e69b45d9ac2f8c2b5fc066bd085e86b76ef9df9984e6113f79fcaf3b9eb1197a9c9fc92cf524269e595d03b5009c46e8889d813475408 src/common/ input.py + compare_digest 1cac1bed0779f480de26054867cd732deaba5e7a46728ed8c203948ed92b96e0dbf2d5d1a696cbca445d5db2b79cde318aac28189d8806fe1ea530392a47f406 src/common/ misc.py + compare_digest e167f8458d1a1fc549f02a42ab9c1dc78ac2540b6a8660c77a06fe32de461eb52361a99582921b00eb50115c9fc70858dff1557c6e7ac69439768481a70b3fa7 src/common/ output.py compare_digest c4d97b497b341f0e7865a4e27a2a2ffd3b3c5a7bfbf72f4676f6b65d6ba66a2adb8fed563f88fa25cef555f0042290ef0ae4cbeed1697a2e19a3b8cff0b9ef1b src/common/ path.py - compare_digest 9e9db25e73e0abb312b540d7601be7dfecae8d0aef6497feb2570f9ddf788fa0e261e276ed5a40e75fee0e0c6e86ccf3b05c110846fde804c302d4aa80a930e5 src/common/ reed_solomon.py - compare_digest 374a9846d2a1f4b462d9fd4b6fd468ecdaf3a1d1ac25916214e949604e83eff66a969a7fe6af79f23f4bf2477f7a390374714602714c42448073392e56065a41 src/common/ statics.py + compare_digest 4365ed3b6951525cb1ec8dc1177d7fd74d5dfa5eab1ca8934775391a8736eed4df039684f19ccc2d8022f20c8cf93a57a736b259e8c7235da5060c5f62057c98 src/common/ reed_solomon.py + compare_digest 1f26c39a8e5ec39a859e90dac2e38c08d86af02e0fc714aa0569b618dbf44de5befde601b0fec23e62e3e3b3f7281727abea37a02818c9994ac47f119a621386 src/common/ statics.py + compare_digest 339b402790cb3002841a1212d4dc24b07236b65baf68e3a8caf8d61bfd48af12887564be449ae49cfe5d7b88107a73e01a05053e3789398fbd560ef89f14afd4 src/common/ word_list.py compare_digest cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e src/receiver/ __init__.py - compare_digest 12cf1127afc6761e61f6dadf6b6f382f4ff00291bcb00a6e9b21c98d1263596041b31892fbfc4be6348bb622aa19b50f38303d51a651d63c2024e8e466cbef07 src/receiver/ commands.py - compare_digest 760edaa44ff6175612b02f95b02b291ae369733a18cc5f87d525b46bcebc35c8a2d169a47962417eab434cf26ea6d5bfcd8894153fae668bb4a8cf2ceb8871f0 src/receiver/ commands_g.py - compare_digest ee192c3529c51031110a49cf919a7b9e516d0eb0cc826da85587323480065cf53d4115f78f8008c305d074628b9de65d9b262ef790c178078cf74d8b3466ae3f src/receiver/ files.py - compare_digest b3b0a61f3aba200e06bb2c6e05a067931be37ccfbc0768167a9d8651f7a8f6ea47871dcb5d63cf238f040d6c482fc45967a5a2ccbadb1308ec76b287b88fa3a6 src/receiver/ key_exchanges.py - compare_digest 65307a0ea2c9ae69859cc8ef62a5d7e45c27bdf5a4ec44db704df143ce3630fdc077fafc7fd4cfc0cd922f350f49f0aa0a880192c40c614b6d3117804ea683ae src/receiver/ messages.py - compare_digest b33a7696b31265027bb137aacd80845d4fefbd112144f3e01cfe4e083e5b6575fcdab387ae9d2435dd1c82bf0cf31b55f934cd12256a7ac78b4b13cc3d593987 src/receiver/ output_loop.py - compare_digest 7a81a142d21ebf8779fa0fec9aeef922d21bd6d57643061722d91047ecafeea7f0bfdd3a2919251a83a02cecbef909756fdb62881d5d2a844c3daba0e7069bf5 src/receiver/ packet.py - compare_digest 01b0ce92b4ab8f37eed55356a65f4d25ffff590955b5ca124dbb541bdec6968e8235242bf47ad5c2bfe57a30d54cf494d9f3836e3d53a73748f203ff68381c14 src/receiver/ receiver_loop.py - compare_digest 17ab4e845274ac6901902537cbea19841de56b5d6e92175599d22b74a3451ecafa3681420e2fcc7f53f6b2ebb7994e5903aabb63a5403fbf7fb892509bb5d11c src/receiver/ windows.py + compare_digest eaf664c520b7a2b4374e258b153940625312262fc54f9435018beff3152073d6dadcf78bedbe081a92cf173b31f2e86582c98a6065be1bba143b6b41b2d4b3b8 src/receiver/ commands.py + compare_digest 051a1eac8e1e177bdf1c94972ed511d56d9ccfb3dc97c6418355b4415b2d1dff42c4ef5420de05d90e1697375f4db119e04932c05d6a93a89e03e7ca4c7c7346 src/receiver/ commands_g.py + compare_digest 4a48adedcb839176e6a5f18b430a97c96558a7474d0840d22d31241a2918f1ffaed53211c5c949b545e231cda5280cf42fb5c20e22bdaef03f5fb2c298e22e07 src/receiver/ files.py + compare_digest 6eae2793bdd72b9581cbbebc012a70b11744c2585fda1d1e253ff4d67cdc1d316d1f3ad7e391f5197aa1447ce21b0ecbc3e34517a8932ecd9eec7ff5d7313b5b src/receiver/ key_exchanges.py + compare_digest d7d28808635a0425d231770b24c1a0f332ad40dffccfcf4f88801299b121e58363b728b0a70b0bc4d1640b850f7cb069ba88f3be966eb84f9e05f0112d3e76fd src/receiver/ messages.py + compare_digest d651d87311ec09b8aa0a3964718ea1ea22c5bc1cc078ca87a367b420fc19716438045b31618d34264f483eb009ed1aff33704d135d0d22374df31d5438f9e00c src/receiver/ output_loop.py + compare_digest a1edb6fe5b04117174ce45739e0737b6e8eedeed134bc95201d837bf6785bb98c86f8404eee18eb398755c8df113d7f8daa9dae43465846b887d514619759ed2 src/receiver/ packet.py + compare_digest 20c6754ddb6261c7a3b479e6ab7bf78eb0ef8783e2141373d7aba857f413091b78dcc9c32667dd8f8d5c41927102da7e35c4c4fcb0aa7376dc42b08c0c01d6e2 src/receiver/ receiver_loop.py + compare_digest 7d3f18351dc97bf4c1c5ab5894619d1f34ec07874758751340d53a12da2d7079944e6187d7436990f09a82d39c89d64a0bb747678c85ead8126eebbac9fcddfd src/receiver/ windows.py compare_digest cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e src/relay/ __init__.py - compare_digest a324e4b065c2bb8c66c1e0e7fbc11de85f4408e4a3c3afd81bbd6829fae4f6cf6a3bba284864476356a57947670e7dffcd6f30faf03e38b38dbed84305def042 src/relay/ client.py - compare_digest 6f0f916f7c879c0383e8dc39593385de8f1ab2f7ba99f59b7fcbee947f8fcfae0935a98dcd8de1db3a600a0d8469a369ea38915fe2932c00b6756fb4944c938d src/relay/ commands.py - compare_digest 261c9a0e7cb552c900f5234136c6251896bb03277c9ceaf175791012433a282373d6af1eec6be916aa955e04d5ffde2fc4bf3d153077baf00731249132b90658 src/relay/ onion.py - compare_digest a18aa0ca4ffff7be99721c094ae44a95ed407e5e4cb25016ce14bf9fca7fef950d0b0460fd91689f2003aeb7f8385cb4f88a5f2e3f7f4ba7b88634412853d888 src/relay/ server.py - compare_digest e03b2896049d6c4f98cc9b52ae43a32078ffe93f6a18848fb96cf4051752872ad20463754fad61244e914b2082839d8297f0d3357e20c8dd22881e885bfbd32a src/relay/ tcb.py + compare_digest 5d34be330731b8b722c3580f12abd2515984ba0589ea95c0960ae099a13b9d66118a5af5cdf137bcf376bde88b0edf055888d2a5fc267081ea118fffc05a2b08 src/relay/ client.py + compare_digest c32b5b78e28567d5ef0c6f41f1a3c69f6d31b1cb3b9d58faf6516fa27fc62e12b2f359f7b60176b5fe20a2d94725f5fd76a879d4b795513d1588f8ecf9bae5b0 src/relay/ commands.py + compare_digest c72a57dda6054b9c020f694740751159df4602f11f7759ff76e48a8b7f07ec829b39d6c366613f3a69e36d3dca0823491f3232506f3a03ecc9ded3e2a4f0230a src/relay/ onion.py + compare_digest fe108f1f642bdfd01d813fd0a183e2f6039c1e64a5ee57f6159fdc67d7574a0ba0ee23608a2a8499071f0844b7d2db6b6a14740046d5d664e09856c35680a0dc src/relay/ server.py + compare_digest 9459e6cbe17fefac356e5ce183d923efff66f6d304111f2c0dbacdfb22a92df77bb11134faf8c15400bc59174ecbec1ea0b436065a9d49d3af70b46b24a77764 src/relay/ tcb.py compare_digest cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e src/transmitter/ __init__.py - compare_digest bb471267d37c73b37f3695bd3f28929612fe2074f7303b77f40cba0be84e975e3498402cbba2b402e45e35e81512ed2f67629cf0584536f36903b326d35cf557 src/transmitter/ commands.py - compare_digest b861e16a6fbe2a93bf9f2dcf3f7f42d3f3ebf240aeaa2d752169a6807e2371eaaaf6230e0a71e6fe1e41e7ce5eb0515e8ec95bed5a7a7a263f42d715d23c399c src/transmitter/ commands_g.py - compare_digest d5ad53e3ba7ae32e0c6255f5fca3a60c267b028d7dfb2f2e28768c2f8824b0ad25afc7edc3a5d6d69bb76986ff8d81549d50d71d8b46ca6a0a9dc37324ae027a src/transmitter/ contact.py - compare_digest dffc059fc25cbfb17beb9f83fc2d52ce043e9b923580ccf655933cf66fefcf6e18bcb923d7cb42a7d547126f938ff867a7638ffd13c14953b4a2d700f8f0d5c4 src/transmitter/ files.py - compare_digest 061d7f3c4f737afafacc65bea744ac9bad16c6b064ab6281a3ab2079b18d984afa00b93e3861209425f78807e335925d824e018e80523b5380ac5244b8f5b8c2 src/transmitter/ input_loop.py - compare_digest 6465a819b1a449145fdb676819446761d8faa96921623433b12b9386b781d7964368c0f2cd5fe48aad5beda3cdc0f6496e95ae89650addd91619c5984979584c src/transmitter/ key_exchanges.py - compare_digest 1249ba23d30031ec8c1b42e999e52bc0903a0ad00c317d27d51c8d650b3343c9475319db6e0f2f16dfae39a942e01eeaae8eb862d541957b1f51690b1eb0f202 src/transmitter/ packet.py - compare_digest 93f874a8d97312ab4be10e803ba5b0432a40cf2c05541775c7117aa68c18e78e52e814d38639b33e87dc33df1773e04dc2c789e61e08c12d9c15512dd9e5d4d3 src/transmitter/ sender_loop.py - compare_digest bcad5d4b9932f1b35b2c74dc083233af584012cacdd1d2cb04b28115b4e224118ce220252754ae532df0adcc6c343b1913f711cf3bd94da9c4cd3eaf23e4b196 src/transmitter/ traffic_masking.py - compare_digest ccbda8415c23b23cc10cda57fb6b32df71e6510f3cb94c7f932b40adcf5f0abdd9842c48a992d56c95755e3024aebd7ecb05f69eb18f3c41656d94cfeabb38fa src/transmitter/ user_input.py - compare_digest 4c5b9c877474257d078723d32533ba614d72bad7b108588f49fae6c26dcb2c49994b256b5457caee0e5ab4628f728b25a54551ce778b585cbb8b1f8c947dc7e6 src/transmitter/ windows.py + compare_digest 2d002fe7aab987512534bdb18433d4a626b404aafc0360253056995559b0d2097938604325bc882a36ba456af0fbcf9620909945ebddf668bc44748a31a225c0 src/transmitter/ commands.py + compare_digest 74291c8b952588caf7c3c6ac3e99679eaf97ba9113bfd560da4c461e16cd36c28d78a4e8750090d18606a9c1610a1a781d37fc6fe52baf47a955e0b5ec801b97 src/transmitter/ commands_g.py + compare_digest 3a2940afcf8752f33c8f5a06293046a83d245630dde1a6877eb3c724cb03ee7b84147b4a57a62135a32862b40db1dc6c6823bedc52404146aeb6e9ef1f79692f src/transmitter/ contact.py + compare_digest 2e78e578e62771adf7ae9f2a576d72b69a64e6b28649361244bd7a75959f2022845d73d68c3d6a4586841bb10cce906edf1c5d863fbf99b6d081dfc030f98a3d src/transmitter/ files.py + compare_digest 7cb9fc9d095f40ce2de6b49c9bd58b9dcab6b835fe7749dce8642c3c87b0eee10c4e53ff986c09ae26fb7b8aad7fe87c5fd56a734f2e013f69195213b9d5e9ec src/transmitter/ input_loop.py + compare_digest e723db5bc403cec60b7df3e34d80caa7868bed4e8f0d08a6504d060fdd6c188f4afa41ecd8feb3a6520a384ccff7f8c64efeaeddb084f825d40e1854fb528f9f src/transmitter/ key_exchanges.py + compare_digest 41798dfe91868b37c130a373accac93c4200dc77bd8b6c40a38835ecf4187b955ccfaa53f842ccddf78ce5607b3e361a30a4bb53bd7cb5ab6d2fb4785454dead src/transmitter/ packet.py + compare_digest 3f1e7a5cb58ba8fcf0ccc66195d589ac0e34153296e2c395ca099304a99bb61c25248078dc453b3cc47e08a0b207a688b0a426d095df4a7bd235d1a95bb3c8d6 src/transmitter/ sender_loop.py + compare_digest c5a6c85e57d4456353f89fc4b2d30fc60775511720a32287720b3b301e0d6e7539677b47c4ff8c6b6f223b93da7dfbb38d1830f43e6f25c598efd54799262956 src/transmitter/ traffic_masking.py + compare_digest 678ae2b63667d93b1d4467d029ab04778614ddf6c09dff4bb61d262373353cd7fe6b8b535292fdf28e1be36c8b57534dee9eb745ee94c72b051798ac4e1cbccd src/transmitter/ user_input.py + compare_digest 00e247854f067194f80c86c9a3b9fbe1975e600844a1f33af79e36618680e0c9ddebaa25ef6df1a48e324e241f2b113f719fc29a2b43626eeeba4b92bdbb8528 src/transmitter/ windows.py } -# PIP dependency file names -ARGON2=argon2_cffi-19.1.0-cp34-abi3-manylinux1_x86_64.whl -ASN1CRYPTO=asn1crypto-0.24.0-py2.py3-none-any.whl -CERTIFI=certifi-2019.6.16-py2.py3-none-any.whl -CFFI=cffi-1.12.3-cp37-cp37m-manylinux1_x86_64.whl -CHARDET=chardet-3.0.4-py2.py3-none-any.whl -CLICK=Click-7.0-py2.py3-none-any.whl -CRYPTOGRAPHY=cryptography-2.7-cp34-abi3-manylinux1_x86_64.whl -FLASK=Flask-1.1.1-py2.py3-none-any.whl -IDNA=idna-2.8-py2.py3-none-any.whl -ITSDANGEROUS=itsdangerous-1.1.0-py2.py3-none-any.whl -JINJA2=Jinja2-2.10.1-py2.py3-none-any.whl -MARKUPSAFE=MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl -PYCPARSER=pycparser-2.19.tar.gz -PYNACL=PyNaCl-1.3.0-cp34-abi3-manylinux1_x86_64.whl -PYSERIAL=pyserial-3.4-py2.py3-none-any.whl -PYSOCKS=PySocks-1.7.0-py3-none-any.whl -REQUESTS=requests-2.22.0-py2.py3-none-any.whl -SETUPTOOLS=setuptools-41.2.0-py2.py3-none-any.whl -SIX=six-1.12.0-py2.py3-none-any.whl -STEM=stem-1.7.1.tar.gz -URLLIB3=urllib3-1.25.3-py2.py3-none-any.whl -VIRTUALENV=virtualenv-16.7.3-py2.py3-none-any.whl -WERKZEUG=Werkzeug-0.15.5-py2.py3-none-any.whl - - function process_tcb_dependencies { # Manage TCB dependencies in batch. The command that uses the files # is passed to the function as a parameter. @@ -145,7 +147,6 @@ function process_tcb_dependencies { sudo $1 /opt/tfc/${SETUPTOOLS} sudo $1 /opt/tfc/${PYNACL} sudo $1 /opt/tfc/${PYSERIAL} - sudo $1 /opt/tfc/${ASN1CRYPTO} sudo $1 /opt/tfc/${CRYPTOGRAPHY} } @@ -154,37 +155,113 @@ function process_tails_dependencies { # Manage Tails dependencies in batch. The command that uses the # files is passed to the function as a parameter. - # Pyserial - t_sudo $1 /opt/tfc/${PYSERIAL} - - # Stem - t_sudo $1 /opt/tfc/${STEM} - - # PySocks - t_sudo $1 /opt/tfc/${PYSOCKS} + t_sudo -E $1 /opt/tfc/${PYSERIAL} + # t_sudo -E $1 /opt/tfc/${STEM} + t_sudo -E $1 /opt/tfc/${PYSOCKS} # Requests - t_sudo $1 /opt/tfc/${URLLIB3} - t_sudo $1 /opt/tfc/${IDNA} - t_sudo $1 /opt/tfc/${CHARDET} - t_sudo $1 /opt/tfc/${CERTIFI} - t_sudo $1 /opt/tfc/${REQUESTS} + t_sudo -E $1 /opt/tfc/${URLLIB3} + t_sudo -E $1 /opt/tfc/${IDNA} + t_sudo -E $1 /opt/tfc/${CHARDET} + t_sudo -E $1 /opt/tfc/${CERTIFI} + t_sudo -E $1 /opt/tfc/${REQUESTS} # Flask - t_sudo $1 /opt/tfc/${WERKZEUG} - t_sudo $1 /opt/tfc/${MARKUPSAFE} - t_sudo $1 /opt/tfc/${JINJA2} - t_sudo $1 /opt/tfc/${ITSDANGEROUS} - t_sudo $1 /opt/tfc/${CLICK} - t_sudo $1 /opt/tfc/${FLASK} + t_sudo -E $1 /opt/tfc/${WERKZEUG} + t_sudo -E $1 /opt/tfc/${MARKUPSAFE} + t_sudo -E $1 /opt/tfc/${JINJA2} + t_sudo -E $1 /opt/tfc/${ITSDANGEROUS} + t_sudo -E $1 /opt/tfc/${CLICK} + t_sudo -E $1 /opt/tfc/${FLASK} # Cryptography - t_sudo $1 /opt/tfc/${SETUPTOOLS} - t_sudo $1 /opt/tfc/${SIX} - t_sudo $1 /opt/tfc/${ASN1CRYPTO} - t_sudo $1 /opt/tfc/${PYCPARSER} - t_sudo $1 /opt/tfc/${CFFI} - t_sudo $1 /opt/tfc/${CRYPTOGRAPHY} + t_sudo -E $1 /opt/tfc/${SIX} + t_sudo -E $1 /opt/tfc/${PYCPARSER} + t_sudo -E $1 /opt/tfc/${CFFI} + t_sudo -E $1 /opt/tfc/${CRYPTOGRAPHY} + + # PyNaCl + t_sudo -E $1 /opt/tfc/${PYNACL} +} + + +function move_tails_dependencies { + # Move Tails dependencies in batch. + t_sudo mv $HOME/${VIRTUALENV} /opt/tfc/ + t_sudo mv $HOME/${PYSERIAL} /opt/tfc/ + # t_sudo mv $HOME/${STEM} /opt/tfc/ + t_sudo mv $HOME/${PYSOCKS} /opt/tfc/ + + # Requests + t_sudo mv $HOME/${URLLIB3} /opt/tfc/ + t_sudo mv $HOME/${IDNA} /opt/tfc/ + t_sudo mv $HOME/${CHARDET} /opt/tfc/ + t_sudo mv $HOME/${CERTIFI} /opt/tfc/ + t_sudo mv $HOME/${REQUESTS} /opt/tfc/ + + # Flask + t_sudo mv $HOME/${WERKZEUG} /opt/tfc/ + t_sudo mv $HOME/${MARKUPSAFE} /opt/tfc/ + t_sudo mv $HOME/${JINJA2} /opt/tfc/ + t_sudo mv $HOME/${ITSDANGEROUS} /opt/tfc/ + t_sudo mv $HOME/${CLICK} /opt/tfc/ + t_sudo mv $HOME/${FLASK} /opt/tfc/ + + # Cryptography + t_sudo mv $HOME/${SIX} /opt/tfc/ + t_sudo mv $HOME/${PYCPARSER} /opt/tfc/ + t_sudo mv $HOME/${CFFI} /opt/tfc/ + t_sudo mv $HOME/${CRYPTOGRAPHY} /opt/tfc/ + + # PyNaCl + t_sudo mv $HOME/${PYNACL} /opt/tfc/ +} + + +function verify_tails_dependencies { + # Tails doesn't allow downloading over PIP to /opt/tfc, so we + # first download to $HOME, move the files to /opt/tfc, and then + # perform additional hash verification + compare_digest e80eb04615d1dcd2546bd5ceef5408bbb577fa0dd725bc69f20dd7840518af575f0b41e629e8164fdaea398628813720a6f70a42e7748336601391605b79f542 '' ${VIRTUALENV} + compare_digest 8333ac2843fd136d5d0d63b527b37866f7d18afc3bb33c4938b63af077492aeb118eb32a89ac78547f14d59a2adb1e5d00728728275de62317da48dadf6cdff9 '' ${PYSERIAL} + # compare_digest a275f59bba650cb5bb151cf53fb1dd820334f9abbeae1a25e64502adc854c7f54c51bc3d6c1656b595d142fc0695ffad53aab3c57bc285421c1f4f10c9c3db4c '' ${STEM} + compare_digest 313b954102231d038d52ab58f41e3642579be29f827135b8dd92c06acb362effcb0a7fd5f35de9273372b92d9fe29f38381ae44f8b41aa90d2564d6dd07ecd12 '' ${PYSOCKS} + + # Requests + compare_digest 719cfa3841d0fe7c7f0a1901b8029df6685825da7f510ba61f095df64f115fae8bfa4118fa7536231ed8187cdf3385cb2d52e53c1b35b8f4aa42f7117cc4d447 '' ${URLLIB3} + compare_digest fb07dbec1de86efbad82a4f73d98123c59b083c1f1277445204bef75de99ca200377ad2f1db8924ae79b31b3dd984891c87d0a6344ec4d07a0ddbbbc655821a3 '' ${IDNA} + compare_digest bfae58c8ea19c87cc9c9bf3d0b6146bfdb3630346bd954fe8e9f7da1f09da1fc0d6943ff04802798a665ea3b610ee2d65658ce84fe5a89f9e93625ea396a17f4 '' ${CHARDET} + compare_digest 06e8e1546d375e528a1486e1dee4fda3e585a03ef23ede85d1dad006e0eda837ebade1edde62fdc987a7f310bda69159e94ec36b79a066e0e13bbe8bf7019cfc '' ${CERTIFI} + compare_digest 9186ce4e39bb64f5931a205ffc9afac61657bc42078bc4754ed12a2b66a12b7a620583440849fc2e161d1061ac0750ddef4670f54916931ace1e9abd2a9fb09c '' ${REQUESTS} + + # Flask + compare_digest 3905022d0c398856b30d2ed6bae046c1532e87f56a0a40060030c18124c6c9c98976d9429e2ab03676c4ce75be4ea915ffc2719e04e4b4912a96e498dcd9eb89 '' ${WERKZEUG} + compare_digest 69e9b9c9ac4fdf3cfa1a3de23d14964b843989128f8cc6ea58617fc5d6ef937bcc3eae9cb32b5164b5f54b06f96bdff9bc249529f20671cc26adc9e6ce8f6bec '' ${MARKUPSAFE} + compare_digest 658d069944c81f9d8b2e90577a9d2c844b4c6a26764efefd7a86f26c05276baf6c7255f381e20e5178782be1786b7400cab12dec15653e7262b36194228bf649 '' ${JINJA2} + compare_digest 891c294867f705eb9c66274bd04ac5d93140d6e9beea6cbf9a44e7f9c13c0e2efa3554bdf56620712759a5cd579e112a782d25f3f91ba9419d60b2b4d2bc5b7c '' ${ITSDANGEROUS} + compare_digest 6b30987349df7c45c5f41cff9076ed45b178b444fca1ab1965f4ae33d1631522ce0a2868392c736666e83672b8b20e9503ae9ce5016dce3fa8f77bc8a3674130 '' ${CLICK} + compare_digest bd49cb364307569480196289fa61fbb5493e46199620333f67617367278e1f56b20fc0d40fd540bef15642a8065e488c24e97f50535e8ec143875095157d8069 '' ${FLASK} + + # Cryptography + compare_digest 326574c7542110d2cd8071136a36a6cffc7637ba948b55e0abb7f30f3821843073223301ecbec1d48b8361b0d7ccb338725eeb0424696efedc3f6bd2a23331d3 '' ${SIX} + compare_digest 7f830e1c9066ee2d297a55e2bf6db4bf6447b6d9da0145d11a88c3bb98505755fb7986eafa6e06ae0b7680838f5e5d6a6d188245ca5ad45c2a727587bac93ab5 '' ${PYCPARSER} + compare_digest fdefd3f63f56adff50723d6a88dc6db816d3d8a31b563599d2a3633ba796f6f70d5a9430510852b3d62b97357f8764f17eeab74b13df16c7cc34e1671a82373b '' ${CFFI} + compare_digest 184003c89fee74892de25c3e5ec366faea7a5f1fcca3c82b0d5e5f9f797286671a820ca54da5266d6f879ab342c97e25bce9db366c5fb1178690cd5978d4d622 '' ${CRYPTOGRAPHY} # manylinux1 + # compare_digest d8ddabe127ae8d7330d219e284de68b37fa450a27b4cf05334e9115388295b00148d9861c23b1a2e5ea9df0c33a2d27f3e4b25ce9abd3c334f1979920b19c902 '' ${CRYPTOGRAPHY} # manylinux2010 + + + # PyNaCl + compare_digest c4017c38b026a5c531b15839b8d61d1fae9907ba1960c2f97f4cd67fe0827729346d5186a6d6927ba84f64b4cbfdece12b287aa7750a039f4160831be871cea3 '' ${PYNACL} +} + + +function install_tails_setuptools { + # Download setuptools package for Tails and then authenticate and install it. + torsocks python3.7 -m pip download --no-cache-dir -r /opt/tfc/requirements-setuptools.txt --require-hashes -d $HOME/ + t_sudo mv $HOME/${SETUPTOOLS} /opt/tfc/ + compare_digest a27b38d596931dfef81d705d05689b7748ce0e02d21af4a37204fc74b0913fa7241b8135535eb7749f09af361cad90c475af98493fef11c4ad974780ee01243d '' ${SETUPTOOLS} + t_sudo python3.7 -m pip install /opt/tfc/${SETUPTOOLS} + t_sudo -E rm /opt/tfc/${SETUPTOOLS} } @@ -203,6 +280,8 @@ function remove_common_files { $1 rm /opt/tfc/requirements.txt $1 rm /opt/tfc/requirements-dev.txt $1 rm /opt/tfc/requirements-relay.txt + $1 rm /opt/tfc/requirements-relay-tails.txt + $1 rm /opt/tfc/requirements-setuptools.txt $1 rm /opt/tfc/requirements-venv.txt $1 rm -f /opt/install.sh $1 rm -f /opt/install.sh.asc @@ -222,7 +301,7 @@ function steps_before_network_kill { check_rm_existing_installation sudo torsocks apt update - sudo torsocks apt install git libssl-dev python3-pip python3-tk net-tools -y + sudo torsocks apt install git gnome-terminal libssl-dev python3-pip python3-tk net-tools -y sudo torsocks git clone --depth 1 https://github.com/maqp/tfc.git /opt/tfc verify_tcb_requirements_files @@ -261,6 +340,7 @@ function install_tcb { sudo rm -r /opt/tfc/src/relay/ sudo rm /opt/tfc/dd.py sudo rm /opt/tfc/relay.py + sudo rm /opt/tfc/tfc.yml sudo rm /opt/tfc/${VIRTUALENV} add_serial_permissions @@ -294,6 +374,7 @@ function install_local_test { # Remove unnecessary files remove_common_files "sudo" process_tcb_dependencies "rm" + sudo rm /opt/tfc/tfc.yml sudo rm /opt/tfc/${VIRTUALENV} install_complete "Installation of TFC for local testing is now complete." @@ -319,11 +400,10 @@ function install_developer { torsocks git clone https://github.com/maqp/tfc.git $HOME/tfc torsocks python3.7 -m pip install -r $HOME/tfc/requirements-venv.txt --require-hashes + python3.7 -m virtualenv $HOME/tfc/venv_tfc --system-site-packages . $HOME/tfc/venv_tfc/bin/activate - torsocks python3.7 -m pip install -r $HOME/tfc/requirements.txt --require-hashes - torsocks python3.7 -m pip install -r $HOME/tfc/requirements-relay.txt --require-hashes torsocks python3.7 -m pip install -r $HOME/tfc/requirements-dev.txt deactivate @@ -368,6 +448,7 @@ function install_relay_ubuntu { sudo rm -r /opt/tfc/src/transmitter/ sudo rm /opt/tfc/dd.py sudo rm /opt/tfc/tfc.py + sudo rm /opt/tfc/tfc.yml sudo rm /opt/tfc/${VIRTUALENV} add_serial_permissions @@ -380,28 +461,45 @@ function install_relay_tails { # Install TFC Relay configuration on Networked Computer running # Tails live distro (https://tails.boum.org/). check_tails_tor_version - read_sudo_pwd + # Apt dependencies t_sudo apt update t_sudo apt install git libssl-dev python3-pip -y || true # Ignore error in case packets can not be persistently installed - git clone --depth 1 https://github.com/maqp/tfc.git $HOME/tfc + torsocks git clone --depth 1 https://github.com/maqp/tfc.git $HOME/tfc t_sudo mv $HOME/tfc/ /opt/tfc/ + t_sudo chown -R root /opt/tfc/ verify_tcb_requirements_files verify_files + create_user_data_dir - t_sudo python3.7 -m pip download --no-cache-dir -r /opt/tfc/requirements-relay.txt --require-hashes -d /opt/tfc/ + install_tails_setuptools + torsocks python3.7 -m pip download --no-cache-dir -r /opt/tfc/requirements-venv.txt --require-hashes -d $HOME/ + torsocks python3.7 -m pip download --no-cache-dir -r /opt/tfc/requirements-relay-tails.txt --require-hashes -d $HOME/ + + move_tails_dependencies + verify_tails_dependencies + + t_sudo python3.7 -m pip install /opt/tfc/${VIRTUALENV} + t_sudo python3.7 -m virtualenv /opt/tfc/venv_relay --system-site-packages + + . /opt/tfc/venv_relay/bin/activate process_tails_dependencies "python3.7 -m pip install" + deactivate + # Complete setup t_sudo mv /opt/tfc/tfc.png /usr/share/pixmaps/ t_sudo mv /opt/tfc/launchers/TFC-RP-Tails.desktop /usr/share/applications/ + t_sudo mv /opt/tfc/tfc.yml /etc/onion-grater.d/ remove_common_files "t_sudo" process_tails_dependencies "rm" + + t_sudo rm /opt/tfc/${VIRTUALENV} t_sudo rm -r /opt/tfc/src/receiver/ t_sudo rm -r /opt/tfc/src/transmitter/ t_sudo rm /opt/tfc/dd.py @@ -419,7 +517,7 @@ function t_sudo { function install_relay { # Determine the Networked Computer OS for Relay Program installation. - if [[ "$(lsb_release -a 2>/dev/null | grep Tails)" ]]; then + if [[ "$(cat /etc/os-release 2>/dev/null | grep Tails)" ]]; then install_relay_tails else install_relay_ubuntu @@ -428,16 +526,10 @@ function install_relay { function install_virtualenv { - # Determine if OS is debian and install virtualenv as sudo so that - # the user (who should be on sudoers list) can see virtualenv on - # when the installer sets up virtual environment to /opt/tfc/. - distro=$(lsb_release -d | awk -F"\t" '{print $2}') - - if [[ "$distro" =~ ^Debian* ]]; then - sudo torsocks python3.7 -m pip install -r /opt/tfc/requirements-venv.txt --require-hashes - else - torsocks python3.7 -m pip install -r /opt/tfc/requirements-venv.txt --require-hashes - fi + # Some distros want virtualenv installed as sudo and other do + # not. Install both to improve the chances of compatibility. + sudo torsocks python3.7 -m pip install -r /opt/tfc/requirements-venv.txt --require-hashes + torsocks python3.7 -m pip install -r /opt/tfc/requirements-venv.txt --require-hashes } @@ -624,6 +716,12 @@ function root_check { function sudoer_check { # Check that the user who launched the installer is on the sudoers list. + + # Tails allows sudo without the user `amnesia` being on sudoers list. + if ! [[ "$(lsb_release -a 2>/dev/null | grep Tails)" ]]; then + return + fi + sudoers=$(getent group sudo |cut -d: -f4 | tr "," "\n") user_is_sudoer=false diff --git a/install.sh.asc b/install.sh.asc index 1d9c0f0..1c41fd5 100644 --- a/install.sh.asc +++ b/install.sh.asc @@ -1,16 +1,16 @@ -----BEGIN PGP SIGNATURE----- -iQIzBAABCAAdFiEE6o84umdLJC6ZRIRcmBNw6XJaD7oFAlxJLVgACgkQmBNw6XJa -D7rz2g//S9fHYr2YHtkW35pywZz2OqrzEjeQUgLkSrDrV6DSgKiuAnVJdqGUzu47 -xvlGXV7w7dGAppvSU7J0kd/b2LXSdB8cPyBC+yoxPt+Uym6+KA79JzggHQ9j2uFR -2st7+Gjdl/UjpP2KL4dyDyBGAuE8rDUhTMsVc7BbYIx2d3PTD6W6N6wbq97Wdjxu -aqq589jd12NIxsJbPMcNhoc9Yw38WlI4vEZaQClVjkyZA6iA4fV/cevLYeR377Cu -61NLByJ/rAfTmcHgbruNuQPUtoinV07eoPmhmpvT8UFpuH05/9+UVikl/1BbhpcX -P279oawjcjF4ZUCQDNtOHRAn+PcVBQ8GeZeN4USqlWm4m14tEdkk5hy5DaQjfmU7 -jco5SrAcWiK1wuIUdc2Z4y2heuZCGP1a1POYfBDjiS6IvXo8tMrkl/sgFlpA72lx -rbJQBRffsWkropvzrdkWo0ShkMYcmUhbUiUstUmML2L7OrI4o/wEc6ZE29MELqnC -nRrcIq7PRchJ/AHg99MWHZA74H106cWBGI82SiJsiShuQDK2S8SvvhVQ0nqPVu8B -o9GnUbuFGooT9HKjZGUVGlSE6VLArb9zzUPTe2BBgV/oRc7+DHH2P4O1VV29jbCt -sAn4wi3Eoah7igulEvvnF6kjdZ3vlkRJv/lDHc4Tz7w1CZY28yI= -=yumw +iQIzBAABCAAdFiEE6o84umdLJC6ZRIRcmBNw6XJaD7oFAlxJMGkACgkQmBNw6XJa +D7pbig//f0vdjRENVNXjVzrtr93ZWiKalltzz6tQJwRMS4Nkc6MlfVI+yEx4ahMP +cFpmRDrUov/g1F5SJYQhiaQVIsM61CLPVHnAilJmGI2g9hx8zmd6pM+ax+JeMmi5 +RSwW1ZrYfNUExLa50XtoCRf/UGFsRoG1u1Ri0xmEWJMMiYfMggFpzynhiitRBeEK +vnHdIRf0b1ZsMmoW6PGPXM6NT4swT75xbI+/MfRg2YotbczEgJFz9WLdPTZBOo/9 +3SvqfMnc+XfbY+GYpgJpD397cY6BZ/qzTge7ffn8YfaM9X0p+M27teyQjHmmpm3t +PVDSyR8f0RIV+8LtizQ5dbJ1tpG1S1UP6eMvupBuiL9LPXtpgLMByIDnfZg1czMS +llaOVbCSbFEb2yioAh+r4G/R1geKZOjmPAt9Sddlf/QakH3Jqj7+SUE/kvSHBzoV +Fcu3sgmrbCp2fWuhyU868471DkGY4MlNYDCLdYxXT7UpRoEryGo+mkp6G4ZDeorT +DK4mx3JKSw+OWVyKOXqhECUWBT/7w9CRC8GfVraan/a4dHaTkoPPwtBGqfP3T2Z/ +NiVvtPCryU2qk762sYXh1cLLRWo6BKIU6VHWQ0CDJqPWhktMXs/zWxH+MDGFoiLw +bKEbOMYZkYEy+2BpjYh19W0nQAk52t/smOM+ehhNiH8jhUiJwUM= +=thVJ -----END PGP SIGNATURE----- diff --git a/launchers/TFC-Dev.desktop b/launchers/TFC-Dev.desktop index 27c1ae4..3089430 100755 --- a/launchers/TFC-Dev.desktop +++ b/launchers/TFC-Dev.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Version=1.19.08 +Version=1.19.10 Name=TFC-Dev-LR Comment=Developer configuration Exec=terminator -m -u -g $HOME/tfc/launchers/terminator-config-dev -p tfc -l tfc-lr diff --git a/launchers/TFC-Local-test.desktop b/launchers/TFC-Local-test.desktop index 5214486..817ab29 100755 --- a/launchers/TFC-Local-test.desktop +++ b/launchers/TFC-Local-test.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Version=1.19.08 +Version=1.19.10 Name=TFC-Local-Test-LR Comment=Local testing configuration Exec=terminator -m -u -g /opt/tfc/terminator-config-local-test -p tfc -l tfc-lr diff --git a/launchers/TFC-RP-Tails.desktop b/launchers/TFC-RP-Tails.desktop index b1921d6..b15ddf2 100755 --- a/launchers/TFC-RP-Tails.desktop +++ b/launchers/TFC-RP-Tails.desktop @@ -1,7 +1,7 @@ [Desktop Entry] -Version=1.19.08 +Version=1.19.10 Name=TFC-Relay -Exec=gnome-terminal -x bash -c "cd /opt/tfc && python3.7 'relay.py' || bash" +Exec=gnome-terminal -x bash -c "cd /opt/tfc && source venv_relay/bin/activate && python3.7 'relay.py' && deactivate || bash" Icon=tfc.png Terminal=false Type=Application diff --git a/launchers/TFC-RP.desktop b/launchers/TFC-RP.desktop index 4f49fff..b15ddf2 100755 --- a/launchers/TFC-RP.desktop +++ b/launchers/TFC-RP.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Version=1.19.08 +Version=1.19.10 Name=TFC-Relay Exec=gnome-terminal -x bash -c "cd /opt/tfc && source venv_relay/bin/activate && python3.7 'relay.py' && deactivate || bash" Icon=tfc.png diff --git a/launchers/TFC-RxP.desktop b/launchers/TFC-RxP.desktop index 36957b1..c5950c8 100755 --- a/launchers/TFC-RxP.desktop +++ b/launchers/TFC-RxP.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Version=1.19.08 +Version=1.19.10 Name=TFC-Receiver Exec=gnome-terminal --maximize -x bash -c "cd /opt/tfc && source venv_tcb/bin/activate && python3.7 'tfc.py' -r && deactivate || bash" Icon=tfc.png diff --git a/launchers/TFC-TxP.desktop b/launchers/TFC-TxP.desktop index 99aef58..0202ea9 100755 --- a/launchers/TFC-TxP.desktop +++ b/launchers/TFC-TxP.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Version=1.19.08 +Version=1.19.10 Name=TFC-Transmitter Exec=gnome-terminal --maximize -x bash -c "cd /opt/tfc && source venv_tcb/bin/activate && python3.7 'tfc.py' && deactivate || bash" Icon=tfc.png diff --git a/relay.py b/relay.py index f1a0f10..899507d 100644 --- a/relay.py +++ b/relay.py @@ -31,10 +31,10 @@ from cryptography.hazmat.primitives.serialization import Encoding, PublicForma from src.common.gateway import Gateway, gateway_loop from src.common.misc import ensure_dir, monitor_processes, process_arguments from src.common.output import print_title -from src.common.statics import C_REQ_MGMT_QUEUE, C_REQ_STATE_QUEUE, CONTACT_MGMT_QUEUE, CONTACT_REQ_QUEUE, DIR_TFC -from src.common.statics import DST_COMMAND_QUEUE, DST_MESSAGE_QUEUE, EXIT_QUEUE, F_TO_FLASK_QUEUE, GATEWAY_QUEUE -from src.common.statics import GROUP_MGMT_QUEUE, GROUP_MSG_QUEUE, M_TO_FLASK_QUEUE, NC, ONION_CLOSE_QUEUE -from src.common.statics import ONION_KEY_QUEUE, SRC_TO_RELAY_QUEUE, TOR_DATA_QUEUE, URL_TOKEN_QUEUE +from src.common.statics import (CONTACT_MGMT_QUEUE, CONTACT_REQ_QUEUE, C_REQ_MGMT_QUEUE, C_REQ_STATE_QUEUE, DIR_TFC, + DST_COMMAND_QUEUE, DST_MESSAGE_QUEUE, EXIT_QUEUE, F_TO_FLASK_QUEUE, GATEWAY_QUEUE, + GROUP_MGMT_QUEUE, GROUP_MSG_QUEUE, M_TO_FLASK_QUEUE, NC, ONION_CLOSE_QUEUE, + ONION_KEY_QUEUE, SRC_TO_RELAY_QUEUE, TOR_DATA_QUEUE, URL_TOKEN_QUEUE) from src.relay.client import c_req_manager, client_scheduler, g_msg_manager from src.relay.commands import relay_command diff --git a/requirements-dev.txt b/requirements-dev.txt index c74152a..6c4eb4b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,46 @@ # Static type checking tool -mypy +mypy>=0.740 # Unit test tools -pytest -pytest-cov -pytest-xdist +pytest>=5.2.1 +pytest-cov>=2.8.1 +pytest-xdist>=1.30.0 + +# TFC dependencies (note: not authenticated with hashes) + +# pyserial +pyserial>=3.4 + +# argon2_cffi +argon2_cffi>=19.1.0 +cffi>=1.13.1 +pycparser>=2.19 +six>=1.12.0 + +# pyca/pynacl +PyNaCl>=1.3.0 +setuptools>=41.4.0 + +# pyca/cryptography +cryptography>=2.8 + +# Stem +stem>=1.7.1 + +# PySocks +pysocks>=1.7.1 + +# Requests +requests>=2.22.0 +certifi>=2019.9.11 +chardet>=3.0.4 +idna>=2.8 +urllib3>=1.25.6 + +# Flask +flask>=1.1.1 +click>=7.0 +itsdangerous>=1.1.0 +jinja2>=2.10.3 +markupsafe>=1.1.1 +werkzeug>=0.16.0 diff --git a/requirements-relay-tails.txt b/requirements-relay-tails.txt new file mode 100644 index 0000000..7affbe9 --- /dev/null +++ b/requirements-relay-tails.txt @@ -0,0 +1,38 @@ +# Sub-dependencies are listed below dependencies + +# Pyserial (Connects the Source/Destination Computer to the Networked Computer) +pyserial==3.4 --hash=sha512:8333ac2843fd136d5d0d63b527b37866f7d18afc3bb33c4938b63af077492aeb118eb32a89ac78547f14d59a2adb1e5d00728728275de62317da48dadf6cdff9 + +# Stem (Connects to Tor and manages Onion Services) +# stem==1.7.1 --hash=sha512:a275f59bba650cb5bb151cf53fb1dd820334f9abbeae1a25e64502adc854c7f54c51bc3d6c1656b595d142fc0695ffad53aab3c57bc285421c1f4f10c9c3db4c + +# PySocks (Routes requests library through SOCKS5 proxy making Onion Service connections possible) +pysocks==1.7.1 --hash=sha512:313b954102231d038d52ab58f41e3642579be29f827135b8dd92c06acb362effcb0a7fd5f35de9273372b92d9fe29f38381ae44f8b41aa90d2564d6dd07ecd12 + +# Requests (Connects to the contact's Tor Onion Service) +requests==2.22.0 --hash=sha512:9186ce4e39bb64f5931a205ffc9afac61657bc42078bc4754ed12a2b66a12b7a620583440849fc2e161d1061ac0750ddef4670f54916931ace1e9abd2a9fb09c +certifi==2019.9.11 --hash=sha512:06e8e1546d375e528a1486e1dee4fda3e585a03ef23ede85d1dad006e0eda837ebade1edde62fdc987a7f310bda69159e94ec36b79a066e0e13bbe8bf7019cfc +chardet==3.0.4 --hash=sha512:bfae58c8ea19c87cc9c9bf3d0b6146bfdb3630346bd954fe8e9f7da1f09da1fc0d6943ff04802798a665ea3b610ee2d65658ce84fe5a89f9e93625ea396a17f4 +idna==2.8 --hash=sha512:fb07dbec1de86efbad82a4f73d98123c59b083c1f1277445204bef75de99ca200377ad2f1db8924ae79b31b3dd984891c87d0a6344ec4d07a0ddbbbc655821a3 +urllib3==1.25.6 --hash=sha512:719cfa3841d0fe7c7f0a1901b8029df6685825da7f510ba61f095df64f115fae8bfa4118fa7536231ed8187cdf3385cb2d52e53c1b35b8f4aa42f7117cc4d447 + +# Flask (Onion Service web server that serves TFC public keys and ciphertexts to contacts) +flask==1.1.1 --hash=sha512:bd49cb364307569480196289fa61fbb5493e46199620333f67617367278e1f56b20fc0d40fd540bef15642a8065e488c24e97f50535e8ec143875095157d8069 +click==7.0 --hash=sha512:6b30987349df7c45c5f41cff9076ed45b178b444fca1ab1965f4ae33d1631522ce0a2868392c736666e83672b8b20e9503ae9ce5016dce3fa8f77bc8a3674130 +itsdangerous==1.1.0 --hash=sha512:891c294867f705eb9c66274bd04ac5d93140d6e9beea6cbf9a44e7f9c13c0e2efa3554bdf56620712759a5cd579e112a782d25f3f91ba9419d60b2b4d2bc5b7c +jinja2==2.10.3 --hash=sha512:658d069944c81f9d8b2e90577a9d2c844b4c6a26764efefd7a86f26c05276baf6c7255f381e20e5178782be1786b7400cab12dec15653e7262b36194228bf649 +markupsafe==1.1.1 --hash=sha512:69e9b9c9ac4fdf3cfa1a3de23d14964b843989128f8cc6ea58617fc5d6ef937bcc3eae9cb32b5164b5f54b06f96bdff9bc249529f20671cc26adc9e6ce8f6bec +werkzeug==0.16.0 --hash=sha512:3905022d0c398856b30d2ed6bae046c1532e87f56a0a40060030c18124c6c9c98976d9429e2ab03676c4ce75be4ea915ffc2719e04e4b4912a96e498dcd9eb89 + +# Cryptography (Handles URL token derivation) +cryptography==2.8 --hash=sha512:184003c89fee74892de25c3e5ec366faea7a5f1fcca3c82b0d5e5f9f797286671a820ca54da5266d6f879ab342c97e25bce9db366c5fb1178690cd5978d4d622 +cffi==1.13.1 --hash=sha512:fdefd3f63f56adff50723d6a88dc6db816d3d8a31b563599d2a3633ba796f6f70d5a9430510852b3d62b97357f8764f17eeab74b13df16c7cc34e1671a82373b +pycparser==2.19 --hash=sha512:7f830e1c9066ee2d297a55e2bf6db4bf6447b6d9da0145d11a88c3bb98505755fb7986eafa6e06ae0b7680838f5e5d6a6d188245ca5ad45c2a727587bac93ab5 +six==1.12.0 --hash=sha512:326574c7542110d2cd8071136a36a6cffc7637ba948b55e0abb7f30f3821843073223301ecbec1d48b8361b0d7ccb338725eeb0424696efedc3f6bd2a23331d3 + +# PyNaCl (Derives TFC account from Onion Service private key) +PyNaCl==1.3.0 --hash=sha512:c4017c38b026a5c531b15839b8d61d1fae9907ba1960c2f97f4cd67fe0827729346d5186a6d6927ba84f64b4cbfdece12b287aa7750a039f4160831be871cea3 +# Duplicate sub-dependencies +# cffi==1.13.1 --hash=sha512:fdefd3f63f56adff50723d6a88dc6db816d3d8a31b563599d2a3633ba796f6f70d5a9430510852b3d62b97357f8764f17eeab74b13df16c7cc34e1671a82373b +# pycparser==2.19 --hash=sha512:7f830e1c9066ee2d297a55e2bf6db4bf6447b6d9da0145d11a88c3bb98505755fb7986eafa6e06ae0b7680838f5e5d6a6d188245ca5ad45c2a727587bac93ab5 +# six==1.12.0 --hash=sha512:326574c7542110d2cd8071136a36a6cffc7637ba948b55e0abb7f30f3821843073223301ecbec1d48b8361b0d7ccb338725eeb0424696efedc3f6bd2a23331d3 diff --git a/requirements-relay.txt b/requirements-relay.txt index 435ba45..9c64c24 100644 --- a/requirements-relay.txt +++ b/requirements-relay.txt @@ -7,34 +7,33 @@ pyserial==3.4 --hash=sha512:8333ac2843fd136d5d0d63b527b37866f7d18afc3bb33c stem==1.7.1 --hash=sha512:a275f59bba650cb5bb151cf53fb1dd820334f9abbeae1a25e64502adc854c7f54c51bc3d6c1656b595d142fc0695ffad53aab3c57bc285421c1f4f10c9c3db4c # PySocks (Routes requests library through SOCKS5 proxy making Onion Service connections possible) -pysocks==1.7.0 --hash=sha512:5bbffb2714a04fb53417058703d8112c5e5dca768df627e64618e8ab8a36a8bdbc27f5d6852f39cff6b8fb4c9a5d13909f86eeb5fe9741ba42bdc985685e5d51 +pysocks==1.7.1 --hash=sha512:313b954102231d038d52ab58f41e3642579be29f827135b8dd92c06acb362effcb0a7fd5f35de9273372b92d9fe29f38381ae44f8b41aa90d2564d6dd07ecd12 # Requests (Connects to the contact's Tor Onion Service) requests==2.22.0 --hash=sha512:9186ce4e39bb64f5931a205ffc9afac61657bc42078bc4754ed12a2b66a12b7a620583440849fc2e161d1061ac0750ddef4670f54916931ace1e9abd2a9fb09c -certifi==2019.6.16 --hash=sha512:d81fe3a75ea611466d5ece7788f47c7946a4226bf4622c2accfd28c1e37b817e748609710c176c51ef2621cbc7ee200dd8d8106e738f1ef7cb96d7f2f82539cc +certifi==2019.9.11 --hash=sha512:06e8e1546d375e528a1486e1dee4fda3e585a03ef23ede85d1dad006e0eda837ebade1edde62fdc987a7f310bda69159e94ec36b79a066e0e13bbe8bf7019cfc chardet==3.0.4 --hash=sha512:bfae58c8ea19c87cc9c9bf3d0b6146bfdb3630346bd954fe8e9f7da1f09da1fc0d6943ff04802798a665ea3b610ee2d65658ce84fe5a89f9e93625ea396a17f4 idna==2.8 --hash=sha512:fb07dbec1de86efbad82a4f73d98123c59b083c1f1277445204bef75de99ca200377ad2f1db8924ae79b31b3dd984891c87d0a6344ec4d07a0ddbbbc655821a3 -urllib3==1.25.3 --hash=sha512:46d144af3633080b9ec8a642ab855b401b8224edb839c237639998b004f19b8cb191155c57e633954cf70b100d6d8b21105cd280acd1ea975aef1dec9a4a5860 +urllib3==1.25.6 --hash=sha512:719cfa3841d0fe7c7f0a1901b8029df6685825da7f510ba61f095df64f115fae8bfa4118fa7536231ed8187cdf3385cb2d52e53c1b35b8f4aa42f7117cc4d447 # Flask (Onion Service web server that serves TFC public keys and ciphertexts to contacts) flask==1.1.1 --hash=sha512:bd49cb364307569480196289fa61fbb5493e46199620333f67617367278e1f56b20fc0d40fd540bef15642a8065e488c24e97f50535e8ec143875095157d8069 click==7.0 --hash=sha512:6b30987349df7c45c5f41cff9076ed45b178b444fca1ab1965f4ae33d1631522ce0a2868392c736666e83672b8b20e9503ae9ce5016dce3fa8f77bc8a3674130 itsdangerous==1.1.0 --hash=sha512:891c294867f705eb9c66274bd04ac5d93140d6e9beea6cbf9a44e7f9c13c0e2efa3554bdf56620712759a5cd579e112a782d25f3f91ba9419d60b2b4d2bc5b7c -jinja2==2.10.1 --hash=sha512:04860c7ff7086f051368787289f75198eec3357c7da7565dc5045353122650a887e063b1a5297578ddefcc77bfdfe3d9a23c868cb3e7f18a0b5f1c475e29339e +jinja2==2.10.3 --hash=sha512:658d069944c81f9d8b2e90577a9d2c844b4c6a26764efefd7a86f26c05276baf6c7255f381e20e5178782be1786b7400cab12dec15653e7262b36194228bf649 markupsafe==1.1.1 --hash=sha512:69e9b9c9ac4fdf3cfa1a3de23d14964b843989128f8cc6ea58617fc5d6ef937bcc3eae9cb32b5164b5f54b06f96bdff9bc249529f20671cc26adc9e6ce8f6bec -werkzeug==0.15.5 --hash=sha512:19728875a846f895b7e20f1e8762455147253b295c29e4fb981f734a7ec6a491ae4a5427b0fcac54013c9fcca3d9a53d2639c00a0913c8d9ce69d8e8e24cab42 +werkzeug==0.16.0 --hash=sha512:3905022d0c398856b30d2ed6bae046c1532e87f56a0a40060030c18124c6c9c98976d9429e2ab03676c4ce75be4ea915ffc2719e04e4b4912a96e498dcd9eb89 # Cryptography (Handles URL token derivation) -cryptography==2.7 --hash=sha512:1285c3f5181da41bace4f9fd5ce5fc4bfba71143b39a4f3d8bab642db65bec9556b1965b1c2990236fed9d6b156bf81e6c0642d1531eadf7b92379c25cc4aeac -asn1crypto==0.24.0 --hash=sha512:8d9bc344981079ac6c00e71e161c34b6f403e575bbfe1ad06e30a3bcb33e0db317bdcb7aed2d18d510cb1b3ee340a649f7f77a00d271fcf3cc388e6655b67533 -cffi==1.12.3 --hash=sha512:69a2d725395a1a3585556cb44b62c49bd7f88f41ff194b60d4b9b591c4878a907c0770ef4052b588eaa9d420a53cbeb6b13237fff4054bf26ba5deaa84e25afa +cryptography==2.8 --hash=sha512:184003c89fee74892de25c3e5ec366faea7a5f1fcca3c82b0d5e5f9f797286671a820ca54da5266d6f879ab342c97e25bce9db366c5fb1178690cd5978d4d622 +cffi==1.13.1 --hash=sha512:fdefd3f63f56adff50723d6a88dc6db816d3d8a31b563599d2a3633ba796f6f70d5a9430510852b3d62b97357f8764f17eeab74b13df16c7cc34e1671a82373b pycparser==2.19 --hash=sha512:7f830e1c9066ee2d297a55e2bf6db4bf6447b6d9da0145d11a88c3bb98505755fb7986eafa6e06ae0b7680838f5e5d6a6d188245ca5ad45c2a727587bac93ab5 six==1.12.0 --hash=sha512:326574c7542110d2cd8071136a36a6cffc7637ba948b55e0abb7f30f3821843073223301ecbec1d48b8361b0d7ccb338725eeb0424696efedc3f6bd2a23331d3 # PyNaCl (Derives TFC account from Onion Service private key) PyNaCl==1.3.0 --hash=sha512:c4017c38b026a5c531b15839b8d61d1fae9907ba1960c2f97f4cd67fe0827729346d5186a6d6927ba84f64b4cbfdece12b287aa7750a039f4160831be871cea3 -setuptools==41.2.0 --hash=sha512:125341f0c22e11d2bd24c453b22e8fd7fd71605ee7a44eb61228686326eaca2e8f35b7ad4d0eacde4865f4d8cb8acb5cb5e3ff2856e756632b71af2f0dbdbee9 +setuptools==41.4.0 --hash=sha512:a27b38d596931dfef81d705d05689b7748ce0e02d21af4a37204fc74b0913fa7241b8135535eb7749f09af361cad90c475af98493fef11c4ad974780ee01243d # Duplicate sub-dependencies -# cffi==1.12.3 --hash=sha512:69a2d725395a1a3585556cb44b62c49bd7f88f41ff194b60d4b9b591c4878a907c0770ef4052b588eaa9d420a53cbeb6b13237fff4054bf26ba5deaa84e25afa +# cffi==1.13.1 --hash=sha512:fdefd3f63f56adff50723d6a88dc6db816d3d8a31b563599d2a3633ba796f6f70d5a9430510852b3d62b97357f8764f17eeab74b13df16c7cc34e1671a82373b # pycparser==2.19 --hash=sha512:7f830e1c9066ee2d297a55e2bf6db4bf6447b6d9da0145d11a88c3bb98505755fb7986eafa6e06ae0b7680838f5e5d6a6d188245ca5ad45c2a727587bac93ab5 # six==1.12.0 --hash=sha512:326574c7542110d2cd8071136a36a6cffc7637ba948b55e0abb7f30f3821843073223301ecbec1d48b8361b0d7ccb338725eeb0424696efedc3f6bd2a23331d3 diff --git a/requirements-setuptools.txt b/requirements-setuptools.txt new file mode 100644 index 0000000..50cf9ed --- /dev/null +++ b/requirements-setuptools.txt @@ -0,0 +1,2 @@ +# Setuptools (Allows installation of pycparser which is a sub-dependency of the cryptography and PyNaCl packages) +setuptools==41.4.0 --hash=sha512:a27b38d596931dfef81d705d05689b7748ce0e02d21af4a37204fc74b0913fa7241b8135535eb7749f09af361cad90c475af98493fef11c4ad974780ee01243d # Tails4: 40.8.0 OnionShare2: - diff --git a/requirements-venv.txt b/requirements-venv.txt index b8e3836..e297edf 100644 --- a/requirements-venv.txt +++ b/requirements-venv.txt @@ -1,2 +1,2 @@ # Virtual environment (Used to create an isolated Python environment for TFC dependencies) -virtualenv==16.7.3 --hash=sha512:760587ac587609607526d20d62c5ef2d768d4bc2dc1f7d5ce338d3525ec49cdb60782311dfd4b814defc486292e181a802f561508980f4eb332366355c5e8cb1 +virtualenv==16.7.7 --hash=sha512:e80eb04615d1dcd2546bd5ceef5408bbb577fa0dd725bc69f20dd7840518af575f0b41e629e8164fdaea398628813720a6f70a42e7748336601391605b79f542 diff --git a/requirements.txt b/requirements.txt index 313e7c6..b6b839a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,24 +5,23 @@ pyserial==3.4 --hash=sha512:8333ac2843fd136d5d0d63b527b37866f7d18afc3bb33c # Argon2 (Derives keys that protect persistent user data) argon2_cffi==19.1.0 --hash=sha512:77b17303a5d22fc35ac4771be5c710627c80ed7d6bf6705f70015197dbbc2b699ad6af0604b4517d1afd2f6d153058150a5d2933d38e4b4ca741e4ac560ddf72 -cffi==1.12.3 --hash=sha512:69a2d725395a1a3585556cb44b62c49bd7f88f41ff194b60d4b9b591c4878a907c0770ef4052b588eaa9d420a53cbeb6b13237fff4054bf26ba5deaa84e25afa +cffi==1.13.1 --hash=sha512:fdefd3f63f56adff50723d6a88dc6db816d3d8a31b563599d2a3633ba796f6f70d5a9430510852b3d62b97357f8764f17eeab74b13df16c7cc34e1671a82373b pycparser==2.19 --hash=sha512:7f830e1c9066ee2d297a55e2bf6db4bf6447b6d9da0145d11a88c3bb98505755fb7986eafa6e06ae0b7680838f5e5d6a6d188245ca5ad45c2a727587bac93ab5 six==1.12.0 --hash=sha512:326574c7542110d2cd8071136a36a6cffc7637ba948b55e0abb7f30f3821843073223301ecbec1d48b8361b0d7ccb338725eeb0424696efedc3f6bd2a23331d3 # PyNaCl (Handles TCB-side XChaCha20-Poly1305 symmetric encryption) PyNaCl==1.3.0 --hash=sha512:c4017c38b026a5c531b15839b8d61d1fae9907ba1960c2f97f4cd67fe0827729346d5186a6d6927ba84f64b4cbfdece12b287aa7750a039f4160831be871cea3 -setuptools==41.2.0 --hash=sha512:125341f0c22e11d2bd24c453b22e8fd7fd71605ee7a44eb61228686326eaca2e8f35b7ad4d0eacde4865f4d8cb8acb5cb5e3ff2856e756632b71af2f0dbdbee9 +setuptools==41.4.0 --hash=sha512:a27b38d596931dfef81d705d05689b7748ce0e02d21af4a37204fc74b0913fa7241b8135535eb7749f09af361cad90c475af98493fef11c4ad974780ee01243d # Duplicate sub-dependencies -# cffi==1.12.3 --hash=sha512:69a2d725395a1a3585556cb44b62c49bd7f88f41ff194b60d4b9b591c4878a907c0770ef4052b588eaa9d420a53cbeb6b13237fff4054bf26ba5deaa84e25afa +# cffi==1.13.1 --hash=sha512:fdefd3f63f56adff50723d6a88dc6db816d3d8a31b563599d2a3633ba796f6f70d5a9430510852b3d62b97357f8764f17eeab74b13df16c7cc34e1671a82373b # pycparser==2.19 --hash=sha512:7f830e1c9066ee2d297a55e2bf6db4bf6447b6d9da0145d11a88c3bb98505755fb7986eafa6e06ae0b7680838f5e5d6a6d188245ca5ad45c2a727587bac93ab5 # six==1.12.0 --hash=sha512:326574c7542110d2cd8071136a36a6cffc7637ba948b55e0abb7f30f3821843073223301ecbec1d48b8361b0d7ccb338725eeb0424696efedc3f6bd2a23331d3 # Cryptography (Handles TCB-side X448 key exchange) -cryptography==2.7 --hash=sha512:1285c3f5181da41bace4f9fd5ce5fc4bfba71143b39a4f3d8bab642db65bec9556b1965b1c2990236fed9d6b156bf81e6c0642d1531eadf7b92379c25cc4aeac -asn1crypto==0.24.0 --hash=sha512:8d9bc344981079ac6c00e71e161c34b6f403e575bbfe1ad06e30a3bcb33e0db317bdcb7aed2d18d510cb1b3ee340a649f7f77a00d271fcf3cc388e6655b67533 +cryptography==2.8 --hash=sha512:184003c89fee74892de25c3e5ec366faea7a5f1fcca3c82b0d5e5f9f797286671a820ca54da5266d6f879ab342c97e25bce9db366c5fb1178690cd5978d4d622 # Duplicate sub-dependencies -# cffi==1.12.3 --hash=sha512:69a2d725395a1a3585556cb44b62c49bd7f88f41ff194b60d4b9b591c4878a907c0770ef4052b588eaa9d420a53cbeb6b13237fff4054bf26ba5deaa84e25afa +# cffi==1.13.1 --hash=sha512:fdefd3f63f56adff50723d6a88dc6db816d3d8a31b563599d2a3633ba796f6f70d5a9430510852b3d62b97357f8764f17eeab74b13df16c7cc34e1671a82373b # pycparser==2.19 --hash=sha512:7f830e1c9066ee2d297a55e2bf6db4bf6447b6d9da0145d11a88c3bb98505755fb7986eafa6e06ae0b7680838f5e5d6a6d188245ca5ad45c2a727587bac93ab5 # six==1.12.0 --hash=sha512:326574c7542110d2cd8071136a36a6cffc7637ba948b55e0abb7f30f3821843073223301ecbec1d48b8361b0d7ccb338725eeb0424696efedc3f6bd2a23331d3 diff --git a/src/common/crypto.py b/src/common/crypto.py index d29251c..42d442b 100755 --- a/src/common/crypto.py +++ b/src/common/crypto.py @@ -48,9 +48,9 @@ from cryptography.hazmat.primitives.serialization import Encoding, PublicForma from src.common.exceptions import CriticalError from src.common.misc import separate_header -from src.common.statics import ARGON2_SALT_LENGTH, BITS_PER_BYTE, BLAKE2_DIGEST_LENGTH, BLAKE2_DIGEST_LENGTH_MAX -from src.common.statics import BLAKE2_DIGEST_LENGTH_MIN, PADDING_LENGTH, SYMMETRIC_KEY_LENGTH, TFC_PUBLIC_KEY_LENGTH -from src.common.statics import XCHACHA20_NONCE_LENGTH +from src.common.statics import (ARGON2_SALT_LENGTH, BITS_PER_BYTE, BLAKE2_DIGEST_LENGTH, BLAKE2_DIGEST_LENGTH_MAX, + BLAKE2_DIGEST_LENGTH_MIN, PADDING_LENGTH, SYMMETRIC_KEY_LENGTH, + TFC_PUBLIC_KEY_LENGTH, X448_SHARED_SECRET_LENGTH, XCHACHA20_NONCE_LENGTH) def blake2b(message: bytes, # Message to hash @@ -118,14 +118,20 @@ def blake2b(message: bytes, # Message to hash https://github.com/python/cpython/blob/3.7/Lib/hashlib.py """ try: - digest = hashlib.blake2b(message, digest_size=digest_size, key=key, salt=salt, person=person).digest() # type: bytes - - if len(digest) != digest_size: - raise CriticalError(f"BLAKE2b digest had invalid length ({len(digest)} bytes).") - + digest = hashlib.blake2b(message, + digest_size=digest_size, + key=key, + salt=salt, + person=person).digest() # type: bytes except ValueError as e: raise CriticalError(str(e)) + if not isinstance(digest, bytes): + raise CriticalError(f"BLAKE2b returned an invalid type ({type(digest)}) digest.") + + if len(digest) != digest_size: + raise CriticalError(f"BLAKE2b digest had invalid length ({len(digest)} bytes).") + return digest @@ -220,6 +226,12 @@ def argon2_kdf(password: str, # Password to derive the key from except argon2.exceptions.Argon2Error as e: raise CriticalError(str(e)) + if not isinstance(key, bytes): + raise CriticalError(f"Argon2 returned an invalid type ({type(key)}) key.") + + if len(key) != SYMMETRIC_KEY_LENGTH: + raise CriticalError(f"Derived an invalid length key from password ({len(key)} bytes).") + return key @@ -357,8 +369,11 @@ class X448(object): public_key = private_key.public_key().public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw) # type: bytes + if not isinstance(public_key, bytes): + raise CriticalError(f"Generated an invalid type ({type(public_key)}) public key.") + if len(public_key) != TFC_PUBLIC_KEY_LENGTH: - raise CriticalError(f"Generated invalid size public key from private key ({len(public_key)} bytes).") + raise CriticalError(f"Generated an invalid size public key from private key ({len(public_key)} bytes).") return public_key @@ -392,6 +407,12 @@ class X448(object): except ValueError as e: raise CriticalError(str(e)) + if not isinstance(shared_secret, bytes): # pragma: no cover + raise CriticalError(f"Derived an invalid type ({type(shared_secret)}) shared secret.") + + if len(shared_secret) != X448_SHARED_SECRET_LENGTH: # pragma: no cover + raise CriticalError(f"Generated an invalid size shared secret ({len(shared_secret)} bytes).") + return blake2b(shared_secret, digest_size=SYMMETRIC_KEY_LENGTH) @@ -548,6 +569,9 @@ def byte_padding(bytestring: bytes # Bytestring to be padded padded = padder.update(bytestring) # type: bytes padded += padder.finalize() + if not isinstance(padded, bytes): + raise CriticalError(f"Padded message had invalid type ({type(padded)}).") + if len(padded) % PADDING_LENGTH != 0: raise CriticalError(f"Padded message had an invalid length ({len(padded)}).") @@ -623,7 +647,7 @@ def csprng(key_length: int = SYMMETRIC_KEY_LENGTH # Length of the key ┃add_device ┃┃add_hwgenerator┃┃ add_input ┃┃ add_disk ┃┃add_interrupt┃ ┃_randomness┃┃ _randomness ┃┃_randomness┃┃_randomness┃┃ _randomness ┃ ┗━━━━━━━━━━━┛┗━━━━━━━━━━━━━━━┛┗━━━━━━━━━━━┛┗━━━━━━━━━━━┛┗━━━━━━━━━━━━━┛ - + [1] https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/Studies/LinuxRNG/LinuxRNG_EN.pdf?__blob=publicationFile&v=16 @@ -642,15 +666,15 @@ def csprng(key_length: int = SYMMETRIC_KEY_LENGTH # Length of the key o add_hwgenerator_randomness: HWRNGs supported by the Linux kernel, if available. The output of the HWRNG device is used to seed the ChaCha20 DRNG if needed, and then to seed the - input_pool directly when the entropy estimator's value falls + input_pool directly when the entropy estimator's value falls below the set threshold. (CPU HWRNG is not processed by the add_hwgenerator_randomness service function).[1; pp.52-54] o add_input_randomness: Key presses, mouse movements, mouse - button presses etc. Repeated event values (e.g. key presses or + button presses etc. Repeated event values (e.g. key presses or same direction mouse movements) are ignored by the service function.[1; p.44] - The event data consists of four LSBs of the event type, + The event data consists of four LSBs of the event type, four MSBs of the event code, the event code itself, and the event value, all XORed together.[1; p.45] The resulting event data is fed into the input_pool via @@ -673,7 +697,7 @@ def csprng(key_length: int = SYMMETRIC_KEY_LENGTH # Length of the key o add_interrupt_randomness: Interrupts (i.e. signals from SW/HW to processor that an event needs immediate attention) occur - hundreds of thousands of times per second under average load. + hundreds of thousands of times per second under average load. The interrupt timestamps and event data are mixed into 128-bit, per-CPU pool called fast_pool. When an interrupt occurs @@ -684,14 +708,14 @@ def csprng(key_length: int = SYMMETRIC_KEY_LENGTH # Length of the key high-resolution timestamp are XORed with the second word of the fast_pool. * The 32 MSBs and LSBs of the 64-bit CPU instruction pointer - value are XORed with the third and fourth word of the + value are XORed with the third and fourth word of the fast_pool. If no pointer is available, the XORed value is instead the return address of the add_interrupt_randomness function. - The raw entropy mixed into the fast_pool is then distributed + The raw entropy mixed into the fast_pool is then distributed more evenly with a function called fast_mix. - The content of the fast_pool is mixed into the input_pool - once it has data about at least 64 interrupt events, and + The content of the fast_pool is mixed into the input_pool + once it has data about at least 64 interrupt events, and (unless the ChaCha20 DRNG is being seeded) at least one second has passed since the fast_pool was last mixed in. The counter keeping track of the interrupt events is then zeroed. @@ -721,16 +745,16 @@ def csprng(key_length: int = SYMMETRIC_KEY_LENGTH # Length of the key - AMD: A set of 16 ring oscillator chains feeds 512 bits of raw entropy to AES256-CBC-MAC based conditioner - again available via RDSEED instruction. The + again available via RDSEED instruction. The conditioner is used to produce 128-bit seeds -- - a process that is repeated thrice to create a - 384-bit seed for the AES256-CTR based DRBG - available via the RDRAND instruction. The DRBG is + a process that is repeated thrice to create a + 384-bit seed for the AES256-CTR based DRBG + available via the RDRAND instruction. The DRBG is reseeded at least every 2048 queries of 32-bits (8kB).[4; pp.2-3] While the RDSEED/RDRAND instructions are used extensively, - because the CPU HWRNG is not an auditable source, it is + because the CPU HWRNG is not an auditable source, it is assumed to provide only a very small amount of entropy. [1; p.83] @@ -765,7 +789,7 @@ def csprng(key_length: int = SYMMETRIC_KEY_LENGTH # Length of the key Initialization of the input_pool -------------------------------- - The input_pool is initialized during boot time of the kernel by + The input_pool is initialized during boot time of the kernel by mixing following data into the entropy pool: 1. The current time with nanosecond precision (64-bit CPUs). 2. Entropy obtained from CPU HWRNG via RDRAND instruction, if @@ -776,7 +800,7 @@ def csprng(key_length: int = SYMMETRIC_KEY_LENGTH # Length of the key Initial seeding and seeding levels of the input_pool ---------------------------------------------------- After a hardware event has occurred, the entropy of the event value - is estimated, and both values are mixed into the input_pool using a + is estimated, and both values are mixed into the input_pool using a function based on a linear feedback shift register (LFSR), one byte at a time.[1; p.23] @@ -805,7 +829,7 @@ def csprng(key_length: int = SYMMETRIC_KEY_LENGTH # Length of the key The "SHA-1" digest is mixed back into the input_pool using the LFSR-based state transition function to provide backtracking resistance.[1; p.18] - If more than 80-bits of entropy is requested, the + If more than 80-bits of entropy is requested, the hash-fold-yield-mix-back operation is repeated until the requested number of bytes are generated. (Reseeding the ChaCha20 DRNG requires four consecutive requests.)[1; p.18] @@ -839,7 +863,7 @@ def csprng(key_length: int = SYMMETRIC_KEY_LENGTH # Length of the key - 4-byte counter (the counter is actually 64-bits[2]) - 12-byte nonce - In addition, the DRNG state contains a 4-byte timestamp called + In addition, the DRNG state contains a 4-byte timestamp called init_time, that keeps track of when the DRNG was last seeded. [1; pp.32-33] @@ -866,9 +890,9 @@ def csprng(key_length: int = SYMMETRIC_KEY_LENGTH # Length of the key input_pool the next time it is called.[1; p.35] **Initially seeded state** - During initialization time of the kernel, the kernel injects four - sets of data from fast_pool into the DRNG (instead of the - input_pool). Each set contains event data and timestamps of 64 + During initialization time of the kernel, the kernel injects four + sets of data from fast_pool into the DRNG (instead of the + input_pool). Each set contains event data and timestamps of 64 interrupt events from add_interrupt_randomness.[1; p.35] In addition, all content from the add_device_randomness source is mixed into the DRNG key state using an LFSR with a period of 255. @@ -920,7 +944,7 @@ def csprng(key_length: int = SYMMETRIC_KEY_LENGTH # Length of the key The ChaCha20 DRNG is reseeded automatically every 300 seconds irrespective of the amount of data produced by the DRNG[1; p.32]. The DRNG is reseeded by obtaining 128..256 bits of entropy - from the input_pool. In the order of preference, the entropy from + from the input_pool. In the order of preference, the entropy from the input_pool is XORed with the output of 1. 32-byte value obtained via RDSEED CPU instruction, or 2. 32-byte value obtained via RDRAND CPU instruction, or @@ -939,7 +963,7 @@ def csprng(key_length: int = SYMMETRIC_KEY_LENGTH # Length of the key GETRANDOM and Python ==================== - Since Python 3.6.0, `os.urandom` has been a wrapper for the best + Since Python 3.6.0, `os.urandom` has been a wrapper for the best available CSPRNG. The 3.17 and earlier versions of the Linux kernel do not support the GETRANDOM call, and Python 3.7's `os.urandom` will in those cases fall back to non-blocking `/dev/urandom` that is @@ -952,7 +976,7 @@ def csprng(key_length: int = SYMMETRIC_KEY_LENGTH # Length of the key the kernel version required by TFC is bumped to 4.8, and to make sure the ChaCha20 DRNG is always seeded from input_pool before its considered fully seeded, the final minimum requirement is 4.17). - The flag 0 means GETRANDOM will block if the DRNG is not fully + The flag 0 means GETRANDOM will block if the DRNG is not fully seeded.[1] Quoting PEP 524 [2]: @@ -1000,6 +1024,9 @@ def csprng(key_length: int = SYMMETRIC_KEY_LENGTH # Length of the key entropy = os.getrandom(key_length, flags=0) # type: bytes + if not isinstance(entropy, bytes): + raise CriticalError(f"GETRANDOM returned invalid type data ({type(entropy)}).") + if len(entropy) != key_length: raise CriticalError(f"GETRANDOM returned invalid amount of entropy ({len(entropy)} bytes).") diff --git a/src/common/db_contacts.py b/src/common/db_contacts.py index 25820fd..3d035fc 100755 --- a/src/common/db_contacts.py +++ b/src/common/db_contacts.py @@ -30,7 +30,10 @@ from src.common.encoding import bytes_to_bool, onion_address_to_pub_key, bytes from src.common.exceptions import CriticalError from src.common.misc import ensure_dir, get_terminal_width, separate_headers, split_byte_string from src.common.output import clear_screen -from src.common.statics import * +from src.common.statics import (CONTACT_LENGTH, CONTACT_LIST_INDENT, DIR_USER_DATA, DUMMY_CONTACT, DUMMY_NICK, ECDHE, + ENCODED_BOOLEAN_LENGTH, FINGERPRINT_LENGTH, KEX_STATUS_HAS_RX_PSK, KEX_STATUS_LENGTH, + KEX_STATUS_NONE, KEX_STATUS_NO_RX_PSK, KEX_STATUS_PENDING, KEX_STATUS_UNVERIFIED, + KEX_STATUS_VERIFIED, LOCAL_ID, ONION_SERVICE_PUBLIC_KEY_LENGTH, PSK) if typing.TYPE_CHECKING: from src.common.db_masterkey import MasterKey diff --git a/src/common/db_groups.py b/src/common/db_groups.py index c62decc..a4c19d3 100755 --- a/src/common/db_groups.py +++ b/src/common/db_groups.py @@ -32,7 +32,10 @@ from src.common.encoding import bytes_to_bool, bytes_to_int, bytes_to_str from src.common.exceptions import CriticalError from src.common.misc import ensure_dir, get_terminal_width, round_up, separate_header, separate_headers from src.common.misc import split_byte_string -from src.common.statics import * +from src.common.statics import (CONTACT_LIST_INDENT, DIR_USER_DATA, DUMMY_GROUP, DUMMY_MEMBER, + ENCODED_BOOLEAN_LENGTH, ENCODED_INTEGER_LENGTH, GROUP_DB_HEADER_LENGTH, + GROUP_ID_LENGTH, GROUP_STATIC_LENGTH, ONION_SERVICE_PUBLIC_KEY_LENGTH, + PADDED_UTF32_STR_LENGTH) if typing.TYPE_CHECKING: from src.common.db_contacts import ContactList diff --git a/src/common/db_keys.py b/src/common/db_keys.py index 168ada2..875c0e6 100644 --- a/src/common/db_keys.py +++ b/src/common/db_keys.py @@ -29,7 +29,10 @@ from src.common.encoding import int_to_bytes, onion_address_to_pub_key from src.common.encoding import bytes_to_int from src.common.exceptions import CriticalError from src.common.misc import ensure_dir, separate_headers, split_byte_string -from src.common.statics import * +from src.common.statics import (DIR_USER_DATA, DUMMY_CONTACT, HARAC_LENGTH, INITIAL_HARAC, KDB_ADD_ENTRY_HEADER, + KDB_CHANGE_MASTER_KEY_HEADER, KDB_REMOVE_ENTRY_HEADER, KDB_UPDATE_SIZE_HEADER, + KEYSET_LENGTH, LOCAL_PUBKEY, ONION_SERVICE_PUBLIC_KEY_LENGTH, RX, + SYMMETRIC_KEY_LENGTH, TX) if typing.TYPE_CHECKING: from src.common.db_masterkey import MasterKey diff --git a/src/common/db_logs.py b/src/common/db_logs.py index f407983..4c0ff9d 100644 --- a/src/common/db_logs.py +++ b/src/common/db_logs.py @@ -34,7 +34,12 @@ from src.common.encoding import b58encode, bytes_to_bool, bytes_to_timestamp, from src.common.exceptions import CriticalError, FunctionReturn from src.common.misc import ensure_dir, get_terminal_width, ignored, separate_header, separate_headers from src.common.output import clear_screen -from src.common.statics import * +from src.common.statics import (ASSEMBLY_PACKET_HEADER_LENGTH, DIR_USER_DATA, GROUP_ID_LENGTH, GROUP_MESSAGE_HEADER, + GROUP_MSG_ID_LENGTH, LOGFILE_MASKING_QUEUE, LOG_ENTRY_LENGTH, LOG_PACKET_QUEUE, + LOG_SETTING_QUEUE, MESSAGE, MESSAGE_HEADER_LENGTH, ONION_SERVICE_PUBLIC_KEY_LENGTH, + ORIGIN_HEADER_LENGTH, ORIGIN_USER_HEADER, PLACEHOLDER_DATA, PRIVATE_MESSAGE_HEADER, + P_N_HEADER, RX, TIMESTAMP_LENGTH, TRAFFIC_MASKING_QUEUE, TX, UNIT_TEST_QUEUE, + WHISPER_FIELD_LENGTH, WIN_TYPE_CONTACT, WIN_TYPE_GROUP) from src.receiver.packet import PacketList from src.receiver.windows import RxWindow @@ -52,7 +57,7 @@ MsgTuple = Tuple[datetime, str, bytes, bytes, bool, bool] def log_writer_loop(queues: Dict[bytes, 'Queue[Any]'], # Dictionary of queues settings: 'Settings', # Settings object - unit_test: bool = False # When True, exits the loop when UNIT_TEST_QUEUE is no longer empty. + unit_test: bool = False # True, exits loop when UNIT_TEST_QUEUE is no longer empty. ) -> None: """Write assembly packets to log database. @@ -299,7 +304,7 @@ def change_log_db_key(previous_key: bytes, temp_name = f'{file_name}_temp' if not os.path.isfile(file_name): - raise FunctionReturn("Error: Could not find log database.") + raise FunctionReturn("No log database available.") if os.path.isfile(temp_name): os.remove(temp_name) diff --git a/src/common/db_masterkey.py b/src/common/db_masterkey.py index 4cf4cd7..b34ca72 100755 --- a/src/common/db_masterkey.py +++ b/src/common/db_masterkey.py @@ -19,11 +19,13 @@ You should have received a copy of the GNU General Public License along with TFC. If not, see . """ +import math import multiprocessing import os.path +import random import time -from typing import Tuple +from typing import List, Tuple from src.common.crypto import argon2_kdf, blake2b, csprng from src.common.encoding import bytes_to_int, int_to_bytes @@ -31,7 +33,11 @@ from src.common.exceptions import CriticalError, graceful_exit from src.common.input import pwd_prompt from src.common.misc import ensure_dir, separate_headers from src.common.output import clear_screen, m_print, phase, print_on_previous_line -from src.common.statics import * +from src.common.word_list import eff_wordlist +from src.common.statics import (ARGON2_MIN_MEMORY_COST, ARGON2_MIN_PARALLELISM, ARGON2_MIN_TIME_COST, + ARGON2_SALT_LENGTH, BLAKE2_DIGEST_LENGTH, DIR_USER_DATA, DONE, + ENCODED_INTEGER_LENGTH, GENERATE, MASTERKEY_DB_SIZE, MAX_KEY_DERIVATION_TIME, + MIN_KEY_DERIVATION_TIME, PASSWORD_MIN_BIT_STRENGTH, RESET) class MasterKey(object): @@ -69,13 +75,30 @@ class MasterKey(object): return master_key, kd_time @staticmethod - def get_free_memory() -> int: - """Return the amount of free memory in the system.""" - fields = os.popen("cat /proc/meminfo").read().splitlines() - field = [f for f in fields if f.startswith('MemFree')][0] - mem_free = int(field.split()[1]) + def get_available_memory() -> int: + """Return the amount of available memory in the system.""" + fields = os.popen("cat /proc/meminfo").read().splitlines() + field = [f for f in fields if f.startswith('MemAvailable')][0] + mem_avail = int(field.split()[1]) - return mem_free + return mem_avail + + @staticmethod + def generate_master_password() -> Tuple[int, str]: + """Generate a strong password using the EFF wordlist.""" + word_space = len(eff_wordlist) + sys_rand = random.SystemRandom() + + pwd_bit_strength = 0.0 + password_words = [] # type: List[str] + + while pwd_bit_strength < PASSWORD_MIN_BIT_STRENGTH: + password_words.append(sys_rand.choice(eff_wordlist)) + pwd_bit_strength = math.log2(word_space ** len(password_words)) + + password = ' '.join(password_words) + + return int(pwd_bit_strength), password def new_master_key(self) -> bytes: """Create a new master key from password and salt. @@ -143,7 +166,7 @@ class MasterKey(object): time_cost = ARGON2_MIN_TIME_COST # Determine the amount of memory used from the amount of free RAM in the system. - memory_cost = self.get_free_memory() + memory_cost = self.get_available_memory() if self.local_test: memory_cost //= 2 @@ -180,8 +203,8 @@ class MasterKey(object): middle = (lower_bound + upper_bound) // 2 master_key, kd_time = self.timed_key_derivation(password, salt, time_cost, middle, parallelism) - # End of search might happen e.g. if external CPU load causes delay in key derivation, which causes - # the search to continue into wrong branch. In such situation the search is restarted. The binary search + # The search might fail e.g. if external CPU load causes delay in key derivation, which causes the + # search to continue into wrong branch. In such a situation the search is restarted. The binary search # is problematic with tight key derivation time target ranges, so if the search keeps restarting, # increasing MAX_KEY_DERIVATION_TIME (and thus expanding the range) will help finding suitable # memory_cost value faster. Increasing MAX_KEY_DERIVATION_TIME slightly affects security (positively) @@ -251,7 +274,19 @@ class MasterKey(object): def new_password(cls, purpose: str = "master password") -> str: """Prompt the user to enter and confirm a new password.""" password_1 = pwd_prompt(f"Enter a new {purpose}: ") - password_2 = pwd_prompt(f"Confirm the {purpose}: ", repeat=True) + + if password_1 == GENERATE: + pwd_bit_strength, password_1 = MasterKey.generate_master_password() + + m_print([f"Generated a {pwd_bit_strength}-bit password:", + '', password_1, '', + "Write down this password and dispose of the copy once you remember it.", + "Press to continue."], manual_proceed=True, box=True, head=1, tail=1) + os.system(RESET) + + password_2 = password_1 + else: + password_2 = pwd_prompt(f"Confirm the {purpose}: ", repeat=True) if password_1 == password_2: return password_1 diff --git a/src/common/db_onion.py b/src/common/db_onion.py index b7f8ef0..5e2a819 100644 --- a/src/common/db_onion.py +++ b/src/common/db_onion.py @@ -29,7 +29,7 @@ from src.common.encoding import pub_key_to_onion_address, pub_key_to_short_add from src.common.exceptions import CriticalError from src.common.misc import ensure_dir from src.common.output import phase -from src.common.statics import * +from src.common.statics import CONFIRM_CODE_LENGTH, DIR_USER_DATA, DONE, ONION_SERVICE_PRIVATE_KEY_LENGTH, TX if typing.TYPE_CHECKING: from src.common.db_masterkey import MasterKey diff --git a/src/common/db_settings.py b/src/common/db_settings.py index c738227..fb6969e 100755 --- a/src/common/db_settings.py +++ b/src/common/db_settings.py @@ -32,7 +32,9 @@ from src.common.exceptions import CriticalError, FunctionReturn from src.common.input import yes from src.common.misc import ensure_dir, get_terminal_width, round_up from src.common.output import clear_screen, m_print -from src.common.statics import * +from src.common.statics import (DIR_USER_DATA, ENCODED_BOOLEAN_LENGTH, ENCODED_FLOAT_LENGTH, ENCODED_INTEGER_LENGTH, + MAX_INT, SETTINGS_INDENT, TRAFFIC_MASKING_MIN_RANDOM_DELAY, + TRAFFIC_MASKING_MIN_STATIC_DELAY, TX) if typing.TYPE_CHECKING: from src.common.db_contacts import ContactList @@ -183,7 +185,7 @@ class Settings(object): raise CriticalError("Invalid attribute type in settings.") except (KeyError, ValueError): - raise FunctionReturn(f"Error: Invalid value '{value_str}'.", head_clear=True) + raise FunctionReturn(f"Error: Invalid setting value '{value_str}'.", head_clear=True) self.validate_key_value_pair(key, value, contact_list, group_list) diff --git a/src/common/encoding.py b/src/common/encoding.py index 534b3cc..8322a3d 100755 --- a/src/common/encoding.py +++ b/src/common/encoding.py @@ -26,7 +26,9 @@ import struct from datetime import datetime from typing import List, Union -from src.common.statics import * +from src.common.statics import (B58_ALPHABET, B58_CHECKSUM_LENGTH, MAINNET_HEADER, ONION_ADDRESS_CHECKSUM_ID, + ONION_ADDRESS_CHECKSUM_LENGTH, ONION_SERVICE_VERSION, ONION_SERVICE_VERSION_LENGTH, + PADDING_LENGTH, TESTNET_HEADER, TRUNC_ADDRESS_LENGTH) def sha256d(message: bytes) -> bytes: @@ -43,11 +45,7 @@ def b58encode(byte_string: bytes, public_key: bool = False) -> str: (WIF) for mainnet and testnet addresses. https://en.bitcoin.it/wiki/Wallet_import_format """ - - mainnet_header = b'\x80' - testnet_header = b'\xef' - net_id = testnet_header if public_key else mainnet_header - + net_id = TESTNET_HEADER if public_key else MAINNET_HEADER byte_string = net_id + byte_string byte_string += sha256d(byte_string)[:B58_CHECKSUM_LENGTH] @@ -70,11 +68,7 @@ def b58encode(byte_string: bytes, public_key: bool = False) -> str: def b58decode(string: str, public_key: bool = False) -> bytes: """Decode a Base58-encoded string and verify the checksum.""" - - mainnet_header = b'\x80' - testnet_header = b'\xef' - net_id = testnet_header if public_key else mainnet_header - + net_id = TESTNET_HEADER if public_key else MAINNET_HEADER orig_len = len(string) string = string.lstrip(B58_ALPHABET[0]) new_len = len(string) @@ -129,10 +123,10 @@ def b10encode(fingerprint: bytes) -> str: (fingerprints are usually read aloud over off band call). Base10 has 41% efficiency but natural languages have evolved in a - way that makes a clear distinction between the way different numbers - are pronounced: reading them is faster and less error-prone. - Compliments to Signal/WA developers for discovering this. - https://signal.org/blog/safety-number-updates/ + way that makes a clear distinction between the way different + numbers are pronounced: reading them is faster and less + error-prone. Compliments to Signal/WA developers for + discovering this: https://signal.org/blog/safety-number-updates/ """ return str(int(fingerprint.hex(), base=16)) diff --git a/src/common/exceptions.py b/src/common/exceptions.py index 9ea41c4..b6e5f59 100755 --- a/src/common/exceptions.py +++ b/src/common/exceptions.py @@ -27,7 +27,7 @@ from datetime import datetime from typing import Optional from src.common.output import clear_screen, m_print -from src.common.statics import * +from src.common.statics import TFC if typing.TYPE_CHECKING: from src.receiver.windows import RxWindow diff --git a/src/common/gateway.py b/src/common/gateway.py index 71676a0..7b73006 100644 --- a/src/common/gateway.py +++ b/src/common/gateway.py @@ -31,7 +31,7 @@ import time import typing from datetime import datetime -from typing import Any, Dict, Optional, Tuple, Union +from typing import Dict, Optional, Tuple, Union from serial.serialutil import SerialException @@ -41,13 +41,16 @@ from src.common.misc import calculate_race_condition_delay, ensure_dir, from src.common.misc import separate_trailer from src.common.output import m_print, phase, print_on_previous_line from src.common.reed_solomon import ReedSolomonError, RSCodec -from src.common.statics import * +from src.common.statics import (BAUDS_PER_BYTE, DIR_USER_DATA, DONE, DST_DD_LISTEN_SOCKET, DST_LISTEN_SOCKET, + GATEWAY_QUEUE, LOCALHOST, LOCAL_TESTING_PACKET_DELAY, MAX_INT, NC, + PACKET_CHECKSUM_LENGTH, RECEIVER, RELAY, RP_LISTEN_SOCKET, RX, + SERIAL_RX_MIN_TIMEOUT, SETTINGS_INDENT, SRC_DD_LISTEN_SOCKET, TRANSMITTER, TX) if typing.TYPE_CHECKING: from multiprocessing import Queue -def gateway_loop(queues: Dict[bytes, 'Queue[Any]'], +def gateway_loop(queues: Dict[bytes, 'Queue[Tuple[datetime, bytes]]'], gateway: 'Gateway', unit_test: bool = False ) -> None: @@ -131,9 +134,9 @@ class Gateway(object): the time it takes to send one byte with given baud rate. """ try: - serial_interface = self.search_serial_interface() - baudrate = self.settings.session_serial_baudrate - self.tx_serial = self.rx_serial = serial.Serial(serial_interface, baudrate, timeout=0) + self.tx_serial = self.rx_serial = serial.Serial(self.search_serial_interface(), + self.settings.session_serial_baudrate, + timeout=0) except SerialException: raise CriticalError("SerialException. Ensure $USER is in the dialout group by restarting this computer.") @@ -509,7 +512,7 @@ class GatewaySettings(object): raise CriticalError("Invalid attribute type in settings.") except (KeyError, ValueError): - raise FunctionReturn(f"Error: Invalid value '{value_str}'.", delay=1, tail_clear=True) + raise FunctionReturn(f"Error: Invalid setting value '{value_str}'.", delay=1, tail_clear=True) self.validate_key_value_pair(key, value) diff --git a/src/common/input.py b/src/common/input.py index edae7f6..060524e 100644 --- a/src/common/input.py +++ b/src/common/input.py @@ -28,7 +28,8 @@ from src.common.encoding import b58decode from src.common.exceptions import CriticalError from src.common.misc import get_terminal_width, terminal_width_check from src.common.output import clear_screen, m_print, print_on_previous_line, print_spacing -from src.common.statics import * +from src.common.statics import (B58_LOCAL_KEY, B58_LOCAL_KEY_GUIDE, B58_PUBLIC_KEY, B58_PUBLIC_KEY_GUIDE, + CURSOR_UP_ONE_LINE, ECDHE, NC_BYPASS_START, NC_BYPASS_STOP) if typing.TYPE_CHECKING: from src.common.db_settings import Settings diff --git a/src/common/misc.py b/src/common/misc.py index 5d81f60..6e20450 100755 --- a/src/common/misc.py +++ b/src/common/misc.py @@ -34,12 +34,17 @@ import zlib from contextlib import contextmanager from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type, Union -from multiprocessing import Process, Queue +from multiprocessing import Process from src.common.reed_solomon import RSCodec -from src.common.statics import * +from src.common.statics import (BAUDS_PER_BYTE, COMMAND_LENGTH, CURSOR_UP_ONE_LINE, DIR_RECV_FILES, DIR_USER_DATA, + DUMMY_CONTACT, DUMMY_GROUP, DUMMY_MEMBER, ECDHE, EVENT, EXIT, EXIT_QUEUE, LOCAL_ID, + LOCAL_PUBKEY, ME, ONION_ADDRESS_CHECKSUM_ID, ONION_ADDRESS_CHECKSUM_LENGTH, + ONION_ADDRESS_LENGTH, ONION_SERVICE_PUBLIC_KEY_LENGTH, PACKET_LENGTH, + PADDING_LENGTH, POWEROFF, PSK, RX, TAILS, TX, WIPE) if typing.TYPE_CHECKING: + from multiprocessing import Queue from src.common.db_contacts import ContactList from src.common.db_groups import GroupList from src.common.db_settings import Settings @@ -164,7 +169,7 @@ def ignored(*exceptions: Type[BaseException]) -> Iterator[Any]: def monitor_processes(process_list: List[Process], software_operation: str, - queues: Dict[bytes, 'Queue[Any]'], + queues: Dict[bytes, 'Queue[bytes]'], error_exit_code: int = 1 ) -> None: """Monitor the status of `process_list` and EXIT_QUEUE. @@ -195,7 +200,9 @@ def monitor_processes(process_list: List[Process], sys.exit(0) if command == WIPE: - if TAILS not in subprocess.check_output('lsb_release -a', shell=True): + with open('/etc/os-release') as f: + data = f.read() + if TAILS not in data: if software_operation == RX: subprocess.Popen("find {} -type f -exec shred -n 3 -z -u {{}} \\;" .format(DIR_RECV_FILES), shell=True).wait() diff --git a/src/common/output.py b/src/common/output.py index 09ff19a..a3e5bb5 100644 --- a/src/common/output.py +++ b/src/common/output.py @@ -29,7 +29,10 @@ from typing import List, Optional, Union from src.common.encoding import b10encode, b58encode, pub_key_to_onion_address from src.common.misc import get_terminal_width, split_string -from src.common.statics import * +from src.common.statics import (ADDED_MEMBERS, ALREADY_MEMBER, B58_LOCAL_KEY_GUIDE, B58_PUBLIC_KEY_GUIDE, BOLD_ON, + CLEAR_ENTIRE_LINE, CLEAR_ENTIRE_SCREEN, CURSOR_LEFT_UP_CORNER, CURSOR_UP_ONE_LINE, + DONE, NC, NEW_GROUP, NORMAL_TEXT, NOT_IN_GROUP, RECEIVER, RELAY, REMOVED_MEMBERS, RX, + TFC, TRANSMITTER, TX, UNKNOWN_ACCOUNTS, VERSION) if typing.TYPE_CHECKING: from src.common.db_contacts import ContactList @@ -205,9 +208,9 @@ def print_key(message: str, # Instructive messag if settings.local_testing_mode: m_print([message, b58key], box=True) else: - guide, chunk_len = (B58_PUBLIC_KEY_GUIDE, 7) if public_key else (B58_LOCAL_KEY_GUIDE, 3) + guide, chunk_length = (B58_PUBLIC_KEY_GUIDE, 7) if public_key else (B58_LOCAL_KEY_GUIDE, 3) - key = ' '.join(split_string(b58key, item_len=chunk_len)) + key = ' '.join(split_string(b58key, item_len=chunk_length)) m_print([message, guide, key], box=True) diff --git a/src/common/reed_solomon.py b/src/common/reed_solomon.py index 059f731..f11552f 100644 --- a/src/common/reed_solomon.py +++ b/src/common/reed_solomon.py @@ -197,11 +197,11 @@ class ReedSolomonError(Exception): """ -For efficiency, gf_exp[] has size 2*GF_SIZE, so that a simple -multiplication of two numbers can be resolved without calling % 255. -For more info on how to generate this extended exponentiation table, -see paper: - "Fast software implementation of finite field operations", +For efficiency, gf_exp[] has size 2*GF_SIZE, so that a simple +multiplication of two numbers can be resolved without calling % 255. +For more info on how to generate this extended exponentiation table, +see paper: + "Fast software implementation of finite field operations", Cheng Huang and Lihao Xu Washington University in St. Louis, Tech. Rep (2003). """ @@ -1568,12 +1568,12 @@ class RSCodec(object): """ def __init__(self, - nsym: int = 10, - nsize: int = 255, - fcr: int = 0, - prim: int = 0x11d, - generator: int = 2, - c_exp: int = 8, + nsym: int = 10, + nsize: int = 255, + fcr: int = 0, + prim: int = 0x11d, + generator: int = 2, + c_exp: int = 8, single_gen: bool = True ) -> None: """\ diff --git a/src/common/statics.py b/src/common/statics.py index c1369f1..5ca87e0 100644 --- a/src/common/statics.py +++ b/src/common/statics.py @@ -21,7 +21,7 @@ along with TFC. If not, see . """Program details""" TFC = 'TFC' -VERSION = '1.19.08' +VERSION = '1.19.10' TRANSMITTER = 'Transmitter' RECEIVER = 'Receiver' RELAY = 'Relay' @@ -41,7 +41,7 @@ DUMMY_GROUP = 'dummy_group' TX = 'tx' RX = 'rx' NC = 'nc' -TAILS = b'Tails' +TAILS = 'TAILS_PRODUCT_NAME="Tails"' """Window identifiers""" @@ -71,8 +71,10 @@ NOT_IN_GROUP = 'not_in_group' UNKNOWN_ACCOUNTS = 'unknown_accounts' -"""Base58 alphabet""" -B58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' +"""Base58 encoding""" +B58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' +MAINNET_HEADER = b'\x80' +TESTNET_HEADER = b'\xef' """Base58 key types""" @@ -81,7 +83,7 @@ B58_LOCAL_KEY = 'b58_local_key' """Base58 key input guides""" -B58_PUBLIC_KEY_GUIDE = ' A B C D E F H H I J K L ' +B58_PUBLIC_KEY_GUIDE = ' A B C D E F G H I J K L ' B58_LOCAL_KEY_GUIDE = ' A B C D E F G H I J K L M N O P Q ' @@ -99,7 +101,9 @@ NOTIFY = 'notify' """Command identifiers""" CLEAR = 'clear' RESET = 'reset' -POWEROFF = 'poweroff' +POWEROFF = 'systemctl poweroff' +GENERATE = 'generate' + """Contact setting management""" CONTACT_SETTING_HEADER_LENGTH = 2 @@ -130,7 +134,7 @@ NCDCRL = 'ncdcrl' """VT100 codes -VT100 codes are used to control printing to the terminal. These make +VT100 codes are used to control printing to the terminal. These make building functions like textbox drawers possible. """ CURSOR_UP_ONE_LINE = '\x1b[1A' @@ -144,7 +148,7 @@ NORMAL_TEXT = '\033[0m' """Separators -Separator byte is a non-printable byte used to separate fields in +Separator byte is a non-printable byte used to separate fields in serialized data structures. """ US_BYTE = b'\x1f' @@ -157,44 +161,44 @@ serial or over the network. They tell the receiving device what type of datagram is in question. Datagrams with local key header contain the encrypted local key, used to -encrypt commands and data transferred between local Source and -Destination computers. Packets with the header are only accepted by the -Relay Program when they originate from the user's Source Computer. Even -if the Networked Computer is compromised and the local key datagram is -injected to the Destination Computer, the injected key could not be +encrypt commands and data transferred between local Source and +Destination computers. Packets with the header are only accepted by the +Relay Program when they originate from the user's Source Computer. Even +if the Networked Computer is compromised and the local key datagram is +injected to the Destination Computer, the injected key could not be accepted by the user as they don't know the decryption key for it. The -worst case scenario is a DoS attack where the Receiver Program receives -new local keys continuously. Such an attack would, however, reveal the -user they are under a sophisticated attack, and that their Networked +worst case scenario is a DoS attack where the Receiver Program receives +new local keys continuously. Such an attack would, however, reveal the +user they are under a sophisticated attack, and that their Networked Computer has been compromised. -Datagrams with Public key header contain TCB-level public keys that +Datagrams with Public key header contain TCB-level public keys that originate from the sender's Source Computer, and are displayed by the recipient's Networked Computer, from where they are manually typed to recipient's Destination Computer. -Message and command type datagrams tell the Receiver Program whether to -parse the trailing fields that determine which XChaCha20-Poly1305 -decryption keys it should load. Contacts can of course try to alter -their datagrams to contain a COMMAND_DATAGRAM_HEADER header, but Relay -Program will by design drop them. Even if a compromised Networked -Computer injects such a datagram to Destination Computer, the Receiver -Program will drop the datagram when the MAC verification of the +Message and command type datagrams tell the Receiver Program whether to +parse the trailing fields that determine which XChaCha20-Poly1305 +decryption keys it should load. Contacts can of course try to alter +their datagrams to contain a COMMAND_DATAGRAM_HEADER header, but Relay +Program will by design drop them. Even if a compromised Networked +Computer injects such a datagram to Destination Computer, the Receiver +Program will drop the datagram when the MAC verification of the encrypted hash ratchet counter value fails. -File type datagram contains an encrypted file that the Receiver Program -caches until its decryption key arrives from the sender inside a +File type datagram contains an encrypted file that the Receiver Program +caches until its decryption key arrives from the sender inside a special, automated key delivery message. -Unencrypted type datagrams contain commands intended for the Relay -Program. These commands are in some cases preceded by an encrypted -version of the command, that the Relay Program forwards to Receiver -Program on Destination Computer. The unencrypted Relay commands are -disabled during traffic masking to hide the quantity and schedule of -communication even from the Networked Computer (in case it's compromised -and monitoring the user). The fact these commands are unencrypted, do -not cause security issues because if an adversary can compromise the -Networked Computer to the point it can issue commands to the Relay +Unencrypted type datagrams contain commands intended for the Relay +Program. These commands are in some cases preceded by an encrypted +version of the command, that the Relay Program forwards to Receiver +Program on Destination Computer. The unencrypted Relay commands are +disabled during traffic masking to hide the quantity and schedule of +communication even from the Networked Computer (in case it's compromised +and monitoring the user). The fact these commands are unencrypted, do +not cause security issues because if an adversary can compromise the +Networked Computer to the point it can issue commands to the Relay Program, they could DoS the Relay Program, and thus TFC, anyway. """ DATAGRAM_TIMESTAMP_LENGTH = 8 @@ -209,9 +213,9 @@ UNENCRYPTED_DATAGRAM_HEADER = b'U' """Group management headers -Group management datagrams are are automatic messages that the -Transmitter Program recommends the user to send when they make changes -to the member list of a group, or when they add or remove groups. These +Group management datagrams are are automatic messages that the +Transmitter Program recommends the user to send when they make changes +to the member list of a group, or when they add or remove groups. These messages are displayed by the Relay Program. """ GROUP_ID_LENGTH = 4 @@ -227,10 +231,10 @@ GROUP_MSG_EXIT_GROUP_HEADER = b'X' """Assembly packet headers -These one-byte assembly packet headers are not part of the padded +These one-byte assembly packet headers are not part of the padded message parsed from assembly packets. They are however the very first -plaintext byte, prepended to every padded assembly packet that is -delivered to the recipient/local Destination Computer. The header +plaintext byte, prepended to every padded assembly packet that is +delivered to the recipient/local Destination Computer. The header delivers the information about if and when to assemble the packet, as well as when to drop any previously collected assembly packets. """ @@ -260,12 +264,12 @@ C_N_HEADER = b'5' # Noise command packet """Unencrypted command headers -These two-byte headers are only used to control the Relay Program on -Networked Computer. These commands will not be used during traffic +These two-byte headers are only used to control the Relay Program on +Networked Computer. These commands will not be used during traffic masking, as they would reveal when TFC is being used. These commands do -not require encryption, because if an attacker can compromise the -Networked Computer to the point it could inject commands to Relay -Program, it could most likely also access any decryption keys used by +not require encryption, because if an attacker can compromise the +Networked Computer to the point it could inject commands to Relay +Program, it could most likely also access any decryption keys used by the Relay Program. """ UNENCRYPTED_COMMAND_HEADER_LENGTH = 2 @@ -285,10 +289,10 @@ UNENCRYPTED_MANAGE_CONTACT_REQ = b'UM' """Encrypted command headers -These two-byte headers determine the type of command for Receiver -Program on local Destination Computer. The header is evaluated after the -Receiver Program has received all assembly packets and assembled the -command. These headers tell the Receiver Program to which function the +These two-byte headers determine the type of command for Receiver +Program on local Destination Computer. The header is evaluated after the +Receiver Program has received all assembly packets and assembled the +command. These headers tell the Receiver Program to which function the provided parameters (if any) must be redirected. """ ENCRYPTED_COMMAND_HEADER_LENGTH = 2 @@ -322,17 +326,17 @@ WIPE_USR_DATA = b'WD' """Origin headers -This one-byte header tells the Relay and Receiver Programs whether the +This one-byte header tells the Relay and Receiver Programs whether the account included in the packet is the source or the destination of the -transmission. The user origin header is used when the Relay Program -forwards the message packets from user's Source Computer to user's -Destination Computer. The contact origin header is used when the program -forwards packets that are loaded from servers of contacts to the user's -Destination Computer. +transmission. The user origin header is used when the Relay Program +forwards the message packets from user's Source Computer to user's +Destination Computer. The contact origin header is used when the program +forwards packets that are loaded from servers of contacts to the user's +Destination Computer. On Destination Computer, the Receiver Program uses the origin header to -determine which unidirectional keys it should load to decrypt the -datagram payload. +determine which unidirectional keys it should load to decrypt the +datagram payload. """ ORIGIN_HEADER_LENGTH = 1 ORIGIN_USER_HEADER = b'o' @@ -341,25 +345,25 @@ ORIGIN_CONTACT_HEADER = b'i' """Message headers -This one-byte header will be prepended to each plaintext message before -padding and splitting the message. It will be evaluated once the Relay +This one-byte header will be prepended to each plaintext message before +padding and splitting the message. It will be evaluated once the Relay Program has received all assembly packets and assembled the message. -The private and group message headers allow the Receiver Program to -determine whether the message should be displayed in a private or in a -group window. This does not allow re-direction of messages to -unauthorized group windows, because TFC's manually managed group -configuration is also a whitelist for accounts that are authorized to +The private and group message headers allow the Receiver Program to +determine whether the message should be displayed in a private or in a +group window. This does not allow re-direction of messages to +unauthorized group windows, because TFC's manually managed group +configuration is also a whitelist for accounts that are authorized to display messages under the group's window. -Messages with the whisper message header have "sender-based control". -Unless the contact maliciously alters their Receiver Program's behavior, +Messages with the whisper message header have "sender-based control". +Unless the contact maliciously alters their Receiver Program's behavior, whispered messages are not logged regardless of in-program controlled settings. -Messages with file key header contain the hash of the file ciphertext -that was sent to the user earlier. It also contains the symmetric -decryption key for that file. +Messages with file key header contain the hash of the file ciphertext +that was sent to the user earlier. It also contains the symmetric +decryption key for that file. """ MESSAGE_HEADER_LENGTH = 1 WHISPER_FIELD_LENGTH = 1 @@ -370,14 +374,14 @@ FILE_KEY_HEADER = b'k' """Delays -Traffic masking packet queue check delay ensures that the lookup time +Traffic masking packet queue check delay ensures that the lookup time for the packet queue is obfuscated. -The local testing packet delay is an arbitrary delay that simulates the +The local testing packet delay is an arbitrary delay that simulates the slight delay caused by data transmission over a serial interface. The Relay client delays are values that determine the delays between -checking the online status of the contact (and the state of their +checking the online status of the contact (and the state of their ephemeral URL token public key). """ TRAFFIC_MASKING_QUEUE_CHECK_DELAY = 0.1 @@ -514,21 +518,25 @@ BITS_PER_BYTE = 8 MAX_INT = 2 ** 64 - 1 B58_CHECKSUM_LENGTH = 4 TRUNC_ADDRESS_LENGTH = 5 +TOR_CONTROL_PORT = 9051 +TOR_SOCKS_PORT = 9050 # Key derivation -ARGON2_MIN_TIME_COST = 1 -ARGON2_MIN_MEMORY_COST = 8 -ARGON2_MIN_PARALLELISM = 1 -ARGON2_SALT_LENGTH = 32 -ARGON2_PSK_TIME_COST = 25 -ARGON2_PSK_MEMORY_COST = 512 * 1024 # kibibytes -ARGON2_PSK_PARALLELISM = 2 -MIN_KEY_DERIVATION_TIME = 3.0 # seconds -MAX_KEY_DERIVATION_TIME = 4.0 # seconds +ARGON2_MIN_TIME_COST = 1 +ARGON2_MIN_MEMORY_COST = 8 +ARGON2_MIN_PARALLELISM = 1 +ARGON2_SALT_LENGTH = 32 +ARGON2_PSK_TIME_COST = 25 +ARGON2_PSK_MEMORY_COST = 512 * 1024 # kibibytes +ARGON2_PSK_PARALLELISM = 2 +MIN_KEY_DERIVATION_TIME = 3.0 # seconds +MAX_KEY_DERIVATION_TIME = 4.0 # seconds +PASSWORD_MIN_BIT_STRENGTH = 128 # Cryptographic field sizes TFC_PRIVATE_KEY_LENGTH = 56 TFC_PUBLIC_KEY_LENGTH = 56 +X448_SHARED_SECRET_LENGTH = 56 FINGERPRINT_LENGTH = 32 ONION_SERVICE_PRIVATE_KEY_LENGTH = 32 ONION_SERVICE_PUBLIC_KEY_LENGTH = 32 diff --git a/src/common/word_list.py b/src/common/word_list.py new file mode 100644 index 0000000..4887ede --- /dev/null +++ b/src/common/word_list.py @@ -0,0 +1,7799 @@ +#!/usr/bin/env python3.7 +# -*- coding: utf-8 -*- + +""" +TFC - Onion-routed, endpoint secure messaging system +Copyright (C) 2013-2019 Markus Ottela + +This file is part of TFC. + +TFC is free software: you can redistribute it and/or modify it under the terms +of the GNU General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +TFC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with TFC. If not, see . +""" + +eff_wordlist = [ +'abacus', +'abdomen', +'abdominal', +'abide', +'abiding', +'ability', +'ablaze', +'able', +'abnormal', +'abrasion', +'abrasive', +'abreast', +'abridge', +'abroad', +'abruptly', +'absence', +'absentee', +'absently', +'absinthe', +'absolute', +'absolve', +'abstain', +'abstract', +'absurd', +'accent', +'acclaim', +'acclimate', +'accompany', +'account', +'accuracy', +'accurate', +'accustom', +'acetone', +'achiness', +'aching', +'acid', +'acorn', +'acquaint', +'acquire', +'acre', +'acrobat', +'acronym', +'acting', +'action', +'activate', +'activator', +'active', +'activism', +'activist', +'activity', +'actress', +'acts', +'acutely', +'acuteness', +'aeration', +'aerobics', +'aerosol', +'aerospace', +'afar', +'affair', +'affected', +'affecting', +'affection', +'affidavit', +'affiliate', +'affirm', +'affix', +'afflicted', +'affluent', +'afford', +'affront', +'aflame', +'afloat', +'aflutter', +'afoot', +'afraid', +'afterglow', +'afterlife', +'aftermath', +'aftermost', +'afternoon', +'aged', +'ageless', +'agency', +'agenda', +'agent', +'aggregate', +'aghast', +'agile', +'agility', +'aging', +'agnostic', +'agonize', +'agonizing', +'agony', +'agreeable', +'agreeably', +'agreed', +'agreeing', +'agreement', +'aground', +'ahead', +'ahoy', +'aide', +'aids', +'aim', +'ajar', +'alabaster', +'alarm', +'albatross', +'album', +'alfalfa', +'algebra', +'algorithm', +'alias', +'alibi', +'alienable', +'alienate', +'aliens', +'alike', +'alive', +'alkaline', +'alkalize', +'almanac', +'almighty', +'almost', +'aloe', +'aloft', +'aloha', +'alone', +'alongside', +'aloof', +'alphabet', +'alright', +'although', +'altitude', +'alto', +'aluminum', +'alumni', +'always', +'amaretto', +'amaze', +'amazingly', +'amber', +'ambiance', +'ambiguity', +'ambiguous', +'ambition', +'ambitious', +'ambulance', +'ambush', +'amendable', +'amendment', +'amends', +'amenity', +'amiable', +'amicably', +'amid', +'amigo', +'amino', +'amiss', +'ammonia', +'ammonium', +'amnesty', +'amniotic', +'among', +'amount', +'amperage', +'ample', +'amplifier', +'amplify', +'amply', +'amuck', +'amulet', +'amusable', +'amused', +'amusement', +'amuser', +'amusing', +'anaconda', +'anaerobic', +'anagram', +'anatomist', +'anatomy', +'anchor', +'anchovy', +'ancient', +'android', +'anemia', +'anemic', +'aneurism', +'anew', +'angelfish', +'angelic', +'anger', +'angled', +'angler', +'angles', +'angling', +'angrily', +'angriness', +'anguished', +'angular', +'animal', +'animate', +'animating', +'animation', +'animator', +'anime', +'animosity', +'ankle', +'annex', +'annotate', +'announcer', +'annoying', +'annually', +'annuity', +'anointer', +'another', +'answering', +'antacid', +'antarctic', +'anteater', +'antelope', +'antennae', +'anthem', +'anthill', +'anthology', +'antibody', +'antics', +'antidote', +'antihero', +'antiquely', +'antiques', +'antiquity', +'antirust', +'antitoxic', +'antitrust', +'antiviral', +'antivirus', +'antler', +'antonym', +'antsy', +'anvil', +'anybody', +'anyhow', +'anymore', +'anyone', +'anyplace', +'anything', +'anytime', +'anyway', +'anywhere', +'aorta', +'apache', +'apostle', +'appealing', +'appear', +'appease', +'appeasing', +'appendage', +'appendix', +'appetite', +'appetizer', +'applaud', +'applause', +'apple', +'appliance', +'applicant', +'applied', +'apply', +'appointee', +'appraisal', +'appraiser', +'apprehend', +'approach', +'approval', +'approve', +'apricot', +'april', +'apron', +'aptitude', +'aptly', +'aqua', +'aqueduct', +'arbitrary', +'arbitrate', +'ardently', +'area', +'arena', +'arguable', +'arguably', +'argue', +'arise', +'armadillo', +'armband', +'armchair', +'armed', +'armful', +'armhole', +'arming', +'armless', +'armoire', +'armored', +'armory', +'armrest', +'army', +'aroma', +'arose', +'around', +'arousal', +'arrange', +'array', +'arrest', +'arrival', +'arrive', +'arrogance', +'arrogant', +'arson', +'art', +'ascend', +'ascension', +'ascent', +'ascertain', +'ashamed', +'ashen', +'ashes', +'ashy', +'aside', +'askew', +'asleep', +'asparagus', +'aspect', +'aspirate', +'aspire', +'aspirin', +'astonish', +'astound', +'astride', +'astrology', +'astronaut', +'astronomy', +'astute', +'atlantic', +'atlas', +'atom', +'atonable', +'atop', +'atrium', +'atrocious', +'atrophy', +'attach', +'attain', +'attempt', +'attendant', +'attendee', +'attention', +'attentive', +'attest', +'attic', +'attire', +'attitude', +'attractor', +'attribute', +'atypical', +'auction', +'audacious', +'audacity', +'audible', +'audibly', +'audience', +'audio', +'audition', +'augmented', +'august', +'authentic', +'author', +'autism', +'autistic', +'autograph', +'automaker', +'automated', +'automatic', +'autopilot', +'available', +'avalanche', +'avatar', +'avenge', +'avenging', +'avenue', +'average', +'aversion', +'avert', +'aviation', +'aviator', +'avid', +'avoid', +'await', +'awaken', +'award', +'aware', +'awhile', +'awkward', +'awning', +'awoke', +'awry', +'axis', +'babble', +'babbling', +'babied', +'baboon', +'backache', +'backboard', +'backboned', +'backdrop', +'backed', +'backer', +'backfield', +'backfire', +'backhand', +'backing', +'backlands', +'backlash', +'backless', +'backlight', +'backlit', +'backlog', +'backpack', +'backpedal', +'backrest', +'backroom', +'backshift', +'backside', +'backslid', +'backspace', +'backspin', +'backstab', +'backstage', +'backtalk', +'backtrack', +'backup', +'backward', +'backwash', +'backwater', +'backyard', +'bacon', +'bacteria', +'bacterium', +'badass', +'badge', +'badland', +'badly', +'badness', +'baffle', +'baffling', +'bagel', +'bagful', +'baggage', +'bagged', +'baggie', +'bagginess', +'bagging', +'baggy', +'bagpipe', +'baguette', +'baked', +'bakery', +'bakeshop', +'baking', +'balance', +'balancing', +'balcony', +'balmy', +'balsamic', +'bamboo', +'banana', +'banish', +'banister', +'banjo', +'bankable', +'bankbook', +'banked', +'banker', +'banking', +'banknote', +'bankroll', +'banner', +'bannister', +'banshee', +'banter', +'barbecue', +'barbed', +'barbell', +'barber', +'barcode', +'barge', +'bargraph', +'barista', +'baritone', +'barley', +'barmaid', +'barman', +'barn', +'barometer', +'barrack', +'barracuda', +'barrel', +'barrette', +'barricade', +'barrier', +'barstool', +'bartender', +'barterer', +'bash', +'basically', +'basics', +'basil', +'basin', +'basis', +'basket', +'batboy', +'batch', +'bath', +'baton', +'bats', +'battalion', +'battered', +'battering', +'battery', +'batting', +'battle', +'bauble', +'bazooka', +'blabber', +'bladder', +'blade', +'blah', +'blame', +'blaming', +'blanching', +'blandness', +'blank', +'blaspheme', +'blasphemy', +'blast', +'blatancy', +'blatantly', +'blazer', +'blazing', +'bleach', +'bleak', +'bleep', +'blemish', +'blend', +'bless', +'blighted', +'blimp', +'bling', +'blinked', +'blinker', +'blinking', +'blinks', +'blip', +'blissful', +'blitz', +'blizzard', +'bloated', +'bloating', +'blob', +'blog', +'bloomers', +'blooming', +'blooper', +'blot', +'blouse', +'blubber', +'bluff', +'bluish', +'blunderer', +'blunt', +'blurb', +'blurred', +'blurry', +'blurt', +'blush', +'blustery', +'boaster', +'boastful', +'boasting', +'boat', +'bobbed', +'bobbing', +'bobble', +'bobcat', +'bobsled', +'bobtail', +'bodacious', +'body', +'bogged', +'boggle', +'bogus', +'boil', +'bok', +'bolster', +'bolt', +'bonanza', +'bonded', +'bonding', +'bondless', +'boned', +'bonehead', +'boneless', +'bonelike', +'boney', +'bonfire', +'bonnet', +'bonsai', +'bonus', +'bony', +'boogeyman', +'boogieman', +'book', +'boondocks', +'booted', +'booth', +'bootie', +'booting', +'bootlace', +'bootleg', +'boots', +'boozy', +'borax', +'boring', +'borough', +'borrower', +'borrowing', +'boss', +'botanical', +'botanist', +'botany', +'botch', +'both', +'bottle', +'bottling', +'bottom', +'bounce', +'bouncing', +'bouncy', +'bounding', +'boundless', +'bountiful', +'bovine', +'boxcar', +'boxer', +'boxing', +'boxlike', +'boxy', +'breach', +'breath', +'breeches', +'breeching', +'breeder', +'breeding', +'breeze', +'breezy', +'brethren', +'brewery', +'brewing', +'briar', +'bribe', +'brick', +'bride', +'bridged', +'brigade', +'bright', +'brilliant', +'brim', +'bring', +'brink', +'brisket', +'briskly', +'briskness', +'bristle', +'brittle', +'broadband', +'broadcast', +'broaden', +'broadly', +'broadness', +'broadside', +'broadways', +'broiler', +'broiling', +'broken', +'broker', +'bronchial', +'bronco', +'bronze', +'bronzing', +'brook', +'broom', +'brought', +'browbeat', +'brownnose', +'browse', +'browsing', +'bruising', +'brunch', +'brunette', +'brunt', +'brush', +'brussels', +'brute', +'brutishly', +'bubble', +'bubbling', +'bubbly', +'buccaneer', +'bucked', +'bucket', +'buckle', +'buckshot', +'buckskin', +'bucktooth', +'buckwheat', +'buddhism', +'buddhist', +'budding', +'buddy', +'budget', +'buffalo', +'buffed', +'buffer', +'buffing', +'buffoon', +'buggy', +'bulb', +'bulge', +'bulginess', +'bulgur', +'bulk', +'bulldog', +'bulldozer', +'bullfight', +'bullfrog', +'bullhorn', +'bullion', +'bullish', +'bullpen', +'bullring', +'bullseye', +'bullwhip', +'bully', +'bunch', +'bundle', +'bungee', +'bunion', +'bunkbed', +'bunkhouse', +'bunkmate', +'bunny', +'bunt', +'busboy', +'bush', +'busily', +'busload', +'bust', +'busybody', +'buzz', +'cabana', +'cabbage', +'cabbie', +'cabdriver', +'cable', +'caboose', +'cache', +'cackle', +'cacti', +'cactus', +'caddie', +'caddy', +'cadet', +'cadillac', +'cadmium', +'cage', +'cahoots', +'cake', +'calamari', +'calamity', +'calcium', +'calculate', +'calculus', +'caliber', +'calibrate', +'calm', +'caloric', +'calorie', +'calzone', +'camcorder', +'cameo', +'camera', +'camisole', +'camper', +'campfire', +'camping', +'campsite', +'campus', +'canal', +'canary', +'cancel', +'candied', +'candle', +'candy', +'cane', +'canine', +'canister', +'cannabis', +'canned', +'canning', +'cannon', +'cannot', +'canola', +'canon', +'canopener', +'canopy', +'canteen', +'canyon', +'capable', +'capably', +'capacity', +'cape', +'capillary', +'capital', +'capitol', +'capped', +'capricorn', +'capsize', +'capsule', +'caption', +'captivate', +'captive', +'captivity', +'capture', +'caramel', +'carat', +'caravan', +'carbon', +'cardboard', +'carded', +'cardiac', +'cardigan', +'cardinal', +'cardstock', +'carefully', +'caregiver', +'careless', +'caress', +'caretaker', +'cargo', +'caring', +'carless', +'carload', +'carmaker', +'carnage', +'carnation', +'carnival', +'carnivore', +'carol', +'carpenter', +'carpentry', +'carpool', +'carport', +'carried', +'carrot', +'carrousel', +'carry', +'cartel', +'cartload', +'carton', +'cartoon', +'cartridge', +'cartwheel', +'carve', +'carving', +'carwash', +'cascade', +'case', +'cash', +'casing', +'casino', +'casket', +'cassette', +'casually', +'casualty', +'catacomb', +'catalog', +'catalyst', +'catalyze', +'catapult', +'cataract', +'catatonic', +'catcall', +'catchable', +'catcher', +'catching', +'catchy', +'caterer', +'catering', +'catfight', +'catfish', +'cathedral', +'cathouse', +'catlike', +'catnap', +'catnip', +'catsup', +'cattail', +'cattishly', +'cattle', +'catty', +'catwalk', +'caucasian', +'caucus', +'causal', +'causation', +'cause', +'causing', +'cauterize', +'caution', +'cautious', +'cavalier', +'cavalry', +'caviar', +'cavity', +'cedar', +'celery', +'celestial', +'celibacy', +'celibate', +'celtic', +'cement', +'census', +'ceramics', +'ceremony', +'certainly', +'certainty', +'certified', +'certify', +'cesarean', +'cesspool', +'chafe', +'chaffing', +'chain', +'chair', +'chalice', +'challenge', +'chamber', +'chamomile', +'champion', +'chance', +'change', +'channel', +'chant', +'chaos', +'chaperone', +'chaplain', +'chapped', +'chaps', +'chapter', +'character', +'charbroil', +'charcoal', +'charger', +'charging', +'chariot', +'charity', +'charm', +'charred', +'charter', +'charting', +'chase', +'chasing', +'chaste', +'chastise', +'chastity', +'chatroom', +'chatter', +'chatting', +'chatty', +'cheating', +'cheddar', +'cheek', +'cheer', +'cheese', +'cheesy', +'chef', +'chemicals', +'chemist', +'chemo', +'cherisher', +'cherub', +'chess', +'chest', +'chevron', +'chevy', +'chewable', +'chewer', +'chewing', +'chewy', +'chief', +'chihuahua', +'childcare', +'childhood', +'childish', +'childless', +'childlike', +'chili', +'chill', +'chimp', +'chip', +'chirping', +'chirpy', +'chitchat', +'chivalry', +'chive', +'chloride', +'chlorine', +'choice', +'chokehold', +'choking', +'chomp', +'chooser', +'choosing', +'choosy', +'chop', +'chosen', +'chowder', +'chowtime', +'chrome', +'chubby', +'chuck', +'chug', +'chummy', +'chump', +'chunk', +'churn', +'chute', +'cider', +'cilantro', +'cinch', +'cinema', +'cinnamon', +'circle', +'circling', +'circular', +'circulate', +'circus', +'citable', +'citadel', +'citation', +'citizen', +'citric', +'citrus', +'city', +'civic', +'civil', +'clad', +'claim', +'clambake', +'clammy', +'clamor', +'clamp', +'clamshell', +'clang', +'clanking', +'clapped', +'clapper', +'clapping', +'clarify', +'clarinet', +'clarity', +'clash', +'clasp', +'class', +'clatter', +'clause', +'clavicle', +'claw', +'clay', +'clean', +'clear', +'cleat', +'cleaver', +'cleft', +'clench', +'clergyman', +'clerical', +'clerk', +'clever', +'clicker', +'client', +'climate', +'climatic', +'cling', +'clinic', +'clinking', +'clip', +'clique', +'cloak', +'clobber', +'clock', +'clone', +'cloning', +'closable', +'closure', +'clothes', +'clothing', +'cloud', +'clover', +'clubbed', +'clubbing', +'clubhouse', +'clump', +'clumsily', +'clumsy', +'clunky', +'clustered', +'clutch', +'clutter', +'coach', +'coagulant', +'coastal', +'coaster', +'coasting', +'coastland', +'coastline', +'coat', +'coauthor', +'cobalt', +'cobbler', +'cobweb', +'cocoa', +'coconut', +'cod', +'coeditor', +'coerce', +'coexist', +'coffee', +'cofounder', +'cognition', +'cognitive', +'cogwheel', +'coherence', +'coherent', +'cohesive', +'coil', +'coke', +'cola', +'cold', +'coleslaw', +'coliseum', +'collage', +'collapse', +'collar', +'collected', +'collector', +'collide', +'collie', +'collision', +'colonial', +'colonist', +'colonize', +'colony', +'colossal', +'colt', +'coma', +'come', +'comfort', +'comfy', +'comic', +'coming', +'comma', +'commence', +'commend', +'comment', +'commerce', +'commode', +'commodity', +'commodore', +'common', +'commotion', +'commute', +'commuting', +'compacted', +'compacter', +'compactly', +'compactor', +'companion', +'company', +'compare', +'compel', +'compile', +'comply', +'component', +'composed', +'composer', +'composite', +'compost', +'composure', +'compound', +'compress', +'comprised', +'computer', +'computing', +'comrade', +'concave', +'conceal', +'conceded', +'concept', +'concerned', +'concert', +'conch', +'concierge', +'concise', +'conclude', +'concrete', +'concur', +'condense', +'condiment', +'condition', +'condone', +'conducive', +'conductor', +'conduit', +'cone', +'confess', +'confetti', +'confidant', +'confident', +'confider', +'confiding', +'configure', +'confined', +'confining', +'confirm', +'conflict', +'conform', +'confound', +'confront', +'confused', +'confusing', +'confusion', +'congenial', +'congested', +'congrats', +'congress', +'conical', +'conjoined', +'conjure', +'conjuror', +'connected', +'connector', +'consensus', +'consent', +'console', +'consoling', +'consonant', +'constable', +'constant', +'constrain', +'constrict', +'construct', +'consult', +'consumer', +'consuming', +'contact', +'container', +'contempt', +'contend', +'contented', +'contently', +'contents', +'contest', +'context', +'contort', +'contour', +'contrite', +'control', +'contusion', +'convene', +'convent', +'copartner', +'cope', +'copied', +'copier', +'copilot', +'coping', +'copious', +'copper', +'copy', +'coral', +'cork', +'cornball', +'cornbread', +'corncob', +'cornea', +'corned', +'corner', +'cornfield', +'cornflake', +'cornhusk', +'cornmeal', +'cornstalk', +'corny', +'coronary', +'coroner', +'corporal', +'corporate', +'corral', +'correct', +'corridor', +'corrode', +'corroding', +'corrosive', +'corsage', +'corset', +'cortex', +'cosigner', +'cosmetics', +'cosmic', +'cosmos', +'cosponsor', +'cost', +'cottage', +'cotton', +'couch', +'cough', +'could', +'countable', +'countdown', +'counting', +'countless', +'country', +'county', +'courier', +'covenant', +'cover', +'coveted', +'coveting', +'coyness', +'cozily', +'coziness', +'cozy', +'crabbing', +'crabgrass', +'crablike', +'crabmeat', +'cradle', +'cradling', +'crafter', +'craftily', +'craftsman', +'craftwork', +'crafty', +'cramp', +'cranberry', +'crane', +'cranial', +'cranium', +'crank', +'crate', +'crave', +'craving', +'crawfish', +'crawlers', +'crawling', +'crayfish', +'crayon', +'crazed', +'crazily', +'craziness', +'crazy', +'creamed', +'creamer', +'creamlike', +'crease', +'creasing', +'creatable', +'create', +'creation', +'creative', +'creature', +'credible', +'credibly', +'credit', +'creed', +'creme', +'creole', +'crepe', +'crept', +'crescent', +'crested', +'cresting', +'crestless', +'crevice', +'crewless', +'crewman', +'crewmate', +'crib', +'cricket', +'cried', +'crier', +'crimp', +'crimson', +'cringe', +'cringing', +'crinkle', +'crinkly', +'crisped', +'crisping', +'crisply', +'crispness', +'crispy', +'criteria', +'critter', +'croak', +'crock', +'crook', +'croon', +'crop', +'cross', +'crouch', +'crouton', +'crowbar', +'crowd', +'crown', +'crucial', +'crudely', +'crudeness', +'cruelly', +'cruelness', +'cruelty', +'crumb', +'crummiest', +'crummy', +'crumpet', +'crumpled', +'cruncher', +'crunching', +'crunchy', +'crusader', +'crushable', +'crushed', +'crusher', +'crushing', +'crust', +'crux', +'crying', +'cryptic', +'crystal', +'cubbyhole', +'cube', +'cubical', +'cubicle', +'cucumber', +'cuddle', +'cuddly', +'cufflink', +'culinary', +'culminate', +'culpable', +'culprit', +'cultivate', +'cultural', +'culture', +'cupbearer', +'cupcake', +'cupid', +'cupped', +'cupping', +'curable', +'curator', +'curdle', +'cure', +'curfew', +'curing', +'curled', +'curler', +'curliness', +'curling', +'curly', +'curry', +'curse', +'cursive', +'cursor', +'curtain', +'curtly', +'curtsy', +'curvature', +'curve', +'curvy', +'cushy', +'cusp', +'cussed', +'custard', +'custodian', +'custody', +'customary', +'customer', +'customize', +'customs', +'cut', +'cycle', +'cyclic', +'cycling', +'cyclist', +'cylinder', +'cymbal', +'cytoplasm', +'cytoplast', +'dab', +'dad', +'daffodil', +'dagger', +'daily', +'daintily', +'dainty', +'dairy', +'daisy', +'dallying', +'dance', +'dancing', +'dandelion', +'dander', +'dandruff', +'dandy', +'danger', +'dangle', +'dangling', +'daredevil', +'dares', +'daringly', +'darkened', +'darkening', +'darkish', +'darkness', +'darkroom', +'darling', +'darn', +'dart', +'darwinism', +'dash', +'dastardly', +'data', +'datebook', +'dating', +'daughter', +'daunting', +'dawdler', +'dawn', +'daybed', +'daybreak', +'daycare', +'daydream', +'daylight', +'daylong', +'dayroom', +'daytime', +'dazzler', +'dazzling', +'deacon', +'deafening', +'deafness', +'dealer', +'dealing', +'dealmaker', +'dealt', +'dean', +'debatable', +'debate', +'debating', +'debit', +'debrief', +'debtless', +'debtor', +'debug', +'debunk', +'decade', +'decaf', +'decal', +'decathlon', +'decay', +'deceased', +'deceit', +'deceiver', +'deceiving', +'december', +'decency', +'decent', +'deception', +'deceptive', +'decibel', +'decidable', +'decimal', +'decimeter', +'decipher', +'deck', +'declared', +'decline', +'decode', +'decompose', +'decorated', +'decorator', +'decoy', +'decrease', +'decree', +'dedicate', +'dedicator', +'deduce', +'deduct', +'deed', +'deem', +'deepen', +'deeply', +'deepness', +'deface', +'defacing', +'defame', +'default', +'defeat', +'defection', +'defective', +'defendant', +'defender', +'defense', +'defensive', +'deferral', +'deferred', +'defiance', +'defiant', +'defile', +'defiling', +'define', +'definite', +'deflate', +'deflation', +'deflator', +'deflected', +'deflector', +'defog', +'deforest', +'defraud', +'defrost', +'deftly', +'defuse', +'defy', +'degraded', +'degrading', +'degrease', +'degree', +'dehydrate', +'deity', +'dejected', +'delay', +'delegate', +'delegator', +'delete', +'deletion', +'delicacy', +'delicate', +'delicious', +'delighted', +'delirious', +'delirium', +'deliverer', +'delivery', +'delouse', +'delta', +'deluge', +'delusion', +'deluxe', +'demanding', +'demeaning', +'demeanor', +'demise', +'democracy', +'democrat', +'demote', +'demotion', +'demystify', +'denatured', +'deniable', +'denial', +'denim', +'denote', +'dense', +'density', +'dental', +'dentist', +'denture', +'deny', +'deodorant', +'deodorize', +'departed', +'departure', +'depict', +'deplete', +'depletion', +'deplored', +'deploy', +'deport', +'depose', +'depraved', +'depravity', +'deprecate', +'depress', +'deprive', +'depth', +'deputize', +'deputy', +'derail', +'deranged', +'derby', +'derived', +'desecrate', +'deserve', +'deserving', +'designate', +'designed', +'designer', +'designing', +'deskbound', +'desktop', +'deskwork', +'desolate', +'despair', +'despise', +'despite', +'destiny', +'destitute', +'destruct', +'detached', +'detail', +'detection', +'detective', +'detector', +'detention', +'detergent', +'detest', +'detonate', +'detonator', +'detoxify', +'detract', +'deuce', +'devalue', +'deviancy', +'deviant', +'deviate', +'deviation', +'deviator', +'device', +'devious', +'devotedly', +'devotee', +'devotion', +'devourer', +'devouring', +'devoutly', +'dexterity', +'dexterous', +'diabetes', +'diabetic', +'diabolic', +'diagnoses', +'diagnosis', +'diagram', +'dial', +'diameter', +'diaper', +'diaphragm', +'diary', +'dice', +'dicing', +'dictate', +'dictation', +'dictator', +'difficult', +'diffused', +'diffuser', +'diffusion', +'diffusive', +'dig', +'dilation', +'diligence', +'diligent', +'dill', +'dilute', +'dime', +'diminish', +'dimly', +'dimmed', +'dimmer', +'dimness', +'dimple', +'diner', +'dingbat', +'dinghy', +'dinginess', +'dingo', +'dingy', +'dining', +'dinner', +'diocese', +'dioxide', +'diploma', +'dipped', +'dipper', +'dipping', +'directed', +'direction', +'directive', +'directly', +'directory', +'direness', +'dirtiness', +'disabled', +'disagree', +'disallow', +'disarm', +'disarray', +'disaster', +'disband', +'disbelief', +'disburse', +'discard', +'discern', +'discharge', +'disclose', +'discolor', +'discount', +'discourse', +'discover', +'discuss', +'disdain', +'disengage', +'disfigure', +'disgrace', +'dish', +'disinfect', +'disjoin', +'disk', +'dislike', +'disliking', +'dislocate', +'dislodge', +'disloyal', +'dismantle', +'dismay', +'dismiss', +'dismount', +'disobey', +'disorder', +'disown', +'disparate', +'disparity', +'dispatch', +'dispense', +'dispersal', +'dispersed', +'disperser', +'displace', +'display', +'displease', +'disposal', +'dispose', +'disprove', +'dispute', +'disregard', +'disrupt', +'dissuade', +'distance', +'distant', +'distaste', +'distill', +'distinct', +'distort', +'distract', +'distress', +'district', +'distrust', +'ditch', +'ditto', +'ditzy', +'dividable', +'divided', +'dividend', +'dividers', +'dividing', +'divinely', +'diving', +'divinity', +'divisible', +'divisibly', +'division', +'divisive', +'divorcee', +'dizziness', +'dizzy', +'doable', +'docile', +'dock', +'doctrine', +'document', +'dodge', +'dodgy', +'doily', +'doing', +'dole', +'dollar', +'dollhouse', +'dollop', +'dolly', +'dolphin', +'domain', +'domelike', +'domestic', +'dominion', +'dominoes', +'donated', +'donation', +'donator', +'donor', +'donut', +'doodle', +'doorbell', +'doorframe', +'doorknob', +'doorman', +'doormat', +'doornail', +'doorpost', +'doorstep', +'doorstop', +'doorway', +'doozy', +'dork', +'dormitory', +'dorsal', +'dosage', +'dose', +'dotted', +'doubling', +'douche', +'dove', +'down', +'dowry', +'doze', +'drab', +'dragging', +'dragonfly', +'dragonish', +'dragster', +'drainable', +'drainage', +'drained', +'drainer', +'drainpipe', +'dramatic', +'dramatize', +'drank', +'drapery', +'drastic', +'draw', +'dreaded', +'dreadful', +'dreadlock', +'dreamboat', +'dreamily', +'dreamland', +'dreamless', +'dreamlike', +'dreamt', +'dreamy', +'drearily', +'dreary', +'drench', +'dress', +'drew', +'dribble', +'dried', +'drier', +'drift', +'driller', +'drilling', +'drinkable', +'drinking', +'dripping', +'drippy', +'drivable', +'driven', +'driver', +'driveway', +'driving', +'drizzle', +'drizzly', +'drone', +'drool', +'droop', +'drop-down', +'dropbox', +'dropkick', +'droplet', +'dropout', +'dropper', +'drove', +'drown', +'drowsily', +'drudge', +'drum', +'dry', +'dubbed', +'dubiously', +'duchess', +'duckbill', +'ducking', +'duckling', +'ducktail', +'ducky', +'duct', +'dude', +'duffel', +'dugout', +'duh', +'duke', +'duller', +'dullness', +'duly', +'dumping', +'dumpling', +'dumpster', +'duo', +'dupe', +'duplex', +'duplicate', +'duplicity', +'durable', +'durably', +'duration', +'duress', +'during', +'dusk', +'dust', +'dutiful', +'duty', +'duvet', +'dwarf', +'dweeb', +'dwelled', +'dweller', +'dwelling', +'dwindle', +'dwindling', +'dynamic', +'dynamite', +'dynasty', +'dyslexia', +'dyslexic', +'each', +'eagle', +'earache', +'eardrum', +'earflap', +'earful', +'earlobe', +'early', +'earmark', +'earmuff', +'earphone', +'earpiece', +'earplugs', +'earring', +'earshot', +'earthen', +'earthlike', +'earthling', +'earthly', +'earthworm', +'earthy', +'earwig', +'easeful', +'easel', +'easiest', +'easily', +'easiness', +'easing', +'eastbound', +'eastcoast', +'easter', +'eastward', +'eatable', +'eaten', +'eatery', +'eating', +'eats', +'ebay', +'ebony', +'ebook', +'ecard', +'eccentric', +'echo', +'eclair', +'eclipse', +'ecologist', +'ecology', +'economic', +'economist', +'economy', +'ecosphere', +'ecosystem', +'edge', +'edginess', +'edging', +'edgy', +'edition', +'editor', +'educated', +'education', +'educator', +'eel', +'effective', +'effects', +'efficient', +'effort', +'eggbeater', +'egging', +'eggnog', +'eggplant', +'eggshell', +'egomaniac', +'egotism', +'egotistic', +'either', +'eject', +'elaborate', +'elastic', +'elated', +'elbow', +'eldercare', +'elderly', +'eldest', +'electable', +'election', +'elective', +'elephant', +'elevate', +'elevating', +'elevation', +'elevator', +'eleven', +'elf', +'eligible', +'eligibly', +'eliminate', +'elite', +'elitism', +'elixir', +'elk', +'ellipse', +'elliptic', +'elm', +'elongated', +'elope', +'eloquence', +'eloquent', +'elsewhere', +'elude', +'elusive', +'elves', +'email', +'embargo', +'embark', +'embassy', +'embattled', +'embellish', +'ember', +'embezzle', +'emblaze', +'emblem', +'embody', +'embolism', +'emboss', +'embroider', +'emcee', +'emerald', +'emergency', +'emission', +'emit', +'emote', +'emoticon', +'emotion', +'empathic', +'empathy', +'emperor', +'emphases', +'emphasis', +'emphasize', +'emphatic', +'empirical', +'employed', +'employee', +'employer', +'emporium', +'empower', +'emptier', +'emptiness', +'empty', +'emu', +'enable', +'enactment', +'enamel', +'enchanted', +'enchilada', +'encircle', +'enclose', +'enclosure', +'encode', +'encore', +'encounter', +'encourage', +'encroach', +'encrust', +'encrypt', +'endanger', +'endeared', +'endearing', +'ended', +'ending', +'endless', +'endnote', +'endocrine', +'endorphin', +'endorse', +'endowment', +'endpoint', +'endurable', +'endurance', +'enduring', +'energetic', +'energize', +'energy', +'enforced', +'enforcer', +'engaged', +'engaging', +'engine', +'engorge', +'engraved', +'engraver', +'engraving', +'engross', +'engulf', +'enhance', +'enigmatic', +'enjoyable', +'enjoyably', +'enjoyer', +'enjoying', +'enjoyment', +'enlarged', +'enlarging', +'enlighten', +'enlisted', +'enquirer', +'enrage', +'enrich', +'enroll', +'enslave', +'ensnare', +'ensure', +'entail', +'entangled', +'entering', +'entertain', +'enticing', +'entire', +'entitle', +'entity', +'entomb', +'entourage', +'entrap', +'entree', +'entrench', +'entrust', +'entryway', +'entwine', +'enunciate', +'envelope', +'enviable', +'enviably', +'envious', +'envision', +'envoy', +'envy', +'enzyme', +'epic', +'epidemic', +'epidermal', +'epidermis', +'epidural', +'epilepsy', +'epileptic', +'epilogue', +'epiphany', +'episode', +'equal', +'equate', +'equation', +'equator', +'equinox', +'equipment', +'equity', +'equivocal', +'eradicate', +'erasable', +'erased', +'eraser', +'erasure', +'ergonomic', +'errand', +'errant', +'erratic', +'error', +'erupt', +'escalate', +'escalator', +'escapable', +'escapade', +'escapist', +'escargot', +'eskimo', +'esophagus', +'espionage', +'espresso', +'esquire', +'essay', +'essence', +'essential', +'establish', +'estate', +'esteemed', +'estimate', +'estimator', +'estranged', +'estrogen', +'etching', +'eternal', +'eternity', +'ethanol', +'ether', +'ethically', +'ethics', +'euphemism', +'evacuate', +'evacuee', +'evade', +'evaluate', +'evaluator', +'evaporate', +'evasion', +'evasive', +'even', +'everglade', +'evergreen', +'everybody', +'everyday', +'everyone', +'evict', +'evidence', +'evident', +'evil', +'evoke', +'evolution', +'evolve', +'exact', +'exalted', +'example', +'excavate', +'excavator', +'exceeding', +'exception', +'excess', +'exchange', +'excitable', +'exciting', +'exclaim', +'exclude', +'excluding', +'exclusion', +'exclusive', +'excretion', +'excretory', +'excursion', +'excusable', +'excusably', +'excuse', +'exemplary', +'exemplify', +'exemption', +'exerciser', +'exert', +'exes', +'exfoliate', +'exhale', +'exhaust', +'exhume', +'exile', +'existing', +'exit', +'exodus', +'exonerate', +'exorcism', +'exorcist', +'expand', +'expanse', +'expansion', +'expansive', +'expectant', +'expedited', +'expediter', +'expel', +'expend', +'expenses', +'expensive', +'expert', +'expire', +'expiring', +'explain', +'expletive', +'explicit', +'explode', +'exploit', +'explore', +'exploring', +'exponent', +'exporter', +'exposable', +'expose', +'exposure', +'express', +'expulsion', +'exquisite', +'extended', +'extending', +'extent', +'extenuate', +'exterior', +'external', +'extinct', +'extortion', +'extradite', +'extras', +'extrovert', +'extrude', +'extruding', +'exuberant', +'fable', +'fabric', +'fabulous', +'facebook', +'facecloth', +'facedown', +'faceless', +'facelift', +'faceplate', +'faceted', +'facial', +'facility', +'facing', +'facsimile', +'faction', +'factoid', +'factor', +'factsheet', +'factual', +'faculty', +'fade', +'fading', +'failing', +'falcon', +'fall', +'false', +'falsify', +'fame', +'familiar', +'family', +'famine', +'famished', +'fanatic', +'fancied', +'fanciness', +'fancy', +'fanfare', +'fang', +'fanning', +'fantasize', +'fantastic', +'fantasy', +'fascism', +'fastball', +'faster', +'fasting', +'fastness', +'faucet', +'favorable', +'favorably', +'favored', +'favoring', +'favorite', +'fax', +'feast', +'federal', +'fedora', +'feeble', +'feed', +'feel', +'feisty', +'feline', +'felt-tip', +'feminine', +'feminism', +'feminist', +'feminize', +'femur', +'fence', +'fencing', +'fender', +'ferment', +'fernlike', +'ferocious', +'ferocity', +'ferret', +'ferris', +'ferry', +'fervor', +'fester', +'festival', +'festive', +'festivity', +'fetal', +'fetch', +'fever', +'fiber', +'fiction', +'fiddle', +'fiddling', +'fidelity', +'fidgeting', +'fidgety', +'fifteen', +'fifth', +'fiftieth', +'fifty', +'figment', +'figure', +'figurine', +'filing', +'filled', +'filler', +'filling', +'film', +'filter', +'filth', +'filtrate', +'finale', +'finalist', +'finalize', +'finally', +'finance', +'financial', +'finch', +'fineness', +'finer', +'finicky', +'finished', +'finisher', +'finishing', +'finite', +'finless', +'finlike', +'fiscally', +'fit', +'five', +'flaccid', +'flagman', +'flagpole', +'flagship', +'flagstick', +'flagstone', +'flail', +'flakily', +'flaky', +'flame', +'flammable', +'flanked', +'flanking', +'flannels', +'flap', +'flaring', +'flashback', +'flashbulb', +'flashcard', +'flashily', +'flashing', +'flashy', +'flask', +'flatbed', +'flatfoot', +'flatly', +'flatness', +'flatten', +'flattered', +'flatterer', +'flattery', +'flattop', +'flatware', +'flatworm', +'flavored', +'flavorful', +'flavoring', +'flaxseed', +'fled', +'fleshed', +'fleshy', +'flick', +'flier', +'flight', +'flinch', +'fling', +'flint', +'flip', +'flirt', +'float', +'flock', +'flogging', +'flop', +'floral', +'florist', +'floss', +'flounder', +'flyable', +'flyaway', +'flyer', +'flying', +'flyover', +'flypaper', +'foam', +'foe', +'fog', +'foil', +'folic', +'folk', +'follicle', +'follow', +'fondling', +'fondly', +'fondness', +'fondue', +'font', +'food', +'fool', +'footage', +'football', +'footbath', +'footboard', +'footer', +'footgear', +'foothill', +'foothold', +'footing', +'footless', +'footman', +'footnote', +'footpad', +'footpath', +'footprint', +'footrest', +'footsie', +'footsore', +'footwear', +'footwork', +'fossil', +'foster', +'founder', +'founding', +'fountain', +'fox', +'foyer', +'fraction', +'fracture', +'fragile', +'fragility', +'fragment', +'fragrance', +'fragrant', +'frail', +'frame', +'framing', +'frantic', +'fraternal', +'frayed', +'fraying', +'frays', +'freckled', +'freckles', +'freebase', +'freebee', +'freebie', +'freedom', +'freefall', +'freehand', +'freeing', +'freeload', +'freely', +'freemason', +'freeness', +'freestyle', +'freeware', +'freeway', +'freewill', +'freezable', +'freezing', +'freight', +'french', +'frenzied', +'frenzy', +'frequency', +'frequent', +'fresh', +'fretful', +'fretted', +'friction', +'friday', +'fridge', +'fried', +'friend', +'frighten', +'frightful', +'frigidity', +'frigidly', +'frill', +'fringe', +'frisbee', +'frisk', +'fritter', +'frivolous', +'frolic', +'from', +'front', +'frostbite', +'frosted', +'frostily', +'frosting', +'frostlike', +'frosty', +'froth', +'frown', +'frozen', +'fructose', +'frugality', +'frugally', +'fruit', +'frustrate', +'frying', +'gab', +'gaffe', +'gag', +'gainfully', +'gaining', +'gains', +'gala', +'gallantly', +'galleria', +'gallery', +'galley', +'gallon', +'gallows', +'gallstone', +'galore', +'galvanize', +'gambling', +'game', +'gaming', +'gamma', +'gander', +'gangly', +'gangrene', +'gangway', +'gap', +'garage', +'garbage', +'garden', +'gargle', +'garland', +'garlic', +'garment', +'garnet', +'garnish', +'garter', +'gas', +'gatherer', +'gathering', +'gating', +'gauging', +'gauntlet', +'gauze', +'gave', +'gawk', +'gazing', +'gear', +'gecko', +'geek', +'geiger', +'gem', +'gender', +'generic', +'generous', +'genetics', +'genre', +'gentile', +'gentleman', +'gently', +'gents', +'geography', +'geologic', +'geologist', +'geology', +'geometric', +'geometry', +'geranium', +'gerbil', +'geriatric', +'germicide', +'germinate', +'germless', +'germproof', +'gestate', +'gestation', +'gesture', +'getaway', +'getting', +'getup', +'giant', +'gibberish', +'giblet', +'giddily', +'giddiness', +'giddy', +'gift', +'gigabyte', +'gigahertz', +'gigantic', +'giggle', +'giggling', +'giggly', +'gigolo', +'gilled', +'gills', +'gimmick', +'girdle', +'giveaway', +'given', +'giver', +'giving', +'gizmo', +'gizzard', +'glacial', +'glacier', +'glade', +'gladiator', +'gladly', +'glamorous', +'glamour', +'glance', +'glancing', +'glandular', +'glare', +'glaring', +'glass', +'glaucoma', +'glazing', +'gleaming', +'gleeful', +'glider', +'gliding', +'glimmer', +'glimpse', +'glisten', +'glitch', +'glitter', +'glitzy', +'gloater', +'gloating', +'gloomily', +'gloomy', +'glorified', +'glorifier', +'glorify', +'glorious', +'glory', +'gloss', +'glove', +'glowing', +'glowworm', +'glucose', +'glue', +'gluten', +'glutinous', +'glutton', +'gnarly', +'gnat', +'goal', +'goatskin', +'goes', +'goggles', +'going', +'goldfish', +'goldmine', +'goldsmith', +'golf', +'goliath', +'gonad', +'gondola', +'gone', +'gong', +'good', +'gooey', +'goofball', +'goofiness', +'goofy', +'google', +'goon', +'gopher', +'gore', +'gorged', +'gorgeous', +'gory', +'gosling', +'gossip', +'gothic', +'gotten', +'gout', +'gown', +'grab', +'graceful', +'graceless', +'gracious', +'gradation', +'graded', +'grader', +'gradient', +'grading', +'gradually', +'graduate', +'graffiti', +'grafted', +'grafting', +'grain', +'granddad', +'grandkid', +'grandly', +'grandma', +'grandpa', +'grandson', +'granite', +'granny', +'granola', +'grant', +'granular', +'grape', +'graph', +'grapple', +'grappling', +'grasp', +'grass', +'gratified', +'gratify', +'grating', +'gratitude', +'gratuity', +'gravel', +'graveness', +'graves', +'graveyard', +'gravitate', +'gravity', +'gravy', +'gray', +'grazing', +'greasily', +'greedily', +'greedless', +'greedy', +'green', +'greeter', +'greeting', +'grew', +'greyhound', +'grid', +'grief', +'grievance', +'grieving', +'grievous', +'grill', +'grimace', +'grimacing', +'grime', +'griminess', +'grimy', +'grinch', +'grinning', +'grip', +'gristle', +'grit', +'groggily', +'groggy', +'groin', +'groom', +'groove', +'grooving', +'groovy', +'grope', +'ground', +'grouped', +'grout', +'grove', +'grower', +'growing', +'growl', +'grub', +'grudge', +'grudging', +'grueling', +'gruffly', +'grumble', +'grumbling', +'grumbly', +'grumpily', +'grunge', +'grunt', +'guacamole', +'guidable', +'guidance', +'guide', +'guiding', +'guileless', +'guise', +'gulf', +'gullible', +'gully', +'gulp', +'gumball', +'gumdrop', +'gumminess', +'gumming', +'gummy', +'gurgle', +'gurgling', +'guru', +'gush', +'gusto', +'gusty', +'gutless', +'guts', +'gutter', +'guy', +'guzzler', +'gyration', +'habitable', +'habitant', +'habitat', +'habitual', +'hacked', +'hacker', +'hacking', +'hacksaw', +'had', +'haggler', +'haiku', +'half', +'halogen', +'halt', +'halved', +'halves', +'hamburger', +'hamlet', +'hammock', +'hamper', +'hamster', +'hamstring', +'handbag', +'handball', +'handbook', +'handbrake', +'handcart', +'handclap', +'handclasp', +'handcraft', +'handcuff', +'handed', +'handful', +'handgrip', +'handgun', +'handheld', +'handiness', +'handiwork', +'handlebar', +'handled', +'handler', +'handling', +'handmade', +'handoff', +'handpick', +'handprint', +'handrail', +'handsaw', +'handset', +'handsfree', +'handshake', +'handstand', +'handwash', +'handwork', +'handwoven', +'handwrite', +'handyman', +'hangnail', +'hangout', +'hangover', +'hangup', +'hankering', +'hankie', +'hanky', +'haphazard', +'happening', +'happier', +'happiest', +'happily', +'happiness', +'happy', +'harbor', +'hardcopy', +'hardcore', +'hardcover', +'harddisk', +'hardened', +'hardener', +'hardening', +'hardhat', +'hardhead', +'hardiness', +'hardly', +'hardness', +'hardship', +'hardware', +'hardwired', +'hardwood', +'hardy', +'harmful', +'harmless', +'harmonica', +'harmonics', +'harmonize', +'harmony', +'harness', +'harpist', +'harsh', +'harvest', +'hash', +'hassle', +'haste', +'hastily', +'hastiness', +'hasty', +'hatbox', +'hatchback', +'hatchery', +'hatchet', +'hatching', +'hatchling', +'hate', +'hatless', +'hatred', +'haunt', +'haven', +'hazard', +'hazelnut', +'hazily', +'haziness', +'hazing', +'hazy', +'headache', +'headband', +'headboard', +'headcount', +'headdress', +'headed', +'header', +'headfirst', +'headgear', +'heading', +'headlamp', +'headless', +'headlock', +'headphone', +'headpiece', +'headrest', +'headroom', +'headscarf', +'headset', +'headsman', +'headstand', +'headstone', +'headway', +'headwear', +'heap', +'heat', +'heave', +'heavily', +'heaviness', +'heaving', +'hedge', +'hedging', +'heftiness', +'hefty', +'helium', +'helmet', +'helper', +'helpful', +'helping', +'helpless', +'helpline', +'hemlock', +'hemstitch', +'hence', +'henchman', +'henna', +'herald', +'herbal', +'herbicide', +'herbs', +'heritage', +'hermit', +'heroics', +'heroism', +'herring', +'herself', +'hertz', +'hesitancy', +'hesitant', +'hesitate', +'hexagon', +'hexagram', +'hubcap', +'huddle', +'huddling', +'huff', +'hug', +'hula', +'hulk', +'hull', +'human', +'humble', +'humbling', +'humbly', +'humid', +'humiliate', +'humility', +'humming', +'hummus', +'humongous', +'humorist', +'humorless', +'humorous', +'humpback', +'humped', +'humvee', +'hunchback', +'hundredth', +'hunger', +'hungrily', +'hungry', +'hunk', +'hunter', +'hunting', +'huntress', +'huntsman', +'hurdle', +'hurled', +'hurler', +'hurling', +'hurray', +'hurricane', +'hurried', +'hurry', +'hurt', +'husband', +'hush', +'husked', +'huskiness', +'hut', +'hybrid', +'hydrant', +'hydrated', +'hydration', +'hydrogen', +'hydroxide', +'hyperlink', +'hypertext', +'hyphen', +'hypnoses', +'hypnosis', +'hypnotic', +'hypnotism', +'hypnotist', +'hypnotize', +'hypocrisy', +'hypocrite', +'ibuprofen', +'ice', +'iciness', +'icing', +'icky', +'icon', +'icy', +'idealism', +'idealist', +'idealize', +'ideally', +'idealness', +'identical', +'identify', +'identity', +'ideology', +'idiocy', +'idiom', +'idly', +'igloo', +'ignition', +'ignore', +'iguana', +'illicitly', +'illusion', +'illusive', +'image', +'imaginary', +'imagines', +'imaging', +'imbecile', +'imitate', +'imitation', +'immature', +'immerse', +'immersion', +'imminent', +'immobile', +'immodest', +'immorally', +'immortal', +'immovable', +'immovably', +'immunity', +'immunize', +'impaired', +'impale', +'impart', +'impatient', +'impeach', +'impeding', +'impending', +'imperfect', +'imperial', +'impish', +'implant', +'implement', +'implicate', +'implicit', +'implode', +'implosion', +'implosive', +'imply', +'impolite', +'important', +'importer', +'impose', +'imposing', +'impotence', +'impotency', +'impotent', +'impound', +'imprecise', +'imprint', +'imprison', +'impromptu', +'improper', +'improve', +'improving', +'improvise', +'imprudent', +'impulse', +'impulsive', +'impure', +'impurity', +'iodine', +'iodize', +'ion', +'ipad', +'iphone', +'ipod', +'irate', +'irk', +'iron', +'irregular', +'irrigate', +'irritable', +'irritably', +'irritant', +'irritate', +'islamic', +'islamist', +'isolated', +'isolating', +'isolation', +'isotope', +'issue', +'issuing', +'italicize', +'italics', +'item', +'itinerary', +'itunes', +'ivory', +'ivy', +'jab', +'jackal', +'jacket', +'jackknife', +'jackpot', +'jailbird', +'jailbreak', +'jailer', +'jailhouse', +'jalapeno', +'jam', +'janitor', +'january', +'jargon', +'jarring', +'jasmine', +'jaundice', +'jaunt', +'java', +'jawed', +'jawless', +'jawline', +'jaws', +'jaybird', +'jaywalker', +'jazz', +'jeep', +'jeeringly', +'jellied', +'jelly', +'jersey', +'jester', +'jet', +'jiffy', +'jigsaw', +'jimmy', +'jingle', +'jingling', +'jinx', +'jitters', +'jittery', +'job', +'jockey', +'jockstrap', +'jogger', +'jogging', +'john', +'joining', +'jokester', +'jokingly', +'jolliness', +'jolly', +'jolt', +'jot', +'jovial', +'joyfully', +'joylessly', +'joyous', +'joyride', +'joystick', +'jubilance', +'jubilant', +'judge', +'judgingly', +'judicial', +'judiciary', +'judo', +'juggle', +'juggling', +'jugular', +'juice', +'juiciness', +'juicy', +'jujitsu', +'jukebox', +'july', +'jumble', +'jumbo', +'jump', +'junction', +'juncture', +'june', +'junior', +'juniper', +'junkie', +'junkman', +'junkyard', +'jurist', +'juror', +'jury', +'justice', +'justifier', +'justify', +'justly', +'justness', +'juvenile', +'kabob', +'kangaroo', +'karaoke', +'karate', +'karma', +'kebab', +'keenly', +'keenness', +'keep', +'keg', +'kelp', +'kennel', +'kept', +'kerchief', +'kerosene', +'kettle', +'kick', +'kiln', +'kilobyte', +'kilogram', +'kilometer', +'kilowatt', +'kilt', +'kimono', +'kindle', +'kindling', +'kindly', +'kindness', +'kindred', +'kinetic', +'kinfolk', +'king', +'kinship', +'kinsman', +'kinswoman', +'kissable', +'kisser', +'kissing', +'kitchen', +'kite', +'kitten', +'kitty', +'kiwi', +'kleenex', +'knapsack', +'knee', +'knelt', +'knickers', +'knoll', +'koala', +'kooky', +'kosher', +'krypton', +'kudos', +'kung', +'labored', +'laborer', +'laboring', +'laborious', +'labrador', +'ladder', +'ladies', +'ladle', +'ladybug', +'ladylike', +'lagged', +'lagging', +'lagoon', +'lair', +'lake', +'lance', +'landed', +'landfall', +'landfill', +'landing', +'landlady', +'landless', +'landline', +'landlord', +'landmark', +'landmass', +'landmine', +'landowner', +'landscape', +'landside', +'landslide', +'language', +'lankiness', +'lanky', +'lantern', +'lapdog', +'lapel', +'lapped', +'lapping', +'laptop', +'lard', +'large', +'lark', +'lash', +'lasso', +'last', +'latch', +'late', +'lather', +'latitude', +'latrine', +'latter', +'latticed', +'launch', +'launder', +'laundry', +'laurel', +'lavender', +'lavish', +'laxative', +'lazily', +'laziness', +'lazy', +'lecturer', +'left', +'legacy', +'legal', +'legend', +'legged', +'leggings', +'legible', +'legibly', +'legislate', +'lego', +'legroom', +'legume', +'legwarmer', +'legwork', +'lemon', +'lend', +'length', +'lens', +'lent', +'leotard', +'lesser', +'letdown', +'lethargic', +'lethargy', +'letter', +'lettuce', +'level', +'leverage', +'levers', +'levitate', +'levitator', +'liability', +'liable', +'liberty', +'librarian', +'library', +'licking', +'licorice', +'lid', +'life', +'lifter', +'lifting', +'liftoff', +'ligament', +'likely', +'likeness', +'likewise', +'liking', +'lilac', +'lilly', +'lily', +'limb', +'limeade', +'limelight', +'limes', +'limit', +'limping', +'limpness', +'line', +'lingo', +'linguini', +'linguist', +'lining', +'linked', +'linoleum', +'linseed', +'lint', +'lion', +'lip', +'liquefy', +'liqueur', +'liquid', +'lisp', +'list', +'litigate', +'litigator', +'litmus', +'litter', +'little', +'livable', +'lived', +'lively', +'liver', +'livestock', +'lividly', +'living', +'lizard', +'lubricant', +'lubricate', +'lucid', +'luckily', +'luckiness', +'luckless', +'lucrative', +'ludicrous', +'lugged', +'lukewarm', +'lullaby', +'lumber', +'luminance', +'luminous', +'lumpiness', +'lumping', +'lumpish', +'lunacy', +'lunar', +'lunchbox', +'luncheon', +'lunchroom', +'lunchtime', +'lung', +'lurch', +'lure', +'luridness', +'lurk', +'lushly', +'lushness', +'luster', +'lustfully', +'lustily', +'lustiness', +'lustrous', +'lusty', +'luxurious', +'luxury', +'lying', +'lyrically', +'lyricism', +'lyricist', +'lyrics', +'macarena', +'macaroni', +'macaw', +'mace', +'machine', +'machinist', +'magazine', +'magenta', +'maggot', +'magical', +'magician', +'magma', +'magnesium', +'magnetic', +'magnetism', +'magnetize', +'magnifier', +'magnify', +'magnitude', +'magnolia', +'mahogany', +'maimed', +'majestic', +'majesty', +'majorette', +'majority', +'makeover', +'maker', +'makeshift', +'making', +'malformed', +'malt', +'mama', +'mammal', +'mammary', +'mammogram', +'manager', +'managing', +'manatee', +'mandarin', +'mandate', +'mandatory', +'mandolin', +'manger', +'mangle', +'mango', +'mangy', +'manhandle', +'manhole', +'manhood', +'manhunt', +'manicotti', +'manicure', +'manifesto', +'manila', +'mankind', +'manlike', +'manliness', +'manly', +'manmade', +'manned', +'mannish', +'manor', +'manpower', +'mantis', +'mantra', +'manual', +'many', +'map', +'marathon', +'marauding', +'marbled', +'marbles', +'marbling', +'march', +'mardi', +'margarine', +'margarita', +'margin', +'marigold', +'marina', +'marine', +'marital', +'maritime', +'marlin', +'marmalade', +'maroon', +'married', +'marrow', +'marry', +'marshland', +'marshy', +'marsupial', +'marvelous', +'marxism', +'mascot', +'masculine', +'mashed', +'mashing', +'massager', +'masses', +'massive', +'mastiff', +'matador', +'matchbook', +'matchbox', +'matcher', +'matching', +'matchless', +'material', +'maternal', +'maternity', +'math', +'mating', +'matriarch', +'matrimony', +'matrix', +'matron', +'matted', +'matter', +'maturely', +'maturing', +'maturity', +'mauve', +'maverick', +'maximize', +'maximum', +'maybe', +'mayday', +'mayflower', +'moaner', +'moaning', +'mobile', +'mobility', +'mobilize', +'mobster', +'mocha', +'mocker', +'mockup', +'modified', +'modify', +'modular', +'modulator', +'module', +'moisten', +'moistness', +'moisture', +'molar', +'molasses', +'mold', +'molecular', +'molecule', +'molehill', +'mollusk', +'mom', +'monastery', +'monday', +'monetary', +'monetize', +'moneybags', +'moneyless', +'moneywise', +'mongoose', +'mongrel', +'monitor', +'monkhood', +'monogamy', +'monogram', +'monologue', +'monopoly', +'monorail', +'monotone', +'monotype', +'monoxide', +'monsieur', +'monsoon', +'monstrous', +'monthly', +'monument', +'moocher', +'moodiness', +'moody', +'mooing', +'moonbeam', +'mooned', +'moonlight', +'moonlike', +'moonlit', +'moonrise', +'moonscape', +'moonshine', +'moonstone', +'moonwalk', +'mop', +'morale', +'morality', +'morally', +'morbidity', +'morbidly', +'morphine', +'morphing', +'morse', +'mortality', +'mortally', +'mortician', +'mortified', +'mortify', +'mortuary', +'mosaic', +'mossy', +'most', +'mothball', +'mothproof', +'motion', +'motivate', +'motivator', +'motive', +'motocross', +'motor', +'motto', +'mountable', +'mountain', +'mounted', +'mounting', +'mourner', +'mournful', +'mouse', +'mousiness', +'moustache', +'mousy', +'mouth', +'movable', +'move', +'movie', +'moving', +'mower', +'mowing', +'much', +'muck', +'mud', +'mug', +'mulberry', +'mulch', +'mule', +'mulled', +'mullets', +'multiple', +'multiply', +'multitask', +'multitude', +'mumble', +'mumbling', +'mumbo', +'mummified', +'mummify', +'mummy', +'mumps', +'munchkin', +'mundane', +'municipal', +'muppet', +'mural', +'murkiness', +'murky', +'murmuring', +'muscular', +'museum', +'mushily', +'mushiness', +'mushroom', +'mushy', +'music', +'musket', +'muskiness', +'musky', +'mustang', +'mustard', +'muster', +'mustiness', +'musty', +'mutable', +'mutate', +'mutation', +'mute', +'mutilated', +'mutilator', +'mutiny', +'mutt', +'mutual', +'muzzle', +'myself', +'myspace', +'mystified', +'mystify', +'myth', +'nacho', +'nag', +'nail', +'name', +'naming', +'nanny', +'nanometer', +'nape', +'napkin', +'napped', +'napping', +'nappy', +'narrow', +'nastily', +'nastiness', +'national', +'native', +'nativity', +'natural', +'nature', +'naturist', +'nautical', +'navigate', +'navigator', +'navy', +'nearby', +'nearest', +'nearly', +'nearness', +'neatly', +'neatness', +'nebula', +'nebulizer', +'nectar', +'negate', +'negation', +'negative', +'neglector', +'negligee', +'negligent', +'negotiate', +'nemeses', +'nemesis', +'neon', +'nephew', +'nerd', +'nervous', +'nervy', +'nest', +'net', +'neurology', +'neuron', +'neurosis', +'neurotic', +'neuter', +'neutron', +'never', +'next', +'nibble', +'nickname', +'nicotine', +'niece', +'nifty', +'nimble', +'nimbly', +'nineteen', +'ninetieth', +'ninja', +'nintendo', +'ninth', +'nuclear', +'nuclei', +'nucleus', +'nugget', +'nullify', +'number', +'numbing', +'numbly', +'numbness', +'numeral', +'numerate', +'numerator', +'numeric', +'numerous', +'nuptials', +'nursery', +'nursing', +'nurture', +'nutcase', +'nutlike', +'nutmeg', +'nutrient', +'nutshell', +'nuttiness', +'nutty', +'nuzzle', +'nylon', +'oaf', +'oak', +'oasis', +'oat', +'obedience', +'obedient', +'obituary', +'object', +'obligate', +'obliged', +'oblivion', +'oblivious', +'oblong', +'obnoxious', +'oboe', +'obscure', +'obscurity', +'observant', +'observer', +'observing', +'obsessed', +'obsession', +'obsessive', +'obsolete', +'obstacle', +'obstinate', +'obstruct', +'obtain', +'obtrusive', +'obtuse', +'obvious', +'occultist', +'occupancy', +'occupant', +'occupier', +'occupy', +'ocean', +'ocelot', +'octagon', +'octane', +'october', +'octopus', +'ogle', +'oil', +'oink', +'ointment', +'okay', +'old', +'olive', +'olympics', +'omega', +'omen', +'ominous', +'omission', +'omit', +'omnivore', +'onboard', +'oncoming', +'ongoing', +'onion', +'online', +'onlooker', +'only', +'onscreen', +'onset', +'onshore', +'onslaught', +'onstage', +'onto', +'onward', +'onyx', +'oops', +'ooze', +'oozy', +'opacity', +'opal', +'open', +'operable', +'operate', +'operating', +'operation', +'operative', +'operator', +'opium', +'opossum', +'opponent', +'oppose', +'opposing', +'opposite', +'oppressed', +'oppressor', +'opt', +'opulently', +'osmosis', +'other', +'otter', +'ouch', +'ought', +'ounce', +'outage', +'outback', +'outbid', +'outboard', +'outbound', +'outbreak', +'outburst', +'outcast', +'outclass', +'outcome', +'outdated', +'outdoors', +'outer', +'outfield', +'outfit', +'outflank', +'outgoing', +'outgrow', +'outhouse', +'outing', +'outlast', +'outlet', +'outline', +'outlook', +'outlying', +'outmatch', +'outmost', +'outnumber', +'outplayed', +'outpost', +'outpour', +'output', +'outrage', +'outrank', +'outreach', +'outright', +'outscore', +'outsell', +'outshine', +'outshoot', +'outsider', +'outskirts', +'outsmart', +'outsource', +'outspoken', +'outtakes', +'outthink', +'outward', +'outweigh', +'outwit', +'oval', +'ovary', +'oven', +'overact', +'overall', +'overarch', +'overbid', +'overbill', +'overbite', +'overblown', +'overboard', +'overbook', +'overbuilt', +'overcast', +'overcoat', +'overcome', +'overcook', +'overcrowd', +'overdraft', +'overdrawn', +'overdress', +'overdrive', +'overdue', +'overeager', +'overeater', +'overexert', +'overfed', +'overfeed', +'overfill', +'overflow', +'overfull', +'overgrown', +'overhand', +'overhang', +'overhaul', +'overhead', +'overhear', +'overheat', +'overhung', +'overjoyed', +'overkill', +'overlabor', +'overlaid', +'overlap', +'overlay', +'overload', +'overlook', +'overlord', +'overlying', +'overnight', +'overpass', +'overpay', +'overplant', +'overplay', +'overpower', +'overprice', +'overrate', +'overreach', +'overreact', +'override', +'overripe', +'overrule', +'overrun', +'overshoot', +'overshot', +'oversight', +'oversized', +'oversleep', +'oversold', +'overspend', +'overstate', +'overstay', +'overstep', +'overstock', +'overstuff', +'oversweet', +'overtake', +'overthrow', +'overtime', +'overtly', +'overtone', +'overture', +'overturn', +'overuse', +'overvalue', +'overview', +'overwrite', +'owl', +'oxford', +'oxidant', +'oxidation', +'oxidize', +'oxidizing', +'oxygen', +'oxymoron', +'oyster', +'ozone', +'paced', +'pacemaker', +'pacific', +'pacifier', +'pacifism', +'pacifist', +'pacify', +'padded', +'padding', +'paddle', +'paddling', +'padlock', +'pagan', +'pager', +'paging', +'pajamas', +'palace', +'palatable', +'palm', +'palpable', +'palpitate', +'paltry', +'pampered', +'pamperer', +'pampers', +'pamphlet', +'panama', +'pancake', +'pancreas', +'panda', +'pandemic', +'pang', +'panhandle', +'panic', +'panning', +'panorama', +'panoramic', +'panther', +'pantomime', +'pantry', +'pants', +'pantyhose', +'paparazzi', +'papaya', +'paper', +'paprika', +'papyrus', +'parabola', +'parachute', +'parade', +'paradox', +'paragraph', +'parakeet', +'paralegal', +'paralyses', +'paralysis', +'paralyze', +'paramedic', +'parameter', +'paramount', +'parasail', +'parasite', +'parasitic', +'parcel', +'parched', +'parchment', +'pardon', +'parish', +'parka', +'parking', +'parkway', +'parlor', +'parmesan', +'parole', +'parrot', +'parsley', +'parsnip', +'partake', +'parted', +'parting', +'partition', +'partly', +'partner', +'partridge', +'party', +'passable', +'passably', +'passage', +'passcode', +'passenger', +'passerby', +'passing', +'passion', +'passive', +'passivism', +'passover', +'passport', +'password', +'pasta', +'pasted', +'pastel', +'pastime', +'pastor', +'pastrami', +'pasture', +'pasty', +'patchwork', +'patchy', +'paternal', +'paternity', +'path', +'patience', +'patient', +'patio', +'patriarch', +'patriot', +'patrol', +'patronage', +'patronize', +'pauper', +'pavement', +'paver', +'pavestone', +'pavilion', +'paving', +'pawing', +'payable', +'payback', +'paycheck', +'payday', +'payee', +'payer', +'paying', +'payment', +'payphone', +'payroll', +'pebble', +'pebbly', +'pecan', +'pectin', +'peculiar', +'peddling', +'pediatric', +'pedicure', +'pedigree', +'pedometer', +'pegboard', +'pelican', +'pellet', +'pelt', +'pelvis', +'penalize', +'penalty', +'pencil', +'pendant', +'pending', +'penholder', +'penknife', +'pennant', +'penniless', +'penny', +'penpal', +'pension', +'pentagon', +'pentagram', +'pep', +'perceive', +'percent', +'perch', +'percolate', +'perennial', +'perfected', +'perfectly', +'perfume', +'periscope', +'perish', +'perjurer', +'perjury', +'perkiness', +'perky', +'perm', +'peroxide', +'perpetual', +'perplexed', +'persecute', +'persevere', +'persuaded', +'persuader', +'pesky', +'peso', +'pessimism', +'pessimist', +'pester', +'pesticide', +'petal', +'petite', +'petition', +'petri', +'petroleum', +'petted', +'petticoat', +'pettiness', +'petty', +'petunia', +'phantom', +'phobia', +'phoenix', +'phonebook', +'phoney', +'phonics', +'phoniness', +'phony', +'phosphate', +'photo', +'phrase', +'phrasing', +'placard', +'placate', +'placidly', +'plank', +'planner', +'plant', +'plasma', +'plaster', +'plastic', +'plated', +'platform', +'plating', +'platinum', +'platonic', +'platter', +'platypus', +'plausible', +'plausibly', +'playable', +'playback', +'player', +'playful', +'playgroup', +'playhouse', +'playing', +'playlist', +'playmaker', +'playmate', +'playoff', +'playpen', +'playroom', +'playset', +'plaything', +'playtime', +'plaza', +'pleading', +'pleat', +'pledge', +'plentiful', +'plenty', +'plethora', +'plexiglas', +'pliable', +'plod', +'plop', +'plot', +'plow', +'ploy', +'pluck', +'plug', +'plunder', +'plunging', +'plural', +'plus', +'plutonium', +'plywood', +'poach', +'pod', +'poem', +'poet', +'pogo', +'pointed', +'pointer', +'pointing', +'pointless', +'pointy', +'poise', +'poison', +'poker', +'poking', +'polar', +'police', +'policy', +'polio', +'polish', +'politely', +'polka', +'polo', +'polyester', +'polygon', +'polygraph', +'polymer', +'poncho', +'pond', +'pony', +'popcorn', +'pope', +'poplar', +'popper', +'poppy', +'popsicle', +'populace', +'popular', +'populate', +'porcupine', +'pork', +'porous', +'porridge', +'portable', +'portal', +'portfolio', +'porthole', +'portion', +'portly', +'portside', +'poser', +'posh', +'posing', +'possible', +'possibly', +'possum', +'postage', +'postal', +'postbox', +'postcard', +'posted', +'poster', +'posting', +'postnasal', +'posture', +'postwar', +'pouch', +'pounce', +'pouncing', +'pound', +'pouring', +'pout', +'powdered', +'powdering', +'powdery', +'power', +'powwow', +'pox', +'praising', +'prance', +'prancing', +'pranker', +'prankish', +'prankster', +'prayer', +'praying', +'preacher', +'preaching', +'preachy', +'preamble', +'precinct', +'precise', +'precision', +'precook', +'precut', +'predator', +'predefine', +'predict', +'preface', +'prefix', +'preflight', +'preformed', +'pregame', +'pregnancy', +'pregnant', +'preheated', +'prelaunch', +'prelaw', +'prelude', +'premiere', +'premises', +'premium', +'prenatal', +'preoccupy', +'preorder', +'prepaid', +'prepay', +'preplan', +'preppy', +'preschool', +'prescribe', +'preseason', +'preset', +'preshow', +'president', +'presoak', +'press', +'presume', +'presuming', +'preteen', +'pretended', +'pretender', +'pretense', +'pretext', +'pretty', +'pretzel', +'prevail', +'prevalent', +'prevent', +'preview', +'previous', +'prewar', +'prewashed', +'prideful', +'pried', +'primal', +'primarily', +'primary', +'primate', +'primer', +'primp', +'princess', +'print', +'prior', +'prism', +'prison', +'prissy', +'pristine', +'privacy', +'private', +'privatize', +'prize', +'proactive', +'probable', +'probably', +'probation', +'probe', +'probing', +'probiotic', +'problem', +'procedure', +'process', +'proclaim', +'procreate', +'procurer', +'prodigal', +'prodigy', +'produce', +'product', +'profane', +'profanity', +'professed', +'professor', +'profile', +'profound', +'profusely', +'progeny', +'prognosis', +'program', +'progress', +'projector', +'prologue', +'prolonged', +'promenade', +'prominent', +'promoter', +'promotion', +'prompter', +'promptly', +'prone', +'prong', +'pronounce', +'pronto', +'proofing', +'proofread', +'proofs', +'propeller', +'properly', +'property', +'proponent', +'proposal', +'propose', +'props', +'prorate', +'protector', +'protegee', +'proton', +'prototype', +'protozoan', +'protract', +'protrude', +'proud', +'provable', +'proved', +'proven', +'provided', +'provider', +'providing', +'province', +'proving', +'provoke', +'provoking', +'provolone', +'prowess', +'prowler', +'prowling', +'proximity', +'proxy', +'prozac', +'prude', +'prudishly', +'prune', +'pruning', +'pry', +'psychic', +'public', +'publisher', +'pucker', +'pueblo', +'pug', +'pull', +'pulmonary', +'pulp', +'pulsate', +'pulse', +'pulverize', +'puma', +'pumice', +'pummel', +'punch', +'punctual', +'punctuate', +'punctured', +'pungent', +'punisher', +'punk', +'pupil', +'puppet', +'puppy', +'purchase', +'pureblood', +'purebred', +'purely', +'pureness', +'purgatory', +'purge', +'purging', +'purifier', +'purify', +'purist', +'puritan', +'purity', +'purple', +'purplish', +'purposely', +'purr', +'purse', +'pursuable', +'pursuant', +'pursuit', +'purveyor', +'pushcart', +'pushchair', +'pusher', +'pushiness', +'pushing', +'pushover', +'pushpin', +'pushup', +'pushy', +'putdown', +'putt', +'puzzle', +'puzzling', +'pyramid', +'pyromania', +'python', +'quack', +'quadrant', +'quail', +'quaintly', +'quake', +'quaking', +'qualified', +'qualifier', +'qualify', +'quality', +'qualm', +'quantum', +'quarrel', +'quarry', +'quartered', +'quarterly', +'quarters', +'quartet', +'quench', +'query', +'quicken', +'quickly', +'quickness', +'quicksand', +'quickstep', +'quiet', +'quill', +'quilt', +'quintet', +'quintuple', +'quirk', +'quit', +'quiver', +'quizzical', +'quotable', +'quotation', +'quote', +'rabid', +'race', +'racing', +'racism', +'rack', +'racoon', +'radar', +'radial', +'radiance', +'radiantly', +'radiated', +'radiation', +'radiator', +'radio', +'radish', +'raffle', +'raft', +'rage', +'ragged', +'raging', +'ragweed', +'raider', +'railcar', +'railing', +'railroad', +'railway', +'raisin', +'rake', +'raking', +'rally', +'ramble', +'rambling', +'ramp', +'ramrod', +'ranch', +'rancidity', +'random', +'ranged', +'ranger', +'ranging', +'ranked', +'ranking', +'ransack', +'ranting', +'rants', +'rare', +'rarity', +'rascal', +'rash', +'rasping', +'ravage', +'raven', +'ravine', +'raving', +'ravioli', +'ravishing', +'reabsorb', +'reach', +'reacquire', +'reaction', +'reactive', +'reactor', +'reaffirm', +'ream', +'reanalyze', +'reappear', +'reapply', +'reappoint', +'reapprove', +'rearrange', +'rearview', +'reason', +'reassign', +'reassure', +'reattach', +'reawake', +'rebalance', +'rebate', +'rebel', +'rebirth', +'reboot', +'reborn', +'rebound', +'rebuff', +'rebuild', +'rebuilt', +'reburial', +'rebuttal', +'recall', +'recant', +'recapture', +'recast', +'recede', +'recent', +'recess', +'recharger', +'recipient', +'recital', +'recite', +'reckless', +'reclaim', +'recliner', +'reclining', +'recluse', +'reclusive', +'recognize', +'recoil', +'recollect', +'recolor', +'reconcile', +'reconfirm', +'reconvene', +'recopy', +'record', +'recount', +'recoup', +'recovery', +'recreate', +'rectal', +'rectangle', +'rectified', +'rectify', +'recycled', +'recycler', +'recycling', +'reemerge', +'reenact', +'reenter', +'reentry', +'reexamine', +'referable', +'referee', +'reference', +'refill', +'refinance', +'refined', +'refinery', +'refining', +'refinish', +'reflected', +'reflector', +'reflex', +'reflux', +'refocus', +'refold', +'reforest', +'reformat', +'reformed', +'reformer', +'reformist', +'refract', +'refrain', +'refreeze', +'refresh', +'refried', +'refueling', +'refund', +'refurbish', +'refurnish', +'refusal', +'refuse', +'refusing', +'refutable', +'refute', +'regain', +'regalia', +'regally', +'reggae', +'regime', +'region', +'register', +'registrar', +'registry', +'regress', +'regretful', +'regroup', +'regular', +'regulate', +'regulator', +'rehab', +'reheat', +'rehire', +'rehydrate', +'reimburse', +'reissue', +'reiterate', +'rejoice', +'rejoicing', +'rejoin', +'rekindle', +'relapse', +'relapsing', +'relatable', +'related', +'relation', +'relative', +'relax', +'relay', +'relearn', +'release', +'relenting', +'reliable', +'reliably', +'reliance', +'reliant', +'relic', +'relieve', +'relieving', +'relight', +'relish', +'relive', +'reload', +'relocate', +'relock', +'reluctant', +'rely', +'remake', +'remark', +'remarry', +'rematch', +'remedial', +'remedy', +'remember', +'reminder', +'remindful', +'remission', +'remix', +'remnant', +'remodeler', +'remold', +'remorse', +'remote', +'removable', +'removal', +'removed', +'remover', +'removing', +'rename', +'renderer', +'rendering', +'rendition', +'renegade', +'renewable', +'renewably', +'renewal', +'renewed', +'renounce', +'renovate', +'renovator', +'rentable', +'rental', +'rented', +'renter', +'reoccupy', +'reoccur', +'reopen', +'reorder', +'repackage', +'repacking', +'repaint', +'repair', +'repave', +'repaying', +'repayment', +'repeal', +'repeated', +'repeater', +'repent', +'rephrase', +'replace', +'replay', +'replica', +'reply', +'reporter', +'repose', +'repossess', +'repost', +'repressed', +'reprimand', +'reprint', +'reprise', +'reproach', +'reprocess', +'reproduce', +'reprogram', +'reps', +'reptile', +'reptilian', +'repugnant', +'repulsion', +'repulsive', +'repurpose', +'reputable', +'reputably', +'request', +'require', +'requisite', +'reroute', +'rerun', +'resale', +'resample', +'rescuer', +'reseal', +'research', +'reselect', +'reseller', +'resemble', +'resend', +'resent', +'reset', +'reshape', +'reshoot', +'reshuffle', +'residence', +'residency', +'resident', +'residual', +'residue', +'resigned', +'resilient', +'resistant', +'resisting', +'resize', +'resolute', +'resolved', +'resonant', +'resonate', +'resort', +'resource', +'respect', +'resubmit', +'result', +'resume', +'resupply', +'resurface', +'resurrect', +'retail', +'retainer', +'retaining', +'retake', +'retaliate', +'retention', +'rethink', +'retinal', +'retired', +'retiree', +'retiring', +'retold', +'retool', +'retorted', +'retouch', +'retrace', +'retract', +'retrain', +'retread', +'retreat', +'retrial', +'retrieval', +'retriever', +'retry', +'return', +'retying', +'retype', +'reunion', +'reunite', +'reusable', +'reuse', +'reveal', +'reveler', +'revenge', +'revenue', +'reverb', +'revered', +'reverence', +'reverend', +'reversal', +'reverse', +'reversing', +'reversion', +'revert', +'revisable', +'revise', +'revision', +'revisit', +'revivable', +'revival', +'reviver', +'reviving', +'revocable', +'revoke', +'revolt', +'revolver', +'revolving', +'reward', +'rewash', +'rewind', +'rewire', +'reword', +'rework', +'rewrap', +'rewrite', +'rhyme', +'ribbon', +'ribcage', +'rice', +'riches', +'richly', +'richness', +'rickety', +'ricotta', +'riddance', +'ridden', +'ride', +'riding', +'rifling', +'rift', +'rigging', +'rigid', +'rigor', +'rimless', +'rimmed', +'rind', +'rink', +'rinse', +'rinsing', +'riot', +'ripcord', +'ripeness', +'ripening', +'ripping', +'ripple', +'rippling', +'riptide', +'rise', +'rising', +'risk', +'risotto', +'ritalin', +'ritzy', +'rival', +'riverbank', +'riverbed', +'riverboat', +'riverside', +'riveter', +'riveting', +'roamer', +'roaming', +'roast', +'robbing', +'robe', +'robin', +'robotics', +'robust', +'rockband', +'rocker', +'rocket', +'rockfish', +'rockiness', +'rocking', +'rocklike', +'rockslide', +'rockstar', +'rocky', +'rogue', +'roman', +'romp', +'rope', +'roping', +'roster', +'rosy', +'rotten', +'rotting', +'rotunda', +'roulette', +'rounding', +'roundish', +'roundness', +'roundup', +'roundworm', +'routine', +'routing', +'rover', +'roving', +'royal', +'rubbed', +'rubber', +'rubbing', +'rubble', +'rubdown', +'ruby', +'ruckus', +'rudder', +'rug', +'ruined', +'rule', +'rumble', +'rumbling', +'rummage', +'rumor', +'runaround', +'rundown', +'runner', +'running', +'runny', +'runt', +'runway', +'rupture', +'rural', +'ruse', +'rush', +'rust', +'rut', +'sabbath', +'sabotage', +'sacrament', +'sacred', +'sacrifice', +'sadden', +'saddlebag', +'saddled', +'saddling', +'sadly', +'sadness', +'safari', +'safeguard', +'safehouse', +'safely', +'safeness', +'saffron', +'saga', +'sage', +'sagging', +'saggy', +'said', +'saint', +'sake', +'salad', +'salami', +'salaried', +'salary', +'saline', +'salon', +'saloon', +'salsa', +'salt', +'salutary', +'salute', +'salvage', +'salvaging', +'salvation', +'same', +'sample', +'sampling', +'sanction', +'sanctity', +'sanctuary', +'sandal', +'sandbag', +'sandbank', +'sandbar', +'sandblast', +'sandbox', +'sanded', +'sandfish', +'sanding', +'sandlot', +'sandpaper', +'sandpit', +'sandstone', +'sandstorm', +'sandworm', +'sandy', +'sanitary', +'sanitizer', +'sank', +'santa', +'sapling', +'sappiness', +'sappy', +'sarcasm', +'sarcastic', +'sardine', +'sash', +'sasquatch', +'sassy', +'satchel', +'satiable', +'satin', +'satirical', +'satisfied', +'satisfy', +'saturate', +'saturday', +'sauciness', +'saucy', +'sauna', +'savage', +'savanna', +'saved', +'savings', +'savior', +'savor', +'saxophone', +'say', +'scabbed', +'scabby', +'scalded', +'scalding', +'scale', +'scaling', +'scallion', +'scallop', +'scalping', +'scam', +'scandal', +'scanner', +'scanning', +'scant', +'scapegoat', +'scarce', +'scarcity', +'scarecrow', +'scared', +'scarf', +'scarily', +'scariness', +'scarring', +'scary', +'scavenger', +'scenic', +'schedule', +'schematic', +'scheme', +'scheming', +'schilling', +'schnapps', +'scholar', +'science', +'scientist', +'scion', +'scoff', +'scolding', +'scone', +'scoop', +'scooter', +'scope', +'scorch', +'scorebook', +'scorecard', +'scored', +'scoreless', +'scorer', +'scoring', +'scorn', +'scorpion', +'scotch', +'scoundrel', +'scoured', +'scouring', +'scouting', +'scouts', +'scowling', +'scrabble', +'scraggly', +'scrambled', +'scrambler', +'scrap', +'scratch', +'scrawny', +'screen', +'scribble', +'scribe', +'scribing', +'scrimmage', +'script', +'scroll', +'scrooge', +'scrounger', +'scrubbed', +'scrubber', +'scruffy', +'scrunch', +'scrutiny', +'scuba', +'scuff', +'sculptor', +'sculpture', +'scurvy', +'scuttle', +'secluded', +'secluding', +'seclusion', +'second', +'secrecy', +'secret', +'sectional', +'sector', +'secular', +'securely', +'security', +'sedan', +'sedate', +'sedation', +'sedative', +'sediment', +'seduce', +'seducing', +'segment', +'seismic', +'seizing', +'seldom', +'selected', +'selection', +'selective', +'selector', +'self', +'seltzer', +'semantic', +'semester', +'semicolon', +'semifinal', +'seminar', +'semisoft', +'semisweet', +'senate', +'senator', +'send', +'senior', +'senorita', +'sensation', +'sensitive', +'sensitize', +'sensually', +'sensuous', +'sepia', +'september', +'septic', +'septum', +'sequel', +'sequence', +'sequester', +'series', +'sermon', +'serotonin', +'serpent', +'serrated', +'serve', +'service', +'serving', +'sesame', +'sessions', +'setback', +'setting', +'settle', +'settling', +'setup', +'sevenfold', +'seventeen', +'seventh', +'seventy', +'severity', +'shabby', +'shack', +'shaded', +'shadily', +'shadiness', +'shading', +'shadow', +'shady', +'shaft', +'shakable', +'shakily', +'shakiness', +'shaking', +'shaky', +'shale', +'shallot', +'shallow', +'shame', +'shampoo', +'shamrock', +'shank', +'shanty', +'shape', +'shaping', +'share', +'sharpener', +'sharper', +'sharpie', +'sharply', +'sharpness', +'shawl', +'sheath', +'shed', +'sheep', +'sheet', +'shelf', +'shell', +'shelter', +'shelve', +'shelving', +'sherry', +'shield', +'shifter', +'shifting', +'shiftless', +'shifty', +'shimmer', +'shimmy', +'shindig', +'shine', +'shingle', +'shininess', +'shining', +'shiny', +'ship', +'shirt', +'shivering', +'shock', +'shone', +'shoplift', +'shopper', +'shopping', +'shoptalk', +'shore', +'shortage', +'shortcake', +'shortcut', +'shorten', +'shorter', +'shorthand', +'shortlist', +'shortly', +'shortness', +'shorts', +'shortwave', +'shorty', +'shout', +'shove', +'showbiz', +'showcase', +'showdown', +'shower', +'showgirl', +'showing', +'showman', +'shown', +'showoff', +'showpiece', +'showplace', +'showroom', +'showy', +'shrank', +'shrapnel', +'shredder', +'shredding', +'shrewdly', +'shriek', +'shrill', +'shrimp', +'shrine', +'shrink', +'shrivel', +'shrouded', +'shrubbery', +'shrubs', +'shrug', +'shrunk', +'shucking', +'shudder', +'shuffle', +'shuffling', +'shun', +'shush', +'shut', +'shy', +'siamese', +'siberian', +'sibling', +'siding', +'sierra', +'siesta', +'sift', +'sighing', +'silenced', +'silencer', +'silent', +'silica', +'silicon', +'silk', +'silliness', +'silly', +'silo', +'silt', +'silver', +'similarly', +'simile', +'simmering', +'simple', +'simplify', +'simply', +'sincere', +'sincerity', +'singer', +'singing', +'single', +'singular', +'sinister', +'sinless', +'sinner', +'sinuous', +'sip', +'siren', +'sister', +'sitcom', +'sitter', +'sitting', +'situated', +'situation', +'sixfold', +'sixteen', +'sixth', +'sixties', +'sixtieth', +'sixtyfold', +'sizable', +'sizably', +'size', +'sizing', +'sizzle', +'sizzling', +'skater', +'skating', +'skedaddle', +'skeletal', +'skeleton', +'skeptic', +'sketch', +'skewed', +'skewer', +'skid', +'skied', +'skier', +'skies', +'skiing', +'skilled', +'skillet', +'skillful', +'skimmed', +'skimmer', +'skimming', +'skimpily', +'skincare', +'skinhead', +'skinless', +'skinning', +'skinny', +'skintight', +'skipper', +'skipping', +'skirmish', +'skirt', +'skittle', +'skydiver', +'skylight', +'skyline', +'skype', +'skyrocket', +'skyward', +'slab', +'slacked', +'slacker', +'slacking', +'slackness', +'slacks', +'slain', +'slam', +'slander', +'slang', +'slapping', +'slapstick', +'slashed', +'slashing', +'slate', +'slather', +'slaw', +'sled', +'sleek', +'sleep', +'sleet', +'sleeve', +'slept', +'sliceable', +'sliced', +'slicer', +'slicing', +'slick', +'slider', +'slideshow', +'sliding', +'slighted', +'slighting', +'slightly', +'slimness', +'slimy', +'slinging', +'slingshot', +'slinky', +'slip', +'slit', +'sliver', +'slobbery', +'slogan', +'sloped', +'sloping', +'sloppily', +'sloppy', +'slot', +'slouching', +'slouchy', +'sludge', +'slug', +'slum', +'slurp', +'slush', +'sly', +'small', +'smartly', +'smartness', +'smasher', +'smashing', +'smashup', +'smell', +'smelting', +'smile', +'smilingly', +'smirk', +'smite', +'smith', +'smitten', +'smock', +'smog', +'smoked', +'smokeless', +'smokiness', +'smoking', +'smoky', +'smolder', +'smooth', +'smother', +'smudge', +'smudgy', +'smuggler', +'smuggling', +'smugly', +'smugness', +'snack', +'snagged', +'snaking', +'snap', +'snare', +'snarl', +'snazzy', +'sneak', +'sneer', +'sneeze', +'sneezing', +'snide', +'sniff', +'snippet', +'snipping', +'snitch', +'snooper', +'snooze', +'snore', +'snoring', +'snorkel', +'snort', +'snout', +'snowbird', +'snowboard', +'snowbound', +'snowcap', +'snowdrift', +'snowdrop', +'snowfall', +'snowfield', +'snowflake', +'snowiness', +'snowless', +'snowman', +'snowplow', +'snowshoe', +'snowstorm', +'snowsuit', +'snowy', +'snub', +'snuff', +'snuggle', +'snugly', +'snugness', +'speak', +'spearfish', +'spearhead', +'spearman', +'spearmint', +'species', +'specimen', +'specked', +'speckled', +'specks', +'spectacle', +'spectator', +'spectrum', +'speculate', +'speech', +'speed', +'spellbind', +'speller', +'spelling', +'spendable', +'spender', +'spending', +'spent', +'spew', +'sphere', +'spherical', +'sphinx', +'spider', +'spied', +'spiffy', +'spill', +'spilt', +'spinach', +'spinal', +'spindle', +'spinner', +'spinning', +'spinout', +'spinster', +'spiny', +'spiral', +'spirited', +'spiritism', +'spirits', +'spiritual', +'splashed', +'splashing', +'splashy', +'splatter', +'spleen', +'splendid', +'splendor', +'splice', +'splicing', +'splinter', +'splotchy', +'splurge', +'spoilage', +'spoiled', +'spoiler', +'spoiling', +'spoils', +'spoken', +'spokesman', +'sponge', +'spongy', +'sponsor', +'spoof', +'spookily', +'spooky', +'spool', +'spoon', +'spore', +'sporting', +'sports', +'sporty', +'spotless', +'spotlight', +'spotted', +'spotter', +'spotting', +'spotty', +'spousal', +'spouse', +'spout', +'sprain', +'sprang', +'sprawl', +'spray', +'spree', +'sprig', +'spring', +'sprinkled', +'sprinkler', +'sprint', +'sprite', +'sprout', +'spruce', +'sprung', +'spry', +'spud', +'spur', +'sputter', +'spyglass', +'squabble', +'squad', +'squall', +'squander', +'squash', +'squatted', +'squatter', +'squatting', +'squeak', +'squealer', +'squealing', +'squeamish', +'squeegee', +'squeeze', +'squeezing', +'squid', +'squiggle', +'squiggly', +'squint', +'squire', +'squirt', +'squishier', +'squishy', +'stability', +'stabilize', +'stable', +'stack', +'stadium', +'staff', +'stage', +'staging', +'stagnant', +'stagnate', +'stainable', +'stained', +'staining', +'stainless', +'stalemate', +'staleness', +'stalling', +'stallion', +'stamina', +'stammer', +'stamp', +'stand', +'stank', +'staple', +'stapling', +'starboard', +'starch', +'stardom', +'stardust', +'starfish', +'stargazer', +'staring', +'stark', +'starless', +'starlet', +'starlight', +'starlit', +'starring', +'starry', +'starship', +'starter', +'starting', +'startle', +'startling', +'startup', +'starved', +'starving', +'stash', +'state', +'static', +'statistic', +'statue', +'stature', +'status', +'statute', +'statutory', +'staunch', +'stays', +'steadfast', +'steadier', +'steadily', +'steadying', +'steam', +'steed', +'steep', +'steerable', +'steering', +'steersman', +'stegosaur', +'stellar', +'stem', +'stench', +'stencil', +'step', +'stereo', +'sterile', +'sterility', +'sterilize', +'sterling', +'sternness', +'sternum', +'stew', +'stick', +'stiffen', +'stiffly', +'stiffness', +'stifle', +'stifling', +'stillness', +'stilt', +'stimulant', +'stimulate', +'stimuli', +'stimulus', +'stinger', +'stingily', +'stinging', +'stingray', +'stingy', +'stinking', +'stinky', +'stipend', +'stipulate', +'stir', +'stitch', +'stock', +'stoic', +'stoke', +'stole', +'stomp', +'stonewall', +'stoneware', +'stonework', +'stoning', +'stony', +'stood', +'stooge', +'stool', +'stoop', +'stoplight', +'stoppable', +'stoppage', +'stopped', +'stopper', +'stopping', +'stopwatch', +'storable', +'storage', +'storeroom', +'storewide', +'storm', +'stout', +'stove', +'stowaway', +'stowing', +'straddle', +'straggler', +'strained', +'strainer', +'straining', +'strangely', +'stranger', +'strangle', +'strategic', +'strategy', +'stratus', +'straw', +'stray', +'streak', +'stream', +'street', +'strength', +'strenuous', +'strep', +'stress', +'stretch', +'strewn', +'stricken', +'strict', +'stride', +'strife', +'strike', +'striking', +'strive', +'striving', +'strobe', +'strode', +'stroller', +'strongbox', +'strongly', +'strongman', +'struck', +'structure', +'strudel', +'struggle', +'strum', +'strung', +'strut', +'stubbed', +'stubble', +'stubbly', +'stubborn', +'stucco', +'stuck', +'student', +'studied', +'studio', +'study', +'stuffed', +'stuffing', +'stuffy', +'stumble', +'stumbling', +'stump', +'stung', +'stunned', +'stunner', +'stunning', +'stunt', +'stupor', +'sturdily', +'sturdy', +'styling', +'stylishly', +'stylist', +'stylized', +'stylus', +'suave', +'subarctic', +'subatomic', +'subdivide', +'subdued', +'subduing', +'subfloor', +'subgroup', +'subheader', +'subject', +'sublease', +'sublet', +'sublevel', +'sublime', +'submarine', +'submerge', +'submersed', +'submitter', +'subpanel', +'subpar', +'subplot', +'subprime', +'subscribe', +'subscript', +'subsector', +'subside', +'subsiding', +'subsidize', +'subsidy', +'subsoil', +'subsonic', +'substance', +'subsystem', +'subtext', +'subtitle', +'subtly', +'subtotal', +'subtract', +'subtype', +'suburb', +'subway', +'subwoofer', +'subzero', +'succulent', +'such', +'suction', +'sudden', +'sudoku', +'suds', +'sufferer', +'suffering', +'suffice', +'suffix', +'suffocate', +'suffrage', +'sugar', +'suggest', +'suing', +'suitable', +'suitably', +'suitcase', +'suitor', +'sulfate', +'sulfide', +'sulfite', +'sulfur', +'sulk', +'sullen', +'sulphate', +'sulphuric', +'sultry', +'superbowl', +'superglue', +'superhero', +'superior', +'superjet', +'superman', +'supermom', +'supernova', +'supervise', +'supper', +'supplier', +'supply', +'support', +'supremacy', +'supreme', +'surcharge', +'surely', +'sureness', +'surface', +'surfacing', +'surfboard', +'surfer', +'surgery', +'surgical', +'surging', +'surname', +'surpass', +'surplus', +'surprise', +'surreal', +'surrender', +'surrogate', +'surround', +'survey', +'survival', +'survive', +'surviving', +'survivor', +'sushi', +'suspect', +'suspend', +'suspense', +'sustained', +'sustainer', +'swab', +'swaddling', +'swagger', +'swampland', +'swan', +'swapping', +'swarm', +'sway', +'swear', +'sweat', +'sweep', +'swell', +'swept', +'swerve', +'swifter', +'swiftly', +'swiftness', +'swimmable', +'swimmer', +'swimming', +'swimsuit', +'swimwear', +'swinger', +'swinging', +'swipe', +'swirl', +'switch', +'swivel', +'swizzle', +'swooned', +'swoop', +'swoosh', +'swore', +'sworn', +'swung', +'sycamore', +'sympathy', +'symphonic', +'symphony', +'symptom', +'synapse', +'syndrome', +'synergy', +'synopses', +'synopsis', +'synthesis', +'synthetic', +'syrup', +'system', +'t-shirt', +'tabasco', +'tabby', +'tableful', +'tables', +'tablet', +'tableware', +'tabloid', +'tackiness', +'tacking', +'tackle', +'tackling', +'tacky', +'taco', +'tactful', +'tactical', +'tactics', +'tactile', +'tactless', +'tadpole', +'taekwondo', +'tag', +'tainted', +'take', +'taking', +'talcum', +'talisman', +'tall', +'talon', +'tamale', +'tameness', +'tamer', +'tamper', +'tank', +'tanned', +'tannery', +'tanning', +'tantrum', +'tapeless', +'tapered', +'tapering', +'tapestry', +'tapioca', +'tapping', +'taps', +'tarantula', +'target', +'tarmac', +'tarnish', +'tarot', +'tartar', +'tartly', +'tartness', +'task', +'tassel', +'taste', +'tastiness', +'tasting', +'tasty', +'tattered', +'tattle', +'tattling', +'tattoo', +'taunt', +'tavern', +'thank', +'that', +'thaw', +'theater', +'theatrics', +'thee', +'theft', +'theme', +'theology', +'theorize', +'thermal', +'thermos', +'thesaurus', +'these', +'thesis', +'thespian', +'thicken', +'thicket', +'thickness', +'thieving', +'thievish', +'thigh', +'thimble', +'thing', +'think', +'thinly', +'thinner', +'thinness', +'thinning', +'thirstily', +'thirsting', +'thirsty', +'thirteen', +'thirty', +'thong', +'thorn', +'those', +'thousand', +'thrash', +'thread', +'threaten', +'threefold', +'thrift', +'thrill', +'thrive', +'thriving', +'throat', +'throbbing', +'throng', +'throttle', +'throwaway', +'throwback', +'thrower', +'throwing', +'thud', +'thumb', +'thumping', +'thursday', +'thus', +'thwarting', +'thyself', +'tiara', +'tibia', +'tidal', +'tidbit', +'tidiness', +'tidings', +'tidy', +'tiger', +'tighten', +'tightly', +'tightness', +'tightrope', +'tightwad', +'tigress', +'tile', +'tiling', +'till', +'tilt', +'timid', +'timing', +'timothy', +'tinderbox', +'tinfoil', +'tingle', +'tingling', +'tingly', +'tinker', +'tinkling', +'tinsel', +'tinsmith', +'tint', +'tinwork', +'tiny', +'tipoff', +'tipped', +'tipper', +'tipping', +'tiptoeing', +'tiptop', +'tiring', +'tissue', +'trace', +'tracing', +'track', +'traction', +'tractor', +'trade', +'trading', +'tradition', +'traffic', +'tragedy', +'trailing', +'trailside', +'train', +'traitor', +'trance', +'tranquil', +'transfer', +'transform', +'translate', +'transpire', +'transport', +'transpose', +'trapdoor', +'trapeze', +'trapezoid', +'trapped', +'trapper', +'trapping', +'traps', +'trash', +'travel', +'traverse', +'travesty', +'tray', +'treachery', +'treading', +'treadmill', +'treason', +'treat', +'treble', +'tree', +'trekker', +'tremble', +'trembling', +'tremor', +'trench', +'trend', +'trespass', +'triage', +'trial', +'triangle', +'tribesman', +'tribunal', +'tribune', +'tributary', +'tribute', +'triceps', +'trickery', +'trickily', +'tricking', +'trickle', +'trickster', +'tricky', +'tricolor', +'tricycle', +'trident', +'tried', +'trifle', +'trifocals', +'trillion', +'trilogy', +'trimester', +'trimmer', +'trimming', +'trimness', +'trinity', +'trio', +'tripod', +'tripping', +'triumph', +'trivial', +'trodden', +'trolling', +'trombone', +'trophy', +'tropical', +'tropics', +'trouble', +'troubling', +'trough', +'trousers', +'trout', +'trowel', +'truce', +'truck', +'truffle', +'trump', +'trunks', +'trustable', +'trustee', +'trustful', +'trusting', +'trustless', +'truth', +'try', +'tubby', +'tubeless', +'tubular', +'tucking', +'tuesday', +'tug', +'tuition', +'tulip', +'tumble', +'tumbling', +'tummy', +'turban', +'turbine', +'turbofan', +'turbojet', +'turbulent', +'turf', +'turkey', +'turmoil', +'turret', +'turtle', +'tusk', +'tutor', +'tutu', +'tux', +'tweak', +'tweed', +'tweet', +'tweezers', +'twelve', +'twentieth', +'twenty', +'twerp', +'twice', +'twiddle', +'twiddling', +'twig', +'twilight', +'twine', +'twins', +'twirl', +'twistable', +'twisted', +'twister', +'twisting', +'twisty', +'twitch', +'twitter', +'tycoon', +'tying', +'tyke', +'udder', +'ultimate', +'ultimatum', +'ultra', +'umbilical', +'umbrella', +'umpire', +'unabashed', +'unable', +'unadorned', +'unadvised', +'unafraid', +'unaired', +'unaligned', +'unaltered', +'unarmored', +'unashamed', +'unaudited', +'unawake', +'unaware', +'unbaked', +'unbalance', +'unbeaten', +'unbend', +'unbent', +'unbiased', +'unbitten', +'unblended', +'unblessed', +'unblock', +'unbolted', +'unbounded', +'unboxed', +'unbraided', +'unbridle', +'unbroken', +'unbuckled', +'unbundle', +'unburned', +'unbutton', +'uncanny', +'uncapped', +'uncaring', +'uncertain', +'unchain', +'unchanged', +'uncharted', +'uncheck', +'uncivil', +'unclad', +'unclaimed', +'unclamped', +'unclasp', +'uncle', +'unclip', +'uncloak', +'unclog', +'unclothed', +'uncoated', +'uncoiled', +'uncolored', +'uncombed', +'uncommon', +'uncooked', +'uncork', +'uncorrupt', +'uncounted', +'uncouple', +'uncouth', +'uncover', +'uncross', +'uncrown', +'uncrushed', +'uncured', +'uncurious', +'uncurled', +'uncut', +'undamaged', +'undated', +'undaunted', +'undead', +'undecided', +'undefined', +'underage', +'underarm', +'undercoat', +'undercook', +'undercut', +'underdog', +'underdone', +'underfed', +'underfeed', +'underfoot', +'undergo', +'undergrad', +'underhand', +'underline', +'underling', +'undermine', +'undermost', +'underpaid', +'underpass', +'underpay', +'underrate', +'undertake', +'undertone', +'undertook', +'undertow', +'underuse', +'underwear', +'underwent', +'underwire', +'undesired', +'undiluted', +'undivided', +'undocked', +'undoing', +'undone', +'undrafted', +'undress', +'undrilled', +'undusted', +'undying', +'unearned', +'unearth', +'unease', +'uneasily', +'uneasy', +'uneatable', +'uneaten', +'unedited', +'unelected', +'unending', +'unengaged', +'unenvied', +'unequal', +'unethical', +'uneven', +'unexpired', +'unexposed', +'unfailing', +'unfair', +'unfasten', +'unfazed', +'unfeeling', +'unfiled', +'unfilled', +'unfitted', +'unfitting', +'unfixable', +'unfixed', +'unflawed', +'unfocused', +'unfold', +'unfounded', +'unframed', +'unfreeze', +'unfrosted', +'unfrozen', +'unfunded', +'unglazed', +'ungloved', +'unglue', +'ungodly', +'ungraded', +'ungreased', +'unguarded', +'unguided', +'unhappily', +'unhappy', +'unharmed', +'unhealthy', +'unheard', +'unhearing', +'unheated', +'unhelpful', +'unhidden', +'unhinge', +'unhitched', +'unholy', +'unhook', +'unicorn', +'unicycle', +'unified', +'unifier', +'uniformed', +'uniformly', +'unify', +'unimpeded', +'uninjured', +'uninstall', +'uninsured', +'uninvited', +'union', +'uniquely', +'unisexual', +'unison', +'unissued', +'unit', +'universal', +'universe', +'unjustly', +'unkempt', +'unkind', +'unknotted', +'unknowing', +'unknown', +'unlaced', +'unlatch', +'unlawful', +'unleaded', +'unlearned', +'unleash', +'unless', +'unleveled', +'unlighted', +'unlikable', +'unlimited', +'unlined', +'unlinked', +'unlisted', +'unlit', +'unlivable', +'unloaded', +'unloader', +'unlocked', +'unlocking', +'unlovable', +'unloved', +'unlovely', +'unloving', +'unluckily', +'unlucky', +'unmade', +'unmanaged', +'unmanned', +'unmapped', +'unmarked', +'unmasked', +'unmasking', +'unmatched', +'unmindful', +'unmixable', +'unmixed', +'unmolded', +'unmoral', +'unmovable', +'unmoved', +'unmoving', +'unnamable', +'unnamed', +'unnatural', +'unneeded', +'unnerve', +'unnerving', +'unnoticed', +'unopened', +'unopposed', +'unpack', +'unpadded', +'unpaid', +'unpainted', +'unpaired', +'unpaved', +'unpeeled', +'unpicked', +'unpiloted', +'unpinned', +'unplanned', +'unplanted', +'unpleased', +'unpledged', +'unplowed', +'unplug', +'unpopular', +'unproven', +'unquote', +'unranked', +'unrated', +'unraveled', +'unreached', +'unread', +'unreal', +'unreeling', +'unrefined', +'unrelated', +'unrented', +'unrest', +'unretired', +'unrevised', +'unrigged', +'unripe', +'unrivaled', +'unroasted', +'unrobed', +'unroll', +'unruffled', +'unruly', +'unrushed', +'unsaddle', +'unsafe', +'unsaid', +'unsalted', +'unsaved', +'unsavory', +'unscathed', +'unscented', +'unscrew', +'unsealed', +'unseated', +'unsecured', +'unseeing', +'unseemly', +'unseen', +'unselect', +'unselfish', +'unsent', +'unsettled', +'unshackle', +'unshaken', +'unshaved', +'unshaven', +'unsheathe', +'unshipped', +'unsightly', +'unsigned', +'unskilled', +'unsliced', +'unsmooth', +'unsnap', +'unsocial', +'unsoiled', +'unsold', +'unsolved', +'unsorted', +'unspoiled', +'unspoken', +'unstable', +'unstaffed', +'unstamped', +'unsteady', +'unsterile', +'unstirred', +'unstitch', +'unstopped', +'unstuck', +'unstuffed', +'unstylish', +'unsubtle', +'unsubtly', +'unsuited', +'unsure', +'unsworn', +'untagged', +'untainted', +'untaken', +'untamed', +'untangled', +'untapped', +'untaxed', +'unthawed', +'unthread', +'untidy', +'untie', +'until', +'untimed', +'untimely', +'untitled', +'untoasted', +'untold', +'untouched', +'untracked', +'untrained', +'untreated', +'untried', +'untrimmed', +'untrue', +'untruth', +'unturned', +'untwist', +'untying', +'unusable', +'unused', +'unusual', +'unvalued', +'unvaried', +'unvarying', +'unveiled', +'unveiling', +'unvented', +'unviable', +'unvisited', +'unvocal', +'unwanted', +'unwarlike', +'unwary', +'unwashed', +'unwatched', +'unweave', +'unwed', +'unwelcome', +'unwell', +'unwieldy', +'unwilling', +'unwind', +'unwired', +'unwitting', +'unwomanly', +'unworldly', +'unworn', +'unworried', +'unworthy', +'unwound', +'unwoven', +'unwrapped', +'unwritten', +'unzip', +'upbeat', +'upchuck', +'upcoming', +'upcountry', +'update', +'upfront', +'upgrade', +'upheaval', +'upheld', +'uphill', +'uphold', +'uplifted', +'uplifting', +'upload', +'upon', +'upper', +'upright', +'uprising', +'upriver', +'uproar', +'uproot', +'upscale', +'upside', +'upstage', +'upstairs', +'upstart', +'upstate', +'upstream', +'upstroke', +'upswing', +'uptake', +'uptight', +'uptown', +'upturned', +'upward', +'upwind', +'uranium', +'urban', +'urchin', +'urethane', +'urgency', +'urgent', +'urging', +'urologist', +'urology', +'usable', +'usage', +'useable', +'used', +'uselessly', +'user', +'usher', +'usual', +'utensil', +'utility', +'utilize', +'utmost', +'utopia', +'utter', +'vacancy', +'vacant', +'vacate', +'vacation', +'vagabond', +'vagrancy', +'vagrantly', +'vaguely', +'vagueness', +'valiant', +'valid', +'valium', +'valley', +'valuables', +'value', +'vanilla', +'vanish', +'vanity', +'vanquish', +'vantage', +'vaporizer', +'variable', +'variably', +'varied', +'variety', +'various', +'varmint', +'varnish', +'varsity', +'varying', +'vascular', +'vaseline', +'vastly', +'vastness', +'veal', +'vegan', +'veggie', +'vehicular', +'velcro', +'velocity', +'velvet', +'vendetta', +'vending', +'vendor', +'veneering', +'vengeful', +'venomous', +'ventricle', +'venture', +'venue', +'venus', +'verbalize', +'verbally', +'verbose', +'verdict', +'verify', +'verse', +'version', +'versus', +'vertebrae', +'vertical', +'vertigo', +'very', +'vessel', +'vest', +'veteran', +'veto', +'vexingly', +'viability', +'viable', +'vibes', +'vice', +'vicinity', +'victory', +'video', +'viewable', +'viewer', +'viewing', +'viewless', +'viewpoint', +'vigorous', +'village', +'villain', +'vindicate', +'vineyard', +'vintage', +'violate', +'violation', +'violator', +'violet', +'violin', +'viper', +'viral', +'virtual', +'virtuous', +'virus', +'visa', +'viscosity', +'viscous', +'viselike', +'visible', +'visibly', +'vision', +'visiting', +'visitor', +'visor', +'vista', +'vitality', +'vitalize', +'vitally', +'vitamins', +'vivacious', +'vividly', +'vividness', +'vixen', +'vocalist', +'vocalize', +'vocally', +'vocation', +'voice', +'voicing', +'void', +'volatile', +'volley', +'voltage', +'volumes', +'voter', +'voting', +'voucher', +'vowed', +'vowel', +'voyage', +'wackiness', +'wad', +'wafer', +'waffle', +'waged', +'wager', +'wages', +'waggle', +'wagon', +'wake', +'waking', +'walk', +'walmart', +'walnut', +'walrus', +'waltz', +'wand', +'wannabe', +'wanted', +'wanting', +'wasabi', +'washable', +'washbasin', +'washboard', +'washbowl', +'washcloth', +'washday', +'washed', +'washer', +'washhouse', +'washing', +'washout', +'washroom', +'washstand', +'washtub', +'wasp', +'wasting', +'watch', +'water', +'waviness', +'waving', +'wavy', +'whacking', +'whacky', +'wham', +'wharf', +'wheat', +'whenever', +'whiff', +'whimsical', +'whinny', +'whiny', +'whisking', +'whoever', +'whole', +'whomever', +'whoopee', +'whooping', +'whoops', +'why', +'wick', +'widely', +'widen', +'widget', +'widow', +'width', +'wieldable', +'wielder', +'wife', +'wifi', +'wikipedia', +'wildcard', +'wildcat', +'wilder', +'wildfire', +'wildfowl', +'wildland', +'wildlife', +'wildly', +'wildness', +'willed', +'willfully', +'willing', +'willow', +'willpower', +'wilt', +'wimp', +'wince', +'wincing', +'wind', +'wing', +'winking', +'winner', +'winnings', +'winter', +'wipe', +'wired', +'wireless', +'wiring', +'wiry', +'wisdom', +'wise', +'wish', +'wisplike', +'wispy', +'wistful', +'wizard', +'wobble', +'wobbling', +'wobbly', +'wok', +'wolf', +'wolverine', +'womanhood', +'womankind', +'womanless', +'womanlike', +'womanly', +'womb', +'woof', +'wooing', +'wool', +'woozy', +'word', +'work', +'worried', +'worrier', +'worrisome', +'worry', +'worsening', +'worshiper', +'worst', +'wound', +'woven', +'wow', +'wrangle', +'wrath', +'wreath', +'wreckage', +'wrecker', +'wrecking', +'wrench', +'wriggle', +'wriggly', +'wrinkle', +'wrinkly', +'wrist', +'writing', +'written', +'wrongdoer', +'wronged', +'wrongful', +'wrongly', +'wrongness', +'wrought', +'xbox', +'xerox', +'yahoo', +'yam', +'yanking', +'yapping', +'yard', +'yarn', +'yeah', +'yearbook', +'yearling', +'yearly', +'yearning', +'yeast', +'yelling', +'yelp', +'yen', +'yesterday', +'yiddish', +'yield', +'yin', +'yippee', +'yo-yo', +'yodel', +'yoga', +'yogurt', +'yonder', +'yoyo', +'yummy', +'zap', +'zealous', +'zebra', +'zen', +'zeppelin', +'zero', +'zestfully', +'zesty', +'zigzagged', +'zipfile', +'zipping', +'zippy', +'zips', +'zit', +'zodiac', +'zombie', +'zone', +'zoning', +'zookeeper', +'zoologist', +'zoology', +'zoom' +] diff --git a/src/receiver/commands.py b/src/receiver/commands.py index f2ca680..4e0c00a 100644 --- a/src/receiver/commands.py +++ b/src/receiver/commands.py @@ -29,7 +29,14 @@ from src.common.encoding import bytes_to_int, pub_key_to_short_address from src.common.exceptions import FunctionReturn from src.common.misc import ensure_dir, separate_header from src.common.output import clear_screen, m_print, phase, print_on_previous_line -from src.common.statics import * +from src.common.statics import (CH_FILE_RECV, CH_LOGGING, CH_MASTER_KEY, CH_NICKNAME, CH_NOTIFY, CH_SETTING, + CLEAR_SCREEN, COMMAND, CONTACT_REM, CONTACT_SETTING_HEADER_LENGTH, DIR_USER_DATA, + DISABLE, DONE, ENABLE, ENCODED_INTEGER_LENGTH, ENCRYPTED_COMMAND_HEADER_LENGTH, EXIT, + EXIT_PROGRAM, GROUP_ADD, GROUP_CREATE, GROUP_DELETE, GROUP_REMOVE, GROUP_RENAME, + KEY_EX_ECDHE, KEY_EX_PSK_RX, KEY_EX_PSK_TX, LOCAL_KEY_RDY, LOCAL_PUBKEY, LOG_DISPLAY, + LOG_EXPORT, LOG_REMOVE, ONION_SERVICE_PUBLIC_KEY_LENGTH, ORIGIN_USER_HEADER, RESET, + RESET_SCREEN, US_BYTE, WIN_ACTIVITY, WIN_SELECT, WIN_TYPE_CONTACT, WIN_TYPE_GROUP, + WIN_UID_FILE, WIN_UID_LOCAL, WIPE, WIPE_USR_DATA) from src.receiver.commands_g import group_add, group_create, group_delete, group_remove, group_rename from src.receiver.key_exchanges import key_ex_ecdhe, key_ex_psk_rx, key_ex_psk_tx, local_key_rdy @@ -58,7 +65,7 @@ def process_command(ts: 'datetime', settings: 'Settings', master_key: 'MasterKey', gateway: 'Gateway', - exit_queue: 'Queue[Any]' + exit_queue: 'Queue[bytes]' ) -> None: """Decrypt command assembly packet and process command.""" assembly_packet = decrypt_assembly_packet(assembly_ct, LOCAL_PUBKEY, ORIGIN_USER_HEADER, @@ -73,7 +80,7 @@ def process_command(ts: 'datetime', header, cmd = separate_header(cmd_packet.assemble_command_packet(), ENCRYPTED_COMMAND_HEADER_LENGTH) no = None - # Keyword Function to run ( Parameters ) + # Keyword Function to run ( Parameters ) # -------------------------------------------------------------------------------------------------------------- d = {LOCAL_KEY_RDY: (local_key_rdy, ts, window_list, contact_list ), WIN_ACTIVITY: (win_activity, window_list ), @@ -136,7 +143,7 @@ def reset_screen(win_uid: bytes, window_list: 'WindowList') -> None: os.system(RESET) -def exit_tfc(exit_queue: 'Queue[Any]') -> None: +def exit_tfc(exit_queue: 'Queue[str]') -> None: """Exit TFC.""" exit_queue.put(EXIT) diff --git a/src/receiver/commands_g.py b/src/receiver/commands_g.py index 84eefd7..5abcd4b 100644 --- a/src/receiver/commands_g.py +++ b/src/receiver/commands_g.py @@ -25,7 +25,9 @@ from src.common.encoding import b58encode from src.common.exceptions import FunctionReturn from src.common.misc import separate_header, split_byte_string, validate_group_name from src.common.output import group_management_print, m_print -from src.common.statics import * +from src.common.statics import (ADDED_MEMBERS, ALREADY_MEMBER, GROUP_ID_LENGTH, NEW_GROUP, NOT_IN_GROUP, + ONION_SERVICE_PUBLIC_KEY_LENGTH, REMOVED_MEMBERS, UNKNOWN_ACCOUNTS, US_BYTE, + WIN_UID_LOCAL) if typing.TYPE_CHECKING: from datetime import datetime diff --git a/src/receiver/files.py b/src/receiver/files.py index 75508f6..a044065 100644 --- a/src/receiver/files.py +++ b/src/receiver/files.py @@ -32,7 +32,8 @@ from src.common.encoding import bytes_to_str from src.common.exceptions import FunctionReturn from src.common.misc import decompress, ensure_dir, separate_headers, separate_trailer from src.common.output import phase, print_on_previous_line -from src.common.statics import * +from src.common.statics import (DIR_RECV_FILES, DONE, ONION_SERVICE_PUBLIC_KEY_LENGTH, ORIGIN_HEADER_LENGTH, + PADDED_UTF32_STR_LENGTH, SYMMETRIC_KEY_LENGTH, US_BYTE) if typing.TYPE_CHECKING: from datetime import datetime @@ -171,10 +172,9 @@ def process_file(ts: 'datetime', # Timestamp of received_packet if not file_name.isprintable() or not file_name or '/' in file_name: raise FunctionReturn(f"Error: Name of file from {nick} was invalid.") - f_data = file_dc[PADDED_UTF32_STR_LENGTH:] - + file_data = file_dc[PADDED_UTF32_STR_LENGTH:] file_dir = f'{DIR_RECV_FILES}{nick}/' - final_name = store_unique(f_data, file_dir, file_name) + final_name = store_unique(file_data, file_dir, file_name) message = f"Stored file from {nick} as '{final_name}'." if settings.traffic_masking and window_list.active_win is not None: diff --git a/src/receiver/key_exchanges.py b/src/receiver/key_exchanges.py index 11e64d6..84005ac 100644 --- a/src/receiver/key_exchanges.py +++ b/src/receiver/key_exchanges.py @@ -27,7 +27,7 @@ import subprocess import tkinter import typing -from typing import Any, List, Tuple +from typing import List, Tuple import nacl.exceptions @@ -39,7 +39,11 @@ from src.common.input import get_b58_key from src.common.misc import separate_header, separate_headers from src.common.output import m_print, phase, print_on_previous_line from src.common.path import ask_path_gui -from src.common.statics import * +from src.common.statics import (ARGON2_PSK_MEMORY_COST, ARGON2_PSK_PARALLELISM, ARGON2_PSK_TIME_COST, + ARGON2_SALT_LENGTH, B58_LOCAL_KEY, CONFIRM_CODE_LENGTH, DONE, FINGERPRINT_LENGTH, + KEX_STATUS_HAS_RX_PSK, KEX_STATUS_LOCAL_KEY, KEX_STATUS_NONE, KEX_STATUS_NO_RX_PSK, + LOCAL_NICK, LOCAL_PUBKEY, ONION_SERVICE_PUBLIC_KEY_LENGTH, PSK_FILE_SIZE, RESET, + SYMMETRIC_KEY_LENGTH, WIN_TYPE_CONTACT, WIN_TYPE_GROUP) if typing.TYPE_CHECKING: from datetime import datetime @@ -60,7 +64,7 @@ def process_local_key(ts: 'datetime', settings: 'Settings', kdk_hashes: List[bytes], packet_hashes: List[bytes], - l_queue: 'Queue[Any]' + l_queue: 'Queue[Tuple[datetime, bytes]]' ) -> None: """Decrypt local key packet and add local contact/keyset.""" bootstrap = not key_list.has_local_keyset() @@ -259,11 +263,12 @@ def key_ex_psk_tx(packet: bytes, tx_hk=tx_hk, rx_hk=bytes(SYMMETRIC_KEY_LENGTH)) + c_code = blake2b(onion_pub_key, digest_size=CONFIRM_CODE_LENGTH) message = f"Added Tx-side PSK for {nick} ({pub_key_to_short_address(onion_pub_key)})." local_win = window_list.get_local_window() local_win.add_new(ts, message) - m_print(message, bold=True, tail_clear=True, delay=1) + m_print([message, f"Confirmation code (to Transmitter): {c_code.hex()}"], box=True) def key_ex_psk_rx(packet: bytes, diff --git a/src/receiver/messages.py b/src/receiver/messages.py index af4952c..1b03079 100644 --- a/src/receiver/messages.py +++ b/src/receiver/messages.py @@ -28,7 +28,11 @@ from src.common.db_logs import write_log_entry from src.common.encoding import bytes_to_bool from src.common.exceptions import FunctionReturn from src.common.misc import separate_header, separate_headers -from src.common.statics import * +from src.common.statics import (ASSEMBLY_PACKET_HEADER_LENGTH, BLAKE2_DIGEST_LENGTH, FILE, FILE_KEY_HEADER, + GROUP_ID_LENGTH, GROUP_MESSAGE_HEADER, GROUP_MSG_ID_LENGTH, LOCAL_PUBKEY, MESSAGE, + MESSAGE_HEADER_LENGTH, ONION_SERVICE_PUBLIC_KEY_LENGTH, ORIGIN_CONTACT_HEADER, + ORIGIN_HEADER_LENGTH, ORIGIN_USER_HEADER, PLACEHOLDER_DATA, PRIVATE_MESSAGE_HEADER, + SYMMETRIC_KEY_LENGTH, WHISPER_FIELD_LENGTH) from src.receiver.packet import decrypt_assembly_packet @@ -57,8 +61,8 @@ def process_message(ts: 'datetime', """Process received private / group message.""" local_window = window_list.get_local_window() - onion_pub_key, origin, assembly_packet_ct \ - = separate_headers(assembly_packet_ct, [ONION_SERVICE_PUBLIC_KEY_LENGTH, ORIGIN_HEADER_LENGTH]) + onion_pub_key, origin, assembly_packet_ct = separate_headers(assembly_packet_ct, [ONION_SERVICE_PUBLIC_KEY_LENGTH, + ORIGIN_HEADER_LENGTH]) if onion_pub_key == LOCAL_PUBKEY: raise FunctionReturn("Warning! Received packet masqueraded as a command.", window=local_window) @@ -123,6 +127,7 @@ def process_message(ts: 'datetime', else: raise FunctionReturn("Error: Message from contact had an invalid header.") + # Logging if whisper: raise FunctionReturn("Whisper message complete.", output=False) diff --git a/src/receiver/output_loop.py b/src/receiver/output_loop.py index c290662..bdb837f 100755 --- a/src/receiver/output_loop.py +++ b/src/receiver/output_loop.py @@ -28,7 +28,9 @@ from typing import Any, Dict, List, Tuple from src.common.exceptions import FunctionReturn from src.common.output import clear_screen -from src.common.statics import * +from src.common.statics import (COMMAND_DATAGRAM_HEADER, EXIT_QUEUE, FILE_DATAGRAM_HEADER, LOCAL_KEY_DATAGRAM_HEADER, + MESSAGE_DATAGRAM_HEADER, ONION_SERVICE_PUBLIC_KEY_LENGTH, UNIT_TEST_QUEUE, + WIN_UID_FILE) from src.receiver.commands import process_command from src.receiver.files import new_file, process_file @@ -59,16 +61,16 @@ def output_loop(queues: Dict[bytes, 'Queue[Any]'], unit_test: bool = False ) -> None: """Process packets in message queues according to their priority.""" - l_queue = queues[LOCAL_KEY_DATAGRAM_HEADER] - m_queue = queues[MESSAGE_DATAGRAM_HEADER] - f_queue = queues[FILE_DATAGRAM_HEADER] - c_queue = queues[COMMAND_DATAGRAM_HEADER] - e_queue = queues[EXIT_QUEUE] + local_key_queue = queues[LOCAL_KEY_DATAGRAM_HEADER] + message_queue = queues[MESSAGE_DATAGRAM_HEADER] + file_queue = queues[FILE_DATAGRAM_HEADER] + command_queue = queues[COMMAND_DATAGRAM_HEADER] + exit_queue = queues[EXIT_QUEUE] - sys.stdin = os.fdopen(stdin_fd) - packet_buf = dict() # type: Dict[bytes, List[Tuple[datetime, bytes]]] - file_buf = dict() # type: Dict[bytes, Tuple[datetime, bytes]] - file_keys = dict() # type: Dict[bytes, bytes] + sys.stdin = os.fdopen(stdin_fd) + packet_buffer = dict() # type: Dict[bytes, List[Tuple[datetime, bytes]]] + file_buffer = dict() # type: Dict[bytes, Tuple[datetime, bytes]] + file_keys = dict() # type: Dict[bytes, bytes] kdk_hashes = [] # type: List[bytes] packet_hashes = [] # type: List[bytes] @@ -79,10 +81,10 @@ def output_loop(queues: Dict[bytes, 'Queue[Any]'], clear_screen() while True: try: - if l_queue.qsize() != 0: - ts, packet = l_queue.get() + if local_key_queue.qsize() != 0: + ts, packet = local_key_queue.get() process_local_key(ts, packet, window_list, contact_list, key_list, - settings, kdk_hashes, packet_hashes, l_queue) + settings, kdk_hashes, packet_hashes, local_key_queue) continue if not contact_list.has_local_contact(): @@ -90,10 +92,10 @@ def output_loop(queues: Dict[bytes, 'Queue[Any]'], continue # Commands - if c_queue.qsize() != 0: - ts, packet = c_queue.get() + if command_queue.qsize() != 0: + ts, packet = command_queue.get() process_command(ts, packet, window_list, packet_list, contact_list, key_list, - group_list, settings, master_key, gateway, e_queue) + group_list, settings, master_key, gateway, exit_queue) continue # File window refresh @@ -101,48 +103,48 @@ def output_loop(queues: Dict[bytes, 'Queue[Any]'], window_list.active_win.redraw_file_win() # Cached message packets - for onion_pub_key in packet_buf: + for onion_pub_key in packet_buffer: if (contact_list.has_pub_key(onion_pub_key) and key_list.has_rx_mk(onion_pub_key) - and packet_buf[onion_pub_key]): - ts, packet = packet_buf[onion_pub_key].pop(0) + and packet_buffer[onion_pub_key]): + ts, packet = packet_buffer[onion_pub_key].pop(0) process_message(ts, packet, window_list, packet_list, contact_list, key_list, group_list, settings, master_key, file_keys) continue # New messages - if m_queue.qsize() != 0: - ts, packet = m_queue.get() + if message_queue.qsize() != 0: + ts, packet = message_queue.get() onion_pub_key = packet[:ONION_SERVICE_PUBLIC_KEY_LENGTH] if contact_list.has_pub_key(onion_pub_key) and key_list.has_rx_mk(onion_pub_key): process_message(ts, packet, window_list, packet_list, contact_list, key_list, group_list, settings, master_key, file_keys) else: - packet_buf.setdefault(onion_pub_key, []).append((ts, packet)) + packet_buffer.setdefault(onion_pub_key, []).append((ts, packet)) continue # Cached files - if file_buf: - for k in file_buf: + if file_buffer: + for k in file_buffer: key_to_remove = b'' try: if k in file_keys: key_to_remove = k - ts_, file_ct = file_buf[k] + ts_, file_ct = file_buffer[k] dec_key = file_keys[k] onion_pub_key = k[:ONION_SERVICE_PUBLIC_KEY_LENGTH] process_file(ts_, onion_pub_key, file_ct, dec_key, contact_list, window_list, settings) finally: if key_to_remove: - file_buf.pop(k) + file_buffer.pop(k) file_keys.pop(k) break # New files - if f_queue.qsize() != 0: - ts, packet = f_queue.get() - new_file(ts, packet, file_keys, file_buf, contact_list, window_list, settings) + if file_queue.qsize() != 0: + ts, packet = file_queue.get() + new_file(ts, packet, file_keys, file_buffer, contact_list, window_list, settings) time.sleep(0.01) diff --git a/src/receiver/packet.py b/src/receiver/packet.py index e648fb2..0e9c5ae 100644 --- a/src/receiver/packet.py +++ b/src/receiver/packet.py @@ -34,7 +34,12 @@ from src.common.exceptions import FunctionReturn from src.common.input import yes from src.common.misc import decompress, readable_size, separate_header, separate_headers, separate_trailer from src.common.output import m_print -from src.common.statics import * +from src.common.statics import (ASSEMBLY_PACKET_HEADER_LENGTH, BLAKE2_DIGEST_LENGTH, COMMAND, C_A_HEADER, C_C_HEADER, + C_E_HEADER, C_L_HEADER, C_N_HEADER, C_S_HEADER, ENCODED_INTEGER_LENGTH, FILE, + F_A_HEADER, F_C_HEADER, F_E_HEADER, F_L_HEADER, F_S_HEADER, HARAC_CT_LENGTH, + HARAC_WARN_THRESHOLD, LOCAL_PUBKEY, MAX_MESSAGE_SIZE, MESSAGE, M_A_HEADER, + M_C_HEADER, M_E_HEADER, M_L_HEADER, M_S_HEADER, ORIGIN_CONTACT_HEADER, + ORIGIN_USER_HEADER, P_N_HEADER, RX, SYMMETRIC_KEY_LENGTH, TX, US_BYTE) from src.receiver.files import process_assembled_file @@ -100,16 +105,16 @@ def decrypt_assembly_packet(packet: bytes, # Assembly packet cip try: harac_bytes = auth_and_decrypt(ct_harac, header_key) except nacl.exceptions.CryptoError: - raise FunctionReturn( - f"Warning! Received {p_type} {direction} {nick} had an invalid hash ratchet MAC.", window=local_window) + raise FunctionReturn(f"Warning! Received {p_type} {direction} {nick} had an invalid hash ratchet MAC.", + window=local_window) # Catch up with hash ratchet offset purp_harac = bytes_to_int(harac_bytes) stored_harac = getattr(keyset, f'{key_dir}_harac') offset = purp_harac - stored_harac if offset < 0: - raise FunctionReturn( - f"Warning! Received {p_type} {direction} {nick} had an expired hash ratchet counter.", window=local_window) + raise FunctionReturn(f"Warning! Received {p_type} {direction} {nick} had an expired hash ratchet counter.", + window=local_window) process_offset(offset, origin, direction, nick, local_window) for harac in range(stored_harac, stored_harac + offset): @@ -119,7 +124,8 @@ def decrypt_assembly_packet(packet: bytes, # Assembly packet cip try: assembly_packet = auth_and_decrypt(ct_assemby_packet, message_key) except nacl.exceptions.CryptoError: - raise FunctionReturn(f"Warning! Received {p_type} {direction} {nick} had an invalid MAC.", window=local_window) + raise FunctionReturn(f"Warning! Received {p_type} {direction} {nick} had an invalid MAC.", + window=local_window) # Update message key and harac new_key = blake2b(message_key + int_to_bytes(stored_harac + offset), digest_size=SYMMETRIC_KEY_LENGTH) diff --git a/src/receiver/receiver_loop.py b/src/receiver/receiver_loop.py index 265dfba..2f8f76c 100755 --- a/src/receiver/receiver_loop.py +++ b/src/receiver/receiver_loop.py @@ -30,7 +30,9 @@ from src.common.encoding import bytes_to_int from src.common.exceptions import FunctionReturn from src.common.misc import ignored, separate_headers from src.common.output import m_print -from src.common.statics import * +from src.common.statics import (COMMAND_DATAGRAM_HEADER, DATAGRAM_HEADER_LENGTH, DATAGRAM_TIMESTAMP_LENGTH, + FILE_DATAGRAM_HEADER, GATEWAY_QUEUE, LOCAL_KEY_DATAGRAM_HEADER, + MESSAGE_DATAGRAM_HEADER) if typing.TYPE_CHECKING: from multiprocessing import Queue diff --git a/src/receiver/windows.py b/src/receiver/windows.py index 03baae8..cb63f1f 100644 --- a/src/receiver/windows.py +++ b/src/receiver/windows.py @@ -27,11 +27,14 @@ import typing from datetime import datetime from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple -from src.common.encoding import pub_key_to_short_address +from src.common.encoding import b58encode, pub_key_to_onion_address, pub_key_to_short_address from src.common.exceptions import FunctionReturn from src.common.misc import get_terminal_width from src.common.output import clear_screen, m_print, print_on_previous_line -from src.common.statics import * +from src.common.statics import (BOLD_ON, EVENT, FILE, FILE_TRANSFER_INDENT, GROUP_ID_LENGTH, GROUP_MSG_ID_LENGTH, ME, + NORMAL_TEXT, ONION_SERVICE_PUBLIC_KEY_LENGTH, ORIGIN_CONTACT_HEADER, + ORIGIN_USER_HEADER, WIN_TYPE_COMMAND, WIN_TYPE_CONTACT, WIN_TYPE_FILE, + WIN_TYPE_GROUP, WIN_UID_FILE, WIN_UID_LOCAL) if typing.TYPE_CHECKING: from src.common.db_contacts import Contact, ContactList @@ -96,7 +99,14 @@ class RxWindow(Iterable[MsgTuple]): self.window_contacts = self.group.members else: - raise FunctionReturn(f"Invalid window '{uid}'.") + if len(uid) == ONION_SERVICE_PUBLIC_KEY_LENGTH: + hr_uid = pub_key_to_onion_address(uid) + elif len(uid) == GROUP_ID_LENGTH: + hr_uid = b58encode(uid) + else: + hr_uid = "" + + raise FunctionReturn(f"Invalid window '{hr_uid}'.") def __iter__(self) -> Iterator[MsgTuple]: """Iterate over window's message log.""" @@ -238,7 +248,6 @@ class RxWindow(Iterable[MsgTuple]): event_msg: bool = False # When True, uses "-!-" as message handle ) -> None: """Add message tuple to message log and optionally print it.""" - self.update_handle_dict(onion_pub_key) msg_tuple = (timestamp, message, onion_pub_key, origin, whisper, event_msg) diff --git a/src/relay/client.py b/src/relay/client.py index bf3de5b..de0998f 100644 --- a/src/relay/client.py +++ b/src/relay/client.py @@ -36,7 +36,15 @@ from src.common.encoding import b58encode, int_to_bytes, onion_address_to_pub_ke from src.common.encoding import pub_key_to_short_address from src.common.misc import ignored, separate_header, split_byte_string, validate_onion_addr from src.common.output import m_print, print_key, rp_print -from src.common.statics import * +from src.common.statics import (CLIENT_OFFLINE_THRESHOLD, CONTACT_MGMT_QUEUE, CONTACT_REQ_QUEUE, C_REQ_MGMT_QUEUE, + C_REQ_STATE_QUEUE, DATAGRAM_HEADER_LENGTH, DST_MESSAGE_QUEUE, FILE_DATAGRAM_HEADER, + GROUP_ID_LENGTH, GROUP_MGMT_QUEUE, GROUP_MSG_EXIT_GROUP_HEADER, + GROUP_MSG_INVITE_HEADER, GROUP_MSG_JOIN_HEADER, GROUP_MSG_MEMBER_ADD_HEADER, + GROUP_MSG_MEMBER_REM_HEADER, GROUP_MSG_QUEUE, MESSAGE_DATAGRAM_HEADER, + ONION_SERVICE_PUBLIC_KEY_LENGTH, ORIGIN_CONTACT_HEADER, PUBLIC_KEY_DATAGRAM_HEADER, + RELAY_CLIENT_MAX_DELAY, RELAY_CLIENT_MIN_DELAY, RP_ADD_CONTACT_HEADER, + RP_REMOVE_CONTACT_HEADER, TFC_PUBLIC_KEY_LENGTH, TOR_DATA_QUEUE, UNIT_TEST_QUEUE, + URL_TOKEN_LENGTH, URL_TOKEN_QUEUE) if typing.TYPE_CHECKING: from src.common.gateway import Gateway @@ -205,7 +213,7 @@ def get_data_loop(onion_addr: str, except requests.exceptions.RequestException: return None - for line in r.iter_lines(): # Iterates over newline-separated datagrams + for line in r.iter_lines(): # Iterate over newline-separated datagrams if not line: continue @@ -219,14 +227,16 @@ def get_data_loop(onion_addr: str, ts = datetime.now() ts_bytes = int_to_bytes(int(ts.strftime('%Y%m%d%H%M%S%f')[:-4])) + # Packet type specific handling + if header == PUBLIC_KEY_DATAGRAM_HEADER: if len(payload_bytes) == TFC_PUBLIC_KEY_LENGTH: msg = f"Received public key from {short_addr} at {ts.strftime('%b %d - %H:%M:%S.%f')[:-4]}:" print_key(msg, payload_bytes, gateway.settings, public_key=True) elif header == MESSAGE_DATAGRAM_HEADER: - queues[DST_MESSAGE_QUEUE].put(header + ts_bytes + onion_pub_key - + ORIGIN_CONTACT_HEADER + payload_bytes) + queues[DST_MESSAGE_QUEUE].put(header + ts_bytes + + onion_pub_key + ORIGIN_CONTACT_HEADER + payload_bytes) rp_print(f"Message from contact {short_addr}", ts) elif header in [GROUP_MSG_INVITE_HEADER, GROUP_MSG_JOIN_HEADER, @@ -282,13 +292,13 @@ def g_msg_manager(queues: 'QueueDict', pub_keys = split_byte_string(data, ONION_SERVICE_PUBLIC_KEY_LENGTH) pub_key_length = ONION_SERVICE_PUBLIC_KEY_LENGTH - members = [k for k in pub_keys if len(k) == pub_key_length ] - known = [f" * {pub_key_to_onion_address(m)}" for m in members if m in existing_contacts] - unknown = [f" * {pub_key_to_onion_address(m)}" for m in members if m not in existing_contacts] + members = [k for k in pub_keys if len(k) == pub_key_length ] + known = [f" * {pub_key_to_onion_address(m)}" for m in members if m in existing_contacts] + unknown = [f" * {pub_key_to_onion_address(m)}" for m in members if m not in existing_contacts] line_list = [] if known: - line_list.extend(["Known contacts"] + known) + line_list.extend(["Known contacts"] + known) if unknown: line_list.extend(["Unknown contacts"] + unknown) @@ -353,7 +363,8 @@ def c_req_manager(queues: 'QueueDict', if show_requests: ts_fmt = datetime.now().strftime('%b %d - %H:%M:%S.%f')[:-4] - m_print([f"{ts_fmt} - New contact request from an unknown TFC account:", purp_onion_address], box=True) + m_print([f"{ts_fmt} - New contact request from an unknown TFC account:", purp_onion_address], + box=True) contact_requests.append(onion_pub_key) if unit_test and queues[UNIT_TEST_QUEUE].qsize() != 0: diff --git a/src/relay/commands.py b/src/relay/commands.py index 158c46c..b0917b7 100644 --- a/src/relay/commands.py +++ b/src/relay/commands.py @@ -31,7 +31,16 @@ from src.common.encoding import bytes_to_bool, bytes_to_int from src.common.exceptions import FunctionReturn from src.common.misc import ignored, separate_header, separate_headers, split_byte_string from src.common.output import clear_screen, m_print -from src.common.statics import * +from src.common.statics import (CONFIRM_CODE_LENGTH, CONTACT_MGMT_QUEUE, C_REQ_MGMT_QUEUE, C_REQ_STATE_QUEUE, + ENCODED_BOOLEAN_LENGTH, ENCODED_INTEGER_LENGTH, EXIT, GROUP_MGMT_QUEUE, + LOCAL_TESTING_PACKET_DELAY, MAX_INT, ONION_CLOSE_QUEUE, ONION_KEY_QUEUE, + ONION_SERVICE_PRIVATE_KEY_LENGTH, ONION_SERVICE_PUBLIC_KEY_LENGTH, RESET, + RP_ADD_CONTACT_HEADER, RP_REMOVE_CONTACT_HEADER, SRC_TO_RELAY_QUEUE, + UNENCRYPTED_ADD_EXISTING_CONTACT, UNENCRYPTED_ADD_NEW_CONTACT, UNENCRYPTED_BAUDRATE, + UNENCRYPTED_COMMAND_HEADER_LENGTH, UNENCRYPTED_EC_RATIO, UNENCRYPTED_EXIT_COMMAND, + UNENCRYPTED_MANAGE_CONTACT_REQ, UNENCRYPTED_ONION_SERVICE_DATA, + UNENCRYPTED_REM_CONTACT, UNENCRYPTED_SCREEN_CLEAR, UNENCRYPTED_SCREEN_RESET, + UNENCRYPTED_WIPE_COMMAND, WIPE) if typing.TYPE_CHECKING: from multiprocessing import Queue diff --git a/src/relay/onion.py b/src/relay/onion.py index 46c76e6..3336d93 100644 --- a/src/relay/onion.py +++ b/src/relay/onion.py @@ -27,22 +27,28 @@ import shlex import socket import tempfile import time +import typing -from multiprocessing import Queue -from typing import Any, Dict +from typing import Any, Dict, Optional import nacl.signing import stem.control import stem.process +from stem.control import Controller + from src.common.encoding import pub_key_to_onion_address from src.common.exceptions import CriticalError from src.common.output import m_print, rp_print -from src.common.statics import * +from src.common.statics import (EXIT, EXIT_QUEUE, ONION_CLOSE_QUEUE, ONION_KEY_QUEUE, + ONION_SERVICE_PRIVATE_KEY_LENGTH, TOR_CONTROL_PORT, TOR_DATA_QUEUE, TOR_SOCKS_PORT) + +if typing.TYPE_CHECKING: + from multiprocessing import Queue -def get_available_port(min_port: int, max_port: int) -> str: +def get_available_port(min_port: int, max_port: int) -> int: """Find a random available port within the given range.""" with socket.socket() as temp_sock: while True: @@ -51,7 +57,11 @@ def get_available_port(min_port: int, max_port: int) -> str: break except OSError: pass - _, port = temp_sock.getsockname() # type: Any, str + _, port = temp_sock.getsockname() # type: Any, int + + if Tor.platform_is_tails(): + return TOR_SOCKS_PORT + return port @@ -59,11 +69,27 @@ class Tor(object): """Tor class manages the starting and stopping of Tor client.""" def __init__(self) -> None: - self.tor_process = None # type: Any - self.controller = None # type: Any + self.tor_process = None # type: Optional[Any] + self.controller = None # type: Optional[Controller] + + @staticmethod + def platform_is_tails() -> bool: + """Return True if Relay Program is running on Tails.""" + with open('/etc/os-release') as f: + data = f.read() + return 'TAILS_PRODUCT_NAME="Tails"' in data + + def connect(self, port: int) -> None: + """Launch Tor as a subprocess. + + If TFC is running on top of Tails, do not launch a separate + instance of Tor. + """ + if self.platform_is_tails(): + self.controller = Controller.from_port(port=TOR_CONTROL_PORT) + self.controller.authenticate() + return None - def connect(self, port: str) -> None: - """Launch Tor as a subprocess.""" tor_data_directory = tempfile.TemporaryDirectory() tor_control_socket = os.path.join(tor_data_directory.name, 'control_socket') @@ -113,7 +139,7 @@ class Tor(object): def stop(self) -> None: """Stop the Tor subprocess.""" - if self.tor_process: + if self.tor_process is not None: self.tor_process.terminate() time.sleep(0.1) if not self.tor_process.poll(): @@ -174,6 +200,9 @@ def onion_service(queues: Dict[bytes, 'Queue[Any]']) -> None: except (EOFError, KeyboardInterrupt): return + if tor.controller is None: + raise CriticalError("No Tor controller") + try: rp_print("Setup 75% - Launching Onion Service...", bold=True) key_data = stem_compatible_ed25519_key_from_private_key(private_key) @@ -206,9 +235,11 @@ def onion_service(queues: Dict[bytes, 'Queue[Any]']) -> None: if queues[ONION_CLOSE_QUEUE].qsize() > 0: command = queues[ONION_CLOSE_QUEUE].get() - tor.controller.remove_hidden_service(response.service_id) - tor.stop() + if not tor.platform_is_tails() and command == EXIT: + tor.controller.remove_hidden_service(response.service_id) + tor.stop() queues[EXIT_QUEUE].put(command) + time.sleep(5) break except (EOFError, KeyboardInterrupt): diff --git a/src/relay/server.py b/src/relay/server.py index 3cd07fe..c573fc3 100644 --- a/src/relay/server.py +++ b/src/relay/server.py @@ -31,7 +31,7 @@ from typing import Any, Dict, List, Optional from flask import Flask, send_file -from src.common.statics import * +from src.common.statics import CONTACT_REQ_QUEUE, F_TO_FLASK_QUEUE, M_TO_FLASK_QUEUE, URL_TOKEN_QUEUE if typing.TYPE_CHECKING: QueueDict = Dict[bytes, Queue[Any]] diff --git a/src/relay/tcb.py b/src/relay/tcb.py index 2f19104..f0ca31b 100644 --- a/src/relay/tcb.py +++ b/src/relay/tcb.py @@ -22,14 +22,20 @@ along with TFC. If not, see . import time import typing -from typing import Any, Dict, Union +from typing import Any, Dict, Tuple, Union from src.common.encoding import bytes_to_int, pub_key_to_short_address from src.common.encoding import int_to_bytes, b85encode from src.common.exceptions import FunctionReturn from src.common.misc import ignored, separate_header, split_byte_string from src.common.output import rp_print -from src.common.statics import * +from src.common.statics import (COMMAND_DATAGRAM_HEADER, DATAGRAM_HEADER_LENGTH, DST_COMMAND_QUEUE, + DST_MESSAGE_QUEUE, ENCODED_INTEGER_LENGTH, FILE_DATAGRAM_HEADER, F_TO_FLASK_QUEUE, + GATEWAY_QUEUE, GROUP_ID_LENGTH, GROUP_MSG_EXIT_GROUP_HEADER, GROUP_MSG_INVITE_HEADER, + GROUP_MSG_JOIN_HEADER, GROUP_MSG_MEMBER_ADD_HEADER, GROUP_MSG_MEMBER_REM_HEADER, + LOCAL_KEY_DATAGRAM_HEADER, MESSAGE_DATAGRAM_HEADER, M_TO_FLASK_QUEUE, + ONION_SERVICE_PUBLIC_KEY_LENGTH, ORIGIN_USER_HEADER, PUBLIC_KEY_DATAGRAM_HEADER, + SRC_TO_RELAY_QUEUE, UNENCRYPTED_DATAGRAM_HEADER, UNIT_TEST_QUEUE) if typing.TYPE_CHECKING: from datetime import datetime @@ -40,7 +46,7 @@ if typing.TYPE_CHECKING: def queue_to_flask(packet: Union[bytes, str], onion_pub_key: bytes, - flask_queue: 'Queue[Any]', + flask_queue: 'Queue[Tuple[Union[bytes, str], bytes]]', ts: 'datetime', header: bytes ) -> None: @@ -122,7 +128,7 @@ def src_incoming(queues: 'QueueDict', def process_group_management_message(ts: 'datetime', packet: bytes, header: bytes, - messages_to_flask: 'Queue[Any]') -> None: + messages_to_flask: 'Queue[Tuple[Union[bytes, str], bytes]]') -> None: """Parse and display group management message.""" header_str = header.decode() group_id, packet = separate_header(packet, GROUP_ID_LENGTH) diff --git a/src/transmitter/commands.py b/src/transmitter/commands.py index e138acd..5b3ffb0 100755 --- a/src/transmitter/commands.py +++ b/src/transmitter/commands.py @@ -35,7 +35,16 @@ from src.common.exceptions import FunctionReturn from src.common.input import yes from src.common.misc import ensure_dir, get_terminal_width, validate_onion_addr from src.common.output import clear_screen, m_print, phase, print_on_previous_line -from src.common.statics import * +from src.common.statics import (CH_MASTER_KEY, CH_SETTING, CLEAR, CLEAR_SCREEN, COMMAND_PACKET_QUEUE, DIR_USER_DATA, + DONE, EXIT_PROGRAM, GROUP_ID_ENC_LENGTH, KDB_CHANGE_MASTER_KEY_HEADER, + KDB_UPDATE_SIZE_HEADER, KEX_STATUS_UNVERIFIED, KEX_STATUS_VERIFIED, + KEY_MANAGEMENT_QUEUE, LOCAL_TESTING_PACKET_DELAY, LOGFILE_MASKING_QUEUE, LOG_DISPLAY, + LOG_EXPORT, LOG_REMOVE, MESSAGE, ONION_ADDRESS_LENGTH, RELAY_PACKET_QUEUE, RESET, + RESET_SCREEN, RX, SENDER_MODE_QUEUE, TRAFFIC_MASKING_QUEUE, TX, UNENCRYPTED_BAUDRATE, + UNENCRYPTED_DATAGRAM_HEADER, UNENCRYPTED_EC_RATIO, UNENCRYPTED_EXIT_COMMAND, + UNENCRYPTED_MANAGE_CONTACT_REQ, UNENCRYPTED_SCREEN_CLEAR, UNENCRYPTED_SCREEN_RESET, + UNENCRYPTED_WIPE_COMMAND, US_BYTE, VERSION, WIN_ACTIVITY, WIN_SELECT, WIN_TYPE_GROUP, + WIN_UID_FILE, WIN_UID_LOCAL, WIPE_USR_DATA) from src.transmitter.commands_g import process_group_command from src.transmitter.contact import add_new_contact, change_nick, contact_setting, remove_contact diff --git a/src/transmitter/commands_g.py b/src/transmitter/commands_g.py index e80299e..c4ddc28 100644 --- a/src/transmitter/commands_g.py +++ b/src/transmitter/commands_g.py @@ -22,7 +22,7 @@ along with TFC. If not, see . import os import typing -from typing import Any, Callable, Dict, List, Optional +from typing import Callable, Dict, List, Optional from src.common.db_logs import remove_logs from src.common.encoding import b58decode, int_to_bytes @@ -30,7 +30,11 @@ from src.common.exceptions import FunctionReturn from src.common.input import yes from src.common.misc import ignored, validate_group_name from src.common.output import group_management_print, m_print -from src.common.statics import * +from src.common.statics import (ADDED_MEMBERS, ALREADY_MEMBER, GROUP_ADD, GROUP_CREATE, GROUP_DELETE, + GROUP_ID_LENGTH, GROUP_MSG_EXIT_GROUP_HEADER, GROUP_MSG_INVITE_HEADER, + GROUP_MSG_JOIN_HEADER, GROUP_MSG_MEMBER_ADD_HEADER, GROUP_MSG_MEMBER_REM_HEADER, + GROUP_REMOVE, GROUP_RENAME, LOG_REMOVE, NEW_GROUP, NOT_IN_GROUP, RELAY_PACKET_QUEUE, + REMOVED_MEMBERS, UNKNOWN_ACCOUNTS, US_BYTE, WIN_TYPE_CONTACT) from src.transmitter.packet import queue_command, queue_to_nc from src.transmitter.user_input import UserInput @@ -42,7 +46,7 @@ if typing.TYPE_CHECKING: from src.common.db_masterkey import MasterKey from src.common.db_settings import Settings from src.transmitter.windows import TxWindow - QueueDict = Dict[bytes, Queue[Any]] + QueueDict = Dict[bytes, Queue[bytes]] FuncDict = (Dict[str, Callable[[str, List[bytes], ContactList, @@ -105,7 +109,7 @@ def process_group_command(user_input: 'UserInput', func_d = dict(create=group_create, join =group_create, add =group_add_member, - rm =group_rm_member) # type: FuncDict + rm =group_rm_member) # type: FuncDict func = func_d[command_type] @@ -123,7 +127,7 @@ def group_create(group_name: str, group_id: Optional[bytes] = None ) -> None: """Create a new group. - + Validate the group name and determine what members can be added. """ error_msg = validate_group_name(group_name, contact_list, group_list) diff --git a/src/transmitter/contact.py b/src/transmitter/contact.py index 52eb965..9ed7fcc 100644 --- a/src/transmitter/contact.py +++ b/src/transmitter/contact.py @@ -29,7 +29,11 @@ from src.common.exceptions import FunctionReturn from src.common.input import box_input, yes from src.common.misc import ignored, validate_key_exchange, validate_nick, validate_onion_addr from src.common.output import m_print -from src.common.statics import * +from src.common.statics import (ALL, CH_FILE_RECV, CH_LOGGING, CH_NICKNAME, CH_NOTIFY, CONTACT_REM, DISABLE, ECDHE, + ENABLE, KDB_REMOVE_ENTRY_HEADER, KEY_MANAGEMENT_QUEUE, LOGGING, LOG_SETTING_QUEUE, + NOTIFY, ONION_ADDRESS_LENGTH, PSK, RELAY_PACKET_QUEUE, STORE, TRUNC_ADDRESS_LENGTH, + UNENCRYPTED_ADD_NEW_CONTACT, UNENCRYPTED_DATAGRAM_HEADER, UNENCRYPTED_REM_CONTACT, + WIN_TYPE_CONTACT, WIN_TYPE_GROUP) from src.transmitter.commands_g import group_rename from src.transmitter.key_exchanges import create_pre_shared_key, start_key_exchange @@ -176,7 +180,7 @@ def remove_contact(user_input: 'UserInput', # If the last member of the group is removed, deselect # the group. Deselection is not done in - # update_group_win_members because it would prevent + # `TxWindow.update_window()` because it would prevent # selecting the empty group for group related commands # such as notifications. if not window.window_contacts: diff --git a/src/transmitter/files.py b/src/transmitter/files.py index b5cfc6f..c24b337 100755 --- a/src/transmitter/files.py +++ b/src/transmitter/files.py @@ -30,7 +30,8 @@ from src.common.crypto import byte_padding, csprng, encrypt_and_sign from src.common.encoding import int_to_bytes from src.common.exceptions import FunctionReturn from src.common.misc import readable_size, split_byte_string -from src.common.statics import * +from src.common.statics import (COMPRESSION_LEVEL, FILE_ETA_FIELD_LENGTH, FILE_PACKET_CTR_LENGTH, + FILE_SIZE_FIELD_LENGTH, PADDING_LENGTH, TRAFFIC_MASKING_QUEUE_CHECK_DELAY, US_BYTE) if typing.TYPE_CHECKING: from src.common.db_settings import Settings diff --git a/src/transmitter/input_loop.py b/src/transmitter/input_loop.py index 5c856b2..bae27bf 100755 --- a/src/transmitter/input_loop.py +++ b/src/transmitter/input_loop.py @@ -24,11 +24,11 @@ import readline import sys import typing -from typing import Any, Dict, NoReturn +from typing import Dict, NoReturn from src.common.exceptions import FunctionReturn from src.common.misc import get_tab_completer, ignored -from src.common.statics import * +from src.common.statics import COMMAND, FILE, MESSAGE from src.transmitter.commands import process_command from src.transmitter.contact import add_new_contact @@ -47,7 +47,7 @@ if typing.TYPE_CHECKING: from src.common.gateway import Gateway -def input_loop(queues: Dict[bytes, 'Queue[Any]'], +def input_loop(queues: Dict[bytes, 'Queue[bytes]'], settings: 'Settings', gateway: 'Gateway', contact_list: 'ContactList', diff --git a/src/transmitter/key_exchanges.py b/src/transmitter/key_exchanges.py index 2e47eae..898decd 100644 --- a/src/transmitter/key_exchanges.py +++ b/src/transmitter/key_exchanges.py @@ -32,7 +32,16 @@ from src.common.exceptions import FunctionReturn from src.common.input import ask_confirmation_code, get_b58_key, nc_bypass_msg, yes from src.common.output import m_print, phase, print_fingerprint, print_key, print_on_previous_line from src.common.path import ask_path_gui -from src.common.statics import * +from src.common.statics import (ARGON2_PSK_MEMORY_COST, ARGON2_PSK_PARALLELISM, ARGON2_PSK_TIME_COST, + B58_PUBLIC_KEY, CONFIRM_CODE_LENGTH, DONE, ECDHE, FINGERPRINT, FINGERPRINT_LENGTH, + HEADER_KEY, KDB_ADD_ENTRY_HEADER, KEX_STATUS_HAS_RX_PSK, KEX_STATUS_LOCAL_KEY, + KEX_STATUS_NO_RX_PSK, KEX_STATUS_PENDING, KEX_STATUS_UNVERIFIED, + KEX_STATUS_VERIFIED, KEY_EX_ECDHE, KEY_EX_PSK_RX, KEY_EX_PSK_TX, + KEY_MANAGEMENT_QUEUE, LOCAL_KEY_DATAGRAM_HEADER, LOCAL_KEY_RDY, LOCAL_NICK, + LOCAL_PUBKEY, MESSAGE_KEY, NC_BYPASS_START, NC_BYPASS_STOP, + PUBLIC_KEY_DATAGRAM_HEADER, RELAY_PACKET_QUEUE, RESET, SYMMETRIC_KEY_LENGTH, + TFC_PUBLIC_KEY_LENGTH, UNENCRYPTED_DATAGRAM_HEADER, UNENCRYPTED_ONION_SERVICE_DATA, + WIN_TYPE_GROUP) from src.transmitter.packet import queue_command, queue_to_nc @@ -484,6 +493,7 @@ def create_pre_shared_key(onion_pub_key: bytes, # Public key of contac m_print("Error: Did not have permission to write to the directory.", delay=0.5) continue + c_code = blake2b(onion_pub_key, digest_size=CONFIRM_CODE_LENGTH) command = (KEY_EX_PSK_TX + onion_pub_key + tx_mk + csprng() @@ -492,6 +502,21 @@ def create_pre_shared_key(onion_pub_key: bytes, # Public key of contac queue_command(command, settings, queues) + while True: + purp_code = ask_confirmation_code('Receiver') + if purp_code == c_code.hex(): + break + + elif purp_code == '': + phase("Resending contact data", head=2) + queue_command(command, settings, queues) + phase(DONE) + print_on_previous_line(reps=5) + + else: + m_print("Incorrect confirmation code.", head=1) + print_on_previous_line(reps=4, delay=2) + contact_list.add_contact(onion_pub_key, nick, bytes(FINGERPRINT_LENGTH), bytes(FINGERPRINT_LENGTH), KEX_STATUS_NO_RX_PSK, diff --git a/src/transmitter/packet.py b/src/transmitter/packet.py index 62b6b12..3ad3591 100755 --- a/src/transmitter/packet.py +++ b/src/transmitter/packet.py @@ -24,7 +24,7 @@ import os import typing import zlib -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Tuple, Union from src.common.crypto import blake2b, byte_padding, csprng, encrypt_and_sign from src.common.encoding import bool_to_bytes, int_to_bytes, str_to_bytes @@ -33,7 +33,14 @@ from src.common.input import yes from src.common.misc import split_byte_string from src.common.output import m_print, phase, print_on_previous_line from src.common.path import ask_path_gui -from src.common.statics import * +from src.common.statics import (ASSEMBLY_PACKET_LENGTH, COMMAND, COMMAND_DATAGRAM_HEADER, COMMAND_PACKET_QUEUE, + COMPRESSION_LEVEL, C_A_HEADER, C_E_HEADER, C_L_HEADER, C_S_HEADER, DONE, FILE, + FILE_DATAGRAM_HEADER, FILE_KEY_HEADER, FILE_PACKET_CTR_LENGTH, F_A_HEADER, + F_C_HEADER, F_E_HEADER, F_L_HEADER, F_S_HEADER, GROUP_MESSAGE_HEADER, + GROUP_MSG_ID_LENGTH, LOCAL_PUBKEY, MESSAGE, MESSAGE_DATAGRAM_HEADER, + MESSAGE_PACKET_QUEUE, M_A_HEADER, M_C_HEADER, M_E_HEADER, M_L_HEADER, M_S_HEADER, + PADDING_LENGTH, PRIVATE_MESSAGE_HEADER, RELAY_PACKET_QUEUE, TM_COMMAND_PACKET_QUEUE, + TM_FILE_PACKET_QUEUE, TM_MESSAGE_PACKET_QUEUE, WIN_TYPE_GROUP) from src.transmitter.files import File from src.transmitter.user_input import UserInput @@ -41,14 +48,16 @@ from src.transmitter.user_input import UserInput if typing.TYPE_CHECKING: from multiprocessing import Queue from src.common.db_keys import KeyList + from src.common.db_masterkey import MasterKey from src.common.db_settings import Settings from src.common.gateway import Gateway from src.transmitter.windows import TxWindow, MockWindow - QueueDict = Dict[bytes, Queue[Any]] + QueueDict = Dict[bytes, Queue[Any]] + log_queue_data = Tuple[Optional[bytes], bytes, Optional[bool], Optional[bool], MasterKey] def queue_to_nc(packet: bytes, - nc_queue: 'Queue[Any]', + nc_queue: 'Queue[bytes]', ) -> None: """Queue unencrypted command/exported file to Networked Computer. @@ -360,7 +369,7 @@ def queue_assembly_packets(assembly_packet_list: List[bytes], settings: 'Settings', queues: 'QueueDict', window: Optional[Union['TxWindow', 'MockWindow']] = None, - log_as_ph: bool = False + log_as_ph: bool = False ) -> None: """Queue assembly packets for sender_loop(). @@ -388,13 +397,13 @@ def queue_assembly_packets(assembly_packet_list: List[bytes], queue.put(assembly_packet) -def send_packet(key_list: 'KeyList', # Key list object - gateway: 'Gateway', # Gateway object - log_queue: 'Queue[Any]', # Multiprocessing queue for logged messages - assembly_packet: bytes, # Padded plaintext assembly packet - onion_pub_key: Optional[bytes] = None, # Recipient v3 Onion Service address - log_messages: Optional[bool] = None, # When True, log the message assembly packet - log_as_ph: Optional[bool] = None # When True, log assembly packet as placeholder data +def send_packet(key_list: 'KeyList', # Key list object + gateway: 'Gateway', # Gateway object + log_queue: 'Queue[log_queue_data]', # Multiprocessing queue for logged messages + assembly_packet: bytes, # Padded plaintext assembly packet + onion_pub_key: Optional[bytes] = None, # Recipient v3 Onion Service address + log_messages: Optional[bool] = None, # When True, log the message assembly packet + log_as_ph: Optional[bool] = None # When True, log assembly packet as placeholder data ) -> None: """Encrypt and send assembly packet. diff --git a/src/transmitter/sender_loop.py b/src/transmitter/sender_loop.py index 8c7fa5d..6769673 100755 --- a/src/transmitter/sender_loop.py +++ b/src/transmitter/sender_loop.py @@ -25,7 +25,12 @@ import typing from typing import Any, Dict, List, Optional, Tuple from src.common.misc import ignored -from src.common.statics import * +from src.common.statics import (COMMAND_PACKET_QUEUE, DATAGRAM_HEADER_LENGTH, EXIT, EXIT_QUEUE, KEY_MANAGEMENT_QUEUE, + LOG_PACKET_QUEUE, MESSAGE_PACKET_QUEUE, RELAY_PACKET_QUEUE, SENDER_MODE_QUEUE, + TM_COMMAND_PACKET_QUEUE, TM_FILE_PACKET_QUEUE, TM_MESSAGE_PACKET_QUEUE, + TM_NOISE_COMMAND_QUEUE, TM_NOISE_PACKET_QUEUE, TRAFFIC_MASKING, + TRAFFIC_MASKING_QUEUE_CHECK_DELAY, UNENCRYPTED_EXIT_COMMAND, UNENCRYPTED_WIPE_COMMAND, + WINDOW_SELECT_QUEUE, WIPE) from src.transmitter.packet import send_packet from src.transmitter.traffic_masking import HideRunTime @@ -35,7 +40,6 @@ if typing.TYPE_CHECKING: from src.common.db_keys import KeyList from src.common.db_settings import Settings from src.common.gateway import Gateway - from src.common.db_settings import Settings QueueDict = Dict[bytes, Queue[Any]] Message_buffer = Dict[bytes, List[Tuple[bytes, bytes, bool, bool, bytes]]] diff --git a/src/transmitter/traffic_masking.py b/src/transmitter/traffic_masking.py index c0dbc94..9ec1b99 100755 --- a/src/transmitter/traffic_masking.py +++ b/src/transmitter/traffic_masking.py @@ -27,7 +27,8 @@ import typing from typing import Any, Dict, Optional, Tuple, Union from src.common.misc import ignored -from src.common.statics import * +from src.common.statics import (C_N_HEADER, NOISE_PACKET_BUFFER, PADDING_LENGTH, P_N_HEADER, STATIC, + TM_NOISE_COMMAND_QUEUE, TM_NOISE_PACKET_QUEUE, TRAFFIC_MASKING) if typing.TYPE_CHECKING: from multiprocessing import Queue diff --git a/src/transmitter/user_input.py b/src/transmitter/user_input.py index c3696a9..397a8a9 100755 --- a/src/transmitter/user_input.py +++ b/src/transmitter/user_input.py @@ -22,7 +22,7 @@ along with TFC. If not, see . import typing from src.common.output import print_on_previous_line -from src.common.statics import * +from src.common.statics import COMMAND, FILE, MESSAGE, WIN_TYPE_GROUP if typing.TYPE_CHECKING: from src.common.db_settings import Settings diff --git a/src/transmitter/windows.py b/src/transmitter/windows.py index 31dc114..e3f0a04 100755 --- a/src/transmitter/windows.py +++ b/src/transmitter/windows.py @@ -27,7 +27,7 @@ from src.common.db_contacts import Contact from src.common.exceptions import FunctionReturn from src.common.input import yes from src.common.output import clear_screen -from src.common.statics import * +from src.common.statics import KEX_STATUS_PENDING, WINDOW_SELECT_QUEUE, WIN_SELECT, WIN_TYPE_CONTACT, WIN_TYPE_GROUP from src.transmitter.contact import add_new_contact from src.transmitter.key_exchanges import export_onion_service_data, start_key_exchange diff --git a/tests/common/test_crypto.py b/tests/common/test_crypto.py index dca8f25..3863850 100644 --- a/tests/common/test_crypto.py +++ b/tests/common/test_crypto.py @@ -40,11 +40,11 @@ from cryptography.hazmat.primitives.serialization import Encoding, NoEncryptio from src.common.crypto import argon2_kdf, auth_and_decrypt, blake2b, byte_padding, check_kernel_version, csprng from src.common.crypto import encrypt_and_sign, rm_padding_bytes, X448 -from src.common.statics import ARGON2_MIN_MEMORY_COST, ARGON2_MIN_PARALLELISM, ARGON2_MIN_TIME_COST, ARGON2_SALT_LENGTH -from src.common.statics import BLAKE2_DIGEST_LENGTH, BLAKE2_DIGEST_LENGTH_MAX, BLAKE2_DIGEST_LENGTH_MIN -from src.common.statics import BLAKE2_KEY_LENGTH_MAX, BLAKE2_PERSON_LENGTH_MAX, BLAKE2_SALT_LENGTH_MAX, PADDING_LENGTH -from src.common.statics import SYMMETRIC_KEY_LENGTH, TFC_PRIVATE_KEY_LENGTH, TFC_PUBLIC_KEY_LENGTH -from src.common.statics import XCHACHA20_NONCE_LENGTH +from src.common.statics import (ARGON2_MIN_MEMORY_COST, ARGON2_MIN_PARALLELISM, ARGON2_MIN_TIME_COST, + ARGON2_SALT_LENGTH, BLAKE2_DIGEST_LENGTH, BLAKE2_DIGEST_LENGTH_MAX, + BLAKE2_DIGEST_LENGTH_MIN, BLAKE2_KEY_LENGTH_MAX, BLAKE2_PERSON_LENGTH_MAX, + BLAKE2_SALT_LENGTH_MAX, PADDING_LENGTH, SYMMETRIC_KEY_LENGTH, TFC_PRIVATE_KEY_LENGTH, + TFC_PUBLIC_KEY_LENGTH, XCHACHA20_NONCE_LENGTH) from tests.utils import cd_unit_test, cleanup @@ -68,6 +68,7 @@ class TestBLAKE2b(unittest.TestCase): """ def setUp(self) -> None: + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() kat_file_url = 'https://raw.githubusercontent.com/BLAKE2/BLAKE2/master/testvectors/blake2b-kat.txt' @@ -109,6 +110,7 @@ class TestBLAKE2b(unittest.TestCase): self.assertEqual(len(set(digests)), 256) def tearDown(self) -> None: + """Post-test actions.""" cleanup(self.unit_test_dir) def test_blake2b_using_the_official_known_answer_tests(self): @@ -144,8 +146,15 @@ class TestBLAKE2bWrapper(unittest.TestCase): with self.assertRaises(SystemExit): blake2b(b'test_string', digest_size=invalid_digest_size) - @mock.patch('hashlib.blake2b', return_value=MagicMock(digest=(MagicMock(side_effect=[(BLAKE2_DIGEST_LENGTH-1)*b'a', - (BLAKE2_DIGEST_LENGTH+1)*b'a'])))) + @mock.patch('hashlib.blake2b', return_value=MagicMock(digest=(MagicMock(side_effect=[BLAKE2_DIGEST_LENGTH*'a'])))) + def test_invalid_blake2b_digest_type_raises_critical_error(self, mock_blake2b): + with self.assertRaises(SystemExit): + blake2b(b'test_string') + mock_blake2b.assert_called() + + @mock.patch('hashlib.blake2b', return_value=MagicMock(digest=( + MagicMock(side_effect=[(BLAKE2_DIGEST_LENGTH-1)*b'a', + (BLAKE2_DIGEST_LENGTH+1)*b'a'])))) def test_invalid_size_blake2b_digest_raises_critical_error(self, mock_blake2b): with self.assertRaises(SystemExit): blake2b(b'test_string') @@ -175,6 +184,7 @@ class TestArgon2KDF(unittest.TestCase): """ def setUp(self) -> None: + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.number_of_tests = 256 @@ -197,6 +207,7 @@ class TestArgon2KDF(unittest.TestCase): subprocess.Popen('make test', shell=True).wait() def tearDown(self) -> None: + """Post-test actions.""" os.chdir('..') cleanup(self.unit_test_dir) @@ -260,6 +271,7 @@ class TestArgon2KDF(unittest.TestCase): class TestArgon2Wrapper(unittest.TestCase): def setUp(self) -> None: + """Pre-test actions.""" self.salt = os.urandom(ARGON2_SALT_LENGTH) def test_invalid_length_salt_raises_critical_error(self): @@ -267,7 +279,21 @@ class TestArgon2Wrapper(unittest.TestCase): ARGON2_SALT_LENGTH+1, 1000]] for invalid_salt in invalid_salts: with self.assertRaises(SystemExit): - argon2_kdf('password', invalid_salt, ARGON2_MIN_TIME_COST, ARGON2_MIN_MEMORY_COST, ARGON2_MIN_PARALLELISM) + argon2_kdf('password', invalid_salt, + ARGON2_MIN_TIME_COST, ARGON2_MIN_MEMORY_COST, ARGON2_MIN_PARALLELISM) + + @mock.patch("argon2.low_level.hash_secret_raw", MagicMock(side_effect=[SYMMETRIC_KEY_LENGTH*'a'])) + def test_invalid_type_key_from_argon2_raises_critical_error(self): + with self.assertRaises(SystemExit): + argon2_kdf('password', self.salt, ARGON2_MIN_TIME_COST, ARGON2_MIN_MEMORY_COST, ARGON2_MIN_PARALLELISM) + + @mock.patch("argon2.low_level.hash_secret_raw", MagicMock(side_effect=[(SYMMETRIC_KEY_LENGTH-1)*b'a', + (SYMMETRIC_KEY_LENGTH+1)*b'a'])) + def test_invalid_size_key_from_argon2_raises_critical_error(self): + with self.assertRaises(SystemExit): + argon2_kdf('password', self.salt, ARGON2_MIN_TIME_COST, ARGON2_MIN_MEMORY_COST, ARGON2_MIN_PARALLELISM) + with self.assertRaises(SystemExit): + argon2_kdf('password', self.salt, ARGON2_MIN_TIME_COST, ARGON2_MIN_MEMORY_COST, ARGON2_MIN_PARALLELISM) def test_too_small_time_cost_raises_critical_error(self): with self.assertRaises(SystemExit): @@ -345,6 +371,13 @@ class TestX448(unittest.TestCase): self.assertIsInstance(public_key, bytes) self.assertEqual(len(public_key), TFC_PUBLIC_KEY_LENGTH) + def test_deriving_invalid_type_public_key_raises_critical_error(self): + private_key = MagicMock(public_key=MagicMock(return_value=MagicMock( + public_bytes=MagicMock(side_effect=[TFC_PUBLIC_KEY_LENGTH * 'a'])))) + + with self.assertRaises(SystemExit): + X448.derive_public_key(private_key) + def test_deriving_invalid_size_public_key_raises_critical_error(self): """ The public key is already validated by the pyca/cryptography @@ -513,6 +546,7 @@ class TestXChaCha20Poly1305(unittest.TestCase): nonce_ct_tag_libsodium = libsodium_nonce + libsodium_ct_tag def setUp(self) -> None: + """Pre-test actions.""" self.assertEqual(self.ietf_plaintext, self.libsodium_plaintext) self.assertEqual(self.ietf_ad, self.libsodium_ad) self.assertEqual(self.ietf_key, self.libsodium_key) @@ -589,6 +623,16 @@ class TestBytePadding(unittest.TestCase): self.assertEqual(padded_bytestring_lengths, {1*PADDING_LENGTH, 2*PADDING_LENGTH, 3*PADDING_LENGTH, 4*PADDING_LENGTH}) + @mock.patch('cryptography.hazmat.primitives.padding.PKCS7', + return_value=MagicMock( + padder=MagicMock(return_value=MagicMock( + update=MagicMock(return_value=''), + finalize=MagicMock(return_value=(PADDING_LENGTH*'a')))))) + def test_invalid_padding_type_raises_critical_error(self, mock_padder): + with self.assertRaises(SystemExit): + byte_padding(b'test_string') + mock_padder.assert_called() + @mock.patch('cryptography.hazmat.primitives.padding.PKCS7', return_value=MagicMock( padder=MagicMock(return_value=MagicMock( @@ -663,6 +707,11 @@ class TestCSPRNG(unittest.TestCase): key = csprng(key_size) self.assertEqual(len(key), key_size) + @mock.patch('os.getrandom', return_value=SYMMETRIC_KEY_LENGTH*'a') + def test_invalid_entropy_type_from_getrandom_raises_critical_error(self, _): + with self.assertRaises(SystemExit): + csprng() + def test_subceeding_hash_function_min_digest_size_raises_critical_error(self): with self.assertRaises(SystemExit): csprng(BLAKE2_DIGEST_LENGTH_MIN-1) diff --git a/tests/common/test_db_contacts.py b/tests/common/test_db_contacts.py index 2ee0812..c0f2f11 100644 --- a/tests/common/test_db_contacts.py +++ b/tests/common/test_db_contacts.py @@ -25,7 +25,10 @@ import unittest from src.common.crypto import encrypt_and_sign from src.common.db_contacts import Contact, ContactList from src.common.misc import ensure_dir -from src.common.statics import * +from src.common.statics import (CLEAR_ENTIRE_SCREEN, CONTACT_LENGTH, CURSOR_LEFT_UP_CORNER, DIR_USER_DATA, ECDHE, + FINGERPRINT_LENGTH, KEX_STATUS_HAS_RX_PSK, KEX_STATUS_LOCAL_KEY, KEX_STATUS_NONE, + KEX_STATUS_NO_RX_PSK, KEX_STATUS_PENDING, KEX_STATUS_UNVERIFIED, + KEX_STATUS_VERIFIED, LOCAL_ID, POLY1305_TAG_LENGTH, PSK, XCHACHA20_NONCE_LENGTH) from tests.mock_classes import create_contact, MasterKey, Settings from tests.utils import cd_unit_test, cleanup, nick_to_onion_address, nick_to_pub_key, tamper_file, TFCTestCase @@ -34,6 +37,7 @@ from tests.utils import cd_unit_test, cleanup, nick_to_onion_address, nic class TestContact(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.contact = Contact(nick_to_pub_key('Bob'), 'Bob', FINGERPRINT_LENGTH * b'\x01', @@ -62,6 +66,7 @@ class TestContact(unittest.TestCase): class TestContactList(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.master_key = MasterKey() self.settings = Settings() @@ -73,6 +78,7 @@ class TestContactList(TFCTestCase): self.real_contact_list.remove(LOCAL_ID) def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) def test_contact_list_iterates_over_contact_objects(self): @@ -101,7 +107,8 @@ class TestContactList(TFCTestCase): def test_invalid_content_raises_critical_error(self): # Setup invalid_data = b'a' - pt_bytes = b''.join([c.serialize_c() for c in self.contact_list.contacts + self.contact_list._dummy_contacts()]) + pt_bytes = b''.join([c.serialize_c() for c in self.contact_list.contacts + + self.contact_list._dummy_contacts()]) ct_bytes = encrypt_and_sign(pt_bytes + invalid_data, self.master_key.master_key) ensure_dir(DIR_USER_DATA) diff --git a/tests/common/test_db_groups.py b/tests/common/test_db_groups.py index 9f8c515..9e74ed9 100644 --- a/tests/common/test_db_groups.py +++ b/tests/common/test_db_groups.py @@ -27,7 +27,8 @@ from src.common.db_contacts import Contact, ContactList from src.common.db_groups import Group, GroupList from src.common.encoding import b58encode from src.common.misc import ensure_dir -from src.common.statics import * +from src.common.statics import (DIR_USER_DATA, GROUP_DB_HEADER_LENGTH, GROUP_ID_LENGTH, GROUP_STATIC_LENGTH, + ONION_SERVICE_PUBLIC_KEY_LENGTH, POLY1305_TAG_LENGTH, XCHACHA20_NONCE_LENGTH) from tests.mock_classes import create_contact, group_name_to_group_id, MasterKey, nick_to_pub_key, Settings from tests.utils import cd_unit_test, cleanup, tamper_file, TFCTestCase @@ -36,6 +37,7 @@ from tests.utils import cd_unit_test, cleanup, tamper_file, TFCTestCase class TestGroup(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.nicks = ['Alice', 'Bob', 'Charlie'] members = list(map(create_contact, self.nicks)) @@ -50,6 +52,7 @@ class TestGroup(unittest.TestCase): ensure_dir(DIR_USER_DATA) def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) def test_group_iterates_over_contact_objects(self): @@ -117,6 +120,7 @@ class TestGroup(unittest.TestCase): class TestGroupList(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.master_key = MasterKey() self.settings = Settings() @@ -146,6 +150,7 @@ class TestGroupList(TFCTestCase): + self.settings.max_number_of_group_members * ONION_SERVICE_PUBLIC_KEY_LENGTH) def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) def test_group_list_iterates_over_group_objects(self): diff --git a/tests/common/test_db_keys.py b/tests/common/test_db_keys.py index aff65bf..7414d41 100644 --- a/tests/common/test_db_keys.py +++ b/tests/common/test_db_keys.py @@ -26,7 +26,9 @@ from src.common.crypto import blake2b, encrypt_and_sign from src.common.db_keys import KeyList, KeySet from src.common.encoding import int_to_bytes from src.common.misc import ensure_dir -from src.common.statics import * +from src.common.statics import (DIR_USER_DATA, INITIAL_HARAC, KDB_ADD_ENTRY_HEADER, KDB_CHANGE_MASTER_KEY_HEADER, + KDB_REMOVE_ENTRY_HEADER, KDB_UPDATE_SIZE_HEADER, KEYSET_LENGTH, LOCAL_ID, LOCAL_PUBKEY, + POLY1305_TAG_LENGTH, RX, SYMMETRIC_KEY_LENGTH, TX, XCHACHA20_NONCE_LENGTH) from tests.mock_classes import create_keyset, MasterKey, nick_to_pub_key, Settings from tests.utils import cd_unit_test, cleanup, tamper_file @@ -35,6 +37,7 @@ from tests.utils import cd_unit_test, cleanup, tamper_file class TestKeySet(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.keyset = KeySet(onion_pub_key=nick_to_pub_key('Alice'), tx_mk=bytes(SYMMETRIC_KEY_LENGTH), rx_mk=bytes(SYMMETRIC_KEY_LENGTH), @@ -86,6 +89,7 @@ class TestKeySet(unittest.TestCase): class TestKeyList(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.master_key = MasterKey() self.settings = Settings() @@ -95,6 +99,7 @@ class TestKeyList(unittest.TestCase): self.keylist.keysets = [create_keyset(n, store_f=self.keylist.store_keys) for n in self.full_contact_list] def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) def test_storing_and_loading_of_keysets(self): diff --git a/tests/common/test_db_logs.py b/tests/common/test_db_logs.py index 1a30b1a..6bf54e7 100644 --- a/tests/common/test_db_logs.py +++ b/tests/common/test_db_logs.py @@ -30,7 +30,12 @@ from unittest import mock from src.common.db_contacts import ContactList from src.common.db_logs import access_logs, change_log_db_key, log_writer_loop, remove_logs, write_log_entry from src.common.encoding import bytes_to_timestamp -from src.common.statics import * +from src.common.statics import (CLEAR_ENTIRE_SCREEN, CURSOR_LEFT_UP_CORNER, C_S_HEADER, DIR_USER_DATA, EXIT, + F_S_HEADER, GROUP_ID_LENGTH, LOGFILE_MASKING_QUEUE, LOG_ENTRY_LENGTH, + LOG_PACKET_QUEUE, LOG_SETTING_QUEUE, MESSAGE, M_A_HEADER, M_C_HEADER, M_S_HEADER, + ORIGIN_CONTACT_HEADER, PADDING_LENGTH, P_N_HEADER, RX, SYMMETRIC_KEY_LENGTH, + TIMESTAMP_LENGTH, TRAFFIC_MASKING_QUEUE, UNIT_TEST_QUEUE, WIN_TYPE_CONTACT, + WIN_TYPE_GROUP) from tests.mock_classes import create_contact, GroupList, MasterKey, RxWindow, Settings from tests.utils import assembly_packet_creator, cd_unit_test, cleanup, group_name_to_group_id, nick_to_pub_key @@ -40,12 +45,15 @@ TIMESTAMP_BYTES = bytes.fromhex('08ceae02') STATIC_TIMESTAMP = bytes_to_timestamp(TIMESTAMP_BYTES).strftime('%H:%M:%S.%f')[:-TIMESTAMP_LENGTH] SLEEP_DELAY = 0.02 + class TestLogWriterLoop(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) def test_function_logs_normal_data(self): @@ -56,10 +64,10 @@ class TestLogWriterLoop(unittest.TestCase): def queue_delayer(): """Place messages to queue one at a time.""" - for p in [(nick_to_pub_key('Alice'), M_S_HEADER + bytes(PADDING_LENGTH), False, False, master_key), - (None, C_S_HEADER + bytes(PADDING_LENGTH), True, False, master_key), - (nick_to_pub_key('Alice'), P_N_HEADER + bytes(PADDING_LENGTH), True, True, master_key), - (nick_to_pub_key('Alice'), F_S_HEADER + bytes(PADDING_LENGTH), True, True, master_key), + for p in [(nick_to_pub_key('Alice'), M_S_HEADER + bytes(PADDING_LENGTH), False, False, master_key), + (None, C_S_HEADER + bytes(PADDING_LENGTH), True, False, master_key), + (nick_to_pub_key('Alice'), P_N_HEADER + bytes(PADDING_LENGTH), True, True, master_key), + (nick_to_pub_key('Alice'), F_S_HEADER + bytes(PADDING_LENGTH), True, True, master_key), (nick_to_pub_key('Alice'), M_S_HEADER + bytes(PADDING_LENGTH), True, False, master_key)]: queues[LOG_PACKET_QUEUE].put(p) time.sleep(SLEEP_DELAY) @@ -90,9 +98,9 @@ class TestLogWriterLoop(unittest.TestCase): def queue_delayer(): """Place messages to queue one at a time.""" - for p in [(nick_to_pub_key('Alice'), M_S_HEADER + bytes(PADDING_LENGTH), False, False, master_key), - (None, C_S_HEADER + bytes(PADDING_LENGTH), True, False, master_key), - (nick_to_pub_key('Alice'), F_S_HEADER + bytes(PADDING_LENGTH), True, True, master_key), + for p in [(nick_to_pub_key('Alice'), M_S_HEADER + bytes(PADDING_LENGTH), False, False, master_key), + (None, C_S_HEADER + bytes(PADDING_LENGTH), True, False, master_key), + (nick_to_pub_key('Alice'), F_S_HEADER + bytes(PADDING_LENGTH), True, True, master_key), (nick_to_pub_key('Alice'), M_S_HEADER + bytes(PADDING_LENGTH), True, False, master_key)]: queues[LOG_PACKET_QUEUE].put(p) time.sleep(SLEEP_DELAY) @@ -194,12 +202,14 @@ class TestLogWriterLoop(unittest.TestCase): class TestWriteLogEntry(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.master_key = MasterKey() self.settings = Settings() self.log_file = f'{DIR_USER_DATA}{self.settings.software_operation}_logs' def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) def test_oversize_packet_raises_critical_error(self): @@ -220,6 +230,7 @@ class TestWriteLogEntry(unittest.TestCase): class TestAccessHistoryAndPrintLogs(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.master_key = MasterKey() self.settings = Settings() @@ -250,6 +261,7 @@ class TestAccessHistoryAndPrintLogs(TFCTestCase): "s neque a facilisis. Mauris id tortor placerat, aliquam dolor ac, venenatis arcu.") def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) def test_missing_log_file_raises_fr(self): @@ -509,6 +521,7 @@ Log file of message(s) to/from group test_group class TestReEncrypt(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.old_key = MasterKey() self.new_key = MasterKey(master_key=os.urandom(SYMMETRIC_KEY_LENGTH)) @@ -517,10 +530,11 @@ class TestReEncrypt(TFCTestCase): self.time = STATIC_TIMESTAMP def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) def test_missing_log_database_raises_fr(self): - self.assert_fr(f"Error: Could not find log database.", + self.assert_fr(f"No log database available.", change_log_db_key, self.old_key.master_key, self.new_key.master_key, self.settings) @mock.patch('struct.pack', return_value=TIMESTAMP_BYTES) @@ -569,6 +583,7 @@ Log file of message(s) sent to contact Alice class TestRemoveLog(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.master_key = MasterKey() self.settings = Settings() @@ -591,6 +606,7 @@ class TestRemoveLog(TFCTestCase): "s neque a facilisis. Mauris id tortor placerat, aliquam dolor ac, venenatis arcu.") def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) def test_missing_log_file_raises_fr(self): diff --git a/tests/common/test_db_masterkey.py b/tests/common/test_db_masterkey.py index 0e2af05..322386d 100644 --- a/tests/common/test_db_masterkey.py +++ b/tests/common/test_db_masterkey.py @@ -28,12 +28,14 @@ from unittest.mock import MagicMock from src.common.db_masterkey import MasterKey from src.common.misc import ensure_dir -from src.common.statics import * +from src.common.statics import (DIR_USER_DATA, MASTERKEY_DB_SIZE, PASSWORD_MIN_BIT_STRENGTH, + SYMMETRIC_KEY_LENGTH, TX) from tests.utils import cd_unit_test, cleanup KL = SYMMETRIC_KEY_LENGTH + class TestMasterKey(unittest.TestCase): input_list = ['password', 'different_password', # Invalid new password pair 'password', 'password', # Valid new password pair @@ -41,13 +43,22 @@ class TestMasterKey(unittest.TestCase): 'password'] # Valid login password def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.operation = TX self.file_name = f"{DIR_USER_DATA}{self.operation}_login_data" def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) + def test_password_generation(self): + bit_strength, password = MasterKey.generate_master_password() + self.assertIsInstance(bit_strength, int) + self.assertIsInstance(password, str) + self.assertGreaterEqual(bit_strength, PASSWORD_MIN_BIT_STRENGTH) + self.assertEqual(len(password.split(' ')), 10) + @mock.patch('time.sleep', return_value=None) def test_invalid_data_in_db_raises_critical_error(self, _): for delta in [-1, 1]: @@ -60,6 +71,8 @@ class TestMasterKey(unittest.TestCase): @mock.patch('src.common.db_masterkey.MIN_KEY_DERIVATION_TIME', 0.01) @mock.patch('src.common.db_masterkey.MAX_KEY_DERIVATION_TIME', 0.1) + @mock.patch('os.popen', return_value=MagicMock( + read=MagicMock(return_value=MagicMock(splitlines=MagicMock(return_value=["MemAvailable 10240"]))))) @mock.patch('os.path.isfile', side_effect=[KeyboardInterrupt, False, True]) @mock.patch('getpass.getpass', side_effect=input_list) @mock.patch('time.sleep', return_value=None) @@ -75,6 +88,18 @@ class TestMasterKey(unittest.TestCase): self.assertIsInstance(master_key2.master_key, bytes) self.assertEqual(master_key.master_key, master_key2.master_key) + @mock.patch('src.common.db_masterkey.MIN_KEY_DERIVATION_TIME', 0.01) + @mock.patch('src.common.db_masterkey.MAX_KEY_DERIVATION_TIME', 0.1) + @mock.patch('os.popen', return_value=MagicMock( + read=MagicMock(return_value=MagicMock(splitlines=MagicMock(return_value=["MemAvailable 10240"]))))) + @mock.patch('getpass.getpass', side_effect=['generate']) + @mock.patch('builtins.input', side_effect=['']) + @mock.patch('os.system', return_value=None) + @mock.patch('time.sleep', return_value=None) + def test_password_generation(self, *_): + master_key = MasterKey(self.operation, local_test=True) + self.assertIsInstance(master_key.master_key, bytes) + @mock.patch('src.common.db_masterkey.MasterKey.timed_key_derivation', MagicMock(side_effect= [(KL*b'a', 0.01)] + 100 * [(KL*b'b', 5.0)] @@ -83,7 +108,7 @@ class TestMasterKey(unittest.TestCase): @mock.patch('os.path.isfile', side_effect=[False, True]) @mock.patch('getpass.getpass', side_effect=input_list) @mock.patch('time.sleep', return_value=None) - def test_kd_binary_serach(self, *_): + def test_kd_binary_search(self, *_): MasterKey(self.operation, local_test=True) diff --git a/tests/common/test_db_onion.py b/tests/common/test_db_onion.py index 230d79f..45f21a7 100644 --- a/tests/common/test_db_onion.py +++ b/tests/common/test_db_onion.py @@ -27,7 +27,8 @@ from unittest import mock from src.common.crypto import encrypt_and_sign from src.common.db_onion import OnionService from src.common.misc import ensure_dir, validate_onion_addr -from src.common.statics import * +from src.common.statics import (DIR_USER_DATA, ONION_SERVICE_PRIVATE_KEY_LENGTH, + POLY1305_TAG_LENGTH, TX, XCHACHA20_NONCE_LENGTH) from tests.mock_classes import MasterKey from tests.utils import cd_unit_test, cleanup, tamper_file @@ -36,11 +37,13 @@ from tests.utils import cd_unit_test, cleanup, tamper_file class TestOnionService(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.master_key = MasterKey() self.file_name = f"{DIR_USER_DATA}{TX}_onion_db" def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) @mock.patch('time.sleep', return_value=None) @@ -67,7 +70,7 @@ class TestOnionService(unittest.TestCase): @mock.patch('time.sleep', return_value=None) def test_loading_invalid_onion_key_raises_critical_error(self, _): # Setup - ct_bytes = encrypt_and_sign((ONION_SERVICE_PRIVATE_KEY_LENGTH +1) * b'a', self.master_key.master_key) + ct_bytes = encrypt_and_sign((ONION_SERVICE_PRIVATE_KEY_LENGTH + 1) * b'a', self.master_key.master_key) ensure_dir(DIR_USER_DATA) with open(f'{DIR_USER_DATA}{TX}_onion_db', 'wb+') as f: diff --git a/tests/common/test_db_settings.py b/tests/common/test_db_settings.py index 04709a7..518d9b7 100644 --- a/tests/common/test_db_settings.py +++ b/tests/common/test_db_settings.py @@ -25,7 +25,7 @@ import unittest from unittest import mock from src.common.db_settings import Settings -from src.common.statics import * +from src.common.statics import CLEAR_ENTIRE_SCREEN, CURSOR_LEFT_UP_CORNER, DIR_USER_DATA, RX, SETTING_LENGTH, TX from tests.mock_classes import ContactList, create_group, GroupList, MasterKey from tests.utils import cd_unit_test, cleanup, tamper_file, TFCTestCase @@ -34,6 +34,7 @@ from tests.utils import cd_unit_test, cleanup, tamper_file, TFCTestCase class TestSettings(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.file_name = f"{DIR_USER_DATA}{TX}_settings" self.master_key = MasterKey() @@ -44,6 +45,7 @@ class TestSettings(TFCTestCase): self.args = self.contact_list, self.group_list def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) def test_invalid_type_raises_critical_error_on_store(self): @@ -99,15 +101,15 @@ class TestSettings(TFCTestCase): self.assertIsNone(self.settings.change_setting('traffic_masking', 'True', *self.args)) def test_change_settings(self): - self.assert_fr("Error: Invalid value 'Falsee'.", + self.assert_fr("Error: Invalid setting value 'Falsee'.", self.settings.change_setting, 'disable_gui_dialog', 'Falsee', *self.args) - self.assert_fr("Error: Invalid value '1.1'.", + self.assert_fr("Error: Invalid setting value '1.1'.", self.settings.change_setting, 'max_number_of_group_members', '1.1', *self.args) - self.assert_fr("Error: Invalid value '18446744073709551616'.", + self.assert_fr("Error: Invalid setting value '18446744073709551616'.", self.settings.change_setting, 'max_number_of_contacts', str(2 ** 64), *self.args) - self.assert_fr("Error: Invalid value '-1.1'.", + self.assert_fr("Error: Invalid setting value '-1.1'.", self.settings.change_setting, 'tm_static_delay', '-1.1', *self.args) - self.assert_fr("Error: Invalid value 'True'.", + self.assert_fr("Error: Invalid setting value 'True'.", self.settings.change_setting, 'tm_static_delay', 'True', *self.args) self.assertIsNone(self.settings.change_setting('traffic_masking', 'True', *self.args)) diff --git a/tests/common/test_encoding.py b/tests/common/test_encoding.py index 285c493..fe5be16 100644 --- a/tests/common/test_encoding.py +++ b/tests/common/test_encoding.py @@ -29,12 +29,15 @@ from src.common.encoding import b58encode, bool_to_bytes, double_to_bytes, str_t from src.common.encoding import b58decode, bytes_to_bool, bytes_to_double, bytes_to_str, bytes_to_int from src.common.encoding import onion_address_to_pub_key, unicode_padding, pub_key_to_short_address, b85encode from src.common.encoding import pub_key_to_onion_address, rm_padding_str, bytes_to_timestamp, b10encode -from src.common.statics import * +from src.common.statics import (ENCODED_BOOLEAN_LENGTH, ENCODED_FLOAT_LENGTH, ENCODED_INTEGER_LENGTH, + FINGERPRINT_LENGTH, ONION_SERVICE_PUBLIC_KEY_LENGTH, PADDED_UTF32_STR_LENGTH, + PADDING_LENGTH, SYMMETRIC_KEY_LENGTH, TFC_PUBLIC_KEY_LENGTH, TRUNC_ADDRESS_LENGTH) class TestBase58EncodeAndDecode(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.key = SYMMETRIC_KEY_LENGTH * b'\x01' def test_encoding_and_decoding_of_random_local_keys(self): @@ -74,7 +77,7 @@ class TestBase58EncodeAndDecode(unittest.TestCase): byte_key = bytes.fromhex("0C28FCA386C7A227600B2FE50B7CAE11" "EC86D3BF1FBE471BE89827E19D72AA1D") - b58_key = "5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ" + b58_key = "5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ" self.assertEqual(b58encode(byte_key), b58_key) self.assertEqual(b58decode(b58_key), byte_key) diff --git a/tests/common/test_gateway.py b/tests/common/test_gateway.py index 0c72b7d..3d4d82b 100644 --- a/tests/common/test_gateway.py +++ b/tests/common/test_gateway.py @@ -33,7 +33,7 @@ from src.common.crypto import blake2b from src.common.gateway import gateway_loop, Gateway, GatewaySettings from src.common.misc import ensure_dir from src.common.reed_solomon import RSCodec -from src.common.statics import * +from src.common.statics import DIR_USER_DATA, GATEWAY_QUEUE, NC, PACKET_CHECKSUM_LENGTH, RX, TX from tests.mock_classes import Settings from tests.utils import cd_unit_test, cleanup, gen_queue_dict, tear_queues, TFCTestCase @@ -42,10 +42,12 @@ from tests.utils import cd_unit_test, cleanup, gen_queue_dict, tear_queue class TestGatewayLoop(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.queues = gen_queue_dict() def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) tear_queues(self.queues) @@ -63,10 +65,12 @@ class TestGatewayLoop(unittest.TestCase): class TestGatewaySerial(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.settings = Settings(session_usb_serial_adapter=True) def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) @mock.patch('time.sleep', return_value=None) @@ -239,6 +243,7 @@ class TestGatewaySerial(TFCTestCase): class TestGatewaySettings(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.default_serialized = """\ { @@ -249,6 +254,7 @@ class TestGatewaySettings(TFCTestCase): }""" def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) @mock.patch('os.listdir', side_effect=[['ttyUSB0'], ['ttyS0'], ['ttyUSB0'], ['ttyS0']]) @@ -462,13 +468,13 @@ class TestGatewaySettings(TFCTestCase): @mock.patch('time.sleep', return_value=None) def test_change_setting(self, _): settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True) - self.assert_fr("Error: Invalid value 'Falsee'.", + self.assert_fr("Error: Invalid setting value 'Falsee'.", settings.change_setting, 'serial_baudrate', 'Falsee') - self.assert_fr("Error: Invalid value '1.1'.", + self.assert_fr("Error: Invalid setting value '1.1'.", settings.change_setting, 'serial_baudrate', '1.1', ) - self.assert_fr("Error: Invalid value '18446744073709551616'.", + self.assert_fr("Error: Invalid setting value '18446744073709551616'.", settings.change_setting, 'serial_baudrate', str(2 ** 64)) - self.assert_fr("Error: Invalid value 'Falsee'.", + self.assert_fr("Error: Invalid setting value 'Falsee'.", settings.change_setting, 'use_serial_usb_adapter', 'Falsee') self.assertIsNone(settings.change_setting('serial_baudrate', '9600')) diff --git a/tests/common/test_input.py b/tests/common/test_input.py index bc29670..fe6e554 100644 --- a/tests/common/test_input.py +++ b/tests/common/test_input.py @@ -24,7 +24,8 @@ import unittest from unittest import mock from src.common.input import ask_confirmation_code, box_input, get_b58_key, nc_bypass_msg, pwd_prompt, yes -from src.common.statics import * +from src.common.statics import (B58_LOCAL_KEY, B58_PUBLIC_KEY, NC_BYPASS_START, NC_BYPASS_STOP, SYMMETRIC_KEY_LENGTH, + TFC_PUBLIC_KEY_LENGTH) from tests.mock_classes import Settings from tests.utils import nick_to_short_address, VALID_ECDHE_PUB_KEY, VALID_LOCAL_KEY_KDK @@ -53,6 +54,7 @@ class TestBoxInput(unittest.TestCase): class TestGetB58Key(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.settings = Settings() @mock.patch('time.sleep', return_value=None) diff --git a/tests/common/test_misc.py b/tests/common/test_misc.py index 5f1658e..9bcd0b0 100644 --- a/tests/common/test_misc.py +++ b/tests/common/test_misc.py @@ -35,7 +35,8 @@ from src.common.misc import get_tab_completer, get_terminal_height, get_termi from src.common.misc import process_arguments, readable_size, round_up, separate_header, separate_headers from src.common.misc import separate_trailer, split_string, split_byte_string, terminal_width_check from src.common.misc import validate_group_name, validate_key_exchange, validate_onion_addr, validate_nick -from src.common.statics import * +from src.common.statics import (DIR_RECV_FILES, DIR_USER_DATA, DUMMY_GROUP, ECDHE, EXIT, EXIT_QUEUE, LOCAL_ID, + PADDING_LENGTH, RX, TAILS, WIPE) from tests.mock_classes import ContactList, Gateway, GroupList, Settings from tests.utils import cd_unit_test, cleanup, gen_queue_dict, ignored, nick_to_onion_address @@ -45,6 +46,7 @@ from tests.utils import nick_to_pub_key, tear_queues, TFCTestCase class TestCalculateRaceConditionDelay(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.settings = Settings() def test_race_condition_delay_calculation(self): @@ -54,6 +56,7 @@ class TestCalculateRaceConditionDelay(unittest.TestCase): class TestDecompress(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.settings = Settings() self.settings.max_decompress_size = 1000 @@ -78,6 +81,7 @@ class TestDecompress(TFCTestCase): class TestEnsureDir(unittest.TestCase): def tearDown(self): + """Post-test actions.""" with ignored(OSError): os.rmdir('test_dir/') @@ -90,6 +94,7 @@ class TestEnsureDir(unittest.TestCase): class TestTabCompleteList(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.contact_list = ContactList(nicks=['Alice', 'Bob']) self.group_list = GroupList(groups=['test_group']) self.settings = Settings(key_list=['key1', 'key2']) @@ -145,10 +150,12 @@ class TestIgnored(unittest.TestCase): class TestMonitorProcesses(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.settings = Settings() def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) @staticmethod @@ -218,13 +225,13 @@ class TestMonitorProcesses(TFCTestCase): monitor_processes(process_list, RX, queues) self.assertFalse(os.path.isdir(DIR_USER_DATA)) self.assertFalse(os.path.isdir(DIR_RECV_FILES)) - mock_os_system.assert_called_with('poweroff') + mock_os_system.assert_called_with('systemctl poweroff') tear_queues(queues) @mock.patch('time.sleep', return_value=None) @mock.patch('os.system', return_value=None) - @mock.patch('subprocess.check_output', lambda *popenargs, timeout=None, **kwargs: TAILS) + @mock.patch('builtins.open', mock.mock_open(read_data=TAILS)) def test_wipe_tails(self, mock_os_system, *_): queues = gen_queue_dict() process_list = [Process(target=self.mock_process)] @@ -244,7 +251,7 @@ class TestMonitorProcesses(TFCTestCase): with self.assertRaises(SystemExit): monitor_processes(process_list, RX, queues) - mock_os_system.assert_called_with('poweroff') + mock_os_system.assert_called_with('systemctl poweroff') # Test that user data wasn't removed self.assertTrue(os.path.isdir(DIR_USER_DATA)) @@ -254,6 +261,8 @@ class TestMonitorProcesses(TFCTestCase): class TestProcessArguments(unittest.TestCase): def setUp(self): + """Pre-test actions.""" + class MockParser(object): """MockParse object.""" def __init__(self, *_, **__): @@ -280,6 +289,7 @@ class TestProcessArguments(unittest.TestCase): argparse.ArgumentParser = MockParser def tearDown(self): + """Post-test actions.""" argparse.ArgumentParser = self.o_argparse def test_process_arguments(self): @@ -429,6 +439,7 @@ class TestValidateOnionAddr(unittest.TestCase): class TestValidateGroupName(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.contact_list = ContactList(nicks=['Alice']) self.group_list = GroupList(groups=['test_group']) @@ -467,6 +478,7 @@ class TestValidateKeyExchange(unittest.TestCase): class TestValidateNick(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.contact_list = ContactList(nicks=['Alice', 'Bob']) self.group_list = GroupList(groups=['test_group']) diff --git a/tests/common/test_output.py b/tests/common/test_output.py index f7ea7a7..5dabdc7 100644 --- a/tests/common/test_output.py +++ b/tests/common/test_output.py @@ -26,7 +26,10 @@ from unittest import mock from src.common.output import clear_screen, group_management_print, m_print, phase, print_fingerprint, print_key from src.common.output import print_title, print_on_previous_line, print_spacing, rp_print -from src.common.statics import * +from src.common.statics import (ADDED_MEMBERS, ALREADY_MEMBER, BOLD_ON, CLEAR_ENTIRE_LINE, CLEAR_ENTIRE_SCREEN, + CURSOR_LEFT_UP_CORNER, CURSOR_UP_ONE_LINE, DONE, FINGERPRINT_LENGTH, NEW_GROUP, + NORMAL_TEXT, NOT_IN_GROUP, REMOVED_MEMBERS, RX, SYMMETRIC_KEY_LENGTH, TX, + UNKNOWN_ACCOUNTS, VERSION) from tests.mock_classes import ContactList, nick_to_pub_key, Settings from tests.utils import TFCTestCase @@ -41,6 +44,7 @@ class TestClearScreen(TFCTestCase): class TestGroupManagementPrint(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.contact_list = ContactList(nicks=['Alice']) self.lines = [nick_to_pub_key('Alice'), nick_to_pub_key('Bob')] self.group_name = 'test_group' @@ -238,6 +242,7 @@ class TestPrintFingerprint(TFCTestCase): class TestPrintKey(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.settings = Settings() def test_print_kdk(self): @@ -294,6 +299,7 @@ class TestPrintSpacing(TFCTestCase): class TestRPPrint(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.ts = datetime.now() self.timestamp = self.ts.strftime("%b %d - %H:%M:%S.%f")[:-4] diff --git a/tests/common/test_path.py b/tests/common/test_path.py index 5f11f35..9fe8361 100644 --- a/tests/common/test_path.py +++ b/tests/common/test_path.py @@ -38,6 +38,7 @@ class TestAskPathGui(TFCTestCase): path = '/home/user/' def setUp(self): + """Pre-test actions.""" self.settings = Settings() @mock.patch('os.path.isfile', return_value=True) @@ -78,6 +79,7 @@ class TestAskPathGui(TFCTestCase): class TestCompleter(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.cwd = os.getcwd() self.unit_test_dir = cd_unit_test() @@ -93,6 +95,7 @@ class TestCompleter(unittest.TestCase): os.chdir('..') def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) os.chdir(self.cwd) @@ -116,10 +119,12 @@ class TestCompleter(unittest.TestCase): class TestPath(TFCTestCase): def setUp(self): + """Pre-test actions.""" with ignored(FileExistsError): os.mkdir('test_dir/') def tearDown(self): + """Post-test actions.""" with ignored(OSError): os.remove('testfile') with ignored(OSError): diff --git a/tests/common/test_reed_solomon.py b/tests/common/test_reed_solomon.py index 7702042..f48e0f7 100644 --- a/tests/common/test_reed_solomon.py +++ b/tests/common/test_reed_solomon.py @@ -16,7 +16,10 @@ import unittest from random import sample -from src.common.reed_solomon import * +from src.common.reed_solomon import (RSCodec, ReedSolomonError, find_prime_polys, gf_add, gf_div, gf_mul, gf_mult_nolut, + gf_mult_nolut_slow, gf_neg, gf_poly_mul, gf_poly_mul_simple, gf_poly_neg, gf_sub, + init_tables, itertools, rs_check, rs_correct_msg, rs_correct_msg_nofsynd, + rs_encode_msg, rs_generator_poly, rs_generator_poly_all, rs_simple_encode_msg) class TestReedSolomon(unittest.TestCase): @@ -75,8 +78,8 @@ class TestReedSolomon(unittest.TestCase): kk = 18 tt = nn - kk rs = RSCodec(tt, fcr=120, prim=0x187) - hexencmsg = '00faa123555555c000000354064432' \ - 'c02800fe97c434e1ff5365cf8fafe4' + hexencmsg = ('00faa123555555c000000354064432' + 'c02800fe97c434e1ff5365cf8fafe4') strf = str encmsg = bytearray.fromhex(strf(hexencmsg)) decmsg = encmsg[:kk] @@ -108,8 +111,8 @@ class TestReedSolomon(unittest.TestCase): kk = 34 tt = nn - kk rs = RSCodec(tt, fcr=120, prim=0x187) - hexencmsg = '08faa123555555c000000354064432c0280e1b4d090cfc04' \ - '887400000003500000000e1985ff9c6b33066ca9f43d12e8' + hexencmsg = ('08faa123555555c000000354064432c0280e1b4d090cfc04' + '887400000003500000000e1985ff9c6b33066ca9f43d12e8') strf = str encmsg = bytearray.fromhex(strf(hexencmsg)) decmsg = encmsg[:kk] diff --git a/tests/common/test_word_list.py b/tests/common/test_word_list.py new file mode 100644 index 0000000..937c860 --- /dev/null +++ b/tests/common/test_word_list.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3.7 +# -*- coding: utf-8 -*- + +""" +TFC - Onion-routed, endpoint secure messaging system +Copyright (C) 2013-2019 Markus Ottela + +This file is part of TFC. + +TFC is free software: you can redistribute it and/or modify it under the terms +of the GNU General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +TFC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with TFC. If not, see . +""" + +import unittest + +from src.common.word_list import eff_wordlist + + +class TestWordList(unittest.TestCase): + + def test_each_word_is_unique(self): + self.assertEqual(len(eff_wordlist), + len(set(eff_wordlist))) + + def test_word_list_length(self): + self.assertEqual(len(eff_wordlist), + 7776) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/mock_classes.py b/tests/mock_classes.py index 8176447..a585990 100644 --- a/tests/mock_classes.py +++ b/tests/mock_classes.py @@ -41,7 +41,9 @@ from src.common.db_settings import Settings as OrigSettings from src.common.encoding import pub_key_to_onion_address, pub_key_to_short_address from src.common.misc import calculate_race_condition_delay from src.common.reed_solomon import RSCodec -from src.common.statics import * +from src.common.statics import (DIR_USER_DATA, FINGERPRINT_LENGTH, INITIAL_HARAC, KEX_STATUS_VERIFIED, LOCAL_ID, + LOCAL_NICK, LOCAL_PUBKEY, ONION_SERVICE_PRIVATE_KEY_LENGTH, SYMMETRIC_KEY_LENGTH, + TX, WIN_TYPE_GROUP, WIN_UID_LOCAL) from src.transmitter.windows import TxWindow as OrigTxWindow @@ -209,11 +211,13 @@ class MasterKey(OrigMasterKey): setattr(self, key, value) def load_master_key(self) -> bytes: + """Create mock master key bytes.""" if getpass.getpass() == 'test_password': return self.master_key else: return SYMMETRIC_KEY_LENGTH * b'f' + class OnionService(OrigOnionService): """Mock the object for unit testing.""" diff --git a/tests/receiver/test_commands.py b/tests/receiver/test_commands.py index 1cb4ba6..9e0c38e 100644 --- a/tests/receiver/test_commands.py +++ b/tests/receiver/test_commands.py @@ -30,7 +30,10 @@ from unittest.mock import MagicMock from src.common.db_logs import write_log_entry from src.common.encoding import int_to_bytes -from src.common.statics import * +from src.common.statics import (CH_FILE_RECV, CH_LOGGING, CH_NOTIFY, CLEAR_ENTIRE_LINE, COMMAND, CURSOR_UP_ONE_LINE, + C_L_HEADER, DISABLE, ENABLE, F_S_HEADER, LOCAL_ID, LOCAL_PUBKEY, LOG_REMOVE, MESSAGE, + ORIGIN_CONTACT_HEADER, PADDING_LENGTH, RESET, RX, SYMMETRIC_KEY_LENGTH, US_BYTE, + WIN_TYPE_CONTACT, WIN_TYPE_GROUP, WIN_UID_FILE, WIPE) from src.receiver.packet import PacketList from src.receiver.commands import ch_contact_s, ch_master_key, ch_nick, ch_setting, contact_rem, exit_tfc, log_command @@ -38,13 +41,14 @@ from src.receiver.commands import process_command, remove_log, reset_screen, win from tests.mock_classes import ContactList, Gateway, group_name_to_group_id, GroupList, KeyList, MasterKey from tests.mock_classes import nick_to_pub_key, RxWindow, Settings, WindowList -from tests.utils import assembly_packet_creator, cd_unit_test, cleanup, ignored, nick_to_short_address, tear_queue -from tests.utils import TFCTestCase +from tests.utils import assembly_packet_creator, cd_unit_test, cleanup, ignored, nick_to_short_address +from tests.utils import tear_queue, TFCTestCase class TestProcessCommand(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.ts = datetime.now() self.settings = Settings() @@ -62,6 +66,7 @@ class TestProcessCommand(TFCTestCase): self.settings, self.master_key, self.gateway, self.exit_queue) def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) tear_queue(self.exit_queue) @@ -81,6 +86,7 @@ class TestProcessCommand(TFCTestCase): class TestWinActivity(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.window_list = WindowList() self.window_list.windows = [RxWindow(name='Alice', unread_messages=4), RxWindow(name='Bob', unread_messages=15)] @@ -99,6 +105,7 @@ class TestWinActivity(TFCTestCase): class TestWinSelect(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.window_list = WindowList() self.window_list.windows = [RxWindow(uid=nick_to_pub_key("Alice"), name='Alice'), RxWindow(uid=nick_to_pub_key("Bob"), name='Bob')] @@ -117,6 +124,7 @@ class TestWinSelect(unittest.TestCase): class TestResetScreen(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.cmd_data = nick_to_pub_key("Alice") self.window_list = WindowList() self.window_list.windows = [RxWindow(uid=nick_to_pub_key("Alice"), name='Alice'), @@ -141,9 +149,11 @@ class TestResetScreen(unittest.TestCase): class TestExitTFC(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.exit_queue = Queue() def tearDown(self): + """Post-test actions.""" tear_queue(self.exit_queue) def test_function(self): @@ -154,6 +164,7 @@ class TestExitTFC(unittest.TestCase): class TestLogCommand(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.cmd_data = int_to_bytes(1) + nick_to_pub_key("Bob") self.ts = datetime.now() @@ -173,6 +184,7 @@ class TestLogCommand(TFCTestCase): self.time = datetime.fromtimestamp(time_float).strftime("%H:%M:%S.%f")[:-4] def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) with ignored(OSError): os.remove('Receiver - Plaintext log (None)') @@ -204,6 +216,7 @@ Log file of 1 most recent message(s) to/from contact Bob class TestRemoveLog(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.win_name = nick_to_pub_key("Alice") self.contact_list = ContactList() @@ -212,6 +225,7 @@ class TestRemoveLog(TFCTestCase): self.master_key = MasterKey() def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) def test_remove_log_file(self): @@ -222,6 +236,7 @@ class TestRemoveLog(TFCTestCase): class TestChMasterKey(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.ts = datetime.now() self.master_key = MasterKey() @@ -234,11 +249,13 @@ class TestChMasterKey(TFCTestCase): self.key_list, self.settings, self.master_key) def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) @mock.patch('src.common.db_masterkey.MIN_KEY_DERIVATION_TIME', 0.1) @mock.patch('src.common.db_masterkey.MIN_KEY_DERIVATION_TIME', 1.0) - @mock.patch('os.popen', return_value=MagicMock(read=MagicMock(return_value=MagicMock(splitlines=MagicMock(return_value=["MemFree 10240"]))))) + @mock.patch('os.popen', return_value=MagicMock( + read=MagicMock(return_value=MagicMock(splitlines=MagicMock(return_value=["MemAvailable 10240"]))))) @mock.patch('multiprocessing.cpu_count', return_value=1) @mock.patch('getpass.getpass', return_value='a') @mock.patch('time.sleep', return_value=None) @@ -261,6 +278,7 @@ class TestChMasterKey(TFCTestCase): class TestChNick(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.ts = datetime.now() self.contact_list = ContactList(nicks=['Alice']) self.window_list = WindowList(contact_list=self.contact_list) @@ -290,6 +308,7 @@ class TestChNick(TFCTestCase): class TestChSetting(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.ts = datetime.now() self.window_list = WindowList() self.contact_list = ContactList() @@ -339,6 +358,7 @@ class TestChSetting(TFCTestCase): class TestChContactSetting(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.ts = datetime.fromtimestamp(1502750000) self.contact_list = ContactList(nicks=['Alice', 'Bob']) self.group_list = GroupList(groups=['test_group', 'test_group2']) @@ -425,6 +445,7 @@ class TestChContactSetting(TFCTestCase): class TestContactRemove(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.ts = datetime.now() self.window_list = WindowList() @@ -434,6 +455,7 @@ class TestContactRemove(TFCTestCase): self.args = self.cmd_data, self.ts, self.window_list def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) def test_no_contact_raises_fr(self): @@ -466,9 +488,11 @@ class TestContactRemove(TFCTestCase): class TestWipe(unittest.TestCase): def setUp(self) -> None: + """Pre-test actions.""" self.exit_queue = Queue() def tearDown(self) -> None: + """Post-test actions.""" tear_queue(self.exit_queue) @mock.patch('os.system', return_value=None) diff --git a/tests/receiver/test_commands_g.py b/tests/receiver/test_commands_g.py index 11340ed..6a2d4a0 100644 --- a/tests/receiver/test_commands_g.py +++ b/tests/receiver/test_commands_g.py @@ -22,7 +22,8 @@ along with TFC. If not, see . import datetime import unittest -from src.common.statics import * +from src.common.statics import US_BYTE + from src.receiver.commands_g import group_add, group_create, group_delete, group_remove, group_rename from tests.mock_classes import Contact, ContactList, GroupList, RxWindow, Settings, WindowList @@ -32,6 +33,7 @@ from tests.utils import group_name_to_group_id, nick_to_pub_key, TFCTestC class TestGroupCreate(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.ts = datetime.datetime.now() self.settings = Settings() self.window_list = WindowList() @@ -78,6 +80,7 @@ class TestGroupCreate(TFCTestCase): class TestGroupAdd(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.ts = datetime.datetime.now() self.settings = Settings() self.window_list = WindowList() @@ -125,6 +128,7 @@ class TestGroupAdd(TFCTestCase): class TestGroupRemove(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.ts = datetime.datetime.now() self.window_list = WindowList() self.contact_list = ContactList(nicks=[f"contact_{n}" for n in range(21)]) @@ -152,6 +156,7 @@ class TestGroupRemove(TFCTestCase): class TestGroupDelete(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.ts = datetime.datetime.now() self.window_list = WindowList() self.group_list = GroupList(groups=['test_group']) @@ -179,6 +184,7 @@ class TestGroupDelete(TFCTestCase): class TestGroupRename(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.ts = datetime.datetime.now() self.group_list = GroupList(groups=['test_group']) self.window_list = WindowList() @@ -186,7 +192,7 @@ class TestGroupRename(TFCTestCase): self.window_list.windows = [self.window] self.contact_list = ContactList(nicks=['alice']) self.args = self.ts, self.window_list, self.contact_list, self.group_list - + def test_missing_group_id_raises_fr(self): # Setup cmd_data = group_name_to_group_id('test_group2') + b'new_name' diff --git a/tests/receiver/test_files.py b/tests/receiver/test_files.py index a98c8f1..5e5abb7 100644 --- a/tests/receiver/test_files.py +++ b/tests/receiver/test_files.py @@ -28,7 +28,7 @@ from unittest import mock from src.common.crypto import blake2b, encrypt_and_sign from src.common.encoding import str_to_bytes -from src.common.statics import * +from src.common.statics import COMPRESSION_LEVEL, DIR_RECV_FILES, ORIGIN_CONTACT_HEADER, SYMMETRIC_KEY_LENGTH, US_BYTE from src.receiver.files import new_file, process_assembled_file, process_file, store_unique @@ -39,12 +39,14 @@ from tests.utils import cd_unit_test, cleanup, nick_to_pub_key, TFCTestCa class TestStoreUnique(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.file_data = os.urandom(100) self.file_dir = 'test_dir/' self.file_name = 'test_file' def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) def test_each_file_is_store_with_unique_name(self): @@ -56,16 +58,18 @@ class TestStoreUnique(unittest.TestCase): class ProcessAssembledFile(TFCTestCase): def setUp(self): - self.unit_test_dir = cd_unit_test() - self.ts = datetime.now() - self.onion_pub_key = nick_to_pub_key('Alice') - self.nick = 'Alice' - self.settings = Settings() - self.window_list = WindowList(nick=['Alice', 'Bob']) - self.key = os.urandom(SYMMETRIC_KEY_LENGTH) - self.args = self.onion_pub_key, self.nick, self.settings, self.window_list + """Pre-test actions.""" + self.unit_test_dir = cd_unit_test() + self.ts = datetime.now() + self.onion_pub_key = nick_to_pub_key('Alice') + self.nick = 'Alice' + self.settings = Settings() + self.window_list = WindowList(nick=['Alice', 'Bob']) + self.key = os.urandom(SYMMETRIC_KEY_LENGTH) + self.args = self.onion_pub_key, self.nick, self.settings, self.window_list def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) def test_invalid_structure_raises_fr(self): @@ -156,6 +160,7 @@ class ProcessAssembledFile(TFCTestCase): class TestNewFile(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.ts = datetime.now() self.packet = b'' @@ -169,6 +174,7 @@ class TestNewFile(TFCTestCase): self.args = self.file_keys, self.file_buf, self.contact_list, self.window_list, self.settings def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) def test_unknown_account_raises_fr(self): @@ -215,6 +221,7 @@ class TestNewFile(TFCTestCase): class TestProcessFile(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.ts = datetime.now() self.account = nick_to_pub_key('Alice') @@ -226,6 +233,7 @@ class TestProcessFile(TFCTestCase): self.args = self.file_key, self.contact_list, self.window_list, self.settings def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) def test_invalid_key_raises_fr(self): diff --git a/tests/receiver/test_key_exchanges.py b/tests/receiver/test_key_exchanges.py index edc04d8..0dcb7b9 100644 --- a/tests/receiver/test_key_exchanges.py +++ b/tests/receiver/test_key_exchanges.py @@ -32,7 +32,9 @@ from unittest.mock import MagicMock from src.common.crypto import argon2_kdf, encrypt_and_sign from src.common.encoding import b58encode, str_to_bytes from src.common.exceptions import FunctionReturn -from src.common.statics import * +from src.common.statics import (ARGON2_SALT_LENGTH, BOLD_ON, CLEAR_ENTIRE_SCREEN, CONFIRM_CODE_LENGTH, + CURSOR_LEFT_UP_CORNER, FINGERPRINT_LENGTH, LOCAL_ID, NORMAL_TEXT, PSK_FILE_SIZE, + SYMMETRIC_KEY_LENGTH, WIN_TYPE_CONTACT, WIN_UID_LOCAL, XCHACHA20_NONCE_LENGTH) from src.receiver.key_exchanges import key_ex_ecdhe, key_ex_psk_rx, key_ex_psk_tx, local_key_rdy, process_local_key @@ -47,6 +49,7 @@ class TestProcessLocalKey(TFCTestCase): new_kek = os.urandom(SYMMETRIC_KEY_LENGTH) def setUp(self): + """Pre-test actions.""" self.contact_list = ContactList(nicks=[LOCAL_ID, 'Alice']) self.key_list = KeyList( nicks=[LOCAL_ID, 'Alice']) self.window_list = WindowList( nicks=[LOCAL_ID, 'Alice']) @@ -59,10 +62,11 @@ class TestProcessLocalKey(TFCTestCase): self.hek = os.urandom(SYMMETRIC_KEY_LENGTH) self.conf_code = os.urandom(CONFIRM_CODE_LENGTH) self.packet = encrypt_and_sign(self.key + self.hek + self.conf_code, key=self.kek) - self.args = (self.window_list, self.contact_list, self.key_list, self.settings, + self.args = (self.window_list, self.contact_list, self.key_list, self.settings, self.kdk_hashes, self.packet_hashes, self.l_queue) def tearDown(self): + """Post-test actions.""" tear_queue(self.l_queue) @mock.patch('tkinter.Tk', return_value=MagicMock()) @@ -150,6 +154,7 @@ class TestProcessLocalKey(TFCTestCase): class TestLocalKeyRdy(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.ts = datetime.fromtimestamp(1502750000) @mock.patch('time.sleep', return_value=None) @@ -181,6 +186,7 @@ class TestLocalKeyRdy(TFCTestCase): class TestKeyExECDHE(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.ts = datetime.fromtimestamp(1502750000) self.window_list = WindowList(nicks=[LOCAL_ID]) self.contact_list = ContactList() @@ -230,6 +236,7 @@ class TestKeyExECDHE(TFCTestCase): class TestKeyExPSKTx(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.ts = datetime.fromtimestamp(1502750000) self.window_list = WindowList(nicks=[LOCAL_ID]) self.contact_list = ContactList() @@ -282,6 +289,7 @@ class TestKeyExPSKRx(TFCTestCase): file_name = f"{nick_to_short_address('User')}.psk - give to {nick_to_short_address('Alice')}" def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.packet = b'\x00' + nick_to_pub_key("Alice") self.ts = datetime.now() @@ -293,6 +301,7 @@ class TestKeyExPSKRx(TFCTestCase): self.args = self.packet, self.ts, self.window_list, self.contact_list, self.key_list, self.settings def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) def test_unknown_account_raises_fr(self): @@ -359,14 +368,14 @@ class TestKeyExPSKRx(TFCTestCase): @mock.patch('getpass.getpass', return_value='test_password') def test_valid_psk(self, *_): # Setup - keyset = self.key_list.get_keyset(nick_to_pub_key("Alice")) - keyset.rx_mk = bytes(SYMMETRIC_KEY_LENGTH) - keyset.rx_hk = bytes(SYMMETRIC_KEY_LENGTH) - salt = os.urandom(ARGON2_SALT_LENGTH) - rx_key = os.urandom(SYMMETRIC_KEY_LENGTH) - rx_hek = os.urandom(SYMMETRIC_KEY_LENGTH) - kek = argon2_kdf('test_password', salt, time_cost=1, memory_cost=100, parallelism=1) - ct_tag = encrypt_and_sign(rx_key + rx_hek, key=kek) + keyset = self.key_list.get_keyset(nick_to_pub_key("Alice")) + keyset.rx_mk = bytes(SYMMETRIC_KEY_LENGTH) + keyset.rx_hk = bytes(SYMMETRIC_KEY_LENGTH) + salt = os.urandom(ARGON2_SALT_LENGTH) + rx_key = os.urandom(SYMMETRIC_KEY_LENGTH) + rx_hek = os.urandom(SYMMETRIC_KEY_LENGTH) + kek = argon2_kdf('test_password', salt, time_cost=1, memory_cost=100, parallelism=1) + ct_tag = encrypt_and_sign(rx_key + rx_hek, key=kek) with open(self.file_name, 'wb+') as f: f.write(salt + ct_tag) @@ -387,9 +396,9 @@ class TestKeyExPSKRx(TFCTestCase): @mock.patch('getpass.getpass', return_value='test_password') def test_valid_psk_overwrite_failure(self, *_): # Setup - keyset = self.key_list.get_keyset(nick_to_pub_key("Alice")) - keyset.rx_mk = bytes(SYMMETRIC_KEY_LENGTH) - keyset.rx_hk = bytes(SYMMETRIC_KEY_LENGTH) + keyset = self.key_list.get_keyset(nick_to_pub_key("Alice")) + keyset.rx_mk = bytes(SYMMETRIC_KEY_LENGTH) + keyset.rx_hk = bytes(SYMMETRIC_KEY_LENGTH) salt = os.urandom(ARGON2_SALT_LENGTH) rx_key = os.urandom(SYMMETRIC_KEY_LENGTH) diff --git a/tests/receiver/test_messages.py b/tests/receiver/test_messages.py index 2588758..b165905 100644 --- a/tests/receiver/test_messages.py +++ b/tests/receiver/test_messages.py @@ -28,7 +28,9 @@ from unittest import mock from src.common.encoding import bool_to_bytes from src.common.misc import ensure_dir -from src.common.statics import * +from src.common.statics import (BLAKE2_DIGEST_LENGTH, DIR_USER_DATA, FILE, FILE_KEY_HEADER, GROUP_ID_LENGTH, LOCAL_ID, + LOCAL_PUBKEY, LOG_ENTRY_LENGTH, MESSAGE, MESSAGE_LENGTH, ORIGIN_CONTACT_HEADER, + ORIGIN_USER_HEADER, SYMMETRIC_KEY_LENGTH) from src.receiver.messages import process_message from src.receiver.packet import PacketList @@ -42,6 +44,7 @@ from tests.utils import nick_to_pub_key, TFCTestCase class TestProcessMessage(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.msg = ("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean condimentum consectetur purus quis" @@ -64,18 +67,19 @@ class TestProcessMessage(TFCTestCase): self.key_list = KeyList( nicks=['Alice', 'Bob', 'Charlie', LOCAL_ID]) self.group_list = GroupList( groups=['test_group']) self.packet_list = PacketList(contact_list=self.contact_list, settings=self.settings) - self.window_list = WindowList(contact_list=self.contact_list, settings=self.settings, + self.window_list = WindowList(contact_list=self.contact_list, settings=self.settings, group_list=self.group_list, packet_list=self.packet_list) self.group_id = group_name_to_group_id('test_group') self.file_keys = dict() self.group_list.get_group('test_group').log_messages = True - self.args = (self.window_list, self.packet_list, self.contact_list, self.key_list, + self.args = (self.window_list, self.packet_list, self.contact_list, self.key_list, self.group_list, self.settings, self.master_key, self.file_keys) ensure_dir(DIR_USER_DATA) def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) # Invalid packets diff --git a/tests/receiver/test_output_loop.py b/tests/receiver/test_output_loop.py index cd7762b..2f91d62 100644 --- a/tests/receiver/test_output_loop.py +++ b/tests/receiver/test_output_loop.py @@ -31,7 +31,11 @@ from unittest.mock import MagicMock from src.common.crypto import blake2b, encrypt_and_sign from src.common.encoding import b58encode, bool_to_bytes, int_to_bytes, str_to_bytes -from src.common.statics import * +from src.common.statics import (CH_FILE_RECV, COMMAND, COMMAND_DATAGRAM_HEADER, CONFIRM_CODE_LENGTH, ENABLE, EXIT, + FILE_DATAGRAM_HEADER, FILE_KEY_HEADER, INITIAL_HARAC, KEY_EX_ECDHE, + LOCAL_KEY_DATAGRAM_HEADER, MESSAGE, MESSAGE_DATAGRAM_HEADER, ORIGIN_CONTACT_HEADER, + PRIVATE_MESSAGE_HEADER, SYMMETRIC_KEY_LENGTH, UNIT_TEST_QUEUE, US_BYTE, WIN_SELECT, + WIN_UID_FILE, WIN_UID_LOCAL) from src.transmitter.packet import split_to_assembly_packets @@ -49,10 +53,12 @@ def rotate_key(key: bytes, harac: int) -> Tuple[bytes, int]: class TestOutputLoop(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.o_sleep = time.sleep time.sleep = lambda _: None def tearDown(self): + """Post-test actions.""" time.sleep = self.o_sleep @mock.patch('tkinter.Tk', return_value=MagicMock()) diff --git a/tests/receiver/test_packet.py b/tests/receiver/test_packet.py index 03476a6..2ebd440 100644 --- a/tests/receiver/test_packet.py +++ b/tests/receiver/test_packet.py @@ -28,7 +28,9 @@ from unittest import mock from src.common.crypto import byte_padding, encrypt_and_sign from src.common.encoding import int_to_bytes -from src.common.statics import * +from src.common.statics import (COMMAND, COMPRESSION_LEVEL, DIR_RECV_FILES, FILE, F_C_HEADER, LOCAL_ID, MESSAGE, + M_A_HEADER, M_E_HEADER, ORIGIN_CONTACT_HEADER, ORIGIN_USER_HEADER, PADDING_LENGTH, + PRIVATE_MESSAGE_HEADER, P_N_HEADER, SYMMETRIC_KEY_LENGTH, US_BYTE) from src.transmitter.packet import split_to_assembly_packets @@ -42,6 +44,7 @@ from tests.utils import UNDECODABLE_UNICODE class TestDecryptAssemblyPacket(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.onion_pub_key = nick_to_pub_key("Alice") self.origin = ORIGIN_CONTACT_HEADER self.window_list = WindowList(nicks=['Alice', LOCAL_ID]) @@ -113,6 +116,7 @@ class TestDecryptAssemblyPacket(TFCTestCase): class TestPacket(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.short_msg = "Lorem ipsum dolor sit amet, consectetur adipiscing elit" self.msg = ("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean condimentum consectetur purus quis" " dapibus. Fusce venenatis lacus ut rhoncus faucibus. Cras sollicitudin commodo sapien, sed bibendu" @@ -140,6 +144,7 @@ class TestPacket(TFCTestCase): self.short_f_data = (int_to_bytes(1) + int_to_bytes(2) + b'testfile.txt' + US_BYTE + encrypted) def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) def test_invalid_assembly_packet_header_raises_fr(self): @@ -412,6 +417,7 @@ class TestPacket(TFCTestCase): class TestPacketList(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.contact_list = ContactList(nicks=['Alice', 'Bob']) self.settings = Settings() self.onion_pub_key = nick_to_pub_key('Alice') diff --git a/tests/receiver/test_receiver_loop.py b/tests/receiver/test_receiver_loop.py index 6179475..7808f7d 100644 --- a/tests/receiver/test_receiver_loop.py +++ b/tests/receiver/test_receiver_loop.py @@ -28,7 +28,9 @@ from multiprocessing import Queue from src.common.encoding import int_to_bytes from src.common.reed_solomon import RSCodec -from src.common.statics import * +from src.common.statics import (COMMAND_DATAGRAM_HEADER, FILE_DATAGRAM_HEADER, GATEWAY_QUEUE, + LOCAL_KEY_DATAGRAM_HEADER, MESSAGE_DATAGRAM_HEADER, + ONION_SERVICE_PUBLIC_KEY_LENGTH) from src.receiver.receiver_loop import receiver_loop @@ -54,9 +56,9 @@ class TestReceiverLoop(unittest.TestCase): ts_bytes = int_to_bytes(int(ts.strftime('%Y%m%d%H%M%S%f')[:-4])) for key in queues: - packet = key + ts_bytes + bytes(ONION_SERVICE_PUBLIC_KEY_LENGTH) - encoded = rs.encode(packet) - broken_p = key + bytes.fromhex('df9005313af4136d') + bytes(ONION_SERVICE_PUBLIC_KEY_LENGTH) + packet = key + ts_bytes + bytes(ONION_SERVICE_PUBLIC_KEY_LENGTH) + encoded = rs.encode(packet) + broken_p = key + bytes.fromhex('df9005313af4136d') + bytes(ONION_SERVICE_PUBLIC_KEY_LENGTH) broken_p += rs.encode(b'a') def queue_delayer(): diff --git a/tests/receiver/test_windows.py b/tests/receiver/test_windows.py index ac709dc..dad5497 100644 --- a/tests/receiver/test_windows.py +++ b/tests/receiver/test_windows.py @@ -24,7 +24,11 @@ import unittest from datetime import datetime from unittest import mock -from src.common.statics import * +from src.common.statics import (BOLD_ON, CLEAR_ENTIRE_LINE, CLEAR_ENTIRE_SCREEN, CURSOR_LEFT_UP_CORNER, + CURSOR_UP_ONE_LINE, FILE, GROUP_ID_LENGTH, LOCAL_ID, NORMAL_TEXT, + ONION_SERVICE_PUBLIC_KEY_LENGTH, ORIGIN_CONTACT_HEADER, ORIGIN_USER_HEADER, + WIN_TYPE_COMMAND, WIN_TYPE_CONTACT, WIN_TYPE_FILE, WIN_TYPE_GROUP, WIN_UID_FILE, + WIN_UID_LOCAL) from src.receiver.windows import RxWindow, WindowList @@ -35,6 +39,7 @@ from tests.utils import group_name_to_group_id, nick_to_pub_key, nick_to_ class TestRxWindow(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.contact_list = ContactList(nicks=['Alice', 'Bob', 'Charlie', LOCAL_ID]) self.group_list = GroupList(groups=['test_group', 'test_group2']) self.settings = Settings() @@ -71,7 +76,14 @@ class TestRxWindow(TFCTestCase): self.assertEqual(window.name, 'test_group') def test_invalid_uid_raises_fr(self): - self.assert_fr("Invalid window 'bad_uid'.", self.create_window, 'bad_uid') + self.assert_fr("Invalid window 'mfqwcylbmfqwcylbmfqwcylbmfqwcylbmfqwcylbmfqwcylbmfqwbfad'.", + self.create_window, ONION_SERVICE_PUBLIC_KEY_LENGTH*b'a') + + self.assert_fr("Invalid window '2dnAMoWNfTXAJ'.", + self.create_window, GROUP_ID_LENGTH*b'a') + + self.assert_fr("Invalid window ''.", + self.create_window, b'bad_uid') def test_window_iterates_over_message_tuples(self): # Setup @@ -373,6 +385,7 @@ testfile2.txt 15.0KB Charlie 7.00% class TestWindowList(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.settings = Settings() self.contact_list = ContactList(nicks=['Alice', 'Bob', 'Charlie', LOCAL_ID]) self.group_list = GroupList(groups=['test_group', 'test_group2']) diff --git a/tests/relay/test_client.py b/tests/relay/test_client.py index 87cabdf..57f42e0 100644 --- a/tests/relay/test_client.py +++ b/tests/relay/test_client.py @@ -31,7 +31,13 @@ import requests from src.common.crypto import X448 from src.common.db_onion import pub_key_to_onion_address, pub_key_to_short_address -from src.common.statics import * +from src.common.statics import (CONTACT_MGMT_QUEUE, CONTACT_REQ_QUEUE, C_REQ_MGMT_QUEUE, C_REQ_STATE_QUEUE, + DST_MESSAGE_QUEUE, EXIT, GROUP_ID_LENGTH, GROUP_MGMT_QUEUE, + GROUP_MSG_EXIT_GROUP_HEADER, GROUP_MSG_INVITE_HEADER, GROUP_MSG_JOIN_HEADER, + GROUP_MSG_MEMBER_ADD_HEADER, GROUP_MSG_MEMBER_REM_HEADER, GROUP_MSG_QUEUE, + MESSAGE_DATAGRAM_HEADER, ONION_SERVICE_PUBLIC_KEY_LENGTH, PUBLIC_KEY_DATAGRAM_HEADER, + RP_ADD_CONTACT_HEADER, RP_REMOVE_CONTACT_HEADER, TFC_PUBLIC_KEY_LENGTH, TOR_DATA_QUEUE, + UNIT_TEST_QUEUE, URL_TOKEN_QUEUE) from src.relay.client import c_req_manager, client, client_scheduler, g_msg_manager, get_data_loop @@ -107,11 +113,13 @@ class TestClient(unittest.TestCase): return TestClient.MockSession() def setUp(self): + """Pre-test actions.""" self.o_session = requests.session self.queues = gen_queue_dict() requests.session = TestClient.mock_session def tearDown(self): + """Post-test actions.""" requests.session = self.o_session tear_queues(self.queues) @@ -243,11 +251,13 @@ class TestGetDataLoop(unittest.TestCase): return TestGetDataLoop.Session() def setUp(self): + """Pre-test actions.""" self.o_session = requests.session self.queues = gen_queue_dict() requests.session = TestGetDataLoop.mock_session def tearDown(self): + """Post-test actions.""" requests.session = self.o_session tear_queues(self.queues) diff --git a/tests/relay/test_commands.py b/tests/relay/test_commands.py index 5a8da51..ad3b1cf 100644 --- a/tests/relay/test_commands.py +++ b/tests/relay/test_commands.py @@ -28,7 +28,11 @@ from unittest import mock from unittest.mock import MagicMock from src.common.encoding import int_to_bytes -from src.common.statics import * +from src.common.statics import (CLEAR_ENTIRE_SCREEN, CONTACT_MGMT_QUEUE, CURSOR_LEFT_UP_CORNER, C_REQ_MGMT_QUEUE, + C_REQ_STATE_QUEUE, EXIT, GROUP_MGMT_QUEUE, LOCAL_TESTING_PACKET_DELAY, + ONION_CLOSE_QUEUE, ONION_KEY_QUEUE, ONION_SERVICE_PRIVATE_KEY_LENGTH, + RP_ADD_CONTACT_HEADER, RP_REMOVE_CONTACT_HEADER, SRC_TO_RELAY_QUEUE, + UNENCRYPTED_SCREEN_CLEAR, WIPE) from src.relay.commands import add_contact, add_onion_data, change_baudrate, change_ec_ratio, clear_windows, exit_tfc from src.relay.commands import manage_contact_req, process_command, race_condition_delay, relay_command, remove_contact @@ -41,11 +45,13 @@ from tests.utils import gen_queue_dict, tear_queues, TFCTestCase class TestRelayCommand(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.gateway = Gateway() self.queues = gen_queue_dict() self.gateway.settings.race_condition_delay = 0.0 def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) @mock.patch('sys.stdin', MagicMock()) @@ -64,10 +70,12 @@ class TestRelayCommand(unittest.TestCase): class TestProcessCommand(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.gateway = Gateway() self.queues = gen_queue_dict() def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def test_invalid_key(self): @@ -77,6 +85,7 @@ class TestProcessCommand(TFCTestCase): class TestRaceConditionDelay(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.gateway = Gateway(local_testing_mode=True, data_diode_sockets=True) @@ -89,6 +98,7 @@ class TestRaceConditionDelay(unittest.TestCase): class TestClearWindows(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.gateway = Gateway(race_condition_delay=0.0) def test_clear_display(self): @@ -106,10 +116,12 @@ class TestResetWindows(TFCTestCase): class TestExitTFC(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.gateway = Gateway(race_condition_delay=0.0) self.queues = gen_queue_dict() def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def test_exit_tfc(self): @@ -120,6 +132,7 @@ class TestExitTFC(unittest.TestCase): class TestChangeECRatio(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.gateway = Gateway() def test_non_digit_value_raises_fr(self): @@ -138,6 +151,7 @@ class TestChangeECRatio(TFCTestCase): class TestChangeBaudrate(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.gateway = Gateway() def test_non_digit_value_raises_fr(self): @@ -156,10 +170,12 @@ class TestChangeBaudrate(TFCTestCase): class TestWipe(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.gateway = Gateway(race_condition_delay=0.0) self.queues = gen_queue_dict() def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) @mock.patch('os.system', return_value=None) @@ -171,9 +187,11 @@ class TestWipe(unittest.TestCase): class TestManageContactReq(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.queues = gen_queue_dict() def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def test_setting_management(self): @@ -186,68 +204,74 @@ class TestManageContactReq(unittest.TestCase): class TestAddContact(unittest.TestCase): - def setUp(self): - self.queues = gen_queue_dict() + def setUp(self): + """Pre-test actions.""" + self.queues = gen_queue_dict() - def tearDown(self): - tear_queues(self.queues) + def tearDown(self): + """Post-test actions.""" + tear_queues(self.queues) - def test_add_contact(self): - command = b''.join([nick_to_pub_key('Alice'), nick_to_pub_key('Bob')]) + def test_add_contact(self): + command = b''.join([nick_to_pub_key('Alice'), nick_to_pub_key('Bob')]) - self.assertIsNone(add_contact(command, True, self.queues)) - self.assertEqual(self.queues[CONTACT_MGMT_QUEUE].qsize(), 1) - for q in [GROUP_MGMT_QUEUE, C_REQ_MGMT_QUEUE]: - command = self.queues[q].get() - self.assertEqual(command, - (RP_ADD_CONTACT_HEADER, b''.join([nick_to_pub_key('Alice'), nick_to_pub_key('Bob')]))) - self.assertEqual(self.queues[CONTACT_MGMT_QUEUE].get(), - (RP_ADD_CONTACT_HEADER, b''.join(list(map(nick_to_pub_key, ['Alice', 'Bob']))), True)) + self.assertIsNone(add_contact(command, True, self.queues)) + self.assertEqual(self.queues[CONTACT_MGMT_QUEUE].qsize(), 1) + for q in [GROUP_MGMT_QUEUE, C_REQ_MGMT_QUEUE]: + command = self.queues[q].get() + self.assertEqual(command, + (RP_ADD_CONTACT_HEADER, b''.join([nick_to_pub_key('Alice'), nick_to_pub_key('Bob')]))) + self.assertEqual(self.queues[CONTACT_MGMT_QUEUE].get(), + (RP_ADD_CONTACT_HEADER, b''.join(list(map(nick_to_pub_key, ['Alice', 'Bob']))), True)) class TestRemContact(unittest.TestCase): - def setUp(self): - self.queues = gen_queue_dict() + def setUp(self): + """Pre-test actions.""" + self.queues = gen_queue_dict() - def tearDown(self): - tear_queues(self.queues) + def tearDown(self): + """Post-test actions.""" + tear_queues(self.queues) - def test_add_contact(self): - command = b''.join([nick_to_pub_key('Alice'), nick_to_pub_key('Bob')]) + def test_add_contact(self): + command = b''.join([nick_to_pub_key('Alice'), nick_to_pub_key('Bob')]) - self.assertIsNone(remove_contact(command, self.queues)) - self.assertEqual(self.queues[CONTACT_MGMT_QUEUE].qsize(), 1) - self.assertEqual(self.queues[CONTACT_MGMT_QUEUE].get(), - (RP_REMOVE_CONTACT_HEADER, - b''.join([nick_to_pub_key('Alice'), nick_to_pub_key('Bob')]), - False) - ) + self.assertIsNone(remove_contact(command, self.queues)) + self.assertEqual(self.queues[CONTACT_MGMT_QUEUE].qsize(), 1) + self.assertEqual(self.queues[CONTACT_MGMT_QUEUE].get(), + (RP_REMOVE_CONTACT_HEADER, + b''.join([nick_to_pub_key('Alice'), nick_to_pub_key('Bob')]), + False) + ) - for q in [GROUP_MGMT_QUEUE, C_REQ_MGMT_QUEUE]: - command = self.queues[q].get() - self.assertEqual(command, (RP_REMOVE_CONTACT_HEADER, - b''.join([nick_to_pub_key('Alice'), nick_to_pub_key('Bob')]))) + for q in [GROUP_MGMT_QUEUE, C_REQ_MGMT_QUEUE]: + command = self.queues[q].get() + self.assertEqual(command, (RP_REMOVE_CONTACT_HEADER, + b''.join([nick_to_pub_key('Alice'), nick_to_pub_key('Bob')]))) class TestAddOnionKey(unittest.TestCase): - def setUp(self): - self.queues = gen_queue_dict() + def setUp(self): + """Pre-test actions.""" + self.queues = gen_queue_dict() - def tearDown(self): - tear_queues(self.queues) + def tearDown(self): + """Post-test actions.""" + tear_queues(self.queues) - def test_add_contact(self): - command = (ONION_SERVICE_PRIVATE_KEY_LENGTH * b'a' - + b'b' - + b'\x01' - + int_to_bytes(1) - + nick_to_pub_key('Alice') - + nick_to_pub_key('Bob')) - self.assertIsNone(add_onion_data(command, self.queues)) - self.assertEqual(self.queues[ONION_KEY_QUEUE].qsize(), 1) - self.assertEqual(self.queues[ONION_KEY_QUEUE].get(), (ONION_SERVICE_PRIVATE_KEY_LENGTH * b'a', b'b')) + def test_add_contact(self): + command = (ONION_SERVICE_PRIVATE_KEY_LENGTH * b'a' + + b'b' + + b'\x01' + + int_to_bytes(1) + + nick_to_pub_key('Alice') + + nick_to_pub_key('Bob')) + self.assertIsNone(add_onion_data(command, self.queues)) + self.assertEqual(self.queues[ONION_KEY_QUEUE].qsize(), 1) + self.assertEqual(self.queues[ONION_KEY_QUEUE].get(), (ONION_SERVICE_PRIVATE_KEY_LENGTH * b'a', b'b')) if __name__ == '__main__': diff --git a/tests/relay/test_onion.py b/tests/relay/test_onion.py index 6905f9f..f0ac3eb 100644 --- a/tests/relay/test_onion.py +++ b/tests/relay/test_onion.py @@ -30,7 +30,8 @@ from unittest.mock import MagicMock import stem.control from src.common.misc import validate_onion_addr -from src.common.statics import * +from src.common.statics import (EXIT, EXIT_QUEUE, ONION_CLOSE_QUEUE, ONION_KEY_QUEUE, ONION_SERVICE_PRIVATE_KEY_LENGTH, + TOR_DATA_QUEUE, TOR_SOCKS_PORT) from src.relay.onion import get_available_port, onion_service, stem_compatible_ed25519_key_from_private_key, Tor @@ -44,6 +45,11 @@ class TestGetAvailablePort(unittest.TestCase): port = get_available_port(1000, 65535) self.assertEqual(port, 1234) + @mock.patch('builtins.open', mock.mock_open(read_data='TAILS_PRODUCT_NAME="Tails"')) + def test_port_is_tor_socket_port_when_running_on_tails(self): + port = get_available_port(1000, 65535) + self.assertEqual(port, TOR_SOCKS_PORT) + class TestTor(unittest.TestCase): @@ -52,7 +58,7 @@ class TestTor(unittest.TestCase): def test_missing_binary_raises_critical_error(self, *_): tor = Tor() with self.assertRaises(SystemExit): - tor.connect('1234') + tor.connect(1234) @mock.patch('time.sleep', return_value=None) @mock.patch('stem.process.launch_tor_with_config', side_effect=[MagicMock(), OSError, MagicMock()]) @@ -60,9 +66,9 @@ class TestTor(unittest.TestCase): side_effect=['NOTICE BOOTSTRAP PROGRESS=100 TAG=done SUMMARY="Done"', stem.SocketClosed]))) def test_closed_socket_raises_critical_error(self, *_): tor = Tor() - self.assertIsNone(tor.connect('1234')) + self.assertIsNone(tor.connect(1234)) with self.assertRaises(SystemExit): - tor.connect('1234') + tor.connect(1234) @mock.patch('time.sleep', return_value=None) @mock.patch('time.monotonic', side_effect=[1, 20, 30, 40]) @@ -72,7 +78,7 @@ class TestTor(unittest.TestCase): @mock.patch('stem.process.launch_tor_with_config', return_value=MagicMock(poll=lambda: False)) def test_timeout_restarts_tor(self, *_): tor = Tor() - self.assertIsNone(tor.connect('1234')) + self.assertIsNone(tor.connect(1234)) tor.stop() @@ -171,6 +177,33 @@ class TestOnionService(unittest.TestCase): # Teardown tear_queues(queues) + @mock.patch('stem.control.Controller.from_port', MagicMock()) + @mock.patch('builtins.open', mock.mock_open(read_data='TAILS_PRODUCT_NAME="Tails"')) + def test_no_tor_process_is_created_when_tails_is_used(self, *_): + tor = Tor() + self.assertIsNone(tor.connect(1234)) + self.assertIsNone(tor.tor_process) + + @mock.patch('time.sleep', return_value=None) + def test_missing_tor_controller_raises_critical_error(self, *_): + # Setup + queues = gen_queue_dict() + orig_tor_connect = Tor.connect + Tor.connect = MagicMock(return_value=None) + + controller = stem.control.Controller + controller.create_ephemeral_hidden_service = MagicMock() + + queues[ONION_KEY_QUEUE].put((bytes(ONION_SERVICE_PRIVATE_KEY_LENGTH), b'\x01')) + + # Test + with self.assertRaises(SystemExit): + onion_service(queues) + + # Teardown + tear_queues(queues) + Tor.connect = orig_tor_connect + if __name__ == '__main__': unittest.main(exit=False) diff --git a/tests/relay/test_server.py b/tests/relay/test_server.py index 13ae952..bc8e47a 100644 --- a/tests/relay/test_server.py +++ b/tests/relay/test_server.py @@ -22,7 +22,7 @@ along with TFC. If not, see . import unittest from src.common.crypto import X448 -from src.common.statics import * +from src.common.statics import CONTACT_REQ_QUEUE, F_TO_FLASK_QUEUE, M_TO_FLASK_QUEUE, URL_TOKEN_QUEUE from src.relay.server import flask_server diff --git a/tests/relay/test_tcb.py b/tests/relay/test_tcb.py index ec3f22e..d7bc9e5 100644 --- a/tests/relay/test_tcb.py +++ b/tests/relay/test_tcb.py @@ -28,7 +28,13 @@ from unittest import mock from src.common.encoding import int_to_bytes from src.common.reed_solomon import RSCodec -from src.common.statics import * +from src.common.statics import (COMMAND_DATAGRAM_HEADER, DST_COMMAND_QUEUE, DST_MESSAGE_QUEUE, EXIT, + FILE_DATAGRAM_HEADER, F_TO_FLASK_QUEUE, GATEWAY_QUEUE, GROUP_ID_LENGTH, + GROUP_MSG_EXIT_GROUP_HEADER, GROUP_MSG_INVITE_HEADER, GROUP_MSG_JOIN_HEADER, + GROUP_MSG_MEMBER_ADD_HEADER, GROUP_MSG_MEMBER_REM_HEADER, + LOCAL_KEY_DATAGRAM_HEADER, MESSAGE_DATAGRAM_HEADER, M_TO_FLASK_QUEUE, + PUBLIC_KEY_DATAGRAM_HEADER, SRC_TO_RELAY_QUEUE, TFC_PUBLIC_KEY_LENGTH, + UNENCRYPTED_DATAGRAM_HEADER, UNIT_TEST_QUEUE) from src.relay.tcb import dst_outgoing, src_incoming @@ -39,6 +45,7 @@ from tests.utils import cd_unit_test, cleanup, gen_queue_dict, tear_queue class TestSRCIncoming(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.settings = Settings() self.unit_test_dir = cd_unit_test() self.gateway = Gateway() @@ -48,6 +55,7 @@ class TestSRCIncoming(unittest.TestCase): self.args = self.queues, self.gateway def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) cleanup(self.unit_test_dir) @@ -200,10 +208,11 @@ class TestDSTOutGoing(unittest.TestCase): def queue_delayer(): """Place packets into queue after delay.""" - time.sleep(0.01) + time.sleep(0.015) queues[DST_COMMAND_QUEUE].put(packet) + time.sleep(0.015) queues[DST_MESSAGE_QUEUE].put(packet) - time.sleep(0.01) + time.sleep(0.015) queues[UNIT_TEST_QUEUE].put(EXIT) threading.Thread(target=queue_delayer).start() diff --git a/tests/test_dd.py b/tests/test_dd.py index 0880957..584cdc5 100644 --- a/tests/test_dd.py +++ b/tests/test_dd.py @@ -28,9 +28,10 @@ from multiprocessing import Queue from unittest import mock from unittest.mock import MagicMock -from src.common.statics import DATA_FLOW, DST_LISTEN_SOCKET, EXIT, EXIT_QUEUE, IDLE, NCDCLR, NCDCRL, RP_LISTEN_SOCKET -from src.common.statics import SCNCLR, SCNCRL -from dd import animate, draw_frame, main, process_arguments, rx_loop, tx_loop +from src.common.statics import (DATA_FLOW, DST_LISTEN_SOCKET, EXIT, EXIT_QUEUE, IDLE, + NCDCLR, NCDCRL, RP_LISTEN_SOCKET, SCNCLR, SCNCRL) + +from dd import animate, draw_frame, main, process_arguments, rx_loop, tx_loop from tests.utils import tear_queue, TFCTestCase @@ -111,9 +112,11 @@ class TestAnimate(unittest.TestCase): class TestRxLoop(unittest.TestCase): def setUp(self) -> None: + """Pre-test actions.""" self.queue = Queue() def tearDown(self) -> None: + """Post-test actions.""" tear_queue(self.queue) @mock.patch('multiprocessing.connection.Listener', return_value=MagicMock( @@ -132,9 +135,11 @@ class TestRxLoop(unittest.TestCase): class TestTxLoop(unittest.TestCase): def setUp(self) -> None: + """Pre-test actions.""" self.o_sleep = time.sleep def tearDown(self) -> None: + """Post-test actions.""" time.sleep = self.o_sleep @mock.patch('time.sleep', lambda _: None) @@ -144,6 +149,7 @@ class TestTxLoop(unittest.TestCase): queue = Queue() def queue_delayer(): + """Place packet to queue after timer runs out.""" self.o_sleep(0.1) queue.put(b'test_packet') threading.Thread(target=queue_delayer).start() @@ -175,9 +181,11 @@ class TestProcessArguments(unittest.TestCase): class TestMain(unittest.TestCase): def setUp(self) -> None: + """Pre-test actions.""" self.queue = Queue() def tearDown(self) -> None: + """Post-test actions.""" tear_queue(self.queue) @mock.patch('time.sleep', lambda _: None) @@ -187,6 +195,7 @@ class TestMain(unittest.TestCase): queues = {EXIT_QUEUE: self.queue} def queue_delayer(): + """Place packet to queue after timer runs out.""" time.sleep(0.1) queues[EXIT_QUEUE].put(EXIT) threading.Thread(target=queue_delayer).start() diff --git a/tests/transmitter/test_commands.py b/tests/transmitter/test_commands.py index d9d6f7a..2208467 100644 --- a/tests/transmitter/test_commands.py +++ b/tests/transmitter/test_commands.py @@ -27,7 +27,13 @@ from unittest.mock import MagicMock from src.common.db_logs import write_log_entry from src.common.encoding import bool_to_bytes -from src.common.statics import * +from src.common.statics import (BOLD_ON, CLEAR_ENTIRE_SCREEN, COMMAND_PACKET_QUEUE, CURSOR_LEFT_UP_CORNER, + DIR_USER_DATA, KEX_STATUS_NO_RX_PSK, KEX_STATUS_UNVERIFIED, KEX_STATUS_VERIFIED, + KEY_MANAGEMENT_QUEUE, LOGFILE_MASKING_QUEUE, LOG_ENTRY_LENGTH, MESSAGE, + MESSAGE_PACKET_QUEUE, M_S_HEADER, NORMAL_TEXT, PADDING_LENGTH, PRIVATE_MESSAGE_HEADER, + RELAY_PACKET_QUEUE, RESET, SENDER_MODE_QUEUE, TM_COMMAND_PACKET_QUEUE, + TRAFFIC_MASKING_QUEUE, TX, UNENCRYPTED_DATAGRAM_HEADER, UNENCRYPTED_WIPE_COMMAND, + VERSION, WIN_TYPE_CONTACT, WIN_TYPE_GROUP) from src.transmitter.commands import change_master_key, change_setting, clear_screens, exit_tfc, log_command from src.transmitter.commands import print_about, print_help, print_recipients, print_settings, process_command @@ -44,6 +50,7 @@ from tests.utils import gen_queue_dict, nick_to_onion_address, nick_to_pu class TestProcessCommand(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.window = TxWindow() self.contact_list = ContactList() self.group_list = GroupList() @@ -56,6 +63,7 @@ class TestProcessCommand(TFCTestCase): self.queues, self.master_key, self.onion_service, self.gateway) def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def test_valid_command(self): @@ -84,12 +92,14 @@ class TestPrintAbout(TFCTestCase): class TestClearScreens(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.window = TxWindow(uid=nick_to_pub_key('Alice')) self.settings = Settings() self.queues = gen_queue_dict() self.args = self.window, self.settings, self.queues def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) @mock.patch('os.system', return_value=None) @@ -130,6 +140,7 @@ class TestClearScreens(unittest.TestCase): class TestRXPShowSysWin(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.window = TxWindow(name='Alice', uid=nick_to_pub_key('Alice')) self.settings = Settings() @@ -137,6 +148,7 @@ class TestRXPShowSysWin(unittest.TestCase): self.args = self.window, self.settings, self.queues def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) @mock.patch('builtins.input', side_effect=['', EOFError, KeyboardInterrupt]) @@ -161,12 +173,14 @@ class TestRXPShowSysWin(unittest.TestCase): class TestExitTFC(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.settings = Settings(local_testing_mode=True) self.queues = gen_queue_dict() self.gateway = Gateway(data_diode_sockets=True) self.args = self.settings, self.queues, self.gateway def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) @mock.patch('time.sleep', return_value=None) @@ -197,17 +211,19 @@ class TestLogCommand(TFCTestCase): @mock.patch("getpass.getpass", return_value='test_password') def setUp(self, _): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() - self.window = TxWindow(name='Alice', - uid=nick_to_pub_key('Alice')) + self.window = TxWindow(name='Alice', uid=nick_to_pub_key('Alice')) self.contact_list = ContactList() self.group_list = GroupList() self.settings = Settings() self.queues = gen_queue_dict() self.master_key = MasterKey() - self.args = self.window, self.contact_list, self.group_list, self.settings, self.queues, self.master_key + self.args = (self.window, self.contact_list, self.group_list, + self.settings, self.queues, self.master_key) def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) tear_queues(self.queues) @@ -252,7 +268,8 @@ class TestLogCommand(TFCTestCase): @mock.patch('src.common.db_masterkey.MIN_KEY_DERIVATION_TIME', 0.1) @mock.patch('src.common.db_masterkey.MAX_KEY_DERIVATION_TIME', 1.0) - @mock.patch('os.popen', return_value=MagicMock(read=MagicMock(return_value=MagicMock(splitlines=MagicMock(return_value=["MemFree 10240"]))))) + @mock.patch('os.popen', return_value=MagicMock( + read=MagicMock(return_value=MagicMock(splitlines=MagicMock(return_value=["MemAvailable 10240"]))))) @mock.patch("multiprocessing.cpu_count", return_value=1) @mock.patch('time.sleep', return_value=None) @mock.patch('builtins.input', return_value='Yes') @@ -263,10 +280,10 @@ class TestLogCommand(TFCTestCase): self.assert_fr("Log file export aborted.", log_command, UserInput('export'), *self.args) - @mock.patch('src.common.db_masterkey.MIN_KEY_DERIVATION_TIME', 0.1) @mock.patch('src.common.db_masterkey.MAX_KEY_DERIVATION_TIME', 1.0) - @mock.patch('os.popen', return_value=MagicMock(read=MagicMock(return_value=MagicMock(splitlines=MagicMock(return_value=["MemFree 10240"]))))) + @mock.patch('os.popen', return_value=MagicMock( + read=MagicMock(return_value=MagicMock(splitlines=MagicMock(return_value=["MemAvailable 10240"]))))) @mock.patch("multiprocessing.cpu_count", return_value=1) @mock.patch("getpass.getpass", side_effect=3*['test_password'] + ['invalid_password'] + ['test_password']) @mock.patch('time.sleep', return_value=None) @@ -293,6 +310,7 @@ class TestSendOnionServiceKey(TFCTestCase): confirmation_code = b'a' def setUp(self): + """Pre-test actions.""" self.contact_list = ContactList() self.settings = Settings() self.onion_service = OnionService() @@ -334,6 +352,7 @@ class TestSendOnionServiceKey(TFCTestCase): class TestPrintHelp(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.settings = Settings() self.settings.traffic_masking = False @@ -466,6 +485,7 @@ Shift + PgUp/PgDn Scroll terminal up/down class TestPrintRecipients(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.contact_list = ContactList(nicks=['Alice', 'Bob']) self.group_list = GroupList(groups=['test_group', 'test_group_2']) self.args = self.contact_list, self.group_list @@ -477,6 +497,7 @@ class TestPrintRecipients(TFCTestCase): class TestChangeMasterKey(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.contact_list = ContactList() self.group_list = GroupList() @@ -489,6 +510,7 @@ class TestChangeMasterKey(TFCTestCase): self.queues, self.master_key, self.onion_service) def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) tear_queues(self.queues) @@ -508,7 +530,7 @@ class TestChangeMasterKey(TFCTestCase): self.assert_fr("Error: Invalid target system 't'.", change_master_key, UserInput("passwd t"), *self.args) - @mock.patch('os.popen', return_value=MagicMock(read=MagicMock(return_value='foo\nMemFree 200'))) + @mock.patch('os.popen', return_value=MagicMock(read=MagicMock(return_value='foo\nMemAvailable 200'))) @mock.patch('getpass.getpass', return_value='a') @mock.patch('time.sleep', return_value=None) @mock.patch('src.common.db_masterkey.MIN_KEY_DERIVATION_TIME', 0.01) @@ -536,6 +558,7 @@ class TestChangeMasterKey(TFCTestCase): class TestRemoveLog(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.contact_list = ContactList(nicks=['Alice']) self.group_list = GroupList(groups=['test_group']) @@ -546,6 +569,7 @@ class TestRemoveLog(TFCTestCase): self.args = self.contact_list, self.group_list, self.settings, self.queues, self.master_key def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) cleanup(self.unit_test_dir) @@ -641,6 +665,7 @@ class TestRemoveLog(TFCTestCase): class TestChangeSetting(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.window = TxWindow() self.contact_list = ContactList() self.group_list = GroupList() @@ -648,10 +673,11 @@ class TestChangeSetting(TFCTestCase): self.queues = gen_queue_dict() self.master_key = MasterKey() self.gateway = Gateway() - self.args = self.window, self.contact_list, self.group_list, \ - self.settings, self.queues, self.master_key, self.gateway + self.args = (self.window, self.contact_list, self.group_list, + self.settings, self.queues, self.master_key, self.gateway) def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def test_missing_setting_raises_fr(self): @@ -841,9 +867,11 @@ serial_error_correction 5 5 Number of byte class TestRxPDisplayUnread(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.queues = gen_queue_dict() def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def test_command(self): @@ -854,6 +882,7 @@ class TestRxPDisplayUnread(unittest.TestCase): class TestVerify(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.window = TxWindow(uid=nick_to_pub_key("Alice"), name='Alice', window_contacts=[create_contact('test_group')], @@ -894,6 +923,7 @@ class TestVerify(TFCTestCase): class TestWhisper(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.window = TxWindow(uid=nick_to_pub_key("Alice"), name='Alice', window_contacts=[create_contact('Alice')], @@ -918,6 +948,7 @@ class TestWhisper(TFCTestCase): class TestWhois(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.contact_list = ContactList(nicks=['Alice']) self.group_list = GroupList(groups=['test_group']) self.args = self.contact_list, self.group_list @@ -960,6 +991,7 @@ class TestWhois(TFCTestCase): class TestWipe(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.settings = Settings() self.queues = gen_queue_dict() self.gateway = Gateway() diff --git a/tests/transmitter/test_commands_g.py b/tests/transmitter/test_commands_g.py index 321024b..2c0bef5 100644 --- a/tests/transmitter/test_commands_g.py +++ b/tests/transmitter/test_commands_g.py @@ -24,7 +24,8 @@ import unittest from unittest import mock from src.common.encoding import b58encode -from src.common.statics import * +from src.common.statics import (COMMAND_PACKET_QUEUE, GROUP_ID_LENGTH, RELAY_PACKET_QUEUE, + WIN_TYPE_CONTACT, WIN_TYPE_GROUP) from src.transmitter.commands_g import group_add_member, group_create, group_rm_group, group_rm_member from src.transmitter.commands_g import process_group_command, group_rename @@ -36,6 +37,7 @@ from tests.utils import cd_unit_test, cleanup, gen_queue_dict, nick_to_pu class TestProcessGroupCommand(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.contact_list = ContactList(nicks=['Alice']) self.group_list = GroupList() self.settings = Settings() @@ -44,12 +46,13 @@ class TestProcessGroupCommand(TFCTestCase): self.args = self.contact_list, self.group_list, self.settings, self.queues, self.settings def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def test_raises_fr_when_traffic_masking_is_enabled(self): # Setup self.settings.traffic_masking = True - + # Test self.assert_fr("Error: Command is disabled during traffic masking.", process_group_command, UserInput(), *self.args) @@ -80,6 +83,7 @@ class TestProcessGroupCommand(TFCTestCase): class TestGroupCreate(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.contact_list = ContactList(nicks=['Alice', 'Bob']) self.group_list = GroupList() self.settings = Settings() @@ -89,15 +93,16 @@ class TestGroupCreate(TFCTestCase): self.args = self.contact_list, self.group_list, self.settings, self.queues, self.settings def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def configure_groups(self, no_contacts: int) -> None: """Configure group list.""" - self.contact_list = ContactList(nicks=[str(n) for n in range(no_contacts)]) - self.group_list = GroupList(groups=['test_group']) - self.group = self.group_list.get_group('test_group') - self.group.members = self.contact_list.contacts - self.account_list = [nick_to_pub_key(str(n)) for n in range(no_contacts)] + self.contact_list = ContactList(nicks=[str(n) for n in range(no_contacts)]) + self.group_list = GroupList(groups=['test_group']) + self.group = self.group_list.get_group('test_group') + self.group.members = self.contact_list.contacts + self.account_list = [nick_to_pub_key(str(n)) for n in range(no_contacts)] def test_invalid_group_name_raises_fr(self): # Setup @@ -142,6 +147,7 @@ class TestGroupCreate(TFCTestCase): class TestGroupAddMember(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.user_input = UserInput() self.contact_list = ContactList(nicks=['Alice', 'Bob']) self.group_list = GroupList() @@ -151,6 +157,7 @@ class TestGroupAddMember(TFCTestCase): self.args = self.contact_list, self.group_list, self.settings, self.queues, self.settings def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def configure_groups(self, no_contacts: int) -> None: @@ -206,6 +213,7 @@ class TestGroupAddMember(TFCTestCase): class TestGroupRmMember(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.user_input = UserInput() self.contact_list = ContactList(nicks=['Alice', 'Bob']) @@ -216,6 +224,7 @@ class TestGroupRmMember(TFCTestCase): self.args = self.contact_list, self.group_list, self.settings, self.queues, self.settings def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) tear_queues(self.queues) @@ -239,6 +248,7 @@ class TestGroupRmMember(TFCTestCase): class TestGroupRmGroup(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.user_input = UserInput() self.contact_list = ContactList(nicks=['Alice', 'Bob']) @@ -249,6 +259,7 @@ class TestGroupRmGroup(TFCTestCase): self.args = self.contact_list, self.group_list, self.settings, self.queues, self.settings def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) tear_queues(self.queues) @@ -280,6 +291,7 @@ class TestGroupRmGroup(TFCTestCase): class TestGroupRename(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.queues = gen_queue_dict() self.settings = Settings() self.contact_list = ContactList() @@ -288,6 +300,7 @@ class TestGroupRename(TFCTestCase): self.args = self.window, self.contact_list, self.group_list, self.settings, self.queues def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def test_contact_window_raises_fr(self): diff --git a/tests/transmitter/test_contact.py b/tests/transmitter/test_contact.py index 504dcc7..e89c264 100644 --- a/tests/transmitter/test_contact.py +++ b/tests/transmitter/test_contact.py @@ -25,7 +25,9 @@ import unittest from unittest import mock from src.common.crypto import blake2b -from src.common.statics import * +from src.common.statics import (COMMAND_PACKET_QUEUE, CONFIRM_CODE_LENGTH, FINGERPRINT_LENGTH, KDB_REMOVE_ENTRY_HEADER, + KEY_MANAGEMENT_QUEUE, LOG_SETTING_QUEUE, RELAY_PACKET_QUEUE, TM_COMMAND_PACKET_QUEUE, + WIN_TYPE_CONTACT, WIN_TYPE_GROUP) from src.transmitter.contact import add_new_contact, change_nick, contact_setting, remove_contact @@ -38,6 +40,7 @@ from tests.utils import nick_to_onion_address, nick_to_pub_key, tear_queu class TestAddNewContact(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.contact_list = ContactList() self.group_list = GroupList() self.settings = Settings(disable_gui_dialog=True) @@ -46,6 +49,7 @@ class TestAddNewContact(TFCTestCase): self.args = self.contact_list, self.group_list, self.settings, self.queues, self.onion_service def tearDown(self): + """Post-test actions.""" with ignored(OSError): os.remove(f'v4dkh.psk - Give to hpcra') tear_queues(self.queues) @@ -76,9 +80,9 @@ class TestAddNewContact(TFCTestCase): self.assertNotEqual(contact.tx_fingerprint, bytes(FINGERPRINT_LENGTH)) @mock.patch('src.transmitter.key_exchanges.ARGON2_PSK_MEMORY_COST', 200) - @mock.patch('src.transmitter.key_exchanges.MIN_KEY_DERIVATION_TIME', 0.1) - @mock.patch('src.transmitter.key_exchanges.MIN_KEY_DERIVATION_TIME', 1.0) - @mock.patch('builtins.input', side_effect=[nick_to_onion_address("Alice"), 'Alice_', 'psk', '.']) + @mock.patch('src.common.statics.MIN_KEY_DERIVATION_TIME', 0.1) + @mock.patch('src.common.statics.MAX_KEY_DERIVATION_TIME', 1.0) + @mock.patch('builtins.input', side_effect=[nick_to_onion_address("Alice"), 'Alice_', 'psk', '.', '', 'ff', 'fc']) @mock.patch('getpass.getpass', return_value='test_password') @mock.patch('time.sleep', return_value=None) def test_standard_nick_psk_kex(self, *_): @@ -97,6 +101,7 @@ class TestAddNewContact(TFCTestCase): class TestRemoveContact(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.contact_list = ContactList(nicks=['Alice']) self.group_list = GroupList(groups=['test_group']) @@ -107,6 +112,7 @@ class TestRemoveContact(TFCTestCase): self.args = self.contact_list, self.group_list, self.settings, self.queues, self.master_key def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) tear_queues(self.queues) @@ -216,6 +222,7 @@ class TestRemoveContact(TFCTestCase): class TestChangeNick(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.contact_list = ContactList(nicks=['Alice']) self.group_list = GroupList() self.settings = Settings() @@ -223,6 +230,7 @@ class TestChangeNick(TFCTestCase): self.args = self.contact_list, self.group_list, self.settings, self.queues def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def test_missing_nick_raises_fr(self): @@ -235,7 +243,8 @@ class TestChangeNick(TFCTestCase): contact=create_contact('Bob')) # Test - self.assert_fr("Error: Nick must be printable.", change_nick, UserInput("nick Alice\x01"), window, *self.args) + self.assert_fr("Error: Nick must be printable.", + change_nick, UserInput("nick Alice\x01"), window, *self.args) def test_no_contact_raises_fr(self): # Setup @@ -244,7 +253,8 @@ class TestChangeNick(TFCTestCase): window.contact = None # Test - self.assert_fr("Error: Window does not have contact.", change_nick, UserInput("nick Alice\x01"), window, *self.args) + self.assert_fr("Error: Window does not have contact.", + change_nick, UserInput("nick Alice\x01"), window, *self.args) def test_successful_nick_change(self): # Setup @@ -274,6 +284,7 @@ class TestChangeNick(TFCTestCase): class TestContactSetting(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.contact_list = ContactList(nicks=['Alice', 'Bob']) self.group_list = GroupList(groups=['test_group']) self.settings = Settings() @@ -282,6 +293,7 @@ class TestContactSetting(TFCTestCase): self.args = self.contact_list, self.group_list, self.settings, self.queues def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def test_invalid_command_raises_fr(self): diff --git a/tests/transmitter/test_files.py b/tests/transmitter/test_files.py index d567694..7b9f09e 100644 --- a/tests/transmitter/test_files.py +++ b/tests/transmitter/test_files.py @@ -31,12 +31,14 @@ from tests.utils import cd_unit_test, cleanup, TFCTestCase class TestFile(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.window = TxWindow() self.settings = Settings() self.args = self.window, self.settings def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) def test_missing_file_raises_fr(self): diff --git a/tests/transmitter/test_input_loop.py b/tests/transmitter/test_input_loop.py index afa6849..8320186 100644 --- a/tests/transmitter/test_input_loop.py +++ b/tests/transmitter/test_input_loop.py @@ -25,7 +25,7 @@ from unittest import mock from unittest.mock import MagicMock from src.common.crypto import blake2b -from src.common.statics import * +from src.common.statics import CONFIRM_CODE_LENGTH from src.transmitter.input_loop import input_loop @@ -51,6 +51,7 @@ class TestInputLoop(unittest.TestCase): '/exit'] # Enter exit command def setUp(self): + """Pre-test actions.""" self.settings = Settings(disable_gui_dialog=True) self.gateway = Gateway() self.contact_list = ContactList() @@ -60,6 +61,7 @@ class TestInputLoop(unittest.TestCase): self.queues = gen_queue_dict() def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) @mock.patch('builtins.input', side_effect=input_list) diff --git a/tests/transmitter/test_key_exchanges.py b/tests/transmitter/test_key_exchanges.py index 5cba691..c10d7ce 100644 --- a/tests/transmitter/test_key_exchanges.py +++ b/tests/transmitter/test_key_exchanges.py @@ -26,7 +26,11 @@ from unittest import mock from src.common.crypto import blake2b from src.common.encoding import b58encode -from src.common.statics import * +from src.common.statics import (COMMAND_PACKET_QUEUE, CONFIRM_CODE_LENGTH, ECDHE, FINGERPRINT_LENGTH, + KDB_ADD_ENTRY_HEADER, KEX_STATUS_HAS_RX_PSK, KEX_STATUS_NO_RX_PSK, KEX_STATUS_PENDING, + KEX_STATUS_UNVERIFIED, KEX_STATUS_VERIFIED, KEY_MANAGEMENT_QUEUE, LOCAL_ID, LOCAL_NICK, + LOCAL_PUBKEY, RELAY_PACKET_QUEUE, SYMMETRIC_KEY_LENGTH, TFC_PUBLIC_KEY_LENGTH, + WIN_TYPE_CONTACT, WIN_TYPE_GROUP, XCHACHA20_NONCE_LENGTH) from src.transmitter.key_exchanges import create_pre_shared_key, export_onion_service_data, new_local_key from src.transmitter.key_exchanges import rxp_load_psk, start_key_exchange, verify_fingerprints @@ -39,6 +43,7 @@ from tests.utils import nick_to_short_address, tear_queues, TFCTestCase, class TestOnionService(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.contact_list = ContactList() self.settings = Settings() self.onion_service = OnionService() @@ -56,12 +61,14 @@ class TestOnionService(TFCTestCase): class TestLocalKey(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.contact_list = ContactList() self.settings = Settings() self.queues = gen_queue_dict() self.args = self.contact_list, self.settings, self.queues def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def test_new_local_key_when_traffic_masking_is_enabled_raises_fr(self): @@ -127,12 +134,14 @@ class TestVerifyFingerprints(unittest.TestCase): class TestKeyExchange(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.contact_list = ContactList() self.settings = Settings() self.queues = gen_queue_dict() self.args = self.contact_list, self.settings, self.queues def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) @mock.patch('shutil.get_terminal_size', return_value=[200, 200]) @@ -143,7 +152,7 @@ class TestKeyExchange(TFCTestCase): @mock.patch('shutil.get_terminal_size', return_value=[200, 200]) @mock.patch('builtins.input', return_value=b58encode((TFC_PUBLIC_KEY_LENGTH-1)*b'a', public_key=True)) def test_invalid_public_key_length_raises_fr(self, *_): - self.assert_fr("Error: Invalid public key length", + self.assert_fr("Error: Invalid public key length", start_key_exchange, nick_to_pub_key("Alice"), 'Alice', *self.args) @mock.patch('builtins.input', side_effect=['', # Empty message should resend key @@ -235,14 +244,16 @@ class TestKeyExchange(TFCTestCase): class TestPSK(TFCTestCase): def setUp(self): - self.unit_test_dir = cd_unit_test() - self.contact_list = ContactList() - self.settings = Settings(disable_gui_dialog=True) - self.queues = gen_queue_dict() - self.onion_service = OnionService() - self.args = self.contact_list, self.settings, self.onion_service, self.queues + """Pre-test actions.""" + self.unit_test_dir = cd_unit_test() + self.contact_list = ContactList() + self.settings = Settings(disable_gui_dialog=True) + self.queues = gen_queue_dict() + self.onion_service = OnionService() + self.args = self.contact_list, self.settings, self.onion_service, self.queues def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) with ignored(OSError): @@ -250,7 +261,7 @@ class TestPSK(TFCTestCase): tear_queues(self.queues) - @mock.patch('builtins.input', side_effect=['/root/', '.']) + @mock.patch('builtins.input', side_effect=['/root/', '.', 'fc']) @mock.patch('time.sleep', return_value=None) @mock.patch('getpass.getpass', return_value='test_password') @mock.patch('src.transmitter.key_exchanges.ARGON2_PSK_MEMORY_COST', 1000) @@ -292,11 +303,13 @@ class TestPSK(TFCTestCase): class TestReceiverLoadPSK(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.settings = Settings() self.queues = gen_queue_dict() self.args = self.settings, self.queues def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def test_raises_fr_when_traffic_masking_is_enabled(self): @@ -324,7 +337,7 @@ class TestReceiverLoadPSK(TFCTestCase): # Test self.assert_fr(f"Error: The current key was exchanged with {ECDHE}.", rxp_load_psk, window, contact_list, *self.args) - + @mock.patch('src.transmitter.key_exchanges.ARGON2_PSK_MEMORY_COST', 1000) @mock.patch('src.transmitter.key_exchanges.ARGON2_PSK_TIME_COST', 0.01) @mock.patch('time.sleep', return_value=None) diff --git a/tests/transmitter/test_packet.py b/tests/transmitter/test_packet.py index 5e1e135..9c303ce 100644 --- a/tests/transmitter/test_packet.py +++ b/tests/transmitter/test_packet.py @@ -27,7 +27,12 @@ import unittest from multiprocessing import Queue from unittest import mock -from src.common.statics import * +from src.common.statics import (ASSEMBLY_PACKET_LENGTH, COMMAND, COMMAND_PACKET_QUEUE, C_A_HEADER, C_E_HEADER, + C_L_HEADER, C_S_HEADER, FILE, F_A_HEADER, F_E_HEADER, F_L_HEADER, F_S_HEADER, + GROUP_MSG_INVITE_HEADER, LOCAL_ID, MESSAGE, MESSAGE_PACKET_QUEUE, M_A_HEADER, + M_E_HEADER, M_L_HEADER, M_S_HEADER, RELAY_PACKET_QUEUE, SYMMETRIC_KEY_LENGTH, + TM_COMMAND_PACKET_QUEUE, TM_FILE_PACKET_QUEUE, TM_MESSAGE_PACKET_QUEUE, + WIN_TYPE_CONTACT, WIN_TYPE_GROUP) from src.transmitter.packet import cancel_packet, queue_command, queue_file, queue_message, queue_assembly_packets from src.transmitter.packet import send_file, send_packet, split_to_assembly_packets @@ -40,11 +45,13 @@ from tests.utils import cd_unit_test, cleanup, gen_queue_dict, tear_queue class TestQueueMessage(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.queues = gen_queue_dict() self.settings = Settings() self.args = self.settings, self.queues def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def test_private_message_header(self): @@ -84,15 +91,17 @@ class TestQueueMessage(unittest.TestCase): class TestSendFile(TFCTestCase): def setUp(self): - self.unit_test_dir = cd_unit_test() - self.settings = Settings() - self.queues = gen_queue_dict() - self.window = TxWindow() - self.onion_service = OnionService() - self.contact_list = ContactList(nicks=['Alice', 'Bob', 'Charlie']) - self.args = self.settings, self.queues, self.window + """Pre-test actions.""" + self.unit_test_dir = cd_unit_test() + self.settings = Settings() + self.queues = gen_queue_dict() + self.window = TxWindow() + self.onion_service = OnionService() + self.contact_list = ContactList(nicks=['Alice', 'Bob', 'Charlie']) + self.args = self.settings, self.queues, self.window def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) tear_queues(self.queues) @@ -149,10 +158,12 @@ class TestQueueFile(TFCTestCase): 'rx_serial_settings.json', 'tx_onion_db') def setUp(self): + """Pre-test actions.""" self.unit_test_dir = cd_unit_test() - self.queues = gen_queue_dict() + self.queues = gen_queue_dict() def tearDown(self): + """Post-test actions.""" cleanup(self.unit_test_dir) tear_queues(self.queues) @@ -266,10 +277,12 @@ class TestQueueFile(TFCTestCase): class TestQueueCommand(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.settings = Settings() self.queues = gen_queue_dict() def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def test_queue_command(self): @@ -323,14 +336,16 @@ class TestSplitToAssemblyPackets(unittest.TestCase): class TestQueueAssemblyPackets(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.settings = Settings() self.queues = gen_queue_dict() self.window = TxWindow(uid=nick_to_pub_key("Alice"), log_messages=True) self.window.window_contacts = [create_contact('Alice')] - self.args = self.settings, self.queues, self.window + self.args = self.settings, self.queues, self.window def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def test_queue_message_traffic_masking(self): @@ -425,6 +440,7 @@ class TestSendPacket(unittest.TestCase): """ def setUp(self): + """Pre-test actions.""" self.l_queue = Queue() self.key_list = KeyList(nicks=['Alice']) self.settings = Settings() @@ -432,6 +448,7 @@ class TestSendPacket(unittest.TestCase): self.onion_service = OnionService() def tearDown(self): + """Post-test actions.""" tear_queue(self.l_queue) def test_message_length(self): @@ -497,9 +514,11 @@ class TestSendPacket(unittest.TestCase): class TestCancelPacket(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.queues = gen_queue_dict() def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def test_cancel_message_during_normal(self): diff --git a/tests/transmitter/test_sender_loop.py b/tests/transmitter/test_sender_loop.py index 35130b0..d7e684a 100644 --- a/tests/transmitter/test_sender_loop.py +++ b/tests/transmitter/test_sender_loop.py @@ -23,7 +23,11 @@ import threading import time import unittest -from src.common.statics import * +from src.common.statics import (C_N_HEADER, EXIT_QUEUE, KDB_ADD_ENTRY_HEADER, KEY_MANAGEMENT_QUEUE, LOCAL_ID, + LOCAL_PUBKEY, PADDING_LENGTH, PUBLIC_KEY_DATAGRAM_HEADER, P_N_HEADER, + RELAY_PACKET_QUEUE, SENDER_MODE_QUEUE, SYMMETRIC_KEY_LENGTH, TFC_PUBLIC_KEY_LENGTH, + TM_NOISE_COMMAND_QUEUE, TM_NOISE_PACKET_QUEUE, UNENCRYPTED_DATAGRAM_HEADER, + UNENCRYPTED_EXIT_COMMAND, UNENCRYPTED_WIPE_COMMAND, WINDOW_SELECT_QUEUE) from src.transmitter.commands import queue_command from src.transmitter.packet import queue_message, queue_to_nc @@ -54,7 +58,7 @@ class TestSenderLoop(unittest.TestCase): settings.traffic_masking = False queues[SENDER_MODE_QUEUE].put(settings) - self.assertIsNone(sender_loop(queues, settings, gateway, key_list, unit_test=True)) # Output Alice and Bob again + self.assertIsNone(sender_loop(queues, settings, gateway, key_list, unit_test=True)) # Output Alice & Bob again self.assertEqual(len(gateway.packets), 1) diff --git a/tests/transmitter/test_traffic_masking.py b/tests/transmitter/test_traffic_masking.py index e7bf2e1..7a78db6 100644 --- a/tests/transmitter/test_traffic_masking.py +++ b/tests/transmitter/test_traffic_masking.py @@ -22,7 +22,8 @@ along with TFC. If not, see . import time import unittest -from src.common.statics import * +from src.common.statics import (C_N_HEADER, PADDING_LENGTH, PLACEHOLDER_DATA, TM_NOISE_COMMAND_QUEUE, + TM_NOISE_PACKET_QUEUE, TRAFFIC_MASKING) from src.transmitter.traffic_masking import HideRunTime, noise_loop @@ -33,6 +34,7 @@ from tests.utils import gen_queue_dict, tear_queues class TestHideRunTime(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.settings = Settings() self.settings.tm_random_delay = 1 self.settings.tm_static_delay = 1 @@ -55,10 +57,12 @@ class TestHideRunTime(unittest.TestCase): class TestNoiseLoop(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.queues = gen_queue_dict() self.contact_list = ContactList(nicks=['Alice']) def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def test_noise_commands(self): diff --git a/tests/transmitter/test_user_input.py b/tests/transmitter/test_user_input.py index e75e78b..9df9e0c 100644 --- a/tests/transmitter/test_user_input.py +++ b/tests/transmitter/test_user_input.py @@ -23,16 +23,15 @@ import unittest from unittest import mock -from src.common.statics import * - +from src.common.statics import COMMAND, FILE, MESSAGE, WIN_TYPE_CONTACT, WIN_TYPE_GROUP from src.transmitter.user_input import get_input, process_aliases, UserInput - -from tests.mock_classes import create_contact, create_group, Settings, TxWindow +from tests.mock_classes import create_contact, create_group, Settings, TxWindow class TestProcessAliases(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.settings = Settings() self.window = TxWindow(name='Alice', type=WIN_TYPE_CONTACT, @@ -59,6 +58,7 @@ class TestProcessAliases(unittest.TestCase): class TestGetInput(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.settings = Settings() self.window = TxWindow(name='Alice', type=WIN_TYPE_CONTACT, diff --git a/tests/transmitter/test_windows.py b/tests/transmitter/test_windows.py index 4971114..bf8e5bb 100644 --- a/tests/transmitter/test_windows.py +++ b/tests/transmitter/test_windows.py @@ -25,7 +25,8 @@ from unittest import mock from src.common.crypto import blake2b from src.common.db_contacts import Contact -from src.common.statics import * +from src.common.statics import (COMMAND_PACKET_QUEUE, CONFIRM_CODE_LENGTH, KEX_STATUS_PENDING, KEX_STATUS_VERIFIED, + WINDOW_SELECT_QUEUE, WIN_TYPE_CONTACT, WIN_TYPE_GROUP) from src.transmitter.windows import MockWindow, select_window, TxWindow @@ -37,6 +38,7 @@ from tests.utils import tear_queues, TFCTestCase, VALID_ECDHE_PUB_KEY class TestMockWindow(unittest.TestCase): def setUp(self): + """Pre-test actions.""" self.window = MockWindow(nick_to_pub_key("Alice"), contacts=[create_contact(n) for n in ['Alice', 'Bob']]) def test_window_iterates_over_contacts(self): @@ -47,6 +49,7 @@ class TestMockWindow(unittest.TestCase): class TestTxWindow(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.contact_list = ContactList(['Alice', 'Bob']) self.group_list = GroupList(groups=['test_group', 'test_group_2']) self.window = TxWindow(self.contact_list, self.group_list) @@ -59,6 +62,7 @@ class TestTxWindow(TFCTestCase): self.args = self.settings, self.queues, self.onion_service, self.gateway def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def test_window_iterates_over_contacts(self): @@ -209,16 +213,16 @@ class TestTxWindow(TFCTestCase): self.window.type = WIN_TYPE_CONTACT self.window.contact = create_contact('Alice') self.window.window_contacts = [self.window.contact] - self.assertIsNot(self.window.contact, + self.assertIsNot(self.window.contact, self.window.contact_list.get_contact_by_pub_key(nick_to_pub_key('Alice'))) - self.assertIsNot(self.window.window_contacts[0], + self.assertIsNot(self.window.window_contacts[0], self.window.contact_list.get_contact_by_pub_key(nick_to_pub_key('Alice'))) # Test self.assertIsNone(self.window.update_window(self.group_list)) self.assertIs(self.window.contact, self.window.contact_list.get_contact_by_pub_key(nick_to_pub_key('Alice'))) - self.assertIs(self.window.window_contacts[0], + self.assertIs(self.window.window_contacts[0], self.window.contact_list.get_contact_by_pub_key(nick_to_pub_key('Alice'))) def test_deactivate_window_if_group_is_not_available(self): @@ -239,7 +243,7 @@ class TestTxWindow(TFCTestCase): @mock.patch('builtins.input', side_effect=['Alice', VALID_ECDHE_PUB_KEY, 'yes', - blake2b(nick_to_pub_key('Alice'), + blake2b(nick_to_pub_key('Alice'), digest_size=CONFIRM_CODE_LENGTH).hex()]) @mock.patch('shutil.get_terminal_size', return_value=[200, 200]) def test_selecting_pending_contact_starts_key_exchange(self, *_): @@ -262,7 +266,7 @@ class TestTxWindow(TFCTestCase): '', VALID_ECDHE_PUB_KEY, 'yes', - blake2b(nick_to_pub_key('Alice'), + blake2b(nick_to_pub_key('Alice'), digest_size=CONFIRM_CODE_LENGTH).hex()]) @mock.patch('shutil.get_terminal_size', return_value=[200, 200]) def test_adding_new_contact_from_contact_selection(self, *_): @@ -312,6 +316,7 @@ class TestTxWindow(TFCTestCase): class TestSelectWindow(TFCTestCase): def setUp(self): + """Pre-test actions.""" self.contact_list = ContactList(nicks=['Alice']) self.group_list = GroupList() self.user_input = UserInput() @@ -323,6 +328,7 @@ class TestSelectWindow(TFCTestCase): self.args = self.user_input, self.window, self.settings, self.queues, self.onion_service, self.gateway def tearDown(self): + """Post-test actions.""" tear_queues(self.queues) def test_invalid_selection_raises_fr(self): diff --git a/tests/utils.py b/tests/utils.py index bbdfa84..1f72355 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -34,7 +34,21 @@ from src.common.crypto import blake2b, byte_padding, csprng, encrypt_and_sig from src.common.encoding import int_to_bytes, pub_key_to_onion_address from src.common.misc import split_byte_string from src.common.exceptions import FunctionReturn -from src.common.statics import * +from src.common.statics import (COMMAND, COMMAND_DATAGRAM_HEADER, COMMAND_PACKET_QUEUE, COMPRESSION_LEVEL, + CONTACT_MGMT_QUEUE, CONTACT_REQ_QUEUE, C_A_HEADER, C_E_HEADER, C_L_HEADER, + C_REQ_MGMT_QUEUE, C_REQ_STATE_QUEUE, C_S_HEADER, DST_COMMAND_QUEUE, + DST_MESSAGE_QUEUE, EXIT_QUEUE, FILE, FILE_DATAGRAM_HEADER, FILE_PACKET_CTR_LENGTH, + F_A_HEADER, F_E_HEADER, F_L_HEADER, F_S_HEADER, F_TO_FLASK_QUEUE, GATEWAY_QUEUE, + GROUP_ID_LENGTH, GROUP_MESSAGE_HEADER, GROUP_MGMT_QUEUE, GROUP_MSG_ID_LENGTH, + GROUP_MSG_QUEUE, INITIAL_HARAC, KEY_MANAGEMENT_QUEUE, LOCAL_KEY_DATAGRAM_HEADER, + LOGFILE_MASKING_QUEUE, LOG_PACKET_QUEUE, LOG_SETTING_QUEUE, MESSAGE, + MESSAGE_DATAGRAM_HEADER, MESSAGE_PACKET_QUEUE, M_A_HEADER, M_E_HEADER, M_L_HEADER, + M_S_HEADER, M_TO_FLASK_QUEUE, ONION_CLOSE_QUEUE, ONION_KEY_QUEUE, PADDING_LENGTH, + PRIVATE_MESSAGE_HEADER, RELAY_PACKET_QUEUE, SENDER_MODE_QUEUE, SRC_TO_RELAY_QUEUE, + SYMMETRIC_KEY_LENGTH, TM_COMMAND_PACKET_QUEUE, TM_FILE_PACKET_QUEUE, + TM_MESSAGE_PACKET_QUEUE, TM_NOISE_COMMAND_QUEUE, TM_NOISE_PACKET_QUEUE, + TOR_DATA_QUEUE, TRAFFIC_MASKING_QUEUE, TRUNC_ADDRESS_LENGTH, UNIT_TEST_QUEUE, + URL_TOKEN_QUEUE, US_BYTE, WINDOW_SELECT_QUEUE) UNDECODABLE_UNICODE = bytes.fromhex('3f264d4189d7a091') @@ -195,7 +209,7 @@ def assembly_packet_creator( message_key: bytes = None, # Allows choosing the message key to encrypt message with header_key: bytes = None, # Allows choosing the header key for hash ratchet encryption tamper_harac: bool = False, # When True, tampers with the MAC of encrypted harac - tamper_message: bool = False, # When True, tampers with the MAC of encrypted messagae + tamper_message: bool = False, # When True, tampers with the MAC of encrypted message onion_pub_key: bytes = b'', # Defines the contact public key to use with datagram creation origin_header: bytes = b'', # Allows editing the origin header ) -> List[bytes]: @@ -242,7 +256,7 @@ def assembly_packet_creator( file_name_bytes = b'test_file.txt' if file_name is None else file_name delimiter = US_BYTE if not omit_header_delim else b'' - payload = time_bytes + size_bytes + file_name_bytes + delimiter + ct_with_key + payload = time_bytes + size_bytes + file_name_bytes + delimiter + ct_with_key elif packet_type == COMMAND: payload = payload @@ -283,9 +297,9 @@ def assembly_packet_creator( payload = bytes(FILE_PACKET_CTR_LENGTH) + payload elif packet_type == COMMAND: - command_hash = blake2b(payload) - command_hash = command_hash if not tamper_cmd_hash else command_hash[::-1] - payload += command_hash + command_hash = blake2b(payload) + command_hash = command_hash if not tamper_cmd_hash else command_hash[::-1] + payload += command_hash padded = payload if no_padding else byte_padding(payload) p_list = split_byte_string(padded, item_len=split_length) diff --git a/tfc.png b/tfc.png old mode 100755 new mode 100644 index d1de8a78394d6bb3ce422a309751ceddc4f5d3dd..c5446990cc4e402c372516675466cd2e39a0881d GIT binary patch literal 41051 zcmX`S1yohv*ENhZ5)#s-AR*lyN`oNXA&qo*OLqxK|LAV%x^hX8F6jnoknV3^|Ihmk z2gBjzo^$qIYpyxxTKgEG^idk~ImvT4I5pz?ysmb(GrBV_P1$Q+pXIu=+nJJ?`75>S-mEW{{DY%8rc3drEDn6rz!=JI-B)=*%?Sg`W%+nkH^KlS#1>ud^ zuB(pY)1;jHV9Ljnu$_)i;R83$0$INg-SYDC!ee4+L`1TCdL$AP6J=y%UcG)D*xbx# zY-}7J8%rxBM3y=fGPKog*JRSPz>a~CF-%}aBqfAxK_o?njnEVNNQxXUAtFW9z8QZV&>U=rMT15CYEch=K1upN6GiK?rr)b9?c0<^$aRYv`YV=R#1( zed5tcnf|P;5oD%aS!8LPPW!_UyK6;JS{lWmI(247BRwO-(#EDBH#aae6xqt!y0pAJ zzq2#@_SOre2n{*t+DGKjhl+|iAiv3f72$;tfeD7T;>7zv#Ko0xcz)&J&${3POk zMMa^oFGf>}W>@j&7nUVGtxT`DiRxrhOU9J+0|wME3m(b3R^oS(a> zsHn(%_(1*kZE2fjWO(>1CZ-CD`mdjfq|%LuO5ZWW^7K|mLz6mCneJA{D%y@sF zN&MKJ+^wyHq6B3xV17&mK`ktW;lH1C6>^hY`|^`-uu)xj4Tpz^xwyHNH8rIb6fl#M zlfR~>s_N@2`1%S{#G%CXPd@NUN=Qs;xn{5#!H)`^VT2l;fYNfjJ8W`)`tiHo;zg)Y zjrA-Gh0nA6Fe1LQxz%ZjJ)tWL6Dy;XlS@7sJ5M}LxQsLd7aZIBqV61T$GyU z?#J1r8nSte*hWS~ti=i*y%H462o6TH;aSjr`s`*_W49y#%O{V`Z%D(e!O(tnm*4zh z%TzRrsqX0IeWu;Q$mM;`{ywbIW{cOn%px-jJtL!{Kzt%Vk~layiMg#_mX?;jp{2#e z#|K+L#0o=bTeACY(>NY#6b&!+O#7Yr=Rs8FT^9=eL`COkmbqJ*qcKZ%S96D#&xZGV!X=})`5q*abgac@8Y~&#!B`vL}D9FqEQdh_EF_Z7T zrzgLHf`Uhf!M$HsHX2Ss?wHm^?|7gp>DTNKSIsptR79`Ct1R0)ax9b|h9}XOKj%nN z|NeENRY>m{&j+Y6v#?M#Fz}l>yjdsg8aUnP9~>So1jW!#>3`o&A+4;8^RYNdyWV{5 z)PtUt^|)bYTV8a>xUr$VJ@eqeM&xEQ3El5;)Zz)7c(=Q|8}-EtI&SV{<$Ni2^It?# zaTE&X=H~8an{I86d}JUFOi;EygpI&j2%n*RoFr@6;|7*6ipl8tRUaG1ka<16VAja` z$;rw3_4W0Jwzjt9WU(Uacc${Ox{A6gYJQR>Rc&oqK&kz4lsi2Ucxq~DV^dQS z-rjAvggC_VG3xfb>Nhtxi#}(=o7>wQ?nionCubfS0R5z=r+?$MrwDsNd6bHxrJ~Y( zFjqgs`PGWQD2%GVfBy=C(G%Q#^!7f-8idYKy?H^w!-J2Qm>9+djvIaKZUtp z5wAHo>h=dzTaAd40uLyLBn;uRW|;@aj514ao^u;eqz<|M+H%5|kd)jiE-2XC+1%XJ zO-Z&$H?IW;mw<3mUVF> zI{WyV!-mlk3YaK*YD%pyhO{3*U~puF7BD9GGEke=SP=y_3=;-6n(v!%Nr4`Pe8#~D zlj1|NckxSh_B9KKo_0-lNxy&prp}j&5pbGT*CgjrIMaHC4cRyy8ykD4u8toP60*6o z<9z-{nSqhf($aF=Rga3w_u;COw+Oy_H;m7vcFtyLW#ufcXZQIYdYXrO>&k+&{%Cbt z(co06YRlP(;LSy~|3hhY&aTD#ol>h5Ggn1;CT8Zt<;$vSS7F+rI-uLdRids7UVdG} z<|ZSV{BRYYx*nsoucV~F=Sz**t+p0Fw_HYdOGls5{8rLB{W{~cP1$&q$H&JDii$Wa zC*C+YIT4Ba2?50P^!Bc~wbd>hmWCSXA?wZLhi6OeoC_tOPa~7TV}+(5KmrZfw5G?$ z-&7Hi!hgzdxnn9#plLpPUAU6>H}uDhj4o^atO_tBi$ zhQ}#e!HYYK(d^-kI8nd8qWUHd0oRA!#tZr3ZRANjG=qdZ)*rtmB5I>9co(TCIk*v3>MQmQ@pKd=4VV<4X)1i}GH`*fdWJy0apYU9vOUNxk{*7jX z;s&<>ANa1aexr2*3&7hTG{RG66t#5a=LhxmNdeK~clm>TcX_y<%}_px zA_>GcVgQwZoP78jyGiqUBx5rmC}IGwh=_=0Jr<~Wz5a`Ph0iTmGD^lAU#1as1BM~y zLv=hi4}Rq>EotlP##j_W14m13gQKIxjg1Y5*Q6_|cqqA)&pbYuslV7N1q1z2cz>OGFK z2_UVhxkEP?ect~aO5=B?oh(wI25M7N!v-|mB9JtYR4ht7B0}#w+==pMZF}t*DF6L*Vt^H)IHa-6{E+Hkg?!d=_ zvK^X|5kOx^Ghv|<=n4sggcVsHU3mzpsjEAltiErwUFhrdy(_7z(omtw>ywi(W&0z* zaFI6Z6S`m377MIz~t!EbiAqs74# zW?lNXZ^y@({_SugryB}QO^k5PfF zkydn5JG);9jDYXmWi9Z0z=4YS`gW-NZ)+XQEG)5>lSPgX7qia2_?qzv34sfTv5j*! zP4NDtIuprX2jW$qRbVhvy12M}2de6PePYyTGuJ(mCCKl%9{hi|A-vc&CF<$#=d_+- zvTI&L1}^KPu`v}^m`S@4Q4>N_NcvkGVgn8MV}xU*pdZ~pZ~mU0!8BF3$*vHns3BX) z*Z*O4F1-{mHVg%Jt-G{EAG73qe1zQE9ylRc5ORn-4Go9!0pc=}24c{UC0kqDmjncL zWcW`3MVUUM^1*Gl#OmJ(fI&cM!|FjsHE3EV-Ee43b^9vN7P)GB(e zS#%<90w+Ck3j7C!NxcL6- zwlnOv=dG)0I63i(C(}%b`W|va_c#jQQ28FfonW1~u@T)D0QP{v!)Z(I@?cKf+PWO~ zUm!V9SLDU;9_-prrR+Ly%ldgtFA8<7T zz!lciD2cQFZx`)<{(P?LytNW}x*6p(>5Hz4@VkP0G2vO36Fy*Ahw1^XtB};6HL29D z553r*1?Ile<==(plx<5R!Dx6eALhLM>oqksLh&R&`t{vdV%}E?)A({^0~CW7$U%a8 z`}=b(&UED(mBypl!X82tG;gS^DkKW*0T`W^TxT8kE1Ni6_tdDUsW&j8*C?y6S4qHE z#HV;xxmVS5MrW1u^nT4ZSOe!8Q}p~_(sn^#k_9~LQS$~?Ro9~zU`i1BCb%ae9SMoh zBvAx^kT+LVRXp#@gEio1%&v}>zBe^7lK-nrLRtZVbXaXK*mq^^P8Q=46Q>yy^=SrF z!%cpT565s03kZRT<&0|T>*>kc*|C6?=arSo?S{kf!aqd%#fZ&rk#BoAqkpl*dGOb- zo;8O>3?YpBAY9x8oIUkbvq`Smnc3NT;Hxw$w0;0`;Zj!m$IGAi$*`*0|H^e<_p)*T zCIyW%72vzsz-o9+hSxYaaF=kb&saT#vM<})g<#hLTLd~L_80B0@I#G!$gc&ks{kfH4k5I|wPT|qUZx}`*P0KLXjJM*Iy%-){M$A4x}-gk zsk!;z5LTbS7U!MB?CjVjyDrjex>ft+`0xQFZ8RZ^^zHScB3c+FP85Ea>+%;3eD`iQ zyrdR4VePk)0#LlRW-A==4Seg;Y)upO!Zg*#FjTXaHG$j7M7Qny#V{bOeVO= zXIoo!*wQ6TOVH8LO=c>z7oA6ie|=*Q3J(t-3^fvh7lN0de+?|lG?oy$(8Bn5g8Tio zV+!l1k#uhB&%oaP-_^i#t~=-4k@;V_`%AmI@vx-sBd_KJqI``Fm+A6Azg7hWQc_o! zn9QhIXEVnEyk2tjzv4asMcI5YquX-WxIn~dj)4`nVX&G3QIM0CFnw2nkPZA|*8IHA zO!cQxz)=IEqnaH5)>|X1sJMgv=^FlQdl={ve%sO`>IuC=+5`>`@k=Z?bfzJ%Mxd6= zx{WoT$EmQ$_yWwd!99VJV-&#JjpL3hSib_E-fhdXU;^oa%Li*6_)qP*5YjJuYwNRv z-?c$a%U)4SZSLhiaAAu0aNEs|0kgrrS4;g0yh{>3K0b*?L{HD}8L`Vjc0+?{S7lcR zFE20Wg}pCNE*I=!0E@9Agk9{rqy)M@SeL_TF+yf+Vp5L*z1AqnbpohLcs)pIRYL(3 z&kW@S#pW>Yis}ALrBw|KkuvtYYQU*&e)x2Y3oBw;TJuaJq7`HcRRJn0=ue;3IEuKP z>s;|LkHd!9-;K5zBup^5ogl@W2J@3K*I+B-lHA)OSXgGVvADI6U{AQ1{iGR01Cvsg zK0pw#E~<^B3=JSPb>vPdB79)rly3D3-1s*zSe_d(yx_*9KMa)y{{gLj@aNBU(5h|1 z67J~eXaaV^D^Pq7yYAxUNgD{jIxGM{no_8)uG<(8oA{=qmj43Gqc-?b5pBY_(cMY%_9RvrXbQEI)o7s5uZ;P<&^ z^M5?cnfq>;B=tQC6gqJ$aI>DPfhf%nmkSY5QEk82qA3xs7>RLkc2VpRo~k=K9w5M3 zcy1+Ya=llAt#1sBNP6|%mwG`grM^Ee|F6oXWjQ^)MR!c77X0FWKx9krYC94DLyIG%M?B0TY@CE2TW}|$ zU`mn&h}6Jqmj+CNU?uu5o)9^C`G1IaU-qOsJw4SV2|fJ;(FebjK(5___%}2)9pAvf z0=CHKb6ul?(}E~~ue3=8*xRn{kjL*lpYn8W)Q^XOgX3>%s8KtHyap&2jsXQ^3P*t~ zc9Z_tB_}8*uS-u~pA+zgLKJcTD0o5?^hRJp>jz2gJf{K8$Wz>~xi0)-9zdySc*_i1 zbTw#;ct82wY_(N&T_^b9^#W#_Xl()e4O@h{%jmzzNUH62z7?pH?%*vKZGneh3{l z7kqvnm576rV6^uM1IlNGbS<@DAZQ|Ij(w=mSEI%gnHdwS6_L5yYt$-ZNAh957Oi=o zx~fjcqqJM~;;>xQU$S86P~{y6mkm;o`Q}8bX!HI_RDX2?uV9?SER9C+T;6GjC+fV* zF(Zs=c7D{@^mwTb(q~CbMR-I99PU7J0wac{7sA`9zM!f0854zeroxJ^TTbILnM|yU-D7uj0*@7IDH1Eu~T^11<FZ!b0!&m3>2Rcq#4I$OfSF<8ADk9L-x zeg|ybs6oGC2%$#TdXwqq?cvTtZP^8cVU>hRl=42BH#xd)P$IeC1kvUbLdvt*tiwBo zNb7GrtQo-t9wGD~*HRx;8AqRXbh!>~gcqI;IC?ABeZmCMZv}Tiy6{ z80P#*1pnDRIU97l=`gY+t~Bm19WLrI#Qy3Fi#P#D2sCHdDSk{K@_YjitvsrSEylm_ zjDGq%`VBvK7zq(KT}VYO@pE+`SM5LlS{d9QeLBIvW8GXRf_uW?;-Al2$lJ$rI3yTnQD48L{+#g4eq?D#oI>uUg&nR?@s#1zfRoTfg}- z73^xFkKsQAMx_3A)8hxFw?ch3K<( zhD%7Px zVi4Wc{;4ldFt9DaW|dmHdd9U#tp7rMv>!~CvFbzmwp)~AeEI%dNy`q+S5%MVIKR#ne$1a*QZkCF(9XFmt zC7vCtHHZykts_rndR0ZCoBZ|u%E^*#PDOi^h z-Z+k;`P|FApBt^QD!!!G=<)1CjiaB4|D!2NW30{r#(fr#7VPt9(PEtTmFR+z!(u&e z(8v#hVXeOY5dvQJ%0VrhRp-Vp|AXAJMLpRY?l_tTL!pt8I~AH|tOzT_HVZn*pGJd8 z>t%-I$MJxt36j!RKp6EsR+x{&Xtne?`;)oaHZo&m9ak1b{fi=ot{rKTU<{8AK+Y=) zrV3_Q?7b00?@1x#F0sm8Zfthdb5_c`pe0E>X% zKiUr!JZ;~MM4K-bjFXtY1~;wx{s*zz$NTMLX^kOCW8WN`2og>IRjuDJJ3eCSE&TTtbp5)DmtHvjUEV zQZ5dvyZFo`NWRUAgC`@@0njPrch?!)Q0Luijs18MWOkfu=~W^Jz`#_kG-%zJ?Ibp@ zK?p}i%;#inh{dgbc#vfwFu*-hp1fpx^WyF+p}ON7^IQ*I?=BhvPJ-X;{6rtRsBco} zA+xyS8TFs`on*Z0vF#=tZOW(x;s{>(pnnWy$se?4s-{3&#fL;~7&L-YVb7-a@j)=? zokDzHjgOuA7=p zx7{(q!Z^{caUBrpYrM%Mco2)?l#$o_)TLhZG$0v?97qs(<6lkAV_ZUIFEGMq+{uDI^}-uc&{-BIM?ZK%25o&I*QE#o;*rNn&72`3-)ZCyet)j;Z- zsFYyv`KQWqYK;lXZuoK2WCfaai7QNhYBBpSPeK!Qs55C(^Hyox?!X&}3x3@c)9~(ZrCR?(q~nxbuYm(95Xzx40fuCi12>d z*OlA$PWoQ0oj6x#!vGFyPo4WJ=2uxMUy#wfa>3{ywRgK2VkxJ{SiEOK zvWI$(hg>#pz9WF`2rc$ovnp?lc14x-7)m#6Ujj>1idv=BhY_e!s`qF_p?%Q2)#Y^RE z5)zwVz7_S(n~|0pEd9qeVyjZZ(?SY2^s4nupz_wFBNA0|tR6?!bNbXCVhrkftLNi| zX!=*h@Yh|C4*_-)F4-h;-xE0mV{7N~UxtQJr^A^&6v( zHT+Y~L<`^YU#tTY<{~|QSr}DrW?~%=nw+|8nDAI%cT3)qz9QAa$ePuc$t)%%Zw7+(#w19@mLKKN zC!LU$V@=rZq;y3WW!D66Usa|4!kp$veP&3~L!n4p@5B&m_-z7oOcQMfMM=@~WVo?~ zMu@G7s%^wtDDvuJ9TuZeo@Db`#jil}zVBJ>X+K_vX)z0^R}v~rqhk-EZ8`RlfNm3n zjNjpkHw+`_GcHc!L<@+-gvWXmQtuS3;X7%7Hbi?%6|FjQkN@Uac~!D-(mg${5yxNs znw##^Zf zvF)vhMi~gRRP)R>Pl`~(^Lm4g>ASh6UB{cCwWfsl99b)<1jbO63+OB|Bi@Vcf1dc; zZ*rD-ZeELeUG{g^e_2sL>+hnRfDbv~64T&1rYMD$1$u3@LN??+P4NaLiw0AY9{i$s zWkKz$R$Oikr}UJc9H7~jc6$6A9ecDyMWThdSQ(5w5||oW>%U`!^qPu8Y+7yR^k`$h z)<@vN25yPGEFw>^O+V<*x&%jAks+_Xob;5Itfs0Nb;Ik7U=w2DW94M5P52yDF@&KC zUhVr$2Ay;r{Lt_LR6LCI_NP7_9k)FKvLMKgSc9^cg)>sZR;_8)w@_7HT+kyL*OL{@ zex$8pL!$n@q^5w{;rL5KEJ*yOj)bS`mS}RSI2pWyC2M4WlkxanTDlxLZZ4O^QcTks z-vCEaV~$Jjy~3%w2@Q41l1rj;mbknlmDZBNv{^21^se0JlNuj_;|D+CX?*T~!8!5t zW%6{RAxH8HVQ-dyF)f$sTIrxE9j`8g>HbP?7v?=?)mjEh(#6y)eMXC{2x@SNs)Lbp zQVuDWy=^fA1MCcd1H?U8_6mSjQ)prbOTVI+P_Ruokc zSt^mz-~DNnsafP4IUzR~xw>-nuy|D__X{=-@pd|ChW6t*NsK1tB8OPRW{E>tc?IJJnP}=X3X>EQFjh}5r9a;)$IE6TKn%Wxpsq`Zg7(#`AOrYHeJ5j zJPjiZKeSvDE4$g}(n(Rjy2+?~8Oi~2+3d=?2N`nYJj~R>U#wZ8(%W-xTKe$%T??vVEzjD{U^P2J2{g zc8t*a42o^spvB~Cj0t)~SXjTIA%3a2KC_y?^v3VQW^54Sp0-^YR?i4J|6-HZF(@Y; zlZ=s}>Gdk{*FdtahI0oHKSZwWK8iIfhx6A(C zl^C~%h4b|Cv|b@SIZ%1B^sE+m!r7&jYR=b9UnXy(=t-heg01+^}llG?f zD3*VZ@nT4HT5n77jnr^5NaDJ}^rxlVA}f!%h7&gF%N<4#uGdwyoe^e=pWAQQY^qun z(Z72h7HsVgz`l;9FkSiQyvwLKpAE8JECQLViprFf2xXY$(r$C1ZrD$M*>_S;r55DM zt0};&1R{bnVu+Y#rs^=AHKpcjpfG)+e%*&AV$h@tv=>8ps~3M$q*fAvG1T4p;^t^w zcUG^oHZgZhEhI_(vh+PZ@3<%Zd{uBL9gwqw7uKcKwn~8T#iP5RqjjkoYs33WM9@@W zwWuW*6=zqQyd&qrJ~?CyReUM)o3EbNt{uD{vv z;8}arf&mk`s?;eF%;r2KT>!MnYF-YgiBXSsrp7rr%+fsZBTdsE8?APkJJYPlq zN?bPG2a27mZGh?L5Atvou#ke49KNBCUc;tTLnZ)KZy~sbdAKBLFwq+QB&+)&16E`O zzy2Hi*)%&|(;{gUt%oPht$X$PopgTGuf(^_UvOa9J|~3M!5YVsiRNqtb%{Sn0^(>f zHZ)OXzuUb8QwkN|OG+T;=b04xaQTNJYYapy9nLJtw(NBeFlBw=Br0+NRO-*A{x$|L z{Ow1?p+7f;y%1M98K#`xzoHfSMP(n203J9*>`{gcv_g(cs>1M5r|qJ=s~lz};ysO@ zW2uT~O>4=1YE*}{>_;99-{`p2?Gett#<^H7FZQ9>SI`ilG60~z--J{_54=S_y_Zou z6Kl|Gswt!AD08aGV1b46R!q#A04;===toj|*-pUS*Vs&}NltHi26|;|(CbTlaAy2K zxM@6N-QMc+(>FK0DhUZ^i@jjh+@$_M&LX_V!raPZQ3-ormb%c8jOR(V5P%5%eI%uvs9VTq+w{2?Qrmasz+6Y?47VQ=%L2vSB6IZa4RLQ$Z4;k&RVRvv z)b7BIKy6+MCZ$a;m+SMu@PT)HDVjao7ME7-t$N`WV%(H)Qjk}r4qWWnYBV6aEMK86 zNy^zl^=k#n8P0rWR?%PyB2iaQ&4FcjupXcw$FoBuMW`(?7CsA}p77vM>2Oy2`<+bz zek@%b_4`4f2xopOOen<3{LR)MThH9EPExWtTZk$Jh1#$7NZ?7jSg0ndLEi*Pn2mfH ztY?$;s8ND^pf_$u*FjI}{z?a?DbDOnq?QeH5}DzFUD@o|FWS)x-o9?pqU>@A_yP?9 zHB*qo#m!ZJP&!Rg&5}yRtcK9%4l1T)fGH#Te?T47o@YqR=)+M>{9RuP?}6|w*Jo43 z1&Gr5`JIQb0jS)Ge)-9DD1JtXNA*Xefx>j0GNiT^t8Urq%nDM}^4dhs2>YQoj~)Ww z=oEZBBSr+BqV($Wt5fStHVlCAnCNGaV;uB#sz=t4u}>o`Lu->%>xtLl#S!Pl)-a)= zi~38G;N!z3wOUZLw*!I7Nqa?28`mBTa&yh_z6BT;!979I>SRm33=#=>-LY%rwQSe_ z;Yd&$FFcwmGRCe@)S$~PiB*tUM=)j2z zKX0VIi~Bp$wPVclXP#l#H3z9I5F8yP18ltv{PxNq^+=n4%%#UFfIrB{^s7SOxX(mK z*%4f}hQI_NDBO7qk!=afLO>t8X4!76Q8@;(|H-Rr6^;s#m*EGFcr zD`xeC8D&>J$#FXLO3#=7Bz+>GbE*{zts~T2yFvl|jtuL3qaR^EK`JBau61>YRo_VZ zgf(5I2mH~yuZhYg+<(C-fE+YfwZE)C)PSB{PUd#LiZIT>bVV5!&kuXnq*|BPz-OdB zV&jz&o3v^0rR$k~RqQir1%zInK+|davQ~1iI9A;s|67$yfhd?ZGNnH~11t`*=@cP9 zX*!%3Gi5*K$}(YJ6lT?Lca)AgVO7WdRw-tB0B1{qYS)86>ZIVuGsTGwAMJ!krE?7= z-Pt20z#6F~?af~r6#Xhr_a;`3P^aUwn8F#Ns9R){;HR}>;EvvLS<>Lucq-n$S<{7> z$VuO3Y}IQ$_P;1CFNJk4go7(<7N^dXtHZ}iL>$-@=+lW=@mT*kt?az#0oi(aBm z-|RSsoPmULRjhwB%$-OGTxZ^c5Lt6va3#?qLy3iq4obxAKUG!gVfJ2JdgBAg4g=7rmeZqtPuC+>mTWfh|%WTV} zU891)Xv(HVRK_eAz%QI7Lf(#A&;?Jc===4EiJ|&F@Rmb|SMg^LR-RAtjGM2Bsp3e{yQ$`N;oUqhj zMbKmA_p^YR@rR~6Ruw;T zuOg=s9e?}KNj^}oQYOb@(EhMWDnxg(w6#TQ-t9<_#VPYs3&2`#n;{>jK*h9j_&t73Ef?sfW!a(guIpkhsDtY!5KT#bW6 z?)U~jM#NRC_X&O$T>ZRJq$zF#i3ZTy=1XRUN+$gposDU;l4c&YAP|BwrtIJuWmpzh zci5H+k`;TlYp?Vkh)xvcq(h&tD2qnr+`#34;Xrt}%XC3Xuk^8fu8n0wcYN+ZL^ZVTWuS?VB2;Z^N?)hLl=5q|u%%YpD10EC z5ChTk>7wEL(J4I!aas0@=^{A3{iT9w(|s>SZjB&C32FFq- zm8CD1dz0EY#B_=$f>Pj&ZgNvniT-EVDc2wBEJ7f9_Uv&z0BrkcSt;hZi-CQzQo(*D2sm!2S-g%Qj^>i= zPRR&@UzLj~-{++@qnmC$ct8A?R2&?Ki{De*dAZ-JhsNZDSxt|aLoBEpe1x#{?F;*? z{_AZp!&N6)IcqI@v#L!)4IP9Hp!}Pa-h&-aPL2KgTRii*y%mFZG}KB1P7jpKjKbK3 z28h&meGVz2>0c9>9J-C;dj>7P7OC!*po{=bQn&h&q5rp?57W!i0$$-Q_h zDWw2o7ou&uZz_$cF5RR@r&myplm$4*$i3U5$w@EiQE`abe}4EFf7e5RSrNasm#uX8 zJow07JyQ})LxB$ODI&`%nEd03zqG5GKczw$!D-U&2AC*WgNYKy!Og?7W&LL4QJkFa z__YJgToM8}@3x@x$cWzx#elG|T+p8GJJah&J95kCc(;7bT8mG6;ypsrR zYDc&^@VJbR)fkjHaVbebg6NaWI!$UplmJFIS<0D3M`r{b@`SN@HC9DaPicwzy99P-^PQqHfs~+iAjS8u$CGQMa zVdJlV^CAu=+Xn~2{zsLnSEg?~`P3%eX4?Ho-izd+a5#pbD>r6^hZD0#)W>IEaV*}KG$KLfR%s=ve@|;&AOU*nnW#{e zE*C}mvnN9A2(#G2Es2+nu(=R4)H1!o;2h_IROe-Aw8V&#?E*6>%iotSpr*>Mh*Gk? zmqq;6Rou(X?pxa8<))09!;o8uW3UHxh{3Dbf76W5w+|CRoXm0UK{f;-2O z86?Q^`kMxGA)cWs02Tdo2Y(DS8sd{#RXBY}lAIJG32$TF-k~dEP`569R?h;x3j%~%PHMBdJ;$YHvnBaj z5H^4gr5LLF{oRX{W~lsS=)fQa+H@z}?*;M#wf(Q}xHSmD6e0FH@jN?!S~Y;~YxF=d z`TZIDIzO# z4px`E)VttbeNRK}WN!AMe6D%VL|!%LHgaH0>6>OxU-8vv!;pbFVWP5-XIZ!R*~ z#Ar^o@^hE`s6mMwf> z)#3bvy;0cvopiYb%aXk~Hz(Yo!hVt@)ovv0EfkMyak!nblI% zfw>`eFo=tz@z;sy;`@(Hf<+%!7)yfaS2cloc z7*JDnSnqU0H_4fRx#Tb;+0W3r4DKtBIuBe4w)$N2s){R_?P;LnVL#lq0#~OF^vCCV z7uJUD29#7C<@u0NLB-CvVtvA>D48ctg(SeZD)o)ZM&ak3r@17p^|dFzWlPCE5;TmVS1m`XR~5I(26$Ay{eQKY9-t&7Ru>Vq;pdcHEC2s&hRV0RErsU`vMim{ z8ru?ZaBcLTI1=dAy{c~A4I2$!fZrRbg2{~N1UU2C<(0mqFq^HH2I5_CJd8Cx1;iWs4@0Rrj%`t(rf{F86!(`$7PFmohs@B&DPIIe2NF4V(_z>bC`T zyPv<0l@YBAv)Uh_g<4Rzm>-0bn6evFlqw-#bLR{{cwlu*-+5ty{LOS+UkwJghIlgQ__Y$y%&(F>L{E?o zcc_A(eWHg&|2m3<4>J>sKPQk{zFmM$n?uGYkRoXP(f{&@>t_{5*YSgUPU$v-0tTjV zi}3daw|O_8L%yUiHyaBF+y(8}jzqporxfR`wdLgByik4KLAhlbM_%)yK%OOAucH}6 zR4!_4u~*OF1F_*>$H%Lcsl@C|rHw9+`sU_J=I55FL5Vwa8g3<@C4PL!E^fH2&hB_> zdzxbKvMy)RsI*dlgqjvFUxEK6yhf3lF1-~)vQN1vU%6*1n7GXv49Tv%@6!F?;qPGWhJkE{1iax=Ko=MYnrulnDOofad3gBptpHu zzun3f_?09_eBXI^a%-?DtPR_v3AURV|;NECQGLsrC$E0vhsTQbnht(%Zs>f%Ic%2Kp&@W zHl6IfWqm;lzuTsMQK|FP|9cl8yX82bSY5^WD(1;=E$YCOu-sh4rq&8cS*F(fz;|QX zAMINB>ibl?7<5 zne`ujPKRC5Tl*uk(i{BCa5s#Z+M#!g!-UvaEH{k%bYlc>{A`hs$&SD2MFxPXyF#51(g{m{`c zWXelqga(Du7b<=}qp4GoKKyf?*_4j86XRNgB`uA$)HPVl5K6v~mi~ipeN1RPBIHc5qi4ZdkvMpYg2kZTJ?pTr%PFYWs--gf5p_2$%KhPi z;UwcowN&`vsr&V^$Cg4+e9z|fOH%ulBajl``|TTRcNhf!R2GQ!88_%T#fTt>YJT$4 zxp-=oF1#G)Bd$Nn9{w6-28|ccN_=zhyC&mQ-xC@?Kh$e|RaHxYJjAP0kutQm8yC-_ zZ`0U?aNmBXYQ@^$>6gCa*6|rz1x3)+=bNqjkt;}spWov>x6m7$aw$8@UDfgwW)1xq zk=xOB`=bYcH+#85IyD4R`-LApbF$!RN*x*&P;uQ7vQpyEscaSl=nicwG1HQ1t&UZB zv542M<#ZRK-=go^hT=hCq{b@m$TydB&r2uUAOGSQc&*le(zv@lJ)8Z@>~A%l2rK8O z4iLgY;kOOdN|-{PS0b^L-lJ>&pHmR1GarvwvF#SwiJ_bt_VS?v=*H zGknT)ed>qMEyk&MGj?g1K?xg`s%J69_uyV5UfOtPyU$>fhE6?-I6kI7E|yNGy=d4h zRUg7HKjyFe3I8Nh$Kvnzhpi1cOLaaL~m4i0WlKlF}ab)9HPK@LcJPP=i&A zaf8<$G&Nh}0cDl;DtC&Q1j@dDG%sJP#D!5BL*v-JZGU9CH%ANt)32~oefT;;i+{qa zSc6%;0;KF#iX~g);uFRvm3}~I!4{2&)QJ9(%njx4aoL8mS*(0DVtYbQYxmqDErV4l z-qq?K@XMzsDLY+oSDz3n9G+yeXw(qmmM`B}j7EcKhn6LikW$Xe9hQu$%8`HWy|KPe z$;AH6(aS)1H6dEJ*PSX3$!WeS82P%Qx_MfC{Kl>a6iiwWE7^K7Yr7e?%Fb=l+3f8m zNErQ!VXe+*#pZ-;Ezt3t@=WMn7DJSUuxY^9S;nc1?Bz4!RtFZKC8e*baq`@ZgL zKJV+g?$GQTa;W=N2u)8fZYk#PaW*8eC6tZuJTUNhFryGBvybHENA%n0d0jb;?~@A| zA5re%-7!%cJ(Ha%*m0Z@QqPIiJJFWa3*XHHPn?$xT+#s1E$Tw4_22S*S5JljoiO%l z4dI)e-cE=!2jMyJO_u{lMfC&Y(4GjMc%Frou|+Qrc;d!NhbNJbBKe#nl_7S<{=FaR zKz^a>3lqXIYO}^>JJEab&(k|h022(`RDDhVtm==6o`WZrpeS<@TeO%QN1Q>s5^Wdu z+QCKGuaLmaz^ZbXN%ICx&5vLGW4F)chdk1l8SvV9)`(H9OpauEztTqG8a0z<(O-%$ zDEf>l#Eq(w9>pH0bj5?SW3NLxMn^yo7=ajm`S)IvNUFExHzAlwdze5RMwg?#;P4|o zrs9>{_m*&@R}+yS>=J!dy2`4zwsyS!BqdVIA&DDS(kvFze1RqG!~~uN2mjIo){g9T z@DL{_urPjEod!qAcmT?sKl9>m=L4}E&Kz)HSw{K1P^ztt@5(H&P)?3wl^8p(-1&r& zXVd|g{4rjq{9>g3|1mbM@^qJG_LhAed!Grjc;P76>OdtsPRfWxa9a7fY4$GouCh=!!+;-!|fl6*(vE}lfie`EFyqriS zIk7W!<(zxe)-N5_PX;E<_ow{@&m8wJv%oicuSP%iot=z}0Faz~9|dzTJGpa3gMmuE zYiqZkNQ>RFSJ$m3D&H4+M`67NI^O*Yk=9K^!<0@TT-4rzhR-^(C%wtHvVL*6E+Y z;u5R%Dd`Bf;jKXTS`i_k%20PynSn`cq%T2Q%Ii<-$(5!rW-$J^+-3jZNhOV|FH5Ox zC?58W)Py|w#_FhSD3r937VIFS8u<3LsZ)fdsBU$*yQG^P4uvMu@viytBkZ$wQ7^T; zp&W$DBaLfOwfgE^&!zeqQLo}B#jk`+7m2nWpVww<7$0C3p!&FLGH$he1x~p%o;W%& zPFckKvh?K-C9dD@{Q@8Y45?V9##lOHOj%0}w)FMiCUuzXuG5ULqv+-DB^Mtr^S4b! zhO+VFPi7}xHrCa?Aj}?~q@o{gggHBI?rR>;s0$)4@p3xdj9?F{^F9tlmEIaP5^oVP z%HTQ^=p(@Y*>mh}NB2(k6i|L7!7^};)#w2yVSd=Mw|6ef)_vSRN(V)+nn!-UY6-3w z)<`99sL-ntwMFBY`K)J_iG#mV_6%_Qt9&EBEAYsNe{4P&Qj8-y#@M;xk=%9V-*bwd zazTL1kJ8_+`C)J7Gjs#SA2LSf$e}`cg%R}I6CA#RN@y89YxXCt zJQ`@>97Q*T$W@cQ9Q@gV#IY%JnN>19-T(wDlO1I2aj4R%0;j&mo+ma1@gN8(Z@sK9 zRP&Xnu;>wj@q6PUm@e_=IL%`%=|bG(d6jPljgiaD2gH`fU66KvP1;$5yx2u@*9{rR zMF5O{~U zuVb%RZ^LH4Gw|5w9G z@y^Cs>5r&V;{dYVJ8YK!O(=e_zG{h^!t>AxQ@t(e;s~Xj zU9G9tjP>Z@%=s6wJyduepb7bqY*uI|Bz{v(!R5@VZ@o&tS(oe3Nw$3TUggU0n;9z~ z;m#jYEJXtO+}L|#w;@Nk+x*!H*YZPp^R?N%gahe*Q*DtvmX8}tQes*Hdp~RLFaX5G zxrk>CaZ<}`m+VRhJz`Te^Uyn>#{{o4b!Ai4$B@>ISE?fE8*AN?k2F|?&e>g1Pm^fx zlS&6E7}{o?vOeam0+rgx_ln$@L9D?;HvIS$HA5)k(5>HCTjYF5edtj6ZAmfft6h9O z3m(^xy2M(-{C|B$=WKKm`~28%#!D~hV=I}C+@s76e;g+*@aGS_UTG=upL7JFUb6b* zYt~}j3Iw&uzhp_3^%A$!5UL1lXoUFXULB%3(&0HDRis!Z)9-*;Q}yetVpIg47QFDn z^2nom^E7NQ(QM}6F}C{g@5vm#xxFY_jBe9U*?_e_xWIWsRpcP$j~9B^_a^A)w<;}^ z^^13!86>usbjaEAla6$PhM$O!L>+H)BWlVrDOU>jC06YVOVBpkT@0h+Tck=JqJ1s|iXy2`5J7+`t?Z5;)JnpU&$wzuzLiLs-Z&G;BWU8h zzH9ev6xSI{hcYZMu+DMY>hnUsb~OR$+pv9wXfvATjk@bcBkU&^ap^0hD+f8S{P=tRE0Nb^_tPKbWwlR5V(V-v zp?r&@%m&xe_GAw8@xKwvULzBDWgPKtBXChYZA^Z(metXVl>FZ6BX}D3%X$D^v)?@x zspPeJ4bLB1pc%FGcEo|G63Eyc``o_DqeVtf2pTK#KlOo#VxMWh?echf1qN zaF!vh>!^Q<+`i9AK-O5sM^zD$=g17G4|5i{^Y!pq=RuY$$0loabsk&8%60E}i)Cu( zV4fJxza$>qOVCapou3#{DE|*%Q1>aqU6ORws4yFi@N{js)#>1!4qMCJp1;-@UER9u zOXFq8k0$DPFSqbKDVsl~2hOhg*V`H^!I$0_7qwZLji_h++4U%^OU&?% zk~fpFxriFX+Xk(u>UVAGAc(B7K6uKslMvp)Q(0X8VT z8DGKT{54WfY&pmN6T`SpeP?9=YOtL-a7{o^+V|&;o&-hpsN%g2TaZWPvmEiQ=DT$h zGaOrgr8I?rO5hQx`}a!pXIhHjt#T)8A7Q7fNM0BAnO|Y$I`<52Q^A+!#o;%}DeaT% z45()WN6B}Y3sca~L@PZ|EX>6TwxC5AWpH#+XSIPYL!xvP^-1p(6T%|GQ zg{MvZ?$_8jEM%aT<0tg2S$rN{!;2!sRAa6=)Lbkt#$1&7Uq$vN$s#Z z{EYKQ4wXEAwkf7$E%I;CijL+*5c%uyDu=jl031{9mJ&U)dTHG0i#qCV`&2r~P~Y;+ zaDT;zA17CNoz^Fg!oV@r6Mjs-`exBR+F^L;9-gOvjy59DhTeGr+q}VQn7ejx!)0={`cs z+3ZE=@jRJJT0Xn`(7bHnCVX~khj!qwJ?(qcZ=s$$rv7UlrhDIv$l2gyWlX#HM>0yD zHGyD_eoe;aBl>ZtDo)g9K&Rg^(Ms(Rz)5bXF8yN5dy10;-@~O_(Q{QDxP^W_>^?7V z_(YQ>2Ru>BJL^a2S#ceV&rp1V{<+|hl#Iz5kE~EOcp^72{2to}p%b4wD`t!#uSO3k z&iTo%+V#t$Waj?15ic;lOz53ky&5yNR|>p~ur5RG7wA|S_6TLP4{5ha&6D)7z#b4w zd<4T*xl!)fDH+*&e|KrSGWHw(Cj@1o+!cR>EJ*&4D^|LRFUXZqu0)aV()B+;6)tXO zS=}qy^G1E|CgHEKQi|Sa^FMph+DBoGHulXov+%tb@`C0orUe?f_Bkd~(hT_yw6lYYG80<`gZz2gt~u znYdls@~5cU{vyi#X-&PLErsMKm(DIajPbDA=UCERqrzZVd&mFUf`p%uqJD&q_Ng55#o1^{M0|dEg`(GM~c!s7(U(mh7mW znM-;6&9YavNkGdXhuwCx-?W6_vN!%lXR9wQ{HgxL4DsvB%xd!lp zi2T@0%unCKUvSh6jFyCX`FBUp_fm`hv_Ro}{b*e*;;b}dJlcCVd-G9(ahB6sBuGiw zMBb9^zOGR18jf;IdD8e}1Qy;|``@3My}_YH-*Hh549qS-<_i9je{V%yC$a-yQ2q&( z_l9Kmp<5!lg0iH0`J0O=7!YIAtj+awb@V zm{r^po}7_%WDU|A#QavM#eZKHedjqwO?_K#!0e7?F8J|?k4VkVVrO)*+HCSW6| zz;joe!_`UwII7tHsQa~Syf_uSGC-CUf2k(EBFZE~2RHw*@J0lCWIT*N*0c6@+bEaW zopUCMI*nli0sU?lu(4<|9)!>iU^yF-G_bBaI4-EO>p+zjr&s&3a*YOb-nhe+Z5h6`*AS!GrO^pfE}sb>~gOyd;@vrV@A-g%aQWDL@`Qo3m!QxUmxR6x33&BAZ;s( z0W}V-Nr+5Po#PKwsl>#ums_4rnFhC*LJMp4CtkLX4LA3Uf_@zuztJy(ryBU;`zJzA zjKHbW1`d9^H)#e^-%cw+4K{b{mFv=93#H7Q%|l@PuAqqG_Cc0_V+H}vrBRK7WRhL` z8D4lKXphDm4+wOmGm21|ApYT?)cl4QsCv{(<*=QgHbT+9W%J0@t*WU zG5@D$UD(B_{Oi9Sg?VjdW&sS0Tsa$TR2Av3@Mo}$kbLrx_GYE+KJjtEfJ#@F*LHBC z_w3yH7e5dFg5YU$j>9heu($0|^dnRiz>e`S8(5Y*0ATRj?=2zg+ssHK0jey+YHQTdJZsQgD)N(|xQ)ODOc*;P9py9k_ zyuxPN#KW)sd(j(c$YPATbEi8L4L?u7k74yL#Ei)=RVEQl=)Tgw9U1YJY&%s$QfXC!uoHay&y! z18#U7>c8)=iZMlnTDZSw*r{%NQ^YEM4sIwT%Oa}n-cLSb2CAbPg|axj3MyfsHq3PL zqkJRPhlalmwhN%W&?U^kOVWSM+@%+IDI-kfLvKA-yACQrW9h%NmEjGEX>GjFT?N#c zXEN(l>?vGS@~DJ z87&>|CLTd}x=1}9bTeHQKLXVQgR1H6rAN^k%!(=YJz z;xCrC7MKZIl`*K!AsW?l?MHg5O=D;-xZJ3)vvsQWi94dXzFw+=N^UfZo_MwFb21#M zS2(Jqs{xXr{P34Z^MjF2m;LXI4gcwYzM034*HXyMp?$K@IImpAtmsA z*z81JheJ@vc{CQ4XU(Sj1((-(blrgESNM?_bn7kFOb|u#(qoogW$QJPGhw0`46OIs z{v^g+RY*yakZNZ4T)YAG&{4@hHEdKRwb`T(Eb06`o$(s)GQi{?&TWHL>F;&<7!^~L zQ7@k^q+3o9Kt7`FwjZu5DKADy_Xom0?#kw?k4XUway{!jLTjMnO+$R}Yx88oVzqAu zIF<9gkGyD=rsn-i1Q8WqcD{{M06Y=R2l+_#)3cLVwf8^w_7-^b$*xMomqQSzW{f@e zQ-_e@kDD*py27D#VfXgNTl9?RRu7d?ZAqlWvqK+VDfe;e3-?xYK*iQG96;6N5kKo} z*=w@=qCU4>9hE$$vr}8Ymc>Q&;bA*%1h&o>6l>E4D8u^PlI+6P3&p$lLB1AIs4ZA; zCM*s|2E_NuGg+RY5>uKbz7;^A_^_G=~ZmS6jy~< z;q%w3R(!)Jo$72a_SU36zMX z5!mw$LjkBo$jyI~!pb-ZpnAMd6EMfj*o@n+D{L4*C#ei{lB6n4QFnX7vJ4}1n_?qg zDfbK0^TOxNxx~|_llVK|!qO|-@HqCB^?&t|x&XpOfwLJEA(z}!;b|~}1ca0Rzs}KZ z)n1kOEgFZMXFX6Xe07W>K~D;36JV6HqZS^K70TMPJd3;=zxB(zoe%f1*M5U%CS(tz zA1G_vR)qj#xW8$ENq+bZ{ONfO&9$kehJ+A%klJ`n)j|jb8<7ig7=Fm}_DMJiVDmC4 z!20};&+~vryYU0mnRGt~n%wIk_k(xwJ3F@caCSF6c;yomKhst5A^6qU$$pVrcbJn9 zRetx0%nk4~a6N_=^HSsJJI!iF`iTs zTt~s+s5`de+4FYV1fM=26ZoKC9}8(1{FQ8riFY2 zNF1a$!v^i+_L<@anHKrgJ2a=emOQxe_GWPx-|5UzL{9xbzKpPlcQ2q1+lL}(g`Ql1 zsNQjs=e}lqN~PcG=iiBcnFkFHQS*OuNV(sSUHeOJU*~>?t)|#NYM=g()%do5%8Mh3_#d^>Q);Nca}w$d+W%+2M-;dI z$21$+)IUD>olg0O{nKe6F8P1E5NL(0|Lv{AZQk`ybDTN~f5odTw50p+v!0k9V)PEH z_xm?DNzA|91OLG6Iyi~6(IoX>{`q7SDfEoYsV+AJ5SP$$cZsix{Bt|pMKW!;7_o%? zzp$_kzY+h$>Mu9#B?ckx;9sV{JA%l#c|}?D6Lrv~jZ@1?GQ&UJ&X+Lyr$0ZCKyd1G z8erYk%e1q!3z=TRga@dS9u3|QV26_c*t`J)*Hg+&;Sc_Ub=+yXBdqsILSE>NI=u#L z_{iz_`SATdA=p~g$&&c)=E)j6xeFtB1}o#Jd`BlOA3Un^NAB`FzY6ni6_X`@8j&&jwA-utToe?|$w5vt?X?8=gnKSD?D}9~hNzBl7P{yTNfPAKsyP$-Vmh zAa5Wfk5Y5zfA)jpZDrNRV;yYluRWt+VJE)4vEIRG$!#vo`y%F z%gbAu?Q|e12d*(}a#q5@6r!_99461HS@YzS&?}Oybf$XdnbQTVtBPImKb3n6jj=a0 zm$-?m9seyTYLxas<++qulJyqe{9ni#=o$}cSX&!3P6V+!A8;J%lkJv z_tP!o-5c*{_u4b?p40V-ASwdY3PfWQnU$-484sKzwaQkA?!v#1-HQkH_v;nvK_34o zJodb3HH#)Bs>E0nGJg4+^~tn#q@J`1+j2Cq>1|nv&58Kf3%ah^0}Pn^kFv^LGk_h?^^!bc#8KuZ^K@Z&Tv5=U(4G{5S02dQGhQ< z=D#f5eAT&)3euuSu~of6w(U&B$yZSOOPg>KB8W|em7paoi`lUk;;Jaz1QBprot2`$ zr`KcbC;jWjpbbIy;iZb%rSXSuae(!nuci*0{dQ<1m`pUieD9yu(@d~P|7_k1g zNv%(#10BtefO|gL;(5Va_!yX*kV5?1HvKQ5T1B%c7}8h+_gq{5y0!)DPa&OC_R++P zn9WXO4`)31fd^5e8w8chP3Rt7(aWsJ1B6tSRkbY6Wx(&fn+^ zaggIEC&JI87|1~T2~v_S4!J3omy78)!xE{~c)$7$+E|bfO{4So3CS;bZA`E{Cf;(K zl4RA^pHD~Xjq3AV$2PIYlaJ{zhLsA#{X|}Uey17_vX$pdO+nRa0s$`Xj~Hh<6-{$HAJTjXEDB|9eDj5 zB`8k7eX|CsdAX!2*c@q9C4X0QJ}G%i@SO+1Bl6y_e@{*AAqEUO_W_3p)Js(e);_sY z-#x(}Dx zf?$U5tLcP{)PBKqeOy5cXvx_L(G-sy{#~^cmC3W^ z?oEiGtADg|hK=PjF-Jdmf(mpDc!`HpUUZq@|^8;$IviC0(s-;CFTukbkJ?H>P`B^OP+kjgkXw_iqlPeJ>Z~M1WjP-GV za~FK+>G`SMN{`u(XsK?*3M%7B-Vs>1o_u>-4}N;&l!w=(8uVc~GtxHxUnBH5jNaeY zB=s8hfKJmMx$87ZK+vs>OD3L>?@VVh{tdtcUPM$dQ~bi!l3H`1av>q|{Nm<+YsBIa zQ}7R8BqQ`B#UBn|VF=eR6DC;IY8pN3g55|V*x|{#0SV4^wwdC2u z;GUEB94$HLjR~!OrBj)_N~>K%CG_lbAXjlgE$>Q?GxkHNgZ35Q&|B5|SB<}jEAktF z)1yFFpPu--OBzH^6sI&%`%c^O6S$#!MG}&E{uSS;ag(bI^z`HMa(UbJu|zgLKj4)U z?DYZtP^fEP8Pxdx=5YM?^Wz*!L+pIMaBsYIP*he@6@$2)9K~7cC5FZJ@%9)R-fhT>`$KHr# zWeU+PJ`HHYK1)4AX|>dXGE3ZZ%YInY@ec_JA1KngJ{d4{S@`*wKhZjgeaMe+DNwZA zx>GN1q5-krI-U}h$uvFezCh0#l>haT9H$@UJ9jlGr1&%8<3=I9lQ6B}L3}}KT2Ac; zidE`y}i~21j_{m-XOoRTX6mLJI+mI z5So{sxo~OCJz)B=TwJLVt{^*7SD%5nc4i+ku_1|Bq%SkA&yR#D8Y2{-Sijk#^-o&~ zIY3(sdAY4iI>3%2R^N}dJcAp-lRRKuFw4<5Mjnd|O~#5D}#tKT*L(Pg6kyIjBik z*gS6mZ53z25-B=iqblPX6X%n+P9$8zlwdJx=Fgc+V{Ju}RebWi$sY{#guQv}RfqzY z1EA984o%?o9kHgJD1n7AgFYzD$nUj1mv4TW^xP@3)T_2gHmoegvfeBk=BBOe>@v`j zBD=Ske!BK7O_+l#(sge0Q(!FM3f^<0&W39I!A=MujF$8&uFO&308fVyKiZLiG;P%n@U_%OL>ZoSe+N6cOx6@cUK=pWfdy9O21-xH#P zVcdBKE$y>;^`*vnL!!*R*|ID)!emm{>DElJtXl?ToTp2j6Qha&mUB;9{l*ix5o{_~ zjoeVN=gWy7?j%F26CxCb`&4rEz_&2Q5A%DICup5h4xlpvsalQ)4`Czjegxm_0B($1K(~_)2z)GB0bDM_&XfPw9{|PNv=^g73S0oZM!DA za_LiZ1_}G}10YX+cGShS+-~uV?9u0dz7fcO2e?(l>E`QCp0Pp*%fA#TPCmcY9IV5C zKwatpQ0?3A`71AtU74NRYHk4vWoRpfVX>`Dk)Ce4$X?gHC%H*`KVkJJuCk}g34y)( zx=D+&BVA((s=!WUKlE#qx>#h1swdK+%EGXPSK&`^b4pmajwo1O@PvZaF^On5V_Ef- zyTi`Gc^)1A2L6*M^q__45B<~|%l3YxUM)3{hm^fMsjHEqdArCrCw=UzvDE9kv+2IG z*@m{wP&jW&`zPXkp80*Nxc@GO(@^~IVm~AI`_6L}xi3L-up8vD(^?90m#Z(nyqW+z zLv~Zh-exI%6jRw(STo-vIa?Fhc1H8@r?yRZ{Xp5y?!eVmnxl{1^@6iKzRTs&N3(}b z)g+U*u?SLeU46TI?2I+>3s=A^d=0LQ4h|aVNWLMwGW$V@)5!iQB3RT31U50w0>kwM z&l0JJd|jDmpG0*Q8>-#Q&7SyA#dXo>i#jn8QIOse|JJWh$X|YqhjQv%{&(@qWEJx5 zPVjxHLOrnBTyMYX#&RHUxrJh%L1-_M_NrOM8_9v#8n||fJw3F$-k@-0zA!?QU3)&y ztc_5rQE<7l;I`r8SobUa{9rbgYS|!u=k{(Z!+X8y5 zs7<1NAuz$&RN9>7AoK9|bH7}X7&+&=TW{UuJ@{jCIkLpvSV+N)+1`{CcG39Dgy!pMxfAr>Mi zo@_Gos*&|(;iHcqrm3I6TEjU+6?wZ+$EgcUV1|KK;Ie0VuGbHJ^DI zKH&QT@6sxK3MQC8ycYjUEzPHTzmgeTJH3k2W$c6xmyv)WL`z0_=mL-X+}iwtE4c3{ z3{*7D@U21z{$V-`hQaS2<1BoA9iJeA*RCSc&@4zESsB5F-{P)mr?T}h+ovSxMNRj$ z3oc@FHOk{2VI&QuVFb=X53qVX^-J^0`UUGS^=78l;(3MOwRlPYSUfWa%WU!~^@2%Z zA1fC(m5h+#+euPg(<2p(?c_)1npdbf5!ceO*)aXu`g4)_<*`1v*K&D{8QGCivh561e( zMHznFZxH_2d)x$#T`4euC_Pqo&1a-}vAz^mB|wTDvanWr-?kceh~BvXH=A^Vhmsr@N_%8&*;|PaG`;&s-MFmEhTo3*6pI&P--h<`q;}!h}nYQX5 zsiO`!M!t=m0pqLhhA+bo4-&=Ho0Eeq%F+QO*eu39H(2$7>l{KRB8q8=du?hqoUARP zZMt*&TH)Dzdj7m>$%U3V5Aa=ABBJZ@{bivGj+wscm#Hu-E!!HcEDodGgsjuSfV>#; z+b12_IPN|56ecjf&Z0k{*8S?02Ke&M>|E1DNs4J}kM#A^i>YkiP0f zlr5NHP}Cyqg-7>5(?GO;@{Yy>eMF#5h>gdTRN!55|0 z;yKu3ZjdHj+l{d4j(h$t@FO}4(*OOiyF87iM!>w%m5-=Hdk6F*>2<4bEVbbJwISwN z<_CZL*W13+rv2Q)V|mKgw&&`{QCj>HU#pKVHl$P!97Ae9Xsc7{fjD6ZTB$8jxx&J8cD(-g^P?zt*2^ zE(5z95uzHcPsjNIg8S^gG;D}6;_weTbx#Y{bC-`l68O1#FNi|SRe^I9{I>L-$tbJJ zKFz6uLJq^I2V~pS+A#FInlCKMwncApSaaDtY1qz2sX%U0FNr|?Qsp&T=Xuty^TMK6 zGT|*ZzXl&mJWeqClto;km{NgKVkoDz+I{L!JuV!8nS2@3TGg(MUyuoBn=-upmre3< zSJqh)0nBt{e_tTIK3G;c=oB7I+CGS0W$XJ8^B+5WrDT#e< zQ5Ma|rNg5RIJGj9gWTob13|mjh-d+7R|B#Je_xFONFYZ}V9VbA8(WfQmPdu8>FCMef0Hopu-7sqK{{p0M~@ zyBHa^Hq!zR_EHc{&<8L2Yz|$D7Wh zfVom(5GK)>+lJm5=##w$2Uk>|O6EMvdq555(2m*JyDYvNrjBBU`M-pmX<-zE za-h7ArFPUgvqoEkKU(%-Ooqd*k4!ZVb~r|$SPJr3xX%2!E@MOnqjFCbkhzYr`SM}& z^IM9E<4D+2E{KNu%963W+Yj+e-27Y&ARxa@f;=E@;I>KWukmt@Y>1g+N3`|k&~&)* z?IB;4K4+%WbW7Jw-V}!F#{4;_HH^lhKrk9vC*N?e3ds(h^m&7gycqh3x14x^BIy zCWS#zqR{4x9FVv}w9}=mnDA=&&kuVJ|5!+I_M>NCd^s81{0#5SdI%7vSQ}svy%=kJ z|B5LG;VtAS1|>3#3NJ7T&r-EH(lf~$D0n~86(TJr@~TI-=}^niSy`X#7DGM6>ndfV zr;-CHZm;WATwJmQTV1PfFgoy5x5;ZOVo%Is$8hiEcA)cAVho9^?i$kQ1N$?hwsnBl zn)ePlu^cetmk2(!wuY$ea}5-j2OR0WT^;#tNiH5Jh+#}0obiA;`)Sgn&rT|RLIqb5 z26`&trNTqD>SVa_ehz*J+Rb|($Nnlx7AXjfm4!TLji&bnjp=^1YA??8a)f%;a zDwycHa3%IrnX`nnd#V<`_#09myne&6@lgEfZ&S|j@>U>xY9Jk>31Y5>c-bijxUAO%3FDE9zfMx)37BCvi9BVwjP1Y=E&!D^*TwY} zJBwlxk3C%CAYE2o(+^>Z-1%MXpN{4^olsKnI$fVxaHK@YRUy4Ww#df7!Xz4Fyuv;0 z*%sKW_m{f zCHhm@>Ns$EvL9K%S6=gCY}dS|wF${;k?_r6Nqb;WdvCc6E%l zYNo8zZQlIiP6jXGBBbx*F!?@Qr^#r#UsD(J#es&-bvt_*<5KjDWr4L4#{p za-!LY?~$z zddOuziC{7}_!I6T@6rBL2D_iLjacgD5 zIIP48SJ~8F{eEPt>hq2V1S_?#LA3uf@0$nskY8cbk^+z}TB0zP{ngr4>en}`YBCZx zIe^WBMCVk+90F^L**%0r-2rmmA_=8Km~y25I_zK{0J%K_hyQC}0(0s;gNu{_w@d8r zrYxnPM2R7Sd)=@2>>Y>LM3s>GEpsf8B=+@Ez;5yRBaM2C>4~D%y9@v(NF4JD_u9jB zhpG?nGS);ujPerswl9If3H7keU@*n_43DETm1xU%{- z;wWVxTppDX=`Rg_+)FCwxkW@Tf4IOEMG z95P+W+}j6yyGg^#d;Z)P@`)NK(h8Yc?VcTE`nZ+)guHhA063JObX@lt(nmkb$eZ=a z0SVhBA?(k+VP`eB$uQmYrkY=&i;)}^kP2gFPY_LW`1Uu0%(Ks~LB|qc(ZI|v_hZjb zt?6g&$2uUo1?*S>?$lcqci!D5OjEj^+$rCCZ}^7?@`io)ed})}hQIw6hd`Rz4Lf3o zre>|iJZK9vblh1i-;)8Bu4gPWFm)TcP_ebPzS8o$A3CEBFBUn`lm>XZbh?O-iL~zI z*B6u2CNdY;2Q0=?NOe1v{~F&z(L>Nv+xYx-x*4lro&U3;2YObLAOOhxWI@A#kfDp* znVJQ@r=hyf2R05ucMkry+8vUX-`O6+f{^NgG+s|*tEY|@t=jl$=~lXp5%!@}14is0 zo!I&uC|WlA-7+Uo`)>%W{#W)*uRV&jXMMXZW1zUk`0~yHF_D3p>tBZ~8DKf97i|lB zTmTrjhX83pSG^1WQc`8>irons-3fv<$m7q3*yY0~E=v1_6>4Eqp#bn2D719sjM2dkb5-{>fXfx<9OhCF9~CWmdXmI&ygtVY)EYtgy2yB?bjDNm z(8Ke~nl9sr9Ei5Qmqv$pZs*Ko;9Z&hg|Y~F?@7x?kL+SBM*G2HDL z1~~Xl4{`&ndM?f9cLM=8%tj2WyFwTBQ1kA@pl?>tI#`Q~xdqfQ|+!0yz^v)MA?}M(hw&lzEf1|YC&{FL^z)hQH*Hf`h0wze7 z10I*2i$p}c+p>6JpLnUBYIlpywdT$7^PsE30}N*$&s=Ed2b5~5;Q)_+8D%``V2lQ1YGMzwHkpXIa!Ua*m$HTsX0-hJhn_8d&M6fB;>(|G8B9K%1Fb;H~>hVIm@BRCE(AQS=9oVA_zc zL<%8gszw*sarf(BW{$Req$hOY_9&Q8V}&c%M$cc=oK$;m+gE% zJM^M`MA!#14$HjsgRZf(G3k@9Ngs1#G|NL9xQRe4CIDI0`Rr_lg0(GXCwM_8@ezDS zi=5~u+j5D;LEB)$w0_&7q-M)@`h+tDaPU+q(6$5s<>q@q+F;1o=QH&XhJzn0C|2Ze zoxy$Di<>D*5&_;q5eFSnTbU&8q6Oijj?# zj0*EQp0(H5^LZpf*r0gpC_TwqbW*r~@8NRD!jZrVH%dm0k*J<$$BvmgT|24COt$3U zzG$OWAQVK@;B0UJ3R)aESz1>(7G^#lpxX7I1ti-^FZytGy;m`&Vs$%B7(BY4+U>HoEK<>64aUwBgTQM8fI$Wo}7EM*TNS%$J3`%<=yER7gs z7d|44qvvu6zjK}EIp==Pea?N}c^^+~ zB#wV#%tX%SC@8ov*LyP`s5fTHTPs#xis}|SVLSZ(Gt-L2RT&=37o*m0Qu^8hPpCEa z(ar)^26LSE(2dn~A#1f2jjN-GF)qbjZXnD~V260qzUOZm>5#dwA=lpk5?(yg@&*p< z+_ogx#ig*eMg<%zNtBB1NSl5pR9A`xf^s%l;`^SV@eRtj?Z@%tWunZ$=uyxg zjR7K!Q!UPHCfP_7P1L*RhQF}l#wx8i9Qy6)_FD62X`7!T*+7)ieE!{T^svkN7D2r> z@0;;?fMzXG!&5D8jDT*Ot>v<*>`zzQj)*QX$j=r5tR9dLMxD2L7Fu(HLe*8?_v6&f z)`2zd0QbkKvvlayz%dNc!Ea3IxA_A0l&~O&*`gcz!823y<*lRY4Lr`ybK^*BV1d+@ zQHx0DG%XwT+pClIgPRXWBml1XqkDv4kMnmMo!Tx}e(b~H@{3-Z;{8DH&nT2!u(-0? z>!SM(85;?d_>(0UImnEYARt|YntWATqp7#UhL$0d>=kp7*34_5x|J41lN~Lysdu$6 zC*H+g)Cs>GSZOZgAvE)c&m;E)Hfku{)ODe+ZIY(SUKmCinGP>`h4P1jip$smS%0W@ zB9MYBn*$i38tWqf-oi_@E4F$&u20yDvS;_pv)j|I7}w^g))otZ9zAzXL?6`j-`}?_ z35wU}Y`cIMkN=T-on$Z38s_1*Nzb0AVFX|L%NSlh0`j+Z9%*Kyk!IdkblqvLS^o#L z#+dl708I8({%Syai;tu-Gqx;g2k(3I?CK}3IDCn88It8nX*TnmaKZWN(=8J@d7`8L z1onNJ>xSGNG(ty8l&bQQy!rk9jWxd6fAh?M&wGFza(@B-s~u~_tDph|n}z2ec5?h2 zywB}RNwlNn`#s7$-WsjczCd5Q%^(q!fh$g+f?RQS)5YglZph-$sdYkwX`c+`zfEC{ zzVB3hR;~){m(~0cJEeDbG@$P`HR!thZ!mP|%}G#!u>1S-tj3jOrdSfctleoxpz-4a ztkp4jV8u=i#Y__`96uR({5fHj<_6yRXeqxJE8UuP6I6GB)0wcqQY6-~#A8*4PwNR4 z|NU_L=nFe#7b`fe@&Y@R#lmb;&d4)H zknSN(|DSyIDu7`IAVaEJz3lZ_7e6BJJr}++bc&uoa~rHk38sYL)^fxa_3>H!7Q39L~P<^ISW>A53x_~Y=H@v+oAk)gdd^nsxa zC`&rp`5C)zY-*piPH>6-2&oRMRPevz4FV7smB)D9bHrwA&f%b%T|Vrb^Qz7LxN2wn zB@9INh{tQDu_*72k#jZC4K!X_&6-v<7fUat{q8nqukhoLt})}Z zwY?*~>=ox$4C$bO_t6$mh@p~W?eIZMR6#})Tl zE>j8#gvTs|sJnDW+_*D#8X|y0Hi+ zL=bIUdTnGwCDYQXB%WqK9w+i=>iU29b#2eCc+3v=dzjCA z!2&8i^~G?^!+59<0S(R894$2QvgKXljVRxsyIsj9SNCtuz7jT#yqF6rxa5v|#(qMb z+P@WA`4Y0__3gNQ_^Kc$Q;~>Zh`NhSP2-!WAs2-klnqK-KYu;0e9&{&J7D+g=j=~sGC2XKf#TO}Gv;l@24z(Q zca8G2b}}ddRZYiiiyewpzD7Qz+0vi!0p2U4qOd3*O?QHzTOXeN~Y5DWEjb%!X;=(0_tA z+r0dLwkb<3`5NH=c=LTJM{Qg3IWV5}(UkSS9K=?CVkYLcB~S%wNps z=ybk;;uK8-)O{fb$}&cWHWO8UCdrhOJ{Z6%~(Tz;Q+3`s>_g&*=aHy!yMAiWdSJjQluZMS{WNPQJIja!{yibFN35}?CY#7^pX&*YfqvgZY4nR}6!~~>a*GPI{oHC; z4cN3pkZKaZ!zk^Zd6qTkdrw^?Z82r@+f-rI%yy}fK&HuQBmj9+YJ``PQ`SNVB3p8Q z?G|QrmP|U-O(YAaV6xqn0)7ii>o{AhcgKaiky{|Dd9@2lw<_WzMS0*l#H#M6h%Qy` zAGut0;wdC4M03dG#)3||9LT?uK^c&fu|Xso@RYiZ#n<>8$oIk9@6FFC12`KJrS27= zi_WZW9WMOKVg!&-*!tQZhUfuN`M{eZ z1165ZvsP#=1}`3_ZTdpJhYek2jGY!6K2 z^_nZT^jaI2NoR;~BROpfPu$(DG7SO68i2yEz)=ITRb<2EEwG{rc-Q*N64SbVqSgyP zb_ul#(#vWyUx-!u|u84Xp@>v5t>OLsytAR*1v0y`fYT zj#__;bw_t0dXyema9zd_BP4TrejXh-XZc*1XwbN4B zk(*F+RXy$S7x1IV z(!Z?$fB}V{*83>D!N4ew=OZ)1b+c#uib;v%!uRM3-DHJsFw_vdCI;~Bg$kjjuSc3J z<0l|~ndn2NH?6Hku8N4^pXuC_`gBTnT4!12M<1-T#%FIc4q=d?yqYl+%*%0&9?#>s zt2e0+fib?VxyfQqI{%&PYAPaYvqhTq!r%_Kxijn#vgE(;?yUS= zpBHs+o2?R&jrF}IvV>0OFYi=CxPQ)sOqyR_yeQ4-`9YN8Ka($UTniec()hjsav|`h7_;`^eW*`5|A2-XIxFMy zva!&bXIl8xq$w-T^P|_Ymp}7*q4GU#-_PMbo3LSGs7bk7yF-n@I5;EN}@-H?W&c0 zhEp|w{A0r-;8k1E`_5E{9CgGx;d)92zpPhR2~h!MNQ4k zBr=)&+C=*3J)A?V3YeaG>q2X`33}*Z0mbt2w^N#^g zI`2xD587~W@5#{K6KPG~nDjQ5Mw!mWnS$LlnF!yQN#7Wk*KHx+>c6YWsD?5hoAE%kp@p4mO-$o0fnd}ybcOqsOl{wUoVRB6kfoC4!*nQ8c+!{J@ w+q~yy%ADIHX0*lkBlW)fWmoe-9&F|Tu|37X;CUx6FyjG14b5-W-1JEJAFF;yr~m)} literal 14827 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

PyKO{s*`bXd)=9~G4|1-jqeWBc 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 T