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
CryptoKeyPairanahtarı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',NotSupportedErrorhatasına neden olur). - KEM anahtar çifti oluştururken
extractable: falseve['encapsulateKey', 'decapsulateKey'](veya['encapsulateBits', 'decapsulateBits']) parametrelerini iletin. AyıklanamayanCryptoKeyveCryptoKeyPairnesneleri, ö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,1184veya1568bayt). KEM ortak anahtarını içe aktarırken['encapsulateKey'](veya['encapsulateBits']) değerini iletin.'raw-seed': Özel anahtar çekirdek baytları ('MLKEM768-X25519'için32bayt;'ML-KEM-768've'ML-KEM-1024'için64bayt). KEM özel anahtarı içe aktarırken['decapsulateKey'](veya['decapsulateBits']) değerini iletin (importKey()veyagetPublicKey()için['encapsulateKey', 'decapsulateKey']değerini iletmekSyntaxErrorhatasına neden olur). Hem'raw'hem de'raw-private', KEM özel anahtarlarındaNotSupportedErrorhatası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).ivtam olarak 12 bayt olmalıdır ve 128 bit Poly1305 kimlik doğrulama etiketi, şifreli metne eklenir.'AES-GCM':generateKey()veyaderiveKey()çağrılırken{ name: 'AES-GCM', length: 256 },encrypt()vedecrypt()çağrılırken ise 12 baytlıkiviletin.additionalData(AAD): Şifrelenmemiş meta verileri (ör. kayıt kimliği, alıcı kimliği veya protokol sürümü)additionalDataaracılığıyla kimlik doğrulama etiketine bağlayın. Böylece, şifreli metin veya meta verilerde değişiklik yapılmasıdecrypt()'ninOperationErrorile 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()öğesinicrypto.subtleüzerinde değil,SubtleCryptooluş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 iseiv(ö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 veyafalsedö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);
}