مقاومسازی مقایسه رمزنگاری در NET.: راهنمای جامع مقایسه زمانثابت (Constant-Time Comparison)
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۵/۱۱ ۰۸:۳۵
آدرس: www.dntips.ir
چکیده: در پیادهسازی سیستمهای احراز هویت و ذخیرهسازی رمزهای عبور، مقایسه صحیح و ایمن هشها یکی از حیاتیترین حلقههای زنجیره امنیت است. متدهای معمولی مقایسه آرایهها یا رشتهها در #C (مانند عملگر==یا متدSequenceEqual) به محض یافتن اولین عدم تطابق، از ادامه پردازش انصراف میدهند. این رفتار، روزنه خطیری تحت عنوان «حمله کانال جانبی زمانبندی» (Timing Side-Channel Attack) ایجاد میکند که به مهاجم اجازه میدهد بایت به بایت مقادیر محرمانه را بازیابی کند. در این مقاله، مفهوم مقایسه زمانثابت (Constant-Time Comparison) را بررسی کرده، متدCryptographicOperations.FixedTimeEqualsدر مدرن NET. را تحلیل نموده و راهکارهای پیادهسازی استاندارد و ایمن برای نسخههای قدیمیتر (.NET Framework) را به همراه نکات پیشرفته بهینهسازی کامپایلری تشریح میکنیم.
==، SequenceEqual یا string.Equals جهت بهینهسازی سرعت و کاهش مصرف پردازنده (مفهوم Short-Circuiting) طراحی شدهاند. الگوریتم کاری آنها بدین شکل است:false برمیگردانند.false بازگردانده میشود.CryptographicOperations در فضای نام System.Security.Cryptography معرفی شد. این کلاس حاوی متدی است به نام FixedTimeEquals که مقایسه را به صورت منحصربهفرد و مقاوم در برابر حملات زمانبندی انجام میدهد.using System;
using System.Security.Cryptography;
public class PasswordHasher
{
// تنظیمات استاندارد رمزنگاری مدرن
private const int SaltSize = 32; // ۳۲ بایت نمک تصادفی (Salt)
private const int HashSize = 64; // ۶۴ بایت خروجی SHA-512
private const int Iterations = 600_000; // تعداد تکرار مناسب و سنگین
private static readonly HashAlgorithmName Algorithm = HashAlgorithmName.SHA512;
/// <summary>
/// تولید نمک و هش گذرواژه
/// </summary>
public static (byte[] Hash, byte[] Salt) HashPassword(string password)
{
byte[] salt = RandomNumberGenerator.GetBytes(SaltSize);
byte[] hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, Iterations, Algorithm, HashSize);
return (hash, salt);
}
/// <summary>
/// راستیآزمایی زمانثابت گذرواژه
/// </summary>
public static bool VerifyPassword(string password, byte[] storedHash, byte[] storedSalt)
{
// محاسبه هش گذرواژه ورودی با همان نمک و تنظیمات
byte[] computedHash = Rfc2898DeriveBytes.Pbkdf2(password, storedSalt, Iterations, Algorithm, HashSize);
// مقایسه ایمن زمانثابت - هرگز از == یا SequenceEqual استفاده نکنید
return CryptographicOperations.FixedTimeEquals(computedHash, storedHash);
}
}CryptographicOperations وجود ندارد. برای رعایت امنیت در این بسترها دو راهکار اصلی وجود دارد:using System.Runtime.CompilerServices;
public static class LegacyCryptoUtils
{
// جلوگیری از بهینهسازی کامپایلر JIT جهت تضمین عدم انصراف زودهنگام
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
public static bool FixedTimeEquals(byte[] a, byte[] b)
{
if (a == null || b == null || a.Length != b.Length)
{
return false;
}
int result = 0;
for (int i = 0; i < a.Length; i++)
{
// تفاوت بیتها باعث غیر صفر شدن result میشود
result |= a[i] ^ b[i];
}
return result == 0;
}
}نکته (کامپایلر و JIT):
در پیادهسازی دستی، کامپایلر JIT ممکن است کدهای ساده را بهینهسازی کرده یا بازنویسی کند. استفاده از ویژگی [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] تضمین میکند که JIT حلقه را سادهسازی نکرده و میانبر ایجاد نمیکند.StructuralComparisons برای مقایسه محتوایی ساختارها طراحی شده است. اگرچه تمام عناصر را پیمایش میکند، اما به دلیل هزینه overhead ناشی از Boxing و تخصیص حافظه، پیادهسازی دستی بیتبهبیت (روش اول) هم از نظر کارایی و هم شفافیت امنیتی برتری دارد.using System.Collections;
public static bool HashesEqualStructural(byte[] a, byte[] b)
{
if (a == null || b == null || a.Length != b.Length)
{
return false;
}
return StructuralComparisons.StructuralEqualityComparer.Equals(a, b);
}| روش مقایسه | مقاوم در برابر Timing Attack | پیچیدگی اجرای زمانی | پشتیبانی فریمورک |
a == b یا SequenceEqual | ❌ خیر (خروج سریع) | O(1) تا O(N) متغیر | تمام نسخهها |
CryptographicOperations.FixedTimeEquals | ✅ بله (استاندارد عالی) | O(N) کاملاً ثابت | .NET Core 2.1+ / .NET 5+ |
| پیادهسازی دستی با XOR و Bitwise OR | ✅ بله (با کنترل JIT) | O(N) کاملاً ثابت | تمام نسخهها (.NET Framework) |
StructuralComparisons | ⚠️ نسبی (غیرتخصصی) | O(N) همراه با Boxing | .NET Framework 4.0+ |
false برمیگرداند. این موضوع در اکثر سیستمها خطری ایجاد نمیکند زیرا طول هشها ثابت است. اما اگر طول هش خود یک راز باشد، باید طول نیز به صورت زمانثابت پردازش شود.Argon2id یا scrypt که در برابر حملات کارت گرافیک (GPU) و ASIC مقاوم هستند، توصیه میشود.CryptographicOperations.FixedTimeEquals استفاده کنید و در صورت نیاز به پشتیبانی از نسخههای قدیمی .NET Framework، الگوریتم زمانثابت اختصاصی را همراه با تنظیمات عدم بهینهسازی کامپایلر پیادهسازی نمایید.