• Non ci sono risultati.

UNIVERSITÀ DEGLI STUDI DI UDINE

N/A
N/A
Protected

Academic year: 2021

Condividi "UNIVERSITÀ DEGLI STUDI DI UDINE"

Copied!
98
0
0

Testo completo

(1)

UNIVERSITÀ DEGLI STUDI DI UDINE

Dipartimento di Ingegneria Elettrica, Gestionale e Meccanica Corso di Laurea in Ingegneria Gestionale

Tesi di Laurea

IMPLEMENTAZIONI DIDATTICHE DI ALGORITMI DI CIFRATURA PER SICUREZZA INFORMATICA

Relatore: Laureando:

Prof. Pier Luca Montessoro Pierpaolo Basso

ANNO ACCADEMICO 2013/2014

(2)
(3)

Alla mia famiglia e a Veronica

(4)
(5)

Sommario

La confidenzialità dei dati è uno dei requisiti essenziali nell'ambito della sicurezza informatica. Oggi, infatti, milioni di persone usano la rete per fare acquisti, per l’home banking e per molte altre applicazioni che richiedono la condivisione di informazioni personali. In tutti questi casi le comunicazioni avvengono attraverso canali pubblici, cioè non sicuri. La crittografia svolge, quindi, un ruolo fondamentale nel prevenire l’estrazione non autorizzata di informazioni da tali comunicazioni.

In questa tesi viene realizzata l’implementazione di due algoritmi di cifratura che consentono una comunicazione sicura (cioè criptata) attraverso un canale pubblico:

l’algoritmo Diffie-Hellman e l’algoritmo RSA.

Innanzitutto, entrambi gli algoritmi saranno analizzati dal punto di vista teorico allo scopo di comprenderne le finalità e i contenuti. Dopodichè verrà presentata la loro implementazione in linguaggio C e C++. Per ciascuno di essi sarà, inoltre, proposto un esempio di utilizzo nella cifratura delle comunicazioni che avvengono attraverso la rete. L’attenzione sarà focalizzata principalmente sulla versione in C++ che verrà puntualmente confrontata con quella in linguaggio C.

Il prodotto finale della tesi è costituito dall’insieme di classi e di librerire realizzate per implementare i due algoritmi e dai programmi per utilizzarle.

(6)
(7)

Indice

Sommario I

Indice III

Indice delle figure V

Introduzione VII

1 Linguaggio e librerie utilizzate 1

1.1 Scelte di progetto 1

1.2 Installazione del compilatore e delle librerie in Windows 3 1.2.1 Installazione del compilatore C/C++ in ambiente Cygwin 3

1.2.2 Installazione libreria GMP 4

1.3 Libreria udpsocketlib 6

2 Algoritmo Diffie-Hellman 11

2.1 Descrizione 11

2.1.1 Sicurezza 12

2.2 Scambio della chiave di cifratura: classe Dh 14

2.2.1 Funzioni crittografiche della classe Dh 17

2.3 Comunicazione criptata: libreria DhDataLib 19

2.4 Esempio di utilizzo 21

2.5 Confronto con la versione in C 24

3 Algoritmo RSA 27

3.1 Descrizione 27

3.1.1 Correttezza dell’algoritmo 28

3.2 Generazione delle chiavi di cifratura: classe Rsa 30

3.3 Comunicazione criptata: libreria RsaDataLib 34

3.4 Esempio di utilizzo 36

3.5 Confronto con la versione in C 39

Conclusioni 43

Bibliografia 45

Appendici 47

File in C++ 47

ClassDh.hpp 47

ClassDh.cpp 48

DhDataLib.hpp 53

DhDataLib.cpp 53

ClassRsa.hpp 56

ClassRsa.cpp 57

RsaDataLib.hpp 61

(8)

RsaDataLib.cpp 61

File in C 64

DhLib.h 64

DhLib.c 65

DhDataLib.h 69

DhDataLib.c 70

Dh_c.c 73

Dh_s.c 74

RsaLib.h 75

RsaLib.c 76

RsaDataLib.h 81

RsaDataLib.c 81

Rsa_s.c 85

Rsa_c.c 86

(9)

Indice delle figure

Figura 1.1 – Cygwin Setup 4

Figura 1.2 – Schema comunicazione via UDP 6

Figura 2.1 – Scambio di chiavi Diffie-Hellman 11

Figura 2.2 – Funzioni per lo scambio della chiave 15

Figura 3.1 – Funzioni per la creazione delle chiavi 31

(10)
(11)

Introduzione

Lo scopo di questa tesi è di implementare in linguaggio C e C++ gli algoritmi crittografici Diffie-Hellman e RSA e di realizzare le librerie e i programmi necessari al loro utilizzo per cifrare le comunicazioni che avvengono attraverso la rete. È importante sottolineare la finalità didattica del lavoro svolto: per permettere la comprensione degli algoritmi sopracitati, tutto il codice è stato scritto in modo tale da presentare in dettaglio i passaggi da essi previsti.

Nel primo capitolo vengono descritte le motivazioni che hanno portato all’utilizzo dei linguaggi di programmazione appena citati e gli strumenti necessari per la realizzazione della tesi.

Il secondo capitolo è interamente dedicato all’algoritmo Diffie-Hellman. Innanzitutto l’algoritmo viene descritto in dettaglio e vengono analizzati i requisiti fondamentali per la sua sicurezza. Dopodichè viene illustrata la classe Dh che implementa in C++

sia l’algoritmo che le funzioni di cifratura e decifratura dei dati. Infine, viene descritta la libreria DhDataLib che consente di realizzare comunicazioni criptate grazie alla classe Dh. L’ultima parte del capitolo comprende i programmi (scritti in C++) che utilizzano questi strumenti e il confronto con la versione realizzata in linguaggio C.

Il terzo capitolo, infine, è dedicato all’algortimo RSA. Anche in questo caso, come per il Diffie-Hellman, nella prima parte del capitolo si ha la descrizione dell’algortimo RSA e l’analisi della sua sicurezza. Dopodichè vengono illustrate la classe Rsa (scritta in linguaggio C++) che implementa l’algoritmo e la libreria RsaDataLib che la utilizza per cifrare le comunizioni in rete. Nell’ultima parte del capitolo vengono descritti i programmi (in C++) che utilizzano questi strumenti e viene analizzata la versione realizzata in linguaggio C.

(12)
(13)

1 Linguaggio e librerie utilizzate

1.1 Scelte di progetto

Il linguaggio di programmazione utilizzato per l’implementazione degli algoritmi di crittografia trattati in questa tesi è il C++.

Questa scelta è dettata da alcune caratteristiche offerte dal tale linguaggio che saranno illustrate brevemente in questo capitolo. Tuttavia, poiché lo scopo della tesi è un utilizzo didattico degli algoritmi implementati, è stata realizzata una versione parallela in C rivolta a chi non avesse studiato il C++. Data la natura didattica dell’implementazione, inoltre, i programmi realizzati (sia in C che in C++) sono stati sviluppati in modo tale da presentare esplicitamente tutti i passaggi degli algoritmi di cifratura: le operazioni svolte vengono, quindi, opportunamente evidenziate per permettere di comprendere il funzionamento degli algoritmi stessi.

Gli algoritmi di crittografia richiedono la gestione di numeri interi molto grandi, non rappresentabili con i tipi di dato standard messi a disposizione da linguaggi di programmazione come il C e il C++. È stato, quindi, necessario l’utilizzo della libreria GMP (GNU Multiple Precision Arithmetic Library). Essa permette di lavorare agevolmente con numeri interi, razionali e in virgola mobile ponendo come unico limite la memoria disponibile del calcolatore.

L’overloading degli operatori del C++ permette una naturale integrazione della libreria GMP. I tipi di dato introdotti da tale libreria possono, quindi, essere trattati allo stesso modo di quelli standard senza la necessità di utilizzare funzioni specifiche per le operazioni aritmetiche, logiche e di input/output, contrariamente a quanto accade in C. Tutto ciò si traduce in una maggiore semplicità e leggibilità del codice scritto in C++. Questo può essere considerato uno dei motivi per cui si è deciso di utilizzare tale linguaggio.

Un’ulteriore motivazione alla scelta del C++ è rappresentata dalla gestione ottimale delle stringhe di caratteri offerta da questo linguaggio, aspetto rilevante nella programmazione connessa alla crittografia. Questa caratteristica è ancora più evidente nel confronto col C, linguaggio in cui la gestione delle stringhe è limitata ai soli vettori di caratteri.

La scelta del C++ è stata dettata, inoltre, da motivazioni riguardanti la sicurezza. Le classi introdotte dal C++ supportano il principio dell'Information Hiding (nascondere le informazioni), comune ai linguaggi di programmazione Object-Oriented.

All’interno di una classe, infatti, possono essere presenti sia sezioni pubbliche sia private. I dati e i metodi contenuti nella sezione pubblica di una classe sono gli unici accessibili dall’esterno. Le funzioni e i dati presenti nella sezione privata, invece, sono accessibili solamente ai metodi di classe, mentre risultano invisibili all’esterno.

È quindi possibile proteggere le chiavi di cifratura e le altre variabili inserendole nella sezione privata della classe. Così facendo le chiavi di cifratura non sono direttamente accessibili dall’esterno ma possono essere generate, modificate e utilizzate solamente attraverso i metodi pubblici della classe. Questo determina un livello di sicurezza più elevato e una maggiore robustezza dal punto di vista dell’ingegneria del software.

(14)

Nella realizzazione della tesi è stato necessario utilizzare altre due librerie, oltre alla già citata GMP:

• La libreria udpsocketlib, appartenente alla libreria socketlib sviluppata dal prof. Montessoro. Quest’ultima è una libreria di funzioni per la scrittura semplificata di applicazioni client/server con i protocolli TCP e UDP. Gli algoritmi implementati prevedono, infatti, la presenza di due parti che devono generare e scambiare una chiave di crittografia attraverso la rete (quindi un canale non sicuro) per poi condividere dati cifrati. È stata quindi utilizzata la libreria udpsocketlib per permettere alle due parti di dialogare attraverso la rete tramite il protocollo UDP. In conformità con tale libreria, possiamo pensare a esse come a un client e a un server che comunicano.

• La libreria OpenSSL, utilizzata per generare numeri primi pseudo-casuali da 512 e da 1024 bit.

Le principali funzioni utilizzate delle librerie GMP e OpenSSL saranno analizzate, se necessario, nei capitoli successivi, non appena le si incontrerà nel codice scritto.

Viceversa, si ritiene opportuno descrivere più in dettaglio le funzioni della libreria udpsocketlib utilizzate per lo scambio di dati in rete attraverso il protocollo UDP.

Questo argomento verrà trattato nel punto 1.3.

(15)

1.2 Installazione del compilatore e delle librerie in Windows

Per l’implementazione degli algoritmi considerati in C/C++, ma più in generale per la realizzazione di programmi scritti in tali linguaggi, un primo passo fondamentale è l’installazione del compilatore e dei pacchetti necessari al suo funzionamento. Il compilatore, infatti, è fondamentale per tradurre il codice scritto in un programma eseguibile.

In ambiente Windows la scelta è stata di installare il compilatore Cygwin GNU C/C++. Nel processo d’installazione, come vedremo tra breve, è possibile selezionare tra i diversi pacchetti disponibili anche la libreria OpenSSL.

La presenza del compilatore, infine, è fondamentale per poter installare la libreria GMP che rappresenta una componente di grande rilevanza per la realizzazione della tesi.

Le operazioni fondamentali da eseguire sono, nell’ordine:

• Installazione del compilatore Cygwin GNU C/C++, compresa la libreria OpenSSL.

• Installazione della libreria GMP.

Per quanto riguarda la libreria “socketlib”, è sufficiente scaricarla dal sito del docente [1] e includere i file sorgenti necessari, ovvero “udpsocketlib.hpp” e

“udpsocketlib.cpp” per il C++ e “udpsocketlib.h” e “udpsocketlib.c” per il C.

1.2.1 Installazione del compilatore C/C++ in ambiente Cygwin

Innanzitutto è necessario collegarsi al sito di Cygwin [2] per ottenere i file di installazione. È necessario, quindi, scaricare il programma d’installazione

“setup.exe” nella versione corretta per la propria architettura, 32 o 64 bit.

Fatto ciò, si deve avviare il setup e selezionare la voce “Install from Internet”

(ovviamente è richiesta una connessione a internet per scaricare i pacchetti necessari). Dopodiché è sufficiente avanzare senza modificare nulla fino alla schermata “Select packages”. In questa pagina è necessario selezionare alcuni pacchetti aggiuntivi rispetto alla configurazione di default prevista dal programma d’installazione.

In particolare, devono essere selezionati i seguenti pacchetti all’interno delle corrispondenti categorie:

• nella categoria “Devel”: cmake, gcc-core, gcc-g++, gdb, git, make

• nella categoria “Interpreters”: m4

• nella categoria “Net”: openssl-devel

Per selezionare questi pacchetti aggiuntivi basta espandere la relativa categoria, individuare il pacchetto desiderato e cliccare una sola volta su “Skip”, così da far apparire il numero indicante l’ultima versione disponibile.

(16)

Vediamo un esempio in figura 1.1:

Questa configurazione permette, quindi, di installare il compilatore GNU C/C++

(gcc-core, gcc-g++), la libreria OpenSSL (openssl-devel) e alcuni componenti aggiuntivi necessari per un corretto funzionamento.

Una volta aggiunti i pacchetti indicati è sufficiente avanzare senza modificare nulla fino a quando il download e l’installazione saranno terminati.

Nella pagina immediatamente successiva a quella di selezione dei pacchetti, ovvero la pagina con titolo “Resolving dependancies”, è fondamentale che sia selezionata l’opzione “Select required packages”.

