Skip to content

The difference of AES Algorithm between PHP and Python Translation

Backend

2024-11-16

Warning

The content was translated from the Chinese version by Generative AI. Please double-check the content.

Background

When using the DogeCloud Video SDK, the result obtained by calculating the signature using Python as per the documentation throws an error: "playToken decryption failed...".

The official documentation only provides PHP sample code. Since I don't know PHP (in fact, even someone who knows PHP would hardly figure out this issue), I spent 4 hours configuring the environment and testing the PHP code. Eventually, after scrolling through page after page of Google results for the abnormal behavior, I finally came across the answer from a British expert attached later.

This expert has been registered on Stack Overflow (the world's largest programming Q&A platform) for over 15 years, and their main focus is Rust. It's truly impressive that they客串ed in the Python and PHP arena to help me. If you have the means, go give that answer an upvote.

Python Code

Similar results were generated by GPT-4o and Claude 3.5 Sonnet respectively. Below is a version that most closely matches the format and comments of DogeCloud's official PHP example:

import base64
import hmac
import json
import time
from hashlib import sha1

# Need to install: pip install pycryptodome
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes

# Define secret key and vcode
SecretKey = 'MY_SECRET_KEY'.encode()
vcode = '70804c544d067f03'


def generate_playtoken():

    # Generate playback policy
    myPolicy = json.dumps({
        "e": int(time.time()) + 120,
        "v": vcode,
        "full": True
    })

    # Generate random IV
    iv = get_random_bytes(16)

    # Encrypt (using pycryptodome library)
    cipher = AES.new(SecretKey, AES.MODE_CFB, iv, segment_size=128)
    encrypted_data = cipher.encrypt(myPolicy.encode())

    # Base64 encode the first segment
    encoded_data = base64.b64encode(encrypted_data).decode()

    # Base64 encode IV (second segment)
    encoded_iv = base64.b64encode(iv).decode()

    # Compute HMAC-SHA1 and Base64 encode (third segment)
    hashed_data = base64.b64encode(
        hmac.new(SecretKey, myPolicy.encode(), sha1).digest()).decode()

    # Concatenate to get the final playback token
    playToken = f"{encoded_data}:{encoded_iv}:{hashed_data}"

    # Replace special characters
    playToken = playToken.replace("+", "-").replace("/", "_")

    return playToken


generate_playtoken()

The segment_size=128 on line 29 is a parameter that isn't commonly used but is necessary here to maintain consistency with PHP.

Reference Materials

This is the only material I found online that is truly relevant to my issue: https://stackoverflow.com/a/46354693/16145283

The original text is attached below.

To use CFB mode you need to specify a segment size for it. OpenSSL has aes-256-cfb (which is 128 bit), aes-256-cfb1 (i.e. 1-bit) and aes-256-cfb8 (8 bit) (and similar modes for AES-128 and 192). So you are using 128 bit cfb in your php code.

The Python library accepts a segment_size argument to AES.new, but the default is 8, so you are using different modes in the two versions.

To get the Python code to decrypt the output of the PHP code, add a segment size of 128 to the cipher object:

cipher = AES.new(encryption_key, AES.MODE_CFB, iv, segment_size=128)

(N.B. this is using the newer PyCryptodome fork of PyCrypto. PyCrypto has a bug here and won’t work.)

Alternatively, you can get the PHP code to use CFB-8 by setting the cipher (don’t change both, obviously):

$encrypted = openssl_encrypt($data, 'aes-256-cfb8', $encryption_key, 1, $iv);

Personal Opinion

PHP is quite outdated in 2024. Documentation that can’t even write clear pseudocode and only provides PHP examples should really improve...

Ukraine Firmly stand with Ukraine against Russia's brutal invasion.