--- title: 'Sécurisation des bases de données' author: 'Brice Allenet & Erwan Benard' disqus: hackmd --- Sécurisation des bases de données === ALLENET Brice e2101070 BENARD Erwan e1900696 Cyberdéfense 4A TD3 ## Sommaire [TOC] ## 1. Introduction Ce document traite du TP de sécurisation de base de données réalisé dans le cadre du parcours Cyberdéfense de l'ENSIBS en quatrième année. Ce TP est composé de trente-deux questions et nous allons nous intéresser ici à la question 31. ### 1.1 Enoncés de la question 31 Pour rappel, les énoncés de cette question traitée sont les suivants : 31 / définissez une nouvelle base mysql avec une table dont une colonne contenant des entiers est chiffrée à l'aide d'un algorithme préservant la relation d'ordre — aka. Order Revealing Encryption — (afin que les requêtes sur intervalles soient permises), puis donnez l'implémentation (en python, c, ou java) du middleware et d'une application cliente de cette base illustrant la récupération et le déchiffrement des informations de la base ainsi que le bon fonctionnement de la relation d'ordre. Focalisez-vous sur la preuve de fonctionnement, on n'attend pas ici un produit fini avec parseur de requête, une application cliente effectuant une requête sur intervalle, vérifiant la réponse, et terminant, est suffisant. 31 bis/ on souhaite effectuer une opération à distance sur des données qui nous appartiennent, sans pour autant révéler à la machine distante le contenu de ces données. Implémentez un middleware côté serveur, capable d'effectuer une somme sur des données chiffrées par un client (le client est en mesure de déchiffrer cette somme). Combinez les deux middlewares afin que : - il soit possible de comparer des entiers chiffrées (cf. relation d'ordre) - il soit possible d'additionner des entiers chiffrées ### 1.2 Problématique de cette question Au travers de cette question, nous cherchons donc à créer une base de données MySQL contenant une table avec une colonne avec un identifiant et une autre avec une valeur chiffrée. Nous allons utiliser cette base à l'aide d'un algorithme de chiffrement préservant la relation d'ordre pour effectuer des requêtes. Pour réaliser cela, nous devons donc créer un middleware et une application cliente afin d'effectuer une PoC (Proof of Concept) dans laquelle nous allons formuler une requête et vérifier la réponse formulée pour effectuer à terme une comparaison et une addition des valeurs chiffrées. ## 2. Présentation du code ### 2.1 Création du client Nous allons décrire ici le fonctionnement général de notre code : Tout d'abord, le script importe les modules nécessaires, notamment le module socket pour établir une connexion réseau et le module pyope.ope pour chiffrer les données. Ensuite, le script initialise une connexion client en créant un objet de type socket avec l'adresse du serveur et le port correspondant. Une fois la connexion établie, le client reçoit un message d'accueil du serveur qui est affiché. La boucle principale commence alors. À chaque tour de boucle, le client demande à l'utilisateur d'entrer une requête SQL. Si la requête contient l'expression "{{OHE}}", cela signifie qu'un ou plusieurs paramètres de la requête doivent être chiffrés. Le client demande alors à l'utilisateur d'entrer les valeurs correspondantes, qui seront converties en chaînes de caractères et stockées dans une liste. La requête SQL et les paramètres chiffrés (s'il y en a) sont ensuite assemblés dans une chaîne de caractères séparée par "|||" et envoyés au serveur à l'aide de la méthode "send" de l'objet socket. Le serveur reçoit la requête, la décrypte et exécute la requête SQL correspondante sur la base de données. Il envoie ensuite la réponse de la base de données au client, qui l'affiche à l'utilisateur. La boucle continue ainsi jusqu'à ce que l'utilisateur quitte le programme en utilisant un raccourci clavier ou en fermant la fenêtre. Enfin, le client ferme la connexion avec le serveur en utilisant la méthode "close" de l'objet socket. Pour rappel, le module OPE (Order Preserving Encryption) en Python est un outil qui permet de chiffrer des données tout en préservant l'ordre de ces dernières. Cela signifie que si deux valeurs sont chiffrées avec OPE, la valeur chiffrée de la plus petite des deux sera toujours inférieure à la valeur chiffrée de la plus grande. Vous pourrez retrouver le code au sein de **l'Annexe Code** sous le nom *client.py*. ### 2.2 Création du serveur Nous allons décrire ici le fonctionnement général de notre code : Le serveur est un serveur de base de données SQLite qui peut être utilisé pour stocker des salaires et effectuer des requêtes dessus. Le serveur écoute les connexions entrantes et attend les requêtes des clients. La classe SqliteConnector initialise la connexion à la base de données SQLite et fournit des méthodes pour exécuter des requêtes sur la base de données. La méthode query_encrypt chiffre les paramètres de requête pour la protection des données en utilisant la bibliothèque pyope.ope qui implémente un chiffrement basé sur la permutation ordonnée. La méthode query_decrypt décrypte les résultats des requêtes. Le serveur écoute les connexions entrantes sur l'adresse localhost et le port 1200. Lorsqu'un client se connecte, il envoie un message de bienvenue au client, contenant une explication sur l'utilisation de la chaîne de caractères {{OHE}} dans la requête pour l'insertion de valeurs entières chiffrées. Le serveur lit les requêtes des clients, les exécute sur la base de données SQLite et renvoie les résultats au client. Si une erreur se produit, le serveur renvoie un message d'erreur au client. Le serveur est conçu pour fonctionner en continu et attend les requêtes des clients tant qu'il est en cours d'exécution. Le code utilise également une fonction nth_replace qui remplace la n-ième occurrence d'une chaîne dans une autre chaîne. Cette fonction est utilisée pour remplacer la chaîne {{OHE}} par la valeur entière chiffrée dans la requête. Vous pourrez retrouver le code au sein de **l'Annexe Code** sous le nom *serveur.py*. ## 3. Tests Afin de valider le bon fonctionnement de notre application, nous précédons à une phase de tests. Dans un premier temps, nous exécutons le code *serveur.py* : ![](https://i.imgur.com/9vRCr8F.png) Ici le serveur s'exécute et initialise la base de données. La table n'existant pas, il la crée en respectant ce format : | Nom | Type | | ------- |:----------------:| | id | INTEGER NOT NULL | | nom | text not null | | montant | text not null | Ensuite, nous exécutons le client *client.py* qui se connecte automatiquement au serveur : ![](https://i.imgur.com/xwGFAON.png) Maintenant, nous pouvons exécuter différentes commandes afin de tester les fonctionnalités. Premièrement, nous remplissons la base : ![](https://i.imgur.com/dJfynw3.png) Nous pouvons ensuite récupérer toutes les valeurs en clair au sein de la base : ![](https://i.imgur.com/DzaqvCf.png) Du côté serveur, les valeurs sont chiffrées et non visible en clair, nous pouvons le vérifier en observant les logs du côté client : ![](https://i.imgur.com/6ssxgkJ.png) Ici les valeurs sont bien chiffrées et non disponibles en clair. On comprend alors qu'il nous est impossible d'obtenir les informations du côté serveur. Enfin, le dernier test consiste à tester la possibilité de comparaison entre valeurs. Pour cela; nous pouvons sélectionner les valeurs supérieures à un certain montant : ![](https://i.imgur.com/uD23V5t.png) Du côté du serveur, le montant est remplacé de la même façon que lors du remplissage de la table puis comparé : ![](https://i.imgur.com/HDX7BPo.png) ## 4. Conclusion Au terme de notre implémentation de l'OPE dans la partie précédente, nous pouvons donc voir que les deux opérations recherchées qu'étaient la comparaison et l'addition de valeurs chiffrées fonctionnent. Nous avons choisi d'utiliser l'OPE puisque c'est donc une technique de chiffrement permettant de conserver l'ordre des données après leur chiffrement tout comme l'ORE. Cependant, elle se différencie de l'ORE sur le fait que l'ORE est utilisée lorsque l'on veut trier les données chiffrées dans un ordre spécifique. Annexe code --- Code client.py ```python= import socket from pyope.ope import OPE # Initialisation du client et connexion au serveur client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_address = ('localhost', 1200) client_socket.connect(server_address) # Réception du message de bienvenue du serveur message_arrivé = client_socket.recv(1024).decode() print(message_arrivé) # Boucle pour envoyer des requêtes SQL au serveur while True: # Lecture de la requête SQL entrée par l'utilisateur sql = input("SQL>") # Lecture des paramètres de la requête chiffrés avec OPE ohe_params = [] if '{{OHE}}' in sql: for i in range(sql.count('{{OHE}}')): while True: try: x = int(input(f"ohe arg {i} :")) ohe_params.append(str(x)) break except: print("[x] OHE Args must be integers !") pass ohe_params_str = ';'.join(ohe_params) trame = f'{sql}|||{ohe_params_str}' client_socket.send(trame.encode()) db_response = client_socket.recv(1024).decode() print(db_response) client_socket.close() ``` Code serveur.py ```python= import socket import sqlite3 as sl from pyope.ope import OPE class SqliteConnector: def __init__(self, name): self.ohe_key = OPE.generate_key() self.ohe = OPE(self.ohe_key) self.con = sl.connect(name) self.drop_table() self.create_table() def query_encrypt(self, query: str, ohe_params: list): new_query = query for i, ohe_param in enumerate(ohe_params): if i == 0 and ohe_param == '': pass else: print("Replace {{OHE}} with", ohe_param) new_query = self.nth_replace(new_query, '{{OHE}}', f"{self.ohe.encrypt(int(ohe_param))}", i + 1) print("[ DEBUG ] Query :", new_query) return new_query def query_decrypt(self, query_result): decrypted_result = [] if len(query_result) > 0: for id, nom, montant in query_result: print("Found :", id, nom, montant) decrypted_result.append((id, nom, self.ohe.decrypt(int(montant)))) return decrypted_result else: return query_result def nth_replace(self, string, old, new, n=1, option='only nth'): """ Cette fonction remplace les occurrences de la chaîne "ancienne" par la chaîne "nouvelle". Il existe trois types de remplacement de la chaîne 'old' : 1) "only nth" remplace uniquement la nième occurrence (par défaut). 2) "all left" remplace la nième occurrence et toutes les occurrences à gauche. 3) 'all right' remplace la nième occurrence et toutes les occurrences à droite. """ if option == 'only nth': left_join = old right_join = old elif option == 'all left': left_join = new right_join = old elif option == 'all right': left_join = old right_join = new else: print("Invalid option. Please choose from: 'only nth' (default), 'all left' or 'all right'") return None groups = string.split(old) nth_split = [left_join.join(groups[:n]), right_join.join(groups[n:])] return new.join(nth_split) def drop_table(self): try: with self.con: self.con.execute(""" drop table salaire; """) print(" -> Existing table salaire droped from database") except Exception as err: print(err) pass def create_table(self): try: with self.con: self.con.execute(""" create table salaire ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, nom text not null, montant text not null ); """) print(" -> New Table salaire create in database") except Exception as err: print(err) pass def query(self, sql_query: str, ohe_parameters: list): with self.con: r = self.con.execute(self.query_encrypt(sql_query, ohe_parameters), []) return self.query_decrypt(r.fetchall()) server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_address = ('localhost', 1200) server_socket.bind(server_address) server_socket.listen(1) print('[-] Database initialization:') con = SqliteConnector('secure.db') client_socket, client_address = server_socket.accept() print('[-] Server waiting for connection...') print(f'[!] Connection established') ``` ###### tags: `ENSIBS` `Documentation` `Brice Allenet` `Erwan Benard` `TD3` `TP5` `Sécurité des bases de données`