mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-04-05 21:51:09 +00:00
258 lines
8.0 KiB
C#
258 lines
8.0 KiB
C#
using System;
|
|
using System.Buffers.Binary;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Security.Cryptography;
|
|
using System.Threading.Tasks;
|
|
using SharpCompress.Common.Zip;
|
|
using Xunit;
|
|
|
|
namespace SharpCompress.Test.Streams;
|
|
|
|
public class WinzipAesCryptoStreamTests
|
|
{
|
|
[Fact]
|
|
public void Read_Decrypts_Data_For_Aligned_Buffer_Size()
|
|
{
|
|
const string password = "sample-password";
|
|
byte[] plainText = Enumerable.Range(0, 64).Select(i => (byte)i).ToArray();
|
|
byte[] salt = [0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87];
|
|
using var stream = CreateStream(plainText, password, salt);
|
|
|
|
byte[] actual = new byte[plainText.Length];
|
|
int bytesRead = stream.Read(actual, 0, actual.Length);
|
|
|
|
Assert.Equal(plainText.Length, bytesRead);
|
|
Assert.Equal(plainText, actual);
|
|
}
|
|
|
|
[Fact]
|
|
public void Read_Preserves_Keystream_Between_NonAligned_Reads()
|
|
{
|
|
const string password = "sample-password";
|
|
byte[] plainText = Enumerable.Range(0, 97).Select(i => (byte)i).ToArray();
|
|
byte[] salt = [0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87];
|
|
using var stream = CreateStream(plainText, password, salt);
|
|
|
|
byte[] actual = ReadWithChunkPattern(
|
|
(buffer, offset, count) => stream.Read(buffer, offset, count),
|
|
plainText.Length,
|
|
[13, 5, 29, 7, 43]
|
|
);
|
|
|
|
Assert.Equal(plainText, actual);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadAsync_Preserves_Keystream_Between_NonAligned_Reads()
|
|
{
|
|
const string password = "sample-password";
|
|
byte[] plainText = Enumerable
|
|
.Range(0, 113)
|
|
.Select(i => unchecked((byte)(255 - i)))
|
|
.ToArray();
|
|
byte[] salt = [0x91, 0x82, 0x73, 0x64, 0x55, 0x46, 0x37, 0x28];
|
|
using var stream = CreateStream(plainText, password, salt);
|
|
|
|
byte[] actual = await ReadWithChunkPatternAsync(
|
|
(buffer, offset, count) => stream.ReadAsync(buffer, offset, count),
|
|
plainText.Length,
|
|
[11, 3, 17, 5, 41]
|
|
);
|
|
|
|
Assert.Equal(plainText, actual);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadAsync_Memory_Preserves_Keystream_Between_NonAligned_Reads()
|
|
{
|
|
const string password = "sample-password";
|
|
byte[] plainText = Enumerable
|
|
.Range(0, 113)
|
|
.Select(i => unchecked((byte)(255 - i)))
|
|
.ToArray();
|
|
byte[] salt = [0x91, 0x82, 0x73, 0x64, 0x55, 0x46, 0x37, 0x28];
|
|
using var stream = CreateStream(plainText, password, salt);
|
|
|
|
byte[] actual = await ReadWithChunkPatternMemoryAsync(
|
|
stream,
|
|
plainText.Length,
|
|
[11, 3, 17, 5, 41]
|
|
);
|
|
|
|
Assert.Equal(plainText, actual);
|
|
}
|
|
|
|
[Fact]
|
|
public void Read_Stops_At_Encrypted_Payload_Length()
|
|
{
|
|
const string password = "sample-password";
|
|
byte[] plainText = Enumerable.Range(0, 31).Select(i => (byte)(i * 3)).ToArray();
|
|
byte[] salt = [0xA1, 0xB2, 0xC3, 0xD4, 0x01, 0x12, 0x23, 0x34];
|
|
using var stream = CreateStream(plainText, password, salt);
|
|
|
|
byte[] actual = new byte[plainText.Length + 16];
|
|
int bytesRead = stream.Read(actual, 0, actual.Length);
|
|
int eofRead = stream.Read(actual, bytesRead, actual.Length - bytesRead);
|
|
|
|
Assert.Equal(plainText.Length, bytesRead);
|
|
Assert.Equal(0, eofRead);
|
|
Assert.Equal(plainText, actual.Take(bytesRead).ToArray());
|
|
}
|
|
|
|
private static WinzipAesCryptoStream CreateStream(
|
|
byte[] plainText,
|
|
string password,
|
|
byte[] salt
|
|
)
|
|
{
|
|
var encryptionData = CreateEncryptionData(password, salt);
|
|
byte[] cipherText = EncryptCtr(plainText, encryptionData.KeyBytes);
|
|
byte[] archiveBytes = cipherText.Concat(new byte[10]).ToArray();
|
|
return new WinzipAesCryptoStream(
|
|
new MemoryStream(archiveBytes, writable: false),
|
|
encryptionData,
|
|
cipherText.Length
|
|
);
|
|
}
|
|
|
|
[SuppressMessage(
|
|
"Security",
|
|
"CA5379:Rfc2898DeriveBytes might be using a weak hash algorithm",
|
|
Justification = "WinZip AES interop requires PBKDF2 with SHA-1."
|
|
)]
|
|
private static WinzipAesEncryptionData CreateEncryptionData(string password, byte[] salt)
|
|
{
|
|
#pragma warning disable SYSLIB0060 // Rfc2898DeriveBytes might be using a weak hash algorithm
|
|
using var deriveBytes = new Rfc2898DeriveBytes(
|
|
password,
|
|
salt,
|
|
1000,
|
|
HashAlgorithmName.SHA1
|
|
);
|
|
#pragma warning restore SYSLIB0060
|
|
deriveBytes.GetBytes(16);
|
|
deriveBytes.GetBytes(16);
|
|
byte[] passwordVerifyValue = deriveBytes.GetBytes(2);
|
|
|
|
return new WinzipAesEncryptionData(
|
|
WinzipAesKeySize.KeySize128,
|
|
salt,
|
|
passwordVerifyValue,
|
|
password
|
|
);
|
|
}
|
|
|
|
private static byte[] EncryptCtr(byte[] plainText, byte[] keyBytes)
|
|
{
|
|
using var aes = Aes.Create();
|
|
aes.BlockSize = 128;
|
|
aes.KeySize = keyBytes.Length * 8;
|
|
aes.Mode = CipherMode.ECB;
|
|
aes.Padding = PaddingMode.None;
|
|
|
|
using var encryptor = aes.CreateEncryptor(keyBytes, new byte[16]);
|
|
byte[] counter = new byte[16];
|
|
byte[] counterOut = new byte[16];
|
|
byte[] cipherText = new byte[plainText.Length];
|
|
int nonce = 1;
|
|
int offset = 0;
|
|
|
|
while (offset < plainText.Length)
|
|
{
|
|
BinaryPrimitives.WriteInt32LittleEndian(counter, nonce++);
|
|
encryptor.TransformBlock(counter, 0, counter.Length, counterOut, 0);
|
|
|
|
int blockLength = Math.Min(counterOut.Length, plainText.Length - offset);
|
|
for (int i = 0; i < blockLength; i++)
|
|
{
|
|
cipherText[offset + i] = (byte)(plainText[offset + i] ^ counterOut[i]);
|
|
}
|
|
|
|
offset += blockLength;
|
|
}
|
|
|
|
return cipherText;
|
|
}
|
|
|
|
private static byte[] ReadWithChunkPattern(
|
|
Func<byte[], int, int, int> read,
|
|
int totalLength,
|
|
int[] chunkPattern
|
|
)
|
|
{
|
|
byte[] actual = new byte[totalLength];
|
|
int offset = 0;
|
|
int chunkIndex = 0;
|
|
|
|
while (offset < totalLength)
|
|
{
|
|
int requested = Math.Min(
|
|
chunkPattern[chunkIndex % chunkPattern.Length],
|
|
totalLength - offset
|
|
);
|
|
int bytesRead = read(actual, offset, requested);
|
|
Assert.True(bytesRead > 0);
|
|
offset += bytesRead;
|
|
chunkIndex++;
|
|
}
|
|
|
|
return actual;
|
|
}
|
|
|
|
private static async Task<byte[]> ReadWithChunkPatternAsync(
|
|
Func<byte[], int, int, Task<int>> readAsync,
|
|
int totalLength,
|
|
int[] chunkPattern
|
|
)
|
|
{
|
|
byte[] actual = new byte[totalLength];
|
|
int offset = 0;
|
|
int chunkIndex = 0;
|
|
|
|
while (offset < totalLength)
|
|
{
|
|
int requested = Math.Min(
|
|
chunkPattern[chunkIndex % chunkPattern.Length],
|
|
totalLength - offset
|
|
);
|
|
int bytesRead = await readAsync(actual, offset, requested);
|
|
Assert.True(bytesRead > 0);
|
|
offset += bytesRead;
|
|
chunkIndex++;
|
|
}
|
|
|
|
return actual;
|
|
}
|
|
|
|
private static async Task<byte[]> ReadWithChunkPatternMemoryAsync(
|
|
Stream stream,
|
|
int totalLength,
|
|
int[] chunkPattern
|
|
)
|
|
{
|
|
byte[] actual = new byte[totalLength];
|
|
int offset = 0;
|
|
int chunkIndex = 0;
|
|
|
|
while (offset < totalLength)
|
|
{
|
|
int requested = Math.Min(
|
|
chunkPattern[chunkIndex % chunkPattern.Length],
|
|
totalLength - offset
|
|
);
|
|
#if NET48
|
|
int bytesRead = await stream.ReadAsync(actual, offset, requested);
|
|
#else
|
|
int bytesRead = await stream.ReadAsync(actual.AsMemory(offset, requested));
|
|
#endif
|
|
Assert.True(bytesRead > 0);
|
|
offset += bytesRead;
|
|
chunkIndex++;
|
|
}
|
|
|
|
return actual;
|
|
}
|
|
}
|