#!/usr/bin/env python3 try: from bottle import route, run, post, request, error, abort except ModuleNotFoundError: print("This scripts requires bottle to run") import sys sys.exit(1) try: from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from Crypto.Random.random import randint except ModuleNotFoundError: print("This scripts requires pycryptodome to run") import sys sys.exit(1) PORT_NUMBER = 1234 ERROR_key_too_long = 490 ERROR_encipher = 491 ERROR_decipher = 492 ERROR_padding = 599 # default key value KEY = "That's not a very good key!" ### # crypto utilities class Error(Exception): def __init__(self, status_code, msg): self.status = status_code self.msg = msg 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}'") 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, 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
OK = 200
PADDING_ERROR = 599
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 (OK, PADDING_ERROR)
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]}") print("without KEY, a default key is used") print("if KEY is equal to RANDOM, a random key is used") exit(1) if len(argv) == 2: KEY = argv[1] if KEY == "RANDOM": import string alpha = string.ascii_letters + string.digits KEY = "".join([alpha[randint(0, len(alpha)-1)] for _ in range(16)]) pad_key(KEY) else: assert len(argv) == 1 # default KEY was defined at the top of file print(f">>> No key given: using KEY='{KEY}'.\n") # run(host='localhost', port=PORT_NUMBER, debug=True, reloader=True) # run(host='localhost', port=PORT_NUMBER, debug=True) run(host='localhost', port=PORT_NUMBER)