262 lines
9.8 KiB
Python
262 lines
9.8 KiB
Python
#!/usr/bin/env python3.7
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
TFC - Onion-routed, endpoint secure messaging system
|
|
Copyright (C) 2013-2019 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 getpass
|
|
import typing
|
|
|
|
from typing import Any, Callable, Optional
|
|
|
|
from src.common.encoding import b58decode
|
|
from src.common.exceptions import CriticalError
|
|
from src.common.misc import get_terminal_width, terminal_width_check
|
|
from src.common.output import clear_screen, m_print, print_on_previous_line, print_spacing
|
|
from src.common.statics import (B58_LOCAL_KEY, B58_LOCAL_KEY_GUIDE, B58_PUBLIC_KEY, B58_PUBLIC_KEY_GUIDE,
|
|
CURSOR_UP_ONE_LINE, ECDHE, NC_BYPASS_START, NC_BYPASS_STOP)
|
|
|
|
if typing.TYPE_CHECKING:
|
|
from src.common.db_settings import Settings
|
|
|
|
|
|
Validator = Callable[..., str]
|
|
|
|
|
|
def ask_confirmation_code(source: str # The system the confirmation code is displayed by
|
|
) -> str: # The confirmation code entered by the user
|
|
"""\
|
|
Ask the user to input confirmation code from Source Computer to
|
|
verify local key has been installed.
|
|
"""
|
|
title = f"Enter confirmation code (from {source}): "
|
|
input_space = len(' ff ')
|
|
|
|
upper_line = ('┌' + (len(title) + input_space) * '─' + '┐')
|
|
title_line = ('│' + title + input_space * ' ' + '│')
|
|
lower_line = ('└' + (len(title) + input_space) * '─' + '┘')
|
|
|
|
terminal_w = get_terminal_width()
|
|
upper_line = upper_line.center(terminal_w)
|
|
title_line = title_line.center(terminal_w)
|
|
lower_line = lower_line.center(terminal_w)
|
|
|
|
terminal_width_check(len(upper_line))
|
|
|
|
print(upper_line)
|
|
print(title_line)
|
|
print(lower_line)
|
|
print(3 * CURSOR_UP_ONE_LINE)
|
|
|
|
indent = title_line.find('│')
|
|
return input(indent * ' ' + f'│ {title}')
|
|
|
|
|
|
def box_input(message: str, # Input prompt message
|
|
default: str = '', # Default return value
|
|
head: int = 0, # Number of new lines to print before the input
|
|
tail: int = 1, # Number of new lines to print after input
|
|
expected_len: int = 0, # Expected length of the input
|
|
key_type: str = '', # When specified, sets input width
|
|
guide: bool = False, # When True, prints the guide for key
|
|
validator: Optional[Validator] = None, # Input validator function
|
|
validator_args: Optional[Any] = None # Arguments required by the validator
|
|
) -> str: # Input from user
|
|
"""Display boxed input prompt with a message."""
|
|
print_spacing(head)
|
|
|
|
terminal_width = get_terminal_width()
|
|
|
|
if key_type:
|
|
key_guide = {B58_LOCAL_KEY: B58_LOCAL_KEY_GUIDE,
|
|
B58_PUBLIC_KEY: B58_PUBLIC_KEY_GUIDE}.get(key_type, '')
|
|
if guide:
|
|
inner_spc = len(key_guide) + 2
|
|
else:
|
|
inner_spc = (86 if key_type == B58_PUBLIC_KEY else 53)
|
|
else:
|
|
key_guide = ''
|
|
inner_spc = terminal_width - 2 if expected_len == 0 else expected_len + 2
|
|
|
|
upper_line = '┌' + inner_spc * '─' + '┐'
|
|
guide_line = '│ ' + key_guide + ' │'
|
|
input_line = '│' + inner_spc * ' ' + '│'
|
|
lower_line = '└' + inner_spc * '─' + '┘'
|
|
box_indent = (terminal_width - len(upper_line)) // 2 * ' '
|
|
|
|
terminal_width_check(len(upper_line))
|
|
|
|
print(box_indent + upper_line)
|
|
if guide:
|
|
print(box_indent + guide_line)
|
|
print(box_indent + input_line)
|
|
print(box_indent + lower_line)
|
|
print((5 if guide else 4) * CURSOR_UP_ONE_LINE)
|
|
print(box_indent + '┌─┤' + message + '├')
|
|
if guide:
|
|
print('')
|
|
|
|
user_input = input(box_indent + '│ ')
|
|
|
|
if user_input == '':
|
|
print(2 * CURSOR_UP_ONE_LINE)
|
|
print(box_indent + '│ ' + default)
|
|
user_input = default
|
|
|
|
if validator is not None:
|
|
error_msg = validator(user_input, validator_args)
|
|
if error_msg:
|
|
m_print(error_msg, head=1)
|
|
print_on_previous_line(reps=4, delay=1)
|
|
return box_input(message, default, head, tail, expected_len, key_type, guide, validator, validator_args)
|
|
|
|
print_spacing(tail)
|
|
|
|
return user_input
|
|
|
|
|
|
def get_b58_key(key_type: str, # The type of Base58 key to be entered
|
|
settings: 'Settings', # Settings object
|
|
short_address: str = '' # The contact's short Onion address
|
|
) -> bytes: # The Base58 decoded key
|
|
"""Ask the user to input a Base58 encoded key."""
|
|
if key_type == B58_PUBLIC_KEY:
|
|
clear_screen()
|
|
m_print(f"{ECDHE} key exchange", head=1, tail=1, bold=True)
|
|
m_print("If needed, resend your public key to the contact by pressing <Enter>", tail=1)
|
|
|
|
box_msg = f"Enter public key of {short_address} (from Relay)"
|
|
elif key_type == B58_LOCAL_KEY:
|
|
box_msg = "Enter local key decryption key (from Transmitter)"
|
|
else:
|
|
raise CriticalError("Invalid key type")
|
|
|
|
while True:
|
|
rx_pk = box_input(box_msg, key_type=key_type, guide=not settings.local_testing_mode)
|
|
rx_pk = ''.join(rx_pk.split())
|
|
|
|
if key_type == B58_PUBLIC_KEY and rx_pk == '':
|
|
return rx_pk.encode()
|
|
|
|
try:
|
|
return b58decode(rx_pk, public_key=(key_type == B58_PUBLIC_KEY))
|
|
except ValueError:
|
|
m_print("Checksum error - Check that the entered key is correct.")
|
|
print_on_previous_line(reps=(4 if settings.local_testing_mode else 5), delay=1)
|
|
|
|
|
|
def nc_bypass_msg(key: str, settings: 'Settings') -> None:
|
|
"""Print messages about bypassing Networked Computer.
|
|
|
|
During ciphertext delivery of local key exchange, these bypass
|
|
messages tell the user when to bypass and remove bypass of Networked
|
|
Computer. Bypass of Networked Computer makes initial bootstrap more
|
|
secure by denying remote attacker the access to the encrypted local
|
|
key. Without the ciphertext, e.g. a visually collected local key
|
|
decryption key is useless.
|
|
"""
|
|
m = {NC_BYPASS_START: "Bypass Networked Computer if needed. Press <Enter> to send local key.",
|
|
NC_BYPASS_STOP: "Remove bypass of Networked Computer. Press <Enter> to continue."}
|
|
|
|
if settings.nc_bypass_messages:
|
|
m_print(m[key], manual_proceed=True, box=True, head=(1 if key == NC_BYPASS_STOP else 0))
|
|
|
|
|
|
def pwd_prompt(message: str, # Prompt message
|
|
repeat: bool = False # When True, prints corner chars for the second box
|
|
) -> str: # Password from user
|
|
"""Prompt the user to enter a password.
|
|
|
|
The getpass library ensures the password is not echoed on screen
|
|
when it is typed.
|
|
"""
|
|
l, r = ('├', '┤') if repeat else ('┌', '┐')
|
|
|
|
terminal_w = get_terminal_width()
|
|
input_space = len(' c ') # `c` is where the caret sits
|
|
|
|
upper_line = ( l + (len(message) + input_space) * '─' + r ).center(terminal_w)
|
|
title_line = ('│' + message + input_space * ' ' + '│').center(terminal_w)
|
|
lower_line = ('└' + (len(message) + input_space) * '─' + '┘').center(terminal_w)
|
|
|
|
terminal_width_check(len(upper_line))
|
|
|
|
print(upper_line)
|
|
print(title_line)
|
|
print(lower_line)
|
|
print(3 * CURSOR_UP_ONE_LINE)
|
|
|
|
indent = title_line.find('│')
|
|
user_input = getpass.getpass(indent * ' ' + f'│ {message}')
|
|
|
|
return user_input
|
|
|
|
|
|
def yes(prompt: str, # Question to be asked
|
|
abort: Optional[bool] = None, # Determines the return value of ^C and ^D
|
|
head: int = 0, # Number of new lines to print before prompt
|
|
tail: int = 0 # Number of new lines to print after prompt
|
|
) -> bool: # True/False depending on input
|
|
"""Prompt the user a question that is answered with yes/no."""
|
|
print_spacing(head)
|
|
|
|
prompt = f"{prompt} (y/n): "
|
|
input_space = len(' yes ')
|
|
|
|
upper_line = ('┌' + (len(prompt) + input_space) * '─' + '┐')
|
|
title_line = ('│' + prompt + input_space * ' ' + '│')
|
|
lower_line = ('└' + (len(prompt) + input_space) * '─' + '┘')
|
|
|
|
terminal_w = get_terminal_width()
|
|
upper_line = upper_line.center(terminal_w)
|
|
title_line = title_line.center(terminal_w)
|
|
lower_line = lower_line.center(terminal_w)
|
|
|
|
indent = title_line.find('│')
|
|
|
|
terminal_width_check(len(upper_line))
|
|
|
|
print(upper_line)
|
|
while True:
|
|
print(title_line)
|
|
print(lower_line)
|
|
print(3 * CURSOR_UP_ONE_LINE)
|
|
|
|
try:
|
|
user_input = input(indent * ' ' + f'│ {prompt}')
|
|
except (EOFError, KeyboardInterrupt):
|
|
if abort is None:
|
|
raise
|
|
print('')
|
|
user_input = 'y' if abort else 'n'
|
|
|
|
print_on_previous_line()
|
|
|
|
if user_input == '':
|
|
continue
|
|
|
|
if user_input.lower() in ['y', 'yes']:
|
|
print(indent * ' ' + f'│ {prompt}Yes │\n')
|
|
print_spacing(tail)
|
|
return True
|
|
|
|
elif user_input.lower() in ['n', 'no']:
|
|
print(indent * ' ' + f'│ {prompt}No │\n')
|
|
print_spacing(tail)
|
|
return False
|