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循环,对密文的每个字符的字节值进行遍历比较,进而得出是否一致的结果。
参考资料: