def generate_dummy_group(self) -> bytes: """Generate a byte string that represents a dummy group.""" name = str_to_bytes('dummy_group') log_messages = bool_to_bytes(False) notifications = bool_to_bytes(False) members = self.settings.m_members_in_group * ['dummy_member'] member_bytes = b''.join([str_to_bytes(m) for m in members]) return name + log_messages + notifications + member_bytes
def dump_c(self) -> bytes: """Return contact data as constant length byte string.""" return str_to_bytes(self.rx_account) \ + str_to_bytes(self.tx_account) \ + str_to_bytes(self.nick) \ + self.tx_fingerprint \ + self.rx_fingerprint \ + bool_to_bytes(self.log_messages) \ + bool_to_bytes(self.file_reception) \ + bool_to_bytes(self.notifications)
def serialize_c(self) -> bytes: """Return contact data as constant length byte string.""" return (str_to_bytes(self.rx_account) + str_to_bytes(self.tx_account) + str_to_bytes(self.nick) + self.tx_fingerprint + self.rx_fingerprint + bool_to_bytes(self.log_messages) + bool_to_bytes(self.file_reception) + bool_to_bytes(self.notifications))
def dump_g(self) -> bytes: """Return group data as constant length byte string.""" name = str_to_bytes(self.name) log_messages = bool_to_bytes(self.log_messages) notifications = bool_to_bytes(self.notifications) members = self.get_list_of_member_accounts() num_of_dummies = self.settings.m_members_in_group - len(self.members) members += num_of_dummies * ['dummy_member'] member_bytes = b''.join([str_to_bytes(m) for m in members]) return name + log_messages + notifications + member_bytes
def serialize_g(self) -> bytes: """Return group data as constant length byte string.""" name = str_to_bytes(self.name) log_messages = bool_to_bytes(self.log_messages) notifications = bool_to_bytes(self.notifications) members = self.get_list_of_member_accounts() num_of_dummies = self.settings.max_number_of_group_members - len( self.members) members += num_of_dummies * [DUMMY_MEMBER] member_bytes = b''.join([str_to_bytes(m) for m in members]) return name + log_messages + notifications + member_bytes
def generate_dummy_contact() -> bytes: """Generate byte string for dummy contact.""" rx_account = str_to_bytes('dummy_contact') tx_account = str_to_bytes('dummy_user') nick = str_to_bytes('dummy_nick') tx_fingerprint = bytes(32) rx_fingerprint = bytes(32) logging_bytes = bool_to_bytes(False) file_r_bytes = bool_to_bytes(False) notify_bytes = bool_to_bytes(False) return rx_account + tx_account + nick \ + tx_fingerprint + rx_fingerprint \ + logging_bytes + file_r_bytes + notify_bytes
def deliver_contact_data( header: bytes, # Key type (x448, PSK) nick: str, # Contact's nickname onion_pub_key: bytes, # Public key of contact's v3 Onion Service tx_mk: bytes, # Message key for outgoing messages rx_mk: bytes, # Message key for incoming messages tx_hk: bytes, # Header key for outgoing messages rx_hk: bytes, # Header key for incoming messages queues: 'QueueDict', # Dictionary of multiprocessing queues settings: 'Settings', # Settings object ) -> None: """Deliver contact data to Destination Computer.""" c_code = blake2b(onion_pub_key, digest_size=CONFIRM_CODE_LENGTH) command = (header + onion_pub_key + tx_mk + rx_mk + tx_hk + rx_hk + str_to_bytes(nick)) queue_command(command, settings, queues) while True: purp_code = ask_confirmation_code("Receiver") if purp_code == c_code.hex(): break elif purp_code == "": phase("Resending contact data", head=2) queue_command(command, settings, queues) phase(DONE) print_on_previous_line(reps=5) else: m_print("Incorrect confirmation code.", head=1) print_on_previous_line(reps=4, delay=2)
def test_valid_import(self): file_name = str_to_bytes('testfile.txt') data = file_name + os.urandom(1000) compressed = zlib.compress(data, level=9) key = os.urandom(32) key_b58 = b58encode(key) packet = IMPORTED_FILE_CT_HEADER + encrypt_and_sign(compressed, key) ts = datetime.datetime.now() window_list = WindowList(nicks=['local']) o_input = builtins.input input_list = ['2QJL5gVSPEjMTaxWPfYkzG9UJxzZDNSx6PPeVWdzS5CFN7knZy', key_b58] gen = iter(input_list) def mock_input(_): return str(next(gen)) builtins.input = mock_input # Setup self.assertIsNone(process_imported_file(ts, packet, window_list)) self.assertTrue(os.path.isfile(f"{DIR_IMPORTED}/testfile.txt")) # Teardown builtins.input = o_input shutil.rmtree(f'{DIR_IMPORTED}/')
def test_successful_storage_of_file(self, _): compressed = zlib.compress(str_to_bytes("test_file.txt") + b'file_data', level=COMPRESSION_LEVEL) file_data = encrypt_and_sign(compressed, self.file_key) self.assertIsNone( process_file(self.ts, self.account, file_data, *self.args))
def test_non_printable_name_raises_fr(self, _): compressed = zlib.compress(str_to_bytes("file\x01") + b'file_data', level=COMPRESSION_LEVEL) file_data = encrypt_and_sign(compressed, self.file_key) self.assert_fr("Error: Name of file from Alice was invalid.", process_file, self.ts, self.account, file_data, *self.args)
def dump_k(self) -> bytes: """Return keyset data as constant length byte string.""" return str_to_bytes(self.rx_account) \ + self.tx_key \ + self.rx_key \ + self.tx_hek \ + self.rx_hek \ + int_to_bytes(self.tx_harac) \ + int_to_bytes(self.rx_harac)
def test_slash_in_name_raises_se(self, _: Any) -> None: compressed = zlib.compress(str_to_bytes("Alice/file.txt") + b'file_data', level=COMPRESSION_LEVEL) file_data = encrypt_and_sign(compressed, self.file_key) self.assert_se("Error: Name of file from Alice was invalid.", process_file, self.ts, self.account, file_data, *self.args)
def test_valid_import(self): # Setup file_name = str_to_bytes('testfile.txt') data = file_name + os.urandom(1000) compressed = zlib.compress(data, level=COMPRESSION_LEVEL) packet = IMPORTED_FILE_HEADER + encrypt_and_sign(compressed, self.key) # Test self.assertIsNone(process_imported_file(self.ts, packet, self.window_list, self.settings)) self.assertTrue(os.path.isfile(f"{DIR_IMPORTED}testfile.txt"))
def test_invalid_name_raises_fr(self): # Setup file_name = str_to_bytes('\x01testfile.txt') data = file_name + os.urandom(1000) compressed = zlib.compress(data, level=COMPRESSION_LEVEL) packet = IMPORTED_FILE_HEADER + encrypt_and_sign(compressed, self.key) # Test self.assertFR("Error: Received file had an invalid name.", process_imported_file, self.ts, packet, self.window_list, self.settings)
def queue_delayer(): time.sleep(0.1) # Queue local key packet local_key_packet = LOCAL_KEY_PACKET_HEADER + encrypt_and_sign( local_key + local_hek + conf_code, key=kek) queues[LOCAL_KEY_PACKET_HEADER].put( (datetime.datetime.now(), local_key_packet)) time.sleep(0.1) # Queue screen clearing command queue_packet(tx_key, tx_hek, INITIAL_HARAC, CLEAR_SCREEN_HEADER) # Queue message that goes to buffer queue_packet(tx_key, tx_hek, INITIAL_HARAC, PRIVATE_MESSAGE_HEADER + b'Hi Bob', b'*****@*****.**') # Queue public key for Bob public_key_packet = PUBLIC_KEY_PACKET_HEADER + KEY_LENGTH * b'a' + ORIGIN_CONTACT_HEADER + b'*****@*****.**' queues[PUBLIC_KEY_PACKET_HEADER].put( (datetime.datetime.now(), public_key_packet)) time.sleep(0.1) # Queue X25519 keyset for Bob command = KEY_EX_X25519_HEADER + 4 * ( KEY_LENGTH * b'a') + b'*****@*****.**' + US_BYTE + b'Bob' queue_packet(hash_chain(tx_key), tx_hek, INITIAL_HARAC + 1, command) # Queue window selection packet command = WINDOW_SELECT_HEADER + b'*****@*****.**' queue_packet(hash_chain(hash_chain(tx_key)), tx_hek, INITIAL_HARAC + 2, command) # Queue message that is displayed directly packet = b'Hi again, Bob' queue_packet(tx_key, tx_hek, INITIAL_HARAC, packet, b'*****@*****.**') # Queue file window selection command command = WINDOW_SELECT_HEADER + WIN_TYPE_FILE.encode() queue_packet(hash_chain(hash_chain(hash_chain(tx_key))), tx_hek, INITIAL_HARAC + 3, command) # Queue imported file packet file_data = str_to_bytes('testfile') + 500 * b'a' compressed = zlib.compress(file_data, level=COMPRESSION_LEVEL) packet = IMPORTED_FILE_HEADER + encrypt_and_sign(compressed, key=fdk) queues[IMPORTED_FILE_HEADER].put((datetime.datetime.now(), packet)) time.sleep(0.1) # Queue exit message to break loop queues[UNITTEST_QUEUE].put(EXIT) time.sleep(0.1)
def generate_dummy_keyset() -> bytes: """Generate bytestring for dummy keyset.""" tx_account = str_to_bytes('dummy_contact') tx_key = bytes(32) rx_key = bytes(32) tx_hek = bytes(32) rx_hek = bytes(32) tx_harac = int_to_bytes(0) rx_harac = int_to_bytes(0) return tx_account + tx_key + rx_key + tx_hek + rx_hek + tx_harac + rx_harac
def setUp(self): self.ts = datetime.fromtimestamp(1502750000) self.window_list = WindowList(nicks=[LOCAL_ID]) self.contact_list = ContactList() self.key_list = KeyList() self.settings = Settings() self.packet = (nick_to_pub_key("Alice") + SYMMETRIC_KEY_LENGTH * b'\x01' + bytes(SYMMETRIC_KEY_LENGTH) + SYMMETRIC_KEY_LENGTH * b'\x02' + bytes(SYMMETRIC_KEY_LENGTH) + str_to_bytes('Alice')) self.args = self.packet, self.ts, self.window_list, self.contact_list, self.key_list, self.settings
def setUp(self) -> None: """Pre-test actions.""" self.unit_test_dir = cd_unit_test() self.ts = datetime.now() self.packet = b'' self.file_keys = dict() self.file_buf = dict() self.contact_list = ContactList(nicks=['Alice']) self.window_list = WindowList() self.file_key = SYMMETRIC_KEY_LENGTH*b'a' self.settings = Settings() self.compressed = zlib.compress(str_to_bytes("test_file.txt") + b'file_data', level=COMPRESSION_LEVEL) self.args = self.file_keys, self.file_buf, self.contact_list, self.window_list, self.settings
def test_successful_storage_during_traffic_masking(self, _: Any) -> None: # Setup self.settings.traffic_masking = True self.window_list.active_win = self.window_list.get_window(nick_to_pub_key('Bob')) compressed = zlib.compress(str_to_bytes("testfile.txt") + b'file_data', level=COMPRESSION_LEVEL) file_data = encrypt_and_sign(compressed, self.file_key) self.assertIsNone(process_file(self.ts, self.account, file_data, *self.args)) self.assertEqual(self.window_list.get_window(nick_to_pub_key('Bob')).message_log[0][1], "Stored file from Alice as 'testfile.txt'.") self.assertTrue(os.path.isfile(f'{DIR_RECV_FILES}Alice/testfile.txt'))
def serialize_c(self) -> bytes: """Return contact data as a constant length byte string. This function serializes the contact's data into a byte string that has the exact length of 3*32 + 4*1 + 1024 = 1124 bytes. The length is guaranteed regardless of the content or length of the attributes' values, including the contact's nickname. The purpose of the constant length serialization is to hide any metadata about the contact the ciphertext length of the contact database would reveal. """ return (self.onion_pub_key + self.tx_fingerprint + self.rx_fingerprint + self.kex_status + bool_to_bytes(self.log_messages) + bool_to_bytes(self.file_reception) + bool_to_bytes(self.notifications) + str_to_bytes(self.nick))
def export_file(settings: 'Settings', nh_queue: 'Queue') -> None: """Encrypt and export file to NH. This is a faster method to send large files. It is used together with file import (/fi) command that uploads ciphertext to RxM for RxM-side decryption. Key is generated automatically so that bad passwords selected by users do not affect security of ciphertexts. """ if settings.session_traffic_masking: raise FunctionReturn( "Error: Command is disabled during traffic masking.") path = ask_path_gui("Select file to export...", settings, get_file=True) name = path.split('/')[-1] data = bytearray() data.extend(str_to_bytes(name)) if not os.path.isfile(path): raise FunctionReturn("Error: File not found.") if os.path.getsize(path) == 0: raise FunctionReturn("Error: Target file is empty.") phase("Reading data") with open(path, 'rb') as f: data.extend(f.read()) phase(DONE) phase("Compressing data") comp = bytes(zlib.compress(bytes(data), level=COMPRESSION_LEVEL)) phase(DONE) phase("Encrypting data") file_key = csprng() file_ct = encrypt_and_sign(comp, key=file_key) phase(DONE) phase("Exporting data") queue_to_nh(EXPORTED_FILE_HEADER + file_ct, settings, nh_queue) phase(DONE) print_key(f"Decryption key for file '{name}':", file_key, settings, no_split=True, file_key=True)
def store_settings(self) -> None: """Store settings to encrypted database.""" attribute_list = [self.__getattribute__(k) for k in self.key_list] # Convert attributes into constant length byte string pt_bytes = b'' for a in attribute_list: if isinstance(a, bool): pt_bytes += bool_to_bytes(a) elif isinstance(a, int): pt_bytes += int_to_bytes(a) elif isinstance(a, float): pt_bytes += double_to_bytes(a) elif isinstance(a, str): pt_bytes += str_to_bytes(a) else: raise CriticalError("Invalid attribute type in settings.") ct_bytes = encrypt_and_sign(pt_bytes, self.master_key.master_key) ensure_dir(f'{DIR_USER_DATA}/') with open(self.file_name, 'wb+') as f: f.write(ct_bytes)
def export_file(settings: 'Settings', gateway: 'Gateway'): """Encrypt and export file to NH. This is a faster method of sending large files. It is used together with '/fi' import_file command that loads ciphertext to RxM for later decryption. Key is generated automatically so that bad passwords by users do not affect security of ciphertexts. As use of this command reveals use of TFC, it is disabled during trickle connection. """ if settings.session_trickle: raise FunctionReturn("Command disabled during trickle connection.") path = ask_path_gui("Select file to export...", settings, get_file=True) name = path.split('/')[-1] data = bytearray() data.extend(str_to_bytes(name)) if not os.path.isfile(path): raise FunctionReturn("Error: File not found.") if os.path.getsize(path) == 0: raise FunctionReturn("Error: Target file is empty. No file was sent.") phase("Reading data") with open(path, 'rb') as f: data.extend(f.read()) phase("Done") phase("Compressing data") comp = bytes(zlib.compress(bytes(data), level=9)) phase("Done") phase("Encrypting data") file_key = keygen() file_ct = encrypt_and_sign(comp, key=file_key) phase("Done") phase("Exporting data") transmit(EXPORTED_FILE_CT_HEADER + file_ct, settings, gateway) phase("Done") box_print([f"Decryption key for file {name}:", '', b58encode(file_key)], head=1, tail=1)
def serialize_g(self) -> bytes: """Return group data as a constant length bytestring. This function serializes the group's data into a bytestring that always has a constant length. The exact length depends on the attribute `max_number_of_group_members` of TFC's Settings object. With the default setting of 50 members per group, the length of the serialized data is 1024 + 4 + 2*1 + 50*32 = 2630 bytes The purpose of the constant length serialization is to hide any metadata the ciphertext length of the group database could reveal. """ members = self.get_list_of_member_pub_keys() number_of_dummies = self.settings.max_number_of_group_members - len( self.members) members += number_of_dummies * [onion_address_to_pub_key(DUMMY_MEMBER)] member_bytes = b''.join(members) return (str_to_bytes(self.name) + self.group_id + bool_to_bytes(self.log_messages) + bool_to_bytes(self.notifications) + member_bytes)
def test_invalid_name_raises_fr(self): # Setup file_name = str_to_bytes('\x01testfile.txt') data = file_name + os.urandom(1000) compressed = zlib.compress(data, level=9) key = os.urandom(32) key_b58 = b58encode(key) packet = IMPORTED_FILE_CT_HEADER + encrypt_and_sign(compressed, key) ts = datetime.datetime.now() window_list = WindowList(nicks=['local']) o_input = builtins.input input_list = ['2QJL5gVSPEjMTaxWPfYkzG9UJxzZDNSx6PPeVWdzS5CFN7knZy', key_b58] gen = iter(input_list) def mock_input(_): return str(next(gen)) builtins.input = mock_input # Test self.assertFR("Received file had an invalid name.", process_imported_file, ts, packet, window_list) # Teardown builtins.input = o_input
def write_log_entry(assembly_packet: bytes, account: str, settings: 'Settings', master_key: 'MasterKey', origin: bytes = ORIGIN_USER_HEADER) -> None: """Add assembly packet to encrypted logfile. This method of logging allows reconstruction of conversation while protecting the metadata about the length of messages other log file formats would reveal. TxM can only log sent messages. This is not useful for recalling conversations but serves an important role in audit of RxM-side logs, where malware could have substituted logged data on RxM. To protect possibly sensitive files that must not be logged, only placeholder data is logged about them. This helps hiding the amount of communication comparison with log file size and output packet count would otherwise reveal. :param assembly_packet: Assembly packet to log :param account: Recipient's account (UID) :param origin: Direction of logged packet :param settings: Settings object :param master_key: Master key object :return: None """ unix_timestamp = int(time.time()) timestamp_bytes = struct.pack('<L', unix_timestamp) encoded_account = str_to_bytes(account) pt_bytes = timestamp_bytes + origin + encoded_account + assembly_packet ct_bytes = encrypt_and_sign(pt_bytes, key=master_key.master_key) ensure_dir(f'{DIR_USER_DATA}/') file_name = f'{DIR_USER_DATA}/{settings.software_operation}_logs' with open(file_name, 'ab+') as f: f.write(ct_bytes)
def test_bytes_to_str(self): encoded = str_to_bytes('test') self.assertEqual(bytes_to_str(encoded), 'test')
def test_str_to_bytes(self): encoded = str_to_bytes('test') self.assertIsInstance(encoded, bytes) self.assertEqual(len(encoded), PADDED_UTF32_STR_LENGTH)
def send_file(path: str, settings: 'Settings', queues: 'QueueDict', window: 'TxWindow' ) -> None: """Send file to window members in a single transmission. This is the default mode for file transmission, used when traffic masking is not enabled. The file is loaded and compressed before it is encrypted. The encrypted file is then exported to Networked Computer along with a list of Onion Service public keys (members in window) of all recipients to whom the Relay Program will multi-cast the file to. Once the file ciphertext has been exported, this function will multi-cast the file decryption key to each recipient inside an automated key delivery message that uses a special FILE_KEY_HEADER in place of standard PRIVATE_MESSAGE_HEADER. To know for which file ciphertext the key is for, an identifier must be added to the key delivery message. The identifier in this case is the BLAKE2b digest of the ciphertext itself. The reason of using the digest as the identifier is, it authenticates both the ciphertext and its origin. To understand this, consider the following attack scenario: Let the file ciphertext identifier be just a random 32-byte value "ID". 1) Alice sends Bob and Chuck (a malicious common peer) a file ciphertext and identifier CT|ID (where | denotes concatenation). 2) Chuck who has compromised Bob's Networked Computer interdicts the CT|ID from Alice. 3) Chuck decrypts CT in his end, makes edits to the plaintext PT to create PT'. 4) Chuck re-encrypts PT' with the same symmetric key to produce CT'. 5) Chuck re-uses the ID and produces CT'|ID. 6) Chuck uploads the CT'|ID to Bob's Networked Computer and replaces the interdicted CT|ID with it. 7) When Bob' Receiver Program receives the automated key delivery message from Alice, his Receiver program uses the bundled ID to identify the key is for CT'. 8) Bob's Receiver decrypts CT' using the newly received key and obtains Chuck's PT', that appears to come from Alice. Now, consider a situation where the ID is instead calculated ID = BLAKE2b(CT), if Chuck edits the PT, the CT' will by definition be different from CT, and the BLAKE2b digest will also be different. In order to make Bob decrypt CT', Chuck needs to also change the hash in Alice's key delivery message, which means Chuck needs to create an existential forgery of the TFC message. Since the Poly1305 tag prevents this, the calculated ID is enough to authenticate the ciphertext. If Chuck attempts to send their own key delivery message, Chuck's own Onion Service public key used to identify the TFC message key (decryption key for the key delivery message) will be permanently associated with the file hash, so if they inject a file CT, and Bob has decided to enable file reception for Chuck, the file CT will appear to come from Chuck, and not from Alice. From the perspective of Bob, it's as if Chuck had dropped Alice's file and sent him another file instead. """ from src.transmitter.windows import MockWindow # Avoid circular import if settings.traffic_masking: raise FunctionReturn("Error: Command is disabled during traffic masking.", head_clear=True) name = path.split('/')[-1] data = bytearray() data.extend(str_to_bytes(name)) if not os.path.isfile(path): raise FunctionReturn("Error: File not found.", head_clear=True) if os.path.getsize(path) == 0: raise FunctionReturn("Error: Target file is empty.", head_clear=True) phase("Reading data") with open(path, 'rb') as f: data.extend(f.read()) phase(DONE) print_on_previous_line(flush=True) phase("Compressing data") comp = bytes(zlib.compress(bytes(data), level=COMPRESSION_LEVEL)) phase(DONE) print_on_previous_line(flush=True) phase("Encrypting data") file_key = csprng() file_ct = encrypt_and_sign(comp, file_key) ct_hash = blake2b(file_ct) phase(DONE) print_on_previous_line(flush=True) phase("Exporting data") no_contacts = int_to_bytes(len(window)) ser_contacts = b''.join([c.onion_pub_key for c in window]) file_packet = FILE_DATAGRAM_HEADER + no_contacts + ser_contacts + file_ct queue_to_nc(file_packet, queues[RELAY_PACKET_QUEUE]) key_delivery_msg = base64.b85encode(ct_hash + file_key).decode() for contact in window: queue_message(user_input=UserInput(key_delivery_msg, MESSAGE), window =MockWindow(contact.onion_pub_key, [contact]), settings =settings, queues =queues, header =FILE_KEY_HEADER, log_as_ph =True) phase(DONE) print_on_previous_line(flush=True) m_print(f"Sent file '{name}' to {window.type_print} {window.name}.")
def test_str_to_bytes(self): encoded = str_to_bytes('test') self.assertIsInstance(encoded, bytes) self.assertEqual(len(encoded), 1024)