L'installazione crea così un'icona, un collegamento indicato con "Cygwin Terminal", che consente di eseguire una shell (finestra di comandi) in cui è possibile utilizzare alcuni comandi Unix e il compilatore C/C++.

1.2.2 Installazione libreria GMP

La libreria GMP può essere reperita dal sito di GMP[3]. Dopo aver scaricato ed estratto gli archivi, si deve aprire la shell “Cygwin Terminal”, installata come visto in precedenza.

Si deve ora cambiare il direttorio corrente (tramite il comando “cd”) spostandosi nella cartella contenente i file della libreria GMP appena estratti. Per maggior chiarezza, si deve spostare il direttorio corrente nella cartella della libreria GMP

Figura 1.1 - Cygwin Setup

(17)

contenente il file “configure”. A questo punto si deve digitare nel terminale il comando “./configure --enable-cxx ”, premere invio e attendere il completamento dell’operazione. Dopodiché si deve fare lo stesso col comando “make”.

Per verificare la corretta esecuzione delle operazioni precedenti si deve digitare

“make check” nel terminale e premere invio. Terminati i diversi test avviati dal comando precedente, se non sono stati riscontrati errori, si deve digitare “make install” nel terminale e attendere il completamento di quest’ultima operazione.

La libreria GMP dovrebbe, così, essere correttamente installata per entrambi i linguaggi C e C++.

(18)

1.3 Libreria udpsocketlib

Il protocollo UDP (User Datagram Protocol) è un protocollo di livello di trasporto non orientato alla connessione. Il protocollo UDP si appoggia direttamente sopra IP, i dati vengono inviati in forma di datagram e non ne è assicurata né l’effettiva ricezione né l'arrivo nell'ordine in cui vengono trasmessi. Non viene gestito, inoltre, né il riordinamento dei pacchetti né la ritrasmissione di quelli non ricevuti. Di conseguenza il protocollo UDP è considerato non affidabile.

In compenso è molto veloce ed efficiente: minimizza i ritardi poiché non si ha ritrasmissione e riordinamento dei dati ed essendo non connesso non presenta overhead dovuto ai pacchetti di servizio necessari per stabilire, mantenere e chiudere una connessione.

Queste caratteristiche fanno sì che l’UDP sia particolarmente conveniente quando si opera su rete locale. La probabilità estremamente bassa di perdita di dati che contraddistingue le reti locali permette, infatti, di sfruttare appieno la rapidità e l’efficienza del protocollo.

Le reti locali rappresentano esattamente l’ambito applicativo per cui sono stati implementati gli algoritmi trattati in questa tesi. Di conseguenza, nei programmi realizzati si è deciso di utilizzare il protocollo UDP per lo scambio di dati in rete.

Come detto, i socket UDP supportano una comunicazione di tipo datagram. Tutto ciò che avviene, quindi, in una comunicazione attraverso socket UDP è la trasmissione di un pacchetto da un client a un server o viceversa, secondo lo schema in figura 1.2.

Come illustrato in figura 1.2, un server UDP, dopo aver creato il socket tramite la funzione socket, esegue la primitiva bind per associare un indirizzo di rete al socket stesso. Un client UDP, invece, deve creare solamente il socket. Dopodiché entrambe le parti utilizzano direttamente le funzioni sendto e recvfrom per inviare e ricevere dati.

Figura 1.2 - Schema comunicazione via UDP

client

socket()

recvfrom() sendto()

server

socket()

bind()

sendto() recvfrom()

(19)

Nelle applicazioni realizzate in questa tesi, come detto in precedenza, ci si è serviti della libreria udpsocketlib per utilizzare i socket UDP. È opportuno, quindi, analizzare le funzioni messe a disposizione da tale libreria per permettere a un client e a un server di comunicare in rete attraverso il protocollo UDP secondo lo schema visto.

Innanzitutto il server esegue la funzione create_udp_server di cui riportiamo di seguito il codice.

1      int  create_udp_server(char  *ip_address,  int  port)   2      {  

3              int  sk;  

4              struct  sockaddr_in  srv;  

5              /*  create  a  socket  descriptor  */  

6              if  ((sk  =  socket(AF_INET,  SOCK_DGRAM,  0))  <  0)   7              {  

8                      error_handler("socket()  [create_udp_server()]");  

9                      return  -­‐1;  

10            }  

11            /*  fill  the  (used)  fields  of  the  socket  address  with   12                  the  server  information  (local  socket  address)  */  

13            bzero((char  *)  &srv,  sizeof(srv));  

14            srv.sin_family  =  AF_INET;  

15            srv.sin_addr.s_addr  =  inet_addr(ip_address);  

16            srv.sin_port  =  htons(port);  

17            /*  add  the  local  socket  address  to  the  socket  descriptor  */  

18            if  (bind(sk,  (struct  sockaddr  *)  &srv,  sizeof(srv))  <  0)   19            {  

20                    error_handler("bind()  [create_udp_server()]");  

21                    return  -­‐1;  

22            }  

23            return  sk;  

24    }  

Da notare la chiamata alla primitiva socket (linea 6), la quale crea il socket e i cui parametri specificano:

• La famiglia di protocolli da usare. “AF_INET” indica il protocollo IPv4.

• Il tipo di servizio. “SOCK_DGRAM” indica l’utilizzo dei datagram.

• Il protocollo da usare con il socket. “0” indica che il protocollo usato è quello di default per la combinazione di dominio e tipo specificata dagli altri due parametri.

La funzione socket, restituisce un intero (assegnato a sk) che sarà usato per i riferimenti al socket stesso. Dopodiché viene eseguita la funzione bind (linea 21) che associa al socket un indirizzo di rete costituito dall’indirizzo IPv4 del server e dalla porta locale su cui attende i dati. Questi dati sono inseriti all’interno di una struct sockaddr (linee 14÷16) passata come parametro alla funzione bind insieme al riferimento sk al socket.

A questo punto, il client esegue la funzione create_udp_client con cui viene creato il socket (linea 5) attraverso la primitiva socket nello stesso modo visto per il server.

(20)

Il codice della funzione è riportato di seguito.

1 int  create_udp_client(void)   2      {  

3              int  sk;  

4              /*  create  a  socket  descriptor  */  

5              if  ((sk  =  socket(AF_INET,  SOCK_DGRAM,  0))  <  0)   6              {  

7                      error_handler("socket()  [create_udp_client()]");  

8                      return  -­‐1;  

9              }  

10            return  sk;  

11    }    

Client e server possono, quindi, utilizzare le funzioni di libreria udp_send e udp_receive per scambiare dati tra loro. Riportiamo di seguito il codice di entrambe.

Da notare in udp_send l’utilizzo della funzione sendto (linea 12) i cui parametri principali sono: una struct sockaddr_in ska in cui sono stati inseriti l’indirizzo IPv4 e la porta del destinatario (linee 8÷10), il riferimento al socket sk e la stringa buffer da inviare.

1      int  udp_send(int  sk,  char  *buffer,  char  *ip_address,  int  port)   2      {  

3              struct  sockaddr_in  ska;  

4              int  msg_len  =  strlen(buffer);  

5              /*  fill  the  (used)  fields  of  the  socket  address  with   6                  the  server  information  (remote  socket  address)  */  

7              bzero((char  *)  &ska,  sizeof(ska));  

8              ska.sin_family  =  AF_INET;  

9              ska.sin_addr.s_addr  =  inet_addr(ip_address);  

10            ska.sin_port  =  htons((unsigned  short)  port);  

11  

12            if  (sendto(sk,  buffer,  msg_len,  0,  (struct  sockaddr  *)  &ska,   sizeof(ska))  !=  msg_len)  

13            {  

14                    error_handler("sendto()  [udp_send()]");  

15                    return  0;  

16            }  

17            return  1;  

18    }  

In udp_receive si osserva la chiamata della funzione recvfrom (linea 24). I parametri più rilevanti sono: il numero intero sk (riferimento al socket), la stringa di caratteri buffer in cui saranno inseriti i dati ricevuti e la struct reply_to_socket_address in cui è memorizzato l’indirizzo del mittente.

1      int  udp_receive(int  sk,  char  *buffer)   2      {  

3              int  dim;  

4              len_of_reply_to_socket_address  =  sizeof(reply_to_socket_address);  

5              if  ((dim  =  recvfrom(sk,  buffer,  BUFSIZ,  0,  (struct  sockaddr  *)  

&reply_to_socket_address,  (int  *)  

&len_of_reply_to_socket_address))  <  0)   6              {  

(21)

7                      error_handler("recvfrom()  [udp_receive()]");  

8                      return  -­‐1;  

9              }  

10            buffer[dim]  =  '\0';  

11            return  dim;  

12    }    

Quest’ultimo dato, inoltre, è usato della funzione udp_reply per rispondere al mittente dell’ultimo pacchetto ricevuto. Nel codice si nota, infatti, come alla funzione sendto è passata la struct reply_to_socket_address (linea 5).

1      int  udp_reply(int  sk,  char  *buffer)   2      {  

3              int  msg_len  =  strlen(buffer);  

4              if  (sendto(sk,  buffer,  msg_len,  0,  (struct  sockaddr  *)  

&reply_to_socket_address,  len_of_reply_to_socket_address)  <  0)   5              {  

6                      error_handler("sendto()  [udp_reply()]");  

7                      return  0;  

8              }  

9              return  1;  

10    }  

È importante, quindi, notare come, a differenza della funzione udp_send, a udp_reply non è passato alcun indirizzo di rete come parametro. Questa può, quindi, essere usata solamente a seguito della ricezione di un pacchetto per rispondere al mittente.

Terminato lo scambio di dati tra client e server, entrambe chiamano la funzione close_udp_socket. La funzione close chiude il socket creato e rilascia tutte le risorse ad esso allocate (linea 3).

1      int  close_udp_socket(int  sk)   2      {  

3              if  (close(sk)  !=  0)   4              {  

5                      error_handler("close()  [close_udp_socket()]");  

6                      return  0;  

7              }  

8              return  1;  

9      }  

(22)
(23)

2 Algoritmo Diffie-Hellman

2.1 Descrizione

L’algoritmo Diffie-Hellman è stato proposto nel 1976 da Whitfield Diffie e Martin E.

Hellman, due crittografi statunitensi, nell’articolo “New Directions in Cryptography”

[4]. Questo algoritmo ha rappresentato una soluzione innovativa ad uno dei problemi classici della crittografia, ovvero la distribuzione delle chiavi di cifratura.

Uno dei principali ambiti applicativi della crittografia è la garanzia della privacy. La criptazione delle comunicazioni che avvengono attraverso un canale non sicuro (pubblico) diventa, quindi, fondamentale per prevenire l’estrazione non autorizzata di informazioni da esse. Di conseguenza, è necessario che le parti in comunicazione condividano una chiave crittografica che non sia nota a nessun altro.

Si presenta, dunque, il problema della generazione e della distribuzione di tale chiave. Questo può essere risolto inviando la chiave di cifratura in anticipo alle parti coinvolte attraverso un canale sicuro come ad esempio un corriere o la posta raccomandata. Tuttavia, i ritardi e i costi introdotti da questa modalità di distribuzione non sono accettabili nella maggior parte delle applicazioni pratiche, specialmente in ambito commerciale. L’algoritmo proposto nell’articolo [4] offre un approccio differente al problema, eliminando, di fatto, la necessità di un canale sicuro per la condivisione delle chiavi.

Noto come “Diffie-Hellman key exchange” (scambio di chiavi Diffie-Hellman), tale algoritmo crittografico consente a due parti di stabilire una chiave di cifratura condivisa e segreta attraverso un canale di comunicazione insicuro (pubblico), senza la necessità che queste si siano scambiate informazioni in precedenza.

La figura 2.1 illustra i passi dell’algoritmo che portano allo scambio della chiave di cifratura K condivisa fra le due parti.

Figura 2.1 - Scambio di chiavi Diffie-Hellman

(24)

Come visto nel capitolo 1, possiamo pensare alle due parti come a un client e a un server che si scambiano una chiave di cifratura.

Il client, innanzitutto, genera un numero primo p molto elevato di almeno 1024 bit (cioè circa 300 cifre decimali) e un generatore g di Zp* (che deve esistere essendo p primo). In aritmetica modulare un generatore modulo p (detto semplicemente generatore) è un intero g le cui potenze modulo p sono congruenti con i numeri coprimi a p. Ovvero g è un intero che elevato a potenza genera tutti i numeri coprimi a p. In particolare, è possibile dimostrare che se p è primo esiste sicuramente un generatore g che produce tutti gli elementi di Zp*, in altre parole i numeri compresi tra 1 e (p -1). Vedremo nel seguito del capitolo il metodo implementato per trovare un generatore di Zp*.

Il client genera quindi un numero casuale a < p e calcola A = ga mod p. Come evidenziato in figura 2.1, i numeri p, g, A sono comunicati pubblicamente al server mentre a non viene condiviso.

In modo analogo il server genera un numero casuale b < p, calcola B = gb mod p e lo invia al client. Entrambe le parti possono quindi calcolare la chiave di cifratura K:

• Il client calcola K = Ba mod p.

• Il server calcola K = Ab mod p.

Come indica l’ultimo passaggio in figura 2.1, client e server trovano lo stesso valore per K. Per le proprietà dell’aritmetica modulare, infatti, si ha che:

K = Ab mod p = (ga mod p)b mod p = gab mod p = (gb mod p)a mod p = Ba mod p La chiave ottenuta può essere, quindi, utilizzata per crittografare le successive comunicazioni fra le due parti mediante un algoritmo di cifratura a chiave simmetrica. In questa tesi, a titolo di esempio applicativo, è stato implementato un algoritmo di cifratura simmetrica che, come vedremo nel seguito del capitolo, esegue l’XOR bit a bit fra la chiave crittografica generata e i dati in chiaro da inviare.

