最も安全な暗号化アルゴリズムBcrypt、もうデータ漏洩の心配はありません〜

Golden Stone Project の最初のチャレンジに参加するためにサインアップしました - 100,000 の賞金プールを共有します。これは私の 136 回目の記事です。クリックしてイベントの詳細を表示します

「Spring Security Advanced」コラムの第3回目となる今回はBCrypt、最も安全な暗号化アルゴリズムとして知られるSpring Securityに組み込まれている暗号化アルゴリズムについて紹介します。

ハッシュと暗号化

ハッシュ (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()に計算され、次に 2 番目のハッシュが実行されます。コードは以下のように表示されます:

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

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

アルゴリズムは異なりますが、これらのシフト操作の後、同じ値に対して同じアルゴリズムが使用されている場合、計算されるハッシュ値は同じでなければならないことがわかります。

では、なぜハッシュは元に戻せないのでしょうか?

3 と 4 の 2 つのパスワードがある場合、私の暗号化アルゴリズムは非常に単純3+4です。結果は 7 ですが、7 までは 2 つのパスワードが 3 と 4 であると確信できません。多くの組み合わせがあり、これが最も単純な元に戻せません。そのため、力ずくで 1 つずつ試すしかありません。

元のテキストの情報の一部は、計算プロセス中に失われます。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 が暗号化されたハッシュ値encodedPassword と一致するかどうかを検証するために使用されます。一致する場合は 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 ランダムな塩を生成します (塩の効果により、料理の味が毎回異なります)。これは、エンコードのたびに異なる結果が生成されることを意味するため、重要です。

この記事が気に入ったら、いいねと転送して私をサポートしてください。ありがとうございます〜

おすすめ

転載: juejin.im/post/7143054506614489101