Skip to content

Commit e9b050b

Browse files
pavetheway91peterpp
authored andcommitted
More secure randomness on PHP5, 256 bits of entropy in random strings
There was a bug that caused seemingly 256-bit keys to actually contain only 128 bits of entropy. Also: because Adminer doesn't state hash extension as a requirement, don't rely on it for hmac
1 parent b87090a commit e9b050b

File tree

7 files changed

+256
-24
lines changed

7 files changed

+256
-24
lines changed

admin/core/Hash.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace AdminNeo;
4+
5+
class Hash
6+
{
7+
/**
8+
* HKDF function. To make sure that this is always available, algo cannot be chosen.
9+
*
10+
* @see https://www.rfc-editor.org/rfc/rfc5869
11+
*
12+
* @param int $length Output length.
13+
* @param string $key Input keying material.
14+
* @param string $info Optional context and application specific information.
15+
* @param string $salt Optional salt value (a non-secret random value).
16+
*
17+
* @return string|false Derived key.
18+
*/
19+
public static function hkdf(int $length, string $key, string $info = "", string $salt = "")
20+
{
21+
if (extension_loaded("hash") && PHP_VERSION_ID >= 70120) {
22+
return hash_hkdf("sha1", $key, $length, $info, $salt);
23+
}
24+
25+
if ($salt == "") {
26+
$salt = str_repeat("\0", 20);
27+
}
28+
29+
$prk = self::hmacSha1($key, $salt);
30+
$okm = "";
31+
32+
for ($keyBlock = "", $blockIndex = 1; !isset($okm[$length - 1]); $blockIndex++) {
33+
$keyBlock = self::hmacSha1($keyBlock . $info . chr($blockIndex), $prk);
34+
$okm .= $keyBlock;
35+
}
36+
37+
return substr($okm, 0, $length);
38+
}
39+
40+
/**
41+
* Always available HMAC-SHA1 function. Binary-only output by design. If hex is desired, just bin2hex the output.
42+
*
43+
* @see https://www.rfc-editor.org/rfc/rfc2104
44+
*
45+
* @param string $data Message to be hashed.
46+
* @param string $key Hashing key.
47+
*
48+
* @return string Calculated message.
49+
*/
50+
public static function hmacSha1(string $data, string $key): string
51+
{
52+
if (!extension_loaded("hash")) {
53+
return hash_hmac("sha1", $data, $key, true);
54+
}
55+
56+
if (strlen($key) > 64) {
57+
$key = sha1($key, true);
58+
}
59+
60+
$key = str_pad($key, 64, "\0");
61+
62+
$ipad = ($key ^ str_repeat("\x36", 64));
63+
$opad = ($key ^ str_repeat("\x5C", 64));
64+
65+
return sha1($opad . sha1($ipad . $data, true), true);
66+
}
67+
}

