Decrypt PHP ChaChaPoly encrypted data in Swift

257 views Asked by At

I am trying to decrypt data with Swift CryptoKit that I have encrypted with php using openssl_encrypt() chacha20-poly1305. Encrypting works, but when decrypting I get the error: incorrectParameterSize (Swift: error 1, line 10).

PHP encryption using openssl_encrypt:

<?php
$plaintext = "Hello World";
$key = base64_decode("O7IaTssF6RKgc84b8daAHojiveFY4vyQ4zKRZv3APKc=");
$nonce = base64_decode("MmTNTi+MmTNTi+fB");
$encrypted = 
openssl_encrypt(
    $plaintext,
    'chacha20-poly1305',
    $key,
    0,
    $nonce
);

echo $encrypted;

Swift decryption using CryptoKit

import CryptoKit
import Foundation
let encoded_data = "OG54KzRFKytSSmQ5d0M4PQ=="
let encoded_nonce = "MmTNTi+MmTNTi+fB"
let encoded_key = "O7IaTssF6RKgc84b8daAHojiveFY4vyQ4zKRZv3APKc="
let data = Data(base64Encoded: encoded_data)!
let nonce = Data(base64Encoded: encoded_nonce)!
let key = SymmetricKey(data: Data(base64Encoded: encoded_key)!)
do{
    let box = try ChaChaPoly.SealedBox(combined: data.base64EncodedData())
    do{
        let decryptedData = try ChaChaPoly.open(box,using: key)
        let decryptedString = String(decoding: decryptedData, as: UTF8.self)
        print("Decrypted string: \(decryptedString)")
    }catch{
        print("error 2: \(error)")
    }
}catch{
    print("error 1: \(error)")
}

I also tried using sodium_crypto_aead_chacha20poly1305_encrypt() to encrypt, which resulted in the error "authenticationFailure" (Swift: error 2, line 12).

PHP encryption using sodium_crypto_aead_chacha20poly1305_encrypt:

<?php

$plaintext = "Hello World";
$key = base64_decode("O7IaTssF6RKgc84b8daAHojiveFY4vyQ4zKRZv3APKc=");
$nonce = random_bytes(SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_NPUBBYTES);
$encrypted = sodium_crypto_aead_chacha20poly1305_encrypt($plaintext,"",$nonce, $key);

echo "Data:" . base64_encode($encrypted) . "\n";
echo "Nonce:" . base64_encode($nonce);
2

There are 2 answers

0
Topaco On

ChaCha20-Poly1305 is authenticated encryption, i.e. during encryption a tag is generated in addition to the ciphertext, which is required for authentication during decryption.
The PHP/OpenSSL code does not generate this tag due to a bug, which is therefore missing when decrypting with the Swift code, so decryption fails (PHP/OpenSSL internally decryption works). More details about the bug are described in #76935.


PHP/Sodium, on the other hand, works:

$plaintext = 'secret message';
$key = base64_decode('XohImNooBHFR0OVvjcYpJ3NgPQ1qq73WKhHvch0VQtg=');
$nonce = base64_decode('F6u88lkmmz8Rrjvf'); // for testing only, otherwise: random_bytes(SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES);
$nonceCtTag = $nonce . sodium_crypto_aead_chacha20poly1305_ietf_encrypt($plaintext, '', $nonce, $key);
print(base64_encode($nonceCtTag) . PHP_EOL); // F6u88lkmmz8RrjvfJ22r4vrKmWuY8a2DNpjUKZVUi9Wp9QjZVgheBihn

The test data is from here, i.e. was generated with CryptoKit's ChaCha20-Poly1305, so decryption with your Swift code should give the original plaintext (the key corresponds to the SHA256 hash of password).

Note that sodium_crypto_aead_chacha20poly1305_ietf_encrypt() returns as result the concatenation of ciphertext and tag, so that $nonceCtTag corresponds to the combined representation (nonce|ciphertext|tag) to be passed in SealedBox(combined: ...).


The code above uses sodium_crypto_aead_chacha20poly1305_ietf_encrypt() with a 12 bytes nonce instead of sodium_crypto_aead_chacha20poly1305_encrypt() with an 8 bytes nonce (see here for more details regarding the different variants).

1
memo On

In a real-world scenario, you would need to validate the data, nonce, and key before attempting to decrypt the data to ensure that they have not been modified or corrupted.

import CryptoKit
import Foundation

let encodedData = "OG54KzRFKytSSmQ5d0M4PQ=="
let encodedNonce = "MmTNTi+MmTNTi+fB"
let encodedKey = "O7IaTssF6RKgc84b8daAHojiveFY4vyQ4zKRZv3APKc="

guard let data = Data(base64Encoded: encodedData),
      let nonce = Data(base64Encoded: encodedNonce),
      let key = SymmetricKey(data: Data(base64Encoded: encodedKey)) else {
    print("Invalid data, nonce, or key")
    return
}

do {
    let box = try ChaChaPoly.SealedBox(combined: data)
    let decryptedData = try ChaChaPoly.open(box, using: key)
    let decryptedString = String(decoding: decryptedData, as: UTF8.self)
    print("Decrypted string: \(decryptedString)")
} catch {
    print("Decryption failed: \(error)")
}