tfc-mirror/src/rx/key_exchanges.py

287 lines
10 KiB
Python

#!/usr/bin/env python3.6
# -*- coding: utf-8 -*-
"""
Copyright (C) 2013-2017 Markus Ottela
This file is part of TFC.
TFC is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.
TFC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with TFC. If not, see <http://www.gnu.org/licenses/>.
"""
import os.path
import pipes
import subprocess
import typing
from typing import Dict
import nacl.exceptions
from src.common.crypto import argon2_kdf, auth_and_decrypt, csprng
from src.common.db_masterkey import MasterKey
from src.common.encoding import b58encode
from src.common.exceptions import FunctionReturn
from src.common.input import get_b58_key
from src.common.misc import split_string
from src.common.output import box_print, c_print, clear_screen, phase, print_key, print_on_previous_line
from src.common.path import ask_path_gui
from src.common.statics import *
if typing.TYPE_CHECKING:
from datetime import datetime
from src.common.db_contacts import ContactList
from src.common.db_keys import KeyList
from src.common.db_settings import Settings
from src.rx.windows import WindowList
# Local key
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 local_key_installed(ts: 'datetime',
window_list: 'WindowList',
contact_list: 'ContactList') -> None:
"""Clear local key bootstrap process from screen."""
message = "Successfully completed local key exchange."
local_win = window_list.get_window(LOCAL_ID)
local_win.add_new(ts, message)
box_print(message)
clear_screen(delay=1)
if not contact_list.has_contacts():
c_print("Waiting for new contacts", head=1, tail=1)
# X25519
def process_public_key(ts: 'datetime',
packet: bytes,
window_list: 'WindowList',
settings: 'Settings',
pubkey_buf: Dict[str, bytes]) -> None:
"""Display contact's public key and add it to buffer."""
pub_key = packet[1:33]
origin = packet[33:34]
try:
account = packet[34:].decode()
except UnicodeError:
raise FunctionReturn("Error! Account for received public key had invalid encoding.")
if origin not in [ORIGIN_CONTACT_HEADER, ORIGIN_USER_HEADER]:
raise FunctionReturn("Error! Received public key had an invalid origin header.")
if origin == ORIGIN_CONTACT_HEADER:
pubkey_buf[account] = pub_key
print_key(f"Received public key from {account}:", pub_key, settings)
local_win = window_list.get_local_window()
pub_key_b58 = ' '.join(split_string(b58encode(pub_key), item_len=(51 if settings.local_testing_mode else 3)))
local_win.add_new(ts, f"Received public key from {account}: {pub_key_b58}")
elif origin == ORIGIN_USER_HEADER and account in pubkey_buf:
clear_screen()
print_key(f"Public key for {account}:", pubkey_buf[account], settings)
def add_x25519_keys(packet: bytes,
ts: 'datetime',
window_list: 'WindowList',
contact_list: 'ContactList',
key_list: 'KeyList',
settings: 'Settings',
pubkey_buf: Dict[str, bytes]) -> None:
"""Add contact and their X25519 keys."""
tx_key = packet[0:32]
tx_hek = packet[32:64]
rx_key = packet[64:96]
rx_hek = packet[96:128]
account, nick = [f.decode() for f in packet[128:].split(US_BYTE)]
contact_list.add_contact(account, DUMMY_USER, nick,
bytes(FINGERPRINT_LEN),
bytes(FINGERPRINT_LEN),
settings.log_messages_by_default,
settings.accept_files_by_default,
settings.show_notifications_by_default)
key_list.add_keyset(account, tx_key, rx_key, tx_hek, rx_hek)
pubkey_buf.pop(account, None)
message = f"Added X25519 keys for {nick} ({account})."
local_win = window_list.get_window(LOCAL_ID)
local_win.add_new(ts, message)
box_print(message)
clear_screen(delay=1)
# PSK
def add_psk_tx_keys(cmd_data: bytes,
ts: 'datetime',
window_list: 'WindowList',
contact_list: 'ContactList',
key_list: 'KeyList',
settings: 'Settings',
pubkey_buf: Dict[str, bytes]) -> None:
"""Add contact and Tx-PSKs."""
tx_key = cmd_data[0:32]
tx_hek = cmd_data[32:64]
account, nick = [f.decode() for f in cmd_data[64:].split(US_BYTE)]
contact_list.add_contact(account, DUMMY_USER, nick,
bytes(FINGERPRINT_LEN), bytes(FINGERPRINT_LEN),
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. This does not allow existential forgeries as
# decrypt_assembly_packet does not allow use of zero-keys for decryption.
key_list.add_keyset(account,
tx_key=tx_key,
rx_key=bytes(KEY_LENGTH),
tx_hek=tx_hek,
rx_hek=bytes(KEY_LENGTH))
pubkey_buf.pop(account, None)
message = f"Added Tx-PSK for {nick} ({account})."
local_win = window_list.get_window(LOCAL_ID)
local_win.add_new(ts, message)
box_print(message)
clear_screen(delay=1)
def import_psk_rx_keys(cmd_data: bytes,
ts: 'datetime',
window_list: 'WindowList',
contact_list: 'ContactList',
key_list: 'KeyList',
settings: 'Settings') -> None:
"""Import Rx-PSK of contact."""
account = cmd_data.decode()
if not contact_list.has_contact(account):
raise FunctionReturn(f"Error: Unknown account '{account}'")
contact = contact_list.get_contact(account)
psk_file = ask_path_gui(f"Select PSK for {contact.nick}", settings, get_file=True)
with open(psk_file, 'rb') as f:
psk_data = f.read()
if len(psk_data) != PSK_FILE_SIZE:
raise FunctionReturn("Error: Invalid PSK data in file.")
salt = psk_data[:ARGON2_SALT_LEN]
ct_tag = psk_data[ARGON2_SALT_LEN:]
while True:
try:
password = MasterKey.get_password("PSK password")
phase("Deriving key decryption key", head=2)
kdk, _ = argon2_kdf(password, salt, parallelism=1)
psk_pt = auth_and_decrypt(ct_tag, key=kdk, soft_e=True)
phase(DONE)
break
except nacl.exceptions.CryptoError:
print_on_previous_line()
c_print("Invalid password. Try again.", head=1)
print_on_previous_line(reps=5, delay=1.5)
except KeyboardInterrupt:
raise FunctionReturn("PSK import aborted.", head=2)
rx_key = psk_pt[0:32]
rx_hek = psk_pt[32:64]
if any(k == bytes(KEY_LENGTH) for k in [rx_key, rx_hek]):
raise FunctionReturn("Error: Received invalid keys from contact.")
keyset = key_list.get_keyset(account)
keyset.rx_key = rx_key
keyset.rx_hek = rx_hek
key_list.store_keys()
# Pipes protects against shell injection. Source of command's parameter
# is user's own RxM and therefore trusted, but it's still good practice.
subprocess.Popen(f"shred -n 3 -z -u {pipes.quote(psk_file)}", shell=True).wait()
if os.path.isfile(psk_file):
box_print(f"Warning! Overwriting of PSK ({psk_file}) failed. Press <Enter> to continue.", manual_proceed=True)
local_win = window_list.get_local_window()
message = f"Added Rx-PSK for {contact.nick} ({account})."
local_win.add_new(ts, message)
box_print([message, '', "Warning!",
"Physically destroy the keyfile transmission ",
"media to ensure that no data escapes RxM!"], head=1, tail=1)