PHP Classes

File: src/EncryptedRow.php

Recommend this page to a friend!
  Classes of Scott Arciszewski   Cipher Sweet   src/EncryptedRow.php   Download  
File: src/EncryptedRow.php
Role: Class source
Content type: text/plain
Description: Class source
Class: Cipher Sweet
Encrypt data in away that can be searched
Author: By
Last change:
Date: 6 years ago
Size: 15,319 bytes
 

Contents

Class file image Download
<?php namespace ParagonIE\CipherSweet; use ParagonIE\CipherSweet\Backend\Key\SymmetricKey; use ParagonIE\CipherSweet\Exception\ArrayKeyException; use ParagonIE\ConstantTime\Hex; /** * Class EncryptedRow * @package ParagonIE\CipherSweet */ class EncryptedRow { const TYPE_BOOLEAN = 'bool'; const TYPE_TEXT = 'string'; const TYPE_INT = 'int'; const TYPE_FLOAT = 'float'; const COMPOUND_SPECIAL = 'special__compound__indexes'; /** * @var CipherSweet $engine */ protected $engine; /** * @var array<string, string> $fieldsToEncrypt */ protected $fieldsToEncrypt = []; /** * @var array<string, array<string, BlindIndex>> $blindIndexes */ protected $blindIndexes = []; /** * @var array<string, CompoundIndex> $compoundIndexes */ protected $compoundIndexes = []; /** * @var string $tableName */ protected $tableName; /** * EncryptedFieldSet constructor. * * @param CipherSweet $engine * @param string $tableName */ public function __construct(CipherSweet $engine, $tableName) { $this->engine = $engine; $this->tableName = $tableName; } /** * Define a field that will be encrypted. * * @param string $fieldName * @param string $type * @return self */ public function addField($fieldName, $type = self::TYPE_TEXT) { $this->fieldsToEncrypt[$fieldName] = $type; return $this; } /** * Define a boolean field that will be encrypted. Nullable. * * @param string $fieldName * @return self */ public function addBooleanField($fieldName) { return $this->addField($fieldName, self::TYPE_BOOLEAN); } /** * Define a floating point number (decimal) field that will be encrypted. * * @param string $fieldName * @return self */ public function addFloatField($fieldName) { return $this->addField($fieldName, self::TYPE_FLOAT); } /** * Define an integer field that will be encrypted. * * @param string $fieldName * @return self */ public function addIntegerField($fieldName) { return $this->addField($fieldName, self::TYPE_INT); } /** * Define a text field that will be encrypted. * * @param string $fieldName * @return self */ public function addTextField($fieldName) { return $this->addField($fieldName, self::TYPE_TEXT); } /** * Add a normal blind index to this EncryptedRow object. * * @param string $column * @param BlindIndex $index * @return self */ public function addBlindIndex($column, BlindIndex $index) { $this->blindIndexes[$column][$index->getName()] = $index; return $this; } /** * Add a compound blind index to this EncryptedRow object. * * @param CompoundIndex $index * @return self */ public function addCompoundIndex(CompoundIndex $index) { $this->compoundIndexes[$index->getName()] = $index; return $this; } /** * Create a compound blind index then add it to this EncryptedRow object. * * @param string $name * @param array<int, string> $columns * @param int $filterBits * @param bool $fastHash * @param array $hashConfig * @return CompoundIndex */ public function createCompoundIndex( $name, array $columns = [], $filterBits = 256, $fastHash = false, array $hashConfig = [] ) { $index = new CompoundIndex( $name, $columns, $filterBits, $fastHash, $hashConfig ); $this->addCompoundIndex($index); return $index; } /** * Get all of the blind indexes and compound indexes defined for this * object, calculated from the input array. * * @param array $row * @return array<string, array<string, string>> * * @throws ArrayKeyException * @throws Exception\CryptoOperationException * @throws \SodiumException */ public function getAllBlindIndexes(array $row) { $return = []; foreach ($this->blindIndexes as $column => $blindIndexes) { /** @var BlindIndex $blindIndex */ foreach ($blindIndexes as $blindIndex) { $return[$blindIndex->getName()] = $this->calcBlindIndex( $row, $column, $blindIndex ); } } /** * @var string $name * @var CompoundIndex $compoundIndex */ foreach ($this->compoundIndexes as $name => $compoundIndex) { $return[$name] = $this->calcCompoundIndex($row, $compoundIndex); } return $return; } /** * Decrypt all of the appropriate fields in the given array. * * If any columns are defined in this object to be decrypted, the value * will be decrypted in-place in the returned array. * * @param array<string, string> $row * @return array<string, string|int|float|bool|null> * @throws Exception\CryptoOperationException * @throws \SodiumException */ public function decryptRow(array $row) { $return = $row; foreach ($this->fieldsToEncrypt as $field => $type) { $key = $this->engine->getFieldSymmetricKey( $this->tableName, $field ); $plaintext = $this ->engine ->getBackend() ->decrypt($row[$field], $key); $return[$field] = $this->convertFromString($plaintext, $type); } return $return; } /** * Encrypt any of the appropriate fields in the given array. * * If any columns are defined in this object to be encrypted, the value * will be encrypted in-place in the returned array. * * @param array<string, string|int|float|bool|null> $row * * @return array<string, string> * @throws ArrayKeyException * @throws Exception\CryptoOperationException * @throws \SodiumException */ public function encryptRow(array $row) { $return = $row; foreach ($this->fieldsToEncrypt as $field => $type) { if (!\array_key_exists($field, $row)) { throw new ArrayKeyException( 'Expected value for column ' . $field . ' on array, nothing given.' ); } /** @var string $plaintext */ $plaintext = $this->convertToString($row[$field], $type); $key = $this->engine->getFieldSymmetricKey( $this->tableName, $field ); $return[$field] = $this ->engine ->getBackend() ->encrypt($plaintext, $key); } /** @var array<string, string> $return */ return $return; } /** * Process an entire row, which means: * * 1. If any columns are defined in this object to be encrypted, the value * will be encrypted in-place in the first array. * 2. Blind indexes and compound indexes are calculated and stored in the * second array. * * Calling encryptRow() and getAllBlindIndexes() is equivalent. * * @param array<string, int|float|string|bool|null> $row * @return array{0: array<string, string>, 1: array<string, array<string, string>>} * * @throws ArrayKeyException * @throws Exception\CryptoOperationException * @throws \SodiumException */ public function prepareRowForStorage(array $row) { return [ $this->encryptRow($row), $this->getAllBlindIndexes($row) ]; } /** * @param array $row * @param string $column * @param BlindIndex $index * @return array<string, string> * * @throws ArrayKeyException * @throws Exception\CryptoOperationException * @throws \SodiumException */ protected function calcBlindIndex(array $row, $column, BlindIndex $index) { $name = $index->getName(); $key = $this->engine->getBlindIndexRootKey( $this->tableName, $column ); $k = $this->engine->getIndexTypeColumn( $this->tableName, $column, $name ); return [ 'type' => $k, 'value' => Hex::encode( $this->calcBlindIndexRaw( $row, $column, $index, $key ) ) ]; } /** * @param array $row * @param CompoundIndex $index * * @return array<string, string> * @throws Exception\CryptoOperationException */ protected function calcCompoundIndex(array $row, CompoundIndex $index) { $name = $index->getName(); $key = $this->engine->getBlindIndexRootKey( $this->tableName, self::COMPOUND_SPECIAL ); $k = $this->engine->getIndexTypeColumn( $this->tableName, self::COMPOUND_SPECIAL, $name ); return [ 'type' => $k, 'value' => Hex::encode( $this->calcCompoundIndexRaw( $row, $index, $key ) ) ]; } /** * @param array $row * @param string $column * @param BlindIndex $index * @param SymmetricKey|null $key * * @return string * @throws Exception\CryptoOperationException * @throws ArrayKeyException * @throws \SodiumException */ protected function calcBlindIndexRaw( array $row, $column, BlindIndex $index, SymmetricKey $key = null ) { if (!$key) { $key = $this->engine->getBlindIndexRootKey( $this->tableName, $column ); } $backend = $this->engine->getBackend(); /** @var string $name */ $name = $index->getName(); /** @var SymmetricKey $subKey */ $subKey = new SymmetricKey( $backend, \hash_hmac( 'sha256', Util::pack([$this->tableName, $column, $name]), $key->getRawKey(), true ) ); if (!\array_key_exists($column, $this->fieldsToEncrypt)) { throw new ArrayKeyException( 'The field ' . $column . ' is not defined in this encrypted row.' ); } /** @var string $fieldType */ $fieldType = $this->fieldsToEncrypt[$column]; /** @var string|bool|int|float|null $unconverted */ $unconverted = $row[$column]; /** @var string $plaintext */ $plaintext = $index->getTransformed( $this->convertToString($unconverted, $fieldType) ); /** @var BlindIndex $index */ $index = $this->blindIndexes[$column][$name]; if ($index->getFastHash()) { return $backend->blindIndexFast( $plaintext, $subKey, $index->getFilterBitLength() ); } return $backend->blindIndexSlow( $plaintext, $subKey, $index->getFilterBitLength(), $index->getHashConfig() ); } /** * @param array $row * @param CompoundIndex $index * @param SymmetricKey|null $key * @return string * * @throws \Exception * @throws Exception\CryptoOperationException */ protected function calcCompoundIndexRaw( array $row, CompoundIndex $index, SymmetricKey $key = null ) { if (!$key) { $key = $this->engine->getBlindIndexRootKey( $this->tableName, self::COMPOUND_SPECIAL ); } $backend = $this->engine->getBackend(); /** @var string $name */ $name = $index->getName(); /** @var SymmetricKey $subKey */ $subKey = new SymmetricKey( $backend, \hash_hmac( 'sha256', Util::pack([$this->tableName, self::COMPOUND_SPECIAL, $name]), $key->getRawKey(), true ) ); /** @var string $plaintext */ $plaintext = $index->getPacked($row); /** @var CompoundIndex $index */ $index = $this->compoundIndexes[$name]; if ($index->getFastHash()) { return $backend->blindIndexFast( $plaintext, $subKey, $index->getFilterBitLength() ); } return $backend->blindIndexSlow( $plaintext, $subKey, $index->getFilterBitLength(), $index->getHashConfig() ); } /** * @param string $data * @param string $type * @return int|string|float|bool|null * @throws \SodiumException */ protected function convertFromString($data, $type) { switch ($type) { case self::TYPE_BOOLEAN: return Util::chrToBool($data); case self::TYPE_FLOAT: return Util::stringToFloat($data); case self::TYPE_INT: return Util::stringToInt($data); default: return (string) $data; } } /** * Convert multiple data types to a string prior to encryption. * * The main goals here are: * * 1. Convert several data types to a string. * 2. Leak no information about the original value in the * output string length. * * @param int|string|float|bool|null $data * @param string $type * @return string * @throws \SodiumException */ protected function convertToString($data, $type) { switch ($type) { // Will return a 1-byte string: case self::TYPE_BOOLEAN: if (!\is_null($data) && !\is_bool($data)) { $data = !empty($data); } return Util::boolToChr($data); // Will return a fixed-length string: case self::TYPE_FLOAT: if (!\is_float($data)) { throw new \TypeError('Expected a float'); } return Util::floatToString($data); // Will return a fixed-length string: case self::TYPE_INT: if (!\is_int($data)) { throw new \TypeError('Expected an integer'); } return Util::intToString($data); // Will return the original string, untouched: default: return (string) $data; } } }