Nonces is a gang of worms.
No, indeed, one of the motivations for several CAESARs was to create an authenticated encryption scheme, preferably based on a stream cipher, that is resistant to nonce reuse. (Reusing nonce with AES-CTR, for example, destroys the privacy of your message to the extent that the student can decrypt it for the first year.)
There are three main schools of thought with noses:
- In symmetric-key cryptography: use an incrementing counter, ignoring its reuse. (This also means using a separate counter for the sender and receiver.) This requires stateful programming (i.e., somewhere the nonce value is stored, so each request does not start with
1 ). - States of random random. Create a random nonce and then remember it for confirmation later. This is the strategy used to defeat CSRF attacks , which sounds closer to what is offered here.
- Large random characters without attacks. Given a robust random number generator, you can almost guarantee that you never repeat nonce twice in your life. This is the strategy used by NaCl for encryption.
So, keeping in mind, the main questions to ask are:
- Which of the above schools of thought is most important for the problem you are trying to solve?
- How do you generate nonce?
- How do you check nonce?
Creating Nonce
The answer to question 2 for any random nonce is to use CSPRNG. For PHP projects, this means one of:
random_bytes() for projects with PHP 7+- paragonie / random_compat , PHP 5 polyfill for
random_bytes() - ircmaxell / RandomLib , which is a Swiss army knife of utilities of randomness, which in most projects that deal with randomness (for example, resetting a password) should consider using instead of its own
These two are morally equivalent:
$factory = new RandomLib\Factory; $generator = $factory->getMediumStrengthGenerator(); $_SESSION['nonce'] [] = $generator->generate(32);
and
$_SESSION['nonce'] []= random_bytes(32);
Incompatibility Check
Stateful
Simple and recommended statements:
$found = array_search($nonce, $_SESSION['nonces']); if (!$found) { throw new Exception("Nonce not found! Handle this or the app crashes"); } // Yay, now delete it. unset($_SESSION['nonce'][$found]);
Remember to replace array_search() with a database search or memcached, etc.
Stateless (dragons will be here)
This is a complex problem: you need to somehow prevent repeated attacks, but your server has general amnesia after every HTTP request.
The only sensible solution would be to authenticate the expiration date / time to minimize the effectiveness of the replay attacks. For example:
// Generating a message bearing a nonce $nonce = random_bytes(32); $expires = new DateTime('now') ->add(new DateInterval('PT01H')); $message = json_encode([ 'nonce' => base64_encode($nonce), 'expires' => $expires->format('Ymd\TH:i:s') ]); $publishThis = base64_encode( hash_hmac('sha256', $message, $authenticationKey, true) . $message ); // Validating a message and retrieving the nonce $decoded = base64_decode($input); if ($decoded === false) { throw new Exception("Encoding error"); } $mac = mb_substr($decoded, 0, 32, '8bit'); // stored $message = mb_substr($decoded, 32, null, '8bit'); $calc = hash_hmac('sha256', $message, $authenticationKey, true); // calcuated if (!hash_equals($calc, $mac)) { throw new Exception("Invalid MAC"); } $message = json_decode($message); $currTime = new DateTime('NOW'); $expireTime = new DateTime($message->expires); if ($currTime > $expireTime) { throw new Exception("Expired token"); } $nonce = $message->nonce; // Valid (for one hour)
A careful observer will notice that this is basically a non-standard version of JSON Web Tokens .