admin/core/Random.php

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
<?php
2+
3+
namespace AdminNeo;
4+
5+
use Exception;
6+
7+
class Random
8+
{
9+
/**
10+
* Returns requested amount of random bytes.
11+
*
12+
* @return string A binary string.
13+
*
14+
* @throws Exception
15+
*/
16+
public static function bytes(int $length): string
17+
{
18+
if (PHP_VERSION_ID >= 70000) {
19+
// There is an astronomically low chance of this throwing an exception. If it happens, the exception is
20+
// purposefully not caught, because it means that there is something very wrong in the system, and it cannot
21+
// be trusted to do TLS securely either.
22+
return random_bytes($length);
23+
}
24+
25+
$result = self::tryAlternatives($length);
26+
if ($result !== false) {
27+
return $result;
28+
}
29+
30+
$result = self::lastResortRandom($length);
31+
if ($result !== false) {
32+
return $result;
33+
}
34+
35+
throw new Exception("Error generating random bytes");
36+
}
37+
38+
/**
39+
* @return string|false
40+
*/
41+
private static function tryAlternatives(int $length)
42+
{
43+
if (extension_loaded("libsodium")) {
44+
return \Sodium\randombytes_buf($length);
45+
}
46+
47+
$unix = DIRECTORY_SEPARATOR === "/";
48+
49+
if ($unix) {
50+
$result = self::readDevUrandom($length);
51+
if ($result !== false) {
52+
return $result;
53+
}
54+
}
55+
56+
// https://bugs.php.net/bug.php?id=69833
57+
$bug69833 = $unix && PHP_VERSION_ID > 50609 && PHP_VERSION_ID < 50613;
58+
59+
if (extension_loaded("mcrypt") && !$bug69833) {
60+
// MCRYPT_DEV_URANDOM means "something secure" provided by the os.
61+
// It's something completely else on Windows.
62+
$result = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
63+
if ($result !== false) {
64+
return $result;
65+
}
66+
}
67+
68+
/* TODO Comment out after testing.
69+
if (!$unix && extension_loaded('com_dotnet') && class_exists('COM'))
70+
{
71+
return self::readCapicom($length);
72+
}
73+
*/
74+
75+
// https://bugs.php.net/bug.php?id=70014
76+
$bug70014 = PHP_VERSION_ID < 50444 || (PHP_VERSION_ID > 50500 && PHP_VERSION_ID < 50528) || (PHP_VERSION_ID > 50600 && PHP_VERSION_ID < 50612);
77+
78+
if (extension_loaded("openssl") && !$bug70014) {
79+
$result = openssl_random_pseudo_bytes($length, $strong);
80+
if ($strong) {
81+
return $result;
82+
}
83+
}
84+
85+
// No reliable source of randomness was found.
86+
return false;
87+
}
88+
89+
/**
90+
* @return string|false
91+
*/
92+
private static function readDevUrandom(int $length)
93+
{
94+
static $file = null;
95+
96+
if ($file === null) {
97+
$file = @fopen("/dev/urandom", "rb");
98+
}
99+
if (!$file) {
100+
return false;
101+
}
102+
103+
$remaining = $length;
104+
$result = "";
105+
106+
do {
107+
$data = fread($file, $remaining);
108+
if ($data === false) {
109+
return false;
110+
}
111+
112+
$remaining -= strlen($data);
113+
$result .= $data;
114+
} while ($remaining > 0);
115+
116+
return $result;
117+
}
118+
119+
/**
120+
* @return string|false
121+
*/
122+
private static function readCapicom(int $length)
123+
{
124+
$com = new \COM("CAPICOM.Utilities.1");
125+
126+
$remaining = $length;
127+
$result = "";
128+
129+
do {
130+
$data = base64_decode((string)$com->GetRandom($length, 0));
131+
$remaining -= strlen($data);
132+
$result .= $data;
133+
} while ($remaining > 0);
134+
135+
return $result;
136+
}
137+
138+
/**
139+
* @return string|false
140+
*/
141+
private static function lastResortRandom(int $length)
142+
{
143+
static $key = null;
144+
static $salt = null;
145+
146+
if ($key === null) {
147+
$data = $_SERVER;
148+
$data[] = uniqid("", true);
149+
shuffle($data);
150+
151+
$key = sha1(serialize($data), true);
152+
153+
// See referenced bug 70014 above.
154+
if (extension_loaded("openssl")) {
155+
$salt = openssl_random_pseudo_bytes(20);
156+
} else {
157+
$salt = "";
158+
for ($i = 0; $i < 20; $i++) {
159+
$salt .= chr((mt_rand() ^ mt_rand()) % 256);
160+
}
161+
}
162+
} else {
163+
if ((ord($key) % 2 === 0) === (ord($salt) % 2 === 0)) {
164+
$key = Hash::hmacSha1($key, $salt);
165+
} else {
166+
$salt = Hash::hmacSha1($salt, $key);
167+
}
168+
}
169+
170+
return Hash::hkdf($length, $key, "$length", $salt);
171+
}
172+
}

