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

# Parola Saklama

Parolanın geri döndürülemez biçimde saklanması: yavaş karma algoritmalarının iş ve bellek katsayıları, süre bütçesine göre parametre seçimi, saklama kaydının biçimi ve karşılaştırmanın sabit zamanlı yapılması.

Önceki konu kimliğin nasıl doğrulandığını kurdu: temel şemadan oturuma, belirteçten çok
etmenli doğrulamaya kadar akışların tamamı, sunucunun "bu istek gerçekten o kişiden mi
geliyor" sorusuna verdiği yanıtlardı. Bu akışların hepsinin altında tek bir varsayım
duruyordu — sunucu, kullanıcının bildiği bir şeyi doğrulayabiliyor. Bu konu o varsayımın
kendisine bakıyor: sunucu parolayı doğrulayabilmek için ne saklıyor?

Kütüphane ödünç servisinde üyeler ve personel aynı sisteme parolayla giriyor. Sunucunun
elindeki kayıt, bir gün başkasının eline geçebilir — yedek dosyası, hatalı bir dışa
aktarım, ele geçirilmiş bir rapor hesabı. Parola saklamanın ölçütü bu andır: kayıt
sızdığında ne kadar bilgi sızmış olur? Bu dersin tamamı, o anın maliyetini düşürmekle
ilgilidir.

## Doğrulanabilir Ama Geri Döndürülemez

Sunucunun parolayı doğrulayabilmesi için parolayı bilmesi gerekmez. Gereken, kullanıcının
gönderdiği dizgiden aynı sonucu üretebilmektir. Bu yüzden parola düz metin olarak değil,
**tek yönlü karma (one-way hash)** olarak saklanır: girdiden çıktıyı hesaplamak kolay,
çıktıdan girdiye dönmek hesaplama açısından yapılamaz olsun.

Karma fonksiyonu kavramı Veri Yapıları kursunda tanıtılmıştı; oradaki amaç kova numarası
üretmekti ve ölçüt hızdı. Parola saklamada ölçüt terse döner. Karma ne kadar hızlı
hesaplanıyorsa, sızmış bir kayıt üzerinde aday parola denemek de o kadar hızlıdır.
Doğrulamanın hızlı olması saldırganın işini kolaylaştırır; yavaş olması ise yalnızca giriş
isteğini birkaç on milisaniye geciktirir. Bu asimetri, parola saklamanın tek tasarım
kaldıracıdır.

Bu nedenle parola için genel amaçlı karma fonksiyonları değil, **anahtar türetme
fonksiyonu (key derivation function)** kullanılır. Bunlar iki ayarlanabilir katsayı taşır:
**iş katsayısı (work factor)**, hesaplamanın kaç tur süreceğini; **bellek katsayısı**,
hesaplamanın ne kadar bellek tutacağını belirler. Belleği zorunlu kılan tasarıma **bellek
sertliği (memory hardness)** denir ve amacı, hesabı çok sayıda küçük işlem birimine
dağıtmayı pahalı hâle getirmektir. `scrypt` ve `Argon2` bu sınıfın iki üyesidir; bu ders
`node` çalışma zamanında yerleşik olan `scrypt` ile ilerliyor.

## Maliyetin Ölçülmesi

İki yaklaşımın farkı tartışılmaz, ölçülür. Aşağıdaki betik aynı makinede tek geçişli bir
karma ile farklı iş katsayılarındaki `scrypt` çağrısının doğrulama süresini karşılaştırır.
Son sütun, tek bir çekirdeğin saniyede kaç doğrulama yapabildiğidir.

```bash
cat > olcum.mjs <<'EOF'
import { createHash, scryptSync, randomBytes } from "node:crypto";

const parola = "sahil-subesi-2026";
const tuz = randomBytes(16);

function sure(kere, is) {
  is();                                        // isinma
  const basla = process.hrtime.bigint();
  for (let i = 0; i < kere; i++) is();
  return Number(process.hrtime.bigint() - basla) / kere / 1e6;   // milisaniye
}

const satirlar = [
  ["sha-256 (tek gecis)", "-", sure(500, () =>
    createHash("sha256").update(tuz).update(parola).digest())],
];

for (const N of [2 ** 12, 2 ** 14, 2 ** 16, 2 ** 17]) {
  const r = 8, p = 1;
  satirlar.push([
    `scrypt N=2^${Math.log2(N)} r=${r} p=${p}`,
    `${((128 * r * N) / 1024 / 1024).toFixed(0)} MiB`,
    sure(3, () => scryptSync(parola, tuz, 32, { N, r, p, maxmem: 512 * 1024 * 1024 })),
  ]);
}

console.log("algoritma              | bellek  | dogrulama  | cekirdek basina giris/s");
console.log("-----------------------|---------|------------|------------------------");
for (const [ad, bellek, ms] of satirlar) {
  console.log(
    ad.padEnd(22) + " | " + bellek.padStart(7) + " | " +
    (ms < 1 ? ms.toFixed(4) : ms.toFixed(1)).padStart(7) + " ms | " +
    (1000 / ms).toFixed(0).padStart(22)
  );
}
EOF
node olcum.mjs
```

```text
algoritma              | bellek  | dogrulama  | cekirdek basina giris/s
-----------------------|---------|------------|------------------------
sha-256 (tek gecis)    |       - |  0.0013 ms |                 781504
scrypt N=2^12 r=8 p=1  |   4 MiB |     4.3 ms |                    231
scrypt N=2^14 r=8 p=1  |  16 MiB |    17.6 ms |                     57
scrypt N=2^16 r=8 p=1  |  64 MiB |    79.4 ms |                     13
scrypt N=2^17 r=8 p=1  | 128 MiB |   159.5 ms |                      6
```

Mutlak süreler makineye ve çalışma zamanı sürümüne göre değişir; buradaki değerler
`node` sürüm 24 ile tek bir masaüstü çekirdeğinde alınmıştır. Değişmeyen şey büyüklük
sırasıdır. Tek geçişli karma ile `N=2^14` arasında yaklaşık on dört bin katlık bir fark
var. Bu fark, sızmış bir kayıt üzerinde aday deneyen tarafın maliyetiyle doğrudan
orantılıdır: hızlı karmayla saniyede yüz binlerce aday denenebilirken, yavaş karmayla
aynı donanımda onlarca aday denenebilir.

Bellek sütunu ikinci savunmayı gösterir. `N=2^16` için her doğrulama 64 MiB tutar. Bu
miktar tek bir sunucu için önemsizdir; binlerce koşutlu hesap birimi üzerinde aynı işi
yapmak isteyen taraf için belirleyicidir. İş katsayısını artırmak hesabı yavaşlatır,
bellek katsayısını artırmak hesabı ucuz donanıma dağıtılamaz kılar.

## İş Katsayısının Seçimi

Parametreler "ne kadar büyükse o kadar iyi" kuralıyla seçilmez. Doğrulama sunucunun
kendi işidir; çok büyük bir katsayı, giriş isteklerinin sunucuyu kilitlemesine yol açar.
Seçimin iki kısıtı vardır: giriş başına ayrılan süre bütçesi ve tepe yükte tutulacak
bellek.

Aşağıdaki betik bu iki kısıtı birlikte uygular. Bütçe giriş başına 100 milisaniye, tepe
yük saniyede yirmi giriş kabul edilmiştir; bellek sütunu, o katsayıda aynı anda
yürüyecek doğrulamaların toplam tüketimidir.

```bash
cat > kalibre.mjs <<'EOF'
import { scryptSync, randomBytes } from "node:crypto";

const BUTCE_MS = 100;                     // giris basina ayrilan sure
const ESZAMANLI_GIRIS = 20;               // saniyede beklenen tepe giris sayisi
const tuz = randomBytes(16), r = 8, p = 1;

function olc(N) {
  const ayar = { N, r, p, maxmem: 512 * 1024 * 1024 };
  scryptSync("olcum", tuz, 32, ayar);
  const basla = process.hrtime.bigint();
  scryptSync("olcum", tuz, 32, ayar);
  return Number(process.hrtime.bigint() - basla) / 1e6;
}

console.log(`butce: ${BUTCE_MS} ms/giris, tepe yuk: ${ESZAMANLI_GIRIS} giris/s`);
console.log("N     | dogrulama | tepe yukte bellek | butce");
console.log("------|-----------|-------------------|-------");
let secilen = null;
for (const us of [12, 13, 14, 15, 16, 17]) {
  const N = 2 ** us, ms = olc(N);
  const bellek = (128 * r * N * Math.ceil(ms / 1000 * ESZAMANLI_GIRIS)) / 1024 / 1024;
  const uyar = ms <= BUTCE_MS ? "uygun" : "asildi";
  if (ms <= BUTCE_MS) secilen = us;
  console.log(`2^${us}  | ${ms.toFixed(1).padStart(6)} ms | ${bellek.toFixed(0).padStart(13)} MiB | ${uyar}`);
}
console.log("secilen: N=2^" + secilen);
EOF
node kalibre.mjs
```

```text
butce: 100 ms/giris, tepe yuk: 20 giris/s
N     | dogrulama | tepe yukte bellek | butce
------|-----------|-------------------|-------
2^12  |    5.3 ms |             4 MiB | uygun
2^13  |    8.7 ms |             8 MiB | uygun
2^14  |   18.2 ms |            16 MiB | uygun
2^15  |   37.7 ms |            32 MiB | uygun
2^16  |   77.8 ms |           128 MiB | uygun
2^17  |  162.7 ms |           512 MiB | asildi
secilen: N=2^16
```

Betiğin ürettiği sayılar makineye özgüdür; ürettiği **karar yordamı** değildir. Kural
şudur: bütçeyi aşmayan en büyük katsayı seçilir ve seçim, kodun içine gömülü bir sabit
değil, dağıtım yapılandırmasından okunan bir değer olur. Sunucu donanımı değiştiğinde
yeniden ölçülür.

Bütçenin kendisi de bir karardır. Giriş, kullanıcının bekleyeceği bir işlemdir ve yüz
milisaniye bu bekleyişte fark edilmez. Aynı hesap her istekte yapılmaz — yalnız girişte
yapılır, sonrasında oturum ya da belirteç devreye girer. Doğrulamanın her API çağrısında
tekrarlandığı bir kurulum, bu bütçeyi kullanılamaz hâle getirir; bu, parola karmasının
API anahtarı doğrulaması yerine kullanılmaması gerektiğinin nedenidir.

## Saklama Kaydının Biçimi

Kayıtta yalnız karma durmaz. Karmayı üreten algoritma ve parametreleri de saklanır;
aksi hâlde parametreler yükseltildiğinde eski kayıtlar doğrulanamaz hâle gelir. Yerleşik
biçim, alanları tek bir dizgide ayraçla birleştirmektir.

```bash
cat > parola.mjs <<'EOF'
import { scryptSync, randomBytes, timingSafeEqual } from "node:crypto";

const AYAR = { N: 2 ** 14, r: 8, p: 1, uzunluk: 32 };

function kaydet(parola) {
  const tuz = randomBytes(16);
  const ozet = scryptSync(parola.normalize("NFKC"), tuz, AYAR.uzunluk, AYAR);
  return ["scrypt", AYAR.N, AYAR.r, AYAR.p,
          tuz.toString("base64"), ozet.toString("base64")].join("$");
}

function dogrula(parola, kayit) {
  const [alg, N, r, p, tuz64, ozet64] = kayit.split("$");
  if (alg !== "scrypt") return false;
  const beklenen = Buffer.from(ozet64, "base64");
  const uretilen = scryptSync(parola.normalize("NFKC"), Buffer.from(tuz64, "base64"),
    beklenen.length, { N: +N, r: +r, p: +p });
  return timingSafeEqual(uretilen, beklenen);
}

const kayit = kaydet("sahil-subesi-2026");
console.log("saklanan kayit :", kayit);
console.log("uzunluk        :", kayit.length, "karakter");
console.log("dogru parola   :", dogrula("sahil-subesi-2026", kayit));
console.log("yanlis parola  :", dogrula("sahil-subesi-2025", kayit));
console.log("ikinci kayit   :", kaydet("sahil-subesi-2026").slice(0, 24) + "...");
EOF
node parola.mjs
```

```text
saklanan kayit : scrypt$16384$8$1$/kFvW5D9IBMpOV7Sn9t0YQ==$JwrU7KOCzQiHUKCE8+Uazs9zrp1RhJgQVcYqhINzCsQ=
uzunluk        : 86 karakter
dogru parola   : true
yanlis parola  : false
ikinci kayit   : scrypt$16384$8$1$QVqPrxj...
```

Rastgele üretilen değerler her çalıştırmada değişir; kaydın uzunluğu ve alan düzeni
değişmez. Kayıt üç bilgi taşır: hangi algoritma, hangi parametreler, hangi rastgele değer.
Bu düzenin doğrudan sonucu, veritabanı sütununun sabit uzunlukta değil, biçim
değişikliklerine yer bırakan genişlikte tanımlanmasıdır.

Son satır bu dersin sonraki konusuna işaret eder: aynı parola ikinci kez kaydedildiğinde
kayıt farklıdır. Bunu sağlayan `randomBytes` çağrısıdır ve nedeni sonraki dersin konusu.

`normalize("NFKC")` çağrısı ayrı bir gerekçeye dayanır. Aynı görünen bir metin farklı bayt
dizileriyle kodlanabilir; kayıt sırasında bir biçim, giriş sırasında öteki biçim gelirse
doğrulama başarısız olur. Kullanıcı için bu, "doğru parolayı yazdım ama kabul etmiyor"
durumudur. Normalleştirme kayıt ve doğrulama yollarının **ikisinde birden** aynı biçimde
uygulanır; yalnız birinde uygulanması sorunu çözmez, taşır.

## Karşılaştırmanın Süresi

Doğrulamanın son adımı iki bayt dizisini karşılaştırmaktır ve bu adımın nasıl yazıldığı
önemlidir. Erken dönen bir karşılaştırma, ilk farklı bayttan sonra durur — dolayısıyla
çalışma süresi, adayın doğruyla kaç bayt boyunca örtüştüğünü ele verir.

```bash
cat > karsilastirma.mjs <<'EOF'
import { randomBytes, timingSafeEqual } from "node:crypto";

const dogru = randomBytes(32);

function erkenDonen(a, b) {                     // ilk farkta durur
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
  return true;
}

function olc(karsilastir, aday) {
  for (let i = 0; i < 2000; i++) karsilastir(dogru, aday);      // isinma
  const basla = process.hrtime.bigint();
  for (let i = 0; i < 200000; i++) karsilastir(dogru, aday);
  return Number(process.hrtime.bigint() - basla) / 200000;       // nanosaniye
}

const ilkte = Buffer.from(dogru); ilkte[0] ^= 1;
const sonda = Buffer.from(dogru); sonda[31] ^= 1;

console.log("karsilastirma   | ilk baytta ayrilan | son baytta ayrilan | oran");
console.log("----------------|--------------------|--------------------|------");
for (const [ad, fn] of [["erken donen", erkenDonen], ["timingSafeEqual", timingSafeEqual]]) {
  const a = olc(fn, ilkte), b = olc(fn, sonda);
  console.log(
    ad.padEnd(15) + " | " + (a.toFixed(1) + " ns").padStart(18) + " | " +
    (b.toFixed(1) + " ns").padStart(18) + " | " + (b / a).toFixed(2).padStart(5)
  );
}
EOF
node karsilastirma.mjs
```

```text
karsilastirma   | ilk baytta ayrilan | son baytta ayrilan | oran
----------------|--------------------|--------------------|------
erken donen     |             3.4 ns |            22.4 ns |  6.60
timingSafeEqual |            32.9 ns |            30.6 ns |  0.93
```

Nanosaniye değerleri makineye göre değişir; oran sütunu değişmez. Erken dönen
karşılaştırmada son baytta ayrılan aday, ilk baytta ayrılandan altı kat uzun sürüyor —
yani süre, örtüşmenin uzunluğunu ölçen bir sinyaldir. `timingSafeEqual` her durumda tüm
baytları işlediği için oran bire yakın kalıyor.

Parolanın kendisinde bu sinyalin uzaktan kullanılabilirliği düşüktür, çünkü karşılaştırma
öncesinde onlarca milisaniyelik bir karma hesabı vardır ve ağ gecikmesi sinyali gürültüye
gömer. Ama aynı karşılaştırma kalıbı, karma hesabı olmayan yerlerde de kullanılır:
oturum kimliği, sıfırlama belirteci, API anahtarı, imza doğrulaması. Bu yüzden kural
gevşetilmez — gizli bir değerle yapılan her karşılaştırma sabit zamanlı yazılır.
İstemci Güvenliği tarafında aynı kural istek belirteci karşılaştırması için konmuştu;
sunucu tarafı da aynı kuralı uygular.

## Parametrelerin Yükseltilmesi

Bugün seçilen katsayı, donanım ucuzladıkça yetersiz kalır. Yükseltme, kayıtta saklanan
parametrelerin varlığı sayesinde kesintisiz yapılabilir: kullanıcı giriş yaptığında
gönderdiği parola elde olur, doğrulama başarılıysa kayıt yeni parametrelerle yeniden
üretilip yazılır. Bu, girişteki bir ek yazma işlemidir ve kullanıcıya görünmez.

Yükseltmenin sınırı şudur: hiç giriş yapmayan hesapların kayıtları eski parametrelerde
kalır. Bu hesaplar için ikinci yol, eski karmanın üzerine yeni algoritmayı uygulayıp
kayda iki katmanlı olduğunu yazmaktır. Kayıt biçiminin algoritma alanı taşıması, bu geçişi
kayıt kaybı olmadan mümkün kılan şeydir.

Kabul edilmeyen üçüncü yol, parolaları düz metin olarak yeniden istemektir. Sistemin
parolayı yeniden isteyebilmesi, onu bir yerde tutabildiği anlamına gelir; bu dersin
tamamı bunun tersini kurar.

## Özet

- Parola geri döndürülemez biçimde saklanır; sunucunun doğrulama için parolayı bilmesi
  gerekmez, aynı sonucu üretebilmesi yeterlidir.
- Genel amaçlı karma fonksiyonları parola için uygun değildir; iş ve bellek katsayısı
  ayarlanabilen anahtar türetme fonksiyonları kullanılır.
- İş katsayısı, giriş başına süre bütçesini aşmayan en büyük değer olarak ölçülerek
  seçilir; tepe yükteki bellek tüketimi ikinci kısıttır.
- Kayıt, karmanın yanında algoritmayı ve parametreleri de taşır; yükseltme başarılı
  girişte kaydın yeniden üretilmesiyle yapılır.
- Gizli bir değerle yapılan karşılaştırma sabit zamanlı yazılır; erken dönen
  karşılaştırmanın süresi örtüşme uzunluğunu ele verir.

## Sonraki Adım

Bu derste her kayıt için `randomBytes` ile bir rastgele değer üretildi ve aynı parolanın
iki kaydı farklı çıktı. O değerin adı ve görevi henüz açıklanmadı. Sonraki ders tuzun ne
işe yaradığını, tuzsuz bir kurulumda aynı parolayı kullanan iki üyenin kaydının neden
birbirinin aynısı olduğunu ve bunun tek bir sızıntıdan kaç kullanıcı hakkında bilgi
çıkardığını gösteriyor. Aynı derste tuzdan ayrı ikinci bir değer olan biberin nerede
durduğu — veritabanının içinde değil, uygulamanın yapılandırmasında — ve bu ayrımın
sızıntı anında ne değiştirdiği ele alınıyor.
