def process_long_header(self, packet: bytes, packet_ct: Optional[bytes] = None ) -> None: """Process first packet of long transmission.""" if self.long_active: self.add_masking_packet_to_log_file(increase=len(self.assembly_pt_list)) if self.type == FILE: self.new_file_packet() try: lh, no_p_bytes, time_bytes, size_bytes, packet \ = separate_headers(packet, [ASSEMBLY_PACKET_HEADER_LENGTH] + 3*[ENCODED_INTEGER_LENGTH]) self.packets = bytes_to_int(no_p_bytes) # added by transmitter.packet.split_to_assembly_packets self.time = str(timedelta(seconds=bytes_to_int(time_bytes))) self.size = readable_size(bytes_to_int(size_bytes)) self.name = packet.split(US_BYTE)[0].decode() packet = lh + packet m_print([f'Receiving file from {self.contact.nick}:', f'{self.name} ({self.size})', f'ETA {self.time} ({self.packets} packets)'], bold=True) except (struct.error, UnicodeError, ValueError): self.add_masking_packet_to_log_file() raise FunctionReturn("Error: Received file packet had an invalid header.") self.assembly_pt_list = [packet] self.long_active = True self.is_complete = False if packet_ct is not None: self.log_ct_list = [packet_ct]
def receiver_loop(queues: Dict[bytes, 'Queue'], gateway: 'Gateway', unittest: bool = False ) -> None: """Decode received packets and forward them to packet queues.""" gateway_queue = queues[GATEWAY_QUEUE] while True: with ignored(EOFError, KeyboardInterrupt): if gateway_queue.qsize() == 0: time.sleep(0.01) _, packet = gateway_queue.get() try: packet = gateway.detect_errors(packet) except FunctionReturn: continue header, ts_bytes, payload = separate_headers(packet, [DATAGRAM_HEADER_LENGTH, DATAGRAM_TIMESTAMP_LENGTH]) try: ts = datetime.strptime(str(bytes_to_int(ts_bytes)), "%Y%m%d%H%M%S%f") except (ValueError, struct.error): m_print("Error: Failed to decode timestamp in the received packet.", head=1, tail=1) continue if header in [MESSAGE_DATAGRAM_HEADER, FILE_DATAGRAM_HEADER, COMMAND_DATAGRAM_HEADER, LOCAL_KEY_DATAGRAM_HEADER]: queues[header].put((ts, payload)) if unittest: break
def key_ex_psk_tx(packet: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', key_list: 'KeyList', settings: 'Settings') -> None: """Add contact and Tx-PSKs.""" onion_pub_key, tx_mk, _, tx_hk, _, nick_bytes \ = separate_headers(packet, [ONION_SERVICE_PUBLIC_KEY_LENGTH] + 4*[SYMMETRIC_KEY_LENGTH]) try: nick = bytes_to_str(nick_bytes) except (struct.error, UnicodeError): raise SoftError("Error: Received invalid contact data") contact_list.add_contact(onion_pub_key, nick, bytes(FINGERPRINT_LENGTH), bytes(FINGERPRINT_LENGTH), KEX_STATUS_NO_RX_PSK, settings.log_messages_by_default, settings.accept_files_by_default, settings.show_notifications_by_default) # The Rx-side keys are set as null-byte strings to indicate they have not # been added yet. The zero-keys do not allow existential forgeries as # `decrypt_assembly_packet`does not allow the use of zero-keys for decryption. key_list.add_keyset(onion_pub_key=onion_pub_key, tx_mk=tx_mk, rx_mk=bytes(SYMMETRIC_KEY_LENGTH), tx_hk=tx_hk, rx_hk=bytes(SYMMETRIC_KEY_LENGTH)) c_code = blake2b(onion_pub_key, digest_size=CONFIRM_CODE_LENGTH) message = f"Added Tx-side PSK for {nick} ({pub_key_to_short_address(onion_pub_key)})." cmd_win = window_list.get_command_window() cmd_win.add_new(ts, message) m_print([message, f"Confirmation code (to Transmitter): {c_code.hex()}"], box=True)
def key_ex_ecdhe(packet: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', key_list: 'KeyList', settings: 'Settings') -> None: """Add contact and symmetric keys derived from X448 shared key.""" onion_pub_key, tx_mk, rx_mk, tx_hk, rx_hk, nick_bytes \ = separate_headers(packet, [ONION_SERVICE_PUBLIC_KEY_LENGTH] + 4*[SYMMETRIC_KEY_LENGTH]) try: nick = bytes_to_str(nick_bytes) except (struct.error, UnicodeError): raise FunctionReturn("Error: Received invalid contact data") contact_list.add_contact(onion_pub_key, nick, bytes(FINGERPRINT_LENGTH), bytes(FINGERPRINT_LENGTH), KEX_STATUS_NONE, settings.log_messages_by_default, settings.accept_files_by_default, settings.show_notifications_by_default) key_list.add_keyset(onion_pub_key, tx_mk, rx_mk, tx_hk, rx_hk) message = f"Successfully added {nick}." local_win = window_list.get_local_window() local_win.add_new(ts, message) c_code = blake2b(onion_pub_key, digest_size=CONFIRM_CODE_LENGTH) m_print([message, f"Confirmation code (to Transmitter): {c_code.hex()}"], box=True)
def add_complete_message_to_message_list( timestamp: bytes, onion_pub_key: bytes, group_msg_id: bytes, packet: 'Packet', message_list: List[MsgTuple], window: Union['TxWindow', 'RxWindow']) -> bytes: """Add complete log file message to `message_list`.""" whisper_byte, header, message = separate_headers( packet.assemble_message_packet(), [WHISPER_FIELD_LENGTH, MESSAGE_HEADER_LENGTH]) whisper = bytes_to_bool(whisper_byte) if header == PRIVATE_MESSAGE_HEADER and window.type == WIN_TYPE_CONTACT: message_list.append((bytes_to_timestamp(timestamp), message.decode(), onion_pub_key, packet.origin, whisper, False)) elif header == GROUP_MESSAGE_HEADER and window.type == WIN_TYPE_GROUP: purp_group_id, message = separate_header(message, GROUP_ID_LENGTH) if window.group is not None and purp_group_id != window.group.group_id: return group_msg_id purp_msg_id, message = separate_header(message, GROUP_MSG_ID_LENGTH) if packet.origin == ORIGIN_USER_HEADER: if purp_msg_id == group_msg_id: return group_msg_id group_msg_id = purp_msg_id message_list.append((bytes_to_timestamp(timestamp), message.decode(), onion_pub_key, packet.origin, whisper, False)) return group_msg_id
def key_ex_psk_tx(packet: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', key_list: 'KeyList', settings: 'Settings') -> None: """Add contact and Tx-PSKs.""" onion_pub_key, tx_mk, _, tx_hk, _, nick_bytes \ = separate_headers(packet, [ONION_SERVICE_PUBLIC_KEY_LENGTH] + 4*[SYMMETRIC_KEY_LENGTH]) try: nick = bytes_to_str(nick_bytes) except (struct.error, UnicodeError): raise FunctionReturn("Error: Received invalid contact data") contact_list.add_contact(onion_pub_key, nick, bytes(FINGERPRINT_LENGTH), bytes(FINGERPRINT_LENGTH), KEX_STATUS_NO_RX_PSK, settings.log_messages_by_default, settings.accept_files_by_default, settings.show_notifications_by_default) # The Rx-side keys are set as null-byte strings to indicate they have not # been added yet. The zero-keys do not allow existential forgeries as # `decrypt_assembly_packet`does not allow the use of zero-keys for decryption. key_list.add_keyset(onion_pub_key=onion_pub_key, tx_mk=tx_mk, rx_mk=bytes(SYMMETRIC_KEY_LENGTH), tx_hk=tx_hk, rx_hk=bytes(SYMMETRIC_KEY_LENGTH)) message = f"Added Tx-side PSK for {nick} ({pub_key_to_short_address(onion_pub_key)})." local_win = window_list.get_local_window() local_win.add_new(ts, message) m_print(message, bold=True, tail_clear=True, delay=1)
def _load_keys(self) -> None: """Load KeySets from the encrypted database. This function first reads and decrypts the database content. It then splits the plaintext into a list of 176-byte blocks. Each block contains the serialized data of one KeySet. Next, the function will remove from the list all dummy KeySets (that start with the `dummy_id` byte string). The function will then populate the `self.keysets` list with KeySet objects, the data of which is sliced and decoded from the dummy-free blocks. """ pt_bytes = self.database.load_database() blocks = split_byte_string(pt_bytes, item_len=KEYSET_LENGTH) df_blocks = [b for b in blocks if not b.startswith(self.dummy_id)] for block in df_blocks: if len(block) != KEYSET_LENGTH: raise CriticalError("Invalid data in key database.") onion_pub_key, tx_mk, rx_mk, tx_hk, rx_hk, tx_harac_bytes, rx_harac_bytes \ = separate_headers(block, [ONION_SERVICE_PUBLIC_KEY_LENGTH] + 4*[SYMMETRIC_KEY_LENGTH] + [HARAC_LENGTH]) self.keysets.append( KeySet(onion_pub_key=onion_pub_key, tx_mk=tx_mk, rx_mk=rx_mk, tx_hk=tx_hk, rx_hk=rx_hk, tx_harac=bytes_to_int(tx_harac_bytes), rx_harac=bytes_to_int(rx_harac_bytes), store_keys=self.store_keys))
def new_file( ts: 'datetime', # Timestamp of received packet packet: bytes, # Sender of file and file ciphertext file_keys: Dict[bytes, bytes], # Dictionary for file decryption keys file_buf: Dict[bytes, Tuple['datetime', bytes]], # Dictionary for cached file ciphertexts contact_list: 'ContactList', # ContactList object window_list: 'WindowList', # WindowList object settings: 'Settings' # Settings object ) -> None: """Validate received file and process or cache it.""" onion_pub_key, _, file_ct = separate_headers( packet, [ONION_SERVICE_PUBLIC_KEY_LENGTH, ORIGIN_HEADER_LENGTH]) if not contact_list.has_pub_key(onion_pub_key): raise SoftError("File from an unknown account.", output=False) contact = contact_list.get_contact_by_pub_key(onion_pub_key) if not contact.file_reception: raise SoftError( f"Alert! Discarded file from {contact.nick} as file reception for them is disabled.", bold=True) k = onion_pub_key + blake2b(file_ct) # Dictionary key if k in file_keys: decryption_key = file_keys[k] process_file(ts, onion_pub_key, file_ct, decryption_key, contact_list, window_list, settings) file_keys.pop(k) else: file_buf[k] = (ts, file_ct)
def load_master_key(self) -> bytes: """Derive the master key from password and salt. Load the salt, hash, and key derivation settings from the login database. Derive the purported master key from the salt and entered password. If the BLAKE2b hash of derived master key matches the hash in the login database, accept the derived master key. """ database_data = self.database.load_database() if len(database_data) != MASTERKEY_DB_SIZE: raise CriticalError(f"Invalid {self.file_name} database size.") salt, key_hash, time_bytes, memory_bytes, parallelism_bytes \ = separate_headers(database_data, [ARGON2_SALT_LENGTH, BLAKE2_DIGEST_LENGTH, ENCODED_INTEGER_LENGTH, ENCODED_INTEGER_LENGTH]) time_cost = bytes_to_int(time_bytes) memory_cost = bytes_to_int(memory_bytes) parallelism = bytes_to_int(parallelism_bytes) while True: password = MasterKey.get_password() phase("Deriving master key", head=2, offset=len("Password correct")) purp_key = argon2_kdf(password, salt, time_cost, memory_cost, parallelism) if blake2b(purp_key) == key_hash: phase("Password correct", done=True, delay=1) clear_screen() return purp_key phase("Invalid password", done=True, delay=1) print_on_previous_line(reps=5)
def add_onion_data(command: bytes, queues: 'QueueDict') -> None: """Add Onion Service data. Separate onion service private key and public keys for pending/existing contacts and add them as contacts. The ONION_KEY_QUEUE is read by relay.onion.onion_service() """ os_private_key, confirmation_code, allow_req_byte, no_pending_bytes, ser_pub_keys \ = separate_headers(command, [ONION_SERVICE_PRIVATE_KEY_LENGTH, CONFIRM_CODE_LENGTH, ENCODED_BOOLEAN_LENGTH, ENCODED_INTEGER_LENGTH]) no_pending = bytes_to_int(no_pending_bytes) public_key_list = split_byte_string(ser_pub_keys, ONION_SERVICE_PUBLIC_KEY_LENGTH) pending_public_keys = public_key_list[:no_pending] existing_public_keys = public_key_list[no_pending:] for onion_pub_key in pending_public_keys: add_contact(onion_pub_key, False, queues) for onion_pub_key in existing_public_keys: add_contact(onion_pub_key, True, queues) manage_contact_req(allow_req_byte, queues, notify=False) queues[ONION_KEY_QUEUE].put((os_private_key, confirmation_code))
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 assemble_and_store_file(self, ts: 'datetime', onion_pub_key: bytes, window_list: 'WindowList') -> None: """Assemble file packet and store it.""" padded = b''.join( [p[ASSEMBLY_PACKET_HEADER_LENGTH:] for p in self.assembly_pt_list]) payload = rm_padding_bytes(padded) no_fields = 3 if len(self.assembly_pt_list) > 1 else 2 *_, payload = separate_headers(payload, no_fields * [ENCODED_INTEGER_LENGTH]) process_assembled_file(ts, payload, onion_pub_key, self.contact.nick, self.settings, window_list)
def access_logs(window: Union['TxWindow', 'RxWindow'], contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', master_key: 'MasterKey', msg_to_load: int = 0, export: bool = False) -> None: """\ Load 'msg_to_load' last messages from log database and display or export them. The default value of zero for `msg_to_load` means all messages for the window will be retrieved from the log database. """ file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs' packet_list = PacketList(settings, contact_list) message_list = [] # type: List[MsgTuple] group_msg_id = b'' check_log_file_exists(file_name) message_log = MessageLog(file_name, master_key.master_key) for log_entry in message_log: onion_pub_key, timestamp, origin, assembly_packet \ = separate_headers(log_entry, [ONION_SERVICE_PUBLIC_KEY_LENGTH, TIMESTAMP_LENGTH, ORIGIN_HEADER_LENGTH]) if window.type == WIN_TYPE_CONTACT and onion_pub_key != window.uid: continue packet = packet_list.get_packet(onion_pub_key, origin, MESSAGE, log_access=True) try: packet.add_packet(assembly_packet) except SoftError: continue if not packet.is_complete: continue group_msg_id = add_complete_message_to_message_list( timestamp, onion_pub_key, group_msg_id, packet, message_list, window) message_log.close_database() print_logs(message_list[-msg_to_load:], export, msg_to_load, window, contact_list, group_list, settings)
def _load_contacts(self) -> None: """Load contacts from the encrypted database. This function first reads and decrypts the database content. It then splits the plaintext into a list of 1124-byte blocks: each block contains the serialized data of one contact. Next, the function will remove from the list all dummy contacts (that start with dummy contact's public key). The function will then populate the `self.contacts` list with Contact objects, the data of which is sliced and decoded from the dummy-free blocks. """ with open(self.file_name, 'rb') as f: ct_bytes = f.read() pt_bytes = auth_and_decrypt(ct_bytes, self.master_key.master_key, database=self.file_name) blocks = split_byte_string(pt_bytes, item_len=CONTACT_LENGTH) df_blocks = [ b for b in blocks if not b.startswith(self.dummy_contact.onion_pub_key) ] for block in df_blocks: if len(block) != CONTACT_LENGTH: raise CriticalError("Invalid data in contact database.") (onion_pub_key, tx_fingerprint, rx_fingerprint, kex_status_byte, log_messages_byte, file_reception_byte, notifications_byte, nick_bytes) = separate_headers( block, [ONION_SERVICE_PUBLIC_KEY_LENGTH] + 2 * [FINGERPRINT_LENGTH] + [KEX_STATUS_LENGTH] + 3 * [ENCODED_BOOLEAN_LENGTH]) self.contacts.append( Contact(onion_pub_key=onion_pub_key, tx_fingerprint=tx_fingerprint, rx_fingerprint=rx_fingerprint, kex_status=kex_status_byte, log_messages=bytes_to_bool(log_messages_byte), file_reception=bytes_to_bool(file_reception_byte), notifications=bytes_to_bool(notifications_byte), nick=bytes_to_str(nick_bytes)))
def process_short_header(self, packet: bytes, packet_ct: Optional[bytes] = None ) -> None: """Process short packet.""" if self.long_active: self.add_masking_packet_to_log_file(increase=len(self.assembly_pt_list)) if self.type == FILE: self.new_file_packet() sh, _, packet = separate_headers(packet, [ASSEMBLY_PACKET_HEADER_LENGTH] + [2*ENCODED_INTEGER_LENGTH]) packet = sh + packet self.assembly_pt_list = [packet] self.long_active = False self.is_complete = True if packet_ct is not None: self.log_ct_list = [packet_ct]
def check_packet_fate(entries_to_keep: List[bytes], packet: 'Packet', removed: bool, selector: bytes) -> bool: """Check whether the packet should be kept.""" _, header, message = separate_headers( packet.assemble_message_packet(), [WHISPER_FIELD_LENGTH, MESSAGE_HEADER_LENGTH]) if header == PRIVATE_MESSAGE_HEADER: entries_to_keep.extend(packet.log_ct_list) packet.clear_assembly_packets() elif header == GROUP_MESSAGE_HEADER: group_id, _ = separate_header(message, GROUP_ID_LENGTH) if group_id == selector: removed = True else: entries_to_keep.extend(packet.log_ct_list) packet.clear_assembly_packets() return removed
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 _load_groups(self) -> None: """Load groups from the encrypted database. The function first reads, authenticates and decrypts the group database data. Next, it slices and decodes the header values that help the function to properly de-serialize the database content. The function then removes dummy groups based on header data. Next, the function updates the group database settings if necessary. It then splits group data based on header data into blocks, which are further sliced, and processed if necessary, to obtain data required to create Group objects. Finally, if needed, the function will update the group database content. """ pt_bytes = self.database.load_database() # Slice and decode headers group_db_headers, pt_bytes = separate_header(pt_bytes, GROUP_DB_HEADER_LENGTH) padding_for_group_db, padding_for_members, number_of_groups, members_in_largest_group \ = list(map(bytes_to_int, split_byte_string(group_db_headers, ENCODED_INTEGER_LENGTH))) # Slice dummy groups bytes_per_group = GROUP_STATIC_LENGTH + padding_for_members * ONION_SERVICE_PUBLIC_KEY_LENGTH dummy_data_len = (padding_for_group_db - number_of_groups) * bytes_per_group group_data = pt_bytes[:-dummy_data_len] update_db = self._check_db_settings(number_of_groups, members_in_largest_group) blocks = split_byte_string(group_data, item_len=bytes_per_group) all_pub_keys = self.contact_list.get_list_of_pub_keys() dummy_pub_key = onion_address_to_pub_key(DUMMY_MEMBER) # Deserialize group objects for block in blocks: if len(block) != bytes_per_group: raise CriticalError("Invalid data in group database.") name_bytes, group_id, log_messages_byte, notification_byte, ser_pub_keys \ = separate_headers(block, [PADDED_UTF32_STR_LENGTH, GROUP_ID_LENGTH] + 2*[ENCODED_BOOLEAN_LENGTH]) pub_key_list = split_byte_string( ser_pub_keys, item_len=ONION_SERVICE_PUBLIC_KEY_LENGTH) group_pub_keys = [k for k in pub_key_list if k != dummy_pub_key] group_members = [ self.contact_list.get_contact_by_pub_key(k) for k in group_pub_keys if k in all_pub_keys ] self.groups.append( Group(name=bytes_to_str(name_bytes), group_id=group_id, log_messages=bytes_to_bool(log_messages_byte), notifications=bytes_to_bool(notification_byte), members=group_members, settings=self.settings, store_groups=self.store_groups)) update_db |= set(all_pub_keys) > set(group_pub_keys) if update_db: self.store_groups()
def access_logs(window: Union['TxWindow', 'RxWindow'], contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', master_key: 'MasterKey', msg_to_load: int = 0, export: bool = False) -> None: """\ Load 'msg_to_load' last messages from log database and display or export them. The default value of zero for `msg_to_load` means all messages for the window will be retrieved from the log database. """ file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs' log_file = get_logfile(file_name) packet_list = PacketList(settings, contact_list) message_log = [] # type: List[MsgTuple] group_msg_id = b'' for ct in iter(lambda: log_file.read(LOG_ENTRY_LENGTH), b''): plaintext = auth_and_decrypt(ct, master_key.master_key, database=file_name) onion_pub_key, timestamp, origin, assembly_packet = separate_headers( plaintext, [ ONION_SERVICE_PUBLIC_KEY_LENGTH, TIMESTAMP_LENGTH, ORIGIN_HEADER_LENGTH ]) if window.type == WIN_TYPE_CONTACT and onion_pub_key != window.uid: continue packet = packet_list.get_packet(onion_pub_key, origin, MESSAGE, log_access=True) try: packet.add_packet(assembly_packet) except FunctionReturn: continue if not packet.is_complete: continue whisper_byte, header, message = separate_headers( packet.assemble_message_packet(), [WHISPER_FIELD_LENGTH, MESSAGE_HEADER_LENGTH]) whisper = bytes_to_bool(whisper_byte) if header == PRIVATE_MESSAGE_HEADER and window.type == WIN_TYPE_CONTACT: message_log.append( (bytes_to_timestamp(timestamp), message.decode(), onion_pub_key, packet.origin, whisper, False)) elif header == GROUP_MESSAGE_HEADER and window.type == WIN_TYPE_GROUP: purp_group_id, message = separate_header(message, GROUP_ID_LENGTH) if window.group is not None and purp_group_id != window.group.group_id: continue purp_msg_id, message = separate_header(message, GROUP_MSG_ID_LENGTH) if packet.origin == ORIGIN_USER_HEADER: if purp_msg_id == group_msg_id: continue group_msg_id = purp_msg_id message_log.append( (bytes_to_timestamp(timestamp), message.decode(), onion_pub_key, packet.origin, whisper, False)) log_file.close() print_logs(message_log[-msg_to_load:], export, msg_to_load, window, contact_list, group_list, settings)
def remove_logs(contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', master_key: 'MasterKey', selector: bytes) -> None: """\ Remove log entries for selector (public key of an account/group ID). If the selector is a public key, all messages (both the private conversation and any associated group messages) sent to and received from the associated contact are removed. If the selector is a group ID, only messages for group determined by that group ID are removed. """ ensure_dir(DIR_USER_DATA) file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs' temp_name = f'{file_name}_temp' log_file = get_logfile(file_name) packet_list = PacketList(settings, contact_list) ct_to_keep = [] # type: List[bytes] removed = False contact = len(selector) == ONION_SERVICE_PUBLIC_KEY_LENGTH for ct in iter(lambda: log_file.read(LOG_ENTRY_LENGTH), b''): plaintext = auth_and_decrypt(ct, master_key.master_key, database=file_name) onion_pub_key, _, origin, assembly_packet = separate_headers( plaintext, [ ONION_SERVICE_PUBLIC_KEY_LENGTH, TIMESTAMP_LENGTH, ORIGIN_HEADER_LENGTH ]) if contact: if onion_pub_key == selector: removed = True else: ct_to_keep.append(ct) else: # Group packet = packet_list.get_packet(onion_pub_key, origin, MESSAGE, log_access=True) try: packet.add_packet(assembly_packet, ct) except FunctionReturn: continue if not packet.is_complete: continue _, header, message = separate_headers( packet.assemble_message_packet(), [WHISPER_FIELD_LENGTH, MESSAGE_HEADER_LENGTH]) if header == PRIVATE_MESSAGE_HEADER: ct_to_keep.extend(packet.log_ct_list) packet.clear_assembly_packets() elif header == GROUP_MESSAGE_HEADER: group_id, _ = separate_header(message, GROUP_ID_LENGTH) if group_id == selector: removed = True else: ct_to_keep.extend(packet.log_ct_list) packet.clear_assembly_packets() log_file.close() if os.path.isfile(temp_name): os.remove(temp_name) with open(temp_name, 'wb+') as f: if ct_to_keep: f.write(b''.join(ct_to_keep)) os.remove(file_name) os.rename(temp_name, file_name) try: name = contact_list.get_contact_by_pub_key(selector).nick \ if contact else group_list.get_group_by_id(selector).name except StopIteration: name = pub_key_to_short_address(selector) \ if contact else b58encode(selector) action = "Removed" if removed else "Found no" win_type = "contact" if contact else "group" raise FunctionReturn(f"{action} log entries for {win_type} '{name}'.")
def process_local_key(ts: 'datetime', packet: bytes, window_list: 'WindowList', contact_list: 'ContactList', key_list: 'KeyList', settings: 'Settings', kdk_hashes: List[bytes], packet_hashes: List[bytes], l_queue: 'Queue') -> None: """Decrypt local key packet and add local contact/keyset.""" bootstrap = not key_list.has_local_keyset() plaintext = None try: packet_hash = blake2b(packet) # Check if the packet is an old one if packet_hash in packet_hashes: raise FunctionReturn("Error: Received old local key packet.", output=False) while True: m_print("Local key setup", bold=True, head_clear=True, head=1, tail=1) kdk = get_b58_key(B58_LOCAL_KEY, settings) kdk_hash = blake2b(kdk) try: plaintext = auth_and_decrypt(packet, kdk) break except nacl.exceptions.CryptoError: # Check if key was an old one if kdk_hash in kdk_hashes: m_print("Error: Entered an old local key decryption key.", delay=1) continue # Check if the kdk was for a packet further ahead in the queue buffer = [] # type: List[Tuple[datetime, bytes]] while l_queue.qsize() > 0: tup = l_queue.get() # type: Tuple[datetime, bytes] if tup not in buffer: buffer.append(tup) for i, tup in enumerate(buffer): try: plaintext = auth_and_decrypt(tup[1], kdk) # If we reach this point, decryption was successful. for unexamined in buffer[i + 1:]: l_queue.put(unexamined) buffer = [] ts = tup[0] break except nacl.exceptions.CryptoError: continue else: # Finished the buffer without finding local key CT # for the kdk. Maybe the kdk is from another session. raise FunctionReturn( "Error: Incorrect key decryption key.", delay=1) break # This catches PyCharm's weird claim that plaintext might be referenced before assignment if plaintext is None: # pragma: no cover raise FunctionReturn("Error: Could not decrypt local key.") # Add local contact to contact list database contact_list.add_contact(LOCAL_PUBKEY, LOCAL_NICK, KEX_STATUS_LOCAL_KEY, bytes(FINGERPRINT_LENGTH), bytes(FINGERPRINT_LENGTH), False, False, True) tx_mk, tx_hk, c_code = separate_headers(plaintext, 2 * [SYMMETRIC_KEY_LENGTH]) # Add local keyset to keyset database key_list.add_keyset(onion_pub_key=LOCAL_PUBKEY, tx_mk=tx_mk, rx_mk=csprng(), tx_hk=tx_hk, rx_hk=csprng()) # Cache hashes needed to recognize reissued local key packets and key decryption keys. packet_hashes.append(packet_hash) kdk_hashes.append(kdk_hash) # Prevent leak of KDK via terminal history / clipboard readline.clear_history() os.system(RESET) root = tkinter.Tk() root.withdraw() try: if root.clipboard_get() == b58encode(kdk): root.clipboard_clear() except tkinter.TclError: pass root.destroy() m_print([ "Local key successfully installed.", f"Confirmation code (to Transmitter): {c_code.hex()}" ], box=True, head=1) local_win = window_list.get_local_window() local_win.add_new(ts, "Added new local key.") if bootstrap: window_list.active_win = local_win except (EOFError, KeyboardInterrupt): m_print("Local key setup aborted.", bold=True, tail_clear=True, delay=1, head=2) if window_list.active_win is not None and not bootstrap: window_list.active_win.redraw() raise FunctionReturn("Local key setup aborted.", output=False)
def remove_logs(contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', master_key: 'MasterKey', selector: bytes) -> None: """\ Remove log entries for selector (public key of an account/group ID). If the selector is a public key, all messages (both the private conversation and any associated group messages) sent to and received from the associated contact are removed. If the selector is a group ID, only messages for the group matching that group ID are removed. """ ensure_dir(DIR_USER_DATA) file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs' temp_name = file_name + TEMP_SUFFIX packet_list = PacketList(settings, contact_list) entries_to_keep = [] # type: List[bytes] removed = False contact = len(selector) == ONION_SERVICE_PUBLIC_KEY_LENGTH check_log_file_exists(file_name) message_log = MessageLog(file_name, master_key.master_key) for log_entry in message_log: onion_pub_key, _, origin, assembly_packet = separate_headers( log_entry, [ ONION_SERVICE_PUBLIC_KEY_LENGTH, TIMESTAMP_LENGTH, ORIGIN_HEADER_LENGTH ]) if contact: if onion_pub_key == selector: removed = True else: entries_to_keep.append(log_entry) else: # Group packet = packet_list.get_packet(onion_pub_key, origin, MESSAGE, log_access=True) try: packet.add_packet(assembly_packet, log_entry) except SoftError: continue if not packet.is_complete: continue removed = check_packet_fate(entries_to_keep, packet, removed, selector) message_log.close_database() message_log_temp = MessageLog(temp_name, master_key.master_key) for log_entry in entries_to_keep: message_log_temp.insert_log_entry(log_entry) message_log_temp.close_database() os.replace(temp_name, file_name) try: name = contact_list.get_nick_by_pub_key( selector) if contact else group_list.get_group_by_id(selector).name except StopIteration: name = pub_key_to_short_address(selector) if contact else b58encode( selector) action = "Removed" if removed else "Found no" win_type = "contact" if contact else "group" raise SoftError(f"{action} log entries for {win_type} '{name}'.")
def test_too_small_string(self): self.assertEqual( separate_headers(b"cypherpunk", header_length_list=[1, 2, 10]), [b"c", b"yp", b"herpunk", b""])
def test_separate_headers(self): self.assertEqual( separate_headers(b"cypherpunk", header_length_list=[1, 2, 3]), [b"c", b"yp", b"her", b"punk"])
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.") # Logging 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_local_key(ts: 'datetime', packet: bytes, window_list: 'WindowList', contact_list: 'ContactList', key_list: 'KeyList', settings: 'Settings', kdk_hashes: List[bytes], packet_hashes: List[bytes], l_queue: 'Queue[Tuple[datetime, bytes]]') -> None: """Decrypt local key packet and add local contact/keyset.""" first_local_key = not key_list.has_local_keyset() try: if blake2b(packet) in packet_hashes: raise SoftError("Error: Received old local key packet.", output=False) m_print("Local key setup", bold=True, head_clear=True, head=1, tail=1) ts, plaintext = decrypt_local_key(ts, packet, kdk_hashes, packet_hashes, settings, l_queue) # Add local contact to contact list database contact_list.add_contact(LOCAL_PUBKEY, LOCAL_NICK, KEX_STATUS_LOCAL_KEY, bytes(FINGERPRINT_LENGTH), bytes(FINGERPRINT_LENGTH), False, False, True) tx_mk, tx_hk, c_code = separate_headers(plaintext, 2 * [SYMMETRIC_KEY_LENGTH]) # Add local keyset to keyset database key_list.add_keyset(onion_pub_key=LOCAL_PUBKEY, tx_mk=tx_mk, rx_mk=csprng(), tx_hk=tx_hk, rx_hk=csprng()) m_print([ "Local key successfully installed.", f"Confirmation code (to Transmitter): {c_code.hex()}" ], box=True, head=1) cmd_win = window_list.get_command_window() if first_local_key: window_list.active_win = cmd_win raise SoftError("Added new local key.", window=cmd_win, ts=ts, output=False) except (EOFError, KeyboardInterrupt): m_print("Local key setup aborted.", bold=True, tail_clear=True, delay=1, head=2) if window_list.active_win is not None and not first_local_key: window_list.active_win.redraw() raise SoftError("Local key setup aborted.", output=False)