From 060f8f848c7156594b356a6fddb2bb480ae7c56d Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Mon, 5 Oct 2026 11:33:15 +0100 Subject: [PATCH] Fix encrypted RAR header state when rescanning archives Reset scan-specific encryption state in sync and async header readers while retaining the KDF cache. Add RAR4/RAR5 rescan and solid encrypted extraction regression coverage. Fixes #1422 --- .../Rar/Headers/RarHeaderFactory.Async.cs | 1 + .../Common/Rar/Headers/RarHeaderFactory.cs | 12 ++ .../Rar/RarEncryptedHeaderScanTests.cs | 140 ++++++++++++++++++ .../Archives/Rar5.solid.encrypted_headers.rar | Bin 0 -> 542 bytes 4 files changed, 153 insertions(+) create mode 100644 tests/SharpCompress.Test/Rar/RarEncryptedHeaderScanTests.cs create mode 100644 tests/TestArchives/Archives/Rar5.solid.encrypted_headers.rar diff --git a/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.Async.cs b/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.Async.cs index 640e2b90..a6108b32 100644 --- a/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.Async.cs +++ b/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.Async.cs @@ -14,6 +14,7 @@ public partial class RarHeaderFactory { public async IAsyncEnumerable ReadHeadersAsync(Stream stream) { + ResetHeaderScanState(); var markHeader = await MarkHeader .ReadAsync( stream, diff --git a/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.cs b/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.cs index 67ee8c69..27ae9db6 100644 --- a/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.cs +++ b/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.cs @@ -39,8 +39,20 @@ public partial class RarHeaderFactory public StreamingMode StreamingMode { get; } public bool IsEncrypted { get; private set; } + private void ResetHeaderScanState() + { + // Metadata probes rewind the stream. Each new scan must rediscover encryption + // from the plaintext archive headers, rather than reuse the previous scan's state. + IsEncrypted = false; + _cryptInfo = null; + _headerKey = null; + _headerPassword = null; + // Keep the password/salt/iteration-keyed KDF cache across scans. + } + public IEnumerable ReadHeaders(Stream stream) { + ResetHeaderScanState(); var markHeader = MarkHeader.Read(stream, Options.LeaveStreamOpen, Options.LookForHeader); _isRar5 = markHeader.IsRar5; yield return markHeader; diff --git a/tests/SharpCompress.Test/Rar/RarEncryptedHeaderScanTests.cs b/tests/SharpCompress.Test/Rar/RarEncryptedHeaderScanTests.cs new file mode 100644 index 00000000..dddb63a2 --- /dev/null +++ b/tests/SharpCompress.Test/Rar/RarEncryptedHeaderScanTests.cs @@ -0,0 +1,140 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Archives.Rar; +using SharpCompress.Common; +using SharpCompress.Common.Rar.Headers; +using SharpCompress.IO; +using SharpCompress.Readers; +using Xunit; + +namespace SharpCompress.Test.Rar; + +public class RarEncryptedHeaderScanTests : TestBase +{ + [Theory] + [InlineData("Rar.encrypted_filesAndHeader.rar", false)] + [InlineData("Rar.encrypted_filesAndHeader.rar", true)] + [InlineData("Rar5.encrypted_filesAndHeader.rar", false)] + [InlineData("Rar5.encrypted_filesAndHeader.rar", true)] + public async Task Rar_EncryptedHeaders_Rescan(string filename, bool useAsync) + { + using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename)); + var factory = new RarHeaderFactory( + StreamingMode.Seekable, + ReaderOptions.ForExternalStream.WithPassword("test") + ); + var expected = await ReadNames(false); + Assert.Equal(6, expected.Count); + stream.Position = 0; + Assert.Single(await ReadNames(true)); + stream.Position = 0; + Assert.Equal(expected, await ReadNames(false)); + + async Task> ReadNames(bool stopAfterFirst) + { + var names = new List(); + if (useAsync) + { + await foreach (var header in factory.ReadHeadersAsync(stream)) + { + if (header is FileHeader file && header.HeaderType == HeaderType.File) + { + Assert.NotNull(file.FileName); + names.Add(file.FileName); + if (stopAfterFirst) + { + break; + } + } + } + } + else + { + foreach (var header in factory.ReadHeaders(stream)) + { + if (header is FileHeader file && header.HeaderType == HeaderType.File) + { + Assert.NotNull(file.FileName); + names.Add(file.FileName); + if (stopAfterFirst) + { + break; + } + } + } + } + return names; + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Rar5_Solid_EncryptedHeaders_ExtractAll(bool readMetadataFirst) + { + using var stream = File.OpenRead( + Path.Combine(TEST_ARCHIVES_PATH, "Rar5.solid.encrypted_headers.rar") + ); + using var archive = RarArchive.OpenArchive( + stream, + ReaderOptions.ForExternalStream.WithPassword("test") + ); + if (readMetadataFirst) + { + Assert.True(archive.IsSolid); + Assert.Equal(3, archive.Entries.Count()); + } + using var reader = archive.ExtractAllEntries(); + var count = 0; + while (reader.MoveToNextEntry()) + { + Assert.Equal($"f{count:D3}.txt", reader.Entry.Key); + using var entryStream = reader.OpenEntryStream(); + using var textReader = new StreamReader(entryStream); + Assert.Equal("Solid encrypted header regression test.\n", textReader.ReadToEnd()); + count++; + } + Assert.Equal(3, count); + Assert.Equal(3, archive.Entries.Count()); + Assert.True(stream.CanRead); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Rar5_Solid_EncryptedHeaders_ExtractAll_Async(bool readMetadataFirst) + { + using var stream = File.OpenRead( + Path.Combine(TEST_ARCHIVES_PATH, "Rar5.solid.encrypted_headers.rar") + ); + await using var archive = await RarArchive.OpenAsyncArchive( + stream, + ReaderOptions.ForExternalStream.WithPassword("test"), + CancellationToken.None + ); + if (readMetadataFirst) + { + Assert.True(await archive.IsSolidAsync()); + Assert.Equal(3, (await archive.EntriesAsync.ToListAsync()).Count); + } + await using var reader = await archive.ExtractAllEntriesAsync(); + var count = 0; + while (await reader.MoveToNextEntryAsync(CancellationToken.None)) + { + Assert.Equal($"f{count:D3}.txt", reader.Entry.Key); + await using var entryStream = await reader.OpenEntryStreamAsync(CancellationToken.None); + using var textReader = new StreamReader(entryStream); + Assert.Equal( + "Solid encrypted header regression test.\n", + await textReader.ReadToEndAsync() + ); + count++; + } + Assert.Equal(3, count); + Assert.Equal(3, (await archive.EntriesAsync.ToListAsync()).Count); + Assert.True(stream.CanRead); + } +} diff --git a/tests/TestArchives/Archives/Rar5.solid.encrypted_headers.rar b/tests/TestArchives/Archives/Rar5.solid.encrypted_headers.rar new file mode 100644 index 0000000000000000000000000000000000000000..a9c035a32efb437b2fe7722045e0440d74419ea5 GIT binary patch literal 542 zcmWGaEK-zWXJq*LbM+BL76t}J{^NZ<6KeeBwa&;Uh$ZjPk+3oo8F@) zEya03cVha5qwZDo+=wjZR(;2slP#!st!D41Yu&XGmOY;&jEgi6dU#*3eIsjCw$OiJ z(`%^;e@o7pH80wjZgzb?wrOTde5BNg+xK6rbN%hU;))^vluOqRZ$7Cln4Bo?pZ7y^ ziG71;d%Zzbt!<`L-}D(5>(*U%epEe6_~t#{M$JR- zRNi6+pNPmP?xsinTosn8vhBNb%3GOImKoo@%^I(IBr{AtJX>k;)t`^-nk-+`&Ncbw zbEj#Ndv2mt^jh=o2cM+_LawRWo_(Tu_R%xp>Ca~N=*^5eZmgBN!+6s6$#H_rQ8Axy z`Yq5EzBeo7*~`N7N6Vy=1lGT8;}d^#{Kv);j;9lae~s;bKb%i-x@r- zz*sRUzHsx&mpXH-iZi;;9N^o{{`5lcy=j#*=6^btQ6Nx$)QO*+b6uitzsA<%r?2lk zdEB?a<@;T(tfV*WhZ>k%j~WGPlr#z4*z|FMNag9yZ>;}|HphIqXeT2Sz4r3ty{vzY zra$<)NW2h4@wCb$<2EA$6sK=P4_pycgG9-aCsu3cZO&3 F1prkN2p9kW literal 0 HcmV?d00001