In Depth: Introducing Random
[InDepth#22] Random generates cryptographically secure random values in a range of different formats through a simple helper package for PHP.
If you like this article, or my package, please consider becoming a paid subscriber to receive these In Depth articles every month and to support my security work within the Laravel and PHP communities.
Something I commonly encounter during my security audits (especially on older codebases) is insecure randomness, usually in places where security is required. It’s usually using some form of rand()
, often injected inside md5()
to generate a random hash, combined with str_shuffle()
to generate new passwords, or used to make an One-Time Password (OTP) with rand(100_000, 999_999)
.
The problem is rand()
is not cryptographically secure, and neither is mt_rand()
, mt_srand()
, str_shuffle()
, array_rand()
, or of the other insecure functions available in PHP. This is a topic I’ve talked about many times before - so I would expect you’re already aware that these methods aren’t safe to use - however, we can’t simply declare these methods insecure, drop the mic, and walk away. Instead, we need to provide secure alternatives - so rather than simply saying “don’t use rand()
in that way”, we can say “here’s a secure method you can use instead”!
That’s why I created Random.
What Is Random?
Random is a new Composer package I’ve built that is designed to provide secure and easy to use implementations of common randomness functions. It’s completely framework agnostic and runs on PHP 7.1 and later, with the only dependency being the excellent php-random-polyfill by Anton Smirnov.
You can find it on:
- GitHub → https://github.com/valorin/random
- Packagist → https://packagist.org/packages/valorin/random
It can be installed as per usual with:
composer require valorin/random
I wanted it to be easy to use as a drop-in toolkit, so all of the methods are accessed via static method calls on the `\Valorin\Random\Random`
class1, which gives it a simple and readable syntax:
$number = Random::number(1, 100);
$password = Random::password();
The intention being that anyone who wants to rip out an insecure implementation can require the package and drop in a single line:
// Original Insecure Version
$otp = rand(100_000, 999_999);
// Secure Version
$otp = Random::otp();
// Original Insecure Version
function generatePassword($length = 10) {
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$password = '';
for ($i = 0; $i < $length; $i++) {
$password .= $characters[rand(0, $charactersLength - 1)];
}
return str_shuffle($password);
}
// Secure Version
function generatePassword($length = 16) {
return Random::password($length);
}
One of the other goals was to support older versions of PHP, as even though older version are no longer supported2, it still takes time for teams to upgrade. Having a toolkit that can be dropped-in now to fix insecure randomness quickly is a huge benefit, and saves these folks from having to attempt their own secure implementations or rushing an upgrade. I went with PHP 7.1 as that’s the earliest the polyfill supported, and 7.0 proved too difficult to get everything working.
That’s enough introduction, let’s explore each of the methods!
Random Integers
Starting with the basics, we can generate a random numbers by using:
$number = Random::number(int $min, int $max): int;
> $number = Random::number(1, 1000);
= 384
Since we already have the fantastic `random_int()`
function available, the only practical reason you’d want to use `Random::number()`
3 is if you’re using custom Engines, such as for seeding random numbers, and we’ll cover these below.
We’ve talked about random_int()
in the past in Cryptographically Secure Randomness.
Random One-Time Passwords (Numeric fixed-length OTPs)
One-Time Passwords (OTPs), Passcodes, Nonces… there are multiple names for a fixed length numeric string sent to users4 for verification purposes.
We can generate these using:
$otp = Random::otp(int $length): string;
> $otp = Random::otp(6);
= "001421"
It’s so common to come across OTPs being generated with something like rand(100_000, 999_999)
, however this approach is doubly flawed as it uses insecure randomness and it loses ~10% of it’s entropy (the 000,000
- 099,999
range).
My suggested fix, as discussed in Magic Emails, features random_int()
and left pad, but I wanted to make it simpler and do all of the work in a single helper so folks don’t need to implement any of it themselves.
The method adds zeros (0
) as a prefix and returns a string to prevent the zeros from being dropped off the front.
I originally had the zero prefix as customisable, but it made no sense to use a non-zero given the purpose of the method. The method also supports longer numbers than the integer limit - not sure why you’d need that, but it’s there if you want it.
There is a question about the naming of this - does otp()
accurately represent what it does without implying extra behaviour? I had some trouble coming up with one I was comfortable with. Let me know in the comments if you have any suggestions!
Random Strings
Now that we’ve covered numbers, we also need to generate random strings with varying character combinations.
We can generate random strings using the primary string()
method:
$string = Random::string(
int $length = 32,
bool $lower = true, // toggle lower case letters
bool $upper = true, // toggle upper case letters
bool $numbers = true, // toggle numbers
bool $symbols = true, // toggle special symbols
bool $requireAll = false // require at least one character from each type
): string;
There are also wrappers for common use cases:
// Random letters only
$string = Random::letters(int $length = 32): string;
// Random letters and numbers (i.e. a random token)
$string = Random::token(int $length = 32): string;
// Random letters, numbers, and symbols (i.e. a random password).
$string = Random::password(int $length = 32, bool $requireAll = false): string;
// Random alphanumeric token string with chunks separated by dashes, making it easy to read and type.
$password = Random::dashed(int $length = 25, string $delimiter = '-', int $chunkLength = 5): string;
> $string = Random::string();
= "QS`#z&/kP4x/R*gc9MomOMD]Q"&Ry62Z"
> $letters = Random::letters();
= "bDIZrdAOdMgxXnnLTrobaHVLMGaWeDgj"
> $token = Random::token();
= "Jz5QSwuUW7cF7J5flYqyrhSQEfZrvWdV"
> $password = Random::password();
= "gB#'JhYc$1YWMOlN"
> $password = Random::dashed();
= "91m3K-TttUb-tBwdV-C5Llm-IngAC"
This method was one of my reasons for building this package. I was constantly seeing insecure password generation functions, doing all sorts of worrying things (such as the example I used above).
I attempted to build it to be as flexible as possible - allowing you to toggle on and off each of the character types, plus optionally require at least one character from each type (using $requireAll
). I also wanted to support custom character sets (more on that later!). These were all requirements I’d seen come in on Laravel’s password generator, so I wanted to support them.
Note: I specifically avoided the space character from the password character set. I know some folks like to include it, but I personally believe it adds needless complexity (cannot be first or last) and confusion (incomplete copy-paste, word wrap, etc) with no real benefit (it’s just another character).
I added the helper wrappers to cover common use cases, so you don’t need to remember all of the parameters of the primary string()
method, and to make the code read a bit nicer when they are being used.
Dashed Passwords
For cases where you need to generate a random that password user needs to read and type, it’s helpful to break up lengthy strings into smaller chunks with a delimiter. That’s what the dashed()
helper does:
// Random::dashed(int $length = 25, string $delimiter = '-', int $chunkLength = 5): string;
> $password = Random::dashed();
= "91m3K-TttUb-tBwdV-C5Llm-IngAC"
You can tweak the length and delimiter, and chunk length to produce a string in the right format:
> $password = Random::dashed($length = 12, $delimiter = '.', $chunkLength 3);
= "6Jl.6sV.iFA.Hd3"
$requireAll
If you need at least one character from each type, you can toggle the $requireAll
parameter.
For example:
// Only uppercase and symbols were randomly picked
> $password = Random::string(length: 5, requireAll: false);
= ")OR`{"
// At least one of each: lower, upper, number, symbols
> $password = Random::string(length: 5, requireAll: true);
= "d4)T-"
I included this as an option even though modern password recommendations no longer require complexity through character types5, as some slower moving compliance and corporate rules may still require this as an option when generating passwords. It’s disabled by default, but there if you need it.
Custom Character Sets
If you need to override the specific character sets used by string()
, you can do this:
// Override just symbols
$generator = Random::useSymbols(['!', '@', '#', '$', '%', '^', '&', '*', '(', ')'])->string();
= "UZWS2KYiK)(XECWLQbs9yYveH#@gwVpo"
// Override everything
$generator = Random::useLower(range('a', 'f'))
->useUpper(range('G', 'L'))
->useNumbers(range(2, 6))
->useSymbols(['!', '@', '#', '$', '%', '^', '&', '*', '(', ')']);
$string = $generator->string(
$length = 32,
$lower = true,
$upper = true,
$numbers = true,
$symbols = true,
$requireAll = true
);
= "fG22aIG@%fad25b264)fe(b5G3JKe46("
These use*()
methods return a new instance of \Valorin\Random\Generator
, which has all of the random methods but will honour the custom character set.
This gives you full control over what characters are included in the generated strings - which is useful if your system supports limited symbols6, or maybe you want to generate a hexadecimal string?
> $string = Random::useUpper(range('A', 'F'))->string(
$length = 32,
$lower = false,
$upper = true,
$numbers = true,
$symbols = false,
$requireAll = true
);
= "5C65DF598AD08CF94D129040F2668025"
Shuffle an Array, String, or Collection
In additional to generating randomness, you’ll also need to securely shuffle arrays, strings, and collections:
$shuffled = Random::shuffle(
array|string|\Illuminate\Support\Collection $values,
bool $preserveKeys = false
): array|string|\Illuminate\Support\Collection;
> $shuffled = Random::shuffle(['a', 'b', 'c', 'd', 'e']);
= [
"e",
"b",
"a",
"d",
"c",
]
> $shuffled = Random::shuffle(['a', 'b', 'c', 'd', 'e'], $preserveKeys = true);
= [
3 => "d",
2 => "c",
1 => "b",
4 => "e",
0 => "a",
]
> $string = Random::shuffle('abcde');
= "bdcae"
> $collection = new Collection(['a', 'b', 'c', 'd', 'e']);
> $shuffled = Random::shuffle($collection);
> $shuffled->toArray();
= [
"a",
"c",
"e",
"d",
"b",
]
This method is the other reason I wanted to build Random. Almost every implementation of a shuffle I’d seen during my audits (and many outside my audits7) was insecure in some way. Most simply use one of PHP’s raw `shuffle()`
methods - but all of these use insecure randomness. Folks aren’t aware of how to securely shuffle values, and I wanted to change that.
PHP 8.2’s new \Random\Randomizer::shuffleArray()
and \Random\Randomizer::shuffleBytes()
helpers give us secure shuffles, so I’ve wrapped them inside Random, and added support for Collections.
I’ve included support for Laravel’s Collections because the `shuffle()`
method on Collections is insecure and shouldn’t be used8. I’ll try and fix this in v11, but older versions will still contain the insecure shuffle, so having a toolkit that can easily handle them is useful.
Pick X Items or Characters
Following on from shuffle, another common use case is to pick one or more items or characters from a value.
$picks = Random::pick(
array|string|\Illuminate\Support\Collection $values,
int $count
): array|string|\Illuminate\Support\Collection;
$pick = Random::pickOne(
array|string|\Illuminate\Support\Collection $values
): array|string|\Illuminate\Support\Collection;
// Pick from array
> $picked = Random::pick(['a', 'b', 'c', 'd', 'e'], 1);
= "c"
> $picked = Random::pick(['a', 'b', 'c', 'd', 'e'], 3);
= [
"b",
"a",
"c",
]
> $picked = Random::pickOne(['a', 'b', 'c', 'd', 'e']);
= "d"
// Pick from string
> $picked = Random::pick('abcde', 1);
= "a"
> $picked = Random::pick('abcde', 3);
= "dbc"
> $picked = Random::pickOne('abcde');
= "e"
// Pick from Collection
> $collection = new Collection(['a', 'b', 'c', 'd', 'e']);
> $picked = Random::pick($collection, 1);
= "d"
> $picked = Random::pick($collection, 3);
> $picked->toArray();
= [
"b",
"c",
"a",
]
> $picked = Random::pickOne($collection, 1);
= "a"
Picking items randomly is fairly easy if you have incremental keys, but I often see it done using rand()
, or shuffle()
. These functions make picking items securely a trivial operation.
I opted to return the single picked value from the array and collection when $count = 1
, as that avoids having to extract a single value from an array. Also, if you haven’t already figured it out, the pickOne()
method is an alias of pick($values, $count = 1)
.
Support for Collections is included here for the same reason as shuffle()
- there existing Laravel methods aren’t secure.
Using a specific \Random\Engine
Random uses PHP 8.2’s Random\Randomizer
internally for all of it’s randomness, which means you can specify a custom \Random\Engine
to power the randomness.
Random supports this through the use()
method, which builds a custom Generator
around the Randomizer Engine, allowing you to use all of the methods on the generator:
$generator = Random::use(\Random\Engine $engine): \Valorin\Random\Generator;
> $generatorOne = Random::use(new \Random\Engine\Mt19937($seed = 3791));
> $generatorTwo = Random::use(new \Random\Engine\Mt19937($seed = 3791));
> $number = $generatorOne->number(1, 1000);
= 65
> $number = $generatorTwo->number(1, 1000);
= 65
> $password = $generatorOne->password();
= "MOz:^U/Hc?PsZD[e"
> $password = $generatorTwo->password();
= "MOz:^U/Hc?PsZD[e"
The returned Generator object will use the provided Engine, independently to any other generator or the primary Random helpers. This allows you do things like set up seeded randomness in a specific object, which don’t affect any other parts of your application.
This is the reason why my attempt at fixing the insecure randomness in Laravel in February 2023 failed. A number of folks were usingsrand()
in their apps, and changing the randomness implementations changed the output back to random from predicted values and broke things…
This was only an issue because I tried to add it after v10 had been released, so it was a breaking change in a minor release. I’m planning to get it fixed in v11 before it’s released, so the breaking change can be documented and folks using custom seeders can update their code as part of the upgrade.
Unless you specifically need one of the custom Engines, or you have to seed random values, setting a custom Engine isn’t something you’ll probably need to use. But it’s there if you do need it.
The Future of Random
Now that we’ve covered all of the current features, what’s next for Random?
I don’t know… It feels feature complete, but I thought that before adding the dashed()
helper - which I did in the middle of writing this article! So I think it’ll be a case of refining the API and adding helpers and support for specific randomness use cases.
I’d love it to be the go-to package any time you need to do something with randomness beyond simply generating a random number.
Summary
I hope you learned something that from our dive into Random, and it’s got you thinking about how you use randomness in your apps. If you’ve got anything insecure floating around, why not drop in Random and see if it’ll do what you need. 🙂
Please check out the code over at GitHub, and keep an eye out for any weaknesses or bugs I might have overlooked. Plus, let me know if you have any suggestions for how to improve it!
As a side point, I specifically wanted static calls on the
`Random`
class, rather than something like`Factory`
or`Generator`
, that I know some packages use for their primary classes. I find you end up aliasing the imports to remove the generic terms from the code. ↩And I always flag them as an issue in my audits! ↩
You may just find
`Random::number()`
looks better in your code! 😉 ↩Via SMS, Email, etc ↩
See: https://www.troyhunt.com/passwords-evolved-authentication-guidance-for-the-modern-era/ ↩
Another legacy requirement that many folks are stuck with. ↩
Such as Laravel’s shuffle methods! I tried to fix this at the start of 2023, and I’ll have another attempt shortly for Laravel v11. ↩
So is the one on
`Arr`
! ↩