#!/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"""

Attaque de Vaudenay ("padding oracle attack")

{html}

liens

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("""

Description du serveur

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"""

changement de clé

clé (ASCII) :

""") @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("""

Chiffrer un texte

texte clair (ASCII) :
""") @post('/encipher') def encipher_process(): try: cleartext = bytes(request.forms.get('clear').strip(), encoding="ASCII") ciphertext = encipher(cleartext, pad_key(KEY)) return template(f"""

Résultat du chiffrement

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("""

Déchiffrer un texte

texte chiffré (hexadécimal) :
""") @post('/decipher') def decipher_process(): try: hex = request.forms.get('ciphertext').strip() ciphertext = bytes.fromhex(hex) cleartext = decipher(ciphertext, pad_key(KEY)) return template(f"""

Résultat du déchiffrement

text clair (ASCII): {cleartext.decode(encoding="ASCII")}

""") except Error as e: abort(e.status, e.msg) @route('/check') def check_form(): return template("""

Envoyer un texte chiffré pour vérification

texte chiffré (hexadécimal) :
""") @post("/check") def check_process(): try: hex = request.forms.get('ciphertext').strip() ciphertext = bytes.fromhex(hex) decipher(ciphertext, pad_key(KEY)) return template(f"""

Vérification d'un texte chiffré

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"""

ERREUR {error.status_code}

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)