WebCrypto ile istemci tarafında verileri şifreleme

Web özellikleri: Web Cryptography

Kuantuma dayanıklı anahtar kapsülleme mekanizmaları (KEM'ler) veya klasik anahtar anlaşması kullanarak güvenilmeyen ağlar üzerinden paylaşılan gizli anahtarlar oluşturmak ve tarayıcıdaki hassas verileri ilişkili verilerle kimliği doğrulanmış şifreleme (AEAD) ile şifrelemek için Web Cryptography API'yi (crypto.subtle) kullanın. Mesajları veya yapıları şifrelemeden imzalama ve doğrulama hakkında bilgi edinmek için WebCrypto ile verileri imzalama ve doğrulama başlıklı makaleyi inceleyin.

Uçtan uca zarf şifreleme akışı iki aşamaya ayrılır:

  • Tek seferlik alıcı kurulumu (1-2. adımlar): Alıcı bir anahtar çifti oluşturur, gelecekteki oturumlar için CryptoKeyPair anahtarını IndexedDB'de saklar ve gönderenlerle paylaşmak üzere ortak anahtarı ('raw-public') dışa aktarır.
  • İleti başına şifreleme (3-4. adımlar): Gönderen, her yük için alıcının ortak anahtarını içe aktarır, yeni bir tek seferlik ortak anahtar ve şifreli metin kapsülü oluşturmak için encapsulateKey() işlevini çağırır, yükü yeni bir 12 baytlık IV ile şifreler ve alıcıya { capsule, iv, ciphertext } gönderir.

1. KEM algoritması seçme ve alıcı anahtar çifti oluşturma

Her iki tarafın da anahtarlarını birleştirdiği Diffie-Hellman anahtar anlaşmasının (deriveKey()) aksine, Anahtar Kapsülleme Mekanizması (KEM), tek bir encapsulateKey() çağrısında hem rastgele bir paylaşılan gizli anahtar hem de şifreli metin kapsülü oluşturmak için alıcının ortak anahtarını kullanır. Alıcı, decapsulateKey() kullanarak şifreli metin kapsülünden aynı paylaşılan anahtarı kurtarır.

Varsayılan olarak 'MLKEM768-X25519' kullanın: Bu algoritma, kuantum sonrası ML-KEM'i (NIST FIPS 203'te standartlaştırılmıştır) klasik 'X25519' ile birleştirir. Böylece, her iki algoritma da geçerli olduğu sürece paylaşılan gizli anahtar güvende kalır. Bir politika, karma bileşen içermeyen saf FIPS 203 çıkışı gerektirdiğinde bağımsız 'ML-KEM-768''yı, NIST Kategori 5 (CNSA 2.0) uyumluluğu gerektirdiğinde ise 'ML-KEM-1024''yı seçin.

Algoritma adı Güvenlik seviyesi ve kullanım alanı Ortak anahtar ('raw-public') Özel tohum ('raw-seed') Şifreli metin boyutu
'MLKEM768-X25519' Karma kuantum sonrası ('ML-KEM-768') + klasik ('X25519'); önerilen varsayılan 1.216 bayt 32 bayt 1.120 bayt
'ML-KEM-768' NIST Kategori 3 (~192 bit güvenlik); bağımsız kuantum sonrası KEM 1184 bayt 64 bayt 1088 bayt
'ML-KEM-1024' NIST Kategori 5 (~256 bit güvenlik); en yüksek güvenlik marjı 1568 bayt 64 bayt 1568 bayt
  • Algoritma adı olarak 'MLKEM768-X25519', 'ML-KEM-768' veya 'ML-KEM-1024' değerini iletin ('X-Wing' veya 'X25519MLKEM768' gibi takma adlar ve 'ML-KEM-512', NotSupportedError hatasına neden olur).
  • KEM anahtar çifti oluştururken extractable: false ve ['encapsulateKey', 'decapsulateKey'] (veya ['encapsulateBits', 'decapsulateBits']) parametrelerini iletin. Ayıklanamayan CryptoKey ve CryptoKeyPair nesneleri, özel anahtar baytları dışa aktarılmadan yapılandırılmış klonlama (objectStore.put(recipientKeyPair, 'recipient-kem-key')) aracılığıyla doğrudan IndexedDB'de oturumlar arasında kalıcı hale getirilebilir:
const recipientKeyPair = await crypto.subtle.generateKey(
  'MLKEM768-X25519',
  false,
  ['encapsulateKey', 'decapsulateKey'],
);

2. Anahtarları dışa ve içe aktarma

Modern WebCrypto algoritmaları ('MLKEM768-X25519', 'ML-KEM-768', 'ML-KEM-1024' ve 'ChaCha20-Poly1305'), importKey() ve exportKey()'da eski 'raw' biçimini reddeder ve NotSupportedError hatası verir. Her zaman açık anahtar biçimini belirtin:

  • 'raw-public': Ortak anahtar baytları (1216, 1184 veya 1568 bayt). KEM ortak anahtarını içe aktarırken ['encapsulateKey'] (veya ['encapsulateBits']) değerini iletin.
  • 'raw-seed': Özel anahtar çekirdek baytları ('MLKEM768-X25519' için 32 bayt; 'ML-KEM-768' ve 'ML-KEM-1024' için 64 bayt). KEM özel anahtarı içe aktarırken ['decapsulateKey'] (veya ['decapsulateBits']) değerini iletin (importKey() veya getPublicKey() için ['encapsulateKey', 'decapsulateKey'] değerini iletmek SyntaxError hatasına neden olur). Hem 'raw' hem de 'raw-private', KEM özel anahtarlarında NotSupportedError hatasına neden olur. 'MLKEM768-X25519''nin yalnızca 'raw-public', 'raw-seed' ve 'jwk''yi desteklediğini, 'spki' veya 'pkcs8''yi desteklemediğini unutmayın.
  • 'raw-secret': Simetrik gizli anahtar baytları ('ChaCha20-Poly1305' ve 'AES-GCM' ile 'HKDF''te kabul edilir).

Ortak anahtar baytlarının serileştirilebilmesi (ör. new Uint8Array(recipientPublicKeyBytes).toBase64() aracılığıyla Base64 veya onaltılık olarak) ve gönderenlerle paylaşılabilmesi için alıcının ortak anahtarını 'raw-public' ile dışa aktarın:

// Export the recipient's public key bytes
const recipientPublicKeyBytes = await crypto.subtle.exportKey(
  'raw-public',
  recipientKeyPair.publicKey,
);

// Import the recipient's public key on the sender (usage must be ['encapsulateKey'])
const importedPublicKey = await crypto.subtle.importKey(
  'raw-public',
  recipientPublicKeyBytes,
  'MLKEM768-X25519',
  true,
  ['encapsulateKey'],
);

Yalnızca özel bir CryptoKey anahtarınız varsa (extractable: false ile bile) ve buna karşılık gelen herkese açık CryptoKey anahtarına ihtiyacınız varsa crypto.subtle.getPublicKey() komutunu çalıştırın:

const publicKey = await crypto.subtle.getPublicKey(
  recipientKeyPair.privateKey,
  ['encapsulateKey'],
);

3. Paylaşılan anahtarı kapsülleme ve kapsüllemeyi kaldırma

encapsulateKey() ve decapsulateKey(), sharedKeyAlgorithm içinde belirtilen algoritmayı kullanarak ('raw-secret' biçimiyle) 32 baytlık (256 bit) KEM paylaşılan gizli anahtarını bir CryptoKey içine aktarır. AEAD şifreleme anahtarı oluşturmak için doğrudan 'ChaCha20-Poly1305' veya 'AES-GCM' değerini iletin:

// Sender encapsulates a 256-bit AEAD key and produces a ciphertext capsule
const { sharedKey: senderKey, ciphertext: encapsulatedCiphertext } =
  await crypto.subtle.encapsulateKey(
    'MLKEM768-X25519',
    importedPublicKey,
    'ChaCha20-Poly1305', // Or 'AES-GCM'
    false,
    ['encrypt', 'decrypt'],
  );

// Recipient decapsulates the ciphertext capsule to recover the identical AEAD key
const recipientKey = await crypto.subtle.decapsulateKey(
  'MLKEM768-X25519',
  recipientKeyPair.privateKey,
  encapsulatedCiphertext,
  'ChaCha20-Poly1305',
  false,
  ['encrypt', 'decrypt'],
);

'HKDF' ile alan adı ayırma

Paylaşılan anahtarı belirli bir protokol bağlamına (info ve salt) bağlamak veya tek bir kapsüllemeden birden fazla alt anahtar türetmek için 'HKDF' değerini ['deriveKey'] kullanımıyla sharedKeyAlgorithm olarak iletin, ardından crypto.subtle.deriveKey() işlevini çağırın:

const { sharedKey: hkdfBaseKey, ciphertext } = await crypto.subtle.encapsulateKey(
  'MLKEM768-X25519',
  importedPublicKey,
  'HKDF',
  false,
  ['deriveKey'],
);

const aeadKey = await crypto.subtle.deriveKey(
  {
    name: 'HKDF',
    hash: 'SHA-256',
    salt: new Uint8Array(32),
    info: new TextEncoder().encode('envelope-v1'),
  },
  hkdfBaseKey,
  { name: 'AES-GCM', length: 256 },
  false,
  ['encrypt', 'decrypt'],
);

4. Yükleri şifreleme ve şifrelerini çözme ('ChaCha20-Poly1305' ve 'AES-GCM')

Her şifreleme işlemi için yeni, kriptografik olarak rastgele 12 baytlık (96 bit) başlatma vektörü (iv) kullanarak AEAD algoritması ('ChaCha20-Poly1305' veya 'AES-GCM') ile yük verilerini şifreleyin. (iv, key) çiftini asla yeniden kullanmayın. Özel AES donanım hızlandırması olmayan cihazlarda tutarlı ve sabit süreli performans için 'ChaCha20-Poly1305''yı, mevcut WebCrypto veya FIPS 140 dağıtımlarıyla birlikte çalışırken 'AES-GCM''yı seçin.

  • 'ChaCha20-Poly1305': Sabit bir 256 bit (32 bayt) anahtar kullanır (generateKey('ChaCha20-Poly1305', extractable, ['encrypt', 'decrypt']), length özelliği olmayan bir dize veya { name: 'ChaCha20-Poly1305' } alır). iv tam olarak 12 bayt olmalıdır ve 128 bit Poly1305 kimlik doğrulama etiketi, şifreli metne eklenir.
  • 'AES-GCM': generateKey() veya deriveKey() çağrılırken { name: 'AES-GCM', length: 256 }, encrypt() ve decrypt() çağrılırken ise 12 baytlık iv iletin.
  • additionalData (AAD): Şifrelenmemiş meta verileri (ör. kayıt kimliği, alıcı kimliği veya protokol sürümü) additionalData aracılığıyla kimlik doğrulama etiketine bağlayın. Böylece, şifreli metin veya meta verilerde değişiklik yapılması decrypt()'nin OperationError ile reddetmesine neden olur.
/**
 * Encrypts UTF-8 text with an AEAD CryptoKey ('ChaCha20-Poly1305' or 'AES-GCM').
 * @param {CryptoKey} key
 * @param {string} plaintext
 * @param {Uint8Array} [additionalData]
 */
export async function encryptMessage(key, plaintext, additionalData = new Uint8Array(0)) {
  // Always generate a fresh 12-byte (96-bit) IV per message
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encoded = new TextEncoder().encode(plaintext);

  const ciphertextBuffer = await crypto.subtle.encrypt(
    {
      name: key.algorithm.name,
      iv,
      additionalData,
    },
    key,
    encoded,
  );

  return { iv, ciphertext: new Uint8Array(ciphertextBuffer) };
}

/**
 * Decrypts and authenticates an AEAD payload. Throws OperationError if tampered.
 * @param {CryptoKey} key
 * @param {Uint8Array} iv
 * @param {Uint8Array} ciphertext
 * @param {Uint8Array} [additionalData]
 */
export async function decryptMessage(key, iv, ciphertext, additionalData = new Uint8Array(0)) {
  const plaintextBuffer = await crypto.subtle.decrypt(
    {
      name: key.algorithm.name,
      iv,
      additionalData,
    },
    key,
    ciphertext,
  );

  return new TextDecoder().decode(plaintextBuffer);
}

Yedek stratejiler

Temel hedefiniz henüz SubtleCrypto.supports(), encapsulateKey() veya 'ChaCha20-Poly1305''yi desteklemiyorsa statik SubtleCrypto.supports() yöntemini kullanarak desteği tespit edin ve geçici statik Diffie-Hellman anahtar anlaşmasına ('X25519' + 'HKDF' + 'AES-GCM' veya 'X25519' desteğinden önceki tarayıcıları hedeflerken 'P-256' ile 'ECDH') geri dönün:

  • SubtleCrypto.supports() öğesini crypto.subtle üzerinde değil, SubtleCrypto oluşturucusunda çağırın: globalThis.SubtleCrypto?.supports?.('encapsulateKey', 'MLKEM768-X25519', 'ChaCha20-Poly1305'), boole değeri döndüren eşzamanlı bir statik yöntemdir (crypto.subtle.supports, undefined). SubtleCrypto.supports() öğesinin işleme özel parametreleri doğruladığını unutmayın: Yalın algoritma dizeleri 'encapsulateKey', 'decapsulateKey', 'generateKey', 'importKey', 'exportKey' ve 'getPublicKey' için çalışır. 'encrypt' veya 'decrypt' öğesini doğrudan araştırmak için ise iv (ör. SubtleCrypto.supports('encrypt', { name: 'ChaCha20-Poly1305', iv: new Uint8Array(12) })) gerekir.
  • Yedek anahtar biçimi ('raw'): Modern WebCrypto'yu destekleyen tarayıcılar 'X25519' ve 'ECDH' üzerinde 'raw-public''yi de kabul ederken, eski tarayıcıları hedefleyen yedek yollarda 'raw''yi kullanın.
  • Klasik yedek ('X25519' + 'HKDF' + 'AES-GCM'): SubtleCrypto.supports() kullanılamadığında veya false döndürdüğünde gönderen için geçici bir 'X25519' anahtar çifti oluşturun, geçici ortak anahtarı kapsül baytları olarak dışa aktarın, deriveBits({ name: 'X25519', public: peerPublicKey }, privateKey, 256) aracılığıyla 256 bit türetin ve 256 bitlik bir 'AES-GCM' anahtarı türetmek için bu bitleri 'HKDF' ('SHA-256') üzerinden iletin.
const KEM_PUBLIC_KEY_ALGORITHMS = {
  1184: 'ML-KEM-768',
  1216: 'MLKEM768-X25519',
  1568: 'ML-KEM-1024',
};

export function supportsPostQuantumEnvelope() {
  return (
    typeof globalThis.SubtleCrypto?.supports === 'function' &&
    SubtleCrypto.supports('encapsulateKey', 'MLKEM768-X25519', 'ChaCha20-Poly1305')
  );
}

export async function generateRecipientKeys() {
  if (supportsPostQuantumEnvelope()) {
    const keyPair = await crypto.subtle.generateKey(
      'MLKEM768-X25519',
      false,
      ['encapsulateKey', 'decapsulateKey'],
    );
    const publicKeyBytes = new Uint8Array(
      await crypto.subtle.exportKey('raw-public', keyPair.publicKey),
    );
    return { suite: 'MLKEM768-X25519+ChaCha20-Poly1305', keyPair, publicKeyBytes };
  }

  // Classical fallback: X25519 key agreement + HKDF + AES-GCM
  const keyPair = await crypto.subtle.generateKey('X25519', false, ['deriveBits']);
  const publicKeyBytes = new Uint8Array(
    await crypto.subtle.exportKey('raw', keyPair.publicKey),
  );
  return { suite: 'X25519+HKDF+AES-GCM', keyPair, publicKeyBytes };
}

async function deriveFallbackAesKey(privateKey, peerPublicKey, infoBytes = new Uint8Array(0)) {
  const sharedSecret = await crypto.subtle.deriveBits(
    { name: 'X25519', public: peerPublicKey },
    privateKey,
    256,
  );
  const hkdfKey = await crypto.subtle.importKey('raw', sharedSecret, 'HKDF', false, ['deriveKey']);
  return crypto.subtle.deriveKey(
    {
      name: 'HKDF',
      hash: 'SHA-256',
      salt: new Uint8Array(32),
      info: infoBytes,
    },
    hkdfKey,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt'],
  );
}

export async function encapsulateEnvelopeKey(
  recipientPublicKeyOrBytes,
  infoBytes = new Uint8Array(0),
) {
  const pubKey =
    recipientPublicKeyOrBytes instanceof CryptoKey
      ? recipientPublicKeyOrBytes
      : await (() => {
          const bytes =
            recipientPublicKeyOrBytes instanceof Uint8Array
              ? recipientPublicKeyOrBytes
              : new Uint8Array(recipientPublicKeyOrBytes);
          const kemName = KEM_PUBLIC_KEY_ALGORITHMS[bytes.byteLength];
          return kemName
            ? crypto.subtle.importKey('raw-public', bytes, kemName, true, ['encapsulateKey'])
            : crypto.subtle.importKey('raw', bytes, 'X25519', true, []);
        })();

  if (pubKey.algorithm.name !== 'X25519') {
    const { sharedKey, ciphertext } = await crypto.subtle.encapsulateKey(
      pubKey.algorithm.name,
      pubKey,
      'ChaCha20-Poly1305',
      false,
      ['encrypt', 'decrypt'],
    );
    return { sharedKey, capsuleBytes: new Uint8Array(ciphertext) };
  }

  // Classical fallback: generate ephemeral X25519 key pair and send ephemeral public key as capsule
  const ephemeral = await crypto.subtle.generateKey('X25519', false, ['deriveBits']);
  const capsuleBytes = new Uint8Array(await crypto.subtle.exportKey('raw', ephemeral.publicKey));
  const sharedKey = await deriveFallbackAesKey(ephemeral.privateKey, pubKey, infoBytes);
  return { sharedKey, capsuleBytes };
}

export async function decapsulateEnvelopeKey(
  recipientPrivateKey,
  capsuleBytes,
  infoBytes = new Uint8Array(0),
) {
  if (recipientPrivateKey.algorithm.name !== 'X25519') {
    return crypto.subtle.decapsulateKey(
      recipientPrivateKey.algorithm.name,
      recipientPrivateKey,
      capsuleBytes,
      'ChaCha20-Poly1305',
      false,
      ['encrypt', 'decrypt'],
    );
  }

  const ephemeralPubKey = await crypto.subtle.importKey('raw', capsuleBytes, 'X25519', true, []);
  return deriveFallbackAesKey(recipientPrivateKey, ephemeralPubKey, infoBytes);
}