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:
- Part 1: Future directions for SecureDrop
- Part 2: Anatomy of a whistleblowing system
- Part 3: How to research your own cryptography and survive
- Part 4: Introducing SecureDrop Protocol
Repository overview
The code in this repository implements three components:
- a server, which can process encrypted messages and attachments
- a source client, which can encrypt and send messages and attachments, and which can receive and decrypt messages
- a journalist client, which can receive and decrypt messages and attachments, and which can encrypt and send messages
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:
- Journalist key material generated by
pki.py
is stored on-disk underkeys/
. It is accessed there by the journalist client, which uploads public keys to the server - The server uses Redis as a key/value store to persist the following:
- for each journalist: long-term signing public key, long-term message-fetching public key, ephemeral public keys, and signatures for all keys
- for each message: ciphertext, per-message public key, and per-message group Diffie Hellman share
- for each attachment: the randomly generated filename of the on-disk binary ciphertext
- Binary ciphertexts of attachments uploaded by sources are stored by the server on-disk under
files/
- Messages downloaded and decrypted by journalists are persisted in an SQLite3 database, which is stored in the
files/
directory - Attachments downloaded and decrypted by journalists are stored on-disk under
downloads/
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