mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-02-08 13:34:57 +00:00
Compare commits
18 Commits
release
...
copilot/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b6831b3f2 | ||
|
|
c64cc98ca7 | ||
|
|
4910d6b567 | ||
|
|
68279e0a2b | ||
|
|
0d6b0aa1ab | ||
|
|
a7d6d6493e | ||
|
|
b6cc95af73 | ||
|
|
90d91cc7c2 | ||
|
|
ec83cf588f | ||
|
|
66e9de2685 | ||
|
|
321520408b | ||
|
|
68451bd75f | ||
|
|
486fdf118b | ||
|
|
bd3cda0617 | ||
|
|
725503d1ce | ||
|
|
42bbfa08a9 | ||
|
|
6efb8be03a | ||
|
|
94b4100a88 |
@@ -46,7 +46,7 @@ namespace SharpCompress.Common.Ace.Headers
|
||||
}
|
||||
}
|
||||
|
||||
public AceFileHeader(ArchiveEncoding archiveEncoding)
|
||||
public AceFileHeader(IArchiveEncoding archiveEncoding)
|
||||
: base(archiveEncoding, AceHeaderType.FILE) { }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -31,13 +31,13 @@ namespace SharpCompress.Common.Ace.Headers
|
||||
(byte)'*',
|
||||
];
|
||||
|
||||
public AceHeader(ArchiveEncoding archiveEncoding, AceHeaderType type)
|
||||
public AceHeader(IArchiveEncoding archiveEncoding, AceHeaderType type)
|
||||
{
|
||||
AceHeaderType = type;
|
||||
ArchiveEncoding = archiveEncoding;
|
||||
}
|
||||
|
||||
public ArchiveEncoding ArchiveEncoding { get; }
|
||||
public IArchiveEncoding ArchiveEncoding { get; }
|
||||
public AceHeaderType AceHeaderType { get; }
|
||||
|
||||
public ushort HeaderFlags { get; set; }
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace SharpCompress.Common.Ace.Headers
|
||||
public List<byte> Comment { get; set; } = new();
|
||||
public byte AceVersion { get; private set; }
|
||||
|
||||
public AceMainHeader(ArchiveEncoding archiveEncoding)
|
||||
public AceMainHeader(IArchiveEncoding archiveEncoding)
|
||||
: base(archiveEncoding, AceHeaderType.MAIN) { }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace SharpCompress.Common.Arc
|
||||
{
|
||||
public class ArcEntryHeader
|
||||
{
|
||||
public ArchiveEncoding ArchiveEncoding { get; }
|
||||
public IArchiveEncoding ArchiveEncoding { get; }
|
||||
public CompressionType CompressionMethod { get; private set; }
|
||||
public string? Name { get; private set; }
|
||||
public long CompressedSize { get; private set; }
|
||||
@@ -16,7 +16,7 @@ namespace SharpCompress.Common.Arc
|
||||
public long OriginalSize { get; private set; }
|
||||
public long DataStartPosition { get; private set; }
|
||||
|
||||
public ArcEntryHeader(ArchiveEncoding archiveEncoding)
|
||||
public ArcEntryHeader(IArchiveEncoding archiveEncoding)
|
||||
{
|
||||
this.ArchiveEncoding = archiveEncoding;
|
||||
}
|
||||
|
||||
@@ -3,55 +3,11 @@ using System.Text;
|
||||
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
public class ArchiveEncoding
|
||||
public class ArchiveEncoding : IArchiveEncoding
|
||||
{
|
||||
/// <summary>
|
||||
/// Default encoding to use when archive format doesn't specify one.
|
||||
/// </summary>
|
||||
public Encoding? Default { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ArchiveEncoding used by encryption schemes which don't comply with RFC 2898.
|
||||
/// </summary>
|
||||
public Encoding? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set this encoding when you want to force it for all encoding operations.
|
||||
/// </summary>
|
||||
public Encoding Default { get; set; } = Encoding.Default;
|
||||
public Encoding Password { get; set; } = Encoding.Default;
|
||||
public Encoding UTF8 { get; set; } = Encoding.UTF8;
|
||||
public Encoding? Forced { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set this when you want to use a custom method for all decoding operations.
|
||||
/// </summary>
|
||||
/// <returns>string Func(bytes, index, length)</returns>
|
||||
public Func<byte[], int, int, string>? CustomDecoder { get; set; }
|
||||
|
||||
public ArchiveEncoding()
|
||||
: this(Encoding.Default, Encoding.Default) { }
|
||||
|
||||
public ArchiveEncoding(Encoding def, Encoding password)
|
||||
{
|
||||
Default = def;
|
||||
Password = password;
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK
|
||||
static ArchiveEncoding() => Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
#endif
|
||||
|
||||
public string Decode(byte[] bytes) => Decode(bytes, 0, bytes.Length);
|
||||
|
||||
public string Decode(byte[] bytes, int start, int length) =>
|
||||
GetDecoder().Invoke(bytes, start, length);
|
||||
|
||||
public string DecodeUTF8(byte[] bytes) => Encoding.UTF8.GetString(bytes, 0, bytes.Length);
|
||||
|
||||
public byte[] Encode(string str) => GetEncoding().GetBytes(str);
|
||||
|
||||
public Encoding GetEncoding() => Forced ?? Default ?? Encoding.UTF8;
|
||||
|
||||
public Encoding GetPasswordEncoding() => Password ?? Encoding.UTF8;
|
||||
|
||||
public Func<byte[], int, int, string> GetDecoder() =>
|
||||
CustomDecoder ?? ((bytes, index, count) => GetEncoding().GetString(bytes, index, count));
|
||||
public Func<byte[], int, int, EncodingType, string>? CustomDecoder { get; set; }
|
||||
}
|
||||
|
||||
87
src/SharpCompress/Common/ArchiveEncodingExtensions.cs
Normal file
87
src/SharpCompress/Common/ArchiveEncodingExtensions.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of encoding to use.
|
||||
/// </summary>
|
||||
public enum EncodingType
|
||||
{
|
||||
/// <summary>
|
||||
/// Uses the default encoding.
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// Uses UTF-8 encoding.
|
||||
/// </summary>
|
||||
UTF8,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for archive encoding.
|
||||
/// </summary>
|
||||
public static class ArchiveEncodingExtensions
|
||||
{
|
||||
#if !NETFRAMEWORK
|
||||
/// <summary>
|
||||
/// Registers the code pages encoding provider.
|
||||
/// </summary>
|
||||
static ArchiveEncodingExtensions() =>
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
#endif
|
||||
|
||||
extension(IArchiveEncoding encoding)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the encoding based on the archive encoding settings.
|
||||
/// </summary>
|
||||
/// <param name="useUtf8">Whether to use UTF-8.</param>
|
||||
/// <returns>The encoding.</returns>
|
||||
public Encoding GetEncoding(bool useUtf8 = false) =>
|
||||
encoding.Forced ?? (useUtf8 ? encoding.UTF8 : encoding.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the decoder function for the archive encoding.
|
||||
/// </summary>
|
||||
/// <returns>The decoder function.</returns>
|
||||
public Func<byte[], int, int, EncodingType, string> GetDecoder() =>
|
||||
encoding.CustomDecoder
|
||||
?? (
|
||||
(bytes, index, count, type) =>
|
||||
encoding.GetEncoding(type == EncodingType.UTF8).GetString(bytes, index, count)
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a string using the default encoding.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to encode.</param>
|
||||
/// <returns>The encoded bytes.</returns>
|
||||
public byte[] Encode(string str) => encoding.Default.GetBytes(str);
|
||||
|
||||
/// <summary>
|
||||
/// Decodes bytes using the specified encoding type.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes to decode.</param>
|
||||
/// <param name="type">The encoding type.</param>
|
||||
/// <returns>The decoded string.</returns>
|
||||
public string Decode(byte[] bytes, EncodingType type = EncodingType.Default) =>
|
||||
encoding.Decode(bytes, 0, bytes.Length, type);
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a portion of bytes using the specified encoding type.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes to decode.</param>
|
||||
/// <param name="start">The start index.</param>
|
||||
/// <param name="length">The length.</param>
|
||||
/// <param name="type">The encoding type.</param>
|
||||
/// <returns>The decoded string.</returns>
|
||||
public string Decode(
|
||||
byte[] bytes,
|
||||
int start,
|
||||
int length,
|
||||
EncodingType type = EncodingType.Default
|
||||
) => encoding.GetDecoder()(bytes, start, length, type);
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@ namespace SharpCompress.Common;
|
||||
|
||||
public abstract class FilePart
|
||||
{
|
||||
protected FilePart(ArchiveEncoding archiveEncoding) => ArchiveEncoding = archiveEncoding;
|
||||
protected FilePart(IArchiveEncoding archiveEncoding) => ArchiveEncoding = archiveEncoding;
|
||||
|
||||
internal ArchiveEncoding ArchiveEncoding { get; }
|
||||
internal IArchiveEncoding ArchiveEncoding { get; }
|
||||
|
||||
internal abstract string? FilePartName { get; }
|
||||
public int Index { get; set; }
|
||||
|
||||
@@ -13,7 +13,7 @@ internal sealed class GZipFilePart : FilePart
|
||||
private string? _name;
|
||||
private readonly Stream _stream;
|
||||
|
||||
internal GZipFilePart(Stream stream, ArchiveEncoding archiveEncoding)
|
||||
internal GZipFilePart(Stream stream, IArchiveEncoding archiveEncoding)
|
||||
: base(archiveEncoding)
|
||||
{
|
||||
_stream = stream;
|
||||
|
||||
36
src/SharpCompress/Common/IArchiveEncoding.cs
Normal file
36
src/SharpCompress/Common/IArchiveEncoding.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the encoding settings for archives.
|
||||
/// </summary>
|
||||
public interface IArchiveEncoding
|
||||
{
|
||||
/// <summary>
|
||||
/// Default encoding to use when archive format doesn't specify one. Required and defaults to Encoding.Default.
|
||||
/// </summary>
|
||||
public Encoding Default { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ArchiveEncoding used by encryption schemes which don't comply with RFC 2898. Required and defaults to Encoding.Default.
|
||||
/// </summary>
|
||||
public Encoding Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default encoding to use when archive format specifies UTF-8 encoding. Required and defaults to Encoding.UTF8.
|
||||
/// </summary>
|
||||
public Encoding UTF8 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set this encoding when you want to force it for all encoding operations.
|
||||
/// </summary>
|
||||
public Encoding? Forced { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set this when you want to use a custom method for all decoding operations.
|
||||
/// </summary>
|
||||
/// <returns>string Func(bytes, index, length, EncodingType)</returns>
|
||||
public Func<byte[], int, int, EncodingType, string>? CustomDecoder { get; set; }
|
||||
}
|
||||
@@ -7,5 +7,5 @@ public class OptionsBase
|
||||
/// </summary>
|
||||
public bool LeaveStreamOpen { get; set; } = true;
|
||||
|
||||
public ArchiveEncoding ArchiveEncoding { get; set; } = new();
|
||||
public IArchiveEncoding ArchiveEncoding { get; set; } = new ArchiveEncoding();
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ internal class RarHeader : IRarHeader
|
||||
internal static RarHeader? TryReadBase(
|
||||
RarCrcBinaryReader reader,
|
||||
bool isRar5,
|
||||
ArchiveEncoding archiveEncoding
|
||||
IArchiveEncoding archiveEncoding
|
||||
)
|
||||
{
|
||||
try
|
||||
@@ -26,7 +26,7 @@ internal class RarHeader : IRarHeader
|
||||
}
|
||||
}
|
||||
|
||||
private RarHeader(RarCrcBinaryReader reader, bool isRar5, ArchiveEncoding archiveEncoding)
|
||||
private RarHeader(RarCrcBinaryReader reader, bool isRar5, IArchiveEncoding archiveEncoding)
|
||||
{
|
||||
_headerType = HeaderType.Null;
|
||||
_isRar5 = isRar5;
|
||||
@@ -115,7 +115,7 @@ internal class RarHeader : IRarHeader
|
||||
|
||||
protected int HeaderSize { get; }
|
||||
|
||||
internal ArchiveEncoding ArchiveEncoding { get; }
|
||||
internal IArchiveEncoding ArchiveEncoding { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Extra header size.
|
||||
|
||||
@@ -15,7 +15,7 @@ internal class SevenZipFilePart : FilePart
|
||||
ArchiveDatabase database,
|
||||
int index,
|
||||
CFileItem fileEntry,
|
||||
ArchiveEncoding archiveEncoding
|
||||
IArchiveEncoding archiveEncoding
|
||||
)
|
||||
: base(archiveEncoding)
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ internal sealed class TarHeader
|
||||
internal static readonly DateTime EPOCH = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
public TarHeader(
|
||||
ArchiveEncoding archiveEncoding,
|
||||
IArchiveEncoding archiveEncoding,
|
||||
TarHeaderWriteFormat writeFormat = TarHeaderWriteFormat.GNU_TAR_LONG_LINK
|
||||
)
|
||||
{
|
||||
@@ -30,7 +30,7 @@ internal sealed class TarHeader
|
||||
internal DateTime LastModifiedTime { get; set; }
|
||||
internal EntryType EntryType { get; set; }
|
||||
internal Stream? PackedStream { get; set; }
|
||||
internal ArchiveEncoding ArchiveEncoding { get; }
|
||||
internal IArchiveEncoding ArchiveEncoding { get; }
|
||||
|
||||
internal const int BLOCK_SIZE = 512;
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ public class TarEntry : Entry
|
||||
StreamingMode mode,
|
||||
Stream stream,
|
||||
CompressionType compressionType,
|
||||
ArchiveEncoding archiveEncoding
|
||||
IArchiveEncoding archiveEncoding
|
||||
)
|
||||
{
|
||||
foreach (var header in TarHeaderFactory.ReadHeader(mode, stream, archiveEncoding))
|
||||
|
||||
@@ -10,7 +10,7 @@ internal static class TarHeaderFactory
|
||||
internal static IEnumerable<TarHeader?> ReadHeader(
|
||||
StreamingMode mode,
|
||||
Stream stream,
|
||||
ArchiveEncoding archiveEncoding
|
||||
IArchiveEncoding archiveEncoding
|
||||
)
|
||||
{
|
||||
while (true)
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace SharpCompress.Common.Zip.Headers;
|
||||
|
||||
internal class DirectoryEntryHeader : ZipFileEntry
|
||||
{
|
||||
public DirectoryEntryHeader(ArchiveEncoding archiveEncoding)
|
||||
public DirectoryEntryHeader(IArchiveEncoding archiveEncoding)
|
||||
: base(ZipHeaderType.DirectoryEntry, archiveEncoding) { }
|
||||
|
||||
internal override void Read(BinaryReader reader)
|
||||
@@ -41,8 +41,8 @@ internal class DirectoryEntryHeader : ZipFileEntry
|
||||
|
||||
if (Flags.HasFlag(HeaderFlags.Efs))
|
||||
{
|
||||
Name = ArchiveEncoding.DecodeUTF8(name);
|
||||
Comment = ArchiveEncoding.DecodeUTF8(comment);
|
||||
Name = ArchiveEncoding.Decode(name, EncodingType.UTF8);
|
||||
Comment = ArchiveEncoding.Decode(comment, EncodingType.UTF8);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace SharpCompress.Common.Zip.Headers;
|
||||
|
||||
internal class LocalEntryHeader : ZipFileEntry
|
||||
{
|
||||
public LocalEntryHeader(ArchiveEncoding archiveEncoding)
|
||||
public LocalEntryHeader(IArchiveEncoding archiveEncoding)
|
||||
: base(ZipHeaderType.LocalEntry, archiveEncoding) { }
|
||||
|
||||
internal override void Read(BinaryReader reader)
|
||||
@@ -33,7 +33,7 @@ internal class LocalEntryHeader : ZipFileEntry
|
||||
|
||||
if (Flags.HasFlag(HeaderFlags.Efs))
|
||||
{
|
||||
Name = ArchiveEncoding.DecodeUTF8(name);
|
||||
Name = ArchiveEncoding.Decode(name, EncodingType.UTF8);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace SharpCompress.Common.Zip.Headers;
|
||||
|
||||
internal abstract class ZipFileEntry : ZipHeader
|
||||
{
|
||||
protected ZipFileEntry(ZipHeaderType type, ArchiveEncoding archiveEncoding)
|
||||
protected ZipFileEntry(ZipHeaderType type, IArchiveEncoding archiveEncoding)
|
||||
: base(type)
|
||||
{
|
||||
Extra = new List<ExtraData>();
|
||||
@@ -30,7 +30,7 @@ internal abstract class ZipFileEntry : ZipHeader
|
||||
|
||||
internal Stream? PackedStream { get; set; }
|
||||
|
||||
internal ArchiveEncoding ArchiveEncoding { get; }
|
||||
internal IArchiveEncoding ArchiveEncoding { get; }
|
||||
|
||||
internal string? Name { get; set; }
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using SharpCompress.Common.Zip.Headers;
|
||||
using SharpCompress.Compressors.Deflate;
|
||||
|
||||
@@ -8,9 +9,9 @@ internal class PkwareTraditionalEncryptionData
|
||||
{
|
||||
private static readonly CRC32 CRC32 = new();
|
||||
private readonly uint[] _keys = { 0x12345678, 0x23456789, 0x34567890 };
|
||||
private readonly ArchiveEncoding _archiveEncoding;
|
||||
private readonly IArchiveEncoding _archiveEncoding;
|
||||
|
||||
private PkwareTraditionalEncryptionData(string password, ArchiveEncoding archiveEncoding)
|
||||
private PkwareTraditionalEncryptionData(string password, IArchiveEncoding archiveEncoding)
|
||||
{
|
||||
_archiveEncoding = archiveEncoding;
|
||||
Initialize(password);
|
||||
@@ -47,6 +48,44 @@ internal class PkwareTraditionalEncryptionData
|
||||
return encryptor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new PkwareTraditionalEncryptionData instance for writing encrypted data.
|
||||
/// </summary>
|
||||
/// <param name="password">The password to use for encryption.</param>
|
||||
/// <param name="archiveEncoding">The archive encoding.</param>
|
||||
/// <returns>A new encryption data instance.</returns>
|
||||
public static PkwareTraditionalEncryptionData ForWrite(
|
||||
string password,
|
||||
IArchiveEncoding archiveEncoding
|
||||
)
|
||||
{
|
||||
return new PkwareTraditionalEncryptionData(password, archiveEncoding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the 12-byte encryption header required for PKWARE traditional encryption.
|
||||
/// </summary>
|
||||
/// <param name="crc">The CRC32 of the uncompressed file data, or the last modified time high byte if using data descriptors.</param>
|
||||
/// <param name="lastModifiedTime">The last modified time (used as verification byte when CRC is unknown).</param>
|
||||
/// <returns>The encrypted 12-byte header.</returns>
|
||||
public byte[] GenerateEncryptionHeader(uint crc, ushort lastModifiedTime)
|
||||
{
|
||||
var header = new byte[12];
|
||||
|
||||
// Fill first 11 bytes with random data
|
||||
using (var rng = RandomNumberGenerator.Create())
|
||||
{
|
||||
rng.GetBytes(header, 0, 11);
|
||||
}
|
||||
|
||||
// The last byte is the verification byte - high byte of CRC, or high byte of lastModifiedTime
|
||||
// When streaming (UsePostDataDescriptor), we use the time as verification
|
||||
header[11] = (byte)((crc >> 24) & 0xff);
|
||||
|
||||
// Encrypt the header
|
||||
return Encrypt(header, header.Length);
|
||||
}
|
||||
|
||||
public byte[] Decrypt(byte[] cipherText, int length)
|
||||
{
|
||||
if (length > cipherText.Length)
|
||||
@@ -103,7 +142,7 @@ internal class PkwareTraditionalEncryptionData
|
||||
|
||||
internal byte[] StringToByteArray(string value)
|
||||
{
|
||||
var a = _archiveEncoding.GetPasswordEncoding().GetBytes(value);
|
||||
var a = _archiveEncoding.Password.GetBytes(value);
|
||||
return a;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory
|
||||
private const int MAX_SEARCH_LENGTH_FOR_EOCD = 65557;
|
||||
private bool _zip64;
|
||||
|
||||
internal SeekableZipHeaderFactory(string? password, ArchiveEncoding archiveEncoding)
|
||||
internal SeekableZipHeaderFactory(string? password, IArchiveEncoding archiveEncoding)
|
||||
: base(StreamingMode.Seekable, password, archiveEncoding) { }
|
||||
|
||||
internal IEnumerable<ZipHeader> ReadSeekableHeader(Stream stream)
|
||||
|
||||
@@ -13,7 +13,7 @@ internal class StreamingZipHeaderFactory : ZipHeaderFactory
|
||||
|
||||
internal StreamingZipHeaderFactory(
|
||||
string? password,
|
||||
ArchiveEncoding archiveEncoding,
|
||||
IArchiveEncoding archiveEncoding,
|
||||
IEnumerable<ZipEntry>? entries
|
||||
)
|
||||
: base(StreamingMode.Streaming, password, archiveEncoding) => _entries = entries;
|
||||
|
||||
@@ -20,7 +20,7 @@ internal class WinzipAesEncryptionData
|
||||
{
|
||||
_keySize = keySize;
|
||||
|
||||
#if NETFRAMEWORK
|
||||
#if NETFRAMEWORK || NETSTANDARD2_0
|
||||
var rfc2898 = new Rfc2898DeriveBytes(password, salt, RFC2898_ITERATIONS);
|
||||
KeyBytes = rfc2898.GetBytes(KeySizeInBytes);
|
||||
IvBytes = rfc2898.GetBytes(KeySizeInBytes);
|
||||
|
||||
205
src/SharpCompress/Common/Zip/WinzipAesEncryptionStream.cs
Normal file
205
src/SharpCompress/Common/Zip/WinzipAesEncryptionStream.cs
Normal file
@@ -0,0 +1,205 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Common.Zip;
|
||||
|
||||
/// <summary>
|
||||
/// Stream that encrypts data using WinZip AES encryption and writes to an underlying stream.
|
||||
/// </summary>
|
||||
internal class WinzipAesEncryptionStream : Stream
|
||||
{
|
||||
private const int BLOCK_SIZE_IN_BYTES = 16;
|
||||
private const int RFC2898_ITERATIONS = 1000;
|
||||
private const int AUTH_CODE_LENGTH = 10;
|
||||
|
||||
private readonly Stream _stream;
|
||||
private readonly SymmetricAlgorithm _cipher;
|
||||
private readonly ICryptoTransform _transform;
|
||||
private readonly HMACSHA1 _hmac;
|
||||
private readonly byte[] _counter = new byte[BLOCK_SIZE_IN_BYTES];
|
||||
private readonly byte[] _counterOut = new byte[BLOCK_SIZE_IN_BYTES];
|
||||
private int _nonce = 1;
|
||||
private bool _isDisposed;
|
||||
|
||||
internal WinzipAesEncryptionStream(Stream stream, string password, WinzipAesKeySize keySize)
|
||||
{
|
||||
_stream = stream;
|
||||
|
||||
// Generate salt
|
||||
var saltLength = GetSaltLength(keySize);
|
||||
var salt = new byte[saltLength];
|
||||
using (var rng = RandomNumberGenerator.Create())
|
||||
{
|
||||
rng.GetBytes(salt);
|
||||
}
|
||||
|
||||
// Derive keys using PBKDF2
|
||||
var keyLength = GetKeyLength(keySize);
|
||||
|
||||
#if NETFRAMEWORK || NETSTANDARD2_0
|
||||
var rfc2898 = new Rfc2898DeriveBytes(password, salt, RFC2898_ITERATIONS);
|
||||
var keyBytes = rfc2898.GetBytes(keyLength);
|
||||
var ivBytes = rfc2898.GetBytes(keyLength);
|
||||
var passwordVerifyValue = rfc2898.GetBytes(2);
|
||||
#elif NET10_0_OR_GREATER
|
||||
var derivedKeySize = (keyLength * 2) + 2;
|
||||
var passwordBytes = Encoding.UTF8.GetBytes(password);
|
||||
var derivedKey = Rfc2898DeriveBytes.Pbkdf2(
|
||||
passwordBytes,
|
||||
salt,
|
||||
RFC2898_ITERATIONS,
|
||||
HashAlgorithmName.SHA1,
|
||||
derivedKeySize
|
||||
);
|
||||
var keyBytes = derivedKey.AsSpan(0, keyLength).ToArray();
|
||||
var ivBytes = derivedKey.AsSpan(keyLength, keyLength).ToArray();
|
||||
var passwordVerifyValue = derivedKey.AsSpan(keyLength * 2, 2).ToArray();
|
||||
#else
|
||||
var rfc2898 = new Rfc2898DeriveBytes(
|
||||
password,
|
||||
salt,
|
||||
RFC2898_ITERATIONS,
|
||||
HashAlgorithmName.SHA1
|
||||
);
|
||||
var keyBytes = rfc2898.GetBytes(keyLength);
|
||||
var ivBytes = rfc2898.GetBytes(keyLength);
|
||||
var passwordVerifyValue = rfc2898.GetBytes(2);
|
||||
#endif
|
||||
|
||||
// Initialize cipher
|
||||
_cipher = CreateCipher(keyBytes);
|
||||
var iv = new byte[BLOCK_SIZE_IN_BYTES];
|
||||
_transform = _cipher.CreateEncryptor(keyBytes, iv);
|
||||
|
||||
// Initialize HMAC for authentication
|
||||
_hmac = new HMACSHA1(ivBytes);
|
||||
|
||||
// Write salt and password verification value
|
||||
_stream.Write(salt, 0, salt.Length);
|
||||
_stream.Write(passwordVerifyValue, 0, passwordVerifyValue.Length);
|
||||
}
|
||||
|
||||
private static int GetSaltLength(WinzipAesKeySize keySize) =>
|
||||
keySize switch
|
||||
{
|
||||
WinzipAesKeySize.KeySize128 => 8,
|
||||
WinzipAesKeySize.KeySize192 => 12,
|
||||
WinzipAesKeySize.KeySize256 => 16,
|
||||
_ => throw new InvalidOperationException(),
|
||||
};
|
||||
|
||||
private static int GetKeyLength(WinzipAesKeySize keySize) =>
|
||||
keySize switch
|
||||
{
|
||||
WinzipAesKeySize.KeySize128 => 16,
|
||||
WinzipAesKeySize.KeySize192 => 24,
|
||||
WinzipAesKeySize.KeySize256 => 32,
|
||||
_ => throw new InvalidOperationException(),
|
||||
};
|
||||
|
||||
private static SymmetricAlgorithm CreateCipher(byte[] keyBytes)
|
||||
{
|
||||
var cipher = Aes.Create();
|
||||
cipher.BlockSize = BLOCK_SIZE_IN_BYTES * 8;
|
||||
cipher.KeySize = keyBytes.Length * 8;
|
||||
cipher.Mode = CipherMode.ECB;
|
||||
cipher.Padding = PaddingMode.None;
|
||||
return cipher;
|
||||
}
|
||||
|
||||
public override bool CanRead => false;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override long Length => throw new NotSupportedException();
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => throw new NotSupportedException();
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Flush() => _stream.Flush();
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var encrypted = EncryptData(buffer, offset, count);
|
||||
_hmac.TransformBlock(encrypted, 0, encrypted.Length, encrypted, 0);
|
||||
_stream.Write(encrypted, 0, encrypted.Length);
|
||||
}
|
||||
|
||||
private byte[] EncryptData(byte[] buffer, int offset, int count)
|
||||
{
|
||||
var result = new byte[count];
|
||||
var posn = 0;
|
||||
|
||||
while (posn < count)
|
||||
{
|
||||
var blockSize = Math.Min(BLOCK_SIZE_IN_BYTES, count - posn);
|
||||
|
||||
// Update counter
|
||||
BinaryPrimitives.WriteInt32LittleEndian(_counter, _nonce++);
|
||||
|
||||
// Encrypt counter to get key stream
|
||||
_transform.TransformBlock(_counter, 0, BLOCK_SIZE_IN_BYTES, _counterOut, 0);
|
||||
|
||||
// XOR with plaintext
|
||||
for (var i = 0; i < blockSize; i++)
|
||||
{
|
||||
result[posn + i] = (byte)(_counterOut[i] ^ buffer[offset + posn + i]);
|
||||
}
|
||||
|
||||
posn += blockSize;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_isDisposed = true;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
// Finalize HMAC and write authentication code
|
||||
_hmac.TransformFinalBlock([], 0, 0);
|
||||
var authCode = _hmac.Hash!;
|
||||
_stream.Write(authCode, 0, AUTH_CODE_LENGTH);
|
||||
|
||||
_transform.Dispose();
|
||||
_cipher.Dispose();
|
||||
_hmac.Dispose();
|
||||
_stream.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the overhead bytes added by encryption (salt + password verification + auth code).
|
||||
/// </summary>
|
||||
internal static int GetEncryptionOverhead(WinzipAesKeySize keySize) =>
|
||||
GetSaltLength(keySize) + 2 + AUTH_CODE_LENGTH;
|
||||
}
|
||||
30
src/SharpCompress/Common/Zip/ZipEncryptionType.cs
Normal file
30
src/SharpCompress/Common/Zip/ZipEncryptionType.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
namespace SharpCompress.Common.Zip;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the encryption method to use when creating encrypted ZIP archives.
|
||||
/// </summary>
|
||||
public enum ZipEncryptionType
|
||||
{
|
||||
/// <summary>
|
||||
/// No encryption.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// PKWARE Traditional (ZipCrypto) encryption.
|
||||
/// This is the older, less secure encryption method but is widely compatible.
|
||||
/// </summary>
|
||||
PkwareTraditional = 1,
|
||||
|
||||
/// <summary>
|
||||
/// WinZip AES-256 encryption.
|
||||
/// This is the more secure encryption method using AES-256.
|
||||
/// </summary>
|
||||
Aes256 = 2,
|
||||
|
||||
/// <summary>
|
||||
/// WinZip AES-128 encryption.
|
||||
/// This uses AES-128 for encryption.
|
||||
/// </summary>
|
||||
Aes128 = 3,
|
||||
}
|
||||
@@ -21,12 +21,12 @@ internal class ZipHeaderFactory
|
||||
protected LocalEntryHeader? _lastEntryHeader;
|
||||
private readonly string? _password;
|
||||
private readonly StreamingMode _mode;
|
||||
private readonly ArchiveEncoding _archiveEncoding;
|
||||
private readonly IArchiveEncoding _archiveEncoding;
|
||||
|
||||
protected ZipHeaderFactory(
|
||||
StreamingMode mode,
|
||||
string? password,
|
||||
ArchiveEncoding archiveEncoding
|
||||
IArchiveEncoding archiveEncoding
|
||||
)
|
||||
{
|
||||
_mode = mode;
|
||||
@@ -157,8 +157,8 @@ internal class ZipHeaderFactory
|
||||
|
||||
var salt = new byte[WinzipAesEncryptionData.KeyLengthInBytes(keySize) / 2];
|
||||
var passwordVerifyValue = new byte[2];
|
||||
stream.Read(salt, 0, salt.Length);
|
||||
stream.Read(passwordVerifyValue, 0, 2);
|
||||
stream.ReadFully(salt);
|
||||
stream.ReadFully(passwordVerifyValue);
|
||||
entryHeader.WinzipAesEncryptionData = new WinzipAesEncryptionData(
|
||||
keySize,
|
||||
salt,
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace SharpCompress.Readers.Ace
|
||||
/// </remarks>
|
||||
public abstract class AceReader : AbstractReader<AceEntry, AceVolume>
|
||||
{
|
||||
private readonly ArchiveEncoding _archiveEncoding;
|
||||
private readonly IArchiveEncoding _archiveEncoding;
|
||||
|
||||
internal AceReader(ReaderOptions options)
|
||||
: base(options, ArchiveType.Ace)
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<PropertyGroup>
|
||||
<AssemblyTitle>SharpCompress - Pure C# Decompression/Compression</AssemblyTitle>
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<VersionPrefix>0.42.0</VersionPrefix>
|
||||
<AssemblyVersion>0.42.0</AssemblyVersion>
|
||||
<FileVersion>0.42.0</FileVersion>
|
||||
<VersionPrefix>0.0.0</VersionPrefix>
|
||||
<AssemblyVersion>0.0.0.0</AssemblyVersion>
|
||||
<FileVersion>0.0.0.0</FileVersion>
|
||||
<Authors>Adam Hathcock</Authors>
|
||||
<TargetFrameworks>net48;net8.0;net10.0</TargetFrameworks>
|
||||
<TargetFrameworks>net48;netstandard20;net8.0;net10.0</TargetFrameworks>
|
||||
<AssemblyName>SharpCompress</AssemblyName>
|
||||
<AssemblyOriginatorKeyFile>../../SharpCompress.snk</AssemblyOriginatorKeyFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
@@ -43,7 +43,7 @@
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' Or '$(TargetFramework)' == 'net10.0' ">
|
||||
<PackageReference Include="Microsoft.NET.ILLink.Tasks" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net48' ">
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net48' Or '$(TargetFramework)' == 'netstandard20' ">
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" />
|
||||
<PackageReference Include="System.Buffers" />
|
||||
|
||||
@@ -12,13 +12,13 @@ internal class ZipCentralDirectoryEntry
|
||||
{
|
||||
private readonly ZipCompressionMethod compression;
|
||||
private readonly string fileName;
|
||||
private readonly ArchiveEncoding archiveEncoding;
|
||||
private readonly IArchiveEncoding archiveEncoding;
|
||||
|
||||
public ZipCentralDirectoryEntry(
|
||||
ZipCompressionMethod compression,
|
||||
string fileName,
|
||||
ulong headerOffset,
|
||||
ArchiveEncoding archiveEncoding
|
||||
IArchiveEncoding archiveEncoding
|
||||
)
|
||||
{
|
||||
this.compression = compression;
|
||||
@@ -35,6 +35,16 @@ internal class ZipCentralDirectoryEntry
|
||||
internal ushort Zip64HeaderOffset { get; set; }
|
||||
internal ulong HeaderOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The encryption type used for this entry.
|
||||
/// </summary>
|
||||
internal ZipEncryptionType EncryptionType { get; set; } = ZipEncryptionType.None;
|
||||
|
||||
/// <summary>
|
||||
/// The actual compression method (used when compression is WinzipAes).
|
||||
/// </summary>
|
||||
internal ZipCompressionMethod ActualCompression { get; set; }
|
||||
|
||||
internal uint Write(Stream outputStream)
|
||||
{
|
||||
var encodedFilename = archiveEncoding.Encode(fileName);
|
||||
@@ -49,12 +59,29 @@ internal class ZipCentralDirectoryEntry
|
||||
var headeroffsetvalue = zip64 ? uint.MaxValue : (uint)HeaderOffset;
|
||||
var extralength = zip64 ? (2 + 2 + 8 + 8 + 8 + 4) : 0;
|
||||
|
||||
// Add AES extra field length if encrypted with AES
|
||||
if (
|
||||
EncryptionType == ZipEncryptionType.Aes128
|
||||
|| EncryptionType == ZipEncryptionType.Aes256
|
||||
)
|
||||
{
|
||||
extralength += 2 + 2 + 7; // ID + size + data
|
||||
}
|
||||
|
||||
// Determine version needed to extract:
|
||||
// - Version 63 for LZMA, PPMd, BZip2, ZStandard (advanced compression methods)
|
||||
// - Version 51 for WinZip AES encryption
|
||||
// - Version 45 for Zip64 extensions (when Zip64HeaderOffset != 0 or actual sizes require it)
|
||||
// - Version 20 for standard Deflate/None compression
|
||||
byte version;
|
||||
if (
|
||||
EncryptionType == ZipEncryptionType.Aes128
|
||||
|| EncryptionType == ZipEncryptionType.Aes256
|
||||
)
|
||||
{
|
||||
version = 51;
|
||||
}
|
||||
else if (
|
||||
compression == ZipCompressionMethod.LZMA
|
||||
|| compression == ZipCompressionMethod.PPMd
|
||||
|| compression == ZipCompressionMethod.BZip2
|
||||
@@ -75,6 +102,13 @@ internal class ZipCentralDirectoryEntry
|
||||
var flags = Equals(archiveEncoding.GetEncoding(), Encoding.UTF8)
|
||||
? HeaderFlags.Efs
|
||||
: HeaderFlags.None;
|
||||
|
||||
// Add encryption flag
|
||||
if (EncryptionType != ZipEncryptionType.None)
|
||||
{
|
||||
flags |= HeaderFlags.Encrypted;
|
||||
}
|
||||
|
||||
if (!outputStream.CanSeek)
|
||||
{
|
||||
// Cannot use data descriptors with zip64:
|
||||
@@ -94,8 +128,8 @@ internal class ZipCentralDirectoryEntry
|
||||
}
|
||||
}
|
||||
|
||||
// Support for zero byte files
|
||||
if (Decompressed == 0 && Compressed == 0)
|
||||
// Support for zero byte files (but not for encrypted files which always have encryption overhead)
|
||||
if (Decompressed == 0 && Compressed == 0 && EncryptionType == ZipEncryptionType.None)
|
||||
{
|
||||
usedCompression = ZipCompressionMethod.None;
|
||||
}
|
||||
@@ -153,11 +187,13 @@ internal class ZipCentralDirectoryEntry
|
||||
outputStream.Write(intBuf.Slice(0, 4)); // Offset of header
|
||||
|
||||
outputStream.Write(encodedFilename, 0, encodedFilename.Length);
|
||||
|
||||
// Write Zip64 extra field
|
||||
if (zip64)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(intBuf, 0x0001);
|
||||
outputStream.Write(intBuf.Slice(0, 2));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(intBuf, (ushort)(extralength - 4));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(intBuf, (ushort)(2 + 2 + 8 + 8 + 8 + 4 - 4));
|
||||
outputStream.Write(intBuf.Slice(0, 2));
|
||||
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(intBuf, Decompressed);
|
||||
@@ -170,6 +206,28 @@ internal class ZipCentralDirectoryEntry
|
||||
outputStream.Write(intBuf.Slice(0, 4)); // VolumeNumber = 0
|
||||
}
|
||||
|
||||
// Write WinZip AES extra field
|
||||
if (
|
||||
EncryptionType == ZipEncryptionType.Aes128
|
||||
|| EncryptionType == ZipEncryptionType.Aes256
|
||||
)
|
||||
{
|
||||
Span<byte> aesExtra = stackalloc byte[11];
|
||||
// Extra field ID: 0x9901 (WinZip AES)
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(aesExtra, 0x9901);
|
||||
// Extra field data size: 7 bytes
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(aesExtra.Slice(2), 7);
|
||||
// AES encryption version: 2 (AE-2)
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(aesExtra.Slice(4), 0x0002);
|
||||
// Vendor ID: "AE" = 0x4541
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(aesExtra.Slice(6), 0x4541);
|
||||
// AES encryption strength: 1=128-bit, 3=256-bit
|
||||
aesExtra[8] = EncryptionType == ZipEncryptionType.Aes128 ? (byte)1 : (byte)3;
|
||||
// Actual compression method
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(aesExtra.Slice(9), (ushort)ActualCompression);
|
||||
outputStream.Write(aesExtra);
|
||||
}
|
||||
|
||||
outputStream.Write(encodedComment, 0, encodedComment.Length);
|
||||
|
||||
return (uint)(
|
||||
|
||||
@@ -27,6 +27,8 @@ public class ZipWriter : AbstractWriter
|
||||
private long streamPosition;
|
||||
private PpmdProperties? ppmdProps;
|
||||
private readonly bool isZip64;
|
||||
private readonly string? password;
|
||||
private readonly ZipEncryptionType encryptionType;
|
||||
|
||||
public ZipWriter(Stream destination, ZipWriterOptions zipWriterOptions)
|
||||
: base(ArchiveType.Zip, zipWriterOptions)
|
||||
@@ -41,6 +43,21 @@ public class ZipWriter : AbstractWriter
|
||||
compressionType = zipWriterOptions.CompressionType;
|
||||
compressionLevel = zipWriterOptions.CompressionLevel;
|
||||
|
||||
// Initialize encryption settings
|
||||
password = zipWriterOptions.Password;
|
||||
if (!string.IsNullOrEmpty(password))
|
||||
{
|
||||
// If password is set but encryption type is None, default to AES-256
|
||||
encryptionType =
|
||||
zipWriterOptions.EncryptionType == ZipEncryptionType.None
|
||||
? ZipEncryptionType.Aes256
|
||||
: zipWriterOptions.EncryptionType;
|
||||
}
|
||||
else
|
||||
{
|
||||
encryptionType = ZipEncryptionType.None;
|
||||
}
|
||||
|
||||
if (WriterOptions.LeaveStreamOpen)
|
||||
{
|
||||
destination = SharpCompressStream.Create(destination, leaveOpen: true);
|
||||
@@ -97,8 +114,20 @@ public class ZipWriter : AbstractWriter
|
||||
entryPath = NormalizeFilename(entryPath);
|
||||
options.ModificationDateTime ??= DateTime.Now;
|
||||
options.EntryComment ??= string.Empty;
|
||||
|
||||
// Determine the effective encryption type for this entry
|
||||
var effectiveEncryption = encryptionType;
|
||||
|
||||
// For WinZip AES, the compression method in the header is set to WinzipAes,
|
||||
// and the actual compression method is stored in the extra field
|
||||
var headerCompression =
|
||||
effectiveEncryption == ZipEncryptionType.Aes128
|
||||
|| effectiveEncryption == ZipEncryptionType.Aes256
|
||||
? ZipCompressionMethod.WinzipAes
|
||||
: compression;
|
||||
|
||||
var entry = new ZipCentralDirectoryEntry(
|
||||
compression,
|
||||
headerCompression,
|
||||
entryPath,
|
||||
(ulong)streamPosition,
|
||||
WriterOptions.ArchiveEncoding
|
||||
@@ -106,6 +135,8 @@ public class ZipWriter : AbstractWriter
|
||||
{
|
||||
Comment = options.EntryComment,
|
||||
ModificationTime = options.ModificationDateTime,
|
||||
EncryptionType = effectiveEncryption,
|
||||
ActualCompression = compression,
|
||||
};
|
||||
|
||||
// Use the archive default setting for zip64 and allow overrides
|
||||
@@ -115,14 +146,23 @@ public class ZipWriter : AbstractWriter
|
||||
useZip64 = options.EnableZip64.Value;
|
||||
}
|
||||
|
||||
var headersize = (uint)WriteHeader(entryPath, options, entry, useZip64);
|
||||
var headersize = (uint)WriteHeader(
|
||||
entryPath,
|
||||
options,
|
||||
entry,
|
||||
useZip64,
|
||||
effectiveEncryption,
|
||||
compression
|
||||
);
|
||||
streamPosition += headersize;
|
||||
return new ZipWritingStream(
|
||||
this,
|
||||
OutputStream.NotNull(),
|
||||
entry,
|
||||
compression,
|
||||
options.CompressionLevel ?? compressionLevel
|
||||
options.CompressionLevel ?? compressionLevel,
|
||||
effectiveEncryption,
|
||||
password
|
||||
);
|
||||
}
|
||||
|
||||
@@ -201,7 +241,15 @@ public class ZipWriter : AbstractWriter
|
||||
useZip64 = options.EnableZip64.Value;
|
||||
}
|
||||
|
||||
var headersize = (uint)WriteHeader(directoryPath, options, entry, useZip64);
|
||||
// Directory entries are never encrypted
|
||||
var headersize = (uint)WriteHeader(
|
||||
directoryPath,
|
||||
options,
|
||||
entry,
|
||||
useZip64,
|
||||
ZipEncryptionType.None,
|
||||
compression
|
||||
);
|
||||
streamPosition += headersize;
|
||||
entries.Add(entry);
|
||||
}
|
||||
@@ -210,7 +258,9 @@ public class ZipWriter : AbstractWriter
|
||||
string filename,
|
||||
ZipWriterEntryOptions zipWriterEntryOptions,
|
||||
ZipCentralDirectoryEntry entry,
|
||||
bool useZip64
|
||||
bool useZip64,
|
||||
ZipEncryptionType encryption,
|
||||
ZipCompressionMethod actualCompression
|
||||
)
|
||||
{
|
||||
// We err on the side of caution until the zip specification clarifies how to support this
|
||||
@@ -221,15 +271,30 @@ public class ZipWriter : AbstractWriter
|
||||
);
|
||||
}
|
||||
|
||||
var explicitZipCompressionInfo = ToZipCompressionMethod(
|
||||
zipWriterEntryOptions.CompressionType ?? compressionType
|
||||
);
|
||||
// Encryption is only supported with seekable streams for now
|
||||
if (!OutputStream.CanSeek && encryption != ZipEncryptionType.None)
|
||||
{
|
||||
throw new NotSupportedException("Encryption is not supported on non-seekable streams");
|
||||
}
|
||||
|
||||
// Determine the compression method to write in the header
|
||||
var headerCompression =
|
||||
encryption == ZipEncryptionType.Aes128 || encryption == ZipEncryptionType.Aes256
|
||||
? ZipCompressionMethod.WinzipAes
|
||||
: actualCompression;
|
||||
|
||||
var encodedFilename = WriterOptions.ArchiveEncoding.Encode(filename);
|
||||
|
||||
Span<byte> intBuf = stackalloc byte[4];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(intBuf, ZipHeaderFactory.ENTRY_HEADER_BYTES);
|
||||
OutputStream.Write(intBuf);
|
||||
if (explicitZipCompressionInfo == ZipCompressionMethod.Deflate)
|
||||
|
||||
// Determine version needed
|
||||
if (encryption == ZipEncryptionType.Aes128 || encryption == ZipEncryptionType.Aes256)
|
||||
{
|
||||
OutputStream.Write(stackalloc byte[] { 51, 0 }); // WinZip AES requires version 5.1
|
||||
}
|
||||
else if (actualCompression == ZipCompressionMethod.Deflate)
|
||||
{
|
||||
if (OutputStream.CanSeek && useZip64)
|
||||
{
|
||||
@@ -244,14 +309,22 @@ public class ZipWriter : AbstractWriter
|
||||
{
|
||||
OutputStream.Write(stackalloc byte[] { 63, 0 }); //version says we used PPMd or LZMA
|
||||
}
|
||||
|
||||
var flags = Equals(WriterOptions.ArchiveEncoding.GetEncoding(), Encoding.UTF8)
|
||||
? HeaderFlags.Efs
|
||||
: 0;
|
||||
: HeaderFlags.None;
|
||||
|
||||
// Add encryption flag
|
||||
if (encryption != ZipEncryptionType.None)
|
||||
{
|
||||
flags |= HeaderFlags.Encrypted;
|
||||
}
|
||||
|
||||
if (!OutputStream.CanSeek)
|
||||
{
|
||||
flags |= HeaderFlags.UsePostDataDescriptor;
|
||||
|
||||
if (explicitZipCompressionInfo == ZipCompressionMethod.LZMA)
|
||||
if (actualCompression == ZipCompressionMethod.LZMA)
|
||||
{
|
||||
flags |= HeaderFlags.Bit1; // eos marker
|
||||
}
|
||||
@@ -259,7 +332,7 @@ public class ZipWriter : AbstractWriter
|
||||
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(intBuf, (ushort)flags);
|
||||
OutputStream.Write(intBuf.Slice(0, 2));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(intBuf, (ushort)explicitZipCompressionInfo);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(intBuf, (ushort)headerCompression);
|
||||
OutputStream.Write(intBuf.Slice(0, 2)); // zipping method
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
intBuf,
|
||||
@@ -274,22 +347,49 @@ public class ZipWriter : AbstractWriter
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(intBuf, (ushort)encodedFilename.Length);
|
||||
OutputStream.Write(intBuf.Slice(0, 2)); // filename length
|
||||
|
||||
// Calculate extra field length
|
||||
var extralength = 0;
|
||||
if (OutputStream.CanSeek && useZip64)
|
||||
{
|
||||
extralength = 2 + 2 + 8 + 8;
|
||||
extralength += 2 + 2 + 8 + 8; // Zip64 extra field
|
||||
}
|
||||
|
||||
// WinZip AES extra field: 2 (id) + 2 (size) + 2 (version) + 2 (vendor) + 1 (strength) + 2 (actual compression)
|
||||
if (encryption == ZipEncryptionType.Aes128 || encryption == ZipEncryptionType.Aes256)
|
||||
{
|
||||
extralength += 2 + 2 + 7;
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(intBuf, (ushort)extralength);
|
||||
OutputStream.Write(intBuf.Slice(0, 2)); // extra length
|
||||
OutputStream.Write(encodedFilename, 0, encodedFilename.Length);
|
||||
|
||||
if (extralength != 0)
|
||||
// Write Zip64 extra field
|
||||
if (OutputStream.CanSeek && useZip64)
|
||||
{
|
||||
OutputStream.Write(new byte[extralength], 0, extralength); // reserve space for zip64 data
|
||||
OutputStream.Write(new byte[2 + 2 + 8 + 8], 0, 2 + 2 + 8 + 8); // reserve space for zip64 data
|
||||
entry.Zip64HeaderOffset = (ushort)(6 + 2 + 2 + 4 + 12 + 2 + 2 + encodedFilename.Length);
|
||||
}
|
||||
|
||||
// Write WinZip AES extra field
|
||||
if (encryption == ZipEncryptionType.Aes128 || encryption == ZipEncryptionType.Aes256)
|
||||
{
|
||||
Span<byte> aesExtra = stackalloc byte[11];
|
||||
// Extra field ID: 0x9901 (WinZip AES)
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(aesExtra, 0x9901);
|
||||
// Extra field data size: 7 bytes
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(aesExtra.Slice(2), 7);
|
||||
// AES encryption version: 2 (AE-2)
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(aesExtra.Slice(4), 0x0002);
|
||||
// Vendor ID: "AE" = 0x4541
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(aesExtra.Slice(6), 0x4541);
|
||||
// AES encryption strength: 1=128-bit, 3=256-bit
|
||||
aesExtra[8] = encryption == ZipEncryptionType.Aes128 ? (byte)1 : (byte)3;
|
||||
// Actual compression method
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(aesExtra.Slice(9), (ushort)actualCompression);
|
||||
OutputStream.Write(aesExtra);
|
||||
}
|
||||
|
||||
return 6 + 2 + 2 + 4 + 12 + 2 + 2 + encodedFilename.Length + extralength;
|
||||
}
|
||||
|
||||
@@ -385,7 +485,10 @@ public class ZipWriter : AbstractWriter
|
||||
private readonly ZipWriter writer;
|
||||
private readonly ZipCompressionMethod zipCompressionMethod;
|
||||
private readonly int compressionLevel;
|
||||
private readonly ZipEncryptionType encryptionType;
|
||||
private readonly string? password;
|
||||
private SharpCompressStream? counting;
|
||||
private Stream? encryptionStream;
|
||||
private ulong decompressed;
|
||||
|
||||
// Flag to prevent throwing exceptions on Dispose
|
||||
@@ -397,15 +500,18 @@ public class ZipWriter : AbstractWriter
|
||||
Stream originalStream,
|
||||
ZipCentralDirectoryEntry entry,
|
||||
ZipCompressionMethod zipCompressionMethod,
|
||||
int compressionLevel
|
||||
int compressionLevel,
|
||||
ZipEncryptionType encryptionType,
|
||||
string? password
|
||||
)
|
||||
{
|
||||
this.writer = writer;
|
||||
this.originalStream = originalStream;
|
||||
this.writer = writer;
|
||||
this.entry = entry;
|
||||
this.zipCompressionMethod = zipCompressionMethod;
|
||||
this.compressionLevel = compressionLevel;
|
||||
this.encryptionType = encryptionType;
|
||||
this.password = password;
|
||||
writeStream = GetWriteStream(originalStream);
|
||||
}
|
||||
|
||||
@@ -427,6 +533,47 @@ public class ZipWriter : AbstractWriter
|
||||
{
|
||||
counting = new SharpCompressStream(writeStream, leaveOpen: true);
|
||||
Stream output = counting;
|
||||
|
||||
// Wrap with encryption stream if needed
|
||||
if (encryptionType == ZipEncryptionType.Aes128)
|
||||
{
|
||||
encryptionStream = new WinzipAesEncryptionStream(
|
||||
counting,
|
||||
password!,
|
||||
WinzipAesKeySize.KeySize128
|
||||
);
|
||||
output = encryptionStream;
|
||||
}
|
||||
else if (encryptionType == ZipEncryptionType.Aes256)
|
||||
{
|
||||
encryptionStream = new WinzipAesEncryptionStream(
|
||||
counting,
|
||||
password!,
|
||||
WinzipAesKeySize.KeySize256
|
||||
);
|
||||
output = encryptionStream;
|
||||
}
|
||||
else if (encryptionType == ZipEncryptionType.PkwareTraditional)
|
||||
{
|
||||
// For PKWARE traditional encryption, we need to write the encryption header
|
||||
// and wrap the stream in the crypto stream
|
||||
var encryptor = PkwareTraditionalEncryptionData.ForWrite(
|
||||
password!,
|
||||
writer.WriterOptions.ArchiveEncoding
|
||||
);
|
||||
// Write the encryption header (12 bytes)
|
||||
// CRC is not known yet, so we use 0 for now (it gets verified with time for streaming)
|
||||
var header = encryptor.GenerateEncryptionHeader(0, 0);
|
||||
counting.Write(header, 0, header.Length);
|
||||
|
||||
encryptionStream = new PkwareTraditionalCryptoStream(
|
||||
new NonDisposingStream(counting),
|
||||
encryptor,
|
||||
CryptoMode.Encrypt
|
||||
);
|
||||
output = encryptionStream;
|
||||
}
|
||||
|
||||
switch (zipCompressionMethod)
|
||||
{
|
||||
case ZipCompressionMethod.None:
|
||||
@@ -436,17 +583,24 @@ public class ZipWriter : AbstractWriter
|
||||
case ZipCompressionMethod.Deflate:
|
||||
{
|
||||
return new DeflateStream(
|
||||
counting,
|
||||
output,
|
||||
CompressionMode.Compress,
|
||||
(CompressionLevel)compressionLevel
|
||||
);
|
||||
}
|
||||
case ZipCompressionMethod.BZip2:
|
||||
{
|
||||
return new BZip2Stream(counting, CompressionMode.Compress, false);
|
||||
return new BZip2Stream(output, CompressionMode.Compress, false);
|
||||
}
|
||||
case ZipCompressionMethod.LZMA:
|
||||
{
|
||||
// LZMA with encryption is not supported per ZIP spec
|
||||
if (encryptionType != ZipEncryptionType.None)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"LZMA compression with encryption is not supported"
|
||||
);
|
||||
}
|
||||
counting.WriteByte(9);
|
||||
counting.WriteByte(20);
|
||||
counting.WriteByte(5);
|
||||
@@ -455,7 +609,7 @@ public class ZipWriter : AbstractWriter
|
||||
var lzmaStream = new LzmaStream(
|
||||
new LzmaEncoderProperties(!originalStream.CanSeek),
|
||||
false,
|
||||
counting
|
||||
output
|
||||
);
|
||||
counting.Write(lzmaStream.Properties, 0, lzmaStream.Properties.Length);
|
||||
return lzmaStream;
|
||||
@@ -463,11 +617,11 @@ public class ZipWriter : AbstractWriter
|
||||
case ZipCompressionMethod.PPMd:
|
||||
{
|
||||
counting.Write(writer.PpmdProperties.Properties, 0, 2);
|
||||
return new PpmdStream(writer.PpmdProperties, counting, true);
|
||||
return new PpmdStream(writer.PpmdProperties, output, true);
|
||||
}
|
||||
case ZipCompressionMethod.ZStandard:
|
||||
{
|
||||
return new CompressionStream(counting, compressionLevel);
|
||||
return new CompressionStream(output, compressionLevel);
|
||||
}
|
||||
default:
|
||||
{
|
||||
@@ -490,6 +644,9 @@ public class ZipWriter : AbstractWriter
|
||||
{
|
||||
writeStream.Dispose();
|
||||
|
||||
// Dispose encryption stream to finalize encryption (e.g., write auth code for AES)
|
||||
encryptionStream?.Dispose();
|
||||
|
||||
if (limitsExceeded)
|
||||
{
|
||||
// We have written invalid data into the archive,
|
||||
@@ -511,12 +668,24 @@ public class ZipWriter : AbstractWriter
|
||||
|
||||
if (originalStream.CanSeek)
|
||||
{
|
||||
// Clear UsePostDataDescriptor flag (bit 3) since we're updating sizes in place
|
||||
// But preserve the Encrypted flag (bit 0) if encryption is enabled
|
||||
originalStream.Position = (long)(entry.HeaderOffset + 6);
|
||||
originalStream.WriteByte(0);
|
||||
// Only the Encrypted flag should be in the low byte for seekable streams
|
||||
originalStream.WriteByte(
|
||||
encryptionType != ZipEncryptionType.None
|
||||
? (byte)HeaderFlags.Encrypted
|
||||
: (byte)0
|
||||
);
|
||||
|
||||
if (countingCount == 0 && entry.Decompressed == 0)
|
||||
if (
|
||||
countingCount == 0
|
||||
&& entry.Decompressed == 0
|
||||
&& encryptionType == ZipEncryptionType.None
|
||||
)
|
||||
{
|
||||
// set compression to STORED for zero byte files (no compression data)
|
||||
// But not if encrypted, as encrypted files always have some data
|
||||
originalStream.Position = (long)(entry.HeaderOffset + 8);
|
||||
originalStream.WriteByte(0);
|
||||
originalStream.WriteByte(0);
|
||||
@@ -638,5 +807,46 @@ public class ZipWriter : AbstractWriter
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A stream wrapper that doesn't dispose the underlying stream when disposed.
|
||||
/// This is used in encryption scenarios where the crypto stream would otherwise
|
||||
/// dispose the counting stream prematurely, before we can read the final count.
|
||||
/// </summary>
|
||||
private class NonDisposingStream : Stream
|
||||
{
|
||||
private readonly Stream _stream;
|
||||
|
||||
public NonDisposingStream(Stream stream) => _stream = stream;
|
||||
|
||||
public override bool CanRead => _stream.CanRead;
|
||||
public override bool CanSeek => _stream.CanSeek;
|
||||
public override bool CanWrite => _stream.CanWrite;
|
||||
public override long Length => _stream.Length;
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => _stream.Position;
|
||||
set => _stream.Position = value;
|
||||
}
|
||||
|
||||
public override void Flush() => _stream.Flush();
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) =>
|
||||
_stream.Read(buffer, offset, count);
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) => _stream.Seek(offset, origin);
|
||||
|
||||
public override void SetLength(long value) => _stream.SetLength(value);
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count) =>
|
||||
_stream.Write(buffer, offset, count);
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
// Don't dispose the underlying stream
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Nested type: ZipWritingStream
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Zip;
|
||||
using SharpCompress.Compressors.Deflate;
|
||||
using D = SharpCompress.Compressors.Deflate;
|
||||
|
||||
@@ -24,6 +25,8 @@ public class ZipWriterOptions : WriterOptions
|
||||
{
|
||||
UseZip64 = writerOptions.UseZip64;
|
||||
ArchiveComment = writerOptions.ArchiveComment;
|
||||
Password = writerOptions.Password;
|
||||
EncryptionType = writerOptions.EncryptionType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,4 +83,19 @@ public class ZipWriterOptions : WriterOptions
|
||||
/// are less than 4GiB in length.
|
||||
/// </summary>
|
||||
public bool UseZip64 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The password to use for encrypting the ZIP archive entries.
|
||||
/// When set, entries will be encrypted using the specified <see cref="EncryptionType"/>.
|
||||
/// If null or empty, no encryption is applied.
|
||||
/// </summary>
|
||||
public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The encryption type to use when a password is set.
|
||||
/// Defaults to <see cref="ZipEncryptionType.None"/>.
|
||||
/// When <see cref="Password"/> is set and this is <see cref="ZipEncryptionType.None"/>,
|
||||
/// <see cref="ZipEncryptionType.Aes256"/> will be used automatically.
|
||||
/// </summary>
|
||||
public ZipEncryptionType EncryptionType { get; set; } = ZipEncryptionType.None;
|
||||
}
|
||||
|
||||
@@ -97,6 +97,110 @@
|
||||
"contentHash": "+RJT4qaekpZ7DDLhf+LTjq+E48jieKiY9ulJ+BoxKmZblIJfIJT8Ufcaa/clQqnYvWs8jugfGSMu8ylS0caG0w=="
|
||||
}
|
||||
},
|
||||
".NETStandard,Version=v2.0": {
|
||||
"Microsoft.Bcl.AsyncInterfaces": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.0, )",
|
||||
"resolved": "10.0.0",
|
||||
"contentHash": "vFuwSLj9QJBbNR0NeNO4YVASUbokxs+i/xbuu8B+Fs4FAZg5QaFa6eGrMaRqTzzNI5tAb97T7BhSxtLckFyiRA==",
|
||||
"dependencies": {
|
||||
"System.Threading.Tasks.Extensions": "4.6.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.3, )",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "8.0.0",
|
||||
"Microsoft.SourceLink.Common": "8.0.0"
|
||||
}
|
||||
},
|
||||
"NETStandard.Library": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.0.3, )",
|
||||
"resolved": "2.0.3",
|
||||
"contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0"
|
||||
}
|
||||
},
|
||||
"System.Buffers": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.6.1, )",
|
||||
"resolved": "4.6.1",
|
||||
"contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw=="
|
||||
},
|
||||
"System.Memory": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.6.3, )",
|
||||
"resolved": "4.6.3",
|
||||
"contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==",
|
||||
"dependencies": {
|
||||
"System.Buffers": "4.6.1",
|
||||
"System.Numerics.Vectors": "4.6.1",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
|
||||
}
|
||||
},
|
||||
"System.Text.Encoding.CodePages": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.0, )",
|
||||
"resolved": "10.0.0",
|
||||
"contentHash": "QLP54mIATaBpjGlsZIxga38VPk1G9js0Kw651B+bvrXi2kSgGZYrxJSpM3whhTZCBK4HEBHX3fzfDQMw7CXHGQ==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.6.3",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
|
||||
},
|
||||
"Microsoft.NETCore.Platforms": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
|
||||
},
|
||||
"System.Numerics.Vectors": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.6.1",
|
||||
"contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q=="
|
||||
},
|
||||
"System.Runtime.CompilerServices.Unsafe": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.1.2",
|
||||
"contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw=="
|
||||
},
|
||||
"System.Threading.Tasks.Extensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.6.3",
|
||||
"contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==",
|
||||
"dependencies": {
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"net10.0": {
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using SharpCompress.Archives.Zip;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Zip;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Writers;
|
||||
using SharpCompress.Writers.Zip;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Zip;
|
||||
@@ -97,4 +102,208 @@ public class ZipWriterTests : WriterTests
|
||||
Assert.Throws<InvalidFormatException>(() =>
|
||||
Write(CompressionType.Rar, "Zip.ppmd.noEmptyDirs.zip", "Zip.ppmd.noEmptyDirs.zip")
|
||||
);
|
||||
|
||||
[Fact]
|
||||
public void Zip_Deflate_Encrypted_Aes256_WriteAndRead()
|
||||
{
|
||||
const string password = "test_password";
|
||||
const string testContent = "Hello, this is a test file for encrypted ZIP.";
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
|
||||
// Write encrypted ZIP
|
||||
var options = new ZipWriterOptions(CompressionType.Deflate)
|
||||
{
|
||||
Password = password,
|
||||
EncryptionType = ZipEncryptionType.Aes256,
|
||||
};
|
||||
|
||||
using (var writer = new ZipWriter(memoryStream, options))
|
||||
{
|
||||
var contentBytes = Encoding.UTF8.GetBytes(testContent);
|
||||
writer.Write("test.txt", new MemoryStream(contentBytes));
|
||||
}
|
||||
|
||||
// Read back the encrypted ZIP
|
||||
memoryStream.Position = 0;
|
||||
|
||||
using var archive = ZipArchive.Open(
|
||||
memoryStream,
|
||||
new ReaderOptions { Password = password }
|
||||
);
|
||||
|
||||
var entry = archive.Entries.First(e => !e.IsDirectory);
|
||||
Assert.Equal("test.txt", entry.Key);
|
||||
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
using var reader = new StreamReader(entryStream);
|
||||
var content = reader.ReadToEnd();
|
||||
|
||||
Assert.Equal(testContent, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Zip_Deflate_Encrypted_Aes128_WriteAndRead()
|
||||
{
|
||||
const string password = "test_password";
|
||||
const string testContent = "Hello, this is a test file for encrypted ZIP with AES-128.";
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
|
||||
// Write encrypted ZIP
|
||||
var options = new ZipWriterOptions(CompressionType.Deflate)
|
||||
{
|
||||
Password = password,
|
||||
EncryptionType = ZipEncryptionType.Aes128,
|
||||
};
|
||||
|
||||
using (var writer = new ZipWriter(memoryStream, options))
|
||||
{
|
||||
var contentBytes = Encoding.UTF8.GetBytes(testContent);
|
||||
writer.Write("test.txt", new MemoryStream(contentBytes));
|
||||
}
|
||||
|
||||
// Read back the encrypted ZIP
|
||||
memoryStream.Position = 0;
|
||||
|
||||
using var archive = ZipArchive.Open(
|
||||
memoryStream,
|
||||
new ReaderOptions { Password = password }
|
||||
);
|
||||
|
||||
var entry = archive.Entries.First(e => !e.IsDirectory);
|
||||
Assert.Equal("test.txt", entry.Key);
|
||||
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
using var reader = new StreamReader(entryStream);
|
||||
var content = reader.ReadToEnd();
|
||||
|
||||
Assert.Equal(testContent, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Zip_None_Encrypted_Aes256_WriteAndRead()
|
||||
{
|
||||
const string password = "test_password";
|
||||
const string testContent = "Uncompressed but encrypted content.";
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
|
||||
// Write encrypted ZIP with no compression
|
||||
var options = new ZipWriterOptions(CompressionType.None)
|
||||
{
|
||||
Password = password,
|
||||
EncryptionType = ZipEncryptionType.Aes256,
|
||||
};
|
||||
|
||||
using (var writer = new ZipWriter(memoryStream, options))
|
||||
{
|
||||
var contentBytes = Encoding.UTF8.GetBytes(testContent);
|
||||
writer.Write("uncompressed.txt", new MemoryStream(contentBytes));
|
||||
}
|
||||
|
||||
// Read back the encrypted ZIP
|
||||
memoryStream.Position = 0;
|
||||
|
||||
using var archive = ZipArchive.Open(
|
||||
memoryStream,
|
||||
new ReaderOptions { Password = password }
|
||||
);
|
||||
|
||||
var entry = archive.Entries.First(e => !e.IsDirectory);
|
||||
Assert.Equal("uncompressed.txt", entry.Key);
|
||||
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
using var reader = new StreamReader(entryStream);
|
||||
var content = reader.ReadToEnd();
|
||||
|
||||
Assert.Equal(testContent, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Zip_Encrypted_MultipleFiles_WriteAndRead()
|
||||
{
|
||||
const string password = "multi_file_password";
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
|
||||
// Write encrypted ZIP with multiple files
|
||||
var options = new ZipWriterOptions(CompressionType.Deflate)
|
||||
{
|
||||
Password = password,
|
||||
EncryptionType = ZipEncryptionType.Aes256,
|
||||
};
|
||||
|
||||
using (var writer = new ZipWriter(memoryStream, options))
|
||||
{
|
||||
writer.Write(
|
||||
"file1.txt",
|
||||
new MemoryStream(Encoding.UTF8.GetBytes("Content of file 1"))
|
||||
);
|
||||
writer.Write(
|
||||
"file2.txt",
|
||||
new MemoryStream(Encoding.UTF8.GetBytes("Content of file 2"))
|
||||
);
|
||||
writer.Write(
|
||||
"folder/file3.txt",
|
||||
new MemoryStream(Encoding.UTF8.GetBytes("Content of file 3"))
|
||||
);
|
||||
}
|
||||
|
||||
// Read back the encrypted ZIP
|
||||
memoryStream.Position = 0;
|
||||
|
||||
using var archive = ZipArchive.Open(
|
||||
memoryStream,
|
||||
new ReaderOptions { Password = password }
|
||||
);
|
||||
|
||||
var entries = archive.Entries.Where(e => !e.IsDirectory).ToList();
|
||||
Assert.Equal(3, entries.Count);
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
using var reader = new StreamReader(entryStream);
|
||||
var content = reader.ReadToEnd();
|
||||
Assert.Contains("Content of file", content);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Zip_Encrypted_DefaultEncryption_WhenPasswordSet()
|
||||
{
|
||||
const string password = "auto_encryption";
|
||||
const string testContent = "Auto encryption type test.";
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
|
||||
// Write ZIP with password but no explicit encryption type
|
||||
// Should default to AES-256
|
||||
var options = new ZipWriterOptions(CompressionType.Deflate)
|
||||
{
|
||||
Password = password,
|
||||
// EncryptionType not set, should default to AES-256 when password is provided
|
||||
};
|
||||
|
||||
using (var writer = new ZipWriter(memoryStream, options))
|
||||
{
|
||||
writer.Write("auto.txt", new MemoryStream(Encoding.UTF8.GetBytes(testContent)));
|
||||
}
|
||||
|
||||
// Read back the encrypted ZIP
|
||||
memoryStream.Position = 0;
|
||||
|
||||
using var archive = ZipArchive.Open(
|
||||
memoryStream,
|
||||
new ReaderOptions { Password = password }
|
||||
);
|
||||
|
||||
var entry = archive.Entries.First(e => !e.IsDirectory);
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
using var reader = new StreamReader(entryStream);
|
||||
var content = reader.ReadToEnd();
|
||||
|
||||
Assert.Equal(testContent, content);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user