diff --git a/.idea/php.xml b/.idea/php.xml
index 2f19bb6..26fbabf 100644
--- a/.idea/php.xml
+++ b/.idea/php.xml
@@ -10,6 +10,11 @@
+
This is where the privacy policy document would reside in
\ No newline at end of file diff --git a/src/Socialbox/Classes/Resources/documents/tos.html b/src/Socialbox/Classes/Resources/documents/tos.html new file mode 100644 index 0000000..721e8dc --- /dev/null +++ b/src/Socialbox/Classes/Resources/documents/tos.html @@ -0,0 +1,2 @@ +This is where the Terms of Service document would reside
\ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/AcceptPrivacyPolicy.php b/src/Socialbox/Classes/StandardMethods/AcceptPrivacyPolicy.php new file mode 100644 index 0000000..d87ada5 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/AcceptPrivacyPolicy.php @@ -0,0 +1,48 @@ +getSessionUuid(), [SessionFlags::VER_PRIVACY_POLICY]); + } + catch (DatabaseOperationException $e) + { + return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Check if all registration flags are removed + if(SessionFlags::isComplete($request->getSession()->getFlags())) + { + // Set the session as authenticated + try + { + SessionManager::setAuthenticated($request->getSessionUuid(), true); + SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::REGISTRATION_REQUIRED, SessionFlags::AUTHENTICATION_REQUIRED]); + } + catch (DatabaseOperationException $e) + { + return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/AcceptTermsOfService.php b/src/Socialbox/Classes/StandardMethods/AcceptTermsOfService.php new file mode 100644 index 0000000..bd5fb8c --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/AcceptTermsOfService.php @@ -0,0 +1,47 @@ +getSessionUuid(), [SessionFlags::VER_TERMS_OF_SERVICE]); + } + catch (DatabaseOperationException $e) + { + return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Check if all registration flags are removed + if(SessionFlags::isComplete($request->getSession()->getFlags())) + { + // Set the session as authenticated + try + { + SessionManager::setAuthenticated($request->getSessionUuid(), true); + SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::REGISTRATION_REQUIRED, SessionFlags::AUTHENTICATION_REQUIRED]); + } + catch (DatabaseOperationException $e) + { + return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/GetPrivacyPolicy.php b/src/Socialbox/Classes/StandardMethods/GetPrivacyPolicy.php new file mode 100644 index 0000000..20ab6c5 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/GetPrivacyPolicy.php @@ -0,0 +1,22 @@ +produceResponse(Resources::getPrivacyPolicy()); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/GetTermsOfService.php b/src/Socialbox/Classes/StandardMethods/GetTermsOfService.php new file mode 100644 index 0000000..c92a38f --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/GetTermsOfService.php @@ -0,0 +1,22 @@ +produceResponse(Resources::getTermsOfService()); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetPassword.php b/src/Socialbox/Classes/StandardMethods/SettingsSetPassword.php new file mode 100644 index 0000000..7269f15 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsSetPassword.php @@ -0,0 +1,76 @@ +containsParameter('password')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'password' parameter"); + } + + if(!preg_match('/^[a-f0-9]{128}$/', $rpcRequest->getParameter('password'))) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'password' parameter, must be sha512 hexadecimal hash"); + } + + try + { + if (PasswordManager::usesPassword($request->getPeer()->getUuid())) + { + return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, "Cannot set password when one is already set, use 'settingsChangePassword' instead"); + } + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to check password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + try + { + // Set the password + PasswordManager::setPassword($request->getPeer(), $rpcRequest->getParameter('password')); + + // Remove the SET_PASSWORD flag + SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::SET_PASSWORD]); + } + catch(Exception $e) + { + throw new StandardException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Check if all registration flags are removed + if(SessionFlags::isComplete($request->getSession()->getFlags())) + { + // Set the session as authenticated + try + { + SessionManager::setAuthenticated($request->getSessionUuid(), true); + SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::REGISTRATION_REQUIRED, SessionFlags::AUTHENTICATION_REQUIRED]); + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to update session due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php index 3d7f5fb..88adeb3 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php @@ -4,6 +4,7 @@ namespace Socialbox\Classes\StandardMethods; use Socialbox\Abstracts\Method; use Socialbox\Enums\Flags\PeerFlags; +use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\StandardException; @@ -11,6 +12,7 @@ use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\CaptchaManager; use Socialbox\Managers\RegisteredPeerManager; use Socialbox\Managers\SessionManager; +use Socialbox\Objects\ClientRequest; use Socialbox\Objects\ClientRequestOld; use Socialbox\Objects\RpcRequest; @@ -20,51 +22,15 @@ class VerificationAnswerImageCaptcha extends Method /** * @inheritDoc */ - public static function execute(ClientRequestOld $request, RpcRequest $rpcRequest): ?SerializableInterface + public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { - // Check if the request has a Session UUID - if($request->getSessionUuid() === null) - { - return $rpcRequest->produceError(StandardError::SESSION_REQUIRED); - } - - // Get the session and check if it's already authenticated - try - { - $session = SessionManager::getSession($request->getSessionUuid()); - } - catch(DatabaseOperationException $e) - { - throw new StandardException("There was an unexpected error while trying to get the session", StandardError::INTERNAL_SERVER_ERROR, $e); - } - - // Check for session conditions - if($session->getPeerUuid() === null) - { - return $rpcRequest->produceError(StandardError::AUTHENTICATION_REQUIRED); - } - - // Get the peer - try - { - $peer = RegisteredPeerManager::getPeer($session->getPeerUuid()); - } - catch(DatabaseOperationException $e) - { - throw new StandardException("There was unexpected error while trying to get the peer", StandardError::INTERNAL_SERVER_ERROR, $e); - } - - // Check if the VER_SOLVE_IMAGE_CAPTCHA flag exists. - if(!$peer->flagExists(PeerFlags::VER_SOLVE_IMAGE_CAPTCHA)) - { - return $rpcRequest->produceError(StandardError::CAPTCHA_NOT_AVAILABLE, 'You are not required to complete a captcha at this time'); - } - if(!$rpcRequest->containsParameter('answer')) { return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The answer parameter is required'); } + $session = $request->getSession(); + try { if(CaptchaManager::getCaptcha($session->getPeerUuid())->isExpired()) @@ -83,14 +49,29 @@ class VerificationAnswerImageCaptcha extends Method if($result) { - RegisteredPeerManager::removeFlag($session->getPeerUuid(), PeerFlags::VER_SOLVE_IMAGE_CAPTCHA); + SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::VER_IMAGE_CAPTCHA]); } - - return $rpcRequest->produceResponse($result); } catch (DatabaseOperationException $e) { throw new StandardException("There was an unexpected error while trying to answer the captcha", StandardError::INTERNAL_SERVER_ERROR, $e); } + + // Check if all registration flags are removed + if(SessionFlags::isComplete($request->getSession()->getFlags())) + { + // Set the session as authenticated + try + { + SessionManager::setAuthenticated($request->getSessionUuid(), true); + SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::REGISTRATION_REQUIRED, SessionFlags::AUTHENTICATION_REQUIRED]); + } + catch (DatabaseOperationException $e) + { + return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + return $rpcRequest->produceResponse($result); } } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php index 1956706..f935346 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php @@ -1,82 +1,68 @@ getSessionUuid() === null) + /** + * @inheritDoc + */ + public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { - return $rpcRequest->produceError(StandardError::SESSION_REQUIRED); - } + $session = $request->getSession(); - // Get the session and check if it's already authenticated - try - { - $session = SessionManager::getSession($request->getSessionUuid()); - } - catch(DatabaseOperationException $e) - { - throw new StandardException("There was an unexpected error while trying to get the session", StandardError::INTERNAL_SERVER_ERROR, $e); - } + // Check for session conditions + if($session->getPeerUuid() === null) + { + return $rpcRequest->produceError(StandardError::AUTHENTICATION_REQUIRED); + } - // Check for session conditions - if($session->getPeerUuid() === null) - { - return $rpcRequest->produceError(StandardError::AUTHENTICATION_REQUIRED); - } + $peer = $request->getPeer(); - // Get the peer - try - { - $peer = RegisteredPeerManager::getPeer($session->getPeerUuid()); - } - catch(DatabaseOperationException $e) - { - throw new StandardException("There was unexpected error while trying to get the peer", StandardError::INTERNAL_SERVER_ERROR, $e); - } + try + { + Logger::getLogger()->debug('Creating a new captcha for peer ' . $peer->getUuid()); + if(CaptchaManager::captchaExists($peer)) + { + $captchaRecord = CaptchaManager::getCaptcha($peer); + if($captchaRecord->isExpired()) + { + $answer = CaptchaManager::createCaptcha($peer); + $captchaRecord = CaptchaManager::getCaptcha($peer); + } + else + { + $answer = $captchaRecord->getAnswer(); + } + } + else + { + $answer = CaptchaManager::createCaptcha($peer); + $captchaRecord = CaptchaManager::getCaptcha($peer); + } + } + catch (DatabaseOperationException $e) + { + throw new StandardException("There was an unexpected error while trying create the captcha", StandardError::INTERNAL_SERVER_ERROR, $e); + } - // Check if the VER_SOLVE_IMAGE_CAPTCHA flag exists. - if(!$peer->flagExists(PeerFlags::VER_SOLVE_IMAGE_CAPTCHA)) - { - return $rpcRequest->produceError(StandardError::CAPTCHA_NOT_AVAILABLE, 'You are not required to complete a captcha at this time'); + // Build the captcha + // Returns HTML base64 encoded image of the captcha + return $rpcRequest->produceResponse(new ImageCaptcha([ + 'expires' => $captchaRecord->getExpires(), + 'content' => (new CaptchaBuilder($answer))->build()->inline() + ])); } - - try - { - Logger::getLogger()->debug('Creating a new captcha for peer ' . $peer->getUuid()); - $answer = CaptchaManager::createCaptcha($peer); - $captchaRecord = CaptchaManager::getCaptcha($peer); - } - catch (DatabaseOperationException $e) - { - throw new StandardException("There was an unexpected error while trying create the captcha", StandardError::INTERNAL_SERVER_ERROR, $e); - } - - // Build the captcha - return $rpcRequest->produceResponse(new ImageCaptcha([ - 'expires' => $captchaRecord->getExpires(), - 'image' => (new CaptchaBuilder($answer))->build()->inline() - ])); // Returns HTML base64 encoded image of the captcha - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/Socialbox/Enums/Flags/SessionFlags.php b/src/Socialbox/Enums/Flags/SessionFlags.php index bf97b95..99a24ed 100644 --- a/src/Socialbox/Enums/Flags/SessionFlags.php +++ b/src/Socialbox/Enums/Flags/SessionFlags.php @@ -51,4 +51,43 @@ return array_map(fn(string $value) => SessionFlags::from(trim($value)), explode(',', $flagString)); } + + /** + * Determines if all required session flags for completion are satisfied based on the given array of flags. + * + * @param array $flags An array of session flags to evaluate. Accepts both enum values (strings) and enum objects. + * @return bool True if all required flags for completion are satisfied, false otherwise. + */ + public static function isComplete(array $flags): bool + { + $flags = array_map(function ($flag) { + return is_string($flag) ? SessionFlags::from($flag) : $flag; + }, $flags); + + $flags = array_map(fn(SessionFlags $flag) => $flag->value, $flags); + + if (in_array(SessionFlags::REGISTRATION_REQUIRED->value, $flags)) { + $flagsToComplete = [ + SessionFlags::SET_PASSWORD->value, + SessionFlags::SET_OTP->value, + SessionFlags::SET_DISPLAY_NAME->value, + SessionFlags::VER_PRIVACY_POLICY->value, + SessionFlags::VER_TERMS_OF_SERVICE->value, + SessionFlags::VER_EMAIL->value, + SessionFlags::VER_SMS->value, + SessionFlags::VER_PHONE_CALL->value, + SessionFlags::VER_IMAGE_CAPTCHA->value + ]; + return !array_intersect($flagsToComplete, $flags); // Check if the intersection is empty + } + if (in_array(SessionFlags::AUTHENTICATION_REQUIRED->value, $flags)) { + $flagsToComplete = [ + SessionFlags::VER_PASSWORD->value, + SessionFlags::VER_OTP->value + ]; + return !array_intersect($flagsToComplete, $flags); // Check if the intersection is empty + + } + return true; + } } diff --git a/src/Socialbox/Enums/StandardError.php b/src/Socialbox/Enums/StandardError.php index 7f80aaf..d634a79 100644 --- a/src/Socialbox/Enums/StandardError.php +++ b/src/Socialbox/Enums/StandardError.php @@ -36,6 +36,7 @@ enum StandardError : int case INVALID_USERNAME = -4001; case USERNAME_ALREADY_EXISTS = -4002; case NOT_REGISTERED = -4003; + case METHOD_NOT_ALLOWED = -4004; /** * Returns the default generic message for the error @@ -69,6 +70,9 @@ enum StandardError : int self::INVALID_USERNAME => 'The given username is invalid, it must be Alphanumeric with a minimum of 3 character but no greater than 255 characters', self::USERNAME_ALREADY_EXISTS => 'The given username already exists on the network', self::NOT_REGISTERED => 'The given username is not registered on the server', + self::METHOD_NOT_ALLOWED => 'The requested method is not allowed', + self::SESSION_EXPIRED => 'The session has expired', + self::SESSION_DHE_REQUIRED => 'The session requires DHE to be established', }; } diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index 65c83bd..eb98544 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -2,8 +2,16 @@ namespace Socialbox\Enums; + use Socialbox\Classes\StandardMethods\AcceptPrivacyPolicy; + use Socialbox\Classes\StandardMethods\AcceptTermsOfService; + use Socialbox\Classes\StandardMethods\GetPrivacyPolicy; use Socialbox\Classes\StandardMethods\GetSessionState; + use Socialbox\Classes\StandardMethods\GetTermsOfService; use Socialbox\Classes\StandardMethods\Ping; + use Socialbox\Classes\StandardMethods\SettingsSetPassword; + use Socialbox\Classes\StandardMethods\VerificationAnswerImageCaptcha; + use Socialbox\Classes\StandardMethods\VerificationGetImageCaptcha; + use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Exceptions\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; @@ -13,12 +21,24 @@ { case PING = 'ping'; case GET_SESSION_STATE = 'getSessionState'; + + case GET_PRIVACY_POLICY = 'getPrivacyPolicy'; + case ACCEPT_PRIVACY_POLICY = 'acceptPrivacyPolicy'; + case GET_TERMS_OF_SERVICE = 'getTermsOfService'; + case ACCEPT_TERMS_OF_SERVICE = 'acceptTermsOfService'; + + case VERIFICATION_GET_IMAGE_CAPTCHA = 'verificationGetImageCaptcha'; + case VERIFICATION_ANSWER_IMAGE_CAPTCHA = 'verificationAnswerImageCaptcha'; + + case SETTINGS_SET_PASSWORD = 'settingsSetPassword'; /** - * @param ClientRequest $request - * @param RpcRequest $rpcRequest - * @return SerializableInterface|null - * @throws StandardException + * Executes the appropriate operation based on the current context and requests provided. + * + * @param ClientRequest $request The client request object containing necessary data for execution. + * @param RpcRequest $rpcRequest The RPC request object providing additional parameters for execution. + * @return SerializableInterface|null The result of the operation as a serializable interface or null if no operation matches. + * @throws StandardException If an error occurs during execution */ public function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { @@ -26,6 +46,74 @@ { self::PING => Ping::execute($request, $rpcRequest), self::GET_SESSION_STATE => GetSessionState::execute($request, $rpcRequest), + + self::GET_PRIVACY_POLICY => GetPrivacyPolicy::execute($request, $rpcRequest), + self::ACCEPT_PRIVACY_POLICY => AcceptPrivacyPolicy::execute($request, $rpcRequest), + self::GET_TERMS_OF_SERVICE => GetTermsOfService::execute($request, $rpcRequest), + self::ACCEPT_TERMS_OF_SERVICE => AcceptTermsOfService::execute($request, $rpcRequest), + + self::VERIFICATION_GET_IMAGE_CAPTCHA => VerificationGetImageCaptcha::execute($request, $rpcRequest), + self::VERIFICATION_ANSWER_IMAGE_CAPTCHA => VerificationAnswerImageCaptcha::execute($request, $rpcRequest), + + self::SETTINGS_SET_PASSWORD => SettingsSetPassword::execute($request, $rpcRequest), }; } + + /** + * Checks if the access method is allowed for the given client request. + * + * @param ClientRequest $clientRequest The client request instance to check access against. + * @return void + * @throws StandardException If the method is not allowed for the given client request. + */ + public function checkAccess(ClientRequest $clientRequest): void + { + if(in_array($this, self::getAllowedMethods($clientRequest))) + { + return; + } + + throw new StandardException(StandardError::METHOD_NOT_ALLOWED->getMessage(), StandardError::METHOD_NOT_ALLOWED); + } + + /** + * Determines the list of allowed methods for a given client request. + * + * @param ClientRequest $clientRequest The client request for which allowed methods are determined. + * @return array Returns an array of allowed methods for the provided client request. + */ + public static function getAllowedMethods(ClientRequest $clientRequest): array + { + $methods = [ + self::PING, + self::GET_SESSION_STATE, + self::GET_PRIVACY_POLICY, + self::GET_TERMS_OF_SERVICE, + ]; + + $session = $clientRequest->getSession(); + + if(in_array(SessionFlags::VER_PRIVACY_POLICY, $session->getFlags())) + { + $methods[] = self::ACCEPT_PRIVACY_POLICY; + } + + if(in_array(SessionFlags::VER_TERMS_OF_SERVICE, $session->getFlags())) + { + $methods[] = self::ACCEPT_TERMS_OF_SERVICE; + } + + if(in_array(SessionFlags::VER_IMAGE_CAPTCHA, $session->getFlags())) + { + $methods[] = self::VERIFICATION_GET_IMAGE_CAPTCHA; + $methods[] = self::VERIFICATION_ANSWER_IMAGE_CAPTCHA; + } + + if(in_array(SessionFlags::SET_PASSWORD, $session->getFlags())) + { + $methods[] = self::SETTINGS_SET_PASSWORD; + } + + return $methods; + } } \ No newline at end of file diff --git a/src/Socialbox/Managers/CaptchaManager.php b/src/Socialbox/Managers/CaptchaManager.php index a2407a4..8151456 100644 --- a/src/Socialbox/Managers/CaptchaManager.php +++ b/src/Socialbox/Managers/CaptchaManager.php @@ -135,10 +135,10 @@ class CaptchaManager * Retrieves the captcha record for the given peer UUID. * * @param string|RegisteredPeerRecord $peer_uuid The UUID of the peer to retrieve the captcha for. - * @return CaptchaRecord The captcha record. + * @return CaptchaRecord|null The captcha record. * @throws DatabaseOperationException If the operation fails. */ - public static function getCaptcha(string|RegisteredPeerRecord $peer_uuid): CaptchaRecord + public static function getCaptcha(string|RegisteredPeerRecord $peer_uuid): ?CaptchaRecord { // If the peer_uuid is a RegisteredPeerRecord, get the UUID if($peer_uuid instanceof RegisteredPeerRecord) @@ -162,7 +162,7 @@ class CaptchaManager if($result === false) { - throw new DatabaseOperationException('The requested captcha does not exist'); + return null; } return CaptchaRecord::fromArray($result); @@ -175,7 +175,7 @@ class CaptchaManager * @return bool True if a captcha exists, false otherwise. * @throws DatabaseOperationException If the operation fails. */ - private static function captchaExists(string|RegisteredPeerRecord $peer_uuid): bool + public static function captchaExists(string|RegisteredPeerRecord $peer_uuid): bool { // If the peer_uuid is a RegisteredPeerRecord, get the UUID if($peer_uuid instanceof RegisteredPeerRecord) diff --git a/src/Socialbox/Managers/PasswordManager.php b/src/Socialbox/Managers/PasswordManager.php new file mode 100644 index 0000000..6de3a9f --- /dev/null +++ b/src/Socialbox/Managers/PasswordManager.php @@ -0,0 +1,187 @@ +getUuid(); + } + + try + { + $stmt = Database::getConnection()->prepare('SELECT COUNT(*) FROM authentication_passwords WHERE peer_uuid=:uuid'); + $stmt->bindParam(':uuid', $peerUuid); + $stmt->execute(); + + return $stmt->fetchColumn() > 0; + } + catch (\PDOException $e) + { + throw new DatabaseOperationException('An error occurred while checking the password usage in the database', $e); + } + } + + /** + * Sets a password for a given user or peer record by securely encrypting it + * and storing it in the authentication_passwords database table. + * + * @param string|RegisteredPeerRecord $peerUuid The UUID of the peer or an instance of RegisteredPeerRecord. + * @param string $password The plaintext password to be securely stored. + * @throws CryptographyException If an error occurs while securing the password. + * @throws DatabaseOperationException If an error occurs while attempting to store the password in the database. + * @throws \DateMalformedStringException If the updated timestamp cannot be formatted. + * @return void + */ + public static function setPassword(string|RegisteredPeerRecord $peerUuid, string $password): void + { + if($peerUuid instanceof RegisteredPeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + $encryptionRecord = EncryptionRecordsManager::getRandomRecord(); + $securedPassword = SecuredPassword::securePassword($peerUuid, $password, $encryptionRecord); + + try + { + $stmt = Database::getConnection()->prepare("INSERT INTO authentication_passwords (peer_uuid, iv, encrypted_password, encrypted_tag) VALUES (:peer_uuid, :iv, :encrypted_password, :encrypted_tag)"); + $stmt->bindParam(":peer_uuid", $peerUuid); + + $iv = $securedPassword->getIv(); + $stmt->bindParam(':iv', $iv); + + $encryptedPassword = $securedPassword->getEncryptedPassword(); + $stmt->bindParam(':encrypted_password', $encryptedPassword); + + $encryptedTag = $securedPassword->getEncryptedTag(); + $stmt->bindParam(':encrypted_tag', $encryptedTag); + + $stmt->execute(); + } + catch(\PDOException $e) + { + throw new DatabaseOperationException(sprintf('Failed to set password for user %s', $peerUuid), $e); + } + } + + /** + * Updates the password for a given peer identified by their UUID or a RegisteredPeerRecord. + * + * @param string|RegisteredPeerRecord $peerUuid The UUID of the peer or an instance of RegisteredPeerRecord. + * @param string $newPassword The new password to be set for the peer. + * @throws CryptographyException If an error occurs while securing the new password. + * @throws DatabaseOperationException If the update operation fails due to a database error. + * @throws \DateMalformedStringException If the updated timestamp cannot be formatted. + * @returns void + */ + public static function updatePassword(string|RegisteredPeerRecord $peerUuid, string $newPassword): void + { + if($peerUuid instanceof RegisteredPeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + + $encryptionRecord = EncryptionRecordsManager::getRandomRecord(); + $securedPassword = SecuredPassword::securePassword($peerUuid, $newPassword, $encryptionRecord); + + try + { + $stmt = Database::getConnection()->prepare("UPDATE authentication_passwords SET iv=:iv, encrypted_password=:encrypted_password, encrypted_tag=:encrypted_tag, updated=:updated WHERE peer_uuid=:peer_uuid"); + $stmt->bindParam(":peer_uuid", $peerUuid); + + $iv = $securedPassword->getIv(); + $stmt->bindParam(':iv', $iv); + + $encryptedPassword = $securedPassword->getEncryptedPassword(); + $stmt->bindParam(':encrypted_password', $encryptedPassword); + + $encryptedTag = $securedPassword->getEncryptedTag(); + $stmt->bindParam(':encrypted_tag', $encryptedTag); + + $updated = $securedPassword->getUpdated()->format('Y-m-d H:i:s'); + $stmt->bindParam(':updated', $updated); + + $stmt->execute(); + } + catch(\PDOException $e) + { + throw new DatabaseOperationException(sprintf('Failed to update password for user %s', $peerUuid), $e); + } + } + + /** + * Retrieves the password record associated with the given peer UUID. + * + * @param string|RegisteredPeerRecord $peerUuid The UUID of the peer or an instance of RegisteredPeerRecord. + * @return SecurePasswordRecord|null Returns a SecurePasswordRecord if found, or null if no record is present. + * @throws DatabaseOperationException If a database operation error occurs during the retrieval process. + */ + private static function getPassword(string|RegisteredPeerRecord $peerUuid): ?SecurePasswordRecord + { + if($peerUuid instanceof RegisteredPeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + try + { + $statement = Database::getConnection()->prepare("SELECT * FROM authentication_passwords WHERE peer_uuid=:peer_uuid LIMIT 1"); + $statement->bindParam(':peer_uuid', $peerUuid); + + $statement->execute(); + $data = $statement->fetch(PDO::FETCH_ASSOC); + + if ($data === false) + { + return null; + } + + return SecurePasswordRecord::fromArray($data); + } + catch(\PDOException $e) + { + throw new DatabaseOperationException(sprintf('Failed to retrieve password record for user %s', $peerUuid), $e); + } + } + + /** + * Verifies if the provided password matches the secured password associated with the given peer UUID. + * + * @param string|RegisteredPeerRecord $peerUuid The unique identifier or registered peer record of the user. + * @param string $password The password to be verified. + * @return bool Returns true if the password is verified successfully; otherwise, false. + * @throws DatabaseOperationException If an error occurs while retrieving the password record from the database. + * @throws CryptographyException If an error occurs while verifying the password. + */ + public static function verifyPassword(string|RegisteredPeerRecord $peerUuid, string $password): bool + { + $securedPassword = self::getPassword($peerUuid); + if($securedPassword === null) + { + return false; + } + + $encryptionRecords = EncryptionRecordsManager::getAllRecords(); + return SecuredPassword::verifyPassword($password, $securedPassword, $encryptionRecords); + } + } \ No newline at end of file diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index 374ff5d..0b50151 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -361,7 +361,7 @@ throw new StandardException(sprintf("The requested session '%s' does not exist", $uuid), StandardError::SESSION_NOT_FOUND); } - return Utilities::unserializeList($data['flags']); + return SessionFlags::fromString($data['flags']); } catch (PDOException $e) { @@ -404,7 +404,7 @@ * Removes specified flags from the session associated with the given UUID. * * @param string $uuid The UUID of the session from which the flags will be removed. - * @param array $flags An array of flags to be removed from the session. + * @param SessionFlags[] $flags An array of flags to be removed from the session. * @return void * @throws DatabaseOperationException|StandardException If there is an error while updating the session in the database. */ @@ -412,16 +412,15 @@ { Logger::getLogger()->verbose(sprintf("Removing flags from session %s", $uuid)); - // First get the existing flags $existingFlags = self::getFlags($uuid); - - // Remove the specified flags - $flags = array_diff($existingFlags, $flags); + $flagsToRemove = array_map(fn($flag) => $flag->value, $flags); + $updatedFlags = array_filter($existingFlags, fn($flag) => !in_array($flag->value, $flagsToRemove)); + $flags = SessionFlags::toString($updatedFlags); try { $statement = Database::getConnection()->prepare("UPDATE sessions SET flags=? WHERE uuid=?"); - $statement->bindValue(1, Utilities::serializeList($flags)); + $statement->bindValue(1, $flags); // Directly use the toString() result $statement->bindParam(2, $uuid); $statement->execute(); } @@ -430,4 +429,29 @@ throw new DatabaseOperationException('Failed to remove flags from session', $e); } } + + /** + * Updates the authentication status for the specified session. + * + * @param string $uuid The unique identifier of the session to be updated. + * @param bool $authenticated The authentication status to set for the session. + * @return void + * @throws DatabaseOperationException If the database operation fails. + */ + public static function setAuthenticated(string $uuid, bool $authenticated): void + { + Logger::getLogger()->verbose(sprintf("Setting session %s as authenticated: %s", $uuid, $authenticated ? 'true' : 'false')); + + try + { + $statement = Database::getConnection()->prepare("UPDATE sessions SET authenticated=? WHERE uuid=?"); + $statement->bindParam(1, $authenticated); + $statement->bindParam(2, $uuid); + $statement->execute(); + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to update authenticated peer', $e); + } + } } \ No newline at end of file diff --git a/src/Socialbox/Objects/ClientRequest.php b/src/Socialbox/Objects/ClientRequest.php index 402c249..2bfb76c 100644 --- a/src/Socialbox/Objects/ClientRequest.php +++ b/src/Socialbox/Objects/ClientRequest.php @@ -10,7 +10,9 @@ use Socialbox\Enums\Types\RequestType; use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\RequestException; + use Socialbox\Managers\RegisteredPeerManager; use Socialbox\Managers\SessionManager; + use Socialbox\Objects\Database\RegisteredPeerRecord; use Socialbox\Objects\Database\SessionRecord; class ClientRequest @@ -113,6 +115,18 @@ return SessionManager::getSession($this->sessionUuid); } + public function getPeer(): ?RegisteredPeerRecord + { + $session = $this->getSession(); + + if($session === null) + { + return null; + } + + return RegisteredPeerManager::getPeer($session->getPeerUuid()); + } + public function getSignature(): ?string { return $this->signature; diff --git a/src/Socialbox/Objects/Database/SecurePasswordRecord.php b/src/Socialbox/Objects/Database/SecurePasswordRecord.php index fb12350..6f02999 100644 --- a/src/Socialbox/Objects/Database/SecurePasswordRecord.php +++ b/src/Socialbox/Objects/Database/SecurePasswordRecord.php @@ -81,4 +81,20 @@ { return $this->updated; } + + public function toArray(): array + { + return [ + 'peer_uuid' => $this->peerUuid, + 'iv' => $this->iv, + 'encrypted_password' => $this->encryptedPassword, + 'encrypted_tag' => $this->encryptedTag, + 'updated' => $this->updated->format('Y-m-d H:i:s') + ]; + } + + public static function fromArray(array $data): SecurePasswordRecord + { + return new SecurePasswordRecord($data); + } } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/SessionRecord.php b/src/Socialbox/Objects/Database/SessionRecord.php index 03dea46..77d75df 100644 --- a/src/Socialbox/Objects/Database/SessionRecord.php +++ b/src/Socialbox/Objects/Database/SessionRecord.php @@ -144,6 +144,26 @@ return $this->flags; } + /** + * Checks if a given flag exists in the list of session flags. + * + * @param string|SessionFlags $flag The flag to check, either as a string or a SessionFlags object. + * @return bool True if the flag exists, false otherwise. + */ + public function flagExists(string|SessionFlags $flag): bool + { + if(is_string($flag)) + { + $flag = SessionFlags::tryFrom($flag); + if($flag === null) + { + return false; + } + } + + return in_array($flag, $this->flags); + } + /** * Retrieves the timestamp of the last request made. * diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index cee7473..c293d67 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -293,6 +293,17 @@ { $method = StandardMethods::tryFrom($rpcRequest->getMethod()); + try + { + $method->checkAccess($clientRequest); + } + catch (StandardException $e) + { + $response = $e->produceError($rpcRequest); + $results[] = $response->toArray(); + continue; + } + if($method === false) { Logger::getLogger()->warning('The requested method does not exist');