=^.^=

Best-Case PHP Password Hashing with Argon2, bcrypt, Salt 'n' Pepper!

...or: The Phattest, Dopest Hashes Ever.

At present, PHP versions 7.3 and up provide the facility to use the much lauded Argon2id hashing algorithm with password_hash() in addition to the PASSWORD_DEFAULT constant which leaves the farm pointing at PASSWORD_BCRYPT. The official documentation for password_hash() encourages using PASSWORD_DEFAULT as it is expected to change as the landscape of hashing algorithms evolves and PHP's support for different, better algorithms naturally expands. bcrypt (based on the Blowfish algorithm) is still considered quite hardy, yet it has remained the default option since PHP's major version 5. Meanwhile, Argon2i support was added a few years ago in PHP version 7.2 followed by Argon2id in 7.3. Though it has not yet become the PASSWORD_DEFAULT as of major version 8, it is newer (c. 2015 vs c. 1999) and widely held to be more secure.

From the official documentation for password_hash() on php.net regarding one's options as of major version 8:

  • PASSWORD_DEFAULT - Use the bcrypt algorithm (default as of PHP 5.5.0). Note that this constant is designed to change over time as new and stronger algorithms are added to PHP. For that reason, the length of the result from using this identifier can change over time. Therefore, it is recommended to store the result in a database column that can expand beyond 60 characters (255 characters would be a good choice).
  • PASSWORD_BCRYPT - Use the CRYPT_BLOWFISH algorithm to create the hash. This will produce a standard crypt() compatible hash using the "$2y$" identifier. The result will always be a 60 character string, or false on failure.
  • PASSWORD_ARGON2I - Use the Argon2i hashing algorithm to create the hash. This algorithm is only available if PHP has been compiled with Argon2 support.
  • PASSWORD_ARGON2ID - Use the Argon2id hashing algorithm to create the hash. This algorithm is only available if PHP has been compiled with Argon2 support.

I have seen it suggested in myriad threads and articles that bcrypt has a maximum 72 or 56 byte or character limit and that one should be cautious of plaintexts truncating if they exceed this limit. I took it upon myself to do a little experimentation with PHP v.7.4.3's implementation and at first it not seem to be the case. By continuously adding characters to the end of a static string I was successfully able to produce different hashes for passphrases well in excess of 72 characters.

<?php $cleartext = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; print("\n".strlen($cleartext)." chars\n"); // 62 chars print(password_hash($cleartext, PASSWORD_BCRYPT)); // $2y$10$Dvkcz7EuXomYp4l0mlvT3.7.iR8lY/haen24/0unZ04HPozxJKsOu $cleartext = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; print("\n".strlen($cleartext)." chars\n"); // 72 chars print(password_hash($cleartext, PASSWORD_BCRYPT)); // $2y$10$knPetj8DXKLL0fQ59Er.B.oThZVRS4.6gxDxiS6TZoawkYmNBLylm $cleartext = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890123456789'; print("\n".strlen($cleartext)." chars\n"); // 82 chars print(password_hash($cleartext, PASSWORD_BCRYPT)); // $2y$10$Po0r9n4nvn.xa47vd71H9ux4sVlZRIBPfUpkFH.P9sh81AjfuHd1a $cleartext = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678901234567890123456789'; print("\n".strlen($cleartext)." chars\n"); // 92 chars print(password_hash($cleartext, PASSWORD_BCRYPT)); // $2y$10$jOWGAlQXumlM8hhrYi0CYul2tDgyNUr7BUAL6OVTgRy8wdopLpKve

Imagine my embarrassment when I realized I might be getting different hashes by dint of the automatic (as prescribed by the documentation) salting. Overriding the salt, my folly crystalized:

<?php $cleartext = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; print("\n".strlen($cleartext)." chars\n"); // 62 chars print(password_hash($cleartext, PASSWORD_BCRYPT, ['salt' => '0123456789012345678901'])); // $2y$10$012345678901234567890ud2EECbHu.rRFwwnefTITxbknYm62FsO $cleartext = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; print("\n".strlen($cleartext)." chars\n"); // 72 chars print(password_hash($cleartext, PASSWORD_BCRYPT, ['salt' => '0123456789012345678901'])); // $2y$10$012345678901234567890uLrcC7snclVwFuX6TBHjVi4bTVNDpAaG $cleartext = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890123456789'; print("\n".strlen($cleartext)." chars\n"); // 82 chars print(password_hash($cleartext, PASSWORD_BCRYPT, ['salt' => '0123456789012345678901'])); // $2y$10$012345678901234567890uLrcC7snclVwFuX6TBHjVi4bTVNDpAaG $cleartext = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678901234567890123456789'; print("\n".strlen($cleartext)." chars\n"); // 92 chars print(password_hash($cleartext, PASSWORD_BCRYPT, ['salt' => '0123456789012345678901'])); // $2y$10$012345678901234567890uLrcC7snclVwFuX6TBHjVi4bTVNDpAaG

