Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Nozioni base sulla sicurezza in Java

Un approfondimento sulle tecniche e gli strumenti per la sicurezza in Java
Un approfondimento sulle tecniche e gli strumenti per la sicurezza in Java
Link copiato negli appunti

Uno dei punti fondamentali di una tecnologia come Java è sicuramente quello della sicurezza. Sappiamo bene infatti quante informazioni di vitale importanza risiedano sui sistemi informatici di tutto e viaggino sovente in rete per essere fruibili dai più svariati servizi.

Oltre alla struttura stessa del linguaggio, basata su alcune scelte architetturali ben precise (si pensi all'assenza dei puntatori ad esempio, che non permette di manipolare locazioni di memoria in modo arbitrario) esistono una serie di package che offrono numerose funzioni il cui obiettivo comune è quello di garantire la massima sicurezza delle applicazioni. Molti di questi package sono accomunati attraverso degli acronimi che ne illustrano le caratteristiche di fondo. Tra i più noti, ricordiamo:

  1. JCE (Java Cryptography Extension)
  2. JSSE (Java Secure Socket Extension)
  3. JAAS (Java Authentication and Authorization Service)

In questo articolo faremo riferimento a JCE ed, in particolare, analizzeremo i seguenti concetti:

  1. Message Digest
  2. Private Key encryption
  3. Public Key encryption
  4. Digital signatures
  5. Digital certificates

Message Digest

Un Message Digest è, semplicemente, uno strumento che garantisce l'integrità di un messaggio. Viene spesso identificato come una sorta di "impronta digitale" ed il suo compito è quello di prendere in input un messaggio e di produrre in output un blocco di bit che ne rappresentino, appunto, l'impronta digitale in modo inequivocabile. È chiaro che due o più persone che calcolino il Message Digest di un particolare messaggio devono necessariamente attendersi lo stesso risultato. Una minima modifica al messaggio di input potrebbe produrre in output un Message Digest completamente diverso.

Ma in che occasioni possono essere utili i Message Digest? Supponiamo che un'ipotetica persona di nome Gianni voglia inviare alla sua amica Laura un messaggio importante. Come farà Laura ad essere sicura che il messaggio di Gianni non sia stato modificato durante il tragitto da qualche malintenzionato?

Sarà sufficiente confrontare il Message Digest che Gianni avrà prodotto dal suo messaggio originale con quello che Laura potrà calcolare dal messaggio a lei giunto. Se il confronto avrà esito positivo allora Laura potrà avere la garanzia che quanto da lei ricevuto corrisponda a quanto scritto da Gianni.

Gli algoritmi più utilizzati per il calcolo del Message Digest sono, fondamentalmente, due:

  1. MD5 (che utilizza 128 bit)
  2. SHA-1 (che utilizza 160 bit)

Vediamo un esempio di codice Java che calcola un Message Digest di una stringa in input e lo stampa a video:

Listato 1. Calcolo del Message Digest

import java.security.*;

public class MessageDigestCreator
{
   public static void main (String[] args) throws Exception
   {
      // Verifica che venga passato un parametro di input
      // (la stringa per cui calcolare il message digest)
      if (args.length !=1)
      {
        System.err.println("È necessario passare un parametro di input");
        System.exit(1);
      }
      byte[] text = args[0].getBytes("UTF8");

      // Ottiene un oggetto di tipo MessageDigest che utilizzi l'algoritmo
      // MD5, usando il metodo statico MessageDigest.getInstance()
      MessageDigest messageDigest = MessageDigest.getInstance("MD5");

      // Visualizza le informazioni sul provider
      System.out.println( "n" + messageDigest.getProvider().getInfo() );

      // calcola il Message Digest e lo visualizza
      messageDigest.update(text);
      System.out.println( "nDigest: " );
      System.out.println( new String( messageDigest.digest(), "UTF8") );
   }
}

L'algoritmo utilizzato, nel codice precedente, per il calcolo del Message Digest è l'MD5, come si evince dall'istruzione MessageDigest.getInstance("MD5"). Tale istruzione utilizza il metodo statico getInstance() della classe MessageDigest per ottenere un'istanza valida di un Message Digest che utilizzi l'algoritmo specificato in input. Una volta creato l'oggetto, il calcolo viene effettuato attraverso il metodo update() ed, infine, viene effettuata la lettura del Message Digest generato utilizzando il metodo digest().

Crittografia a chiave privata

Probabilmente, osservando il precedente esempio del Message Digest, qualcuno si sarà domandato: «Ok, il Message Digest mi fornisce un'informazione sulla integrità del messaggio ricevuto ma non garantisce che il messaggio stesso non possa essere intercettato e letto durante il percorso!».

Osservazione correttissima. Infatti il Message Digest non introduce una codifica crittografica, quindi permetterebbe la lettura del messaggio ma rappresenta un modo certo per provare che un messaggio non sia stato modificato durante il tragitto.

Uno dei metodi utilizzati per crittografare un messaggio è basato sul concetto di chiave privata: due persone che desiderano scambiarsi un messaggio riservato decidono di utilizzare di comune accordo una chiave privata e segreta (private key) per crittografare il messaggio stesso prima dell'invio e decrittografarlo quando arrivato a destinazione.

Prima di vedere come scrivere un semplice programma Java che faccia uso di una chiave privata, è importante definire alcuni concetti. Innanzitutto, la crittografia di un messaggio avviene, solitamente, a blocchi di 64 bit, detti cipher blocks.

Tale suddivisione comporta, quasi sempre, la necessità di crittografare la parte finale del messaggio (che ovviamente sarà più corta di 64 bit) utilizzando le cosiddette tecniche di padding. Tra le più note citiamo:

  1. No padding (Nessun padding)
  2. PKCS5
  3. OAEP
  4. SSL3

Quando si crea una chiave privata, inoltre, è necessario specificare la modalità per la creazione della chiave stessa (mode). Ad esempio, si potrebbe scegliere di crittografare un blocco in relazione alla crittografia eseguita sul blocco precedente oppure decidere un modo particolare tramite il quale stabilire le relazioni che i blocchi cifrati devono avere. Tra i gli encryption modes più utilizzati, abbiamo:

  1. ECB (Electronic Code Book)
  2. CBC (Cipher Block Chaining)
  3. CFB (Cipher Feedback Mode)
  4. OFB (Output Feedback Mode)
  5. PCBC (Propagating Cipher Block Chaining)

Naturalmente, per completare il quadro, è necessario citare gli algoritmi di crittografia (private key encryption algorithms) da utilizzare per la generazione delle chiavi private. I packages Java offrono svariate soluzioni in tal senso, supportando diversi algoritmi tra i quali citiamo:

  1. DES. Data Encryption Standard
  2. TripleDES.
  3. AES. Advanced Encryption Standard
  4. RC2, RC4, RC5.
  5. Blowfish.
  6. PBE. Password Based Encryption

Vediamo un esempio pratico in Java che esegue la creazione di una chiave privata, crittografa una stringa in input con tale chiave e quindi decrittografa la stessa stringa ottenendo l'input iniziale. Per tale esempio si sono utilizzati i seguenti criteri:

  1. Padding: PKCS5
  2. Mode: ECB
  3. Encryption Algorithm: DES

Listato 2. Esempio di crittazione a chiave privata

import java.security.*;
import javax.crypto.*;

public class PrivateKeyExample
{
 
 public static void main (String[] args) throws Exception
 {
  // Verifica che venga passato un parametro di input
  if (args.length != 1)
  {
   System.err.println("E' necessario passare un parametro di input");
   System.exit(1);
  }

  byte[] text = args[0].getBytes("UTF8");

  // Crea una chiave private attraverso l'algoritmo DES
  System.out.println( "nCreazione Chiave Privata - Start" );
  KeyGenerator keyGen = KeyGenerator.getInstance("DES");
  keyGen.init(56);
  Key key = keyGen.generateKey();
  System.out.println( "nCreazione Chiave Privata - End" );

  // Ottiene un oggetto di tipo Cipher e ne visualizza il provider
  System.out.println( "n" + cipher.getProvider().getInfo() );

  // Cripta il messaggio contenuto in text con la chiave key
  System.out.println( "nEncryption - Start" );
  cipher.init(Cipher.ENCRYPT_MODE, key);
  byte[] cipherText = cipher.doFinal(text);
  System.out.println( "nEncryption - End" );
  System.out.println( new String(cipherText, "UTF8") );

  // Decripta il testo cifrato usando la medesima chiave
  System.out.println( "nDecryption - Start" );
  cipher.init(Cipher.DECRYPT_MODE, key);
  byte[] newText = cipher.doFinal(cipherText);
  System.out.println( "nDecryption - End" );

  System.out.println( new String(newText, "UTF8") );
 }
}

Nella parte precedente dell'articolo abbiamo affrontato la tecnica di crittazione a chiave privata, proseguiamo in questa parte a parlare di sicurezza e Java partendo dalla crittografia a chiave pubblica.

Crittografia a chiave pubblica

Siamo sicuri che con l'utilizzo della sola chiave privata tutti i problemi di sicurezza siano stati risolti? Purtroppo no o, per lo meno, non in modo agevole. Infatti, se qualche hacker provasse a mettersi in ascolto sulla rete ed intercettasse un messaggio crittografato che viene scambiato, con molta probabilità lo avremmo beffato (se abbiamo utilizzato una chiave a 128 bit è, probabilmente, più facile per il nostro hacker totalizzare 6 al supernalotto che decriptare il nostro messaggio!).

Ma, come si è detto, per far sì che entrambi gli utenti che dialogano possano crittografare e decrittografare i messaggi, è necessario che questi siano in possesso della medesima chiave privata. Se i due si sono incontrati personalmente e si sono scambiati tale chiave allora il problema è risolto. Ma, come è facile intuire, non sempre questa soluzione si presenta agevole e sorge l'esigenza di spedire la chiave privata attraverso la rete. Si capisce che se il nostro hacker è pronto ad intervenire anche in questo caso la sicurezza di cui facevamo menzione in precedenza salta.

Ecco che, per risolvere l'ennesimo problema, viene in aiuto il modello a chiave pubblica, inventato nel lontano 1970. Il concetto principale, in questo caso, è che gli utenti hanno entrambi due chiavi: una privata ed una pubblica.

La chiave privata non sarà, come nel caso del Private Key Encryption, condivisa tra gli utenti ma ognuno di essi sarà in possesso di una private key propria che non verrà mai, in nessun modo, fatta viaggiare sulla rete. Quelle che viaggiano, saranno invece le chiavi pubbliche. Facciamo un esempio, allora, per capire come si svolgerà la comunicazione:

Se Gianni e Laura vorranno inviarsi dei messaggi riservati dovranno seguire i seguenti passi:

  1. Entrambi creano una propria chiave pubblica ed una privata (vedremo a breve come farlo in Java).
  2. Laura invia a Gianni la propria chiave pubblica e Gianni fa altrettanto con Laura.
  3. Quando Gianni invia un messaggio a Laura:
    1. Gianni crittografa il messaggio stesso usando la chiave pubblica di Laura
    2. Laura riceve il messaggio crittografato e lo decritta con la propria chiave privata
  4. Quando Laura invia un messaggio a Gianni:
    1. Laura crittografa il messaggio stesso usando la chiave pubblica di Gianni
    2. Gianni riceve il messaggio crittografato e lo decritta con la propria chiave privata.

È il momento di vedere il codice:

Listato 3. Esempio di crittografia a chiave pubblica

import java.security.*;
import javax.crypto.*;

public class PublicKeyExample
{
 public static void main (String[] args) throws Exception
 {
  if (args.length !=1)
  {
   System.err.println("È necessario passare un parametro di
     input");
   System.exit(1);
  }
  byte[] text = args[0].getBytes("UTF8");

  // Crea una coppia di chiavi pubblica/privata
  KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
  keyGen.initialize(1024);
  KeyPair key = keyGen.generateKeyPair();

  // Ottiene un oggetto di tipo Cipher e ne visualizza il provider
  Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
  System.out.println( "n" + cipher.getProvider().getInfo() );

  // Cripta il messaggio contenuto in text con la chiave public key
  // e visualizza il risultato a video
  System.out.println( "nEncryption - Start" );
  cipher.init(Cipher.ENCRYPT_MODE, key.getPublic());
  byte[] cipherText = cipher.doFinal(text);
  System.out.println( "Encryption - end. Stringa risultante: " );
  System.out.println( new String(cipherText, "UTF8") );

  // Decritta il testo cifrato usando la chiave privata
  // Al termine, visualizza il risultato che corrisponderà
  // con la stringa iniziale inserita dall'utente.
  System.out.println( "nDecryption - Start" );
  cipher.init(Cipher.DECRYPT_MODE, key.getPrivate());
  byte[] newText = cipher.doFinal(cipherText);
  System.out.println( "nDecryption - End" );
  System.out.println( new String(newText, "UTF8") );
 }
}

Firma digitale

La prudenza non è mai troppa e così, se nell'esempio precedente il nostro hacker decidesse di fare le veci di Laura, comunicandoci la sua chiave pubblica e facendoci credere che sia quella di Laura ci ritroveremmo a scambiare messaggi con lui senza accorgercene! In altre parole: Chi ci garantisce che sia effettivamente Laura a scambiare messaggi con Gianni? Questo problema si risolve tramite l'ausilio delle firme digitali (digital signatures).

Una firma digitale è uno strumento che ci aiuta a capire che un determinato messaggio proviene da una particolare persona (che lo ha firmato digitalmente). Il concetto è un po' l'opposto di quello visto nel precedente paragrafo, ovvero: il mittente di un messaggio firma il messaggio stesso utilizzando una chiave privata e il destinatario decrittografa quanto ricevuto attraverso la chiave pubblica del mittente. In tal modo, poiché soltanto il mittente è in possesso della chiave privata, il destinatario può essere sicuro che se riesce a decrittografare il messaggio, questo è certamente attribuibile al mittente. Quello che avviene nella pratica è che soltanto il message digest del messaggio viene firmato e non l'intero contenuto dello stesso.

Certificati digitali

Arriviamo così ai famosi certificati digitali. Cosa sono? Arriviamo alla risposta partendo dalle firme digitali. Abbiamo visto che una firma digitale garantisce che un certo messaggio appartenga ad una determinata persona, ma chi garantisce che quella persona è davvero chi dice di essere? C'È bisogno di qualcuno che faccia da garante. Esistono delle organizzazioni che si occupano proprio di questo: le certificate authority (CA).

Una CA garantisce che la paternità di un messaggio (unito ad una firma digitale e una chiave pubblica per decifrarla) è davvero quella dichiarata, utilizzando un ulteriore livello di firma digitale, denominato certificato digitale. Attraverso un certificato digitale, una CA firma con una propria chiave privata le digital signatures (con chiave pubblica) di un individuo (o azienda) facendo poi in modo che chiunque voglia possa decrittarne il contenuto attraverso la chiave pubblica della CA stessa. In tal modo, riproponendo il ragionamento fatto per le firme digitali, si ottiene la garanzia massima che il mittente di un messaggio è effettivamente chi dichiara di essere.

Java offre uno strumento per creare i propri certificati digitali, il keytool. Lo vedremo all'opera quando parleremo di firma di un Applet in uno dei prossimi articoli.

Alla prossima.

Ti consigliamo anche