new: [security] Store authkeys for servers encrypted

pull/7938/head
Jakub Onderka 2021-11-07 16:58:42 +01:00
parent d355f0f6d5
commit 5569d7d2bf
6 changed files with 185 additions and 77 deletions

View File

@ -0,0 +1,68 @@
<?php
App::uses('BetterSecurity', 'Tools');
/**
* Class for ondemand encryption of JSON serialized value
*/
class EncryptedValue implements JsonSerializable
{
const ENCRYPTED_MAGIC = "\x1F\x1D";
/** @var string */
private $value;
/** @var bool */
private $isJson;
public function __construct($value, $isJson = false)
{
$this->value = $value;
$this->isJson = $isJson;
}
/**
* @return mixed
* @throws JsonException
* @throws Exception
*/
public function decrypt()
{
$decrypt = BetterSecurity::decrypt(substr($this->value, 2), Configure::read('Security.encryption_key'));
return $this->isJson ? JsonTool::decode($decrypt) : $decrypt;
}
public function __toString()
{
return $this->decrypt();
}
public function jsonSerialize()
{
return $this->decrypt();
}
/**
* Encrypt provided string if encryption is enabled. If not enabled, input value will be returned.
* @param string $value
* @return string
* @throws Exception
*/
public static function encryptIfEnabled($value)
{
$key = Configure::read('Security.encryption_key');
if ($key) {
return EncryptedValue::ENCRYPTED_MAGIC . BetterSecurity::encrypt($value, $key);
}
return $value;
}
/**
* Check if value is encrypted (starts with encrypted magic)
* @param string $value
* @return bool
*/
public static function isEncrypted($value)
{
return substr($value, 0, 2) === EncryptedValue::ENCRYPTED_MAGIC;
}
}

View File

@ -1597,6 +1597,8 @@ class AppModel extends Model
`value` blob NOT NULL,
PRIMARY KEY (`setting`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;";
$sqlArray[] = "ALTER TABLE `servers` MODIFY COLUMN `authkey` VARBINARY(255) NOT NULL;";
$sqlArray[] = "ALTER TABLE `cerebrates` MODIFY COLUMN `authkey` VARBINARY(255) NOT NULL;";
break;
case 'fixNonEmptySharingGroupID':
$sqlArray[] = 'UPDATE `events` SET `sharing_group_id` = 0 WHERE `distribution` != 4;';
@ -2683,15 +2685,21 @@ class AppModel extends Model
{
$version = implode('.', $this->checkMISPVersion());
$commit = $this->checkMIPSCommit();
$request = array(
$authkey = $server[$model]['authkey'];
App::uses('EncryptedValue', 'Tools');
if (EncryptedValue::isEncrypted($authkey)) {
$authkey = (string)new EncryptedValue($authkey);
}
return array(
'header' => array(
'Authorization' => $server[$model]['authkey'],
'Authorization' => $authkey,
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'User-Agent' => 'MISP ' . $version . (empty($commit) ? '' : ' - #' . $commit),
)
);
return $request;
}
/**

View File

@ -1,6 +1,6 @@
<?php
App::uses('AppModel', 'Model');
App::uses('EncryptedValue', 'Tools');
class Cerebrate extends AppModel
{
@ -16,12 +16,23 @@ class Cerebrate extends AppModel
public $belongsTo = array(
'Organisation' => array(
'className' => 'Organisation',
'foreignKey' => 'org_id'
'className' => 'Organisation',
'foreignKey' => 'org_id'
)
);
public function queryInstance($options) {
public function beforeSave($options = array())
{
$cerebrate = &$this->data['Server'];
// Encrypt authkey if plain key provided and encryption is enabled
if (!empty($cerebrate['authkey']) && strlen($cerebrate['authkey']) === 40) {
$cerebrate['authkey'] = EncryptedValue::encryptIfEnabled($cerebrate['authkey']);
}
return true;
}
public function queryInstance($options)
{
$url = $options['cerebrate']['Cerebrate']['url'] . $options['path'];
$url_params = [];
@ -413,4 +424,30 @@ class Cerebrate extends AppModel
}
return __('The retrieved data isn\'t a valid sharing group.');
}
/**
* @param string|null $old Old (or current) encryption key.
* @param string|null $new New encryption key. If empty, encrypted values will be decrypted.
* @throws Exception
*/
public function reencryptAuthKeys($old, $new)
{
$cerebrates = $this->find('list', [
'fields' => ['Cerebrate.id', 'Cerebrate.authkey'],
]);
$toSave = [];
foreach ($cerebrates as $id => $authkey) {
if (EncryptedValue::isEncrypted($authkey)) {
$authkey = BetterSecurity::decrypt(substr($authkey, 2), $old);
}
if (!empty($new)) {
$authkey = EncryptedValue::ENCRYPTED_MAGIC . BetterSecurity::encrypt($authkey, $new);
}
$toSave[] = ['Cerebrate' => [
'id' => $id,
'authkey' => $authkey,
]];
}
return $this->saveMany($toSave);
}
}

View File

@ -3,6 +3,7 @@ App::uses('AppModel', 'Model');
App::uses('GpgTool', 'Tools');
App::uses('ServerSyncTool', 'Tools');
App::uses('SystemSetting', 'Model');
App::uses('EncryptedValue', 'Tools');
/**
* @property-read array $serverSettings
@ -165,8 +166,11 @@ class Server extends AppModel
public function beforeSave($options = array())
{
$this->data['Server']['url'] = rtrim($this->data['Server']['url'], '/');
if (empty($this->data['Server']['id'])) {
$server = &$this->data['Server'];
if (!empty($server['url'])) {
$server['url'] = rtrim($server['url'], '/');
}
if (empty($server['id'])) {
$max_prio = $this->find('first', array(
'recursive' => -1,
'order' => array('Server.priority' => 'DESC'),
@ -177,7 +181,11 @@ class Server extends AppModel
} else {
$max_prio = $max_prio['Server']['priority'];
}
$this->data['Server']['priority'] = $max_prio + 1;
$server['priority'] = $max_prio + 1;
}
// Encrypt authkey if plain key provided and encryption is enabled
if (!empty($server['authkey']) && strlen($server['authkey']) === 40) {
$server['authkey'] = EncryptedValue::encryptIfEnabled($server['authkey']);
}
return true;
}
@ -4522,6 +4530,32 @@ class Server extends AppModel
];
}
/**
* @param string|null $old Old (or current) encryption key.
* @param string|null $new New encryption key. If empty, encrypted values will be decrypted.
* @throws Exception
*/
public function reencryptAuthKeys($old, $new)
{
$servers = $this->find('list', [
'fields' => ['Server.id', 'Server.authkey'],
]);
$toSave = [];
foreach ($servers as $id => $authkey) {
if (EncryptedValue::isEncrypted($authkey)) {
$authkey = BetterSecurity::decrypt(substr($authkey, 2), $old);
}
if (!empty($new)) {
$authkey = EncryptedValue::ENCRYPTED_MAGIC . BetterSecurity::encrypt($authkey, $new);
}
$toSave[] = ['Server' => [
'id' => $id,
'authkey' => $authkey,
]];
}
return $this->saveMany($toSave);
}
/**
* Generate just when required
* @return array[]
@ -6001,6 +6035,12 @@ class Server extends AppModel
/** @var SystemSetting $systemSetting */
$systemSetting = ClassRegistry::init('SystemSetting');
$systemSetting->reencrypt($old, $new);
$this->reencryptAuthKeys($old, $new);
/** @var Cerebrate $cerebrate */
$cerebrate = ClassRegistry::init('Cerebrate');
$cerebrate->reencryptAuthKeys($old, $new);
return true;
},
'type' => 'string',

View File

