下面是我编写的代码,用于使用用户提供的密码来实现加密任意数据。
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
我正在尝试加密一个 2,677 字节长的文件。生成的(加密的)文件大小为 2,800 字节。解密后,最终输出文件为 2,689,比初始文件长 12 个字节。
当对原始文件和解密文件进行二进制比较时,它们直到最后都是完全相同的。原始文件的最后五个字节似乎被替换为 17 个字节的
00
。
我怀疑存在某种填充问题,但我还无法弄清楚这一点。知道为什么会发生这种情况吗?
在确定
ContentLength
中的DecryptFromStream()
时,没有考虑PKCS#7的填充,因此缓冲区R
比实际的明文要大。Read()
的返回值(当前代码中未考虑)。然后可以使用该值,例如将数据从太大的缓冲区R
复制到所需大小的缓冲区中。
由于这个重大变化,还必须考虑到
Read()
不能保证所请求的字节将被读取(仅保证至少读取1个字节,0标记流的末尾,s.here)。这可能是丢失 5 个字节的原因。R
,直到到达流的末尾。或者(更方便),可以使用 MemoryStream
将数据读入 CopyTo()
(在这种情况下,不需要 R
和 ContentLength
),如以下代码片段所示:
...
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
...