Luke a Pro

Luke Sun

Developer & Marketer

🇺🇦
EN||

雜湊函式到底是不是加密

| , 5 minutes reading.

1. 為什麼要關心這個問題?

一個開發者這樣儲存使用者密碼:

hashed = sha256(password)
database.store(hashed)

「這很安全,」他們說。「我用的是 SHA-256,一個強大的雜湊函式。」

然後他們的資料庫洩漏了。幾小時內,攻擊者恢復了 80% 的密碼。

哪裡出錯了?

這個開發者把「雜湊」和「安全密碼儲存」混淆了。這兩者不是一回事。SHA-256 是一個雜湊函式,不是密碼儲存解決方案。直接用它來存密碼就像用錘子當螺絲起子——用錯了工具。

2. 定義

雜湊函式接受任意大小的輸入,產生固定大小的輸出(「雜湊值」或「摘要」)。它被設計成單向的:你可以從輸入計算雜湊,但無法從雜湊計算出輸入。

密碼學雜湊函式的關鍵特性:

  • 確定性: 相同輸入總是產生相同輸出
  • 固定輸出大小: SHA-256 總是輸出 256 位元,無論輸入多大
  • 單向性: 計算上不可逆
  • 抗碰撞性: 難以找到兩個產生相同雜湊的不同輸入
  • 雪崩效應: 輸入的微小變化導致輸出劇烈變化

3. 根本區別

加密:設計上雙向

明文 ──[用金鑰加密]──► 密文 ──[用金鑰解密]──► 明文

加密是可逆的。有了金鑰,你總能恢復原始資料。

雜湊:設計上單向

輸入 ──[雜湊函式]──► 雜湊值

    (無法返回)

雜湊是不可逆的。沒有金鑰。沒有解密。原始資料在數學上被銷毀了——只留下一個指紋。

為什麼會混淆?

兩者都從可讀的輸入產生「亂碼輸出」。但目的完全不同:

特性加密雜湊
目的臨時隱藏資料永久建立指紋
可逆是(有金鑰)
需要金鑰
輸出大小隨輸入變化固定
使用場景保護傳輸/儲存中的資料驗證完整性,儲存密碼

4. 雜湊函式如何運作

高層流程

┌─────────────────────────────────────────────────────────────┐
│ 1. 填充                                                     │
│    - 添加位元使輸入成為區塊大小的整數倍                     │
├─────────────────────────────────────────────────────────────┤
│ 2. 分塊處理                                                 │
│    - 分割成固定大小的區塊                                   │
│    - 每個區塊通過壓縮函式處理                               │
│    - 每個區塊的輸出饋入下一個區塊                           │
├─────────────────────────────────────────────────────────────┤
│ 3. 最終化                                                   │
│    - 輸出最終內部狀態作為雜湊值                             │
└─────────────────────────────────────────────────────────────┘

雪崩效應

這是雜湊對完整性檢查有用的原因:

import hashlib

text1 = "Hello, World!"
text2 = "Hello, World."  # 只是把 ! 改成了 .

print(hashlib.sha256(text1.encode()).hexdigest())
# dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f

print(hashlib.sha256(text2.encode()).hexdigest())
# f8c3bf62a9aa3e6fc1619c250e48abe7519373d3edf41be62eb5dc45199af2ef

一個字元的變化 → 完全不同的雜湊。這使得「猜測」原始輸入變得不可能。

5. 為什麼你無法「解密」雜湊

資訊遺失

雜湊函式將任意長度的輸入壓縮成固定長度的輸出。資訊在數學上遺失了。

"Hello"(5 位元組)       → 256 位元雜湊
"戰爭與和平"(3MB)       → 256 位元雜湊
所有可能的檔案            → 256 位元雜湊

無限的輸入映射到有限的輸出。多個輸入會產生相同的雜湊(碰撞)。你無法逆轉這個過程,因為你不知道使用了無限可能輸入中的哪一個。

沒有金鑰,就沒有解密

加密在沒有金鑰時是安全的,因為找到金鑰在計算上不可行。

雜湊沒有金鑰。沒有什麼可找的。「逆轉」需要反轉一個被設計成不可逆的數學函式。

「破解」雜湊意味著什麼

當我們說雜湊被「破解」時,我們指的是:

  • 碰撞攻擊: 找到了兩個具有相同雜湊的不同輸入
  • 原像攻擊: 給定一個雜湊,找到某個產生它的輸入(不一定是原始的)

兩者都不意味著「解密」。即使是被破解的雜湊函式也不會變得可逆。

6. MD5:為什麼死而不僵

MD5 是 1991 年設計的。它從 2004 年就被「破解」了。但你仍然到處能看到它。

為什麼 MD5 是破碎的

  1. 碰撞攻擊是實用的: 你可以建立兩個具有相同 MD5 雜湊的不同檔案
  2. 選定前綴攻擊有效: 給定任意兩個前綴,你可以向每個追加資料,使結果具有相同的雜湊
  3. 它太快了: 現代 GPU 每秒 95 億個 MD5 雜湊

為什麼 MD5 死而不僵

# 在生產系統中仍然能看到:
file_checksum = hashlib.md5(file_content).hexdigest()  # "只是用於完整性"
cache_key = hashlib.md5(query).hexdigest()  # "只是用於快取鍵"

人們辯稱:「我不是用它來做安全,只是校驗和。」

問題在於: 需求會變。今天的「只是校驗和」會變成明天的安全控制。而且 MD5 太快了,即使非安全用途也會啟用攻擊。

MD5 實際上沒問題的場景

  • 比較你完全控制的檔案
  • 封閉系統中的非安全校驗和
  • 遺留系統相容性(完全了解風險的情況下)

