如何在JavaScript中解密Swift CryptoKit ChaCha20-Poly1305密文并实现跨平台加解密互操作?
Let's break down how to solve both your interop problems between Swift's CryptoKit ChaChaPoly and JavaScript's Web Crypto API (which natively supports ChaCha20-Poly1305, so no third-party libraries needed!).
First: Understand CryptoKit's SealedBox Structure
The key mistake in your initial JS code is trying to split the base64 string directly by character length. CryptoKit's SealedBox.combined is a binary blob structured as:
- 12-byte nonce (ChaCha20-Poly1305 standard nonce size)
- Ciphertext (variable length)
- 16-byte authentication tag (Poly1305 standard tag size)
Base64 encodes bytes into characters at a 4:3 ratio (with padding), so splitting the base64 string won't align with the byte boundaries you need. You must first decode the base64 string into a binary Uint8Array before splitting.
Problem 1: Decrypt CryptoKit's SealedBox in JavaScript
Here's a complete working decrypt function using the Web Crypto API:
async function decryptFromCryptoKit(b64SealedBox, password) { // Step 1: Decode base64 to binary array const combinedBuffer = Uint8Array.from(atob(b64SealedBox), c => c.charCodeAt(0)); // Step 2: Split into nonce, ciphertext+tag const nonce = combinedBuffer.slice(0, 12); // First 12 bytes = nonce const ciphertextWithTag = combinedBuffer.slice(12); // Remaining bytes = ciphertext + tag // Step 3: Derive key (match Swift's SHA256 hash of password) const passwordBytes = new TextEncoder().encode(password); const hashBuffer = await crypto.subtle.digest('SHA-256', passwordBytes); const key = await crypto.subtle.importKey( 'raw', hashBuffer, { name: 'CHACHA20-POLY1305' }, false, ['decrypt'] ); // Step 4: Decrypt const plaintextBuffer = await crypto.subtle.decrypt( { name: 'CHACHA20-POLY1305', nonce: nonce }, key, ciphertextWithTag ); // Convert buffer to string return new TextDecoder().decode(plaintextBuffer); } // Usage example: // decryptFromCryptoKit("F6u88lkmmz8RrjvfJ22r4vrKmWuY8a2DNpjUKZVUi9Wp9QjZVgheBihn", "password") // .then(plaintext => console.log(plaintext)); // Should log "secret message"
Problem 2: Encrypt in JavaScript for Swift's CryptoKit to Decrypt
To generate a SealedBox-compatible blob in JS, we need to replicate CryptoKit's structure (nonce + ciphertext + tag). Here's the encryption function:
async function encryptForCryptoKit(plaintext, password) { // Step 1: Generate 12-byte nonce (matches CryptoKit's default) const nonce = crypto.getRandomValues(new Uint8Array(12)); // Step 2: Derive key (same SHA256 process as Swift) const passwordBytes = new TextEncoder().encode(password); const hashBuffer = await crypto.subtle.digest('SHA-256', passwordBytes); const key = await crypto.subtle.importKey( 'raw', hashBuffer, { name: 'CHACHA20-POLY1305' }, false, ['encrypt'] ); // Step 3: Encrypt (returns ciphertext + tag) const plaintextBytes = new TextEncoder().encode(plaintext); const ciphertextWithTagBuffer = await crypto.subtle.encrypt( { name: 'CHACHA20-POLY1305', nonce: nonce }, key, plaintextBytes ); // Step 4: Combine nonce + ciphertext+tag into a single buffer const combinedBuffer = new Uint8Array(nonce.length + ciphertextWithTagBuffer.byteLength); combinedBuffer.set(nonce, 0); combinedBuffer.set(new Uint8Array(ciphertextWithTagBuffer), nonce.length); // Convert to base64 return btoa(String.fromCharCode(...combinedBuffer)); } // Usage example: // encryptForCryptoKit("secret message", "password") // .then(b64 => console.log(b64)); // This string can be decrypted by your Swift code
Key Notes to Avoid Issues
- Nonce Size: Both Swift's CryptoKit and the Web Crypto API use the standard 12-byte nonce for ChaCha20-Poly1305 — don't change this size, or decryption will fail.
- Key Derivation: We're using SHA256 to hash the password into a 32-byte (256-bit) key, which matches exactly what your Swift code does with
SymmetricKey(data: SHA256.hash(data: ...)). - Binary vs Base64: Always decode base64 to binary arrays before splitting/processing — never split the base64 string directly, as byte boundaries won't align with character counts.
- Async/Await: Web Crypto API methods are asynchronous, so the functions above use
async/awaitfor readability.
内容的提问来源于stack exchange,提问作者user2779114




