開會討論到一個系統需求,要從大量資料標註出特定類別,例如:呼叫端提交一百萬筆帳號,系統由資料庫查詢屬於該類別的帳號清單(假設有五萬筆),API 服務要在標記出這五萬筆並傳回結果。

由於帳號具敏感性,為進一步降低資訊外洩風險,既然只要比對相符,我提議傳入帳號雜湊值取代帳號本身,感覺一定比傳明碼安全。不過,馬上被問了一句:「這樣速度會變慢很多嗎?」

依前陣子玩 SHA 暴力破解的經驗,SHA256 甚至 SHA512 的運算速度遠比想像快,執行速度上不用擔心。但沒有數據佐證難以服人,提議之人有舉證之責,就來做個實驗吧。

我設計一個實驗如下,用迴圈產生一百萬筆來源資料,帳號文字用 string GenSimuValue(int i) => $"{i:X4}{i:000000}"; 模擬,第 9999 筆為 270F009999,再用 270F 當資料序號,其檔案範例如下:

若轉為 SHA256,會像這樣:

第一個效能問題是資料量變大,傳輸速度變慢,一百萬筆明文版為 18,870,272 Bytes、SHA256 版為 51,937,280 Bytes,增加 33MB 左右,若未壓縮傳輸,以內網 100Mbps 傳送將增加 2.5 秒。若經過壓縮,差距縮小為 19MB (模擬資料用流水號,明文碼壓縮比較高,實際資料的差異會更小一點),增加的傳輸資料時間抓 1.6 秒。(樂觀值)

接下來我用一段程式測試一百萬筆明文版跟 SHA256 版雜湊版的資料產生與比對效能:(測速部分用之前發明的極簡風格 .NET Stopwatch 計時法)

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

const int DATA_COUNT = 1_000_000;
const int TARGET_COUNT = 50_000;
var sha = SHA256.Create();

//https://blog.darkthread.net/blog/handy-stopwatch-library/
using (var ts = new TimeMeasureScope("GenRawData")) {
    GenRaw("data", DATA_COUNT);
}

using (var ts = new TimeMeasureScope("GenRawTarget")) {
    GenRaw("target", TARGET_COUNT);
}

using (var ts = new TimeMeasureScope("FindRaw")) 
{
    var data = File.ReadAllLines(Path.Combine("data", "raw.txt"))
        .Select(x => x.Split(','))
        .Select(o => new { SeqNo = o[0], Data = o[1]});
    var target = File.ReadAllLines(Path.Combine("target", "raw.txt"))
        .Select(x => x.Split(',').Last())
        .ToHashSet<string>();
    using var sw = new StreamWriter(Path.Combine("data", "result-raw.txt"));
    foreach (var item in data) 
    {
        sw.WriteLine($"{item.SeqNo},{(target.Contains(item.Data) ? "Y" : "N")}");
    }
}

using (var ts = new TimeMeasureScope("GenHashData")) {
    GenHash("data", DATA_COUNT);
}

using (var ts = new TimeMeasureScope("FindHash")) {
    var data = File.ReadAllLines(Path.Combine("data", "hash.txt"))
        .Select(x => x.Split(','))
        .Select(o => new { SeqNo = o[0], Hash = o[1]});
    var target = File.ReadAllLines(Path.Combine("target", "raw.txt"))
        .Select(x => x.Split(',').Last())
        .Select(x => GenShaHash(x))
        .ToHashSet<string>();
    using var sw = new StreamWriter(Path.Combine("data", "result-hash.txt"));
    foreach (var item in data) 
    {
        sw.WriteLine($"{item.SeqNo},{(target.Contains(item.Hash) ? "Y" : "N")}");
    }        
}

//比較結果
var hashResult = File.ReadAllLines(Path.Combine("data", "result-hash.txt"));
var rawResult = File.ReadAllLines(Path.Combine("data", "result-raw.txt"));
if (hashResult.Length == rawResult.Length) 
{
    var diff = hashResult.Zip(rawResult, 
        (h, r) => h == r ? null : $"{h} != {r}")
        .Where(x => x != null).ToArray();
    if (diff.Any())
    {
        Console.WriteLine("資料不一致");
        foreach (var item in diff) Console.WriteLine(item);
    }
    else 
        Console.WriteLine("資料完全相同");
}
else 
    Console.WriteLine("資料筆數不同");

//TODO: 若要提高安全強度,每次執行雙方可約定隨機產生的 Salt 串接明文字串,讓相同值每次產生的雜湊結果不同
string GenShaHash(string s) => 
    Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(s)));
string GenSimuValue(int i) => $"{i:X4}{i:000000}";

void GenHash(string folder, int count)
{
    using var sw = new StreamWriter(Path.Combine(folder, "hash.txt"));
    foreach (int i in Enumerable.Range(0, count)) 
        sw.WriteLine($"{i:X4},{GenShaHash(GenSimuValue(i))}");
}

void GenRaw(string folder, int count)
{
    using var sw = new StreamWriter(Path.Combine(folder, "raw.txt"));
    foreach (int i in Enumerable.Range(0, count)) 
        sw.WriteLine($"{i:X4},{GenSimuValue(i)}");
}

實測結果,明文版比對 330ms,SHA256 版 482ms,以一百萬筆來說,差異可以無視,實際資料量預估只會有幾十萬筆,比較部分的差異應該更小。

小結,由明碼改為 SHA256 比對,雜湊計算部分增加運算負擔可忽略,變慢主因來自傳輸資料量變大,於內網 100M/1G 網路傳送,若每天只執行數次,幾秒的差異亦可無視。此一構想可行!

Simple experiment to meanure performance difference between comparing plain-text vs comparing SHA256 hash.


Comments

Be the first to post a comment

Post a comment