İçeriğe atla
HexaTransfer
Bloga dön
Sifreleme ve guvenlik

İstemci tarafı şifreleme öğreticisi: sıfırdan oluşturun

Web uygulamasında istemci tarafı şifreleme uygulamak için adım adım öğretici. Dosyaları cihazdan ayrılmadan önce tarayıcıda şifreleyin.

Tarayıcıda istemci taraflı dosya şifrelemesi, Web Crypto API kullanan yaklaşık 80 satır JavaScript alır. Örüntü şudur: tarayıcıda AES-256-GCM anahtarı üretin, dosyayı rastgele 96 bit nonce ile şifreleyin, şifreli metni HTTPS/TLS 1.3 üzerinden yükleyin ve elde edilen URL'yi sunuculara hiçbir zaman iletilmeyen parça tanımlayıcısına (#key=...) gömülü anahtarla paylaşın. Alıcı aynı parçayı kullanarak tarayıcıda şifresini çözer. Bu öğretici; büyük dosyalar için parçalama, 600.000 iterasyonla PBKDF2 aracılığıyla parola türetilen anahtarlar ve ilk denemeleri bozan tuzaklar dahil olmak üzere çalışan bir implementasyonu adım adım ele alır.

Tek Şemada Mimari

[Gönderici Tarayıcı]              [Sunucu]                [Alıcı Tarayıcı]
  Dosyayı Oku → AES anahtarı      POST kabul eder          Şifreli metin al
  AES-256-GCM ile şifrele         Şifreli metin depolar    Anahtarı URL #parçasından çıkar
  Şifreli metni POST et           Anahtar yok, düz metin   Tarayıcıda şifresini çöz
  #key=... ile URL oluştur        yok                      Dosyayı diske kaydet
                                  İndirme URL'si döner

Sunucu, yalnızca şifreli metni depolayan bir nesne deposudur. Yalnızca şifreli metni görür; şifreyi çözemez. Şifre çözme anahtarı URL parçasında yaşar; tarayıcılar bunu özel biçimde işler: HTTP istek satırında hiçbir zaman gönderilmez. Bu, HexaTransfer dahil her sıfır bilgi dosya aktarım hizmetinin temelidir.

Adım 1: Simetrik Anahtar Üretme

async function generateKey() {
  return await crypto.subtle.generateKey(
    { name: "AES-GCM", length: 256 },
    true, // URL'ye dışa aktarmamız gerektiğinden dışa aktarılabilir
    ["encrypt", "decrypt"]
  );
}

extractable: true bayrağı zorunludur; çünkü anahtarı bir URL parçasına serileştirmemiz gerekir. Anahtarın yalnızca bellekte yaşadığı bir akış oluşturuyorsanız (örneğin yapıştır-ve-gönder aracı), false olarak ayarlayın.

Adım 2: Dosyayı ArrayBuffer Olarak Okuma

async function readFile(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = () => reject(reader.error);
    reader.readAsArrayBuffer(file);
  });
}

Bu, dosyanın tamamını belleğe yükler. 500 MB altındaki dosyalar için uygundur. Daha büyük dosyalar için doğrudan akış bölümüne atlayın.

Adım 3: Tamponu Şifreleme

async function encryptFile(key, plaintext) {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const ciphertext = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv },
    key,
    plaintext
  );
  // Alıcının çıkarabilmesi için IV'yi şifreli metnin önüne ekle
  const combined = new Uint8Array(iv.length + ciphertext.byteLength);
  combined.set(iv, 0);
  combined.set(new Uint8Array(ciphertext), iv.length);
  return combined.buffer;
}

Nonce (IV) NIST SP 800-38D uyarınca 96 bit (12 bayt) uzunluğundadır. Gizli değildir; ancak anahtar başına benzersiz olmalıdır. Burada rastgele nonce'lar güvenlidir; çünkü dosya başına taze bir anahtar üretiyoruz. IV'yi şifreli metnin önüne eklemek yaygın bir kuraldır; alıcı şifre çözmeden önce bunu geri ayırır.

Adım 4: Şifreli Metni Yükleme

async function uploadCiphertext(ciphertext) {
  const response = await fetch("/api/upload", {
    method: "POST",
    body: ciphertext,
    headers: { "Content-Type": "application/octet-stream" },
  });
  const { fileId } = await response.json();
  return fileId;
}

Sunucu bir ikili blob alır, bir ID atar, saklar ve bu ID'yi döndürür. Hiçbir başlık dosya adını açığa çıkarmaz; hiçbir sorgu parametresi anahtarı taşımaz. Sunucunun hard diski yarın çalınsa, bir saldırgan anlamsız verilerle karşılaşır.

Adım 5: Parçada Anahtarla Paylaşım URL'si Oluşturma

async function buildShareURL(fileId, key) {
  const rawKey = await crypto.subtle.exportKey("raw", key);
  const keyBase64 = btoa(String.fromCharCode(...new Uint8Array(rawKey)))
    .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
  return `${location.origin}/f/${fileId}#${keyBase64}`;
}

Base64url kodlaması (+ ve / yerine - ve _) URL kaçış sorunlarını önler. = dolgusu estetik amaçlı kaldırılır.

Parça (#...) burada sihirlidir. Alıcı URL'yi yüklediğinde, tarayıcı parçayı istemci tarafında tutar. /f/{fileId} için HTTP GET, istek satırına #keyBase64 dahil etmez; dolayısıyla sunucu anahtarı hiçbir zaman öğrenmez. Bunu kendiniz doğrulayın: herhangi bir parça içeren URL'de tarayıcı geliştirici araçlarını açın ve Ağ sekmesini izleyin.

Adım 6: Alıcı Taraflı Şifre Çözme

async function downloadAndDecrypt() {
  const fileId = location.pathname.split("/").pop();
  const keyBase64 = location.hash.slice(1);
  const rawKey = Uint8Array.from(
    atob(keyBase64.replace(/-/g, "+").replace(/_/g, "/")),
    c => c.charCodeAt(0)
  );
  const key = await crypto.subtle.importKey(
    "raw", rawKey, "AES-GCM", false, ["decrypt"]
  );
  const response = await fetch(`/api/download/${fileId}`);
  const combined = new Uint8Array(await response.arrayBuffer());
  const iv = combined.slice(0, 12);
  const ciphertext = combined.slice(12);
  const plaintext = await crypto.subtle.decrypt(
    { name: "AES-GCM", iv }, key, ciphertext
  );
  const blob = new Blob([plaintext]);
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = "downloaded-file";
  a.click();
}

GCM'nin kimlik doğrulama etiketi decrypt() sırasında kontrol edilir. Şifreli metin kurcalanmışsa, çağrı OperationError fırlatır; temiz bir başarısızlık modudur.

PBKDF2 Aracılığıyla Parola Türetilen Anahtarlar

Kullanıcılar rastgele anahtar yerine parola giriyorsa, AES anahtarını PBKDF2 üzerinden türetin:

async function deriveKey(password, salt) {
  const passwordKey = await crypto.subtle.importKey(
    "raw", new TextEncoder().encode(password),
    "PBKDF2", false, ["deriveKey"]
  );
  return await crypto.subtle.deriveKey(
    {
      name: "PBKDF2",
      salt,
      iterations: 600000,
      hash: "SHA-256",
    },
    passwordKey,
    { name: "AES-GCM", length: 256 },
    false,
    ["encrypt", "decrypt"]
  );
}

600.000 iterasyonlu PBKDF2-SHA-256, OWASP'ın 2023 taban çizgisidir. Salt 16 rastgele bayt olmalı ve şifreli metinle birlikte depolanmalıdır (gizli değil, yalnızca benzersiz olması gerekir). Yeni kodlar için argon2-browser gibi bir kütüphane üzerinden Argon2id'yi göz önünde bulundurun; GPU saldırılarına çok daha iyi direnir.

Büyük Dosyaları Akışla İşleme

500 MB üzerindeki dosyalar parçalanmalıdır. File.stream() ile okuyun, her parçayı şifreleyin, sırayla yükleyin:

async function encryptStream(file, key) {
  const reader = file.stream().getReader();
  const chunks = [];
  let chunkIndex = 0;
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    const iv = new Uint8Array(12);
    // Benzersizliği garantilemek için nonce'a parça indeksini kodla
    new DataView(iv.buffer).setBigUint64(4, BigInt(chunkIndex++));
    const ct = await crypto.subtle.encrypt(
      { name: "AES-GCM", iv }, key, value
    );
    chunks.push({ iv, ct });
  }
  return chunks;
}

Nonce'u parça indeksinden türetmek, durum takibi yapmaksızın benzersizliği garanti eder. Alıcı tarafında sıralı şifre çözme ve birleştirme yapılır.

Gerçek akış AEAD için libsodium'un crypto_secretstream_xchacha20poly1305'i libsodium.js aracılığıyla daha temizdir ve kesme saldırılarını algılar. Web Crypto'nun 2026'da eşdeğer bir primitifi bulunmuyor.

Test Etme ve Tuzaklar

Kaçınılması gereken yaygın hatalar:

  • Anahtar veya nonce için Math.random() kullanmak: her zaman crypto.getRandomValues() kullanın.
  • Aynı anahtarla nonce yeniden kullanımı: GCM güvenliğini bozar. Dosya başına rastgele anahtarlar bunu güvenli kılar; parça bazlı akışlar parça başına benzersiz nonce gerektirir.
  • HTTPS kontrolü yapmamak: crypto.subtle güvensiz kaynaklarda tanımsızdır. Geliştirme sırasında localhost veya öz imzalı sertifika ile test edin.
  • Anahtarları localStorage'da depolamak: kaynak üzerindeki herhangi bir XSS okuyabilir. Bunun yerine URL parçası örüntüsünü veya dışa aktarılamaz anahtarları kullanın.
  • IV'yi şifreli metinle birlikte eklemeyi unutmak: faydalı bir hata mesajı olmaksızın şifre çözme başarısız olur. Her zaman önüne ekleyin veya birlikte serileştirin.
  • Parçayı yanlış işlemek: URL'yi (parçayla birlikte) yanlışlıkla üçüncü taraf bir hizmete göndermeyin. Parça hassassa yalnızca uçtan uca kanallar üzerinden paylaşın.

Sunucu Taraflı Sorumluluklar

İstemci taraflı şifreleme mimarisinde sunucunun görevi küçüktür: POST kabul et, blob depola, ID döndür, blob için GET sun, süre sonunda sil. Kripto yok. Sunucunun depolama ötesinde yapması gerekenler:

  • Dosya boyutu sınırlarını zorla (kötüye kullanımı önle)
  • Yükleme ve indirme hız sınırı uygula
  • Kısa saklama ayarla (HexaTransfer gibi 7 gün makul bir varsayılandır)
  • Yalnızca gerekli olanı günlüğe kaydet (yükleme zaman damgası; gizlilik önceliğiyse IP adresi kaydetme)
  • HSTS ile TLS 1.3 üzerinden sun
  • API yalnızca kendi etki alanlarınızdan çağrılıyorsa kaynakları kısıtlayan CORS başlıkları ekle

Hepsini Bir Araya Getirme

Minimal çalışan uygulama, bir HTML dosyası ve 50 satırlık bir Express arka ucuna sığar. Toplam bağımlılık: istemcide hiçbiri yok (Web Crypto yerel), sunucuda Express ve multer. Şifreleme, AES-256-GCM primitiviyle aynı güçtedir; çünkü tam olarak onu kullanıyorsunuz. Yanlış gidecek gizli bir algoritma yok; yalnızca doğru kullanılacak primitifler var.

En zor kısımlar kenar durumlardır: büyük dosyalar, parola-to-anahtar akışları, şifre çözme başarısız olduğunda alıcı UX, süresi dolmuş bağlantıları zarif biçimde işleme. Temel kriptografi açıktır.

hexatransfer.com'da deneyin — ücretsiz, hesap gerekmez, 10 GB'a kadar.

Uçtan uca şifreleme ile büyük dosyaları güvenle gönderin

Uçtan uca şifreleme ile 10 GB'a kadar dosya ücretsiz aktarın. Hesap gerekmez. Dosyalarınız yüklenmeden önce tarayıcınızda şifrelenir — başka kimse okuyamaz.

Dosya gönder