ক্রিপ্টো প্রিমিটিভস ল্যাব
Crypto Primitives Lab
SHA-256 from-scratch লিখে hashlib-এর সাথে বিট-নির্ভুল মিলিয়ে, বাস্তব AES দিয়ে ECB-leak ও nonce-reuse বিপর্যয় reproduce করে, real-size Diffie-Hellman আর HKDF দিয়ে দুই পক্ষের মধ্যে একটা কাজ-করা end-to-end secure channel বানানো।
কেন এই প্রজেক্ট
আগের তিনটা লেসনে hash function, symmetric encryption, আর asymmetric
key-exchange — প্রতিটা যন্ত্রের আচরণ কোড চালিয়ে দেখা হয়েছে। কিন্তু
আচরণ দেখা আর নিজে বানানো এক জিনিস না। SHA-256-এর padding নিয়ম নিজে হাতে
না লিখলে বোঝা যায় না কেন 0x80 byte-টা লাগে; ECB-এর leak শুধু পড়ে
বিশ্বাস করা আর নিজের হাতে বাস্তব AES দিয়ে সেই leak reproduce করে দেখা —
দুইটার আত্মবিশ্বাস সম্পূর্ণ ভিন্ন মাপের। এই প্রজেক্টে চারটা কাজ করব:
নিজে একটা সম্পূর্ণ SHA-256 লিখে Python-এর real hashlib এর সাথে মিলিয়ে
প্রমাণ করা যে সেটা সত্যিই একই ফাংশন; বাস্তব AES লাইব্রেরি দিয়ে ঠিক
কোন ভুলগুলো (ECB, nonce-reuse) বিপর্যয় ডেকে আনে তা reproduce করা; একটা
toy-না-হয়ে বাস্তব-সাইজের Diffie-Hellman চালিয়ে দেখা কেন parameter-সাইজ
নিরাপত্তার আসল মাপকাঠি; আর শেষে সবকিছু জুড়ে একটা ছোট working “secure
channel” বানানো।
দুইটা জিনিস স্পষ্ট রাখা জরুরি। প্রথম, hash function-এর জন্য এখানে
toy না, সত্যিকারের SHA-256 লিখব — কারণ compression function
নিজে-লেখা সম্ভব এবং সেখানেই আসল শিক্ষা (bitwise rotation, message
schedule, Merkle–Damgård padding)। কিন্তু AES-এর জন্য উল্টো পথ — নিজে
S-box/round-function লেখা এত দীর্ঘ যে শেখার-প্রতি-লাইন অনুপাত খারাপ; তাই
সেখানে বাস্তব cryptography লাইব্রেরি সঠিকভাবে ব্যবহার করা-ই আজকের
দক্ষতা — ঠিক production-এ যা করবেন। দ্বিতীয়, এই পুরো প্রজেক্টের একটা
লাইনও production-এ সরাসরি বসানো উচিত না। নিজে hash লেখা, toy DH চালানো,
raw AES mode হাতে জোড়া লাগানো — এসবই শেখার জন্য; বাস্তব সিস্টেমে
সবসময় audited library (OpenSSL, libsodium, cryptography) আর
standard protocol (TLS, age, Noise) ব্যবহার করবেন। কিন্তু ভাঙা জিনিস
নিজে বানানোর অভিজ্ঞতা ছাড়া library-র ভেতরে কী রক্ষা করছে, কেন — সেই
স্বজ্ঞা কখনো তৈরি হয় না।
ধাপে ধাপে
এই প্রজেক্টের জন্য cryptography লাইব্রেরি লাগবে:
pip install cryptography
১. SHA-256 from-scratch — শুধু bitwise আর mod-arithmetic দিয়ে
FIPS 180-4-এর সংজ্ঞা সরাসরি কোডে বসাই — round constant K, initial
hash value H0, message schedule expansion, আর ৬৪-রাউন্ড compression
loop:
# sha256_scratch.py — SHA-256 সম্পূর্ণ from-scratch, শুধু basic bitwise/mod-arithmetic দিয়ে
import hashlib
K = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
]
H0 = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]
MASK = 0xffffffff
def rotr(x, n):
return ((x >> n) | (x << (32 - n))) & MASK
def sha256(message: bytes) -> bytes:
msg_len_bits = len(message) * 8
padded = message + b"\x80"
while len(padded) % 64 != 56:
padded += b"\x00"
padded += msg_len_bits.to_bytes(8, "big") # Merkle-Damgard padding
h = H0[:]
for chunk_start in range(0, len(padded), 64):
chunk = padded[chunk_start:chunk_start + 64]
w = [int.from_bytes(chunk[i:i+4], "big") for i in range(0, 64, 4)]
for i in range(16, 64):
s0 = rotr(w[i-15], 7) ^ rotr(w[i-15], 18) ^ (w[i-15] >> 3)
s1 = rotr(w[i-2], 17) ^ rotr(w[i-2], 19) ^ (w[i-2] >> 10)
w.append((w[i-16] + s0 + w[i-7] + s1) & MASK)
a, b, c, d, e, f, g, hh = h
for i in range(64):
S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)
ch = (e & f) ^ ((~e & MASK) & g)
temp1 = (hh + S1 + ch + K[i] + w[i]) & MASK
S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)
maj = (a & b) ^ (a & c) ^ (b & c)
temp2 = (S0 + maj) & MASK
hh, g, f, e = g, f, e, (d + temp1) & MASK
d, c, b, a = c, b, a, (temp1 + temp2) & MASK
h = [(x + y) & MASK for x, y in zip(h, [a, b, c, d, e, f, g, hh])]
return b"".join(x.to_bytes(4, "big") for x in h)
# ---------- real hashlib.sha256-এর সাথে তুলনা ----------
test_vectors = [b"", b"abc",
"বাংলা ইউনিকোড টেক্সট, byte-level কাজ করে".encode("utf-8"),
b"a" * 55, b"a" * 64] # ৫৫/৬৪ byte — padding-boundary edge case
all_ok = True
for tv in test_vectors:
mine, real = sha256(tv).hex(), hashlib.sha256(tv).hexdigest()
all_ok &= (mine == real)
print(f"len={len(tv):3d} match={mine == real}")
print(f"\nসব {len(test_vectors)}টা vector hashlib-এর সাথে মিলল: {all_ok}")
আসল পরীক্ষা চালিয়ে পাওয়া output:
len= 0 match=True
len= 3 match=True
len= 55 match=True
len= 55 match=True
len= 64 match=True
সব 5টা vector hashlib-এর সাথে মিলল: True
(নোট: বাংলা string আর "a"*55 দুইটারই byte-length কাকতালীয়ভাবে ৫৫ —
আসল রানে দেখুন len() byte-এ, str-এর character-সংখ্যায় না। গুরুত্বপূর্ণ
লাইনটাই শেষেরটা — পাঁচটাই True।) "a"*55 আর "a"*64 ইচ্ছাকৃতভাবে
বেছে নেওয়া — ৫৬-byte padding-boundary-র ঠিক দুই পাশে, কারণ এখানেই একটা
off-by-one bug সবচেয়ে সহজে লুকিয়ে থাকে (৫৫-byte message একটা block-এ
padding নিয়ে ফিট করে, ৫৬+ করে না, তাই আরেকটা পুরো block লাগে)।
Checkpoint: পাঁচটা test vector-ই match=True; ইচ্ছে করে rotr-এ
একটা shift-amount ভুল করে দেখুন (যেমন ৬-এর বদলে ৭) — অন্তত একটা vector
mismatch করবে, প্রমাণ করে test suite আসলেই কাজ করছে, শুধু tautology না।
২. ECB mode pattern-leak — বাস্তব AES দিয়ে
cryptography লাইব্রেরির Cipher/modes API দিয়ে real AES-256 চালিয়ে
lesson-এর plus-চিহ্ন পরীক্ষাটাই আবার করি — এবার toy stand-in না, আসল
cipher:
# aes_ecb_cbc.py — বাস্তব AES (cryptography library) দিয়ে ECB leak বনাম CBC
import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
KEY = os.urandom(32) # AES-256 — প্রতিবার চালালে ভিন্ন, কিন্তু গুণাবলী একই থাকবে
def aes_ecb_encrypt(key, data):
enc = Cipher(algorithms.AES(key), modes.ECB()).encryptor()
return enc.update(data) + enc.finalize()
def aes_cbc_encrypt(key, iv, data):
enc = Cipher(algorithms.AES(key), modes.CBC(iv)).encryptor()
return enc.update(data) + enc.finalize()
TILE_BG, TILE_FG = b"\x00" * 16, b"\xff" * 16
W, H = 12, 10
blocks = []
for y in range(H):
for x in range(W):
on_bar, on_beam = (4 <= x <= 7), (3 <= y <= 6)
blocks.append(TILE_FG if (on_bar or on_beam) else TILE_BG)
plaintext = b"".join(blocks)
ecb_ct = aes_ecb_encrypt(KEY, plaintext)
ecb_blocks = [ecb_ct[i:i+16] for i in range(0, len(ecb_ct), 16)]
print(f"plaintext distinct blocks: {len(set(blocks))}")
print(f"AES-ECB ciphertext distinct blocks: {len(set(ecb_blocks))}")
iv = os.urandom(16)
cbc_ct = aes_cbc_encrypt(KEY, iv, plaintext)
cbc_blocks = [cbc_ct[i:i+16] for i in range(0, len(cbc_ct), 16)]
print(f"AES-CBC ciphertext distinct blocks: {len(set(cbc_blocks))} (মোট {len(cbc_blocks)})")
আসল রান থেকে (key/IV প্রতিবার random, কিন্তু নিচের সংখ্যাগুলো সবসময় একই):
plaintext distinct blocks: 2
AES-ECB ciphertext distinct blocks: 2
AES-CBC ciphertext distinct blocks: 120 (মোট 120)
এইখানেই symmetric-encryption লেসনের toy-cipher-experiment বাস্তব AES
দিয়ে confirm হলো: hashlib-স্ট্যান্ড-ইন না, সত্যিকারের FIPS-197 AES,
তবু ECB-তে distinct block সংখ্যা প্লেইনটেক্সটের 2-ই থেকে গেল — cipher
যত শক্তিশালী হোক, mode ভুল হলে structure leak হয়। CBC-তে সেই একই AES,
শুধু IV+chaining যোগ করায় 120-এর 120-ই আলাদা।
Checkpoint: ECB distinct blocks == plaintext distinct blocks
(leak প্রমাণিত) বনাম CBC distinct blocks == মোট block সংখ্যা (leak
বন্ধ) — দুইটা assert লিখে স্বয়ংক্রিয়ভাবে যাচাই করুন।
৩. CTR nonce-reuse বিপর্যয় — real AES keystream দিয়েও একই গণিত ধসে
# aes_ctr_reuse.py — একই (key, nonce) দুইবার ব্যবহার হলে বাস্তব AES-CTR-ও ভাঙে
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
def aes_ctr_encrypt(key, nonce, data):
enc = Cipher(algorithms.AES(key), modes.CTR(nonce)).encryptor()
return enc.update(data) + enc.finalize()
nonce16 = b"\x00" * 16 # bug: fixed nonce, দুইবার ব্যবহার
msg1 = b"To: bank-api Amount: $100.00 To-Acct: alice-0091 padXX"
msg2 = b"To: bank-api Amount: $9999.99 To-Acct: mallory-77 padXX"
assert len(msg1) == len(msg2)
c1 = aes_ctr_encrypt(KEY, nonce16, msg1)
c2 = aes_ctr_encrypt(KEY, nonce16, msg2)
xor_c = bytes(a ^ b for a, b in zip(c1, c2))
xor_p = bytes(a ^ b for a, b in zip(msg1, msg2))
print(f"c1 XOR c2 == m1 XOR m2 : {xor_c == xor_p}")
recovered = bytes(a ^ b for a, b in zip(xor_c, msg1)) # attacker msg1 জানে
print(f"recovered m2 == real m2 : {recovered == msg2}")
print(f"recovered text: {recovered!r}")
আসল রান (আবারও: KEY random কিন্তু নিচের তিনটা লাইন সবসময় এভাবেই আসবে):
c1 XOR c2 == m1 XOR m2 : True
recovered m2 == real m2 : True
recovered text: b'To: bank-api Amount: $9999.99 To-Acct: mallory-77 padXX'
symmetric-encryption লেসনের keyed-hash-stand-in আর এখানকার সত্যিকারের
AES — গাণিতিক ফলাফল হুবহু এক, কারণ ভাঙনটা AES-এর দুর্বলতা না, mode-এর
ব্যবহারের নিয়ম ভাঙার ফল। key কখনো ভাঙা লাগেনি — শুধু একটা XOR।
Checkpoint: msg1, msg2-র length বদলে দিয়ে (ভিন্ন length) আবার
চালান — recovery আংশিক হবে (common-prefix পর্যন্তই), পুরোপুরি ভাঙবে না;
ব্যাখ্যা করুন কেন length mismatch keystream-alignment ভেঙে দেয়।
৪. AES-GCM — AEAD round-trip ও tamper-detection
# aes_gcm.py — AES-GCM: এক পাসেই encryption + authentication
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
aesgcm = AESGCM(KEY)
nonce12 = os.urandom(12)
aad = b"user-id=1001;route=/transfer"
plaintext = b"Amount: $250.00 To-Acct: bob-2002"
ct_tag = aesgcm.encrypt(nonce12, plaintext, aad)
print(f"ciphertext+tag length = {len(ct_tag)} (plaintext {len(plaintext)} + 16-byte tag)")
decrypted = aesgcm.decrypt(nonce12, ct_tag, aad)
print(f"decrypt সফল: {decrypted == plaintext}")
tampered_ct = bytearray(ct_tag); tampered_ct[0] ^= 0x01
try:
aesgcm.decrypt(nonce12, bytes(tampered_ct), aad)
print("tamper ধরা পড়েনি!!")
except Exception as e:
print(f"tampered ciphertext REJECTED: {type(e).__name__}")
try:
aesgcm.decrypt(nonce12, ct_tag, aad + b"-EVIL")
print("tampered AAD ধরা পড়েনি!!")
except Exception as e:
print(f"tampered AAD REJECTED: {type(e).__name__}")
আসল রান:
ciphertext+tag length = 49 (plaintext 33 + 16-byte tag)
decrypt সফল: True
tampered ciphertext REJECTED: InvalidTag
tampered AAD REJECTED: InvalidTag
দুই ধরনের tamper-ই — encrypted অংশ বদলানো, আর plaintext-থাকা AAD
বদলানো — একই InvalidTag exception-এ ধরা পড়ল। AAD নিজে কখনো encrypt
হয়নি (route/user-id প্লেইন টেক্সটেই ছিল), তবু tag-এর হিসাবে বাঁধা থাকায়
এক bit বদলালেই পুরো decrypt বাতিল।
Checkpoint: tag-এর শেষ byte বদলে (ciphertext না, শুধু tag অংশ)
আবারও InvalidTag আসা উচিত — GCM-এর ১৬-byte tag পুরোটাই integrity-এর
জন্য দায়ী, শুধু একটা “checksum bit” না।
৫. Diffie-Hellman — toy p=23 না, real-world size দিয়ে
lesson-এর DH ঠিক এই গণিত ব্যবহার করেছিল, কিন্তু p=23-এ brute-force এক
লাইনে জিতে যায়। এখানে RFC 3526 Group 14-র real ২০৪৮-বিট prime বসাই:
# dh_real.py — Diffie-Hellman, RFC 3526 Group 14 (real-world 2048-bit MODP prime)
import secrets, time
P_2048_HEX = """
FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B
E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9
DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510
15728E5A 8AACAA68 FFFFFFFF FFFFFFFF
""".replace(" ", "").replace("\n", "")
P, G = int(P_2048_HEX, 16), 2
print(f"real DH prime bit-length = {P.bit_length()} বিট")
alice_priv = secrets.randbelow(P - 2) + 2
bob_priv = secrets.randbelow(P - 2) + 2
t0 = time.perf_counter(); alice_pub = pow(G, alice_priv, P); t_a = time.perf_counter() - t0
t0 = time.perf_counter(); bob_pub = pow(G, bob_priv, P); t_b = time.perf_counter() - t0
print(f"Alice public গণনা করতে: {t_a*1000:.3f} ms, Bob public গণনা করতে: {t_b*1000:.3f} ms")
alice_shared = pow(bob_pub, alice_priv, P)
bob_shared = pow(alice_pub, bob_priv, P)
print(f"shared secret মেলে: {alice_shared == bob_shared} (bit-length {alice_shared.bit_length()})")
# ---------- কেন size গুরুত্বপূর্ণ: toy p বনাম real p-তে backward-cost ----------
def find_prime_near(n):
def is_prime(k):
return k > 1 and all(k % i for i in range(2, int(k**0.5) + 1))
x = n
while not is_prime(x): x += 1
return x
toy_p, toy_g = find_prime_near(2**20), 2
toy_secret = toy_p // 3
toy_pub = pow(toy_g, toy_secret, toy_p)
t0 = time.perf_counter()
for cand in range(toy_p):
if pow(toy_g, cand, toy_p) == toy_pub:
found = cand; break
t_brute = time.perf_counter() - t0
print(f"\ntoy p bit-length = {toy_p.bit_length()}; brute-force discrete-log: "
f"{t_brute:.3f}s, {found:,} candidates")
print(f"real p bit-length = {P.bit_length()}; একই loop-এর candidate-সংখ্যা ~2^{P.bit_length()}")
print(f"— toy p মাত্র {P.bit_length()/toy_p.bit_length():.0f}x ছোট বিট-এ, "
f"backward-cost-এর পার্থক্য astronomical (exponential)")
আসল রান (private exponent আর timing মেশিন-নির্ভর বদলাবে, structure একই থাকবে):
real DH prime bit-length = 2048 বিট
Alice public গণনা করতে: 21.761 ms, Bob public গণনা করতে: 19.106 ms
shared secret মেলে: True (bit-length 2047)
toy p bit-length = 21; brute-force discrete-log: 0.651s, 349,527 candidates
real p bit-length = 2048; একই loop-এর candidate-সংখ্যা ~2^2048
— toy p মাত্র 98x ছোট বিট-এ, backward-cost-এর পার্থক্য astronomical (exponential)
শিক্ষাটা asymmetric-crypto লেসনের trapdoor-experiment-এরই সরাসরি
সম্প্রসারণ: forward দিক (public value গণনা) real ২০৪৮-বিট prime-এও
মাত্র ~২০ মিলিসেকেন্ড — ব্যবহারযোগ্য। toy 21-বিট prime-এ backward
(brute-force) মাত্র 0.65 সেকেন্ডে ভেঙে গেছে; real 2048-বিট prime-এ
সেই একই আক্রমণ মহাবিশ্বের বয়স ছাড়িয়ে যায় — বিট-সংখ্যা মাত্র ~১০০ গুণ
বাড়ায় backward-cost exponentially বেড়েছে, forward প্রায় অপরিবর্তিত।
Checkpoint: alice_shared == bob_shared সবসময় True; P.bit_length()
সবসময় 2048; toy-loop-এর candidate-সংখ্যা প্রতিবার আলাদা (random secret)
কিন্তু order-of-magnitude সবসময় toy_p-এর কাছাকাছি।
৬. Integration — DH + HKDF + AES-GCM মিলিয়ে একটা secure channel
শেষ ধাপ: raw DH shared-secret সরাসরি AES key হিসেবে বসানো ঠিক না — সেটা একটা সংখ্যা, uniformly-random byte-string না, আর কোনো context-binding নেই। HKDF (RFC 5869) সেই ফাঁক পূরণ করে — shared secret-কে একটা properly-derived, fixed-length, context-bound key-তে রূপান্তর করে:
# secure_channel.py — DH shared secret -> HKDF -> AES-256-GCM: একটা minimal secure channel
import secrets
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
class Party:
def __init__(self, name):
self.name = name
self.private = secrets.randbelow(P - 2) + 2
self.public = pow(G, self.private, P)
self.aead_key = None
def derive_channel_key(self, peer_public, info: bytes):
shared_int = pow(peer_public, self.private, P)
shared_bytes = shared_int.to_bytes(256, "big")
hkdf = HKDF(algorithm=hashes.SHA256(), length=32, salt=None, info=info)
self.aead_key = hkdf.derive(shared_bytes)
return self.aead_key
class SecureChannel:
def __init__(self, key):
self.aead = AESGCM(key)
self._counter = 0
def send(self, plaintext, aad=b""):
nonce = self._counter.to_bytes(12, "big") # per-channel monotonic — reuse অসম্ভব
self._counter += 1
return nonce, self.aead.encrypt(nonce, plaintext, aad)
def receive(self, nonce, ciphertext, aad=b""):
return self.aead.decrypt(nonce, ciphertext, aad)
alice, bob = Party("Alice"), Party("Bob")
info = b"alice<->bob secure-channel v1"
alice_key = alice.derive_channel_key(bob.public, info)
bob_key = bob.derive_channel_key(alice.public, info)
print(f"দুইজনের derived AES key মেলে: {alice_key == bob_key}")
alice_ch, bob_ch = SecureChannel(alice_key), SecureChannel(bob_key)
messages = [
b"hello bob, this channel key never touched the network in the clear",
b"transfer: $42.00 to bob-savings",
b"session complete",
]
for i, m in enumerate(messages, 1):
aad = f"seq={i}".encode()
nonce, ct = alice_ch.send(m, aad)
pt = bob_ch.receive(nonce, ct, aad)
print(f" msg {i}: sent={len(m):3d}B ciphertext={len(ct):3d}B decrypt_ok={pt == m}")
আসল রান:
দুইজনের derived AES key মেলে: True
msg 1: sent= 66B ciphertext= 82B decrypt_ok=True
msg 2: sent= 31B ciphertext= 47B decrypt_ok=True
msg 3: sent= 16B ciphertext= 32B decrypt_ok=True
Eve — passive eavesdropper — যা দেখেছে: p, g, দুইজনের ২০৪৮-বিট
public value, আর প্রতিটা ciphertext+nonce। যা দেখেনি: দুই private
exponent, shared secret integer, আর derived AES key। এই একটা প্রোগ্রাম
এখন পর্যন্ত পড়া চারটা লেসনের প্রতিটা primitive-কে একটা কাজ-করা pipeline-এ
জুড়ে দিয়েছে — key-agreement (DH) থেকে key-derivation (HKDF) থেকে
authenticated bulk-encryption (GCM) পর্যন্ত, ঠিক TLS/SSH-এর মূল কাঠামোর
মতোই, শুধু TLS-এর সার্টিফিকেট/signature স্তর বাদে।
Checkpoint: alice_key == bob_key সবসময় True, তিনটা message-এই
decrypt_ok=True; info parameter বদলে দুইজনের একজনকে ভিন্ন context
দিন — key mismatch হয়ে সব decrypt fail করবে, প্রমাণ করে context-binding
সত্যিই কার্যকর।
নিজেকে চ্যালেঞ্জ করুন
sha256_scratch.py-এর নিজের implementation-এ length-extension attack চালান — hash-functions লেসনের toy-hash-এর মতোই কৌশল, কিন্তু এবার আসল SHA-256 padding নিয়ম মেনে; দেখানH(secret||message)-কে MAC হিসেবে ব্যবহার করলে এখানেও একই বিপর্যয় ঘটে- নিজের SHA-256-এর avalanche effect পরিমাপ করুন — কয়েক হাজার random ১-বিট-different input জোড়া চালিয়ে bit-diff-এর mean আর standard deviation বের করুন, hashlib.sha256-এর সাথে তুলনা করে দেখান দুইটার distribution আলাদা করা যায় না
- Toy RSA-তে proper OAEP padding যোগ করুন (asymmetric-crypto লেসনের textbook-RSA-র সীমাবদ্ধতা মনে রেখে) — কেন padding ছাড়া RSA deterministic থেকে যায় আর সেটা কী ফাঁস করে দেখান
- Encrypt-then-MAC বনাম MAC-then-encrypt দুইটাই বাস্তবায়ন করুন (AES-CBC
- HMAC দিয়ে) — MAC-then-encrypt ভার্সনে ইচ্ছাকৃতভাবে padding-oracle পরিস্থিতি তৈরি করে দেখান কেন ক্রম গুরুত্বপূর্ণ
- ChaCha20-Poly1305 (
cryptographyলাইব্রেরিতেই আছে) দিয়ে ধাপ ৪-এর secure channel আবার বানান, AES-GCM-এর সাথে throughput বেঞ্চমার্ক তুলনা করুন — hardware AES-NI ছাড়া কোনটা এগিয়ে - DH exchange-এ একটা active man-in-the-middle simulate করুন (asymmetric-crypto লেসনের misconception অংশ মনে করে) — দেখান plain DH কীভাবে ভাঙে, আর কোন অতিরিক্ত উপাদান (signature/authentication) সেটা ঠেকাতে পারত
যা এখানে শিখলেন, পরে যেখানে লাগবে
| এখানে | পরে |
|---|---|
| SHA-256 internals (padding, compression, Merkle–Damgård) | Blockchain hash-puzzle বোঝা, Git-এর SHA-1→SHA-256 migration decision পড়া, HMAC কেন লাগে যুক্তি দেওয়া |
| ECB leak / nonce-reuse / AEAD tamper-detection বাস্তব AES দিয়ে | Code review-তে crypto misuse ধরা, TLS/SSH library configuration audit |
| RFC 3526 real-size DH parameters ও size-vs-security trade-off | TLS handshake-এর key-exchange group পছন্দ, VPN (IPsec) config review |
| HKDF দিয়ে shared-secret থেকে key derive করা | Signal/Noise Protocol-এর key-ratchet design পড়া, session-key hierarchy বোঝা |
| DH+HKDF+AEAD জুড়ে end-to-end secure channel design | Production auth/messaging system security review, “কেন audited library” যুক্তি দৃঢ়ভাবে দেওয়া |