存儲用戶密碼應(yīng)該使用什么加密算法?

概述

編程開發(fā)中,像用戶登錄注冊這種功能很常見,那么對于用戶密碼處理,我們該選擇什么樣的加密算法呢?在這種場景下,算法需要滿足下面兩個條件:

  • 算法需不可逆,這樣才能有效防止密碼泄露。
  • 算法需相對慢,可以動態(tài)調(diào)整計算成本,緩慢是應(yīng)對暴力破解有效方式。

目前來看有這么幾個算法 PBKDF2、 BCryptSCrypt 可以滿足。我們先看下舊的密碼加密方式。

舊的加密

過去密碼加密常用MD5或者SHA。MD5是早期設(shè)計的加密哈希,它生成哈希速度很快,隨著計算機能力的增強,出現(xiàn)了被破解的情況,所以又有了一些長度增大的哈希函數(shù),如:SHA-1,SHA-256等。下面是它們的一些比較:

  • MD5:速度快生成短哈希(16 字節(jié))。意外碰撞的概率約為:1.47 \times 10^{-29}

  • SHA1:比 md5 慢 20%,生成的哈希比 MD5 長一點(20 字節(jié))。意外碰撞的概率約為:1 \times 10^{-45}。

  • SHA256:最慢,通常比 md5 慢 60%,并且生成的哈希長(32 字節(jié))。意外碰撞的概率約為:4.3 \times 10^{-60} 。

為了確保安全你可能會選擇目前長度最長的哈希SHA-512,但硬件能力在增強,或許有一天又會發(fā)現(xiàn)新的漏洞,研究人員又推出較新的版本,新版本的長度也會越來越長,而且他們也可能會發(fā)布底層算法,所以我們應(yīng)該另外尋找更合適的算法。

加鹽操作

密碼安全,除了要選擇足夠可靠的加密算法外,輸入數(shù)據(jù)的強度也要提升,因為密碼是人設(shè)置的,其字符長度組合強度不可能一致,如果直接進行哈希存儲往往會提升爆破的概率,這時我們需要加鹽。

加鹽是密碼學(xué)中經(jīng)常提到的概念,其實就是隨機數(shù)據(jù)。下面是一個 java 中生成鹽的例子:

    public static byte[] generateSalt() {
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[16];
        random.nextBytes(salt);
        return salt;
    }

SHA-512 加鹽哈希密碼

    public static String sha512(String rawPassword, byte[] salt) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-512");
            // 加點鹽
            md.update(salt);
            return Hex.encodeHexString(md.digest(rawPassword.getBytes(StandardCharsets.UTF_8)));
        } catch (GeneralSecurityException ex) {
            throw new IllegalStateException("Could not create hash", ex);
        }
    }

PBKDF2

PBKDF1PBKDF2是一個密鑰派生函數(shù),其作用就是根據(jù)指定的密碼短語生成加密密鑰。之前在 常見加密算法 提到過。它雖然不是加密哈希函數(shù),但它仍然適用密碼存儲場景,因為它有足夠的安全性,PBKDF2 函數(shù)計算如下:
DK = PBKDF2(PRF, Password, Salt, Iterations, HashWidth)

  • PRF 是偽隨機函數(shù)兩個參數(shù),輸出固定的長度(例如,HMAC);
  • Password 是生成派生密鑰的主密碼;
  • Salt 是加密鹽;
  • Iterations 是迭代次數(shù),次數(shù)越多;
  • HashWidth 是派生密鑰的長度;
  • DK 是生成的派生密鑰。

PRF(HMAC)大致迭代過程,第一次時將 Password 作為密鑰和Salt傳入,然后再將輸出結(jié)果作為輸入重復(fù)完成后面迭代。

PBKDF2

HMAC:基于哈希的消息認證碼,可以使用共享密鑰提供身份驗證。比如HMAC-SHA256,輸入需要認證的消息密鑰進行計算,然后輸出sha256的哈希值。

PBKDF2不同于MD和SHA哈希函數(shù),它通過增加迭代次數(shù)提升了破解難度,并且還可以根據(jù)情況進行配置,這使得它具有滑動計算成本。

對于MD5和SHA,攻擊者每秒可以猜測數(shù)十億個密碼。而使用 PBKDF2,攻擊者每秒只能進行幾千次猜測(或更少,取決于配置),所以它適用于抗擊暴力攻擊。

