Skip to content

Commit

Permalink
Merge pull request #788 from Erior/develop
Browse files Browse the repository at this point in the history
RAR5 decryption support
  • Loading branch information
adamhathcock authored Dec 18, 2023
2 parents 08eed53 + 619e44b commit 28ea50b
Show file tree
Hide file tree
Showing 16 changed files with 419 additions and 2,137 deletions.
11 changes: 10 additions & 1 deletion src/SharpCompress/Archives/Rar/SeekableFilePart.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,19 @@ internal SeekableFilePart(
internal override Stream GetCompressedStream()
{
stream.Position = FileHeader.DataStartPosition;

if (FileHeader.R4Salt != null)
{
return new RarCryptoWrapper(stream, password!, FileHeader.R4Salt);
var cryptKey = new CryptKey3(password!);
return new RarCryptoWrapper(stream, FileHeader.R4Salt, cryptKey);
}

if (FileHeader.Rar5CryptoInfo != null)
{
var cryptKey = new CryptKey5(password!, FileHeader.Rar5CryptoInfo);
return new RarCryptoWrapper(stream, FileHeader.Rar5CryptoInfo.Salt, cryptKey);
}

return stream;
}

Expand Down
87 changes: 87 additions & 0 deletions src/SharpCompress/Common/Rar/CryptKey3.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#nullable disable

using System.Security.Cryptography;
using System.Text;
using SharpCompress.Common.Rar.Headers;

namespace SharpCompress.Common.Rar;

internal class CryptKey3 : ICryptKey
{
const int AES_128 = 128;

private string _password;

public CryptKey3(string password)
{
_password = password ?? "";
}

public ICryptoTransform Transformer(byte[] salt)
{
var aesIV = new byte[EncryptionConstV5.SIZE_INITV];

var rawLength = 2 * _password.Length;
var rawPassword = new byte[rawLength + EncryptionConstV5.SIZE_SALT30];
var passwordBytes = Encoding.UTF8.GetBytes(_password);
for (var i = 0; i < _password.Length; i++)
{
rawPassword[i * 2] = passwordBytes[i];
rawPassword[(i * 2) + 1] = 0;
}

for (var i = 0; i < salt.Length; i++)
{
rawPassword[i + rawLength] = salt[i];
}

var msgDigest = SHA1.Create();
const int noOfRounds = (1 << 18);
const int iblock = 3;

byte[] digest;
var data = new byte[(rawPassword.Length + iblock) * noOfRounds];

//TODO slow code below, find ways to optimize
for (var i = 0; i < noOfRounds; i++)
{
rawPassword.CopyTo(data, i * (rawPassword.Length + iblock));

data[(i * (rawPassword.Length + iblock)) + rawPassword.Length + 0] = (byte)i;
data[(i * (rawPassword.Length + iblock)) + rawPassword.Length + 1] = (byte)(i >> 8);
data[(i * (rawPassword.Length + iblock)) + rawPassword.Length + 2] = (byte)(i >> 16);

if (i % (noOfRounds / EncryptionConstV5.SIZE_INITV) == 0)
{
digest = msgDigest.ComputeHash(data, 0, (i + 1) * (rawPassword.Length + iblock));
aesIV[i / (noOfRounds / EncryptionConstV5.SIZE_INITV)] = digest[19];
}
}
digest = msgDigest.ComputeHash(data);
//slow code ends

var aesKey = new byte[EncryptionConstV5.SIZE_INITV];
for (var i = 0; i < 4; i++)
{
for (var j = 0; j < 4; j++)
{
aesKey[(i * 4) + j] = (byte)(
(
((digest[i * 4] * 0x1000000) & 0xff000000)
| (uint)((digest[(i * 4) + 1] * 0x10000) & 0xff0000)
| (uint)((digest[(i * 4) + 2] * 0x100) & 0xff00)
| (uint)(digest[(i * 4) + 3] & 0xff)
) >> (j * 8)
);
}
}

var aes = Aes.Create();
aes.KeySize = AES_128;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.None;
aes.Key = aesKey;
aes.IV = aesIV;
return aes.CreateDecryptor();
}
}
97 changes: 97 additions & 0 deletions src/SharpCompress/Common/Rar/CryptKey5.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using SharpCompress.Common.Rar.Headers;

namespace SharpCompress.Common.Rar;

internal class CryptKey5 : ICryptKey
{
const int AES_256 = 256;
const int DERIVED_KEY_LENGTH = 0x10;
const int SHA256_DIGEST_SIZE = 32;

private string _password;
private Rar5CryptoInfo _cryptoInfo;
private byte[] _pswCheck = { };
private byte[] _hashKey = { };

public CryptKey5(string password, Rar5CryptoInfo rar5CryptoInfo)
{
_password = password;
_cryptoInfo = rar5CryptoInfo;
}

public byte[] PswCheck => _pswCheck;

public byte[] HashKey => _hashKey;

private static List<byte[]> GenerateRarPBKDF2Key(
string password,
byte[] salt,
int iterations,
int keyLength
)
{
using (HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(password)))
{
byte[] block = hmac.ComputeHash(salt);
byte[] finalHash = (byte[])block.Clone();

var loop = new int[] { iterations, 17, 17 };
var res = new List<byte[]> { };

for (int x = 0; x < 3; x++)
{
for (int i = 1; i < loop[x]; i++)
{
block = hmac.ComputeHash(block);
for (int j = 0; j < finalHash.Length; j++)
{
finalHash[j] ^= block[j];
}
}

res.Add((byte[])finalHash.Clone());
}

return res;
}
}

public ICryptoTransform Transformer(byte[] salt)
{
int iterations = (1 << _cryptoInfo.LG2Count); // Adjust the number of iterations as needed

var salt_rar5 = salt.Concat(new byte[] { 0, 0, 0, 1 });
var derivedKey = GenerateRarPBKDF2Key(
_password,
salt_rar5.ToArray(),
iterations,
DERIVED_KEY_LENGTH
);

_hashKey = derivedKey[1];

_pswCheck = new byte[EncryptionConstV5.SIZE_PSWCHECK];

for (int i = 0; i < SHA256_DIGEST_SIZE; i++)
{
_pswCheck[i % EncryptionConstV5.SIZE_PSWCHECK] ^= derivedKey[2][i];
}

if (_cryptoInfo.UsePswCheck && !_cryptoInfo.PswCheck.SequenceEqual(_pswCheck))
{
throw new CryptographicException("The password did not match.");
}

var aes = Aes.Create();
aes.KeySize = AES_256;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.None;
aes.Key = derivedKey[0];
aes.IV = _cryptoInfo.InitV;
return aes.CreateDecryptor();
}
}
41 changes: 6 additions & 35 deletions src/SharpCompress/Common/Rar/Headers/ArchiveCryptHeader.cs
Original file line number Diff line number Diff line change
@@ -1,50 +1,21 @@
#nullable disable

using System;
using System.Security.Cryptography;
using SharpCompress.Common.Rar.Headers;
using SharpCompress.IO;

namespace SharpCompress.Common.Rar.Headers;

internal class ArchiveCryptHeader : RarHeader
{
private const int CRYPT_VERSION = 0; // Supported encryption version.
private const int SIZE_SALT50 = 16;
private const int SIZE_PSWCHECK = 8;
private const int SIZE_PSWCHECK_CSUM = 4;
private const int CRYPT5_KDF_LG2_COUNT_MAX = 24; // LOG2 of maximum accepted iteration count.

private bool _usePswCheck;
private uint _lg2Count; // Log2 of PBKDF2 repetition count.
private byte[] _salt;
private byte[] _pswCheck;
private byte[] _pswCheckCsm;

public ArchiveCryptHeader(RarHeader header, RarCrcBinaryReader reader)
: base(header, reader, HeaderType.Crypt) { }

public Rar5CryptoInfo CryptInfo = new Rar5CryptoInfo();

protected override void ReadFinish(MarkingBinaryReader reader)
{
var cryptVersion = reader.ReadRarVIntUInt32();
if (cryptVersion > CRYPT_VERSION)
{
//error?
return;
}
var encryptionFlags = reader.ReadRarVIntUInt32();
_usePswCheck = FlagUtility.HasFlag(encryptionFlags, EncryptionFlagsV5.CHFL_CRYPT_PSWCHECK);
_lg2Count = reader.ReadRarVIntByte(1);

//UsePswCheck = HasHeaderFlag(EncryptionFlagsV5.CHFL_CRYPT_PSWCHECK);
if (_lg2Count > CRYPT5_KDF_LG2_COUNT_MAX)
{
//error?
return;
}

_salt = reader.ReadBytes(SIZE_SALT50);
if (_usePswCheck)
{
_pswCheck = reader.ReadBytes(SIZE_PSWCHECK);
_pswCheckCsm = reader.ReadBytes(SIZE_PSWCHECK_CSUM);
}
CryptInfo = new Rar5CryptoInfo(reader, false);
}
}
Loading

0 comments on commit 28ea50b

Please sign in to comment.