在ASP.NET Membership Provider中实现PBKDF2遇非负参数错误
解决PBKDF2加密转换VB.NET后的srcOffset负数错误
问题背景
为使用ASP.NET Membership Provider的老旧应用实现PBKDF2加密,参考相关方案将C#代码转换为VB.NET后,执行Buffer.BlockCopy(m_buffer, m_startIndex, password, 0, size)时抛出错误:
Non-negative number required. Parameter name: srcOffset
排查发现m_startIndex的值变为-1,原VB.NET代码如下:
Imports System.IO Imports System.Security.Cryptography Namespace custom.hashing.keyderivation Public Class PBKDF2Hash Inherits KeyedHashAlgorithm Private Const kHashBytes As Integer = 64 Private _ms As System.IO.MemoryStream Public Property WorkFactor As Integer Public Sub New() MyBase.New() Me.WorkFactor = 128000 Me.Key = New Byte(31) {} Using rngCsp = New RNGCryptoServiceProvider() rngCsp.GetBytes(Me.Key) End Using End Sub Public Overrides ReadOnly Property HashSize As Integer Get Return kHashBytes * 8 End Get End Property Protected Overrides Sub HashCore(ByVal array As Byte(), ByVal ibStart As Integer, ByVal cbSize As Integer) If IsNothing(_ms) Then _ms = New MemoryStream() End If _ms.Write(array, ibStart, cbSize) End Sub Protected Overrides Function HashFinal() As Byte() If Me.Key Is Nothing OrElse Me.Key.Length = 0 Then Throw New CryptographicException("Missing KeyedAlgorithm key") End If _ms.Flush() Dim arr = _ms.ToArray() _ms = Nothing Using hmac As HMACSHA512 = New HMACSHA512() Return New MyRfc2898DeriveBytes(arr, Me.Key, Me.WorkFactor, hmac).GetBytes(kHashBytes) End Using End Function Public Overrides Sub Initialize() _ms = Nothing End Sub End Class End Namespace Imports System.Diagnostics.Contracts Imports System.Security Imports System.Security.Cryptography Public Class MyRfc2898DeriveBytes Inherits DeriveBytes Private m_buffer As Byte() Private m_salt As Byte() Private m_hmac As HMAC Private m_iterations As UInteger Private m_block As UInteger Private m_startIndex As Integer = 0 Private m_endIndex As Integer = 0 Private m_blockSize As Integer = 0 <SecuritySafeCritical> Public Sub New(ByVal password As Byte(), ByVal salt As Byte(), ByVal iterations As Integer, ByVal hmac As HMAC) salt = salt IterationCount = iterations hmac.Key = password m_hmac = hmac m_blockSize = hmac.HashSize >> 3 Initialize() End Sub Public Property IterationCount As Integer Get Return CInt(m_iterations) End Get Set(ByVal value As Integer) If value <= 0 Then Throw New ArgumentOutOfRangeException("value", "Error: Iteration count is zero or less") m_iterations = CUInt(value) Initialize() End Set End Property Public Property Salt As Byte() Get Return CType(m_salt.Clone(), Byte()) End Get Set(ByVal value As Byte()) If value Is Nothing Then Throw New ArgumentNullException("value") If value.Length < 8 Then Throw New ArgumentException("Error: Salt size is less than 8") m_salt = CType(value.Clone(), Byte()) Initialize() End Set End Property Public Overrides Function GetBytes(ByVal cb As Integer) As Byte() If cb <= 0 Then Throw New ArgumentOutOfRangeException("cb", "Error: Hash size is zero or less") End If Contract.Assert(m_blockSize > 0) Dim password As Byte() = New Byte(cb - 1) {} Dim offset As Integer = 0 Dim size As Integer = m_endIndex - m_startIndex If size > 0 Then If cb >= size Then Buffer.BlockCopy(m_buffer, m_startIndex, password, 0, size) m_startIndex = m_endIndex = 0 offset += size Else Buffer.BlockCopy(m_buffer, m_startIndex, password, 0, cb) m_startIndex += cb Return password End If End If Contract.Assert(m_startIndex = 0 AndAlso m_endIndex = 0, "Invalid start or end index in the internal buffer.") While offset < cb Dim T_block As Byte() = Func() Dim remainder As Integer = cb - offset If remainder > m_blockSize Then Buffer.BlockCopy(T_block, 0, password, offset, m_blockSize) offset += m_blockSize Else Buffer.BlockCopy(T_block, 0, password, offset, remainder) offset += remainder Buffer.BlockCopy(T_block, remainder, m_buffer, m_startIndex, m_blockSize - remainder) m_endIndex += (m_blockSize - remainder) Return password End If End While Return password End Function Public Overrides Sub Reset() Initialize() End Sub Protected Overrides Sub Dispose(ByVal disposing As Boolean) MyBase.Dispose(disposing) If disposing Then If m_hmac IsNot Nothing Then m_hmac.Dispose() End If If m_buffer IsNot Nothing Then Array.Clear(m_buffer, 0, m_buffer.Length) End If If m_salt IsNot Nothing Then Array.Clear(m_salt, 0, m_salt.Length) End If End If End Sub Private Sub Initialize() If m_buffer IsNot Nothing Then Array.Clear(m_buffer, 0, m_buffer.Length) m_buffer = New Byte(m_blockSize - 1) {} m_block = 1 m_startIndex = m_endIndex = 0 End Sub Friend Shared Function GetBytesFromInt(ByVal i As UInteger) As Byte() End Function Private Function Func() As Byte() Dim INT_block As Byte() = GetBytesFromInt(m_block) m_hmac.TransformBlock(m_salt, 0, m_salt.Length, Nothing, 0) m_hmac.TransformBlock(INT_block, 0, INT_block.Length, Nothing, 0) m_hmac.TransformFinalBlock(New Byte(-1) {}, 0, 0) Dim temp As Byte() = m_hmac.Hash m_hmac.Initialize() Dim ret As Byte() = temp For i As Integer = 2 To m_iterations m_hmac.TransformBlock(temp, 0, temp.Length, Nothing, 0) m_hmac.TransformFinalBlock(New Byte(-1) {}, 0, 0) temp = m_hmac.Hash For j As Integer = 0 To m_blockSize - 1 ret(j) = ret(j) Xor temp(j) Next m_hmac.Initialize() Next If m_block = UInteger.MaxValue Then Throw New InvalidOperationException("Derived key too long.") End If m_block += 1 Return ret End Function End Class
错误原因分析
- 构造函数未正确赋值salt:
MyRfc2898DeriveBytes构造函数中salt = salt是无效的自我赋值,导致m_salt始终为Nothing,后续Func方法中访问m_salt.Length会引发未处理的空引用异常,间接导致索引混乱。 - GetBytesFromInt方法为空实现:该方法负责将UInt32类型的块编号转换为字节数组,空实现会返回
Nothing,导致Func方法中调用m_hmac.TransformBlock时传入无效数组,破坏内部状态。 - m_blockSize可能为0:如果
hmac.HashSize为0(虽然HMACSHA512不会出现,但代码未验证),m_blockSize = hmac.HashSize >> 3会得到0,此时Initialize方法中m_buffer = New Byte(m_blockSize - 1) {}会创建长度为-1的数组(即空数组),后续Buffer.BlockCopy操作会引发索引异常。
修复方案
1. 修正构造函数的salt赋值
将构造函数中的salt = salt替换为:
m_salt = CType(salt.Clone(), Byte())
2. 实现GetBytesFromInt方法
补充该方法的逻辑,将UInt32转换为大端字节序的4字节数组:
Friend Shared Function GetBytesFromInt(ByVal i As UInteger) As Byte() Dim bytes As Byte() = BitConverter.GetBytes(i) ' PBKDF2要求使用大端字节序 If BitConverter.IsLittleEndian Then Array.Reverse(bytes) End If Return bytes End Function
3. 增加参数验证
在构造函数中添加对salt和hmac的有效性验证:
<SecuritySafeCritical> Public Sub New(ByVal password As Byte(), ByVal salt As Byte(), ByVal iterations As Integer, ByVal hmac As HMAC) If salt Is Nothing Then Throw New ArgumentNullException(NameOf(salt)) If salt.Length < 8 Then Throw New ArgumentException("Salt must be at least 8 bytes long.", NameOf(salt)) If hmac Is Nothing Then Throw New ArgumentNullException(NameOf(hmac)) m_salt = CType(salt.Clone(), Byte()) IterationCount = iterations m_hmac = hmac m_hmac.Key = password.Clone() ' 避免外部修改影响内部状态 m_blockSize = hmac.HashSize >> 3 If m_blockSize <= 0 Then Throw New ArgumentException("Invalid HMAC hash size.", NameOf(hmac)) End If Initialize() End Sub
修正后的完整代码
Imports System.IO Imports System.Security.Cryptography Namespace custom.hashing.keyderivation Public Class PBKDF2Hash Inherits KeyedHashAlgorithm Private Const kHashBytes As Integer = 64 Private _ms As System.IO.MemoryStream Public Property WorkFactor As Integer Public Sub New() MyBase.New() Me.WorkFactor = 128000 Me.Key = New Byte(31) {} Using rngCsp = New RNGCryptoServiceProvider() rngCsp.GetBytes(Me.Key) End Using End Sub Public Overrides ReadOnly Property HashSize As Integer Get Return kHashBytes * 8 End Get End Property Protected Overrides Sub HashCore(ByVal array As Byte(), ByVal ibStart As Integer, ByVal cbSize As Integer) If IsNothing(_ms) Then _ms = New MemoryStream() End If _ms.Write(array, ibStart, cbSize) End Sub Protected Overrides Function HashFinal() As Byte() If Me.Key Is Nothing OrElse Me.Key.Length = 0 Then Throw New CryptographicException("Missing KeyedAlgorithm key") End If _ms.Flush() Dim arr = _ms.ToArray() _ms = Nothing Using hmac As HMACSHA512 = New HMACSHA512() Return New MyRfc2898DeriveBytes(arr, Me.Key, Me.WorkFactor, hmac).GetBytes(kHashBytes) End Using End Function Public Overrides Sub Initialize() _ms = Nothing End Sub End Class End Namespace Imports System.Diagnostics.Contracts Imports System.Security Imports System.Security.Cryptography Public Class MyRfc2898DeriveBytes Inherits DeriveBytes Private m_buffer As Byte() Private m_salt As Byte() Private m_hmac As HMAC Private m_iterations As UInteger Private m_block As UInteger Private m_startIndex As Integer = 0 Private m_endIndex As Integer = 0 Private m_blockSize As Integer = 0 <SecuritySafeCritical> Public Sub New(ByVal password As Byte(), ByVal salt As Byte(), ByVal iterations As Integer, ByVal hmac As HMAC) If salt Is Nothing Then Throw New ArgumentNullException(NameOf(salt)) If salt.Length < 8 Then Throw New ArgumentException("Salt must be at least 8 bytes long.", NameOf(salt)) If hmac Is Nothing Then Throw New ArgumentNullException(NameOf(hmac)) m_salt = CType(salt.Clone(), Byte()) IterationCount = iterations m_hmac = hmac m_hmac.Key = password.Clone() m_blockSize = hmac.HashSize >> 3 If m_blockSize <= 0 Then Throw New ArgumentException("Invalid HMAC hash size.", NameOf(hmac)) End If Initialize() End Sub Public Property IterationCount As Integer Get Return CInt(m_iterations) End Get Set(ByVal value As Integer) If value <= 0 Then Throw New ArgumentOutOfRangeException(NameOf(value), "Error: Iteration count is zero or less") m_iterations = CUInt(value) Initialize() End Set End Property Public Property Salt As Byte() Get Return CType(m_salt.Clone(), Byte()) End Get Set(ByVal value As Byte()) If value Is Nothing Then Throw New ArgumentNullException(NameOf(value)) If value.Length < 8 Then Throw New ArgumentException("Error: Salt size is less than 8", NameOf(value)) m_salt = CType(value.Clone(), Byte()) Initialize() End Set End Property Public Overrides Function GetBytes(ByVal cb As Integer) As Byte() If cb <= 0 Then Throw New ArgumentOutOfRangeException(NameOf(cb), "Error: Hash size is zero or less") End If Contract.Assert(m_blockSize > 0) Dim password As Byte() = New Byte(cb - 1) {} Dim offset As Integer = 0 Dim size As Integer = m_endIndex - m_startIndex If size > 0 Then If cb >= size Then Buffer.BlockCopy(m_buffer, m_startIndex, password, 0, size) m_startIndex = m_endIndex = 0 offset += size Else Buffer.BlockCopy(m_buffer, m_startIndex, password, 0, cb) m_startIndex += cb Return password End If End If Contract.Assert(m_startIndex = 0 AndAlso m_endIndex = 0, "Invalid start or end index in the internal buffer.") While offset < cb Dim T_block As Byte() = Func() Dim remainder As Integer = cb - offset If remainder > m_blockSize Then Buffer.BlockCopy(T_block, 0, password, offset, m_blockSize) offset += m_blockSize Else Buffer.BlockCopy(T_block, 0, password, offset, remainder) offset += remainder Buffer.BlockCopy(T_block, remainder, m_buffer, m_startIndex, m_blockSize - remainder) m_endIndex += (m_blockSize - remainder) Return password End If End While Return password End Function Public Overrides Sub Reset() Initialize() End Sub Protected Overrides Sub Dispose(ByVal disposing As Boolean) MyBase.Dispose(disposing) If disposing Then If m_hmac IsNot Nothing Then m_hmac.Dispose() End If If m_buffer IsNot Nothing Then Array.Clear(m_buffer, 0, m_buffer.Length) End If If m_salt IsNot Nothing Then Array.Clear(m_salt, 0, m_salt.Length) End If End If End Sub Private Sub Initialize() If m_buffer IsNot Nothing Then Array.Clear(m_buffer, 0, m_buffer.Length) m_buffer = New Byte(m_blockSize - 1) {} m_block = 1 m_startIndex = m_endIndex = 0 End Sub Friend Shared Function GetBytesFromInt(ByVal i As UInteger) As Byte() Dim bytes As Byte() = BitConverter.GetBytes(i) ' PBKDF2规范要求使用大端字节序 If BitConverter.IsLittleEndian Then Array.Reverse(bytes) End If Return bytes End Function Private Function Func() As Byte() Dim INT_block As Byte() = GetBytesFromInt(m_block) m_hmac.TransformBlock(m_salt, 0, m_salt.Length, Nothing, 0) m_hmac.TransformBlock(INT_block, 0, INT_block.Length, Nothing, 0) m_hmac.TransformFinalBlock(New Byte(-1) {}, 0, 0) Dim temp As Byte() = m_hmac.Hash m_hmac.Initialize() Dim ret As Byte() = temp For i As Integer = 2 To m_iterations m_hmac.TransformBlock(temp, 0, temp.Length, Nothing, 0) m_hmac.TransformFinalBlock(New Byte(-1) {}, 0, 0) temp = m_hmac.Hash For j As Integer = 0 To m_block
相关产品推荐
相关产品推荐

