Sicurezza in Java (III)
----------------------
Ecco la versione IV della guida alla sicurezza in Java, questa volta tratteremo delle impronte di messagio con MD5 e SHA-1 e della crittografia asimmettrica con RSA.
Prima di iniziare definiamo meglio queste due cose.
=========

Impronte di messaggio
----------------------
Un'impronta di messaggio è un algoritmo per la stringa in entrata ne restituisce una univoca per quella data; ad esempio se uso la stringa "ciao" il risultato sarà sempre lo stesso, il risultato non è reversibile(dal risultato non posso tornare al sorgente) e basta che cambi una maiuscola che cambi tutta l'impronta. E' usato essenzialmente per:

- verificare l'integrità dei dati, se il sorgente e/o la sua impronta vengono modificati facendo la verifica risulteranno diversi.
- salvare password, quando inserisco la password salvo il suo hash per non salvarla in chiaro e per non dover usare codifiche per dover salvare dopo altre chiavi, altre password. Solitamente in questo caso si usano anche salt e iterazioni.
=======

Codifica asimmetrica
----------------------
La codifica asimmetrica, o a doppia chiave, è stata una necessità: rendere sicuro lo scambio delle chiavi; se usassimo la cifratura simmetrica la chiave usata per codificare è la stessa per decodificare, quindi dovremmo far girare la chiave in modo sicuro rischiando un "man in the middle" quindi qualcuno che la intercetti. Ecco che usando la cirfatura asimmetrica abbiamo la chiave pubblica, che codifica ma non decodifica, che può essere liberamente passata, e la chiave privata, che decodifica i testi cifrati con la sua relativa privata, che non viene gatta girare, resta al proprietario; ecco un esempio:

Prendo un programma di crittografia che usa RSA, appena lo installo genera la mia chiave pubblica e la mia chiave privata; a questo punto devo mandare un testo riservato ad un'azienda, per codificarlo chiado all'azienda la sua chiave pubblica, la aggiungo al mio programma e codifico con la chiave, del destinatario quindi, il messaggio. Il risultato, incomprensibile, viene mandato all'azienda, che avendo la sua privata potrà decodificarlo.

Il trucco di RSA ve lo spiegherò in un altro articolo, mostrando l'algoritmo, il funzionamento ecc...
==========

Usare le impronte di messaggio (con MD5 e SHA-1) in Java:
------------------------------------------------------------
Per creare un'impronta si usa MessageDigest e i suoi metodi, partiamo subito con un esempio:

------
import java.security.*;
...
/*
Creiamo un istanza di MessageDigest col metodo factory specificando che algoritmo usare
*/
MessageDigest md = MessageDigest.getInstance("MD5");//o SHA-1 al posto di MD5
/*
update aggiunge la stringa di cui fare l'impronta, se lo richiamiamo verra concatenata la seconda alla prima e così via.
*/
md.update("Stringa di cui fare l'impronta".getBytes());

/*
genere l'impronta e viene ritornata in formato di array di byte
*/
byte[] result = md.digest();
...
-----

Semplice no? Le cose si complicano un pò di più se aggiungiamo ad esempio un salt, ovvero un numero casuale di byte che vengono aggiunti al testo(inizio o fine), hashato, e al risultato raggiunto lo stesso hash; per verificare si estraggono tot byte dall'hash, si riaggiungono al testo e se si ottiene quello che avanza del hash è lo stesso; ottimo per evitare brute force o dictionary.

Per chi non avesse capito scrivo un esempio usato anche nalla parte I dell'articolo;-)

-----
Mettiamo caso che un utente inesperto a cui piace tanto andare in bici usi come password "Bicicletta", un dizionario conterrà sicuramente questa parola e quindi l'hash pronto, il salt corrisponde nell'aggiungere dei byte casuali alla fine della password e poi calcolarci l'hash, quindi la password in realtà diventerà: "BiciclettaTÕZÕNTLçðÂE?", il salt poi verrà riaggiungo al hash risultante: "TÕZÕNTLçðÂE?hash..." il programmatore saprà che il salt corrisponde a 8 byte, che sono stati messi all'inizio e che durante la codifica li ha aggiungi alla fine della password e non all'inizio o al centro. Questo renderà molto più difficle e lunga una forzatura del cifrato.
-----

Ecco due metodi semplici semplici uno per calcolare un hash con salt e uno per verificarlo:

------
// crea hash
public void createHash() throws Exception {
                byte[] salt = new byte[12];
                SecureRandom sr = new SecureRandom();
                sr.nextBytes(salt);
                MessageDigest md = MessageDigest.getInstance("MD5");
                md.update(salt);
                md.update(passw.getBytes("UTF8"));
                byte[] digest = md.digest();
                FileOutputStream fos = new FileOutputStream(store);
                fos.write(salt);
                fos.write(digest);
                fos.close();
            }

// verifica
public void isCorrect() throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        FileInputStream fis = new FileInputStream(store);
        int thebyte = 0;

        while((thebyte = fis.read()) != -1) {
            baos.write(thebyte);    
        }
            fis.close();
            byte[] hashstore = baos.toByteArray();
            byte salt[] = new byte[12];
            System.arraycopy(hashstore, 0, salt, 0, 12);
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(salt);
            md.update(passw.getBytes("UTF8"));
            byte[] digest = md.digest();
            byte[] hashfile = new byte[hashstore.length-12];
            System.arraycopy(hashstore, 12, hashfile, 0, hashstore.length-12);
            return Arrays.equals(digest, hashfile);
------
===========

Usare la cifratura asimmetrica (con RSA) in Java
----------------------
Per usare RSA bisogna fare essenzialmente questi passi:

Generare chiavi
- creare una coppia di chiavi
- salvare la chiave privata in formato PKCS#8
- salvare la pubblica in formato X.509

Codifica
- leggere chiave pubblica
- codificare con la pubblica

Decodifica
- leggere la privata
- decodificare con la privata

faremo un metodo per ogni cosa da fare creando alla fine un programma completo.
Il programma in questione userà come algoritmo RSA con chiavi da 1024 bit come modalità la ECB e come padding il PKCS1; il fatto di usare RSA a 1024 bit porta a un limite: si può cifrare solo un blocco da 117 byte(117 caratteri), per cifrare più dati si usa una chiave di sessione, ovvero si crittano i dati simmetricamente e asimmetri la chiave usata prima, ma per ora vediamo per ora una codifica asimmetrica normale:

-------------------------------
import java.security.*;
import java.security.spec.*;

import javax.crypto.*;

import java.io.*;

public class RSACoder {
    
