Duwamish密碼分析篇, Part 1
Written by: Rickie Lee
Nov. 05, 2004
繼續前面關於Duwamish的POST,這裏將學習Duwamish中關於Password的處理方式。Duwamish 7.0範例中的帳戶密碼通過SHA1散列運算和對散列執行Salt運算後,是以byte形式存放在Database中,避免明文的方式,以提高系統的安全性。
Duwamish的用戶註冊部分是封裝在/web/modules/accountmodule.ascx用戶控件內。隨便提一下,Duwamish web tier中採用了大量的user control,並且所有的user control都繼承/web/ModuleBase.cs 類,與web page繼承PageBase.cs類相似,這種做法值得推薦。Duwamish中user control主要是封裝一些相應的功能,模塊化。這樣不僅可以在本web項目內重用,而且以後維護也比較方便,如/web/modules/accountmodule.ascx user control就封裝了用戶註冊部分的功能。
下面看看【用戶註冊】功能模塊具體的實現代碼(/web/modules/accountmodule.ascx):
1,獲取用戶登記/註冊password,並帳戶密碼執行散列運算。
byte [] bytePassword = null;
String tmpPassword = PasswordTextBox.Text;
if (tmpPassword == ConfirmPasswordTextBox.Text)
{
SHA1 sha1 = SHA1.Create();
bytePassword = sha1.ComputeHash(Encoding.Unicode.GetBytes(tmpPassword));
}
……
retVal = (new CustomerSystem()).CreateCustomer(EmailTextBox.Text,
bytePassword,
AcctNameTextBox.Text,
AddressTextBox.Text,
CountryTextBox.Text,
PhoneTextBox.Text,
FaxTextBox.Text,
out moduleCustomerInfo);
先使用實現 160 位 SHA-1 標準的 System.Security.Cryptography 命名空間對密碼進行散列運算。然後調用BusinessFacade/CustomerSystem類的CreateCustomer()方法。
知識點:
散列簡介
散列(Hash)是一種單向算法,一旦數據被轉換,將無法再獲得其原始值。大多數開發人員使用數據庫存儲密碼,如果密碼直接以明文的形式存放在數據庫中,則開發人員也能夠看到這些密碼,甚至包括用戶的Credit Card信息。
不過,我們可以使用散列算法對密碼進行加密,然後再將其存儲在數據庫中。用戶輸入密碼後,可以再次使用散列算法對其進行轉換,然後將其與存儲在數據庫中的散列進行比較。散列的特點之一是,即使原始數據只發生一個小小的改動,數據的散列也會發生非常大的變化。Rickie 和 Ricky 這兩個單詞非常相似,但使用散列算法加密後的結果卻相去甚遠。你可能根本看不出二者之間有什麼相似之處。
.NET 開發人員可以使用多種散列算法類。最常用的是 SHA1 和 MD5。下面我們看一下如何爲Rickie這樣的普通字符串生成散列,使任何人都無法識別它。
(1)使用 SHA1 生成散列
通過如下的示例代碼,來演示如何通過SHA1生成散列:
byte [] bytePassword = null;
string tmpPassword = txtPassword.Text.Trim();
// 創建新的加密服務提供程序對象
SHA1 sha1 = SHA1.Create();
// 將原始字符串轉換成字節數組,然後計算散列,並返回一個字節數組
bytePassword = sha1.ComputeHash(Encoding.Unicode.GetBytes(tmpPassword));
// Releases all resources used by the System.Security.Cryptography.HashAlgorithm.
sha1.Clear();
// 返回散列值的 Base64 編碼字符串
txtResults.Text = Convert.ToBase64String(bytePassword);
傳遞不同的字符串值來調用該例程,查看散列值的變化。例如,如果將字符串Rickie傳遞給該例程,輸出結果:
v8ocXHBvlh4EqY/2HsJNH5XBVG0=
現在,將此過程中的輸入值更改爲Ricky。你將看到以下輸出結果:
luQsSa61sB/7PT9piDx+OAGqCnI=
如此可見,輸入字符串的一個小小變化就會產生完全不同的字符組合。這正是散列算法之所以有效的原因,它使我們很難找到輸入字符串的規律,也很難根據加密後的字符弄清楚字符串原來的模樣。
(2)使用MD5也可以生成散列
通過如下的示例代碼,來演示如何通過MD5生成散列:
byte [] bytePassword = null;
string tmpPassword = txtPassword.Text.Trim();
MD5 md5 = MD5.Create();
bytePassword = md5.ComputeHash(Encoding.Unicode.GetBytes(tmpPassword));
// Releases all resources used by the System.Security.Cryptography.HashAlgorithm.
md5.Clear();
txtResults.Text = Convert.ToBase64String(bytePassword);
輸入Rickie,MD5散列算法的輸出結果:
YUqR1JfNxrciyG0ixNj58A==
同樣,加密後的字符串看起來也與原始輸入相去甚遠。這些散列算法對於創建沒有任何意義的密碼來說非常有用,也使黑客很難猜出這些密碼。之所以使用散列算法,是因爲可以用這種算法對密碼進行加密並將其存儲在數據庫中。然後,當用戶輸入真實密碼時,需要先對用戶輸入的密碼進行同樣的散列,然後通過網絡發送到數據庫中,比較它與數據庫中的密碼是否匹配。
請記住,散列是單向操作。使用散列算法對原始密碼加密後將無法再恢復。
上述兩種散列算法都執行同一種操作。不同之處只在於生成散列的密鑰大小以及使用的算法。使用的密鑰越大,加密就越安全。例如,MD5 使用的加密密鑰比 SHA1 使用的密鑰大,因此 MD5 散列較難破解。
對於散列算法要考慮的另外一點是,從實踐或理論的角度上看是否存在衝突的可能性。衝突是我們所不希望的,因爲兩個不同的單詞可能會生成相同的散列。例如,SHA1 從實踐或理論上來講沒有發生衝突的可能性。MD5 從理論上講有發生衝突的可能性,但從實踐上講沒有發生衝突的可能性。因此,選擇哪種算法歸根結底取決於所需要的安全級別。
(3)Summary
一般情況下,將上述加密的字節數組,通過使用Convert.ToBase64String(bytePassword)方法把字節數組轉換成 Base64 編碼的字符串,然後存儲在數據庫中即可完成一般的商業應用。
2,調用BusinessFacade/CustomerSystem類,對散列執行Salt運算。
到目前爲止,散列算法暴露出來的問題之一是,如果兩個用戶碰巧使用相同的密碼,那麼散列值將完全相同。如果黑客看到您存儲密碼的表格,會從中找到規律並明白您很可能使用了常見的詞語,然後黑客會開始詞典攻擊以確定這些密碼。要確保任何兩個用戶密碼的散列值都不相同,一種方法是在加密密碼之前,在每個用戶的密碼中添加一個唯一的值。這個唯一值稱爲“鹽”值(Salt)。
雖然對密碼執行散列運算是一個好的開端,但若要增加免受潛在攻擊的安全性,則可以對密碼散列執行 Salt 運算。Salt 就是在已執行散列運算的密碼中插入的一個隨機數字。這一策略有助於阻止潛在的攻擊者利用預先計算的字典攻擊。字典攻擊是攻擊者使用密鑰的所有可能組合來破解密碼的攻擊。當您使用 Salt 值使散列運算進一步隨機化後,攻擊者將需要爲每個 Salt 值創建一個字典,這將使攻擊變得非常複雜且成本極高。
Salt 值隨散列存儲在一起,並且未經過加密。所存儲的 Salt 值可以在隨後用於密碼驗證。
下面看看Duwamish 7.0中是如何實現Salt運算:
(1)BusinessFacade/CustomerSystem class中Create Customer()方法
public bool CreateCustomer(String emailAddress,
byte [] password,
String name,
String address,
String country,
String phoneNumber,
String fax,
out CustomerData custData)
{
// create a salted password
byte [] saltedPassword = CreateDbPassword(password);
//
// Create a new row
//
custData = new CustomerData();
DataTable table = custData.Tables[CustomerData.CUSTOMERS_TABLE];
DataRow row = table.NewRow();
//
// Fill input data into new row
//
row[CustomerData.EMAIL_FIELD] = emailAddress;
row[CustomerData.PASSWORD_FIELD] = saltedPassword;
row[CustomerData.NAME_FIELD] = name;
row[CustomerData.ADDRESS_FIELD] = address;
row[CustomerData.COUNTRY_FIELD] = country;
row[CustomerData.PHONE_FIELD] = phoneNumber;
row[CustomerData.FAX_FIELD] = fax;
//
// Add it to the table
//
table.Rows.Add(row);
// 調用Business rules tier的Customer Class
// Insert the customer using the business rules
//
return (new Customer()).Insert(custData);
}
首先調用Facade/CustomerSystem 類的私有方法CreateDbPassword(),獲取對散列執行Salt運算結果(長度爲24個字節的byte數組),然後調用Business rules tier中的Customer class的Insert()方法,將用戶信息,包括密碼存放在數據庫中。
(2)Facade/CustomerSystem 類的私有方法 CreateDbPassword()
// create salted password to save in Db
private byte [] CreateDbPassword(byte[] unsaltedPassword)
{
//Create a salt value
byte[] saltValue = new byte[saltLength];
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
//用加密型強隨機字節填充的數組
rng.GetBytes(saltValue);
return CreateSaltedPassword(saltValue, unsaltedPassword);
}
上述代碼片斷使用 .NET Framework 類 RNGCryptoServiceProvider 創建一個隨機的數字字符串。RNG 表示隨機數生成器。該類可以創建一個任意長度的隨機字節數組,長度由您指定。您可以使用此隨機字節數組作爲散列算法的Salt值。要採用這種方法,必須安全地存儲該Salt值。
saltLength=4(常量),Duwamish 7 示例用RNGCryptoServiceProvider創建一個 4 字節 Salt 值。然後調用Facade/CustomerSystem 類的私有方法CreateSaltedPassword(),獲取對散列執行Salt運算後的結果。
(3)Facade/CustomerSystem 類的私有方法CreateSaltedPassword()
// create a salted password given the salt value
private byte[] CreateSaltedPassword(byte[] saltValue, byte[] unsaltedPassword)
{
// add the salt to the hash
byte[] rawSalted = new byte[unsaltedPassword.Length + saltValue.Length];
// Copies all the elements of the current one-dimensional System.Array to the specified one-dimensional System.Array starting at the specified destination System.Array index.
unsaltedPassword.CopyTo(rawSalted,0);
saltValue.CopyTo(rawSalted,unsaltedPassword.Length);
//Create the salted hash
SHA1 sha1 = SHA1.Create();
byte[] saltedPassword = sha1.ComputeHash(rawSalted);
// add the salt value to the salted hash
byte[] dbPassword = new byte[saltedPassword.Length + saltValue.Length];
saltedPassword.CopyTo(dbPassword,0);
saltValue.CopyTo(dbPassword,saltedPassword.Length);
return dbPassword;
}
該方法根據傳入的Salt值(長度爲4個字節的byte數組)和已執行散列運算的密碼(長度爲20個字節的byte數組),拼接爲長度爲24的byte數組。然後對上述拼接後的數組再進行SHA1散列運算,得到結果saltedPassword(長度爲20個字節的byte數組)。
最後將saltedPassword(長度爲20個字節的byte數組)和Salt值(長度爲4個字節的byte數組)拼接爲dbPassword(長度爲4個字節的byte數組)返回。
3,調用BusinessRules/Customer類的Insert()方法。
Insert()方法根據傳入的CustomerData對象,驗證數據的合法性,然後調用Data Access tier的Customers對象的InsertCustomer()方法。
具體代碼請參考Duwamish 7.0範例。
4,調用DataAccess/Customers類的InsertCustomer()方法。
InsertCustomer()方法根據傳入的CustomerData對象,調用Database端的Stored Procedure,執行真正的數據庫insert操作。可以觀察到Duwamish7 Database中Customers表的Password字段類型爲binary且長度爲24。
具體代碼請參考Duwamish 7.0範例。
下一篇POST《Duwamish密碼分析篇 Part 2》將分析【用戶登錄】流程的密碼驗證過程。