پشتیبانی بومی از رمزگذاری و تعیین کلمهعبور در فایلهای ZIP در داتنت ۱۱
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۵/۲۳ ۰۹:۴۵
آدرس: www.dntips.ir
چکیده: یکی از کمبودهای دیرینه در کتابخانه استاندارد داتنت (System.IO.Compression)، عدم امکان خواندن و نوشتن فایلهای فشرده ZIP دارای کلمهعبور بهصورت توکار بود؛ محدودیتی که توسعهدهندگان را ناچار به استفاده از پکیجهای شخص ثالث (Third-party) مانند DotNetZip یا SharpZipLib میکرد. با معرفی قابلیتهای جدید در پلتفرم داتنت، پشتیبانی بومی از الگوریتمهای رمزگذاری سنتی (ZipCrypto) و استاندارد مدرنWinZip AESبه این فضا افزوده شده است. این مقاله به بررسی معماری، متدهای افزوده شده نظیرOpenAsyncوCreateEntryFromFileAsync، بهینهسازیهای حافظهای مبتنی برReadOnlySpanو بررسی خطاهای امنیتی مرتبط میپردازد.
System.IO.Compression در اکوسیستم داتنت، توسعهدهندگان همواره برای فشردهسازی دادههای حساس نیازمند محافظت با رمز عبور (Password Protection) بودند. تا پیش از این، موتور پایه داتنت تنها قادر به خواندن و نوشتن آرشیوهای بدون رمزگذاری بود و هرگونه نیاز به امنیت در سطح فایل، مستلزم تحمیل وابستگیهای جانبی (Dependencies) خارجی بود که خود چالشهای نگهداری و خطرات زنجیره تامین (Supply Chain) را در پی داشت. در بهروزرسانیهای جدید فریمورک داتنت (از پیشنمایش نسخه ۱۱)، مایکروسافت با افزودن قابلیتهای بومی به این کتابخانه، امکان تولید و خواندن ورودیهای رمزگذاریشده ZIP را فراهم کرده است.ZipEncryptionMethod)ZipEncryptionMethod معرفی کرده که الگوریتمهای زیر را پوشش میدهد:ZipCrypto (سنتی/Legacy): الگوریتم کلاسیک ZIP که به دلایل ضعفهای شناختهشده رمزنگاری، استفاده از آن در پروژههای مدرن پیشنهاد نمیشود و تنها برای سازگاری با سیستمهای قدیمی تعبیه شده است.Aes128 و Aes192 و Aes256 (استاندارد WinZip AES): استانداردهای قدرتمند مبتنی بر AES که سطح امنیت سازمانی را ارائه میدهند. استاندارد AES-256 به عنوان گزینه پیشفرض و پیشنهادی برای دادههای محرمانه محسوب میشود.ReadOnlySpan و ReadOnlyMemory دریافت میکنند تا از تکثیر بیهوده رشتههای حساس در Heap (و خطرات افشای حافظه) جلوگیری شود.using System;
using System.IO;
using System.IO.Compression;
// ۱. ایجاد آرشیو فشرده رمزگذاریشده با AES-256
using (var archive = ZipFile.Open("secrets.zip", ZipArchiveMode.Create))
{
// تعریف ورودی جدید با مشخص کردن کلمه عبور و الگوریتم
ZipArchiveEntry entry = archive.CreateEntry(
entryName: "notes.txt",
password: "MySecurePassword123#",
encryptionMethod: ZipEncryptionMethod.Aes256);
// نوشتن محتوای رمزگذاریشده در جریان فایل
using Stream stream = entry.Open("MySecurePassword123#");
using var writer = new StreamWriter(stream);
writer.WriteLine("این یک متن فوق محرمانه است.");
}
// ۲. خواندن و اعتبارسنجی الگوریتم ورودی
using (var archive = ZipFile.OpenRead("secrets.zip"))
{
ZipArchiveEntry entry = archive.GetEntry("notes.txt")!;
// شناسایی خودکار روش رمزنگاری فایل
Console.WriteLine($"روش رمزنگاری: {entry.EncryptionMethod}"); // خروجی: Aes256
// رمزگشایی و استخراج محتوا
using Stream stream = entry.Open("MySecurePassword123#");
using var reader = new StreamReader(stream);
Console.WriteLine($"محتوا: {reader.ReadToEnd()}");
}CreateEntryFromFileAsync)CreateEntryFromFileAsync معرفی شده است:using System.IO;
using System.IO.Compression;
using System.Threading;
using System.Threading.Tasks;
string targetZipPath = "WarAndPeaceArchive.zip";
string sourceFilePath = "war-and-peace.txt";
ReadOnlyMemory<char> password = "LeoTolstoy123%#".AsMemory();
await using (var archive = ZipFile.Open(targetZipPath, ZipArchiveMode.Create))
{
// افزودن مستقیم فایل متنی به آرشیو همراه با رمزگذاری AES-256 و بالاترین سطح فشردهسازی
await archive.CreateEntryFromFileAsync(
sourceFileName: sourceFilePath,
entryName: "WarAndPeace.txt",
compressionLevel: CompressionLevel.SmallestSize,
password: password,
encryptionMethod: ZipEncryptionMethod.Aes256,
cancellationToken: CancellationToken.None
);
}InvalidDataException پرتاب میکند:using (var archive = ZipFile.OpenRead("secrets.zip"))
{
ZipArchiveEntry entry = archive.GetEntry("notes.txt")!;
try
{
using Stream stream = entry.Open("WrongPassword");
using var reader = new StreamReader(stream);
_ = reader.ReadToEnd();
}
catch (InvalidDataException)
{
// در استاندارد ZIP، تفکیک خرابی داده از رمز عبور اشتباه عموما به یک وضعیت ختم میشود
Console.WriteLine("خطا: کلمه عبور نامعتبر است یا ساختار فایل آسیب دیده است.");
}
}نکته امنیتی: عدم ارائه استثنای مشخصی مانندWrongPasswordExceptionو استفاده ازInvalidDataExceptionمطابق با اصول امنیتی استاندارد است تا از حملات کانال جانبی (Side-channel attacks) و حدسزدن وضعیت در سطح پایین جلوگیری شود.
System.IO.Compression در داتنت، گامی مهم در جهت استقلال پروژهها از پکیجهای شخص ثالث به شمار میرود. این ویژگی نه تنها کدنویسی را خواناتر و تمیزتر میکند، بلکه به دلیل ادغام با متدهای ناهمگام مدرن (Async/Await) و ساختارهای بهینه حافظه (Span/Memory)، عملکرد و مقیاسپذیری بالاتری در فشردهسازی دادههای محرمانه سازمانی ارائه میدهد.Stream و متد Open بررسی شد. با این حال، در سناریوهای واقعی توسعه نرمافزار، سناریوی رایجتر استخراج مستقیم فایلها از داخل آرشیو و ذخیره آنها روی دیسک (File System) است. تا پیش از این، برای استخراج مستقیم یک فایل به دیسک بدون درگیر شدن با مدیریت دستی Streamها و بافرها، مکانیزم سادهای برای تزریق کلمه عبور وجود نداشت. داتنت با معرفی کلاس تنظیمات ZipExtractionOptions و بهروزرسانی متد پرکاربرد ExtractToFileAsync، این شکاف را برطرف کرده است.ZipExtractionOptionsZipExtractionOptions را معرفی کرده است. این کلاس ویژگیهایی نظیر موارد زیر را ارائه میدهد:Password: دریافت کلمه عبور در قالب ساختار بهینه ReadOnlyMemory.ExtractToFileAsync)ZipExtractionOptions مستقیماً روی دیسک ذخیره میشوند:using System;
using System.IO;
using System.IO.Compression;
using System.Threading;
using System.Threading.Tasks;
string sourceZipPath = "WarAndPeaceArchive.zip";
string outputDirectory = Path.Combine(AppContext.BaseDirectory, "ExtractedFiles");
ReadOnlyMemory<char> password = "LeoTolstoy123%#".AsMemory();
// ایجاد پوشه خروجی در صورت عدم وجود
Directory.CreateDirectory(outputDirectory);
// ۱. باز کردن ناهمگام فایل آرشیو جهت خواندن
await using (ZipArchive archive = await ZipFile.OpenReadAsync(sourceZipPath))
{
// ۲. تنظیم گزینههای استخراج و تعریف کلمه عبور
var extractionOptions = new ZipExtractionOptions
{
Password = password,
Overwrite = true // در صورت نیاز به بازنویسی فایلهای همنام موجود
};
// ۳. پیمایش تمام ورودیها و استخراج مستقیم آنها روی دیسک
foreach (ZipArchiveEntry entry in archive.Entries)
{
string destinationPath = Path.Combine(outputDirectory, entry.FullName);
// اجرای عملیات استخراج مستقیم فایل به همراه اعتبارسنجی رمز عبور
await entry.ExtractToFileAsync(
destinationFileName: destinationPath,
options: extractionOptions,
cancellationToken: CancellationToken.None
);
Console.WriteLine($"فایل با موفقیت استخراج شد: {entry.FullName}");
}
}| قابلیت / سناریو | استفاده از entry.Open(password) | استفاده از entry.ExtractToFileAsync(..., options) |
| نوع خروجی | جریان داده (Stream) در حافظه | ذخیره مستقیم فایل فیزیکی روی دیسک |
| کاربرد اصلی | پردازش در حافظه، ارسال به پایگاه داده یا کلاینت بدون نوشتن روی هارد | دانلود یا استخراج ابزارهای نصبی، خروجیهای گزارشی و اسناد دیسکی |
| پیکربندی | دریافت مستقیم رشته یا Span | دریافت شیء ساختاریافته ZipExtractionOptions |
| پشتیبانی از بازنویسی (Overwrite) | نیازمند پیادهسازی دستی در سطح فایل | پشتیبانی توکار از طریق تنظیمات استخراج |
ZipExtractionOptions و اورلودهای جدید ExtractToFile / ExtractToFileAsync:CreateEntryFromFileAsync تا استخراج کامل روی دیسک با ExtractToFileAsync) بدون یک خط کد جانبی یا پکیج متفرقه تکمیل شده است.ReadOnlyMemory در تمامی مراحل حفظ میشود.ReadOnlyMemory و متد AsMemory()، ابتدا باید نگاهی به انقلاب مدیریت حافظه مدرن در داتنت (از زمان معرفی C# 7.2 و .NET Core 2.1 به بعد) بیندازیم. هدف اصلی معرفی این ساختارها، دستیابی به کارایی بالا (High Performance) و کاهش تخصیص حافظه (Zero-Allocation) بود.string یا آرایه:string.Substring() استفاده کنید، داتنت یک رشته جدید در حافظه Heap تخصیص داده و کاراکترها را کپی میکند. این موضوع باعث افزایش بار Garbage Collector (GC) و افت کارایی در برنامههای پربار میشود.Spanو محدودیتهای آن در کدهای ناهمگام (async)Span (و همتای فقطخواندنی آن ReadOnlySpan) را معرفی کرد.Span یک «پنجره» (View) بدون کپی روی یک تکه پیوسته از حافظه (آرایه، رشته، یا حافظه محلی Stack) است.Span یک ref struct است؛ یعنی فقط و فقط میتواند در Stack زندگی کند. بنابراین شما نمیتوانید از Span در:async / await (چون ماشین حالت async متغیرها را به Heap منتقل میکند)ReadOnlyMemory وارد میدان میشود.ReadOnlyMemoryچیست؟ReadOnlyMemory یک نمای فقطخواندنی (Read-Only) از یک تکه حافظه پیوسته است، با این تفاوت کلیدی که:struct معمولی است (نه ref struct).async و تسکهای ناهمگام کاملاً قابل استفاده است.ReadOnlyMemory همان قابلیتهای ReadOnlySpan را ارائه میدهد، اما برای دنیای متدهای ناهمگام (async/await) طراحی شده است.+-------------------------------------------------------------------+
| حافظه اصلی (String / Array) |
+-------------------------------------------------------------------+
^
| (بدون کپی داده)
+---------------------------+
| ReadOnlyMemory<char> | ---> قابل عبور از مرزهای async / await
+---------------------------+
|
v (.Span)
+---------------------------+
| ReadOnlySpan<char> | ---> جهت پردازشهای سریع محاسباتی
+---------------------------+AsMemory()چیست و چه میکند؟AsMemory() یک Extension Method فوقالعاده سریع است که روی انواع دادهای مانند string و آرایهها (T[]) تعریف شده است. این متد بدون اینکه داده را کپی کند، یک شیء ReadOnlyMemory (یا Memory) میسازد که مستقیماً به داده اصلی اشاره دارد:string password = "LeoTolstoy123%#"; // بدون ایجاد رشته جدید یا کپی کاراکترها در حافظه: ReadOnlyMemory<char> memoryPassword = password.AsMemory(); // حتی میتوانید فقط بخشی از آن را برش (Slice) بزنید: ReadOnlyMemory<char> sliced = password.AsMemory(0, 10); // ۱۰ کاراکتر اول بدون کپی!
ReadOnlyMemoryاستفاده میکنند؟await archive.CreateEntryFromFileAsync(
sourceFileName: "war-and-peace.txt",
entryName: "War And Peace",
compressionLevel: CompressionLevel.SmallestSize,
password: ZipFilePassword.AsMemory(),
encryptionMethod: ZipEncryptionMethod.Aes256,
cancellationToken: CancellationToken.None);async): متد CreateEntryFromFileAsync یک متد ناهمگام است. بنابراین نمیتوانست از ReadOnlySpan استفاده کند. استفاده از ReadOnlyMemory اجازه میدهد کلمه عبور در طول چرخه حیات Task در حافظه نگهداری شود.string (که مقداری کپی اضافه تولید میکند) نیست.ReadOnly تضمین میکند که کدهای داخلی فشردهسازی نمیتوانند محتوای بافر کلمه عبور را دستکاری کنند.| نوع داده | ذخیره در Heap؟ | قابل استفاده در متد async؟ | تخصیص حافظه (Allocation) |
string | بله | بله | ایجاد کپی در عملیات برش/دستکاری |
ReadOnlySpan | خیر (فقط Stack) | خیر | صفر (Zero-Allocation) |
ReadOnlyMemory | بله | بله | صفر (Zero-Allocation) |