GitHub - freedomofpress/securedrop-protocol: Research and proof of concept to develop the next SecureDrop with end to end encryption. (original) (raw)

SecureDrop Protocol

Version
Implementation (here) 0.1
Specification 0.2

Status

Warning

This is proof-of-concept code and is not intended for production use. The protocol details are not yet finalized.

January 2025: A formal analysis was performed byLuca Maier inhttps://github.com/lumaier/securedrop-formalanalysis and published as "A Formal Analysis of the SecureDrop Protocol", supervised by David Basin, Felix Linker, and Shannon Veitch in the Information Security Group at ETH Zürich.

December 2023: A preliminary cryptographic audit was performed byMichele Orrù. See#36.

Background

To better understand the context of this research and the previous steps that led to it, read the following blog posts:

Repository overview

The code in this repository implements three components:

In this proof-of-concept implementation, the components are not fully separated; for example, commons.py includes code and configuration shared between all components.

Data is persisted in the following ways:

See the documentation for the protocol's architecture, threat model, and specification.

Another PoC server implementation in Lua is available in the securedrop-protocol-server-resty repository.

Installation (Fedora)

Install dependencies and create the virtual environment.

sudo dnf install redis
sudo systemctl start redis
python3 -m virtualenv .venv
source .venv/bin/activate
pip3 install -r requirements.txt

Generate the FPF root key, the intermediate key, and the journalists' long term keys, and sign them all hierarchically.

Run the server:

FLASK_DEBUG=1 flask --app server run

Impersonate the journalists and generate ephemeral keys for each of them. Upload all the public keys and their signature to the server.

for i in <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">(</mo><mi>s</mi><mi>e</mi><mi>q</mi><mn>09</mn><mo stretchy="false">)</mo><mo separator="true">;</mo><mi>d</mi><mi>o</mi><mi>p</mi><mi>y</mi><mi>t</mi><mi>h</mi><mi>o</mi><mi>n</mi><mn>3</mn><mi>j</mi><mi>o</mi><mi>u</mi><mi>r</mi><mi>n</mi><mi>a</mi><mi>l</mi><mi>i</mi><mi>s</mi><mi>t</mi><mi mathvariant="normal">.</mi><mi>p</mi><mi>y</mi><mo>−</mo><mi>j</mi></mrow><annotation encoding="application/x-tex">(seq 0 9); do python3 journalist.py -j </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord mathnormal">se</span><span class="mord mathnormal" style="margin-right:0.03588em;">q</span><span class="mord">09</span><span class="mclose">)</span><span class="mpunct">;</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">d</span><span class="mord mathnormal">o</span><span class="mord mathnormal">p</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord mathnormal">o</span><span class="mord mathnormal">n</span><span class="mord">3</span><span class="mord mathnormal" style="margin-right:0.05724em;">j</span><span class="mord mathnormal">o</span><span class="mord mathnormal">u</span><span class="mord mathnormal" style="margin-right:0.02778em;">r</span><span class="mord mathnormal">na</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">i</span><span class="mord mathnormal">s</span><span class="mord mathnormal">t</span><span class="mord">.</span><span class="mord mathnormal">p</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.854em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.05724em;">j</span></span></span></span>i -a upload_keys; done;

Call/caller charts can be generated with make docs.

Config

In commons.py there are the following configuration values which are global for all components, even though not all parties need all of them.

Variable Value Components Description
SERVER 127.0.0.1:5000 source, journalist The URL the Flask server listens on; used by both the journalist and the source clients.
DIR keys/ server, source, journalist The folder where everybody will load the keys from. There is no separation for demo simplicity but in an actual implementation everybody will only have their keys and the required public one to ensure the trust chain.
UPLOADS files/ server The folder where the Flask server will store uploaded files
JOURNALISTS 10 server, source How many journalists do we create and enroll. In general, this is realistic; in current SecureDrop usage it is typically a smaller number. For demo purposes everybody knows this, in a real scenario it would not be needed.
ONETIMEKEYS 30 journalist How many ephemeral keys each journalist creates, signs and uploads when required.
MAX_MESSAGES 500 server How many potential messages the server sends to each party when they try to fetch messages. This basically must be more than the messages in the database, otherwise we need to develop a mechanism to group messages adding some bits of metadata.
CHUNK 512 * 1024 source The base size of every part which attachments are split into or padded to. This is not the actual size on disk; that will be a bit larger depending on the nacl SecretBox implementation.

The following parameters are not currently configurable or used but should be in a production implementation:

Variable Value Components Description
SOURCE_PASSPHRASE_DICTIONARY_MIN_SIZE 7300 components Require that DICTIONARY_SIZE >= SOURCE_PASSPHRASE_DICTIONARY_MIN_SIZE. Inherited from current SecureDrop.
SOURCE_PASSPHRASE_ENTROPY_MIN_BITS 89 source Target for textttSOURCEPASSPHRASEWORDSNUMtimeslog2(textttDICTIONARYSIZE)\texttt{SOURCE\_PASSPHRASE\_WORDS\_NUM} \times \log_2(\texttt{DICTIONARY\_SIZE})textttSOURCEPASSPHRASEWORDSNUMtimeslog2(textttDICTIONARYSIZE).
SOURCE_PASSPHRASE_WORDS_NUM 7 source Inherited from current SecureDrop.

Demo

The demo script will clean past keys and files, flush Redis, generate a new PKI, start the server, generate and upload journalists and simulate submissions and replies from different sources/journalists.

Usage

Source

Help

# python3 source.py -h
usage: source.py [-h] [-p PASSPHRASE] -a {fetch,read,reply,submit,delete} [-i ID] [-m MESSAGE] [-f FILES [FILES ...]]

options:
  -h, --help            show this help message and exit
  -p PASSPHRASE, --passphrase PASSPHRASE
                        Source passphrase if returning
  -a {fetch,read,reply,submit,delete}, --action {fetch,read,reply,submit,delete}
                        Action to perform
  -i ID, --id ID        Message id
  -m MESSAGE, --message MESSAGE
                        Plaintext message content for submissions or replies
  -f FILES [FILES ...], --files FILES [FILES ...]
                        List of local files to submit

Send a submission (without attachments)

# python3 source.py -a submit -m "My first contact message with a newsroom :)"
[+] New submission passphrase: 23a90f6499c5f3bc630e7103a4e63c131a8248c1ae5223541660b7bcbda8b2a9

Send a submission (with attachments)

# python3 source.py -a submit -m "My first contact message with a newsroom, plus evidence and a supporting video :)" -f /tmp/secret_files/file1.mkv /tmp/secret_files/file2.zip
[+] New submission passphrase: c2cf422563cd2dc2813150faf2f40cf6c2032e3be6d57d1cd4737c70925743f6

Fetch replies

# python3 source.py -p 23a90f6499c5f3bc630e7103a4e63c131a8248c1ae5223541660b7bcbda8b2a9 -a fetch
[+] Found 1 message(s)
    de55e92ca3d89de37855cea52e77c182111ca3fd00cf623a11c1f41ceb2a19ca

Read a reply

# python3 source.py -p 23a90f6499c5f3bc630e7103a4e63c131a8248c1ae5223541660b7bcbda8b2a9 -a read -i de55e92ca3d89de37855cea52e77c182111ca3fd00cf623a11c1f41ceb2a19ca
[+] Successfully decrypted message de55e92ca3d89de37855cea52e77c182111ca3fd00cf623a11c1f41ceb2a19ca

    ID: de55e92ca3d89de37855cea52e77c182111ca3fd00cf623a11c1f41ceb2a19ca
    From: a1eb055608e169d04392607a79a3bf8ac4ccfc9e0d3f5056941f31be78a12be1
    Date: 2023-01-23 23:42:14
    Text: This is a reply to the message without attachments, it is identified only by the id

Send an additional reply

# python3 source.py -p 23a90f6499c5f3bc630e7103a4e63c131a8248c1ae5223541660b7bcbda8b2a9 -a reply -i de55e92ca3d89de37855cea52e77c182111ca3fd00cf623a11c1f41ceb2a19ca -m "This is a second source to journalist reply"

Delete a message

# python3 source.py -p 23a90f6499c5f3bc630e7103a4e63c131a8248c1ae5223541660b7bcbda8b2a9 -a delete -i de55e92ca3d89de37855cea52e77c182111ca3fd00cf623a11c1f41ceb2a19ca
[+] Message de55e92ca3d89de37855cea52e77c182111ca3fd00cf623a11c1f41ceb2a19ca deleted

Journalist

Help

# python3 journalist.py -h
usage: journalist.py [-h] -j [0, 9] [-a {upload_keys,fetch,read,reply,delete}] [-i ID] [-m MESSAGE]

options:
  -h, --help            show this help message and exit
  -j [0, 9], --journalist [0, 9]
                        Journalist number
  -a {upload_keys,fetch,read,reply,delete}, --action {upload_keys,fetch,read,reply,delete}
                        Action to perform
  -i ID, --id ID        Message id
  -m MESSAGE, --message MESSAGE
                        Plaintext message content for replies

Fetch replies and submissions

# python3 journalist.py -j 7 -a fetch
[+] Found 2 message(s)
    0358306e106d1d9e0449e8e35a59c37c41b28a5e6630b88360738f5989da501c
    1216789eab54869259e168b02825151b665f04b0b9f01f654c913e3bbea1f627

Read a submission/reply (without attachments)

# python3 journalist.py -j 7 -a read -i 1216789eab54869259e168b02825151b665f04b0b9f01f654c913e3bbea1f627
[+] Successfully decrypted message 1216789eab54869259e168b02825151b665f04b0b9f01f654c913e3bbea1f627

    ID: 1216789eab54869259e168b02825151b665f04b0b9f01f654c913e3bbea1f627
    Date: 2023-01-23 23:37:15
    Text: My first contact message with a newsroom :)

Read a submission/reply (with attachments)

# python3 journalist.py -j 7 -a read -i 0358306e106d1d9e0449e8e35a59c37c41b28a5e6630b88360738f5989da501c
[+] Successfully decrypted message 0358306e106d1d9e0449e8e35a59c37c41b28a5e6630b88360738f5989da501c

    ID: 0358306e106d1d9e0449e8e35a59c37c41b28a5e6630b88360738f5989da501c
    Date: 2023-01-23 23:38:27
    Attachment: name=file1.mkv;size=1562624;parts_count=3
    Attachment: name=file2.zip;size=93849;parts_count=1
    Text: My first contact message with a newsroom with collected evidences and a supporting video :)

Send a reply

# python3 journalist.py -j 7 -a reply -i 1216789eab54869259e168b02825151b665f04b0b9f01f654c913e3bbea1f627 -m "This is a reply to the message without attachments, it is identified only by the id"

Delete a message

# python3 journalist.py -j 7 -a delete -i 1216789eab54869259e168b02825151b665f04b0b9f01f654c913e3bbea1f627
[+] Message 1216789eab54869259e168b02825151b665f04b0b9f01f654c913e3bbea1f627 deleted