Recently wrote user management-related micro-services, the more important question is how to save the user's password hash salt is a common practice. We know there is a problem on almost everyone can first read: The most common method of preserving salted passwords that?

For each user's password, the salt should be used a unique value, or each time a new registered user to change the password, you should use the new salt value is encrypted, and the salt value should be long enough so that there is enough salt value for encryption. With the advent and increasing of rainbow tables, MD5 algorithm does not propose to use.

The step of storing passwords

  1. Using (Cryptographically Number Generator Secure Pseudo-Random - CSPRNG) encryption-based pseudo-random number generator generates a value long enough salt, such as in Java java.security.SecureRandom
  2. The mixed salt value password (common practice prior to placement in a password), using standard encrypted cryptographic hash function, such as SHA256
  3. The hash value is stored together with the salt value and that record corresponding to this user database

Verifying the password

  1. Removed from the database user password hash value and a corresponding salt value
  2. The mixed salt value entered by the user password, using the same hash function and encrypting
  3. Previous results and compare hash values ​​are the same, so if the same password is correct, otherwise the wrong password

Salt so an attacker can not adopt a specific query table or rainbow table fast break a lot of hash value, but can not prevent dictionary attacks or brute force attack. It is assumed that the attacker has to get the user database, meaning that the attacker knows the salt value for each user, according to Kerckhoffs's Principle , we should assume that the attacker knows the user's system password encryption algorithm, if the attacker uses high-end GPU or custom ASIC , each sec can be billions of times a hash calculation, efficiency dictionary lookup for each user is still very efficient.

In order to reduce such attacks, you can use a technique called key expansion of technology, so the hash function becomes very slow, even if the GPU or ASIC dictionary attack or brute force attack will be slow so that attackers can not accept. Key Expansion implementation relies on a CPU-intensive hash function such PBKDF2 and will herein be described Bcrypt . Such a function using a safety factor or the number of iterations as a parameter, which determines the value of the hash function there will be more slowly.

Bcrypt

Bcrypt is based on a cryptographic hash function Blowfish cipher designed by Niels Provos and DavidMazières, published on the USENIX 1999.

There are entries on wikipedia Bcrypt pseudo code of the algorithm:

Function bcrypt
    Input:
        cost:     Number (4..31)                // 该值决定了密钥扩展的迭代次数 Iterations = 2^cost
        salt:     array of Bytes (16 bytes)     // 随机数
        password: array of Bytes (1..72 bytes)  // 用户密码
    Output:
        hash:     array of Bytes (24 bytes)     // 返回的哈希值

    // 使用Expensive key setup算法初始化Blowfish状态
    state <- EksBlowfishSetup(cost, salt, password)     // 这一步是整个算法中最耗时的步骤

    ctext <- "OrpheanBeholderScryDoubt"     // 24 bytes,初始向量
    repeat (64)
        ctext <- EncryptECB(state, ctext)   // 使用 blowfish 算法的ECB模式进行加密

    return Concatenate(cost, salt, ctext)

// Expensive key setup
Function EksBlowfishSetup
    Input:
        cost:     Number (4..31)
        salt:     array of Bytes (16 bytes)
        password: array of Bytes (1..72 bytes)
    Output:
        state:    opaque BlowFish state structure

    state <- InitialState()
    state <- ExpandKey(state, salt, password)
    repeat (2 ^ cost)           // 计算密集型
        state <- ExpandKey(state, 0, password)
        state <- ExpandKey(state, 0, salt)

    return state

Function ExpandKey(state, salt, password)
    Input:
        state:    Opaque BlowFish state structure  // 内部包含 P-array 和 S-box
        salt:     array of Bytes (16 bytes)
        password: array of Bytes (1..72 bytes)
    Output:
        state:    Opaque BlowFish state structure

    // ExpandKey 是对输入参数进行固定的移位异或等运算,这里不列出

As it can be seen by the pseudo-code, through the development of various cost values, such that the number of operations can significantly enhance EksBlowfishSetup, so as to achieve slow hash.

Spring Security in Bcrypt

Understand the principles Bcrypt algorithm, and then look to achieve in Spring Security is very simple.

package org.springframework.security.crypto.bcrypt;

...省略import...

public class BCryptPasswordEncoder implements PasswordEncoder {
    ...省略log...

    private final int strength;         // 相当于wiki伪代码中的cost,默认为10

    private final SecureRandom random;  // CSPRNG

    // 构造函数
    public BCryptPasswordEncoder() {
        this(-1);
    }

    // 相当于伪代码中的cost, 长度 4 ~ 31
    public BCryptPasswordEncoder(int strength) {
        this(strength, null);
    }

