You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在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

  1. 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.
  2. 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: ...)).
  3. 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.
  4. Async/Await: Web Crypto API methods are asynchronous, so the functions above use async/await for readability.

内容的提问来源于stack exchange,提问作者user2779114

火山引擎 最新活动