vendor/symfony/framework-bundle/Secrets/SodiumVault.php line 33

  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Bundle\FrameworkBundle\Secrets;
  11. use Symfony\Component\DependencyInjection\EnvVarLoaderInterface;
  12. use Symfony\Component\VarExporter\VarExporter;
  13. /**
  14.  * @author Tobias Schultze <http://tobion.de>
  15.  * @author Jérémy Derussé <jeremy@derusse.com>
  16.  * @author Nicolas Grekas <p@tchwork.com>
  17.  */
  18. class SodiumVault extends AbstractVault implements EnvVarLoaderInterface
  19. {
  20.     private ?string $encryptionKey null;
  21.     private string|\Stringable|null $decryptionKey null;
  22.     private string $pathPrefix;
  23.     private ?string $secretsDir;
  24.     /**
  25.      * @param $decryptionKey A string or a stringable object that defines the private key to use to decrypt the vault
  26.      *                       or null to store generated keys in the provided $secretsDir
  27.      */
  28.     public function __construct(string $secretsDir, #[\SensitiveParameterstring|\Stringable $decryptionKey null)
  29.     {
  30.         $this->pathPrefix rtrim(strtr($secretsDir'/'\DIRECTORY_SEPARATOR), \DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR.basename($secretsDir).'.';
  31.         $this->decryptionKey $decryptionKey;
  32.         $this->secretsDir $secretsDir;
  33.     }
  34.     public function generateKeys(bool $override false): bool
  35.     {
  36.         $this->lastMessage null;
  37.         if (null === $this->encryptionKey && '' !== $this->decryptionKey = (string) $this->decryptionKey) {
  38.             $this->lastMessage 'Cannot generate keys when a decryption key has been provided while instantiating the vault.';
  39.             return false;
  40.         }
  41.         try {
  42.             $this->loadKeys();
  43.         } catch (\RuntimeException) {
  44.             // ignore failures to load keys
  45.         }
  46.         if ('' !== $this->decryptionKey && !is_file($this->pathPrefix.'encrypt.public.php')) {
  47.             $this->export('encrypt.public'$this->encryptionKey);
  48.         }
  49.         if (!$override && null !== $this->encryptionKey) {
  50.             $this->lastMessage sprintf('Sodium keys already exist at "%s*.{public,private}" and won\'t be overridden.'$this->getPrettyPath($this->pathPrefix));
  51.             return false;
  52.         }
  53.         $this->decryptionKey sodium_crypto_box_keypair();
  54.         $this->encryptionKey sodium_crypto_box_publickey($this->decryptionKey);
  55.         $this->export('encrypt.public'$this->encryptionKey);
  56.         $this->export('decrypt.private'$this->decryptionKey);
  57.         $this->lastMessage sprintf('Sodium keys have been generated at "%s*.public/private.php".'$this->getPrettyPath($this->pathPrefix));
  58.         return true;
  59.     }
  60.     public function seal(string $namestring $value): void
  61.     {
  62.         $this->lastMessage null;
  63.         $this->validateName($name);
  64.         $this->loadKeys();
  65.         $filename $this->getFilename($name);
  66.         $this->export($filenamesodium_crypto_box_seal($value$this->encryptionKey ?? sodium_crypto_box_publickey($this->decryptionKey)));
  67.         $list $this->list();
  68.         $list[$name] = null;
  69.         uksort($list'strnatcmp');
  70.         file_put_contents($this->pathPrefix.'list.php'sprintf("<?php\n\nreturn %s;\n"VarExporter::export($list)), \LOCK_EX);
  71.         $this->lastMessage sprintf('Secret "%s" encrypted in "%s"; you can commit it.'$name$this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR));
  72.     }
  73.     public function reveal(string $name): ?string
  74.     {
  75.         $this->lastMessage null;
  76.         $this->validateName($name);
  77.         $filename $this->getFilename($name);
  78.         if (!is_file($file $this->pathPrefix.$filename.'.php')) {
  79.             $this->lastMessage sprintf('Secret "%s" not found in "%s".'$name$this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR));
  80.             return null;
  81.         }
  82.         if (!\function_exists('sodium_crypto_box_seal')) {
  83.             $this->lastMessage sprintf('Secret "%s" cannot be revealed as the "sodium" PHP extension missing. Try running "composer require paragonie/sodium_compat" if you cannot enable the extension."'$name);
  84.             return null;
  85.         }
  86.         $this->loadKeys();
  87.         if ('' === $this->decryptionKey) {
  88.             $this->lastMessage sprintf('Secret "%s" cannot be revealed as no decryption key was found in "%s".'$name$this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR));
  89.             return null;
  90.         }
  91.         if (false === $value sodium_crypto_box_seal_open(include $file$this->decryptionKey)) {
  92.             $this->lastMessage sprintf('Secret "%s" cannot be revealed as the wrong decryption key was provided for "%s".'$name$this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR));
  93.             return null;
  94.         }
  95.         return $value;
  96.     }
  97.     public function remove(string $name): bool
  98.     {
  99.         $this->lastMessage null;
  100.         $this->validateName($name);
  101.         $filename $this->getFilename($name);
  102.         if (!is_file($file $this->pathPrefix.$filename.'.php')) {
  103.             $this->lastMessage sprintf('Secret "%s" not found in "%s".'$name$this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR));
  104.             return false;
  105.         }
  106.         $list $this->list();
  107.         unset($list[$name]);
  108.         file_put_contents($this->pathPrefix.'list.php'sprintf("<?php\n\nreturn %s;\n"VarExporter::export($list)), \LOCK_EX);
  109.         $this->lastMessage sprintf('Secret "%s" removed from "%s".'$name$this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR));
  110.         return @unlink($file) || !file_exists($file);
  111.     }
  112.     public function list(bool $reveal false): array
  113.     {
  114.         $this->lastMessage null;
  115.         if (!is_file($file $this->pathPrefix.'list.php')) {
  116.             return [];
  117.         }
  118.         $secrets = include $file;
  119.         if (!$reveal) {
  120.             return $secrets;
  121.         }
  122.         foreach ($secrets as $name => $value) {
  123.             $secrets[$name] = $this->reveal($name);
  124.         }
  125.         return $secrets;
  126.     }
  127.     public function loadEnvVars(): array
  128.     {
  129.         return $this->list(true);
  130.     }
  131.     private function loadKeys(): void
  132.     {
  133.         if (!\function_exists('sodium_crypto_box_seal')) {
  134.             throw new \LogicException('The "sodium" PHP extension is required to deal with secrets. Alternatively, try running "composer require paragonie/sodium_compat" if you cannot enable the extension.".');
  135.         }
  136.         if (null !== $this->encryptionKey || '' !== $this->decryptionKey = (string) $this->decryptionKey) {
  137.             return;
  138.         }
  139.         if (is_file($this->pathPrefix.'decrypt.private.php')) {
  140.             $this->decryptionKey = (string) include $this->pathPrefix.'decrypt.private.php';
  141.         }
  142.         if (is_file($this->pathPrefix.'encrypt.public.php')) {
  143.             $this->encryptionKey = (string) include $this->pathPrefix.'encrypt.public.php';
  144.         } elseif ('' !== $this->decryptionKey) {
  145.             $this->encryptionKey sodium_crypto_box_publickey($this->decryptionKey);
  146.         } else {
  147.             throw new \RuntimeException(sprintf('Encryption key not found in "%s".'\dirname($this->pathPrefix)));
  148.         }
  149.     }
  150.     private function export(string $filenamestring $data): void
  151.     {
  152.         $b64 'decrypt.private' === $filename '// SYMFONY_DECRYPTION_SECRET='.base64_encode($data)."\n" '';
  153.         $name basename($this->pathPrefix.$filename);
  154.         $data str_replace('%''\x'rawurlencode($data));
  155.         $data sprintf("<?php // %s on %s\n\n%sreturn \"%s\";\n"$namedate('r'), $b64$data);
  156.         $this->createSecretsDir();
  157.         if (false === file_put_contents($this->pathPrefix.$filename.'.php'$data\LOCK_EX)) {
  158.             $e error_get_last();
  159.             throw new \ErrorException($e['message'] ?? 'Failed to write secrets data.'0$e['type'] ?? \E_USER_WARNING);
  160.         }
  161.     }
  162.     private function createSecretsDir(): void
  163.     {
  164.         if ($this->secretsDir && !is_dir($this->secretsDir) && !@mkdir($this->secretsDir0777true) && !is_dir($this->secretsDir)) {
  165.             throw new \RuntimeException(sprintf('Unable to create the secrets directory (%s).'$this->secretsDir));
  166.         }
  167.         $this->secretsDir null;
  168.     }
  169.     private function getFilename(string $name): string
  170.     {
  171.         // The MD5 hash allows making secrets case-sensitive. The filename is not enough on Windows.
  172.         return $name.'.'.substr(md5($name), 06);
  173.     }
  174. }