가장 안전한 암호화 알고리즘 Bcrypt, 더 이상 데이터 유출 걱정하지 마세요~

골든 스톤 프로젝트의 첫 번째 챌린지에 참여하기 위해 가입했습니다 - 100,000 상금 풀을 공유합니다. 이것은 136번째 글 입니다. 이벤트 세부 정보를 보려면 클릭하세요.

"Spring Security Advanced" 칼럼의 세 번째 글입니다. Spring Security에 내장된 암호화 알고리즘을 소개합니다. BCrypt가장 안전한 암호화 알고리즘으로 알려져 있습니다.

해시 및 암호화

Hash(해시)는 대상 텍스트를 동일한 길이의 비가역적인 해시 문자열(또는 메시지 다이제스트)로 변환하는 것이고, 암호화(Encrypt)는 대상 텍스트를 다른 길이의 가역적인 암호문으로 변환하는 것입니다.

  • 해시 알고리즘은 종종 동일한 길이의 텍스트를 생성하도록 설계되는 반면 암호화 알고리즘은 길이가 일반 텍스트 자체의 길이와 관련된 텍스트를 생성합니다.
  • 해시 알고리즘은 되돌릴 수 없지만 암호화 알고리즘은 되돌릴 수 있습니다.

HASH 알고리즘은 암호화 알고리즘이 아닌 메시지 다이제스트 알고리즘이지만 단방향 연산으로 인해 특정 비가역성을 가지며 암호화 알고리즘의 구성 요소가 됩니다.

JDK의 문자열 해시 알고리즘. 코드 쇼는 아래와 같습니다.

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;
}
复制代码

JDK API에서 알 수 있듯이 알고리즘 방정식은 s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1], 여기서 s[i]는 인덱스 i를 가진 문자이고 n은 문자열의 길이입니다.

HashMap의 해시 계산이 먼저 계산된 hashCode()다음 두 번째 해시가 수행됩니다. 코드 쇼는 아래와 같습니다.

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

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

알고리즘은 다르지만 이러한 시프트 연산 후에 같은 값에 대해 같은 알고리즘을 사용한다면 계산된 해시 값은 같아야 함을 알 수 있다.

그렇다면 해시가 되돌릴 수 없는 이유는 무엇입니까?

암호 3과 4가 두 개 있으면 내 암호화 알고리즘은 매우 간단 3+4하고 결과는 7이지만 7을 통해 두 암호가 3과 4인지 확신할 수 없으며 많은 조합이 있습니다. 이것이 가장 간단하고 되돌릴 수 없는 것입니다. 따라서 무차별 대입으로 하나씩 만 시도 할 수 있습니다.

계산 과정에서 원본 텍스트의 정보 중 일부가 손실됩니다. 이론적으로 MD5에는 유한 한 MD5수의 원본 텍스트와 무한한 수의 원본 텍스트가 있기 때문에 여러 원본 텍스트에 대응할 수 있습니다.

되돌릴 수 없는 MD5가 안전하지 않은 이유는 무엇입니까?

因为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;
   }
}
复制代码

这个接口有三个方法:

  • encode메서드에서 허용하는 매개변수는 원래 암호 문자열이고 반환 값은 암호화된 해시 값이며 해시 값은 역으로 해독할 수 없습니다. 이 방법은 일반적으로 시스템에 사용자를 추가하거나 사용자가 등록할 때 사용됩니다.
  • matches이 메서드는 사용자의 입력 암호 rawPassword가 암호화된 해시 값인codedPassword와 일치하는지 확인하는 데 사용됩니다. 일치할 수 있으면 true를 반환하여 사용자가 입력한 rawPassword 암호가 정확함을 나타내고 그렇지 않으면 fasle을 반환합니다. 즉, 이 해시 값은 역으로 복호화할 수는 없지만 원래 암호와 일치하는지 여부를 판단할 수 있습니다. 이 방법은 일반적으로 사용자가 로그인할 때 사용자가 입력한 암호의 정확성을 확인합니다.
  • upgradeEncoding디자인의 목적은 현재 암호를 업그레이드해야 하는지 여부를 결정하는 것입니다. 즉, 다시 암호화해야 합니까? 필요한 경우 true를 반환하고 필요하지 않은 경우 false를 반환합니다. 기본 구현은 false를 반환하는 것입니다.

예를 들어 다음 샘플 코드를 통해 사용자 등록 중에 사용자 암호를 암호화하고 저장할 수 있습니다.

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

BCryptPasswordEncoderSpring Security에서 권장하는 PasswordEncoder 인터페이스의 구현 클래스입니다.

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));
  }
}
复制代码

위의 테스트 케이스를 실행한 결과는 다음과 같다. (참고: 동일한 원본 비밀번호라도 암호화 후 해시 비밀번호가 다릅니다. 이것이 BCryptPasswordEncoder의 힘입니다. 크랙이 안될 뿐만 아니라 공통 비밀번호 비교표를 통해 건초 더미에서 바늘을 찾을 수 없습니다. .) , 출력은 다음과 같습니다.

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

BCrypt 임의의 소금을 생성합니다 (소금의 효과는 접시의 맛이 매번 다르다는 것입니다). 이는 인코딩할 때마다 다른 결과가 생성된다는 것을 의미하기 때문에 중요합니다.

이 글이 마음에 드셨다면 좋아요와 전달 부탁드립니다~ 감사합니다~

Acho que você gosta

Origin juejin.im/post/7143054506614489101
Recomendado
Clasificación