Add Randomizer::getBytesFromString() method (#9664)

* Add `Randomizer::getBytesFromAlphabet()` method

* Rename `getBytesFromAlphabet` to `getBytesFromString`

* [ci skip] Add NEWS/UPGRADING for Randomizer::getBytesFromString()

Co-authored-by: Tim Düsterhus <tim@bastelstu.be>
This commit is contained in:
Joshua Rüsweg 2022-12-09 17:39:13 +01:00 committed by GitHub
parent 1b503a1f52
commit ac3ecd03af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 238 additions and 7 deletions

3
NEWS
View File

@ -55,6 +55,9 @@ PHP NEWS
- Posix:
. Added posix_sysconf. (David Carlier)
- Random:
. Added Randomizer::getBytesFromString(). (Joshua Rüsweg)
- Reflection:
. Fix GH-9470 (ReflectionMethod constructor should not find private parent
method). (ilutov)

View File

@ -62,6 +62,10 @@ PHP 8.3 UPGRADE NOTES
- Posix:
. Added posix_sysconf call to get runtime informations.
- Random:
. Added Randomizer::getBytesFromString().
RFC: https://wiki.php.net/rfc/randomizer_additions
- Sockets:
. Added socket_atmark to checks if the socket is OOB marked.

View File

@ -74,6 +74,8 @@ PHPAPI double php_combined_lcg(void);
# define MT_N (624)
#define PHP_RANDOM_RANGE_ATTEMPTS (50)
PHPAPI void php_mt_srand(uint32_t seed);
PHPAPI uint32_t php_mt_rand(void);
PHPAPI zend_long php_mt_rand_range(zend_long min, zend_long max);

View File

@ -86,8 +86,6 @@ static zend_object_handlers random_engine_xoshiro256starstar_object_handlers;
static zend_object_handlers random_engine_secure_object_handlers;
static zend_object_handlers random_randomizer_object_handlers;
#define RANDOM_RANGE_ATTEMPTS (50)
static inline uint32_t rand_range32(const php_random_algo *algo, php_random_status *status, uint32_t umax)
{
uint32_t result, limit;
@ -124,8 +122,8 @@ static inline uint32_t rand_range32(const php_random_algo *algo, php_random_stat
/* Discard numbers over the limit to avoid modulo bias */
while (UNEXPECTED(result > limit)) {
/* If the requirements cannot be met in a cycles, return fail */
if (++count > RANDOM_RANGE_ATTEMPTS) {
zend_throw_error(random_ce_Random_BrokenRandomEngineError, "Failed to generate an acceptable random number in %d attempts", RANDOM_RANGE_ATTEMPTS);
if (++count > PHP_RANDOM_RANGE_ATTEMPTS) {
zend_throw_error(random_ce_Random_BrokenRandomEngineError, "Failed to generate an acceptable random number in %d attempts", PHP_RANDOM_RANGE_ATTEMPTS);
return 0;
}
@ -180,8 +178,8 @@ static inline uint64_t rand_range64(const php_random_algo *algo, php_random_stat
/* Discard numbers over the limit to avoid modulo bias */
while (UNEXPECTED(result > limit)) {
/* If the requirements cannot be met in a cycles, return fail */
if (++count > RANDOM_RANGE_ATTEMPTS) {
zend_throw_error(random_ce_Random_BrokenRandomEngineError, "Failed to generate an acceptable random number in %d attempts", RANDOM_RANGE_ATTEMPTS);
if (++count > PHP_RANDOM_RANGE_ATTEMPTS) {
zend_throw_error(random_ce_Random_BrokenRandomEngineError, "Failed to generate an acceptable random number in %d attempts", PHP_RANDOM_RANGE_ATTEMPTS);
return 0;
}

View File

@ -137,6 +137,8 @@ namespace Random
public function getBytes(int $length): string {}
public function getBytesFromString(string $string, int $length): string {}
public function shuffleArray(array $array): array {}
public function shuffleBytes(string $bytes): string {}

View File

@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 6cc9022516ce23c2e95af30606db43e9fc28e38a */
* Stub hash: a4226bc7838eba98c5a935b279f681a7d083c0b2 */
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_lcg_value, 0, 0, IS_DOUBLE, 0)
ZEND_END_ARG_INFO()
@ -94,6 +94,11 @@ ZEND_END_ARG_INFO()
#define arginfo_class_Random_Randomizer_getBytes arginfo_random_bytes
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Random_Randomizer_getBytesFromString, 0, 2, IS_STRING, 0)
ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0)
ZEND_ARG_TYPE_INFO(0, length, IS_LONG, 0)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Random_Randomizer_shuffleArray, 0, 1, IS_ARRAY, 0)
ZEND_ARG_TYPE_INFO(0, array, IS_ARRAY, 0)
ZEND_END_ARG_INFO()
@ -133,6 +138,7 @@ ZEND_METHOD(Random_Randomizer, __construct);
ZEND_METHOD(Random_Randomizer, nextInt);
ZEND_METHOD(Random_Randomizer, getInt);
ZEND_METHOD(Random_Randomizer, getBytes);
ZEND_METHOD(Random_Randomizer, getBytesFromString);
ZEND_METHOD(Random_Randomizer, shuffleArray);
ZEND_METHOD(Random_Randomizer, shuffleBytes);
ZEND_METHOD(Random_Randomizer, pickArrayKeys);
@ -209,6 +215,7 @@ static const zend_function_entry class_Random_Randomizer_methods[] = {
ZEND_ME(Random_Randomizer, nextInt, arginfo_class_Random_Randomizer_nextInt, ZEND_ACC_PUBLIC)
ZEND_ME(Random_Randomizer, getInt, arginfo_class_Random_Randomizer_getInt, ZEND_ACC_PUBLIC)
ZEND_ME(Random_Randomizer, getBytes, arginfo_class_Random_Randomizer_getBytes, ZEND_ACC_PUBLIC)
ZEND_ME(Random_Randomizer, getBytesFromString, arginfo_class_Random_Randomizer_getBytesFromString, ZEND_ACC_PUBLIC)
ZEND_ME(Random_Randomizer, shuffleArray, arginfo_class_Random_Randomizer_shuffleArray, ZEND_ACC_PUBLIC)
ZEND_ME(Random_Randomizer, shuffleBytes, arginfo_class_Random_Randomizer_shuffleBytes, ZEND_ACC_PUBLIC)
ZEND_ME(Random_Randomizer, pickArrayKeys, arginfo_class_Random_Randomizer_pickArrayKeys, ZEND_ACC_PUBLIC)

View File

@ -258,6 +258,102 @@ PHP_METHOD(Random_Randomizer, pickArrayKeys)
}
/* }}} */
/* {{{ Get Random Bytes for String */
PHP_METHOD(Random_Randomizer, getBytesFromString)
{
php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
zend_long length;
zend_string *source, *retval;
size_t total_size = 0;
ZEND_PARSE_PARAMETERS_START(2, 2);
Z_PARAM_STR(source)
Z_PARAM_LONG(length)
ZEND_PARSE_PARAMETERS_END();
const size_t source_length = ZSTR_LEN(source);
if (source_length < 1) {
zend_argument_value_error(1, "cannot be empty");
RETURN_THROWS();
}
if (length < 1) {
zend_argument_value_error(2, "must be greater than 0");
RETURN_THROWS();
}
retval = zend_string_alloc(length, 0);
if (source_length > 0xFF) {
while (total_size < length) {
uint64_t offset = randomizer->algo->range(randomizer->status, 0, source_length - 1);
if (EG(exception)) {
zend_string_free(retval);
RETURN_THROWS();
}
ZSTR_VAL(retval)[total_size++] = ZSTR_VAL(source)[offset];
}
} else {
uint64_t mask;
if (source_length <= 0x1) {
mask = 0x0;
} else if (source_length <= 0x2) {
mask = 0x1;
} else if (source_length <= 0x4) {
mask = 0x3;
} else if (source_length <= 0x8) {
mask = 0x7;
} else if (source_length <= 0x10) {
mask = 0xF;
} else if (source_length <= 0x20) {
mask = 0x1F;
} else if (source_length <= 0x40) {
mask = 0x3F;
} else if (source_length <= 0x80) {
mask = 0x7F;
} else {
mask = 0xFF;
}
int failures = 0;
while (total_size < length) {
uint64_t result = randomizer->algo->generate(randomizer->status);
if (EG(exception)) {
zend_string_free(retval);
RETURN_THROWS();
}
for (size_t i = 0; i < randomizer->status->last_generated_size; i++) {
uint64_t offset = (result >> (i * 8)) & mask;
if (offset >= source_length) {
if (++failures > PHP_RANDOM_RANGE_ATTEMPTS) {
zend_string_free(retval);
zend_throw_error(random_ce_Random_BrokenRandomEngineError, "Failed to generate an acceptable random number in %d attempts", PHP_RANDOM_RANGE_ATTEMPTS);
RETURN_THROWS();
}
continue;
}
failures = 0;
ZSTR_VAL(retval)[total_size++] = ZSTR_VAL(source)[offset];
if (total_size >= length) {
break;
}
}
}
}
ZSTR_VAL(retval)[length] = '\0';
RETURN_STR(retval);
}
/* }}} */
/* {{{ Random\Randomizer::__serialize() */
PHP_METHOD(Random_Randomizer, __serialize)
{

View File

@ -49,6 +49,18 @@ try {
echo $e->getMessage(), PHP_EOL;
}
try {
var_dump(randomizer()->getBytesFromString('123', 10));
} catch (Random\BrokenRandomEngineError $e) {
echo $e->getMessage(), PHP_EOL;
}
try {
var_dump(randomizer()->getBytesFromString(str_repeat('a', 500), 10));
} catch (Random\BrokenRandomEngineError $e) {
echo $e->getMessage(), PHP_EOL;
}
?>
--EXPECTF--
Failed to generate an acceptable random number in 50 attempts
@ -56,3 +68,5 @@ int(%d)
string(2) "ff"
Failed to generate an acceptable random number in 50 attempts
Failed to generate an acceptable random number in 50 attempts
Failed to generate an acceptable random number in 50 attempts
Failed to generate an acceptable random number in 50 attempts

View File

@ -49,6 +49,18 @@ try {
echo $e->getMessage(), PHP_EOL;
}
try {
var_dump(randomizer()->getBytesFromString('123', 10));
} catch (Random\BrokenRandomEngineError $e) {
echo $e->getMessage(), PHP_EOL;
}
try {
var_dump(randomizer()->getBytesFromString(str_repeat('a', 500), 10));
} catch (Random\BrokenRandomEngineError $e) {
echo $e->getMessage(), PHP_EOL;
}
?>
--EXPECT--
A random engine must return a non-empty string
@ -56,3 +68,5 @@ A random engine must return a non-empty string
A random engine must return a non-empty string
A random engine must return a non-empty string
A random engine must return a non-empty string
A random engine must return a non-empty string
A random engine must return a non-empty string

View File

@ -0,0 +1,63 @@
--TEST--
Random: Randomizer: getBytesFromString(): Basic functionality
--FILE--
<?php
use Random\Engine;
use Random\Engine\Mt19937;
use Random\Engine\PcgOneseq128XslRr64;
use Random\Engine\Secure;
use Random\Engine\Test\TestShaEngine;
use Random\Engine\Xoshiro256StarStar;
use Random\Randomizer;
require __DIR__ . "/../../engines.inc";
$engines = [];
$engines[] = new Mt19937(null, MT_RAND_MT19937);
$engines[] = new Mt19937(null, MT_RAND_PHP);
$engines[] = new PcgOneseq128XslRr64();
$engines[] = new Xoshiro256StarStar();
$engines[] = new Secure();
$engines[] = new TestShaEngine();
foreach ($engines as $engine) {
echo $engine::class, PHP_EOL;
$randomizer = new Randomizer($engine);
var_dump($randomizer->getBytesFromString('a', 10));
var_dump($randomizer->getBytesFromString(str_repeat('a', 256), 5));
for ($i = 1; $i < 250; $i++) {
$output = $randomizer->getBytesFromString(str_repeat('ab', $i), 500);
// This check can theoretically fail with a chance of 0.5**500.
if (!str_contains($output, 'a') || !str_contains($output, 'b')) {
die("failure: didn't see both a and b at {$i}");
}
}
}
die('success');
?>
--EXPECT--
Random\Engine\Mt19937
string(10) "aaaaaaaaaa"
string(5) "aaaaa"
Random\Engine\Mt19937
string(10) "aaaaaaaaaa"
string(5) "aaaaa"
Random\Engine\PcgOneseq128XslRr64
string(10) "aaaaaaaaaa"
string(5) "aaaaa"
Random\Engine\Xoshiro256StarStar
string(10) "aaaaaaaaaa"
string(5) "aaaaa"
Random\Engine\Secure
string(10) "aaaaaaaaaa"
string(5) "aaaaa"
Random\Engine\Test\TestShaEngine
string(10) "aaaaaaaaaa"
string(5) "aaaaa"
success

View File

@ -0,0 +1,28 @@
--TEST--
Random: Randomizer: getBytesFromString(): Parameters are correctly validated
--FILE--
<?php
use Random\Randomizer;
function randomizer(): Randomizer
{
return new Randomizer();
}
try {
var_dump(randomizer()->getBytesFromString("", 2));
} catch (ValueError $e) {
echo $e->getMessage(), PHP_EOL;
}
try {
var_dump(randomizer()->getBytesFromString("abc", 0));
} catch (ValueError $e) {
echo $e->getMessage(), PHP_EOL;
}
?>
--EXPECTF--
Random\Randomizer::getBytesFromString(): Argument #1 ($string) cannot be empty
Random\Randomizer::getBytesFromString(): Argument #2 ($length) must be greater than 0