tfc-mirror/tests/transmitter/test_commands.py

1171 lines
55 KiB
Python

#!/usr/bin/env python3.7
# -*- coding: utf-8 -*-
"""
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/>.
"""
import os
import time
import unittest
from multiprocessing import Process
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.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,
KEX_STATUS_VERIFIED, KEY_MANAGEMENT_QUEUE, LOGFILE_MASKING_QUEUE, MESSAGE,
MESSAGE_PACKET_QUEUE, M_S_HEADER, NORMAL_TEXT, PADDING_LENGTH,
PRIVATE_MESSAGE_HEADER, RELAY_PACKET_QUEUE, RESET, SENDER_MODE_QUEUE,
TM_COMMAND_PACKET_QUEUE, TRAFFIC_MASKING_QUEUE, TX, UNENCRYPTED_DATAGRAM_HEADER,
UNENCRYPTED_WIPE_COMMAND, VERSION, WIN_TYPE_CONTACT, WIN_TYPE_GROUP,
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,
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,
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):
def setUp(self) -> None:
"""Pre-test actions."""
self.window = TxWindow()
self.contact_list = ContactList()
self.group_list = GroupList()
self.settings = Settings()
self.queues = gen_queue_dict()
self.master_key = MasterKey()
self.onion_service = OnionService()
self.gateway = Gateway()
self.args = (self.window, self.contact_list, self.group_list, self.settings,
self.queues, self.master_key, self.onion_service, self.gateway)
def tearDown(self) -> None:
"""Post-test actions."""
tear_queues(self.queues)
def test_valid_command(self) -> None:
self.assertIsNone(process_command(UserInput('about'), *self.args))
def test_invalid_command(self) -> None:
self.assert_se("Error: Invalid command 'abou'.", process_command, UserInput('abou'), *self.args)
def test_empty_command(self) -> None:
self.assert_se("Error: Invalid command.", process_command, UserInput(' '), *self.args)
class TestPrintAbout(TFCTestCase):
def test_print_about(self) -> None:
self.assert_prints(CLEAR_ENTIRE_SCREEN + CURSOR_LEFT_UP_CORNER + f"""\
Tinfoil Chat {VERSION}
Website: https://github.com/maqp/tfc/
Wikipage: https://github.com/maqp/tfc/wiki
""", print_about)
class TestClearScreens(unittest.TestCase):
def setUp(self) -> None:
"""Pre-test actions."""
self.window = TxWindow(uid=nick_to_pub_key('Alice'))
self.settings = Settings()
self.queues = gen_queue_dict()
self.args = self.window, self.settings, self.queues
def tearDown(self) -> None:
"""Post-test actions."""
tear_queues(self.queues)
@mock.patch('os.system', return_value=None)
def test_clear_screens(self, _) -> None:
self.assertIsNone(clear_screens(UserInput('clear'), *self.args))
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 1)
self.assertEqual(self.queues[RELAY_PACKET_QUEUE].qsize(), 1)
@mock.patch('os.system', return_value=None)
def test_no_relay_clear_cmd_when_traffic_masking_is_enabled(self, _) -> None:
# Setup
self.settings.traffic_masking = True
# Test
self.assertIsNone(clear_screens(UserInput('clear'), *self.args))
self.assertEqual(self.queues[TM_COMMAND_PACKET_QUEUE].qsize(), 1)
self.assertEqual(self.queues[RELAY_PACKET_QUEUE].qsize(), 0)
@mock.patch('os.system', return_value=None)
def test_reset_screens(self, mock_os_system) -> None:
self.assertIsNone(clear_screens(UserInput('reset'), *self.args))
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 1)
self.assertEqual(self.queues[RELAY_PACKET_QUEUE].qsize(), 1)
mock_os_system.assert_called_with(RESET)
@mock.patch('os.system', return_value=None)
def test_no_relay_reset_cmd_when_traffic_masking_is_enabled(self, mock_os_system: MagicMock) -> None:
# Setup
self.settings.traffic_masking = True
# Test
self.assertIsNone(clear_screens(UserInput('reset'), *self.args))
self.assertEqual(self.queues[TM_COMMAND_PACKET_QUEUE].qsize(), 1)
self.assertEqual(self.queues[RELAY_PACKET_QUEUE].qsize(), 0)
mock_os_system.assert_called_with(RESET)
class TestRXPShowSysWin(unittest.TestCase):
def setUp(self) -> None:
"""Pre-test actions."""
self.window = TxWindow(name='Alice', uid=nick_to_pub_key('Alice'))
self.settings = Settings()
self.queues = gen_queue_dict()
self.args = self.window, self.settings, self.queues
def tearDown(self) -> None:
"""Post-test actions."""
tear_queues(self.queues)
@mock.patch('builtins.input', side_effect=['', EOFError, KeyboardInterrupt])
def test_cmd_window(self, _: Any) -> None:
self.assertIsNone(rxp_show_sys_win(UserInput(plaintext='cmd'), *self.args))
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 2)
self.assertIsNone(rxp_show_sys_win(UserInput(plaintext='cmd'), *self.args))
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 4)
self.assertIsNone(rxp_show_sys_win(UserInput(plaintext='cmd'), *self.args))
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 6)
@mock.patch('builtins.input', side_effect=['', EOFError, KeyboardInterrupt])
def test_file_window(self, _: Any) -> None:
self.assertIsNone(rxp_show_sys_win(UserInput(plaintext='fw'), *self.args))
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 2)
self.assertIsNone(rxp_show_sys_win(UserInput(plaintext='fw'), *self.args))
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 4)
self.assertIsNone(rxp_show_sys_win(UserInput(plaintext='fw'), *self.args))
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 6)
class TestExitTFC(unittest.TestCase):
def setUp(self) -> None:
"""Pre-test actions."""
self.settings = Settings(local_testing_mode=True)
self.queues = gen_queue_dict()
self.gateway = Gateway(data_diode_sockets=True)
self.args = self.settings, self.queues, self.gateway
def tearDown(self) -> None:
"""Post-test actions."""
tear_queues(self.queues)
@mock.patch('time.sleep', return_value=None)
def test_exit_tfc_local_test(self, _: Any) -> None:
# Setup
for _ in range(2):
self.queues[COMMAND_PACKET_QUEUE].put("dummy command")
# Test
self.assertIsNone(exit_tfc(*self.args))
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 1)
self.assertEqual(self.queues[RELAY_PACKET_QUEUE].qsize(), 1)
@mock.patch('time.sleep', return_value=None)
def test_exit_tfc(self, _: Any) -> None:
# Setup
self.settings.local_testing_mode = False
for _ in range(2):
self.queues[COMMAND_PACKET_QUEUE].put("dummy command")
# Test
self.assertIsNone(exit_tfc(*self.args))
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 1)
self.assertEqual(self.queues[RELAY_PACKET_QUEUE].qsize(), 1)
class TestLogCommand(TFCTestCase):
@mock.patch("getpass.getpass", return_value='test_password')
def setUp(self, _: Any) -> None:
"""Pre-test actions."""
self.unit_test_dir = cd_unit_test()
self.window = TxWindow(name='Alice', uid=nick_to_pub_key('Alice'))
self.contact_list = ContactList()
self.group_list = GroupList()
self.settings = Settings()
self.queues = gen_queue_dict()
self.master_key = MasterKey()
self.args = (self.window, self.contact_list, self.group_list,
self.settings, self.queues, self.master_key)
self.log_file = f'{DIR_USER_DATA}{self.settings.software_operation}_logs'
self.tfc_log_database = MessageLog(self.log_file, self.master_key.master_key)
def tearDown(self) -> None:
"""Post-test actions."""
cleanup(self.unit_test_dir)
tear_queues(self.queues)
def test_invalid_export(self) -> None:
self.assert_se("Error: Invalid number of messages.",
log_command, UserInput("history a"), *self.args)
@mock.patch("getpass.getpass", return_value='test_password')
def test_log_printing(self, _: Any) -> None:
# Setup
os.remove(self.log_file)
# Test
self.assert_se(f"No log database available.",
log_command, UserInput("history 4"), *self.args)
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 1)
def test_log_printing_when_no_password_is_asked(self) -> None:
# Setup
self.settings.ask_password_for_log_access = False
os.remove(self.log_file)
# Test
self.assert_se(f"No log database available.",
log_command, UserInput("history 4"), *self.args)
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 1)
@mock.patch("getpass.getpass", return_value='test_password')
def test_log_printing_all(self, _: Any) -> None:
# Setup
os.remove(self.log_file)
# Test
self.assert_se(f"No log database available.",
log_command, UserInput("history"), *self.args)
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 1)
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_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_soft_error(self, *_: Any) -> None:
self.assert_se("Log file export aborted.",
log_command, UserInput('export'), *self.args)
@mock.patch('src.common.db_masterkey.MIN_KEY_DERIVATION_TIME', 0.1)
@mock.patch('src.common.db_masterkey.MAX_KEY_DERIVATION_TIME', 1.0)
@mock.patch('os.popen', return_value=MagicMock(
read=MagicMock(return_value=MagicMock(splitlines=MagicMock(return_value=["MemAvailable 10240"])))))
@mock.patch("multiprocessing.cpu_count", return_value=1)
@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_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)
@mock.patch('src.common.db_masterkey.MIN_KEY_DERIVATION_TIME', 0.1)
@mock.patch('src.common.db_masterkey.MAX_KEY_DERIVATION_TIME', 1.0)
@mock.patch('os.popen', return_value=MagicMock(
read=MagicMock(return_value=MagicMock(splitlines=MagicMock(return_value=["MemAvailable 10240"])))))
@mock.patch("multiprocessing.cpu_count", return_value=1)
@mock.patch("getpass.getpass", side_effect=3*['test_password'] + ['invalid_password'] + ['test_password'])
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', return_value='Yes')
def test_successful_export_command(self, *_: Any) -> None:
# Setup
self.master_key = OrigMasterKey(operation=TX, local_test=True)
self.window.type = WIN_TYPE_CONTACT
self.window.uid = nick_to_pub_key('Alice')
whisper_header = bool_to_bytes(False)
packet = split_to_assembly_packets(whisper_header + PRIVATE_MESSAGE_HEADER + b'test', MESSAGE)[0]
self.tfc_log_database.database_key = self.master_key.master_key
write_log_entry(packet, nick_to_pub_key('Alice'), self.tfc_log_database)
# Test
for command in ['export', 'export 1']:
self.assert_se(f"Exported log file of contact 'Alice'.",
log_command, UserInput(command), self.window, ContactList(nicks=['Alice']),
self.group_list, self.settings, self.queues, self.master_key)
class TestSendOnionServiceKey(TFCTestCase):
confirmation_code = b'a'
def setUp(self) -> None:
"""Pre-test actions."""
self.contact_list = ContactList()
self.settings = Settings()
self.onion_service = OnionService()
self.gateway = Gateway()
self.args = self.contact_list, self.settings, self.onion_service, self.gateway
@mock.patch('time.sleep', return_value=None)
@mock.patch('os.urandom', return_value=confirmation_code)
@mock.patch('builtins.input', side_effect=['Yes', confirmation_code.hex()])
def test_onion_service_key_delivery_traffic_masking(self, *_: Any) -> None:
self.assertIsNone(send_onion_service_key(*self.args))
self.assertEqual(len(self.gateway.packets), 1)
@mock.patch('time.sleep', return_value=None)
@mock.patch('os.urandom', return_value=confirmation_code)
@mock.patch('builtins.input', side_effect=[KeyboardInterrupt, 'No'])
def test_onion_service_key_delivery_traffic_masking_abort(self, *_: Any) -> None:
# Setup
self.settings.traffic_masking = True
# Test
for _ in range(2):
self.assert_se("Onion Service data export canceled.", send_onion_service_key, *self.args)
@mock.patch('os.urandom', return_value=confirmation_code)
@mock.patch('builtins.input', return_value=confirmation_code.hex())
def test_onion_service_key_delivery(self, *_: Any) -> None:
self.assertIsNone(send_onion_service_key(*self.args))
self.assertEqual(len(self.gateway.packets), 1)
@mock.patch('time.sleep', return_value=None)
@mock.patch('os.urandom', return_value=confirmation_code)
@mock.patch('builtins.input', side_effect=[EOFError, KeyboardInterrupt])
def test_onion_service_key_delivery_cancel(self, *_: Any) -> None:
for _ in range(2):
self.assert_se("Onion Service data export canceled.", send_onion_service_key, *self.args)
class TestPrintHelp(TFCTestCase):
def setUp(self) -> None:
"""Pre-test actions."""
self.settings = Settings()
self.settings.traffic_masking = False
@mock.patch('shutil.get_terminal_size', return_value=[60, 60])
def test_print_normal(self, _: Any) -> None:
self.assert_prints(CLEAR_ENTIRE_SCREEN + CURSOR_LEFT_UP_CORNER + """\
List of commands:
/about Show links to project resources
/add Add new contact
/cm Cancel message transmission to
active contact/group
/clear, ' ' Clear TFC screens
/cmd, '//' Display command window on Receiver
/connect Resend Onion Service data to Relay
/exit Exit TFC on all three computers
/export (n) Export (n) messages from
recipient's log file
/file Send file to active contact/group
/help Display this list of commands
/history (n) Print (n) messages from
recipient's log file
/localkey Generate new local key pair
/logging {on,off}(' all') Change message log setting (for
all contacts)
/msg {A,N,G} Change recipient to Account, Nick,
or Group
/names List contacts and groups
/nick N Change nickname of active
recipient/group to N
/notify {on,off} (' all') Change notification settings (for
all contacts)
/passwd {tx,rx} Change master password on target
system
/psk Open PSK import dialog on Receiver
/reset Reset ephemeral session log for
active window
/rm {A,N} Remove contact specified by
account A or nick N
/rmlogs {A,N} Remove log entries for account A
or nick N
/set S V Change setting S to value V
/settings List setting names, values and
descriptions
/store {on,off} (' all') Change file reception (for all
contacts)
/unread, ' ' List windows with unread messages
on Receiver
/verify Verify fingerprints with active
contact
/whisper M Send message M, asking it not to
be logged
/whois {A,N} Check which A corresponds to N or
vice versa
/wipe Wipe all TFC user data and power
off systems
Shift + PgUp/PgDn Scroll terminal up/down
────────────────────────────────────────────────────────────
Group management:
/group create G A₁..Aₙ Create group G and add accounts
A₁..Aₙ
/group join ID G A₁..Aₙ Join group ID, call it G and add
accounts A₁..Aₙ
/group add G A₁..Aₙ Add accounts A₁..Aₙ to group G
/group rm G A₁..Aₙ Remove accounts A₁..Aₙ from group G
/group rm G Remove group G
────────────────────────────────────────────────────────────
""", print_help, self.settings)
@mock.patch('shutil.get_terminal_size', return_value=[80, 80])
def test_print_during_traffic_masking(self, _: Any) -> None:
self.settings.traffic_masking = True
self.assert_prints(CLEAR_ENTIRE_SCREEN + CURSOR_LEFT_UP_CORNER + """\
List of commands:
/about Show links to project resources
/cf Cancel file transmission to active contact/group
/cm Cancel message transmission to active contact/group
/clear, ' ' Clear TFC screens
/cmd, '//' Display command window on Receiver
/connect Resend Onion Service data to Relay
/exit Exit TFC on all three computers
/export (n) Export (n) messages from recipient's log file
/file Send file to active contact/group
/fw Display file reception window on Receiver
/help Display this list of commands
/history (n) Print (n) messages from recipient's log file
/logging {on,off}(' all') Change message log setting (for all contacts)
/names List contacts and groups
/nick N Change nickname of active recipient/group to N
/notify {on,off} (' all') Change notification settings (for all contacts)
/reset Reset ephemeral session log for active window
/rmlogs {A,N} Remove log entries for account A or nick N
/set S V Change setting S to value V
/settings List setting names, values and descriptions
/store {on,off} (' all') Change file reception (for all contacts)
/unread, ' ' List windows with unread messages on Receiver
/verify Verify fingerprints with active contact
/whisper M Send message M, asking it not to be logged
/whois {A,N} Check which A corresponds to N or vice versa
/wipe Wipe all TFC user data and power off systems
Shift + PgUp/PgDn Scroll terminal up/down
────────────────────────────────────────────────────────────────────────────────
""", print_help, self.settings)
class TestPrintRecipients(TFCTestCase):
def setUp(self) -> None:
"""Pre-test actions."""
self.contact_list = ContactList(nicks=['Alice', 'Bob'])
self.group_list = GroupList(groups=['test_group', 'test_group_2'])
self.args = self.contact_list, self.group_list
def test_printing(self) -> None:
self.assertIsNone(print_recipients(*self.args))
class TestChangeMasterKey(TFCTestCase):
def setUp(self) -> None:
"""Pre-test actions."""
self.unit_test_dir = cd_unit_test()
self.contact_list = ContactList()
self.group_list = GroupList()
self.settings = Settings()
self.queues = gen_queue_dict()
self.master_key = MasterKey()
self.file_name = f'{DIR_USER_DATA}/unittest'
self.log_file = f'{DIR_USER_DATA}{self.settings.software_operation}_logs'
self.tfc_log_database = MessageLog(self.log_file, self.master_key.master_key)
self.onion_service = OnionService(master_key=self.master_key,
file_name=self.file_name,
database=TFCDatabase(self.file_name, self.master_key))
self.args = (self.contact_list, self.group_list, self.settings,
self.queues, self.master_key, self.onion_service)
def tearDown(self) -> None:
"""Post-test actions."""
cleanup(self.unit_test_dir)
tear_queues(self.queues)
def test_raises_fr_during_traffic_masking(self) -> None:
# Setup
self.settings.traffic_masking = True
# Test
self.assert_se("Error: Command is disabled during traffic masking.",
change_master_key, UserInput(), *self.args)
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_soft_error(self, _: Any) -> None:
self.assert_se("Error: Invalid target system 't'.",
change_master_key, UserInput("passwd t"), *self.args)
@mock.patch('src.common.db_keys.KeyList', return_value=MagicMock())
@mock.patch('os.popen', return_value=MagicMock(read=MagicMock(return_value='foo\nMemAvailable 200')))
@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_soft_error(self, *_: Any) -> None:
# Setup
def mock_sender_loop() -> None:
"""Mock sender loop key management functionality."""
while self.queues[KEY_MANAGEMENT_QUEUE].empty():
time.sleep(0.1)
if self.queues[KEY_MANAGEMENT_QUEUE].get()[0] == KDB_M_KEY_CHANGE_HALT_HEADER:
self.queues[KEY_MGMT_ACK_QUEUE].put('WRONG_HEADER')
p = Process(target=mock_sender_loop, args=())
p.start()
# Test
self.assert_se("Error: Key database returned wrong signal.",
change_master_key, UserInput("passwd tx"), *self.args)
# Teardown
p.terminate()
@mock.patch('src.common.db_keys.KeyList', return_value=MagicMock())
@mock.patch('os.popen', return_value=MagicMock(read=MagicMock(return_value='foo\nMemAvailable 200')))
@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_critical_error_if_key_database_returns_invalid_master_key(self, *_: Any) -> None:
# Setup
def mock_sender_loop() -> None:
"""Mock sender loop key management functionality."""
while self.queues[KEY_MANAGEMENT_QUEUE].empty():
time.sleep(0.1)
if self.queues[KEY_MANAGEMENT_QUEUE].get()[0] == KDB_M_KEY_CHANGE_HALT_HEADER:
self.queues[KEY_MGMT_ACK_QUEUE].put(KDB_HALT_ACK_HEADER)
while self.queues[KEY_MANAGEMENT_QUEUE].empty():
time.sleep(0.1)
_ = self.queues[KEY_MANAGEMENT_QUEUE].get()
self.queues[KEY_MGMT_ACK_QUEUE].put(b'invalid_master_key')
p = Process(target=mock_sender_loop, args=())
p.start()
self.contact_list.file_name = f'{DIR_USER_DATA}{TX}_contacts'
self.group_list.file_name = f'{DIR_USER_DATA}{TX}_groups'
self.settings.file_name = f'{DIR_USER_DATA}{TX}_settings'
self.onion_service.file_name = f'{DIR_USER_DATA}{TX}_onion_db'
self.contact_list.database = TFCDatabase(self.contact_list.file_name, self.contact_list.master_key)
self.group_list.database = TFCDatabase(self.group_list.file_name, self.group_list.master_key)
self.settings.database = TFCDatabase(self.settings.file_name, self.settings.master_key)
self.onion_service.database = TFCDatabase(self.onion_service.file_name, self.onion_service.master_key)
orig_cl_rd = self.contact_list.database.replace_database
orig_gl_rd = self.group_list.database.replace_database
orig_st_rd = self.settings.database.replace_database
orig_os_rd = self.onion_service.database.replace_database
self.contact_list.database.replace_database = lambda: None
self.group_list.database.replace_database = lambda: None
self.settings.database.replace_database = lambda: None
self.onion_service.database.replace_database = lambda: None
write_log_entry(M_S_HEADER + PADDING_LENGTH * b'a', nick_to_pub_key('Alice'), self.tfc_log_database)
# Test
with self.assertRaises(SystemExit):
self.assertIsNone(change_master_key(UserInput("passwd tx"), *self.args))
# Teardown
p.terminate()
self.contact_list.database.replace_database = orig_cl_rd
self.group_list.database.replace_database = orig_gl_rd
self.settings.database.replace_database = orig_st_rd
self.onion_service.database.replace_database = orig_os_rd
@mock.patch('src.common.db_keys.KeyList', return_value=MagicMock())
@mock.patch('os.popen', return_value=MagicMock(read=MagicMock(return_value='foo\nMemAvailable 200')))
@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(self, *_: Any) -> None:
# Setup
def mock_sender_loop() -> None:
"""Mock sender loop key management functionality."""
while self.queues[KEY_MANAGEMENT_QUEUE].empty():
time.sleep(0.1)
if self.queues[KEY_MANAGEMENT_QUEUE].get()[0] == KDB_M_KEY_CHANGE_HALT_HEADER:
self.queues[KEY_MGMT_ACK_QUEUE].put(KDB_HALT_ACK_HEADER)
while self.queues[KEY_MANAGEMENT_QUEUE].empty():
time.sleep(0.1)
master_key = self.queues[KEY_MANAGEMENT_QUEUE].get()
self.queues[KEY_MGMT_ACK_QUEUE].put(master_key)
p = Process(target=mock_sender_loop, args=())
p.start()
self.contact_list.file_name = f'{DIR_USER_DATA}{TX}_contacts'
self.group_list.file_name = f'{DIR_USER_DATA}{TX}_groups'
self.settings.file_name = f'{DIR_USER_DATA}{TX}_settings'
self.onion_service.file_name = f'{DIR_USER_DATA}{TX}_onion_db'
self.contact_list.database = TFCDatabase(self.contact_list.file_name, self.contact_list.master_key)
self.group_list.database = TFCDatabase(self.group_list.file_name, self.group_list.master_key)
self.settings.database = TFCDatabase(self.settings.file_name, self.settings.master_key)
self.onion_service.database = TFCDatabase(self.onion_service.file_name, self.onion_service.master_key)
orig_cl_rd = self.contact_list.database.replace_database
orig_gl_rd = self.group_list.database.replace_database
orig_st_rd = self.settings.database.replace_database
orig_os_rd = self.onion_service.database.replace_database
self.contact_list.database.replace_database = lambda: None
self.group_list.database.replace_database = lambda: None
self.settings.database.replace_database = lambda: None
self.onion_service.database.replace_database = lambda: None
write_log_entry(M_S_HEADER + PADDING_LENGTH * b'a', nick_to_pub_key('Alice'), self.tfc_log_database)
# Test
self.assertIsNone(change_master_key(UserInput("passwd tx"), *self.args))
p.terminate()
# Teardown
self.contact_list.database.replace_database = orig_cl_rd
self.group_list.database.replace_database = orig_gl_rd
self.settings.database.replace_database = orig_st_rd
self.onion_service.database.replace_database = orig_os_rd
def test_receiver_command(self) -> None:
self.assertIsNone(change_master_key(UserInput("passwd rx"), *self.args))
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 1)
self.assertEqual(self.queues[KEY_MANAGEMENT_QUEUE].qsize(), 0)
@mock.patch('time.sleep', return_value=None)
@mock.patch('getpass.getpass', side_effect=KeyboardInterrupt)
def test_keyboard_interrupt_raises_soft_error(self, *_: Any) -> None:
self.assert_se("Authentication aborted.", change_master_key, UserInput("passwd tx"), *self.args)
class TestRemoveLog(TFCTestCase):
def setUp(self) -> None:
"""Pre-test actions."""
self.unit_test_dir = cd_unit_test()
self.contact_list = ContactList(nicks=['Alice'])
self.group_list = GroupList(groups=['test_group'])
self.settings = Settings()
self.queues = gen_queue_dict()
self.master_key = MasterKey()
self.file_name = f'{DIR_USER_DATA}{self.settings.software_operation}_logs'
self.args = self.contact_list, self.group_list, self.settings, self.queues, self.master_key
self.log_file = f'{DIR_USER_DATA}{self.settings.software_operation}_logs'
self.tfc_log_database = MessageLog(self.log_file, self.master_key.master_key)
def tearDown(self) -> None:
"""Post-test actions."""
tear_queues(self.queues)
cleanup(self.unit_test_dir)
def test_missing_contact_raises_soft_error(self) -> None:
self.assert_se("Error: No contact/group specified.",
remove_log, UserInput(''), *self.args)
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', return_value='No')
def test_no_aborts_removal(self, *_: Any) -> None:
# Setup
self.assertIsNone(write_log_entry(M_S_HEADER + PADDING_LENGTH * b'a', nick_to_pub_key('Alice'),
self.tfc_log_database))
# Test
self.assert_se("Log file removal aborted.",
remove_log, UserInput('/rmlogs Alice'), *self.args)
@mock.patch('shutil.get_terminal_size', return_value=[150, 150])
@mock.patch('builtins.input', return_value='Yes')
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_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)
@mock.patch('builtins.input', return_value='Yes')
def test_log_remove_with_nick(self, _: Any) -> None:
# Setup
write_log_entry(M_S_HEADER + PADDING_LENGTH * b'a', nick_to_pub_key("Alice"), self.tfc_log_database)
# Test
self.assert_se("Removed log entries for contact 'Alice'.",
remove_log, UserInput('/rmlogs Alice'), *self.args)
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 1)
@mock.patch('shutil.get_terminal_size', return_value=[150, 150])
@mock.patch('builtins.input', return_value='Yes')
def test_log_remove_with_onion_address(self, *_: Any) -> None:
# Setup
write_log_entry(M_S_HEADER + PADDING_LENGTH * b'a', nick_to_pub_key("Alice"), self.tfc_log_database)
# Test
self.assert_se("Removed log entries for contact 'Alice'.",
remove_log, UserInput(f'/rmlogs {nick_to_onion_address("Alice")}'), *self.args)
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 1)
@mock.patch('shutil.get_terminal_size', return_value=[150, 150])
@mock.patch('builtins.input', return_value='Yes')
def test_log_remove_with_unknown_onion_address(self, *_: Any) -> None:
# Setup
write_log_entry(M_S_HEADER + PADDING_LENGTH * b'a', nick_to_pub_key("Alice"), self.tfc_log_database)
# Test
self.assert_se("Found no log entries for contact 'w5sm3'.",
remove_log, UserInput(f'/rmlogs {nick_to_onion_address("Unknown")}'), *self.args)
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 1)
@mock.patch('builtins.input', return_value='Yes')
def test_log_remove_with_group_name(self, _: Any) -> None:
# Setup
for p in assembly_packet_creator(MESSAGE, 'This is a short group message',
group_id=group_name_to_group_id('test_group')):
write_log_entry(p, nick_to_pub_key('Alice'), self.tfc_log_database)
# Test
self.assert_se("Removed log entries for group 'test_group'.",
remove_log, UserInput(f'/rmlogs test_group'), *self.args)
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 1)
@mock.patch('builtins.input', return_value='Yes')
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)
# Test
self.assert_se("Error: Unknown selector.", remove_log, UserInput(f'/rmlogs unknown'), *self.args)
class TestChangeSetting(TFCTestCase):
def setUp(self) -> None:
"""Pre-test actions."""
self.window = TxWindow()
self.contact_list = ContactList()
self.group_list = GroupList()
self.settings = Settings()
self.queues = gen_queue_dict()
self.master_key = MasterKey()
self.gateway = Gateway()
self.args = (self.window, self.contact_list, self.group_list,
self.settings, self.queues, self.master_key, self.gateway)
def tearDown(self) -> None:
"""Post-test actions."""
tear_queues(self.queues)
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_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_soft_error(self) -> None:
self.assert_se("Error: No value for setting specified.",
change_setting, UserInput("set serial_error_correction"), *self.args)
def test_serial_settings_raise_se(self) -> None:
self.assert_se("Error: Serial interface setting can only be changed manually.",
change_setting, UserInput("set use_serial_usb_adapter True"), *self.args)
self.assert_se("Error: Serial interface setting can only be changed manually.",
change_setting, UserInput("set built_in_serial_interface Truej"), *self.args)
@mock.patch('time.sleep', return_value=None)
@mock.patch('getpass.getpass', side_effect=[KeyboardInterrupt])
def test_changing_ask_password_for_log_access_asks_for_password(self, *_: Any) -> None:
self.assert_se("Authentication aborted.",
change_setting, UserInput("set ask_password_for_log_access False"), *self.args)
@mock.patch('time.sleep', return_value=None)
@mock.patch('getpass.getpass', return_value='invalid_password')
def test_invalid_password_raises_function_return(self, *_: Any) -> None:
self.assert_se("Error: No permission to change setting.",
change_setting, UserInput("set ask_password_for_log_access False"), *self.args)
def test_relay_commands_raise_fr_when_traffic_masking_is_enabled(self) -> None:
# Setup
self.settings.traffic_masking = True
# Test
key_list = ['serial_error_correction', 'serial_baudrate', 'allow_contact_requests']
for key, value in zip(key_list, ['5', '5', 'True']):
self.assert_se("Error: Can't change this setting during traffic masking.",
change_setting, UserInput(f"set {key} {value}"), *self.args)
def test_individual_settings(self) -> None:
self.assertIsNone(change_setting(UserInput("set serial_error_correction 5"), *self.args))
self.assertEqual(self.queues[RELAY_PACKET_QUEUE].qsize(), 1)
self.assertIsNone(change_setting(UserInput("set serial_baudrate 9600"), *self.args))
self.assertEqual(self.queues[RELAY_PACKET_QUEUE].qsize(), 2)
self.assertIsNone(change_setting(UserInput("set allow_contact_requests True"), *self.args))
self.assertEqual(self.queues[RELAY_PACKET_QUEUE].qsize(), 3)
self.assertIsNone(change_setting(UserInput("set traffic_masking True"), *self.args))
self.assertIsInstance(self.queues[SENDER_MODE_QUEUE].get(), Settings)
self.assertTrue(self.queues[TRAFFIC_MASKING_QUEUE].get())
self.settings.traffic_masking = False
self.assertIsNone(change_setting(UserInput("set max_number_of_group_members 100"), *self.args))
self.assertTrue(self.group_list.store_groups_called)
self.group_list.store_groups_called = False
self.assertIsNone(change_setting(UserInput("set max_number_of_groups 100"), *self.args))
self.assertTrue(self.group_list.store_groups_called)
self.group_list.store_groups_called = False
self.assertIsNone(change_setting(UserInput("set max_number_of_contacts 100"), *self.args))
self.assertEqual(self.queues[KEY_MANAGEMENT_QUEUE].qsize(), 1)
self.assertIsNone(change_setting(UserInput("set log_file_masking True"), *self.args))
self.assertTrue(self.queues[LOGFILE_MASKING_QUEUE].get())
class TestPrintSettings(TFCTestCase):
def test_print_settings(self) -> None:
self.assert_prints(f"""\
{CLEAR_ENTIRE_SCREEN}{CURSOR_LEFT_UP_CORNER}
Setting name Current value Default value Description
────────────────────────────────────────────────────────────────────────────────
disable_gui_dialog False False True replaces
GUI dialogs with
CLI prompts
max_number_of_group_members 50 50 Maximum number
of members in a
group
max_number_of_groups 50 50 Maximum number
of groups
max_number_of_contacts 50 50 Maximum number
of contacts
log_messages_by_default False False Default logging
setting for new
contacts/groups
accept_files_by_default False False Default file
reception
setting for new
contacts
show_notifications_by_default True True Default message
notification
setting for new
contacts/groups
log_file_masking False False True hides real
size of log file
during traffic
masking
ask_password_for_log_access True True False disables
password prompt
when viewing/exp
orting logs
nc_bypass_messages False False False removes
Networked
Computer bypass
interrupt
messages
confirm_sent_files True True False sends
files without
asking for
confirmation
double_space_exits False False True exits,
False clears
screen with
double space
command
traffic_masking False False True enables
traffic masking
to hide metadata
tm_static_delay 2.0 2.0 The static delay
between traffic
masking packets
tm_random_delay 2.0 2.0 Max random delay
for traffic
masking timing
obfuscation
allow_contact_requests True True When False, does
not show TFC
contact requests
new_message_notify_preview False False When True, shows
a preview of the
received message
new_message_notify_duration 1.0 1.0 Number of
seconds new
message
notification
appears
max_decompress_size 100000000 100000000 Max size
Receiver accepts
when
decompressing
file
Serial interface setting Current value Default value Description
────────────────────────────────────────────────────────────────────────────────
serial_baudrate 19200 19200 The speed of
serial interface
in bauds per
second
serial_error_correction 5 5 Number of byte
errors serial
datagrams can
recover from
""", print_settings, Settings(), Gateway())
class TestRxPDisplayUnread(unittest.TestCase):
def setUp(self) -> None:
"""Pre-test actions."""
self.queues = gen_queue_dict()
def tearDown(self) -> None:
"""Post-test actions."""
tear_queues(self.queues)
def test_command(self) -> None:
self.assertIsNone(rxp_display_unread(Settings(), self.queues))
self.assertEqual(self.queues[COMMAND_PACKET_QUEUE].qsize(), 1)
class TestVerify(TFCTestCase):
def setUp(self) -> None:
"""Pre-test actions."""
self.window = TxWindow(uid=nick_to_pub_key("Alice"),
name='Alice',
window_contacts=[create_contact('test_group')],
log_messages=True,
type=WIN_TYPE_CONTACT)
self.contact_list = ContactList(nicks=['Alice'])
self.contact = self.contact_list.get_contact_by_address_or_nick('Alice')
self.window.contact = self.contact
self.args = self.window, self.contact_list
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_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)
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', side_effect=['No', 'Yes'])
def test_fingerprint_check(self, *_: Any) -> None:
self.contact.kex_status = KEX_STATUS_VERIFIED
self.assertIsNone(verify(*self.args))
self.assertEqual(self.contact.kex_status, KEX_STATUS_UNVERIFIED)
self.assertIsNone(verify(*self.args))
self.assertEqual(self.contact.kex_status, KEX_STATUS_VERIFIED)
@mock.patch('time.sleep', return_value=None)
@mock.patch('builtins.input', side_effect=KeyboardInterrupt)
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)
class TestWhisper(TFCTestCase):
def setUp(self) -> None:
"""Pre-test actions."""
self.window = TxWindow(uid=nick_to_pub_key("Alice"),
name='Alice',
window_contacts=[create_contact('Alice')],
log_messages=True)
self.settings = Settings()
self.queues = gen_queue_dict()
self.args = self.window, self.settings, self.queues
def test_empty_input_raises_soft_error(self) -> None:
self.assert_se("Error: No whisper message specified.",
whisper, UserInput("whisper"), *self.args)
def test_whisper(self) -> None:
self.assertIsNone(whisper(UserInput("whisper This message ought not to be logged."), *self.args))
message, pub_key, logging, log_as_ph, win_uid = self.queues[MESSAGE_PACKET_QUEUE].get()
self.assertEqual(pub_key, nick_to_pub_key("Alice"))
self.assertTrue(logging)
self.assertTrue(log_as_ph)
class TestWhois(TFCTestCase):
def setUp(self) -> None:
"""Pre-test actions."""
self.contact_list = ContactList(nicks=['Alice'])
self.group_list = GroupList(groups=['test_group'])
self.args = self.contact_list, self.group_list
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_soft_error(self) -> None:
self.assert_se("Error: Unknown selector.", whois, UserInput("whois alice"), *self.args)
def test_nick_from_account(self) -> None:
self.assert_prints(
f"""\
{BOLD_ON} Nick of 'hpcrayuxhrcy2wtpfwgwjibderrvjll6azfr4tqat3eka2m2gbb55bid' is {NORMAL_TEXT}
{BOLD_ON} Alice {NORMAL_TEXT}\n""",
whois, UserInput("whois hpcrayuxhrcy2wtpfwgwjibderrvjll6azfr4tqat3eka2m2gbb55bid"), *self.args)
def test_account_from_nick(self) -> None:
self.assert_prints(
f"""\
{BOLD_ON} Account of 'Alice' is {NORMAL_TEXT}
{BOLD_ON} hpcrayuxhrcy2wtpfwgwjibderrvjll6azfr4tqat3eka2m2gbb55bid {NORMAL_TEXT}\n""",
whois, UserInput("whois Alice"), *self.args)
def test_group_id_from_group_name(self) -> None:
self.assert_prints(
f"""\
{BOLD_ON} Group ID of group 'test_group' is {NORMAL_TEXT}
{BOLD_ON} 2dbCCptB9UGo9 {NORMAL_TEXT}\n""",
whois, UserInput(f"whois test_group"), *self.args)
def test_group_name_from_group_id(self) -> None:
self.assert_prints(
f"""\
{BOLD_ON} Name of group with ID '2dbCCptB9UGo9' is {NORMAL_TEXT}
{BOLD_ON} test_group {NORMAL_TEXT}\n""",
whois, UserInput("whois 2dbCCptB9UGo9"), *self.args)
class TestWipe(TFCTestCase):
def setUp(self) -> None:
"""Pre-test actions."""
self.settings = Settings()
self.queues = gen_queue_dict()
self.gateway = Gateway()
self.args = self.settings, self.queues, self.gateway
@mock.patch('builtins.input', return_value='No')
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)
@mock.patch('builtins.input', return_value='Yes')
@mock.patch('time.sleep', return_value=None)
def test_wipe_local_testing(self, *_: Any) -> None:
# Setup
self.settings.local_testing_mode = True
self.gateway.settings.data_diode_sockets = True
for _ in range(2):
self.queues[COMMAND_PACKET_QUEUE].put("dummy command")
self.queues[RELAY_PACKET_QUEUE].put("dummy packet")
# Test
self.assertIsNone(wipe(*self.args))
wipe_packet = UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_WIPE_COMMAND
self.assertTrue(self.queues[RELAY_PACKET_QUEUE].get().startswith(wipe_packet))
@mock.patch('os.system', return_value=None)
@mock.patch('builtins.input', return_value='Yes')
@mock.patch('time.sleep', return_value=None)
def test_wipe(self, *_: Any) -> None:
# Setup
for _ in range(2):
self.queues[COMMAND_PACKET_QUEUE].put("dummy command")
self.queues[RELAY_PACKET_QUEUE].put("dummy packet")
# Test
self.assertIsNone(wipe(*self.args))
wipe_packet = UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_WIPE_COMMAND
self.assertTrue(self.queues[RELAY_PACKET_QUEUE].get().startswith(wipe_packet))
if __name__ == '__main__':
unittest.main(exit=False)