tfc-mirror/src/tx/files.py

162 lines
6.1 KiB
Python
Executable File

#!/usr/bin/env python3.6
# -*- coding: utf-8 -*-
"""
Copyright (C) 2013-2017 Markus Ottela
This file is part of .
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 base64
import datetime
import os
import typing
import zlib
from src.common.crypto import byte_padding, csprng, encrypt_and_sign
from src.common.encoding import int_to_bytes
from src.common.exceptions import FunctionReturn
from src.common.misc import readable_size, split_byte_string
from src.common.reed_solomon import RSCodec
from src.common.statics import *
if typing.TYPE_CHECKING:
from src.common.db_settings import Settings
from src.common.gateway import Gateway
from src.tx.windows import TxWindow
class File(object):
"""File object wraps methods around file data/header processing."""
def __init__(self,
path: str,
window: 'TxWindow',
settings: 'Settings',
gateway: 'Gateway') -> None:
"""Load file data from specified path and add headers."""
self.path = path
self.window = window
self.settings = settings
self.gateway = gateway
self.name = None # type: bytes
self.size = None # type: bytes
self.data = None # type: bytes
self.time_bytes = bytes(FILE_ETA_FIELD_LEN)
self.time_print = ''
self.size_print = ''
self.plaintext = b''
self.load_file_data()
self.process_file_data()
self.finalize()
def load_file_data(self) -> None:
"""Load file name, size and data from specified path."""
if not os.path.isfile(self.path):
raise FunctionReturn("Error: File not found.")
self.name = (self.path.split('/')[-1]).encode()
self.name_length_check()
byte_size = os.path.getsize(self.path)
if byte_size == 0:
raise FunctionReturn("Error: Target file is empty.")
self.size = int_to_bytes(byte_size)
self.size_print = readable_size(byte_size)
with open(self.path, 'rb') as f:
self.data = f.read()
def process_file_data(self) -> None:
"""Compress, encrypt and encode file data.
Compress file to reduce data transmission time. Add inner
layer of encryption to provide sender-based control over
partial transmission. Encode data with Base85. This prevents
inner ciphertext from colliding with file header delimiters.
"""
compressed = zlib.compress(self.data, level=COMPRESSION_LEVEL)
file_key = csprng()
encrypted = encrypt_and_sign(compressed, key=file_key)
encrypted += file_key
self.data = base64.b85encode(encrypted)
def finalize(self) -> None:
"""Finalize packet and generate plaintext."""
self.update_delivery_time()
self.plaintext = self.time_bytes + self.size + self.name + US_BYTE + self.data
def name_length_check(self) -> None:
"""Ensure that file header fits the first packet."""
header = bytes(FILE_PACKET_CTR_LEN + FILE_ETA_FIELD_LEN + FILE_SIZE_FIELD_LEN)
header += self.name + US_BYTE
if len(header) >= PADDING_LEN:
raise FunctionReturn("Error: File name is too long.")
def count_number_of_packets(self) -> int:
"""Count number of packets needed for file delivery."""
packet_data = self.time_bytes + self.size + self.name + US_BYTE + self.data
if len(packet_data) < PADDING_LEN:
return 1
else:
packet_data += bytes(FILE_PACKET_CTR_LEN)
packet_data = byte_padding(packet_data)
return len(split_byte_string(packet_data, item_len=PADDING_LEN))
def update_delivery_time(self) -> None:
"""Calculate transmission time.
Transmission time is based on average delays and settings.
"""
no_packets = self.count_number_of_packets()
if self.settings.session_traffic_masking:
avg_delay = self.settings.traffic_masking_static_delay + (self.settings.traffic_masking_random_delay / 2)
if self.settings.multi_packet_random_delay:
avg_delay += (self.settings.max_duration_of_random_delay / 2)
total_time = len(self.window) * no_packets * avg_delay
total_time *= 2 # Accommodate command packets between file packets
total_time += no_packets * TRAFFIC_MASKING_QUEUE_CHECK_DELAY
else:
# Determine total data to be transmitted over serial
rs = RSCodec(2 * self.settings.session_serial_error_correction)
total_data = 0
for c in self.window:
data = os.urandom(PACKET_LENGTH) + c.rx_account.encode() + c.tx_account.encode()
enc_data = rs.encode(data)
total_data += no_packets * len(enc_data)
# Determine time required to send all data
total_time = 0.0
if self.settings.local_testing_mode:
total_time += no_packets * LOCAL_TESTING_PACKET_DELAY
else:
total_bauds = total_data * BAUDS_PER_BYTE
total_time += total_bauds / self.settings.session_serial_baudrate
total_time += no_packets * self.settings.txm_inter_packet_delay
if self.settings.multi_packet_random_delay:
total_time += no_packets * (self.settings.max_duration_of_random_delay / 2)
# Update delivery time
self.time_bytes = int_to_bytes(int(total_time))
self.time_print = str(datetime.timedelta(seconds=int(total_time)))