2021 年,OWASP 建議對 PBKDF2-HMAC-SHA256 使用 310000 次迭代,對 PBKDF2-HMAC-SHA512 使用 120000 次迭代

    public static String pbkdf2Encode(String rawPassword, byte[] salt) {
        try {
            int iterations = 310000;
            int hashWidth = 256;
            PBEKeySpec spec = new PBEKeySpec(rawPassword.toCharArray(), salt, iterations, hashWidth);
            SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
            return Base64.getEncoder().encodeToString(skf.generateSecret(spec).getEncoded());
        } catch (GeneralSecurityException ex) {
            throw new IllegalStateException("Could not create hash", ex);
        }
    }

Bcrypt

簡介

bcrypt 是基于 eksblowfish 算法設(shè)計的加密哈希函數(shù),它最大的特點是:可以動態(tài)調(diào)整工作因子(迭代次數(shù))來調(diào)整計算速度,因此就算以后計算機能力不斷增加,它仍然可以抵抗暴力攻擊。

關(guān)于eksblowfish算法,它是采用分組加密模式,并且支持動態(tài)設(shè)定密鑰計算成本(迭代次數(shù))。算法的詳細介紹可查看下面文章:

https://www.usenix.org/legacy/publications/library/proceedings/usenix99/full_papers/provos/provos_html/node4.html

結(jié)構(gòu)

bcrypt 函數(shù)輸入的密碼字符串不超過 72 個字節(jié)、包含算法標(biāo)識符、一個計算成本和一個 16 字節(jié)(128 位)的鹽值。通過輸入計算得到 24字節(jié)(192位)哈希,最終輸出格式如下:

$2a$12$DQoa2eT/aXFPgIoGwfllHuj4wEA3F71WWT7E/Trez331HGDUSRvXi
\__/\/ \____________________/\_____________________________/
Alg Cost      Salt                        Hash
  • $2a$: bcrypt 算法標(biāo)識符或叫版本;
  • 12: 工作因子 (2^12 表示 4096 次迭代)
  • DQoa2eT/aXFPgIoGwfllHu: base64 的鹽值;
  • j4wEA3F71WWT7E/Trez331HGDUSRvXi: 計算后的 Base64 哈希值(24 字節(jié))。

bcrypt 版本

  • $2a$: 規(guī)定哈希字符串必須是 UTF-8 編碼,必須包含空終止符。
  • $2y$: 該版本為修復(fù) 2011年6月 PHP 在 bcrypt 實現(xiàn)中的一個錯誤。
  • $2b$: 該版本為修復(fù) 2014年2月 OpenBSD 在 bcrypt 實現(xiàn)中的一個錯誤。

2014年2月 在 OpenBSD 的 bcrypt 實現(xiàn)中發(fā)現(xiàn),它使用一個無符號的 8 位值來保存密碼的長度。對于長度超過255字節(jié)的密碼,密碼將在72或長度模256 中的較小者處被截斷,而不是被截斷為72字節(jié)。例如:260 字節(jié)的密碼將被截斷為4個字節(jié),而不是截斷為 72 個字節(jié)。

實踐

bcrypt 關(guān)鍵在于設(shè)定合適的工作因子,理想的工作因子沒有特定的法則,主要還是取決于服務(wù)器的性能和應(yīng)用程序上的用戶數(shù)量,一般在安全性應(yīng)用性能之間權(quán)衡設(shè)定。

假如你的因子設(shè)置比較高,雖然可以保證攻擊者難以破解哈希,但是登錄驗證也會變慢,嚴重影響用戶體驗,而且也可能被攻擊者通過大量登錄嘗試耗盡服務(wù)器的 CPU 來執(zhí)行拒絕服務(wù)攻擊。一般來說計算哈希的時間不應(yīng)該超過一秒。

我們使用 spring security BCryptPasswordEncoder 看下不同因子下產(chǎn)生哈希的時間,我電腦配置如下:

處理器:2.2 GHz 四核Intel Core i7
內(nèi)存:16 GB 1600 MHz DDR3
顯卡:Intel Iris Pro 1536 MB

