MokaByte Numero 13 - Novembre 1997
Foto
Implementazione di tecniche 
crittografiche con la Java Security API
di 
Fabrizio
Guidici
 

 


 



Tutta la problematica relativa alla sicurezza dei dati è diventata un tema molto importante in relazione alle tecnologie di rete. L'evidente motivo è stato l'introduzione della possibilità di effettuare transazioni monetarie e commercio di tipo elettronico, ma non solo: la continua informatizzazione della società contemporanea fa sì che sempre più dati non strettamente di ambito commerciale, ma comunque da trattare con riserbo, vengano gestiti in modo elettronico. Ne sono un esempio evidente le cartelle mediche personali, ma vale la pena di ricordare che una legge entrata recentemente in vigore in Italia sancisce che qualsiasi database di dati personali (quindi anche un banale archivio di clienti) deve essere protetto da qualsiasi tipo di uso non corretto. Per tutti questi motivi, Java mette a disposizione gli strumenti necessari per implementare efficaci strategie di protezione dei dati.

 

Una breve introduzione alla sicurezza di rete ed alla crittografia

Che Internet ed in generale una rete non siano di per sè stesse un "luogo sicuro" è risaputo: la stessa facilità che consente l'accesso ad una rete commerciale da parte di un cliente, ovunque esso sia, facilita purtroppo la vita ai pirati informatici che, per divertimento o per lucro, riescono ad entrare su sistemi su cui l'accesso dovrebbe essere impedito, o a carpire informazioni riservate mentre sono in transito.

La soluzione al problema è la crittografia: ovvero la scienza di nascondere i dati in modo tale che solo un ridotto insieme di persone autorizzate possano accedervi (sul rovescio della medaglia, la crittografia è anche la scienza di rubare i dati che dovrebbero essere riservati ad un ridotto insieme di persone autorizzate…).

Il tipico problema della crittografia coinvolge tre giocatori:

    1. Il primo è il proprietario dei dati. Si presume che egli abbia delle informazioni riservate che voglia far giungere ad alcuni "amici" e solo a loro. La trasmissione deve avvenire su un canale non sicuro (come Internet), e quindi non può essere "fisicamente" protetta. Una variante del problema può essere la necessità di garantire l'autenticità del messaggio (cioè assicurarsi che il mittente sia proprio chi asserisce di essere) e non proteggerne i contenuti.
    2. Il secondo è il ricevente, che vuole semplicemente leggere i dati così come li può leggere il loro proprietario.
    3. Il terzo è il "cattivo", il criptanalista, che cerca di spiare la conversazione tra il mittente ed il ricevente e di carpire i dati trasmessi.
La crittografia consiste nel trasformare i dati in una forma che possa essere ritrasformata nell'originale solo dai riceventi autorizzati e non dagli "spioni". Questa trasformazione non è fisica, come la scrittura di una lettera in inchiostro invisibile, ma una trasformazione matematica reversibile, che solo una persona autorizzata sa "invertire". La trasformazione è detta crittazione e viene effettuata dal mittente, mentre la sua trasformazione inversa è detta decrittazione e viene effettuata dal ricevente. I dati originali sono chiamati "testo in chiaro", quelli trasformati "testo cifrato". Il meccanismo di crittazione/decrittazione è composto da una parte fissa e generale (algoritmo di decrittazione), generalmente nota a tutti, e da una personalizzazione dell'algoritmo (chiave), nota solo alle persone autorizzate.

Una volta che la chiave è stata scambiata con successo, il mittente ed il ricevente possono implementare separatamente i loro algoritmi di crittazione e decrittazione. Anche se il "cattivo" riesce a leggere i dati cifrati mentre vengono trasmessi sulla rete, non è in grado di riuscire a decodificarli se non possiede la chiave corretta.

D'altro canto se egli entra in possesso della chiave, tutto il sistema fallisce. La trasmissione della chiave diventa quindi la parte più delicata di tutta l'operazione. Per questa ragione, una speciale categoria di algoritmi di crittazione è diventata molto popolare: i Sistemi a Chiave Pubblica (Public Key Systems). Senza entrare nei dettagli, un sistema a chiave pubblica ha quest'importante proprietà: la crittazione e la decrittazione usano chiavi differenti, ed una delle due può essere resa pubblica senza inconvenienti, perché da essa non si può risalire a quella privata; generalmente le due chiavi sono interscambiabili (cioè ognuna delle due può essere arbitrariamente scelta per effettuare la crittazione o la decrittazione).

