tfc-mirror/src/receiver/messages.py

204 lines
8.5 KiB
Python

#!/usr/bin/env python3.6
# -*- 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 base64
import typing
from typing import Dict
from src.common.db_logs import write_log_entry
from src.common.encoding import bytes_to_bool
from src.common.exceptions import FunctionReturn
from src.common.misc import separate_header, separate_headers
from src.common.statics import *
from src.receiver.packet import decrypt_assembly_packet
if typing.TYPE_CHECKING:
from datetime import datetime
from src.common.db_contacts import ContactList
from src.common.db_groups import GroupList
from src.common.db_keys import KeyList
from src.common.db_masterkey import MasterKey
from src.common.db_settings import Settings
from src.receiver.packet import PacketList
from src.receiver.windows import WindowList
def process_message(ts: 'datetime',
assembly_packet_ct: bytes,
window_list: 'WindowList',
packet_list: 'PacketList',
contact_list: 'ContactList',
key_list: 'KeyList',
group_list: 'GroupList',
settings: 'Settings',
master_key: 'MasterKey',
file_keys: Dict[bytes, bytes]
) -> None:
"""Process received private / group message."""
local_window = window_list.get_local_window()
onion_pub_key, origin, assembly_packet_ct \
= separate_headers(assembly_packet_ct, [ONION_SERVICE_PUBLIC_KEY_LENGTH, ORIGIN_HEADER_LENGTH])
if onion_pub_key == LOCAL_PUBKEY:
raise FunctionReturn("Warning! Received packet masqueraded as a command.", window=local_window)
if origin not in [ORIGIN_USER_HEADER, ORIGIN_CONTACT_HEADER]:
raise FunctionReturn("Error: Received packet had an invalid origin-header.", window=local_window)
assembly_packet = decrypt_assembly_packet(assembly_packet_ct, onion_pub_key, origin,
window_list, contact_list, key_list)
p_type = FILE if assembly_packet[:ASSEMBLY_PACKET_HEADER_LENGTH].isupper() else MESSAGE
packet = packet_list.get_packet(onion_pub_key, origin, p_type)
logging = contact_list.get_contact_by_pub_key(onion_pub_key).log_messages
def log_masking_packets(completed: bool = False) -> None:
"""Add masking packets to log file.
If logging and log file masking are enabled, this function will
in case of erroneous transmissions, store the correct number of
placeholder data packets to log file to hide the quantity of
communication that log file observation would otherwise reveal.
"""
if logging and settings.log_file_masking and (packet.log_masking_ctr or completed):
no_masking_packets = len(packet.assembly_pt_list) if completed else packet.log_masking_ctr
for _ in range(no_masking_packets):
write_log_entry(PLACEHOLDER_DATA, onion_pub_key, settings, master_key, origin)
packet.log_masking_ctr = 0
try:
packet.add_packet(assembly_packet)
except FunctionReturn:
log_masking_packets()
raise
log_masking_packets()
if not packet.is_complete:
return None
try:
if p_type == FILE:
packet.assemble_and_store_file(ts, onion_pub_key, window_list)
raise FunctionReturn("File storage complete.", output=False) # Raising allows calling log_masking_packets
elif p_type == MESSAGE:
whisper_byte, header, assembled = separate_headers(packet.assemble_message_packet(),
[WHISPER_FIELD_LENGTH, MESSAGE_HEADER_LENGTH])
if len(whisper_byte) != WHISPER_FIELD_LENGTH:
raise FunctionReturn("Error: Message from contact had an invalid whisper header.")
whisper = bytes_to_bool(whisper_byte)
if header == GROUP_MESSAGE_HEADER:
logging = process_group_message(assembled, ts, onion_pub_key, origin, whisper, group_list, window_list)
elif header == PRIVATE_MESSAGE_HEADER:
window = window_list.get_window(onion_pub_key)
window.add_new(ts, assembled.decode(), onion_pub_key, origin, output=True, whisper=whisper)
elif header == FILE_KEY_HEADER:
nick = process_file_key_message(assembled, onion_pub_key, origin, contact_list, file_keys)
raise FunctionReturn(f"Received file decryption key from {nick}", window=local_window)
else:
raise FunctionReturn("Error: Message from contact had an invalid header.")
if whisper:
raise FunctionReturn("Whisper message complete.", output=False)
if logging:
for p in packet.assembly_pt_list:
write_log_entry(p, onion_pub_key, settings, master_key, origin)
except (FunctionReturn, UnicodeError):
log_masking_packets(completed=True)
raise
finally:
packet.clear_assembly_packets()
def process_group_message(assembled: bytes,
ts: 'datetime',
onion_pub_key: bytes,
origin: bytes,
whisper: bool,
group_list: 'GroupList',
window_list: 'WindowList'
) -> bool:
"""Process a group message."""
group_id, assembled = separate_header(assembled, GROUP_ID_LENGTH)
if not group_list.has_group_id(group_id):
raise FunctionReturn("Error: Received message to an unknown group.", output=False)
group = group_list.get_group_by_id(group_id)
if not group.has_member(onion_pub_key):
raise FunctionReturn("Error: Account is not a member of the group.", output=False)
group_msg_id, group_message = separate_header(assembled, GROUP_MSG_ID_LENGTH)
try:
group_message_str = group_message.decode()
except UnicodeError:
raise FunctionReturn("Error: Received an invalid group message.")
window = window_list.get_window(group.group_id)
# All copies of group messages the user sends to members contain
# the same message ID. This allows the Receiver Program to ignore
# duplicates of outgoing messages sent by the user to each member.
if origin == ORIGIN_USER_HEADER:
if window.group_msg_id != group_msg_id:
window.group_msg_id = group_msg_id
window.add_new(ts, group_message_str, onion_pub_key, origin, output=True, whisper=whisper)
elif origin == ORIGIN_CONTACT_HEADER:
window.add_new(ts, group_message_str, onion_pub_key, origin, output=True, whisper=whisper)
return group.log_messages
def process_file_key_message(assembled: bytes,
onion_pub_key: bytes,
origin: bytes,
contact_list: 'ContactList',
file_keys: Dict[bytes, bytes]
) -> str:
"""Process received file key delivery message."""
if origin == ORIGIN_USER_HEADER:
raise FunctionReturn("File key message from the user.", output=False)
try:
decoded = base64.b85decode(assembled)
except ValueError:
raise FunctionReturn("Error: Received an invalid file key message.")
ct_hash, file_key = separate_header(decoded, BLAKE2_DIGEST_LENGTH)
if len(ct_hash) != BLAKE2_DIGEST_LENGTH or len(file_key) != SYMMETRIC_KEY_LENGTH:
raise FunctionReturn("Error: Received an invalid file key message.")
file_keys[onion_pub_key + ct_hash] = file_key
nick = contact_list.get_contact_by_pub_key(onion_pub_key).nick
return nick