Add OTP support with implementation for creation, deletion, and verification.
This commit is contained in:
parent
d9c8208310
commit
866bb90f2a
8 changed files with 572 additions and 28 deletions
1
.idea/sqldialects.xml
generated
1
.idea/sqldialects.xml
generated
|
@ -7,6 +7,7 @@
|
|||
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/sessions.sql" dialect="MariaDB" />
|
||||
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/variables.sql" dialect="MariaDB" />
|
||||
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/CaptchaManager.php" dialect="MariaDB" />
|
||||
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/OneTimePasswordManager.php" dialect="MariaDB" />
|
||||
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/PasswordManager.php" dialect="MariaDB" />
|
||||
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/RegisteredPeerManager.php" dialect="MariaDB" />
|
||||
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/ResolvedDnsRecordsManager.php" dialect="MariaDB" />
|
||||
|
|
|
@ -45,6 +45,17 @@
|
|||
$config->setDefault('security.display_internal_exceptions', false);
|
||||
$config->setDefault('security.resolved_servers_ttl', 600);
|
||||
$config->setDefault('security.captcha_ttl', 200);
|
||||
// Server-side OTP Security options
|
||||
// The time step in seconds for the OTP generation
|
||||
// Default: 30 seconds
|
||||
$config->setDefault('security.otp_secret_key_length', 32);
|
||||
$config->setDefault('security.otp_time_step', 30);
|
||||
// The number of digits in the OTP
|
||||
$config->setDefault('security.otp_digits', 6);
|
||||
// The hash algorithm to use for the OTP generation (sha1, sha256, sha512)
|
||||
$config->setDefault('security.otp_hash_algorithm', 'sha512');
|
||||
// The window of time steps to allow for OTP verification
|
||||
$config->setDefault('security.otp_window', 1);
|
||||
|
||||
// Cryptography Configuration
|
||||
// The Unix Timestamp for when the host's keypair should expire
|
||||
|
|
|
@ -7,6 +7,11 @@
|
|||
private bool $displayInternalExceptions;
|
||||
private int $resolvedServersTtl;
|
||||
private int $captchaTtl;
|
||||
private int $otpSecretKeyLength;
|
||||
private int $otpTimeStep;
|
||||
private int $otpDigits;
|
||||
private string $otpHashAlgorithm;
|
||||
private int $otpWindow;
|
||||
|
||||
/**
|
||||
* Constructor method for initializing class properties.
|
||||
|
@ -20,6 +25,11 @@
|
|||
$this->displayInternalExceptions = $data['display_internal_exceptions'];
|
||||
$this->resolvedServersTtl = $data['resolved_servers_ttl'];
|
||||
$this->captchaTtl = $data['captcha_ttl'];
|
||||
$this->otpSecretKeyLength = $data['otp_secret_key_length'];
|
||||
$this->otpTimeStep = $data['otp_time_step'];
|
||||
$this->otpDigits = $data['otp_digits'];
|
||||
$this->otpHashAlgorithm = $data['otp_hash_algorithm'];
|
||||
$this->otpWindow = $data['otp_window'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -52,4 +62,53 @@
|
|||
return $this->captchaTtl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the length of the secret key used for OTP generation.
|
||||
*
|
||||
* @return int The length of the secret key used for OTP generation.
|
||||
*/
|
||||
public function getOtpSecretKeyLength(): int
|
||||
{
|
||||
return $this->otpSecretKeyLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the time step value for OTP generation.
|
||||
*
|
||||
* @return int The time step value for OTP generation.
|
||||
*/
|
||||
public function getOtpTimeStep(): int
|
||||
{
|
||||
return $this->otpTimeStep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the number of digits in the OTP.
|
||||
*
|
||||
* @return int The number of digits in the OTP.
|
||||
*/
|
||||
public function getOtpDigits(): int
|
||||
{
|
||||
return $this->otpDigits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the hash algorithm used for OTP generation.
|
||||
*
|
||||
* @return string The hash algorithm used for OTP generation.
|
||||
*/
|
||||
public function getOtpHashAlgorithm(): string
|
||||
{
|
||||
return $this->otpHashAlgorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the window value for OTP generation.
|
||||
*
|
||||
* @return int The window value for OTP generation.
|
||||
*/
|
||||
public function getOtpWindow(): int
|
||||
{
|
||||
return $this->otpWindow;
|
||||
}
|
||||
}
|
|
@ -7,15 +7,17 @@
|
|||
|
||||
class OtpCryptography
|
||||
{
|
||||
private const string URI_FORMAT = 'otpauth://totp/%s?secret=%s%s&algorithm=%s&digits=%d&period=%d';
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @throws CryptographyException If the length is less than or equal to 0.
|
||||
* @throws RandomException If an error occurs while generating random bytes.
|
||||
*/
|
||||
public static function generateSecretKey(int $length = 32): string
|
||||
public static function generateSecretKey(int $length=32): string
|
||||
{
|
||||
if($length <= 0)
|
||||
{
|
||||
|
@ -32,11 +34,11 @@
|
|||
* @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'.
|
||||
* @param string $hashAlgorithm The hash algorithm used for OTP generation. Default is 'sha512'.
|
||||
* @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
|
||||
public static function generateOTP(string $secretKey, int $timeStep=30, int $digits=6, int $counter=null, string $hashAlgorithm='sha512'): string
|
||||
{
|
||||
if ($counter === null)
|
||||
{
|
||||
|
@ -73,6 +75,7 @@
|
|||
* @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.
|
||||
* @throws CryptographyException If the generated hash length is less than 20 bytes.
|
||||
*/
|
||||
public static function verifyOTP(string $secretKey, string $otp, int $timeStep=30, int $window=1, int $digits=6, string $hashAlgorithm='sha512'): bool
|
||||
{
|
||||
|
@ -93,28 +96,20 @@
|
|||
}
|
||||
|
||||
/**
|
||||
* Generates a QR code payload for a TOTP-based authentication system.
|
||||
* Generates a key URI for use in configuring an authenticator application.
|
||||
*
|
||||
* 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.
|
||||
* @param string $label A unique label to identify the account (e.g., user or service name).
|
||||
* @param string $secretKey The secret key used for generating the OTP.
|
||||
* @param string|null $issuer The name of the organization or service issuing the key. Default is null.
|
||||
* @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 generated OTP. Default is 6.
|
||||
* @param string $hashAlgorithm The hash algorithm used for OTP generation. Default is 'sha512'.
|
||||
* @return string Returns the URI string formatted
|
||||
*/
|
||||
public static function generateQrPayload(string $account, string $secretKey, string $issuer): string
|
||||
public static function generateKeyUri(string $label, string $secretKey, ?string $issuer = null, int $timeStep=30, int $digits=6, string $hashAlgorithm='sha512'): 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));
|
||||
$issuerPart = $issuer ? "&issuer=" . rawurlencode($issuer) : '';
|
||||
return sprintf(self::URI_FORMAT, rawurlencode($label), $secretKey, $issuerPart, strtoupper($hashAlgorithm), $digits, $timeStep);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -123,15 +118,14 @@
|
|||
* @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.
|
||||
*
|
||||
* @*/
|
||||
* @throws CryptographyException If the algorithm is not supported.
|
||||
*/
|
||||
private static function hashHmac(string $algorithm, string $data, string $key): string
|
||||
{
|
||||
return match($algorithm)
|
||||
{
|
||||
'sha1', 'sha256', 'sha384', 'sha512' => hash_hmac($algorithm, $data, $key, true),
|
||||
'sha1', 'sha256', 'sha512' => hash_hmac($algorithm, $data, $key, true),
|
||||
default => throw new CryptographyException('Algorithm not supported')
|
||||
};
|
||||
}
|
||||
|
|
93
src/Socialbox/Classes/StandardMethods/SettingsDeleteOtp.php
Normal file
93
src/Socialbox/Classes/StandardMethods/SettingsDeleteOtp.php
Normal file
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
namespace Socialbox\Classes\StandardMethods;
|
||||
|
||||
use Exception;
|
||||
use Socialbox\Abstracts\Method;
|
||||
use Socialbox\Classes\Configuration;
|
||||
use Socialbox\Classes\Cryptography;
|
||||
use Socialbox\Enums\Flags\SessionFlags;
|
||||
use Socialbox\Enums\StandardError;
|
||||
use Socialbox\Exceptions\DatabaseOperationException;
|
||||
use Socialbox\Exceptions\StandardException;
|
||||
use Socialbox\Interfaces\SerializableInterface;
|
||||
use Socialbox\Managers\OneTimePasswordManager;
|
||||
use Socialbox\Managers\PasswordManager;
|
||||
use Socialbox\Managers\SessionManager;
|
||||
use Socialbox\Objects\ClientRequest;
|
||||
use Socialbox\Objects\RpcRequest;
|
||||
|
||||
class SettingsDeleteOtp extends Method
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
|
||||
{
|
||||
if(Configuration::getRegistrationConfiguration()->isOtpRequired())
|
||||
{
|
||||
return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, 'One Time Password is required for this server');
|
||||
}
|
||||
|
||||
$peer = $request->getPeer();
|
||||
|
||||
try
|
||||
{
|
||||
if (!OneTimePasswordManager::usesOtp($peer->getUuid()))
|
||||
{
|
||||
return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, "Cannot delete One Time Password when none is set");
|
||||
}
|
||||
}
|
||||
catch (DatabaseOperationException $e)
|
||||
{
|
||||
throw new StandardException('Failed to check One Time Password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$usesPassword = PasswordManager::usesPassword($peer);
|
||||
}
|
||||
catch (DatabaseOperationException $e)
|
||||
{
|
||||
throw new StandardException('Failed to check password usage due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e);
|
||||
}
|
||||
|
||||
// Password verification is required to set an OTP if a password is set
|
||||
if($usesPassword)
|
||||
{
|
||||
if(!$rpcRequest->containsParameter('password'))
|
||||
{
|
||||
return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'When a password is set, the current password must be provided to delete an OTP');
|
||||
}
|
||||
|
||||
if(!Cryptography::validateSha512($rpcRequest->getParameter('password')))
|
||||
{
|
||||
return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided password is not a valid SHA-512 hash');
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if(!PasswordManager::verifyPassword($peer, $rpcRequest->getParameter('password')))
|
||||
{
|
||||
return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The provided password is incorrect');
|
||||
}
|
||||
}
|
||||
catch(Exception $e)
|
||||
{
|
||||
throw new StandardException('Failed to verify password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Delete the OTP
|
||||
OneTimePasswordManager::deleteOtp($peer);
|
||||
}
|
||||
catch(Exception $e)
|
||||
{
|
||||
throw new StandardException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e);
|
||||
}
|
||||
|
||||
return $rpcRequest->produceResponse(true);
|
||||
}
|
||||
}
|
90
src/Socialbox/Classes/StandardMethods/SettingsSetOtp.php
Normal file
90
src/Socialbox/Classes/StandardMethods/SettingsSetOtp.php
Normal file
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
|
||||
namespace Socialbox\Classes\StandardMethods;
|
||||
|
||||
use Exception;
|
||||
use Socialbox\Abstracts\Method;
|
||||
use Socialbox\Classes\Cryptography;
|
||||
use Socialbox\Enums\Flags\SessionFlags;
|
||||
use Socialbox\Enums\StandardError;
|
||||
use Socialbox\Exceptions\DatabaseOperationException;
|
||||
use Socialbox\Exceptions\StandardException;
|
||||
use Socialbox\Interfaces\SerializableInterface;
|
||||
use Socialbox\Managers\OneTimePasswordManager;
|
||||
use Socialbox\Managers\PasswordManager;
|
||||
use Socialbox\Managers\SessionManager;
|
||||
use Socialbox\Objects\ClientRequest;
|
||||
use Socialbox\Objects\RpcRequest;
|
||||
|
||||
class SettingsSetOtp extends Method
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
|
||||
{
|
||||
$peer = $request->getPeer();
|
||||
|
||||
try
|
||||
{
|
||||
if (OneTimePasswordManager::usesOtp($peer->getUuid()))
|
||||
{
|
||||
return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, "Cannot set One Time Password when one is already set, use 'settingsUpdateOtp' instead");
|
||||
}
|
||||
}
|
||||
catch (DatabaseOperationException $e)
|
||||
{
|
||||
throw new StandardException('Failed to check One Time Password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$usesPassword = PasswordManager::usesPassword($peer);
|
||||
}
|
||||
catch (DatabaseOperationException $e)
|
||||
{
|
||||
throw new StandardException('Failed to check password usage due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e);
|
||||
}
|
||||
|
||||
// Password verification is required to set an OTP if a password is set
|
||||
if($usesPassword)
|
||||
{
|
||||
if(!$rpcRequest->containsParameter('password'))
|
||||
{
|
||||
return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'When a password is set, the current password must be provided to set an OTP');
|
||||
}
|
||||
|
||||
if(!Cryptography::validateSha512($rpcRequest->getParameter('password')))
|
||||
{
|
||||
return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided password is not a valid SHA-512 hash');
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if(!PasswordManager::verifyPassword($peer, $rpcRequest->getParameter('password')))
|
||||
{
|
||||
return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The provided password is incorrect');
|
||||
}
|
||||
}
|
||||
catch(Exception $e)
|
||||
{
|
||||
throw new StandardException('Failed to verify password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Create a new OTP and return the OTP URI to the client
|
||||
$totpUri = OneTimePasswordManager::createOtp($peer);
|
||||
|
||||
// Remove the SET_PASSWORD flag & update the session flow if necessary
|
||||
SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_OTP]);
|
||||
}
|
||||
catch(Exception $e)
|
||||
{
|
||||
throw new StandardException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e);
|
||||
}
|
||||
|
||||
return $rpcRequest->produceResponse($totpUri);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace Socialbox\Classes\StandardMethods;
|
||||
|
||||
use Exception;
|
||||
use Socialbox\Abstracts\Method;
|
||||
use Socialbox\Classes\Configuration;
|
||||
use Socialbox\Enums\Flags\SessionFlags;
|
||||
use Socialbox\Enums\StandardError;
|
||||
use Socialbox\Exceptions\CryptographyException;
|
||||
use Socialbox\Exceptions\StandardException;
|
||||
use Socialbox\Interfaces\SerializableInterface;
|
||||
use Socialbox\Managers\OneTimePasswordManager;
|
||||
use Socialbox\Managers\SessionManager;
|
||||
use Socialbox\Objects\ClientRequest;
|
||||
use Socialbox\Objects\RpcRequest;
|
||||
|
||||
class VerificationOtpAuthentication extends Method
|
||||
{
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
|
||||
{
|
||||
if(!$rpcRequest->containsParameter('code'))
|
||||
{
|
||||
return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'code' parameter");
|
||||
}
|
||||
|
||||
if(strlen($rpcRequest->getParameter('code')) !== Configuration::getSecurityConfiguration()->getOtpDigits())
|
||||
{
|
||||
return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'code' parameter length");
|
||||
}
|
||||
|
||||
$session = $request->getSession();
|
||||
if(!$session->flagExists(SessionFlags::VER_OTP))
|
||||
{
|
||||
return $rpcRequest->produceError(StandardError::FORBIDDEN, 'One Time Password verification is not required at this time');
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$result = OneTimePasswordManager::verifyOtp($request->getPeer(), $rpcRequest->getParameter('code'));
|
||||
|
||||
if($result)
|
||||
{
|
||||
// If the OTP is verified, remove the OTP verification flag
|
||||
SessionManager::updateFlow($session, [SessionFlags::VER_OTP]);
|
||||
}
|
||||
}
|
||||
catch (CryptographyException)
|
||||
{
|
||||
return $rpcRequest->produceResponse(false);
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
throw new StandardException('Failed to verify password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e);
|
||||
}
|
||||
|
||||
return $rpcRequest->produceResponse($result);
|
||||
}
|
||||
}
|
233
src/Socialbox/Managers/OneTimePasswordManager.php
Normal file
233
src/Socialbox/Managers/OneTimePasswordManager.php
Normal file
|
@ -0,0 +1,233 @@
|
|||
<?php
|
||||
|
||||
namespace Socialbox\Managers;
|
||||
|
||||
use DateTime;
|
||||
use PDOException;
|
||||
use Socialbox\Classes\Configuration;
|
||||
use Socialbox\Classes\Cryptography;
|
||||
use Socialbox\Classes\Database;
|
||||
use Socialbox\Classes\OtpCryptography;
|
||||
use Socialbox\Exceptions\CryptographyException;
|
||||
use Socialbox\Exceptions\DatabaseOperationException;
|
||||
use Socialbox\Objects\Database\RegisteredPeerRecord;
|
||||
|
||||
class OneTimePasswordManager
|
||||
{
|
||||
/**
|
||||
* Checks if a given peer uses OTP for authentication.
|
||||
*
|
||||
* @param string|RegisteredPeerRecord $peerUuid Either a UUID as a string or a RegisteredPeerRecord object representing the peer.
|
||||
* @return bool Returns true if the peer uses OTP, otherwise false.
|
||||
* @throws DatabaseOperationException Thrown when a database error occurs.
|
||||
*/
|
||||
public static function usesOtp(string|RegisteredPeerRecord $peerUuid): bool
|
||||
{
|
||||
if($peerUuid instanceof RegisteredPeerRecord)
|
||||
{
|
||||
$peerUuid = $peerUuid->getUuid();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$stmt = Database::getConnection()->prepare('SELECT COUNT(*) FROM authentication_otp WHERE peer_uuid=:uuid');
|
||||
$stmt->bindParam(':uuid', $peerUuid);
|
||||
$stmt->execute();
|
||||
}
|
||||
catch (PDOException $e)
|
||||
{
|
||||
throw new DatabaseOperationException('An error occurred while checking the OTP usage in the database', $e);
|
||||
}
|
||||
|
||||
return $stmt->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and stores a new OTP (One-Time Password) secret for the specified peer, and generates a key URI.
|
||||
*
|
||||
* @param string|RegisteredPeerRecord $peer The unique identifier of the peer, either as a string UUID
|
||||
* or an instance of RegisteredPeerRecord.
|
||||
* @return string The generated OTP key URI that can be used for applications like authenticator apps.
|
||||
* @throws DatabaseOperationException If there is an error during the database operation.
|
||||
*/
|
||||
public static function createOtp(string|RegisteredPeerRecord $peer): string
|
||||
{
|
||||
if(is_string($peer))
|
||||
{
|
||||
$peer = RegisteredPeerManager::getPeer($peer);
|
||||
}
|
||||
|
||||
$secret = OtpCryptography::generateSecretKey(Configuration::getSecurityConfiguration()->getOtpSecretKeyLength());
|
||||
$encryptionKey = Configuration::getCryptographyConfiguration()->getRandomInternalEncryptionKey();
|
||||
$encryptedSecret = Cryptography::encryptMessage($secret, $encryptionKey, Configuration::getCryptographyConfiguration()->getEncryptionKeysAlgorithm());
|
||||
|
||||
try
|
||||
{
|
||||
$stmt = Database::getConnection()->prepare("INSERT INTO authentication_otp (peer_uuid, secret) VALUES (:peer_uuid, :secret)");
|
||||
$stmt->bindParam(':peer_uuid', $peer);
|
||||
$stmt->bindParam(':secret', $encryptedSecret);
|
||||
$stmt->execute();
|
||||
}
|
||||
catch(PDOException $e)
|
||||
{
|
||||
throw new DatabaseOperationException('An error occurred while creating the OTP secret in the database', $e);
|
||||
}
|
||||
|
||||
return OtpCryptography::generateKeyUri($peer->getAddress(), $secret,
|
||||
Configuration::getInstanceConfiguration()->getDomain(),
|
||||
Configuration::getSecurityConfiguration()->getOtpTimeStep(),
|
||||
Configuration::getSecurityConfiguration()->getOtpDigits(),
|
||||
Configuration::getSecurityConfiguration()->getOtpHashAlgorithm()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the provided OTP (One-Time Password) against the stored secret associated with the specified peer.
|
||||
*
|
||||
* @param string|RegisteredPeerRecord $peerUuid The unique identifier of the peer, either as a string UUID
|
||||
* or an instance of RegisteredPeerRecord.
|
||||
* @param string $otp The OTP to be verified.
|
||||
* @return bool Returns true if the OTP is valid; otherwise, false.
|
||||
* @throws DatabaseOperationException If there is an error during the database operation.
|
||||
* @throws CryptographyException If there is a failure in decrypting the stored OTP secret.
|
||||
*/
|
||||
public static function verifyOtp(string|RegisteredPeerRecord $peerUuid, string $otp): bool
|
||||
{
|
||||
if($peerUuid instanceof RegisteredPeerRecord)
|
||||
{
|
||||
$peerUuid = $peerUuid->getUuid();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$stmt = Database::getConnection()->prepare('SELECT secret FROM authentication_otp WHERE peer_uuid=:uuid');
|
||||
$stmt->bindParam(':uuid', $peerUuid);
|
||||
$stmt->execute();
|
||||
|
||||
$encryptedSecret = $stmt->fetchColumn();
|
||||
|
||||
if($encryptedSecret === false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch(PDOException $e)
|
||||
{
|
||||
throw new DatabaseOperationException('An error occurred while retrieving the OTP secret from the database', $e);
|
||||
}
|
||||
|
||||
$decryptedSecret = null;
|
||||
foreach(Configuration::getCryptographyConfiguration()->getInternalEncryptionKeys() as $encryptionKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
$decryptedSecret = Cryptography::decryptMessage($encryptedSecret, $encryptionKey, Configuration::getCryptographyConfiguration()->getEncryptionKeysAlgorithm());
|
||||
}
|
||||
catch(CryptographyException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if($decryptedSecret === null)
|
||||
{
|
||||
throw new CryptographyException('Failed to decrypt the OTP secret');
|
||||
}
|
||||
|
||||
return OtpCryptography::verifyOTP($decryptedSecret, $otp,
|
||||
Configuration::getSecurityConfiguration()->getOtpTimeStep(),
|
||||
Configuration::getSecurityConfiguration()->getOtpWindow(),
|
||||
Configuration::getSecurityConfiguration()->getOtpDigits(),
|
||||
Configuration::getSecurityConfiguration()->getOtpHashAlgorithm()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the OTP record associated with the specified peer.
|
||||
*
|
||||
* @param string|RegisteredPeerRecord $peerUuid The peer's UUID or an instance of RegisteredPeerRecord whose OTP record needs to be deleted.
|
||||
* @return void
|
||||
* @throws DatabaseOperationException if the database operation fails.
|
||||
*/
|
||||
public static function deleteOtp(string|RegisteredPeerRecord $peerUuid): void
|
||||
{
|
||||
if($peerUuid instanceof RegisteredPeerRecord)
|
||||
{
|
||||
$peerUuid = $peerUuid->getUuid();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$stmt = Database::getConnection()->prepare('DELETE FROM authentication_otp WHERE peer_uuid=:uuid');
|
||||
$stmt->bindParam(':uuid', $peerUuid);
|
||||
$stmt->execute();
|
||||
}
|
||||
catch(PDOException $e)
|
||||
{
|
||||
throw new DatabaseOperationException('An error occurred while deleting the OTP secret from the database', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the last updated timestamp for the OTP record of the specified peer.
|
||||
*
|
||||
* @param string|RegisteredPeerRecord $peerUuid The peer's UUID or an instance of RegisteredPeerRecord whose OTP record's last updated timestamp needs to be retrieved
|
||||
* @return int The last updated timestamp of the OTP record, or 0 if no such record exists
|
||||
*/
|
||||
public static function getLastUpdated(string|RegisteredPeerRecord $peerUuid): int
|
||||
{
|
||||
if($peerUuid instanceof RegisteredPeerRecord)
|
||||
{
|
||||
$peerUuid = $peerUuid->getUuid();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$stmt = Database::getConnection()->prepare('SELECT updated FROM authentication_otp WHERE peer_uuid=:uuid');
|
||||
$stmt->bindParam(':uuid', $peerUuid);
|
||||
$stmt->execute();
|
||||
|
||||
/** @var \DateTime $updated */
|
||||
$updated = $stmt->fetchColumn();
|
||||
|
||||
if($updated === false)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
catch(PDOException $e)
|
||||
{
|
||||
throw new DatabaseOperationException('An error occurred while retrieving the last updated timestamp from the database', $e);
|
||||
}
|
||||
|
||||
return $updated->getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the last updated timestamp for the OTP record of the specified peer.
|
||||
*
|
||||
* @param string|RegisteredPeerRecord $peerUuid The peer's UUID or an instance of RegisteredPeerRecord whose OTP record needs to be updated.
|
||||
* @return void
|
||||
* @throws DatabaseOperationException if the database operation fails.
|
||||
*/
|
||||
public static function updateOtp(string|RegisteredPeerRecord $peerUuid): void
|
||||
{
|
||||
if($peerUuid instanceof RegisteredPeerRecord)
|
||||
{
|
||||
$peerUuid = $peerUuid->getUuid();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$stmt = Database::getConnection()->prepare('UPDATE authentication_otp SET updated=:updated WHERE peer_uuid=:uuid');
|
||||
$updated = (new DateTime())->setTimestamp(time());
|
||||
$stmt->bindParam(':updated', $updated);
|
||||
$stmt->bindParam(':uuid', $peerUuid);
|
||||
$stmt->execute();
|
||||
}
|
||||
catch(PDOException $e)
|
||||
{
|
||||
throw new DatabaseOperationException(sprintf('Failed to update the OTP secret for user %s', $peerUuid), $e);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue