Réseaux et télécommunications
Fermer ×

Programmation réseau

Programmation en C

Avertissement

Pour la conception de programmes en C, voir le chapitre sur le langage C.

Tous les exemples de programmes C ont été compilés avec gcc et make et testés sous linux.

Structures de données spécifiques aux réseaux

Les fonctions de gestion du réseau utilisent deux principales structures de données, la première qui contient les informations de la connexion réseau, et la deuxième qui contient les informations utilisées par les fonctions de connexions et/ou d'échanges des données sur le réseau. Les champs de cette dernière structure dépendent du type de connexion ipv4 ou ipv6.

Structure pour la connexion ipv4

La structure sockaddr_in contient les champs :
  • sin_family qui vaut AF_INET pour ipv4
  • sin_port qui est un entier qui contient le numéro de port sur 16 bits au format big endian
  • sin_addr structure qui contient le champ :
    • s_addr de type uint32_t qui contient l'adresse ip au format entier 32 bits avec abcd qui correspond au déplacement croissant en mémoire pour une adresse ip de la forme a.b.c.d

Structure pour la connexion ipv6

La structure sockaddr_in6 contient les champs :

  • sin6_family qui vaut AF_INET6 pour ipv6
  • sin6_port qui est un entier qui contient le numéro de port sur 16 bits au format big endian
  • sin6_addr structure qui contient le champ :
    • s6_addr de type tableau de 16 octets non signés.

La structure addrinfo, élément d'une liste chaînée, qui contient principalement les champs :

Les formats des données entières peuvent être différentes entre le réseau et l'ordinateur, afin d'éviter toute erreur, il convient d'utiliser les fonctions adaptées pour le numéro de port et l'adresse ip, voici quelques exemples de fonctions :

Accès aux informations réseaux

Ces informations sont accessibles à l'aide de la fonction getaddrinfo qui, en plus de fournir les informations des interfaces réseaux, permet de fournir l'adresse ip à partir du nom FQDN (Fully qualified domain name) en utilisant la résolution DNS si cela est possible. Ces informations sont stockées dans la liste chaînée d'éléments de type addrinfo.

Utilisation du protocole connecté TCP

Principe de fonctionnement du client et du serveur

Client

  1. Paramétrage de l'interface pour le type d'ip ainsi que le protocole
  2. Initialisation de la structure de données de la connexion
  3. Connexion au serveur en utilisant la structure de données de la connexion
  4. Echange des données avec les fonctions de lecture et d'écriture de données sur le réseau
  5. Fermeture de la connexion

Serveur

  1. Paramétrage de l'interface pour le type d'ip ainsi que le protocole
  2. Initialisation de la structure de données de la connexion
  3. Eventuellement configuration supplémentaire de l'interface en utilisant la structure de données de la connexion
  4. Assignation de l'interface avec l'adresse IP
  5. Paramétrage du mode écoute
  6. Attente bloquante de la connexion du client
  7. Echange des données avec les fonctions de lecture et d'écriture de données sur le réseau
  8. Fermeture des échanges
  9. Fermeture de la connexion

Les fonctions en C

Exemple de programmes client et serveur en mode connecté avec ipv4

Voir le code des fonctions de configuration de la connexion et d'initialisation des modes clients et serveurs

Toutes ces fonctions peuvent être groupées dans un fichier netcommon.c et définies dans le fichier netcommon.h, dans lequel, il ne faut pas oublier d'inclure les fichiers de prototypes et constantes : rpa/inet.h et netdb.h

Il faudra déclarer les constantes


#define NON_ERREUR 0
#define ERREUR -1
#define SOCK_ERR -1

#define PORT_DEFAUT	1234
#define IP_DEFAUT	"127.0.0.1"
#define NOM_DEFAUT	"localhost"
#define TAILLE_BUFFER 1024
#define TAILLE_NOM_IP 100				
			

Fonction de configuration de la structure de données


int createsock(char *nom,unsigned int port,struct sockaddr_in *hote) {
	struct addrinfo *hoteinfo, hotecfg;
	int lg = 0 ;
	hote->sin_family = AF_INET ;
	hote->sin_port=htons(port) ;
	int ipAddr = inet_addr(nom);
	if (ipAddr != INADDR_NONE) {
		lg = sizeof(struct sockaddr_in) ;
		hote->sin_addr.s_addr = ipAddr ;
	}
	else {
		bzero(&hotecfg, sizeof(struct addrinfo));
		hotecfg.ai_family = AF_INET ;
		int err = getaddrinfo(nom,NULL,&hotecfg,&hoteinfo);
		if (err == 0) {
			lg = hoteinfo->ai_addrlen;
			hote->sin_addr.s_addr = ((struct sockaddr_in *)(hoteinfo->ai_addr))->sin_addr.s_addr ;	
		}
		else {
			perror("erreur ");
		}
		freeaddrinfo(hoteinfo);
	}
	return lg;
}
					

Cette fonction initialise les champs d'une structure de type sockaddr_in. L'adresse IP est soit transmise sous la forme d'une chaîne de caractère, soit obtenue à partir du nom DNS FQDN (Fully qualified domain name).

Afin de détecter si le nom correspond à une adresse IP ou bien un nom, on utilise la fonction inet_addr. Si la fonction retourne une erreur, on considère qu'il s'agit d'un nom et on retrouve l'adresse IP avec la fonction getaddrinfo.

Cette fonction retourne la taille de la structure de données ou bien 0 en cas d'erreur.

La fonction htons permet de convertir le type entier de l'ordinateur au type entier du réseau quelque soit le format utilisé par l'ordinateur.

La fonction bzero permet d'initialiser à 0 une zone mémoire.

Initialisation mode client


int InitClientSocket(unsigned int numport,struct sockaddr_in *p_socket_adresse) {
	int clientsocket;
	int err;
	clientsocket = socket(PF_INET, SOCK_STREAM, 0);
	if (clientsocket == SOCK_ERR) {
		perror("Erreur socket ");
		return clientsocket;
	}
	err=connect(clientsocket,(struct sockaddr *)p_socket_adresse,sizeof(struct sockaddr_in));
	if (err == SOCK_ERR) {
		perror("Erreur connect ");
		return err;
	}
	return clientsocket;
}
					

La fonction socket initialise la connexion et retourne un identifiant qui est utilisé par les fonctions qui accèdent au réseau.

  • PF_INET pour un socket de type réseau.
  • SOCK_STREAM pour un protocole connecté TCP.

La fonction connect établit une connexion avec le serveur en utilisant la structure de donnée sockaddr_in.

Cette fonction d'initialisation retourne l'identifiant de connexion ou un code d'erreur (-1).

Cet identifiant sera ensuite utilisé par les fonctions de lecture et d'écriture.

Initialisation mode serveur


int InitServerSocket(unsigned int numport, struct sockaddr_in *p_socket_adresse) {
	int serveursocket;
	int err=0;
	serveursocket = socket(PF_INET, SOCK_STREAM, 0);
	if (serveursocket == SOCK_ERR) {
		return serveursocket;
	}
	int p=1;
	err=setsockopt (serveursocket, SOL_SOCKET, SO_REUSEADDR, &p, sizeof(int));
	if (err == SOCK_ERR) {
		perror("Erreur socket ");
		return err;
	}
	err=bind(serveursocket, (struct sockaddr *)p_socket_adresse, sizeof(struct sockaddr_in ));
	if (err == SOCK_ERR) {
		perror("Erreur bind ");
		return err;
	}
	err=listen(serveursocket, 10);
	if (err == SOCK_ERR) {
		perror("Erreur listen ");
		return err;
	}
	return serveursocket;
}
					

La fonction setsockopt permet de modifier le paramétrage de la connexion par défaut. Ici elle est utilisée pour permettre de réutiliser la connexion, immédiatement après une erreur, ce qui n'est pas le cas par défaut où il faut attendre un certain temps avant de refaire un essai.

La fonction bind assigne la connexion à la carte réseau. Pour un serveur, il n'y a pas de fonction connect.

La fonction listen définit la taille de la file d'attente pour les connexions.

Cette fonction d'initialisation retourne un identifiant pour la fonction d'attente de connexion ou une erreur (-1) en cas de problème.

Voir le code des programmes de test clients et serveurs

Programme client


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"

int main(int nbargs,char *args[]) {
  int clientsocket;
  struct sockaddr_in sock_adresse;
  int port = PORT_DEFAUT ;
  char nom[TAILLE_NOM_IP] = NOM_DEFAUT;
  int lg ;
  int taille;
  char buffer[TAILLE_BUFFER];

  printf("connexion à %s port %d\n",nom,port);
  lg = createsock(nom,port,&sock_adresse);
  if (lg == 0) {
     perror("Erreur nom machine ou ip");
     return EXIT_FAILURE;	  
  }
  clientsocket=InitClientSocket(port,&sock_adresse);
  if (clientsocket == SOCK_ERR) {
     perror("Erreur connect");
     return EXIT_FAILURE;
  }
  printf("connecte\n");
  if (nbargs == 2) {
	  strcpy(buffer,args[1]);
  }
  else {
	strcpy(buffer,"bonjour");
  }
  taille = strlen(buffer);
  printf("envoi : %s\n",buffer);
  send(clientsocket,buffer,taille+1,0);
  taille = recv(clientsocket,buffer,TAILLE_BUFFER,0);
  printf("reçu : %s\n",buffer);
  strcpy(buffer,"merci pour tout");
  taille = strlen(buffer);
  printf("envoi : %s\n",buffer);
  send(clientsocket,buffer,taille+1,0);  
  close(clientsocket);   
  return EXIT_SUCCESS;
}
					

Le programme de test envoie un texte qui peut être fourni en ligne de commande ou "bonjour" par défaut.

Après avoir initialiser la connexion avec le nom ou l'adresse IP du serveur et le numéro de port. Le programme envoie le texte, attend la réponse puis envoie le message "merci pour tout".

Le programme envoie la chaîne avec le caractère 0 de fin de chaîne. Ce qui explique la valeur taille+1 dans la fonction send.

L'affichage de la réponse fonctionne si, comme ce programme, le serveur a envoyé le 0 de fin de chaîne.

Il ne faut pas oublier la fonction close qui termine la connexion.

Exemple d'affichage

connexion à localhost port 1234
connecte
envoi : bonjour
reçu : bonjour OK
envoi : merci pour tout
					

Programme serveur


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"

int main(int nbargs,char *args[]) {
  int serveursocket, client;
  struct sockaddr_in socket_adresse;
  socklen_t long_adr;
  int port = PORT_DEFAUT;
  char buffer[TAILLE_BUFFER];
  int taille;
  
  long_adr = createsock("0.0.0.0",port,&socket_adresse);
  if (long_adr == 0) {
     perror("Erreur nom machine ou ip");
     return EXIT_FAILURE;	  
  }
  serveursocket = InitServerSocket(port,&socket_adresse);
  if (serveursocket == -1) {
    perror("Erreur socket ");
    return EXIT_FAILURE;
  }
  printf("Ecoute port %d\n",port);
  client = accept (serveursocket, (struct sockaddr *)&socket_adresse, &long_adr);
  printf("client connecte, ...\n");
  taille = recv(client,buffer,TAILLE_BUFFER,0);
  printf("reçu : %s\n",buffer);
  strcat(buffer," OK");
  taille = strlen(buffer);
  printf("envoi : %s\n",buffer);
  send(client,buffer,taille+1,0);
  taille = recv(client,buffer,TAILLE_BUFFER,0);
  printf("reçu : %s\n",buffer);
  close(client);
  close(serveursocket);
  return EXIT_SUCCESS;
}

					

La fonction accept est bloquante et attend la connexion du programme client. Cette fonction retourne un identifiant utilisé par les fonctions de lecture et écriture.

Ce programme attend le message du programme client, l'affiche, ajoute OK à la fin du message puis envoie le message complet. Il attend ensuite un dernier message du programme client qu'il affiche.

Il ne faut pas oublier la fonction close qui met fin à l'échange de données.

Il ne faut pas oublier la fonction close qui termine la connexion.

Exemple d'affichage

Ecoute port 1234
client connecte, ...
reçu : bonjour
envoi : bonjour OK
reçu : merci pour tout
					

Exemple de programmes client et serveur en mode connecté avec ipv6

Voir le code des fonctions de configuration de la connexion et d'initialisation des modes clients et serveurs

Toutes ces fonctions peuvent être groupées dans un fichier netcommon6.c et définies dans le fichier netcommon6.h, dans lequel, il ne faut pas oublier d'inclure les fichiers de prototypes et constantes : rpa/inet.h et netdb.h

Il faudra déclarer les constantes

#define NON_ERREUR 0
#define ERREUR -1
#define SOCK_ERR -1

#define PORT_DEFAUT	1234
#define IP_DEFAUT	"::1"
#define NOM_DEFAUT	"localhost"
#define TAILLE_BUFFER 1024				
			

Fonction de configuration de la structure de données


int createsock(char *nom,unsigned int port,struct sockaddr_in6 *hote) {
	struct addrinfo hotecfg, *hoteinfo;
	unsigned char *p = hote->sin6_addr.s6_addr;
	int lg = 0 ;
	hote->sin6_family=AF_INET6;
	hote->sin6_port=htons(port);
	int r = inet_pton(AF_INET6,nom, p);
	if (r == 1) {
		lg = sizeof(struct sockaddr_in) ;
		inet_pton(AF_INET6,nom, p);
	}
	else {
		bzero(&hotecfg,sizeof (hotecfg));
		hotecfg.ai_family = AF_INET6;
		int err = getaddrinfo(nom,NULL,&hotecfg,&hoteinfo);
		if (err == 0) {
			lg = hoteinfo->ai_addrlen;
			bcopy(((struct sockaddr_in6 *)(hoteinfo->ai_addr))->sin6_addr.s6_addr,hote->sin6_addr.s6_addr,lg);
		}
		else {
			fprintf(stderr,"ERREUR DNS\n");
		}
		freeaddrinfo(hoteinfo);
	}
	return lg;
}
					

Cette fonction initialise les champs d'une structure de type sockaddr_in6. L'adresse IP est soit transmise sous la forme d'une chaîne de caractère, soit obtenue à partir du nom DNS (FQDN (Fully qualified domain name)).

Afin de détecter si le nom correspond à une adresse IP ou bien un nom, on utilise la fonction inet_pton. Si la fonction retourne une erreur, on considère qu'il s'agit d'un nom et on retrouve l'adresse IP avec la fonction getaddrinfo.

Cette fonction retourne la taille de la structure de données ou bien 0 en cas d'erreur.

La fonction htons permet de convertir le type entier de l'ordinateur au type entier du réseau quelque soit le format utilisé par l'ordinateur.

La fonction bzero permet d'initialiser à 0 une zone mémoire.

Initialisation mode client


int InitClientSocket6(unsigned int numport,struct sockaddr_in6 *p_socket_adresse) {
	int clientsocket;
	int err;
	clientsocket = socket(PF_INET, SOCK_STREAM, 0);
	if (clientsocket == SOCK_ERR) {
		perror("Erreur socket ");
		return clientsocket;
	}
	err=connect(clientsocket,(struct sockaddr *)p_socket_adresse,sizeof(struct sockaddr_in6));
	if (err == SOCK_ERR) {
		perror("Erreur connect ");
		return err;
	}
	return clientsocket;
}
					

En dehors de la structure de donnée sockaddr_in6, cette fonction est identique à celle utilisée avec ipv4.

Initialisation mode serveur


int InitServerSocket6(unsigned int numport, struct sockaddr_in6 *p_socket_adresse) {
	int serveursocket;
	int err=0;
	serveursocket = socket(PF_INET, SOCK_STREAM, 0);
	if (serveursocket == SOCK_ERR) {
		perror("Erreur socket ");
		return serveursocket;
	}  
	int p=1;
	err=setsockopt (serveursocket, SOL_SOCKET, SO_REUSEADDR, &p, sizeof(int));
	if (err == SOCK_ERR) {
		perror("Erreur socket opt");
		return err;
	}
	err=bind(serveursocket, (struct sockaddr *)p_socket_adresse, sizeof(struct sockaddr_in6));
	if (err == SOCK_ERR) {
		perror("Erreur bind ");
		return err;
	}
	err=listen(serveursocket, 10);
	if (err == SOCK_ERR) {
		perror("Erreur listen ");
		return err;
	}
	return serveursocket;
}
					

En dehors de la structure de donnée sockaddr_in6, cette fonction est identique à celle utilisée avec ipv4.

Voir le code des programmes de test clients et serveurs

Programme client


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "net6common.h"

int main(int nbargs,char *args[]) {
  int clientsocket;
  struct sockaddr_in6 socket_adresse;
  int port = PORT_DEFAUT ;
  char nom[100] = NOM_DEFAUT;
  char buffer[TAILLE_BUFFER];
  int lg;
  int taille ;

  printf("connexion à %s port %d\n",nom,port);
  lg = createsock(nom,port,&socket_adresse);
  if (lg == 0) {
     perror("Erreur nom machine ou ip");
     return EXIT_FAILURE;	  
  }
  clientsocket=InitClientSocket6(port,&socket_adresse);
  if (clientsocket == SOCK_ERR) {
     perror("Erreur connexion");
     return EXIT_FAILURE;
  }
  printf("connecte\n");
  if (nbargs == 2) {
	  strcpy(buffer,args[1]);
  }
  else {
	strcpy(buffer,"bonjour");
  }
  taille = strlen(buffer);
  printf("envoi : %s\n",buffer);
  send(clientsocket,buffer,taille+1,0);
  taille = recv(clientsocket,buffer,TAILLE_BUFFER,0);
  printf("reçu : %s\n",buffer);
  strcpy(buffer,"merci pour tout");
  taille = strlen(buffer);
  printf("envoi : %s\n",buffer);
  send(clientsocket,buffer,taille+1,0);  
  close(clientsocket);   
  return EXIT_SUCCESS;
}
					

En dehors de la structure de donnée sockaddr_in6, ce programme est identique à celle utilisée avec ipv4.

Programme serveur


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "net6common.h"

int main(int nbargs,char *args[]) {
  int serveursocket, client;
  struct sockaddr_in6 socket_adresse;
  socklen_t long_adr;
  int port = PORT_DEFAUT;
  char buffer[TAILLE_BUFFER];
  int taille;

  printf("connexion %d\n",port);
  long_adr = createsock("::0",port,&socket_adresse);
  if (long_adr == 0) {
     perror("Erreur nom machine ou ip");
     return EXIT_FAILURE;	  
  } 
  serveursocket = InitServerSocket6(port,&socket_adresse);
  if (serveursocket == -1) {
    perror("Erreur init socket ");
    return EXIT_FAILURE;
  }
  printf("Ecoute port %d\n",port);
  client = accept (serveursocket, (struct sockaddr *)&socket_adresse, &long_adr);
  printf("client connecte\n");
  taille = recv(client,buffer,TAILLE_BUFFER,0);
  printf("reçu : %s\n",buffer);
  strcat(buffer," OK");
  taille = strlen(buffer);
  printf("envoi : %s\n",buffer);
  send(client,buffer,taille+1,0);
  taille = recv(client,buffer,TAILLE_BUFFER,0);
  printf("reçu : %s\n",buffer);
  close(client);
  close(serveursocket);
  return EXIT_SUCCESS;
}
					

En dehors de la structure de donnée sockaddr_in6, ce programme est identique à celle utilisée avec ipv4.

Utilisation du protocole non connecté UDP

Principe de fonctionnement du client et du serveur

Client

  1. Paramétrage de l'interface pour le type d'ip ainsi que le protocole
  2. Initialisation de la structure de données de la connexion
  3. Echange des données avec les fonctions de lecture et d'écriture de données sur le réseau
  4. Fermeture de la connexion

Serveur

  1. Paramétrage de l'interface pour le type d'ip ainsi que le protocole
  2. Initialisation de la structure de données de la connexion
  3. Eventuellement configuration supplémentaire de l'interface en utilisant la structure de données de la connexion
  4. Assignation de l'interface avec l'adresse IP
  5. Echange des données avec les fonctions de lecture et d'écriture de données sur le réseau
  6. Fermeture de la connexion

Les fonctions en C

Exemple de programmes client et serveur en mode non connecté avec ipv4

Voir le code des fonctions de configuration de la connexion et d'initialisation des modes clients et serveurs

Initialisation mode client


int InitClientSocketUDP(unsigned int numport,struct sockaddr_in *p_socket_adresse) {
	int clientsocket;
	clientsocket = socket(PF_INET, SOCK_DGRAM, 0);
	if (clientsocket == SOCK_ERR) {
		perror("Erreur socket ");
		return clientsocket;
	}
	return clientsocket;
}
					

Par rapport au mode connecté, on utilise la valeur SOCK_DGRAM, et on n'utilise plus la fonction de connexion connect.

Initialisation mode serveur


int InitServerSocketUDP(unsigned int numport, struct sockaddr_in *p_socket_adresse) {
	int serveursocket;
	int err=0;
	serveursocket = socket(PF_INET, SOCK_DGRAM, 0);
	if (serveursocket == SOCK_ERR) {
		return serveursocket;
	}
	int p=1;
	err=setsockopt (serveursocket, SOL_SOCKET, SO_REUSEADDR, &p, sizeof(int));
	if (err == SOCK_ERR) {
		perror("Erreur socket ");
		return err;
	}
	err=bind(serveursocket, (struct sockaddr *)p_socket_adresse, sizeof(struct sockaddr_in ));
	if (err == SOCK_ERR) {
		perror("Erreur bind ");
		return err;
	}
	return serveursocket;
}
					

Par rapport au mode connecté, on utilise la valeur SOCK_DGRAM, et on n'utilise plus la fonction d'écoute listen.

Voir le code des programmes de test clients et serveurs

Programme client


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"

int main(int nbargs,char *args[]) {
  int clientsocket;
  struct sockaddr_in sock_adresse;
  int port = PORT_DEFAUT ;
  char nom[TAILLE_NOM_IP] = NOM_DEFAUT;
  int lg ;
  int taille;
  char buffer[TAILLE_BUFFER];

  printf("connexion à %s:%d\n",nom,port);
  lg = createsock(nom,port,&sock_adresse);
  if (lg == 0) {
     perror("Erreur nom machine ou ip");
     return EXIT_FAILURE;	  
  }
  clientsocket=InitClientSocketUDP(port,&sock_adresse);
  if (clientsocket == SOCK_ERR) {
     perror("Erreur connect");
     return EXIT_FAILURE;
  }
  if (nbargs == 2) {
	  strcpy(buffer,args[1]);
  }
  else {
	strcpy(buffer,"bonjour");
  }
  taille = strlen(buffer);
  printf("envoi : %s\n",buffer);
  int envoye  = sendto(clientsocket,buffer,taille+1,0,(struct sockaddr *)&sock_adresse, lg);
  printf("envoye %d\n",envoye);
  close(clientsocket);   
  return EXIT_SUCCESS;
}
					

Cette fois on utilise la fonction sendto qui utilise la structure de donnée de la connexion en plus.

Programme serveur


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"

int main(int nbargs,char *args[]) {
  int serveursocket;
  struct sockaddr_in socket_adresse;
  socklen_t long_adr;
  int port = PORT_DEFAUT;
  char buffer[TAILLE_BUFFER];
  int taille;
  
  long_adr = createsock("0.0.0.0",port,&socket_adresse);
  if (long_adr == 0) {
     perror("Erreur nom machine ou ip");
     return EXIT_FAILURE;	  
  }
  serveursocket = InitServerSocketUDP(port,&socket_adresse);
  if (serveursocket == -1) {
    perror("Erreur socket ");
    return EXIT_FAILURE;
  }
  printf("Ecoute port %d\n",port);
  taille = recvfrom(serveursocket, buffer, TAILLE_BUFFER, 0, (struct sockaddr *)&socket_adresse, &long_adr);
  printf("reçu %d : %s\n",taille,buffer);
  close(serveursocket);
  return EXIT_SUCCESS;
}
					

Il n'y a plus de fonction bloquante accept

Cette fois on utilise la fonction recvfrom qui utilise la structure de donnée de la connexion en plus. La structure socket_adresse contient les informations su client (ip et port) qui peuvent être utilisées dans la suite de l'échange par sendto

Contrôle de l'intégrité des données en C

Somme de contrôle ou checksum

Méthode de calcul

La somme de contrôle ou cheksum est la somme de tous les octets modulo 256. Si on a une suite de n octets, le calcul s'écrit :

s = ( k = 0 n - 1 a k ) mod 256

Code C


uint8_t calculsomme(unsigned char *donnees,int taille) {
	uint32_t somme = donnees[0];
	for(int i=1;i<taille;i+=1) {
		somme += donnees[i];
	}
	return (somme % 256);
}
				

L'octet est converti en entier 32 bits, puis l'addition est réalisée. On réalise donc des additions sur 32 bits. Le modulo est calculé sur 32 bits, puis converti sur 8 bits. Ici on applique la définition.


uint8_t calculsomme(unsigned char *donnees,int taille) {
	uint8_t somme = donnees[0];
	for(int i=1;i<taille;i+=1) {
		somme += donnees[i];
	}
	return somme;
}
				

Tout les calculs sont fait sur 8 bits. On a donc des additions modulo 256.

Ces deux fonctions donnent le même résultat avec un calcul différent, quelle est la propriété mathématique utilisée ?

Voir la solution

Distributivité du modulo par rapport à l'addition, également utilisée avec la preuve par neuf :

( a + b ) mod n = ( ( a mod n ) + ( b mod n ) ) mod n

Exemple de transmission avec checksum

Voir le code des programmes de test clients et serveurs

void printhex(unsigned char *buffer,int taille) {
	for(int i=0;i<taille;i+=1) {
		printf("%02x ",buffer[i]);
	}
	printf("\n");
}
					

La fonction de calcul de la somme de contrôle calculsomme et la fonction d'affichage d'un tableau d'octets en hexadécimal printhexsont inclus dans le fichier common.c et déclaré dans le fichier common.h

Programme client


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"
#include "common.h"

int main(int nbargs,char *args[]) {
  struct sockaddr_in sock_adresse;
  int clientsocket;
  int port = PORT_DEFAUT ;
  char nom[TAILLE_NOM_IP] = NOM_DEFAUT;
  unsigned char trame[TAILLE_BUFFER];
  unsigned char message[TAILLE_BUFFER];
  int lg ;
  int taille;

  printf("connexion à %s port %d\n",nom,port);
  lg = createsock(nom,port,&sock_adresse);
  if (lg == 0) {
     perror("Erreur nom machine ou ip");
     return EXIT_FAILURE;	  
  }
  clientsocket=InitClientSocket(port,&sock_adresse);
  if (clientsocket == SOCK_ERR) {
     perror("Erreur connect");
     return EXIT_FAILURE;
  }
  printf("connecte\n");
  if (nbargs == 2) {
	  strcpy((char *)message,args[1]);
  }
  else {
	strcpy((char *)message,"bonjour");
  }
  taille = strlen((char *)message);
  uint8_t somme = calculsomme(message,++taille);
  printf("somme = %02x\n",somme);
  bcopy(message,trame,taille);
  trame[taille++] = somme;
  printhex(trame,taille);
  send(clientsocket,trame,taille,0);
  close(clientsocket);   
  return EXIT_SUCCESS;
}
					

La somme de contrôle est calculée sur la chaîne de caractères en incluant le 0 de fin de chaîne. Puis elle ajoutée à la fin de la chaîne pour former la trame à transmettre.

Exemple d'affichage du résultat

connexion à localhost port 1234
connecte
somme = ff
62 6f 6e 6a 6f 75 72 00 ff
					

Programme serveur


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"
#include "common.h"

int main(int nbargs,char *args[]) {
  
  int serveursocket, client;
  struct sockaddr_in socket_adresse;
  socklen_t long_adr;
  int port = PORT_DEFAUT;
  unsigned char trame[TAILLE_BUFFER];
  unsigned char message[TAILLE_BUFFER];
  int taille;
  
  long_adr = createsock("0.0.0.0",port,&socket_adresse);
  if (long_adr == 0) {
     perror("Erreur nom machine ou ip");
     return EXIT_FAILURE;	  
  }
  serveursocket = InitServerSocket(port,&socket_adresse);
  if (serveursocket == -1) {
    perror("Erreur socket ");
    return EXIT_FAILURE;
  }
  printf("Ecoute port %d\n",port);
  client = accept (serveursocket, (struct sockaddr *)&socket_adresse, &long_adr);
  printf("client connecte, ...\n");
  taille = recv(client,trame,TAILLE_BUFFER,0);
  printhex(trame,taille);
  bcopy(trame,message,--taille);
  uint8_t sommerecu = trame[taille];
  bcopy(trame,message,taille);
  uint8_t sommecalcule = calculsomme(message,taille);
  if (sommerecu == sommecalcule) {
	  printf("crc %02x OK\n",sommerecu);
  }
  else {
	  printf("recu %02x, calcule %02x\n",sommerecu,sommecalcule);
  }
  close(client);
  close(serveursocket);
  return EXIT_SUCCESS;
}
					

