tfc-mirror/src/receiver/messages.py

267 lines
13 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 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 SoftError
from src.common.misc import separate_header, separate_headers
from src.common.statics import (ASSEMBLY_PACKET_HEADER_LENGTH, BLAKE2_DIGEST_LENGTH, FILE, FILE_KEY_HEADER,
GROUP_ID_LENGTH, GROUP_MESSAGE_HEADER, GROUP_MSG_ID_LENGTH, LOCAL_PUBKEY, MESSAGE,
MESSAGE_HEADER_LENGTH, ONION_SERVICE_PUBLIC_KEY_LENGTH, ORIGIN_CONTACT_HEADER,
ORIGIN_HEADER_LENGTH, ORIGIN_USER_HEADER, PLACEHOLDER_DATA, PRIVATE_MESSAGE_HEADER,
SYMMETRIC_KEY_LENGTH, WHISPER_FIELD_LENGTH)
from src.receiver.packet import decrypt_assembly_packet
if typing.TYPE_CHECKING:
from datetime import datetime
from src.common.database import MessageLog
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_settings import Settings
from src.receiver.packet import Packet, PacketList
from src.receiver.windows import WindowList
def log_masking_packets(onion_pub_key: bytes, # Onion address of associated contact
origin: bytes, # Origin of packet (user / contact)
logging: bool, # When True, message will be logged
settings: 'Settings', # Settings object
packet: 'Packet', # Packet object
message_log: 'MessageLog', # MessageLog object
completed: bool = False, # When True, logs placeholder data for completed message
) -> 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, message_log, origin)
packet.log_masking_ctr = 0
def process_message_packet(ts: 'datetime', # Timestamp of received message packet
assembly_packet_ct: bytes, # Encrypted assembly packet
window_list: 'WindowList', # WindowList object
packet_list: 'PacketList', # PacketList object
contact_list: 'ContactList', # ContactList object
key_list: 'KeyList', # KeyList object
group_list: 'GroupList', # GroupList object
settings: 'Settings', # Settings object
file_keys: Dict[bytes, bytes], # Dictionary of file decryption keys
message_log: 'MessageLog', # MessageLog object
) -> None:
"""Process received message packet."""
command_window = window_list.get_command_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 SoftError("Warning! Received packet masqueraded as a command.", window=command_window)
if origin not in [ORIGIN_USER_HEADER, ORIGIN_CONTACT_HEADER]:
raise SoftError("Error: Received packet had an invalid origin-header.", window=command_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
try:
packet.add_packet(assembly_packet)
except SoftError:
log_masking_packets(onion_pub_key, origin, logging, settings, packet, message_log)
raise
log_masking_packets(onion_pub_key, origin, logging, settings, packet, message_log)
if packet.is_complete:
process_complete_message_packet(ts, onion_pub_key, p_type, origin, logging, packet, window_list,
contact_list, group_list, settings, message_log, file_keys)
def process_complete_message_packet(ts: 'datetime', # Timestamp of received message packet
onion_pub_key: bytes, # Onion address of associated contact
p_type: str, # Packet type (file, message)
origin: bytes, # Origin of packet (user / contact)
logging: bool, # When True, message will be logged
packet: 'Packet', # Packet object
window_list: 'WindowList', # WindowList object
contact_list: 'ContactList', # ContactList object
group_list: 'GroupList', # GroupList object
settings: 'Settings', # Settings object
message_log: 'MessageLog', # MessageLog object
file_keys: Dict[bytes, bytes] # Dictionary of file decryption keys
) -> None:
"""Process complete message packet.
The assembled message packet might contain a file if the sender
has traffic masking enabled, or it might contain other data.
"""
try:
if p_type == FILE:
packet.assemble_and_store_file(ts, onion_pub_key, window_list)
raise SoftError("File storage complete.", output=False) # Raising allows calling log_masking_packets
if p_type == MESSAGE:
process_message(ts, onion_pub_key, origin, logging, packet, window_list,
contact_list, group_list, message_log, file_keys)
except (SoftError, UnicodeError):
log_masking_packets(onion_pub_key, origin, logging, settings, packet, message_log, completed=True)
raise
finally:
packet.clear_assembly_packets()
def process_message(ts: 'datetime', # Timestamp of received message packet
onion_pub_key: bytes, # Onion address of associated contact
origin: bytes, # Origin of message (user / contact)
logging: bool, # When True, message will be logged
packet: 'Packet', # Packet object
window_list: 'WindowList', # WindowList object
contact_list: 'ContactList', # ContactList object
group_list: 'GroupList', # GroupList object
message_log: 'MessageLog', # MessageLog object
file_keys: Dict[bytes, bytes] # Dictionary of file decryption keys
) -> None:
"""Process message packet.
The received message might be a private or group message, or it
might contain decryption key for file received earlier.
Each received message contains a whisper header that allows the
sender to request the message to not be logged. This request will
be obeyed as long as the recipient does not edit the source code
below. Thus, the sender should not trust a whisper message is
never logged.
"""
whisper_byte, header, assembled = separate_headers(packet.assemble_message_packet(),
[WHISPER_FIELD_LENGTH, MESSAGE_HEADER_LENGTH])
if len(whisper_byte) != WHISPER_FIELD_LENGTH:
raise SoftError("Error: Message from contact had an invalid whisper header.")
whisper = bytes_to_bool(whisper_byte)
if header == GROUP_MESSAGE_HEADER:
logging = process_group_message(ts, assembled, 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 SoftError(f"Received file decryption key from {nick}", window=window_list.get_command_window())
else:
raise SoftError("Error: Message from contact had an invalid header.")
# Logging
if whisper:
raise SoftError("Whisper message complete.", output=False)
if logging:
for p in packet.assembly_pt_list:
write_log_entry(p, onion_pub_key, message_log, origin)
def process_group_message(ts: 'datetime', # Timestamp of group message
assembled: bytes, # Group message and its headers
onion_pub_key: bytes, # Onion address of associated contact
origin: bytes, # Origin of group message (user / contact)
whisper: bool, # When True, message is not logged.
group_list: 'GroupList', # GroupList object
window_list: 'WindowList' # WindowList object
) -> bool:
"""Process a group message."""
group_id, assembled = separate_header(assembled, GROUP_ID_LENGTH)
if not group_list.has_group_id(group_id):
raise SoftError("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 SoftError("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 SoftError("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 the group's logging setting because it might be different
# from the logging setting of the contact who sent group message.
return group.log_messages
def process_file_key_message(assembled: bytes, # File decryption key
onion_pub_key: bytes, # Onion address of associated contact
origin: bytes, # Origin of file key packet (user / contact)
contact_list: 'ContactList', # ContactList object
file_keys: Dict[bytes, bytes] # Dictionary of file identifiers and decryption keys
) -> str:
"""Process received file key delivery message."""
if origin == ORIGIN_USER_HEADER:
raise SoftError("File key message from the user.", output=False)
try:
decoded = base64.b85decode(assembled)
except ValueError:
raise SoftError("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 SoftError("Error: Received an invalid file key message.")
file_keys[onion_pub_key + ct_hash] = file_key
nick = contact_list.get_nick_by_pub_key(onion_pub_key)
return nick