如何正確對用戶密碼進行加密?轉自https://blog.csdn.net/zhouyan8603/article/details/80473083


本文介紹了對密碼哈希加密的基礎知識,以及什么是正確的加密方式。還介紹了常見的密碼破解方法,給出了如何避免密碼被破解的思路。相信讀者閱讀本文后,就會對密碼的加密有一個正確的認識,並對密碼正確進行加密措施。

作為一名Web開發人員,我們經常需要與用戶的帳號系統打交道,而這其中最大的挑戰就是如何保護用戶的密碼。經常會看到用戶賬戶數據庫頻繁被黑,所以我們必須采取一些措施來保護用戶密碼,以免導致不必要的數據泄露。保護密碼的最好辦法是使用加鹽密碼哈希( salted password hashing)。

 

重要警告:請放棄編寫自己的密碼哈希加密代碼的念頭!因為這件事太容易搞砸了。就算你在大學學過密碼學的知識,也應該遵循這個警告。所有人都要謹記這點:不要自己寫哈希加密算法! 存儲密碼的相關問題已經有了成熟的解決方案,就是使用 phpass,或者在 defuse/password-hashing 或 libsodium 上的 PHP 、 C# 、 Java 和 Ruby 的實現。在對密碼進行哈希加密的問題上,人們有很多爭論和誤解,可能是由於網絡上有大量錯誤信息的原因吧。對密碼哈希加密是一件很簡單的事,但很多人都犯了錯。本文將會重點分享如何進行正確加密用戶密碼。

密碼哈希是什么?

hash("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
hash("hbllo") = 58756879c05c68dfac9866712fad6a93f8146f337a69afe7dd238f3364946366
hash("waltz") = c0e81794384491161f1777c232bc6bd9ec38f616560b120fda8e90f383853542
 

哈希算法是一種單向函數。它把任意數量的數據轉換為固定長度的“指紋”,而且這個過程無法逆轉。它們有這樣的特性:如果輸入發生了一點改變,由此產生的哈希值會完全不同(參見上面的例子)。這個特性很適合用來存儲密碼。因為我們需要一種不可逆的算法來加密存儲的密碼,同時保證我們也能夠驗證用戶登陸的密碼是否正確。

在基於哈希加密的帳號系統中,用戶注冊和認證的大致流程如下。

  1. 用戶創建自己的帳號。
  2. 密碼經過哈希加密后存儲在數據庫中。密碼一旦寫入到磁盤,任何時候都不允許是明文形式。
  3. 當用戶試圖登錄時,系統從數據庫取出已經加密的密碼,和經過哈希加密的用戶輸入的密碼進行對比。
  4. 如果哈希值相同,用戶將被授予訪問權限。否則,告知用戶他們輸入的登陸憑據無效。
  5. 每當有人試圖嘗試登陸,就重復步驟3和4。

在步驟4中,永遠不要告訴用戶輸錯的究竟是用戶名還是密碼。就像通用的提示那樣,始終顯示:“無效的用戶名或密碼。”就行了。這樣可以防止攻擊者在不知道密碼的情況下枚舉出有效的用戶名。

應當注意的是,用來保護密碼的哈希函數,和數據結構課學到的哈希函數是不同的。例如,實現哈希表的哈希函數設計目的是快速查找,而非安全性。只有加密哈希函數( cryptographic hash function)才可以用來進行密碼哈希加密。像 SHA256 、 SHA512 、 RIPEMD 和 WHIRLPOOL 都是加密哈希函數。

人們很容易認為,Web開發人員所做的就是:只需通過執行加密哈希函數就可以讓用戶密碼得以安全。然而並不是這樣。有很多方法可以從簡單的哈希值中快速恢復出明文的密碼。有幾種易於實施的技術,使這些“破解”的效率大為降低。網上有這種專門破解MD5的網站,只需提交一個哈希值,不到一秒鍾就能得到破解的結果。顯然,單純的對密碼進行哈希加密遠遠達不到我們的安全要求。下一節將討論一些用來破解簡單密碼哈希常用的手段。

如何破解哈希?

字典攻擊和暴力攻擊( Dictionary and Brute Force Attacks)

字典攻擊

暴力攻擊

Trying apple : failed

Trying aaaa : failed

Trying blueberry : failed

Trying aaab : failed

Trying justinbeiber : failed

Trying aaac : failed

Trying letmein : failed

Trying acdb : failed

Trying s3cr3t : success!

Trying acdc : success!

破解哈希加密最簡單的方法是嘗試猜測密碼,哈希每個猜測的密碼,並對比猜測密碼的哈希值是否等於被破解的哈希值。如果相等,則猜中。猜測密碼攻擊的兩種最常見的方法是字典攻擊和暴力攻擊 。

字典攻擊使用包含單詞、短語、常用密碼和其他可能用做密碼的字符串的字典文件。對文件中的每個詞都進行哈希加密,將這些哈希值和要破解的密碼哈希值比較。如果它們相同,這個詞就是密碼。字典文件是通過大段文本中提取的單詞構成,甚至還包括一些數據庫中真實的密碼。還可以對字典文件進一步處理以使其更為有效:如單詞 “hello” 按網絡用語寫法轉成 “h3110” 。

暴力攻擊是對於給定的密碼長度,嘗試每一種可能的字符組合。這種方式會消耗大量的計算,也是破解哈希加密效率最低的辦法,但最終會找出正確的密碼。因此密碼應該足夠長,以至於遍歷所有可能的字符組合,耗費的時間太長令人無法承受,從而放棄破解。

目前沒有辦法來組織字典攻擊或暴力攻擊。只能想辦法讓它們變得低效。如果密碼哈希系統設計是安全的,破解哈希的唯一方法就是進行字典攻擊或暴力攻擊遍歷每一個哈希值了。

查表法( Lookup Tables)

Searching: 5f4dcc3b5aa765d61d8327deb882cf99: FOUND: password5
Searching: 6cbe615c106f422d23669b610b564800:  not in database
Searching: 630bf032efe4507f2c57b280995925a9: FOUND: letMEin12 
Searching: 386f43fab5d096a7a66d67c8f213e5ec: FOUND: mcd0nalds
Searching: d5ec75d5fe70d428685510fae36492d9: FOUND: p@ssw0rd!

對於破解相同類型的哈希值,查表法是一種非常高效的方式。主要理念是預先計算( pre-compute)出密碼字典中的每個密碼的哈希值,然后把他們相應的密碼存儲到一個表里。一個設計良好的查詢表結構,即使包含了數十億個哈希值,仍然可以實現每秒鍾查詢數百次哈希。

如果你想感受查表法的速度有多快,嘗試一下用 CrackStation 的 free hash cracker 來破解下面的 SHA256。

c11083b4b0a7743af748c85d343dfee9fbb8b2576c05f3a7f0d632b0926aadfc
  08eac03b80adc33dc7d8fbe44b7c7b05d3a2c511166bdb43fcb710b03ba919e7
  e4ba5cbd251c98e6cd1c23f126a3b81d8d8328abc95387229850952b3ef9f904
  5206b8b8a996cf5320cb12ca91c7b790fba9f030408efe83ebb83548dc3007bd

反向查表法( Reverse Lookup Tables)

Searching for hash(apple) in users' hash list...     : Matches [alice3, 0bob0, charles8]
Searching for hash(blueberry) in users' hash list... : Matches [usr10101, timmy, john91]
Searching for hash(letmein) in users' hash list...   : Matches [wilson10, dragonslayerX, joe1984]
Searching for hash(s3cr3t) in users' hash list...    : Matches [bruce19, knuth1337, john87]
Searching for hash(z@29hjja) in users' hash list...  : No users used this password

這種攻擊允許攻擊者無需預先計算好查詢表的情況下同時對多個哈希值發起字典攻擊或暴力攻擊。

首先,攻擊者從被黑的用戶帳號數據庫創建一個用戶名和對應的密碼哈希表,然后,攻擊者猜測一系列哈希值並使用該查詢表來查找使用此密碼的用戶。通常許多用戶都會使用相同的密碼,因此這種攻擊方式特別有效。

彩虹表( Rainbow Tables)

彩虹表是一種以空間換時間的技術。與查表法相似,只是它為了使查詢表更小,犧牲了破解速度。因為彩虹表更小,所以在單位空間可以存儲更多的哈希值,從而使攻擊更有效。能夠破解任何最多8位長度的 MD5 值的彩虹表已經出現

接下來,我們來看一種謂之“加鹽( salting)”的技術,能夠讓查表法和彩虹表都失效。

加鹽( Adding Salt)

hash("hello")                    = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
hash("hello" + "QxLUF1bgIAdeQX") = 9e209040c863f84a31e719795b2577523954739fe5ed3b58a75cff2127075ed1
hash("hello" + "bv5PehSMfV11Cd") = d1d3ec2e6f20fd420d50e2642992841d8338a314b8ea157c9e18477aaef226ab
hash("hello" + "YYLmfY6IehjZMQ") = a49670c3c18b9e079b9cfaf51634f563dc8ae3070db2c4a8544305df1b60f007

查表法和彩虹表只有在所有密碼都以完全相同的方式進行哈希加密才有效。如果兩個用戶有相同的密碼,他們將有相同的密碼哈希值。我們可以通過“隨機化”哈希,當同一個密碼哈希兩次后,得到的哈希值是不一樣的,從而避免了這種攻擊。

我們可以通過在密碼中加入一段隨機字符串再進行哈希加密,這個被加的字符串稱之為鹽值。如上例所示,這使得相同的密碼每次都被加密為完全不同的字符串。我們需要鹽值來校驗密碼是否正確。通常和密碼哈希值一同存儲在帳號數據庫中,或者作為哈希字符串的一部分。

鹽值無需加密。由於隨機化了哈希值,查表法、反向查表法和彩虹表都會失效。因為攻擊者無法事先知道鹽值,所以他們就沒有辦法預先計算查詢表或彩虹表。如果每個用戶的密碼用不同的鹽再進行哈希加密,那么反向查表法攻擊也將不能奏效。

接下來,我們看看加鹽哈希通常會有哪些不正確的措施。

錯誤的方法:短鹽值和鹽值復用

最常見的錯誤,是多次哈希加密使用相同的鹽值,或者鹽值太短。

鹽值復用( Salt Reuse)

一個常見的錯誤是每次都使用相同的鹽值進行哈希加密,這個鹽值要么被硬編碼到程序里,要么只在第一次使用時隨機獲得。這樣的做法是無效的,因為如果兩個用戶有相同的密碼,他們仍然會有相同的哈希值。攻擊者仍然可以使用反向查表法對每個哈希值進行字典攻擊。他們只是在哈希密碼之前,將固定的鹽值應用到每個猜測的密碼就可以了。如果鹽值被硬編碼到一個流行的軟件里,那么查詢表和彩虹表可以內置該鹽值,以使其更容易破解它產生的哈希值。

用戶創建帳號或者更改密碼時,都應該用新的隨機鹽值進行加密。

短鹽值( Short Slat)

如果鹽值太短,攻擊者可以預先制作針對所有可能的鹽值的查詢表。例如,如果鹽值只有三個 ASCII 字符,那么只有 95x95x95=857,375種可能性。這看起來很多,但如果每個查詢表包含常見的密碼只有 1MB,857,375個鹽值總共只需 837GB,一塊時下不到100美元的 1TB硬盤就能解決問題了。

出於同樣的原因,不應該將用戶名用作鹽值。對每一個服務來說,用戶名是唯一的,但它們是可預測的,並且經常重復應用於其他服務。攻擊者可以用常見用戶名作為鹽值來建立查詢表和彩虹表來破解密碼哈希。

為使攻擊者無法構造包含所有可能鹽值的查詢表,鹽值必須足夠長。一個好的經驗是使用和哈希函數輸出的字符串等長的鹽值。例如, SHA256 的輸出為256位(32字節),所以該鹽也應該是32個隨機字節。

雙重哈希和古怪的哈希函數

本節將介紹另一種常見的密碼哈希的誤解:古怪哈希的算法組合。人們很容易沖昏頭腦,嘗試不同的哈希函數相結合一起使用,希望讓數據會更安全。但在實踐中,這樣做並沒有什么好處。它帶來了函數之間互通性的問題,而且甚至可能會使哈希變得更不安全。永遠不要試圖去創造你自己的哈希加密算法,要使用專家設計好的標准算法。有人會說,使用多個哈希函數會降低計算速度,從而增加破解的難度。但是使破解過程變慢還有更好的辦法,我們將在后面講到。

下面是在網上見過的古怪的哈希函數組合的一些例子。

  • md5(sha1(password))

  • md5(md5(salt) + md5(password))

  • sha1(sha1(password))

  • sha1(str_rot13(password + salt))

  • md5(sha1(md5(md5(password) + sha1(password)) + md5(password))))

不要使用其中任何一種。

注意:此部分是有爭議的。我收到了一些電子郵件,他們認為古怪的哈希函數是有意義的,理由是,如果攻擊者不知道系統使用哪個哈希函數,那么攻擊者就不太可能預先計算出這種古怪的哈希函數彩虹表,於是破解起來要花更多的時間。

當攻擊者不知道哈希加密算法的時候,是無法發起攻擊的。但是要考慮到柯克霍夫原則,攻擊者通常會獲得源代碼(尤其是免費或者開源軟件)。通過系統中找出密碼-哈希值對應關系,很容易反向推導出加密算法。使用一個很難被並行計算結果的迭代算法(下面將予以討論),然后增加適當的鹽值防止彩虹表攻擊。

如果你真的想用一個標准的“古怪”的哈希函數,如 HMAC ,亦無不可。但是,如果你目的是想降低哈希計算速度,那么可以閱讀下面有關密鑰擴展的部分。

如果創造新的哈希函數,可能會帶來風險,構造希函數的組合又會導致函數互通性的問題。它們帶來一點的好處和這些比起來微不足道。很顯然,最好的辦法是,使用標准、經過完整測試的算法。

哈希碰撞( Hash Collisions)

由於哈希函數將任意大小的數據轉化為定長的字符串,因此,必定有一些不同的輸入經過哈希計算后得到了相同的字符串的情況。加密哈希函數( Cryptographic hash function)的設計初衷就是使這些碰撞盡量難以被找到。現在,密碼學家發現攻擊哈希函數越來越容易找到碰撞了。最近的例子是MD5算法,它的碰撞已經實現了。

