This commit is contained in:
Markus Ottela 2020-03-06 11:21:13 +02:00
parent 0202e4faa0
commit 0ac650c2fe
87 changed files with 2058 additions and 1194 deletions

0
.coveragerc Normal file → Executable file
View File

0
.travis.yml Normal file → Executable file
View File

View File

@ -80,6 +80,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
MIT License
applies to:
- The appdirs library, Copyright (c) 2010 ActiveState Software Inc.
(https://github.com/ActiveState/appdirs)
- The Argon2 library, Copyright © 2015, Hynek Schlawack
(https://github.com/hynek/argon2_cffi)
@ -119,6 +122,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Copyright © 2011-2016, The virtualenv developers
(https://github.com/pypa/virtualenv)
- The zipp library. Copyright © Jason R. Coombs
(https://github.com/jaraco/zipp)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Permission is hereby granted, free of charge, to any person obtaining a copy
@ -1210,6 +1216,9 @@ Public License instead of this License. But first, please read
https://www.apache.org/licenses/
applies to:
- The importlib_metadata library, Copyright © 2017-2019 Jason R. Coombs, Barry Warsaw
(https://gitlab.com/python-devs/importlib_metadata)
- The OpenSSL library, Copyright © 1995-1998, Eric A. Young, Tim J. Hudson
Copyright © 1999-2018, The OpenSSL Project
(https://github.com/openssl/openssl)
@ -2469,6 +2478,7 @@ Library.
Copyright © Guido van Rossum <guido@cwi.nl> and others.
applies to:
- The CPython3.7 programming language, Copyright © 1991-1995, Stichting Mathematisch Centrum, Amsterdam.
All Rights Reserved.
@ -2482,6 +2492,9 @@ Library.
All Rights Reserved.
(https://www.python.org/)
- distlib library Copyright © 2020 [Python Packaging Authority Developers]
(https://bitbucket.org/pypa/distlib/src/master/)
- The python3-tk library, Copyright © 2006, Matthias Klose
This package was debianized by Matthias Klose <doko@debian.org> on
Wed, 7 Jun 2006 15:02:31 +0200.
@ -2909,6 +2922,9 @@ OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
Note: This unlicense is meant only as a description
applies to:
- The py-filelock libary by Benedikt Schmitt
(https://github.com/benediktschmitt/py-filelock)
- The Reed-Solomon erasure code library by Tomer Filiba, Stephen Larroque
(https://github.com/lrq3000/reedsolomon/)
(https://github.com/tomerfiliba/reedsolomon)

View File

@ -94,15 +94,16 @@ TFC is designed to be used in hardware configuration that provides strong
[endpoint security](https://en.wikipedia.org/wiki/Endpoint_security).
This configuration uses three computers per endpoint: Encryption and decryption processes
are separated from each other onto two isolated computers, the Source Computer, and the
Destination Computer. These two systems are are dedicated for TFC. This split
Destination Computer. These two devices are are dedicated for TFC. This split
[TCB](https://en.wikipedia.org/wiki/Trusted_computing_base)
interacts with the network via the user's daily computer, called the Networked Computer.
Data moves from the Source Computer to the Networked Computer, and from the Networked
Computer to the Destination Computer, unidirectionally. The unidirectionality of data flow
is enforced with a free hardware design
In TFC data moves from the Source Computer to the Networked Computer, and from the Networked
Computer to the Destination Computer, unidirectionally. The unidirectionality of data
flow is enforced, as the data is passed from one device to another only through a free
hardware design
[data diode](https://en.wikipedia.org/wiki/Unidirectional_network),
which is connected to the three computers using one USB-cable per computer.
that is connected to the three computers using one USB-cable per device.
The Source and Destination Computers are not connected to the Internet, or to any device
other than the data diode.
@ -114,8 +115,12 @@ Optical repeater inside the
[optocouplers](https://en.wikipedia.org/wiki/Opto-isolator)
of the data diode enforce direction of data transmission with the fundamental laws of
physics. This protection is so strong, the certified implementations of data diodes are
typically found in critical infrastructure protection and government networks where
classification level of data varies between systems.
typically found in critical infrastructure protection and government networks where the
classification level of data varies between systems. A data diode might e.g. allow access
to a nuclear power plant's safety system readings, while at the same time preventing
attackers from exploiting these critical systems. An alternative use case is to allow
importing data from less secure systems to ones that contain classified documents that
must be protected from exfiltration.
In TFC the hardware data diode ensures that neither of the TCB-halves can be accessed
bidirectionally. Since the protection relies on physical limitations of the hardware's
@ -183,18 +188,39 @@ the Source or Destination Computer, the ciphertexts are of no value to the attac
[Exfiltration security](https://www.cs.helsinki.fi/u/oottela/wiki/readme/attacks.png)
### Qubes-isolated intermediate solution
For some users the
[APTs](https://en.wikipedia.org/wiki/Advanced_persistent_threat)
of the modern world are not part of the threat model, and for others, the
requirement of having to build the data diode by themselves is a deal breaker. Yet, for
all of them, storing private keys on a networked device is still a security risk.
To meet these users' needs, TFC can also be run in three dedicated
[Qubes](https://www.qubes-os.org/)
virtual machines. With the Qubes configuration, the isolation is provided by the
[Xen hypervisor](https://xenproject.org/users/security/),
and the unidirectionality of data flow between the VMs is enforced with strict firewall
rules. This intermediate isolation mechanism runs on a single computer which means no
hardware data diode is needed.
### Supported Operating Systems
#### Source/Destination Computer
- Debian 10
- PureOS 9.0
- *buntu 19.10
- LMDE 4
- Qubes 4 (Debian 10 VM)
#### Networked Computer
- Tails 4.0
- Debian 10
- PureOS 9.0
- *buntu 19.10
- LMDE 4
- Qubes 4 (Debian 10 VM)
### More information
@ -209,10 +235,9 @@ Hardware Data Diode<Br>
How to use<br>
&nbsp;&nbsp;&nbsp;&nbsp;[Installation](https://github.com/maqp/tfc/wiki/Installation)<br>
&nbsp;&nbsp;&nbsp;&nbsp;[Launching TFC](https://github.com/maqp/tfc/wiki/Launching-TFC)<br>
&nbsp;&nbsp;&nbsp;&nbsp;[Setup master password](https://github.com/maqp/tfc/wiki/Master-Password)<br>
&nbsp;&nbsp;&nbsp;&nbsp;[Master password setup](https://github.com/maqp/tfc/wiki/Master-Password)<br>
&nbsp;&nbsp;&nbsp;&nbsp;[Local key setup](https://github.com/maqp/tfc/wiki/Local-Key-Setup)<br>
&nbsp;&nbsp;&nbsp;&nbsp;[Launch Onion Service](https://github.com/maqp/tfc/wiki/Onion-Service-Setup)<br>
&nbsp;&nbsp;&nbsp;&nbsp;[Onion Service setup](https://github.com/maqp/tfc/wiki/Onion-Service-Setup)<br>
&nbsp;&nbsp;&nbsp;&nbsp;[X448 key exchange](https://github.com/maqp/tfc/wiki/X448)<br>
&nbsp;&nbsp;&nbsp;&nbsp;[Pre-shared keys](https://github.com/maqp/tfc/wiki/PSK)<br>
&nbsp;&nbsp;&nbsp;&nbsp;[Commands](https://github.com/maqp/tfc/wiki/Commands)<br>

1120
install.sh

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,16 @@
-----BEGIN PGP SIGNATURE-----
iQIzBAABCAAdFiEEE3wqdU+qbbuozmTV+rAVyyKvL4QFAl42DPoACgkQ+rAVyyKv
L4S3Gg/+OujW2IlEDBpxd97jPRRH1L3UZ3tHOV2VuV5hIukkblOLx1UZJbWWL/VC
/Q4Yd9Xi6f58Jwz/f7RIFBzp1xNa6rEcTYT6CBTvzsyxDyrUQQVgGzdJhuYHqoRk
j6b8SLuxnafEEtVjgESoy0Ei5bSgs9l4aZU/Jd86ClUI0yF4SWeh562UWGHXObVJ
/RtjrpnKn6OnIVY5QvbYOpTk2Q3dd0sz26/pxykptselzN+2kFCl+4mtu5oT1bkx
+c33lp3ihyJyNpEkEqISudtfR5FfQlq5ZbQRL9p77Y9e4ePUG6wlUN0dlm7oXS2/
uS1Y1+U1wQNcMjisOo/bZs9wPzatfP9cl9I5DCl1vogMheSYKuORR3kc0FR+cAuX
/J0KryZrMP3kK43eM4LzHdoimGyX6D79Wdy2cPZ70QpKYrCDjSawanmX1ZxxPblD
HfmHnJ0Inc1o85lf5l/PYy3xLQrQbBuUIlctBFWbpW7XdUqKS9HdVqzxGnbOAJnP
C59O3EpkiMqV3I9zn3e85wMzKy4xrbrm/asl+S97BdHzZf8xGdvBRBwYK1OFfzKL
7fqxJDfkkOoTyrC0vhO+mbm2ktyR1oOjRCsEitXWA1sYmz1x+NbuaMQoiwpnefFG
JG1EYzokYahZFTz3NrfG2IK+2vOr7TV6KlWasQLKboleiMNQEZw=
=v3mk
iQIzBAABCAAdFiEEE3wqdU+qbbuozmTV+rAVyyKvL4QFAl42GLEACgkQ+rAVyyKv
L4SRQA//ccJj0h7tRE9kVu0Txi7BXDBzUQCD7c8yhTqRxoGTTzRj1bHrBDDeP2e/
pd2c5MQLNFE26pnGxhpvVgEFfSWSxQxxs5BgPhFAj9V1Bn1CrrBJYueuupBo8An3
VdRzqArBljGgScGOPXECeTwldoWY5ugtxREwQlBL7JCix9wmq4/yghzE61YdN5K3
tx+WVj4Y06SWZAni6nssYiBYrToAslAgTlyAtaCYJccOUpHgnsyfqzZLH7+a/6Lz
NiHTJHm9zsZ6KzzpgnhDNMhTlZK9m2fuwdVMU6JjScNZA9gswTdKyi8kPeVpm/1g
m0LZAxOLcZuMKNdG/Wrtm/174yFIURoOmg7rF8m1FKHwvLQa2+FICICx7CLBiASA
Z+vVzfI7py97/hiVTNFDTlKENk4kS9Auhaf5pI6f2v/ehKXYnTYc8sLSUk5MYVWI
06ZmMJ3cvD4P/NPr7nCDT9WHUx+qKMnSQirQ86/wSxK3KcjE9Fu8Q8AXTYVZSN11
xtCCtDkrd6TbxTwl5K54syoerg9PqkiWnRmf0gi00LuoJExg8i4Td2jBMVpxRJhi
KGGIj2GhexiB/slyz2kEZsmIkZr+dMqHTxoQwSoop9Ev0GHjkgkGa10LxxoRAzUg
x0A1+8TJo1dOs8+GD5qN6N68ZyMQhlAmp5b2EED0lrbQVRkZEig=
=/l6F
-----END PGP SIGNATURE-----

View File

@ -1,5 +1,5 @@
[Desktop Entry]
Version=1.20.02
Version=1.20.03
Name=TFC-Dev-LR
Comment=Developer configuration
Exec=terminator -m -u -g $HOME/tfc/launchers/terminator-config-dev -p tfc -l tfc-lr

View File

@ -1,5 +1,5 @@
[Desktop Entry]
Version=1.20.02
Version=1.20.03
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

8
launchers/TFC-RP-Qubes.desktop Executable file
View File

@ -0,0 +1,8 @@
[Desktop Entry]
Version=1.20.03
Name=TFC-Relay
Exec=gnome-terminal --geometry=94x25 -x bash -c "source /opt/tfc/venv_relay/bin/activate && python3.7 /opt/tfc/relay.py -q && deactivate || bash"
Icon=tfc.png
Terminal=false
Type=Application
Categories=Network;Messaging;Security;

View File

@ -1,7 +1,7 @@
[Desktop Entry]
Version=1.20.02
Version=1.20.03
Name=TFC-Relay
Exec=gnome-terminal -x bash -c "cd /opt/tfc && source venv_relay/bin/activate && python3.7 'relay.py' && deactivate || bash"
Exec=gnome-terminal --geometry=105x25 -x bash -c "cd /opt/tfc && source venv_relay/bin/activate && python3.7 'relay.py' && deactivate || bash"
Icon=tfc.png
Terminal=false
Type=Application

View File

@ -1,7 +1,7 @@
[Desktop Entry]
Version=1.20.02
Version=1.20.03
Name=TFC-Relay
Exec=gnome-terminal -x bash -c "cd /opt/tfc && source venv_relay/bin/activate && python3.7 'relay.py' && deactivate || bash"
Exec=gnome-terminal --geometry=105x25 -x bash -c "cd /opt/tfc && source venv_relay/bin/activate && python3.7 'relay.py' && deactivate || bash"
Icon=tfc.png
Terminal=false
Type=Application

View File

@ -0,0 +1,8 @@
[Desktop Entry]
Version=1.20.03
Name=TFC-Receiver
Exec=gnome-terminal --geometry=94x25 -x bash -c "source /opt/tfc/venv_tcb/bin/activate && python3.7 /opt/tfc/tfc.py -r -q && deactivate || bash"
Icon=tfc.png
Terminal=false
Type=Application
Categories=Network;Messaging;Security;

View File

@ -1,5 +1,5 @@
[Desktop Entry]
Version=1.20.02
Version=1.20.03
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

View File

@ -0,0 +1,8 @@
[Desktop Entry]
Version=1.20.03
Name=TFC-Transmitter
Exec=gnome-terminal --geometry=94x25 -x bash -c "source /opt/tfc/venv_tcb/bin/activate && python3.7 /opt/tfc/tfc.py -q && deactivate || bash"
Icon=tfc.png
Terminal=false
Type=Application
Categories=Network;Messaging;Security;

View File

@ -1,5 +1,5 @@
[Desktop Entry]
Version=1.20.02
Version=1.20.03
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

19
launchers/tfc-qubes-receiver Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
# TFC - Onion-routed, endpoint secure messaging system
# Copyright (C) 2013-2020 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 <https://www.gnu.org/licenses/>.
cd /opt/tfc/ && source venv_tcb/bin/activate && python3.7 tfc.py -q -r

19
launchers/tfc-qubes-relay Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
# TFC - Onion-routed, endpoint secure messaging system
# Copyright (C) 2013-2020 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 <https://www.gnu.org/licenses/>.
cd /opt/tfc/ && source venv_relay/bin/activate && python3.7 relay.py -q

19
launchers/tfc-qubes-transmitter Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
# TFC - Onion-routed, endpoint secure messaging system
# Copyright (C) 2013-2020 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 <https://www.gnu.org/licenses/>.
cd /opt/tfc/ && source venv_tcb/bin/activate && python3.7 tfc.py -q

View File

@ -139,9 +139,9 @@ def main() -> None:
ensure_dir(working_dir)
os.chdir(working_dir)
_, local_test, data_diode_sockets = process_arguments()
_, local_test, data_diode_sockets, qubes = process_arguments()
gateway = Gateway(NC, local_test, data_diode_sockets)
gateway = Gateway(NC, local_test, data_diode_sockets, qubes)
print_title(NC)
@ -167,7 +167,7 @@ def main() -> None:
ONION_KEY_QUEUE: Queue(), # Onion Service private key from `relay_command` to `onion_service`
TOR_DATA_QUEUE: Queue(), # Open port for Tor from `onion_service` to `client_scheduler`
EXIT_QUEUE: Queue(), # EXIT/WIPE signal from `relay_command` to `main`
ACCOUNT_CHECK_QUEUE: Queue(), # Incorrectly typed accounts from `src_incomfing` to `account_checker`
ACCOUNT_CHECK_QUEUE: Queue(), # Incorrectly typed accounts from `src_incoming` to `account_checker`
ACCOUNT_SEND_QUEUE: Queue(), # Contact requests from `flask_server` to `account_checker`
USER_ACCOUNT_QUEUE: Queue(), # User's public key from `onion_service` to `account_checker`
PUB_KEY_CHECK_QUEUE: Queue(), # Typed public keys from `src_incoming` to `pub_key_checker`
@ -184,7 +184,7 @@ def main() -> None:
Process(target=flask_server, args=(queues, url_token_public_key )),
Process(target=onion_service, args=(queues, )),
Process(target=relay_command, args=(queues, gateway, )),
Process(target=account_checker, args=(queues, sys.stdin.fileno())),
Process(target=account_checker, args=(queues, sys.stdin.fileno() )),
Process(target=pub_key_checker, args=(queues, local_test ))]
for p in process_list:

View File

@ -1,57 +1,72 @@
# Static type checking tool
# argon2_cffi
six>=1.14.0
pycparser>=2.20
cffi>=1.14.0
argon2_cffi>=19.2.0
# cryptography (pyca)
cryptography>=2.8
# Flask
Werkzeug>=1.0.0
MarkupSafe>=1.1.1
Jinja2>=2.11.1
itsdangerous>=1.1.0
click>=7.0
Flask>=1.1.1
# mypy static type checking tool
typing-extensions>=3.7.4.1
typed-ast>=1.4.1
mypy-extensions>=0.4.3
mypy>=0.761
mypy_extensions>=0.4.3
typed_ast>=1.4.1
typing_extensions>=3.7.4.1
# Unit test tools
pytest>=5.3.5
pytest-cov>=2.8.1
pytest-xdist>=1.31.0
# PyLama
pyflakes>=2.1.1
snowballstemmer>=2.0.0
pydocstyle>=5.0.2
pycodestyle>=2.5.0
mccabe>=0.6.1
pylama>=7.7.1
# TFC dependencies (note: not authenticated with hashes)
# PyNaCl (pyca)
setuptools>=45.2.0
PyNaCl>=1.3.0
# pyserial
pyserial>=3.4
# argon2_cffi
argon2_cffi>=19.2.0
cffi>=1.13.2
pycparser>=2.19
six>=1.14.0
# PySocks
PySocks>=1.7.1
# pyca/pynacl
PyNaCl>=1.3.0
setuptools>=45.1.0
# pytest
wcwidth>=0.1.8
py>=1.8.1
pluggy>=0.13.1
pyparsing>=2.4.6
packaging>=20.3
more-itertools>=8.2.0
zipp>=3.1.0
importlib-metadata>=1.5.0
attrs>=19.3.0
pytest>=5.3.5
# pyca/cryptography
cryptography>=2.8
# pytest-cov
coverage>=5.0.3
pytest-cov>=2.8.1
# xdist: pytest distributed testing plugin
pytest-forked>=1.1.3
apipkg>=1.5
execnet>=1.7.1
pytest-xdist>=1.31.0
# Requests
requests>=2.23.0
certifi>=2019.11.28
chardet>=3.0.4
idna>=2.9
urllib3>=1.25.8
# Stem
stem>=1.8.0
# PySocks
pysocks>=1.7.1
# Requests
requests>=2.22.0
certifi>=2019.11.28
chardet>=3.0.4
idna>=2.8
urllib3>=1.25.8
# Flask
flask>=1.1.1
click>=7.0
itsdangerous>=1.1.0
jinja2>=2.11.1
markupsafe>=1.1.1
werkzeug>=0.16.1
# PyLama
pylama>=7.7.1
snowballstemmer>=2.0.0
pyflakes>=2.1.1
pydocstyle>=5.0.2
pycodestyle>=2.5.0
mccabe>=0.6.1

View File

@ -4,16 +4,16 @@
pyserial==3.4 --hash=sha512:8333ac2843fd136d5d0d63b527b37866f7d18afc3bb33c4938b63af077492aeb118eb32a89ac78547f14d59a2adb1e5d00728728275de62317da48dadf6cdff9
# Stem (Connects to Tor and manages Onion Services)
stem==1.8.0 --hash=sha512:aa2033567b79aef960f8321e4c6cbc28105c59d6513ff49a9f12509d8f97b1a2e8a3b04dc28abb07fad59b0f6ba66443b92bbefa0d08b26038bbaf24f7f2846d
# stem==1.8.0 --hash=sha512:aa2033567b79aef960f8321e4c6cbc28105c59d6513ff49a9f12509d8f97b1a2e8a3b04dc28abb07fad59b0f6ba66443b92bbefa0d08b26038bbaf24f7f2846d
# 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
requests==2.23.0 --hash=sha512:98e4c9435434b8f63fc37a21133adbbfeb471bfb8b40d60f04bded5cbe328c14a22527d54ab2a55a81d93110d627bacc26943e55ec338b7bed8708b55e15fff3
certifi==2019.11.28 --hash=sha512:fe5b05c29c1e1d9079150aaea28b09d84f0dd15907e276ccabb314433cfaac948a9615e10d6d01cbd537f99eed8072fbda7cb901e932fbab4f1286ae8c50471b
chardet==3.0.4 --hash=sha512:bfae58c8ea19c87cc9c9bf3d0b6146bfdb3630346bd954fe8e9f7da1f09da1fc0d6943ff04802798a665ea3b610ee2d65658ce84fe5a89f9e93625ea396a17f4
idna==2.8 --hash=sha512:fb07dbec1de86efbad82a4f73d98123c59b083c1f1277445204bef75de99ca200377ad2f1db8924ae79b31b3dd984891c87d0a6344ec4d07a0ddbbbc655821a3
idna==2.9 --hash=sha512:be96b782728404acec374f446b11811f8e76d5ed42d4673a07e883220f5ba2a099a8124cda5898c3f5da7d92b87b36127e8fd42e9edb240b587a380ed73cce93
urllib3==1.25.8 --hash=sha512:f7fd3b54b7c555c0e74eb445e543763d233b5c6f8021ccf46a45d452c334953276d43ecd8f3d0eafefa35103a7d1874e291216fc9a41362eb6f1250a2a670f16
# Flask (Onion Service web server that serves TFC public keys and ciphertexts to contacts)
@ -22,12 +22,13 @@ click==7.0 --hash=sha512:6b30987349df7c45c5f41cff9076ed45b178b444fca1ab
itsdangerous==1.1.0 --hash=sha512:891c294867f705eb9c66274bd04ac5d93140d6e9beea6cbf9a44e7f9c13c0e2efa3554bdf56620712759a5cd579e112a782d25f3f91ba9419d60b2b4d2bc5b7c
jinja2==2.11.1 --hash=sha512:461bbd517560f1c4dbf7309bdf0cf33b468938fddfa2c3385fab07343269732d8ce68d8827148645113267d48e7d67b03f1663cc64839dd1fcec723ea606aaf4
markupsafe==1.1.1 --hash=sha512:69e9b9c9ac4fdf3cfa1a3de23d14964b843989128f8cc6ea58617fc5d6ef937bcc3eae9cb32b5164b5f54b06f96bdff9bc249529f20671cc26adc9e6ce8f6bec
werkzeug==0.16.1 --hash=sha512:4c982970fef39bf7cfbb4e516864fec0f8ec3f743ccb632d1659c6ee415597d98f4abd63b5c0fd999eb43fc0c89a97123f07625b01ea86b02ef51cb67a2b148d
werkzeug==1.0.0 --hash=sha512:82a0f1776820d07e929daa60bfa0a3e746464b0f2923376330f8ae5abf535bcb756c7384757b2ff8e0076f299fe85d96ef34b3a8eede21c11df9aba8cc58cb77
# Cryptography (Handles URL token derivation)
cryptography==2.8 --hash=sha512:184003c89fee74892de25c3e5ec366faea7a5f1fcca3c82b0d5e5f9f797286671a820ca54da5266d6f879ab342c97e25bce9db366c5fb1178690cd5978d4d622
cffi==1.13.2 --hash=sha512:b8753a0435cc7a2176f8748badc074ec6ffab6698d6be42b1770c85871f85aa7cf60152a8be053c3031b234a286c5cef07267cb812accb704783d74a2675ed3b
pycparser==2.19 --hash=sha512:7f830e1c9066ee2d297a55e2bf6db4bf6447b6d9da0145d11a88c3bb98505755fb7986eafa6e06ae0b7680838f5e5d6a6d188245ca5ad45c2a727587bac93ab5
cryptography==2.8 --hash=sha512:184003c89fee74892de25c3e5ec366faea7a5f1fcca3c82b0d5e5f9f797286671a820ca54da5266d6f879ab342c97e25bce9db366c5fb1178690cd5978d4d622 \
--hash=sha512:d8ddabe127ae8d7330d219e284de68b37fa450a27b4cf05334e9115388295b00148d9861c23b1a2e5ea9df0c33a2d27f3e4b25ce9abd3c334f1979920b19c902
cffi==1.14.0 --hash=sha512:5b315a65fc8f40622ceef35466546620aaca9dd304f5491a845239659b4066469c5fb3f1683c382eb57f8975caf318e5d88852e3dbb049cde193c9189b88c9c0
pycparser==2.20 --hash=sha512:06dc9cefdcde6b97c96d0452a77db42a629c48ee545edd7ab241763e50e3b3c56d21f9fcce4e206817aa1a597763d948a10ccc73572490d739c89eea7fede0a1
six==1.14.0 --hash=sha512:a6e7e35921ce8f2f8e79a296ea79a9c3515ff6dd7e777d7892fe4988594f1b3a442a68ffb89cf64530b90a32ceeea00e4ab9069bb697629ab4eb7262c68d1b0f
# PyNaCl (Derives TFC account from Onion Service private key)

View File

@ -10,10 +10,10 @@ stem==1.8.0 --hash=sha512:aa2033567b79aef960f8321e4c6cbc28105c59d6513ff4
pysocks==1.7.1 --hash=sha512:313b954102231d038d52ab58f41e3642579be29f827135b8dd92c06acb362effcb0a7fd5f35de9273372b92d9fe29f38381ae44f8b41aa90d2564d6dd07ecd12
# Requests (Connects to the contact's Tor Onion Service)
requests==2.22.0 --hash=sha512:9186ce4e39bb64f5931a205ffc9afac61657bc42078bc4754ed12a2b66a12b7a620583440849fc2e161d1061ac0750ddef4670f54916931ace1e9abd2a9fb09c
requests==2.23.0 --hash=sha512:98e4c9435434b8f63fc37a21133adbbfeb471bfb8b40d60f04bded5cbe328c14a22527d54ab2a55a81d93110d627bacc26943e55ec338b7bed8708b55e15fff3
certifi==2019.11.28 --hash=sha512:fe5b05c29c1e1d9079150aaea28b09d84f0dd15907e276ccabb314433cfaac948a9615e10d6d01cbd537f99eed8072fbda7cb901e932fbab4f1286ae8c50471b
chardet==3.0.4 --hash=sha512:bfae58c8ea19c87cc9c9bf3d0b6146bfdb3630346bd954fe8e9f7da1f09da1fc0d6943ff04802798a665ea3b610ee2d65658ce84fe5a89f9e93625ea396a17f4
idna==2.8 --hash=sha512:fb07dbec1de86efbad82a4f73d98123c59b083c1f1277445204bef75de99ca200377ad2f1db8924ae79b31b3dd984891c87d0a6344ec4d07a0ddbbbc655821a3
idna==2.9 --hash=sha512:be96b782728404acec374f446b11811f8e76d5ed42d4673a07e883220f5ba2a099a8124cda5898c3f5da7d92b87b36127e8fd42e9edb240b587a380ed73cce93
urllib3==1.25.8 --hash=sha512:f7fd3b54b7c555c0e74eb445e543763d233b5c6f8021ccf46a45d452c334953276d43ecd8f3d0eafefa35103a7d1874e291216fc9a41362eb6f1250a2a670f16
# Flask (Onion Service web server that serves TFC public keys and ciphertexts to contacts)
@ -22,15 +22,16 @@ click==7.0 --hash=sha512:6b30987349df7c45c5f41cff9076ed45b178b444fca1ab
itsdangerous==1.1.0 --hash=sha512:891c294867f705eb9c66274bd04ac5d93140d6e9beea6cbf9a44e7f9c13c0e2efa3554bdf56620712759a5cd579e112a782d25f3f91ba9419d60b2b4d2bc5b7c
jinja2==2.11.1 --hash=sha512:461bbd517560f1c4dbf7309bdf0cf33b468938fddfa2c3385fab07343269732d8ce68d8827148645113267d48e7d67b03f1663cc64839dd1fcec723ea606aaf4
markupsafe==1.1.1 --hash=sha512:69e9b9c9ac4fdf3cfa1a3de23d14964b843989128f8cc6ea58617fc5d6ef937bcc3eae9cb32b5164b5f54b06f96bdff9bc249529f20671cc26adc9e6ce8f6bec
werkzeug==0.16.1 --hash=sha512:4c982970fef39bf7cfbb4e516864fec0f8ec3f743ccb632d1659c6ee415597d98f4abd63b5c0fd999eb43fc0c89a97123f07625b01ea86b02ef51cb67a2b148d
werkzeug==1.0.0 --hash=sha512:82a0f1776820d07e929daa60bfa0a3e746464b0f2923376330f8ae5abf535bcb756c7384757b2ff8e0076f299fe85d96ef34b3a8eede21c11df9aba8cc58cb77
# Cryptography (Handles URL token derivation)
cryptography==2.8 --hash=sha512:184003c89fee74892de25c3e5ec366faea7a5f1fcca3c82b0d5e5f9f797286671a820ca54da5266d6f879ab342c97e25bce9db366c5fb1178690cd5978d4d622
cffi==1.13.2 --hash=sha512:b8753a0435cc7a2176f8748badc074ec6ffab6698d6be42b1770c85871f85aa7cf60152a8be053c3031b234a286c5cef07267cb812accb704783d74a2675ed3b
pycparser==2.19 --hash=sha512:7f830e1c9066ee2d297a55e2bf6db4bf6447b6d9da0145d11a88c3bb98505755fb7986eafa6e06ae0b7680838f5e5d6a6d188245ca5ad45c2a727587bac93ab5
cryptography==2.8 --hash=sha512:184003c89fee74892de25c3e5ec366faea7a5f1fcca3c82b0d5e5f9f797286671a820ca54da5266d6f879ab342c97e25bce9db366c5fb1178690cd5978d4d622 \
--hash=sha512:d8ddabe127ae8d7330d219e284de68b37fa450a27b4cf05334e9115388295b00148d9861c23b1a2e5ea9df0c33a2d27f3e4b25ce9abd3c334f1979920b19c902
cffi==1.14.0 --hash=sha512:5b315a65fc8f40622ceef35466546620aaca9dd304f5491a845239659b4066469c5fb3f1683c382eb57f8975caf318e5d88852e3dbb049cde193c9189b88c9c0
pycparser==2.20 --hash=sha512:06dc9cefdcde6b97c96d0452a77db42a629c48ee545edd7ab241763e50e3b3c56d21f9fcce4e206817aa1a597763d948a10ccc73572490d739c89eea7fede0a1
six==1.14.0 --hash=sha512:a6e7e35921ce8f2f8e79a296ea79a9c3515ff6dd7e777d7892fe4988594f1b3a442a68ffb89cf64530b90a32ceeea00e4ab9069bb697629ab4eb7262c68d1b0f
# PyNaCl (Derives TFC account from Onion Service private key)
PyNaCl==1.3.0 --hash=sha512:c4017c38b026a5c531b15839b8d61d1fae9907ba1960c2f97f4cd67fe0827729346d5186a6d6927ba84f64b4cbfdece12b287aa7750a039f4160831be871cea3
setuptools==45.1.0 --hash=sha512:761e4c8df239b8d173513b08959b387c1059e3e023ba6b3f6250fade518d6ef29f287ab90dd35d02bb681b410a050b30b2ed44849638b6f98831f4290a4ccd15
setuptools==45.2.0 --hash=sha512:de1ac45cb52e8a28322048e6a2b95015aa6826c49679349a1b579cb46b95cb2ffd62242c861c2fe3e059c0c55d4fdb4384c51b964ca2634b2843263543f8842a
# Duplicate sub-dependencies: cffi, pycparser, six

View File

@ -1,2 +1,2 @@
# Setuptools (Allows installation of pycparser which is a sub-dependency of the cryptography and PyNaCl packages)
setuptools==45.1.0 --hash=sha512:761e4c8df239b8d173513b08959b387c1059e3e023ba6b3f6250fade518d6ef29f287ab90dd35d02bb681b410a050b30b2ed44849638b6f98831f4290a4ccd15
setuptools==45.2.0 --hash=sha512:de1ac45cb52e8a28322048e6a2b95015aa6826c49679349a1b579cb46b95cb2ffd62242c861c2fe3e059c0c55d4fdb4384c51b964ca2634b2843263543f8842a

View File

@ -1,2 +1,12 @@
# Sub-dependencies are listed below dependencies
# Virtual environment (Used to create an isolated Python environment for TFC dependencies)
virtualenv==16.7.9 --hash=sha512:f4e7148f1de50fa2e69061e72db211085fc2f44007de4d18ee02a20d34bca30a00d2fe56ff6f3132e696c3f6efd4151863f26dac4c1d43e87b597c47a51c52ad
virtualenv==20.0.8 --hash=sha512:8b85fa635c5ec51881aed2238f1e9229d6607644995e26e3f9fe6f8bb6313c51f7b290a6ac1347738866626b1b49d08c5622836dfe2a39ae60f697888bcea615
appdirs==1.4.3 --hash=sha512:b79e9fa76eadee595fe47ea7efd35c4cc72f058a9ed16a95cfa4d91a52c330efba50df7a9926900bbced229cca7bbfb05bbf0a8ee1d46bac2362c98ab9a5154d
distlib==0.3.0 --hash=sha512:6f910a9607569c9023a19aee35be15cf8521ec7c07c5d478e6d555a301d024a2ee1db48562707b238a72c631d75d9dc154d38b39ed51746b66c938ac40671e60
six==1.14.0 --hash=sha512:a6e7e35921ce8f2f8e79a296ea79a9c3515ff6dd7e777d7892fe4988594f1b3a442a68ffb89cf64530b90a32ceeea00e4ab9069bb697629ab4eb7262c68d1b0f
importlib_metadata==1.5.0 --hash=sha512:53e51d4b75c1df19fcb6b32e57fa73ffcb00eede86fee7ac9634f02661360538a74d3546b65a641b68ee84c0d78293fe03d09b65cb85359780822b56f813b926
# importlib_metadata sub-dependencies
filelock==3.0.12 --hash=sha512:d13edd50779bca9842694e0da157ca1fdad9d28166771275049f41dea4b8d8466fc5604b610b6ad64552cdf4c1d3cada9977ca37c6b775c4cc92f333709e8ea3
zipp==3.1.0 --hash=sha512:89170b91cfdc0ef4d85b5316b484c8d6e01985f19bb9f545b11d648e122392efa68d40c66e056b8998fb69af49f4e18707f783be8d500b8957ce3a885662d27c

View File

@ -5,15 +5,16 @@ pyserial==3.4 --hash=sha512:8333ac2843fd136d5d0d63b527b37866f7d18afc3bb33c
# Argon2 (Derives keys that protect persistent user data)
argon2_cffi==19.2.0 --hash=sha512:91c4afc2d0cac14cf4342f198f68afd6477dc5bdf2683476c6f8e253de7b3bdc83b229ce96d0280f656ff33667ab9902c92741b82faee8d8892307cde6199845
cffi==1.13.2 --hash=sha512:b8753a0435cc7a2176f8748badc074ec6ffab6698d6be42b1770c85871f85aa7cf60152a8be053c3031b234a286c5cef07267cb812accb704783d74a2675ed3b
pycparser==2.19 --hash=sha512:7f830e1c9066ee2d297a55e2bf6db4bf6447b6d9da0145d11a88c3bb98505755fb7986eafa6e06ae0b7680838f5e5d6a6d188245ca5ad45c2a727587bac93ab5
cffi==1.14.0 --hash=sha512:5b315a65fc8f40622ceef35466546620aaca9dd304f5491a845239659b4066469c5fb3f1683c382eb57f8975caf318e5d88852e3dbb049cde193c9189b88c9c0
pycparser==2.20 --hash=sha512:06dc9cefdcde6b97c96d0452a77db42a629c48ee545edd7ab241763e50e3b3c56d21f9fcce4e206817aa1a597763d948a10ccc73572490d739c89eea7fede0a1
six==1.14.0 --hash=sha512:a6e7e35921ce8f2f8e79a296ea79a9c3515ff6dd7e777d7892fe4988594f1b3a442a68ffb89cf64530b90a32ceeea00e4ab9069bb697629ab4eb7262c68d1b0f
# PyNaCl (Handles TCB-side XChaCha20-Poly1305 symmetric encryption)
PyNaCl==1.3.0 --hash=sha512:c4017c38b026a5c531b15839b8d61d1fae9907ba1960c2f97f4cd67fe0827729346d5186a6d6927ba84f64b4cbfdece12b287aa7750a039f4160831be871cea3
setuptools==45.1.0 --hash=sha512:761e4c8df239b8d173513b08959b387c1059e3e023ba6b3f6250fade518d6ef29f287ab90dd35d02bb681b410a050b30b2ed44849638b6f98831f4290a4ccd15
setuptools==45.2.0 --hash=sha512:de1ac45cb52e8a28322048e6a2b95015aa6826c49679349a1b579cb46b95cb2ffd62242c861c2fe3e059c0c55d4fdb4384c51b964ca2634b2843263543f8842a
# Duplicate sub-dependencies: cffi, pycparser, six
# Cryptography (Handles TCB-side X448 key exchange)
cryptography==2.8 --hash=sha512:184003c89fee74892de25c3e5ec366faea7a5f1fcca3c82b0d5e5f9f797286671a820ca54da5266d6f879ab342c97e25bce9db366c5fb1178690cd5978d4d622
cryptography==2.8 --hash=sha512:184003c89fee74892de25c3e5ec366faea7a5f1fcca3c82b0d5e5f9f797286671a820ca54da5266d6f879ab342c97e25bce9db366c5fb1178690cd5978d4d622 \
--hash=sha512:d8ddabe127ae8d7330d219e284de68b37fa450a27b4cf05334e9115388295b00148d9861c23b1a2e5ea9df0c33a2d27f3e4b25ce9abd3c334f1979920b19c902
# Duplicate sub-dependencies: cffi, pycparser, six

View File

@ -210,7 +210,7 @@ def argon2_kdf(password: str, # Password to derive the key from
[1] https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf
[2] https://password-hashing.net/submissions/specs/Catena-v5.pdf
[3] https://crypto.stanford.edu/balloon/
[4] https://tools.ietf.org/html/draft-irtf-cfrg-argon2-06#section-9.4
[4] https://tools.ietf.org/html/draft-irtf-cfrg-argon2-09#section-8.4
[5] https://github.com/P-H-C/phc-winner-argon2
https://github.com/hynek/argon2_cffi
"""
@ -355,14 +355,14 @@ class X448(object):
fully seeded. This is the same case as with TFC's `csprng()`
function.
[1] https://github.com/pyca/cryptography/blob/2.7/src/cryptography/hazmat/primitives/asymmetric/x448.py#L38
[2] https://github.com/pyca/cryptography/blob/2.7/src/cryptography/hazmat/backends/openssl/backend.py#L2445
[3] https://github.com/pyca/cryptography/blob/2.7/src/cryptography/hazmat/backends/openssl/backend.py#L115
[4] https://github.com/pyca/cryptography/blob/2.7/src/cryptography/hazmat/backends/openssl/backend.py#L122
[1] https://github.com/pyca/cryptography/blob/2.8/src/cryptography/hazmat/primitives/asymmetric/x448.py#L38
[2] https://github.com/pyca/cryptography/blob/2.8/src/cryptography/hazmat/backends/openssl/backend.py#L2483
[3] https://github.com/pyca/cryptography/blob/2.8/src/cryptography/hazmat/backends/openssl/backend.py#L118
[4] https://github.com/pyca/cryptography/blob/2.8/src/cryptography/hazmat/backends/openssl/backend.py#L125
[5] https://cryptography.io/en/latest/hazmat/backends/openssl/#activate_osrandom_engine
[6] https://cryptography.io/en/latest/hazmat/backends/openssl/#os-random-engine
[7] https://cryptography.io/en/latest/hazmat/backends/openssl/#os-random-sources
[8] https://github.com/pyca/cryptography/blob/master/src/_cffi_src/openssl/src/osrandom_engine.c#L391
[8] https://github.com/pyca/cryptography/blob/master/src/_cffi_src/openssl/src/osrandom_engine.c#L395
"""
return X448PrivateKey.generate()
@ -419,11 +419,13 @@ class X448(object):
return blake2b(shared_secret, digest_size=SYMMETRIC_KEY_LENGTH)
@staticmethod
def derive_keys(dh_shared_key: bytes,
tfc_public_key_user: bytes,
tfc_public_key_contact: bytes
) -> Tuple[bytes, bytes, bytes, bytes, bytes, bytes]:
"""Create domain separated message and header keys and fingerprints from shared key.
def derive_subkeys(dh_shared_key: bytes,
tfc_public_key_user: bytes,
tfc_public_key_contact: bytes
) -> Tuple[bytes, bytes, bytes, bytes, bytes, bytes]:
"""\
Create domain separated message and header subkeys and fingerprints
from the shared key.
Domain separate unidirectional keys from shared key by using public
keys as message and the context variable as personalization string.
@ -451,7 +453,7 @@ class X448(object):
key_tuple = tx_mk, rx_mk, tx_hk, rx_hk, tx_fp, rx_fp
if len(set(key_tuple)) != len(key_tuple):
raise CriticalError("Derived keys were not unique.")
raise CriticalError("Derived subkeys were not unique.")
return key_tuple
@ -515,7 +517,7 @@ def encrypt_and_sign(plaintext: bytes, # Plaintext to encrypt
tested by TFC unit tests. The testing is done in limited scope by
using the libsodium and official IETF test vectors.
[1] https://tools.ietf.org/html/draft-irtf-cfrg-xchacha-01
[1] https://tools.ietf.org/html/draft-irtf-cfrg-xchacha-03
[2] https://tools.ietf.org/html/rfc8439
[3] https://download.libsodium.org/doc/secret-key_cryptography/aead/chacha20-poly1305/xchacha20-poly1305_construction
[4] https://cr.yp.to/snuffle/keysizes.pdf
@ -887,8 +889,8 @@ def csprng(key_length: int = SYMMETRIC_KEY_LENGTH # Length of the key
entropy estimator by 1024 bits.[1; pp.59-60]
[1] https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/Studies/LinuxRNG/LinuxRNG_EN.pdf
[2] https://github.com/torvalds/linux/blob/master/drivers/char/random.c#L791
[3] https://github.com/torvalds/linux/blob/master/drivers/char/random.c#L1032
[2] https://github.com/torvalds/linux/blob/master/drivers/char/random.c#L734
[3] https://github.com/torvalds/linux/blob/master/drivers/char/random.c#L952
The ChaCha20 DRNG
=================
@ -994,10 +996,10 @@ def csprng(key_length: int = SYMMETRIC_KEY_LENGTH # Length of the key
[1] https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/Studies/LinuxRNG/LinuxRNG_EN.pdf
[2] https://lkml.org/lkml/2019/5/30/867
[3] https://github.com/torvalds/linux/blob/master/drivers/char/random.c#L889
https://github.com/torvalds/linux/blob/master/drivers/char/random.c#L1058
[4] https://github.com/torvalds/linux/blob/master/lib/chacha.c#L87
https://github.com/torvalds/linux/blob/master/drivers/char/random.c#L1064
[3] https://github.com/torvalds/linux/blob/master/drivers/char/random.c#L810
https://github.com/torvalds/linux/blob/master/drivers/char/random.c#L977
[4] https://github.com/torvalds/linux/blob/master/lib/crypto/chacha.c#L89
https://github.com/torvalds/linux/blob/master/drivers/char/random.c#L983
GETRANDOM and Python

View File

@ -64,7 +64,7 @@ class TFCDatabase(object):
os.fsync(f.fileno())
def verify_file(self, database_name: str) -> bool:
"""Verify integrity of file content."""
"""Verify integrity of database content."""
with open(database_name, 'rb') as f:
purp_data = f.read()

View File

@ -25,8 +25,8 @@ import typing
from typing import Iterable, Iterator, List, Optional, Sized
from src.common.database import TFCDatabase
from src.common.encoding import bool_to_bytes, pub_key_to_onion_address, str_to_bytes, pub_key_to_short_address
from src.common.encoding import bytes_to_bool, onion_address_to_pub_key, bytes_to_str
from src.common.encoding import (bool_to_bytes, pub_key_to_onion_address, str_to_bytes, pub_key_to_short_address,
bytes_to_bool, onion_address_to_pub_key, bytes_to_str)
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
@ -111,8 +111,8 @@ class Contact(object):
notifications: This setting defines whether, in situations where
some other window is active, the Receiver Program
displays a notification about the contact sending a
new message to their window. The setting has no
displays a notification about the contact sending
a new message to their window. The setting has no
effect on user's Transmitter Program.
tfc_private_key: This value is an ephemerally stored private key
@ -139,8 +139,8 @@ class Contact(object):
) -> None:
"""Create a new Contact object.
`self.short_address` is a truncated version of the account used
to identify TFC account in printed messages.
`self.short_address` is the truncated version of the account
used to identify TFC account in printed messages.
"""
self.onion_pub_key = onion_pub_key
self.nick = nick
@ -473,8 +473,7 @@ class ContactList(Iterable[Contact], Sized):
KEX_STATUS_UNVERIFIED: f"{ECDHE} (Unverified)",
KEX_STATUS_VERIFIED: f"{ECDHE} (Verified)",
KEX_STATUS_NO_RX_PSK: f"{PSK} (No contact key)",
KEX_STATUS_HAS_RX_PSK: PSK
}
KEX_STATUS_HAS_RX_PSK: PSK}
# Populate columns with contact data
for c in self.get_list_of_contacts():

View File

@ -27,11 +27,11 @@ from typing import Callable, Iterable, Iterator, List, Sized
from src.common.database import TFCDatabase
from src.common.db_contacts import Contact
from src.common.encoding import bool_to_bytes, int_to_bytes, str_to_bytes, onion_address_to_pub_key, b58encode
from src.common.encoding import bytes_to_bool, bytes_to_int, bytes_to_str
from src.common.encoding import (bool_to_bytes, int_to_bytes, str_to_bytes, onion_address_to_pub_key,
bytes_to_bool, bytes_to_int, bytes_to_str, b58encode)
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.misc import (ensure_dir, get_terminal_width, round_up, separate_header, separate_headers,
split_byte_string)
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,
@ -266,8 +266,8 @@ class GroupList(Iterable[Group], Sized):
content. The function then removes dummy groups based on header
data. Next, the function updates the group database settings if
necessary. It then splits group data based on header data into
blocks, which are further sliced, and processed if necessary, to
obtain data required to create Group objects. Finally, if
blocks, which are further sliced, and processed if necessary,
to obtain data required to create Group objects. Finally, if
needed, the function will update the group database content.
"""
pt_bytes = self.database.load_database()
@ -319,8 +319,8 @@ class GroupList(Iterable[Group], Sized):
members_in_largest_group: int
) -> bool:
"""\
Adjust TFC's settings automatically if loaded group database was
stored using larger database setting values.
Adjust TFC's settings automatically if the loaded group database
was stored using larger database setting values.
If settings had to be adjusted, return True so the method
`self._load_groups` knows to write changes to a new database.

View File

@ -27,8 +27,7 @@ from typing import Any, Callable, Dict, List
from src.common.crypto import blake2b, csprng
from src.common.database import TFCDatabase
from src.common.encoding import int_to_bytes, onion_address_to_pub_key
from src.common.encoding import bytes_to_int
from src.common.encoding import bytes_to_int, int_to_bytes, onion_address_to_pub_key
from src.common.exceptions import CriticalError
from src.common.misc import ensure_dir, separate_headers, split_byte_string
from src.common.statics import (DIR_USER_DATA, DUMMY_CONTACT, HARAC_LENGTH, INITIAL_HARAC, KDB_ADD_ENTRY_HEADER,
@ -52,16 +51,16 @@ class KeySet(object):
Tor Onion Service address. Used to uniquely identify
the KeySet object.
tx_mk: Forward secret message key for sent messages.
tx_mk: The forward secret message key for sent messages.
rx_mk: Forward secret message key for received messages.
rx_mk: The forward secret message key for received messages.
Used only by the Receiver Program.
tx_hk: Static header key used to encrypt and sign the hash
tx_hk: The static header key used to encrypt and sign the hash
ratchet counter provided along the encrypted
assembly packet.
rx_hk: Static header key used to authenticate and decrypt
rx_hk: The static header key used to authenticate and decrypt
the hash ratchet counter of received messages. Used
only by the Receiver Program.

View File

@ -60,7 +60,7 @@ def log_writer_loop(queues: Dict[bytes, 'Queue[Any]'], # Dictionary of que
message_log: 'MessageLog', # MessageLog object
unit_test: bool = False # True, exits loop when UNIT_TEST_QUEUE is no longer empty.
) -> None:
"""Write assembly packets to log database.
"""Write assembly packets to the log database.
When traffic masking is enabled, the fact this loop is run as a
separate process, means the rate at which `sender_loop` outputs
@ -84,17 +84,17 @@ def log_writer_loop(queues: Dict[bytes, 'Queue[Any]'], # Dictionary of que
while log_packet_queue.qsize() == 0:
time.sleep(0.01)
traffic_masking, logfile_masking = check_log_setting_queues(traffic_masking,
traffic_masking_queue,
logfile_masking,
logfile_masking_queue)
traffic_masking, logfile_masking = check_setting_queues(traffic_masking,
traffic_masking_queue,
logfile_masking,
logfile_masking_queue)
onion_pub_key, assembly_packet, log_messages, log_as_ph, master_key = log_packet_queue.get()
# Update log database key
# Update the log database key
message_log.database_key = master_key.master_key
# Detect and ignore commands.
# Detect commands and ignore them
if onion_pub_key is None:
continue
@ -135,12 +135,12 @@ def log_writer_loop(queues: Dict[bytes, 'Queue[Any]'], # Dictionary of que
break
def check_log_setting_queues(traffic_masking: bool,
traffic_masking_queue: 'Queue[Any]',
logfile_masking: bool,
logfile_masking_queue: 'Queue[Any]'
) -> Tuple[bool, bool]:
"""Check for updates to logging settings."""
def check_setting_queues(traffic_masking: bool,
traffic_masking_queue: 'Queue[Any]',
logfile_masking: bool,
logfile_masking_queue: 'Queue[Any]'
) -> Tuple[bool, bool]:
"""Check queues for updates to traffic masking and logging settings."""
if traffic_masking_queue.qsize():
traffic_masking = traffic_masking_queue.get()
@ -159,10 +159,10 @@ def update_logging_state(assembly_packet: bytes,
`logging_state` retains the logging setting for noise packets that
do not know the log setting of the window. To prevent logging of
noise packets in situation where logging has been disabled, but no
new message assembly packet carrying the logging setting is received,
the LOG_SETTING_QUEUE is checked for up-to-date logging setting for
every received noise packet.
noise packets in a situation where logging has been disabled, but no
new message assembly packet carrying the logging setting has been
received, the LOG_SETTING_QUEUE is checked for up-to-date logging
setting for every received noise packet.
"""
if assembly_packet[:ASSEMBLY_PACKET_HEADER_LENGTH] == P_N_HEADER:
if log_setting_queue.qsize():
@ -181,7 +181,7 @@ def write_log_entry(assembly_packet: bytes, # Assembly pac
Logging assembly packets allows reconstruction of conversation while
protecting metadata about the length of messages alternative log
file formats could reveal.
file formats could reveal to a physical attacker.
Transmitter Program can only log sent messages. This is not useful
for recalling conversations but it makes it possible to audit
@ -335,7 +335,7 @@ def change_log_db_key(old_key: bytes,
new_key: bytes,
settings: 'Settings'
) -> None:
"""Re-encrypt log database with a new master key."""
"""Re-encrypt the log database with a new master key."""
ensure_dir(DIR_USER_DATA)
file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs'
temp_name = file_name + TEMP_POSTFIX
@ -357,7 +357,7 @@ def change_log_db_key(old_key: bytes,
def replace_log_db(settings: 'Settings') -> None:
"""Replace log database with temp file."""
"""Replace the log database with the temp file."""
ensure_dir(DIR_USER_DATA)
file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs'
temp_name = file_name + TEMP_POSTFIX
@ -378,7 +378,7 @@ def remove_logs(contact_list: 'ContactList',
If the selector is a public key, all messages (both the private
conversation and any associated group messages) sent to and received
from the associated contact are removed. If the selector is a group
ID, only messages for group determined by that group ID are removed.
ID, only messages for the group matching that group ID are removed.
"""
ensure_dir(DIR_USER_DATA)
file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs'

View File

@ -111,14 +111,14 @@ class MasterKey(object):
The generated master key depends on a 256-bit salt and the
password entered by the user. Additional computational strength
is added by the slow hash function (Argon2id). The more cores and
the faster each core is, and the more memory the system has, the
more secure TFC data is under the same password.
is added by the slow hash function (Argon2id). The more cores
and the faster each core is, and the more memory the system has,
the more secure TFC data is under the same password.
This method automatically tweaks the Argon2 time and memory cost
parameters according to best practices as determined in
https://tools.ietf.org/html/draft-irtf-cfrg-argon2-04#section-4
https://tools.ietf.org/html/draft-irtf-cfrg-argon2-09#section-4
1) For Argon2 type (y), Argon2id was selected because the
adversary might be able to run arbitrary code on Destination
@ -145,93 +145,45 @@ class MasterKey(object):
share the same salt is just 10^(-18).*
* https://en.wikipedia.org/wiki/Birthday_attack
The salt does not need additional protection as the security it
provides depends on the salt space in relation to the number of
attacked targets (i.e. if two or more physically compromised
systems happen to share the same salt, the attacker can speed up
the attack against those systems with time-memory-trade-off
attack).
The salt does not need additional protection as the security
it provides depends on the salt space in relation to the
number of attacked targets (i.e. if two or more physically
compromised systems happen to share the same salt, the
attacker can speed up the attack against those systems with
time-memory-trade-off attack).
6) The tag length isn't utilized. The result of the key derivation is
the master encryption key itself, which is set to 32 bytes for
use in XChaCha20-Poly1305.
6) The tag length isn't utilized. The result of the key
derivation is the master encryption key itself, which is set
to 32 bytes for use in XChaCha20-Poly1305.
7) Memory wiping feature is not provided.
To recognize the password is correct, the BLAKE2b hash of the master
key is stored together with key derivation parameters into the
login database.
The preimage resistance of BLAKE2b prevents derivation of master
key from the stored hash, and Argon2id ensures brute force and
dictionary attacks against the master password are painfully
slow even with GPUs/ASICs/FPGAs, as long as the password is
sufficiently strong.
To recognize the password is correct, the BLAKE2b hash of the
master key is stored together with key derivation parameters
into the login database.
The preimage resistance of BLAKE2b prevents derivation of
master key from the stored hash, and Argon2id ensures brute
force and dictionary attacks against the master password are
painfully slow even with GPUs/ASICs/FPGAs, as long as the
password is sufficiently strong.
"""
password = MasterKey.new_password()
salt = csprng(ARGON2_SALT_LENGTH)
time_cost = ARGON2_MIN_TIME_COST
password = MasterKey.new_password()
salt = csprng(ARGON2_SALT_LENGTH)
# Determine the amount of memory used from the amount of free RAM in the system.
memory_cost = self.get_available_memory()
# Determine the amount of threads to use
# Determine the number of threads to use
parallelism = multiprocessing.cpu_count()
if self.local_test:
parallelism = max(ARGON2_MIN_PARALLELISM, parallelism // 2)
# Initial key derivation
phase("Deriving master key", head=2, offset=0)
master_key, kd_time = self.timed_key_derivation(password, salt, time_cost, memory_cost, parallelism)
phase("", done=True, tail=1)
# If derivation was too fast, increase time_cost
while kd_time < MIN_KEY_DERIVATION_TIME:
print_on_previous_line()
phase(f"Trying time cost {time_cost+1}")
time_cost += 1
master_key, kd_time = self.timed_key_derivation(password, salt, time_cost, memory_cost, parallelism)
phase(f"{kd_time:.1f}s", done=True)
# At this point time_cost may have value of 1 or it may have increased to e.g. 3, which might make it take
# longer than MAX_KEY_DERIVATION_TIME. If that's the case, it makes no sense to lower it back to 2 because even
# with all memory, time_cost=2 will still be too fast. We therefore accept the time_cost whatever it is.
# If the key derivation time is too long, we do a binary search on the amount
# of memory to use until we hit the desired key derivation time range.
middle = None
# Determine time cost
time_cost, kd_time, master_key = self.determine_time_cost(password, salt, memory_cost, parallelism)
# Determine memory cost
if kd_time > MAX_KEY_DERIVATION_TIME:
lower_bound = ARGON2_MIN_MEMORY_COST
upper_bound = memory_cost
while kd_time < MIN_KEY_DERIVATION_TIME or kd_time > MAX_KEY_DERIVATION_TIME:
middle = (lower_bound + upper_bound) // 2
print_on_previous_line()
phase(f"Trying memory cost {middle} KiB")
master_key, kd_time = self.timed_key_derivation(password, salt, time_cost, middle, parallelism)
phase(f"{kd_time:.1f}s", done=True)
# 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)
# and user experience (negatively).
if middle == lower_bound or middle == upper_bound:
lower_bound = ARGON2_MIN_MEMORY_COST
upper_bound = self.get_available_memory()
continue
if kd_time < MIN_KEY_DERIVATION_TIME:
lower_bound = middle
elif kd_time > MAX_KEY_DERIVATION_TIME:
upper_bound = middle
memory_cost = middle if middle is not None else memory_cost
memory_cost, master_key = self.determine_memory_cost(password, salt, time_cost, memory_cost, parallelism)
# Store values to database
database_data = (salt
@ -249,12 +201,156 @@ class MasterKey(object):
# the database data.
self.database_data = database_data
print_on_previous_line(2)
print_on_previous_line()
phase("Deriving master key")
phase(DONE, delay=1)
return master_key
def determine_time_cost(self,
password: str,
salt: bytes,
memory_cost: int,
parallelism: int
) -> Tuple[int, float, bytes]:
"""Find suitable time_cost value for Argon2id.
There are two acceptable time_cost values.
1. A time_cost value that together with all available memory
sets the key derivation time between MIN_KEY_DERIVATION_TIME
and MAX_KEY_DERIVATION_TIME. If during the search we find
such suitable time_cost value, we accept it as such.
2. In a situation where no time_cost value is suitable alone,
there will exist some time_cost value `t` that makes key
derivation too fast, and another time_cost value `t+1` that
makes key derivation too slow. In this case we are interested
in the latter value, as unlike `t`, the value `t+1` can be
fine-tuned to suitable key derivation time range by adjusting
the memory_cost parameter.
As time_cost has no upper limit, and as the amount of available
memory has tremendous effect on how long one round takes, it's
difficult to determine the upper bound for a time_cost binary
search. We therefore start with a single round, and by
benchmarking it, estimate how many rounds are needed to reach
the target zone. After every try, we update our time_cost
candidate based on new average time per round estimate, a value
that gets more accurate as the search progresses. If this
method isn't able to suggest a value larger than 1, we increase
time_cost by 1 anyway to prevent an Alderson loop.
Every time the time_cost value is increased, we update the lower
bound to narrow the search space of the binary search we can
switch to immediately, once the MAX_KEY_DERIVATION_TIME is
exceeded (i.e. once an upper bound is found). At that point, the
time_cost `t+1` can be found in log(n) time.
"""
lower_bound = ARGON2_MIN_TIME_COST # type: int
upper_bound = None # type: Optional[int]
time_cost = lower_bound
print(2*'\n')
while True:
print_on_previous_line()
phase(f"Trying time cost {time_cost}")
master_key, kd_time = self.timed_key_derivation(password, salt, time_cost, memory_cost, parallelism)
phase(f"{kd_time:.1f}s", done=True)
# Sentinel that checks if the binary search has ended, and that restarts
# the search if kd_time repeats. This prevents an Alderson loop.
if upper_bound is not None and time_cost in [lower_bound, upper_bound]: # pragma: no cover
lower_bound = ARGON2_MIN_TIME_COST
upper_bound = None
continue
if MIN_KEY_DERIVATION_TIME <= kd_time <= MAX_KEY_DERIVATION_TIME:
break
if kd_time < MIN_KEY_DERIVATION_TIME:
lower_bound = time_cost
if upper_bound is None:
avg_time_per_round = kd_time / time_cost
time_cost_candidate = math.floor(MAX_KEY_DERIVATION_TIME / avg_time_per_round)
time_cost = max(time_cost+1, time_cost_candidate)
else:
if time_cost + 1 == upper_bound:
time_cost += 1
break
time_cost = math.floor((lower_bound + upper_bound) / 2)
elif kd_time > MAX_KEY_DERIVATION_TIME:
upper_bound = time_cost
# Sentinel: If even a single round takes too long, it's the `t+1` we're looking for.
if time_cost == 1:
break
# Sentinel: If the current time_cost value (that was too large) is one
# greater than the lower_bound, we know current time_cost is at `t+1`.
if time_cost == lower_bound + 1:
break
# Otherwise we know the current time_cost is at least two integers greater
# than `t`. Our best candidate for `t` is lower_bound, but for all we know,
# `t` might be a much greater value. So we continue binary search for `t+1`
time_cost = math.floor((lower_bound + upper_bound) / 2)
return time_cost, kd_time, master_key
def determine_memory_cost(self,
password: str,
salt: bytes,
time_cost: int,
memory_cost: int,
parallelism: int,
) -> Tuple[int, bytes]:
"""Determine suitable memory_cost value for Argon2id.
If we reached this function, it means we found a `t+1` value for
time_cost (explained in the `determine_time_cost` function). We
therefore do a binary search on the amount of memory to use
until we hit the desired key derivation time range.
"""
lower_bound = ARGON2_MIN_MEMORY_COST
upper_bound = memory_cost
while True:
memory_cost = int(round((lower_bound + upper_bound) // 2, -3))
print_on_previous_line()
phase(f"Trying memory cost {memory_cost} KiB")
master_key, kd_time = self.timed_key_derivation(password, salt, time_cost, memory_cost, parallelism)
phase(f"{kd_time:.1f}s", done=True)
# If we found a suitable memory_cost value, we accept the key and the memory_cost.
if MIN_KEY_DERIVATION_TIME <= kd_time <= MAX_KEY_DERIVATION_TIME:
return memory_cost, master_key
# 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) and user
# experience (negatively).
if memory_cost == lower_bound or memory_cost == upper_bound:
lower_bound = ARGON2_MIN_MEMORY_COST
upper_bound = self.get_available_memory()
continue
if kd_time < MIN_KEY_DERIVATION_TIME:
lower_bound = memory_cost
elif kd_time > MAX_KEY_DERIVATION_TIME:
upper_bound = memory_cost
def replace_database_data(self) -> None:
"""Store cached database data into database."""
if self.database_data is not None:

View File

@ -26,8 +26,8 @@ import typing
from typing import Union
from src.common.database import TFCDatabase
from src.common.encoding import bool_to_bytes, double_to_bytes, int_to_bytes
from src.common.encoding import bytes_to_bool, bytes_to_double, bytes_to_int
from src.common.encoding import (bool_to_bytes, double_to_bytes, int_to_bytes,
bytes_to_bool, bytes_to_double, bytes_to_int)
from src.common.exceptions import CriticalError, SoftError
from src.common.input import yes
from src.common.misc import ensure_dir, get_terminal_width, round_up
@ -53,6 +53,7 @@ class Settings(object):
master_key: 'MasterKey', # MasterKey object
operation: str, # Operation mode of the program (Tx or Rx)
local_test: bool, # Local testing setting from command-line argument
qubes: bool = False # Qubes setting from command-line argument
) -> None:
"""Create a new Settings object.
@ -91,6 +92,7 @@ class Settings(object):
self.master_key = master_key
self.software_operation = operation
self.local_testing_mode = local_test
self.qubes = qubes
self.file_name = f'{DIR_USER_DATA}{operation}_settings'
self.database = TFCDatabase(self.file_name, master_key)
@ -199,7 +201,7 @@ class Settings(object):
Settings.validate_max_number_of_groups(key, value, group_list)
Settings.validate_max_number_of_contacts(key, value, contact_list)
Settings.validate_new_message_notify_duration(key, value)
Settings.validate_traffic_maskig_delay(key, value, contact_list)
Settings.validate_traffic_masking_delay(key, value, contact_list)
@staticmethod
def validate_database_limit(key: str, value: 'SettingType') -> None:
@ -248,10 +250,10 @@ class Settings(object):
raise SoftError("Error: Too small value for message notify duration.", head_clear=True)
@staticmethod
def validate_traffic_maskig_delay(key: str,
value: 'SettingType',
contact_list: 'ContactList'
) -> None:
def validate_traffic_masking_delay(key: str,
value: 'SettingType',
contact_list: 'ContactList'
) -> None:
"""Validate setting value for traffic masking delays."""
if key in ["tm_static_delay", "tm_random_delay"]:

View File

@ -19,6 +19,7 @@ You should have received a copy of the GNU General Public License
along with TFC. If not, see <https://www.gnu.org/licenses/>.
"""
import base64
import hashlib
import json
import multiprocessing.connection
@ -36,15 +37,17 @@ from typing import Any, Dict, Optional, Tuple, Union
from serial.serialutil import SerialException
from src.common.exceptions import CriticalError, graceful_exit, SoftError
from src.common.input import yes
from src.common.misc import calculate_race_condition_delay, ensure_dir, ignored, get_terminal_width
from src.common.misc import separate_trailer
from src.common.input import box_input, yes
from src.common.misc import (calculate_race_condition_delay, ensure_dir, ignored, get_terminal_width,
separate_trailer, split_byte_string, validate_ip_address)
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 (BAUDS_PER_BYTE, DIR_USER_DATA, DONE, DST_DD_LISTEN_SOCKET, DST_LISTEN_SOCKET,
GATEWAY_QUEUE, LOCALHOST, LOCAL_TESTING_PACKET_DELAY, MAX_INT, NC,
QUBES_DST_LISTEN_SOCKET, QUBES_RX_IP_ADDR_FILE, QUBES_SRC_LISTEN_SOCKET,
PACKET_CHECKSUM_LENGTH, RECEIVER, RELAY, RP_LISTEN_SOCKET, RX,
SERIAL_RX_MIN_TIMEOUT, SETTINGS_INDENT, SRC_DD_LISTEN_SOCKET, TRANSMITTER, TX)
SERIAL_RX_MIN_TIMEOUT, SETTINGS_INDENT, SOCKET_BUFFER_SIZE, SRC_DD_LISTEN_SOCKET,
TRANSMITTER, TX, US_BYTE)
if typing.TYPE_CHECKING:
from multiprocessing import Queue
@ -59,9 +62,10 @@ def gateway_loop(queues: Dict[bytes, 'Queue[Tuple[datetime, bytes]]'],
Also place the current timestamp to queue to be delivered to the
Receiver Program. The timestamp is used both to notify when the sent
message was received by Relay Program, and as part of a commitment
scheme: For more information, see the section on "Covert channel
based on user interaction" under TFC's Security Design wiki article.
message was received by the Relay Program, and as part of a
commitment scheme: For more information, see the section on "Covert
channel based on user interaction" under TFC's Security Design wiki
article.
"""
queue = queues[GATEWAY_QUEUE]
@ -75,20 +79,23 @@ def gateway_loop(queues: Dict[bytes, 'Queue[Tuple[datetime, bytes]]'],
class Gateway(object):
"""\
Gateway object is a wrapper for interfaces that connect
Source/Destination Computer with the Networked computer.
Source/Destination Computer with the Networked Computer.
"""
def __init__(self,
operation: str,
local_test: bool,
dd_sockets: bool
dd_sockets: bool,
qubes: bool,
) -> None:
"""Create a new Gateway object."""
self.settings = GatewaySettings(operation, local_test, dd_sockets)
self.tx_serial = None # type: Optional[serial.Serial]
self.rx_serial = None # type: Optional[serial.Serial]
self.rx_socket = None # type: Optional[multiprocessing.connection.Connection]
self.tx_socket = None # type: Optional[multiprocessing.connection.Connection]
self.settings = GatewaySettings(operation, local_test, dd_sockets, qubes)
self.tx_serial = None # type: Optional[serial.Serial]
self.rx_serial = None # type: Optional[serial.Serial]
self.rx_socket = None # type: Optional[multiprocessing.connection.Connection]
self.tx_socket = None # type: Optional[multiprocessing.connection.Connection]
self.txq_socket = None # type: Optional[socket.socket]
self.rxq_socket = None # type: Optional[socket.socket]
# Initialize Reed-Solomon erasure code handler
self.rs = RSCodec(2 * self.settings.session_serial_error_correction)
@ -102,6 +109,11 @@ class Gateway(object):
self.client_establish_socket()
if self.settings.software_operation in [NC, RX]:
self.server_establish_socket()
elif qubes:
if self.settings.software_operation in [TX, NC]:
self.qubes_client_establish_socket()
if self.settings.software_operation in [NC, RX]:
self.qubes_server_establish_socket()
else:
self.establish_serial()
@ -141,6 +153,19 @@ class Gateway(object):
except SerialException:
raise CriticalError("SerialException. Ensure $USER is in the dialout group by restarting this computer.")
def write_udp_packet(self, packet: bytes) -> None:
"""Split packet to smaller parts and transmit them over the socket."""
udp_port = QUBES_SRC_LISTEN_SOCKET if self.settings.software_operation == TX else QUBES_DST_LISTEN_SOCKET
packet = base64.b85encode(packet)
packets = split_byte_string(packet, SOCKET_BUFFER_SIZE)
if self.txq_socket is not None:
for p in packets:
self.txq_socket.sendto(p, (self.settings.rx_udp_ip, udp_port))
time.sleep(0.000001)
self.txq_socket.sendto(US_BYTE, (self.settings.rx_udp_ip, udp_port))
def write(self, orig_packet: bytes) -> None:
"""Add error correction data and output data via socket/serial interface.
@ -157,6 +182,10 @@ class Gateway(object):
time.sleep(LOCAL_TESTING_PACKET_DELAY)
except BrokenPipeError:
raise CriticalError("Relay IPC server disconnected.", exit_code=0)
elif self.txq_socket is not None:
self.write_udp_packet(packet)
elif self.tx_serial is not None:
try:
self.tx_serial.write(packet)
@ -180,6 +209,24 @@ class Gateway(object):
except EOFError:
raise CriticalError("Relay IPC client disconnected.", exit_code=0)
def read_qubes_socket(self) -> bytes:
"""Read packet from Qubes' socket interface."""
if self.rxq_socket is None:
raise CriticalError("Socket interface has not been initialized.")
while True:
try:
read_buffer = bytearray()
while True:
read = self.rxq_socket.recv(SOCKET_BUFFER_SIZE)
if read == US_BYTE:
return read_buffer
read_buffer.extend(read)
except (EOFError, KeyboardInterrupt):
pass
def read_serial(self) -> bytes:
"""Read packet from serial interface.
@ -215,8 +262,11 @@ class Gateway(object):
def read(self) -> bytes:
"""Read data via socket/serial interface."""
data = (self.read_socket() if self.settings.local_testing_mode else self.read_serial())
return data
if self.settings.local_testing_mode:
return self.read_socket()
if self.settings.qubes:
return self.read_qubes_socket()
return self.read_serial()
def add_error_correction(self, packet: bytes) -> bytes:
"""Add error correction to packet that will be output.
@ -230,8 +280,10 @@ class Gateway(object):
If error correction is set to 0, errors are only detected. This
is done by using a BLAKE2b based, 128-bit checksum.
If Qubes is used, Reed-Solomon is not used as it only slows down data transfer.
"""
if self.settings.session_serial_error_correction:
if self.settings.session_serial_error_correction and not self.settings.qubes:
packet = self.rs.encode(packet)
else:
packet = packet + hashlib.blake2b(packet, digest_size=PACKET_CHECKSUM_LENGTH).digest()
@ -239,7 +291,13 @@ class Gateway(object):
def detect_errors(self, packet: bytes) -> bytes:
"""Handle received packet error detection and/or correction."""
if self.settings.session_serial_error_correction:
if self.settings.qubes:
try:
packet = base64.b85decode(packet)
except ValueError:
raise SoftError("Error: Received packet had invalid Base85 encoding.")
if self.settings.session_serial_error_correction and not self.settings.qubes:
try:
packet, _ = self.rs.decode(packet)
return bytes(packet)
@ -280,6 +338,30 @@ class Gateway(object):
return f'/dev/{self.settings.built_in_serial_interface}'
raise CriticalError(f"Error: /dev/{self.settings.built_in_serial_interface} was not found.")
# Qubes
def qubes_client_establish_socket(self) -> None:
"""Establish Qubes socket for outgoing data."""
self.txq_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
def qubes_server_establish_socket(self) -> None:
"""Establish Qubes socket for incoming data."""
udp_port = QUBES_SRC_LISTEN_SOCKET if self.settings.software_operation == NC else QUBES_DST_LISTEN_SOCKET
self.rxq_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.rxq_socket.bind((self.get_local_ip_addr(), udp_port))
@staticmethod
def get_local_ip_addr() -> str:
"""Get local IP address of the system."""
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(('192.0.0.8', 1027))
except socket.error:
raise CriticalError("Socket error")
ip_address = s.getsockname()[0] # type: str
return ip_address
# Local testing
def server_establish_socket(self) -> None:
@ -354,19 +436,20 @@ class GatewaySettings(object):
unencrypted JSON database.
The reason these settings are in plaintext is it protects the system
from inconsistent state of serial settings: Would the user reconfigure
their serial settings, and would the setting altering packet to
Receiver Program drop, Relay Program could in some situations no
longer communicate with the Receiver Program.
from an inconsistent serial setting state: Would the user change one
or more settings of their serial interfaces, and would the setting
adjusting packet to Receiver Program drop, Relay Program could in
some situations no longer communicate with the Receiver Program.
Serial interface settings are not sensitive enough to justify the
inconvenience of encrypting the setting values.
inconveniences that would result from encrypting the setting values.
"""
def __init__(self,
operation: str,
local_test: bool,
dd_sockets: bool
dd_sockets: bool,
qubes: bool
) -> None:
"""Create a new Settings object.
@ -379,11 +462,13 @@ class GatewaySettings(object):
self.serial_error_correction = 5
self.use_serial_usb_adapter = True
self.built_in_serial_interface = 'ttyS0'
self.rx_udp_ip = ''
self.software_operation = operation
self.local_testing_mode = local_test
self.data_diode_sockets = dd_sockets
self.qubes = qubes
self.all_keys = list(vars(self).keys())
self.key_list = self.all_keys[:self.all_keys.index('software_operation')]
self.defaults = {k: self.__dict__[k] for k in self.key_list}
@ -428,7 +513,7 @@ class GatewaySettings(object):
Ensure that the serial interface is available before proceeding.
"""
if not self.local_testing_mode:
if not self.local_testing_mode and not self.qubes:
name = {TX: TRANSMITTER, NC: RELAY, RX: RECEIVER}[self.software_operation]
self.use_serial_usb_adapter = yes(f"Use USB-to-serial/TTL adapter for {name} Computer?", head=1, tail=1)
@ -444,6 +529,22 @@ class GatewaySettings(object):
m_print(f"Error: Serial interface /dev/{self.built_in_serial_interface} not found.")
self.setup()
if self.qubes and self.software_operation != RX:
# Check if IP address was stored by the installer.
if os.path.isfile(QUBES_RX_IP_ADDR_FILE):
cached_ip = open(QUBES_RX_IP_ADDR_FILE).read().strip()
os.remove(QUBES_RX_IP_ADDR_FILE)
if validate_ip_address(cached_ip) == '':
self.rx_udp_ip = cached_ip
return
# If we reach this point, no cached IP was found, prompt for IP address from the user.
rx_device, short = ('Networked', 'NET') if self.software_operation == TX else ('Destination', 'DST')
m_print(f"Enter the IP address of the {rx_device} Computer", head=1, tail=1)
self.rx_udp_ip = box_input(f"{short} IP-address", expected_len=15, validator=validate_ip_address, tail=1)
def store_settings(self) -> None:
"""Store serial settings in JSON format."""
serialized = json.dumps(self, default=(lambda o: {k: self.__dict__[k] for k in self.key_list}), indent=4)
@ -484,36 +585,75 @@ class GatewaySettings(object):
def check_missing_settings(self, json_dict: Any) -> None:
"""Check for missing JSON fields and invalid values."""
for key in self.key_list:
if key not in json_dict:
m_print([f"Error: Missing setting '{key}' in '{self.file_name}'.",
f"The value has been set to default ({self.defaults[key]})."], head=1, tail=1)
setattr(self, key, self.defaults[key])
try:
self.check_key_in_key_list(key, json_dict)
if key == 'serial_baudrate':
self.validate_serial_baudrate(key, json_dict)
elif key == 'serial_error_correction':
self.validate_serial_error_correction(key, json_dict)
elif key == 'use_serial_usb_adapter':
self.validate_serial_usb_adapter_value(key, json_dict)
elif key == 'built_in_serial_interface':
self.validate_serial_interface_value(key, json_dict)
elif key == 'rx_udp_ip':
json_dict[key] = self.validate_rx_udp_ip_address(key, json_dict)
except SoftError:
continue
# Closer inspection of each setting value
if key == 'serial_baudrate' and json_dict[key] not in serial.Serial().BAUDRATES:
self.invalid_setting(key, json_dict)
continue
elif key == 'serial_error_correction' and (not isinstance(json_dict[key], int) or json_dict[key] < 0):
self.invalid_setting(key, json_dict)
continue
elif key == 'use_serial_usb_adapter':
if not isinstance(json_dict[key], bool):
self.invalid_setting(key, json_dict)
continue
elif key == 'built_in_serial_interface':
if not isinstance(json_dict[key], str):
self.invalid_setting(key, json_dict)
continue
if not any(json_dict[key] == f for f in os.listdir('/sys/class/tty')):
self.invalid_setting(key, json_dict)
continue
setattr(self, key, json_dict[key])
def check_key_in_key_list(self, key: str, json_dict: Any) -> None:
"""Check if the setting's key value is in the setting dictionary."""
if key not in json_dict:
m_print([f"Error: Missing setting '{key}' in '{self.file_name}'.",
f"The value has been set to default ({self.defaults[key]})."], head=1, tail=1)
setattr(self, key, self.defaults[key])
raise SoftError("Missing key", output=False)
def validate_serial_usb_adapter_value(self, key: str, json_dict: Any) -> None:
"""Validate the serial usb adapter setting value."""
if not isinstance(json_dict[key], bool):
self.invalid_setting(key, json_dict)
raise SoftError("Invalid value", output=False)
def validate_serial_baudrate(self, key: str, json_dict: Any) -> None:
"""Validate the serial baudrate setting value."""
if json_dict[key] not in serial.Serial().BAUDRATES:
self.invalid_setting(key, json_dict)
raise SoftError("Invalid value", output=False)
def validate_serial_error_correction(self, key: str, json_dict: Any) -> None:
"""Validate the serial error correction setting value."""
if not isinstance(json_dict[key], int) or json_dict[key] < 0:
self.invalid_setting(key, json_dict)
raise SoftError("Invalid value", output=False)
def validate_serial_interface_value(self, key: str, json_dict: Any) -> None:
"""Validate the serial interface setting value."""
if not isinstance(json_dict[key], str):
self.invalid_setting(key, json_dict)
raise SoftError("Invalid value", output=False)
if not any(json_dict[key] == f for f in os.listdir('/sys/class/tty')):
self.invalid_setting(key, json_dict)
raise SoftError("Invalid value", output=False)
def validate_rx_udp_ip_address(self, key: str, json_dict: Any) -> str:
"""Validate IP address of receiving Qubes VM."""
if self.qubes:
if not isinstance(json_dict[key], str) or validate_ip_address(json_dict[key]) != '':
self.setup()
return self.rx_udp_ip
rx_udp_ip = json_dict[key] # type: str
return rx_udp_ip
def change_setting(self, key: str, value_str: str) -> None:
"""Parse, update and store new setting value."""
attribute = self.__getattribute__(key)

View File

@ -149,7 +149,7 @@ def get_b58_key(key_type: str, # The type of Base58 key to be enter
raise CriticalError("Invalid key type")
while True:
rx_pk = box_input(box_msg, key_type=key_type, guide=not settings.local_testing_mode)
rx_pk = box_input(box_msg, key_type=key_type, guide=not (settings.local_testing_mode or settings.qubes))
rx_pk = ''.join(rx_pk.split())
if key_type == B58_PUBLIC_KEY and rx_pk == '':
@ -175,8 +175,8 @@ def nc_bypass_msg(key: str, settings: 'Settings') -> None:
key. Without the ciphertext, e.g. a visually collected local key
decryption key is useless.
"""
m = {NC_BYPASS_START: "Bypass Networked Computer if needed. Press <Enter> to send local key.",
NC_BYPASS_STOP: "Remove bypass of Networked Computer. Press <Enter> to continue."}
m = {NC_BYPASS_START: "Bypass the Networked Computer if needed. Press <Enter> to send local key.",
NC_BYPASS_STOP: "Remove bypass of the Networked Computer. Press <Enter> to continue."}
if settings.nc_bypass_messages:
m_print(m[key], manual_proceed=True, box=True, head=(1 if key == NC_BYPASS_STOP else 0))

View File

@ -27,6 +27,7 @@ import math
import os
import random
import shutil
import socket
import subprocess
import sys
import time
@ -60,13 +61,13 @@ def calculate_race_condition_delay(serial_error_correction: int,
Calculate the delay required to prevent Relay Program race condition.
When Transmitter Program outputs a command to exit or wipe data,
Relay program will also receive a copy of the command. If Relay
Program acts on the command too early, Receiver Program will not
Relay program will also receive a copy of the command. If the Relay
Program acts on the command too early, the Receiver Program will not
receive the exit/wipe command at all.
This program calculates the delay Transmitter Program should wait
before outputting command for Relay Program, to ensure Receiver
Program has received the encrypted command.
This function calculates the delay Transmitter Program should wait
before outputting command to the Relay Program, to ensure the
Receiver Program has received its encrypted command.
"""
rs = RSCodec(2 * serial_error_correction)
message_length = PACKET_LENGTH + ONION_ADDRESS_LENGTH
@ -276,7 +277,7 @@ def power_off_system() -> None:
os.system(POWEROFF)
def process_arguments() -> Tuple[str, bool, bool]:
def process_arguments() -> Tuple[str, bool, bool, bool]:
"""Load program-specific settings from command line arguments.
The arguments are determined by the desktop entries and in the
@ -305,10 +306,16 @@ def process_arguments() -> Tuple[str, bool, bool]:
dest='data_diode_sockets',
help="use data diode simulator sockets during local testing mode")
parser.add_argument('-q',
action='store_true',
default=False,
dest='qubes',
help="output data as UDP packets. Allows running TFC in qubes")
args = parser.parse_args()
operation = RX if args.operation else TX
return operation, args.local_test, args.data_diode_sockets
return operation, args.local_test, args.data_diode_sockets, args.qubes
def readable_size(size: int) -> str:
@ -477,6 +484,15 @@ def validate_group_name(group_name: str, # Name of the group
return error_msg
def validate_ip_address(ip_address: str, *_: Any) -> str:
"""Validate the IP address."""
try:
socket.inet_aton(ip_address)
return ''
except socket.error:
return 'Invalid IP address'
def validate_key_exchange(key_ex: str, # Key exchange selection to validate
*_: Any # Unused arguments
) -> str: # Error message if validation failed, else empty string

View File

@ -205,9 +205,9 @@ def print_key(message: str, # Instructive messag
) -> None:
"""Print a symmetric key in WIF format.
If local testing is not enabled, this function adds spacing in the
middle of the key, as well as guide letters to help the user keep
track of typing progress:
If serial-interface based platform is used, this function adds
spacing in the middle of the key, as well as guide letters to help
the user keep track of typing progress:
Local key encryption keys:
@ -220,7 +220,7 @@ def print_key(message: str, # Instructive messag
4EcuqaD ddsdsuc gBX2PY2 qR8hReA aeSN2oh JB9w5Cv q6BQjDa PPgzSvW 932aHio sT42SKJ Gu2PpS1 Za3Xrao
"""
b58key = b58encode(key_bytes, public_key)
if settings.local_testing_mode:
if settings.local_testing_mode or settings.qubes:
m_print([message, b58key], box=True)
else:
guide, chunk_length = (B58_PUBLIC_KEY_GUIDE, 7) if public_key else (B58_LOCAL_KEY_GUIDE, 3)

View File

@ -21,7 +21,7 @@ along with TFC. If not, see <https://www.gnu.org/licenses/>.
"""Program details"""
TFC = 'TFC'
VERSION = '1.20.02'
VERSION = '1.20.03'
TRANSMITTER = 'Transmitter'
RECEIVER = 'Receiver'
RELAY = 'Relay'
@ -503,6 +503,12 @@ DST_LISTEN_SOCKET = 5008
DD_ANIMATION_LENGTH = 16
DD_OFFSET_FROM_CENTER = 4
# Qubes related
QUBES_SRC_LISTEN_SOCKET = 2063
QUBES_DST_LISTEN_SOCKET = 2064
SOCKET_BUFFER_SIZE = 4096
QUBES_RX_IP_ADDR_FILE = 'rx_ip_addr'
# Field lengths
ENCODED_BOOLEAN_LENGTH = 1
ENCODED_BYTE_LENGTH = 1

View File

@ -79,7 +79,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 )
# Header Function to run ( Parameters )
# --------------------------------------------------------------------------------------------------------------
d = {LOCAL_KEY_RDY: (local_key_rdy, ts, window_list, contact_list ),
WIN_ACTIVITY: (win_activity, window_list ),

View File

@ -93,9 +93,7 @@ def process_assembled_file(ts: 'datetime', # Timestamp last receiv
if len(file_key) != SYMMETRIC_KEY_LENGTH:
raise SoftError("Error: Received file had an invalid key.")
decrypt_and_store_file(
ts, file_ct, file_key, file_name, onion_pub_key, nick, window_list, settings
)
decrypt_and_store_file(ts, file_ct, file_key, file_name, onion_pub_key, nick, window_list, settings)
def decrypt_and_store_file(ts: 'datetime', # Timestamp of received packet
@ -146,8 +144,7 @@ def new_file(ts: 'datetime', # Timestamp o
contact = contact_list.get_contact_by_pub_key(onion_pub_key)
if not contact.file_reception:
raise SoftError(
f"Alert! Discarded file from {contact.nick} as file reception for them is disabled.", bold=True)
raise SoftError(f"Alert! Discarded file from {contact.nick} as file reception for them is disabled.", bold=True)
k = onion_pub_key + blake2b(file_ct) # Dictionary key

View File

@ -77,7 +77,7 @@ def protect_kdk(kdk: bytes) -> None:
def process_local_key_buffer(kdk: bytes,
l_queue: 'local_key_queue'
) -> Tuple[datetime, bytes]:
"""Check if the kdk was for a packet further ahead in the queue."""
"""Check if the KDK was for a packet further ahead in the queue."""
buffer = [] # type: List[Tuple[datetime, bytes]]
while l_queue.qsize() > 0:
tup = l_queue.get() # type: Tuple[datetime, bytes]
@ -113,7 +113,7 @@ def decrypt_local_key(ts: 'datetime',
) -> Tuple['datetime', bytes]:
"""Decrypt local key packet."""
while True:
kdk = get_b58_key(B58_LOCAL_KEY, settings)
kdk = get_b58_key(B58_LOCAL_KEY, settings)
kdk_hash = blake2b(kdk)
# Check if the key was an old one.

View File

@ -90,22 +90,22 @@ def decrypt_assembly_packet(packet: bytes, # Assembly packet cip
While all message datagrams have been implicitly assumed to have
originated from some contact until this point, to prevent the
possibility of existential forgeries, the origin of message will be
validated at this point with the cryptographic Poly1305-tag.
possibility of existential forgeries, the origin of the message will
be validated at this point with the cryptographic Poly1305-tag.
As per the cryptographic doom principle, the message will not be
even decrypted unless the Poly1305 tag of the ciphertext is valid.
As per the cryptographic doom principle, the message won't be even
decrypted unless the Poly1305 tag of the ciphertext is valid.
This function also authentication of packets that handle control
flow of the Receiver program. Like messages, command datagrams have
been implicitly assumed to be commands until this point. However,
unless the Poly1305-tag of the purported command is found to be valid
with the forward secret local key, it will not be even decrypted,
let alone processed.
This function also authenticates packets that handle control flow of
the Receiver program. Like messages, command datagrams have been
implicitly assumed to be commands until this point. However, unless
the Poly1305-tag of the purported command is found to be valid with
the forward secret local key, it will not be even decrypted, let
alone processed.
"""
ct_harac, ct_assemby_packet = separate_header(packet, header_length=HARAC_CT_LENGTH)
cmd_win = window_list.get_command_window()
command = onion_pub_key == LOCAL_PUBKEY
ct_harac, ct_assembly_packet = separate_header(packet, header_length=HARAC_CT_LENGTH)
cmd_win = window_list.get_command_window()
command = onion_pub_key == LOCAL_PUBKEY
p_type = "command" if command else "packet"
direction = "from" if command or (origin == ORIGIN_CONTACT_HEADER) else "sent to"
@ -142,7 +142,7 @@ def decrypt_assembly_packet(packet: bytes, # Assembly packet cip
# Decrypt packet
try:
assembly_packet = auth_and_decrypt(ct_assemby_packet, message_key)
assembly_packet = auth_and_decrypt(ct_assembly_packet, message_key)
except nacl.exceptions.CryptoError:
raise SoftError(f"Warning! Received {p_type} {direction} {nick} had an invalid MAC.",
window=cmd_win)

View File

@ -32,19 +32,17 @@ import requests
from cryptography.hazmat.primitives.asymmetric.x448 import X448PublicKey, X448PrivateKey
from src.common.encoding import b58encode, int_to_bytes, onion_address_to_pub_key, pub_key_to_onion_address
from src.common.encoding import pub_key_to_short_address
from src.common.encoding import (b58encode, int_to_bytes, onion_address_to_pub_key, pub_key_to_onion_address,
pub_key_to_short_address)
from src.common.exceptions import SoftError
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 (ACCOUNT_SEND_QUEUE,
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, PUB_KEY_SEND_QUEUE,
from src.common.statics import (ACCOUNT_SEND_QUEUE, 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, PUB_KEY_SEND_QUEUE,
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)
@ -406,9 +404,9 @@ def process_group_management_message(data: bytes,
def c_req_manager(queues: 'QueueDict', unit_test: bool = False) -> None:
"""Manage incoming contact requests."""
existing_contacts = [] # type: List[bytes]
contact_requests = [] # type: List[bytes]
"""Manage displayed contact requests."""
existing_contacts = [] # type: List[bytes]
displayed_requests = [] # type: List[bytes]
request_queue = queues[CONTACT_REQ_QUEUE]
contact_queue = queues[C_REQ_MGMT_QUEUE]
@ -431,7 +429,7 @@ def c_req_manager(queues: 'QueueDict', unit_test: bool = False) -> None:
onion_pub_key = onion_address_to_pub_key(purp_onion_address)
if onion_pub_key in existing_contacts:
continue
if onion_pub_key in contact_requests:
if onion_pub_key in displayed_requests:
continue
if show_requests:
@ -439,7 +437,7 @@ def c_req_manager(queues: 'QueueDict', unit_test: bool = False) -> None:
m_print([f"{ts} - New contact request from an unknown TFC account:", purp_onion_address], box=True)
account_queue.put(purp_onion_address)
contact_requests.append(onion_pub_key)
displayed_requests.append(onion_pub_key)
if unit_test and queues[UNIT_TEST_QUEUE].qsize() != 0:
break

View File

@ -72,7 +72,7 @@ def process_command(command: bytes,
"""Select function for received Relay Program command."""
header, command = separate_header(command, UNENCRYPTED_COMMAND_HEADER_LENGTH)
# Keyword Function to run ( Parameters )
# Header Function to run ( Parameters )
# ---------------------------------------------------------------------------------
function_d = {UNENCRYPTED_SCREEN_CLEAR: (clear_windows, gateway, ),
UNENCRYPTED_SCREEN_RESET: (reset_windows, gateway, ),
@ -81,8 +81,8 @@ def process_command(command: bytes,
UNENCRYPTED_EC_RATIO: (change_ec_ratio, command, gateway, ),
UNENCRYPTED_BAUDRATE: (change_baudrate, command, gateway, ),
UNENCRYPTED_MANAGE_CONTACT_REQ: (manage_contact_req, command, queues),
UNENCRYPTED_ADD_NEW_CONTACT: (add_contact, command, False, queues),
UNENCRYPTED_ADD_EXISTING_CONTACT: (add_contact, command, True, queues),
UNENCRYPTED_ADD_NEW_CONTACT: (add_contact, command, queues, False ),
UNENCRYPTED_ADD_EXISTING_CONTACT: (add_contact, command, queues, True ),
UNENCRYPTED_REM_CONTACT: (remove_contact, command, queues),
UNENCRYPTED_ONION_SERVICE_DATA: (add_onion_data, command, queues),
UNENCRYPTED_ACCOUNT_CHECK: (compare_accounts, command, queues),
@ -185,8 +185,8 @@ def manage_contact_req(command: bytes,
def add_contact(command: bytes,
queues: 'QueueDict',
existing: bool,
queues: 'QueueDict'
) -> None:
"""Add clients to Relay Program.
@ -232,9 +232,9 @@ def add_onion_data(command: bytes, queues: 'QueueDict') -> None:
existing_public_keys = public_key_list[no_pending:]
for onion_pub_key in pending_public_keys:
add_contact(onion_pub_key, False, queues)
add_contact(onion_pub_key, queues, existing=False)
for onion_pub_key in existing_public_keys:
add_contact(onion_pub_key, True, queues)
add_contact(onion_pub_key, queues, existing=True)
manage_contact_req(allow_req_byte, queues, notify=False)
queues[ONION_KEY_QUEUE].put((os_private_key, confirmation_code))

View File

@ -196,34 +196,31 @@ def show_value_diffs(value_type: str,
purp_value: str,
local_test: bool
) -> None:
"""Compare purported value with correct value."""
"""Show differences between purported value and correct value."""
# Pad with underscores to denote missing chars
while len(purp_value) < ENCODED_B58_PUB_KEY_LENGTH:
purp_value += '_'
replace_l = ''
purported = ''
rep_arrows = ''
purported = ''
for c1, c2 in zip(purp_value, true_value):
if c1 == c2:
replace_l += ' '
purported += c1
else:
replace_l += ''
purported += c1
rep_arrows += ' ' if c1 == c2 else ''
purported += c1
message_list = [f"Source Computer received an invalid {value_type}.",
"See arrows below that point to correct characters."]
if local_test:
m_print(message_list + ['', purported, replace_l, true_value], box=True)
m_print(message_list + ['', purported, rep_arrows, true_value], box=True)
else:
purported = ' '.join(split_string(purported, item_len=7))
replace_l = ' '.join(split_string(replace_l, item_len=7))
rep_arrows = ' '.join(split_string(rep_arrows, item_len=7))
true_value = ' '.join(split_string(true_value, item_len=7))
m_print(message_list + ['',
B58_PUBLIC_KEY_GUIDE,
purported,
replace_l,
rep_arrows,
true_value,
B58_PUBLIC_KEY_GUIDE], box=True)

View File

@ -19,8 +19,8 @@ You should have received a copy of the GNU General Public License
along with TFC. If not, see <https://www.gnu.org/licenses/>.
"""
import hmac
import logging
import secrets
import typing
from io import BytesIO
@ -67,7 +67,10 @@ def validate_url_token(purp_url_token: str,
# True if a matching shared secret was found in pub_key_dict.
valid_url_token = False
for url_token in pub_key_dict:
valid_url_token |= hmac.compare_digest(purp_url_token, url_token)
try:
valid_url_token |= secrets.compare_digest(purp_url_token, url_token)
except TypeError:
valid_url_token |= False
return valid_url_token

View File

@ -24,8 +24,7 @@ import typing
from typing import Any, Dict, List, 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.encoding import b85encode, bytes_to_int, int_to_bytes, pub_key_to_short_address
from src.common.exceptions import SoftError
from src.common.misc import ignored, separate_header, split_byte_string
from src.common.output import rp_print
@ -217,7 +216,8 @@ def process_add_or_group_remove_member(ts: 'datetime',
header_str: str,
group_id: bytes,
messages_to_flask: 'Queue[Tuple[Union[bytes, str], bytes]]',
remaining: List[bytes], removable: List[bytes]
remaining: List[bytes],
removable: List[bytes]
) -> None:
"""Process group add or remove member packet."""
packet_str = header_str + b85encode(group_id + b"".join(removable))

View File

@ -79,7 +79,7 @@ def process_command(user_input: 'UserInput',
Select function based on the first keyword of the
issued command, and pass relevant parameters to it.
"""
# Keyword Function to run ( Parameters )
# Command Function to run ( Parameters )
# -----------------------------------------------------------------------------------------------------------------------------------------
d = {'about': (print_about, ),
'add': (add_new_contact, contact_list, group_list, settings, queues, onion_service ),

View File

@ -212,7 +212,7 @@ def new_local_key(contact_list: 'ContactList',
key, csprng(),
hek, csprng()))
# Notify Receiver that confirmation code was successfully entered
# Notify Receiver Program that confirmation code was successfully entered
queue_command(LOCAL_KEY_RDY, settings, queues)
m_print("Successfully completed the local key exchange.", bold=True, tail_clear=True, delay=1, head=1)
@ -319,7 +319,7 @@ def start_key_exchange(onion_pub_key: bytes, # Public key of contact's
dh_shared_key = X448.shared_key(tfc_private_key_user, tfc_public_key_contact)
tx_mk, rx_mk, tx_hk, rx_hk, tx_fp, rx_fp \
= X448.derive_keys(dh_shared_key, tfc_public_key_user, tfc_public_key_contact)
= X448.derive_subkeys(dh_shared_key, tfc_public_key_user, tfc_public_key_contact)
kex_status = validate_contact_fingerprint(tx_fp, rx_fp)
@ -577,10 +577,10 @@ def store_keys_on_removable_drive(ct_tag: bytes, # Encrypted PS
settings: 'Settings', # Settings object
) -> None:
"""Store keys for contact on a removable media."""
trunc_addr = pub_key_to_short_address(onion_pub_key)
while True:
trunc_addr = pub_key_to_short_address(onion_pub_key)
store_d = ask_path_gui(f"Select removable media for {nick}", settings)
f_name = f"{store_d}/{onion_service.user_short_address}.psk - Give to {trunc_addr}"
store_d = ask_path_gui(f"Select removable media for {nick}", settings)
f_name = f"{store_d}/{onion_service.user_short_address}.psk - Give to {trunc_addr}"
try:
with open(f_name, "wb+") as f:
@ -624,4 +624,4 @@ def rxp_load_psk(window: 'TxWindow',
print_on_previous_line(reps=4, delay=2)
except (EOFError, KeyboardInterrupt):
raise SoftError("PSK verification aborted.", tail_clear=True, delay=1, head=2)
raise SoftError("PSK install verification aborted.", tail_clear=True, delay=1, head=2)

View File

@ -25,8 +25,8 @@ import typing
from typing import Any, Dict, Optional, Tuple, Union
from src.common.misc import ignored
from src.common.statics import (C_N_HEADER, NOISE_PACKET_BUFFER, PADDING_LENGTH, P_N_HEADER,
TM_NOISE_COMMAND_QUEUE, TM_NOISE_PACKET_QUEUE)
from src.common.statics import (C_N_HEADER, NOISE_PACKET_BUFFER, PADDING_LENGTH, P_N_HEADER, TM_NOISE_COMMAND_QUEUE,
TM_NOISE_PACKET_QUEUE)
if typing.TYPE_CHECKING:
from multiprocessing import Queue

View File

@ -39,8 +39,8 @@ import nacl.utils
from cryptography.hazmat.primitives.asymmetric.x448 import X448PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat
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.crypto import (argon2_kdf, auth_and_decrypt, blake2b, byte_padding, check_kernel_version, csprng,
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, BLAKE2_DIGEST_LENGTH, BLAKE2_DIGEST_LENGTH_MAX,
BLAKE2_DIGEST_LENGTH_MIN, BLAKE2_KEY_LENGTH_MAX, BLAKE2_PERSON_LENGTH_MAX,
@ -125,7 +125,7 @@ class TestBLAKE2bWrapper(unittest.TestCase):
These tests ensure the BLAKE2b implementation detects invalid
parameters.
"""
def setUp(self) -> None:
"""Pre-test actions."""
self.test_string = b'test_string'
@ -184,7 +184,7 @@ class TestArgon2KDF(unittest.TestCase):
output of the argon2_cffi library to the output of the command-line
utility under those input parameters.
[1] https://tools.ietf.org/html/draft-irtf-cfrg-argon2-03#section-6.3
[1] https://tools.ietf.org/html/draft-irtf-cfrg-argon2-09#section-5.3
[2] https://github.com/P-H-C/phc-winner-argon2#command-line-utility
"""
@ -339,8 +339,8 @@ class TestX448(unittest.TestCase):
The pyca/cryptography library does not provide bindings for the
OpenSSL's X448 internals, but both KATs are done by OpenSSL tests:
https://github.com/openssl/openssl/blob/master/test/curve448_internal_test.c#L654
https://github.com/openssl/openssl/blob/master/test/curve448_internal_test.c#L668
https://github.com/openssl/openssl/blob/master/test/curve448_internal_test.c#L655
https://github.com/openssl/openssl/blob/master/test/curve448_internal_test.c#L669
"""
sk_alice = bytes.fromhex(
'9a8f4925d1519f5775cf46b04b5800d4ee9ee8bae8bc5565d498c28d'
@ -446,23 +446,23 @@ class TestX448(unittest.TestCase):
self.assertEqual(shared_secret1, blake2b(TestX448.shared_secret))
self.assertEqual(shared_secret2, blake2b(TestX448.shared_secret))
def test_non_unique_keys_raise_critical_error(self) -> None:
def test_non_unique_subkeys_raise_critical_error(self) -> None:
# Setup
shared_key = os.urandom(SYMMETRIC_KEY_LENGTH)
tx_public_key = os.urandom(TFC_PUBLIC_KEY_LENGTH)
# Test
with self.assertRaises(SystemExit):
X448.derive_keys(shared_key, tx_public_key, tx_public_key)
X448.derive_subkeys(shared_key, tx_public_key, tx_public_key)
def test_x448_key_derivation(self) -> None:
def test_x448_subkey_derivation(self) -> None:
# Setup
shared_key = os.urandom(SYMMETRIC_KEY_LENGTH)
tx_public_key = os.urandom(TFC_PUBLIC_KEY_LENGTH)
rx_public_key = os.urandom(TFC_PUBLIC_KEY_LENGTH)
# Test
key_set = X448.derive_keys(shared_key, tx_public_key, rx_public_key)
key_set = X448.derive_subkeys(shared_key, tx_public_key, rx_public_key)
# Test that correct number of keys were returned
self.assertEqual(len(key_set), 6)
@ -486,7 +486,7 @@ class TestXChaCha20Poly1305(unittest.TestCase):
ciphertext and tag.
IETF test vectors:
https://tools.ietf.org/html/draft-irtf-cfrg-xchacha-01#appendix-A.1
https://tools.ietf.org/html/draft-irtf-cfrg-xchacha-03#appendix-A.3
Libsodium test vectors:
Message: https://github.com/jedisct1/libsodium/blob/master/test/default/aead_xchacha20poly1305.c#L22
@ -500,38 +500,28 @@ class TestXChaCha20Poly1305(unittest.TestCase):
"""
ietf_plaintext = bytes.fromhex(
'4c 61 64 69 65 73 20 61 6e 64 20 47 65 6e 74 6c'
'65 6d 65 6e 20 6f 66 20 74 68 65 20 63 6c 61 73'
'73 20 6f 66 20 27 39 39 3a 20 49 66 20 49 20 63'
'6f 75 6c 64 20 6f 66 66 65 72 20 79 6f 75 20 6f'
'6e 6c 79 20 6f 6e 65 20 74 69 70 20 66 6f 72 20'
'74 68 65 20 66 75 74 75 72 65 2c 20 73 75 6e 73'
'63 72 65 65 6e 20 77 6f 75 6c 64 20 62 65 20 69'
'74 2e')
'4c616469657320616e642047656e746c656d656e206f662074686520636c6173'
'73206f66202739393a204966204920636f756c64206f6666657220796f75206f'
'6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73'
'637265656e20776f756c642062652069742e')
ietf_ad = bytes.fromhex(
'50 51 52 53 c0 c1 c2 c3 c4 c5 c6 c7')
'50515253c0c1c2c3c4c5c6c7')
ietf_key = bytes.fromhex(
'80 81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 8f'
'90 91 92 93 94 95 96 97 98 99 9a 9b 9c 9d 9e 9f')
'808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f')
ietf_nonce = bytes.fromhex(
'40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f'
'50 51 52 53 54 55 56 57')
'404142434445464748494a4b4c4d4e4f5051525354555657')
ietf_ciphertext = bytes.fromhex(
'bd 6d 17 9d 3e 83 d4 3b 95 76 57 94 93 c0 e9 39'
'57 2a 17 00 25 2b fa cc be d2 90 2c 21 39 6c bb'
'73 1c 7f 1b 0b 4a a6 44 0b f3 a8 2f 4e da 7e 39'
'ae 64 c6 70 8c 54 c2 16 cb 96 b7 2e 12 13 b4 52'
'2f 8c 9b a4 0d b5 d9 45 b1 1b 69 b9 82 c1 bb 9e'
'3f 3f ac 2b c3 69 48 8f 76 b2 38 35 65 d3 ff f9'
'21 f9 66 4c 97 63 7d a9 76 88 12 f6 15 c6 8b 13'
'b5 2e')
'bd6d179d3e83d43b9576579493c0e939572a1700252bfaccbed2902c21396cbb'
'731c7f1b0b4aa6440bf3a82f4eda7e39ae64c6708c54c216cb96b72e1213b452'
'2f8c9ba40db5d945b11b69b982c1bb9e3f3fac2bc369488f76b2383565d3fff9'
'21f9664c97637da9768812f615c68b13b52e')
ietf_tag = bytes.fromhex(
'c0:87:59:24:c1:c7:98:79:47:de:af:d8:78:0a:cf:49'.replace(':', ''))
'c0875924c1c7987947deafd8780acf49')
nonce_ct_tag_ietf = ietf_nonce + ietf_ciphertext + ietf_tag
@ -712,7 +702,7 @@ class TestCSPRNG(unittest.TestCase):
https://github.com/smuellerDD/lrng/tree/master/test
The report on the statistical tests of the LRNG can be found from
Chapter 3 (pp.26-48) of the whitepaper:
Chapter 3 (pp.30-46) of the white paper:
https://www.chronox.de/lrng/doc/lrng.pdf
Further analysis of the LRNG can be found from Chapters 4-8
@ -742,7 +732,7 @@ class TestCSPRNG(unittest.TestCase):
with self.assertRaises(SystemExit):
csprng()
def test_subceeding_hash_function_min_digest_size_raises_critical_error(self) -> None:
def test_subceding_hash_function_min_digest_size_raises_critical_error(self) -> None:
with self.assertRaises(SystemExit):
csprng(BLAKE2_DIGEST_LENGTH_MIN-1)

View File

@ -303,7 +303,7 @@ class TestTFCUnencryptedDatabase(unittest.TestCase):
self.assertEqual(self.database.load_database(), data_old)
self.assertFalse(os.path.isfile(self.database.database_temp))
def test_load_database_prefers_valid_temp_database(self) -> None:
def test_load_database_prioritizes_valid_temp_database(self) -> None:
# Setup
data_old = os.urandom(MASTERKEY_DB_SIZE)
checksummed_old = data_old + blake2b(data_old)
@ -432,7 +432,7 @@ class TestMessageLog(unittest.TestCase):
self.message_log.c.execute(f"""INSERT INTO log_entries (log_entry) VALUES (?)""",
(os.urandom(LOG_ENTRY_LENGTH),))
# Test that TFC reopens closed database on write
# Test closed database is re-opened during write
data = os.urandom(LOG_ENTRY_LENGTH)
self.assertIsNone(self.message_log.insert_log_entry(data))

View File

@ -322,12 +322,12 @@ class TestContactList(TFCTestCase):
self.assertFalse(self.contact_list.has_contacts())
def test_has_only_pending_contacts(self) -> None:
# Change all to pending
# Change all contacts' kex status to pending
for contact in self.contact_list.get_list_of_contacts():
contact.kex_status = KEX_STATUS_PENDING
self.assertTrue(self.contact_list.has_only_pending_contacts())
# Change one from pending
# Change one kex status to unverified
alice = self.contact_list.get_contact_by_address_or_nick('Alice')
alice.kex_status = KEX_STATUS_UNVERIFIED
self.assertFalse(self.contact_list.has_only_pending_contacts())

View File

@ -170,7 +170,7 @@ class TestGroupList(TFCTestCase):
+ self.settings.max_number_of_groups * self.single_member_data_len
+ POLY1305_TAG_LENGTH)
# Reduce setting values from 20 to 10
# Reduce group database setting values from 20 to 10
self.settings.max_number_of_groups = 10
self.settings.max_number_of_group_members = 10
@ -244,29 +244,29 @@ class TestGroupList(TFCTestCase):
def test_add_group(self) -> None:
members = [create_contact('Laura')]
self.group_list.add_group('test_group_12', bytes(GROUP_ID_LENGTH), False, False, members)
self.group_list.add_group('test_group_12', bytes(GROUP_ID_LENGTH), False, True, members)
self.group_list.add_group('test_group_12', bytes(GROUP_ID_LENGTH), False, True, members)
self.assertTrue(self.group_list.get_group('test_group_12').notifications)
self.assertEqual(len(self.group_list), len(self.group_names)+1)
def test_remove_group_by_name(self) -> None:
self.assertEqual(len(self.group_list), len(self.group_names))
# Remove non-existing group
# Test removing a non-existing group
self.assertIsNone(self.group_list.remove_group_by_name('test_group_12'))
self.assertEqual(len(self.group_list), len(self.group_names))
# Remove existing group
# Test removing an existing group
self.assertIsNone(self.group_list.remove_group_by_name('test_group_11'))
self.assertEqual(len(self.group_list), len(self.group_names)-1)
def test_remove_group_by_id(self) -> None:
self.assertEqual(len(self.group_list), len(self.group_names))
# Remove non-existing group
# Test removing a non-existing group
self.assertIsNone(self.group_list.remove_group_by_id(group_name_to_group_id('test_group_12')))
self.assertEqual(len(self.group_list), len(self.group_names))
# Remove existing group
# Test removing an existing group
self.assertIsNone(self.group_list.remove_group_by_id(group_name_to_group_id('test_group_11')))
self.assertEqual(len(self.group_list), len(self.group_names)-1)

View File

@ -160,26 +160,26 @@ class TestKeyList(unittest.TestCase):
new_key = bytes(SYMMETRIC_KEY_LENGTH)
self.keylist.keysets = [create_keyset(LOCAL_ID)]
# Check that KeySet exists and that its keys are different
# Check that KeySet exists and that its keys are different from the new ones
self.assertNotEqual(self.keylist.keysets[0].rx_hk, new_key)
# Replace existing KeySet
# Replace the existing KeySet
self.assertIsNone(self.keylist.add_keyset(LOCAL_PUBKEY,
new_key, new_key,
new_key, new_key))
# Check that new KeySet replaced the old one
# Check that the new KeySet replaced the old one
self.assertEqual(self.keylist.keysets[0].onion_pub_key, LOCAL_PUBKEY)
self.assertEqual(self.keylist.keysets[0].rx_hk, new_key)
def test_remove_keyset(self) -> None:
# Test KeySet for Bob exists
# Test that the KeySet for Bob exists
self.assertTrue(self.keylist.has_keyset(nick_to_pub_key('Bob')))
# Remove KeySet for Bob
# Remove the KeySet for Bob
self.assertIsNone(self.keylist.remove_keyset(nick_to_pub_key('Bob')))
# Test KeySet was removed
# Test that the KeySet was removed
self.assertFalse(self.keylist.has_keyset(nick_to_pub_key('Bob')))
@mock.patch('builtins.input', side_effect=['test_password'])
@ -190,18 +190,18 @@ class TestKeyList(unittest.TestCase):
queues = gen_queue_dict()
def queue_delayer() -> None:
"""Place packet to queue after timer runs out."""
"""Place packet to the key management queue after timer runs out."""
time.sleep(0.1)
queues[KEY_MANAGEMENT_QUEUE].put(master_key2.master_key)
threading.Thread(target=queue_delayer).start()
# Test that new key is different from existing one
# Test that the new key is different from the existing one
self.assertNotEqual(key, self.master_key.master_key)
# Change master key
# Change the master key
self.assertIsNone(self.keylist.change_master_key(queues))
# Test that master key has changed
# Test that the master key was changed
self.assertEqual(self.keylist.master_key.master_key, key)
self.assertEqual(self.keylist.database.database_key, key)
@ -254,20 +254,20 @@ class TestKeyList(unittest.TestCase):
# Setup
queues = gen_queue_dict()
# Test that KeySet for David does not exist
# Test that the KeySet for David does not exist
self.assertFalse(self.keylist.has_keyset(nick_to_pub_key('David')))
# Test adding KeySet
# Test adding the KeySet for David
self.assertIsNone(self.keylist.manage(queues, KDB_ADD_ENTRY_HEADER, nick_to_pub_key('David'),
bytes(SYMMETRIC_KEY_LENGTH), bytes(SYMMETRIC_KEY_LENGTH),
bytes(SYMMETRIC_KEY_LENGTH), bytes(SYMMETRIC_KEY_LENGTH)))
self.assertTrue(self.keylist.has_keyset(nick_to_pub_key('David')))
# Test removing KeySet
# Test removing David's KeySet
self.assertIsNone(self.keylist.manage(queues, KDB_REMOVE_ENTRY_HEADER, nick_to_pub_key('David')))
self.assertFalse(self.keylist.has_keyset(nick_to_pub_key('David')))
# Test changing master key
# Test changing the master key
new_key = SYMMETRIC_KEY_LENGTH * b'\x01'
self.assertNotEqual(self.master_key.master_key, new_key)
@ -278,7 +278,7 @@ class TestKeyList(unittest.TestCase):
self.assertEqual(self.keylist.master_key.master_key, new_key)
self.assertEqual(self.keylist.database.database_key, new_key)
# Test invalid KeyList management command raises Critical Error
# Test an invalid KeyList management command raises CriticalError
with self.assertRaises(SystemExit):
self.keylist.manage(queues, 'invalid_key', None)

View File

@ -41,8 +41,8 @@ from src.common.statics import (CLEAR_ENTIRE_SCREEN, CURSOR_LEFT_UP_CORNER,
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
from tests.utils import nick_to_short_address, tear_queues, TFCTestCase, gen_queue_dict
from tests.utils import (assembly_packet_creator, cd_unit_test, cleanup, group_name_to_group_id, nick_to_pub_key,
nick_to_short_address, tear_queues, TFCTestCase, gen_queue_dict)
TIMESTAMP_BYTES = bytes.fromhex('08ceae02')
STATIC_TIMESTAMP = bytes_to_timestamp(TIMESTAMP_BYTES).strftime('%H:%M:%S.%f')[:-TIMESTAMP_LENGTH]
@ -68,7 +68,7 @@ class TestLogWriterLoop(unittest.TestCase):
queues = gen_queue_dict()
def queue_delayer() -> None:
"""Place messages to queue one at a time."""
"""Place messages to the logging 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),
@ -101,7 +101,7 @@ class TestLogWriterLoop(unittest.TestCase):
queues[TRAFFIC_MASKING_QUEUE].put(True)
def queue_delayer() -> None:
"""Place messages to queue one at a time."""
"""Place messages to the logging 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),
@ -131,7 +131,7 @@ class TestLogWriterLoop(unittest.TestCase):
queues = gen_queue_dict()
def queue_delayer() -> None:
"""Place messages to queue one at a time."""
"""Place messages to the logging queue one at a time."""
for p in [(None, C_S_HEADER + bytes(PADDING_LENGTH), True, False, master_key),
(nick_to_pub_key('Alice'), M_S_HEADER + bytes(PADDING_LENGTH), False, False, master_key),
(nick_to_pub_key('Alice'), F_S_HEADER + bytes(PADDING_LENGTH), True, True, master_key)]:
@ -139,7 +139,7 @@ class TestLogWriterLoop(unittest.TestCase):
queues[LOG_PACKET_QUEUE].put(p)
time.sleep(SLEEP_DELAY)
queues[LOGFILE_MASKING_QUEUE].put(True) # Start logging noise packets
queues[LOGFILE_MASKING_QUEUE].put(True) # Start logging of noise packets
time.sleep(SLEEP_DELAY)
for _ in range(2):
@ -171,7 +171,7 @@ class TestLogWriterLoop(unittest.TestCase):
noise_tuple = (nick_to_pub_key('Alice'), P_N_HEADER + bytes(PADDING_LENGTH), True, True, master_key)
def queue_delayer() -> None:
"""Place packets to log into queue after delay."""
"""Place packets to log into the log queue after delay."""
for _ in range(5):
queues[LOG_PACKET_QUEUE].put(noise_tuple) # Not logged because logging_state is False by default
time.sleep(SLEEP_DELAY)
@ -267,7 +267,7 @@ class TestAccessHistoryAndPrintLogs(TFCTestCase):
"""Post-test actions."""
cleanup(self.unit_test_dir)
def test_missing_log_file_raises_se(self) -> None:
def test_missing_log_file_raises_soft_error(self) -> None:
# Setup
os.remove(self.log_file)
@ -443,8 +443,8 @@ Log file of message(s) sent to group test_group
group=self.group,
type_print='group')
# Add an assembly packet sequence sent to contact Alice in group containing cancel packet.
# Access_logs should skip this.
# Add an assembly packet sequence sent to contact Alice in group
# containing cancel packet. Access_logs should skip this.
packets = assembly_packet_creator(MESSAGE, self.msg, group_id=group_name_to_group_id('test_group'))
packets = packets[2:] + [M_C_HEADER + bytes(PADDING_LENGTH)]
for p in packets:
@ -461,8 +461,8 @@ Log file of message(s) sent to group test_group
for p in assembly_packet_creator(MESSAGE, 'This is a short group message', group_id=GROUP_ID_LENGTH * b'1'):
write_log_entry(p, nick_to_pub_key('Alice'), self.tfc_log_database)
# Add messages to Alice and Charlie in group.
# Add duplicate of outgoing message that should be skipped by access_logs.
# Add messages to Alice and Charlie in group. Add duplicate
# of outgoing message that should be skipped by access_logs.
for p in assembly_packet_creator(MESSAGE, self.msg, group_id=group_name_to_group_id('test_group')):
write_log_entry(p, nick_to_pub_key('Alice'), self.tfc_log_database)
write_log_entry(p, nick_to_pub_key('Alice'), self.tfc_log_database, origin=ORIGIN_CONTACT_HEADER)
@ -546,7 +546,7 @@ class TestReEncrypt(TFCTestCase):
"""Post-test actions."""
cleanup(self.unit_test_dir)
def test_missing_log_database_raises_se(self) -> None:
def test_missing_log_database_raises_soft_error(self) -> None:
# Setup
os.remove(self.log_file)
@ -632,7 +632,7 @@ class TestRemoveLog(TFCTestCase):
"""Post-test actions."""
cleanup(self.unit_test_dir)
def test_missing_log_file_raises_se(self) -> None:
def test_missing_log_file_raises_soft_error(self) -> None:
# Setup
os.remove(self.file_name)

View File

@ -135,15 +135,53 @@ class TestMasterKey(TFCTestCase):
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)]
MagicMock(side_effect= [(KL*b'a', 3.5)] # Early exit to create the object.
+ [(KL*b'a', 4.1)] # Test1: Make key derivation too slow so it returns with time_cost 1
+ [(KL*b'a', 2.0)] # Test2: Second key derivation time sentinel
+ [(KL*b'a', 4.1)]
+ [(KL*b'a', 0.1)] # Test3: Complete binary search with search end
+ [(KL*b'a', 6.0)]
+ 7 * [(KL*b'a', 2.5)]))
@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_determine_time_cost(self, *_: Any) -> None:
master_key = MasterKey(self.operation, local_test=True)
# Test1: Sentinel returns immediately if MAX_KEY_DERIVATION_TIME is exceeded
time_cost, kd_time, _ = master_key.determine_time_cost("password", 8*b'salt', memory_cost=512, parallelism=1)
self.assertEqual(time_cost, 1)
self.assertEqual(kd_time, 4.1)
# Test2: Second key derivation time sentinel
time_cost, kd_time, _ = master_key.determine_time_cost("password", 8 * b'salt', memory_cost=512, parallelism=1)
self.assertEqual(time_cost, 2)
self.assertEqual(kd_time, 4.1)
# Test3: Complete binary search with search end
time_cost, kd_time, _ = master_key.determine_time_cost("password", 8 * b'salt', memory_cost=512, parallelism=1)
self.assertEqual(time_cost, 40)
self.assertEqual(kd_time, 2.5)
@mock.patch('src.common.db_masterkey.MasterKey.timed_key_derivation',
MagicMock(side_effect= [(KL*b'a', 4.1)]
+ [(KL*b'a', 3.5)]
+ [(KL*b'a', 0.01)]
+ 100 * [(KL*b'b', 5.0)]
+ 2 * [(KL*b'a', 2.5)]
+ [(KL*b'a', 3.0)]))
@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_search(self, *_: Any) -> None:
MasterKey(self.operation, local_test=True)
+ [(KL*b'a', 3.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_determine_memory_cost(self, *_: Any) -> None:
master_key = MasterKey(self.operation, local_test=True)
master_key.determine_memory_cost("password", 8*b'salt', time_cost=1, memory_cost=1024, parallelism=1)
@mock.patch('src.common.db_masterkey.MIN_KEY_DERIVATION_TIME', 0.01)
@mock.patch('src.common.db_masterkey.MAX_KEY_DERIVATION_TIME', 0.1)

View File

@ -113,8 +113,8 @@ class TestSettings(TFCTestCase):
self.assert_se("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))
self.assertIsNone(self.settings.change_setting('max_number_of_group_members', '100', *self.args))
self.assertIsNone(self.settings.change_setting('traffic_masking', 'True', *self.args))
self.assertIsNone(self.settings.change_setting('max_number_of_group_members', '100', *self.args))
@mock.patch('builtins.input', side_effect=['No', 'Yes'])
def test_validate_key_value_pair(self, _: Any) -> None:

View File

@ -25,10 +25,10 @@ import unittest
from datetime import datetime
from src.common.encoding import b58encode, bool_to_bytes, double_to_bytes, str_to_bytes, int_to_bytes
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.encoding import (b58encode, bool_to_bytes, double_to_bytes, str_to_bytes, int_to_bytes,
b58decode, bytes_to_bool, bytes_to_double, bytes_to_str, bytes_to_int,
onion_address_to_pub_key, unicode_padding, pub_key_to_short_address, b85encode,
pub_key_to_onion_address, rm_padding_str, bytes_to_timestamp, b10encode)
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)

View File

@ -19,6 +19,7 @@ You should have received a copy of the GNU General Public License
along with TFC. If not, see <https://www.gnu.org/licenses/>.
"""
import base64
import os
import unittest
import socket
@ -34,7 +35,8 @@ 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 DIR_USER_DATA, GATEWAY_QUEUE, NC, PACKET_CHECKSUM_LENGTH, RX, TX
from src.common.statics import (DIR_USER_DATA, GATEWAY_QUEUE, NC, PACKET_CHECKSUM_LENGTH, QUBES_RX_IP_ADDR_FILE,
RX, TX, US_BYTE)
from tests.mock_classes import Settings
from tests.utils import cd_unit_test, cleanup, gen_queue_dict, tear_queues, TFCTestCase
@ -55,7 +57,7 @@ class TestGatewayLoop(unittest.TestCase):
@mock.patch('multiprocessing.connection.Listener',
return_value=MagicMock(accept=lambda: MagicMock(recv=MagicMock(return_value='message'))))
def test_loop(self, _: Any) -> None:
gateway = Gateway(operation=RX, local_test=True, dd_sockets=False)
gateway = Gateway(operation=RX, local_test=True, dd_sockets=False, qubes=False)
self.assertIsNone(gateway_loop(self.queues, gateway, unit_test=True))
data = self.queues[GATEWAY_QUEUE].get()
@ -79,7 +81,7 @@ class TestGatewaySerial(TFCTestCase):
@mock.patch('os.listdir', side_effect=[['ttyUSB0'], ['ttyUSB0']])
@mock.patch('builtins.input', side_effect=['Yes'])
def test_search_and_establish_serial(self, *_: Any) -> None:
gateway = Gateway(operation=RX, local_test=False, dd_sockets=False)
gateway = Gateway(operation=RX, local_test=False, dd_sockets=False, qubes=False)
self.assertIsInstance(gateway.rs, RSCodec)
self.assertIs(gateway.tx_serial, gateway.rx_serial)
@ -87,16 +89,16 @@ class TestGatewaySerial(TFCTestCase):
@mock.patch('serial.Serial', side_effect=SerialException)
@mock.patch('os.listdir', side_effect=[['ttyUSB0'], ['ttyUSB0']])
@mock.patch('builtins.input', side_effect=['Yes'])
def test_serialexception_during_establish_exists(self, *_: Any) -> None:
def test_serial_exception_during_establish_exists(self, *_: Any) -> None:
with self.assertRaises(SystemExit):
Gateway(operation=RX, local_test=False, dd_sockets=False)
Gateway(operation=RX, local_test=False, dd_sockets=False, qubes=False)
@mock.patch('time.sleep', return_value=None)
@mock.patch('serial.Serial', return_value=MagicMock(write=MagicMock(side_effect=[SerialException, None])))
@mock.patch('os.listdir', side_effect=[['ttyUSB0'], ['ttyUSB0'], ['ttyUSB0']])
@mock.patch('builtins.input', side_effect=['Yes'])
def test_write_serial_(self, *_: Any) -> None:
gateway = Gateway(operation=RX, local_test=False, dd_sockets=False)
gateway = Gateway(operation=RX, local_test=False, dd_sockets=False, qubes=False)
self.assertIsNone(gateway.write(b"message"))
@mock.patch("time.sleep", return_value=None)
@ -106,7 +108,7 @@ class TestGatewaySerial(TFCTestCase):
@mock.patch("builtins.input", side_effect=["Yes"])
def test_serial_uninitialized_serial_interface_for_read_raises_critical_error(self, *_) -> None:
# Setup
gateway = Gateway(operation=RX, local_test=False, dd_sockets=False)
gateway = Gateway(operation=RX, local_test=False, dd_sockets=False, qubes=False)
gateway.rx_serial = None
# Test
@ -119,7 +121,7 @@ class TestGatewaySerial(TFCTestCase):
@mock.patch("builtins.input", side_effect=["Yes"])
def test_serial_uninitialized_socket_interface_for_read_raises_critical_error(self, *_) -> None:
# Setup
gateway = Gateway(operation=RX, local_test=True, dd_sockets=False)
gateway = Gateway(operation=RX, local_test=True, dd_sockets=False, qubes=False)
gateway.rx_socket = None
# Test
@ -133,7 +135,7 @@ class TestGatewaySerial(TFCTestCase):
@mock.patch("os.listdir", side_effect=[["ttyUSB0"], ["ttyUSB0"], ["ttyUSB0"]])
@mock.patch("builtins.input", side_effect=["Yes"])
def test_read_socket(self, *_) -> None:
gateway = Gateway(operation=RX, local_test=True, dd_sockets=False)
gateway = Gateway(operation=RX, local_test=True, dd_sockets=False, qubes=False)
data = gateway.read()
self.assertEqual(data, b"12")
@ -144,7 +146,7 @@ class TestGatewaySerial(TFCTestCase):
@mock.patch("os.listdir", side_effect=[["ttyUSB0"], ["ttyUSB0"], ["ttyUSB0"]])
@mock.patch("builtins.input", side_effect=["Yes"])
def test_read_serial(self, *_) -> None:
gateway = Gateway(operation=RX, local_test=False, dd_sockets=False)
gateway = Gateway(operation=RX, local_test=False, dd_sockets=False, qubes=False)
data = gateway.read()
self.assertEqual(data, b"12")
@ -153,29 +155,26 @@ class TestGatewaySerial(TFCTestCase):
@mock.patch("os.listdir", side_effect=[["ttyUSB0"], ["ttyUSB0"]])
@mock.patch("builtins.input", side_effect=["Yes"])
def test_add_error_correction(self, *_) -> None:
gateway = Gateway(operation=RX, local_test=False, dd_sockets=False)
gateway = Gateway(operation=RX, local_test=False, dd_sockets=False, qubes=False)
packet = b"packet"
# Test BLAKE2b based checksum
gateway.settings.session_serial_error_correction = 0
self.assertEqual(
gateway.add_error_correction(packet),
packet + blake2b(packet, digest_size=PACKET_CHECKSUM_LENGTH),
)
self.assertEqual(gateway.add_error_correction(packet),
packet + blake2b(packet, digest_size=PACKET_CHECKSUM_LENGTH))
# Test Reed-Solomon erasure code
gateway.settings.session_serial_error_correction = 5
gateway.rs = RSCodec(gateway.settings.session_serial_error_correction)
self.assertEqual(
gateway.add_error_correction(packet), gateway.rs.encode(packet)
)
self.assertEqual(gateway.add_error_correction(packet),
gateway.rs.encode(packet))
@mock.patch('time.sleep', return_value=None)
@mock.patch('serial.Serial', return_value=MagicMock())
@mock.patch('os.listdir', side_effect=[['ttyUSB0'], ['ttyUSB0']])
@mock.patch('builtins.input', side_effect=['Yes'])
def test_detect_errors(self, *_: Any) -> None:
gateway = Gateway(operation=RX, local_test=False, dd_sockets=False)
gateway = Gateway(operation=RX, local_test=False, dd_sockets=False, qubes=False)
packet = b'packet'
# Test BLAKE2b based checksum
@ -183,7 +182,7 @@ class TestGatewaySerial(TFCTestCase):
self.assertEqual(gateway.detect_errors(gateway.add_error_correction(packet)),
packet)
# Test unrecoverable error raises FR
# Test unrecoverable error raises SoftError
self.assert_se("Warning! Received packet had an invalid checksum.",
gateway.detect_errors, 300 * b'a')
@ -193,16 +192,30 @@ class TestGatewaySerial(TFCTestCase):
self.assertEqual(gateway.detect_errors(gateway.add_error_correction(packet)),
packet)
# Test unrecoverable error raises FR
# Test unrecoverable error raises SoftError
self.assert_se("Error: Reed-Solomon failed to correct errors in the received packet.",
gateway.detect_errors, 300 * b'a')
# Qubes
# Test with B58 encoding
gateway.settings.qubes = True
packet_with_error_correction = base64.b85encode(gateway.add_error_correction(packet))
self.assertEqual(gateway.detect_errors(packet_with_error_correction), packet)
# Test invalid B85 encoding raises SoftError
packet_with_error_correction = base64.b85encode(gateway.add_error_correction(packet))
packet_with_error_correction += b'\x00'
self.assert_se("Error: Received packet had invalid Base85 encoding.",
gateway.detect_errors, packet_with_error_correction)
gateway.settings.qubes = False
@mock.patch('time.sleep', return_value=None)
@mock.patch('serial.Serial', return_value=MagicMock())
@mock.patch('os.listdir', side_effect=[['ttyUSB0'], ['ttyUSB0'], [''], ['ttyUSB0'], ['ttyS0'], ['']])
@mock.patch('builtins.input', side_effect=['Yes'])
def test_search_serial_interfaces(self, *_: Any) -> None:
gateway = Gateway(operation=RX, local_test=False, dd_sockets=False)
gateway = Gateway(operation=RX, local_test=False, dd_sockets=False, qubes=False)
interface = gateway.search_serial_interface()
self.assertEqual(interface, '/dev/ttyUSB0')
@ -220,39 +233,38 @@ class TestGatewaySerial(TFCTestCase):
@mock.patch('multiprocessing.connection.Client', MagicMock())
@mock.patch('multiprocessing.connection.Listener', MagicMock())
def test_establish_local_testing_gateway(self, *_: Any) -> None:
gateway = Gateway(operation=NC, local_test=True, dd_sockets=False)
gateway = Gateway(operation=NC, local_test=True, dd_sockets=False, qubes=False)
self.assertIsInstance(gateway.rs, RSCodec)
@mock.patch('time.sleep', return_value=None)
@mock.patch('multiprocessing.connection.Client', MagicMock(side_effect=KeyboardInterrupt))
def test_keyboard_interrupt_exits(self, *_: Any) -> None:
with self.assertRaises(SystemExit):
Gateway(operation=TX, local_test=True, dd_sockets=False)
Gateway(operation=TX, local_test=True, dd_sockets=False, qubes=False)
@mock.patch('time.sleep', return_value=None)
@mock.patch('multiprocessing.connection.Client', MagicMock(
side_effect=[socket.error, ConnectionRefusedError, MagicMock()]))
def test_socket_client(self, *_: Any) -> None:
gateway = Gateway(operation=TX, local_test=True, dd_sockets=False)
gateway = Gateway(operation=TX, local_test=True, dd_sockets=False, qubes=False)
self.assertIsInstance(gateway, Gateway)
@mock.patch('time.sleep', return_value=None)
@mock.patch('multiprocessing.connection.Listener', MagicMock(
side_effect=[MagicMock(), KeyboardInterrupt]))
def test_socket_server(self, *_: Any) -> None:
gateway = Gateway(operation=RX, local_test=True, dd_sockets=False)
gateway = Gateway(operation=RX, local_test=True, dd_sockets=False, qubes=False)
self.assertIsInstance(gateway, Gateway)
with self.assertRaises(SystemExit):
Gateway(operation=RX, local_test=True, dd_sockets=False)
Gateway(operation=RX, local_test=True, dd_sockets=False, qubes=False)
@mock.patch('time.sleep', return_value=None)
@mock.patch('multiprocessing.connection.Listener', return_value=MagicMock(
accept=lambda: MagicMock(recv=MagicMock(side_effect=[KeyboardInterrupt, b'data', EOFError]))))
def test_local_testing_read(self, *_: Any) -> None:
gateway = Gateway(operation=RX, local_test=True, dd_sockets=False)
gateway = Gateway(operation=RX, local_test=True, dd_sockets=False, qubes=False)
self.assertEqual(gateway.read(), b'data')
with self.assertRaises(SystemExit):
gateway.read()
@ -260,13 +272,61 @@ class TestGatewaySerial(TFCTestCase):
@mock.patch('multiprocessing.connection.Client', return_value=MagicMock(
send=MagicMock(side_effect=[None, BrokenPipeError])))
def test_local_testing_write(self, *_: Any) -> None:
gateway = Gateway(operation=TX, local_test=True, dd_sockets=False)
gateway = Gateway(operation=TX, local_test=True, dd_sockets=False, qubes=False)
self.assertIsNone(gateway.write(b'data'))
with self.assertRaises(SystemExit):
gateway.write(b'data')
# Qubes
@mock.patch('time.sleep', return_value=None)
@mock.patch('socket.socket', MagicMock(return_value=MagicMock(
recv=MagicMock(side_effect=[EOFError, b'data', US_BYTE]))))
def test_qubes_socket_server(self, *_: Any) -> None:
gateway = Gateway(operation=RX, local_test=False, dd_sockets=False, qubes=True)
self.assertIsInstance(gateway, Gateway)
self.assertEqual(gateway.read(), b'data')
@mock.patch('time.sleep', return_value=None)
@mock.patch('socket.socket', MagicMock(return_value=MagicMock(
recv=MagicMock(side_effect=[EOFError, b'data', US_BYTE]))))
def test_qubes_socket_server_raises_critical_error_if_interface_is_not_initialized(self, *_: Any) -> None:
# Setup
gateway = Gateway(operation=RX, local_test=False, dd_sockets=False, qubes=True)
gateway.rxq_socket = None
# Test
with self.assertRaises(SystemExit):
self.assertEqual(gateway.read(), b'data')
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', side_effect=['10.137.0.17'])
@mock.patch('socket.socket', MagicMock())
def test_qubes_socket_client(self, *_: Any) -> None:
gateway = Gateway(operation=TX, local_test=False, dd_sockets=False, qubes=True)
self.assertIsInstance(gateway, Gateway)
self.assertIsNone(gateway.write(b'data'))
@mock.patch('time.sleep', return_value=None)
@mock.patch('socket.socket', MagicMock())
def test_qubes_auto_config_from_file(self, *_: Any) -> None:
# Setup
test_ip = '10.137.0.17'
open(QUBES_RX_IP_ADDR_FILE, 'w+').write(test_ip)
# Test
self.assertTrue(os.path.isfile(QUBES_RX_IP_ADDR_FILE))
gateway = Gateway(operation=TX, local_test=False, dd_sockets=False, qubes=True)
self.assertEqual(gateway.settings.rx_udp_ip, test_ip)
self.assertFalse(os.path.isfile(QUBES_RX_IP_ADDR_FILE))
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', side_effect=['10.137.0.17'])
@mock.patch('socket.socket', MagicMock(return_value=MagicMock(connect=MagicMock(side_effect=[socket.error]))))
def test_socket_error_raises_critical_error(self, *_: Any) -> None:
gateway = Gateway(operation=TX, local_test=False, dd_sockets=False, qubes=True)
with self.assertRaises(SystemExit):
gateway.get_local_ip_addr()
class TestGatewaySettings(TFCTestCase):
@ -278,7 +338,8 @@ class TestGatewaySettings(TFCTestCase):
"serial_baudrate": 19200,
"serial_error_correction": 5,
"use_serial_usb_adapter": true,
"built_in_serial_interface": "ttyS0"
"built_in_serial_interface": "ttyS0",
"rx_udp_ip": ""
}"""
def tearDown(self) -> None:
@ -288,11 +349,11 @@ class TestGatewaySettings(TFCTestCase):
@mock.patch('os.listdir', side_effect=[['ttyUSB0'], ['ttyS0'], ['ttyUSB0'], ['ttyS0']])
@mock.patch('builtins.input', side_effect=['yes', 'yes', 'no', 'no'])
def test_gateway_setup(self, *_: Any) -> None:
settings = GatewaySettings(operation=TX, local_test=False, dd_sockets=True)
settings = GatewaySettings(operation=TX, local_test=False, dd_sockets=True, qubes=False)
self.assertIsNone(settings.setup())
def test_store_and_load_of_settings(self) -> None:
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True)
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True, qubes=False)
self.assertTrue(os.path.isfile(f'{DIR_USER_DATA}/{TX}_serial_settings.json'))
self.assertEqual(settings.serial_baudrate, 19200)
@ -301,7 +362,7 @@ class TestGatewaySettings(TFCTestCase):
settings.use_serial_usb_adapter = False
self.assertIsNone(settings.store_settings())
settings2 = GatewaySettings(operation=TX, local_test=True, dd_sockets=True)
settings2 = GatewaySettings(operation=TX, local_test=True, dd_sockets=True, qubes=False)
self.assertEqual(settings2.serial_baudrate, 115200)
self.assertEqual(settings.use_serial_usb_adapter, False)
@ -315,14 +376,16 @@ class TestGatewaySettings(TFCTestCase):
"serial_baudrate": 9600,
"serial_error_correction": 1,
"use_serial_usb_adapter": false,
"built_in_serial_interface": "ttyS0"
"built_in_serial_interface": "ttyS0",
"rx_udp_ip": "10.137.0.17"
}""")
# Test
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True)
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True, qubes=False)
self.assertEqual(settings.serial_baudrate, 9600)
self.assertEqual(settings.serial_error_correction, 1)
self.assertEqual(settings.use_serial_usb_adapter, False)
self.assertEqual(settings.built_in_serial_interface, 'ttyS0')
self.assertEqual(settings.rx_udp_ip, '10.137.0.17')
def test_missing_values_are_set_to_default_and_database_is_overwritten(self) -> None:
# Setup
@ -335,7 +398,7 @@ class TestGatewaySettings(TFCTestCase):
"relay_usb_serial_adapter": false
}""")
# Test
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True)
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True, qubes=False)
self.assertEqual(settings.serial_baudrate, 19200)
self.assertEqual(settings.serial_error_correction, 1)
self.assertEqual(settings.use_serial_usb_adapter, False)
@ -352,7 +415,7 @@ class TestGatewaySettings(TFCTestCase):
}""")
# Test
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True)
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True, qubes=False)
self.assertEqual(settings.serial_baudrate, 19200)
self.assertEqual(settings.serial_error_correction, 5)
self.assertEqual(settings.use_serial_usb_adapter, True)
@ -372,10 +435,11 @@ class TestGatewaySettings(TFCTestCase):
"serial_baudrate": 19201,
"serial_error_correction": 5,
"use_serial_usb_adapter": true,
"built_in_serial_interface": "ttyS0"
"built_in_serial_interface": "ttyS0",
"rx_udp_ip": ""
}""")
# Test
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True)
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True, qubes=False)
self.assertEqual(settings.serial_baudrate, 19200)
self.assertEqual(settings.serial_error_correction, 5)
self.assertEqual(settings.use_serial_usb_adapter, True)
@ -395,10 +459,11 @@ class TestGatewaySettings(TFCTestCase):
"serial_baudrate": 19200,
"serial_error_correction": -1,
"use_serial_usb_adapter": true,
"built_in_serial_interface": "ttyS0"
"built_in_serial_interface": "ttyS0",
"rx_udp_ip": ""
}""")
# Test
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True)
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True, qubes=False)
self.assertEqual(settings.serial_baudrate, 19200)
self.assertEqual(settings.serial_error_correction, 5)
self.assertEqual(settings.use_serial_usb_adapter, True)
@ -418,10 +483,11 @@ class TestGatewaySettings(TFCTestCase):
"serial_baudrate": 19200,
"serial_error_correction": 5,
"use_serial_usb_adapter": true,
"built_in_serial_interface": "does_not_exist"
"built_in_serial_interface": "does_not_exist",
"rx_udp_ip": ""
}""")
# Test
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True)
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True, qubes=False)
self.assertEqual(settings.serial_baudrate, 19200)
self.assertEqual(settings.serial_error_correction, 5)
self.assertEqual(settings.use_serial_usb_adapter, True)
@ -432,6 +498,48 @@ class TestGatewaySettings(TFCTestCase):
self.assertEqual(data, self.default_serialized)
@mock.patch('builtins.input', side_effect=['10.137.0.17'])
def test_invalid_rx_udp_ip_is_replaced_with_user_input(self, _) -> None:
# Setup
ensure_dir(DIR_USER_DATA)
with open(f"{DIR_USER_DATA}{TX}_serial_settings.json", 'w+') as f:
f.write("""\
{
"serial_baudrate": 19200,
"serial_error_correction": 5,
"use_serial_usb_adapter": true,
"built_in_serial_interface": "ttyS0",
"rx_udp_ip": "256.256.256.256"
}""")
# Test
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True, qubes=True)
self.assertEqual(settings.serial_baudrate, 19200)
self.assertEqual(settings.serial_error_correction, 5)
self.assertEqual(settings.use_serial_usb_adapter, True)
self.assertEqual(settings.built_in_serial_interface, 'ttyS0')
self.assertEqual(settings.rx_udp_ip, '10.137.0.17')
@mock.patch('builtins.input', side_effect=['10.137.0.17'])
def test_invalid_rx_udp_ip_type_is_replaced_with_user_input(self, _) -> None:
# Setup
ensure_dir(DIR_USER_DATA)
with open(f"{DIR_USER_DATA}{TX}_serial_settings.json", 'w+') as f:
f.write("""\
{
"serial_baudrate": 19200,
"serial_error_correction": 5,
"use_serial_usb_adapter": true,
"built_in_serial_interface": "ttyS0",
"rx_udp_ip": 5
}""")
# Test
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True, qubes=True)
self.assertEqual(settings.serial_baudrate, 19200)
self.assertEqual(settings.serial_error_correction, 5)
self.assertEqual(settings.use_serial_usb_adapter, True)
self.assertEqual(settings.built_in_serial_interface, 'ttyS0')
self.assertEqual(settings.rx_udp_ip, '10.137.0.17')
def test_invalid_type_is_replaced_with_default(self) -> None:
# Setup
ensure_dir(DIR_USER_DATA)
@ -441,10 +549,11 @@ class TestGatewaySettings(TFCTestCase):
"serial_baudrate": "115200",
"serial_error_correction": "5",
"use_serial_usb_adapter": "true",
"built_in_serial_interface": true
"built_in_serial_interface": true,
"rx_udp_ip": ""
}""")
# Test
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True)
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True, qubes=False)
self.assertEqual(settings.serial_baudrate, 19200)
self.assertEqual(settings.serial_error_correction, 5)
self.assertEqual(settings.use_serial_usb_adapter, True)
@ -468,7 +577,7 @@ class TestGatewaySettings(TFCTestCase):
"this_should_not_be_here": 1
}""")
# Test
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True)
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True, qubes=False)
self.assertEqual(settings.serial_baudrate, 19200)
self.assertEqual(settings.serial_error_correction, 5)
self.assertEqual(settings.use_serial_usb_adapter, True)
@ -487,7 +596,7 @@ class TestGatewaySettings(TFCTestCase):
with open(f"{DIR_USER_DATA}{TX}_serial_settings.json", 'w+') as f:
f.write(self.default_serialized)
settings = GatewaySettings(operation=TX, local_test=False, dd_sockets=True)
settings = GatewaySettings(operation=TX, local_test=False, dd_sockets=True, qubes=False)
# Test
self.assertIsNone(settings.setup())
@ -495,7 +604,7 @@ class TestGatewaySettings(TFCTestCase):
@mock.patch('time.sleep', return_value=None)
def test_change_setting(self, _: Any) -> None:
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True)
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True, qubes=False)
self.assert_se("Error: Invalid setting value 'Falsee'.",
settings.change_setting, 'serial_baudrate', 'Falsee')
self.assert_se("Error: Invalid setting value '1.1'.",
@ -506,14 +615,14 @@ class TestGatewaySettings(TFCTestCase):
settings.change_setting, 'use_serial_usb_adapter', 'Falsee')
self.assertIsNone(settings.change_setting('serial_baudrate', '9600'))
self.assertEqual(GatewaySettings(operation=TX, local_test=True, dd_sockets=True).serial_baudrate, 9600)
self.assertEqual(GatewaySettings(operation=TX, local_test=True, dd_sockets=True, qubes=False).serial_baudrate, 9600)
settings.serial_baudrate = b'bytestring'
with self.assertRaises(SystemExit):
settings.change_setting('serial_baudrate', '9600')
def test_validate_key_value_pair(self) -> None:
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True)
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True, qubes=False)
self.assert_se("Error: The specified baud rate is not supported.",
settings.validate_key_value_pair, 'serial_baudrate', 0)
self.assert_se("Error: The specified baud rate is not supported.",
@ -529,11 +638,11 @@ class TestGatewaySettings(TFCTestCase):
@mock.patch('shutil.get_terminal_size', return_value=(64, 64))
def test_too_narrow_terminal_raises_fr_when_printing_settings(self, _: Any) -> None:
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True)
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True, qubes=False)
self.assert_se("Error: Screen width is too small.", settings.print_settings)
def test_print_settings(self) -> None:
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True)
settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True, qubes=False)
self.assert_prints("""\
Serial interface setting Current value Default value Description

View File

@ -33,18 +33,18 @@ from typing import Any, NoReturn
from unittest.mock import MagicMock
from src.common.misc import calculate_race_condition_delay, decompress, ensure_dir, get_tab_complete_list
from src.common.misc import get_tab_completer, get_terminal_height, get_terminal_width, HideRunTime, ignored
from src.common.misc import monitor_processes, process_arguments, readable_size, reset_terminal, round_up
from src.common.misc import separate_header, separate_headers, separate_trailer, split_string, split_byte_string
from src.common.misc import split_to_substrings, terminal_width_check, validate_group_name, validate_key_exchange
from src.common.misc import validate_onion_addr, validate_nick
from src.common.misc import (calculate_race_condition_delay, decompress, ensure_dir, get_tab_complete_list,
get_tab_completer, get_terminal_height, get_terminal_width, HideRunTime, ignored,
monitor_processes, process_arguments, readable_size, reset_terminal, round_up,
separate_header, separate_headers, separate_trailer, split_string, split_byte_string,
split_to_substrings, terminal_width_check, validate_group_name, validate_ip_address,
validate_key_exchange, validate_onion_addr, validate_nick)
from src.common.statics import (DIR_RECV_FILES, DIR_USER_DATA, DUMMY_GROUP, ECDHE, EXIT, EXIT_QUEUE, LOCAL_ID,
PADDING_LENGTH, RESET, RX, TAILS, TRAFFIC_MASKING, WIPE)
from tests.mock_classes import ContactList, Gateway, GroupList, Settings
from tests.utils import cd_unit_test, cleanup, gen_queue_dict, nick_to_onion_address
from tests.utils import nick_to_pub_key, tear_queues, TFCTestCase
from tests.utils import (cd_unit_test, cleanup, gen_queue_dict, nick_to_onion_address, nick_to_pub_key,
tear_queues, TFCTestCase)
class TestCalculateRaceConditionDelay(unittest.TestCase):
@ -72,7 +72,7 @@ class TestDecompress(TFCTestCase):
# Test
self.assertEqual(decompress(compressed, self.settings.max_decompress_size), data)
def test_oversize_decompression_raises_se(self) -> None:
def test_oversize_decompression_raises_soft_error(self) -> None:
# Setup
data = os.urandom(self.settings.max_decompress_size + 1)
compressed = zlib.compress(data)
@ -272,9 +272,10 @@ class TestProcessArguments(unittest.TestCase):
def __init__(self) -> None:
"""Create new Args mock object."""
self.operation = True
self.local_test = True
self.operation = True
self.local_test = True
self.data_diode_sockets = True
self.qubes = False
class MockParser(object):
"""MockParse object."""
@ -298,7 +299,7 @@ class TestProcessArguments(unittest.TestCase):
argparse.ArgumentParser = self.o_argparse
def test_process_arguments(self) -> None:
self.assertEqual(process_arguments(), (RX, True, True))
self.assertEqual(process_arguments(), (RX, True, True, False))
class TestReadableSize(unittest.TestCase):
@ -499,6 +500,16 @@ class TestValidateGroupName(unittest.TestCase):
'')
class TestValidateIpAddress(unittest.TestCase):
def test_validate_ip_address(self) -> None:
self.assertEqual(validate_ip_address("10.137.0.17"), '')
self.assertEqual(validate_ip_address("10.137.0.255"), '')
self.assertEqual(validate_ip_address("255.255.255.255"), '')
self.assertEqual(validate_ip_address("10.137.0.256"), 'Invalid IP address')
self.assertEqual(validate_ip_address("256.256.256.256"), 'Invalid IP address')
class TestValidateKeyExchange(unittest.TestCase):
def test_validate_key_exchange(self) -> None:

View File

@ -25,8 +25,8 @@ from datetime import datetime
from unittest import mock
from typing import Any
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.output import (clear_screen, group_management_print, m_print, phase, print_fingerprint, print_key,
print_title, print_on_previous_line, print_spacing, rp_print)
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,

View File

@ -63,7 +63,7 @@ class TestAskPathGui(TFCTestCase):
@mock.patch('tkinter.Tk', return_value=MagicMock())
@mock.patch('tkinter.filedialog.askopenfilename', return_value='')
def test_no_path_to_file_raises_se(self, *_: Any) -> None:
def test_no_path_to_file_raises_soft_error(self, *_: Any) -> None:
self.assert_se("File selection aborted.", ask_path_gui, 'test message', self.settings, True)
@mock.patch('tkinter.Tk', return_value=MagicMock())
@ -73,7 +73,7 @@ class TestAskPathGui(TFCTestCase):
@mock.patch('tkinter.Tk', return_value=MagicMock())
@mock.patch('tkinter.filedialog.askdirectory', return_value='')
def test_no_path_raises_se(self, *_: Any) -> None:
def test_no_path_raises_soft_error(self, *_: Any) -> None:
self.assert_se("Path selection aborted.", ask_path_gui, 'test message', self.settings, False)

View File

@ -75,66 +75,66 @@ class TestReedSolomon(unittest.TestCase):
self.assertEqual(dec_enc2, enc)
def test_prim_fcr_basic(self) -> None:
nn = 30
kk = 18
tt = nn - kk
rs = RSCodec(tt, fcr=120, prim=0x187)
hexencmsg = ('00faa123555555c000000354064432'
nn = 30
kk = 18
tt = nn - kk
rs = RSCodec(tt, fcr=120, prim=0x187)
hex_enc_msg = ('00faa123555555c000000354064432'
'c02800fe97c434e1ff5365cf8fafe4')
strf = str
encmsg = bytearray.fromhex(strf(hexencmsg))
decmsg = encmsg[:kk]
tem = rs.encode(decmsg)
self.assertEqual(encmsg, tem, msg="encoded does not match expected")
strf = str
enc_msg = bytearray.fromhex(strf(hex_enc_msg))
dec_msg = enc_msg[:kk]
tem = rs.encode(dec_msg)
self.assertEqual(enc_msg, tem, msg="encoded does not match expected")
tdm, rtem = rs.decode(tem)
self.assertEqual(tdm, decmsg, msg="decoded does not match original")
self.assertEqual(rtem, tem, msg="decoded mesecc does not match original")
self.assertEqual(tdm, dec_msg, msg="decoded does not match original")
self.assertEqual(rtem, tem, msg="decoded mesecc does not match original")
tem1 = bytearray(tem) # Clone a copy
# Encoding and decoding intact message seem OK, so test errors
numerrs = tt >> 1 # Inject tt/2 errors (expected to recover fully)
for i in sample(range(nn), numerrs): # inject errors in random places
num_errs = tt >> 1 # Inject tt/2 errors (expected to recover fully)
for i in sample(range(nn), num_errs): # inject errors in random places
tem1[i] ^= 0xff # flip all 8 bits
tdm, _ = rs.decode(tem1)
self.assertEqual(tdm, decmsg, msg="decoded with errors does not match original")
self.assertEqual(tdm, dec_msg, msg="decoded with errors does not match original")
tem1 = bytearray(tem) # Clone another copy
numerrs += 1 # Inject tt/2 + 1 errors (expected to fail and detect it)
for i in sample(range(nn), numerrs): # Inject errors in random places
num_errs += 1 # Inject tt/2 + 1 errors (expected to fail and detect it)
for i in sample(range(nn), num_errs): # Inject errors in random places
tem1[i] ^= 0xff # Flip all 8 bits
# If this fails, it means excessive errors not detected
self.assertRaises(ReedSolomonError, rs.decode, tem1)
def test_prim_fcr_long(self) -> None:
nn = 48
kk = 34
tt = nn - kk
rs = RSCodec(tt, fcr=120, prim=0x187)
hexencmsg = ('08faa123555555c000000354064432c0280e1b4d090cfc04'
'887400000003500000000e1985ff9c6b33066ca9f43d12e8')
strf = str
encmsg = bytearray.fromhex(strf(hexencmsg))
decmsg = encmsg[:kk]
tem = rs.encode(decmsg)
self.assertEqual(encmsg, tem, msg="encoded does not match expected")
nn = 48
kk = 34
tt = nn - kk
rs = RSCodec(tt, fcr=120, prim=0x187)
hex_enc_msg = ('08faa123555555c000000354064432c0280e1b4d090cfc04'
'887400000003500000000e1985ff9c6b33066ca9f43d12e8')
strf = str
enc_msg = bytearray.fromhex(strf(hex_enc_msg))
dec_msg = enc_msg[:kk]
tem = rs.encode(dec_msg)
self.assertEqual(enc_msg, tem, msg="encoded does not match expected")
tdm, rtem = rs.decode(tem)
self.assertEqual(tdm, decmsg, msg="decoded does not match original")
self.assertEqual(rtem, tem, msg="decoded mesecc does not match original")
self.assertEqual(tdm, dec_msg, msg="decoded does not match original")
self.assertEqual(rtem, tem, msg="decoded mesecc does not match original")
tem1 = bytearray(tem)
numerrs = tt >> 1
for i in sample(range(nn), numerrs):
num_errs = tt >> 1
for i in sample(range(nn), num_errs):
tem1[i] ^= 0xff
tdm, rtem = rs.decode(tem1)
self.assertEqual(tdm, decmsg, msg="decoded with errors does not match original")
self.assertEqual(rtem, tem, msg="decoded mesecc with errors does not match original")
self.assertEqual(tdm, dec_msg, msg="decoded with errors does not match original")
self.assertEqual(rtem, tem, msg="decoded mesecc with errors does not match original")
tem1 = bytearray(tem)
numerrs += 1
for i in sample(range(nn), numerrs):
num_errs += 1
for i in sample(range(nn), num_errs):
tem1[i] ^= 0xff
self.assertRaises(ReedSolomonError, rs.decode, tem1)

View File

@ -265,6 +265,7 @@ class Settings(OrigSettings):
self.master_key = MasterKey()
self.software_operation = TX
self.local_testing_mode = False
self.qubes = False
self.all_keys = list(vars(self).keys())
self.key_list = self.all_keys[:self.all_keys.index('master_key')]
@ -304,6 +305,8 @@ class GatewaySettings(OrigGatewaySettings):
self.local_testing_mode = False
self.data_diode_sockets = False
self.qubes = False
self.all_keys = list(vars(self).keys())
self.key_list = self.all_keys[:self.all_keys.index('software_operation')]
self.defaults = {k: self.__dict__[k] for k in self.key_list}

View File

@ -36,14 +36,14 @@ from src.common.statics import (CH_FILE_RECV, CH_LOGGING, CH_NOTIFY, CLEAR_ENTI
LOCAL_PUBKEY, 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.commands import (ch_contact_s, ch_master_key, ch_nick, ch_setting, contact_rem, exit_tfc, log_command,
process_command, remove_log, reset_screen, win_activity, win_select, 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
from src.receiver.commands import process_command, remove_log, reset_screen, win_activity, win_select, wipe
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
from tests.utils import tear_queue, TFCTestCase
from tests.mock_classes import (ContactList, Gateway, group_name_to_group_id, GroupList, KeyList, MasterKey,
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, TFCTestCase)
class TestProcessCommand(TFCTestCase):
@ -71,7 +71,7 @@ class TestProcessCommand(TFCTestCase):
cleanup(self.unit_test_dir)
tear_queue(self.exit_queue)
def test_incomplete_command_raises_se(self) -> None:
def test_incomplete_command_raises_soft_error(self) -> None:
packet = assembly_packet_creator(COMMAND, b'test_command', s_header_override=C_L_HEADER, encrypt_packet=True)[0]
self.assert_se("Incomplete command.", process_command, self.ts, packet, *self.args)
@ -316,7 +316,7 @@ class TestChMasterKey(TFCTestCase):
@mock.patch('getpass.getpass', return_value='a')
@mock.patch('time.sleep', return_value=None)
@mock.patch('os.getrandom', side_effect=KeyboardInterrupt)
def test_keyboard_interrupt_raises_se(self, *_) -> None:
def test_keyboard_interrupt_raises_soft_error(self, *_) -> None:
self.assert_se("Error: Invalid password.", ch_master_key, *self.args)
@ -332,7 +332,7 @@ class TestChNick(TFCTestCase):
self.window = self.window_list.get_window(nick_to_pub_key("Alice"))
self.window.type = WIN_TYPE_CONTACT
def test_unknown_account_raises_se(self) -> None:
def test_unknown_account_raises_soft_error(self) -> None:
# Setup
cmd_data = nick_to_pub_key("Bob") + b'Bob_'
@ -364,7 +364,7 @@ class TestChSetting(TFCTestCase):
self.args = (self.ts, self.window_list, self.contact_list, self.group_list,
self.key_list, self.settings, self.gateway)
def test_invalid_data_raises_se(self) -> None:
def test_invalid_data_raises_soft_error(self) -> None:
# Setup
self.settings.key_list = ['']
@ -372,7 +372,7 @@ class TestChSetting(TFCTestCase):
cmd_data = b'setting' + b'True'
self.assert_se("Error: Received invalid setting data.", ch_setting, cmd_data, *self.args)
def test_invalid_setting_raises_se(self) -> None:
def test_invalid_setting_raises_soft_error(self) -> None:
# Setup
self.settings.key_list = ['']
@ -411,7 +411,7 @@ class TestChContactSetting(TFCTestCase):
group_list=self.group_list)
self.args = self.ts, self.window_list, self.contact_list, self.group_list
def test_invalid_window_raises_se(self) -> None:
def test_invalid_window_raises_soft_error(self) -> None:
# Setup
cmd_data = ENABLE + nick_to_pub_key("Bob")
header = CH_LOGGING
@ -503,7 +503,7 @@ class TestContactRemove(TFCTestCase):
"""Post-test actions."""
cleanup(self.unit_test_dir)
def test_no_contact_raises_se(self) -> None:
def test_no_contact_raises_soft_error(self) -> None:
# Setup
contact_list = ContactList(nicks=['Alice'])
group_list = GroupList(groups=[])

View File

@ -39,7 +39,7 @@ class TestGroupCreate(TFCTestCase):
self.window_list = WindowList()
self.group_id = group_name_to_group_id('test_group')
def test_too_many_purp_accounts_raises_se(self) -> None:
def test_too_many_purp_accounts_raises_soft_error(self) -> None:
# Setup
create_list = [nick_to_pub_key(str(n)) for n in range(51)]
cmd_data = self.group_id + b'test_group' + US_BYTE + b''.join(create_list)
@ -52,7 +52,7 @@ class TestGroupCreate(TFCTestCase):
self.assert_se("Error: TFC settings only allow 50 members per group.",
group_create, cmd_data, self.ts, self.window_list, contact_list, group_list, self.settings)
def test_full_group_list_raises_se(self) -> None:
def test_full_group_list_raises_soft_error(self) -> None:
# Setup
cmd_data = self.group_id + b'test_group' + US_BYTE + nick_to_pub_key('51')
group_list = GroupList(groups=[f"test_group_{n}" for n in range(50)])
@ -85,7 +85,7 @@ class TestGroupAdd(TFCTestCase):
self.settings = Settings()
self.window_list = WindowList()
def test_too_large_final_member_list_raises_se(self) -> None:
def test_too_large_final_member_list_raises_soft_error(self) -> None:
# Setup
group_list = GroupList(groups=['test_group'])
contact_list = ContactList(nicks=[str(n) for n in range(51)])
@ -97,7 +97,7 @@ class TestGroupAdd(TFCTestCase):
self.assert_se("Error: TFC settings only allow 50 members per group.",
group_add, cmd_data, self.ts, self.window_list, contact_list, group_list, self.settings)
def test_unknown_group_id_raises_se(self) -> None:
def test_unknown_group_id_raises_soft_error(self) -> None:
# Setup
group_list = GroupList(groups=['test_group'])
contact_list = ContactList(nicks=[str(n) for n in range(21)])
@ -137,7 +137,7 @@ class TestGroupRemove(TFCTestCase):
self.group.members = self.contact_list.contacts[:19]
self.settings = Settings()
def test_unknown_group_id_raises_se(self) -> None:
def test_unknown_group_id_raises_soft_error(self) -> None:
# Setup
group_list = GroupList(groups=['test_group'])
contact_list = ContactList(nicks=[str(n) for n in range(21)])
@ -161,12 +161,12 @@ class TestGroupDelete(TFCTestCase):
self.window_list = WindowList()
self.group_list = GroupList(groups=['test_group'])
def test_missing_group_raises_se(self) -> None:
def test_missing_group_raises_soft_error(self) -> None:
cmd_data = group_name_to_group_id('test_group2')
self.assert_se("Error: No group with ID '2e7mHQznTMsP6' found.",
group_delete, cmd_data, self.ts, self.window_list, self.group_list)
def test_unknown_group_id_raises_se(self) -> None:
def test_unknown_group_id_raises_soft_error(self) -> None:
# Setup
group_list = GroupList(groups=['test_group'])
cmd_data = group_name_to_group_id('test_group2')
@ -193,21 +193,21 @@ class TestGroupRename(TFCTestCase):
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_se(self) -> None:
def test_missing_group_id_raises_soft_error(self) -> None:
# Setup
cmd_data = group_name_to_group_id('test_group2') + b'new_name'
# Test
self.assert_se("Error: No group with ID '2e7mHQznTMsP6' found.", group_rename, cmd_data, *self.args)
def test_invalid_group_name_encoding_raises_se(self) -> None:
def test_invalid_group_name_encoding_raises_soft_error(self) -> None:
# Setup
cmd_data = group_name_to_group_id('test_group') + b'new_name' + UNDECODABLE_UNICODE
# Test
self.assert_se("Error: New name for group 'test_group' was invalid.", group_rename, cmd_data, *self.args)
def test_invalid_group_name_raises_se(self) -> None:
def test_invalid_group_name_raises_soft_error(self) -> None:
# Setup
cmd_data = group_name_to_group_id('test_group') + b'new_name\x1f'

View File

@ -73,7 +73,7 @@ class ProcessAssembledFile(TFCTestCase):
"""Post-test actions."""
cleanup(self.unit_test_dir)
def test_invalid_structure_raises_se(self) -> None:
def test_invalid_structure_raises_soft_error(self) -> None:
# Setup
payload = b'testfile.txt'
@ -81,7 +81,7 @@ class ProcessAssembledFile(TFCTestCase):
self.assert_se("Error: Received file had an invalid structure.",
process_assembled_file, self.ts, payload, *self.args)
def test_invalid_encoding_raises_se(self) -> None:
def test_invalid_encoding_raises_soft_error(self) -> None:
# Setup
payload = UNDECODABLE_UNICODE + US_BYTE + b'file_data'
@ -89,7 +89,7 @@ class ProcessAssembledFile(TFCTestCase):
self.assert_se("Error: Received file name had an invalid encoding.",
process_assembled_file, self.ts, payload, *self.args)
def test_invalid_name_raises_se(self) -> None:
def test_invalid_name_raises_soft_error(self) -> None:
# Setup
payload = b'\x01filename' + US_BYTE + b'file_data'
@ -97,7 +97,7 @@ class ProcessAssembledFile(TFCTestCase):
self.assert_se("Error: Received file had an invalid name.",
process_assembled_file, self.ts, payload, *self.args)
def test_slash_in_file_name_raises_se(self) -> None:
def test_slash_in_file_name_raises_soft_error(self) -> None:
# Setup
payload = b'file/name' + US_BYTE + b'file_data'
@ -105,7 +105,7 @@ class ProcessAssembledFile(TFCTestCase):
self.assert_se("Error: Received file had an invalid name.",
process_assembled_file, self.ts, payload, *self.args)
def test_invalid_key_raises_se(self) -> None:
def test_invalid_key_raises_soft_error(self) -> None:
# Setup
payload = b'testfile.txt' + US_BYTE + b'file_data'
@ -113,7 +113,7 @@ class ProcessAssembledFile(TFCTestCase):
self.assert_se("Error: Received file had an invalid key.",
process_assembled_file, self.ts, payload, *self.args)
def test_decryption_fail_raises_se(self) -> None:
def test_decryption_fail_raises_soft_error(self) -> None:
# Setup
file_data = encrypt_and_sign(b'file_data', self.key)[::-1]
payload = b'testfile.txt' + US_BYTE + file_data
@ -122,7 +122,7 @@ class ProcessAssembledFile(TFCTestCase):
self.assert_se("Error: Decryption of file data failed.",
process_assembled_file, self.ts, payload, *self.args)
def test_invalid_compression_raises_se(self) -> None:
def test_invalid_compression_raises_soft_error(self) -> None:
# Setup
compressed = zlib.compress(b'file_data', level=COMPRESSION_LEVEL)[::-1]
file_data = encrypt_and_sign(compressed, self.key) + self.key
@ -178,7 +178,7 @@ class TestNewFile(TFCTestCase):
"""Post-test actions."""
cleanup(self.unit_test_dir)
def test_unknown_account_raises_se(self) -> None:
def test_unknown_account_raises_soft_error(self) -> None:
# Setup
file_ct = encrypt_and_sign(self.compressed, self.file_key)
packet = nick_to_pub_key('Bob') + ORIGIN_CONTACT_HEADER + file_ct
@ -186,7 +186,7 @@ class TestNewFile(TFCTestCase):
# Test
self.assert_se("File from an unknown account.", new_file, self.ts, packet, *self.args)
def test_disabled_file_reception_raises_se(self) -> None:
def test_disabled_file_reception_raises_soft_error(self) -> None:
# Setup
file_ct = encrypt_and_sign(self.compressed, self.file_key)
packet = nick_to_pub_key('Alice') + ORIGIN_CONTACT_HEADER + file_ct
@ -237,13 +237,14 @@ class TestProcessFile(TFCTestCase):
"""Post-test actions."""
cleanup(self.unit_test_dir)
def test_invalid_key_raises_se(self) -> None:
def test_invalid_key_raises_soft_error(self) -> None:
self.file_key = SYMMETRIC_KEY_LENGTH * b'f'
self.args = self.file_key, self.contact_list, self.window_list, self.settings
self.assert_se("Error: Decryption key for file from Alice was invalid.",
process_file, self.ts, self.account, self.file_ct, *self.args)
def test_invalid_compression_raises_se(self) -> None:
def test_invalid_compression_raises_soft_error(self) -> None:
compressed = zlib.compress(b'file_data', level=COMPRESSION_LEVEL)[::-1]
file_data = encrypt_and_sign(compressed, self.file_key)
@ -251,7 +252,7 @@ class TestProcessFile(TFCTestCase):
process_file, self.ts, self.account, file_data, *self.args)
@mock.patch('time.sleep', return_value=None)
def test_invalid_file_name_raises_se(self, _: Any) -> None:
def test_invalid_file_name_raises_soft_error(self, _: Any) -> None:
compressed = zlib.compress(UNDECODABLE_UNICODE + b'file_data', level=COMPRESSION_LEVEL)
file_data = encrypt_and_sign(compressed, self.file_key)
@ -259,7 +260,7 @@ class TestProcessFile(TFCTestCase):
process_file, self.ts, self.account, file_data, *self.args)
@mock.patch('time.sleep', return_value=None)
def test_non_printable_name_raises_se(self, _: Any) -> None:
def test_non_printable_name_raises_soft_error(self, _: Any) -> None:
compressed = zlib.compress(str_to_bytes("file\x01") + b'file_data', level=COMPRESSION_LEVEL)
file_data = encrypt_and_sign(compressed, self.file_key)
@ -267,7 +268,7 @@ class TestProcessFile(TFCTestCase):
process_file, self.ts, self.account, file_data, *self.args)
@mock.patch('time.sleep', return_value=None)
def test_slash_in_name_raises_se(self, _: Any) -> None:
def test_slash_in_name_raises_soft_error(self, _: Any) -> None:
compressed = zlib.compress(str_to_bytes("Alice/file.txt") + b'file_data', level=COMPRESSION_LEVEL)
file_data = encrypt_and_sign(compressed, self.file_key)

View File

@ -40,8 +40,8 @@ from src.common.statics import (ARGON2_SALT_LENGTH, BOLD_ON, CLEAR_ENTIRE_SCR
from src.receiver.key_exchanges import key_ex_ecdhe, key_ex_psk_rx, key_ex_psk_tx, local_key_rdy, process_local_key
from tests.mock_classes import Contact, ContactList, KeyList, KeySet, Settings, WindowList
from tests.utils import cd_unit_test, cleanup, nick_to_short_address, nick_to_pub_key, tear_queue, TFCTestCase
from tests.utils import UNDECODABLE_UNICODE
from tests.utils import (cd_unit_test, cleanup, nick_to_short_address, nick_to_pub_key, tear_queue, TFCTestCase,
UNDECODABLE_UNICODE)
class TestProcessLocalKey(TFCTestCase):
@ -73,7 +73,7 @@ class TestProcessLocalKey(TFCTestCase):
@mock.patch('tkinter.Tk', return_value=MagicMock())
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', return_value='5KfgdgUvseWfNkoUPWSvxMPNStu5wBBxyjz1zpZtLEjk7ZvwEAT')
def test_invalid_decryption_key_raises_se(self, *_: Any) -> None:
def test_invalid_decryption_key_raises_soft_error(self, *_: Any) -> None:
# Setup
packet = b''
self.key_list.keysets = []
@ -104,7 +104,7 @@ class TestProcessLocalKey(TFCTestCase):
@mock.patch('tkinter.Tk', return_value=MagicMock())
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', side_effect=KeyboardInterrupt)
def test_keyboard_interrupt_raises_se(self, *_: Any) -> None:
def test_keyboard_interrupt_raises_soft_error(self, *_: Any) -> None:
# Setup
self.window_list.active_win = self.window_list.get_window(nick_to_pub_key('Alice'))
@ -115,7 +115,7 @@ class TestProcessLocalKey(TFCTestCase):
@mock.patch('tkinter.Tk', return_value=MagicMock())
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', side_effect=[b58encode(kek), b58encode(kek), b58encode(kek), b58encode(new_kek)])
def test_old_local_key_packet_raises_se(self, *_: Any) -> None:
def test_old_local_key_packet_raises_soft_error(self, *_: Any) -> None:
# Setup
self.key_list.keysets = []
new_key = os.urandom(SYMMETRIC_KEY_LENGTH)
@ -202,7 +202,7 @@ class TestKeyExECDHE(TFCTestCase):
self.args = self.packet, self.ts, self.window_list, self.contact_list, self.key_list, self.settings
@mock.patch('time.sleep', return_value=None)
def test_invalid_nick_raises_se(self, _: Any) -> None:
def test_invalid_nick_raises_soft_error(self, _: Any) -> None:
self.packet = (nick_to_pub_key("Alice")
+ SYMMETRIC_KEY_LENGTH * b'\x01'
+ SYMMETRIC_KEY_LENGTH * b'\x02'
@ -252,7 +252,7 @@ class TestKeyExPSKTx(TFCTestCase):
self.args = self.packet, self.ts, self.window_list, self.contact_list, self.key_list, self.settings
@mock.patch('time.sleep', return_value=None)
def test_invalid_nick_raises_se(self, _: Any) -> None:
def test_invalid_nick_raises_soft_error(self, _: Any) -> None:
self.packet = (nick_to_pub_key("Alice")
+ SYMMETRIC_KEY_LENGTH * b'\x01'
+ bytes(SYMMETRIC_KEY_LENGTH)
@ -305,13 +305,13 @@ class TestKeyExPSKRx(TFCTestCase):
"""Post-test actions."""
cleanup(self.unit_test_dir)
def test_unknown_account_raises_se(self) -> None:
def test_unknown_account_raises_soft_error(self) -> None:
self.assert_se(f"Error: Unknown account '{nick_to_short_address('Bob')}'.",
key_ex_psk_rx, b'\x00' + nick_to_pub_key("Bob"),
self.ts, self.window_list, self.contact_list, self.key_list, self.settings)
@mock.patch('builtins.input', return_value=file_name)
def test_invalid_psk_data_raises_se(self, _: Any) -> None:
def test_invalid_psk_data_raises_soft_error(self, _: Any) -> None:
# Setup
with open(self.file_name, 'wb+') as f:
f.write(os.urandom(135))
@ -321,7 +321,7 @@ class TestKeyExPSKRx(TFCTestCase):
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', return_value=file_name)
def test_permission_error_raises_se(self, *_: Any) -> None:
def test_permission_error_raises_soft_error(self, *_: Any) -> None:
# Setup
with open(self.file_name, 'wb+') as f:
f.write(os.urandom(PSK_FILE_SIZE))
@ -423,7 +423,7 @@ class TestKeyExPSKRx(TFCTestCase):
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', side_effect=[file_name, ''])
@mock.patch('getpass.getpass', side_effect=[KeyboardInterrupt])
def test_valid_psk_keyboard_interrupt_raises_se(self, *_: Any) -> None:
def test_valid_psk_keyboard_interrupt_raises_soft_error(self, *_: Any) -> None:
with open(self.file_name, 'wb+') as f:
f.write(bytes(PSK_FILE_SIZE))

View File

@ -38,8 +38,8 @@ from src.receiver.packet import PacketList
from src.receiver.windows import WindowList
from tests.mock_classes import ContactList, GroupList, KeyList, MasterKey, Settings
from tests.utils import assembly_packet_creator, cd_unit_test, cleanup, group_name_to_group_id
from tests.utils import nick_to_pub_key, TFCTestCase
from tests.utils import (assembly_packet_creator, cd_unit_test, cleanup, group_name_to_group_id,
nick_to_pub_key, TFCTestCase)
class TestProcessMessagePacket(TFCTestCase):
@ -88,7 +88,7 @@ class TestProcessMessagePacket(TFCTestCase):
# Invalid packets
@mock.patch('time.sleep', return_value=None)
def test_invalid_origin_header_raises_se(self, _: Any) -> None:
def test_invalid_origin_header_raises_soft_error(self, _: Any) -> None:
# Setup
invalid_origin_header = b'e'
packet = nick_to_pub_key('Alice') + invalid_origin_header + MESSAGE_LENGTH * b'm'
@ -98,7 +98,7 @@ class TestProcessMessagePacket(TFCTestCase):
process_message_packet, self.ts, packet, *self.args)
@mock.patch('time.sleep', return_value=None)
def test_masqueraded_command_raises_se(self, _: Any) -> None:
def test_masqueraded_command_raises_soft_error(self, _: Any) -> None:
for origin_header in [ORIGIN_USER_HEADER, ORIGIN_CONTACT_HEADER]:
# Setup
packet = LOCAL_PUBKEY + origin_header + MESSAGE_LENGTH * b'm'
@ -172,7 +172,7 @@ class TestProcessMessagePacket(TFCTestCase):
# File key messages
@mock.patch('time.sleep', return_value=None)
def test_user_origin_raises_se(self, _: Any) -> None:
def test_user_origin_raises_soft_error(self, _: Any) -> None:
assembly_ct_list = assembly_packet_creator(MESSAGE, ' ', origin_header=ORIGIN_USER_HEADER,
encrypt_packet=True, onion_pub_key=nick_to_pub_key('Alice'),
message_header=FILE_KEY_HEADER)
@ -181,7 +181,7 @@ class TestProcessMessagePacket(TFCTestCase):
self.assert_se("File key message from the user.", process_message_packet, self.ts, p, *self.args)
@mock.patch('time.sleep', return_value=None)
def test_invalid_file_key_data_raises_se(self, _: Any) -> None:
def test_invalid_file_key_data_raises_soft_error(self, _: Any) -> None:
assembly_ct_list = assembly_packet_creator(MESSAGE, ' ', origin_header=ORIGIN_CONTACT_HEADER,
encrypt_packet=True, onion_pub_key=nick_to_pub_key('Alice'),
message_header=FILE_KEY_HEADER)
@ -191,7 +191,7 @@ class TestProcessMessagePacket(TFCTestCase):
process_message_packet, self.ts, p, *self.args)
@mock.patch('time.sleep', return_value=None)
def test_too_large_file_key_data_raises_se(self, _: Any) -> None:
def test_too_large_file_key_data_raises_soft_error(self, _: Any) -> None:
assembly_ct_list = assembly_packet_creator(MESSAGE, base64.b85encode(BLAKE2_DIGEST_LENGTH * b'a'
+ SYMMETRIC_KEY_LENGTH * b'b'
+ b'a').decode(),
@ -216,7 +216,7 @@ class TestProcessMessagePacket(TFCTestCase):
# Group messages
@mock.patch('time.sleep', return_value=None)
def test_invalid_message_header_raises_se(self, _: Any) -> None:
def test_invalid_message_header_raises_soft_error(self, _: Any) -> None:
# Setup
assembly_ct_list = assembly_packet_creator(MESSAGE, 'test_message', origin_header=ORIGIN_CONTACT_HEADER,
encrypt_packet=True, onion_pub_key=nick_to_pub_key('Alice'),
@ -227,7 +227,7 @@ class TestProcessMessagePacket(TFCTestCase):
process_message_packet, self.ts, assembly_ct_list[0], *self.args)
@mock.patch('time.sleep', return_value=None)
def test_invalid_window_raises_se(self, _: Any) -> None:
def test_invalid_window_raises_soft_error(self, _: Any) -> None:
# Setup
assembly_ct_list = assembly_packet_creator(MESSAGE, 'test_message', origin_header=ORIGIN_CONTACT_HEADER,
encrypt_packet=True, onion_pub_key=nick_to_pub_key('Alice'),
@ -240,7 +240,7 @@ class TestProcessMessagePacket(TFCTestCase):
process_message_packet, self.ts, assembly_ct_list[0], *self.args)
@mock.patch('time.sleep', return_value=None)
def test_invalid_message_raises_se(self, _: Any) -> None:
def test_invalid_message_raises_soft_error(self, _: Any) -> None:
# Setup
assembly_ct_list = assembly_packet_creator(MESSAGE, ' ', origin_header=ORIGIN_CONTACT_HEADER,
encrypt_packet=True, onion_pub_key=nick_to_pub_key('Alice'),
@ -251,7 +251,7 @@ class TestProcessMessagePacket(TFCTestCase):
process_message_packet, self.ts, assembly_ct_list[0], *self.args)
@mock.patch('time.sleep', return_value=None)
def test_invalid_whisper_header_raises_se(self, _: Any) -> None:
def test_invalid_whisper_header_raises_soft_error(self, _: Any) -> None:
# Setup
assembly_ct_list = assembly_packet_creator(MESSAGE, '', origin_header=ORIGIN_CONTACT_HEADER,
encrypt_packet=True, onion_pub_key=nick_to_pub_key('Alice'),
@ -262,7 +262,7 @@ class TestProcessMessagePacket(TFCTestCase):
process_message_packet, self.ts, assembly_ct_list[0], *self.args)
@mock.patch('time.sleep', return_value=None)
def test_contact_not_in_group_raises_se(self, _: Any) -> None:
def test_contact_not_in_group_raises_soft_error(self, _: Any) -> None:
# Setup
assembly_ct_list = assembly_packet_creator(MESSAGE, 'test_message', origin_header=ORIGIN_CONTACT_HEADER,

View File

@ -129,7 +129,7 @@ class TestOutputLoop(unittest.TestCase):
tx_pub_key)
tx_mk, tx_harac = rotate_key(tx_mk, tx_harac)
# ECDHE keyset for Bob
# ECDHE keyset to Bob
command = (KEY_EX_ECDHE
+ nick_to_pub_key("Bob")
+ (4 * SYMMETRIC_KEY_LENGTH * b"a")
@ -138,7 +138,7 @@ class TestOutputLoop(unittest.TestCase):
local_key, local_harac = rotate_key(local_key, local_harac)
o_sleep(test_delay)
# Message for Bob
# Message to Bob
queue_packet(tx_mk,
tx_hk,
tx_harac,
@ -147,7 +147,7 @@ class TestOutputLoop(unittest.TestCase):
tx_mk, tx_harac = rotate_key(tx_mk, tx_harac)
o_sleep(test_delay)
# Enable file reception for Bob
# Enable file reception to Bob
command = CH_FILE_RECV + ENABLE.upper() + US_BYTE
queue_packet(local_key, tx_hk, local_harac, command)
o_sleep(test_delay)

View File

@ -38,8 +38,8 @@ from src.transmitter.packet import split_to_assembly_packets
from src.receiver.packet import decrypt_assembly_packet, Packet, PacketList
from tests.mock_classes import ContactList, create_contact, KeyList, Settings, WindowList
from tests.utils import assembly_packet_creator, cd_unit_test, cleanup, nick_to_pub_key, TFCTestCase
from tests.utils import UNDECODABLE_UNICODE
from tests.utils import (assembly_packet_creator, cd_unit_test, cleanup, nick_to_pub_key, TFCTestCase,
UNDECODABLE_UNICODE)
class TestDecryptAssemblyPacket(TFCTestCase):
@ -54,7 +54,7 @@ class TestDecryptAssemblyPacket(TFCTestCase):
self.keyset = self.key_list.get_keyset(nick_to_pub_key("Alice"))
self.args = self.onion_pub_key, self.origin, self.window_list, self.contact_list, self.key_list
def test_decryption_with_zero_rx_key_raises_se(self) -> None:
def test_decryption_with_zero_rx_key_raises_soft_error(self) -> None:
# Setup
keyset = self.key_list.get_keyset(nick_to_pub_key("Alice"))
keyset.rx_mk = bytes(SYMMETRIC_KEY_LENGTH)
@ -64,12 +64,12 @@ class TestDecryptAssemblyPacket(TFCTestCase):
self.assert_se("Warning! Loaded zero-key for packet decryption.",
decrypt_assembly_packet, packet, *self.args)
def test_invalid_harac_ct_raises_se(self) -> None:
def test_invalid_harac_ct_raises_soft_error(self) -> None:
packet = assembly_packet_creator(MESSAGE, payload="Test message", encrypt_packet=True, tamper_harac=True)[0]
self.assert_se("Warning! Received packet from Alice had an invalid hash ratchet MAC.",
decrypt_assembly_packet, packet, *self.args)
def test_decryption_with_zero_rx_hek_raises_se(self) -> None:
def test_decryption_with_zero_rx_hek_raises_soft_error(self) -> None:
# Setup
keyset = self.key_list.get_keyset(nick_to_pub_key("Alice"))
keyset.rx_hk = bytes(SYMMETRIC_KEY_LENGTH)
@ -78,7 +78,7 @@ class TestDecryptAssemblyPacket(TFCTestCase):
# Test
self.assert_se("Warning! Loaded zero-key for packet decryption.", decrypt_assembly_packet, packet, *self.args)
def test_expired_harac_raises_se(self) -> None:
def test_expired_harac_raises_soft_error(self) -> None:
# Setup
self.keyset.rx_harac = 1
@ -93,7 +93,7 @@ class TestDecryptAssemblyPacket(TFCTestCase):
self.assert_se("Dropped packet from Alice.",
decrypt_assembly_packet, packet, *self.args)
def test_invalid_packet_ct_raises_se(self) -> None:
def test_invalid_packet_ct_raises_soft_error(self) -> None:
packet = assembly_packet_creator(MESSAGE, payload="Test message", encrypt_packet=True, tamper_message=True)[0]
self.assert_se("Warning! Received packet from Alice had an invalid MAC.",
decrypt_assembly_packet, packet, *self.args)
@ -148,7 +148,7 @@ class TestPacket(TFCTestCase):
"""Post-test actions."""
cleanup(self.unit_test_dir)
def test_invalid_assembly_packet_header_raises_se(self) -> None:
def test_invalid_assembly_packet_header_raises_soft_error(self) -> None:
# Setup
packet = Packet(self.onion_pub_key, ORIGIN_CONTACT_HEADER, MESSAGE, self.contact, self.settings)
a_packet = assembly_packet_creator(MESSAGE, payload=self.short_msg, s_header_override=b'i')[0]
@ -157,7 +157,7 @@ class TestPacket(TFCTestCase):
self.assert_se("Error: Received packet had an invalid assembly packet header.", packet.add_packet, a_packet)
self.assertEqual(packet.log_masking_ctr, 1)
def test_missing_start_packet_raises_se(self) -> None:
def test_missing_start_packet_raises_soft_error(self) -> None:
# Setup
packet = Packet(self.onion_pub_key, ORIGIN_USER_HEADER, MESSAGE, self.contact, self.settings)
@ -179,7 +179,7 @@ class TestPacket(TFCTestCase):
self.whisper_header + PRIVATE_MESSAGE_HEADER + self.short_msg.encode())
self.assertEqual(packet.log_ct_list, [b'test_ct'])
def test_compression_error_raises_se(self) -> None:
def test_compression_error_raises_soft_error(self) -> None:
# Setup
packet = Packet(self.onion_pub_key, ORIGIN_USER_HEADER, MESSAGE, self.contact, self.settings)
packet_list = assembly_packet_creator(MESSAGE, self.short_msg, tamper_compression=True)
@ -203,7 +203,7 @@ class TestPacket(TFCTestCase):
self.assertEqual(message, self.whisper_header + PRIVATE_MESSAGE_HEADER + self.msg.encode())
self.assertEqual(packet.log_ct_list, 3 * [b'test_ct'])
def test_decryption_error_raises_se(self) -> None:
def test_decryption_error_raises_soft_error(self) -> None:
# Setup
packet = Packet(self.onion_pub_key, ORIGIN_USER_HEADER, MESSAGE, self.contact, self.settings)
packet_list = assembly_packet_creator(MESSAGE, self.msg, tamper_ciphertext=True)
@ -235,7 +235,7 @@ class TestPacket(TFCTestCase):
self.assertIsNone(packet.assemble_and_store_file(self.ts, self.onion_pub_key, self.window_list))
self.assertTrue(os.path.isfile(f'{DIR_RECV_FILES}Alice/testfile.txt.1'))
def test_short_file_from_user_raises_se(self) -> None:
def test_short_file_from_user_raises_soft_error(self) -> None:
# Setup
packet = Packet(self.onion_pub_key, ORIGIN_USER_HEADER, FILE, self.contact, self.settings)
packets = split_to_assembly_packets(self.short_f_data, FILE)
@ -245,7 +245,7 @@ class TestPacket(TFCTestCase):
self.assert_se("Ignored file from the user.", packet.add_packet, p)
self.assertEqual(packet.log_masking_ctr, 1)
def test_unauthorized_file_from_contact_raises_se(self) -> None:
def test_unauthorized_file_from_contact_raises_soft_error(self) -> None:
# Setup
self.contact.file_reception = False
@ -305,7 +305,7 @@ class TestPacket(TFCTestCase):
self.assert_se("Alert! File reception disabled mid-transfer.", packet.add_packet, p)
self.assertEqual(packet.log_masking_ctr, len(packet_list))
def test_long_file_from_user_raises_se(self) -> None:
def test_long_file_from_user_raises_soft_error(self) -> None:
# Setup
packet = Packet(self.onion_pub_key, ORIGIN_USER_HEADER, FILE, self.contact, self.settings)
packet_list = assembly_packet_creator(FILE)
@ -314,7 +314,7 @@ class TestPacket(TFCTestCase):
self.assert_se("Ignored file from the user.", packet.add_packet, packet_list[0])
self.assertEqual(packet.log_masking_ctr, 1)
def test_unauthorized_long_file_raises_se(self) -> None:
def test_unauthorized_long_file_raises_soft_error(self) -> None:
# Setup
self.contact.file_reception = False
@ -326,7 +326,7 @@ class TestPacket(TFCTestCase):
packet.add_packet, packet_list[0])
self.assertEqual(packet.log_masking_ctr, 1)
def test_invalid_long_file_header_raises_se(self) -> None:
def test_invalid_long_file_header_raises_soft_error(self) -> None:
# Setup
packet = Packet(self.onion_pub_key, ORIGIN_CONTACT_HEADER, FILE, self.contact, self.settings)
packet_list = assembly_packet_creator(FILE, file_name=UNDECODABLE_UNICODE)
@ -390,7 +390,7 @@ class TestPacket(TFCTestCase):
self.assertEqual(packet.assemble_command_packet(), command)
self.assertEqual(packet.log_masking_ctr, 0)
def test_long_command_hash_mismatch_raises_se(self) -> None:
def test_long_command_hash_mismatch_raises_soft_error(self) -> None:
# Setup
packet = Packet(LOCAL_ID, ORIGIN_CONTACT_HEADER, COMMAND, self.contact, self.settings)
packet_list = assembly_packet_creator(COMMAND, os.urandom(500), tamper_cmd_hash=True)
@ -402,7 +402,7 @@ class TestPacket(TFCTestCase):
self.assert_se("Error: Received an invalid command.", packet.assemble_command_packet)
self.assertEqual(packet.log_masking_ctr, 0)
def test_long_command_compression_error_raises_se(self) -> None:
def test_long_command_compression_error_raises_soft_error(self) -> None:
# Setup
packet = Packet(LOCAL_ID, ORIGIN_CONTACT_HEADER, COMMAND, self.contact, self.settings)
packet_list = assembly_packet_creator(COMMAND, os.urandom(500), tamper_compression=True)

View File

@ -76,7 +76,7 @@ class TestRxWindow(TFCTestCase):
self.assertEqual(window.window_contacts[0].onion_pub_key, nick_to_pub_key("Alice"))
self.assertEqual(window.name, 'test_group')
def test_invalid_uid_raises_se(self) -> None:
def test_invalid_uid_raises_soft_error(self) -> None:
self.assert_se("Invalid window 'mfqwcylbmfqwcylbmfqwcylbmfqwcylbmfqwcylbmfqwcylbmfqwbfad'.",
self.create_window, ONION_SERVICE_PUBLIC_KEY_LENGTH * b'a')

View File

@ -136,11 +136,11 @@ class TestChangeECRatio(TFCTestCase):
"""Pre-test actions."""
self.gateway = Gateway()
def test_non_digit_value_raises_se(self) -> None:
def test_non_digit_value_raises_soft_error(self) -> None:
self.assert_se("Error: Received invalid EC ratio value from Transmitter Program.",
change_ec_ratio, b'a', self.gateway)
def test_invalid_digit_value_raises_se(self) -> None:
def test_invalid_digit_value_raises_soft_error(self) -> None:
self.assert_se("Error: Received invalid EC ratio value from Transmitter Program.",
change_ec_ratio, b'-1', self.gateway)
@ -155,11 +155,11 @@ class TestChangeBaudrate(TFCTestCase):
"""Pre-test actions."""
self.gateway = Gateway()
def test_non_digit_value_raises_se(self) -> None:
def test_non_digit_value_raises_soft_error(self) -> None:
self.assert_se("Error: Received invalid baud rate value from Transmitter Program.",
change_baudrate, b'a', self.gateway)
def test_invalid_digit_value_raises_se(self) -> None:
def test_invalid_digit_value_raises_soft_error(self) -> None:
self.assert_se("Error: Received invalid baud rate value from Transmitter Program.",
change_baudrate, b'1300', self.gateway)
@ -216,7 +216,7 @@ class TestAddContact(unittest.TestCase):
def test_add_contact(self) -> None:
command = b''.join([nick_to_pub_key('Alice'), nick_to_pub_key('Bob')])
self.assertIsNone(add_contact(command, True, self.queues))
self.assertIsNone(add_contact(command, self.queues, True))
self.assertEqual(self.queues[CONTACT_MGMT_QUEUE].qsize(), 1)
for q in [GROUP_MGMT_QUEUE, C_REQ_MGMT_QUEUE]:
command = self.queues[q].get()

View File

@ -107,9 +107,9 @@ class TestPubKeyChecker(unittest.TestCase):
@mock.patch('shutil.get_terminal_size', return_value=[200, 200])
def test_pub_key_checker(self, _: Any) -> None:
# Setup
public_key = TFC_PUBLIC_KEY_LENGTH*b'a'
public_key = TFC_PUBLIC_KEY_LENGTH*b'a'
invalid_public_key = b58encode(public_key, public_key=True)[:-1] + 'a'
account = nick_to_pub_key('Bob')
account = nick_to_pub_key('Bob')
for local_test in [True, False]:
self.queues[PUB_KEY_SEND_QUEUE].put((account, public_key))

View File

@ -38,7 +38,7 @@ class TestFlaskServer(unittest.TestCase):
url_token_public_key = X448.derive_public_key(url_token_private_key).hex()
url_token = 'a450987345098723459870234509827340598273405983274234098723490285'
url_token_old = 'a450987345098723459870234509827340598273405983274234098723490286'
url_token_invalid = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
url_token_invalid = 'ääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääää'
onion_pub_key = nick_to_pub_key('Alice')
onion_address = nick_to_onion_address('Alice')
packet1 = "packet1"
@ -48,6 +48,13 @@ class TestFlaskServer(unittest.TestCase):
# Test
app = flask_server(queues, url_token_public_key, unit_test=True)
# Test valid URL token returns all queued messages
queues[URL_TOKEN_QUEUE].put((onion_pub_key, url_token_old))
queues[URL_TOKEN_QUEUE].put((onion_pub_key, url_token))
queues[M_TO_FLASK_QUEUE].put((packet1, onion_pub_key))
queues[M_TO_FLASK_QUEUE].put((packet2, onion_pub_key))
queues[F_TO_FLASK_QUEUE].put((packet3, onion_pub_key))
with app.test_client() as c:
# Test root domain returns public key of server.
resp = c.get('/')
@ -63,13 +70,6 @@ class TestFlaskServer(unittest.TestCase):
resp = c.get(f'/{url_token_invalid}/files/')
self.assertEqual(b'', resp.data)
# Test valid URL token returns all queued messages
queues[URL_TOKEN_QUEUE].put((onion_pub_key, url_token_old))
queues[URL_TOKEN_QUEUE].put((onion_pub_key, url_token))
queues[M_TO_FLASK_QUEUE].put((packet1, onion_pub_key))
queues[M_TO_FLASK_QUEUE].put((packet2, onion_pub_key))
queues[F_TO_FLASK_QUEUE].put((packet3, onion_pub_key))
with app.test_client() as c:
resp = c.get(f'/{url_token}/messages/')
self.assertEqual(b'packet1\npacket2', resp.data)

View File

@ -28,9 +28,9 @@ from unittest import mock
from unittest.mock import MagicMock
from typing import Any
from src.common.database import TFCDatabase, MessageLog
from src.common.db_logs import write_log_entry
from src.common.encoding import bool_to_bytes
from src.common.database import TFCDatabase, MessageLog
from src.common.db_logs import write_log_entry
from src.common.encoding import bool_to_bytes
from src.common.db_masterkey import MasterKey as OrigMasterKey
from src.common.statics import (BOLD_ON, CLEAR_ENTIRE_SCREEN, COMMAND_PACKET_QUEUE, CURSOR_LEFT_UP_CORNER,
DIR_USER_DATA, KEY_MGMT_ACK_QUEUE, KEX_STATUS_NO_RX_PSK, KEX_STATUS_UNVERIFIED,
@ -41,16 +41,16 @@ from src.common.statics import (BOLD_ON, CLEAR_ENTIRE_SCREEN, COMMAND_PACKE
UNENCRYPTED_WIPE_COMMAND, VERSION, WIN_TYPE_CONTACT, WIN_TYPE_GROUP,
KDB_HALT_ACK_HEADER, KDB_M_KEY_CHANGE_HALT_HEADER)
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
from src.transmitter.commands import remove_log, rxp_display_unread, rxp_show_sys_win, send_onion_service_key, verify
from src.transmitter.commands import whisper, whois, wipe
from src.transmitter.commands import (change_master_key, change_setting, clear_screens, exit_tfc, log_command,
print_about, print_help, print_recipients, print_settings, process_command,
remove_log, rxp_display_unread, rxp_show_sys_win, send_onion_service_key,
verify, whisper, whois, wipe)
from src.transmitter.packet import split_to_assembly_packets
from tests.mock_classes import ContactList, create_contact, Gateway, GroupList, MasterKey, OnionService, Settings
from tests.mock_classes import TxWindow, UserInput
from tests.utils import assembly_packet_creator, cd_unit_test, cleanup, group_name_to_group_id
from tests.utils import gen_queue_dict, nick_to_onion_address, nick_to_pub_key, tear_queues, TFCTestCase
from tests.mock_classes import (ContactList, create_contact, Gateway, GroupList, MasterKey, OnionService, Settings,
TxWindow, UserInput)
from tests.utils import (assembly_packet_creator, cd_unit_test, cleanup, group_name_to_group_id, gen_queue_dict,
nick_to_onion_address, nick_to_pub_key, tear_queues, TFCTestCase)
class TestProcessCommand(TFCTestCase):
@ -268,17 +268,17 @@ class TestLogCommand(TFCTestCase):
log_command, UserInput("history"), *self.args)
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 1)
def test_invalid_number_raises_se(self) -> None:
def test_invalid_number_raises_soft_error(self) -> None:
self.assert_se("Error: Invalid number of messages.",
log_command, UserInput('history a'), *self.args)
def test_too_high_number_raises_se(self) -> None:
def test_too_high_number_raises_soft_error(self) -> None:
self.assert_se("Error: Invalid number of messages.",
log_command, UserInput('history 94857634985763454345'), *self.args)
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', return_value='No')
def test_user_abort_raises_se(self, *_: Any) -> None:
def test_user_abort_raises_soft_error(self, *_: Any) -> None:
self.assert_se("Log file export aborted.",
log_command, UserInput('export'), *self.args)
@ -290,7 +290,7 @@ class TestLogCommand(TFCTestCase):
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', return_value='Yes')
@mock.patch('getpass.getpass', side_effect=['test_password', 'test_password', KeyboardInterrupt])
def test_keyboard_interrupt_raises_se(self, *_: Any) -> None:
def test_keyboard_interrupt_raises_soft_error(self, *_: Any) -> None:
self.master_key = OrigMasterKey(operation=TX, local_test=True)
self.assert_se("Authentication aborted.",
log_command, UserInput('export'), *self.args)
@ -543,12 +543,12 @@ class TestChangeMasterKey(TFCTestCase):
self.assert_se("Error: Command is disabled during traffic masking.",
change_master_key, UserInput(), *self.args)
def test_missing_target_sys_raises_se(self) -> None:
def test_missing_target_sys_raises_soft_error(self) -> None:
self.assert_se("Error: No target-system ('tx' or 'rx') specified.",
change_master_key, UserInput("passwd "), *self.args)
@mock.patch('getpass.getpass', return_value='test_password')
def test_invalid_target_sys_raises_se(self, _: Any) -> None:
def test_invalid_target_sys_raises_soft_error(self, _: Any) -> None:
self.assert_se("Error: Invalid target system 't'.",
change_master_key, UserInput("passwd t"), *self.args)
@ -557,7 +557,7 @@ class TestChangeMasterKey(TFCTestCase):
@mock.patch('getpass.getpass', side_effect=['test_password', 'a', 'a'])
@mock.patch('time.sleep', return_value=None)
@mock.patch('src.common.db_masterkey.MIN_KEY_DERIVATION_TIME', 0.01)
def test_invalid_response_from_key_db_raises_se(self, *_: Any) -> None:
def test_invalid_response_from_key_db_raises_soft_error(self, *_: Any) -> None:
# Setup
def mock_sender_loop() -> None:
"""Mock sender loop key management functionality."""
@ -581,7 +581,7 @@ class TestChangeMasterKey(TFCTestCase):
@mock.patch('getpass.getpass', side_effect=['test_password', 'a', 'a'])
@mock.patch('time.sleep', return_value=None)
@mock.patch('src.common.db_masterkey.MIN_KEY_DERIVATION_TIME', 0.01)
def test_transmitter_command_raises_system_exit_if_key_database_returns_invalid_master_key(self, *_: Any) -> None:
def test_transmitter_command_raises_critical_error_if_key_database_returns_invalid_master_key(self, *_: Any) -> None:
# Setup
def mock_sender_loop() -> None:
"""Mock sender loop key management functionality."""
@ -693,7 +693,7 @@ class TestChangeMasterKey(TFCTestCase):
@mock.patch('time.sleep', return_value=None)
@mock.patch('getpass.getpass', side_effect=KeyboardInterrupt)
def test_keyboard_interrupt_raises_se(self, *_: Any) -> None:
def test_keyboard_interrupt_raises_soft_error(self, *_: Any) -> None:
self.assert_se("Authentication aborted.", change_master_key, UserInput("passwd tx"), *self.args)
@ -717,7 +717,7 @@ class TestRemoveLog(TFCTestCase):
tear_queues(self.queues)
cleanup(self.unit_test_dir)
def test_missing_contact_raises_se(self) -> None:
def test_missing_contact_raises_soft_error(self) -> None:
self.assert_se("Error: No contact/group specified.",
remove_log, UserInput(''), *self.args)
@ -734,12 +734,12 @@ class TestRemoveLog(TFCTestCase):
@mock.patch('shutil.get_terminal_size', return_value=[150, 150])
@mock.patch('builtins.input', return_value='Yes')
def test_removal_with_invalid_account_raises_se(self, *_: Any) -> None:
def test_removal_with_invalid_account_raises_soft_error(self, *_: Any) -> None:
self.assert_se("Error: Invalid account.",
remove_log, UserInput(f'/rmlogs {nick_to_onion_address("Alice")[:-1] + "a"}'), *self.args)
@mock.patch('builtins.input', return_value='Yes')
def test_invalid_group_id_raises_se(self, _: Any) -> None:
def test_invalid_group_id_raises_soft_error(self, _: Any) -> None:
self.assert_se("Error: Invalid group ID.",
remove_log, UserInput(f'/rmlogs {group_name_to_group_id("test_group")[:-1] + b"a"}'), *self.args)
@ -788,7 +788,7 @@ class TestRemoveLog(TFCTestCase):
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 1)
@mock.patch('builtins.input', return_value='Yes')
def test_unknown_selector_raises_se(self, _: Any) -> None:
def test_unknown_selector_raises_soft_error(self, _: Any) -> None:
# Setup
write_log_entry(M_S_HEADER + PADDING_LENGTH * b'a', nick_to_pub_key("Alice"), self.tfc_log_database)
@ -814,15 +814,15 @@ class TestChangeSetting(TFCTestCase):
"""Post-test actions."""
tear_queues(self.queues)
def test_missing_setting_raises_se(self) -> None:
def test_missing_setting_raises_soft_error(self) -> None:
self.assert_se("Error: No setting specified.",
change_setting, UserInput('set'), *self.args)
def test_invalid_setting_raises_se(self) -> None:
def test_invalid_setting_raises_soft_error(self) -> None:
self.assert_se("Error: Invalid setting 'e_correction_ratia'.",
change_setting, UserInput("set e_correction_ratia true"), *self.args)
def test_missing_value_raises_se(self) -> None:
def test_missing_value_raises_soft_error(self) -> None:
self.assert_se("Error: No value for setting specified.",
change_setting, UserInput("set serial_error_correction"), *self.args)
@ -1027,11 +1027,11 @@ class TestVerify(TFCTestCase):
self.window.contact = self.contact
self.args = self.window, self.contact_list
def test_active_group_raises_se(self) -> None:
def test_active_group_raises_soft_error(self) -> None:
self.window.type = WIN_TYPE_GROUP
self.assert_se("Error: A group is selected.", verify, *self.args)
def test_psk_raises_se(self) -> None:
def test_psk_raises_soft_error(self) -> None:
self.contact.kex_status = KEX_STATUS_NO_RX_PSK
self.assert_se("Pre-shared keys have no fingerprints.", verify, *self.args)
@ -1048,7 +1048,7 @@ class TestVerify(TFCTestCase):
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', side_effect=KeyboardInterrupt)
def test_keyboard_interrupt_raises_se(self, *_: Any) -> None:
def test_keyboard_interrupt_raises_soft_error(self, *_: Any) -> None:
self.contact.kex_status = KEX_STATUS_VERIFIED
self.assert_se("Fingerprint verification aborted.", verify, *self.args)
self.assertEqual(self.contact.kex_status, KEX_STATUS_VERIFIED)
@ -1066,7 +1066,7 @@ class TestWhisper(TFCTestCase):
self.queues = gen_queue_dict()
self.args = self.window, self.settings, self.queues
def test_empty_input_raises_se(self) -> None:
def test_empty_input_raises_soft_error(self) -> None:
self.assert_se("Error: No whisper message specified.",
whisper, UserInput("whisper"), *self.args)
@ -1087,10 +1087,10 @@ class TestWhois(TFCTestCase):
self.group_list = GroupList(groups=['test_group'])
self.args = self.contact_list, self.group_list
def test_missing_selector_raises_se(self) -> None:
def test_missing_selector_raises_soft_error(self) -> None:
self.assert_se("Error: No account or nick specified.", whois, UserInput("whois"), *self.args)
def test_unknown_account_raises_se(self) -> None:
def test_unknown_account_raises_soft_error(self) -> None:
self.assert_se("Error: Unknown selector.", whois, UserInput("whois alice"), *self.args)
def test_nick_from_account(self) -> None:
@ -1132,7 +1132,7 @@ class TestWipe(TFCTestCase):
self.args = self.settings, self.queues, self.gateway
@mock.patch('builtins.input', return_value='No')
def test_no_raises_se(self, _: Any) -> None:
def test_no_raises_soft_error(self, _: Any) -> None:
self.assert_se("Wipe command aborted.", wipe, *self.args)
@mock.patch('os.system', return_value=None)

View File

@ -28,8 +28,8 @@ from src.common.encoding import b58encode
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
from src.transmitter.commands_g import (group_add_member, group_create, group_rm_group, group_rm_member,
process_group_command, group_rename)
from tests.mock_classes import create_group, Contact, ContactList, GroupList, MasterKey, Settings, UserInput, TxWindow
from tests.utils import cd_unit_test, cleanup, gen_queue_dict, nick_to_pub_key, tear_queues, TFCTestCase
@ -58,19 +58,19 @@ class TestProcessGroupCommand(TFCTestCase):
self.assert_se("Error: Command is disabled during traffic masking.",
process_group_command, UserInput(), *self.args)
def test_invalid_command_raises_se(self) -> None:
def test_invalid_command_raises_soft_error(self) -> None:
self.assert_se("Error: Invalid group command.", process_group_command, UserInput('group '), *self.args)
def test_invalid_command_parameters_raises_se(self) -> None:
def test_invalid_command_parameters_raises_soft_error(self) -> None:
self.assert_se("Error: Invalid group command.", process_group_command, UserInput('group bad'), *self.args)
def test_missing_group_id_raises_se(self) -> None:
def test_missing_group_id_raises_soft_error(self) -> None:
self.assert_se("Error: No group ID specified.", process_group_command, UserInput('group join '), *self.args)
def test_invalid_group_id_raises_se(self) -> None:
def test_invalid_group_id_raises_soft_error(self) -> None:
self.assert_se("Error: Invalid group ID.", process_group_command, UserInput('group join invalid'), *self.args)
def test_missing_name_raises_se(self) -> None:
def test_missing_name_raises_soft_error(self) -> None:
self.assert_se("Error: No group name specified.", process_group_command, UserInput('group create '), *self.args)
@mock.patch('builtins.input', return_value='Yes')
@ -105,7 +105,7 @@ class TestGroupCreate(TFCTestCase):
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_se(self) -> None:
def test_invalid_group_name_raises_soft_error(self) -> None:
# Setup
self.configure_groups(no_contacts=21)
@ -113,7 +113,7 @@ class TestGroupCreate(TFCTestCase):
self.assert_se("Error: Group name must be printable.",
group_create, 'test_group\x1f', self.account_list, *self.args)
def test_too_many_purp_accounts_raises_se(self) -> None:
def test_too_many_purp_accounts_raises_soft_error(self) -> None:
# Setup
self.configure_groups(no_contacts=60)
@ -123,7 +123,7 @@ class TestGroupCreate(TFCTestCase):
group_create, 'test_group_50', cl_str,
self.contact_list, self.group_list, self.settings, self.queues, self.master_key)
def test_full_group_list_raises_se(self) -> None:
def test_full_group_list_raises_soft_error(self) -> None:
# Setup
self.group_list = GroupList(groups=[f"testgroup_{n}" for n in range(50)])
@ -180,7 +180,7 @@ class TestGroupAddMember(TFCTestCase):
def test_raises_fr_if_specified_group_does_not_exist_and_user_chooses_no(self, *_: Any) -> None:
self.assert_se("Group creation aborted.", group_add_member, 'test_group', [], *self.args)
def test_too_large_final_member_list_raises_se(self) -> None:
def test_too_large_final_member_list_raises_soft_error(self) -> None:
# Setup
contact_list = ContactList(nicks=[str(n) for n in range(51)])
group_list = GroupList(groups=['testgroup'])
@ -266,18 +266,18 @@ class TestGroupRmGroup(TFCTestCase):
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', return_value='No')
def test_cancel_of_remove_raises_se(self, *_: Any) -> None:
def test_cancel_of_remove_raises_soft_error(self, *_: Any) -> None:
self.assert_se("Group removal aborted.", group_rm_group, 'test_group', *self.args)
@mock.patch('builtins.input', return_value='Yes')
def test_remove_group_not_on_transmitter_raises_se(self, _: Any) -> None:
def test_remove_group_not_on_transmitter_raises_soft_error(self, _: Any) -> None:
unknown_group_id = b58encode(bytes(GROUP_ID_LENGTH))
self.assert_se("Transmitter has no group '2dVseX46KS9Sp' to remove.",
group_rm_group, unknown_group_id, *self.args)
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 2)
@mock.patch('builtins.input', return_value='Yes')
def test_invalid_group_id_raises_se(self, _: Any) -> None:
def test_invalid_group_id_raises_soft_error(self, _: Any) -> None:
invalid_group_id = b58encode(bytes(GROUP_ID_LENGTH))[:-1]
self.assert_se("Error: Invalid group name/ID.", group_rm_group, invalid_group_id, *self.args)
@ -304,14 +304,14 @@ class TestGroupRename(TFCTestCase):
"""Post-test actions."""
tear_queues(self.queues)
def test_contact_window_raises_se(self) -> None:
def test_contact_window_raises_soft_error(self) -> None:
# Setup
self.window.type = WIN_TYPE_CONTACT
# Test
self.assert_se("Error: Selected window is not a group window.", group_rename, "window", *self.args)
def test_invalid_group_name_raises_se(self) -> None:
def test_invalid_group_name_raises_soft_error(self) -> None:
# Setup
self.window.type = WIN_TYPE_GROUP
self.window.group = self.group_list.get_group('test_group')

View File

@ -30,13 +30,13 @@ from src.common.statics import (COMMAND_PACKET_QUEUE, CONFIRM_CODE_LENGTH, FINGE
KEY_MANAGEMENT_QUEUE, LOCAL_ID, 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, get_onion_address_from_user
from src.transmitter.contact import remove_contact
from src.transmitter.contact import (add_new_contact, change_nick, contact_setting, get_onion_address_from_user,
remove_contact)
from tests.mock_classes import ContactList, create_contact, create_group, Group, GroupList, MasterKey, OnionService
from tests.mock_classes import Settings, TxWindow, UserInput
from tests.utils import cd_unit_test, cleanup, gen_queue_dict, group_name_to_group_id, ignored
from tests.utils import nick_to_onion_address, nick_to_pub_key, tear_queues, TFCTestCase, VALID_ECDHE_PUB_KEY
from tests.mock_classes import (ContactList, create_contact, create_group, Group, GroupList, MasterKey, OnionService,
Settings, TxWindow, UserInput)
from tests.utils import (cd_unit_test, cleanup, gen_queue_dict, group_name_to_group_id, ignored,
nick_to_onion_address, nick_to_pub_key, tear_queues, TFCTestCase, VALID_ECDHE_PUB_KEY)
class TestAddNewContact(TFCTestCase):
@ -56,14 +56,14 @@ class TestAddNewContact(TFCTestCase):
os.remove(f'v4dkh.psk - Give to hpcra')
tear_queues(self.queues)
def test_adding_new_contact_during_traffic_masking_raises_se(self) -> None:
def test_adding_new_contact_during_traffic_masking_raises_soft_error(self) -> None:
# Setup
self.settings.traffic_masking = True
# Test
self.assert_se("Error: Command is disabled during traffic masking.", add_new_contact, *self.args)
def test_contact_list_full_raises_se(self) -> None:
def test_contact_list_full_raises_soft_error(self) -> None:
# Setup
contact_list = ContactList(nicks=[str(n) for n in range(50)])
self.contact_list.contacts = contact_list.contacts
@ -96,7 +96,7 @@ class TestAddNewContact(TFCTestCase):
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', side_effect=KeyboardInterrupt)
def test_keyboard_interrupt_raises_se(self, *_: Any) -> None:
def test_keyboard_interrupt_raises_soft_error(self, *_: Any) -> None:
self.assert_se('Contact creation aborted.', add_new_contact, *self.args)
@ -147,7 +147,7 @@ class TestRemoveContact(TFCTestCase):
cleanup(self.unit_test_dir)
tear_queues(self.queues)
def test_contact_removal_during_traffic_masking_raises_se(self) -> None:
def test_contact_removal_during_traffic_masking_raises_soft_error(self) -> None:
# Setup
self.settings.traffic_masking = True
@ -155,13 +155,13 @@ class TestRemoveContact(TFCTestCase):
self.assert_se("Error: Command is disabled during traffic masking.",
remove_contact, UserInput(), None, *self.args)
def test_missing_account_raises_se(self) -> None:
def test_missing_account_raises_soft_error(self) -> None:
self.assert_se("Error: No account specified.", remove_contact, UserInput('rm '), None, *self.args)
@mock.patch('time.sleep', return_value=None)
@mock.patch('shutil.get_terminal_size', return_value=[150, 150])
@mock.patch('builtins.input', return_value='Yes')
def test_invalid_account_raises_se(self, *_: Any) -> None:
def test_invalid_account_raises_soft_error(self, *_: Any) -> None:
# Setup
user_input = UserInput(f'rm {nick_to_onion_address("Alice")[:-1]}')
window = TxWindow(window_contacts=[self.contact_list.get_contact_by_address_or_nick('Alice')],
@ -174,7 +174,7 @@ class TestRemoveContact(TFCTestCase):
@mock.patch('time.sleep', return_value=None)
@mock.patch('shutil.get_terminal_size', return_value=[150, 150])
@mock.patch('builtins.input', return_value='No')
def test_user_abort_raises_se(self, *_: Any) -> None:
def test_user_abort_raises_soft_error(self, *_: Any) -> None:
# Setup
user_input = UserInput(f'rm {nick_to_onion_address("Alice")}')
@ -264,11 +264,11 @@ class TestChangeNick(TFCTestCase):
"""Post-test actions."""
tear_queues(self.queues)
def test_missing_nick_raises_se(self) -> None:
def test_missing_nick_raises_soft_error(self) -> None:
self.assert_se("Error: No nick specified.",
change_nick, UserInput("nick "), TxWindow(type=WIN_TYPE_CONTACT), *self.args)
def test_invalid_nick_raises_se(self) -> None:
def test_invalid_nick_raises_soft_error(self) -> None:
# Setup
window = TxWindow(type=WIN_TYPE_CONTACT,
contact=create_contact('Bob'))
@ -277,7 +277,7 @@ class TestChangeNick(TFCTestCase):
self.assert_se("Error: Nick must be printable.",
change_nick, UserInput("nick Alice\x01"), window, *self.args)
def test_no_contact_raises_se(self) -> None:
def test_no_contact_raises_soft_error(self) -> None:
# Setup
window = TxWindow(type=WIN_TYPE_CONTACT,
contact=create_contact('Bob'))
@ -327,13 +327,13 @@ class TestContactSetting(TFCTestCase):
"""Post-test actions."""
tear_queues(self.queues)
def test_invalid_command_raises_se(self) -> None:
def test_invalid_command_raises_soft_error(self) -> None:
self.assert_se("Error: Invalid command.", contact_setting, UserInput('loging on'), None, *self.args)
def test_missing_parameter_raises_se(self) -> None:
def test_missing_parameter_raises_soft_error(self) -> None:
self.assert_se("Error: Invalid command.", contact_setting, UserInput(''), None, *self.args)
def test_invalid_extra_parameter_raises_se(self) -> None:
def test_invalid_extra_parameter_raises_soft_error(self) -> None:
self.assert_se("Error: Invalid command.", contact_setting, UserInput('logging on al'), None, *self.args)
def test_enable_logging_for_user(self) -> None:

View File

@ -41,10 +41,10 @@ class TestFile(TFCTestCase):
"""Post-test actions."""
cleanup(self.unit_test_dir)
def test_missing_file_raises_se(self) -> None:
def test_missing_file_raises_soft_error(self) -> None:
self.assert_se("Error: File not found.", File, './testfile.txt', *self.args)
def test_empty_file_raises_se(self) -> None:
def test_empty_file_raises_soft_error(self) -> None:
# Setup
with open('testfile.txt', 'wb+') as f:
f.write(b'')
@ -52,7 +52,7 @@ class TestFile(TFCTestCase):
# Test
self.assert_se("Error: Target file is empty.", File, './testfile.txt', *self.args)
def test_oversize_filename_raises_se(self) -> None:
def test_oversize_filename_raises_soft_error(self) -> None:
# Setup
f_name = 250 * 'a' + '.txt'
with open(f_name, 'wb+') as f:

View File

@ -33,12 +33,12 @@ from src.common.statics import (COMMAND_PACKET_QUEUE, CONFIRM_CODE_LENGTH, ECDH
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
from src.transmitter.key_exchanges import (create_pre_shared_key, export_onion_service_data, new_local_key,
rxp_load_psk, start_key_exchange, verify_fingerprints)
from tests.mock_classes import ContactList, create_contact, Gateway, OnionService, Settings, TxWindow
from tests.utils import cd_unit_test, cleanup, gen_queue_dict, ignored, nick_to_pub_key
from tests.utils import nick_to_short_address, tear_queues, TFCTestCase, VALID_ECDHE_PUB_KEY
from tests.utils import (cd_unit_test, cleanup, gen_queue_dict, ignored, nick_to_pub_key, nick_to_short_address,
tear_queues, TFCTestCase, VALID_ECDHE_PUB_KEY)
class TestOnionService(TFCTestCase):
@ -72,7 +72,7 @@ class TestLocalKey(TFCTestCase):
"""Post-test actions."""
tear_queues(self.queues)
def test_new_local_key_when_traffic_masking_is_enabled_raises_se(self) -> None:
def test_new_local_key_when_traffic_masking_is_enabled_raises_soft_error(self) -> None:
self.settings.traffic_masking = True
self.contact_list.contacts = [create_contact(LOCAL_ID)]
self.assert_se("Error: Command is disabled during traffic masking.", new_local_key, *self.args)
@ -117,7 +117,7 @@ class TestLocalKey(TFCTestCase):
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', side_effect=KeyboardInterrupt)
@mock.patch('os.getrandom', lambda x, flags: x * b'a')
def test_keyboard_interrupt_raises_se(self, *_: Any) -> None:
def test_keyboard_interrupt_raises_soft_error(self, *_: Any) -> None:
self.assert_se("Local key setup aborted.", new_local_key, *self.args)
@ -147,12 +147,12 @@ class TestKeyExchange(TFCTestCase):
@mock.patch('shutil.get_terminal_size', return_value=[200, 200])
@mock.patch('builtins.input', return_value=b58encode(bytes(TFC_PUBLIC_KEY_LENGTH), public_key=True))
def test_zero_public_key_raises_se(self, *_: Any) -> None:
def test_zero_public_key_raises_soft_error(self, *_: Any) -> None:
self.assert_se("Error: Zero public key", start_key_exchange, nick_to_pub_key("Alice"), 'Alice', *self.args)
@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_se(self, *_: Any) -> None:
def test_invalid_public_key_length_raises_soft_error(self, *_: Any) -> None:
self.assert_se("Error: Invalid public key length",
start_key_exchange, nick_to_pub_key("Alice"), 'Alice', *self.args)
@ -164,7 +164,7 @@ class TestKeyExchange(TFCTestCase):
'No']) # Fingerprint mismatch)
@mock.patch('time.sleep', return_value=None)
@mock.patch('shutil.get_terminal_size', return_value=[200, 200])
def test_fingerprint_mismatch_raises_se(self, *_: Any) -> None:
def test_fingerprint_mismatch_raises_soft_error(self, *_: Any) -> None:
self.assert_se("Error: Fingerprint mismatch", start_key_exchange, nick_to_pub_key("Alice"), 'Alice', *self.args)
@mock.patch('builtins.input', side_effect=['', # Resend public key
@ -297,7 +297,7 @@ class TestPSK(TFCTestCase):
@mock.patch('time.sleep', return_value=None)
@mock.patch('getpass.getpass', side_effect=KeyboardInterrupt)
def test_keyboard_interrupt_raises_se(self, *_: Any) -> None:
def test_keyboard_interrupt_raises_soft_error(self, *_: Any) -> None:
self.assert_se("PSK generation aborted.", create_pre_shared_key, nick_to_pub_key("Alice"), 'Alice', *self.args)
@ -320,14 +320,14 @@ class TestReceiverLoadPSK(TFCTestCase):
# Test
self.assert_se("Error: Command is disabled during traffic masking.", rxp_load_psk, None, None, *self.args)
def test_active_group_raises_se(self) -> None:
def test_active_group_raises_soft_error(self) -> None:
# Setup
window = TxWindow(type=WIN_TYPE_GROUP)
# Test
self.assert_se("Error: Group is selected.", rxp_load_psk, window, None, *self.args)
def test_ecdhe_key_raises_se(self) -> None:
def test_ecdhe_key_raises_soft_error(self) -> None:
# Setup
contact = create_contact('Alice')
contact_list = ContactList(contacts=[contact])
@ -360,7 +360,7 @@ class TestReceiverLoadPSK(TFCTestCase):
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', side_effect=KeyboardInterrupt)
def test_keyboard_interrupt_raises_se(self, *_: Any) -> None:
def test_keyboard_interrupt_raises_soft_error(self, *_: Any) -> None:
# Setup
contact = create_contact('Alice', kex_status=KEX_STATUS_NO_RX_PSK)
contact_list = ContactList(contacts=[contact])
@ -369,7 +369,7 @@ class TestReceiverLoadPSK(TFCTestCase):
contact=contact)
# Test
self.assert_se("PSK verification aborted.", rxp_load_psk, window, contact_list, *self.args)
self.assert_se("PSK install verification aborted.", rxp_load_psk, window, contact_list, *self.args)
if __name__ == '__main__':

View File

@ -35,11 +35,11 @@ from src.common.statics import (ASSEMBLY_PACKET_LENGTH, COMMAND, COMMAND_PACKET_
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
from src.transmitter.packet import (cancel_packet, queue_command, queue_file, queue_message, queue_assembly_packets,
send_file, send_packet, split_to_assembly_packets)
from tests.mock_classes import create_contact, create_group, create_keyset, Gateway, ContactList, KeyList
from tests.mock_classes import nick_to_pub_key, OnionService, Settings, TxWindow, UserInput
from tests.mock_classes import (create_contact, create_group, create_keyset, Gateway, ContactList, KeyList,
nick_to_pub_key, OnionService, Settings, TxWindow, UserInput)
from tests.utils import cd_unit_test, cleanup, gen_queue_dict, tear_queue, tear_queues, TFCTestCase
@ -106,14 +106,14 @@ class TestSendFile(TFCTestCase):
cleanup(self.unit_test_dir)
tear_queues(self.queues)
def test_traffic_masking_raises_se(self) -> None:
def test_traffic_masking_raises_soft_error(self) -> None:
self.settings.traffic_masking = True
self.assert_se("Error: Command is disabled during traffic masking.", send_file, "testfile.txt", *self.args)
def test_missing_file_raises_se(self) -> None:
def test_missing_file_raises_soft_error(self) -> None:
self.assert_se("Error: File not found.", send_file, "testfile.txt", *self.args)
def test_empty_file_raises_se(self) -> None:
def test_empty_file_raises_soft_error(self) -> None:
# Setup
open('testfile.txt', 'wb+').close()
@ -170,7 +170,7 @@ class TestQueueFile(TFCTestCase):
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', side_effect=file_list)
def test_tfc_database_raises_se(self, *_: Any) -> None:
def test_tfc_database_raises_soft_error(self, *_: Any) -> None:
window = TxWindow(name='Alice',
type=WIN_TYPE_CONTACT,
type_print='contact',
@ -253,7 +253,7 @@ class TestQueueFile(TFCTestCase):
@mock.patch('shutil.get_terminal_size', return_value=[150, 150])
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', side_effect=['./testfile.txt', KeyboardInterrupt])
def test_keyboard_interrupt_raises_se(self, *_: Any) -> None:
def test_keyboard_interrupt_raises_soft_error(self, *_: Any) -> None:
# Setup
input_data = os.urandom(2000)
with open('testfile.txt', 'wb+') as f:
@ -344,7 +344,7 @@ class TestQueueAssemblyPackets(unittest.TestCase):
log_messages=True)
self.window.window_contacts = [create_contact('Alice')]
self.args = self.settings, self.queues, self.window
def tearDown(self) -> None:
"""Post-test actions."""
tear_queues(self.queues)

View File

@ -24,9 +24,11 @@ import unittest
from unittest import mock
from typing import Any
from src.common.statics import COMMAND, FILE, MESSAGE, WIN_TYPE_CONTACT, WIN_TYPE_GROUP
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):

View File

@ -33,8 +33,8 @@ from src.transmitter.windows import select_window, TxWindow
from tests.mock_classes import ContactList, create_contact, Gateway, GroupList, OnionService, Settings, UserInput
from tests.utils import gen_queue_dict, group_name_to_group_id, nick_to_onion_address, nick_to_pub_key
from tests.utils import tear_queues, TFCTestCase, VALID_ECDHE_PUB_KEY
from tests.utils import (gen_queue_dict, group_name_to_group_id, nick_to_onion_address, nick_to_pub_key,
tear_queues, TFCTestCase, VALID_ECDHE_PUB_KEY)
class TestTxWindow(TFCTestCase):
@ -72,7 +72,7 @@ class TestTxWindow(TFCTestCase):
# Test
self.assertEqual(len(self.window), 2)
def test_group_window_change_during_traffic_masking_raises_se(self) -> None:
def test_group_window_change_during_traffic_masking_raises_soft_error(self) -> None:
# Setup
self.settings.traffic_masking = True
self.window.uid = 'test_group'
@ -81,7 +81,7 @@ class TestTxWindow(TFCTestCase):
self.assert_se("Error: Can't change window during traffic masking.",
self.window.select_tx_window, *self.args, selection='test_group_2', cmd=True)
def test_contact_window_change_during_traffic_masking_raises_se(self) -> None:
def test_contact_window_change_during_traffic_masking_raises_soft_error(self) -> None:
# Setup
self.settings.traffic_masking = True
self.window.uid = nick_to_pub_key("Alice")
@ -109,7 +109,7 @@ class TestTxWindow(TFCTestCase):
self.assertIsNone(self.window.select_tx_window(*self.args, selection='test_group', cmd=True))
self.assertEqual(self.window.uid, group_name_to_group_id('test_group'))
def test_invalid_selection_raises_se(self) -> None:
def test_invalid_selection_raises_soft_error(self) -> None:
# Setup
self.window.uid = nick_to_pub_key("Alice")
@ -275,12 +275,12 @@ class TestTxWindow(TFCTestCase):
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', side_effect=['/rm '])
def test_missing_account_when_removing_raises_se(self, *_: Any) -> None:
def test_missing_account_when_removing_raises_soft_error(self, *_: Any) -> None:
self.assert_se("Error: No account specified.", self.window.select_tx_window, *self.args)
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', side_effect=['/rm Charlie', 'yes'])
def test_unknown_account_when_removing_raises_se(self, *_: Any) -> None:
def test_unknown_account_when_removing_raises_soft_error(self, *_: Any) -> None:
self.assert_se("Error: Unknown contact 'Charlie'.", self.window.select_tx_window, *self.args)
@mock.patch('time.sleep', return_value=None)
@ -301,7 +301,7 @@ class TestTxWindow(TFCTestCase):
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', side_effect=['/help'])
def test_invalid_command_raises_se(self, *_: Any) -> None:
def test_invalid_command_raises_soft_error(self, *_: Any) -> None:
self.assert_se("Error: Invalid command.", self.window.select_tx_window, *self.args)
@ -323,7 +323,7 @@ class TestSelectWindow(TFCTestCase):
"""Post-test actions."""
tear_queues(self.queues)
def test_invalid_selection_raises_se(self) -> None:
def test_invalid_selection_raises_soft_error(self) -> None:
# Setup
self.user_input.plaintext = 'msg'
self.assert_se("Error: Invalid recipient.", select_window, *self.args)

6
tfc.py
View File

@ -95,15 +95,15 @@ def main() -> None:
ensure_dir(working_dir)
os.chdir(working_dir)
operation, local_test, data_diode_sockets = process_arguments()
operation, local_test, data_diode_sockets, qubes = process_arguments()
check_kernel_version()
print_title(operation)
master_key = MasterKey( operation, local_test)
gateway = Gateway( operation, local_test, data_diode_sockets)
settings = Settings( master_key, operation, local_test)
gateway = Gateway( operation, local_test, data_diode_sockets, qubes)
settings = Settings( master_key, operation, local_test, qubes)
contact_list = ContactList(master_key, settings)
key_list = KeyList( master_key, settings)
group_list = GroupList( master_key, settings, contact_list)

View File

@ -1,6 +1,6 @@
---
- apparmor-profiles:
- '/opt/tfc/venv_relay/bin/python3.7'
- '/usr/bin/python3.7'
users:
- 'amnesia'
commands:

View File

@ -40,9 +40,14 @@ sudo rm -f /usr/share/pixmaps/tfc.png
sudo rm -f /usr/share/applications/TFC-Dev.desktop
sudo rm -f /usr/share/applications/TFC-Local-test.desktop
sudo rm -f /usr/share/applications/TFC-RP.desktop
sudo rm -f /usr/share/applications/TFC-RP-Qubes.desktop
sudo rm -f /usr/share/applications/TFC-RP-Tails.desktop
sudo rm -f /usr/share/applications/TFC-RxP.desktop
sudo rm -f /usr/share/applications/TFC-RxP-Qubes.desktop
sudo rm -f /usr/share/applications/TFC-TxP.desktop
sudo rm -f /usr/share/applications/TFC-TxP-Qubes.desktop
sudo rm -f /usr/bin/tfc-transmitter
sudo rm -f /usr/bin/tfc-receiver
sudo rm -f /usr/bin/tfc-relay
sudo rm -rf /opt/tfc/
yn_prompt "Remove user data?" "rm -rf $HOME/tfc/"