Un sistema a chiave pubblica può essere usato, per esempio, per garantire l'autenticità dei dati: in tal caso il mittente genera una coppia di chiavi, distribuisce pubblicamente la chiave di decrittazione ed usa la chiave privata per crittare i dati da trasmettere. Chi riceve prova a decrittare i dati usando la chiave pubblica: se ci riesce, ha la certezza sull'autenticità del messaggio (il "cattivo" dovrebbe usare la chiave privata per impersonare il mittente, ma proprio in quanto privata essa non può essere in suo possesso). Se viceversa un messaggio viene crittato con la chiave pubblica, solo il destinatario sarà in grado di decrittarlo con la sua corrispondente chiave privata, garantendo la riservatezza.

Questo è giusto un "assaggio": una descrizione più completa del funzionamento di chiavi pubbliche/private e più in generale delle tecniche crittografiche richiederebbe un libro intero. Tuttavia ora ne sappiamo abbastanza per affrontare qualche considerazione più "pratica" (cioè: "come si implementano queste cose in un programma?").

 

Considerazioni pratiche sull'uso delle tecniche crittografiche

Oggi esistono moltissimi algoritmi crittografici differenti, e in maggioranza sono sufficientemente robusti per trasmettere dati riservati anche di tipo commerciale. Progettare e testare un algoritmo crittografico è un lavoro da specialisti (tipicamente più nel versante matematico che informatico), ma non da progettisti di programma applicativo. Un buon progettista deve rivolgersi alle soluzioni già esistenti sul mercato, tenendo in considerazione prezzi, prestazioni e disponibilità (per motivi legali non tutti gli algoritmi possono essere usati fuori dagli USA). Inoltre, a volte capita che un algoritmo ritenuto robusto venga improvvisamente "rotto" da un ricercatore o da un pirata informatico, costringendo tutti i suoi utilizzatori a ripiegare verso un'alternativa più valida.

Per tutti questi motivi, un'applicazione sicura con un buon progetto non deve basarsi strettamente su un particolare algoritmo crittografico, ma deve essere realizzata in modo modulare.

 

L'implementazione della crittografia nel linguaggio Java

Java possiede un'API appositamente dedicata alle funzionalità crittografiche e facilmente incorporabile nei propri programmi: la Security API. Essa è disponibile come parte integrante del JDK 1.1, anche se alcune funzionalità sono state rese disponibili solo recentemente in un'estensione (JCE - Java Cryptographic Extension) che non è attualmente (Ottobre 1997) disponibile al di fuori degli USA per i già citati problemi legali.

Le funzionalità crittografiche presenti nella Security API sono:

  1. Crittazione: trasformazione di dati in modo che essi possano essere accessibili solo ad utenti autorizzati.
  2. Firme Digitali: particolari sequenze di numeri che possono essere associate a dati digitali per assicurare l'utente della loro provenienza. Possono anche essere utilizzate per "firmare" del codice Java: in questo modo, applet o servlet particolari possono essere in grado di rilassare i vincoli del proprio Security Manager, in quanto sono in grado di provare la loro provenienza "fidata".
  3. Gestione delle chiavi. A come visto nell'introduzione, le chiavi sono gli oggetti con cui un utente può provare di avere l'autorizzazione all'accesso di dati riservati. Le chiavi, che poi altro non sono che numeri "speciali", vanno generate con algoritmi appositi e gestite in modo opportuno.

La Java Security API è stata progettata seguendo due principali fini:

L'indipendenza dell'implementazione e dell'algoritmo vuol dire rendere gli utenti in grado di utilizzare funzionalità crittografiche senza preoccuparsi di come esse sono effettivamente implementate. Questo vuol dire, per esempio, che un nuovo algoritmo crittografico può essere acquistato da un produttore indipendente e facilmente integrato in un'applicazione già esistente.

L'interoperabilità delle implementazioni vuol dire che diverse implementazioni di funzionalità crittografiche possono funzionare tra di loro, scambiarsi le rispettive chiavi, o verificare le rispettive firme digitali.

 

La Security API di Java

La Java Security API è molto ricca di funzionalità e per motivi di spazio non è possibile descriverla tutta in questo articolo. Pertanto ci limiteremo a vedere, con un semplice esempio, come sia possibile dotare un'applicazione di un meccanismo di crittazione basato su meccanismo a chiave pubblica.

