PHP和Java在bcrypt加密算法实现上的差异

背景:

首先,要说明的是,无论PHP还是Java,使用bcrypt进行hash得到的密文字符串长度都是60,其中密文的前4位,称为salt

        // php版是 $2y$10$Cih2shiBNg5jWrj0i.2hbuzZ5.g9T6caaxNP4yYtp3.wpi48rXomu
        // java版是 $2a$10$Cih2shiBNg5jWrj0i.2hbuzZ5.g9T6caaxNP4yYtp3.wpi48rXomu;

 

PHP的bcrypt默认采用的是CRYPT_BLOWFISH加密算法,使用的salt是$2y$,而Java使用的salt是$2a$,当使用Java对由PHP的bcrypt加密的密文进行校验时,会因为salt的这个差异导致Java出现下面的错误:

Encoded password does not look like BCrypt

从官方文档对CRYPT_BLOWFISH的说明里,可以证实:

  • CRYPT_BLOWFISH - Blowfish hashing with a salt as follows: "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters from the alphabet "./0-9A-Za-z". Using characters outside of this range in the salt will cause crypt() to return a zero-length string. The two digit cost parameter is the base-2 logarithm of the iteration count for the underlying Blowfish-based hashing algorithm and must be in range 04-31, values outside this range will cause crypt() to fail. "$2x$" hashes are potentially weak; "$2a$" hashes are compatible and mitigate this weakness. For new hashes, "$2y$" should be used. Please refer to » this document for full details of the related security fix.

解决办法,分为两种:

第一种,也是最简单的,在密文校验前,先将密文的$2y$替换为$2a$

第二种,重写spring boot的BCryptPasswordEncoder.java,之所以会出现上述错误,主要是下面这个方法:

    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);
    }

 

延申思考,spring boot的Bcrypt是怎么校验2个密文是不是一致的呢?通过上面的代码,我们知道校验密文是否一致,是调用BCrypt.checkpw(rawPassword.toString(), encodedPassword)实现的,来看看

Bcrypt.java里checkpw的实现逻辑:

    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;
    }

可以看到,校验2个密文是否一致前,把密文字符串通过toCharArray转成char数组,会先检查密文的长度,如果长度不一致,说明这2个密文是不一致的,这很好理解。如果长度一致,接下来,会通过for循环,对密文的每个字符的字节值进行遍历比较,进而得出是否一致的结果。

 

参考资料:

https://www.php.net/manual/en/function.crypt.php

posted @ 2022-04-26 12:46  jamstack  阅读(423)  评论(0编辑  收藏  举报