270 lines
11 KiB
Python
270 lines
11 KiB
Python
#!/usr/bin/env python3.7
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
TFC - Onion-routed, endpoint secure messaging system
|
|
Copyright (C) 2013-2020 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 <https://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
import textwrap
|
|
import time
|
|
import typing
|
|
import sys
|
|
|
|
from datetime import datetime
|
|
from typing import List, Optional, Tuple, Union
|
|
|
|
from src.common.encoding import b10encode, b58encode, pub_key_to_onion_address
|
|
from src.common.misc import get_terminal_width, split_string
|
|
from src.common.statics import (ADDED_MEMBERS, ALREADY_MEMBER, B58_LOCAL_KEY_GUIDE, B58_PUBLIC_KEY_GUIDE, BOLD_ON,
|
|
CLEAR_ENTIRE_LINE, CLEAR_ENTIRE_SCREEN, CURSOR_LEFT_UP_CORNER, CURSOR_UP_ONE_LINE,
|
|
DONE, NC, NEW_GROUP, NORMAL_TEXT, NOT_IN_GROUP, RECEIVER, RELAY, REMOVED_MEMBERS, RX,
|
|
TFC, TRANSMITTER, TX, UNKNOWN_ACCOUNTS, VERSION)
|
|
|
|
if typing.TYPE_CHECKING:
|
|
from src.common.db_contacts import ContactList
|
|
from src.common.db_settings import Settings
|
|
from src.common.gateway import GatewaySettings as GWSettings
|
|
|
|
MsgListType = Union[str, List[str]]
|
|
|
|
|
|
def clear_screen(delay: float = 0.0) -> None:
|
|
"""Clear the terminal window."""
|
|
time.sleep(delay)
|
|
sys.stdout.write(CLEAR_ENTIRE_SCREEN + CURSOR_LEFT_UP_CORNER)
|
|
sys.stdout.flush()
|
|
|
|
|
|
def group_management_print(key: str, # Group management message identifier
|
|
members: List[bytes], # List of members' Onion public keys
|
|
contact_list: 'ContactList', # ContactList object
|
|
group_name: str = '' # Name of the group
|
|
) -> None:
|
|
"""Print group management command results."""
|
|
m = {NEW_GROUP: "Created new group '{}' with following members:".format(group_name),
|
|
ADDED_MEMBERS: "Added following accounts to group '{}':" .format(group_name),
|
|
ALREADY_MEMBER: "Following accounts were already in group '{}':".format(group_name),
|
|
REMOVED_MEMBERS: "Removed following members from group '{}':" .format(group_name),
|
|
NOT_IN_GROUP: "Following accounts were not in group '{}':" .format(group_name),
|
|
UNKNOWN_ACCOUNTS: "Following unknown accounts were ignored:"}[key]
|
|
|
|
if members:
|
|
m_list = ([contact_list.get_nick_by_pub_key(m) for m in members if contact_list.has_pub_key(m)]
|
|
+ [pub_key_to_onion_address(m) for m in members if not contact_list.has_pub_key(m)])
|
|
|
|
just_len = max(len(m) for m in m_list)
|
|
justified = [m] + [f" * {m.ljust(just_len)}" for m in m_list]
|
|
m_print(justified, box=True)
|
|
|
|
|
|
def m_print(msg_list: Union[str, List[str]], # List of lines to print
|
|
manual_proceed: bool = False, # Wait for user input before continuing
|
|
bold: bool = False, # When True, prints the message in bold style
|
|
center: bool = True, # When False, does not center message
|
|
box: bool = False, # When True, prints a box around the message
|
|
head_clear: bool = False, # When True, clears screen before printing message
|
|
tail_clear: bool = False, # When True, clears screen after printing message (requires delay)
|
|
delay: float = 0, # Delay before continuing
|
|
max_width: int = 0, # Maximum width of message
|
|
head: int = 0, # Number of new lines to print before the message
|
|
tail: int = 0, # Number of new lines to print after the message
|
|
) -> None:
|
|
"""Print message to screen.
|
|
|
|
The message automatically wraps if the terminal is too narrow to
|
|
display the message.
|
|
"""
|
|
if isinstance(msg_list, str):
|
|
msg_list = [msg_list]
|
|
|
|
terminal_width = get_terminal_width()
|
|
len_widest_msg, msg_list = split_too_wide_messages(box, max_width, msg_list, terminal_width)
|
|
|
|
if box or center:
|
|
# Insert whitespace around every line to make them equally long
|
|
msg_list = [f'{m:^{len_widest_msg}}' for m in msg_list]
|
|
|
|
if box:
|
|
# Add box chars around the message
|
|
msg_list = [f'│ {m} │' for m in msg_list]
|
|
msg_list.insert(0, '┌' + (len_widest_msg + 2) * '─' + '┐')
|
|
msg_list.append( '└' + (len_widest_msg + 2) * '─' + '┘')
|
|
|
|
# Print the message
|
|
if head_clear:
|
|
clear_screen()
|
|
print_spacing(head)
|
|
|
|
for message in msg_list:
|
|
if center:
|
|
message = message.center(terminal_width)
|
|
if bold:
|
|
message = BOLD_ON + message + NORMAL_TEXT
|
|
print(message)
|
|
|
|
print_spacing(tail)
|
|
time.sleep(delay)
|
|
if tail_clear:
|
|
clear_screen()
|
|
|
|
# Check if message needs to be manually dismissed
|
|
if manual_proceed:
|
|
input('')
|
|
print_on_previous_line()
|
|
|
|
|
|
def split_too_wide_messages(box: bool,
|
|
max_width: int,
|
|
msg_list: 'MsgListType',
|
|
terminal_width: int
|
|
) -> Tuple[int, 'MsgListType']:
|
|
"""Split too wide messages to multiple lines."""
|
|
len_widest_msg = max(len(m) for m in msg_list)
|
|
spc_around_msg = 4 if box else 2
|
|
max_msg_width = terminal_width - spc_around_msg
|
|
|
|
if max_width:
|
|
max_msg_width = min(max_width, max_msg_width)
|
|
|
|
if len_widest_msg > max_msg_width:
|
|
new_msg_list = []
|
|
for msg in msg_list:
|
|
if len(msg) > max_msg_width:
|
|
new_msg_list.extend(textwrap.fill(msg, max_msg_width).split("\n"))
|
|
else:
|
|
new_msg_list.append(msg)
|
|
|
|
msg_list = new_msg_list
|
|
len_widest_msg = max(len(m) for m in msg_list)
|
|
|
|
return len_widest_msg, msg_list
|
|
|
|
|
|
def phase(string: str, # Description of the phase
|
|
done: bool = False, # When True, uses string as the phase completion message
|
|
head: int = 0, # Number of inserted new lines before print
|
|
tail: int = 0, # Number of inserted new lines after print
|
|
offset: int = 4, # Offset of phase string from center to left
|
|
delay: float = 0.5 # Duration of phase completion message
|
|
) -> None:
|
|
"""Print the name of the next phase.
|
|
|
|
The notification of completion of the phase is printed on the same
|
|
line as the phase message.
|
|
"""
|
|
print_spacing(head)
|
|
|
|
if string == DONE or done:
|
|
print(string)
|
|
time.sleep(delay)
|
|
else:
|
|
string += '... '
|
|
indent = ((get_terminal_width() - (len(string) + offset)) // 2) * ' '
|
|
|
|
print(indent + string, end='', flush=True)
|
|
|
|
print_spacing(tail)
|
|
|
|
|
|
def print_fingerprint(fp: bytes, # Contact's fingerprint
|
|
msg: str = '' # Title message
|
|
) -> None:
|
|
"""Print a formatted message and fingerprint inside the box.
|
|
|
|
Truncate fingerprint for clean layout with three rows that have
|
|
five groups of five numbers. The resulting fingerprint has
|
|
249.15 bits of entropy which is more than the symmetric security
|
|
of X448.
|
|
"""
|
|
p_lst = [msg, ''] if msg else []
|
|
b10fp = b10encode(fp)[:(3*5*5)]
|
|
parts = split_string(b10fp, item_len=(5*5))
|
|
p_lst += [' '.join(split_string(p, item_len=5)) for p in parts]
|
|
|
|
m_print(p_lst, box=True)
|
|
|
|
|
|
def print_key(message: str, # Instructive message
|
|
key_bytes: bytes, # 32-byte key to be displayed
|
|
settings: Union['Settings', 'GWSettings'], # Settings object
|
|
public_key: bool = False # When True, uses Testnet address WIF format
|
|
) -> None:
|
|
"""Print a symmetric key in WIF format.
|
|
|
|
If serial-interface based platform is used, this function adds
|
|
spacing in the middle of the key, as well as guide letters to help
|
|
the user keep track of typing progress:
|
|
|
|
Local key encryption keys:
|
|
|
|
A B C D E F G H I J K L M N O P Q
|
|
5Ka 52G yNz vjF nM4 2jw Duu rWo 7di zgi Y8g iiy yGd 78L cCx mwQ mWV
|
|
|
|
X448 public keys:
|
|
|
|
A B C D E F H H I J K L
|
|
4EcuqaD ddsdsuc gBX2PY2 qR8hReA aeSN2oh JB9w5Cv q6BQjDa PPgzSvW 932aHio sT42SKJ Gu2PpS1 Za3Xrao
|
|
"""
|
|
b58key = b58encode(key_bytes, public_key)
|
|
if settings.local_testing_mode or settings.qubes:
|
|
m_print([message, b58key], box=True)
|
|
else:
|
|
guide, chunk_length = (B58_PUBLIC_KEY_GUIDE, 7) if public_key else (B58_LOCAL_KEY_GUIDE, 3)
|
|
|
|
key = ' '.join(split_string(b58key, item_len=chunk_length))
|
|
m_print([message, guide, key], box=True)
|
|
|
|
|
|
def print_title(operation: str) -> None:
|
|
"""Print the TFC title."""
|
|
operation_name = {TX: TRANSMITTER, RX: RECEIVER, NC: RELAY}[operation]
|
|
m_print(f"{TFC} - {operation_name} {VERSION}", bold=True, head_clear=True, head=1, tail=1)
|
|
|
|
|
|
def print_on_previous_line(reps: int = 1, # Number of times to repeat the action
|
|
delay: float = 0.0, # Time to sleep before clearing lines above
|
|
flush: bool = False # Flush stdout when true
|
|
) -> None:
|
|
"""Next message is printed on upper line."""
|
|
time.sleep(delay)
|
|
|
|
for _ in range(reps):
|
|
sys.stdout.write(CURSOR_UP_ONE_LINE + CLEAR_ENTIRE_LINE)
|
|
if flush:
|
|
sys.stdout.flush()
|
|
|
|
|
|
def print_spacing(count: int = 0) -> None:
|
|
"""Print `count` many new-lines."""
|
|
for _ in range(count):
|
|
print()
|
|
|
|
|
|
def rp_print(message: str, # Message to print
|
|
ts: Optional['datetime'] = None, # Timestamp for displayed event
|
|
bold: bool = False # When True, prints the message in bold style
|
|
) -> None:
|
|
"""Print an event in Relay Program."""
|
|
if ts is None:
|
|
ts = datetime.now()
|
|
ts_fmt = ts.strftime('%b %d - %H:%M:%S.%f')[:-4]
|
|
|
|
if bold:
|
|
print(f"{BOLD_ON}{ts_fmt} - {message}{NORMAL_TEXT}")
|
|
else:
|
|
print(f"{ts_fmt} - {message}")
|