├── .gitignore ├── AmbtaDoctrineEncryptBundle.php ├── Command ├── AbstractCommand.php ├── DoctrineDecryptDatabaseCommand.php ├── DoctrineEncryptDatabaseCommand.php └── DoctrineEncryptStatusCommand.php ├── Configuration └── Encrypted.php ├── DependencyInjection ├── Compiler │ └── RegisterServiceCompilerPass.php ├── Configuration.php └── DoctrineEncryptExtension.php ├── Encryptors ├── AES192Encryptor.php ├── AES256Encryptor.php ├── EncryptorInterface.php └── VariableEncryptor.php ├── Example └── YourBundle │ ├── Entity │ └── User.php │ ├── Library │ └── Encryptor │ │ └── AES128Encryptor.php │ └── Resources │ └── config │ └── config.yml ├── LICENSE.md ├── README.md ├── Resources ├── config │ └── orm-services.yml └── doc │ ├── commands.md │ ├── configuration.md │ ├── configuration_reference.md │ ├── custom_encryptor.md │ ├── example_of_usage.md │ ├── index.md │ ├── installation.md │ └── usage.md ├── Services └── Encryptor.php ├── Subscribers └── DoctrineEncryptSubscriber.php ├── Tests ├── AES192EncryptorTest.php ├── AES256EncryptorTest.php └── VariableEncryptorTest.php ├── composer.json └── phpunit.xml.dist /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | .idea/ 3 | .php_cs.cache -------------------------------------------------------------------------------- /AmbtaDoctrineEncryptBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new RegisterServiceCompilerPass(), PassConfig::TYPE_AFTER_REMOVING); 16 | } 17 | 18 | public function getContainerExtension() 19 | { 20 | return new DoctrineEncryptExtension(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Command/AbstractCommand.php: -------------------------------------------------------------------------------- 1 | 15 | **/ 16 | abstract class AbstractCommand extends ContainerAwareCommand 17 | { 18 | 19 | /** 20 | * @var EntityManagerInterface 21 | */ 22 | protected $entityManager; 23 | 24 | /** 25 | * @var DoctrineEncryptSubscriber 26 | */ 27 | protected $subscriber; 28 | 29 | /** 30 | * @var AnnotationReader 31 | */ 32 | protected $annotationReader; 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | protected function initialize(InputInterface $input, OutputInterface $output) 38 | { 39 | $container = $this->getContainer(); 40 | $this->entityManager = $container->get('doctrine.orm.entity_manager'); 41 | $this->annotationReader = $container->get('annotation_reader'); 42 | $this->subscriber = $container->get('ambta_doctrine_encrypt.subscriber'); 43 | } 44 | 45 | /** 46 | * Get an result iterator over the whole table of an entity. 47 | * 48 | * @param string $entityName 49 | * 50 | * @return \Doctrine\ORM\Internal\Hydration\IterableResult 51 | */ 52 | protected function getEntityIterator($entityName) 53 | { 54 | $query = $this->entityManager->createQuery(sprintf('SELECT o FROM %s o', $entityName)); 55 | return $query->iterate(); 56 | } 57 | 58 | /** 59 | * Get the number of rows in an entity-table 60 | * 61 | * @param string $entityName 62 | * 63 | * @return int 64 | */ 65 | protected function getTableCount($entityName) 66 | { 67 | $query = $this->entityManager->createQuery(sprintf('SELECT COUNT(o) FROM %s o', $entityName)); 68 | return (int) $query->getSingleScalarResult(); 69 | } 70 | 71 | /** 72 | * Return an array of entity-metadata for all entities 73 | * that have at least one encrypted property. 74 | * 75 | * @return array 76 | */ 77 | protected function getEncryptionableEntityMetaData() 78 | { 79 | $validMetaData = []; 80 | $metaDataArray = $this->entityManager->getMetadataFactory()->getAllMetadata(); 81 | 82 | foreach ($metaDataArray as $entityMetaData) 83 | { 84 | if ($entityMetaData->isMappedSuperclass) { 85 | continue; 86 | } 87 | 88 | $properties = $this->getEncryptionableProperties($entityMetaData); 89 | if (count($properties) == 0) { 90 | continue; 91 | } 92 | 93 | $validMetaData[] = $entityMetaData; 94 | } 95 | 96 | return $validMetaData; 97 | } 98 | 99 | /** 100 | * @param $entityMetaData 101 | * 102 | * @return array 103 | */ 104 | protected function getEncryptionableProperties($entityMetaData) 105 | { 106 | //Create reflectionClass for each meta data object 107 | $reflectionClass = New \ReflectionClass($entityMetaData->name); 108 | $propertyArray = $reflectionClass->getProperties(); 109 | $properties = []; 110 | 111 | foreach ($propertyArray as $property) { 112 | if ($this->annotationReader->getPropertyAnnotation($property, 'Ambta\DoctrineEncryptBundle\Configuration\Encrypted')) { 113 | $properties[] = $property; 114 | } 115 | } 116 | 117 | return $properties; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Command/DoctrineDecryptDatabaseCommand.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Michael Feinbier 17 | */ 18 | class DoctrineDecryptDatabaseCommand extends AbstractCommand 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | protected function configure() 24 | { 25 | $this 26 | ->setName('doctrine:decrypt:database') 27 | ->setDescription('Decrypt whole database on tables which are encrypted') 28 | ->addArgument('encryptor', InputArgument::OPTIONAL, 'The encryptor you want to decrypt the database with') 29 | ->addArgument('batchSize', InputArgument::OPTIONAL, 'The update/flush batch size', 20); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | protected function execute(InputInterface $input, OutputInterface $output) 36 | { 37 | //Get entity manager, question helper, subscriber service and annotation reader 38 | $question = $this->getHelper('question'); 39 | 40 | //Get list of supported encryptors 41 | $supportedExtensions = DoctrineEncryptExtension::$supportedEncryptorClasses; 42 | $batchSize = $input->getArgument('batchSize'); 43 | 44 | //If encryptor has been set use that encryptor else use default 45 | if ($input->getArgument('encryptor')) { 46 | if (isset($supportedExtensions[$input->getArgument('encryptor')])) { 47 | $this->subscriber->setEncryptor($supportedExtensions[$input->getArgument('encryptor')]); 48 | } else { 49 | if (class_exists($input->getArgument('encryptor'))) { 50 | $this->subscriber->setEncryptor($input->getArgument('encryptor')); 51 | } else { 52 | $output->writeln('\nGiven encryptor does not exists'); 53 | $output->writeln('Supported encryptors: '.implode(', ', array_keys($supportedExtensions))); 54 | $output->writeln('You can also define your own class. (example: \Ambta\DoctrineEncryptBundle\Encryptors\AES256Encryptor)'); 55 | 56 | return; 57 | } 58 | } 59 | } 60 | 61 | //Get entity manager metadata 62 | $metaDataArray = $this->entityManager->getMetadataFactory()->getAllMetadata(); 63 | 64 | //Set counter and loop through entity manager meta data 65 | $propertyCount = 0; 66 | foreach ($metaDataArray as $metaData) { 67 | if ($metaData->isMappedSuperclass) { 68 | continue; 69 | } 70 | 71 | $countProperties = count($this->getEncryptionableProperties($metaData)); 72 | $propertyCount += $countProperties; 73 | } 74 | 75 | $confirmationQuestion = new ConfirmationQuestion( 76 | "\n".count($metaDataArray).' entities found which are containing '.$propertyCount." properties with the encryption tag. \n\n". 77 | 'Which are going to be decrypted with ['.$this->subscriber->getEncryptor()."]. \n\n". 78 | "Wrong settings can mess up your data and it will be unrecoverable. \n". 79 | "I advise you to make a backup. \n\n". 80 | 'Continue with this action? (y/yes)', false 81 | ); 82 | 83 | if (!$question->ask($input, $output, $confirmationQuestion)) { 84 | return; 85 | } 86 | 87 | //Start decrypting database 88 | $output->writeln("\nDecrypting all fields. This can take up to several minutes depending on the database size."); 89 | 90 | $valueCounter = 0; 91 | 92 | //Loop through entity manager meta data 93 | foreach ($this->getEncryptionableEntityMetaData() as $metaData) { 94 | $i = 0; 95 | $iterator = $this->getEntityIterator($metaData->name); 96 | $totalCount = $this->getTableCount($metaData->name); 97 | 98 | $output->writeln(sprintf('Processing %s', $metaData->name)); 99 | $progressBar = new ProgressBar($output, $totalCount); 100 | foreach ($iterator as $row) { 101 | $entity = $row[0]; 102 | 103 | //Create reflectionClass for each entity 104 | $entityReflectionClass = new \ReflectionClass($entity); 105 | 106 | //Get the current encryptor used 107 | $encryptorUsed = $this->subscriber->getEncryptor(); 108 | 109 | //Loop through the property's in the entity 110 | foreach ($this->getEncryptionableProperties($metaData) as $property) { 111 | //Get and check getters and setters 112 | $methodeName = ucfirst($property->getName()); 113 | 114 | $getter = 'get'.$methodeName; 115 | $setter = 'set'.$methodeName; 116 | 117 | //Check if getter and setter are set 118 | if ($entityReflectionClass->hasMethod($getter) && $entityReflectionClass->hasMethod($setter)) { 119 | //Get decrypted data 120 | $unencrypted = $entity->$getter(); 121 | 122 | //Set raw data 123 | $entity->$setter($unencrypted); 124 | 125 | ++$valueCounter; 126 | } 127 | } 128 | 129 | //Disable the encryptor 130 | $this->subscriber->setEncryptor(null); 131 | $this->entityManager->persist($entity); 132 | 133 | if (($i % $batchSize) === 0) { 134 | $this->entityManager->flush(); 135 | $this->entityManager->clear(); 136 | } 137 | $progressBar->advance(1); 138 | ++$i; 139 | 140 | //Set the encryptor again 141 | $this->subscriber->setEncryptor($encryptorUsed); 142 | } 143 | 144 | $progressBar->finish(); 145 | $output->writeln(''); 146 | //Get the current encryptor used 147 | $encryptorUsed = $this->subscriber->getEncryptor(); 148 | $this->subscriber->setEncryptor(null); 149 | $this->entityManager->flush(); 150 | $this->entityManager->clear(); 151 | //Set the encryptor again 152 | $this->subscriber->setEncryptor($encryptorUsed); 153 | } 154 | 155 | //Say it is finished 156 | $output->writeln("\nDecryption finished values found: ".$valueCounter.', decrypted: '.$this->subscriber->decryptCounter.".\nAll values are now decrypted."); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /Command/DoctrineEncryptDatabaseCommand.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Michael Feinbier 17 | */ 18 | class DoctrineEncryptDatabaseCommand extends AbstractCommand 19 | { 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | protected function configure() 25 | { 26 | $this 27 | ->setName('doctrine:encrypt:database') 28 | ->setDescription('Encrypt whole database on tables which are not encrypted yet') 29 | ->addArgument('encryptor', InputArgument::OPTIONAL, 'The encryptor you want to decrypt the database with') 30 | ->addArgument('batchSize', InputArgument::OPTIONAL, 'The update/flush batch size', 20); 31 | 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | protected function execute(InputInterface $input, OutputInterface $output) 38 | { 39 | //Get entity manager, question helper, subscriber service and annotation reader 40 | $question = $this->getHelper('question'); 41 | $batchSize = $input->getArgument('batchSize'); 42 | 43 | //Get list of supported encryptors 44 | $supportedExtensions = DoctrineEncryptExtension::$supportedEncryptorClasses; 45 | 46 | //If encryptor has been set use that encryptor else use default 47 | if($input->getArgument('encryptor')) { 48 | if(isset($supportedExtensions[$input->getArgument('encryptor')])) { 49 | $this->subscriber->setEncryptor($supportedExtensions[$input->getArgument('encryptor')]); 50 | } else { 51 | if(class_exists($input->getArgument('encryptor'))) 52 | { 53 | $this->subscriber->setEncryptor($input->getArgument('encryptor')); 54 | } else { 55 | $output->writeln('\nGiven encryptor does not exists'); 56 | $output->writeln('Supported encryptors: ' . implode(', ', array_keys($supportedExtensions))); 57 | $output->writeln('You can also define your own class. (example: \Ambta\DoctrineEncryptBundle\Encryptors\AES256Encryptor)'); 58 | return; 59 | } 60 | } 61 | } 62 | 63 | //Get entity manager metadata 64 | $metaDataArray = $this->getEncryptionableEntityMetaData(); 65 | $confirmationQuestion = new ConfirmationQuestion( 66 | "\n" . count($metaDataArray) . " entities found which are containing properties with the encryption tag.\n\n" . 67 | "Which are going to be encrypted with [" . $this->subscriber->getEncryptor() . "]. \n\n". 68 | "Wrong settings can mess up your data and it will be unrecoverable. \n" . 69 | "I advise you to make a backup. \n\n" . 70 | "Continue with this action? (y/yes)", false 71 | ); 72 | 73 | if (!$question->ask($input, $output, $confirmationQuestion)) { 74 | return; 75 | } 76 | 77 | //Start decrypting database 78 | $output->writeln("\nEncrypting all fields can take up to several minutes depending on the database size."); 79 | 80 | //Loop through entity manager meta data 81 | foreach($metaDataArray as $metaData) { 82 | $i = 0; 83 | $iterator = $this->getEntityIterator($metaData->name); 84 | $totalCount = $this->getTableCount($metaData->name); 85 | 86 | $output->writeln(sprintf('Processing %s', $metaData->name)); 87 | $progressBar = new ProgressBar($output, $totalCount); 88 | foreach ($iterator as $row) { 89 | $this->subscriber->processFields($row[0]); 90 | 91 | if (($i % $batchSize) === 0) { 92 | $this->entityManager->flush(); 93 | $this->entityManager->clear(); 94 | $progressBar->advance($batchSize); 95 | } 96 | $i++; 97 | } 98 | 99 | $progressBar->finish(); 100 | $output->writeln(''); 101 | $this->entityManager->flush(); 102 | } 103 | 104 | //Say it is finished 105 | $output->writeln("\nEncryption finished. Values encrypted: " . $this->subscriber->encryptCounter . " values.\nAll values are now encrypted."); 106 | } 107 | 108 | 109 | } 110 | -------------------------------------------------------------------------------- /Command/DoctrineEncryptStatusCommand.php: -------------------------------------------------------------------------------- 1 | 12 | * @author Michael Feinbier 13 | */ 14 | class DoctrineEncryptStatusCommand extends AbstractCommand 15 | { 16 | 17 | /** 18 | * {@inheritdoc} 19 | */ 20 | protected function configure() 21 | { 22 | $this 23 | ->setName('doctrine:encrypt:status') 24 | ->setDescription('Get status of doctrine encrypt bundle and the database'); 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function execute(InputInterface $input, OutputInterface $output) 31 | { 32 | $metaDataArray = $this->entityManager->getMetadataFactory()->getAllMetadata(); 33 | 34 | $totalCount = 0; 35 | foreach($metaDataArray as $metaData) { 36 | if ($metaData->isMappedSuperclass) { 37 | continue; 38 | } 39 | 40 | $count = 0; 41 | $encryptedPropertiesCount = count($this->getEncryptionableProperties($metaData)); 42 | if ($encryptedPropertiesCount > 0) { 43 | $totalCount += $encryptedPropertiesCount; 44 | $count += $encryptedPropertiesCount; 45 | } 46 | 47 | if ($count > 0) { 48 | $output->writeln(sprintf('%s has %d properties which are encrypted.', $metaData->name, $count)); 49 | } else { 50 | $output->writeln(sprintf('%s has no properties which are encrypted.', $metaData->name)); 51 | } 52 | } 53 | 54 | $output->writeln(''); 55 | $output->writeln(sprintf('%d entities found which are containing %d encrypted properties.', count($metaDataArray), $totalCount)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Configuration/Encrypted.php: -------------------------------------------------------------------------------- 1 | 9 | * @Annotation 10 | */ 11 | class Encrypted { 12 | //Just an placeholder 13 | } -------------------------------------------------------------------------------- /DependencyInjection/Compiler/RegisterServiceCompilerPass.php: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | getRootNode(); 26 | } else { 27 | $treeBuilder = new TreeBuilder(); 28 | $rootNode = $treeBuilder->root('ambta_doctrine_encrypt'); 29 | } 30 | 31 | // Grammar of config tree 32 | $rootNode 33 | ->children() 34 | ->scalarNode('secret_key') 35 | ->end() 36 | ->scalarNode('encrypted_suffix') 37 | ->end() 38 | ->scalarNode('encryptor') 39 | ->end() 40 | ->scalarNode('encryptor_class') 41 | ->end() 42 | ->end(); 43 | 44 | return $treeBuilder; 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /DependencyInjection/DoctrineEncryptExtension.php: -------------------------------------------------------------------------------- 1 | AES256Encryptor::class, 23 | AES192Encryptor::METHOD_NAME => AES192Encryptor::class, 24 | ]; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function load(array $configs, ContainerBuilder $container) 30 | { 31 | //Create configuration object 32 | $configuration = new Configuration(); 33 | $config = $this->processConfiguration($configuration, $configs); 34 | 35 | //Set orm-service in array of services 36 | $services = ['orm' => 'orm-services']; 37 | 38 | //set supported encryptor classes 39 | $supportedEncryptorClasses = self::$supportedEncryptorClasses; 40 | 41 | //If no secret key is set, check for framework secret, otherwise throw exception 42 | if (empty($config['secret_key'])) { 43 | if ($container->hasParameter('secret')) { 44 | $config['secret_key'] = $container->getParameter('secret'); 45 | } else { 46 | throw new \RuntimeException('You must provide "secret_key" for DoctrineEncryptBundle or "secret" for framework'); 47 | } 48 | } 49 | 50 | //If no encryption suffix then set the default 51 | if (empty($config['encrypted_suffix'])) { 52 | $config['encrypted_suffix'] = ''; 53 | } 54 | 55 | 56 | //If empty encryptor class, use Rijndael 256 encryptor 57 | if (empty($config['encryptor_class'])) { 58 | if (isset($config['encryptor']) and isset($supportedEncryptorClasses[$config['encryptor']])) { 59 | $config['encryptor_class'] = $supportedEncryptorClasses[$config['encryptor']]; 60 | } else { 61 | $config['encryptor_class'] = $supportedEncryptorClasses[AES256Encryptor::METHOD_NAME]; 62 | } 63 | } 64 | 65 | //Set parameters 66 | $container->setParameter('ambta_doctrine_encrypt.encryptor_class_name', $config['encryptor_class']); 67 | $container->setParameter('ambta_doctrine_encrypt.secret_key', $config['secret_key']); 68 | $container->setParameter('ambta_doctrine_encrypt.encrypted_suffix', $config['encrypted_suffix']); 69 | 70 | //Load service file 71 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 72 | $loader->load(sprintf('%s.yml', $services['orm'])); 73 | } 74 | 75 | /** 76 | * Get alias for configuration. 77 | * 78 | * @return string 79 | */ 80 | public function getAlias() 81 | { 82 | return 'ambta_doctrine_encrypt'; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Encryptors/AES192Encryptor.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class AES192Encryptor implements EncryptorInterface 11 | { 12 | const METHOD_NAME = 'aes-192'; 13 | const ENCRYPT_MODE = 'ecb'; 14 | 15 | /** 16 | * @var string 17 | */ 18 | private $secretKey; 19 | 20 | /** 21 | * @var string 22 | */ 23 | private $suffix; 24 | 25 | /** 26 | * @var string 27 | */ 28 | private $encryptMethod; 29 | 30 | /** 31 | * @var string 32 | */ 33 | private $initializationVector; 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function __construct($key, $suffix) 39 | { 40 | $this->secretKey = md5($key); 41 | $this->suffix = $suffix; 42 | $this->encryptMethod = sprintf('%s-%s', self::METHOD_NAME, self::ENCRYPT_MODE); 43 | $this->initializationVector = false; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function encrypt($data) 50 | { 51 | if (is_string($data)) { 52 | return trim(base64_encode(openssl_encrypt( 53 | $data, 54 | $this->encryptMethod, 55 | $this->secretKey, 56 | 0, 57 | $this->initializationVector 58 | ))).$this->suffix; 59 | } 60 | 61 | return $data; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function decrypt($data) 68 | { 69 | if (is_string($data)) { 70 | $data = str_replace($this->suffix, '', $data); 71 | 72 | return trim(openssl_decrypt( 73 | base64_decode($data), 74 | $this->encryptMethod, 75 | $this->secretKey, 76 | 0, 77 | $this->initializationVector 78 | )); 79 | } 80 | 81 | return $data; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Encryptors/AES256Encryptor.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class AES256Encryptor implements EncryptorInterface 11 | { 12 | const METHOD_NAME = 'aes-256'; 13 | const ENCRYPT_MODE = 'ecb'; 14 | 15 | /** 16 | * @var string 17 | */ 18 | private $secretKey; 19 | 20 | /** 21 | * @var string 22 | */ 23 | private $suffix; 24 | 25 | /** 26 | * @var string 27 | */ 28 | private $encryptMethod; 29 | 30 | /** 31 | * @var string 32 | */ 33 | private $initializationVector; 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function __construct($key, $suffix) 39 | { 40 | $this->secretKey = md5($key); 41 | $this->suffix = $suffix; 42 | $this->encryptMethod = sprintf('%s-%s', self::METHOD_NAME, self::ENCRYPT_MODE); 43 | $this->initializationVector = false; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function encrypt($data) 50 | { 51 | if (is_string($data)) { 52 | return trim(base64_encode(openssl_encrypt( 53 | $data, 54 | $this->encryptMethod, 55 | $this->secretKey, 56 | 0, 57 | $this->initializationVector 58 | ))).$this->suffix; 59 | } 60 | 61 | return $data; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function decrypt($data) 68 | { 69 | if (is_string($data)) { 70 | $data = str_replace($this->suffix, '', $data); 71 | 72 | return trim(openssl_decrypt( 73 | base64_decode($data), 74 | $this->encryptMethod, 75 | $this->secretKey, 76 | 0, 77 | $this->initializationVector 78 | )); 79 | } 80 | 81 | return $data; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Encryptors/EncryptorInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface EncryptorInterface { 11 | 12 | /** 13 | * Must accept secret key for encryption 14 | * @param string $secretKey the encryption key 15 | */ 16 | public function __construct($secretKey, $suffix); 17 | 18 | /** 19 | * @param string $data Plain text to encrypt 20 | * @return string Encrypted text 21 | */ 22 | public function encrypt($data); 23 | 24 | /** 25 | * @param string $data Encrypted text 26 | * @return string Plain text 27 | */ 28 | public function decrypt($data); 29 | } 30 | -------------------------------------------------------------------------------- /Encryptors/VariableEncryptor.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class VariableEncryptor implements EncryptorInterface 11 | { 12 | const ENCRYPT_METHOD = 'aes-256-ecb'; 13 | 14 | /** 15 | * @var string 16 | */ 17 | private $secretKey; 18 | 19 | /** 20 | * @var string 21 | */ 22 | private $suffix; 23 | 24 | /** 25 | * @var string 26 | */ 27 | private $initializationVector; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function __construct($key, $suffix) 33 | { 34 | $this->secretKey = md5($key); 35 | $this->suffix = $suffix; 36 | $this->initializationVector = false; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function encrypt($data) 43 | { 44 | if (is_string($data)) { 45 | return trim(base64_encode(openssl_encrypt( 46 | $data, 47 | self::ENCRYPT_METHOD, 48 | $this->secretKey, 49 | 0, 50 | $this->initializationVector 51 | ))).$this->suffix; 52 | } 53 | 54 | /* 55 | * Use ROT13 which is an simple letter substitution cipher with some additions 56 | * Not the safest option but it makes it alot harder for the attacker 57 | * 58 | * Not used, needs improvement or other solution 59 | */ 60 | if (is_integer($data)) { 61 | //Not sure 62 | } 63 | 64 | return $data; 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function decrypt($data) 71 | { 72 | if (is_string($data)) { 73 | $data = str_replace($this->suffix, '', $data); 74 | 75 | return trim(openssl_decrypt( 76 | base64_decode($data), 77 | self::ENCRYPT_METHOD, 78 | $this->secretKey, 79 | 0, 80 | $this->initializationVector 81 | )); 82 | } 83 | 84 | return $data; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Example/YourBundle/Entity/User.php: -------------------------------------------------------------------------------- 1 | 11 | * @ORM\Table(name="user") 12 | * @ORM\Entity() 13 | */ 14 | class User 15 | { 16 | /** 17 | * @ORM\Id 18 | * @ORM\Column(type="integer") 19 | * @ORM\GeneratedValue(strategy="AUTO") 20 | */ 21 | private $id; 22 | 23 | /** 24 | * @Encrypted 25 | * @ORM\Column(type="string", length=25, unique=true) 26 | */ 27 | private $username; 28 | 29 | /** 30 | * @ORM\Column(type="string", length=64) 31 | */ 32 | private $password; 33 | 34 | /** 35 | * @ORM\Column(name="is_active", type="boolean") 36 | */ 37 | private $isActive; 38 | 39 | /** 40 | * @Encrypted 41 | * @ORM\Column(name="roles", type="text") 42 | */ 43 | private $roles; 44 | 45 | /** 46 | * @ORM\Column(name="is_removed", type="boolean") 47 | */ 48 | private $isRemoved; 49 | 50 | 51 | public function __construct() 52 | { 53 | 54 | } 55 | 56 | /** 57 | * Get username 58 | * 59 | * @return string 60 | */ 61 | public function getUsername() 62 | { 63 | return $this->username; 64 | } 65 | 66 | /** 67 | * Set username 68 | * 69 | * @param string $username 70 | * 71 | * @return $this 72 | */ 73 | public function setUsername($username) 74 | { 75 | $this->username = $username; 76 | 77 | return $this; 78 | } 79 | 80 | 81 | /** 82 | * Get password 83 | * 84 | * @return string 85 | */ 86 | public function getPassword() 87 | { 88 | return $this->password; 89 | } 90 | 91 | /** 92 | * Set password 93 | * 94 | * @param string $password 95 | * 96 | * @return User 97 | */ 98 | public function setPassword($password) 99 | { 100 | $this->password = password_hash($password, PASSWORD_BCRYPT, array('cost' => 12)); 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * Get user roles 107 | * 108 | * @return string[] 109 | */ 110 | public function getRoles() 111 | { 112 | return explode(',', $this->roles); 113 | } 114 | 115 | /** 116 | * Set user roles 117 | * 118 | * @param string[] $roles 119 | * 120 | * @return $this 121 | */ 122 | public function setRoles($roles) 123 | { 124 | $this->roles = implode(',', $roles); 125 | return $this; 126 | } 127 | 128 | 129 | /** 130 | * Get id 131 | * 132 | * @return integer 133 | */ 134 | public function getId() 135 | { 136 | return $this->id; 137 | } 138 | 139 | /** 140 | * Set isActive 141 | * 142 | * @param boolean $isActive 143 | * @return User 144 | */ 145 | public function setIsActive($isActive) 146 | { 147 | $this->isActive = $isActive; 148 | 149 | return $this; 150 | } 151 | 152 | /** 153 | * Get isActive 154 | * 155 | * @return boolean 156 | */ 157 | public function getIsActive() 158 | { 159 | return $this->isActive; 160 | } 161 | 162 | /** 163 | * Set is removed 164 | * 165 | * @param boolean $isRemoved 166 | * 167 | * @return $this 168 | */ 169 | public function setIsRemoved($isRemoved) 170 | { 171 | $this->isRemoved = $isRemoved; 172 | 173 | return $this; 174 | } 175 | 176 | /** 177 | * Get is removed 178 | * 179 | * @return boolean 180 | */ 181 | public function getIsRemoved() 182 | { 183 | return $this->isRemoved; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /Example/YourBundle/Library/Encryptor/AES128Encryptor.php: -------------------------------------------------------------------------------- 1 | secretKey = md5($key); 39 | $this->suffix = $suffix; 40 | $this->encryptMethod = sprintf('%s-%s', self::ENCRYPT_NAME, self::ENCRYPT_MODE); 41 | $this->initializationVector = openssl_random_pseudo_bytes( 42 | openssl_cipher_iv_length($this->encryptMethod) 43 | ); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function encrypt($data) 50 | { 51 | if (is_string($data)) { 52 | return trim(base64_encode(openssl_encrypt( 53 | $data, 54 | $this->encryptMethod, 55 | $this->secretKey, 56 | 0, 57 | $this->initializationVector 58 | ))).$this->suffix; 59 | } 60 | 61 | return $data; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function decrypt($data) 68 | { 69 | if (is_string($data)) { 70 | $data = str_replace($this->suffix, '', $data); 71 | 72 | return trim(openssl_decrypt( 73 | base64_decode($data), 74 | $this->encryptMethod, 75 | $this->secretKey, 76 | 0, 77 | $this->initializationVector 78 | )); 79 | } 80 | 81 | return $data; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Example/YourBundle/Resources/config/config.yml: -------------------------------------------------------------------------------- 1 | ambta_doctrine_encrypt: 2 | secret_key: AB1CD2EF3GH4IJ5KL6MN7OP8QR9ST0UW # Your own random 256 bit key (32 characters) 3 | #encryptor: AES-256 # AES-256 or AES-192 4 | encryptor_class: \Ambta\DoctrineEncryptBundle\Encryptors\AES256Encryptor # your own encryption class -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of 2 | this software and associated documentation files (the "Software"), to deal in 3 | the Software without restriction, including without limitation the rights to 4 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 5 | of the Software, and to permit persons to whom the Software is furnished to do 6 | so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DoctrineEncryptBundle 2 | 3 | > **Warning** 4 | > This repository is not actively maintained or developed anymore. In our opinion encrypting database information this way is not good practice and mainly gives little extra protection, but at a huge cost of usability and performance. It can only help when the database itself is stolen without keys. We have now phased it out in our own projects. 5 | 6 | Bundle allows to create doctrine entities with fields that will be protected with 7 | help of some encryption algorithm in database and it will be clearly for developer, because bundle is uses doctrine life cycle events 8 | 9 | This is an fork from the original bundle created by vmelnik-ukrain (Many thanks to him) which can be found here: 10 | [vmelnik-ukraine/DoctrineEncryptBundle](https://github.com/vmelnik-ukraine/DoctrineEncryptBundle) 11 | 12 | I improved several things, i make better use of the doctrine events. and it works with lazy loading (relationships)! 13 | This will be an long term project we will be working on with long-term support and backward compatibility. We are using this bundle in all our own symfony2 project. 14 | More about us can be found on our website. [Ambta.com](https://ambta.com) 15 | 16 | ### What does it do exactly 17 | 18 | It gives you the opportunity to add the @Encrypted annotation above each string property 19 | 20 | ```php 21 | /** 22 | * @Encrypted 23 | */ 24 | protected $username; 25 | ``` 26 | 27 | The bundle uses doctrine his life cycle events to encrypt the data when inserted into the database and decrypt the data when loaded into your entity manager. 28 | It is only able to encrypt string values at the moment, numbers and other fields will be added later on in development. 29 | 30 | ### Advantages and disadvantaged of an encrypted database 31 | 32 | #### Advantages 33 | - Information is stored safely 34 | - Not worrying about saving backups at other locations 35 | - Unreadable for employees managing the database 36 | 37 | #### Disadvantages 38 | - Can't use ORDER BY on encrypted data 39 | - In SELECT WHERE statements the where values also have to be encrypted 40 | - When you lose your key you lose your data (Make a backup of the key on a safe location) 41 | 42 | ### Documentation 43 | 44 | This bundle is responsible for encryption/decryption of the data in your database. 45 | All encryption/decryption work on the server side. 46 | 47 | The following documents are available: 48 | 49 | * [Installation](https://github.com/ambta/DoctrineEncryptBundle/blob/master/Resources/doc/installation.md) 50 | * [Configuration](https://github.com/ambta/DoctrineEncryptBundle/blob/master/Resources/doc/configuration.md) 51 | * [Usage](https://github.com/ambta/DoctrineEncryptBundle/blob/master/Resources/doc/usage.md) 52 | * [Console commands](https://github.com/ambta/DoctrineEncryptBundle/blob/master/Resources/doc/commands.md) 53 | * [Custom encryption class](https://github.com/ambta/DoctrineEncryptBundle/blob/master/Resources/doc/custom_encryptor.md) 54 | 55 | ### License 56 | 57 | This bundle is under the MIT license. See the complete license in the bundle 58 | 59 | ### Versions 60 | 61 | I'm using Semantic Versioning like described [here](http://semver.org) 62 | 63 | ### Todos 64 | 65 | The following items will be done in order 66 | 67 | 1. ~~Review of complete code + fixes/improvements and inline documentation (2.1.1)~~ 68 | 2. ~~Add support for the other doctrine relationships (manyToMany, ManyToOne) (2.2)~~ 69 | 4. ~~Recreate documentation (2.3)~~ 70 | 5. ~~Create example code (2.3)~~ 71 | 6. ~~Create an function to encrypt unencrypted database and vice versa (console command, migration, changed key, etc.) (2.4)~~ 72 | 7. Look for a posibility of automatic encryption of query parameters (2.5) 73 | 8. Look for a posibility to override findOneBy for automatic encryption of parameters (2.6) 74 | 9. Add support to encrypt data by reference to other property as key (Encrypt data specific to user with user key etc.) (2.7) 75 | 10. Add [Format-preserving encryption](http://en.wikipedia.org/wiki/Format-preserving_encryption) for all data types [Doctrine documentation Types](http://doctrine-dbal.readthedocs.org/en/latest/reference/types.html) (3.0) 76 | -------------------------------------------------------------------------------- /Resources/config/orm-services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | ambta_doctrine_encrypt.orm_subscriber: 3 | class: Ambta\DoctrineEncryptBundle\Subscribers\DoctrineEncryptSubscriber 4 | arguments: ["@annotation_reader", "%ambta_doctrine_encrypt.encryptor_class_name%", "%ambta_doctrine_encrypt.secret_key%", "%ambta_doctrine_encrypt.encrypted_suffix%"] 5 | tags: 6 | - { name: doctrine.event_subscriber } 7 | ambta_doctrine_encrypt.subscriber: 8 | alias: ambta_doctrine_encrypt.orm_subscriber 9 | ambta_doctrine_encrypt.encryptor: 10 | class: Ambta\DoctrineEncryptBundle\Services\Encryptor 11 | arguments: 12 | - "%ambta_doctrine_encrypt.encryptor_class_name%" 13 | - "%ambta_doctrine_encrypt.secret_key%" 14 | - "%ambta_doctrine_encrypt.encrypted_suffix%" 15 | -------------------------------------------------------------------------------- /Resources/doc/commands.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | To make your life a little easier we created some commands that you can use for encrypting and decrypting your current database. 4 | 5 | ## 1) Get status 6 | 7 | You can use the comment `doctrine:encrypt:status` to get the current database and encryption information. 8 | 9 | ``` 10 | $ php app/console doctrine:encrypt:status 11 | ``` 12 | 13 | This command will return the amount of entities and the amount of properties with the @Encrypted tag for each entity. 14 | The result will look like this: 15 | 16 | ``` 17 | DoctrineEncrypt\Entity\User has 3 properties which are encrypted. 18 | DoctrineEncrypt\Entity\UserDetail has 13 properties which are encrypted. 19 | 20 | 2 entities found which are containing 16 encrypted properties. 21 | ``` 22 | 23 | ## 2) Encrypt current database 24 | 25 | You can use the comment `doctrine:encrypt:database [encryptor]` to encrypt the current database. 26 | 27 | * Optional parameter [encryptor] 28 | * An encryptor provided by the bundle (AES-256 or AES-128) or your own [encryption class](https://github.com/ambta/DoctrineEncryptBundle/blob/master/Resources/doc/custom_encryptor.md). 29 | * Default: Your encryptor set in the configuration file or the default encryption class when not set in the configuration file 30 | 31 | ``` 32 | $ php app/console doctrine:encrypt:database 33 | ``` 34 | 35 | or you can provide an encryptor (optional). 36 | 37 | ``` 38 | $ php app/console doctrine:encrypt:database AES-256 39 | ``` 40 | 41 | ``` 42 | $ php app/console doctrine:encrypt:database \Ambta\DoctrineEncryptBundle\Encryptors\AES256Encryptor 43 | ``` 44 | 45 | This command will return the amount of values encrypted in the database. 46 | 47 | ``` 48 | Encryption finished values encrypted: 203 values. 49 | ``` 50 | 51 | 52 | ## 3) Decrypt current database 53 | 54 | You can use the comment `doctrine:decrypt:database [encryptor]` to decrypt the current database. 55 | 56 | * Optional parameter [encryptor] 57 | * An encryptor provided by the bundle (AES-256 or AES-128) or your own [encryption class](https://github.com/ambta/DoctrineEncryptBundle/blob/master/Resources/doc/custom_encryptor.md). 58 | * Default: Your encryptor set in the configuration file or the default encryption class when not set in the configuration file 59 | 60 | ``` 61 | $ php app/console doctrine:encrypt:database 62 | ``` 63 | 64 | or you can provide an encryptor (optional). 65 | 66 | ``` 67 | $ php app/console doctrine:decrypt:database AES-256 68 | ``` 69 | 70 | ``` 71 | $ php app/console doctrine:decrypt:database \Ambta\DoctrineEncryptBundle\Encryptors\AES256Encryptor 72 | ``` 73 | 74 | This command will return the amount of entities and the amount of values decrypted in the database. 75 | 76 | ``` 77 | Decryption finished entities found: 26, decrypted 195 values. 78 | ``` 79 | 80 | ## Custom encryption class 81 | 82 | You may want to use your own encryption class learn how here: 83 | 84 | #### [Custom encryption class](https://github.com/ambta/DoctrineEncryptBundle/blob/master/Resources/doc/custom_encryptor.md) -------------------------------------------------------------------------------- /Resources/doc/configuration.md: -------------------------------------------------------------------------------- 1 | #Configuration Reference 2 | 3 | There are 3 paramaters in the configuration of the Doctrine encryption bundle which are all optional. 4 | 5 | * **secret_key** - The key used to encrypt the data (256 bit) 6 | * 32 character long string 7 | * Default: empty, the bundle will use your Symfony2 secret key. 8 | 9 | * **encrypted_suffix** - The suffix to add to the end of encrypted strings 10 | * any string which won't likely occur in the encrypted data 11 | * Default: 12 | 13 | * **encryptor** - The encryptor used to encrypt the data 14 | * Encryptor name, currently available: AES-192 and AES-256 15 | * Default: AES-256 16 | 17 | * **encryptor_class** - Custom class for encrypting data 18 | * Encryptor class, [your own encryptor class](https://github.com/ambta/DoctrineEncryptBundle/blob/master/Resources/doc/custom_encryptor.md) will override encryptor paramater 19 | * Default: empty 20 | 21 | ## yaml 22 | 23 | ``` yaml 24 | ambta_doctrine_encrypt: 25 | secret_key: AB1CD2EF3GH4IJ5KL6MN7OP8QR9ST0UW # Your own random 256 bit key (32 characters) 26 | encrypted_suffix: # or any other string that you wish that won't cause issues with your DB 27 | encryptor: AES-256 # AES-256 or AES-192 28 | encryptor_class: \Ambta\DoctrineEncryptBundle\Encryptors\AES256Encryptor # your own encryption class 29 | ``` 30 | 31 | ### xml 32 | 33 | ``` xml 34 | 35 | 36 | AB1CD2EF3GH4IJ5KL6MN7OP8QR9ST0UW 37 | 38 | ]]> 39 | 40 | AES-256 41 | 42 | \Ambta\DoctrineEncryptBundle\Encryptors\AES256Encryptor 43 | 44 | ``` 45 | 46 | ## Usage 47 | 48 | Read how to use the database encryption bundle in your project. 49 | 50 | #### [Usage](https://github.com/ambta/DoctrineEncryptBundle/blob/master/Resources/doc/usage.md) -------------------------------------------------------------------------------- /Resources/doc/configuration_reference.md: -------------------------------------------------------------------------------- 1 | #Configuration Reference 2 | 3 | All available configuration options are listed below. 4 | 5 | ``` yaml 6 | ambta_doctrine_encrypt: 7 | # Secret key for encrypt algorithm. All secret key checks are encryptor tasks only. 8 | # We recommend an 32 character long key (256 bits), Use another key for each project! 9 | # Store a backup of this key on a secure location, losing this key will mean losing your data! 10 | secret_key: ~ # Required 11 | # If you want, you can use your own Encryptor. Encryptor must implements EncryptorInterface interface 12 | # Default: Ambta\DoctrineEncryptBundle\Encryptors\VariableEncryptor 13 | encryptor_class: ~ #optional 14 | ``` 15 | -------------------------------------------------------------------------------- /Resources/doc/custom_encryptor.md: -------------------------------------------------------------------------------- 1 | 2 | # Customer encryption class 3 | 4 | We can imagine that you want to use your own encryption class, it is simpel. 5 | 6 | ### Warning: make sure you add the encryption suffix after your encrypted string. 7 | 8 | 1. Create an new class and implement Ambta\DoctrineEncryptBundle\Encryptors\EncryptorInterface. 9 | 2. Create a constructor with the parameter secret key `__construct($secretKey)` 10 | 3. Create a function called encrypt with parameter data `encrypt($data)` 11 | 4. Create a function called decrypt with parameter data `decrypt($data)` 12 | 5. Insert your own encryption/decryption methods in those functions. 13 | 6. Define the class in your configuration file 14 | 15 | ## Example 16 | 17 | ### MyRijndael192Encryptor.php 18 | 19 | ``` php 20 | secretKey = md5($key); 60 | $this->suffix = $suffix; 61 | $this->encryptMethod = sprintf('%s-%s', self::ENCRYPT_NAME, self::ENCRYPT_MODE); 62 | $this->initializationVector = openssl_random_pseudo_bytes( 63 | openssl_cipher_iv_length($this->encryptMethod) 64 | ); 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function encrypt($data) 71 | { 72 | if (is_string($data)) { 73 | return trim(base64_encode(openssl_encrypt( 74 | $data, 75 | $this->encryptMethod, 76 | $this->secretKey, 77 | 0, 78 | $this->initializationVector 79 | ))).$this->suffix; 80 | } 81 | 82 | return $data; 83 | } 84 | 85 | /** 86 | * {@inheritdoc} 87 | */ 88 | public function decrypt($data) 89 | { 90 | if (is_string($data)) { 91 | $data = str_replace($this->suffix, '', $data); 92 | 93 | return trim(openssl_decrypt( 94 | base64_decode($data), 95 | $this->encryptMethod, 96 | $this->secretKey, 97 | 0, 98 | $this->initializationVector 99 | )); 100 | } 101 | 102 | return $data; 103 | } 104 | } 105 | ``` 106 | 107 | ### config.yaml 108 | 109 | ``` yaml 110 | ambta_doctrine_encrypt: 111 | secret_key: AB1CD2EF3GH4IJ5KL6MN7OP8QR9ST0UW # Your own random 256 bit key (32 characters) 112 | encryptor_class: \YourBundle\Library\Encryptor\MyRijndael192Encryptor # your own encryption class 113 | ``` 114 | 115 | Now your encryption is used to encrypt and decrypt data in the database. 116 | 117 | # Store the key in a file 118 | 119 | If you want to store the key outside your application it is possible thanks to CompilerPass component. First you'll have to create your compiler. 120 | 121 | ``` php 122 | setParameter( 134 | 'vmelnik_doctrine_encrypt.secret_key', 135 | file_get_contents('../keys/aes256_secret.key') // You can choose whatever you want, you can also get the path from a parameter from config.yml 136 | ); 137 | } 138 | } 139 | 140 | ``` 141 | 142 | Then you need to register your compiler in the bundle's definition 143 | 144 | 145 | ```php 146 | addCompilerPass(new ChangeSecretKeyAESCompiler()); 161 | } 162 | } 163 | 164 | ``` 165 | 166 | And that's it ! Now you rely on a file instead of a configuration value 167 | 168 | #### [Back to the index](https://github.com/ambta/DoctrineEncryptBundle/blob/master/Resources/doc/index.md) 169 | -------------------------------------------------------------------------------- /Resources/doc/example_of_usage.md: -------------------------------------------------------------------------------- 1 | #Example Of Usage 2 | 3 | Lets imagine that we are storing some private data in our database and we don't want 4 | to somebody can see it even if he will get raw database on his hands in some dirty way. 5 | With this bundle this task can be easily made and we even don't see these processes 6 | because bundle uses some doctrine life cycle events. In database information will 7 | be encoded. In the same time entities in program will be clear as always and all 8 | these things will be happen automatically. 9 | 10 | ## Simple example 11 | 12 | For example, we have some user entity with two fields which we want to encode in database. 13 | We must import annotation `@Encrypted` first and then mark fields with it. 14 | 15 | ###Doctrine Entity 16 | 17 | ```php 18 | namespace Acme\DemoBundle\Entity; 19 | 20 | use Doctrine\ORM\Mapping as ORM; 21 | 22 | // importing @Encrypted annotation 23 | use Ambta\DoctrineEncryptBundle\Configuration\Encrypted; 24 | 25 | /** 26 | * @ORM\Entity 27 | * @ORM\Table(name="user_v") 28 | */ 29 | class UserV { 30 | 31 | /** 32 | * @ORM\Id 33 | * @ORM\GeneratedValue(strategy="AUTO") 34 | * @ORM\Column(type="integer") 35 | * @var int 36 | */ 37 | private $id; 38 | 39 | /** 40 | * @ORM\Column(type="text", name="total_money") 41 | * @Encrypted 42 | * @var int 43 | */ 44 | private $totalMoney; 45 | 46 | /** 47 | * @ORM\Column(type="string", length=100, name="first_name") 48 | * @var string 49 | */ 50 | private $firstName; 51 | 52 | /** 53 | * @ORM\Column(type="string", length=100, name="last_name") 54 | * @var string 55 | */ 56 | private $lastName; 57 | 58 | /** 59 | * @ORM\Column(type="text", name="credit_card_number") 60 | * @Encrypted 61 | * @var string 62 | */ 63 | private $creditCardNumber; 64 | 65 | //common getters/setters here... 66 | 67 | } 68 | ``` 69 | 70 | ###Fixtures 71 | 72 | ```php 73 | 74 | namespace Acme\DemoBundle\DataFixtures\ORM; 75 | 76 | use Doctrine\Common\Persistence\ObjectManager; 77 | use Doctrine\Common\DataFixtures\FixtureInterface; 78 | use Acme\DemoBundle\Entity\UserV; 79 | 80 | class LoadUserData implements FixtureInterface 81 | { 82 | public function load(ObjectManager $manager) 83 | { 84 | $user = new UserV(); 85 | $user->setFirstName('Victor'); 86 | $user->setLastName('Melnik'); 87 | $user->setTotalMoney(20); 88 | $user->setCreditCardNumber('1234567890'); 89 | 90 | $manager->persist($user); 91 | $manager->flush(); 92 | } 93 | } 94 | ``` 95 | 96 | ###Controller 97 | 98 | ```php 99 | 100 | namespace Acme\DemoBundle\Controller; 101 | 102 | use Symfony\Bundle\FrameworkBundle\Controller\Controller; 103 | 104 | // these import the "@Route" and "@Template" annotations 105 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 106 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; 107 | 108 | // our entity 109 | use Acme\DemoBundle\Entity\UserV; 110 | 111 | class DemoController extends Controller 112 | { 113 | /** 114 | * @Route("/show-user/{id}", name="_ambta_decrypt_test", requirements={"id" = "\d+"}) 115 | * @Template 116 | */ 117 | public function getUserAction(UserV $user) {} 118 | } 119 | ``` 120 | 121 | ###Template 122 | 123 | ```twig 124 |
Common info: {{ user.lastName ~ ' ' ~ user.firstName }}
125 |
126 | Decoded info: 127 |
128 |
Total money
129 |
{{ user.totalMoney }}
130 |
Credit card
131 |
{{ user.creditCardNumber }}
132 |
133 |
134 | ``` 135 | 136 | When we follow link /show-user/{x}, where x - id of our user in DB, we will see that 137 | user's information is decoded and in the same time information in database will 138 | be encoded. In database we'll have something like this: 139 | 140 | ``` 141 | id | 1 142 | total_money | dx+taMIxyUdI3OTlqkjDBKRWP9Qr28PCaCCYxwbjEQU= 143 | first_name | Victor 144 | last_name | Melnik 145 | credit_card_number | 1Y+Yzq6/dDXvtnYHhTyadWfIm6xhGLxuKL2oSuxuzL4= 146 | ``` 147 | 148 | So our information is encoded and all okay. 149 | 150 | ###Requirements 151 | 152 | You need `DoctrineFixturesBundle` and `php-openssl` extension for this example 153 | -------------------------------------------------------------------------------- /Resources/doc/index.md: -------------------------------------------------------------------------------- 1 | #DoctrineEncryptBundle 2 | 3 | This bundle is responsible for encryption/decryption of the data in your database. 4 | All encryption/decryption work on the server side. 5 | 6 | The following documents are available: 7 | 8 | * [Installation](https://github.com/ambta/DoctrineEncryptBundle/blob/master/Resources/doc/installation.md) 9 | * [Configuration](https://github.com/ambta/DoctrineEncryptBundle/blob/master/Resources/doc/configuration.md) 10 | * [Usage](https://github.com/ambta/DoctrineEncryptBundle/blob/master/Resources/doc/usage.md) 11 | * [Console commands](https://github.com/ambta/DoctrineEncryptBundle/blob/master/Resources/doc/commands.md) 12 | * [Cusom encryptor class](https://github.com/ambta/DoctrineEncryptBundle/blob/master/Resources/doc/custom_encryptor.md) 13 | -------------------------------------------------------------------------------- /Resources/doc/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | 1. Download AmbtaDoctrineEncryptBundle using composer 4 | 2. Enable the database encryption bundle 5 | 3. Configure the database encryption bundle 6 | 7 | ### Requirements 8 | 9 | * php 5.4 10 | * php-openssl 11 | * [doctrine/orm](https://packagist.org/packages/doctrine/orm) >= 2.5 12 | * [symfony/framework-bundle](https://packagist.org/packages/symfony/framework-bundle) >= 2.0 13 | 14 | ### Step 1: Download AmbtaDoctrineEncryptBundle using composer 15 | 16 | AmbtaDoctrineEncryptBundle should be installed usin [Composer](http://getcomposer.org/): 17 | 18 | ``` js 19 | { 20 | "require": { 21 | "ambta/doctrine-encrypt-bundle": "^2.5" 22 | } 23 | } 24 | ``` 25 | 26 | Now tell composer to download the bundle by running the command: 27 | 28 | ``` bash 29 | $ php composer.phar update ambta/doctrine-encrypt-bundle 30 | ``` 31 | 32 | Composer will install the bundle to your project's `vendor/ambta` directory. 33 | 34 | ### Step 2: Enable the bundle 35 | 36 | Enable the bundle in the Symfony2 kernel by adding it in your /app/AppKernel.php file: 37 | 38 | ``` php 39 | public function registerBundles() 40 | { 41 | $bundles = array( 42 | // ... 43 | new Ambta\DoctrineEncryptBundle\AmbtaDoctrineEncryptBundle(), 44 | ); 45 | } 46 | ``` 47 | 48 | ### Step 3: Set your configuration 49 | 50 | All configuration value's are optional. 51 | On the following page you can find de configuration information. 52 | 53 | #### [Configuration](https://github.com/ambta/DoctrineEncryptBundle/blob/master/Resources/doc/configuration.md) 54 | -------------------------------------------------------------------------------- /Resources/doc/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | Lets imagine that we are storing some private data in our database and we don't want 4 | to somebody can see it even if he will get raw database on his hands in some dirty way. 5 | With this bundle this task can be easily made and we even don't see these processes 6 | because bundle uses some doctrine life cycle events. In database information will 7 | be encoded. In the same time entities in program will be clear as always and all 8 | these things will be happen automatically. 9 | 10 | ## Example 11 | 12 | For example, we have some user entity with two fields which we want to encode in database. 13 | We must import annotation `@Encrypted` first and then mark fields with it. 14 | 15 | ### Doctrine Entity 16 | 17 | ``` php 18 | namespace Acme\DemoBundle\Entity; 19 | 20 | use Doctrine\ORM\Mapping as ORM; 21 | 22 | // importing @Encrypted annotation 23 | use Ambta\DoctrineEncryptBundle\Configuration\Encrypted; 24 | 25 | /** 26 | * @ORM\Entity 27 | * @ORM\Table(name="user") 28 | */ 29 | class User { 30 | 31 | .. 32 | 33 | /** 34 | * @ORM\Column(type="string", name="email") 35 | * @Encrypted 36 | * @var int 37 | */ 38 | private $email; 39 | 40 | .. 41 | 42 | } 43 | ``` 44 | 45 | It is as simple as that, the field will now be encrypted the first time the users entity gets edited. 46 | We use a suffix () to check if data is encrypted or not so, unencrypted data will still work even if the field is encrypted. 47 | 48 | ## Console commands 49 | 50 | There are some console commands that can help you encrypt your existing database or change encryption methods. 51 | Read more about the database encryption commands provided with this bundle. 52 | 53 | #### [Console commands](https://github.com/ambta/DoctrineEncryptBundle/blob/master/Resources/doc/commands.md) 54 | -------------------------------------------------------------------------------- /Services/Encryptor.php: -------------------------------------------------------------------------------- 1 | encryptor = $reflectionClass->newInstanceArgs( array( 21 | $key, $suffix 22 | )); 23 | } 24 | 25 | public function getEncryptor() { 26 | return $this->encryptor; 27 | } 28 | 29 | public function decrypt($string) { 30 | return $this->encryptor->decrypt($string); 31 | } 32 | 33 | public function encrypt($string) { 34 | return $this->encryptor->encrypt($string); 35 | } 36 | } -------------------------------------------------------------------------------- /Subscribers/DoctrineEncryptSubscriber.php: -------------------------------------------------------------------------------- 1 | annReader = $annReader; 85 | $this->secretKey = $secretKey; 86 | $this->suffix = $suffix; 87 | 88 | if ($service instanceof EncryptorInterface) { 89 | $this->encryptor = $service; 90 | } else { 91 | $this->encryptor = $this->encryptorFactory($encryptorClass, $secretKey, $suffix); 92 | } 93 | 94 | $this->restoreEncryptor = $this->encryptor; 95 | } 96 | 97 | /** 98 | * Change the encryptor 99 | * 100 | * @param $encryptorClass 101 | */ 102 | public function setEncryptor($encryptorClass) { 103 | 104 | if(!is_null($encryptorClass)) { 105 | $this->encryptor = $this->encryptorFactory($encryptorClass, $this->secretKey, $this->suffix); 106 | return; 107 | } 108 | 109 | $this->encryptor = null; 110 | } 111 | 112 | /** 113 | * Get the current encryptor 114 | */ 115 | public function getEncryptor() { 116 | if(!empty($this->encryptor)) { 117 | return get_class($this->encryptor); 118 | } else { 119 | return null; 120 | } 121 | } 122 | 123 | /** 124 | * Restore encryptor set in config 125 | */ 126 | public function restoreEncryptor() { 127 | $this->encryptor = $this->restoreEncryptor; 128 | } 129 | 130 | /** 131 | * Listen a postUpdate lifecycle event. 132 | * Decrypt entities property's values when post updated. 133 | * 134 | * So for example after form submit the preUpdate encrypted the entity 135 | * We have to decrypt them before showing them again. 136 | * 137 | * @param LifecycleEventArgs $args 138 | */ 139 | public function postUpdate(LifecycleEventArgs $args) { 140 | 141 | $entity = $args->getEntity(); 142 | $this->processFields($entity, false); 143 | 144 | } 145 | 146 | /** 147 | * Listen a preUpdate lifecycle event. 148 | * Encrypt entities property's values on preUpdate, so they will be stored encrypted 149 | * 150 | * @param PreUpdateEventArgs $args 151 | */ 152 | public function preUpdate(PreUpdateEventArgs $args) { 153 | $entity = $args->getEntity(); 154 | $this->processFields($entity); 155 | } 156 | 157 | /** 158 | * Listen a postLoad lifecycle event. 159 | * Decrypt entities property's values when loaded into the entity manger 160 | * 161 | * @param LifecycleEventArgs $args 162 | */ 163 | public function postLoad(LifecycleEventArgs $args) { 164 | 165 | //Get entity and process fields 166 | $entity = $args->getEntity(); 167 | $this->processFields($entity, false); 168 | 169 | } 170 | 171 | /** 172 | * Listen to preflush event 173 | * Encrypt entities that are inserted into the database 174 | * 175 | * @param PreFlushEventArgs $preFlushEventArgs 176 | */ 177 | public function preFlush(PreFlushEventArgs $preFlushEventArgs) { 178 | $unitOfWork = $preFlushEventArgs->getEntityManager()->getUnitOfWork(); 179 | foreach ($unitOfWork->getIdentityMap() as $className => $entities) { 180 | $class = $preFlushEventArgs->getEntityManager()->getClassMetadata($className); 181 | if ($class->isReadOnly) { 182 | continue; 183 | } 184 | 185 | foreach ($entities as $entity) { 186 | if ($entity instanceof Proxy && !$entity->__isInitialized__) { 187 | continue; 188 | } 189 | $this->processFields($entity); 190 | } 191 | } 192 | } 193 | 194 | /** 195 | * Listen to postFlush event 196 | * Decrypt entities that after inserted into the database 197 | * 198 | * @param PostFlushEventArgs $postFlushEventArgs 199 | */ 200 | public function postFlush(PostFlushEventArgs $postFlushEventArgs) { 201 | $unitOfWork = $postFlushEventArgs->getEntityManager()->getUnitOfWork(); 202 | foreach($unitOfWork->getIdentityMap() as $entityMap) { 203 | foreach($entityMap as $entity) { 204 | $this->processFields($entity, false); 205 | } 206 | } 207 | } 208 | 209 | /** 210 | * Realization of EventSubscriber interface method. 211 | * 212 | * @return Array Return all events which this subscriber is listening 213 | */ 214 | public function getSubscribedEvents() { 215 | return array( 216 | Events::postUpdate, 217 | Events::preUpdate, 218 | Events::postLoad, 219 | Events::preFlush, 220 | Events::postFlush 221 | ); 222 | } 223 | 224 | /** 225 | * Process (encrypt/decrypt) entities fields 226 | * 227 | * @param Object $entity doctrine entity 228 | * @param Boolean $isEncryptOperation If true - encrypt, false - decrypt entity 229 | * 230 | * @throws \RuntimeException 231 | * 232 | * @return object|null 233 | */ 234 | public function processFields($entity, $isEncryptOperation = true) { 235 | 236 | if(!empty($this->encryptor)) { 237 | 238 | //Check which operation to be used 239 | $encryptorMethod = $isEncryptOperation ? 'encrypt' : 'decrypt'; 240 | 241 | //Get the real class, we don't want to use the proxy classes 242 | if(strstr(get_class($entity), "Proxies")) { 243 | $realClass = ClassUtils::getClass($entity); 244 | } else { 245 | $realClass = get_class($entity); 246 | } 247 | 248 | //Get ReflectionClass of our entity 249 | $reflectionClass = new ReflectionClass($realClass); 250 | $properties = $this->getClassProperties($realClass); 251 | 252 | //Foreach property in the reflection class 253 | foreach ($properties as $refProperty) { 254 | 255 | if ($this->annReader->getPropertyAnnotation($refProperty, 'Doctrine\ORM\Mapping\Embedded')) { 256 | $this->handleEmbeddedAnnotation($entity, $refProperty, $isEncryptOperation); 257 | continue; 258 | } 259 | 260 | /** 261 | * If property is an normal value and contains the Encrypt tag, lets encrypt/decrypt that property 262 | */ 263 | if ($this->annReader->getPropertyAnnotation($refProperty, self::ENCRYPTED_ANN_NAME)) { 264 | 265 | $pac = PropertyAccess::createPropertyAccessor(); 266 | $value = $pac->getValue($entity, $refProperty->getName()); 267 | if($encryptorMethod == "decrypt") { 268 | if(!is_null($value) and !empty($value)) { 269 | if(substr($value, -5) == "") { 270 | $this->decryptCounter++; 271 | $currentPropValue = $this->encryptor->decrypt(substr($value, 0, -5)); 272 | $pac->setValue($entity, $refProperty->getName(), $currentPropValue); 273 | } 274 | } 275 | } else { 276 | if(!is_null($value) and !empty($value)) { 277 | if(substr($value, -5) != "") { 278 | $this->encryptCounter++; 279 | $currentPropValue = $this->encryptor->encrypt($value); 280 | $pac->setValue($entity, $refProperty->getName(), $currentPropValue); 281 | } 282 | } 283 | } 284 | } 285 | } 286 | 287 | return $entity; 288 | } 289 | 290 | return null; 291 | } 292 | 293 | private function handleEmbeddedAnnotation($entity, $embeddedProperty, $isEncryptOperation = true) 294 | { 295 | $reflectionClass = new ReflectionClass($entity); 296 | $propName = $embeddedProperty->getName(); 297 | 298 | $pac = PropertyAccess::createPropertyAccessor(); 299 | 300 | $embeddedEntity = $pac->getValue($entity, $propName); 301 | 302 | if ($embeddedEntity) { 303 | $this->processFields($embeddedEntity, $isEncryptOperation); 304 | } 305 | } 306 | 307 | /** 308 | * Recursive function to get an associative array of class properties 309 | * including inherited ones from extended classes 310 | * 311 | * @param string $className Class name 312 | * 313 | * @return array 314 | */ 315 | function getClassProperties($className){ 316 | 317 | $reflectionClass = new ReflectionClass($className); 318 | $properties = $reflectionClass->getProperties(); 319 | $propertiesArray = array(); 320 | 321 | foreach($properties as $property){ 322 | $propertyName = $property->getName(); 323 | $propertiesArray[$propertyName] = $property; 324 | } 325 | 326 | if($parentClass = $reflectionClass->getParentClass()){ 327 | $parentPropertiesArray = $this->getClassProperties($parentClass->getName()); 328 | if(count($parentPropertiesArray) > 0) 329 | $propertiesArray = array_merge($parentPropertiesArray, $propertiesArray); 330 | } 331 | 332 | return $propertiesArray; 333 | } 334 | 335 | /** 336 | * Encryptor factory. Checks and create needed encryptor 337 | * 338 | * @param string $classFullName Encryptor namespace and name 339 | * @param string $secretKey Secret key for encryptor 340 | * 341 | * @return EncryptorInterface 342 | * @throws \RuntimeException 343 | */ 344 | private function encryptorFactory($classFullName, $secretKey, $suffix) { 345 | $refClass = new \ReflectionClass($classFullName); 346 | if ($refClass->implementsInterface(self::ENCRYPTOR_INTERFACE_NS)) { 347 | return new $classFullName($secretKey, $suffix); 348 | } else { 349 | throw new \RuntimeException('Encryptor must implements interface EncryptorInterface'); 350 | } 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /Tests/AES192EncryptorTest.php: -------------------------------------------------------------------------------- 1 | encrypt($data); 24 | 25 | self::assertEquals($data, $aes->decrypt($encryptData)); 26 | } 27 | 28 | /** 29 | * @test 30 | * @dataProvider getContentData() 31 | * 32 | * @param mixed $data 33 | */ 34 | public function checkEncryptorWithoutString($data) 35 | { 36 | $aes = new AES192Encryptor(self::SECRET_KEY); 37 | $encryptData = $aes->encrypt($data); 38 | 39 | self::assertEquals($encryptData, $data); 40 | self::assertEquals($data, $aes->decrypt($encryptData)); 41 | } 42 | 43 | /** 44 | * @return array 45 | */ 46 | public function getContentData() 47 | { 48 | return [ 49 | [1234], 50 | [100.123], 51 | [true], 52 | [false], 53 | [null], 54 | ]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Tests/AES256EncryptorTest.php: -------------------------------------------------------------------------------- 1 | encrypt($data); 24 | 25 | self::assertEquals($data, $aes->decrypt($encryptData)); 26 | } 27 | 28 | /** 29 | * @test 30 | * @dataProvider getContentData() 31 | * 32 | * @param mixed $data 33 | */ 34 | public function checkEncryptorOtherTypes($data) 35 | { 36 | $aes = new AES256Encryptor(self::SECRET_KEY); 37 | $encryptData = $aes->encrypt($data); 38 | 39 | self::assertEquals($encryptData, $data); 40 | self::assertEquals($data, $aes->decrypt($encryptData)); 41 | } 42 | 43 | /** 44 | * @return array 45 | */ 46 | public function getContentData() 47 | { 48 | return [ 49 | [1234], 50 | [100.123], 51 | [true], 52 | [false], 53 | [null], 54 | ]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Tests/VariableEncryptorTest.php: -------------------------------------------------------------------------------- 1 | encrypt($data); 24 | 25 | self::assertEquals($data, $aes->decrypt($encryptData)); 26 | } 27 | 28 | /** 29 | * @test 30 | */ 31 | public function checkEncryptorWithoutString() 32 | { 33 | $aes = new VariableEncryptor(self::SECRET_KEY); 34 | 35 | $data = 12345; 36 | $encryptData = $aes->encrypt($data); 37 | 38 | self::assertEquals($encryptData, $data); 39 | self::assertEquals($data, $aes->decrypt($encryptData)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ambta/doctrine-encrypt-bundle", 3 | "type": "library", 4 | "keywords": ["doctrine", "symfony", "aes256", "rijndael", "encrypt", "decrypt"], 5 | "license": "MIT", 6 | "description": "Symfony 2 bundle which allows to encrypt data in database with some encrypt algorithm", 7 | "require": { 8 | "php": ">=5.6", 9 | "symfony/framework-bundle": ">=2.5", 10 | "doctrine/orm": ">=2.5", 11 | "ext-openssl": "*" 12 | }, 13 | "autoload": { 14 | "psr-0": { "Ambta\\DoctrineEncryptBundle": "" } 15 | }, 16 | "target-dir": "Ambta/DoctrineEncryptBundle", 17 | "authors": [ 18 | { 19 | "name": "Marcel van Nuil", 20 | "email": "marcel@ambta.com" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./Tests 6 | 7 | 8 | --------------------------------------------------------------------------------