    private static void codifica() throws Exception {
        
        // LEGGI CHIAVE PUBBLICA CODIFICATA IN X509
        
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        System.out.print("Inserire keystore della chiave pubblica: ");
        String publicKeystore = br.readLine();
        FileInputStream fis = new FileInputStream(publicKeystore);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int i = 0;
        while((i = fis.read()) != -1) {
            baos.write(i);
        }
        fis.close();
        byte[] publicKeyBytes = baos.toByteArray();
        baos.close();
        
        // CONVERTI CHIAVE PUBBLICA DA X509 A CHIAVE UTILIZZABILE
        
        // Inizializza convertitore da X.509 a chiave pubblica
        X509EncodedKeySpec ks = new X509EncodedKeySpec(publicKeyBytes);
        // Inizializza un KeyFactory per ricreare la chiave usando RSA
        KeyFactory kf = KeyFactory.getInstance("RSA");
        // Crea una chiave pubblica usando generatePublic di KeyFactory in base la chiave decodificata da ks
        PublicKey publicKey = kf.generatePublic(ks);
        
        // LEGGI FILE SORGENTE
        
        System.out.print("\nInserire path file da codificare, tra " se contiene spazi: ");
        String sorgente = br.readLine();
        fis = new FileInputStream(sorgente);
        baos.reset();
        byte[] plainFile;
        i = 0;
        while((i = fis.read()) != -1) {
            baos.write(i);
        }
        fis.close();
        plainFile = baos.toByteArray();
        
        // CODIFICA FILE SORGENTE
        System.out.print("\nInizio codifica");
        // Inizializzo un cifrario che usa come algoritmo RSA, come modalità ECB e come padding PKCS1
        Cipher c = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        System.out.print(".");
        // Lo inizializzo dicendo modalità di codifica e chiave pubblica da usare
        c.init(Cipher.ENCRYPT_MODE, publicKey);
        System.out.print(".");
        // codifico e metto il risultato in encodeFile
        byte[] encodeFile = c.doFinal(plainFile);
        System.out.println(". Codifica terminata!");
        
        // SALVA FILE CODIFICATO
        
        System.out.print("\nInserire path del file in cui salvare la codifica, tra " se contiene spazi: ");
        String dest = br.readLine();
        FileOutputStream fos = new FileOutputStream(dest);
        fos.write(encodeFile);
        fos.close();
    }
    
    private static void decodifica() throws Exception {

        // LEGGI CHIAVE PRIVATA CODIFICATA IN PKCS#8
        
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        System.out.print("\nInserire keystore chiave provata: ");
        String privateKeystore = br.readLine();
        FileInputStream fis = new FileInputStream(privateKeystore);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int i = 0;
        while((i = fis.read()) != -1) {
            baos.write(i);
        }
        fis.close();
        byte[] privateKeyBytes = baos.toByteArray();
        baos.close();
        
        // CONVERTI CHIAVE PRIVATA PKCS8 IN CHIAVE NORMALE
        
        PKCS8EncodedKeySpec ks = new PKCS8EncodedKeySpec(privateKeyBytes);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = kf.generatePrivate(ks);
        
        // LEGGI FILE CODIFICATO
        
        System.out.print("\nInserire path file da codificare, tra " se contiene spazi: ");
        String sorgente = br.readLine();
        fis = new FileInputStream(sorgente);
        baos.reset();
        byte[] codFile;
        i = 0;
        while((i = fis.read()) != -1) {
            baos.write(i);
        }
        fis.close();
        codFile = baos.toByteArray();
        
        // DECODIFICA
        
        System.out.print("\nInizio decodifica");
        Cipher c = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        System.out.print(".");
        c.init(Cipher.DECRYPT_MODE, privateKey);
        System.out.print(".");
        byte[] plainFile = c.doFinal(codFile);
        System.out.println(". Decodifica terminata!");
        
        // SALVA FILE
        
        System.out.print("\nInserire path del file in cui salvare la decodifica, tra " se contiene spazi: ");
        String dest = br.readLine();
        FileOutputStream fos = new FileOutputStream(dest);
        fos.write(plainFile);
        fos.close();
    }
    
    public static void main(String args[]) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int scelta = 0;
        String option;
        do {
            System.out.println("\n\nInserire opzione: ");
            System.out.println("1. crea coppia di chiavi");
            System.out.println("2. codifica");
            System.out.println("3. decodifica");
            System.out.println("4. esci\n");
            option = br.readLine();
            try {
                scelta = Integer.parseInt(option);
            } catch(NumberFormatException nfe) {
                System.out.println("Inserire numero intero");
                continue;
            }
            switch(scelta) {
            case 1:
                createKeys();
                break;
                case 2:
                    codifica();
                    break;
                    case 3:
                        decodifica();
                        break;
                        case 4:
                            System.exit(0);
                            break;
                        default:
                            System.out.println("Le opzioni sono 1, 2, 3 o 4");
            }
        } while(!(scelta >= 1 && scelta <= 4));
    }
    
    private static void createKeys() throws Exception {
        
        // GENERA COPPIA DI CHIAVI
        
        //Un reader per leggere la console
        BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
        System.out.print("Inizio generazione chiavi RSA");
        //inizializza un generatore di coppie di chiavi usando RSA
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
        System.out.print(".");
        // le chiavi sono molto lunghe: 1024 bit sono 128 byte.
        // La forza di RSA è nell'impossibilità pratica di fattorizzare
        // numeri così grandi.
        kpg.initialize(1024);
        System.out.print(".");
        // genera la coppia
        KeyPair kp = kpg.generateKeyPair();
        System.out.print(". Chiavi generate!\n");
        
        // SALVA CHIAVE PUBBLICA
        
        System.out.print("Inserire keystore per la chiave pubblica: ");
        String publicKeystore = input.readLine();
        // ottieni la versione codificata in X.509 della chiave pubblica
        // (senza cifrare)
        byte[] publicBytes = kp.getPublic().getEncoded();
        // salva nel keystore selezionato dall'utente
        FileOutputStream fos = new FileOutputStream(publicKeystore);
        fos.write(publicBytes);
        fos.close();
        
        // SALVA CHIAVE PRIVATA
        
            System.out.print("Inserire keystore per la chiave privata: ");
        String privateKeystore = input.readLine();
        // ottieni la versione codificata in PKCS#8
        byte[] privateBytes = kp.getPrivate().getEncoded();
        
        fos = new FileOutputStream(privateKeystore);
        fos.write(privateBytes);
        fos.close();
    }
}
----------------------------

Questo programma di esempio lo trovate anche nella sezione programmi Java già compilato e funzionante.

Per domande disponibile sul forum o via email