bcrypt,是一个跨平台的文件加密工具。由它加密的文件可在所有支持的操作系统和处理器上进行转移。它的口令必须是8至56个字符,并将在内部被转化为448位的密钥;bcrypt 使用的是布鲁斯·施内尔在1993年发布的 Blowfish 加密算法。具体来说,bcrypt 使用保罗·柯切尔的算法实现。随 bcrypt 一起发布的源代码对原始版本作了略微改动。
在Spring Secutity框架中会使用bcrypt算法对用户输入的密码进行加密,然后对比;由BCryptPasswordEncoder提供了相关的方法;
bcrypt有两个特点:1.每一次Hash出来的值不一样 2.计算非常缓慢
import org.mindrot.jbcrypt.BCrypt;
String pw = "abc123456";
String hashpw = BCrypt.hashpw(pw, BCrypt.gensalt());
System.out.println("第一:"+hashpw);
String hashpw1 = BCrypt.hashpw(pw, BCrypt.gensalt());
System.out.println("第二: "+hashpw1);
String hashpw2 = BCrypt.hashpw(pw, BCrypt.gensalt());
System.out.println("第三:"+hashpw2);
输出:
第一:$2a$10$8fVlbQI/sEueD58hWyATxOQEn8XUCXiU/gU6hFQMLoTsRNOpLInGC
第二: $2a$10$aCf7FxEvIkKm7UebXHHfqu9hBq5qrzMliBZy.uJWRcY1P1EDX6bse
第三:$2a$10$02UwB4ODFQLfwNbQtB3Fs.NecXUy4ssiUYKZzeP7ftW4tdn5FO4nK看源码前先去看一下gensalt的底层实现::
public static String gensalt(int log_rounds, SecureRandom random) {
StringBuffer rs = new StringBuffer();
byte[] rnd = new byte[16];
random.nextBytes(rnd);
rs.append("$2a$");
if (log_rounds < 10) {
rs.append("0");
}
if (log_rounds > 30) {
throw new IllegalArgumentException("log_rounds exceeds maximum (30)");
} else {
rs.append(Integer.toString(log_rounds));
rs.append("$");
rs.append(encode_base64(rnd, rnd.length));
return rs.toString();
}
}这个底层组成三个部分,第一个固定($2a$),第二个其实也是固定的值为10,第三个使用了base64对刚才随机拿到的byte数组遍历进行转码,再来看看这个encode_base64方法:
private static String encode_base64(byte[] d, int len) throws IllegalArgumentException {
int off = 0;
StringBuffer rs = new StringBuffer();
if (len > 0 && len <= d.length) {
while(off < len) {
int c1 = d[off++] & 255;
rs.append(base64_code[c1 >> 2 & 63]);
c1 = (c1 & 3) << 4;
if (off >= len) {
rs.append(base64_code[c1 & 63]);
break;
}
int c2 = d[off++] & 255;
c1 |= c2 >> 4 & 15;
rs.append(base64_code[c1 & 63]);
c1 = (c2 & 15) << 2;
if (off >= len) {
rs.append(base64_code[c1 & 63]);
break;
}
c2 = d[off++] & 255;
c1 |= c2 >> 6 & 3;
rs.append(base64_code[c1 & 63]);
rs.append(base64_code[c2 & 63]);
}
return rs.toString();
} else {
throw new IllegalArgumentException("Invalid len");
}
}这里出现了&,&即是运算符也是逻辑运算符,&的两侧可以是int也可以是boolean表达式,当&两侧是int时,先要把运算符两侧的数字转为二进制在进行运算; 这里遍历第一个byte然后和固定的255进行运算,然后出现>>符号,相当于num/2n,算数右移,(这里的<<和>>自行百度,不做过多的阐述)在和固定的63进行运算,得出的数对应base64_code中的char字符,添加进StringBuffer中,然后再次进入运算c1,其目的是判断当前数组是否结束,然后再拿下一个索引的值进行相关的计算,并添加进StringBuffer中,每次循环完索引数组,off++,第一遍循环完off是等于3的,而添加进StringBuffer中是四次,注意末尾端的c1和c2添加;
private static final char[] base64_code = new char[]{'.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
继续往下走 看一下hashpw的底层实现:
public static String hashpw(String password, String salt) {
char minor = 0;
int off = false;
StringBuffer rs = new StringBuffer();
if (salt.charAt(0) == '$' && salt.charAt(1) == '2') {
byte off;
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 (salt.charAt(off + 2) > '$') {
throw new IllegalArgumentException("Missing salt rounds");
} else {
int rounds = Integer.parseInt(salt.substring(off, off + 2));
String real_salt = salt.substring(off + 3, off + 25);
byte[] passwordb;
try {
passwordb = (password + (minor >= 'a' ? "\u0000" : "")).getBytes("UTF-8");
} catch (UnsupportedEncodingException var12) {
throw new AssertionError("UTF-8 is not supported");
}
byte[] saltb = decode_base64(real_salt, 16);
BCrypt B = new BCrypt();
byte[] hashed = B.crypt_raw(passwordb, saltb, rounds, (int[])((int[])bf_crypt_ciphertext.clone()));
rs.append("$2");
if (minor >= 'a') {
rs.append(minor);
}
rs.append("$");
if (rounds < 10) {
rs.append("0");
}
if (rounds > 30) {
throw new IllegalArgumentException("rounds exceeds maximum (30)");
} else {
rs.append(Integer.toString(rounds));
rs.append("$");
rs.append(encode_base64(saltb, saltb.length));
rs.append(encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1));
return rs.toString();
}
}
} else {
throw new IllegalArgumentException("Invalid salt version");
}
} private static final int[] bf_crypt_ciphertext = new int[]{1332899944, 1700884034, 1701343084, 1684370003, 1668446532, 1869963892};进入会先判断相对应的salt是否符合,不符合报错,然后在进行判断salt的第二位,拿到第二位,赋值给上面的minor,并且判断是否符合,如果不符合报错,符合对off赋值为4,然后用off的值进行相加判断该位,不符合报错,符合的话进行位置的提取,分盐轮和真盐。然后将密码字节化,组成新的passwordb,然后将刚才的passwordb和拿到的盐的16个byte数组等进入crypt_raw()这个方法内;
public byte[] crypt_raw(byte[] password, byte[] salt, int log_rounds, int[] cdata) {
int clen = cdata.length;
if (log_rounds >= 4 && log_rounds <= 30) {
int rounds = 1 << log_rounds;
if (salt.length != 16) {
throw new IllegalArgumentException("Bad salt length");
} else {
this.init_key();
this.ekskey(salt, password);
int i;
for(i = 0; i != rounds; ++i) {
this.key(password);
this.key(salt);
}
int j;
for(i = 0; i < 64; ++i) {
for(j = 0; j < clen >> 1; ++j) {
this.encipher(cdata, j << 1);
}
}
byte[] ret = new byte[clen * 4];
i = 0;
for(j = 0; i < clen; ++i) {
ret[j++] = (byte)(cdata[i] >> 24 & 255);
ret[j++] = (byte)(cdata[i] >> 16 & 255);
ret[j++] = (byte)(cdata[i] >> 8 & 255);
ret[j++] = (byte)(cdata[i] & 255);
}
return ret;
}
} else {
throw new IllegalArgumentException("Bad number of rounds");
}
}这个过程比较繁琐,一些具体的循环运算,不做过多阐述,最终返回这个ret;再最后把这个返回的字节数组通过encode_base64这个方法得出后的结果存入StringBuffer中返回给用户;
即使黑客得到了bcrypt密码,他也无法转换明文,因为bcrypt是单向hash算法;
有文章指出bcrypt一个密码出来的时间比较长,需要0.3秒,而MD5只需要一微秒(百万分之一秒),一个40秒可以穷举得到明文的MD5,在bcrypt需要12年,时间成本太高。
在下次校验时,从myHash中取出salt,salt跟password进行hash;得到的结果跟保存在DB中的hash进行比对,如Spring Security框架中实现的Bcrypt密码验证Bcrypet。checkpw(candidatePassword,dpPassword);
总体来说,以上分析很拉垮,所以没有以下;