This shows you the differences between two versions of the page.
ac:laboratoare:07 [2024/11/07 12:50] dimitrie.valu |
ac:laboratoare:07 [2024/11/14 13:05] (current) dimitrie.valu |
||
---|---|---|---|
Line 1: | Line 1: | ||
===== Lab 07 - Whatsapp End-to-end Encryption ===== | ===== Lab 07 - Whatsapp End-to-end Encryption ===== | ||
- | In this lab you will implement a simplified version of The Signal Protocol, which is the basis for WhatsApp's end-to-end encryption. | + | In this lab you will implement a simplified version of the Signal Protocol, which is the basis for WhatsApp's end-to-end encryption. |
The first versions of Whatsapp protocol were described [[https://cryptome.org/2016/04/whatsapp-crypto.pdf|here]]. A more recent document is available [[https://www.whatsapp.com/security/WhatsApp-Security-Whitepaper.pdf|here]]. | The first versions of Whatsapp protocol were described [[https://cryptome.org/2016/04/whatsapp-crypto.pdf|here]]. A more recent document is available [[https://www.whatsapp.com/security/WhatsApp-Security-Whitepaper.pdf|here]]. | ||
Line 9: | Line 9: | ||
For the Elliptic Curves, you can use [[https://github.com/Muterra/donna25519|this]] library. | For the Elliptic Curves, you can use [[https://github.com/Muterra/donna25519|this]] library. | ||
- | For installation, follow these steps: | + | For installation, follow these steps (NOTE: **you can use your ''%%fep%%'' instance via Python3 environments**): |
- | * [If in the lab] Log-in as admin (need admin user/password from lab host) | + | * Install the necessary tools (not necessary on ''%%fep%%''): |
- | * [If in the lab] Select Administration->Software Sources and then select the romanian repos for high bandwidth | + | <code> |
- | * Install the necessary tools: sudo apt-get install build-essential setuptools python-dev | + | sudo apt install build-essential python3-dev |
- | * Install pip: sudo apt-get install pip | + | sudo apt install python3-pip |
- | * Install donna via pip: sudo pip install donna25519 | + | </code> |
- | * Log-out from admin account | + | * Use ''%%wget%%'' to download the required zip (find it below) |
- | * Log-in with student as usual | + | * Create a Python3 environment, make sure PyPI is up to date and install the required packages: |
+ | <code> | ||
+ | python3 -m venv create env | ||
+ | source ./env/bin/activate | ||
+ | pip install --upgrade pip | ||
+ | pip install cryptography donna25519 | ||
+ | </code> | ||
- | === Task 1 === | + | **If local installation does not work, use your ''%%fep%%'' instance.** |
- | Create a common master_secret for two clients which communicate through a server. (TODO 1.1 & TODO 1.2) | + | |
+ | === Task === | ||
+ | Find the required zip here - {{:ac:laboratoare:lab07.zip|}}. | ||
+ | |||
+ | Create a common ''%%master_secret%%'' for two clients which communicate through a server. (**TODO 1.1** & **TODO 1.2**) | ||
Print it on both clients and make sure they both have the same secret. | Print it on both clients and make sure they both have the same secret. | ||
Line 25: | Line 35: | ||
Open three different terminals. | Open three different terminals. | ||
- | First terminal: | + | First terminal (start the server): |
<code>python main_server.py</code> | <code>python main_server.py</code> | ||
- | Second terminal: | + | Second terminal (start the first client and enter ''%%RECV%%'' mode: |
- | <code>python main_client.py</code> | + | <code> |
- | + | python main_client.py | |
- | Third terminal: | + | RECV |
- | <code>python main_client.py | + | |
- | MSG <id_other_client> Hello!</code> | + | |
- | + | ||
- | Second terminal: | + | |
- | <code>RECV</code> | + | |
- | + | ||
- | <code python 'main_client.py'> | + | |
- | from wa_client import Client | + | |
- | from wa_server import Server | + | |
- | + | ||
- | SERVER_IP = '127.0.0.1' | + | |
- | SERVER_PORT = 7778 | + | |
- | + | ||
- | if __name__ == '__main__': | + | |
- | c = Client(SERVER_IP, SERVER_PORT) | + | |
- | while True: | + | |
- | cmd = input('MSG <user_id> <message>\n') | + | |
- | cmd = cmd.split(" ") | + | |
- | if cmd[0] == "MSG": | + | |
- | user_id = int(cmd[1]) | + | |
- | msg = " ".join(cmd[2:]) | + | |
- | c.send_message(user_id, msg) | + | |
- | elif cmd[0] == "RECV": | + | |
- | msg = c.recv_message() | + | |
- | print(msg) | + | |
</code> | </code> | ||
- | <code python 'main_server.py'> | + | Third terminal (start the second client and send a message): |
- | from wa_client import Client | + | <code> |
- | from wa_server import Server | + | python main_client.py |
- | + | MSG <id_other_client> Hello! | |
- | SERVER_PORT = 7778 | + | |
- | + | ||
- | if __name__ == '__main__': | + | |
- | s = Server(SERVER_PORT) | + | |
- | s.start() | + | |
</code> | </code> | ||
- | <code python 'wa_client.py'> | ||
- | from donna25519 import * | ||
- | import os | ||
- | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes | ||
- | import struct | ||
- | import socket | ||
- | import hashlib | ||
- | import hmac | ||
- | from math import ceil | ||
- | |||
- | O_NUM = 10 # number of initial One-Time Pre Keys | ||
- | |||
- | hash_len = 32 | ||
- | def hmac_sha256(key, data): | ||
- | return hmac.new(key, data, hashlib.sha256).digest() | ||
- | |||
- | def hkdf(length, ikm, salt = b""): | ||
- | """Parameters: | ||
- | length: output length in bytes | ||
- | ikm: input key material | ||
- | """ | ||
- | prk = hmac_sha256(salt, ikm) | ||
- | t = b"" | ||
- | okm = b"" | ||
- | for i in range(int(ceil(1.0 * length / hash_len))): | ||
- | t = hmac_sha256(prk, t + bytes([1+i])) | ||
- | okm += t | ||
- | return okm[:length] | ||
- | |||
- | class ClientSession: | ||
- | def __init__(self, root_key, chain_key_s, chain_key_r, eph_own, eph_peer): | ||
- | self.root_key = root_key | ||
- | self.chain_key_s = chain_key_s | ||
- | self.chain_key_r = chain_key_r | ||
- | self.eph_own = eph_own | ||
- | self.eph_peer = eph_peer | ||
- | |||
- | def update_keys(self, eph_peer): | ||
- | """Performs vertical ratcheting | ||
- | Updates root & chain keys to match peer's (for decrypting its messages) | ||
- | Then updates root & chain keys again with fresh ephemereal key (for encrypting | ||
- | my messages) | ||
- | """ | ||
- | self.eph_peer = eph_peer | ||
- | # Update RootKey & receiving ChainKey | ||
- | # TODO 2 Obtain new root_key and chain_key_r from hkdf over | ||
- | # DH(eph_own, eph_peer) and root_key (can be used as salt parameter in hkdf) | ||
- | self.root_key = "" | ||
- | self.chain_key_r = "" | ||
- | |||
- | # Update RootKey & sending ChainKey | ||
- | self.eph_own = None # TODO 2 Generate new ephemereal key pair | ||
- | # TODO 2 Obtain new root_key and chain_key_s from hkdf over | ||
- | # DH(eph_own, eph_peer) and root_key (can be used as salt parameter in hkdf) | ||
- | self.root_key = "" | ||
- | self.chain_key_s = "" | ||
- | |||
- | class Client: | ||
- | def __init__(self, server_ip, server_port): | ||
- | self.I = PrivateKey() # Identity Key Pair | ||
- | self.S = PrivateKey() # Signed Pre Key | ||
- | self.O_queue = [PrivateKey() for i in range(O_NUM)] # One-Time Pre Keys | ||
- | |||
- | self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
- | self.s.connect((server_ip, server_port)) | ||
- | self.user_id = self.register() | ||
- | |||
- | # self.existing_sessions[user_id] = session with that user (ClientSession) | ||
- | self.existing_sessions = {} # initially no existing session | ||
- | |||
- | def register(self): | ||
- | """Lets the server know about its presence""" | ||
- | # TODO 1.1 send public keys of I, S and the list of O to the server | ||
- | # e.g. self.s.send(self.I.get_public().public) | ||
- | # You may also print a log message, e.g. print "Sent I = " + self.I.get_public().public.hex() | ||
- | |||
- | # You can send the list of O by first sending its length and then the keys one by one | ||
- | # use struct.pack('!i', my_int) to send 4 byte signed integers | ||
- | |||
- | # receive your user id from the server (might need later) | ||
- | # use struct.unpack('!i', received_int)[0] to get an integer | ||
- | # e.g. struct.unpack('!i', self.s.recv(4))[0] | ||
- | |||
- | user_id = 0 # TODO 1.1 change with received user id | ||
- | print("Got User ID = %d" % user_id) | ||
- | return user_id | ||
- | |||
- | def get_send_message_key(self, peer_user_id): | ||
- | """Gets sending MessageKey using ChainKey from peer_user_id's ClientSession""" | ||
- | session = self.existing_sessions[peer_user_id] | ||
- | |||
- | # TODO 2 Generate message_key from chain_key_s (session.chain_key_s) | ||
- | # message_key = HMAC_SHA256(ChainKey, 0x01) | ||
- | message_key = None | ||
- | |||
- | # TODO 2 Update chain_key_s | ||
- | # ChainKey = HMAC_SHA256(ChainKey, 0x02) | ||
- | session.chain_key_s = None | ||
- | |||
- | return message_key | ||
- | |||
- | def get_recv_message_key(self, peer_user_id): | ||
- | """Gets MessageKey using ChainKey from peer_user_id's ClientSession""" | ||
- | session = self.existing_sessions[peer_user_id] | ||
- | |||
- | # TODO 2 Generate message_key from chain_key_r (session.chain_key_r) | ||
- | # message_key = HMAC_SHA256(ChainKey, 0x01) | ||
- | message_key = None | ||
- | |||
- | # TODO 2 Update chain_key_r | ||
- | # ChainKey = HMAC_SHA256(ChainKey, 0x02) | ||
- | session.chain_key = None | ||
- | |||
- | return message_key | ||
- | |||
- | def pad_message(self, message): | ||
- | pad_number = (-len(message)) % 16 | ||
- | if pad_number == 0: | ||
- | pad_number += 16 | ||
- | |||
- | message += ''.join([chr(pad_number) for i in xrange(pad_number)]) | ||
- | return message | ||
- | |||
- | def unpad_message(self, message): | ||
- | pad_number = ord(message[-1]) | ||
- | message = message[:-pad_number] | ||
- | return message | ||
- | |||
- | def send_message(self, to_user_id, message): | ||
- | """Sends a message to user with id <to_user_id> | ||
- | If there is no existing ClientSession between them, they create one | ||
- | """ | ||
- | if to_user_id not in self.existing_sessions: | ||
- | self.setup_session(to_user_id) | ||
- | |||
- | self.s.send(b"SEND") | ||
- | self.s.send(struct.pack('!i', to_user_id)) | ||
- | |||
- | # Get MessageKey for next message | ||
- | message_key = self.get_send_message_key(to_user_id) | ||
- | |||
- | # Encrypt message using MessageKey | ||
- | iv = os.urandom(16) | ||
- | cipher = Cipher(algorithms.AES(message_key), modes.CBC(iv)) | ||
- | encryptor = cipher.encryptor() | ||
- | message = self.pad_message(message) | ||
- | enc_message = encryptor.update(message) + encryptor.finalize() | ||
- | |||
- | # TODO 2 Send own ephemereal public key (get it from the ClientSession with to_user_id) | ||
- | session = self.existing_sessions[to_user_id] | ||
- | raw_eph_own = session.eph_own.get_public().public | ||
- | |||
- | # TODO 2 Send encrypted message length to server | ||
- | |||
- | # TODO 2 Send encrypted message to server | ||
- | |||
- | def recv_message(self): | ||
- | """Receives a message | ||
- | It can be either from a new session exchange (tag=NEWS), or an actual message(tag=MESG)""" | ||
- | tag = self.s.recv(4) | ||
- | if tag == "NEWS": # NEW Session | ||
- | ini_user_id = struct.unpack('!i', self.s.recv(4))[0] | ||
- | self.setup_session_rec(ini_user_id) | ||
- | tag = self.s.recv(4) | ||
- | |||
- | if tag == "MESG": # message incoming | ||
- | # Get sender user id | ||
- | sender_user_id = struct.unpack('!i', self.s.recv(4))[0] | ||
- | |||
- | # Get ephemereal key | ||
- | raw_eph_peer = self.s.recv(32) | ||
- | |||
- | |||
- | session = self.existing_sessions[sender_user_id] | ||
- | # TOD2 3 If received eph_peer is unknown (different than the one stored in the | ||
- | # ClientSession), update the keys | ||
- | # The first time current eph_peer will be None, so make sure you update in that | ||
- | # case also | ||
- | if False: # change with actual condition | ||
- | eph_peer = PublicKey(raw_eph_peer) | ||
- | session.update_keys(eph_peer) | ||
- | |||
- | # Get message length | ||
- | mlen = struct.unpack('!i', self.s.recv(4))[0] | ||
- | |||
- | # Get message | ||
- | enc_message = self.s.recv(mlen) | ||
- | |||
- | # Decrypt message using MessageKey | ||
- | message_key = self.get_recv_message_key(sender_user_id) | ||
- | iv = enc_message[:16] | ||
- | enc_message = enc_message[16:] | ||
- | cipher = Cipher(algorithms.AES(message_key), modes.CBC(iv)) | ||
- | decryptor = cipher.decryptor() | ||
- | msg = decryptor.update(enc_message) + decryptor.finalize() | ||
- | msg = self.unpad_message(msg) | ||
- | |||
- | return msg | ||
- | |||
- | def record_new_client_session(self, master_secret, client_id, eph_peer = None): | ||
- | """Forges RootKey and ChainKey from master_secret and stores them in a ClientSession | ||
- | which is then put in the existing_sessions dictionary""" | ||
- | |||
- | root_key = master_secret # master_secret is considered as the base root key | ||
- | if eph_peer: # initiator considers Srec as initial peer ephemereal key | ||
- | eph_own = PrivateKey() # Generate new send ephemereal key | ||
- | |||
- | # TODO 2 Get root_key and chain_key_s with hkdf over DH(eph_own, eph_peer) and | ||
- | # master secret | ||
- | root_key = "" | ||
- | chain_key_s = "" | ||
- | chain_key_r = None # Will initialize with first received message | ||
- | else: # the non-initiator (first receiver) will enter this branch | ||
- | eph_own = self.S # initiator considers receiver's S as first "ephemereal" key | ||
- | chain_key_s = None | ||
- | chain_key_r = None | ||
- | |||
- | new_client_session = ClientSession(root_key, chain_key_s, chain_key_r, eph_own, eph_peer) | ||
- | self.existing_sessions[client_id] = new_client_session | ||
- | |||
- | def setup_session(self, to_user_id): | ||
- | """Initiates a new session with to_user_id""" | ||
- | self.s.send(b"GENS") # GENerate Session command | ||
- | self.s.send(struct.pack('!i', to_user_id)) | ||
- | resp = self.s.recv(4) | ||
- | if resp == "FAIL": | ||
- | print("User does not exist") | ||
- | return False | ||
- | |||
- | # TODO 1.1 Get Irec (done), Srec, Orec from server | ||
- | Irec = PublicKey(self.s.recv(32)) | ||
- | # Srec = ... | ||
- | # Prec = ... | ||
- | |||
- | # TODO 1.1 Generate Eini (ephemeral private key of initiator) | ||
- | # Eini = ... (see above for function to retrieve ephemeral private key) | ||
- | |||
- | # TODO 1.1 Compute master_secret from DH(Iini, Srec), DH(Eini, Irec), DH(Eini, Srec), DH(Eini, Orec) | ||
- | # See method do_exchange in donna library: https://github.com/Muterra/donna25519 | ||
- | # Use '+' to concatenate output of various DH exchanges | ||
- | master_secret = "" | ||
- | print(master_secret.hex()) | ||
- | |||
- | # TODO 1.2 Send Eini to the other client (through server) | ||
- | |||
- | # Generate new ClientSession using master_secret | ||
- | self.record_new_client_session(master_secret, to_user_id, Srec) | ||
- | |||
- | def setup_session_rec(self, from_user_id): | ||
- | """Receives a new session with from_user_id""" | ||
- | |||
- | # TODO 1.2 Receive initiator's public keys Eini, Iini and Orec_pub | ||
- | Eini = PublicKey(self.s.recv(32)) | ||
- | # Iini = ... | ||
- | # Orec_pub = ... | ||
- | |||
- | # TODO 1.2 Get private Orec from my O_queue based on received public Orec and then remove from O_queue | ||
- | |||
- | # TODO 1.2 Compute master_secret from DH(Iini, Srec), DH(Eini, Irec), DH(Eini, Srec), DH(Eini, Orec) | ||
- | # See above, in method setup_session | ||
- | master_secret = "" | ||
- | print(master_secret.hex()) | ||
- | |||
- | # Generate new ClientSession using master_secret | ||
- | self.record_new_client_session(master_secret, from_user_id) | ||
- | </code> | ||
- | |||
- | <code python 'wa_server.py'> | ||
- | from donna25519 import * | ||
- | import socket | ||
- | import threading | ||
- | import struct | ||
- | |||
- | TCP_IP = '127.0.0.1' | ||
- | MAX_CLIENTS = 10 | ||
- | |||
- | class ClientRecord: | ||
- | """Structure for keeping record of a client's public keys""" | ||
- | def __init__(self, I, S, O_queue, clientsocket): | ||
- | self.I = I | ||
- | self.S = S | ||
- | self.O_queue = O_queue | ||
- | self.s = clientsocket | ||
- | |||
- | class Server: | ||
- | def __init__(self, port): | ||
- | self.port = port | ||
- | self.next_id = 1 | ||
- | |||
- | self.registered_clients = {} | ||
- | |||
- | def start(self): | ||
- | """Start listening to client connections""" | ||
- | print("Starting server on port %d" % self.port) | ||
- | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
- | s.bind((TCP_IP, self.port)) | ||
- | s.listen(MAX_CLIENTS) | ||
- | while True: | ||
- | c, addr = s.accept() # connection with new client | ||
- | print("New client from address " + str(addr)) | ||
- | threading.Thread(target = self.on_new_client, args = (c, addr)).start() | ||
- | |||
- | s.close() | ||
- | | ||
- | def on_new_client(self, clientsocket, addr): | ||
- | """Runs when a new client connects to the server""" | ||
- | I = PublicKey(clientsocket.recv(32)) | ||
- | print("Received I = " + I.public.hex()) | ||
- | S = PublicKey(clientsocket.recv(32)) | ||
- | print("Received S = " + S.public.hex()) | ||
- | o_num = struct.unpack('!i', clientsocket.recv(4))[0] | ||
- | O_queue = [] | ||
- | for i in xrange(o_num): | ||
- | O_queue.append(PublicKey(clientsocket.recv(32))) | ||
- | print("Received O = " + O_queue[i].public.hex()) | ||
- | |||
- | user_id = self.register_client(I, S, O_queue, clientsocket) | ||
- | clientsocket.send(struct.pack('!i', user_id)) | ||
- | |||
- | while True: | ||
- | cmd = clientsocket.recv(4) | ||
- | if cmd == "GENS": # GENerate Session | ||
- | raw_id = clientsocket.recv(4) | ||
- | rec_user_id = struct.unpack('!i', raw_id)[0] | ||
- | if rec_user_id not in self.registered_clients: | ||
- | clientsocket.send("FAIL") | ||
- | continue | ||
- | clientsocket.send(b"GOOD") | ||
- | # Send Irec, Srec, Orec to initiator | ||
- | Irec = self.registered_clients[rec_user_id].I | ||
- | Srec = self.registered_clients[rec_user_id].S | ||
- | Orec = self.registered_clients[rec_user_id].O_queue.pop() | ||
- | clientsocket.send(Irec.public) | ||
- | clientsocket.send(Srec.public) | ||
- | clientsocket.send(Orec.public) | ||
- | |||
- | # get Eini from initiator | ||
- | Eini = PublicKey(clientsocket.recv(32)) | ||
- | |||
- | rec_clientsocket = self.registered_clients[rec_user_id].s | ||
- | rec_clientsocket.send(b"NEWS") # NEW Session | ||
- | rec_clientsocket.send(struct.pack('!i', user_id)) | ||
- | # Send Eini, Iini, Orec to recipient | ||
- | rec_clientsocket.send(Eini.public) | ||
- | rec_clientsocket.send(I.public) | ||
- | rec_clientsocket.send(Orec.public) | ||
- | if cmd == "SEND": # SEND Message | ||
- | # Get recipient used id | ||
- | raw_id = clientsocket.recv(4) | ||
- | rec_user_id = struct.unpack('!i', raw_id)[0] | ||
- | |||
- | rec_clientsocket = self.registered_clients[rec_user_id].s | ||
- | rec_clientsocket.send(b"MESG") # send message tag | ||
- | |||
- | # Send sender user id | ||
- | rec_clientsocket.send(struct.pack('!i', user_id)) | ||
- | |||
- | # Forward sender ephemereal key | ||
- | eph_s = clientsocket.recv(32) | ||
- | rec_clientsocket.send(eph_s) | ||
- | |||
- | # Forward message length | ||
- | raw_mlen = clientsocket.recv(4) | ||
- | rec_clientsocket.send(raw_mlen) | ||
- | |||
- | # Forward message | ||
- | mlen = struct.unpack('!i', raw_mlen)[0] | ||
- | raw_msg = clientsocket.recv(mlen) | ||
- | rec_clientsocket.send(raw_msg) | ||
- | |||
- | def register_client(self, I, S, O_queue, clientsocket): | ||
- | """Registers new client's I, S, and O_queue and returns the new client's id""" | ||
- | new_client = ClientRecord(I, S, O_queue, clientsocket) | ||
- | |||
- | user_id = self.next_id | ||
- | self.next_id += 1 | ||
- | |||
- | self.registered_clients[user_id] = new_client | ||
- | |||
- | return user_id | ||
- | </code> |