Using argon2id as part of a 2FA (2 Factor Authentication) process on the IBM i
In the following blog we will highlight example code showing how to perform password hashing using argon2id. This is part of a multipart article outlining a complete example web app on the IBM i.
In this second part of the blog we will demonstrate, with code, examples of how to create password hashes that will be stored as part of our 2FA (2 Factor Authentication) process.
The code examples are supplied purely as a guide on how these various facets of application security could be deployed on the IBM i platform. The example application that we will outline later is not meant to be a finished product but is an aide which may help you develop the framework and direction necessary to drive a web based project forward. The entire demonstration app will be downloadable and will setup all the required configuration descriptions and settings using the supplied commands. We are using Zend Community PHP where the Command Line Interface (CLI) program runs from /QOpenSys/pkgs/bin/php. The code was written using IBM i (i5/OS) v7.2
When building a login mechanism for a web based IBM i app it is not practical to create IBM i user profiles for every user. A better way of doing this is storing the logon credentials in a database or validation list (object type *VLDL). A large security risk occurs if the passwords are stored as plain text in a database. There is still a risk when storing passwords using encryption when the keys used to unencrypt the passwords get into the wrong hands.
A better way of treating passwords is to store a hash of the password and use a hashing algorithm to match passwords at logon. Password hashing algorithms use a one way process whereby the hash of the password can be discovered with the salt but the hash cannot be easily reverse engineered to discover the password. This way if someone does gain access to the password hash they will find it very difficult to find out the actual password. Argon2id uses a random salt for each hash that is created and this gets stored with the hash. That way a brute force attack upon a multiple number of hashes would have to restart the attack from scratch for each individual hash. A good password policy which reduces the instance of weak passwords is an important part of any security strategy.
Here is a code snippet which shows how to run the PHP script from a CL program using QSH in order to hash the password. The PHP script writes the password hash to a temporary file in the IFS which is then retrieved using a read operation.
//---------------------------------------------------------------------------------------------// // All source code included in this document are provided on an "AS IS" basis without warranty // // of any kind. NOVAGEM WILL NOT BE LIABLE FOR ANY ACTUAL, DIRECT, SPECIAL, INCIDENTAL, OR // // INDIRECT DAMAGES OR FOR ANY ECONOMIC CONSEQUENTIAL DAMAGES INCLUDING LOST PROFITS OR // // SAVINGS. // //---------------------------------------------------------------------------------------------//
dcl-s resultFile varchar(100); dcl-s fd int(10); dcl-s rc int(10); dcl-s password sqltype(VARBINARY:432) ; dcl-ds *n PSDS; jobNo char(6) pos(264); end-ds; dcl-pr HASHPWD extpgm('HASHPWD'); *N like(password) const; *N like(resultFile) const; end-pr; // ************************ begsr GeneratePasswordHash; // ************************ // This code runs a CL program that uses QSH to run a PHP script. It passes the password input by the user as // an argument to the script which then takes the password and runs the sodium_crypto_pwhash_str function // to generate the argon2id password hash. The hash is then written to a file in the IFS, created with a file // name generated below, and read by this program in order to write the hash to the user details or a // validation list. // We have demonstrated returning the password hash back from PHP by writing it to a file to keep things simple // but other methods to return arguments back from PHP are available. exec sql SET :resultFile=TRIM(:'/exampleapp/tmp/hsh' CONCAT :jobNo CONCAT VARCHAR_FORMAT(NOW(12),'YYYYMMDDHHMISSFF12') CONCAT CHAR(INT(RAND()*10000))); // Unique file name to hold result of password hashing algorithm // Call PHP password hash function (Run via QSH in a CL program) HASHPWD(%trim(password):%trim(resultFile)); // Read the result from the file created and populated in the PHP hash password script and then delete the file. %len(password)=%len(password:*MAX); fd=open(resultFile:O_RDONLY+O_TEXTDATA); rc=read(fd:%addr(password:*DATA):%size(password)); %len(password)=rc; rc=close(fd); rc=unlink(%trim(resultFile)); // *** endsr; // ***
Below is the CL program which uses QSH to run the PHP script which performs the password hash
/* Hash Password */ PGM PARM(&PASSWORD &RESULTFILE) DCL VAR(&CMD) TYPE(*CHAR) LEN(1000) DCL VAR(&LEN) TYPE(*DEC) LEN(5 0) DCL VAR(&LOGCLPGM) TYPE(*CHAR) LEN(10) DCL VAR(&LOGLVL) TYPE(*CHAR) LEN(1) DCL VAR(&LOGSEV) TYPE(*DEC) LEN(2 0) DCL VAR(&LOGTYPE) TYPE(*CHAR) LEN(10) DCL VAR(&NULL) TYPE(*CHAR) LEN(1) VALUE(X'00') DCL VAR(&PASSWORD) TYPE(*CHAR) LEN(434) DCL VAR(&RESULTFILE) TYPE(*CHAR) LEN(102) /* Massage variable length field so it can be used in CL */ CHGVAR VAR(&LEN) VALUE(%BIN(&PASSWORD 1 2)) CHGVAR VAR(&PASSWORD) VALUE(%SST(&PASSWORD 3 &LEN)) CHGVAR VAR(&LEN) VALUE(%BIN(&RESULTFILE 1 2)) CHGVAR VAR(&RESULTFILE) VALUE(%SST(&RESULTFILE 3 &LEN)) // Setup call to PHP script with arguments CHGVAR VAR(&CMD) VALUE('/QOpenSys/pkgs/bin/php ' *BCAT + '/exampleapp/php/hashpassword.php ' *CAT '''' + *CAT &PASSWORD *TCAT ''' ''' *CAT &RESULTFILE + *TCAT ''' ''HASH_PASSWORD''') /* Save current logging levels */ RTVJOBA LOGLVL(&LOGLVL) LOGSEV(&LOGSEV) + LOGTYPE(&LOGTYPE) LOGCLPGM(&LOGCLPGM) /* Log nothing for QSH job */ CHGJOB LOG(0 99 *NOLIST) LOGCLPGM(*NO) /* Run PHP hash password script */ QSH CMD(&CMD) /* Put back logging levels */ CHGJOB LOG(&LOGLVL &LOGSEV &LOGTYPE) LOGCLPGM(&LOGCLPGM) ENDPGM
Below is the PHP script used to generate the argon2id password hash.
<?php // only allow from the command line and with arguments if (php_sapi_name() != 'cli' or $argv[3] != 'HASH_PASSWORD') { die(); } // hash password $hash = sodium_crypto_pwhash_str( $argv[1], SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE ); /* Write password hash to a file in the IFS */ file_put_contents($argv[2], $hash, LOCK_EX ); ?>
In order to verify the password when a user is logging in we have created the following SQLRPGLE code snippet which runs a CL program in order to call the PHP script via QSH that performs the password hash verify function.
dcl-s verifiedOKKey char(38); // ********************* dcl-proc VerifyPassword; // ********************* dcl-pi *N char(3); end-pi; dcl-pr VERIFYPWD extpgm('VERIFYPWD'); *N like(storedPasswordHash) const; *N like(password) const; *N like(resultFile) const; *N like(verifiedOKKey) const; end-pr; dcl-s fd int(10); dcl-s rc int(10); dcl-s verifyresult like(verifiedOKKey); dcl-s resultFile varchar(100); // The PHP verify password function writes the verifiedOKKey string, random string generated below, to a file // which this program compares on the way back to see if the verify was OK. // Other methods to return arguments back from PHP do exist but for simplicity we are writing it to a file. exec sql SET :verifiedOKKey=HEX(:jobNo CONCAT GENERATE_UNIQUE()); exec sql SET :resultFile=TRIM('/exampleapp/tmp/vfy' CONCAT :jobNo CONCAT VARCHAR_FORMAT(NOW(12),'YYYYMMDDHHMISSFF12') CONCAT CHAR(INT(RAND()*10000))); // Call PHP password verify function (Run via QSH in a CL program) VERIFYPWD(%trim(storedPasswordHash):%trim(password):%trim(resultFile):%trim(verifiedOKKey)); // Read the result from the file created and populated in the PHP verify password function and then delete the file fd=open(resultFile:O_RDONLY+O_TEXTDATA); rc=read(fd:%addr(verifyResult):%size(verifyResult)); rc=close(fd); rc=unlink(%trim(resultFile)); // Compare random string passed to, and back from, the PHP password verify function to determine if the password is OK if verifiedOKKey=verifyResult; return 'Yes'; else; return 'No'; endif; // ****** end-proc; // ******
Below is the CL program which uses QSH to run the PHP script which performs the password hash verify.
/* Verify Password */ PGM PARM(&STOREDPHSH &PASSWORD &RESULTFILE + &VRFYOKKY) DCL VAR(&CMD) TYPE(*CHAR) LEN(1000) DCL VAR(&LEN) TYPE(*DEC) LEN(5 0) DCL VAR(&LOGCLPGM) TYPE(*CHAR) LEN(10) DCL VAR(&LOGLVL) TYPE(*CHAR) LEN(1) DCL VAR(&LOGSEV) TYPE(*DEC) LEN(2 0) DCL VAR(&LOGTYPE) TYPE(*CHAR) LEN(10) DCL VAR(&NULL) TYPE(*CHAR) LEN(1) VALUE(X'00') DCL VAR(&PASSWORD) TYPE(*CHAR) LEN(432) DCL VAR(&RESULTFILE) TYPE(*CHAR) LEN(102) DCL VAR(&STOREDPHSH) TYPE(*CHAR) LEN(432) DCL VAR(&VRFYOKKY) TYPE(*CHAR) LEN(38) /* Massage variable length field so it can be used in CL */ CHGVAR VAR(&LEN) VALUE(%BIN(&RESULTFILE 1 2)) CHGVAR VAR(&RESULTFILE) VALUE(%SST(&RESULTFILE 3 &LEN)) /* Setup call to PHP script with arguments */ CHGVAR VAR(&CMD) VALUE('/QOpenSys/pkgs/bin/php ' *BCAT + /exampleapp/php/verifypassword.php ' *CAT '''' + *CAT &STOREDPHSH *TCAT ''' ''' *CAT + &PASSWORD *TCAT ''' ''' *CAT &RESULTFILE + *TCAT ''' ''' *CAT &VRFYOKKY *TCAT ''' ''CHECK_PASSWORD''') /* Save current logging levels */ RTVJOBA LOGLVL(&LOGLVL) LOGSEV(&LOGSEV) + LOGTYPE(&LOGTYPE) LOGCLPGM(&LOGCLPGM) /* Log nothing for QSH job */ CHGJOB LOG(0 99 *NOLIST) LOGCLPGM(*NO) /* Run PHP verify password script */ QSH CMD(&CMD) /* Put back logging levels */ CHGJOB LOG(&LOGLVL &LOGSEV &LOGTYPE) LOGCLPGM(&LOGCLPGM) ENDPGM
Below is the PHP script used to perform the argon2id password hash verify.
<?php // only allow from the command line and with arguments if (php_sapi_name() != 'cli' or $argv[5] != 'CHECK_PASSWORD') { die(); } // verify password if (sodium_crypto_pwhash_str_verify($argv[1], $argv[2])) { /* Password OK then write password OK key back in result file */ file_put_contents($argv[3], $argv[4], LOCK_EX ); }