This commit is contained in:
Markus Ottela 2017-04-10 04:00:10 +03:00
parent 9b9559ee2a
commit 29f285b1f4
120 changed files with 18175 additions and 23593 deletions

14
.travis.yml Normal file
View File

@ -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

1318
LICENSE.md Executable file → Normal file

File diff suppressed because it is too large Load Diff

1825
NH.py

File diff suppressed because it is too large Load Diff

View File

@ -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>
&nbsp;&nbsp;&nbsp;&nbsp;[Security design](https://github.com/maqp/tfc/wiki/Security-design)<br>
&nbsp;&nbsp;&nbsp;&nbsp;[Protocol](https://github.com/maqp/tfc/wiki/Protocol)<br>
Hardware<br>
&nbsp;&nbsp;&nbsp;&nbsp;[Hardware configurations](https://github.com/maqp/tfc/wiki/Hardware-configurations)<br>
&nbsp;&nbsp;&nbsp;&nbsp;[Data diode (perfboard)](https://github.com/maqp/tfc/wiki/Data-Diode-(perfboard))<br>
&nbsp;&nbsp;&nbsp;&nbsp;[Data diode (point to point)](https://github.com/maqp/tfc/wiki/Data-diode-(point-to-point))<br>
&nbsp;&nbsp;&nbsp;&nbsp;[HWRNG (perfboard)](https://github.com/maqp/tfc/wiki/HWRNG-(perfboard))<br>
&nbsp;&nbsp;&nbsp;&nbsp;[HWRNG (breadboard)](https://github.com/maqp/tfc/wiki/HWRNG-(breadboard))<br>
Hardware<Br>
&nbsp;&nbsp;&nbsp;&nbsp;[Data diode (breadboard)](https://github.com/maqp/tfc/wiki/TTL-Data-Diode-(breadboard))<br>
Software<Br>
&nbsp;&nbsp;&nbsp;&nbsp;[Installation](https://github.com/maqp/tfc/wiki/Installation)<br>
&nbsp;&nbsp;&nbsp;&nbsp;[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>

4574
Rx.py

File diff suppressed because it is too large Load Diff

5020
Tx.py

File diff suppressed because it is too large Load Diff

317
dd.py Executable file → Normal file
View File

@ -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
View File

@ -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()

366
install.sh Normal file
View File

@ -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

17
install.sh.asc Normal file
View File

@ -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-----

8
launchers/TFC-DD-LR.desktop Executable file
View File

@ -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;

8
launchers/TFC-DD-RL.desktop Executable file
View File

@ -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;

8
launchers/TFC-LR.desktop Executable file
View File

@ -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;

7
launchers/TFC-NH-Tails.desktop Executable file
View File

@ -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;

7
launchers/TFC-NH.desktop Executable file
View File

@ -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;

8
launchers/TFC-RL.desktop Executable file
View File

@ -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;

7
launchers/TFC-RxM.desktop Executable file
View File

@ -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;

7
launchers/TFC-TxM.desktop Executable file
View File

@ -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;

248
launchers/config Normal file
View File

@ -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

76
nh.py Normal file
View File

@ -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()

1
requirements-nh.txt Normal file
View File

@ -0,0 +1 @@
pyserial==3.3 --hash=sha512:19545d2121e0f43aba7e423df94c6836aba037b86500e2cd9b59555c80fb8596950329833b6d7aeb12a5d3b8e6639a14d64df984bf4e78a1f646364a05e08e4c

9
requirements.txt Normal file
View File

@ -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

17
sender.py Normal file
View File

@ -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)

1866
setup.py

File diff suppressed because it is too large Load Diff

View File

@ -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
src/__init__.py Executable file
View File

0
src/common/__init__.py Executable file
View File

279
src/common/crypto.py Executable file
View File

@ -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")

249
src/common/db_contacts.py Executable file
View File

@ -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')

318
src/common/db_groups.py Executable file
View File

@ -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')

212
src/common/db_keys.py Normal file
View File

@ -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.")

295
src/common/db_logs.py Normal file
View File

@ -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)

121
src/common/db_masterkey.py Executable file
View File

@ -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)

322
src/common/db_settings.py Executable file
View File

@ -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')

131
src/common/encoding.py Executable file
View File

@ -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'))

63
src/common/errors.py Executable file
View File

@ -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()

178
src/common/gateway.py Normal file
View File

@ -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))

211
src/common/input.py Normal file
View File

@ -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

244
src/common/misc.py Executable file
View File

@ -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, ''

225
src/common/output.py Normal file
View File

@ -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)

183
src/common/path.py Normal file
View File

@ -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.")

464
src/common/reed_solomon.py Executable file
View File

@ -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

238
src/common/statics.py Normal file
View File

@ -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
src/nh/__init__.py Normal file
View File

109
src/nh/commands.py Normal file
View File

@ -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

170
src/nh/gateway.py Normal file
View File

@ -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))

241
src/nh/misc.py Normal file
View File

@ -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

180
src/nh/pidgin.py Normal file
View File

@ -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

108
src/nh/settings.py Normal file
View File

@ -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)

102
src/nh/tcb.py Normal file
View File

@ -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
src/rx/__init__.py Executable file
View File

321
src/rx/commands.py Normal file
View File

@ -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)

161
src/rx/commands_g.py Normal file
View File

@ -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 )

144
src/rx/files.py Normal file
View File

@ -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)])

261
src/rx/key_exchanges.py Normal file
View File

@ -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)

187
src/rx/messages.py Normal file
View File

@ -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)

299
src/rx/packet.py Normal file
View File

@ -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_))

62
src/rx/receiver_loop.py Executable file
View File

@ -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

123
src/rx/rx_loop.py Executable file
View File

@ -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

288
src/rx/windows.py Normal file
View File

@ -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
src/tx/__init__.py Executable file
View File

484
src/tx/commands.py Executable file
View File

@ -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)

291
src/tx/commands_g.py Normal file
View File

@ -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('')

227
src/tx/contact.py Normal file
View File

@ -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('')

250
src/tx/files.py Executable file
View File

@ -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))

343
src/tx/key_exchanges.py Normal file
View File

@ -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)

98
src/tx/messages.py Normal file
View File

@ -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))

185
src/tx/packet.py Executable file
View File

@ -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)

160
src/tx/sender_loop.py Executable file
View File

@ -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

86
src/tx/trickle.py Executable file
View File

@ -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

95
src/tx/tx_loop.py Executable file
View File

@ -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

96
src/tx/user_input.py Executable file
View File

@ -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)

152
src/tx/windows.py Executable file
View File

@ -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
tests/__init__.py Normal file
View File

0
tests/common/__init__.py Executable file
View File

267
tests/common/test_crypto.py Normal file
View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

120
tests/common/test_input.py Normal file
View File

@ -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)

161
tests/common/test_misc.py Normal file
View File

@ -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)

View File

@ -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)

66
tests/common/test_path.py Normal file
View File

@ -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)

View File

@ -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)

627
tests/mock_classes.py Normal file
View File

@ -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
tests/nh/__init__.py Executable file
View File

43
tests/nh/test_gateway.py Normal file
View File

@ -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)

117
tests/nh/test_misc.py Normal file
View File

@ -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)

72
tests/nh/test_settings.py Normal file
View File

@ -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
tests/rx/__init__.py Executable file
View File

313
tests/rx/test_commands.py Normal file
View File

@ -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)

156
tests/rx/test_commands_g.py Normal file
View File

@ -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)

229
tests/rx/test_files.py Normal file
View File

@ -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)

View File

@ -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