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》将分析【用户登录】流程的密码验证过程。
References:
1, MSDN, Duwamish 7.0