MokaByte 99 - 7mbre 2005
 
MokaByte 99 - 7mbre 2005 Prima pagina Cerca Home Page

 

 

 

Autenticazione password crittografata

La realizzazione di un sistema di autenticazione basato su password crittografata. L'utilizzo del salt e delle piu importanti funzionalità di crittografia del jdk

Introduzione
In questo articolo spiegherò come realizzare un sistema di autenticazione utilizzando le classi MessageDigest e SecureRandom per implementare un meccanismo di autenticazione basato sul Hashing delle password.

 

L'autenticazione in breve
Il sistema che andrò ad illustrare è in larga parte utilizzato e simile in sistemi UNIX/LINUX . La password verrà crittografata per mezzo di un algoritmo unidirezionale ovvero sarà possibile criptare la password ma non decriptarla (quindi tornare indietro al testo in chiaro) e salvata sul database. In primo luogo:

  • Si registrano i dati dell'utente su db password crittografata compresa.
  • Al momento della login sistema il prende la password (testo in chiaro), che ha inviato l'utente ed esegue la crittografia ottenendo un hash.
  • Si confronta il contenuto dell'hash con quello relativo all'account-utente presente sul db. Se corrispondono l'utente accede al sistema. Per crittografare la password abbiamo bisogno di una chiave che non dovrebbe essere statica ma cambiare spesso in modo che il valore dell'hash cambi ad ogni passaggio.


Il salt
Il salt è un array di byte generato in modo casuale che occorre per criptare la password in chiaro e di conseguenza ottenere l,'hash. La concatenazione dall'hash + salt andrà a formare la password crittografata che verrà salvata sul db. La password sarà composta pressappoco come nella figura 1.


Figura 1 - array della password criptata

Naturalmente il contenuto crittografato nella norma è più lungo ma questo vuole suolo essere un esempio. Il contenuto della figura 1 è composto quindi da:

  • un prima parte denominata hash che è sostanzialmente la password in chiaro crittografata, ovvero il risultato dell'agoritmo di crittografia.
  • una seconda (salt) che contiene la chiave utilizzata per crittografare la password in ingresso ed ottenere l'hash.

Importante: Deve essere nota la lunghezza del salt che nell'esempio è composto da soli 9 byte. Solitamente il salt ha una lunghezza sempre fissa per poter essere estratto dall'intero array. Nel nostro caso la lunghezza del salt sarà di 5 + la lunghezza della password in chiaro.
Quando l'utente prova ad effettuare la login invia username e password: Il sistema effettua i seguenti passaggi:

  • Cerca l'utente sul db per mezzo dello username.
  • Estrae il record contenente informazioni dell'account dell'utente ma utilizzerà per il momento solo la password crittografata presente(hash + salt)
  • Estrae la chiave(salt): Per estrarre il salt bisogna ottenerne la lunghezza. Si somma a 5 la lunghezza della password in chiaro inviata al momento della login dall'utente. Il numero ottenuto sono gli ultimi n byte del campo. Nello schema la password è di soli 4 caratteri perchè se sommiamo a 5 otteniamo un salt di 9 byte. Contando dall'ultima posizione(20) arriviamo a 12. Da qui comincia il salt. Naturalmente una password di 4 caratteri è tutt'altro che sicura ma questo vuole solo essere un esempio. Quindi
  • Si estrae l'hash.
  • Prova a crittografare la password inserita dall'utente ovvero viene generato un nuovo hash a partire dalla password in chiaro utilizzando il salt estratto in precedenza.
  • Controlla il nuovo hash generato se corrisponde a quello estratto dal db.

Se l'autenticazione ha avuto esito positivo conosciamo il testo in chiaro della password inserita al momento della login. Ci comportiamo di conseguenza:

  • Generiamo un nuovo salt.
  • Prendiamo la password che ha inserito l'utente (sappiamo che è quella giusta e che è quella in chiaro) e generiamo un nuovo hash utilizzando come chiave il nuovo salt generato.
  • Concateniamo hash + salt ed otteniamo una nuova password crittografata. Aggiorniamo il campo sul db con questo valore.
    Secondo questo sistema sappiamo che:
  • la password in chiaro non viene salvata mai da nessuna parte, ma utilizzata solo in ingresso al momento della login.
  • il contenuto crittografato sul db della password cambia ad ogni login perché utilizziamo una chiave che cambia ad ogni autenticazione.

La registrazione dell'utente
Questa fase non meno importante in quanto permette, assieme alla creazione dell'utente implica la generazione della password crittografata. In primo luogo quando utente vuole registrarsi gli viene richiesto di inserire i suoi dati anagrafici. Tra l'altro l'inserimento dellla password è previsto due volte. Il procedimento è simile come nell'autenticazione: Si prendono le due password inserite, si genera uno solo salt per tutte e due. Dalle due password in chiaro applicando il salt si ottengono due hash. Se sono identici si concatena hash e salt per ottenere la password crittografa. Si crea l'account sul db con tutti i dati.

 

Passiamo al codice
Non mi dilungherò molto nell'implementazione della funzionalità in struts, quanto per classi dedicate alla funzionalità di autenticazione.

LoginAction
Questa action di struts non fa altro che delegare ad un oggetto di tipo AuthenticationModule la funzionalità di autenticazione passando i parametri di accesso dell'utente. Il compito della login action è preparare i parametri di ingresso e gestire il risultato dell'autenticazione per effettuare il forward delle varie pagine.

AuthenticationModule
Questa classe viene utilizzata al momento della login: Il costruttore accetta due parametri:
public AuthenticationModule(String username,String password)
Che sono in pratica lo username e la password in chiaro provenienti dalla request. Il metodo.

public UserBean login() throws Exception;

restituisce un oggetto UserBean se l'autenticazione va a buon fine oppure rilascia un eccezione che può essere.

  • Un eccezione AuthenticationException.
  • Un eccezione generica java.lang.Exception.
    Se abbiamo un eccezione del tipo AuthenticationException vuol dire che l'autenticazione ha avuto esito negativo: Si verifica nel caso l'utente non sia presente sul db oppure semplicemente l'utente è presente ma la password è errata. E' comunque possibile gestire i due casi tramite un codice di errore.

AuthenticationUtil: Creazione della password
Questa classe viene utilizzata all'interno della login ed è una classe di supporto. Ha due metodi il primo è:
public static byte[] createPassword(byte password[],byte oldPassword[])
Questo metodo genera una nuova password crittografata a partire da un testo. Il secondo parametro rappresenta la password registrata attualmente sul db. Questo secondo parametro servirà per generare un nuovo salt in con la massima causalità. Anche nella fase di creazione di un utente viene utilizzato questo metodo ma è ovvio che per la creazione della prima password non si gestisce il secondo parametro.
Per prima otteniamo un istanza di messagedigest.

MessageDigest sha = MessageDigest.getInstance(ALGORITHMS_DIGEST);
SecureRandom random = null;

Adesso controlliamo se ci è stata passata la vecchia password. Se non arriva generiamo direttamente il salt.

if (oldPassword==null) random = SecureRandom.getInstance(ALGORITHMS_RANDOM);

Se ci viene passata la vecchia password. Utilizziamo questa (come seme) per generare il nuovo salt.

if (oldPassword!=null) random = new SecureRandom(oldPassword);
byte salt[] = random.generateSeed(SALT_MIN_LEN + password.length);
sha.reset();

A questo punto applico l'algoritmo alla password in chiaro assieme al salt per ottenere l'hash.

sha.update(password);
sha.update(salt);
byte encr[] = sha.digest(salt);

Creo un nuovo array con dimensioni pari a contenere l'hash ed il salt che ho creato.

result = new byte[encr.length + salt.length];

Quindi copio prima l'hash all'inizio del nuovo array.

System.arraycopy(encr, 0, result, 0, encr.length);

Infine copio il salt in coda sempre nello stesso array.

System.arraycopy(salt, 0, result, encr.length, salt.length);
return result;
AuthenticationUtil: Verifica della password
public static boolean verify(byte password[],byte dbpassword[])

Il metodo verify, come suggerisce il nome, serve per confrontare un testo in chiaro con uno crittografato. Se crittografando il primo parametro (password in ingresso) ottengo un hash con contenuto identico al secondo parametro il metodo ha esito positivo. Come vedremo piu in dettaglio per criptare la password la funzione utilizzerà il salt che è parte del contenuto del parametro dbpassword.
Per prima cosa inizializzo il digest,

sha = MessageDigest.getInstance(ALGORITHMS_DIGEST);
sha.reset();

Adesso calcolo la posizione di inizio del salt nella password crittografata SALT_MIN_LEN è una costante intera che impostata 5. Sommo a questo la lunghezza della password in chiaro. Infine sottraggo alla lunghezza della password crittografata il totale ed ottengo la posizione del salt nell'array.

int startAt = dbpassword.length - (SALT_MIN_LEN + password.length);
if (startAt>0){

Istanzio l'array che dovrà contenere solo il salt.

byte salt[] = new byte[SALT_MIN_LEN + password.length];

Istranzio l'array che dovrà contenere solo l'hash

byte enc[] = new byte[startAt];

Copio soltanto la porzione che riguarda il salt nel nuovo array.

System.arraycopy(dbpassword,startAt,salt,0,salt.length);

Infine copio l'hash nel nuovo array.

System.arraycopy(dbpassword,0,enc,0,enc.length);

Adesso provo a generare il nuovo hash con la passoword in chiaro(primo parametro)

sha.update(password);
sha.update(salt);
byte toBytePassword[] = sha.digest(salt);

confronto l'hash generato dalla password in chiaro con quello che ho estratto prima e restituisco il risultato.
result = MessageDigest.isEqual(toBytePassword,enc);

Util
Questa classe di supporto viene utilizzata semplicemente per delle operazioni di conversione di formato.

public static String toString (byte[] bytes)

Converte un array di byte in una stringa esadecimale. La stringa risultante è un numero in formato esadecimale. Nella stringa generata un byte occuperà sempre due caratteri.

public static byte[] toByte (String hex)

Viceversa questa funzione converte un stringa in formato esadecimale in un array di byte.
La password crittografata dell'utente verrà infatti salvata sul db in formato esadecimale (utilizzeremo per questo campo un tipo VARCHAR) e viceversa convertita in byte durante la lettura. La scelta di scrivere in formato esadecimale sul db è solamente una scelta di comodo si potrebbe scrivere ad esempio anche in formato BASE64.

RegisterUser
Questa classe si utilizza utilizzata semplicemente per registrare un utente. Il costruttore
RegisterUser(UserBean userBean,byte password[])

  • Accetta un oggetto UserBean.
  • La password già crittografata

UserBean è appunto un bean che dovrebbe contenere tutte informazioni relative all'utente che si sta registrando (Cognome,Nome,indirizzo etc..), per semplicità il bean contiene solo nome,cognome e idutente.
Infine il metodo:

public boolean register() throws Exception

che crea fisicamente il record contenente l'informazioni dell'utente. Il metodo restituisce un booleano. Se restituisce true la registrazione è avvenuta con successo; se restituisce false significa che esiste già un utente con tale id.
Mi connetto al db utilizzando il datasource.

InitialContext initCtx = new InitialContext();
DataSource mysqlds= (DataSource)initCtx.lookup(Constants.JNDI_DATA_SOURCE_NAME);
connection = mysqlds.getConnection();
statement = connection.createStatement();

Converto la password già crittografata che ho passato al costruttore in esadecimale per mezzo della classe Util.

String dbpassword = Util.toString(password);

Prendo tutte le informazione dal bean (cognome,nome etc)

String nome = userBean.getNome();
..

Effettuo una ricerca per vedere se non esiste già un utente con questa username.

String queryUser="SELECT COUNT(*) AS CONTEGGIO FROM USERS WHERE USERNAME='" + username + "'";
resultset = statement.executeQuery(queryUser);
if (resultset.next()) {
result = resultset.getInt("CONTEGGIO")==0;
try{resultset.close();}catch(Exception er){};
}

if (result) {

Se l'utente non esiste eseguo la insert sul db (password crittografata compresa).

String queryInsert="INSERT INTO USERS (USERNAME,COGNOME,NOME,PASSWORD) VALUES('" + username + "','" + cognome + "','" + nome + "','" + dbpassword + "')";
statement.executeUpdate(queryInsert);

}

Il metodo login di AutenticationModule.

public UserBean login() throws AuthenticationException,Exception{
...

Accedo al db per ricavare i dati dell'utente. Cerco in base alla username

InitialContext initCtx = new InitialContext();
DataSource mysqlds= (DataSource)initCtx.lookup(Constants.JNDI_DATA_SOURCE_NAME); connection = mysqlds.getConnection();
statement = connection.createStatement();
resultset = statement.executeQuery("SELECT USERNAME,NOME,COGNOME,PASSWORD FROM USERS WHERE USERNAME='" + username + "'");

Controllo se esiste un record per l'utente.

if (resultset.next()){

Ricavo l'intera password dal db. La password è in formato esadecimale. Quindi utilizzo la classe Util per convertire in in un array di byte

dbpassword = Util.toByte(resultset.getString("PASSWORD"));

Adesso utilizzo la classe AuthenticationUtil ed il metodo verify per controllare se la password inserita dall'utente corrisponde a quella sul db.

result = AuthenticationUtil.verify(password,dbpassword);

l'autenticazione ha esito positivo carico i dati anagrafici dell'utente.

if (result){

userBean = new UserBean(); userBean.setCognome(resultset.getString("COGNOME"));
..

Quindi creo una nuova password (salt ed hash) ed effettuo l'aggiornamento del campo sul db (nuova password).

String newPassword = Util.toString(AuthenticationUtil.createPassword( password,dbpassword));
String updateQuery="UPDATE USERS SET PASSWORD='" + newPassword + "' WHERE USERNAME='" + username + "'";
statement.executeUpdate(updateQuery);

Se invece la password non corrisponde a quella sul db preparo il codice di errore (AuthenticationException).

}else errorcode = AuthenticationException.INVALID_PASSWORD;

Nel caso in cui l'utente è stato trovato sul db preparo un codice di errore differente.

}else{
errorcode = AuthenticationException.USERS_NOT_EXIST;
}

Chiudo comunque tutte le connessioni.

}finally{

try{resultset.close() ;resultset = null;}catch(Exception ee){};
try{statement.close() ;statement = null;}catch(Exception ee){};
try{connection.close();connection = null;}catch(Exception ee){};

}

Controllo se esiste un errore di autenticazione. Se esiste lancio l'eccezione. Altrimenti vado avanti e restituisco il bean dei dati utente.

if (errorcode>-1) throw new AuthenticationException(errorcode);
return userBean;

 

Conclusioni
La funzionalità di autenticazione è completa ed il codice di esempio prevede l'utilizzo di struts. E' molto semplice riadattare il codice per utilizzarla con una normale applicazione web. Per sviluppo ed il testing dell'applicazione ho utilizzato:

  • Apache Tomcat 4.1.31
  • MySql 4.0.24
  • Junit
  • Jakarta Struts

Nella seconda parte illustrerò come controllare, per mezzo di un impronta digitale, la sessione utente. Osserveremo come sia possibile utilizzare l'impronta per controllare anche l'accesso concorrente dello stesso account.

 

Bibliografia
Oaks Scott - "Java Security, Second Edition", Apogeo, 2001
Hoepli - "Programming Jakarta Struts", O'Really, 2003

 

Risorse
L'esempio mostrato in questo articolo è scaricabile qui