Key integration (easy_encrypted provider + entity hooks)#
Easy Encrypted key provider (full implementation)#
// Structure of documents
└── src/
└── Plugin/
└── KeyProvider/
└── EasyEncryptedKeyProvider.php
Path: /src/Plugin/KeyProvider/EasyEncryptedKeyProvider.php#
namespace Drupal\easy_encryption\Plugin\KeyProvider;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface as ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait as StringTranslationTrait;
use Drupal\Core\Utility\Error as Error;
use Drupal\easy_encryption\Encryption\EncryptedValue as EncryptedValue;
use Drupal\easy_encryption\Encryption\EncryptionException as EncryptionException;
use Drupal\easy_encryption\Encryption\EncryptorInterface as EncryptorInterface;
use Drupal\easy_encryption\KeyManagement\Adapter\ConfigKeyRegistry as ConfigKeyRegistry;
use Drupal\easy_encryption\KeyManagement\Port\KeyRegistryInterface as KeyRegistryInterface;
use Drupal\easy_encryption\Sodium\SodiumKeyPairReadRepositoryInterface as SodiumKeyPairReadRepositoryInterface;
use Drupal\easy_encryption\Sodium\SodiumKeyPairRepositoryUsingKeyEntities as SodiumKeyPairRepositoryUsingKeyEntities;
use Drupal\key\KeyInterface as KeyInterface;
use Drupal\key\Plugin\KeyProviderBase as KeyProviderBase;
use Drupal\key\Plugin\KeyProviderSettableValueInterface as KeyProviderSettableValueInterface;
use Psr\Log\LoggerInterface as LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface as ContainerInterface;
/**
* Stores key values encrypted at rest using Easy Encryption.
*
* This provider encrypts key values before storing them in configuration,
* allowing sensitive credentials to be safely exported and versioned.
* Decryption requires the private key to be available in the environment.
*
* @KeyProvider(
* id = "easy_encrypted",
* label = @Translation("Easy Encrypted"),
* description = @Translation("Encrypts key values at rest using an asymmetric key pair."),
* tags = {
* "encryption",
* },
* key_value = {
* "accepted" = TRUE,
* "required" = FALSE
* }
* )
*
* @internal
* This is an internal part of Easy Encryption and may be changed or removed
* at any time without warning. External code should not interact with this
* class.
*/
final class EasyEncryptedKeyProvider extends KeyProviderBase implements KeyProviderSettableValueInterface, ContainerFactoryPluginInterface
{
use StringTranslationTrait;
/** The encryptor service. */
protected EncryptorInterface $encryptor;
/** The logger channel. */
protected LoggerInterface $logger;
/**
* {@inheritdoc}
*/
public static function create(
ContainerInterface $container,
array $configuration,
$plugin_id,
$plugin_definition,
): self
{
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->encryptor = $container->get(EncryptorInterface::class);
$instance->logger = $container->get('logger.channel.easy_encryption');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getKeyValue(KeyInterface $key): string
{
$config = $this->getConfiguration();
// Let's be lenient on stub keys.
if ($config === [] || empty($config['value'] ?? '')) {
return '';
}
// A valid configuration on must contain both encrypted value and
// key pair ID.
if (empty($config['encryption_key_id'] ?? '')) {
$this->logger->error('Key entity @id is missing encrypted key pair ID in configuration.', [
'@id' => $key->id(),
]);
return '';
}
try {
return $this->encryptor->decrypt(
EncryptedValue::fromHex($config['value'], $config['encryption_key_id'])
);
}
catch (EncryptionException $e) {
$this->messenger()->addError($this->t('Failed to decrypt the encrypted value. Check logs for more information.'));
Error::logException($this->logger, $e, 'Failed to decrypt key entity @id with key pair @key_pair_id. @message', [
'@id' => $key->id(),
'@key_pair_id' => $config['encryption_key_id'],
]);
return '';
}
}
/**
* {@inheritdoc}
*/
public function setKeyValue(
KeyInterface $key,
#[\SensitiveParameter]
$key_value,
): bool
{
// An empty value cannot be encrypted, but it can be stored in a key and
// leads to a stub key.
if ($key_value === '') {
return TRUE;
}
try {
$encrypted = $this->encryptor->encrypt($key_value);
}
catch (EncryptionException $e) {
$this->messenger()->addError($this->t('Failed to encrypt value. Check logs for more information.'));
Error::logException($this->logger, $e, 'Failed to encrypt value for key entity @id. @message', [
'@id' => $key->id(),
]);
return FALSE;
}
$configuration = [
'value' => $encrypted->getCiphertextHex(),
'encryption_key_id' => $encrypted->keyId->value,
];
$this->setConfiguration($configuration);
return TRUE;
}
/**
* {@inheritdoc}
*/
public function deleteKeyValue(KeyInterface $key): true
{
$this->setConfiguration([]);
return TRUE;
}
/**
* {@inheritdoc}
*/
public function calculateDependencies(): array
{
$dependencies = [];
// Ideally, accessing a port in this layer should not happen, but
// since we need to expose plugin dependencies for the Key module, we have
// to make this compromise. If we did that, we also allow accessing the
// service directly via the container.
// @phpstan-ignore-next-line
$key_registry = \Drupal::service(KeyRegistryInterface::class);
// @phpstan-ignore instanceof.alwaysTrue
if ($key_registry instanceof ConfigKeyRegistry) {
$dependencies['config'][] = ConfigKeyRegistry::CONFIG_NAME;
}
$active = $key_registry->getActiveKeyId();
if ($active['result']) {
// @phpstan-ignore-next-line
$sodium_repository = \Drupal::service(SodiumKeyPairReadRepositoryInterface::class);
// @phpstan-ignore instanceof.alwaysTrue
if ($sodium_repository instanceof SodiumKeyPairRepositoryUsingKeyEntities) {
$dependencies = $sodium_repository->calculateKeyPairPluginDependencies($active['result']->value);
}
}
return $dependencies;
}
}
SOURCE: Entity hooks affecting Key entities (full implementation)
Entity hooks affecting Key entities (full implementation)#
// Structure of documents
└── src/
└── Hook/
└── KeyEntityHooks.php
Path: /src/Hook/KeyEntityHooks.php#
namespace Drupal\easy_encryption\Hook;
use Drupal\Core\Access\AccessResult as AccessResult;
use Drupal\Core\Cache\CacheableMetadata as CacheableMetadata;
use Drupal\Core\Form\FormStateInterface as FormStateInterface;
use Drupal\Core\Hook\Attribute\Hook as Hook;
use Drupal\Core\Session\AccountInterface as AccountInterface;
use Drupal\Core\Site\Settings as Settings;
use Drupal\Core\StringTranslation\PluralTranslatableMarkup as PluralTranslatableMarkup;
use Drupal\easy_encryption\KeyManagement\KeyUsageTrackerInterface as KeyUsageTrackerInterface;
use Drupal\easy_encryption\KeyManagement\Port\KeyRegistryInterface as KeyRegistryInterface;
use Drupal\easy_encryption\Sodium\SodiumKeyPairRepositoryUsingKeyEntities as SodiumKeyPairRepositoryUsingKeyEntities;
use Drupal\easy_encryption\Sodium\SodiumKeyPairWriteRepositoryInterface as SodiumKeyPairWriteRepositoryInterface;
use Drupal\key\Entity\Key as Key;
use Drupal\key\KeyInterface as KeyInterface;
use Symfony\Component\DependencyInjection\Attribute\AutowireServiceClosure as AutowireServiceClosure;
/**
* Key entity hook implementations.
*
* @internal
* This is an internal part of Easy Encrypt and may be changed or removed at
* any time without warning. External code should not interact with
* this class.
*/
final class KeyEntityHooks
{
/**
* Constructs a new object.
*
* @phpstan-param \Closure(): \Drupal\key\Plugin\KeyPluginManager $keyProviderManager
*/
public function __construct(
private readonly KeyRegistryInterface $keyRegistry,
private readonly KeyUsageTrackerInterface $usageTracker,
#[AutowireServiceClosure('plugin.manager.key.key_provider')]
private readonly \Closure $keyProviderManager,
private readonly SodiumKeyPairWriteRepositoryInterface $sodiumKeyPairWriteRepository,
private readonly Settings $settings,
) {
}
/**
* Implements hook_ENTITY_TYPE_prepare_form() for Key form.
*/
#[Hook('key_prepare_form')]
public function keyEntityPrepareForm(KeyInterface $key, string $operation, FormStateInterface $formState): void
{
// Suggest easy_encrypted key provider on key add form instead of built-in
// insecure providers, such as config or state.
if ($operation === 'add' && !array_key_exists('key_provider', $formState->getUserInput())) {
$key->setPlugin('key_provider', 'easy_encrypted');
}
}
/**
* Implements hook_form_FORM_ID_alter() for Key add form.
*/
#[Hook('form_key_add_form_alter')]
public function keyAddFormAlter(array &$form, FormStateInterface $formState, string $form_id): void
{
$upgraded_key_provider_ids = $this->getUpgradableKeyProviders();
if (!empty($upgraded_key_provider_ids)) {
$definitions = ($this->keyProviderManager)()->getDefinitions();
$upgraded_key_providers = array_intersect_key($definitions, array_flip($upgraded_key_provider_ids));
if (empty($upgraded_key_providers)) {
return;
}
$provider_labels = array_column($upgraded_key_providers, 'label');
$form['easy_encryption_protected_providers_message'] = [
'#theme' => 'status_messages',
'#message_list' => [
'warning' => [
new PluralTranslatableMarkup(
count($provider_labels),
'New keys created using the @providers key provider will be automatically converted to use Easy Encrypted to store credentials securely.',
'New keys created using any of the following key providers will be automatically converted to use Easy Encrypted to store credentials securely: @providers.',
['@providers' => implode(', ', $provider_labels)],
),
],
],
'#weight' => -100,
];
}
}
/**
* Implements hook_ENTITY_TYPE_presave() for Key entities.
*
* Default new keys to easy_encrypted when they are using an insecure
* key provider so credentials created by recipes and automated tooling are
* stored securely by default.
*/
#[Hook('key_presave')]
public function onKeyPreSave(KeyInterface $key): void
{
if (!$key->isNew() || $key->get('key_provider') === 'easy_encrypted') {
return;
}
assert($key instanceof Key);
$upgradeable_key_provider_ids = $this->getUpgradableKeyProviders();
$should_bypass_key_provider_upgrade = self::shouldBypassKeyProviderUpgrade($key);
if (!$should_bypass_key_provider_upgrade && in_array($key->get('key_provider'), $upgradeable_key_provider_ids, TRUE)) {
$unencrypted_value = $key->getKeyValue();
$key->set('key_provider', 'easy_encrypted');
// @phpstan-ignore-next-line
$key->getKeyProvider()->setKeyValue($key, $unencrypted_value);
$key->set('key_provider_settings', $key->getPluginCollection('key_provider')->get('easy_encrypted')->getConfiguration());
}
// Do not leave traces of this feature flag in config.
if ($should_bypass_key_provider_upgrade) {
self::deactivateKeyProviderUpgradeBypass($key);
}
}
/**
* Implements hook_ENTITY_TYPE_delete() for Key entities.
*/
#[Hook('key_delete')]
public function onKeyDelete(KeyInterface $key): void
{
if ($this->sodiumKeyPairWriteRepository instanceof SodiumKeyPairRepositoryUsingKeyEntities) {
$this->sodiumKeyPairWriteRepository->handleEntityDeletion($key);
}
}
/**
* Implements hook_ENTITY_TYPE_access() for Key entities.
*
* This is an imperfect solution until the follow Key issue is open.
*
* @see https://www.drupal.org/project/key/issues/3568554
*/
#[Hook('key_access')]
public function onKeyAccess(KeyInterface $key, string $operation, AccountInterface $account): AccessResult
{
if ($operation !== 'delete') {
return AccessResult::neutral();
}
$cacheability = new CacheableMetadata();
$active = $this->keyRegistry->getActiveKeyId();
$cacheability->addCacheableDependency($active['cacheability']);
$active_id = $active['result'];
if ($active_id !== NULL) {
$protected_key_ids = [];
$protected_key_ids[] = SodiumKeyPairRepositoryUsingKeyEntities::privateKeyKeyEntityId($active_id->value);
$protected_key_ids[] = SodiumKeyPairRepositoryUsingKeyEntities::publicKeyKeyEntityId($active_id->value);
if (in_array($key->id(), $protected_key_ids, TRUE)) {
return AccessResult::forbidden('This key entity is part of an active key pair used Easy Encryption and cannot be deleted.')->addCacheableDependency($cacheability);
}
}
$usage_mapping = $this->usageTracker->getKeyUsageMapping();
$cacheability->addCacheableDependency($usage_mapping['cacheability']);
foreach ($usage_mapping['result'] as $mapping) {
$cacheability->addCacheableDependency($mapping);
$referenced_id = $mapping->keyId;
$protected_key_ids = [];
$protected_key_ids[] = SodiumKeyPairRepositoryUsingKeyEntities::privateKeyKeyEntityId($referenced_id->value);
$protected_key_ids[] = SodiumKeyPairRepositoryUsingKeyEntities::publicKeyKeyEntityId($referenced_id->value);
if (in_array($key->id(), $protected_key_ids, TRUE)) {
return AccessResult::forbidden('This key entity is part of an active or still in used Easy Encryption key pair and cannot be deleted.')->addCacheableDependency($cacheability);
}
}
return AccessResult::allowed()->addCacheableDependency($cacheability);
}
/**
* Activates the third-party setting for bypassing key provider upgrades.
*
* @internal
*/
public static function activateKeyProviderUpgradeBypass(KeyInterface $key): void
{
$key->setThirdPartySetting('easy_encryption', 'bypass_key_provider_upgrade', TRUE);
}
/**
* Gets whether key provider upgrade should be bypassed.
*
* @internal
*/
public static function shouldBypassKeyProviderUpgrade(KeyInterface $key): bool
{
return (bool) $key->getThirdPartySetting('easy_encryption', 'bypass_key_provider_upgrade', FALSE);
}
/**
* Deactivates the third-party setting for bypassing key provider upgrades.
*
* @internal
*/
public static function deactivateKeyProviderUpgradeBypass(KeyInterface $key): void
{
$key->unsetThirdPartySetting('easy_encryption', 'bypass_key_provider_upgrade');
}
}
File Statistics
- Size: 14.33 KB
- Lines: 445
File: core/key_integration/easy_encrypted.md