浅析ASP.NET 2.0的用户密码加密机制

1 加Salt散列
  2 ASP.NET 2.0 Membership中与密码散列有关的代码

  声明:本文所罗列之源代码均通过Reflector取自.NET Framework类库,Anders Liu引用这些代码仅出于学习和研究的目的。

  前一段关于密码的存储问题产生了一些讨论。我所看到的景象是,首先在cnbeta新 闻中提到中国某银行将强制冻结密码过于简单(如6个8)的帐户,引发了争论。一方认为银行采用明文存放用户密码;另一方则认为,即便密码是经过散列存放的,但只要得到“6个8”的散列值,通过对比散列值也可以发现具有特定密码的用户。

  后来在博客园(cnblogs.com)也看到有朋友发帖讨论了密码的散列存储,再后来在MVP的QQ群里也就这个问题小小议论了一番。

  其实,对密码进行散列存储不是一个新鲜话题了,解决起来也不是很难,但很多人还是不大了解。Anders Liu这个小文只是强调一下“加Salt散列”这个简单的技术,并给出ASP.NET Membership所使用的代码。

  本来打算写一篇介绍如何实现用户登录功能的文章的,但因为时间有限,所以先介绍一下密码的散列,下一篇再介绍用户登录。
  1 密码必须散列存储

  (内容略)

  2 加Salt散列

  我们知道,如果直接对密码进行散列,那么黑客(统称那些有能力窃取用户数据并企图得到用户密码的人)可以对一个已知密码进行散列,然后通过对比散列值得到某用户的密码。换句话说,虽然黑客不能取得某特定用户的密码,但他可以知道使用特定密码的用户有哪些。

  加Salt可以一定程度上解决这一问题。所谓加Salt,就是加点“佐料”。其基本想法是这样的——当用户首次提供密码时(通常是注册时),由系统自动往这个密码里撒一些“佐料”,然后再散列。而当用户登录时,系统为用户提供的代码撒上同样的“佐料”,然后散列,再比较散列值,已确定密码是否正确。

  这里的“佐料”被称作“Salt值”,这个值是由系统随机生成的,并且只有系统知道。这样,即便两个用户使用了同一个密码,由于系统为它们生成的salt值不同,他们的散列值也是不同的。即便黑客可以通过自己的密码和自己生成的散列值来找具有特定密码的用户,但这个几率太小了(密码和salt值都得和黑客使用的一样才行)。

  下面详细介绍一下加Salt散列的过程。介绍之前先强调一点,前面说过,验证密码时要使用和最初散列密码时使用“相同的”佐料。所以Salt值是要存放在数据库里的。
浅析ASP.NET 2.0的用户密码加密机制_第1张图片
  图1. 用户注册

  如图1所示,注册时,

  1)用户提供密码(以及其他用户信息);
  2)系统为用户生成Salt值;
  3)系统将Salt值和用户密码连接到一起;
  4)对连接后的值进行散列,得到Hash值;
  5)将Hash值和Salt值分别放到数据库中。
浅析ASP.NET 2.0的用户密码加密机制_第2张图片
  图2. 用户登录

  如图2所示,登录时,

  1)用户提供用户名和密码;
  2)系统通过用户名找到与之对应的Hash值和Salt值;
  3)系统将Salt值和用户提供的密码连接到一起;
  4)对连接后的值进行散列,得到Hash'(注意有个“撇”);
  5)比较Hash和Hash'是否相等,相等则表示密码正确,否则表示密码错误。

  3 ASP.NET 2.0 Membership中的相关代码

  (省略关于Membership的介绍若干字)

  本文Anders Liu仅研究了SqlMembershipProvider,该类位于System.Web.dll,System.Web.Security命名空间中。

  首先,要使用Membership,必须先用aspnet_regsql.exe命令来配置数据库,该工具会向现有数据库中添加一系列表和存储过程等,配置好的数据库中有一个表aspnet_Membership,就是用于存放用户帐户信息的。其中我们所关注的列有三个——Password、PasswordFormat和PasswordSalt。

  Password存放的是密码的散列值,PasswordFormat存放用于散列密码所使用的算法,PasswordSalt就是系统生成的Salt值了。

  注册时用到了该类的CreateUser方法,该方法主要代码如下:

CreateUser
public   override  MembershipUser CreateUser( string  username,  string  password,  string  email,  string  passwordQuestion,  string  passwordAnswer,  bool  isApproved,  object  providerUserKey,  out  MembershipCreateStatus status)
{
string  str3; 
MembershipUser user; 
if  ( ! SecUtility.ValidateParameter( ref  password,  true true false 0x80 ))
{
status 
=  MembershipCreateStatus.InvalidPassword; 
return   null
}
//  生成salt值
string  salt  =   base .GenerateSalt(); 
//  结合salt值对密码进行散列
string  objValue  =   base .EncodePassword(password, ( int this ._PasswordFormat, salt); 
if  (objValue.Length >  0x80 )
{
status 
=  MembershipCreateStatus.InvalidPassword; 
return   null
}
if  (passwordAnswer  !=   null )
{
passwordAnswer 
=  passwordAnswer.Trim(); 
}
if  ( ! string .IsNullOrEmpty(passwordAnswer))
{
 
if  (passwordAnswer.Length >  0x80 )
  {
  status 
=  MembershipCreateStatus.InvalidAnswer; 
  
return   null
}
  str3 
=   base .EncodePassword(passwordAnswer.ToLower(CultureInfo.InvariantCulture), ( int this ._PasswordFormat, salt); 
}
else
{
str3 
=  passwordAnswer; 
}
if  ( ! SecUtility.ValidateParameter( ref  str3,  this .RequiresQuestionAndAnswer,  true false 0x80 ))
{
status 
=  MembershipCreateStatus.InvalidAnswer; 
return   null
}
 
if  ( ! SecUtility.ValidateParameter( ref  username,  true true true 0x100 ))
{
status 
=  MembershipCreateStatus.InvalidUserName; 
return   null
}
if  ( ! SecUtility.ValidateParameter( ref  email,  this .RequiresUniqueEmail,  this .RequiresUniqueEmail,  false 0x100 ))
{
status 
=  MembershipCreateStatus.InvalidEmail; 
return   null
}
 
if  ( ! SecUtility.ValidateParameter( ref  passwordQuestion,  this .RequiresQuestionAndAnswer,  true false 0x100 ))
{
status 
=  MembershipCreateStatus.InvalidQuestion; 
return   null
}
if  ((providerUserKey  !=   null &&   ! (providerUserKey  is  Guid))
{
status 
=  MembershipCreateStatus.InvalidProviderUserKey; 
return   null
}
if  (password.Length <  this .MinRequiredPasswordLength)
{
status 
=  MembershipCreateStatus.InvalidPassword; 
return   null
}
int  num  =   0
for  ( int  i  =   0 ; i <password.Length; i ++ )
{
if  ( ! char .IsLetterOrDigit(password, i))
{
num
++
}
}
if  (num <  this .MinRequiredNonAlphanumericCharacters)
{
status 
=  MembershipCreateStatus.InvalidPassword; 
return   null
}
if  (( this .PasswordStrengthRegularExpression.Length >  0 &&   ! Regex.IsMatch(password,  this .PasswordStrengthRegularExpression))
{
status 
=  MembershipCreateStatus.InvalidPassword; 
return   null
}
ValidatePasswordEventArgs e 
=   new  ValidatePasswordEventArgs(username, password,  true ); 
this .OnValidatingPassword(e); 
if  (e.Cancel)
{
status 
=  MembershipCreateStatus.InvalidPassword; 
return   null
}
try
{
SqlConnectionHolder connection 
=   null
try
{
  connection 
=  SqlConnectionHelper.GetConnection( this ._sqlConnectionString,  true ); 
 
this .CheckSchemaVersion(connection.Connection); 
 DateTime time 
=   this .RoundToSeconds(DateTime.UtcNow); 
 SqlCommand command 
=   new  SqlCommand( " dbo.aspnet_Membership_CreateUser " , connection.Connection); 
 command.CommandTimeout 
=   this .CommandTimeout; 
 command.CommandType 
=  CommandType.StoredProcedure; 
 command.Parameters.Add(
this .CreateInputParam( " @ApplicationName " , SqlDbType.NVarChar,  this .ApplicationName)); 
 command.Parameters.Add(
this .CreateInputParam( " @UserName " , SqlDbType.NVarChar, username)); 
 command.Parameters.Add(
this .CreateInputParam( " @Password " , SqlDbType.NVarChar, objValue)); 
 command.Parameters.Add(
this .CreateInputParam( " @PasswordSalt " , SqlDbType.NVarChar, salt)); 
 command.Parameters.Add(
this .CreateInputParam( " @Email " , SqlDbType.NVarChar, email)); 
 command.Parameters.Add(
this .CreateInputParam( " @PasswordQuestion " , SqlDbType.NVarChar, passwordQuestion)); 
 command.Parameters.Add(
this .CreateInputParam( " @PasswordAnswer " , SqlDbType.NVarChar, str3)); 
 command.Parameters.Add(
this .CreateInputParam( " @IsApproved " , SqlDbType.Bit, isApproved)); 

 command.Parameters.Add(
this .CreateInputParam( " @UniqueEmail " , SqlDbType.Int,  this .RequiresUniqueEmail  ?   1  :  0 )); 
 command.Parameters.Add(
this .CreateInputParam( " @PasswordFormat " , SqlDbType.Int, ( int this .PasswordFormat)); 
 command.Parameters.Add(
this .CreateInputParam( " @CurrentTimeUtc " , SqlDbType.DateTime, time)); 
 SqlParameter parameter 
=   this .CreateInputParam( " @UserId " , SqlDbType.UniqueIdentifier, providerUserKey); 
parameter.Direction 
=  ParameterDirection.InputOutput; 
command.Parameters.Add(parameter); 
parameter 
=   new  SqlParameter( " @ReturnValue " , SqlDbType.Int); 
parameter.Direction 
=  ParameterDirection.ReturnValue; 
command.Parameters.Add(parameter); 
command.ExecuteNonQuery(); 
int  num3  =  (parameter.Value  !=   null ?  (( int ) parameter.Value) :  - 1
if  ((num3 <  0 ||  (num3 >  11 ))
{
num3 
=   11
}
status 
=  (MembershipCreateStatus) num3; 
if  (num3  !=   0 )
{
return   null
}
 providerUserKey 
=   new  Guid(command.Parameters[ " @UserId " ].Value.ToString()); 
 time 
=  time.ToLocalTime(); 
 user 
=   new  MembershipUser( this .Name, username, providerUserKey, email, passwordQuestion,  null , isApproved,  false , time, time, time, time,  new  DateTime( 0x6da 1 1 )); 
}
finally
{
if  (connection  !=   null )
{
connection.Close(); 
connection 
=   null
}
}
}
catch
{
throw
}
return  user; 


  其中我们可以看到两个比较令人感兴趣的方法:GenerateSalt和EncodePassword。由于本文讨论的仅仅是密码的散列,而不是整个用户注册过程,所以这里只对这两个函数进行分析。

  这两个方法来自于SqlMembershipProvider的父类,MembershipProvider。

GenerateSalt方法的代码比较简单:

GenerateSalt
internal   string  GenerateSalt()
{
byte [] data  =   new   byte [ 0x10 ]; 
new  RNGCryptoServiceProvider().GetBytes(data); 
return  Convert.ToBase64String(data); 


  但是要注意的是,在这种方法里Salt值的高度随机性是安全的保障,所以不能简单的使用Random来获取随机数,而应该使用更安全的方式。这里使用了RNGCryptoServiceProvider来生成随机数。

EncodePassword方法的代码也不难:

EncodePassword
internal   string  EncodePassword( string  pass,  int  passwordFormat,  string  salt)
{
if  (passwordFormat  ==   0 )
{
return  pass; 
}
//  将密码和salt值转换成字节形式并连接起来
byte [] bytes  =  Encoding.Unicode.GetBytes(pass); 
byte [] src  =  Convert.FromBase64String(salt); 
byte [] dst  =   new   byte [src.Length  +  bytes.Length]; 
byte [] inArray  =   null
Buffer.BlockCopy(src, 
0 , dst,  0 , src.Length); 
Buffer.BlockCopy(bytes, 
0 , dst, src.Length, bytes.Length); 
//  选择算法,对连接后的值进行散列
if  (passwordFormat  ==   1 )
{
 HashAlgorithm algorithm 
=  HashAlgorithm.Create(Membership.HashAlgorithmType); 
 
if  ((algorithm  ==   null &&  Membership.IsHashAlgorithmFromMembershipConfig)
 {
 RuntimeConfig.GetAppConfig().Membership.ThrowHashAlgorithmException(); 
 }
inArray 
=  algorithm.ComputeHash(dst); 
}
else
{
inArray 
=   this .EncryptPassword(dst); 
}
//  以字符串形式返回散列值
return  Convert.ToBase64String(inArray); 


  这段代码的作用就是,首先将密码和salt值转换成字节数组(分别放到bytes和src数组中),然后拼接到一起(dst数组)。之后再根据Web.config中设置的加密算法,对这个拼接值进行散列,最后把散列值转换成字符串形式返回。

  最后,用户登录时,将会使用SqlMembershipProvider的CheckPassword方法对密码进行检验。该方法有两种重载形式,最为完整的一种如下所示:

CheckPassword
private   bool  CheckPassword( string  username,  string  password,  bool  updateLastLoginActivityDate,  bool  failIfNotApproved,  out   string  salt,  out   int  passwordFormat)
{
SqlConnectionHolder connection 
=   null
string  str;  //  密码散列值
int  num; 
int  num2; 
int  num3; 
bool  flag2; 
DateTime time; 
DateTime time2; 
//  从数据库中拿到Hash和Salt
this .GetPasswordWithFormat(username, updateLastLoginActivityDate,  out  num,  out  str,  out  passwordFormat,  out  salt,  out  num2,  out  num3,  out  flag2,  out  time,  out  time2); 
if  (num  !=   0 )
{
return   false
}
if  ( ! flag2  &&  failIfNotApproved)
{
return   false
}
//  对用户刚刚输入的密码进行散列
string  str2  =   base .EncodePassword(password, passwordFormat, salt); 

//  比较两个散列值,看密码是否相等
bool  objValue  =  str.Equals(str2); 
if  ((objValue  &&  (num2  ==   0 ))  &&  (num3  ==   0 ))
{
return   true
}
try
{
 
try
 {
 connection 
=  SqlConnectionHelper.GetConnection( this ._sqlConnectionString,  true ); 
this .CheckSchemaVersion(connection.Connection); 
 SqlCommand command 
=   new  SqlCommand( " dbo.aspnet_Membership_UpdateUserInfo " , connection.Connection); 
DateTime utcNow 
=  DateTime.UtcNow; 
command.CommandTimeout 
=   this .CommandTimeout; 
command.CommandType 
=  CommandType.StoredProcedure; 
 command.Parameters.Add(
this .CreateInputParam( " @ApplicationName " , SqlDbType.NVarChar,  this .ApplicationName)); 
command.Parameters.Add(
this .CreateInputParam( " @UserName " , SqlDbType.NVarChar, username)); 
command.Parameters.Add(
this .CreateInputParam( " @IsPasswordCorrect " , SqlDbType.Bit, objValue)); 

 command.Parameters.Add(
this .CreateInputParam( " @UpdateLastLoginActivityDate " , SqlDbType.Bit, updateLastLoginActivityDate)); 
command.Parameters.Add(
this .CreateInputParam( " @MaxInvalidPasswordAttempts " , SqlDbType.Int,  this .MaxInvalidPasswordAttempts)); 
command.Parameters.Add(
this .CreateInputParam( " @PasswordAttemptWindow " , SqlDbType.Int,  this .PasswordAttemptWindow)); 
 command.Parameters.Add(
this .CreateInputParam( " @CurrentTimeUtc " , SqlDbType.DateTime, utcNow)); 
 command.Parameters.Add(
this .CreateInputParam( " @LastLoginDate " , SqlDbType.DateTime, objValue  ?  utcNow : time)); 
  command.Parameters.Add(
this .CreateInputParam( " @LastActivityDate " , SqlDbType.DateTime, objValue  ?  utcNow : time2)); 
 SqlParameter parameter 
=   new  SqlParameter( " @ReturnValue " , SqlDbType.Int); 
 parameter.Direction 
=  ParameterDirection.ReturnValue; 
 command.Parameters.Add(parameter); 
 command.ExecuteNonQuery(); 
 num 
=  (parameter.Value  !=   null ?  (( int ) parameter.Value) :  - 1
return  objValue; 
}
finally
{
if  (connection  !=   null )
{
connection.Close(); 
connection 
=   null
}
}
}
catch
{
throw
}
return  objValue; 


  这个代码首先通过GetPasswordWithFormat得到了Hash值(变量str)和Salt值(变量salt),然后对用户输入的密码(参数password)进行与注册时一样的散列(只是salt值使用了数据库中现存的值)得到散列值str2,之后通过对比str和str2,就知道密码正确与否了。

  4 小结

  本文只是简单地介绍了加Salt散列的工作方式(而非原理)、ASP.NET 在Membership中对其的实现。通过本文大家虽然无法对加Salt加密的有点和原理“知其所以然”,但相信大家应该大致了解了这种方式的使用方法,并能通过修改Membership的代码实现自己的密码散列存储了。

  由于时间有限,Anders Liu这篇文章写得很潦草,罗列了不少代码却没有系统性介绍,还望大家原谅。下一篇文章我将相对完整地介绍如何实现自己的用户登录(无需使用MembershipProvider,但同时也丧失了Login等控件为我们带来的便利)。

你可能感兴趣的:(加密,数据库,null,asp.net,byte,DST)