    // 构造函数
    public BCryptPasswordEncoder(int strength, SecureRandom random) {
        if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) {
            throw new IllegalArgumentException("Bad strength");
        }
        this.strength = strength;
        this.random = random;
    }

    // 加密函数
    public String encode(CharSequence rawPassword) {
        String salt;
        if (strength > 0) {
            if (random != null) {
                salt = BCrypt.gensalt(strength, random);
            }
            else {
                salt = BCrypt.gensalt(strength);
            }
        }
        else {
            salt = BCrypt.gensalt();
        }
        return BCrypt.hashpw(rawPassword.toString(), salt);
    }

    // 密码匹配函数
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (encodedPassword == null || encodedPassword.length() == 0) {
            logger.warn("Empty encoded password");
            return false;
        }

        if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
            logger.warn("Encoded password does not look like BCrypt");
            return false;
        }

        return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
    }
}

package org.springframework.security.crypto.bcrypt;

public class BCrypt {

    // 生成盐值的函数 "$2a$" + 2字节log_round + "$" + 22字节随机数Base64编码
    public static String gensalt(int log_rounds, SecureRandom random) {
        if (log_rounds < MIN_LOG_ROUNDS || log_rounds > MAX_LOG_ROUNDS) {
            throw new IllegalArgumentException("Bad number of rounds");
        }
        StringBuilder rs = new StringBuilder();
        byte rnd[] = new byte[BCRYPT_SALT_LEN];

        random.nextBytes(rnd);

        rs.append("$2a$");
        if (log_rounds < 10) {
            rs.append("0");
        }
        rs.append(log_rounds);
        rs.append("$");
        encode_base64(rnd, rnd.length, rs);
        return rs.toString();   // 总长度29字节
    }

    /**
     * Hash a password using the OpenBSD bcrypt scheme
     * @param password the password to hash
     * @param salt the salt to hash with (perhaps generated using BCrypt.gensalt)
     * @return the hashed password
     * @throws IllegalArgumentException if invalid salt is passed
     */
    public static String hashpw(String password, String salt) throws IllegalArgumentException {
        // 该函数在验证阶段也会用到,因为前29字节为盐值,所以可以将之前计算过的密码哈希值做为盐值
        BCrypt B;
        String real_salt;
        byte passwordb[], saltb[], hashed[];
        char minor = (char) 0;
        int rounds, off = 0;
        StringBuilder rs = new StringBuilder();

        if (salt == null) {
            throw new IllegalArgumentException("salt cannot be null");
        }

        int saltLength = salt.length();

        if (saltLength < 28) {
            throw new IllegalArgumentException("Invalid salt");
        }

        if (salt.charAt(0) != '$' || salt.charAt(1) != '2') {
            throw new IllegalArgumentException("Invalid salt version");
        }
        if (salt.charAt(2) == '$') {
            off = 3;
        }
        else {
            minor = salt.charAt(2);
            if (minor != 'a' || salt.charAt(3) != '$') {
                throw new IllegalArgumentException("Invalid salt revision");
            }
            off = 4;
        }

        if (saltLength - off < 25) {
            throw new IllegalArgumentException("Invalid salt");
        }

        // Extract number of rounds
        if (salt.charAt(off + 2) > '$') {
            throw new IllegalArgumentException("Missing salt rounds");
        }
        rounds = Integer.parseInt(salt.substring(off, off + 2));

        real_salt = salt.substring(off + 3, off + 25);
        try {
            passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes("UTF-8");
        }
        catch (UnsupportedEncodingException uee) {
            throw new AssertionError("UTF-8 is not supported");
        }

        // 解析成16字节的字节数组
        saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);

        // 这里new了一个新的对象是因为会用到BCrypt中int P[]和 int S[],扩展的密钥存放在这两个结构体
        B = new BCrypt();
        hashed = B.crypt_raw(passwordb, saltb, rounds);

        rs.append("$2");
        if (minor >= 'a') {
            rs.append(minor);
        }
        rs.append("$");
        if (rounds < 10) {
            rs.append("0");
        }
        rs.append(rounds);
        rs.append("$");
        encode_base64(saltb, saltb.length, rs);
        encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1, rs);
        return rs.toString();
    }

    /**
     * Perform the central password hashing step in the bcrypt scheme
     * @param password the password to hash
     * @param salt the binary salt to hash with the password
     * @param log_rounds the binary logarithm of the number of rounds of hashing to apply
     * @return an array containing the binary hashed password
     */
    private byte[] crypt_raw(byte password[], byte salt[], int log_rounds) {
        int cdata[] = (int[]) bf_crypt_ciphertext.clone();
        int clen = cdata.length;
        byte ret[];

        // rounds = 1 << log_round
        long rounds = roundsForLogRounds(log_rounds);

        init_key();
        ekskey(salt, password);
        for (long i = 0; i < rounds; i++) { // 最耗时的密钥扩展
            key(password);
            key(salt);
        }

        for (int i = 0; i < 64; i++) {
            for (int j = 0; j < (clen >> 1); j++) {
                encipher(cdata, j << 1);
            }
        }

        ret = new byte[clen * 4];
        for (int i = 0, j = 0; i < clen; i++) {
            ret[j++] = (byte) ((cdata[i] >> 24) & 0xff);
            ret[j++] = (byte) ((cdata[i] >> 16) & 0xff);
            ret[j++] = (byte) ((cdata[i] >> 8) & 0xff);
            ret[j++] = (byte) (cdata[i] & 0xff);
        }
        return ret;
    }

    /**
     * Check that a plaintext password matches a previously hashed one
     * @param plaintext the plaintext password to verify
     * @param hashed the previously-hashed password
     * @return true if the passwords match, false otherwise
     */
    public static boolean checkpw(String plaintext, String hashed) {
        return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed));
    }

    static boolean equalsNoEarlyReturn(String a, String b) {
        char[] caa = a.toCharArray();
        char[] cab = b.toCharArray();

        if (caa.length != cab.length) {
            return false;
        }

        byte ret = 0;
        for (int i = 0; i < caa.length; i++) {
            ret |= caa[i] ^ cab[i];
        }
        return ret == 0;
    }
}

performance

Slow hash is necessary to prevent an attacker can not use violence, sabotage, can not affect the user experience, due to differences in machine performance, the best way to get the intensity parameter is the implementation of a short performance benchmarks to find the hash function takes about 0.5 sec value.

public class BCryptBench {
    public static void main(String[] args) {
        long startTime, endTime, duration;

        // the default strength 10
        BCryptPasswordEncoder bCryptPasswordEncoder10 = new BCryptPasswordEncoder();
        duration = 0;

        for (int i = 0; i < 10; i++) {
            startTime = System.currentTimeMillis();
            System.out.println(bCryptPasswordEncoder10.encode("admin"));
            endTime = System.currentTimeMillis();
            duration += (endTime - startTime);
        }

        System.out.println(duration / 10.0);    // 88.4ms

        // strength 11
        // the default strength 10
        BCryptPasswordEncoder bCryptPasswordEncoder11 = new BCryptPasswordEncoder(11);
        duration = 0;

        for (int i = 0; i < 10; i++) {
            startTime = System.currentTimeMillis();
            System.out.println(bCryptPasswordEncoder11.encode("admin"));
            endTime = System.currentTimeMillis();
            duration += (endTime - startTime);
        }

        System.out.println(duration / 10.0);    // 175.3ms

        // strength 12
        BCryptPasswordEncoder bCryptPasswordEncoder12 = new BCryptPasswordEncoder(12);
        duration = 0;

        for (int i = 0; i < 10; i++) {
            startTime = System.currentTimeMillis();
            System.out.println(bCryptPasswordEncoder12.encode("admin"));
            endTime = System.currentTimeMillis();
            duration += (endTime - startTime);
        }

        System.out.println( duration / 10.0);   // 344.3ms

        // strength 13
        BCryptPasswordEncoder bCryptPasswordEncoder13 = new BCryptPasswordEncoder(13);
        duration = 0;

        for (int i = 0; i < 10; i++) {
            startTime = System.currentTimeMillis();
            System.out.println(bCryptPasswordEncoder13.encode("admin"));
            endTime = System.currentTimeMillis();
            duration += (endTime - startTime);
        }

        System.out.println(duration / 10.0);    // 703.8ms

        // strength 14
        BCryptPasswordEncoder bCryptPasswordEncoder14 = new BCryptPasswordEncoder(14);
        duration = 0;

        for (int i = 0; i < 10; i++) {
            startTime = System.currentTimeMillis();
            System.out.println(bCryptPasswordEncoder14.encode("admin"));
            endTime = System.currentTimeMillis();
            duration += (endTime - startTime);
        }

        System.out.println(duration / 10.0);    // 1525.0ms

        // strength 15
        BCryptPasswordEncoder bCryptPasswordEncoder15 = new BCryptPasswordEncoder(15);
        duration = 0;

        for (int i = 0; i < 10; i++) {
            startTime = System.currentTimeMillis();
            System.out.println(bCryptPasswordEncoder15.encode("admin"));
            endTime = System.currentTimeMillis();
            duration += (endTime - startTime);
        }

        System.out.println(duration / 10.0);    // 2921.9ms
    }
}

From the results of the test, if you want to select a hash slower execution time of 0.5 seconds, the intensity Bcrypt function needs to be set to 12 or 13. And in our own micro-services using the default intensity of 10.



Original Address: https: //zhjwpku.com/2017/11/30/bcrypt-in-spring-security.html