def test_invalid_size_entropy_from_getrandom_raises_critical_error( self, mock_getrandom: MagicMock, mock_blake2b: MagicMock) -> None: with self.assertRaises(SystemExit): csprng() with self.assertRaises(SystemExit): csprng() mock_getrandom.assert_called_with(SYMMETRIC_KEY_LENGTH, flags=0) mock_blake2b.assert_not_called()
def test_travis_mock(self): # Setup o_environ = os.environ os.environ = dict(TRAVIS='true') # Test self.assertEqual(len(csprng()), KEY_LENGTH) self.assertIsInstance(csprng(), bytes) # Teardown os.environ = o_environ
def process_local_key(ts: 'datetime', packet: bytes, window_list: 'WindowList', contact_list: 'ContactList', key_list: 'KeyList', settings: 'Settings') -> None: """Decrypt local key packet and add local contact/keyset.""" bootstrap = not key_list.has_local_key() try: while True: clear_screen() box_print("Received encrypted local key", tail=1) kdk = get_b58_key(B58_LOCAL_KEY, settings) try: pt = auth_and_decrypt(packet[1:], key=kdk, soft_e=True) break except nacl.exceptions.CryptoError: if bootstrap: raise FunctionReturn( "Error: Incorrect key decryption key.", delay=1.5) c_print("Incorrect key decryption key.", head=1) clear_screen(delay=1.5) key = pt[0:32] hek = pt[32:64] conf_code = pt[64:65] # Add local contact to contact list database contact_list.add_contact(LOCAL_ID, LOCAL_ID, LOCAL_ID, bytes(FINGERPRINT_LEN), bytes(FINGERPRINT_LEN), False, False, True) # Add local keyset to keyset database key_list.add_keyset(rx_account=LOCAL_ID, tx_key=key, rx_key=csprng(), tx_hek=hek, rx_hek=csprng()) box_print(f"Confirmation code for TxM: {conf_code.hex()}", 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 KeyboardInterrupt: raise FunctionReturn("Local key setup aborted.", delay=1, head=3, tail_clear=True)
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 test_getrandom_called_with_correct_parameters_and_hashes_with_blake2b( self, mock_getrandom: MagicMock) -> None: key = csprng(XCHACHA20_NONCE_LENGTH) self.assertEqual( key, blake2b(self.mock_entropy, digest_size=XCHACHA20_NONCE_LENGTH)) mock_getrandom.assert_called_with(XCHACHA20_NONCE_LENGTH, flags=0)
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 directed to this function during traffic masking have been compressed at an earlier point. If the compressed message cannot be sent over one packet, it is split into multiple assembly packets. Long messages are encrypted with an inner layer of XChaCha20-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 before the file data is passed to this function. Commands do not need sender-based control, so they are only delivered with a hash that makes integrity check easy. First assembly packet in file transmission is prepended with an 8-byte packet counter header that tells the sender and receiver how many packets the file transmission requires. Each assembly packet is prepended with a header that tells the Receiver Program if the packet is a short (single packet) transmission or if it's the start packet, a continuation packet, or the last packet of a multi-packet transmission. """ 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_LENGTH: 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_LENGTH) + payload elif p_type == COMMAND: payload += blake2b(payload) padded = byte_padding(payload) p_list = split_byte_string(padded, item_len=PADDING_LENGTH) if p_type == FILE: p_list[0] = int_to_bytes(len(p_list)) + p_list[0][FILE_PACKET_CTR_LENGTH:] 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 generate_dummy_keyset() -> 'KeySet': """Generate a dummy KeySet object. The dummy KeySet simplifies the code around the constant length serialization when the data is stored to, or read from the database. In case the dummy keyset would ever be loaded accidentally, it uses a set of random keys to prevent decryption by eavesdropper. """ return KeySet(onion_pub_key=onion_address_to_pub_key(DUMMY_CONTACT), tx_mk=csprng(), rx_mk=csprng(), tx_hk=csprng(), rx_hk=csprng(), tx_harac=INITIAL_HARAC, rx_harac=INITIAL_HARAC, store_keys=lambda: None)
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 new_master_key(self) -> bytes: """Create a new master key from password and salt. The generated master key depends on a 256-bit salt and the password entered by the user. Additional computational strength is added by the slow hash function (Argon2d). This method automatically tweaks the Argon2 memory parameter so that key derivation on used hardware takes at least three seconds. The more cores and the faster each core is, the more security a given password provides. The preimage resistance of BLAKE2b prevents derivation of master key from the stored hash, and Argon2d ensures brute force and dictionary attacks against the master password are painfully slow even with GPUs/ASICs/FPGAs, as long as the password is sufficiently strong. The salt does not need additional protection as the security it provides depends on the salt space in relation to the number of attacked targets (i.e. if two or more physically compromised systems happen to share the same salt, the attacker can speed up the attack against those systems with time-memory-trade-off attack). A 256-bit salt ensures that even in a group of 4.8*10^29 users, the probability that two users share the same salt is just 10^(-18).* * https://en.wikipedia.org/wiki/Birthday_attack """ password = MasterKey.new_password() salt = csprng(ARGON2_SALT_LENGTH) memory = ARGON2_MIN_MEMORY parallelism = multiprocessing.cpu_count() if self.local_test: parallelism = max(1, parallelism // 2) phase("Deriving master key", head=2) while True: time_start = time.monotonic() master_key = argon2_kdf(password, salt, ARGON2_ROUNDS, memory, parallelism) kd_time = time.monotonic() - time_start if kd_time < MIN_KEY_DERIVATION_TIME: memory *= 2 else: ensure_dir(DIR_USER_DATA) with open(self.file_name, 'wb+') as f: f.write(salt + blake2b(master_key) + int_to_bytes(memory) + int_to_bytes(parallelism)) phase(DONE) return master_key
def process_file_data(data: bytes) -> bytes: """Compress, encrypt and encode file data. Compress file to reduce data transmission time. Add an inner layer of encryption to provide sender-based control over partial transmission. """ compressed = zlib.compress(data, level=COMPRESSION_LEVEL) file_key = csprng() processed = encrypt_and_sign(compressed, key=file_key) processed += file_key return processed
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 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 __init__(self, master_key: 'MasterKey') -> None: """Create a new OnionService object.""" self.master_key = master_key self.file_name = f'{DIR_USER_DATA}{TX}_onion_db' self.is_delivered = False self.conf_code = csprng(CONFIRM_CODE_LENGTH) ensure_dir(DIR_USER_DATA) if os.path.isfile(self.file_name): self.onion_private_key = self.load_onion_service_private_key() else: self.onion_private_key = self.new_onion_service_private_key() self.store_onion_service_private_key() self.public_key = bytes( nacl.signing.SigningKey(seed=self.onion_private_key).verify_key) self.user_onion_address = pub_key_to_onion_address(self.public_key) self.user_short_address = pub_key_to_short_address(self.public_key)
def test_key_generation(self): self.assertEqual(len(csprng()), KEY_LENGTH) self.assertIsInstance(csprng(), bytes)
def new_master_key(self, replace: bool = True) -> bytes: """Create a new master key from password and salt. The generated master key depends on a 256-bit salt and the password entered by the user. Additional computational strength is added by the slow hash function (Argon2id). The more cores and the faster each core is, and the more memory the system has, the more secure TFC data is under the same password. This method automatically tweaks the Argon2 time and memory cost parameters according to best practices as determined in https://tools.ietf.org/html/draft-irtf-cfrg-argon2-09#section-4 1) For Argon2 type (y), Argon2id was selected because the adversary might be able to run arbitrary code on Destination Computer and thus perform a side-channel attack against the function. 2) The maximum number of threads (h) is determined by the number available in the system. However, during local testing this number is reduced to half to allow simultaneous login to Transmitter and Receiver Program. 3) The maximum amount of memory (m) is what the system has to offer. For hard-drive encryption purposes, the recommendation is 6GiB. TFC will use that amount (or even more) if available. However, on less powerful systems, it will settle for less. 4) For key derivation time (x), the value is set to at least 3 seconds, with the maximum being 4 seconds. The minimum value is the same as the recommendation for hard-drive encryption. 5) The salt length is set to 256-bits which is double the recommended length. The salt size ensures that even in a group of 4.8*10^29 users, the probability that two users share the same salt is just 10^(-18).* * https://en.wikipedia.org/wiki/Birthday_attack The salt does not need additional protection as the security it provides depends on the salt space in relation to the number of attacked targets (i.e. if two or more physically compromised systems happen to share the same salt, the attacker can speed up the attack against those systems with time-memory-trade-off attack). 6) The tag length isn't utilized. The result of the key derivation is the master encryption key itself, which is set to 32 bytes for use in XChaCha20-Poly1305. 7) Memory wiping feature is not provided. To recognize the password is correct, the BLAKE2b hash of the master key is stored together with key derivation parameters into the login database. The preimage resistance of BLAKE2b prevents derivation of master key from the stored hash, and Argon2id ensures brute force and dictionary attacks against the master password are painfully slow even with GPUs/ASICs/FPGAs, as long as the password is sufficiently strong. """ password = MasterKey.new_password() salt = csprng(ARGON2_SALT_LENGTH) # Determine the amount of memory used from the amount of free RAM in the system. memory_cost = self.get_available_memory() # Determine the number of threads to use parallelism = multiprocessing.cpu_count() if self.local_test: parallelism = max(ARGON2_MIN_PARALLELISM, parallelism // 2) # Determine time cost time_cost, kd_time, master_key = self.determine_time_cost( password, salt, memory_cost, parallelism) # Determine memory cost if kd_time > MAX_KEY_DERIVATION_TIME: memory_cost, master_key = self.determine_memory_cost( password, salt, time_cost, memory_cost, parallelism) # Store values to database database_data = (salt + blake2b(master_key) + int_to_bytes(time_cost) + int_to_bytes(memory_cost) + int_to_bytes(parallelism)) if replace: self.database.store_unencrypted_database(database_data) else: # When replacing the master key, the new master key needs to be generated before # databases are encrypted. However, storing the new master key shouldn't be done # before all new databases have been successfully written. We therefore just cache # the database data. self.database_data = database_data print_on_previous_line() phase("Deriving master key") phase(DONE, delay=1) return master_key
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 start_key_exchange( onion_pub_key: bytes, # Public key of contact's v3 Onion Service nick: str, # Contact's nickname contact_list: 'ContactList', # Contact list object settings: 'Settings', # Settings object queues: 'QueueDict' # Dictionary of multiprocessing queues ) -> None: """Start X448 key exchange with the recipient. This function first creates the X448 key pair. It then outputs the public key to Relay Program on Networked Computer, that passes the public key to contact's Relay Program. When contact's public key reaches the user's Relay Program, the user will manually copy the key into their Transmitter Program. The X448 shared secret is used to create unidirectional message and header keys, that will be used in forward secret communication. This is followed by the fingerprint verification where the user manually authenticates the public key. Once the fingerprint has been accepted, this function will add the contact/key data to contact/key databases, and export that data to the Receiver Program on Destination Computer. The transmission is encrypted with the local key. --- TFC provides proactive security by making fingerprint verification part of the key exchange. This prevents the situation where the users don't know about the feature, and thus helps minimize the risk of MITM attack. The fingerprints can be skipped by pressing Ctrl+C. This feature is not advertised however, because verifying fingerprints the only strong way to be sure TFC is not under MITM attack. When verification is skipped, TFC marks the contact's X448 keys as "Unverified". The fingerprints can later be verified with the `/verify` command: answering `yes` to the question on whether the fingerprints match, marks the X448 keys as "Verified". Variable naming: tx = user's key rx = contact's key fp = fingerprint mk = message key hk = header key """ if not contact_list.has_pub_key(onion_pub_key): contact_list.add_contact(onion_pub_key, nick, bytes(FINGERPRINT_LENGTH), bytes(FINGERPRINT_LENGTH), KEX_STATUS_PENDING, settings.log_messages_by_default, settings.accept_files_by_default, settings.show_notifications_by_default) contact = contact_list.get_contact_by_pub_key(onion_pub_key) # Generate new private key or load cached private key if contact.tfc_private_key is None: tfc_private_key_user = X448.generate_private_key() else: tfc_private_key_user = contact.tfc_private_key try: tfc_public_key_user = X448.derive_public_key(tfc_private_key_user) # Import public key of contact while True: public_key_packet = PUBLIC_KEY_DATAGRAM_HEADER + onion_pub_key + tfc_public_key_user queue_to_nc(public_key_packet, queues[RELAY_PACKET_QUEUE]) tfc_public_key_contact = get_b58_key(B58_PUBLIC_KEY, settings, contact.short_address) if tfc_public_key_contact != b'': break # Validate public key of contact if len(tfc_public_key_contact) != TFC_PUBLIC_KEY_LENGTH: m_print([ "Warning!", "Received invalid size public key.", "Aborting key exchange for your safety." ], bold=True, tail=1) raise FunctionReturn("Error: Invalid public key length", output=False) if tfc_public_key_contact == bytes(TFC_PUBLIC_KEY_LENGTH): # The public key of contact is zero with negligible probability, # therefore we assume such key is malicious and attempts to set # the shared key to zero. m_print([ "Warning!", "Received a malicious zero-public key.", "Aborting key exchange for your safety." ], bold=True, tail=1) raise FunctionReturn("Error: Zero public key", output=False) # Derive shared key dh_shared_key = X448.shared_key(tfc_private_key_user, tfc_public_key_contact) # Domain separate unidirectional keys from shared key by using public # keys as message and the context variable as personalization string. tx_mk = blake2b(tfc_public_key_contact, dh_shared_key, person=b'message_key', digest_size=SYMMETRIC_KEY_LENGTH) rx_mk = blake2b(tfc_public_key_user, dh_shared_key, person=b'message_key', digest_size=SYMMETRIC_KEY_LENGTH) tx_hk = blake2b(tfc_public_key_contact, dh_shared_key, person=b'header_key', digest_size=SYMMETRIC_KEY_LENGTH) rx_hk = blake2b(tfc_public_key_user, dh_shared_key, person=b'header_key', digest_size=SYMMETRIC_KEY_LENGTH) # Domain separate fingerprints of public keys by using the # shared secret as key and the context variable as # personalization string. This way entities who might monitor # fingerprint verification channel are unable to correlate # spoken values with public keys that they might see on RAM or # screen of Networked Computer: Public keys can not be derived # from the fingerprints due to preimage resistance of BLAKE2b, # and fingerprints can not be derived from public key without # the X448 shared secret. Using the context variable ensures # fingerprints are distinct from derived message and header keys. tx_fp = blake2b(tfc_public_key_user, dh_shared_key, person=b'fingerprint', digest_size=FINGERPRINT_LENGTH) rx_fp = blake2b(tfc_public_key_contact, dh_shared_key, person=b'fingerprint', digest_size=FINGERPRINT_LENGTH) # Verify fingerprints try: if not verify_fingerprints(tx_fp, rx_fp): m_print([ "Warning!", "Possible man-in-the-middle attack detected.", "Aborting key exchange for your safety." ], bold=True, tail=1) raise FunctionReturn("Error: Fingerprint mismatch", delay=2.5, output=False) kex_status = KEX_STATUS_VERIFIED except (EOFError, KeyboardInterrupt): m_print([ "Skipping fingerprint verification.", '', "Warning!", "Man-in-the-middle attacks can not be detected", "unless fingerprints are verified! To re-verify", "the contact, use the command '/verify'.", '', "Press <enter> to continue." ], manual_proceed=True, box=True, head=2) kex_status = KEX_STATUS_UNVERIFIED # Send keys to the Receiver Program c_code = blake2b(onion_pub_key, digest_size=CONFIRM_CODE_LENGTH) command = (KEY_EX_ECDHE + 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) # Store contact data into databases contact.tfc_private_key = None contact.tx_fingerprint = tx_fp contact.rx_fingerprint = rx_fp contact.kex_status = kex_status contact_list.store_contacts() queues[KEY_MANAGEMENT_QUEUE].put( (KDB_ADD_ENTRY_HEADER, onion_pub_key, tx_mk, csprng(), tx_hk, csprng())) m_print(f"Successfully added {nick}.", bold=True, tail_clear=True, delay=1, head=1) except (EOFError, KeyboardInterrupt): contact.tfc_private_key = tfc_private_key_user raise FunctionReturn("Key exchange interrupted.", tail_clear=True, delay=1, head=2)
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 start_key_exchange( onion_pub_key: bytes, # Public key of contact's v3 Onion Service nick: str, # Contact's nickname contact_list: 'ContactList', # ContactList object settings: 'Settings', # Settings object queues: 'QueueDict' # Dictionary of multiprocessing queues ) -> None: """Start X448 key exchange with the recipient. This function first creates the X448 key pair. It then outputs the public key to Relay Program on Networked Computer, that passes the public key to contact's Relay Program where it is displayed. When the contact's public key reaches the user's Relay Program, the user will manually type the key into their Transmitter Program. The X448 shared secret is used to create unidirectional message and header keys, that will be used in forward secret communication. This is followed by the fingerprint verification where the user manually authenticates the public key. Once the fingerprint has been accepted, this function will add the contact/key data to contact/key databases, and export that data to the Receiver Program on Destination Computer. The transmission is encrypted with the local key. --- TFC provides proactive security by making fingerprint verification part of the key exchange. This prevents the situation where the users don't know about the feature, and thus helps minimize the risk of MITM attack. The fingerprints can be skipped by pressing Ctrl+C. This feature is not advertised however, because verifying fingerprints the only strong way to be sure TFC is not under MITM attack. When verification is skipped, TFC marks the contact's X448 keys as "Unverified". The fingerprints can later be verified with the `/verify` command: answering `yes` to the question on whether the fingerprints match, marks the X448 keys as "Verified". Variable naming: tx = user's key rx = contact's key fp = fingerprint mk = message key hk = header key """ if not contact_list.has_pub_key(onion_pub_key): contact_list.add_contact(onion_pub_key, nick, bytes(FINGERPRINT_LENGTH), bytes(FINGERPRINT_LENGTH), KEX_STATUS_PENDING, settings.log_messages_by_default, settings.accept_files_by_default, settings.show_notifications_by_default) contact = contact_list.get_contact_by_pub_key(onion_pub_key) # Generate new private key or load cached private key if contact.tfc_private_key is None: tfc_private_key_user = X448.generate_private_key() else: tfc_private_key_user = contact.tfc_private_key try: tfc_public_key_user = X448.derive_public_key(tfc_private_key_user) kdk_hash = contact_list.get_contact_by_pub_key( LOCAL_PUBKEY).tx_fingerprint tfc_public_key_contact = exchange_public_keys(onion_pub_key, tfc_public_key_user, kdk_hash, contact, settings, queues) validate_contact_public_key(tfc_public_key_contact) dh_shared_key = X448.shared_key(tfc_private_key_user, tfc_public_key_contact) tx_mk, rx_mk, tx_hk, rx_hk, tx_fp, rx_fp \ = X448.derive_subkeys(dh_shared_key, tfc_public_key_user, tfc_public_key_contact) kex_status = validate_contact_fingerprint(tx_fp, rx_fp) deliver_contact_data(KEY_EX_ECDHE, nick, onion_pub_key, tx_mk, rx_mk, tx_hk, rx_hk, queues, settings) # Store contact data into databases contact.tfc_private_key = None contact.tx_fingerprint = tx_fp contact.rx_fingerprint = rx_fp contact.kex_status = kex_status contact_list.store_contacts() queues[KEY_MANAGEMENT_QUEUE].put( (KDB_ADD_ENTRY_HEADER, onion_pub_key, tx_mk, csprng(), tx_hk, csprng())) m_print(f"Successfully added {nick}.", bold=True, tail_clear=True, delay=1, head=1) except (EOFError, KeyboardInterrupt): contact.tfc_private_key = tfc_private_key_user raise SoftError("Key exchange interrupted.", tail_clear=True, delay=1, head=2)
def new_confirmation_code(self) -> None: """Generate new confirmation code for Onion Service data.""" self.conf_code = csprng(CONFIRM_CODE_LENGTH)
def new_local_key(contact_list: 'ContactList', settings: 'Settings', queues: 'QueueDict') -> None: """Run local key exchange protocol. Local key encrypts commands and data sent from Source Computer to user's Destination Computer. The key is delivered to Destination Computer in packet encrypted with an ephemeral, symmetric, key encryption key. The check-summed Base58 format key decryption key is typed to Receiver Program manually. This prevents local key leak in following scenarios: 1. CT is intercepted by an adversary on compromised Networked Computer, but no visual eavesdropping takes place. 2. CT is not intercepted by an adversary on Networked Computer, but visual eavesdropping records key decryption key. 3. CT is delivered from Source Computer to Destination Computer directly (bypassing compromised Networked Computer), and visual eavesdropping records key decryption key. Once the correct key decryption key is entered to Receiver Program, it will display the 2-hexadecimal confirmation code generated by the Transmitter Program. The code will be entered back to Transmitter Program to confirm the user has successfully delivered the key decryption key. The protocol is completed with Transmitter Program sending LOCAL_KEY_RDY signal to the Receiver Program, that then moves to wait for public keys from contact. """ try: if settings.traffic_masking and contact_list.has_local_contact(): raise FunctionReturn( "Error: Command is disabled during traffic masking.", head_clear=True) m_print("Local key setup", bold=True, head_clear=True, head=1, tail=1) if not contact_list.has_local_contact(): time.sleep(0.5) key = csprng() hek = csprng() kek = csprng() c_code = os.urandom(CONFIRM_CODE_LENGTH) local_key_packet = LOCAL_KEY_DATAGRAM_HEADER + encrypt_and_sign( plaintext=key + hek + c_code, key=kek) # Deliver local key to Destination computer nc_bypass_msg(NC_BYPASS_START, settings) queue_to_nc(local_key_packet, queues[RELAY_PACKET_QUEUE]) while True: print_key("Local key decryption key (to Receiver)", kek, settings) purp_code = ask_confirmation_code('Receiver') if purp_code == c_code.hex(): nc_bypass_msg(NC_BYPASS_STOP, settings) break elif purp_code == '': phase("Resending local key", head=2) queue_to_nc(local_key_packet, queues[RELAY_PACKET_QUEUE]) phase(DONE) print_on_previous_line( reps=(9 if settings.local_testing_mode else 10)) else: m_print([ "Incorrect confirmation code. If Receiver did not receive", "the encrypted local key, resend it by pressing <Enter>." ], head=1) print_on_previous_line( reps=(9 if settings.local_testing_mode else 10), delay=2) # Add local contact to contact list database contact_list.add_contact(LOCAL_PUBKEY, LOCAL_NICK, bytes(FINGERPRINT_LENGTH), bytes(FINGERPRINT_LENGTH), KEX_STATUS_LOCAL_KEY, False, False, False) # Add local contact to keyset database queues[KEY_MANAGEMENT_QUEUE].put( (KDB_ADD_ENTRY_HEADER, LOCAL_PUBKEY, key, csprng(), hek, csprng())) # Notify Receiver that confirmation code was successfully entered queue_command(LOCAL_KEY_RDY, settings, queues) m_print("Successfully completed the local key exchange.", bold=True, tail_clear=True, delay=1, head=1) os.system(RESET) except (EOFError, KeyboardInterrupt): raise FunctionReturn("Local key setup aborted.", tail_clear=True, delay=1, head=2)
def test_exceeding_hash_function_max_digest_size_raises_critical_error( self) -> None: with self.assertRaises(SystemExit): csprng(BLAKE2_DIGEST_LENGTH_MAX + 1)
def test_invalid_entropy_type_from_getrandom_raises_critical_error( self, _: Callable[..., None]) -> None: with self.assertRaises(SystemExit): csprng()
def test_function_returns_key_of_specified_size(self) -> None: for key_size in range(BLAKE2_DIGEST_LENGTH_MIN, BLAKE2_DIGEST_LENGTH_MAX + 1): key = csprng(key_size) self.assertEqual(len(key), key_size)
def new_local_key(contact_list: 'ContactList', settings: 'Settings', queues: 'QueueDict') -> None: """Run local key exchange protocol. Local key encrypts commands and data sent from Source Computer to user's Destination Computer. The key is delivered to Destination Computer in packet encrypted with an ephemeral, symmetric, key encryption key. The check-summed Base58 format key decryption key is typed to Receiver Program manually. This prevents local key leak in following scenarios: 1. CT is intercepted by an adversary on compromised Networked Computer, but no visual eavesdropping takes place. 2. CT is not intercepted by an adversary on Networked Computer, but visual eavesdropping records key decryption key. 3. CT is delivered from Source Computer to Destination Computer directly (bypassing compromised Networked Computer), and visual eavesdropping records key decryption key. Once the correct key decryption key is entered to Receiver Program, it will display the 2-hexadecimal confirmation code generated by the Transmitter Program. The code will be entered back to Transmitter Program to confirm the user has successfully delivered the key decryption key. The protocol is completed with Transmitter Program sending LOCAL_KEY_RDY signal to the Receiver Program, that then moves to wait for public keys from contact. """ try: if settings.traffic_masking and contact_list.has_local_contact(): raise SoftError( "Error: Command is disabled during traffic masking.", head_clear=True) m_print("Local key setup", bold=True, head_clear=True, head=1, tail=1) if not contact_list.has_local_contact(): time.sleep(0.5) key = csprng() hek = csprng() kek = csprng() c_code = os.urandom(CONFIRM_CODE_LENGTH) local_key_packet = LOCAL_KEY_DATAGRAM_HEADER + encrypt_and_sign( plaintext=key + hek + c_code, key=kek) deliver_local_key(local_key_packet, kek, c_code, settings, queues) # Add local contact to contact list database contact_list.add_contact(LOCAL_PUBKEY, LOCAL_NICK, blake2b(b58encode(kek).encode()), bytes(FINGERPRINT_LENGTH), KEX_STATUS_LOCAL_KEY, False, False, False) # Add local contact to keyset database queues[KEY_MANAGEMENT_QUEUE].put( (KDB_ADD_ENTRY_HEADER, LOCAL_PUBKEY, key, csprng(), hek, csprng())) # Notify Receiver Program that confirmation code was successfully entered queue_command(LOCAL_KEY_RDY, settings, queues) m_print("Successfully completed the local key exchange.", bold=True, tail_clear=True, delay=1, head=1) reset_terminal() except (EOFError, KeyboardInterrupt): raise SoftError("Local key setup aborted.", tail_clear=True, delay=1, head=2)
def new_onion_service_private_key() -> bytes: """Generate a new Onion Service private key and store it.""" phase("Generate Tor OS key") onion_private_key = csprng(ONION_SERVICE_PRIVATE_KEY_LENGTH) phase(DONE) return onion_private_key
def create_pre_shared_key( onion_pub_key: bytes, # Public key of contact's v3 Onion Service nick: str, # Nick of contact contact_list: 'ContactList', # Contact list object settings: 'Settings', # Settings object onion_service: 'OnionService', # OnionService object queues: 'QueueDict' # Dictionary of multiprocessing queues ) -> None: """Generate a new pre-shared key for manual key delivery. Pre-shared keys offer a low-tech solution against the slowly emerging threat of quantum computers. PSKs are less convenient and not usable in every scenario, but until a quantum-safe key exchange algorithm with reasonably short keys is standardized, TFC can't provide a better alternative against quantum computers. The generated keys are protected by a key encryption key, derived from a 256-bit salt and a password (that is to be shared with the recipient) using Argon2id key derivation function. The encrypted message and header keys are stored together with salt on a removable media. This media must be a never-before-used device from sealed packaging. Re-using an old device might infect Source Computer, and the malware could either copy sensitive data on that removable media, or Source Computer might start transmitting the sensitive data covertly over the serial interface to malware on Networked Computer. Once the key has been exported to the clean drive, contact data and keys are exported to the Receiver Program on Destination computer. The transmission is encrypted with the local key. """ try: tx_mk = csprng() tx_hk = csprng() salt = csprng() password = MasterKey.new_password("password for PSK") phase("Deriving key encryption key", head=2) kek = argon2_kdf(password, salt, ARGON2_PSK_TIME_COST, ARGON2_PSK_MEMORY_COST, ARGON2_PSK_PARALLELISM) phase(DONE) ct_tag = encrypt_and_sign(tx_mk + tx_hk, key=kek) store_keys_on_removable_drive(ct_tag, salt, nick, onion_pub_key, onion_service, settings) deliver_contact_data(KEY_EX_PSK_TX, nick, onion_pub_key, tx_mk, csprng(), tx_hk, csprng(), queues, settings) 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) queues[KEY_MANAGEMENT_QUEUE].put( (KDB_ADD_ENTRY_HEADER, onion_pub_key, tx_mk, csprng(), tx_hk, csprng())) m_print(f"Successfully added {nick}.", bold=True, tail_clear=True, delay=1, head=1) except (EOFError, KeyboardInterrupt): raise SoftError("PSK generation aborted.", tail_clear=True, delay=1, head=2)
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)
def assembly_packet_creator( # --- Payload creation --- # Common settings packet_type: str, # Packet type (MESSAGE, FILE, or COMMAND, do not use tampered values) payload: Union[bytes, str] = None, # Payload message content (Plaintext message (str), file data (bytes), or command (bytes)) inner_key: bytes = None, # Key for inner encryption layer tamper_ciphertext: bool = False, # When True, tampers with the inner layer of encryption to make it undecryptable # Message packet parameters message_header: bytes = None, # Message header (PRIVATE_MESSAGE_HEADER, GROUP_MESSAGE_HEADER, FILE_KEY_HEADER, or tamper byte) tamper_plaintext: bool = False, # When true, replaces plaintext with undecodable bytestring. group_id: bytes = None, # When specified, creates message for group (4 byte random string) group_msg_id: bytes = None, # The group message id (16 byte random string) whisper_header: bytes = b'\x00', # Define whisper-header (b'\x00' for False, b'\x01' for True, others for tampering) # File packet parameters create_zip_bomb: bool = False, # When True, creates large enough ciphertext to trigger zip bomb protection tamper_compression: bool = False, # When True, tampers with compression to make decompression impossible packet_time: bytes = None, # Allows overriding the 8-byte packet time header packet_size: bytes = None, # Allows overriding the 8-byte packet size header file_name: bytes = None, # Name of the file (allows e.g. injection of invalid file names) omit_header_delim: bool = False, # When True, omits the file_name<>file_data delimiter. # --- Assembly packet splitting --- s_header_override: bytes = None, # Allows overriding the `short packet` assembly packet header l_header_override: bytes = None, # Allows overriding the `start of long packet` assembly packet header a_header_override: bytes = None, # Allows overriding the `appended long packet` assembly packet header e_header_override: bytes = None, # Allows overriding the `last packet of long packet` assembly packet header tamper_cmd_hash: bool = False, # When True, tampers with the command hash to make it undecryptable no_padding: bool = False, # When True, does not add padding to assembly packet. split_length: int = PADDING_LENGTH, # Allows configuring the length to which assembly packets are split # --- Packet encryption --- encrypt_packet: bool = False, # When True, encrypts packet into set of datagrams starting with default key (32*b'\x01') message_number: int = 0, # Determines the message key and harac for message harac: int = INITIAL_HARAC, # Allows choosing the hash ratchet counter for packet encryption message_key: bytes = None, # Allows choosing the message key to encrypt message with header_key: bytes = None, # Allows choosing the header key for hash ratchet encryption tamper_harac: bool = False, # When True, tampers with the MAC of encrypted harac tamper_message: bool = False, # When True, tampers with the MAC of encrypted messagae onion_pub_key: bytes = b'', # Defines the contact public key to use with datagram creation origin_header: bytes = b'', # Allows editing the origin header ) -> List[bytes]: """Create assembly packet list and optionally encrypt it to create datagram list.""" # ------------------------------------------------------------------------------------------------------------------ # | Create payload | # ------------------------------------------------------------------------------------------------------------------ if packet_type == MESSAGE: assert isinstance(payload, str) if message_header is None: if group_id is not None: group_msg_id_bytes = bytes(GROUP_MSG_ID_LENGTH) if group_msg_id is None else group_msg_id header = GROUP_MESSAGE_HEADER + group_id + group_msg_id_bytes else: header = PRIVATE_MESSAGE_HEADER else: header = message_header payload_bytes = UNDECODABLE_UNICODE if tamper_plaintext else payload.encode() payload = whisper_header + header + payload_bytes # --- elif packet_type == FILE: # Create packets for traffic masking file transmission file_data_size = 100_000_001 if create_zip_bomb else 10_000 payload_bytes = os.urandom(file_data_size) if payload is None else payload compressed = zlib.compress(payload_bytes, level=COMPRESSION_LEVEL) compressed = compressed if not tamper_compression else compressed[::-1] file_key_bytes = os.urandom(SYMMETRIC_KEY_LENGTH) if inner_key is None else inner_key ciphertext = encrypt_and_sign(compressed, key=file_key_bytes) ciphertext = ciphertext if not tamper_ciphertext else ciphertext[::-1] ct_with_key = ciphertext + file_key_bytes time_bytes = int_to_bytes(2) if packet_time is None else packet_time size_bytes = int_to_bytes(file_data_size) if packet_size is None else packet_size file_name_bytes = b'test_file.txt' if file_name is None else file_name delimiter = US_BYTE if not omit_header_delim else b'' payload = time_bytes + size_bytes + file_name_bytes + delimiter + ct_with_key elif packet_type == COMMAND: payload = payload else: raise ValueError(f"Invalid packet type '{packet_type}'.") # ------------------------------------------------------------------------------------------------------------------ # | Split payload to assembly packets | # ------------------------------------------------------------------------------------------------------------------ s_header = {MESSAGE: M_S_HEADER, FILE: F_S_HEADER, COMMAND: C_S_HEADER}[packet_type] l_header = {MESSAGE: M_L_HEADER, FILE: F_L_HEADER, COMMAND: C_L_HEADER}[packet_type] a_header = {MESSAGE: M_A_HEADER, FILE: F_A_HEADER, COMMAND: C_A_HEADER}[packet_type] e_header = {MESSAGE: M_E_HEADER, FILE: F_E_HEADER, COMMAND: C_E_HEADER}[packet_type] s_header = s_header if s_header_override is None else s_header_override l_header = l_header if l_header_override is None else l_header_override a_header = a_header if a_header_override is None else a_header_override e_header = e_header if e_header_override is None else e_header_override if packet_type in [MESSAGE, COMMAND]: compressed = zlib.compress(payload, level=COMPRESSION_LEVEL) payload = compressed if not tamper_compression else compressed[::-1] if len(payload) < PADDING_LENGTH: padded = byte_padding(payload) packet_list = [s_header + padded] else: if packet_type == MESSAGE: msg_key = csprng() if inner_key is None else inner_key payload = encrypt_and_sign(payload, msg_key) payload = payload if not tamper_ciphertext else payload[::-1] payload += msg_key elif packet_type == FILE: payload = bytes(FILE_PACKET_CTR_LENGTH) + payload elif packet_type == COMMAND: command_hash = blake2b(payload) command_hash = command_hash if not tamper_cmd_hash else command_hash[::-1] payload += command_hash padded = payload if no_padding else byte_padding(payload) p_list = split_byte_string(padded, item_len=split_length) if packet_type == FILE: p_list[0] = int_to_bytes(len(p_list)) + p_list[0][FILE_PACKET_CTR_LENGTH:] packet_list = ([l_header + p_list[0]] + [a_header + p for p in p_list[1:-1]] + [e_header + p_list[-1]]) if not encrypt_packet: return packet_list # ------------------------------------------------------------------------------------------------------------------ # | Encrypt assembly packets to create datagrams | # ------------------------------------------------------------------------------------------------------------------ message_key = SYMMETRIC_KEY_LENGTH * b'\x01' if message_key is None else message_key header_key = SYMMETRIC_KEY_LENGTH * b'\x01' if header_key is None else header_key for _ in range(message_number): message_key = blake2b(message_key + int_to_bytes(harac), digest_size=SYMMETRIC_KEY_LENGTH) harac += 1 assembly_ct_list = [] for packet in packet_list: harac_in_bytes = int_to_bytes(harac) encrypted_harac = encrypt_and_sign(harac_in_bytes, header_key) encrypted_message = encrypt_and_sign(packet, message_key) encrypted_harac = encrypted_harac if not tamper_harac else tamper_last_byte(encrypted_harac) encrypted_message = encrypted_message if not tamper_message else tamper_last_byte(encrypted_message) encrypted_packet = onion_pub_key + origin_header + encrypted_harac + encrypted_message assembly_ct_list.append(encrypted_packet) message_key = blake2b(message_key + int_to_bytes(harac), digest_size=SYMMETRIC_KEY_LENGTH) harac += 1 return assembly_ct_list
def test_default_key_type_and_size(self) -> None: key = csprng() self.assertIsInstance(key, bytes) self.assertEqual(len(key), SYMMETRIC_KEY_LENGTH)