2.1.1 Sicurezza

La sicurezza dell’algoritmo si basa sulla complessità computazionale del calcolo del logaritmo discreto.

Una terza parte può intercettare i parametri p, g, A, B ma non è in grado di calcolare la chiave di cifratura K non conoscendo a o b. Non essendo stati condivisi, l’unico modo per determinare questi numeri è risolvere il problema del logaritmo discreto, infatti:

• a = logg A mod p

• b = logg B mod p

La sicurezza dell’algoritmo, quindi, è garantita fino a quando il calcolo del logaritmo discreto è computazionalmente proibitivo.

(25)

A tale scopo, il generatore g gioca un ruolo fondamentale. Come documentato nei siti [5] e [6], affinché l’algoritmo Diffie-Hellman sia sicuro non è necessario che il generatore produca tutti gli interi modulo p non nulli, ma è sufficiente che il sottogruppo di Zp* generato da g sia tale da rendere elevata la complessità computazionale del logaritmo discreto.

Per ottenere ciò devono essere soddisfatte le seguenti condizioni:

• p sia un numero primo generato in modo casuale e sufficientemente grande, almeno 1024 bit.

• il generatore g ha ordine r che è il più piccolo intero per cui gr = 1 mod p. Il più grande divisore primo di r deve essere elevato, di almeno 2k bit, dove k bit è il livello di sicurezza desiderato.

La prima condizione può essere facilmente soddisfatta grazie alle numerose librerie disponibili per la generazione di numeri primi casuali, come vedremo nell’implementazione realizzata dell’algoritmo.

Fare in modo che l’ordine r del generatore scelto abbia un divisore primo sufficientemente grande, invece, può essere più complesso. È conveniente, quindi, procedere nel modo descritto di seguito (come indicato nel sito [6]). Si deve, innanzitutto, generare un numero primo casuale p “sicuro”, cioè tale che (p-1)/2 sia anch’esso primo. Indichiamo con q il numero (p-1)/2 e quindi possiamo esprimere p come p = 2q +1. Poiché p deve essere un numero primo sufficientemente grande, anche q lo sarà.

Si può dimostrare che un qualunque intero casuale g modulo p non nullo ha ordine r pari a 1, 2, q o 2q. Gli ordini pari a 1 e 2 si ottengono rispettivamente per g uguale a 1 e (p-1). Qualunque altro intero casuale g compreso nell’intervallo [2, (p-2)] avrà, quindi, ordine r pari a q o 2q e può quindi essere usato come generatore per l’algoritmo Diffie-Hellman. Infatti, essendo q un numero primo sufficientemente grande, l’ordine r del generatore sarà elevato, come richiesto per la sicurezza dell’algoritmo.

Soddisfatte queste condizioni, per una terza parte che intercetta la comunicazione è pressoché impossibile ricostruire la chiave scambiata dalle informazioni ottenute.

Purtroppo l'algoritmo Diffie-Hellman è vulnerabile all'attacco "Man in the middle"

(del terzo uomo interposto). Supponiamo che una terza parte intercetti il parametro A che il client invia al server con cui vuole comunicare. Questa può generare, fingendosi il server, un suo numero B e inviarlo al client scambiando così una chiave di cifratura condivisa. In modo del tutto analogo, questa terza parte può generare una chiave condivisa col server e, da qui in avanti, intercettare e leggere tutta la corrispondenza tra i due.

Per prevenire simili attacchi, la soluzione è di usare un ente certificatore che garantisca l'identità dei corrispondenti.

(26)

2.2 Scambio della chiave di cifratura: classe Dh

Analizziamo, quindi, l’implementazione dell’algoritmo Diffie-Hellman realizzata in questa tesi.

Nella versione in linguaggio C++ è stata creata la classe Dh, la quale consente lo scambio di una chiave crittografica secondo le modalità previste dall’algoritmo e, inoltre, gestisce la cifratura e la decifratura dei dati attraverso tale chiave.

L’interfaccia della classe Dh e l’implementazione dei suoi metodi sono interamente riportate in appendice, rispettivamente in ClassDh.hpp e ClassDh.cpp. Tuttavia, per semplicità di consultazione, ne richiamiamo di seguito la dichiarazione.

14    class  Dh   15    {  

16            public:  

17                    mpz_class  ReturnKey()  const  {  return  K;  }  

18                    int  SizeOfK()  const  {  return  mpz_sizeinbase(K.get_mpz_t(),   10);}  

19                    bool  GenerateEncryptionKey_Client(int  socket,  string   ipAddress,  int  udpPort);  

20                    bool  GenerateEncryptionKey_Server(int  socket);  

21                    string  DhEncrypt(const  string&  toEncryptStr);  

22                    string  DhDecrypt(const  char*  toDecrypt);  

23  

24            private:  

25                    mpz_class  p,  g,  a,  b,  A,  B;  

26                    mpz_class  K;  //key  

27                    mpz_class  GetVariable(string  varName);  

28                    mpz_class  GeneratePrimeNumber(string  name,  unsigned  numBits);  

29                    mpz_class  FindGenerator();  

30                    void  GetPgaAndSetA(int  choice);  

31                    bool  SendPGA(int  socket,  string  ipAddress,  int  udpPort);  

32                    bool  ReceiveBandComputeK(int  socket);  

33                    bool  ReceivePGA(int  socket);  

34                    bool  SendB(int  socket);  

35                    void  ComputeBandKey();  

36                    int  rdtsc();  

37    };  

Da notare, innanzitutto, come le variabili dell’algoritmo p, g, a, b, A, B nonché la chiave di cifratura K siano inserite nella parte privata, secondo il principio dell’information hiding (linee 25 e 26). Chi utilizza la classe, quindi, non può accedere direttamente a questi dati ma può solamente ricorrere ai metodi presenti nella sua parte pubblica. Tutto ciò si traduce in una maggior sicurezza poiché la chiave di cifratura e tutte le variabili che permettono di generarla non possono essere modificate in modo indiscriminato ma solamente secondo le modalità previste dalle funzioni pubbliche della classe.

Osserviamo, inoltre, come questi parametri siano di tipo mpz_class. Questa, infatti, è la classe messa a disposizione dalla libreria GMP per la gestione di numeri interi molto elevati.

(27)

Lo scambio della chiave crittografica fra due parti, qui pensate come un client e un server, è realizzato dai metodi pubblici GenerateEncryptionKey_Client() e GenerateEncryptionKey_Server(). Essi utilizzano le funzioni della parte privata della classe Dh per accedere alle variabili dell’algoritmo e impostarne i valori, come evidenziato in figura 2.2.

La figura 2.2, riportata di seguito, rappresenta in dettaglio la successione delle chiamate alle funzioni di classe.

Lo scambio della chiave ha inizio col client che chiama il metodo pubblico GenerateEncryptionKey_Client() (linee 182÷209 in appendice). Come evidenziato in figura 2.2, questo metodo prevede l’esecuzione di tre funzioni private della classe Dh, la prima delle quali è GetPgaAndSetA() che imposta i parametri p, g, a, A.

L’utente del client può lasciare che sia il calcolatore a generare i due parametri principali p, g oppure inserirli manualmente (linee 184÷194). La seconda possibilità è certamente utile dal punto di vista didattico poiché permette all’utente di osservare il funzionamento dell’algoritmo con diverse combinazioni dei suoi parametri.

Tuttavia, la generazione manuale di numeri interi molto elevati che soddisfino i criteri di sicurezza richiesti risulta estremamente complessa. A questo scopo è, quindi, possibile selezionare la prima opzione che garantisce l’affidabilità dell’algoritmo.

La funzione GetPgaAndSetA() riceve come parametro d’ingresso un intero choice che rappresenta la scelta fatta dall’utente: 1 se viene scelta la prima opzione, 2 per la seconda (linea 196).

Figura 2.2 - Funzioni per lo scambio della chiave

GenerateEncryptionKey_Client()

GetPgaAndSetA()

SendPGA()

ReceiveBandComputeK()

GenerateEncryptionKey_Server()

ReceivePGA()

ComputeBandKey()

SendB()

client server

(28)

Nel primo caso, la generazione dei parametri avviene in questo modo:

• p viene creato dalla funzione GeneratePrimeNumber() (linea 24 in appendice) a cui viene passato in ingresso un intero numBits, oltre ad una stringa name che indica il nome della variabile (p in questo caso). La funzione utilizza il metodo BN_generate_prime_ex() fornito dalla libreria OpenSSL per generare un numero primo pseudo-casuale di almeno numBits bit di lunghezza; qui è stato utilizzato un valore di 1024 bit (linea 78 in appendice). Il terzo parametro passato a BN_generate_prime_ex() è un intero di valore uno; questo indica alla funzione di generare un numero primo

“sicuro”. Da notare come la libreria OpenSSL utilizzi il tipo di dato BIGNUM e sia, quindi, necessaria una conversione intermedia in una stringa per tradurlo in un intero mpz_class (linea 80 in appendice). In C++ questo passaggio risulta, di fatto, trasparente: grazie all’overload dell’operatore “=”, è, infatti, possibile assegnare all’intero mpz_class la stringa restituita dalla funzione BN_bn2dec() corrispondente al numero primo generato. Il parametro p così creato è, quindi, un numero primo pseudo-casuale di almeno 1024 bit “sicuro”, cioè tale che anche (p-1)/2 è primo.

• g viene generato dalla funzione FindGenerator(). Come visto in precedenza, poiché p è un numero primo “sicuro”, è possibile utilizzare come generatore un numero intero casuale di valore compreso fra 2 e p-2. Questo è esattamente il compito svolto dalla funzione sopraccitata. Innanzitutto viene prodotto un intero casuale size minore o uguale al numero di cifre di p-2.

Questa sarà la dimensione del generatore. Infatti, viene generato un numero size di interi casuali che vanno a costituire le cifre di g nel seguente modo: se size è uguale alla dimensione di p-2, allora ogni cifra di g è generata come intero casuale minore di quella corrispondente in p-2 altrimenti come intero casuale compreso tra 0 e 9. Questo garantisce che il generatore sia un numero casuale minore di p-2 (linee 48÷65 in appendice). Se si dovesse ottenere un valore di g pari a 2, la procedura verrebbe ripetuta.

Da notare l’utilizzo del numero di cicli di clock della CPU come seme per srand(). Questa informazione è fornita dall’istruzione assembly rdtsc (eseguita dall’omonima funzione della classe Dh) e garantisce una migliore randomizzazione essendo un numero che varia in modo estremamente rapido e difficilmente prevedibile.

Nel secondo caso, invece, viene chiamata la funzione GetVariable() per permettere all’utente del client di inserire i valori di p e g desiderati.

Dopodiché, indipendentemente da come sono stati generati i due parametri, all’utente spetta il compito di scegliere e inserire un valore casuale a minore di p (sempre attraverso la funzione GetVariable()); a questo punto A viene calcolato come A = ga mod p (linee 32 e 34 in appendice).

Da notare come la funzione mpz_powm() fornita dalla libreria GMP per l’elevamento a potenza esegua interamente il calcolo precedente, compresa l’operazione di modulo. Come si vede nel codice in appendice, infatti, la funzione accetta quattro parametri: rispettivamente A (in cui è salvato il risultato dell’operazione), la base g, l’esponente a e il divisore p.

(29)

Al client non resta che comunicare p, g e A al server tramite il metodo privato SendPGA(). Come evidenziato in figura 2.2, infatti, contemporaneamente al client, il server esegue la funzione GenerateEncryptionKey_Server() e si mette in attesa di questi tre parametri chiamando ReceivePGA().

Se entrambe le operazioni hanno successo, il server procede generando b, calcolando B e la chiave di cifratura K tramite la funzione privata ComputeBandKey().

All’utente del server viene chiesto, quindi, di inserire un numero intero casuale b diverso da uno ma minore di p; dopodichè si procede al calcolo di B = gb mod p e quindi di K = Ab mod p (linee 174÷178 in appendice).

Il server mantiene segreto il numero b scelto e la chiave K ma condivide B chiamando la funzione SendB(). Il client tramite il metodo ReceiveBandComputeK() può infine ricevere B e calcolare K = Ba mod p (linee 109÷118 in appendice).

Le chiavi così calcolate dalle due parti risultano identiche come dimostrato in precedenza.

Da notare come lo scambio dei dati tra le due parti avvenga, attraverso la rete, secondo le modalità descritte in precedenza al punto 1.3. A questo proposito si evidenziano le chiamate alle funzioni udp_send(), udp_reply() e udp_receive() (linee 95, 109, 163 e 133 in appendice) rispettivamente per inviare e ricevere i dati.

Ricordiamo come sia necessario per il client e il server creare il socket prima di utilizzare tali funzioni. Questo passaggio non è gestito direttamente dalla classe Dh e deve, quindi, essere svolto dall’applicazione che la utilizza, come vedremo nell’esempio che sarà illustrato nel seguito del capitolo.

2.2.1 Funzioni crittografiche della classe Dh

Come si nota nella sua dichiarazione (linee 21 e 22), la classe Dh comprende le due funzioni DhEncrypt() e DhDecrypt() che utilizzano la chiave crittografica condivisa rispettivamente per la cifratura e la decifratura dei dati. Come detto in precedenza, nell’esempio applicativo realizzato la cifratura avviene eseguendo l’XOR tra la chiave e i dati da trasmettere.

