在 .Net (C#) 中解密 mcrypt 文件
Decrypting an mcrypt file in .Net (C#)
我成为 C# 爱好者已有一段时间了,我会考虑拥有中级开发技能,但几乎没有加密知识。作为副项目的一部分,我需要解密使用 MCrypt 加密的文件。似乎没有任何特殊参数被传递到命令中。例如,这很常见(密钥和文件名已更改)并且密钥的长度各不相同,从 14 到 18 个字符不等。
mcrypt -a rijndael-256 fileToEncrypt.tar.gz -k 0123456789abcdef1
到目前为止,我已经采用了两种方法来完成这项任务。第一种是使用 mcrypt.exe 并使用 Process
启动进程。但是,我觉得这使得代码(和程序流程)非常笨拙。第二种是尝试直接从我的内部程序解密文件,并且外部程序依赖性为零;我想走这条路。
我对 MCrypt 格式有点困惑。我已经查看了源代码中的 FORMAT 文档(here 以在线查看)并且我相信我已经正确处理了 header 的开始部分。但是,我似乎无法解密文件中的加密数据。
1) IV 有多大,我如何将它传递到我的解密器中?
2) 文件末尾的校验和有多大,我需要它吗?
3)以上是静态的长度吗?
4) 什么是keymode (mcrypt-sha1) 以及如何使用?
5) 我注意到在正确解密(使用 mcrypt.exe)时,加密文件和解密文件之间存在 140 字节的差异。这 140 个字节是由什么组成的?
下面是加密文件的代码和开头;毫无疑问,我的代码从注释 "Get the data" 开始是错误的
任何正确方向的指示将不胜感激。
/// <summary>
/// Decrypt an mcrypt file using rijndael-256
/// </summary>
/// <param name="inputFile">File to decrypt</param>
/// <param name="encryptionKey">Password</param>
/// <param name="purge"></param>
public static bool Decrypt (string inputFile, string encryptionKey)
{
var rv = false;
if (File.Exists(inputFile) == true)
{
using (FileStream stream = new FileStream(inputFile, FileMode.Open))
{
var buffer = new byte[1024];
// MCrypt header
stream.Read(buffer, 0, 3);
if (buffer[0] == 0x00 && buffer[1] == 0x6D && buffer[2] == 0x03)
{
// Flag
// Bit 7 - Salt Used
// Bit 8 - IV not used
var flag = (byte)stream.ReadByte();
byte[] saltVal = null;
var saltUsed = Utils.GetBit(flag, 6);
byte[] ivVal = new byte[16];
var ivUsed = (Utils.GetBit(flag, 7) == false);
var algorithmName = Utils.GetNullTerminatedString(stream);
stream.Read(buffer, 0, 2);
var keyLen = (buffer[1] << 8) + buffer[0];
var algorithModeName = Utils.GetNullTerminatedString(stream);
var keygenName = Utils.GetNullTerminatedString(stream);
if (saltUsed)
{
var saltFlag = (byte)stream.ReadByte();
if (Utils.GetBit(saltFlag, 0))
{
// After clearing the first bit the salt flag is now the length
Utils.ClearBit (ref saltFlag, 0);
saltVal = new byte[saltFlag];
stream.Read(saltVal, 0, saltFlag);
}
}
var algorithmModeName = Utils.GetNullTerminatedString(stream);
if (ivUsed)
{
stream.Read(ivVal, 0, ivVal.Length);
}
// Get the data - how much to get???
buffer = new byte[stream.Length - stream.Position + 1];
var bytesRead = stream.Read(buffer, 0, buffer.Length);
using (MemoryStream ms = new MemoryStream())
{
using (RijndaelManaged rijndael = new RijndaelManaged())
{
rijndael.KeySize = 256;
rijndael.BlockSize = 128;
var key = new Rfc2898DeriveBytes(System.Text.Encoding.ASCII.GetBytes(encryptionKey), saltVal, 1000);
rijndael.Key = key.GetBytes(rijndael.KeySize / 8);
//AES.Key = System.Text.Encoding.ASCII.GetBytes(encryptionKey);
//AES.IV = key.GetBytes(AES.BlockSize / 8);
rijndael.IV = ivVal;
rijndael.Mode = CipherMode.CBC;
rijndael.Padding = PaddingMode.None;
using (var cs = new CryptoStream(ms, rijndael.CreateDecryptor(), CryptoStreamMode.Write))
{
cs.Write(buffer, 0, buffer.Length);
cs.Close();
using (FileStream fs = new FileStream(inputFile + Consts.FILE_EXT, FileMode.Create))
{
byte[] decryptedBytes = ms.ToArray();
fs.Write(decryptedBytes, 0, decryptedBytes.Length);
fs.Close();
rv = true;
}
}
}
}
}
}
}
return rv;
}
编辑
打开其详细模式且未指定 rijndael-256 时,我收到以下消息。当我指定算法时,它确实在详细输出中反映了这一点;两者都正确解密文件。剧情变厚了……
算法:rijndael-128
密钥大小:32
模式:cbc
关键字模式:mcrypt-sha1
文件格式:mcrypt
此外,"passwords" 用于在软件的各个部分进行加密,范围从 12 到 28 个字符。
MCrypt 文件格式
使用 mcrypt-2.6.7-win32 进行观察,使用命令 mcrpyt.exe --no-openpgp -V test_in.txt
加密以下文件
test_in.txt
未加密的长度为25字节,上面的命令加密如下,得到文件test_out.txt.nc
,长度为125字节。
+-------------+----------------------+----------------+---------------------------------------------+
| File Offset | Field Length (bytes) | Field Content | Description |
+-------------+----------------------+----------------+---------------------------------------------+
| 0 | 1 | 0x0 | Zero byte |
+-------------+----------------------+----------------+---------------------------------------------+
| 1 | 1 | 0x6d | m |
+-------------+----------------------+----------------+---------------------------------------------+
| 2 | 1 | 0x3 | Version |
+-------------+----------------------+----------------+---------------------------------------------+
| 3 | 1 | 0x40 | Flags - bit 7 set = salt, bit 8 set = no IV |
+-------------+----------------------+----------------+---------------------------------------------+
| 4 | 13 | rijndael-128 | Algorithm name |
+-------------+----------------------+----------------+---------------------------------------------+
| 17 | 2 | 32 | Key Size |
+-------------+----------------------+----------------+---------------------------------------------+
| 19 | 4 | cbc | Algorithm mode |
+-------------+----------------------+----------------+---------------------------------------------+
| 23 | 12 | mcrypt-sha1 | Key generator algorithm |
+-------------+----------------------+----------------+---------------------------------------------+
| 35 | 1 | 21 | Salt length + 1 |
+-------------+----------------------+----------------+---------------------------------------------+
| 36 | 20 | Salt data | Salt |
+-------------+----------------------+----------------+---------------------------------------------+
| 56 | 5 | sha1 | Check sum algorithm |
+-------------+----------------------+----------------+---------------------------------------------+
| 61 | 16 | IV data | Initialisation vector |
+-------------+----------------------+----------------+---------------------------------------------+
| 77 | 48 | Encrypted data | 25 original data + 20 check sum + 3 padding |
+-------------+----------------------+----------------+---------------------------------------------+
| TOTAL | 125 | | |
+-------------+----------------------+----------------+---------------------------------------------+
观察不同场景下的输出,使用了以下block/key/IV尺寸:
+--------------+--------------------+------------+------------------+
| Algorithm | Block Size (bytes) | IV (bytes) | Key Size (bytes) |
+--------------+--------------------+------------+------------------+
| rijndael-128 | 16 | 16 | 32 |
+--------------+--------------------+------------+------------------+
| rijndael-256 | 32 | 32 | 32 |
+--------------+--------------------+------------+------------------+
校验和是对加密前的原始数据进行的,并附加在原始数据的末尾。使用的默认校验和算法是 SHA-1,它会产生 20 字节的散列。所以,原来25字节的数据变成了45字节。对于 128 位(16 字节)的块大小,这会导致 3 个字节的填充以达到 48 字节的块大小。对于 256 位(32 字节)的块大小,将有 19 字节的填充以达到 64 字节。零字节用于填充,这在解密过程中很重要,因为这些不会自动删除,因为原始数据的大小未知。
正在阅读header
这里是读取文件尾部header和加密数据的代码示例。为简洁起见,并未包含所有辅助函数。
public void ReadHeader(Stream stream)
{
byte[] buffer = new byte[512];
stream.Read(buffer, 0, 3);
if (buffer[0] != 0x0) throw new FormatException($"First byte is not 0x0, invalid MCrypt file");
if ((char)buffer[1] != 'm') throw new FormatException($"Second byte is not null, invalid MCrypt file");
if (buffer[2] != 0x3) throw new FormatException($"Third byte is not 0x3, invalid MCrypt file");
byte flags = (byte)stream.ReadByte();
KeyGeneratorUsesSalt = (flags & (1 << 6)) != 0;
HasInitialisationVector = (flags & (1 << 7)) != 1;
AlgorithmName = ReadNullTerminatedString(stream);
stream.Read(buffer, 0, 2);
KeySize = BitConverter.ToUInt16(buffer, 0);
BlockSize = GetBlockSize(AlgorithmName);
var cipherModeAsString = ReadNullTerminatedString(stream);
CipherMode cipherMode;
if (Enum.TryParse<CipherMode>(cipherModeAsString, out cipherMode))
CipherMode = cipherMode;
KeyGeneratorName = ReadNullTerminatedString(stream);
if (KeyGeneratorUsesSalt)
{
var saltSize = ((byte)stream.ReadByte()) - 1;
Salt = new byte[saltSize];
stream.Read(Salt, 0, saltSize);
}
CheckSumAlgorithmName = ReadNullTerminatedString(stream);
if (HasInitialisationVector)
{
InitialisationVector = new byte[BlockSize / 8];
stream.Read(InitialisationVector, 0, BlockSize / 8);
}
int read = 0;
byte[] remainingData = null;
using (MemoryStream mem = new MemoryStream())
{
while ((read = stream.Read(buffer, 0, buffer.Length)) != 0)
{
mem.Write(buffer, 0, read);
remainingData = mem.ToArray();
}
}
EncryptedData = remainingData;
}
密钥生成
密钥生成器算法在 header 中指定,默认情况下 MCrypt 格式为 mcrypt-sha1。查看 mcrypt 源,该密钥是使用 mhash 库生成的。它将密码短语与盐相结合,生成算法所需字节数的密钥(在我查看的两种情况下均为 32 字节)。我将函数 _mhash_gen_key_mcrypt
从 mhash 库翻译成 C#,如下所示 - 也许它已经在 .NET 框架中的某个地方,但如果是的话我找不到它。
public byte[] GenerateKeyMcryptSha1(string passPhrase, byte[] salt, int keySize)
{
byte[] key = new byte[KeySize], digest = null;
int hashSize = 20;
byte[] password = Encoding.ASCII.GetBytes(passPhrase);
int keyBytes = 0;
while (true)
{
byte[] inputData = null;
using (MemoryStream stream = new MemoryStream())
{
if (Salt != null)
stream.Write(salt, 0, salt.Length);
stream.Write(password, 0, password.Length);
if (keyBytes > 0)
stream.Write(key, 0, keyBytes);
inputData = stream.ToArray();
}
using (var sha1 = new SHA1Managed())
digest = sha1.ComputeHash(inputData);
if (keySize > hashSize)
{
Buffer.BlockCopy(digest, 0, key, keyBytes, hashSize);
keySize -= hashSize;
keyBytes += hashSize;
}
else
{
Buffer.BlockCopy(digest, 0, key, keyBytes, keySize);
break;
}
}
return key;
}
解密
我们可以使用标准 .NET 加密 类 进行大部分解密,传入我们通过散列密码和盐生成的 32 字节密钥以及我们使用 128 位或 256 位的密钥-bit flavor 基于来自 header 的算法名称。我们分配通过 rijndael.IV = InitialisationVector;
.
从 header 读取的初始化向量 (IV)
/// <summary>
/// Decrypt using Rijndael
/// </summary>
/// <param name="key">Key to use for decryption that was generated from passphrase + salt</param>
/// <param name="keySize">Algo key size, e.g. 128 bit, 256 bit</param>
/// <returns>Unencrypted data</returns>
private byte[] DecryptRijndael(byte[] key, int keySize)
{
using (RijndaelManaged rijndael = GetRijndael(key, keySize))
{
rijndael.IV = InitialisationVector;
using (MemoryStream unencryptedStream = new MemoryStream())
using (MemoryStream encryptedStream = new MemoryStream(EncryptedData))
{
using (var cs = new CryptoStream(encryptedStream, rijndael.CreateDecryptor(), CryptoStreamMode.Read))
cs.CopyTo(unencryptedStream);
byte[] unencryptedData = RemovePaddingAndCheckSum(unencryptedStream.ToArray(), GetCheckSumLen());
return unencryptedData;
}
}
}
/// <summary>
/// Set algorithm mode/settings
/// </summary>
/// <param name="key">Key to use for decryption that was generated from passphrase + salt</param>
/// <param name="keySize">Algo key size, e.g. 128 bit, 256 bit</param>
/// <returns>Instance ready to decrypt</returns>
private RijndaelManaged GetRijndael(byte[] key, int keySize)
{
var rijndael = new RijndaelManaged()
{
Mode = CipherMode, // e.g. CBC
KeySize = keySize, // e.g. 256 bits
Key = key, // e.g. 32-byte sha-1 hash of passphrase + salt
BlockSize = BlockSize, // e.g. 256 bits
Padding = PaddingMode.Zeros
};
return rijndael;
}
由于填充样式是zero-bytes,这些在解密过程中不会被删除,因为我们不知道此时原始数据的大小,所以解密数据将始终是块的倍数大小与原始数据的大小无关。它还将在末尾附加校验和。我们可以简单地从解密块的尾部删除所有零字节,但如果真的以零字节结束,我们将冒着破坏校验和和原始数据的风险。
因此,我们可以从尾部一次向后处理一个字节,并使用校验和来验证我们何时拥有正确的原始数据。
/// <summary>
/// Remove zero padding by starting at the end of the data block assuming
/// no padding, and using the check sum appended to the end of the data to
/// verify the original data, incrementing padding until we match the
/// check sum or conclude data is corrupt
/// </summary>
/// <param name="data">Decrypted data block, including zero padding and checksum at end</param>
/// <param name="checkSumLen">Length of the checksum appended to the end of the data</param>
/// <returns>Unencrypted original data without padding and without check sum</returns>
private byte[] RemovePaddingAndCheckSum(byte[] data, int checkSumLen)
{
byte[] checkSum = new byte[checkSumLen];
int padding = 0;
while ((data.Length - checkSumLen - padding) > 0)
{
int checkSumStart = data.Length - checkSumLen - padding;
Buffer.BlockCopy(data, checkSumStart, checkSum, 0, checkSumLen);
int dataLength = data.Length - checkSumLen - padding;
byte[] dataClean = new byte[dataLength];
Buffer.BlockCopy(data, 0 , dataClean, 0, dataLength);
if (VerifyCheckSum(dataClean, checkSum))
return dataClean;
padding++;
}
throw new InvalidDataException("Unable to decrypt, check sum does not match");
}
SHA1 20 字节校验和可以简单地根据数据进行验证,如下所示:
private bool VerifySha1Hash(byte[] data, byte[] checkSum)
{
using (SHA1Managed sha1 = new SHA1Managed())
{
var checkSumRedone = sha1.ComputeHash(data);
return checkSumRedone.SequenceEqual(checkSum);
}
}
就是这样,通过3次尝试后的128位我们应该得到正确的校验和和相应的原始数据,然后我们将return作为未加密的原始数据发送给调用者。
我成为 C# 爱好者已有一段时间了,我会考虑拥有中级开发技能,但几乎没有加密知识。作为副项目的一部分,我需要解密使用 MCrypt 加密的文件。似乎没有任何特殊参数被传递到命令中。例如,这很常见(密钥和文件名已更改)并且密钥的长度各不相同,从 14 到 18 个字符不等。
mcrypt -a rijndael-256 fileToEncrypt.tar.gz -k 0123456789abcdef1
到目前为止,我已经采用了两种方法来完成这项任务。第一种是使用 mcrypt.exe 并使用 Process
启动进程。但是,我觉得这使得代码(和程序流程)非常笨拙。第二种是尝试直接从我的内部程序解密文件,并且外部程序依赖性为零;我想走这条路。
我对 MCrypt 格式有点困惑。我已经查看了源代码中的 FORMAT 文档(here 以在线查看)并且我相信我已经正确处理了 header 的开始部分。但是,我似乎无法解密文件中的加密数据。
1) IV 有多大,我如何将它传递到我的解密器中?
2) 文件末尾的校验和有多大,我需要它吗?
3)以上是静态的长度吗?
4) 什么是keymode (mcrypt-sha1) 以及如何使用?
5) 我注意到在正确解密(使用 mcrypt.exe)时,加密文件和解密文件之间存在 140 字节的差异。这 140 个字节是由什么组成的?
下面是加密文件的代码和开头;毫无疑问,我的代码从注释 "Get the data" 开始是错误的 任何正确方向的指示将不胜感激。
编辑
算法:rijndael-128
此外,"passwords" 用于在软件的各个部分进行加密,范围从 12 到 28 个字符。/// <summary>
/// Decrypt an mcrypt file using rijndael-256
/// </summary>
/// <param name="inputFile">File to decrypt</param>
/// <param name="encryptionKey">Password</param>
/// <param name="purge"></param>
public static bool Decrypt (string inputFile, string encryptionKey)
{
var rv = false;
if (File.Exists(inputFile) == true)
{
using (FileStream stream = new FileStream(inputFile, FileMode.Open))
{
var buffer = new byte[1024];
// MCrypt header
stream.Read(buffer, 0, 3);
if (buffer[0] == 0x00 && buffer[1] == 0x6D && buffer[2] == 0x03)
{
// Flag
// Bit 7 - Salt Used
// Bit 8 - IV not used
var flag = (byte)stream.ReadByte();
byte[] saltVal = null;
var saltUsed = Utils.GetBit(flag, 6);
byte[] ivVal = new byte[16];
var ivUsed = (Utils.GetBit(flag, 7) == false);
var algorithmName = Utils.GetNullTerminatedString(stream);
stream.Read(buffer, 0, 2);
var keyLen = (buffer[1] << 8) + buffer[0];
var algorithModeName = Utils.GetNullTerminatedString(stream);
var keygenName = Utils.GetNullTerminatedString(stream);
if (saltUsed)
{
var saltFlag = (byte)stream.ReadByte();
if (Utils.GetBit(saltFlag, 0))
{
// After clearing the first bit the salt flag is now the length
Utils.ClearBit (ref saltFlag, 0);
saltVal = new byte[saltFlag];
stream.Read(saltVal, 0, saltFlag);
}
}
var algorithmModeName = Utils.GetNullTerminatedString(stream);
if (ivUsed)
{
stream.Read(ivVal, 0, ivVal.Length);
}
// Get the data - how much to get???
buffer = new byte[stream.Length - stream.Position + 1];
var bytesRead = stream.Read(buffer, 0, buffer.Length);
using (MemoryStream ms = new MemoryStream())
{
using (RijndaelManaged rijndael = new RijndaelManaged())
{
rijndael.KeySize = 256;
rijndael.BlockSize = 128;
var key = new Rfc2898DeriveBytes(System.Text.Encoding.ASCII.GetBytes(encryptionKey), saltVal, 1000);
rijndael.Key = key.GetBytes(rijndael.KeySize / 8);
//AES.Key = System.Text.Encoding.ASCII.GetBytes(encryptionKey);
//AES.IV = key.GetBytes(AES.BlockSize / 8);
rijndael.IV = ivVal;
rijndael.Mode = CipherMode.CBC;
rijndael.Padding = PaddingMode.None;
using (var cs = new CryptoStream(ms, rijndael.CreateDecryptor(), CryptoStreamMode.Write))
{
cs.Write(buffer, 0, buffer.Length);
cs.Close();
using (FileStream fs = new FileStream(inputFile + Consts.FILE_EXT, FileMode.Create))
{
byte[] decryptedBytes = ms.ToArray();
fs.Write(decryptedBytes, 0, decryptedBytes.Length);
fs.Close();
rv = true;
}
}
}
}
}
}
}
return rv;
}
打开其详细模式且未指定 rijndael-256 时,我收到以下消息。当我指定算法时,它确实在详细输出中反映了这一点;两者都正确解密文件。剧情变厚了……
密钥大小:32
模式:cbc
关键字模式:mcrypt-sha1
文件格式:mcrypt
MCrypt 文件格式
使用 mcrypt-2.6.7-win32 进行观察,使用命令 mcrpyt.exe --no-openpgp -V test_in.txt
test_in.txt
未加密的长度为25字节,上面的命令加密如下,得到文件test_out.txt.nc
,长度为125字节。
+-------------+----------------------+----------------+---------------------------------------------+
| File Offset | Field Length (bytes) | Field Content | Description |
+-------------+----------------------+----------------+---------------------------------------------+
| 0 | 1 | 0x0 | Zero byte |
+-------------+----------------------+----------------+---------------------------------------------+
| 1 | 1 | 0x6d | m |
+-------------+----------------------+----------------+---------------------------------------------+
| 2 | 1 | 0x3 | Version |
+-------------+----------------------+----------------+---------------------------------------------+
| 3 | 1 | 0x40 | Flags - bit 7 set = salt, bit 8 set = no IV |
+-------------+----------------------+----------------+---------------------------------------------+
| 4 | 13 | rijndael-128 | Algorithm name |
+-------------+----------------------+----------------+---------------------------------------------+
| 17 | 2 | 32 | Key Size |
+-------------+----------------------+----------------+---------------------------------------------+
| 19 | 4 | cbc | Algorithm mode |
+-------------+----------------------+----------------+---------------------------------------------+
| 23 | 12 | mcrypt-sha1 | Key generator algorithm |
+-------------+----------------------+----------------+---------------------------------------------+
| 35 | 1 | 21 | Salt length + 1 |
+-------------+----------------------+----------------+---------------------------------------------+
| 36 | 20 | Salt data | Salt |
+-------------+----------------------+----------------+---------------------------------------------+
| 56 | 5 | sha1 | Check sum algorithm |
+-------------+----------------------+----------------+---------------------------------------------+
| 61 | 16 | IV data | Initialisation vector |
+-------------+----------------------+----------------+---------------------------------------------+
| 77 | 48 | Encrypted data | 25 original data + 20 check sum + 3 padding |
+-------------+----------------------+----------------+---------------------------------------------+
| TOTAL | 125 | | |
+-------------+----------------------+----------------+---------------------------------------------+
观察不同场景下的输出,使用了以下block/key/IV尺寸:
+--------------+--------------------+------------+------------------+
| Algorithm | Block Size (bytes) | IV (bytes) | Key Size (bytes) |
+--------------+--------------------+------------+------------------+
| rijndael-128 | 16 | 16 | 32 |
+--------------+--------------------+------------+------------------+
| rijndael-256 | 32 | 32 | 32 |
+--------------+--------------------+------------+------------------+
校验和是对加密前的原始数据进行的,并附加在原始数据的末尾。使用的默认校验和算法是 SHA-1,它会产生 20 字节的散列。所以,原来25字节的数据变成了45字节。对于 128 位(16 字节)的块大小,这会导致 3 个字节的填充以达到 48 字节的块大小。对于 256 位(32 字节)的块大小,将有 19 字节的填充以达到 64 字节。零字节用于填充,这在解密过程中很重要,因为这些不会自动删除,因为原始数据的大小未知。
正在阅读header
这里是读取文件尾部header和加密数据的代码示例。为简洁起见,并未包含所有辅助函数。
public void ReadHeader(Stream stream)
{
byte[] buffer = new byte[512];
stream.Read(buffer, 0, 3);
if (buffer[0] != 0x0) throw new FormatException($"First byte is not 0x0, invalid MCrypt file");
if ((char)buffer[1] != 'm') throw new FormatException($"Second byte is not null, invalid MCrypt file");
if (buffer[2] != 0x3) throw new FormatException($"Third byte is not 0x3, invalid MCrypt file");
byte flags = (byte)stream.ReadByte();
KeyGeneratorUsesSalt = (flags & (1 << 6)) != 0;
HasInitialisationVector = (flags & (1 << 7)) != 1;
AlgorithmName = ReadNullTerminatedString(stream);
stream.Read(buffer, 0, 2);
KeySize = BitConverter.ToUInt16(buffer, 0);
BlockSize = GetBlockSize(AlgorithmName);
var cipherModeAsString = ReadNullTerminatedString(stream);
CipherMode cipherMode;
if (Enum.TryParse<CipherMode>(cipherModeAsString, out cipherMode))
CipherMode = cipherMode;
KeyGeneratorName = ReadNullTerminatedString(stream);
if (KeyGeneratorUsesSalt)
{
var saltSize = ((byte)stream.ReadByte()) - 1;
Salt = new byte[saltSize];
stream.Read(Salt, 0, saltSize);
}
CheckSumAlgorithmName = ReadNullTerminatedString(stream);
if (HasInitialisationVector)
{
InitialisationVector = new byte[BlockSize / 8];
stream.Read(InitialisationVector, 0, BlockSize / 8);
}
int read = 0;
byte[] remainingData = null;
using (MemoryStream mem = new MemoryStream())
{
while ((read = stream.Read(buffer, 0, buffer.Length)) != 0)
{
mem.Write(buffer, 0, read);
remainingData = mem.ToArray();
}
}
EncryptedData = remainingData;
}
密钥生成
密钥生成器算法在 header 中指定,默认情况下 MCrypt 格式为 mcrypt-sha1。查看 mcrypt 源,该密钥是使用 mhash 库生成的。它将密码短语与盐相结合,生成算法所需字节数的密钥(在我查看的两种情况下均为 32 字节)。我将函数 _mhash_gen_key_mcrypt
从 mhash 库翻译成 C#,如下所示 - 也许它已经在 .NET 框架中的某个地方,但如果是的话我找不到它。
public byte[] GenerateKeyMcryptSha1(string passPhrase, byte[] salt, int keySize)
{
byte[] key = new byte[KeySize], digest = null;
int hashSize = 20;
byte[] password = Encoding.ASCII.GetBytes(passPhrase);
int keyBytes = 0;
while (true)
{
byte[] inputData = null;
using (MemoryStream stream = new MemoryStream())
{
if (Salt != null)
stream.Write(salt, 0, salt.Length);
stream.Write(password, 0, password.Length);
if (keyBytes > 0)
stream.Write(key, 0, keyBytes);
inputData = stream.ToArray();
}
using (var sha1 = new SHA1Managed())
digest = sha1.ComputeHash(inputData);
if (keySize > hashSize)
{
Buffer.BlockCopy(digest, 0, key, keyBytes, hashSize);
keySize -= hashSize;
keyBytes += hashSize;
}
else
{
Buffer.BlockCopy(digest, 0, key, keyBytes, keySize);
break;
}
}
return key;
}
解密
我们可以使用标准 .NET 加密 类 进行大部分解密,传入我们通过散列密码和盐生成的 32 字节密钥以及我们使用 128 位或 256 位的密钥-bit flavor 基于来自 header 的算法名称。我们分配通过 rijndael.IV = InitialisationVector;
.
/// <summary>
/// Decrypt using Rijndael
/// </summary>
/// <param name="key">Key to use for decryption that was generated from passphrase + salt</param>
/// <param name="keySize">Algo key size, e.g. 128 bit, 256 bit</param>
/// <returns>Unencrypted data</returns>
private byte[] DecryptRijndael(byte[] key, int keySize)
{
using (RijndaelManaged rijndael = GetRijndael(key, keySize))
{
rijndael.IV = InitialisationVector;
using (MemoryStream unencryptedStream = new MemoryStream())
using (MemoryStream encryptedStream = new MemoryStream(EncryptedData))
{
using (var cs = new CryptoStream(encryptedStream, rijndael.CreateDecryptor(), CryptoStreamMode.Read))
cs.CopyTo(unencryptedStream);
byte[] unencryptedData = RemovePaddingAndCheckSum(unencryptedStream.ToArray(), GetCheckSumLen());
return unencryptedData;
}
}
}
/// <summary>
/// Set algorithm mode/settings
/// </summary>
/// <param name="key">Key to use for decryption that was generated from passphrase + salt</param>
/// <param name="keySize">Algo key size, e.g. 128 bit, 256 bit</param>
/// <returns>Instance ready to decrypt</returns>
private RijndaelManaged GetRijndael(byte[] key, int keySize)
{
var rijndael = new RijndaelManaged()
{
Mode = CipherMode, // e.g. CBC
KeySize = keySize, // e.g. 256 bits
Key = key, // e.g. 32-byte sha-1 hash of passphrase + salt
BlockSize = BlockSize, // e.g. 256 bits
Padding = PaddingMode.Zeros
};
return rijndael;
}
由于填充样式是zero-bytes,这些在解密过程中不会被删除,因为我们不知道此时原始数据的大小,所以解密数据将始终是块的倍数大小与原始数据的大小无关。它还将在末尾附加校验和。我们可以简单地从解密块的尾部删除所有零字节,但如果真的以零字节结束,我们将冒着破坏校验和和原始数据的风险。
因此,我们可以从尾部一次向后处理一个字节,并使用校验和来验证我们何时拥有正确的原始数据。
/// <summary>
/// Remove zero padding by starting at the end of the data block assuming
/// no padding, and using the check sum appended to the end of the data to
/// verify the original data, incrementing padding until we match the
/// check sum or conclude data is corrupt
/// </summary>
/// <param name="data">Decrypted data block, including zero padding and checksum at end</param>
/// <param name="checkSumLen">Length of the checksum appended to the end of the data</param>
/// <returns>Unencrypted original data without padding and without check sum</returns>
private byte[] RemovePaddingAndCheckSum(byte[] data, int checkSumLen)
{
byte[] checkSum = new byte[checkSumLen];
int padding = 0;
while ((data.Length - checkSumLen - padding) > 0)
{
int checkSumStart = data.Length - checkSumLen - padding;
Buffer.BlockCopy(data, checkSumStart, checkSum, 0, checkSumLen);
int dataLength = data.Length - checkSumLen - padding;
byte[] dataClean = new byte[dataLength];
Buffer.BlockCopy(data, 0 , dataClean, 0, dataLength);
if (VerifyCheckSum(dataClean, checkSum))
return dataClean;
padding++;
}
throw new InvalidDataException("Unable to decrypt, check sum does not match");
}
SHA1 20 字节校验和可以简单地根据数据进行验证,如下所示:
private bool VerifySha1Hash(byte[] data, byte[] checkSum)
{
using (SHA1Managed sha1 = new SHA1Managed())
{
var checkSumRedone = sha1.ComputeHash(data);
return checkSumRedone.SequenceEqual(checkSum);
}
}
就是这样,通过3次尝试后的128位我们应该得到正确的校验和和相应的原始数据,然后我们将return作为未加密的原始数据发送给调用者。