tfc-mirror/src/common/crypto.py

480 lines
20 KiB
Python
Executable File

#!/usr/bin/env python3.6
# -*- coding: utf-8 -*-
"""
TFC - Onion-routed, endpoint secure messaging system
Copyright (C) 2013-2019 Markus Ottela
This file is part of TFC.
TFC is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.
TFC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with TFC. If not, see <https://www.gnu.org/licenses/>.
---
This module contains TFC's cryptographic functions. Most algorithms are
based on the ChaCha20 stream cipher by Daniel J. Bernstein (djb).
X448
ChaCha20
├─ Linux kernel CSPRNG
├─ XChaCha20-Poly1305 (IETF) AEAD
└─ BLAKE2b cryptographic hash function
└─ Argon2d key derivation function
"""
import hashlib
import os
import argon2
import nacl.bindings
import nacl.exceptions
import nacl.secret
import nacl.utils
from cryptography.hazmat.primitives.asymmetric.x448 import X448PrivateKey, X448PublicKey
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from src.common.exceptions import CriticalError
from src.common.misc import ignored, separate_header
from src.common.output import m_print, phase, print_on_previous_line
from src.common.statics import *
def blake2b(message: bytes, # Message to hash
key: bytes = b'', # Key for keyed hashing
salt: bytes = b'', # Salt for randomized hashing
person: bytes = b'', # Personalization string
digest_size: int = BLAKE2_DIGEST_LENGTH # Length of the digest
) -> bytes: # The BLAKE2b digest
"""Generate BLAKE2b digest (i.e. cryptographic hash) of a message.
BLAKE2 is the successor of SHA3-finalist BLAKE*, designed by
Jean-Philippe Aumasson, Samuel Neves, Zooko Wilcox-O'Hearn and
Christian Winnerlein. The hash function is based on the ChaCha20
stream cipher, designed by djb.
* BLAKE was designed by Jean-Philippe Aumasson, Luca Henzen,
Willi Meier, and Raphael C.-W. Phan.
For more details, see
https://blake2.net/
https://leastauthority.com/blog/BLAKE2-harder-better-faster-stronger-than-MD5/
The reasons for using BLAKE2b in TFC include
o BLAKE received* more in-depth cryptanalysis than Keccak (SHA3):
"Keccak received a significant amount of cryptanalysis,
although not quite the depth of analysis applied to BLAKE,
Grøstl, or Skein."
(https://nvlpubs.nist.gov/nistpubs/ir/2012/NIST.IR.7896.pdf # p. 13)
* https://blake2.net/#cr
o BLAKE shares design elements with SHA-2 that has 16 years of
cryptanalysis behind it.
(https://en.wikipedia.org/wiki/SHA-2#Cryptanalysis_and_validation)
o 128-bit collision/preimage/second-preimage resistance against
Grover's algorithm running on a quantum Turing machine.
o The algorithm is bundled in Python3.6's hashlib.
o Compared to SHA3-256, the algorithm runs faster on CPUs which
means better hash ratchet performance.
o Compared to SHA3-256, the algorithm runs slower on ASICs which
means attacks by high-budget adversaries are slower.
Note that while the default length of BLAKE2b (the implementation
optimized for AMD64 systems) digest is 512 bits, the digest size is
truncated to 256 bits for the use in TFC.
The correctness of the BLAKE2b implementation* is tested by TFC unit
tests. The testing is done in limited scope by using an official KAT.
* https://github.com/python/cpython/tree/3.6/Modules/_blake2
https://github.com/python/cpython/blob/3.6/Lib/hashlib.py
"""
return hashlib.blake2b(message, digest_size=digest_size, key=key, salt=salt, person=person).digest()
def argon2_kdf(password: str, # Password to derive the key from
salt: bytes, # Salt to derive the key from
rounds: int = ARGON2_ROUNDS, # Number of iterations
memory: int = ARGON2_MIN_MEMORY, # Amount of memory to use (in bytes)
parallelism: int = 1 # Number of threads to use
) -> bytes: # The derived key
"""Derive an encryption key from password and salt using Argon2d.
Argon2 is a key derivation function (KDF) designed by Alex Biryukov,
Daniel Dinu, and Dmitry Khovratovich from the University of
Luxembourg. The algorithm is the winner of the 2015 Password Hashing
Competition (PHC).
For more details, see
https://password-hashing.net/
https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf
https://en.wikipedia.org/wiki/Argon2
The purpose of the KDF is to stretch a password into a 256-bit key.
Argon2 features a slow, memory-hard hash function that consumes
computational resources of an attacker that attempts a dictionary
or a brute force attack. The accompanied 256-bit salt prevents
rainbow-table attacks, forcing each attack to take place against an
individual (physically compromised) TFC-endpoint, or PSK
transmission media.
The used Argon2 version is Argon2d that uses data-dependent memory
access, which maximizes security against time-memory trade-off
(TMTO) attacks at the risk of side-channel attacks. The IETF
recommends using Argon2id (that is side-channel resistant and almost
as secure as Argon2d against TMTO attacks) **except** when there is
a reason to prefer Argon2d (or Argon2i). The reason TFC uses Argon2d
is key derivation only takes place on Source and Destination
Computer. As these computers are connected to the Networked Computer
only via a data diode, they do not leak any information via
side-channels to the adversary. The expected attacks are against
physically compromised data storage devices where the encrypted data
is at rest. In such situation, Argon2d is the most secure option.
The correctness of the Argon2d implementation* is tested by TFC unit
tests. The testing is done in limited scope by using an official KAT.
* https://github.com/P-H-C/phc-winner-argon2
https://github.com/hynek/argon2_cffi
"""
assert len(salt) == ARGON2_SALT_LENGTH
key = argon2.low_level.hash_secret_raw(secret=password.encode(),
salt=salt,
time_cost=rounds,
memory_cost=memory,
parallelism=parallelism,
hash_len=SYMMETRIC_KEY_LENGTH,
type=argon2.Type.D) # type: bytes
return key
class X448(object):
"""
X448 is the Diffie-Hellman function for Curve448-Goldilocks, a
state-of-the-art elliptical curve designed by Mike Hamburg in 2014:
https://eprint.iacr.org/2015/625.pdf
The reasons for using X448 in TFC include
o It meets the criterion for a safe curve.
(https://safecurves.cr.yp.to/)
o NIST has announced X448 will be included in the SP 800-186.
(https://csrc.nist.gov/News/2017/Transition-Plans-for-Key-Establishment-Schemes)
o It provides conservative 224 bits of symmetric security.
o It is immune against invalid curve attacks: Its public keys do
not require validation as long as the public key is not zero.
o Its public keys are reasonably short (84 Base58 chars) to be
manually typed from Networked Computer to Source Computer.
The correctness of the X448 implementation* is tested by TFC unit
tests. The testing is done in limited scope by using official test
vectors.
* https://github.com/openssl/openssl/tree/OpenSSL_1_1_1-stable/crypto/ec/curve448
https://github.com/pyca/cryptography/blob/master/src/cryptography/hazmat/primitives/asymmetric/x448.py
"""
@staticmethod
def generate_private_key() -> 'X448PrivateKey':
"""Generate the X448 private key.
The size of the private key is 56 bytes (448 bits).
"""
return X448PrivateKey.generate()
@staticmethod
def derive_public_key(private_key: 'X448PrivateKey') -> bytes:
"""Derive public key from X448 private key."""
public_key = private_key.public_key().public_bytes(encoding=Encoding.Raw,
format=PublicFormat.Raw) # type: bytes
return public_key
@staticmethod
def shared_key(private_key: 'X448PrivateKey', public_key: bytes) -> bytes:
"""Derive the X448 shared key.
Since the shared secret is zero if contact's public key is zero,
this function asserts the public key is a valid non-zero
bytestring.
Because the raw bits of the X448 shared secret might not be
uniformly distributed in the keyspace (i.e. bits might have bias
towards 0 or 1), the raw shared secret is passed through BLAKE2b
CSPRF to ensure uniformly random shared key.
"""
assert len(public_key) == TFC_PUBLIC_KEY_LENGTH
assert public_key != bytes(TFC_PUBLIC_KEY_LENGTH)
shared_secret = private_key.exchange(X448PublicKey.from_public_bytes(public_key))
return blake2b(shared_secret, digest_size=SYMMETRIC_KEY_LENGTH)
def encrypt_and_sign(plaintext: bytes, # Plaintext to encrypt
key: bytes, # 32-byte symmetric key
ad: bytes = b'' # Associated data
) -> bytes: # Nonce + ciphertext + tag
"""Encrypt plaintext with XChaCha20-Poly1305.
ChaCha20 is a stream cipher published by Daniel J. Bernstein (djb)
in 2008. The algorithm is an improved version of Salsa20 -- another
stream cipher by djb -- selected by ECRYPT into the eSTREAM
portfolio in 2008. The improvement in question is, ChaCha20
increases the per-round diffusion compared to Salsa20 while
maintaining or increasing speed.
For more details, see
https://cr.yp.to/chacha/chacha-20080128.pdf
https://en.wikipedia.org/wiki/Salsa20#ChaCha_variant
The Poly1305 is a Wegman-Carter Message Authentication Code also
designed by djb. The MAC is provably secure if ChaCha20 is secure.
The 128-bit tag space ensures the attacker's advantage to create an
existential forgery is negligible.
For more details, see
https://cr.yp.to/mac.html
The version used in TFC is the XChaCha20-Poly1305-IETF*, a variant
of the ChaCha20-Poly1305-IETF (RFC 7539**). Quoting libsodium, the
XChaCha20 (=eXtended-nonce ChaCha20) variant allows encryption of
~2^64 bytes per message, encryption of up to 2^64 messages per key,
and safe use of random nonces due to the 192-bit nonce space***.
* https://tools.ietf.org/html/draft-arciszewski-xchacha-00
** https://tools.ietf.org/html/rfc7539
*** https://download.libsodium.org/doc/secret-key_cryptography/aead/chacha20-poly1305#variants
The reasons for using XChaCha20-Poly1305 in TFC include
o The Salsa20 algorithm has 14 years of cryptanalysis behind it.
(https://en.wikipedia.org/wiki/Salsa20#Cryptanalysis_of_Salsa20)
o The increased diffusion over the well-received Salsa20.
o The algorithm is much faster compared to AES (in cases where
the CPU and/or implementation does not support AES-NI).
o Security against cache-timing attacks on all CPUs (unlike AES
on CPUs without AES-NI).
o The good name of djb.
The correctness of the XChaCha20-Poly1305 implementation* is tested
by TFC unit tests. The testing is done in limited scope by using
libsodium and IETF test vectors.
* https://github.com/jedisct1/libsodium/tree/master/src/libsodium/crypto_aead/xchacha20poly1305/sodium
https://github.com/pyca/pynacl/blob/master/src/nacl/bindings/crypto_aead.py
"""
assert len(key) == SYMMETRIC_KEY_LENGTH
nonce = csprng(XCHACHA20_NONCE_LENGTH)
ct_tag = nacl.bindings.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, ad, nonce, key) # type: bytes
return nonce + ct_tag
def auth_and_decrypt(nonce_ct_tag: bytes, # Nonce + ciphertext + tag
key: bytes, # 32-byte symmetric key
database: str = '', # When provided, gracefully exists TFC when the tag is invalid
ad: bytes = b'' # Associated data
) -> bytes: # Plaintext
"""Authenticate and decrypt XChaCha20-Poly1305 ciphertext.
The Poly1305 tag is checked using constant time `sodium_memcmp`:
https://download.libsodium.org/doc/helpers#constant-time-test-for-equality
When TFC decrypts ciphertext from an untrusted source (i.e., a
contact), no `database` parameter is provided. In such situation, if
the tag of the untrusted ciphertext is invalid, TFC discards the
ciphertext and recovers appropriately.
When TFC decrypts ciphertext from a trusted source (i.e., a
database), the `database` parameter is provided, so the function
knows which database is in question. In case the authentication
fails due to invalid tag, the data is assumed to be either tampered
or corrupted. TFC will in such case gracefully exit to avoid
processing the unsafe data and warn the user in which database the
issue was detected.
"""
assert len(key) == SYMMETRIC_KEY_LENGTH
nonce, ct_tag = separate_header(nonce_ct_tag, XCHACHA20_NONCE_LENGTH)
try:
plaintext = nacl.bindings.crypto_aead_xchacha20poly1305_ietf_decrypt(ct_tag, ad, nonce, key) # type: bytes
return plaintext
except nacl.exceptions.CryptoError:
if database:
raise CriticalError(f"Authentication of data in database '{database}' failed.")
raise
def byte_padding(bytestring: bytes # Bytestring to be padded
) -> bytes: # Padded bytestring
"""Pad bytestring to next 255 bytes.
TFC adds padding to messages it outputs. The padding ensures each
assembly packet has a constant length. When traffic masking is
disabled, because of padding the packet length reveals only the
maximum length of the compressed message.
When traffic masking is enabled, the padding contributes to traffic
flow confidentiality: During traffic masking, TFC will output a
constant stream of padded packets at constant intervals that hides
metadata about message length (i.e., the adversary won't be able to
distinguish when transmission of packet or series of packets starts
and stops), as well as the type (message/file) of transferred data.
TFC uses PKCS #7 padding scheme described in RFC 2315 and RFC 5652:
https://tools.ietf.org/html/rfc2315#section-10.3
https://tools.ietf.org/html/rfc5652#section-6.3
For a better explanation, see
https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS#5_and_PKCS#7
"""
padding_len = PADDING_LENGTH - (len(bytestring) % PADDING_LENGTH)
bytestring += padding_len * bytes([padding_len])
assert len(bytestring) % PADDING_LENGTH == 0
return bytestring
def rm_padding_bytes(bytestring: bytes # Bytestring from which padding is removed
) -> bytes: # Bytestring without padding
"""Remove padding from plaintext.
The length of padding is determined by the ord-value of the last
byte that is always part of the padding.
"""
length = ord(bytestring[-1:])
return bytestring[:-length]
def csprng(key_length: int = SYMMETRIC_KEY_LENGTH) -> bytes:
"""Generate a cryptographically secure random key.
The default key length is 256 bits.
The key is generated by the Linux kernel's cryptographically secure
pseudo-random number generator (CSPRNG).
Since Python 3.6.0, `os.urandom` is a wrapper for best available
CSPRNG. The 3.17 and earlier versions of Linux kernel do not support
the GETRANDOM call, and Python 3.6's `os.urandom` will in those
cases fall back to non-blocking `/dev/urandom` that is not secure on
live distros as they have low entropy at the start of the session.
TFC uses `os.getrandom(n, flags=0)` explicitly. This forces use of
recent enough Python interpreter (3.6.0 or later) and limits Linux
kernel version to 3.17 or later.* The flag 0 will block urandom if
the internal state of the CSPRNG has less than 128 bits of entropy.
See PEP 524 for more details:
https://www.python.org/dev/peps/pep-0524/
* The `/dev/urandom` was redesigned around ChaCha20 in the version
4.8 of Linux kernel (https://lwn.net/Articles/686033/), so as a
good practice TFC runs the `check_kernel_version` to ensure only
the new design of the CSPRNG is used.
Quoting PEP 524:
"The os.getrandom() is a thin wrapper on the getrandom()
syscall/C function and so inherit of its behaviour. For
example, on Linux, it can return less bytes than
requested if the syscall is interrupted by a signal."
However, quoting (https://lwn.net/Articles/606141/) on GETRANDOM:
"--reads of 256 bytes or less from /dev/urandom are guaranteed to
return the full request once that device has been initialized."
Since the largest key generated in TFC is the 56-byte X448 private
key, GETRANDOM is guaranteed to always work. As a good practice
however, TFC asserts that the length of the obtained entropy is
correct.
The output of GETRANDOM is further compressed with BLAKE2b. The
preimage resistance of the hash function protects the internal
state of the entropy pool just in case some user decides to modify
the source to accept pre-4.8 Linux Kernel that has no backtracking
protection. Another reason for the hashing is its recommended by djb:
https://media.ccc.de/v/32c3-7210-pqchacks#video&t=1116
Since BLAKE2b only produces 1..64 byte digests, its use limits the
size of the key to 64 bytes. This is not a problem for TFC because
again, the largest key it generates is the 56-byte X448 private key.
"""
assert key_length <= BLAKE2_DIGEST_LENGTH_MAX
entropy = os.getrandom(key_length, flags=0)
assert len(entropy) == key_length
compressed = blake2b(entropy, digest_size=key_length)
assert len(compressed) == key_length
return compressed
def check_kernel_entropy() -> None:
"""Wait until the kernel CSPRNG is sufficiently seeded.
Wait until the `entropy_avail` file states that kernel entropy pool
has at least 512 bits of entropy. The waiting ensures the ChaCha20
CSPRNG is fully seeded (i.e., it has the maximum of 384 bits of
entropy) when it generates keys. The same entropy threshold is used
by the GETRANDOM syscall in random.c:
#define CRNG_INIT_CNT_THRESH (2*CHACHA20_KEY_SIZE)
For more information on the kernel CSPRNG threshold, see
https://security.stackexchange.com/a/175771/123524
https://crypto.stackexchange.com/a/56377
"""
message = "Waiting for kernel CSPRNG entropy pool to fill up"
phase(message, head=1)
ent_avail = 0
while ent_avail < ENTROPY_THRESHOLD:
with ignored(EOFError, KeyboardInterrupt):
with open('/proc/sys/kernel/random/entropy_avail') as f:
ent_avail = int(f.read().strip())
m_print(f"{ent_avail}/{ENTROPY_THRESHOLD}")
print_on_previous_line(delay=0.1)
print_on_previous_line()
phase(message)
phase(DONE)
def check_kernel_version() -> None:
"""Check that the Linux kernel version is at least 4.8.
This check ensures that TFC only runs on Linux kernels that use the
new ChaCha20 based CSPRNG that among many things, adds backtracking
protection:
https://lkml.org/lkml/2016/7/25/43
"""
major_v, minor_v = [int(i) for i in os.uname()[2].split('.')[:2]]
if major_v < 4 or (major_v == 4 and minor_v < 8):
raise CriticalError("Insecure kernel CSPRNG version detected.")