tfc-mirror/src/common/db_keys.py

384 lines
16 KiB
Python

#!/usr/bin/env python3.7
# -*- coding: utf-8 -*-
"""
TFC - Onion-routed, endpoint secure messaging system
Copyright (C) 2013-2020 Markus Ottela
This file is part of TFC.
TFC is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.
TFC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with TFC. If not, see <https://www.gnu.org/licenses/>.
"""
import os
import time
import typing
from typing import Any, Callable, Dict, List
from src.common.crypto import blake2b, csprng
from src.common.database import TFCDatabase
from src.common.encoding import int_to_bytes, onion_address_to_pub_key
from src.common.encoding import bytes_to_int
from src.common.exceptions import CriticalError
from src.common.misc import ensure_dir, separate_headers, split_byte_string
from src.common.statics import (DIR_USER_DATA, DUMMY_CONTACT, HARAC_LENGTH, INITIAL_HARAC, KDB_ADD_ENTRY_HEADER,
KDB_HALT_ACK_HEADER, KDB_M_KEY_CHANGE_HALT_HEADER, KDB_REMOVE_ENTRY_HEADER,
KDB_UPDATE_SIZE_HEADER, KEY_MANAGEMENT_QUEUE, KEY_MGMT_ACK_QUEUE, KEYSET_LENGTH,
LOCAL_PUBKEY, ONION_SERVICE_PUBLIC_KEY_LENGTH, RX, SYMMETRIC_KEY_LENGTH, TX)
if typing.TYPE_CHECKING:
from multiprocessing import Queue
from src.common.db_masterkey import MasterKey
from src.common.db_settings import Settings
QueueDict = Dict[bytes, Queue[Any]]
class KeySet(object):
"""\
KeySet object contains frequently changing keys and hash ratchet
counters of contacts:
onion_pub_key: The public key that corresponds to the contact's v3
Tor Onion Service address. Used to uniquely identify
the KeySet object.
tx_mk: Forward secret message key for sent messages.
rx_mk: Forward secret message key for received messages.
Used only by the Receiver Program.
tx_hk: Static header key used to encrypt and sign the hash
ratchet counter provided along the encrypted
assembly packet.
rx_hk: Static header key used to authenticate and decrypt
the hash ratchet counter of received messages. Used
only by the Receiver Program.
tx_harac: The hash ratchet counter for sent messages.
rx_harac: The hash ratchet counter for received messages. Used
only by the Receiver Program.
"""
def __init__(self,
onion_pub_key: bytes,
tx_mk: bytes,
rx_mk: bytes,
tx_hk: bytes,
rx_hk: bytes,
tx_harac: int,
rx_harac: int,
store_keys: Callable[..., None]
) -> None:
"""Create a new KeySet object.
The `self.store_keys` is a reference to the method of the parent
object KeyList that stores the list of KeySet objects into an
encrypted database.
"""
self.onion_pub_key = onion_pub_key
self.tx_mk = tx_mk
self.rx_mk = rx_mk
self.tx_hk = tx_hk
self.rx_hk = rx_hk
self.tx_harac = tx_harac
self.rx_harac = rx_harac
self.store_keys = store_keys
def serialize_k(self) -> bytes:
"""Return KeySet data as a constant length byte string.
This function serializes the KeySet's data into a byte string
that has the exact length of 32 + 4*32 + 2*8 = 176 bytes. The
length is guaranteed regardless of the content of the
attributes' values. The purpose of the constant length
serialization is to hide any metadata about the KeySet database
the ciphertext length of the key database would reveal.
"""
return (self.onion_pub_key
+ self.tx_mk
+ self.rx_mk
+ self.tx_hk
+ self.rx_hk
+ int_to_bytes(self.tx_harac)
+ int_to_bytes(self.rx_harac))
def rotate_tx_mk(self) -> None:
"""\
Update Transmitter Program's tx-message key and tx-harac.
Replacing the key with its hash provides per-message forward
secrecy for sent messages. The hash ratchet used is also known
as the SCIMP Ratchet[1], and it is widely used, e.g., as part of
Signal's Double Ratchet[2].
To ensure the hash ratchet does not fall into a short cycle of
keys, the harac (that is a non-repeating value) is used as an
additional input when deriving the next key.
[1] (pp. 17-18) https://netzpolitik.org/wp-upload/SCIMP-paper.pdf
[2] https://signal.org/blog/advanced-ratcheting/
"""
self.tx_mk = blake2b(self.tx_mk + int_to_bytes(self.tx_harac), digest_size=SYMMETRIC_KEY_LENGTH)
self.tx_harac += 1
self.store_keys()
def update_mk(self,
direction: str,
key: bytes,
offset: int
) -> None:
"""Update Receiver Program's tx/rx-message key and tx/rx-harac.
This method provides per-message forward secrecy for received
messages. Due to the possibility of dropped packets, the
Receiver Program might have to jump over some key values and
ratchet counter states. Therefore, the increase done by this
function is not linear like in the case of `rotate_tx_mk`.
"""
if direction == TX:
self.tx_mk = key
self.tx_harac += offset
self.store_keys()
elif direction == RX:
self.rx_mk = key
self.rx_harac += offset
self.store_keys()
else:
raise CriticalError("Invalid key direction.")
class KeyList(object):
"""\
KeyList object manages TFC's KeySet objects and the storage of the
objects in an encrypted database.
The main purpose of this object is to manage the `self.keysets`-list
that contains TFC's keys. The database is stored on disk in
encrypted form. Prior to encryption, the database is padded with
dummy KeySets. The dummy KeySets hide the number of actual KeySets
and thus the number of contacts, that would otherwise be revealed by
the size of the encrypted database. As long as the user has less
than 50 contacts, the database will effectively hide the actual
number of contacts.
The KeySet database is separated from contact database as traffic
masking needs to update keys frequently with no risk of read/write
queue blocking that occurs, e.g., when an updated nick of contact is
being stored in the database.
"""
def __init__(self, master_key: 'MasterKey', settings: 'Settings') -> None:
"""Create a new KeyList object."""
self.master_key = master_key
self.settings = settings
self.keysets = [] # type: List[KeySet]
self.dummy_keyset = self.generate_dummy_keyset()
self.dummy_id = self.dummy_keyset.onion_pub_key
self.file_name = f'{DIR_USER_DATA}{settings.software_operation}_keys'
self.database = TFCDatabase(self.file_name, master_key)
ensure_dir(DIR_USER_DATA)
if os.path.isfile(self.file_name):
self._load_keys()
else:
self.store_keys()
def store_keys(self, replace: bool = True) -> None:
"""Write the list of KeySet objects to an encrypted database.
This function will first create a list of KeySets and dummy
KeySets. It will then serialize every KeySet object on that list
and join the constant length byte strings to form the plaintext
that will be encrypted and stored in the database.
By default, TFC has a maximum number of 50 contacts. In
addition, the database stores the KeySet used to encrypt
commands from Transmitter to Receiver Program). The plaintext
length of 51 serialized KeySets is 51*176 = 8976 bytes. The
ciphertext includes a 24-byte nonce and a 16-byte tag, so the
size of the final database is 9016 bytes.
"""
pt_bytes = b''.join([k.serialize_k() for k in self.keysets + self._dummy_keysets()])
self.database.store_database(pt_bytes, replace)
def _load_keys(self) -> None:
"""Load KeySets from the encrypted database.
This function first reads and decrypts the database content. It
then splits the plaintext into a list of 176-byte blocks. Each
block contains the serialized data of one KeySet. Next, the
function will remove from the list all dummy KeySets (that start
with the `dummy_id` byte string). The function will then
populate the `self.keysets` list with KeySet objects, the data
of which is sliced and decoded from the dummy-free blocks.
"""
pt_bytes = self.database.load_database()
blocks = split_byte_string(pt_bytes, item_len=KEYSET_LENGTH)
df_blocks = [b for b in blocks if not b.startswith(self.dummy_id)]
for block in df_blocks:
if len(block) != KEYSET_LENGTH:
raise CriticalError("Invalid data in key database.")
onion_pub_key, tx_mk, rx_mk, tx_hk, rx_hk, tx_harac_bytes, rx_harac_bytes \
= separate_headers(block, [ONION_SERVICE_PUBLIC_KEY_LENGTH] + 4*[SYMMETRIC_KEY_LENGTH] + [HARAC_LENGTH])
self.keysets.append(KeySet(onion_pub_key=onion_pub_key,
tx_mk=tx_mk,
rx_mk=rx_mk,
tx_hk=tx_hk,
rx_hk=rx_hk,
tx_harac=bytes_to_int(tx_harac_bytes),
rx_harac=bytes_to_int(rx_harac_bytes),
store_keys=self.store_keys))
@staticmethod
def generate_dummy_keyset() -> 'KeySet':
"""Generate a dummy KeySet object.
The dummy KeySet simplifies the code around the constant length
serialization when the data is stored to, or read from the
database.
In case the dummy keyset would ever be loaded accidentally, it
uses a set of random keys to prevent decryption by eavesdropper.
"""
return KeySet(onion_pub_key=onion_address_to_pub_key(DUMMY_CONTACT),
tx_mk=csprng(),
rx_mk=csprng(),
tx_hk=csprng(),
rx_hk=csprng(),
tx_harac=INITIAL_HARAC,
rx_harac=INITIAL_HARAC,
store_keys=lambda: None)
def _dummy_keysets(self) -> List[KeySet]:
"""\
Generate a proper size list of dummy KeySets for database
padding.
The additional contact (+1) is the local key.
"""
number_of_contacts_to_store = self.settings.max_number_of_contacts + 1
number_of_dummies = number_of_contacts_to_store - len(self.keysets)
return [self.dummy_keyset] * number_of_dummies
def add_keyset(self,
onion_pub_key: bytes,
tx_mk: bytes,
rx_mk: bytes,
tx_hk: bytes,
rx_hk: bytes) -> None:
"""\
Add a new KeySet to `self.keysets` list and write changes to the
database.
"""
if self.has_keyset(onion_pub_key):
self.remove_keyset(onion_pub_key)
self.keysets.append(KeySet(onion_pub_key=onion_pub_key,
tx_mk=tx_mk,
rx_mk=rx_mk,
tx_hk=tx_hk,
rx_hk=rx_hk,
tx_harac=INITIAL_HARAC,
rx_harac=INITIAL_HARAC,
store_keys=self.store_keys))
self.store_keys()
def remove_keyset(self, onion_pub_key: bytes) -> None:
"""\
Remove KeySet from `self.keysets` based on Onion Service public key.
If the KeySet was found and removed, write changes to the database.
"""
for i, k in enumerate(self.keysets):
if k.onion_pub_key == onion_pub_key:
del self.keysets[i]
self.store_keys()
break
def change_master_key(self, queues: 'QueueDict') -> None:
"""Change the master key and encrypt the database with the new key."""
key_queue = queues[KEY_MANAGEMENT_QUEUE]
ack_queue = queues[KEY_MGMT_ACK_QUEUE]
# Halt sender loop here until keys have been replaced by the
# `input_loop` process, and new master key is delivered.
ack_queue.put(KDB_HALT_ACK_HEADER)
while key_queue.qsize() == 0:
time.sleep(0.001)
new_master_key = key_queue.get()
# Replace master key.
self.database.database_key = new_master_key
self.master_key.master_key = new_master_key
# Send new master key back to `input_loop` process to verify it was received.
ack_queue.put(new_master_key)
def update_database(self, settings: 'Settings') -> None:
"""Update settings and database size."""
self.settings = settings
self.store_keys()
def get_keyset(self, onion_pub_key: bytes) -> KeySet:
"""\
Return KeySet object from `self.keysets`-list that matches the
Onion Service public key used as the selector.
"""
return next(k for k in self.keysets if k.onion_pub_key == onion_pub_key)
def get_list_of_pub_keys(self) -> List[bytes]:
"""Return list of Onion Service public keys for KeySets."""
return [k.onion_pub_key for k in self.keysets if k.onion_pub_key != LOCAL_PUBKEY]
def has_keyset(self, onion_pub_key: bytes) -> bool:
"""Return True if KeySet with matching Onion Service public key exists, else False."""
return any(onion_pub_key == k.onion_pub_key for k in self.keysets)
def has_rx_mk(self, onion_pub_key: bytes) -> bool:
"""\
Return True if KeySet with matching Onion Service public key has
rx-message key, else False.
When the PSK key exchange option is selected, the KeySet for
newly created contact on Receiver Program is a null-byte string.
This default value indicates the PSK of contact has not yet been
imported.
"""
return self.get_keyset(onion_pub_key).rx_mk != bytes(SYMMETRIC_KEY_LENGTH)
def has_local_keyset(self) -> bool:
"""Return True if local KeySet object exists, else False."""
return any(k.onion_pub_key == LOCAL_PUBKEY for k in self.keysets)
def manage(self, queues: 'QueueDict', command: str, *params: Any) -> None:
"""Manage KeyList based on a command.
The command is delivered from `input_process` to `sender_loop`
process via the `KEY_MANAGEMENT_QUEUE`.
"""
if command == KDB_ADD_ENTRY_HEADER:
self.add_keyset(*params)
elif command == KDB_REMOVE_ENTRY_HEADER:
self.remove_keyset(*params)
elif command == KDB_M_KEY_CHANGE_HALT_HEADER:
self.change_master_key(queues)
elif command == KDB_UPDATE_SIZE_HEADER:
self.update_database(*params)
else:
raise CriticalError(f"Invalid KeyList management command '{command}'.")