عنوان:

‫ساده سازی روش‌های هش کردن اطلاعات در دات‌نت 9


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۲/۱۹ ۰۷:۲۳
آدرس: www.dntips.ir
هش کردن (Hashing) یکی از مفاهیم بنیادین در حوزه امنیت نرم‌افزار و برنامه‌نویسی است که برای تبدیل داده‌های ورودی به یک مقدار ثابت و غیرقابل بازگشت استفاده می‌شود. این فرآیند در حفاظت از اطلاعات حساس مانند رمزهای عبور، امضاهای دیجیتال و اعتبارسنجی داده‌ها نقش کلیدی دارد. در چارچوب دات‌نت، روش‌های هش کردن از نسخه‌های اولیه تا جدیدترین نگارش‌ها، یعنی دات‌نت 9، دستخوش تغییرات و بهبودهای قابل‌توجهی شده‌اند. این مقاله به بررسی تاریخچه روش‌های هش کردن در دات‌نت، با تمرکز بر ساده‌سازی‌های ارائه‌شده در دات‌نت 9، می‌پردازد.

مفاهیم پایه هش کردن در دات‌نت
هش کردن فرآیندی است که داده‌های ورودی با طول متغیر را به یک مقدار خروجی با طول ثابت، که به آن مقدار هش (Hash Value) گفته می‌شود، تبدیل می‌کند. این فرآیند یک‌طرفه است، به این معنا که نمی‌توان از مقدار هش به داده اولیه بازگشت. در دات‌نت، این عملیات از طریق کلاس‌های موجود در فضای نام System.Security.Cryptography پیاده‌سازی می‌شود. الگوریتم‌های رایج هش مانند MD5، SHA-1، SHA-256 و SHA-512 در نسخه‌های مختلف دات‌نت پشتیبانی شده‌اند، اما با پیشرفت فناوری، برخی از این الگوریتم‌ها مانند MD5 و SHA-1 به دلیل آسیب‌پذیری‌های امنیتی منسوخ شده‌اند.
در نسخه‌های اولیه دات‌نت، توسعه‌دهندگان برای هش کردن داده‌ها از کلاس‌های خاص الگوریتم مانند SHA256 یا SHA512 استفاده می‌کردند. این روش نیازمند کدهای تکراری برای هر الگوریتم بود. برای مثال، کد زیر نشان می‌دهد چگونه در نسخه‌های قدیمی‌تر دات‌نت، داده‌ای با استفاده از الگوریتم SHA-256 هش می‌شد:
var originalData = Encoding.Default.GetBytes("This is some data");
var hashedData = SHA256.HashData(originalData);
این کد داده‌ای متنی را به آرایه‌ای از بایت تبدیل کرده و سپس با استفاده از متد HashData از کلاس SHA256، مقدار هش را تولید می‌کند. برای استفاده از الگوریتم SHA-512، کدی مشابه با جایگزینی کلاس SHA512 نوشته می‌شد:
var originalData = Encoding.Default.GetBytes("This is some data");
var hashedData = SHA512.HashData(originalData);
این رویکرد، اگرچه ساده بود، اما برای تغییر الگوریتم نیاز به بازنویسی بخش‌هایی از کد داشت و انعطاف‌پذیری محدودی ارائه می‌داد. علاوه بر این، استفاده از کلاس‌های جداگانه برای هر الگوریتم، پیچیدگی و احتمال خطا را افزایش می‌داد.

تکامل روش‌های هش کردن تا دات‌نت 8
با پیشرفت دات‌نت، مایکروسافت بهبودهایی در کتابخانه‌های رمزنگاری (Cryptography) اعمال کرد. در نسخه‌های بعدی، مانند دات‌نت Core و دات‌نت 5 تا 8، تمرکز بر بهبود عملکرد و امنیت الگوریتم‌ها بود. برای مثال، الگوریتم‌های سری SHA-2 (مانند SHA-256 و SHA-512) به دلیل مقاومت بالاتر در برابر حملات برخورد (Collision Attacks) جایگزین MD5 و SHA-1 شدند. همچنین، امکان استفاده از داده‌های ورودی متنوع‌تر، مانند جریان داده (Stream) یا آرایه‌های بایت (Byte Arrays)، فراهم شد.
یکی از چالش‌های موجود در این نسخه‌ها، نیاز به مدیریت دستی الگوریتم‌ها و عدم وجود یک روش متمرکز برای هش کردن بود. توسعه‌دهندگان باید با توجه به نیاز پروژه، الگوریتم مناسب را انتخاب کرده و کد مربوطه را پیاده‌سازی می‌کردند. این موضوع گاهی منجر به انتخاب نادرست الگوریتم‌های ناامن یا پیاده‌سازی‌های غیربهینه می‌شد. به عنوان مثال، استفاده از MD5 برای هش کردن رمزهای عبور، که به دلیل سرعت بالا و آسیب‌پذیری در برابر حملات brute-force مناسب نیست، در برخی پروژه‌ها رایج بود.

نوآوری‌های دات‌نت 9 در هش کردن
دات‌نت 9، که در نوامبر 2024 منتشر شد، با معرفی کلاس CryptographicOperations و متدهای HashData و HashDataAsync، رویکردی متمرکز و ساده‌تر برای هش کردن ارائه کرد. این متدها امکان انتخاب الگوریتم هش را از طریق پارامتر HashAlgorithmName فراهم می‌کنند، که انعطاف‌پذیری و خوانایی کد را به طور قابل‌توجهی بهبود می‌بخشد. کد زیر نمونه‌ای از استفاده از این متد در دات‌نت 9 است:
csharp
var originalData = Encoding.Default.GetBytes("This is some data");
var token = CancellationToken.None;
var sha256HashedData = await CryptographicOperations.HashDataAsync(HashAlgorithmName.SHA256, originalData, token);
var sha512HashedData = await CryptographicOperations.HashDataAsync(HashAlgorithmName.SHA512, originalData, token);
در این کد، متد HashDataAsync با دریافت نام الگوریتم (SHA-256 یا SHA-512) و داده ورودی، فرآیند هش کردن را انجام می‌دهد. استفاده از CancellationToken نیز امکان لغو عملیات در سناریوهای ناهمگام (Asynchronous) را فراهم می‌کند. این روش نه تنها کد را تمیزتر و قابل‌نگهداری‌تر می‌کند، بلکه با کاهش نیاز به کلاس‌های جداگانه برای هر الگوریتم، احتمال خطا را کاهش می‌دهد.
علاوه بر این، دات‌نت 9 پشتیبانی از ورودی‌های متنوع مانند Stream و ReadOnlySpan<byte> را بهبود بخشیده است. این قابلیت برای سناریوهایی مانند هش کردن فایل‌های بزرگ یا داده‌های جریانی بسیار کاربردی است، زیرا امکان پردازش داده‌ها بدون نیاز به بارگذاری کامل آن‌ها در حافظه را فراهم می‌کند. برای مثال، هش کردن یک فایل بزرگ با استفاده از Stream به صورت زیر انجام می‌شود:
using var stream = File.OpenRead("largefile.txt");
var hashedData = await CryptographicOperations.HashDataAsync(HashAlgorithmName.SHA256, stream, CancellationToken.None);
این کد جریان داده فایل را مستقیماً به متد HashDataAsync ارسال کرده و مقدار هش را تولید می‌کند، که از نظر مصرف حافظه بهینه است.

ارزش افزوده دات‌نت 9 و ارتباط با سایر مفاهیم
یکی از مزایای اصلی رویکرد جدید در دات‌نت 9، کاهش پیچیدگی در انتخاب و استفاده از الگوریتم‌های هش است. این موضوع برای توسعه‌دهندگانی که با امنیت نرم‌افزار آشنا نیستند، اما نیاز به پیاده‌سازی مکانیزم‌های رمزنگاری دارند، بسیار مفید است. علاوه بر این، معرفی KMAC (Keyed Message Authentication Code) در دات‌نت 9، امکان تولید مقادیر هش با کلید را فراهم کرده است که برای اعتبارسنجی یکپارچگی داده‌ها و احراز هویت پیام‌ها کاربرد دارد. این الگوریتم، که بر اساس استاندارد NIST SP-800-185 طراحی شده، امنیت بیشتری نسبت به الگوریتم‌های سنتی ارائه می‌دهد.
ارتباط این نوآوری‌ها با سایر حوزه‌های برنامه‌نویسی نیز قابل‌توجه است. برای مثال، در توسعه وب با ASP.NET Core، هش کردن برای ذخیره امن رمزهای عبور کاربران حیاتی است. استفاده از متدهای جدید دات‌نت 9 در کنار الگوریتم‌های مقاوم مانند PBKDF2 یا Argon2 می‌تواند امنیت برنامه‌های وب را بهبود بخشد. همچنین، در برنامه‌های مبتنی بر بلاک‌چین، هش کردن برای تولید امضاهای دیجیتال و اعتبارسنجی تراکنش‌ها استفاده می‌شود، که نشان‌دهنده اهمیت این فرآیند در فناوری‌های نوین است.

نتیجه‌گیری
هش کردن داده‌ها از نسخه‌های اولیه دات‌نت تا دات‌نت 9 مسیری طولانی را طی کرده است. در حالی که نسخه‌های اولیه با کلاس‌های جداگانه برای هر الگوریتم، انعطاف‌پذیری محدودی ارائه می‌دادند، دات‌نت 9 با معرفی متدهای متمرکز HashData و HashDataAsync، فرآیند هش کردن را ساده‌تر، خواناتر و انعطاف‌پذیرتر کرده است. این بهبودها نه تنها کار توسعه‌دهندگان را آسان‌تر می‌کنند، بلکه با پشتیبانی از الگوریتم‌های امن‌تر و ورودی‌های متنوع، امنیت و عملکرد برنامه‌ها را ارتقا می‌دهند.


مشاهده مطلب اصلی

نظرات

  • شریعتی پیام در ۱۴۰۵/۰۵/۱۱ ۱۵:۲۸
    پیاده سازی Argon2 id
    نصب کتابخانه:
    dotnet add package Konscious.Security.Cryptography.Argon2
    هش کردن با Argon2 id و Verify کردن آن
    public interface IPasswordHasher
    {
        string Hash(string plainPassword);
    
        bool Verify(string plainPassword, string passwordHash);
    }

    using System.Globalization;
    using System.Security.Cryptography;
    using System.Text;
    using Konscious.Security.Cryptography;
    
    namespace ProjectName.Infrastructure.Authentication;
    
    public sealed class PasswordHasher : IPasswordHasher
    {
        /// <summary>
        /// Hash with Argon2Id
        /// </summary>
        /// <param name="plainPassword">Plain password</param>
        /// <returns>Hashed password</returns>
        public string Hash(string plainPassword)
        {
            ArgumentNullException.ThrowIfNull(plainPassword);
    
            const int saltSize = 16; // 128 bits
            const int hashSize = 32; // 256 bits
            var parallelism = GetRecommendedParallelism(); // Number of threads to use
            const int iterations = 4; // Number of iterations
            const int memorySize = 1024 * 256;
    
            var salt = new byte[saltSize];
            RandomNumberGenerator.Fill(salt);
    
            var hash = ComputeHash(plainPassword, salt, iterations, memorySize, parallelism, hashSize);
    
            return $"$argon2id$v=19$m={memorySize},t={iterations},p={parallelism.ToString(CultureInfo.InvariantCulture)}" +
                   $"${Convert.ToBase64String(salt)}${Convert.ToBase64String(hash)}";
        }
    
        public bool Verify(string plainPassword, string passwordHash)
        {
            ArgumentNullException.ThrowIfNull(plainPassword);
    
            var parts = passwordHash.Split('$', StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length != 5 || !parts[0].Equals("argon2id", StringComparison.Ordinal))
            {
                return false;
            }
    
            var paramStr = parts[2];
            var paramDict = paramStr.Split(',')
                .Select(p => p.Split('='))
                .ToDictionary(p => p[0], p => int.Parse(p[1], CultureInfo.InvariantCulture), StringComparer.Ordinal);
    
            var memory = paramDict["m"];
            var iterations = paramDict["t"];
            var parallelism = paramDict["p"];
            var salt = Convert.FromBase64String(parts[3]);
            var expectedHash = Convert.FromBase64String(parts[4]);
    
            using var argon2 = new Argon2id(Encoding.UTF8.GetBytes(plainPassword))
            {
                Salt = salt,
                MemorySize = memory,
                Iterations = iterations,
                DegreeOfParallelism = parallelism
            };
    
            var hash = ComputeHash(plainPassword, salt, iterations, memory, parallelism, expectedHash.Length);
    
            return CryptographicOperations.FixedTimeEquals(expectedHash, hash);
        }
    
        private static byte[] ComputeHash(string plainPassword, byte[] salt, int iterations, int memorySize, int parallelism, int hashSize)
        {
            using var argon2 = new Argon2id(Encoding.UTF8.GetBytes(plainPassword))
            {
                Salt = salt,
                Iterations = iterations,
                MemorySize = memorySize,
                DegreeOfParallelism = parallelism
            };
    
            return argon2.GetBytes(hashSize);
        }
    
        private static int GetRecommendedParallelism()
        {
            return Environment.ProcessorCount switch
            {
                <= 2 => 1,
                <= 4 => 2,
                <= 8 => 3,
                _ => Math.Min(4, Environment.ProcessorCount / 2)
            };
        }
    }
    در این پیاده‌سازی، هش به فرمت استاندارد زیر ذخیره می‌شود:
    $argon2id$v=19$m=262144,t=4,p=4$Salt$Hash
    در نتیجه علاوه بر خود هش، پارامترهای مورد استفاده نیز ذخیره می‌شوند. این کار باعث می‌شود اگر در آینده تصمیم گرفتید پارامترهای Argon2 را تغییر دهید، همچنان امکان Verify کردن رمزهای عبور قدیمی وجود داشته باشد؛ زیرا هنگام Verify پارامترها از روی رشته هش استخراج شده و همان مقادیر برای محاسبه مجدد استفاده می‌شوند.

    توضیح پارامترها

    • MemorySize (m): میزان حافظه‌ای است که الگوریتم برای محاسبه هش مصرف می‌کند (بر حسب KiB). هرچه این مقدار بیشتر باشد، حملات مبتنی بر GPU، FPGA و ASIC پرهزینه‌تر می‌شوند، زیرا مهاجم برای محاسبه همزمان تعداد زیادی هش به حافظه بیشتری نیاز دارد. البته افزایش این مقدار باعث می‌شود فرآیند Login نیز زمان بیشتری صرف کند و مصرف RAM سرور افزایش یابد.
    • Iterations (t): تعداد دفعات اجرای الگوریتم است. افزایش این مقدار باعث می‌شود هر بار محاسبه هش زمان بیشتری ببرد و هزینه حملات Brute Force افزایش پیدا کند.
    • DegreeOfParallelism (p): تعداد Threadهایی است که Argon2 برای پردازش استفاده می‌کند. مقدار مناسب آن معمولاً به تعداد هسته‌های CPU بستگی دارد. در این نمونه مقدار آن به صورت پویا و بر اساس Environment.ProcessorCount انتخاب شده است.
    • Salt: برای هر رمز عبور یک Salt تصادفی تولید می‌شود. این کار باعث می‌شود حتی اگر دو کاربر رمز عبور یکسانی داشته باشند، خروجی هش آن‌ها متفاوت باشد و حملاتی مانند Rainbow Table عملاً بی‌اثر شوند.

    چرا پارامترها داخل رشته هش ذخیره می‌شوند؟

    فرض کنید امروز از MemorySize = 262144 استفاده می‌کنید و یک سال بعد تصمیم می‌گیرید آن را به 524288 افزایش دهید. اگر پارامترها کنار هش ذخیره نشده باشند، دیگر نمی‌توان فهمید رمزهای قبلی با چه تنظیماتی تولید شده‌اند و Verify آن‌ها با مشکل مواجه خواهد شد.
    به همین دلیل تقریباً تمام الگوریتم‌های مدرن مانند Argon2، bcrypt و scrypt پارامترهای خود را همراه هش ذخیره می‌کنند.

    انتخاب مقادیر مناسب

    هیچ مقدار ثابتی که برای همه پروژه‌ها مناسب باشد وجود ندارد. بهتر است پارامترها را روی سخت‌افزار واقعی سرور تست کنید و به گونه‌ای انتخاب شوند که زمان محاسبه هر هش تقریباً بین 100 تا 500 میلی‌ثانیه باشد. این بازه معمولاً تعادل مناسبی بین تجربه کاربری و مقاومت در برابر حملات ایجاد می‌کند.
    همچنین با پیشرفت سخت‌افزار می‌توانید در نسخه‌های بعدی نرم‌افزار، پارامترها را افزایش دهید. از آنجا که پارامترها داخل رشته هش ذخیره شده‌اند، رمزهای قدیمی همچنان قابل Verify خواهند بود و می‌توان پس از ورود موفق کاربر، آن‌ها را با تنظیمات جدید دوباره هش و ذخیره کرد (Password Rehash).
    همچنین برای جلوگیری از حملات Timing Attack و اطمینان از مقایسه Constant-Time، از CryptographicOperations.FixedTimeEquals استفاده شده است.