La funzione DhEncrypt() riceve in ingresso il parametro toEncryptStr, cioè la stringa di dati da crittografare. Partendo dal primo, viene eseguito l’XOR tra il valore decimale di ciascun carattere di toDecrypt e quello del carattere corrispondente della chiave di cifratura; il risultato è poi memorizzato nella stringa encrypted (linee 230÷235 in appendice). Da notare il cast verso il tipo unsigned char (linea 233), utilizzato per ottenere solamente valori positivi dall’operazione di XOR, contrariamente a quanto accadrebbe se toEncryptStr contenesse caratteri non appartenenti alla tabella ASCII standard.

Terminata l’elaborazione, la stringa encrypted che contiene i dati crittografati viene restituita come output dalla funzione. È fondamentale osservare come la lunghezza di toEncryptStr debba essere minore o uguale a quella della chiave, altrimenti una parte delle informazioni non sarebbe cifrata. La classe Dh fornisce, quindi, il selettore SizeOfK() che restituisce esattamente il numero di caratteri della chiave crittografica.

Questa informazione permette di suddividere correttamente i dati da trasmettere prima di utilizzare DhEncrypt().

(30)

La funzione DhDecrypt() esegue, invece, l’operazione di decifratura (linee 239÷258 in appendice). Essa riceve in ingresso la stringa toDecrypt che contiene i dati iniziali crittografati, ovvero una successione di cifre decimali. Ogni terna di caratteri della stringa costituisce, quindi, un numero intero, risultato dell’operazione di XOR eseguita dalla funzione DhEncrypt() appena illustrata. Eseguendo nuovamente l’XOR tra questi interi e i caratteri della chiave di cifratura si ottengono i dati di partenza che vengono memorizzati in decrypted (linea 253).

La funzione DhDecrypt() restituisce, quindi, la stringa decrypted contenente i dati decriptati.

(31)

2.3 Comunicazione criptata: libreria DhDataLib

La libreria DhDataLib è stata creata per la trasmissione e la ricezione di dati cifrati utilizzando la classe Dh. Il codice relativo è riportato interamente in appendice (si faccia riferimento alle voci DhDataLib.hpp e DhDataLib.cpp).

Di seguito ne richiamiamo l’header:

1      #ifndef  DHDATALIB_HPP   2      #define  DHDATALIB_HPP   3  

4      #include  "ClassDh.hpp"  

5      #include  <fstream>  

6  

7      bool  ReadAndSendData(Dh&  srv,  int  socket,  string  ipAddress,  int   udpPort);  

8      bool  ReceiveAndDecryptData(Dh&  srv,  int  socket);  

9      void  SendData(const  string&  data,  Dh&  srv,  int  socket);  

10    string  ReadFromFile();  

11    string  ReadMessage();  

12    void  printStrInDec(const  string&  toPrint);  

13    void  printStrInHex(const  string&  toPrint,  bool  crypted);  

14  

15    #endif  

Le due funzioni principali della libreria DhDataLib sono ReadAndSendData() e ReceiveAndDecryptData(). La prima è utilizzata per inviare dati criptati mentre la seconda è impiegata per riceverli e decifrarli.

La funzione ReadAndSendData() (linee 71÷105 in appendice) permette, innanzitutto, di scegliere fra tre opzioni:

1) Inviare i dati inseriti da tastiera.

2) Inviare i dati contenuti in un file di testo.

3) Terminare la trasmissione.

Nel primo caso viene chiamata il metodo ReadMessage() che legge i caratteri inseriti da testiera, accoda ad essi la stringa di controllo "EOF~EOF~EOF" che ne indica la fine e memorizza il tutto nella stringa restituita come output. All’interno di ReadAndSendData() i caratteri letti dal metodo appena citato sono assegnati alla stringa message che viene poi inviata grazie alla funzione SendData(), che verrà descritta nel seguito (linee 87 e 88 in appendice).

La seconda opzione prevede la trasmissione del contenuto di un file di testo. L’unica differenza rispetto al caso precedente, quindi, sta nel fatto che i dati del file sono letti dalla funzione ReadFromFile() e non da ReadMessage() (linea 93).

Nel terzo caso, invece, la stringa di controllo "EOTS~EOTS~EOTS" viene inviata tramite SendData() per indicare al ricevitore la fine della trasmissione.

ReadAndSendData() restituisce, infine, un valore booleano che indica se è stata scelta o meno la terza opzione, ovvero se la trasmissione deve essere interrotta (linea 104).

(32)

Come detto, quindi, è SendData() la funzione che invia effettivamente i dati da trasmettere, dopo averli crittografati grazie alla classe Dh (linee 140÷163 in appendice).

Nel punto 2.2.1 del capitolo 2 abbiamo visto come i dati da cifrare debbano essere suddivisi in blocchi di caratteri di lunghezza minore o uguale a quella della chiave crittografica per utilizzare la funzione DhEncrypt().

SendData() deve gestire, quindi, anche questa operazione. Come si evince dal codice, la funzione riceve in ingresso la stringa data da trasmettere e, partendo dal primo, ne copia i caratteri in encryptedStr fino a quando la lunghezza di quest’ultima è pari a quella della chiave crittografica. A questo punto encryptedStr viene cifrata tramite DhEncrypt() ed inoltrata grazie alla funzione udp_reply() della libreria udpsocketlib (linee 152 e 156). Tutti i caratteri dalla stringa encryptedStr vengono, quindi, eliminati e la procedura viene iterata fino a quando data non è stata completamente inviata.

La funzione ReceiveAndDecryptData() (linee 35÷69 in appendice) deve svolgere quindi l’operazione inversa e riscostruire interamente i dati iniziali a partire dai frammenti cifrati ricevuti, che vengono prodotti come visto sopra.

Ognuna di queste stringhe criptate è ricevuta attraverso la funzione udp_receive() e assegnata al vettore di caratteri buffer (linea 45). Quest’ultimo viene decifrato dalla funzione DhDecrypt() e il risultato è salvato nella stringa bufferDecrypted (linea 49) che viene poi aggiunta a messageDecripted (linea 52). Quando in bufferDecrypted è presente la successione di caratteri "EOF~EOF~EOF" (che viene rimossa), la stringa messageDecripted contiene i dati iniziali trasmessi completamente ricostruiti (linee 59÷66).

La funzione ReceiveAndDecryptData() restituisce un valore booleano che sarà vero solamente quando viene ricevuta la stringa "EOTS~EOTS~EOTS" che indica il termine della comunicazione.

La libreria DhDataLib presenta, infine, i due metodi printStrInDec() printStrInHex() che permettono di stampare a video rispettivamente i valori decimali ed esadecimali dei caratteri delle stringhe fornite in input (linee 3÷33).

(33)

2.4 Esempio di utilizzo

Vediamo ora un esempio di utilizzo della libreria DhDataLib e della classe Dh.

Come illustrato nei capitoli precedenti, due parti, che indichiamo come client e server, grazie alla classe Dh generano una chiave di cifratura condivisa che viene usata dalle funzioni della libreria DhDataLib per crittografare la comunicazione tra di esse.

Riportiamo di seguito il codice in linguaggio C++ relativo alle applicazioni del client e del server che utilizzano gli strumenti appena citati, insieme alla libreria udpsocketlib.

Server

1      #include  "ClassDh.hpp"  

2      #include  "udpsocketlib.hpp"  

3      #include  "DhDataLib.hpp"  

4  

5      int  main(int  argc,  char  *argv[])   6      {  

7              Dh  srv;  

8              int  sk,  udpPort;  

9              string  serverIpAdrress,  message;  

10            bool  done  =  false;  

11  

12            if  (argc  ==  1)   13            {  

14                    cout<<"Server  ip  address:";  

15                    cin>>serverIpAdrress;  

16                    cout<<"Udp  port:";  

17                    cin>>udpPort;  

18            }  

19            else  if  (argc  ==  3)   20            {  

21                    serverIpAdrress  =  argv[1];  

22                    udpPort  =  atoi(argv[2]);  

23            }   24            else   25            {  

26                    cout<<"Required  arguments:  server  ip  address,  udp  port"<<endl;  

27                    exit(1);  

28            }   29  

30            if  ((sk  =  create_udp_server((char*)serverIpAdrress.c_str(),   udpPort))  <  0)  

31            {  

32                    cout<<"Cannot  open  server  socket"<<endl;  

33                    exit(1);  

34            }   35  

36            if  (!srv.GenerateEncryptionKey_Server(sk))   37                    exit(1);  

38  

39            do  

(34)

40            {  

41                    done  =  ReceiveAndDecryptData(srv,  sk);  

42                    if  (!done)  

43                            done  =  ReadAndSendData(srv,  sk);  

44            }while  (!done);  

45  

46            close_udp_socket(sk);  

47            cout<<"\n\nExit..\n\n";  

48            return  0;  

49    }  

Client

1      #include  "ClassDh.hpp"  

2      #include  "udpsocketlib.hpp"  

3      #include  "DhDataLib.hpp"  

4  

5      int  main(int  argc,  char  *argv[])   6      {  

7              Dh  clnt;  

8              int  sk,  udpPort;  

9              string  serverIpAdrress;  

10            bool  done  =  false;  

11  

12            if  (argc  ==  1)   13            {  

14                    cout<<"Server  ip  address:";  

15                    cin>>serverIpAdrress;  

16                    cout<<"Udp  port:";  

17                    cin>>udpPort;  

18            }  

19            else  if  (argc  ==  3)   20            {  

21                    serverIpAdrress  =  argv[1];  

22                    udpPort  =  atoi(argv[2]);  

23            }   24            else   25            {  

26                    cout<<"Required  arguments:  server  ip  address,  udp  port"<<endl;  

27                    exit(1);  

28            }   29  

30            if  ((sk  =  create_udp_client())  <  0)   31            {  

32                    cout<<"Cannot  open  client  socket"<<endl;  

33                    exit(1);  

34            }   35  

36            if  (!clnt.GenerateEncryptionKey_Client(sk,  serverIpAdrress   ,udpPort))  

37                    exit(1);  

38  

39            do   40            {  

41                    done  =  ReadAndSendData(clnt,  sk);  

42                    if  (!done)  

(35)

43                            done  =  ReceiveAndDecryptData(clnt,  sk);  

44            }while  (!done);  

45  

46            close_udp_socket(sk);  

47            cout<<"\n\nExit.."<<endl<<endl;  

48            return  0;  

49    }  

Innanzitutto, entrambe le parti creano il socket UDP grazie alle due funzioni create_udp_server() e create_udp_client() messe a disposizione dalla libreria udpsocketlib. Come visto nel punto 1.3 del capitolo 1, create_udp_server() riceve in ingresso l’indirizzo IPv4 del server e la porta locale su cui attenderà i dati per associarle al socket (linea 30, Server). Queste informazioni devono essere fornite come argomento della funzione main del server direttamente dalla linea di comando, altrimenti il loro inserimento viene richiesto esplicitamente dal programma (linee 12÷28, Server).

Le stesse informazioni devono essere fornite all’applicazione del client; in questo caso però non saranno associate al socket ma saranno utilizzate, quando necessario, solamente per inviare dati al server. Da notare, infatti, come create_udp_client() non riceva parametri in ingresso (linea 30, Client).

Una volta creato il socket, client e server possono procedere alla generazione di una chiave crittografica condivisa, come illustrato in precedenza in questo capitolo.

Vengono chiamate, infatti, da entrambe le parti le funzioni della classe Dh GenerateEncryptionKey_Server() e GenerateEncryptionKey_Client() per lo scambio della chiave (linea 36, Client e Server).

Se l’operazione ha successo, le due parti possono, infine, utilizzare la libreria DhDataLib per comunicare in modo protetto attraverso la rete. Come si vede nel codice, entrambe le parti entrano in un ciclo do..while in cui vengono eseguite alternativamente le due funzioni ReadAndSendData() ReceiveAndDecryptData() (linee 39÷44).

La comunicazione ha inizio con il client che chiama ReadAndSendData() per inviare i dati al server, il quale a sua volta esegue ReceiveAndDecryptData() per riceverli.

Dopodiché le parti si scambiano i ruoli e il ciclo continua fino a quando una delle due non termina la comunicazione. Ricordiamo, infatti, che entrambe le funzioni restituiscono un valore booleano che indica il raggiungimento di tale condizione, come visto nel punto 2.3 del capitolo 2.

A questo punto da entrambe le parti il socket viene chiuso e le applicazioni terminano (linee 46 e 48).

Riferimenti

Documenti correlati

Le slide possono essere riprodotte ed utilizzate liberamente dagli istituti di ricerca, scolastici ed universitari afferenti al Ministero della Pubblica Istruzione e al Ministero

Ogni altro utilizzo o riproduzione (ivi incluse, ma non limitatamente, le riproduzioni su supporti magnetici, su reti di calcolatori e stampe) in toto o in parte è vietata, se

L’informazione contenuta in queste slide è ritenuta essere accurata alla data della pubblicazione.. Essa è fornita per scopi meramente didattici e non per essere utilizzata in

Le slide possono essere riprodotte ed utilizzate liberamente dagli istituti di ricerca, scolastici ed universitari afferenti al Ministero della Pubblica Istruzione e al Ministero

uguali con il carattere riservato seguito da uno solo dei byte ripetuti più un.. contatore del numero

In ogni caso non può essere dichiarata conformità all’informazione contenuta in queste slide.. In ogni caso questa nota di copyright e il suo richiamo in calce ad ogni slide non

In ogni caso questa nota di copyright e il suo richiamo in calce ad ogni slide non devono mai essere rimossi e devono essere riportati anche in utilizzi parziali.. Nota

• L'espressione può essere omessa, e in tal caso il tipo della funzione (che non ritorna alcun valore) dovrebbe essere void.. Chiamata a