Model encryption is used for highly sensitive (or high risk) information, including bank-account numbers, or personal contact information.
Note:
Any information that legally falls under the General Data Protection Regulation (GDPR) must be stored encrypted according to legal requirements.
Keys are provided outside of the interface methods, use a factory for "creating" a new encryptor instance.
- SymmetricEncryptor (interface; input/output data; pass-key is provided as HiddenString)
- AsymmetricEncryptor (Uses public key for encryption; private for decrypting)
- HaliteEncryptor
- SodiumEncryptor (only for storage that must be made available outside of the application)
Model data-encryption is specified under the following levels, increasing per step of provided protection.
- Levels:
- C1: Public information (no encryption)
- C2: personal/private information (should be encrypted; allows blind-index)
- C3: Sensitive information (must be encrypted; SymmetricEncryptor or AsymmetricEncryptor)
- C4: Highly Sensitive information, encryption keys (must be encrypted with asymmetric cryptography (public key cryptography))
Blind-index: Store the encrypted information in the main column, to allow finding
and unique-indexes create a blind-index in [column]_index
.
https://paragonie.com/blog/2017/05/building-searchable-encrypted-databases-with-php-and-sql
DO NOT USE AN INDEX FOR C3 AND HIGHER!
- To-do
- Blind index handling
- Call
__setCryptoEngine()
using reflection - HiddenString handling (for C3 and higher)
interface CryptoEngine
{
public function encrypt(string | HiddenString $data): string;
public function decrypt(string $encrypted): string;
}
trait ModelEncryption
{
private $__decryptedData = [];
private ?CryptoEngine $__cryptoEngine;
/** @internal */
final private function __setCryptoEngine(CryptoEngine $engine): void
{
$this->__cryptoEngine = $engine;
}
/**
* Tracks a fields value as decrypted data.
*
* Set what's kept as model field's information. To access the actual data use __getEncryptedField()
*
* @return bool Returns if the data was newly set, returns false is nothing changed
*/
final protected function __setEncryptedField(string $name, $value, ?callable $valueTransformer = null): bool
{
// TODO Check if actually changed, and return false otherwise
// XXX Needs custom comparator
$this->__decryptedData[$name] = $value;
// TODO Common transformers
if ($valueTransformer) {
$value = $valueTransformer($value);
}
$this->{$name} = $this->__encryptFieldData($value).'<ENC>';
}
private function __encryptFieldData(string $encrypted): string
{
if ($this->__cryptoEngine === null) {
throw new \RuntimeException('No crypto engine set. Call __setCryptoEngine() first.');
}
return $this->__cryptoEngine->decrypt($encrypted);
}
final protected function __getEncryptedField(string $name, ?callable $valueTransformer = null)
{
if (!array_key_exists($name, $this->__decryptedData)) {
$decrypted = $this->__decryptFieldData($this->{$name});
// TODO Common transformers
if ($valueTransformer) {
$decrypted = $valueTransformer($decrypted);
}
$this->__decryptedData[$name] = $decrypted;
}
return $this->__decryptedData[$name];
}
private function __decryptFieldData(string $encrypted)
{
if (substr($encrypted, -5) !== '<ENC>') {
return $encrypted;
}
if ($this->__cryptoEngine === null) {
throw new \RuntimeException('No crypto engine set. Call __setCryptoEngine() first.');
}
return $this->__cryptoEngine->decrypt(substr($encrypted, 0, -5));
}
}
class Customer
{
use ModelEncryption;
public function changeBankAccount(string $number, string $holderName)
{
$this->__setEncryptedField('bankNumber', $number);
$this->__setEncryptedField('bankHolderName', $holderName);
}
public function getBankAccount()
{
// BAD Example don't do this! Use a ValueObject instead.
return [
'number' => $this->__getEncryptedField('bankNumber'),
'holder_name' => $this->__getEncryptedField('bankHolderName'),
];
}
}