Skip to content

Commit 1169dfe

Browse files
author
Till Brehm
committed
Merge branch 'mysql_sha2' into 'develop'
feat(mysql): Support the caching_sha2_password auth for newer MySQL servers Closes #6702 and #6695 See merge request ispconfig/ispconfig3!1936
2 parents c4adbe9 + 43986d8 commit 1169dfe

File tree

9 files changed

+1059
-709
lines changed

9 files changed

+1059
-709
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,9 @@ Temporary Items
5858

5959
# Visual Studio IDE cache/options directory
6060
.vs/
61+
62+
# do not version control generated config files
63+
/server/lib/mysql_clientdb.conf
64+
/server/lib/config.inc.php
65+
/server/lib/config.inc.local.php
66+
/interface/lib/config.inc.local.php
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: 170 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ private function securityScan($string) {
239239
}
240240
}
241241
}
242-
if($ok == true) {
242+
if($ok) {
243243
return true;
244244
} else {
245245
if($ids_config['sql_scan_action'] == 'warn') {
@@ -252,6 +252,8 @@ private function securityScan($string) {
252252
}
253253
}
254254
}
255+
256+
return true;
255257
}
256258

257259
private function _query($sQuery = '') {
@@ -298,7 +300,9 @@ private function _query($sQuery = '') {
298300
} while($ok == false);
299301

300302
$sQuery = call_user_func_array(array(&$this, '_build_query_string'), $aArgs);
301-
$this->securityScan($sQuery);
303+
if (!$this->securityScan($sQuery)) {
304+
return false;
305+
}
302306
$this->_iQueryId = mysqli_query($this->_iConnId, $sQuery);
303307
if (!$this->_iQueryId) {
304308
$this->_sqlerror('Falsche Anfrage / Wrong Query', 'SQL-Query = ' . $sQuery);
@@ -590,6 +594,7 @@ public function unquote($formfield) {
590594
}
591595

592596
public function toLower($record) {
597+
$out = [];
593598
if(is_array($record)) {
594599
foreach($record as $key => $val) {
595600
$key = strtolower($key);
@@ -678,7 +683,7 @@ public function getDatabaseSize($database_name) {
678683
$result = $db->_query("SELECT SUM(data_length+index_length) FROM information_schema.TABLES WHERE table_schema='".$db->escape($database_name)."'");
679684
if(!$result) {
680685
$db->_sqlerror('Unable to determine the size of database ' . $database_name);
681-
return;
686+
return 0;
682687
}
683688
$database_size = $result->getAsRow();
684689
$result->free();
@@ -1075,7 +1080,7 @@ function tableInfo($table_name) {
10751080
}
10761081

10771082
public function mapType($metaType, $typeValue) {
1078-
global $go_api;
1083+
global $app;
10791084
$metaType = strtolower($metaType);
10801085
switch ($metaType) {
10811086
case 'int16':
@@ -1107,6 +1112,8 @@ public function mapType($metaType, $typeValue) {
11071112
return 'date';
11081113
break;
11091114
}
1115+
$app->error('Unknown meta type: '.$metaType);
1116+
return false;
11101117
}
11111118

11121119
/**
@@ -1148,36 +1155,173 @@ public function getDatabaseVersion($major_version_only = false) {
11481155
* Get a mysql password hash
11491156
*
11501157
* @access public
1151-
* @param string cleartext password
1158+
* @param string $password cleartext password
1159+
* @param string $hash_type MySQL hash type to use. either mysql_native_password or caching_sha2_password
11521160
* @return string Password hash
11531161
*/
11541162

1155-
public function getPasswordHash($password) {
1163+
public function getPasswordHash($password, $hash_type = 'mysql_native_password') {
1164+
if($hash_type == 'caching_sha2_password') {
1165+
$password_hash = $this->mysqlSha256Crypt($password, $this->genSalt(20), 5000);
1166+
} else {
1167+
$password_hash = '*' . strtoupper(sha1(sha1($password, true)));
1168+
}
11561169

1157-
$password_type = 'password';
1170+
return $password_hash;
1171+
}
11581172

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';
1173+
/**
1174+
* @param $size int length of salt in bytes
1175+
*
1176+
* @return string
1177+
*/
1178+
private function genSalt($size) {
1179+
$salt = random_bytes($size);
1180+
if($salt === false) {
1181+
throw new Exception('Cannot generate salt.');
1182+
}
1183+
for($i = 0; $i < $size; $i++) {
1184+
$ord = ord($salt[$i]) & 0x7f;
1185+
if($ord < 32) {
1186+
$ord += 32;
1187+
}
1188+
if($ord == 36 /* $ */) {
1189+
$ord += 1;
11651190
}
1191+
$salt[$i] = chr($ord);
11661192
}
1167-
*/
11681193

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

1178-
return $password_hash;
1179-
}
1282+
if($i % 3 != 0) {
1283+
hash_update($ctxC, $S);
1284+
}
1285+
1286+
if($i % 7 != 0) {
1287+
hash_update($ctxC, $P);
1288+
}
1289+
1290+
if(($i & 1) != 0) {
1291+
hash_update($ctxC, $i == 0 ? $A : $C);
1292+
} else {
1293+
hash_update($ctxC, $P);
1294+
}
1295+
$C = hash_final($ctxC, true);
1296+
}
11801297

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

11821326
}
11831327

@@ -1191,10 +1335,11 @@ class db_result {
11911335

11921336
/**
11931337
*
1194-
*
1338+
* @var mysqli_result|null
11951339
* @access private
11961340
*/
11971341
private $_iResId = null;
1342+
/** @var mysqli|null */
11981343
private $_iConnection = null;
11991344

12001345

@@ -1406,7 +1551,7 @@ public function getAsRow() {
14061551
*
14071552
* @access public
14081553
* @param int $iStart offset to start read
1409-
* @param int iLength amount of datasets to read
1554+
* @param int $iLength amount of datasets to read
14101555
*/
14111556
public function limit_result($iStart, $iLength) {
14121557
$this->aLimitedData = array_slice($this->aResultData, $iStart, $iLength, true);

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)