web-dev-qa-db-fra.com

Comment générer un SALT dans Java pour Salted-Hash?

J'ai regardé autour de moi et la réponse la plus proche est: Comment générer une chaîne alphanumérique aléatoire?

Je veux suivre ce flux de travail en fonction de ceci tutoriel CrackStation :

Pour stocker un mot de passe

  1. Générez un long sel aléatoire en utilisant un CSPRNG.

  2. Ajoutez le sel au mot de passe et hachez-le avec une fonction de hachage cryptographique standard telle que SHA256.

  3. Enregistrez à la fois le sel et le hachage dans l'enregistrement de la base de données de l'utilisateur.

Pour valider un mot de passe

  1. Récupérez le sel et le hachage de l'utilisateur de la base de données.

  2. Ajoutez le sel au mot de passe indiqué et hachez-le en utilisant la même fonction de hachage.

  3. Comparez le hachage du mot de passe donné avec le hachage de la base de données. S'ils correspondent, le mot de passe est correct. Sinon, le mot de passe est incorrect.

Je ne sais pas comment générer un SEL. J'ai compris comment générer un hachage à l'aide de MessageDigest. J'ai essayé d'utiliser SecureRandom mais la méthode nextByte produit un code corrompu.

Edit: Je ne sais pas quelle réponse choisir, ils sont trop compliqués pour moi, j'ai décidé d'utiliser jBCrypt; jBCript est facile à utiliser, fait toutes les choses complexes dans les coulisses. alors je laisserai la communauté voter pour la meilleure réponse.

39
Louis Hong

Inspiré de this post et this post , j'utilise ce code pour générer et vérifier les mots de passe hachés salés. Il utilise uniquement les classes fournies par JDK, pas de dépendance externe.

Le processus est:

  • vous créez un sel avec getNextSalt
  • vous demandez à l'utilisateur son mot de passe et utilisez la méthode hash pour générer un mot de passe salé et haché. La méthode retourne un byte[] que vous pouvez enregistrer tel quel dans une base de données avec le sel
  • pour authentifier un utilisateur, vous lui demandez son mot de passe, récupérez le mot de passe salt et hashed de la base de données et utilisez la méthode isExpectedPassword pour vérifier que les détails correspondent.
/**
 * A utility class to hash passwords and check passwords vs hashed values. It uses a combination of hashing and unique
 * salt. The algorithm used is PBKDF2WithHmacSHA1 which, although not the best for hashing password (vs. bcrypt) is
 * still considered robust and <a href="https://security.stackexchange.com/a/6415/12614"> recommended by NIST </a>.
 * The hashed value has 256 bits.
 */
public class Passwords {

  private static final Random RANDOM = new SecureRandom();
  private static final int ITERATIONS = 10000;
  private static final int KEY_LENGTH = 256;

  /**
   * static utility class
   */
  private Passwords() { }

  /**
   * Returns a random salt to be used to hash a password.
   *
   * @return a 16 bytes random salt
   */
  public static byte[] getNextSalt() {
    byte[] salt = new byte[16];
    RANDOM.nextBytes(salt);
    return salt;
  }

  /**
   * Returns a salted and hashed password using the provided hash.<br>
   * Note - side effect: the password is destroyed (the char[] is filled with zeros)
   *
   * @param password the password to be hashed
   * @param salt     a 16 bytes salt, ideally obtained with the getNextSalt method
   *
   * @return the hashed password with a pinch of salt
   */
  public static byte[] hash(char[] password, byte[] salt) {
    PBEKeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_LENGTH);
    Arrays.fill(password, Character.MIN_VALUE);
    try {
      SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
      return skf.generateSecret(spec).getEncoded();
    } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
      throw new AssertionError("Error while hashing a password: " + e.getMessage(), e);
    } finally {
      spec.clearPassword();
    }
  }

  /**
   * Returns true if the given password and salt match the hashed value, false otherwise.<br>
   * Note - side effect: the password is destroyed (the char[] is filled with zeros)
   *
   * @param password     the password to check
   * @param salt         the salt used to hash the password
   * @param expectedHash the expected hashed value of the password
   *
   * @return true if the given password and salt match the hashed value, false otherwise
   */
  public static boolean isExpectedPassword(char[] password, byte[] salt, byte[] expectedHash) {
    byte[] pwdHash = hash(password, salt);
    Arrays.fill(password, Character.MIN_VALUE);
    if (pwdHash.length != expectedHash.length) return false;
    for (int i = 0; i < pwdHash.length; i++) {
      if (pwdHash[i] != expectedHash[i]) return false;
    }
    return true;
  }

  /**
   * Generates a random password of a given length, using letters and digits.
   *
   * @param length the length of the password
   *
   * @return a random password
   */
  public static String generateRandomPassword(int length) {
    StringBuilder sb = new StringBuilder(length);
    for (int i = 0; i < length; i++) {
      int c = RANDOM.nextInt(62);
      if (c <= 9) {
        sb.append(String.valueOf(c));
      } else if (c < 36) {
        sb.append((char) ('a' + c - 10));
      } else {
        sb.append((char) ('A' + c - 36));
      }
    }
    return sb.toString();
  }
}
48
assylias

