diff --git a/docs/API.md b/docs/API.md index bad17881..bae3d649 100644 --- a/docs/API.md +++ b/docs/API.md @@ -486,6 +486,8 @@ using (var archive = ZipArchive.OpenArchive("file.zip")) `CheckCrc` validates archive-level payload checksums when the format stores reliable metadata, such as ZIP CRC32 values. Formats without payload checksums skip this validation. Decompressor integrity checks that are required to decode a stream may still fail even when `CheckCrc` is disabled. +7z AES key derivation accepts iteration exponents 0 through 24 and the special `0x3F` direct-key mode. Larger iteration exponents throw `NotSupportedException` before key derivation, including when salt and IV are absent. + When using `SymbolicLinkHandler`, directory extraction rejects link targets outside the extraction root and never follows symbolic links or reparse points while extracting later entries. The handler itself remains trusted application code. ### Options matrix diff --git a/src/SharpCompress/Compressors/LZMA/AesDecoderStream.cs b/src/SharpCompress/Compressors/LZMA/AesDecoderStream.cs index 52793292..72f05db6 100644 --- a/src/SharpCompress/Compressors/LZMA/AesDecoderStream.cs +++ b/src/SharpCompress/Compressors/LZMA/AesDecoderStream.cs @@ -24,7 +24,7 @@ internal sealed partial class AesDecoderStream : DecoderStream2 public AesDecoderStream(Stream input, byte[] info, IPasswordProvider pass, long limit) { var password = pass.CryptoGetTextPassword(); - if (password == null) + if (password is null) { throw new SharpCompress.Common.CryptographicException( "Encrypted 7Zip archive has no password specified." @@ -43,7 +43,7 @@ internal sealed partial class AesDecoderStream : DecoderStream2 var passwordBytes = Encoding.Unicode.GetBytes(password); var key = InitKey(numCyclesPower, salt, passwordBytes); - if (key == null) + if (key is null) { throw new ArchiveOperationException("Initialized with null key"); } @@ -147,13 +147,24 @@ internal sealed partial class AesDecoderStream : DecoderStream2 private void Init(byte[] info, out int numCyclesPower, out byte[] salt, out byte[] iv) { + if (info.Length == 0) + { + throw new InvalidFormatException("Missing 7z AES properties."); + } var bt = info[0]; numCyclesPower = bt & 0x3F; + // Check before the no-salt/no-IV return, and before starting expensive key derivation. + // 0x3F is the format's direct-key mode, not an iteration exponent. + if (numCyclesPower > 24 && numCyclesPower != 0x3F) + { + throw new NotSupportedException("Unsupported 7z AES KDF count."); + } + if ((bt & 0xC0) == 0) { salt = Array.Empty(); - iv = Array.Empty(); + iv = new byte[16]; return; } @@ -183,11 +194,6 @@ internal sealed partial class AesDecoderStream : DecoderStream2 { iv[i] = info[i + saltSize + 2]; } - - if (numCyclesPower > 24) - { - throw new NotSupportedException(); - } } private byte[]? InitKey(int mNumCyclesPower, byte[] salt, byte[] pass) diff --git a/tests/SharpCompress.Test/SevenZip/SevenZipAesKdfTests.cs b/tests/SharpCompress.Test/SevenZip/SevenZipAesKdfTests.cs new file mode 100644 index 00000000..c7dcae90 --- /dev/null +++ b/tests/SharpCompress.Test/SevenZip/SevenZipAesKdfTests.cs @@ -0,0 +1,35 @@ +using System; +using System.IO; +using SharpCompress.Compressors.LZMA; +using SharpCompress.Compressors.LZMA.Utilities; +using Xunit; + +namespace SharpCompress.Test.SevenZip; + +public class SevenZipAesKdfTests +{ + [Theory] + [InlineData(25)] + [InlineData(31)] + [InlineData(62)] + public void SevenZipAes_Rejects_Excessive_Kdf_Without_Salt(int exponent) + { + using var input = new MemoryStream(); + Assert.Throws(() => + new AesDecoderStream(input, [(byte)exponent], new Password(), 0) + ); + } + + [Fact] + public void SevenZipAes_Allows_Direct_Key_Without_Iv() + { + using var input = new MemoryStream(); + using var decoder = new AesDecoderStream(input, [0x3F], new Password(), 0); + Assert.Equal(0, decoder.Read(new byte[1], 0, 1)); + } + + private sealed class Password : IPasswordProvider + { + public string CryptoGetTextPassword() => "test"; + } +}