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
|