Node.js'te bir veriyi daha sonra geri çözebileceğiniz biçimde şifrelemek için node:crypto modülündeki createCipheriv() ve createDecipheriv() fonksiyonlarını kullanırsınız. Bugün en yaygın tercih AES-256-GCM'dir. Kısa cevap: 32 baytlık rastgele bir anahtar ve her mesaj için yeni, 12 baytlık rastgele bir IV ile şifreler, final() sonrasında getAuthTag() ile 16 baytlık doğrulama etiketini alırsınız. IV, etiket ve şifreli veriyi birlikte saklar; çözerken setAuthTag() ile etiketi verirsiniz. Veri ya da etiket değiştirilmişse final() hata fırlatır.
Bu yazı, kişisel veri alanlarını veritabanında şifreli tutmak, yedek dosyalarını şifrelemek ya da bir token'ın içeriğini gizlemek gibi geri çözülebilir şifreleme ihtiyaçları içindir. Kullanıcı parolalarını saklamak farklı bir iştir: parola geri çözülmemeli, yavaş bir hash ile saklanmalıdır; bunun için parola hash'leme yazısına bakın. Özet ve HMAC konuları da crypto hash ve HMAC yazısında. Aşağıdaki bütün örnekler Node.js v26.13.0 ile çalıştırıldı; modül belgede Stability: 2 - Stable olarak işaretli.
AES-256-GCM neden tercih edilir?
GCM, belgenin "kimliği doğrulanmış şifreleme" (AEAD) olarak listelediği modlardan biridir; aynı listede CCM, OCB, SIV, GCM-SIV ve chacha20-poly1305 de vardır. AEAD modlarının ortak özelliği, şifreleme sırasında bir doğrulama etiketi (authentication tag) üretmeleridir. Çözme tarafı bu etiketi kullanarak verinin değiştirilip değiştirilmediğini anlar; tek bir bit bile değişmişse çözme başarısız olur. Belgenin bazı örneklerinde kullanılan aes-192-cbc gibi modlar ise AEAD listesinde yer almaz ve getAuthTag() ile bir etiket üretmez; bu modlarda şifreli metnin değiştirilip değiştirilmediğini denetlemek için ayrıca bir HMAC eklemeniz gerekir. GCM bu iki işi tek adımda yapar.
GCM'i CCM ve SIV gibi diğer AEAD modlarından pratikte ayıran nokta akış desteğidir. Belgeye göre CCM, SIV ve GCM-SIV modları mesajın tamamının tek bir update() çağrısıyla verilmesini ister; CCM ayrıca authTagLength ve plaintextLength seçeneklerini zorunlu kılar. GCM'de ise update() istediğiniz kadar çağrılabilir, bu da büyük dosyaları stream olarak şifrelemeyi mümkün kılar. Seçeceğiniz algoritmanın gereksinimlerini Node.js'in kendisine sorabilirsiniz:
import { getCipherInfo, createCipheriv, randomBytes } from 'node:crypto';
console.log(getCipherInfo('aes-256-gcm'));
console.log(getCipherInfo('aes-256-gcm', { ivLength: 16 }) !== undefined);
// Anahtar uzunluğu algoritmaya uymazsa
try {
createCipheriv('aes-256-gcm', randomBytes(16), randomBytes(12));
} catch (err) {
console.log(err.name, err.code, err.message);
}
// çıktı: [Object: null prototype] {
// çıktı: mode: 'gcm',
// çıktı: name: 'id-aes256-gcm',
// çıktı: nid: 901,
// çıktı: keyLength: 32,
// çıktı: blockSize: 1,
// çıktı: ivLength: 12
// çıktı: }
// çıktı: true
// çıktı: RangeError ERR_CRYPTO_INVALID_KEYLEN Invalid key lengthgetCipherInfo() çıktısına göre aes-256-gcm 32 baytlık (256 bit) anahtar ve varsayılan olarak 12 baytlık IV bekliyor. ivLength seçeneğiyle farklı bir uzunluğun kabul edilip edilmediğini test edebilirsiniz; belgeye göre kabul edilmeyen değerlerde undefined döner. Yanlış uzunlukta anahtar verildiğinde createCipheriv() ERR_CRYPTO_INVALID_KEYLEN koduyla bir RangeError fırlattı. Kullanılabilen bütün algoritmaları crypto.getCiphers() listeler; liste platformdaki OpenSSL'e bağlıdır.
Anahtar, IV, AAD ve etiket: dört parça

AES-GCM ile çalışırken dört kavramı ayırt etmeniz gerekir:
| Parça | Boyut (aes-256-gcm) | Gizli mi? | Kural |
|---|---|---|---|
| Anahtar | 32 bayt | Evet | Rastgele üretilir ya da paroladan scrypt ile türetilir; koda gömülmez |
| IV (nonce) | 12 bayt (varsayılan) | Hayır | Aynı anahtarla her şifrelemede yeni olmalı; şifreli veriyle birlikte saklanır |
| AAD (ek veri) | İsteğe bağlı | Hayır | Şifrelenmez ama etikete dahil edilir; update öncesi setAAD ile verilir |
| Doğrulama etiketi | 16 bayt (varsayılan) | Hayır | final sonrası getAuthTag ile alınır; çözmede final öncesi setAuthTag ile verilir |
IV için belge genel bir kural koyar: gereksinimler algoritmaya bağlıdır; bazı algoritmalarda IV tahmin edilemez ve benzersiz olmalı, bazılarında yalnızca benzersizlik yeterlidir. IV'lerin genellikle gizli tutulması gerekmez ve şifreli metinle birlikte gönderilebilir. GCM tabanlı GMAC için belgedeki tablo, aynı anahtarla doğrulanan her mesaj için benzersiz bir IV gerektiğini açıkça yazar. En güvenli ve en basit uygulama, her şifreleme işleminde randomBytes(12) ile yeni bir IV üretmektir; aynı anahtarla bir IV'yi ikinci kez kullanmayın.
AAD (additional authenticated data) çoğu zaman gözden kaçan ama çok işe yarayan bir özelliktir. Şifrelenmeyen ama değiştirilmemesi gereken bağlam bilgisini (kayıt kimliği, kullanıcı kimliği, sürüm numarası) etikete bağlar. Örneğin bir müşterinin şifreli kimlik numarasını başka bir müşterinin satırına kopyalayan biri, AAD olarak müşteri kimliği kullanıldıysa çözme sırasında yakalanır.
Şifreleme ve çözme: çalışan tam örnek

Aşağıdaki iki fonksiyon, tek bir dizeyi şifreleyip IV, etiket ve şifreli veriyi tek bir base64 değerinde birleştiriyor; çözme fonksiyonu da bu değeri parçalarına ayırıp geri çözüyor. Bu "iv + etiket + veri" düzeni, veritabanında tek bir sütunda saklamak için pratiktir. Parçaları ayırmak için kullandığımız subarray() metodunu Buffer yazısında anlattık.
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
const ALG = 'aes-256-gcm';
function sifrele(metin, anahtar, ekVeri) {
const iv = randomBytes(12); // her mesaj için yeni IV
const cipher = createCipheriv(ALG, anahtar, iv);
if (ekVeri) cipher.setAAD(Buffer.from(ekVeri)); // update'ten önce
const sifreli = Buffer.concat([cipher.update(metin, 'utf8'), cipher.final()]);
const etiket = cipher.getAuthTag(); // final'dan sonra, 16 bayt
// Saklama biçimi: iv (12) + etiket (16) + şifreli veri
return Buffer.concat([iv, etiket, sifreli]).toString('base64');
}
function coz(paket, anahtar, ekVeri) {
const ham = Buffer.from(paket, 'base64');
const iv = ham.subarray(0, 12);
const etiket = ham.subarray(12, 28);
const sifreli = ham.subarray(28);
const decipher = createDecipheriv(ALG, anahtar, iv);
if (ekVeri) decipher.setAAD(Buffer.from(ekVeri));
decipher.setAuthTag(etiket); // final'dan önce
return Buffer.concat([decipher.update(sifreli), decipher.final()]).toString('utf8');
}
const anahtar = randomBytes(32); // 256 bit
const p1 = sifrele('TC kimlik: 12345678901', anahtar, 'musteri:42');
const p2 = sifrele('TC kimlik: 12345678901', anahtar, 'musteri:42');
console.log(p1 === p2); // aynı metin, farklı IV: farklı çıktı
console.log(Buffer.from(p1, 'base64').length); // 12 + 16 + 22
console.log(coz(p1, anahtar, 'musteri:42'));
// çıktı: false
// çıktı: 50
// çıktı: TC kimlik: 12345678901Sıralama kuralları belgede açıkça yazar ve en sık hata kaynağıdır: cipher.setAAD(), update() çağrısından önce yapılmalıdır; cipher.getAuthTag() ancak şifreleme final() ile tamamlandıktan sonra çağrılmalıdır. Çözme tarafında decipher.setAuthTag(), GCM için final() çağrısından önce ve yalnızca bir kez yapılabilir; decipher.setAAD() de yine update() öncesidir. Aynı metni aynı anahtarla iki kez şifrelediğimizde farklı çıktılar aldık, çünkü IV her seferinde yeni üretildi. Toplam boyut 50 bayt çıktı: 12 bayt IV, 16 bayt etiket ve 22 baytlık metnin 22 baytlık şifreli hali; GCM dolgu (padding) eklemez.
Değiştirilmiş veri, yanlış anahtar ve hatalı etiket
GCM'in asıl değeri, bir şeyler yanlış gittiğinde sessizce bozuk veri döndürmek yerine hata vermesidir. Belgeye göre etiket verilmezse ya da şifreli metin değiştirilmişse decipher.final() hata fırlatır ve bu, şifreli metnin kimlik doğrulaması başarısız olduğu için atılması gerektiğini gösterir. Aşağıdaki örnek beş farklı durumu deniyor.
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
const anahtar = randomBytes(32);
const iv = randomBytes(12);
const c = createCipheriv('aes-256-gcm', anahtar, iv);
c.setAAD(Buffer.from('musteri:42'));
const sifreli = Buffer.concat([c.update('bakiye=1500', 'utf8'), c.final()]);
const etiket = c.getAuthTag();
function dene(aciklama, { veri = sifreli, aad = 'musteri:42', tag = etiket, key = anahtar } = {}) {
try {
const d = createDecipheriv('aes-256-gcm', key, iv);
d.setAAD(Buffer.from(aad));
d.setAuthTag(tag);
const acik = Buffer.concat([d.update(veri), d.final()]).toString();
console.log(aciklama, 'tamam:', acik);
} catch (err) {
console.log(aciklama, 'hata:', err.message);
}
}
const bozuk = Buffer.from(sifreli);
bozuk[0] ^= 0x01; // tek bir biti değiştir
dene('doğru');
dene('değişmiş veri', { veri: bozuk });
dene('farklı AAD', { aad: 'musteri:43' });
dene('yanlış anahtar', { key: randomBytes(32) });
dene('kısa etiket', { tag: etiket.subarray(0, 8) });
// çıktı: doğru tamam: bakiye=1500
// çıktı: değişmiş veri hata: Unsupported state or unable to authenticate data
// çıktı: farklı AAD hata: Unsupported state or unable to authenticate data
// çıktı: yanlış anahtar hata: Unsupported state or unable to authenticate data
// çıktı: kısa etiket hata: Invalid authentication tag length: 8Değişmiş veri, farklı AAD ve yanlış anahtar aynı genel mesajla reddedildi; hata mesajı hangi parçanın yanlış olduğunu söylemez ve bu bilinçli bir tasarımdır. Kısa etiket ise daha erken, setAuthTag() aşamasında yakalandı. Belgedeki YAML kaydına göre v26.0.0 ile, deşifre nesnesi oluşturulurken authTagLength belirtilmeden 128 bit dışında bir GCM etiket uzunluğu kullanmak artık izin verilmeyen bir davranış oldu; bu, daha önce v22.0.0 ve v20.13.0 ile kullanımdan kaldırılmıştı. Kısa etiket kullanmanız gerekiyorsa iki tarafta da authTagLength seçeneğini açıkça verin.
Paroladan anahtar türetmek: scrypt
Anahtarı bir paroladan elde etmeniz gerekiyorsa (kullanıcının girdiği bir yedek parolası gibi) parolayı doğrudan anahtar olarak kullanmayın. Belge, dize kullanmanın entropiyi düşürdüğünü, gizli anahtarların neredeyse her zaman rastgele ya da sözde rastgele bayt dizileri olması gerektiğini söyler. Paroladan anahtar türetmek için belgede kullanılan yöntem crypto.scrypt() fonksiyonudur. Belgeye göre scrypt, kaba kuvvet saldırılarını kârsız kılmak için hem işlem hem bellek açısından pahalı olacak şekilde tasarlanmış, paroladan anahtar türeten bir fonksiyondur. Varsayılan parametreleri cost (N) 16384, blockSize (r) 8, parallelization (p) 1 ve maxmem 32 MiB'dir.
import { scrypt, randomBytes, createCipheriv, createDecipheriv } from 'node:crypto';
import { promisify } from 'node:util';
const scryptP = promisify(scrypt);
// Paroladan anahtar türetme: tuz rastgele, en az 16 bayt; şifreli veriyle birlikte saklanır
const parola = 'uzun ve tahmin edilmesi zor bir parola';
const tuz = randomBytes(16);
const anahtar = await scryptP(parola.normalize('NFC'), tuz, 32);
console.log(anahtar.length);
// Aynı parola ve aynı tuz her zaman aynı anahtarı verir
const tekrar = await scryptP(parola.normalize('NFC'), tuz, 32);
console.log(anahtar.equals(tekrar));
// Farklı tuz, farklı anahtar
const baska = await scryptP(parola.normalize('NFC'), randomBytes(16), 32);
console.log(anahtar.equals(baska));
const iv = randomBytes(12);
const c = createCipheriv('aes-256-gcm', anahtar, iv);
const veri = Buffer.concat([c.update('yedek.tar içeriği'), c.final()]);
const etiket = c.getAuthTag();
const d = createDecipheriv('aes-256-gcm', tekrar, iv);
d.setAuthTag(etiket);
console.log(Buffer.concat([d.update(veri), d.final()]).toString());
// çıktı: 32
// çıktı: true
// çıktı: false
// çıktı: yedek.tar içeriğiTuz (salt) için belge, olabildiğince benzersiz olmasını, rastgele ve en az 16 bayt uzunluğunda seçilmesini önerir ve NIST SP 800-132'ye yönlendirir. Tuz gizli değildir; şifreli veriyle birlikte saklanır, çünkü aynı anahtarı yeniden türetmek için gereklidir. Örnekte gördüğünüz gibi aynı parola ve aynı tuz her zaman aynı anahtarı veriyor, tuz değişince anahtar da değişiyor. Parolayı normalize('NFC') ile normalleştirdik: belge, aynı görünen Unicode karakterlerin farklı bayt dizilerine sahip olabileceğini ve Node.js'in bunu kendiliğinden normalleştirmediğini belirtir. Türkçe karakter içeren bir parola, farklı bir klavye ya da işletim sisteminden girildiğinde aksi halde farklı bir anahtar üretebilir.
scrypt() fonksiyonunun asenkron sürümü libuv'nin Worker Pool havuzunda çalışır; Node.js Learn rehberi senkron scryptSync() ve benzerlerinin sunucuda olay döngüsünü bloklayacağını hatırlatır. Örnekte util.promisify() ile asenkron sürümü Promise'e çevirdik; bu fonksiyonu util.promisify yazısında anlattık.
Büyük dosyayı stream olarak şifrelemek
Cipheriv ve Decipheriv sınıfları birer Transform stream'dir; bu yüzden dosyayı belleğe almadan, stream.pipeline() ile okuma stream'inden yazma stream'ine akıtarak şifreleyebilirsiniz. GCM'de etiket ancak akış bittikten sonra hazır olur; pipeline() Promise'i çözüldükten sonra getAuthTag() çağrılır. Çözme tarafında etiketi akış başlamadan önce vermek gerekir.
import { createReadStream, createWriteStream, writeFileSync, readFileSync } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
writeFileSync('rapor.csv', 'ad,tutar\n'.repeat(50_000)); // yaklaşık 450 KB
const anahtar = randomBytes(32);
const iv = randomBytes(12);
// Şifreleme: dosya, Cipheriv (bir Transform stream) üzerinden akar
const cipher = createCipheriv('aes-256-gcm', anahtar, iv);
await pipeline(createReadStream('rapor.csv'), cipher, createWriteStream('rapor.csv.enc'));
const etiket = cipher.getAuthTag(); // akış bittikten sonra hazır
// Çözme
const decipher = createDecipheriv('aes-256-gcm', anahtar, iv);
decipher.setAuthTag(etiket);
await pipeline(createReadStream('rapor.csv.enc'), decipher, createWriteStream('rapor.cozulmus.csv'));
console.log(readFileSync('rapor.csv').equals(readFileSync('rapor.cozulmus.csv')));
console.log(readFileSync('rapor.csv.enc').length === readFileSync('rapor.csv').length);
// çıktı: true
// çıktı: trueŞifreli dosyanın boyutu açık dosyayla aynı çıktı; IV ve etiketi ayrı saklamanız gerektiğini bu yüzden unutmayın (örneğin dosyanın başına IV, sonuna etiket yazarak ya da ayrı bir meta veri kaydında). Bir dikkat noktası daha var: çözme sırasında etiket doğrulaması akışın en sonunda, final() aşamasında yapılır. Bu yüzden pipeline() hatayı ancak veri işlendikten sonra bildirir ve o ana kadar çıktı dosyasına yazılmış veri doğrulanmamıştır. Aşağıdaki örnekte şifreli dosyanın tek bir bitini bozduk.
import { createReadStream, createWriteStream, writeFileSync, readFileSync, statSync, rmSync } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
writeFileSync('veri.txt', 'x'.repeat(200_000));
const anahtar = randomBytes(32);
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', anahtar, iv);
await pipeline(createReadStream('veri.txt'), cipher, createWriteStream('veri.enc'));
const etiket = cipher.getAuthTag();
// Şifreli dosyada tek bir biti bozalım
const bozuk = readFileSync('veri.enc');
bozuk[10] ^= 1;
writeFileSync('veri.enc', bozuk);
const decipher = createDecipheriv('aes-256-gcm', anahtar, iv);
decipher.setAuthTag(etiket);
try {
await pipeline(createReadStream('veri.enc'), decipher, createWriteStream('veri.cozulmus'));
} catch (err) {
console.log('hata:', err.message);
console.log('çıktı dosyasına veri yazıldı mı:', statSync('veri.cozulmus').size > 0);
rmSync('veri.cozulmus'); // doğrulanmamış çıktıyı silin
}
// çıktı: hata: Unsupported state or unable to authenticate data
// çıktı: çıktı dosyasına veri yazıldı mı: trueÇözme hata verdi ama çıktı dosyasına çoktan veri yazılmıştı (yazılan bayt sayısı çalıştırmadan çalıştırmaya değişebildiği için yalnızca sıfırdan büyük olup olmadığını yazdırdık). Bu yüzden stream ile çözerken çıktıyı önce geçici bir dosyaya yazın, pipeline() başarıyla bitince asıl adına taşıyın; hata olursa geçici dosyayı silin. pipeline() ve backpressure ayrıntıları için pipeline yazısına bakın.
Anahtarı nerede saklamalı?
Şifrelemenin güvenliği anahtarın gizliliğine bağlıdır. Anahtarı şifreli verinin yanında, aynı veritabanında ya da kaynak kodun içinde tutmak şifrelemeyi büyük ölçüde anlamsız kılar. En azından anahtarı ortam değişkeninden (örneğin hex ya da base64 olarak kodlanmış biçimde) okuyun ve uygulama açılışında uzunluğunu doğrulayın. Node.js'in yerleşik --env-file desteğini env-file yazısında, .env dosyalarıyla ilgili genel iyi uygulamaları .env yazısında anlattık. Anahtarı bir KeyObject içine almak için crypto.createSecretKey() kullanılabilir; belgeye göre bu fonksiyon simetrik şifreleme ya da Hmac için gizli anahtar içeren bir nesne döndürür.
Anahtar değiştirmeyi (rotation) de baştan düşünün: saklanan değerin başına bir anahtar sürümü koymak (ve bunu AAD'ye de dahil etmek), eski verileri yeni anahtarla yeniden şifrelemeyi kolaylaştırır. Bir sürüm notu: createCipheriv() belgesindeki kayda göre v26.0.0 ile anahtar olarak bir CryptoKey vermek kullanımdan kaldırıldı; Buffer veya KeyObject kullanın. Aynı kayıtlara göre v26.8.0 ile SIV ve GCM-SIV modları desteklendi; belge AES-GCM-SIV için 12 baytlık IV gerektiğini belirtir.
Sık yapılan hatalar

- Aynı anahtarla aynı IV'yi tekrar kullanmak: Her şifrelemede
randomBytes(12)ile yeni IV üretin. - Etiketi saklamamak: Etiket olmadan GCM şifreli metni doğrulanamaz ve
final()hata verir. - getAuthTag() çağrısını final() öncesinde yapmak: Etiket ancak şifreleme tamamlandıktan sonra hazırdır.
- setAuthTag() çağrısını unutmak ya da final() sonrasına bırakmak: GCM'de final öncesinde, yalnızca bir kez verilmelidir.
- Parolayı doğrudan anahtar olarak kullanmak: Paroladan anahtar türetmek için rastgele tuzla scrypt kullanın.
- Şifreli çıktıyı UTF-8 dizeye çevirmek: İkili veri bozulur; base64 veya hex kullanın.
- Parolaları geri çözülebilir şekilde şifrelemek: Kullanıcı parolaları şifrelenmez, hash'lenir; parola hash'leme yazısına bakın.
- Doğrulanmamış kısmi açık metni kullanmak:
final()başarılı olmadan çıktıya güvenmeyin.
Sık Sorulan Sorular
IV'yi gizli tutmam gerekir mi?
Hayır. Belgeye göre IV'lerin genellikle gizli olması gerekmez ve şifreli metinle birlikte iletilebilir. Önemli olan, aynı anahtarla her şifrelemede benzersiz olmasıdır.
Şifreli veriyi metin sütununda nasıl saklarım?
IV, etiket ve şifreli baytları birleştirip base64 ile kodlamak yaygın bir yoldur; yazıdaki örnek tam olarak bunu yapıyor. Çözerken aynı sırayla parçalara ayırın. İkili sütun destekleyen veritabanlarında Buffer'ı doğrudan da saklayabilirsiniz.
AES-256-GCM yerine chacha20-poly1305 kullanabilir miyim?
Belge chacha20-poly1305 şifresini de desteklenen AEAD algoritmaları arasında sayar; authTagLength bu şifre için isteğe bağlıdır ve varsayılanı 16 bayttır. API kullanımı GCM ile aynıdır: setAAD(), getAuthTag() ve setAuthTag() aynı sıralama kurallarıyla çağrılır.
Web Crypto API ile şifrelemek daha mı iyi?
İkisi de aynı temel algoritmaları kullanabilir. Tarayıcı ve Node.js arasında ortak kod yazıyorsanız crypto.webcrypto ile sunulan Web Crypto API taşınabilirlik sağlar. Yalnızca Node.js hedefliyorsanız bu yazıdaki node:crypto fonksiyonları stream desteğiyle daha doğrudan bir kullanım sunar.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: Node.js öğrenme rehberi · crypto hash, HMAC ve randomUUID · parola hash'leme · stream pipeline · env-file ve process.env