This shows you the differences between two versions of the page.
ac:laboratoare:08 [2022/12/08 18:58] tiberiu.iorgulescu |
ac:laboratoare:08 [2024/11/14 13:05] (current) dimitrie.valu |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ===== Lab 08 - Whatsapp End-to-end Encryption ===== | + | ===== Lab 08 - Whatsapp End-to-end Encryption (part 2) ===== |
- | 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 continue the implementation 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 protocol is described [[https://cryptome.org/2016/04/whatsapp-crypto.pdf|here]]. |
- | WhatsApp's security is based on the Signal protocol, which was first used by TextSecure. The Signal protocol is | + | For more details you can also check [[https://s3.amazonaws.com/files.douglas.stebila.ca/files/research/papers/EuroSP-CCDGS17-full.pdf|this]] paper. |
- | described in detail in [[https://s3.amazonaws.com/files.douglas.stebila.ca/files/research/papers/EuroSP-CCDGS17-full.pdf|this]] paper. | + | |
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: | + | If you solved the previous lab, use your previous setup (replace the files with the ones from the ''%%.zip%%'' below to prevent any issues). If you are starting out with these labs, follow the steps below (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 | + | |
- | * Log-out from admin account | + | |
- | * Log-in with student as usual | + | |
- | + | ||
- | === Task 1 === | + | |
- | 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. | + | |
- | + | ||
- | == How to run == | + | |
- | Open three different terminals. | + | |
- | + | ||
- | First terminal: | + | |
- | <code>python main_server.py</code> | + | |
- | + | ||
- | Second terminal: | + | |
- | <code>python main_client.py</code> | + | |
- | + | ||
- | Third terminal: | + | |
- | <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 = raw_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> | ||
- | + | * Use ''%%wget%%'' to download the required zip (find it below) | |
- | <code python 'main_server.py'> | + | * Create a Python3 environment, make sure PyPI is up to date and install the required packages: |
- | from wa_client import Client | + | <code> |
- | from wa_server import Server | + | python3 -m venv create env |
- | + | source ./env/bin/activate | |
- | SERVER_PORT = 7778 | + | pip install --upgrade pip |
- | + | pip install cryptography donna25519 | |
- | if __name__ == '__main__': | + | |
- | s = Server(SERVER_PORT) | + | |
- | s.start() | + | |
</code> | </code> | ||
- | <code python 'wa_client.py'> | + | **If local installation does not work, use your ''%%fep%%'' instance.** |
- | from donna25519 import * | + | |
- | from Crypto.Cipher import AES | + | |
- | from Crypto import Random | + | |
- | 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 | + | === Task - Vertical & Horizontal ratcheting === |
- | def hmac_sha256(key, data): | + | |
- | return hmac.new(key, data, hashlib.sha256).digest() | + | |
- | def hkdf(length, ikm, salt = b""): | + | See the previous lab for how to create a common ''%%master_secret%%'' for two clients which communicate through a server. |
- | """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: | + | Then, send messages with different keys each time, by recalculating the Chain Key according to the Signal Protocol. |
- | 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): | + | Recalculate the Root Key for each round trip with the new DH public keys sent in messages. |
- | """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 | + | |
- | self.root_key = "" | + | |
- | self.chain_key_r = "" | + | |
- | # Update RootKey & sending ChainKey | + | For this task you need to embed a new ephemeral public key in each message, in order to create a new RootKey. |
- | 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 | + | |
- | self.root_key = "" | + | |
- | self.chain_key_s = "" | + | |
- | class Client: | + | You can find a good description of the ratcheting protocol [[https://signal.org/docs/specifications/doubleratchet/|here]]. |
- | 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 xrange(O_NUM)] # One-Time Pre Keys | + | |
- | self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | + | For the sake of simplicity, we will consider that all messages are in-order and none of them is lost. |
- | self.s.connect((server_ip, server_port)) | + | |
- | self.user_id = self.register() | + | |
- | # self.existing_sessions[user_id] = session with that user (ClientSession) | + | You may start this lab from {{:ac:laboratoare:lab08.zip|this}} code. |
- | self.existing_sessions = {} # initially no existing session | + | |
- | def register(self): | + | <note> |
- | """Lets the server know about its presence""" | + | To generate the root keys and chain keys (e.g. in the method update_keys) you need to basically apply |
- | # TODO 1.1 send public keys of I, S and the list of O to the server | + | the HKDF method provided (hkdf) with a 64 byte output (512 bits), |
+ | which is then split into the root key (first 32 bytes) and chain key (last 32 bytes) | ||
- | # You can send the list of O by first sending its length and then the keys one by one | + | You should do the same also in the method record_new_client_session, for the case when the client is the initiator. |
- | # use struct.pack('!i', my_int) to send 4 byte signed integers | + | When the client is not the initiator both chain keys (chain_key_s and chain_key_r) will be initialized after receiving a message, |
+ | so you can keep them as 'None'. As initiator you should only initialize root_key and chain_key_s as explained above, | ||
+ | while chain_key_r can be left as 'None' for now. | ||
+ | </note> | ||
- | # receive your user id from the server (might need later) | + | == How to run == |
- | # use struct.unpack('!i', received_int)[0] to get an integer | + | Open three different terminals. |
- | user_id = 0 # TODO 1.1 change with received user id | + | First terminal (start the server): |
- | print "Got User ID = %d" % user_id | + | <code>python main_server.py</code> |
- | 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 | + | |
- | # message_key = HMAC_SHA256(ChainKey, 0x01) | + | |
- | message_key = None | + | |
- | + | ||
- | # TODO 2 Update chain_key_s | + | |
- | # ChainKey = HMAC_SHA256(ChainKey, 0x02) | + | |
- | session.chain_key = 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 | + | |
- | # 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("SEND") | + | |
- | # TODO 2 Send to_user_id to server | + | |
- | + | ||
- | # Get MessageKey for next message | + | |
- | message_key = self.get_send_message_key(to_user_id) | + | |
- | + | ||
- | # Encrypt message using MessageKey | + | |
- | iv = Random.new().read(AES.block_size) | + | |
- | cipher = AES.new(message_key, AES.MODE_CBC, iv) | + | |
- | message = self.pad_message(message) | + | |
- | enc_message = iv + cipher.encrypt(message) | + | |
- | + | ||
- | # TODO 2 Send own ephemereal public key (get it from the ClientSession with to_user_id) | + | |
- | + | ||
- | # 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 = AES.new(message_key, AES.MODE_CBC, iv) | + | |
- | msg = cipher.decrypt(enc_message) | + | |
- | + | ||
- | 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 = None # TODO 2 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("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, Srec, Orec from server | + | |
- | + | ||
- | # TODO 1.1 Generate Eini (ephemeral private key of initiator) | + | |
- | + | ||
- | # TODO 1.1 Compute master_secret from DH(Iini, Srec), DH(Eini, Irec), DH(Eini, Srec), DH(Eini, Orec) | + | |
- | master_secret = "" | + | |
- | print master_secret.encode('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 | + | |
- | + | ||
- | # 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) | + | |
- | master_secret = "" | + | |
- | print master_secret.encode('hex') | + | |
- | # Generate new ClientSession using master_secret | + | Second terminal (start the first client and enter ''%%RECV%%'' mode: |
- | self.record_new_client_session(master_secret, from_user_id) | + | <code> |
+ | python main_client.py | ||
+ | RECV | ||
</code> | </code> | ||
- | <code python 'wa_server.py'> | + | Third terminal (start the second client and send a message): |
- | from donna25519 import * | + | <code> |
- | import socket | + | python main_client.py |
- | import threading | + | MSG <id_other_client> Hello! |
- | 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.encode('hex') | + | |
- | S = PublicKey(clientsocket.recv(32)) | + | |
- | print "Received S = " + S.public.encode('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.encode('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("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("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("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> | </code> | ||
- | |||
- |