Results 1 to 10 of 10

Thread: Password Encryption - PBKDF2

  1. #1

    Thread Starter
    Super Moderator dday9's Avatar
    Join Date
    Mar 2011
    Location
    South Louisiana
    Posts
    11,698

    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:
    1. Generate a salt using the GenerateSalt method
    2. Generate a hash using the GenerateHash method with the salt that was generated in step 1
    3. 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.
    "Code is like humor. When you have to explain it, it is bad." - Cory House
    VbLessons | Code Tags | Sword of Fury - Jameram

  2. #2
    PowerPoster wqweto's Avatar
    Join Date
    May 2011
    Location
    Sofia, Bulgaria
    Posts
    5,093

    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.

  3. #3

    Thread Starter
    Super Moderator dday9's Avatar
    Join Date
    Mar 2011
    Location
    South Louisiana
    Posts
    11,698

    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.
    "Code is like humor. When you have to explain it, it is bad." - Cory House
    VbLessons | Code Tags | Sword of Fury - Jameram

  4. #4
    PowerPoster wqweto's Avatar
    Join Date
    May 2011
    Location
    Sofia, Bulgaria
    Posts
    5,093

    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>

  5. #5

    Thread Starter
    Super Moderator dday9's Avatar
    Join Date
    Mar 2011
    Location
    South Louisiana
    Posts
    11,698

    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?
    "Code is like humor. When you have to explain it, it is bad." - Cory House
    VbLessons | Code Tags | Sword of Fury - Jameram

  6. #6
    Fanatic Member
    Join Date
    Jun 2019
    Posts
    556

    Re: Password Encryption

    Quote Originally Posted by dday9 View Post
    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.

  7. #7

    Thread Starter
    Super Moderator dday9's Avatar
    Join Date
    Mar 2011
    Location
    South Louisiana
    Posts
    11,698

    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.
    "Code is like humor. When you have to explain it, it is bad." - Cory House
    VbLessons | Code Tags | Sword of Fury - Jameram

  8. #8
    PowerPoster wqweto's Avatar
    Join Date
    May 2011
    Location
    Sofia, Bulgaria
    Posts
    5,093

    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>

  9. #9

    Thread Starter
    Super Moderator dday9's Avatar
    Join Date
    Mar 2011
    Location
    South Louisiana
    Posts
    11,698

    Re: Password Encryption - PBKDF2

    I have updated the original code with the following changes:
    1. I replaced creating a new instance of the RNGCryptoServiceProvider with the RandomNumberGenerator.Create method
    2. I refactored the IsPasswordValid method so that the first argument is a byte array instead of a String
    3. I have rescoped my private shared variables to be ReadOnly
    "Code is like humor. When you have to explain it, it is bad." - Cory House
    VbLessons | Code Tags | Sword of Fury - Jameram

  10. #10

    Thread Starter
    Super Moderator dday9's Avatar
    Join Date
    Mar 2011
    Location
    South Louisiana
    Posts
    11,698

    Re: Password Encryption - PBKDF2

    I have updated the code with the following changes:
    1. Created the Authentication.Authenticate method
    2. Created the Authentication.EncryptPlainText method
    "Code is like humor. When you have to explain it, it is bad." - Cory House
    VbLessons | Code Tags | Sword of Fury - Jameram

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Click Here to Expand Forum to Full Width