---
title: 'Tuz ve Biber'
source: 'https://academia.sh/tr/kurslar/kimlik-ve-yetki/tuz-ve-biber'
course: 'Kimlik Doğrulama ve Yetkilendirme'
language: tr
updated: '2026-08-17T18:06:49+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

Ö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.

```bash
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
```

```text
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.

```bash
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
```

```text
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.

```bash
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
```

```text
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.