碰撞攻擊是指存在一個和用戶密碼不同的字符串,卻有相同的哈希值。然而,即使是像MD5這樣的脆弱的哈希函數找到碰撞也需要大量的專門算力( dedicated computing power),所以在實際中“意外地”出現哈希碰撞的情況不太可能。對於實用性而言,加鹽 MD5 和加鹽 SHA256 的安全性一樣。盡管如此,可能的話,要使用更安全的哈希函數,比如 SHA256 、 SHA512 、 RipeMD 或 WHIRLPOOL 。

如何正確進行哈希加密

本節介紹了究竟應該如何對密碼進行哈希加密。第一部分介紹基礎知識,這部分是必須的。后面闡述如何在這個基礎上增強安全性,使哈希加密變得更難破解。

基礎知識:加鹽哈希( Hashing with Salt)

我們已經知道,惡意攻擊者使用查詢表和彩虹表,破解普通哈希加密有多么快。我們也已經了解到,使用隨機加鹽哈希可以解決這個問題。但是,我們使用什么樣的鹽值,又如何將其混入密碼中?

鹽值應該使用加密的安全偽隨機數生成器( Cryptographically Secure Pseudo-Random Number Generator,CSPRNG )產生。CSPRNG和普通的偽隨機數生成器有很大不同,如“ C ”語言的rand()函數。顧名思義, CSPRNG 被設計成用於加密安全,這意味着它能提供高度隨機、完全不可預測的隨機數。我們不希望鹽值能夠被預測到,所以必須使用 CSPRNG 。下表列出了一些當前主流編程平台的 CSPRNG 方法。

Platform

CSPRNG

PHP

mcrypt_create_iv, openssl_random_pseudo_bytes

Java

java.security.SecureRandom

