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?
When determining
ContentLength
inDecryptFromStream()
, the PKCS#7 padding is not taken into account, so that the bufferR
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 bufferR
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 aMemoryStream
with aCopyTo()
(in this case,R
andContentLength
are not required), as in the following code snippet: