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

Büyük dosyalar için progresif şifreleme: akış ve şifreleme

Akış API'leriyle büyük dosyaları progresif olarak şifreleyin. Bellek tükenmeden çok GB'lık dosyaları parçalar halinde şifreleyin.

Progresif (akışlı) şifreleme, tam veri yükünü hiçbir zaman belleğe almadan bir dosyayı parça parça işler. Tarayıcıda 10 GB'lık bir yükleme için bu, çalışan bir uygulama ile çöken bir uygulama arasındaki farktır. Model şu şekilde işliyor: File.stream() üzerinden bir parça okuyun, benzersiz nonce ile AES-256-GCM kullanarak şifreleyin, şifreli metni doğrudan ReadableStream gövdesiyle fetch üzerinden bir yükleme akışına iletin, tamponu boşaltın ve devam edin. Dosya boyutundan bağımsız olarak bellek 4-16 MB'da sabit kalıyor. libsodium'un crypto_secretstream_xchacha20poly1305'i, kırpma algılaması dahil uygun akışlı AEAD anlamsallığı ekliyor. Gerçek donanımda geçerliliğini koruyan rakamlarla somut uygulama aşağıda.

Tampon Şifrelemenin Neden Başarısız Olduğu

10 GB'lık bir dosyada FileReader.readAsArrayBuffer(file) kullanmak tarayıcı belleğinde 10 GB ayırıyor. 32 GB RAM'li masaüstü Chrome'da bu işe yarayabilir. Sekme başına 400 MB bellek sınırına sahip mobil Safari'de tamamlanmadan çöküyor. Firefox'ta 2 GB üzerindeki ArrayBuffer dahili V8 sınırlarına çarparak RangeError fırlatıyor.

Tahsisi karşılayabilen donanımda bile 10 GB'ı elde tutmak çöp toplamayı engelliyor ve patolojik sayfalamayı tetikliyor. Doğru yanıt, tam tamponu hiçbir zaman tahsis etmemektir.

Akış Deseni

async function streamEncrypt(file, key, uploadURL) {
  const CHUNK_SIZE = 4 * 1024 * 1024; // 4 MB
  const reader = file.stream().getReader();
  let chunkIndex = 0;
  let buffer = new Uint8Array(0);

  const uploadStream = new ReadableStream({
    async pull(controller) {
      while (buffer.length < CHUNK_SIZE) {
        const { done, value } = await reader.read();
        if (done) {
          if (buffer.length > 0) {
            await enqueueEncrypted(controller, buffer, chunkIndex++, key);
          }
          controller.close();
          return;
        }
        const newBuf = new Uint8Array(buffer.length + value.length);
        newBuf.set(buffer, 0);
        newBuf.set(value, buffer.length);
        buffer = newBuf;
      }
      const chunk = buffer.subarray(0, CHUNK_SIZE);
      buffer = buffer.subarray(CHUNK_SIZE);
      await enqueueEncrypted(controller, chunk, chunkIndex++, key);
    }
  });

  await fetch(uploadURL, {
    method: "POST",
    body: uploadStream,
    duplex: "half",
    headers: { "Content-Type": "application/octet-stream" },
  });
}

async function enqueueEncrypted(controller, chunk, index, key) {
  const iv = new Uint8Array(12);
  new DataView(iv.buffer).setBigUint64(4, BigInt(index));
  const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, chunk);
  controller.enqueue(new Uint8Array(ct));
}

İki temel API: File.stream(), dosya içeriklerinin ReadableStream'ini veriyor; ReadableStream gövdesiyle fetch, tam gövdeyi tamponlamadan yüklemeyi akıtıyor. duplex: "half", akışlı istek gövdeleri için Chrome 105+'ta zorunlu.

Bellek kullanımı: herhangi bir anda bir kaynak parçası, bir tampon artığı, bir şifreli parça. 4 MB parça boyutunda zirve ~12-16 MB.

Akışlarda Nonce Yönetimi

Her parça benzersiz bir nonce gerektiriyor. Üç yaklaşım:

Sayaç tabanlı: parça dizinini 96 bitlik nonce'a göm. Çakışmaları önlemek için üst 32 biti rastgele bir öneke ayarla, alt 64 biti parça dizinine.

