Hashing and encryption

Three jobs every application with users meets: keeping passwords so that nobody - the database's reader included - can read them back; handing a value to the browser that it cannot read or change; and sending a link that works for the one who got it, until it expires. PHP has the building blocks - password_hash, sodium, hash_hmac - and these helpers put them together the safe way.

function what it answers
padHash ( $password ) the password hashed with password_hash and PHP's default algorithm - the text to store
padHashCheck ( $password, $hash ) whether the password matches the hash
padHashNeedsRehash ( $hash ) whether the hash was made with weaker settings than PHP's default now
padEncrypt ( $value ) the value sealed: JSON-encoded, encrypted and signed, as one URL-safe string
padDecrypt ( $payload, $default = NULL ) the value back - or the default for a payload that was changed, cut short or sealed with another key
padSignedUrl ( $page, $vars = [], $expires = NULL ) a link to a page of this application with an expiry time and a signature
padSignatureValid () whether this request came by such a link, unchanged and not expired

Passwords

A password is never stored, only its hash. padHash makes one with a new random salt each time, so two users with the same password get different hashes; padHashCheck tells whether a password typed at login matches the stored hash.

PHP
fragments/hashing_and_encryption_1.php
HTML
fragments/hashing_and_encryption_1.pad
Result
<?php

$phrase
= 'correct horse battery staple';
$hash = padHash ( $phrase );

$right = padHashCheck ( $phrase, $hash ) ? 'yes' : 'no';
$wrong = padHashCheck ( 'Tr0ub4dor&3', $hash ) ? 'yes' : 'no';
$rehash = padHashNeedsRehash ( $hash ) ? 'yes' : 'no';
$shape = substr ( $hash, 0, 4 ) . '... '
. strlen ( $hash ) . ' characters';

?>
<p>Stored: {$shape}</p>
<p>The right password matches: {$right}</p>
<p>Another password matches: {$wrong}</p>
<p>Needs a new hash: {$rehash}</p>

Stored: $2y$... 60 characters

The right password matches: yes

Another password matches: no

Needs a new hash: no

Sealed values

padEncrypt seals a value - text, a number, an array: whatever JSON holds - so it can travel through the browser in a cookie, a hidden field or a link, and come back unread and unchanged. Anyone can carry the payload; only the application can open it.

PHP
fragments/hashing_and_encryption_2.php
HTML
fragments/hashing_and_encryption_2.pad
Result
<?php

$payload
= padEncrypt ( [ 'order' => 1042,
'step' => 'payment' ] );

$length = strlen ( $payload );
$back = padDecrypt ( $payload );
$changed = substr ( $payload, 0, -1 ) . 'x';
$forged = padDecrypt ( $changed, 'refused' );

?>
<p>Sealed: {$length} URL-safe characters</p>
<p>Opened: order {$back.order}, step {$back.step}</p>
<p>One character changed: {$forged}</p>

Sealed: 95 URL-safe characters

Opened: order 1042, step payment

One character changed: refused

Signed links

A link in a mail - confirm your address, download your invoice - has to work without a login, for the one who got it, and stop working after a while. padSignedUrl adds the time it expires and a signature over the application, the page and every value; padSignatureValid (), in the page the link leads to, says whether the request came by such a link.

PHP
fragments/hashing_and_encryption_3.php
HTML
fragments/hashing_and_encryption_3.pad
Result
<?php

$link
= padSignedUrl ( 'invoice', [ 'id' => 1042 ], 3600 );

$shown = preg_replace (
[
'/padExpires=\d+/', '/padSignature=\w+/' ],
[
'padExpires=...', 'padSignature=...' ],
$link
);

$here = padSignatureValid () ? 'yes' : 'no';

?>
<p><code>{$shown}</code></p>
<p>This page came by a signed link: {$here}</p>

http://localhost/pad/manual/?invoice&id=1042&padExpires=...&padSignature=...

This page came by a signed link: no

The page the link leads to checks it before anything else:

// invoice.php
if ( ! padSignatureValid () )
  padRedirect ( 'link_expired' );

$invoice = db ( "RECORD * FROM invoices WHERE id = {0}", [ $id ] );

The key

Sealing and signing use the application key. Without a setting it is the file DATA/keys/<application>.key, made the first time it is needed - 32 random bytes, readable by its owner only, under DATA/ where no URL reaches. Set $padAppKey in _config/config.php - 32 bytes, or 'base64:' and 32 bytes in base64, the form the key file holds - when several servers must read each other's values, and keep it out of version control. A new key makes every value sealed and every link signed before it unreadable. The error reports never show it.