Skip to content

Commit

Permalink
Merge pull request #5 from carnage/migration
Browse files Browse the repository at this point in the history
Migration
  • Loading branch information
carnage authored Jun 29, 2017
2 parents e5d1f40 + 423dd2a commit 0096246
Show file tree
Hide file tree
Showing 21 changed files with 549 additions and 13 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#Doctrine Encryted Column
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/carnage/doctrine-encrypted-column/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/carnage/doctrine-encrypted-column/?branch=master) [![Code Coverage](https://scrutinizer-ci.com/g/carnage/doctrine-encrypted-column/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/carnage/doctrine-encrypted-column/?branch=master)

# Motivation

Currently there are about a dozen encrypted column extensions for doctrine. None of them are very well implemented and are
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"doctrine/orm": "^2.5",
"ocramius/proxy-manager": "^1.0.2",
"paragonie/halite": "^2.0",
"psr/container": "^1.0"
"psr/container": "^1.0",
"phpseclib/phpseclib": "~2.0"
},
"require-dev": {
"phpunit/phpunit":"^5"
Expand Down
2 changes: 1 addition & 1 deletion example/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

// Create a simple "default" Doctrine ORM configuration for Annotations
$isDevMode = true;
$config = Setup::createAnnotationMetadataConfiguration(array(__DIR__."/entities"), $isDevMode, null,null,false);
$config = Setup::createAnnotationMetadataConfiguration(array(__DIR__ . "/entities"), $isDevMode, null, null, false);

// database configuration parameters
$conn = array(
Expand Down
12 changes: 10 additions & 2 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@

<phpunit colors="true" bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="dec">
<directory>./test</directory>
<testsuite name="Functional tests">
<directory>./test/Functional</directory>
</testsuite>
<testsuite name="Migration tests">
<directory>./test/Migration</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./src</directory>
</whitelist>
</filter>
</phpunit>
114 changes: 114 additions & 0 deletions src/Dbal/EncryptedColumnLegacySupport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php
namespace Carnage\EncryptedColumn\Dbal;

use Carnage\EncryptedColumn\Service\EncryptionService;
use Carnage\EncryptedColumn\ValueObject\ValueHolder;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\Type;
use Carnage\EncryptedColumn\ValueObject\EncryptedColumn as EncryptedColumnVO;

/**
* Object type for reading from legacy encrypted column extensions and converting to a better format
*
* This type is a drop in replacement for the EncryptedColumn class but drops some strictness to allow for
* reading data that is not in an expected format. You should only use this if you have existing data in
* your database you wish to convert
*
* Class EncryptedColumn
*/
class EncryptedColumnLegacySupport extends Type
{
const ENCRYPTED = 'encrypted';

/**
* @var EncryptionService
*/
private $encryptionService;

public static function create(EncryptionService $encryptionService)
{
Type::addType(EncryptedColumnLegacySupport::ENCRYPTED, EncryptedColumnLegacySupport::class);
/** @var EncryptedColumnLegacySupport $instance */
$instance = Type::getType(EncryptedColumnLegacySupport::ENCRYPTED);
$instance->encryptionService = $encryptionService;
}

public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return $platform->getClobTypeDeclarationSQL($fieldDeclaration);
}

public function requiresSQLCommentHint(AbstractPlatform $platform)
{
return true;
}

public function getName()
{
return self::ENCRYPTED;
}

public function convertToPHPValue($value, AbstractPlatform $platform)
{
if ($value === null) {
return null;
}

try {
$decoded = $this->decodeJson($value);
} catch (ConversionException $e) {
//The data wasn't in the format we expected, assume it is legacy data which needs converting
//Drop in some defaults to allow the library to handle it.
$decoded = [
'data' => $value,
'classname' => ValueHolder::class,
'serializer' => 'legacy',
'encryptor' => 'legacy'
];
}

return $this->encryptionService->decryptField(EncryptedColumnVO::fromArray($decoded));
}

public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if ($value === null) {
return null;
}

return json_encode($this->encryptionService->encryptField($value));
}

/**
* Based on: https://github.com/schmittjoh/serializer/blob/master/src/JMS/Serializer/JsonDeserializationVisitor.php
*
* @param $value
* @return mixed
* @throws ConversionException
*/
private function decodeJson($value)
{
$decoded = json_decode($value, true);

switch (json_last_error()) {
case JSON_ERROR_NONE:
if (!is_array($decoded)) {
throw ConversionException::conversionFailed($value, 'Json was not an array');
}
return $decoded;
case JSON_ERROR_DEPTH:
throw ConversionException::conversionFailed($value, 'Could not decode JSON, maximum stack depth exceeded.');
case JSON_ERROR_STATE_MISMATCH:
throw ConversionException::conversionFailed($value, 'Could not decode JSON, underflow or the nodes mismatch.');
case JSON_ERROR_CTRL_CHAR:
throw ConversionException::conversionFailed($value, 'Could not decode JSON, unexpected control character found.');
case JSON_ERROR_SYNTAX:
throw ConversionException::conversionFailed($value, 'Could not decode JSON, syntax error - malformed JSON.');
case JSON_ERROR_UTF8:
throw ConversionException::conversionFailed($value, 'Could not decode JSON, malformed UTF-8 characters (incorrectly encoded?)');
default:
throw ConversionException::conversionFailed($value, 'Could not decode Json');
}
}
}
43 changes: 43 additions & 0 deletions src/Encryptor/LegacyEncryptor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Carnage\EncryptedColumn\Encryptor;

use Carnage\EncryptedColumn\Exception\PopArtPenguinException;
use Carnage\EncryptedColumn\ValueObject\EncryptorIdentity;
use Carnage\EncryptedColumn\ValueObject\IdentityInterface;
use phpseclib\Crypt\Base;
use phpseclib\Crypt\Rijndael;

class LegacyEncryptor implements EncryptorInterface
{
const IDENTITY = 'legacy';
/**
* @var string
*/
private $secret;

public function __construct($secret)
{
$this->secret = $secret;
}

public function encrypt($data)
{
throw new PopArtPenguinException();
}

public function decrypt($data)
{
$cipher = new Rijndael(Base::MODE_ECB);
$cipher->setBlockLength(256);
$cipher->setKey($this->secret);
$cipher->padding = false;

return trim($cipher->decrypt(base64_decode($data)));
}

public function getIdentifier(): IdentityInterface
{
return new EncryptorIdentity(self::IDENTITY);
}
}
13 changes: 13 additions & 0 deletions src/Exception/PopArtPenguinException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Carnage\EncryptedColumn\Exception;

class PopArtPenguinException extends \BadMethodCallException
{
public function __construct()
{
parent::__construct(
'The encryption class you attempted to use is not considered secure and is only suitable for creating pop art penguins https://blog.filippo.io/the-ecb-penguin/'
);
}
}
27 changes: 27 additions & 0 deletions src/Serializer/LegacySerializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Carnage\EncryptedColumn\Serializer;

use Carnage\EncryptedColumn\ValueObject\IdentityInterface;
use Carnage\EncryptedColumn\ValueObject\SerializerIdentity;
use Carnage\EncryptedColumn\ValueObject\ValueHolder;

class LegacySerializer implements SerializerInterface
{
const IDENTITY = 'legacy';

public function serialize($data)
{
throw new \Exception('This class is for read only access to legacy data');
}

public function unserialize($data)
{
return new ValueHolder($data);
}

public function getIdentifier(): IdentityInterface
{
return new SerializerIdentity(self::IDENTITY);
}
}
4 changes: 2 additions & 2 deletions src/Service/EncryptionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public function encryptField($value): EncryptedColumnVO
if (
!$original->needsReencryption($this->encryptor->getIdentifier(), $this->serializer->getIdentifier())
) {
return $original;
return $original;
}
}

Expand Down Expand Up @@ -109,7 +109,7 @@ private function createInitializer(EncryptedColumnVO $value): \Closure
$serializer = $this->serializers->get($value->getSerializerIdentifier()->toString());
$encryptor = $this->encryptors->get($value->getEncryptorIdentifier()->toString());

return function (& $wrappedObject, LazyLoadingInterface $proxy, $method, array $parameters, & $initializer) use ($serializer, $encryptor, $value) {
return function(& $wrappedObject, LazyLoadingInterface $proxy, $method, array $parameters, & $initializer) use ($serializer, $encryptor, $value) {
$initializer = null;
$wrappedObject = $serializer->unserialize($encryptor->decrypt($value->getData()));

Expand Down
99 changes: 99 additions & 0 deletions src/Setup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

namespace Carnage\EncryptedColumn;

use Carnage\EncryptedColumn\Container\VersionedContainer;
use Carnage\EncryptedColumn\Dbal\EncryptedColumn;
use Carnage\EncryptedColumn\Dbal\EncryptedColumnLegacySupport;
use Carnage\EncryptedColumn\Encryptor\HaliteEncryptor;
use Carnage\EncryptedColumn\Encryptor\LegacyEncryptor;
use Carnage\EncryptedColumn\Serializer\LegacySerializer;
use Carnage\EncryptedColumn\Serializer\PhpSerializer;
use Carnage\EncryptedColumn\Service\EncryptionService;
use Doctrine\ORM\EntityManagerInterface;

final class Setup
{
private $keyPath;
private $enableLegacy = false;
private $legacyKey;

public function register(EntityManagerInterface $em)
{
if ($this->enableLegacy) {
$this->doRegisterLegacy($em);
} else {
$this->doRegister($em);
}
}

public function enableLegacy(string $legacyKey)
{
$this->enableLegacy = true;
$this->legacyKey = $legacyKey;
return $this;
}

public function withKeyPath(string $keypath)
{
$this->keyPath = $keypath;
return $this;
}

private function buildEncryptionService(): EncryptionService
{
$encryptors = self::buildEncryptorsContainer();
$serializers = self::buildSerilaizerContainer();
return new EncryptionService(
$encryptors->get(HaliteEncryptor::IDENTITY),
$serializers->get(PhpSerializer::IDENTITY),
$encryptors,
$serializers
);
}

private function buildEncryptorsContainer(): VersionedContainer
{
$services = [new HaliteEncryptor($this->keyPath)];
if ($this->enableLegacy) {
$services[] = new LegacyEncryptor($this->legacyKey);
}
//@TODO add legacy encryptor, throw exceptions if required keys aren't specified
return new VersionedContainer(...$services);
}

private function buildSerilaizerContainer(): VersionedContainer
{
$services = [new PhpSerializer()];
if ($this->enableLegacy) {
$services[] = new LegacySerializer();
}
return new VersionedContainer(...$services);
}

/**
* @param EntityManagerInterface $em
*/
private function doRegister(EntityManagerInterface $em)
{
EncryptedColumn::create($this->buildEncryptionService());
$conn = $em->getConnection();
$conn->getDatabasePlatform()->registerDoctrineTypeMapping(
EncryptedColumn::ENCRYPTED,
EncryptedColumn::ENCRYPTED
);
}

/**
* @param EntityManagerInterface $em
*/
private function doRegisterLegacy(EntityManagerInterface $em)
{
EncryptedColumnLegacySupport::create($this->buildEncryptionService());
$conn = $em->getConnection();
$conn->getDatabasePlatform()->registerDoctrineTypeMapping(
EncryptedColumnLegacySupport::ENCRYPTED,
EncryptedColumnLegacySupport::ENCRYPTED
);
}
}
2 changes: 1 addition & 1 deletion src/ValueObject/EncryptedColumn.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public static function fromArray(array $data)
{
// If an old version has saved data, these fields won't be available
// Default to the only services available in V0.1
if(!isset($data['serializer'])) {
if (!isset($data['serializer'])) {
return new self(
$data['classname'],
$data['data'],
Expand Down
21 changes: 21 additions & 0 deletions src/ValueObject/ValueHolder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Carnage\EncryptedColumn\ValueObject;

class ValueHolder
{
private $value;

public function __construct($value)
{
$this->value = $value;
}

/**
* @return mixed
*/
public function getValue()
{
return $this->value;
}
}
Loading

0 comments on commit 0096246

Please sign in to comment.