製作帶有會員登入的網站時,免不了要在資料庫紀錄密碼欄位
早期的開發人員對資安不講究,常看到以明文儲存密碼的荒唐事
只要資料庫被駭,所有人的帳號密碼都直接曝光
即使沒有被駭,也可能發生管理者監守自盜的情形
總之,密碼用明文儲存是個極度不正確的事情

在出現了雜湊後,由於雜湊值不可逆的特性,也被用於密碼的加密上
稍微解釋下,雜湊演算法會將內容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做比對

arrow
arrow
    文章標籤
    Java
    全站熱搜
    創作者介紹
    創作者 蕭雲 的頭像
    蕭雲

    正因為活著

    蕭雲 發表在 痞客邦 留言(0) 人氣()