The most secure encryption algorithm Bcrypt, no longer have to worry about data leakage~

I signed up to participate in the first challenge of the Golden Stone Project - share the 100,000 prize pool, this is my 136th article, click to view the details of the event

This is the third article of the "Spring Security Advanced" column. I will introduce you to the encryption algorithm built in Spring Security. It is BCryptknown as the most secure encryption algorithm.

Hash and Encrypt

Hash (Hash) is to convert the target text into an irreversible hash string (or message digest) with the same length, and encryption (Encrypt) is to convert the target text into a different length, reversible ciphertext.

  • Hash algorithms are often designed to generate text of the same length, while encryption algorithms generate text whose length is related to the length of the plaintext itself.
  • Hash algorithms are irreversible, while encryption algorithms are reversible.

The HASH algorithm is a message digest algorithm, not an encryption algorithm, but due to its one-way operation, it has certain irreversibility and becomes a component of the encryption algorithm.

JDK's String Hash algorithm. code show as below:

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}
复制代码

As can be seen from the JDK API, its algorithm equation is s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1], where s[i]is the character with index i, and n is the length of the string.

The hash calculation of HashMap is calculated first hashCode(), and then the second hash is performed. code show as below:

// 计算二次Hash    
int hash = hash(key.hashCode());

static int hash(int h) {
	h ^= (h >>> 20) ^ (h >>> 12);
	return h ^ (h >>> 7) ^ (h >>> 4);
}
复制代码

It can be found that although the algorithms are different, after these shift operations, if the same algorithm is used for the same value, the calculated hash value must be the same.

So, why is hash irreversible?

If there are two passwords 3 and 4, my encryption algorithm is very simple 3+4, the result is 7, but through 7 I can't be sure that the two passwords are 3 and 4, there are many combinations, this is the simplest irreversible, so You can only try one by one by brute force.

Part of the information in the original text is lost during the calculation process. In theory, one MD5can correspond to multiple original texts, because MD5 has a finite number of original texts and an infinite number of original texts.

Why is irreversible MD5 unsafe?

因为hash算法是固定的,所以同一个字符串计算出来的hash串是固定的,所以,可以采用如下的方式进行破解。

  1. 暴力枚举法:简单粗暴地枚举出所有原文,并计算出它们的哈希值,看看哪个哈希值和给定的信息摘要一致。
  2. 字典法:黑客利用一个巨大的字典,存储尽可能多的原文和对应的哈希值。每次用给定的信息摘要查找字典,即可快速找到碰撞的结果。
  3. 彩虹表(rainbow)法:在字典法的基础上改进,以时间换空间。是现在破解哈希常用的办法。

对于单机来说,暴力枚举法的时间成本很高(以14位字母和数字的组合密码为例,共有1.24×10^25种可能,即使电脑每秒钟能进行10亿次运算,也需要4亿年才能破解),字典法的空间成本很高(仍以14位字母和数字的组合密码为例,生成的密码32位哈希串的对照表将占用5.7×10^14 TB的存储空间)。但是利用分布式计算和分布式存储,仍然可以有效破解MD5算法。因此这两种方法同样被黑客们广泛使用。

如何防御彩虹表的破解?

虽然彩虹表有着如此惊人的破解效率,但网站的安全人员仍然有办法防御彩虹表。最有效的方法就是“加盐”,即在密码的特定位置插入特定的字符串,这个特定字符串就是“盐(Salt)”,加盐后的密码经过哈希加密得到的哈希串与加盐前的哈希串完全不同,黑客用彩虹表得到的密码根本就不是真正的密码。即使黑客知道了“盐”的内容、加盐的位置,还需要对H函数和R函数进行修改,彩虹表也需要重新生成,因此加盐能大大增加利用彩虹表攻击的难度。

一个网站,如果加密算法和盐都泄露了,那针对性攻击依然是非常不安全的。因为同一个加密算法同一个盐加密后的字符串仍然还是一毛一样滴!

一个更难破解的加密算法Bcrypt

BCrypt是由Niels Provos和David Mazières设计的密码哈希函数,他是基于Blowfish密码而来的,并于1999年在USENIX上提出。

除了加盐来抵御rainbow table 攻击之外,bcrypt的一个非常重要的特征就是自适应性,可以保证加密的速度在一个特定的范围内,即使计算机的运算能力非常高,可以通过增加迭代次数的方式,使得加密速度变慢,从而可以抵御暴力搜索攻击。

