安全密碼存儲,該怎么做,不該怎么做?

jopen 12年前發布 | 20K 次閱讀 安全 Java開發

作為軟件開發者,其中最重要的一個責任就是保護用戶的個人信息,如果用戶沒有相關的技術知識,他們在使用我們的服務的時候別無選擇只能信任我們。可惜的是,當我們調查關于密碼的處理的時候,我們發現有各種不同的處理方式,而這些方式有很多都不安全。雖然構建一個完全安全的系統是不可能的,但是我們可以通過一些簡單的步驟讓我們的密碼存儲足夠安全。

不應該:

首先讓我們看看當我們構建一個需要用戶認證的系統的時候不應該怎么做。
  • 在不得已的時候不要自己存儲用戶的認證信息。我們可以考慮使用OAuth的提供者例如Google、非死book。如果構建企業內部的應用,可以考慮使用已有的內部認證服務,例如企業LDAP或者Kerberos服務。無論是面向公眾的還是面向內部的應用程序,用戶會喜歡這個應用,因為他不需要多記住一個ID和密碼,同時也少了受黑客攻擊的危險。
  • 如果你必須存儲認證信息,不要存儲明文密碼。這句話就不解釋了。
  • 不要使用可逆的加密方式,除非你在某種狀況下真的需要查出來明文密碼。因為在進行用戶身份驗證的時候并不需要明文密碼去比對。
  • 不要使用過時的哈希算法,例如md5,在現在這個社會,有人可以通過構建一個超大的md5庫來反向的查詢出明文。換句話說md5哈希基本上沒什么用,你要是不相信可以拿這個密文(569a70c2ccd0ac41c9d1637afe8cd932)去 http://www.md5hacker.com/ 上看看,幾秒內就可以查出明文了。

應該:

說完了不應該做的,就說說應該做的:
  • 選擇一個單向(不可逆)的加密算法。就像我上面說的一樣,僅僅存儲加密后的用戶密碼,用戶每次認證就使用相同的算法加密后比對就可以了。
  • 選擇一個你的應用可以承受的最慢的加密算法。任何現代的加密算法都支持在加密的時候接受參數從而使加密時間延長,而解密也自然就更難。(例如PBKDF2,可以通過制定迭代的次數來實現)。為什么慢了好呢?因為用戶幾乎不會關心他為了認證自己的賬戶額外的花銷了100ms。但是黑客就不同了,當他進行上10億次的嘗試計算的時候,就有他喝一壺的了。
  • 選擇一個流行的算法。美國國家標準與技術研究院推薦使用PBKDF2加密密碼。

PBKDF2

在我給出示例代碼前,讓我們先來看看PBKDF2算法。
  • 美國國家標準與技術研究院推薦。
  • 可以通過調整key來擴展,從而避免暴力破解。通過key擴展的基本思路是,在將密碼哈希后,再使用key加上哈希值再使用相同的算法進行多次的哈希。如果黑客嘗試去破解的話,他會因此多花費幾十億次計算的時間。前面提到過,越慢越好,PBKDF2可以通過指定迭代次數,你想讓他多慢,他就有多慢。
  • 通過加鹽的方式預防彩虹表的破解方式。鹽是一個添加到用戶的密碼哈希過程中的一段隨機序列。這個機制能夠防止通過預先計算結果的彩虹表破解。每個用戶都有自己的鹽,這樣的結果就是即使用戶的密碼相同,通過加鹽后哈希值也將不同。然而,在將鹽與密文存儲的位置上有很多矛盾的地方,有的時候將兩者存在一起比較方便,有的時候為了安全考慮又不得不將兩者分開存儲。由于PBKDF2算法通過key的機制避免了暴力破解,我覺得沒必要將鹽隱藏起來,就跟密文存儲在同一個位置。
  • 不需要額外的庫或者工具,這是一個開源的實現,在工作環境中能很方便的使用。

最后讓我們來看一個例子

這里使用PBKDF2加密的Java代碼,僅僅依賴Java SE 6.
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

public class PasswordEncryptionService {

 public boolean authenticate(String attemptedPassword, byte[] encryptedPassword, byte[] salt)
   throws NoSuchAlgorithmException, InvalidKeySpecException {
  // Encrypt the clear-text password using the same salt that was used to
  // encrypt the original password
  byte[] encryptedAttemptedPassword = getEncryptedPassword(attemptedPassword, salt);

  // Authentication succeeds if encrypted password that the user entered
  // is equal to the stored hash
  return Arrays.equals(encryptedPassword, encryptedAttemptedPassword);
 }

 public byte[] getEncryptedPassword(String password, byte[] salt)
   throws NoSuchAlgorithmException, InvalidKeySpecException {
  // PBKDF2 with SHA-1 as the hashing algorithm. Note that the NIST
  // specifically names SHA-1 as an acceptable hashing algorithm for PBKDF2
  String algorithm = "PBKDF2WithHmacSHA1";
  // SHA-1 generates 160 bit hashes, so that's what makes sense here
  int derivedKeyLength = 160;
  // Pick an iteration count that works for you. The NIST recommends at
  // least 1,000 iterations:
  // http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf
  // iOS 4.x reportedly uses 10,000:
  // http://blog.crackpassword.com/2010/09/smartphone-forensics-cracking-blackberry-backup-passwords/
  int iterations = 20000;

  KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, derivedKeyLength);

  SecretKeyFactory f = SecretKeyFactory.getInstance(algorithm);

  return f.generateSecret(spec).getEncoded();
 }

 public byte[] generateSalt() throws NoSuchAlgorithmException {
  // VERY important to use SecureRandom instead of just Random
  SecureRandom random = SecureRandom.getInstance("SHA1PRNG");

  // Generate a 8 byte (64 bit) salt as recommended by RSA PKCS5
  byte[] salt = new byte[8];
  random.nextBytes(salt);

  return salt;
 }
}

流程是這樣:

  1. 當增加一個用戶的時候,調用generateSalt()生成鹽,然后調用getEncryptedPassword(),同時存儲鹽和密文。再次強調,不要存儲明文密碼,不要存儲明文密碼,因為沒必要!不要擔心將鹽和密文存儲在同一張表中,上面已經說過了,這個無關緊要。
  2. 當認證用戶的時候,從數據庫中取出鹽和密文,將他們和明文密碼同時傳給authenticate(),根據返回結果判斷是否認證成功。
  3. 當用戶修改密碼的時候,仍然可以使用原來的鹽,只需要調用getEncryptedPassword()方法重新生成密文就可以了。
參考文檔:

原文鏈接

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!