Map<Integer, BCryptPasswordEncoder> encoderMap = new LinkedHashMap<>();
        for (int i = 8; i <= 21; i++) {
            encoderMap.put(i, new BCryptPasswordEncoder(i));
        }
        String plainTextPassword = "huhdfJ*!4";
        for (int i : encoderMap.keySet()) {
            BCryptPasswordEncoder encoder = encoderMap.get(i);
            long start = System.currentTimeMillis();
            encoder.encode(plainTextPassword);
            long end = System.currentTimeMillis();
            System.out.println(String.format("bcrypt | cost: %d, time : %dms", i, end - start));
        }
bcrypt | cost: 8, time : 39ms
bcrypt | cost: 9, time : 45ms
bcrypt | cost: 10, time : 89ms
bcrypt | cost: 11, time : 195ms
bcrypt | cost: 12, time : 376ms
bcrypt | cost: 13, time : 720ms
bcrypt | cost: 14, time : 1430ms
bcrypt | cost: 15, time : 2809ms
bcrypt | cost: 16, time : 5351ms
bcrypt | cost: 17, time : 10737ms
bcrypt | cost: 18, time : 21417ms
bcrypt | cost: 19, time : 43789ms
bcrypt | cost: 20, time : 88723ms
bcrypt | cost: 21, time : 176704ms

擬合得到以下公式:
10.3064 \cdot e^{0.696464 x}

bcrypt

BCryptPasswordEncoder 因子范圍在 4-31 ,默認是 10,我們根據(jù)公式推導(dǎo)一下 31時需要多長時間。

    /**
     * @param strength the log rounds to use, between 4 and 31
     */
    public BCryptPasswordEncoder(int strength) {
        this(strength, null);
    }

10.3064 \cdot e^{0.696464(31)} = 24529665567.08815

工作因子 31 時大概需要 284 天,所以我們知道使用 bcrypt 可以很容易的擴展哈希計算過程以適應(yīng)更快的硬件,為我們留出很大的回旋余地,以防止攻擊者從未來的技術(shù)改進中受益。

SCrypt

SCrypt 比上面提到的算法出來較晚,是Colin Percival于 2009 年 3 月創(chuàng)建的基于密碼的密鑰派生函數(shù)。關(guān)于該算法我們需要明白下面兩點:

  • 該算法專門設(shè)計用于通過需要大量內(nèi)存來執(zhí)行大規(guī)模自定義硬件攻擊,成本高昂。
  • 它屬于密鑰派生函數(shù)和上面提到 PBKDF2 屬于同一類別。

Spring security 也實現(xiàn)該算法 SCryptPasswordEncoder ,輸入?yún)?shù)如下:

  • CpuCost: 算法的 cpu 成本。 必須是大于 1 的 2 的冪。默認當(dāng)前為 16,384 或 2^14)
  • MemoryCost: 算法的內(nèi)存成本。默認當(dāng)前為 8。
  • Parallelization: 算法的并行化當(dāng)前默認為 1。請注意,該實現(xiàn)當(dāng)前不利用并行化。
  • KeyLength: 算法的密鑰長度。 當(dāng)前默認值為 32。
  • SaltLength: 鹽長度。 當(dāng)前默認值為 64。

不過也有人提到,并不建議在生產(chǎn)系統(tǒng)中使用它來存儲密碼,他的結(jié)論是首先 SCrypt 設(shè)計目的是密鑰派生函數(shù)而不是加密哈希,另外它實現(xiàn)上也并不那么完美。詳細可查看下面文章。

https://blog.ircmaxell.com/2014/03/why-i-dont-recommend-scrypt.html

結(jié)論

我會推薦使用 bcrypt。為什么是 bcrypt 呢?

密碼存儲這種場景下,將密碼哈希處理是最好的方式,第一它本身就是加密哈希函數(shù),其次按照摩爾定律的定義,集成系統(tǒng)上每平方英寸的晶體管數(shù)量大約每 18 個月翻一番。在 2 年內(nèi),我們可以增加它的工作因子以適應(yīng)任何變化。

當(dāng)然這并不是說其它算法不夠安全,你仍然可以選擇其它算法。建議優(yōu)先使用 bcrypt,其次是密鑰派生類(PBKDF2 和 SCrypt),最后是哈希+鹽(SHA256(salt))。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容