Bcrypt可以简单理解为它内部自己实现了随机加盐处理。使用Bcrypt,每次加密后的密文是不一样的。

对一个密码,Bcrypt每次生成的hash都不一样,那么它是如何进行校验的?

  1. 虽然对同一个密码,每次生成的hash不一样,但是hash中包含了salt(hash产生过程:先随机生成salt,salt跟password进行hash);
  2. 在下次校验时,从hash中取出salt,salt跟password进行hash;得到的结果跟保存在DB中的hash进行比对。

在Spring Security 中 内置了Bcrypt加密算法,构建也很简单,代码如下:

@Bean
public PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();
}
复制代码

生成的加密字符串格式如下:

$2b$[cost]$[22 character salt][31 character hash]
复制代码

比如:

$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
\__/\/ \____________________/\_____________________________/
 Alg Cost      Salt                        Hash
复制代码

上面例子中,$2a$ 表示的hash算法的唯一标志。这里表示的是Bcrypt算法。

10 表示的是代价因子,这里是2的10次方,也就是1024轮。

N9qo8uLOickgx2ZMRZoMye 是16个字节(128bits)的salt经过base64编码得到的22长度的字符。

最后的IjZAgcfl7p92ldGxad68LJZdL17lhWy是24个字节(192bits)的hash,经过bash64的编码得到的31长度的字符。

PasswordEncoder 接口

这个接口是Spring Security 内置的,如下:

public interface PasswordEncoder {
   String encode(CharSequence rawPassword);

   boolean matches(CharSequence rawPassword, String encodedPassword);

   default boolean upgradeEncoding(String encodedPassword) {
      return false;
   }
}
复制代码

这个接口有三个方法:

  • encodeThe parameter accepted by the method is the original password string, and the return value is the encrypted hash value. The hash value cannot be reversely decrypted. This method is usually used when adding users to the system, or when users register.
  • matchesThe method is used to verify whether the user's input password rawPassword matches the encrypted hash value encodedPassword. If it can match, it returns true, indicating that the password rawPassword entered by the user is correct, otherwise it returns fasle. That is to say, although this hash value cannot be reversely decrypted, it can be judged whether it matches the original password. This method usually checks the correctness of the password entered by the user when the user logs in.
  • upgradeEncodingThe purpose of the design is to determine whether the current password needs to be upgraded. That is, does it need to be re-encrypted? Returns true if needed, false if not needed. The default implementation is to return false.

For example, we can encrypt and store user passwords during user registration through the following sample code

//将User保存到数据库表,该表包含password列
user.setPassword(passwordEncoder.encode(user.getPassword()));
复制代码

BCryptPasswordEncoderIt is the implementation class of the PasswordEncoder interface recommended by Spring Security

public class PasswordEncoderTest {
  @Test
  void bCryptPasswordTest(){
    PasswordEncoder passwordEncoder =  new BCryptPasswordEncoder();
    String rawPassword = "123456";  //原始密码
    String encodedPassword = passwordEncoder.encode(rawPassword); //加密后的密码

    System.out.println("原始密码" + rawPassword);
    System.out.println("加密之后的hash密码:" + encodedPassword);

    System.out.println(rawPassword + "是否匹配" + encodedPassword + ":"   //密码校验:true
            + passwordEncoder.matches(rawPassword, encodedPassword));

    System.out.println("654321是否匹配" + encodedPassword + ":"   //定义一个错误的密码进行校验:false
            + passwordEncoder.matches("654321", encodedPassword));
  }
}
复制代码

The result of the execution of the above test case is as follows. (Note: For the same original password, the hash password after each encryption is different. This is the power of BCryptPasswordEncoder. Not only can it not be cracked, but you can’t find a needle in a haystack through the common password comparison table.) , the output is as follows:

原始密码123456
加密之后的hash密码:$2a$10$zt6dUMTjNSyzINTGyiAgluna3mPm7qdgl26vj4tFpsFO6WlK5lXNm
123456是否匹配$2a$10$zt6dUMTjNSyzINTGyiAgluna3mPm7qdgl26vj4tFpsFO6WlK5lXNm:true
654321是否匹配$2a$10$zt6dUMTjNSyzINTGyiAgluna3mPm7qdgl26vj4tFpsFO6WlK5lXNm:false
复制代码

BCrypt Generate random salt (the effect of salt is that each time the dish tastes different). This is important because it means that each time the encode will produce a different result.

If you like this article, please like and forward it to support me, thank you~

Guess you like

Origin juejin.im/post/7143054506614489101