Skip to content

Commit

Permalink
Disallow sources to access or delete their own submissions
Browse files Browse the repository at this point in the history
Fixes #10
  • Loading branch information
eaon committed Feb 24, 2023
1 parent ae5cfa6 commit 11b455a
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 37 deletions.
53 changes: 48 additions & 5 deletions journalist.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from hashlib import sha3_256
from os import listdir, mkdir, path
from time import time
from secrets import token_hex

import nacl.secret
import requests
Expand Down Expand Up @@ -44,6 +45,38 @@ def load_ephemeral_keys(journalist_key, journalist_id, journalist_uid):
return ephemeral_keys


def add_file_tokens(journalist_key, journalist_uid):
file_tokens = []
for _ in range(commons.ONETIMEKEYS):
file_tokens.append((token_hex(32), token_hex(32)))

file_tokens = json.dumps(file_tokens)
file_tokens_dir = f"{commons.DIR}journalists/file_tokens/"
try:
mkdir(file_tokens_dir)
except Exception:
pass
with open(f"{file_tokens_dir}{token_hex(32)}.json", "w") as f:
f.write(file_tokens)

sig = pki.sign(journalist_key, file_tokens.encode("ascii"))

response = requests.post(f"http://{commons.SERVER}/file_tokens", json={
"journalist_uid": journalist_uid,
"sig": b64encode(sig).decode("ascii"),
"file_tokens": file_tokens,
})

return (response.status_code == 200)


def load_file_tokens():
pairs = []
for file_name in listdir(f"{commons.DIR}journalists/file_tokens/"):
pairs.extend(json.load(open(f"{commons.DIR}journalists/file_tokens/{file_name}")))
return dict(pairs)


# Try all the ephemeral keys to build an encryption shared secret to decrypt a message.
# This is inefficient, but on an actual implementation we would discard already used keys
def decrypt_message(ephemeral_keys, message):
Expand All @@ -55,7 +88,7 @@ def decrypt_message(ephemeral_keys, message):
return message_plaintext


def journalist_reply(message, reply, journalist_uid):
def journalist_reply(message, reply, journalist_uid, file_tokens):
# This function builds the per-message keys and returns a nacl encrypting box
message_public_key, message_challenge, box = commons.build_message(
message["source_challenge_public_key"],
Expand All @@ -72,10 +105,13 @@ def journalist_reply(message, reply, journalist_uid):
"group_members": [],
"timestamp": int(time())}

# in a reply, we need to let the source know what the file_name is so it can access the
# full message and delete it at its leisure
file_id, key = commons.upload_message(json.dumps(message_dict))
file_name = file_tokens[file_id]

message_ciphertext = b64encode(box.encrypt(
(json.dumps({"file_id": file_id, "key": key})).encode('ascii'))
(json.dumps({"file_name": file_name, "key": key})).encode('ascii'))
).decode("ascii")

# Send the message to the server API using the generic /send endpoint
Expand All @@ -96,6 +132,10 @@ def main(args):
# Generate and upload a bunch (30) of ephemeral keys
add_ephemeral_keys(journalist_key, journalist_id, journalist_uid)

elif args.action == "upload_file_tokens":
# Generate and upload signed file tokens to be used by the server to hide file_ids from sources
add_file_tokens(journalist_key, journalist_uid)

elif args.action == "fetch":
# Check if there are messages
messages_list = commons.fetch_messages_id(journalist_chal_key)
Expand Down Expand Up @@ -175,11 +215,14 @@ def main(args):
message = commons.get_message(message_id)
ephemeral_keys = load_ephemeral_keys(journalist_key, journalist_id, journalist_uid)
envelope_plaintext = decrypt_message(ephemeral_keys, message)
message_ciphertext = commons.get_file(envelope_plaintext['file_id'])
# journalists must map the file_id to file_name, as the source cannot tell us the latter
file_tokens = load_file_tokens()
file_name = file_tokens[envelope_plaintext['file_id']]
message_ciphertext = commons.get_file(file_name)
message_symmetric_key = bytes.fromhex(envelope_plaintext['key'])
message_plaintext = commons.decrypt_message_symmetric(message_ciphertext,
message_symmetric_key)
journalist_reply(message_plaintext, args.message, journalist_uid)
journalist_reply(message_plaintext, args.message, journalist_uid, file_tokens)

elif args.action == "delete":
message_id = args.id
Expand All @@ -191,7 +234,7 @@ def main(args):
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument("-j", "--journalist", help="Journalist number", type=int, choices=range(0, commons.JOURNALISTS), metavar=f"[0, {commons.JOURNALISTS - 1}]", required=True)
parser.add_argument("-a", "--action", help="Action to perform", default="fetch", choices=["upload_keys", "fetch", "read", "reply", "delete", "thread"])
parser.add_argument("-a", "--action", help="Action to perform", default="fetch", choices=["upload_keys", "upload_file_tokens", "fetch", "read", "reply", "delete", "thread"])
parser.add_argument("-i", "--id", help="Message id")
parser.add_argument("-t", "--thread", help="Thread id")
parser.add_argument("-m", "--message", help="Plaintext message content for replies")
Expand Down
21 changes: 15 additions & 6 deletions pki.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,27 +89,36 @@ def generate_key(name):
return key


# Sign a given public key with the pubblid private key
def sign_key(signing_pivate_key, signed_public_key, signature_name):
sig = signing_pivate_key.sign_deterministic(
signed_public_key.to_string(),
def sign(signing_private_key, signed):
return signing_private_key.sign_deterministic(
signed,
hashfunc=sha3_256,
sigencode=sigencode_der
)


# Sign a given public key with the pubblid private key
def sign_key(signing_private_key, signed_public_key, signature_name):
sig = sign(signing_private_key, signed_public_key.to_string())

with open(signature_name, "wb") as f:
f.write(sig)

return sig


# Verify a signature
def verify(signing_public_key, signed, sig):
signing_public_key.verify(sig, signed, hashfunc=sha3_256, sigdecode=sigdecode_der)
return sig


def verify_key(signing_public_key, signed_public_key, signature_name, sig=None):
if not sig:
with open(signature_name, "rb") as f:
sig = f.read()
signing_public_key.verify(sig, signed_public_key.to_string(), hashfunc=sha3_256, sigdecode=sigdecode_der)
return sig

return verify(signing_public_key, signed_public_key.to_string(), sig)


def generate_pki():
Expand Down
79 changes: 55 additions & 24 deletions server.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,38 +90,42 @@ def download_file():
except Exception:
return {"status": "KO"}, 400

file_name = token_hex(32)
file_id = token_hex(32)
redis.set(f"file:{file_id}", file_name.encode("ascii"))
file_id, file_name = json.loads(redis.spop("file_token"))

redis.set(f"file:{file_name}", 1)

file.save(f"{commons.UPLOADS}{file_name}.enc")

return {"status": "OK", "file_id": file_id}, 200


@app.route("/file/<file_id>", methods=["GET"])
def get_file(file_id):
file_name = redis.get(f"file:{file_id}")
if not file_name:
@app.route("/file/<file_name>", methods=["GET"])
def get_file(file_name):
if not redis.get(f"file:{file_name}"):
return {"status": "KO"}, 404
else:
file_name = file_name.decode('ascii')
return send_file(f"{commons.UPLOADS}{file_name}.enc")
return send_file(f"{commons.UPLOADS}{file_name}.enc")


@app.route("/file/<file_id>", methods=["DELETE"])
def delete_file(file_id):
file = redis.get(f"file:{file_id}")
if not file:
@app.route("/file/<file_name>", methods=["DELETE"])
def delete_file(file_name):
if not redis.get(f"file:{file_name}"):
return {"status": "KO"}, 404
else:
file = file.decode('ascii')

redis.delete(f"file:{file_id}")
remove(f"{commons.UPLOADS}{file}.enc")
redis.delete(f"file:{file_name}")
remove(f"{commons.UPLOADS}{file_name}.enc")
return {"status": "OK"}, 200


def get_journalist_verifying_key(journalist_uid):
journalists = redis.smembers("journalists")

for journalist in journalists:
journalist_dict = json.loads(journalist.decode("ascii"))
if journalist_dict["journalist_uid"] == journalist_uid:
return pki.public_b642key(journalist_dict["journalist_key"])

return None


@app.route("/ephemeral_keys", methods=["POST"])
def add_ephemeral_keys():
content = request.json
Expand All @@ -132,12 +136,10 @@ def add_ephemeral_keys():
return {"status": "KO"}, 400

journalist_uid = content["journalist_uid"]
journalists = redis.smembers("journalists")
journalist_verifying_key = get_journalist_verifying_key(journalist_uid)
if journalist_verifying_key is None:
return {"status": "KO"}, 400

for journalist in journalists:
journalist_dict = json.loads(journalist.decode("ascii"))
if journalist_dict["journalist_uid"] == journalist_uid:
journalist_verifying_key = pki.public_b642key(journalist_dict["journalist_key"])
ephemeral_keys = content["ephemeral_keys"]

for ephemeral_key_dict in ephemeral_keys:
Expand Down Expand Up @@ -175,6 +177,35 @@ def get_ephemeral_keys():
return {"status": "OK", "count": len(ephemeral_keys), "ephemeral_keys": ephemeral_keys}, 200


@app.route("/file_tokens", methods=["POST"])
def add_file_tokens():
content = request.json
try:
assert ("journalist_uid" in content)
assert ("sig" in content)
assert ("file_tokens" in content)
except Exception:
return {"status": "KO"}, 400

journalist_uid = content["journalist_uid"]
journalist_verifying_key = get_journalist_verifying_key(journalist_uid)
if journalist_verifying_key is None:
return {"status": "KO"}, 400

sig = b64decode(content["sig"])
file_tokens = content["file_tokens"]

try:
pki.verify(journalist_verifying_key, file_tokens.encode("ascii"), sig)
except Exception:
return {"status": "KO"}, 400

for file_id, file_name in json.loads(file_tokens):
redis.sadd("file_token", json.dumps([file_id, file_name]))

return {"status": "OK"}, 200


@app.route("/challenge", methods=["GET"])
def get_messages_challenge():
# SERVER EPHEMERAL CHALLENGE KEY
Expand Down
5 changes: 3 additions & 2 deletions source.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,11 @@ def main(args):

if message_plaintext:
print(f"[+] Successfully decrypted message {message_id}")
print(f"[+] file_id: {message_plaintext['file_id']}, key: {message_plaintext['key']}")
# sources receive the otherwise secret file_name from journalists
print(f"[+] file_name: {message_plaintext['file_name']}, key: {message_plaintext['key']}")
print()
key = message_plaintext['key']
encrypted_message_content = commons.get_file(message_plaintext['file_id'])
encrypted_message_content = commons.get_file(message_plaintext['file_name'])
message_plaintext = commons.decrypt_message_symmetric(encrypted_message_content, bytes.fromhex(key))
print(f"\tID: {message_id}")
print(f"\tFrom: {message_plaintext['sender']}")
Expand Down

0 comments on commit 11b455a

Please sign in to comment.