MokaByte
Numero 13 - Novembre 1997
|
|||
|
crittografiche con la Java Security API |
||
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:
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:
La Java Security API è stata progettata seguendo due principali fini:
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
ricerca nuovi collaboratori
|
||
|