-
Aug 20th, 2022, 12:39 AM
#1
Password Encryption - PBKDF2
Here is an example of leveraging the System.Security.Cryptography class to encrypt password then check an incoming password against an encrypted one:
Code:
Option Strict On
Option Explicit On
Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.Security.Cryptography
Public Class Authentication
''' <summary>
''' Gets the length of the salt and hash
''' </summary>
Private Shared ReadOnly _byteLength As Integer = 32
''' <summary>
''' Gets the number of iterations to derive the key using Rfc2898
''' </summary>
Private Shared ReadOnly _rcfIterations As Integer = 100000
''' <summary>
''' Compares a plaintext <see cref="String"/> against a previously encrypted plaintext value
''' </summary>
''' <param name="plaintext">The <see cref="String"/> to encrypt</param>
''' <param name="encryptedText">A collection of <see cref="Byte"/> representing the previously encrypted plaintext value</param>
''' <returns><see cref="Boolean"/></returns>
''' <remarks>This method takes steps to prevent timing attacks by not immediately returning a False value if the password does not authenticate. For more information visit: https://en.wikipedia.org/wiki/Timing_attack</remarks>
Public Shared Function Authenticate(plaintext As String, encryptedText As Byte()) As Boolean
Dim authenticated = True
Dim salt(_byteLength - 1) As Byte
If (encryptedText.Length > _byteLength) Then
For index = 0 To _byteLength - 1
salt(index) = encryptedText(index)
Next
Else
For index = 0 To _byteLength - 1
salt(index) = [Byte].MinValue
Next
authenticated = False
End If
Dim hash() As Byte = {}
Try
hash = Authentication.GenerateHash(plaintext, salt)
Catch
For index = 0 To _byteLength - 1
hash(index) = [Byte].MinValue
Next
authenticated = False
End Try
If (encryptedText.Length - _byteLength = hash.Length) Then
For index = 0 To hash.Length - 1
If (authenticated) Then
authenticated = hash(index) = encryptedText(_byteLength + index)
Else
authenticated = [Byte].MinValue = [Byte].MaxValue
End If
Next
Else
For index = 0 To hash.Length - 1
authenticated = [Byte].MinValue = [Byte].MaxValue
Next
End If
Return authenticated
End Function
''' <summary>
''' Prepends one <see cref="Byte"/> array representing the salt to another <see cref="Byte"/> array representing the hash
''' </summary>
''' <param name="salt">A collection of <see cref="Byte"/> representing the salt</param>
''' <param name="hash">A collection of <see cref="Byte"/> representing the hash</param>
''' <returns><see cref="IEnumerable(Of Byte)"/></returns>
''' <exception cref="ArgumentOutOfRangeException"><see cref="_byteLength"/> cannot be less than 1</exception>
''' <exception cref="ArgumentOutOfRangeException"><param name="salt"/> is invalid because the length does not exactly match <see cref="_byteLength"/></exception>
''' <exception cref="ArgumentOutOfRangeException"><param name="hash"/> is invalid because the length does not exactly match <see cref="_byteLength"/></exception>
Public Shared Function CombineSaltAndHash(salt As Byte(), hash As Byte()) As IEnumerable(Of Byte)
If (_byteLength < 1) Then
Throw New ArgumentOutOfRangeException($"{NameOf(_byteLength)} cannot be less than 1")
End If
If (salt.Length <> _byteLength) Then
Throw New ArgumentOutOfRangeException(NameOf(salt), "The incoming salt is invalid")
End If
If (hash.Length <> _byteLength) Then
Throw New ArgumentOutOfRangeException(NameOf(hash), "The incoming hash is invalid")
End If
Return salt.Concat(hash).ToArray()
End Function
''' <summary>
''' Encrypts a plaintext <see cref="String"/> to a collection of <see cref="Byte"/>
''' </summary>
''' <param name="plaintext">The <see cref="String"/> to encrypt</param>
''' <returns><see cref="IEnumerable(Of Byte)"/></returns>
Public Shared Function EncryptPlainText(plaintext As String) As IEnumerable(Of Byte)
Dim salt = Authentication.GenerateSalt()
Dim hash = Authentication.GenerateHash(plaintext, salt)
Dim combinedHash = Authentication.CombineSaltAndHash(salt, hash)
Return combinedHash
End Function
''' <summary>
''' Uses <see cref="Rfc2898DeriveBytes"/> to generate a hash
''' </summary>
''' <param name="plaintext">The <see cref="String"/> representing the password used to derive the key</param>
''' <param name="salt">The collection of <see cref="Byte"/> representing the salt used to derive the key</param>
''' <returns>Collection of <see cref="Byte"/></returns>
''' <exception cref="ArgumentOutOfRangeException"><see cref="_byteLength"/> cannot be less than 1</exception>
''' <exception cref="ArgumentOutOfRangeException"><see cref="_rcfIterations"/> cannot be less than 1</exception>
''' <exception cref="ArgumentNullException"><paramref name="plaintext"/> cannot be null</exception>
''' <exception cref="ArgumentOutOfRangeException"><paramref name="salt"/> is invalid because the length does not exactly match <see cref="_byteLength"/></exception>
Public Shared Function GenerateHash(plaintext As String, salt As Byte()) As Byte()
If (_byteLength < 1) Then
Throw New ArgumentOutOfRangeException($"{NameOf(_byteLength)} cannot be less than 1")
End If
If (_rcfIterations < 1) Then
Throw New ArgumentOutOfRangeException($"{NameOf(_rcfIterations)} cannot be less than 1")
End If
If (String.IsNullOrWhiteSpace(plaintext)) Then
Throw New ArgumentNullException(NameOf(plaintext))
End If
If (salt.Length <> _byteLength) Then
Throw New ArgumentOutOfRangeException(NameOf(salt), "The incoming salt is invalid")
End If
Dim hash() As Byte = {}
Using rcf = New Rfc2898DeriveBytes(plaintext, salt, _rcfIterations)
hash = rcf.GetBytes(_byteLength)
End Using
Return hash
End Function
''' <summary>
''' Uses <see cref="RandomNumberGenerator"/> to generate a salt
''' </summary>
''' <returns>Collection of <see cref="Byte"/></returns>
''' <exception cref="ArgumentOutOfRangeException"><see cref="_byteLength"/> cannot be less than 1</exception>
Public Shared Function GenerateSalt() As Byte()
If (_byteLength < 1) Then
Throw New ArgumentOutOfRangeException($"{NameOf(_byteLength)} cannot be less than 1")
End If
Dim salt(_byteLength - 1) As Byte
Using provider = RandomNumberGenerator.Create()
provider.GetBytes(salt)
End Using
Return salt
End Function
End Class
The basic idea is that you would follow these steps to encrypt a password:
- Generate a salt using the GenerateSalt method
- Generate a hash using the GenerateHash method with the salt that was generated in step 1
- Generate the combined salt and hash from steps 1 and two using the CombineSaltAndHash method
Alternatively the Authentication.EncryptPlainText does this for you.
Here is an example of using it:
Code:
Public Module Module1
Public Sub Main()
Dim existingPassword = Authentication.EncryptPlainText("Password123456789")
Dim incomingPassword = Console.ReadLine()
Dim authenticated = Authentication.Authenticate(incomingPassword, existingPassword)
End Sub
End Module
Fiddle: https://dotnetfiddle.net/sVvYeS
GitHub: https://github.com/dday9/.NET-Authen...hentication.vb
Last edited by dday9; Nov 2nd, 2022 at 01:51 PM.
-
Aug 20th, 2022, 04:00 AM
#2
Re: Password Encryption
For base64 encoding (not encryption) you can have several different valid outputs for a single input which is not obvious. For instance A can be encoded to B or C so that decode(B) and decode(C) return the same A.
In this regard your final equality comparison should better be on decoded byta-arrays instead of encoded strings because it’s theorethically possible to have different base64 strings which encode the same byte-arrays.
-
Aug 20th, 2022, 10:31 AM
#3
Re: Password Encryption
Thank you for the feedback. I've updated the code to compare the individual bytes in the comparison method.
Also, small stylistic change in that I converted the Module to a Class.
-
Aug 20th, 2022, 10:59 AM
#4
Re: Password Encryption
Btw, the popular name for key derivation construct in RFC 2898 is PBKDF2 which might be a good keyword for the submission to include in the title.
Also anything less that 100k iterations for PBKDF2 is consider medium to low security on modern h/w so a conservative default should be north of 10k (if 100k straight seems outrageous).
cheers,
</wqw>
-
Aug 20th, 2022, 11:23 AM
#5
Re: Password Encryption
So I don't fully understand the correlation between the number of iterations and performance, could you elaborate a bit on that?
-
Aug 20th, 2022, 11:47 AM
#6
Re: Password Encryption
 Originally Posted by dday9
So I don't fully understand the correlation between the number of iterations and performance, could you elaborate a bit on that?
Number of iterations is related how long it will take to calculate the final value. More iterations = more time = longer and harder to bruteforce.
-
Aug 20th, 2022, 11:50 AM
#7
Re: Password Encryption
No, I completely understand that. I just don't understand, on a practical level, at what point is the number of iterations just too high.
-
Aug 20th, 2022, 11:59 AM
#8
Re: Password Encryption - PBKDF2
I guess you don't want end-users to wait 10+ seconds on every logon so you refrain from using million of iterations for this reason.
Another problem is that if you have 1000s of logons per second (not facebook but stackoverflow scale) you might need multiple CPUs/hosts just for the PBKDF2 computation not to bottleneck the site.
cheers,
</wqw>
-
Oct 25th, 2022, 10:18 AM
#9
Re: Password Encryption - PBKDF2
I have updated the original code with the following changes:
- I replaced creating a new instance of the RNGCryptoServiceProvider with the RandomNumberGenerator.Create method
- I refactored the IsPasswordValid method so that the first argument is a byte array instead of a String
- I have rescoped my private shared variables to be ReadOnly
-
Nov 2nd, 2022, 01:50 PM
#10
Re: Password Encryption - PBKDF2
I have updated the code with the following changes:
- Created the Authentication.Authenticate method
- Created the Authentication.EncryptPlainText method
Posting Permissions
- You may not post new threads
- You may not post replies
- You may not post attachments
- You may not edit your posts
-
Forum Rules
|
Click Here to Expand Forum to Full Width
|