رمزهای کاربرها رو امن ذخیره کنید!

مقدمه

اگر شما دولوپر باشید، حتما لازم بوده که سیستم مدیریت کاربرها رو (Authentication and user management service) بسازید. مهمترین ویژگی این سیستم اینه که پسوردها چطور مدیریت میشن. خب واضح هست که پایگاه‌داده‌هایی که اطلاعات کاربرها توشون هست خیلی زیاد مورد حمله قرار می‌گیرن، بنابراین حتما باید فکری برای مراقبت و نگه‌داری از رمزها کرده باشید! یکی از بهترین روش‌های رمز‌گذاری، استفاده از متد Salted Password Hashin یا استفاده از سالت برای رمز‌گذاری روی پسوردهاست.

ایده‌‌های خیلی زیادی (و غلط!) و مفاهیم متعددی برای توضیح رمزگذاری وجود داره که یکی از دلایل وجود این‌همه اطلاعات اشتباه بنظرم، مدیریت نادرست اطلاعات در وب‌سایت‌هاست. هش کردن رمزها یکی از همین مفاهیم ساده رمزگذاریه که متاسفانه خیلی وقت‌ها به اشتباه انجام میشه. اینجا سعی دارم تا توضیح بدم که این کار رو چطور به روش درست انجام بدیم.

توجه! اگر به این فکر میکنید که روش رمزگذاری خودتون رو پیاده کنید، بدونید که دارید اشتباه میکنید! خیلی راحت میشه همه‌چیز رو خراب کرد. حتی اگر کلاس‌های مختلف رمز‌گذاری رو هم گذروندید باز هم راه دوری نرفتید و اصلا برای اینکار تلاش نکنید.
یادتون باشه که مشکل ذخیره کردن رمزها قبلا حل شده و شما چیز جدیدی رو نمیسازید (مگر اینکه یه نابغه رمزنگاری باشید)

اگر به هر شکلی توضیح بالا رو از دست داید، لطفا برگردید و حتما بخونیدش. من اینجا توضیح نمیدم که چطور رمزگذاری خودتون رو پیاده کنید، بلکه اینجا میگم روش درست رمزنگاری چی هست.

هَش کردن رمز چی هست؟

hash("aien") = f3bab1f4f25e655834c6c0ab5189248e
hash("Aien") = 478a2428cde6daf521d8bb3ad1376f70
hash("saidi27.com") = 478a2428cde6daf521d8bb3ad1376f70

الگوریتم‌های هش، توابعی یک‌به‌یک هستند. این توابع ورودی‌هاشون رو به داده‌هایی با طول یکسان تبدیل میکنند که قابل برگشت نیستن. یعنی شما میتونید مثلا اسمتون رو به هش تبدیل کنید، اما هش اسمتون رو نمیتونید به اسمتون برگردونید. یکی از مهمترین ویژگی‌های این توابع، اینه که با هر تغییر کوچیکی، خروجی متفاوتی میدن (به مثال بالا توجه کنید). این روش خیلی خوبی برای دخیره‌کردن رمزهاست، چون ما می‌خوایم که رمزها ناخوانا باشن، و در عین حال باید بتونیم این رمزها رو چک کنیم تا به کاربر مجوزهای لازم رو بدیم.

جریان کلی رجیستر کردن کاربرها و مجوز دادن بهشون، تو سیستم‌های هش به این ترتیبه:

  1. کاربر اکانتش رو میسازه،
  2. رمزش هش میشه و توی پایگاه‌داده ذخیره میشه (هرگز نباید رمز خام (Plain Password) که هش نشده، تو پایگاه‌داده ذخیره بشه).
  3. زمانی که کاربر درخواست ورود میکنه، رمز هش شدش، با رمز هش شده‌ای که تو پایگاه‌داده ذخیره کردیم بررسی میشه.
  4. اگر هش‌ها با هم یکی بودن، کاربر اجازه ورود داره، اگر نه باید بهشون گفت که یه اشکالی هست.

تو مرحله چهارم، هیچوقت به کاربر نگید که کدوم یک از نام‌کاربری یا پسوردشون اشتباهه! چون هکرها نباید بفهمن که یوزر درستی رو وارد کردن تا پسوردش رو چک کنن.

مهمترین نکته اینه که این توابع هش‌گذاری، اصلا امن نیستن و سرعت بالایی دارن. برای هش‌کردن رمزها باید از توابع هش رمزنگاری‌شده یا Cyptographic hash functions استفاده کنید. توابعی مثل SHA256، SHA512 و RipeMD توابع رمز‌گذاری شده هستند.

خیلی ساده میشه به این فکر کرد، که فقط کافیه رمز کاربر رو تو یکی از این توابع بندازید و خروجیش رو دخیره کنید چون امن شده. باید بگم که اشتباه میکنید! روش‌های خیلی زیادی برای بازگردوندن رمزها و چک کردنشون وجود داره که این پایین چندتاشون رو توضیح میدم:

هش‌ها چطور شکسته میشن؟

دیکشنری و Brute Force Attack

Dictionary Attack

Trying ramz        : failed
Trying password    : failed
Trying amirali : failed

...

Trying p@ssword      : failed
Trying s3cr3t       : success!

Brute Force Attack

Trying aaaa : failed
Trying aaab : failed
Trying aaac : failed

...

Trying acdb : failed
Trying acdc : success!

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

دیکشنری اتک به این صورت کار میکنه که یک فایلی داریم که حاوی تعداد زیادی کلمه، واژه و رمزهای معروفیه که اکثرا کاربرها استفاده میکنن و احتمال داره که هرکدومشون به عنوان رمز استفاده بشن. هر کدوم از این کلمات هش میشن و به سرور ارسال میشن...

بروت فورس اتک سعی میکنه که ترکیب‌های مختلف و محتمل از کاراکترها رو کنار هم قرار بده و رمزهای مختلف رو بسازه. این روش از نظر منابع کامپیوتری خیلی گرون قیمته، اما در نهایت رمز رو باز میکنه. تنها روش مقابله با این روش، اینه که رمز‌ها انقدر طولانی باشن که جستجو بین محتملات رو تقریبا غیر ممکن یا انقدر پر هزینه کنن که برای کسی صرف نکنه.

هیچ روشی برای جلوگیری از این دو متد هک وجود نداره، بهترین راه در مقابلشون اینه که فقط کار رو براشون سخت کنیم.

Lookup Table

Searching: 5f4dcc3b5aa765d61d8327deb882cf99: FOUND: password5
Searching: 6cbe615c106f422d23669b610b564800:  not in database
Searching: 630bf032efe4507f2c57b280995925a9: FOUND: letMEin12
Searching: 386f43fab5d096a7a66d67c8f213e5ec: FOUND: mcd0nalds
Searching: d5ec75d5fe70d428685510fae36492d9: FOUND: p@ssw0rd!

این روش عالیه برای هک کردن رمز. ایده‌ی کلی اینه که هش رمزهارو از قبل ذخیره کنیم و این هش‌هارو با هش‌های داخل دیتابیس مقایسه کنیم.

اضافه کردن سالت (Salt)



hash("hello") = 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03

hash("hello" + "QxLUF1bgIAdeQX") = 68290787dab7259f607c8d9fa58df0ac07ec0a33d151a60127a55f429d09ad52
hash("hello" + "bv5PehSMfV11Cd") = 0a7d4f0c9b8962788d4b6b157e9e6cdf794bf5c9b790915dd24ba5ab962a3fe2
hash("hello" + "YYLmfY6IehjZMQ") = 6034fd9ad717f98425f8135fcfe9de956b7d94d885b371394ee149f8a3ce1e24

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

میتونیم رمزها رو تصادفی کنیم، به شرطی که بهشون هش‌های تصادفی رو اضافه کنیم. به این هش‌های تصادفی میگن سالت یا Salt. در واقع ما به رمز خام، یک هش رو اضافه میکنیم و نتیجه رو هش میکنیم (مثال بالا)، این کار باعث میشه که حتی با داشتن رمزهای خام ثابت، هش‌های متفاوتی رو داشته باشیم. برای اینکه رمز کاربر رو چک کنیم تا بهش اجازه ورود بدیم، باید سالتش رو که توی پایگاه‌داده ذخیره کردیم، به رمزش اضافه کنیم و نتیجه رو با رمزی که توی پایگاه داده ذخیره شده مطابقت بدیم.

سالت نیازی نداره که امن باشه. ما با اینکار صرفا رمزشکنی رو سخت‌تر میکنیم. دلیلش اینه که هکر نمیدونه که سالت ذخیره شده چیه و نمیتونه هش رو ازش دریافت کنه.

روش غلط سالت کردن: سالت‌های کوتاه و مشابه

دو روش معمول (و غلط) برای سالت‌کردن و ساختن سالت‌ها وجود داره، که یکیش استفاده از سالت‌های مشابه هست و دیگری استفاده از سالت‌های کوتاه

سالت‌های مشابه

یک اشتباه بزرگ اینه که از یک سالت برای هش‌ها استفاده کنیم! تو این روش یا اصل سالت رو تو برنامه مینویسیم (هارد کد میکنیم)  یا اینکه از یه الگوریتمی استفاده میکنیم که بالاخره یه‌جایی تکرار میشه و یک عدد تکراری رو برمی‌گردونه. این روش بدرد نمیخوره، چون اگر دو کاربر پسورد یکسانی داشته باشن، هش پسوردشون هم مثل هم میشه و هکر ها از راه جداول لوک‌آپ به پسورد دسترسی پیدا کنن. فقط کافیه سالت رو پیدا کنن و به هر رمز بچسبونن و لوک‌آپ کنن.

همیشه باید برای هر کاربر، سالت جداگانه ساخته بشه!

سالت‌های کوتاه

اگر سالت کوتاه باشه، هکر میتونه یه جدول لوک‌آپ برای سالت‌ها بسازه. مثلا اگر سالت فقط ۲تا کاراکتر اسکی (ASCII) باشه، فقط ۸۵۷هزارتا (حدودا) سالت وجود خواهد داشت که شاید به نظر زیاد بیاد ولی با فرض اینکه هر جدول لوک‌آپ هم ۱مگابایت حجم داشته باشه، حدودا ۸۳۰گیگابایت برای داشتن تمام رمز‌ها لازم داره که خب، هارد درایو با این حجم زیاد گرون نیست و هر کسی میتونه داشته باشه.

به همین دلیل، نام کاربری نباید به عنوان سالت استفاده بشه. شاید برای یه سایتی، نام‌های کاربری یکتا باشن، اما قابل پیشبینی هستند و معمولا کاربرها از یک نام‌کاربری برای چند سایت استفاده می‌کنن.

برای اینکه کار برای هکرها سخت بشه، سالت باید طولانی باشه. یک نکته خوب اینه که اندازه سالت، با اندازه رمز هش شده یکی باشه (مثلا هر دو ۳۲ بایت باشن). به عنوان مثال، SHA256 به شما ۲۵۶ بیت برمی‌گردونه (۳۲ بایت)، برای همین هم سالت باید ۳۲ بایت رندوم باشه.

روش غلط سالت کردن: سالت‌های تو در تو یا ضعیف

اینجا به روش‌های دیگه‌ای اشاره میکنم که عموما به صورت اشتباه توی سایت‌‌ها بکار میرن، به این امید که پسورد‌های امنی بسازن: هش‌های تو در تو!

خیلی راحت ممکنه به این فکر کنید که میشه توابع مختلف رو باهم استفاده کرد (توابع تو در تو) به این امید که هش بدست اومده امن باشه. با این حال در عمل تقریبا شانسی برای اینکار نیست و نتیجه بدست اومده با اونچه که شما فکر می‌کنید متفاوت خواهد بود. در واقع این روش تنها کاری که میکنه، احتمال همکاری بین متدها رو کم میکنه و حتی ممکنه هش‌هایی رو بسازه که حتی کمتر امن هستن! هیچوقت تلاش نکنید که رمزنگاری خودتون رو بسازید، همیشه از یک استاندارد استفاده کنید که توسط افراد حرفه‌ای ساخته شده.

بعضی‌ها ممکنه بگن که استفاده از روش تو در تو، زمان زیادی رو برای هش کردن ایجاد میکنه و در نتیجه پروسه شکستن و کرک کردن رمز میتونه بیشتر زمان ببره. من اینجا یک روش بهتر برای کند کردن این پروسه بهتون ارائه میدم که پایین‌تر توضیح میدم.

اینا نمونه‌هایی از هش‌‌ها و سالت‌های تودرتو هستن که نباید ازشون استفاده بشه:

  • md5(sha1(password))
  • md5(md5(salt) + md5(password))
  • sha1(sha1(password))
  • sha1(str_rot13(password + salt))
  • md5(sha1(md5(md5(password) + sha1(password)) + md5(password)))

این قسمت میتونه بحث‌برانگیز باشه، که بعضی‌ها بگن استفاده از هش‌های تودرتو خوبه چون هکر‌ها نمیدونن از چه الگوریتم‌هایی استفاده میشه و خیلی بعیده که برای رمزها لوک‌آپ داشته باشن.

این حرف درسته، هکر‌ها نمیتونن حمله کنن اگر الگوریتم‌ها رو نداشته باشن، اما نکته اینجاست که اکثر هکرها به سورس‌کد دسترسی دارن! (خصوصا اگر اوپن‌سورس باشه نرم‌افزار) و از اونجایی که الگوریتم‌های زیادی برای رمزنگاری وجود ندارن، مهندسی معکوس کردن کار خیلی سختی نخواهد بود. درسته، زمان بیشتری طی میشه تا هش‌های تودرتو شکسته بشن، ولی این زمان تفاوت خیلی زیادی با حالت عادی نمیکته (شاید در حد چند ساعت!).

اگر هدفتون اینه که از هش‌های تودرتو استاندارد مثل HMAC استفاده کنید مشکلی نیست، اما اگر صرفا هدفتون اینه که سرعت پردازش رو پایین بیارید، باید بگم که در اشتباه هستید.

روش درست سالت کردن

از اونجایی که الگوریتم‌ها هش، ورودی‌ها رو به تعداد مشخصی کاراکتر تبدیل میکنن، باید ورودی‌هایی وجود داشته باشن که به هش‌های یکسانی تبدیل بشن (اگر میخواید اطلاعات عمیق‌تری پیدا کنید این مقاله رو بخونید). توابع هش رمزنگاری شده، احتمال این یکنواختی (Hash Collision) رو بسیار کم میکنن.

روش درست هش کردن رمزها!

من اینجا سعی میکنم به طور کامل توضیح بدم که هش کردن صحیح رمزها چطور باید انجام بشه. اوایل به مسائل ساده رسیدگی و کم‌کم پیچیده‌ترش میکنم.

پایه: هش کردن با سالت

قبلا توضیح دادم که هکرها چطور میتونن هش‌های ساده رو بشکنن و رمزها رو بردارن و فهمیدیم که چطور میشه سالت رو ساخت.

سالت حتما باید توسط CSPRNGها یا (Cryptographically Secure Pseudo-Random Number Generator)ها ساخته بشن. سعی کردم فارسیش رو پیدا کنم و موفق نشدم، برای همین از اینجا به بعد بهشون CSPRNG میگم.

CSPRNGها با سازنده‌های اعداد معمولی خیلی فرق دارن (نمونش تابع rand() در زبان C). در واقع CSPRNGها ساخته شدن تا از نظر رمزنگاری امن باشن، به این معنی که خروجیشون خیلی تصادفی‌تر از توابع معمولیه و کاملا غیر قابل پیشبینی هستند. ما هم نمیخوایم که سالت‌هامون قابل پیشبینی باشن، برای همین باید از CSPRNGها استفاده کنیم.

این پایین CSPRNGهایی که برای زبان‌های مورد استفاده خودم رو مینویسم، اگر زبانتون متفاوت هست، فقط کافیه یکمی جست‌وجو کنید، یا به من بگید تا به این لیست اضافه کنم (که خیلی خوب میشه!)

CSPRNGزبانmcrypt_create_iv($size)PHPcrypto/randGolangcsprngJavaScriptSecureRandom()Java

سالت‌ها باید به ازای کاربر و رمز یکتا باشن. هربار که کاربری، حسابی رو می‌سازه یا رمزش رو بروز می‌کنه، رمزش باید توسط یک سالت جدید هش بشه. هیچوقت از یک سالت نباید چندبار استفاده بشه و باید در عین حال طول زیادی داشته باشه. (حداقل به اندازه هش پسورد)

برای ذخیره رمز کافیه که:

  1. یک سالت طولانی با استفاده از CSPRNGها بسازید
  2. سالت رو به رمز بچسبونید و حاصل رو با استفاده از یک تابع هش استاندارد مثل bcrypt هش کنید
  3. هش و سالت رو به صورت جداگانه تو پایگاه داده ذخیره کنید.

برای بررسی رمزها هم:

  1. سالت کاربر رو از پایگاه داده بخونید،
  2. سالت و رمز رو به هم بجسبونید و با همون الگوریتم قبلی هش کنید،
  3. هش ساخته شده رو با اونچه که تو پایگاه داده ذخیره کردید بررسی کنید.

اگر از وب‌اپلیکیشن‌ها استفاده می‌کنید و مشغول ساخت یکی هستید، حتما هش رو سمت سرور انجام بدید! (قابل توجه برنامه‌نویس‌های React, Vue و غیره)

اگر در حال ساخت یک وب‌اپلیکیشن هستید، شاید براتون سوال بشه که هش رو باید کجا انجام بدید؟ آیا رمز باید سمت کاربر هش بشه یا سمت سرور؟ یا اینکه باید بصورت خام یا plain سمت سرور ارسال بشه و بعد تبدیل بشه؟

حتی اگر رمز کاربر رو با جاوااسکریپت هش میکنید، حتما این کار رو سمت سرور انجام بدید! ممکنه بگید، من رمز رو سمت کاربر نگه می‌دارم و اینطوری اصلا رمزی به سرور ارسال نمیشه و اینطوری امن‌تر به نظر میاد. اما اینطور نیست!

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

هش کردن سمت کاربر خوبه، ولی حتما باید این شروط برقرار باشن:

  1. هش کردن سمت کاربر، اگر ارتباط امن نباشه (HTTPSهای SSL یا TLS) کاربردی نداره، یک واسط خیلی راحت میتونه این وسط قرار بگیره و سورس کد رو دستکاری کنه و رمز رو استفاده کنه،
  2. بعضی از مرورگرها جاوااسکریپت رو خوب پشتیبانی نمیکنن، بعضی کاربرها ممکنه جاوااسکریپت رو غیر فعال کرده باشن و مشکلات دیگه‌ای وجود داشته باشه. برای راحتی بیشتر، اپ شما باید چک کنه که اصلا میتونه اینکار رو انجام بده یا نه،
  3. هر هشی که از سمت کاربر میاد رو هم باید سالت کنید! راحتترین روش اینه که از سرور بخواید تا براتون سالت رو ارسال کنه تا شما سمت کاربر رمز و سالت رو بررسی کنید، نکنید این کار رو!

شکستن رمز رو سخت‌تر کنید

برای اینکار میشه از توابع هش کند استفاده کرد. کاری که سالت انجام میده اینه که کار رو برای هکرها از طریق جداول لوک‌آپ و رنگین‌کمان سخت‌تر میکنه، اما نمیتونه جلوی بروت فورس اتک و دیکشنری رو بگیره. امروزه کارت گرافیک‌های پیشرفته (GPU) میتونن میلیاردها هش رو در ثانیه حساب کنن، پس همچنان این حمله‌ها اثر گذار خواهند بود. برای اینکه بشه جلوی این روش ها رو تا حدود زیادی گرفت، باید از تکنیک کشش کلیدی یا Key Stretching استفاده کرد.

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

کشش کلیدی از توابع خاصی استفاده میکنه که مستقیما از سی‌پی‌یو کار میکشن. این توابع، معمولا یه فاکتور امنیتی یا تعداد تکرار رو به عنوان آرگومان دریافت میکنن. این آرگومان مشخص میکنه که این پروسه چقدر باید کند انجام بشه.

حواستون باشه که اگر از وب‌اپ استفاده می‌کنید، این روش میتونه ضربه حاصل از حملات داس (DoS یا Denial of Service) رو زیاد کنه. بنابراین فاکتور امنیتی رو باید به دقت انتخاب کنید، و بهتره تعداد پایین رو در نظر داشته باشید. یکی از کارهایی هم که میتونید بکنید، استفاده از کد کپچا (Captcha) برای هربار لاگین توسط کاربره. این میتونه جلوی حملات داس رو بگیره.

شکستن رمز رو غیر ممکن کنید!

هکرها همیشه میتونن با استفاده از بروت فورس اتک رمز رو پیدا کنن. اینجا میخوام روشی رو توضیح بدم که با استفاده از اون، هش‌ها رو فقط برای یک شخص خاص قابل استفاده میکنه.

اضافه کردن یک کلید امنیتی یا Secret Key به رمزها، باعث میشه تا رمز فقط برای شخص یا اشخاصی که به این کلید دسترسی دارن بتونن از رمز استفاده کنن. این کار با استفاده از رمزنگاری توسط سایفری مثل AES و یا اضافه کردن کلیدی به هش با استفاده از HMAC هست.

اینکار انقدرها هم که به نظر میاد ساده نیست. کلید چیزیه که حتما باید هنگام نفوذ از دست هکرها دور بمونه. اگر هکرها به کل سیستم دسترسی پیدا کنن، کلید هیچ فایده‌ای نداره و دیگه سیستم امن نیست. برای همین بهترین‌کار اینه که کلید رو مثلا تو یه پارتیشن دیگه که خ.ودش رمزنگاری شده نگه‌داریم.

من این روش رو به هر سرویسی که بیشتر از ۱۰۰هزار کاربر داره توصیه میکنم و بنظر سرویس‌هایی که بیش از یک میلیون کاربر دارن حتما باید از این روش استفاده کنن.

اگر توانایی خرید سرورهای مختلف رو ندارید، میتونید مثلا با یه اسکریپت موقع نصب سایت روی سرور و کانفیگش، یک کد تصادفی بسازید و اون رو توی یک فایل ذخیره کنید که دسترسی بهش به جز از طریق کد شما، غیر مجاز باشه.

کارهایی که حتما باید انجام بدید

  • تمام توضیحات بالا برای این هستن که اگر سایت نفوذ پیدا کرد، رمزها به سرقت نرن! یادتون باشه که باید تلاش کنید تا هکرها حتی به مرحله نفوذ هم نرسن
  • حتی برنامه‌نویس‌های حرفه‌ای هم میتونن اشتباهات امنیتی کنن. سعی کنید در مورد امنیت اطلاعات کسب کنید و دانشتون رو بالا ببرید.
  • همیشه از برنامه‌هاتون تست نفوذ بگیرید
  • سرورتون رو مانیتور کنید و مطمئن بشید که کسی مشغول دستکاری نیست

برای راحتی، یک نمونه کد هم به زبان Go نوشتم و پاسخ برخی از سوالاتی که به طور معمول برای برنامه‌نویس‌ها ایجاد میشه رو هم دادم. برای ادامه این مطلب میتونید به سایت خودم یه سری بزنید.

موفق باشید