এই পর্বে যা শিখবেন
- কেন plaintext বা ফেরত পড়া যায় এমন password রাখা চলে না
- Salt, pepper আর ধীর hash প্রত্যেকটি ঠিক কোন বিপদ ঠেকায়, কোনটি ঠেকায় না
- PBKDF2, bcrypt, scrypt আর Argon2id-এর পার্থক্য, OWASP-এর মান আর নিজের server-এ cost মাপা
- পুরোনো hash লগইনের সময় আপগ্রেড করা, আর bcrypt-এর ৭২ byte সীমা সামলানো
- Database ফাঁস হলে কী করবেন, ফাঁস হওয়া তালিকা নিরাপদে মিলিয়ে দেখবেন কীভাবে
সীমারেখা: এই পর্ব রক্ষকের জন্য। এখানে শুধু নিজের system-এর password রাখার নিয়ম আর খরচের তুলনা আছে। অন্যের hash বা ফাঁস হওয়া ডেটা নিয়ে পরীক্ষা করা অনুমতি ছাড়া অপরাধ হতে পারে। পর্ব ০৬-এর নিয়ম এখানেও প্রযোজ্য।
১ · Server-এর password জানার দরকারই নেই
একটি কাল্পনিক অনলাইন বাজারের কথা ভাবুন। দশ লাখ গ্রাহক সেখানে হিসাব খুলেছেন। Developer প্রথম দিনে সবচেয়ে সহজ কাজটি করলেন: users টেবিলে একটি password কলাম, আর গ্রাহক যা লিখলেন, তা-ই সেখানে। এটিকে বলে plaintext, মানে পড়া যায় এমন খোলা লেখা। লগইনে দুটি লেখা মেলালেই কাজ শেষ।
এবার ভাবুন, একদিন একটি পুরোনো backup ভুল জায়গায় পড়ে রইল। তখন কী হলো? শুধু এই বাজারের হিসাব নয়। মানুষ একই password বহু জায়গায় ব্যবহার করেন। তাই আপনার ভুলের দাম দেবে গ্রাহকের email, ফেসবুক, এমনকি অফিসের হিসাবও।
মোবাইল ব্যাংকিংয়ের এজেন্টের কথা ভাবুন। তিনি আপনার PIN জানেন না, জানার কথাও নয়। সিস্টেম শুধু বলে দেয় PIN মিলেছে কি না। অফিসের রেজিস্টার খাতায় সবার আলমারির চাবির নম্বর লিখে রাখলে খাতাটি একবার চুরি হলেই সব আলমারি খোলা। ভালো নিয়ম হলো, খাতায় এমন কিছু থাকবে, যা দিয়ে শুধু “এই চাবি ঠিক কি না” যাচাই করা যায়, চাবি বানানো যায় না।
Login-এর প্রশ্ন একটিই: “এই ব্যক্তি যা লিখলেন, তা কি আগের লেখার সমান?” এই প্রশ্নের উত্তর দিতে আসল লেখা ফেরত পাওয়ার দরকার নেই। তাই password রাখা হয় এক দিকের password hash হিসেবে। এটি পর্ব ০৩-এর least privilege-এর সহজ প্রয়োগ: যে তথ্য দরকার নেই, তা হাতে না রাখাই সবচেয়ে ভালো সুরক্ষা।
Encryption করলে হবে না কেন?
অনেকে ভাবেন, “তাহলে password encrypt করে রাখি।” কিন্তু encryption ফেরত পড়ার জন্যই বানানো। একে বলা হয় reversible encryption. Login-এর সময় server-কে key হাতে রাখতেই হয়। তাহলে key সাধারণত database-এর কাছাকাছি থাকে, আর দুটি একসঙ্গে ফাঁস হলে সব password একসঙ্গে খোলা। কর্মী, backup আর debug tool সবার জন্য একটি বাড়তি পথ খুলে যায়। “Password ভুলে গেলে আমরা email-এ পাঠিয়ে দেব” যদি কোনো সেবা বলে, বুঝবেন তারা password ফেরত পড়তে পারে। সেটিই সমস্যার লক্ষণ।
Log-ও একটি database. Login request-এর body যদি log-এ চলে যায়, তাহলে সুন্দর hash করেও লাভ নেই। ২০১৯ সালে Meta জানায়, কিছু ব্যবহারকারীর password অভ্যন্তরীণ system-এ plaintext অবস্থায় ছিল। ২০২৪ সালে আয়ারল্যান্ডের Data Protection Commission এই ঘটনায় €৯১ মিলিয়ন জরিমানা ঘোষণা করে। প্রতিষ্ঠানটির বক্তব্য ছিল, বাইরের কারো কাছে সেগুলো যায়নি। শিক্ষা হলো, শুধু database নয়, password যেসব পথে যায়, পুরো পথটি দেখতে হয়: reverse proxy, application log, error tracker আর crash dump.
২ · দ্রুত hash কেন ভুল হাতিয়ার
Hash function এক দিকের ছাপ দেয়। এর মৌলিক কথা পর্ব ০৯-এর বিষয়, আর গভীর আলোচনা আছে Cryptography basics-এ। তাহলে মনে হতে পারে, SHA-256(password) রাখলেই তো হলো। ফাইলের ছাপের জন্য SHA-256 চমৎকার। Password-এর জন্য নয়। কারণটি বুঝতে হলে আগে বিপদটি বুঝতে হবে।
যে বিপদ ঠেকাচ্ছি: ফাঁসের পরে অফলাইনে অনুমান
দুই রকম অনুমান আছে। Online guessing-এ কেউ আপনার login পাতায় বারবার চেষ্টা করেন। সেখানে আপনার হাতে নিয়ন্ত্রণ আছে: rate limit, lockout, MFA. কিন্তু database ফাঁস হলে খেলাটি বদলে যায়। ফাঁস হওয়া কপি অন্যের হাতে, তখন আর আপনার server-এ আসতে হয় না। প্রতিটি অনুমানের ফল নিজের মেশিনেই মেলানো যায়, আপনি log-এও কিছু দেখবেন না। একে বলে offline guessing.
Hash উল্টো করা যায় না, কিন্তু অনুমান করে মেলানো যায়। তাই মানুষের বাছা password-এর নিরাপত্তা নির্ভর করে দুটি সংখ্যার ওপর: password কত অনিশ্চিত (entropy), আর একটি অনুমান পরীক্ষার দাম কত। প্রথমটি ব্যবহারকারীর হাতে। দ্বিতীয়টি আপনার হাতে।
বাড়ির তালার কথা ভাবুন। তালা খুলতে যদি ৩ সেকেন্ড লাগে, মালিকের কিছু আসে যায় না, তিনি দিনে একবার খোলেন। কিন্তু চোরের হাতে হাজারটি চাবি থাকলে প্রতিটি পরীক্ষায় ৩ সেকেন্ড মানে পুরো রাত ফুরিয়ে যাওয়া। ইচ্ছাকৃত ধীর hash ঠিক এই অসমতা কাজে লাগায়: বৈধ ব্যবহারকারী একবার দাম দেন, অনুমানকারীকে দিতে হয় কোটি বার। পর্ব ০৩-এর work factor-এর ধারণা এটিই।
| প্রশ্ন | দ্রুত hash (SHA-256) | Password hash (Argon2id) |
|---|---|---|
| কীসের জন্য বানানো | বড় ডেটার ছাপ, সই, অখণ্ডতা | কম entropy-র গোপন লেখা রাখা |
| গতি | যত দ্রুত, তত ভালো | ইচ্ছাকৃতভাবে ধীর, সেটিই বৈশিষ্ট্য |
| Memory | খুব কম | নিয়ন্ত্রণযোগ্য, অনেক বেশি |
| Cost বাড়ানো যায়? | না | হ্যাঁ, হার্ডওয়্যারের সঙ্গে তাল রেখে |
৩ · Salt: একই password, আলাদা ছাপ
Salt ছাড়া hash-এ দুটি সমস্যা থাকে। প্রথমত, একই password-এর ছাপ সবসময় একই। হাজার জন একই password নিলে টেবিলে হাজারটি একই সারি, দেখলেই বোঝা যায়। দ্বিতীয়ত, জনপ্রিয় password-এর ছাপ আগে থেকে হিসাব করে টেবিলে রাখা যায়, পরে শুধু খুঁজে নিলেই চলে। এমন টেবিলের একটি পরিচিত রূপ rainbow table.
Salt হলো প্রতিটি হিসাবের জন্য আলাদা একটি এলোমেলো মান। Password-এর সঙ্গে মিশিয়ে hash করা হয়, আর hash-এর পাশেই খোলা অবস্থায় রাখা হয়।
মরিচ বাটলে আর আস্ত মরিচ ফেরত পাওয়া যায় না, hash-ও তেমন। এবার ভাবুন, প্রতিটি বাড়ির রান্নায় আলাদা এক মুঠো মসলা পড়ে। এক বাড়ির তরকারির স্বাদ মুখস্থ করে অন্য বাড়িরটা চেনা যায় না। Salt আগে থেকে তৈরি “স্বাদের খাতা” অকেজো করে দেয়, কারণ প্রতিটি হিসাবের জন্য খাতাটি নতুন করে লিখতে হতো।
Salt গোপন নয়, এটি মনে রাখা জরুরি। Salt একটি অনুমানকে সহজ বা কঠিন করে না। সে যা করে তা হলো, এক ধাক্কায় সবাইকে যাচাই করার সুবিধা কেড়ে নেওয়া। NIST SP 800-63B (Revision 4, ২০২৫) ন্যূনতম ৩২ bit salt চায়। RFC 9106 Argon2-র জন্য ১২৮ bit সুপারিশ করে। সাধারণত ১৬ byte নিরাপদ এলোমেলো উৎস থেকে নিলে সমস্যা থাকে না।
নিচের ডায়াগ্রামে signup আর login-এর পুরো পথ দেখুন। মানগুলো আসল PBKDF2 হিসাব, ডেমোর জন্য কম cost আর ছোট salt নিয়ে।
1receive password over TLS2salt = secure_random(8)3hash = pbkdf2(password, salt)4store(user, salt, cost, hash)5login: load salt and cost6recompute, compare_digest
| user | |
| password | |
| salt | |
| hash |
- Password এলো। ব্রাউজার থেকে TLS দিয়ে এনক্রিপ্ট হয়ে server-এ এলো
nodi-ilish-brishti-cha. এটি শুধু memory-তে থাকে, log-এ যায় না। - Salt তৈরি। Rahima-র জন্য নিরাপদ এলোমেলো উৎস থেকে নতুন salt:
9f2c4a71…. ডেমোতে ৮ byte, বাস্তবে ১৬ byte নেওয়া ভালো। - ধীর hash. Password আর salt মিলে ফল এলো
7ca38906…. ডেমোতে PBKDF2 মাত্র ১০০০ ঘুরেছে। বাস্তবের মান অনেক বেশি, অংশ ৫-এ দেখবেন। - রাখা হলো, মুছে ফেলা হলো। Database-এ যায় user, salt, cost আর hash. Password আর কোথাও থাকে না।
- Karim-ও একই password নিলেন। কিন্তু তাঁর salt
41e07bd2…, তাই hashcef9351f…. একই password, সম্পূর্ণ আলাদা ছাপ। টেবিল দেখে কেউ বুঝবেন না দুজনের password এক। - সঠিক login. Server rahima-র সারি থেকে salt আর cost পড়ল, টাইপ করা password দিয়ে আবার হিসাব করল। ফল
7ca38906…, রাখা মানের সমান, লগইন সফল। - ভুল login. এক অক্ষর বেশি দিতেই ফল
dd6f9fbc…, কাছাকাছিও নয়। বার্তায় বলা হয় না username ভুল না password ভুল, যাতে কোন হিসাব আছে তা বোঝা না যায়।
একই কাজ এবার আসল কোডে। Python-এর hashlib.pbkdf2_hmac আর hmac.compare_digest ব্যবহার করে দেখুন।
import hashlib, hmac
ITER = 1000 # শুধু ডেমোর জন্য কম; বাস্তবের মান অংশ ৫-এ
def make_hash(password, salt_hex):
salt = bytes.fromhex(salt_hex)
return hashlib.pbkdf2_hmac("sha256", password.encode(), salt, ITER, 16)
def verify(password, salt_hex, stored):
fresh = make_hash(password, salt_hex)
return hmac.compare_digest(fresh, stored) # constant-time তুলনা
# আউটপুট মেলানোর জন্য salt স্থির; বাস্তবে secrets.token_bytes(16)
same_pw = "nodi-ilish-brishti-cha"
db = {}
for user, salt in [("rahima", "9f2c4a71d03e5b86"),
("karim", "41e07bd2c9a5163f")]:
db[user] = (salt, make_hash(same_pw, salt))
print(f"{user:7} salt={salt} hash={db[user][1].hex()}")
print("same hash for same password?", db["rahima"][1] == db["karim"][1])
salt, stored = db["rahima"]
print("login, correct password :", verify(same_pw, salt, stored))
print("login, one extra letter :", verify(same_pw + "a", salt, stored))আউটপুটে দুই ব্যবহারকারীর সারি: rahima … hash=7ca38906d9ccfa724f72073b0d4c9bf2 আর karim … hash=cef9351f7a3adf2bb726f35c988c00d4. তারপর same hash for same password? False, login, correct password : True আর login, one extra letter : False. Constant-time compare সব byte মিলিয়ে তবেই উত্তর দেয়, তাই উত্তরের সময় দেখে কতটুকু মিলেছে তা বোঝা যায় না। সাধারণ == প্রথম অমিলেই থেমে যেতে পারে।
৪ · ধীর আর memory-খেকো: PBKDF2, bcrypt, scrypt, Argon2id
ধীর করার প্রথম উপায় হলো একই হিসাব বহুবার ঘোরানো। একে বলে key stretching. কিন্তু শুধু ঘোরালেই সব মেটে না। অনেক core-ওয়ালা GPU আর বিশেষ চিপ একসঙ্গে বহু হিসাব চালাতে পারে, যদি প্রতিটি হিসাবে memory কম লাগে। তাই পরের ধাপ হলো memory-hard নকশা।
ধরুন তালা খুলতে হলে একটি বিশাল টেবিলে ছড়ানো নকশা দেখে মেলাতে হয়। মালিকের একটিই টেবিল, একটিই তালা। হাজার তালা একসঙ্গে পরীক্ষা করতে হলে হাজারটি বিশাল টেবিল লাগবে। কর্মী বাড়ানো সস্তা হতে পারে, কিন্তু টেবিলের জায়গা সস্তা নয়। Memory-hard hash-এর যুক্তি এটিই: CPU-র কাজ ভাগ করা যায়, memory-র দাম ফাঁকি দেওয়া কঠিন।
| নাম | সময় ও উৎস | Cost কীভাবে | কখন |
|---|---|---|---|
| PBKDF2 | ২০০০ সালের PKCS #5 (RFC 2898), এখন RFC 8018 | iteration সংখ্যা; memory-hard নয় | FIPS-140 (মার্কিন সরকারি মান) মানার শর্ত থাকলে |
| bcrypt | ১৯৯৯, Provos আর Mazières | cost প্রতি ১ বাড়লে কাজ দ্বিগুণ; সামান্য memory | পুরোনো system, যেখানে নতুনগুলো নেই |
| scrypt | ২০০৯, Percival; পরে RFC 7914 | N, r, p; memory-hard | Argon2id না থাকলে |
| Argon2id | Password Hashing Competition ২০১৫-এর বিজয়ী পরিবার; RFC 9106 (২০২১) | m, t, p; memory-hard | নতুন system-এ প্রথম পছন্দ |
একটি বিষয় খেয়াল করুন। ভালো password hash তার ফলাফলের সঙ্গে নিজের নিয়মও লিখে রাখে: কোন algorithm, কত cost, কোন salt. একে self-describing বা নিজের পরিচয় বহনকারী record বলা যায়। NIST-ও cost আর scheme-এর তথ্য রাখতে বলে, যাতে পরে নতুন নিয়মে যাওয়া যায়। নিচের কোডে দেখুন record কীভাবে নিজেই যাচাইয়ের সব তথ্য বহন করে।
import hashlib, hmac, unicodedata
def norm(pw): # NFC: একই লেখা, একই bytes
return unicodedata.normalize("NFC", pw).encode()
def encode(pw, salt, n, r, p):
key = hashlib.scrypt(norm(pw), salt=salt, n=n, r=r, p=p, dklen=16)
return f"scrypt${n}${r}${p}${salt.hex()}${key.hex()}"
def verify(pw, record):
_, n, r, p, salt, key = record.split("$")
fresh = hashlib.scrypt(norm(pw), salt=bytes.fromhex(salt),
n=int(n), r=int(r), p=int(p), dklen=16)
return hmac.compare_digest(fresh.hex(), key)
salt = bytes.fromhex("c04e91aa6b27d358") # ডেমোর জন্য স্থির
# ডেমোর জন্য কম cost; বাস্তবের ন্যূনতম মান অংশ ৫-এর টেবিলে
for n in (2**10, 2**12):
rec = encode("amar-sonar-bangla-1971", salt, n, 8, 1)
mib = 128 * n * 8 / 2**20 # scrypt-এর memory = 128 x N x r bytes
print(f"N={n:5} memory={mib:.0f} MiB")
print(" ", rec)
rec = encode("amar-sonar-bangla-1971", salt, 2**10, 8, 1)
print("record carries its own cost:", verify("amar-sonar-bangla-1971", rec))
# একই বাংলা অক্ষর য়, দুই রকম Unicode গঠন
one, two = "য়", "য়"
print("raw strings equal?", one == two)
print("bytes before NFC :", len(one.encode()), len(two.encode()))
print("after NFC login :", verify(two, encode(one, salt, 2**10, 8, 1)))N= 1024 memory=1 MiB আর N= 4096 memory=4 MiB: N চার গুণ হলে memory-ও চার গুণ। Record দুটি scrypt$1024$8$1$c04e91aa6b27d358$4c7febe8… আর scrypt$4096$8$1$…$9021e996…, দুটিতেই cost লেখা, তাই record carries its own cost: True. পরের অংশটি বাংলা-লেখা password-এর জন্য জরুরি। “য়” অক্ষরটি দুই রকম Unicode গঠনে আসতে পারে: raw strings equal? False, bytes before NFC : 3 6. চোখে এক, bytes-এ আলাদা। ফোনের keyboard একভাবে আর কম্পিউটারের keyboard আরেকভাবে পাঠালে ব্যবহারকারী নিজের password দিয়েও আটকে যেতেন। NIST hash করার আগে Unicode normalization (NFC) করতে বলে, তাই after NFC login : True.
আরও গভীরে: Argon2-র তিন রূপ আছে। Argon2d memory-তে ডেটা-নির্ভর পথে যায়, আর Argon2i ডেটা-নিরপেক্ষ পথে, যা side-channel-এর ঝুঁকি কমায়। RFC 9106 জানায়, Argon2id প্রথম pass-এর প্রথম অর্ধেকে Argon2i-র মতো, বাকিটা Argon2d-র মতো চলে, তাই দুটি সুবিধাই মেলে। Password Hashing Competition ২০১৩ থেকে ২০১৫ পর্যন্ত চলেছিল, আর এই খোলা প্রতিযোগিতা Kerckhoffs-এর নীতিরই চর্চা। Algorithm গোপন নয়, পরীক্ষিত।
৫ · কোন parameter, আর নিজের server-এ cost মাপা
নিচের মানগুলো OWASP Password Storage Cheat Sheet থেকে নেওয়া, ২০২৬ সালের অক্টোবরে মিলিয়ে দেখা। এই সংখ্যাগুলো সময়ের সঙ্গে বদলায়, তাই আসল পাতা দেখে নেবেন। মনে রাখবেন, এগুলো ন্যূনতম মান, লক্ষ্য নয়।
| Algorithm | OWASP-এর মান | মন্তব্য |
|---|---|---|
| Argon2id | m=19456 KiB (19 MiB), t=2, p=1 | সমমানের বিকল্প: m=46 MiB, t=1, p=1; m=12 MiB, t=3, p=1 |
| scrypt | N=217, r=8, p=1 | 128 × N × r = 128 MiB memory |
| bcrypt | work factor ন্যূনতম 10 | শুধু পুরোনো system-এ; সীমা ৭২ byte |
| PBKDF2-HMAC-SHA256 | 600,000 iteration | FIPS-এর শর্ত থাকলে |
| PBKDF2-HMAC-SHA512 | 220,000 iteration | ওই একই ক্ষেত্রে |
উৎসভেদে সংখ্যা আলাদা হতে পারে, আর সেটি স্বাভাবিক। RFC 9106-এর দুটি সুপারিশ হলো t=1, p=4, m=2 GiB, আর memory কম থাকলে t=3, p=4, m=64 MiB. OWASP-এর ন্যূনতম মান এর চেয়ে অনেক ছোট, কারণ দুটি আলাদা প্রশ্নের উত্তর: একটি “নিরাপদ শুরু কোথায়”, অন্যটি “সম্ভব হলে কতটা”।
Cost কীভাবে ঠিক করবেন
- আপনার আসল production-এর মতো মেশিনে মাপুন, নিজের laptop-এ নয়।
- ন্যূনতম মান থেকে শুরু করে cost বাড়ান, যতক্ষণ peak সময়ে login ধীর না হয়। OWASP-এর সাধারণ নিয়ম: একটি hash হিসাবে এক সেকেন্ডের কম লাগা উচিত। অনেক দল এর চেয়ে কম সময় বেছে নেন, যেমন ২০০ থেকে ৫০০ ms. এটি কোনো মান নয়, চর্চা।
- Server-এর ক্ষমতা হিসাব করুন। প্রতিটি login CPU আর memory খায়, তাই ধীর hash নিজেই denial of service-এর পথ খুলতে পারে। Login পথে rate limit আর সর্বোচ্চ একসঙ্গে-চলা hash-এর সীমা রাখুন।
- Cost record-এ লিখে রাখুন, আর হার্ডওয়্যার বদলালে আবার মাপুন।
খরচের হিসাব: ধীর hash রক্ষকের অবস্থান কীভাবে বদলায়
সূত্রটি সহজ। E bit entropy-র password-এ সম্ভাব্য মান 2Eটি। গড়ে অর্ধেকে মিলবে ধরলে সময় = 2E−1 ÷ হার। এখানে দুটি হার শুধু ধরে নেওয়া: দ্রুত hash-এ সেকেন্ডে 1010টি, ধীর hash-এ 103টি। আসল হার hardware আর parameter-ভেদে অনেক বদলায়। এই তুলনার উদ্দেশ্য অনুপাত দেখা।
পূর্ণ উদাহরণ (৪০ bit): 239 = 549,755,813,888. দ্রুত hash-এ 549,755,813,888 ÷ 1010 ≈ ৫৫ সেকেন্ড। ধীর hash-এ ÷ 103 ≈ 5.5 × 108 সেকেন্ড ≈ ১৭.৪ বছর। একই password, শুধু আপনার বাছা algorithm বদলানোয় এক মিনিট থেকে সতেরো বছর।
// সব হার ধরে নেওয়া মান, শুধু দুই hash-এর খরচ তুলনার জন্য।
// কোনো আসল hardware বা আসল algorithm-এর মাপ নয়।
const FAST = 1e10; // দ্রুত hash: প্রতি সেকেন্ডে পরীক্ষা (assumed)
const SLOW = 1e3; // ধীর hash: প্রতি সেকেন্ডে পরীক্ষা (assumed)
function human(sec) {
if (sec < 1) return "<1 sec";
if (sec < 3600) return (sec / 60).toFixed(1) + " min";
if (sec < 86400) return (sec / 3600).toFixed(1) + " hours";
if (sec < 86400 * 365.25) return (sec / 86400).toFixed(1) + " days";
return (sec / (86400 * 365.25)).toExponential(1) + " years";
}
console.log("Average cost to exhaust half of the space");
console.log("entropy".padEnd(9) + "fast hash".padEnd(16) + "slow hash");
for (const bits of [20, 30, 40, 50, 60, 72]) {
const half = 2 ** (bits - 1);
console.log((bits + " bit").padEnd(9) +
human(half / FAST).padEnd(16) + human(half / SLOW));
}
console.log("slowdown factor:", (FAST / SLOW).toExponential(0));
// Salt-এর প্রভাব: একটি অনুমান ১০ লাখ ব্যবহারকারীর ওপর যাচাই করতে কত hash?
const users = 1000000;
console.log("hashes per guess, no salt :", 1);
console.log("hashes per guess, unique salt:", users);
// রক্ষকের নিজের খরচ: peak সময়ে কত CPU core আর memory লাগবে?
const logins = 100, hashSec = 0.25, memMiB = 19; // ধরে নেওয়া মান
const busy = logins * hashSec;
console.log("server cores busy at peak:", busy);
console.log("server memory for hashing:", busy * memMiB, "MiB");টেবিলের সারি পড়ুন: 40 bit 0.9 min 1.7e+1 years, 50 bit 15.6 hours 1.8e+4 years, আর 60 bit 1.8e+0 years 1.8e+7 years. slowdown factor: 1e+7 মানে হিসাবের দিক থেকে যেন প্রতিটি password-এ প্রায় ২৩ bit entropy যোগ হলো, কারণ log2(107) ≈ ২৩.৩. কিন্তু প্রথম সারিটিও দেখুন: 20 bit <1 sec 8.7 min. ধীর hash দুর্বল password-কে শক্ত বানায় না, শুধু সময় কেনে। Salt-এর লাইন দুটি (1 বনাম 1000000) দেখায়, ১০ লাখ হিসাবে একটি অনুমান মেলাতে খরচ ১০ লাখ গুণ বাড়ে। শেষে রক্ষকের দাম: server cores busy at peak: 25 আর server memory for hashing: 475 MiB. ধীর hash বিনা মূল্যে আসে না, এই খরচ আপনি জেনেশুনে দিচ্ছেন।
৬ · Pepper: database-এর বাইরে রাখা গোপন চাবি
Salt গোপন নয়। তাহলে এমন কিছু কি রাখা যায়, যা database ফাঁস হলেও ফাঁস হবে না? Pepper সেটিই। এটি সবার জন্য একটি (বা কয়েকটি) গোপন key, যা database-এর বাইরে রাখা হয়। NIST SP 800-63B “pepper” শব্দটি ব্যবহার করে না, তবে একই ধারণায় গোপন key দিয়ে বাড়তি একটি keyed hash বা encryption ধাপের কথা বলে, আর key আলাদা রাখতে বলে।
ব্যাংকের লকারের কথা ভাবুন। লকারে দুটি চাবি লাগে: একটি গ্রাহকের, একটি ব্যাংকের। শুধু খাতা চুরি করলে কিছু হবে না, কারণ ব্যাংকের চাবি অন্য জায়গায়। Pepper হলো ব্যাংকের চাবি, আর এর মূল্য পুরোপুরি নির্ভর করে একটি শর্তে: এটি খাতার সঙ্গে এক জায়গায় থাকবে না।
Password কোথায় কী থাকে
Trust boundaries: browser, app server, database, secret store
Key management: pepper-এর আসল কাজ এখানেই
- কোথায় থাকবে: database-এর বাইরে, secret vault বা HSM-এ। OWASP আর NIST দুটোই আলাদা রাখতে বলে। Source code বা repository-তে নয়।
- কীভাবে বানাবেন: নিরাপদ এলোমেলো উৎস থেকে। মানুষের বানানো লেখা pepper নয়।
- Version রাখুন: প্রতিটি record-এ pepper-এর version লিখুন। না হলে পরে কোন key দিয়ে যাচাই হবে, তা জানা যাবে না।
- হারালে বিপদ: Pepper হারালে সব hash অকেজো, সবাইকে reset করতে হবে। তাই এর backup-ও আলাদা, নিরাপদ জায়গায় চাই।
পরিবর্তনের প্রশ্নটি কঠিন। Pepper দুইভাবে লাগানো যায়: hash করার আগে password-এ, অথবা hash-এর ফলাফলে HMAC দিয়ে। OWASP সতর্ক করে, এভাবে লাগানো pepper ব্যবহারকারীর password না জেনে বদলানো যায় না, ফাঁস হলে সবার reset লাগতে পারে। অন্য পথ হলো hash-এর ফলাফল key দিয়ে encrypt করা, যা key বদলের সময় সবটা নতুন key-তে আবার encrypt করতে দেয়। Dropbox ২০১৬ সালের প্রকৌশল-লেখায় নিজেদের এমন নকশার কথা জানিয়েছিল: bcrypt-এর ফল AES-256 দিয়ে encrypt, পরিবর্তনযোগ্য pepper হিসেবে। এটি একটি প্রকাশিত উদাহরণ, সবার জন্য নিয়ম নয়। এখানে একটিমাত্র সঠিক উত্তর নেই, trade-off আছে।
Pepper salt বা ধীর hash-এর বদলি নয়। OWASP নিজেই বলে, এটি শুধু বাড়তি স্তর, একা কোনো সুরক্ষা দেয় না। আর application server পুরো দখল হলে pepper-ও ঝুঁকিতে, কারণ server-কেই সেটি ব্যবহার করতে হয়। HSM ব্যবহার করলে key বাইরে আসে না, তাই ঝুঁকি কমে, শেষ হয় না। এটি defense in depth-এর একটি স্তর।
৭ · পুরোনো hash আপগ্রেড আর bcrypt-এর ৭২ byte সীমা
আপনার system হয়তো দশ বছরের পুরোনো, আর টেবিলে MD5 বা SHA-1 আছে। সবার password আপনার জানা নেই, তাই একবারে বদলানো সম্ভব নয়। তাহলে উপায়? লগইনের মুহূর্তটি। ওই একটি মুহূর্তেই password server-এর হাতে আসে।
1r = load(user)2ok = verify(password, r)3if not ok: reject4if r.alg or r.cost is old:5 r = new_hash(password)6 save(r)7start_session()
| user | |
| alg | |
| cost | |
| salt | |
| hash |
- Record আনা হলো। Rahima-র record পুরোনো:
sha256, ছোট salt7ac1, hashd101a208…. এখানে দেখানো মান ডেমো, যার পুরোটা আসল হিসাব থেকে নেওয়া আর ছোট করে দেখানো। - পুরোনো পদ্ধতিতে যাচাই। Record-এ যা লেখা, সেই নিয়মেই হিসাব হলো। ফল
d101a208…, মিলেছে। ভুল password হলে এখানেই থামত, record বদলাত না। - নীতি মিলছে? Record বলছে
sha256, নীতি বলছেscrypt. তাই আপগ্রেড দরকার। - নতুন hash. নতুন salt
c04e91aa…নিয়ে একই password দিয়ে scrypt হিসাব হলো। ফল4c7febe8…. পুরোনো salt ফেলে দেওয়া হয়, salt পুনর্ব্যবহার করা হয় না। - সংরক্ষণ। Record একসঙ্গে একবারে বদলানো হলো, যাতে অর্ধেক লেখা অবস্থায় হিসাব নষ্ট না হয়। পুরোনো মানের backup-ও একসময় মুছতে হবে, না হলে দুর্বল hash সেখানে রয়ে যায়।
- পরের login. এবার record-এ
scrypt,N=1024. নীতির সঙ্গে মিলেছে, তাই আপগ্রেড নেই, শুধু লগইন। - যিনি আর আসেন না। Nabil অনেক দিন আসেননি, তাঁর সারি এখনো
sha256, hashb5361a5d…. OWASP-ও বলে, ফিরে না এলে এমন hash আপগ্রেড হয় না। তাই একটি শেষ তারিখ দরকার: এরপর পুরোনো hash মুছে দিয়ে ফিরলে reset করতে বলা।
এবার চারটি খেলনা record-এ নীতিটি চালিয়ে দেখুন। এখানে toyHash real নয়, শুধু ধারণার জন্য। আসল সিস্টেমে library-র needs_rehash ধরনের সুবিধা থাকলে সেটিই ব্যবহার করবেন।
// খেলনা সিমুলেটর: toyHash real নয়, শুধু ধারণার জন্য
function toyHash(pw, salt, cost) {
let h = 2166136261;
for (let i = 0; i < cost; i++)
for (const c of salt + pw) h = Math.imul(h ^ c.charCodeAt(0), 16777619) >>> 0;
return h.toString(16).padStart(8, "0");
}
let s = 7; // স্থির seed, তাই ফল একই থাকে
const newSalt = () => ((s = Math.imul(s, 48271) >>> 0) & 0xffff).toString(16);
const TARGET = { alg: "slow", cost: 1024 };
const rec = (alg, cost, pw) => { const salt = newSalt();
return { alg, cost, salt, hash: toyHash(pw, salt, cost) }; };
const db = {
rahima: rec("fast", 1, "pw-rahima"),
karim: rec("slow", 256, "pw-karim"),
sadia: rec("slow", 1024, "pw-sadia"),
nabil: rec("fast", 1, "pw-nabil"),
};
function login(user, pw) {
const r = db[user];
if (toyHash(pw, r.salt, r.cost) !== r.hash)
return console.log(`${user}: wrong password, record unchanged`);
const stale = r.alg !== TARGET.alg || r.cost < TARGET.cost;
if (stale) db[user] = rec(TARGET.alg, TARGET.cost, pw); // password এখন হাতে আছে
console.log(`${user}: ok` + (stale ? `, upgraded ${r.alg}/${r.cost} -> slow/1024` : ", already current"));
}
login("rahima", "pw-rahima"); login("karim", "wrong-guess");
login("karim", "pw-karim"); login("sadia", "pw-sadia");
const left = Object.keys(db).filter(u => db[u].cost < TARGET.cost);
console.log("still on old hash:", left.join(", "));
for (const u in db) console.log(u.padEnd(7), db[u].alg, db[u].cost, db[u].salt, db[u].hash);Rahima সঠিক লগইনে আপগ্রেড হলেন: rahima: ok, upgraded fast/1 -> slow/1024. Karim-এর ভুল password-এ karim: wrong password, record unchanged, তারপর সঠিক password-এ karim: ok, upgraded slow/256 -> slow/1024. Sadia আগেই ঠিক ছিলেন: sadia: ok, already current. আর শেষে still on old hash: nabil: তাঁর সারি nabil fast 1 ea47 281c6e7e অপরিবর্তিত। এই তালিকাটিই আপনার মাপকাঠি, কত শতাংশ record এখনো পুরোনো।
পুরোনো দুর্বল hash-কে সরাসরি নতুন ধীর hash-এর ভেতরে মুড়ে রাখার একটি অন্তর্বর্তী পথও আছে, যাতে অপেক্ষার সময়েও সবাই ধীর স্তরে থাকেন। OWASP এটিকে সাময়িক ধরে, আর পরের login-এ সরাসরি password-এর hash-এ ফিরতে বলে। এতে বাড়তি জটিলতা আছে, আর জোড়া লাগানো অংশ নিজে পরীক্ষা না করে চালানো ঠিক নয়।
bcrypt-এর ৭২ byte সীমা
bcrypt ইনপুটের ৭২ byte পর্যন্তই ব্যবহার করে। Byte, অক্ষর নয়। ইংরেজিতে এক অক্ষর এক byte. কিন্তু UTF-8-এ একটি বাংলা অক্ষর সাধারণত ৩ byte. তাই মাত্র ২৪টি বাংলা অক্ষরেই (24 × 3 = 72) সীমা শেষ, আর বাকি অংশ কোনো কাজে আসে না। কিছু library চুপচাপ বাকি অংশ ফেলে দেয়, কিছু error দেয়। আপনার library কী করে, তা পরীক্ষা করে জেনে নিন।
| উপায় | ভালো দিক | সতর্কতা |
|---|---|---|
| নতুন system-এ Argon2id বা scrypt | সীমাটি আর থাকে না | সবচেয়ে পরিষ্কার পথ |
| সর্বোচ্চ ৭২ byte জোর করা | সহজ, OWASP-এর পরামর্শ | গণনা byte-এ করতে হবে, অক্ষরে নয় |
| আগে hash করে bcrypt-এ দেওয়া | লম্বা password চলে | OWASP একে বিপজ্জনক বলে, কারণ hash-এর ফলে null byte (মান 0 বিশিষ্ট byte) থাকতে পারে, আর অন্য জায়গায় ফাঁস হওয়া দ্রুত hash এই স্তরকে দুর্বল করতে পারে (password shucking) |
প্রশ্ন আসে, password-এর সর্বোচ্চ দৈর্ঘ্য রাখা কি ঠিক? রাখা দরকার, তবে খুব ছোট নয়। NIST SP 800-63B অন্তত ৬৪ অক্ষর পর্যন্ত অনুমতি দিতে বলে। কোনো সীমাই না রাখলে খুব বড় ইনপুট দিয়ে server ব্যস্ত করা যায়। দৈর্ঘ্যের নীতির বিস্তারিত পর্ব ১৫-এর বিষয়।
৮ · ফাঁস হলে কী মানে, আর কী করবেন
একই ঘটনা, একই চুরি হওয়া টেবিল। কিন্তু ক্ষতি নির্ভর করে আপনি আগে কী বেছেছিলেন তার ওপর। নিচে দুটি হিসাবই example-pass-1 password নিয়েছে। ছয় রকম সংরক্ষণে ফাঁস হওয়া ফাইলটি কেমন দেখায় আর তার অর্থ কী, তা দেখুন।
rahima example-pass-1 karim example-pass-1
rahima 6f1d09c2b7a4e035
karim 6f1d09c2b7a4e035
app.conf: DB_KEY=example-key
rahima 79f87c98a8c83899… karim 79f87c98a8c83899…
rahima a1f0 5c25871686ef3f52… karim 6d3e bef66de507b2b404…
rahima scrypt$1024$8$1$c04e…$687adb62… karim scrypt$1024$8$1$5be1…$f4c4bec5…
rahima scrypt$…$c04e…$b202dd5d… p3
pepper p3 is in the vault, not here
- Plaintext. সবচেয়ে খারাপ। ফাইল পড়লেই সব password, সঙ্গে অন্য সেবার ঝুঁকি। সময় হাতে নেই। সবাইকে এখনই reset, সবাইকে জানানো।
- Reversible encryption. Key ফাইলের কাছেই থাকলে plaintext-এর সমান। IV (প্রতিবার আলাদা শুরুর মান) ছাড়া হলে একই password-এর encrypt করা ফল বা ciphertext-ও একই, তাই এখানে দুটি সারি হুবহু মিলে গেছে। এখানকার মান কাল্পনিক। ব্যবস্থা plaintext-এর মতোই।
- দ্রুত hash, salt নেই। দুই সারি মিলে গেল, তাই বোঝা গেল দুজনের password এক। আগে-বানানো টেবিল কাজে লাগে আর অনুমানের দাম খুব কম। ধরে নিন সাধারণ password খুব দ্রুত উন্মুক্ত হবে।
- দ্রুত hash, salt আছে। সারি আলাদা, আগে-বানানো টেবিল অকেজো। কিন্তু প্রতিটি অনুমান এখনো সস্তা। এক ধাপ ভালো, যথেষ্ট নয়। Reset করতেই হবে।
- ধীর hash, salt আছে। প্রতিটি অনুমানের বড় দাম দিতে হয়, প্রতিটি হিসাবের জন্য আলাদাভাবে। দুর্বল password তবুও ঝুঁকিতে, শক্তিশালীগুলো বাস্তবে নিরাপদ। সময় পাওয়া গেছে, reset ধাপে ধাপে করা যায়। তবু ব্যবহারকারীকে জানাতে হবে।
- ধীর hash + pepper. শুধু database ফাঁস হলে pepper ছাড়া একটি অনুমানও যাচাই করা যায় না। কিন্তু এটি নিশ্চিত হতে হবে যে pepper ফাঁস হয়নি। প্রমাণ না থাকলে ধরে নিন হয়েছে।
| ব্যবস্থা | যা ঠেকায় | যা ঠেকায় না |
|---|---|---|
| Salt | আগে-বানানো টেবিল, একই password চেনা | একজনের ওপর সরাসরি অনুমান |
| ধীর hash | দ্রুত offline অনুমান | খুব দুর্বল password-এর ঝুঁকি পুরোপুরি |
| Pepper | শুধু database ফাঁস হলে অনুমান | Application server দখল |
| পুরোটাই | টেবিল চুরির ক্ষতি | Phishing, keylogger, অন্য সাইট থেকে ফাঁস হওয়া একই password |
কীভাবে জানবেন ফাঁস হয়েছে?
ফাঁস অনেক সময় নীরব। তাই নজরদারি দরকার: database থেকে অস্বাভাবিক বড় export, backup bucket-এ অচেনা access, আর অনেক হিসাবে অল্প সময়ে ব্যর্থ login, যা credential stuffing-এর লক্ষণ হতে পারে। ২০১৩ সালে Juels আর Rivest honeyword প্রস্তাব করেন: প্রতিটি হিসাবে কয়েকটি নকল password-ছাপ রাখা, যার কোনোটি দিয়ে login চেষ্টা হলে বোঝা যায় টেবিল বাইরে গেছে। এগুলোর log আর alert পর্ব ৫৫-এর বিষয়।
ফাঁসের পরে করণীয়
- থামান আর প্রমাণ রাখুন। ফাঁসের পথ বন্ধ করুন, log সংরক্ষণ করুন। Incident management প্রক্রিয়া চালু করুন, বিস্তারিত পর্ব ৬১-তে।
- কী গেছে তা বের করুন। কোন টেবিল, কত record, কোন hash, pepper কি নিরাপদ? উপরের ছয়টি ধাপের কোনটিতে আপনি আছেন, সেটিই পরের সিদ্ধান্ত ঠিক করে।
- Key আর session বদলান। Pepper বা key-র ওপর সন্দেহ থাকলে নতুন key, আর সক্রিয় session বাতিল।
- বাধ্যতামূলক reset. NIST নিয়মিত সময় পর পর password বদলাতে বাধ্য করা নিষেধ করে, কিন্তু ঝুঁকির প্রমাণ থাকলে বদলানো বাধ্যতামূলক। Reset-এর পথটিও যেন নিরাপদ হয়, শুধু ফাঁস হওয়া ডেটা দিয়ে যেন reset করা না যায়।
- ব্যবহারকারীকে সৎভাবে জানান। কী হয়েছে, কী ডেটা গেছে, কী করবেন। বিশেষ করে বলুন, একই password অন্য জায়গায় থাকলে সেখানেও বদলান। সতর্কবার্তার পরে phishing বাড়তে পারে, তাই বার্তায় ক্লিক করার চাপ না দিয়ে নিজে সাইটে যেতে বলুন।
- আইনি বাধ্যবাধকতা দেখুন। GDPR-এর ক্ষেত্রে article 33 সম্ভব হলে ৭২ ঘণ্টার মধ্যে কর্তৃপক্ষকে জানাতে বলে। বাংলাদেশের প্রেক্ষাপটের জন্য পর্ব ০৬ দেখুন, আর এটি আইনি পরামর্শ নয়।
- শিখুন আর ঠিক করুন। সব পুরোনো hash আপগ্রেড, MFA বাড়ানো, আর দোষারোপহীন পর্যালোচনা।
৯ · ফাঁস হওয়া তালিকা মিলিয়ে দেখা আর ব্যবহারকারীর দিক
আপনার hash যত ভালোই হোক, ব্যবহারকারী যদি এমন password নেন যা আগে কোথাও ফাঁস হয়েছে, তবে সেটির প্রথমে পরীক্ষা হওয়ার সম্ভাবনাই বেশি। NIST তাই password বসানোর সময় পরিচিত, অনুমেয় বা ফাঁস হওয়া password-এর blocklist-এর সঙ্গে মেলানো বাধ্যতামূলক করেছে।
কিন্তু একটি ধাঁধা আছে। বাইরের কোনো তালিকা-সেবায় password পাঠিয়ে জিজ্ঞেস করলে সেটিই তো ফাঁস! সমাধান k-anonymity-ভিত্তিক খোঁজ: পুরো ছাপ নয়, শুধু প্রথম কয়েকটি অক্ষর পাঠানো। সেবাটি ওই শুরুর সঙ্গে মেলা বহু ছাপ ফেরত দেয়, আর আসল মিলটি হয় আপনার নিজের দিকে। Have I Been Pwned-এর Pwned Passwords এই ধারণার একটি পরিচিত উদাহরণ, যা SHA-1 ছাপের প্রথম ৫ hex অক্ষর ব্যবহার করে। অন্য সেবা বা নিজের সংরক্ষিত তালিকাও ব্যবহার করা যায়।
password: example-pass-1 SHA-1: 9BE84F856BCC258F0BD1…C8897
prefix: 9BE84 suffix: F856BCC258F0BD107…C8897 suffix stays on your side
range request: 9BE84 16^5 = 1,048,576 possible prefixes
0A1C…77D2:12
3FE9…10B4:1
F856…C8897:3
… many more
suffix found, seen 3 times (toy list)
reject: choose another password
- নিজের দিকে ছাপ। Signup-এর সময় password থেকে ছাপ বের করা হয়। এই SHA-1 শুধু তালিকা খোঁজার সূচি, password রাখার জন্য নয়। রাখার কাজ আগের মতোই Argon2id-এর।
- দুই ভাগ। প্রথম ৫ অক্ষর
9BE84, বাকি ৩৫ অক্ষর নিজের কাছে। - শুধু prefix বাইরে। 165 = 1,048,576টি সম্ভাব্য prefix-এর একটি। একই prefix-এ বহু ভিন্ন ছাপ পড়ে, তাই সেবাটি বুঝতে পারে না আসলে কোনটি।
- Range ফিরে এলো। Prefix-এ মেলা বহু suffix আর প্রতিটি কতবার দেখা গেছে। এখানকার তালিকা বানানো, শুধু ধারণার জন্য। কিছু সেবা ঐচ্ছিক padding দেয়, যাতে উত্তরের আকার দেখেও কিছু বোঝা না যায়।
- নিজের দিকে সিদ্ধান্ত। নিজের suffix তালিকায় আছে, তাই ব্যবহারকারীকে কারণসহ অন্য password নিতে বলা হয়। NIST-ও কারণ জানাতে বলে। Password কোথাও log করা হয় না।
ব্যবহারকারীর দিক: password manager
সবচেয়ে ভালো hash-ও এক জায়গার password পুনর্ব্যবহার ঠেকাতে পারে না। ব্যবহারকারীর সেরা অভ্যাসটি সহজ: প্রতিটি সেবায় আলাদা, লম্বা, এলোমেলো password, আর সেটি মনে রাখবে একটি password manager. ভাবুন একটি মজবুত আলমারি, ভেতরে সব চাবি। আপনি মনে রাখবেন শুধু আলমারির একটি শক্ত চাবি, যা হতে পারে কয়েকটি এলোমেলো শব্দের লম্বা বাক্যাংশ।
- এক সেবা ফাঁস হলে ক্ষতি সেখানেই থামে, এটিই সবচেয়ে বড় লাভ।
- Manager ঠিকানা মিলিয়ে তবেই password বসায়, তাই নকল সাইটে কিছুটা সুরক্ষাও পাওয়া যায়।
- মূল password আর recovery পথ হারালে সব হারাতে পারে। তাই সেগুলো নিরাপদে আলাদা জায়গায় রাখুন।
- Manager-এর ঝুঁকিও আছে: এটি একটি একক লক্ষ্য। তাই MFA আর বিশ্বস্ত, হালনাগাদ পণ্য জরুরি।
Developer হিসেবে আপনার দায়িত্ব: password field-এ paste আর autofill চালু রাখা। NIST password manager ব্যবহার করতে দেওয়া বাধ্যতামূলক করেছে, আর paste-ও অনুমতি দিতে বলে। পরে পর্ব ১৬-তে দেখবেন passkey এই পুরো সমস্যার অনেকটাই কীভাবে কমায়।
১০ · AI-এর যুগে password কোড
“একটি login system লিখে দাও” বললে AI সুন্দর, চলনসই কোড দেয়। আর চলনসই কোড সবচেয়ে বিপজ্জনক, কারণ ভুল hash-ও লগইন ঠিকই করে দেয়। ভুলটি ধরা পড়ে শুধু ফাঁসের দিনে।
| AI-এর সাধারণ ভুল | কেন ভুল | যা যাচাই করবেন |
|---|---|---|
| MD5 বা SHA-256 দিয়ে password hash | পুরোনো উদাহরণ থেকে শেখা, দ্রুত hash | Argon2id বা অন্তত স্বীকৃত password hash |
| সব ব্যবহারকারীর জন্য একটি code-এ বসানো salt | Salt আর pepper গুলিয়ে ফেলা | প্রতি record-এ আলাদা salt, secure random |
| বানানো parameter বা ভুল উদ্ধৃতি | “OWASP বলে” লিখে পুরোনো সংখ্যা | OWASP-এর আসল পাতা নিজে খুলে মেলান |
| Request body log করা | Debug-এর সুবিধা দেখে | Log-এ password, token আছে কি না খুঁজুন |
Hash তুলনায় == | সবচেয়ে সহজ কোড | Library-র verify বা constant-time compare |
| “ফেরত পাঠানো” password reset | ফেরত পড়া যায় এমন সংরক্ষণ | একবার ব্যবহারযোগ্য token দিয়ে reset |
নিচের prompt-এ AI-কে লেখক নয়, পর্যালোচক হিসেবে ব্যবহার করা হয়েছে। আসল key বা গ্রাহকের ডেটা কখনো পেস্ট করবেন না। উত্তরের প্রতিটি সংখ্যা মূল উৎসে মেলাবেন।
AI-এর লেখা password কোড মেলানোর চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে: এ ধরনের প্রশ্ন আসতে পারে।
Salt আর pepper-এর পার্থক্য? Salt প্রতি record-এ আলাদা, খোলা, database-এ থাকে; pepper গোপন, database-এর বাইরে থাকে।
SHA-256 কেন password-এর জন্য ভালো নয়? এটি দ্রুত, memory কম লাগে, আর cost বাড়ানো যায় না।
Salt কি গোপন রাখতে হয়? না, এর কাজ প্রতিটি ছাপ আলাদা করা।
নতুন system-এ কোনটি? Argon2id, না পেলে scrypt; FIPS-এর শর্তে PBKDF2; bcrypt পুরোনো system-এ।
Rehash কখন সম্ভব? লগইনের সময়, কারণ তখনই password server-এর হাতে থাকে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১১৯৭৯ সালের Unix থেকে আজকের Argon2 পর্যন্ত password রাখার ইতিহাস আসলে কোন সংগ্রামের গল্প?
এটি হার্ডওয়্যারের সঙ্গে রক্ষকের দৌড়ের গল্প। ১৯৭৯ সালে Robert Morris আর Ken Thompson “Password Security: A Case History” প্রবন্ধে Unix-এর অভিজ্ঞতা লেখেন। তাঁরা দেখান, মানুষের বাছা password দুর্বল, তাই শুধু লুকিয়ে রাখা যথেষ্ট নয়। সমাধানে দুটি ধারণা এলো, যা আজও টিকে আছে: DES-ভিত্তিক হিসাব বহুবার ঘুরিয়ে ধীর করা (২৫ বার), আর ১২ bit-এর salt, যাতে একই password আলাদা ছাপ দেয়।
কিন্তু একটি নির্দিষ্ট cost সময়ের সঙ্গে দুর্বল হয়ে যায়, কারণ হার্ডওয়্যার সস্তা হয়। ১৯৯৯ সালে Niels Provos আর David Mazières bcrypt আনেন, যার মূল চিন্তা cost একটি সংখ্যা, হার্ডওয়্যার বাড়লে সংখ্যাটিও বাড়ানো যায়। এরপর সমান্তরাল হার্ডওয়্যারের যুগ এলো। GPU হাজারো হিসাব একসঙ্গে চালাতে পারে, যদি প্রতিটিতে memory কম লাগে। ২০০৯ সালে Colin Percival scrypt দিয়ে সেই দুর্বলতায় হাত দেন, memory-কেই খরচ বানিয়ে। ২০১৩ থেকে ২০১৫ পর্যন্ত খোলা Password Hashing Competition হয়, যার বিজয়ী Argon2 RFC 9106 (২০২১) হিসেবে মানসম্মত হয়।
লক্ষণীয়, প্রতিটি ধাপে algorithm বদলের চেয়ে বড় হলো দাম নিয়ে ভাবনা। Morris আর Thompson-ও সময়ের দাম নিয়েই ভেবেছিলেন, আজ আমরা সময়ের সঙ্গে memory-র দাম যোগ করেছি। অর্থাৎ প্রশ্নটি কখনো “ভাঙা যায় কি না” ছিল না, ছিল “ভাঙতে কত খরচ, আর রক্ষকের খরচ কত।”
মূল উপলব্ধি: Password storage-এ কোনো চিরস্থায়ী সমাধান নেই। তাই নকশায় আগে থেকেই cost বাড়ানো আর algorithm বদলানোর পথ রাখতে হয়। এটি crypto agility-র প্রথম বাস্তব উদাহরণগুলোর একটি।
প্র ০২RockYou, LinkedIn আর Adobe-এর ফাঁস থেকে সংরক্ষণের কোন আলাদা শিক্ষা পাওয়া যায়?
তিনটি ঘটনা তিনটি আলাদা ভুলের শিক্ষণীয় উদাহরণ, যদিও বিস্তারিত প্রকাশিত প্রতিবেদনের ওপর নির্ভর করে, আর সেগুলোর পরিমাণ উৎসভেদে আলাদা।
RockYou (২০০৯): প্রতিবেদন অনুযায়ী বিপুল সংখ্যক password plaintext অবস্থায় ছিল। ফলে ফাঁসের দিনই ক্ষতি পুরো হয়ে যায়। আরও বড় ক্ষতি পরে দেখা গেল: খোলা তালিকাটি মানুষের password বাছার অভ্যাসের বিশাল নমুনা হয়ে ওঠে। অর্থাৎ এক সেবার ফাঁস অন্য সব সেবার ঝুঁকি বাড়ায়। এই কারণেই আজ NIST ফাঁস হওয়া password-এর তালিকা মেলাতে বলে।
LinkedIn (২০১২): প্রতিবেদন অনুযায়ী password ছিল salt ছাড়া SHA-1 ছাপে। Hash ছিল, কিন্তু ভুল ধরনের। দ্রুত hash আর salt-এর অভাবে অনেক password অল্প সময়ে উন্মুক্ত হয়। পরে জানা যায়, ফাঁসের পরিমাণ শুরুর অনুমানের চেয়ে অনেক বড় ছিল। অর্থাৎ ফাঁসের পরিধি অনেক সময় দেরিতে বোঝা যায়, তাই শুরুর ঘোষণা সতর্কভাবে দিতে হয়।
Adobe (২০১৩): প্রতিবেদন অনুযায়ী password hash নয়, block cipher দিয়ে encrypt করা ছিল, একই key-তে, আর password-এর ইঙ্গিতও পাশে ছিল। ফলে একই password-এর ciphertext এক, আর একই গুচ্ছের সবার ইঙ্গিত মিলিয়ে পড়া যেত। এটি reversible encryption-এর সমস্যার একটি শিক্ষণীয় নমুনা: key না জেনেও প্যাটার্ন ফাঁস হয়।
তিনটি ক্ষেত্রেই প্রতিষ্ঠানগুলোর দুর্বলতা ছিল নকশার, প্রকৌশল-দক্ষতার অভাব নয়। “পরে ঠিক করব” ধরনের সিদ্ধান্ত বহু বছর টিকে থাকে।
মূল উপলব্ধি: সংরক্ষণের সিদ্ধান্ত নেওয়া হয় প্রথম দিনে, আর তার পরীক্ষা হয় ফাঁসের দিনে। তিনটি ভুলের প্রতিষেধক একই: প্রতি record-এ salt, ধীর hash, আর পরিবর্তনের পরিকল্পনা।
প্র ০৩bcrypt-এর ৭২ byte সীমা কি শুধু লম্বা password-এর সমস্যা? ২০২৪ সালের একটি প্রকাশিত advisory কী শেখায়?
না। সীমাটি সবচেয়ে বিপজ্জনক হয় যখন bcrypt-কে এমন কাজে লাগানো হয়, যার জন্য বানানো হয়নি। ২০২৪ সালের অক্টোবরের শেষে ধরা পড়া একটি ত্রুটি নিয়ে Okta একটি নিরাপত্তা advisory প্রকাশ করে। AD/LDAP delegated authentication-এর একটি cache key তৈরি হতো userId + username + password জুড়ে bcrypt দিয়ে। Advisory অনুযায়ী, username ৫২ অক্ষর বা বেশি হলে আর অন্য কয়েকটি শর্ত মিললে পুরোনো cache মিলে যেতে পারত। প্রকাশিত ব্যাখ্যায় বলা হয়, bcrypt নির্দিষ্ট দৈর্ঘ্যের পরের ইনপুট উপেক্ষা করে, তাই সামনের অংশ লম্বা হলে শেষের password ইনপুটের ভেতরে ঢুকতেই পারত না।
লক্ষ করুন, এখানে bcrypt-এর “ভাঙন” ছিল না। Algorithm ঠিকভাবে কাজ করছিল, ভুল ছিল ধরে নেওয়া চুক্তিতে। Advisory অনুসারে সমাধান ছিল bcrypt থেকে PBKDF2-তে যাওয়া, আর গ্রাহকদের log পরীক্ষা করতে আর MFA চালু রাখতে বলা হয়। অর্থাৎ একটি স্তর পড়ে গেলেও MFA পরিস্থিতি বাঁচাতে পারে, যা defense in depth-এর বাস্তব উদাহরণ।
রক্ষকের জন্য শিক্ষা কয়েকটি। প্রথমত, প্রতিটি cryptographic function-এর চুক্তি পড়ুন: ইনপুটের সীমা কত, null byte-এ কী হয়, বেশি দিলে error না নীরবতা। দ্বিতীয়ত, নিচের স্তরের কাজকে জুড়ে “লেখা বানিয়ে” দেবেন না, password hash শুধু password hash-এর কাজেই। তৃতীয়ত, boundary test লিখুন: ৭১, ৭২, ৭৩ byte, আর শুরুর লম্বা অংশ একই রেখে শেষ অংশ বদলে দেখুন ফল বদলায় কি না। আপনার নিজের system-এ এই পরীক্ষা দামি নয়।
মূল উপলব্ধি: নিরাপদ primitive ভুল জায়গায় ব্যবহার করলে নিরাপদ থাকে না। Library-র documentation আর সীমা পড়া ঐচ্ছিক কাজ নয়, নিরাপত্তার অংশ।
প্র ০৪Memory-hard হওয়া কেন এত গুরুত্বপূর্ণ, আর RFC 9106 ও OWASP-এর সংখ্যা এত আলাদা কেন?
দাম নিয়ে ভাবুন। PBKDF2-এর মতো শুধু হিসাব-নির্ভর function-এ প্রতিটি অনুমানের জন্য অল্প কিছু গেট লাগে। তাই বিশেষ হার্ডওয়্যারে একই চিপে বহু হিসাব একসঙ্গে বসানো যায়, আর প্রতি হিসাবে খরচ খুব কমে যায়। রক্ষকের সাধারণ CPU আর অনুমানকারীর বিশেষ চিপের দামের ফারাক অনেক হয়ে দাঁড়ায়, রক্ষকের পক্ষে নয়।
Memory-hard function ভিন্ন খেলা খেলে। একটি হিসাব চালাতে যদি ১৯ MiB বা তারও বেশি memory বারবার পড়তে হয়, তবে একসঙ্গে হাজার হিসাবে হাজার গুণ memory লাগে। চিপে memory-র জায়গা ব্যয়বহুল, আর memory-র গতি কোর বাড়ানোর মতো সহজে বাড়ে না। ফলে রক্ষক আর অনুমানকারীর দামের ফারাক কমে আসে। Time-memory trade-off, মানে কম memory নিয়ে বেশি সময়ে হিসাব করার চেষ্টাও, ভালো নকশায় লাভজনক না হওয়ার কথা। গবেষণায় এই দাবিগুলো পরীক্ষা হতে থাকে, তাই RFC-র সুপারিশ আর বিশ্লেষণ সময়ের সঙ্গে বদলাতে পারে।
তাহলে RFC 9106-এর ২ GiB আর OWASP-এর ১৯ MiB-এর মধ্যে এত ফারাক কেন? দুটি আলাদা অবস্থান থেকে দেখা। RFC বলে, memory যথেষ্ট থাকলে দামি পথ নিন। আর কম থাকলে ৬৪ MiB. OWASP বাস্তবের web সেবার জন্য ন্যূনতম গ্রহণযোগ্য তল দেয়, আর m ও t-র সমমানের কয়েকটি জোড়া দেয়, যাতে সীমিত server-ও শুরু করতে পারে। ২ GiB প্রতি login-এ একটি ব্যস্ত সেবায় ব্যবহার করলে server নিজেই থেমে যেতে পারে।
মূল উপলব্ধি: সঠিক সংখ্যা বলে একটি জিনিস নেই, আছে আপনার server, আপনার peak load আর আপনার ঝুঁকি। ন্যূনতম মানের ওপরে যতটা সামর্থ্য আছে, ততটা নিন, আর মেপে নিন।
প্র ০৫Browser-এই hash করে পাঠালে কি server-এর দায়িত্ব কমে? “Server password দেখবেই না” কি সম্ভব?
শুনতে ভালো লাগে, কিন্তু সরল রূপে এটি কাজ করে না। ধরুন browser SHA-256(password) পাঠায়, আর server সেই মানটি সরাসরি মিলিয়ে দেখে। তাহলে server-এর কাছে ওই hash-ই নতুন password. Database ফাঁস হলে ফাঁস হওয়া মানটিই পাঠিয়ে login করা যাবে, অনুমান করারও দরকার নেই। অর্থাৎ hash password-এ পরিণত হলো। তাই ধীর hash server-এর দিকেই থাকতে হয়।
তাহলে server-এর দিকে ধীর hash থাকলে আর browser-এ বাড়তি কিছু করলে? সেটি সম্ভব, একে কখনো client-side pre-hashing বলা হয়। এতে server সরাসরি password দেখে না, ফলে log-এ পড়ে যাওয়ার ঝুঁকি কমে। কিন্তু এর দামও আছে: দুর্বল ফোনে ধীর হিসাব ব্যবহারকারীকে কষ্ট দেয়, browser-এ পাঠানো JavaScript-এর ওপর বিশ্বাস রাখতেই হয়, আর সেই কোড server-ই দেয়। Server দখল হলে কোড বদলে দেওয়া যায়। এছাড়া আগের আলোচনা মতো ৭২ byte আর null byte-এর ঝামেলাও ফিরে আসে।
আরও শক্তিশালী পথ হলো অন্য ধরনের প্রোটোকল। Password-authenticated key exchange (PAKE), যেমন OPAQUE, এমনভাবে নকশা করা যাতে server password না দেখেও যাচাই করতে পারে। OPAQUE প্রকাশিত হয়েছে RFC 9807 (২০২৫) হিসেবে, যা একটি Informational নথি। তবে web-এ এর ব্যবহার এখনো সীমিত, আর জটিলতা বেশি। আরেকটি পথ হলো passkey, যেখানে server শুধু public key রাখে, তাই ফাঁসের মতো কোনো password টেবিলই থাকে না। পর্ব ১৬-তে এটি দেখবেন।
মূল উপলব্ধি: সমস্যাটি কোথায় সরানো হচ্ছে, সেটি দেখুন, শুধু কোথায় লেখা হচ্ছে নয়। ধীর hash server-এর দিকে রেখে, TLS আর log-পরিষ্কার পথ নিশ্চিত করা এখনো মূলধারার ভালো অভ্যাস, আর দীর্ঘ মেয়াদে password কমানোই বড় লক্ষ্য।
অনুশীলন
সব কাজ নিজের কম্পিউটারে, খেলনা ডেটায়। অন্য কারো hash নিয়ে নয়।
-
খরচের হিসাব: ধরে নিন দ্রুত hash-এ সেকেন্ডে 5 × 109টি, ধীর hash-এ 2 × 103টি পরীক্ষা হয়। ৩৬ bit entropy-র password-এ গড় সময় (অর্ধেক মান) কত? দুই হারের অনুপাত কত? এবার আপনার সেবায় peak সময়ে প্রতি সেকেন্ডে ৪০টি login, প্রতি hash-এ ০.২ সেকেন্ড আর ৪৬ MiB. কতগুলো core আর কত memory লাগবে?
অর্ধেক মান 235 = 34,359,738,368. দ্রুত hash: ÷ 5 × 109 ≈ ৬.৯ সেকেন্ড। ধীর hash: ÷ 2 × 103 ≈ 1.718 × 107 সেকেন্ড ≈ ১৯৮.৮ দিন। অনুপাত 2.5 × 106, মানে ২৫ লাখ গুণ।
রক্ষকের খরচ: 40 × 0.2 = ৮টি core একসঙ্গে ব্যস্ত, আর 8 × 46 MiB = ৩৬৮ MiB memory. এই ক্ষমতা না থাকলে মান কমান বা hardware বাড়ান, আর আসল মেশিনে মেপে নিন।
-
নিজের মেশিনে cost মাপুন: নিজের কম্পিউটারে নিচের স্ক্রিপ্টটি চালান (উদাহরণ, এই পাতায় চলবে না)। কোন N-এ একটি hash ২৫০ ms-এর কাছাকাছি লাগে? N দ্বিগুণ করলে সময় আর memory কী হয়?
# উদাহরণ: শুধু নিজের মেশিনে, খেলনা password দিয়ে import hashlib, time, secrets for exp in range(14, 18): n, salt = 2**exp, secrets.token_bytes(16) t0 = time.perf_counter() hashlib.scrypt(b"example-pass-1", salt=salt, n=n, r=8, p=1, maxmem=512 * 1024 * 1024, dklen=32) ms = (time.perf_counter() - t0) * 1000 print(f"N=2^{exp} memory={128*n*8/2**20:.0f} MiB time={ms:.0f} ms")N দ্বিগুণ করলে memory ঠিক দ্বিগুণ (128 × N × r), আর সময়ও মোটামুটি দ্বিগুণ হয়। প্রতিটি মেশিনে ফল আলাদা, আর সেটিই মাপার কারণ। ২১৭-এ memory ১২৮ MiB, যা OWASP-এর ন্যূনতম মান। Python-এর
hashlib.scrypt-এ ডিফল্ট memory সীমা কম, তাই বড় N-এর জন্যmaxmemবাড়াতে হয়। Python-এর এমন সীমা না জানলে AI-এর কোড error দেবে, আর তখন ভুল করে N কমিয়ে দেওয়ার প্রলোভন আসে। -
নকশা সমালোচনা: একজন সহকর্মীর কোডের বর্ণনা: সব ব্যবহারকারীর জন্য code-এ লেখা একটি salt,
md5(salt + password), লগইন ব্যর্থ হলে পুরো request log, আর “password ভুলে গেলে” email-এ পুরোনো password. চারটি ভুল চিহ্নিত করে প্রতিটির সমাধান লিখুন।- এক salt সবার জন্য: একই password একই ছাপ দেয়। সমাধান: প্রতি record-এ নতুন secure random salt.
- MD5: দ্রুত, password-এর জন্য নয়। সমাধান: Argon2id, আর পুরোনো record লগইনে আপগ্রেড।
- পুরো request log: password log-এ plaintext. সমাধান: সংবেদনশীল field log-এর আগেই বাদ, আর পুরোনো log পরিষ্কার।
- Email-এ পুরোনো password: মানে সেটি ফেরত পড়া যায়, যা নিজেই ভুল। সমাধান: একবার ব্যবহারযোগ্য, কম মেয়াদের reset token.
পুরোনো log আর backup-এও password থাকতে পারে, তাই ঘটনাটিকে সম্ভাব্য ফাঁস ধরে নিয়ে মূল্যায়ন করা উচিত।
-
ফল অনুমান করুন: নীতি
slow/1024. শুরুতে A:fast/1, B:slow/256, C:slow/1024. ঘটনাক্রম: B ভুল password দিলেন, A সঠিক দিলেন, C সঠিক দিলেন। শেষে তিনটি record-এর alg/cost কী, salt কার বদলাল, আর “still on old hash” তালিকায় কে?A:
slow/1024, নতুন salt. B:slow/256, ভুল password-এ record বদলায় না। C:slow/1024, আগেই ঠিক ছিল, তাই বদলায়নি। তালিকায় শুধু B. কারণ আপগ্রেড হয় শুধু সফল যাচাইয়ের পরে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমLogin-এ password যাচাইয়ের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Username দিয়ে record আনা
- Record থেকে algorithm, cost আর salt পড়া
- Secret store থেকে দরকারি version-এর pepper নেওয়া
- টাইপ করা password দিয়ে আবার hash হিসাব
- Constant-time তুলনা
- মিললে পুরোনো record হলে rehash, তারপর session শুরু
Password-এর জন্য salt-এর মূল কাজ কী?
- গোপন key হিসেবে hash রক্ষা করা
- প্রতিটি ছাপ আলাদা করে আগে-বানানো টেবিল অকেজো করা
- একটি অনুমান পরীক্ষার সময় বহু গুণ বাড়ানো
- ব্যবহারকারীর password-এর entropy বাড়ানো
একটি নতুন web সেবা শুরু হচ্ছে, কোনো বিশেষ compliance শর্ত নেই। OWASP Password Storage Cheat Sheet অনুযায়ী প্রথম পছন্দ কোনটি?
- Salt সহ SHA-256
- PBKDF2, সবচেয়ে পুরোনো বলে
- Argon2id
- AES-256 encryption
bcrypt-এর ইনপুট সীমা ৭২ byte. UTF-8-এ প্রতিটি বাংলা অক্ষর ৩ byte ধরলে কতগুলো বাংলা অক্ষরে সীমা শেষ হয়?
- ২৪টি
- ৩৬টি
- ৭২টি
- ২১৬টি
পুরোনো SHA-1 hash-এর একটি টেবিল Argon2id-এ নেওয়ার সবচেয়ে স্বাভাবিক সময় কখন, যখন সরাসরি password-এর hash চাই?
- রাতের একটি batch job-এ, এক ধাক্কায় সবার
- Log থেকে password নিয়ে
- ব্যবহারকারী ভুল password দিলে
- সফল লগইনের ঠিক পরে
একটি সেবায় pepper একই database-এর config টেবিলে রাখা। Database ফাঁস হলে pepper কতটা কাজে আসবে?
- পুরো কাজে আসবে, কারণ pepper আছে
- প্রায় কোনো কাজে আসবে না, কারণ pepper-ও একসঙ্গে ফাঁস হয়েছে
- Salt-এর কাজ pepper নিজে করে দেবে
- ধীর hash-ও সঙ্গে সঙ্গে অকেজো হবে
k-anonymity ভিত্তিক ফাঁস-তালিকা যাচাইয়ে SHA-1 ছাপের কোন অংশ বাইরের সেবায় পাঠানো হয়?
- Password-এর খোলা লেখা
- পুরো SHA-1 ছাপ
- শুধু শুরুর অল্প কয়েকটি অক্ষর
- Database-এর Argon2id hash
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Server-এর password জানার দরকার নেই, শুধু যাচাই করা দরকার। Plaintext আর reversible দুটোই ভুল পথ।
- Salt আগে-বানানো টেবিল অকেজো করে, ধীর hash অনুমানের দাম বাড়ায়, pepper database-এর বাইরে থাকলে বাড়তি স্তর দেয়।
- নতুন system-এ Argon2id, অন্তত OWASP-এর ন্যূনতম মান, আর নিজের server-এ মেপে cost.
- পুরোনো hash লগইনের সময় আপগ্রেড, শেষ তারিখ ঠিক করে। bcrypt-এর সীমা ৭২ byte, অক্ষর নয়।
- ফাঁসের পরিকল্পনা আগে থেকেই: প্রমাণ রাখা, বাধ্যতামূলক reset, সৎ যোগাযোগ, আর ফাঁস হওয়া তালিকার সঙ্গে মেলানো।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৪ · Cryptography-র সাধারণ ভুল — নিজে না বানানো, key management আর post-quantumপরবর্তী পর্বCrypto ব্যবহারের সাধারণ ভুল, key-এর জীবনচক্র আর post-quantum প্রস্তুতি।
- সাইবার নিরাপত্তা · কোর্স হোম৮০টি পর্বপুরো কোর্সের রোডম্যাপ।
- পর্ব ০৬ · আইন আর নৈতিকতাসীমারেখাঅনুমতি, দায়িত্বশীল প্রকাশ আর বাংলাদেশের আইন।
- Operating Systems · OS securityপাশের ট্র্যাকOS কীভাবে password রাখে, আর MFA ও passkey-র কাজ।
- Cloud & DevOps · Secrets আর KMSগভীরে যানPepper-এর মতো key কোথায় রাখবেন, কীভাবে বদলাবেন।
- System Design · Rate limitingপাশের ট্র্যাকLogin পথে অনুমান আর চাপ সামলানো।