Dot NET (C#, VB)

System.Security.Cryptography.RNGCryptoServiceProvider

Ruby

SecureRandom

Python

os.urandom

Perl

Math::Random::Secure

C/C++ (Windows API)

CryptGenRandom

Any language on GNU/Linux or Unix

Read from /dev/random or /dev/urandom

每個用戶的每一個密碼都要使用獨一無二的鹽值。用戶每次創建帳號或更改密碼時,密碼應采用一個新的隨機鹽值。永遠不要重復使用某個鹽值。這個鹽值也應該足夠長,以使有足夠多的鹽值能用於哈希加密。一個經驗規則是,鹽值至少要跟哈希函數的輸出一樣長。該鹽應和密碼哈希一起存儲在用戶帳號表中。

存儲密碼的步驟:

  1. 使用 CSPRNG 生成足夠長的隨機鹽值。
  2. 將鹽值混入密碼,並使用標准的密碼哈希函數進行加密,如Argon2、 bcrypt 、 scrypt 或 PBKDF2 。
  3. 將鹽值和對應的哈希值一起存入用戶數據庫。

校驗密碼的步驟:

  1. 從數據庫檢索出用戶的鹽值和對應的哈希值。
  2. 將鹽值混入用戶輸入的密碼,並且使用通用的哈希函數進行加密。
  3. 比較上一步的結果,是否和數據庫存儲的哈希值相同。如果它們相同,則表明密碼是正確的;否則,該密碼錯誤。

在 Web 應用中,永遠在服務端上進行哈希加密

如果您正在編寫一個 Web 應用,你可能會疑惑究竟在哪里進行哈希加密,是在用戶的瀏覽器上使用 JavaScript 對密碼進行哈希加密呢,還是將明文發送到服務端上再進行哈希加密呢?

就算瀏覽器上已經用JavaScript哈希加密了,但你你還是要在服務端上將得到的密碼哈希值再進行一次哈希加密。試想一個網站,將用戶在瀏覽器輸入的密碼經過哈希加密,而不是在傳送到服務端再進行哈希。為了驗證用戶,這個網站將接受來自瀏覽器的哈希值,並和數據庫中的哈希值進行匹配即可。因為用戶的密碼從未明文傳輸到服務端,這樣子看上去更安全,但事實並非如此。

問題是,從客戶端的角度來看,經過哈希的密碼,從邏輯上成為用戶的密碼了。所有用戶需要做的認證就是將它們的密碼哈希值告訴服務端。如果一個攻擊者得到了用戶的哈希值,他們可以用它來通過認證,而不必知道用戶的明文密碼!所以,如果攻擊者使用某種手段拖了網站的數據庫,他們就可以隨意使用每個人的帳號直接訪問,而無需猜測任何密碼。

這並不是說你不應該在瀏覽器進行哈希加密,但是如果你這樣做了,你一定要在服務端上再進行一次哈希加密。在瀏覽器中進行哈希加密無疑是一個好主意,但實現的時候要考慮以下幾點:

  • 客戶端密碼哈希加密不是 HTTPS(SSL/TLS)的替代品。如果瀏覽器和服務端之間的連接是不安全的,那么中間人攻擊可以修改 JavaScript 代碼,刪除加密函數,從而獲取用戶的密碼。
  • 某些瀏覽器不支持 JavaScript ,還有一些用戶在瀏覽器中禁用 JavaScript 功能。因此,為了更好的兼容性,您的應用應該檢測瀏覽器是否支持 JavaScript ,如果不支持,就需要在服務端模擬客戶端進行哈希加密。
  • 客戶端的哈希加密同樣需要加鹽。顯而易見的解決方案是使客戶端腳本向服務端請求用戶的鹽值。但是不提倡這樣做,因為它可以讓攻擊者能夠在不知道密碼的情況下檢測用戶名是否有效。既然你已經在服務端上對密碼進行了加鹽哈希(使用合格的鹽值),那么在客戶端,將用戶名(或郵箱)加上網站特有的字符串(如域名)作為客戶端的鹽值也是可行的。

使密碼更難破解:慢哈希函數( Slow Hash Function)

加鹽可以確保攻擊者無法使用像查詢表和彩虹表攻擊那樣對大量哈希值進行破解,但依然不能阻止他們使用字典攻擊或暴力攻擊。高端顯卡( GPU )和定制的硬件每秒可以進行十億次哈希計算,所以這些攻擊還是很有效的。為了降低使這些攻擊的效率,我們可以使用一個叫做密鑰擴展( key stretching)的技術。

這樣做的初衷是為了將哈希函數變得非常慢,即使有一塊快速的 GPU 或定制的硬件,字典攻擊和暴力攻擊也會慢得令人失去耐心。終極目標是使哈希函數的速度慢到足以令攻擊者放棄,但由此造成的延遲又不至於引起用戶的注意。

密鑰擴展的實現使用了一種 CPU 密集型哈希函數( CPU-intensive hash function)。不要試圖去創造你自己的迭代哈希加密函數。迭代不夠多的話,它可以被高效的硬件快速並行計算出來,就跟普通的哈希一樣。要使用標准的算法,比如 PBKDF2 或 bcrypt 。你可以在這里找到 PBKDF2 在 PHP 上的實現。

這類算法采取安全因子或迭代次數作為參數。此值決定哈希函數將會如何緩慢。對於桌面軟件或智能手機應用,確定這個參數的最佳方式是在設備上運行很短的性能基准測試,找到使哈希大約花費半秒的值。通過這種方式,程序可以盡可能保證安全而又不影響用戶體驗。

如果您想在一個 Web 應用使用密鑰擴展,須知你需要額外的計算資源來處理大量的身份認證請求,並且密鑰擴展也容易讓服務端遭受拒絕服務攻擊( DoS )。盡管如此,我還是建議使用密鑰擴展,只不過要設定較低一些的迭代次數。這個次數需要根據自己服務器的計算能力和預計每秒需要處理的認證請求次數來設置。消除拒絕服務的威脅可以通過要求用戶每次登陸時輸入驗證碼( CAPTCHA )來做到。系統設計時要將迭代次數可隨時方便調整。

如果你擔心計算帶來負擔,但又想在 Web 應用中使用密鑰擴展,可以考慮在瀏覽器中使用 JavaScript 完成。斯坦福大學的 JavaScript 加密庫就包含了 PBKDF2 的實現。迭代次數應設置足夠低,以適應速度較慢的客戶端,如移動設備。同時,如果用戶的瀏覽器不支持 JavaScript ,服務端應該接手進行計算。客戶端密鑰擴展並不能免除服務端端進行哈希加密的需要。你必須對客戶端生成的哈希值再次進行哈希加密,就跟普通口令的處理一樣。

不可能破解的哈希加密:密鑰哈希和密碼哈希設備

只要攻擊者可以使用哈希來檢查密碼的猜測是對還是錯,那么他們可以進行字典攻擊或暴力攻擊。下一步是將密鑰( secret key)添加到哈希加密,這樣只有知道密鑰的人才可以驗證密碼。有兩種實現的方式,使用ASE算法對哈希值加密;或者使用密鑰哈希算法 HMAC 將密鑰包含到哈希字符串中。

實現起來並沒那么容易。這個密鑰必須在任何情況下,即使系統因為漏洞被攻陷,也不能被攻擊者獲取。如果攻擊者完全進入系統,密鑰不管存儲在何處,總能被找到。因此,密鑰必須密鑰必須被存儲在外部系統,例如專用於密碼驗證一個物理上隔離的服務端,或者連接到服務端,例如一個特殊的硬件設備,如 YubiHSM 。

我強烈建議所有大型服務(超過10萬用戶)使用這種方式。我認為對於任何超過100萬用戶的服務托管是非常有必要的。

如果您難以負擔多個服務端或專用硬件的費用,依然有辦法在標准的Web服務端上使用密鑰哈希技術。大多數數據庫被拖庫是由於 SQL 注入攻擊,因此,不要給攻擊者進入本地文件系統的權限(禁止數據庫服務訪問本地文件系統,如果有此功能的話)。如果您生成一個隨機密鑰並將其存儲在一個通過 Web 無法訪問的文件上,然后進行加鹽哈希加密,那么得到的哈希值就不會那么容易被破解了,就算數據庫已經遭受注入攻擊,也是安全的。不要將密鑰硬編碼到代碼中,應該在安裝應用時隨機生成。這么做並不像使用一個獨立的系統那樣安全,因為如果 Web 應用存在 SQL 注入點,那么有可能存在其他一些問題,如本地文件包含漏洞( Local File Inclusion ),攻擊者可以利用它讀取本地密鑰文件。無論如何,這個措施總比沒有好。

請注意,密鑰哈希並不意味着無需進行加鹽。高明的攻擊者最終會想方設法找到密鑰,因此,對密碼哈希仍然需要進行加鹽和密鑰擴展,這一點非常重要。

其他安全措施

密碼哈希僅僅在安全受到破壞時保護密碼。它並不能使整個應用更加安全。首先有很多事必須完成,來保證密碼哈希值(和其他用戶數據)不被竊取。

即使是經驗豐富的開發人員也必須學習安全知識,才能編寫安全的應用。此處有關於Web應用漏洞的重要資源: The Open Web Application Security Project (OWASP)。還有一個很好的介紹: OWASP Top Ten Vulnerability List 。除非你理解了列表中的所有漏洞,否則不要去嘗試編寫一個處理敏感數據的Web應用程序。雇主也有責任確保所有開發人員在安全應用開發方面經過充分的培訓。

對您的應用進行第三方“滲透測試”是一個很好的主意。即使最好的程序員也可能會犯錯,所以,讓安全專家審計代碼尋找潛在的漏洞是有意義的。找一個值得信賴的機構(或招聘人員)來定期審計代碼。安全審計應該從開發初期就着手進行,並貫穿整個開發過程。

監控您的網站來發現入侵行為也很重要。我建議至少雇用一名全職人員負責監測和處理安全漏洞。如果某個漏洞沒被發現,攻擊者可能通過網站利用惡意軟件感染訪問者,因此,檢測漏洞並及時處理是極為重要的。

常見疑問

我應該使用什么樣的哈希算法?

可以使用:

不可使用:

  • 快速加密哈希函數,如 MD5 、SHA1、SHA256、SHA512、RipeMD、WHIRLPOOL、SHA3等。
  • crypt()的不安全版本。
  • 任何自己設計的加密算法。只應該使用那些在公開領域中的、由經驗豐富的密碼學家完整測試過的技術。

盡管目前還沒有一種針對MD5或SHA1非常高效的攻擊手段,但它們過於古老以至於被廣泛認為不足以用來存儲密碼(可能有些不恰當)。所以我不推薦使用它們。但是也有例外,PBKDF2中經常使用SHA1作為它底層的哈希函數。

當用戶忘記密碼時如何重置密碼?

這是我個人的觀點:當下所有廣泛使用的密碼重置機制都是不安全的。如果你對高安全性有要求,如加密服務,那么就不要讓用戶重設密碼。

大多數網站向那些忘記密碼的用戶發送電子郵件來進行身份認證。要做到這一點,需要隨機生成一個一次性使用的令牌( token ),直接關聯到用戶的帳號。然后將這個令牌混入一個重置密碼的鏈接中,發送到用戶的電子郵箱。當用戶點擊包含有效令牌的密碼重置鏈接,就提示他們輸入新密碼。確保令牌只對一個帳號有效,以防攻擊者從郵箱獲取到令牌后用來重置其他用戶的密碼。

令牌必須在15分鍾內使用,且一旦使用后就立即作廢。當用戶登錄成功時(表明還記得自己的密碼), 或者重新請求令牌時,使原令牌失效是一個好做法。如果令牌永不過期,那么它就可以一直用於入侵用戶的賬號。電子郵件(SMTP)是一個純文本協議,網絡上有很多惡意路由在截取郵件信息。在用戶修改密碼后,那些包含重置密碼鏈接的郵件在很長時間內缺乏保護,因此,盡早使令牌盡快過期,來降低用戶信息暴露給攻擊者的風險。

攻擊者能夠篡改令牌,因此不要把帳號信息和失效時間存儲在其中。它們應該以不可猜測的二進制形式存在,並且只用來識別數據庫中某條用戶的記錄。

千萬不要通過電子郵件向用戶發送新密碼。記得在用戶重置密碼時隨機生成一個新的鹽值用來加密,不要重復使用已用於密碼哈希加密的舊鹽值。

如果帳號數據庫被泄漏或入侵,應該怎么做?

你的首要任務是,確定系統被暴露到什么程度,然后修復攻擊者利用的的漏洞。如果你沒有應對入侵的經驗,我強烈建議聘請第三方安全公司來做這件事。

捂住一個漏洞並期待沒人知道,是不是很省事,又誘人?但是這樣做只會讓你的處境變得更糟糕,因為你在用戶不知情的情況下,將它們的密碼和個人信息置於暴露風險之中。就算你還沒有完全發生什么事情時,你也應該盡快通知用戶。例如在首頁放置一個鏈接,指向對此問題更為詳細的說明;如果可能的話通過電子郵件發送通知給每個用戶告知目前的情況。

向用戶說明他們的密碼究竟是如何被保護的:最好是使用了加鹽哈希。但是,即使用了加鹽哈希,惡意黑客仍然可以使用字典攻擊和暴力攻擊。如果用戶在很多服務使用相同的密碼,惡意黑客會利用他們找到的密碼去嘗試登陸其他網站。告知用戶這個風險,建議他們修改所有類似的密碼,不論密碼用在哪個服務上。強制他們下次登錄你的網站時更改密碼。大多數用戶會嘗試“修改”自己的密碼為原始密碼,以便記憶。您應該使用當前密碼哈希值以確保用戶無法做到這一點。

就算有加鹽哈希的保護,也存在攻擊者快速破解其中一些弱口令密碼的可能性。為了減少攻擊者使用這些密碼的機會,應該對這些密碼的帳號發送認證電子郵件,直到用戶修改了密碼。可參考前面提到的問題:當用戶忘記密碼時如何重置密碼?這其中有一些實現電子郵件認證的要點。

另外告訴你的用戶,網站存儲了哪些個人信息。如果您的數據庫包括信用卡號碼,您應該通知用戶仔細檢查近期賬單並銷掉這張信用卡。

應該使用什么樣的密碼策略?是否應該使用強密碼?

如果您的服務沒有嚴格的安全要求,那么不要對用戶進行限制。我建議在用戶輸入密碼時,頁面顯示出密碼強度,由他們自己決定需要多安全的密碼。如果你有特殊的安全需求,那就應該實施長度至少為12個字符的密碼,並且至少需要兩個字母、兩個數字和兩個符號。

不要過於頻繁地強制你的用戶更改密碼,最多每半年一次,超過這個次數,用戶就會感到疲勞。相反,更好的做法是教育用戶,當他們感覺密碼可能泄露時主動修改,並且提示用戶不要把密碼告訴任何人。如果這是一個商業環境,鼓勵員工利用工作時間熟記並使用他們的密碼。

如果攻擊者入侵了數據庫,他不能直接替換哈希值登陸任意帳號么?

是的,但如果有人入侵您的數據庫,他們很可能已經能夠訪問您的服務端上的所有內容,這樣他們就不需要登錄到您的帳號,就可以獲得他們想要的東西。密碼哈希(對網站而言)的目的不是為了保護被入侵的網站,而是在入侵已經發生時保護數據庫中的密碼。

你可以通過給數據庫連接設置兩種權限,防止密碼哈希在遭遇注入攻擊時被篡改。一種權限用於創建用戶,一種權限用於用戶登陸。“創建用戶”的代碼應該能夠讀寫用戶表;但“用戶登陸”的代碼應該只能夠讀取用戶表而不能寫入。

為什么要使用一種像HMAC的特殊算法,而不是只將密鑰混入密碼?

如 MD5、SHA1、SHA2 和 Hash 函數使用 Merkle–Damg?rd ,這使得它們很容易受到所謂的長度擴展攻擊( length extension attack)。意思是給定的哈希值 H(X),對於任意的字符串 Y,攻擊者可以計算出 H(pad(X)+Y) 的值,而無需知道 X 的值。其中, pad(X) 是哈希函數的填充函數。

這意味着,攻擊者不知道密鑰的情況下,仍然可以根據給定的哈希值 H(key+message) 計算出 H(pad(key+message)+extension) 。如果該哈希值用於身份認證,並依靠其中的密鑰來防止攻擊者篡改消息,這方法已經行不通。因為攻擊者無需知道密鑰也能構造出包含 message+extension 的一個有效的哈希值。

目前尚不清楚攻擊者如何利用這種攻擊來快速破解密碼哈希。然而,由於這種攻擊的出現,不建議使用普通的哈希函數對密鑰進行哈希加密。將來也許某個高明的密碼學家有一天發現利用長度擴展攻擊的新思路,從而更快的破解密碼,所以還是使用 HMAC 為好。

鹽值應該加到密碼之前還是之后?

無所謂,選擇一個並保持風格一致即可,以免出現互操作方面的問題。鹽值加到密碼之前較為普遍。

為何本文的哈希代碼都以固定時間比較哈希值?

使用固定的時間來比較哈希值可以防止攻擊者在在線系統使用基於時間差的攻擊,以此獲取密碼的哈希值,然后進行本地破解。

比較兩個字節序列(字符串)是否相同的標准做法是,從第一個字節開始,每個字節逐一順序比較。只要發現某個字節不同,就可以知道它們是不同的,立即返回false。如果遍歷整個字符串沒有找到不同的字節,可以確認兩個字符串就是相同的,可以返回true。這意味着比較兩個字符串,如果它們相同的長度不一樣,花費的時間不一樣。開始部分相同的長度越長,花費的時間也就越長。

例如,字符串 “XYZABC” 和 “abcxyz” 的標准比較,會立即看到,第一個字符是不同的,就不需要檢查字符串的其余部分。相反,當字符串 “aaaaaaaaaaB” 和 “aaaaaaaaaaZ” 進行比較時,比較算法就需要遍歷最后一位前所有的 “a” ,然后才能知道他們是不同的。

假設攻擊者試圖入侵一個在線系統,這個系統限制了每秒只能嘗試一次用戶認證。還假設攻擊者已經知道密碼哈希所有的參數(鹽值、哈希函數的類型等),除了密碼的哈希值和密碼本身。如果攻擊者能精確測量在線系統耗時多久去比較他猜測的密碼和真實密碼,那么他就能使用時序攻擊獲取密碼的哈希值,然后進行離線破解,從而繞過系統對認證頻率的限制。

首先攻擊者准備256個字符串,它們的哈希值的第一字節包含了所有可能的情況。他將每個字符串發送給在線系統嘗試登陸,並記錄系統響應所消耗的時間。耗時最長的字符串就是第一字節相匹配的。攻擊者知道第一字節后,並可以用同樣的方式繼續猜測第二字節、第三字節等等。一旦攻擊者獲得足夠長的哈希值片段,他就可以在自己的機器上來破解,不受在線系統的限制。

在網絡上進行這種攻擊似乎不可能。然而,有人已經實現了,並已證明是實用的。這就是為什么本文提到的代碼,它利用固定時間去比較字符串,而不管有多大的字符串。

“慢比較( slowequals)”函數如何工作?

前一個問題解釋了為什么“慢比較”是必要的,現在來解釋代碼如何工作。

private static boolean slowEquals(byte[] a, byte[] b)
     {
         int diff = a.length ^ b.length;
         for(int i = 0; i < a.length && i < b.length; i++)
             diff |= a[i] ^ b[i];
         return diff == 0;
     }

該代碼使用異或運算符“^”來比較兩個整數是否相等,而不是“==”運算符。下面解釋原因。當且僅當兩位相等時,異或的結果將是零。這是因為:

0 XOR 0 = 0,1 XOR 1 = 0,0 XOR 1 = 1,1 XOR 0 = 1

如果我們將其應用到整數中每一位,當且僅當字節兩個整數各位都相等,結果才是0。

所以,在代碼的第一行中,如果a.length等於b.length ,相同的話得到0,否者得到非零值。然后使用異或比較數組中各字節,並且將結果和diff求或。如果有任何一個字節不相同,diff就會變成非零值。因為或運算沒有“置0”的功能,所以循環結束后diff是0的話只有一種可能,那就是循環前兩個數組長度相等(a.length == b.length),並且數組中每一個字節都相同(每次異或的結果都非0)。

我們需要使用XOR,而不是“==”運算符比較整數的原因是,“==”通常是編譯成一個分支的語句。例如,C語言代碼中“ diff &= a == b”可能編譯以下x86匯編:

MOV EAX, [A]
CMP [B], EAX
JZ equal
JMP done
equal:
AND [VALID], 1
done:
AND [VALID], 0

其中的分支導致代碼運行的時間不固定,決定於兩個整數相等的程度和CPU內部的跳轉預測機制(branch prediction)。

而C語言代碼“diff |= a ^ b”會被編譯為下面的樣子,它執行的時間和兩個變量是否相等無關。

MOV EAX,[A]
XOR EAX,[B]
OR [DIFF],EAX

為何要進行哈希?

用戶在你的網站上輸入密碼,是因為他們相信你能保證密碼的安全。如果你的數據庫遭到黑客攻擊,而用戶的密碼又不受保護,那么惡意黑客可以利用這些密碼嘗試登陸其他網站和服務(大多數用戶會在所有地方使用相同的密碼)。這不僅僅關乎你網站的安全,更關系到用戶的安全。你有責任負責用戶的安全。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM