AthensFramework/encryption

View on GitHub
src/EncryptionBehavior.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php

namespace Athens\Encryption;

use Propel\Generator\Model\Behavior;

/**
 * Class EncryptionBehavior
 *
 * @package Athens\Encryption
 */
class EncryptionBehavior extends Behavior
{

    /** @var array */
    protected $parameters = [
        'searchable' => false
    ];

    /**
     * Multiple encrypted columns in the same table is OK.
     *
     * @return boolean
     */
    public function allowMultiple()
    {
        return true;
    }

    /**
     * @param string $script
     * @return void
     * @throws \Exception If the schema specifies encryption on fields which are not
     *                    VARBINARY.
     */
    public function tableMapFilter(&$script)
    {
        $table = $this->getTable();

        foreach ($this->getColumnNames() as $columnName) {
            $column = $table->getColumn($columnName);

            $columnIsVarbinary = $column->getType() === "VARBINARY";

            if ($columnIsVarbinary === false) {
                throw new \Exception("Encrypted columns must be of type VARBINARY. " .
                    "Encrypted column '{$column->getName()}' of type '{$column->getType()}' found. " .
                    "Revise your schema.");
            }
        }

        if (static::encryptedColumnsDeclarationExists($script) === false) {
            $insertLocation = strpos($script, ";", strpos($script, "const TABLE_NAME")) + 1;
            static::insertEncryptedColumnsDeclaration($script, $insertLocation);
            static::insertEncryptedColumnNameAccessMethods($script);
        }

        foreach ($this->getColumnRealNames() as $realColumnName) {
            static::insertEncryptedColumnName($script, $realColumnName);

            if ($this->isSearchable() === true || strtolower($this->isSearchable()) === 'true') {
                static::insertSearchableEncryptedColumnName($script, $realColumnName);
            }
        }
    }

    /**
     * @param string $script
     * @return void
     */
    public function objectFilter(&$script)
    {
        $phpColumnNames = $this->getColumnPhpNames();

        foreach ($phpColumnNames as $columnPhpName) {
            $this->addEncryptionToSetter($script, $columnPhpName);
            $this->addDecryptionToGetter($script, $columnPhpName);
        }
    }

    /**
     * @param string $script
     * @return void
     */
    public function queryFilter(&$script)
    {
        if (strpos($script, "addUsingOperator") !== false) {
            return;
        }

        $useString = <<<'EOT'

    public function addUsingOperator($p1, $value = null, $operator = null, $preferColumnCondition = true)
    {
        /** @var StudentTableMap $tableMap */
        $tableMap = $this->getTableMap();

        /** @var boolean $isCriterion */
        $isCriterion = $p1 instanceof \Propel\Runtime\ActiveQuery\Criterion\AbstractCriterion;

        /** @var string $columnName */
        $columnName = $isCriterion ? $p1->getTable()->getName() . $p1->getColumn() : $p1;

        /** @var boolean $isEncryptedColumn */
        $isEncryptedColumn = $tableMap->isEncryptedColumnName($columnName);

        /** @var boolean $isEncryptedSearchableColumn */
        $isEncryptedSearchableColumn =  $tableMap->isEncryptedSearchableColumnName($columnName);

        if ($isEncryptedColumn) {
            if (
                $isCriterion
                || !$isEncryptedSearchableColumn
                || ($operator !== null && $operator !== Criteria::EQUAL && $operator !== Criteria::NOT_EQUAL)
            ) {
                throw new \Exception("The column $columnName is encrypted, and does not support this form of query.");
            } else {
                $value = \Athens\Encryption\Cipher::getInstance()->deterministicEncrypt((string)$value);
            }
        }

        return parent::addUsingOperator($p1, $value, $operator, $preferColumnCondition);
    }
EOT;
        $script = substr_replace(
            $script,
            $useString,
            strrpos($script, '}') - 1,
            0
        );
    }

    /**
     * @return string[]
     */
    protected function getColumnNames()
    {
        $columnNames = [];
        foreach ($this->getParameters() as $key => $columnName) {
            if (strpos($key, "column_name") !== false && empty($columnName) !== true) {
                $columnNames[] = $columnName;
            }
        }
        return $columnNames;
    }

    /**
     * @return string[]
     */
    protected function getColumnPhpNames()
    {
        $table = $this->getTable();

        return array_map(
            function ($columnName) use ($table) {
                return $table->getColumn($columnName)->getPhpName();
            },
            $this->getColumnNames()
        );
    }

    /**
     * @return string[]
     */
    protected function getColumnRealNames()
    {
        $tableName = $this->getTable()->getName();

        return array_map(
            function ($columnName) use ($tableName) {
                return "$tableName.$columnName";
            },
            $this->getColumnNames()
        );
    }

    /**
     * @return boolean
     */
    protected function isSearchable()
    {
        return $this->getParameter('searchable');
    }

    /**
     * @param string $script
     * @return boolean
     */
    protected static function encryptedColumnsDeclarationExists($script)
    {
        return strpos($script, 'protected static $encryptedColumns') !== false;
    }

    /**
     * @param string $script
     * @return void
     */
    protected static function insertEncryptedColumnNameAccessMethods(&$script)
    {
        $useString = <<<'EOT'

    /**
     * @param $columnName
     * @return boolean
     */
    public static function isEncryptedColumnName($columnName)
    {
        return array_search($columnName, static::$encryptedColumns) !== false;
    }

    /**
     * @param $columnName
     * @return boolean
     */
    public static function isEncryptedSearchableColumnName($columnName)
    {
        return array_search($columnName, static::$encryptedSearchableColumns) !== false;
    }
EOT;

        $script = substr_replace(
            $script,
            $useString,
            strrpos($script, '}') - 1,
            0
        );

    }

    /**
     * @param string  $script
     * @param integer $position
     * @return void
     */
    protected static function insertEncryptedColumnsDeclaration(&$script, $position)
    {

        $content = <<<'EOT'


    /**
     * Those columns encrypted by Athens/Encryption
     */
    protected static $encryptedColumns = array(
        );

    /**
     * Those columns encrypted deterministically by Athens/Encryption
     */
    protected static $encryptedSearchableColumns = array(
        );
EOT;

        $script = substr_replace($script, $content, $position, 0);
    }

    /**
     * @param string $script
     * @param string $realColumnName
     * @return void
     */
    public static function insertEncryptedColumnName(&$script, $realColumnName)
    {
        $insertContent = "\n            '$realColumnName', ";

        $insertLocation = strpos($script, '$encryptedColumns = array(') + strlen('$encryptedColumns = array(');
        $script = substr_replace($script, $insertContent, $insertLocation, 0);
    }

    /**
     * @param string $script
     * @param string $realColumnName
     * @return void
     */
    public static function insertSearchableEncryptedColumnName(&$script, $realColumnName)
    {
        $insertContent = "\n            '$realColumnName', ";

        $insertLocation = strpos($script, '$encryptedSearchableColumns = array(')
            + strlen('$encryptedSearchableColumns = array(');

        $script = substr_replace($script, $insertContent, $insertLocation, 0);
    }

    /**
     * @param string $script
     * @param string $columnPhpName
     * @return void
     */
    protected function addEncryptionToSetter(&$script, $columnPhpName)
    {
        $setterLocation = strpos($script, "set$columnPhpName");

        $start = strpos($script, "(", $setterLocation) + 1;
        $length = strpos($script, ")", $setterLocation) - $start;
        $variableName = substr($script, $start, $length);

        $encryptionMethod = $this->isSearchable() ? "deterministicEncrypt" : "encrypt";
        $content = <<<EOT

        // Encrypt the variable, per \Athens\Encryption\EncryptionBehavior.
        $variableName = \Athens\Encryption\Cipher::getInstance()->$encryptionMethod($variableName);

EOT;

        $insertionStart = strpos($script, "{", $setterLocation) + 1;
        $script = substr_replace($script, $content, $insertionStart, 0);
    }

    /**
     * @param string $script
     * @param string $columnPhpName
     * @return void
     */
    protected function addDecryptionToGetter(&$script, $columnPhpName)
    {
        $getterLocation = strpos($script, "get$columnPhpName");

        $start = strpos($script, "return", $getterLocation) + 7;
        $length = strpos($script, ";", $getterLocation) - $start;
        $variableName = substr($script, $start, $length);

        $insertionStart = strpos($script, "return", $getterLocation);
        $insertionLength = strpos($script, ";", $insertionStart) - $insertionStart + 1;


        $content = <<<EOT
// Decrypt the variable, per \Athens\Encryption\EncryptionBehavior.
        \$fieldValue = $variableName;
        if (is_resource(\$fieldValue) && get_resource_type(\$fieldValue) === "stream") {
            \$fieldValue = \Athens\Encryption\Cipher::getInstance()->decryptStream(\$fieldValue);
        }
        return \$fieldValue;
EOT;

        $script = substr_replace($script, $content, $insertionStart, $insertionLength);
    }
}