前情提要: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 次。

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

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

我目前心中預設的 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:

而在 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 大約等一秒即可,我覺得速度與安全性都可被接受:

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

SHA256 is too easy to be crack, so I improve it by adding IV salt and repeating hashing to be used for password hashing.


Comments

# by alvin

$6$w6aqmqpN$neHEEsegVSXPdXaEy8lfe2ygKJnX/tdxFltmG3PaI5zDbx4ggvyd5WELKCni2UXd1bZpGcJQ702f5tVZWi8hY0

# by alvin

解密

Post a comment