Vous aviez raison quant à la manière dont vous voulez générer du sel, c’est-à-dire qu’il s’agit d’un nombre aléatoire. Dans ce cas particulier, cela protégerait votre système des attaques éventuelles du dictionnaire. Maintenant, pour le deuxième problème, vous pouvez utiliser Base64 au lieu d’utiliser le codage UTF-8. Voici un exemple pour générer un hachage. J'utilise les codecs courants Apache pour coder en base64. Vous pouvez en choisir un de votre choix.

public byte[] generateSalt() {
        SecureRandom random = new SecureRandom();
        byte bytes[] = new byte[20];
        random.nextBytes(bytes);
        return bytes;
    }

public String bytetoString(byte[] input) {
        return org.Apache.commons.codec.binary.Base64.encodeBase64String(input);
    }

public byte[] getHashWithSalt(String input, HashingTechqniue technique, byte[] salt) throws NoSuchAlgorithmException {
        MessageDigest digest = MessageDigest.getInstance(technique.value);
        digest.reset();
        digest.update(salt);
        byte[] hashedBytes = digest.digest(stringToByte(input));
        return hashedBytes;
    }
public byte[] stringToByte(String input) {
        if (Base64.isBase64(input)) {
            return Base64.decodeBase64(input);

        } else {
            return Base64.encodeBase64(input.getBytes());
        }
    }

Voici quelques références supplémentaires de la pratique standard en matière de hachage de mot de passe directement à partir de OWASP

11
Raunak Agarwal

Une autre version utilisant SHA-3, j'utilise Bouncycastle:

L'interface:

public interface IPasswords {

    /**
     * Generates a random salt.
     *
     * @return a byte array with a 64 byte length salt.
     */
    byte[] getSalt64();

    /**
     * Generates a random salt
     *
     * @return a byte array with a 32 byte length salt.
     */
    byte[] getSalt32();

    /**
     * Generates a new salt, minimum must be 32 bytes long, 64 bytes even better.
     *
     * @param size the size of the salt
     * @return a random salt.
     */
    byte[] getSalt(final int size);

    /**
     * Generates a new hashed password
     *
     * @param password to be hashed
     * @param salt the randomly generated salt
     * @return a hashed password
     */
    byte[] hash(final String password, final byte[] salt);

    /**
     * Expected password
     *
     * @param password to be verified
     * @param salt the generated salt (coming from database)
     * @param hash the generated hash (coming from database)
     * @return true if password matches, false otherwise
     */
    boolean isExpectedPassword(final String password, final byte[] salt, final byte[] hash);

    /**
     * Generates a random password
     *
     * @param length desired password length
     * @return a random password
     */
    String generateRandomPassword(final int length);
}

La mise en oeuvre:

import org.Apache.commons.lang3.ArrayUtils;
import org.Apache.commons.lang3.Validate;
import org.Apache.log4j.Logger;
import org.bouncycastle.jcajce.provider.digest.SHA3;

import Java.io.Serializable;
import Java.io.UnsupportedEncodingException;
import Java.security.SecureRandom;
import Java.util.ArrayList;
import Java.util.Arrays;
import Java.util.List;
import Java.util.Random;

public final class Passwords implements IPasswords, Serializable {

    /*serialVersionUID*/
    private static final long serialVersionUID = 8036397974428641579L;
    private static final Logger LOGGER = Logger.getLogger(Passwords.class);
    private static final Random RANDOM = new SecureRandom();
    private static final int DEFAULT_SIZE = 64;
    private static final char[] symbols;

    static {
            final StringBuilder tmp = new StringBuilder();
            for (char ch = '0'; ch <= '9'; ++ch) {
                    tmp.append(ch);
            }
            for (char ch = 'a'; ch <= 'z'; ++ch) {
                    tmp.append(ch);
            }
            symbols = tmp.toString().toCharArray();
    }

    @Override public byte[] getSalt64() {
            return getSalt(DEFAULT_SIZE);
    }

    @Override public byte[] getSalt32() {
            return getSalt(32);
    }

    @Override public byte[] getSalt(int size) {
            final byte[] salt;
            if (size < 32) {
                    final String message = String.format("Size < 32, using default of: %d", DEFAULT_SIZE);
                    LOGGER.warn(message);
                    salt = new byte[DEFAULT_SIZE];
            } else {
                    salt = new byte[size];
            }
            RANDOM.nextBytes(salt);
            return salt;
    }

    @Override public byte[] hash(String password, byte[] salt) {

            Validate.notNull(password, "Password must not be null");
            Validate.notNull(salt, "Salt must not be null");

            try {
                    final byte[] passwordBytes = password.getBytes("UTF-8");
                    final byte[] all = ArrayUtils.addAll(passwordBytes, salt);
                    SHA3.DigestSHA3 md = new SHA3.Digest512();
                    md.update(all);
                    return md.digest();
            } catch (UnsupportedEncodingException e) {
                    final String message = String
                            .format("Caught UnsupportedEncodingException e: <%s>", e.getMessage());
                    LOGGER.error(message);
            }
            return new byte[0];
    }

    @Override public boolean isExpectedPassword(final String password, final byte[] salt, final byte[] hash) {

            Validate.notNull(password, "Password must not be null");
            Validate.notNull(salt, "Salt must not be null");
            Validate.notNull(hash, "Hash must not be null");

            try {
                    final byte[] passwordBytes = password.getBytes("UTF-8");
                    final byte[] all = ArrayUtils.addAll(passwordBytes, salt);

                    SHA3.DigestSHA3 md = new SHA3.Digest512();
                    md.update(all);
                    final byte[] digest = md.digest();
                    return Arrays.equals(digest, hash);
            }catch(UnsupportedEncodingException e){
                    final String message =
                            String.format("Caught UnsupportedEncodingException e: <%s>", e.getMessage());
                    LOGGER.error(message);
            }
            return false;


    }

    @Override public String generateRandomPassword(final int length) {

            if (length < 1) {
                    throw new IllegalArgumentException("length must be greater than 0");
            }

            final char[] buf = new char[length];
            for (int idx = 0; idx < buf.length; ++idx) {
                    buf[idx] = symbols[RANDOM.nextInt(symbols.length)];
            }
            return shuffle(new String(buf));
    }


    private String shuffle(final String input){
            final List<Character> characters = new ArrayList<Character>();
            for(char c:input.toCharArray()){
                    characters.add(c);
            }
            final StringBuilder output = new StringBuilder(input.length());
            while(characters.size()!=0){
                    int randPicker = (int)(Math.random()*characters.size());
                    output.append(characters.remove(randPicker));
            }
            return output.toString();
    }
}

Les cas de test:

public class PasswordsTest {

    private static final Logger LOGGER = Logger.getLogger(PasswordsTest.class);

    @Before
    public void setup(){
            BasicConfigurator.configure();
    }

    @Test
    public void testGeSalt() throws Exception {

            IPasswords passwords = new Passwords();
            final byte[] bytes = passwords.getSalt(0);
            int arrayLength = bytes.length;

            assertThat("Expected length is", arrayLength, is(64));
    }

    @Test
    public void testGeSalt32() throws Exception {
            IPasswords passwords = new Passwords();
            final byte[] bytes = passwords.getSalt32();
            int arrayLength = bytes.length;
            assertThat("Expected length is", arrayLength, is(32));
    }

    @Test
    public void testGeSalt64() throws Exception {
            IPasswords passwords = new Passwords();
            final byte[] bytes = passwords.getSalt64();
            int arrayLength = bytes.length;
            assertThat("Expected length is", arrayLength, is(64));
    }

    @Test
    public void testHash() throws Exception {
            IPasswords passwords = new Passwords();
            final byte[] hash = passwords.hash("holacomoestas", passwords.getSalt64());
            assertThat("Array is not null", hash, Matchers.notNullValue());
    }


    @Test
    public void testSHA3() throws UnsupportedEncodingException {
            SHA3.DigestSHA3 md = new SHA3.Digest256();
            md.update("holasa".getBytes("UTF-8"));
            final byte[] digest = md.digest();
             assertThat("expected digest is:",digest,Matchers.notNullValue());
    }

    @Test
    public void testIsExpectedPasswordIncorrect() throws Exception {

            String password = "givemebeer";
            IPasswords passwords = new Passwords();

            final byte[] salt64 = passwords.getSalt64();
            final byte[] hash = passwords.hash(password, salt64);
            //The salt and the hash go to database.

            final boolean isPasswordCorrect = passwords.isExpectedPassword("jfjdsjfsd", salt64, hash);

            assertThat("Password is not correct", isPasswordCorrect, is(false));

    }

    @Test
    public void testIsExpectedPasswordCorrect() throws Exception {
            String password = "givemebeer";
            IPasswords passwords = new Passwords();
            final byte[] salt64 = passwords.getSalt64();
            final byte[] hash = passwords.hash(password, salt64);
            //The salt and the hash go to database.
            final boolean isPasswordCorrect = passwords.isExpectedPassword("givemebeer", salt64, hash);
            assertThat("Password is correct", isPasswordCorrect, is(true));
    }

    @Test
    public void testGenerateRandomPassword() throws Exception {
            IPasswords passwords = new Passwords();
            final String randomPassword = passwords.generateRandomPassword(10);
            LOGGER.info(randomPassword);
            assertThat("Random password is not null", randomPassword, Matchers.notNullValue());
    }
}

pom.xml (uniquement les dépendances):

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testng</groupId>
        <artifactId>testng</artifactId>
        <version>6.1.1</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.hamcrest</groupId>
        <artifactId>hamcrest-all</artifactId>
        <version>1.3</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>

    <dependency>
        <groupId>org.bouncycastle</groupId>
        <artifactId>bcprov-jdk15on</artifactId>
        <version>1.51</version>
        <type>jar</type>
    </dependency>


    <dependency>
        <groupId>org.Apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.3.2</version>
    </dependency>


</dependencies>