WebAssembly ile AES Şifreleme Uygulaması Geliştirme
Veri güvenliği artık bir seçenek değil, zorunluluk haline geldi. Özellikle hassas verileri doğrudan tarayıcı tarafında şifrelemek gerektiğinde, JavaScript’in performans sınırlarıyla sık sık karşılaşıyoruz. İşte tam burada WebAssembly devreye giriyor. WASM ile AES şifreleme uygulaması geliştirmek, hem native’e yakın performans hem de platform bağımsızlığı sunuyor. Bu yazıda, gerçek dünya senaryoları üzerinden adım adım bir AES-256-GCM şifreleme modülü geliştireceğiz.
Neden WebAssembly ile Şifreleme?
JavaScript’te pure JS ile yazılmış AES implementasyonları, büyük dosyaları işlerken ciddi performans sorunlarına yol açıyor. 100 MB’lık bir dosyayı tarayıcı tarafında şifrelemeye çalıştığınızda, UI thread’i bloklanıyor ve kullanıcı deneyimi berbat bir hal alıyor. Web Crypto API bir alternatif olsa da özelleştirme imkanı kısıtlı ve bazı edge ortamlarında eksik implementasyonlarla karşılaşabiliyorsunuz.
WebAssembly bu problemi şu şekilde çözüyor:
- Native yakın hız: C/C++ veya Rust ile yazılan şifreleme kodu, WASM’a derlendikten sonra JS’in 10-20 katı hızda çalışabiliyor
- SIMD desteği: Modern WASM runtime’ları SIMD intrinsic’leri destekliyor, bu AES-NI gibi donanım hızlandırması anlamına geliyor
- Taşınabilirlik: Cloudflare Workers, Fastly Compute@Edge, Deno Deploy gibi edge platformlarında sorunsuz çalışıyor
- Kontrol: Kendi şifreleme pipeline’ınızı tam anlamıyla kontrol edebiliyorsunuz
Geliştirme Ortamını Hazırlamak
Önce gerekli araçları kuralım. Ben bu örnekleri Ubuntu 22.04 üzerinde geliştirdim ama Rust toolchain sayesinde her platformda aynı sonucu alırsınız.
# Rust kurulumu (zaten kuruluysa atlayın)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
# wasm-pack kurulumu
cargo install wasm-pack
# Node.js ve npm (test için)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
# wasm-bindgen CLI
cargo install wasm-bindgen-cli
# Projeyi oluştur
cargo new --lib wasm-aes-crypto
cd wasm-aes-crypto
Şimdi Cargo.toml dosyamızı düzenleyelim. Bağımlılıkları doğru ayarlamak kritik önem taşıyor:
cat > Cargo.toml << 'EOF'
[package]
name = "wasm-aes-crypto"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
aes-gcm = { version = "0.10", features = ["aes"] }
rand = { version = "0.8", features = ["getrandom"] }
getrandom = { version = "0.2", features = ["js"] }
base64 = "0.21"
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6"
js-sys = "0.3"
web-sys = { version = "0.3", features = ["console"] }
hex = "0.4"
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
EOF
Temel AES-GCM Implementasyonu
Şimdi asıl şifreleme kodunu yazalım. src/lib.rs dosyasına AES-256-GCM implementasyonumuzu ekleyeceğiz:
cat > src/lib.rs << 'EOF'
use wasm_bindgen::prelude::*;
use aes_gcm::{
aead::{Aead, AeadCore, KeyInit, OsRng},
Aes256Gcm, Key, Nonce
};
use base64::{Engine as _, engine::general_purpose};
// Panic hook'u production'da kaldırın
#[wasm_bindgen(start)]
pub fn init() {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
}
#[wasm_bindgen]
pub struct AesCrypto {
cipher: Aes256Gcm,
}
#[wasm_bindgen]
impl AesCrypto {
/// 256-bit (32 byte) key ile yeni bir şifreleme contexti oluşturur
#[wasm_bindgen(constructor)]
pub fn new(key_b64: &str) -> Result<AesCrypto, JsValue> {
let key_bytes = general_purpose::STANDARD
.decode(key_b64)
.map_err(|e| JsValue::from_str(&format!("Key decode hatasi: {}", e)))?;
if key_bytes.len() != 32 {
return Err(JsValue::from_str("Key 32 byte (256-bit) olmali"));
}
let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
let cipher = Aes256Gcm::new(key);
Ok(AesCrypto { cipher })
}
/// Rastgele 256-bit key üretir, base64 döndürür
#[wasm_bindgen]
pub fn generate_key() -> String {
let key = Aes256Gcm::generate_key(OsRng);
general_purpose::STANDARD.encode(key.as_slice())
}
/// Plaintext'i şifreler, "nonce:ciphertext" formatında base64 döndürür
#[wasm_bindgen]
pub fn encrypt(&self, plaintext: &str) -> Result<String, JsValue> {
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let ciphertext = self.cipher
.encrypt(&nonce, plaintext.as_bytes())
.map_err(|e| JsValue::from_str(&format!("Sifrelem hatasi: {}", e)))?;
let nonce_b64 = general_purpose::STANDARD.encode(nonce.as_slice());
let ct_b64 = general_purpose::STANDARD.encode(&ciphertext);
Ok(format!("{}:{}", nonce_b64, ct_b64))
}
/// "nonce:ciphertext" formatındaki base64 veriyi çözer
#[wasm_bindgen]
pub fn decrypt(&self, encrypted: &str) -> Result<String, JsValue> {
let parts: Vec<&str> = encrypted.splitn(2, ':').collect();
if parts.len() != 2 {
return Err(JsValue::from_str("Gecersiz format: 'nonce:ciphertext' bekleniyor"));
}
let nonce_bytes = general_purpose::STANDARD
.decode(parts[0])
.map_err(|e| JsValue::from_str(&format!("Nonce decode hatasi: {}", e)))?;
let ciphertext = general_purpose::STANDARD
.decode(parts[1])
.map_err(|e| JsValue::from_str(&format!("Ciphertext decode hatasi: {}", e)))?;
let nonce = Nonce::from_slice(&nonce_bytes);
let plaintext = self.cipher
.decrypt(nonce, ciphertext.as_ref())
.map_err(|_| JsValue::from_str("Sifre cozme basarisiz: yanlis key veya bozuk veri"))?;
String::from_utf8(plaintext)
.map_err(|e| JsValue::from_str(&format!("UTF-8 donusum hatasi: {}", e)))
}
}
EOF
Büyük Dosyalar İçin Streaming Şifreleme
Gerçek dünyada 100 MB’lık dosyaları tek seferde belleğe almak istemeyiz. Chunk tabanlı bir yaklaşım gerekiyor:
cat >> src/lib.rs << 'EOF'
/// Büyük veri setleri için chunk bazlı şifreleme
#[wasm_bindgen]
pub struct StreamCipher {
cipher: Aes256Gcm,
chunk_size: usize,
}
#[wasm_bindgen]
impl StreamCipher {
#[wasm_bindgen(constructor)]
pub fn new(key_b64: &str, chunk_size: usize) -> Result<StreamCipher, JsValue> {
let key_bytes = general_purpose::STANDARD
.decode(key_b64)
.map_err(|e| JsValue::from_str(&format!("Hata: {}", e)))?;
let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
let cipher = Aes256Gcm::new(key);
Ok(StreamCipher {
cipher,
chunk_size: if chunk_size == 0 { 65536 } else { chunk_size },
})
}
/// Uint8Array chunk'ını şifreler
#[wasm_bindgen]
pub fn encrypt_chunk(&self, data: &[u8]) -> Result<Vec<u8>, JsValue> {
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let mut ciphertext = self.cipher
.encrypt(&nonce, data)
.map_err(|e| JsValue::from_str(&format!("Chunk sifrelem hatasi: {}", e)))?;
// Nonce'u başa ekle (12 byte fixed)
let mut result = nonce.to_vec();
result.append(&mut ciphertext);
Ok(result)
}
/// Şifreli chunk'ı çözer
#[wasm_bindgen]
pub fn decrypt_chunk(&self, data: &[u8]) -> Result<Vec<u8>, JsValue> {
if data.len() < 12 {
return Err(JsValue::from_str("Veri cok kisa, nonce eksik"));
}
let (nonce_bytes, ciphertext) = data.split_at(12);
let nonce = Nonce::from_slice(nonce_bytes);
self.cipher
.decrypt(nonce, ciphertext)
.map_err(|_| JsValue::from_str("Chunk sifre cozme basarisiz"))
}
#[wasm_bindgen]
pub fn get_chunk_size(&self) -> usize {
self.chunk_size
}
}
EOF
Derleme ve Optimizasyon
WASM modülünü derlemek için wasm-pack kullanıyoruz. Farklı hedefler için farklı komutlar gerekiyor:
# Web tarayıcıları için (ES modules)
wasm-pack build --target web --release --out-dir pkg/web
# Node.js için
wasm-pack build --target nodejs --release --out-dir pkg/node
# Cloudflare Workers / edge için (bundler)
wasm-pack build --target bundler --release --out-dir pkg/bundler
# Üretilen dosyaların boyutunu kontrol edin
ls -lh pkg/web/
# wasm-aes-crypto_bg.wasm boyutu ~150KB civarında olmalı
# wasm-opt ile daha fazla optimizasyon (opsiyonel ama önerilen)
sudo apt-get install -y binaryen
wasm-opt -O3 -o pkg/web/wasm_aes_crypto_bg_opt.wasm pkg/web/wasm_aes_crypto_bg.wasm
ls -lh pkg/web/wasm_aes_crypto_bg_opt.wasm
JavaScript Entegrasyonu
Derleme sonrası WASM modülünü bir web uygulamasına entegre edelim. Gerçek bir senaryo: kullanıcının yerel dosyalarını tarayıcıda şifreleyip upload etmeden önce local storage’a kaydetmek.
cat > index.html << 'EOF'
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<title>WASM AES Dosya Sifreleyici</title>
</head>
<body>
<div id="app">
<h2>Guvenli Dosya Sifreleyici</h2>
<button id="genKey">Yeni Anahtar Uret</button>
<br><br>
<input type="text" id="keyInput" placeholder="Base64 key girin..." style="width:400px">
<br><br>
<input type="file" id="fileInput">
<button id="encryptBtn">Sifrele ve Indir</button>
<br><br>
<div id="status"></div>
</div>
<script type="module">
import init, { AesCrypto, StreamCipher } from './pkg/web/wasm_aes_crypto.js';
let wasmReady = false;
async function bootstrap() {
await init();
wasmReady = true;
document.getElementById('status').textContent = 'WASM modulu hazir.';
}
document.getElementById('genKey').addEventListener('click', () => {
if (!wasmReady) return;
const key = AesCrypto.generate_key();
document.getElementById('keyInput').value = key;
document.getElementById('status').textContent = 'Yeni anahtar uretildi. Kaydedin!';
});
document.getElementById('encryptBtn').addEventListener('click', async () => {
const keyB64 = document.getElementById('keyInput').value.trim();
const file = document.getElementById('fileInput').files[0];
if (!keyB64 || !file) {
alert('Anahtar ve dosya secmek zorunlu');
return;
}
const status = document.getElementById('status');
const CHUNK_SIZE = 64 * 1024; // 64KB chunks
try {
const streamer = new StreamCipher(keyB64, CHUNK_SIZE);
const reader = file.stream().getReader();
const encryptedChunks = [];
let totalBytes = 0;
// Chunk bazlı isleme
while (true) {
const { done, value } = await reader.read();
if (done) break;
const encChunk = streamer.encrypt_chunk(value);
// Her chunk'in boyutunu 4 byte big-endian olarak oncesine ekle
const sizeBuffer = new Uint8Array(4);
new DataView(sizeBuffer.buffer).setUint32(0, encChunk.length, false);
encryptedChunks.push(sizeBuffer, encChunk);
totalBytes += encChunk.length;
status.textContent = `Isleniyor: ${(totalBytes / 1024).toFixed(1)} KB sifrelendi`;
// UI thread'i bloke etmemek icin
await new Promise(r => setTimeout(r, 0));
}
const blob = new Blob(encryptedChunks, { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = file.name + '.enc';
a.click();
URL.revokeObjectURL(url);
status.textContent = `Tamamlandi! ${file.name}.enc indirildi.`;
} catch (err) {
status.textContent = `Hata: ${err}`;
console.error(err);
}
});
bootstrap();
</script>
</body>
</html>
EOF
Cloudflare Workers’ta Deployment
Edge computing senaryosu için Cloudflare Workers entegrasyonu kritik bir kullanım alanı. API endpoint’lerinde şifreleme/deşifreleme yapabilmek için:
# Wrangler kurulumu
npm install -g wrangler
# Workers projesi oluştur
mkdir cf-worker-crypto && cd cf-worker-crypto
wrangler init --yes
# WASM modülünü kopyala
cp -r ../wasm-aes-crypto/pkg/bundler ./crypto-wasm
# package.json güncelle
cat > package.json << 'PKGJSON'
{
"name": "cf-worker-crypto",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy"
},
"dependencies": {
"wasm-aes-crypto": "file:./crypto-wasm"
}
}
PKGJSON
cat > src/index.js << 'WORKER'
import { AesCrypto } from 'wasm-aes-crypto';
// Worker başlangıcında WASM modülü bir kez init edilir
// Cloudflare Workers'ta top-level await destekleniyor
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
if (request.method === 'POST' && url.pathname === '/encrypt') {
return handleEncrypt(request, env);
}
if (request.method === 'POST' && url.pathname === '/decrypt') {
return handleDecrypt(request, env);
}
return new Response('Not Found', { status: 404 });
}
};
async function handleEncrypt(request, env) {
try {
const body = await request.json();
const { plaintext, key } = body;
if (!plaintext || !key) {
return Response.json({ error: 'plaintext ve key zorunlu' }, { status: 400 });
}
const crypto = new AesCrypto(key);
const encrypted = crypto.encrypt(plaintext);
return Response.json({
success: true,
encrypted,
timestamp: Date.now()
});
} catch (err) {
return Response.json({ error: err.toString() }, { status: 500 });
}
}
async function handleDecrypt(request, env) {
try {
const body = await request.json();
const { encrypted, key } = body;
const crypto = new AesCrypto(key);
const plaintext = crypto.decrypt(encrypted);
return Response.json({ success: true, plaintext });
} catch (err) {
return Response.json({ error: 'Sifre cozme basarisiz' }, { status: 400 });
}
}
WORKER
# Deploy
wrangler deploy
Performans Testi ve Benchmark
WASM implementasyonumuzun ne kadar hızlı çalıştığını ölçelim. Bunu hem Node.js ortamında hem de karşılaştırmalı olarak yapacağız:
cat > benchmark.mjs << 'EOF'
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const { AesCrypto, StreamCipher } = require('./pkg/node/wasm_aes_crypto.js');
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
// Test verisi: 10MB
const TEST_SIZE = 10 * 1024 * 1024;
const testData = randomBytes(TEST_SIZE).toString('base64');
const key = AesCrypto.generate_key();
console.log(`Test verisi boyutu: ${(testData.length / 1024 / 1024).toFixed(2)} MB`);
console.log('='.repeat(50));
// WASM AES-256-GCM testi
const ITERATIONS = 5;
let wasmTotalMs = 0;
const crypto = new AesCrypto(key);
for (let i = 0; i < ITERATIONS; i++) {
const start = performance.now();
const enc = crypto.encrypt(testData);
const dec = crypto.decrypt(enc);
wasmTotalMs += performance.now() - start;
}
console.log(`WASM AES-256-GCM ortalama: ${(wasmTotalMs / ITERATIONS).toFixed(2)}ms`);
// Node.js Web Crypto API karsilastirma
const { subtle } = globalThis.crypto;
let nodeTotalMs = 0;
for (let i = 0; i < ITERATIONS; i++) {
const nodeKey = await subtle.generateKey(
{ name: 'AES-GCM', length: 256 },
true, ['encrypt', 'decrypt']
);
const iv = randomBytes(12);
const start = performance.now();
const encoded = new TextEncoder().encode(testData);
const encrypted = await subtle.encrypt({ name: 'AES-GCM', iv }, nodeKey, encoded);
const decrypted = await subtle.decrypt({ name: 'AES-GCM', iv }, nodeKey, encrypted);
nodeTotalMs += performance.now() - start;
}
console.log(`Node.js Web Crypto ortalama: ${(nodeTotalMs / ITERATIONS).toFixed(2)}ms`);
console.log(`WASM/WebCrypto hiz orani: ${(nodeTotalMs / wasmTotalMs).toFixed(2)}x`);
// Chunk-based streaming test
const streamer = new StreamCipher(key, 65536);
const rawData = new Uint8Array(TEST_SIZE);
const CHUNK_SIZE = 65536;
const streamStart = performance.now();
let offset = 0;
const chunks = [];
while (offset < rawData.length) {
const chunk = rawData.slice(offset, offset + CHUNK_SIZE);
chunks.push(streamer.encrypt_chunk(chunk));
offset += CHUNK_SIZE;
}
const streamMs = performance.now() - streamStart;
console.log(`nStream sifrelem (${TEST_SIZE / 1024 / 1024}MB): ${streamMs.toFixed(2)}ms`);
console.log(`Throughput: ${((TEST_SIZE / 1024 / 1024) / (streamMs / 1000)).toFixed(2)} MB/s`);
EOF
node benchmark.mjs
Güvenlik Hususları ve Best Practices
Şifreleme uygulamaları geliştirirken dikkat etmemiz gereken bazı kritik noktalar var:
- Key management: WASM modülünüze geçirdiğiniz anahtarları JavaScript memory’sinde olabildiğince kısa süre tutun. WeakRef ve FinalizationRegistry kullanarak GC’nin hemen temizlemesine yardımcı olun
- Nonce tekrarı: AES-GCM’de aynı key ile aynı nonce kullanmak felaket demek. Her şifrelemede
OsRngile üretilen rastgele nonce zorunludur, bizim implementasyonumuz bunu otomatik yapıyor - AAD (Additional Authenticated Data): Şifrelemediğiniz ama bütünlüğünü korumanız gereken metadata varsa AES-GCM’nin AAD özelliğini kullanın
- Yanlış hata mesajları: Şifre çözme başarısız olduğunda detaylı hata vermeyin, “sifre cozme basarisiz” gibi genel bir mesaj kullanın. Bizim implementasyonumuz bunu yapıyor
- WASM memory güvenliği: Şifreleme sonrası artık kullanılmayan plaintext verilerini
Uint8Array.fill(0)ile sıfırlayın - Content Security Policy: WASM kullanan sayfalarınızda CSP header’ınıza
wasm-unsafe-evaleklemek zorundaysanız bunu minimum scope ile yapın - Side-channel saldırıları: Rust’ın
subtlecrate’ini kullanarak constant-time karşılaştırmalar yapın, bu özellikle key karşılaştırmalarında önemli
Yaygın Hatalar ve Çözümleri
Uygulamayı geliştirirken karşılaşacağınız tipik sorunlar:
getrandomhatası: Browser ortamındagetrandomcrate’ijsfeature ile aktif edilmezse panic verir.Cargo.toml‘dagetrandom = { version = "0.2", features = ["js"] }şeklinde tanımlanmalı- CORS sorunları: WASM dosyalarını serve ederken
Content-Type: application/wasmheader’ı zorunlu. Nginx’temime.typesdosyasına ekleyin - SharedArrayBuffer: Çok thread’li WASM için
Cross-Origin-Opener-Policy: same-originveCross-Origin-Embedder-Policy: require-corpheader’ları gerekiyor - Memory leak:
StreamCipherobjelerini işiniz bitincefree()metoduyla veyausingdeclaration ile serbest bırakın
# Nginx mime type ekleme
echo 'application/wasm wasm;' | sudo tee -a /etc/nginx/mime.types
sudo nginx -t && sudo systemctl reload nginx
# CORS ve güvenlik header'larını test etmek
curl -I https://yourdomain.com/pkg/web/wasm_aes_crypto_bg.wasm | grep -E 'Content-Type|Cross-Origin'
Sonuç
WebAssembly ile AES şifreleme uygulaması geliştirmek, başlangıçta karmaşık görünse de Rust ekosistemi ve wasm-pack sayesinde oldukça akıcı bir deneyim sunuyor. Bu yazıda öğrendiklerimizi özetleyelim:
- Rust + wasm-pack kombinasyonu production grade şifreleme için doğru araç seçimi.
aes-gcmcrate’i hem güvenli hem de hızlı - AES-256-GCM authenticated encryption sağlıyor, yani hem şifreleme hem de bütünlük doğrulaması tek geçişte yapılıyor. Bu büyük avantaj
- Chunk bazlı streaming büyük dosyalar için zorunlu. Memory’de tüm dosyayı açmak yerine 64KB chunk’larla çalışmak hem daha stabil hem de daha hızlı
- Edge deployment Cloudflare Workers gibi platformlarda WASM native olarak destekleniyor ve cold start süreleri son derece düşük
- Benchmark sonuçları gösteriyor ki WASM implementasyonu pure JS’e göre ciddi hız avantajı sağlıyor, özellikle büyük veri setlerinde bu fark daha belirgin
Sonraki adım olarak SIMD özelliğini aktif ederek (target-feature=+simd128) AES-NI donanım hızlandırmasından yararlanabilir ve throughput’u daha da artırabilirsiniz. Ayrıca Web Workers ile paralel şifreleme pipeline’ı kurarak çok çekirdekli sistemlerde lineer ölçeklenme elde etmek mümkün. Güvenli ve hızlı uygulamalar geliştirmenin yolu, doğru araçları doğru yerde kullanmaktan geçiyor.
