tfc-mirror/tests/receiver/test_packet.py

455 lines
20 KiB
Python

#!/usr/bin/env python3.7
# -*- coding: utf-8 -*-
"""
TFC - Onion-routed, endpoint secure messaging system
Copyright (C) 2013-2019 Markus Ottela
This file is part of TFC.
TFC is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.
TFC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with TFC. If not, see <https://www.gnu.org/licenses/>.
"""
import os
import unittest
import zlib
from datetime import datetime
from unittest import mock
from src.common.crypto import byte_padding, encrypt_and_sign
from src.common.encoding import int_to_bytes
from src.common.statics import (COMMAND, COMPRESSION_LEVEL, DIR_RECV_FILES, FILE, F_C_HEADER, LOCAL_ID, MESSAGE,
M_A_HEADER, M_E_HEADER, ORIGIN_CONTACT_HEADER, ORIGIN_USER_HEADER, PADDING_LENGTH,
PRIVATE_MESSAGE_HEADER, P_N_HEADER, SYMMETRIC_KEY_LENGTH, US_BYTE)
from src.transmitter.packet import split_to_assembly_packets
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
class TestDecryptAssemblyPacket(TFCTestCase):
def setUp(self):
"""Pre-test actions."""
self.onion_pub_key = nick_to_pub_key("Alice")
self.origin = ORIGIN_CONTACT_HEADER
self.window_list = WindowList(nicks=['Alice', LOCAL_ID])
self.contact_list = ContactList(nicks=['Alice', LOCAL_ID])
self.key_list = KeyList(nicks=['Alice', LOCAL_ID])
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_fr(self):
# Setup
keyset = self.key_list.get_keyset(nick_to_pub_key("Alice"))
keyset.rx_mk = bytes(SYMMETRIC_KEY_LENGTH)
packet = assembly_packet_creator(MESSAGE, payload="Test message", encrypt_packet=True)[0]
# Test
self.assert_fr("Warning! Loaded zero-key for packet decryption.",
decrypt_assembly_packet, packet, *self.args)
def test_invalid_harac_ct_raises_fr(self):
packet = assembly_packet_creator(MESSAGE, payload="Test message", encrypt_packet=True, tamper_harac=True)[0]
self.assert_fr("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_fr(self):
# Setup
keyset = self.key_list.get_keyset(nick_to_pub_key("Alice"))
keyset.rx_hk = bytes(SYMMETRIC_KEY_LENGTH)
packet = assembly_packet_creator(MESSAGE, payload="Test message", encrypt_packet=True)[0]
# Test
self.assert_fr("Warning! Loaded zero-key for packet decryption.", decrypt_assembly_packet, packet, *self.args)
def test_expired_harac_raises_fr(self):
# Setup
self.keyset.rx_harac = 1
# Test
packet = assembly_packet_creator(MESSAGE, payload="Test message", encrypt_packet=True, harac=0)[0]
self.assert_fr("Warning! Received packet from Alice had an expired hash ratchet counter.",
decrypt_assembly_packet, packet, *self.args)
@mock.patch('builtins.input', return_value='No')
def test_harac_dos_can_be_interrupted(self, _):
packet = assembly_packet_creator(MESSAGE, payload="Test message", encrypt_packet=True, harac=100_001)[0]
self.assert_fr("Dropped packet from Alice.",
decrypt_assembly_packet, packet, *self.args)
def test_invalid_packet_ct_raises_fr(self):
packet = assembly_packet_creator(MESSAGE, payload="Test message", encrypt_packet=True, tamper_message=True)[0]
self.assert_fr("Warning! Received packet from Alice had an invalid MAC.",
decrypt_assembly_packet, packet, *self.args)
def test_successful_packet_decryption(self):
packet = assembly_packet_creator(MESSAGE, payload="Test message", encrypt_packet=True)[0]
self.assertEqual(decrypt_assembly_packet(packet, *self.args),
assembly_packet_creator(MESSAGE, payload="Test message")[0])
def test_successful_packet_decryption_with_offset(self):
packet = assembly_packet_creator(MESSAGE, payload="Test message", encrypt_packet=True, message_number=3)[0]
self.assertEqual(decrypt_assembly_packet(packet, *self.args),
assembly_packet_creator(MESSAGE, payload="Test message", message_number=3)[0])
def test_successful_command_decryption(self):
packet = assembly_packet_creator(COMMAND, payload=b"command_data", encrypt_packet=True)[0]
self.assertEqual(decrypt_assembly_packet(packet, *self.args),
assembly_packet_creator(COMMAND, payload=b"command_data")[0])
class TestPacket(TFCTestCase):
def setUp(self):
"""Pre-test actions."""
self.short_msg = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
self.msg = ("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean condimentum consectetur purus quis"
" dapibus. Fusce venenatis lacus ut rhoncus faucibus. Cras sollicitudin commodo sapien, sed bibendu"
"m velit maximus in. Aliquam ac metus risus. Sed cursus ornare luctus. Integer aliquet lectus id ma"
"ssa blandit imperdiet. Ut sed massa eget quam facilisis rutrum. Mauris eget luctus nisl. Sed ut el"
"it iaculis, faucibus lacus eget, sodales magna. Nunc sed commodo arcu. In hac habitasse platea dic"
"tumst. Integer luctus aliquam justo, at vestibulum dolor iaculis ac. Etiam laoreet est eget odio r"
"utrum, vel malesuada lorem rhoncus. Cras finibus in neque eu euismod. Nulla facilisi. Nunc nec ali"
"quam quam, quis ullamcorper leo. Nunc egestas lectus eget est porttitor, in iaculis felis sceleris"
"que. In sem elit, fringilla id viverra commodo, sagittis varius purus. Pellentesque rutrum loborti"
"s neque a facilisis. Mauris id tortor placerat, aliquam dolor ac, venenatis arcu.")
self.unit_test_dir = cd_unit_test()
self.ts = datetime.now()
self.contact = create_contact('Alice')
self.settings = Settings(log_file_masking=True)
self.onion_pub_key = nick_to_pub_key('Alice')
self.window_list = WindowList()
self.whisper_header = b'\x00'
compressed = zlib.compress(b'file_data', level=COMPRESSION_LEVEL)
file_key = os.urandom(SYMMETRIC_KEY_LENGTH)
encrypted = encrypt_and_sign(compressed, key=file_key)
encrypted += file_key
self.short_f_data = (int_to_bytes(1) + int_to_bytes(2) + b'testfile.txt' + US_BYTE + encrypted)
def tearDown(self):
"""Post-test actions."""
cleanup(self.unit_test_dir)
def test_invalid_assembly_packet_header_raises_fr(self):
# 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]
# Test
self.assert_fr("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_fr(self):
# Setup
packet = Packet(self.onion_pub_key, ORIGIN_USER_HEADER, MESSAGE, self.contact, self.settings)
# Test
for header in [M_A_HEADER, M_E_HEADER]:
self.assert_fr("Missing start packet.", packet.add_packet, header + bytes(PADDING_LENGTH))
self.assertEqual(packet.log_masking_ctr, 2)
def test_short_message(self):
# Setup
packet = Packet(self.onion_pub_key, ORIGIN_USER_HEADER, MESSAGE, self.contact, self.settings)
packet_list = assembly_packet_creator(MESSAGE, self.short_msg)
for p in packet_list:
packet.add_packet(p, packet_ct=b'test_ct')
# Test
self.assertEqual(packet.assemble_message_packet(),
self.whisper_header + PRIVATE_MESSAGE_HEADER + self.short_msg.encode())
self.assertEqual(packet.log_ct_list, [b'test_ct'])
def test_compression_error_raises_fr(self):
# 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)
for p in packet_list:
packet.add_packet(p)
# Test
self.assert_fr("Error: Decompression of message failed.", packet.assemble_message_packet)
def test_long_message(self):
# Setup
packet = Packet(self.onion_pub_key, ORIGIN_USER_HEADER, MESSAGE, self.contact, self.settings)
packet_list = assembly_packet_creator(MESSAGE, self.msg)
for p in packet_list:
packet.add_packet(p, packet_ct=b'test_ct')
# Test
message = packet.assemble_message_packet()
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_fr(self):
# 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)
for p in packet_list:
packet.add_packet(p)
# Test
self.assert_fr("Error: Decryption of message failed.", packet.assemble_message_packet)
def test_short_file(self):
# Setup
packets = split_to_assembly_packets(self.short_f_data, FILE)
# Test
self.assertFalse(os.path.isfile(f'{DIR_RECV_FILES}Alice/testfile.txt'))
self.assertFalse(os.path.isfile(f'{DIR_RECV_FILES}Alice/testfile.txt.1'))
packet = Packet(self.onion_pub_key, ORIGIN_CONTACT_HEADER, FILE, self.contact, self.settings)
packet.long_active = True
for p in packets:
packet.add_packet(p)
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'))
for p in packets:
packet.add_packet(p)
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_fr(self):
# 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)
# Test
for p in packets:
self.assert_fr("Ignored file from the user.", packet.add_packet, p)
self.assertEqual(packet.log_masking_ctr, 1)
def test_unauthorized_file_from_contact_raises_fr(self):
# Setup
self.contact.file_reception = False
packet = Packet(self.onion_pub_key, ORIGIN_CONTACT_HEADER, FILE, self.contact, self.settings)
packets = split_to_assembly_packets(self.short_f_data, FILE)
# Test
for p in packets:
self.assert_fr("Alert! File transmission from Alice but reception is disabled.", packet.add_packet, p)
self.assertEqual(packet.log_masking_ctr, 1)
def test_long_file(self):
# Setup
packet = Packet(self.onion_pub_key, ORIGIN_CONTACT_HEADER, FILE, self.contact, self.settings)
packet.long_active = True
packet_list = assembly_packet_creator(FILE)
for p in packet_list:
packet.add_packet(p)
# Test
self.assertIsNone(packet.assemble_and_store_file(self.ts, self.onion_pub_key, self.window_list))
self.assertEqual(os.path.getsize(f'{DIR_RECV_FILES}Alice/test_file.txt'), 10000)
def test_disabled_file_reception_raises_fr_with_append_packet(self):
# Setup
packet = Packet(self.onion_pub_key, ORIGIN_CONTACT_HEADER, FILE, self.contact, self.settings)
packet.long_active = True
packet_list = assembly_packet_creator(FILE)
for p in packet_list[:2]:
self.assertIsNone(packet.add_packet(p))
packet.contact.file_reception = False
# Test
self.assert_fr("Alert! File reception disabled mid-transfer.", packet.add_packet, packet_list[2])
for p in packet_list[3:]:
self.assert_fr("Missing start packet.", packet.add_packet, p)
self.assertEqual(packet.log_masking_ctr, len(packet_list))
def test_disabled_file_reception_raises_fr_with_end_packet(self):
# Setup
packet = Packet(self.onion_pub_key, ORIGIN_CONTACT_HEADER, FILE, self.contact, self.settings)
packet.long_active = True
packet_list = assembly_packet_creator(FILE)
for p in packet_list[:-1]:
self.assertIsNone(packet.add_packet(p))
packet.contact.file_reception = False
# Test
for p in packet_list[-1:]:
self.assert_fr("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_fr(self):
# Setup
packet = Packet(self.onion_pub_key, ORIGIN_USER_HEADER, FILE, self.contact, self.settings)
packet_list = assembly_packet_creator(FILE)
# Test
self.assert_fr("Ignored file from the user.", packet.add_packet, packet_list[0])
self.assertEqual(packet.log_masking_ctr, 1)
def test_unauthorized_long_file_raises_fr(self):
# Setup
self.contact.file_reception = False
packet = Packet(self.onion_pub_key, ORIGIN_CONTACT_HEADER, FILE, self.contact, self.settings)
packet_list = assembly_packet_creator(FILE)
# Test
self.assert_fr("Alert! File transmission from Alice but reception is disabled.",
packet.add_packet, packet_list[0])
self.assertEqual(packet.log_masking_ctr, 1)
def test_invalid_long_file_header_raises_fr(self):
# 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)
# Test
self.assert_fr("Error: Received file packet had an invalid header.", packet.add_packet, packet_list[0])
self.assertEqual(packet.log_masking_ctr, 1)
def test_contact_canceled_file(self):
# Setup
packet = Packet(self.onion_pub_key, ORIGIN_CONTACT_HEADER, FILE, self.contact, self.settings)
packet_list = assembly_packet_creator(FILE)[:20]
packet_list.append(byte_padding(F_C_HEADER)) # Add cancel packet
for p in packet_list:
packet.add_packet(p)
# Test
self.assertEqual(len(packet.assembly_pt_list), 0) # Cancel packet empties packet list
self.assertFalse(packet.long_active)
self.assertFalse(packet.is_complete)
self.assertEqual(packet.log_masking_ctr, len(packet_list))
def test_noise_packet_interrupts_file(self):
# Setup
packet = Packet(self.onion_pub_key, ORIGIN_CONTACT_HEADER, FILE, self.contact, self.settings)
packet_list = assembly_packet_creator(FILE)[:20]
packet_list.append(byte_padding(P_N_HEADER)) # Add noise packet
for p in packet_list:
packet.add_packet(p)
# Test
self.assertEqual(len(packet.assembly_pt_list), 0) # Noise packet empties packet list
self.assertFalse(packet.long_active)
self.assertFalse(packet.is_complete)
self.assertEqual(packet.log_masking_ctr, len(packet_list))
def test_short_command(self):
# Setup
packet = Packet(LOCAL_ID, ORIGIN_CONTACT_HEADER, COMMAND, self.contact, self.settings)
packets = assembly_packet_creator(COMMAND, b'test_command')
for p in packets:
packet.add_packet(p)
# Test
self.assertEqual(packet.assemble_command_packet(), b'test_command')
self.assertEqual(packet.log_masking_ctr, 0)
def test_long_command(self):
# Setup
packet = Packet(LOCAL_ID, ORIGIN_CONTACT_HEADER, COMMAND, self.contact, self.settings)
command = 500*b'test_command'
packets = assembly_packet_creator(COMMAND, command)
for p in packets:
packet.add_packet(p)
# Test
self.assertEqual(packet.assemble_command_packet(), command)
self.assertEqual(packet.log_masking_ctr, 0)
def test_long_command_hash_mismatch_raises_fr(self):
# 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)
for p in packet_list:
packet.add_packet(p)
# Test
self.assert_fr("Error: Received an invalid command.", packet.assemble_command_packet)
self.assertEqual(packet.log_masking_ctr, 0)
def test_long_command_compression_error_raises_fr(self):
# 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)
for p in packet_list:
packet.add_packet(p)
# Test
self.assert_fr("Error: Decompression of command failed.", packet.assemble_command_packet)
self.assertEqual(packet.log_masking_ctr, 0)
class TestPacketList(unittest.TestCase):
def setUp(self):
"""Pre-test actions."""
self.contact_list = ContactList(nicks=['Alice', 'Bob'])
self.settings = Settings()
self.onion_pub_key = nick_to_pub_key('Alice')
packet = Packet(self.onion_pub_key, ORIGIN_CONTACT_HEADER, MESSAGE,
self.contact_list.get_contact_by_address_or_nick('Alice'), self.settings)
self.packet_list = PacketList(self.settings, self.contact_list)
self.packet_list.packets = [packet]
def test_packet_list_iterates_over_contact_objects(self):
for p in self.packet_list:
self.assertIsInstance(p, Packet)
def test_len_returns_number_of_contacts(self):
self.assertEqual(len(self.packet_list), 1)
def test_has_packet(self):
self.assertTrue(self.packet_list.has_packet(self.onion_pub_key, ORIGIN_CONTACT_HEADER, MESSAGE))
self.assertFalse(self.packet_list.has_packet(self.onion_pub_key, ORIGIN_USER_HEADER, MESSAGE))
def test_get_packet(self):
packet = self.packet_list.get_packet(self.onion_pub_key, ORIGIN_CONTACT_HEADER, MESSAGE)
self.assertEqual(packet.onion_pub_key, self.onion_pub_key)
self.assertEqual(packet.origin, ORIGIN_CONTACT_HEADER)
self.assertEqual(packet.type, MESSAGE)
packet = self.packet_list.get_packet(self.onion_pub_key, ORIGIN_CONTACT_HEADER, MESSAGE)
self.assertEqual(packet.onion_pub_key, self.onion_pub_key)
self.assertEqual(packet.origin, ORIGIN_CONTACT_HEADER)
self.assertEqual(packet.type, MESSAGE)
if __name__ == '__main__':
unittest.main(exit=False)