admin/include/bootstrap.inc.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
include __DIR__ . "/../core/Server.php";
3131
include __DIR__ . "/../core/Config.php";
3232
include __DIR__ . "/../core/Settings.php";
33+
include __DIR__ . "/../core/Hash.php";
34+
include __DIR__ . "/../core/Random.php";
3335
include __DIR__ . "/polyfill.inc.php";
3436
include __DIR__ . "/functions.inc.php";
3537
include __DIR__ . "/html.inc.php";

admin/include/design.inc.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,17 +264,18 @@ function page_headers(): void
264264
}
265265

266266
/**
267-
* Gets a CSP nonce.
267+
* Returns a CSP nonce.
268+
*
269+
* @return string Random string with 256 bits of entropy.
268270
*
269-
* @return string Base64 value.
270271
* @throws \Random\RandomException
271272
*/
272-
function get_nonce()
273+
function get_nonce(): string
273274
{
274275
static $nonce;
275276

276277
if (!$nonce) {
277-
$nonce = base64_encode(get_random_string(true));
278+
$nonce = get_random_string();
278279
}
279280

280281
return $nonce;

admin/include/functions.inc.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -929,17 +929,15 @@ function get_private_key($create)
929929
}
930930

931931
/**
932-
* Returns a random 32 characters long string.
932+
* Returns a random string with 256 bits of entropy.
933+
*
934+
* The result is safe to use in URL parameters or file names.
933935
*
934-
* @param $binary bool
935-
* @return string
936936
* @throws \Random\RandomException
937937
*/
938-
function get_random_string($binary = false)
938+
function get_random_string(): string
939939
{
940-
$bytes = function_exists('random_bytes') ? random_bytes(32) : uniqid(mt_rand(), true);
941-
942-
return $binary ? $bytes : md5($bytes);
940+
return strtr(rtrim(base64_encode(Random::bytes(32)), "="), "+/", "-_");
943941
}
944942

945943
/** Format value to use in select

plugins/FileUploadPlugin.php

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
namespace AdminNeo;
44

5-
use Exception;
6-
75
/**
86
* Replaces fields ending with "_path" by `<input type="file">` in edit form and displays links to the uploaded files in
97
* table select.
@@ -62,6 +60,9 @@ public function getFieldInput(?string $table, array $field, string $attrs, $valu
6260
return "<input type='file'$attrs>";
6361
}
6462

63+
/**
64+
* @throws \Random\RandomException
65+
*/
6566
public function processFieldInput(?array $field, string $value, string $function = ""): ?string
6667
{
6768
if (!$field || !($shortFieldName = $this->matchField($field))) {
@@ -86,7 +87,7 @@ public function processFieldInput(?array $field, string $value, string $function
8687

8788
// Generate random unique file name.
8889
do {
89-
$filename = $this->generateName() . $matches[0];
90+
$filename = get_random_string() . $matches[0];
9091

9192
$targetPath = "$targetDir/" . $this->encodeFs($shortFieldName) . "-$filename";
9293
} while (file_exists($targetPath));
@@ -104,15 +105,6 @@ private function matchField(array $field): ?string
104105
return preg_match('~(.*)_path$~', $field["field"], $matches) ? $matches[1] : null;
105106
}
106107

107-
private function generateName(): string
108-
{
109-
try {
110-
return function_exists('random_bytes') ? bin2hex(random_bytes(8)) : uniqid("", true);
111-
} catch (Exception $e) {
112-
return uniqid("", true);
113-
}
114-
}
115-
116108
private function encodeUrl(string $value): string
117109
{
118110
return rawurlencode(str_replace("/", "%2F", $value));

plugins/OtpLoginPlugin.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public function authenticate(string $username, string $password): ?string
7676
private function getOtp(int $timeSlot): int
7777
{
7878
$data = str_pad(pack("N", $timeSlot), 8, "\0", STR_PAD_LEFT);
79-
$hash = hash_hmac("sha1", $data, $this->secret, true);
79+
$hash = Hash::hmacSha1($data, $this->secret);
8080
$offset = ord(substr($hash, -1)) & 0xF;
8181
$unpacked = unpack("N", substr($hash, $offset, 4));
8282

0 commit comments

Comments
 (0)