def decrypt_assembly_packet(packet: bytes, window_list: 'WindowList', contact_list: 'ContactList', key_list: 'KeyList') -> Tuple[bytes, str, bytes]: """Decrypt assembly packet from contact/local TxM.""" enc_harac = packet[1:49] enc_msg = packet[49:345] window = window_list.get_local_window() origin, direction, key_dir, p_type, account, nick = get_packet_values( packet, window, contact_list) # Load keys keyset = key_list.get_keyset(account) header_key = getattr(keyset, f'{key_dir}_hek') message_key = getattr(keyset, f'{key_dir}_key') if any(k == bytes(KEY_LENGTH) for k in [header_key, message_key]): raise FunctionReturn("Warning! Loaded zero-key for packet decryption.") # Decrypt hash ratchet counter try: harac_bytes = auth_and_decrypt(enc_harac, header_key, soft_e=True) except nacl.exceptions.CryptoError: raise FunctionReturn( f"Warning! Received {p_type} {direction} {nick} had an invalid hash ratchet MAC.", window=window) # Catch up with hash ratchet offset purp_harac = bytes_to_int(harac_bytes) stored_harac = getattr(keyset, f'{key_dir}_harac') offset = purp_harac - stored_harac if offset < 0: raise FunctionReturn( f"Warning! Received {p_type} {direction} {nick} had an expired hash ratchet counter.", window=window) process_offset(offset, origin, direction, nick, window) for _ in range(offset): message_key = hash_chain(message_key) # Decrypt packet try: assembly_packet = auth_and_decrypt(enc_msg, message_key, soft_e=True) except nacl.exceptions.CryptoError: raise FunctionReturn( f"Warning! Received {p_type} {direction} {nick} had an invalid MAC.", window=window) # Update keys in database keyset.update_key(key_dir, hash_chain(message_key), offset + 1) return assembly_packet, account, origin
def b58decode(string: str) -> bytes: """Decode a Base58-encoded string and verify checksum.""" b58_alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' orig_len = len(string) string = string.lstrip('1') new_len = len(string) p, acc = 1, 0 for c in string[::-1]: acc += p * b58_alphabet.index(c) p *= 58 decoded = [] while acc > 0: acc, mod = divmod(acc, 256) decoded.append(mod) decoded_ = ( bytes(decoded) + (orig_len - new_len) * b'\x00')[::-1] # type: Union[bytes, List[int]] if hash_chain(bytes(decoded_[:-4]))[:4] != decoded_[-4:]: raise ValueError return bytes(decoded_[:-4])
def queue_command(payload: bytes, settings: 'Settings', c_queue: 'Queue') -> None: """Split command into assembly packets and queue them. :param payload: Command's plaintext string. :param settings: Settings object :param c_queue: Multiprocessing queue for commands :return: None """ payload = zlib.compress(payload, level=9) if len(payload) < 255: padded = byte_padding(payload) packet_list = [C_S_HEADER + padded] else: payload += hash_chain(payload) padded = byte_padding(payload) p_list = split_byte_string(padded, item_len=255) packet_list = ([C_L_HEADER + p_list[0]] + [C_A_HEADER + p for p in p_list[1:-1]] + [C_E_HEADER + p_list[-1]]) if settings.session_trickle: for p in packet_list: c_queue.put(p) else: for p in packet_list: c_queue.put((p, settings))
def new_master_key(self) -> None: """Create a new master key from salt and password.""" password = MasterKey.new_password() salt = csprng() rounds = ARGON2_ROUNDS memory = ARGON2_MIN_MEMORY phase("Deriving master key", head=2) while True: time_start = time.monotonic() master_key, parallellism = argon2_kdf(password, salt, rounds, memory=memory, local_test=self.local_test) time_final = time.monotonic() - time_start if time_final > 3.0: self.master_key = master_key ensure_dir(f'{DIR_USER_DATA}/') with open(self.file_name, 'wb+') as f: f.write(salt + hash_chain(self.master_key) + int_to_bytes(rounds) + int_to_bytes(memory) + int_to_bytes(parallellism)) phase(DONE) break else: memory *= 2
def new_master_key(self) -> None: """Create a new master key from salt and password. The number of rounds starts at 1 but is increased dynamically based on system performance. This allows more security on faster platforms without additional cost on key derivation time. """ password = MasterKey.new_password() salt = keygen() rounds = 1 phase("Deriving master key", head=2) while True: time_start = time.monotonic() master_key, memory = argon2_kdf(password, salt, rounds, local_testing=self.local_test) time_final = time.monotonic() - time_start if time_final > 3.0: self.master_key = master_key master_key_hash = hash_chain(master_key) ensure_dir(f'{DIR_USER_DATA}/') with open(self.file_name, 'wb+') as f: f.write(salt + master_key_hash + int_to_bytes(rounds) + int_to_bytes(memory)) phase('Done') break else: rounds *= 2
def b58encode(byte_string: bytes) -> str: """Encode byte string to checksummed Base58 string. This format is very similar to Bitcoin's Wallet Import Format, however the SHA256(SHA256(key)) checksum has been replaced with TFC's hash chain (truncated to 4 bytes). """ b58_alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' byte_string += hash_chain(byte_string)[:4] orig_len = len(byte_string) byte_string = byte_string.lstrip(b'\x00') new_len = len(byte_string) p, acc = 1, 0 for byte in bytearray(byte_string[::-1]): acc += p * byte p *= 256 encoded = '' while acc > 0: acc, mod = divmod(acc, 58) encoded += b58_alphabet[mod] return (encoded + (orig_len - new_len) * '1')[::-1]
def load_master_key(self) -> None: """Derive master key from password from stored values (salt, rounds, memory).""" ensure_dir(f'{DIR_USER_DATA}/') with open(self.file_name, 'rb') as f: data = f.read() salt = data[0:32] k_hash = data[32:64] rounds = bytes_to_int(data[64:72]) memory = bytes_to_int(data[72:80]) while True: password = MasterKey.get_password() phase("Deriving master key", head=2, offset=16) purp_key, _ = argon2_kdf(password, salt, rounds, memory, local_testing=self.local_test) if hash_chain(purp_key) == k_hash: phase("Password correct", done=True) self.master_key = purp_key clear_screen(delay=0.5) break else: phase("Invalid password", done=True) print_on_previous_line(reps=5, delay=1)
def rotate_tx_key(self) -> None: """\ Update TxM side tx-key and harac (provides forward secrecy for sent messages). """ self.tx_key = hash_chain(self.tx_key) self.tx_harac += 1 self.store_keys()
def test_decryption_with_zero_rx_hek_raises_fr(self): # Setup self.create_encrypted_packet(tx_harac=2, rx_harac=1, key=(hash_chain(KEY_LENGTH*b'\x01'))) keyset = self.key_list.get_keyset('*****@*****.**') keyset.rx_hek = bytes(KEY_LENGTH) # Test self.assertFR("Warning! Loaded zero-key for packet decryption.", decrypt_assembly_packet, self.packet, self.window_list, self.contact_list, self.key_list)
def test_successful_packet_decryption_with_offset(self): # Setup self.create_encrypted_packet(tx_harac=2, rx_harac=1, key=(hash_chain(KEY_LENGTH*b'\x01'))) # Test assembly_pt, account, origin = decrypt_assembly_packet(self.packet, self.window_list, self.contact_list, self.key_list) self.assertEqual(rm_padding_bytes(assembly_pt), PRIVATE_MESSAGE_HEADER + b'test') self.assertEqual(account, '*****@*****.**') self.assertEqual(origin, ORIGIN_CONTACT_HEADER)
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 mock_command_preprocessor(command): payload = zlib.compress(command, level=9) if len(payload) < 255: padded = byte_padding(payload) packet_list = [C_S_HEADER + padded] else: payload += hash_chain(payload) padded = byte_padding(payload) p_list = split_byte_string(padded, item_len=255) packet_list = ([C_L_HEADER + p_list[0]] + [C_A_HEADER + p for p in p_list[1:-1]] + [C_E_HEADER + p_list[-1]]) return packet_list
def assembly_packet_creator(p_type: str, payload: bytes = b'', origin: bytes = b'', header: bytes = b'', group_name: str = None, encrypt: bool = False, break_g_name: bool = False, origin_acco: bytes = b'*****@*****.**'): """Create assembly packet list and optionally encrypt it.""" if p_type == MESSAGE: if not header: if group_name is not None: group_msg_id = GROUP_MSG_ID_LEN * b'a' group_name = binascii.unhexlify( 'a466c02c221cb135') if break_g_name else group_name.encode( ) header = GROUP_MESSAGE_HEADER + group_msg_id + group_name + US_BYTE else: header = PRIVATE_MESSAGE_HEADER payload = header + payload if p_type == FILE: if not payload: compressed = zlib.compress(os.urandom(10000), level=COMPRESSION_LEVEL) file_key = os.urandom(KEY_LENGTH) encrypted = encrypt_and_sign(compressed, key=file_key) + file_key encoded = base64.b85encode(encrypted) payload = int_to_bytes(1) + int_to_bytes( 2) + b'testfile.txt' + US_BYTE + encoded packet_list = split_to_assembly_packets(payload, p_type) if not encrypt: return packet_list if encrypt: harac = 1 m_key = KEY_LENGTH * b'\x01' m_hek = KEY_LENGTH * b'\x01' assembly_ct_list = [] for p in packet_list: harac_in_bytes = int_to_bytes(harac) encrypted_harac = encrypt_and_sign(harac_in_bytes, m_hek) encrypted_message = encrypt_and_sign(p, m_key) encrypted_packet = MESSAGE_PACKET_HEADER + encrypted_harac + encrypted_message + origin + origin_acco assembly_ct_list.append(encrypted_packet) m_key = hash_chain(m_key) harac += 1 return assembly_ct_list
def assemble_command_packet(self) -> bytes: """Assemble command packet.""" padded = b''.join([p[1:] for p in self.assembly_pt_list]) payload = rm_padding_bytes(padded) if len(self.assembly_pt_list) > 1: cmd_hash = payload[-KEY_LENGTH:] payload = payload[:-KEY_LENGTH] if hash_chain(payload) != cmd_hash: raise FunctionReturn("Error: Received an invalid command.") try: return zlib.decompress(payload) except zlib.error: raise FunctionReturn("Error: Decompression of command failed.")
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 test_long_command_compression_error_raises_fr(self): # Setup packet = Packet(LOCAL_ID, self.contact, ORIGIN_CONTACT_HEADER, COMMAND, self.settings) command = os.urandom(500) + b'a' payload = zlib.compress(command, level=COMPRESSION_LEVEL)[::-1] payload += hash_chain(payload) padded = byte_padding(payload) p_list = split_byte_string(padded, item_len=PADDING_LEN) packet_list = ([C_L_HEADER + p_list[0]] + [C_A_HEADER + p for p in p_list[1:-1]] + [C_E_HEADER + p_list[-1]]) for p in packet_list: packet.add_packet(p) # Test self.assertFR("Error: Decompression of command failed.", packet.assemble_command_packet) self.assertEqual(packet.log_masking_ctr, 0)
def test_successful_packet_decryption_with_offset(self): # Setup message = PRIVATE_MESSAGE_HEADER + byte_padding(b'test') encrypted_message = encrypt_and_sign(message, hash_chain(32 * b'\x01')) harac_in_bytes = int_to_bytes(2) encrypted_harac = encrypt_and_sign(harac_in_bytes, 32 * b'\x01') packet = MESSAGE_PACKET_HEADER + encrypted_harac + encrypted_message + ORIGIN_CONTACT_HEADER + b'*****@*****.**' window_list = WindowList(nicks=['Alice', 'local']) contact_list = ContactList(nicks=['Alice', 'local']) key_list = KeyList(nicks=['Alice', 'local']) keyset = key_list.get_keyset('*****@*****.**') keyset.rx_harac = 1 # Test assembly_pt, account, origin = decrypt_assembly_packet(packet, window_list, contact_list, key_list) self.assertEqual(assembly_pt, message) self.assertEqual(account, '*****@*****.**') self.assertEqual(origin, ORIGIN_CONTACT_HEADER)
def new_master_key(self): password = MasterKey.new_password() salt = os.urandom(32) rounds = 1 assert isinstance(salt, bytes) while True: time_start = time.monotonic() master_key, memory = argon2_kdf(password, salt, rounds, local_testing=False) time_final = time.monotonic() - time_start if time_final > 3.0: self.master_key = master_key master_key_hash = hash_chain(master_key) ensure_dir(f'{DIR_USER_DATA}/') with open(self.file_name, 'wb+') as f: f.write(salt + master_key_hash + int_to_bytes(rounds) + int_to_bytes(memory)) break else: rounds *= 2
def create_file_apct(): def mock_file_preprocessor(payload): payload = bytes(8) + payload padded = byte_padding(payload) p_list = split_byte_string(padded, item_len=255) packet_list = ( [F_L_HEADER + int_to_bytes(len(p_list)) + p_list[0][8:]] + [F_A_HEADER + p for p in p_list[1:-1]] + [F_E_HEADER + p_list[-1]]) return packet_list file_data = os.urandom(10000) compressed = zlib.compress(file_data, level=9) file_key = os.urandom(32) encrypted = encrypt_and_sign(compressed, key=file_key) encrypted += file_key encoded = base64.b85encode(encrypted) file_data = US_BYTE.join( [b'testfile.txt', b'11.0B', b'00d 00h 00m 00s', encoded]) packets = mock_file_preprocessor(file_data) harac = 1 m_key = 32 * b'\x01' apctl = [] for p in packets: harac_in_bytes = int_to_bytes(harac) encrypted_harac = encrypt_and_sign(harac_in_bytes, 32 * b'\x01') encrypted_message = encrypt_and_sign(p, m_key) encrypted_packet = MESSAGE_PACKET_HEADER + encrypted_harac + encrypted_message + ORIGIN_CONTACT_HEADER + b'*****@*****.**' apctl.append(encrypted_packet) harac += 1 m_key = hash_chain(m_key) return apctl
def load_master_key(self) -> None: """Derive master key from password and salt.""" with open(self.file_name, 'rb') as f: data = f.read() salt = data[0:32] key_hash = data[32:64] rounds = bytes_to_int(data[64:72]) memory = bytes_to_int(data[72:80]) parallelism = bytes_to_int(data[80:88]) while True: password = MasterKey.get_password() phase("Deriving master key", head=2, offset=16) purp_key, _ = argon2_kdf(password, salt, rounds, memory, parallelism) if hash_chain(purp_key) == key_hash: self.master_key = purp_key phase("Password correct", done=True) clear_screen(delay=0.5) break else: phase("Invalid password", done=True) print_on_previous_line(reps=5, delay=1)
def create_message_apct(origin, message, header=None, group_name=None): if not header: if group_name is not None: timestamp = double_to_bytes(time.time() * 1000) header = GROUP_MESSAGE_HEADER + timestamp + group_name + US_BYTE else: header = PRIVATE_MESSAGE_HEADER plaintext = header + message payload = zlib.compress(plaintext, level=9) if len(payload) < 255: padded = byte_padding(payload) packet_list = [M_S_HEADER + padded] else: msg_key = os.urandom(32) payload = encrypt_and_sign(payload, msg_key) payload += msg_key padded = byte_padding(payload) p_list = split_byte_string(padded, item_len=255) packet_list = ([M_L_HEADER + p_list[0]] + [M_A_HEADER + p for p in p_list[1:-1]] + [M_E_HEADER + p_list[-1]]) harac = 1 m_key = 32 * b'\x01' apctl = [] for p in packet_list: harac_in_bytes = int_to_bytes(harac) encrypted_harac = encrypt_and_sign(harac_in_bytes, 32 * b'\x01') encrypted_message = encrypt_and_sign(p, m_key) encrypted_packet = MESSAGE_PACKET_HEADER + encrypted_harac + encrypted_message + origin + b'*****@*****.**' apctl.append(encrypted_packet) harac += 1 m_key = hash_chain(m_key) return apctl
def test_chain(self): """Sanity check after verifying function. No official test vectors exist.""" self.assertEqual( hash_chain(bytes(32)), binascii.unhexlify('8d8c36497eb93a6355112e253f705a32' '85f3e2d82b9ac29461cd8d4f764e5d41'))
def test_rotate_tx_key(self): self.assertIsNone(self.keyset.rotate_tx_key()) self.assertEqual(self.keyset.tx_key, hash_chain(KEY_LENGTH * b'\x00')) self.assertEqual(self.keyset.tx_harac, 1)
def start_key_exchange(account: str, user: str, nick: str, contact_list: 'ContactList', settings: 'Settings', queues: Dict[bytes, 'Queue']) -> None: """Start X25519 key exchange with recipient. Variable naming: tx = user's key rx = contact's key sk = private (secret) key pk = public key key = message key hek = header key dh_ssk = X25519 shared secret :param account: The contact's account name (e.g. [email protected]) :param user: The user's account name (e.g. [email protected]) :param nick: Contact's nickname :param contact_list: Contact list object :param settings: Settings object :param queues: Dictionary of multiprocessing queues :return: None """ try: tx_sk = nacl.public.PrivateKey(csprng()) tx_pk = bytes(tx_sk.public_key) while True: queue_to_nh(PUBLIC_KEY_PACKET_HEADER + tx_pk + user.encode() + US_BYTE + account.encode(), settings, queues[NH_PACKET_QUEUE]) rx_pk = get_b58_key(B58_PUB_KEY, settings) if rx_pk != RESEND.encode(): break if rx_pk == bytes(KEY_LENGTH): # Public key is zero with negligible probability, therefore we # assume such key is malicious and attempts to either result in # zero shared key (pointless considering implementation), or to # DoS the key exchange as libsodium does not accept zero keys. box_print(["Warning!", "Received a malicious public key from network.", "Aborting key exchange for your safety."], tail=1) raise FunctionReturn("Error: Zero public key", output=False) dh_box = nacl.public.Box(tx_sk, nacl.public.PublicKey(rx_pk)) dh_ssk = dh_box.shared_key() # Domain separate each key with key-type specific context variable # and with public keys that both clients know which way to place. tx_key = hash_chain(dh_ssk + rx_pk + b'message_key') rx_key = hash_chain(dh_ssk + tx_pk + b'message_key') tx_hek = hash_chain(dh_ssk + rx_pk + b'header_key') rx_hek = hash_chain(dh_ssk + tx_pk + b'header_key') # Domain separate fingerprints of public keys by using the shared # secret as salt. This way entities who might monitor fingerprint # verification channel are unable to correlate spoken values with # public keys that transit through a compromised IM server. This # protects against de-anonymization of IM accounts in cases where # clients connect to the compromised server via Tor. The preimage # resistance of hash chain protects the shared secret from leaking. tx_fp = hash_chain(dh_ssk + tx_pk + b'fingerprint') rx_fp = hash_chain(dh_ssk + rx_pk + b'fingerprint') if not verify_fingerprints(tx_fp, rx_fp): box_print(["Warning!", "Possible man-in-the-middle attack detected.", "Aborting key exchange for your safety."], tail=1) raise FunctionReturn("Error: Fingerprint mismatch", output=False) packet = KEY_EX_X25519_HEADER \ + tx_key + tx_hek \ + rx_key + rx_hek \ + account.encode() + US_BYTE + nick.encode() queue_command(packet, settings, queues[COMMAND_PACKET_QUEUE]) contact_list.add_contact(account, user, nick, tx_fp, rx_fp, settings.log_messages_by_default, settings.accept_files_by_default, settings.show_notifications_by_default) # Use random values as Rx-keys to prevent decryption if they're accidentally used. queues[KEY_MANAGEMENT_QUEUE].put((KDB_ADD_ENTRY_HEADER, account, tx_key, csprng(), tx_hek, csprng())) box_print(f"Successfully added {nick}.") clear_screen(delay=1) except KeyboardInterrupt: raise FunctionReturn("Key exchange aborted.", delay=1, head=2, tail_clear=True)
def start_key_exchange(account: str, user: str, nick: str, contact_list: 'ContactList', settings: 'Settings', queues: Dict[bytes, 'Queue'], gateway: 'Gateway') -> None: """Start X25519 key exchange with recipient. Variable naming: tx = user's key rx = contact's key sk = private (secret) key pk = public key key = message key hek = header key dh_ssk = DH shared secret :param account: The contact's account name (e.g. [email protected]) :param user: The user's account name (e.g. [email protected]) :param nick: Contact's nickname :param contact_list: Contact list object :param settings: Settings object :param queues: Dictionary of multiprocessing queues :param gateway: Gateway object :return: None """ try: tx_sk = nacl.public.PrivateKey.generate() tx_pk = bytes(tx_sk.public_key) transmit( PUBLIC_KEY_PACKET_HEADER + tx_pk + user.encode() + US_BYTE + account.encode(), settings, gateway) rx_pk = nacl.public.PublicKey(get_b58_key('pubkey')) dh_box = nacl.public.Box(tx_sk, rx_pk) dh_ssk = dh_box.shared_key() rx_pk = bytes(rx_pk) # Domain separate each key with key-type specific byte-string and # with public keys that both clients know which way to place. tx_key = hash_chain(dh_ssk + rx_pk + b'message_key') rx_key = hash_chain(dh_ssk + tx_pk + b'message_key') tx_hek = hash_chain(dh_ssk + rx_pk + b'header_key') rx_hek = hash_chain(dh_ssk + tx_pk + b'header_key') # Domain separate fingerprints of public keys by using the shared # secret as salt. This way entities who might monitor fingerprint # verification channel are unable to correlate spoken values with # public keys that transit through a compromised IM server. This # protects against deanonymization of IM accounts in cases where # clients connect to the compromised server via Tor. tx_fp = hash_chain(dh_ssk + tx_pk + b'fingerprint') rx_fp = hash_chain(dh_ssk + rx_pk + b'fingerprint') if not verify_fingerprints(tx_fp, rx_fp): box_print([ "Possible man-in-the-middle attack detected.", "Aborting key exchange for your safety." ], tail=1) raise FunctionReturn("Fingerprint mismatch", output=False, delay=2.5) packet = KEY_EX_ECDHE_HEADER \ + tx_key + tx_hek \ + rx_key + rx_hek \ + account.encode() + US_BYTE + nick.encode() queue_command(packet, settings, queues[COMMAND_PACKET_QUEUE]) contact_list.add_contact(account, user, nick, tx_fp, rx_fp, settings.log_msg_by_default, settings.store_file_default, settings.n_m_notify_privacy) # Null-bytes below are fillers for Rx-keys not used by TxM. queues[KEY_MANAGEMENT_QUEUE].put( ('ADD', account, tx_key, bytes(32), tx_hek, bytes(32))) box_print([f"Successfully added {nick}."]) clear_screen(delay=1) except KeyboardInterrupt: raise FunctionReturn("Key exchange aborted.", delay=1)
def test_chain(self): """Sanity check after verifying function. No official vectors are available.""" self.assertEqual( hash_chain(bytes(32)), binascii.unhexlify("8d8c36497eb93a6355112e253f705a32" "85f3e2d82b9ac29461cd8d4f764e5d41"))