Skip to content

Commit fa21a45

Browse files
committed
Support the caching_sha2_password auth for newer MySQL servers #6754
1 parent c865f18 commit fa21a45

File tree

8 files changed

+404
-107
lines changed

8 files changed

+404
-107
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE `web_database_user` ADD `database_password_sha2` varchar(70) DEFAULT NULL AFTER `database_password`;

install/sql/ispconfig3.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1947,6 +1947,7 @@ CREATE TABLE IF NOT EXISTS `web_database_user` (
19471947
`database_user` varchar(64) DEFAULT NULL,
19481948
`database_user_prefix` varchar(50) NOT NULL default '',
19491949
`database_password` varchar(64) DEFAULT NULL,
1950+
`database_password_sha2` varchar(70) DEFAULT NULL,
19501951
`database_password_mongo` varchar(32) DEFAULT NULL,
19511952
PRIMARY KEY (`database_user_id`)
19521953
) DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

interface/lib/classes/db_mysql.inc.php

Lines changed: 157 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,36 +1148,174 @@ public function getDatabaseVersion($major_version_only = false) {
11481148
* Get a mysql password hash
11491149
*
11501150
* @access public
1151-
* @param string cleartext password
1151+
* @param string $password cleartext password
1152+
* @param string $hash_type MySQL hash type to use. either mysql_native_password or caching_sha2_password
11521153
* @return string Password hash
11531154
*/
11541155

1155-
public function getPasswordHash($password) {
1156+
public function getPasswordHash($password, $hash_type = 'mysql_native_password') {
1157+
if($hash_type == 'caching_sha2_password') {
1158+
$password_hash = $this->mysqlSha256Crypt($password, $this->genSalt(20), 5000);
1159+
} else {
1160+
$password_hash = '*'.strtoupper(sha1(sha1($password, true)));
1161+
}
11561162

1157-
$password_type = 'password';
1163+
return $password_hash;
1164+
}
11581165

1159-
/* Disabled until caching_sha2_password is implemented
1160-
if($this->getDatabaseType() == 'mysql' && $this->getDatabaseVersion(true) >= 8) {
1161-
// we are in MySQL 8 mode
1162-
$tmp = $this->queryOneRecord("show variables like 'default_authentication_plugin'");
1163-
if($tmp['default_authentication_plugin'] == 'caching_sha2_password') {
1164-
$password_type = 'caching_sha2_password';
1166+
/**
1167+
* @param $size int length of salt in bytes
1168+
*
1169+
* @return string
1170+
*/
1171+
private function genSalt($size)
1172+
{
1173+
$salt = random_bytes($size);
1174+
if ($salt === false) {
1175+
throw new Exception('Cannot generate salt.');
1176+
}
1177+
for ($i = 0; $i < $size; $i++) {
1178+
$ord = ord($salt[$i]) & 0x7f;
1179+
if ($ord < 32) {
1180+
$ord += 32;
11651181
}
1182+
if ($ord == 36 /* $ */) {
1183+
$ord += 1;
1184+
}
1185+
$salt[$i] = chr($ord);
11661186
}
1167-
*/
11681187

1169-
if($password_type == 'caching_sha2_password') {
1170-
/*
1171-
caching_sha2_password hashing needs to be implemented, have not
1172-
found valid PHP implementation for the new password hash type.
1173-
*/
1174-
} else {
1175-
$password_hash = '*'.strtoupper(sha1(sha1($password, true)));
1188+
return $salt;
1189+
}
1190+
1191+
/**
1192+
* this is the SHA256 algorithm of the crypt unix call – the only difference is that we do not truncate the salt to 16 chars
1193+
* @see https://www.akkadia.org/drepper/SHA-crypt.txt
1194+
* @see https://github.com/mysql/mysql-server/blob/trunk/mysys/crypt_genhash_impl.cc
1195+
*
1196+
* @param string $plaintext the plain text password
1197+
* @param string $salt the raw salt (needs to be 20 bytes long)
1198+
* @param int $rounds number of rounds. MySQL default is 5000. Must be between 1000 and 4095000 (0xFFF * 1000)
1199+
*
1200+
* @return string hashed password in MySQL format
1201+
*/
1202+
private function mysqlSha256Crypt($plaintext, $salt, $rounds)
1203+
{
1204+
$plaintext_len = strlen($plaintext);
1205+
$salt_len = strlen($salt);
1206+
1207+
// 1
1208+
$ctxA = hash_init('sha256');
1209+
// 2
1210+
hash_update($ctxA, $plaintext);
1211+
// 3
1212+
hash_update($ctxA, $salt);
1213+
// 4
1214+
$ctxB = hash_init('sha256');
1215+
// 5
1216+
hash_update($ctxB, $plaintext);
1217+
// 6
1218+
hash_update($ctxB, $salt);
1219+
// 7
1220+
hash_update($ctxB, $plaintext);
1221+
// 8
1222+
$B = hash_final($ctxB, true);
1223+
// 9
1224+
for ($i = $plaintext_len; $i > 32; $i -= 32) {
1225+
hash_update($ctxA, $B);
11761226
}
1227+
// 10
1228+
hash_update($ctxA, substr($B, 0, $i));
1229+
// 11
1230+
for ($i = $plaintext_len; $i > 0; $i >>= 1) {
1231+
if (($i & 1) != 0) {
1232+
hash_update($ctxA, $B);
1233+
} else {
1234+
hash_update($ctxA, $plaintext);
1235+
}
1236+
}
1237+
// 12
1238+
$A = hash_final($ctxA, true);
1239+
// 13
1240+
$ctxDP = hash_init('sha256');
1241+
// 14
1242+
for ($i = 0; $i < $plaintext_len; $i++) {
1243+
hash_update($ctxDP, $plaintext);
1244+
}
1245+
// 15
1246+
$DP = hash_final($ctxDP, true);
1247+
// 16
1248+
$P = "";
1249+
for ($i = $plaintext_len; $i > 32; $i -= 32) {
1250+
$P .= $DP;
1251+
}
1252+
$P .= substr($DP, 0, $i);
1253+
// 17
1254+
$ctxDS = hash_init('sha256');
1255+
// 18
1256+
for ($i = 0; $i < 16 + ord($A[0]); $i++) {
1257+
hash_update($ctxDS, $salt);
1258+
}
1259+
// 19
1260+
$DS = hash_final($ctxDS, true);
1261+
// 20
1262+
$S = "";
1263+
for ($i = $salt_len; $i >= 32; $i -= 32) {
1264+
$S .= $DS;
1265+
}
1266+
$S .= substr($DS, 0, $i);
1267+
// 21
1268+
$C = "";
1269+
for ($i = 0; $i < $rounds; $i++) {
1270+
$ctxC = hash_init('sha256');
1271+
if (($i & 1) != 0) {
1272+
hash_update($ctxC, $P);
1273+
} else {
1274+
hash_update($ctxC, $i == 0 ? $A : $C);
1275+
}
11771276

1178-
return $password_hash;
1179-
}
1277+
if ($i % 3 != 0) {
1278+
hash_update($ctxC, $S);
1279+
}
1280+
1281+
if ($i % 7 != 0) {
1282+
hash_update($ctxC, $P);
1283+
}
1284+
1285+
if (($i & 1) != 0) {
1286+
hash_update($ctxC, $i == 0 ? $A : $C);
1287+
} else {
1288+
hash_update($ctxC, $P);
1289+
}
1290+
$C = hash_final($ctxC, true);
1291+
}
11801292

1293+
// 22
1294+
$b64result = str_repeat(' ', 43);
1295+
$p = 0;
1296+
$b64_from_24bit = function ($B2, $B1, $B0, $N) use (&$b64result, &$p) {
1297+
$w = ($B2 << 16) | ($B1 << 8) | $B0;
1298+
$n = $N;
1299+
while (--$n >= 0) {
1300+
$b64result[$p++] = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"[$w & 0x3f];
1301+
$w = $w >> 6;
1302+
}
1303+
};
1304+
$b64_from_24bit(ord($C[0]), ord($C[10]), ord($C[20]), 4);
1305+
$b64_from_24bit(ord($C[21]), ord($C[1]), ord($C[11]), 4);
1306+
$b64_from_24bit(ord($C[12]), ord($C[22]), ord($C[2]), 4);
1307+
$b64_from_24bit(ord($C[3]), ord($C[13]), ord($C[23]), 4);
1308+
$b64_from_24bit(ord($C[24]), ord($C[4]), ord($C[14]), 4);
1309+
$b64_from_24bit(ord($C[15]), ord($C[25]), ord($C[5]), 4);
1310+
$b64_from_24bit(ord($C[6]), ord($C[16]), ord($C[26]), 4);
1311+
$b64_from_24bit(ord($C[27]), ord($C[7]), ord($C[17]), 4);
1312+
$b64_from_24bit(ord($C[18]), ord($C[28]), ord($C[8]), 4);
1313+
$b64_from_24bit(ord($C[9]), ord($C[19]), ord($C[29]), 4);
1314+
$b64_from_24bit(0, ord($C[31]), ord($C[30]), 3);
1315+
1316+
// we do not truncate $salt to 16 chars since MySQL does not do that and uses 20 bytes salts
1317+
return sprintf('$A$%03x$%s%s', $rounds / 1000, $salt, $b64result);
1318+
}
11811319

11821320
}
11831321

interface/lib/classes/tform_base.inc.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,6 +1376,9 @@ protected function _getSQL($record, $tab, $action = 'INSERT', $primary_id = 0, $
13761376
} elseif (isset($field['encryption']) && $field['encryption'] == 'MYSQL') {
13771377
$record[$key] = $app->db->getPasswordHash($record[$key]);
13781378
$sql_insert_val .= "'".$app->db->quote($record[$key])."', ";
1379+
} elseif (isset($field['encryption']) && $field['encryption'] == 'MYSQLSHA2') {
1380+
$record[$key] = $app->db->getPasswordHash($record[$key], 'caching_sha2_password');
1381+
$sql_insert_val .= "'".$app->db->quote($record[$key])."', ";
13791382
} else {
13801383
$record[$key] = md5(stripslashes($record[$key]));
13811384
$sql_insert_val .= "'".$app->db->quote($record[$key])."', ";
@@ -1407,6 +1410,9 @@ protected function _getSQL($record, $tab, $action = 'INSERT', $primary_id = 0, $
14071410
} elseif (isset($field['encryption']) && $field['encryption'] == 'MYSQL') {
14081411
$record[$key] = $app->db->getPasswordHash($record[$key]);
14091412
$sql_update .= "`$key` = '".$app->db->quote($record[$key])."', ";
1413+
} elseif (isset($field['encryption']) && $field['encryption'] == 'MYSQLSHA2') {
1414+
$record[$key] = $app->db->getPasswordHash($record[$key], 'caching_sha2_password');
1415+
$sql_update .= "`$key` = '".$app->db->quote($record[$key])."', ";
14101416
} else {
14111417
$record[$key] = md5(stripslashes($record[$key]));
14121418
$sql_update .= "`$key` = '".$app->db->quote($record[$key])."', ";

interface/web/sites/database_user_edit.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,13 @@ function onBeforeUpdate() {
168168
$this->dataRecord['database_user'] = substr($dbuser_prefix . $this->dataRecord['database_user'], 0, 32);
169169
}
170170

171+
// always copy over the password to the SHA2 column
172+
if ($this->dataRecord['database_password']) {
173+
$this->dataRecord['database_password_sha2'] = $this->dataRecord['database_password'];
174+
} else {
175+
$this->dataRecord['database_password_sha2'] = '';
176+
}
177+
171178
/* prepare password for MongoDB */
172179
// TODO: this still doens't work as when only the username changes we have no database_password.
173180
// taking the one from oldData doesn't work as it's encrypted...shit!
@@ -184,10 +191,13 @@ function onBeforeInsert() {
184191

185192
//* Database username shall not be empty
186193
if($this->dataRecord['database_user'] == '') $app->tform->errorMessage .= $app->tform->wordbook["database_user_error_empty"].'<br />';
187-
194+
188195
//* Database password shall not be empty
189196
if($this->dataRecord['database_password'] == '') $app->tform->errorMessage .= $app->tform->wordbook["database_password_error_empty"].'<br />';
190197

198+
// always copy over the password to the SHA2 column
199+
$this->dataRecord['database_password_sha2'] = $this->dataRecord['database_password'];
200+
191201
//* Get the database name and database user prefix
192202
$app->uses('getconf,tools_sites');
193203
$global_config = $app->getconf->get_global_config('sites');

interface/web/sites/form/database_user.tform.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,15 @@
117117
'width' => '30',
118118
'maxlength' => '255'
119119
),
120+
'database_password_sha2' => array (
121+
'datatype' => 'VARCHAR',
122+
'formtype' => 'PASSWORD',
123+
'encryption' => 'MYSQLSHA2',
124+
'default' => '',
125+
'value' => '',
126+
'width' => '30',
127+
'maxlength' => '255'
128+
),
120129
'database_password_mongo' => array (
121130
'datatype' => 'VARCHAR',
122131
'formtype' => 'PASSWORD',

0 commit comments

Comments
 (0)