SICUREZZA IN JAVA - PARTE PRIMA


SICUREZZA A LIVELLO DI LINGUAGGIO
------------------------------------------------------------
=================================

Modificatori d'accesso
----------------------

In Java, ma anche in C++,  i modificatori d'accesso come private, public e protected sono controllati solo a compile-time,  questo può dare la possibilità ad una classe malevola di accedere a campi che inizialmente dovrebbero essere non visibili ed accessibili solo alla classe che li incapsula e eventualmente renderli visibili o permetterne la modifica tramite metodi appositi in modo che vengano letti/modificati sotto il controllo della classe.

Un esempio di questa "debolezza", che è Java è risolta come vedremo poi, e il seguente codice:


public class Vittima {
private String super_var_private;

public Vittima() {
super_var_private = "variabile che NON deve essere acessibile da altre classi";
}

}


Ed ora abbiamo la classe malevola:


public class Malevola {

public static void main(String args[])) {
Vittima v = new Vittima();
System.out.println(v.super_var_private);
}

}


Compiliao separatamente i due files

javac Vittima

javac Malevola

Eseguiamo con

java Malevolo

E, nonostante super_var_private sia privata, vedremo il suo valore e volendo potremmo pure modificarlo.
Come detto prima anche altri linguaggi come C++ hanno questo problema(in questo linguaggio non mi sono documentato ne su come sfruttarlo ne su come risolverlo).

Tornando a Java, c'è la possibilità di attivare il verify del bytecode che fa questi ed altri controlli a run-time, per attivarlo bsta usare il setuente comando:

java -verify Malevolo

Ed ecco che viene lanciato l'errore (attenzione, l'errore non l'eccezione)
IllegalAccessError.


Erase sicuro di dati
--------------------

Quando in Java viene allocata memoria, avviene tutto automaticamente e il momento di de-allocarla non è sempre il programmatore a deciderlo(si potrebbe utilizzare System.gc() ma non è detto che la Garabage Collection ritenga certi oggetti inutili anche se in realtà lo sono).
Vi faccio un esempio:


public class Decodificatore {
private byte[] chiave_privata;
//[...]
}


Dentro chiave_privata c'è la chiave per decodificare un messaggio.
Quando siamo usciti dal programma e quindi dalla JVM si potrebbe benissimo fare un programma in C che alloca molta memoria e ne mostra il contenuto, dal quale si può risalire alla chiave vista sopra.

Per risolvere la question in Java e consigliabile azzerare il contenuto delle variabili prima di uscire, ecco un esempio


public class Erase {
private byte[] chiave;

//[...]

public void clear() {
for(int i = 0; i < chiave.length; i++)
chiave[i] = (byte) 0x00;
}

//[...]

}


Magari dentro chiave potrebbe esserci anche il numero di carta di credito, anche se lo si memorizza codificato e buona norma azzerare i byte come visto sopra.


CRITTOGRAFIA IN JAVA
-------------------------------------
=====================

Requisiti e installazione

In questa guida verranno usati i provider crittografici JCE di sun e della bouncycastle, il primo fornito di default il secondo reperibile all'url http://www.bouncycastle.org

Una volta scaricato il zip estraete ed entrate in src, vedrete delle cartelle javax e org, compilate tutto ed eliminate i .java(occupano spazio per niente)creare il jar col seguente comando:

jar cvf bouncycastle.jar javax org

E copiarlo in JAVA_HOME/jre/lib/ext

dove JAVA_HOME sarà qualcosa tipo jdk_1.4.2_05 o circa, di default posizionato in C: in windows, mentre in *nix in /usr/local/jdk1.4 o qualcosa di simile, ma credo che sappiate dov'è il vostro Java Development Kit :-)

Fatto questo bisogna fare in modo che il provider nuovo appena installato sia riconosciuto dal file java.security, quindi aggiungiamo la seguente riga a questo file che si trova in JAVA_HOME/jre/lib/security:

security.provider.3=org.bouncycastle.jce.provider.BouncyCastleProvider

e lo mettiamo subito dopo la dichiarazione dei provider di Sun(ovviamente già presente)

# questo lo trovate già

security.provider.1=sun.security.provider.Sun
security.provider.2=com.sun.rsajca.Provider

# voi aggiungete questo:

security.provider.3=org.bouncycastle.jce.provider.BouncyCastleProvider

Se avete altri privider alternativi installati quindi:

security.provider.1... ecc
security.provider.2... ecc
security.provider.3... ecc
security.provider.4... ecc

la riga da aggiungere sarà la stessa, ma al posto di 3 avrà 5.... per i più dummies:

security.provider.5=org.bouncycastle.jce.provider.BouncyCastleProvider

ATTENZIONE:
Se avete problemi tipo errori di algoritmo non supportato fate le seguenti cose:
- Eliminare il JCE di Sun(1.2.1 o sup blocca altri provider), lo riconoscete visto che ha il nome jce.jar o sunjce o jce_1..1.jar2 o local_policy. Non deve trovarsi in lib/ext
- Nel file java.security commentate con # tutti i provider escluso il bouncycastle che deve essere impostato a 1(security.provider.1 ecc...)
- In windows fare le modifiche sia nel JDK che nel JRE se sono separati
Controllare di aver scaricato la "JCE with provider and lightweight API"adatta al proprio jdk.
- Verificare la presenza di più file.security usati dalla jvm e configurarli corretamente.

CIFRATURA SIMMETRICA
----------------------------------------
======================

La cifratura simmetrica è quella che ha la stessa chiave per codificare è la stessa per decodificare, il problema è che la chiave dovrà essere detta prima, quindi può verificarsi un problema definito "man in the midle", ovvero qualcuno che intercetta la chiave.
I pregi sono che è molto veloce nel processo di codifica e di decodifica, quindi si può utilizzare per codificare una grande quantità di dati. Spesso si usa il sistema misto: il testo codificato simmetricamente e la chiave asimetricamente(detto chiave di sessione ma vedremo più avanti). Un'ultima cosa da sapere è che le chiavi simmetriche per essere sicure devono avere una grandezza minima di 128 bit.

Adesso passiamo alla parte pratica in java, dopo l'installazione del JCE della bouncycastle avremo disponibile il package javax.crypto, adesso useremo la classe Cipher per creare un cifrario al quale passeremo anche un oggetto key che contiene una chiave adatta all'algoritmo usato.
Per curiosità, in javax.cipher raramente gli oggetti vengono allocati con new, ma vengono utilizzati i metodi factory.

La classe Cipher quindi verà allocata nel seguente modo:


Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Pdding");


DESede è l'algoritmo di codifica, ECB sta per Eletronic Code Book ed è la modalità del cifrario, PKCS5Pdding è il padding, ma gli ultimi due argomenti li vedremo più avanti.

La seconda fase da fare sarà inizializzare il cifrario con init() e passeremo come argomenti la modalità(codifica o decodifica) e la chiave da usare, la chiave la generaimo nel seguente modo:


KeyGenerator kg = KeyGenerator.getInstance("DESede");//anche qui metodo factory
kg.init(168);//DESede, conosciuto anche come TripleDes, utilizza chiavi da 168 bit
Key oggettoKey = kg.generateKey();//chiave generata


Quindi inizializziamo:


cipher.init(Cipher.ENCRYPT_MODE, oggettoKey);



La prossima fase, facoltativa, sarà aggiornare il cifrario, ovvero, usando il metodo update, diremo quali byte codificare, nel caso l'imput non è sufficientemente grande, verrà ritornato null:


/*
specificare sempre la codifica, se codifichiamo con una e decodifichiamo con un'altra
potremmo avere problemi con i risultati
*/

byte[] byteDaCodificare = stringaCodificare.getBytes("UTF8");//codifica UTF8
byte[] codificati = cipher.update(byteDaCodificare);


Adesso avremo la possibilità di ottenere veramente un array di byte codificati col metodo doFinal():


byte[] codificati = cipher.doFinal();
//se si salta la fase update, si utilizzerà doFinel(byte byte_da_codificare);


Perfetto, se qualcosa non è chiaro, per chiarire il tutto scrivo un programma completo, a console, che codifica una stringa specificata a riga di comando:


Esempio completo 1:
----------------------
import javax.crypto.*;
import java.security.*;

/**
* @author Netarrow
* Programma d'esempio per l'articolo Sicurezza in Java
*/
public class Esempio1 {
    /**
     * @param args Stringa da codificare. Se non c'è, di default si usa TESTO SEGRETO, per passare una frase metterla fra firgolette " "
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        String chiaro = "";
        if(args.length != 1) {
System.out.println("Parametri errati, codifico "TESTO SEGRETO"");
            chiaro = "TESTO SEGRETO";
        } else {
            chiaro = args[0];
        System.out.println("Codifico " + chiaro + "n");
        }
    Cipher c = Cipher.getInstance("DESede/ECB/PKCS5Padding");
        KeyGenerator kg = KeyGenerator.getInstance("DESede");
        kg.init(168);
        System.out.print("Sto generando la chiave...");
        Key key = kg.generateKey();
        System.out.println(" fatto");
        c.init(Cipher.ENCRYPT_MODE, key);
        System.out.print("Sto codificando... ");
    byte[] codificati = c.doFinal(chiaro.getBytes("UTF8"));
        System.out.println("fatto");
        System.out.println(chiaro + " codificato sarà:n");
        String cod = new String(codificati, "UTF8");
        System.out.println(cod);
    System.out.println("nDecodificato " + cod + " diventa:");
        c.init(Cipher.DECRYPT_MODE, key);
        byte[] decod = c.doFinal(codificati);
        String dec = new String(decod, "UTF8");
        System.out.println(dec);
    }
}
----------------------------------------------------------------------------
                                                                                                        

Se volessimo adattare l'esempio sopra per utilizzare l'algoritmo Blowfish al posto del DESede(conoscuto anche come TripleDes, ricordate) basterà apportare poche modifiche, innanzitutto nel parametro del metodo factory useremo la stringa

"Blowfish/ECB/PKCS5Padding"

al posto della

"DESede/ECB/PKCS5Padding"

Poi nel metodo factory del KeyGenerator modificheremo "DESede" con "Blowfish" e nel metodo init(), sempre di KeyGenerator(istanzioto col nome kg nell'esempio), utilizzeremo chiavi a 128 bit visto che questo algoritmo ha la key-size diversa.


PBE: PASSWORD BASED ENCRYPTION
----------------------------------------------------
==============================

Nella parte precedente le chiavi erano binarie lunghe 128 e 168 bit, ovvero 16 byte paragonabili a 16 caratteri e 168 paragonabili a 21 caratteri. Tenendo conto che convertendo i byte della chiave in caratteri ASCI(ASCI standar ha byte da 7 bit) sarebbe una frase lunga, senza senso e con simboli di ogni genere ed inoltre, senza ricorrere alla codifica Base64, 1 bit per ogni carattere verrebbe perso portando qualche incompatibilità, ma comunque anche se usassimo base64 per ottenere una stringa ASCI sarebbe scomodo saperla a memoria, però usare chiavi di questo genere da la possibilità di generare 2^128 chiavi diverse con dimensioni da 128 bit, la pecca è che queste keys vengono memorizzate su dischi fissi in file detti keystore, o in floppy, cd e altri oggetti che possono essere rubati; questo è insicuro.
E' per questo che viene utilizzata anche la cifratura basata su password, dove la parola segreta sarà ricordata a memoria solo dall'utente, in un luogo sicuro come la mente. La pecca è che in media gli utenti scelgono password da 6 caratteri, se si usano solo caratteri minuscoli andiamo a 2^28, se aggiungiamo simboli e maiuscole migliora, ma sarà sempre inferiore al livello di sicurezza del keyspace di algoritmi simmetrici come quelli visti fono ad ora.

Passiamo alla parte pratica, la PDE funziona nel seguente modo:

Codifica

* Viene inserita una password
* Alla password viene calcolata la sua impronta(detta hash)
* Sulla base dell'impronta verrà generata una chiave per un algoritmo simmetrico
* Verrà creato un cifrario
* Col cifrario verrà codificato il testo in chiaro


Decodifica

* Viene inserita una password
* Se l'hash corrisponde a quello inserito durante la codifica, è ok
* Se è giusto quindi verrà ricreata la chiave e decodificato
* Sennò non si decodifica


Una pecca di sicurezza sarà il fatto che per ogni hash corrisponderà una chiave e visto che l'hash è univoco per ogni password(si vedrà più avanti l'impronta di messaggio con MD5 e SHA-1) si può fare una sorta di dizionario di chiavi; per risolvere il problema si utilizzerà il salt e il conteggio di ripetizioni.

Salt:
-----
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.

Conteggio di ripetizioni:
-------------------------
Come se non fosse già risolto il problema, il conteggio di ripetizioni consiste nel ricalcolare più volte l'hash, se impostiamo 1000 ripetizioni, verrà calcolato l'hash della password, l'hash dell'hash, l'hash dell'hash dell'hash e ogni risultato verrà calcolato fino a 1000 volte... bel casino tornare indietro a tentativi eh? :-)))


Classi per il PBE con crittografia simmetrica
---------------------------------------------

Ecco le classi che si usano per implementare la cifratura basata su password:

PBEKeySpec:
-----------
E' il primo passo per generare una chiave, ecco un esemio di codice

char[] password = "password".toCharArray();
PBEKeySpec keySpec = new PBEKeySpec(password);

Si usa un array di caratteri perchè un oggeto String è specificato come final, quindi è immodificabile ed è più probabile che rimanga in memoria e possa essere ripescato da altri programmi risalendo alla passworrd.

SecretKeyFactory e SecretKey:
-----------------------------
E' il secondo passo per generare la chiave:

SecretKeyFactory kf = SecretKeyFactory.getInstance("PBEWithSHAAndTwofish-CBC");
SecretKey key = kf.generateSecret(keySpec);//si passa l'istanza di PBEKeySpec visto sopra.

In key avremo incapsulata la chiave basata sulla password "password" specificata prima al PBEKeySpec.

PBEParameterSpec e il solito Cipher
-----------------------------------
A PBEParameterSpec vengono passati il salt(vedi il paragrafo "salt") e le iterazioni(vedi il paragrafo "conteggio di ripetizioni"), una volta fatto questo ci crea il solito oggetto Cipher, gli si passa l'algoritmo da usare(ora stiamo usando PBEWithSHAAndTwofish-CBC), la modalità, la chiave e il PBEParameterSpec, vediamo un esempio chiarificatore:

int conteggio_ripetizioni = 1000;
char[] salt = new char[8];
Random r = new Random();//random presente in java.util
r.nextBytes(salt);

PBEParameterSpec ps = new PBEParameterSpec(salt, conteggio_ripetizioni);
Cipher c = Cipher.getInstance("PBEWithSHAAndTwofish-CBC");
c.init(Cipher.ENCRYPT_MODE, key, ps);
//key è la chiave di prima generata con SecretKey basato su PBEKeySpec ecc.. ecc...

Fatto questo il cifrario è pronto e si possono codificare messaggi.
Piccola nota sull'algoritmo PBEWithSHAAndTwofish-CBC, questo vuol dire che i è usata una cifratura basata su password usando SHA per fare l'impronta di messaggio e Twofish in modalità CBC(alternativa a ECB visto prima) per codificare il testo. Ad ogni modo, per altri algoritmi disponibili rimando alla documentazione del bouncycastle, l'unica cosa su cui informarvi è che mi è stato detto che l'algoritmo disponibile "PBEWithMD5AndDes" è meno sicuro degli altri.

Ora si passa ad un programma completo, così chiariremo i dubbi:

Esempio completo 2:
-----------------------------------------------------------------

import javax.crypto.*;
import javax.crypto.spec.*;

import sun.misc.*;//serve per BASE64, dopo spiego meglio cosa serve e cos' è

import java.util.*;

/**
* @author Netarrow
* Programma d'esempio per l'articolo Sicurezza in Java
*/
public class Esempio2 {
    private static int ripetizioni = 1000;
    private static byte[] salt = new byte[8];
    private static String password;
    
    public static void utilizzo() {
    System.out.println("Uso: java modalità | testo | passwordn");
    System.out.println("modalità: -c codifican          -d decodifica");
    System.exit(1);
    }
    
    public static void main(String[] args) throws Exception {
        if(args.length != 3) {
            utilizzo();
        }
        Random r = new Random();
        r.nextBytes(salt);
        String in_text = args[1];
        password = args[2];
        String out_text = null;
    if(args[0].compareToIgnoreCase("-c") == 0)
out_text = codifica(in_text);
else if(args[0].compareToIgnoreCase("-d") == 0)
out_text = decodifica(in_text);
        else {
    System.out.println("Modalità " + args[0] + " inesistenten");
            utilizzo();
            System.exit(1);
        }
System.out.println(out_text);
}
    
    public static String codifica(String text) throws Exception {
        //genera la chiave basata sulla password
    PBEKeySpec ks = new PBEKeySpec(password.toCharArray());
SecretKeyFactory kf = SecretKeyFactory.getInstance("PBEWithSHAAndTwofish-CBC");
        SecretKey key = kf.generateSecret(ks);
        
//complica la vita ai cattivi aggiungendo i famigerati salt e conteggi :-)
PBEParameterSpec ps = new PBEParameterSpec(salt, ripetizioni);
        
        //crea il cifrario e fa la codifica
Cipher c = Cipher.getInstance("PBEWithSHAAndTwofish-CBC");
        c.init(Cipher.ENCRYPT_MODE, key, ps);
        byte[] codificato = c.doFinal(text.getBytes("UTF8"));
        
//BASE64 serve per convertire i caratteri codificati in standar ASCI, che ha
//un bit in meno
        BASE64Encoder enc = new BASE64Encoder();
        String salt_str = enc.encode(salt);
        String cod_str = enc.encode(codificato);
        
        
return "Il testo "" + text + "" codificato diventan" + salt_str + cod_str;
    }        
    
    public static String decodifica(String text) throws Exception {
        String salt_getted = text.substring(0, 12);
        String text_cod = text.substring(12, text.length());
        
        //riconverte i bytes da BASE64 a bytes standar da 8 bit
        BASE64Decoder bd = new BASE64Decoder();
        byte[] salt_bytes = bd.decodeBuffer(salt_getted);
        byte[] cod_bytes = bd.decodeBuffer(text_cod);
        
        //ricrea la password
        PBEKeySpec ks = new PBEKeySpec(password.toCharArray());
SecretKeyFactory kf = SecretKeyFactory.getInstance("PBEWithSHAAndTwofish-CBC");
        SecretKey key = kf.generateSecret(ks);
        
    //dopo aver ripescato il salt ricrea i complicatori di forzatura
PBEParameterSpec ps = new PBEParameterSpec(salt_bytes, ripetizioni);
        
        //ricrea il cifrario e fa decodifica
Cipher c = Cipher.getInstance("PBEWithSHAAndTwofish-CBC");
        c.init(Cipher.DECRYPT_MODE, key, ps);
        byte[] decodificato = c.doFinal(cod_bytes);
        String decod = new String(decodificato);
        
        
        
    return "Il testo "" + text + "" decodificato diventan" + decod;
    }
    
    }
---------------------------------------------------------

Spiego meglio BASE64: se mostriamo i caratteri di byte normali vengono fuori simboli sballati dato che c'è un bit in meno disponibile in ASCI, BASE64 rimedia proprio a questa differenza trasformando caratteri assurdi in caratteri asci più "chiari" anche se comunque non hanno alcun senso messi assieme.

Approfondiamo la modalità:

ECB: In pratica se dobbiamo codificare "ciao a tutti" ogni blocco e viene fuori un blocco "utti" verra modifica sempre con lo stesso codice cifrato, un' un'pò come se ogni letterà corrisponde a un insieme di simboli.

CBC: l'opposto dell'altra, ogni blocco avrà un sistema di codifica diverso

CFB: è un CBC specializzato per codificare piccoli blocchi, utilissomo per usarlo ad esempio per rendere sicure delle chat dove vengono inviate stringhe corte solitamente.

OFB: Una versione di CFB che salvaguarda la perdita dei dati, se durante una chat si perde un bit, ancge il testo in chiaro perderà solo un bit. Con le altre modaòità va perso tutto il blocco.

FINE PRIMA PARTE

Qui finisce la prima parte, quando ne farò un'eventuale seconda verranno trattate più approfonditamente le modalità viste sopra, la tecniche per salvare le chiavi, il padding e quindi termineremo la crittografia simmetrica.
Molto probabilmente, inizierò già a parlare anche di quella asimmetrica, di key agreement e accennerò le impronte di messaggio e le firme, ma vedremo come organizzare intanto studiate questo.

Per problemi disponibile sul forum e via e-mail