Skip to content

Commit 1890654

Browse files
cedric-annetrasher
authored andcommitted
Encrypt API clients and users tokens
1 parent 74495d1 commit 1890654

File tree

17 files changed

+405
-77
lines changed

17 files changed

+405
-77
lines changed

.phpstan-baseline.php

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7009,12 +7009,6 @@
70097009
'count' => 1,
70107010
'path' => __DIR__ . '/src/User.php',
70117011
];
7012-
$ignoreErrors[] = [
7013-
'message' => '#^Negated boolean expression is always true\\.$#',
7014-
'identifier' => 'booleanNot.alwaysTrue',
7015-
'count' => 1,
7016-
'path' => __DIR__ . '/src/User.php',
7017-
];
70187012
$ignoreErrors[] = [
70197013
'message' => '#^PHPDoc tag @var with type array is not subtype of native type array\\{\\}\\|array\\{list\\<string\\>, list\\<string\\>\\}\\.$#',
70207014
'identifier' => 'varTag.nativeType',

install/empty_data.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,8 @@ public function getEmptyData(): array
353353
'initialized_rules_collections' => '[]',
354354
'timeline_action_btn_layout' => 0,
355355
'timeline_date_format' => 0,
356+
'are_apiclients_tokens_encrypted' => 1,
357+
'are_users_tokens_encrypted' => 1,
356358
];
357359

358360
$tables['glpi_configs'] = [];
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
/**
4+
* ---------------------------------------------------------------------
5+
*
6+
* GLPI - Gestionnaire Libre de Parc Informatique
7+
*
8+
* http://glpi-project.org
9+
*
10+
* @copyright 2015-2025 Teclib' and contributors.
11+
* @licence https://www.gnu.org/licenses/gpl-3.0.html
12+
*
13+
* ---------------------------------------------------------------------
14+
*
15+
* LICENSE
16+
*
17+
* This file is part of GLPI.
18+
*
19+
* This program is free software: you can redistribute it and/or modify
20+
* it under the terms of the GNU General Public License as published by
21+
* the Free Software Foundation, either version 3 of the License, or
22+
* (at your option) any later version.
23+
*
24+
* This program is distributed in the hope that it will be useful,
25+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
26+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27+
* GNU General Public License for more details.
28+
*
29+
* You should have received a copy of the GNU General Public License
30+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
31+
*
32+
* ---------------------------------------------------------------------
33+
*/
34+
35+
/**
36+
* Update from 10.0.20 to 10.0.21
37+
*
38+
* @return bool for success (will die for most error)
39+
**/
40+
function update10020to10021()
41+
{
42+
/**
43+
* @var \DBmysql $DB
44+
* @var \Migration $migration
45+
*/
46+
global $DB, $migration;
47+
48+
$updateresult = true;
49+
$ADDTODISPLAYPREF = [];
50+
$DELFROMDISPLAYPREF = [];
51+
$update_dir = __DIR__ . '/update_10.0.20_to_10.0.21/';
52+
53+
//TRANS: %s is the number of new version
54+
$migration->displayTitle(sprintf(__('Update to %s'), '10.0.21'));
55+
$migration->setVersion('10.0.21');
56+
57+
$update_scripts = scandir($update_dir);
58+
foreach ($update_scripts as $update_script) {
59+
if (preg_match('/\.php$/', $update_script) !== 1) {
60+
continue;
61+
}
62+
require $update_dir . $update_script;
63+
}
64+
65+
// ************ Keep it at the end **************
66+
$migration->updateDisplayPrefs($ADDTODISPLAYPREF, $DELFROMDISPLAYPREF);
67+
68+
$migration->executeMigration();
69+
70+
return $updateresult;
71+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<?php
2+
3+
/**
4+
* ---------------------------------------------------------------------
5+
*
6+
* GLPI - Gestionnaire Libre de Parc Informatique
7+
*
8+
* http://glpi-project.org
9+
*
10+
* @copyright 2015-2025 Teclib' and contributors.
11+
* @licence https://www.gnu.org/licenses/gpl-3.0.html
12+
*
13+
* ---------------------------------------------------------------------
14+
*
15+
* LICENSE
16+
*
17+
* This file is part of GLPI.
18+
*
19+
* This program is free software: you can redistribute it and/or modify
20+
* it under the terms of the GNU General Public License as published by
21+
* the Free Software Foundation, either version 3 of the License, or
22+
* (at your option) any later version.
23+
*
24+
* This program is distributed in the hope that it will be useful,
25+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
26+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27+
* GNU General Public License for more details.
28+
*
29+
* You should have received a copy of the GNU General Public License
30+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
31+
*
32+
* ---------------------------------------------------------------------
33+
*/
34+
35+
/**
36+
* @var \DBmysql $DB
37+
* @var \Migration $migration
38+
*/
39+
40+
$glpi_key_manager = new GLPIKey();
41+
$use_legacy_key = $glpi_key_manager->keyExists() === false;
42+
43+
if ($use_legacy_key) {
44+
$glpi_key = $glpi_key_manager->getLegacyKey();
45+
$encrypt_fct = function (string $value) use ($glpi_key): string {
46+
// Code corresponding to encryption used prior to GLPI 9.5 (copied from `Toolbox::encrypt()`).
47+
// /!\ It is mandatory to encrypt data using the legacy key to handle migrations from a GLPI version < 9.5.0.
48+
// Data will be re-encrypted at the update process when a new key will be generated.
49+
$result = '';
50+
$strlen = strlen($value);
51+
for ($i = 0; $i < $strlen; $i++) {
52+
$char = substr($value, $i, 1);
53+
$keychar = substr($glpi_key, ($i % strlen($glpi_key)) - 1, 1);
54+
$char = chr(ord($char) + ord($keychar));
55+
$result .= $char;
56+
}
57+
return base64_encode($result);
58+
};
59+
} else {
60+
$glpi_key = $glpi_key_manager->get();
61+
$encrypt_fct = fn(string $value) => $glpi_key_manager->encrypt($value);
62+
}
63+
64+
if ($glpi_key === null) {
65+
// If `$glpi_key` is `null`, it means tha the key file exists but an error occurs while reading it.
66+
// It is preferable to fail here rather than ruin all tokens values in database.
67+
throw new RuntimeException('Unable to get the GLPI encryption key value.');
68+
}
69+
70+
// Encrypt API clients tokens
71+
$are_apiclients_tokens_encrypted = $DB->request([
72+
'FROM' => 'glpi_configs',
73+
'WHERE' => [
74+
'name' => 'are_apiclients_tokens_encrypted',
75+
'context' => 'core',
76+
],
77+
])->current()['value'] ?? false;
78+
79+
if ((bool) $are_apiclients_tokens_encrypted === false) {
80+
$api_clients_iterator = $DB->request([
81+
'FROM' => 'glpi_apiclients',
82+
]);
83+
84+
foreach ($api_clients_iterator as $api_client_data) {
85+
if (empty($api_client_data['app_token'])) {
86+
continue;
87+
}
88+
89+
$migration->addPostQuery(
90+
$DB->buildUpdate(
91+
'glpi_apiclients',
92+
[
93+
'app_token' => $encrypt_fct($api_client_data['app_token']),
94+
],
95+
[
96+
'id' => $api_client_data['id'],
97+
]
98+
)
99+
);
100+
}
101+
102+
$migration->addConfig(['are_apiclients_tokens_encrypted' => 1]);
103+
}
104+
105+
// Encrypt users tokens
106+
$are_users_tokens_encrypted = $DB->request([
107+
'FROM' => 'glpi_configs',
108+
'WHERE' => [
109+
'name' => 'are_users_tokens_encrypted',
110+
'context' => 'core',
111+
],
112+
])->current()['value'] ?? false;
113+
114+
if ((bool) $are_users_tokens_encrypted === false) {
115+
$migration->changeField('glpi_users', 'password_forget_token', 'password_forget_token', 'string'); // change length from 40 to 255
116+
117+
$users_iterator = $DB->request([
118+
'FROM' => 'glpi_users',
119+
]);
120+
121+
foreach ($users_iterator as $user_data) {
122+
$update_input = [];
123+
124+
foreach (['password_forget_token', 'personal_token', 'api_token', 'cookie_token'] as $token_field) {
125+
if (!empty($user_data[$token_field])) {
126+
$update_input[$token_field] = $encrypt_fct($user_data[$token_field]);
127+
}
128+
}
129+
130+
if ($update_input === []) {
131+
continue;
132+
}
133+
134+
$migration->addPostQuery(
135+
$DB->buildUpdate(
136+
'glpi_users',
137+
$update_input,
138+
[
139+
'id' => $user_data['id'],
140+
]
141+
)
142+
);
143+
}
144+
145+
$migration->addConfig(['are_users_tokens_encrypted' => 1]);
146+
}

install/mysql/glpi-empty.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7610,7 +7610,7 @@ CREATE TABLE `glpi_users` (
76107610
`followup_private` tinyint DEFAULT NULL,
76117611
`task_private` tinyint DEFAULT NULL,
76127612
`default_requesttypes_id` int unsigned DEFAULT NULL,
7613-
`password_forget_token` char(40) DEFAULT NULL,
7613+
`password_forget_token` varchar(255) DEFAULT NULL,
76147614
`password_forget_token_date` timestamp NULL DEFAULT NULL,
76157615
`user_dn` text,
76167616
`user_dn_hash` varchar(32),

phpunit/APIBaseClass.php

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,7 @@ public function testInitSessionUserToken()
8989
$user = new User();
9090
$uid = getItemByTypeName('User', TU_USER, true);
9191
$user->getFromDB($uid);
92-
$token = $user->fields['api_token'] ?? "";
93-
if (empty($token)) {
94-
$token = $user->getAuthToken('api_token');
95-
}
92+
$token = $user->getAuthToken('api_token');
9693

9794
$data = $this->query(
9895
'initSession',
@@ -120,8 +117,10 @@ public function testAppToken()
120117
])
121118
);
122119

123-
$app_token = $apiclient->fields['app_token'];
124-
$this->assertNotEmpty($app_token);
120+
$encrypted_app_token = $apiclient->fields['app_token'];
121+
$this->assertNotEmpty($encrypted_app_token);
122+
123+
$app_token = (new GLPIKey())->decrypt($encrypted_app_token);
125124
$this->assertSame(40, strlen($app_token));
126125

127126
// test valid app token -> expect ok session
@@ -1667,7 +1666,11 @@ public function testLostPasswordRequest()
16671666

16681667
// get the password recovery token
16691668
$user = getItemByTypeName('User', TU_USER);
1670-
$token = $user->fields['password_forget_token'];
1669+
1670+
$encrypted_token = $user->fields['password_forget_token'];
1671+
$this->assertNotEmpty($encrypted_token);
1672+
1673+
$token = (new \GLPIKey())->decrypt($encrypted_token);
16711674
$this->assertNotEmpty($token);
16721675

16731676
// Test reset password with a bad token

phpunit/functional/GLPIKeyTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,10 +450,15 @@ public function testGetFields()
450450

451451
$this->assertEquals(
452452
[
453+
'glpi_apiclients.app_token',
453454
'glpi_authldaps.rootdn_passwd',
454455
'glpi_mailcollectors.passwd',
455456
'glpi_snmpcredentials.auth_passphrase',
456457
'glpi_snmpcredentials.priv_passphrase',
458+
'glpi_users.api_token',
459+
'glpi_users.cookie_token',
460+
'glpi_users.password_forget_token',
461+
'glpi_users.personal_token',
457462
'glpi_plugin_myplugin_remote.key',
458463
'glpi_plugin_myplugin_remote.secret',
459464
'glpi_plugin_anotherplugin_link.pass',

phpunit/functional/UserTest.php

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public function testGenerateUserToken()
5656
$this->assertNotEmpty($token);
5757

5858
$user->getFromDB($user->getID());
59-
$this->assertSame($token, $user->fields['personal_token']);
59+
$this->assertSame($token, (new \GLPIKey())->decrypt($user->fields['personal_token']));
6060
$this->assertSame($_SESSION['glpi_currenttime'], $user->fields['personal_token_date']);
6161
}
6262

@@ -75,10 +75,17 @@ public function testLostPasswordInvalidMail()
7575
public function testLostPasswordInvalidToken()
7676
{
7777
$user = getItemByTypeName('User', TU_USER);
78+
7879
// Test reset password with a bad token
7980
$result = $user->forgetPassword($user->getDefaultEmail());
8081
$this->assertTrue($result);
81-
$token = $user->fields['password_forget_token'];
82+
83+
$this->assertTrue($user->getFromDB($user->getID()));
84+
85+
$encrypted_token = $user->fields['password_forget_token'];
86+
$this->assertNotEmpty($encrypted_token);
87+
88+
$token = (new \GLPIKey())->decrypt($encrypted_token);
8289
$this->assertNotEmpty($token);
8390

8491
$input = [
@@ -100,8 +107,13 @@ public function testLostPassword()
100107

101108
// Test reset password with good token
102109
// 1 - Refresh the in-memory instance of user and get the current password
103-
$user->getFromDB($user->getID());
104-
$token = $user->fields['password_forget_token'];
110+
$this->assertTrue($user->getFromDB($user->getID()));
111+
112+
$encrypted_token = $user->fields['password_forget_token'];
113+
$this->assertNotEmpty($encrypted_token);
114+
115+
$token = (new \GLPIKey())->decrypt($encrypted_token);
116+
$this->assertNotEmpty($token);
105117

106118
// 2 - Set a new password
107119
$input = [

phpunit/web/APIRestTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ public function testInitSessionUserToken()
282282
$updated = $DB->update(
283283
'glpi_users',
284284
[
285-
'api_token' => $token,
285+
'api_token' => (new \GLPIKey())->encrypt($token),
286286
],
287287
['id' => $uid]
288288
);

src/APIClient.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,10 @@ public function prepareInputForUpdate($input)
243243
$input['app_token_date'] = $_SESSION['glpi_currenttime'];
244244
}
245245

246+
if (isset($input['app_token'])) {
247+
$input['app_token'] = (new GLPIKey())->encrypt($input['app_token']);
248+
}
249+
246250
return $input;
247251
}
248252

0 commit comments

Comments
 (0)