Validate 7z AES KDF limits before unsalted key derivation

This commit is contained in:
Adam Hathcock
2026-10-10 15:40:33 +01:00
parent ab3ac18e98
commit 9ba6d17b5d
3 changed files with 51 additions and 8 deletions

View File

@@ -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

View File

@@ -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<byte>();
iv = Array.Empty<byte>();
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)

View File

@@ -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<NotSupportedException>(() =>
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";
}
}