La prima cosa che dobbiamo fare è generare la coppia di chiavi. Per questo scopo utilizzeremo le classi Key, KeyPair e KeyPairGenerator, le cui funzionalità sono facilmente desumibili dai nomi: le prime sono semplicemente dei "contenitori" per chiavi singole e doppie, mentre KeyPairGenerator viene usato invece per generare chiavi doppie. In particolare esso va istanziato con un nome di algoritmo (useremo nell'esempio il DSA) e con un numero "casuale". Questo "seme" deve essere il "più casuale possibile" e, nel caso di una transazione, potrebbe essere calcolato partendo dal nome dell'utente e dalla data corrente (per garantire ogni volta sequenze di numeri diversi). Tipicamente un buon valore può essere la "firma digitale" dei dati in questione, che può essere calcolata facendo uso di una classe speciale, MessageDigest.
 
 
 
  {

   MessageDigest md = MessageDigest.getInstance("SHA");

    String string = url + user + (new Date()).toString();

    md.update(string.getBytes());



    KeyPairGenerator keygen = KeyPairGenerator.getInstance("DSA");

    keygen.initialize(512, new SecureRandom(md.digest()));

    KeyPair pair = keygen.generateKeyPair();

    ps.println(" Public-key:  " + pair.getPublic().getEncoded()+

               " Private-key: " + pair.getPrivate().getEncoded()));

  }

L'efficacia della casualità in questi algoritmi è fondamentale. Algoritmi famosi per la loro robustezza possono essere resi vulnerabili da implementazioni scadenti in cui è possibile risalire con buona approssimazione al valore dei numeri casuali generati. Per fare un paragone: è inutile comprare una serratura di massima sicurezza se poi lasciamo vedere la chiave ad uno specialista in grado di duplicarla! Sembra un concetto banale, ma un po' di tempo fa uno dei primi algoritmi implementati in Netscape Navigator fu rotto, secondo questo principio, dai "soliti" studenti di college americano…

Per questo nell'esempio sopra mostrato si è utilizzata una speciale classe, SecureRandom, per la generazione di un numero casuale. Essa è più sofisticata della classica random() ed assicura la massima scorrelazione (cioè impredicibilità) tra il seme (md.digest()) ed il valore usato per generare la coppia di chiavi.

Una volta generata la chiave (o la coppia di chiavi), si crea il motore di crittazione vero e proprio. Esso è implementato dalla classe Cipher, che va inizializzata con il nome dell'algoritmo crittografico da usare (nell'esempio PKA) e con la chiave di crittazione:
 
 
 
Cipher myCipher = Cipher.newInstance("PKA");

myCipher.initEncrypt(key);

Byte[] data;

myCipher.cript(data);

Il codice di crittazione effettivo viene caricato da un "driver" a partire dal nome specificato, in maniera analoga alla selezione dei driver di database in JDBC. È qui che iniziano i problemi: il codice effettivo di crittazione è contenuto nella Java Cryptographic Extension, non disponibile fuori dagli USA… Tuttavia le specifiche di interfaccia per questi driver sono disponibili e se scriviamo una nostra implementazione di un algoritmo crittografico americano non commettiamo nessuna violazione della legge. Infatti la legge non vieta l'uso degli algoritmi "protetti", ma la loro esportazione al di fuori degli USA…

Torniamo l listato. I dati vengono crittati a blocchi richiamando la routine cript; ma se dobbiamo fare operazioni di input/output possiamo usare un filtro di i/o, per esempio CipherOutputStream, che eseguirà tutto trasparentemente:
 
 
 
CipherOutputStream cos = new CipherOutputStream(
  new FileOutputStream("name", myCipher));
// Usare come uno stream normale…

Tutto ovviamente vale per file su disco come per i socket sulla rete. Se per esempio disponiamo di un package che implementa l'algoritmo Secure Socket Layer (SSL), usato dal Netscape Navigator, possiamo implementare facilmente il lato server di una transazione sicura.

 

Conclusione

L'argomento è "delicato" e la progettazione di codice "sicuro" non è cosa banale… per esempio bisogna assicurarsi che le chiavi non viaggino su rete e non vengano memorizzate in strutture dati facilmente accessibili.

Tuttavia le proprietà di safety tipiche di Java ci danno un grande aiuto, perché impediscono ai "cattivi" di penetrare dentro le strutture dati di un programma usando trucchi come puntatori vaganti o sfondamenti di array o dello stack. Queste caratteristiche, insieme alla presenza di interfacce appositamente studiate e standardizzate, come quelle contenute nella Security API, fanno di Java il linguaggio più adatto per l'implementazione di transazioni di rete sicure.
  
 

 

MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it