mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-11-15 17:02:03 +00:00
Merge pull request #1419 from adamhathcock/fix/rar5-header-key-cache
Cache RAR5 encrypted header key derivation
This commit is contained in:
@@ -12,8 +12,11 @@ internal class CryptKey5 : ICryptKey
|
||||
const int DERIVED_KEY_LENGTH = 0x10;
|
||||
const int SHA256_DIGEST_SIZE = 32;
|
||||
|
||||
private string _password;
|
||||
private Rar5CryptoInfo _cryptoInfo;
|
||||
private readonly string _password;
|
||||
private readonly Rar5CryptoInfo _cryptoInfo;
|
||||
private byte[]? _aesKey;
|
||||
private byte[] _derivedSalt = [];
|
||||
private int _derivedLg2Count;
|
||||
private byte[] _pswCheck = [];
|
||||
private byte[] _hashKey = [];
|
||||
|
||||
@@ -69,23 +72,29 @@ internal class CryptKey5 : ICryptKey
|
||||
|
||||
public ICryptoTransform Transformer(byte[] salt)
|
||||
{
|
||||
var 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 (var i = 0; i < SHA256_DIGEST_SIZE; i++)
|
||||
// Header IVs change for each block, but the expensive KDF inputs do not.
|
||||
// Snapshot the salt so in-place changes cannot reuse an unrelated key.
|
||||
if (
|
||||
_aesKey is null
|
||||
|| _derivedLg2Count != _cryptoInfo.LG2Count
|
||||
|| !_derivedSalt.SequenceEqual(salt)
|
||||
)
|
||||
{
|
||||
_pswCheck[i % EncryptionConstV5.SIZE_PSWCHECK] ^= derivedKey[2][i];
|
||||
var derivedKey = GenerateRarPBKDF2Key(
|
||||
_password,
|
||||
salt.Concat(new byte[] { 0, 0, 0, 1 }).ToArray(),
|
||||
1 << _cryptoInfo.LG2Count,
|
||||
DERIVED_KEY_LENGTH
|
||||
);
|
||||
_hashKey = derivedKey[1];
|
||||
_pswCheck = new byte[EncryptionConstV5.SIZE_PSWCHECK];
|
||||
for (var i = 0; i < SHA256_DIGEST_SIZE; i++)
|
||||
{
|
||||
_pswCheck[i % EncryptionConstV5.SIZE_PSWCHECK] ^= derivedKey[2][i];
|
||||
}
|
||||
_derivedSalt = (byte[])salt.Clone();
|
||||
_derivedLg2Count = _cryptoInfo.LG2Count;
|
||||
_aesKey = derivedKey[0];
|
||||
}
|
||||
|
||||
if (_cryptoInfo.UsePswCheck && !_cryptoInfo.PswCheck.SequenceEqual(_pswCheck))
|
||||
@@ -93,11 +102,11 @@ internal class CryptKey5 : ICryptKey
|
||||
throw new CryptographicException("The password did not match.");
|
||||
}
|
||||
|
||||
var aes = Aes.Create();
|
||||
using var aes = Aes.Create();
|
||||
aes.KeySize = AES_256;
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.None;
|
||||
aes.Key = derivedKey[0];
|
||||
aes.Key = _aesKey;
|
||||
aes.IV = _cryptoInfo.InitV;
|
||||
return aes.CreateDecryptor();
|
||||
}
|
||||
|
||||
@@ -67,10 +67,8 @@ public partial class RarHeaderFactory
|
||||
await _cryptInfo
|
||||
.ReadInitVAsync(new AsyncMarkingBinaryReader(stream))
|
||||
.ConfigureAwait(false);
|
||||
var _headerKey = new CryptKey5(Options.Password!, _cryptInfo);
|
||||
|
||||
reader = await AsyncRarCryptoBinaryReader
|
||||
.Create(stream, _headerKey, _cryptInfo.Salt)
|
||||
.Create(stream, GetHeaderKey(), _cryptInfo.Salt)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
@@ -215,6 +213,7 @@ public partial class RarHeaderFactory
|
||||
.ConfigureAwait(false);
|
||||
IsEncrypted = true;
|
||||
_cryptInfo = cryptoHeader.CryptInfo;
|
||||
_headerKey = null;
|
||||
|
||||
return cryptoHeader;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,20 @@ public partial class RarHeaderFactory
|
||||
private bool _isRar5;
|
||||
|
||||
private Rar5CryptoInfo? _cryptInfo;
|
||||
private CryptKey5? _headerKey;
|
||||
private string? _headerPassword;
|
||||
|
||||
private CryptKey5 GetHeaderKey()
|
||||
{
|
||||
// ReaderOptions is mutable; changing its password must not keep using
|
||||
// a key derived from the previous password.
|
||||
if (_headerKey is null || _headerPassword != Options.Password)
|
||||
{
|
||||
_headerPassword = Options.Password;
|
||||
_headerKey = new CryptKey5(_headerPassword, _cryptInfo.NotNull());
|
||||
}
|
||||
return _headerKey;
|
||||
}
|
||||
|
||||
public RarHeaderFactory(StreamingMode mode, ReaderOptions options)
|
||||
{
|
||||
@@ -62,9 +76,7 @@ public partial class RarHeaderFactory
|
||||
if (_isRar5 && _cryptInfo != null)
|
||||
{
|
||||
_cryptInfo.ReadInitV(new MarkingBinaryReader(stream));
|
||||
var _headerKey = new CryptKey5(Options.Password!, _cryptInfo);
|
||||
|
||||
reader = RarCryptoBinaryReader.Create(stream, _headerKey, _cryptInfo.Salt);
|
||||
reader = RarCryptoBinaryReader.Create(stream, GetHeaderKey(), _cryptInfo.Salt);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -191,6 +203,7 @@ public partial class RarHeaderFactory
|
||||
var cryptoHeader = ArchiveCryptHeader.Create(header, reader);
|
||||
IsEncrypted = true;
|
||||
_cryptInfo = cryptoHeader.CryptInfo;
|
||||
_headerKey = null;
|
||||
|
||||
return cryptoHeader;
|
||||
}
|
||||
|
||||
84
tests/SharpCompress.Test/Rar/CryptKey5Tests.cs
Normal file
84
tests/SharpCompress.Test/Rar/CryptKey5Tests.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SharpCompress.Common.Rar;
|
||||
using SharpCompress.Common.Rar.Headers;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Rar;
|
||||
|
||||
public class CryptKey5Tests : TestBase
|
||||
{
|
||||
private Rar5CryptoInfo ReadCryptoInfo()
|
||||
{
|
||||
using var stream = File.OpenRead(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Rar5.encrypted_filesAndHeader.rar")
|
||||
);
|
||||
var factory = new RarHeaderFactory(StreamingMode.Seekable, ReaderOptions.ForExternalStream);
|
||||
var info = factory.ReadHeaders(stream).OfType<ArchiveCryptHeader>().First().CryptInfo;
|
||||
info.InitV = new byte[16];
|
||||
return info;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rar5_Key_ReusesDerivedMaterial_WithDifferentIVs()
|
||||
{
|
||||
var info = ReadCryptoInfo();
|
||||
var key = new CryptKey5("test", info);
|
||||
using var first = key.Transformer(info.Salt);
|
||||
var firstBlock = first.TransformFinalBlock(new byte[16], 0, 16);
|
||||
var hashKey = key.HashKey;
|
||||
var passwordCheck = key.PswCheck;
|
||||
|
||||
info.InitV[0] = 1;
|
||||
using var second = key.Transformer((byte[])info.Salt.Clone());
|
||||
var secondBlock = second.TransformFinalBlock(new byte[16], 0, 16);
|
||||
Assert.Same(hashKey, key.HashKey);
|
||||
Assert.Same(passwordCheck, key.PswCheck);
|
||||
Assert.False(firstBlock.SequenceEqual(secondBlock));
|
||||
|
||||
using var fresh = new CryptKey5("test", info).Transformer(info.Salt);
|
||||
Assert.Equal(fresh.TransformFinalBlock(new byte[16], 0, 16), secondBlock);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void Rar5_Key_InvalidatesDerivedMaterial_WhenInputsChange(bool changeIterations)
|
||||
{
|
||||
var info = ReadCryptoInfo();
|
||||
var key = new CryptKey5("test", info);
|
||||
using var first = key.Transformer(info.Salt);
|
||||
var hashKey = key.HashKey;
|
||||
info.UsePswCheck = false;
|
||||
if (changeIterations)
|
||||
{
|
||||
info.LG2Count--;
|
||||
}
|
||||
else
|
||||
{
|
||||
info.Salt[0] ^= 1;
|
||||
}
|
||||
|
||||
using var second = key.Transformer(info.Salt);
|
||||
using var fresh = new CryptKey5("test", info).Transformer(info.Salt);
|
||||
Assert.NotSame(hashKey, key.HashKey);
|
||||
Assert.Equal(
|
||||
fresh.TransformFinalBlock(new byte[16], 0, 16),
|
||||
second.TransformFinalBlock(new byte[16], 0, 16)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rar5_Key_ValidatesPasswordCheck_OnCacheHit()
|
||||
{
|
||||
var info = ReadCryptoInfo();
|
||||
var key = new CryptKey5("test", info);
|
||||
using var first = key.Transformer(info.Salt);
|
||||
info.PswCheck[0] ^= 1;
|
||||
Assert.Throws<SharpCompress.Common.CryptographicException>(() =>
|
||||
key.Transformer(info.Salt)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Rar.Headers;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
@@ -52,4 +54,64 @@ public class RarHeaderFactoryTest : TestBase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task Rar5_ReadHeaders_WithCachedKey(bool useAsync)
|
||||
{
|
||||
var options = ReaderOptions.ForExternalStream.WithPassword("test");
|
||||
Assert.Equal(6, await CountEncryptedFileHeaders(useAsync, options, false));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task Rar5_ReadHeaders_RejectChangedPassword(bool useAsync)
|
||||
{
|
||||
var options = ReaderOptions.ForExternalStream.WithPassword("test");
|
||||
await Assert.ThrowsAsync<CryptographicException>(async () =>
|
||||
await CountEncryptedFileHeaders(useAsync, options, true)
|
||||
);
|
||||
}
|
||||
|
||||
private async Task<int> CountEncryptedFileHeaders(
|
||||
bool useAsync,
|
||||
ReaderOptions options,
|
||||
bool changePassword
|
||||
)
|
||||
{
|
||||
using var stream = File.OpenRead(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Rar5.encrypted_filesAndHeader.rar")
|
||||
);
|
||||
var factory = new RarHeaderFactory(StreamingMode.Seekable, options);
|
||||
var count = 0;
|
||||
if (useAsync)
|
||||
{
|
||||
await foreach (var header in factory.ReadHeadersAsync(stream))
|
||||
{
|
||||
CountHeader(header);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var header in factory.ReadHeaders(stream))
|
||||
{
|
||||
CountHeader(header);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
|
||||
void CountHeader(IRarHeader header)
|
||||
{
|
||||
if (header.HeaderType == HeaderType.File)
|
||||
{
|
||||
count++;
|
||||
if (changePassword)
|
||||
{
|
||||
options.Password = "failed";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user