در حال بارگذاری 0%
// منو.exe
خانه رزومه بلاگ تماس سفارش
English فارسی
~/blog / security / password-hashing-salt-pepper

هش، Salt و Pepper چیست؟ چرا رمز عبور نباید رمزگذاری ساده شود؟

فرض کنید کاربری با رمز عبور BlueTrain!2026 در سایت ما ثبت‌نام می‌کند. ساده‌ترین راه این است که همان متن را در دیتابیس بنویسیم. راه کمی حرفه‌ای‌تر این است که آن را با یک کلید رمزگذاری کنیم و نسخه رمز‌شده را ذخیره کنیم. در هر دو حالت، سرور می‌تواند روزی دوباره به رمز اصلی برسد.

هش، Salt و Pepper چیست؟ چرا رمز عبور نباید رمزگذاری ساده شود؟

# اول سؤال درست را بپرسیم: چه چیزی باید در دیتابیس ذخیره شود؟

اما سؤال مهم‌تر این است: سرور اصلاً چرا باید بتواند رمز عبور کاربر را بازیابی کند؟ برای ورود فقط لازم است بفهمیم رمز واردشده درست است یا نه؛ لازم نیست مقدار اصلی آن را بدانیم. همین تفاوت کوچک، طراحی کل سیستم ذخیره رمز عبور را عوض می‌کند.
در زمان ثبت‌نام، برنامه از رمز عبور یک «مقدار قابل بررسی» یا Verifier می‌سازد و فقط همان را ذخیره می‌کند:
ثبت‌نام
Password
Password Hashing Function
Verifier
Database
هنگام ورود نیز رمز جدید از همان فرایند عبور می‌کند. اگر نتیجه با مقدار ذخیره‌شده سازگار بود، ورود پذیرفته می‌شود:
ورود
Password واردشده
محاسبه دوباره Verifier
مقایسه امن با دیتابیس
برابر است؟ ورود مجاز
در هیچ مرحله‌ای عملیات decrypt() وجود ندارد. سرور نیز نباید گزینه‌ای با عنوان «نمایش رمز فعلی کاربر» داشته باشد. اگر کاربر رمز را فراموش کرد، راه درست ساختن رمز جدید از طریق فرایند Password Reset است؛ نه ارسال رمز قبلی برای او.
نکته کلیدی: برای احراز هویت، به خود رمز عبور احتیاج نداریم؛ به مدرکی نیاز داریم که با آن بتوانیم یک حدس جدید را بررسی کنیم.

# Encryption، Hash و Password Hashing یک چیز نیستند

این سه مفهوم گاهی در گفت‌وگوهای روزمره همگی «رمزنگاری» نامیده می‌شوند، اما رفتارشان متفاوت است:
ENC
Encryption — برگشت‌پذیر، با کلید
مخفی‌کردن داده‌ای که بعداً باید دوباره خوانده شود
HASH
Hash عمومی — برگشت‌ناپذیر، بدون کلید
اثر انگشت داده، بررسی تمامیت و شناسه‌سازی
KDF
Password Hashing / KDF — کند و عمداً گران
گران‌کردن حدس‌زدن رمز عبور؛ معمولاً همراه Salt
رمزگذاری یا Encryption دوطرفه است:
encryption.txt
Plaintext + Key → Ciphertext
Ciphertext + Key → Plaintext
این ویژگی برای شماره کارت، سند محرمانه یا داده‌ای که باید دوباره خوانده شود مفید است. اما اگر رمزهای عبور را Encrypt کنیم، یک کلید وجود خواهد داشت که همه آن‌ها را به متن اصلی برمی‌گرداند. اگر مهاجم علاوه بر دیتابیس به فایل تنظیمات، Backup یا Secret مربوط به آن کلید برسد، کل مجموعه رمزها یک‌جا قابل بازیابی می‌شود.
Hash مسیر برگشت تعریف‌شده‌ای ندارد:
hash.txt
Password → Hash
Hash     → Password  
بااین‌حال جمله «هش غیرقابل برگشت است» ممکن است یک سوءتفاهم بسازد. مهاجم لازم نیست Hash را برگرداند؛ می‌تواند رمزهای احتمالی را یکی‌یکی Hash کند و خروجی‌ها را مقایسه کند:
guessing.log
حدس: 123456       → Hash → برابر نیست
حدس: password     → Hash → برابر نیست
حدس: BlueTrain!2026 → Hash → برابر است ✓
پس Hash را معمولاً Decrypt نمی‌کنند؛ آن را با حدس‌های فراوان پیدا می‌کنند.

# چرا SHA-256(password) هنوز امن نیست؟

فرض کنید این کد را برای ثبت‌نام نوشته‌ایم:
insecure_hash.py
# فقط برای نمایش مشکل
 
import hashlib
 
 
def hash_password(password: str) -> str:
    return hashlib.sha256(password.encode("utf-8")).hexdigest()
 
 
print(hash_password("123456"))
خروجی از بیرون تصادفی و طولانی به نظر می‌رسد، اما دو مشکل اساسی دارد. اول اینکه ورودی یکسان همیشه خروجی یکسان می‌سازد؛ اگر صد کاربر رمز 123456 داشته باشند، Hash هر صد نفر دقیقاً یکسان خواهد بود و مهاجم حتی پیش از پیدا کردن رمز می‌فهمد این حساب‌ها از یک رمز مشترک استفاده کرده‌اند.
مشکل دوم مهم‌تر است: SHA-256 برای سریع‌بودن طراحی شده است. سرعت برای بررسی تمامیت فایل و امضای دیجیتال مزیت است، اما در ذخیره رمز عبور به مهاجم اجازه می‌دهد حدس‌های بسیار زیادی را با هزینه کم امتحان کند. یک حمله آموزشی کوچک را ببینید:
crack_demo.py
# حمله فقط روی Hash آزمایشی خودمان
 
import hashlib
 
 
target = hashlib.sha256(b"sunshine").hexdigest()
 
guesses = [
    "123456",
    "password",
    "qwerty",
    "sunshine",
    "letmein",
]
 
for guess in guesses:
    candidate = hashlib.sha256(guess.encode("utf-8")).hexdigest()
 
    if candidate == target:
        print("Password found:", guess)
        break
$ python crack_demo.py
Password found: sunshine
ما Hash را معکوس نکردیم؛ فقط پنج حدس ساختیم و همان تابع را روی آن‌ها اجرا کردیم. مهاجم واقعی می‌تواند از فهرست رمزهای افشاشده، Dictionaryها، الگوهای رایج و سخت‌افزار موازی استفاده کند. به همین دلیل یک Hash سریع—حتی اگر از نظر رمزنگاری مانند SHA-256 شکسته نشده باشد—به‌تنهایی Password Hash مناسبی نیست.

# Salt چیست؟ یک رمز یکسان، دو خروجی متفاوت

Salt مقداری تصادفی و منحصربه‌فرد است که برای هر Hash رمز عبور تولید می‌شود و پیش از اجرای تابع Password Hashing در محاسبه وارد می‌شود:
نقش Salt
Password
+ Salt تصادفی
Password Hashing Function
Verifier
فرض کنید علی و رضا تصادفاً رمز یکسانی انتخاب کرده‌اند:
salt_demo.txt
Ali
Password = sunshine
Salt     = 8f2a...
Result   = a34c...
 
Reza
Password = sunshine
Salt     = c91d...
Result   = 77b0...
رمز هر دو sunshine است، اما چون Saltها فرق دارند، خروجی‌ها نیز متفاوت‌اند. مهاجم دیگر نمی‌تواند یک بار Hash رمز sunshine را محاسبه کند و آن را با تمام کاربران مقایسه کند؛ باید برای Salt هر رکورد محاسبه جداگانه‌ای انجام دهد. جدول‌های ازپیش‌محاسبه‌شده مانند Rainbow Table نیز ارزش قبلی خود را از دست می‌دهند.
Salt قرار نیست محرمانه باشد؛ معمولاً کنار Hash در همان ستون دیتابیس ذخیره می‌شود؛ چیزی شبیه این رشته:
encoded_verifier.txt
$argon2id$v=19$m=19456,t=2,p=1$RANDOM_SALT$PASSWORD_HASH
این رشته نام الگوریتم، نسخه، هزینه حافظه، تعداد تکرار، Parallelism، Salt و نتیجه نهایی را در خود نگه می‌دارد. وجود Salt در دیتابیس نقص امنیتی نیست؛ طراحی آن از ابتدا بر عمومی‌بودن Salt بنا شده است.
هشدار ساده‌سازی: اضافه‌کردن Salt به SHA-256 مشکل خروجی‌های تکراری را حل می‌کند، اما SHA-256 را به یک Password Hash مناسب تبدیل نمی‌کند؛ الگوریتم همچنان سریع است. Salt جلوی محاسبه از پیش و حمله هم‌زمان به چند حساب را می‌گیرد، ولی جلوی حدس‌زدن جداگانه هر رمز ضعیف را نمی‌گیرد.

# Pepper چیست؟ Saltی که نباید داخل دیتابیس باشد

Pepper نیز یک داده اضافی در محاسبه رمز عبور است، اما نقش و محل نگهداری آن با Salt فرق دارد:
Salt

برای هر رمز متفاوت، غیرمحرمانه، کنار Hash در دیتابیس ذخیره می‌شود.

عمومی + منحصربه‌فرد
Pepper

میان رکوردها مشترک، محرمانه، و هرگز کنار Hash ذخیره نمی‌شود.

محرمانه + خارج از دیتابیس
اگر مهاجم فقط یک Dump از دیتابیس یا Backup آن را بدزدد، Saltها را خواهد داشت؛ چون Salt عمومی است. اما اگر Pepper جداگانه در یک Secret Manager نگهداری شده باشد، در فایل دیتابیس وجود ندارد و مهاجم برای بررسی حدس‌هایش هنوز یک جزء ضروری را کم دارد:
storage_layout.txt
Database
  ├─ Algorithm
  ├─ Parameters
  ├─ Salt
  └─ Password Verifier
 
Secret Manager / HSM
  └─ Pepper
Pepper را نباید با عبارت ساده‌ای مانند password + pepper به رمز چسباند. یک روش استانداردتر استفاده از HMAC است؛ یعنی Pepper نقش کلید یک Hash کلیددار را بازی کند:
ترکیب Pepper و Argon2id
HMAC-SHA256 (key = Pepper)
Argon2id + Salt
Verifier
در این طراحی، HMAC یک ورودی ثابت و غیرقابل‌استفاده بدون Pepper برای Argon2id تولید می‌کند. سپس Argon2id با Salt تصادفی و هزینه محاسباتی مناسب، Verifier نهایی را می‌سازد.
اما Pepper یک سپر جادویی نیست. اگر مهاجم کل Application Server را در اختیار بگیرد، ممکن است برنامه در زمان اجرا به Pepper دسترسی داشته باشد. Pepper بیشتر برای سناریویی مفید است که دیتابیس به‌تنهایی افشا شده باشد. همچنین اگر Pepper لو برود، تعویض آن ساده نیست؛ چون برای ساخت Verifier جدید به رمز اصلی کاربران نیاز داریم و در رخداد افشای Pepper معمولاً باید کاربران را مجبور به Reset رمز کرد.
نکته عملیاتی: Pepper یک لایه Defense in Depth است، نه جایگزین Salt، الگوریتم درست، Rate Limiting یا MFA. سیستمی با Pepper و SHA-256 سریع همچنان طراحی خوبی ندارد.

# Work Factor؛ چرا الگوریتم رمز عبور عمداً باید کند باشد؟

برای کاربر واقعی، محاسبه یک Hash هنگام ورود فقط یک بار انجام می‌شود. اگر این محاسبه کسری از ثانیه زمان ببرد، تجربه کاربری تقریباً تغییری نمی‌کند. اما مهاجم آفلاین باید همان هزینه را برای تک‌تک حدس‌ها بپردازد:
کاربر واقعی

۱ ورود × هزینه Hash

قابل قبول
مهاجم

میلیون‌ها حدس × همان هزینه

گران و زمان‌بر
الگوریتم‌های مدرن فقط CPU را درگیر نمی‌کنند؛ می‌توانند مقدار قابل‌تنظیمی حافظه نیز مصرف کنند. این ویژگی Memory Hardness نام دارد. اجرای تعداد بسیار زیادی حدس موازی روی GPU یا سخت‌افزار اختصاصی، وقتی هر حدس به حافظه قابل‌توجهی احتیاج دارد، پرهزینه‌تر می‌شود.
در زمان نگارش این مقاله، انتخاب اصلی پیشنهادی برای پروژه‌های جدید Argon2id است. سه پارامتر مهم آن عبارت‌اند از:
m
Memory Cost
چه مقدار حافظه مصرف شود
t
Time Cost
چند مرحله یا Pass انجام شود
p
Parallelism
چند Lane موازی استفاده شود
راهنمای OWASP برای Argon2id حداقل پیکربندی m=19456 KiB، t=2 و p=1 را پیشنهاد می‌کند. این عدد نقطه شروع است، نه عددی مقدس برای تمام سرورها؛ تنظیمات باید روی سخت‌افزار واقعی Benchmark شوند و با رشد توان پردازشی در آینده افزایش یابند.
هدف این نیست که Login را بی‌دلیل سنگین کنیم. Work Factor بیش‌ازحد بزرگ می‌تواند مصرف CPU و RAM را بالا ببرد و حتی مسیر حمله Denial of Service بسازد. نقطه مناسب باید با اندازه سیستم، تعداد ورودها، ظرفیت سرور و Rate Limiting هماهنگ شود.

# بیایید یک سیستم واقعی با Argon2id بسازیم

برای نمونه عملی از کتابخانه argon2-cffi استفاده می‌کنیم. این کتابخانه Salt تصادفی را برای هر Hash می‌سازد و تمام اطلاعات لازم برای Verify را در قالب استاندارد رشته خروجی قرار می‌دهد:
terminal
$ python -m pip install argon2-cffi
ابتدا یک Pepper تصادفی ۳۲ بایتی تولید می‌کنیم و برای اجرای آزمایشی در متغیر محیطی قرار می‌دهیم:
generate_pepper.sh
$ python -c "import base64,secrets; print(base64.b64encode(secrets.token_bytes(32)).decode())"
 
# Linux / macOS
$ export PASSWORD_PEPPER="BASE64_VALUE_HERE"
 
# Windows PowerShell
$env:PASSWORD_PEPPER="BASE64_VALUE_HERE"
در محیط Production: متغیر محیطی فقط یک مثال ساده است. Pepper بهتر است در سرویس مدیریت Secret یا HSM نگهداری و در زمان اجرا با کنترل دسترسی مناسب دریافت شود؛ نه در سورس‌کد، Git، Docker Image یا همان دیتابیس کاربران.
فایل زیر یک پیاده‌سازی کامل برای ساخت Hash، بررسی ورود و ارتقای تنظیمات قدیمی است:
password_store.py
from __future__ import annotations
 
import base64
import binascii
import hashlib
import hmac
import os
 
from argon2 import PasswordHasher
from argon2.exceptions import InvalidHashError, VerificationError
from argon2.low_level import Type
 
 
# نقطه شروع مطابق حداقل فعلی OWASP؛
# در Production روی سخت‌افزار واقعی Benchmark شود.
PASSWORD_HASHER = PasswordHasher(
    time_cost=2,
    memory_cost=19 * 1024,  # KiB = 19 MiB
    parallelism=1,
    hash_len=32,
    salt_len=16,
    type=Type.ID,
)
 
 
def load_pepper() -> bytes:
    encoded = os.environ.get("PASSWORD_PEPPER")
 
    if encoded is None:
        raise RuntimeError("PASSWORD_PEPPER is not configured")
 
    try:
        pepper = base64.b64decode(encoded, validate=True)
    except (binascii.Error, ValueError) as error:
        raise RuntimeError("PASSWORD_PEPPER must be valid Base64") from error
 
    if len(pepper) < 32:
        raise RuntimeError("PASSWORD_PEPPER must contain at least 32 random bytes")
 
    return pepper
 
 
PEPPER = load_pepper()
 
 
def pepper_password(password: str) -> str:
    """Create a keyed, fixed-length input for Argon2id."""
 
    digest = hmac.new(
        key=PEPPER,
        msg=password.encode("utf-8"),
        digestmod=hashlib.sha256,
    ).digest()
 
    return base64.b64encode(digest).decode("ascii")
 
 
def hash_password(password: str) -> str:
    """Return the encoded Argon2id verifier stored in the database."""
 
    return PASSWORD_HASHER.hash(pepper_password(password))
 
 
def verify_password(stored_hash: str, password: str) -> tuple[bool, str | None]:
    """
    Return (valid, upgraded_hash).
 
    upgraded_hash is not None when the password is correct but
    the stored Argon2 parameters are older than the current policy.
    """
 
    candidate = pepper_password(password)
 
    try:
        PASSWORD_HASHER.verify(stored_hash, candidate)
    except (VerificationError, InvalidHashError):
        return False, None
 
    if PASSWORD_HASHER.check_needs_rehash(stored_hash):
        return True, PASSWORD_HASHER.hash(candidate)
 
    return True, None
 
 
if __name__ == "__main__":
    password = "BlueTrain!2026"
    stored_hash = hash_password(password)
 
    print("Stored:", stored_hash)
    print("Correct:", verify_password(stored_hash, password)[0])
    print("Wrong:", verify_password(stored_hash, "wrong-password")[0])
خروجی نمونه چیزی شبیه این خواهد بود؛ Salt و بخش Hash در هر اجرا تغییر می‌کنند:
output
$ python password_store.py
 
Stored: $argon2id$v=19$m=19456,t=2,p=1$...$...
Correct: True
Wrong: False
چند اتفاق مهم داخل این کد رخ می‌دهد:
01
HMAC-SHA256 با Pepper
رمز را به ورودی کلیدداری برای Argon2id تبدیل می‌کند
02
PasswordHasher.hash()
برای هر رمز یک Salt تصادفی جدید می‌سازد
03
رشته استاندارد خروجی
الگوریتم، پارامترها، Salt و Verifier در همان $argon2id$...
04
Verify هنگام Login
کتابخانه Salt و تنظیمات را از رشته می‌خواند و به‌شکل امن بررسی می‌کند
05
check_needs_rehash()
تشخیص می‌دهد Hash با سیاست جدید سرور قدیمی شده است یا نه
نکته مهم: برنامه ما Pepper را داخل خروجی ذخیره نمی‌کند. اگر Pepper را کنار password_hash در همان جدول بگذاریم، لایه امنیتی اضافه‌شده عملاً از بین می‌رود.

# چرا یک رمز یکسان هر بار Hash متفاوتی می‌سازد؟

این آزمایش را به انتهای فایل اضافه کنید:
same_password_test.py
first = hash_password("same-password")
second = hash_password("same-password")
 
print(first)
print(second)
print("Different:", first != second)
 
print("First valid:", verify_password(first, "same-password")[0])
print("Second valid:", verify_password(second, "same-password")[0])
$argon2id$...$SALT_A$HASH_A
$argon2id$...$SALT_B$HASH_B
Different: True
First valid: True — Second valid: True
در نگاه اول عجیب است: اگر Hashها متفاوت‌اند، برنامه چگونه رمز را تأیید می‌کند؟ پاسخ داخل همان رشته است. هر رکورد Salt خودش را نگه می‌دارد. کتابخانه هنگام Verify آن Salt را استخراج می‌کند، رمز واردشده را با همان تنظیمات دوباره پردازش می‌کند و نتیجه را مقایسه می‌کند.
جمله کلیدی: Salt مخفی نیست؛ منحصربه‌فرد است. Pepper منحصربه‌فرد نیست؛ مخفی است. ترکیب این دو نقش متفاوت، نکته اصلی طراحی است.

# اگر تنظیمات Argon2 در آینده عوض شود چه می‌شود؟

قدرت سخت‌افزار ثابت نمی‌ماند. تنظیمی که امروز هزینه مناسبی دارد ممکن است چند سال بعد ارزان شده باشد. به همین دلیل نام الگوریتم و Work Factor باید همراه Hash ذخیره شوند. هنگام ورود موفق، برنامه هنوز رمز خام را فقط در حافظه درخواست فعلی دارد؛ همان لحظه بهترین فرصت برای ارتقاست:
ارتقای خودکار هنگام Login موفق
Login موفق
Hash قدیمی است؟
بله → ساخت Hash جدید → Update دیتابیس
این همان کاری است که check_needs_rehash() در نمونه انجام می‌دهد. اگر مقدار دوم خروجی تابع verify_password() یک رشته بود، Application باید آن را جایگزین Hash قدیمی کاربر کند. کاربرانی که دیگر Login نمی‌کنند خودکار ارتقا پیدا نمی‌کنند؛ بسته به حساسیت سیستم، می‌توان Hashهای بسیار قدیمی را منقضی کرد و هنگام مراجعه بعدی، Password Reset اجباری خواست.

# با Pepper قدیمی چه کنیم؟

برای Pepper بهتر است یک نسخه غیرمحرمانه نیز کنار رکورد نگه داریم:
users_table.txt
password_hash  = $argon2id$...
pepper_version  = 2
در یک Rotation برنامه‌ریزی‌شده، برنامه می‌تواند Pepper نسخه قدیمی را برای مدت محدود بشناسد و پس از Login موفق، Hash را با Pepper جدید بازسازی کند. اما اگر Pepper قدیمی افشا شده باشد، نگه‌داشتن طولانی آن مسئله را حل نمی‌کند و حساب‌هایی که هنوز مهاجرت نکرده‌اند باید Reset اجباری داشته باشند.
این یکی از تفاوت‌های مهم Salt و Pepper است: Salt بدون داشتن رمز قابل تعویض نیست، ولی نیازی هم به تعویض محرمانه ندارد؛ چون Secret نیست. Pepper Secret است و رخداد افشای آن یک عملیات امنیتی واقعی می‌خواهد.

# اگر سیستم قدیمی SHA-256 یا MD5 دارد، چطور مهاجرت کنیم؟

Hash قدیمی را نمی‌توان به‌صورت مستقیم به Argon2id تبدیل کرد؛ چون رمز اصلی داخل آن نیست. بهترین مسیر مهاجرت معمولاً هنگام Login رخ می‌دهد:
1
تشخیص — نوع Hash رکورد را مشخص کن
2
Verify قدیمی — رمز واردشده را با روش قدیمی بررسی کن
3
Hash جدید — با Argon2id + Salt + Pepper بساز
4
جایگزینی — Hash جدید را ذخیره کن
5
قطع مسیر قدیمی — دیگر روش قدیمی را نپذیر
می‌توان برای Hashهای قدیمی یک Prefix یا ستون password_scheme داشت:
schemes.txt
legacy-sha256$...
$argon2id$v=19$m=19456,t=2,p=1$...
پیچیدن Argon2 دور Hash قدیمی ممکن است به‌عنوان یک لایه موقت هزینه حمله را بیشتر کند، اما جای مهاجرت واقعی با رمز اصلی را نمی‌گیرد. حساب‌های غیرفعال نیز بهتر است پس از یک مهلت مشخص مجبور به Reset شوند.

# این طراحی در برابر کدام حمله‌ها مقاومت می‌کند؟

هیچ جدول Password Hash را نباید با برچسب «غیرقابل هک» معرفی کرد. امنیت به چیزی بستگی دارد که مهاجم توانسته به دست بیاورد:
DB
فقط دیتابیس
Saltها عمومی‌اند؛ Argon2id حدس‌ها را گران می‌کند و Pepper جداگانه یک مانع اضافه است
DB+P
دیتابیس + Pepper
حمله آفلاین ممکن می‌شود، اما هنوز هزینه Argon2id برای هر حدس پرداخت می‌شود
SRV
Application Server فعال
Pepper و رمزهای هنگام Login ممکن است در دسترس باشند؛ پاک‌سازی رخداد و تعویض Secret لازم است
PWD
رمز ضعیف و تکراری کاربر
Salt آن را قوی نمی‌کند؛ احتمال حدس همچنان بالاست
ONL
حمله آنلاین روی فرم Login
Hash به‌تنهایی کافی نیست؛ Rate Limiting، MFA و تشخیص رفتار مشکوک لازم‌اند
Password Hashing عمدتاً برای زمانی طراحی می‌شود که فایل Hashها به دست مهاجم افتاده و او می‌خواهد بدون ارتباط با سرور، حدس‌های آفلاین انجام دهد. برای حمله آنلاین، کنترل‌های دیگری لازم است.

# اشتباه‌های رایجی که کل طراحی را خراب می‌کنند

ذخیره متن اصلی: هر خواندن غیرمجاز دیتابیس، Backup، Log یا پنل مدیریت به افشای مستقیم رمزها تبدیل می‌شود.
mistake_1.txt
password = "BlueTrain!2026"  
رمزگذاری برگشت‌پذیر رمز عبور: برای احراز هویت عادی نیازی به بازیابی رمز نیست و یک کلید مرکزی نیز نقطه شکست بزرگی می‌سازد.
mistake_2.txt
AES(password, one_master_key)  
استفاده از Hash سریع: حتی SHA256(salt + password) نیز عمداً کند و Memory-hard نیست.
mistake_3.txt
SHA256(password)  
MD5(password)     
SHA1(password)    
Salt ثابت برای تمام کاربران: Salt باید برای هر Hash تصادفی و منحصربه‌فرد باشد؛ ثابت و عمومی‌بودن هم‌زمان، مزیت اصلی Salt را حذف می‌کند.
mistake_4.txt
GLOBAL_SALT = "my-site-2026"  
ذخیره Pepper کنار Hash: در این حالت هر مهاجمی که دیتابیس را بگیرد، هر دو جزء را دارد.
mistake_5.txt
users.password_hash
users.pepper  
نوشتن الگوریتم رمزنگاری اختصاصی: ترکیب چند Hash، جابه‌جایی کاراکترها یا Base64 کردن، الگوریتم Password Hashing نمی‌سازد؛ از کتابخانه‌های شناخته‌شده و فرمت‌های استاندارد استفاده کنید.
ثبت رمز در Log: ممکن است دیتابیس کاملاً درست طراحی شده باشد، اما Middleware، Analytics، Error Tracker یا Debug Log بدنه فرم Login را ذخیره کند. رمز عبور نباید وارد Log، Trace، Crash Report یا پیام خطا شود.
فراموش‌کردن TLS، Rate Limiting و MFA: Argon2id از Hash افشاشده محافظت می‌کند؛ جلوی شنود اتصال ناامن، فیشینگ، Credential Stuffing یا میلیون‌ها درخواست آنلاین را به‌تنهایی نمی‌گیرد. فرم ورود همچنان به HTTPS، محدودیت تلاش، هشدار ورود مشکوک و برای حساب‌های حساس به MFA نیاز دارد.

# نسخه نهایی جریان ثبت‌نام و ورود

حالا می‌توانیم تمام قطعات را کنار هم قرار دهیم:
REGISTER
Password
HMAC-SHA256 با Pepper محرمانه
Argon2id با Salt تصادفی و Work Factor
Encoded Verifier در Database
LOGIN
Password واردشده
HMAC-SHA256 با همان Pepper
Argon2id با Salt و پارامترهای ذخیره‌شده
Verify امن + Rehash در صورت نیاز
هر قطعه یک مشکل متفاوت را حل می‌کند:
HSH
Hash / KDF
نیاز به نگهداری رمز اصلی را حذف می‌کند
SLT
Salt
رمزهای یکسان را از هم جدا می‌کند
WF
Work Factor
هر حدس مهاجم را گران می‌کند
MEM
Memory Cost
موازی‌سازی گسترده حمله را پرهزینه‌تر می‌کند
PPR
Pepper
یک Secret خارج از دیتابیس اضافه می‌کند
RH
Rehash
امکان ارتقای امنیت در آینده را می‌دهد

# رمز عبور را نگه نمی‌داریم؛ امکان بررسی آن را نگه می‌داریم

تفاوت اصلی میان یک سیستم ضعیف و یک طراحی درست در پیچیدگی ظاهری Hash نیست. یک رشته ۶۴ کاراکتری SHA-256 ممکن است بسیار فنی به نظر برسد، اما اگر سریع، بدون Salt و قابل بررسی با میلیون‌ها حدس باشد، ظاهر پیچیده آن کمکی نمی‌کند.
طراحی درست از یک تصمیم ساده شروع می‌شود: رمز اصلی کاربر چیزی نیست که برنامه بعداً به آن احتیاج داشته باشد. فقط باید بتوانیم درست یا غلط بودن ورودی جدید را بررسی کنیم. Salt باعث می‌شود دو رمز یکسان اثر یکسان نگذارند. Argon2id هزینه هر حدس را بالا می‌برد. Pepper بخشی از فرایند را از دیتابیس بیرون می‌برد. و ذخیره الگوریتم و Work Factor اجازه می‌دهد سیستم با گذشت زمان ارتقا پیدا کند.
مهم‌ترین جمله شاید همین باشد: Hash رمز عبور گاوصندوقی نیست که کلیدش را گم کرده‌ایم. یک آزمون عمداً پرهزینه است که فقط می‌گوید حدس جدید با رمز قبلی سازگار است یا نه.
takeaway.txt
رمز عبور را Encrypt نمی‌کنیم تا بعداً آن را بازیابی کنیم؛ بلکه آن را همراه با یک Salt و با استفاده از یک الگوریتم Password Hashing به یک Verifier تبدیل می‌کنیم.

Salt عمومی و منحصربه‌فرد است، در حالی که Pepper محرمانه بوده و خارج از دیتابیس نگهداری می‌شود.

امنیت واقعی نیز از ترکیب صحیح و هماهنگ این اجزا به دست می‌آید.

# مقالات مرتبط