WebAssembly ile Kriptografi: Tarayıcıda AES, SHA ve RSA Operasyonları
Tarayıcı güvenliği denince aklımıza genellikle HTTPS, CSP başlıkları veya cookie bayrakları gelir. Ama modern web uygulamaları artık çok daha fazlasını yapıyor: istemci tarafında şifreleme, dijital imza doğrulama, hassas veri işleme. İşte tam bu noktada WebAssembly devreye giriyor ve tarayıcıyı neredeyse bir HSM (Hardware Security Module) gibi kullanmamıza olanak tanıyor. Bu yazıda AES, SHA ve RSA operasyonlarını doğrudan tarayıcıda nasıl çalıştırabileceğimizi, performans tuzaklarını ve gerçek dünya kullanım senaryolarını ele alacağız.
WebAssembly ve Kriptografi: Neden Bu Kombinasyon Mantıklı?
JavaScript’in kriptografi kütüphaneleri yıllardır var. OpenPGP.js, forge, node-forge gibi kütüphaneler işe yarıyor. Ama sorun şu: saf JavaScript ile yazılmış kriptografik operasyonlar, native koda kıyasla dramatik şekilde yavaş kalıyor. Büyük dosyaları AES ile şifrelerken veya yüzlerce RSA imzası doğrularken bu fark kendini acı bir şekilde hissettiriyor.
WebAssembly burada birkaç kritik avantaj sunuyor:
- Performans: WASM, JavaScript’e göre kriptografik işlemlerde 2x ile 10x arasında hız kazanımı sağlıyor
- Bellek kontrolü: Linear memory modeli sayesinde hassas verilerin (anahtarlar, plaintext) yaşam döngüsünü kontrol edebiliyorsunuz
- Taşınabilirlik: C ile yazılmış OpenSSL veya Rust ile yazılmış RustCrypto gibi battle-tested kütüphaneleri doğrudan derleyip kullanabiliyorsunuz
- Sandboxing: WASM modülü tarayıcının sandbox ortamında çalışıyor, sistem kaynaklarına doğrudan erişemiyor
Gerçek dünya senaryosu olarak şunu düşünün: Bir sağlık bilgi sistemi geliştiriyorsunuz. Hasta verileri sunucuya gitmeden önce tarayıcıda şifrelenmeli. JavaScript ile bunu yapabilirsiniz ama büyük görüntü dosyaları söz konusu olduğunda kullanıcı “sayfa dondu” diye şikayet etmeye başlar. WASM tabanlı bir çözüm bu problemi ortadan kaldırıyor.
Ortamı Hazırlamak: Emscripten ve wasm-pack
Önce araç zincirini kuralım. İki ana yol var: C/C++ için Emscripten, Rust için wasm-pack.
# Emscripten kurulumu
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
# Kurulumu doğrula
emcc --version
# Rust + wasm-pack kurulumu
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo install wasm-pack
# wasm-bindgen CLI aracı
cargo install wasm-bindgen-cli
# wasm-opt optimizasyon aracı (Binaryen)
apt-get install binaryen # Debian/Ubuntu
brew install binaryen # macOS
SHA-256 ile Başlayalım: C’den WASM’a
En basit örnekle başlamak mantıklı. SHA-256 implementasyonunu C’de yazıp WASM’a derleyelim.
// sha256_wasm.c
#include <emscripten/emscripten.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
// SHA-256 sabitler
static const uint32_t K[64] = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
// ... (kısaltıldı, gerçek implementasyonda tüm değerler olmalı)
};
EMSCRIPTEN_KEEPALIVE
void sha256_hash(const uint8_t* input, size_t length, uint8_t* output) {
// SHA-256 implementasyonu
// Gerçek projede OpenSSL veya libsodium kullanın
}
EMSCRIPTEN_KEEPALIVE
uint8_t* allocate_buffer(size_t size) {
return (uint8_t*)malloc(size);
}
EMSCRIPTEN_KEEPALIVE
void free_buffer(uint8_t* ptr) {
free(ptr);
}
# WASM'a derleme
emcc sha256_wasm.c
-O3
-s WASM=1
-s EXPORTED_RUNTIME_METHODS='["ccall","cwrap","getValue","setValue"]'
-s ALLOW_MEMORY_GROWTH=1
-s MODULARIZE=1
-s EXPORT_NAME='SHA256Module'
-o sha256.js
# Çıktı dosyalarını kontrol et
ls -lh sha256.js sha256.wasm
# wasm-opt ile optimize et
wasm-opt -O3 sha256.wasm -o sha256_optimized.wasm
Derleme çıktısı iki dosya üretiyor: sha256.js (glue kodu) ve sha256.wasm (binary). Production’da bu ikisini birlikte dağıtmanız gerekiyor.
Rust ile AES-256-GCM Implementasyonu
Rust ekosistemi WASM kriptografi için çok daha olgun. RustCrypto crates koleksiyonu kapsamlı ve iyi test edilmiş. AES-GCM implementasyonuna bakalım.
# Yeni Rust projesi oluştur
cargo new --lib wasm-crypto
cd wasm-crypto
# wasm32 target ekle
rustup target add wasm32-unknown-unknown
Cargo.toml dosyasını düzenleyin:
[package]
name = "wasm-crypto"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
aes-gcm = "0.10"
sha2 = "0.10"
rsa = { version = "0.9", features = ["pem"] }
rand = "0.8"
getrandom = { version = "0.2", features = ["js"] }
base64 = "0.21"
[profile.release]
opt-level = "z" # Boyut optimizasyonu
lto = true # Link-time optimization
codegen-units = 1
panic = "abort" # Panic handler boyutu küçültür
Şimdi src/lib.rs dosyasını yazalım:
use wasm_bindgen::prelude::*;
use aes_gcm::{
aead::{Aead, AeadCore, KeyInit, OsRng},
Aes256Gcm, Key, Nonce,
};
use sha2::{Sha256, Sha512, Digest};
use base64::{Engine as _, engine::general_purpose};
// AES-256-GCM şifreleme
#[wasm_bindgen]
pub fn aes_encrypt(key_b64: &str, plaintext: &[u8]) -> Result<String, JsValue> {
let key_bytes = general_purpose::STANDARD
.decode(key_b64)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
if key_bytes.len() != 32 {
return Err(JsValue::from_str("AES-256 için 32 byte anahtar gerekli"));
}
let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
let cipher = Aes256Gcm::new(key);
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let ciphertext = cipher
.encrypt(&nonce, plaintext)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
// Nonce + ciphertext birleştir, base64 encode et
let mut combined = nonce.to_vec();
combined.extend_from_slice(&ciphertext);
Ok(general_purpose::STANDARD.encode(&combined))
}
// AES-256-GCM şifre çözme
#[wasm_bindgen]
pub fn aes_decrypt(key_b64: &str, ciphertext_b64: &str) -> Result<Vec<u8>, JsValue> {
let key_bytes = general_purpose::STANDARD
.decode(key_b64)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let combined = general_purpose::STANDARD
.decode(ciphertext_b64)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
if combined.len() < 12 {
return Err(JsValue::from_str("Geçersiz ciphertext uzunluğu"));
}
let (nonce_bytes, ciphertext) = combined.split_at(12);
let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
let cipher = Aes256Gcm::new(key);
let nonce = Nonce::from_slice(nonce_bytes);
cipher
.decrypt(nonce, ciphertext)
.map_err(|e| JsValue::from_str(&e.to_string()))
}
// SHA-256 hash
#[wasm_bindgen]
pub fn sha256(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
let result = hasher.finalize();
hex::encode(result)
}
// SHA-512 hash
#[wasm_bindgen]
pub fn sha512(data: &[u8]) -> String {
let mut hasher = Sha512::new();
hasher.update(data);
let result = hasher.finalize();
hex::encode(result)
}
// Yeni AES anahtarı üret
#[wasm_bindgen]
pub fn generate_aes_key() -> String {
let key = Aes256Gcm::generate_key(&mut OsRng);
general_purpose::STANDARD.encode(key)
}
# WASM paketi oluştur
wasm-pack build --target web --release
# pkg/ dizinini kontrol et
ls -lh pkg/
# wasm_crypto_bg.wasm, wasm_crypto.js, wasm_crypto.d.ts
# WASM dosyası boyutunu kontrol et
wc -c pkg/wasm_crypto_bg.wasm
JavaScript Tarafından Kullanım
WASM modülünü tarayıcıda nasıl kullanacağımıza bakalım. Modern ES modules yaklaşımıyla:
// crypto-worker.js (Web Worker içinde kullanım önerilir)
import init, {
aes_encrypt,
aes_decrypt,
sha256,
sha512,
generate_aes_key
} from './pkg/wasm_crypto.js';
let wasmReady = false;
// WASM modülünü başlat
async function initWasm() {
await init();
wasmReady = true;
console.log('WASM crypto modülü hazır');
}
// Dosya şifreleme örneği
async function encryptFile(file) {
if (!wasmReady) await initWasm();
const key = generate_aes_key();
const arrayBuffer = await file.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
console.time('aes-encrypt');
const encrypted = aes_encrypt(key, uint8Array);
console.timeEnd('aes-encrypt');
return { encrypted, key, filename: file.name };
}
// Veri bütünlüğü kontrolü
async function verifyIntegrity(data) {
if (!wasmReady) await initWasm();
const uint8Array = new Uint8Array(data);
const hash = sha256(uint8Array);
return hash;
}
// Mesaj kuyruğu ile Web Worker entegrasyonu
self.onmessage = async (event) => {
const { type, payload } = event.data;
switch (type) {
case 'ENCRYPT':
const result = await encryptFile(payload.file);
self.postMessage({ type: 'ENCRYPT_DONE', result });
break;
case 'HASH':
const hash = await verifyIntegrity(payload.data);
self.postMessage({ type: 'HASH_DONE', hash });
break;
}
};
initWasm();
RSA İmza Doğrulama: Güvenli Belge Sistemi Senaryosu
Gerçek dünyadan bir senaryo: Bir e-devlet uygulamasında PDF belgelerinin dijital imzalarını tarayıcıda doğrulamanız gerekiyor. Sunucuya belgeyi göndermek istemiyorsunuz çünkü gizlilik kritik. WASM tabanlı RSA doğrulama tam burada değer katıyor.
// src/rsa_verify.rs - lib.rs'e ekleyin
use rsa::{RsaPublicKey, pkcs8::DecodePublicKey, pss::VerifyingKey};
use rsa::signature::{Verifier, SignatureEncoding};
use sha2::Sha256;
#[wasm_bindgen]
pub fn rsa_verify_signature(
public_key_pem: &str,
message: &[u8],
signature_b64: &str,
) -> Result<bool, JsValue> {
// PEM formatından public key parse et
let public_key = RsaPublicKey::from_public_key_pem(public_key_pem)
.map_err(|e| JsValue::from_str(&format!("Public key parse hatası: {}", e)))?;
let verifying_key: VerifyingKey<Sha256> = VerifyingKey::new(public_key);
// Base64 signature decode
let sig_bytes = general_purpose::STANDARD
.decode(signature_b64)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let signature = rsa::pss::Signature::try_from(sig_bytes.as_slice())
.map_err(|e| JsValue::from_str(&e.to_string()))?;
// İmzayı doğrula
match verifying_key.verify(message, &signature) {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
// RSA anahtar çifti oluştur (test amaçlı, production'da key management ayrı olmalı)
#[wasm_bindgen]
pub fn generate_rsa_keypair_2048() -> Result<JsValue, JsValue> {
use rsa::pkcs8::{EncodePrivateKey, EncodePublicKey, LineEnding};
let mut rng = OsRng;
let private_key = rsa::RsaPrivateKey::new(&mut rng, 2048)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let public_key = rsa::RsaPublicKey::from(&private_key);
let private_pem = private_key
.to_pkcs8_pem(LineEnding::LF)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let public_pem = public_key
.to_public_key_pem(LineEnding::LF)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let result = js_sys::Object::new();
js_sys::Reflect::set(&result, &"privateKey".into(), &private_pem.as_str().into())?;
js_sys::Reflect::set(&result, &"publicKey".into(), &public_pem.as_str().into())?;
Ok(result.into())
}
Web Crypto API ile WASM’ı Birleştirmek
WASM her zaman tek başına kullanılmak zorunda değil. Tarayıcının native Web Crypto API’si ile hibrit bir yaklaşım çok daha akıllıca olabilir. Web Crypto, AES ve SHA için WASM’dan da hızlı olabiliyor çünkü native implementasyonlar hardware acceleration kullanıyor.
// hybrid-crypto.js
class HybridCrypto {
constructor() {
this.wasmModule = null;
this.webCrypto = window.crypto.subtle;
}
async init() {
// WASM modülünü yükle (RSA doğrulama için kullanacağız)
const { default: init, rsa_verify_signature, sha256 } =
await import('./pkg/wasm_crypto.js');
await init();
this.wasmModule = { rsa_verify_signature, sha256 };
}
// AES için Web Crypto kullan (hardware acceleration)
async encryptAES(keyMaterial, data) {
const key = await this.webCrypto.importKey(
'raw',
keyMaterial,
{ name: 'AES-GCM' },
false,
['encrypt']
);
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const encrypted = await this.webCrypto.encrypt(
{ name: 'AES-GCM', iv },
key,
data
);
// iv + ciphertext
const result = new Uint8Array(iv.length + encrypted.byteLength);
result.set(iv);
result.set(new Uint8Array(encrypted), iv.length);
return result;
}
// SHA için Web Crypto kullan
async hashSHA256(data) {
const hashBuffer = await this.webCrypto.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
// RSA imza doğrulama için WASM kullan
async verifyDocument(publicKeyPem, documentData, signatureB64) {
if (!this.wasmModule) throw new Error('WASM modülü başlatılmamış');
return this.wasmModule.rsa_verify_signature(
publicKeyPem,
new Uint8Array(documentData),
signatureB64
);
}
// Büyük dosyalar için streaming hash (WASM ile)
async hashLargeFile(file) {
const CHUNK_SIZE = 1024 * 1024; // 1MB chunks
const chunks = Math.ceil(file.size / CHUNK_SIZE);
let combinedHash = '';
for (let i = 0; i < chunks; i++) {
const chunk = file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
const buffer = await chunk.arrayBuffer();
const chunkHash = await this.hashSHA256(buffer);
combinedHash += chunkHash;
// Progress bildirimi
self.postMessage({
type: 'PROGRESS',
progress: Math.round(((i + 1) / chunks) * 100)
});
}
// Final hash of hashes
const encoder = new TextEncoder();
return this.hashSHA256(encoder.encode(combinedHash));
}
}
Güvenlik Dikkat Noktaları ve Production Checklist
WASM kriptografi implementasyonu yaparken gözden kaçırdığınızda başınızı ağrıtacak konular var.
Bellek yönetimi ve key sıfırlama:
// Hassas veriyi bellekten güvenle sil
#[wasm_bindgen]
pub fn secure_zero(data: &mut [u8]) {
// Compiler'ın optimize edip silmemesi için volatile write kullan
for byte in data.iter_mut() {
unsafe {
std::ptr::write_volatile(byte, 0u8);
}
}
}
CORS ve Content Security Policy ayarları:
# Nginx konfigürasyonu - WASM dosyaları için
# /etc/nginx/sites-available/your-app.conf
server {
listen 443 ssl http2;
server_name your-app.example.com;
# WASM için MIME type
types {
application/wasm wasm;
}
# SharedArrayBuffer için gerekli headers (COOP/COEP)
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
# CSP - WASM'a izin ver
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'wasm-unsafe-eval';
worker-src 'self' blob:;
connect-src 'self';
" always;
# WASM dosyaları için cache
location ~* .wasm$ {
add_header Cache-Control "public, max-age=31536000, immutable";
gzip_static on;
}
location / {
root /var/www/your-app;
try_files $uri $uri/ /index.html;
}
}
Dikkat etmeniz gereken kritik noktalar:
- Timing attack: JavaScript’te
===ile hash karşılaştırması yapmayın, constant-time comparison kullanın - Key storage: WASM linear memory’de duran anahtarlar,
secure_zeroçağrılmadan GC’ye bırakılmamalı - PRNG kalitesi:
getrandomcrate’i tarayıcıdawindow.crypto.getRandomValueskullanıyor, güvenli; ama WebWorker dışında dikkatli olun - Side-channel: Branch prediction ve cache timing saldırıları için constant-time implementasyon şart
- Wasm module integrity: WASM dosyasının bütünlüğünü Subresource Integrity (SRI) ile doğrulayın
<!-- SRI ile WASM yükleme -->
<script type="module">
// WASM modülü hash kontrolü
const response = await fetch('./pkg/wasm_crypto_bg.wasm');
const buffer = await response.arrayBuffer();
const hash = await crypto.subtle.digest('SHA-256', buffer);
const hashHex = Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0')).join('');
// Beklenen hash ile karşılaştır (build time'da üret)
const expectedHash = 'a1b2c3...'; // CI/CD'den inject edilmeli
if (hashHex !== expectedHash) {
throw new Error('WASM dosyası bütünlük kontrolü başarısız!');
}
</script>
Performans Benchmarking
Gerçek ölçümler olmadan karar vermek zor. Basit bir benchmark kurulumu:
// benchmark.js
async function runBenchmarks() {
const { default: init, aes_encrypt, sha256 } =
await import('./pkg/wasm_crypto.js');
await init();
const testData = new Uint8Array(1024 * 1024); // 1MB
window.crypto.getRandomValues(testData);
const key = generate_aes_key();
// WASM AES benchmark
const wasmStart = performance.now();
for (let i = 0; i < 100; i++) {
aes_encrypt(key, testData);
}
const wasmTime = performance.now() - wasmStart;
// Web Crypto AES benchmark
const keyBuffer = Uint8Array.from(atob(key), c => c.charCodeAt(0));
const cryptoKey = await crypto.subtle.importKey(
'raw', keyBuffer, { name: 'AES-GCM' }, false, ['encrypt']
);
const webcryptoStart = performance.now();
for (let i = 0; i < 100; i++) {
const iv = crypto.getRandomValues(new Uint8Array(12));
await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, cryptoKey, testData);
}
const webcryptoTime = performance.now() - webcryptoStart;
console.log(`WASM AES (100x1MB): ${wasmTime.toFixed(2)}ms`);
console.log(`WebCrypto AES (100x1MB): ${webcryptoTime.toFixed(2)}ms`);
console.log(`Oran: ${(wasmTime / webcryptoTime).toFixed(2)}x`);
}
Genel bulgular şöyle özetlenebilir: AES için Web Crypto genellikle WASM’dan 1.5x-3x daha hızlı (hardware AES-NI sayesinde). SHA-256 için benzer tablo var. Ancak RSA ve daha egzotik algoritmalar (ChaCha20-Poly1305, Ed25519) için WASM rekabetçi hatta bazen daha iyi sonuç veriyor. Bunun yanı sıra Web Crypto’da bulunmayan algoritmaları (örneğin SM2, SM4 gibi Çin standartları veya post-quantum algoritmalar) WASM ile tarayıcıya taşımak için başka alternatif yok.
Sonuç
WebAssembly ile tarayıcı tabanlı kriptografi artık niş bir teknik değil, pratik bir çözüm. Sağlıktan fintech’e, e-devlet uygulamalarından kurumsal document management sistemlerine kadar geniş bir yelpazede kullanım alanı buluyor.
Özetle pratikte şu yaklaşımı öneriyorum: Standart AES ve SHA operasyonları için Web Crypto API’yi tercih edin, hardware acceleration’dan faydalanın. RSA, Ed25519 veya Web Crypto kapsamı dışındaki algoritmalar için Rust + wasm-pack kombinasyonunu kullanın. Güvenlik açısından bellek temizleme, timing attack koruması ve WASM dosyası bütünlük doğrulamasını asla atlamamalısınız.
Web Crypto ve WASM arasında seçim yapmak zorunda değilsiniz, ikisini birleştiren hibrit bir mimari çoğu zaman en iyi sonucu veriyor. Önemli olan kullanım senaryonuzu iyi analiz etmek: hangi algoritma, hangi veri boyutu, hangi performans hedefi. Bu soruları netleştirdikten sonra araç seçimi kendiliğinden ortaya çıkıyor.
Son olarak: tarayıcıda kriptografi yapmak sunucu tarafı güvenliği yerine geçmez, onu tamamlar. End-to-end encryption senaryolarında veya privacy-by-design mimarilerinde istemci tarafı kriptografi güçlü bir araç, doğru kullanıldığında gerçek değer katıyor.
