Ders 13 / 23
Tuz ve Biber
Kayıt başına rastgele tuzun ön hesaplanmış tablolara karşı ne sağladığı, tuzsuz kurulumun sızdırdığı bilgi, tuzun ve biberin ayrı görevleri ve biberin veritabanı dışında tutulmasının sızıntı anındaki sonucu.
İçindekiler
Önceki derste her kayıt için randomBytes ile on altı baytlık bir değer üretildi ve aynı
parolanın iki kaydı farklı çıktı. O değerin adı tuz (salt) ve görevi karmayı gizlemek
değil, kayıtlar arasındaki eşitliği bozmaktır. Bu ders tuzun tam olarak neyi engellediğini,
tuzsuz bir kurulumun neyi ele verdiğini ve tuzdan ayrı ikinci bir değer olan biberin nerede
durduğunu ele alıyor.
Soru şuradan doğar: yavaş karma seçildi, parametreler ölçülerek ayarlandı, kayıt biçimi kuruldu. Kayıtlar sızarsa her birinin ayrı ayrı ele alınması gerekir mi, yoksa tek bir iş tüm kayıtlara birden mi yarar? Tuz, bu iki durumu birbirinden ayıran şeydir.
Tuzun Görevi
Karma fonksiyonu belirlenimcidir: aynı girdi her zaman aynı çıktıyı verir. Bu özellik doğrulamayı mümkün kılan şeydir, ama tek başına kullanıldığında istenmeyen bir sonucu vardır — aynı parolayı seçen iki üyenin kaydı birebir aynı olur.
Tuz, karma hesabına giren ikinci bir girdidir. Her kayıt için ayrı üretilir, gizli değildir ve karmanın yanında saklanır. Sonucu şudur: aynı parola farklı tuzlarla farklı karmalar üretir. Doğrulama sırasında tuz kayıttan okunur, kullanıcının gönderdiği parolayla birleştirilir, sonuç karşılaştırılır. Doğrulama akışı değişmez; değişen, kayıtlar arasındaki ilişkidir.
Tuzsuz Kurulumun Gözlenebilir Sonucu
Aşağıdaki betik kütüphane üye tablosunu iki sütunla kuruyor: aynı parolaların tuzsuz ve tuzlu karmaları. Veri yereldir ve örnek amaçlıdır; sekiz üyeden üçü aynı parolayı, ikişer üye de iki ayrı parolayı paylaşıyor.
cat > tuz.mjs <<'EOF' import { createHash, randomBytes, scryptSync } from "node:crypto"; import { DatabaseSync } from "node:sqlite"; const db = new DatabaseSync(":memory:"); db.exec(`CREATE TABLE uye(kod TEXT PRIMARY KEY, sube TEXT, tuzsuz TEXT, tuzlu TEXT)`); // Yerel ornek veri; kurulum farkini gostermek icin. const uyeler = [ ["U-1001", "S-01", "deniz-kabugu-7"], ["U-1002", "S-01", "kutuphane"], ["U-1003", "S-02", "deniz-kabugu-7"], ["U-1004", "S-02", "kutuphane"], ["U-1005", "S-01", "yaz-2026-okuma"], ["U-1006", "S-03", "kutuphane"], ["U-1007", "S-03", "cinar-agaci"], ["U-1008", "S-02", "yaz-2026-okuma"], ]; const ayar = { N: 2 ** 12, r: 8, p: 1 }; const ekle = db.prepare("INSERT INTO uye VALUES (?,?,?,?)"); for (const [kod, sube, parola] of uyeler) { const tuzsuz = createHash("sha256").update(parola).digest("base64"); const tuz = randomBytes(16); const tuzlu = tuz.toString("base64") + "$" + scryptSync(parola, tuz, 32, ayar).toString("base64"); ekle.run(kod, sube, tuzsuz, tuzlu); } const yaz = (baslik, satirlar) => { console.log(baslik); for (const s of satirlar) console.log(" " + s); }; yaz("tuzsuz kayitlar (ilk 12 karakter):", db.prepare("SELECT kod, substr(tuzsuz,1,12) k FROM uye ORDER BY kod").all() .map((s) => `${s.kod} ${s.k}`)); yaz("\nayni karmayi paylasan uyeler:", db.prepare(`SELECT substr(tuzsuz,1,12) k, COUNT(*) adet, group_concat(kod,' ') kimler FROM uye GROUP BY tuzsuz HAVING COUNT(*) > 1`).all() .map((s) => `${s.k} ${s.adet} uye: ${s.kimler}`)); yaz("\ntuzlu kayitlarda ayni gruplama:", db.prepare(`SELECT COUNT(*) grup FROM (SELECT tuzlu FROM uye GROUP BY tuzlu HAVING COUNT(*) > 1)`).all() .map((s) => `eslesen grup sayisi: ${s.grup}`)); EOF node tuz.mjs
tuzsuz kayitlar (ilk 12 karakter): U-1001 LCg5/0sSaTRk U-1002 w+UPBHK8+dLk U-1003 LCg5/0sSaTRk U-1004 w+UPBHK8+dLk U-1005 QHYLNUanoH3r U-1006 w+UPBHK8+dLk U-1007 djFHjvQkKdlh U-1008 QHYLNUanoH3r ayni karmayi paylasan uyeler: LCg5/0sSaTRk 2 uye: U-1001 U-1003 QHYLNUanoH3r 2 uye: U-1005 U-1008 w+UPBHK8+dLk 3 uye: U-1002 U-1004 U-1006 tuzlu kayitlarda ayni gruplama: eslesen grup sayisi: 0
Tek bir GROUP BY sorgusu, parolaları hiç çözmeden üç bilgi üretti.
Birincisi, hangi üyelerin aynı parolayı kullandığı. U-1002, U-1004 ve U-1006 aynı dizgiyi seçmiş. Bu üç hesap artık tek bir hesap gibidir: biri hakkında öğrenilen her şey diğer ikisi için de geçerlidir.
İkincisi, en kalabalık grubun hangisi olduğu. Üç üyeli grup, bu tabloda en yaygın seçimi işaret eder. Yaygın seçim, aday listelerinin başında duran seçimdir.
Üçüncüsü, bir yönetici ya da personel hesabının parolasının bir üye hesabıyla aynı olup olmadığı. Bu, hesaplar arası bir bağdır ve tabloya bakan herkese açıktır.
Tuzlu sütunda aynı sorgu sıfır grup döndürüyor. Parolalar değişmedi; değişen, kayıtların birbirine bakarak bilgi vermesi. Tuz bir parolayı güçlendirmez — parolalar arasındaki görünür ilişkiyi yok eder.
Ön Hesaplanmış Tablonun Maliyeti
Tuzun ikinci ve daha bilinen etkisi ön hesaplanmış tablolara (precomputed table) karşıdır. Aday parolaların karmaları bir kez hesaplanıp saklandığında, sızmış bir tuzsuz kayıt için yapılacak iş bir arama işlemine iner; hesap zaten yapılmıştır. Bu tabloların yer kazanmak için sıkıştırılmış biçimine gökkuşağı tablosu denir; kurulum tarafındaki karşılığı ikisinde de aynıdır.
Tuz bu ekonomiyi bozar. Tuz kayıt başına farklı olduğu için, hazır tablo hiçbir kayda uymaz; iş her kayıt için baştan yapılır. Aşağıdaki betik, önceki derste ölçülen sürelerle bu farkı sayıya çeviriyor.
cat > maliyet.mjs <<'EOF' const ADAY = 1_000_000; // on hesaplanmis listedeki aday sayisi const KULLANICI = 100_000; // sizan kayit sayisi const HIZLI_MS = 0.0013; // olculen: sha-256 tek gecis const YAVAS_MS = 18.2; // olculen: scrypt N=2^14 function bicimle(ms) { const s = ms / 1000; if (s < 3600) return `${s.toFixed(1)} saniye`; const saat = s / 3600; if (saat < 24 * 365) return `${saat.toFixed(1)} saat`; return `${(saat / 24 / 365).toFixed(1)} yil`; } const kurulumlar = [ ["tuzsuz + hizli karma", ADAY * HIZLI_MS, "liste bir kez uretilir, her kayda uyar"], ["tuzsuz + yavas karma", ADAY * YAVAS_MS, "liste bir kez uretilir, her kayda uyar"], ["tuzlu + hizli karma", ADAY * KULLANICI * HIZLI_MS, "her kayit icin bastan is"], ["tuzlu + yavas karma", ADAY * KULLANICI * YAVAS_MS, "her kayit icin bastan is"], ]; console.log(`aday: ${ADAY.toLocaleString("tr-TR")} sizan kayit: ${KULLANICI.toLocaleString("tr-TR")}`); console.log("kurulum | tek cekirdekte is | not"); console.log("---------------------|-------------------|---------------------------------------"); for (const [ad, ms, not] of kurulumlar) { console.log(ad.padEnd(20) + " | " + bicimle(ms).padStart(17) + " | " + not); } EOF node maliyet.mjs
aday: 1.000.000 sizan kayit: 100.000 kurulum | tek cekirdekte is | not ---------------------|-------------------|--------------------------------------- tuzsuz + hizli karma | 1.3 saniye | liste bir kez uretilir, her kayda uyar tuzsuz + yavas karma | 5.1 saat | liste bir kez uretilir, her kayda uyar tuzlu + hizli karma | 36.1 saat | her kayit icin bastan is tuzlu + yavas karma | 57.7 yil | her kayit icin bastan is
Tablo iki savunmanın birbirini nasıl çarptığını gösteriyor. Yalnız yavaş karma, tuzsuz kurulumda beş saatlik bir işle karşılanıyor — çünkü iş bir kez yapılıp yüz bin kaydın tamamına uygulanıyor. Yalnız tuz, hızlı karmayla otuz altı saate çıkıyor. İkisi birlikte kullanıldığında sayı elli yedi yıla gidiyor.
Buradaki mutlak sayılar bir tahmin değil, ölçülmüş sürelerin çarpımıdır ve donanıma göre değişir. Değişmeyen, çarpanın yapısıdır: tuz, işi kayıt sayısıyla çarpar; yavaş karma, işi birim maliyetle çarpar. İkisi ayrı sorunları çözer ve biri diğerinin yerine geçmez.
Tuzun Kuralları
Tuzun doğru kurulumu dört kuralla tanımlanır.
Kayıt başına ayrı üretilir. Uygulama genelinde tek bir tuz kullanmak, tuzsuz kurulumun eşitlik sorununu olduğu gibi bırakır — aynı parolayı kullanan iki kayıt yine aynı karmayı verir.
Rastgele üretilir. Kullanıcı kodundan, e-posta adresinden ya da kayıt zamanından türetilen tuz, öngörülebilir olduğu için ön hesaplamayı yeniden mümkün kılar. Kaynak, kriptografik rastgele sayı üretecidir.
Yeterli uzunlukta olur. On altı bayt yerleşik seçimdir. Ölçüt, tuzların çakışma olasılığının önemsiz kalmasıdır; kısa tuz, farklı kayıtların aynı tuza düşmesine ve o kayıtlar arasında eşitliğin geri gelmesine yol açar.
Gizli değildir. Tuz karmanın yanında, aynı satırda saklanır. Gizli tutulması gereken bir değer değildir; görevi gizlilik değil çeşitliliktir. Tuzu gizli saymak, biberi tuz yerine kullanmaya yol açan yaygın karışıklıktır.
Son kural, parola değiştirme akışını da belirler: parola değiştiğinde tuz da yenilenir. Eski tuzun korunması, yeni karmanın eskisiyle karşılaştırılabilir kalmasına yol açar.
Biber: Veritabanının Dışındaki Değer
Biber (pepper) tuzdan üç noktada ayrılır: tüm kayıtlar için ortaktır, gizlidir ve veritabanında durmaz. Uygulamanın yapılandırmasında — ortam değişkeninde ya da gizli değer deposunda — tutulur. Sunucu Tarafı Temelleri kursunda gizli değerlerin kod ve veri yollarından ayrılması kurulmuştu; biber o ayrımın parola tarafındaki uygulamasıdır.
Uygulanışı anahtarlı karma (HMAC) iledir: yavaş karmanın çıktısı, biber anahtarıyla bir kez daha karmalanır. Kayda yazılan değer bu son çıktıdır.
cat > biber.mjs <<'EOF' import { scryptSync, randomBytes, createHmac, timingSafeEqual } from "node:crypto"; const AYAR = { N: 2 ** 12, r: 8, p: 1 }; const BIBERLER = { v1: process.env.BIBER_V1 ?? "" }; // ortam degiskeninden okunur function kaydet(parola, surum = "v1") { const tuz = randomBytes(16); const yavas = scryptSync(parola, tuz, 32, AYAR); const ozet = createHmac("sha256", BIBERLER[surum]).update(yavas).digest(); return [surum, tuz.toString("base64"), ozet.toString("base64")].join("$"); } function dogrula(parola, kayit) { const [surum, tuz64, ozet64] = kayit.split("$"); const biber = BIBERLER[surum]; if (biber === undefined) return "biber bulunamadi"; const yavas = scryptSync(parola, Buffer.from(tuz64, "base64"), 32, AYAR); const uretilen = createHmac("sha256", biber).update(yavas).digest(); return timingSafeEqual(uretilen, Buffer.from(ozet64, "base64")); } const kayit = kaydet("deniz-kabugu-7"); console.log("veritabanina yazilan :", kayit); console.log("biber kaydin icinde mi:", kayit.includes(BIBERLER.v1)); console.log("dogru biber, dogru parola :", dogrula("deniz-kabugu-7", kayit)); console.log("dogru biber, yanlis parola :", dogrula("deniz-kabugu-8", kayit)); BIBERLER.v1 = "baska-bir-deger"; // biber bilinmiyor console.log("yanlis biber, dogru parola :", dogrula("deniz-kabugu-7", kayit)); EOF BIBER_V1='ornek-uygulama-sirri' node biber.mjs
veritabanina yazilan : v1$grq+kKcerhuc05CsczIyCA==$TdBHs+6NHkjyTM7gfYgMOpd5k0yIYh9/AAsphp+3h0k= biber kaydin icinde mi: false dogru biber, dogru parola : true dogru biber, yanlis parola : false yanlis biber, dogru parola : false
Tuz alanı her çalıştırmada değişir; satırların anlamı değişmez. İkinci satır biberin kayda hiçbir biçimde girmediğini doğruluyor. Son satır ise biberin işlevini gösteriyor: parola doğru olduğu hâlde, biber bilinmediğinde doğrulama olumsuz dönüyor.
Bunun sızıntı anındaki karşılığı şudur. Yalnız veritabanı sızmışsa — yedek dosyası, salt okunur rapor bağlantısı, hatalı bir dışa aktarım — eldeki kayıtlar üzerinde aday denemek işe yaramaz, çünkü hesabın son adımı için gereken anahtar orada değildir. Biber, tuzun ve yavaş karmanın üstüne yalnız bu senaryoyu ekler.
Biberin Sınırları ve Döndürülmesi
Biberin koruma alanı dar tanımlanmalıdır. Uygulama sunucusu da ele geçirilmişse biber de ele geçmiştir; o durumda kurulum, bibersiz kurulumla aynı düzeye iner. Bu yüzden biber, tuzun ya da yavaş karmanın yerini almaz — ikisinin üstüne konur.
İkinci sınır işletimseldir. Biber kaybolursa hiçbir kullanıcı giriş yapamaz ve kayıtlardan geri dönüş yoktur; tek çıkış, tüm parolaların sıfırlanmasıdır. Bu yüzden biber, uygulama yapılandırmasının en dikkatli yedeklenen parçasıdır ve kütük kayıtlarına, hata iletilerine ya da tanılama çıktısına asla girmez.
Üçüncüsü döndürmedir. Kayıttaki v1 alanı bunun içindir: yeni bir biber eklendiğinde eski
sürüm doğrulama için yapılandırmada kalır, başarılı her girişte kayıt yeni sürümle yeniden
üretilir. Bu, önceki dersteki parametre yükseltmesiyle aynı yordamdır — iki durumda da
geçişi mümkün kılan şey, kaydın kendi üretim koşullarını taşımasıdır.
Özet
- Tuz kayıt başına üretilen, gizli olmayan ve karmanın yanında saklanan bir değerdir; görevi kayıtlar arasındaki eşitliği bozmaktır.
- Tuzsuz kurulumda tek bir gruplama sorgusu, parolalar çözülmeden aynı parolayı kullanan hesapları ve en yaygın seçimi ortaya çıkarır.
- Tuz, ön hesaplanmış tablonun maliyetini kayıt sayısıyla çarpar; yavaş karma birim maliyeti artırır. İkisi ayrı sorunları çözer.
- Tuz kayıt başına, rastgele ve yeterli uzunlukta üretilir; parola değiştiğinde yenilenir.
- Biber tüm kayıtlar için ortak, gizli ve veritabanı dışında tutulan bir anahtardır; yalnız veritabanının sızdığı durumda koruma sağlar ve sürüm alanıyla döndürülür.
Sonraki Adım
Buraya kadarki savunmaların hepsi sunucu tarafındaydı ve kullanıcının seçtiği parolaya dokunmuyordu. Oysa gruplama sorgusunda görülen şey — üç üyenin aynı dizgiyi seçmesi — sunucu tarafındaki hiçbir parametreyle giderilemez. Sonraki ders parola politikalarına geçiyor: uzunluk ve karmaşıklık kurallarının kullanıcı davranışını nasıl değiştirdiğini bir model üzerinden hesaplıyor ve sızmış parola listesine karşı denetimin, listeyi ya da parolayı açığa çıkarmadan nasıl yapılacağını gösteriyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.