عنوان:

‫پشتیبانی بومی از رمزگذاری و تعیین کلمه‌عبور در فایل‌های ZIP در دات‌نت ۱۱


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۵/۲۳ ۰۹:۴۵
آدرس: www.dntips.ir
چکیده: یکی از کمبودهای دیرینه در کتابخانه استاندارد دات‌نت (System.IO.Compression)، عدم امکان خواندن و نوشتن فایل‌های فشرده ZIP دارای کلمه‌عبور به‌صورت توکار بود؛ محدودیتی که توسعه‌دهندگان را ناچار به استفاده از پکیج‌های شخص ثالث (Third-party) مانند DotNetZip یا SharpZipLib می‌کرد. با معرفی قابلیت‌های جدید در پلتفرم دات‌نت، پشتیبانی بومی از الگوریتم‌های رمزگذاری سنتی (ZipCrypto) و استاندارد مدرن WinZip AES به این فضا افزوده شده است. این مقاله به بررسی معماری، متدهای افزوده شده نظیر OpenAsync و CreateEntryFromFileAsync، بهینه‌سازی‌های حافظه‌ای مبتنی بر ReadOnlySpan و بررسی خطاهای امنیتی مرتبط می‌پردازد.

۱. مقدمه
فرمت ZIP یکی از پرکاربردترین قالب‌های آرشیو و فشرده‌سازی داده‌ها در دنیای نرم‌افزار است. با وجود قدرت و بلوغ بالای فضای‌نام System.IO.Compression در اکوسیستم دات‌نت، توسعه‌دهندگان همواره برای فشرده‌سازی داده‌های حساس نیازمند محافظت با رمز عبور (Password Protection) بودند. تا پیش از این، موتور پایه دات‌نت تنها قادر به خواندن و نوشتن آرشیوهای بدون رمزگذاری بود و هرگونه نیاز به امنیت در سطح فایل، مستلزم تحمیل وابستگی‌های جانبی (Dependencies) خارجی بود که خود چالش‌های نگهداری و خطرات زنجیره تامین (Supply Chain) را در پی داشت. در به‌روزرسانی‌های جدید فریم‌ورک دات‌نت (از پیش‌نمایش نسخه ۱۱)، مایکروسافت با افزودن قابلیت‌های بومی به این کتابخانه، امکان تولید و خواندن ورودی‌های رمزگذاری‌شده ZIP را فراهم کرده است.

۲. بررسی فنی و متدهای جدید
۲.۱. الگوریتم‌های پشتیبانی‌شده (ZipEncryptionMethod)
دات‌نت اکنون یک اینام (Enum) جدید با عنوان ZipEncryptionMethod معرفی کرده که الگوریتم‌های زیر را پوشش می‌دهد:
  • ZipCrypto (سنتی/Legacy): الگوریتم کلاسیک ZIP که به دلایل ضعف‌های شناخته‌شده رمزنگاری، استفاده از آن در پروژه‌های مدرن پیشنهاد نمی‌شود و تنها برای سازگاری با سیستم‌های قدیمی تعبیه شده است.
  • Aes128 و Aes192 و Aes256 (استاندارد WinZip AES): استانداردهای قدرتمند مبتنی بر AES که سطح امنیت سازمانی را ارائه می‌دهند. استاندارد AES-256 به عنوان گزینه پیش‌فرض و پیشنهادی برای داده‌های محرمانه محسوب می‌شود.

۲.۲. کارایی و مدیریت حافظه
مایکروسافت در طراحی APIهای جدید، تمرکز بالایی بر کاهش تخصیص حافظه (Zero/Low Allocation) داشته است؛ از این رو متدهای باز کردن و ساخت فایل، کلمات عبور را در قالب ReadOnlySpan و ReadOnlyMemory دریافت می‌کنند تا از تکثیر بیهوده رشته‌های حساس در Heap (و خطرات افشای حافظه) جلوگیری شود.

۳. پیاده‌سازی و نمونه‌های کد
۳.۱. ایجاد فایل فشرده رمزگذاری‌شده و خواندن مجدد آن
در این سناریو، یک جریان داده با استاندارد AES-256 رمزگذاری شده و سپس خوانده می‌شود:
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)
برای فایل‌های بزرگ و عملیات‌های ناهمگام (I/O-Bound)، متد الحاقی (Extension Method) کارآمد 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
    );
}

۴. مدیریت استثناها و ملاحظات امنیتی
۴.۱. تشخیص کلمه عبور نادرست
در صورتی که کلمه عبور اشتباه وارد شود یا ساختار ورودی به دلیل خرابی داده‌ها (Data Corruption) معتبر نباشد، دات‌نت استثنای 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) و حدس‌زدن وضعیت در سطح پایین جلوگیری شود.

۵. نتیجه‌گیری
افزودن قابلیت پشتیبانی از کلمات عبور و رمزنگاری WinZip AES به فضای‌نام System.IO.Compression در دات‌نت، گامی مهم در جهت استقلال پروژه‌ها از پکیج‌های شخص ثالث به شمار می‌رود. این ویژگی نه تنها کدنویسی را خواناتر و تمیزتر می‌کند، بلکه به دلیل ادغام با متدهای ناهمگام مدرن (Async/Await) و ساختارهای بهینه حافظه (Span/Memory)، عملکرد و مقیاس‌پذیری بالاتری در فشرده‌سازی داده‌های محرمانه سازمانی ارائه می‌دهد.

نظرات

  • وحید نصیری در ۱۴۰۵/۰۵/۲۴ ۰۸:۴۵
    بخش تکمیلی: استخراج مستقیم و ناهمگام آرشیوهای رمزگذاری‌شده به دیسک

    در این مطلب، نحوه خواندن ورودی‌های رمزگذاری‌شده از طریق Stream و متد Open بررسی شد. با این حال، در سناریوهای واقعی توسعه نرم‌افزار، سناریوی رایج‌تر استخراج مستقیم فایل‌ها از داخل آرشیو و ذخیره آن‌ها روی دیسک (File System) است. تا پیش از این، برای استخراج مستقیم یک فایل به دیسک بدون درگیر شدن با مدیریت دستی Streamها و بافرها، مکانیزم ساده‌ای برای تزریق کلمه عبور وجود نداشت. دات‌نت با معرفی کلاس تنظیمات ZipExtractionOptions و به‌روزرسانی متد پرکاربرد ExtractToFileAsync، این شکاف را برطرف کرده است.

    کلاس جدیدZipExtractionOptions
    مایکروسافت برای استانداردسازی پارامترهای استخراج (مانند کلمه عبور و رفتارهای جایگزینی فایل)، شیء پیکربندی ZipExtractionOptions را معرفی کرده است. این کلاس ویژگی‌هایی نظیر موارد زیر را ارائه می‌دهد:
    • 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:
    • چرخه کامل حیات (Full Lifecycle) فایل‌های ZIP رمزگذاری‌شده (از ساخت فایل با CreateEntryFromFileAsync تا استخراج کامل روی دیسک با ExtractToFileAsync) بدون یک خط کد جانبی یا پکیج متفرقه تکمیل شده است.
    • امنیت حافظه به دلیل استفاده از ReadOnlyMemory در تمامی مراحل حفظ می‌شود.
    • تمامی فرایندها به شکل کاملاً ناهمگام (Non-blocking I/O) و مناسب برای سناریوهای با بار کاری بالا (High-Throughput) پیاده‌سازی شده‌اند.
    • وحید نصیری در ۱۴۰۵/۰۵/۲۴ ۰۸:۵۱
      برای درک عمیق 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 منتقل می‌کند)
      • فیلدهای کلاس‌های معمولی (Heap)
      • عبارات لامبدا یا جنریک‌های متداول

      اینجا دقیقاً نقطه‌ای است که ReadOnlyMemory وارد میدان می‌شود.

      ReadOnlyMemoryچیست؟
      ReadOnlyMemory یک نمای فقط‌خواندنی (Read-Only) از یک تکه حافظه پیوسته است، با این تفاوت کلیدی که:
      • یک struct معمولی است (نه ref struct).
      • می‌تواند در Heap ذخیره شود و فیلد یک کلاس باشد.
      • در متدهای 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); // ۱۰ کاراکتر اول بدون کپی!

      چرا متدهای ZIP در دات‌نت از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)