phpencryptionjwtopenssldecode

Custom encryption approach in PHP (like JWT) – is this secure enough


I'm trying to build a custom token system in PHP that works similarly to JWT. The idea is to encode and encrypt some user data, then send it to the client as a token. Here's the simplified example of what I've done so far:

<?php
function generateCustomToken($data, $secretKey) {
    $payload = json_encode($data);
    $encrypted = openssl_encrypt($payload, 'AES-256-CBC', $secretKey, 0, $secretKey);
    $signature = hash_hmac('sha256', $encrypted, $secretKey);

    return base64_encode($encrypted . '.' . $signature);
}

function decodeCustomToken($token, $secretKey) {
    $decoded = base64_decode($token);
    list($encryptedData, $signature) = explode('.', $decoded);

    $calculatedSig = hash_hmac('sha256', $encryptedData, $secretKey);
    if (!hash_equals($calculatedSig, $signature)) {
        return null; // invalid token
    }

    $payload = openssl_decrypt($encryptedData, 'AES-256-CBC', $secretKey, 0, $secretKey);
    return json_decode($payload, true);
}
?>

My main concern is security. I want to prevent attackers from tampering with the token or extracting sensitive information from it. Are there any obvious flaws in this approach? Should I be handling the IV differently or using a completely different method? Also, are there standard library functions or well-known packages I can use instead of reinventing the wheel? Any guidance would be appreciated


Solution

  • Your approach essentially corresponds to the encrypt-then-MAC scheme.

    However, you should not use the key as IV, but a random IV for each encryption. The IV is not secret, is usually concatenated with the ciphertext (IV|ciphertext|MAC) (and is also to be MACed).

    Ideally, different keys should be applied for encryption and authentication (see here).

    A possible implementation based on your code that takes this into account is:

    <?php
    function generateCustomToken($data, $keys) { // $keys: two concatenated 32 bytes keys
        $encKey = substr($keys, 0, 32);
        $macKey = substr($keys, 32);
        $payload = json_encode($data);
        $iv = openssl_random_pseudo_bytes(16);
        $ciphertext = openssl_encrypt($payload, 'AES-256-CBC', $encKey, OPENSSL_RAW_DATA, $iv);
        $ivCiphertext = $iv . $ciphertext;
        $mac = hash_hmac('sha256', $ivCiphertext, $macKey, true);
        return base64_encode($ivCiphertext . $mac);
    }
    
    function decodeCustomToken($token, $keys) {  // $keys: two concatenated 32 bytes keys
        $encKey = substr($keys, 0, 32);
        $macKey = substr($keys, 32);
        $ivCiphertextMac = base64_decode($token);
        $iv = substr($ivCiphertextMac, 0, 16);
        $ciphertext = substr($ivCiphertextMac, 16, -32);
        $mac = substr($ivCiphertextMac, -32);
        $calculatedMac = hash_hmac('sha256', $iv . $ciphertext, $macKey, true);
        if (!hash_equals($calculatedMac, $mac)) {
            return null; 
        }
        $payload = openssl_decrypt($ciphertext, 'AES-256-CBC', $encKey, OPENSSL_RAW_DATA, $iv);
        return json_decode($payload, true);
    }
    
    // Sample use case
    $data = array('key1' => 'vlaue1', 'key2' => 'vlaue2');
    $keys = hex2bin('cd418f373f7ba9fd203b5a9771864eb962e63998f2d4df48bdac714d07e6958faf92f94dc37e33035646c1d0a8c33a6f82999f04106e2aac268b2506d4323135');
    $encryptedData = generateCustomToken($data, $keys);
    print($encryptedData . PHP_EOL);
    $decryptedData = decodeCustomToken($encryptedData, $keys);
    print_r($decryptedData);
    ?>
    

    An alternative is JWE. JWE generates a token which (in compact serialization) consists of 5 parts separated by dots: The header (with information on the algorithms used), the encrypted key, the IV, the ciphertext and the authentication tag.
    As the key is not to be encrypted in your case, direct JWE is to be applied. In this case, the second part is an empty string.

    There are various libraries for JWE, e.g. the jwt-framework. Sample implementation for direct JWE (with AES-256 and GCM):

    <?php
        require __DIR__ . '/vendor/autoload.php';
        
        use Jose\Component\Core\AlgorithmManager;
        use Jose\Component\Encryption\Algorithm\KeyEncryption\Dir;
        use Jose\Component\Encryption\Algorithm\ContentEncryption\A256GCM;
        use Jose\Component\Core\JWK;
        use Jose\Component\Encryption\Serializer\CompactSerializer;
        use Jose\Component\Encryption\JWEBuilder;
        use Jose\Component\Encryption\JWEDecrypter;
    
        $keyEncryptionAlgorithmManager = new AlgorithmManager([
            new Dir(),
        ]);
    
        $contentEncryptionAlgorithmManager = new AlgorithmManager([
            new A256GCM(),
        ]);
    
        $jwk = new JWK([
            'kty' => 'oct',
            'k' => 'MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE',
        ]);
    
        $serializer = new CompactSerializer(); 
    
        // Encryption
        $jweBuilder = new JWEBuilder(
            $keyEncryptionAlgorithmManager,
            $contentEncryptionAlgorithmManager
        );
        
        $payload = "some payload";
    
        $jwe = $jweBuilder
            ->create()              
            ->withPayload($payload) 
            ->withSharedProtectedHeader([
                'alg' => 'dir',        
                'enc' => 'A256GCM' 
            ]) 
            ->addRecipient($jwk)   
            ->build();
    
        $token = $serializer->serialize($jwe, 0);
        print("Token: " . $token . PHP_EOL);
    
        // Decryption
        $jweDecrypter = new JWEDecrypter(
            $keyEncryptionAlgorithmManager,
            $contentEncryptionAlgorithmManager
        );
    
        $jwe = $serializer->unserialize($token);
    
        $jweDecrypter->decryptUsingKey($jwe, $jwk, 0);
        $payload = $jwe->getPayload();
        print("Payload: " . $payload . PHP_EOL);    
    ?>
    

    Sample output:

    Token: eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..RVoWvebzuMIpcuTq.VfLcctgHZthdvHda.JjGEESVSAcbZxo6IgdK_qw 
    Payload: some payload
    

    JWE is quite powerful and supports many algorithms, e.g. in addition to A256GCM used above, also algorithms as A128CBC-HS256 and A256CBC-HS512, which apply the primitives you are using (see here).