天天看點

【Java編碼準則】の #13使用散列函數儲存密碼

     明文儲存密碼的程式在很多方面容易造成密碼的洩漏。雖然使用者輸入的密碼一般時明文形式,但是應用程式必須保證密碼不是以明文形式存儲的。

     限制密碼洩漏危險的一個有效的方法是使用散列函數,它使得程式中可以間接的對使用者輸入的密碼和原來的密碼進行比較,而不需要儲存明文或者對密碼進行解密後比較。這個方法使密碼洩漏的風險降到最低,同時沒有引入其他缺點。

[加密散列函數]

     散列函數産生的值稱為哈希值或者消息散列,散列函數是計算可行函數,但反過來是計算不可行的。事實上,密碼可以被編碼為一個哈希值,但哈希值不能被解碼成對應的密碼。兩個密碼是否相同,可以通過判斷它們的哈希值是否相等。

     一個好的實踐是對明文加鹽之後再進行哈希值的計算。鹽是一個唯一的序列或者随機生成的資料片段,通常和哈希值一起存放。鹽的作用是防止哈希值的暴力破解,前提是鹽足夠長以産生足夠的熵(長度較短的鹽值不足以降低暴力攻擊的速度)。每個密碼必須有自己的唯一的鹽,如果多個密碼共用同一個鹽的話,兩個使用者可能會有相同的密碼。

     散列函數和鹽的長度的選擇是安全和性能的一個均衡。選擇強度高的散列函數以增強暴力破解的難度的同時,也會增加密碼驗證的時間。增強鹽的長度可以增大暴力破解的難度,但同時會增加額外的存儲空間。

     Java的MessageDigest類提供了多種加密散列函數的實作,但要避免使用有缺陷的散列函數,例如MD5,而像SHA-1和SHA-2之類由美國國家安全局維護的散列函數,目前是安全的。實踐中,很多應用使用SHA-256散列函數,因為這個函數兼顧了性能和安全。

[不符合安全要求的代碼示例]

     下面的代碼使用對稱加密算法對存儲在password.bin檔案中的密碼進行加密和解密操作。

public final class Password {

  private void setPassword(byte[] pass) throws Exception {
    // arbitrary encryption scheme
    byte[] encryted = encrypt(pass);
    clearArray(pass);
    
    // encrypted password to password.bin
    saveBytes(encrypted, "password.bin");
    clearArray(encrypted);
  }
  
  boolean checkPassword(byte[] pass) throws Exception {
    // load the encrypted password
    byte[] encrypted = loadBytes("password.bin");
    byte[] decrpyted = decrypt(encrypted);
    boolean arraysEqual = Arrays.equals(decrypted, pass);
    clearArray(decrypted);
    clearArray(pass);
    return arraysEqual;
  }
  
  private void clearArray(byte[] a) {
    for (int i=0; i<a.length; ++i) {
      a[i] = 0;
    }
  }
}      

     攻擊者可能對上述bin檔案進行解密,然後獲得密碼,尤其當攻擊者知道程式中使用的密鑰和加密方式時。密碼甚至必須不讓系統管理者或者特權使用者知道。是以,使用加密方式對防止密碼洩漏危險的作用有限。

[不符合安全的代碼示例]

     下面代碼基于MessageDigest類使用SHA-256散列函數來對比字元串,而不是使用明文,但它使用的是String對象來存儲密碼。

public final class Password {

  private void setPassword(String pass) throws Exception {
    byte[] salt = generateSalt(12);
    MessageDigest msgDigest = MessageDigest.getInstance("SHA-256");
    // encode the string and salt
    byte[] hashVal = msgDigest.digest((pass + salt).getBytes());
    saveBytes(salt, "salt.bin");
    // save the hash value to password.bin
    saveBytes(hashVal, "password.bin");
  }
  
  boolean checkPassword(String pass) throws Exception {
    byte[] salt = loadBytes("salt.bin");
    MessageDigest msgDigest = MessageDigest.getInstance("SHA-256");
    // encode the string and salt
    byte[] hashVal1 = msgDigest.digest((pass + salt).getBytes());
    // load the hash value stored in password.bin
    byte[] hashVal2 = loadBytes("password.bin");
    return Arrays.equals(hashVal1, hashVal2);
  }
  
  private byte[] generateSalt(int n) {
    // generate a random byte array of length n
  }
}      

     即使攻擊者知道程式使用SHA-256散列函數和12位元組的鹽,他還是不能從password.bin和salt.bin中擷取擷取真實的密碼。

     雖然上面的代碼解決了密碼被解密的問題,但是這段代碼使用String對象來存放明文的密碼,是以,可參見“#00限制敏感資料的生命周期”進行改進。

[符合安全要求的解決方案]

     下面的代碼使用byte數組來存儲明文密碼。

public final class Password {

  private void setPassword(byte[] pass) throws Exception {
    byte[] salt = generateSalt(12);
    byte[] input = appendArrays(pass, salt);
    MessageDigest msgDigest = MessageDigest.getInstance("SHA-256");
    // encode the string and salt
    byte[] hashVal = msgDigest.digest(input);
    clearArray(pass);
    clearArray(input);
    saveBytes(salt, "salt.bin");
    
    // save the hash value to password.bin
    saveBytes(hashVal, "password.bin");
    clearArray(salt);
    clearArray(hashVal);
  }
  
  boolean checkPassword(byte[] pass) throws Exception {
    byte[] salt = loadBytes("salt.bin");
    byte[] input = appendArrays(pass, salt);
    MessageDigest msgDigest = MessageDigest.getInstance("SHA-256");
    // encode the string and salt
    byte[] hashVal1 = msgDigest.digest(input);
    clearArray(pass);
    clearArray(input);
    
    // load the hash value stored in password.bin
    byte[] hashVal2 = loadBytes("password.bin");
    boolean arraysEqual = Arrays.equals(hashVal1, hashVal2);
    clearArray(hashVal1);
    clearArray(hashVal2);
    return arraysEqual;
  }
  
  private byte[] generateSalt(int n) {
    // generate a random byte array of length n
  }
  
  private byte[] appendArray(byte[] a, byte[] b) {
    // return a new array of a[] appended to b[]
  }
  
  private void clearArray(byte[] a) {
    for (int i=0; i<a.length; ++i) {
      a[i] = 0;
    }
  }
}      

     在setPassword()和checkPassword()函數中,明文密碼在使用後立即清空。是以,攻擊者在明文密碼清空後必須花費更多精力才能擷取明文密碼。提供有保證的密碼清空是極具挑戰的,它可能是平台相關的,甚至可能由于複制垃圾回收/動态分頁/其他運作在Java語言之下的平台機制而變得不可行。

[适用性]

     沒有經過安全散列運算就進行存儲的密碼容易被非法的使用者擷取到,違背本條約将導緻程式容易被非法利用。

像密碼管理器這樣的應用程式可能需要取得原始密碼并将其填寫到第三方應用中。這一點是允許的,即使它違背了本條約。密碼管理器是由單一使用者通路的,而且一般是經過使用者許可的才可以儲存使用者的密碼,同時根據要求進行密碼的展示。是以,安全的決定因素取決于使用者個人的能力而非程式的功能。