Search code examples
vb.netencryptioncryptographyaesrfc2898

Encryption implementation corrupting only last few bytes


Below is the code I've put together to implement encrypting arbitrary data using a user-supplied password.

Imports System.IO
Imports System.Security.Cryptography

Public Class PasswordCrytoSerializer
    Private Const SaltSizeBytes = 32
    Private Const IVSizeBytes = 16
    Private Const KeySizeBytes = 32
    Private Const HashSizeBytes = 32

    Public Property PBKDF2Iterations As Integer = 100000

    ''' <summary>
    ''' Encrypts and then saves binary data into a file
    ''' </summary>
    ''' <param name="fileContent">Array of bytes containing the file content</param>
    ''' <param name="password">Password to derive the encryption key from</param>
    ''' <param name="filePath">Path to save the encrypted data to</param>
    ''' <param name="cipherMode">Ciphermode to use for encryption. It Is <see cref="CipherMode.CBC"/> by default.</param>
    Public Sub EncryptToFile(fileContent As Byte(), password As String, filePath As String, Optional cipherMode As CipherMode = CipherMode.CBC)
        Using OutputFile As New FileStream(filePath, FileMode.Create)
            EncryptToStream(fileContent, password, OutputFile, cipherMode)
        End Using
    End Sub

    ''' <summary>
    ''' Encrypts and then returns the binary data in an array
    ''' </summary>
    ''' <param name="content">Array of bytes containing the data content to encrypt</param>
    ''' <param name="password">Password to derive the encryption key from</param>
    ''' <param name="cipherMode">Ciphermode to use for encryption. It Is <see cref="CipherMode.CBC"/> by default.</param>
    Public Function EncryptToMemory(content As Byte(), password As String, Optional cipherMode As CipherMode = CipherMode.CBC) As Byte()
        Using output As New MemoryStream
            EncryptToStream(content, password, output, cipherMode)
            Return output.ToArray()
        End Using
    End Function

    ''' <summary>
    ''' Encrypts and then saves binary data into a stream
    ''' </summary>
    ''' <param name="content">Array of bytes containing the data content to encrypt</param>
    ''' <param name="password">Password to derive the encryption key from</param>
    ''' <param name="outputStream">The stream to write the encrypted content into</param>
    ''' <param name="cipherMode">Ciphermode to use for encryption. It Is <see cref="CipherMode.CBC"/> by default.</param>
    Public Sub EncryptToStream(content As Byte(), password As String, outputStream As Stream, Optional cipherMode As CipherMode = CipherMode.CBC)
        Using AES = Security.Cryptography.Aes.Create()
            AES.Mode = cipherMode
            AES.Padding = 

            'Key salt is used to create a unique key from the user-supplied password
            Dim keySalt = GenerateRandomSalt()
            'Rfc2898 creates a key from user password and key salt (first 32 bytes are used)
            AES.Key = (New Rfc2898DeriveBytes(password, keySalt, PBKDF2Iterations, HashAlgorithmName.SHA256)).GetBytes(KeySizeBytes)

            'IV is used to create a unique encryted output even when given the same key
            AES.GenerateIV()

            'Hash salt is used to create a secure hash of the key
            Dim hashSalt = GenerateRandomSalt()
            'Rfc2898 creates a hash from the key using its own salt. These hashes can be compared to see if the key (and thus the base password) is correct without having to decrypt the content.
            Dim keyHash = (New Rfc2898DeriveBytes(AES.Key, hashSalt, PBKDF2Iterations, HashAlgorithmName.SHA256)).GetBytes(HashSizeBytes)


            'Save the key salt, IV, hash salt and hash (in that order) into the output before the encrypted data (they will be needed to unencrypt it)
            outputStream.Write(keySalt, 0, keySalt.Length)
            outputStream.Write(AES.IV, 0, AES.IV.Length)
            outputStream.Write(hashSalt, 0, hashSalt.Length)
            outputStream.Write(keyHash, 0, keyHash.Length)

            Using Encrypter As New CryptoStream(outputStream, AES.CreateEncryptor, CryptoStreamMode.Write)
                Encrypter.Write(content, 0, content.Length)
            End Using
        End Using
    End Sub

    ''' <summary>
    ''' Generates a random salt using <see cref="RandomNumberGenerator"/>
    ''' </summary>
    ''' <returns>An array of random bytes</returns>
    Private Function GenerateRandomSalt() As Byte()
        Dim salt(SaltSizeBytes - 1) As Byte

        Using random = RandomNumberGenerator.Create()
            random.GetBytes(salt)
        End Using

        Return salt
    End Function

    ''' <summary>
    ''' Decrypts a file that was encrypted using <see cref="EncryptToFile(Byte(), String, String, CipherMode)"/> and returns the decrypted content as binary data
    ''' </summary>
    ''' <param name="filePath">Path to the file to decrypt</param>
    ''' <param name="password">Password to derive the encryption key from</param>
    ''' <returns>Null if the given password is found to be incorrect, otherwise an array of bytes containing the decrypted data from the file</returns>
    Public Function DecryptFromFile(filePath As String, password As String) As Byte()
        Using Reader As New FileStream(filePath, FileMode.Open)
            Return DecryptFromStream(Reader, password)
        End Using
    End Function

    ''' <summary>
    ''' Decrypts a file that was encrypted using <see cref="EncryptToFile(Byte(), String, String, CipherMode)"/> and returns the decrypted content as binary data
    ''' </summary>
    ''' <param name="bytes">An array containing the binary data to decrypt</param>
    ''' <param name="password">Password to derive the encryption key from</param>
    ''' <returns>Null if the given password is found to be incorrect, otherwise an array of bytes containing the decrypted data from the file</returns>
    Public Function DecryptFromMemory(ByRef bytes As Byte(), password As String) As Byte()
        Using s As New MemoryStream(bytes)
            Return DecryptFromStream(s, password)
        End Using
    End Function

    ''' <summary>
    ''' Decrypts a file that was encrypted using <see cref="EncryptToStream(Byte(), String, Stream, CipherMode)"/> and returns the decrypted content as binary data
    ''' </summary>
    ''' <param name="encryptedSource">A stream containing the binary data to decrypt</param>
    ''' <param name="password">Password to derive the encryption key from</param>
    ''' <returns>Null if the given password is found to be incorrect, otherwise an array of bytes containing the decrypted data from the source</returns>
    Public Function DecryptFromStream(encryptedSource As Stream, password As String) As Byte()

        'Read key salt, IV, hash salt and key hash from begining of file
        Dim keySalt(SaltSizeBytes - 1) As Byte
        encryptedSource.Read(keySalt, 0, SaltSizeBytes)

        Dim iv(IVSizeBytes - 1) As Byte
        encryptedSource.Read(iv, 0, IVSizeBytes)

        Dim hashSalt(SaltSizeBytes - 1) As Byte
        encryptedSource.Read(hashSalt, 0, SaltSizeBytes)

        Dim keyHash(KeySizeBytes - 1) As Byte
        encryptedSource.Read(keyHash, 0, HashSizeBytes)

        Dim ContentLength = encryptedSource.Length - (SaltSizeBytes + IVSizeBytes + SaltSizeBytes + HashSizeBytes)
        Dim R(ContentLength) As Byte

        Using AES = Security.Cryptography.Aes.Create()
            'Calculate key using user-supplied password and key salt from file
            AES.Key = (New Rfc2898DeriveBytes(password, keySalt, PBKDF2Iterations, HashAlgorithmName.SHA256)).GetBytes(KeySizeBytes)
            AES.IV = iv

            'Generate a hash of the newly-generated key using the hash salt from the file
            Dim keyHashToCompare = (New Rfc2898DeriveBytes(AES.Key, hashSalt, PBKDF2Iterations, HashAlgorithmName.SHA256)).GetBytes(HashSizeBytes)
            'If the above hash does not match the hash from the file, the password given for decryption is not correct
            If Not keyHash.SequenceEqual(keyHashToCompare) Then Return Nothing

            'If we made it this far, the password is correct. Decrypt the data and return it.
            Using Decrypter As New CryptoStream(encryptedSource, AES.CreateDecryptor, CryptoStreamMode.Read)
                Decrypter.Read(R, 0, ContentLength)
                Return R
            End Using
        End Using
    End Function

End Class

I'm attempting to encrypt a file which is 2,677 bytes long. The resulting (encrypted) file is 2,800 bytes. When decrypted, the final output file is 2,689, which is 12 bytes longer than the initial file.

When conducting a binary comparison between the original and the decrypted files, they are completely identical up until the end. The last five bytes of the original file seems to have been replaced with 17 bytes of 00.

I suspect some kind of padding issue but I haven't been able to figure this out yet. Any idea why this is happening?


Solution

  • When determining ContentLength in DecryptFromStream(), the PKCS#7 padding is not taken into account, so that the buffer R is larger than the actual plaintext.
    To determine the size of the actual data (i.e. without padding bytes), the return value of Read() must be used (which is not considered in the current code). This value can then be used e.g. to copy the data from the too large buffer R into a buffer of the required size.

    Due to this breaking change, it must also be taken into account that Read() does not guarantee that the requested bytes will be read (it is only guaranteed that at least 1 byte is read, 0 marks the end of the stream, s. here). This is probably the cause of the missing 5 bytes.
    To read all the data from the stream, R must be filled in a loop until the end of the stream is reached. Alternatively (and more conveniently), the data can be read into a MemoryStream with a CopyTo() (in this case, R and ContentLength are not required), as in the following code snippet:

    ...
    Using MemStream As New MemoryStream
        Using Decrypter As New CryptoStream(encryptedSource, AES.CreateDecryptor, CryptoStreamMode.Read)
            Decrypter.CopyTo(MemStream)
        End Using
        Return MemStream.ToArray()
    End Using
    ...