Compare commits

...
2 Commits
6 changed files with 113 additions and 11 deletions
+41
View File
@@ -1,3 +1,5 @@
import logging
from logging.config import dictConfig
from typing import Optional, Callable
from Crypto.PublicKey.ECC import EccKey
@@ -7,6 +9,45 @@ from SantaExchange.MessageHandler import MessageHandler
from SantaExchange.SantasBrain import Brain
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):
def __init__(self, send_message_function, receive_user_function, announce_recipient_function):
MessageHandler.__init__(self)
+2 -2
View File
@@ -21,13 +21,13 @@ async function prepare() {
}
function printLogs(record) {
console.log(record);
postMessage({type: 'status', stage: 'exchange', text: record});
}
pyodide.globals.set('send_message', sendMessage);
pyodide.globals.set('receive_user', receiveUser);
pyodide.globals.set('announce_recipient', announceRecipient);
pyodide.globals.set('print_logs_function', printLogs);
pyodide.globals.set('logging_function', printLogs);
pyodide.runPython(`
exchange_worker = ExchangeClient(send_message, receive_user, announce_recipient)
+4
View File
@@ -36,4 +36,8 @@ textarea {
width: 30vw;
height: 20vh;
font-size: 20px;
}
.leftjust {
text-align: left;
}
+10 -1
View File
@@ -8,19 +8,28 @@
</head>
<body>
<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 />
<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 />
<button id="submit_info_button" onclick="submitInfo();">Submit your info</button>
<br /><br /><br />
<pre id="statusParagraph"></pre>
<button id="start_exchange_button" onclick="submitStart();">Start with current participants</button>
<p>Current participants:</p>
<ul id='participantList'>
</ul>
<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 client_id = Math.floor(Math.random() * 2**64)
+47 -4
View File
@@ -9,10 +9,11 @@
<body>
<div class="bodyDiv">
<h1>DecentraSanta</h1>
<p>Welcome to the decentralised secret santa exchange. This website uses cryptography to distribute secret santas
in such a way that no one but you, not even the server the communication goes through, can know who you're
giving a
gift to.</p>
<p>
Welcome to the decentralised secret santa exchange. This website uses cryptography to distribute secret
santas in such a way that no one but you, not even the server the communication goes through, can know who
you're giving a gift to.
</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" />
<br/>
<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>
<script type="text/javascript">
+9 -4
View File
@@ -216,7 +216,6 @@ class Brain:
or self.announcement_build_cipher is None
or self.announcement_shuffle_cipher is None
):
logger.debug('Building ciphers')
should_continue = self.build_ciphers()
if not should_continue:
return messages_to_send
@@ -226,7 +225,6 @@ class Brain:
# Shuffle cards
if not self.sent_card_shuffling:
logger.debug('Shuffling vards')
shuffle_message = self.build_shuffle_message(all_participants)
if shuffle_message is None:
return messages_to_send
@@ -235,7 +233,6 @@ class Brain:
# Decrypt shuffled cards
if not self.sent_card_decryption:
logger.debug('Decrypting shuffled cards')
decrypt_cards_message = self.build_decrypt_cards_message(all_participants)
if decrypt_cards_message is None:
return messages_to_send
@@ -359,6 +356,7 @@ class Brain:
received_seeds.append(received_seed)
# 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)
total_seed = b'\0' * number_of_seed_bytes
for received_seed in received_seeds:
@@ -401,6 +399,7 @@ class Brain:
card_deck = [card for card in message.get_cards()]
# Shuffle by drawing random numbers from secret
logger.info('Shuffling encrypted cards')
shuffled_deck = []
while len(card_deck) > 0:
drawn_card = secrets.randbelow(len(card_deck))
@@ -432,6 +431,7 @@ class Brain:
if message is None:
return None
logger.info('Decrypting shuffled cards')
card_deck = message.get_cards()
decrypted_cards = []
for index, card_value in enumerate(card_deck):
@@ -448,7 +448,7 @@ class Brain:
return ShuffleMessage(self.own_name, decrypted_cards, DECRYPT_CARDS_STAGE)
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)
if decrypted_card_bytes not in self.card_values:
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()]
logger.info('Creating anonymous message')
anonymous_message = {
'card_no': self.card_drawn,
'key': self.secret_key.public_key().export_key(format='OpenSSH')
@@ -515,6 +516,7 @@ class Brain:
if card_deck is None:
return None
logger.info('Building deck of anonymous messages')
encrypted_deck = []
for index, card_value in enumerate(card_deck):
if index == own_index:
@@ -553,6 +555,7 @@ class Brain:
if card_deck is None:
return None
logger.info('Shuffling anonymous messages')
shuffled_cards = []
while len(card_deck) > 0:
draw_number = secrets.randbelow(len(card_deck))
@@ -595,6 +598,7 @@ class Brain:
if card_deck is None:
return None
logger.info('Decrypting anonymous messages')
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)
@@ -646,6 +650,7 @@ class Brain:
if self.anonymous_keys is None:
return None
logger.info("Creating encrypted announcement for secret santa")
santa_card = (self.card_drawn - 1) % len(all_participants)
santa_key = self.anonymous_keys[santa_card]