@ -1,45 +1,8 @@
<?php
App::uses('AppModel', 'Model');
App::uses('JsonTool', 'Tools');
App::uses('EncryptedValue', 'Tools');
App::uses('BetterSecurity', 'Tools');
/**
* Class for ondemand encryption of JSON serialized value
*/
class EncryptedValue implements JsonSerializable
{
const ENCRYPTED_MAGIC = "\x1F\x1D";
/** @var string */
private $value;
public function __construct($value)
{
$this->value = $value;
}
/**
* @return mixed
* @throws JsonException
* @throws Exception
*/
public function decrypt()
{
$decrypt = BetterSecurity::decrypt($this->value, Configure::read('Security.encryption_key'));
return JsonTool::decode($decrypt);
}
public function __toString()
{
return $this->decrypt();
}
public function jsonSerialize()
{
return $this->decrypt();
}
}
class SystemSetting extends AppModel
{
public $actsAs = [
@ -91,13 +54,20 @@ class SystemSetting extends AppModel
/**
* @return array
* @throws JsonException
*/
public function getSettings()
{
$settings = $this->find('list', [
'fields' => ['SystemSetting.setting', 'SystemSetting.value'],
]);
return array_map([$this, 'decode'], $settings);
return array_map(function ($value) {
if (EncryptedValue::isEncrypted($value)) {
return new EncryptedValue($value, true);
} else {
return JsonTool::decode($value);
}
}, $settings);
}
/**
@ -122,9 +92,8 @@ class SystemSetting extends AppModel
$value = JsonTool::encode($value);
// If encryption is enabled and setting name contains `password` or `apikey` string, encrypt value to protect it
$key = Configure::read('Security.encryption_key');
if ($key && self::isSensitive($setting)) {
$value = EncryptedValue::ENCRYPTED_MAGIC . BetterSecurity::encrypt($value, $key);
if (self::isSensitive($setting)) {
$value = EncryptedValue::encryptIfEnabled($value);
}
$valid = $this->save(['SystemSetting' => [
@ -140,7 +109,7 @@ class SystemSetting extends AppModel
/**
* @param string|null $old Old (or current) encryption key.
* @param string|null $new New encryption key. If empty, encrypted values will be decrypted.
* @throws JsonException
* @throws Exception
*/
public function reencrypt($old, $new)
{
@ -152,7 +121,7 @@ class SystemSetting extends AppModel
if (!self::isSensitive($setting)) {
continue;
}
if (substr($value, 0, 2) === EncryptedValue::ENCRYPTED_MAGIC) {
if (EncryptedValue::isEncrypted($value)) {
$value = BetterSecurity::decrypt(substr($value, 2), $old);
}
if (!empty($new)) {
@ -166,20 +135,6 @@ class SystemSetting extends AppModel
return $this->saveMany($toSave);
}
/**
* @param string $value
* @return EncryptedValue|mixed
* @throws JsonException
*/
private function decode($value)
{
if (substr($value, 0, 2) === EncryptedValue::ENCRYPTED_MAGIC) {
return new EncryptedValue(substr($value, 2));
} else {
return JsonTool::decode($value);
}
}
/**
* Sensitive setting are passwords or api keys.
* @param string $setting Setting name

View File

@ -769,12 +769,12 @@
},
{
"column_name": "authkey",
"is_nullable": "YES",
"data_type": "varchar",
"character_maximum_length": "40",
"is_nullable": "NO",
"data_type": "varbinary",
"character_maximum_length": "255",
"numeric_precision": null,
"collation_name": "ascii_general_ci",
"column_type": "varchar(40)",
"collation_name": null,
"column_type": "varbinary(255)",
"column_default": null,
"extra": ""
},
@ -5063,11 +5063,11 @@
{
"column_name": "authkey",
"is_nullable": "NO",
"data_type": "varchar",
"character_maximum_length": "40",
"data_type": "varbinary",
"character_maximum_length": "255",
"numeric_precision": null,
"collation_name": "utf8_bin",
"column_type": "varchar(40)",
"collation_name": null,
"column_type": "varbinary(255)",
"column_default": null,
"extra": ""
},