6

Coding4Fun - SHA 密碼雜湊攻防 / 加鹽翻炒版

 1 year ago
source link: https://blog.darkthread.net/blog/iv-salted-sha-hash/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Coding4Fun - SHA 密碼雜湊攻防 / 加鹽翻炒版-黑暗執行緒

前情提要:SCH (Self-Contained Html) 單檔 HTML 文件提供密碼保護功能,做法是「用密碼字串 SHA256 雜湊當金鑰加隨機 IV 對內容做 AES256 加密」,但因解密會在瀏覽器執行,JavaScript 端解密邏輯是公開的祕密,有心人寫支程式就能暴力破解。

而我自己寫了簡單程式實測暴力破解,發現這套密碼字串 SHA256 直接轉金鑰的做法,若遇上強度不夠的密碼,連我這樣的非專業人士用一台普通的 i5 PC 就能破解,10 碼純數字、5 碼英數字加符號的密碼,只需兩天就能被解開。

在密碼雜湊的專業領域,已有 PBKDF2、Scrypt、Bcrypt、Argon2... 等高階密碼專用雜湊演算法 (延伸閱讀:密碼要怎麼儲存才安全?該加多少鹽?-科普角度),但由於 SCH 解密需在瀏覽器端實作,專用演算法得依賴第三方程式庫,目前 GZIP 解壓跟 SHA 雜湊都靠瀏覽器原生 API 解決,我想堅守香草精神 (Vanilla JavaScript),看能不能在 SHA256 加點簡單變化,就讓破解難度驟升。

以下是我想出的加鹽改良版密碼 SHA 雜湊演算法:

thumbnail

照片來源:olaycekirg@Twitter

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

public class CodecNetFx
{
    public static int PasswdHashComplexity = 2048;
    private class AesKeyIV
    {
        public Byte[] Key = new Byte[32];
        public Byte[] IV = new Byte[16];
        public AesKeyIV(string strKey, byte[] iv = null!)
        {
            var sha = SHA512.Create();
            var raw = Encoding.ASCII.GetBytes(strKey);
            if (iv == null) new Random().NextBytes(IV);
            else Array.Copy(iv, IV, 16);
            var hash = sha.ComputeHash(raw);
            for (var i = 0; i < PasswdHashComplexity; i++)
            {
                hash[i % hash.Length] ^= IV[i % IV.Length];
                byte[] buff = new byte[hash.Length * PasswdHashComplexity];
                for (var j = 0; j < PasswdHashComplexity; j++)
                    Array.Copy(hash, 0, buff, j * hash.Length, hash.Length);
                hash = sha.ComputeHash(buff);
            }
            Array.Copy(hash, 0, Key, 0, 32);
        }
    }
    public static (byte[] data, byte[] iv) AesEncrypt(string key, byte[] data)
    {
        //...略....
    }

    public static byte[] AesDecrypt(string key, byte[] data, byte[] iv)
    {
        var keyIv = new AesKeyIV(key, iv);
        //...略...
    }
}

在原本的版本加入幾點強化:

  1. 核心雜湊由 SHA256 改為 SHA512,計算再耗時一些
  2. 加入 PasswdHashComplexity 因子,為一正整數,決定雜湊演算的反覆次數及計算資料量
  3. 原本密碼字串只做一次 SHA256 得到結果,現在要用迴圈反覆做 PasswdHashComplexity 次
    而迴圈中計算雜湊的對象擴大為 byte[64 * PasswdHashComplexity],資料內容則是前次 SHA512 結果重複 PasswdHashComplexity 次
  4. 為避免彩虹表之類攻擊手法,解密時會將 IV byte[16] 當成鹽混入雜湊計算過程,做法是每次取出一個 Byte 對前次雜湊結果的某個 Byte 做 XOR,取用及 XOR 對象位址由迴圈次數取餘數決定。

經過這番修改,可實現 PasswdHashComplexity 愈大,計算愈耗時(迴圈次數增加、要雜湊的資料量增加)的效果。當 PasswdHashComplexity = 2048,雜湊對象為 128KB,要做 2048 次。

Fig1_638214593657908817.png

實測簡單到靠北的 3 碼純數字,原本只要 72ms 就能破解,當 PasswdHashComplexity = 64 會增加到 178ms,256 為 1.9s,1024 為 29s,2048 為 117s。

SCH 預設密碼位數下限為 6 碼,測試結果,原本需 25s,PasswdHashComplexity = 16 會上升到 31s,64 是 156s,256 的測試我沒耐心等,也不想軟燒我的 CPU,以 88s 完成 5% 推測大約需要近半小時。

Fig2_638214593660344887.png

我目前心中預設的 PasswdHashComplexity 會抓 2048,用跑 1100 次花 116,672ms 估算跑一次約 100ms (0.1秒),以四碼英數字(不含符號)組合量為標準,62^4 = 14,776,336,換算要 410 小時;若加長到六碼,則需 1,577,784 小時,大約是 180 年,我覺得夠了。

若不放心,還可將 PasswdHashComplexity 加碼到 4096、8192,破解難度就更高了。用 3 碼及 2 碼純數字測 4096 跟 8192,4096 算一次要 0.42 秒、8192 每次要 1.62s:

Fig3_638214593662267954.png

而在 JavaScript 端,我用瀏覽器內建 API 實做相同邏輯成功解密,不需依賴第三方程式庫,保留了香草的純粹滋味。

const schMode = document.head.querySelector('meta[name="SCH-Mode"]').content;
async function createCryptoKey(key, keyUsage) {
    const pwdData = schMode.split('-');
    const pwdHashComplexity = pwdData[1];
    let iv = new Uint8Array(atob(pwdData[2]).split('').map(c => c.charCodeAt(0)));

    let hash = new Uint8Array(await window.crypto.subtle.digest("SHA-512", new TextEncoder().encode(key)));
    for (let i = 0; i < pwdHashComplexity; i++) {
        hash[i % hash.length] ^= iv[i % iv.length];
        let buff = new Uint8Array(hash.length * pwdHashComplexity);
        for (let j = 0; j < pwdHashComplexity; j++) {
            buff.set(hash, j * hash.length);
        }
        hash = new Uint8Array(await window.crypto.subtle.digest("SHA-512", buff));
    }
    const cryptoKey = await window.crypto.subtle.importKey(
        "raw", new Uint8Array(hash.slice(0, 32)), { name: "AES-CBC" }, false, [keyUsage]
    );
    return { cryptoKey, ivPart: iv };
}

不過,JavaScript 端的 SHA512 運算較慢,雜湊複雜度設 8192 有些吃力,輸入密碼得等 8 秒才有結果(如下圖),逼得我還加了解鎖動畫請使用者耐心等待。若用預設值 2048,在我的 12 代 i5 大約等一秒即可,我覺得速度與安全性都可被接受:

Fig5_638214593668719640.gif

當然,密碼複雜度還是最大關鍵,密碼雜湊演算再強大,遇上 "1234"、"password" 一樣白搭。但我想 SCH 使用這套 IV 加鹽反覆 SHA512 雜湊,PasswdHashComplexity 取 2048 密碼限六碼以上,應該夠用。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK