tfc-mirror/src/rx/commands.py

358 lines
16 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
import typing
from typing import Any, Dict, Union
from src.common.db_logs import access_logs, re_encrypt, remove_logs
from src.common.encoding import bytes_to_int
from src.common.exceptions import FunctionReturn
from src.common.misc import ensure_dir
from src.common.output import box_print, clear_screen, phase, print_on_previous_line
from src.common.statics import *
from src.rx.commands_g import group_add_member, group_create, group_rm_member, remove_group
from src.rx.key_exchanges import add_psk_tx_keys, add_x25519_keys, import_psk_rx_keys, local_key_installed
from src.rx.packet import decrypt_assembly_packet
if typing.TYPE_CHECKING:
from datetime import datetime
from multiprocessing import Queue
from src.common.db_contacts import Contact, ContactList
from src.common.db_groups import Group, GroupList
from src.common.db_keys import KeyList
from src.common.db_masterkey import MasterKey
from src.common.db_settings import Settings
from src.rx.packet import PacketList
from src.rx.windows import WindowList
def process_command(ts: 'datetime',
assembly_ct: bytes,
window_list: 'WindowList',
packet_list: 'PacketList',
contact_list: 'ContactList',
key_list: 'KeyList',
group_list: 'GroupList',
settings: 'Settings',
master_key: 'MasterKey',
pubkey_buf: Dict[str, bytes],
exit_queue: 'Queue') -> None:
"""Decrypt command assembly packet and process command."""
assembly_packet, account, origin = decrypt_assembly_packet(assembly_ct, window_list, contact_list, key_list)
cmd_packet = packet_list.get_packet(account, origin, COMMAND)
cmd_packet.add_packet(assembly_packet)
if not cmd_packet.is_complete:
raise FunctionReturn("Incomplete command.", output=False)
command = cmd_packet.assemble_command_packet()
header = command[:2]
cmd_data = command[2:]
# Keyword Function to run ( Parameters )
# -----------------------------------------------------------------------------------------------------------------------------------------
d = {LOCAL_KEY_INSTALLED_HEADER: (local_key_installed, ts, window_list, contact_list ),
SHOW_WINDOW_ACTIVITY_HEADER: (show_win_activity, window_list ),
WINDOW_SELECT_HEADER: (select_win_cmd, cmd_data, window_list ),
CLEAR_SCREEN_HEADER: (clear_active_window, ),
RESET_SCREEN_HEADER: (reset_active_window, cmd_data, window_list ),
EXIT_PROGRAM_HEADER: (exit_tfc, exit_queue),
LOG_DISPLAY_HEADER: (log_command, cmd_data, None, window_list, contact_list, group_list, settings, master_key),
LOG_EXPORT_HEADER: (log_command, cmd_data, ts, window_list, contact_list, group_list, settings, master_key),
LOG_REMOVE_HEADER: (remove_log, cmd_data, settings, master_key),
CHANGE_MASTER_K_HEADER: (change_master_key, ts, window_list, contact_list, group_list, key_list, settings, master_key),
CHANGE_NICK_HEADER: (change_nick, cmd_data, ts, window_list, contact_list, ),
CHANGE_SETTING_HEADER: (change_setting, cmd_data, ts, window_list, contact_list, group_list, settings, ),
CHANGE_LOGGING_HEADER: (contact_setting, cmd_data, ts, window_list, contact_list, group_list, header ),
CHANGE_FILE_R_HEADER: (contact_setting, cmd_data, ts, window_list, contact_list, group_list, header ),
CHANGE_NOTIFY_HEADER: (contact_setting, cmd_data, ts, window_list, contact_list, group_list, header ),
GROUP_CREATE_HEADER: (group_create, cmd_data, ts, window_list, contact_list, group_list, settings ),
GROUP_ADD_HEADER: (group_add_member, cmd_data, ts, window_list, contact_list, group_list, settings ),
GROUP_REMOVE_M_HEADER: (group_rm_member, cmd_data, ts, window_list, contact_list, group_list, ),
GROUP_DELETE_HEADER: (remove_group, cmd_data, ts, window_list, group_list, ),
KEY_EX_X25519_HEADER: (add_x25519_keys, cmd_data, ts, window_list, contact_list, key_list, settings, pubkey_buf),
KEY_EX_PSK_TX_HEADER: (add_psk_tx_keys, cmd_data, ts, window_list, contact_list, key_list, settings, pubkey_buf),
KEY_EX_PSK_RX_HEADER: (import_psk_rx_keys, cmd_data, ts, window_list, contact_list, key_list, settings ),
CONTACT_REMOVE_HEADER: (remove_contact, cmd_data, ts, window_list, contact_list, group_list, key_list, ),
WIPE_USER_DATA_HEADER: (wipe, exit_queue)} # type: Dict[bytes, Any]
try:
from_dict = d[header]
except KeyError:
raise FunctionReturn("Error: Received an invalid command.")
func = from_dict[0]
parameters = from_dict[1:]
func(*parameters)
def show_win_activity(window_list: 'WindowList') -> None:
"""Show number of unread messages in each window."""
unread_wins = [w for w in window_list if (w.uid != LOCAL_ID and w.unread_messages > 0)]
print_list = ["Window activity"] if unread_wins else ["No window activity"]
print_list += [f"{w.name}: {w.unread_messages}" for w in unread_wins]
box_print(print_list)
print_on_previous_line(reps=(len(print_list) + 2), delay=1.5)
def select_win_cmd(cmd_data: bytes, window_list: 'WindowList') -> None:
"""Select window specified by TxM."""
window_uid = cmd_data.decode()
if window_uid == WIN_TYPE_FILE:
clear_screen()
window_list.select_rx_window(window_uid)
def clear_active_window() -> None:
"""Clear active screen."""
clear_screen()
def reset_active_window(cmd_data: bytes, window_list: 'WindowList') -> None:
"""Reset window specified by TxM."""
uid = cmd_data.decode()
window = window_list.get_window(uid)
window.reset_window()
os.system('reset')
def exit_tfc(exit_queue: 'Queue') -> None:
"""Exit TFC."""
exit_queue.put(EXIT)
def log_command(cmd_data: bytes,
ts: 'datetime',
window_list: 'WindowList',
contact_list: 'ContactList',
group_list: 'GroupList',
settings: 'Settings',
master_key: 'MasterKey') -> None:
"""Display or export logfile for active window."""
export = ts is not None
win_uid, no_msg_bytes = cmd_data.split(US_BYTE)
no_messages = bytes_to_int(no_msg_bytes)
window = window_list.get_window(win_uid.decode())
access_logs(window, contact_list, group_list, settings, master_key, msg_to_load=no_messages, export=export)
if export:
local_win = window_list.get_window(LOCAL_ID)
local_win.add_new(ts, f"Exported logfile of {window.type_print} {window.name}.", output=True)
def remove_log(cmd_data: bytes,
settings: 'Settings',
master_key: 'MasterKey') -> None:
"""Remove log entries for contact."""
window_name = cmd_data.decode()
remove_logs(window_name, settings, master_key)
def change_master_key(ts: 'datetime',
window_list: 'WindowList',
contact_list: 'ContactList',
group_list: 'GroupList',
key_list: 'KeyList',
settings: 'Settings',
master_key: 'MasterKey') -> None:
"""Prompt user for new master password and derive new master key from that."""
try:
old_master_key = master_key.master_key[:]
master_key.new_master_key()
phase("Re-encrypting databases")
ensure_dir(DIR_USER_DATA)
file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs'
if os.path.isfile(file_name):
re_encrypt(old_master_key, master_key.master_key, settings)
key_list.store_keys()
settings.store_settings()
contact_list.store_contacts()
group_list.store_groups()
phase(DONE)
box_print("Master key successfully changed.", head=1)
clear_screen(delay=1.5)
local_win = window_list.get_window(LOCAL_ID)
local_win.add_new(ts, "Changed RxM master key.")
except KeyboardInterrupt:
raise FunctionReturn("Password change aborted.", delay=1, head=3, tail_clear=True)
def change_nick(cmd_data: bytes,
ts: 'datetime',
window_list: 'WindowList',
contact_list: 'ContactList') -> None:
"""Change contact nick."""
account, nick = [f.decode() for f in cmd_data.split(US_BYTE)]
window = window_list.get_window(account)
window.name = nick
window.handle_dict[account] = (contact_list.get_contact(account).nick
if contact_list.has_contact(account) else account)
contact_list.get_contact(account).nick = nick
contact_list.store_contacts()
cmd_win = window_list.get_local_window()
cmd_win.add_new(ts, f"Changed {account} nick to '{nick}'", output=True)
def change_setting(cmd_data: bytes,
ts: 'datetime',
window_list: 'WindowList',
contact_list: 'ContactList',
group_list: 'GroupList',
settings: 'Settings') -> None:
"""Change TFC setting."""
setting, value = [f.decode() for f in cmd_data.split(US_BYTE)]
if setting not in settings.key_list:
raise FunctionReturn(f"Error: Invalid setting '{setting}'")
settings.change_setting(setting, value, contact_list, group_list)
local_win = window_list.get_local_window()
local_win.add_new(ts, f"Changed setting {setting} to '{value}'", output=True)
def contact_setting(cmd_data: bytes,
ts: 'datetime',
window_list: 'WindowList',
contact_list: 'ContactList',
group_list: 'GroupList',
header: bytes) -> None:
"""Change contact/group related setting."""
setting, win_uid = [f.decode() for f in cmd_data.split(US_BYTE)]
attr, desc, file_cmd = {CHANGE_LOGGING_HEADER: ('log_messages', 'Logging of messages', False),
CHANGE_FILE_R_HEADER: ('file_reception', 'Reception of files', True ),
CHANGE_NOTIFY_HEADER: ('notifications', 'Message notifications', False)}[header]
action, b_value = {ENABLE: ('enable', True),
DISABLE: ('disable', False)}[setting.lower().encode()]
if setting.isupper():
# Change settings for all contacts (and groups)
enabled = [getattr(c, attr) for c in contact_list.get_list_of_contacts()]
enabled += [getattr(g, attr) for g in group_list] if not file_cmd else []
status = "was already" if (( all(enabled) and b_value)
or (not any(enabled) and not b_value)) else 'has been'
specifier = 'every '
w_type = 'contact'
w_name = '.' if file_cmd else ' and group.'
# Set values
for c in contact_list.get_list_of_contacts():
setattr(c, attr, b_value)
contact_list.store_contacts()
if not file_cmd:
for g in group_list:
setattr(g, attr, b_value)
group_list.store_groups()
else:
# Change setting for contacts in specified window
if not window_list.has_window(win_uid):
raise FunctionReturn(f"Error: Found no window for '{win_uid}'")
window = window_list.get_window(win_uid)
group_window = window.type == WIN_TYPE_GROUP
contact_window = window.type == WIN_TYPE_CONTACT
if contact_window:
target = contact_list.get_contact(win_uid) # type: Union[Contact, Group]
else:
target = group_list.get_group(win_uid)
if file_cmd:
enabled = [getattr(m, attr) for m in window.window_contacts]
changed = not all(enabled) if b_value else any(enabled)
else:
changed = getattr(target, attr) != b_value
status = "has been" if changed else "was already"
specifier = 'members in ' if (file_cmd and group_window) else ''
w_type = window.type_print
w_name = f" {window.name}."
# Set values
if contact_window or (group_window and file_cmd):
for c in window.window_contacts:
setattr(c, attr, b_value)
contact_list.store_contacts()
elif window.type == WIN_TYPE_GROUP:
setattr(group_list.get_group(win_uid), attr, b_value)
group_list.store_groups()
message = f"{desc} {status} {action}d for {specifier}{w_type}{w_name}"
local_win = window_list.get_window(LOCAL_ID)
local_win.add_new(ts, message, output=True)
def remove_contact(cmd_data: bytes,
ts: 'datetime',
window_list: 'WindowList',
contact_list: 'ContactList',
group_list: 'GroupList',
key_list: 'KeyList') -> None:
"""Remove contact from RxM."""
rx_account = cmd_data.decode()
key_list.remove_keyset(rx_account)
window_list.remove_window(rx_account)
if not contact_list.has_contact(rx_account):
raise FunctionReturn(f"RxM has no account '{rx_account}' to remove.")
nick = contact_list.get_contact(rx_account).nick
contact_list.remove_contact(rx_account)
message = f"Removed {nick} from contacts."
box_print(message, head=1, tail=1)
local_win = window_list.get_local_window()
local_win.add_new(ts, message)
if any([g.remove_members([rx_account]) for g in group_list]):
box_print(f"Removed {rx_account} from group(s).", tail=1)
def wipe(exit_queue: 'Queue') -> None:
"""Reset terminals, wipe all user data on RxM and power off system.
No effective RAM overwriting tool currently exists, so as long as TxM/RxM
use FDE and DDR3 memory, recovery of user data becomes impossible very fast:
https://www1.cs.fau.de/filepool/projects/coldboot/fares_coldboot.pdf
"""
os.system('reset')
exit_queue.put(WIPE)