製作帶有會員登入的網站時,免不了要在資料庫紀錄密碼欄位
早期的開發人員對資安不講究,常看到以明文儲存密碼的荒唐事
只要資料庫被駭,所有人的帳號密碼都直接曝光
即使沒有被駭,也可能發生管理者監守自盜的情形
總之,密碼用明文儲存是個極度不正確的事情
在出現了雜湊後,由於雜湊值不可逆的特性,也被用於密碼的加密上
稍微解釋下,雜湊演算法會將內容A轉成雜湊值B,但無法從B推回A的內容,且A以外的內容都無法轉成B
所以將輸入的密碼A做雜湊運算後,跟紀錄的雜湊值B比對,就能知道輸入的內容是否相同
密碼可不用以明文紀錄在資料庫
但雜湊也不是萬能的,像是比較不嚴謹的雜湊算法可能造成「碰撞」
(有多個內容能轉換成B,不符合唯一性)
還有彩虹表攻擊(先行算好常用密碼的雜湊,
假設密碼"AAA"可以轉成雜湊值"ABC",只要我在資料庫看到ABC就能反推回AAA)
於是又衍生了加鹽(Salt)的概念,也就是將密碼原文增加特殊文字後再進行雜湊
例如"AAA"先轉成"*&AAA&*",雜湊值可能會變成"DEF"
這樣不知道加鹽規則就無法使用彩虹表
只是這種作法治標不治本,因為加鹽規則固定
只要駭客知道規則仍可產生新的彩虹表來比對
比較好的作法是以亂數產生鹽,讓同樣密碼每次加密都能變成不同雜湊
如此一來,即使雜湊密碼洩漏,也能大幅降低被算出密碼的可能性
雖然說了這麼多,那要如何實作呢?
在ASP.NET MVC預設引用參考System.Web.Helpers
已經實作了Crypto.HashPassword跟Crypto.VerifyHashPassword兩種方法
(ASP.NET Core也有,不過是放在Microsoft.AspNet.Identity的Crypto)
用法也非常簡單,將密碼傳入HashPassword就能做出雜湊值
將輸入的密碼與雜湊值傳入VerifyHashPassword就能比對是否相同
(由於有加隨機鹽,不能直接把輸入密碼用HashPassword後比對)
雖然.NET內建了這種方法,但Java並沒有內建
這導致我在.NET產生出來的雜湊密碼,無法在Java環境比對
我一開始也嘗試找了Java版本的實作(網路上幾乎都是照抄同份Code)
但測試後發現驗證出來的結果不相符!?
這是怎麼一回事呢?我追了一下Source Code(感謝.NET開源)
發現是程式與參數不同導致的
首先Crypto.HashPassword是採用Rfc2898DeriveBytes類別
這是依據 HMACSHA1 使用虛擬亂數產生器,實作密碼式的金鑰衍生功能 PBKDF2
這個方法需要傳入迭代次數、Key長度、Salt長度三個數值
在.NET中三個值設定為1000次/256 bits/128 bits
與網路流傳的程式碼設定不相符
另外網路流傳的程式碼是將密碼與鹽一同傳入並組合
但.NET版本只需要傳密碼,密碼本身已含鹽,不需要再加鹽
修改這兩點後,兩者就能做出一樣的結果
我測試過.NET做出的Hash在Java可以正常比對,反之亦然
以下是原始碼,其中encode/decode base64需引入commons-codec這個套件
如果不想引入,也可上網抓encodeBase64String跟decodeBase64的原始碼
相關代碼我已經上傳到Github,有需要可以參考
---
Rfc2898DeriveBytes.java
---
package com.web.helpers; import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Random; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Base64; /** * This implementation follows RFC 2898 recommendations. See * http://www.ietf.org/rfc/Rfc2898.txt */ public class Rfc2898DeriveBytes { private static final int BLOCK_SIZE = 20; private static Random random = new Random(); private Mac hmacsha1; private byte[] salt; private int iterations; private byte[] buffer = new byte[BLOCK_SIZE]; private int startIndex = 0; private int endIndex = 0; private int block = 1; /** * Creates new instance. * * @param password * The password used to derive the key. * @param salt * The key salt used to derive the key. * @param iterations * The number of iterations for the operation. * @throws NoSuchAlgorithmException * HmacSHA1 algorithm cannot be found. * @throws InvalidKeyException * Salt must be 8 bytes or more. -or- Password cannot be null. */ public Rfc2898DeriveBytes(byte[] password, byte[] salt, int iterations) throws NoSuchAlgorithmException, InvalidKeyException { this.salt = salt; this.iterations = iterations; this.hmacsha1 = Mac.getInstance("HmacSHA1"); this.hmacsha1.init(new SecretKeySpec(password, "HmacSHA1")); } /** * Creates new instance. * * @param password * The password used to derive the key. * @param salt * The key salt used to derive the key. * @param iterations * The number of iterations for the operation. * @throws NoSuchAlgorithmException * HmacSHA1 algorithm cannot be found. * @throws InvalidKeyException * Salt must be 8 bytes or more. -or- Password cannot be null. * @throws UnsupportedEncodingException */ public Rfc2898DeriveBytes(String password, int saltSize, int iterations) throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException { this.salt = randomSalt(saltSize); this.iterations = iterations; this.hmacsha1 = Mac.getInstance("HmacSHA1"); this.hmacsha1.init(new SecretKeySpec(password.getBytes("UTF-8"), "HmacSHA1")); this.buffer = new byte[BLOCK_SIZE]; this.block = 1; this.startIndex = this.endIndex = 1; } /** * Creates new instance. * * @param password * The password used to derive the key. * @param salt * The key salt used to derive the key. * @param iterations * The number of iterations for the operation. * @throws NoSuchAlgorithmException * HmacSHA1 algorithm cannot be found. * @throws InvalidKeyException * Salt must be 8 bytes or more. -or- Password cannot be null. * @throws UnsupportedEncodingException */ public Rfc2898DeriveBytes(String password, int saltSize) throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException { this(password, saltSize, 1000); } /** * Creates new instance. * * @param password * The password used to derive the key. * @param salt * The key salt used to derive the key. * @param iterations * The number of iterations for the operation. * @throws NoSuchAlgorithmException * HmacSHA1 algorithm cannot be found. * @throws InvalidKeyException * Salt must be 8 bytes or more. -or- Password cannot be null. * @throws UnsupportedEncodingException * UTF-8 encoding is not supported. */ public Rfc2898DeriveBytes(String password, byte[] salt, int iterations) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException { this(password.getBytes("UTF8"), salt, iterations); } public byte[] getSalt() { return this.salt; } public String getSaltAsString() { return Base64.encodeBase64String(this.salt); } /** * Returns a pseudo-random key from a data, salt and iteration count. * * @param cb * Number of bytes to return. * @return Byte array. */ public byte[] getBytes(int cb) { byte[] result = new byte[cb]; int offset = 0; int size = this.endIndex - this.startIndex; if (size > 0) { // if there is some data in buffer if (cb >= size) { // if there is enough data in buffer System.arraycopy(this.buffer, this.startIndex, result, 0, size); this.startIndex = this.endIndex = 0; offset += size; } else { System.arraycopy(this.buffer, this.startIndex, result, 0, cb); startIndex += cb; return result; } } while (offset < cb) { byte[] block = this.func(); int remainder = cb - offset; if (remainder > BLOCK_SIZE) { System.arraycopy(block, 0, result, offset, BLOCK_SIZE); offset += BLOCK_SIZE; } else { System.arraycopy(block, 0, result, offset, remainder); offset += remainder; System.arraycopy(block, remainder, this.buffer, startIndex, BLOCK_SIZE - remainder); endIndex += (BLOCK_SIZE - remainder); return result; } } return result; } public static byte[] randomSalt(int size) { byte[] salt = new byte[size]; random.nextBytes(salt); return salt; } /** * Generate random Salt * * @param size * @return */ public static String generateSalt(int size) { byte[] salt = randomSalt(size); return Base64.encodeBase64String(salt); } private byte[] func() { this.hmacsha1.update(this.salt, 0, this.salt.length); byte[] tempHash = this.hmacsha1.doFinal(getBytesFromInt(this.block)); this.hmacsha1.reset(); byte[] finalHash = tempHash; for (int i = 2; i <= this.iterations; i++) { tempHash = this.hmacsha1.doFinal(tempHash); for (int j = 0; j < 20; j++) { finalHash[j] = (byte) (finalHash[j] ^ tempHash[j]); } } if (this.block == 2147483647) { this.block = -2147483648; } else { this.block += 1; } return finalHash; } private static byte[] getBytesFromInt(int i) { return new byte[] { (byte) (i >>> 24), (byte) (i >>> 16), (byte) (i >>> 8), (byte) i }; } }
---
Crypto.java
---
package com.web.helpers; import org.apache.commons.codec.binary.Base64; public class Crypto { private static int saltSize = 16; private static int iterations = 1000; private static int subKeySize = 32; /** * Get salt. * @return */ public static String getSalt() { return Rfc2898DeriveBytes.generateSalt(saltSize); } /** * Get hashed password. * @param password * @return */ public static String hashPassword(String password) { Rfc2898DeriveBytes keyGenerator = null; try { keyGenerator = new Rfc2898DeriveBytes(password, saltSize, iterations); } catch (Exception e1) { e1.printStackTrace(); } byte[] subKey = keyGenerator.getBytes(subKeySize); byte[] bSalt = keyGenerator.getSalt(); byte[] hashPassword = new byte[1 + saltSize + subKeySize]; System.arraycopy(bSalt, 0, hashPassword, 1, saltSize); System.arraycopy(subKey, 0, hashPassword, saltSize + 1, subKeySize); return Base64.encodeBase64String(hashPassword); } /** * Verify hashed password. * @param hashedPassword * @param password * @return */ public static boolean verifyHashedPassword(String hashedPassword, String password) { byte[] hashedPasswordBytes = Base64.decodeBase64(hashedPassword); if (hashedPasswordBytes.length != (1 + saltSize + subKeySize) || hashedPasswordBytes[0] != 0x00) { return false; } byte[] bSalt = new byte[saltSize]; System.arraycopy(hashedPasswordBytes, 1, bSalt, 0, saltSize); byte[] storedSubkey = new byte[subKeySize]; System.arraycopy(hashedPasswordBytes, 1 + saltSize, storedSubkey, 0, subKeySize); Rfc2898DeriveBytes deriveBytes = null; try { deriveBytes = new Rfc2898DeriveBytes(password, bSalt, iterations); } catch (Exception e) { e.printStackTrace(); } byte[] generatedSubkey = deriveBytes.getBytes(subKeySize); return byteArraysEqual(storedSubkey, generatedSubkey); } /** * Compare two byte arrays are equal or not. * @param storedSubkey * @param generatedSubkey * @return */ private static boolean byteArraysEqual(byte[] storedSubkey, byte[] generatedSubkey) { int size = storedSubkey.length; if (size != generatedSubkey.length) { return false; } for (int i = 0; i < size; i++) { if (storedSubkey[i] != generatedSubkey[i]) { return false; } } return true; } }
---
使用方法同C#,呼叫Crypto.hashPassword做雜湊
Crypto.verifyHashedPassword做比對
留言列表