0.17.04
This commit is contained in:
parent
9b9559ee2a
commit
29f285b1f4
|
@ -0,0 +1,14 @@
|
|||
language: python
|
||||
|
||||
python:
|
||||
- "3.6"
|
||||
|
||||
install:
|
||||
- pip install pytest pytest-cov pyyaml coveralls
|
||||
- pip install -r requirements.txt
|
||||
|
||||
script:
|
||||
- py.test --cov=src tests/
|
||||
|
||||
after_success:
|
||||
- coveralls
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,6 @@
|
|||
<img align="right" src="https://cs.helsinki.fi/u/oottela/tfclogo.png" style="position: relative; top: 0; left: 0;">
|
||||
|
||||
|
||||
###Tinfoil Chat
|
||||
### Tinfoil Chat
|
||||
|
||||
Tinfoil Chat (TFC) is a high assurance encrypted messaging system that
|
||||
operates on top of existing IM clients. The
|
||||
|
@ -16,15 +15,9 @@ organized crime and nation state attackers.
|
|||
and [Poly1305-AES](https://cr.yp.to/mac/poly1305-20050329.pdf) MACs provide
|
||||
[end-to-end encrypted](https://en.wikipedia.org/wiki/End-to-end_encryption)
|
||||
communication with [deniable authentication](https://en.wikipedia.org/wiki/Deniable_encryption#Deniable_authentication): Symmetric keys are either
|
||||
pre-shared, or exchanged using [Curve25519 ECDHE](https://cr.yp.to/ecdh/curve25519-20060209.pdf), the public
|
||||
keys of which are verified via off-band channel.
|
||||
|
||||
Key generation relies on Kernel CSPRNG, but also supports mixing of
|
||||
external entropy from [open circuit design HWRNG](http://holdenc.altervista.org/avalanche/),
|
||||
that is sampled by a [RPi](https://www.raspberrypi.org/) through it's
|
||||
GPIO interface either natively, or remotely (SSH over direct ethernet
|
||||
cable). TFC provides per-message forward secrecy with PBKDF2-HMAC-SHA256
|
||||
[hash ratchet](https://en.wikipedia.org/wiki/Double_Ratchet_Algorithm).
|
||||
pre-shared, or exchanged using [X25519](https://cr.yp.to/ecdh/curve25519-20060209.pdf), the public
|
||||
keys of which are verified via off-band channel. TFC provides per-message forward secrecy with [hash ratchet](https://en.wikipedia.org/wiki/Double_Ratchet_Algorithm)
|
||||
the KDF of which chains SHA3-256, Blake2s and SHA256.
|
||||
|
||||
The software is used in hardware configuration that provides strong
|
||||
endpoint security: Encryption and decryption are separated on two
|
||||
|
@ -47,33 +40,33 @@ inserted into a constant stream of encrypted noise traffic. Covert file
|
|||
transfer can take place in background during conversation over the
|
||||
trickle connection.
|
||||
|
||||
###How it works
|
||||
### How it works
|
||||
|
||||
![](https://cs.helsinki.fi/u/oottela/tfcwiki/tfc_overview.png)
|
||||
![](https://cs.helsinki.fi/u/oottela/tfcwiki/tfc_overview2.jpg)
|
||||
|
||||
TFC uses three computers per endpoint. Alice enters her messages and
|
||||
commands to program Tx.py running on her transmitter computer (TxM), a
|
||||
TCB separated from network. Tx.py encrypts and signs plaintext data and
|
||||
relays the ciphertext from TxM to her networked computer (NH) trough a
|
||||
serial (RS-232) interface and a hardware data diode.
|
||||
commands to Transmitter program running on her transmitter computer (TxM), a
|
||||
TCB separated from network. The transmitter program encrypts and signs
|
||||
plaintext data and relays the ciphertext from TxM to her networked computer
|
||||
(NH) trough a serial interface and a hardware data diode.
|
||||
|
||||
Messages and commands received to NH are relayed to IM client (Pidgin or
|
||||
Finch), and to Alice's receiver computer (RxM) via another serial interface
|
||||
and data diode. The program Rx.py on Alice's RxM authenticates, decrypts
|
||||
and data diode. The Receiver program on Alice's RxM authenticates, decrypts
|
||||
and processes the received messages and commands.
|
||||
|
||||
The IM client sends the packet either directly or through Tor network to
|
||||
IM server, that then forwards it directly (or again through Tor) to Bob.
|
||||
|
||||
IM client on Bob's NH forwards packet to NH.py, that then forwards it to
|
||||
Bob's RxM (again through data diode enforced serial interface). Bob's
|
||||
Rx.py on his RxM then authenticates, decrypts, and processes the packet.
|
||||
IM client on Bob's NH forwards packet to nh.py plugin program, that then
|
||||
forwards it to Bob's RxM (again through data diode enforced serial interface).
|
||||
Bob's Receiver program on his RxM then authenticates, decrypts, and processes the packet.
|
||||
|
||||
When the Bob responds, he will send the message using Tx.py on his
|
||||
TxM and in the end, Alice reads the message from Rx.py on her RxM.
|
||||
When the Bob responds, he will type the message to his Transmitter computer and in the end,
|
||||
Alice reads the message from her Receiver computer.
|
||||
|
||||
|
||||
###Why keys can not be exfiltrated
|
||||
### Why keys can not be exfiltrated
|
||||
|
||||
1. Malware that exploits an unknown vulnerability in RxM can infiltrate
|
||||
the system, but is unable to exfiltrate keys or plaintexts, as data
|
||||
|
@ -86,49 +79,35 @@ is manually typed by the user.
|
|||
3. The NH is assumed to be compromised: all sensitive data that passes
|
||||
through NH is always encrypted and signed.
|
||||
|
||||
![](https://cs.helsinki.fi/u/oottela/tfcwiki/tfc_attacks.png)
|
||||
![](https://cs.helsinki.fi/u/oottela/tfcwiki/tfc_attacks2.jpg)
|
||||
|
||||
Optical repeater inside the [optocoupler](https://en.wikipedia.org/wiki/Opto-isolator)
|
||||
of the data diode (below) enforces direction of data transmission with
|
||||
the laws of physics.
|
||||
|
||||
<img src="https://cs.helsinki.fi/u/oottela/tfcwiki/pbdd.jpg" align="center" width="74%" height="74%"/>
|
||||
<img src="https://cs.helsinki.fi/u/oottela/tfcwiki/bbdd.jpg" align="center" width="74%" height="74%"/>
|
||||
|
||||
###Supported Operating Systems
|
||||
### Supported Operating Systems
|
||||
|
||||
####TxM and RxM
|
||||
#### TxM and RxM
|
||||
- *buntu 16.04
|
||||
- Linux Mint 18 Sarah
|
||||
- Raspbian Jessie (Only use RPi version
|
||||
[1](https://www.raspberrypi.org/products/model-b-plus/) or
|
||||
[2](https://www.raspberrypi.org/products/raspberry-pi-2-model-b/))
|
||||
- Linux Mint 18.1 Serena
|
||||
|
||||
####NH
|
||||
- Tails 2.6
|
||||
#### NH
|
||||
- Tails 3.0
|
||||
- *buntu 16.04
|
||||
- Linux Mint 18 Sarah
|
||||
- Raspbian Jessie
|
||||
|
||||
###More information
|
||||
- Linux Mint 18.1 Serena
|
||||
|
||||
### More information
|
||||
[Threat model](https://github.com/maqp/tfc/wiki/Threat-model)<br>
|
||||
[FAQ](https://github.com/maqp/tfc/wiki/FAQ)<br>
|
||||
[Security design](https://github.com/maqp/tfc/wiki/Security-design)<br>
|
||||
|
||||
In depth<br>
|
||||
[Security design](https://github.com/maqp/tfc/wiki/Security-design)<br>
|
||||
[Protocol](https://github.com/maqp/tfc/wiki/Protocol)<br>
|
||||
|
||||
Hardware<br>
|
||||
[Hardware configurations](https://github.com/maqp/tfc/wiki/Hardware-configurations)<br>
|
||||
|
||||
[Data diode (perfboard)](https://github.com/maqp/tfc/wiki/Data-Diode-(perfboard))<br>
|
||||
[Data diode (point to point)](https://github.com/maqp/tfc/wiki/Data-diode-(point-to-point))<br>
|
||||
|
||||
[HWRNG (perfboard)](https://github.com/maqp/tfc/wiki/HWRNG-(perfboard))<br>
|
||||
[HWRNG (breadboard)](https://github.com/maqp/tfc/wiki/HWRNG-(breadboard))<br>
|
||||
Hardware<Br>
|
||||
[Data diode (breadboard)](https://github.com/maqp/tfc/wiki/TTL-Data-Diode-(breadboard))<br>
|
||||
|
||||
Software<Br>
|
||||
[Installation](https://github.com/maqp/tfc/wiki/Installation)<br>
|
||||
[How to use](https://github.com/maqp/tfc/wiki/How-to-use)<br>
|
||||
|
||||
[Update Log](https://github.com/maqp/tfc/wiki/Update-Log)<br>
|
||||
[Update Log](https://github.com/maqp/tfc/wiki/Update-Log)<br>
|
|
@ -1,10 +1,8 @@
|
|||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3.6
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# TFC 0.16.10 || dd.py
|
||||
|
||||
"""
|
||||
Copyright (C) 2013-2016 Markus Ottela
|
||||
Copyright (C) 2013-2017 Markus Ottela
|
||||
|
||||
This file is part of TFC.
|
||||
|
||||
|
@ -12,265 +10,214 @@ 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.
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with TFC. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import multiprocessing.connection
|
||||
import multiprocessing
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
from multiprocessing import Queue, Process
|
||||
|
||||
|
||||
###############################################################################
|
||||
# DATA DIODE FRAMES #
|
||||
# DATA DIODE ANIMATION FRAMES #
|
||||
###############################################################################
|
||||
|
||||
def lr_upper():
|
||||
def lr_upper() -> None:
|
||||
"""Print high signal frame (left to right)."""
|
||||
|
||||
print("""
|
||||
Data flow
|
||||
→
|
||||
Tx───┬─╮ ╭──────╮
|
||||
│ S > R █±6V
|
||||
GND━━━┿━┥ ├──Rx ├──GND
|
||||
│ S R █±6V
|
||||
╰─╯ ╰──────╯""")
|
||||
Data flow
|
||||
→
|
||||
─────╮ ╭─────
|
||||
Tx │ > │ Rx
|
||||
─────╯ ╰─────
|
||||
""")
|
||||
|
||||
|
||||
def lr_lower():
|
||||
def lr_lower() -> None:
|
||||
"""Print low signal frame (left to right)."""
|
||||
|
||||
print("""
|
||||
Data flow
|
||||
→
|
||||
Tx───┬─╮ ╭──────╮
|
||||
│ S R █±6V
|
||||
GND━━━┿━┥ ├──Rx ├──GND
|
||||
│ S > R █±6V
|
||||
╰─╯ ╰──────╯""")
|
||||
Data flow
|
||||
→
|
||||
─────╮ ╭─────
|
||||
Tx │ │ Rx
|
||||
─────╯ ╰─────
|
||||
""")
|
||||
|
||||
|
||||
def lr_idle():
|
||||
def lr_idle() -> None:
|
||||
"""Print no signal frame (left to right)."""
|
||||
|
||||
print("""
|
||||
Data flow
|
||||
→
|
||||
Tx───┬─╮ ╭──────╮
|
||||
│ S R █±6V
|
||||
GND━━━┿━┥ ├──Rx ├──GND
|
||||
│ S R █±6V
|
||||
╰─╯ ╰──────╯""")
|
||||
Idle
|
||||
|
||||
─────╮ ╭─────
|
||||
Tx │ │ Rx
|
||||
─────╯ ╰─────
|
||||
""")
|
||||
|
||||
|
||||
def rl_upper():
|
||||
def rl_upper() -> None:
|
||||
"""Print high signal frame (right to left)."""
|
||||
|
||||
print("""
|
||||
Data flow
|
||||
←
|
||||
╭──────╮ ╭─┬───Tx
|
||||
±6V█ R < S │
|
||||
GND──┤ Rx──┤ ┝━┿━━━GND
|
||||
±6V█ R S │
|
||||
╰──────╯ ╰─╯""")
|
||||
Data flow
|
||||
←
|
||||
─────╮ ╭─────
|
||||
Rx │ < │ Tx
|
||||
─────╯ ╰─────
|
||||
""")
|
||||
|
||||
|
||||
def rl_lower():
|
||||
def rl_lower() -> None:
|
||||
"""Print low signal frame (right to left)."""
|
||||
|
||||
print("""
|
||||
Data flow
|
||||
←
|
||||
╭──────╮ ╭─┬───Tx
|
||||
±6V█ R S │
|
||||
GND──┤ Rx──┤ ┝━┿━━━GND
|
||||
±6V█ R < S │
|
||||
╰──────╯ ╰─╯""")
|
||||
Data flow
|
||||
←
|
||||
─────╮ ╭─────
|
||||
Rx │ │ Tx
|
||||
─────╯ ╰─────
|
||||
""")
|
||||
|
||||
|
||||
def rl_idle():
|
||||
def rl_idle() -> None:
|
||||
"""Print no signal frame (right to left)."""
|
||||
|
||||
print("""
|
||||
Data flow
|
||||
←
|
||||
╭──────╮ ╭─┬───Tx
|
||||
±6V█ R S │
|
||||
GND──┤ Rx──┤ ┝━┿━━━GND
|
||||
±6V█ R S │
|
||||
╰──────╯ ╰─╯""")
|
||||
Idle
|
||||
|
||||
─────╮ ╭─────
|
||||
Rx │ │ Tx
|
||||
─────╯ ╰─────
|
||||
""")
|
||||
|
||||
|
||||
###############################################################################
|
||||
# DATA DIODE ANIMATORS #
|
||||
###############################################################################
|
||||
|
||||
def lr():
|
||||
"""Draw animation (left to right)."""
|
||||
def clear_screen() -> None:
|
||||
"""Clear terminal window."""
|
||||
sys.stdout.write('\x1b[2J\x1b[H')
|
||||
sys.stdout.flush()
|
||||
|
||||
for _ in range(10):
|
||||
os.system("clear")
|
||||
|
||||
def lr_animate() -> None:
|
||||
"""Draw animation (left to right)."""
|
||||
for _ in range(8):
|
||||
clear_screen()
|
||||
lr_lower()
|
||||
time.sleep(0.04)
|
||||
os.system("clear")
|
||||
clear_screen()
|
||||
lr_upper()
|
||||
time.sleep(0.04)
|
||||
os.system("clear")
|
||||
clear_screen()
|
||||
|
||||
|
||||
def rl():
|
||||
def rl_animate() -> None:
|
||||
"""Draw animation (right to left)."""
|
||||
|
||||
for _ in range(10):
|
||||
os.system("clear")
|
||||
for _ in range(8):
|
||||
clear_screen()
|
||||
rl_lower()
|
||||
time.sleep(0.04)
|
||||
os.system("clear")
|
||||
clear_screen()
|
||||
rl_upper()
|
||||
time.sleep(0.04)
|
||||
os.system("clear")
|
||||
clear_screen()
|
||||
|
||||
|
||||
###############################################################################
|
||||
# DATA DIODE PROCESSES #
|
||||
###############################################################################
|
||||
|
||||
def tx_process():
|
||||
"""Process that reads from sending computer."""
|
||||
|
||||
if tx_nh_lr or nh_rx_rl:
|
||||
def tx_process(io_queue: 'Queue',
|
||||
output_socket: int,
|
||||
argv: str) -> None:
|
||||
"""Process that sends to receiving computer."""
|
||||
if argv in ['txnhlr', 'nhrxrl']:
|
||||
lr_idle()
|
||||
|
||||
if tx_nh_rl or nh_rx_lr:
|
||||
if argv in ['txnhrl', 'nhrxlr']:
|
||||
rl_idle()
|
||||
|
||||
while True:
|
||||
if io_queue.empty():
|
||||
time.sleep(0.001)
|
||||
continue
|
||||
try:
|
||||
interface = multiprocessing.connection.Client(('localhost', output_socket))
|
||||
break
|
||||
except socket.error:
|
||||
time.sleep(0.01)
|
||||
|
||||
msg = io_queue.get()
|
||||
while True:
|
||||
try:
|
||||
while io_queue.empty():
|
||||
time.sleep(0.01)
|
||||
|
||||
if tx_nh_lr or nh_rx_rl:
|
||||
lr()
|
||||
lr_idle()
|
||||
msg = io_queue.get()
|
||||
|
||||
if tx_nh_rl or nh_rx_lr:
|
||||
rl()
|
||||
rl_idle()
|
||||
if argv in ['txnhlr', 'nhrxrl']:
|
||||
lr_animate()
|
||||
lr_idle()
|
||||
|
||||
ipx_send.send(msg)
|
||||
if argv in ['txnhrl', 'nhrxlr']:
|
||||
rl_animate()
|
||||
rl_idle()
|
||||
|
||||
interface.send(msg)
|
||||
|
||||
except(EOFError, KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
|
||||
def rx_process():
|
||||
"""Process that sends to receiving computer."""
|
||||
def rx_process(io_queue: 'Queue', input_socket: int) -> None:
|
||||
"""Process that reads from sending computer."""
|
||||
listener = multiprocessing.connection.Listener(('localhost', input_socket))
|
||||
interface = listener.accept()
|
||||
|
||||
def ipc_to_queue(conn):
|
||||
"""
|
||||
Load packet from IPC.
|
||||
|
||||
:param conn: Listener object
|
||||
:return: [no return value]
|
||||
"""
|
||||
|
||||
while True:
|
||||
time.sleep(0.001)
|
||||
pkg = conn.recv()
|
||||
io_queue.put(pkg)
|
||||
try:
|
||||
l = multiprocessing.connection.Listener(("localhost", input_socket))
|
||||
while True:
|
||||
ipc_to_queue(l.accept())
|
||||
except EOFError:
|
||||
exit_queue.put("exit")
|
||||
while True:
|
||||
time.sleep(0.01)
|
||||
try:
|
||||
io_queue.put(interface.recv())
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
tx_nh_lr = False
|
||||
nh_rx_lr = False
|
||||
tx_nh_rl = False
|
||||
nh_rx_rl = False
|
||||
|
||||
input_socket = 0
|
||||
def main() -> None:
|
||||
"""Run data diode simulator."""
|
||||
output_socket = 0
|
||||
|
||||
# Resize terminal
|
||||
# sys.stdout.write("\x1b[8;{rows};{cols}t".format(rows=25, cols=12))
|
||||
input_socket = 0
|
||||
argv = ''
|
||||
|
||||
try:
|
||||
# Simulates data diode between Tx.py on left, NH.py on right.
|
||||
if str(sys.argv[1]) == "txnhlr":
|
||||
tx_nh_lr = True
|
||||
input_socket = 5000
|
||||
output_socket = 5001
|
||||
|
||||
# Simulates data diode between Tx.py on right, NH.py on left.
|
||||
elif str(sys.argv[1]) == "txnhrl":
|
||||
tx_nh_rl = True
|
||||
input_socket = 5000
|
||||
output_socket = 5001
|
||||
|
||||
# Simulates data diode between Rx.py on left, NH.py on right.
|
||||
elif str(sys.argv[1]) == "nhrxlr":
|
||||
nh_rx_lr = True
|
||||
input_socket = 5002
|
||||
output_socket = 5003
|
||||
|
||||
# Simulates data diode between Rx.py on right, NH.py on left.
|
||||
elif str(sys.argv[1]) == "nhrxrl":
|
||||
nh_rx_rl = True
|
||||
input_socket = 5002
|
||||
output_socket = 5003
|
||||
|
||||
else:
|
||||
os.system("clear")
|
||||
print("\nUsage: python dd.py {txnh{lr,rl}, nhrx{lr,rl}\n")
|
||||
exit()
|
||||
|
||||
except IndexError:
|
||||
os.system("clear")
|
||||
argv = str(sys.argv[1])
|
||||
input_socket = dict(txnhlr=5000, txnhrl=5000, nhrxlr=5002, nhrxrl=5002)[argv]
|
||||
output_socket = dict(txnhlr=5001, txnhrl=5001, nhrxlr=5003, nhrxrl=5003)[argv]
|
||||
except (IndexError, KeyError):
|
||||
clear_screen()
|
||||
print("\nUsage: python dd.py {txnh{lr,rl}, nhrx{lr,rl}}\n")
|
||||
exit()
|
||||
|
||||
try:
|
||||
print("Waiting for socket")
|
||||
ipx_send = multiprocessing.connection.Client(("localhost",
|
||||
output_socket))
|
||||
print("Connection established.")
|
||||
time.sleep(0.3)
|
||||
os.system("clear")
|
||||
except KeyboardInterrupt:
|
||||
exit()
|
||||
io_queue = Queue()
|
||||
tx = Process(target=tx_process, args=(io_queue, output_socket, argv))
|
||||
rx = Process(target=rx_process, args=(io_queue, input_socket))
|
||||
process_list = [tx, rx]
|
||||
|
||||
exit_queue = multiprocessing.Queue()
|
||||
io_queue = multiprocessing.Queue()
|
||||
for p in process_list:
|
||||
p.start()
|
||||
|
||||
txp = multiprocessing.Process(target=tx_process)
|
||||
rxp = multiprocessing.Process(target=rx_process)
|
||||
while True:
|
||||
try:
|
||||
time.sleep(0.1)
|
||||
if not all([p.is_alive() for p in process_list]):
|
||||
for p in process_list:
|
||||
p.terminate()
|
||||
exit()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
txp.start()
|
||||
rxp.start()
|
||||
|
||||
try:
|
||||
while True:
|
||||
if not exit_queue.empty():
|
||||
command = exit_queue.get()
|
||||
if command == "exit":
|
||||
txp.terminate()
|
||||
rxp.terminate()
|
||||
exit()
|
||||
time.sleep(0.01)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
txp.terminate()
|
||||
rxp.terminate()
|
||||
exit()
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
101
hwrng.py
101
hwrng.py
|
@ -1,101 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# TFC 0.16.10 || hwrng.py
|
||||
|
||||
"""
|
||||
Copyright (C) 2013-2016 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 binascii
|
||||
import time
|
||||
import sys
|
||||
|
||||
try:
|
||||
import RPi.GPIO as GPIO
|
||||
except ImportError:
|
||||
GPIO = None
|
||||
pass
|
||||
|
||||
|
||||
###############################################################################
|
||||
# CONFIGURATION #
|
||||
###############################################################################
|
||||
|
||||
sample_delay = 0.1 # Delay in seconds between samples
|
||||
|
||||
gpio_port = 4 # RPi's GPIO pin (Broadcom layout) to collect entropy from
|
||||
|
||||
|
||||
###############################################################################
|
||||
# MAIN #
|
||||
###############################################################################
|
||||
|
||||
def main():
|
||||
"""
|
||||
Load 256 or 512 bits of entropy from HWRNG.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setup(gpio_port, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
|
||||
|
||||
ent_size = int(sys.argv[1])
|
||||
|
||||
if ent_size not in [256, 512, 768]:
|
||||
print('S')
|
||||
exit()
|
||||
|
||||
init0 = 0
|
||||
init1 = 0
|
||||
vnd = ''
|
||||
|
||||
while init0 < 1500 or init1 < 1500:
|
||||
time.sleep(0.001)
|
||||
|
||||
if GPIO.input(gpio_port) == 1:
|
||||
init1 += 1
|
||||
else:
|
||||
init0 += 1
|
||||
|
||||
while len(vnd) != ent_size:
|
||||
|
||||
# Perform Von Neumann whitening during sampling
|
||||
first_bit = GPIO.input(gpio_port)
|
||||
time.sleep(sample_delay)
|
||||
|
||||
second_bit = GPIO.input(gpio_port)
|
||||
time.sleep(sample_delay)
|
||||
|
||||
if first_bit == second_bit:
|
||||
continue
|
||||
else:
|
||||
vnd += str(first_bit)
|
||||
sys.stdout.flush()
|
||||
sys.stdout.write("N\n")
|
||||
|
||||
# Convert bits to byte string
|
||||
ent = ''.join(chr(int(vnd[i:i + 8], 2)) for i in range(0, len(vnd), 8))
|
||||
|
||||
if len(ent) != ent_size / 8:
|
||||
print('L')
|
||||
|
||||
GPIO.cleanup()
|
||||
print(binascii.hexlify(ent))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,366 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# 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/>.
|
||||
|
||||
|
||||
dl_verify () {
|
||||
wget https://raw.githubusercontent.com/maqp/tfc/master$2$3 -q
|
||||
|
||||
if sha256sum $3 | grep -Eo '^\w+' | cmp -s <(echo "$1")
|
||||
then
|
||||
echo Valid SHA256 hash for file $3
|
||||
else
|
||||
echo Error: $3 had invalid SHA256 hash.
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
activate_nh_venv () {
|
||||
. $HOME/tfc/venv_nh/bin/activate
|
||||
}
|
||||
activate_tfc_venv () {
|
||||
. $HOME/tfc/venv_tfc/bin/activate
|
||||
}
|
||||
|
||||
tfc_download () {
|
||||
|
||||
mkdir tfc/
|
||||
cd tfc/
|
||||
|
||||
mkdir src/
|
||||
cd src/
|
||||
|
||||
mkdir nh/
|
||||
cd nh/
|
||||
dl_verify e88c26e22f1fadde164fb177c5f4ec476f1f3aaf4c2a726b8a4af8b79ba0859a /src/nh/ pidgin.py
|
||||
dl_verify c1a71d9bbf843c5e51833e6429cfd7cbf6eff76e8fd1777c84c00b257f352ada /src/nh/ misc.py
|
||||
dl_verify b5414b9288253be6af758713d8e49815bcb28d4c213c8e2d995c55c98f4c3a74 /src/nh/ gateway.py
|
||||
dl_verify e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 /src/nh/ __init__.py
|
||||
dl_verify 95e3a2d4e1df6f67aa813b619b94299d42b744b281873c6b661e65077f0c7a17 /src/nh/ settings.py
|
||||
dl_verify 838515f786943744d8c67138f17e1b00211f9b11fa11a5d87b012466aff6c1cf /src/nh/ tcb.py
|
||||
dl_verify b54c28dacea537c50a0f9b9ebb6a598e08978a2050a095d74c04b9caf95d9953 /src/nh/ commands.py
|
||||
cd ..
|
||||
|
||||
mkdir tx/
|
||||
cd tx/
|
||||
dl_verify 7d1a0474e8100bfabfaa95b8c1c75f96fb2f8c4d0f06ba5bc08929896de15734 /src/tx/ contact.py
|
||||
dl_verify 9399695807414de36d5a10258a28c065dc845440dcb8178ca27dce0949977d28 /src/tx/ files.py
|
||||
dl_verify a65859da2c1ee16ab8917aa7698233a1fee6a0ad02236a118b848e7b5fa55cb6 /src/tx/ key_exchanges.py
|
||||
dl_verify e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 /src/tx/ __init__.py
|
||||
dl_verify 96214c2268599cc146cc03b1d250dc50b0884ccbe01ee8df90db401562b0e4a6 /src/tx/ windows.py
|
||||
dl_verify 3581ccfbeac71e5290c336c8ad41024afa27fa876598cead927a0f37fc91c99b /src/tx/ messages.py
|
||||
dl_verify 56cdefb3651a4085dca821048a3f78dc4c55033b127f375c4dcf0ac6431e70ff /src/tx/ tx_loop.py
|
||||
dl_verify 32322e37d93afe66b2434781093116f20c279948bcb97742d3f58ef8e129b085 /src/tx/ trickle.py
|
||||
dl_verify a65ebc3ab0c402a078342993bd0d0a94ec4d9eb99f118b92ec3fc72cee30c1f0 /src/tx/ commands_g.py
|
||||
dl_verify 80e441c99e47b980fe88c8cf05dfe9cd25bb7d10268830b80fef13a0f09f6253 /src/tx/ commands.py
|
||||
dl_verify 39e9060875116f0ea99a2dd3b42df7111364c2ef9e47f460e606e4f9d5789926 /src/tx/ packet.py
|
||||
dl_verify 9e50cc93f4a35f42820a1f39daee92c8699c4b7c700413ca9cf82f9a149b5484 /src/tx/ sender_loop.py
|
||||
dl_verify 0d31c7f87692e3e3836cd144b3a6d38728832ae1febcad3f8a25fba407843842 /src/tx/ user_input.py
|
||||
cd ..
|
||||
|
||||
mkdir common/
|
||||
cd common/
|
||||
dl_verify 57c5c827264bf43f73207d6906c1834131c406db945ff7c8310b6efaeac169e2 /src/common/ statics.py
|
||||
dl_verify a9e1f97dc03be04554f57d39c74c979527210965098ab87e30e768db13f1337f /src/common/ reed_solomon.py
|
||||
dl_verify b40dcdb6a23d35f2969ec097fbb94c08fc9a366eca2c6f32c56dc68ae73a595e /src/common/ db_settings.py
|
||||
dl_verify 45caef85023b3f60335bf4abc10c31b9e1c9da9ef1c2cdf42f1a77921c80f91d /src/common/ misc.py
|
||||
dl_verify b4477ccd38895be7fc04d76a00a7e0f629ad8f97c21fb157ef83ecc09669d276 /src/common/ db_logs.py
|
||||
dl_verify 487c7993c8fc2953f8741edede9af11f34fdc8531f96dc70ce16cd3557cc1487 /src/common/ gateway.py
|
||||
dl_verify 994cb1be594d7f97bd9d096ea4d8e4d3d723d1ffa95e21d8cac38d09e576fb20 /src/common/ input.py
|
||||
dl_verify 1ef8b21c6f57396cb4536a40ce86fd5acac930a2f7f1412f520071a66ba0bf43 /src/common/ db_masterkey.py
|
||||
dl_verify eed56cb166cdafa02584270d5a658aa8ddcaa9c48b1a5156ad471ac8e3bbf551 /src/common/ db_keys.py
|
||||
dl_verify e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 /src/common/ __init__.py
|
||||
dl_verify 2ada11493417bb6f31b3c6270c6b2c525f20310f750057d385b578d1caf96c3a /src/common/ encoding.py
|
||||
dl_verify 72c753182896e5e8f45f278bcb785b0debc8e0bb9feddabd86488214e33484b8 /src/common/ crypto.py
|
||||
dl_verify 540478dc7f25c76b1f058c3a54b8a1ac115d237e2858d87fb9ac95bf6981dae5 /src/common/ db_groups.py
|
||||
dl_verify 89cf69a77b15d68d4f1a3bcf6d0a28e162b4dedc4e760d0681fc14913b12a0b3 /src/common/ output.py
|
||||
dl_verify 6c22c62ec9083ebe16163363b3cbe4dc3c115a855de91a172e29ecc8a65367b9 /src/common/ path.py
|
||||
dl_verify d3db9172b3752887506944a6ec4a03c86eb41fb4eaaa537fe626a67ec3dc5cde /src/common/ errors.py
|
||||
dl_verify 4762ae73254b594f96f66f312895c843dc918d73d085cef9e64e56f77fde589c /src/common/ db_contacts.py
|
||||
cd ..
|
||||
|
||||
mkdir rx/
|
||||
cd rx/
|
||||
dl_verify f9414a1147a894c745c73638b390a93e327854bb942d0949c872ef0dec1a0515 /src/rx/ files.py
|
||||
dl_verify 3536a64fafe7d52e0d87f8317ac1af2f4146d9259fc5dc5c000af038c30bb33b /src/rx/ key_exchanges.py
|
||||
dl_verify 0ecc6b59e71e3be181b3e0817ebbb3f0b974d56944d5896669ad274575d0be7b /src/rx/ receiver_loop.py
|
||||
dl_verify e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 /src/rx/ __init__.py
|
||||
dl_verify 537b33ca75a04c305b36ba422f758dc4e1f86681a0cce0ce56b4b22db293eee8 /src/rx/ windows.py
|
||||
dl_verify d46660aff0aed8b977e751641f244001a24ebb1614de828dcb3266f564796c80 /src/rx/ messages.py
|
||||
dl_verify e889cc3e2d5893679d76215072790723847e785401f7dddea61462e2f280578c /src/rx/ commands_g.py
|
||||
dl_verify a281f42889649c6962b081e32962a881591894d795c81b20ba6346e2201a230c /src/rx/ commands.py
|
||||
dl_verify 5a1bc935a6e34bc2c009a280ce74f44132861b2bc6267dbf41738aa9aecd0722 /src/rx/ packet.py
|
||||
dl_verify a86110caad1ee2569f109e9e94a5dd88b1c8e3f2798fd2c4c262550fb71143fe /src/rx/ rx_loop.py
|
||||
cd ..
|
||||
|
||||
dl_verify e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 /src/ __init__.py
|
||||
cd ..
|
||||
|
||||
dl_verify 188352b6f8408a552ef72b1c45851141f0b3de0286f6374bd1c9f78c488d5e8d /'' requirements.txt
|
||||
dl_verify 043f6f1738fc85c8b6c8b7943b08e0aeb5f82397175503fb69427d869c706251 /'' tfc.png
|
||||
|
||||
mkdir tests
|
||||
cd tests
|
||||
|
||||
mkdir nh/
|
||||
cd nh/
|
||||
dl_verify 03d9a010aed6085353b218baffd66345aee6d85ce821b1abfdd0bfce92e254ab /tests/nh/ test_settings.py
|
||||
dl_verify e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 /tests/nh/ __init__.py
|
||||
dl_verify 8f7c82acecf1de5bb7b575e6564a1d802d862d30d2883ef7d2ebcd92e1a05943 /tests/nh/ test_gateway.py
|
||||
dl_verify b24af3ec561ba4ce8c48d29ecd64211677df1993b431ac66ce4d875cf2ab2a21 /tests/nh/ test_misc.py
|
||||
cd ..
|
||||
|
||||
dl_verify c6d9d76546b4a5a68c16e3f9156ca5a09ee215245828a35c05cd2e2dcaa4ab2e /tests/ utils.py
|
||||
|
||||
mkdir tx/
|
||||
cd tx/
|
||||
dl_verify 5b7fa7843fb84f59a7e41f4be8c12da9de6cbccd88519ea7f9e9b6247b532b1e /tests/tx/ test_commands_g.py
|
||||
dl_verify e36a0286d6f70f10ede4204c8ee7a8e98ab6d4f64e88366a073ff0269b0ac60a /tests/tx/ test_packet.py
|
||||
dl_verify bfccac83b9a4e88b1918701091dcee4425af23932603922999da612003b88ec0 /tests/tx/ test_trickle.py
|
||||
dl_verify 5f33e4c9aac2cd438f3aeb4b5f6157a0b1d34026ae19d2bbf2ffeef237a76a12 /tests/tx/ test_files.py
|
||||
dl_verify e25a63fd7f8f69f42cd5176cba53f6ac41b5b278a6e5bed36400a6b8e345227f /tests/tx/ test_windows.py
|
||||
dl_verify e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 /tests/tx/ __init__.py
|
||||
dl_verify 12cf5d7eb667aa5be33d70b6abe04704efd848e0c8ead4fe4a8bb34177135cbd /tests/tx/ test_messages.py
|
||||
dl_verify af7cc6140093370f0500a87626821ac68845eb439df6205edec2fbfd791853e8 /tests/tx/ test_contact.py
|
||||
dl_verify cb516eb6171c68054343a1639a1c7066843c4a440f7bfa7c2fc2a5d0fc4f6940 /tests/tx/ test_commands.py
|
||||
dl_verify 693e4ee72e75160f787a9bd40dc7d89696ec665938b6377d5fd9a6a7eb09550c /tests/tx/ test_user_input.py
|
||||
dl_verify 1c5cba9aa5131d362e96ca01deb91f728a78174deb13ca0d3367a06ec2fcb3cd /tests/tx/ test_key_exchanges.py
|
||||
cd ..
|
||||
|
||||
mkdir common/
|
||||
cd common/
|
||||
dl_verify 885b7f4013dc85b4faf41195ac6eb598bc6051df51c212ea559ffa7f21d8b7c8 /tests/common/ test_crypto.py
|
||||
dl_verify e869f29c51e0ed22f89efc0a836a22c7446703b14519e1aa0d1746f473f89f20 /tests/common/ test_reed_solomon.py
|
||||
dl_verify 6c7e8e693f828d577faaadea3c7236af5fcdad8b714df9787aba18d56fa267d6 /tests/common/ test_db_groups.py
|
||||
dl_verify 62b9df59579efc7ffccec750419af0dccfd821d64d4f9e400681a682ffa18630 /tests/common/ test_path.py
|
||||
dl_verify e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 /tests/common/ __init__.py
|
||||
dl_verify 809044c6ec5bc0da9f24f373c757ae28ed2237430318b44a4be2469dda0f9c46 /tests/common/ test_db_masterkey.py
|
||||
dl_verify 9363c2d65f0278e55c7bb8d9c896460fe012d28f2195bbb76d4d543b57ebdf68 /tests/common/ test_gateway.py
|
||||
dl_verify 7dfab78f13031da2702fb468a4cfdd3706bf34ea097ea419aca18391bc617d10 /tests/common/ test_db_contacts.py
|
||||
dl_verify 6bf42b295a192e68077f3839b6b3b91cafc53ddf8ad2db7f430ec19a9dd68870 /tests/common/ test_db_settings.py
|
||||
dl_verify 97fb88aa500d9e56192554825c335dd07471dd108a1885180bd61b13fabe26ff /tests/common/ test_input.py
|
||||
dl_verify 0fbd8a70bf97db1694b4e0aa46b1da46179868df107ff104df115cf421e84c66 /tests/common/ test_db_logs.py
|
||||
dl_verify 40718dc469185870edc2439ea3c5bbee1a97c9af87d5996de2e2257e5fa6fc4e /tests/common/ test_output.py
|
||||
dl_verify 9fc64a9f3913be44b0ea523c18854f6e984c2947ceafe2b28d16c1c761373821 /tests/common/ test_errors.py
|
||||
dl_verify dbe47433f41566db093db1e6512da528074f002332d57b9ad70570f609d65d7c /tests/common/ test_encoding.py
|
||||
dl_verify 9540c6de160eca24be8458cd10106ddde7e83c16d06ceff828d0788fe7a635b5 /tests/common/ test_misc.py
|
||||
dl_verify 06c104f28a6a77860041b18a9b717087ff5dd722af319858299c0113e6dde252 /tests/common/ test_db_keys.py
|
||||
cd ..
|
||||
|
||||
mkdir rx/
|
||||
cd rx/
|
||||
dl_verify feb93d9f57651a7407f7a8126da4021cb2d92799f2d859061969bc41820be9fc /tests/rx/ test_commands_g.py
|
||||
dl_verify 2587a94659e9f8d06ae6a506490da5a6e4d2b38ce493eb276d47145406db5591 /tests/rx/ test_packet.py
|
||||
dl_verify 97947c4c7a33f115356a66db1e668b6cb58e1163471fe760613071da57f58977 /tests/rx/ test_files.py
|
||||
dl_verify e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 /tests/rx/ __init__.py
|
||||
dl_verify 721b5edf6696661016a99e891a4f0faeed2bf577709fac529497225d3b96aac8 /tests/rx/ test_messages.py
|
||||
dl_verify 63758cbb491d55a4ddaf7ec581766f5b1e3487cda0bf89fe1243a9e56534fb1c /tests/rx/ test_commands.py
|
||||
dl_verify ced0b513630818168077d4aec2d2b60da335b16592d221798dfb2dc1932d3e0e /tests/rx/ test_window.py
|
||||
dl_verify 3481ba01673f2c989caa06c2d961ad265fc8de8d2b5ec8322147c6c928bd95d7 /tests/rx/ test_key_exchanges.py
|
||||
cd ..
|
||||
|
||||
dl_verify e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 /tests/ __init__.py
|
||||
dl_verify 2e38cc5cb01cb9e2280d0f90620818ac992c44ae13aa8b2e03ed4aeb17ff6b8e /tests/ mock_classes.py
|
||||
cd ..
|
||||
|
||||
mkdir launchers/
|
||||
cd launchers/
|
||||
dl_verify 4d4db7915fcf2126695283dc63ab6fae8f1277e12f8bd3961f8e00bca77fa508 /launchers/ TFC-NH-Tails.desktop
|
||||
dl_verify a3e976e37eab126b828c3001714aeaa82cc50ebbbca2af4daf22fc252202fd8c /launchers/ TFC-NH.desktop
|
||||
dl_verify 6e760a543685141787ce320c425ada1c5880c79cf928d2e08b7becc322cf4408 /launchers/ TFC-DD-RL.desktop
|
||||
dl_verify 43f60afaf0dc7b26828a052c7c679d8e6f188975ea66fa4f173952dd72e3ac43 /launchers/ TFC-RxM.desktop
|
||||
dl_verify ed5b54e7846738dc10950187e6a49f40220ae11bde340726429305cc89cc78b9 /launchers/ config
|
||||
dl_verify 3ad19e79e95555cceee52c7fc9805989eff8c0b0a997820656ab337369eb5948 /launchers/ TFC-LR.desktop
|
||||
dl_verify 62f64d0037f9f860d3afbb682e5e087156b4e42c38e177ea1d9e070b9d453bfe /launchers/ TFC-RL.desktop
|
||||
dl_verify d877f71fd9b9b46eb16371c9a7613d682332638b467a1f4752739f9f543212cf /launchers/ TFC-DD-LR.desktop
|
||||
dl_verify cd1cf1e403c0b4d308abb6bfb0f12ab8929840c49051dbb22f5c56f910ae8aef /launchers/ TFC-TxM.desktop
|
||||
cd ..
|
||||
|
||||
dl_verify 8639e791dab49ab8a7bb9bbfa722911d4edd9e46cfb0c832dcfec02ea772bfc5 /'' LICENSE.md
|
||||
|
||||
dl_verify 559bef13cabc7cc907bc9287cd9638bf2778d5ed5b1015cc4ed54935ee0d9b93 /'' tfc.py
|
||||
dl_verify a31565c6563f97ea019e70fc551817af6d3db06d96ce6072aab77702f52b9bb6 /'' nh.py
|
||||
dl_verify e7cfd80de08972865efb143f0a845a5327a5b8a67bcfe8cadf9ec9764d407fce /'' requirements-nh.txt
|
||||
dl_verify a50e90cfe7ece7796d064b6f457e64087c1cc68305a1c0160980ff21e2af17b1 /'' dd.py
|
||||
}
|
||||
|
||||
kill_network () {
|
||||
for interface in /sys/class/net/*; do
|
||||
sudo ifconfig `basename $interface` down
|
||||
done
|
||||
clear
|
||||
echo "\nThis computer needs to be airgapped. Installer has disabled network interfaces."\
|
||||
"Disconnect ethernet cable now. If you're using a wireless network interface, it"\
|
||||
"must be removed immediately after this installer completes.\n"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm $HOME/tfc/requirements.txt
|
||||
rm $HOME/tfc/requirements-nh.txt
|
||||
rm -r $HOME/tfc/launchers/
|
||||
}
|
||||
|
||||
install_tcb () {
|
||||
sudo add-apt-repository ppa:jonathonf/python-3.6 -y
|
||||
sudo apt update
|
||||
sudo apt install python3.6 python3.6-dev python3-setuptools python3-pip python3-tk libffi-dev -y
|
||||
|
||||
tfc_download
|
||||
|
||||
python3.6 -m pip download -r requirements.txt --require-hashes
|
||||
|
||||
kill_network
|
||||
|
||||
python3.6 -m pip install virtualenv-15.1.0-py2.py3-none-any.whl
|
||||
python3.6 -m virtualenv --system-site-packages venv_tfc
|
||||
|
||||
activate_tfc_venv
|
||||
python3.6 -m pip install pycparser-2.17.tar.gz
|
||||
python3.6 -m pip install cffi-1.9.1-cp36-cp36m-manylinux1_x86_64.whl
|
||||
python3.6 -m pip install PyNaCl-1.1.1.tar.gz
|
||||
python3.6 -m pip install argon2-0.1.10.tar.gz
|
||||
python3.6 -m pip install pyserial-3.3-py2.py3-none-any.whl
|
||||
deactivate
|
||||
|
||||
sudo mv $HOME/tfc/tfc.png /usr/share/pixmaps/
|
||||
sudo cp $HOME/tfc/launchers/TFC-RxM.desktop /usr/share/applications/
|
||||
sudo cp $HOME/tfc/launchers/TFC-TxM.desktop /usr/share/applications/
|
||||
|
||||
chmod a+rwx -R $HOME/tfc/
|
||||
cleanup
|
||||
rm $HOME/tfc/*.tar.gz
|
||||
rm $HOME/tfc/*.whl
|
||||
rm -r $HOME/tfc/src/nh
|
||||
rm $HOME/tfc/nh.py
|
||||
rm $HOME/tfc/dd.py
|
||||
|
||||
sudo adduser $USER dialout
|
||||
clear
|
||||
echo 'Installation of TFC on this device is now complete.'
|
||||
echo 'Reboot the computer to update serial port use rights'
|
||||
}
|
||||
|
||||
install_local_test () {
|
||||
for r in ppa:jonathonf/python-3.6 ppa:gnome-terminator; do sudo add-apt-repository $r -y; done
|
||||
sudo apt update
|
||||
sudo apt install python3.6 python3.6-dev python3-setuptools python3-pip python3-tk libffi-dev pidgin pidgin-otr terminator -y
|
||||
|
||||
tfc_download
|
||||
|
||||
python3.5 -m pip install virtualenv
|
||||
python3.6 -m pip install virtualenv
|
||||
python3.5 -m virtualenv --system-site-packages venv_nh
|
||||
python3.6 -m virtualenv --system-site-packages venv_tfc
|
||||
|
||||
activate_nh_venv
|
||||
python3.5 -m pip install -r requirements-nh.txt --require-hashes
|
||||
deactivate
|
||||
|
||||
activate_tfc_venv
|
||||
python3.6 -m pip install -r requirements.txt --require-hashes
|
||||
deactivate
|
||||
|
||||
sudo mv $HOME/tfc/tfc.png /usr/share/pixmaps/
|
||||
sudo cp $HOME/tfc/launchers/TFC-DD-LR.desktop /usr/share/applications/
|
||||
sudo cp $HOME/tfc/launchers/TFC-DD-RL.desktop /usr/share/applications/
|
||||
sudo cp $HOME/tfc/launchers/TFC-LR.desktop /usr/share/applications/
|
||||
sudo cp $HOME/tfc/launchers/TFC-RL.desktop /usr/share/applications/
|
||||
|
||||
mkdir -p $HOME/.config/terminator
|
||||
mv $HOME/.config/terminator/config $HOME/.config/terminator/config_before_tfc 2>/dev/null
|
||||
mv $HOME/tfc/launchers/config $HOME/.config/terminator/config
|
||||
sudo chown $USER -R $HOME/.config/terminator/
|
||||
|
||||
chmod a+rwx -R $HOME/tfc/
|
||||
cleanup
|
||||
clear
|
||||
echo 'Installation of TFC for local testing is now complete.'
|
||||
}
|
||||
|
||||
install_nh_ubuntu () {
|
||||
sudo apt update
|
||||
sudo apt install python3-pip pidgin pidgin-otr -y
|
||||
|
||||
tfc_download
|
||||
|
||||
python3.5 -m pip install virtualenv
|
||||
python3.5 -m virtualenv --system-site-packages venv_nh
|
||||
|
||||
activate_nh_venv
|
||||
python3.5 -m pip install -r requirements-nh.txt --require-hashes
|
||||
deactivate
|
||||
|
||||
sudo mv $HOME/tfc/tfc.png /usr/share/pixmaps/
|
||||
sudo cp $HOME/tfc/launchers/TFC-NH.desktop /usr/share/applications/
|
||||
|
||||
chmod a+rwx -R $HOME/tfc/
|
||||
cleanup
|
||||
|
||||
rm -r $HOME/tfc/src/tx
|
||||
rm -r $HOME/tfc/src/rx
|
||||
rm $HOME/tfc/tfc.py
|
||||
rm $HOME/tfc/dd.py
|
||||
|
||||
sudo adduser $USER dialout
|
||||
clear
|
||||
echo 'Installation of NH configuration is now complete.'
|
||||
echo 'Reboot the computer to update serial port use rights'
|
||||
}
|
||||
|
||||
install_nh_tails () {
|
||||
tfc_download
|
||||
|
||||
sudo mv tfc.png /usr/share/pixmaps/
|
||||
sudo cp $HOME/tfc/launchers/TFC-NH-Tails.desktop /usr/share/applications/
|
||||
|
||||
chmod a+rwx -R $HOME/tfc/
|
||||
cleanup
|
||||
|
||||
rm -r $HOME/tfc/src/tx
|
||||
rm -r $HOME/tfc/src/rx
|
||||
rm $HOME/tfc/tfc.py
|
||||
rm $HOME/tfc/dd.py
|
||||
clear
|
||||
echo 'Installation of NH configuration is now complete.'
|
||||
# Tails user is already in dialout group so no restart is required.
|
||||
}
|
||||
|
||||
arg_error () {
|
||||
clear
|
||||
echo 'Usage: bash install [OPTION]'
|
||||
echo -e '\nMandatory arguments'
|
||||
echo ' tcb Install TxM/RxM configuration (Ubuntu)'
|
||||
echo ' nhu Install NH configuration (Ubuntu)'
|
||||
echo ' nht Install NH configuration (Tails 3.0+)'
|
||||
echo ' lt local testing mode (Ubuntu)'
|
||||
exit
|
||||
}
|
||||
|
||||
if [[ !$EUID -ne 0 ]]; then
|
||||
clear
|
||||
echo "Error: This installer must not be run as root." 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd $HOME
|
||||
|
||||
case $1 in
|
||||
tcb ) install_tcb;;
|
||||
nhu ) install_nh_ubuntu;;
|
||||
nht ) install_nh_tails;;
|
||||
lt ) install_local_test;;
|
||||
* ) arg_error;;
|
||||
esac
|
|
@ -0,0 +1,17 @@
|
|||
-----BEGIN PGP SIGNATURE-----
|
||||
Version: GnuPG v1
|
||||
|
||||
iQIcBAABAgAGBQJX42tiAAoJENJrq8gPhjL0IGsQAIPCLmbHu07593P9AxDj2M0A
|
||||
7Q6Ugua+woEtdxtjiXftchU/L+40S7Hz1QNeukObePm09D0xQE/8EogbevdFQfJy
|
||||
W+MNKQu62XcnqmIxncm64nC+ci3QkWU7B5G6G2Qnq0WGC8qcyXRBWTDCaGTHajf4
|
||||
0v6YCuJCbzRt/BvAphMHyFAwZkNfVeg8XJjCEWaeVxLIAO2JgVPUKFj0HmOXeE0x
|
||||
U0wq+w83hN51kFXeH3aTwL+vuKcglIRXDYH3MBHBeEED9dfIzmblWMhbGk4kKM+6
|
||||
hXMPSs9jUNjzrLF9gvc5wCFJfcgVbQZIyEmaO/ACcoRciEDz+lZxONm8tv5lHELu
|
||||
XDgF53+Tg/EcvTifKAG2fJ4cVbDPbTmty6b1rXTBvk7xYbdIW/ChRNg+IwoTEbDx
|
||||
me2faEH+gWLE15mLCRYNDk/29J+JBj2wRvzcMwCqiQG+07qz8BU583JaRPhUdOlw
|
||||
YlKmKgvitYkJgzHE0OZF10r+Mx7je0QUVyOZs8R5JR2XI4qQ35/jE6c1zJGGDu1h
|
||||
NfJulEHGP2BGORwvslh6dMIago1S7WXQz4e5N0FOf7suP1I1NVt3VenETATve+mL
|
||||
CkvwpSNLBzgXO/WKRLKUBKOXm2uuytITJBmCy8U85vqnV7nrhPNJFFux2JjpnnD1
|
||||
YGDqRCuPUL9h60FxozcA
|
||||
=qq5q
|
||||
-----END PGP SIGNATURE-----
|
|
@ -0,0 +1,8 @@
|
|||
[Desktop Entry]
|
||||
Name=TFC-DD-LR
|
||||
Comment=Local testing
|
||||
Exec=terminator -m -p tfc -l tfc-dd-lr
|
||||
Icon=tfc.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Network;Messaging;Security;
|
|
@ -0,0 +1,8 @@
|
|||
[Desktop Entry]
|
||||
Name=TFC-DD-RL
|
||||
Comment=Local testing
|
||||
Exec=terminator -m -p tfc -l tfc-dd-rl
|
||||
Icon=tfc.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Network;Messaging;Security;
|
|
@ -0,0 +1,8 @@
|
|||
[Desktop Entry]
|
||||
Name=TFC-LR
|
||||
Comment=Local testing
|
||||
Exec=terminator -m -p tfc -l tfc-lr
|
||||
Icon=tfc.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Network;Messaging;Security;
|
|
@ -0,0 +1,7 @@
|
|||
[Desktop Entry]
|
||||
Name=TFC-NH
|
||||
Exec=gnome-terminal -x bash -c "cd $HOME/tfc; python3.5 'nh.py'"
|
||||
Icon=tfc.png
|
||||
Terminal=true
|
||||
Type=Application
|
||||
Categories=Network;Messaging;Security;
|
|
@ -0,0 +1,7 @@
|
|||
[Desktop Entry]
|
||||
Name=TFC-NH
|
||||
Exec=gnome-terminal -x bash -c "cd $HOME/tfc; source venv_nh/bin/activate; python3.5 'nh.py'; deactivate"
|
||||
Icon=tfc.png
|
||||
Terminal=true
|
||||
Type=Application
|
||||
Categories=Network;Messaging;Security;
|
|
@ -0,0 +1,8 @@
|
|||
[Desktop Entry]
|
||||
Name=TFC-RL
|
||||
Comment=Local testing
|
||||
Exec=terminator -m -p tfc -l tfc-rl
|
||||
Icon=tfc.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Network;Messaging;Security;
|
|
@ -0,0 +1,7 @@
|
|||
[Desktop Entry]
|
||||
Name=TFC-RxM
|
||||
Exec=gnome-terminal --maximize -x bash -c "cd $HOME/tfc; source venv_tfc/bin/activate; python3.6 'tfc.py' -rx; deactivate"
|
||||
Icon=tfc.png
|
||||
Terminal=true
|
||||
Type=Application
|
||||
Categories=Network;Messaging;Security;
|
|
@ -0,0 +1,7 @@
|
|||
[Desktop Entry]
|
||||
Name=TFC-TxM
|
||||
Exec=gnome-terminal --maximize -x bash -c "cd $HOME/tfc; source venv_tfc/bin/activate; python3.6 'tfc.py'; deactivate"
|
||||
Icon=tfc.png
|
||||
Terminal=true
|
||||
Type=Application
|
||||
Categories=Network;Messaging;Security;
|
|
@ -0,0 +1,248 @@
|
|||
[global_config]
|
||||
geometry_hinting = False
|
||||
suppress_multiple_term_dialog = True
|
||||
window_state = maximise
|
||||
[keybindings]
|
||||
[layouts]
|
||||
[[default]]
|
||||
[[[child1]]]
|
||||
parent = window0
|
||||
type = Terminal
|
||||
[[[window0]]]
|
||||
parent = ""
|
||||
type = Window
|
||||
[[tfc-dd-lr]]
|
||||
[[[child0]]]
|
||||
fullscreen = False
|
||||
maximised = True
|
||||
order = 0
|
||||
parent = ""
|
||||
position = 1969:24
|
||||
size = 1871, 1056
|
||||
title = TFC
|
||||
type = Window
|
||||
[[[child1]]]
|
||||
order = 0
|
||||
parent = child0
|
||||
position = 851
|
||||
ratio = 0.4564404062
|
||||
type = HPaned
|
||||
[[[child2]]]
|
||||
order = 0
|
||||
parent = child1
|
||||
position = 525
|
||||
ratio = 0.5
|
||||
type = VPaned
|
||||
[[[child5]]]
|
||||
order = 1
|
||||
parent = child1
|
||||
position = 321
|
||||
ratio = 0.319526627219
|
||||
type = HPaned
|
||||
[[[child6]]]
|
||||
order = 0
|
||||
parent = child5
|
||||
position = 525
|
||||
ratio = 0.5
|
||||
type = VPaned
|
||||
[[[terminal3]]]
|
||||
command = cd $HOME/tfc/; source venv_tfc/bin/activate; python3.6 tfc.py -l -rx; deactivate
|
||||
directory = ""
|
||||
order = 0
|
||||
parent = child2
|
||||
profile = tfc
|
||||
type = Terminal
|
||||
[[[terminal4]]]
|
||||
command = cd $HOME/tfc/; source venv_tfc/bin/activate; python3.6 tfc.py -l -d; deactivate
|
||||
directory = ""
|
||||
order = 1
|
||||
parent = child2
|
||||
profile = tfc
|
||||
type = Terminal
|
||||
[[[terminal7]]]
|
||||
command = cd $HOME/tfc/; python3.6 dd.py nhrxlr
|
||||
directory = ""
|
||||
order = 0
|
||||
parent = child6
|
||||
profile = tfc
|
||||
type = Terminal
|
||||
[[[terminal8]]]
|
||||
command = cd $HOME/tfc/; python3.6 dd.py txnhlr
|
||||
directory = ""
|
||||
order = 1
|
||||
parent = child6
|
||||
profile = tfc
|
||||
type = Terminal
|
||||
[[[terminal9]]]
|
||||
command = cd $HOME/tfc/; source venv_nh/bin/activate; python3.5 nh.py -l -d; deactivate
|
||||
directory = ""
|
||||
order = 1
|
||||
parent = child5
|
||||
profile = tfc
|
||||
type = Terminal
|
||||
[[tfc-dd-rl]]
|
||||
[[[child0]]]
|
||||
fullscreen = False
|
||||
maximised = True
|
||||
order = 0
|
||||
parent = ""
|
||||
position = 1969:24
|
||||
size = 1871, 1056
|
||||
title = TFC
|
||||
type = Window
|
||||
[[[child1]]]
|
||||
order = 0
|
||||
parent = child0
|
||||
position = 983
|
||||
ratio = 0.52699091395
|
||||
type = HPaned
|
||||
[[[child2]]]
|
||||
order = 0
|
||||
parent = child1
|
||||
position = 742
|
||||
ratio = 0.757884028484
|
||||
type = HPaned
|
||||
[[[child4]]]
|
||||
order = 1
|
||||
parent = child2
|
||||
position = 525
|
||||
ratio = 0.5
|
||||
type = VPaned
|
||||
[[[child7]]]
|
||||
order = 1
|
||||
parent = child1
|
||||
position = 525
|
||||
ratio = 0.5
|
||||
type = VPaned
|
||||
[[[terminal3]]]
|
||||
command = cd $HOME/tfc/; source venv_nh/bin/activate; python3.5 nh.py -l -d; deactivate
|
||||
directory = ""
|
||||
order = 0
|
||||
parent = child2
|
||||
profile = tfc
|
||||
type = Terminal
|
||||
[[[terminal5]]]
|
||||
command = cd $HOME/tfc/; python3.6 dd.py nhrxrl
|
||||
directory = ""
|
||||
order = 0
|
||||
parent = child4
|
||||
profile = tfc
|
||||
type = Terminal
|
||||
[[[terminal6]]]
|
||||
command = cd $HOME/tfc/; python3.6 dd.py txnhrl
|
||||
directory = ""
|
||||
order = 1
|
||||
parent = child4
|
||||
profile = tfc
|
||||
type = Terminal
|
||||
[[[terminal8]]]
|
||||
command = cd $HOME/tfc/; source venv_tfc/bin/activate; python3.6 tfc.py -l -rx; deactivate
|
||||
directory = ""
|
||||
order = 0
|
||||
parent = child7
|
||||
profile = tfc
|
||||
type = Terminal
|
||||
[[[terminal9]]]
|
||||
command = cd $HOME/tfc/; source venv_tfc/bin/activate; python3.6 tfc.py -l -d; deactivate
|
||||
directory = ""
|
||||
order = 1
|
||||
parent = child7
|
||||
profile = tfc
|
||||
type = Terminal
|
||||
[[tfc-lr]]
|
||||
[[[child0]]]
|
||||
fullscreen = False
|
||||
maximised = False
|
||||
order = 0
|
||||
parent = ""
|
||||
position = 1986:40
|
||||
size = 1538, 990
|
||||
title = TFC
|
||||
type = Window
|
||||
[[[child1]]]
|
||||
order = 0
|
||||
parent = child0
|
||||
position = 931
|
||||
ratio = 0.607282184655
|
||||
type = HPaned
|
||||
[[[child2]]]
|
||||
order = 0
|
||||
parent = child1
|
||||
position = 484
|
||||
ratio = 0.491919191919
|
||||
type = VPaned
|
||||
[[[terminal3]]]
|
||||
command = cd $HOME/tfc/; source venv_tfc/bin/activate; python3.6 tfc.py -l -rx; deactivate
|
||||
directory = ""
|
||||
order = 0
|
||||
parent = child2
|
||||
profile = tfc
|
||||
type = Terminal
|
||||
[[[terminal4]]]
|
||||
command = cd $HOME/tfc/; source venv_tfc/bin/activate; python3.6 tfc.py -l; deactivate
|
||||
directory = ""
|
||||
order = 1
|
||||
parent = child2
|
||||
profile = tfc
|
||||
type = Terminal
|
||||
[[[terminal5]]]
|
||||
command = cd $HOME/tfc/; source venv_nh/bin/activate; python3.5 nh.py -l; deactivate
|
||||
directory = ""
|
||||
order = 1
|
||||
parent = child1
|
||||
profile = tfc
|
||||
type = Terminal
|
||||
[[tfc-rl]]
|
||||
[[[child0]]]
|
||||
fullscreen = False
|
||||
maximised = True
|
||||
order = 0
|
||||
parent = ""
|
||||
position = 1969:24
|
||||
size = 1871, 1056
|
||||
title = TFC
|
||||
type = Window
|
||||
[[[child1]]]
|
||||
order = 0
|
||||
parent = child0
|
||||
position = 792
|
||||
ratio = 0.42490646713
|
||||
type = HPaned
|
||||
[[[child3]]]
|
||||
order = 1
|
||||
parent = child1
|
||||
position = 525
|
||||
ratio = 0.5
|
||||
type = VPaned
|
||||
[[[terminal2]]]
|
||||
command = cd $HOME/tfc/; source venv_nh/bin/activate; python3.5 nh.py -l; deactivate
|
||||
directory = ""
|
||||
order = 0
|
||||
parent = child1
|
||||
profile = tfc
|
||||
type = Terminal
|
||||
[[[terminal4]]]
|
||||
command = cd $HOME/tfc/; source venv_tfc/bin/activate; python3.6 tfc.py -l -rx; deactivate
|
||||
directory = ""
|
||||
order = 0
|
||||
parent = child3
|
||||
profile = tfc
|
||||
type = Terminal
|
||||
[[[terminal5]]]
|
||||
command = cd $HOME/tfc/; source venv_tfc/bin/activate; python3.6 tfc.py -l; deactivate
|
||||
directory = ""
|
||||
order = 1
|
||||
parent = child3
|
||||
profile = tfc
|
||||
type = Terminal
|
||||
[plugins]
|
||||
[profiles]
|
||||
[[default]]
|
||||
background_image = None
|
||||
[[tfc]]
|
||||
background_color = "#4c4c4c"
|
||||
background_image = None
|
||||
exit_action = hold
|
||||
foreground_color = "#a1c1c7"
|
||||
scrollback_infinite = True
|
||||
show_titlebar = False
|
|
@ -0,0 +1,76 @@
|
|||
#!/usr/bin/env python3.5
|
||||
# -*- 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 sys
|
||||
import time
|
||||
|
||||
from multiprocessing import Queue, Process
|
||||
|
||||
from src.nh.commands import nh_command
|
||||
from src.nh.gateway import Gateway, gw_incoming
|
||||
from src.nh.misc import c_print, clear_screen, process_arguments
|
||||
from src.nh.pidgin import ensure_im_connection, im_command, im_incoming, im_outgoing
|
||||
from src.nh.settings import Settings
|
||||
from src.nh.tcb import rxm_outgoing, txm_incoming
|
||||
|
||||
__version__ = '0.17.04'
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Start NH side IM plugin for TFC."""
|
||||
settings = Settings(*process_arguments())
|
||||
gateway = Gateway(settings)
|
||||
|
||||
clear_screen()
|
||||
c_print("TFC", head=1, tail=1)
|
||||
|
||||
ensure_im_connection()
|
||||
|
||||
q_to_tip = Queue() # Packets from Gateway to 'tip process'
|
||||
q_to_rxm = Queue() # Packets from TxM/IM client to RxM
|
||||
q_to_im = Queue() # Packets from TxM to IM client
|
||||
q_to_nh = Queue() # Packets from TxM to NH (commands)
|
||||
q_im_cmd = Queue() # Commands from NH to IM client
|
||||
|
||||
gip = Process(target=gw_incoming, args=( q_to_tip, gateway))
|
||||
tip = Process(target=txm_incoming, args=(settings, q_to_tip, q_to_rxm, q_to_im, q_to_nh))
|
||||
rop = Process(target=rxm_outgoing, args=(settings, q_to_rxm, gateway))
|
||||
iip = Process(target=im_incoming, args=(settings, q_to_rxm))
|
||||
iop = Process(target=im_outgoing, args=(settings, q_to_im))
|
||||
imc = Process(target=im_command, args=( q_im_cmd,))
|
||||
nhc = Process(target=nh_command, args=(settings, q_to_nh, q_to_rxm, q_im_cmd, sys.stdin.fileno()))
|
||||
|
||||
process_list = [gip, tip, rop, iip, iop, imc, nhc]
|
||||
for p in process_list:
|
||||
p.start()
|
||||
|
||||
while True:
|
||||
try:
|
||||
time.sleep(0.1)
|
||||
if not all([p.is_alive() for p in process_list]):
|
||||
for p in process_list:
|
||||
p.terminate()
|
||||
exit()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1 @@
|
|||
pyserial==3.3 --hash=sha512:19545d2121e0f43aba7e423df94c6836aba037b86500e2cd9b59555c80fb8596950329833b6d7aeb12a5d3b8e6639a14d64df984bf4e78a1f646364a05e08e4c
|
|
@ -0,0 +1,9 @@
|
|||
# PyNaCl
|
||||
six==1.10.0 --hash=sha512:a41b40b720c5267e4a47ffb98cdc79238831b4fbc0b20abb125504881b73ae38d5ef0215ee91f0d3582e7887244346e45da9410195d023105fccd96239f0ee95
|
||||
pycparser==2.17 --hash=sha512:c9caaa8d256748e0623d077b11931abb38d19367136c70a835f7587e1f7ceb64f3acb7a983dcb68bedd2cf187517762a5753844e8ed58d1d9ed6f364c55839b4
|
||||
cffi==1.9.1 --hash=sha512:0cff1ba30d40837b116c74cb5e1238f70aba40ecedf6034d0e5d7f9be2bb06b2aa9c105cb6ba19b50da967fe9cf5546a5de3255aeabf2c21b24c534fb0f6ed56
|
||||
PyNaCl==1.1.1 --hash=sha512:6410f6ed2a474fefd5df425ea7e76fbe527a9d2ed09b36291caf2c5d0e68704e58caa694e06b01ea61323b2ef16ce85c1478191cf49d7eea969395a3d74d09a8
|
||||
|
||||
argon2==0.1.10 --hash=sha512:d9df98cff4b13cbcd36f5ecea7e3f62e7c21d2bda07d98cfe97f451af0ac3548ce3150c2c35f9f5a45e2143ecf552b6f4807c758ab7ac56a1940aeba3f144ba8
|
||||
pyserial==3.3 --hash=sha512:19545d2121e0f43aba7e423df94c6836aba037b86500e2cd9b59555c80fb8596950329833b6d7aeb12a5d3b8e6639a14d64df984bf4e78a1f646364a05e08e4c
|
||||
virtualenv==15.1.0 --hash=sha512:9988af801d9ad15c3f9831489ee9b49b54388e8349be201e7f7db3f2f1e59d033d3117f12e2f1909d65f052c5f1eacd87a894c6f7f703d770add3a0179e95863
|
|
@ -0,0 +1,17 @@
|
|||
#!/usr/bin/env python3.6
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import hashlib
|
||||
import serial
|
||||
|
||||
|
||||
def sha3_256(message: bytes) -> bytes:
|
||||
"""Generate SHA3-256 digest from message."""
|
||||
return hashlib.sha256(message).hexdigest()
|
||||
|
||||
port = serial.Serial("/dev/ttyUSB0", 9600)
|
||||
|
||||
data = open('install.sh', 'rb').read()
|
||||
|
||||
print(sha3_256(data))
|
||||
port.write(data)
|
21
setup.py.asc
21
setup.py.asc
|
@ -1,21 +0,0 @@
|
|||
-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA1
|
||||
|
||||
47322690ab1817b2770db17c8f830a5d4c5bfb86f712730176320435ddd46c41 setup.py
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
Version: GnuPG v1
|
||||
|
||||
iQIcBAEBAgAGBQJX4vxlAAoJEGXa0OZTMFFOMckP/2KdIN46LCrkKr8fQFZt8bSx
|
||||
e6eUwSDKXTLcnKizAqzz3KNefYSPWVlzK3Ii4jXpAmlunKVF7vmGOPqi6wkvRWQE
|
||||
u+Sgt/7uYfRDOfZhTtFFMpzVzLz7Bn4VlyX4X+jCPKKNkXIquFz3SqrjvWL87Pqp
|
||||
NQ9/yGiu/vVHvXlcfcwgSbMyffinL0VAfopLOT//XXxLPGm7uPRF9/kW+mYq1D3X
|
||||
0FyxyS44YGo6IZ2dvCQ8HkcMtxCbppq6C7jeAETZKUrmJZxQHfRRhrLRe1W4ldQr
|
||||
+t12XRJiNVUC74APgcDHvyXONqBgjNHMB+0jZjOI4usXue8L1oKk/3cQ/cZc4o7B
|
||||
1b+hz+TL2m1V+vTIOtBenu6rNmnk593OGv9zxP1LPV7rEXKGedAWY+++9B8yPZ4p
|
||||
N/kTEZWuW56SKe24x6am/x98EcJu3dje7gdqOH2oThahTQJNo1VhO5oz0Kt4VATB
|
||||
soO64f61jxqsWhbdj8uqD9fUx2hz0p7uE1BXZHWm8MS+6qWWFPtM+gso3cxtQ4E8
|
||||
44pp8K4cls7tSl56i+ZSZc2KN71nYBy7RXgZMMobcc1Wg7QiWYVXMptM7F9czkKj
|
||||
KmT5N9bn6gcAXAkGJYtqqydzrdTbaaLIeo1XVjKFbrw/dZ2jk8Dm4N7TxaK8IuHG
|
||||
lRbVMpdOVfjymyMyAzad
|
||||
=EMRx
|
||||
-----END PGP SIGNATURE-----
|
|
@ -0,0 +1,279 @@
|
|||
#!/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 hashlib
|
||||
import multiprocessing
|
||||
import os
|
||||
|
||||
from typing import Any, Optional, Tuple
|
||||
|
||||
import argon2
|
||||
import nacl.encoding
|
||||
import nacl.exceptions
|
||||
import nacl.public
|
||||
import nacl.secret
|
||||
import nacl.utils
|
||||
|
||||
from src.common.errors import CriticalError
|
||||
from src.common.misc import clear_screen
|
||||
from src.common.output import c_print, phase, print_on_previous_line
|
||||
|
||||
|
||||
def sha3_256(message: bytes) -> bytes:
|
||||
"""Generate SHA3-256 digest from message."""
|
||||
return hashlib.sha3_256(message).digest()
|
||||
|
||||
|
||||
def blake2s(message: bytes, key: bytes = b'') -> bytes:
|
||||
"""Generate Blake2s digest from message."""
|
||||
return hashlib.blake2s(message, key=key).digest()
|
||||
|
||||
|
||||
def sha256(message: bytes) -> bytes:
|
||||
"""Generate SHA256 digest from message."""
|
||||
return hashlib.sha256(message).digest()
|
||||
|
||||
|
||||
def hash_chain(message: bytes) -> bytes:
|
||||
"""Distribute trust on possibly untrustworthy hash functions.
|
||||
|
||||
In case where the the hash algorithm or implementation of it is not secure,
|
||||
this construction prevents single hash function that outputs a low entropy
|
||||
digest from compromising the entire construct. It also distributes trust
|
||||
of pre-image resistance on multiple algorithms. Finally, by creating digests
|
||||
in multiple orders, an individual malicious, stateless hash function is unable
|
||||
to reliably determine what kind of value it should output in order to
|
||||
compromise the mixing phase. A malicious algorithm guessing it's position
|
||||
in construction will eventually cause a key state mismatch.
|
||||
"""
|
||||
d1 = sha3_256(blake2s(sha256(message)))
|
||||
d2 = sha3_256(sha256(blake2s(message)))
|
||||
|
||||
d3 = blake2s(sha3_256(sha256(message)))
|
||||
d4 = blake2s(sha256(sha3_256(message)))
|
||||
|
||||
d5 = sha256(blake2s(sha3_256(message)))
|
||||
d6 = sha256(sha3_256(blake2s(message)))
|
||||
|
||||
d7 = sha3_256(message)
|
||||
d8 = blake2s(message)
|
||||
d9 = sha256(message)
|
||||
|
||||
# Mixing phase
|
||||
x1 = xor(d1, d2)
|
||||
x2 = xor(x1, d3)
|
||||
x3 = xor(x2, d4)
|
||||
x4 = xor(x3, d5)
|
||||
x5 = xor(x4, d6)
|
||||
x6 = xor(x5, d7)
|
||||
x7 = xor(x6, d8)
|
||||
x8 = xor(x7, d9)
|
||||
|
||||
return x8
|
||||
|
||||
|
||||
def argon2_kdf(password: str,
|
||||
salt: bytes,
|
||||
rounds: int,
|
||||
memory: int = None,
|
||||
parallelism: int = None,
|
||||
local_testing: bool = False) -> Tuple[Any, Optional[int]]:
|
||||
"""Derive key from password and salt using Argon2 (PHC winner).
|
||||
|
||||
Adjust parallelism and memory automatically.
|
||||
|
||||
During local testing, drop resource requirements
|
||||
in half to allow simultaneous login on Tx/Rx side.
|
||||
"""
|
||||
if parallelism is None:
|
||||
parallelism = multiprocessing.cpu_count()
|
||||
if local_testing:
|
||||
parallelism = max(1, parallelism // 2)
|
||||
|
||||
if memory is None:
|
||||
with open('/proc/meminfo') as f:
|
||||
mem_avail = int(f.readlines()[2].split()[1])
|
||||
memory = max(128000, mem_avail) # Fail-safe in case available memory is low.
|
||||
if local_testing:
|
||||
memory //= 2
|
||||
|
||||
# Reduce amount of memory required under Travis to avoid ARGON2_MEMORY_ALLOCATION_ERROR.
|
||||
if "TRAVIS" in os.environ and os.environ["TRAVIS"] == "true":
|
||||
memory = 1024
|
||||
|
||||
return argon2.argon2_hash(password, salt, t=rounds, m=memory, p=parallelism, buflen=32), memory
|
||||
|
||||
|
||||
def encrypt_and_sign(plaintext: bytes, key: bytes) -> bytes:
|
||||
"""Encrypt plaintext string with XSalsa20-Poly1305 using 192-bit nonce and 256-bit key.
|
||||
|
||||
:param plaintext: Plaintext to encrypt
|
||||
:param key: 32-byte key
|
||||
:return: Ciphertext + tag
|
||||
"""
|
||||
secret_box = nacl.secret.SecretBox(key)
|
||||
nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE)
|
||||
return bytes(secret_box.encrypt(plaintext, nonce))
|
||||
|
||||
|
||||
def auth_and_decrypt(nonce_ct_tag: bytes, key: bytes, soft_e: bool = False) -> bytes:
|
||||
"""Authenticate and decrypt XSalsa20-Poly1305 ciphertext.
|
||||
|
||||
:param nonce_ct_tag: Nonce, ciphertext and tag
|
||||
:param key: 32-byte key
|
||||
:param soft_e: When True, raises CryptoError instead of graceful exit
|
||||
:return: Plaintext
|
||||
"""
|
||||
try:
|
||||
secret_box = nacl.secret.SecretBox(key)
|
||||
return secret_box.decrypt(nonce_ct_tag)
|
||||
except nacl.exceptions.CryptoError:
|
||||
if not soft_e:
|
||||
raise CriticalError("Ciphertext MAC fail.")
|
||||
raise
|
||||
|
||||
|
||||
def unicode_padding(string: str) -> str:
|
||||
"""Pad unicode string to 255 chars.
|
||||
|
||||
Database fields are padded with unicode chars and then encoded
|
||||
with UTF-32 to hide any metadata about plaintext field length.
|
||||
|
||||
:param string: String to be padded
|
||||
:return: Padded string
|
||||
"""
|
||||
assert len(string) <= 254
|
||||
|
||||
length = 255 - (len(string) % 255)
|
||||
string += length * chr(length)
|
||||
|
||||
assert len(string) == 255
|
||||
|
||||
return string
|
||||
|
||||
|
||||
def byte_padding(string: bytes) -> bytes:
|
||||
"""Pad input bytes to packet max size (255 bytes).
|
||||
|
||||
Output data is encoded with UTF-8 to speed up transmission over serial
|
||||
interface. In normal mode padding hides maximum length of message and
|
||||
during trickle connection because each ciphertext will have constant
|
||||
length, it hides when data transmission takes place.
|
||||
|
||||
:param string: String to be padded
|
||||
:return: Padded string
|
||||
"""
|
||||
length = 255 - (len(string) % 255)
|
||||
string += length * bytes([length])
|
||||
|
||||
assert len(string) % 255 == 0
|
||||
|
||||
return string
|
||||
|
||||
|
||||
def rm_padding_bytes(string: bytes) -> bytes:
|
||||
"""Remove padding from plaintext.
|
||||
|
||||
The length of padding is determined by the ord-value
|
||||
of last character that is always a padding character.
|
||||
|
||||
:param string: String from which padding is removed
|
||||
:return: String without padding
|
||||
"""
|
||||
return string[:-ord(string[-1:])]
|
||||
|
||||
|
||||
def rm_padding_str(string: str) -> str:
|
||||
"""Remove padding from plaintext.
|
||||
|
||||
:param string: String from which padding is removed
|
||||
:return: String without padding
|
||||
"""
|
||||
return string[:-ord(string[-1:])]
|
||||
|
||||
|
||||
def xor(string1: bytes, string2: bytes) -> bytes:
|
||||
"""XOR two byte-strings together."""
|
||||
if len(string1) != len(string2):
|
||||
raise CriticalError("String length mismatch.")
|
||||
|
||||
parts = []
|
||||
for b1, b2 in zip(string1, string2):
|
||||
parts.append(bytes([b1 ^ b2]))
|
||||
return b''.join(parts)
|
||||
|
||||
|
||||
def keygen() -> bytes:
|
||||
"""Generate list of random keys for use in different encryption functions.
|
||||
|
||||
Hash chain is used to add strong pre-image resistance to internal state or
|
||||
/dev/urandom that that only uses SHA1 to compress output.
|
||||
|
||||
As of Python3.6.0, os.urandom is a wrapper for best available CSPRNG. Linux
|
||||
3.17 and older kernels do not support the GETRANDOM call, thus on such
|
||||
kernels, Python3.6's os.urandom will fallback to non-blocking /dev/urandom
|
||||
that is not secure on live distros that have low entropy at the start of
|
||||
session.
|
||||
|
||||
TFC uses os.getrandom(32, flags=0) explicitly. This forces the version of
|
||||
Python interpreter to version 3.6 or later and Linux kernel version to 3.17
|
||||
or later. The flag 0 will block urandom if internal state has less than 128
|
||||
bits of entropy. Secure key entropy is thus enforced on all platforms.
|
||||
|
||||
Since kernel 4.8, /dev/urandom has been upgraded to use ChaCha20 instead
|
||||
of SHA1.
|
||||
|
||||
:return: Cryptographically secure 256-bit random key
|
||||
"""
|
||||
# Fallback to urandom on Travis' Python3.6 that currently lacks os.getrandom call.
|
||||
if "TRAVIS" in os.environ and os.environ["TRAVIS"] == "true":
|
||||
return hash_chain(os.urandom(32))
|
||||
else:
|
||||
return hash_chain(os.getrandom(32, flags=0))
|
||||
|
||||
|
||||
def init_entropy() -> None:
|
||||
"""Wait until Kernel CSPRNG is sufficiently seeded.
|
||||
|
||||
Wait until entropy_avail file states that system has at least 512 bits of
|
||||
entropy. The headroom allows room for error in accuracy of entropy
|
||||
collector's entropy estimator; As long as input has at least 4 bits per
|
||||
byte of actual entropy, /dev/urandom will be sufficiently seeded when
|
||||
it is allowed to generate keys.
|
||||
"""
|
||||
clear_screen()
|
||||
phase("Waiting for Kernel CSPRNG random pool to fill up", head=1)
|
||||
|
||||
ent_avail = 0
|
||||
threshold = 512
|
||||
|
||||
while ent_avail < threshold:
|
||||
try:
|
||||
with open('/proc/sys/kernel/random/entropy_avail') as f:
|
||||
value = f.read()
|
||||
ent_avail = int(value.strip())
|
||||
c_print("{}/{}".format(ent_avail, threshold))
|
||||
print_on_previous_line(delay=0.01)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
pass
|
||||
|
||||
print_on_previous_line()
|
||||
phase("Waiting for Kernel CSPRNG random pool to fill up")
|
||||
phase("Done")
|
|
@ -0,0 +1,249 @@
|
|||
#!/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 List
|
||||
|
||||
from src.common.crypto import auth_and_decrypt, encrypt_and_sign
|
||||
from src.common.encoding import bool_to_bytes, str_to_bytes
|
||||
from src.common.encoding import bytes_to_bool, bytes_to_str
|
||||
from src.common.misc import clear_screen, ensure_dir, get_tty_w, split_byte_string
|
||||
from src.common.statics import *
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.common.db_masterkey import MasterKey
|
||||
from src.common.db_settings import Settings
|
||||
|
||||
|
||||
class Contact(object):
|
||||
"""Contact object collects contact-data unrelated to key rotation."""
|
||||
|
||||
def __init__(self,
|
||||
rx_account: str,
|
||||
tx_account: str,
|
||||
nick: str,
|
||||
tx_fingerprint: bytes,
|
||||
rx_fingerprint: bytes,
|
||||
log_messages: bool,
|
||||
file_reception: bool,
|
||||
notifications: bool) -> None:
|
||||
"""Create a new contact object."""
|
||||
self.rx_account = rx_account
|
||||
self.tx_account = tx_account
|
||||
self.nick = nick
|
||||
|
||||
self.tx_fingerprint = tx_fingerprint
|
||||
self.rx_fingerprint = rx_fingerprint
|
||||
|
||||
self.log_messages = log_messages
|
||||
self.file_reception = file_reception
|
||||
self.notifications = notifications
|
||||
|
||||
def dump_c(self) -> bytes:
|
||||
"""Return contact data as constant length byte string."""
|
||||
return str_to_bytes(self.rx_account) \
|
||||
+ str_to_bytes(self.tx_account) \
|
||||
+ str_to_bytes(self.nick) \
|
||||
+ self.tx_fingerprint \
|
||||
+ self.rx_fingerprint \
|
||||
+ bool_to_bytes(self.log_messages) \
|
||||
+ bool_to_bytes(self.file_reception) \
|
||||
+ bool_to_bytes(self.notifications)
|
||||
|
||||
|
||||
class ContactList(object):
|
||||
"""ContactList object manages list of contact objects."""
|
||||
|
||||
def __init__(self, master_key: 'MasterKey', settings: 'Settings') -> None:
|
||||
"""Create a new contact list object."""
|
||||
self.master_key = master_key
|
||||
self.settings = settings
|
||||
self.contacts = [] # type: List[Contact]
|
||||
self.file_name = f'{DIR_USER_DATA}/{settings.software_operation}_contacts'
|
||||
|
||||
if os.path.isfile(self.file_name):
|
||||
self.load_contacts()
|
||||
else:
|
||||
self.store_contacts()
|
||||
|
||||
def __iter__(self) -> 'ContactList':
|
||||
"""Iterate over contacts."""
|
||||
for c in self.contacts:
|
||||
yield c
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return number of contacts in contact list."""
|
||||
return len(self.contacts)
|
||||
|
||||
def load_contacts(self) -> None:
|
||||
"""Load contacts from encrypted database."""
|
||||
ensure_dir(f'{DIR_USER_DATA}/')
|
||||
with open(self.file_name, 'rb') as f:
|
||||
ct_bytes = f.read()
|
||||
|
||||
pt_bytes = auth_and_decrypt(ct_bytes, self.master_key.master_key)
|
||||
entries = split_byte_string(pt_bytes, item_len=3139) # 3 * 1024 + 2 * 32 + 3 * 1
|
||||
dummy_id = 'dummy_contact'.encode('utf-32')
|
||||
contacts = [e for e in entries if not e.startswith(dummy_id)]
|
||||
|
||||
for c in contacts:
|
||||
rx_account = bytes_to_str(c[ 0:1024])
|
||||
tx_account = bytes_to_str(c[1024:2048])
|
||||
nick = bytes_to_str(c[2048:3072])
|
||||
tx_fingerprint = c[3072:3104]
|
||||
rx_fingerprint = c[3104:3136]
|
||||
log_messages = bytes_to_bool(c[3136:3137])
|
||||
file_reception = bytes_to_bool(c[3137:3138])
|
||||
notifications = bytes_to_bool(c[3138:3139])
|
||||
|
||||
self.contacts.append(Contact(rx_account, tx_account, nick,
|
||||
tx_fingerprint, rx_fingerprint,
|
||||
log_messages, file_reception, notifications))
|
||||
|
||||
def store_contacts(self) -> None:
|
||||
"""Write contacts to encrypted database."""
|
||||
dummy_contact_bytes = self.generate_dummy_contact()
|
||||
number_of_dummies = self.settings.m_number_of_accnts - len(self.contacts)
|
||||
|
||||
pt_bytes = b''.join([c.dump_c() for c in self.contacts])
|
||||
pt_bytes += number_of_dummies * dummy_contact_bytes
|
||||
ct_bytes = encrypt_and_sign(pt_bytes, self.master_key.master_key)
|
||||
|
||||
ensure_dir(f'{DIR_USER_DATA}/')
|
||||
with open(self.file_name, 'wb+') as f:
|
||||
f.write(ct_bytes)
|
||||
|
||||
@staticmethod
|
||||
def generate_dummy_contact() -> bytes:
|
||||
"""Generate byte string for dummy contact."""
|
||||
rx_account = str_to_bytes('dummy_contact')
|
||||
tx_account = str_to_bytes('dummy_user')
|
||||
nick = str_to_bytes('dummy_nick')
|
||||
tx_fingerprint = bytes(32)
|
||||
rx_fingerprint = bytes(32)
|
||||
logging_bytes = bool_to_bytes(False)
|
||||
file_r_bytes = bool_to_bytes(False)
|
||||
notify_bytes = bool_to_bytes(False)
|
||||
|
||||
return rx_account + tx_account + nick \
|
||||
+ tx_fingerprint + rx_fingerprint \
|
||||
+ logging_bytes + file_r_bytes + notify_bytes
|
||||
|
||||
def get_contact(self, selector: str) -> Contact:
|
||||
"""Load contact from list based on unique ID (account name or nick)."""
|
||||
return next(c for c in self.contacts if selector in [c.rx_account, c.nick])
|
||||
|
||||
def contact_selectors(self) -> List[str]:
|
||||
"""Return list of UIDs contacts can be selected with."""
|
||||
return self.get_list_of_accounts() + self.get_list_of_nicks()
|
||||
|
||||
def get_list_of_accounts(self) -> List[str]:
|
||||
"""Return list of accounts."""
|
||||
return [c.rx_account for c in self.contacts if c.rx_account != 'local']
|
||||
|
||||
def get_list_of_nicks(self) -> List[str]:
|
||||
"""Return list of nicks."""
|
||||
return [c.nick for c in self.contacts if c.nick != 'local']
|
||||
|
||||
def get_list_of_users_accounts(self) -> List[str]:
|
||||
"""Return list of user's accounts."""
|
||||
return list(set([c.tx_account for c in self.contacts if c.tx_account != 'local']))
|
||||
|
||||
def remove_contact(self, selector: str) -> None:
|
||||
"""Remove account based on account/nick, update database file."""
|
||||
for i, c in enumerate(self.contacts):
|
||||
if selector in [c.rx_account, c.nick]:
|
||||
del self.contacts[i]
|
||||
self.store_contacts()
|
||||
break
|
||||
|
||||
def has_contacts(self) -> bool:
|
||||
"""Return True if contact list has any contacts, else False."""
|
||||
return any(self.get_list_of_accounts())
|
||||
|
||||
def has_contact(self, selector: str) -> bool:
|
||||
"""Return True if contact with account/nick exists, else False."""
|
||||
return selector in self.contact_selectors()
|
||||
|
||||
def has_local_contact(self) -> bool:
|
||||
"""Return True if local key exists, else False."""
|
||||
return any(c.rx_account == 'local' for c in self.contacts)
|
||||
|
||||
def add_contact(self,
|
||||
rx_account: str,
|
||||
tx_account: str,
|
||||
nick: str,
|
||||
tx_fingerprint: bytes,
|
||||
rx_fingerprint: bytes,
|
||||
log_messages: bool,
|
||||
file_reception: bool,
|
||||
notifications: bool) -> None:
|
||||
"""Add new contact to contact list, write changes to database."""
|
||||
if self.has_contact(rx_account):
|
||||
self.remove_contact(rx_account)
|
||||
|
||||
contact = Contact(rx_account, tx_account, nick,
|
||||
tx_fingerprint, rx_fingerprint,
|
||||
log_messages, file_reception, notifications)
|
||||
|
||||
self.contacts.append(contact)
|
||||
self.store_contacts()
|
||||
|
||||
def print_contacts(self, spacing: bool = True) -> None:
|
||||
"""Print list of contacts."""
|
||||
# Columns
|
||||
c1 = ['Contact']
|
||||
c2 = ['Logging']
|
||||
c3 = ['Notify']
|
||||
c4 = ['Files ']
|
||||
c5 = ['Key Ex']
|
||||
c6 = ['Account']
|
||||
|
||||
for c in self.contacts:
|
||||
if c.rx_account == 'local':
|
||||
continue
|
||||
|
||||
c1.append(c.nick)
|
||||
c2.append('Yes' if c.log_messages else 'No')
|
||||
c3.append('Yes' if c.notifications else 'No')
|
||||
c4.append('Accept' if c.file_reception else 'Reject')
|
||||
c5.append('PSK' if c.tx_fingerprint == bytes(32) else 'X25519')
|
||||
c6.append(c.rx_account)
|
||||
|
||||
lst = []
|
||||
for nick, log_setting, notify_setting, file_reception_setting, key_exchange, account in zip(c1, c2, c3, c4, c5, c6):
|
||||
lst.append('{0:{6}} {1:{7}} {2:{8}} {3:{9}} {4:{10}} {5}'.format(
|
||||
nick, log_setting, notify_setting, file_reception_setting, key_exchange, account,
|
||||
len(max(c1, key=len)) + 4,
|
||||
len(max(c2, key=len)) + 4,
|
||||
len(max(c3, key=len)) + 4,
|
||||
len(max(c4, key=len)) + 4,
|
||||
len(max(c5, key=len)) + 4,
|
||||
len(max(c6, key=len)) + 4))
|
||||
|
||||
if spacing:
|
||||
clear_screen()
|
||||
print('')
|
||||
|
||||
lst.insert(1, get_tty_w() * '─')
|
||||
print('\n'.join(str(l) for l in lst))
|
||||
print('\n')
|
|
@ -0,0 +1,318 @@
|
|||
#!/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 textwrap
|
||||
import typing
|
||||
|
||||
from typing import Callable, List
|
||||
|
||||
from src.common.crypto import auth_and_decrypt, encrypt_and_sign
|
||||
from src.common.encoding import bool_to_bytes, int_to_bytes, str_to_bytes
|
||||
from src.common.encoding import bytes_to_bool, bytes_to_int, bytes_to_str
|
||||
from src.common.misc import ensure_dir, get_tty_w, round_up, split_byte_string
|
||||
from src.common.statics import *
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.common.db_contacts import Contact, ContactList
|
||||
from src.common.db_masterkey import MasterKey
|
||||
from src.common.db_settings import Settings
|
||||
|
||||
|
||||
class Group(object):
|
||||
"""Group object contains a list of contact objects (members of group) and settings related to group."""
|
||||
|
||||
def __init__(self,
|
||||
name: str,
|
||||
log_messages: bool,
|
||||
notifications: bool,
|
||||
members: List['Contact'],
|
||||
settings: 'Settings',
|
||||
store_groups: Callable # Reference to group list's method that stores groups
|
||||
) -> None:
|
||||
"""Create a new group object."""
|
||||
self.name = name
|
||||
self.log_messages = log_messages
|
||||
self.notifications = notifications
|
||||
self.members = members
|
||||
self.settings = settings
|
||||
self.store_groups = store_groups
|
||||
|
||||
def __iter__(self) -> 'Contact':
|
||||
"""Iterate over members in group."""
|
||||
for m in self.members:
|
||||
yield m
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return number of members in group."""
|
||||
return len(self.members)
|
||||
|
||||
def dump_g(self) -> bytes:
|
||||
"""Return group data as constant length byte string."""
|
||||
name = str_to_bytes(self.name)
|
||||
log_messages = bool_to_bytes(self.log_messages)
|
||||
notifications = bool_to_bytes(self.notifications)
|
||||
members = self.get_list_of_member_accounts()
|
||||
|
||||
num_of_dummies = self.settings.m_members_in_group - len(self.members)
|
||||
members += num_of_dummies * ['dummy_member']
|
||||
member_bytes = b''.join([str_to_bytes(m) for m in members])
|
||||
|
||||
return name + log_messages + notifications + member_bytes
|
||||
|
||||
def get_list_of_member_accounts(self) -> List[str]:
|
||||
"""Return list of members' rx_accounts."""
|
||||
return [m.rx_account for m in self.members]
|
||||
|
||||
def get_list_of_member_nicks(self) -> List[str]:
|
||||
"""Return list of members' nicks."""
|
||||
return [m.nick for m in self.members]
|
||||
|
||||
def has_member(self, account: str) -> bool:
|
||||
"""Return True if specified account is in group, else False."""
|
||||
return any(m.rx_account == account for m in self.members)
|
||||
|
||||
def has_members(self) -> bool:
|
||||
"""Return True if group has contact objects, else False."""
|
||||
return any(self.members)
|
||||
|
||||
def add_members(self, contacts: List['Contact']) -> None:
|
||||
"""Add list of contact objects to group."""
|
||||
for c in contacts:
|
||||
if c.rx_account not in self.get_list_of_member_accounts():
|
||||
self.members.append(c)
|
||||
self.store_groups()
|
||||
|
||||
def remove_members(self, accounts: List[str]) -> bool:
|
||||
"""Remove contact objects from group."""
|
||||
removed_one = False
|
||||
for account in accounts:
|
||||
for i, m in enumerate(self.members):
|
||||
if account == m.rx_account:
|
||||
del self.members[i]
|
||||
removed_one = True
|
||||
if removed_one:
|
||||
self.store_groups()
|
||||
return removed_one
|
||||
|
||||
|
||||
class GroupList(object):
|
||||
"""GroupList object manages list of group objects and encrypted group database."""
|
||||
|
||||
def __init__(self,
|
||||
master_key: 'MasterKey',
|
||||
settings: 'Settings',
|
||||
contact_list: 'ContactList') -> None:
|
||||
"""Create a new group list object."""
|
||||
self.master_key = master_key
|
||||
self.contact_list = contact_list
|
||||
self.settings = settings
|
||||
self.groups = [] # type: List[Group]
|
||||
self.file_name = f'{DIR_USER_DATA}/{settings.software_operation}_groups'
|
||||
|
||||
if os.path.isfile(self.file_name):
|
||||
self.load_groups()
|
||||
else:
|
||||
self.store_groups()
|
||||
|
||||
def __iter__(self) -> 'Group':
|
||||
"""Iterate over list of groups."""
|
||||
for g in self.groups:
|
||||
yield g
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return number of groups."""
|
||||
return len(self.groups)
|
||||
|
||||
def load_groups(self) -> None:
|
||||
"""Load groups from encrypted database."""
|
||||
ensure_dir(f'{DIR_USER_DATA}/')
|
||||
with open(self.file_name, 'rb') as f:
|
||||
ct_bytes = f.read()
|
||||
|
||||
pt_bytes = auth_and_decrypt(ct_bytes, self.master_key.master_key)
|
||||
update_db = False
|
||||
|
||||
# Slice and decode headers
|
||||
padding_for_g = bytes_to_int(pt_bytes[0:8])
|
||||
padding_for_m = bytes_to_int(pt_bytes[8:16])
|
||||
n_of_actual_g = bytes_to_int(pt_bytes[16:24])
|
||||
largest_group = bytes_to_int(pt_bytes[24:32])
|
||||
|
||||
if n_of_actual_g > self.settings.m_number_of_groups:
|
||||
self.settings.m_number_of_groups = round_up(n_of_actual_g)
|
||||
self.settings.store_settings()
|
||||
update_db = True
|
||||
print("Group database had {} groups. Increased max number of groups to {}."
|
||||
.format(n_of_actual_g, self.settings.m_number_of_groups))
|
||||
|
||||
if largest_group > self.settings.m_members_in_group:
|
||||
self.settings.m_members_in_group = round_up(largest_group)
|
||||
self.settings.store_settings()
|
||||
update_db = True
|
||||
print("A group in group database had {} members. Increased max size of groups to {}."
|
||||
.format(largest_group, self.settings.m_members_in_group))
|
||||
|
||||
# Strip header bytes
|
||||
pt_bytes = pt_bytes[32:]
|
||||
|
||||
# ( no_fields * (padding + BOM) * bytes/char) + booleans
|
||||
bytes_per_group = ((1 + padding_for_m) * ( 255 + 1 ) * 4 ) + 2
|
||||
|
||||
# Remove dummy groups
|
||||
no_dummy_groups = padding_for_g - n_of_actual_g
|
||||
pt_bytes = pt_bytes[:-(no_dummy_groups * bytes_per_group)]
|
||||
|
||||
groups = split_byte_string(pt_bytes, item_len=bytes_per_group)
|
||||
|
||||
for g in groups:
|
||||
|
||||
# Remove padding
|
||||
name = bytes_to_str( g[ 0:1024])
|
||||
log_messages = bytes_to_bool( g[1024:1025])
|
||||
notifications = bytes_to_bool( g[1025:1026])
|
||||
members_b = split_byte_string(g[1026:], item_len=1024)
|
||||
members = [bytes_to_str(m) for m in members_b]
|
||||
|
||||
# Remove dummy members
|
||||
members_df = [m for m in members if not m == 'dummy_member']
|
||||
|
||||
# Load contacts based on stored rx_account
|
||||
group_members = [self.contact_list.get_contact(m) for m in members_df if self.contact_list.has_contact(m)]
|
||||
|
||||
self.groups.append(Group(name, log_messages, notifications, group_members, self.settings, self.store_groups))
|
||||
|
||||
if update_db:
|
||||
self.store_groups()
|
||||
|
||||
def store_groups(self) -> None:
|
||||
"""Write groups to encrypted database."""
|
||||
dummy_group_bytes = self.generate_dummy_group()
|
||||
number_of_dummies = self.settings.m_number_of_groups - len(self.groups)
|
||||
|
||||
pt_bytes = self.generate_header()
|
||||
pt_bytes += b''.join([g.dump_g() for g in self.groups])
|
||||
pt_bytes += number_of_dummies * dummy_group_bytes
|
||||
ct_bytes = encrypt_and_sign(pt_bytes, self.master_key.master_key)
|
||||
|
||||
ensure_dir(f'{DIR_USER_DATA}/')
|
||||
with open(self.file_name, 'wb+') as f:
|
||||
f.write(ct_bytes)
|
||||
|
||||
def generate_header(self) -> bytes:
|
||||
"""Generate group database metadata header."""
|
||||
padding_for_g = int_to_bytes(self.settings.m_number_of_groups)
|
||||
padding_for_m = int_to_bytes(self.settings.m_members_in_group)
|
||||
n_of_actual_g = int_to_bytes(len(self.groups))
|
||||
largest_group = int_to_bytes(self.largest_group())
|
||||
|
||||
return b''.join([padding_for_g, padding_for_m, n_of_actual_g, largest_group])
|
||||
|
||||
|
||||
def generate_dummy_group(self) -> bytes:
|
||||
"""Generate a byte string that represents a dummy group."""
|
||||
name = str_to_bytes('dummy_group')
|
||||
log_messages = bool_to_bytes(False)
|
||||
notifications = bool_to_bytes(False)
|
||||
members = self.settings.m_members_in_group * ['dummy_member']
|
||||
member_bytes = b''.join([str_to_bytes(m) for m in members])
|
||||
|
||||
return name + log_messages + notifications + member_bytes
|
||||
|
||||
def add_group(self,
|
||||
name: str,
|
||||
log_messages: bool,
|
||||
notifications: bool,
|
||||
members: List['Contact']) -> None:
|
||||
"""Add a new group to group list."""
|
||||
if self.has_group(name):
|
||||
self.remove_group(name)
|
||||
|
||||
self.groups.append(Group(name, log_messages, notifications, members, self.settings, self.store_groups))
|
||||
self.store_groups()
|
||||
|
||||
def largest_group(self) -> int:
|
||||
"""Return size of largest group."""
|
||||
largest = 0
|
||||
for g in self.groups:
|
||||
largest = max(len(g), largest)
|
||||
return largest
|
||||
|
||||
def get_list_of_group_names(self) -> List[str]:
|
||||
"""Return list of group names."""
|
||||
return [g.name for g in self.groups]
|
||||
|
||||
def get_group(self, name: str) -> Group:
|
||||
"""Return group object based on it's name."""
|
||||
return next(g for g in self.groups if g.name == name)
|
||||
|
||||
def has_group(self, name: str) -> bool:
|
||||
"""Return True if group list has group with specified name, else False."""
|
||||
return any([g.name == name for g in self.groups])
|
||||
|
||||
def has_groups(self) -> bool:
|
||||
"""Return True if group list has groups, else False."""
|
||||
return any(self.groups)
|
||||
|
||||
def get_group_members(self, name: str) -> List['Contact']:
|
||||
"""Return list of group members."""
|
||||
return self.get_group(name).members
|
||||
|
||||
def remove_group(self, name: str) -> None:
|
||||
"""Remove group from group list."""
|
||||
for i, g in enumerate(self.groups):
|
||||
if g.name == name:
|
||||
del self.groups[i]
|
||||
self.store_groups()
|
||||
break
|
||||
|
||||
def print_groups(self) -> None:
|
||||
"""Print list of groups."""
|
||||
# Columns
|
||||
c1 = ['Group ']
|
||||
c2 = ['Logging']
|
||||
c3 = ['Notify']
|
||||
c4 = ['Members']
|
||||
|
||||
for g in self.groups:
|
||||
c1.append(g.name)
|
||||
c2.append('Yes' if g.log_messages else 'No')
|
||||
c3.append('Yes' if g.notifications else 'No')
|
||||
|
||||
m_indent = 40
|
||||
m_string = ', '.join(sorted([m.nick for m in g.members]))
|
||||
wrapper = textwrap.TextWrapper(width=max(1, (get_tty_w() - m_indent)))
|
||||
mem_lines = wrapper.fill(m_string).split('\n')
|
||||
f_string = mem_lines[0] + '\n'
|
||||
|
||||
for l in mem_lines[1:]:
|
||||
f_string += m_indent * ' ' + l + '\n'
|
||||
c4.append(f_string)
|
||||
|
||||
lst = []
|
||||
for name, log_setting, notify_setting, members in zip(c1, c2, c3, c4):
|
||||
lst.append('{0:{4}} {1:{5}} {2:{6}} {3}'.format(
|
||||
name, log_setting, notify_setting, members,
|
||||
len(max(c1, key=len)) + 4,
|
||||
len(max(c2, key=len)) + 4,
|
||||
len(max(c3, key=len)) + 4))
|
||||
|
||||
print(lst[0] + '\n' + get_tty_w() * '─')
|
||||
print('\n'.join(str(l) for l in lst[1:]) + '\n')
|
|
@ -0,0 +1,212 @@
|
|||
#!/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, Callable, List
|
||||
|
||||
from src.common.crypto import auth_and_decrypt, encrypt_and_sign, hash_chain
|
||||
from src.common.errors import CriticalError
|
||||
from src.common.encoding import str_to_bytes, int_to_bytes
|
||||
from src.common.encoding import bytes_to_str, bytes_to_int
|
||||
from src.common.misc import ensure_dir, split_byte_string
|
||||
from src.common.statics import *
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.common.db_masterkey import MasterKey
|
||||
from src.common.db_settings import Settings
|
||||
|
||||
|
||||
class KeySet(object):
|
||||
"""KeySet object handles frequently changing data (keys and haracs) of contacts."""
|
||||
|
||||
def __init__(self,
|
||||
rx_account: str,
|
||||
tx_key: bytes,
|
||||
rx_key: bytes,
|
||||
tx_hek: bytes,
|
||||
rx_hek: bytes,
|
||||
tx_harac: int,
|
||||
rx_harac: int,
|
||||
store_keys: Callable) -> None:
|
||||
"""Create a new keyset object.
|
||||
|
||||
:param rx_account: Use account as UID for each recipient
|
||||
:param tx_key: Forward secret message key for sent messages
|
||||
:param rx_key: Forward secret message key for received messages (RxM only)
|
||||
:param tx_hek: Static header key for hash ratchet counter encryption
|
||||
:param rx_hek: Static header key for hash ratchet counter decryption (RxM only)
|
||||
:param tx_harac: Hash ratchet counter for sent messages
|
||||
:param rx_harac: Hash ratchet counter for received messages
|
||||
:param store_keys: Reference to KeyLists's method that writes all keys to db.
|
||||
"""
|
||||
self.rx_account = rx_account
|
||||
self.tx_key = tx_key
|
||||
self.rx_key = rx_key
|
||||
self.tx_hek = tx_hek
|
||||
self.rx_hek = rx_hek
|
||||
self.tx_harac = tx_harac
|
||||
self.rx_harac = rx_harac
|
||||
self.store_keys = store_keys
|
||||
|
||||
def dump_k(self) -> bytes:
|
||||
"""Return keyset data as constant length byte string."""
|
||||
return str_to_bytes(self.rx_account) \
|
||||
+ self.tx_key \
|
||||
+ self.rx_key \
|
||||
+ self.tx_hek \
|
||||
+ self.rx_hek \
|
||||
+ int_to_bytes(self.tx_harac) \
|
||||
+ int_to_bytes(self.rx_harac)
|
||||
|
||||
def rotate_tx_key(self) -> None:
|
||||
"""Update TxM side tx-key and harac (provides forward secrecy for messages)."""
|
||||
self.tx_key = hash_chain(self.tx_key)
|
||||
self.tx_harac += 1
|
||||
self.store_keys()
|
||||
|
||||
def update_key(self, direction: str, key: bytes, offset: int) -> None:
|
||||
"""Update RxM side tx/rx-key and harac."""
|
||||
if direction == 'tx':
|
||||
self.tx_key = key
|
||||
self.tx_harac += offset
|
||||
self.store_keys()
|
||||
|
||||
if direction == 'rx':
|
||||
self.rx_key = key
|
||||
self.rx_harac += offset
|
||||
self.store_keys()
|
||||
|
||||
|
||||
class KeyList(object):
|
||||
"""KeyList object manages list of keyset objects and encrypted keyset database.
|
||||
|
||||
The keyset database is separated from contact database as trickle connection needs to update
|
||||
keys frequently with no risk of read/write queue blocking that occurs e.g. when new nick is
|
||||
being stored.
|
||||
"""
|
||||
|
||||
def __init__(self, master_key: 'MasterKey', settings: 'Settings') -> None:
|
||||
"""Create a new key list object."""
|
||||
self.master_key = master_key
|
||||
self.settings = settings
|
||||
self.keysets = [] # type: List[KeySet]
|
||||
self.file_name = f'{DIR_USER_DATA}/{settings.software_operation}_keys'
|
||||
|
||||
if os.path.isfile(self.file_name):
|
||||
self.load_keys()
|
||||
else:
|
||||
self.store_keys()
|
||||
|
||||
def load_keys(self) -> None:
|
||||
"""Load keys from encrypted database."""
|
||||
ensure_dir(f'{DIR_USER_DATA}/')
|
||||
with open(self.file_name, 'rb') as f:
|
||||
ct_bytes = f.read()
|
||||
|
||||
pt_bytes = auth_and_decrypt(ct_bytes, self.master_key.master_key)
|
||||
keysets = split_byte_string(pt_bytes, item_len=1168) # 1024 + 4 * 32 + 2 * 8
|
||||
dummy_id = 'dummy_contact'.encode('utf-32')
|
||||
keysets = [k for k in keysets if not k.startswith(dummy_id)]
|
||||
|
||||
for k in keysets:
|
||||
rx_account = bytes_to_str(k[0:1024])
|
||||
tx_key = k[1024:1056]
|
||||
rx_key = k[1056:1088]
|
||||
tx_hek = k[1088:1120]
|
||||
rx_hek = k[1120:1152]
|
||||
tx_harac = bytes_to_int(k[1152:1160])
|
||||
rx_harac = bytes_to_int(k[1160:1168])
|
||||
|
||||
self.keysets.append(KeySet(rx_account,
|
||||
tx_key, rx_key,
|
||||
tx_hek, rx_hek,
|
||||
tx_harac, rx_harac,
|
||||
self.store_keys))
|
||||
|
||||
def store_keys(self) -> None:
|
||||
"""Write keys to encrypted database."""
|
||||
num_of_dummies = self.settings.m_number_of_accnts - len(self.keysets)
|
||||
dummy_keyset = self.generate_dummy_keyset()
|
||||
|
||||
pt_bytes = b''.join([k.dump_k() for k in self.keysets])
|
||||
pt_bytes += num_of_dummies * dummy_keyset
|
||||
ct_bytes = encrypt_and_sign(pt_bytes, self.master_key.master_key)
|
||||
|
||||
ensure_dir(f'{DIR_USER_DATA}/')
|
||||
with open(self.file_name, 'wb+') as f:
|
||||
f.write(ct_bytes)
|
||||
|
||||
@staticmethod
|
||||
def generate_dummy_keyset() -> bytes:
|
||||
"""Generate bytestring for dummy keyset."""
|
||||
tx_account = str_to_bytes('dummy_contact')
|
||||
tx_key = bytes(32)
|
||||
rx_key = bytes(32)
|
||||
tx_hek = bytes(32)
|
||||
rx_hek = bytes(32)
|
||||
tx_harac = int_to_bytes(0)
|
||||
rx_harac = int_to_bytes(0)
|
||||
|
||||
return tx_account + tx_key + rx_key + tx_hek + rx_hek + tx_harac + rx_harac
|
||||
|
||||
def get_keyset(self, account: str) -> KeySet:
|
||||
"""Load keyset from list based on unique account name."""
|
||||
return next(k for k in self.keysets if account == k.rx_account)
|
||||
|
||||
def has_local_key(self) -> bool:
|
||||
"""Return True if local key exists."""
|
||||
return any(k.rx_account == 'local' for k in self.keysets)
|
||||
|
||||
def has_keyset(self, account: str) -> bool:
|
||||
"""Return True if contact with account exists."""
|
||||
return any(account == k.rx_account for k in self.keysets)
|
||||
|
||||
def add_keyset(self, rx_account: str, tx_key: bytes, rx_key: bytes, tx_hek: bytes, rx_hek: bytes) -> None:
|
||||
"""Add new keyset to key list, write changes to database."""
|
||||
if self.has_keyset(rx_account):
|
||||
self.remove_keyset(rx_account)
|
||||
self.keysets.append(KeySet(rx_account, tx_key, rx_key, tx_hek, rx_hek, 0, 0, self.store_keys))
|
||||
self.store_keys()
|
||||
|
||||
def remove_keyset(self, name: str) -> None:
|
||||
"""Remove keyset from keys based on account, write changes to database."""
|
||||
for i, k in enumerate(self.keysets):
|
||||
if name == k.rx_account:
|
||||
del self.keysets[i]
|
||||
self.store_keys()
|
||||
break
|
||||
|
||||
def change_master_key(self, master_key: 'MasterKey') -> None:
|
||||
"""Change master key, write changes to database."""
|
||||
self.master_key = master_key
|
||||
self.store_keys()
|
||||
|
||||
def manage(self, command: str, *params: Any) -> None:
|
||||
"""Manage keyset database based on data received over km_queue."""
|
||||
if command == 'ADD':
|
||||
self.add_keyset(*params)
|
||||
elif command == 'REM':
|
||||
self.remove_keyset(*params)
|
||||
elif command == 'KEY':
|
||||
self.change_master_key(*params)
|
||||
else:
|
||||
raise CriticalError("Invalid KeyList management command.")
|
|
@ -0,0 +1,295 @@
|
|||
#!/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 datetime
|
||||
import os.path
|
||||
import struct
|
||||
import sys
|
||||
import textwrap
|
||||
import time
|
||||
import typing
|
||||
import zlib
|
||||
|
||||
from typing import List, Tuple, Union
|
||||
|
||||
from src.common.crypto import auth_and_decrypt, encrypt_and_sign, rm_padding_bytes
|
||||
from src.common.errors import FunctionReturn
|
||||
from src.common.encoding import bytes_to_str, str_to_bytes
|
||||
from src.common.misc import clear_screen, ensure_dir, get_tty_w
|
||||
from src.common.statics import *
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from multiprocessing import Queue
|
||||
from src.common.db_contacts import ContactList
|
||||
from src.common.db_masterkey import MasterKey
|
||||
from src.common.db_settings import Settings
|
||||
from src.rx.windows import Window
|
||||
from src.tx.windows import Window as Window_
|
||||
|
||||
|
||||
def log_writer(l_queue: 'Queue') -> None:
|
||||
"""Read log data from queue and write them to log database.
|
||||
|
||||
This process separates writing to logfile from sender_loop to prevent IO
|
||||
delays caused by access to logfile from revealing metadata about when
|
||||
communication takes place.
|
||||
"""
|
||||
while True:
|
||||
try:
|
||||
if l_queue.empty():
|
||||
time.sleep(0.01)
|
||||
continue
|
||||
|
||||
packet, rx_account, settings, master_key = l_queue.get()
|
||||
|
||||
if packet[0] == P_N_HEADER:
|
||||
continue
|
||||
|
||||
# File assembly packet headers are capitalized
|
||||
if bytes([packet[0]]).isupper() and settings.log_dummy_file_a_p:
|
||||
# Log placeholder data instead of sent file
|
||||
packet = F_S_HEADER + bytes(255)
|
||||
|
||||
write_log_entry(packet, rx_account, settings, master_key)
|
||||
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
|
||||
def write_log_entry(assembly_packet: bytes,
|
||||
account: str,
|
||||
settings: 'Settings',
|
||||
master_key: 'MasterKey',
|
||||
origin: bytes = ORIGIN_USER_HEADER) -> None:
|
||||
"""Add assembly packet to encrypted logfile.
|
||||
|
||||
This method of logging allows reconstruction of conversation while protecting
|
||||
the metadata about the length of messages other log file formats would reveal.
|
||||
|
||||
TxM can only log sent messages. This is not useful for recalling conversations
|
||||
but serves an important role in audit of RxM-side logs, where malware could
|
||||
have substituted logged data on RxM.
|
||||
|
||||
To protect possibly sensitive files that must not be logged, only placeholder
|
||||
data is logged about them. This helps hiding the amount of communication
|
||||
comparison with log file size and output packet count would otherwise reveal.
|
||||
|
||||
:param assembly_packet: Assembly packet to log
|
||||
:param account: Recipient's account (UID)
|
||||
:param origin: Direction of logged packet
|
||||
:param settings: Settings object
|
||||
:param master_key: Master key object
|
||||
:return: None
|
||||
"""
|
||||
unix_timestamp = int(time.time())
|
||||
timestamp_bytes = struct.pack('<L', unix_timestamp)
|
||||
encoded_account = str_to_bytes(account)
|
||||
|
||||
pt_bytes = timestamp_bytes + origin + encoded_account + assembly_packet
|
||||
ct_bytes = encrypt_and_sign(pt_bytes, key=master_key.master_key)
|
||||
|
||||
ensure_dir(f'{DIR_USER_DATA}/')
|
||||
file_name = f'{DIR_USER_DATA}/{settings.software_operation}_logs'
|
||||
with open(file_name, 'ab+') as f:
|
||||
f.write(ct_bytes)
|
||||
|
||||
|
||||
def access_history(window: Union['Window', 'Window_'],
|
||||
contact_list: 'ContactList',
|
||||
settings: 'Settings',
|
||||
master_key: 'MasterKey',
|
||||
msg_to_load: int = 0,
|
||||
export: bool = False) -> None:
|
||||
"""Decrypt 'msg_to_load' last messages from log database and display/export it.
|
||||
|
||||
:param window: Window object
|
||||
:param contact_list: ContactList object
|
||||
:param settings: Settings object
|
||||
:param master_key: Master key object
|
||||
:param msg_to_load: Number of messages to load
|
||||
:param export: When True, write logged messages into
|
||||
plaintext file instead of printing them.
|
||||
:return: None
|
||||
"""
|
||||
|
||||
def read_entry():
|
||||
"""Read encrypted log entry.
|
||||
|
||||
Length | Data type
|
||||
--------|--------------------------------
|
||||
24 | XSalsa20 nonce
|
||||
4 | Timestamp
|
||||
4 | UTF-32 BOM
|
||||
4*255 | Padded account (UTF-32)
|
||||
1 | Origin header
|
||||
1 | Assembly packet header
|
||||
255 | Padded assembly packet (UTF-8)
|
||||
16 | Poly1305 tag
|
||||
"""
|
||||
return log_file.read(1325)
|
||||
|
||||
ensure_dir(f'{DIR_USER_DATA}/')
|
||||
file_name = f'{DIR_USER_DATA}/{settings.software_operation}_logs'
|
||||
if not os.path.isfile(file_name):
|
||||
raise FunctionReturn(f"Error: Could not find '{file_name}'.")
|
||||
|
||||
log_file = open(file_name, 'rb')
|
||||
ts_message_list = [] # type: List[Tuple[str, str, bytes, str]]
|
||||
assembly_p_buffer = dict()
|
||||
group_timestamp = b''
|
||||
|
||||
for ct in iter(read_entry, b''):
|
||||
pt = auth_and_decrypt(ct, key=master_key.master_key)
|
||||
account = bytes_to_str(pt[5:1029])
|
||||
|
||||
if window.type == 'contact' and window.uid != account:
|
||||
continue
|
||||
|
||||
t_stamp = parse_ts_bytes(pt[0:4], settings)
|
||||
origin_byte = pt[4:5]
|
||||
origin = origin_byte.decode()
|
||||
assembly_header = pt[1029:1030]
|
||||
assembly_pt = pt[1030:]
|
||||
|
||||
if assembly_header == M_S_HEADER:
|
||||
depadded = rm_padding_bytes(assembly_pt)
|
||||
decompressed = zlib.decompress(depadded)
|
||||
if decompressed[:1] == PRIVATE_MESSAGE_HEADER:
|
||||
if window.type == 'group':
|
||||
continue
|
||||
decoded = decompressed[1:].decode()
|
||||
|
||||
elif decompressed[:1] == GROUP_MESSAGE_HEADER:
|
||||
|
||||
group_name, decoded = [f.decode() for f in decompressed[9:].split(US_BYTE)]
|
||||
if group_name != window.name:
|
||||
continue
|
||||
if group_timestamp == decompressed[1:9]:
|
||||
continue
|
||||
else:
|
||||
group_timestamp = decompressed[1:9]
|
||||
|
||||
ts_message_list.append((t_stamp, account, origin_byte, decoded))
|
||||
|
||||
elif assembly_header == M_L_HEADER:
|
||||
assembly_p_buffer[origin + account] = assembly_pt
|
||||
|
||||
elif assembly_header == M_A_HEADER:
|
||||
if (origin + account) in assembly_p_buffer:
|
||||
assembly_p_buffer[origin + account] += assembly_pt
|
||||
|
||||
elif assembly_header == M_E_HEADER:
|
||||
if (origin + account) in assembly_p_buffer:
|
||||
assembly_p_buffer[origin + account] += assembly_pt
|
||||
|
||||
pt_buf = assembly_p_buffer.pop(origin + account)
|
||||
inner_l = rm_padding_bytes(pt_buf)
|
||||
msg_key = inner_l[-32:]
|
||||
enc_msg = inner_l[:-32]
|
||||
decrypted = auth_and_decrypt(enc_msg, key=msg_key)
|
||||
decompressed = zlib.decompress(decrypted)
|
||||
|
||||
if decompressed[:1] == PRIVATE_MESSAGE_HEADER:
|
||||
if window.type == 'group':
|
||||
continue
|
||||
decoded = decompressed[1:].decode()
|
||||
|
||||
elif decompressed[:1] == GROUP_MESSAGE_HEADER:
|
||||
group_name, decoded = [f.decode() for f in decompressed[9:].split(US_BYTE)]
|
||||
if group_name != window.name:
|
||||
continue
|
||||
if group_timestamp == decompressed[1:9]: # Skip duplicates of outgoing messages
|
||||
continue
|
||||
else:
|
||||
group_timestamp = decompressed[1:9]
|
||||
|
||||
ts_message_list.append((t_stamp, account, origin_byte, decoded))
|
||||
|
||||
elif assembly_header == M_C_HEADER:
|
||||
assembly_p_buffer.pop(origin + account, None)
|
||||
|
||||
log_file.close()
|
||||
|
||||
if not export:
|
||||
clear_screen()
|
||||
print('')
|
||||
|
||||
tty_w = get_tty_w()
|
||||
|
||||
system = dict(tx="TxM", rx="RxM", ut="Unittest")[settings.software_operation]
|
||||
m_dir = dict(tx="sent to", rx="to/from", ut="to/from")[settings.software_operation]
|
||||
|
||||
f_name = open(f"{system} - Plaintext log ({window.name})", 'w+') if export else sys.stdout
|
||||
subset = '' if msg_to_load == 0 else f"{msg_to_load} most recent "
|
||||
title = textwrap.fill(f"Log file of {subset}message(s) {m_dir} {window.name}", tty_w)
|
||||
|
||||
print(title, file=f_name)
|
||||
print(tty_w * '═', file=f_name)
|
||||
|
||||
for timestamp, account, origin_, message in ts_message_list[-msg_to_load:]:
|
||||
|
||||
nick = "Me" if origin_ == ORIGIN_USER_HEADER else contact_list.get_contact(account).nick
|
||||
|
||||
print(textwrap.fill(f"{timestamp} {nick}:", tty_w), file=f_name)
|
||||
print('', file=f_name)
|
||||
print(textwrap.fill(message, tty_w), file=f_name)
|
||||
print('', file=f_name)
|
||||
print(tty_w * '─', file=f_name)
|
||||
|
||||
if export:
|
||||
f_name.close()
|
||||
else:
|
||||
print('')
|
||||
|
||||
|
||||
def re_encrypt(previous_key: bytes, new_key: bytes, settings: 'Settings') -> None:
|
||||
"""Re-encrypt database with a new master key."""
|
||||
ensure_dir(f'{DIR_USER_DATA}/')
|
||||
file_name = f'{DIR_USER_DATA}/{settings.software_operation}_logs'
|
||||
temp_name = f'{DIR_USER_DATA}/{settings.software_operation}_logs_temp'
|
||||
|
||||
if not os.path.isfile(file_name):
|
||||
raise FunctionReturn(f"Error: Could not find '{file_name}'.")
|
||||
|
||||
if os.path.isfile(temp_name):
|
||||
os.remove(temp_name)
|
||||
|
||||
f_old = open(file_name, 'rb')
|
||||
f_new = open(temp_name, 'ab+')
|
||||
|
||||
def read_entry():
|
||||
"""Read log entry."""
|
||||
return f_old.read(1325)
|
||||
|
||||
for ct_old in iter(read_entry, b''):
|
||||
pt_new = auth_and_decrypt(ct_old, key=previous_key)
|
||||
f_new.write(encrypt_and_sign(pt_new, key=new_key))
|
||||
|
||||
f_old.close()
|
||||
f_new.close()
|
||||
|
||||
os.remove(file_name)
|
||||
os.rename(temp_name, file_name)
|
||||
|
||||
|
||||
def parse_ts_bytes(ts_bytes: bytes, settings: 'Settings') -> str:
|
||||
"""Convert bytes to timestamp string."""
|
||||
ts = struct.unpack('<L', ts_bytes)[0]
|
||||
return datetime.datetime.fromtimestamp(ts).strftime(settings.format_of_logfiles)
|
|
@ -0,0 +1,121 @@
|
|||
#!/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 time
|
||||
|
||||
from src.common.crypto import argon2_kdf, hash_chain, keygen
|
||||
from src.common.encoding import bytes_to_int, int_to_bytes
|
||||
from src.common.errors import graceful_exit
|
||||
from src.common.input import pwd_prompt
|
||||
from src.common.misc import clear_screen, ensure_dir
|
||||
from src.common.output import c_print, phase, print_on_previous_line
|
||||
from src.common.statics import *
|
||||
|
||||
|
||||
class MasterKey(object):
|
||||
"""MasterKey object manages the 32-byte master key and related methods."""
|
||||
|
||||
def __init__(self, operation: str, local_test: bool) -> None:
|
||||
"""Create a new MasterKey object."""
|
||||
self.master_key = None # type: bytes
|
||||
self.file_name = f'{DIR_USER_DATA}/{operation}_login_data'
|
||||
self.local_test = local_test
|
||||
|
||||
try:
|
||||
if os.path.isfile(self.file_name):
|
||||
self.load_master_key()
|
||||
else:
|
||||
self.new_master_key()
|
||||
except KeyboardInterrupt:
|
||||
graceful_exit()
|
||||
|
||||
def new_master_key(self) -> None:
|
||||
"""Create a new master key from salt and password.
|
||||
|
||||
The number of rounds starts at 1 but is increased dynamically based
|
||||
on system performance. This allows more security on faster platforms
|
||||
without additional cost on key derivation time.
|
||||
"""
|
||||
password = MasterKey.new_password()
|
||||
salt = keygen()
|
||||
rounds = 1
|
||||
|
||||
phase("Deriving master key", head=2)
|
||||
while True:
|
||||
time_start = time.monotonic()
|
||||
master_key, memory = argon2_kdf(password, salt, rounds, local_testing=self.local_test)
|
||||
time_final = time.monotonic() - time_start
|
||||
|
||||
if time_final > 3.0:
|
||||
self.master_key = master_key
|
||||
master_key_hash = hash_chain(master_key)
|
||||
ensure_dir(f'{DIR_USER_DATA}/')
|
||||
with open(self.file_name, 'wb+') as f:
|
||||
f.write(salt
|
||||
+ master_key_hash
|
||||
+ int_to_bytes(rounds)
|
||||
+ int_to_bytes(memory))
|
||||
phase('Done')
|
||||
break
|
||||
else:
|
||||
rounds *= 2
|
||||
|
||||
def load_master_key(self) -> None:
|
||||
"""Derive master key from password from stored values (salt, rounds, memory)."""
|
||||
ensure_dir(f'{DIR_USER_DATA}/')
|
||||
with open(self.file_name, 'rb') as f:
|
||||
data = f.read()
|
||||
salt = data[0:32]
|
||||
k_hash = data[32:64]
|
||||
rounds = bytes_to_int(data[64:72])
|
||||
memory = bytes_to_int(data[72:80])
|
||||
|
||||
while True:
|
||||
password = MasterKey.get_password()
|
||||
phase("Deriving master key", head=2, offset=16)
|
||||
purp_key, _ = argon2_kdf(password, salt, rounds, memory, local_testing=self.local_test)
|
||||
if hash_chain(purp_key) == k_hash:
|
||||
phase("Password correct", done=True)
|
||||
self.master_key = purp_key
|
||||
clear_screen(delay=0.5)
|
||||
break
|
||||
else:
|
||||
phase("Invalid password", done=True)
|
||||
print_on_previous_line(reps=5, delay=1)
|
||||
|
||||
@classmethod
|
||||
def get_password(cls, purpose: str = "master password") -> str:
|
||||
"""Load password from user."""
|
||||
return pwd_prompt(f"Enter {purpose}: ", '┌', '┐')
|
||||
|
||||
@classmethod
|
||||
def new_password(cls, purpose: str = "master password") -> str:
|
||||
"""Prompt user to enter and confirm a new password."""
|
||||
password_1 = pwd_prompt(f"Enter a new {purpose}: ", '┌', '┐')
|
||||
password_2 = pwd_prompt(f"Confirm the {purpose}: ", '├', '┤')
|
||||
|
||||
if password_1 == password_2:
|
||||
return password_1
|
||||
else:
|
||||
c_print("Error: Passwords did not match. Try again.", head=1, tail=1)
|
||||
time.sleep(1)
|
||||
print_on_previous_line(reps=7)
|
||||
return cls.new_password(purpose)
|
|
@ -0,0 +1,322 @@
|
|||
#!/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 serial
|
||||
import struct
|
||||
import textwrap
|
||||
import typing
|
||||
|
||||
from typing import Union
|
||||
|
||||
from src.common.crypto import auth_and_decrypt, encrypt_and_sign
|
||||
from src.common.encoding import bool_to_bytes, double_to_bytes, int_to_bytes, str_to_bytes
|
||||
from src.common.encoding import bytes_to_bool, bytes_to_double, bytes_to_int, bytes_to_str
|
||||
from src.common.errors import CriticalError, FunctionReturn
|
||||
from src.common.misc import clear_screen, ensure_dir, get_tty_w, round_up
|
||||
from src.common.input import yes
|
||||
from src.common.output import c_print
|
||||
from src.common.statics import *
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.common.db_contacts import ContactList
|
||||
from src.common.db_groups import GroupList
|
||||
from src.common.db_masterkey import MasterKey
|
||||
|
||||
|
||||
class Settings(object):
|
||||
"""Settings object stores all user adjustable settings under an encrypted database."""
|
||||
|
||||
def __init__(self,
|
||||
master_key: 'MasterKey',
|
||||
operation: str,
|
||||
local_test: bool,
|
||||
dd_sockets: bool) -> None:
|
||||
"""Create a new settings object.
|
||||
|
||||
The settings below are altered from within the program itself.
|
||||
Changes made to settings are stored inside an encrypted database.
|
||||
|
||||
:param master_key: Settings database encryption key
|
||||
:param operation: Operation mode of the program (tx or rx)
|
||||
:param local_test: Setting value passed from command line argument
|
||||
:param dd_sockets: Setting value passed from command line argument
|
||||
"""
|
||||
# WARNING
|
||||
# THESE ARE DEFAULT VALUES FOR
|
||||
# SETTINGS. DO NOT EDIT THEM.
|
||||
# USE THE '/set' COMMAND INSTEAD.
|
||||
|
||||
# Common settings
|
||||
self.format_of_logfiles = '%Y-%m-%d %H:%M:%S' # Timestamp format of logged messages
|
||||
self.disable_gui_dialog = False # True replaces Tkinter dialogs with CLI prompts
|
||||
self.m_members_in_group = 20 # Max members in group (Rx.py must have same value)
|
||||
self.m_number_of_groups = 20 # Max number of groups (Rx.py must have same value)
|
||||
self.m_number_of_accnts = 20 # Max number of accounts (Rx.py must have same val)
|
||||
self.serial_iface_speed = 19200 # The speed of serial interface in bauds per sec
|
||||
self.e_correction_ratio = 5 # N/o byte errors serial datagrams can recover from
|
||||
self.log_msg_by_default = False # Default logging setting for new contacts
|
||||
self.store_file_default = False # True accepts files from new contacts by default
|
||||
self.n_m_notify_privacy = False # Default privacy notification setting for new contacts
|
||||
self.log_dummy_file_a_p = True # False disables storage of placeholder data for files
|
||||
|
||||
# Transmitter settings
|
||||
self.txm_serial_adapter = True # False searches for integrated serial interface
|
||||
self.nh_bypass_messages = True # False removes interrupting NH bypass messages
|
||||
self.confirm_sent_files = True # False sends files without asking for confirmation
|
||||
self.double_space_exits = False # True exits with doubles space, False clears screen
|
||||
self.trickle_connection = False # True enables trickle connection to hide metadata
|
||||
self.trickle_stat_delay = 2.0 # Static delay between trickle packets
|
||||
self.trickle_rand_delay = 2.0 # Max random delay for timing obfuscation
|
||||
self.long_packet_rand_d = False # True adds spam guard evading delay
|
||||
self.max_val_for_rand_d = 10.0 # Spam guard evasion max delay
|
||||
|
||||
# Receiver settings
|
||||
self.rxm_serial_adapter = True # False searches for integrated serial interface
|
||||
self.new_msg_notify_dur = 1.0 # Number of seconds new msg notification appears
|
||||
|
||||
self.master_key = master_key
|
||||
self.software_operation = operation
|
||||
self.local_testing_mode = local_test
|
||||
self.data_diode_sockets = dd_sockets
|
||||
|
||||
self.file_name = f'{DIR_USER_DATA}/{operation}_settings'
|
||||
index_of_last_attr = list(self.__dict__.keys()).index('new_msg_notify_dur') + 1 # Include last index in slice
|
||||
self.key_list = list(self.__dict__.keys())[0:index_of_last_attr]
|
||||
self.defaults = {k: self.__dict__[k] for k in list(self.__dict__.keys())[:index_of_last_attr]}
|
||||
|
||||
if os.path.isfile(self.file_name):
|
||||
self.load_settings()
|
||||
# TxM is unable to send serial interface type changing command
|
||||
# if RxM looks for the type of adapter user doesn't have available.
|
||||
if operation == 'rx':
|
||||
self.setup()
|
||||
self.store_settings()
|
||||
else:
|
||||
self.setup()
|
||||
self.store_settings()
|
||||
|
||||
# Following settings change only when program is restarted on TxM/RxM/NH
|
||||
self.session_ec_ratio = self.e_correction_ratio
|
||||
self.session_if_speed = self.serial_iface_speed
|
||||
self.session_trickle = self.trickle_connection
|
||||
self.session_usb_iface = self.rxm_serial_adapter if operation == 'rx' else self.txm_serial_adapter
|
||||
|
||||
def load_settings(self) -> None:
|
||||
"""Load settings from encrypted database."""
|
||||
ensure_dir(f'{DIR_USER_DATA}/')
|
||||
with open(self.file_name, 'rb') as f:
|
||||
ct_bytes = f.read()
|
||||
|
||||
pt_bytes = auth_and_decrypt(ct_bytes, self.master_key.master_key)
|
||||
|
||||
# Update settings based on plaintext byte string content
|
||||
for i, key in enumerate(self.key_list):
|
||||
|
||||
attribute = self.__getattribute__(key)
|
||||
|
||||
if isinstance(attribute, bool):
|
||||
value = bytes_to_bool(pt_bytes[0]) # type: Union[bool, int, float, str]
|
||||
pt_bytes = pt_bytes[1:]
|
||||
|
||||
elif isinstance(attribute, int):
|
||||
value = bytes_to_int(pt_bytes[:8])
|
||||
pt_bytes = pt_bytes[8:]
|
||||
|
||||
elif isinstance(attribute, float):
|
||||
value = bytes_to_double(pt_bytes[:8])
|
||||
pt_bytes = pt_bytes[8:]
|
||||
|
||||
elif isinstance(attribute, str):
|
||||
value = bytes_to_str(pt_bytes[:1024])
|
||||
pt_bytes = pt_bytes[1024:] # 255 * 4 = 1020. The four additional bytes is the UTF-32 BOM.
|
||||
|
||||
else:
|
||||
raise CriticalError("Invalid data type in settings default values.")
|
||||
|
||||
setattr(self, key, value)
|
||||
|
||||
def store_settings(self) -> None:
|
||||
"""Store settings to encrypted database."""
|
||||
attribute_list = [self.__getattribute__(k) for k in self.key_list]
|
||||
|
||||
# Convert attributes into constant length byte string
|
||||
pt_bytes = b''
|
||||
for a in attribute_list:
|
||||
if isinstance(a, bool): pt_bytes += bool_to_bytes(a)
|
||||
elif isinstance(a, int): pt_bytes += int_to_bytes(a)
|
||||
elif isinstance(a, float): pt_bytes += double_to_bytes(a)
|
||||
elif isinstance(a, str): pt_bytes += str_to_bytes(a)
|
||||
else: raise CriticalError("Invalid attribute type in settings.")
|
||||
|
||||
ct_bytes = encrypt_and_sign(pt_bytes, self.master_key.master_key)
|
||||
|
||||
ensure_dir(f'{DIR_USER_DATA}/')
|
||||
with open(self.file_name, 'wb+') as f:
|
||||
f.write(ct_bytes)
|
||||
|
||||
def change_setting(self,
|
||||
key: str,
|
||||
value: str,
|
||||
contact_list: 'ContactList',
|
||||
group_list: 'GroupList') -> None:
|
||||
"""Parse, update and store new setting value."""
|
||||
attribute = self.__getattribute__(key)
|
||||
|
||||
if isinstance(attribute, bool):
|
||||
value_ = value
|
||||
value = value.lower().capitalize()
|
||||
if value not in ['True', 'False']:
|
||||
raise FunctionReturn(f"Invalid value {value_}.")
|
||||
|
||||
elif isinstance(attribute, int):
|
||||
if not value.isdigit() or eval(value) < 0 or eval(value) > 7378697629483820640:
|
||||
raise FunctionReturn(f"Invalid value {value}.")
|
||||
|
||||
elif isinstance(attribute, float):
|
||||
if not isinstance(eval(value), float) or eval(value) < 0.0:
|
||||
raise FunctionReturn(f"Invalid value {value}.")
|
||||
try:
|
||||
double_to_bytes(eval(value))
|
||||
except struct.error:
|
||||
raise FunctionReturn(f"Invalid value {value}.")
|
||||
|
||||
elif isinstance(attribute, str):
|
||||
if len(value) > 255:
|
||||
raise FunctionReturn(f"Setting must be shorter than 256 chars.")
|
||||
|
||||
else:
|
||||
raise CriticalError("Invalid attribute type in settings.")
|
||||
|
||||
self.validate_key_value_pair(key, value, contact_list, group_list)
|
||||
|
||||
value = value if isinstance(attribute, str) else eval(value)
|
||||
setattr(self, key, value)
|
||||
self.store_settings()
|
||||
|
||||
@staticmethod
|
||||
def validate_key_value_pair(key: str,
|
||||
value: str,
|
||||
contact_list: 'ContactList',
|
||||
group_list: 'GroupList') -> None:
|
||||
"""Check values of some settings in closer detail."""
|
||||
if key in ['m_members_in_group', 'm_number_of_groups', 'm_number_of_accnts']:
|
||||
if eval(value) % 10 != 0:
|
||||
raise FunctionReturn("Database padding settings must be divisible by 10.")
|
||||
|
||||
if key == 'm_members_in_group':
|
||||
min_size = round_up(group_list.largest_group())
|
||||
if eval(value) < min_size:
|
||||
raise FunctionReturn(f"Can't set max number of members lower than {min_size}.")
|
||||
|
||||
if key == 'm_number_of_groups':
|
||||
min_size = round_up(len(group_list))
|
||||
if eval(value) < min_size:
|
||||
raise FunctionReturn(f"Can't set max number of groups lower than {min_size}.")
|
||||
|
||||
if key == 'm_number_of_accnts':
|
||||
min_size = round_up(len(contact_list))
|
||||
if eval(value) < min_size:
|
||||
raise FunctionReturn(f"Can't set max number of contacts lower than {min_size}.")
|
||||
|
||||
if key == 'serial_iface_speed':
|
||||
if eval(value) not in serial.Serial().BAUDRATES:
|
||||
raise FunctionReturn("Specified baud rate is not supported.")
|
||||
c_print("Baud rate will change on restart.", head=1, tail=1)
|
||||
|
||||
if key == 'e_correction_ratio':
|
||||
if not value.isdigit() or eval(value) < 1:
|
||||
raise FunctionReturn("Invalid value for error correction ratio.")
|
||||
c_print("Error correction ratio will change on restart.", head=1, tail=1)
|
||||
|
||||
if key in ['rxm_serial_adapter', 'txm_serial_adapter']:
|
||||
c_print("Interface will change on restart.", head=1, tail=1)
|
||||
|
||||
if key in ['trickle_connection', 'trickle_stat_delay', 'trickle_rand_delay']:
|
||||
c_print("Trickle setting will change on restart.", head=1, tail=1)
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Prompt user to enter initial settings."""
|
||||
clear_screen()
|
||||
if not self.local_testing_mode:
|
||||
if self.software_operation == 'tx':
|
||||
self.txm_serial_adapter = yes("Does TxM use USB-to-serial/TTL adapter?", head=1, tail=1)
|
||||
else:
|
||||
self.rxm_serial_adapter = yes("Does RxM use USB-to-serial/TTL adapter?", head=1, tail=1)
|
||||
|
||||
def print_settings(self) -> None:
|
||||
"""Print list of settings, their current and default values and setting descriptions."""
|
||||
|
||||
# Common
|
||||
desc_d = {
|
||||
"format_of_logfiles": "Timestamp format of logged messages",
|
||||
"disable_gui_dialog": "True replaces Tkinter dialogs with CLI prompts",
|
||||
|
||||
"m_members_in_group": "Max members in group (Must be same on TxM/RxM)",
|
||||
"m_number_of_groups": "Max number of groups (Must be same on TxM/RxM)",
|
||||
"m_number_of_accnts": "Max number of accounts (Must be same on TxM/RxM)",
|
||||
|
||||
"serial_iface_speed": "The speed of serial interface in bauds per sec",
|
||||
"e_correction_ratio": "N/o byte errors serial datagrams can recover from",
|
||||
|
||||
"log_msg_by_default": "Default logging setting for new contacts",
|
||||
"store_file_default": "True accepts files from new contacts by default",
|
||||
"n_m_notify_privacy": "Default message notification setting for new contacts",
|
||||
"log_dummy_file_a_p": "False disables storage of placeholder data for files",
|
||||
|
||||
# TxM
|
||||
"txm_serial_adapter": "False uses system's integrated serial interface",
|
||||
"nh_bypass_messages": "False removes NH bypass interrupt messages",
|
||||
"confirm_sent_files": "False sends files without asking for confirmation",
|
||||
"double_space_exits": "True exits with doubles space, else clears screen",
|
||||
|
||||
"trickle_connection": "True enables trickle connection to hide metadata",
|
||||
"trickle_stat_delay": "Static delay between trickle packets",
|
||||
"trickle_rand_delay": "Max random delay for timing obfuscation",
|
||||
|
||||
"long_packet_rand_d": "True adds spam guard evading delay",
|
||||
"max_val_for_rand_d": "Maximum time for random spam guard evasion delay",
|
||||
|
||||
# RxM
|
||||
"rxm_serial_adapter": "False uses system's integrated serial interface",
|
||||
"new_msg_notify_dur": "Number of seconds new msg notification appears"}
|
||||
|
||||
clear_screen()
|
||||
tty_w = get_tty_w()
|
||||
|
||||
print("Setting name Current value Default value Description")
|
||||
print(tty_w * '-')
|
||||
|
||||
for key in self.defaults:
|
||||
def_value = str(self.defaults[key]).ljust(len('%Y-%m-%d %H:%M:%S'))
|
||||
description = desc_d[key]
|
||||
wrapper = textwrap.TextWrapper(width=max(1, (tty_w - 59)))
|
||||
desc_lines = wrapper.fill(description).split('\n')
|
||||
current_value = str(self.__getattribute__(key)).ljust(17)
|
||||
|
||||
print(f"{key} {current_value} {def_value} {desc_lines[0]}")
|
||||
|
||||
# Print wrapped description lines with indent
|
||||
if len(desc_lines) > 1:
|
||||
for line in desc_lines[1:]:
|
||||
print(58 * ' ' + line)
|
||||
print('')
|
||||
|
||||
print('\n')
|
|
@ -0,0 +1,131 @@
|
|||
#!/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 struct
|
||||
|
||||
from typing import List, Union
|
||||
|
||||
from src.common.crypto import hash_chain, rm_padding_str, unicode_padding
|
||||
|
||||
|
||||
def b58encode(byte_string: bytes) -> str:
|
||||
"""Encode byte string to checksummed Base58 string.
|
||||
|
||||
This format is very similar to Bitcoin's Wallet Import Format,
|
||||
however the SHA256(SHA256(key)) checksum has been replaced with
|
||||
TFC's hash chain (truncated to 4 bytes).
|
||||
"""
|
||||
b58_alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
||||
|
||||
byte_string += hash_chain(byte_string)[:4]
|
||||
|
||||
orig_len = len(byte_string)
|
||||
byte_string = byte_string.lstrip(b'\x00')
|
||||
new_len = len(byte_string)
|
||||
|
||||
p, acc = 1, 0
|
||||
for byte in bytearray(byte_string[::-1]):
|
||||
acc += p * byte
|
||||
p *= 256
|
||||
|
||||
encoded = ''
|
||||
while acc > 0:
|
||||
acc, mod = divmod(acc, 58)
|
||||
encoded += b58_alphabet[mod]
|
||||
|
||||
return (encoded + (orig_len - new_len) * '1')[::-1]
|
||||
|
||||
|
||||
def b58decode(string: str) -> bytes:
|
||||
"""Decode a Base58-encoded string and verify checksum."""
|
||||
b58_alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
||||
|
||||
orig_len = len(string)
|
||||
string = string.lstrip('1')
|
||||
new_len = len(string)
|
||||
|
||||
p, acc = 1, 0
|
||||
for c in string[::-1]:
|
||||
acc += p * b58_alphabet.index(c)
|
||||
p *= 58
|
||||
|
||||
decoded = []
|
||||
while acc > 0:
|
||||
acc, mod = divmod(acc, 256)
|
||||
decoded.append(mod)
|
||||
|
||||
decoded_ = (bytes(decoded) + (orig_len - new_len) * b'\x00')[::-1] # type: Union[bytes, List[int]]
|
||||
|
||||
if hash_chain(bytes(decoded_[:-4]))[:4] != decoded_[-4:]:
|
||||
raise ValueError
|
||||
|
||||
return bytes(decoded_[:-4])
|
||||
|
||||
|
||||
# Database constant length encoding
|
||||
|
||||
def bool_to_bytes(boolean: bool) -> bytes:
|
||||
"""Convert boolean value to 1-byte byte string."""
|
||||
return bytes([boolean])
|
||||
|
||||
|
||||
def int_to_bytes(integer: int) -> bytes:
|
||||
"""Convert integer to 8-byte byte string."""
|
||||
return struct.pack('!Q', integer)
|
||||
|
||||
|
||||
def double_to_bytes(double_: float) -> bytes:
|
||||
"""Convert double to 8-byte byte string."""
|
||||
return struct.pack('d', double_)
|
||||
|
||||
|
||||
def str_to_bytes(string: str) -> bytes:
|
||||
"""Pad string with unicode chars and encode it with UTF-32.
|
||||
|
||||
Length of padded string is 255 * 4 + 4 (BOM) = 1024 bytes.
|
||||
"""
|
||||
return unicode_padding(string).encode('utf-32')
|
||||
|
||||
|
||||
# Decoding
|
||||
|
||||
def bytes_to_bool(byte_string: Union[bytes, int]) -> bool:
|
||||
"""Convert 1-byte byte string to boolean value."""
|
||||
if isinstance(byte_string, bytes):
|
||||
byte_string = byte_string[0]
|
||||
return bool(byte_string)
|
||||
|
||||
|
||||
def bytes_to_int(byte_string: bytes) -> int:
|
||||
"""Convert 8-byte byte string to integer."""
|
||||
return struct.unpack('!Q', byte_string)[0]
|
||||
|
||||
|
||||
def bytes_to_double(byte_string: bytes) -> float:
|
||||
"""Convert 8-byte byte string to double."""
|
||||
return struct.unpack('d', byte_string)[0]
|
||||
|
||||
|
||||
def bytes_to_str(byte_string: bytes) -> str:
|
||||
"""Convert 1024-byte bytestring to unicode string.
|
||||
|
||||
Decode bytestring with UTF-32 and remove unicode padding.
|
||||
"""
|
||||
return rm_padding_str(byte_string.decode('utf-32'))
|
|
@ -0,0 +1,63 @@
|
|||
#!/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 datetime
|
||||
import inspect
|
||||
import sys
|
||||
import time
|
||||
|
||||
from typing import Any
|
||||
|
||||
from src.common.misc import clear_screen
|
||||
from src.common.output import c_print
|
||||
from src.common.statics import *
|
||||
|
||||
|
||||
class CriticalError(Exception):
|
||||
"""A variety of errors during which Tx.py should gracefully exit."""
|
||||
|
||||
def __init__(self, error_message: str) -> None:
|
||||
graceful_exit("Critical error in function '{}':\n{}"
|
||||
.format(inspect.stack()[1][3], error_message), clear=False)
|
||||
|
||||
|
||||
class FunctionReturn(Exception):
|
||||
"""Print return message and return to exception handler function."""
|
||||
|
||||
def __init__(self, return_msg: str, output: bool = True, delay: float = 0, window: Any = None) -> None:
|
||||
self.message = return_msg
|
||||
|
||||
if window is None:
|
||||
if output:
|
||||
clear_screen()
|
||||
c_print(self.message, head=1, tail=1)
|
||||
time.sleep(delay)
|
||||
else:
|
||||
window.print_new(datetime.datetime.now(), return_msg)
|
||||
|
||||
|
||||
def graceful_exit(message='', clear=True):
|
||||
"""Display a message and exit Tx.py."""
|
||||
if clear:
|
||||
sys.stdout.write(CLEAR_ENTIRE_SCREEN + CURSOR_LEFT_UP_CORNER)
|
||||
if message:
|
||||
print("\n" + message)
|
||||
print("\nExiting TFC.\n")
|
||||
exit()
|
|
@ -0,0 +1,178 @@
|
|||
#!/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 multiprocessing.connection
|
||||
import os.path
|
||||
import platform
|
||||
import serial
|
||||
import socket
|
||||
import time
|
||||
import typing
|
||||
|
||||
from serial.serialutil import SerialException
|
||||
from typing import Any, Union
|
||||
|
||||
from src.common.errors import CriticalError, graceful_exit
|
||||
from src.common.output import phase, print_on_previous_line
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from multiprocessing import Queue
|
||||
from src.common.db_settings import Settings
|
||||
|
||||
|
||||
def gw_incoming(gateway: 'Gateway', q_to_tip: 'Queue'):
|
||||
"""Process that loads data from TxM side gateway."""
|
||||
while True:
|
||||
q_to_tip.put(gateway.read())
|
||||
|
||||
|
||||
class Gateway(object):
|
||||
"""Gateway object is a wrapper that provides interconnection between NH and TxM/RxM."""
|
||||
|
||||
def __init__(self, settings: 'Settings') -> None:
|
||||
"""Create a new gateway object."""
|
||||
self.settings = settings
|
||||
self.interface = None # type: Union[Any]
|
||||
|
||||
# Set True when serial adapter is initially found so that further
|
||||
# serial interface searches know to announce disconnection.
|
||||
self.init_found = False
|
||||
bauds_per_byte = 10
|
||||
bytes_per_s = self.settings.serial_iface_speed / bauds_per_byte
|
||||
byte_travel_t = 1 / bytes_per_s
|
||||
self.timeout = max(2 * byte_travel_t, 0.01)
|
||||
self.delay = 2 * self.timeout
|
||||
|
||||
if self.settings.local_testing_mode:
|
||||
if self.settings.software_operation == 'tx':
|
||||
self.client_establish_socket()
|
||||
else:
|
||||
self.server_establish_socket()
|
||||
else:
|
||||
self.establish_serial()
|
||||
|
||||
def write(self, packet: bytes) -> None:
|
||||
"""Output data via socket/serial interface."""
|
||||
if self.settings.local_testing_mode:
|
||||
self.interface.send(packet)
|
||||
else:
|
||||
try:
|
||||
self.interface.write(packet)
|
||||
self.interface.flush()
|
||||
time.sleep(self.delay)
|
||||
except SerialException:
|
||||
self.establish_serial()
|
||||
self.write(packet)
|
||||
|
||||
def read(self) -> bytes:
|
||||
"""Read data via socket/serial interface."""
|
||||
if self.settings.local_testing_mode:
|
||||
while True:
|
||||
try:
|
||||
return self.interface.recv()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except EOFError:
|
||||
graceful_exit("IPC client disconnected.")
|
||||
else:
|
||||
while True:
|
||||
try:
|
||||
start_time = 0.0
|
||||
read_buffer = bytearray()
|
||||
while True:
|
||||
read = self.interface.read(1000)
|
||||
if read:
|
||||
start_time = time.monotonic()
|
||||
read_buffer.extend(read)
|
||||
else:
|
||||
if read_buffer:
|
||||
delta = time.monotonic() - start_time
|
||||
if delta > self.timeout:
|
||||
return bytes(read_buffer)
|
||||
else:
|
||||
time.sleep(0.001)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except SerialException:
|
||||
self.establish_serial()
|
||||
self.read()
|
||||
|
||||
def server_establish_socket(self) -> None:
|
||||
"""Establish IPC server."""
|
||||
listener = multiprocessing.connection.Listener(('localhost', 5003))
|
||||
self.interface = listener.accept()
|
||||
|
||||
def client_establish_socket(self) -> None:
|
||||
"""Establish IPC client."""
|
||||
try:
|
||||
phase("Waiting for connection to NH", offset=11)
|
||||
while True:
|
||||
try:
|
||||
socket_number = 5000 if self.settings.data_diode_sockets else 5001
|
||||
self.interface = multiprocessing.connection.Client(('localhost', socket_number))
|
||||
phase("Established", done=True)
|
||||
break
|
||||
except socket.error:
|
||||
time.sleep(0.1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
graceful_exit()
|
||||
|
||||
def establish_serial(self):
|
||||
"""Create new serial interface object."""
|
||||
try:
|
||||
serial_nh = self.search_serial_interface()
|
||||
self.interface = serial.Serial(serial_nh, self.settings.session_if_speed, timeout=0)
|
||||
except SerialException:
|
||||
graceful_exit("SerialException. Ensure $USER is in dialout group.")
|
||||
|
||||
def search_serial_interface(self) -> str:
|
||||
"""Search for serial interface."""
|
||||
if self.settings.session_usb_iface:
|
||||
search_announced = False
|
||||
|
||||
if not self.init_found:
|
||||
print_on_previous_line()
|
||||
phase("Searching for USB-to-serial interface")
|
||||
|
||||
while True:
|
||||
time.sleep(0.1)
|
||||
for f in sorted(os.listdir('/dev')):
|
||||
if f.startswith('ttyUSB'):
|
||||
if self.init_found:
|
||||
time.sleep(1.5)
|
||||
phase('Found', done=True)
|
||||
if self.init_found:
|
||||
print_on_previous_line(reps=2)
|
||||
self.init_found = True
|
||||
return '/dev/{}'.format(f)
|
||||
else:
|
||||
if not search_announced:
|
||||
if self.init_found:
|
||||
phase("Serial adapter disconnected. Waiting for interface", head=1)
|
||||
search_announced = True
|
||||
|
||||
else:
|
||||
f = 'serial0' if 'Raspbian' in platform.platform() else 'ttyS0'
|
||||
if f in sorted(os.listdir('/dev/')):
|
||||
return '/dev/{}'.format(f)
|
||||
else:
|
||||
raise CriticalError("Error: /dev/{} was not found.".format(f))
|
|
@ -0,0 +1,211 @@
|
|||
#!/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 getpass
|
||||
import typing
|
||||
|
||||
from typing import Any, Callable
|
||||
|
||||
from src.common.encoding import b58decode
|
||||
from src.common.errors import CriticalError
|
||||
from src.common.misc import clear_screen, get_tty_w
|
||||
from src.common.output import box_print, c_print, message_printer, print_on_previous_line
|
||||
from src.common.statics import *
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.common.db_settings import Settings
|
||||
|
||||
|
||||
def box_input(title: str,
|
||||
default: str = '',
|
||||
head: int = 0,
|
||||
tail: int = 0,
|
||||
expected_len: int = 0,
|
||||
validator: Callable = None,
|
||||
validator_args: Any = None) -> str:
|
||||
"""Display boxed prompt for user with title.
|
||||
|
||||
:param title: Title for data to prompt
|
||||
:param default: Default return value
|
||||
:param head: Number of new lines to print before input
|
||||
:param tail: Number of new lines to print after input
|
||||
:param expected_len Expected length of input
|
||||
:param validator: Input validator function
|
||||
:param validator_args: Arguments required by the validator
|
||||
:return: Input from user
|
||||
"""
|
||||
for _ in range(head):
|
||||
print('')
|
||||
|
||||
tty_w = get_tty_w()
|
||||
input_len = tty_w - 2 if expected_len == 0 else expected_len + 2
|
||||
|
||||
input_top_line = '┌' + input_len * '─' + '┐'
|
||||
input_line = '│' + input_len * ' ' + '│'
|
||||
input_bot_line = '└' + input_len * '─' + '┘'
|
||||
|
||||
input_line_indent = (tty_w - len(input_line)) // 2
|
||||
input_box_indent = input_line_indent * ' '
|
||||
|
||||
print(input_box_indent + input_top_line)
|
||||
print(input_box_indent + input_line)
|
||||
print(input_box_indent + input_bot_line)
|
||||
print(4 * CURSOR_UP_ONE_LINE)
|
||||
print(input_box_indent + '┌─┤' + title + '├')
|
||||
|
||||
user_input = input(input_box_indent + '│ ')
|
||||
|
||||
if user_input == '':
|
||||
print(2 * CURSOR_UP_ONE_LINE)
|
||||
print(input_box_indent + f'│ {default}')
|
||||
user_input = default
|
||||
|
||||
if validator is not None:
|
||||
success, error_msg = validator(user_input, validator_args)
|
||||
if not success:
|
||||
c_print("Error: {}".format(error_msg), head=1)
|
||||
print_on_previous_line(reps=4, delay=1.5)
|
||||
return box_input(title, default, head, tail, expected_len, validator, validator_args)
|
||||
|
||||
for _ in range(tail):
|
||||
print('')
|
||||
|
||||
return user_input
|
||||
|
||||
|
||||
def get_b58_key(k_type: str) -> bytes:
|
||||
"""Ask user to input Base58 encoded public key from RxM."""
|
||||
if k_type == 'pubkey':
|
||||
clear_screen()
|
||||
c_print("Import public key from RxM", head=1, tail=1)
|
||||
c_print("WARNING")
|
||||
message_printer("Key exchange will break the HW separation. "
|
||||
"Outside specific requests TxM (this computer) "
|
||||
"makes, you must never copy any data from "
|
||||
"NH/RxM to TxM. Doing so could infect TxM, that "
|
||||
"could then later covertly transmit private "
|
||||
"keys/messages to adversary on NH.", head=1, tail=1)
|
||||
box_msg = "Enter contact's public key from RxM"
|
||||
elif k_type == 'localkey':
|
||||
box_msg = "Enter local key decryption key from TxM"
|
||||
elif k_type == 'imported_file':
|
||||
box_msg = "Enter file decryption key"
|
||||
else:
|
||||
raise CriticalError("Invalid key type")
|
||||
|
||||
while True:
|
||||
pub_key = box_input(box_msg, expected_len=59)
|
||||
pub_key = ''.join(pub_key.split())
|
||||
|
||||
try:
|
||||
return b58decode(pub_key)
|
||||
except ValueError:
|
||||
c_print("Checksum error - Check that entered key is correct.", head=1)
|
||||
print_on_previous_line(reps=4, delay=1.5)
|
||||
|
||||
|
||||
def nh_bypass_msg(key: str, settings: 'Settings') -> None:
|
||||
"""Print messages about bypassing NH."""
|
||||
m = dict(start ="Bypass NH if needed. Press <Enter> to send local key.",
|
||||
finish="Remove bypass of NH. Press <Enter> to continue.")
|
||||
|
||||
if settings.nh_bypass_messages:
|
||||
box_print(m[key], manual_proceed=True)
|
||||
|
||||
|
||||
def pwd_prompt(message: str, lc: str, rc: str) -> str:
|
||||
"""Prompt user to enter a password.
|
||||
|
||||
:param message: Prompt message
|
||||
:param lc: Upper-left corner box character
|
||||
:param rc: Upper-right corner box character
|
||||
:return: Password from user
|
||||
"""
|
||||
upper_line = (lc + (len(message) + 3) * '─' + rc)
|
||||
title_line = ('│' + message + 3 * ' ' + '│')
|
||||
lower_line = ('└' + (len(message) + 3) * '─' + '┘')
|
||||
|
||||
upper_line = upper_line.center(get_tty_w())
|
||||
title_line = title_line.center(get_tty_w())
|
||||
lower_line = lower_line.center(get_tty_w())
|
||||
|
||||
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, head: int = 0, tail: int = 0) -> bool:
|
||||
"""Prompt user a question that is answered with yes / no.
|
||||
|
||||
:param prompt: Question to be asked
|
||||
:param head: Number of new lines to print before prompt
|
||||
:param tail: Number of new lines to print after prompt
|
||||
:return: True if user types 'y' or 'yes'
|
||||
False if user types 'n' or 'no'
|
||||
"""
|
||||
for _ in range(head):
|
||||
print('')
|
||||
|
||||
prompt = "{} (y/n): ".format(prompt)
|
||||
tty_w = get_tty_w()
|
||||
upper_line = ('┌' + (len(prompt) + 5) * '─' + '┐')
|
||||
title_line = ('│' + prompt + 5 * ' ' + '│')
|
||||
lower_line = ('└' + (len(prompt) + 5) * '─' + '┘')
|
||||
|
||||
upper_line = upper_line.center(tty_w)
|
||||
title_line = title_line.center(tty_w)
|
||||
lower_line = lower_line.center(tty_w)
|
||||
|
||||
indent = title_line.find('│')
|
||||
print(upper_line)
|
||||
print(title_line)
|
||||
print(lower_line)
|
||||
print(3 * CURSOR_UP_ONE_LINE)
|
||||
|
||||
while True:
|
||||
print(title_line)
|
||||
print(lower_line)
|
||||
print(3 * CURSOR_UP_ONE_LINE)
|
||||
answer = input(indent * ' ' + f'│ {prompt}')
|
||||
print_on_previous_line()
|
||||
|
||||
if answer == '':
|
||||
continue
|
||||
|
||||
if answer.lower() in 'yes':
|
||||
print(indent * ' ' + f'│ {prompt}Yes │\n')
|
||||
for _ in range(tail):
|
||||
print('')
|
||||
return True
|
||||
|
||||
elif answer.lower() in 'no':
|
||||
print(indent * ' ' + f'│ {prompt}No │\n')
|
||||
for _ in range(tail):
|
||||
print('')
|
||||
return False
|
||||
|
||||
else:
|
||||
continue
|
|
@ -0,0 +1,244 @@
|
|||
#!/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 argparse
|
||||
import shutil
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import typing
|
||||
|
||||
from typing import Any, Callable, List, Tuple, Union
|
||||
|
||||
from src.common.statics import *
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.common.db_contacts import ContactList
|
||||
from src.common.db_groups import GroupList
|
||||
from src.common.db_settings import Settings
|
||||
|
||||
|
||||
def clear_screen(delay: float = 0.0) -> None:
|
||||
"""Clear terminal window."""
|
||||
time.sleep(delay)
|
||||
sys.stdout.write(CLEAR_ENTIRE_SCREEN + CURSOR_LEFT_UP_CORNER)
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def ensure_dir(directory: str) -> None:
|
||||
"""Ensure directory exists."""
|
||||
name = os.path.dirname(directory)
|
||||
if not os.path.exists(name):
|
||||
os.makedirs(name)
|
||||
|
||||
|
||||
def get_tab_complete_list(contact_list: 'ContactList',
|
||||
group_list: 'GroupList',
|
||||
settings: 'Settings') -> List[str]:
|
||||
"""Return a list of tab-complete words."""
|
||||
tc_list = ['about', 'add ', 'all', 'clear', 'cmd', 'create ', 'exit', 'export ',
|
||||
'false', 'file', 'fingerprints', 'group ', 'help', 'history ', 'localkey',
|
||||
'logging ', 'msg ', 'names', 'nick ', 'notify ', 'passwd ', 'psk',
|
||||
'reset', 'rm ', 'set ', 'settings', 'store ', 'true', 'unread']
|
||||
|
||||
tc_list += [(c + ' ') for c in contact_list.get_list_of_accounts()]
|
||||
tc_list += [(n + ' ') for n in contact_list.get_list_of_nicks()]
|
||||
tc_list += [(u + ' ') for u in contact_list.get_list_of_users_accounts()]
|
||||
tc_list += [(g + ' ') for g in group_list.get_list_of_group_names()]
|
||||
tc_list += [(s + ' ') for s in settings.key_list]
|
||||
|
||||
return tc_list
|
||||
|
||||
|
||||
def get_tab_completer(contact_list: 'ContactList',
|
||||
group_list: 'GroupList',
|
||||
settings: 'Settings') -> Callable:
|
||||
"""Return tab completer object."""
|
||||
|
||||
def tab_complete(text, state):
|
||||
"""Return tab_complete options."""
|
||||
tab_complete_list = get_tab_complete_list(contact_list, group_list, settings)
|
||||
options = [t for t in tab_complete_list if t.startswith(text)]
|
||||
try:
|
||||
return options[state]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
return tab_complete
|
||||
|
||||
|
||||
def get_tty_w() -> int:
|
||||
"""Return width of terminal TFC is running in."""
|
||||
return shutil.get_terminal_size()[0]
|
||||
|
||||
|
||||
def process_arguments() -> Tuple[str, bool, bool]:
|
||||
"""Define Tx.py settings from arguments passed from command line."""
|
||||
parser = argparse.ArgumentParser('python tfc.py',
|
||||
usage='%(prog)s [OPTION]',
|
||||
description='')
|
||||
|
||||
parser.add_argument('-rx',
|
||||
action='store_true',
|
||||
default=False,
|
||||
dest='operation',
|
||||
help="Run RxM side program")
|
||||
|
||||
parser.add_argument('-l',
|
||||
action='store_true',
|
||||
default=False,
|
||||
dest='local_test',
|
||||
help="Enable local testing mode")
|
||||
|
||||
parser.add_argument('-d',
|
||||
action='store_true',
|
||||
default=False,
|
||||
dest='dd_sockets',
|
||||
help="Data diode simulator socket configuration for local testing")
|
||||
|
||||
args = parser.parse_args()
|
||||
operation = 'rx' if args.operation else 'tx'
|
||||
local_test = args.local_test
|
||||
dd_sockets = args.dd_sockets
|
||||
|
||||
return operation, local_test, dd_sockets
|
||||
|
||||
|
||||
def resize_terminal(height: int, width: int) -> None:
|
||||
"""Resize Terminal to specified size.
|
||||
|
||||
:param height: Terminal height in chars
|
||||
:param width: Terminal width in chars
|
||||
:return: None
|
||||
"""
|
||||
sys.stdout.write('\x1b[8;{};{}t\n'.format(height, width))
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
def round_up(x: Union[int, float]) -> int:
|
||||
"""Round value to next 10."""
|
||||
return int(math.ceil(x / 10.0)) * 10
|
||||
|
||||
|
||||
def split_string(string: str, item_len: int) -> List[str]:
|
||||
"""Split string into list of specific length substrings.
|
||||
|
||||
:param string: String to split
|
||||
:param item_len: Length of list items
|
||||
:return: String split to list
|
||||
"""
|
||||
return [string[i:i + item_len] for i in range(0, len(string), item_len)]
|
||||
|
||||
|
||||
def split_byte_string(string: bytes, item_len: int) -> List[bytes]:
|
||||
"""Split byte string into list of specific length substrings.
|
||||
|
||||
:param string: String to split
|
||||
:param item_len: Length of list items
|
||||
:return: String split to list
|
||||
"""
|
||||
return [string[i:i + item_len] for i in range(0, len(string), item_len)]
|
||||
|
||||
|
||||
def validate_account(account: str, *_: Any) -> Tuple[bool, str]:
|
||||
"""Validate account name."""
|
||||
error_msg = ''
|
||||
|
||||
# Length limited by database's unicode padding
|
||||
if len(account) > 254:
|
||||
error_msg = "Account must be shorter than 255 chars."
|
||||
|
||||
if not re.match(ACCOUNT_FORMAT, account):
|
||||
error_msg = "Invalid account format."
|
||||
|
||||
# Avoid delimiter char collision in output packets
|
||||
if not account.isprintable():
|
||||
error_msg = "Account must be printable."
|
||||
|
||||
if error_msg:
|
||||
return False, error_msg
|
||||
|
||||
return True, ''
|
||||
|
||||
|
||||
def validate_key_exchange(key_ex: str, *_: Any) -> Tuple[bool, str]:
|
||||
"""Validate specified key exchange."""
|
||||
if key_ex.lower() in ['e', 'ecdhe']:
|
||||
return True, ''
|
||||
elif key_ex.lower() in ['p', 'psk']:
|
||||
return True, ''
|
||||
else:
|
||||
return False, "Invalid key exchange selection."
|
||||
|
||||
|
||||
def validate_nick(nick: str, args: Tuple['ContactList', 'GroupList', str]) -> Tuple[bool, str]:
|
||||
"""Validate nickname for account.
|
||||
|
||||
:param nick: Nick to validate
|
||||
:param args: Contact list and group list databases
|
||||
:return: True if nick is valid, else False
|
||||
"""
|
||||
contact_list, group_list, account = args
|
||||
|
||||
error_msg = ''
|
||||
|
||||
# Length limited by database's unicode padding
|
||||
if len(nick) > 254:
|
||||
error_msg = "Nick must be shorter than 255 chars."
|
||||
|
||||
# Avoid delimiter char collision in output packets
|
||||
if not nick.isprintable():
|
||||
error_msg = "Nick must be printable."
|
||||
|
||||
if nick == '':
|
||||
error_msg = "Nick can't be empty."
|
||||
|
||||
# RxM displays sent messages under "Me"
|
||||
if nick.lower() == 'me':
|
||||
error_msg = "'Me' is a reserved nick."
|
||||
|
||||
# RxM displays system notifications under nick '-!-'.
|
||||
if nick.lower() == '-!-':
|
||||
error_msg = "'-!-' is a reserved nick."
|
||||
|
||||
# Ensure that nicks, accounts and group names are UIDs in recipient selection
|
||||
if nick == 'local':
|
||||
error_msg = "Nick can't refer to local keyfile."
|
||||
|
||||
if re.match(ACCOUNT_FORMAT, nick):
|
||||
error_msg = "Nick can't have format of an account."
|
||||
|
||||
if nick in contact_list.get_list_of_nicks():
|
||||
error_msg = "Nick already in use."
|
||||
|
||||
# Allow if nick matches the account the key is being re-exchanged for.
|
||||
if contact_list.has_contact(account):
|
||||
if nick == contact_list.get_contact(account).nick:
|
||||
error_msg = ''
|
||||
|
||||
if nick in group_list.get_list_of_group_names():
|
||||
error_msg = "Nick can't be a group name."
|
||||
|
||||
if error_msg:
|
||||
return False, error_msg
|
||||
|
||||
return True, ''
|
|
@ -0,0 +1,225 @@
|
|||
#!/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 binascii
|
||||
import textwrap
|
||||
import time
|
||||
import typing
|
||||
import sys
|
||||
|
||||
from typing import List, Union
|
||||
|
||||
from src.common.misc import get_tty_w, split_string
|
||||
from src.common.statics import *
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.common.db_contacts import ContactList
|
||||
|
||||
|
||||
def box_print(msg_list: Union[str, list],
|
||||
manual_proceed: bool = False,
|
||||
head: int = 0,
|
||||
tail: int = 0) -> None:
|
||||
"""Print message inside a box.
|
||||
|
||||
:param msg_list: List of lines to print
|
||||
:param manual_proceed: Wait for user input before continuing
|
||||
:param head: Number of new lines to print before box
|
||||
:param tail: Number of new lines to print after box
|
||||
:return: None
|
||||
"""
|
||||
for _ in range(head):
|
||||
print('')
|
||||
|
||||
if isinstance(msg_list, str):
|
||||
msg_list = [msg_list]
|
||||
|
||||
tty_w = get_tty_w()
|
||||
widest = max(msg_list, key=len)
|
||||
|
||||
msg_list = ['{:^{}}'.format(m, len(widest)) for m in msg_list]
|
||||
|
||||
top_line = '┌' + (len(msg_list[0]) + 2) * '─' + '┐'
|
||||
bot_line = '└' + (len(msg_list[0]) + 2) * '─' + '┘'
|
||||
msg_list = ['│ {} │'.format(m) for m in msg_list]
|
||||
|
||||
top_line = top_line.center(tty_w)
|
||||
msg_list = [m.center(tty_w) for m in msg_list]
|
||||
bot_line = bot_line.center(tty_w)
|
||||
|
||||
print(top_line)
|
||||
for m in msg_list:
|
||||
print(m)
|
||||
print(bot_line)
|
||||
|
||||
for _ in range(tail):
|
||||
print('')
|
||||
|
||||
if manual_proceed:
|
||||
input('')
|
||||
print_on_previous_line()
|
||||
|
||||
|
||||
def c_print(string: str, head: int = 0, tail: int = 0) -> None:
|
||||
"""Print string to center of screen.
|
||||
|
||||
:param string: String to print
|
||||
:param head: Number of new lines to print before string
|
||||
:param tail: Number of new lines to print after string
|
||||
:return: None
|
||||
"""
|
||||
for _ in range(head):
|
||||
print('')
|
||||
|
||||
print(string.center(get_tty_w()))
|
||||
|
||||
for _ in range(tail):
|
||||
print('')
|
||||
|
||||
|
||||
def message_printer(message: str, head: int = 0, tail: int = 0) -> None:
|
||||
"""Print long message in the middle of the screen.
|
||||
|
||||
:param message: Message to print
|
||||
:param head: Number of new lines to print before message
|
||||
:param tail: Number of new lines to print after message
|
||||
:return: None
|
||||
"""
|
||||
for _ in range(head):
|
||||
print('')
|
||||
|
||||
line_list = (textwrap.fill(message, min(49, (get_tty_w() - 6))).split('\n'))
|
||||
for l in line_list:
|
||||
c_print(l)
|
||||
|
||||
for _ in range(tail):
|
||||
print('')
|
||||
|
||||
|
||||
def phase(string: str,
|
||||
done: bool = False,
|
||||
head: int = 0,
|
||||
offset: int = 2) -> None:
|
||||
"""Print name of next phase.
|
||||
|
||||
Message about completion will be printed on same line.
|
||||
|
||||
:param string: String to be printed
|
||||
:param done: Notify with custom message
|
||||
:param head: N.o. inserted new lines before print
|
||||
:param offset: Offset of message from center to left
|
||||
:return: None
|
||||
"""
|
||||
for _ in range(head):
|
||||
print('')
|
||||
|
||||
if string == 'Done' or done:
|
||||
print(string)
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
string = '{}... '.format(string)
|
||||
indent = ((get_tty_w() - (len(string) + offset)) // 2) * ' '
|
||||
|
||||
print(indent + string, end='', flush=True)
|
||||
|
||||
|
||||
def print_fingerprints(fp: bytes, msg: str = '') -> None:
|
||||
"""Print fingerprints of contact and user.
|
||||
|
||||
:param fp: Contact's fingerprint
|
||||
:param msg: Title message
|
||||
:return: None
|
||||
"""
|
||||
|
||||
def base10encode(fingerprint: bytes) -> str:
|
||||
"""Encode fingerprint to decimals for distinct communication.
|
||||
|
||||
Base64 has 75% efficiency but encoding is bad as user might
|
||||
confuse upper case I with lower case l, 0 with O etc.
|
||||
|
||||
Base58 has 73% efficiency and removes the problem of Base64
|
||||
explained above, but works only when manually typing
|
||||
strings because user has to take time to explain which
|
||||
letters were capitalized etc.
|
||||
|
||||
Base16 has 50% efficiency and removes the capitalisation problem
|
||||
with Base58 but the choice is bad as '3', 'b', 'c', 'd'
|
||||
and 'e' are hard to distinguish in English language
|
||||
(fingerprints are usually read aloud over off band call).
|
||||
|
||||
Base10 has 41% efficiency but as languages have evolved in a
|
||||
way that makes clear distinction between the way different
|
||||
numbers are pronounced: reading them is faster and less
|
||||
error prone. Compliments to OWS/WA developers for
|
||||
discovering this.
|
||||
|
||||
Truncate fingerprint for clean layout with three rows that each
|
||||
have five groups of five numbers. The resulting fingerprint has
|
||||
249.15 bits of entropy.
|
||||
"""
|
||||
hex_representation = binascii.hexlify(fingerprint)
|
||||
dec_representation = str(int(hex_representation, base=16))
|
||||
return dec_representation[:75]
|
||||
|
||||
p_lst = [msg, ''] if msg else []
|
||||
parts = split_string(base10encode(fp), item_len=25)
|
||||
p_lst += [' '.join(p[i:i + 5] for i in range(0, len(p), 5)) for p in parts]
|
||||
|
||||
box_print(p_lst)
|
||||
|
||||
|
||||
def print_on_previous_line(reps: int = 1,
|
||||
delay: float = 0.0,
|
||||
flush: bool = False) -> None:
|
||||
"""Next message will be printed on upper line.
|
||||
|
||||
:param reps: Number of times to repeat function
|
||||
:param delay: Time to sleep before clearing lines above
|
||||
:param flush: Flush stdout when true
|
||||
:return: None
|
||||
"""
|
||||
time.sleep(delay)
|
||||
|
||||
for _ in range(reps):
|
||||
sys.stdout.write(CURSOR_UP_ONE_LINE + CLEAR_ENTIRE_LINE)
|
||||
if flush:
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def g_mgmt_print(key: str,
|
||||
members: List[str],
|
||||
contact_list: 'ContactList',
|
||||
g_name: str = '') -> None:
|
||||
"""Lists members at different parts of group management."""
|
||||
m = dict(new_g="Created new group '{}' with following members:".format(g_name),
|
||||
add_m="Added following accounts to group '{}':".format(g_name),
|
||||
add_a="Following accounts were already in group '{}':".format(g_name),
|
||||
rem_m="Removed following members from group '{}':".format(g_name),
|
||||
rem_n="Following accounts were not in group '{}':".format(g_name),
|
||||
unkwn="Following unknown accounts were ignored:")[key]
|
||||
|
||||
if members:
|
||||
m_list = [] # type: List[str]
|
||||
m_list += [contact_list.get_contact(m).nick for m in members if contact_list.has_contact(m)]
|
||||
m_list += [m for m in members if not contact_list.has_contact(m)]
|
||||
|
||||
just_len = len(max(m_list, key=len))
|
||||
justified = [m] + [" * {}".format(m.ljust(just_len)) for m in m_list]
|
||||
box_print(justified, head=1, tail=1)
|
|
@ -0,0 +1,183 @@
|
|||
#!/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 readline
|
||||
import time
|
||||
import typing
|
||||
|
||||
from src.common.errors import FunctionReturn
|
||||
from src.common.output import c_print, print_on_previous_line
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.common.db_settings import Settings
|
||||
|
||||
|
||||
def ask_path_gui(prompt_msg: str,
|
||||
settings: 'Settings',
|
||||
get_file: bool = False) -> str:
|
||||
"""Prompt PSK path with Tkinter dialog. Fallback to CLI if not available.
|
||||
|
||||
:param prompt_msg: Directory selection prompt
|
||||
:param settings: Settings object
|
||||
:param get_file: When True, prompts for path to file instead of directory
|
||||
:return: Selected directory / file
|
||||
"""
|
||||
try:
|
||||
import _tkinter
|
||||
from tkinter import filedialog, Tk
|
||||
|
||||
try:
|
||||
if settings.disable_gui_dialog:
|
||||
raise _tkinter.TclError
|
||||
|
||||
root = Tk()
|
||||
root.withdraw()
|
||||
|
||||
if get_file:
|
||||
f_path = filedialog.askopenfilename(title=prompt_msg)
|
||||
else:
|
||||
f_path = filedialog.askdirectory(title=prompt_msg)
|
||||
|
||||
root.destroy()
|
||||
|
||||
if not f_path:
|
||||
t = "File" if get_file else "Path"
|
||||
raise FunctionReturn(t + " selection aborted.")
|
||||
|
||||
return f_path
|
||||
|
||||
except _tkinter.TclError:
|
||||
return ask_path_cli(prompt_msg, get_file)
|
||||
|
||||
# Fallback to CLI if Tkinter is not installed
|
||||
except ImportError:
|
||||
if 0: # Remove warnings
|
||||
_tkinter, filedialog, Tk = None, None, None
|
||||
_, _, _ = _tkinter, filedialog, Tk
|
||||
return ask_path_cli(prompt_msg, get_file)
|
||||
|
||||
|
||||
def ask_path_cli(prompt_msg: str, get_file: bool = False) -> str:
|
||||
"""Prompt file location / store dir for PSK with tab-complete supported CLI.
|
||||
|
||||
:param prompt_msg: File/PSK selection prompt
|
||||
:param get_file: When True, prompts for file instead of directory
|
||||
:return: Selected directory
|
||||
"""
|
||||
class Completer(object):
|
||||
"""readline tab-completer for paths and files."""
|
||||
|
||||
@staticmethod
|
||||
def listdir(root):
|
||||
"""Return list of subdirectories (and files)."""
|
||||
res = []
|
||||
for name in os.listdir(root):
|
||||
path = os.path.join(root, name)
|
||||
if os.path.isdir(path):
|
||||
name += os.sep
|
||||
res.append(name)
|
||||
elif get_file:
|
||||
res.append(name)
|
||||
return res
|
||||
|
||||
def complete_path(self, path=None):
|
||||
"""Return list of directories."""
|
||||
# Return subdirectories
|
||||
if not path:
|
||||
return self.listdir('.')
|
||||
|
||||
dirname, rest = os.path.split(path)
|
||||
tmp = dirname if dirname else '.'
|
||||
res = [os.path.join(dirname, p) for p in self.listdir(tmp) if p.startswith(rest)]
|
||||
|
||||
# Multiple directories, return list of dirs
|
||||
if len(res) > 1 or not os.path.exists(path):
|
||||
return res
|
||||
|
||||
# Single directory, return list of files
|
||||
if os.path.isdir(path):
|
||||
return [os.path.join(path, p) for p in self.listdir(path)]
|
||||
|
||||
# Exact file match terminates this completion
|
||||
return [path + ' ']
|
||||
|
||||
def path_complete(self, args):
|
||||
"""Return list of directories from current directory."""
|
||||
if not args:
|
||||
return self.complete_path('.')
|
||||
|
||||
# Treat the last arg as a path and complete it
|
||||
return self.complete_path(args[-1])
|
||||
|
||||
def complete(self, _, state):
|
||||
"""Return complete options."""
|
||||
line = readline.get_line_buffer().split()
|
||||
return (self.path_complete(line) + [None])[state]
|
||||
|
||||
comp = Completer()
|
||||
readline.set_completer_delims(' \t\n;')
|
||||
readline.parse_and_bind('tab: complete')
|
||||
readline.set_completer(comp.complete)
|
||||
print('')
|
||||
|
||||
if get_file:
|
||||
while True:
|
||||
try:
|
||||
path_to_file = input(prompt_msg + ": ")
|
||||
|
||||
if not path_to_file:
|
||||
print_on_previous_line()
|
||||
raise KeyboardInterrupt
|
||||
|
||||
if os.path.isfile(path_to_file):
|
||||
if path_to_file.startswith('./'):
|
||||
path_to_file = path_to_file[2:]
|
||||
print('')
|
||||
return path_to_file
|
||||
|
||||
c_print("File selection error.", head=1, tail=1)
|
||||
time.sleep(1.5)
|
||||
print_on_previous_line(4)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print_on_previous_line()
|
||||
raise FunctionReturn("File selection aborted.")
|
||||
|
||||
else:
|
||||
while True:
|
||||
try:
|
||||
directory = input(prompt_msg + ": ")
|
||||
|
||||
if directory.startswith('./'):
|
||||
directory = directory[2:]
|
||||
|
||||
if not directory.endswith(os.sep):
|
||||
directory += os.sep
|
||||
|
||||
if not os.path.isdir(directory):
|
||||
c_print("Error: Invalid directory.", head=1, tail=1)
|
||||
print_on_previous_line(reps=4, delay=1.5)
|
||||
continue
|
||||
|
||||
return directory
|
||||
|
||||
except KeyboardInterrupt:
|
||||
raise FunctionReturn("File path selection aborted.")
|
|
@ -0,0 +1,464 @@
|
|||
#!/usr/bin/env python3.5
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
# Copyright (c) 2012-2015 Tomer Filiba <tomerfiliba@gmail.com>
|
||||
# Copyright (c) 2015 rotorgit
|
||||
# Copyright (c) 2015 Stephen Larroque <LRQ3000@gmail.com>
|
||||
|
||||
The code below is edited and used under public domain license:
|
||||
https://github.com/tomerfiliba/reedsolomon/blob/master/LICENSE
|
||||
|
||||
The comments/unused code have been intentionally removed. Original code's at
|
||||
https://github.com/tomerfiliba/reedsolomon/blob/master/reedsolo.py
|
||||
"""
|
||||
|
||||
import itertools
|
||||
|
||||
|
||||
class ReedSolomonError(Exception):
|
||||
"""Reed solomon error stub."""
|
||||
pass
|
||||
|
||||
gf_exp = bytearray([1] * 512)
|
||||
gf_log = bytearray(256)
|
||||
field_charac = int(2 ** 8 - 1)
|
||||
|
||||
|
||||
def init_tables(prim=0x11d, generator=2, c_exp=8):
|
||||
"""
|
||||
Precompute the logarithm and anti-log tables for faster computation
|
||||
later, using the provided primitive polynomial. These tables are
|
||||
used for multiplication/division since addition/substraction are
|
||||
simple XOR operations inside GF of characteristic 2. The basic idea
|
||||
is quite simple: since b**(log_b(x), log_b(y)) == x * y given any
|
||||
number b (the base or generator of the logarithm), then we can use
|
||||
any number b to precompute logarithm and anti-log (exponentiation)
|
||||
tables to use for multiplying two numbers x and y.
|
||||
|
||||
That's why when we use a different base/generator number, the log and
|
||||
anti-log tables are drastically different, but the resulting
|
||||
computations are the same given any such tables. For more information, see
|
||||
https://en.wikipedia.org/wiki/Finite_field_arithmetic#Implementation_tricks
|
||||
"""
|
||||
global gf_exp, gf_log, field_charac
|
||||
|
||||
field_charac = int(2 ** c_exp - 1)
|
||||
gf_exp = bytearray(field_charac * 2)
|
||||
gf_log = bytearray(field_charac + 1)
|
||||
x = 1
|
||||
for i in range(field_charac):
|
||||
gf_exp[i] = x
|
||||
gf_log[x] = i
|
||||
x = fg_mult_nolut(x, generator, prim, field_charac + 1)
|
||||
|
||||
for i in range(field_charac, field_charac * 2):
|
||||
gf_exp[i] = gf_exp[i - field_charac]
|
||||
|
||||
return [gf_log, gf_exp]
|
||||
|
||||
|
||||
def gf_sub(x, y):
|
||||
return x ^ y
|
||||
|
||||
|
||||
def gf_inverse(x):
|
||||
return gf_exp[field_charac - gf_log[x]]
|
||||
|
||||
|
||||
def gf_mul(x, y):
|
||||
if x == 0 or y == 0:
|
||||
return 0
|
||||
return gf_exp[(gf_log[x] + gf_log[y]) % field_charac]
|
||||
|
||||
|
||||
def gf_div(x, y):
|
||||
if y == 0:
|
||||
raise ZeroDivisionError()
|
||||
if x == 0:
|
||||
return 0
|
||||
return gf_exp[(gf_log[x] + field_charac - gf_log[y]) % field_charac]
|
||||
|
||||
|
||||
def gf_pow(x, power):
|
||||
return gf_exp[(gf_log[x] * power) % field_charac]
|
||||
|
||||
|
||||
def fg_mult_nolut(x, y, prim=0, field_charac_full=256, carryless=True):
|
||||
"""
|
||||
Galois Field integer multiplication using Russian Peasant Multiplication
|
||||
algorithm (faster than the standard multiplication + modular reduction).
|
||||
If prim is 0 and carryless=False, then the function produces the result
|
||||
for a standard integers multiplication (no carry-less arithmetics nor
|
||||
modular reduction).
|
||||
"""
|
||||
r = 0
|
||||
while y:
|
||||
if y & 1:
|
||||
r = r ^ x if carryless else r + x
|
||||
y >>= 1
|
||||
x <<= 1
|
||||
if prim > 0 and x & field_charac_full:
|
||||
x ^= prim
|
||||
return r
|
||||
|
||||
|
||||
def gf_poly_scale(p, x):
|
||||
return bytearray([gf_mul(p[i], x) for i in range(len(p))])
|
||||
|
||||
|
||||
def gf_poly_add(p, q):
|
||||
r = bytearray(max(len(p), len(q)))
|
||||
r[len(r) - len(p):len(r)] = p
|
||||
for i in range(len(q)):
|
||||
r[i + len(r) - len(q)] ^= q[i]
|
||||
return r
|
||||
|
||||
|
||||
def gf_poly_mul(p, q):
|
||||
"""
|
||||
Multiply two polynomials, inside Galois Field (but the procedure
|
||||
is generic). Optimized function by precomputation of log.
|
||||
"""
|
||||
r = bytearray(len(p) + len(q) - 1)
|
||||
lp = [gf_log[p[i]] for i in range(len(p))]
|
||||
for j in range(len(q)):
|
||||
qj = q[j]
|
||||
if qj != 0:
|
||||
lq = gf_log[qj]
|
||||
for i in range(len(p)):
|
||||
if p[i] != 0:
|
||||
r[i + j] ^= gf_exp[lp[i] + lq]
|
||||
return r
|
||||
|
||||
|
||||
def gf_poly_div(dividend, divisor):
|
||||
"""
|
||||
Fast polynomial division by using Extended Synthetic Division and optimized
|
||||
for GF(2^p) computations (doesn't work with standard polynomials outside of
|
||||
this galois field).
|
||||
"""
|
||||
msg_out = bytearray(dividend)
|
||||
for i in range(len(dividend) - (len(divisor) - 1)):
|
||||
coef = msg_out[i]
|
||||
if coef != 0:
|
||||
for j in range(1, len(divisor)):
|
||||
if divisor[j] != 0:
|
||||
msg_out[i + j] ^= gf_mul(divisor[j], coef)
|
||||
|
||||
separator = -(len(divisor) - 1)
|
||||
return msg_out[:separator], msg_out[separator:]
|
||||
|
||||
|
||||
def gf_poly_eval(poly, x):
|
||||
"""
|
||||
Evaluates a polynomial in GF(2^p) given the value for x.
|
||||
This is based on Horner's scheme for maximum efficiency.
|
||||
"""
|
||||
y = poly[0]
|
||||
for i in range(1, len(poly)):
|
||||
y = gf_mul(y, x) ^ poly[i]
|
||||
return y
|
||||
|
||||
|
||||
def rs_generator_poly(nsym, fcr=0, generator=2):
|
||||
"""
|
||||
Generate an irreducible generator polynomial
|
||||
(necessary to encode a message into Reed-Solomon)
|
||||
"""
|
||||
g = bytearray([1])
|
||||
for i in range(nsym):
|
||||
g = gf_poly_mul(g, [1, gf_pow(generator, i + fcr)])
|
||||
return g
|
||||
|
||||
|
||||
def rs_encode_msg(msg_in, nsym, fcr=0, generator=2, gen=None):
|
||||
"""
|
||||
Reed-Solomon main encoding function, using polynomial division (Extended
|
||||
Synthetic Division, the fastest algorithm available to my knowledge),
|
||||
better explained at http://research.swtch.com/field
|
||||
"""
|
||||
global field_charac
|
||||
if (len(msg_in) + nsym) > field_charac:
|
||||
raise ValueError("Message is too long ({} when max is {})".format(len(msg_in) + nsym, field_charac))
|
||||
|
||||
if gen is None:
|
||||
gen = rs_generator_poly(nsym, fcr, generator)
|
||||
|
||||
msg_in = bytearray(msg_in)
|
||||
msg_out = bytearray(msg_in) + bytearray(len(gen) - 1)
|
||||
lgen = bytearray([gf_log[gen[j]] for j in range(len(gen))])
|
||||
|
||||
for i in range(len(msg_in)):
|
||||
coef = msg_out[i]
|
||||
|
||||
if coef != 0:
|
||||
lcoef = gf_log[coef]
|
||||
for j in range(1, len(gen)):
|
||||
msg_out[i + j] ^= gf_exp[lcoef + lgen[j]]
|
||||
|
||||
msg_out[:len(msg_in)] = msg_in
|
||||
return msg_out
|
||||
|
||||
|
||||
def rs_calc_syndromes(msg, nsym, fcr=0, generator=2):
|
||||
"""
|
||||
Given the received codeword msg and the number of error correcting symbols
|
||||
(nsym), computes the syndromes polynomial. Mathematically, it's essentially
|
||||
equivalent to a Fourier Transform (Chien search being the inverse).
|
||||
"""
|
||||
return [0] + [gf_poly_eval(msg, gf_pow(generator, i + fcr))
|
||||
for i in range(nsym)]
|
||||
|
||||
|
||||
def rs_correct_errata(msg_in, synd, err_pos, fcr=0, generator=2):
|
||||
"""
|
||||
Forney algorithm, computes the values (error magnitude) to correct in_msg.
|
||||
"""
|
||||
global field_charac
|
||||
msg = bytearray(msg_in)
|
||||
coef_pos = [len(msg) - 1 - p for p in err_pos]
|
||||
err_loc = rs_find_errata_locator(coef_pos, generator)
|
||||
err_eval = rs_find_error_evaluator(synd[::-1], err_loc, len(err_loc) - 1)[::-1]
|
||||
|
||||
x = []
|
||||
for i in range(len(coef_pos)):
|
||||
l = field_charac - coef_pos[i]
|
||||
x.append(gf_pow(generator, -l))
|
||||
|
||||
e_ = bytearray(len(msg))
|
||||
xlength = len(x)
|
||||
for i, Xi in enumerate(x):
|
||||
xi_inv = gf_inverse(Xi)
|
||||
err_loc_prime_tmp = []
|
||||
for j in range(xlength):
|
||||
if j != i:
|
||||
err_loc_prime_tmp.append(gf_sub(1, gf_mul(xi_inv, x[j])))
|
||||
|
||||
err_loc_prime = 1
|
||||
for coef in err_loc_prime_tmp:
|
||||
err_loc_prime = gf_mul(err_loc_prime, coef)
|
||||
|
||||
y = gf_poly_eval(err_eval[::-1], xi_inv)
|
||||
y = gf_mul(gf_pow(Xi, 1 - fcr), y)
|
||||
magnitude = gf_div(y, err_loc_prime)
|
||||
e_[err_pos[i]] = magnitude
|
||||
|
||||
msg = gf_poly_add(msg, e_)
|
||||
return msg
|
||||
|
||||
|
||||
def rs_find_error_locator(synd, nsym, erase_loc=None, erase_count=0):
|
||||
"""
|
||||
Find error/errata locator and evaluator
|
||||
polynomials with Berlekamp-Massey algorithm
|
||||
"""
|
||||
if erase_loc:
|
||||
err_loc = bytearray(erase_loc)
|
||||
old_loc = bytearray(erase_loc)
|
||||
else:
|
||||
err_loc = bytearray([1])
|
||||
old_loc = bytearray([1])
|
||||
|
||||
synd_shift = 0
|
||||
if len(synd) > nsym:
|
||||
synd_shift = len(synd) - nsym
|
||||
|
||||
for i in range(nsym - erase_count):
|
||||
if erase_loc:
|
||||
k_ = erase_count + i + synd_shift
|
||||
else:
|
||||
k_ = i + synd_shift
|
||||
|
||||
delta = synd[k_]
|
||||
for j in range(1, len(err_loc)):
|
||||
delta ^= gf_mul(err_loc[-(j + 1)], synd[k_ - j])
|
||||
old_loc += bytearray([0])
|
||||
|
||||
if delta != 0:
|
||||
if len(old_loc) > len(err_loc):
|
||||
new_loc = gf_poly_scale(old_loc, delta)
|
||||
old_loc = gf_poly_scale(err_loc, gf_inverse(delta))
|
||||
err_loc = new_loc
|
||||
err_loc = gf_poly_add(err_loc, gf_poly_scale(old_loc, delta))
|
||||
|
||||
err_loc = list(itertools.dropwhile(lambda x: x == 0, err_loc))
|
||||
errs = len(err_loc) - 1
|
||||
if (errs - erase_count) * 2 + erase_count > nsym:
|
||||
raise ReedSolomonError("Too many errors to correct")
|
||||
|
||||
return err_loc
|
||||
|
||||
|
||||
def rs_find_errata_locator(e_pos, generator=2):
|
||||
"""
|
||||
Compute the erasures/errors/errata locator polynomial from the
|
||||
erasures/errors/errata positions (the positions must be relative to the x
|
||||
coefficient, eg: "hello worldxxxxxxxxx" is tampered to
|
||||
"h_ll_ worldxxxxxxxxx" with xxxxxxxxx being the ecc of length n-k=9, here
|
||||
the string positions are [1, 4], but the coefficients are reversed since
|
||||
the ecc characters are placed as the first coefficients of the polynomial,
|
||||
thus the coefficients of the erased characters are n-1 - [1, 4] = [18, 15]
|
||||
= erasures_loc to be specified as an argument.
|
||||
"""
|
||||
e_loc = [1]
|
||||
|
||||
if len(e_pos) > 0:
|
||||
print("\nWarning! Reed-Solomon erasure code\n"
|
||||
"detected and corrected {} errors in\n"
|
||||
"received packet. This might indicate\n"
|
||||
"eminent serial adapter or data diode\n"
|
||||
"HW failure, or that serial interface\n"
|
||||
"speed is set too high.\n".format(len(e_pos)))
|
||||
|
||||
for i in e_pos:
|
||||
e_loc = gf_poly_mul(e_loc, gf_poly_add([1], [gf_pow(generator, i), 0]))
|
||||
return e_loc
|
||||
|
||||
|
||||
def rs_find_error_evaluator(synd, err_loc, nsym):
|
||||
"""
|
||||
Compute the error (or erasures if you supply sigma=erasures locator
|
||||
polynomial, or errata) evaluator polynomial Omega from the syndrome and the
|
||||
error/erasures/errata locator Sigma. Omega is already computed at the same
|
||||
time as Sigma inside the Berlekamp-Massey implemented above, but in case
|
||||
you modify Sigma, you can recompute Omega afterwards using this method, or
|
||||
just ensure that Omega computed by BM is correct given Sigma.
|
||||
"""
|
||||
_, remainder = gf_poly_div(gf_poly_mul(synd, err_loc), ([1] + [0] * (nsym + 1)))
|
||||
return remainder
|
||||
|
||||
|
||||
def rs_find_errors(err_loc, nmess, generator=2):
|
||||
"""
|
||||
Find the roots (ie, where evaluation = zero) of error polynomial by
|
||||
bruteforce trial, this is a sort of Chien's search (but less efficient,
|
||||
Chien's search is a way to evaluate the polynomial such that each
|
||||
evaluation only takes constant time).
|
||||
"""
|
||||
errs = len(err_loc) - 1
|
||||
err_pos = []
|
||||
for i in range(nmess):
|
||||
if gf_poly_eval(err_loc, gf_pow(generator, i)) == 0:
|
||||
err_pos.append(nmess - 1 - i)
|
||||
|
||||
if len(err_pos) != errs:
|
||||
raise ReedSolomonError("Too many (or few) errors found by Chien "
|
||||
"search for the errata locator polynomial!")
|
||||
return err_pos
|
||||
|
||||
|
||||
def rs_forney_syndromes(synd, pos, nmess, generator=2):
|
||||
erase_pos_reversed = [nmess - 1 - p for p in pos]
|
||||
fsynd = list(synd[1:])
|
||||
for i in range(len(pos)):
|
||||
x = gf_pow(generator, erase_pos_reversed[i])
|
||||
for j in range(len(fsynd) - 1):
|
||||
fsynd[j] = gf_mul(fsynd[j], x) ^ fsynd[j + 1]
|
||||
return fsynd
|
||||
|
||||
|
||||
def rs_correct_msg(msg_in, nsym, fcr=0, generator=2, erase_pos=None, only_erasures=False):
|
||||
"""Reed-Solomon main decoding function."""
|
||||
global field_charac
|
||||
if len(msg_in) > field_charac:
|
||||
raise ValueError("Message is too long ({} when max is {})".format(len(msg_in), field_charac))
|
||||
|
||||
msg_out = bytearray(msg_in)
|
||||
if erase_pos is None:
|
||||
erase_pos = []
|
||||
else:
|
||||
for e_pos in erase_pos:
|
||||
msg_out[e_pos] = 0
|
||||
|
||||
if len(erase_pos) > nsym:
|
||||
raise ReedSolomonError("Too many erasures to correct")
|
||||
synd = rs_calc_syndromes(msg_out, nsym, fcr, generator)
|
||||
|
||||
if max(synd) == 0:
|
||||
return msg_out[:-nsym], msg_out[-nsym:]
|
||||
|
||||
if only_erasures:
|
||||
err_pos = []
|
||||
else:
|
||||
fsynd = rs_forney_syndromes(synd, erase_pos, len(msg_out), generator)
|
||||
err_loc = rs_find_error_locator(fsynd, nsym, erase_count=len(erase_pos))
|
||||
err_pos = rs_find_errors(err_loc[::-1], len(msg_out), generator)
|
||||
|
||||
if err_pos is None:
|
||||
raise ReedSolomonError("Could not locate error")
|
||||
|
||||
msg_out = rs_correct_errata(msg_out, synd, (erase_pos + err_pos), fcr, generator)
|
||||
synd = rs_calc_syndromes(msg_out, nsym, fcr, generator)
|
||||
if max(synd) > 0:
|
||||
raise ReedSolomonError("Could not correct message")
|
||||
return msg_out[:-nsym], msg_out[-nsym:]
|
||||
|
||||
|
||||
class RSCodec(object):
|
||||
"""
|
||||
A Reed Solomon encoder/decoder. After initializing the object, use
|
||||
``encode`` to encode a (byte)string to include the RS correction code, and
|
||||
pass such an encoded (byte)string to ``decode`` to extract the original
|
||||
message (if the number of errors allows for correct decoding). The ``nsym``
|
||||
argument is the length of the correction code, and it determines the number
|
||||
of error bytes (if I understand this correctly, half of ``nsym`` is
|
||||
correctable).
|
||||
|
||||
Modifications by rotorgit 2/3/2015:
|
||||
Added support for US FAA ADSB UAT RS FEC, by allowing user to specify
|
||||
different primitive polynomial and non-zero first consecutive root (fcr).
|
||||
For UAT/ADSB use, set fcr=120 and prim=0x187 when instantiating
|
||||
the class; leaving them out will default for previous values (0 and
|
||||
0x11d)
|
||||
"""
|
||||
|
||||
def __init__(self, nsym=10, nsize=255, fcr=0, prim=0x11d, generator=2,
|
||||
c_exp=8):
|
||||
"""
|
||||
Initialize the Reed-Solomon codec. Note that different parameters
|
||||
change the internal values (the ecc symbols, look-up table values, etc)
|
||||
but not the output result (whether your message can be repaired or not,
|
||||
there is no influence of the parameters).
|
||||
"""
|
||||
self.nsym = nsym
|
||||
self.nsize = nsize
|
||||
self.fcr = fcr
|
||||
self.prim = prim
|
||||
self.generator = generator
|
||||
self.c_exp = c_exp
|
||||
init_tables(prim, generator, c_exp)
|
||||
|
||||
def encode(self, data):
|
||||
"""
|
||||
Encode a message (ie, add the ecc symbols) using Reed-Solomon,
|
||||
whatever the length of the message because we use chunking.
|
||||
"""
|
||||
if isinstance(data, str):
|
||||
data = bytearray(data, "latin-1")
|
||||
chunk_size = self.nsize - self.nsym
|
||||
enc = bytearray()
|
||||
|
||||
for i in range(0, len(data), chunk_size):
|
||||
chunk = data[i:i + chunk_size]
|
||||
enc.extend(rs_encode_msg(chunk, self.nsym, fcr=self.fcr, generator=self.generator))
|
||||
return enc
|
||||
|
||||
def decode(self, data, erase_pos=None, only_erasures=False):
|
||||
"""Repair a message, whatever its size is, by using chunking."""
|
||||
if isinstance(data, str):
|
||||
data = bytearray(data, "latin-1")
|
||||
dec = bytearray()
|
||||
for i in range(0, len(data), self.nsize):
|
||||
chunk = data[i:i + self.nsize]
|
||||
e_pos = []
|
||||
if erase_pos:
|
||||
e_pos = [x for x in erase_pos if x <= self.nsize]
|
||||
erase_pos = [x - (self.nsize + 1)
|
||||
for x in erase_pos if x > self.nsize]
|
||||
|
||||
dec.extend(rs_correct_msg(chunk, self.nsym, fcr=self.fcr,
|
||||
generator=self.generator,
|
||||
erase_pos=e_pos,
|
||||
only_erasures=only_erasures)[0])
|
||||
return dec
|
|
@ -0,0 +1,238 @@
|
|||
#!/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/>.
|
||||
"""
|
||||
|
||||
|
||||
"""VT100 codes
|
||||
|
||||
VT100 codes are used to control printing to terminals. These
|
||||
make building functions like text box drawers possible.
|
||||
"""
|
||||
CURSOR_UP_ONE_LINE = '\x1b[1A'
|
||||
CURSOR_RIGHT_ONE_COLUMN = '\x1b[1C'
|
||||
CLEAR_ENTIRE_LINE = '\x1b[2K'
|
||||
CLEAR_ENTIRE_SCREEN = '\x1b[2J'
|
||||
CURSOR_LEFT_UP_CORNER = '\x1b[H'
|
||||
BOLD_ON = '\033[1m'
|
||||
BOLD_OFF = '\033[0m'
|
||||
|
||||
|
||||
"""Separators
|
||||
|
||||
Separator byte/char is a non-printable byte used
|
||||
to separate fields in serialized data structures.
|
||||
"""
|
||||
US_BYTE = b'\x1f'
|
||||
US_STR = '\x1f'
|
||||
|
||||
|
||||
"""Datagram headers
|
||||
|
||||
These headers are prepended to transmissions that are transmitted over
|
||||
Serial or over the network. They tell receiving device what type of
|
||||
packet is in question.
|
||||
|
||||
Local key packets are only accepted by NH from local TxM. Even if NH is
|
||||
compromised, the worst case scenario is a denial of service attack where
|
||||
RxM receives new local keys user has to cancel when they do not know the
|
||||
key decryption key for.
|
||||
|
||||
Public keys are delivered from contact all the way to RxM provided they
|
||||
are of correct format.
|
||||
|
||||
Message and command packet headers tell RxM whether to parse trailing
|
||||
fields that determine which decryption keys for XSalsa20-Poly1305 it
|
||||
should load. Contacts can alter their packets to deliver C header, but
|
||||
NH will by design drop them and even if it somehow couldn't, RxM would
|
||||
drop the packet after MAC verification of encrypted harac fails.
|
||||
|
||||
Unencrypted packet headers are intended to notify NH that the packet
|
||||
is intended for it: Trailing the header follows a command for the NH.
|
||||
These commands are not delivered to RxM, but a standard encrypted
|
||||
command is sent to RxM before any unencrypted command is sent to NH.
|
||||
During trickle connection, unencrypted commands are disabled to hide
|
||||
the quantity and schedule of communication even if NH is compromised and
|
||||
monitoring user. Unencrypted commands do not cause issues in security
|
||||
because if adversary can compromise NH to the point it can issue
|
||||
commands to NH, they could DoS NH anyway.
|
||||
|
||||
File CT headers are for faster non-trickle file export from TxM to NH
|
||||
and in receiving end, import from NH to RxM.
|
||||
"""
|
||||
LOCAL_KEY_PACKET_HEADER = b'L'
|
||||
PUBLIC_KEY_PACKET_HEADER = b'P'
|
||||
MESSAGE_PACKET_HEADER = b'M'
|
||||
COMMAND_PACKET_HEADER = b'C'
|
||||
UNENCRYPTED_PACKET_HEADER = b'U'
|
||||
EXPORTED_FILE_CT_HEADER = b'E'
|
||||
IMPORTED_FILE_CT_HEADER = b'I'
|
||||
|
||||
|
||||
"""Assembly packet headers
|
||||
|
||||
These one byte assembly packet headers are not part of the padded
|
||||
message parsed from assembly packets. They are however the very first
|
||||
plaintext byte, prepended to every padded assembly packet delivered to
|
||||
recipient or local RxM. They deliver information about if and when to
|
||||
process the packet and when to drop previously collected assembly packets.
|
||||
"""
|
||||
M_S_HEADER = b'a' # Short message packet
|
||||
M_L_HEADER = b'b' # First packet of multi-packet message
|
||||
M_A_HEADER = b'c' # Appended packet of multi-packet message
|
||||
M_E_HEADER = b'd' # Last packet of multi-packet message
|
||||
M_C_HEADER = b'e' # Cancelled multi-packet message
|
||||
P_N_HEADER = b'f' # Noise message packet (no separate packet for files is needed)
|
||||
|
||||
F_S_HEADER = b'A' # Short file packet
|
||||
F_L_HEADER = b'B' # First packet of multi-packet file
|
||||
F_A_HEADER = b'C' # Appended packet for multi-packet file
|
||||
F_E_HEADER = b'D' # Last packet of multi-packet file
|
||||
F_C_HEADER = b'E' # Cancelled multi-packet file
|
||||
|
||||
C_S_HEADER = b'0' # Short command packet
|
||||
C_L_HEADER = b'1' # First packet of multi-packet command
|
||||
C_A_HEADER = b'2' # Appended packet of multi-packet command
|
||||
C_E_HEADER = b'3' # Last packet of multi-packet command
|
||||
C_C_HEADER = b'4' # Cancelled multi-packet command (not implemented)
|
||||
C_N_HEADER = b'5' # Noise command packet
|
||||
|
||||
|
||||
"""Unencrypted command headers
|
||||
|
||||
These two-byte headers are only used to control NH. These commands will
|
||||
not be used during trickle connection to hide when TFC is being used.
|
||||
These commands are not encrypted because if attacker is able to inject
|
||||
commands from within NH, it could also access any keys stored on NH.
|
||||
"""
|
||||
UNENCRYPTED_SCREEN_CLEAR = b'SC'
|
||||
UNENCRYPTED_SCREEN_RESET = b'SR'
|
||||
UNENCRYPTED_EXIT_COMMAND = b'EX'
|
||||
UNENCRYPTED_IMPORT_COMMAND = b'IF'
|
||||
UNENCRYPTED_EC_RATIO = b'EC'
|
||||
UNENCRYPTED_BAUDRATE = b'BR'
|
||||
UNENCRYPTED_GUI_DIALOG = b'GD'
|
||||
|
||||
|
||||
"""Encrypted command headers
|
||||
|
||||
These two-byte headers are prepended to each command delivered to local
|
||||
RxM. The header is evaluated after RxM has received all assembly packets
|
||||
of one transmission. These headers tell RxM to what function the command
|
||||
must be redirected to.
|
||||
"""
|
||||
LOCAL_KEY_INSTALLED_HEADER = b'LI'
|
||||
SHOW_WINDOW_ACTIVITY_HEADER = b'SA'
|
||||
WINDOW_CHANGE_HEADER = b'WS'
|
||||
CLEAR_SCREEN_HEADER = b'SC'
|
||||
RESET_SCREEN_HEADER = b'SR'
|
||||
EXIT_PROGRAM_HEADER = b'EX'
|
||||
LOG_DISPLAY_HEADER = b'LD'
|
||||
LOG_EXPORT_HEADER = b'LE'
|
||||
CHANGE_MASTER_K_HEADER = b'MK'
|
||||
CHANGE_NICK_HEADER = b'NC'
|
||||
CHANGE_SETTING_HEADER = b'CS'
|
||||
CHANGE_LOGGING_HEADER = b'CL'
|
||||
CHANGE_FILE_R_HEADER = b'CF'
|
||||
CHANGE_NOTIFY_HEADER = b'CN'
|
||||
GROUP_CREATE_HEADER = b'GC'
|
||||
GROUP_ADD_HEADER = b'GA'
|
||||
GROUP_REMOVE_M_HEADER = b'GR'
|
||||
GROUP_DELETE_HEADER = b'GD'
|
||||
KEY_EX_ECDHE_HEADER = b'KE'
|
||||
KEY_EX_PSK_TX_HEADER = b'KT'
|
||||
KEY_EX_PSK_RX_HEADER = b'KR'
|
||||
CONTACT_REMOVE_HEADER = b'CR'
|
||||
|
||||
|
||||
"""Origin headers
|
||||
|
||||
This one byte header notifies RxM whether the account delivered with
|
||||
packet is from user to that account or from the account to user.
|
||||
"""
|
||||
ORIGIN_USER_HEADER = b'u'
|
||||
ORIGIN_CONTACT_HEADER = b'c'
|
||||
|
||||
|
||||
"""Message headers
|
||||
|
||||
This one byte header will be prepended to each plaintext message prior
|
||||
to padding and splitting the message. It will be evaluated once RxM
|
||||
has received all assembly packets. It allows RxM to detect whether
|
||||
the message should be displayed on private or group window. This does
|
||||
not allow spoofing of messages in unauthorized group windows, because
|
||||
the group configuration managed personally by the recipient white lists
|
||||
accounts who are authorized to display the message under the window.
|
||||
"""
|
||||
PRIVATE_MESSAGE_HEADER = b'P'
|
||||
GROUP_MESSAGE_HEADER = b'G'
|
||||
|
||||
|
||||
"""Group management headers
|
||||
|
||||
Group messages are automatically parsed messages that TxM recommends user
|
||||
to send when they make changes to group members or add/remove groups.
|
||||
These messages are displayed temporarily on whatever active window and
|
||||
later in command window.
|
||||
"""
|
||||
GROUP_MSG_INVITATION_HEADER = b'I'
|
||||
GROUP_MSG_ADD_NOTIFY_HEADER = b'A'
|
||||
GROUP_MSG_MEMBER_RM_HEADER = b'R'
|
||||
GROUP_MSG_EXIT_GROUP_HEADER = b'E'
|
||||
|
||||
|
||||
"""Delays
|
||||
|
||||
Trickle packet queue check delay ensures that the lookup time for packet
|
||||
queue is obfuscated.
|
||||
|
||||
Serial packet output delay ensures that receiving device will timeout
|
||||
between datagrams.
|
||||
"""
|
||||
TRICKLE_QUEUE_CHECK_DELAY = 0.1
|
||||
|
||||
|
||||
"""Default folders"""
|
||||
DIR_USER_DATA = 'user_data'
|
||||
DIR_RX_FILES = 'received_files'
|
||||
DIR_IMPORTED = 'imported_files'
|
||||
|
||||
|
||||
"""Static identifiers"""
|
||||
LOCAL_WIN_ID_BYTES = b'local'
|
||||
FILE_R_WIN_ID_BYTES = b'file_window'
|
||||
|
||||
|
||||
"""Regular expressions
|
||||
|
||||
These are used to specify exact format of some inputs.
|
||||
"""
|
||||
ACCOUNT_FORMAT = "(^.[^/:,]*@.[^/:,]*\.[^/:,]*.$)" # <something>@<something>.<something>
|
||||
|
||||
|
||||
"""Queue dictionary keys"""
|
||||
|
||||
MESSAGE_PACKET_QUEUE = b'm'
|
||||
FILE_PACKET_QUEUE = b'f'
|
||||
COMMAND_PACKET_QUEUE = b'c'
|
||||
LOG_PACKET_QUEUE = b'l'
|
||||
NOISE_PACKET_QUEUE = b'np'
|
||||
NOISE_COMMAND_QUEUE = b'nc'
|
||||
KEY_MANAGEMENT_QUEUE = b'km'
|
||||
WINDOW_SELECT_QUEUE = b'ws' # For trickle connection
|
||||
GATEWAY_QUEUE = b'gw'
|
|
@ -0,0 +1,109 @@
|
|||
#!/usr/bin/env python3.5
|
||||
# -*- 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 serial
|
||||
import sys
|
||||
import time
|
||||
import typing
|
||||
|
||||
from src.common.errors import FunctionReturn
|
||||
from src.common.path import ask_path_gui
|
||||
from src.common.statics import *
|
||||
from src.nh.misc import c_print, clear_screen
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from multiprocessing import Queue
|
||||
from src.nh.settings import Settings
|
||||
|
||||
|
||||
def nh_command(settings: 'Settings',
|
||||
q_to_nh: 'Queue',
|
||||
q_to_rxm: 'Queue',
|
||||
q_im_cmd: 'Queue',
|
||||
file_no: int # stdin input file descriptor
|
||||
) -> None:
|
||||
"""Process NH side commands."""
|
||||
|
||||
sys.stdin = os.fdopen(file_no)
|
||||
|
||||
while True:
|
||||
try:
|
||||
if q_to_nh.empty():
|
||||
time.sleep(0.001)
|
||||
continue
|
||||
command = q_to_nh.get()
|
||||
header = command[:2]
|
||||
|
||||
if header in [UNENCRYPTED_SCREEN_CLEAR, UNENCRYPTED_SCREEN_RESET]:
|
||||
# Handle race condition with RxM command notification
|
||||
time.sleep(0.1)
|
||||
if settings.local_testing_mode and settings.data_diode_sockets:
|
||||
time.sleep(0.7)
|
||||
|
||||
if header == UNENCRYPTED_SCREEN_CLEAR:
|
||||
q_im_cmd.put(command)
|
||||
clear_screen()
|
||||
|
||||
if header == UNENCRYPTED_SCREEN_RESET:
|
||||
q_im_cmd.put(command)
|
||||
os.system('reset')
|
||||
|
||||
if header == UNENCRYPTED_EXIT_COMMAND:
|
||||
exit()
|
||||
|
||||
if header == UNENCRYPTED_EC_RATIO:
|
||||
value = eval(command[2:])
|
||||
|
||||
if not isinstance(value, int) or value < 1:
|
||||
c_print("Error: Received Invalid EC ratio value from TxM.")
|
||||
continue
|
||||
|
||||
settings.e_correction_ratio = value
|
||||
settings.store_settings()
|
||||
c_print("Error correction ratio will change on restart.", head=1, tail=1)
|
||||
|
||||
if header == UNENCRYPTED_BAUDRATE:
|
||||
value = eval(command[2:])
|
||||
|
||||
if not isinstance(value, int) or value not in serial.Serial.BAUDRATES:
|
||||
c_print("Error: Received invalid baud rate value from TxM.")
|
||||
continue
|
||||
|
||||
settings.serial_iface_speed = value
|
||||
settings.store_settings()
|
||||
c_print("Baud rate will change on restart.", head=1, tail=1)
|
||||
|
||||
if header == UNENCRYPTED_IMPORT_COMMAND:
|
||||
f_path = ask_path_gui("Select file to import...", settings, get_file=True)
|
||||
with open(f_path, 'rb') as f:
|
||||
f_data = f.read()
|
||||
q_to_rxm.put(IMPORTED_FILE_CT_HEADER + f_data)
|
||||
|
||||
if header == UNENCRYPTED_GUI_DIALOG:
|
||||
value = eval(command[2:])
|
||||
|
||||
settings.disable_gui_dialog = value
|
||||
settings.store_settings()
|
||||
|
||||
c_print("Changed setting disable_gui_dialog to {}.".format(value), head=1, tail=1)
|
||||
|
||||
except (KeyboardInterrupt, EOFError, FunctionReturn):
|
||||
pass
|
|
@ -0,0 +1,170 @@
|
|||
#!/usr/bin/env python3.5
|
||||
# -*- 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 multiprocessing.connection
|
||||
import os.path
|
||||
import platform
|
||||
import serial
|
||||
import time
|
||||
import typing
|
||||
|
||||
from serial.serialutil import SerialException
|
||||
from typing import Any
|
||||
|
||||
from src.common.errors import CriticalError
|
||||
from src.nh.misc import graceful_exit, print_on_previous_line, phase
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from multiprocessing import Queue
|
||||
from src.nh.settings import Settings
|
||||
|
||||
|
||||
def gw_incoming(q_to_tip: 'Queue', gateway: 'Gateway'):
|
||||
"""Process that loads data from TxM side gateway."""
|
||||
while True:
|
||||
try:
|
||||
q_to_tip.put(gateway.read())
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
|
||||
class Gateway(object):
|
||||
"""Gateway object is a wrapper that provides interconnection between NH and TxM/RxM."""
|
||||
|
||||
def __init__(self, settings: 'Settings') -> None:
|
||||
"""Create a new gateway object."""
|
||||
self.settings = settings
|
||||
self.txm_interface = None # type: Any
|
||||
self.rxm_interface = None # type: Any
|
||||
|
||||
# Remember when serial adapter is found the first time so that
|
||||
# further serial interface searches know to announce disconnection.
|
||||
self.init_found = False
|
||||
bauds_per_byte = 10
|
||||
bytes_per_s = self.settings.serial_iface_speed / bauds_per_byte
|
||||
byte_travel_t = 1 / bytes_per_s
|
||||
self.timeout = max(2 * byte_travel_t, 0.01)
|
||||
self.delay = 2 * self.timeout
|
||||
|
||||
if settings.local_testing_mode:
|
||||
self.establish_socket()
|
||||
else:
|
||||
self.txm_interface = self.rxm_interface = self.establish_serial()
|
||||
|
||||
def read(self) -> bytes:
|
||||
"""Read data via socket/serial interface."""
|
||||
if self.settings.local_testing_mode:
|
||||
while True:
|
||||
try:
|
||||
return self.txm_interface.recv()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except EOFError:
|
||||
graceful_exit("IPC client disconnected.")
|
||||
else:
|
||||
while True:
|
||||
try:
|
||||
start_time = 0.0
|
||||
read_buffer = bytearray()
|
||||
while True:
|
||||
read = self.txm_interface.read(1000)
|
||||
if read:
|
||||
start_time = time.monotonic()
|
||||
read_buffer.extend(read)
|
||||
else:
|
||||
if read_buffer:
|
||||
delta = time.monotonic() - start_time
|
||||
if delta > self.timeout:
|
||||
return bytes(read_buffer)
|
||||
else:
|
||||
time.sleep(0.001)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except SerialException:
|
||||
self.txm_interface = self.establish_serial()
|
||||
self.read()
|
||||
|
||||
def write(self, packet: bytes) -> None:
|
||||
"""Output data via socket/serial interface."""
|
||||
if self.settings.local_testing_mode:
|
||||
self.rxm_interface.send(packet)
|
||||
else:
|
||||
try:
|
||||
self.rxm_interface.write(packet)
|
||||
self.rxm_interface.flush()
|
||||
time.sleep(self.delay)
|
||||
except SerialException:
|
||||
self.rxm_interface = self.establish_serial()
|
||||
self.write(packet)
|
||||
|
||||
def establish_socket(self) -> None:
|
||||
"""Establish local testing socket connections."""
|
||||
listener = multiprocessing.connection.Listener(('localhost', 5001))
|
||||
self.txm_interface = listener.accept()
|
||||
|
||||
while True:
|
||||
try:
|
||||
rxm_socket = 5002 if self.settings.data_diode_sockets else 5003
|
||||
self.rxm_interface = multiprocessing.connection.Client(('localhost', rxm_socket))
|
||||
break
|
||||
except ConnectionRefusedError:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
def establish_serial(self) -> Any:
|
||||
"""Create new serial interface object."""
|
||||
try:
|
||||
serial_nh = self.search_serial_interface()
|
||||
return serial.Serial(serial_nh, self.settings.session_if_speed, timeout=0)
|
||||
except SerialException:
|
||||
raise CriticalError("SerialException. Ensure $USER is in dialout group.")
|
||||
|
||||
def search_serial_interface(self) -> str:
|
||||
"""Search for serial interface."""
|
||||
if self.settings.serial_usb_adapter:
|
||||
search_announced = False
|
||||
|
||||
if not self.init_found:
|
||||
print_on_previous_line()
|
||||
phase("Searching for USB-to-serial interface")
|
||||
|
||||
while True:
|
||||
time.sleep(0.1)
|
||||
for f in sorted(os.listdir('/dev')):
|
||||
if f.startswith('ttyUSB'):
|
||||
if self.init_found:
|
||||
time.sleep(1.5)
|
||||
phase('Found', done=True)
|
||||
if self.init_found:
|
||||
print_on_previous_line(reps=2)
|
||||
self.init_found = True
|
||||
return '/dev/{}'.format(f)
|
||||
else:
|
||||
if not search_announced:
|
||||
if self.init_found:
|
||||
phase("Serial adapter disconnected. Waiting for interface", head=1)
|
||||
search_announced = True
|
||||
|
||||
else:
|
||||
f = 'serial0' if 'Raspbian' in platform.platform() else 'ttyS0'
|
||||
if f in sorted(os.listdir('/dev/')):
|
||||
return '/dev/{}'.format(f)
|
||||
raise CriticalError("Error: /dev/{} was not found.".format(f))
|
|
@ -0,0 +1,241 @@
|
|||
#!/usr/bin/env python3.5
|
||||
# -*- 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 argparse
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
|
||||
from typing import Tuple, Union
|
||||
|
||||
from src.common.statics import *
|
||||
|
||||
|
||||
def box_print(msg_list: Union[str, list],
|
||||
manual_proceed: bool = False,
|
||||
head: int = 0,
|
||||
tail: int = 0) -> None:
|
||||
"""Print message inside a box.
|
||||
|
||||
:param msg_list: List of lines to print
|
||||
:param manual_proceed: Wait for user input before continuing
|
||||
:param head: Number of new lines to print before box
|
||||
:param tail: Number of new lines to print after box
|
||||
:return: None
|
||||
"""
|
||||
for _ in range(head):
|
||||
print('')
|
||||
|
||||
if isinstance(msg_list, str):
|
||||
msg_list = [msg_list]
|
||||
|
||||
tty_w = get_tty_w()
|
||||
widest = max(msg_list, key=len)
|
||||
|
||||
msg_list = ['{:^{}}'.format(m, len(widest)) for m in msg_list]
|
||||
|
||||
top_line = '┌' + (len(msg_list[0]) + 2) * '─' + '┐'
|
||||
bot_line = '└' + (len(msg_list[0]) + 2) * '─' + '┘'
|
||||
msg_list = ['│ {} │'.format(m) for m in msg_list]
|
||||
|
||||
top_line = top_line.center(tty_w)
|
||||
msg_list = [m.center(tty_w) for m in msg_list]
|
||||
bot_line = bot_line.center(tty_w)
|
||||
|
||||
print(top_line)
|
||||
for m in msg_list:
|
||||
print(m)
|
||||
print(bot_line)
|
||||
|
||||
for _ in range(tail):
|
||||
print('')
|
||||
|
||||
if manual_proceed:
|
||||
input('')
|
||||
print_on_previous_line()
|
||||
|
||||
|
||||
def c_print(string: str, head: int = 0, tail: int = 0) -> None:
|
||||
"""Print string to center of screen.
|
||||
|
||||
:param string: String to print
|
||||
:param head: Number of new lines to print before string
|
||||
:param tail: Number of new lines to print after string
|
||||
:return: None
|
||||
"""
|
||||
for _ in range(head):
|
||||
print('')
|
||||
|
||||
print(string.center(get_tty_w()))
|
||||
|
||||
for _ in range(tail):
|
||||
print('')
|
||||
|
||||
|
||||
def clear_screen(delay: int = 0) -> None:
|
||||
"""Clear terminal window."""
|
||||
time.sleep(delay)
|
||||
sys.stdout.write(CLEAR_ENTIRE_SCREEN + CURSOR_LEFT_UP_CORNER)
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def ensure_dir(directory: str) -> None:
|
||||
"""Ensure directory exists."""
|
||||
name = os.path.dirname(directory)
|
||||
if not os.path.exists(name):
|
||||
os.makedirs(name)
|
||||
|
||||
|
||||
def get_tty_w() -> int:
|
||||
"""Return width of terminal TFC is running in."""
|
||||
return shutil.get_terminal_size()[0]
|
||||
|
||||
|
||||
def graceful_exit(message: str = '', clear: bool = True) -> None:
|
||||
"""Display a message and exit Tx.py."""
|
||||
if clear:
|
||||
sys.stdout.write(CLEAR_ENTIRE_SCREEN + CURSOR_LEFT_UP_CORNER)
|
||||
if message:
|
||||
print("\n{}".format(message))
|
||||
print("\nExiting TFC.\n")
|
||||
exit()
|
||||
|
||||
|
||||
def phase(string: str,
|
||||
done: bool = False,
|
||||
head: int = 0,
|
||||
offset: int = 2) -> None:
|
||||
"""Print name of next phase.
|
||||
|
||||
Message about completion will be printed on same line.
|
||||
|
||||
:param string: String to be printed
|
||||
:param done: Notify with custom message
|
||||
:param head: N.o. inserted new lines before print
|
||||
:param offset: Offset of message from center to left
|
||||
:return: None
|
||||
"""
|
||||
for _ in range(head):
|
||||
print('')
|
||||
|
||||
if string == 'Done' or done:
|
||||
print(string)
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
string = '{}... '.format(string)
|
||||
indent = ((get_tty_w() - (len(string) + offset)) // 2) * ' '
|
||||
|
||||
print(indent + string, end='', flush=True)
|
||||
|
||||
|
||||
def print_on_previous_line(reps: int = 1,
|
||||
delay: float = 0.0,
|
||||
flush: bool = False) -> None:
|
||||
"""Next message will be printed on upper line.
|
||||
|
||||
:param reps: Number of times to repeat function
|
||||
:param delay: Time to sleep before clearing lines above
|
||||
:param flush: Flush stdout when true
|
||||
:return: None
|
||||
"""
|
||||
time.sleep(delay)
|
||||
|
||||
for _ in range(reps):
|
||||
sys.stdout.write(CURSOR_UP_ONE_LINE + CLEAR_ENTIRE_LINE)
|
||||
if flush:
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def process_arguments() -> Tuple[bool, bool]:
|
||||
"""Define NH.py settings from arguments passed from command line."""
|
||||
parser = argparse.ArgumentParser("python NH.py",
|
||||
usage="%(prog)s [OPTION]",
|
||||
description="More options inside NH.py")
|
||||
|
||||
parser.add_argument("-l",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="local_test",
|
||||
help="Enable local testing mode")
|
||||
|
||||
parser.add_argument("-d",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="dd_sockets",
|
||||
help="Enable data diode simulator sockets")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
return args.local_test, args.dd_sockets
|
||||
|
||||
|
||||
def yes(prompt: str, head: int = 0, tail: int = 0) -> bool:
|
||||
"""Prompt user a question that is answered with yes / no.
|
||||
|
||||
:param prompt: Question to be asked
|
||||
:param head: Number of new lines to print before prompt
|
||||
:param tail: Number of new lines to print after prompt
|
||||
:return: True if user types 'y' or 'yes'
|
||||
False if user types 'n' or 'no'
|
||||
"""
|
||||
for _ in range(head):
|
||||
print('')
|
||||
|
||||
prompt = "{} (y/n): ".format(prompt)
|
||||
tty_w = get_tty_w()
|
||||
upper_line = ('┌' + (len(prompt) + 5) * '─' + '┐')
|
||||
title_line = ('│' + prompt + 5 * ' ' + '│')
|
||||
lower_line = ('└' + (len(prompt) + 5) * '─' + '┘')
|
||||
|
||||
upper_line = upper_line.center(tty_w)
|
||||
title_line = title_line.center(tty_w)
|
||||
lower_line = lower_line.center(tty_w)
|
||||
|
||||
indent = title_line.find('│')
|
||||
print(upper_line)
|
||||
print(title_line)
|
||||
print(lower_line)
|
||||
print(3 * CURSOR_UP_ONE_LINE)
|
||||
|
||||
while True:
|
||||
print(title_line)
|
||||
print(lower_line)
|
||||
print(3 * CURSOR_UP_ONE_LINE)
|
||||
answer = input(indent * ' ' + '│ {}'.format(prompt))
|
||||
print_on_previous_line()
|
||||
|
||||
if answer == '':
|
||||
continue
|
||||
|
||||
if answer.lower() in 'yes':
|
||||
print(indent * ' ' + '│ {}Yes │\n'.format(prompt))
|
||||
for _ in range(tail):
|
||||
print('')
|
||||
return True
|
||||
|
||||
elif answer.lower() in 'no':
|
||||
print(indent * ' ' + '│ {}No │\n'.format(prompt))
|
||||
for _ in range(tail):
|
||||
print('')
|
||||
return False
|
||||
|
||||
else:
|
||||
continue
|
|
@ -0,0 +1,180 @@
|
|||
#!/usr/bin/env python3.5
|
||||
# -*- 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 base64
|
||||
import datetime
|
||||
import dbus
|
||||
import dbus.exceptions
|
||||
import time
|
||||
import typing
|
||||
|
||||
from typing import Any
|
||||
|
||||
from dbus.mainloop.glib import DBusGMainLoop
|
||||
from gi.repository import GObject
|
||||
|
||||
from src.nh.misc import box_print, c_print, clear_screen, phase
|
||||
from src.common.statics import *
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from multiprocessing import Queue
|
||||
from src.nh.settings import Settings
|
||||
|
||||
|
||||
def ensure_im_connection() -> None:
|
||||
"""Check that NH.py has connection to Pidgin before launching other processes."""
|
||||
phase("Waiting for enabled account in Pidgin", offset=1)
|
||||
|
||||
while True:
|
||||
try:
|
||||
bus = dbus.SessionBus(private=True)
|
||||
obj = bus.get_object("im.pidgin.purple.PurpleService", "/im/pidgin/purple/PurpleObject")
|
||||
purple = dbus.Interface(obj, "im.pidgin.purple.PurpleInterface")
|
||||
|
||||
while not purple.PurpleAccountsGetAllActive():
|
||||
time.sleep(0.001)
|
||||
continue
|
||||
phase('OK', done=True)
|
||||
|
||||
accounts = []
|
||||
for a in purple.PurpleAccountsGetAllActive():
|
||||
accounts.append(purple.PurpleAccountGetUsername(a)[:-1])
|
||||
|
||||
just_len = len(max(accounts, key=len))
|
||||
justified = ["Active accounts in Pidgin:"] + ["* {}".format(a.ljust(just_len)) for a in accounts]
|
||||
box_print(justified, head=1, tail=1)
|
||||
return None
|
||||
|
||||
except (IndexError, dbus.exceptions.DBusException):
|
||||
continue
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
clear_screen()
|
||||
exit()
|
||||
|
||||
|
||||
def im_command(q_im_cmd: 'Queue') -> None:
|
||||
"""Run IM client command."""
|
||||
bus = dbus.SessionBus(private=True)
|
||||
obj = bus.get_object("im.pidgin.purple.PurpleService", "/im/pidgin/purple/PurpleObject")
|
||||
purple = dbus.Interface(obj, "im.pidgin.purple.PurpleInterface")
|
||||
account = purple.PurpleAccountsGetAllActive()[0]
|
||||
|
||||
while True:
|
||||
try:
|
||||
if q_im_cmd.empty():
|
||||
time.sleep(0.001)
|
||||
continue
|
||||
command = q_im_cmd.get()
|
||||
|
||||
if command[:2] in [UNENCRYPTED_SCREEN_CLEAR, UNENCRYPTED_SCREEN_RESET]:
|
||||
contact = command[2:]
|
||||
new_conv = purple.PurpleConversationNew(1, account, contact)
|
||||
purple.PurpleConversationClearMessageHistory(new_conv)
|
||||
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
pass
|
||||
except dbus.exceptions.DBusException:
|
||||
continue
|
||||
|
||||
|
||||
def im_incoming(settings: 'Settings', q_to_rxm: 'Queue') -> None:
|
||||
"""Start signal receiver for packets from Pidgin."""
|
||||
|
||||
def pidgin_to_rxm(account: str, sender: str, message: str, *_: Any) -> None:
|
||||
"""Process received packet from Pidgin."""
|
||||
sender = sender.split('/')[0]
|
||||
ts = datetime.datetime.now().strftime(settings.t_fmt)
|
||||
s_bus = dbus.SessionBus(private=True)
|
||||
obj = s_bus.get_object("im.pidgin.purple.PurpleService", "/im/pidgin/purple/PurpleObject")
|
||||
purple = dbus.Interface(obj, "im.pidgin.purple.PurpleInterface")
|
||||
|
||||
user = ''
|
||||
for a in purple.PurpleAccountsGetAllActive():
|
||||
if a == account:
|
||||
user = purple.PurpleAccountGetUsername(a)[:-1]
|
||||
|
||||
if not message.startswith('TFC'):
|
||||
return None
|
||||
|
||||
try:
|
||||
__, header, payload = message.split('|')
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
if header.encode() == PUBLIC_KEY_PACKET_HEADER:
|
||||
print("{} - pub key {} > {} > RxM".format(ts, sender, user))
|
||||
|
||||
elif header.encode() == MESSAGE_PACKET_HEADER:
|
||||
print("{} - message {} > {} > RxM".format(ts, sender, user))
|
||||
|
||||
else:
|
||||
print("Received invalid packet from {}".format(sender))
|
||||
return None
|
||||
|
||||
decoded = base64.b64decode(payload)
|
||||
packet = header.encode() + decoded + ORIGIN_CONTACT_HEADER + sender.encode()
|
||||
q_to_rxm.put(packet)
|
||||
|
||||
while True:
|
||||
try:
|
||||
bus = dbus.SessionBus(private=True, mainloop=DBusGMainLoop())
|
||||
bus.add_signal_receiver(pidgin_to_rxm, dbus_interface="im.pidgin.purple.PurpleInterface", signal_name="ReceivedImMsg")
|
||||
GObject.MainLoop().run()
|
||||
except (dbus.exceptions.DBusException, EOFError, KeyboardInterrupt):
|
||||
continue
|
||||
|
||||
|
||||
def im_outgoing(settings: 'Settings', q_to_pidgin: 'Queue') -> None:
|
||||
"""Send message from queue to Pidgin."""
|
||||
bus = dbus.SessionBus(private=True)
|
||||
obj = bus.get_object("im.pidgin.purple.PurpleService", "/im/pidgin/purple/PurpleObject")
|
||||
purple = dbus.Interface(obj, "im.pidgin.purple.PurpleInterface")
|
||||
|
||||
while True:
|
||||
try:
|
||||
if q_to_pidgin.empty():
|
||||
time.sleep(0.001)
|
||||
continue
|
||||
|
||||
header, payload, user, contact = q_to_pidgin.get()
|
||||
|
||||
b64_str = base64.b64encode(payload).decode()
|
||||
payload = '|'.join(['TFC', header.decode(), b64_str])
|
||||
user = user.decode()
|
||||
contact = contact.decode()
|
||||
|
||||
user_found = False
|
||||
for u in purple.PurpleAccountsGetAllActive():
|
||||
if user == purple.PurpleAccountGetUsername(u)[:-1]:
|
||||
user_found = True
|
||||
if settings.relay_to_im_client:
|
||||
new_conv = purple.PurpleConversationNew(1, u, contact)
|
||||
sel_conv = purple.PurpleConvIm(new_conv)
|
||||
purple.PurpleConvImSend(sel_conv, payload)
|
||||
continue
|
||||
|
||||
if not user_found:
|
||||
c_print("Error: No user {} found.".format(user), head=1, tail=1)
|
||||
continue
|
||||
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
pass
|
||||
except dbus.exceptions.DBusException:
|
||||
continue
|
|
@ -0,0 +1,108 @@
|
|||
#!/usr/bin/env python3.5
|
||||
# -*- 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 struct
|
||||
|
||||
from typing import Union
|
||||
|
||||
from src.common.misc import ensure_dir
|
||||
from src.common.statics import *
|
||||
from src.nh.misc import yes
|
||||
|
||||
|
||||
def bool_to_bytes(boolean: bool) -> bytes:
|
||||
"""Convert boolean value to 1-byte byte string."""
|
||||
return bytes([boolean])
|
||||
|
||||
|
||||
def int_to_bytes(integer: int) -> bytes:
|
||||
"""Convert integer to 8-byte byte string."""
|
||||
return struct.pack('!Q', integer)
|
||||
|
||||
|
||||
def bytes_to_bool(byte_string: Union[bytes, int]) -> bool:
|
||||
"""Convert 1-byte byte string to boolean value."""
|
||||
if isinstance(byte_string, bytes):
|
||||
byte_string = byte_string[0]
|
||||
return bool(byte_string)
|
||||
|
||||
|
||||
def bytes_to_int(byte_string: bytes) -> int:
|
||||
"""Convert 8-byte byte string to integer."""
|
||||
return struct.unpack('!Q', byte_string)[0]
|
||||
|
||||
|
||||
class Settings(object):
|
||||
"""Settings object stores NH side persistent settings
|
||||
|
||||
NH-side settings are not encrypted because NH is assumed to be in
|
||||
control of the adversary. Encryption would require password and
|
||||
because some users might use same password for NH and TxM/RxM,
|
||||
sensitive passwords might leak to remote attacker who might later
|
||||
physically compromise the endpoint.
|
||||
"""
|
||||
|
||||
def __init__(self, local_testing: bool, dd_sockets: bool, operation='nh') -> None:
|
||||
|
||||
# Settings from launcher / CLI arguments
|
||||
self.local_testing_mode = local_testing
|
||||
self.data_diode_sockets = dd_sockets
|
||||
|
||||
self.t_fmt = "%m-%d / %H:%M:%S" # Timestamp format of displayed messages
|
||||
self.disable_gui_dialog = False # When True, only uses CLI prompts for RxM file imports
|
||||
self.relay_to_im_client = True # False stops sending messages to IM client
|
||||
self.serial_usb_adapter = True # Number of USB-to-serial adapters used (0, 1 or 2)
|
||||
self.serial_iface_speed = 19200 # The speed of serial interface in bauds per sec
|
||||
self.e_correction_ratio = 5 # N/o byte errors serial datagrams can recover from
|
||||
|
||||
self.software_operation = operation
|
||||
self.file_name = '{}/{}_settings'.format(DIR_USER_DATA, operation)
|
||||
|
||||
if os.path.isfile(self.file_name):
|
||||
self.load_settings()
|
||||
else:
|
||||
self.setup()
|
||||
self.store_settings()
|
||||
self.session_ec_ratio = self.e_correction_ratio
|
||||
self.session_if_speed = self.serial_iface_speed
|
||||
|
||||
def load_settings(self) -> None:
|
||||
"""Load persistent settings from file."""
|
||||
ensure_dir('{}/'.format(DIR_USER_DATA))
|
||||
settings = open(self.file_name, 'rb').read()
|
||||
self.serial_iface_speed = bytes_to_int(settings[0:8])
|
||||
self.e_correction_ratio = bytes_to_int(settings[8:16])
|
||||
self.serial_usb_adapter = bytes_to_bool(settings[16:17])
|
||||
self.disable_gui_dialog = bytes_to_bool(settings[17:18])
|
||||
|
||||
def store_settings(self) -> None:
|
||||
"""Store persistent settings to file."""
|
||||
setting_data = int_to_bytes(self.serial_iface_speed)
|
||||
setting_data += int_to_bytes(self.e_correction_ratio)
|
||||
setting_data += bool_to_bytes(self.serial_usb_adapter)
|
||||
setting_data += bool_to_bytes(self.disable_gui_dialog)
|
||||
ensure_dir('{}/'.format(DIR_USER_DATA))
|
||||
open(self.file_name, 'wb+').write(setting_data)
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Prompt user to enter initial settings."""
|
||||
if not self.local_testing_mode:
|
||||
self.serial_usb_adapter = yes("Does NH use USB-to-serial/TTL adapter?", tail=1)
|
|
@ -0,0 +1,102 @@
|
|||
#!/usr/bin/env python3.5
|
||||
# -*- 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 datetime
|
||||
import os
|
||||
import time
|
||||
import typing
|
||||
|
||||
from src.nh.misc import box_print
|
||||
from src.common.reed_solomon import ReedSolomonError, RSCodec
|
||||
from src.common.statics import *
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from multiprocessing import Queue
|
||||
from src.nh.gateway import Gateway
|
||||
from src.nh.settings import Settings
|
||||
|
||||
|
||||
def txm_incoming(settings: 'Settings',
|
||||
q_to_tip: 'Queue',
|
||||
q_to_rxm: 'Queue',
|
||||
q_to_im: 'Queue',
|
||||
q_to_nh: 'Queue') -> None:
|
||||
"""Load messages from TxM and forward them to appropriate process via queue."""
|
||||
rs = RSCodec(2 * settings.session_ec_ratio)
|
||||
|
||||
while True:
|
||||
try:
|
||||
if q_to_tip.empty():
|
||||
time.sleep(0.001)
|
||||
packet = q_to_tip.get()
|
||||
|
||||
try:
|
||||
packet = bytes(rs.decode(packet))
|
||||
except ReedSolomonError:
|
||||
box_print(["Warning! Failed to correct errors in received packet."], head=1, tail=1)
|
||||
continue
|
||||
|
||||
ts = datetime.datetime.now().strftime(settings.t_fmt)
|
||||
header = packet[:1]
|
||||
|
||||
if header == UNENCRYPTED_PACKET_HEADER:
|
||||
q_to_nh.put(packet[1:])
|
||||
|
||||
elif header in [LOCAL_KEY_PACKET_HEADER, COMMAND_PACKET_HEADER]:
|
||||
p_type = 'local key' if header == LOCAL_KEY_PACKET_HEADER else 'command'
|
||||
print("{} - {} TxM > RxM".format(ts, p_type))
|
||||
q_to_rxm.put(packet)
|
||||
|
||||
elif header in [MESSAGE_PACKET_HEADER, PUBLIC_KEY_PACKET_HEADER]:
|
||||
payload_len, p_type = (32, 'pub key') if header == PUBLIC_KEY_PACKET_HEADER else (344, 'message')
|
||||
payload = packet[1:1 + payload_len]
|
||||
trailer = packet[1 + payload_len:]
|
||||
user, contact = trailer.split(US_BYTE)
|
||||
|
||||
print("{} - {} TxM > {} > {}".format(ts, p_type, user.decode(), contact.decode()))
|
||||
q_to_im.put((header, payload, user, contact))
|
||||
q_to_rxm.put(header + payload + ORIGIN_USER_HEADER + contact)
|
||||
|
||||
elif header == EXPORTED_FILE_CT_HEADER:
|
||||
payload = packet[1:]
|
||||
file_name = os.urandom(16).hex()
|
||||
with open(file_name, 'wb+') as f:
|
||||
f.write(payload)
|
||||
print("{} - Exported file from TxM as {}".format(ts, file_name))
|
||||
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
|
||||
def rxm_outgoing(settings: 'Settings',
|
||||
q_to_rxm: 'Queue',
|
||||
gateway: 'Gateway') -> None:
|
||||
"""Output packets from RxM-queue to RxM."""
|
||||
rs = RSCodec(2 * settings.session_ec_ratio)
|
||||
while True:
|
||||
try:
|
||||
if q_to_rxm.empty():
|
||||
time.sleep(0.001)
|
||||
continue
|
||||
from_q = q_to_rxm.get()
|
||||
packet = rs.encode(bytearray(from_q))
|
||||
gateway.write(packet)
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
pass
|
|
@ -0,0 +1,321 @@
|
|||
#!/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
|
||||
|
||||
from src.common.db_logs import access_history, re_encrypt
|
||||
from src.common.encoding import bytes_to_int
|
||||
from src.common.errors import FunctionReturn, graceful_exit
|
||||
from src.common.misc import clear_screen, ensure_dir, validate_nick
|
||||
from src.common.output import box_print, 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 ecdhe_command, local_key_installed, psk_command, psk_import
|
||||
from src.rx.packet import decrypt_assembly_packet
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
from src.common.db_contacts import ContactList
|
||||
from src.common.db_groups import 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_packet_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, str]) -> None:
|
||||
"""Decrypt command assembly packet and process command."""
|
||||
|
||||
assembly_packet, account, origin = decrypt_assembly_packet(assembly_packet_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:
|
||||
return None
|
||||
|
||||
command = cmd_packet.assemble_command_packet()
|
||||
header = command[:2]
|
||||
cmd_data = command[2:]
|
||||
|
||||
# Keyword Function to run ( Parameters )
|
||||
# ---------------------------------------------------------------------------------------------------------------------------------------
|
||||
function_d = {LOCAL_KEY_INSTALLED_HEADER: (local_key_installed, ts, window_list, contact_list ),
|
||||
SHOW_WINDOW_ACTIVITY_HEADER: (show_win_activity, window_list ),
|
||||
WINDOW_CHANGE_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: (graceful_exit, ),
|
||||
LOG_DISPLAY_HEADER: (display_logs, cmd_data, window_list, contact_list, settings, master_key),
|
||||
LOG_EXPORT_HEADER: (export_logs, cmd_data, ts, window_list, contact_list, 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, group_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, 'L' ),
|
||||
CHANGE_FILE_R_HEADER: (contact_setting, cmd_data, ts, window_list, contact_list, group_list, 'F' ),
|
||||
CHANGE_NOTIFY_HEADER: (contact_setting, cmd_data, ts, window_list, contact_list, group_list, 'N' ),
|
||||
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_ECDHE_HEADER: (ecdhe_command, cmd_data, ts, window_list, contact_list, key_list, settings, pubkey_buf),
|
||||
KEY_EX_PSK_TX_HEADER: (psk_command, cmd_data, ts, window_list, contact_list, key_list, settings, pubkey_buf),
|
||||
KEY_EX_PSK_RX_HEADER: (psk_import, 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, )} # type: Dict[bytes, Any]
|
||||
|
||||
if header not in function_d:
|
||||
raise FunctionReturn("Received packet had an invalid command header.")
|
||||
|
||||
from_dict = function_d[header]
|
||||
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' and w.unread_messages > 0)]
|
||||
print_list = ["Window activity"] if unread_wins else ["No window activity"]
|
||||
for w in unread_wins:
|
||||
print_list.append(f"{w.name}: {w.unread_messages}")
|
||||
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 cmd_data == FILE_R_WIN_ID_BYTES:
|
||||
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()
|
||||
|
||||
|
||||
def display_logs(cmd_data: bytes,
|
||||
window_list: 'WindowList',
|
||||
contact_list: 'ContactList',
|
||||
settings: 'Settings',
|
||||
master_key: 'MasterKey') -> None:
|
||||
"""Display log file for active window."""
|
||||
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_history(window, contact_list, settings, master_key, msg_to_load=no_messages)
|
||||
|
||||
|
||||
def export_logs(cmd_data: bytes,
|
||||
ts: 'datetime',
|
||||
window_list: 'WindowList',
|
||||
contact_list: 'ContactList',
|
||||
settings: 'Settings',
|
||||
master_key: 'MasterKey') -> None:
|
||||
"""Export log file for active window."""
|
||||
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_history(window, contact_list, settings, master_key, msg_to_load=no_messages, export=True)
|
||||
|
||||
local_win = window_list.get_window('local')
|
||||
local_win.print_new(ts, f"Exported logfile of {window.type} {window.name}.")
|
||||
|
||||
|
||||
def change_master_key(ts: 'datetime',
|
||||
window_list: 'WindowList',
|
||||
contact_list: 'ContactList',
|
||||
group_list: 'GroupList',
|
||||
key_list: 'KeyList',
|
||||
settings: 'Settings',
|
||||
master_key: 'MasterKey') -> None:
|
||||
"""Derive new master key based on master password delivered by TxM."""
|
||||
old_master_key = master_key.master_key[:]
|
||||
master_key.new_master_key()
|
||||
new_master_key = master_key.master_key
|
||||
|
||||
ensure_dir(f'{DIR_USER_DATA}/')
|
||||
file_name = f'{DIR_USER_DATA}/{settings.software_operation}_logs'
|
||||
if os.path.isfile(file_name):
|
||||
phase("Re-encrypting log-file")
|
||||
re_encrypt(old_master_key, new_master_key, settings)
|
||||
phase('Done')
|
||||
|
||||
key_list.store_keys()
|
||||
settings.store_settings()
|
||||
contact_list.store_contacts()
|
||||
group_list.store_groups()
|
||||
|
||||
box_print("Master key successfully changed.", head=1)
|
||||
clear_screen(delay=1.5)
|
||||
|
||||
local_win = window_list.get_window('local')
|
||||
local_win.print_new(ts, "Changed RxM master key.", print_=False)
|
||||
|
||||
|
||||
def change_nick(cmd_data: bytes,
|
||||
ts: 'datetime',
|
||||
window_list: 'WindowList',
|
||||
contact_list: 'ContactList',
|
||||
group_list: 'GroupList') -> None:
|
||||
"""Change contact nick."""
|
||||
account, nick = [f.decode() for f in cmd_data.split(US_BYTE)]
|
||||
success, error_msg = validate_nick(nick, (contact_list, group_list, account))
|
||||
if not success:
|
||||
raise FunctionReturn(error_msg)
|
||||
|
||||
c_window = window_list.get_window(account)
|
||||
c_window.name = nick
|
||||
contact = contact_list.get_contact(account)
|
||||
contact.nick = nick
|
||||
contact_list.store_contacts()
|
||||
|
||||
cmd_win = window_list.get_local_window()
|
||||
cmd_win.print_new(ts, f"Changed {account} nick to {nick}.")
|
||||
|
||||
|
||||
def change_setting(cmd_data: bytes,
|
||||
ts: 'datetime',
|
||||
window_list: 'WindowList',
|
||||
contact_list: 'ContactList',
|
||||
group_list: 'GroupList',
|
||||
settings: 'Settings') -> None:
|
||||
"""Change TFC setting."""
|
||||
key, value = [f.decode() for f in cmd_data.split(US_BYTE)]
|
||||
|
||||
if key not in settings.key_list:
|
||||
raise FunctionReturn(f"Invalid setting {key}.")
|
||||
|
||||
settings.change_setting(key, value, contact_list, group_list)
|
||||
local_win = window_list.get_local_window()
|
||||
local_win.print_new(ts, f"Changed setting {key} to {value}.")
|
||||
|
||||
|
||||
def contact_setting(cmd_data: bytes,
|
||||
ts: 'datetime',
|
||||
window_list: 'WindowList',
|
||||
contact_list: 'ContactList',
|
||||
group_list: 'GroupList',
|
||||
setting_type: str) -> None:
|
||||
"""Change contact/group related setting."""
|
||||
attr = dict(L='log_messages',
|
||||
F='file_reception',
|
||||
N='notifications')[setting_type]
|
||||
|
||||
desc = dict(L='logging of messages',
|
||||
F='reception of files',
|
||||
N='message notifications')[setting_type]
|
||||
|
||||
if cmd_data[:1].islower():
|
||||
|
||||
setting, win_uid, = [f.decode() for f in cmd_data.split(US_BYTE)]
|
||||
|
||||
if not window_list.has_window(win_uid):
|
||||
raise FunctionReturn(f"Error: Found no window for {win_uid}.")
|
||||
|
||||
b_value, header = dict(e=(True, "Enabled"), d=(False, "Disabled"))[setting]
|
||||
window = window_list.get_window(win_uid)
|
||||
trailer = f"for {window.type} {window.name}"
|
||||
|
||||
if window.type == 'group' and setting_type == 'F':
|
||||
trailer = f"for members in group {window.name}"
|
||||
|
||||
if window.type == 'group':
|
||||
group = group_list.get_group(win_uid)
|
||||
if setting_type == 'F':
|
||||
for c in contact_list:
|
||||
c.file_reception = b_value
|
||||
contact_list.store_contacts()
|
||||
else:
|
||||
setattr(group, attr, b_value)
|
||||
group_list.store_groups()
|
||||
|
||||
elif window.type == 'contact':
|
||||
contact = contact_list.get_contact(win_uid)
|
||||
setattr(contact, attr, b_value)
|
||||
contact_list.store_contacts()
|
||||
|
||||
# For all
|
||||
else:
|
||||
setting = cmd_data[:1].decode()
|
||||
b_value, header = dict(E=(True, "Enabled"), D=(False, "Disabled"))[setting]
|
||||
trailer = "for all contacts" + (' and groups' if setting_type != 'F' else '')
|
||||
|
||||
for c in contact_list:
|
||||
setattr(c, attr, b_value)
|
||||
contact_list.store_contacts()
|
||||
|
||||
if setting_type != 'F':
|
||||
for g in group_list:
|
||||
setattr(g, attr, b_value)
|
||||
group_list.store_groups()
|
||||
|
||||
local_win = window_list.get_window('local')
|
||||
local_win.print_new(ts, f"{header} {desc} {trailer}.")
|
||||
|
||||
|
||||
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)
|
||||
|
||||
if rx_account in contact_list.get_list_of_accounts():
|
||||
nick = contact_list.get_contact(rx_account).nick
|
||||
contact_list.remove_contact(rx_account)
|
||||
box_print(f"Removed {nick} from contacts.", head=1, tail=1)
|
||||
|
||||
local_win = window_list.get_local_window()
|
||||
local_win.print_new(ts, f"Removed {nick} from RxM.", print_=False)
|
||||
|
||||
else:
|
||||
box_print(f"RxM has no account {rx_account} to remove.", head=1, tail=1)
|
||||
|
||||
if any([g.remove_members([rx_account]) for g in group_list]):
|
||||
box_print(f"Removed {rx_account} from group(s).", tail=1)
|
|
@ -0,0 +1,161 @@
|
|||
#!/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 typing
|
||||
|
||||
from src.common.errors import FunctionReturn
|
||||
from src.common.output import box_print, g_mgmt_print
|
||||
from src.common.statics import *
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
from src.common.db_contacts import ContactList
|
||||
from src.common.db_groups import GroupList
|
||||
from src.common.db_settings import Settings
|
||||
from src.rx.windows import WindowList
|
||||
|
||||
|
||||
def group_create(cmd_data: bytes,
|
||||
ts: 'datetime',
|
||||
window_list: 'WindowList',
|
||||
contact_list: 'ContactList',
|
||||
group_list: 'GroupList',
|
||||
settings: 'Settings') -> None:
|
||||
"""Create a new group."""
|
||||
fields = [f.decode() for f in cmd_data.split(US_BYTE)]
|
||||
group_name = fields[0]
|
||||
|
||||
purpaccs = set(fields[1:])
|
||||
accounts = set(contact_list.get_list_of_accounts())
|
||||
|
||||
accepted = list(accounts & purpaccs)
|
||||
rejected = list(purpaccs - accounts)
|
||||
|
||||
if len(accepted) > settings.m_members_in_group:
|
||||
raise FunctionReturn("Error: TFC settings only allow {} members per group.".format(settings.m_members_in_group))
|
||||
|
||||
if len(group_list) == settings.m_number_of_groups:
|
||||
raise FunctionReturn("Error: TFC settings only allow {} groups.".format(settings.m_number_of_groups))
|
||||
|
||||
a_contacts = [contact_list.get_contact(c) for c in accepted]
|
||||
group_list.add_group(group_name,
|
||||
settings.log_msg_by_default,
|
||||
settings.n_m_notify_privacy,
|
||||
a_contacts)
|
||||
|
||||
g_mgmt_print('new_g', accepted, contact_list, group_name)
|
||||
g_mgmt_print('unkwn', rejected, contact_list, group_name)
|
||||
|
||||
# Reset members in window.
|
||||
window = window_list.get_window(group_name)
|
||||
window.window_contacts = a_contacts
|
||||
window.message_log = []
|
||||
window.unread_messages = 0
|
||||
|
||||
local_win = window_list.get_window('local')
|
||||
local_win.print_new(ts, f"Created new group {group_name}.", print_=False)
|
||||
|
||||
|
||||
def group_add_member(cmd_data: bytes,
|
||||
ts: 'datetime',
|
||||
window_list: 'WindowList',
|
||||
contact_list: 'ContactList',
|
||||
group_list: 'GroupList',
|
||||
settings: 'Settings') -> None:
|
||||
"""Add member(s) to group."""
|
||||
fields = [f.decode() for f in cmd_data.split(US_BYTE)]
|
||||
group_name = fields[0]
|
||||
|
||||
purpaccs = set(fields[1:])
|
||||
accounts = set(contact_list.get_list_of_accounts())
|
||||
before_a = set(group_list.get_group(group_name).get_list_of_member_accounts())
|
||||
ok_accos = set(accounts & purpaccs)
|
||||
new_in_g = set(ok_accos - before_a)
|
||||
|
||||
e_asmbly = list(before_a | new_in_g)
|
||||
rejected = list(purpaccs - accounts)
|
||||
in_alrdy = list(before_a & purpaccs)
|
||||
n_in_g_l = list(new_in_g)
|
||||
|
||||
if len(e_asmbly) > settings.m_members_in_group:
|
||||
raise FunctionReturn("Error: TFC settings only allow {} members per group.".format(settings.m_members_in_group))
|
||||
|
||||
group = group_list.get_group(group_name)
|
||||
group.add_members([contact_list.get_contact(a) for a in new_in_g])
|
||||
|
||||
g_mgmt_print('add_m', n_in_g_l, contact_list, group_name)
|
||||
g_mgmt_print('add_a', in_alrdy, contact_list, group_name)
|
||||
g_mgmt_print('unkwn', rejected, contact_list, group_name)
|
||||
|
||||
window = window_list.get_window(group_name)
|
||||
window.add_contacts(n_in_g_l)
|
||||
|
||||
local_win = window_list.get_window('local')
|
||||
local_win.print_new(ts, f"Added members to group {group_name}.", print_=False)
|
||||
|
||||
|
||||
def group_rm_member(cmd_data: bytes,
|
||||
ts: 'datetime',
|
||||
window_list: 'WindowList',
|
||||
contact_list: 'ContactList',
|
||||
group_list: 'GroupList') -> None:
|
||||
"""Remove member(s) from group."""
|
||||
fields = [f.decode() for f in cmd_data.split(US_BYTE)]
|
||||
group_name = fields[0]
|
||||
|
||||
purpaccs = set(fields[1:])
|
||||
accounts = set(contact_list.get_list_of_accounts())
|
||||
before_r = set(group_list.get_group(group_name).get_list_of_member_accounts())
|
||||
ok_accos = set(purpaccs & accounts)
|
||||
remove_s = set(before_r & ok_accos)
|
||||
|
||||
not_in_g = list(ok_accos - before_r)
|
||||
rejected = list(purpaccs - accounts)
|
||||
remove_l = list(remove_s)
|
||||
|
||||
group = group_list.get_group(group_name)
|
||||
group.remove_members(remove_l)
|
||||
|
||||
g_mgmt_print('rem_m', remove_l, contact_list, group_name)
|
||||
g_mgmt_print('rem_n', not_in_g, contact_list, group_name)
|
||||
g_mgmt_print('unkwn', rejected, contact_list, group_name)
|
||||
|
||||
window = window_list.get_window(group_name)
|
||||
window.remove_contacts(remove_l)
|
||||
|
||||
local_win = window_list.get_window('local')
|
||||
local_win.print_new(ts, f"Removed members from group {group_name}.", print_=False)
|
||||
|
||||
|
||||
def remove_group(cmd_data: bytes,
|
||||
ts: 'datetime',
|
||||
window_list: 'WindowList',
|
||||
group_list: 'GroupList') -> None:
|
||||
"""Remove group."""
|
||||
group_name = cmd_data.decode()
|
||||
|
||||
if group_name not in group_list.get_list_of_group_names():
|
||||
raise FunctionReturn(f"RxM has no group {group_name} to remove.")
|
||||
|
||||
group_list.remove_group(group_name)
|
||||
|
||||
box_print(f"Removed group {group_name}", head=1, tail=1)
|
||||
local_win = window_list.get_window('local')
|
||||
local_win.print_new(ts, f"Removed group {group_name}.", print_=False )
|
|
@ -0,0 +1,144 @@
|
|||
#!/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 base64
|
||||
import binascii
|
||||
import os.path
|
||||
import typing
|
||||
import zlib
|
||||
|
||||
import nacl.exceptions
|
||||
|
||||
from src.common.crypto import auth_and_decrypt
|
||||
from src.common.encoding import bytes_to_str
|
||||
from src.common.errors import FunctionReturn
|
||||
from src.common.input import get_b58_key
|
||||
from src.common.misc import ensure_dir
|
||||
from src.common.output import box_print, c_print, phase, print_on_previous_line
|
||||
from src.common.statics import *
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
from src.rx.windows import WindowList
|
||||
|
||||
|
||||
def store_unique(file_data: bytes, f_dir: str, f_name: str) -> str:
|
||||
"""Store file under unique filename.
|
||||
|
||||
Add trailing counter .# to duplicate files.
|
||||
"""
|
||||
ensure_dir(f'{f_dir}/')
|
||||
|
||||
if os.path.isfile(f'{f_dir}/{f_name}'):
|
||||
f_name += '.1'
|
||||
while os.path.isfile(f'{f_dir}/{f_name}'):
|
||||
*name_parts, ctr = f_name.split('.')
|
||||
f_name = '.'.join(name_parts)
|
||||
f_name += ('.' + str(int(ctr) + 1))
|
||||
|
||||
with open('{}/{}'.format(f_dir, f_name), 'wb+') as f:
|
||||
f.write(file_data)
|
||||
return f_name
|
||||
|
||||
|
||||
def process_imported_file(ts: 'datetime',
|
||||
packet: bytes,
|
||||
window_list: 'WindowList'):
|
||||
"""Decrypt and store imported file."""
|
||||
while True:
|
||||
try:
|
||||
print('')
|
||||
key = get_b58_key('imported_file')
|
||||
phase("Decrypting file", head=1)
|
||||
file_pt = auth_and_decrypt(packet[1:], key, soft_e=True)
|
||||
phase("Done")
|
||||
break
|
||||
except nacl.exceptions.CryptoError:
|
||||
c_print("Invalid decryption key. Try again.", head=2)
|
||||
print_on_previous_line(reps=6, delay=1.5)
|
||||
except KeyboardInterrupt:
|
||||
raise FunctionReturn("File import aborted.")
|
||||
|
||||
try:
|
||||
phase("Decompressing file")
|
||||
file_dc = zlib.decompress(file_pt)
|
||||
phase("Done")
|
||||
except zlib.error:
|
||||
raise FunctionReturn("Decompression of file data failed.")
|
||||
|
||||
try:
|
||||
f_name = bytes_to_str(file_dc[:1024])
|
||||
except UnicodeError:
|
||||
raise FunctionReturn("Received file had an invalid name.")
|
||||
|
||||
if not f_name.isprintable():
|
||||
raise FunctionReturn("Received file had an invalid name.")
|
||||
|
||||
f_data = file_dc[1024:]
|
||||
final_name = store_unique(f_data, DIR_IMPORTED, f_name)
|
||||
|
||||
message = "Stored imported file to {}/{}".format(DIR_IMPORTED, final_name)
|
||||
box_print(message, head=1)
|
||||
|
||||
local_win = window_list.get_local_window()
|
||||
local_win.print_new(ts, message, print_=False)
|
||||
|
||||
|
||||
def process_received_file(payload: bytes, nick: str) -> None:
|
||||
"""Process received file assembly packets"""
|
||||
try:
|
||||
f_name, _, _, f_data = payload.split(US_BYTE)
|
||||
except ValueError:
|
||||
raise FunctionReturn("Received file had invalid structure.")
|
||||
|
||||
try:
|
||||
f_name_d = f_name.decode()
|
||||
except UnicodeError:
|
||||
raise FunctionReturn("Received file had an invalid name.")
|
||||
|
||||
if not f_name_d.isprintable():
|
||||
raise FunctionReturn("Received file had an invalid name.")
|
||||
|
||||
try:
|
||||
f_data = base64.b85decode(f_data)
|
||||
except (binascii.Error, ValueError):
|
||||
raise FunctionReturn("Received file had invalid encoding.")
|
||||
|
||||
file_ct = f_data[:-32]
|
||||
file_key = f_data[-32:]
|
||||
if len(file_key) != 32:
|
||||
raise FunctionReturn("Received file had an invalid key.")
|
||||
|
||||
try:
|
||||
file_pt = auth_and_decrypt(file_ct, file_key, soft_e=True)
|
||||
except nacl.exceptions.CryptoError:
|
||||
raise FunctionReturn("Decryption of file data failed.")
|
||||
|
||||
try:
|
||||
file_dc = zlib.decompress(file_pt)
|
||||
except zlib.error:
|
||||
raise FunctionReturn("Decompression of file data failed.")
|
||||
|
||||
if len(file_dc) == 0:
|
||||
raise FunctionReturn("Received file did not contain data.")
|
||||
|
||||
f_dir = f'{DIR_RX_FILES}/{nick}'
|
||||
final_name = store_unique(file_dc, f_dir, f_name_d)
|
||||
box_print(["Stored file from {} as {}.".format(nick, final_name)])
|
|
@ -0,0 +1,261 @@
|
|||
#!/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 time
|
||||
import typing
|
||||
|
||||
from typing import Dict
|
||||
|
||||
import nacl.exceptions
|
||||
|
||||
from src.common.crypto import argon2_kdf, auth_and_decrypt
|
||||
from src.common.db_masterkey import MasterKey
|
||||
from src.common.encoding import b58encode
|
||||
from src.common.errors import FunctionReturn
|
||||
from src.common.input import get_b58_key
|
||||
from src.common.misc import clear_screen, split_string
|
||||
from src.common.output import box_print, c_print, phase, 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(packet: bytes,
|
||||
contact_list: 'ContactList',
|
||||
key_list: 'KeyList') -> None:
|
||||
"""Decrypt local key packet, add local contact/keyset."""
|
||||
try:
|
||||
clear_screen()
|
||||
box_print(["Received encrypted local key"], tail=1)
|
||||
|
||||
kdk = get_b58_key('localkey')
|
||||
|
||||
try:
|
||||
pt = auth_and_decrypt(packet[1:], key=kdk, soft_e=True)
|
||||
except nacl.exceptions.CryptoError:
|
||||
raise FunctionReturn("Invalid key decryption key.", 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', 'local', 'local',
|
||||
bytes(32), bytes(32),
|
||||
False, False, True)
|
||||
|
||||
# Add local contact to keyset database
|
||||
key_list.add_keyset('local', key, bytes(32), hek, bytes(32))
|
||||
box_print([f"Confirmation code for TxM: {conf_code.hex()}"], head=1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
raise FunctionReturn("Local key setup aborted.", delay=1)
|
||||
|
||||
|
||||
def local_key_installed(ts: 'datetime',
|
||||
window_list: 'WindowList',
|
||||
contact_list: 'ContactList') -> None:
|
||||
"""Clear local key bootstrap process from screen."""
|
||||
local_win = window_list.get_window('local')
|
||||
local_win.print_new(ts, "Created a new local key.", print_=False)
|
||||
|
||||
box_print(["Successfully added a new local key."])
|
||||
clear_screen(delay=1)
|
||||
|
||||
if not contact_list.has_contacts():
|
||||
clear_screen()
|
||||
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, str]) -> None:
|
||||
"""Display public from contact."""
|
||||
pub_key = packet[1:33]
|
||||
origin = packet[33:34]
|
||||
account = packet[34:].decode()
|
||||
|
||||
if origin == ORIGIN_CONTACT_HEADER:
|
||||
pub_key_enc = b58encode(pub_key)
|
||||
ssl = {48: 8, 49: 7, 50: 5}.get(len(pub_key_enc), 5)
|
||||
pub_key_enc = pub_key_enc if settings.local_testing_mode else ' '.join(split_string(pub_key_enc, item_len=ssl))
|
||||
|
||||
pubkey_buf[account] = pub_key_enc
|
||||
|
||||
box_print([f"Received public key from {account}", '', pubkey_buf[account]], head=1, tail=1)
|
||||
|
||||
local_win = window_list.get_local_window()
|
||||
local_win.print_new(ts, f"Received public key from {account}: {pub_key_enc}", print_=False)
|
||||
|
||||
if origin == ORIGIN_USER_HEADER and account in pubkey_buf:
|
||||
clear_screen()
|
||||
box_print([f"Public key for {account}", '', pubkey_buf[account]], head=1, tail=1)
|
||||
|
||||
|
||||
def ecdhe_command(cmd_data: bytes,
|
||||
ts: 'datetime',
|
||||
window_list: 'WindowList',
|
||||
contact_list: 'ContactList',
|
||||
key_list: 'KeyList',
|
||||
settings: 'Settings',
|
||||
pubkey_buf: Dict[str, str]) -> None:
|
||||
"""Add contact and it's X25519 keys."""
|
||||
tx_key = cmd_data[0:32]
|
||||
tx_hek = cmd_data[32:64]
|
||||
rx_key = cmd_data[64:96]
|
||||
rx_hek = cmd_data[96:128]
|
||||
|
||||
account, nick = [f.decode() for f in cmd_data[128:].split(US_BYTE)]
|
||||
|
||||
contact_list.add_contact(account, 'user_placeholder', nick,
|
||||
bytes(32), bytes(32),
|
||||
settings.log_msg_by_default,
|
||||
settings.store_file_default,
|
||||
settings.n_m_notify_privacy)
|
||||
|
||||
key_list.add_keyset(account, tx_key, rx_key, tx_hek, rx_hek)
|
||||
|
||||
pubkey_buf.pop(account, None)
|
||||
|
||||
local_win = window_list.get_window('local')
|
||||
local_win.print_new(ts, f"Added X25519 keys for {nick} ({account}).", print_=False)
|
||||
|
||||
box_print([f"Successfully added {nick}."])
|
||||
clear_screen(delay=1)
|
||||
|
||||
|
||||
###############################################################################
|
||||
# PSK #
|
||||
###############################################################################
|
||||
|
||||
def psk_command(cmd_data: bytes,
|
||||
ts: 'datetime',
|
||||
window_list: 'WindowList',
|
||||
contact_list: 'ContactList',
|
||||
key_list: 'KeyList',
|
||||
settings: 'Settings',
|
||||
pubkey_buf: Dict[str, str]) -> 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, 'user_placeholder', nick,
|
||||
bytes(32), bytes(32),
|
||||
settings.log_msg_by_default,
|
||||
settings.store_file_default,
|
||||
settings.n_m_notify_privacy)
|
||||
|
||||
# The Rx-side keys are set as null-byte strings to indicate they have not been added yet.
|
||||
key_list.add_keyset(account, tx_key, bytes(32), tx_hek, bytes(32))
|
||||
|
||||
pubkey_buf.pop(account, None)
|
||||
|
||||
local_win = window_list.get_window('local')
|
||||
local_win.print_new(ts, f"Added Tx-PSK for {nick} ({account}).", print_=False)
|
||||
|
||||
box_print([f"Successfully added {nick}."])
|
||||
clear_screen(delay=1)
|
||||
|
||||
|
||||
def psk_import(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"Unknown account {account}.")
|
||||
|
||||
contact = contact_list.get_contact(account)
|
||||
pskf = ask_path_gui(f"Select PSK for {contact.nick}", settings, get_file=True)
|
||||
|
||||
with open(pskf, 'rb') as f:
|
||||
psk_data = f.read()
|
||||
|
||||
if len(psk_data) != 136: # Nonce (24) + Salt (32) + rx-key (32) + rx-hek (32) + tag (16)
|
||||
raise FunctionReturn("Invalid PSK data in file.")
|
||||
|
||||
salt = psk_data[:32]
|
||||
ct_tag = psk_data[32:]
|
||||
|
||||
while True:
|
||||
try:
|
||||
password = MasterKey.get_password("PSK password")
|
||||
phase("Deriving key decryption key", head=2)
|
||||
kdk, _ = argon2_kdf(password, salt, rounds=16, memory=128000, 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.")
|
||||
|
||||
rx_key = psk_pt[0:32]
|
||||
rx_hek = psk_pt[32:64]
|
||||
|
||||
if rx_key == bytes(32) or rx_hek == bytes(32):
|
||||
raise FunctionReturn("Keys from contact are not valid.")
|
||||
|
||||
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
|
||||
# is trusted (user's own TxM) but it's still good practice.
|
||||
subprocess.Popen("shred -n 3 -z -u {}".format(pipes.quote(pskf)), shell=True).wait()
|
||||
if os.path.isfile(pskf):
|
||||
box_print(f"Warning! Overwriting of PSK ({pskf}) failed.")
|
||||
time.sleep(3)
|
||||
|
||||
local_win = window_list.get_local_window()
|
||||
local_win.print_new(ts, f"Added Rx-PSK for {contact.nick} ({account})", print_=False)
|
||||
|
||||
box_print([f"Added Rx-PSK for {contact.nick}.", '', "Warning!",
|
||||
"Physically destroy the keyfile transmission ",
|
||||
"media to ensure that no data escapes RxM!"], head=1, tail=1)
|
|
@ -0,0 +1,187 @@
|
|||
#!/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 struct
|
||||
import typing
|
||||
|
||||
from src.common.db_logs import write_log_entry
|
||||
from src.common.encoding import bytes_to_double
|
||||
from src.common.errors import FunctionReturn
|
||||
from src.common.output import box_print
|
||||
from src.common.statics import *
|
||||
from src.rx.packet import decrypt_assembly_packet
|
||||
|
||||
from typing import List
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
from src.common.db_contacts import ContactList
|
||||
from src.common.db_groups import 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_message(ts: 'datetime',
|
||||
assembly_packet_ct: bytes,
|
||||
window_list: 'WindowList',
|
||||
packet_list: 'PacketList',
|
||||
contact_list: 'ContactList',
|
||||
key_list: 'KeyList',
|
||||
group_list: 'GroupList',
|
||||
settings: 'Settings',
|
||||
master_key: 'MasterKey') -> None:
|
||||
"""Process received private / group message.
|
||||
|
||||
Group management messages have automatic formatting and window
|
||||
redirection based on group configuration managed by user.
|
||||
"""
|
||||
assembly_packet, account, origin = decrypt_assembly_packet(assembly_packet_ct, window_list, contact_list, key_list)
|
||||
|
||||
p_type = 'file' if assembly_packet[:1].isupper() else 'message'
|
||||
packet = packet_list.get_packet(account, origin, p_type)
|
||||
packet.add_packet(assembly_packet)
|
||||
|
||||
if not packet.is_complete:
|
||||
return None
|
||||
|
||||
if p_type == 'file':
|
||||
packet.assemble_and_store_file()
|
||||
|
||||
if contact_list.get_contact(account).log_messages and settings.log_dummy_file_a_p:
|
||||
# Store placeholder data.
|
||||
for _ in packet.assembly_pt_list:
|
||||
place_holder = F_S_HEADER + bytes(255)
|
||||
write_log_entry(place_holder, account, settings, master_key, origin)
|
||||
|
||||
if p_type == 'message':
|
||||
assembled = packet.assemble_message_packet()
|
||||
header = assembled[:1]
|
||||
assembled = assembled[1:]
|
||||
|
||||
# Messages to group
|
||||
|
||||
if header == GROUP_MESSAGE_HEADER:
|
||||
|
||||
try:
|
||||
timestamp = bytes_to_double(assembled[:8])
|
||||
except struct.error:
|
||||
raise FunctionReturn("Received an invalid group timestamp.")
|
||||
|
||||
try:
|
||||
group_name = assembled[8:].split(US_BYTE)[0].decode()
|
||||
except (UnicodeError, IndexError):
|
||||
raise FunctionReturn("Group name had invalid encoding.")
|
||||
|
||||
try:
|
||||
group_message = assembled[8:].split(US_BYTE)[1]
|
||||
except (ValueError, IndexError):
|
||||
raise FunctionReturn("Received an invalid group message.")
|
||||
|
||||
if not group_list.has_group(group_name):
|
||||
raise FunctionReturn("Received message to unknown group.", output=False)
|
||||
|
||||
window = window_list.get_window(group_name)
|
||||
group = group_list.get_group(group_name)
|
||||
|
||||
if not group.has_member(account):
|
||||
raise FunctionReturn("Group message to group contact is not member of.", output=False)
|
||||
|
||||
if window.has_contact(account):
|
||||
# All copies of group messages user sends to members contain same timestamp header.
|
||||
# This allows RxM to ignore copies of messages sent by the user.
|
||||
if origin == ORIGIN_USER_HEADER:
|
||||
if window.group_timestamp == timestamp:
|
||||
return None
|
||||
window.group_timestamp = timestamp
|
||||
window.print_new(ts, group_message.decode(), account, origin)
|
||||
|
||||
if group_list.get_group(group_name).log_messages:
|
||||
for p in packet.assembly_pt_list:
|
||||
write_log_entry(p, account, settings, master_key, origin)
|
||||
return None
|
||||
|
||||
# Messages to contact
|
||||
|
||||
else:
|
||||
if header == PRIVATE_MESSAGE_HEADER:
|
||||
window = window_list.get_window(account)
|
||||
window.print_new(ts, assembled.decode(), account, origin)
|
||||
|
||||
# Group management messages
|
||||
else:
|
||||
local_win = window_list.get_local_window()
|
||||
nick = contact_list.get_contact(account).nick
|
||||
|
||||
group_name, *members = [f.decode() for f in assembled.split(US_BYTE)]
|
||||
|
||||
# Ignore group management messages from user
|
||||
if origin == ORIGIN_USER_HEADER:
|
||||
return None
|
||||
|
||||
if header == GROUP_MSG_INVITATION_HEADER:
|
||||
action = 'invited you to'
|
||||
if group_list.has_group(group_name) and group_list.get_group(group_name).has_member(account):
|
||||
action = 'joined'
|
||||
message = ["{} has {} group '{}'".format(nick, action, group_name)] # type: List[str]
|
||||
lw_msg = "{} has {} group '{}'".format(nick, action, group_name) # type: str
|
||||
|
||||
# Print group management message
|
||||
if members:
|
||||
message[0] += " with following members:"
|
||||
known = [contact_list.get_contact(m).nick for m in members if contact_list.has_contact(m)]
|
||||
unknown = [m for m in members if not contact_list.has_contact(m)]
|
||||
just_len = len(max(known + unknown, key=len))
|
||||
message += [" * {}".format(m.ljust(just_len)) for m in (known + unknown)]
|
||||
lw_msg += " with members " + ", ".join(known + unknown)
|
||||
|
||||
box_print(message, head=1, tail=1)
|
||||
|
||||
# Persistent message in cmd window
|
||||
local_win.print_new(ts, lw_msg, print_=False)
|
||||
|
||||
elif header in [GROUP_MSG_ADD_NOTIFY_HEADER, GROUP_MSG_MEMBER_RM_HEADER]:
|
||||
action = "added following member(s) to" if header == GROUP_MSG_ADD_NOTIFY_HEADER else "removed following member(s) from"
|
||||
message_ = ["{} has {} group {}: ".format(nick, action, group_name)] # type: List[str]
|
||||
lw_msg_ = "{} has {} group {}: ".format(nick, action, group_name) # type: str
|
||||
|
||||
if members:
|
||||
known = [contact_list.get_contact(m).nick for m in members if contact_list.has_contact(m)]
|
||||
unknown = [m for m in members if not contact_list.has_contact(m)]
|
||||
just_len = len(max(known + unknown, key=len))
|
||||
lw_msg_ += ", ".join(known + unknown)
|
||||
message_ += [" * {}".format(m.ljust(just_len)) for m in (known + unknown)]
|
||||
|
||||
box_print(message_, head=1, tail=1)
|
||||
local_win.print_new(ts, lw_msg_, print_=False)
|
||||
|
||||
elif header == GROUP_MSG_EXIT_GROUP_HEADER:
|
||||
box_print(["{} has left group {}.".format(nick, group_name), '', 'Warning',
|
||||
"Unless you remove the contact from the group, they",
|
||||
"can still decrypt messages you send to the group."],
|
||||
head=1, tail=1)
|
||||
else:
|
||||
raise FunctionReturn(f"Message from had invalid header.")
|
||||
|
||||
if contact_list.get_contact(account).log_messages:
|
||||
for p in packet.assembly_pt_list:
|
||||
write_log_entry(p, account, settings, master_key, origin)
|
|
@ -0,0 +1,299 @@
|
|||
#!/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 struct
|
||||
import typing
|
||||
import zlib
|
||||
|
||||
from typing import List, Tuple
|
||||
|
||||
import nacl.exceptions
|
||||
|
||||
from src.common.crypto import auth_and_decrypt, hash_chain, rm_padding_bytes
|
||||
from src.common.encoding import bytes_to_int
|
||||
from src.common.errors import FunctionReturn
|
||||
from src.common.input import yes
|
||||
from src.common.output import box_print, c_print
|
||||
from src.common.statics import *
|
||||
from src.rx.files import process_received_file
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.common.db_contacts import Contact, ContactList
|
||||
from src.common.db_keys import KeyList
|
||||
from src.common.db_settings import Settings
|
||||
from src.rx.windows import WindowList
|
||||
|
||||
|
||||
def decrypt_assembly_packet(packet: bytes, # Received packet
|
||||
window_list: 'WindowList',
|
||||
contact_list: 'ContactList',
|
||||
key_list: 'KeyList') -> Tuple[bytes, str, bytes]:
|
||||
"""Decrypt assembly packet from contact/local TxM."""
|
||||
message = packet[:1] == MESSAGE_PACKET_HEADER
|
||||
enc_harac = packet[1:49]
|
||||
enc_msg = packet[49:345]
|
||||
|
||||
# Set packet-related variables
|
||||
|
||||
if message:
|
||||
origin = packet[345:346]
|
||||
account = packet[346:].decode()
|
||||
|
||||
if origin not in [ORIGIN_CONTACT_HEADER, ORIGIN_USER_HEADER]:
|
||||
raise FunctionReturn("Received packet had an invalid origin-header.")
|
||||
|
||||
direction, key_dir = {ORIGIN_USER_HEADER: ("sent to", 'tx'), ORIGIN_CONTACT_HEADER: ("from", 'rx')}[origin]
|
||||
p_type = "packet"
|
||||
nick = contact_list.get_contact(account).nick
|
||||
|
||||
else:
|
||||
origin = ORIGIN_USER_HEADER
|
||||
account = 'local'
|
||||
direction = "from"
|
||||
key_dir = 'tx'
|
||||
p_type = 'command'
|
||||
nick = "local TxM"
|
||||
|
||||
window = window_list.get_local_window()
|
||||
keyset = key_list.get_keyset(account)
|
||||
|
||||
if keyset.rx_hek == bytes(32) and origin == ORIGIN_CONTACT_HEADER:
|
||||
raise FunctionReturn(f"Warning! Received {p_type} from {nick} but no PSK exists.", window=window)
|
||||
|
||||
# Decrypt hash ratchet counter
|
||||
|
||||
try:
|
||||
header_key = getattr(keyset, f'{key_dir}_hek')
|
||||
harac_bytes = auth_and_decrypt(enc_harac, header_key, soft_e=True)
|
||||
except nacl.exceptions.CryptoError:
|
||||
raise FunctionReturn(f"Warning! Received {p_type} {direction} {nick} had an invalid hash ratchet MAC.", window=window)
|
||||
|
||||
# Catch up with hash ratchet offset
|
||||
|
||||
purp_harac = bytes_to_int(harac_bytes)
|
||||
stored_harac = getattr(keyset, f'{key_dir}_harac')
|
||||
|
||||
if stored_harac > purp_harac:
|
||||
raise FunctionReturn(f"Warning! Received {p_type} {direction} {nick} had an expired hash ratchet counter.", window=window)
|
||||
|
||||
key_candidate = getattr(keyset, f'{key_dir}_key')
|
||||
offset = purp_harac - stored_harac
|
||||
|
||||
if offset:
|
||||
box_print(f"Warning! {offset} {p_type}(s) {direction} {nick} were not received.")
|
||||
|
||||
if offset > 1000 and origin == ORIGIN_CONTACT_HEADER:
|
||||
box_print([f"This might indicate that {offset} packets have been lost since last received ",
|
||||
f"message, or that the contact is attempting a denial of service attack.",
|
||||
f"You can catch up with key offset but this might take a long time or forever."])
|
||||
if not yes("Proceed with the key catchup?", tail=1):
|
||||
raise FunctionReturn(f"Dropped packet from {nick}.", window=window)
|
||||
|
||||
for _ in range(offset):
|
||||
key_candidate = hash_chain(key_candidate)
|
||||
|
||||
# Decrypt packet
|
||||
|
||||
try:
|
||||
assembly_packet = auth_and_decrypt(enc_msg, key_candidate, soft_e=True)
|
||||
except nacl.exceptions.CryptoError:
|
||||
raise FunctionReturn(f"Warning! Received {p_type} {direction} {nick} had an invalid MAC.", window=window)
|
||||
|
||||
# Update keys in database
|
||||
keyset.update_key(key_dir, hash_chain(key_candidate), offset + 1)
|
||||
|
||||
return assembly_packet, account, origin
|
||||
|
||||
|
||||
class Packet(object):
|
||||
"""Packet objects collect and keep track of received, related assembly packets."""
|
||||
|
||||
def __init__(self,
|
||||
account: str,
|
||||
contact: 'Contact',
|
||||
origin: bytes,
|
||||
type_: str,
|
||||
settings: 'Settings') -> None:
|
||||
"""Create new packet."""
|
||||
self.account = account
|
||||
self.contact = contact
|
||||
self.origin = origin
|
||||
self.type = type_
|
||||
self.settings = settings
|
||||
|
||||
# File information
|
||||
self.f_name = None # type: str
|
||||
self.f_size = None # type: str
|
||||
self.f_packets = None # type: int
|
||||
self.f_eta = None # type: str
|
||||
|
||||
self.sh = dict(message=M_S_HEADER, file=F_S_HEADER, command=C_S_HEADER)[self.type]
|
||||
self.lh = dict(message=M_L_HEADER, file=F_L_HEADER, command=C_L_HEADER)[self.type]
|
||||
self.ah = dict(message=M_A_HEADER, file=F_A_HEADER, command=C_A_HEADER)[self.type]
|
||||
self.eh = dict(message=M_E_HEADER, file=F_E_HEADER, command=C_E_HEADER)[self.type]
|
||||
self.ch = dict(message=M_C_HEADER, file=F_C_HEADER, command=C_C_HEADER)[self.type]
|
||||
self.nh = dict(message=P_N_HEADER, file=P_N_HEADER, command=C_N_HEADER)[self.type]
|
||||
|
||||
self.assembly_pt_list = [] # type: List[bytes]
|
||||
self.lt_active = False
|
||||
self.is_complete = False
|
||||
|
||||
def add_packet(self, packet: bytes) -> None:
|
||||
"""Add new assembly packet to the object"""
|
||||
header = packet[:1]
|
||||
|
||||
if header == self.sh:
|
||||
if self.type == 'file':
|
||||
if self.origin == ORIGIN_USER_HEADER:
|
||||
raise FunctionReturn("Ignored short file from user.", output=False)
|
||||
if not self.contact.file_reception:
|
||||
c_print("{} sent a file but file reception was disabled!".format(self.contact.nick), head=1, tail=1)
|
||||
raise FunctionReturn("Unauthorized short file from contact.", output=False)
|
||||
self.assembly_pt_list = [packet]
|
||||
self.lt_active = False
|
||||
self.is_complete = True
|
||||
|
||||
if header == self.lh:
|
||||
if self.type == 'file':
|
||||
if self.origin == ORIGIN_USER_HEADER:
|
||||
raise FunctionReturn("Ignored long file from user.", output=False)
|
||||
if not self.contact.file_reception:
|
||||
c_print("{} is sending file but file reception is disabled!".format(self.contact.nick), head=1, tail=1)
|
||||
raise FunctionReturn("Unauthorized long file from contact.", output=False)
|
||||
|
||||
try:
|
||||
self.f_packets = bytes_to_int(packet[1:9])
|
||||
self.f_name, self.f_size, self.f_eta, _ = [f.decode() for f in packet[9:].split(US_BYTE)]
|
||||
except (struct.error, ValueError, UnicodeError):
|
||||
self.assembly_pt_list = []
|
||||
self.lt_active = False
|
||||
self.is_complete = False
|
||||
raise FunctionReturn("Received packet had an invalid header.")
|
||||
|
||||
box_print(['Receiving file from {}:'.format(self.contact.nick),
|
||||
'{} ({})'.format(self.f_name, self.f_size),
|
||||
'ETA {} ({} packets)'.format(self.f_eta, self.f_packets)])
|
||||
packet = self.lh + packet[9:]
|
||||
|
||||
self.assembly_pt_list = [packet]
|
||||
self.lt_active = True
|
||||
self.is_complete = False
|
||||
|
||||
if header == self.ah:
|
||||
if not self.lt_active:
|
||||
raise FunctionReturn("Missing start packet.", output=False)
|
||||
self.assembly_pt_list.append(packet)
|
||||
|
||||
if header == self.eh:
|
||||
if not self.lt_active:
|
||||
raise FunctionReturn("Missing start packet.", output=False)
|
||||
self.assembly_pt_list.append(packet)
|
||||
self.lt_active = False
|
||||
self.is_complete = True
|
||||
|
||||
if header in [self.ch, self.nh]:
|
||||
if self.type == 'file':
|
||||
c_print("{} cancelled file.".format(self.contact.nick), head=1, tail=1)
|
||||
self.assembly_pt_list = []
|
||||
self.lt_active = False
|
||||
self.is_complete = False
|
||||
|
||||
def assemble_message_packet(self) -> bytes:
|
||||
"""Assemble message packet."""
|
||||
padded = b''.join([p[1:] for p in self.assembly_pt_list])
|
||||
payload = rm_padding_bytes(padded)
|
||||
|
||||
if len(self.assembly_pt_list) > 1:
|
||||
|
||||
if len(payload) < (24 + 1 + 16 + 32):
|
||||
raise FunctionReturn("Received invalid packet.")
|
||||
|
||||
msg_ct = payload[:-32]
|
||||
msg_key = payload[-32:]
|
||||
|
||||
try:
|
||||
payload = auth_and_decrypt(msg_ct, msg_key, soft_e=True)
|
||||
except (nacl.exceptions.CryptoError, nacl.exceptions.ValueError):
|
||||
raise FunctionReturn("Decryption of long message failed.")
|
||||
|
||||
try:
|
||||
payload = zlib.decompress(payload)
|
||||
except zlib.error:
|
||||
raise FunctionReturn("Decompression of long message failed.")
|
||||
|
||||
return payload
|
||||
|
||||
def assemble_and_store_file(self) -> None:
|
||||
"""Assemble file packet and store it to file."""
|
||||
padded = b''.join([p[1:] for p in self.assembly_pt_list])
|
||||
payload = rm_padding_bytes(padded)
|
||||
process_received_file(payload, self.contact.nick)
|
||||
|
||||
|
||||
def assemble_command_packet(self) -> bytes:
|
||||
"""Assemble command packet."""
|
||||
padded = b''.join([p[1:] for p in self.assembly_pt_list])
|
||||
payload = rm_padding_bytes(padded)
|
||||
|
||||
if len(self.assembly_pt_list) > 1:
|
||||
cmd_hash = payload[-32:]
|
||||
payload = payload[:-32]
|
||||
if hash_chain(payload) != cmd_hash:
|
||||
raise FunctionReturn("Received an invalid command.")
|
||||
|
||||
return zlib.decompress(payload)
|
||||
|
||||
|
||||
class PacketList(object):
|
||||
"""PacketList manages all file, message, and command packets."""
|
||||
|
||||
def __init__(self, contact_list: 'ContactList', settings: 'Settings') -> None:
|
||||
"""Create a new packet list object."""
|
||||
self.contact_list = contact_list
|
||||
self.settings = settings
|
||||
self.packet_l = [] # type: List[Packet]
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over packet list."""
|
||||
for p in self.packet_l:
|
||||
yield p
|
||||
|
||||
def __len__(self):
|
||||
"""Return number of packets in packet list."""
|
||||
return len(self.packet_l)
|
||||
|
||||
def has_packet(self, account: str, origin: bytes, type_: str) -> bool:
|
||||
"""Return True if packet object for account exists, else False."""
|
||||
return any(p for p in self.packet_l if (p.account == account
|
||||
and p.origin == origin
|
||||
and p.type == type_))
|
||||
|
||||
def get_packet(self, account: str, origin: bytes, type_: str) -> Packet:
|
||||
"""Get packet based on account, origin and type.
|
||||
|
||||
If packet does not exist, create it.
|
||||
"""
|
||||
if not self.has_packet(account, origin, type_):
|
||||
contact = self.contact_list.get_contact(account)
|
||||
self.packet_l.append(Packet(account, contact, origin, type_, self.settings))
|
||||
|
||||
return next(p for p in self.packet_l if (p.account == account
|
||||
and p.origin == origin
|
||||
and p.type == type_))
|
|
@ -0,0 +1,62 @@
|
|||
#!/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 datetime
|
||||
import time
|
||||
import typing
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from src.common.output import box_print
|
||||
from src.common.reed_solomon import ReedSolomonError, RSCodec
|
||||
from src.common.statics import *
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from multiprocessing import Queue
|
||||
from src.common.db_settings import Settings
|
||||
|
||||
|
||||
def receiver_loop(settings: 'Settings', queues: Dict[bytes, 'Queue']) -> None:
|
||||
"""Decode and queue received packets."""
|
||||
rs = RSCodec(2 * settings.session_ec_ratio)
|
||||
gw_queue = queues[GATEWAY_QUEUE]
|
||||
|
||||
while True:
|
||||
try:
|
||||
if gw_queue.empty():
|
||||
time.sleep(0.001)
|
||||
|
||||
packet = gw_queue.get()
|
||||
ts = datetime.datetime.now()
|
||||
|
||||
try:
|
||||
packet = bytes(rs.decode(bytearray(packet)))
|
||||
except ReedSolomonError:
|
||||
box_print(["Warning! Failed to correct errors in received packet."], head=1, tail=1)
|
||||
continue
|
||||
|
||||
p_header = packet[:1]
|
||||
if p_header in [PUBLIC_KEY_PACKET_HEADER, MESSAGE_PACKET_HEADER,
|
||||
LOCAL_KEY_PACKET_HEADER, COMMAND_PACKET_HEADER,
|
||||
IMPORTED_FILE_CT_HEADER]:
|
||||
queues[p_header].put((ts, packet))
|
||||
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
pass
|
|
@ -0,0 +1,123 @@
|
|||
#!/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 sys
|
||||
import time
|
||||
import typing
|
||||
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from src.common.errors import FunctionReturn
|
||||
from src.common.misc import clear_screen
|
||||
from src.common.statics import *
|
||||
from src.rx.commands import process_command
|
||||
from src.rx.files import process_imported_file
|
||||
from src.rx.key_exchanges import process_local_key, process_public_key
|
||||
from src.rx.messages import process_message
|
||||
from src.rx.packet import PacketList
|
||||
from src.rx.windows import WindowList
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
from multiprocessing import Queue
|
||||
from src.common.db_contacts import ContactList
|
||||
from src.common.db_groups import GroupList
|
||||
from src.common.db_keys import KeyList
|
||||
from src.common.db_masterkey import MasterKey
|
||||
from src.common.db_settings import Settings
|
||||
|
||||
|
||||
def rx_loop(settings: 'Settings',
|
||||
queues: Dict[bytes, 'Queue'],
|
||||
contact_list: 'ContactList',
|
||||
key_list: 'KeyList',
|
||||
group_list: 'GroupList',
|
||||
master_key: 'MasterKey',
|
||||
file_no: int # stdin file descriptor
|
||||
) -> None:
|
||||
"""Process received packets depending on their priorities."""
|
||||
l_queue = queues[LOCAL_KEY_PACKET_HEADER]
|
||||
p_queue = queues[PUBLIC_KEY_PACKET_HEADER]
|
||||
m_queue = queues[MESSAGE_PACKET_HEADER]
|
||||
c_queue = queues[COMMAND_PACKET_HEADER]
|
||||
f_queue = queues[IMPORTED_FILE_CT_HEADER]
|
||||
|
||||
packet_buf = dict() # type: Dict[str, List[Tuple[datetime, bytes]]]
|
||||
pubkey_buf = dict() # type: Dict[str, str]
|
||||
sys.stdin = os.fdopen(file_no)
|
||||
packet_list = PacketList(contact_list, settings)
|
||||
window_list = WindowList(contact_list, group_list, packet_list, settings)
|
||||
|
||||
clear_screen()
|
||||
while True:
|
||||
try:
|
||||
if not l_queue.empty():
|
||||
ts, packet = l_queue.get()
|
||||
process_local_key(packet, contact_list, key_list)
|
||||
|
||||
if not contact_list.has_local_contact():
|
||||
time.sleep(0.01)
|
||||
continue
|
||||
|
||||
if not p_queue.empty():
|
||||
ts, packet = p_queue.get()
|
||||
process_public_key(ts, packet, window_list, settings, pubkey_buf)
|
||||
continue
|
||||
|
||||
if not c_queue.empty():
|
||||
ts, packet = c_queue.get()
|
||||
process_command(ts, packet, window_list, packet_list, contact_list, key_list, group_list, settings, master_key, pubkey_buf)
|
||||
continue
|
||||
|
||||
if window_list.active_win is not None and window_list.active_win.uid == FILE_R_WIN_ID_BYTES.decode():
|
||||
window_list.active_win.redraw()
|
||||
|
||||
# Check if keys have been added by contact and process all messages immediately.
|
||||
for rx_account in packet_buf:
|
||||
if contact_list.has_contact(rx_account):
|
||||
for _ in packet_buf[rx_account]:
|
||||
ts, packet = packet_buf[rx_account].pop(0)
|
||||
process_message(ts, packet, window_list, packet_list, contact_list, key_list, group_list, settings, master_key)
|
||||
continue
|
||||
|
||||
if not m_queue.empty():
|
||||
ts, packet = m_queue.get()
|
||||
rx_account = packet[346:].decode() # header (1) + ct (24 + 8 + 16 + 24 + 256 + 16) + origin (1)
|
||||
|
||||
if contact_list.has_contact(rx_account):
|
||||
process_message(ts, packet, window_list, packet_list, contact_list, key_list, group_list, settings, master_key)
|
||||
else:
|
||||
# If contact derives X25519 shared key first and sends message before user has created
|
||||
# their copy of shared key, buffer received messages until decryption keys are received.
|
||||
if rx_account not in packet_buf:
|
||||
packet_buf[rx_account] = []
|
||||
packet_buf[rx_account].append((ts, packet))
|
||||
continue
|
||||
|
||||
if not f_queue.empty():
|
||||
ts, packet = f_queue.get()
|
||||
process_imported_file(ts, packet, window_list)
|
||||
continue
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
except (FunctionReturn, KeyboardInterrupt):
|
||||
pass
|
|
@ -0,0 +1,288 @@
|
|||
#!/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 datetime
|
||||
import os
|
||||
import textwrap
|
||||
import time
|
||||
import typing
|
||||
|
||||
from typing import Iterable, List, Tuple, Union
|
||||
|
||||
from src.common.misc import clear_screen, get_tty_w
|
||||
from src.common.output import c_print, print_on_previous_line
|
||||
from src.common.statics import *
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.common.db_contacts import Contact, ContactList
|
||||
from src.common.db_groups import GroupList
|
||||
from src.common.db_settings import Settings
|
||||
from src.rx.packet import PacketList
|
||||
|
||||
|
||||
class FileWindow(object):
|
||||
"""FileWindow is a graphical display of ongoing file transmissions."""
|
||||
|
||||
def __init__(self, uid: str, packet_list: 'PacketList') -> None:
|
||||
"""Create a new file window object."""
|
||||
self.uid = uid
|
||||
self.packet_list = packet_list
|
||||
self.unread_messages = 0
|
||||
self.is_active = False
|
||||
|
||||
def redraw(self):
|
||||
"""Draw file window frame."""
|
||||
ft_found = False
|
||||
line_ctr = 0
|
||||
longest_title = 0
|
||||
tty_w = get_tty_w()
|
||||
|
||||
for p in self.packet_list:
|
||||
if p.type == 'file' and len(p.assembly_pt_list) > 0:
|
||||
title = "{} ({}) from {} ".format(p.f_name, p.f_size, p.contact.nick)
|
||||
longest_title = max(longest_title, len(title))
|
||||
|
||||
for p in self.packet_list:
|
||||
if p.type == 'file' and len(p.assembly_pt_list) > 0:
|
||||
line_ctr += 1
|
||||
ft_found = True
|
||||
title = "{} ({}) from {} ".format(p.f_name, p.f_size, p.contact.nick)
|
||||
title += (longest_title - len(title)) * ' '
|
||||
|
||||
bar_len = max(tty_w - (4 + len(title)), 1)
|
||||
ready = int((len(p.assembly_pt_list) / p.f_packets) * bar_len)
|
||||
missing = bar_len - ready
|
||||
bar = title + '[' + (ready - 1) * '=' + '>' + missing * ' ' + ']'
|
||||
print(bar)
|
||||
|
||||
print_on_previous_line(reps=line_ctr)
|
||||
|
||||
if not ft_found:
|
||||
c_print("No file transmissions currently in progress.", head=1, tail=1)
|
||||
print_on_previous_line(reps=3)
|
||||
|
||||
|
||||
class Window(object):
|
||||
"""Window is an ephemeral message log for contact or group."""
|
||||
|
||||
def __init__(self,
|
||||
uid: str,
|
||||
contact_list: 'ContactList',
|
||||
group_list: 'GroupList',
|
||||
settings: 'Settings') -> None:
|
||||
"""Create a new window object."""
|
||||
self.uid = uid
|
||||
self.contact_list = contact_list
|
||||
self.group_list = group_list
|
||||
self.settings = settings
|
||||
|
||||
self.type = None # type: str
|
||||
self.is_active = False
|
||||
self.group_timestamp = time.time() * 1000
|
||||
|
||||
self.window_contacts = [] # type: List[Contact]
|
||||
self.message_log = [] # type: List[Tuple[datetime.datetime, str, str, bytes]]
|
||||
self.unread_messages = 0
|
||||
|
||||
if self.uid == 'local':
|
||||
self.type = 'command'
|
||||
self.window_contacts = [contact_list.get_contact('local')]
|
||||
self.name = 'system messages'
|
||||
|
||||
elif self.uid in self.contact_list.get_list_of_accounts():
|
||||
self.type = 'contact'
|
||||
self.window_contacts = [self.contact_list.get_contact(uid)]
|
||||
self.name = self.contact_list.get_contact(uid).nick
|
||||
|
||||
elif self.uid in self.group_list.get_list_of_group_names():
|
||||
self.type = 'group'
|
||||
self.window_contacts = self.group_list.get_group_members(self.uid)
|
||||
self.name = self.group_list.get_group(self.uid).name
|
||||
|
||||
else:
|
||||
raise ValueError(f"Invalid window UID {uid}.")
|
||||
|
||||
# This attribute is a helper that remembers the timestamp of previous
|
||||
# message. It is updated by print_to_window after every printed message
|
||||
# so the function knows when to display notification about date changing.
|
||||
self.previous_msg_ts = datetime.datetime.now()
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return number of messages."""
|
||||
return len(self.message_log)
|
||||
|
||||
def __iter__(self) -> Iterable:
|
||||
"""Iterate over message log."""
|
||||
for m in self.message_log:
|
||||
yield m
|
||||
|
||||
def remove_contacts(self, accounts: List[str]) -> None:
|
||||
"""Remove contact objects from window."""
|
||||
for account in accounts:
|
||||
for i, m in enumerate(self.window_contacts):
|
||||
if account == m.rx_account:
|
||||
del self.window_contacts[i]
|
||||
|
||||
def add_contacts(self, accounts: List[str]) -> None:
|
||||
"""Add contact objects to window."""
|
||||
for a in accounts:
|
||||
if not self.has_contact(a) and self.contact_list.has_contact(a):
|
||||
self.window_contacts.append(self.contact_list.get_contact(a))
|
||||
|
||||
def reset_window(self) -> None:
|
||||
"""Reset window."""
|
||||
self.message_log = []
|
||||
os.system('reset')
|
||||
|
||||
@staticmethod
|
||||
def clear_window() -> None:
|
||||
"""Clear window."""
|
||||
clear_screen()
|
||||
|
||||
def has_contact(self, account: str) -> bool:
|
||||
"""Return true if contact with specified account is in window."""
|
||||
return any(c.rx_account == account for c in self.window_contacts)
|
||||
|
||||
def print(self, msg_tuple: Tuple['datetime.datetime', str, str, bytes]) -> None:
|
||||
"""Print new message to window."""
|
||||
ts, message, account, origin = msg_tuple
|
||||
|
||||
if self.type == 'command':
|
||||
nick = '-!-'
|
||||
else:
|
||||
window_nicks = [c.nick for c in self.window_contacts] + ['Me']
|
||||
len_of_longest = len(max(window_nicks, key=len))
|
||||
nick = 'Me' if origin == ORIGIN_USER_HEADER else self.contact_list.get_contact(account).nick
|
||||
indent = len_of_longest - len(nick)
|
||||
nick = indent * ' ' + nick + ':'
|
||||
|
||||
if self.previous_msg_ts.date() != ts.date():
|
||||
print(f"00:00 -!- Day changed to {str(ts.date())}")
|
||||
self.previous_msg_ts = ts
|
||||
|
||||
timestamp = ts.strftime('%H:%M')
|
||||
ts_nick = f"{timestamp} {nick} "
|
||||
|
||||
if not self.is_active and self.type == 'group':
|
||||
ts_nick += f"(group {self.name}) "
|
||||
|
||||
wrapper = textwrap.TextWrapper(initial_indent=ts_nick, subsequent_indent=(len(ts_nick)) * ' ', width=get_tty_w())
|
||||
wrapped = wrapper.fill(message)
|
||||
|
||||
# Add bold-effect after wrapping so length of injected VT100 codes does not affect wrapping.
|
||||
wrapped = BOLD_ON + wrapped[:len(ts_nick)] + BOLD_OFF + wrapped[len(ts_nick):]
|
||||
|
||||
if self.is_active:
|
||||
print(wrapped)
|
||||
else:
|
||||
self.unread_messages += 1
|
||||
if self.contact_list.get_contact(account).notifications:
|
||||
# Preview only first line of long message
|
||||
if len(wrapped.split('\n')) > 1:
|
||||
print(wrapped.split('\n')[0][:-3] + '...')
|
||||
else:
|
||||
print(wrapped)
|
||||
print_on_previous_line(delay=self.settings.new_msg_notify_dur, flush=True)
|
||||
|
||||
def print_new(self,
|
||||
timestamp: 'datetime.datetime',
|
||||
message: str,
|
||||
account: str = 'local',
|
||||
origin: bytes = ORIGIN_USER_HEADER,
|
||||
print_: bool = True) -> None:
|
||||
"""Add message tuple to list (and usually print it)."""
|
||||
msg_tuple = (timestamp, message, account, origin)
|
||||
self.message_log.append(msg_tuple)
|
||||
if print_:
|
||||
self.print(msg_tuple)
|
||||
|
||||
def redraw(self) -> None:
|
||||
"""Print all messages received to window."""
|
||||
self.clear_window()
|
||||
self.unread_messages = 0
|
||||
if self.message_log:
|
||||
self.previous_msg_ts = self.message_log[0][0]
|
||||
else:
|
||||
c_print(f"This window for {self.name} is currently empty.", head=1, tail=1)
|
||||
|
||||
for msg_tuple in self.message_log:
|
||||
self.print(msg_tuple)
|
||||
|
||||
|
||||
class WindowList(object):
|
||||
"""WindowList manages a list of window objects."""
|
||||
|
||||
def __init__(self,
|
||||
contact_list: 'ContactList',
|
||||
group_list: 'GroupList',
|
||||
packet_list: 'PacketList',
|
||||
settings: 'Settings') -> None:
|
||||
"""Create a new window list object."""
|
||||
self.contact_list = contact_list
|
||||
self.group_list = group_list
|
||||
self.packet_list = packet_list
|
||||
self.settings = settings
|
||||
self.windows = [] # type: List[Union[Window, FileWindow]]
|
||||
self.active_win = None # type: Union[Window, FileWindow]
|
||||
|
||||
for rx_acco in self.contact_list.get_list_of_accounts():
|
||||
self.windows.append(Window(rx_acco, self.contact_list, self.group_list, self.settings))
|
||||
|
||||
for name in self.group_list.get_list_of_group_names():
|
||||
self.windows.append(Window(name, self.contact_list, self.group_list, self.settings))
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return number of windows."""
|
||||
return len(self.windows)
|
||||
|
||||
def __iter__(self) -> 'WindowList':
|
||||
"""Iterate over window list."""
|
||||
for w in self.windows:
|
||||
yield w
|
||||
|
||||
def select_rx_window(self, name: str) -> None:
|
||||
"""Select new active window."""
|
||||
if self.active_win is not None:
|
||||
self.active_win.is_active = False
|
||||
self.active_win = self.get_window(name)
|
||||
self.active_win.is_active = True
|
||||
self.active_win.redraw()
|
||||
|
||||
def has_window(self, name: str) -> bool:
|
||||
"""Return True if window exists, else False."""
|
||||
return name in self.get_list_of_window_names()
|
||||
|
||||
def get_list_of_window_names(self) -> List[str]:
|
||||
"""Return list of window names."""
|
||||
return [w.uid for w in self.windows]
|
||||
|
||||
def get_local_window(self) -> 'Window':
|
||||
"""Return command window."""
|
||||
return self.get_window('local')
|
||||
|
||||
def get_window(self, name: str) -> 'Window':
|
||||
"""Return window that matches the specified name."""
|
||||
if not self.has_window(name):
|
||||
if name == FILE_R_WIN_ID_BYTES.decode():
|
||||
self.windows.append(FileWindow(name, self.packet_list))
|
||||
else:
|
||||
self.windows.append(Window(name, self.contact_list, self.group_list, self.settings))
|
||||
|
||||
return next(w for w in self.windows if w.uid == name)
|
|
@ -0,0 +1,484 @@
|
|||
#!/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 textwrap
|
||||
import time
|
||||
import typing
|
||||
import zlib
|
||||
|
||||
from multiprocessing import Queue
|
||||
from typing import Any, Dict, List, Tuple, Union
|
||||
|
||||
from src.common.crypto import encrypt_and_sign, keygen
|
||||
from src.common.db_logs import access_history, re_encrypt
|
||||
from src.common.encoding import b58encode, int_to_bytes, str_to_bytes
|
||||
from src.common.errors import FunctionReturn, graceful_exit
|
||||
from src.common.input import yes
|
||||
from src.common.misc import clear_screen, ensure_dir, get_tty_w
|
||||
from src.common.output import box_print, phase, print_on_previous_line
|
||||
from src.common.path import ask_path_gui
|
||||
from src.common.statics import *
|
||||
from src.tx.commands_g import process_group_command
|
||||
from src.tx.contact import add_new_contact, change_nick, remove_contact, contact_setting, fingerprints
|
||||
from src.tx.key_exchanges import new_local_key, rxm_load_psk
|
||||
from src.tx.packet import cancel_packet, queue_command, transmit
|
||||
from src.tx.windows import select_window
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.common.db_contacts import ContactList
|
||||
from src.common.db_groups import GroupList
|
||||
from src.common.db_masterkey import MasterKey
|
||||
from src.common.db_settings import Settings
|
||||
from src.common.gateway import Gateway
|
||||
from src.tx.user_input import UserInput
|
||||
from src.tx.windows import Window
|
||||
|
||||
|
||||
class Command(object):
|
||||
"""Commands are created only after user input has been interpreted."""
|
||||
|
||||
def __init__(self, plaintext: str) -> None:
|
||||
self.plaintext = plaintext
|
||||
self.type = 'command'
|
||||
|
||||
|
||||
def process_command(user_input: 'UserInput',
|
||||
window: 'Window',
|
||||
settings: 'Settings',
|
||||
queues: Dict[bytes, 'Queue'],
|
||||
contact_list: 'ContactList',
|
||||
group_list: 'GroupList',
|
||||
gateway: 'Gateway',
|
||||
master_key: 'MasterKey') -> None:
|
||||
"""Process command based on user input."""
|
||||
c = COMMAND_PACKET_QUEUE
|
||||
# Keyword Function to run ( Parameters )
|
||||
# ---------------------------------------------------------------------------------------------------------------------------------
|
||||
d = {'about': (print_about, ),
|
||||
'add': (add_new_contact, contact_list, group_list, settings, queues, gateway ),
|
||||
'clear': (clear_screens, window, settings, queues[c], gateway ),
|
||||
'cmd': (rxm_show_cmd_win, window, settings, queues[c] ),
|
||||
'cm': (cancel_packet, user_input, window, settings, queues ),
|
||||
'cf': (cancel_packet, user_input, window, settings, queues ),
|
||||
'exit': (exit_tfc, settings, queues[c], gateway ),
|
||||
'export': (export_logs, user_input, window, contact_list, settings, queues[c], master_key),
|
||||
'fingerprints': (fingerprints, window ),
|
||||
'fe': (export_file, settings, gateway ),
|
||||
'fi': (import_file, settings, gateway ),
|
||||
'fw': (rxm_display_f_win, window, settings, queues[c] ),
|
||||
'group': (process_group_command, user_input, contact_list, group_list, settings, queues ),
|
||||
'help': (print_help, settings ),
|
||||
'history': (print_logs, user_input, window, contact_list, settings, queues[c], master_key),
|
||||
'localkey': (new_local_key, contact_list, settings, queues, gateway ),
|
||||
'logging': (contact_setting, user_input, window, contact_list, group_list, settings, queues[c] ),
|
||||
'msg': (select_window, user_input, window, settings, queues ),
|
||||
'names': (print_recipients, contact_list, group_list, ),
|
||||
'nick': (change_nick, user_input, window, contact_list, group_list, settings, queues[c] ),
|
||||
'notify': (contact_setting, user_input, window, contact_list, group_list, settings, queues[c] ),
|
||||
'passwd': (change_master_key, user_input, contact_list, group_list, settings, queues, master_key),
|
||||
'psk': (rxm_load_psk, window, contact_list, settings, queues[c] ),
|
||||
'reset': (reset_screens, window, settings, queues[c], gateway ),
|
||||
'rm': (remove_contact, user_input, window, contact_list, group_list, settings, queues ),
|
||||
'set': (change_setting, user_input, contact_list, group_list, settings, queues[c], gateway ),
|
||||
'settings': (settings.print_settings, ),
|
||||
'store': (contact_setting, user_input, window, contact_list, group_list, settings, queues[c] ),
|
||||
'unread': (rxm_display_unread, settings, queues[c] )} # type: Dict[str, Any]
|
||||
|
||||
cmd_key = user_input.plaintext.split()[0]
|
||||
if cmd_key not in d:
|
||||
raise FunctionReturn(f"Invalid command '{cmd_key}'.")
|
||||
|
||||
from_dict = d[cmd_key]
|
||||
func = from_dict[0]
|
||||
parameters = from_dict[1:]
|
||||
func(*parameters)
|
||||
|
||||
|
||||
def print_about() -> None:
|
||||
"""Print URLs that direct to TFC project site and documentation."""
|
||||
from tfc import __version__
|
||||
|
||||
clear_screen()
|
||||
|
||||
print(f"\n Tinfoil Chat {__version__} \n\n"
|
||||
" Website: https://github.com/maqp/tfc/ \n"
|
||||
" Wikipage: https://github.com/maqp/tfc/wiki \n"
|
||||
" White paper: https://cs.helsinki.fi/u/oottela/tfc.pdf\n")
|
||||
|
||||
|
||||
def clear_screens(window: 'Window',
|
||||
settings: 'Settings',
|
||||
c_queue: 'Queue',
|
||||
gateway: 'Gateway') -> None:
|
||||
"""Clear TxM, RxM and NH screens."""
|
||||
clear_screen()
|
||||
queue_command(CLEAR_SCREEN_HEADER, settings, c_queue)
|
||||
if not settings.session_trickle:
|
||||
if window.imc_name is not None:
|
||||
im_window = window.imc_name.encode()
|
||||
time.sleep(0.5)
|
||||
transmit(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_SCREEN_CLEAR + im_window, settings, gateway)
|
||||
|
||||
|
||||
def rxm_show_cmd_win(window: 'Window',
|
||||
settings: 'Settings',
|
||||
c_queue: 'Queue') -> None:
|
||||
"""Show command window on RxM until user presses Enter."""
|
||||
packet = WINDOW_CHANGE_HEADER + LOCAL_WIN_ID_BYTES
|
||||
queue_command(packet, settings, c_queue)
|
||||
|
||||
box_print(f"<Enter> returns RxM to {window.name}'s window", manual_proceed=True)
|
||||
print_on_previous_line(reps=4, flush=True)
|
||||
|
||||
packet = WINDOW_CHANGE_HEADER + window.uid.encode()
|
||||
queue_command(packet, settings, c_queue)
|
||||
|
||||
|
||||
def exit_tfc(settings: 'Settings',
|
||||
c_queue: 'Queue',
|
||||
gateway: 'Gateway') -> None:
|
||||
"""Exit TFC on TxM/RxM/NH."""
|
||||
queue_command(EXIT_PROGRAM_HEADER, settings, c_queue)
|
||||
time.sleep(0.5)
|
||||
|
||||
transmit(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_EXIT_COMMAND, settings, gateway)
|
||||
|
||||
if settings.local_testing_mode:
|
||||
time.sleep(0.8)
|
||||
if settings.data_diode_sockets:
|
||||
time.sleep(2.2)
|
||||
|
||||
graceful_exit()
|
||||
|
||||
|
||||
def export_logs(user_input: 'UserInput',
|
||||
window: 'Window',
|
||||
contact_list: 'ContactList',
|
||||
settings: 'Settings',
|
||||
c_queue: 'Queue',
|
||||
master_key: 'MasterKey') -> None:
|
||||
"""Export log files to plaintext file on TxM/RxM.
|
||||
|
||||
TxM only exports sent messages, RxM exports full conversation.
|
||||
"""
|
||||
try:
|
||||
no_messages_str = user_input.plaintext.split()[1]
|
||||
if not no_messages_str.isdigit():
|
||||
raise FunctionReturn("Specified invalid number of messages to export.")
|
||||
no_messages = int(no_messages_str)
|
||||
except IndexError:
|
||||
no_messages = 0
|
||||
|
||||
if not yes(f"Export logs for {window.name} in plaintext?", head=1, tail=1):
|
||||
raise FunctionReturn("Logfile export aborted.")
|
||||
|
||||
packet = LOG_EXPORT_HEADER + window.uid.encode() + US_BYTE + int_to_bytes(no_messages)
|
||||
queue_command(packet, settings, c_queue)
|
||||
|
||||
access_history(window, contact_list, settings, master_key, no_messages, export=True)
|
||||
|
||||
|
||||
def export_file(settings: 'Settings', gateway: 'Gateway'):
|
||||
"""Encrypt and export file to NH.
|
||||
|
||||
This is a faster method of sending large files. It is used together with '/fi' import_file
|
||||
command that loads ciphertext to RxM for later decryption. Key is generated automatically
|
||||
so that bad passwords by users do not affect security of ciphertexts.
|
||||
|
||||
As use of this command reveals use of TFC, it is disabled during trickle connection.
|
||||
"""
|
||||
if settings.session_trickle:
|
||||
raise FunctionReturn("Command disabled during trickle connection.")
|
||||
|
||||
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. No file was sent.")
|
||||
|
||||
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=9))
|
||||
phase("Done")
|
||||
|
||||
phase("Encrypting data")
|
||||
file_key = keygen()
|
||||
file_ct = encrypt_and_sign(comp, key=file_key)
|
||||
phase("Done")
|
||||
|
||||
phase("Exporting data")
|
||||
transmit(EXPORTED_FILE_CT_HEADER + file_ct, settings, gateway)
|
||||
phase("Done")
|
||||
|
||||
box_print([f"Decryption key for file {name}:", '', b58encode(file_key)], head=1, tail=1)
|
||||
|
||||
|
||||
def import_file(settings: 'Settings', gateway: 'Gateway'):
|
||||
"""Import files from NH to RxM and decrypt them."""
|
||||
if settings.session_trickle:
|
||||
raise FunctionReturn("Command disabled during trickle connection.")
|
||||
|
||||
transmit(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_IMPORT_COMMAND, settings, gateway)
|
||||
|
||||
|
||||
def rxm_display_f_win(window: 'Window',
|
||||
settings: 'Settings',
|
||||
c_queue: 'Queue'):
|
||||
"""Show file reception window on RxM until user presses Enter."""
|
||||
packet = WINDOW_CHANGE_HEADER + FILE_R_WIN_ID_BYTES
|
||||
queue_command(packet, settings, c_queue)
|
||||
|
||||
box_print(f"<Enter> returns RxM to {window.name}'s window", manual_proceed=True)
|
||||
print_on_previous_line(reps=4, flush=True)
|
||||
|
||||
packet = WINDOW_CHANGE_HEADER + window.uid.encode()
|
||||
queue_command(packet, settings, c_queue)
|
||||
|
||||
|
||||
def print_help(settings: 'Settings') -> None:
|
||||
"""Print the list of commands."""
|
||||
|
||||
def help_printer(tuple_list: List[Union[Tuple[str, str, bool]]]) -> None:
|
||||
"""Print help menu, style depending on terminal width and display conditions.
|
||||
|
||||
:param tuple_list: List of command-description-display tuples
|
||||
"""
|
||||
longest_command = ''
|
||||
for t in tuple_list:
|
||||
longest_command = max(t[0], longest_command, key=len)
|
||||
longest_command += ' ' # Add spacing
|
||||
|
||||
for help_cmd, description, display_condition in tuple_list:
|
||||
|
||||
if not display_condition:
|
||||
continue
|
||||
|
||||
wrapper = textwrap.TextWrapper(width=max(1, (get_tty_w() - len(longest_command))))
|
||||
desc_lines = wrapper.fill(description).split('\n')
|
||||
spacing = (len(longest_command) - len(help_cmd)) * ' '
|
||||
|
||||
print(help_cmd + spacing + desc_lines[0])
|
||||
|
||||
# Print wrapped description lines with indent
|
||||
if len(desc_lines) > 1:
|
||||
for line in desc_lines[1:]:
|
||||
print(len(longest_command) * ' ' + line)
|
||||
print('')
|
||||
|
||||
common = [("/about", "Show links to project resources", True),
|
||||
("/add", "Add new contact", not settings.session_trickle),
|
||||
("/cf", "Cancel file transmission to recipients", True),
|
||||
("/cm", "Cancel message transmission to recipients", True),
|
||||
("/clear, ' '", "Clear screens from TxM, RxM and IM client", True),
|
||||
("/cmd, '//'", "Display command window on RxM", True),
|
||||
("/exit", "Exit TFC on TxM, NH and RxM", True),
|
||||
("/export (n)", "Export (n) messages from recipient's logfile", True),
|
||||
("/file", "Send file to active contact/group", True),
|
||||
("/fingerprints", "Print public key fingerprints of user and contact", True),
|
||||
("/fe", "Encrypt and export file to NH", not settings.session_trickle),
|
||||
("/fi", "Import file from NH to RxM", not settings.session_trickle),
|
||||
("/fw", "Display file reception window on RxM", True),
|
||||
("/help", "Display this list of commands", True),
|
||||
("/history (n)", "Print (n) messages from recipient's logfile", True),
|
||||
("/localkey", "Generate new local key pair", not settings.session_trickle),
|
||||
("/logging {on,off}(' all')", "Change log_messages setting (for all contacts)", True),
|
||||
("/msg", "Change active recipient", not settings.session_trickle),
|
||||
("/names", "List contacts and groups", True),
|
||||
("/nick N", "Change nickname of active recipient to N", True),
|
||||
("/notify {on,off} (' all')", "Change notification settings (for all contacts)", True),
|
||||
("/passwd {tx,rx}", "Change master password on TxM/RxM", not settings.session_trickle),
|
||||
("/psk", "Open PSK import dialog on RxM", True),
|
||||
("/reset", "Reset ephemeral session log on TxM/RxM/IM client", not settings.session_trickle),
|
||||
("/rm A", "Remove account A from TxM and RxM", not settings.session_trickle),
|
||||
("/set S V", "Change setting S to value V on TxM/RxM", not settings.session_trickle),
|
||||
("/settings", "List settings, default values and descriptions", not settings.session_trickle),
|
||||
("/store {on,off} (' all')", "Change file reception (for all contacts)", True),
|
||||
("/unread, ' '", "List windows with unread messages on RxM", True),
|
||||
("Shift + PgUp/PgDn", "Scroll terminal up/down", True)]
|
||||
|
||||
groupc = [("/group create G A1 .. An", "Create group G and add accounts A1 .. An", not settings.session_trickle),
|
||||
("/group add G A1 .. An", "Add accounts A1 .. An to group G", not settings.session_trickle),
|
||||
("/group rm G A1 .. An", "Remove accounts A1 .. An from group G", not settings.session_trickle),
|
||||
("/group rm G", "Remove group G", not settings.session_trickle)]
|
||||
|
||||
terminal_width = get_tty_w()
|
||||
|
||||
clear_screen()
|
||||
|
||||
print(textwrap.fill("List of commands:", width=terminal_width))
|
||||
print('')
|
||||
help_printer(common)
|
||||
print(terminal_width * '-')
|
||||
|
||||
if settings.session_trickle:
|
||||
print('')
|
||||
else:
|
||||
print("Group management:\n")
|
||||
help_printer(groupc)
|
||||
print(terminal_width * '-' + '\n')
|
||||
|
||||
|
||||
def print_logs(user_input: 'UserInput',
|
||||
window: 'Window',
|
||||
contact_list: 'ContactList',
|
||||
settings: 'Settings',
|
||||
c_queue: 'Queue',
|
||||
master_key: 'MasterKey') -> None:
|
||||
"""Print log files on screen."""
|
||||
try:
|
||||
no_messages_str = user_input.plaintext.split()[1]
|
||||
if not no_messages_str.isdigit():
|
||||
raise FunctionReturn("Specified invalid number of messages to print.")
|
||||
no_messages = int(no_messages_str)
|
||||
except IndexError:
|
||||
no_messages = 0
|
||||
|
||||
packet = LOG_DISPLAY_HEADER + window.uid.encode() + US_BYTE + int_to_bytes(no_messages)
|
||||
queue_command(packet, settings, c_queue)
|
||||
|
||||
access_history(window, contact_list, settings, master_key, no_messages)
|
||||
|
||||
|
||||
def print_recipients(contact_list: 'ContactList', group_list: 'GroupList') -> None:
|
||||
"""Print list of contacts and groups."""
|
||||
contact_list.print_contacts(spacing=True)
|
||||
group_list.print_groups()
|
||||
|
||||
|
||||
def change_master_key(user_input: 'UserInput',
|
||||
contact_list: 'ContactList',
|
||||
group_list: 'GroupList',
|
||||
settings: 'Settings',
|
||||
queues: Dict[bytes, 'Queue'],
|
||||
master_key: 'MasterKey') -> None:
|
||||
"""Change master key on TxM/RxM."""
|
||||
try:
|
||||
if settings.session_trickle:
|
||||
raise FunctionReturn("Command disabled during trickle connection.")
|
||||
|
||||
try:
|
||||
device = user_input.plaintext.split()[1]
|
||||
except IndexError:
|
||||
raise FunctionReturn("No target system specified.")
|
||||
|
||||
if device.lower() not in ['tx', 'txm', 'rx', 'rxm']:
|
||||
raise FunctionReturn("Invalid target system.")
|
||||
|
||||
if device.lower() in ['rx', 'rxm']:
|
||||
queue_command(CHANGE_MASTER_K_HEADER, settings, queues[COMMAND_PACKET_QUEUE])
|
||||
print('')
|
||||
return None
|
||||
|
||||
old_master_key = master_key.master_key[:]
|
||||
master_key.new_master_key()
|
||||
new_master_key = master_key.master_key
|
||||
|
||||
ensure_dir(f'{DIR_USER_DATA}/')
|
||||
file_name = f'{DIR_USER_DATA}/{settings.software_operation}_logs'
|
||||
if os.path.isfile(file_name):
|
||||
phase("Re-encrypting log-file")
|
||||
re_encrypt(old_master_key, new_master_key, settings)
|
||||
phase("Done")
|
||||
|
||||
queues[KEY_MANAGEMENT_QUEUE].put(('KEY', master_key))
|
||||
|
||||
settings.store_settings()
|
||||
contact_list.store_contacts()
|
||||
group_list.store_groups()
|
||||
|
||||
box_print("Master key successfully changed.", head=1)
|
||||
clear_screen(delay=1.5)
|
||||
except KeyboardInterrupt:
|
||||
raise FunctionReturn("Password change aborted.")
|
||||
|
||||
|
||||
def reset_screens(window: 'Window',
|
||||
settings: 'Settings',
|
||||
c_queue: 'Queue',
|
||||
gateway: 'Gateway') -> None:
|
||||
"""Reset screens on TxM/RxM/NH."""
|
||||
queue_command(RESET_SCREEN_HEADER + window.uid.encode(), settings, c_queue)
|
||||
|
||||
if not settings.session_trickle:
|
||||
if window.imc_name is not None:
|
||||
im_window = window.imc_name.encode()
|
||||
time.sleep(0.5)
|
||||
transmit(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_SCREEN_RESET + im_window, settings, gateway)
|
||||
|
||||
os.system('reset')
|
||||
|
||||
|
||||
def change_setting(user_input: 'UserInput',
|
||||
contact_list: 'ContactList',
|
||||
group_list: 'GroupList',
|
||||
settings: 'Settings',
|
||||
c_queue: 'Queue',
|
||||
gateway: 'Gateway') -> None:
|
||||
"""Change setting on TxM / RxM."""
|
||||
try:
|
||||
key = user_input.plaintext.split()[1]
|
||||
except IndexError:
|
||||
raise FunctionReturn("No setting specified.")
|
||||
|
||||
try:
|
||||
_ = user_input.plaintext.split()[2]
|
||||
except IndexError:
|
||||
raise FunctionReturn("No value for setting specified.")
|
||||
|
||||
value = ' '.join(user_input.plaintext.split()[2:])
|
||||
|
||||
if key not in settings.key_list:
|
||||
raise FunctionReturn(f"Invalid setting {key}.")
|
||||
|
||||
if settings.session_trickle:
|
||||
if key in ['e_correction_ratio', 'serial_iface_speed']:
|
||||
raise FunctionReturn("Change of setting disabled during trickle connection.")
|
||||
|
||||
settings.change_setting(key, value, contact_list, group_list)
|
||||
|
||||
if key == 'e_correction_ratio':
|
||||
time.sleep(0.5)
|
||||
transmit(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_EC_RATIO + value.encode(), settings, gateway)
|
||||
|
||||
if key == 'serial_iface_speed':
|
||||
time.sleep(0.5)
|
||||
transmit(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_BAUDRATE + value.encode(), settings, gateway)
|
||||
|
||||
if key == 'disable_gui_dialog':
|
||||
time.sleep(0.5)
|
||||
transmit(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_GUI_DIALOG + value.encode(), settings, gateway)
|
||||
|
||||
packet = CHANGE_SETTING_HEADER + key.encode() + US_BYTE + value.encode()
|
||||
queue_command(packet, settings, c_queue)
|
||||
|
||||
|
||||
def rxm_display_unread(settings: 'Settings', c_queue: 'Queue') -> None:
|
||||
"""Temporarily display list of windows with unread messages on RxM."""
|
||||
queue_command(SHOW_WINDOW_ACTIVITY_HEADER, settings, c_queue)
|
|
@ -0,0 +1,291 @@
|
|||
#!/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 re
|
||||
import typing
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
from src.common.errors import FunctionReturn
|
||||
from src.common.input import yes
|
||||
from src.common.output import box_print, g_mgmt_print
|
||||
from src.common.statics import *
|
||||
from src.tx.messages import Message, queue_message
|
||||
from src.tx.packet import queue_command
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from multiprocessing import Queue
|
||||
from src.common.db_contacts import Contact, ContactList
|
||||
from src.common.db_groups import Group, GroupList
|
||||
from src.common.db_settings import Settings
|
||||
from src.tx.user_input import UserInput
|
||||
|
||||
|
||||
class MockWindow(object):
|
||||
"""Mock window simplifies queueing of message assembly packets."""
|
||||
|
||||
def __init__(self, uid: str, contacts: List['Contact']) -> None:
|
||||
"""Create new mock window."""
|
||||
self.uid = uid
|
||||
self.window_contacts = contacts
|
||||
self.type = 'contact'
|
||||
self.group = None # type: Group
|
||||
self.name = None # type: str
|
||||
|
||||
def __iter__(self) -> 'MockWindow':
|
||||
"""Iterate over contact objects in window."""
|
||||
for c in self.window_contacts:
|
||||
yield c
|
||||
|
||||
|
||||
def process_group_command(user_input: 'UserInput',
|
||||
contact_list: 'ContactList',
|
||||
group_list: 'GroupList',
|
||||
settings: 'Settings',
|
||||
queues: Dict[bytes, 'Queue']) -> None:
|
||||
"""Parse group command and process it accordingly."""
|
||||
if settings.session_trickle:
|
||||
raise FunctionReturn("Command disabled during trickle connection.")
|
||||
|
||||
params = user_input.plaintext
|
||||
|
||||
try:
|
||||
command_type = params.split()[1]
|
||||
except IndexError:
|
||||
raise FunctionReturn("Invalid group command.")
|
||||
|
||||
if command_type not in ['create', 'add', 'rm']:
|
||||
raise FunctionReturn("Invalid group command.")
|
||||
|
||||
try:
|
||||
group_name = params.split()[2]
|
||||
except IndexError:
|
||||
raise FunctionReturn("No group name specified.")
|
||||
|
||||
purp_members = params.split()[3:]
|
||||
|
||||
# Swap specified nicks to rx_accounts
|
||||
for i, m in enumerate(purp_members):
|
||||
if m in contact_list.get_list_of_nicks():
|
||||
purp_members[i] = contact_list.get_contact(m).rx_account
|
||||
|
||||
func = dict(create=group_create,
|
||||
add =group_add_member,
|
||||
rm =group_rm_member)[command_type]
|
||||
|
||||
func(group_name, purp_members, group_list, contact_list, settings, queues)
|
||||
|
||||
|
||||
def group_create(group_name: str, # Name of group to manage
|
||||
purp_members: List[str], # Members specified by user
|
||||
group_list: 'GroupList',
|
||||
contact_list: 'ContactList',
|
||||
settings: 'Settings',
|
||||
queues: Dict[bytes, 'Queue']) -> None:
|
||||
"""Create a new group.
|
||||
|
||||
Validate group name and determine what members that can be added.
|
||||
"""
|
||||
# Avoids collision with delimiters
|
||||
if not group_name.isprintable():
|
||||
raise FunctionReturn("Group name must be printable.")
|
||||
|
||||
# Length limited by database's unicode padding
|
||||
if len(group_name) > 254:
|
||||
raise FunctionReturn("Group name must be less than 255 chars long.")
|
||||
|
||||
if group_name == 'dummy_group':
|
||||
raise FunctionReturn("Group name can't use name reserved for database padding.")
|
||||
|
||||
if re.match(ACCOUNT_FORMAT, group_name):
|
||||
raise FunctionReturn("Group name can't have format of an account.")
|
||||
|
||||
if group_name in contact_list.get_list_of_nicks():
|
||||
raise FunctionReturn("Group name can't be nick of contact.")
|
||||
|
||||
if group_name in group_list.get_list_of_group_names():
|
||||
if not yes(f"Group with name {group_name} already exists. Overwrite?"):
|
||||
raise FunctionReturn("Group creation aborted.")
|
||||
|
||||
accounts = set(contact_list.get_list_of_accounts())
|
||||
purpaccs = set(purp_members)
|
||||
|
||||
accepted = list(accounts & purpaccs)
|
||||
rejected = list(purpaccs - accounts)
|
||||
|
||||
if len(accepted) > settings.m_members_in_group:
|
||||
raise FunctionReturn("Error: TFC settings only allow {} members per group."
|
||||
.format(settings.m_members_in_group))
|
||||
|
||||
if len(group_list) == settings.m_number_of_groups:
|
||||
raise FunctionReturn("Error: TFC settings only allow {} groups."
|
||||
.format(settings.m_number_of_groups))
|
||||
|
||||
a_contacts = [contact_list.get_contact(c) for c in accepted]
|
||||
group_list.add_group(group_name,
|
||||
settings.log_msg_by_default,
|
||||
settings.n_m_notify_privacy,
|
||||
a_contacts)
|
||||
|
||||
fields = [f.encode() for f in ([group_name] + accepted)]
|
||||
packet = GROUP_CREATE_HEADER + US_BYTE.join(fields)
|
||||
queue_command(packet, settings, queues[COMMAND_PACKET_QUEUE])
|
||||
|
||||
g_mgmt_print('new_g', accepted, contact_list, group_name)
|
||||
g_mgmt_print('unkwn', rejected, contact_list, group_name)
|
||||
|
||||
if accepted:
|
||||
if yes("Publish list of group members to participants?"):
|
||||
for member in accepted:
|
||||
m_list = accepted[:]
|
||||
m_list.remove(member)
|
||||
message = Message(US_STR.join([group_name] + m_list))
|
||||
contact = contact_list.get_contact(member)
|
||||
mock_win = MockWindow(contact.rx_account, [contact])
|
||||
queue_message(message, mock_win, settings, queues[MESSAGE_PACKET_QUEUE], header=GROUP_MSG_INVITATION_HEADER)
|
||||
|
||||
else:
|
||||
box_print(f"Created an empty group {group_name}.", head=1)
|
||||
print('')
|
||||
|
||||
|
||||
def group_add_member(group_name: str,
|
||||
purp_members: List['str'],
|
||||
group_list: 'GroupList',
|
||||
contact_list: 'ContactList',
|
||||
settings: 'Settings',
|
||||
queues: Dict[bytes, 'Queue']) -> None:
|
||||
"""Add new member(s) to group."""
|
||||
if group_name not in group_list.get_list_of_group_names():
|
||||
if yes(f"Group {group_name} was not found. Create new group?"):
|
||||
group_create(group_name, purp_members, group_list, contact_list, settings, queues)
|
||||
return None
|
||||
else:
|
||||
raise FunctionReturn("Group creation aborted.")
|
||||
|
||||
purpaccs = set(purp_members)
|
||||
accounts = set(contact_list.get_list_of_accounts())
|
||||
before_a = set(group_list.get_group(group_name).get_list_of_member_accounts())
|
||||
ok_accos = set(accounts & purpaccs)
|
||||
new_in_g = set(ok_accos - before_a)
|
||||
|
||||
e_asmbly = list(before_a | new_in_g)
|
||||
rejected = list(purpaccs - accounts)
|
||||
in_alrdy = list(before_a & purpaccs)
|
||||
n_in_g_l = list(new_in_g)
|
||||
ok_accol = list(ok_accos)
|
||||
|
||||
if len(e_asmbly) > settings.m_members_in_group:
|
||||
raise FunctionReturn("Error: TFC settings only allow {} members per group."
|
||||
.format(settings.m_members_in_group))
|
||||
|
||||
group = group_list.get_group(group_name)
|
||||
group.add_members([contact_list.get_contact(a) for a in new_in_g])
|
||||
|
||||
fields = [f.encode() for f in ([group_name] + ok_accol)]
|
||||
packet = GROUP_ADD_HEADER + US_BYTE.join(fields)
|
||||
queue_command(packet, settings, queues[COMMAND_PACKET_QUEUE])
|
||||
|
||||
g_mgmt_print('add_m', n_in_g_l, contact_list, group_name)
|
||||
g_mgmt_print('add_a', in_alrdy, contact_list, group_name)
|
||||
g_mgmt_print('unkwn', rejected, contact_list, group_name)
|
||||
|
||||
if new_in_g:
|
||||
if yes("Publish new list of members to involved?"):
|
||||
for member in before_a:
|
||||
message = Message(US_STR.join([group_name] + n_in_g_l))
|
||||
contact = contact_list.get_contact(member)
|
||||
mock_win = MockWindow(contact.rx_account, [contact])
|
||||
queue_message(message, mock_win, settings, queues[MESSAGE_PACKET_QUEUE],
|
||||
header=GROUP_MSG_ADD_NOTIFY_HEADER)
|
||||
|
||||
for member_ in new_in_g:
|
||||
m_list = e_asmbly[:]
|
||||
m_list.remove(member_)
|
||||
message = Message(US_STR.join([group_name] + m_list))
|
||||
contact = contact_list.get_contact(member_)
|
||||
mock_win = MockWindow(contact.rx_account, [contact])
|
||||
queue_message(message, mock_win, settings, queues[MESSAGE_PACKET_QUEUE], header=GROUP_MSG_INVITATION_HEADER)
|
||||
print('')
|
||||
|
||||
|
||||
def group_rm_member(group_name: str,
|
||||
purp_members: List[str],
|
||||
group_list: 'GroupList',
|
||||
contact_list: 'ContactList',
|
||||
settings: 'Settings',
|
||||
queues: Dict[bytes, 'Queue']) -> None:
|
||||
"""Remove member(s) from group or group itself if no members are specified. """
|
||||
purpaccs = set(purp_members)
|
||||
|
||||
if not purpaccs:
|
||||
if not yes(f"Remove group '{group_name}'?", head=1):
|
||||
raise FunctionReturn("Group removal aborted.")
|
||||
|
||||
packet = GROUP_DELETE_HEADER + group_name.encode()
|
||||
queue_command(packet, settings, queues[COMMAND_PACKET_QUEUE])
|
||||
|
||||
if group_name not in group_list.get_list_of_group_names():
|
||||
raise FunctionReturn(f"TxM has no group {group_name} to remove.")
|
||||
|
||||
group = group_list.get_group(group_name)
|
||||
if group.has_members():
|
||||
if yes("Notify members about leaving the group?"):
|
||||
message = Message(group_name)
|
||||
for member in group:
|
||||
mock_win = MockWindow(member.rx_account, [member])
|
||||
queue_message(message, mock_win, settings, queues[MESSAGE_PACKET_QUEUE], header=GROUP_MSG_EXIT_GROUP_HEADER)
|
||||
|
||||
group_list.remove_group(group_name)
|
||||
raise FunctionReturn(f"Removed group {group_name}.")
|
||||
|
||||
if group_name not in group_list.get_list_of_group_names():
|
||||
raise FunctionReturn(f"Group '{group_name}' does not exist.")
|
||||
|
||||
accounts = set(contact_list.get_list_of_accounts())
|
||||
before_r = set(group_list.get_group(group_name).get_list_of_member_accounts())
|
||||
ok_accos = set(purpaccs & accounts)
|
||||
remove_s = set(before_r & ok_accos)
|
||||
|
||||
e_asmbly = list(before_r - remove_s)
|
||||
not_in_g = list(ok_accos - before_r)
|
||||
rejected = list(purpaccs - accounts)
|
||||
remove_l = list(remove_s)
|
||||
ok_accol = list(ok_accos)
|
||||
|
||||
group = group_list.get_group(group_name)
|
||||
group.remove_members(remove_l)
|
||||
|
||||
fields = [f.encode() for f in ([group_name] + ok_accol)]
|
||||
packet = GROUP_REMOVE_M_HEADER + US_BYTE.join(fields)
|
||||
queue_command(packet, settings, queues[COMMAND_PACKET_QUEUE])
|
||||
|
||||
g_mgmt_print('rem_m', remove_l, contact_list, group_name)
|
||||
g_mgmt_print('rem_n', not_in_g, contact_list, group_name)
|
||||
g_mgmt_print('unkwn', rejected, contact_list, group_name)
|
||||
|
||||
if remove_l and e_asmbly:
|
||||
if yes("Publish list of removed members to remaining members?"):
|
||||
for member_ in e_asmbly:
|
||||
message = Message(US_STR.join([group_name] + remove_l))
|
||||
contact = contact_list.get_contact(member_)
|
||||
mock_win = MockWindow(contact.rx_account, [contact])
|
||||
queue_message(message, mock_win, settings, queues[MESSAGE_PACKET_QUEUE], header=GROUP_MSG_MEMBER_RM_HEADER)
|
||||
print('')
|
|
@ -0,0 +1,227 @@
|
|||
#!/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 typing
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from src.common.errors import FunctionReturn
|
||||
from src.common.input import box_input, yes
|
||||
from src.common.misc import clear_screen, validate_account, validate_key_exchange, validate_nick
|
||||
from src.common.output import box_print, c_print, print_fingerprints
|
||||
from src.common.statics import *
|
||||
from src.tx.key_exchanges import new_psk, start_key_exchange
|
||||
from src.tx.packet import queue_command
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from multiprocessing import Queue
|
||||
from src.common.db_contacts import ContactList
|
||||
from src.common.db_groups import GroupList
|
||||
from src.common.db_settings import Settings
|
||||
from src.common.gateway import Gateway
|
||||
from src.tx.user_input import UserInput
|
||||
from src.tx.windows import Window
|
||||
|
||||
|
||||
def add_new_contact(contact_list: 'ContactList',
|
||||
group_list: 'GroupList',
|
||||
settings: 'Settings',
|
||||
queues: Dict[bytes, 'Queue'],
|
||||
gateway: 'Gateway') -> None:
|
||||
"""Prompt for contact account details and initialize desired key exchange method."""
|
||||
try:
|
||||
if settings.session_trickle:
|
||||
raise FunctionReturn("Command disabled during trickle connection.")
|
||||
|
||||
if len(contact_list) >= settings.m_number_of_accnts:
|
||||
raise FunctionReturn(f"Error: TFC settings only allow {settings.m_number_of_accnts} accounts.")
|
||||
|
||||
clear_screen()
|
||||
c_print("Add new contact", head=1)
|
||||
|
||||
acco = box_input("Contact account", tail=1, validator=validate_account).strip()
|
||||
user = box_input("Your account", tail=1, validator=validate_account).strip()
|
||||
defn = acco.split('@')[0].capitalize()
|
||||
nick = box_input(f"Contact nick [{defn}]", default=defn, tail=1, validator=validate_nick, validator_args=(contact_list, group_list, acco)).strip()
|
||||
keyx = box_input("Key exchange ([ECDHE],PSK) ", default='ECDHE', tail=1, validator=validate_key_exchange).strip()
|
||||
|
||||
if keyx.lower() in 'ecdhe':
|
||||
start_key_exchange(acco, user, nick, contact_list, settings, queues, gateway)
|
||||
|
||||
elif keyx.lower() in 'psk':
|
||||
new_psk( acco, user, nick, contact_list, settings, queues)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
raise FunctionReturn("Contact creation aborted.")
|
||||
|
||||
|
||||
def remove_contact(user_input: 'UserInput',
|
||||
window: 'Window',
|
||||
contact_list: 'ContactList',
|
||||
group_list: 'GroupList',
|
||||
settings: 'Settings',
|
||||
queues: Dict[bytes, 'Queue']) -> None:
|
||||
"""Remove contact on TxM/RxM."""
|
||||
if settings.session_trickle:
|
||||
raise FunctionReturn("Command disabled during trickle connection.")
|
||||
|
||||
try:
|
||||
selection = user_input.plaintext.split()[1]
|
||||
except IndexError:
|
||||
raise FunctionReturn("Error: No account specified.")
|
||||
|
||||
if not yes(f"Remove {selection} completely?", head=1):
|
||||
raise FunctionReturn("Removal of contact aborted.")
|
||||
|
||||
# Load account if user enters nick
|
||||
if selection in contact_list.get_list_of_nicks():
|
||||
selection = contact_list.get_contact(selection).rx_account
|
||||
|
||||
packet = CONTACT_REMOVE_HEADER + selection.encode()
|
||||
queue_command(packet, settings, queues[COMMAND_PACKET_QUEUE])
|
||||
|
||||
if selection in contact_list.get_list_of_accounts():
|
||||
queues[KEY_MANAGEMENT_QUEUE].put(('REM', selection))
|
||||
contact_list.remove_contact(selection)
|
||||
box_print(f"Removed {selection} from contacts.", head=1, tail=1)
|
||||
else:
|
||||
box_print(f"TxM has no {selection} to remove.", head=1, tail=1)
|
||||
|
||||
if any([g.remove_members([selection]) for g in group_list]):
|
||||
box_print(f"Removed {selection} from group(s).", tail=1)
|
||||
|
||||
for c in window:
|
||||
if selection == c.rx_account:
|
||||
if window.type == 'contact':
|
||||
window.deselect()
|
||||
elif window.type == 'group':
|
||||
window.update_group_win_members(group_list)
|
||||
|
||||
# If last member from group is removed, deselect group.
|
||||
# This is not done in update_group_win_members because
|
||||
# It would prevent selecting the empty group for group
|
||||
# related commands such as notifications.
|
||||
if not window.window_contacts:
|
||||
window.deselect()
|
||||
|
||||
|
||||
def change_nick(user_input: 'UserInput',
|
||||
window: 'Window',
|
||||
contact_list: 'ContactList',
|
||||
group_list: 'GroupList',
|
||||
settings: 'Settings',
|
||||
c_queue: 'Queue') -> None:
|
||||
"""Change nick of contact."""
|
||||
if window.type == 'group':
|
||||
raise FunctionReturn("Error: Group is selected.")
|
||||
|
||||
try:
|
||||
nick = user_input.plaintext.split()[1]
|
||||
except IndexError:
|
||||
raise FunctionReturn("Error: No nick specified.")
|
||||
|
||||
rx_acco = window.contact.rx_account
|
||||
success, error_msg = validate_nick(nick, (contact_list, group_list, rx_acco))
|
||||
if not success:
|
||||
raise FunctionReturn(error_msg)
|
||||
window.contact.nick = nick
|
||||
window.name = nick
|
||||
contact_list.store_contacts()
|
||||
|
||||
packet = CHANGE_NICK_HEADER + rx_acco.encode() + US_BYTE + nick.encode()
|
||||
queue_command(packet, settings, c_queue)
|
||||
|
||||
box_print(f"Changed {rx_acco} nick to {nick}.")
|
||||
|
||||
|
||||
def contact_setting(user_input: 'UserInput',
|
||||
window: 'Window',
|
||||
contact_list: 'ContactList',
|
||||
group_list: 'GroupList',
|
||||
settings: 'Settings',
|
||||
c_queue: 'Queue') -> None:
|
||||
"""Change logging, file reception, or message notification setting of (all) contact(s)."""
|
||||
try:
|
||||
parameters = user_input.plaintext.split()
|
||||
cmd_key = parameters[0]
|
||||
cmd_header = dict(logging=CHANGE_LOGGING_HEADER,
|
||||
store =CHANGE_FILE_R_HEADER,
|
||||
notify =CHANGE_NOTIFY_HEADER)[cmd_key]
|
||||
|
||||
s_value = dict(on=b'e', off=b'd' )[parameters[1]]
|
||||
b_value = dict(on=True, off=False)[parameters[1]]
|
||||
|
||||
except (IndexError, KeyError):
|
||||
raise FunctionReturn("Error: Invalid command.")
|
||||
|
||||
# If second parameter 'all' is included, apply setting for all contacts and groups
|
||||
try:
|
||||
target = b''
|
||||
if parameters[2] == 'all':
|
||||
cmd_value = s_value.upper()
|
||||
else:
|
||||
raise FunctionReturn("Error: Invalid command.")
|
||||
except IndexError:
|
||||
target = window.uid.encode()
|
||||
cmd_value = s_value + US_BYTE + target
|
||||
|
||||
if target:
|
||||
if window.type == 'contact':
|
||||
if cmd_key == 'logging': window.contact.log_messages = b_value
|
||||
if cmd_key == 'store': window.contact.file_reception = b_value
|
||||
if cmd_key == 'notify': window.contact.notifications = b_value
|
||||
contact_list.store_contacts()
|
||||
|
||||
if window.type == 'group':
|
||||
if cmd_key == 'logging': window.group.log_messages = b_value
|
||||
if cmd_key == 'store':
|
||||
for c in window:
|
||||
c.file_reception = b_value
|
||||
if cmd_key == 'notify': window.group.notifications = b_value
|
||||
group_list.store_groups()
|
||||
|
||||
else:
|
||||
for contact in contact_list:
|
||||
if cmd_key == 'logging': contact.log_messages = b_value
|
||||
if cmd_key == 'store': contact.file_reception = b_value
|
||||
if cmd_key == 'notify': contact.notifications = b_value
|
||||
contact_list.store_contacts()
|
||||
|
||||
for group in group_list:
|
||||
if cmd_key == 'logging': group.log_messages = b_value
|
||||
if cmd_key == 'notify': group.notifications = b_value
|
||||
group_list.store_groups()
|
||||
|
||||
packet = cmd_header + cmd_value
|
||||
queue_command(packet, settings, c_queue)
|
||||
|
||||
|
||||
def fingerprints(window: 'Window') -> None:
|
||||
"""Print domain separated fingerprints of shared secret on TxM."""
|
||||
if window.type == 'group':
|
||||
raise FunctionReturn('Group is selected.')
|
||||
|
||||
if window.contact.tx_fingerprint == bytes(32):
|
||||
raise FunctionReturn(f"Key have been pre-shared with {window.name} and thus have no fingerprints.")
|
||||
|
||||
clear_screen()
|
||||
print_fingerprints(window.contact.tx_fingerprint, " Your fingerprint (you read) ")
|
||||
print_fingerprints(window.contact.rx_fingerprint, "Contact's fingerprint (they read)")
|
||||
print('')
|
|
@ -0,0 +1,250 @@
|
|||
#!/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 base64
|
||||
import datetime
|
||||
import os
|
||||
import typing
|
||||
import zlib
|
||||
|
||||
from src.common.crypto import byte_padding, encrypt_and_sign, keygen
|
||||
from src.common.encoding import int_to_bytes
|
||||
from src.common.errors import FunctionReturn
|
||||
from src.common.input import yes
|
||||
from src.common.misc import split_byte_string
|
||||
from src.common.path import ask_path_gui
|
||||
from src.common.reed_solomon import RSCodec
|
||||
from src.common.statics import *
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from multiprocessing import Queue
|
||||
from src.common.db_settings import Settings
|
||||
from src.common.gateway import Gateway
|
||||
from src.tx.windows import Window
|
||||
|
||||
class File(object):
|
||||
"""File object wraps methods around file data/header processing."""
|
||||
|
||||
def __init__(self,
|
||||
path: str,
|
||||
window: 'Window',
|
||||
settings: 'Settings',
|
||||
gateway: 'Gateway') -> None:
|
||||
"""Load file data from specified path and add headers."""
|
||||
self.path = path
|
||||
self.window = window
|
||||
self.settings = settings
|
||||
self.gateway = gateway
|
||||
self.type = 'file'
|
||||
|
||||
self.name = None # type: bytes
|
||||
self.size = None # type: bytes
|
||||
self.data = None # type: bytes
|
||||
|
||||
self.time_s = ''
|
||||
self.time_l = b'00d 00h 00m 00s'
|
||||
self.plaintext = b''
|
||||
|
||||
self.verify_file_exists()
|
||||
self.get_file_size()
|
||||
self.get_file_name()
|
||||
self.load_file_data()
|
||||
self.compress_file_data()
|
||||
self.encrypt_file_data()
|
||||
self.encode_file_data()
|
||||
self.header_length_check()
|
||||
self.finalize()
|
||||
|
||||
def verify_file_exists(self) -> None:
|
||||
"""Check that file exists (when specified in CLI)."""
|
||||
if not os.path.isfile(self.path):
|
||||
raise FunctionReturn("Error: File not found.")
|
||||
|
||||
def get_file_size(self) -> None:
|
||||
"""Get size of file."""
|
||||
size_bytes = os.path.getsize(self.path)
|
||||
if size_bytes == 0:
|
||||
raise FunctionReturn("Error: Target file is empty. No file was sent.")
|
||||
self.size = File.readable_size(size_bytes)
|
||||
|
||||
def get_file_name(self) -> None:
|
||||
"""Parse name of file."""
|
||||
self.name = (self.path.split('/')[-1]).encode()
|
||||
|
||||
def load_file_data(self) -> None:
|
||||
"""Load binary data of file."""
|
||||
with open(self.path, 'rb') as f:
|
||||
self.data = f.read()
|
||||
|
||||
def compress_file_data(self) -> None:
|
||||
"""Compress file for faster delivery."""
|
||||
self.data = zlib.compress(self.data, level=9)
|
||||
|
||||
def encrypt_file_data(self) -> None:
|
||||
"""Encrypt file data with inner layer.
|
||||
|
||||
This prevents decryption of partially received data if user cancels file transmission.
|
||||
"""
|
||||
file_key = keygen()
|
||||
self.data = encrypt_and_sign(self.data, key=file_key)
|
||||
self.data += file_key
|
||||
|
||||
def encode_file_data(self) -> None:
|
||||
"""Encode file data with Base85.
|
||||
|
||||
This prevents inner ciphertext from
|
||||
colliding with file header delimiters.
|
||||
"""
|
||||
self.data = base64.b85encode(self.data)
|
||||
|
||||
def header_length_check(self) -> None:
|
||||
"""Ensure that file header fits the first packet."""
|
||||
header = US_BYTE.join([self.name, bytearray(8), self.size, self.time_l, US_BYTE])
|
||||
if len(header) > 254:
|
||||
raise FunctionReturn("Error: File name is too long. No file was sent.")
|
||||
|
||||
def finalize(self) -> None:
|
||||
"""Finalize packet and generate plaintext."""
|
||||
self.update_delivery_time()
|
||||
self.plaintext = US_BYTE.join([self.name, self.size, self.time_l, self.data])
|
||||
|
||||
def update_delivery_time(self) -> None:
|
||||
"""Calculate transmission time.
|
||||
|
||||
Transmission time is based on average delays and settings.
|
||||
"""
|
||||
packet_data = US_BYTE.join([self.name, self.size, self.time_l, self.data])
|
||||
|
||||
if len(packet_data) < 255:
|
||||
no_packets = 1
|
||||
else:
|
||||
packet_data = bytes(8) + packet_data
|
||||
packet_data = byte_padding(packet_data)
|
||||
no_packets = len(split_byte_string(packet_data, item_len=255))
|
||||
|
||||
no_recipients = len(self.window)
|
||||
|
||||
if self.settings.session_trickle:
|
||||
avg_delay = self.settings.trickle_stat_delay + (self.settings.trickle_rand_delay / 2)
|
||||
|
||||
if self.settings.long_packet_rand_d:
|
||||
avg_delay += (self.settings.max_val_for_rand_d / 2)
|
||||
|
||||
# Multiply by two as trickle sends a command packet between every file packet.
|
||||
total_time = 2 * no_recipients * no_packets * avg_delay
|
||||
|
||||
# Add constant time queue load time
|
||||
total_time += no_packets * TRICKLE_QUEUE_CHECK_DELAY
|
||||
|
||||
else:
|
||||
total_data = 0
|
||||
rs = RSCodec(2 * self.settings.session_ec_ratio)
|
||||
static_data_len = (1 + 24 + 8 + 16 + 24 + 256 + 16 + 1) # header + nonce + harac-ct + tag + nonce + ass. p. ct + tag + US_BYTE
|
||||
for c in self.window.window_contacts:
|
||||
data_len = static_data_len + (len(c.rx_account.encode()) + len(c.tx_account.encode()))
|
||||
enc_data_len = len(rs.encode((os.urandom(data_len))))
|
||||
total_data += (no_packets * enc_data_len)
|
||||
|
||||
total_time = 0.0
|
||||
if not self.settings.local_testing_mode:
|
||||
bauds_in_byte = 10
|
||||
total_bauds = total_data * bauds_in_byte
|
||||
total_time += total_bauds / self.settings.session_if_speed
|
||||
|
||||
total_time += no_packets * self.gateway.delay
|
||||
|
||||
if self.settings.long_packet_rand_d:
|
||||
total_time += no_packets * (self.settings.max_val_for_rand_d / 2)
|
||||
|
||||
delta_seconds = datetime.timedelta(seconds=int(total_time))
|
||||
delivery_time = datetime.datetime(1, 1, 1) + delta_seconds
|
||||
|
||||
# Format delivery time string
|
||||
if delivery_time.second == 0:
|
||||
self.time_s = '00s'
|
||||
self.time_l = b'00d 00h 00m 00s'
|
||||
return None
|
||||
|
||||
time_l_str = ''
|
||||
self.time_s = ''
|
||||
|
||||
for i in [(delivery_time.day - 1, 'd'), (delivery_time.hour, 'h'),
|
||||
(delivery_time.minute, 'm'), (delivery_time.second, 's')]:
|
||||
if i[0] > 0:
|
||||
self.time_s += str(i[0]).zfill(2) + f'{i[1]} '
|
||||
time_l_str += str(i[0]).zfill(2) + f'{i[1]} '
|
||||
|
||||
self.time_s = self.time_s.strip(' ')
|
||||
time_l_str.strip()
|
||||
self.time_l = time_l_str.encode()
|
||||
|
||||
@classmethod
|
||||
def readable_size(cls, size: int) -> bytes:
|
||||
"""Convert file size from bytes to human readable form."""
|
||||
f_size = float(size)
|
||||
for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']:
|
||||
if abs(f_size) < 1024.0:
|
||||
return '{:3.1f}{}B'.format(f_size, unit).encode()
|
||||
f_size /= 1024.0
|
||||
|
||||
return '{:.1f}{}B'.format(f_size, 'Y').encode()
|
||||
|
||||
|
||||
def queue_file(window: 'Window',
|
||||
settings: 'Settings',
|
||||
f_queue: 'Queue',
|
||||
gateway: 'Gateway') -> None:
|
||||
"""Ask file path and load file data."""
|
||||
path = ask_path_gui("Select file to send...", settings, get_file=True)
|
||||
file = File(path, window, settings, gateway)
|
||||
name = file.name.decode()
|
||||
size = file.size.decode()
|
||||
payload = file.plaintext
|
||||
|
||||
if len(payload) < 255:
|
||||
padded = byte_padding(payload)
|
||||
packet_list = [F_S_HEADER + padded]
|
||||
else:
|
||||
payload = bytes(8) + payload
|
||||
padded = byte_padding(payload)
|
||||
p_list = split_byte_string(padded, item_len=255)
|
||||
|
||||
# < number of packets >
|
||||
packet_list = ([F_L_HEADER + int_to_bytes(len(p_list)) + p_list[0][8:]] +
|
||||
[F_A_HEADER + p for p in p_list[1:-1]] +
|
||||
[F_E_HEADER + p_list[-1]])
|
||||
|
||||
for p in packet_list:
|
||||
assert len(p) == 256
|
||||
|
||||
if settings.confirm_sent_files:
|
||||
if not yes(f"Send {name} ({size}) to {window.type} {window.name} "
|
||||
f"({len(packet_list)} packets, time: {file.time_s})?", tail=1):
|
||||
raise FunctionReturn("File selection aborted.")
|
||||
|
||||
if settings.session_trickle:
|
||||
log_m_dictionary = dict((c.rx_account, c.log_messages) for c in window)
|
||||
for p in packet_list:
|
||||
f_queue.put((p, log_m_dictionary))
|
||||
|
||||
else:
|
||||
for c in window:
|
||||
for p in packet_list:
|
||||
f_queue.put((p, settings, c.rx_account, c.tx_account, c.log_messages, window.uid))
|
|
@ -0,0 +1,343 @@
|
|||
#!/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 Dict
|
||||
|
||||
import nacl.encoding
|
||||
import nacl.public
|
||||
|
||||
from src.common.crypto import argon2_kdf, encrypt_and_sign, hash_chain, keygen
|
||||
from src.common.db_masterkey import MasterKey
|
||||
from src.common.encoding import b58encode
|
||||
from src.common.errors import FunctionReturn
|
||||
from src.common.input import get_b58_key, nh_bypass_msg, yes
|
||||
from src.common.misc import clear_screen, get_tty_w, split_string
|
||||
from src.common.output import box_print, c_print, message_printer, phase, print_fingerprints, print_on_previous_line
|
||||
from src.common.path import ask_path_gui
|
||||
from src.common.statics import *
|
||||
from src.tx.packet import queue_command, transmit
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from multiprocessing import Queue
|
||||
from src.common.db_contacts import ContactList
|
||||
from src.common.db_settings import Settings
|
||||
from src.common.gateway import Gateway
|
||||
from src.tx.windows import Window
|
||||
|
||||
|
||||
###############################################################################
|
||||
# LOCAL KEY #
|
||||
###############################################################################
|
||||
|
||||
def ask_confirmation_code() -> str:
|
||||
"""Ask user to input confirmation code from RxM to verify local key has been installed."""
|
||||
title = "Enter confirmation code (from RxM): "
|
||||
|
||||
upper_line = ('┌' + (len(title) + 8) * '─' + '┐')
|
||||
title_line = ('│' + title + 8 * ' ' + '│')
|
||||
lower_line = ('└' + (len(title) + 8) * '─' + '┘')
|
||||
|
||||
ttyw = get_tty_w()
|
||||
|
||||
upper_line = upper_line.center(ttyw)
|
||||
title_line = title_line.center(ttyw)
|
||||
lower_line = lower_line.center(ttyw)
|
||||
|
||||
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 print_kdk(kdk_bytes: bytes, settings: 'Settings') -> None:
|
||||
"""Print symmetric key decryption key.
|
||||
|
||||
If local testing is not enabled, this function will add spacing between
|
||||
key decryption key to help user keep track of key typing progress. The
|
||||
length of the Base58 encoded key varies between 48..50 characters, thus
|
||||
spacing is adjusted to get even length for each substring.
|
||||
|
||||
:param kdk_bytes: Key decryption key
|
||||
:param settings: Settings object
|
||||
:return: None
|
||||
"""
|
||||
kdk_enc = b58encode(kdk_bytes)
|
||||
ssl = {48: 8, 49: 7, 50: 5}.get(len(kdk_enc), 5)
|
||||
kdk = kdk_enc if settings.local_testing_mode else ' '.join(split_string(kdk_enc, item_len=ssl))
|
||||
|
||||
box_print(["Local key decryption key (to RxM)", kdk])
|
||||
|
||||
|
||||
def new_local_key(contact_list: 'ContactList',
|
||||
settings: 'Settings',
|
||||
queues: Dict[bytes, 'Queue'],
|
||||
gateway: 'Gateway') -> None:
|
||||
"""Run local key agreement protocol.
|
||||
|
||||
Local key encrypts commands and data sent from TxM to RxM. The key is
|
||||
delivered to RxM in packet encrypted with an ephemeral symmetric key.
|
||||
The checksummed Base58 format decryption key is typed on RxM manually.
|
||||
"""
|
||||
try:
|
||||
if contact_list.has_local_contact and settings.session_trickle:
|
||||
raise FunctionReturn("Command disabled during trickle connection.")
|
||||
|
||||
clear_screen()
|
||||
c_print("Local key setup", head=1, tail=1)
|
||||
|
||||
conf_code = os.urandom(1)
|
||||
key = keygen()
|
||||
hek = keygen()
|
||||
kek = keygen()
|
||||
packet = LOCAL_KEY_PACKET_HEADER + encrypt_and_sign(key + hek + conf_code, key=kek)
|
||||
|
||||
nh_bypass_msg('start', settings)
|
||||
transmit(packet, settings, gateway)
|
||||
|
||||
while True:
|
||||
print_kdk(kek, settings)
|
||||
purp_code = ask_confirmation_code()
|
||||
if purp_code == conf_code.hex():
|
||||
print('')
|
||||
break
|
||||
elif purp_code == 'resend':
|
||||
phase("Resending local key", head=2)
|
||||
transmit(packet, settings, gateway)
|
||||
phase('Done')
|
||||
print_on_previous_line(reps=9)
|
||||
else:
|
||||
box_print(["Incorrect confirmation code. If RxM did not receive",
|
||||
"encrypted local key, resend it by typing 'resend'."], head=1)
|
||||
print_on_previous_line(reps=11, delay=2)
|
||||
|
||||
nh_bypass_msg('finish', settings)
|
||||
|
||||
# Add local contact to contact list database
|
||||
contact_list.add_contact('local', 'local', 'local',
|
||||
bytes(32), bytes(32),
|
||||
False, False, False)
|
||||
|
||||
# Add local contact to keyset database
|
||||
queues[KEY_MANAGEMENT_QUEUE].put(('ADD', 'local', key, bytes(32), hek, bytes(32)))
|
||||
|
||||
# Notify RxM that confirmation code was successfully entered.
|
||||
queue_command(LOCAL_KEY_INSTALLED_HEADER, settings, queues[COMMAND_PACKET_QUEUE])
|
||||
|
||||
box_print(["Successfully added a new local key."])
|
||||
clear_screen(delay=1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
raise FunctionReturn("Local key setup aborted.", delay=1)
|
||||
|
||||
|
||||
###############################################################################
|
||||
# X25519 #
|
||||
###############################################################################
|
||||
|
||||
def verify_fingerprints(tx_fp: bytes, rx_fp: bytes) -> bool:
|
||||
"""Verify fingerprints over off-band channel to detect MITM attacks between NHs.
|
||||
|
||||
:param tx_fp: User's fingerprint
|
||||
:param rx_fp: Contact's fingerprint
|
||||
:return: True if fingerprints match, else False
|
||||
"""
|
||||
clear_screen()
|
||||
|
||||
message_printer("To verify the public key was not swapped during delivery, "
|
||||
"call your contact over end-to-end encrypted line, preferably "
|
||||
"Signal by Open Whisper Systems. Verify call's Short "
|
||||
"Authentication String and then compare fingerprints below.", head=1, tail=1)
|
||||
|
||||
print_fingerprints(tx_fp, " Your fingerprint (you read) ")
|
||||
print_fingerprints(rx_fp, "Purported fingerprint for contact (they read)")
|
||||
|
||||
return yes("Is the contact's fingerprint correct?")
|
||||
|
||||
|
||||
def start_key_exchange(account: str,
|
||||
user: str,
|
||||
nick: str,
|
||||
contact_list: 'ContactList',
|
||||
settings: 'Settings',
|
||||
queues: Dict[bytes, 'Queue'],
|
||||
gateway: 'Gateway') -> None:
|
||||
"""Start X25519 key exchange with recipient.
|
||||
|
||||
Variable naming:
|
||||
|
||||
tx = user's key rx = contact's key
|
||||
sk = private (secret) key pk = public key
|
||||
key = message key hek = header key
|
||||
dh_ssk = DH shared secret
|
||||
|
||||
:param account: The contact's account name (e.g. alice@jabber.org)
|
||||
:param user: The user's account name (e.g. bob@jabber.org)
|
||||
:param nick: Contact's nickname
|
||||
:param contact_list: Contact list object
|
||||
:param settings: Settings object
|
||||
:param queues: Dictionary of multiprocessing queues
|
||||
:param gateway: Gateway object
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
tx_sk = nacl.public.PrivateKey.generate()
|
||||
tx_pk = bytes(tx_sk.public_key)
|
||||
|
||||
transmit(PUBLIC_KEY_PACKET_HEADER
|
||||
+ tx_pk
|
||||
+ user.encode()
|
||||
+ US_BYTE
|
||||
+ account.encode(),
|
||||
settings, gateway)
|
||||
|
||||
rx_pk = nacl.public.PublicKey(get_b58_key('pubkey'))
|
||||
dh_box = nacl.public.Box(tx_sk, rx_pk)
|
||||
dh_ssk = dh_box.shared_key()
|
||||
rx_pk = bytes(rx_pk)
|
||||
|
||||
# Domain separate each key with key-type specific byte-string and
|
||||
# with public keys that both clients know which way to place.
|
||||
tx_key = hash_chain(dh_ssk + rx_pk + b'message_key')
|
||||
rx_key = hash_chain(dh_ssk + tx_pk + b'message_key')
|
||||
|
||||
tx_hek = hash_chain(dh_ssk + rx_pk + b'header_key')
|
||||
rx_hek = hash_chain(dh_ssk + tx_pk + b'header_key')
|
||||
|
||||
# Domain separate fingerprints of public keys by using the shared
|
||||
# secret as salt. This way entities who might monitor fingerprint
|
||||
# verification channel are unable to correlate spoken values with
|
||||
# public keys that transit through a compromised IM server. This
|
||||
# protects against deanonymization of IM accounts in cases where
|
||||
# clients connect to the compromised server via Tor.
|
||||
tx_fp = hash_chain(dh_ssk + tx_pk + b'fingerprint')
|
||||
rx_fp = hash_chain(dh_ssk + rx_pk + b'fingerprint')
|
||||
|
||||
if not verify_fingerprints(tx_fp, rx_fp):
|
||||
box_print(["Possible man-in-the-middle attack detected.",
|
||||
"Aborting key exchange for your safety."], tail=1)
|
||||
raise FunctionReturn("Fingerprint mismatch", output=False, delay=2.5)
|
||||
|
||||
packet = KEY_EX_ECDHE_HEADER \
|
||||
+ tx_key + tx_hek \
|
||||
+ rx_key + rx_hek \
|
||||
+ account.encode() + US_BYTE + nick.encode()
|
||||
|
||||
queue_command(packet, settings, queues[COMMAND_PACKET_QUEUE])
|
||||
|
||||
contact_list.add_contact(account, user, nick,
|
||||
tx_fp, rx_fp,
|
||||
settings.log_msg_by_default,
|
||||
settings.store_file_default,
|
||||
settings.n_m_notify_privacy)
|
||||
|
||||
# Null-bytes below are fillers for Rx-keys not used by TxM.
|
||||
queues[KEY_MANAGEMENT_QUEUE].put(('ADD', account, tx_key, bytes(32), tx_hek, bytes(32)))
|
||||
|
||||
box_print([f"Successfully added {nick}."])
|
||||
clear_screen(delay=1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
raise FunctionReturn("Key exchange aborted.", delay=1)
|
||||
|
||||
|
||||
###############################################################################
|
||||
# PSK #
|
||||
###############################################################################
|
||||
|
||||
def new_psk(account: str,
|
||||
user: str,
|
||||
nick: str,
|
||||
contact_list: 'ContactList',
|
||||
settings: 'Settings',
|
||||
queues: Dict[bytes, 'Queue']) -> None:
|
||||
"""Generate new pre-shared key for manual key delivery.
|
||||
|
||||
:param account: The contact's account name (e.g. alice@jabber.org)
|
||||
:param user: The user's account name (e.g. bob@jabber.org)
|
||||
:param nick: Nick of contact
|
||||
:param contact_list: Contact list object
|
||||
:param settings: Settings object
|
||||
:param queues: Dictionary of multiprocessing queues
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
tx_key = keygen()
|
||||
tx_hek = keygen()
|
||||
salt = keygen()
|
||||
password = MasterKey.new_password("password for PSK")
|
||||
|
||||
phase("Deriving key encryption key", head=2)
|
||||
kek, _ = argon2_kdf(password, salt, rounds=16, memory=128000, parallelism=1)
|
||||
phase('Done')
|
||||
|
||||
ct_tag = encrypt_and_sign(tx_key + tx_hek, key=kek)
|
||||
store_d = ask_path_gui(f"Select removable media for {nick}", settings)
|
||||
f_name = f"{store_d}/{user}.psk - Give to {account}"
|
||||
|
||||
try:
|
||||
with open(f_name, 'wb+') as f:
|
||||
f.write(salt + ct_tag)
|
||||
except PermissionError:
|
||||
raise FunctionReturn("Error: Did not have permission to write to directory.")
|
||||
|
||||
packet = KEY_EX_PSK_TX_HEADER \
|
||||
+ tx_key \
|
||||
+ tx_hek \
|
||||
+ account.encode() + US_BYTE + nick.encode()
|
||||
|
||||
queue_command(packet, settings, queues[COMMAND_PACKET_QUEUE])
|
||||
|
||||
contact_list.add_contact(account, user, nick,
|
||||
bytes(32), bytes(32),
|
||||
settings.log_msg_by_default,
|
||||
settings.store_file_default,
|
||||
settings.n_m_notify_privacy)
|
||||
|
||||
queues[KEY_MANAGEMENT_QUEUE].put(('ADD', account, tx_key, bytes(32), tx_hek, bytes(32)))
|
||||
|
||||
box_print([f"Successfully added {nick}."], head=1)
|
||||
clear_screen(delay=1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
raise FunctionReturn("PSK generation aborted.")
|
||||
|
||||
|
||||
def rxm_load_psk(window: 'Window',
|
||||
contact_list: 'ContactList',
|
||||
settings: 'Settings',
|
||||
c_queue: 'Queue') -> None:
|
||||
"""Load PSK for selected contact on RxM."""
|
||||
if settings.session_trickle:
|
||||
raise FunctionReturn("Command disabled during trickle connection.")
|
||||
|
||||
if window.type == 'group':
|
||||
raise FunctionReturn("Group is selected.")
|
||||
|
||||
if contact_list.get_contact(window.uid).tx_fingerprint != bytes(32):
|
||||
raise FunctionReturn("Current key was exchanged with X25519.")
|
||||
|
||||
packet = KEY_EX_PSK_RX_HEADER + window.uid.encode()
|
||||
queue_command(packet, settings, c_queue)
|
|
@ -0,0 +1,98 @@
|
|||
#!/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 time
|
||||
import typing
|
||||
import zlib
|
||||
|
||||
from typing import Union
|
||||
|
||||
from src.common.crypto import byte_padding, encrypt_and_sign, keygen
|
||||
from src.common.encoding import double_to_bytes
|
||||
from src.common.misc import split_byte_string
|
||||
from src.common.statics import *
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from multiprocessing import Queue
|
||||
from src.common.db_settings import Settings
|
||||
from src.tx.user_input import UserInput
|
||||
from src.tx.windows import Window
|
||||
from src.tx.commands_g import MockWindow
|
||||
|
||||
|
||||
class Message(object):
|
||||
"""The message object is an automatically generated message.
|
||||
|
||||
It has same the attributes as messages created from UserInput object.
|
||||
"""
|
||||
def __init__(self, plaintext: str) -> None:
|
||||
self.plaintext = plaintext
|
||||
self.type = 'message'
|
||||
|
||||
|
||||
def queue_message(user_input: Union['UserInput', 'Message'],
|
||||
window: Union['MockWindow', 'Window'],
|
||||
settings: 'Settings',
|
||||
m_queue: 'Queue',
|
||||
header: bytes = b'') -> None:
|
||||
"""Convert message into set of assembly packets and queue them.
|
||||
|
||||
:param user_input: UserInput object
|
||||
:param window: Window object
|
||||
:param settings: Settings object
|
||||
:param m_queue: Multiprocessing message queue
|
||||
:param header: Overrides message header with group management header
|
||||
:return: None
|
||||
"""
|
||||
if not header:
|
||||
if window.type == 'group':
|
||||
timestamp = double_to_bytes(time.time() * 1000)
|
||||
header = GROUP_MESSAGE_HEADER + timestamp + window.name.encode() + US_BYTE
|
||||
else:
|
||||
header = PRIVATE_MESSAGE_HEADER
|
||||
|
||||
plaintext = user_input.plaintext.encode()
|
||||
payload = header + plaintext
|
||||
payload = zlib.compress(payload, level=9)
|
||||
|
||||
if len(payload) < 255:
|
||||
padded = byte_padding(payload)
|
||||
packet_list = [M_S_HEADER + padded]
|
||||
else:
|
||||
msg_key = keygen()
|
||||
payload = encrypt_and_sign(payload, msg_key)
|
||||
payload += msg_key
|
||||
padded = byte_padding(payload)
|
||||
p_list = split_byte_string(padded, item_len=255)
|
||||
|
||||
packet_list = ([M_L_HEADER + p_list[0]] +
|
||||
[M_A_HEADER + p for p in p_list[1:-1]] +
|
||||
[M_E_HEADER + p_list[-1]])
|
||||
|
||||
if settings.session_trickle:
|
||||
log_m_dictionary = dict((c.rx_account, c.log_messages) for c in window)
|
||||
for p in packet_list:
|
||||
m_queue.put((p, log_m_dictionary))
|
||||
|
||||
else:
|
||||
for c in window:
|
||||
log_setting = window.group.log_messages if window.type == 'group' else c.log_messages
|
||||
for p in packet_list:
|
||||
m_queue.put((p, settings, c.rx_account, c.tx_account, log_setting, window.uid))
|
|
@ -0,0 +1,185 @@
|
|||
#!/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 random
|
||||
import time
|
||||
import typing
|
||||
import zlib
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from src.common.crypto import byte_padding, encrypt_and_sign, hash_chain
|
||||
from src.common.encoding import int_to_bytes
|
||||
from src.common.errors import CriticalError
|
||||
from src.common.misc import split_byte_string
|
||||
from src.common.output import c_print
|
||||
from src.common.reed_solomon import RSCodec
|
||||
from src.common.statics import *
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from multiprocessing import Queue
|
||||
from src.common.db_keys import KeyList
|
||||
from src.common.db_settings import Settings
|
||||
from src.common.gateway import Gateway
|
||||
from src.tx.user_input import UserInput
|
||||
from src.tx.windows import Window
|
||||
|
||||
|
||||
def queue_command(payload: bytes,
|
||||
settings: 'Settings',
|
||||
c_queue: 'Queue') -> None:
|
||||
"""Split command into assembly packets and queue them.
|
||||
|
||||
:param payload: Command's plaintext string.
|
||||
:param settings: Settings object
|
||||
:param c_queue: Multiprocessing queue for commands
|
||||
:return: None
|
||||
"""
|
||||
payload = zlib.compress(payload, level=9)
|
||||
|
||||
if len(payload) < 255:
|
||||
padded = byte_padding(payload)
|
||||
packet_list = [C_S_HEADER + padded]
|
||||
else:
|
||||
payload += hash_chain(payload)
|
||||
padded = byte_padding(payload)
|
||||
p_list = split_byte_string(padded, item_len=255)
|
||||
|
||||
packet_list = ([C_L_HEADER + p_list[0]] +
|
||||
[C_A_HEADER + p for p in p_list[1:-1]] +
|
||||
[C_E_HEADER + p_list[-1]])
|
||||
|
||||
if settings.session_trickle:
|
||||
for p in packet_list:
|
||||
c_queue.put(p)
|
||||
else:
|
||||
for p in packet_list:
|
||||
c_queue.put((p, settings))
|
||||
|
||||
|
||||
def send_packet(packet: bytes,
|
||||
key_list: 'KeyList',
|
||||
settings: 'Settings',
|
||||
gateway: 'Gateway',
|
||||
l_queue: 'Queue',
|
||||
rx_account: str = None,
|
||||
tx_account: str = None,
|
||||
logging: bool = None) -> None:
|
||||
"""Encrypt and send assembly packet.
|
||||
|
||||
Load keys from key database, encrypt assembly packet, add
|
||||
headers, send and optionally log the assembly packet.
|
||||
|
||||
:param packet: Padded plaintext assembly packet
|
||||
:param key_list: Key list object
|
||||
:param settings: Settings object
|
||||
:param gateway: Gateway object
|
||||
:param l_queue: Multiprocessing queue for logged messages
|
||||
:param rx_account: Recipient account
|
||||
:param tx_account: Sender's account associated with recipient's account
|
||||
:param logging: When True, log the assembly packet
|
||||
:return: None
|
||||
"""
|
||||
if len(packet) != 256:
|
||||
raise CriticalError("Invalid assembly packet PT length.")
|
||||
|
||||
if rx_account is None:
|
||||
keyset = key_list.get_keyset('local')
|
||||
header = COMMAND_PACKET_HEADER
|
||||
trailer = b''
|
||||
else:
|
||||
keyset = key_list.get_keyset(rx_account)
|
||||
header = MESSAGE_PACKET_HEADER
|
||||
trailer = tx_account.encode() + US_BYTE + rx_account.encode()
|
||||
|
||||
harac_in_bytes = int_to_bytes(keyset.tx_harac)
|
||||
encrypted_harac = encrypt_and_sign(harac_in_bytes, keyset.tx_hek)
|
||||
encrypted_message = encrypt_and_sign(packet, keyset.tx_key)
|
||||
encrypted_packet = header + encrypted_harac + encrypted_message + trailer
|
||||
transmit(encrypted_packet, settings, gateway)
|
||||
|
||||
keyset.rotate_tx_key()
|
||||
|
||||
if logging and rx_account is not None:
|
||||
l_queue.put((packet, rx_account, settings, key_list.master_key))
|
||||
|
||||
|
||||
def transmit(packet: bytes, settings: 'Settings', gateway: 'Gateway') -> None:
|
||||
"""Add Reed-Solomon erasure code and output packet via gateway."""
|
||||
rs = RSCodec(2 * settings.session_ec_ratio)
|
||||
packet = rs.encode(packet)
|
||||
gateway.write(packet)
|
||||
|
||||
if not settings.session_trickle:
|
||||
if settings.long_packet_rand_d:
|
||||
random_delay = random.SystemRandom().uniform(0, settings.max_val_for_rand_d)
|
||||
time.sleep(random_delay)
|
||||
|
||||
|
||||
def cancel_packet(user_input: 'UserInput',
|
||||
window: 'Window',
|
||||
settings: 'Settings',
|
||||
queues: Dict[bytes, 'Queue']) -> None:
|
||||
"""Cancel sent message/file to contact/group."""
|
||||
command = user_input.plaintext
|
||||
queue = dict(cm=queues[MESSAGE_PACKET_QUEUE], cf=queues[FILE_PACKET_QUEUE])[command]
|
||||
cancel_pt = dict(cm=M_C_HEADER, cf=F_C_HEADER )[command] + bytes(255)
|
||||
p_type = dict(cm='messages', cf='files' )[command]
|
||||
cancel = False
|
||||
|
||||
if settings.session_trickle:
|
||||
if not queue.empty():
|
||||
cancel = True
|
||||
while not queue.empty():
|
||||
queue.get()
|
||||
log_m_dictionary = dict((c.rx_account, c.log_messages) for c in window)
|
||||
queue.put((cancel_pt, log_m_dictionary))
|
||||
|
||||
message = f"Cancelled queues {p_type}." if cancel else f"No {p_type} to cancel."
|
||||
c_print(message, head=1, tail=1)
|
||||
|
||||
else:
|
||||
p_buffer = []
|
||||
|
||||
while not queue.empty():
|
||||
packet, settings, rx_account, tx_account, logging, win = queue.get()
|
||||
|
||||
# Put messages unrelated to active window into buffer
|
||||
if win != window.uid:
|
||||
p_buffer.append((packet, settings, rx_account, tx_account, logging, win))
|
||||
else:
|
||||
cancel = True
|
||||
|
||||
# Put cancel packets for each window contact to queue first
|
||||
if cancel:
|
||||
for c in window:
|
||||
print('put cancel packet to queue')
|
||||
queue.put((cancel_pt, settings, c.rx_account, c.tx_account, c.log_messages, window.uid))
|
||||
|
||||
# Put buffered tuples back to queue
|
||||
for p in p_buffer:
|
||||
queue.put(p)
|
||||
|
||||
if cancel:
|
||||
message = f"Cancelled queued {p_type} to {window.type} {window.name}."
|
||||
else:
|
||||
message = f"No {p_type} queued for {window.type} {window.name}"
|
||||
|
||||
c_print(message, head=1, tail=1)
|
|
@ -0,0 +1,160 @@
|
|||
#!/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 time
|
||||
import typing
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from typing import List, Tuple
|
||||
|
||||
from src.common.statics import *
|
||||
from src.tx.packet import send_packet
|
||||
from src.tx.trickle import ConstantTime
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from multiprocessing import Queue
|
||||
from src.common.db_keys import KeyList
|
||||
from src.common.db_settings import Settings
|
||||
from src.common.gateway import Gateway
|
||||
from src.common.db_settings import Settings
|
||||
from src.tx.windows import Window
|
||||
|
||||
|
||||
def sender_loop(settings: 'Settings',
|
||||
queues: Dict[bytes, 'Queue'],
|
||||
gateway: 'Gateway',
|
||||
key_list: 'KeyList') -> None:
|
||||
"""Load assembly packets from queues based on their priority, encrypt and output them.
|
||||
|
||||
Sender loop handles a set of queues. As Python's multiprocessing lacks priority queues,
|
||||
several queues are prioritized based on their status. In both trickle and non-trickle
|
||||
mode, file are only transmitted when no messages are being output. This is because file
|
||||
transmission is usually very slow and user might need to send messages in the meantime.
|
||||
In normal (non-trickle) mode commands take highest priority as they are not output
|
||||
all the time. In trickle mode commands are output between each output message packet.
|
||||
This allows commands to take effect as soon as possible but slows down message/file delivery
|
||||
by half. In trickle mode each contact in window is cycled in order. Making changes to
|
||||
recipient list during use is prevented to protect user from accidentally revealing use
|
||||
of TFC. In trickle mode, if no packets are available in either m_queue or f_queue,
|
||||
a noise assembly packet is loaded from np_queue. If no command packet is available in
|
||||
c_queue, a noise command packet is loaded from nc_queue. TFC does it's best to hide the
|
||||
loading times and encryption duration by using constant time context manager and constant
|
||||
time queue status lookup, as well as constant time XSalsa20 cipher.
|
||||
"""
|
||||
m_queue = queues[MESSAGE_PACKET_QUEUE]
|
||||
f_queue = queues[FILE_PACKET_QUEUE]
|
||||
c_queue = queues[COMMAND_PACKET_QUEUE]
|
||||
l_queue = queues[LOG_PACKET_QUEUE]
|
||||
km_queue = queues[KEY_MANAGEMENT_QUEUE]
|
||||
np_queue = queues[NOISE_PACKET_QUEUE]
|
||||
nc_queue = queues[NOISE_COMMAND_QUEUE]
|
||||
ws_queue = queues[WINDOW_SELECT_QUEUE]
|
||||
|
||||
m_buffer = [] # type: List[Tuple[bytes, Settings, str, str, bool, Window]]
|
||||
f_buffer = [] # type: List[Tuple[bytes, Settings, str, str, bool, Window]]
|
||||
|
||||
|
||||
if settings.session_trickle:
|
||||
|
||||
while ws_queue.empty():
|
||||
time.sleep(0.01)
|
||||
|
||||
window = ws_queue.get()
|
||||
|
||||
while True:
|
||||
try:
|
||||
with ConstantTime(settings, length=TRICKLE_QUEUE_CHECK_DELAY):
|
||||
queue = [[m_queue, m_queue], [f_queue, np_queue]][m_queue.empty()][f_queue.empty()]
|
||||
packet, log_dict = queue.get()
|
||||
|
||||
for c in window:
|
||||
|
||||
with ConstantTime(settings, d_type='trickle'):
|
||||
send_packet(packet, key_list, settings, gateway, l_queue, c.rx_account, c.tx_account, log_dict[c.rx_account])
|
||||
|
||||
with ConstantTime(settings, d_type='trickle'):
|
||||
queue = [c_queue, nc_queue][c_queue.empty()]
|
||||
command = queue.get()
|
||||
send_packet(command, key_list, settings, gateway, l_queue)
|
||||
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
else:
|
||||
while True:
|
||||
try:
|
||||
time.sleep(0.001)
|
||||
|
||||
# Keylist database management packets have highest priority.
|
||||
if not km_queue.empty():
|
||||
command, *params = km_queue.get()
|
||||
key_list.manage(command, *params)
|
||||
continue
|
||||
|
||||
# packets from c_queue come only from local contact. Until keys for local contact
|
||||
# have been added, no command is loaded. Commands have second highest priority.
|
||||
if not c_queue.empty():
|
||||
if key_list.has_local_key():
|
||||
command, settings = c_queue.get()
|
||||
send_packet(command, key_list, settings, gateway, l_queue)
|
||||
continue
|
||||
|
||||
# Iterate through buffer list that contains tuples of transmission information
|
||||
# loaded from m_queue in the order they were placed into the buffer. As soon as
|
||||
# keys are available, send packet. Restart the loop to prioritize keylist
|
||||
# management and command packets before going through the buffer list again.
|
||||
for i, params in enumerate(m_buffer):
|
||||
packet, settings, rx_account, tx_account, logging, window = params
|
||||
if key_list.has_keyset(rx_account):
|
||||
m_buffer.pop(i)
|
||||
send_packet(packet, key_list, settings, gateway, l_queue, rx_account, tx_account, logging)
|
||||
continue
|
||||
|
||||
# Any new messages take priority only after the ones in buffer are sent.
|
||||
# If key is not on list, place the message packet into the buffer.
|
||||
if not m_queue.empty():
|
||||
packet, settings, rx_account, tx_account, logging, window = m_queue.get()
|
||||
if key_list.has_keyset(rx_account):
|
||||
send_packet(packet, key_list, settings, gateway, l_queue, rx_account, tx_account, logging)
|
||||
else:
|
||||
m_buffer.append((packet, settings, rx_account, tx_account, logging, window))
|
||||
continue
|
||||
|
||||
# When no more messages can be processed, check if the
|
||||
# file buffer has packets that can be sent to contacts.
|
||||
for i, params in enumerate(f_buffer):
|
||||
packet, settings, rx_account, tx_account, logging, window = params
|
||||
if key_list.has_keyset(rx_account):
|
||||
f_buffer.pop(i)
|
||||
send_packet(packet, key_list, settings, gateway, l_queue, rx_account, tx_account, logging)
|
||||
continue
|
||||
|
||||
# If file buffer is empty, check if new file packets are available. If there are and
|
||||
# contact has key, send file packet, otherwise place it into the file packet buffer.
|
||||
if not f_queue.empty():
|
||||
packet, settings, rx_account, tx_account, logging, window = f_queue.get()
|
||||
if key_list.has_keyset(rx_account):
|
||||
send_packet(packet, key_list, settings, gateway, l_queue, rx_account, tx_account, logging)
|
||||
else:
|
||||
f_buffer.append((packet, settings, rx_account, tx_account, logging, window))
|
||||
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
pass
|
|
@ -0,0 +1,86 @@
|
|||
#!/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 random
|
||||
import time
|
||||
import threading
|
||||
import typing
|
||||
|
||||
from typing import Dict, Tuple, Union
|
||||
|
||||
from src.common.crypto import byte_padding
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from multiprocessing import Queue
|
||||
from src.common.db_settings import Settings
|
||||
from src.common.db_contacts import ContactList
|
||||
|
||||
|
||||
class ConstantTime:
|
||||
"""Constant time context manager.
|
||||
|
||||
By joining a thread that sleeps for longer time than it takes
|
||||
for the function to run, this context manager hides the actual
|
||||
running time of the function.
|
||||
"""
|
||||
def __init__(self,
|
||||
settings: 'Settings',
|
||||
d_type: str = 'static',
|
||||
length: float = 0.0) -> None:
|
||||
|
||||
if d_type == 'trickle':
|
||||
self.length = settings.trickle_stat_delay
|
||||
self.length += random.SystemRandom().uniform(0, settings.trickle_rand_delay)
|
||||
if settings.long_packet_rand_d:
|
||||
self.length += random.SystemRandom().uniform(0, settings.max_val_for_rand_d)
|
||||
|
||||
elif d_type == 'static':
|
||||
self.length = length
|
||||
|
||||
def __enter__(self) -> None:
|
||||
self.timer = threading.Thread(target=time.sleep, args=(self.length,))
|
||||
self.timer.start()
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.timer.join()
|
||||
|
||||
|
||||
def noise_process(header: bytes,
|
||||
queue: 'Queue',
|
||||
contact_list: 'ContactList' = None) -> None:
|
||||
"""Ensure noise queues have noise packets (with padded length of 256) always available."""
|
||||
packet = header + byte_padding(header)
|
||||
|
||||
if contact_list is None:
|
||||
content = packet # type: Union[bytes, Tuple[bytes, Dict[str, bool]]]
|
||||
else:
|
||||
log_dict = dict()
|
||||
for c in contact_list:
|
||||
log_dict[c.rx_account] = False
|
||||
content = (packet, log_dict)
|
||||
|
||||
while True:
|
||||
try:
|
||||
if queue.qsize() < 1000:
|
||||
queue.put(content)
|
||||
else:
|
||||
time.sleep(0.1)
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
pass
|
|
@ -0,0 +1,95 @@
|
|||
#!/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 readline
|
||||
import sys
|
||||
import typing
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from src.common.errors import FunctionReturn
|
||||
from src.common.misc import get_tab_completer
|
||||
from src.common.statics import *
|
||||
from src.tx.commands import process_command
|
||||
from src.tx.contact import add_new_contact
|
||||
from src.tx.files import queue_file
|
||||
from src.tx.key_exchanges import new_local_key
|
||||
from src.tx.messages import queue_message
|
||||
from src.tx.user_input import UserInput
|
||||
from src.tx.windows import Window
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from multiprocessing import Queue
|
||||
from src.common.db_contacts import ContactList
|
||||
from src.common.db_groups import GroupList
|
||||
from src.common.db_masterkey import MasterKey
|
||||
from src.common.db_settings import Settings
|
||||
from src.common.gateway import Gateway
|
||||
|
||||
|
||||
def tx_loop(settings: 'Settings',
|
||||
queues: Dict[bytes, 'Queue'],
|
||||
gateway: 'Gateway',
|
||||
contact_list: 'ContactList',
|
||||
group_list: 'GroupList',
|
||||
master_key: 'MasterKey',
|
||||
file_no: int # stdin input file descriptor
|
||||
) -> None:
|
||||
"""Get input from user and process it accordingly.
|
||||
|
||||
Tx side of TFC runs two processes -- input and output loop -- separate from
|
||||
one another. This approach allows queueing assembly packets and their output
|
||||
based on priority of different packets. tx_loop handles TxM-side functions
|
||||
excluding message encryption, output and hash ratchet key/counter updates in
|
||||
key_list database and log file writes.
|
||||
"""
|
||||
sys.stdin = os.fdopen(file_no)
|
||||
window = Window(contact_list, group_list)
|
||||
|
||||
while True:
|
||||
try:
|
||||
readline.set_completer(get_tab_completer(contact_list, group_list, settings))
|
||||
readline.parse_and_bind('tab: complete')
|
||||
|
||||
window.update_group_win_members(group_list)
|
||||
|
||||
while not contact_list.has_local_contact():
|
||||
new_local_key(contact_list, settings, queues, gateway)
|
||||
|
||||
while not contact_list.has_contacts():
|
||||
add_new_contact(contact_list, group_list, settings, queues, gateway)
|
||||
|
||||
while not window.is_selected():
|
||||
window.select_tx_window(settings, queues)
|
||||
|
||||
user_input = UserInput(window, settings)
|
||||
|
||||
if user_input.type == 'message':
|
||||
queue_message(user_input, window, settings, queues[MESSAGE_PACKET_QUEUE])
|
||||
|
||||
elif user_input.type == 'file':
|
||||
queue_file(window, settings, queues[FILE_PACKET_QUEUE], gateway)
|
||||
|
||||
elif user_input.type == 'command':
|
||||
process_command(user_input, window, settings, queues, contact_list, group_list, gateway, master_key)
|
||||
|
||||
except (EOFError, FunctionReturn, KeyboardInterrupt):
|
||||
pass
|
|
@ -0,0 +1,96 @@
|
|||
#!/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 typing
|
||||
|
||||
from src.common.output import print_on_previous_line
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.common.db_settings import Settings
|
||||
from src.tx.windows import Window
|
||||
|
||||
|
||||
class UserInput(object):
|
||||
"""UserInput objects are messages, files or commands.
|
||||
|
||||
The type of created object is determined based on input by user. Commands
|
||||
start with slash, but as files are a special case of command, commands
|
||||
starting with /file are interpreted as file type. The type allows tx_loop
|
||||
to determine what function should process the user input.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
window: 'Window',
|
||||
settings: 'Settings') -> None:
|
||||
"""Create a new UserInput object."""
|
||||
self.window = window
|
||||
self.settings = settings
|
||||
self.w_type = 'group ' if window.type == 'group' else ''
|
||||
self.plaintext = self.get_input()
|
||||
self.process_aliases()
|
||||
self.type = self.detect_input_type()
|
||||
self.check_empty_group()
|
||||
|
||||
def get_input(self) -> str:
|
||||
"""Get message/command from user."""
|
||||
try:
|
||||
plaintext = input(f"Msg to {self.w_type}{self.window.name}: ")
|
||||
|
||||
# Ignore empty inputs
|
||||
if plaintext in ['', '/']:
|
||||
print_on_previous_line()
|
||||
return self.get_input()
|
||||
|
||||
return plaintext
|
||||
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print('')
|
||||
print_on_previous_line()
|
||||
return self.get_input()
|
||||
|
||||
def process_aliases(self) -> None:
|
||||
"""Check if input was an alias for existing command."""
|
||||
aliases = [(' ', '/unread'),
|
||||
(' ', '/exit' if self.settings.double_space_exits else '/clear'),
|
||||
('//', '/cmd')]
|
||||
|
||||
for a in aliases:
|
||||
if self.plaintext == a[0]:
|
||||
self.plaintext = a[1]
|
||||
print_on_previous_line()
|
||||
print(f"Msg to {self.w_type}{self.window.name}: {self.plaintext}")
|
||||
|
||||
def detect_input_type(self) -> str:
|
||||
"""Detect type of input to process."""
|
||||
if self.plaintext == '/file':
|
||||
return 'file'
|
||||
elif self.plaintext.startswith('/'):
|
||||
self.plaintext = self.plaintext[1:]
|
||||
return 'command'
|
||||
else:
|
||||
return 'message'
|
||||
|
||||
def check_empty_group(self) -> None:
|
||||
"""Notify the user if group was empty."""
|
||||
if self.type == 'message' and self.window.type == 'group' and len(self.window.window_contacts) == 0:
|
||||
print_on_previous_line()
|
||||
print(f"Msg to {self.w_type}{self.window.name}: Error: Group is empty.")
|
||||
print_on_previous_line(delay=0.5)
|
||||
self.__init__(self.window, self.settings)
|
|
@ -0,0 +1,152 @@
|
|||
#!/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 typing
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
from src.common.errors import FunctionReturn
|
||||
from src.common.misc import clear_screen
|
||||
from src.common.statics import *
|
||||
from src.tx.packet import queue_command
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from multiprocessing import Queue
|
||||
from src.common.db_contacts import Contact, ContactList
|
||||
from src.common.db_groups import Group, GroupList
|
||||
from src.common.db_settings import Settings
|
||||
from src.tx.user_input import UserInput
|
||||
|
||||
|
||||
class Window(object):
|
||||
"""
|
||||
Window objects manages ephemeral communications
|
||||
data associated with selected contact or group.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
contact_list: 'ContactList',
|
||||
group_list: 'GroupList') -> None:
|
||||
"""Create a new window object."""
|
||||
self.contact_list = contact_list
|
||||
self.group_list = group_list
|
||||
self.window_contacts = [] # type: List[Contact]
|
||||
self.group = None # type: Group
|
||||
self.contact = None # type: Contact
|
||||
self.name = None # type: str
|
||||
self.type = None # type: str
|
||||
self.uid = None # type: str
|
||||
self.imc_name = None # type: str
|
||||
|
||||
def __iter__(self) -> 'Contact':
|
||||
"""Iterate over contact objects in window."""
|
||||
for c in self.window_contacts:
|
||||
yield c
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return the number of contacts in current window."""
|
||||
return len(self.window_contacts)
|
||||
|
||||
def is_selected(self) -> bool:
|
||||
"""Return True if a window is selected, else False."""
|
||||
return self.name is not None
|
||||
|
||||
def deselect(self) -> None:
|
||||
"""Deselect active window."""
|
||||
self.window_contacts = []
|
||||
self.group = None # type: Group
|
||||
self.contact = None # type: Contact
|
||||
self.name = None # type: str
|
||||
self.type = None # type: str
|
||||
self.uid = None # type: str
|
||||
self.imc_name = None # type: str
|
||||
|
||||
def update_group_win_members(self, group_list: 'GroupList') -> None:
|
||||
"""Update window's group members list."""
|
||||
if self.type == 'group':
|
||||
if group_list.has_group(self.name):
|
||||
self.group = group_list.get_group(self.name)
|
||||
self.window_contacts = self.group.members
|
||||
if self.window_contacts:
|
||||
self.imc_name = self.window_contacts[0].rx_account
|
||||
else:
|
||||
self.deselect()
|
||||
|
||||
def select_tx_window(self,
|
||||
settings: 'Settings',
|
||||
queues: Dict[bytes, 'Queue'],
|
||||
selection: str = None,
|
||||
cmd: bool = False) -> None:
|
||||
"""Select specified window or ask the user to specify one."""
|
||||
if selection is None:
|
||||
self.contact_list.print_contacts()
|
||||
self.group_list.print_groups()
|
||||
selection = input("Select recipient: ").strip()
|
||||
|
||||
if selection in self.group_list.get_list_of_group_names():
|
||||
if cmd and settings.session_trickle and selection != self.uid:
|
||||
raise FunctionReturn("Can't change window during trickle connection.")
|
||||
|
||||
self.group = self.group_list.get_group(selection)
|
||||
self.window_contacts = self.group.members
|
||||
self.name = self.group.name
|
||||
self.uid = self.name
|
||||
self.type = 'group'
|
||||
|
||||
if self.window_contacts:
|
||||
self.imc_name = self.window_contacts[0].rx_account
|
||||
|
||||
elif selection in self.contact_list.contact_selectors():
|
||||
|
||||
if cmd and settings.session_trickle:
|
||||
contact = self.contact_list.get_contact(selection)
|
||||
if self.uid != contact.rx_account:
|
||||
raise FunctionReturn("Can't change window during trickle connection.")
|
||||
|
||||
self.contact = self.contact_list.get_contact(selection)
|
||||
self.window_contacts = [self.contact]
|
||||
self.name = self.contact.nick
|
||||
self.uid = self.contact.rx_account
|
||||
self.imc_name = self.contact.rx_account
|
||||
self.type = 'contact'
|
||||
|
||||
else:
|
||||
raise FunctionReturn("Error: No contact/group was found.")
|
||||
|
||||
if settings.session_trickle and not cmd:
|
||||
queues[WINDOW_SELECT_QUEUE].put(self.window_contacts)
|
||||
|
||||
packet = WINDOW_CHANGE_HEADER + self.uid.encode()
|
||||
queue_command(packet, settings, queues[COMMAND_PACKET_QUEUE])
|
||||
|
||||
clear_screen()
|
||||
|
||||
|
||||
def select_window(user_input: 'UserInput',
|
||||
window: 'Window',
|
||||
settings: 'Settings',
|
||||
queues: Dict[bytes, 'Queue']) -> None:
|
||||
"""Select new window for messages."""
|
||||
try:
|
||||
selection = user_input.plaintext.split()[1]
|
||||
except (IndexError, TypeError):
|
||||
raise FunctionReturn("Invalid recipient.")
|
||||
|
||||
window.select_tx_window(settings, queues, selection, cmd=True)
|
|
@ -0,0 +1,267 @@
|
|||
#!/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 binascii
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import nacl.exceptions
|
||||
import nacl.utils
|
||||
|
||||
from src.common.crypto import sha3_256, blake2s, sha256, hash_chain, argon2_kdf, encrypt_and_sign, auth_and_decrypt
|
||||
from src.common.crypto import unicode_padding, byte_padding, rm_padding_bytes, rm_padding_str, xor, keygen, init_entropy
|
||||
|
||||
|
||||
class TestSHA3256(unittest.TestCase):
|
||||
|
||||
def test_SHA3_256_KAT(self):
|
||||
"""\
|
||||
Run sanity check with official SHA3-256 KAT:
|
||||
csrc.nist.gov/groups/ST/toolkit/documents/Examples/SHA3-256_Msg0.pdf
|
||||
"""
|
||||
self.assertEqual(sha3_256(b''),
|
||||
binascii.unhexlify("a7ffc6f8bf1ed76651c14756a061d662"
|
||||
"f580ff4de43b49fa82d80a4b80f8434a"))
|
||||
|
||||
|
||||
class TestBlake2s(unittest.TestCase):
|
||||
|
||||
def test_blake2s_KAT(self):
|
||||
"""\
|
||||
Run sanity check with official Blake2s KAT:
|
||||
https://github.com/BLAKE2/BLAKE2/blob/master/testvectors/blake2s-kat.txt
|
||||
"""
|
||||
data = key = binascii.unhexlify("000102030405060708090a0b0c0d0e0f"
|
||||
"101112131415161718191a1b1c1d1e1f")
|
||||
|
||||
self.assertEqual(blake2s(data, key),
|
||||
binascii.unhexlify("c03bc642b20959cbe133a0303e0c1abf"
|
||||
"f3e31ec8e1a328ec8565c36decff5265"))
|
||||
|
||||
|
||||
class TestSHA256(unittest.TestCase):
|
||||
|
||||
def test_SHA256_KAT(self):
|
||||
"""\
|
||||
Run sanity check with official SHA256 KAT:
|
||||
http://csrc.nist.gov/groups/ST/toolkit/documents/Examples/SHA_All.pdf // page 14
|
||||
"""
|
||||
self.assertEqual(sha256(b"abc"),
|
||||
binascii.unhexlify("ba7816bf8f01cfea414140de5dae2223"
|
||||
"b00361a396177a9cb410ff61f20015ad"))
|
||||
|
||||
|
||||
class TestHashChain(unittest.TestCase):
|
||||
|
||||
def test_chain(self):
|
||||
"""Sanity check after verifying function. No official vectors are available."""
|
||||
self.assertEqual(hash_chain(bytes(32)),
|
||||
binascii.unhexlify("8d8c36497eb93a6355112e253f705a32"
|
||||
"85f3e2d82b9ac29461cd8d4f764e5d41"))
|
||||
|
||||
|
||||
class TestArgon2KDF(unittest.TestCase):
|
||||
|
||||
def test_Argon2_KAT(self):
|
||||
"""\
|
||||
The Argon2 KAT vectors are available at
|
||||
https://tools.ietf.org/html/draft-irtf-cfrg-argon2-01#section-6.2
|
||||
|
||||
However, the python bindings of argon2 package do not allow associated
|
||||
data to be input to the function, thus KAT can not be performed.
|
||||
"""
|
||||
pass
|
||||
|
||||
@unittest.skipIf("TRAVIS" in os.environ and os.environ["TRAVIS"] == "true", "Skipping this test on Travis CI.")
|
||||
def test_sanity_check(self):
|
||||
key, mem = argon2_kdf("test_password", salt=bytes(32), rounds=1, memory=128000, parallelism=1)
|
||||
|
||||
self.assertEqual(mem, 128000)
|
||||
self.assertEqual(key.hex(), "73883b6b2ea60d0adf27fb52e1f41af4"
|
||||
"29bfe8a0d79ae4a2f87be6c4d73e6a11")
|
||||
|
||||
def test_autoconf_sanity_check(self):
|
||||
key, mem = argon2_kdf("test_password", salt=bytes(32), rounds=1)
|
||||
|
||||
self.assertIsInstance(key, bytes)
|
||||
self.assertIsInstance(mem, int)
|
||||
|
||||
|
||||
def test_local_testing_sanity_check(self):
|
||||
key, mem = argon2_kdf("test_password", salt=bytes(32), rounds=1, local_testing=True)
|
||||
|
||||
self.assertIsInstance(key, bytes)
|
||||
self.assertIsInstance(mem, int)
|
||||
|
||||
|
||||
class TestXSalsa20Poly1305(unittest.TestCase):
|
||||
"""\
|
||||
Test vectors:
|
||||
https://cr.yp.to/highspeed/naclcrypto-20090310.pdf // page 35
|
||||
"""
|
||||
nonce = binascii.unhexlify("69696ee955b62b73"
|
||||
"cd62bda875fc73d6"
|
||||
"8219e0036b7a0b37")
|
||||
|
||||
key_tv = binascii.unhexlify("1b27556473e985d4"
|
||||
"62cd51197a9a46c7"
|
||||
"6009549eac6474f2"
|
||||
"06c4ee0844f68389")
|
||||
|
||||
pt_tv = binascii.unhexlify("be075fc53c81f2d5"
|
||||
"cf141316ebeb0c7b"
|
||||
"5228c52a4c62cbd4"
|
||||
"4b66849b64244ffc"
|
||||
"e5ecbaaf33bd751a"
|
||||
"1ac728d45e6c6129"
|
||||
"6cdc3c01233561f4"
|
||||
"1db66cce314adb31"
|
||||
"0e3be8250c46f06d"
|
||||
"ceea3a7fa1348057"
|
||||
"e2f6556ad6b1318a"
|
||||
"024a838f21af1fde"
|
||||
"048977eb48f59ffd"
|
||||
"4924ca1c60902e52"
|
||||
"f0a089bc76897040"
|
||||
"e082f93776384864"
|
||||
"5e0705")
|
||||
|
||||
ct_tv = binascii.unhexlify("f3ffc7703f9400e5"
|
||||
"2a7dfb4b3d3305d9"
|
||||
"8e993b9f48681273"
|
||||
"c29650ba32fc76ce"
|
||||
"48332ea7164d96a4"
|
||||
"476fb8c531a1186a"
|
||||
"c0dfc17c98dce87b"
|
||||
"4da7f011ec48c972"
|
||||
"71d2c20f9b928fe2"
|
||||
"270d6fb863d51738"
|
||||
"b48eeee314a7cc8a"
|
||||
"b932164548e526ae"
|
||||
"90224368517acfea"
|
||||
"bd6bb3732bc0e9da"
|
||||
"99832b61ca01b6de"
|
||||
"56244a9e88d5f9b3"
|
||||
"7973f622a43d14a6"
|
||||
"599b1f654cb45a74"
|
||||
"e355a5")
|
||||
|
||||
def test_encrypt_and_sign_with_kat(self):
|
||||
"""Test encryption with official test vectors"""
|
||||
# Setup
|
||||
o_nacl_utils_random = nacl.utils.random
|
||||
nacl.utils.random = lambda x: self.nonce
|
||||
|
||||
# Test
|
||||
self.assertEqual(encrypt_and_sign(self.pt_tv, self.key_tv), self.nonce + self.ct_tv)
|
||||
|
||||
# Teardown
|
||||
nacl.utils.random = o_nacl_utils_random
|
||||
|
||||
def test_auth_and_decrypt_with_kat(self):
|
||||
"""Test decryption with official test vectors"""
|
||||
self.assertEqual(auth_and_decrypt(self.nonce + self.ct_tv, self.key_tv), self.pt_tv)
|
||||
|
||||
def test_invalid_decryption_raises_critical_error(self):
|
||||
with self.assertRaises(SystemExit):
|
||||
self.assertEqual(auth_and_decrypt(self.nonce + self.ct_tv, bytes(32)), self.pt_tv)
|
||||
|
||||
def test_invalid_decryption_raises_soft_error(self):
|
||||
with self.assertRaises(nacl.exceptions.CryptoError):
|
||||
self.assertEqual(auth_and_decrypt(self.nonce + self.ct_tv, bytes(32), soft_e=True), self.pt_tv)
|
||||
|
||||
|
||||
class TestUnicodePadding(unittest.TestCase):
|
||||
|
||||
def test_padding_with_length_check(self):
|
||||
for s in range(0, 255):
|
||||
string = s * 'm'
|
||||
padded = unicode_padding(string)
|
||||
self.assertEqual(len(padded), 255)
|
||||
|
||||
# Verify removal of padding doesn't alter the string.
|
||||
self.assertEqual(string, padded[:-ord(padded[-1:])])
|
||||
|
||||
def test_oversize_pt(self):
|
||||
for s in range(255, 260):
|
||||
with self.assertRaises(AssertionError):
|
||||
unicode_padding(s * 'm')
|
||||
|
||||
|
||||
class TestBytePadding(unittest.TestCase):
|
||||
|
||||
def test_padding_with_length_check(self):
|
||||
for s in range(0, 255):
|
||||
string = s * b'm'
|
||||
padded = byte_padding(string)
|
||||
self.assertEqual(len(padded), 255)
|
||||
|
||||
# Verify removal of padding doesn't alter the string.
|
||||
self.assertEqual(string, padded[:-ord(padded[-1:])])
|
||||
|
||||
|
||||
class TestRmPaddingBytes(unittest.TestCase):
|
||||
|
||||
def test_function(self):
|
||||
for i in range(0, 1000):
|
||||
string = i * b'm'
|
||||
length = 255 - (len(string) % 255)
|
||||
padded = string + length * bytes([length])
|
||||
self.assertEqual(rm_padding_bytes(padded), string)
|
||||
|
||||
|
||||
class TestRmPaddingStr(unittest.TestCase):
|
||||
|
||||
def test_function(self):
|
||||
for i in range(0, 1000):
|
||||
string = i * 'm'
|
||||
length = 255 - (len(string) % 255)
|
||||
padded = string + length * chr(length)
|
||||
self.assertEqual(rm_padding_str(padded), string)
|
||||
|
||||
|
||||
class TestXOR(unittest.TestCase):
|
||||
|
||||
def test_length_mismatch(self):
|
||||
with self.assertRaises(SystemExit):
|
||||
xor(bytes(32), bytes(31))
|
||||
|
||||
def test_function(self):
|
||||
b1 = b'\x00\x01\x00\x01\x01'
|
||||
b2 = b'\x00\x00\x01\x01\x02'
|
||||
b3 = b'\x00\x01\x01\x00\x03'
|
||||
self.assertEqual(xor(b1, b2), b3)
|
||||
|
||||
|
||||
class TestKeyGen(unittest.TestCase):
|
||||
|
||||
def test_function(self):
|
||||
self.assertEqual(len(keygen()), 32)
|
||||
self.assertIsInstance(keygen(), bytes)
|
||||
|
||||
|
||||
class TestInitEntropy(unittest.TestCase):
|
||||
|
||||
def test_function(self):
|
||||
self.assertIsNone(init_entropy())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,213 @@
|
|||
#!/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 unittest
|
||||
|
||||
import src.common.misc
|
||||
|
||||
from src.common.db_contacts import Contact, ContactList
|
||||
from src.common.statics import *
|
||||
|
||||
from tests.mock_classes import create_contact, MasterKey, Settings
|
||||
from tests.utils import cleanup
|
||||
|
||||
|
||||
class TestContact(unittest.TestCase):
|
||||
|
||||
def test_dump_c(self):
|
||||
# Setup
|
||||
contact = Contact('alice@jabber.org', 'bob@jabber.org', 'Alice',
|
||||
32 * b'\x01', 32 * b'\x02', True, True, True)
|
||||
bytestring = contact.dump_c()
|
||||
|
||||
# Test
|
||||
self.assertEqual(len(bytestring), (3 * 1024 + 32 + 32 + 1 + 1 + 1))
|
||||
self.assertIsInstance(bytestring, bytes)
|
||||
|
||||
|
||||
class TestContactList(unittest.TestCase):
|
||||
|
||||
def tearDown(self):
|
||||
cleanup()
|
||||
|
||||
def test_iterate_over_contacts(self):
|
||||
# Setup
|
||||
contact = Contact('alice@jabber.org', 'bob@jabber.org', 'Alice',
|
||||
32 * b'\x01', 32 * b'\x02', True, True, True)
|
||||
contact_l = ContactList(MasterKey(), Settings())
|
||||
contact_l.contacts = 5 * [contact]
|
||||
|
||||
# Test
|
||||
for c in contact_l:
|
||||
self.assertIsInstance(c, Contact)
|
||||
|
||||
def test_len_returns_number_of_contacts(self):
|
||||
# Setup
|
||||
contact = Contact('alice@jabber.org', 'bob@jabber.org', 'Alice',
|
||||
32 * b'\x01', 32 * b'\x02', True, True, True)
|
||||
contact_l = ContactList(MasterKey(), Settings())
|
||||
contact_l.contacts = 5 * [contact]
|
||||
|
||||
# Test
|
||||
self.assertEqual(len(contact_l), 5)
|
||||
|
||||
def test_store_and_load_contacts(self):
|
||||
# Setup
|
||||
contact = Contact('alice@jabber.org', 'bob@jabber.org', 'Alice',
|
||||
32 * b'\x01', 32 * b'\x02', True, True, True)
|
||||
settings = Settings()
|
||||
master_k = MasterKey()
|
||||
contact_l = ContactList(master_k, settings)
|
||||
contact_l.contacts = 5 * [contact]
|
||||
contact_l.store_contacts()
|
||||
|
||||
# Test
|
||||
contact_l2 = ContactList(master_k, settings)
|
||||
self.assertEqual(len(contact_l2), 5)
|
||||
for c in contact_l2:
|
||||
self.assertIsInstance(c, Contact)
|
||||
|
||||
self.assertTrue(os.path.isfile(f'{DIR_USER_DATA}/ut_contacts'))
|
||||
self.assertEqual(os.path.getsize(f'{DIR_USER_DATA}/ut_contacts'), 24 + 20 * (1024 + 1024 + 1024 + 32 + 32 + 1 + 1 + 1) + 16)
|
||||
os.remove(f'{DIR_USER_DATA}/ut_contacts')
|
||||
|
||||
def test_generate_dummy_contact(self):
|
||||
dummy_data = ContactList.generate_dummy_contact()
|
||||
self.assertEqual(len(dummy_data), (1024 + 1024 + 1024 + 32 + 32 + 1 + 1 + 1))
|
||||
self.assertIsInstance(dummy_data, bytes)
|
||||
|
||||
def test_get_contact(self):
|
||||
# Setup
|
||||
contact1 = Contact('alice@jabber.org', 'bob@jabber.org', 'Alice',
|
||||
32 * b'\x01', 32 * b'\x02', True, True, True)
|
||||
contact2 = Contact('charlie@jabber.org', 'bob@jabber.org', 'Charlie',
|
||||
32 * b'\x01', 32 * b'\x02', True, True, True)
|
||||
settings = Settings()
|
||||
master_k = MasterKey()
|
||||
contact_l = ContactList(master_k, settings)
|
||||
contact_l.contacts = [contact1, contact2]
|
||||
|
||||
# Test
|
||||
co1 = contact_l.get_contact('alice@jabber.org')
|
||||
self.assertIsInstance(co1, Contact)
|
||||
self.assertEqual(co1.rx_account, 'alice@jabber.org')
|
||||
|
||||
co2 = contact_l.get_contact('Alice')
|
||||
self.assertIsInstance(co2, Contact)
|
||||
self.assertEqual(co2.rx_account, 'alice@jabber.org')
|
||||
|
||||
def test_getters(self):
|
||||
# Setup
|
||||
contact1 = Contact('alice@jabber.org', 'bob@jabber.org', 'Alice',
|
||||
32 * b'\x01', 32 * b'\x02', True, True, True)
|
||||
contact2 = Contact('charlie@jabber.org', 'bob@jabber.org', 'Charlie',
|
||||
32 * b'\x01', 32 * b'\x02', True, True, True)
|
||||
settings = Settings()
|
||||
master_k = MasterKey()
|
||||
contact_l = ContactList(master_k, settings)
|
||||
contact_l.contacts = [contact1, contact2]
|
||||
|
||||
# Test
|
||||
self.assertEqual(contact_l.contact_selectors(), ['alice@jabber.org', 'charlie@jabber.org', 'Alice', 'Charlie'])
|
||||
self.assertEqual(contact_l.get_list_of_accounts(), ['alice@jabber.org', 'charlie@jabber.org'])
|
||||
self.assertEqual(contact_l.get_list_of_nicks(), ['Alice', 'Charlie'])
|
||||
self.assertEqual(contact_l.get_list_of_users_accounts(), ['bob@jabber.org'])
|
||||
|
||||
def test_remove_contact(self):
|
||||
# Setup
|
||||
contact1 = Contact('alice@jabber.org', 'bob@jabber.org', 'Alice',
|
||||
32 * b'\x01', 32 * b'\x02', True, True, True)
|
||||
contact2 = Contact('charlie@jabber.org', 'bob@jabber.org', 'Charlie',
|
||||
32 * b'\x01', 32 * b'\x02', True, True, True)
|
||||
contact_l = ContactList(MasterKey(), Settings())
|
||||
contact_l.contacts = [contact1, contact2]
|
||||
|
||||
# Test
|
||||
self.assertTrue(contact_l.has_contacts())
|
||||
self.assertTrue(contact_l.has_contact('Alice'))
|
||||
self.assertTrue(contact_l.has_contact('alice@jabber.org'))
|
||||
|
||||
contact_l.remove_contact('alice@jabber.org')
|
||||
self.assertFalse(contact_l.has_contact('Alice'))
|
||||
self.assertFalse(contact_l.has_contact('alice@jabber.org'))
|
||||
|
||||
contact_l.remove_contact('Charlie')
|
||||
self.assertEqual(len(contact_l.contacts), 0)
|
||||
self.assertFalse(contact_l.has_contacts())
|
||||
|
||||
def test_add_contact(self):
|
||||
# Setup
|
||||
contact1 = Contact('alice@jabber.org', 'bob@jabber.org', 'Alice',
|
||||
32 * b'\x01', 32 * b'\x02', True, True, True)
|
||||
contact2 = Contact('charlie@jabber.org', 'bob@jabber.org', 'Charlie',
|
||||
32 * b'\x01', 32 * b'\x02', True, True, True)
|
||||
settings = Settings(software_operation='ut', m_number_of_accnts=20)
|
||||
master_k = MasterKey()
|
||||
contact_l = ContactList(master_k, settings)
|
||||
contact_l.contacts = [contact1, contact2]
|
||||
|
||||
contact_l.add_contact('alice@jabber.org', 'bob@jabber.org', 'Alice',
|
||||
32 * b'\x03', 32 * b'\x04', True, True, True)
|
||||
contact_l.add_contact('david@jabber.org', 'bob@jabber.org', 'David',
|
||||
32 * b'\x03', 32 * b'\x04', True, True, True)
|
||||
|
||||
contact_l2 = ContactList(master_k, settings)
|
||||
c_alice = contact_l2.get_contact('Alice')
|
||||
c_david = contact_l2.get_contact('David')
|
||||
|
||||
# Test
|
||||
self.assertIsInstance(c_alice, Contact)
|
||||
self.assertIsInstance(c_david, Contact)
|
||||
self.assertEqual(c_alice.tx_fingerprint, 32 * b'\x03')
|
||||
self.assertEqual(c_david.tx_fingerprint, 32 * b'\x03')
|
||||
|
||||
def test_local_contact(self):
|
||||
# Setup
|
||||
contact1 = Contact('alice@jabber.org', 'bob@jabber.org', 'Alice',
|
||||
32 * b'\x01', 32 * b'\x02', True, True, True)
|
||||
contact_l = ContactList(MasterKey(), Settings())
|
||||
contact_l.contacts = [contact1]
|
||||
o_get_tty_w = src.common.misc.get_tty_w
|
||||
src.common.misc.get_tty_w = lambda x: 1
|
||||
|
||||
# Test
|
||||
self.assertFalse(contact_l.has_local_contact())
|
||||
|
||||
contact_l.add_contact('local', 'local', 'local',
|
||||
32 * b'\x03', 32 * b'\x04', True, True, True)
|
||||
|
||||
self.assertTrue(contact_l.has_local_contact())
|
||||
self.assertIsNone(contact_l.print_contacts())
|
||||
self.assertIsNone(contact_l.print_contacts(spacing=True))
|
||||
|
||||
# Teardown
|
||||
src.common.misc.get_tty_w = o_get_tty_w
|
||||
|
||||
def test_contact_printing(self):
|
||||
# Setup
|
||||
contact_list = ContactList(MasterKey(), Settings())
|
||||
contact_list.contacts = [create_contact(n) for n in ['Alice', 'Bob', 'Charlie', 'David']]
|
||||
# Teardown
|
||||
self.assertIsNone(contact_list.print_contacts())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,145 @@
|
|||
#!/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 unittest
|
||||
|
||||
from src.common.statics import *
|
||||
from src.common.db_contacts import Contact, ContactList
|
||||
from src.common.db_groups import Group, GroupList
|
||||
|
||||
from tests.mock_classes import create_contact, MasterKey, Settings
|
||||
from tests.utils import cleanup
|
||||
|
||||
|
||||
class TestGroup(unittest.TestCase):
|
||||
|
||||
def test_class(self):
|
||||
# Setup
|
||||
settings = Settings()
|
||||
members = [create_contact(n) for n in ['Alice', 'Bob', 'Charlie']]
|
||||
sg_mock = lambda: None
|
||||
group = Group('testgroup', False, False, members, settings, sg_mock)
|
||||
|
||||
# Test
|
||||
for c in group:
|
||||
self.assertIsInstance(c, Contact)
|
||||
self.assertEqual(len(group), 3)
|
||||
|
||||
bytestring = group.dump_g()
|
||||
self.assertIsInstance(bytestring, bytes)
|
||||
self.assertEqual(len(bytestring), 1024 + 2 + (20 * 1024))
|
||||
|
||||
self.assertEqual(group.get_list_of_member_accounts(), ['alice@jabber.org', 'bob@jabber.org', 'charlie@jabber.org'])
|
||||
self.assertEqual(group.get_list_of_member_nicks(), ['Alice', 'Bob', 'Charlie'])
|
||||
|
||||
self.assertTrue(group.has_members())
|
||||
self.assertFalse(group.has_member('david@jabber.org'))
|
||||
|
||||
group.add_members([create_contact(n) for n in ['David']])
|
||||
self.assertTrue(group.has_member('david@jabber.org'))
|
||||
|
||||
self.assertFalse(group.remove_members(['eric@jabber.org']))
|
||||
self.assertTrue(group.remove_members(['david@jabber.org']))
|
||||
self.assertFalse(group.has_member('david@jabber.org'))
|
||||
|
||||
# Teardown
|
||||
cleanup()
|
||||
|
||||
|
||||
class TestGroupList(unittest.TestCase):
|
||||
|
||||
def test_class(self):
|
||||
# Setup
|
||||
master_key = MasterKey()
|
||||
settings = Settings()
|
||||
contact_list = ContactList(master_key, settings)
|
||||
group_list = GroupList(master_key, settings, contact_list)
|
||||
members = [create_contact(n) for n in ['Alice', 'Bob', 'Charlie', 'David', 'Eric',
|
||||
'Fido', 'Gunter', 'Heidi', 'Ivan', 'Joana', 'Karol']]
|
||||
contact_list.contacts = members
|
||||
groups = [Group(n, False, False, members, settings, group_list.store_groups())
|
||||
for n in ['testgroup_1', 'testgroup_2', 'testgroup3', 'testgroup_4', 'testgroup_5',
|
||||
'testgroup_6', 'testgroup_7', 'testgroup8', 'testgroup_9', 'testgroup_10',
|
||||
'testgroup_11']]
|
||||
group_list.groups = groups
|
||||
group_list.store_groups()
|
||||
|
||||
# Test
|
||||
for g in group_list:
|
||||
self.assertIsInstance(g, Group)
|
||||
self.assertEqual(len(group_list), 11)
|
||||
|
||||
self.assertTrue(os.path.isfile(f'{DIR_USER_DATA}/ut_groups'))
|
||||
self.assertEqual(os.path.getsize(f'{DIR_USER_DATA}/ut_groups'), 24 + 32 + 20 * (1024 + 2 + (20 * 1024)) + 16)
|
||||
|
||||
settings.m_number_of_groups = 10
|
||||
settings.m_members_in_group = 10
|
||||
|
||||
group_list2 = GroupList(master_key, settings, contact_list)
|
||||
|
||||
self.assertEqual(len(group_list2), 11)
|
||||
|
||||
self.assertEqual(settings.m_number_of_groups, 20)
|
||||
self.assertEqual(settings.m_members_in_group, 20)
|
||||
|
||||
bytestring = group_list2.generate_header()
|
||||
self.assertEqual(len(bytestring), 32)
|
||||
self.assertIsInstance(bytestring, bytes)
|
||||
|
||||
dg_bytestring = group_list2.generate_dummy_group()
|
||||
self.assertEqual(len(dg_bytestring), (1024 + 2 + (20 * 1024)))
|
||||
self.assertIsInstance(dg_bytestring, bytes)
|
||||
|
||||
members.append(create_contact('Laura'))
|
||||
group_list2.add_group('testgroup_12', False, False, members)
|
||||
group_list2.add_group('testgroup_12', False, True, members)
|
||||
self.assertTrue(group_list2.get_group('testgroup_12').notifications)
|
||||
self.assertEqual(len(group_list2), 12)
|
||||
self.assertEqual(group_list2.largest_group(), 12)
|
||||
|
||||
g_names = ['testgroup_1', 'testgroup_2', 'testgroup3', 'testgroup_4', 'testgroup_5', 'testgroup_6',
|
||||
'testgroup_7', 'testgroup8', 'testgroup_9', 'testgroup_10', 'testgroup_11', 'testgroup_12']
|
||||
self.assertEqual(group_list2.get_list_of_group_names(), g_names)
|
||||
|
||||
g_o = group_list2.get_group('testgroup_1')
|
||||
self.assertIsInstance(g_o, Group)
|
||||
self.assertEqual(g_o.name, 'testgroup_1')
|
||||
self.assertTrue(group_list2.has_group('testgroup_12'))
|
||||
self.assertFalse(group_list2.has_group('testgroup_13'))
|
||||
self.assertTrue(group_list2.has_groups(), True)
|
||||
|
||||
members = group_list2.get_group_members('testgroup_1')
|
||||
for c in members:
|
||||
self.assertIsInstance(c, Contact)
|
||||
|
||||
self.assertEqual(len(group_list2), 12)
|
||||
group_list2.remove_group('testgroup_13')
|
||||
self.assertEqual(len(group_list2), 12)
|
||||
group_list2.remove_group('testgroup_12')
|
||||
self.assertEqual(len(group_list2), 11)
|
||||
self.assertIsNone(group_list2.print_groups())
|
||||
|
||||
# Teardown
|
||||
cleanup()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,124 @@
|
|||
#!/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 binascii
|
||||
import os.path
|
||||
import unittest
|
||||
|
||||
from src.common.db_keys import KeyList, KeySet
|
||||
from src.common.statics import *
|
||||
|
||||
from tests.mock_classes import create_keyset, MasterKey, Settings
|
||||
from tests.utils import cleanup
|
||||
|
||||
|
||||
class TestKeySet(unittest.TestCase):
|
||||
|
||||
def test_class(self):
|
||||
# Setup
|
||||
m_sk = lambda: None
|
||||
keyset = KeySet('alice@jabber.org',
|
||||
32 * b'\x00',
|
||||
32 * b'\x00',
|
||||
32 * b'\x00',
|
||||
32 * b'\x00',
|
||||
0, 0, m_sk)
|
||||
|
||||
# Test
|
||||
bytestring = keyset.dump_k()
|
||||
self.assertEqual(len(bytestring), 1024 + 4 * 32 + 8 + 8)
|
||||
self.assertIsInstance(bytestring, bytes)
|
||||
self.assertIsNone(keyset.rotate_tx_key())
|
||||
self.assertEqual(keyset.tx_key, binascii.unhexlify("8d8c36497eb93a6355112e253f705a32"
|
||||
"85f3e2d82b9ac29461cd8d4f764e5d41"))
|
||||
self.assertEqual(keyset.tx_harac, 1)
|
||||
|
||||
keyset.tx_key = 32 * b'\x00'
|
||||
|
||||
keyset.update_key('tx', 32 * b'\x01', 2)
|
||||
self.assertEqual(keyset.tx_key, 32 * b'\x01')
|
||||
self.assertEqual(keyset.rx_key, 32 * b'\x00')
|
||||
self.assertEqual(keyset.tx_hek, 32 * b'\x00')
|
||||
self.assertEqual(keyset.rx_hek, 32 * b'\x00')
|
||||
self.assertEqual(keyset.tx_harac, 3)
|
||||
|
||||
keyset.update_key('rx', 32 * b'\x01', 2)
|
||||
self.assertEqual(keyset.tx_key, 32 * b'\x01')
|
||||
self.assertEqual(keyset.rx_key, 32 * b'\x01')
|
||||
self.assertEqual(keyset.tx_hek, 32 * b'\x00')
|
||||
self.assertEqual(keyset.rx_hek, 32 * b'\x00')
|
||||
self.assertEqual(keyset.rx_harac, 2)
|
||||
|
||||
|
||||
class TestKeyList(unittest.TestCase):
|
||||
|
||||
def test_class(self):
|
||||
# Setup
|
||||
masterkey = MasterKey()
|
||||
settings = Settings()
|
||||
keylist = KeyList(masterkey, settings)
|
||||
keylist.keysets = [create_keyset(n, store_f=keylist.store_keys) for n in ['Alice', 'Bob', 'Charlie']]
|
||||
|
||||
keylist.store_keys()
|
||||
|
||||
# Test
|
||||
self.assertTrue(os.path.isfile(f'{DIR_USER_DATA}/ut_keys'))
|
||||
self.assertEqual(os.path.getsize(f'{DIR_USER_DATA}/ut_keys'), 24 + 20 * (1024 + 4*32 + 2*8) + 16)
|
||||
|
||||
keylist2 = KeyList(masterkey, settings)
|
||||
|
||||
for k in keylist2.keysets:
|
||||
self.assertIsInstance(k, KeySet)
|
||||
|
||||
self.assertEqual(len(keylist2.keysets), 3)
|
||||
|
||||
bytestring = keylist2.generate_dummy_keyset()
|
||||
self.assertEqual(len(bytestring), 1024 + 4 * 32 + 8 + 8)
|
||||
self.assertIsInstance(bytestring, bytes)
|
||||
|
||||
keyset = keylist2.get_keyset('alice@jabber.org')
|
||||
self.assertIsInstance(keyset, KeySet)
|
||||
|
||||
self.assertFalse(keylist2.has_local_key())
|
||||
self.assertIsNone(keylist2.manage('ADD', 'local', bytes(32), bytes(32), bytes(32), bytes(32)))
|
||||
self.assertTrue(keylist2.has_local_key())
|
||||
|
||||
self.assertTrue(keylist2.has_keyset('bob@jabber.org'))
|
||||
self.assertIsNone(keylist2.manage('REM', 'bob@jabber.org'))
|
||||
self.assertFalse(keylist2.has_keyset('bob@jabber.org'))
|
||||
|
||||
keylist2.get_keyset('charlie@jabber.org').tx_harac = 1
|
||||
self.assertIsNone(keylist2.manage('ADD', 'charlie@jabber.org', bytes(32), bytes(32), bytes(32), bytes(32)))
|
||||
self.assertEqual(keylist2.get_keyset('charlie@jabber.org').tx_harac, 0)
|
||||
|
||||
masterkey2 = MasterKey()
|
||||
masterkey2.master_key = 32 * b'\x01'
|
||||
keylist2.manage('KEY', masterkey2)
|
||||
self.assertEqual(keylist2.master_key.master_key, 32 * b'\x01')
|
||||
|
||||
with self.assertRaises(SystemExit):
|
||||
keylist2.manage('invalid_key', masterkey2)
|
||||
|
||||
# Teardown
|
||||
cleanup()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,225 @@
|
|||
#!/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 multiprocessing
|
||||
import os.path
|
||||
import time
|
||||
import unittest
|
||||
import zlib
|
||||
|
||||
from typing import List
|
||||
|
||||
from src.common.crypto import byte_padding, encrypt_and_sign
|
||||
from src.common.errors import FunctionReturn
|
||||
from src.common.encoding import double_to_bytes
|
||||
from src.common.misc import split_byte_string
|
||||
from src.common.db_contacts import ContactList
|
||||
from src.common.db_logs import access_history, log_writer, re_encrypt, write_log_entry
|
||||
from src.common.statics import *
|
||||
|
||||
from tests.mock_classes import create_contact, MasterKey, Settings, Window
|
||||
from tests.utils import cleanup
|
||||
|
||||
|
||||
class TestLogWriter(unittest.TestCase):
|
||||
|
||||
def test_lw_process(self):
|
||||
# Setup
|
||||
cleanup()
|
||||
m_queue = multiprocessing.Queue()
|
||||
lwp = multiprocessing.Process(target=log_writer, args=(m_queue,))
|
||||
lwp.start()
|
||||
time.sleep(0.1)
|
||||
|
||||
settings = Settings()
|
||||
master_key = MasterKey()
|
||||
|
||||
m_queue.put((P_N_HEADER + bytes(255), 'alice@ajbber.org', settings, master_key))
|
||||
m_queue.put((M_S_HEADER + bytes(255), 'alice@ajbber.org', settings, master_key))
|
||||
m_queue.put((F_S_HEADER + bytes(255), 'alice@ajbber.org', settings, master_key))
|
||||
|
||||
time.sleep(0.2)
|
||||
lwp.terminate()
|
||||
|
||||
# Test
|
||||
self.assertTrue(os.path.isfile(f'{DIR_USER_DATA}/ut_logs'))
|
||||
entry_size = 24 + 4 + 1 + 1024 + 1 + 255 + 16
|
||||
self.assertTrue(os.path.getsize(f'{DIR_USER_DATA}/ut_logs') % entry_size == 0)
|
||||
|
||||
# Teardown
|
||||
cleanup()
|
||||
|
||||
|
||||
class TestWriteLogEntry(unittest.TestCase):
|
||||
|
||||
def test_function(self):
|
||||
#Setup
|
||||
masterkey = MasterKey()
|
||||
settings = Settings()
|
||||
|
||||
# Test
|
||||
self.assertIsNone(write_log_entry(F_S_HEADER + bytes(255), 'alice@jabber.org', settings, masterkey))
|
||||
self.assertTrue(os.path.isfile(f'{DIR_USER_DATA}/ut_logs'))
|
||||
entry_size = 24 + 4 + 1 + 1024 + 1 + 255 + 16
|
||||
self.assertTrue(os.path.getsize(f'{DIR_USER_DATA}/ut_logs') % entry_size == 0)
|
||||
self.assertIsNone(write_log_entry(F_S_HEADER + bytes(255), 'alice@jabber.org', settings, masterkey))
|
||||
self.assertTrue(os.path.getsize(f'{DIR_USER_DATA}/ut_logs') % entry_size == 0)
|
||||
|
||||
# Teardown
|
||||
cleanup()
|
||||
|
||||
class TestAccessHistory(unittest.TestCase):
|
||||
|
||||
@staticmethod
|
||||
def mock_entry_preprocessor(message: str, header: bytes = b'', group: bool = False) -> List[bytes]:
|
||||
if not header:
|
||||
if group:
|
||||
timestamp = double_to_bytes(time.time() * 1000)
|
||||
header = GROUP_MESSAGE_HEADER + timestamp + 'testgroup'.encode() + US_BYTE
|
||||
else:
|
||||
header = PRIVATE_MESSAGE_HEADER
|
||||
|
||||
plaintext = message.encode()
|
||||
payload = header + plaintext
|
||||
payload = zlib.compress(payload, level=9)
|
||||
|
||||
if len(payload) < 255:
|
||||
padded = byte_padding(payload)
|
||||
packet_list = [M_S_HEADER + padded]
|
||||
else:
|
||||
msg_key = bytes(32)
|
||||
payload = encrypt_and_sign(payload, msg_key)
|
||||
payload += msg_key
|
||||
padded = byte_padding(payload)
|
||||
p_list = split_byte_string(padded, item_len=255)
|
||||
|
||||
packet_list = ([M_L_HEADER + p_list[0]] +
|
||||
[M_A_HEADER + p for p in p_list[1:-1]] +
|
||||
[M_E_HEADER + p_list[-1]])
|
||||
|
||||
return packet_list
|
||||
|
||||
|
||||
def test_read_private_message(self):
|
||||
# Setup
|
||||
masterkey = MasterKey()
|
||||
settings = Settings()
|
||||
window = Window(type='contact', uid='alice@jabber.org', name='Alice')
|
||||
|
||||
contact_list = ContactList(masterkey, settings)
|
||||
contact_list.contacts = [create_contact('Alice')]
|
||||
|
||||
with self.assertRaises(FunctionReturn):
|
||||
self.assertIsNone(access_history(window, contact_list, settings, masterkey))
|
||||
|
||||
for p in self.mock_entry_preprocessor('This is a short message'):
|
||||
write_log_entry(p, 'alice@jabber.org', settings, masterkey)
|
||||
|
||||
long_msg = ("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean condimentum consectetur purus quis"
|
||||
" dapibus. Fusce venenatis lacus ut rhoncus faucibus. Cras sollicitudin commodo sapien, sed bibendu"
|
||||
"m velit maximus in. Aliquam ac metus risus. Sed cursus ornare luctus. Integer aliquet lectus id ma"
|
||||
"ssa blandit imperdiet. Ut sed massa eget quam facilisis rutrum. Mauris eget luctus nisl. Sed ut el"
|
||||
"it iaculis, faucibus lacus eget, sodales magna. Nunc sed commodo arcu. In hac habitasse platea dic"
|
||||
"tumst. Integer luctus aliquam justo, at vestibulum dolor iaculis ac. Etiam laoreet est eget odio r"
|
||||
"utrum, vel malesuada lorem rhoncus. Cras finibus in neque eu euismod. Nulla facilisi. Nunc nec ali"
|
||||
"quam quam, quis ullamcorper leo. Nunc egestas lectus eget est porttitor, in iaculis felis sceleris"
|
||||
"que. In sem elit, fringilla id viverra commodo, sagittis varius purus. Pellentesque rutrum loborti"
|
||||
"s neque a facilisis. Mauris id tortor placerat, aliquam dolor ac, venenatis arcu.")
|
||||
|
||||
for p in self.mock_entry_preprocessor(long_msg):
|
||||
write_log_entry(p, 'alice@jabber.org', settings, masterkey)
|
||||
|
||||
# Add packet cancelled half-way
|
||||
packets = self.mock_entry_preprocessor(long_msg)
|
||||
packets = packets[2:] + [M_C_HEADER + bytes(255)]
|
||||
for p in packets:
|
||||
write_log_entry(p, 'alice@jabber.org', settings, masterkey)
|
||||
|
||||
# Test
|
||||
self.assertIsNone(access_history(window, contact_list, settings, masterkey))
|
||||
|
||||
# Test window UID mismatch
|
||||
window.uid = 'bob@jabber.org'
|
||||
self.assertIsNone(access_history(window, contact_list, settings, masterkey))
|
||||
|
||||
# Test window type mismatch
|
||||
window.uid = 'alice@jabber.org'
|
||||
window.type = 'group'
|
||||
self.assertIsNone(access_history(window, contact_list, settings, masterkey))
|
||||
|
||||
# Group messages
|
||||
|
||||
window = Window(type='group', uid='testgroup', name='testgroup')
|
||||
|
||||
contact_list = ContactList(masterkey, settings)
|
||||
contact_list.contacts = [create_contact(n) for n in ['Alice', 'Charlie']]
|
||||
|
||||
for p in self.mock_entry_preprocessor('This is a short message', group=True):
|
||||
write_log_entry(p, 'alice@jabber.org', settings, masterkey)
|
||||
write_log_entry(p, 'charlie@jabber.org', settings, masterkey)
|
||||
|
||||
long_msg = ("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean condimentum consectetur purus quis"
|
||||
" dapibus. Fusce venenatis lacus ut rhoncus faucibus. Cras sollicitudin commodo sapien, sed bibendu"
|
||||
"m velit maximus in. Aliquam ac metus risus. Sed cursus ornare luctus. Integer aliquet lectus id ma"
|
||||
"ssa blandit imperdiet. Ut sed massa eget quam facilisis rutrum. Mauris eget luctus nisl. Sed ut el"
|
||||
"it iaculis, faucibus lacus eget, sodales magna. Nunc sed commodo arcu. In hac habitasse platea dic"
|
||||
"tumst. Integer luctus aliquam justo, at vestibulum dolor iaculis ac. Etiam laoreet est eget odio r"
|
||||
"utrum, vel malesuada lorem rhoncus. Cras finibus in neque eu euismod. Nulla facilisi. Nunc nec ali"
|
||||
"quam quam, quis ullamcorper leo. Nunc egestas lectus eget est porttitor, in iaculis felis sceleris"
|
||||
"que. In sem elit, fringilla id viverra commodo, sagittis varius purus. Pellentesque rutrum loborti"
|
||||
"s neque a facilisis. Mauris id tortor placerat, aliquam dolor ac, venenatis arcu.")
|
||||
|
||||
for p in self.mock_entry_preprocessor(long_msg, group=True):
|
||||
write_log_entry(p, 'alice@jabber.org', settings, masterkey)
|
||||
write_log_entry(p, 'charlie@jabber.org', settings, masterkey)
|
||||
|
||||
# Test
|
||||
self.assertIsNone(access_history(window, contact_list, settings, masterkey))
|
||||
|
||||
# Test window name mismatch
|
||||
window.name = 'bob@jabber.org'
|
||||
self.assertIsNone(access_history(window, contact_list, settings, masterkey))
|
||||
|
||||
# Test window type mismatch
|
||||
window.name = 'testgroup'
|
||||
window.type = 'contact'
|
||||
self.assertIsNone(access_history(window, contact_list, settings, masterkey))
|
||||
|
||||
# Re-encrypt log database
|
||||
|
||||
# Create garbage file to remove
|
||||
with open(f'{DIR_USER_DATA}/{settings.software_operation}_logs_temp', 'wb+') as f:
|
||||
f.write(b'will screw decryption')
|
||||
|
||||
self.assertIsNone(re_encrypt(masterkey.master_key, 32 * b'\x01', settings))
|
||||
masterkey.master_key = 32 * b'\x01'
|
||||
self.assertIsNone(access_history(window, contact_list, settings, masterkey))
|
||||
self.assertIsNone(access_history(window, contact_list, settings, masterkey, export=True))
|
||||
|
||||
cleanup()
|
||||
with self.assertRaises(FunctionReturn):
|
||||
re_encrypt(masterkey.master_key, 32 * b'\x01', settings)
|
||||
|
||||
# Teardown
|
||||
os.remove("Unittest - Plaintext log (testgroup)")
|
||||
cleanup()
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,58 @@
|
|||
#!/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 getpass
|
||||
import os.path
|
||||
import unittest
|
||||
|
||||
from src.common.db_masterkey import MasterKey
|
||||
from src.common.statics import *
|
||||
|
||||
from tests.utils import cleanup
|
||||
|
||||
|
||||
class TestMasterKey(unittest.TestCase):
|
||||
|
||||
def test_class(self):
|
||||
# Setup
|
||||
o_get_password = getpass.getpass
|
||||
getpass.getpass = lambda x: 'testpwd'
|
||||
|
||||
# Test
|
||||
masterkey = MasterKey('ut', local_test=False)
|
||||
self.assertIsInstance(masterkey.master_key, bytes)
|
||||
|
||||
os.path.isfile(f"{DIR_USER_DATA}/ut_login_data")
|
||||
self.assertEqual(os.path.getsize(f"{DIR_USER_DATA}/ut_login_data"), 32 + 32 + 8 + 8)
|
||||
cleanup()
|
||||
|
||||
masterkey = MasterKey('ut', local_test=True)
|
||||
self.assertIsInstance(masterkey.master_key, bytes)
|
||||
|
||||
masterkey = MasterKey('ut', local_test=True)
|
||||
self.assertIsInstance(masterkey.master_key, bytes)
|
||||
|
||||
# Teardown
|
||||
getpass.getpass = o_get_password
|
||||
cleanup()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,94 @@
|
|||
#!/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 builtins
|
||||
import os.path
|
||||
import unittest
|
||||
|
||||
from src.common.db_settings import Settings
|
||||
from src.common.statics import *
|
||||
|
||||
from tests.mock_classes import create_group, ContactList, GroupList, MasterKey
|
||||
from tests.utils import cleanup, TFCTestCase
|
||||
|
||||
|
||||
class TestSettings(TFCTestCase):
|
||||
|
||||
def test_class(self):
|
||||
# Setup
|
||||
masterkey = MasterKey()
|
||||
o_input = builtins.input
|
||||
builtins.input = lambda x: 'yes'
|
||||
settings = Settings(masterkey, 'ut', False, False)
|
||||
contact_list = ContactList(nicks=['contact_{}'.format(n) for n in range(18)])
|
||||
group_list = GroupList(groups =['group_{}'.format(n) for n in range(18)])
|
||||
group_list.groups[0] = create_group('group_0', ['contact_{}'.format(n) for n in range(18)])
|
||||
|
||||
# Test store/load
|
||||
self.assertFalse(settings.disable_gui_dialog)
|
||||
settings.disable_gui_dialog = True
|
||||
settings.store_settings()
|
||||
|
||||
self.assertTrue(os.path.isfile(f"{DIR_USER_DATA}/ut_settings"))
|
||||
self.assertEqual(os.path.getsize(f"{DIR_USER_DATA}/ut_settings"), 24 + 1024 + 9*8 + 12*1 + 16)
|
||||
|
||||
settings2 = Settings(masterkey, 'ut', False, False)
|
||||
self.assertTrue(settings2.disable_gui_dialog)
|
||||
|
||||
settings2.format_of_logfiles = b'invalid'
|
||||
with self.assertRaises(SystemExit):
|
||||
settings2.store_settings()
|
||||
with self.assertRaises(SystemExit):
|
||||
settings2.change_setting('format_of_logfiles', '%Y-%m-%d %H:%M:%S', contact_list, group_list)
|
||||
settings2.format_of_logfiles = '%Y-%m-%d %H:%M:%S'
|
||||
|
||||
# Test change_setting
|
||||
self.assertFR('Invalid value Falsee.', settings2.change_setting, 'disable_gui_dialog', 'Falsee', contact_list, group_list)
|
||||
self.assertFR('Invalid value 1.1.', settings2.change_setting, 'm_members_in_group', '1.1', contact_list, group_list)
|
||||
self.assertFR('Invalid value 7378697629483820650.', settings2.change_setting, 'm_members_in_group', '7378697629483820650', contact_list, group_list)
|
||||
self.assertFR('Invalid value True.', settings2.change_setting, 'trickle_stat_delay', 'True', contact_list, group_list)
|
||||
self.assertFR("Setting must be shorter than 256 chars.", settings2.change_setting, 'format_of_logfiles', 256*'a', contact_list, group_list)
|
||||
|
||||
self.assertIsNone(settings2.change_setting('format_of_logfiles', '%Y-%m-%d %H:%M:%S', contact_list, group_list))
|
||||
self.assertIsNone(settings2.change_setting('e_correction_ratio', '10', contact_list, group_list))
|
||||
self.assertIsNone(settings2.change_setting('rxm_serial_adapter', 'True', contact_list, group_list))
|
||||
self.assertIsNone(settings2.change_setting('trickle_connection', 'True', contact_list, group_list))
|
||||
|
||||
self.assertFR("Database padding settings must be divisible by 10.", settings2.validate_key_value_pair, 'm_members_in_group', '18', contact_list, group_list)
|
||||
self.assertFR("Database padding settings must be divisible by 10.", settings2.validate_key_value_pair, 'm_number_of_groups', '18', contact_list, group_list)
|
||||
self.assertFR("Database padding settings must be divisible by 10.", settings2.validate_key_value_pair, 'm_number_of_accnts', '18', contact_list, group_list)
|
||||
self.assertFR("Can't set max number of members lower than 20.", settings2.validate_key_value_pair, 'm_members_in_group', '10', contact_list, group_list)
|
||||
self.assertFR("Can't set max number of groups lower than 20.", settings2.validate_key_value_pair, 'm_number_of_groups', '10', contact_list, group_list)
|
||||
self.assertFR("Can't set max number of contacts lower than 20.", settings2.validate_key_value_pair, 'm_number_of_accnts', '10', contact_list, group_list)
|
||||
self.assertFR("Specified baud rate is not supported.", settings2.validate_key_value_pair, 'serial_iface_speed', '10', contact_list, group_list)
|
||||
self.assertFR("Invalid value for error correction ratio.", settings2.validate_key_value_pair, 'e_correction_ratio', '0', contact_list, group_list)
|
||||
self.assertFR("Invalid value for error correction ratio.", settings2.validate_key_value_pair, 'e_correction_ratio', 'a', contact_list, group_list)
|
||||
self.assertFR("Invalid value for error correction ratio.", settings2.validate_key_value_pair, 'e_correction_ratio', '-1', contact_list, group_list)
|
||||
|
||||
self.assertIsNone(settings2.print_settings())
|
||||
|
||||
builtins.input = o_input
|
||||
|
||||
def tearDown(self):
|
||||
cleanup()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,81 @@
|
|||
#!/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 binascii
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from src.common.encoding import b58encode, bool_to_bytes, double_to_bytes, str_to_bytes, int_to_bytes
|
||||
from src.common.encoding import b58decode, bytes_to_bool, bytes_to_double, bytes_to_str, bytes_to_int
|
||||
|
||||
|
||||
class TestB58(unittest.TestCase):
|
||||
|
||||
def test_function(self):
|
||||
for _ in range(1000):
|
||||
key = os.urandom(32)
|
||||
encoded = b58encode(key)
|
||||
decoded = b58decode(encoded)
|
||||
self.assertEqual(key, decoded)
|
||||
|
||||
def test_invalid_decoding(self):
|
||||
key = 32 * b'\x01'
|
||||
encoded = b58encode(key) # SeLqn3UAUoRymWmwW7axrzJK7JfNaBR2cHCryA6cFsiJ67Em
|
||||
changed = encoded[:-1] + 'a'
|
||||
with self.assertRaises(ValueError):
|
||||
b58decode(changed)
|
||||
|
||||
|
||||
class TestConversions(unittest.TestCase):
|
||||
|
||||
def test_bool_to_bytes(self):
|
||||
self.assertEqual(bool_to_bytes(False), b'\x00')
|
||||
self.assertEqual(bool_to_bytes(True), b'\x01')
|
||||
|
||||
def test_bytes_to_bool(self):
|
||||
self.assertEqual(bytes_to_bool(b'\x00'), False)
|
||||
self.assertEqual(bytes_to_bool(b'\x01'), True)
|
||||
|
||||
def int_to_bytes(self):
|
||||
self.assertEqual(int_to_bytes(1), b'\x00\x00\x00\x00\x00\x00\x00\x01')
|
||||
|
||||
def test_bytes_to_int(self):
|
||||
self.assertEqual(bytes_to_int(b'\x00\x00\x00\x00\x00\x00\x00\x01'), 1)
|
||||
|
||||
def test_double_to_bytes(self):
|
||||
self.assertEqual(double_to_bytes(1.0), binascii.unhexlify('000000000000f03f'))
|
||||
self.assertEqual(double_to_bytes(1.1), binascii.unhexlify('9a9999999999f13f'))
|
||||
|
||||
def test_bytes_to_double(self):
|
||||
self.assertEqual(bytes_to_double(binascii.unhexlify('000000000000f03f')), 1.0)
|
||||
self.assertEqual(bytes_to_double(binascii.unhexlify('9a9999999999f13f')), 1.1)
|
||||
|
||||
def test_str_to_bytes(self):
|
||||
encoded = str_to_bytes('test')
|
||||
self.assertIsInstance(encoded, bytes)
|
||||
self.assertEqual(len(encoded), 1024)
|
||||
|
||||
def test_bytes_to_str(self):
|
||||
encoded = str_to_bytes('test')
|
||||
self.assertEqual(bytes_to_str(encoded), 'test')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,48 @@
|
|||
#!/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 unittest
|
||||
|
||||
from src.common.errors import CriticalError, graceful_exit
|
||||
|
||||
|
||||
class Window(object):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def print_new(self, *_):
|
||||
pass
|
||||
|
||||
|
||||
class TestErrors(unittest.TestCase):
|
||||
|
||||
def test_critical_error(self):
|
||||
with self.assertRaises(SystemExit):
|
||||
CriticalError('test')
|
||||
|
||||
def test_graceful_exit(self):
|
||||
with self.assertRaises(SystemExit):
|
||||
graceful_exit('test message')
|
||||
graceful_exit('test message', clear=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,43 @@
|
|||
#!/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 unittest
|
||||
|
||||
from src.common.gateway import Gateway
|
||||
|
||||
from tests.mock_classes import Settings
|
||||
|
||||
|
||||
class TestGateway(unittest.TestCase):
|
||||
|
||||
@unittest.skipIf("TRAVIS" in os.environ and os.environ["TRAVIS"] == "true", "Skipping this test on Travis CI.")
|
||||
def test_class(self):
|
||||
# Setup
|
||||
settings = Settings()
|
||||
gateway = Gateway(settings)
|
||||
|
||||
# Test
|
||||
self.assertIsNone(gateway.write(b'test'))
|
||||
self.assertEqual(gateway.search_serial_interface(), '/dev/ttyS0')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,120 @@
|
|||
#!/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 builtins
|
||||
import getpass
|
||||
import unittest
|
||||
|
||||
from src.common.input import box_input, get_b58_key, nh_bypass_msg, pwd_prompt, yes
|
||||
|
||||
from tests.mock_classes import Settings
|
||||
|
||||
|
||||
class TestInputs(unittest.TestCase):
|
||||
|
||||
o_input = builtins.input
|
||||
|
||||
def tearDown(self):
|
||||
builtins.input = self.o_input
|
||||
|
||||
def test_box_input(self):
|
||||
builtins.input = lambda x: 'mock_input'
|
||||
self.assertEqual(box_input('test title'), 'mock_input')
|
||||
self.assertEqual(box_input('test title', head=1, tail=1, expected_len=20), 'mock_input')
|
||||
|
||||
builtins.input = lambda x: ''
|
||||
self.assertEqual(box_input('test title', head=1, tail=1, default = 'mock_input', expected_len=20), 'mock_input')
|
||||
|
||||
def test_validator(string, *_):
|
||||
if string == 'ok':
|
||||
return True, ''
|
||||
else:
|
||||
print(string)
|
||||
return False, 'Error'
|
||||
|
||||
input_list = ['bad', 'ok']
|
||||
gen = iter(input_list)
|
||||
|
||||
def mock_input(_):
|
||||
return str(next(gen))
|
||||
|
||||
builtins.input = mock_input
|
||||
self.assertEqual(box_input('test title', validator=test_validator), 'ok')
|
||||
|
||||
def test_get_b58_key(self):
|
||||
|
||||
for kt in ['pubkey', 'localkey', 'imported_file']:
|
||||
input_list = ['bad',
|
||||
"2QJL5gVSPEjMTaxWPfYkzG9UJxzZDNSx6PPeVWdzS5CFN7knZa",
|
||||
"2QJL5gVSPEjMTaxWPfYkzG9UJxzZDNSx6PPeVWdzS5CFN7knZy"]
|
||||
|
||||
gen = iter(input_list)
|
||||
def mock_input(_):
|
||||
return str(next(gen))
|
||||
|
||||
builtins.input = mock_input
|
||||
key = get_b58_key(kt)
|
||||
self.assertIsInstance(key, bytes)
|
||||
self.assertEqual(len(key), 32)
|
||||
|
||||
with self.assertRaises(SystemExit):
|
||||
get_b58_key('invalid_keytype')
|
||||
|
||||
def test_nh_bypass_msg(self):
|
||||
# Setup
|
||||
settings = Settings()
|
||||
builtins.input = lambda x: ''
|
||||
|
||||
# Test
|
||||
self.assertIsNone(nh_bypass_msg('start', settings))
|
||||
self.assertIsNone(nh_bypass_msg('finish', settings))
|
||||
|
||||
def test_pwd_prompt(self):
|
||||
# Setup
|
||||
o_getpass = getpass.getpass
|
||||
getpass.getpass = lambda x: 'testpwd'
|
||||
|
||||
# Test
|
||||
self.assertEqual(pwd_prompt("test prompt", '┌', '┐'), 'testpwd')
|
||||
|
||||
# Teardown
|
||||
getpass.getpass = o_getpass
|
||||
|
||||
|
||||
def test_yes(self):
|
||||
# Setup
|
||||
words = ['BAD', '', 'bad', 'Y', 'YES', 'N', 'NO']
|
||||
input_list = words
|
||||
gen = iter(input_list)
|
||||
|
||||
def mock_input(_):
|
||||
return str(next(gen))
|
||||
|
||||
builtins.input = mock_input
|
||||
|
||||
# Test
|
||||
self.assertTrue(yes('test prompt', head=1, tail=1))
|
||||
self.assertTrue(yes('test prompt'))
|
||||
self.assertFalse(yes('test prompt', head=1, tail=1))
|
||||
self.assertFalse(yes('test prompt'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,161 @@
|
|||
#!/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 argparse
|
||||
import os
|
||||
import types
|
||||
import unittest
|
||||
|
||||
from src.common.misc import clear_screen, ensure_dir, get_tab_complete_list, get_tab_completer, get_tty_w
|
||||
from src.common.misc import process_arguments, resize_terminal, round_up, split_string, split_byte_string
|
||||
from src.common.misc import validate_account, validate_key_exchange, validate_nick
|
||||
|
||||
from tests.mock_classes import ContactList, GroupList, Settings
|
||||
|
||||
|
||||
class TestMisc(unittest.TestCase):
|
||||
|
||||
def test_clear_screen(self):
|
||||
self.assertIsNone(clear_screen())
|
||||
|
||||
def test_ensure_dir(self):
|
||||
self.assertIsNone(ensure_dir('test_dir/'))
|
||||
try:
|
||||
os.rmdir('test_dir/')
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def test_get_tab_complete_list(self):
|
||||
# Setup
|
||||
contact_list = ContactList(nicks=['Alice', 'Bob'])
|
||||
group_list = GroupList(groups=['testgroup'])
|
||||
settings = Settings(key_list = ['key1', 'key2'])
|
||||
|
||||
tclst = ['about', 'add ', 'all', 'clear', 'cmd', 'create ', 'exit', 'export ', 'false', 'file', 'fingerprints',
|
||||
'group ', 'help', 'history ', 'localkey', 'logging ', 'msg ', 'names', 'nick ', 'notify ', 'passwd ',
|
||||
'psk', 'reset', 'rm ', 'set ', 'settings', 'store ', 'true', 'unread', 'key1 ', 'key2 ',
|
||||
'alice@jabber.org ', 'user@jabber.org ', 'Alice ', 'bob@jabber.org ', 'Bob ', 'testgroup ']
|
||||
|
||||
# Test
|
||||
self.assertEqual(set(get_tab_complete_list(contact_list, group_list, settings)), set(tclst))
|
||||
self.assertIsInstance(get_tab_completer(contact_list, group_list, settings), types.FunctionType)
|
||||
|
||||
def test_get_tty_w(self):
|
||||
self.assertIsInstance(get_tty_w(), int)
|
||||
|
||||
def test_process_arguments(self):
|
||||
# Setup
|
||||
class MockParser(object):
|
||||
def __init__(self, *_, **__):
|
||||
pass
|
||||
|
||||
def parse_args(self):
|
||||
class Args(object):
|
||||
def __init__(self):
|
||||
self.operation = True
|
||||
self.local_test = True
|
||||
self.dd_sockets = True
|
||||
args = Args()
|
||||
return args
|
||||
|
||||
def add_argument(self, *_, **__):
|
||||
pass
|
||||
|
||||
o_argparse = argparse.ArgumentParser
|
||||
argparse.ArgumentParser = MockParser
|
||||
|
||||
# Test
|
||||
self.assertEqual(process_arguments(), ('rx', True, True))
|
||||
|
||||
# Teardown
|
||||
argparse.ArgumentParser = o_argparse
|
||||
|
||||
def test_resize_terminal(self):
|
||||
self.assertIsNone(resize_terminal(24, 80))
|
||||
|
||||
def test_round_up(self):
|
||||
self.assertEqual(round_up(1), 10)
|
||||
self.assertEqual(round_up(5), 10)
|
||||
self.assertEqual(round_up(8), 10)
|
||||
self.assertEqual(round_up(10), 10)
|
||||
self.assertEqual(round_up(11), 20)
|
||||
self.assertEqual(round_up(15), 20)
|
||||
self.assertEqual(round_up(18), 20)
|
||||
self.assertEqual(round_up(20), 20)
|
||||
self.assertEqual(round_up(21), 30)
|
||||
|
||||
def test_split_string(self):
|
||||
self.assertEqual(split_string('teststring', 1), ['t', 'e', 's', 't', 's', 't', 'r', 'i', 'n', 'g'])
|
||||
self.assertEqual(split_string('teststring', 2), ['te', 'st', 'st', 'ri', 'ng'])
|
||||
self.assertEqual(split_string('teststring', 3), ['tes', 'tst', 'rin', 'g'])
|
||||
self.assertEqual(split_string('teststring', 5), ['tests', 'tring'])
|
||||
self.assertEqual(split_string('teststring', 10), ['teststring'])
|
||||
self.assertEqual(split_string('teststring', 15), ['teststring'])
|
||||
|
||||
def test_split_byte_string(self):
|
||||
self.assertEqual(split_byte_string(b'teststring', 1), [b't', b'e', b's', b't', b's', b't', b'r', b'i', b'n', b'g'])
|
||||
self.assertEqual(split_byte_string(b'teststring', 2), [b'te', b'st', b'st', b'ri', b'ng'])
|
||||
self.assertEqual(split_byte_string(b'teststring', 3), [b'tes', b'tst', b'rin', b'g'])
|
||||
self.assertEqual(split_byte_string(b'teststring', 5), [b'tests', b'tring'])
|
||||
self.assertEqual(split_byte_string(b'teststring', 10), [b'teststring'])
|
||||
self.assertEqual(split_byte_string(b'teststring', 15), [b'teststring'])
|
||||
|
||||
def test_validate_account(self):
|
||||
self.assertEqual(validate_account(248 * 'a' + '@a.com'), (True, ''))
|
||||
self.assertEqual(validate_account(249 * 'a' + '@a.com'), (False, "Account must be shorter than 255 chars."))
|
||||
self.assertEqual(validate_account(250 * 'a' + '@a.com'), (False, "Account must be shorter than 255 chars."))
|
||||
self.assertEqual(validate_account('bob@jabberorg'), (False, "Invalid account format."))
|
||||
self.assertEqual(validate_account('bobjabber.org'), (False, "Invalid account format."))
|
||||
self.assertEqual(validate_account('\x1fbobjabber.org'), (False, "Account must be printable."))
|
||||
|
||||
def test_validate_key_exchange(self):
|
||||
self.assertEqual(validate_key_exchange(''), (False, 'Invalid key exchange selection.'))
|
||||
self.assertEqual(validate_key_exchange('ec'), (False, 'Invalid key exchange selection.'))
|
||||
self.assertEqual(validate_key_exchange('e'), (True, ''))
|
||||
self.assertEqual(validate_key_exchange('E'), (True, ''))
|
||||
self.assertEqual(validate_key_exchange('ecdhe'), (True, ''))
|
||||
self.assertEqual(validate_key_exchange('ECDHE'), (True, ''))
|
||||
self.assertEqual(validate_key_exchange('p'), (True, ''))
|
||||
self.assertEqual(validate_key_exchange('P'), (True, ''))
|
||||
self.assertEqual(validate_key_exchange('psk'), (True, ''))
|
||||
self.assertEqual(validate_key_exchange('PSK'), (True, ''))
|
||||
|
||||
def test_validate_nick(self):
|
||||
# Setup
|
||||
contact_list = ContactList(nicks=['Alice', 'Bob'])
|
||||
group_list = GroupList(groups=['testgroup'])
|
||||
|
||||
# Test
|
||||
self.assertEqual(validate_nick("Alice_", (contact_list, group_list, 'alice@jabber.org')), (True, ''))
|
||||
self.assertEqual(validate_nick(254*"a", (contact_list, group_list, 'alice@jabber.org')), (True, ''))
|
||||
self.assertEqual(validate_nick(255*"a", (contact_list, group_list, 'alice@jabber.org')), (False, 'Nick must be shorter than 255 chars.'))
|
||||
self.assertEqual(validate_nick("\x01Alice", (contact_list, group_list, 'alice@jabber.org')), (False, 'Nick must be printable.'))
|
||||
self.assertEqual(validate_nick('', (contact_list, group_list, 'alice@jabber.org')), (False, "Nick can't be empty."))
|
||||
self.assertEqual(validate_nick('Me', (contact_list, group_list, 'alice@jabber.org')), (False, "'Me' is a reserved nick."))
|
||||
self.assertEqual(validate_nick('-!-', (contact_list, group_list, 'alice@jabber.org')), (False, "'-!-' is a reserved nick."))
|
||||
self.assertEqual(validate_nick('local', (contact_list, group_list, 'alice@jabber.org')), (False, "Nick can't refer to local keyfile."))
|
||||
self.assertEqual(validate_nick('a@b.org', (contact_list, group_list, 'alice@jabber.org')), (False, "Nick can't have format of an account."))
|
||||
self.assertEqual(validate_nick('Bob', (contact_list, group_list, 'alice@jabber.org')), (False, 'Nick already in use.'))
|
||||
self.assertEqual(validate_nick("Alice", (contact_list, group_list, 'alice@jabber.org')), (True, ''))
|
||||
self.assertEqual(validate_nick("testgroup", (contact_list, group_list, 'alice@jabber.org')), (False, "Nick can't be a group name."))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,74 @@
|
|||
#!/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 builtins
|
||||
import unittest
|
||||
|
||||
from src.common.output import box_print, c_print, message_printer, phase
|
||||
from src.common.output import print_fingerprints, print_on_previous_line, g_mgmt_print
|
||||
|
||||
from tests.mock_classes import ContactList
|
||||
|
||||
|
||||
class TestOutput(unittest.TestCase):
|
||||
|
||||
def test_box_print(self):
|
||||
self.assertIsNone(box_print("Test message", head=1, tail=1))
|
||||
self.assertIsNone(box_print(["Test message", '', "Another message"], head=1, tail=1))
|
||||
|
||||
o_input = builtins.input
|
||||
builtins.input = lambda x: ''
|
||||
self.assertIsNone(box_print("Test message", manual_proceed=True))
|
||||
builtins.input = o_input
|
||||
|
||||
def test_c_print(self):
|
||||
self.assertIsNone(c_print('Test message', head=1, tail=1))
|
||||
|
||||
def test_message_printer(self):
|
||||
self.assertIsNone(message_printer('Test message', head=1, tail=1))
|
||||
|
||||
def test_phase(self):
|
||||
self.assertIsNone(phase('Entering phase'))
|
||||
self.assertIsNone(phase('Done'))
|
||||
self.assertIsNone(phase('Starting phase', head=1, offset=len("Finished")))
|
||||
self.assertIsNone(phase('Finished', done=True))
|
||||
|
||||
def test_print_fingerprints(self):
|
||||
self.assertIsNone(print_fingerprints(32 * b'\x01'), 'test')
|
||||
|
||||
def test_print_on_previous_line(self):
|
||||
self.assertIsNone(print_on_previous_line())
|
||||
self.assertIsNone(print_on_previous_line(reps=2, flush=True))
|
||||
|
||||
def test_g_mgmt_print(self):
|
||||
# Setup
|
||||
contact_list = ContactList(nicks=['Alice'])
|
||||
|
||||
# Test
|
||||
self.assertIsNone(g_mgmt_print("new_g", ['alice@jabber.org', 'bob@jabber.org'], contact_list, g_name='testgroup'))
|
||||
self.assertIsNone(g_mgmt_print("add_m", ['alice@jabber.org', 'bob@jabber.org'], contact_list, g_name='testgroup'))
|
||||
self.assertIsNone(g_mgmt_print("add_a", ['alice@jabber.org', 'bob@jabber.org'], contact_list, g_name='testgroup'))
|
||||
self.assertIsNone(g_mgmt_print("rem_m", ['alice@jabber.org', 'bob@jabber.org'], contact_list, g_name='testgroup'))
|
||||
self.assertIsNone(g_mgmt_print("rem_n", ['alice@jabber.org', 'bob@jabber.org'], contact_list, g_name='testgroup'))
|
||||
self.assertIsNone(g_mgmt_print("unkwn", ['alice@jabber.org', 'bob@jabber.org'], contact_list, g_name='testgroup'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,66 @@
|
|||
#!/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 builtins
|
||||
import unittest
|
||||
|
||||
from src.common.path import ask_path_cli
|
||||
|
||||
from tests.utils import TFCTestCase
|
||||
|
||||
|
||||
class TestPath(TFCTestCase):
|
||||
|
||||
def test_ask_path_cli(self):
|
||||
# Setup
|
||||
o_input = builtins.input
|
||||
|
||||
# Test
|
||||
input_list = ['/dev/zero', "/bin/mv"]
|
||||
gen = iter(input_list)
|
||||
def mock_input(_):
|
||||
return str(next(gen))
|
||||
builtins.input = mock_input
|
||||
self.assertEqual(ask_path_cli('prompt_msg', get_file=True), "/bin/mv")
|
||||
|
||||
builtins.input = lambda x: ''
|
||||
self.assertFR("File selection aborted.", ask_path_cli, 'prompt_msg', True)
|
||||
|
||||
builtins.input = lambda x: "/home/"
|
||||
self.assertEqual(ask_path_cli('prompt_msg'), "/home/")
|
||||
|
||||
builtins.input = lambda x: "/home"
|
||||
self.assertEqual(ask_path_cli('prompt_msg'), "/home/")
|
||||
|
||||
input_list = ['/doesnotexist', "/bin/"]
|
||||
gen = iter(input_list)
|
||||
|
||||
def mock_input(_):
|
||||
return str(next(gen))
|
||||
|
||||
builtins.input = mock_input
|
||||
self.assertEqual(ask_path_cli('prompt_msg'), "/bin/")
|
||||
|
||||
# Teardown
|
||||
builtins.input = o_input
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,40 @@
|
|||
#!/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 unittest
|
||||
|
||||
from src.common.reed_solomon import RSCodec
|
||||
|
||||
|
||||
class TestRS(unittest.TestCase):
|
||||
|
||||
def test_reed_solomon(self):
|
||||
reed_solomon = RSCodec(20)
|
||||
string = 10 * "TestMessage"
|
||||
encoded = reed_solomon.encode(string)
|
||||
error = 5
|
||||
altered = os.urandom(error) + encoded[error:]
|
||||
corrected = reed_solomon.decode(altered).decode('latin-1')
|
||||
self.assertEqual(string, corrected)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,627 @@
|
|||
#!/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 datetime
|
||||
import os
|
||||
import time
|
||||
|
||||
from src.common.crypto import argon2_kdf, hash_chain
|
||||
from src.common.db_contacts import Contact
|
||||
from src.common.db_groups import Group
|
||||
from src.common.db_keys import KeySet
|
||||
from src.common.encoding import int_to_bytes
|
||||
from src.common.errors import CriticalError
|
||||
from src.common.input import pwd_prompt
|
||||
from src.common.misc import ensure_dir
|
||||
from src.common.statics import *
|
||||
|
||||
|
||||
def create_contact(nick='Alice',
|
||||
user='user',
|
||||
txfp=32 * b'\x01',
|
||||
rxfp=32 * b'\x02',
|
||||
l=True, f=True, n=True):
|
||||
"""Create mock contact object."""
|
||||
account = 'local' if nick == 'local' else f'{nick.lower()}@jabber.org'
|
||||
user = 'local' if nick == 'local' else f'{user.lower()}@jabber.org'
|
||||
return Contact(account, user, nick, txfp, rxfp, l, f, n)
|
||||
|
||||
|
||||
class ContactList(object):
|
||||
"""Mock object for unittesting."""
|
||||
|
||||
def __init__(self, nicks=None, **kwargs):
|
||||
self.master_key = MasterKey()
|
||||
self.settings = Settings()
|
||||
if nicks is None:
|
||||
self.contacts = []
|
||||
else:
|
||||
self.contacts = [create_contact(n) for n in nicks]
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def __iter__(self):
|
||||
for c in self.contacts:
|
||||
yield c
|
||||
|
||||
def __len__(self):
|
||||
return len(self.contacts)
|
||||
|
||||
def store_contacts(self):
|
||||
pass
|
||||
|
||||
def get_contact(self, selector):
|
||||
return next(c for c in self.contacts if selector in [c.rx_account, c.nick])
|
||||
|
||||
def contact_selectors(self):
|
||||
return self.get_list_of_accounts() + self.get_list_of_nicks()
|
||||
|
||||
def get_list_of_accounts(self):
|
||||
return [c.rx_account for c in self.contacts if c.rx_account != 'local']
|
||||
|
||||
def get_list_of_nicks(self):
|
||||
return [c.nick for c in self.contacts if c.nick != 'local']
|
||||
|
||||
def get_list_of_users_accounts(self):
|
||||
return list(set([c.tx_account for c in self.contacts if c.tx_account != 'local']))
|
||||
|
||||
def remove_contact(self, selector):
|
||||
for i, c in enumerate(self.contacts):
|
||||
if selector in [c.rx_account, c.nick]:
|
||||
del self.contacts[i]
|
||||
self.store_contacts()
|
||||
break
|
||||
|
||||
def has_contacts(self):
|
||||
return any(self.get_list_of_accounts())
|
||||
|
||||
def has_contact(self, selector):
|
||||
return selector in self.contact_selectors()
|
||||
|
||||
def has_local_contact(self):
|
||||
return any(c.rx_account == 'local' for c in self.contacts)
|
||||
|
||||
def add_contact(self, rx_account, tx_account, nick, tx_fingerprint, rx_fingerprint, log_messages, file_reception, notifications):
|
||||
if self.has_contact(rx_account):
|
||||
self.remove_contact(rx_account)
|
||||
contact = Contact(rx_account, tx_account, nick,
|
||||
tx_fingerprint, rx_fingerprint,
|
||||
log_messages, file_reception, notifications)
|
||||
self.contacts.append(contact)
|
||||
self.store_contacts()
|
||||
|
||||
@staticmethod
|
||||
def print_contacts(spacing=False):
|
||||
print(spacing)
|
||||
|
||||
|
||||
def create_group(name='testgroup', nick_list=None):
|
||||
"""Create mock group object."""
|
||||
if nick_list is None:
|
||||
nick_list = ['Alice', 'Bob']
|
||||
settings = Settings()
|
||||
store_f = lambda: None
|
||||
contacts = [create_contact(n) for n in nick_list]
|
||||
return Group(name, False, False, contacts, settings, store_f)
|
||||
|
||||
|
||||
class GroupList(object):
|
||||
"""Mock object for unittesting."""
|
||||
|
||||
def __init__(self, groups = None, **kwargs):
|
||||
self.groups = []
|
||||
if groups is not None:
|
||||
for g in groups:
|
||||
self.groups.append(create_group(g))
|
||||
self.master_key = MasterKey()
|
||||
self.contact_list = ContactList()
|
||||
self.settings = Settings()
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def __iter__(self):
|
||||
for g in self.groups:
|
||||
yield g
|
||||
|
||||
def __len__(self):
|
||||
return len(self.groups)
|
||||
|
||||
def store_groups(self):
|
||||
pass
|
||||
|
||||
def add_group(self, name, logging, notifications, members):
|
||||
if self.has_group(name):
|
||||
self.remove_group(name)
|
||||
self.groups.append(Group(name, logging, notifications, members, self.settings, self.store_groups))
|
||||
self.store_groups()
|
||||
|
||||
def largest_group(self):
|
||||
largest = 0
|
||||
for g in self.groups:
|
||||
largest = max(len(g), largest)
|
||||
return largest
|
||||
|
||||
def get_list_of_group_names(self):
|
||||
return [g.name for g in self.groups]
|
||||
|
||||
def get_group(self, name):
|
||||
return next(g for g in self.groups if g.name == name)
|
||||
|
||||
def has_group(self, name):
|
||||
return any([g.name == name for g in self.groups])
|
||||
|
||||
def has_groups(self):
|
||||
return any(self.groups)
|
||||
|
||||
def get_group_members(self, name):
|
||||
return self.get_group(name).members
|
||||
|
||||
def remove_group(self, name):
|
||||
for i, g in enumerate(self.groups):
|
||||
if g.name == name:
|
||||
del self.groups[i]
|
||||
self.store_groups()
|
||||
break
|
||||
|
||||
@staticmethod
|
||||
def print_groups():
|
||||
print('mock group printing')
|
||||
|
||||
|
||||
def create_keyset(nick='Alice',
|
||||
tx_key=32 * b'\x01',
|
||||
tx_hek=32 * b'\x01',
|
||||
rx_key=32 * b'\x01',
|
||||
rx_hek=32 * b'\x01',
|
||||
tx_harac=0,
|
||||
rx_harac=0,
|
||||
store_f=None):
|
||||
"""Create mock keyset object."""
|
||||
if store_f is None:
|
||||
store_f = lambda: None
|
||||
account = 'local' if nick == 'local' else f'{nick.lower()}@jabber.org'
|
||||
return KeySet(account, tx_key, tx_hek, rx_key, rx_hek, tx_harac, rx_harac, store_f)
|
||||
|
||||
|
||||
class KeyList(object):
|
||||
"""Mock object for unittesting."""
|
||||
|
||||
def __init__(self, nicks=None, **kwargs):
|
||||
self.master_key = MasterKey()
|
||||
self.settings = Settings()
|
||||
if nicks is None:
|
||||
self.keysets = []
|
||||
else:
|
||||
self.keysets = [create_keyset(n) for n in nicks]
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def store_keys(self):
|
||||
pass
|
||||
|
||||
def get_keyset(self, account):
|
||||
return next(k for k in self.keysets if account == k.rx_account)
|
||||
|
||||
def has_local_key(self):
|
||||
return any(k.rx_account == 'local' for k in self.keysets)
|
||||
|
||||
def has_keyset(self, account):
|
||||
return any(account == k.rx_account for k in self.keysets)
|
||||
|
||||
def add_keyset(self, rx_account, tx_key, rx_key, tx_hek, rx_hek):
|
||||
if self.has_keyset(rx_account):
|
||||
self.remove_keyset(rx_account)
|
||||
self.keysets.append(KeySet(rx_account, tx_key, rx_key, tx_hek, rx_hek, 0, 0, self.store_keys))
|
||||
self.store_keys()
|
||||
|
||||
def remove_keyset(self, name):
|
||||
for i, k in enumerate(self.keysets):
|
||||
if name == k.rx_account:
|
||||
del self.keysets[i]
|
||||
break
|
||||
|
||||
def change_master_key(self, master_key):
|
||||
self.master_key = master_key
|
||||
|
||||
def manage(self, command, *params):
|
||||
if command == 'ADD': self.add_keyset(*params)
|
||||
elif command == 'REM': self.remove_keyset(*params)
|
||||
elif command == 'KEY': self.change_master_key(*params)
|
||||
else: raise CriticalError("Invalid KeyList management command.")
|
||||
|
||||
|
||||
class MasterKey(object):
|
||||
"""Mock object for unittesting."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.master_key = bytes(32)
|
||||
self.file_name = f'{DIR_USER_DATA}/ut_login_data'
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
@classmethod
|
||||
def get_password(cls, purpose="master password"):
|
||||
return pwd_prompt(f"Enter {purpose}: ", '┌', '┐')
|
||||
|
||||
@classmethod
|
||||
def new_password(cls, purpose="master password"):
|
||||
password_1 = pwd_prompt(f"Enter a new {purpose}: ", '┌', '┐')
|
||||
password_2 = pwd_prompt(f"Confirm the {purpose}: ", '├', '┤')
|
||||
if password_1 == password_2:
|
||||
return password_1
|
||||
else:
|
||||
return cls.new_password(purpose)
|
||||
|
||||
def new_master_key(self):
|
||||
password = MasterKey.new_password()
|
||||
salt = os.urandom(32)
|
||||
rounds = 1
|
||||
|
||||
assert isinstance(salt, bytes)
|
||||
while True:
|
||||
time_start = time.monotonic()
|
||||
master_key, memory = argon2_kdf(password, salt, rounds, local_testing=False)
|
||||
time_final = time.monotonic() - time_start
|
||||
|
||||
if time_final > 3.0:
|
||||
self.master_key = master_key
|
||||
master_key_hash = hash_chain(master_key)
|
||||
ensure_dir(f'{DIR_USER_DATA}/')
|
||||
with open(self.file_name, 'wb+') as f:
|
||||
f.write(salt
|
||||
+ master_key_hash
|
||||
+ int_to_bytes(rounds)
|
||||
+ int_to_bytes(memory))
|
||||
break
|
||||
else:
|
||||
rounds *= 2
|
||||
|
||||
|
||||
class Settings(object):
|
||||
"""Mock object for unittesting."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.format_of_logfiles = '%Y-%m-%d %H:%M:%S'
|
||||
self.disable_gui_dialog = False
|
||||
self.m_members_in_group = 20
|
||||
self.m_number_of_groups = 20
|
||||
self.m_number_of_accnts = 20
|
||||
self.serial_iface_speed = 19200
|
||||
self.e_correction_ratio = 5
|
||||
self.log_msg_by_default = False
|
||||
self.store_file_default = False
|
||||
self.n_m_notify_privacy = False
|
||||
self.log_dummy_file_a_p = True
|
||||
|
||||
# Transmitter settings
|
||||
self.txm_serial_adapter = True
|
||||
self.nh_bypass_messages = True
|
||||
self.confirm_sent_files = True
|
||||
self.double_space_exits = False
|
||||
self.trickle_connection = False
|
||||
self.trickle_stat_delay = 2.0
|
||||
self.trickle_rand_delay = 2.0
|
||||
self.long_packet_rand_d = False
|
||||
self.max_val_for_rand_d = 10.0
|
||||
|
||||
# Receiver settings
|
||||
self.rxm_serial_adapter = True
|
||||
self.new_msg_notify_dur = 1.0
|
||||
|
||||
self.master_key = MasterKey()
|
||||
self.software_operation = 'ut'
|
||||
self.local_testing_mode = False
|
||||
self.data_diode_sockets = False
|
||||
|
||||
self.session_ec_ratio = self.e_correction_ratio
|
||||
self.session_if_speed = self.serial_iface_speed
|
||||
self.session_trickle = self.trickle_connection
|
||||
self.session_usb_iface = None
|
||||
|
||||
# Override defaults with specified kwargs
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def store_settings(self):
|
||||
pass
|
||||
|
||||
def change_setting(self, key, value, *_):
|
||||
attribute = self.__getattribute__(key)
|
||||
if isinstance(attribute, bool):
|
||||
value = value.lower().capitalize()
|
||||
value = value if isinstance(attribute, str) else eval(value)
|
||||
setattr(self, key, value)
|
||||
|
||||
@staticmethod
|
||||
def print_settings():
|
||||
print("Mock setting printing")
|
||||
|
||||
|
||||
class UserInput(object):
|
||||
"""Mock object for unittesting."""
|
||||
|
||||
def __init__(self, plaintext=None, **kwargs):
|
||||
self.plaintext = plaintext
|
||||
self.window = None
|
||||
self.settings = None
|
||||
self.w_type = None
|
||||
self.type = None
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
|
||||
class Window(object):
|
||||
"""Mock object for unittesting."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.contact_list = ContactList()
|
||||
self.group_list = GroupList()
|
||||
self.window_contacts = []
|
||||
self.group = None
|
||||
self.contact = None
|
||||
self.name = None
|
||||
self.type = None
|
||||
self.uid = None
|
||||
self.imc_name = None
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def __iter__(self):
|
||||
for c in self.window_contacts:
|
||||
yield c
|
||||
|
||||
def __len__(self):
|
||||
return len(self.window_contacts)
|
||||
|
||||
def deselect(self):
|
||||
pass
|
||||
|
||||
def update_group_win_members(self, group_list):
|
||||
if self.type == 'group':
|
||||
if group_list.has_group(self.name):
|
||||
self.group = group_list.get_group(self.name)
|
||||
self.window_contacts = self.group.members
|
||||
if self.window_contacts:
|
||||
self.imc_name = self.window_contacts[0].rx_account
|
||||
else:
|
||||
self.deselect()
|
||||
|
||||
def is_selected(self):
|
||||
return self.name is not None
|
||||
|
||||
|
||||
class FileWindow(object):
|
||||
"""Mock object for unittesting."""
|
||||
|
||||
def __init__(self, uid, packet_list=None, **kwargs):
|
||||
self.uid = uid
|
||||
self.unread_messages = 0
|
||||
self.is_active = False
|
||||
if packet_list is None:
|
||||
self.packet_list = PacketList()
|
||||
else:
|
||||
self.packet_list = packet_list
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def redraw(self):
|
||||
pass
|
||||
|
||||
|
||||
def create_window(nick='Alice'):
|
||||
account = 'local' if nick == 'local' else f'{nick.lower()}@jabber.org'
|
||||
return RxMWindow(uid=account)
|
||||
|
||||
class RxMWindow(object):
|
||||
"""Mock object for unittesting."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.uid = None
|
||||
self.contact_list = None
|
||||
self.group_list = None
|
||||
self.settings = None
|
||||
self.type = None
|
||||
self.name = None
|
||||
self.is_active = False
|
||||
self.group_timestamp = time.time() * 1000
|
||||
self.window_contacts = []
|
||||
self.message_log = []
|
||||
self.unread_messages = 0
|
||||
self.previous_msg_ts = datetime.datetime.now()
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.message_log)
|
||||
|
||||
def __iter__(self):
|
||||
for m in self.message_log:
|
||||
yield m
|
||||
|
||||
def remove_contacts(self, accounts):
|
||||
for account in accounts:
|
||||
for i, m in enumerate(self.window_contacts):
|
||||
if account == m.rx_account:
|
||||
del self.window_contacts[i]
|
||||
|
||||
def add_contacts(self, accounts):
|
||||
for a in accounts:
|
||||
if not self.has_contact(a) and self.contact_list.has_contact(a):
|
||||
self.window_contacts.append(self.contact_list.get_contact(a))
|
||||
|
||||
def reset_window(self):
|
||||
self.message_log = []
|
||||
|
||||
def clear_window(self):
|
||||
pass
|
||||
|
||||
def has_contact(self, account):
|
||||
return any(c.rx_account == account for c in self.window_contacts)
|
||||
|
||||
def print(self, msg_tuple):
|
||||
ts, message, account, origin = msg_tuple
|
||||
if self.previous_msg_ts.date() != ts.date():
|
||||
print(f"00:00 -!- Day changed.")
|
||||
self.previous_msg_ts = ts
|
||||
if self.is_active:
|
||||
print(message)
|
||||
else:
|
||||
self.unread_messages += 1
|
||||
|
||||
def print_new(self,
|
||||
timestamp,
|
||||
message,
|
||||
account='local',
|
||||
origin=ORIGIN_USER_HEADER,
|
||||
print_=True):
|
||||
msg_tuple = (timestamp, message, account, origin)
|
||||
self.message_log.append(msg_tuple)
|
||||
if print_:
|
||||
self.print(msg_tuple)
|
||||
|
||||
def redraw(self):
|
||||
self.unread_messages = 0
|
||||
if self.message_log:
|
||||
self.previous_msg_ts = self.message_log[0][0]
|
||||
for msg_tuple in self.message_log:
|
||||
self.print(msg_tuple)
|
||||
|
||||
|
||||
class WindowList(object):
|
||||
"""Mock object for unittesting."""
|
||||
|
||||
def __init__(self, nicks=None, **kwargs):
|
||||
self.contact_list = ContactList()
|
||||
self.group_list = GroupList()
|
||||
self.packet_list = PacketList()
|
||||
self.settings = Settings()
|
||||
if nicks is None:
|
||||
self.windows = []
|
||||
else:
|
||||
self.windows = [create_window(n) for n in nicks]
|
||||
|
||||
self.active_win = None
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.windows)
|
||||
|
||||
def __iter__(self):
|
||||
for w in self.windows:
|
||||
yield w
|
||||
|
||||
def select_rx_window(self, name):
|
||||
if self.active_win is not None:
|
||||
self.active_win.is_active = False
|
||||
self.active_win = self.get_window(name)
|
||||
self.active_win.is_active = True
|
||||
|
||||
def has_window(self, name):
|
||||
return name in self.get_list_of_window_names()
|
||||
|
||||
def get_list_of_window_names(self):
|
||||
return [w.uid for w in self.windows]
|
||||
|
||||
def get_local_window(self):
|
||||
return self.get_window('local')
|
||||
|
||||
def get_window(self, name):
|
||||
if not self.has_window(name):
|
||||
if name == FILE_R_WIN_ID_BYTES.decode():
|
||||
self.windows.append(FileWindow(name, self.packet_list))
|
||||
else:
|
||||
self.windows.append(RxMWindow(uid=name, contact_list=self.contact_list, group_list=self.group_list, settings=self.settings))
|
||||
|
||||
return next(w for w in self.windows if w.uid == name)
|
||||
|
||||
|
||||
class Gateway(object):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.packets = []
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def write(self, output):
|
||||
self.packets.append(output)
|
||||
|
||||
|
||||
class Packet(object):
|
||||
"""Mock object for unittesting."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.account = None
|
||||
self.contact = None
|
||||
self.origin = None
|
||||
self.type = None
|
||||
self.settings = None
|
||||
self.f_name = None
|
||||
self.f_size = None
|
||||
self.f_packets = None
|
||||
self.f_eta = None
|
||||
self.lt_active = False
|
||||
self.is_complete = False
|
||||
self.assembly_pt_list = []
|
||||
self.payload = None # Unittest mock return value
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def add_packet(self, packet):
|
||||
pass
|
||||
|
||||
def assemble_message_packet(self):
|
||||
return self.payload
|
||||
|
||||
def assemble_and_store_file(self):
|
||||
return self.payload
|
||||
|
||||
def assemble_command_packet(self):
|
||||
return self.payload
|
||||
|
||||
|
||||
class PacketList(object):
|
||||
"""Mock object for unittesting."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.contact_list = ContactList()
|
||||
self.settings = Settings()
|
||||
self.packet_l = []
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def __iter__(self):
|
||||
for p in self.packet_l:
|
||||
yield p
|
||||
|
||||
def __len__(self):
|
||||
return len(self.packet_l)
|
||||
|
||||
def has_packet(self, account, origin, type_):
|
||||
return any(p for p in self.packet_l if (p.account == account
|
||||
and p.origin == origin
|
||||
and p.type == type_))
|
||||
|
||||
def get_packet(self, account, origin, type_):
|
||||
if not self.has_packet(account, origin, type_):
|
||||
contact = self.contact_list.get_contact(account)
|
||||
self.packet_l.append(Packet(account=account, contact=contact, origin=origin, type=type_, settings=self.settings))
|
||||
return next(p for p in self.packet_l if (p.account == account
|
||||
and p.origin == origin
|
||||
and p.type == type_))
|
|
@ -0,0 +1,43 @@
|
|||
#!/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 unittest
|
||||
|
||||
from src.nh.gateway import Gateway
|
||||
|
||||
from tests.mock_classes import Settings
|
||||
|
||||
|
||||
class TestGateway(unittest.TestCase):
|
||||
|
||||
@unittest.skipIf("TRAVIS" in os.environ and os.environ["TRAVIS"] == "true", "Skipping this test on Travis CI.")
|
||||
def test_class(self):
|
||||
# Setup
|
||||
settings = Settings(serial_usb_adapter=False)
|
||||
gateway = Gateway(settings)
|
||||
|
||||
# Test
|
||||
self.assertIsNone(gateway.write(b'test'))
|
||||
self.assertEqual(gateway.search_serial_interface(), '/dev/ttyS0')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,117 @@
|
|||
#!/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 argparse
|
||||
import builtins
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from src.nh.misc import box_print, c_print, clear_screen, ensure_dir, get_tty_w, graceful_exit
|
||||
from src.nh.misc import phase, print_on_previous_line, process_arguments, yes
|
||||
|
||||
|
||||
class TestMisc(unittest.TestCase):
|
||||
|
||||
def test_box_print(self):
|
||||
self.assertIsNone(box_print("Test message", head=1, tail=1))
|
||||
self.assertIsNone(box_print(["Test message", '', "Another message"], head=1, tail=1))
|
||||
|
||||
o_input = builtins.input
|
||||
builtins.input = lambda x: ''
|
||||
self.assertIsNone(box_print("Test message", manual_proceed=True))
|
||||
builtins.input = o_input
|
||||
|
||||
def test_c_print(self):
|
||||
self.assertIsNone(c_print('Test message', head=1, tail=1))
|
||||
|
||||
def test_clear_screen(self):
|
||||
self.assertIsNone(clear_screen())
|
||||
|
||||
def test_ensure_dir(self):
|
||||
self.assertIsNone(ensure_dir('test_dir/'))
|
||||
try:
|
||||
os.rmdir('test_dir/')
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def test_get_tty_w(self):
|
||||
self.assertIsInstance(get_tty_w(), int)
|
||||
|
||||
def test_graceful_exit(self):
|
||||
with self.assertRaises(SystemExit):
|
||||
graceful_exit('test message')
|
||||
graceful_exit('test message', clear=True)
|
||||
|
||||
def test_phase(self):
|
||||
self.assertIsNone(phase('Entering phase'))
|
||||
self.assertIsNone(phase('Done'))
|
||||
self.assertIsNone(phase('Starting phase', head=1, offset=len("Finished")))
|
||||
self.assertIsNone(phase('Finished', done=True))
|
||||
|
||||
def test_print_on_previous_line(self):
|
||||
self.assertIsNone(print_on_previous_line())
|
||||
self.assertIsNone(print_on_previous_line(reps=2, flush=True))
|
||||
|
||||
def test_process_arguments(self):
|
||||
# Setup
|
||||
class MockParser(object):
|
||||
def __init__(self, *_, **__):
|
||||
pass
|
||||
|
||||
def parse_args(self):
|
||||
class Args(object):
|
||||
def __init__(self):
|
||||
self.local_test = True
|
||||
self.dd_sockets = True
|
||||
args = Args()
|
||||
return args
|
||||
|
||||
def add_argument(self, *_, **__):
|
||||
pass
|
||||
|
||||
o_argparse = argparse.ArgumentParser
|
||||
argparse.ArgumentParser = MockParser
|
||||
|
||||
# Test
|
||||
self.assertEqual(process_arguments(), (True, True))
|
||||
|
||||
# Teardown
|
||||
argparse.ArgumentParser = o_argparse
|
||||
|
||||
def test_yes(self):
|
||||
# Setup
|
||||
words = ['BAD', '', 'bad', 'Y', 'YES', 'N', 'NO']
|
||||
input_list = words
|
||||
gen = iter(input_list)
|
||||
|
||||
def mock_input(_):
|
||||
return str(next(gen))
|
||||
|
||||
builtins.input = mock_input
|
||||
|
||||
# Test
|
||||
self.assertTrue(yes('test prompt', head=1, tail=1))
|
||||
self.assertTrue(yes('test prompt'))
|
||||
self.assertFalse(yes('test prompt', head=1, tail=1))
|
||||
self.assertFalse(yes('test prompt'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,72 @@
|
|||
#!/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 builtins
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from src.common.statics import *
|
||||
from src.nh.settings import bool_to_bytes, int_to_bytes, bytes_to_bool, bytes_to_int, Settings
|
||||
|
||||
from tests.utils import cleanup
|
||||
|
||||
class TestConversions(unittest.TestCase):
|
||||
|
||||
def test_bool_to_bytes(self):
|
||||
self.assertEqual(bool_to_bytes(False), b'\x00')
|
||||
self.assertEqual(bool_to_bytes(True), b'\x01')
|
||||
|
||||
def int_to_bytes(self):
|
||||
self.assertEqual(int_to_bytes(1), b'\x00\x00\x00\x00\x00\x00\x00\x01')
|
||||
|
||||
def test_bytes_to_bool(self):
|
||||
self.assertEqual(bytes_to_bool(b'\x00'), False)
|
||||
self.assertEqual(bytes_to_bool(b'\x01'), True)
|
||||
|
||||
def test_bytes_to_int(self):
|
||||
self.assertEqual(bytes_to_int(b'\x00\x00\x00\x00\x00\x00\x00\x01'), 1)
|
||||
|
||||
|
||||
class TestSettings(unittest.TestCase):
|
||||
|
||||
def test_class(self):
|
||||
# Setup
|
||||
o_input = builtins.input
|
||||
builtins.input = lambda x: 'yes'
|
||||
settings = Settings(False, False, 'ut')
|
||||
|
||||
# Test store/load
|
||||
settings.disable_gui_dialog = True
|
||||
settings.store_settings()
|
||||
|
||||
self.assertTrue(os.path.isfile(f"{DIR_USER_DATA}/ut_settings"))
|
||||
self.assertEqual(os.path.getsize(f"{DIR_USER_DATA}/ut_settings"), 8 + 8 + 1 + 1)
|
||||
|
||||
settings2 = Settings(False, False, 'ut')
|
||||
self.assertTrue(settings2.disable_gui_dialog)
|
||||
|
||||
builtins.input = o_input
|
||||
|
||||
def tearDown(self):
|
||||
cleanup()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,313 @@
|
|||
#!/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 datetime
|
||||
import getpass
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from src.common.encoding import int_to_bytes
|
||||
from src.common.db_logs import write_log_entry
|
||||
from src.common.statics import *
|
||||
from src.rx.commands import show_win_activity, select_win_cmd, clear_active_window, reset_active_window, display_logs
|
||||
from src.rx.commands import export_logs, change_master_key, change_nick, change_setting, contact_setting, remove_contact
|
||||
|
||||
from tests.mock_classes import Settings, ContactList, GroupList, MasterKey, RxMWindow, WindowList, KeyList
|
||||
from tests.utils import cleanup, TFCTestCase
|
||||
|
||||
|
||||
class TestShowWinActivity(unittest.TestCase):
|
||||
|
||||
def test_function(self):
|
||||
# Setup
|
||||
window_list = WindowList()
|
||||
window_list.windows = [RxMWindow(name='Alice', unread_messages=4)]
|
||||
|
||||
# Test
|
||||
self.assertIsNone(show_win_activity(window_list))
|
||||
|
||||
|
||||
class TestSelectWinCMD(unittest.TestCase):
|
||||
|
||||
def test_function(self):
|
||||
# Setup
|
||||
window_list = WindowList()
|
||||
window_list.windows = [RxMWindow(uid='alice@jabber.org', name='Alice'),
|
||||
RxMWindow(uid='bob@jabber.org', name='Bob')]
|
||||
|
||||
# Test
|
||||
self.assertIsNone(select_win_cmd(b'alice@jabber.org', window_list))
|
||||
self.assertEqual(window_list.active_win.name, 'Alice')
|
||||
|
||||
self.assertIsNone(select_win_cmd(b'bob@jabber.org', window_list))
|
||||
self.assertEqual(window_list.active_win.name, 'Bob')
|
||||
|
||||
self.assertIsNone(select_win_cmd(FILE_R_WIN_ID_BYTES, window_list))
|
||||
self.assertEqual(window_list.active_win.uid, 'file_window')
|
||||
|
||||
|
||||
class TestClearActiveWindow(unittest.TestCase):
|
||||
|
||||
def test_function(self):
|
||||
self.assertIsNone(clear_active_window())
|
||||
|
||||
|
||||
class TestResetActiveWindow(unittest.TestCase):
|
||||
|
||||
def test_function(self):
|
||||
# Setup
|
||||
cmd_data = b'alice@jabber.org'
|
||||
window_list = WindowList()
|
||||
window_list.windows = [RxMWindow(uid='alice@jabber.org', name='Alice'),
|
||||
RxMWindow(uid='bob@jabber.org', name='Bob')]
|
||||
|
||||
# Test
|
||||
self.assertIsNone(reset_active_window(cmd_data, window_list))
|
||||
|
||||
|
||||
class TestDisplayLogs(TFCTestCase):
|
||||
|
||||
def test_function(self):
|
||||
# Setup
|
||||
no_msg = int_to_bytes(1)
|
||||
cmd_data = b'alice@jabber.org' + US_BYTE + no_msg
|
||||
window_list = WindowList()
|
||||
contact_list = ContactList()
|
||||
settings = Settings()
|
||||
master_key = MasterKey()
|
||||
|
||||
# Test
|
||||
self.assertFR("Error: Could not find 'user_data/ut_logs'.", display_logs, cmd_data, window_list, contact_list, settings, master_key)
|
||||
|
||||
|
||||
class TestExportLogs(TFCTestCase):
|
||||
|
||||
def test_function(self):
|
||||
# Setup
|
||||
ts = datetime.datetime.now()
|
||||
no_msg = int_to_bytes(1)
|
||||
cmd_data = b'alice@jabber.org' + US_BYTE + no_msg
|
||||
window_list = WindowList()
|
||||
contact_list = ContactList()
|
||||
settings = Settings()
|
||||
master_key = MasterKey()
|
||||
write_log_entry(F_S_HEADER + bytes(255), 'alice@jabber.org', settings, master_key)
|
||||
|
||||
# Test
|
||||
self.assertIsNone(export_logs(cmd_data, ts, window_list, contact_list, settings, master_key))
|
||||
os.remove('Unittest - Plaintext log (None)')
|
||||
cleanup()
|
||||
|
||||
|
||||
class TestChangeMasterKey(unittest.TestCase):
|
||||
|
||||
def test_function(self):
|
||||
# Setup
|
||||
master_key = MasterKey()
|
||||
settings = Settings()
|
||||
ts = datetime.datetime.now()
|
||||
o_getpass = getpass.getpass
|
||||
window_list = WindowList()
|
||||
contact_list = ContactList()
|
||||
group_list = GroupList()
|
||||
key_list = KeyList()
|
||||
getpass.getpass = lambda x: 'a'
|
||||
|
||||
write_log_entry(F_S_HEADER + bytes(255), 'alice@jabber.org', settings, master_key)
|
||||
|
||||
# Test
|
||||
self.assertEqual(master_key.master_key, bytes(32))
|
||||
self.assertIsNone(change_master_key(ts, window_list, contact_list, group_list, key_list, settings, master_key))
|
||||
self.assertNotEqual(master_key.master_key, bytes(32))
|
||||
|
||||
# Teardown
|
||||
getpass.getpass = o_getpass
|
||||
cleanup()
|
||||
|
||||
|
||||
class TestChangeNick(TFCTestCase):
|
||||
|
||||
def test_invalid_nick_raises_fr(self):
|
||||
# Setup
|
||||
cmd_data = b'alice@jabber.org' + US_BYTE + b'Me'
|
||||
ts = datetime.datetime.now()
|
||||
window_list = WindowList()
|
||||
contact_list = ContactList()
|
||||
group_list = GroupList()
|
||||
|
||||
# Test
|
||||
self.assertFR("'Me' is a reserved nick.", change_nick, cmd_data, ts, window_list, contact_list, group_list)
|
||||
|
||||
|
||||
def test_nick_change(self):
|
||||
# Setup
|
||||
cmd_data = b'alice@jabber.org' + US_BYTE + b'Alice_'
|
||||
ts = datetime.datetime.now()
|
||||
window_list = WindowList()
|
||||
contact_list = ContactList(nicks=['Alice'])
|
||||
group_list = GroupList()
|
||||
|
||||
# Test
|
||||
self.assertIsNone(change_nick(cmd_data, ts, window_list, contact_list, group_list))
|
||||
|
||||
|
||||
class TestChangeSetting(TFCTestCase):
|
||||
|
||||
def test_invalid_setting_raises_r(self):
|
||||
# Setup
|
||||
cmd_data = b'setting' + US_BYTE + b'True'
|
||||
ts = datetime.datetime.now()
|
||||
window_list = WindowList()
|
||||
contact_list = ContactList()
|
||||
group_list = GroupList()
|
||||
settings = Settings(key_list=[''])
|
||||
|
||||
# Test
|
||||
self.assertFR("Invalid setting setting.", change_setting, cmd_data, ts, window_list, contact_list, group_list, settings)
|
||||
|
||||
def test_valid_setting_change(self):
|
||||
# Setup
|
||||
cmd_data = b'e_correction_ratio' + US_BYTE + b'5'
|
||||
ts = datetime.datetime.now()
|
||||
window_list = WindowList()
|
||||
contact_list = ContactList()
|
||||
group_list = GroupList()
|
||||
settings = Settings(key_list=['e_correction_ratio'])
|
||||
|
||||
# Test
|
||||
self.assertIsNone(change_setting(cmd_data, ts, window_list, contact_list, group_list, settings))
|
||||
|
||||
|
||||
class TestContactSetting(TFCTestCase):
|
||||
|
||||
def test_invalid_window_raises_fr(self):
|
||||
# Setup
|
||||
cmd_data = b'e' + US_BYTE + b'bob@jabber.org'
|
||||
ts = datetime.datetime.now()
|
||||
window_list = WindowList()
|
||||
contact_list = ContactList()
|
||||
group_list = GroupList()
|
||||
setting_type = 'L'
|
||||
|
||||
# Test
|
||||
self.assertFR("Error: Found no window for bob@jabber.org.", contact_setting, cmd_data, ts, window_list ,contact_list, group_list, setting_type)
|
||||
|
||||
def test_enable_logging_contact(self):
|
||||
# Setup
|
||||
cmd_data = b'e' + US_BYTE + b'bob@jabber.org'
|
||||
ts = datetime.datetime.now()
|
||||
contact_list = ContactList(nicks=['Bob'])
|
||||
window_list = WindowList(windows=[RxMWindow(type='contact', name='Bob', uid='bob@jabber.org')])
|
||||
group_list = GroupList()
|
||||
setting_type = 'L'
|
||||
|
||||
# Test
|
||||
contact_list.get_contact('bob@jabber.org').log_messages = False
|
||||
self.assertFalse(contact_list.get_contact('bob@jabber.org').log_messages)
|
||||
self.assertIsNone(contact_setting(cmd_data, ts, window_list, contact_list, group_list, setting_type))
|
||||
self.assertTrue(contact_list.get_contact('bob@jabber.org').log_messages)
|
||||
|
||||
def test_enable_logging_group(self):
|
||||
# Setup
|
||||
cmd_data = b'e' + US_BYTE + b'testgroup'
|
||||
ts = datetime.datetime.now()
|
||||
contact_list = ContactList(nicks=['Bob'])
|
||||
window_list = WindowList(windows=[RxMWindow(type='group', name='testgroup', uid='testgroup')])
|
||||
group_list = GroupList(groups=['testgroup'])
|
||||
setting_type = 'L'
|
||||
|
||||
# Test
|
||||
group_list.get_group('testgroup').log_messages = False
|
||||
self.assertIsNone(contact_setting(cmd_data, ts, window_list, contact_list, group_list, setting_type))
|
||||
self.assertTrue(group_list.get_group('testgroup').log_messages)
|
||||
|
||||
def test_enable_logging_all(self):
|
||||
# Setup
|
||||
cmd_data = b'E'
|
||||
ts = datetime.datetime.now()
|
||||
contact_list = ContactList(nicks=['Alice', 'Bob', 'Charlie'])
|
||||
window_list = WindowList(windows=[RxMWindow(type='group', name='testgroup', uid='testgroup')])
|
||||
group_list = GroupList(groups=['testgroup'])
|
||||
setting_type = 'L'
|
||||
|
||||
# Test
|
||||
for c in contact_list:
|
||||
c.log_messages = False
|
||||
|
||||
group_list.get_group('testgroup').log_messages = False
|
||||
self.assertIsNone(contact_setting(cmd_data, ts, window_list, contact_list, group_list, setting_type))
|
||||
self.assertTrue(group_list.get_group('testgroup').log_messages)
|
||||
for c in contact_list:
|
||||
self.assertTrue(c.log_messages)
|
||||
|
||||
def test_enable_file_reception_group(self):
|
||||
# Setup
|
||||
cmd_data = b'd' + US_BYTE + b'testgroup'
|
||||
ts = datetime.datetime.now()
|
||||
contact_list = ContactList(nicks=['Bob', 'Alice'])
|
||||
group_list = GroupList(groups=['testgroup'])
|
||||
window_list = WindowList(windows=[RxMWindow(type='group', name='testgroup', uid='testgroup')])
|
||||
setting_type = 'F'
|
||||
|
||||
for c in contact_list:
|
||||
self.assertTrue(c.file_reception)
|
||||
|
||||
# Test
|
||||
self.assertIsNone(contact_setting(cmd_data, ts, window_list, contact_list, group_list, setting_type))
|
||||
|
||||
for c in contact_list:
|
||||
self.assertFalse(c.file_reception)
|
||||
|
||||
|
||||
class TestRemoveContact(unittest.TestCase):
|
||||
|
||||
def test_no_contact(self):
|
||||
# Setup
|
||||
cmd_data = b'bob@jabber.org'
|
||||
ts = datetime.datetime.now()
|
||||
contact_list = ContactList(nicks=['Alice'])
|
||||
group_list = GroupList(groups=[])
|
||||
key_list = KeyList(nicks=['Alice'])
|
||||
window_list = WindowList()
|
||||
|
||||
# Test
|
||||
self.assertIsNone(remove_contact(cmd_data, ts, window_list, contact_list, group_list, key_list))
|
||||
|
||||
|
||||
def test_successful_removal(self):
|
||||
# Setup
|
||||
cmd_data = b'bob@jabber.org'
|
||||
ts = datetime.datetime.now()
|
||||
contact_list = ContactList(nicks=['Alice', 'Bob'])
|
||||
contact = contact_list.get_contact('bob@jabber.org')
|
||||
group_list = GroupList(groups=['testgroup', 'testgroup2'])
|
||||
key_list = KeyList(nicks=['Alice', 'Bob'])
|
||||
window_list = WindowList()
|
||||
|
||||
# Test
|
||||
self.assertIsNone(remove_contact(cmd_data, ts, window_list, contact_list, group_list, key_list))
|
||||
self.assertFalse(contact_list.has_contact('bob@jabber.org'))
|
||||
self.assertFalse(key_list.has_keyset('bob@jabber.org'))
|
||||
for g in group_list:
|
||||
self.assertFalse(contact in g.members)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,156 @@
|
|||
#!/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 datetime
|
||||
import unittest
|
||||
|
||||
from src.common.statics import *
|
||||
from src.rx.commands_g import group_create, group_add_member, group_rm_member, remove_group
|
||||
|
||||
from tests.mock_classes import Contact, ContactList, GroupList, Settings, WindowList
|
||||
from tests.utils import TFCTestCase
|
||||
|
||||
|
||||
class TestGroupCreate(TFCTestCase):
|
||||
|
||||
def test_too_many_purp_accounts_raises_fr(self):
|
||||
# Setup
|
||||
ts = datetime.datetime.now()
|
||||
cl = ["contact_{}@jabber.org".format(n).encode() for n in range(21)]
|
||||
cmd_data = US_BYTE.join([b'testgroup2'] + cl)
|
||||
group_list = GroupList(groups=['testgroup'])
|
||||
contact_list = ContactList(nicks=["contact_{}".format(n) for n in range(21)])
|
||||
group = group_list.get_group('testgroup')
|
||||
group.members = contact_list.contacts
|
||||
settings = Settings()
|
||||
window_list = WindowList()
|
||||
|
||||
# Test
|
||||
self.assertFR("Error: TFC settings only allow 20 members per group.", group_create, cmd_data, ts, window_list, contact_list, group_list, settings)
|
||||
|
||||
def test_full_group_list_raises_fr(self):
|
||||
# Setup
|
||||
ts = datetime.datetime.now()
|
||||
cmd_data = US_BYTE.join([b'testgroup_21', b'contact_21@jabber.org'])
|
||||
group_list = GroupList(groups=["testgroup_{}".format(n) for n in range(20)])
|
||||
contact_list = ContactList(nicks=['Alice'])
|
||||
settings = Settings()
|
||||
window_list = WindowList()
|
||||
|
||||
# Test
|
||||
self.assertFR("Error: TFC settings only allow 20 groups.", group_create, cmd_data, ts, window_list, contact_list, group_list, settings)
|
||||
|
||||
def test_successful_group_creation(self):
|
||||
# Setup
|
||||
ts = datetime.datetime.now()
|
||||
group_list = GroupList(groups=['testgroup'])
|
||||
cmd_data = US_BYTE.join([b'testgroup_2', b'bob@jabber.org'])
|
||||
contact_list = ContactList(nicks=['Alice', 'Bob'])
|
||||
settings = Settings()
|
||||
window_list = WindowList(nicks =['Alice', 'Bob'],
|
||||
contact_list=contact_list,
|
||||
group_lis =group_list,
|
||||
packet_list =None,
|
||||
settings =Settings)
|
||||
# Test
|
||||
self.assertIsNone(group_create(cmd_data, ts, window_list, contact_list, group_list, settings))
|
||||
|
||||
class TestGroupAddMember(TFCTestCase):
|
||||
|
||||
def test_too_large_final_member_list_raises_fr(self):
|
||||
# Setup
|
||||
ts = datetime.datetime.now()
|
||||
group_list = GroupList(groups=['testgroup'])
|
||||
contact_list = ContactList(nicks=["contact_{}".format(n) for n in range(21)])
|
||||
group = group_list.get_group('testgroup')
|
||||
group.members = contact_list.contacts[:20]
|
||||
settings = Settings()
|
||||
cmd_data = US_BYTE.join([b'testgroup', b'contact_20@jabber.org'])
|
||||
window_list = WindowList()
|
||||
|
||||
# Test
|
||||
self.assertFR("Error: TFC settings only allow 20 members per group.", group_add_member, cmd_data, ts, window_list, contact_list, group_list, settings)
|
||||
|
||||
def test_successful_group_add(self):
|
||||
# Setup
|
||||
ts = datetime.datetime.now()
|
||||
contact_list = ContactList(nicks=["contact_{}".format(n) for n in range(21)])
|
||||
group_list = GroupList(groups=['testgroup'])
|
||||
group = group_list.get_group('testgroup')
|
||||
group.members = contact_list.contacts[:19]
|
||||
settings = Settings()
|
||||
cmd_data = US_BYTE.join([b'testgroup', b'contact_20@jabber.org'])
|
||||
window_list = WindowList()
|
||||
|
||||
# Test
|
||||
self.assertIsNone(group_add_member(cmd_data, ts, window_list, contact_list, group_list, settings))
|
||||
|
||||
group2 = group_list.get_group('testgroup')
|
||||
self.assertEqual(len(group2), 20)
|
||||
|
||||
for c in group2:
|
||||
self.assertIsInstance(c, Contact)
|
||||
|
||||
|
||||
class TestGroupRMMember(unittest.TestCase):
|
||||
|
||||
def test_function(self):
|
||||
# Setup
|
||||
cmd_data = US_BYTE.join([b'testgroup', b'contact_18@jabber.org', b'contact_20@jabber.org'])
|
||||
ts = datetime.datetime.now()
|
||||
window_list = WindowList()
|
||||
contact_list = ContactList(nicks=["contact_{}".format(n) for n in range(21)])
|
||||
group_list = GroupList(groups=['testgroup'])
|
||||
group = group_list.get_group('testgroup')
|
||||
group.members = contact_list.contacts[:19]
|
||||
|
||||
# Test
|
||||
self.assertIsNone(group_rm_member(cmd_data, ts, window_list, contact_list, group_list))
|
||||
|
||||
members = [c.rx_account for c in group.members]
|
||||
self.assertFalse(b'contact@jabber.org' in members)
|
||||
|
||||
|
||||
class TestRemoveGroup(TFCTestCase):
|
||||
|
||||
def test_missing_group_raises_fr(self):
|
||||
# Setup
|
||||
cmd_data = b'testgroup_2'
|
||||
ts = datetime.datetime.now()
|
||||
window_list = WindowList()
|
||||
group_list = GroupList(groups=['testgroup'])
|
||||
|
||||
# Test
|
||||
self.assertFR("RxM has no group testgroup_2 to remove.", remove_group, cmd_data, ts, window_list, group_list)
|
||||
|
||||
def test_successful_remove(self):
|
||||
# Setup
|
||||
cmd_data = b'testgroup'
|
||||
ts = datetime.datetime.now()
|
||||
window_list = WindowList()
|
||||
group_list = GroupList(groups=['testgroup'])
|
||||
|
||||
# Test
|
||||
self.assertIsNone(remove_group(cmd_data, ts, window_list, group_list))
|
||||
self.assertEqual(len(group_list.groups), 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,229 @@
|
|||
#!/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 base64
|
||||
import builtins
|
||||
import datetime
|
||||
import os
|
||||
import shutil
|
||||
import unittest
|
||||
import zlib
|
||||
|
||||
from src.common.crypto import encrypt_and_sign
|
||||
from src.common.encoding import b58encode, str_to_bytes
|
||||
from src.common.statics import *
|
||||
from src.rx.files import store_unique, process_imported_file, process_received_file
|
||||
from tests.mock_classes import WindowList
|
||||
from tests.utils import TFCTestCase
|
||||
|
||||
|
||||
class TestStoreUnique(unittest.TestCase):
|
||||
|
||||
def test_function(self):
|
||||
# Setup
|
||||
f_data = os.urandom(100)
|
||||
f_dir = 'test_dir'
|
||||
f_name = 'test_file'
|
||||
|
||||
# Test
|
||||
self.assertEqual(store_unique(f_data, f_dir, f_name), 'test_file')
|
||||
self.assertEqual(store_unique(f_data, f_dir, f_name), 'test_file.1')
|
||||
self.assertEqual(store_unique(f_data, f_dir, f_name), 'test_file.2')
|
||||
|
||||
# Teardown
|
||||
shutil.rmtree('test_dir/')
|
||||
|
||||
|
||||
class TestProcessImportedFile(TFCTestCase):
|
||||
|
||||
def test_invalid_compression_raises_fr(self):
|
||||
# Setup
|
||||
data = os.urandom(1000)
|
||||
compressed = zlib.compress(data, level=9)
|
||||
compressed = compressed[:-2] + b'aa'
|
||||
key = os.urandom(32)
|
||||
key_b58 = b58encode(key)
|
||||
packet = IMPORTED_FILE_CT_HEADER + encrypt_and_sign(compressed, key)
|
||||
ts = datetime.datetime.now()
|
||||
window_list = WindowList()
|
||||
|
||||
o_input = builtins.input
|
||||
input_list = ['bad', key_b58]
|
||||
gen = iter(input_list)
|
||||
|
||||
def mock_input(_):
|
||||
return str(next(gen))
|
||||
|
||||
builtins.input = mock_input
|
||||
|
||||
# Test
|
||||
self.assertFR("Decompression of file data failed.", process_imported_file, ts, packet, window_list)
|
||||
|
||||
# Teardown
|
||||
builtins.input = o_input
|
||||
|
||||
|
||||
def test_invalid_name_raises_fr(self):
|
||||
# Setup
|
||||
file_name = str_to_bytes('\x01testfile.txt')
|
||||
data = file_name + os.urandom(1000)
|
||||
compressed = zlib.compress(data, level=9)
|
||||
key = os.urandom(32)
|
||||
key_b58 = b58encode(key)
|
||||
packet = IMPORTED_FILE_CT_HEADER + encrypt_and_sign(compressed, key)
|
||||
ts = datetime.datetime.now()
|
||||
window_list = WindowList(nicks=['local'])
|
||||
o_input = builtins.input
|
||||
input_list = ['2QJL5gVSPEjMTaxWPfYkzG9UJxzZDNSx6PPeVWdzS5CFN7knZy', key_b58]
|
||||
gen = iter(input_list)
|
||||
|
||||
def mock_input(_):
|
||||
return str(next(gen))
|
||||
|
||||
builtins.input = mock_input
|
||||
|
||||
# Test
|
||||
self.assertFR("Received file had an invalid name.", process_imported_file, ts, packet, window_list)
|
||||
|
||||
# Teardown
|
||||
builtins.input = o_input
|
||||
|
||||
def test_valid_import(self):
|
||||
file_name = str_to_bytes('testfile.txt')
|
||||
data = file_name + os.urandom(1000)
|
||||
compressed = zlib.compress(data, level=9)
|
||||
key = os.urandom(32)
|
||||
key_b58 = b58encode(key)
|
||||
packet = IMPORTED_FILE_CT_HEADER + encrypt_and_sign(compressed, key)
|
||||
ts = datetime.datetime.now()
|
||||
window_list = WindowList(nicks=['local'])
|
||||
o_input = builtins.input
|
||||
input_list = ['2QJL5gVSPEjMTaxWPfYkzG9UJxzZDNSx6PPeVWdzS5CFN7knZy', key_b58]
|
||||
gen = iter(input_list)
|
||||
|
||||
def mock_input(_):
|
||||
return str(next(gen))
|
||||
|
||||
builtins.input = mock_input
|
||||
|
||||
# Setup
|
||||
self.assertIsNone(process_imported_file(ts, packet, window_list))
|
||||
self.assertTrue(os.path.isfile(f"{DIR_IMPORTED}/testfile.txt"))
|
||||
|
||||
# Teardown
|
||||
builtins.input = o_input
|
||||
shutil.rmtree(f'{DIR_IMPORTED}/')
|
||||
|
||||
|
||||
class TestProcessReceivedFile(TFCTestCase):
|
||||
|
||||
def test_invalid_structure_raises_fr(self):
|
||||
# Setup
|
||||
payload = US_BYTE.join([b'filename', b'unused', b'next is missing'])
|
||||
nick = 'Alice'
|
||||
|
||||
# Test
|
||||
self.assertFR("Received file had invalid structure.", process_received_file, payload, nick)
|
||||
|
||||
def test_invalid_name_raises_fr(self):
|
||||
# Setup
|
||||
payload = US_BYTE.join([b'\x01filename', b'unused', b'unused', b'filedata'])
|
||||
nick = 'Alice'
|
||||
|
||||
# Test
|
||||
self.assertFR("Received file had an invalid name.", process_received_file, payload, nick)
|
||||
|
||||
def test_invalid_encoding_raises_fr(self):
|
||||
# Setup
|
||||
f_data = b'\x01filedata'
|
||||
payload = US_BYTE.join([b'filename', b'unused', b'unused', f_data])
|
||||
nick = 'Alice'
|
||||
|
||||
# Test
|
||||
self.assertFR("Received file had invalid encoding.", process_received_file, payload, nick)
|
||||
|
||||
def test_invalid_key_raises_fr(self):
|
||||
# Setup
|
||||
f_data = base64.b85encode(b'filedata')
|
||||
payload = US_BYTE.join([b'filename', b'unused', b'unused', f_data])
|
||||
nick = 'Alice'
|
||||
|
||||
# Test
|
||||
self.assertFR("Received file had an invalid key.", process_received_file, payload, nick)
|
||||
|
||||
def test_decryption_fail_raises_fr(self):
|
||||
# Setup
|
||||
key = os.urandom(32)
|
||||
f_data = encrypt_and_sign(b'filedata', key)
|
||||
f_data += key[1:] + b''
|
||||
f_data = base64.b85encode(f_data)
|
||||
payload = US_BYTE.join([b'filename', b'unused', b'unused', f_data])
|
||||
nick = 'Alice'
|
||||
|
||||
# Test
|
||||
self.assertFR("Decryption of file data failed.", process_received_file, payload, nick)
|
||||
|
||||
def test_invalid_compression_raises_fr(self):
|
||||
# Setup
|
||||
key = os.urandom(32)
|
||||
compressed = zlib.compress(b'filedata', level=9)
|
||||
compressed = compressed[:-1] + b'a'
|
||||
f_data = encrypt_and_sign(compressed, key)
|
||||
f_data += key
|
||||
f_data = base64.b85encode(f_data)
|
||||
payload = US_BYTE.join([b'filename', b'unused', b'unused', f_data])
|
||||
nick = 'Alice'
|
||||
|
||||
# Test
|
||||
self.assertFR("Decompression of file data failed.", process_received_file, payload, nick)
|
||||
|
||||
def test_missing_file_data_raises_fr(self):
|
||||
# Setup
|
||||
key = os.urandom(32)
|
||||
compressed = zlib.compress(b'', level=9)
|
||||
f_data = encrypt_and_sign(compressed, key)
|
||||
f_data += key
|
||||
f_data = base64.b85encode(f_data)
|
||||
payload = US_BYTE.join([b'filename', b'unused', b'unused', f_data])
|
||||
nick = 'Alice'
|
||||
|
||||
# Test
|
||||
self.assertFR("Received file did not contain data.", process_received_file, payload, nick)
|
||||
|
||||
def test_successful_reception(self):
|
||||
# Setup
|
||||
key = os.urandom(32)
|
||||
compressed = zlib.compress(b'filedata', level=9)
|
||||
f_data = encrypt_and_sign(compressed, key)
|
||||
f_data += key
|
||||
f_data = base64.b85encode(f_data)
|
||||
payload = US_BYTE.join([b'filename', b'unused', b'unused', f_data])
|
||||
nick = 'Alice'
|
||||
|
||||
# Test
|
||||
self.assertIsNone(process_received_file(payload, nick))
|
||||
self.assertTrue(os.path.isfile(f'{DIR_RX_FILES}/Alice/filename'))
|
||||
|
||||
# Teardown
|
||||
shutil.rmtree(f'{DIR_RX_FILES}/')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
|
@ -0,0 +1,268 @@
|
|||
#!/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 builtins
|
||||
import datetime
|
||||
import getpass
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from src.common.crypto import encrypt_and_sign, argon2_kdf
|
||||
from src.common.encoding import b58encode
|
||||
from src.common.statics import *
|
||||
from src.rx.key_exchanges import process_local_key, local_key_installed, process_public_key, ecdhe_command, psk_command, psk_import
|
||||
|
||||
from tests.mock_classes import Contact, ContactList, KeyList, KeySet, Settings, WindowList
|
||||
from tests.utils import TFCTestCase
|
||||
|
||||
|
||||
class TestProcessLocalKey(TFCTestCase):
|
||||
|
||||
def test_invalid_decryption_key_raises_fr(self):
|
||||
# Setup
|
||||
packet = b''
|
||||
contact_list = ContactList()
|
||||
key_list = KeyList()
|
||||
o_input = builtins.input
|
||||
builtins.input = lambda x: '2QJL5gVSPEjMTaxWPfYkzG9UJxzZDNSx6PPeVWdzS5CFN7knZy'
|
||||
|
||||
# Test
|
||||
self.assertFR("Invalid key decryption key.", process_local_key, packet, contact_list, key_list)
|
||||
|
||||
# Teardown
|
||||
builtins.input = o_input
|
||||
|
||||
def test_successful_local_key_processing(self):
|
||||
# Setup
|
||||
conf_code = os.urandom(1)
|
||||
key = os.urandom(32)
|
||||
hek = os.urandom(32)
|
||||
kek = os.urandom(32)
|
||||
packet = LOCAL_KEY_PACKET_HEADER + encrypt_and_sign(key + hek + conf_code, key=kek)
|
||||
contact_list = ContactList()
|
||||
key_list = KeyList()
|
||||
o_input = builtins.input
|
||||
builtins.input = lambda x: b58encode(kek)
|
||||
|
||||
# Test
|
||||
self.assertIsNone(process_local_key(packet, contact_list, key_list))
|
||||
|
||||
# Teardown
|
||||
builtins.input = o_input
|
||||
|
||||
|
||||
class TestLocalKeyInstalled(unittest.TestCase):
|
||||
|
||||
def test_function(self):
|
||||
# Setup
|
||||
ts = datetime.datetime.now()
|
||||
window_list = WindowList(nicks=['local'])
|
||||
contact_list = ContactList(nicks=['local'])
|
||||
|
||||
# Test
|
||||
self.assertIsNone(local_key_installed(ts, window_list, contact_list))
|
||||
|
||||
|
||||
class TestProcessPublicKey(unittest.TestCase):
|
||||
|
||||
def test_function(self):
|
||||
# Setup
|
||||
packet = PUBLIC_KEY_PACKET_HEADER + os.urandom(32) + ORIGIN_CONTACT_HEADER + b'alice@jabber.org'
|
||||
ts = datetime.datetime.now()
|
||||
window_list = WindowList(nicks=['local'])
|
||||
settings = Settings()
|
||||
pubkey_buf = dict()
|
||||
|
||||
# Test
|
||||
self.assertIsNone(process_public_key(ts, packet, window_list, settings, pubkey_buf))
|
||||
packet = PUBLIC_KEY_PACKET_HEADER + os.urandom(32) + ORIGIN_USER_HEADER + b'alice@jabber.org'
|
||||
self.assertIsNone(process_public_key(ts, packet, window_list, settings, pubkey_buf))
|
||||
|
||||
|
||||
class TestECDHECommand(unittest.TestCase):
|
||||
|
||||
def test_function(self):
|
||||
# Setup
|
||||
packet = 32 * b'\x01' + 32 * b'\x02' \
|
||||
+ 32 * b'\x03' + 32 * b'\x04' \
|
||||
+ b'alice@jabber.org' + US_BYTE + b'Alice'
|
||||
ts = datetime.datetime.now()
|
||||
window_list = WindowList(nicks=['local'])
|
||||
settings = Settings()
|
||||
pubkey_buf = dict()
|
||||
contact_list = ContactList()
|
||||
key_list = KeyList()
|
||||
|
||||
# Test
|
||||
self.assertIsNone(ecdhe_command(packet, ts, window_list, contact_list, key_list, settings, pubkey_buf))
|
||||
keyset = key_list.get_keyset('alice@jabber.org')
|
||||
self.assertIsInstance(keyset, KeySet)
|
||||
self.assertEqual(keyset.rx_account, 'alice@jabber.org')
|
||||
self.assertEqual(keyset.tx_key, 32 * b'\x01')
|
||||
self.assertEqual(keyset.tx_hek, 32 * b'\x02')
|
||||
self.assertEqual(keyset.rx_key, 32 * b'\x03')
|
||||
self.assertEqual(keyset.rx_hek, 32 * b'\x04')
|
||||
|
||||
contact = contact_list.get_contact('alice@jabber.org')
|
||||
self.assertIsInstance(contact, Contact)
|
||||
|
||||
self.assertEqual(contact.rx_account, 'alice@jabber.org')
|
||||
self.assertEqual(contact.nick, 'Alice')
|
||||
self.assertEqual(contact.rx_fingerprint, bytes(32))
|
||||
self.assertEqual(contact.tx_fingerprint, bytes(32))
|
||||
|
||||
|
||||
class TestPSKCommand(unittest.TestCase):
|
||||
|
||||
def test_function(self):
|
||||
# Setup
|
||||
packet = 32 * b'\x01' + 32 * b'\x02' + b'alice@jabber.org' + US_BYTE + b'Alice'
|
||||
ts = datetime.datetime.now()
|
||||
window_list = WindowList(nicks=['local'])
|
||||
settings = Settings()
|
||||
pubkey_buf = dict()
|
||||
contact_list = ContactList()
|
||||
key_list = KeyList()
|
||||
|
||||
# Test
|
||||
self.assertIsNone(psk_command(packet, ts, window_list, contact_list, key_list, settings, pubkey_buf))
|
||||
|
||||
keyset = key_list.get_keyset('alice@jabber.org')
|
||||
self.assertIsInstance(keyset, KeySet)
|
||||
self.assertEqual(keyset.rx_account, 'alice@jabber.org')
|
||||
self.assertEqual(keyset.tx_key, 32 * b'\x01')
|
||||
self.assertEqual(keyset.tx_hek, 32 * b'\x02')
|
||||
self.assertEqual(keyset.rx_key, bytes(32))
|
||||
self.assertEqual(keyset.rx_hek, bytes(32))
|
||||
|
||||
contact = contact_list.get_contact('alice@jabber.org')
|
||||
self.assertIsInstance(contact, Contact)
|
||||
|
||||
self.assertEqual(contact.rx_account, 'alice@jabber.org')
|
||||
self.assertEqual(contact.nick, 'Alice')
|
||||
self.assertEqual(contact.rx_fingerprint, bytes(32))
|
||||
self.assertEqual(contact.tx_fingerprint, bytes(32))
|
||||
|
||||
|
||||
class TestPSKImport(TFCTestCase):
|
||||
|
||||
def test_unknown_account_raises_fr(self):
|
||||
# Setup
|
||||
packet = b'alice@jabber.org'
|
||||
contact_list = ContactList()
|
||||
|
||||
# Test
|
||||
self.assertFR("Unknown account alice@jabber.org.", psk_import, packet, None, None, contact_list, None, None)
|
||||
|
||||
def test_invalid_psk_data_raises_fr(self):
|
||||
# Setup
|
||||
packet = b'alice@jabber.org'
|
||||
contact_list = ContactList(nicks=['Alice'])
|
||||
settings = Settings(disable_gui_dialog=True)
|
||||
o_input = builtins.input
|
||||
builtins.input = lambda x: 'ut_psk'
|
||||
|
||||
with open('ut_psk', 'wb+') as f:
|
||||
f.write(os.urandom(135))
|
||||
|
||||
# Test
|
||||
self.assertFR("Invalid PSK data in file.", psk_import, packet, None, None, contact_list, None, settings)
|
||||
|
||||
# Teardown
|
||||
builtins.input = o_input
|
||||
|
||||
def test_invalid_keys_raise_fr(self):
|
||||
# Setup
|
||||
packet = b'alice@jabber.org'
|
||||
contact_list = ContactList(nicks=['Alice', 'local'])
|
||||
key_list = KeyList(nicks=['Alice', 'local'])
|
||||
keyset = key_list.get_keyset('alice@jabber.org')
|
||||
keyset.rx_key = bytes(32)
|
||||
keyset.rx_hek = bytes(32)
|
||||
window_list = WindowList(nicks=['Alice', 'local'])
|
||||
ts = datetime.datetime.now()
|
||||
settings = Settings(disable_gui_dialog=True)
|
||||
o_input = builtins.input
|
||||
o_getpass = getpass.getpass
|
||||
builtins.input = lambda x: 'ut_psk'
|
||||
input_list = ['bad', 'testpassword']
|
||||
gen = iter(input_list)
|
||||
|
||||
def mock_input(_):
|
||||
return str(next(gen))
|
||||
|
||||
getpass.getpass = mock_input
|
||||
password = 'testpassword'
|
||||
salt = os.urandom(32)
|
||||
rx_key = bytes(32)
|
||||
rx_hek = os.urandom(32)
|
||||
kek, _ = argon2_kdf(password, salt, rounds=16, memory=128000, parallelism=1)
|
||||
ct_tag = encrypt_and_sign(rx_key + rx_hek, key=kek)
|
||||
|
||||
with open('ut_psk', 'wb+') as f:
|
||||
f.write(salt + ct_tag)
|
||||
|
||||
# Test
|
||||
self.assertFR("Keys from contact are not valid.", psk_import, packet, ts, window_list, contact_list, key_list, settings)
|
||||
|
||||
# Teardown
|
||||
os.remove('ut_psk')
|
||||
builtins.input = o_input
|
||||
getpass.getpass = o_getpass
|
||||
|
||||
def test_valid_psk(self):
|
||||
# Setup
|
||||
packet = b'alice@jabber.org'
|
||||
contact_list = ContactList(nicks=['Alice', 'local'])
|
||||
key_list = KeyList(nicks=['Alice', 'local'])
|
||||
keyset = key_list.get_keyset('alice@jabber.org')
|
||||
keyset.rx_key = bytes(32)
|
||||
keyset.rx_hek = bytes(32)
|
||||
window_list = WindowList(nicks=['Alice', 'local'])
|
||||
ts = datetime.datetime.now()
|
||||
settings = Settings(disable_gui_dialog=True)
|
||||
o_input = builtins.input
|
||||
o_getpass = getpass.getpass
|
||||
builtins.input = lambda x: 'ut_psk'
|
||||
getpass.getpass = lambda x: 'testpassword'
|
||||
password = 'testpassword'
|
||||
salt = os.urandom(32)
|
||||
rx_key = os.urandom(32)
|
||||
rx_hek = os.urandom(32)
|
||||
kek, _ = argon2_kdf(password, salt, rounds=16, memory=128000, parallelism=1)
|
||||
ct_tag = encrypt_and_sign(rx_key + rx_hek, key=kek)
|
||||
|
||||
with open('ut_psk', 'wb+') as f:
|
||||
f.write(salt + ct_tag)
|
||||
|
||||
# Test
|
||||
self.assertTrue(os.path.isfile('ut_psk'))
|
||||
self.assertIsNone(psk_import(packet, ts, window_list, contact_list, key_list, settings))
|
||||
self.assertFalse(os.path.isfile('ut_psk'))
|
||||
self.assertEqual(keyset.rx_key, rx_key)
|
||||
self.assertEqual(keyset.rx_hek, rx_hek)
|
||||
|
||||
# Teardown
|
||||
builtins.input = o_input
|
||||
getpass.getpass = o_getpass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(exit=False)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue