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 OsRng ile ü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-eval eklemek zorundaysanız bunu minimum scope ile yapın
  • Side-channel saldırıları: Rust’ın subtle crate’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:

  • getrandom hatası: Browser ortamında getrandom crate’i js feature ile aktif edilmezse panic verir. Cargo.toml‘da getrandom = { version = "0.2", features = ["js"] } şeklinde tanımlanmalı
  • CORS sorunları: WASM dosyalarını serve ederken Content-Type: application/wasm header’ı zorunlu. Nginx’te mime.types dosyasına ekleyin
  • SharedArrayBuffer: Çok thread’li WASM için Cross-Origin-Opener-Policy: same-origin ve Cross-Origin-Embedder-Policy: require-corp header’ları gerekiyor
  • Memory leak: StreamCipher objelerini işiniz bitince free() metoduyla veya using declaration 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-gcm crate’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.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir