This commit is contained in:
Markus Ottela 2016-01-27 11:54:20 +02:00
commit 9f571bdb74
12 changed files with 15554 additions and 0 deletions

260
LICENCE.md Normal file
View File

@ -0,0 +1,260 @@
#Licences
### C-programs
Copyrights of the design schematic for HW RNG belong to Giorgio Vazzana and
are modified and published under GNU free license and written permission of the
author.
### Data diode
Copyrights for the design schematics of the RS-232 Data Diode belong to
Douglas W. Jones and are published used under GNU free license and used in
this document accordingly.
### TFC Documents
Both the white paper and manual are released under GNU Free Documentation
License 1.3
### TFC Suite
Tx.py, Rx.py, NH.py, test_tx.py, test_nh.py, test_rx.py, hwrng.py, setup.py
and dd.py are part of the TFC application, which is free software: You can
redistribute it and/or modify it under the terms of the GNU General Public
Licence as published by the Free Software Foundation, either version 3 of the
Licence, or apart from specified sections[1], (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 Licence for more details. For a
copy of the GNU General Public License, see http://www.gnu.org/licenses/
[1] print_banner() style 3 in Tx.py, Rx.py and NH.py are based on matrix curses
by Tom Wallroth. This code is used, modified and published under GNU GPL v2
licence.
##PyNaCl
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
###NOTICES
Since making changes to crypto libraries is generally not a good idea,
here is documentation that explains why each change was made and how
they do not weaken security.
https://github.com/maqp/pynacl/commit/
a89b862a703353fd536951eea574ef4305030a11#diff-ac3b39a5ab4db5864edbe34708be717d
Added a getter shared_key() to extract the ECDHE shared secret after key
exchange; This allows Tx.py to generate two separate symmetric keys by
generating them using PBKDF2-HMAC-SHA256, each key salted with a different
public key. While nonce-based crypto would be secure with two parties
using the same key, it would have no forward secrecy. TFC generates
forward secrecy with cyclic hashing of key through PBKDF2-HMAC-SHA256
between every message. Using only one key would mean any offset in state
of key would lead to retrospective decryption of messages between the
two states, if end points were physically compromised.
TFC has always included an option for Raspberry Pi and HWRNG generated keys.
The edit to PrivateKey.generate() allows user to mix in entropy from the
HWRNG during private key generation. External entropy is XORed with standard
entropy of the library -- it has no adverse effects and can only add entropy
to keys.
Simple proof of this is assume the external entropy is malicious. It could
try to generate a keystream based on nacl.utils.random(), aiming to produce
a final output that is a bit string of zeroes. This is not possible as
external entropy is loaded before PyNaCl's CSPRNG random() is called.
Alternatively, the external entropy can be malicious, predictable bit
sequence that provides no additional entropy. This is in no way different
from XORing the random() function's output with arbitrary number of times
with any completely predictable string, e.g. a bit-string of 0s, that has
no effect on entropy random() returns. Therefore, the external entropy can
not weaken the overall security.
This property is used when no external entropy is provided. In those cases,
the output random() is in fact XORed with bit-string of zeroes, and it will
lead to exact same output as that of random(). Since constant time XOR
operation is performed for every generation time, it introduces practically
no timing attacks. Also, in the case of TFC, adversary is unable to exploit
TxM behind data diode, and obtain information about when the key generation
started. Instead, they will first learn about the key generation when TxM
outputs the public key to contact.

988
NH.py Normal file
View File

@ -0,0 +1,988 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# TFC-NaCl 0.16.01 beta || NH.py
"""
GPL License
This software is part of the TFC application, which 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. For
a copy of the GNU General Public License, see <http://www.gnu.org/licenses/>.
"""
from argparse import ArgumentParser
from binascii import hexlify
from datetime import datetime
from dbus import Interface, SessionBus
from dbus.exceptions import DBusException
from dbus.mainloop.qt import DBusQtMainLoop
from fcntl import ioctl
from multiprocessing import Process, Queue
from multiprocessing.connection import Client, Listener
from os import listdir, system
from random import choice, randrange, randint as sysrandint
from serial import Serial, serialutil
from struct import unpack
from sys import argv, stdout
from termios import TIOCGWINSZ
from time import sleep
import curses
from hashlib import sha256
from PyQt4.QtGui import QApplication
str_version = "0.16.01 beta"
int_version = 1601
###############################################################################
# CONFIGURATION #
###############################################################################
# UI settings
t_fmt = "%m-%d / %H:%M:%S" # Format of timestamps when displaying messages
startup_banner = True # False disables the animated startup banner
# Local Testing
local_testing = False # True enables testing of TFC on a single computer
dd_sockets = False # True changes sockets for data diode simulator
output_pidgin = True # False stops sending messages to Pidgin
# Serial port settings
baud_rate = 9600 # The serial interface speed
checksum_len = 8 # Data diode error detection rate. 8 hex = 32-bit
txm_usb_adapter = True # False = Use integrated serial interface for TxM
rxm_usb_adapter = True # False = Use integrated serial interface for RxM
###############################################################################
# ERROR CLASSES #
###############################################################################
class CriticalError(Exception):
def __init__(self, function_name, error_message):
system("clear")
print("\nERROR: M(%s): %s\n" % (function_name, error_message))
graceful_exit()
class FunctionParameterTypeError(Exception):
def __init__(self, function_name):
system("clear")
print("\nERROR: M(%s): Wrong input type.\n" % function_name)
graceful_exit()
###############################################################################
# HELPERS #
###############################################################################
def phase(string, dist):
"""
Print name of next phase. Next message (about completion), printed after
the phase will be printed on same line as the name specified by 'string'.
:param string: String to be printed.
:param dist: Indentation of completion message.
:return: None
"""
if not isinstance(string, str) or not isinstance(dist, (int, long)):
raise FunctionParameterTypeError("phase")
stdout.write(string + ((dist - len(string)) * ' '))
stdout.flush()
sleep(0.02)
return None
def sha2_256(message):
"""
Generate SHA256 digest from message.
:param message: Input to hash function.
:return: Hex representation of SHA256 digest.
"""
if not isinstance(message, str):
raise FunctionParameterTypeError("sha2_256")
h_function = sha256()
h_function.update(message)
hex_digest = hexlify(h_function.digest())
return hex_digest
def verify_checksum(packet):
"""
Detect transmission errors by verifying SHA256-based checksum.
:param packet: Packet to calculate checksum for.
:return: True if checksum was correct, else False.
"""
if not isinstance(packet, str):
raise FunctionParameterTypeError("verify_checksum")
chksum_pckt = packet[-checksum_len:]
separated_p = packet[:-(checksum_len + 1)]
chksum_calc = sha2_256(separated_p)[:checksum_len]
if chksum_calc == chksum_pckt:
return True
else:
print("\nChecksum error: Command / message was discarded.\n"
"If error persists, check TxM data diode batteries.\n")
return False
def clean_exit(message=''):
"""
Print exit message and close program.
:param message: Message to print.
:return: [no return value]
"""
if not isinstance(message, str):
raise FunctionParameterTypeError("clean_exit")
system("clear")
if message:
print("\n%s\n" % message)
print("\nExiting TFC-NaCl.\n")
exit()
def graceful_exit():
"""
Display a message and exit Tx.py.
If trickle connection is enabled, output a message to
exit_queue so main loop can kill processes and exit.
:return: None
"""
if unittesting:
raise SystemExit
else:
exit_queue.put("exit")
sleep(1)
def get_tty_wh():
"""
Get width and height of terminal.
:return: Width and height of terminal.
"""
def ioctl_gwin_size(fd):
"""
No definition.
:param fd: [no definition]
:return: [no definition]
"""
return unpack("hh", ioctl(fd, TIOCGWINSZ, "1234"))
cr = ioctl_gwin_size(0) or ioctl_gwin_size(1) or ioctl_gwin_size(2)
return int(cr[1]), int(cr[0])
def print_banner():
"""
Print animated startup banner.
Style 3:
Matrix-Curses - See how deep the rabbit hole goes.
Copyright (c) 2012 Tom Wallroth
http://github.com/devsnd/matrix-curses/
Used and modified under GNU GPL version 3
:return: None
"""
string = "Tinfoil Chat NaCl %s" % str_version
system("clear")
width, height = get_tty_wh()
print((height / 2) - 1) * '\n'
# Style 1
animation = sysrandint(1, 3)
if animation == 1:
i = 0
while i <= len(string):
stdout.write("\x1b[1A" + ' ')
stdout.flush()
if i == len(string):
print((width - len(string)) / 2) * ' ' + string[:i]
else:
rc = chr(randrange(32, 126))
print((width - len(string)) / 2) * ' ' + string[:i] + rc
i += 1
sleep(0.03)
# Style 2
if animation == 2:
char_l = len(string) * ['']
while True:
stdout.write("\x1b[1A" + ' ')
stdout.flush()
st = ''
for i in range(len(string)):
if char_l[i] != string[i]:
char_l[i] = chr(randrange(32, 126))
else:
char_l[i] = string[i]
st += char_l[i]
print((width - len(string)) / 2) * ' ' + st
sleep(0.004)
if st == string:
break
# Style 3
if animation == 3:
string = "Tinfoil Chat NaCl 0.16.1"
dropping_chars = 50
random_cleanup = 80
min_speed = 3
max_speed = 7
sleep_ms = 0.005
scroll_chars = ''
for a in range(32, 126):
scroll_chars += chr(a)
class FChar(object):
list_chr = list(scroll_chars)
normal_attr = curses.A_NORMAL
highlight_attr = curses.A_REVERSE
def __init__(self, o_width, speed_min, speed_max):
self.x = 0
self.y = 0
self.speed = 1
self.char = ' '
self.offset = randint(0, self.speed)
self.reset(o_width, speed_min, speed_max)
self.completed = []
def reset(self, c_width, speed_min, speed_max):
self.char = choice(FChar.list_chr)
self.x = randint(1, c_width - 1)
self.y = 0
self.speed = randint(speed_min, speed_max)
self.offset = randint(0, self.speed)
def get_completed(self):
return self.completed
def tick(self, scr, steps):
win_h, win_w = scr.getmaxyx()
if self.advances(steps):
# If window was re-sized and char is out of bounds, reset
self.out_of_bounds_reset(win_w, win_h)
# Make previous char curses.A_NORMAL
scr.addstr(self.y, self.x, self.char, curses.A_NORMAL)
# Choose new char and draw it A_NORMAL if not out of bounds
self.y += 1
if self.y == win_h / 2:
indent_len = (win_w - len(string)) / 2
prepended_ind = (indent_len * ' ')
final_string = prepended_ind + string + ' '
if self.x > indent_len - 1:
try:
self.char = final_string[self.x]
self.completed.append(self.x)
except IndexError:
self.char = choice(FChar.list_chr)
else:
self.char = choice(FChar.list_chr)
if not self.out_of_bounds_reset(win_w, win_h):
scr.addstr(self.y, self.x, self.char, curses.A_NORMAL)
def out_of_bounds_reset(self, win_w, win_h):
if self.x > win_w - 2:
self.reset(win_w, min_speed, max_speed)
return True
if self.y > win_h - 2:
self.reset(win_w, min_speed, max_speed)
return True
return False
def advances(self, steps):
if steps % (self.speed + self.offset) == 0:
return True
return False
# Use insecure but fast PRNG
def rand():
p = sysrandint(0, 1000000000)
while True:
p ^= (p << 21) & 0xffffffffffffffff
p ^= (p >> 35)
p ^= (p << 4) & 0xffffffffffffffff
yield p
def randint(_min, _max):
n = r.next()
return (n % (_max - _min)) + _min
def main():
steps = 0
scr = curses.initscr()
scr.nodelay(1)
curses.curs_set(0)
curses.noecho()
win_h, win_w = scr.getmaxyx()
if win_w < len(string):
raise KeyboardInterrupt
window_animation = None
lines = []
for _ in range(dropping_chars):
fc = FChar(win_w, min_speed, max_speed)
fc.y = randint(0, win_h - 2)
lines.append(fc)
scr.refresh()
completion = []
delay = 0
while True:
win_h, win_w = scr.getmaxyx()
for line in lines:
line.tick(scr, steps)
completed = line.get_completed()
for c in completed:
if c not in completion:
completion.append(c)
if len(completion) >= len(string):
if delay > 600:
raise KeyboardInterrupt
else:
delay += 1
for _ in range(random_cleanup):
x = randint(0, win_w - 1)
y = randint(0, win_h - 1)
indent_len = (win_w - len(string)) / 2
prepended_ind = (indent_len * ' ')
if y == win_h / 2:
if x < len(prepended_ind):
scr.addstr(y, x, ' ')
if x > len(prepended_ind + string):
scr.addstr(y, x, ' ')
else:
scr.addstr(y, x, ' ')
if window_animation is not None:
if not window_animation.tick(scr, steps):
window_animation = None
scr.refresh()
sleep(sleep_ms)
steps += 1
try:
r = rand()
main()
except KeyboardInterrupt:
curses.endwin()
curses.curs_set(1)
curses.reset_shell_mode()
curses.echo()
system("clear")
sleep(0.3)
system("clear")
return None
def get_serial_interfaces():
"""
Depending on NH.py settings, determine correct serial interfaces.
:return: tx_if: Serial interface that connects to TxM.
rx_if: Serial interface that connects to RxM.
"""
dev_files = [df for df in listdir("/dev/")]
dev_files.sort()
tx_if = ''
rx_if = ''
if txm_usb_adapter and not rxm_usb_adapter:
adapters = []
for dev_file in dev_files:
if dev_file.startswith("ttyUSB"):
adapters.append(dev_file)
break
tx_if = "/dev/%s" % adapters[0]
rx_if = "/dev/ttyS0"
if rxm_usb_adapter and not txm_usb_adapter:
adapters = []
for dev_file in dev_files:
if dev_file.startswith("ttyUSB"):
adapters.append(dev_file)
break
tx_if = "/dev/ttyS0"
rx_if = "/dev/%s" % adapters[0]
if txm_usb_adapter and rxm_usb_adapter:
adapters = []
for dev_file in dev_files:
if dev_file.startswith("ttyUSB"):
adapters.append(dev_file)
adapters.sort()
if not adapters:
clean_exit("Error: No USB-serial adapters were not found.")
if len(adapters) < 2:
clean_exit("Error: Settings require two USB-serial adapters.")
tx_if = "/dev/%s" % adapters[0]
rx_if = "/dev/%s" % adapters[1]
if not txm_usb_adapter and not rxm_usb_adapter:
s0_found = False
s1_found = False
for dev_file in dev_files:
if dev_file.startswith("ttyS0"):
s0_found = True
if dev_file.startswith("ttyS1"):
s1_found = True
if s0_found:
tx_if = "/dev/ttyS0"
else:
clean_exit("Error: /dev/ttyS0 not found.")
if s1_found:
rx_if = "/dev/ttyS1"
else:
clean_exit("Error: /dev/ttyS1 not found.")
return tx_if, rx_if
###############################################################################
# RECEIVER #
###############################################################################
def dbus_receiver():
"""
Start Qt loop that loads messages from Pidgin.
:return: [no return value]
"""
DBusQtMainLoop(set_as_default=True)
bus = SessionBus()
bus.add_signal_receiver(pidgin_to_rxm_queue,
dbus_interface="im.pidgin.purple.PurpleInterface",
signal_name="ReceivedImMsg")
def pidgin_to_rxm_queue(account, sender, message, conversation, flags):
"""
Load message from Pidgin. Put it to queue.
:param account: Account ID.
:param sender: Sender account address.
:param message: Message from sender.
:param conversation: Conversation ID.
:param flags: Flags.
:return: [no return value]
"""
# Clear PEP8 warning
test = False
if test:
print(account, conversation, flags)
sender = sender.split('/')[0]
tstamp = datetime.now().strftime(t_fmt)
if message.startswith("TFC|N|%s|P|" % int_version):
to_rxm = str("%s|rx.%s" % (message, sender)) # Unicode to string conv.
print("%s - pub key %s > RxM" % (tstamp, sender))
packet_to_rxm.put(to_rxm)
if message.startswith("TFC|N|%s|M|" % int_version):
to_rxm = str("%s|rx.%s" % (message, sender))
print("%s - message %s > RxM" % (tstamp, sender))
packet_to_rxm.put(to_rxm)
def pidgin_receiver_process():
"""
Start QApplication as a separate process.
:return: [no return value]
"""
try:
app = QApplication(argv)
dbus_receiver()
app.exec_()
except DBusException:
pass
###############################################################################
# OTHER #
###############################################################################
def header_printer_process():
"""
Print NH.py headers.
:return: [no return value]
"""
try:
bus = SessionBus()
obj = bus.get_object("im.pidgin.purple.PurpleService",
"/im/pidgin/purple/PurpleObject")
purple = Interface(obj, "im.pidgin.purple.PurpleInterface")
active = purple.PurpleAccountsGetAllActive()[0]
acco_u = purple.PurpleAccountGetUsername(active)[:-1]
print("TFC-NaCl %s | NH.py\n" % str_version)
print("Active account: %s\n" % acco_u)
except DBusException:
raise CriticalError("header_printer_process", "DBusException. Ensure "
"Pidgin is running.")
def nh_side_command_process():
"""
Execute command (clear screen or exit) on NH.
:return: [no return value]
"""
bus = SessionBus()
obj = bus.get_object("im.pidgin.purple.PurpleService",
"/im/pidgin/purple/PurpleObject")
purple = Interface(obj, "im.pidgin.purple.PurpleInterface")
account = purple.PurpleAccountsGetAllActive()[0]
while True:
if nh_side_command.empty():
sleep(0.001)
continue
cmd = nh_side_command.get()
if cmd.startswith("TFC|N|%s|U|CLEAR|" % int_version):
contact = cmd.split('|')[5]
new_conv = purple.PurpleConversationNew(1, account, contact)
purple.PurpleConversationClearMessageHistory(new_conv)
system("clear")
def queue_to_pidgin_process():
"""
Send message from queue to Pidgin.
:return: [no return value]
"""
bus = SessionBus()
obj = bus.get_object("im.pidgin.purple.PurpleService",
"/im/pidgin/purple/PurpleObject")
purple = Interface(obj, "im.pidgin.purple.PurpleInterface")
account = purple.PurpleAccountsGetAllActive()[0]
while True:
if message_to_pidgin.empty():
sleep(0.001)
continue
message = message_to_pidgin.get()
if message.startswith("TFC|N|%s|M|" % int_version):
tfc, model, ver, pt, ct, key_id, recipient = message.split('|')
to_pidgin = '|'.join(message.split('|')[:6])
if output_pidgin:
new_conv = purple.PurpleConversationNew(1, account, recipient)
sel_conv = purple.PurpleConvIm(new_conv)
purple.PurpleConvImSend(sel_conv, to_pidgin)
if message.startswith("TFC|N|%s|P|" % int_version):
tfc, model, ver, pt, pub_key, recipient = message.split('|')
to_pidgin = '|'.join(message.split('|')[:5])
if output_pidgin:
new_conv = purple.PurpleConversationNew(1, account, recipient)
sel_conv = purple.PurpleConvIm(new_conv)
purple.PurpleConvImSend(sel_conv, to_pidgin)
def nh_to_rxm_sender_process():
"""
Send message from queue to RxM.
:return: [no return value]
"""
while True:
if packet_to_rxm.empty():
sleep(0.001)
continue
packet = packet_to_rxm.get()
chksum = sha2_256(packet)[:checksum_len]
packet = "%s|%s\n" % (packet, chksum)
if local_testing:
ipc_rx.send(packet)
else:
port_to_rxm.write(packet)
###############################################################################
# PACKETS FROM TxM #
###############################################################################
def choose_txm_packet_queues(packet):
"""
Copy message from TxM to correct queues.
:param packet: Packet to copy.
:return: None
"""
if not isinstance(packet, str):
raise FunctionParameterTypeError("choose_txm_packet_queues")
timestamp = datetime.now().strftime(t_fmt)
if packet.startswith("TFC|N|%s|M|" % int_version):
recipient = packet.split('|')[6]
print("%s - message TxM > %s" % (timestamp, recipient))
to_rxm = "%s|me.%s" % ('|'.join(packet.split('|')[:6]), recipient)
packet_to_rxm.put(to_rxm)
message_to_pidgin.put(packet)
elif packet.startswith("TFC|N|%s|P|" % int_version):
recipient = packet.split('|')[5]
print("%s - pub key TxM > %s" % (timestamp, recipient))
message_to_pidgin.put(packet)
elif packet.startswith("TFC|N|%s|C|" % int_version):
print("%s - command TxM > RxM" % timestamp)
packet_to_rxm.put(packet)
elif packet.startswith("TFC|N|%s|L|" % int_version):
print("%s - Local key TxM > RxM" % timestamp)
packet_to_rxm.put(packet)
elif packet.startswith("TFC|N|%s|U|EXIT" % int_version):
sleep(0.5) # Time for nh_to_rxm_sender_process() to send exit-packet.
system("clear")
graceful_exit()
elif packet.startswith("TFC|N|%s|U|" % int_version):
packet_to_rxm.put(packet)
nh_side_command.put(packet)
elif packet.startswith("TFC|N|%s|I|" % int_version):
# Skip interface configuration packet
pass
else:
print("Illegal packet from TxM:\n%s" % packet)
return None
def txm_packet_load_process():
"""
Load packet from TxM via serial port (or IPC if local_testing is enabled).
:return: [no return value]
"""
if local_testing:
def ipc_to_queue(conn):
"""
Load packet from IPC.
:param conn: Listener object.
:return: [no return value]
"""
while True:
sleep(0.001)
pkg = str(conn.recv())
if pkg == '':
continue
pkg = pkg.strip('\n')
if not verify_checksum(pkg):
continue
choose_txm_packet_queues(pkg[:-9])
try:
l = Listener(('', 5001))
while True:
ipc_to_queue(l.accept())
except EOFError:
system("clear")
print("\nTxM <> NH IPC disconnected.\n")
graceful_exit()
else:
while True:
sleep(0.001)
packet = port_to_txm.readline()
if packet == '':
continue
packet = packet.strip('\n')
if not verify_checksum(packet):
continue
choose_txm_packet_queues(packet[:-9])
def rxm_port_listener():
"""
Process that waits for packets from RxM interface during initial serial
interface configuring.
:return: [no return value]
"""
while True:
data = port_to_rxm.readline()
if data:
configure_queue.put('FLIP')
sleep(0.001)
def txm_port_listener():
"""
Process that waits for packets from TxM interface during initial serial
interface configuring.
:return: [no return value]
"""
while True:
data = port_to_txm.readline()
if data:
configure_queue.put('OK')
sleep(0.001)
###############################################################################
# MAIN #
###############################################################################
unittesting = False # Alters function input during unittesting
if __name__ == "__main__":
parser = ArgumentParser("python NH.py", usage="%(prog)s [OPTION]")
parser.add_argument("-p",
action="store_true",
default=False,
dest="quiet",
help="do not output messages to Pidgin")
parser.add_argument("-l",
action="store_true",
default=False,
dest="local",
help="enable local testing mode")
parser.add_argument("-d",
action="store_true",
default=False,
dest="ddsockets",
help="enable data diode simulator sockets")
args = parser.parse_args()
if args.ddsockets:
dd_sockets = True
if args.quiet:
output_pidgin = False
if args.local:
local_testing = True
if startup_banner:
print_banner()
# If local testing is disabled, initialize serial ports.
if local_testing:
if dd_sockets:
rx_socket = 5002
else:
rx_socket = 5003
try:
phase("\nWaiting for socket from Rx.py...", 35)
ipc_rx = Client(("localhost", rx_socket))
print("Connection established.\n")
sleep(0.5)
system("clear")
except KeyboardInterrupt:
clean_exit()
else:
serial_tx, serial_rx = get_serial_interfaces()
try:
port_to_txm = Serial(serial_tx, baud_rate, timeout=0.1)
port_to_rxm = Serial(serial_rx, baud_rate, timeout=0.1)
except serialutil.SerialException:
clean_exit("Error: Serial interfaces are set incorrectly.")
# Auto configure NH side serial ports
phase("Waiting for configuration packet from TxM...", 46)
configure_queue = Queue()
tl = Process(target=txm_port_listener)
rl = Process(target=rxm_port_listener)
tl.start()
rl.start()
try:
while True:
sleep(0.001)
if not configure_queue.empty():
command = configure_queue.get()
if command == "FLIP":
port_to_txm = Serial(serial_rx, baud_rate, timeout=0.1)
port_to_rxm = Serial(serial_tx, baud_rate, timeout=0.1)
print("Interfaces flipped.\n")
tl.terminate()
rl.terminate()
break
if command == "OK":
print("Interfaces OK.\n")
tl.terminate()
rl.terminate()
break
except KeyboardInterrupt:
tl.terminate()
rl.terminate()
clean_exit()
exit_queue = Queue()
packet_to_rxm = Queue()
packet_from_txm = Queue()
nh_side_command = Queue()
message_to_pidgin = Queue()
message_from_pidgin = Queue()
hp = Process(target=header_printer_process)
sm = Process(target=txm_packet_load_process)
po = Process(target=queue_to_pidgin_process)
cp = Process(target=nh_side_command_process)
nr = Process(target=pidgin_receiver_process)
rs = Process(target=nh_to_rxm_sender_process)
hp.start()
sleep(0.5) # Allow header_printer_process() time to catch DBusException.
sm.start()
po.start()
cp.start()
nr.start()
rs.start()
try:
while True:
if not exit_queue.empty():
command = exit_queue.get()
if command == "exit":
hp.terminate()
sm.terminate()
po.terminate()
cp.terminate()
nr.terminate()
rs.terminate()
clean_exit()
sleep(0.001)
except KeyboardInterrupt:
hp.terminate()
sm.terminate()
po.terminate()
cp.terminate()
nr.terminate()
rs.terminate()
clean_exit()

2856
Rx.py Normal file

File diff suppressed because it is too large Load Diff

4117
Tx.py Normal file

File diff suppressed because it is too large Load Diff

242
dd.py Normal file
View File

@ -0,0 +1,242 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# TFC-NaCl 0.16.01 beta || dd.py
"""
GPL License
This software is part of the TFC application, which 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. For
a copy of the GNU General Public License, see <http://www.gnu.org/licenses/>.
"""
from os import system
from time import sleep
from multiprocessing.connection import Client, Listener
from multiprocessing import Process, Queue
from sys import argv
###############################################################################
# DATA DIODE FRAMES #
###############################################################################
def lr_upper():
print("""
Data flow
Tx
S > R ±6V
GND Rx GND
S R ±6V
""")
def lr_lower():
print("""
Data flow
Tx
S R ±6V
GND Rx GND
S > R ±6V
""")
def lr_idle():
print("""
Data flow
Tx
S R ±6V
GND Rx GND
S R ±6V
""")
def rl_upper():
print("""
Data flow
Tx
±6V R < S
GND Rx GND
±6V R S
""")
def rl_lower():
print("""
Data flow
Tx
±6V R S
GND Rx GND
±6V R < S
""")
def rl_idle():
print("""
Data flow
Tx
±6V R S
GND Rx GND
±6V R S
""")
###############################################################################
# DATA DIODE ANIMATORS #
###############################################################################
def lr():
for i in range(10):
system('clear')
lr_lower()
sleep(0.04)
system('clear')
lr_upper()
sleep(0.04)
system("clear")
def rl():
for i in range(10):
system('clear')
rl_lower()
sleep(0.04)
system('clear')
rl_upper()
sleep(0.04)
system("clear")
###############################################################################
# DATA DIODE PROCESSES #
###############################################################################
def tx_process():
if tx_nh_lr or nh_rx_rl:
lr_idle()
if tx_nh_rl or nh_rx_lr:
rl_idle()
while True:
if io_queue.empty():
sleep(0.001)
continue
msg = io_queue.get()
if tx_nh_lr or nh_rx_rl:
lr()
lr_idle()
if tx_nh_rl or nh_rx_lr:
rl()
rl_idle()
ipx_send.send(msg)
def rx_process():
def ipc_to_queue(conn):
while True:
sleep(0.001)
pkg = str(conn.recv())
io_queue.put(pkg)
try:
l = Listener(('', input_socket))
while True:
ipc_to_queue(l.accept())
except EOFError:
exit_queue.put("exit")
tx_nh_lr = False
nh_rx_lr = False
tx_nh_rl = False
nh_rx_rl = False
input_socket = 0
output_socket = 0
try:
# Simulates data diode between Tx.py on left, NH.py on right.
if str(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(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(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(argv[1]) == "nhrxrl":
nh_rx_rl = True
input_socket = 5002
output_socket = 5003
else:
system("clear")
print "\nUsage: python dd.py {txnh{lr,rl}, nhrx{lr,rl}\n"
exit()
except IndexError:
system("clear")
print "\nUsage: python dd.py {txnh{lr,rl}, nhrx{lr,rl}}\n"
exit()
try:
print('Waiting for socket"')
ipx_send = Client(("localhost", output_socket))
print("Connection established.")
sleep(0.3)
system("clear")
except KeyboardInterrupt:
exit()
exit_queue = Queue()
io_queue = Queue()
txp = Process(target=tx_process)
rxp = Process(target=rx_process)
txp.start()
rxp.start()
try:
while True:
if not exit_queue.empty():
command = exit_queue.get()
if command == "exit":
txp.terminate()
rxp.terminate()
exit()
sleep(0.01)
except KeyboardInterrupt:
txp.terminate()
rxp.terminate()
exit()

165
hwrng.py Normal file
View File

@ -0,0 +1,165 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# TFC-NaCl 0.16.01 beta || hwrng.py
"""
GPL License
This software is part of the TFC application, which 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. For
a copy of the GNU General Public License, see <http://www.gnu.org/licenses/>.
"""
from binascii import hexlify
from os import system
from time import sleep
from hashlib import sha256
from simplesha3 import sha3256
try:
import RPi.GPIO as GPIO
except ImportError:
GPIO = None
pass
str_version = "0.16.01 beta"
int_version = 1601
###############################################################################
# ERROR CLASSES #
###############################################################################
class FunctionParameterTypeError(Exception):
"""
hwrng.py should gracefully exit if function is called with incorrect
parameter types.
"""
def __init__(self, function_name):
system("clear")
print("\nError: M(%s): Wrong input type.\n" % function_name)
exit()
###############################################################################
# CRYPTOGRAPHY #
###############################################################################
def sha2_256(message):
"""
Generate SHA256 digest from message.
:param message: Input to hash function.
:return: Hex representation of SHA256 digest.
"""
if not isinstance(message, str):
raise FunctionParameterTypeError("sha2_256")
h_function = sha256()
h_function.update(message)
hex_digest = hexlify(h_function.digest())
return hex_digest
def sha3_256(message):
"""
Generate SHA3-256 digest from message.
:param message: Input to hash function.
:return: Hex representation of SHA3-256 digest.
"""
if not isinstance(message, str):
raise FunctionParameterTypeError("sha3_256")
return hexlify(sha3256(message))
def get_hwrng_entropy():
"""
Get HWRNG entropy.
Load entropy from HWRNG through GPIO pins.
Before sampling starts, a loop collects 3000 samples to allow the HWRNG
time to warm. Sampling is done at 10Hz frequency to ensure minimal
auto-correlation between samples. Entropy is compressed with SHA3-256
before it is returned.
:return: 32 bytes of entropy.
"""
def digits_to_bytes(di):
"""
Convert string of binary digits to byte string.
:param di: Digit string.
:return: Byte string.
"""
return ''.join(chr(int(di[i:i + 8], 2)) for i in xrange(0, len(di), 8))
# Sampling settings
sample_delay = 0.1
gpio_port = 4
samples_n = 256
GPIO.setmode(GPIO.BCM)
GPIO.setup(gpio_port, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
try:
warmup_zero = 0
warmup_one = 0
while True:
if warmup_one > 1500 and warmup_zero > 1500:
break
if GPIO.input(gpio_port) == 1:
warmup_one += 1
else:
warmup_zero += 1
sleep(0.001)
# Perform Von Neumann whitening during sampling
vn_digits = ''
while True:
if len(vn_digits) >= samples_n:
break
first_bit = GPIO.input(gpio_port)
sleep(sample_delay)
second_bit = GPIO.input(gpio_port)
sleep(sample_delay)
if first_bit == second_bit:
continue
else:
vn_digits += str(first_bit)
hwrng_bytes = digits_to_bytes(vn_digits)
if len(hwrng_bytes) != 32:
print("ERROR")
entropy = sha3_256(hwrng_bytes)
except KeyboardInterrupt:
GPIO.cleanup()
raise
GPIO.cleanup()
print entropy
get_hwrng_entropy()

97
readme.md Normal file
View File

@ -0,0 +1,97 @@
<img align="right" src="https://cs.helsinki.fi/u/oottela/tfclogo.png" style="position: relative; top: 0; left: 0;">
###Tinfoil Chat NaCl
TFC-NaCl is a high assurance encryption plugin for Pidgin IM client that
protects users from [passive eavesdropping](https://en.wikipedia.org/wiki/Upstream_collection),
[active MITM attacks](https://en.wikipedia.org/wiki/Man-in-the-middle_attack)
and remote CNE (aka equipment interference) of endpoints practiced by TLAs such
as the [NSA](https://firstlook.org/theintercept/2014/03/12/nsa-plans-infect-millions-computers-malware/),
[GCHQ](http://www.wired.co.uk/news/archive/2014-03/13/nsa-turbine) and
[BKA](http://ccc.de/en/updates/2011/staatstrojaner).
###Overview
**High end end-to-end encryption** with Curve25519 ECDHE / PSKs.
XSalsa20-Poly1305 AEAD with forward secrecy and deniability.
**High entropy key generation** with /dev/urandom mixed with entropy from
open circuit design GPIO HWRNG of RPi that acts either as TCB itself or as
a sampling device over SSH.
**Strong endpoint security** is obtained with hardware-based TCB separation:
Unidirectional data flow between end point's three computers prevents
either injection of malware or exfiltration of keys and plaintexts from TCB
regardless of existing software zero-day vulnerabilities in software.
**Defeats metadata** about quantity and schedule of communication with
trickle connection that outputs constant stream of encrypted noise data.
**Covert file transfer** that takes place in background during
trickle connection.
**Multicasting** of messages and files to multiple recipients (no trickle
supported).
###How it works
![](https://cs.helsinki.fi/u/oottela/tfc_graph2.png)
TFC uses three computers per endpoint. Alice enters her commands and messages to
program Tx.py running on her Transmitter computer (TxM), a [TCB](https://en.wikipedia.org/wiki/Trusted_computing_base)
separated from network. Tx.py encrypts and signs plaintext data and relays it
to receiver computers (RxM) via networked computer (NH) through RS-232 interface
and a data diode.
Depending on packet type, the program NH.py running on Alice's NH forwards
packets from TxM's serial interface to Pidgin and local RxM (through another
RS-232 interface and data diode). Local RxM authenticates and decrypts received
data before processing it.
Pidgin sends the packet either directly or through Tor network to IM server,
that then forwards it directly (or again through Tor) to Bob.
NH.py on Bob's NH receives Alice's packet from Pidgin, and forwards it through
RS-232 interface and data diode to Bob's RxM, where the ciphertext is
authenticated, decrypted, displayed and optionally also logged. When the Bob
responds, he will send the message using his TxM and in the end Alice reads the
message from her RxM.
###Why keys can not be exfiltrated
1. Malware that exploits an unknown vulnerability in RxM can infiltrate to
system, but is unable to exfiltrate keys or plaintexts, as data diode prevents
all outbound traffic.
2. Malware can not breach TxM as data diode prevents all inbound traffic. The
only data input from RxM to TxM is the 72 char public key, manually typed by
user.
3. The NH is assumed to be compromised, but unencrypted data never touches it.
![](https://cs.helsinki.fi/u/oottela/tfc_attacks2.png)
The optical gap of the data diode (below) physically blocks back channels.
<img src="https://cs.helsinki.fi/u/oottela/data_diode.png" align="center" width="74%" height="74%"/>
###More information
White paper and manual for previous versions are listed below. TFC-NaCl specific
updates are listed in the updatelog. Updated white paper and documentation are
under work.
White paper: https://cs.helsinki.fi/u/oottela/tfc.pdf
Manual: https://cs.helsinki.fi/u/oottela/tfc-manual.pdf

805
setup.py Normal file
View File

@ -0,0 +1,805 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# TFC-NaCl 0.16.01 beta || setup.py
"""
GPL License
This software is part of the TFC application, which 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. For
a copy of the GNU General Public License, see <http://www.gnu.org/licenses/>.
"""
from os import chdir, getcwd, system, geteuid
from subprocess import Popen, check_output
str_version = "0.16.01 beta"
int_version = 1601
repository = "https://raw.githubusercontent.com/maqp/tfc-nacl/master/"
###############################################################################
# APT COMMANDS #
###############################################################################
sagi = "sudo apt-get --yes install"
def cmd(message, command):
"""
Display message and run command as subprocess.
:param message: Message to be displayed.
:param command: Command to run.
:return: None
"""
print("\n%s\n" % message)
Popen(command, shell=True).wait()
return None
def update_repositories():
cmd("Updating repository list", "sudo apt-get --yes update")
def install_python_serial():
cmd("Installing Python serial", "%s python-serial" % sagi)
def install_python_qt4():
cmd("Installing Python QT4", "%s python-qt4" % sagi)
def install_python_qt4_dbus():
cmd("Installing Python QT4-DBus", "%s python-qt4-dbus" % sagi)
def install_pidgin():
cmd("Installing Pidgin", "%s pidgin" % sagi)
def install_pidgin_otr():
cmd("Installing Pidgin OTR", "%s pidgin-otr" % sagi)
def install_python_pip():
cmd("Installing Python pip", "%s python-pip" % sagi)
def install_python_setuptools():
cmd("Installing Python Setuptools", "%s python-setuptools" % sagi)
def install_python_dev():
cmd("Installing Python-Dev", "%s python-dev" % sagi)
def install_libffi_dev():
cmd("Installing libffi", "%s libffi-dev" % sagi)
def install_tkinter():
cmd("Installing Python Tkinter", "%s python-tk" % sagi)
###############################################################################
# PIP COMMANDS #
###############################################################################
def pip_passlib():
cmd("Installing PassLib", "sudo pip install passlib")
def pip_simplesha3():
cmd("Installing SimpleSHA3", " sudo pip install simplesha3")
def pip_paramiko():
cmd("Installing SimpleSHA3", " sudo pip install paramiko")
###############################################################################
# FILE HASH VERIFICATION #
###############################################################################
hash_list = ("""
b7093331d65eaf36b218f105a0b7a710ade5cf520b96da4f4b580600adf4ad71 tfc-mods.zip
c76c6ec69dc7b233dc58aff42b45d87e8ec8f8e4e32ecf790260ca9e436ec42f dd.py
264889d2d2f06d258035bec8baa2e1300f42805d2d6b1987faf1c5cb6e72bbf7 hwrng.py
142cb4cb8ba803880015ccd3a82d5428a4480f4e7e463245977ba797ebf72565 NH.py
d101d5a1308a09a79824d2652dcad0319c504e50a71d96fee9d3d189df50a946 Rx.py
af392c14f4390103ab9f28d042b8dc353b1d9f5590aff01d9a456047c6babd4d test_nh.py
acd7ca80c2b7b3f15c5a588d0dfc858246f1675456d787d655e7af093d5ab2ed test_rx.py
73a01d9176f883e16b48858ee62ae847f09b9ab0e663db5a4f8628081638a0b9 test_tx.py
1553911611f2ebdd54c55d8c2d9ef822378169a0c3ea8e66495c2d6ae73346d5 Tx.py
""")
def check_file_hash(filename):
"""
Verify that SHA-256 hash of file matches the one in installer.
WARNING!
Downloading the installer through TLS-encrypted GitHub website is *NOT* a
guarantee that a state adversary could not edit the downloaded source code
on the fly with great ease.
Unless you can verify the origin of *this* installer, you can NOT trust the
hashes written above: in other words, the hashes only protect against
unintentional transmission errors, NOT against malicious actor. You must
obtain the GPG signing key to verify the authenticity of this installer.
--------------------------------------------------------------------
No TLS-MITM attack free way exists to obtain the signing key online.
--------------------------------------------------------------------
The only reasonably secure way to obtain the signing key is personal
handout of key or GPG Web of Trust with respected and trustworthy members.
:param filename: File to verify.
:return: None
"""
f_hash = check_output(["sha256sum", filename]).split()[0]
h_list = hash_list.split('\n')
for h in h_list:
if filename in h:
if f_hash not in h:
system("clear")
print("CRITICAL ERROR: SHA2-256 hash of %s was incorrect. \n"
"This might indicate a TLS-MITM attack, transmission\n"
"error or that this installer is outdated.\n") % filename
exit()
print("\nSHA256 hash of %s was correct.\n" % filename)
return None
###############################################################################
# CRYPTO LIBRARIES COMMANDS #
###############################################################################
def pynacl_install():
"""
Install the PyNaCl library.
:return: None
"""
app_root_directory = getcwd()
cmd("Downloading PyNaCl Library", "wget https://github.com/maqp/"
"pynacl/archive/tfc-mods.zip")
check_file_hash("tfc-mods.zip")
cmd("Unzipping PyNaCl Library", "unzip tfc-mods.zip")
chdir("pynacl-tfc-mods/")
cmd("Installing PyNaCl Library", "sudo python setup.py install")
chdir(app_root_directory)
cmd("Removing PyNaCl (tfc-mods.zip)", "rm tfc-mods.zip")
Popen("sudo rm -r pynacl-master/", shell=True).wait()
system("clear")
if not yes("\n Keep PyNaCl source files?\n"):
Popen("sudo rm -rf pynacl-tfc-mods", shell=True).wait()
return None
###############################################################################
# DOWNLOAD TFC PROGRAMS #
###############################################################################
def get_tx():
cmd("Downloading Tx.py (TxM)", "wget %sTx.py" % repository)
check_file_hash("Tx.py")
cmd("Downloading test_tx.py (TxM)", "wget %sunittests/test_tx.py"
% repository)
check_file_hash("test_tx.py")
def get_hwrng():
cmd("Downloading hwrng.py", "wget %shwrng.py" % repository)
check_file_hash("hwrng.py")
def get_rx():
cmd("Downloading Rx.py (RxM)", "wget %sRx.py" % repository)
check_file_hash("Rx.py")
cmd("Downloading test_rx.py (RxM)", "wget %sunittests/test_rx.py"
% repository)
check_file_hash("test_rx.py")
def get_nh():
cmd("Downloading NH.py (NH)", "wget %sNH.py" % repository)
check_file_hash("NH.py")
cmd("Downloading test_nh.py (NH)", "wget %sunittests/test_nh.py"
% repository)
check_file_hash("test_nh.py")
def get_dd():
cmd("Downloading dd.py (NH)", "wget %sdd.py" % repository)
check_file_hash("dd.py")
###############################################################################
# EDIT TFC PROGRAMS #
###############################################################################
def rasp_disable_boot_info():
"""
Disable Boot info through serial port to keep NH's TxM serial interface's
input only related to Tx.py output.
:return: None
"""
print("\nEditing file 'cmdline.txt'.\n")
try:
content = open('/boot/cmdline.txt').readline()
content = content.replace(" console=ttyAMA0,115200", '')
open('/boot/cmdline.txt', 'w+').write(content)
except IOError:
print("CRITICAL ERROR! M(rasp_disable_boot_info):\n"
"/boot/cmdline.txt could not be accessed.\n"
"Exiting setup.py")
exit()
return None
def ssh_hwrng_connection():
"""
Ask user whether they will use Raspbian over SSH to load entropy. If yes,
enable SSH client for Tx.py.
:return: None
"""
if not yes("Will TxM load entropy from Raspberry Pi over SSH?"):
return None
print("\nEnabling SSH client during key generation\n")
contents = open("Tx.py").read()
contents = contents.replace("use_ssh_hwrng = False",
"use_ssh_hwrng = True")
open("Tx.py", "w+").write(contents)
return None
def serial_config_raspbian(program):
"""
Ask user whether they will use USB serial interface.
Answering no enables Raspberry Pi's integrated interface /dev/ttyAMA0.
:param program: Program the serial interface of which is changed.
:return: None
"""
if program == "Tx.py":
if yes("Will TxM connect to NH using USB to serial adapter?"):
return None
if program == "Rx.py":
if yes("Will RxM connect to NH using USB to serial adapter?"):
return None
print("\nChanging %s's NH serial-interface to integrated.\n" % program)
contents = open(program).read()
contents = contents.replace("nh_usb_adapter = True",
"nh_usb_adapter = False")
open(program, "w+").write(contents)
return None
def serial_config_integrated(program):
"""
Ask user whether they will use USB to serial adapter.
Answering no enables integrated interface /dev/ttyS0.
:param program: Program the serial interface of which is changed.
:return: None
"""
if program == "NH.py":
if yes("Will NH connect to TxM using USB to serial adapter?"):
pass
else:
print("\nChanging NH.py's TxM serial-interface to integrated.\n")
contents = open(program).read()
contents = contents.replace("txm_usb_adapter = True",
"txm_usb_adapter = False")
open(program, "w+").write(contents)
if yes("Will NH connect to RxM using USB to serial adapter?"):
pass
else:
print("\nChanging NH.py's RxM serial-interface to integrated.\n")
contents = open(program).read()
contents = contents.replace("rxm_usb_adapter = True",
"rxm_usb_adapter = False")
open(program, "w+").write(contents)
return None
if program == "Tx.py":
if yes("Will TxM connect to NH using USB to serial adapter?"):
return None
if program == "Rx.py":
if yes("Will RxM connect to NH using USB to serial adapter?"):
return None
print("\nChanging %s's NH serial-interface to integrated.\n" % program)
contents = open(program).read()
contents = contents.replace("nh_usb_adapter = True",
"nh_usb_adapter = False")
open(program, "w+").write(contents)
return None
def change_to_local(file_name):
"""
Configure {Tx,Rx,NH}.py local_testing boolean to True.
:param file_name: Target program from which local_testing is enabled.
:return: None
"""
print("\nEnabling 'local_testing' boolean in program '%s'.\n" % file_name)
content = open(file_name).read()
content = content.replace("local_testing = False",
"local_testing = True")
open(file_name, "w+").write(content)
return None
###############################################################################
# MISC #
###############################################################################
def set_serial_permissions(username=''):
"""
Add username to 'dialout' group to allow operation
of serial port without root privileges.
:param username: Username to be added.
:return: None
"""
if username == '':
while True:
print("\nType name of the user that will be running TFC to add\n"
"them to dialout group, that enables serial interfaces.")
username = raw_input("\n >")
if yes("\n Confirm user '%s'?" % username):
break
cmd('', "sudo gpasswd --add %s dialout" % username)
return None
def yes(prompt):
"""
Prompt user a question that is answered with yes / no.
:param prompt: Question to be asked.
:return: True if user types 'y' or 'yes', otherwise returns False.
"""
while True:
try:
answer = raw_input("%s (y/n): " % prompt)
except KeyboardInterrupt:
raise
if answer.lower() in ("yes", 'y'):
return True
elif answer.lower() in ("no", 'n'):
return False
def print_menu():
"""
Display the menu with list of installation configurations.
:return: None
"""
print("TFC-NaCl %s || setup.py" % str_version)
print("""
Select a device-OS configuration (tested distros are listed):
TxM
1. Raspbian Jessie (Run this installer as sudo)
2. Ubuntu 14.04 LTS
Kubuntu 14.04 LTS
Xubuntu 14.04 LTS
Lubuntu 15.04
Linux Mint 17.3 Rosa
HWRNG (over SSH from TxM)
3. Raspbian Jessie (Run this installer as sudo)
RxM
4. Raspbian Jessie (Run this installer as sudo)
5. Ubuntu 14.04 LTS
Kubuntu 14.04 LTS
Xubuntu 14.04 LTS
Lubuntu 15.04
Linux Mint 17.3 Rosa
NH
6. Ubuntu 14.04 LTS
Kubuntu 14.04 LTS
Xubuntu 14.04 LTS
Lubuntu 15.04
Linux Mint 17.3 Rosa
7. Tails 2.0
Local Testing (insecure)
8. Ubuntu 14.04 LTS
Kubuntu 14.04 LTS
Xubuntu 14.04 LTS
Lubuntu 15.04
Linux Mint 17.3 Rosa\n""")
return None
def print_local_tester_warning():
"""
Display a warning about insecurity of local testing.
:return: None
"""
print("\n WARNING! \n"
" YOU HAVE SELECTED THE LOCAL TESTING CONFIGURATION FOR TFC. \n"
" THIS VERSION IS INTENDED ONLY FOR TRYING OUT THE FEATURES AND \n"
" STABILITY OF THE SYSTEM. IN THIS CONFIGURATION, THE ENCRYPTION\n"
" KEYS ARE GENERATED, STORED AND HANDLED ON NETWORK-CONNECTED \n"
" COMPUTER, SO ANYONE WHO BREAKS IN TO IT BY EXPLOITING A KNOWN \n"
" (OR UNKNOWN ZERO DAY) VULNERABILITY, CAN DECRYPT AND/OR FORGE \n"
" ALL MESSAGES YOU SEND AND RECEIVE!")
return None
def print_local_test_install_finish():
print("\n Test folder 'tfc-nacl' has been generated. Initiate \n"
" OTR-encrypted Pidgin conversation and run Tx.py, Rx.py\n"
" and NH.py in their own terminals.\n")
###############################################################################
# INSTALL ROUTINES #
###############################################################################
def raspbian_txm():
if yes("Install TxM configuration for Raspbian Jessie?"):
if geteuid() != 0:
print("\nError: Raspbian installer requires root privileges.\n"
"\nExiting.\n")
exit()
update_repositories()
install_python_setuptools()
install_python_dev()
install_libffi_dev()
install_python_pip()
pip_passlib()
pip_simplesha3()
pynacl_install()
rasp_disable_boot_info()
Popen("mkdir tfc-nacl", shell=True).wait()
chdir("tfc-nacl/")
get_tx()
serial_config_raspbian("Tx.py")
set_serial_permissions()
system("clear")
print("\nTxM side installation complete.\n"
"Reboot the system before running.\n")
exit()
def raspbian_hwrng():
if yes("Install HWRNG configuration for Raspbian Jessie?"):
if geteuid() != 0:
print("\nError: Raspbian installer requires root privileges.\n"
"\nExiting.\n")
exit()
update_repositories()
install_python_setuptools()
install_python_dev()
install_libffi_dev()
install_python_pip()
pip_simplesha3()
get_hwrng()
system("clear")
print("\nHWRNG side installation complete.\n"
"Reboot the system before running.\n")
exit()
def ubuntu_txm():
if yes("Install TxM configuration for *buntu / Linux Mint?"):
update_repositories()
install_python_serial()
install_tkinter()
install_python_setuptools()
install_python_dev()
install_libffi_dev()
install_python_pip()
pip_passlib()
pip_simplesha3()
pip_paramiko()
pynacl_install()
Popen("mkdir tfc-nacl", shell=True).wait()
chdir("tfc-nacl/")
get_tx()
serial_config_integrated("Tx.py")
ssh_hwrng_connection()
set_serial_permissions()
system("clear")
print("\nTxM side installation complete.\n"
"Reboot the system before running.\n")
exit()
else:
return None
def raspbian_rxm():
if yes("Install RxM configuration for Raspbian Jessie?"):
if geteuid() != 0:
print("\nError: Raspbian installer requires root privileges.\n"
"\nExiting.\n")
exit()
update_repositories()
install_python_setuptools()
install_python_dev()
install_libffi_dev()
install_python_pip()
pip_passlib()
pip_simplesha3()
pynacl_install()
rasp_disable_boot_info()
Popen("mkdir tfc-nacl", shell=True).wait()
chdir("tfc-nacl/")
get_rx()
serial_config_raspbian("Rx.py")
set_serial_permissions()
system("clear")
print("\nRxM side installation complete.\n"
"Reboot the system before running.\n")
exit()
else:
return None
def ubuntu_rxm():
if yes("Install RxM configuration for *buntu / Linux Mint?"):
update_repositories()
install_python_serial()
install_python_setuptools()
install_python_dev()
install_libffi_dev()
install_python_pip()
pip_passlib()
pip_simplesha3()
pynacl_install()
Popen("mkdir tfc-nacl", shell=True).wait()
chdir("tfc-nacl/")
get_rx()
serial_config_integrated("Rx.py")
set_serial_permissions()
system("clear")
print("\nRxM side installation complete.\n"
"Reboot the system before running.\n")
exit()
else:
return None
def ubuntu_nh():
if yes("Install NH configuration for *buntu / Linux Mint?"):
update_repositories()
install_python_qt4()
install_python_qt4_dbus()
install_python_serial()
if yes("\nInstall Pidgin with OTR-plugin?"):
install_pidgin()
install_pidgin_otr()
get_nh()
serial_config_integrated("NH.py")
set_serial_permissions()
system("clear")
print("\nNH side installation complete.\n"
"Reboot the system before running.\n")
exit()
else:
return None
def tails_nh():
if yes("Install NH configuration for Tails LiveCD / LiveUSB?"):
update_repositories()
install_python_serial()
install_python_qt4_dbus()
set_serial_permissions("amnesia")
get_nh()
serial_config_integrated("NH.py")
system("clear")
print("\nNH install script completed. Initiate OTR-encrypted\n"
"Pidgin conversation and launch NH.py.\n\nExiting.\n")
exit()
else:
return None
def local_testing():
system("clear")
print_local_tester_warning()
if not raw_input("\n TYPE 'INSECURE' TO VERIFY "
"YOU UNDERSTAND THE RISKS: ") == "INSECURE":
return None
system("clear")
update_repositories()
install_python_qt4()
install_python_qt4_dbus()
install_python_serial()
install_tkinter()
install_python_setuptools()
install_python_dev()
install_libffi_dev()
install_python_pip()
pip_passlib()
pip_simplesha3()
pip_paramiko()
pynacl_install()
if yes("\n Install Pidgin with OTR-plugin?"):
install_pidgin()
install_pidgin_otr()
Popen("mkdir tfc-nacl", shell=True).wait()
chdir("tfc-nacl/")
get_tx()
get_nh()
get_rx()
get_dd()
change_to_local("Tx.py")
change_to_local("Rx.py")
change_to_local("NH.py")
ssh_hwrng_connection()
system("clear")
print_local_test_install_finish()
print(" Exiting.\n")
exit()
######################################################################
# MAIN LOOP #
######################################################################
while True:
try:
system("clear")
print_menu()
selection = int(raw_input("1..8: "))
if selection == 1:
raspbian_txm()
if selection == 2:
ubuntu_txm()
if selection == 3:
raspbian_hwrng()
if selection == 4:
raspbian_rxm()
if selection == 5:
ubuntu_rxm()
if selection == 6:
ubuntu_nh()
if selection == 7:
tails_nh()
if selection == 8:
local_testing()
except (ValueError, IndexError):
continue
except KeyboardInterrupt:
print("\n\nExiting.\n")
exit()

211
unittests/test_nh.py Normal file
View File

@ -0,0 +1,211 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# TFC-NaCl || test_nh.py
"""
GPL License
This software is part of the TFC application, which 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. For
a copy of the GNU General Public License, see <http://www.gnu.org/licenses/>.
"""
import unittest
import NH
from NH import *
from binascii import hexlify
from hashlib import sha256
###############################################################################
# UNITTEST HELPERS #
###############################################################################
def ut_sha2_256(message):
h_function = sha256()
h_function.update(message)
return hexlify(h_function.digest())
###############################################################################
# HELPERS #
###############################################################################
NH.unittesting = True
class TestPhase(unittest.TestCase):
def test_1_input_parameters(self):
for a in [1, 1.0, True]:
for b in ["string", 1.0, True]:
with self.assertRaises(SystemExit):
phase(a, b)
def test_2_output_type(self):
self.assertIsNone(phase("test", 10))
class TestSHA256(unittest.TestCase):
def test_1_input_parameter(self):
for a in [1, 1.0, True]:
with self.assertRaises(SystemExit):
sha2_256(a)
def test_2_SHA256_vector(self):
"""
Test SHA256 with official test vector:
http://csrc.nist.gov/groups/ST/toolkit/
documents/Examples/SHA_All.pdf // page 14
"""
self.assertEqual(sha2_256("abc"), "ba7816bf8f01cfea414140de5dae2223"
"b00361a396177a9cb410ff61f20015ad")
class TestVerifyChecksum(unittest.TestCase):
def test_1_input_parameter(self):
for a in [1, 1.0, True]:
with self.assertRaises(SystemExit):
verify_checksum(a)
def test_2_function(self):
pt = "test_packet"
tv = ut_sha2_256(pt)
self.assertTrue(verify_checksum("%s|%s" % (pt, tv[:NH.checksum_len])))
class TestCleanExit(unittest.TestCase):
def test_1_input_parameter(self):
for a in [1, 1.0, True]:
with self.assertRaises(SystemExit):
clean_exit(a)
def test_2_exit_no_msg(self):
with self.assertRaises(SystemExit):
clean_exit()
def test_3_exit_with_msg(self):
with self.assertRaises(SystemExit):
clean_exit("test message")
class TestGracefulExit(unittest.TestCase):
def test_1_function(self):
with self.assertRaises(SystemExit):
graceful_exit()
class TestGetTTyWH(unittest.TestCase):
def test_output_types(self):
w, h = get_tty_wh()
self.assertTrue(isinstance(w, int))
self.assertTrue(isinstance(h, int))
class TestPrintBanner(unittest.TestCase):
def test_output_type(self):
self.assertIsNone(print_banner())
class TestGetSerialInterfaces(unittest.TestCase):
"""
This function doesn't have any tests yet.
"""
###############################################################################
# RECEIVER #
###############################################################################
class TestDBusReceiver(unittest.TestCase):
"""
This function doesn't have any tests yet.
"""
class TestPidginToRxMQueue(unittest.TestCase):
"""
This function doesn't have any tests yet.
"""
class TestPidginReceiverProcess(unittest.TestCase):
"""
This function doesn't have any tests yet.
"""
###############################################################################
# OTHER #
###############################################################################
class TestHeaderPrinterProcess(unittest.TestCase):
"""
This function doesn't have any tests yet.
"""
class TestNHSideCommandProcess(unittest.TestCase):
"""
This function doesn't have any tests yet.
"""
class TestQueueToPidginProcess(unittest.TestCase):
"""
This function doesn't have any tests yet.
"""
class NHToRxMSenderProcess(unittest.TestCase):
"""
This function doesn't have any tests yet.
"""
###############################################################################
# PACKETS FROM TxM #
###############################################################################
class TestChooseTxMPacketQueues(unittest.TestCase):
def test_1_input_parameter(self):
for a in [1, 1.0, True]:
with self.assertRaises(SystemExit):
choose_txm_packet_queues(a)
class TestTxMPacketLoadProcess(unittest.TestCase):
"""
This function doesn't have any tests yet.
"""
class TestRxMPortListener(unittest.TestCase):
"""
This function doesn't have any tests yet.
"""
class TestTxMPortListener(unittest.TestCase):
"""
This function doesn't have any tests yet.
"""
if __name__ == "__main__":
unittest.main(exit=False)

2593
unittests/test_rx.py Normal file

File diff suppressed because it is too large Load Diff

2808
unittests/test_tx.py Normal file

File diff suppressed because it is too large Load Diff

412
updatelog Normal file
View File

@ -0,0 +1,412 @@
TFC NaCl 0.16.01 | Update log
Local key transmission: Bootstrapping security
----------------------------------------------
In the initial bootstrap, Tx.py generates 256-bit local key and 256-bit key
encryption keys with separate processes, where 32-byte /dev/urandom string is
hashed with SHA3-256 (simplesha3 library), that is then passed through 25k
iteration PBKDF2-HMAC-SHA256.
If Tx.py is run on a Raspbian (Raspberry Pi), user can mix entropy from free
hardware design HWRNG, connected to GPIO 4 of the SoC board into the PBKDF2
function. Entropy is sampled with 10Hz frequency and von Neumann whitened
during sampling. Once the vN whitened samples have been collected, the
256-bit entropy is compressed using SHA3-256 hash function.
If Tx.py is run on a computer that has direct ethernet connection to Raspberry
Pi with GPIO HWRNG, Tx.py can SSH into that computer using paramiko library,
and query entropy with program hwrng.py.
The local key is padded to 254 bytes and encrypted with the key encryption key
using PyCrypto library's XSalsa20 stream cipher (192 bit nonce), and 128-bit
Poly1305 MAC. The signed ciphertext is transmitted to RxM through data diode.
User can choose whether to leave data diodes connected to NH and let signed
ciphertext pass through, or whether they want to transmit keys directly from
TxM to RxM by connecting serial interfaces that way. Such configuration
prevents adversary from obtaining the signed ciphertext from NH in case it has
been compromised at this point.
RxM will then ask user to manually type the 64 hex char key decryption key
displayed by TxM. The key decryption key is concatenated with 8 hex char
checksum, that is the truncated SHA3-256 hash of the key decryption key.
Once the 72 char hex key has been typed to Rx.py, the program decrypts the
local key, adds it to database, and shows a two char hex code that lets TxM
know local key delivery succeeded. This is a pre-requisite for the next step,
where account data/keys are transmitted to RxM, encrypted with the local key.
If RxM does not receive the encrypted local key, user can send the packet again
by typing "replay" instead of the device code. The local key transmission
can later be redone with command '/localkey'.
First contact
-------------
User is guided when adding first contact. Tx.py first prompts for account, e.g.
alice@jabber.org and a nick. Empty nick uses nick 'Alice' (automatically parsed
from account). Tx.py asks user to confirm account and nick (choosing no jumps
to beginning of account creation). After this Tx.py prompts user to choose key
exchange method:
Use PSK instead of ECDHE (y/n)?
PSK is explained later so here we choose 'n'/'no' to start ECDHE key exchange.
ECDHE Private key generation
----------------------------
Tx.py generates a curve 25519 ECDHE private key by with PyNaCl's
PrivateKey.generate(), that has been modified to XOR in either bitstring of 256
zeroes, or if provided, 256 bits string of external entropy. Tx.py always
provides the external entropy:
ext_ent = PBKDF2-HMAC-SHA256(SHA3-256(32-bytes from /dev/urandom, salt)).
If Raspbian with HWRNG is available, Tx.py will add the salt to PBKDF2-mix:
salt = SHA3-256( Von Neumann whitening (HWRNG samples))
The number of samples is dynamic and depends on results. This ensures SHA3
always takes in 256 Von Neumann whitened samples. If HWRNG produces bad entropy
i.e. chunk of just zeroes / ones, they are not added as samples.
PyNaCl's PrivateKey.generate()'s own entropy is loaded after external entropy
is provided, thus even if external entropy adds zero entropy, it does not
weaken the overall security, as in such case it would be the equal of XORing
PyNaCl's entropy with bitstring of zeroes.
Once the final entropy has been generated, PyNaCl will use it to create a
private key object.
ECDHE Public key delivery and verification of key authenticity
--------------------------------------------------------------
Tx.py generates the ECDHE public key from the private key object, and transmits
it to the recipient's RxM. Once the 64 hex char public key of contact has been
received by Rx.py the program shows it concatenated with an eight char hex
checksum like the one in local key. User must manually type the 72 hex char
string to TxM. Manual process ensures no automated data channel from network
can infect TxM after setup.
Once key has been successfully entered, Tx.py will ask user to use Signal by
Open Whisper Systems to verify the authenticity of contact's public key.
MITM against OTR session is possible if the adversary has has exfiltrated
respective keys from end points of users. If users find out during Signal call
that the public keys have been generated by a man in the middle, they do not
have to throw in the towel. Instead, users can read the public keys to each
other over Signal, manually type in new public key and effectively bypass the
MITM in network. Once the public key has been successfully typed, Tx.py will
log the public keys for later, higher assurance verification done face to face.
This also enables retrospective detection of MITM, if for some reason users are
unable to verify authenticity of public keys during key exchange.
Shared secret derivation
------------------------
TxM is the only device that receives no automatically input data after setup,
so it is the only device trusted to generate secret keys. Tx.py will generate
two symmetric keys (one for encryption, one for decryption) by concatenating
ECDHE shared secret with one of the public keys, and passing them through
PBKDF2-HMAC-SHA256 (25 000 iterations). This is mandatory for the forward
secrecy to work. Forward secrecy is obtained by passing the encryption keys
through PBKDF2-HMAC-SHA256 (1 000 iterations) between every message. Were the
ECDHE shared secret used as symmetric key for communication in both directions,
offset in key derivation could lead to decryption of collected ciphertexts by
iterating physically compromised key, that lacks behind in PBKDF2 iterations.
Once the symmetric keys are generated, the account details are encrypted and
signed with XSalsa20-Poly1305 using the local key, and transmitted to RxM,
where the contact is automatically added. While this packet doesn't differ in
any way from encrypted command, user can still choose to prevent NH from
receiving the ciphertext by plugging the TxM's data diode directly to RxM once
the public key of contact has been received to RxM. During this period any
message sent by contacts is not received. Dropped packets do not cause
de-synchronisation of keys, as each packet contains the number of times Tx.py
has iterated the initial symmetric encryption key with PBKDF2. This means both
receiving devices can catch up with the Tx.py's key. If Rx.py detects dropped
packets, it will display the catch up progress. KeyID introduces a DoS attack
vector where packets with great keyID blocks Rx.py from operating. This would
however require a MITM attack against the OTR, or messing with the NH; Such DoS
would blow the cover of the high-strength attacker. A more covert DoS can be
mitigated from within the network or IM server, so this attack is an unlikely
problem. Were this evaluation incorrect, an efficient fix would be to use a
separate static MAC key that signs the keyID. For now, it is not implemented.
It should be mentioned that this DoS attack can also be mitigated by frenemies,
but they are easy to block from the IM client.
Additional accounts can be added with commands
/dh <account>( <nick>)
and
/add <account>( <nick>)
PSK keys
--------
While TFC-NaCl is all about convenient public key crypto with exfiltration
secure private keys, it's security will not hold in the long run. Quantum
computers are making their way albeit slowly, and in the future any symmetric
key agreed using Curve25519 ECDHE will be broken. The only public key algorithm
secure against Shor's quantum algorithm is McEliece with Goppa Codes. Long term
security would require users to type in 1 000 000 bit public keys, which is
highly inconvenient regardless of encoding used. As an initial answer to
quantum computing, TFC-NaCl retains the possibility to create pre-shared
symmetric keys for quantum-secure 256-bit XSalsa20-Poly1305:
Answering yes to PSK question in the beginning will create symmetric keys with
PBKDF-HMAC-SHA256 (25 000 iterations), using SHA3-256(urandom(32)) as password,
and if HWRNG connected to Raspbian (SSH or not) is available, by using
SHA3-512(VN(256-bit HWRNG entropy)) as salt.
The contact's account, nick and the symmetric key for local decryption is sent
to RxM, encrypted with local key (again, NH doesn't have to be in between if
user so prefers). Tx.py then prompts user for his or her account name.
If Alice is adding bob@jabber.org as contact, a copy of her symmetric key is
generated for Bob, and conveniently placed inside folder "PSKs", under the name
rx.alice@jabber.org.e - Give this file to bob@jabber.org
Alice must then take a never before used thumb drive, copy the PSK to that and
give it to Bob in a face-to-face meeting. Bob must also have generated a PSK
for Alice in advance. Once Alice has received the thumb drive from Bob, she
plugs it into her RxM (NOT TxM), and copies the
rx.bob@jabber.org.e - Give this file to alice@jabber.org
keyfile to folder "keys". She must then restart Rx.py. Rx.py automatically
strips the trailing instruction from the keyfile and adds Bob as contact. Since
keys for encrypting messages to Bob are already installed, once Bob has copied
keyfile generated by Alice to his RxM, they are ready to communicate.
To ensure forward secrecy and privacy of messages, both parties MUST ensure the
thumb drive given by their contact is physically destroyed immediately after
keys have been added.
If contacts have already been generated, new PSKs can be generated with command
/psk <account>( <nick>)
--------------------------------------------------------------------------------
Other updates over TFC 0.5.5:
-Change of version style to 0.16.01 (0.YY.MM).
-Fully PEP8 compliant coding style/variable naming.
New local testing IPC
---------------------
Local testing of TFC with single computer now uses multiprocessing sockets
instead of files. This reduces IO errors also means less files in TFC root
directory.
Data diode simulators
---------------------
Added dd.py program that emulates data diodes on screen. User
needs to enable dd_sockets boolean in Tx.py and NH.py with argument -d
before this can take place. Each dd.py program is launched with a set of
arguments so that they know which sockets to use and to what direction data
visually flows between terminals. An example set of commands that launches TFC
on *buntu is
gnome-terminal --title='TxM' --geometry=100x35+0+630 -x sh -c "python /dir/Tx.py -d"
gnome-terminal --title='NH' --geometry=71x41+920+150 -x sh -c "python /dir/NH.py -d"
gnome-terminal --title='RxM' --geometry=100x20+0+0 -x sh -c "python /dir/Rx.py"
gnome-terminal --title='dd' --geometry=25x12+740+630 -x sh -c "python /dir/dd.py txnhlr"
gnome-terminal --title='dd' --geometry=25x12+740+425 -x sh -c "python /dir/dd.py nhrxlr"
Setting keyboard shortcut or alias for each of these makes local test startup
fast.
Serial interface auto-config
----------------------------
User's device configuration is asked during installation. This will configure
Tx.py, NH.py and Rx.py to look for either integrated ttyS# (or ttyAMA0 in the
case of Raspbian) serial interface, or for a USB interface (ttyUSB#) if user
decides to use those. The serial interface numbering is finally automatically
detected, so if user accidentally pulls out serial interface, user doesn't have
to manually edit the interface if OS remaps /dev/ttyUSB0 to /dev/ttyUSB1.
NH interface auto-flip
----------------------
Removed the need to run NH.py with -f flag to flip interfaces:
Tx.py will send a serial interface configuration packet to NH.py during start.
In the event NH.py has mapped TxM and RxM in opposite interfaces, initial
listener processes detect Tx.py's configuration packet (or any packet for that
matter), and map correct interfaces to TxM and RxM.
Organized files to folders
--------------------------
Moved location of group files, keyfiles, received files and logfiles to
respective folders. Renamed txc.tfc and rxc.tfc as dotfiles .tx_contacts and
.rx_contacts. The only TFC files visible in software root directory are .py
files (and syslog.tfc once it's first generated).
Added some metadata about TFC to packet headers
-----------------------------------------------
In future this prevents interoperation with older versions of TFC. It allows
fingerprinting of users (ONLY if NH is remotely exploited or OTR is being MITM
attacked), but forces users to keep up with security updates. New protocol uses
more simple and mature separation of payload components.
Re-designed trickle connection
------------------------------
input_process() now prepares all data to packets with length in range [0, 253]
and puts the data into message or file queue. The queue is periodically read by
sender_process() that opens a thread that padds, encrypts, signs, adds headers
and outputs messages. XSalsa20-Poly1305 algorithms are designed to run constant
time. The system time in ms is recorded immediately before function starts;
Once the function finishes, the timestamp is passed to function trickle_delay()
for evaluation. This function will then sleep the difference between Thread
runtime and value trickle_c_delay to hide platform speed. Platforms should not
introduce problems, as older Raspberry Pi (generation 2), only takes about
400ms to encrypt a message, leaving 1600ms headroom for even slower SoC boards.
Changed random sleep to use /dev/urandom instead of Python math.random. The
CSPRNG prevents statistical attacks that might be able to calculate internal
state of math.random() to detect whether communication is taking place. When
print_ct_stats boolean is enabled, Tx.py shows the statistics about how long
the message/command output thread ran, how long constant time sleep was added,
and how well the constant time matched trickle_c_delay: Average error in
constant time delay was 2ms so the CSPRNG based random sleep will hide the
slight differences effectively. Tx.py will gracefully exit if thread runtime
exceeds trickle_c_delay.
The packet_delay used to prevent congestion / IM server spam with long messages
is now also constant time, to prevent slower data transmission with slower
devices. Default value is 500ms which should be enough to hide metadata about
TxM device performance and make hardware fingerprinting harder. This also makes
prediction of file transfer duration easier.
Improved logging
----------------
Logging can now be enabled for individual contacts with command
/logging {on,off} <account>. When logging is enabled, in addition to automatic
logging of public keys, Tx.py will also log messages sent to contact. This
allows the two users to manually audit whether malware has at some point
infected RxM device(s) and substituted words in logfiles, by cross-comparing
the clean TxM logfile of Alice and purported RxM logfile of Bob, and vice
versa. Rx.py also stores information about dropped packets. Malware that
replaces received messages with notification about dropped packet is harder to
detect; offline record keeping might be required.
Tweakable checksums for data diode error detection
--------------------------------------------------
Changed CRC32 checksums to truncated SHA-256 hashes (8 hex = 32 bits) so
data diode error detection accuracy can be tweaked at will by changing
variable checksum_len. The checksum doesn't have any effect on security:
it only detects transmission errors inside datadiode.
Rewritten NH.py
---------------
Converted classes to functions to fit with procedural programming style. The
program is now much more structured and easier to audit. Added processes that
exchange data via queues. DBus / purple should no longer output error messages,
nor should they drop packets. NH multiprocessing now cleanly exits.
Re-designed file transmission.
------------------------------
/file 1.txt still sends the .txt file from TxM directory. Absolute paths can
also be defined. Non-TFC filenames in Tx.py directory are now included in
tab-complete list. Command "/file" opens a file dialog for easy file selection.
Tx.py will first encode file data to base64 and load it to memory. It'll
evaluate and display approximate amount of packets and time required for file
transfer, based on delay settings.
File data is prepended with a header that contains the name, extension, file
size and estimated number of packets and delivery time for transfer. This
information is displayed to recipient after first packet of file has been
received. File reception must be enabled enabled.
User can control global file reception by setting boolean file_saving to True.
/store {on,off} enables/disables file reception for all contacts
/store on alice@jabber.org enables file reception for just Alice
By default boolean setting a_close_f_recv is True. This means file reception
for Alice is closed automatically after file has been received. When the
variable is false, file storage will remain open until user enters
/store off alice@jabber.org
/store off
During trickle connection, messages and files can be cancelled with commands
'/cm' and '/cf'. Already sent data is out of control of user. Rx.py will
discard received data when it receives cancel packet / new message/file, but
this is a convenience feature, NOT a security feature. Sender-based control is
snake oil: Nothing prevents receiver from modifying their Rx.py to show/log
partially transmitted data. The only solution to this would be to offer closed
source version of TFC, which would defeat all security free software is
designed to offer.
Since the base64 encoding of transmitted files depends on TxM of contact, a
frenemy could still send malware to user's RxM. Thus, the entire manual
decoding feature was removed. File is no longer generated with subprocess.
This prevents shell injection with custom file names that would have otherwise
become possible.
File data during trickle connection is loaded from separate queue that has
lower priority than message queue. This means that users can still exchange
messages during file transmission: file data is output to contact when there
are no messages to output. File data is indistinguishable from messages to NH,
IM client, IM server and any adversary who might observe data from these
systems. During trickle connection, it is extremely hard to determine whether
user is sending noise data, messages or files. No guarantees can unfortunately
be made. Timing attacks are prone to side channel attacks anywhere from
NH/smartphone microphone to user moving the mouse on NH. Python is also a
high level language, which has it's own problems.
Command line arguments
----------------------
Added arguments for Tx.py, Rx.py and NH.py to control many settings from
command line. View arguments by launching program with -h/--help flag.
Hard coded packet length
------------------------
Packet length was hard coded to 254 to ensure everyone uses same value,
and to minimize chance for errors when sending encrypted commands. The
adjustable length was a legacy feature necessary only in TFC-OTP: Key material
is not a problem with symmetric crypto, thus adjusting key material expenditure
is no longer necessary.
Unittesting
-----------
Wrote almost 400 unittests (5600 LoC) for Tx.py, Rx.py and NH.py to reduce
the amount of bugs. Created custom error classes for some functions for this
purpose as well. Unittesting is a work in progress, so while TFC is more stable
than ever, no guarantees are made.
New startup banner animation
----------------------------
I want to thank Tom Wallroth for his awesome work. I had so much fun editing
his project to fit print_banner(). I'll leave the details of style as a
surprise, check it out.
Made edits to installer
-----------------------
Installer now uses SHA256 instead of SHA512. As collision resistance against
quantum computers is still 128 bits. While weaker, the security is as strong
as the 256-bit XSalsa20 used. The reason for this was simplified import of
file hashes when making changes.
Added installation configuration for HWRNG Pi that TxM connects to over SSH.
Ensured support for latest *buntu operating systems and fixed issues with
Lubuntu/Xubuntu in previous versions. Dropped NH support for Fedora and
OpenSUSE. Ensured NH configuration works with latest release of Tails (2.0).
Changed installer naming style, instead of tfcNaClinstaller.py, it's just
setup.py.