前言:昨天看到新聞 - 全球最大 3D 列印模型交流網站 Thingiverse 資料庫備份外流,其中包含 22 萬 8 千筆會員資料,包含生日、IP、姓名、密碼、地址、帳號... 密碼為「未加鹽的 SHA1 雜湊」。這篇聊密碼雜湊跟鹽的文章在草稿區擱了很久,順應天意,花了點時間把它寫完。

跟小木頭聊到(謎之聲:在家裡閒聊有必要這麼硬?):如果系統有密碼需要儲存,要怎麼設計才安全?存明碼不行,為什麼加密寫進資料庫也被說不安全?

我不是這個領域的專家,但身為老司機加減略懂,這篇嘗試著用淺白的問答形式,挑戰寫成高中生也能看懂的科普文。至於若有謬誤,也歡迎指教補充。(請鞭小力一點)

  1. 如何讓儲存的密碼更安全更不易被破解?
    答案很簡單,沒事不要自己寫。
    自己造輪子樂無窮,造輪成癮的我完全懂。但登入機制與密碼保存實在太重要了,事關身家性命安危,如果能整合 Microsoft、Google 等大廠開發的第三方登入服務或身分認證機制,就別自己搞(除非,閣下功力足以碾壓 Google 跟微軟工程師,請受小弟一拜);即使要自己開發,引用業界慣用有經過市場考驗的程式庫,也好過全部自己搞。
    這件事就像防彈衣,你該自己找材料做一件,還是去買專業廠商的成熟產品?不用說大家都知怎麼選。但很不幸,總有些特殊需求必須自己儲存密碼,所以我們還是要對密碼儲存有些認識。

  2. 密碼用明碼存入資料庫真的不行嗎?資料庫又不是阿貓阿狗都能連?
    說「用明碼存密碼必死無疑」是誇張了點,嚴格說應是「發生災難的機率很高且代價慘重」,雖然存明碼十幾年相安無事也不是不可能,但想像一下出事的後果,你應該不會想賭。
    資安有個重要心法是「不怕一萬只怕萬一,即使最壞情況發生,也要降低損失」,就像戴安全帽跟綁安全帶,很可能終其一生都沒派上用場,但只要發生一次,你都會謝天謝地幸好有遵守 SOP。(後面會常引用「萬一」這個概念)
    資料庫可能被堅守自盜、被入侵、備份外流(像文章開頭的新聞),用明碼儲存密碼讓惡意人士連程式碼都不用研究,不費吹灰之力拿到所有人的密碼,危險指數最高。

  3. 不用明碼,那加密後儲存總可以了吧?
    不妥。如果只有資料被人偷走的確沒什麼搞頭。萬一對方也掌握加解密細節,加密密碼便跟明碼密碼沒有兩樣,駭客可輕鬆取得所有人的密碼。
    加密方法寫在程式裡,壞人怎麼知道?密碼學有個柯克霍夫原則,主張系統要設計成即便原始碼被敵人掌握,只要金鑰沒洩漏就應該要是安全的。進一步可以延伸成 - 永遠假設你寫的所有程式碼已被公開的,在這個前題下設法做到安全。
    即便如此,將密碼加密後儲存,遇到攻擊者同時取得資料、加密方法及金鑰,防禦層瞬間崩潰,花幾秒鐘便可拿到所有人的密碼,風險過高,故不建議使用。

  4. 用雜湊(Hash)形式儲存密碼,為什麼比較安全?
    雜湊函式有個重要特性,它能將任意長度內容轉換成一段固定長度的指紋,內容的微小差異會導致截然不同的指紋;相同內容產生的指紋永遠相同,而要用指紋反推其對映的內容幾乎是不可能的(除非透過暴力破解,這就是比特幣挖礦為什麼要動用成千上萬台機器 24 小時不斷計算的理由。延伸閱讀:比特幣挖礦在挖什麼?)。雜湊概念很重要,是當今電子交易、數位簽章、數位貨幣的重要基礎。
    如以下例子,ABC、ABD、ABCD 三者的雜湊值截然不同,且推敲不出與原始內容的關聯。如此,即便駭客偷到 B5D4045C3F466FA91FE2CC6ABE79232A1A57CDF104F7A26E716E0A1E2789DF78,無法反推密碼是 ABC。但當使用者登入時輸入 ABC,系統用 ABC 重算雜湊得到 B5D4045C3F466FA91FE2CC6ABE79232A1A57CDF104F7A26E716E0A1E2789DF78,雖然不知密碼為何,一致也能確定密碼是對的。

    雜湊演算法有好幾種,例如:MD5、SHA1、SHA256,MD5 跟 SHA1 長度較短破解難度較低,已擋不住這些年每幾年計算能力就加倍的電腦,故目前 MD5 及 SHA1 已被視為不安全,建議使用 SHA256 或「更高階的密碼專用雜湊演算法」。(像我,寫了這麼多年程式從沒用過 Scrypt、Bcrypt 或 ARGON2 雜湊,隔行如隔山,這是為什麼儲存密碼最好「閃開,讓專業的來」的好理由。)

  5. 所以,雜湊可以被破解?
    雖然駭客無法直接由雜湊反推密碼,但如果知道系統用的是哪一種雜湊函式(例如 SHA256),他可以用「猜」的,計算 A 的 SHA256、計算 B 的 SHA256 ... 計算 Z 的 SHA256,計算 AA 的 SHA256 ... 按這規則寫程式把各種字元組合試過一遍,試到 ABC 時算出 B5D4045...2789DF78,便知道密碼是 ABC。這種做法叫做暴力攻擊(Brute Force Attack),每次把所有字元組合重算一次太浪費時間,故將字典單字、常用密碼,甚至所有字元排列組合的雜湊預先算好,想破解時直接查表就能輕鬆反推密碼。目前還有所謂彩虹表(Rainbow Table)攻擊工具,使用優化過的雜湊對映表,可以在 160 秒內破解 Windows 2003 的 14 位文數字密碼(例如:Fgpyyih804423)。
    以上說的攻擊方法,要不是很耗時間,就是很耗空間,例如要破解 14 位長度密碼,使用的字元範圍愈大,要準備的資料表空間就愈大,這也是為什麼密碼愈長,混雜英數字跟符號愈複雜愈難破解的理由:

    Character SetTable Size
    ABCDEFGHIJKLMNOPQRSTUVWXYZ0.6 GB
    ABCDEFGHIJKLMNOPQRSTUVWXYZ01234567893 GB
    ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_+=24 GB
    ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_+=~`[]{}|:;"'<>,.?/64 GB

    延伸閱讀:Rainbow Hash Cracking by CODING HORROR

  6. 聽說加鹽(Salt)可以讓雜湊更安全,是怎麼辦到的?
    上面提到攻擊者只需事先算好所有字元組合的雜湊對映值,透過查表就能反查出密碼內容。而雜湊有個特性,在原本內容多加一個字元,產生結果便完全不同。於是就有人想出在密碼前後加上額外內容的做法,讓查表法當場失效,這種額外加入的字串稱為
    例如:密碼 ABC 對應的 SHA256 是 B5D4045...2789DF78,若攻擊者預先算好所有 4 位字元組合的 SHA256,他可以很快由 B5D4045...2789DF78 反推密碼是 ABC;但如果我們在真正密碼前面加一個 X,XABC 對映的 SHA256 是 49EA032D3238425FE093466C325F3729EBB3F0C8FCEAD0B8D5DC49816493D49E,攻擊者查表反推以為密碼是 XABC。比對密碼時,系統用 "X" + "XABC" 產生 SHA256,結果不等於 49EA032D...6493D49E,登入失敗。除非攻擊者知道哪些部分是鹽,將之剔除才能求出真正的密碼。
    此外,在未加鹽時,可由兩個帳號密碼雜湊相同可判定其密碼相同。若每個帳號均加入不同的鹽,即使密碼相同雜湊也不會相同,也無法尋找已知密碼的雜湊驗證推測,攻擊難度亦會上升。 而在 Bcrypt 等高階密碼雜湊中,一樣會透過加鹽讓同一密碼產生不同雜湊結果,但方式不是在明文密碼加字元這麼簡單,且由於加鹽演算複雜,不存在反推明碼剔除鹽取出原始密碼的可能性,做法上更安全。

  7. 那,加鹽要怎麼加才夠安全?
    不同雜湊演算法加鹽的實作方式不同,但目標都讓相同密碼計算的雜湊值不同,查預先算好查表的攻擊手法破功,因此不同雜湊對鹽的需求也不同。若以最簡單的字元串接鹽為例(註:此類做法已不符現代安全要求,以下屬設計思考方向練習),試想以下幾種加鹽方法:

    • 所有密碼加上固定文字,例如:用「"AddS@lt" + 真正的密碼」計算雜湊
      引用前面提過的「萬一」思考法,萬一駭客偷到"AddS@lt"這串內容,他只需將查表得到結果移掉最前面的"AddS@lt",就是真正的密碼。或套用這個鹽重算所有字元組合的雜湊對映表,便可輕鬆破解所有密碼。
    • 密碼加上某些使用者資訊,例如:註冊日期/會員編號/使用者帳號 + 真正的密碼
      引用前面提過的「柯克霍夫原則」,假設攻擊者看得到你的原始碼又拿到使用者資料,套用鎖定對象的專屬鹽以字典檔或暴力攻擊,難度便下降到跟沒加鹽一樣。
    • 密碼加上固定位數的隨機數字
      引用「柯克霍夫原則」,駭客由程式碼得知前方 N 位是鹽,用查表反推的結果再移去前方 N 個字元便得到真正的密碼。

    故字串相加形式的鹽至少要具有:隨機產生、每個使用者不同、長度不固定 等特色,若要讓破解難度更高,鹽的長度最好長一點並混入特殊符號,樣式接近一般密碼,甚至設計成有時加前面有時加後面甚至多處穿插。長度加長及包含特殊符號會使建立破解資料表的成本上升,樣式接近一般密碼且加入位置不固定可讓駭客即使反推成功,也不易分出哪部分是鹽,哪部分是真正的密碼。另外,如能將鹽跟雜湊分開儲存又更好,例如:只有密碼資料庫外流時,鹽不致同時落入賊人之手,破解難度會上升,但分開儲存將增加系統設計及管理複雜度,需衡量利弊。
    至於高階密碼雜湊的鹽,已非採文字串接方式,基本上在建立帳號或設定密碼時依其規格隨機產生一段數字供後續便用即可,不需有太多考量,而鹽外流的風險也較低。

  8. 什麼是高階密碼專用雜湊演算法?
    MD5、SHA1、SHA256 這些雜湊演算法也用於檔案或文件內容檢核,以「內容稍有更動雜湊值就截然不同」、「難以竄改部分內容仍保持雜湊值一致」為最主要使命,依內容算出雜湊的過程愈有效率愈好。而雜湊函式用於密碼則有點不同,需全力防止由雜湊反推原始資料內容,於是就發展出一些需要消耗可觀 CPU 或記憶體才能完成雜湊演算法,例如:PBKDF2、Scrypt、Bcrypt、Argon2... 如此,駭客想暴力攻擊,每次計算雜湊要花的資源是 SHA256 的 N 倍,讓破解難度也驟升 N 倍。例如:若要在一年內破解 10 個字元的密碼,若用 MD5 雜湊只需一萬美金的電腦就可以辦到,若用 Bcrypt (95ms) 雜湊需要 12 億美金的硬體,Scrypt (64ms) 雜湊則需要 430 億美金。

    圖表來源:Password Hashing: PBKDF2, Scrypt, Bcrypt
    雖然用高階密碼雜湊在儲存密碼及每次比對密碼時要消耗較多 CPU、記憶體,會折損一些效能;但對駭客而言,暴力攻擊成本將被放大數百萬倍,幾已無實現可能。我把它想成七傷拳,靠自損三分立於不敗。而當代儲存密碼雜湊,使用高階密碼雜湊已是主流。
    延伸閱讀:Password Hashing: Scrypt, Bcrypt and ARGON2

【小結】

關於如何儲存密碼才安全,我簡單整理為以下四點:

  • 「閃開,讓專業的來」是王道,優先考慮用微軟、Google 等大廠的服務或認證機制,非不得已別自己寫
  • 如果要自己儲存,勿使用明碼或加密方式儲存,請務必使用雜湊並加鹽
  • 加鹽原則:每個使用者不同、隨機無法預測、長度不固定、長一點並含特殊符號... 都有利於提高安全度,至於高階密碼雜湊,鹽屬必備要項,依規格建立即具有很好的保護效果
  • 雜湊演算法選擇:隨著電腦計算能力不斷翻倍,SHA1、MD5 已被視為不安全,SHA256 已是基本要求,當用於密碼保護,建議優先考慮使用 Scrypt、Bcrypt、ARGON2 等專門用來對抗暴力破解的密碼專用雜湊演算法

FAQ of how to store password safely.


Comments

# by rock

認識機制>認證機制?

# by Jeffrey

to rock, 感謝指正。

# by abc

萬一對方也知道加密方法,加密密碼便跟明碼密碼沒有差異 知道加密方法 不代表 "經加密的密碼" 會存在問題 是如何安全儲存 可以解密 "經加密的密碼" 的私鑰 才是問題

# by Jeffrey

to abc, 第 3 點第一段「加密方法」原意包含加密演算法及金鑰,但第二段的「加密方法」意義卻又偏向加密演算法,的確使人混淆,已將第一段「知道加密方法」修改為「取得加解密細節」,謝謝你的指正。

# by 無名

感謝提醒

# by 99

關於內文, 第四點「寫了這麼多年程式從沒用過 Scrypt、Bcrypt 或 ARGON2 雜湊」, 最後小結,又提到「當用於密碼保護,建議優先考慮使用 Scrypt、Bcrypt、ARGON2 等專門用來對抗暴力破解的密碼專用雜湊演算法」 還是筆者的意思是,密碼加密處理是專業學問,但筆者本身並非該領域人員,因此只採用自己習慣的加密方法,而非採用多種加密算法?

# by Jeffrey

to 99, 補充說明:密碼有強度較高的專用雜湊函式,與一般程式常用雜湊不同,故即使寫了多年程式可能也未必知道;當有密碼雜湊需求時,可能依經驗用一般的雜湊函式,導致防護強度較差。現在既然已知有密碼專用的雜湊函數,若真有要自己儲存密碼的需求,建議使用 Scrypt、Bcrypt、ARGON2 會更安全。 希望以上有解答疑惑。另外,加密跟雜湊不大一樣,加密可以還原回明碼,雜湊無法還原只能比對是否相同。

# by P

那麽 salt 要怎樣保存才安全?

# by Jeffrey

to P, 如文章所說,加 Salt 相同密碼產生不同雜湊值,增加破解難度,儲存時比照一般密敏資料,不需要額外特別處理;至於高階密碼雜湊的鹽,基本上在建立帳號或設定密碼時依其規格隨機產生一段數字,跟密碼一起保存即可。

# by fisy

印象中鹽不算是秘密,主要作用是要讓同一個密碼產生不同的hash,讓彩虹表失效。

# by nameless

一直想不太通, 已知 attacker 有辦法拿到程式的原始碼以及密碼的 hash 表 不加 salt 的話 attacker 可以只對存放密碼的 hash 表破解 加了 salt 的話 attacker 需要再多增加一張 salt 的表來破解 所以安全性的增加實際上就是增加一張 salt 表... 1. 既然都有能力拿到密碼的hash表了...拿到 salt 表的難度應該是一樣的吧? 2. 若安全性的增加是增加一層salt表,那增加10個 salt 表不就增加10層安全性了...?XD 3. 彩虹表實際上是透過枚舉獲得的話,那 salt 長度應該越長越好?

# by Jeffrey

to nameless,原本攻擊者可以用查表法秒破密碼(不需暴力一個一個字元試),加鹽會讓預先算好的表無用武之地,除非列舉出所有可能的鹽,每個都預先算一張彩虹表,這個計算成本應會讓所有攻擊者打退堂鼓。 假設攻擊者真的願意為不同的鹽各算一張表,其成本跟鹽的長度呈幾何級數成長,8 bit 是 2^8 倍,64 bit 是 2^64 倍。(鹽是為每個密碼隨機產生,應該不會是用 10 張 Salt 表這種角度來看)

# by nameless

To Jeffrey大, 感謝您的回覆,澄清一下我所說的情境 我所說的 "密碼的hash表" 以及 "salt表" 預設是指存放在database上面的表 還有取得 "密碼的hash表" 以及 "salt表" 的難度應該是一樣的(或是幾乎相等) 而彩虹表我所想像的應該是找dict[hash_text] = password 這樣的資料結構。 之所以我用 10 張 salt 表的角度來看是因為 attacker 對於單一一張 salt 表的 hash 需要知道 1. hash method 2. 那一張 salt 表和密碼的 hash 表 才可以推出明文密碼 所以我才認為 salt 之所以能夠被認為增加安全性在於多增加一筆機密資料存放在"不同的地方" 越多張 salt 表,代表 attacker 需要從越多不同的地方找出 salt 資料才能夠破解出來

# by nameless

>> 永遠假設你寫的所有程式碼已被公開的,在這個前題下設法做到安全。 attacker 已知 hash method,對於拿到的 salt 怎麼對 password 作處理也已知 >>假設攻擊者看得到你的原始碼又拿到"使用者資料" attacker 已知 user password 的 hash 表 那 attacker 應該就剩下 salt 表尚未知道而已,假設hash(value) 的 value 是 salt1 + salt2 + ... + salt10 + password, 那 attacker 就必須要知道 salt1 ~ salt10 的字串,再根據彩虹表反推 value 後去掉 salt1 ~ salt10,得出真正的 password

Post a comment