Using only php to decrypt zend-crypt hybrid


#1

I’m developing an API that to return sensitive data will require a sort of handshake, which will return a public key for the client to encrypt the information before sending it to the API.

For the API I am using expressive zend and to perform the handshake, I am using the zend-crypt module, more specifically Hybrid encryption.

the workflow is more or less the following:

The client application sends a public key to the api.
The API uses this public key to encrypt, using hybrid encryption, the public key provided by the server.
The public key of the encrypted server is returned to the client
The client must decrypt the key to be able to use it for cryptography of sensitive data.

Below is the source code that performs public key cryptography:

$publicKeyStr = $request->getHeader('hello');
        
try {
    $publicKey = new \Zend\Crypt\PublicKey\Rsa\PublicKey(base64_decode($publicKeyStr[0]));
        
    $b64Key = base64_encode($this->crypto->getPublicKey());
        
    $hybridCrypt = new \Zend\Crypt\Hybrid();
        
    $serverPk = $hybridCrypt->encrypt($b64Key, $publicKey);   
        
    return new \Zend\Diactoros\Response\JsonResponse(array('hello'=>$serverPk));
} catch (\Zend\Crypt\PublicKey\Rsa\Exception\RuntimeException $ex) {
    return new \Zend\Diactoros\Response\EmptyResponse(\Fig\Http\Message\StatusCodeInterface::STATUS_UNAUTHORIZED);
}

My question is this:
How do I decrypt this public key on a client that does not use the framework, pure PHP only, for example?

Since this API can be used for mobile application development or desktop software, I need to understand the logic to decrypt to be able to perform tests on different platforms.

I am currently performing integration testing of this middleware using Codeception, and the test code is this:

public function testPublicKeyAccess(ApiTester $I)
{
    $I->wantTo('Test if API return a valid public key');

    $I->am('Client');
    $I->amBearerAuthenticated($this->token);
    $I->haveHttpHeader('hello', base64_encode($this->publicKey));
    $I->sendGET('/handshake');
    
    
    $I->seeResponseCodeIs(200);
    $I->seeResponseContainsJson();

    $response = json_decode($I->grabResponse());
    
    list($encryptedKeys, $msg) = explode(';', $response->hello);
    $keysArr = explode(':', $encryptedKeys);
    $encryptedKey = base64_decode($keysArr[1]);
    
    // Decrypt $envKey, this works
    
    $envKey = '';
    openssl_private_decrypt($encryptedKey, $envKey,  $this->privateKey, OPENSSL_PKCS1_OAEP_PADDING);
     
    // Decrypt public key, this don't works
    $cipher = 'aes-256-cbc';
    $hmacSize = 46;
    $hmac = mb_substr($msg, 0, $hmacSize, '8bit');
    
    $ivSize = 32; //openssl_cipher_iv_length($cipher);
    $iv = mb_substr($msg, $hmacSize, $ivSize, '8bit'); // 64
    
    $cipherText = base64_decode(mb_substr($msg, $hmacSize+$ivSize, null, '8bit'));
    
    $serverKey = '';
    
    codecept_debug(var_dump($ivSize));
    
    openssl_open($cipherText, $serverKey, $envKey, $this->privateKey, $cipher, $ivSize);
    codecept_debug(var_dump($cipherText));
    codecept_debug(var_dump($serverKey));
    
    $I->assertRegExp('/-----BEGIN PUBLIC KEY-----(.*)-----END PUBLIC KEY-----/s', $serverKey);
}

#2

Hi @RodriAndreotti I don’t understand why you are encrypting the server public key with this handshake protocol. The public keys are public by definition, that means everyone can use it. If you encrypt something with it, only the owner with the correct private key can decrypt it.
That said, if you want to understand how Zend\Crypt\Hybrid works you can have a look at the source code here.

The default crypto engine used by Zend\Crypt is OpenSSL. The Hybrid algorithm basically generates a random session key (here) and use it to encrypt a message using a blockcipher algorithm in encrypt-then-authenticate mode (the default algorithms are: AES algorithm, CBC mode, HMAC with SHA256, PKCS#7 padding.).
The output of hybrid encryption is:

return $encKeys . ';' . $ciphertext;

where $ciphertext is:

$ciphertext = $hmac . base64_encode($ciphertext2);

and $ciphertext2 is:

$ciphertext2 = $iv . $result;

where $result is openssl_encrypt() output (see here).


#3

Hello, @enrico !
How are you?
I’ve been thinking the same between yesterday and today, I think in the security concern I ended up exaggerating a bit, since only app clients that are authenticated via Bearer Token will have access to this handshake.

Thank you for your help!