Map Elixir encryption and decryption logic to PHP and vice versa

Viewed 166

I'm trying to write logic with Elixir and PHP to encrypt and decrypt text. My goal is to decrypt data which is encrypted in Elixir and vice versa. Both algorithms works fine, but the only problem I'm encountering is separating Initialization Vector from cipher text and conversion of binary to string in PHP and also decrypted text in Elixir.

Elixir Implementation:

    defmodule Crypto.AES do
    @block_size 16
    @secret_key "something secret here"

    def encrypt(text) do
        secret_key_hash = make_hash(@secret_key, 32)

        IO.inspect secret_key_hash

        # create random Initialisation Vector
        iv = :crypto.strong_rand_bytes(@block_size)

        text = pad_pkcs7(text, @block_size)
        encrypted_text = :crypto.crypto_one_time(:aes_256_cbc, secret_key_hash, iv, text, true )
        
        encrypted_text = ( iv <> <<"::">> <>  encrypted_text )
        
        Base.encode64(encrypted_text)
    end

    def decrypt(ciphertext) do
        secret_key_hash = make_hash(@secret_key, 32)

        {:ok, ciphertext} = Base.decode64(ciphertext)
        <<iv::binary-16, rp::binary-2, ciphertext::binary>> = ciphertext
        decrypted_text = :crypto.crypto_one_time(:aes_256_cbc, secret_key_hash, iv, ciphertext, false)

        unpad_pkcs7(decrypted_text)
    end
    
    @doc """
    Pad the `message` by extending it to the nearest `blocksize` boundary,
    appending the number of bytes of padding to the end of the block.
    If the original `message` is a multiple of `blocksize`, an additional block
    of bytes with value `blocksize` is added.
    """

    def pad_pkcs7(message, blocksize) do
        pad = blocksize - rem(byte_size(message), blocksize)
        message <> to_string(List.duplicate(pad, pad))
    end

    @doc """
    Remove the PKCS#7 padding from the end of `data`.
    """
    def unpad_pkcs7(data) do
        <<pad>> = binary_part(data, byte_size(data), -1)
        binary_part(data, 0, byte_size(data) - pad)
    end

    def make_hash(text, length) do
      :crypto.hash(:sha512, text)
        |> Base.encode16
        |> String.downcase
        |> String.slice(0, length)
    end

end

PHP Implementation:

    <?php
    $ENCRYPTION_KEY = 'something secret here';
    $ENCRYPTION_ALGORITHM = 'AES-256-CBC';

    function encrypt($plain_text) {
        global $ENCRYPTION_KEY;
        global $ENCRYPTION_ALGORITHM;
        $EncryptionKey = make_hash($ENCRYPTION_KEY, 32);

        // create random Initialisation Vector
        $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($ENCRYPTION_ALGORITHM));

        $encrypted_text = openssl_encrypt(
            $plain_text,
            $ENCRYPTION_ALGORITHM,
            $EncryptionKey,
            0,
            $iv
        );

        return  base64_encode($encrypted_text . '::' . $iv);
    }

    function decrypt($ciphertext) {
        global $ENCRYPTION_KEY;
        global $ENCRYPTION_ALGORITHM;
        $EncryptionKey = make_hash($ENCRYPTION_KEY, 32);

        $split = explode('::', base64_decode($ciphertext), 2);
        list($iv, $encrypted_text) = array_pad($split, 2, null);

        $plain_text = openssl_decrypt(
            $encrypted_text,
            $ENCRYPTION_ALGORITHM,
            $EncryptionKey,
            0,
            $iv
        );

        return $plain_text;
    }

    function make_hash($text, $length) {
        $hash_key = hash("sha512", $text, false);
        return substr($hash_key,0,$length);
    }

    $ct = encrypt("hello");
    // echo $ct."\n";
    echo decrypt("Sr4nMnMdDHhUQcnW6RwZ2Do6rhBh/ytW1W/x7Xx2/Xrv3A==")."\n";
?>

Please suggest some possible solutions. Thanks

2 Answers

The two codes are incompatible for two reasons:

  • The PHP code returns the ciphertext Base64 encoded by default during encryption. This must be disabled because the Base64 encoding is performed explicitly after the concatenation of IV and ciphertext. Disabling is possible by passing OPENSSL_RAW_DATA as 4th parameter in openssl_encrypt(). The same applies for decryption:

    $encrypted_text = openssl_encrypt(
        $plain_text,
        $ENCRYPTION_ALGORITHM,
        $EncryptionKey,
        OPENSSL_RAW_DATA, // Fix: Don't Base64 encode the ciphertext
        $iv
    );
    
    ...
    
    $plain_text = openssl_decrypt(
        $encrted_text,
        $ENCRYPTION_ALGORITHM,
        $EncryptionKey,
        OPENSSL_RAW_DATA, // Fix: Don't Base64 decode the ciphertext
        $iv
    );
    
  • As already noted in the comment, both codes assume a different order of IV and ciphertext when separating during decryption. Menwhile you have adapted the separation during decryption in the PHP code to that of the Elixir code.
    But also for encryption, both codes use a different order of IV and ciphertext. The modification of the PHP code concerning the encryption is still missing:

    ...
    return  base64_encode($iv . '::' . $encrypted_text); // Reverse order
    

With these changes, the two codes are functionally identical and a ciphertext generated with the PHP code can be decrypted with the Elixir code and vice versa.

A note regarding concatenation: Both the IV and ciphertext can contain the separator :: with a certain probability, which can cause problems. It would be better to concatenate the data without a separator and use the known length of the IV for the separation.

Also, using a hash function for key derivation is insecure, better apply a reliable key derivation function like PBKDF2. Furthermore, taking the hex encoded key instead of the binary data reduces security.

With suggestions (except PBKDF2, will add later) from @Topaco, here's complete solution.

Elixir - git-link

defmodule Crypto.AES do
    @block_size 16
    @secret_key "put something secret here"

    def encrypt(plain_text) do
        secret_key_hash = make_hash(@secret_key, 32)

        # create Initialisation Vector
        iv = :crypto.strong_rand_bytes(@block_size)

        padded_text = pad_pkcs7(plain_text, @block_size)
        encrypted_text = :crypto.crypto_one_time(:aes_256_cbc, secret_key_hash, iv, padded_text, true )
        
        # concatenate IV for decryption
        encrypted_text = ( iv <>  encrypted_text )
        
        Base.encode64(encrypted_text)
    end

    def decrypt(ciphertext) do
        secret_key_hash = make_hash(@secret_key, 32)

        {:ok, ciphertext} = Base.decode64(ciphertext)
        <<iv::binary-16, ciphertext::binary>> = ciphertext
        decrypted_text = :crypto.crypto_one_time(:aes_256_cbc, secret_key_hash, iv, ciphertext, false)

        unpad_pkcs7(decrypted_text)
    end
    
    defp pad_pkcs7(message, blocksize) do
        pad = blocksize - rem(byte_size(message), blocksize)
        message <> to_string(List.duplicate(pad, pad))
    end

    defp unpad_pkcs7(data) do
        <<pad>> = binary_part(data, byte_size(data), -1)
        binary_part(data, 0, byte_size(data) - pad)
    end

    defp make_hash(text, length) do
      :crypto.hash(:sha512, text)
        |> Base.encode16
        |> String.downcase
        |> String.slice(0, length)
    end

end

PHP - gist-link

<?php
    $ENCRYPTION_KEY = "put something secret here";
    $ENCRYPTION_ALGORITHM = 'AES-256-CBC';

    function encrypt($plain_text) {
        global $ENCRYPTION_KEY;
        global $ENCRYPTION_ALGORITHM;
        $EncryptionKey = make_hash($ENCRYPTION_KEY, 32);

        // create random Initialization Vector
        $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($ENCRYPTION_ALGORITHM));

        $encrypted_text = openssl_encrypt(
            $plain_text,
            $ENCRYPTION_ALGORITHM,
            $EncryptionKey,
            OPENSSL_RAW_DATA,
            $iv 
        );

        # concatenate the IV for decryption
        return  base64_encode($iv . $encrypted_text);
    }

    function decrypt($ciphertext) {
        global $ENCRYPTION_KEY;
        global $ENCRYPTION_ALGORITHM;
        $EncryptionKey = make_hash($ENCRYPTION_KEY, 32);

        $ciphertext = base64_decode($ciphertext);
        // get Initialization Vector part (16 bytes long)
        $iv = substr($ciphertext, 0, 16);
        // rest is actual cipher text
        $ciphertext = substr($ciphertext, 16);

        $decrypted_text = openssl_decrypt(
            $ciphertext,
            $ENCRYPTION_ALGORITHM,
            $EncryptionKey,
            OPENSSL_RAW_DATA,
            $iv
        );

        return $decrypted_text;
    }

    function make_hash($text, $length) {
        $hash_key = hash("sha512", $text, false);
        return substr($hash_key,0,$length);
    }

    // $ct = encrypt("code");
    // $dt = decrypt($ct);
    // echo $ct."\n";
    // echo $dt."\n";
?>
Related