Il salvataggio delle password utente in un database è comune, ma non tutti gli sviluppatori lo fanno bene. In questo articolo spiegherò un modo corretto e semplice per farlo.
Memorizzare una password come campo di testo normale è un'idea terribile, ma anche archiviarla crittografata utilizzando un algoritmo a due vie è un metodo errato.
Perché? Perché se crittografiamo e archiviamo la password utilizzando una chiave di crittografia e un algoritmo che ci consente di crittografare la password e decrittografarla da quella chiave, chiunque abbia la chiave di crittografia può decrittografarla. Allora qual è il modo giusto per farlo?
Nella maggior parte dei casi, memorizzare la rappresentazione di una password nel database è la cosa giusta da fare.
Per fare ciò, devi eseguire l'hashing della password utilizzando un salt diverso per ogni utente che utilizza un algoritmo unidirezionale e archiviare il risultato, rimuovendo la password originale. Quindi, quando si desidera verificare una password, si esegue nuovamente l'hashing della password in testo semplice utilizzando lo stesso algoritmo di hashing e lo stesso salt e lo si confronta con il valore memorizzato con hash nel database. Ma cosa intendi per usare un salt diverso per ogni utente?
Un salt è un dato casuale utilizzato come input aggiuntivo per un algoritmo unidirezionale che esegue l'hashing dei dati. Storicamente, solo una funzione di hash crittografico della password è stata memorizzata su un sistema, ma nel tempo sono state sviluppate ulteriori misure di sicurezza per proteggere dall'identificazione di password comuni o duplicate.
Vediamo un caso pratico. Immaginiamo di avere una tabella nel database per memorizzare gli utenti della mia applicazione. Questa tabella ha i seguenti campi che utilizzeremo per memorizzare l'utente e la password:
CREATE TABLE `USERS` (
`ID` int(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`HASHED_PASSWORD` varchar(255) NOT NULL,
`USERNAME` varchar(255) NOT NULL,
`SALT` varchar(1024) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Il passaggio successivo consiste nel generare un salt univoco per ogni utente ogni volta che creiamo un utente in quella tabella. Per questo, definirò una classe usando Java con un metodo che genererà il salt. Farlo usando qualsiasi altro linguaggio di programmazione è simile.
package com.saverioriotto.example.utils;
import ...
public class PasswordUtils {
public static final int KEY_LENGTH = 512;
private static final SecureRandom RAND = new SecureRandom();
public static Optional generateSalt(final int length) {
byte[] salt = new byte[length];
RAND.nextBytes(salt);
return Optional.of(Base64.getEncoder().encodeToString(salt));
}
}
Una volta ottenuto il salt per l'utente, il passaggio successivo consiste nell'hash della password in testo semplice utilizzando quel salt. Per fare ciò, aggiungeremo alla nostra classe un metodo chiamato hashThePlainTextPassword:
public static final int ITERATIONS = 1000;
public static final int KEY_LENGTH = 512;
private static final String ALGORITHM = "PBKDF2WithHmacSHA512";
public static Optional hashThePlainTextPassword(String plainTextPassword, String salt) {
char[] chars = plainTextPassword.toCharArray();
byte[] bytes = salt.getBytes();
PBEKeySpec spec = new PBEKeySpec(chars, bytes, ITERATIONS, KEY_LENGTH);
Arrays.fill(chars, Character.MIN_VALUE);
try {
SecretKeyFactory fac = SecretKeyFactory.getInstance(ALGORITHM);
byte[] securePassword = fac.generateSecret(spec).getEncoded();
return Optional.of(Base64.getEncoder().encodeToString(securePassword));
} catch (NoSuchAlgorithmException | InvalidKeySpecException ex) {
//Handle the exception
return Optional.empty();
} finally {
spec.clearPassword();
}
}
Una volta che abbiamo la nostra password con salt, possiamo memorizzarla sul db insieme al suo salt. Nota che quando si utilizza un algoritmo unidirezionale per decodificare una password, non conosceremo mai il valore originale della password in testo normale. Quindi, anche se ottieni il salt dell'utente, se non conosci la password in chiaro originale, non genererai mai lo stesso hash. Quindi come posso sapere se la password è corretta?
Se vogliamo verificare una password, eseguiamo nuovamente l'hashing del valore (usando lo stesso algoritmo di hashing e il salt) e lo confrontiamo con il valore hash dell'utente nel database.
Per fare ciò, aggiungeremo alla nostra classe un metodo chiamato verifyThePlainTextPassword. Questo metodo esegue l'hashing della password in chiaro con il valore del salt originale. Ora, se il valore memorizzato dell'utente nel database e il valore con hash generato sono identici, la password è la stessa.
public static boolean verifyThePlainTextPassword(
String plainTextPassword,
String hashedPassword,
String salt) {
Optional optEncrypted = hashThePlainTextPassword(plainTextPassword, salt);
if (!optEncrypted.isPresent()) {
return false;
}
return optEncrypted.get().equals(hashedPassword);
}
La classe completa:
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.Base64;
import java.util.Optional;
public class PasswordUtils {
public static final int ITERATIONS = 1000;
public static final int KEY_LENGTH = 512;
private static final String ALGORITHM = "PBKDF2WithHmacSHA512";
private static final SecureRandom RAND = new SecureRandom();
public static Optional generateSalt(final int length) {
byte[] salt = new byte[length];
RAND.nextBytes(salt);
return Optional.of(Base64.getEncoder().encodeToString(salt));
}
public static Optional hashThePlainTextPassword(
String plainTextPassword,
String salt) {
char[] chars = plainTextPassword.toCharArray();
byte[] bytes = salt.getBytes();
PBEKeySpec spec = new PBEKeySpec(chars, bytes, ITERATIONS, KEY_LENGTH);
Arrays.fill(chars, Character.MIN_VALUE);
try {
SecretKeyFactory fac = SecretKeyFactory.getInstance(ALGORITHM);
byte[] securePassword = fac.generateSecret(spec).getEncoded();
return Optional.of(Base64.getEncoder().encodeToString(securePassword));
} catch (NoSuchAlgorithmException | InvalidKeySpecException ex) {
//Handle the exception
return Optional.empty();
} finally {
spec.clearPassword();
}
}
public static boolean verifyThePlainTextPassword(
String plainTextPassword,
String hashedPassword,
String salt) {
Optional optEncrypted = hashThePlainTextPassword(plainTextPassword, salt);
if (!optEncrypted.isPresent()) {
return false;
}
return optEncrypted.get().equals(hashedPassword);
}
}
Come puoi vedere, salvare una password nel database è semplice e farlo correttamente eviterà molti problemi di sicurezza.