Skip to content

Instantly share code, notes, and snippets.

@sstok
Created July 24, 2021 17:10
Show Gist options
  • Save sstok/bf6b09e07e521496850ee455a87e60aa to your computer and use it in GitHub Desktop.
Save sstok/bf6b09e07e521496850ee455a87e60aa to your computer and use it in GitHub Desktop.
Model Encryption (POC ONLY)

Model Encryption

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'),
      ];
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment