the-kbA-team/data-protection

View on GitHub
src/SecureSearch.php

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
<?php

namespace kbATeam\DataProtection;

use RuntimeException;

/**
 * Class kbATeam\DataProtection\SecureSearch
 *
 * Deterministic one-way encryption of unique sensitive data.
 * See README.md for details!
 *
 * The code surrounded by codeCoverageIgnore tags is not supposed to be reached,
 * except when it happens (e.g. in the unlikely event openssl doesn't know the cipher
 * being used) you'd want to know what happened where and to stop and further
 * execution.
 *
 * @category library
 * @package  kbATeam\DataProtection
 * @license  MIT
 */
class SecureSearch
{
    /**
     * @const Encryption method used to encrypt the data.
     *        Only CBC based ciphers are applicable here! See README.md for details.
     */
    const CIPHER = 'AES-256-CBC';

    /**
     * @const Hash method used for the IV derivation.
     */
    const HASH = 'SHA256';

    /**
     * @const Number of iterations for the Password-Based Key Derivation Function.
     */
    const PBKDF2_ITERATIONS = 64000;

    /**
     * @const Length of the key generated.
     */
    const KEY_LENGTH = 32;

    /**
     * One-way encryption of unique sensitive data.
     *
     * @param string $data The data to encrypt.
     * @param string $key  The key to use for encryption.
     * @return string The base64 encoded encrypted data.
     * @throws \RuntimeException in case the determination of the IV length,
     *              the IV derivation from the data, or the encryption fails.
     */
    public static function encrypt($data, $key): string
    {
        /**
         * Determine the length of the initialization vector for the given cipher.
         */
        $iv_length = openssl_cipher_iv_length(self::CIPHER);
        /**
         * Throw exception in case the IV length cannot be determined.
         * Are you messing with the cipher used?
         */
        if (false === $iv_length) {
            //@codeCoverageIgnoreStart
            throw new RuntimeException('error determining IV length for cipher');
            //@codeCoverageIgnoreEnd
        }
        /**
         * Use the hash of the key as salt for the IV creation.
         * This can be anything as long as it's deterministic.
         */
        $salt = hash(self::HASH, $key);

        /**
         * Derive the initialization vector from the given data.
         * Attention: binary content.
         */
        $iv = openssl_pbkdf2($data, $salt, $iv_length, self::PBKDF2_ITERATIONS, self::HASH);
        /**
         * Throw exception in case the IV derivation from the plain text failed.
         */
        if (false === $iv) {
            //@codeCoverageIgnoreStart
            throw new RuntimeException('IV derivation from data failed');
            //@codeCoverageIgnoreEnd
        }
        /**
         * Encrypt the data.
         * Options "0" means, that the result is base64 encoded.
         */
        $result = openssl_encrypt($data, self::CIPHER, $key, 0, $iv);
        if (false === $result) {
            //@codeCoverageIgnoreStart
            throw new RuntimeException('encryption of data failed');
            //@codeCoverageIgnoreEnd
        }
        /**
         * Make sure the contents of these variables isn't accessible anymore because
         * they contain sensitive information that can compromise the security of the
         * encryption.
         */
        $key_hash = $iv_length = $iv = '';
        unset($key_hash, $iv_length, $iv);

        return $result;
    }

    /**
     * Generate a key for encryption.
     *
     * @return string Hexadecimal representation of the generated key.
     * @throws \RuntimeException in case the key length is too weak or the key
     *                           generation failed.
     */
    public static function generateKey(): string
    {
        /**
         * Generate a secret key based on the defined key length.
         */
        $key_raw = openssl_random_pseudo_bytes(self::KEY_LENGTH, $is_strong);
        /**
         * Throw exception in case the random key generation failed.
         */
        if (true !== $is_strong || false == $key_raw) {
            //@codeCoverageIgnoreStart
            throw new RuntimeException('key generation failed');
            //@codeCoverageIgnoreEnd
        }
        return bin2hex($key_raw);
    }
}