Added OTP Cryptography (In-Development)
This commit is contained in:
parent
3d57347023
commit
d9c8208310
2 changed files with 321 additions and 0 deletions
138
src/Socialbox/Classes/OtpCryptography.php
Normal file
138
src/Socialbox/Classes/OtpCryptography.php
Normal file
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
|
||||
namespace Socialbox\Classes;
|
||||
|
||||
use Random\RandomException;
|
||||
use Socialbox\Exceptions\CryptographyException;
|
||||
|
||||
class OtpCryptography
|
||||
{
|
||||
/**
|
||||
* Generates a random secret key of the specified length.
|
||||
*
|
||||
* @param int $length The length of the secret key in bytes. Default is 32.
|
||||
* @return string Returns the generated secret key as a hexadecimal string.
|
||||
* @throws CryptographyException
|
||||
* @throws RandomException
|
||||
*/
|
||||
public static function generateSecretKey(int $length = 32): string
|
||||
{
|
||||
if($length <= 0)
|
||||
{
|
||||
throw new CryptographyException("Invalid secret key length: must be greater than 0.");
|
||||
}
|
||||
|
||||
return bin2hex(random_bytes($length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a one-time password (OTP) based on the provided parameters.
|
||||
*
|
||||
* @param string $secretKey The secret key used to generate the OTP.
|
||||
* @param int $timeStep The time step in seconds used for OTP generation. Default is 30 seconds.
|
||||
* @param int $digits The number of digits in the OTP. Default is 6.
|
||||
* @param int|null $counter Optional counter value. If not provided, it is calculated based on the current time and time step.
|
||||
* @param string $hashAlgorithm The hash algorithm used for OTP generation. Default is 'sha1'.
|
||||
* @return string Returns the generated OTP as a string with the specified number of digits.
|
||||
* @throws CryptographyException If the generated hash length is less than 20 bytes.
|
||||
*/
|
||||
public static function generateOTP(string $secretKey, int $timeStep=30, int $digits=6, int $counter=null, string $hashAlgorithm='sha1'): string
|
||||
{
|
||||
if ($counter === null)
|
||||
{
|
||||
$counter = floor(time() / $timeStep);
|
||||
}
|
||||
|
||||
$hash = self::hashHmac($hashAlgorithm, pack('J', $counter), $secretKey);
|
||||
|
||||
if (strlen($hash) < 20)
|
||||
{
|
||||
throw new CryptographyException("Invalid hash length: must be at least 20 bytes.");
|
||||
}
|
||||
|
||||
// Validate the $secretKey
|
||||
if (!ctype_xdigit($secretKey))
|
||||
{
|
||||
throw new CryptographyException("Invalid secret key: must be a hexadecimal string.");
|
||||
}
|
||||
|
||||
$offset = ord($hash[strlen($hash) - 1]) & 0x0F;
|
||||
$binary = unpack('N', substr($hash, $offset, 4))[1] & 0x7FFFFFFF;
|
||||
$otp = $binary % (10 ** $digits);
|
||||
|
||||
return str_pad((string)$otp, $digits, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a one-time password (OTP) based on the provided parameters.
|
||||
*
|
||||
* @param string $secretKey The secret key used to generate the OTP.
|
||||
* @param string $otp The one-time password to verify.
|
||||
* @param int $timeStep The time step in seconds used for OTP generation. Default is 30 seconds.
|
||||
* @param int $window The allowed window of time steps for verification. Default is 1.
|
||||
* @param int $digits The number of digits in the OTP. Default is 6.
|
||||
* @param string $hashAlgorithm The hash algorithm used for OTP generation. Default is 'sha512'.
|
||||
* @return bool Returns true if the OTP is valid within the provided parameters, otherwise false.
|
||||
*/
|
||||
public static function verifyOTP(string $secretKey, string $otp, int $timeStep=30, int $window=1, int $digits=6, string $hashAlgorithm='sha512'): bool
|
||||
{
|
||||
$currentTime = time();
|
||||
$counter = floor($currentTime / $timeStep);
|
||||
|
||||
for ($i = -$window; $i <= $window; $i++)
|
||||
{
|
||||
$testCounter = $counter + $i;
|
||||
$expectedOtp = self::generateOTP($secretKey, $timeStep, $digits, $testCounter, $hashAlgorithm);
|
||||
|
||||
if (hash_equals($expectedOtp, $otp))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a QR code payload for a TOTP-based authentication system.
|
||||
*
|
||||
* The method constructs a URI in the format compatible with TOTP applications.
|
||||
*
|
||||
* @param string $account The account name or identifier associated with the QR code.
|
||||
* @param string $secretKey The secret key to be included in the payload.
|
||||
* @param string $issuer The issuer name to identify the organization or service.
|
||||
*
|
||||
* @return string A formatted string representing the QR code payload.
|
||||
*
|
||||
* @throws CryptographyException If the domain configuration is missing.
|
||||
*/
|
||||
public static function generateQrPayload(string $account, string $secretKey, string $issuer): string
|
||||
{
|
||||
$domain = Configuration::getInstanceConfiguration()->getDomain();
|
||||
|
||||
if (!$domain)
|
||||
{
|
||||
throw new CryptographyException("Domain configuration is missing.");
|
||||
}
|
||||
|
||||
return sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s", rawurlencode($domain), rawurlencode($account), rawurlencode($secretKey), rawurlencode($issuer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a hash-based message authentication code (HMAC) using the specified algorithm.
|
||||
*
|
||||
* @param string $algorithm The hashing algorithm to be used (e.g., 'sha1', 'sha256', 'sha384', 'sha512').
|
||||
* @param string $data The data to be hashed.
|
||||
* @param string $key The secret key used for the HMAC generation.
|
||||
*
|
||||
* @return string The generated HMAC as a raw binary string.
|
||||
*
|
||||
* @*/
|
||||
private static function hashHmac(string $algorithm, string $data, string $key): string
|
||||
{
|
||||
return match($algorithm)
|
||||
{
|
||||
'sha1', 'sha256', 'sha384', 'sha512' => hash_hmac($algorithm, $data, $key, true),
|
||||
default => throw new CryptographyException('Algorithm not supported')
|
||||
};
|
||||
}
|
||||
}
|
183
tests/Socialbox/Classes/OtpCryptographyTest.php
Normal file
183
tests/Socialbox/Classes/OtpCryptographyTest.php
Normal file
|
@ -0,0 +1,183 @@
|
|||
<?php
|
||||
|
||||
namespace Socialbox\Classes;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Socialbox\Exceptions\CryptographyException;
|
||||
|
||||
class OtpCryptographyTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Test that generateSecretKey generates a key of default length.
|
||||
*/
|
||||
public function testGenerateSecretKeyDefaultLength()
|
||||
{
|
||||
$result = OtpCryptography::generateSecretKey();
|
||||
$this->assertEquals(64, strlen($result), 'Default secret key length (32 bytes) should produce 64 hex characters.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test generateOTP with valid parameters to ensure a correct OTP.
|
||||
*/
|
||||
public function testGenerateOTPValidParameters()
|
||||
{
|
||||
$secretKey = '12345678901234567890';
|
||||
$timeStep = 30;
|
||||
$digits = 6;
|
||||
$counter = 1;
|
||||
$otp = OtpCryptography::generateOTP($secretKey, $timeStep, $digits, $counter, 'sha1');
|
||||
$this->assertEquals(6, strlen($otp), 'OTP should have the correct number of digits.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test generateOTP produces consistent results for the same inputs.
|
||||
*/
|
||||
public function testGenerateOTPConsistency()
|
||||
{
|
||||
$secretKey = '12345678901234567890';
|
||||
$timeStep = 30;
|
||||
$digits = 6;
|
||||
$counter = 1;
|
||||
$otp1 = OtpCryptography::generateOTP($secretKey, $timeStep, $digits, $counter, 'sha1');
|
||||
$otp2 = OtpCryptography::generateOTP($secretKey, $timeStep, $digits, $counter, 'sha1');
|
||||
$this->assertEquals($otp1, $otp2, 'OTP should be consistent for the same secret key, counter, and configuration.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test generateOTP produces different OTPs for the same secret key but different counters.
|
||||
*/
|
||||
public function testGenerateOTPDifferentCounters()
|
||||
{
|
||||
$secretKey = '12345678901234567890';
|
||||
$timeStep = 30;
|
||||
$digits = 6;
|
||||
$otp1 = OtpCryptography::generateOTP($secretKey, $timeStep, $digits, 1, 'sha1');
|
||||
$otp2 = OtpCryptography::generateOTP($secretKey, $timeStep, $digits, 2, 'sha1');
|
||||
$this->assertNotEquals($otp1, $otp2, 'OTP should differ for the same secret key but different counters.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test generateOTP throws an exception if hash length is invalid.
|
||||
*/
|
||||
public function testGenerateOTPInvalidHashLength()
|
||||
{
|
||||
$secretKey = 'shortkey';
|
||||
$timeStep = 30;
|
||||
$digits = 6;
|
||||
$this->expectException(CryptographyException::class);
|
||||
OtpCryptography::generateOTP($secretKey, $timeStep, $digits, 1, 'sha1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test generateOTP correctly handles different digit lengths.
|
||||
*/
|
||||
public function testGenerateOTPDigitLength()
|
||||
{
|
||||
$secretKey = '12345678901234567890';
|
||||
$timeStep = 30;
|
||||
$otp6Digits = OtpCryptography::generateOTP($secretKey, $timeStep, 6, 1, 'sha1');
|
||||
$otp8Digits = OtpCryptography::generateOTP($secretKey, $timeStep, 8, 1, 'sha1');
|
||||
$this->assertEquals(6, strlen($otp6Digits), 'OTP with 6 digits should be generated.');
|
||||
$this->assertEquals(8, strlen($otp8Digits), 'OTP with 8 digits should be generated.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that generateSecretKey generates a key of custom length.
|
||||
*/
|
||||
public function testGenerateSecretKeyCustomLength()
|
||||
{
|
||||
$customLength = 16; // 16 bytes
|
||||
$result = OtpCryptography::generateSecretKey($customLength);
|
||||
$this->assertEquals(32, strlen($result), "A secret key of $customLength bytes should produce 32 hex characters.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that generateSecretKey with a length of 0 results in a valid empty key.
|
||||
*/
|
||||
public function testGenerateSecretKeyZeroLength()
|
||||
{
|
||||
$this->expectException(CryptographyException::class);
|
||||
OtpCryptography::generateSecretKey(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that generateSecretKey with negative length throws an exception.
|
||||
*/
|
||||
public function testGenerateSecretKeyNegativeLength()
|
||||
{
|
||||
$this->expectException(CryptographyException::class);
|
||||
OtpCryptography::generateSecretKey(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that generateSecretKey produces unique keys for multiple calls.
|
||||
*/
|
||||
public function testGenerateSecretKeyUniqueKeys()
|
||||
{
|
||||
$key1 = OtpCryptography::generateSecretKey();
|
||||
$key2 = OtpCryptography::generateSecretKey();
|
||||
$this->assertNotEquals($key1, $key2, 'Generated secret keys should be unique.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test verifyOTP with a valid OTP to ensure it returns true.
|
||||
*/
|
||||
public function testVerifyOTPValid()
|
||||
{
|
||||
$secretKey = '12345678901234567890';
|
||||
$timeStep = 30;
|
||||
$digits = 6;
|
||||
// Generate the OTP to test validity
|
||||
$otp = OtpCryptography::generateOTP($secretKey, $timeStep, $digits, null, 'sha512');
|
||||
$this->assertTrue(OtpCryptography::verifyOTP($secretKey, $otp, $timeStep, 1, $digits, 'sha512'), 'verifyOTP should return true for a valid OTP.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test verifyOTP with an invalid OTP to ensure it returns false.
|
||||
*/
|
||||
public function testVerifyOTPInvalid()
|
||||
{
|
||||
$secretKey = '12345678901234567890';
|
||||
$timeStep = 30;
|
||||
$digits = 6;
|
||||
$invalidOtp = '999999'; // Arbitrary invalid OTP
|
||||
$this->assertFalse(OtpCryptography::verifyOTP($secretKey, $invalidOtp, $timeStep, 1, $digits, 'sha512'), 'verifyOTP should return false for an invalid OTP.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test verifyOTP with an expired OTP window to ensure it returns false.
|
||||
*/
|
||||
public function testVerifyOTPEExpiredWindow()
|
||||
{
|
||||
$secretKey = '12345678901234567890';
|
||||
$timeStep = 30;
|
||||
$digits = 6;
|
||||
$pastCounter = floor(time() / $timeStep) - 10; // Simulate OTP generated far in the past
|
||||
$expiredOtp = OtpCryptography::generateOTP($secretKey, $timeStep, $digits, $pastCounter, 'sha512');
|
||||
$this->assertFalse(OtpCryptography::verifyOTP($secretKey, $expiredOtp, $timeStep, 1, $digits, 'sha512'), 'verifyOTP should return false for an OTP outside the valid window.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test verifyOTP with an invalid secret key to ensure it throws an exception.
|
||||
*/
|
||||
public function testVerifyOTPInvalidSecretKey()
|
||||
{
|
||||
$this->expectException(CryptographyException::class);
|
||||
$invalidSecretKey = 'invalidkey';
|
||||
$otp = '123456';
|
||||
OtpCryptography::verifyOTP($invalidSecretKey, $otp, 30, 1, 6, 'sha512');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test verifyOTP with an incorrect digit length to ensure it returns false.
|
||||
*/
|
||||
public function testVerifyOTPInvalidDigitLength()
|
||||
{
|
||||
$secretKey = '12345678901234567890';
|
||||
$timeStep = 30;
|
||||
$digits = 8; // Generate an 8-digit OTP
|
||||
$validOtp = OtpCryptography::generateOTP($secretKey, $timeStep, $digits, null, 'sha512');
|
||||
// Verifying with a 6-digit configuration instead
|
||||
$this->assertFalse(OtpCryptography::verifyOTP($secretKey, $validOtp, $timeStep, 1, 6, 'sha512'), 'verifyOTP should return false if the digit length does not match.');
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue