diff --git a/classes/cliTool/CommandInterface.php b/classes/cliTool/CommandInterface.php
new file mode 100644
index 00000000000..562a8d74ca7
--- /dev/null
+++ b/classes/cliTool/CommandInterface.php
@@ -0,0 +1,34 @@
+setOutput($output);
+ }
+
+ public function errorBlock(array $messages = [], ?string $title = null): void
+ {
+ $this->getOutput()->block(
+ $messages,
+ $title,
+ 'fg=white;bg=red',
+ ' ',
+ true
+ );
+ }
+}
diff --git a/classes/cliTool/traits/HasCommandInterface.php b/classes/cliTool/traits/HasCommandInterface.php
new file mode 100644
index 00000000000..180f5767656
--- /dev/null
+++ b/classes/cliTool/traits/HasCommandInterface.php
@@ -0,0 +1,69 @@
+commandInterface = $commandInterface ?? new CommandInterface;
+
+ return $this;
+ }
+
+ /**
+ * Get the command interface
+ */
+ public function getCommandInterface(): CommandInterface
+ {
+ return $this->commandInterface;
+ }
+
+ /**
+ * Print given options in a pretty way.
+ */
+ protected function printCommandList(array $options, bool $shouldTranslate = true): void
+ {
+ $width = (int)collect(array_keys($options))
+ ->map(fn($command) => Helper::width($command))
+ ->sort()
+ ->last() + 2;
+
+ foreach ($options as $commandName => $description) {
+ $spacingWidth = $width - Helper::width($commandName);
+ $this->getCommandInterface()->line(
+ sprintf(
+ ' %s%s%s',
+ $commandName,
+ str_repeat(' ', $spacingWidth),
+ $shouldTranslate ? __($description) : $description
+ )
+ );
+ }
+ }
+}
diff --git a/classes/cliTool/traits/HasParameterList.php b/classes/cliTool/traits/HasParameterList.php
new file mode 100644
index 00000000000..8199b3269ba
--- /dev/null
+++ b/classes/cliTool/traits/HasParameterList.php
@@ -0,0 +1,78 @@
+parameterList = $parameters;
+
+ return $this;
+ }
+
+ /**
+ * Get the parameter list passed on CLI
+ */
+ public function getParameterList(): ?array
+ {
+ return $this->parameterList;
+ }
+
+ /**
+ * Get the value of a specific parameter
+ */
+ protected function getParameterValue(string $parameter, mixed $default = null): mixed
+ {
+ if (!isset($this->getParameterList()[$parameter])) {
+ return $default;
+ }
+
+ return $this->getParameterList()[$parameter];
+ }
+
+ /**
+ * Determined if the given flag set on CLI
+ */
+ protected function hasFlagSet(string $flag): bool
+ {
+ return in_array($flag, $this->getParameterList());
+ }
+}
diff --git a/classes/core/PKPAppKey.php b/classes/core/PKPAppKey.php
new file mode 100644
index 00000000000..8d621596934
--- /dev/null
+++ b/classes/core/PKPAppKey.php
@@ -0,0 +1,183 @@
+ ['size' => 16, 'aead' => false],
+ 'aes-256-cbc' => ['size' => 32, 'aead' => false],
+ 'aes-128-gcm' => ['size' => 16, 'aead' => true],
+ 'aes-256-gcm' => ['size' => 32, 'aead' => true],
+ ];
+
+ /**
+ * The default cipher algorithms
+ *
+ * @var string
+ */
+ private static $defaultCipher = 'aes-256-cbc';
+
+ /**
+ * Get the list of supported ciphers
+ */
+ public static function getSupportedCiphers(): array
+ {
+ return self::$supportedCiphers;
+ }
+
+ /**
+ * Get the defined cipher
+ */
+ public static function getCipher(): string
+ {
+ return Config::getVar('security', 'cipher', self::$defaultCipher);
+ }
+
+ /**
+ * Has the app key defined in config file
+ */
+ public static function hasKey(): bool
+ {
+ return !empty(Config::getVar('general', 'app_key', ''));
+ }
+
+ /**
+ * Has the app key variable defined in config file
+ */
+ public static function hasKeyVariable(): bool
+ {
+ return Config::hasVar('general', 'app_key');
+ }
+
+ /**
+ * Get the app key defined in config file
+ */
+ public static function getKey(): string
+ {
+ return Config::getVar('general', 'app_key', '');
+ }
+
+ /**
+ * Validate a given cipher
+ */
+ public static function validateCipher(string $cipher): string
+ {
+ $cipher = strtolower($cipher);
+
+ if (!in_array($cipher, array_keys(static::getSupportedCiphers()))) {
+ $ciphers = implode(', ', array_keys(static::getSupportedCiphers()));
+
+ throw new Exception(
+ sprintf(
+ 'Invalid cipher %s provided, must be among [%s]',
+ $cipher,
+ $ciphers
+ )
+ );
+ }
+
+ return $cipher;
+ }
+
+ /**
+ * Validate given or config defined app
+ */
+ public static function validate(string $key = null, string $cipher = null): bool
+ {
+ $config = app('config')->get('app');
+
+ return Encrypter::supported(
+ static::parseKey($key ?? $config['key']),
+ static::validateCipher($cipher ?? $config['cipher'])
+ );
+ }
+
+ /**
+ * Generate a new app key
+ */
+ public static function generate(string $cipher = null): string
+ {
+ $config = app('config')->get('app');
+
+ return 'base64:'.base64_encode(
+ Encrypter::generateKey(static::validateCipher($cipher ?? $config['cipher']))
+ );
+ }
+
+ /**
+ * Write the given app key in the config file
+ */
+ public static function writeToConfig(string $key): bool
+ {
+ if (!static::validate($key)) {
+ $ciphers = implode(', ', array_keys(static::getSupportedCiphers()));
+
+ // Error invalid app key
+ throw new Exception(
+ "Unsupported cipher or incorrect key length. Supported ciphers are: {$ciphers}."
+ );
+ }
+
+ $configParser = new ConfigParser;
+ $configParams = [
+ 'general' => [
+ 'app_key' => $key,
+ ],
+ ];
+
+ if (!static::hasKeyVariable()) {
+ // Error if the config key `app_key` not defined under `general` section
+ throw new Exception('Config variable named `app_key` not defined in the `general` section');
+ }
+
+ if (!$configParser->updateConfig(Config::getConfigFileName(), $configParams)) {
+ // Error reading config file
+ throw new Exception('Unable to read the config file');
+ }
+
+ if (!$configParser->writeConfig(Config::getConfigFileName())) {
+ // Error writing config file
+ throw new Exception('Unable to write the app key in the config file');
+ }
+
+ return true;
+ }
+
+ /**
+ * Parse the given app key and return the real key value
+ */
+ public static function parseKey(string $key): string
+ {
+ if (Str::startsWith($key, $prefix = 'base64:')) {
+ $key = base64_decode(Str::after($key, $prefix));
+ }
+
+ return $key;
+ }
+}
diff --git a/classes/core/PKPApplication.php b/classes/core/PKPApplication.php
index 5a49dc6aa83..9d9b5b33cd0 100644
--- a/classes/core/PKPApplication.php
+++ b/classes/core/PKPApplication.php
@@ -389,22 +389,6 @@ public static function getName()
return 'pkp-lib';
}
- /**
- * Get the default cipher algorithm
- * Available and Valid cipher algorithms are
- * - aes-128-cbc
- * - aes-256-cbc
- * - aes-128-gcm
- * - aes-256-gcm
- * @see \Illuminate\Encryption\Encrypter::$supportedCiphers
- *
- * @return string
- */
- public static function getDefaultCipher(): string
- {
- return 'AES-256-CBC';
- }
-
/**
* Get the locale key for the name of this application.
*
diff --git a/classes/core/PKPContainer.php b/classes/core/PKPContainer.php
index a29afcefe59..881dc8d21ef 100644
--- a/classes/core/PKPContainer.php
+++ b/classes/core/PKPContainer.php
@@ -29,6 +29,7 @@
use Illuminate\Log\LogServiceProvider;
use Illuminate\Queue\Failed\DatabaseFailedJobProvider;
use Illuminate\Support\Facades\Facade;
+use PKP\core\PKPAppKey;
use PKP\config\Config;
use PKP\i18n\LocaleServiceProvider;
use PKP\core\PKPUserProvider;
@@ -319,8 +320,8 @@ protected function loadConfiguration()
$_request = Application::get()->getRequest();
$items['app'] = [
- 'key' => Config::getVar('general', 'key', ''),
- 'cipher' => Config::getVar('security', 'cipher', Application::getDefaultCipher()),
+ 'key' => PKPAppKey::getKey(),
+ 'cipher' => PKPAppKey::getCipher(),
];
// Database connection
diff --git a/classes/core/PKPEncryptionServiceProvider.php b/classes/core/PKPEncryptionServiceProvider.php
index c96b69f5da5..5e1e7d37e1d 100644
--- a/classes/core/PKPEncryptionServiceProvider.php
+++ b/classes/core/PKPEncryptionServiceProvider.php
@@ -20,9 +20,7 @@
class PKPEncryptionServiceProvider extends IlluminateEncryptionServiceProvider
{
/**
- * Register the encrypter.
- *
- * @return void
+ * @copydoc Illuminate\Encryption\EncryptionServiceProvider::registerEncrypter()
*/
protected function registerEncrypter()
{
diff --git a/classes/install/PKPInstall.php b/classes/install/PKPInstall.php
index e10f0199f2c..f382a01c7fe 100644
--- a/classes/install/PKPInstall.php
+++ b/classes/install/PKPInstall.php
@@ -188,6 +188,7 @@ public function createConfig()
return $this->updateConfig(
[
'general' => [
+ 'app_key' => \PKP\core\PKPAppKey::generate(),
'installed' => 'On',
'base_url' => $request->getBaseUrl(),
'enable_beacon' => $this->getParam('enableBeacon') ? 'On' : 'Off',
diff --git a/locale/en/admin.po b/locale/en/admin.po
index cf45ca86e9e..9e7a8e0e46f 100644
--- a/locale/en/admin.po
+++ b/locale/en/admin.po
@@ -933,3 +933,36 @@ msgstr "Dispatch a job to automatically remove expired Invitations"
msgid "admin.settings.statistics.sushiPlatform.isSiteSushiPlatform"
msgstr "Use the site as the platform for all journals."
+
+msgid "admin.cli.tool.appKey.options.usage.description"
+msgstr "Display the AppKey usage parameters"
+
+msgid "admin.cli.tool.appKey.options.validate.description"
+msgstr "Validate the current app key if any found in the config file"
+
+msgid "admin.cli.tool.appKey.options.generate.description"
+msgstr "Generate a new app key and replace in the config file. pass with flag --show to only view and --force to overwrite an existing valid app key."
+
+msgid "admin.cli.tool.appKey.mean.those"
+msgstr "Did you mean one of the following?"
+
+msgid "admin.cli.tool.appKey.show"
+msgstr "Generated App Key: {$appKey}"
+
+msgid "admin.cli.tool.appKey.error.missingKeyVariable"
+msgstr "No key variable named `app_key` defined in the `general` section of config file. Please update the config file's general section and add line `app_key = `"
+
+msgid "admin.cli.tool.appKey.error.missingAppKey"
+msgstr "No app key set in the config file ."
+
+msgid "admin.cli.tool.appKey.error.InvalidAppKey"
+msgstr "Invalid app key set, unsupported cipher or incorrect key length. Supported ciphers are: {$ciphers}."
+
+msgid "admin.cli.tool.appKey.warning.replaceValidKey"
+msgstr "A valid APP Key already set in the config file. To overwrite, pass the flag --force with the command."
+
+msgid "admin.cli.tool.appKey.success.writtenToConfig"
+msgstr "App key set successfully in the config file."
+
+msgid "admin.cli.tool.appKey.success.valid"
+msgstr "A valid app key set in the config file"
diff --git a/tools/appKey.php b/tools/appKey.php
index e69de29bb2d..21a0225dac4 100644
--- a/tools/appKey.php
+++ b/tools/appKey.php
@@ -0,0 +1,188 @@
+ 'admin.cli.tool.appKey.options.validate.description',
+ 'generate' => 'admin.cli.tool.appKey.options.generate.description',
+ 'usage' => 'admin.cli.tool.appKey.options.usage.description',
+ ];
+
+ /**
+ * Which option will be call?
+ */
+ protected ?string $option;
+
+ /**
+ * Constructor
+ */
+ public function __construct($argv = [])
+ {
+ parent::__construct($argv);
+
+ array_shift($argv); // Shift the tool name off the top
+
+ $this->setParameterList($argv);
+
+ if (!isset($this->getParameterList()[0])) {
+ throw new CommandNotFoundException(
+ __('admin.cli.tool.jobs.empty.option'),
+ array_keys(self::AVAILABLE_OPTIONS)
+ );
+ }
+
+ $this->option = $this->getParameterList()[0];
+
+ $this->setCommandInterface();
+ }
+
+ /**
+ * Parse and execute the command
+ */
+ public function execute()
+ {
+ if (!isset(self::AVAILABLE_OPTIONS[$this->option])) {
+ throw new CommandNotFoundException(
+ __('admin.cli.tool.jobs.option.doesnt.exists', ['option' => $this->option]),
+ array_keys(self::AVAILABLE_OPTIONS)
+ );
+ }
+
+ $this->{$this->option}();
+ }
+
+ /**
+ * Print command usage information.
+ */
+ public function usage(): void
+ {
+ $this->getCommandInterface()->line('' . __('admin.cli.tool.usage.title') . '');
+ $this->getCommandInterface()->line(__('admin.cli.tool.usage.parameters') . PHP_EOL);
+ $this->getCommandInterface()->line('' . __('admin.cli.tool.available.commands', ['namespace' => 'appKey']) . '');
+
+ $this->printCommandList(self::AVAILABLE_OPTIONS);
+ }
+
+ /**
+ * Generate the app key and write in the config file
+ */
+ protected function generate(): void
+ {
+ $output = $this->getCommandInterface()->getOutput();
+
+ try {
+ $appKey = PKPAppKey::generate();
+ } catch (Throwable $exception) {
+ $output->error($exception->getMessage());
+ return;
+ }
+
+ if ($this->hasFlagSet('--show')) {
+ $output->info(__('admin.cli.tool.appKey.show', ['appKey' => $appKey]));
+ return;
+ }
+
+ if (!PKPAppKey::hasKeyVariable()) {
+ $output->error(__('admin.cli.tool.appKey.error.missingKeyVariable'));
+ return;
+ }
+
+ if ((PKPAppKey::hasKey() && PKPAppKey::validate(PKPAppKey::getKey())) &&
+ !$this->hasFlagSet('--force')) {
+
+ $output->warning(__('admin.cli.tool.appKey.warning.replaceValidKey'));
+ return;
+ }
+
+ try {
+ PKPAppKey::writeToConfig($appKey);
+ $output->success(__('admin.cli.tool.appKey.success.writtenToConfig'));
+ } catch (Throwable $exception) {
+ $this->getCommandInterface()->getOutput()->error($exception->getMessage());
+ } finally {
+ return;
+ }
+ }
+
+ /**
+ * Validate the app key from config file
+ */
+ protected function validate(): void
+ {
+ $output = $this->getCommandInterface()->getOutput();
+
+ if (!PKPAppKey::hasKeyVariable()) {
+ $output->error(__('admin.cli.tool.appKey.error.missingKeyVariable'));
+ return;
+ }
+
+ if (!PKPAppKey::hasKey()) {
+ $output->error(__('admin.cli.tool.appKey.error.missingAppKey'));
+ return;
+ }
+
+ if (!PKPAppKey::validate(PKPAppKey::getKey())) {
+ $output->error(__('admin.cli.tool.appKey.error.InvalidAppKey', [
+ 'ciphers' => implode(', ', array_keys(PKPAppKey::getSupportedCiphers()))
+ ]));
+ return;
+ }
+
+ $output->success(__('admin.cli.tool.appKey.success.valid'));
+ }
+}
+
+try {
+ $tool = new CommandAppKey($argv ?? []);
+ $tool->execute();
+} catch (\Throwable $exception) {
+ $output = new \PKP\cliTool\CommandInterface;
+
+ if ($exception instanceof CommandInvalidArgumentException) {
+ $output->errorBlock([$exception->getMessage()]);
+
+ return;
+ }
+
+ if ($exception instanceof CommandNotFoundException) {
+ $alternatives = $exception->getAlternatives();
+
+ $message = __('admin.cli.tool.appKey.mean.those') . PHP_EOL . implode(PHP_EOL, $alternatives);
+
+ $output->errorBlock([$exception->getMessage(), $message]);
+
+ return;
+ }
+
+ throw $exception;
+}
diff --git a/tools/jobs.php b/tools/jobs.php
index 57b402d4fa0..aad8ba8129f 100644
--- a/tools/jobs.php
+++ b/tools/jobs.php
@@ -1,7 +1,5 @@
setOutput($output);
- }
-
- public function errorBlock(array $messages = [], ?string $title = null): void
- {
- $this->getOutput()->block(
- $messages,
- $title,
- 'fg=white;bg=red',
- ' ',
- true
- );
- }
-}
-
class commandJobs extends CommandLineTool
{
+ use HasParameterList;
+ use HasCommandInterface;
+
protected const AVAILABLE_OPTIONS = [
'list' => 'admin.cli.tool.jobs.available.options.list.description',
'purge' => 'admin.cli.tool.jobs.available.options.purge.description',
@@ -96,16 +68,6 @@ class commandJobs extends CommandLineTool
*/
protected $option = null;
- /**
- * @var null|array Parameters and arguments from CLI
- */
- protected $parameterList = null;
-
- /**
- * CLI interface, this object should extends InteractsWithIO
- */
- protected $commandInterface = null;
-
/**
* Constructor
*/
@@ -126,68 +88,7 @@ public function __construct($argv = [])
$this->option = $this->getParameterList()[0];
- $this->setCommandInterface(new commandInterface());
- }
-
- public function setCommandInterface(commandInterface $commandInterface): self
- {
- $this->commandInterface = $commandInterface;
-
- return $this;
- }
-
- public function getCommandInterface(): commandInterface
- {
- return $this->commandInterface;
- }
-
- /**
- * Save the parameter list passed on CLI
- *
- * @param array $items Array with parameters and arguments passed on CLI
- *
- */
- public function setParameterList(array $items): self
- {
- $parameters = [];
-
- foreach ($items as $param) {
- if (strpos($param, '=')) {
- [$key, $value] = explode('=', ltrim($param, '-'));
- $parameters[$key] = $value;
-
- continue;
- }
-
- $parameters[] = $param;
- }
-
- $this->parameterList = $parameters;
-
- return $this;
- }
-
- /**
- * Get the parameter list passed on CLI
- *
- */
- public function getParameterList(): ?array
- {
- return $this->parameterList;
- }
-
- /**
- * Get the value of a specific parameter
- *
- *
- */
- protected function getParameterValue(string $parameter, mixed $default = null): mixed
- {
- if (!isset($this->getParameterList()[$parameter])) {
- return $default;
- }
-
- return $this->getParameterList()[$parameter];
+ $this->setCommandInterface();
}
/**
@@ -199,7 +100,7 @@ public function usage()
$this->getCommandInterface()->line(__('admin.cli.tool.usage.parameters') . PHP_EOL);
$this->getCommandInterface()->line('' . __('admin.cli.tool.available.commands', ['namespace' => 'jobs']) . '');
- $this->printUsage(self::AVAILABLE_OPTIONS);
+ $this->printCommandList(self::AVAILABLE_OPTIONS);
}
/**
@@ -210,20 +111,6 @@ public function help(): void
$this->usage();
}
- /**
- * Retrieve the columnWidth based on the commands text size
- */
- protected function getColumnWidth(array $commands): int
- {
- $widths = [];
-
- foreach ($commands as $command) {
- $widths[] = Helper::width($command);
- }
-
- return $widths ? max($widths) + 2 : 0;
- }
-
/**
* Failed jobs list/redispatch/remove
*/
@@ -632,27 +519,7 @@ protected function workerOptionsHelp(): void
'--test' => __('admin.cli.tool.jobs.work.option.test.description'),
];
- $this->printUsage($options, false);
- }
-
- /**
- * Print given options in a pretty way.
- */
- protected function printUsage(array $options, bool $shouldTranslate = true): void
- {
- $width = $this->getColumnWidth(array_keys($options));
-
- foreach ($options as $commandName => $description) {
- $spacingWidth = $width - Helper::width($commandName);
- $this->getCommandInterface()->line(
- sprintf(
- ' %s%s%s',
- $commandName,
- str_repeat(' ', $spacingWidth),
- $shouldTranslate ? __($description) : $description
- )
- );
- }
+ $this->printCommandList($options, false);
}
/**
@@ -697,7 +564,7 @@ public function execute()
$tool = new commandJobs($argv ?? []);
$tool->execute();
} catch (Throwable $e) {
- $output = new commandInterface();
+ $output = new \PKP\cliTool\CommandInterface;
if ($e instanceof CommandInvalidArgumentException) {
$output->errorBlock([$e->getMessage()]);