#!/usr/bin/env python3 try: from bottle import route, run, post, request, error, abort from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from Crypto.Random.random import randint except: print(f"Install bottle and pycryptodome to run this local server") import sys sys.exit(1) ### # crypto utilities class Error(Exception): def __init__(self, status_code, msg): self.status = status_code self.msg = msg PORT_NUMBER = 1234 ERROR_key_too_long = 490 ERROR_encipher = 491 ERROR_decipher = 492 ERROR_padding = 599 KEY = "that's not a key" def pad_key(key): if len(key) in AES.key_size: return bytes(key, encoding="ASCII") size = 0 for s in AES.key_size: if s > len(key) and (size == 0 or s < size): size = s if size == 0: raise Error(ERROR_key_too_long, "AES key is too long") print(bytes(key + '\0'*(size-len(key)), encoding="ASCII")) return bytes(key + '\0'*(size-len(key)), encoding="ASCII") def encipher(cleartext, key): print(f"cleartext = '{cleartext}'") print(f"key = '{key}'") cleartext = cleartext padded_cleartext = pad(cleartext, AES.block_size) IV = bytes([randint(0, 255) for _ in range(AES.block_size)]) try: cipher = AES.new(key, mode=AES.MODE_CBC, IV=IV) ciphertext = cipher.encrypt(padded_cleartext) except Exception as e: raise Error(ERROR_encipher, str(e)) return IV+ciphertext def decipher(ciphertext, key): IV = ciphertext[0:AES.block_size] ciphertext = ciphertext[AES.block_size:] try: cipher = AES.new(key, mode=AES.MODE_CBC, IV=IV) padded_cleartext = cipher.decrypt(ciphertext) except Exception as e: raise Error(ERROR_decipher, str(e)) try: cleartext = unpad(padded_cleartext, AES.block_size) except ValueError: raise Error(ERROR_padding, "padding error") return cleartext ### # template def template(html): return """ """ + f"""
Ces liens ne sont pas nécessaires pour implémenter l'attaque mais vous permettent de configurer le serveur en choisissant la clé et de tester le chiffrement / déchiffrement. Vous pouvez aussi tester interactivement la réponse du serveur sur un message chiffré erroné.
Vous devrez par contre générer un message chiffré qui servira d'entrée à votre programme qui implémente l'attaque de Vaudenay.
""" ### # routes @route('/') def index(): return template("""Ceci est un petit serveur bottle pour expérimenter avec l'attaque de Vaudenay. Le chiffrement / déchiffrement utilisent la bibliothèque PyCryptodome.
Les seules requêtes nécessaires pour cette attaque sont des requête POST sur la route /check_process, avec un champs ciphertext contenant une chaine donnant le code hexadécimal du texte chiffré.
Pour faire ceci en Python, vous pourrez utiliser le code suivant, qui fait une
requête et renvoie le code de retour de cette requête : soit 200 (OK),
soit 599 (erreur de remplissage) :
from urllib import request
from urllib.error import HTTPError, URLError
def check(ciphertext):
"check ciphertext by sending a request to the server"
url = f"http://localhost:{PORT_NUMBER}/check"
data = bytes('ciphertext=' + ciphertext.hex(), encoding="ASCII")
req = request.Request(url, data, method="POST")
try:
resp = request.urlopen(req)
code = resp.getcode()
except HTTPError as e:
code = e.getcode()
except URLError as e:
import sys
print(f"** connection problem: {e}.")
print(f"** Is the server running on port {PORT_NUMBER}?")
sys.exit(2)
assert code in (200, 599)
return code
Vous pouvez transformer une chaine en héxadécimal (0123456789abcdef) en tableau d'octets avec
B = bytearray.fromhex(chaine)
Pour info, mon code (Python) pour l'attaque complète fait 125 lignes (en comptant la fonction check) et a été écrit en 2h.
""") @route('/change_key') def change_key_form(): return template(f"""""") @post('/change_key') def change_key_process(): global KEY KEY = request.forms.get('key').strip() pad_key(KEY) # check sanity return change_key_form() @route('/encipher') def encipher_form(): return template("""
text clair (ASCII): {cleartext.decode(encoding="ASCII")}
clé (ASCII): {KEY}
texte chiffré (héxadécimal) : {ciphertext.hex()}
""") except Error as e: abort(e.status, e.msg) @route('/decipher') def decipher_form(): return template("""text clair (ASCII): {cleartext.decode(encoding="ASCII")}
""") except Error as e: abort(e.status, e.msg) @route('/check') def check_form(): return template("""texte chiffré {hex.upper()} OK
""") except Error as e: abort(e.status, e.msg) @error(ERROR_key_too_long) @error(ERROR_encipher) @error(ERROR_decipher) @error(ERROR_padding) def error_page(error): return template(f"""message : {error.body}
""") if __name__ == "__main__": from sys import argv if len(argv) > 2: print(f"usage: {argv[0] [KEY]}") exit(1) if len(argv) > 1: KEY = argv[1] pad_key(KEY) else: # import string # alpha = string.ascii_letters + string.digits # KEY = "".join([alpha[randint(0, len(alpha)-1)] for _ in range(16)]) KEY = "that's not a key" # run(host='localhost', port=PORT_NUMBER, debug=True, reloader=True) # run(host='localhost', port=PORT_NUMBER, debug=True) run(host='localhost', port=PORT_NUMBER)