Формат файла 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 | | |
+-------------+----------------------+----------------+---------------------------------------------+
Наблюдая вывод в различных сценариях, следующий блок / ключ /Используются размеры 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 байта.Нулевые байты используются для заполнения, что важно во время расшифровки, так как они не удаляются автоматически, поскольку размер исходных данных неизвестен.
Чтение заголовка
Вот пример кодачтение заголовка и зашифрованных данных в конце файла.Для краткости включены не все вспомогательные функции.
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;
}
Генерация ключей
Алгоритм генерации ключей указан в заголовке и по умолчанию в формате MCrypt - mcrypt-sha1.Если посмотреть на источник mcrypt, этот ключ генерируется с использованием библиотеки mhash.Он комбинирует парольную фразу с солью, чтобы получить ключ с необходимым количеством байтов для алгоритма (32 байта в обоих случаях, которые я рассматривал).Я перевел функцию _mhash_gen_key_mcrypt
из библиотеки mhash на C #, как показано ниже - возможно, она уже где-то в .NET Framework, но если так, я не смог ее найти.
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-битный вариант на основе алгоритманазвание из шапки.Мы присваиваем вектор инициализации (IV), который мы читаем из заголовка, с помощью 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;
}
Поскольку стиль заполнения равен 0 байтам, они не удаляются при расшифровке, поскольку мы не знаем размерисходных данных в этой точке, поэтому дешифрованные данные всегда будут кратны размеру блока независимо от размера исходных данных.Также будет добавлена контрольная сумма до конца.Мы могли бы просто удалить все нулевые байты из хвоста дешифрованного блока, но мы рискнули бы испортить контрольную сумму и исходные данные, если бы это действительно заканчивалось нулевым байтом.
Так что вместо этого мы могли бы работать в обратном направлении на один байтвремя от конца и используйте контрольную сумму для проверки правильности исходных данных.
/// <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");
}
Контрольная сумма в 20 байтов SHA1 может быть проверена по данным просто следующим образом:
private bool VerifySha1Hash(byte[] data, byte[] checkSum)
{
using (SHA1Managed sha1 = new SHA1Managed())
{
var checkSumRedone = sha1.ComputeHash(data);
return checkSumRedone.SequenceEqual(checkSum);
}
}
И все, с 128-битным после 3 попыток мы должны получить правильную контрольную сумму и соответствующие исходные данные, которые мы затем возвращаем вызывающей стороне как незашифрованные исходные данные.