It is therefore important to bear in mind that any more than 72 characters - perhaps even less when dealing with unicode characters outside the initial ASCII compatible block - will be truncated. This is particularly relevant in cases where you are concatenating salts or peppers to the passphrase.

It is recommended to use Argon2id instead of Argon2i wherever possible as it adds side channel resiliency to the Argon2i variant's GPU cracking resistance. In the interest of making as-portable-as-possible code, I have eschewed the PHP Sodium library (the functions of which include possibly more secure and exotic alternatives) and whipped up a quick and dirty little function that checks if the default algorithm is still bcrypt and then, if available, bumps up to the Argon2id variant. I chose to hard code the computational complexity parameters to the values of the default constants on this server's environment because they exceeded the parameters suggested on the Argon2 wikipedia article as well as the recommendations put forward by OWASP and that suggests to me that they are a reasonable floor to anchor to in case the code some day finds shelter in older or more resource competitive environments that come out of the box with weaker settings - and also to make it easier for you to twiddle all the knobs without having to refer to the documentation (which is, as always, excellent).

function user_authentication_hash($plaintext, $pepper='') { $pass_algo = PASSWORD_DEFAULT; // PASSWORD_BCRYPT [default per PHP 5.5-8] | PASSWORD_ARGON2I | PASSWORD_ARGON2ID $pass_options = array('cost' => 15); // Default PASSWORD_BCRYPT cost 10 $pass_supported = password_algos(); if(PASSWORD_DEFAULT == PASSWORD_BCRYPT) // Bump up from BCRYPT (PHP 7.2+), unless a better algo than Argon2 becomes default { if(in_array('argon2id', $pass_supported)) { $pass_algo = PASSWORD_ARGON2ID; $pass_options = array( // https://en.wikipedia.org/wiki/Argon2#Recommended_minimum_parameters 'memory_cost' => 65536, // default PASSWORD_ARGON2_DEFAULT_MEMORY_COST == 65536 KiB (64MB) 'time_cost' => 4, // default PASSWORD_ARGON2_DEFAULT_TIME_COST == 4 'threads' => 1 // default PASSWORD_ARGON2_DEFAULT_THREADS == 1 ); } elseif(in_array('argon2i', $pass_supported)) { $pass_algo = PASSWORD_ARGON2I; $pass_options = array( 'memory_cost' => 65536, 'time_cost' => 4, 'threads' => 1 ); } } return password_hash($plaintext.$pepper, $pass_algo, $pass_options); }

It should be noted that in earlier versions of PHP the PASSWORD_ constants represented integer values; this has changed - for example bcrypt is represented by the token 2y as demonstrated by print()ing PASSWORD_BCRYPT and its element's value in the array returned by password_algos():

<php print(PASSWORD_BCRYPT."\n\n"); print_r(password_algos());

2y Array ( [0] => 2y [1] => argon2i [2] => argon2id )

Being a crypt() formatted hash regardless of your choice of algorithm, you may implement later upconversions as the algorithmic landscape changes by leveraging password_needs_rehash(). Algorithmically agnostic validation of passphrases is handled by password_verify() thus:

$bool = password_verify($plain_text.$pepper, $stored_hash);

My favourite part of the crypt() interface, aside from identifying the appropriate algorithm and parameters inline with the cyphertext, is the automatic inclusion of a randomized salt. You may also wish to investigate the use of an additional string concatenation included from a disparate source, i.e. a variable or algorithm hidden in a file outside of the DocumentRoot or perhaps a separate/remote database, to accomplish salting's savory counterpart: peppering. While salts are stored inline with the cryptographic product and are therefore exposed at the same time as the cyphertext during, for example, a database breach (i.e by way of SQL injection), a secret value stored separately - even a single variable reused - increases the scope an attacker may have to compromise to enable cryptographic attacks and further negates the utility of precomputed rainbow tables.

According to OWASP, a pepper can be even better implemented as the key to a symmetric encryption scheme that further obfuscates one's hashes rather than concatenating a simple string to the cleartext. Cribbing from Harry Gogonas' Medium article "Symmetric Encryption in PHP" (with deference and appreciation for his clean-cut implementation) I drew up a little class for portability:

class UserAuthentication { // https://foxpa.ws/php-password-hashing-and-peppering // This class grabs a symmetric encryption key from $_GLOBALS['pepper'] to permit static instantiation public static function hash($cleartext) { $pass_algo = PASSWORD_DEFAULT; // PASSWORD_BCRYPT [default per PHP 5.5-8] | PASSWORD_ARGON2I | PASSWORD_ARGON2ID $pass_options = array('cost' => 15); // Default PASSWORD_BCRYPT cost 10 $pass_supported = password_algos(); if(PASSWORD_DEFAULT == PASSWORD_BCRYPT) // Bump up from BCRYPT (PHP 7.2+), unless a better algo than Argon2 becomes default { if(in_array('argon2id', $pass_supported)) { $pass_algo = PASSWORD_ARGON2ID; $pass_options = array( // https://en.wikipedia.org/wiki/Argon2#Recommended_minimum_parameters 'memory_cost' => 65536, // default PASSWORD_ARGON2_DEFAULT_MEMORY_COST == 65536 KiB (64MB) 'time_cost' => 4, // default PASSWORD_ARGON2_DEFAULT_TIME_COST == 4 'threads' => 1 // default PASSWORD_ARGON2_DEFAULT_THREADS == 1 ); } elseif(in_array('argon2i', $pass_supported)) { $pass_algo = PASSWORD_ARGON2I; $pass_options = array( 'memory_cost' => 65536, 'time_cost' => 4, 'threads' => 1 ); } } return password_hash($cleartext, $pass_algo, $pass_options); } public static function encrypt($cleartext) { $key = $_GLOBALS{'pepper'}; $length = openssl_cipher_iv_length('AES-256-CBC'); $iv = openssl_random_pseudo_bytes($length); $encrypted = openssl_encrypt($cleartext, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv); $cyphertext = base64_encode($encrypted) . '|' . base64_encode($iv); return $cyphertext; } public static function decrypt($input) { $key = $_GLOBALS{'pepper'}; list($cyphertext, $iv) = explode('|', $input); $iv = base64_decode($iv); $cleartext = openssl_decrypt($cyphertext, 'AES-256-CBC', $key, 0, $iv); return $cleartext; } public static function write($cleartext) // Prepare a hash for insertion into users table { return self::encrypt(self::hash($cleartext)); } public static function read($cyphertext) // Resolve a hash from symmetrically encrypted cyphertext { return self::decrypt($cyphertext); } public static function verify($cleartext, $cyphertext) // Verifies raw user input, $cyphertext is sourced directly from users table { return password_verify($cleartext, self::read($cyphertext)); } }

Recover Cisco IOS Switch Passwords (Quick-and-Dirty)

The official documentation for this procedure is available at Recover Password for Catalyst Fixed Configuration Switches. What follows below is a condensed version, for our mutual convenience:

Hold down the mode switch while power cycling the unit. Issue the following over the serial terminal (9600 8 N 1) and note that load_helper may not be available on your device:
flash_init load_helper rename flash:config.text flash:config.bak boot

Abort the configuration wizard by specifying n at the prompt: --- System Configuration Dialog --- At any point you can enter a question mark '?' for help. Use ctrl-c to abort configuration dialog at any prompt. Default settings are in square brackets '[]'. Continue with configuration dialog? [yes/no]: n

Enter enable mode at the Switch> prompt:
en !-- Recover the old configuration thus: rename flash:config.bak flash:config.text copy flash:config.text system:running-config !-- Then commit new secrets to the configuration: configure terminal enable secret password enable password password line vty 0 15 password password login line con 0 password password write memory

Accidentally Stuck in Zoom aka Screen Magnification in XFCE? Try this.

Using my fresh install of Qubes 4.1.2 I found myself somehow (clearly a result of careless mashing at the keyboard) stuck in an obnoxious screen magnification conundrum. If this sounds like you, try holding down the Alt key and scrolling. If you're on a laptop that does not have designated scroll bars on its (multitouch) touchpad the default XFCE emulation for a scrollwheel is two-fingered scrolling. Now instead of a nuisance we have discovered a handy tool together!

Qubes (as of 4.1.2) Does Not Support SATA Optical Disc Burning

Today I learned that QubesOS, at least as of version 4.1.2 does not support burning CDs, DVDs or Blu-Ray discs via internal SATA drives - at least on the primary controller. According to the official documentation:

Passthrough reading and recording (a.k.a., “burning”) are not supported by Qubes OS. This is not a limitation of Xen, which provides scsiback and scsifront drivers, but of Qubes OS. It will be fixed in the future.

It is necessary to either attach a secondary SATA controller, a USB burner or to burn via dom0 - which of course is strongly recommended against.

furry.media Donation Auction

The furry.media sites are being held ransom to the tune of $170USD/230CAD, due to an unpaid bill with our hosting company. furry.media sites include the under-development VIPlush site, Ychan, TentacleRape.Net and many more sites and services, including our Telegram bot. What this means is that it is no longer possible for me to update, fix, backup or otherwise work on any of these until the bill is paid. If it goes on too long sites will be disconnected (some already are for other reasons but I am currently powerless to fix them).

Due to personal financial struggles I'm not able to cover the bill on my own so I'd like to try something new. I'm asking for donations by paypal to fopspaws [at] gmail.com - the highest donor as of the deadline at midnight, July 19 will receive the following six brand new miniature plushies. Thank you so much for your support! Don't forget to include your shipping address with your donation! For assistance or information please feel free to drop by the foxpa.ws Telegram Group.

[attachment-Z4WxDG]
[attachment-ltpkKx]
[attachment-6MnfL5]