mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-11-15 17:02:03 +00:00
Reuse RAR5 derived keys across encrypted file entries
This commit is contained in:
@@ -12,7 +12,7 @@ internal sealed class FileInfoRarFilePart : SeekableFilePart
|
||||
FileHeader fh,
|
||||
FileInfo fi
|
||||
)
|
||||
: base(mh, fh, volume.Index, volume.Stream, password) => FileInfo = fi;
|
||||
: base(mh, fh, volume.Index, volume.Stream, password, volume.KeyCache) => FileInfo = fi;
|
||||
|
||||
internal FileInfo FileInfo { get; }
|
||||
|
||||
|
||||
@@ -8,18 +8,21 @@ internal class SeekableFilePart : RarFilePart
|
||||
{
|
||||
private readonly Stream _stream;
|
||||
private readonly string? _password;
|
||||
private readonly Rar5KeyCache _keyCache;
|
||||
|
||||
internal SeekableFilePart(
|
||||
MarkHeader mh,
|
||||
FileHeader fh,
|
||||
int index,
|
||||
Stream stream,
|
||||
string? password
|
||||
string? password,
|
||||
Rar5KeyCache keyCache
|
||||
)
|
||||
: base(mh, fh, index)
|
||||
{
|
||||
_stream = stream;
|
||||
_password = password;
|
||||
_keyCache = keyCache;
|
||||
}
|
||||
|
||||
internal override Stream GetCompressedStream()
|
||||
@@ -34,7 +37,7 @@ internal class SeekableFilePart : RarFilePart
|
||||
|
||||
if (FileHeader.Rar5CryptoInfo != null)
|
||||
{
|
||||
var cryptKey = new CryptKey5(_password!, FileHeader.Rar5CryptoInfo);
|
||||
var cryptKey = new CryptKey5(_password!, FileHeader.Rar5CryptoInfo, _keyCache);
|
||||
return new RarCryptoWrapper(_stream, FileHeader.Rar5CryptoInfo.Salt, cryptKey);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,5 +18,12 @@ internal class StreamRarArchiveVolume : RarVolume
|
||||
GetVolumeFilePartsAsync();
|
||||
|
||||
internal override RarFilePart CreateFilePart(MarkHeader markHeader, FileHeader fileHeader) =>
|
||||
new SeekableFilePart(markHeader, fileHeader, Index, Stream, ReaderOptions.Password);
|
||||
new SeekableFilePart(
|
||||
markHeader,
|
||||
fileHeader,
|
||||
Index,
|
||||
Stream,
|
||||
ReaderOptions.Password,
|
||||
KeyCache
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,35 +9,32 @@ 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 readonly string _password;
|
||||
private readonly Rar5CryptoInfo _cryptoInfo;
|
||||
private readonly Rar5KeyCache _keyCache;
|
||||
private byte[]? _aesKey;
|
||||
private byte[] _derivedSalt = [];
|
||||
private int _derivedLg2Count;
|
||||
private byte[] _pswCheck = [];
|
||||
private byte[] _hashKey = [];
|
||||
|
||||
public CryptKey5(string? password, Rar5CryptoInfo rar5CryptoInfo)
|
||||
public CryptKey5(string? password, Rar5CryptoInfo rar5CryptoInfo, Rar5KeyCache? keyCache = null)
|
||||
{
|
||||
_password = password ?? "";
|
||||
_cryptoInfo = rar5CryptoInfo;
|
||||
_keyCache = keyCache ?? new Rar5KeyCache();
|
||||
}
|
||||
|
||||
public byte[] PswCheck => _pswCheck;
|
||||
|
||||
public byte[] HashKey => _hashKey;
|
||||
|
||||
private static List<byte[]> GenerateRarPBKDF2Key(
|
||||
string password,
|
||||
byte[] salt,
|
||||
int iterations,
|
||||
int keyLength
|
||||
)
|
||||
internal static List<byte[]> GenerateRarPBKDF2Key(string password, byte[] salt, int iterations)
|
||||
{
|
||||
var passwordBytes = Encoding.UTF8.GetBytes(password);
|
||||
salt = salt.Concat(new byte[] { 0, 0, 0, 1 }).ToArray();
|
||||
#if LEGACY_DOTNET
|
||||
using var hmac = new HMACSHA256(passwordBytes);
|
||||
var block = hmac.ComputeHash(salt);
|
||||
@@ -80,12 +77,7 @@ internal class CryptKey5 : ICryptKey
|
||||
|| !_derivedSalt.SequenceEqual(salt)
|
||||
)
|
||||
{
|
||||
var derivedKey = GenerateRarPBKDF2Key(
|
||||
_password,
|
||||
salt.Concat(new byte[] { 0, 0, 0, 1 }).ToArray(),
|
||||
1 << _cryptoInfo.LG2Count,
|
||||
DERIVED_KEY_LENGTH
|
||||
);
|
||||
var derivedKey = _keyCache.GetKeys(_password, salt, _cryptoInfo.LG2Count);
|
||||
_hashKey = derivedKey[1];
|
||||
_pswCheck = new byte[EncryptionConstV5.SIZE_PSWCHECK];
|
||||
for (var i = 0; i < SHA256_DIGEST_SIZE; i++)
|
||||
|
||||
@@ -185,7 +185,8 @@ public partial class RarHeaderFactory
|
||||
fh.R4Salt is null
|
||||
? new CryptKey5(
|
||||
Options.Password,
|
||||
fh.Rar5CryptoInfo.NotNull()
|
||||
fh.Rar5CryptoInfo.NotNull(),
|
||||
KeyCache
|
||||
)
|
||||
: new CryptKey3(Options.Password)
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ public partial class RarHeaderFactory
|
||||
private Rar5CryptoInfo? _cryptInfo;
|
||||
private CryptKey5? _headerKey;
|
||||
private string? _headerPassword;
|
||||
internal Rar5KeyCache KeyCache { get; } = new();
|
||||
|
||||
private CryptKey5 GetHeaderKey()
|
||||
{
|
||||
@@ -23,7 +24,7 @@ public partial class RarHeaderFactory
|
||||
if (_headerKey is null || _headerPassword != Options.Password)
|
||||
{
|
||||
_headerPassword = Options.Password;
|
||||
_headerKey = new CryptKey5(_headerPassword, _cryptInfo.NotNull());
|
||||
_headerKey = new CryptKey5(_headerPassword, _cryptInfo.NotNull(), KeyCache);
|
||||
}
|
||||
return _headerKey;
|
||||
}
|
||||
@@ -180,7 +181,8 @@ public partial class RarHeaderFactory
|
||||
fh.R4Salt is null
|
||||
? new CryptKey5(
|
||||
Options.Password,
|
||||
fh.Rar5CryptoInfo.NotNull()
|
||||
fh.Rar5CryptoInfo.NotNull(),
|
||||
KeyCache
|
||||
)
|
||||
: new CryptKey3(Options.Password)
|
||||
);
|
||||
@@ -247,7 +249,7 @@ public partial class RarHeaderFactory
|
||||
return new RarCryptoWrapper(
|
||||
packedStream,
|
||||
header.Rar5CryptoInfo.Salt,
|
||||
new CryptKey5(Options.Password, header.Rar5CryptoInfo)
|
||||
new CryptKey5(Options.Password, header.Rar5CryptoInfo, KeyCache)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
40
src/SharpCompress/Common/Rar/Rar5KeyCache.cs
Normal file
40
src/SharpCompress/Common/Rar/Rar5KeyCache.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace SharpCompress.Common.Rar;
|
||||
|
||||
// Cache only KDF output, never entry-specific IVs, checks, or AES transforms.
|
||||
internal sealed class Rar5KeyCache
|
||||
{
|
||||
private const int Capacity = 4;
|
||||
private readonly List<(string Password, byte[] Salt, int Lg2Count, List<byte[]> Keys)> entries =
|
||||
new();
|
||||
|
||||
internal List<byte[]> GetKeys(string password, byte[] salt, int lg2Count)
|
||||
{
|
||||
lock (entries)
|
||||
{
|
||||
foreach (var (cachedPassword, cachedSalt, cachedCount, cachedKeys) in entries)
|
||||
{
|
||||
if (
|
||||
cachedPassword == password
|
||||
&& cachedCount == lg2Count
|
||||
&& cachedSalt.SequenceEqual(salt)
|
||||
)
|
||||
{
|
||||
return cachedKeys;
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot mutable metadata so later changes cannot corrupt cache identity.
|
||||
var snapshot = (byte[])salt.Clone();
|
||||
var keys = CryptKey5.GenerateRarPBKDF2Key(password, snapshot, 1 << lg2Count);
|
||||
if (entries.Count == Capacity)
|
||||
{
|
||||
entries.RemoveAt(0);
|
||||
}
|
||||
entries.Add((password, snapshot, lg2Count, keys));
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ public abstract class RarVolume : Volume
|
||||
private const int MaxCommentSize = 16 * 1024 * 1024;
|
||||
|
||||
private readonly RarHeaderFactory _headerFactory;
|
||||
internal Rar5KeyCache KeyCache => _headerFactory.KeyCache;
|
||||
private int _maxCompressionAlgorithm;
|
||||
|
||||
internal RarVolume(StreamingMode mode, Stream stream, ReaderOptions options, int index)
|
||||
|
||||
@@ -21,6 +21,51 @@ public class CryptKey5Tests : TestBase
|
||||
return info;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rar5_Key_SharesDerivedMaterial_AcrossEntries()
|
||||
{
|
||||
var cache = new Rar5KeyCache();
|
||||
var firstInfo = ReadCryptoInfo();
|
||||
var secondInfo = ReadCryptoInfo();
|
||||
secondInfo.InitV[0] = 1;
|
||||
var first = new CryptKey5("test", firstInfo, cache);
|
||||
var second = new CryptKey5("test", secondInfo, cache);
|
||||
using var firstTransform = first.Transformer(firstInfo.Salt);
|
||||
using var secondTransform = second.Transformer(secondInfo.Salt);
|
||||
Assert.Same(first.HashKey, second.HashKey);
|
||||
Assert.Equal(first.PswCheck, second.PswCheck);
|
||||
var firstBlock = firstTransform.TransformFinalBlock(new byte[16], 0, 16);
|
||||
var secondBlock = secondTransform.TransformFinalBlock(new byte[16], 0, 16);
|
||||
Assert.False(firstBlock.SequenceEqual(secondBlock));
|
||||
using var fresh = new CryptKey5("test", secondInfo).Transformer(secondInfo.Salt);
|
||||
Assert.Equal(fresh.TransformFinalBlock(new byte[16], 0, 16), secondBlock);
|
||||
|
||||
secondInfo.PswCheck[0] ^= 1;
|
||||
Assert.Throws<SharpCompress.Common.CryptographicException>(() =>
|
||||
new CryptKey5("test", secondInfo, cache).Transformer(secondInfo.Salt)
|
||||
);
|
||||
Assert.Throws<SharpCompress.Common.CryptographicException>(() =>
|
||||
new CryptKey5("failed", firstInfo, cache).Transformer(firstInfo.Salt)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rar5_KeyCache_MatchesInputs_AndBoundsRetention()
|
||||
{
|
||||
var cache = new Rar5KeyCache();
|
||||
var salt = new byte[16];
|
||||
var original = cache.GetKeys("test", salt, 0);
|
||||
Assert.Same(original, cache.GetKeys("test", (byte[])salt.Clone(), 0));
|
||||
Assert.NotSame(original, cache.GetKeys("other", salt, 0));
|
||||
Assert.NotSame(original, cache.GetKeys("test", salt, 1));
|
||||
salt[0] = 1;
|
||||
Assert.NotSame(original, cache.GetKeys("test", salt, 0));
|
||||
Assert.Same(original, cache.GetKeys("test", new byte[16], 0));
|
||||
salt[0] = 2;
|
||||
cache.GetKeys("test", salt, 0);
|
||||
Assert.NotSame(original, cache.GetKeys("test", new byte[16], 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rar5_Key_ReusesDerivedMaterial_WithDifferentIVs()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user