313 lines
12 KiB
Python
Executable File
313 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3.6
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
Copyright (C) 2013-2017 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 <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
import os
|
|
import random
|
|
import time
|
|
import typing
|
|
import zlib
|
|
|
|
from typing import Dict, List, Union
|
|
|
|
from src.common.crypto import byte_padding, csprng, encrypt_and_sign, hash_chain
|
|
from src.common.encoding import int_to_bytes
|
|
from src.common.exceptions import CriticalError, FunctionReturn
|
|
from src.common.input import yes
|
|
from src.common.misc import split_byte_string
|
|
from src.common.output import c_print
|
|
from src.common.path import ask_path_gui
|
|
from src.common.reed_solomon import RSCodec
|
|
from src.common.statics import *
|
|
|
|
from src.tx.files import File
|
|
|
|
if typing.TYPE_CHECKING:
|
|
from multiprocessing import Queue
|
|
from src.common.db_keys import KeyList
|
|
from src.common.db_settings import Settings
|
|
from src.common.gateway import Gateway
|
|
from src.tx.user_input import UserInput
|
|
from src.tx.windows import MockWindow, TxWindow
|
|
|
|
|
|
def queue_message(user_input: 'UserInput',
|
|
window: Union['MockWindow', 'TxWindow'],
|
|
settings: 'Settings',
|
|
m_queue: 'Queue',
|
|
header: bytes = b'',
|
|
log_as_ph: bool = False) -> None:
|
|
"""Prepend header, split to assembly packets and queue them."""
|
|
if not header:
|
|
if window.type == WIN_TYPE_GROUP:
|
|
group_msg_id = os.urandom(GROUP_MSG_ID_LEN)
|
|
header = GROUP_MESSAGE_HEADER + group_msg_id + window.name.encode() + US_BYTE
|
|
else:
|
|
header = PRIVATE_MESSAGE_HEADER
|
|
|
|
payload = header + user_input.plaintext.encode()
|
|
packet_list = split_to_assembly_packets(payload, MESSAGE)
|
|
|
|
queue_packets(packet_list, MESSAGE, settings, m_queue, window, log_as_ph)
|
|
|
|
|
|
def queue_file(window: 'TxWindow',
|
|
settings: 'Settings',
|
|
f_queue: 'Queue',
|
|
gateway: 'Gateway') -> None:
|
|
"""Ask file path and load file data."""
|
|
path = ask_path_gui("Select file to send...", settings, get_file=True)
|
|
file = File(path, window, settings, gateway)
|
|
|
|
packet_list = split_to_assembly_packets(file.plaintext, FILE)
|
|
|
|
if settings.confirm_sent_files:
|
|
try:
|
|
if not yes(f"Send {file.name.decode()} ({file.size_print}) to {window.type_print} {window.name} "
|
|
f"({len(packet_list)} packets, time: {file.time_print})?"):
|
|
raise FunctionReturn("File selection aborted.")
|
|
except KeyboardInterrupt:
|
|
raise FunctionReturn("File selection aborted.", head=3)
|
|
|
|
queue_packets(packet_list, FILE, settings, f_queue, window, log_as_ph=True)
|
|
|
|
|
|
def queue_command(command: bytes,
|
|
settings: 'Settings',
|
|
c_queue: 'Queue',
|
|
window: 'TxWindow' = None) -> None:
|
|
"""Split command to assembly packets and queue them for sender_loop()."""
|
|
packet_list = split_to_assembly_packets(command, COMMAND)
|
|
|
|
queue_packets(packet_list, COMMAND, settings, c_queue, window)
|
|
|
|
|
|
def queue_to_nh(packet: bytes,
|
|
settings: 'Settings',
|
|
nh_queue: 'Queue',
|
|
delay: bool = False) -> None:
|
|
"""Queue unencrypted command/exported file to NH."""
|
|
nh_queue.put((packet, delay, settings))
|
|
|
|
|
|
def split_to_assembly_packets(payload: bytes, p_type: str) -> List[bytes]:
|
|
"""Split payload to assembly packets.
|
|
|
|
Messages and commands are compressed to reduce transmission time.
|
|
Files have been compressed at earlier phase, before B85 encoding.
|
|
|
|
If the compressed message can not be sent over one packet, it is
|
|
split into multiple assembly packets with headers. Long messages
|
|
are encrypted with inner layer of XSalsa20-Poly1305 to provide
|
|
sender based control over partially transmitted data. Regardless
|
|
of packet size, files always have an inner layer of encryption,
|
|
and it is added in earlier phase. Commands do not need
|
|
sender-based control, so they are only delivered with hash that
|
|
makes integrity check easy.
|
|
|
|
First assembly packet in file transmission is prepended with 8-byte
|
|
packet counter that tells sender and receiver how many packets the
|
|
file transmission requires.
|
|
"""
|
|
s_header = {MESSAGE: M_S_HEADER, FILE: F_S_HEADER, COMMAND: C_S_HEADER}[p_type]
|
|
l_header = {MESSAGE: M_L_HEADER, FILE: F_L_HEADER, COMMAND: C_L_HEADER}[p_type]
|
|
a_header = {MESSAGE: M_A_HEADER, FILE: F_A_HEADER, COMMAND: C_A_HEADER}[p_type]
|
|
e_header = {MESSAGE: M_E_HEADER, FILE: F_E_HEADER, COMMAND: C_E_HEADER}[p_type]
|
|
|
|
if p_type in [MESSAGE, COMMAND]:
|
|
payload = zlib.compress(payload, level=COMPRESSION_LEVEL)
|
|
|
|
if len(payload) < PADDING_LEN:
|
|
padded = byte_padding(payload)
|
|
packet_list = [s_header + padded]
|
|
|
|
else:
|
|
if p_type == MESSAGE:
|
|
msg_key = csprng()
|
|
payload = encrypt_and_sign(payload, msg_key)
|
|
payload += msg_key
|
|
|
|
elif p_type == FILE:
|
|
payload = bytes(FILE_PACKET_CTR_LEN) + payload
|
|
|
|
elif p_type == COMMAND:
|
|
payload += hash_chain(payload)
|
|
|
|
padded = byte_padding(payload)
|
|
p_list = split_byte_string(padded, item_len=PADDING_LEN)
|
|
|
|
if p_type == FILE:
|
|
p_list[0] = int_to_bytes(len(p_list)) + p_list[0][FILE_PACKET_CTR_LEN:]
|
|
|
|
packet_list = ([l_header + p_list[0]] +
|
|
[a_header + p for p in p_list[1:-1]] +
|
|
[e_header + p_list[-1]])
|
|
|
|
return packet_list
|
|
|
|
|
|
def queue_packets(packet_list: List[bytes],
|
|
p_type: str,
|
|
settings: 'Settings',
|
|
queue: 'Queue',
|
|
window: Union['MockWindow', 'TxWindow'] = None,
|
|
log_as_ph: bool = False) -> None:
|
|
"""Queue assembly packets for sender_loop()."""
|
|
if p_type in [MESSAGE, FILE] and window is not None:
|
|
|
|
if settings.session_traffic_masking:
|
|
for p in packet_list:
|
|
queue.put((p, window.log_messages, log_as_ph))
|
|
else:
|
|
for c in window:
|
|
for p in packet_list:
|
|
queue.put((p, settings, c.rx_account, c.tx_account, window.log_messages, log_as_ph, window.uid))
|
|
|
|
elif p_type == COMMAND:
|
|
if settings.session_traffic_masking:
|
|
for p in packet_list:
|
|
if window is None:
|
|
log_setting = None
|
|
else:
|
|
log_setting = window.log_messages
|
|
queue.put((p, log_setting))
|
|
else:
|
|
for p in packet_list:
|
|
queue.put((p, settings))
|
|
|
|
|
|
def send_packet(key_list: 'KeyList',
|
|
gateway: 'Gateway',
|
|
log_queue: 'Queue',
|
|
packet: bytes,
|
|
settings: 'Settings',
|
|
rx_account: str = None,
|
|
tx_account: str = None,
|
|
logging: bool = None,
|
|
log_as_ph: bool = None) -> None:
|
|
"""Encrypt and send assembly packet.
|
|
|
|
:param packet: Padded plaintext assembly packet
|
|
:param key_list: Key list object
|
|
:param settings: Settings object
|
|
:param gateway: Gateway object
|
|
:param log_queue: Multiprocessing queue for logged messages
|
|
:param rx_account: Recipient account
|
|
:param tx_account: Sender's account associated with recipient's account
|
|
:param logging: When True, log the assembly packet
|
|
:param log_as_ph: When True, log assembly packet as placeholder data
|
|
:return: None
|
|
"""
|
|
if len(packet) != ASSEMBLY_PACKET_LEN:
|
|
raise CriticalError("Invalid assembly packet PT length.")
|
|
|
|
if rx_account is None:
|
|
keyset = key_list.get_keyset(LOCAL_ID)
|
|
header = COMMAND_PACKET_HEADER
|
|
trailer = b''
|
|
else:
|
|
keyset = key_list.get_keyset(rx_account)
|
|
header = MESSAGE_PACKET_HEADER
|
|
trailer = tx_account.encode() + US_BYTE + rx_account.encode()
|
|
|
|
harac_in_bytes = int_to_bytes(keyset.tx_harac)
|
|
encrypted_harac = encrypt_and_sign(harac_in_bytes, keyset.tx_hek)
|
|
encrypted_message = encrypt_and_sign(packet, keyset.tx_key)
|
|
encrypted_packet = header + encrypted_harac + encrypted_message + trailer
|
|
transmit(encrypted_packet, settings, gateway)
|
|
|
|
keyset.rotate_tx_key()
|
|
|
|
log_queue.put((logging, log_as_ph, packet, rx_account, settings, key_list.master_key))
|
|
|
|
|
|
def transmit(packet: bytes,
|
|
settings: 'Settings',
|
|
gateway: 'Gateway',
|
|
delay: bool = True) -> None:
|
|
"""Add Reed-Solomon erasure code and output packet via gateway.
|
|
|
|
Note that random.SystemRandom() uses Kernel CSPRNG (/dev/urandom),
|
|
not Python's weak RNG based on Mersenne Twister:
|
|
https://docs.python.org/2/library/random.html#random.SystemRandom
|
|
"""
|
|
rs = RSCodec(2 * settings.session_serial_error_correction)
|
|
packet = rs.encode(packet)
|
|
gateway.write(packet)
|
|
|
|
if settings.local_testing_mode:
|
|
time.sleep(LOCAL_TESTING_PACKET_DELAY)
|
|
|
|
if not settings.session_traffic_masking:
|
|
if settings.multi_packet_random_delay and delay:
|
|
random_delay = random.SystemRandom().uniform(0, settings.max_duration_of_random_delay)
|
|
time.sleep(random_delay)
|
|
|
|
|
|
def cancel_packet(user_input: 'UserInput',
|
|
window: 'TxWindow',
|
|
settings: 'Settings',
|
|
queues: Dict[bytes, 'Queue']) -> None:
|
|
"""Cancel sent message/file to contact/group."""
|
|
|
|
queue, header, p_type = dict(cm=(queues[MESSAGE_PACKET_QUEUE], M_C_HEADER, 'messages'),
|
|
cf=(queues[FILE_PACKET_QUEUE], F_C_HEADER, 'files' ))[user_input.plaintext]
|
|
|
|
cancel_pt = header + bytes(PADDING_LEN)
|
|
|
|
cancel = False
|
|
if settings.session_traffic_masking:
|
|
if queue.qsize() != 0:
|
|
cancel = True
|
|
while queue.qsize() != 0:
|
|
queue.get()
|
|
log_m_dictionary = dict((c.rx_account, c.log_messages) for c in window)
|
|
queue.put((cancel_pt, log_m_dictionary, True))
|
|
|
|
message = f"Cancelled queues {p_type}." if cancel else f"No {p_type} to cancel."
|
|
c_print(message, head=1, tail=1)
|
|
|
|
else:
|
|
p_buffer = []
|
|
while queue.qsize() != 0:
|
|
q_data = queue.get()
|
|
win_uid = q_data[6]
|
|
|
|
# Put messages unrelated to active window into buffer
|
|
if win_uid != window.uid:
|
|
p_buffer.append(q_data)
|
|
else:
|
|
cancel = True
|
|
|
|
# Put cancel packets for each window contact to queue first
|
|
if cancel:
|
|
for c in window:
|
|
queue.put((cancel_pt, settings, c.rx_account, c.tx_account, c.log_messages, window.uid))
|
|
|
|
# Put buffered tuples back to queue
|
|
for p in p_buffer:
|
|
queue.put(p)
|
|
|
|
if cancel:
|
|
message = f"Cancelled queued {p_type} to {window.type_print} {window.name}."
|
|
else:
|
|
message = f"No {p_type} queued for {window.type_print} {window.name}."
|
|
|
|
c_print(message, head=1, tail=1)
|