const noncePrefix = crypto.getRandomValues(new Uint32Array(1));
function makeNonce(chunkIndex) {
  const iv = new Uint8Array(12);
  new DataView(iv.buffer).setUint32(0, noncePrefix[0]);
  new DataView(iv.buffer).setBigUint64(4, BigInt(chunkIndex));
  return iv;
}

Parça başına rastgele: crypto.getRandomValues(new Uint8Array(12)). Dosya başına anahtarlar için güvenli; parçalar arasındaki doğum günü çakışmaları ~2^48'de gerçekleşiyor. Nonce'u her parçanın şifreli metninin yanında saklayın.

HKDF üzerinden türetilmiş: parça başına anahtarlar türetmek için HKDF kullanın, ardından sabit nonce kullanın. Çoğu durum için aşırı.

Dosya başına taze anahtar için sayaç tabanlı yaklaşım en basit; parça başına ayrı nonce saklamayı ortadan kaldırıyor.

Kırpma Saldırıları ve Nasıl Algılanır

Naif parçalı AES-GCM'deki kritik açık: saldırgan sondaki parçaları düşürebilir ve her hayatta kalan parça sorunsuz şifre çözüyor. Algılama, parçaları birbirine bağlamayı gerektiriyor.

Seçenek 1: her parçanın AAD'sine toplam parça sayısını dahil edin. Alıcı sayının alınanla eşleştiğini doğrular.

const aad = new TextEncoder().encode(JSON.stringify({
  totalChunks,
  fileSize: file.size,
}));
const ct = await crypto.subtle.encrypt(
  { name: "AES-GCM", iv, additionalData: aad },
  key,
  chunk
);

Seçenek 2: libsodium'un crypto_secretstream_xchacha20poly1305'ini kullanın. Parçaları kriptografik olarak zincirliyor ve alıcının doğruladığı TAG_FINAL işaretçisini gönderiyor:

const { state, header } = sodium.crypto_secretstream_xchacha20poly1305_init_push(key);
// Her parça için TAG_MESSAGE ile push yapın
// Son parça için TAG_FINAL ile push yapın
const lastCt = sodium.crypto_secretstream_xchacha20poly1305_push(
  state, lastChunk, null,
  sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
);

Şifre çözme sırasında alıcının pull çağrıları zinciri doğrulayıp eksik son parçaları algılıyor. libsodium.js göndermek isteyenler için en temiz seçenek bu.

Alıcı Tarafında Akışlı Şifre Çözme

Alıcı tarafında simetrik desen:

async function streamDecrypt(downloadURL, key, onChunk) {
  const response = await fetch(downloadURL);
  const reader = response.body.getReader();
  let buffer = new Uint8Array(0);
  let chunkIndex = 0;
  const ENCRYPTED_CHUNK_SIZE = 4 * 1024 * 1024 + 16; // GCM etiketi dahil

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    const newBuf = new Uint8Array(buffer.length + value.length);
    newBuf.set(buffer);
    newBuf.set(value, buffer.length);
    buffer = newBuf;
    while (buffer.length >= ENCRYPTED_CHUNK_SIZE) {
      const ct = buffer.subarray(0, ENCRYPTED_CHUNK_SIZE);
      buffer = buffer.subarray(ENCRYPTED_CHUNK_SIZE);
      const iv = makeNonce(chunkIndex++);
      const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct);
      onChunk(new Uint8Array(pt));
    }
  }
  // Son kısmi parçayı ele al
  if (buffer.length > 0) {
    const iv = makeNonce(chunkIndex);
    const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, buffer);
    onChunk(new Uint8Array(pt));
  }
}

Alıcı tarafında onChunk geri aramaları, şifresi çözülen baytları doğrudan disk yazmaları için File System Access API'ye ya da tarayıcı yerel indirme için Blob'a ekleyebilir.

File System Access API Üzerinden Diske Yazma

Çok büyük indirmeler için tam şifresi çözülmüş sonucu Blob'a yüklemek akış kullanmanın amacını ortadan kaldırıyor. File System Access API (Chrome 86+, OPFS aracılığıyla kısmi Safari desteği) alıcının yerel bir dosya seçip parçaları doğrudan yazmasına olanak tanıyor:

const handle = await window.showSaveFilePicker({
  suggestedName: "decrypted-file",
});
const writable = await handle.createWritable();

await streamDecrypt(url, key, async (chunk) => {
  await writable.write(chunk);
});
await writable.close();

Parçalar hemen diske gittiğinden bellek sınırlı kalıyor. Arayüz gerçekçi ilerleme gösteriyor. Kullanıcılar indirmenin ortasında iptal edebiliyor.

Firefox masaüstünde henüz showSaveFilePicker'ı desteklemiyor. Birkaç yüz MB'ın altındaki dosyalar için Blob'a geri dönün ya da GB üzeri Firefox iş akışları için Origin Private File System kullanın.

fetch Üzerinden Yükleme Akışı

Chrome 105+ ve Firefox 127+, duplex: "half" ile akışlı istek gövdelerini destekliyor. Bundan önce yüklemeler tam tamponlar olmak zorundaydı ya da elle yönetilen parçalı aktarım kodlamasıyla multipart yapısı kullanılıyordu.

S3 uyumlu çok parçalı yüklemeler için her parça ayrı bir istek olarak gönderiliyor. Şifreli akışı 5-25 MB'lık parçalara bölün (S3'ün minimum parça boyutu 5 MB, maksimum 5 GB) ve son CompleteMultipartUpload çağrısıyla tamamlayın. Bu tüm tarayıcılarda çalışıyor ve ücretsiz olarak devam edilebilirlik sağlıyor.

Akışlar Üzerinden İlerleme Raporlama

İşlenen baytları takip edin:

let processed = 0;
const onChunk = (chunkSize) => {
  processed += chunkSize;
  updateProgressBar(processed / file.size);
};

Gereksiz yeniden çizimleri önlemek için ilerleme güncellemelerini requestAnimationFrame ile 10-20 Hz'e kısıtlayın. 100 MB/s işleme hızında 10 GB'lık dosyalar için ham olaylar saniyede 100 güncelleme üretiyor; arayüz bunun çok azına ihtiyaç duyuyor.

10 GB Dosyada Kıyaslamalar

2024 MacBook Pro (M3 Max) ve hızlı SSD üzerinde: File.stream() ile diskten ham okuma 2,5 GB/s, Web Crypto ile AES-256-GCM 1,7 GB/s, birleşik ardışık düzen 1,1 GB/s (seri zincir tarafından sınırlı), Gigabit Ethernet üzerinden yükleme 115 MB/s (ağ sınırlı), dosya boyutundan bağımsız zirve bellek 14 MB. Mobil rakamlar yaklaşık masaüstünün %30-50'si. 10 GB'lık dosya Gigabit'te ~90 saniyede, tipik ev bağlantısında ~15 dakikada yükleniyor. Şifreleme dar boğaz değil — ağ.

Hata Kurtarma

10 GB'lık yükleme sırasında ağ kesintisi yaygın. Stratejiler:

  • Çok parçalı devam edilebilir yüklemeler: her parça bağımsız; yalnızca başarısız olan parçayı yeniden yükleyin.
  • tus protokolü: Vimeo gibi şirketler tarafından desteklenen açık devam edilebilir yükleme standardı; akış için doğal.
  • Kaynak dosya tanıtıcısını açık tutun: File.slice tekrarlanabilirse son başarılı parçadan yeniden başlayın.

HexaTransfer'ın 10 GB sınırı tek bir tarayıcı sekmesinde ulaşılabilir; çünkü bu akış ardışık düzeni belleği sınırlı tutuyor ve çok parçalı yeniden denemeyle kesintileri zarif biçimde karşılıyor. Arka uçta desteklenirse aynı model daha büyük sınırlara ölçekleniyor.

Özet

Tüm dosyayı tahsis etmeyin. Parçalar halinde okuyun, parçalar halinde şifreleyin, parçalar halinde yükleyin, her parçayı işledikten sonra boşaltın. Kırpma saldırılarını önlemek için parçaları AAD veya akışlı AEAD ile kriptografik olarak bağlayın. Her şeye ilerleme çubuğu koyun. Yalnızca masaüstünde değil, mobilde de test edin.

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