Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Changelog

5.1.0 - 2026-07-13
- Added code to encrypt sensitive values in database to improve security

5.0.4 - 2026-01-05
- Updated Api timeout to 5 seconds and changed log levels

Expand Down
60 changes: 60 additions & 0 deletions Configuration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/

namespace Piwik\Plugins\Slack;

use Piwik\Config;

class Configuration
{
public const SECTION_NAME = 'Slack';
public const KEY_ENCRYPTION_KEY = 'encryption_key';

public function install(): void
{
$this->getOrCreateEncryptionKey();
}

public function getEncryptionKey(): string
{
$config = $this->getConfig();

return (string) ($config->{self::SECTION_NAME}[self::KEY_ENCRYPTION_KEY] ?? '');
}

public function getOrCreateEncryptionKey(): string
{
$key = $this->getEncryptionKey();

if ($key !== '') {
return $key;
}

$key = base64_encode(random_bytes(32));
$this->setEncryptionKey($key);

return $key;
}

public function setEncryptionKey(
#[\SensitiveParameter]
string $key
): void {
$config = $this->getConfig();
$pluginConfig = $config->{self::SECTION_NAME} ?: [];
$pluginConfig[self::KEY_ENCRYPTION_KEY] = $key;
$config->{self::SECTION_NAME} = $pluginConfig;
$config->forceSave();
}

private function getConfig(): Config
{
return Config::getInstance();
}
}
119 changes: 119 additions & 0 deletions Encryption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/

namespace Piwik\Plugins\Slack;

use Piwik\Piwik;
use Piwik\Plugins\Slack\Exceptions\SecretConfigurationException;

class Encryption
{
public const ENCRYPTED_PREFIX = 'enc:v1:';
private const CIPHER = 'AES-256-CBC';

/**
* @var Configuration
*/
private $configuration;

public function __construct(?Configuration $configuration = null)
{
$this->configuration = $configuration ?: new Configuration();
}

public function isEncrypted($value): bool
{
return is_string($value) && strpos($value, self::ENCRYPTED_PREFIX) === 0;
}

public function encryptString(
#[\SensitiveParameter]
string $value
): string {
if (!extension_loaded('openssl')) {
throw new SecretConfigurationException(Piwik::translate('Slack_EncryptionOpenSslRequired'));
}

$key = $this->getEncryptionKey(true);
$ivLength = (int) openssl_cipher_iv_length(self::CIPHER);
$iv = random_bytes($ivLength);
$ciphertext = openssl_encrypt($value, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv);

if (!is_string($ciphertext)) {
throw new SecretConfigurationException(Piwik::translate('Slack_EncryptionFailed'));
}

$payload = [
'iv' => base64_encode($iv),
'value' => base64_encode($ciphertext),
'mac' => base64_encode(hash_hmac('sha256', self::ENCRYPTED_PREFIX . $iv . $ciphertext, $key, true)),
];

return self::ENCRYPTED_PREFIX . base64_encode(json_encode($payload));
}

public function decryptString(
#[\SensitiveParameter]
string $value
): string {
if (!$this->isEncrypted($value)) {
return $value;
}

if (!extension_loaded('openssl')) {
throw new SecretConfigurationException(Piwik::translate('Slack_EncryptionOpenSslRequired'));
}

$key = $this->getEncryptionKey(false);
$encodedPayload = substr($value, strlen(self::ENCRYPTED_PREFIX));
$payload = json_decode(base64_decode($encodedPayload, true) ?: '', true);

if (empty($payload['iv']) || empty($payload['value']) || empty($payload['mac'])) {
throw new SecretConfigurationException($this->getInvalidKeyMessage());
}

$iv = base64_decode($payload['iv'], true);
$ciphertext = base64_decode($payload['value'], true);
$mac = base64_decode($payload['mac'], true);

if (!is_string($iv) || !is_string($ciphertext) || !is_string($mac)) {
throw new SecretConfigurationException($this->getInvalidKeyMessage());
}

$expectedMac = hash_hmac('sha256', self::ENCRYPTED_PREFIX . $iv . $ciphertext, $key, true);
if (!hash_equals($expectedMac, $mac)) {
throw new SecretConfigurationException($this->getInvalidKeyMessage());
}

$plaintext = openssl_decrypt($ciphertext, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv);
if (!is_string($plaintext)) {
throw new SecretConfigurationException($this->getInvalidKeyMessage());
}

return $plaintext;
}

private function getEncryptionKey(bool $createIfMissing): string
{
$key = $createIfMissing
? $this->configuration->getOrCreateEncryptionKey()
: $this->configuration->getEncryptionKey();

if ($key === '') {
throw new SecretConfigurationException($this->getInvalidKeyMessage());
}

return hash('sha256', $key, true);
}

private function getInvalidKeyMessage(): string
{
return Piwik::translate('Slack_EncryptionKeyInvalid');
}
}
14 changes: 14 additions & 0 deletions Exceptions/SecretConfigurationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/

namespace Piwik\Plugins\Slack\Exceptions;

class SecretConfigurationException extends \Exception
{
}
86 changes: 86 additions & 0 deletions Settings/EncryptedSlackOauthTokenSetting.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/

namespace Piwik\Plugins\Slack\Settings;

use Piwik\Log;
use Piwik\Plugins\Slack\Encryption;
use Piwik\Plugins\Slack\Exceptions\SecretConfigurationException;
use Piwik\Settings\Plugin\SystemSetting;
use Piwik\Settings\Storage\Backend\PluginSettingsTable;

class EncryptedSlackOauthTokenSetting extends SystemSetting
{
/**
* @var Encryption
*/
private $encryption;

public function __construct($name, $defaultValue, $type, $pluginName, ?Encryption $encryption = null)
{
parent::__construct($name, $defaultValue, $type, $pluginName);
$this->encryption = $encryption ?: new Encryption();
}

public function getValue()
{
$value = parent::getValue();

if (!is_string($value) || $value === '') {
return $value;
}

try {
return $this->encryption->decryptString($value);
} catch (SecretConfigurationException $e) {
Log::error('[Slack] ' . $e->getMessage());
return $this->getDefaultValue();
}
}

public function setValue($value)
{
if (($value === '' || !is_string($value)) && $this->hasUndecryptableValue()) {
return;
}

parent::setValue($value);
$normalizedValue = parent::getValue();

if (!is_string($normalizedValue) || $normalizedValue === '' || $this->encryption->isEncrypted($normalizedValue)) {
return;
}

$encryptedValue = $this->encryption->encryptString($normalizedValue);
$this->storage->setValue($this->name, $encryptedValue);
$backend = $this->storage->getBackend();
if ($backend instanceof PluginSettingsTable && method_exists($backend, 'saveValue')) {
$backend->saveValue($this->name, $encryptedValue);
return;
}

$this->storage->save();
}

private function hasUndecryptableValue(): bool
{
$stored = parent::getValue();

if (!is_string($stored) || !$this->encryption->isEncrypted($stored)) {
return false;
}

try {
$this->encryption->decryptString($stored);
return false;
} catch (SecretConfigurationException $e) {
return true;
}
}
}
7 changes: 6 additions & 1 deletion SystemSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Piwik\Piwik;
use Piwik\Settings\Setting;
use Piwik\Settings\FieldConfig;
use Piwik\Plugins\Slack\Settings\EncryptedSlackOauthTokenSetting;
use Piwik\Url;

class SystemSettings extends \Piwik\Settings\Plugin\SystemSettings
Expand All @@ -27,7 +28,8 @@ protected function init()

private function createSlackOauthTokenSetting()
{
return $this->makeSetting('slackOauthToken', $default = '', FieldConfig::TYPE_STRING, function (FieldConfig $field) {
$setting = new EncryptedSlackOauthTokenSetting('slackOauthToken', $default = '', FieldConfig::TYPE_STRING, $this->pluginName);
$setting->setConfigureCallback(function (FieldConfig $field) {
$field->title = Piwik::translate('Slack_OauthTokenSettingTitle');
$field->uiControl = FieldConfig::UI_CONTROL_PASSWORD;
$link = Url::addCampaignParametersToMatomoLink('https://matomo.org/faq/reports/how-to-get-the-slack-oauth-token-for-matomo-integration/', null, null, 'App.SystemSettings.Slack') . '#step-1-get-a-slack-oauth-token';
Expand All @@ -36,5 +38,8 @@ private function createSlackOauthTokenSetting()
return trim($value);
};
});
$this->addSetting($setting);

return $setting;
}
}
44 changes: 44 additions & 0 deletions Updates/5.1.0.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/

namespace Piwik\Plugins\Slack;

use Piwik\Common;
use Piwik\Db;
use Piwik\Updater;
use Piwik\Updates as PiwikUpdates;

class Updates_5_1_0 extends PiwikUpdates
{
public function doUpdate(Updater $updater)
{
$configuration = new Configuration();
$configuration->install();

$encryption = new Encryption($configuration);
$table = Common::prefixTable('plugin_setting');
$rows = Db::fetchAll(
'SELECT `setting_value` FROM ' . $table . ' WHERE `plugin_name` = ? AND `user_login` = ? AND `setting_name` = ?',
['Slack', '', 'slackOauthToken']
);

foreach ($rows as $row) {
$value = $row['setting_value'] ?? '';

if (!is_string($value) || $value === '' || $encryption->isEncrypted($value)) {
continue;
}

Db::query(
'UPDATE ' . $table . ' SET `setting_value` = ? WHERE `plugin_name` = ? AND `user_login` = ? AND `setting_name` = ? AND `setting_value` = ?',
[$encryption->encryptString($value), 'Slack', '', 'slackOauthToken', $value]
);
}
}
}
3 changes: 3 additions & 0 deletions lang/en.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"Slack": {
"ChannelId": "Slack Channel ID",
"EncryptionFailed": "Failed to encrypt the Slack credential.",
"EncryptionKeyInvalid": "Slack encryption key is missing or invalid.",
"EncryptionOpenSslRequired": "OpenSSL is required to encrypt or decrypt Slack credentials.",
"NoOauthTokenAdded": "Please add your Slack OAuth Token %1$shere%2$s.",
"OauthTokenRequiredErrorMessage": "To enable Slack, provide your Slack OAuth Token in General settings > Slack.",
"OauthTokenSettingTitle": "Slack OAuth Token",
Expand Down
4 changes: 2 additions & 2 deletions plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "Slack",
"description": "Send Matomo reports and alerts to Slack channels, keeping your team informed and ready to act in real time.",
"version": "5.0.4",
"version": "5.1.0",
"theme": false,
"require": {
"matomo": ">=5.0.0,<6.0.0-b1"
Expand All @@ -28,4 +28,4 @@
"CustomAlerts"
],
"category": "integration"
}
}
Loading
Loading