On extrait le message de la trame, puis on calcule la somme de contrôle du message. On compare cette somme calculée avec le dernier octet de la trame qui contient la somme de contrôle transmise. Si ces deux sommes sont identiques, on peut dire que la transmission est sans erreur dans les limites de la redondance de cette somme de contrôle.

Exemple d'affichage du résultat

Ecoute port 1234
client connecte, ...
62 6f 6e 6a 6f 75 72 00 ff 
crc ff OK
					

Contrôle de redondance cyclique ou CRC

Corps de Galois

Définitions

Le traitement de suites binaires en faisant référence au corps de Galois à deux éléments nommé GF2 a déjà été présenté dans le chapitre logique séquentielle.

Le corps de Galois est un corps à éléments finis avec deux éléments {0,1} où l'opération d'addition est réduite à la fonction logique OU exclusif et la multiplication réduite à la fonction logique ET (fonctions décrites dans le chapitre algèbre de Boole). Cette représentation fait que les valeurs 1 et -1 sont représentés par 1.

On associe des polynômes aux suites binaires en partant soit de la puissance 0 ou bien de la puissance maximale, ainsi la suite binaire {1, 0, 1, 0, 0, 1} est représentée par le polynôme :

1 . x 0 + 1 . x 1 + 1 . x 2 + 0 . x 3 + 0 . x 4 + 1 . x 5
et donne la fonction associée :
f ( x ) = 1 + x 2 + x 5

Les propriétés des opérations binaires donnent les résultats des opérations sur les polynômes en algèbre modulo 2 :

Multiplication de polynômes en algèbre modulo 2
( 1 + x + x 4 ) ( 1 + x + x 3 ) = 1 + x + x 4 + x + x 2 + x 5 + x 3 + x 4 + x 7 = 1 + x 2 + x 3 + x 5 + x 7
Puissance de polynôme en algèbre modulo 2<
( 1 + x ) 2 = 1 + x + x + x 2 = 1 + x 2 ( 1 + x ) 3 = 1 + x + x 2 + x 3 ( 1 + x ) 4 = ( 1 + x 2 ) 2 = 1 + x 2 + x 2 + x 4 = 1 + x 4
Plus généralement :
( 1 + x ) 2 p = 1 + x 2 p
Division de polynômes en algèbre modulo 2

Représentation polynomiale

x7+x5+x3+x2+1x4+x+1
x7+x4+x3x3+x+1
x5+x4
x5+x2+x
x4+x
x4+x+1
0

Représentation binaire

1010110110011
10011   01011
00110   
11000 
10011 
01001 
10010
10011
00000
0

Représentation polynomiale

x7+x5+x3+x2+xx4+x+1
x7+x4+x3 x3+x+1
  x5+x4
  x5+x2+x
   x4
   x4+x+1
      x+1

Représentation binaire

1010111010011
10011   01011
00110   
11000 
10011 
01000 
10000
10011
00011
11

Calcul du CRC

Le CRC ou Code de redondance Cyclique est le reste de la division de la suite binaire par un polynôme choisi dans un ensemble de polynômes prédéfinis et normalisés, dont voici un extrait pour les crc 8 bits :

DésignationPolynômeValeur normaleValeur inverséeValeur réciproqueValeur réciproque inversée
CRC-8
x 8 + x 7 + x 6 + x 4 + x 2 + 1
0xD50xAB0x570xEA
CRC-8-AUTOSAR
x 8 + x 5 + x 3 + x 2 + x + 1
0x2F0xF40xE90x97
CRC-8-Bluetooth
x 8 + x 7 + x 5 + x 2 + x + 1
0xA70xE50xCB0xD3
CRC-8-CCITT
x 8 + x 2 + x + 1
0x070xE00xC10x83
CRC-8-Dallas
x 8 + x 5 + x 4 + 1
0x310x8C0x190x98
CRC-8-DARC
x 8 + x 5 + x 4 + x 3 + 1
0x390x9C0x390x9C
CRC-8-GSM-B
x 8 + x 6 + x 3 + 1
0x490x920x250xA4
CRC-8-SAE
x 8 + x 4 + x 3 + x 2 + 1
0x1D0xB80x710x8E
CRC-8-WCDMA
x 8 + x 7 + x 4 + x 3 + x + 1
0x9B0xD90xB30xCD

Exemple de calcul

On propose de calculer le CRC-8-SAE (0x1D) de l'octet de donnée 0xaa. Pour effectuer la division on multiplie le polynôme qui représente la donnée par x8 (ajout de 8 bits 0 à droite de la suite binaire). On va ensuite effectuer la division du polynôme

x 8 ( x 7 + x 5 + x 3 + x ) = x 15 + x 13 + x 11 + x 9
par
x 8 + x 4 + x 3 + x 2 + 1

Division polynomiale dans GF2

x15+x13+x11+x9x8+x4+x3+x2+1
x15+x11+x10+x9+x7x7+x5+x2+x+1
x13+x10+x7
x13+x9+x8+x7+x5
x10+x9+x8+x5
x10+x6+x5+x4+x2
x9+x8+x6+x4+x2
x9+x5+x4+x3+x
x8+x6+x5+x3+x2+x
x8+x4+x3+x2+1
x6+x5+x4+x+1

Division binaire dans GF2

1010101000000000100011101
100011101       010100111
001001001       
100100100     
100011101     
000111001     
111001000  
100011101  
011010101  
110101010 
100011101 
010110111 
101101110
100011101
001110011
1110011

Le reste est {0,1,1,1,0,0,1,1} qui correspond au polynôme :

x 6 + x 5 + x 4 + x + 1

Code C de la fonction de calcul


unsigned crc8(unsigned char *data,int dataSize,unsigned polynome) {
	unsigned crc,bit7crc;
	unsigned char bitoctet;
	crc=0;
	for(int i=0;i<dataSize;i+=1) {		
		for(int k=0;k<8;k+=1) {
			bitoctet = ((data[i] >> (7-k)) & 0x01);
			bit7crc = (crc >> (7)) & 0x01;
			crc <<= 1 ;
			if ((bit7crc ^ bitoctet)==1) {
				crc  ^= polynome;
			}
		}
	}
	return crc & 0xff;
}
				

Le code C est la traduction de cet algorithme de calcul du reste de la division binaire modulo 2 de la page CRC.

Le calcul se fait octet par octet. Pour chaque octet on calcul le CRC que l'on cumule avec le CRC de l'octet précédent. Pour le premier octet le CRC vaut 0.

La masque de fin permet de ne conserver que les 8 bits du CRC.

Exemple de transmission avec CRC
Voir le code des programmes de test clients et serveurs

Programme client


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"
#include "common.h"

int main(int nbargs,char *args[]) {
  struct sockaddr_in sock_adresse;
  int clientsocket;
  int port = PORT_DEFAUT ;
  char nom[TAILLE_NOM_IP] = NOM_DEFAUT;
  unsigned char trame[TAILLE_BUFFER];
  unsigned char message[TAILLE_BUFFER];
  int lg ;
  int taille;

  printf("connexion à %s port %d\n",nom,port);
  lg = createsock(nom,port,&sock_adresse);
  if (lg == 0) {
     perror("Erreur nom machine ou ip");
     return EXIT_FAILURE;	  
  }
  clientsocket=InitClientSocket(port,&sock_adresse);
  if (clientsocket == SOCK_ERR) {
     perror("Erreur connect");
     return EXIT_FAILURE;
  }
  printf("connecte\n");
  if (nbargs == 2) {
	  strcpy((char *)message,args[1]);
  }
  else {
	strcpy((char *)message,"bonjour");
  }
  taille = strlen((char *)message);
  uint8_t crc = crc8(message,++taille,0x1d);
  printf("crc = %02x\n",crc);
  bcopy(message,trame,taille);
  trame[taille++] = crc;
  printhex(trame,taille);
  send(clientsocket,trame,taille,0);
  close(clientsocket);   
  return EXIT_SUCCESS;
}
					

Ce programme transmet le message bonjour ou bien le message transmis en ligne de commande.

L'augmentation de la taille (++taille) permet d'intégrer le 0 de fin de chaîne dans la transmission.

trame[taille++]=crc ajoute le CC en fin de trame de données en mettant à jour la taille de la trame.

La trame qui inclut le crc est affichée en hexadécimal.

Affichage du résultat

connexion à localhost port 1234
connecte
crc = e0
62 6f 6e 6a 6f 75 72 00 e0 
					

Programme serveur


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"
#include "common.h"

int main(int nbargs,char *args[]) {
  int serveursocket, client;
  struct sockaddr_in socket_adresse;
  socklen_t long_adr;
  int port = PORT_DEFAUT;
  unsigned char trame[TAILLE_BUFFER];
  unsigned char message[TAILLE_BUFFER];
  int taille;
  
  long_adr = createsock("0.0.0.0",port,&socket_adresse);
  if (long_adr == 0) {
     perror("Erreur nom machine ou ip");
     return EXIT_FAILURE;	  
  }
  serveursocket = InitServerSocket(port,&socket_adresse);
  if (serveursocket == -1) {
    perror("Erreur socket ");
    return EXIT_FAILURE;
  }
  printf("Ecoute port %d\n",port);
  client = accept (serveursocket, (struct sockaddr *)&socket_adresse, &long_adr);
  printf("client connecte, ...\n");
  taille = recv(client,trame,TAILLE_BUFFER,0);
  printhex(trame,taille);
  bcopy(trame,message,--taille);
  uint8_t crcrecu = trame[taille];
  uint8_t crccalcule = crc8(message,taille,0x1d);
  if (crcrecu == crccalcule) {
	  printf("crc %02x OK\n",crcrecu);
  }
  else {
	  printf("recu %02x, calcule %02x\n",crcrecu,crccalcule);
  }
  close(client);
  close(serveursocket);
  return EXIT_SUCCESS;
}
					

Après réception bcopy(trame,message,--taille) copie la trame reçue dans message sans le crc en décrémentant la taille de 1 octet.

Le CRC reçu est stocké dans crcrecu, on calcule le CRC du message reçu : crccalcule. On vérifie que les 2 CRC sont égaux et on affiche le message correspondant.

Affichage du résultat

Ecoute port 1234
client connecte, ...
62 6f 6e 6a 6f 75 72 00 e0 
crc e0 OK
					

Cryptographie en C

Présentation

La cryptographie est une technique qui permet de protéger des données en les rendant incompréhensibles pour celui qui n'a pas la clé.

Cette technique a été utilisée à travers les siècles comme par exemple, le chiffrement par décalage ou chiffrement de césar, utilisé par Jules César, est un système où une lettre est remplacée par une autre, la clé étant le nombre de lettres de décalage dans l'alphabet.

On peut également citer la machine enigma utilisée par les allemands pendant la deuxième guerre mondiale. Ce système de codage, supposé parfaitement indéchiffrable, fût pourtant déchiffré au centre de décryptage de Bletchley Park en Angleterre. Parmi les personnes ayant participé à cette opération, on compte le mathématicien Alan Turing qui est le concepteur du projet ACE (Automatic Computing Engine) que l'on peut considérer comme le premier ordinateur.

Parmi les méthodes utilisées aujourd'hui, nous présenterons deux méthodes :

Utilisation du chiffrement RC4 en C

Cette méthode est composée de deux étapes

  1. Génération de la clé de codage à partir d'un mot de passe ou bien d'une passphrase.
  2. Utilisation de la clé pour encoder le message en utilisant des permutations et la fonction OU exclusif.

Avec cette méthode les fonctions de cryptage et décryptage sont identiques.

Les fonctions RC4 sont disponibles dans la librairie openssl (prototypes dans openssl/rc>4.h, édition de lien avec la librairie crypto) :

Exemple de chiffrement RC4

Voir le code des programmes clients et serveurs

Programme client


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"
#include "common.h"
#include <openssl/rc4.h>

int main(int nbargs,char *args[]) {
  struct sockaddr_in sock_adresse;
  int clientsocket;
  int port = PORT_DEFAUT ;
  char nom[TAILLE_NOM_IP] = NOM_DEFAUT;
  unsigned char trame[TAILLE_BUFFER];
  unsigned char message[TAILLE_BUFFER];
  char cle[TAILLE_BUFFER];
  int lg ;
  int taille;
  RC4_KEY key;

  printf("connexion à %s port %d\n",nom,port);
  lg = createsock(nom,port,&sock_adresse);
  if (lg == 0) {
     perror("Erreur nom machine ou ip");
     return EXIT_FAILURE;	  
  }
  clientsocket=InitClientSocket(port,&sock_adresse);
  if (clientsocket == SOCK_ERR) {
     perror("Erreur connect");
     return EXIT_FAILURE;
  }
  printf("connecte\n");
  if (nbargs == 2) {
	  strcpy((char *)message,args[1]);
	  strcpy(cle,"monsecret");
  }
  else {
	  if (nbargs == 3) { 
		strcpy((char *)message,args[1]);
		strcpy(cle,args[2]);
	  }
	  else {
		strcpy((char *)message,"bonjour");
		strcpy(cle,"monsecret");
	  }
  }
  RC4_set_key(&key,strlen(cle),(unsigned char *)cle);
  taille = strlen((char *)message);
  printf("Message        : ");
  printhex(message,++taille);
  RC4(&key,taille,(unsigned char*)message,(unsigned char *)trame);
  printf("Trame chiffrée : ");
  printhex(trame,taille);
  send(clientsocket,trame,taille,0);
  close(clientsocket);   
  return EXIT_SUCCESS;
}
					

Le programme accepte 1 argument qui est le message ou bien 2 arguments qui sont le message suivi du mot de passe secret.

Le zéro de fin de chaîne est ajouté au message avant le chiffrement. Le message et la trame chiffrée sont affichés en hexadécimal.

Affichage des résultats

connexion à localhost port 1234
connecte
Message        : 62 6f 6e 6a 6f 75 72 00 
Trame chiffrée : d6 9d 55 1e 59 5b 6b 5b 
					

Programme serveur


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"
#include "common.h"
#include <openssl/rc4.h>

int main(int nbargs,char *args[]) {
  int serveursocket, client;
  struct sockaddr_in socket_adresse;
  socklen_t long_adr;
  int port = PORT_DEFAUT;
  unsigned char trame[TAILLE_BUFFER];
  unsigned char message[TAILLE_BUFFER];
  char cle[TAILLE_BUFFER];
  int taille;
  RC4_KEY key;
  
  long_adr = createsock("0.0.0.0",port,&socket_adresse);
  if (long_adr == 0) {
     perror("Erreur nom machine ou ip");
     return EXIT_FAILURE;	  
  }
  serveursocket = InitServerSocket(port,&socket_adresse);
  if (serveursocket == -1) {
    perror("Erreur socket ");
    return EXIT_FAILURE;
  }
  if (nbargs == 2) {
	  strcpy(cle,args[1]);
  }
  else {
	  strcpy(cle,"monsecret");
  }
  RC4_set_key(&key,strlen(cle),(unsigned char *)cle);
  printf("Ecoute port %d\n",port);
  client = accept (serveursocket, (struct sockaddr *)&socket_adresse, &long_adr);
  printf("client connecte, ...\n");
  taille = recv(client,trame,TAILLE_BUFFER,0);
  printf("Trame chiffrée : ");
  printhex(trame,taille);
  RC4(&key,taille,trame,message);
  printf("Message        : ");
  printhex(message,taille);
  close(client);
  close(serveursocket);
  return EXIT_SUCCESS;
}
					

Le programme accepte 1 argument qui est le mot de passe secret. Après réception, la trame est déchiffrée. La trame chiffrée et le message en clair sont affichés en hexadécimal.

Affichage des résultats

Ecoute port 1234
client connecte, ...
Trame chiffrée : d6 9d 55 1e 59 5b 6b 5b 
Message        : 62 6f 6e 6a 6f 75 72 00 
					

Utilisation du chiffrement AES en C

AES est un chiffrement par blocs qui peuvent être par exemple 4x4 qui donnent des blocs de 16 octets.

Ce type de calcul nécessite de fonctions de cryptage et décryptage différentes.

Le chiffrement par blocs utilise plusieurs modes d'opérations, comme par exemple :

Les fonctions AES sont également disponibles dans la librairie openssl (prototype dans openssl/aes.h, édition de lien avec la librairie crypto) :

Exemple de chiffrement AES

Voir le code des fonctions de chiffrement et déchiffrement

Fonction de chiffrement


#include <stdlib.h>
#include <string.h>
#include <openssl/aes.h>
						
#define TAILLEBLOC	16

int AES_encode(unsigned char *message,int taille,unsigned char *trame,unsigned char *cle) {
	unsigned char buffer[16];
	AES_KEY key;
	int nbpaquets = taille / TAILLEBLOC ;
	int reste = taille % TAILLEBLOC ;
	int tailletrame = nbpaquets*TAILLEBLOC + (reste!=0?TAILLEBLOC:0) ;
	AES_set_encrypt_key(cle, 128, &key);
	trame[0] = taille ;
	int i=0;
	for(i=0;i<nbpaquets;i+=1) {
	  bcopy(&message[i*TAILLEBLOC],buffer,TAILLEBLOC);
	  AES_encrypt((unsigned char *)buffer, (unsigned char *)&trame[i*TAILLEBLOC+1], &key); 
	}
	bcopy(&message[i*TAILLEBLOC],buffer,reste);
	AES_encrypt((unsigned char *)buffer, (unsigned char *)&trame[i*TAILLEBLOC+1], &key); 
	return tailletrame;
}
					

La fonction de chiffrement découpe le message en blocs de 16 octets. La clé doit avoir une taille de 16 octets.

Les blocs sont chiffrés avec l'opération ECB.

La taille de la trame (valeur de retour) est calculée en nombre d'octets multiples de 16 octets.

La taille du message est enregistrée dans la trame avant le message chiffré. En effet le chiffrement envoie des paquets de 16 octets, ce qui signifie que la taille du message ne sera pas connue du récepteur (serveur). IL faut donc transmettre cette information.

Fonction de déchiffrement


#include <stdlib.h>
#include <string.h>
#include <openssl/aes.h>

#define TAILLEBLOC	16

int AES_decode(unsigned char *trame,int taille,unsigned char *message,unsigned char *cle) {
	unsigned char buffer[16];
	AES_KEY key;
	int taillecrypte = taille - 1 ;
	int nbpaquets = taillecrypte / TAILLEBLOC ;
	int taillemsg = trame[0] ;
	AES_set_decrypt_key(cle, 128, &key);
	int i=0;  
	for(i=0;i<nbpaquets;i+=1) {
	  bcopy(&trame[i*TAILLEBLOC+1],buffer,TAILLEBLOC);
	  AES_decrypt(buffer,&message[i*TAILLEBLOC], &key);
	}  
	return taillemsg;
}
					

La taille de la trame chiffrée est un multiple de 16 octets + 1 octet pour la taille du message qui se situe en début de trame (0).

La trame chiffrée, qui démarre à l'octet 1, est découpée en paquets des 16 octets, qui sont déchiffrés. Les paquets déchiffrés sont ensuite concaténés pour former le message complet.

Voir le code des programmes clients et serveurs

Programme client


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"
#include "common.h"

int main(int nbargs,char *args[]) {
  struct sockaddr_in sock_adresse;
  int clientsocket;
  int port = PORT_DEFAUT ;
  char nom[TAILLE_NOM_IP] = NOM_DEFAUT;
  unsigned char trame[TAILLE_BUFFER];
  unsigned char message[TAILLE_BUFFER];
  unsigned char cle[TAILLE_BUFFER];
  int lg ;
  int taille;
  int tailletrame;

  printf("connexion à %s port %d\n",nom,port);
  lg = createsock(nom,port,&sock_adresse);
  if (lg == 0) {
     perror("Erreur nom machine ou ip");
     return EXIT_FAILURE;	  
  }
  clientsocket=InitClientSocket(port,&sock_adresse);
  if (clientsocket == SOCK_ERR) {
     perror("Erreur connect");
     return EXIT_FAILURE;
  }
  printf("connecte\n");
  if (nbargs == 2) {
	  strcpy((char *)message,args[1]);
	  strcpy((char *)cle,"0123456789ABCDEF");
  }
  else {
	  if (nbargs == 3) { 
		strcpy((char *)message,args[1]);
		strcpy((char *)cle,args[2]);
	  }
	  else {
		strcpy((char *)message,"0123456789ABCDEF");
		strcpy((char *)cle,"0123456789ABCDEF");
	  }
  }
  printf("cle : ");
  printhex((unsigned char *)cle,strlen((char *)cle));
  printf("Message : %s\n",message);
  taille = strlen((char *)message);
  printf("Message en clair (%d octets) : ",taille);
  printhex(message,taille);
  tailletrame = AES_encode(message,taille,trame,cle);
  printf("Trame chiffrée (%d octets)   : ",tailletrame);
  printhex((unsigned char *)&trame[1],tailletrame++);
  send(clientsocket,trame,tailletrame,0);
  close(clientsocket);   
  return EXIT_SUCCESS;
}
					

La passphrase est de 16 octets. Elle peut être saisie en ligne de commande après le message.

Le zéro de fin de chaîne n'est pas transmis. La passphrase, le message en clair ainsi que le message chiffré sont affichés en hexadécimal avant d'être envoyés.

Affichage des résultats

connexion à localhost port 1234
connecte
cle : 30 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 
Message : 0123456789ABCDEF
Message en clair (16 octets) : 30 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 
Trame chiffrée (16 octets)   : 8d 83 5d 0c ff d8 ba d5 85 fe 8b 42 94 c4 21 88 
					

Programme serveur


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"
#include "common.h"

int main(int nbargs,char *args[]) {
  int serveursocket, client;
  struct sockaddr_in socket_adresse;
  socklen_t long_adr;
  int port = PORT_DEFAUT;
  unsigned char trame[TAILLE_BUFFER];
  unsigned char message[TAILLE_BUFFER];
  unsigned char cle[TAILLE_BUFFER];
  int taille;
  int taillemsg;
  
  long_adr = createsock("0.0.0.0",port,&socket_adresse);
  if (long_adr == 0) {
     perror("Erreur nom machine ou ip");
     return EXIT_FAILURE;	  
  }
  serveursocket = InitServerSocket(port,&socket_adresse);
  if (serveursocket == -1) {
    perror("Erreur socket ");
    return EXIT_FAILURE;
  }
  if (nbargs == 2) {
	 strcpy((char *)cle,args[1]);
  }
  else {
	 strcpy((char *)cle,"0123456789ABCDEF");
  }
  printf("cle : ");
  printhex((unsigned char *)cle,strlen((char *)cle));
  printf("Ecoute port %d\n",port);
  client = accept (serveursocket, (struct sockaddr *)&socket_adresse, &long_adr);
  printf("client connecte, ...\n");
  taille = recv(client,trame,TAILLE_BUFFER,0);
  int taillecrypte = taille - 1;
  printf("Trame chiffrée (%d octets)   : ",taillecrypte);
  printhex(&trame[1],taillecrypte);
  taillemsg = AES_decode(trame,taille,message,cle);
  printf("Message en clair (%d octets) : ",taillemsg);
  printhex(message,taillemsg);
  message[taillemsg] = 0;
  printf("Message : %s\n",message);
  close(client);
  close(serveursocket);
  return EXIT_SUCCESS;
}
					

La clé de 16 octets peut être saisie en ligne de commande.

Après réception, la trame chiffrée est affichée en hexadécimal.

Après décodage, le message en clair est affiché en hexadécimal, puis sous la forme texte, cela est rendu possible par l'ajout d'un zéro à la fin du message reçu.

Affichage des résultats

cle : 30 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 
Ecoute port 1234
client connecte, ...
Trame chiffrée (16 octets)   : 8d 83 5d 0c ff d8 ba d5 85 fe 8b 42 94 c4 21 88 
Message en clair (16 octets) : 30 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 
Message : 0123456789ABCDEF
					

Programmation en Python

Avertissement

Pour la conception de programmes en python, voir le chapitre sur le langage python.

L'accès au réseau en python utilise le module socket décrit dans cette documentation.

Accès aux informations réseaux

Cela se fait avec la fonction socket.getaddrinfos(nom,port,famille,type) avec nom qui peut être le nom DNS (FQDN (Fully qualified domain name))ou bien l'adresse IP, port qui est le numéro de port, famille qui est socket.AF_INET ou socket.AF_INET6 et type qui peut être socket.SOCK_STREAM ou socket.SOCK_DGRAM. Cette fonction retourne une liste de données où chaque élément contient l'ensemble des informations :

Exemple avec python en ligne de commande :

>>> import socket
>>> socket.getaddrinfo("localhost",80,socket.AF_INET,socket.SOCK_DGRAM)
[(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_DGRAM: 2>, 17, '', ('127.0.0.1', 80))]
		

Utilisation du protocole connecté TCP

Fonctions utilisées

On utilise les fonctions du module socket

  1. socket.socket(famille,type) avec famille qui vaut socket.AF_INET et type qui vaut socket.SOCK_STREAM. Cette fonction retourne un objet qui fournit les méthodes de connexion, d'envoi , de réception et de fermeture.
  2. Préparation de la connexion
    • bind(serveur) (côté serveur) assignation avec serveur qui est une liste qui contient le nom DNS (FQDN (Fully qualified domain name)) ou l'adresse IP ainsi que le numéro de port des clients autorisés à se connecter (nom est une chaîne vide pour autoriser tous les IP des clients).
    • listen(nombre) (côté serveur) définit la file d'attente de l'écoute.
    • accept() (côté serveur) attend la connexion et retourne un identifiant de connexion
    • connect(serveur) (côté client) établissement de la connexion avec serveur qui est une liste qui contient le nom DNS (FQDN (Fully qualified domain name)) ou l'adresse IP ainsi que le numéro de port.
  3. Envoi et réception de données pour le client et le serveur
    • send(donnees) : envoi des données avec donnees qui est de type bytes
    • recv(taille) : réception des données avec une taille maximale (taille). Cette fonction retourne les données dans un type bytes.
  4. close() (côté serveur) : fermeture de la connexion.
  5. close() (client et serveur) : fermeture du socket.

Exemple de programmes client et serveur en mode connecté avec ipv4

Voir le code des programmes de test clients et serveurs

Programme client


import socket

nom="localhost"
port=1234
serveur=(nom,port)

def main(args):
	if (len(args)==2):
		message = args[1]
	else:
		message = "bonjour"
	print(f"connexion à {nom} port {port}")
	parametres = socket.getaddrinfo(nom, port)
	ip=parametres[0][4][0] 
	numport=parametres[0][4][1]
	print(f"{ip} , {numport}")
	clientsocket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	clientsocket.connect(serveur)
	print("connecte")
	donnees = bytes(message, 'utf-8')
	print(f"envoi : {message}")
	clientsocket.send(donnees)
	donnees = clientsocket.recv(1024)
	message = donnees.decode('utf-8')
	print(f"reçu : {message}")
	message = "merci pour tout"
	donnees = bytes(message, 'utf-8')
	print(f"envoi : {message}")
	clientsocket.send(donnees)	
	clientsocket.close()
	return 0

if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))
					

La connexion se fait sur localhost avec le port 1234. La fonction connect utilise directement le nom DNS (FQDN (Fully qualified domain name)). Ce qui fait que l'utilisation de getaddrinfo n'est pas nécessaire. Elle est présente uniquement pour montrer un exemple d'utilisation.

La conversion chaîne vers tableau d'octets se fait avec le constructeur avec le message ainsi que le type d'encodage du message.

La conversion tableau d'octets vers chaîne de caractères se fait avec la méthode decode de la classe bytes en précisant le type d'encodage des caractères.

Affichage du résultat

connexion à localhost port 1234
127.0.0.1 , 1234
connecte
envoi : bonjour
reçu : bonjour OK
envoi : merci pour tout
					

Programme serveur


import socket

nom=""
port=1234
serveur=(nom,port)

def main(args):
	serveursocket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	serveursocket.bind(serveur)
	serveursocket.listen(1)
	print(f"écoute : {port}")
	client,addresse  = serveursocket.accept()  
	print("client connecté, ...")  
	donnees = client.recv(1024)
	message = donnees.decode('utf-8')
	print(f"reçu : {message}")
	reponse = message + " OK"
	donnees = bytes(reponse, 'utf-8')
	print(f"envoi : {message}")
	client.send(donnees)
	donnees = client.recv(1024)
	message = donnees.decode('utf-8')
	print(f"reçu : {message}")
	client.close()
	serveursocket.close()
	return 0

if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))
					

La méthode bind utilise une liste serveur dans laquelle l'adresse est laissée vide pour accepter un client quelque soit son adresse IP.

La méthode accept retourne une liste qui contient l'identifiant de connexion utilisé pour l'échange de données ainsi que l'adresse IP du client.

Affichage du résultat

écoute : 1234
client connecté, ...
reçu : bonjour
envoi : bonjour
reçu : merci pour tout
					

Exemple de programmes client et serveur en mode connecté avec ipv6

Voir le code des programmes de test clients et serveurs

Programme client


import socket

nom="localhost"
port=1234
serveur=(nom,port)

def main(args):
	if (len(args)==2):
		message = args[1]
	else:
		message = "bonjour"
	print(f"{nom} , {port}")
	parametres = socket.getaddrinfo(nom, port,socket.AF_INET6)
	ip=parametres[0][4][0] 
	numport=parametres[0][4][1]
	print(f"{ip} , {numport}")
	clientsocket=socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
	clientsocket.connect(serveur)
	print("connecte")
	donnees = bytes(message, 'utf-8')
	print(f"envoi : {message}")
	clientsocket.send(donnees)
	donnees = clientsocket.recv(1024)
	message = donnees.decode('utf-8')
	print(f"reçu : {message}")
	message = "merci pour tout"
	donnees = bytes(message, 'utf-8')
	print(f"envoi : {message}")
	clientsocket.send(donnees)	
	clientsocket.close()
	return 0

if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))
					

Il n'y a pas de différence avec la version ipv4 en dehors du paramètre socket.AF_INET6.

Affichage du résultat

localhost , 1234
::1 , 1234
connecte
envoi : bonjour
reçu : bonjour OK
envoi : merci pour tout
					

Il n'y a pas de différence avec la version ipv4 en dehors du paramètre socket.AF_INET6.

Programme serveur


import socket

nom=""
port=1234
serveur=(nom,port)

def main(args):
	serveursocket=socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
	serveursocket.bind(serveur)
	serveursocket.listen(1)
	print(f"écoute : {port}")
	client,addresse  = serveursocket.accept()  
	print("client connecté, ...")  
	donnees = client.recv(1024)
	message = donnees.decode('utf-8')
	print(f"reçu : {message}")
	reponse = message + " OK"
	donnees = bytes(reponse, 'utf-8')
	print(f"envoi : {reponse}")
	client.send(donnees)
	donnees = client.recv(1024)
	message = donnees.decode('utf-8')
	print(f"reçu : {message}")
	client.close()
	serveursocket.close()
	return 0

if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))
					

Affichage du résultat

écoute : 1234
client connecté, ...
reçu : bonjour
envoi : bonjour OK
reçu : merci pour tout
					

Utilisation du protocole non connecté UDP

Fonctions utilisées

On utilise les fonctions du module socket

  1. socket.socket(famille,type) avec famille qui vaut socket.AF_INET et type qui vaut socket.SOCK_STREAM. Cette fonction retourne un objet qui fournit les méthodes d'assignation, d'envoi, de réception et de fermeture.
  2. bind(serveur) assignation avec serveur qui est une liste qui contient le nom DNS (FQDN (Fully qualified domain name)) ou l'adresse IP ainsi que le numéro de port.
  3. Envoi et réception de données
    • sendto(donnees,(nom,port)) : envoi des données avec donnees qui est de type bytes
    • recvfrom(taille) : réception des données avec une taille maximale (taille). Cette fonction retourne une liste qui contient les données dans un type bytes d'une part et une liste qui contient l'adresse IP ainsi que le numéro de port de l'expéditeur d'autre part.
  4. close() : fermeture du lien.

Exemple de programmes client et serveur en mode non connecté avec ipv4

Voir le code des programmes de test clients et serveurs

Programme client


import socket

nom="localhost"
port=1234
serveur=(nom,port)

def main(args):
	if (len(args)==2):
		message = args[1]
	else:
		message = "bonjour"
	print(f"connexion à {nom} port {port}")
	parametres = socket.getaddrinfo(nom, port)
	ip=parametres[0][4][0] 
	numport=parametres[0][4][1]
	print(f"{ip} , {numport}")
	clientsocket=socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
	donnees = bytes(message, 'utf-8')
	print(f"envoi : {message}")
	clientsocket.sendto(donnees,(ip,port))
	donnees,adresse = clientsocket.recvfrom(1024)
	message = donnees.decode('utf-8')
	print(f"reçu de {adresse}")
	taille = len(donnees)
	print(f"reçu {taille}: {message}")
	clientsocket.close()
	return 0

if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))
					

Il n'y a plus de connexion, mais uniquement l'initialisation suivi des méthodes de lecture et d'écriture qui contiennent les paramètres IP et port du serveur.

La fonction de lecture renvoie les données ainsi que l'adresse IP et le port de l'expéditeur.

Affichage du résultat

connexion à localhost port 1234
127.0.0.1 , 1234
envoi : bonjour
reçu de ('127.0.0.1', 1234)
reçu 10: bonjour OK
					

Programme serveur


import socket

nom=""
port=1234
serveur=(nom,port)

def main(args):
	serveursocket=socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
	serveursocket.bind(serveur)
	print(f"écoute : {port}")
	donnees,adresse = serveursocket.recvfrom(1024)
	message = donnees.decode('utf-8')
	print(f"reçu de {adresse}")
	taille = len(donnees)
	print(f"reçu {taille}: {message}")
	reponse = message + " OK "
	donnees = bytes(reponse, 'utf-8')
	print(f"envoi : {reponse}")
	serveursocket.sendto(donnees,adresse)
	serveursocket.close()
	return 0

if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))
					

Après l'assignation, il n'y a plus les méthodes utilisées dans la connexion, mais seulement les méthodes de lecture et écriture.

L'adresse IP et le numéro de port du client (expéditeur) est utilisé par la fonction sendto pour la réponse.

Affichage du résultat

écoute : 1234
reçu de ('127.0.0.1', 50356)
reçu 7: bonjour
envoi : bonjour OK 
					

Contrôle de l'intégrité des données en Python

Somme de contrôle ou checksum

La somme est calculée avec la fonction sum de python.

Exemple de transmission avec checksum

Voir le code des programmes de test clients et serveurs

Programme client


import socket

nom="localhost"
port=1234
serveur=(nom,port)

def main(args):
	if (len(args)==2):
		message = args[1]
	else:
		message = "bonjour"

	print(f"{nom} , {port}")
	parametres = socket.getaddrinfo(nom, port)
	ip=parametres[0][4][0] 
	numport=parametres[0][4][1]
	print(f"{ip} , {numport}")
	clientsocket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	clientsocket.connect(serveur)
	print("connecte")
	donnees = bytearray(message, 'utf-8')
	somme = sum(donnees) % 256 
	print(f"somme={hex(somme)}")
	donnees.append(somme)
	print(donnees)
	clientsocket.send(donnees)
	clientsocket.close()
	return 0

if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))
					

La checksum est calculée sur les données puis ajoutée à la fin des données.

Affichage du résultat

localhost , 1234
127.0.0.1 , 1234
connecte
somme=0xff
bytearray(b'bonjour\xff')
					

Programme serveur


import socket

nom=""
port=1234
serveur=(nom,port)

def main(args):
	serveursocket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	serveursocket.bind(serveur)
	serveursocket.listen(1)
	print(f"écoute : {port}")
	client,addresse  = serveursocket.accept()   
	print("client connecte, ...") 
	trame = client.recv(1024)
	donnees = bytearray(trame)
	sommerecu = donnees.pop()
	sommecalcule = sum(donnees) % 256 
	if (sommecalcule == sommerecu):
		print(f"somme={hex(sommerecu)} OK")
	else:
		print(f"reçu  {hex(sommerecu)}, calcule {hex(sommecalcule)}")
	print(donnees)
	client.close()
	return 0

if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))
					

La checksum est extraite des données reçues. Un autre checksum est calculée à partir des données reçues. Ces deux valeurs sont comparées afin d'afficher un message d'intégrité des données reçues.

Affichage du résultat

écoute : 1234
client connecte, ...
somme=0xff OK
bytearray(b'bonjour')
					

Contrôle de redondance cyclique ou CRC

Fonctions utilisées

Le calcul du crc utilise le module crcmod décrit dans cette documentation

On définit la fonction de calcul de CRC avec la fonction :

crcmod.mkCrcFun(polynome, ...) qui retourne un lien vers la fonction de calcul de CRC, polynome est la valeur hexadécimale du CRC incluant tous les bits. Les autres paramètres définissent le type de polynôme :

Exemple de transmission avec CRC

Voir le code des programmes de test clients et serveurs

Programme client


import socket
import crcmod

nom="localhost"
port=1234
serveur=(nom,port)

def main(args):
	if (len(args)==2):
		message = args[1]
	else:
		message = "bonjour"
	fonction_crc = crcmod.mkCrcFun(0x11d, initCrc=0, rev=False)
	print(f"{nom} , {port}")
	parametres = socket.getaddrinfo(nom, port)
	ip=parametres[0][4][0] 
	numport=parametres[0][4][1]
	print(f"{ip} , {numport}")
	clientsocket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	clientsocket.connect(serveur)
	print("connecte")
	donnees = bytearray(message, 'utf-8')
	crc = fonction_crc(donnees)
	print(f"crc={hex(crc)}")
	donnees.append(crc)
	print(donnees)
	clientsocket.send(donnees)
	clientsocket.close()
	return 0

if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))
					

La fonction crcmod.mkCrcFun créé la fonction de calcul fonction_crc avec le polynôme :

x 8 + x 4 + x 3 + x 2 + 1
qui donne la valeur hexadécimale 0x11D, rev vaut false pour avoir un calcul normal.

Le crc calculé est ajouté à la fin du message.

Affichage du résultat

localhost , 1234
127.0.0.1 , 1234
connecte
crc=0x64
bytearray(b'bonjourd')
					

Programme serveur


import socket
import crcmod

nom=""
port=1234
serveur=(nom,port)

def main(args):
	fonction_crc = crcmod.mkCrcFun(0x11d, initCrc=0, rev=False)
	serveursocket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	serveursocket.bind(serveur)
	serveursocket.listen(1)
	print(f"écoute : {port}")
	client,addresse  = serveursocket.accept()   
	print("client connecte, ...") 
	trame = client.recv(1024)
	donnees = bytearray(trame)
	crcrecu = donnees.pop()
	crccalcule = fonction_crc(donnees)
	if (crccalcule == crcrecu):
		print(f"crc={hex(crcrecu)} OK")
	else:
		print(f"reçu  {hex(crcrecu)}, calcule {hex(crccalcule)}")
	print(donnees)
	client.close()
	return 0

if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))
					

La fonction de calcul du crc est identique à celle du client.

On extrait le CRC et le message des données reçues, puis on calcule le crc du message. On compare les deux CRC afin de vérifier l'intégrité des données.

Affichage du résultat

écoute : 1234
client connecte, ...
crc=0x64 OK
bytearray(b'bonjour')
					

Chiffrement en Python

Utilisation du chiffrement RC4

On utilise le module ARC4 de Crypto.Cipher décrit dans cette documentation.

Le codage se fait en deux étapes :

  1. ARC4.new(cle) avec cle qui un tableau d'octets, et qui retourne un objet qui fournit les fonctions de chiffrement et déchiffrement.
  2. Chiffrement et déchiffrement
    • encrypt(donnees) avec donnees qui est un tableau d'octets et qui retourne le tableau d'octets des données chiffrées.
    • decrypt(donnees) avec donnees qui est un tableau d'octets chiffrés et qui retourne le tableau d'octets des données déchiffrées.

Exemple de transmission avec RC4

Voir le code des programmes de test clients et serveurs

Programme client


import socket
from Crypto.Cipher import ARC4

nom="localhost"
port=1234
serveur=(nom,port)

def main(args):
	if (len(args)==2):
		message = args[1]
		cle = "monsecret"
	elif (len(args)==3):
		message = args[1]
		cle = args[2]
	else:
		message = "bonjour"
		cle = "monsecret"
	chiffrage = ARC4.new(bytes(cle,"utf-8"))
	print(f"{nom} , {port}")
	parametres = socket.getaddrinfo(nom, port)
	ip=parametres[0][4][0] 
	numport=parametres[0][4][1]
	print(f"{ip} , {numport}")
	clientsocket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	clientsocket.connect(serveur)
	print("connecte")
	donnees = bytes(message, 'utf-8')
	print(donnees)
	trame = chiffrage.encrypt(donnees)
	print(trame)
	clientsocket.send(trame)
	clientsocket.close()
	return 0

if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))
					

La clé fournie sous la forme d'une chaîne de caractères est transformée en tableau d'octets. L'objet chiffrage crée va permettre de chiffrer le message en clair en utilisant la méthode chifrage.encrypt(donnees).

Ensuite la trame est transmise.

Affichage du résultat

localhost , 1234
127.0.0.1 , 1234
connecte
b'bonjour'
b'\xd6\x9dU\x1eY[k'
					

Programme serveur


import socket
from Crypto.Cipher import ARC4

nom=""
port=1234
serveur=(nom,port)

def main(args):
	if (len(args)==2):
		cle = args[1]
	else:
		cle = "monsecret"
	chiffrage = ARC4.new(bytes(cle,"utf-8"))
	serveursocket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	serveursocket.bind(serveur)
	serveursocket.listen(1)
	print("écoute : " + str(port))
	client,addresse  = serveursocket.accept()    
	trame = client.recv(1024)
	print(f"trame chiffrée : {trame}")
	donnees = chiffrage.decrypt(trame)
	message = donnees.decode('utf-8')
	print(f"message        : {donnees}")
	client.close()
	return 0

if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))
					

La trame chiffrée reçue est déchiffrée avec la méthode chiffrage.decrypt(trame), elle retourne le message en clair.

Affichage du résultat

écoute : 1234
trame chiffrée : b'\xd6\x9dU\x1eY[k'
message        : b'bonjour'
					

Utilisation du chiffrement AES

On utilise le module AES de Crypto.Cipher décrit dans cette documentation.

Le codage se fait en deux étapes :

  1. AES.new(cle) avec cle qui un tableau de 16,24 ou 32 octets, et qui retourne un objet qui fournit les fonctions de chiffrement et déchiffrement.
  2. Chiffrement et déchiffrement par blocs de 16,24 ou 32 octets.
    • encrypt(donnees) avec donnees qui est un tableau d'octets et qui retourne le tableau d'octets des données chiffrées.
    • decrypt(donnees) avec donnees qui est un tableau d'octets chiffrés et qui retourne le tableau d'octets des données déchiffrées.

Exemple de transmission avec AES

Voir le code des fonctions de chiffrement et déchiffrement

Fonction chiffrement


from Crypto.Cipher import AES

TAILLEBLOC=16

def AESencode(message,cle):
	donnees = bytes(message, 'utf-8')
	taille = len(donnees)
	nbpaquets = taille // TAILLEBLOC
	reste = taille % TAILLEBLOC
	trame = bytearray([taille])
	chiffrage = AES.new(bytes(cle,"utf-8"))
	for i in range(nbpaquets):
		blocdonnees = donnees[i*TAILLEBLOC:(i+1)*TAILLEBLOC]
		blocchiffre = chiffrage.encrypt(blocdonnees)
		trame = trame + blocchiffre
	if (reste != 0):
		blocdonnees = donnees[nbpaquets*TAILLEBLOC:nbpaquets*TAILLEBLOC+reste] + bytes(TAILLEBLOC-reste)
		blocchiffre = chiffrage.encrypt(blocdonnees)
		trame = trame + blocchiffre	
	return trame
					

Le message est découpé en blocs de 16 octets. Le dernier bloc est complété par des 0 afin d'obtenir un bloc de 16 octets.

La trame créée commence par la valeur de la taille du message en clair qui est suivie du message crypté.

Fonction déchiffrement


from Crypto.Cipher import AES

TAILLEBLOC=16

def AESdecode(trame,cle):
	tramemodifiee = bytes(trame)
	taillemsg = tramemodifiee[0]
	trame = bytes(tramemodifiee[1:])
	taille = len(trame)
	nbpaquets = taille // TAILLEBLOC
	chiffrage = AES.new(bytes(cle,"utf-8"))
	donnees = bytearray()
	for i in range(nbpaquets):
		blocdonnees = trame[i*TAILLEBLOC:(i+1)*TAILLEBLOC]
		blocdechiffre = chiffrage.decrypt(blocdonnees)
		donnees = donnees + blocdechiffre
	donneesmessage = donnees[0:taillemsg]
	return donneesmessage
					

La taille de la trame reçue est un multiple de 16 octets + 1 octet. Après avoir sauvé la taille du message, Cette trame est découpée en blocs de 16 octets. Chaque bloc est déchiffré, puis concaténé avec le précédent afin de former la trame en clair.

Le message en clair est extrait des données déchiffrées en utilisant la taille du message reçue.

Voir le code des programmes de test clients et serveurs

Programme client


import socket

nom="localhost"
port=1234
serveur=(nom,port)

def main(args):
	if (len(args)==2):
		message = args[1]
		cle = "0123456789ABCDEF"
	elif (len(args)==3):
		message = args[1]
		cle = args[2]
	else:
		message = "0123456789ABCDEF"
		cle = "0123456789ABCDEF"
	chiffrage = AES.new(bytes(cle,"utf-8"))
	print(f"{nom} , {port}")
	parametres = socket.getaddrinfo(nom, port)
	ip=parametres[0][4][0] 
	numport=parametres[0][4][1]
	print(f"{ip} , {numport}")
	clientsocket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	clientsocket.connect(serveur)
	taille = len(message)
	print("connecte")
	print(f"Message ({taille} octets ) : {message}")
	trame = AESencode(message,cle)
	taille = len(trame)
	print(f"Trame ({taille} octets )   : {trame}")
	clientsocket.send(trame)
	clientsocket.close()
	return 0

if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))
					

Le message est chiffré puis transmis

Affichage du résultat

localhost , 1234
127.0.0.1 , 1234
connecte
Message (16 octets ) : 0123456789ABCDEF
Trame (17 octets )   : bytearray(b'\x10\x8d\x83]\x0c\xff\xd8\xba\xd5\x85\xfe\x8bB\x94\xc4!\x88')
					

Programme serveur


import socket

nom=""
port=1234
serveur=(nom,port)
def main(args):
	if (len(args)==2):
		cle = args[1]
	else:
		cle = "0123456789ABCDEF"
	chiffrage = AES.new(bytes(cle,"utf-8"))
	serveursocket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	serveursocket.bind(serveur)
	serveursocket.listen(1)
	print(f"écoute : {port}")
	client,addresse  = serveursocket.accept()    
	trame = client.recv(1024)
	taille = len(trame[1:])
	print(f"Trame chiffrée ({taille} octets ): {trame}")
	donnees = AESdecode(trame,cle)
	taillemsg = trame[0]
	print(f"donnees  ({taillemsg} octets)    : {donnees}")
	print(donnees.decode('utf-8'))
	client.close()
	return 0

if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))
					

La trame chiffrée reçue est déchiffrée, puis le message en clair est affiché.

Affichage du résultat

écoute : 1234
Trame chiffrée (16 octets ): b'\x10\x8d\x83]\x0c\xff\xd8\xba\xd5\x85\xfe\x8bB\x94\xc4!\x88'
donnees  (16 octets)    : bytearray(b'0123456789ABCDEF')
0123456789ABCDEF
					

Utilisation des Codes C et python

On a écrit des codes en C et en python. Les programmes clients et serveurs ne sont pas sur le même système, ce qui fait qu'il peut y avoir un programme client écrit en C et un programme serveur écrit en python ou vice-versa.

Afin de tester la robustesse des programmes, on teste chaque client en C avec le serveur correspondant en python, ensuite on inverse le procédé avec un client en python et un serveur en C. Tous ces programmes doivent fonctionner correctement, ce qui est le cas des programmes donnés ici.

Ce test n'est pas suffisant, mais il permet de vérifier la rigueur des formats de transmission.