Password Hashing — Authentication & Security
تجزئة كلمات المرور (Password Hashing)
قواعد البيانات تُسرَّب — حتى عند الشركات الكبرى. السؤال ليس "هل" بل "ماذا يجد المهاجم عندها". الهدف: حتى لو سُرقت القاعدة، لا يستطيع أحد استعادة كلمات المرور.
ما الخطأ في الطرق البسيطة؟
| الطريقة | المشكلة |
|---|---|
| نص صريح | كارثة: كل الحسابات مكشوفة، والناس يكررون كلمات المرور في مواقع أخرى |
| تشفير (قابل للفك) | من يملك المفتاح يفكّ الكل؛ وغالباً المفتاح على نفس الخادم |
| SHA-256 / MD5 | سريعة جداً: بطاقة رسومية تجرب مليارات المحاولات في الثانية؛ وكلمتا مرور متطابقتان تعطيان نفس الناتج |
الحل: تجزئة بطيئة مع ملح
- الملح (Salt): قيمة عشوائية فريدة لكل مستخدم تُضاف قبل التجزئة ← كلمتا مرور متطابقتان تعطيان ناتجين مختلفين، وتسقط الجداول المحسوبة مسبقاً (Rainbow Tables).
- بطء مقصود: كل محاولة تستغرق ~100–250 ملّي ثانية. لا يزعج مستخدماً يسجل دخوله، لكنه يجعل تجربة مليار كلمة مرور مستحيلة عملياً.
| الخوارزمية | ملاحظات |
|---|---|
| Argon2id | التوصية الأولى الحالية (OWASP)؛ مقاومة لبطاقات الرسوميات بالذاكرة |
| bcrypt | ممتازة ومنتشرة جداً؛ عامل التكلفة 12 تقريباً؛ تتجاهل ما بعد 72 بايتاً |
| scrypt / PBKDF2 | مقبولة (PBKDF2 بعدد تكرارات كبير جداً: 600,000 لـ SHA-256) |
bcrypt تضع الملح والتكلفة داخل الناتج نفسه ($2b$12$...)، فلا تحتاج عموداً منفصلاً للملح.
بلغة Node المدمجة (scrypt)
const crypto = require("node:crypto");
function hashPassword(password) {
const salt = crypto.randomBytes(16);
const hash = crypto.scryptSync(password, salt, 64, { N: 2 ** 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
return `${salt.toString("hex")}:${hash.toString("hex")}`;
}
function verifyPassword(password, stored) {
const [saltHex, hashHex] = stored.split(":");
const hash = crypto.scryptSync(password, Buffer.from(saltHex, "hex"), 64, { N: 2 ** 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
return crypto.timingSafeEqual(hash, Buffer.from(hashHex, "hex")); // مقارنة ثابتة الزمن
}
const stored = hashPassword("Kodax-2026!");
console.log(stored.slice(0, 40) + "…");
console.log(verifyPassword("Kodax-2026!", stored), verifyPassword("wrong", stored));timingSafeEqual يمنع استنتاج الناتج من زمن المقارنة (=== يتوقف عند أول اختلاف).
مع bcrypt (المثال الكامل)
bcrypt.hash(password, 12) و bcrypt.compare(input, hash) — المقارنة آمنة زمنياً تلقائياً. نسخة bcryptjs بلا اعتماديات أصلية (أبطأ قليلاً).
سياسة كلمات المرور الحديثة (NIST)
- الطول أهم من التعقيد: 8 أحرف حداً أدنى (12+ أفضل)، واسمح بالطويلة (64+) وبالمسافات والعربية.
- افحص كلمة المرور مقابل قوائم المسرّب (خدمة Have I Been Pwned بطريقة k-anonymity).
- لا تفرض تغييراً دورياً بلا سبب، ولا قواعد مثل "رمز + رقم + حرف كبير" إلزامياً.
- شجّع مديري كلمات المرور والمصادقة متعددة العوامل.
استعادة كلمة المرور
رمز عشوائي طويل لمرة واحدة (crypto.randomBytes(32))، خزّن تجزئته لا الرمز نفسه، صلاحية قصيرة (15–60 دقيقة)، وألغِ كل الجلسات بعد التغيير. ولا تكشف إن كان البريد مسجلاً ("إن كان الحساب موجوداً ستصلك رسالة").
تمرين
عدّل عامل تكلفة bcrypt في المثال من 10 إلى 14 وقِس الزمن لكل قيمة. اختر قيمة تعطي ~250ms على جهازك. لماذا يجب رفعها مع مرور السنين؟
const bcrypt = require('bcryptjs');
// ===== Hashing Password =====
const hashPassword = async (password) => {
// Salt rounds: كلما زاد كلما كان أبطأ وأأمن
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(password, saltRounds);
return hashedPassword;
};
// ===== Comparing Passwords =====
const verifyPassword = async (inputPassword, hashedPassword) => {
const isMatch = await bcrypt.compare(inputPassword, hashedPassword);
return isMatch;
};
// ===== Example Usage =====
async function demo() {
const password = 'mySecretPassword123';
// تشفير
const hash = await hashPassword(password);
console.log('Hash:', hash);
// $2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8...
// التحقق - صحيح
const isValid = await verifyPassword('mySecretPassword123', hash);
console.log('Valid:', isValid); // true
// التحقق - خاطئ
const isInvalid = await verifyPassword('wrongPassword', hash);
console.log('Invalid:', isInvalid); // false
}
// ===== In User Model (Mongoose) =====
const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true, select: false }
});
// Pre-save middleware
userSchema.pre('save', async function(next) {
// فقط إذا تغيرت كلمة المرور
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
// Instance method
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
const User = mongoose.model('User', userSchema);
// ===== Using in Auth =====
app.post('/register', async (req, res) => {
// كلمة المرور ستُشفَّر تلقائياً في pre-save
const user = await User.create({
email: req.body.email,
password: req.body.password
});
res.json({ success: true });
});
app.post('/login', async (req, res) => {
const user = await User.findOne({ email: req.body.email }).select('+password');
if (!user || !await user.comparePassword(req.body.password)) {
return res.status(401).json({ error: 'بيانات خاطئة' });
}
const token = generateToken(user);
res.json({ token });
});اختبر فهمك