Compare commits
4
Commits
564e449e33
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
343cd24095 | ||
|
|
e38628cbc6 | ||
|
|
5d6138bdc2 | ||
|
|
da99a1bdd5 |
+23
@@ -0,0 +1,23 @@
|
|||||||
|
FROM python:3.13-slim-bookworm AS build
|
||||||
|
|
||||||
|
WORKDIR /santa
|
||||||
|
RUN mkdir -p /santa/WebServer/static
|
||||||
|
COPY setup-server.sh /santa/setup-server.sh
|
||||||
|
RUN apt-get update && apt-get install -y curl bzip2
|
||||||
|
RUN /santa/setup-server.sh --download-only
|
||||||
|
COPY requirements.txt /santa/requirements.txt
|
||||||
|
COPY src/setup.py /santa/src/setup.py
|
||||||
|
COPY src/SantaExchange/ /santa/src/SantaExchange/
|
||||||
|
RUN pip install -r /santa/requirements.txt
|
||||||
|
RUN /santa/setup-server.sh
|
||||||
|
|
||||||
|
FROM python:3.13-slim-bookworm
|
||||||
|
RUN pip install flask flask-socketio gunicorn eventlet
|
||||||
|
COPY --from=build /santa/WebServer /WebServer
|
||||||
|
COPY WebServer/__init__.py WebServer/server.py /WebServer/
|
||||||
|
COPY WebServer/templates/ /WebServer/templates/
|
||||||
|
COPY WebServer/static/exchange_client.py WebServer/static/exchange_worker.js WebServer/static/style.css /WebServer/static/
|
||||||
|
|
||||||
|
WORKDIR /WebServer
|
||||||
|
EXPOSE 8000
|
||||||
|
CMD ["gunicorn", "--worker-class", "eventlet", "-w", "1", "--bind", "0.0.0.0:8000", "server:app"]
|
||||||
+5
-3
@@ -1,11 +1,13 @@
|
|||||||
import json
|
import os
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
from flask import Flask, render_template
|
from flask import Flask, render_template
|
||||||
from flask_socketio import SocketIO, join_room, leave_room, send
|
from flask_socketio import SocketIO, join_room, leave_room, send
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
socketio = SocketIO(app)
|
if 'CORS_ORIGIN' in os.environ and os.environ['CORS_ORIGIN'] is not None:
|
||||||
|
socketio = SocketIO(app, cors_allowed_origins=[os.environ['CORS_ORIGIN']])
|
||||||
|
else:
|
||||||
|
socketio = SocketIO(app)
|
||||||
|
|
||||||
if __name__ == 'main':
|
if __name__ == 'main':
|
||||||
socketio.run(app)
|
socketio.run(app)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import logging
|
||||||
|
from logging.config import dictConfig
|
||||||
from typing import Optional, Callable
|
from typing import Optional, Callable
|
||||||
|
|
||||||
from Crypto.PublicKey.ECC import EccKey
|
from Crypto.PublicKey.ECC import EccKey
|
||||||
@@ -7,6 +9,45 @@ from SantaExchange.MessageHandler import MessageHandler
|
|||||||
from SantaExchange.SantasBrain import Brain
|
from SantaExchange.SantasBrain import Brain
|
||||||
from SantaExchange.UserInterface import UserInterface
|
from SantaExchange.UserInterface import UserInterface
|
||||||
|
|
||||||
|
logging_function: Optional[Callable[[str], None]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeLogger(logging.Handler):
|
||||||
|
def emit(self, record: logging.LogRecord):
|
||||||
|
if logging_function is not None:
|
||||||
|
logging_function(record.message)
|
||||||
|
|
||||||
|
|
||||||
|
LOGGING_SETTINGS = {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'formatters': {
|
||||||
|
'std_formatter': {'format': '%(message)s'}
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'stdhandler': {
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'formatter': 'std_formatter',
|
||||||
|
'stream': 'ext://sys.stdout'
|
||||||
|
},
|
||||||
|
'ExchangeLogger': {
|
||||||
|
'class': f'{__name__}.ExchangeLogger',
|
||||||
|
'formatter': 'std_formatter',
|
||||||
|
'level': 'INFO'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'root': {
|
||||||
|
'handlers': ['stdhandler', 'ExchangeLogger'],
|
||||||
|
'level': 'NOTSET',
|
||||||
|
'propagate': True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dictConfig(LOGGING_SETTINGS)
|
||||||
|
|
||||||
|
|
||||||
class ExchangeClient(MessageHandler, UserInterface):
|
class ExchangeClient(MessageHandler, UserInterface):
|
||||||
def __init__(self, send_message_function, receive_user_function, announce_recipient_function):
|
def __init__(self, send_message_function, receive_user_function, announce_recipient_function):
|
||||||
MessageHandler.__init__(self)
|
MessageHandler.__init__(self)
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ async function prepare() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function printLogs(record) {
|
function printLogs(record) {
|
||||||
console.log(record);
|
postMessage({type: 'status', stage: 'exchange', text: record});
|
||||||
}
|
}
|
||||||
|
|
||||||
pyodide.globals.set('send_message', sendMessage);
|
pyodide.globals.set('send_message', sendMessage);
|
||||||
pyodide.globals.set('receive_user', receiveUser);
|
pyodide.globals.set('receive_user', receiveUser);
|
||||||
pyodide.globals.set('announce_recipient', announceRecipient);
|
pyodide.globals.set('announce_recipient', announceRecipient);
|
||||||
pyodide.globals.set('print_logs_function', printLogs);
|
pyodide.globals.set('logging_function', printLogs);
|
||||||
|
|
||||||
pyodide.runPython(`
|
pyodide.runPython(`
|
||||||
exchange_worker = ExchangeClient(send_message, receive_user, announce_recipient)
|
exchange_worker = ExchangeClient(send_message, receive_user, announce_recipient)
|
||||||
|
|||||||
@@ -36,4 +36,8 @@ textarea {
|
|||||||
width: 30vw;
|
width: 30vw;
|
||||||
height: 20vh;
|
height: 20vh;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftjust {
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
@@ -8,19 +8,28 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Santa exchange</h1>
|
<h1>Santa exchange</h1>
|
||||||
|
<p id="santaexchangename">This santa exchange is named: </p>
|
||||||
|
<button onclick="copyLinkToClipboard();">Copy link to clipboard</button><br /><br /><br />
|
||||||
|
<p>Status:</p>
|
||||||
|
<pre id="statusParagraph"></pre>
|
||||||
<label for="own_name">Your name: </label><br /><input type="text" id="own_name" /><br />
|
<label for="own_name">Your name: </label><br /><input type="text" id="own_name" /><br />
|
||||||
<br />
|
<br />
|
||||||
<label for="info_for_santa">Info for your santa (e.g. shipping address or similar):</label><br />
|
<label for="info_for_santa">Info for your santa (e.g. shipping address or similar):</label><br />
|
||||||
<textarea id="info_for_santa"></textarea><br />
|
<textarea id="info_for_santa"></textarea><br />
|
||||||
<button id="submit_info_button" onclick="submitInfo();">Submit your info</button>
|
<button id="submit_info_button" onclick="submitInfo();">Submit your info</button>
|
||||||
<br /><br /><br />
|
<br /><br /><br />
|
||||||
<pre id="statusParagraph"></pre>
|
|
||||||
<button id="start_exchange_button" onclick="submitStart();">Start with current participants</button>
|
<button id="start_exchange_button" onclick="submitStart();">Start with current participants</button>
|
||||||
<p>Current participants:</p>
|
<p>Current participants:</p>
|
||||||
<ul id='participantList'>
|
<ul id='participantList'>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
document.getElementById('santaexchangename').innerHTML += window.location.pathname.substring(1);
|
||||||
|
|
||||||
|
function copyLinkToClipboard() {
|
||||||
|
navigator.clipboard.writeText(window.location.href);
|
||||||
|
}
|
||||||
|
|
||||||
const socket = io();
|
const socket = io();
|
||||||
const client_id = Math.floor(Math.random() * 2**64)
|
const client_id = Math.floor(Math.random() * 2**64)
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,11 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="bodyDiv">
|
<div class="bodyDiv">
|
||||||
<h1>DecentraSanta</h1>
|
<h1>DecentraSanta</h1>
|
||||||
<p>Welcome to the decentralised secret santa exchange. This website uses cryptography to distribute secret santas
|
<p>
|
||||||
in such a way that no one but you, not even the server the communication goes through, can know who you're
|
Welcome to the decentralised secret santa exchange. This website uses cryptography to distribute secret
|
||||||
giving a
|
santas in such a way that no one but you, not even the server the communication goes through, can know who
|
||||||
gift to.</p>
|
you're giving a gift to.
|
||||||
|
</p>
|
||||||
|
|
||||||
<p>You can either start a new secret santa exchange, or join an existing one.</p>
|
<p>You can either start a new secret santa exchange, or join an existing one.</p>
|
||||||
|
|
||||||
@@ -23,6 +24,48 @@
|
|||||||
<input type="text" id="existing_address" />
|
<input type="text" id="existing_address" />
|
||||||
<br/>
|
<br/>
|
||||||
<button id="join_existing" onclick="onJoinExchange();">Join existing exchange</button>
|
<button id="join_existing" onclick="onJoinExchange();">Join existing exchange</button>
|
||||||
|
<br /><br /><br /><br />
|
||||||
|
<p>
|
||||||
|
<b>
|
||||||
|
WARNING: I have partially rolled my own crypto for this project. Please don't use it for anything serious!
|
||||||
|
</b>
|
||||||
|
</p>
|
||||||
|
<p class="leftjust">
|
||||||
|
How does this work: the simplified analogy is that we build a deck of cards numbered from 1 up to the number
|
||||||
|
of participants. This deck is shuffled, and each participant draws a card. The cards determine the secret
|
||||||
|
santa order: The participant who draws card 1 is the secret santa for the participant who draws card 2, who
|
||||||
|
is secret santa for the participant who draws card 3, and so forth.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In this analogy, once the participants have drawn their cards, they each anonymously put up a mailbox that
|
||||||
|
is marked with number of the card they drew. Each participant then puts their name and address (secretly)
|
||||||
|
into the mailbox with the number of their secret santa. Finally, they secretly open their mailboxes to find
|
||||||
|
the name and address of the recipient of their gifts.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
More technically: the shuffling and drawing of the cards is done using the
|
||||||
|
<a href="https://people.csail.mit.edu/rivest/pubs/SRA81.pdf">mental poker algorithm described by Shamir,
|
||||||
|
Rivest, and Adleman.</a> This algorithm depends on a commutative encryption algorithm. Here, I have used the
|
||||||
|
SRA algorithm, which is a modification of the RSA algorithm where the modulus used to encrypt and decrypt
|
||||||
|
values is public, but the both the encryption and decryption keys are kept private. I couldn't find any
|
||||||
|
Python libraries that implement this algorithm, so I have implemented it on my own. I make no guarantees
|
||||||
|
that this is secure in any way.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In the analogy above, the mailboxes represent public key cryptography. It is possible to anonymously announce
|
||||||
|
a participant's public key using the same mental poker algorithm from above: each participant publishes a
|
||||||
|
"card" containing, in encrypted form, their public key and the card number they drew in the previous step.
|
||||||
|
Each other participant then encrypts this card with their own key. Once all the cards have been encrypted
|
||||||
|
in this way, each participant then removes their key, shuffles the deck, and applies a new key (which should
|
||||||
|
make it impossible to correlate cards before and after shuffling). Finally, each participant removes their
|
||||||
|
key from each of the cards. This should give a deck of cards containing participant numbers and public keys,
|
||||||
|
shuffled in such a way that it is impossible to know which card originated from which participant.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Finally, each participant publishes their name and address, encrypted so that only their secret santa can read
|
||||||
|
it. Each participant must then try to decrypt all of these encrypted messages until their gift receiver is found.
|
||||||
|
</p>
|
||||||
|
<p><a href="https://gitea.martinserver.no/martin/DecentraSanta">The source code for this web site can be found here!</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ if ! test -f ./WebServer/static/socket.io.min.js; then
|
|||||||
fi
|
fi
|
||||||
echo "Socket.io is installed"
|
echo "Socket.io is installed"
|
||||||
|
|
||||||
|
if [[ "$1" == "--download-only" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Building wheel file of SantaExchange package"
|
echo "Building wheel file of SantaExchange package"
|
||||||
cd src/
|
cd src/
|
||||||
python setup.py bdist_wheel
|
python setup.py bdist_wheel
|
||||||
|
|||||||
@@ -216,7 +216,6 @@ class Brain:
|
|||||||
or self.announcement_build_cipher is None
|
or self.announcement_build_cipher is None
|
||||||
or self.announcement_shuffle_cipher is None
|
or self.announcement_shuffle_cipher is None
|
||||||
):
|
):
|
||||||
logger.debug('Building ciphers')
|
|
||||||
should_continue = self.build_ciphers()
|
should_continue = self.build_ciphers()
|
||||||
if not should_continue:
|
if not should_continue:
|
||||||
return messages_to_send
|
return messages_to_send
|
||||||
@@ -226,7 +225,6 @@ class Brain:
|
|||||||
|
|
||||||
# Shuffle cards
|
# Shuffle cards
|
||||||
if not self.sent_card_shuffling:
|
if not self.sent_card_shuffling:
|
||||||
logger.debug('Shuffling vards')
|
|
||||||
shuffle_message = self.build_shuffle_message(all_participants)
|
shuffle_message = self.build_shuffle_message(all_participants)
|
||||||
if shuffle_message is None:
|
if shuffle_message is None:
|
||||||
return messages_to_send
|
return messages_to_send
|
||||||
@@ -235,7 +233,6 @@ class Brain:
|
|||||||
|
|
||||||
# Decrypt shuffled cards
|
# Decrypt shuffled cards
|
||||||
if not self.sent_card_decryption:
|
if not self.sent_card_decryption:
|
||||||
logger.debug('Decrypting shuffled cards')
|
|
||||||
decrypt_cards_message = self.build_decrypt_cards_message(all_participants)
|
decrypt_cards_message = self.build_decrypt_cards_message(all_participants)
|
||||||
if decrypt_cards_message is None:
|
if decrypt_cards_message is None:
|
||||||
return messages_to_send
|
return messages_to_send
|
||||||
@@ -359,6 +356,7 @@ class Brain:
|
|||||||
|
|
||||||
received_seeds.append(received_seed)
|
received_seeds.append(received_seed)
|
||||||
# Next, create a combined random seed from all the provided seeds
|
# Next, create a combined random seed from all the provided seeds
|
||||||
|
logger.info('Building cryptographic ciphers')
|
||||||
number_of_seed_bytes = max(len(seed) for seed in received_seeds)
|
number_of_seed_bytes = max(len(seed) for seed in received_seeds)
|
||||||
total_seed = b'\0' * number_of_seed_bytes
|
total_seed = b'\0' * number_of_seed_bytes
|
||||||
for received_seed in received_seeds:
|
for received_seed in received_seeds:
|
||||||
@@ -401,6 +399,7 @@ class Brain:
|
|||||||
card_deck = [card for card in message.get_cards()]
|
card_deck = [card for card in message.get_cards()]
|
||||||
|
|
||||||
# Shuffle by drawing random numbers from secret
|
# Shuffle by drawing random numbers from secret
|
||||||
|
logger.info('Shuffling encrypted cards')
|
||||||
shuffled_deck = []
|
shuffled_deck = []
|
||||||
while len(card_deck) > 0:
|
while len(card_deck) > 0:
|
||||||
drawn_card = secrets.randbelow(len(card_deck))
|
drawn_card = secrets.randbelow(len(card_deck))
|
||||||
@@ -432,6 +431,7 @@ class Brain:
|
|||||||
if message is None:
|
if message is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
logger.info('Decrypting shuffled cards')
|
||||||
card_deck = message.get_cards()
|
card_deck = message.get_cards()
|
||||||
decrypted_cards = []
|
decrypted_cards = []
|
||||||
for index, card_value in enumerate(card_deck):
|
for index, card_value in enumerate(card_deck):
|
||||||
@@ -448,7 +448,7 @@ class Brain:
|
|||||||
return ShuffleMessage(self.own_name, decrypted_cards, DECRYPT_CARDS_STAGE)
|
return ShuffleMessage(self.own_name, decrypted_cards, DECRYPT_CARDS_STAGE)
|
||||||
|
|
||||||
def decrypt_card_value(self, card: bytes):
|
def decrypt_card_value(self, card: bytes):
|
||||||
logger.debug('Decrypting own drawn card')
|
logger.info('Decrypting drawn card')
|
||||||
decrypted_card_bytes = self.card_exchange_cipher.decode(card)
|
decrypted_card_bytes = self.card_exchange_cipher.decode(card)
|
||||||
if decrypted_card_bytes not in self.card_values:
|
if decrypted_card_bytes not in self.card_values:
|
||||||
logging.critical(f'Received an invalid card after shuffling. Secret santa exchange failed!')
|
logging.critical(f'Received an invalid card after shuffling. Secret santa exchange failed!')
|
||||||
@@ -477,6 +477,7 @@ class Brain:
|
|||||||
|
|
||||||
card_deck = [card for card in message.get_cards()]
|
card_deck = [card for card in message.get_cards()]
|
||||||
|
|
||||||
|
logger.info('Creating anonymous message')
|
||||||
anonymous_message = {
|
anonymous_message = {
|
||||||
'card_no': self.card_drawn,
|
'card_no': self.card_drawn,
|
||||||
'key': self.secret_key.public_key().export_key(format='OpenSSH')
|
'key': self.secret_key.public_key().export_key(format='OpenSSH')
|
||||||
@@ -515,6 +516,7 @@ class Brain:
|
|||||||
if card_deck is None:
|
if card_deck is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
logger.info('Building deck of anonymous messages')
|
||||||
encrypted_deck = []
|
encrypted_deck = []
|
||||||
for index, card_value in enumerate(card_deck):
|
for index, card_value in enumerate(card_deck):
|
||||||
if index == own_index:
|
if index == own_index:
|
||||||
@@ -553,6 +555,7 @@ class Brain:
|
|||||||
if card_deck is None:
|
if card_deck is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
logger.info('Shuffling anonymous messages')
|
||||||
shuffled_cards = []
|
shuffled_cards = []
|
||||||
while len(card_deck) > 0:
|
while len(card_deck) > 0:
|
||||||
draw_number = secrets.randbelow(len(card_deck))
|
draw_number = secrets.randbelow(len(card_deck))
|
||||||
@@ -595,6 +598,7 @@ class Brain:
|
|||||||
if card_deck is None:
|
if card_deck is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
logger.info('Decrypting anonymous messages')
|
||||||
decrypted_deck = [self.announcement_shuffle_cipher.decode(card_value) for card_value in card_deck]
|
decrypted_deck = [self.announcement_shuffle_cipher.decode(card_value) for card_value in card_deck]
|
||||||
shuffle_message = ShuffleMessage(self.own_name, decrypted_deck, DECRYPT_ANONYMOUS_STAGE)
|
shuffle_message = ShuffleMessage(self.own_name, decrypted_deck, DECRYPT_ANONYMOUS_STAGE)
|
||||||
|
|
||||||
@@ -646,6 +650,7 @@ class Brain:
|
|||||||
if self.anonymous_keys is None:
|
if self.anonymous_keys is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
logger.info("Creating encrypted announcement for secret santa")
|
||||||
santa_card = (self.card_drawn - 1) % len(all_participants)
|
santa_card = (self.card_drawn - 1) % len(all_participants)
|
||||||
|
|
||||||
santa_key = self.anonymous_keys[santa_card]
|
santa_key = self.anonymous_keys[santa_card]
|
||||||
|
|||||||
Reference in New Issue
Block a user