MD5 不行的場景

  • 任何安全敏感的應用
  • 面向使用者的檔案驗證
  • 密碼雜湊(絕對不行!)
  • 數位簽章
  • 憑證驗證

7. 密碼儲存:正確的雜湊方式

以下是為什麼 sha256(password) 會失敗:

問題 1:速度

SHA-256 被設計成很快。非常快。

SHA-256:     ~85 億次雜湊/秒 (GPU)
bcrypt:      ~71,000 次雜湊/秒 (同樣的 GPU)
Argon2:      ~1,000 次雜湊/秒 (同樣的 GPU,經過調優)

快速雜湊意味著快速破解。一個 8 字元的密碼大約有 6 千萬億種可能。以每秒 85 億次的速度,全部嘗試需要 8 天。

問題 2:沒有鹽

沒有鹽,相同的密碼有相同的雜湊。

資料庫洩漏:
user1: 5e884898da28047d9...  ← "password"
user2: 5e884898da28047d9...  ← 也是 "password"
user3: 5e884898da28047d9...  ← 也是 "password"

攻擊者預先計算常見密碼的雜湊(彩虹表)。一次查找,數千個帳戶被攻破。

問題 3:彩虹表

預計算的表,將常見密碼映射到它們的雜湊。僅使用 SHA-256,一個 10GB 的彩虹表可以即時破解大多數弱密碼。

解決方案:密碼雜湊函式

import bcrypt
import argon2

# bcrypt:久經考驗,廣泛支援
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))

# argon2:密碼雜湊競賽的現代贏家
ph = argon2.PasswordHasher(
    time_cost=2,
    memory_cost=102400,  # 100 MB
    parallelism=8
)
hashed = ph.hash(password)

這些函式是:

  • 故意慢: 可配置的工作因子
  • 記憶體密集: (Argon2)需要大量 RAM,擊敗 GPU 攻擊
  • 自動加鹽: 即使密碼相同,每個雜湊也是唯一的

正確的密碼儲存流程

註冊:
password → [鹽 + 慢雜湊] → stored_hash

驗證:
input_password + stored_salt → [相同的慢雜湊] → 與 stored_hash 比較

8. 雜湊函式選擇指南

使用場景推薦避免
密碼儲存Argon2id, bcrypt, scryptSHA-*, MD5
檔案完整性SHA-256, SHA-3, BLAKE3MD5, SHA-1
數位簽章SHA-256, SHA-3MD5, SHA-1
HMACSHA-256, SHA-3MD5
非安全校驗和CRC32, xxHash(都可以)
內容定址儲存SHA-256, BLAKE3MD5

9. 程式碼範例:正確的密碼處理

import argon2
from argon2 import PasswordHasher, exceptions

# 配置:根據你的伺服器能力調整
ph = PasswordHasher(
    time_cost=2,      # 迭代次數
    memory_cost=65536, # 64 MB 記憶體使用
    parallelism=4,     # 平行執行緒數
    hash_len=32,       # 輸出雜湊長度
    salt_len=16        # 鹽長度
)

def hash_password(password: str) -> str:
    """雜湊密碼用於儲存。"""
    return ph.hash(password)

def verify_password(stored_hash: str, password: str) -> bool:
    """驗證密碼與儲存的雜湊。"""
    try:
        ph.verify(stored_hash, password)
        return True
    except exceptions.VerifyMismatchError:
        return False
    except exceptions.InvalidHashError:
        # 雜湊格式無效
        return False

def needs_rehash(stored_hash: str) -> bool:
    """檢查密碼是否需要重新雜湊(參數已更改)。"""
    return ph.check_needs_rehash(stored_hash)

# 使用
password = "user_password_here"

# 註冊
hashed = hash_password(password)
print(f"儲存的雜湊: {hashed}")
# $argon2id$v=19$m=65536,t=2,p=4$...

# 登入
if verify_password(hashed, password):
    print("登入成功")

    # 檢查是否應該升級雜湊
    if needs_rehash(hashed):
        new_hash = hash_password(password)
        # 用 new_hash 更新資料庫

10. 常見誤區

誤區現實
「有足夠的計算能力就能解密雜湊」不能。雜湊會銷毀資訊。沒有東西可解密。
「SHA-256 適合用於密碼儲存」SHA-256 太快了。使用 bcrypt/Argon2。
「更長的雜湊 = 更安全」安全性取決於演算法,而不僅僅是長度。SHA-512 不是 SHA-256 的「兩倍安全」。
「我把密碼雜湊兩次來增加安全性」這沒有幫助,在某些情況下實際上可能降低安全性。
「MD5 用於非安全目的沒問題」直到需求改變。即使「只是校驗和」也使用 SHA-256。

11. 本章小結

三點要記住:

  1. 雜湊不是加密。 雜湊函式設計上是單向的。你不能也不應該期望「解密」一個雜湊。沒有金鑰,沒有逆轉,只有指紋。

  2. 速度是密碼儲存的敵人。 像 SHA-256 這樣的通用雜湊函式被設計成快速的。像 Argon2 這樣的密碼雜湊函式被設計成慢速的。為工作使用正確的工具。

  3. MD5 和 SHA-1 在安全方面已被棄用。 即使「只是校驗和」,也優先使用 SHA-256 或 BLAKE3。需求會變,你不希望在它們變化時被抓到使用了破碎的密碼學。

12. 下一步

我們已經介紹了加密、雜湊及其區別。但我們忽略了一些關鍵的東西:金鑰和鹽從哪裡來?

在下一篇文章中,我們將探索:隨機數——密碼學系統中最被低估的組件,以及為什麼 rand() 會殺死你的安全性。