phpsess/sodium-encryption

View on GitHub
src/SodiumEncryption.php

Summary

Maintainability
A
1 hr
Test Coverage
<?php

declare(strict_types=1);

namespace PHPSess\Encryption;

use PHPSess\Exception\BadSessionContentException;
use PHPSess\Exception\UnableToEncryptException;
use PHPSess\Interfaces\EncryptionInterface;
use PHPSess\Exception\UnableToDecryptException;
use Exception;
use stdClass;

class SodiumEncryption implements EncryptionInterface
{

    /**
     * @var string $appKey The hashed app key.
     */
    private $appKey;

    /**
     * SodiumEncryption constructor.
     *
     * @param  string $appKey              Defines the App Key.
     */
    public function __construct(string $appKey)
    {
        $binaryHash = sodium_crypto_generichash($appKey);
        $this->appKey = sodium_bin2hex($binaryHash);
    }

    /**
     * Makes a session identifier based on the session id.
     *
     * @param  string $sessionId The session id.
     * @return string The session identifier.
     */
    public function makeSessionIdentifier(string $sessionId): string
    {
        $hashContent = $sessionId . $this->appKey;
        $binaryHash = sodium_crypto_generichash($hashContent);
        return sodium_bin2hex($binaryHash);
    }

    /**
     * Encrypts the session data.
     *
     * @throws Exception
     * @param  string $sessionId   The session id.
     * @param  string $sessionData The session data.
     * @return string The encrypted session data.
     */
    public function encryptSessionData(string $sessionId, string $sessionData): string
    {
        $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);

        $encryptionKey = $this->getEncryptionKey($sessionId);

        $encryptedBinary = sodium_crypto_secretbox($sessionData, $nonce, $encryptionKey);

        return (string) json_encode([
            'data' => sodium_bin2hex($encryptedBinary),
            'nonce' => sodium_bin2hex($nonce)
        ]);
    }

    /**
     * Decrypts the session data.
     *
     * @throws UnableToDecryptException
     * @throws BadSessionContentException
     * @param  string $sessionId   The session id.
     * @param  string $sessionData The encrypted session data.
     * @return string The decrypted session data.
     */
    public function decryptSessionData(string $sessionId, string $sessionData): string
    {
        if (!$sessionData) {
            return '';
        }

        $data = $this->parseEncryptedData($sessionData);

        $encryptionKey = $this->getEncryptionKey($sessionId);

        $decryptedData = sodium_crypto_secretbox_open($data->data, $data->nonce, $encryptionKey);

        if ($decryptedData === false) {
            $errorMessage = 'Could not decrypt the session data. Was it tampered or just corrupted?';
            throw new UnableToDecryptException($errorMessage);
        }

        return $decryptedData;
    }

    /**
     * Calculates the key to be used in the session encryption.
     *
     * @param  string $sessionId Id of the session
     * @return string Encryption key
     */
    private function getEncryptionKey(string $sessionId): string
    {
        $hashContent = $this->appKey . $sessionId;
        $binaryHash = sodium_crypto_generichash($hashContent);
        $hash = sodium_bin2hex($binaryHash);
        return substr($hash, 0, SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
    }

    /**
     * @todo Create a proper class instead of using stdClass
     * @throws BadSessionContentException
     * @param string $data
     * @return stdClass
     */
    private function parseEncryptedData(string $data): stdClass
    {
        $data = json_decode($data);

        if (json_last_error() !== JSON_ERROR_NONE) {
            $errorMessage = 'The session content is not parsable as JSON.';
            throw new BadSessionContentException($errorMessage);
        }

        if (empty($data->nonce)) {
            $errorMessage = 'The session content has no "nonce".';
            throw new BadSessionContentException($errorMessage);
        }

        if (!isset($data->data)) {
            $errorMessage = 'The session content has no "data" field.';
            throw new BadSessionContentException($errorMessage);
        }

        try {
            $nonce = sodium_hex2bin($data->nonce);
        } catch (Exception $exception) {
            $errorMessage = 'The nonce could not be converted from hexadecimal to binary.';
            throw new BadSessionContentException($errorMessage);
        }

        try {
            $data = sodium_hex2bin($data->data);
        } catch (Exception $exception) {
            $errorMessage = 'The data could not be converted from hexadecimal to binary.';
            throw new BadSessionContentException($errorMessage);
        }

        $session = new stdClass();
        $session->data = $data;
        $session->nonce = $nonce;

        return $session;
    }
}