diff --git a/examples/index.php b/examples/index.php index 875a454..fc2b206 100644 --- a/examples/index.php +++ b/examples/index.php @@ -5,7 +5,7 @@ try { - \Socialbox\Socialbox::handleRpc(); + \Socialbox\Socialbox::handleRequest(); } catch(Exception $e) { diff --git a/src/Socialbox/Abstracts/Method.php b/src/Socialbox/Abstracts/Method.php index d3f5971..beac6a0 100644 --- a/src/Socialbox/Abstracts/Method.php +++ b/src/Socialbox/Abstracts/Method.php @@ -8,6 +8,7 @@ use Socialbox\Exceptions\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\SessionManager; use Socialbox\Objects\ClientRequest; +use Socialbox\Objects\ClientRequestOld; use Socialbox\Objects\Database\SessionRecord; use Socialbox\Objects\RpcRequest; @@ -22,27 +23,4 @@ abstract class Method * @throws StandardException If a standard exception is thrown, it will be handled by the engine. */ public static abstract function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface; - - /** - * @param ClientRequest $request The client request object - * @return SessionRecord|null Returns null if the client has not provided a Session UUID - * @throws StandardException Thrown if standard exceptions are to be thrown regarding this - */ - protected static function getSession(ClientRequest $request): ?SessionRecord - { - if($request->getSessionUuid() === null) - { - return null; - } - - try - { - // NOTE: If the session UUID was provided, it has already been validated up until this point - return SessionManager::getSession($request->getSessionUuid()); - } - catch(DatabaseOperationException $e) - { - throw new StandardException("There was an error while retrieving the session from the server", StandardError::INTERNAL_SERVER_ERROR); - } - } } \ No newline at end of file diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index 7537424..2b0d315 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -35,7 +35,7 @@ class Configuration $config->setDefault('instance.domain', null); $config->setDefault('instance.rpc_endpoint', null); $config->setDefault('instance.encryption_keys_count', 5); - $config->setDefault('instance.encryption_record_count', 5); + $config->setDefault('instance.encryption_records_count', 5); $config->setDefault('instance.private_key', null); $config->setDefault('instance.public_key', null); $config->setDefault('instance.encryption_keys', null); diff --git a/src/Socialbox/Classes/Cryptography.php b/src/Socialbox/Classes/Cryptography.php index 28e0b17..19b17a0 100644 --- a/src/Socialbox/Classes/Cryptography.php +++ b/src/Socialbox/Classes/Cryptography.php @@ -16,6 +16,7 @@ class Cryptography private const int PADDING = OPENSSL_PKCS1_OAEP_PADDING; private const string PEM_PRIVATE_HEADER = 'PRIVATE'; private const string PEM_PUBLIC_HEADER = 'PUBLIC'; + private const string TRANSPORT_ENCRYPTION = 'aes-256-cbc'; /** * Generates a new public-private key pair. @@ -307,4 +308,74 @@ class Cryptography return $keys; } + + public static function generateEncryptionKey(): string + { + try + { + return base64_encode(random_bytes(32)); + } + catch (RandomException $e) + { + throw new CryptographyException('Failed to generate encryption key: ' . $e->getMessage()); + } + } + + /** + * Encrypts the given content for transport using the provided encryption key. + * + * @param string $content The content to be encrypted. + * @param string $encryptionKey The encryption key used for encrypting the content. + * @return string The Base64 encoded string containing the IV and the encrypted content. + * @throws CryptographyException If the IV generation or encryption process fails. + */ + public static function encryptTransport(string $content, string $encryptionKey): string + { + try + { + $iv = random_bytes(openssl_cipher_iv_length('aes-256-cbc')); + } + catch (RandomException $e) + { + throw new CryptographyException('Failed to generate IV: ' . $e->getMessage()); + } + + $encrypted = openssl_encrypt($content, self::TRANSPORT_ENCRYPTION, base64_decode($encryptionKey), OPENSSL_RAW_DATA, $iv); + + if($encrypted === false) + { + throw new CryptographyException('Failed to encrypt transport content: ' . openssl_error_string()); + } + + return base64_encode($iv . $encrypted); + } + + /** + * Decrypts the given encrypted transport content using the provided encryption key. + * + * @param string $encryptedContent The Base64 encoded encrypted content to be decrypted. + * @param string $encryptionKey The Base64 encoded encryption key used for decryption. + * @return string The decrypted content as a string. + * @throws CryptographyException If the decryption process fails. + */ + public static function decryptTransport(string $encryptedContent, string $encryptionKey): string + { + $decodedData = base64_decode($encryptedContent); + $ivLength = openssl_cipher_iv_length(self::TRANSPORT_ENCRYPTION); + + // Perform decryption + $decryption = openssl_decrypt(substr($decodedData, $ivLength), + self::TRANSPORT_ENCRYPTION, + base64_decode($encryptionKey), + OPENSSL_RAW_DATA, + substr($decodedData, 0, $ivLength) + ); + + if($decryption === false) + { + throw new CryptographyException('Failed to decrypt transport content: ' . openssl_error_string()); + } + + return $decryption; + } } \ No newline at end of file diff --git a/src/Socialbox/Classes/RpcHandler.php b/src/Socialbox/Classes/RpcHandler.php index 458a57c..e64268b 100644 --- a/src/Socialbox/Classes/RpcHandler.php +++ b/src/Socialbox/Classes/RpcHandler.php @@ -7,10 +7,11 @@ use InvalidArgumentException; use RuntimeException; use Socialbox\Enums\StandardHeaders; use Socialbox\Exceptions\DatabaseOperationException; +use Socialbox\Exceptions\RequestException; use Socialbox\Exceptions\RpcException; use Socialbox\Exceptions\StandardException; use Socialbox\Managers\SessionManager; -use Socialbox\Objects\ClientRequest; +use Socialbox\Objects\ClientRequestOld; use Socialbox\Objects\RpcRequest; class RpcHandler @@ -19,16 +20,12 @@ class RpcHandler * Gets the incoming ClientRequest object, validates if the request is valid & if a session UUID is provided * checks if the request signature matches the client's provided public key. * - * @return ClientRequest The parsed ClientRequest object + * @return ClientRequestOld The parsed ClientRequest object + * @throws RequestException * @throws RpcException Thrown if the request is invalid */ - public static function getClientRequest(): ClientRequest + public static function getClientRequest(): ClientRequestOld { - if($_SERVER['REQUEST_METHOD'] !== 'POST') - { - throw new RpcException('Invalid Request Method, expected POST', 400); - } - try { $headers = Utilities::getRequestHeaders(); @@ -36,7 +33,7 @@ class RpcHandler { if (!isset($headers[$header])) { - throw new RpcException("Missing required header: $header", 400); + throw new RequestException("Missing required header: $header", 400); } // Validate the headers @@ -73,7 +70,7 @@ class RpcHandler throw new RpcException("Failed to parse request: " . $e->getMessage(), 400, $e); } - $clientRequest = new ClientRequest($headers, self::getRpcRequests(), self::getRequestHash()); + $clientRequest = new ClientRequestOld($headers, self::getRpcRequests(), self::getRequestHash()); // Verify the session & request signature if($clientRequest->getSessionUuid() !== null) diff --git a/src/Socialbox/Classes/StandardMethods/Authenticate.php b/src/Socialbox/Classes/StandardMethods/Authenticate.php deleted file mode 100644 index b3eaf19..0000000 --- a/src/Socialbox/Classes/StandardMethods/Authenticate.php +++ /dev/null @@ -1,50 +0,0 @@ -getParameters()['type'])) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required parameter \'type\''); - } - - if(strlen($rpcRequest->getParameters()['type']) == 0) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Parameter \'type\' cannot be empty'); - } - - return match (FirstLevelAuthentication::tryFrom($rpcRequest->getParameters()['type'])) - { - FirstLevelAuthentication::PASSWORD => self::handlePassword($request), - - default => $rpcRequest->produceError(StandardError::UNSUPPORTED_AUTHENTICATION_TYPE, - sprintf('Unsupported authentication type: %s', $rpcRequest->getParameters()['type']) - ), - }; - } - - /** - * Handles the password authentication phase for the peer - * - * @param ClientRequest $request - * @return SerializableInterface - */ - private static function handlePassword(ClientRequest $request): SerializableInterface - { - - } -} \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/CreateSession.php b/src/Socialbox/Classes/StandardMethods/CreateSession.php deleted file mode 100644 index 9b11360..0000000 --- a/src/Socialbox/Classes/StandardMethods/CreateSession.php +++ /dev/null @@ -1,45 +0,0 @@ -containsParameter('public_key')) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing parameter \'public_key\''); - } - - try - { - $uuid = SessionManager::createSession($rpcRequest->getParameter('public_key')); - } - catch(DatabaseOperationException $e) - { - return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, 'There was an error while trying to create a new session: ' . $e->getMessage()); - } - catch(InvalidArgumentException $e) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, $e->getMessage()); - } - - return $rpcRequest->produceResponse($uuid); - } -} \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/GetMe.php b/src/Socialbox/Classes/StandardMethods/GetMe.php deleted file mode 100644 index fbd9826..0000000 --- a/src/Socialbox/Classes/StandardMethods/GetMe.php +++ /dev/null @@ -1,47 +0,0 @@ -getSessionUuid() === null) - { - return $rpcRequest->produceError(StandardError::SESSION_REQUIRED); - } - - try - { - // Get the session and check if it's already authenticated - $session = SessionManager::getSession($request->getSessionUuid()); - if($session->getPeerUuid() === null) - { - return $rpcRequest->produceError(StandardError::AUTHENTICATION_REQUIRED); - } - - // Get the peer and return it - return $rpcRequest->produceResponse(RegisteredPeerManager::getPeer($session->getPeerUuid())->toSelfUser()); - } - catch(DatabaseOperationException $e) - { - throw new StandardException("There was an unexpected error while trying to register", StandardError::INTERNAL_SERVER_ERROR, $e); - } - } -} \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/GetSession.php b/src/Socialbox/Classes/StandardMethods/GetSession.php deleted file mode 100644 index f0035ae..0000000 --- a/src/Socialbox/Classes/StandardMethods/GetSession.php +++ /dev/null @@ -1,40 +0,0 @@ -getSessionUuid() === null) - { - return $rpcRequest->produceError(StandardError::SESSION_REQUIRED); - } - - try - { - // Get the session - $session = SessionManager::getSession($request->getSessionUuid()); - } - catch(DatabaseOperationException $e) - { - throw new StandardException("There was an unexpected error while trying to retrieve the session", StandardError::INTERNAL_SERVER_ERROR, $e); - } - - - } - } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/Identify.php b/src/Socialbox/Classes/StandardMethods/Identify.php deleted file mode 100644 index 42d324d..0000000 --- a/src/Socialbox/Classes/StandardMethods/Identify.php +++ /dev/null @@ -1,73 +0,0 @@ -containsParameter('username')) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing parameter \'username\''); - } - - // Check if the username is valid - if(!Validator::validateUsername($rpcRequest->getParameter('username'))) - { - return $rpcRequest->produceError(StandardError::INVALID_USERNAME, StandardError::INVALID_USERNAME->getMessage()); - } - - // Check if the request has a Session UUID - if($request->getSessionUuid() === null) - { - return $rpcRequest->produceError(StandardError::SESSION_REQUIRED); - } - - try - { - // Get the session and check if it's already authenticated - $session = SessionManager::getSession($request->getSessionUuid()); - - // If the session is already authenticated, return an error - if($session->getPeerUuid() !== null) - { - return $rpcRequest->produceError(StandardError::ALREADY_AUTHENTICATED); - } - - // If the username does not exist, return an error - if(!RegisteredPeerManager::usernameExists($rpcRequest->getParameter('username'))) - { - return $rpcRequest->produceError(StandardError::NOT_REGISTERED, StandardError::NOT_REGISTERED->getMessage()); - } - - // Create session to be identified as the provided username - SessionManager::updatePeer($session->getUuid(), $rpcRequest->getParameter('username')); - - // Set the required session flags - $initialFlags = []; - } - catch(DatabaseOperationException $e) - { - throw new StandardException("There was an unexpected error while trying to register", StandardError::INTERNAL_SERVER_ERROR, $e); - } - - // Return true to indicate the operation was a success - return $rpcRequest->produceResponse(true); - } - } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/Ping.php b/src/Socialbox/Classes/StandardMethods/Ping.php index c0b8007..93724a9 100644 --- a/src/Socialbox/Classes/StandardMethods/Ping.php +++ b/src/Socialbox/Classes/StandardMethods/Ping.php @@ -1,19 +1,20 @@ produceResponse(true); - } -} \ No newline at end of file + /** + * @inheritDoc + */ + public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface + { + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/Register.php b/src/Socialbox/Classes/StandardMethods/Register.php deleted file mode 100644 index 6bedc2d..0000000 --- a/src/Socialbox/Classes/StandardMethods/Register.php +++ /dev/null @@ -1,80 +0,0 @@ -isRegistrationEnabled()) - { - return $rpcRequest->produceError(StandardError::REGISTRATION_DISABLED, StandardError::REGISTRATION_DISABLED->getMessage()); - } - - // Check if the username parameter exists - if(!$rpcRequest->containsParameter('username')) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing parameter \'username\''); - } - - // Check if the username is valid - if(!Validator::validateUsername($rpcRequest->getParameter('username'))) - { - return $rpcRequest->produceError(StandardError::INVALID_USERNAME, StandardError::INVALID_USERNAME->getMessage()); - } - - // Check if the username exists already - try - { - if (RegisteredPeerManager::usernameExists($rpcRequest->getParameter('username'))) - { - return $rpcRequest->produceError(StandardError::USERNAME_ALREADY_EXISTS, StandardError::USERNAME_ALREADY_EXISTS->getMessage()); - } - } - catch (DatabaseOperationException $e) - { - throw new StandardException("There was an unexpected error while trying to check the username existence", StandardError::INTERNAL_SERVER_ERROR, $e); - } - - // Check if the request has a Session UUID - if($request->getSessionUuid() === null) - { - return $rpcRequest->produceError(StandardError::SESSION_REQUIRED); - } - - try - { - // Get the session and check if it's already authenticated - $session = SessionManager::getSession($request->getSessionUuid()); - if($session->getPeerUuid() !== null) - { - return $rpcRequest->produceError(StandardError::ALREADY_AUTHENTICATED); - } - - // Create the peer & set the current's session authenticated peer as the newly created peer - SessionManager::updatePeer($session->getUuid(), RegisteredPeerManager::createPeer($rpcRequest->getParameter('username'))); - } - catch(DatabaseOperationException $e) - { - throw new StandardException("There was an unexpected error while trying to register", StandardError::INTERNAL_SERVER_ERROR, $e); - } - - // Return true to indicate the operation was a success - 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 5cb8524..3d7f5fb 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php @@ -11,7 +11,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; class VerificationAnswerImageCaptcha extends Method @@ -20,7 +20,7 @@ class VerificationAnswerImageCaptcha extends Method /** * @inheritDoc */ - public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface + public static function execute(ClientRequestOld $request, RpcRequest $rpcRequest): ?SerializableInterface { // Check if the request has a Session UUID if($request->getSessionUuid() === null) diff --git a/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php index 681e366..1956706 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php @@ -13,7 +13,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; use Socialbox\Objects\Standard\ImageCaptcha; @@ -22,7 +22,7 @@ class VerificationGetImageCaptcha extends Method /** * @inheritDoc */ - public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface + public static function execute(ClientRequestOld $request, RpcRequest $rpcRequest): ?SerializableInterface { // Check if the request has a Session UUID if($request->getSessionUuid() === null) diff --git a/src/Socialbox/Classes/Utilities.php b/src/Socialbox/Classes/Utilities.php index a935c76..efa786b 100644 --- a/src/Socialbox/Classes/Utilities.php +++ b/src/Socialbox/Classes/Utilities.php @@ -7,6 +7,7 @@ use InvalidArgumentException; use JsonException; use RuntimeException; use Socialbox\Enums\StandardHeaders; +use Socialbox\Objects\PeerAddress; use Throwable; class Utilities diff --git a/src/Socialbox/Enums/Flags/PeerFlags.php b/src/Socialbox/Enums/Flags/PeerFlags.php index 86b69bb..1ca08b2 100644 --- a/src/Socialbox/Enums/Flags/PeerFlags.php +++ b/src/Socialbox/Enums/Flags/PeerFlags.php @@ -13,15 +13,6 @@ enum PeerFlags : string // General Flags case VERIFIED = 'VERIFIED'; - // Verification Flags - case VER_SET_PASSWORD = 'VER_SET_PASSWORD'; - case VER_SET_OTP = 'VER_SET_OTP'; - case VER_SET_DISPLAY_NAME = 'VER_SET_DISPLAY_NAME'; - case VER_EMAIL = 'VER_EMAIL'; - case VER_SMS = 'VER_SMS'; - case VER_PHONE_CALL = 'VER_PHONE_CALL'; - case VER_SOLVE_IMAGE_CAPTCHA = 'VER_SOLVE_IMAGE_CAPTCHA'; - /** * Converts an array of PeerFlags enums to a string representation * @@ -48,20 +39,4 @@ enum PeerFlags : string return array_map(fn(string $value) => PeerFlags::from(trim($value)), explode(',', $flagString)); } - - /** - * Returns whether the flag is public. Public flags can be seen by other peers. - * - * @return bool - */ - public function isPublic(): bool - { - return match($this) - { - self::VER_SET_PASSWORD, - self::VER_SET_OTP, - self::VER_SOLVE_IMAGE_CAPTCHA => false, - default => true, - }; - } } diff --git a/src/Socialbox/Enums/Flags/SessionFlags.php b/src/Socialbox/Enums/Flags/SessionFlags.php index 821c514..15be9a7 100644 --- a/src/Socialbox/Enums/Flags/SessionFlags.php +++ b/src/Socialbox/Enums/Flags/SessionFlags.php @@ -5,9 +5,9 @@ enum SessionFlags : string { // Verification, require fields - case VER_SET_PASSWORD = 'VER_SET_PASSWORD'; // Peer has to set a password - case VER_SET_OTP = 'VER_SET_OTP'; // Peer has to set an OTP - case VER_SET_DISPLAY_NAME = 'VER_SET_DISPLAY_NAME'; // Peer has to set a display name + case SET_PASSWORD = 'SET_PASSWORD'; // Peer has to set a password + case SET_OTP = 'SET_OTP'; // Peer has to set an OTP + case SET_DISPLAY_NAME = 'SET_DISPLAY_NAME'; // Peer has to set a display name // Verification, verification requirements case VER_EMAIL = 'VER_EMAIL'; // Peer has to verify their email @@ -18,4 +18,31 @@ // Login, require fields case VER_PASSWORD = 'VER_PASSWORD'; // Peer has to enter their password case VER_OTP = 'VER_OTP'; // Peer has to enter their OTP + + /** + * Converts an array of SessionFlags to a comma-separated string of their values. + * + * @param array $flags An array of SessionFlags objects to be converted. + * @return string A comma-separated string of the values of the provided SessionFlags. + */ + public static function toString(array $flags): string + { + return implode(',', array_map(fn(SessionFlags $flag) => $flag->value, $flags)); + } + + /** + * Converts a comma-separated string of flag values into an array of SessionFlags objects. + * + * @param string $flagString A comma-separated string representing flag values. + * @return array An array of SessionFlags objects created from the provided string. + */ + public static function fromString(string $flagString): array + { + if (empty($flagString)) + { + return []; + } + + return array_map(fn(string $value) => SessionFlags::from(trim($value)), explode(',', $flagString)); + } } diff --git a/src/Socialbox/Enums/SessionState.php b/src/Socialbox/Enums/SessionState.php index f277d2b..21aff42 100644 --- a/src/Socialbox/Enums/SessionState.php +++ b/src/Socialbox/Enums/SessionState.php @@ -4,6 +4,11 @@ namespace Socialbox\Enums; enum SessionState : string { + /** + * The session is awaiting a Diffie-Hellman exchange to be completed + */ + case AWAITING_DHE = 'AWAITING_DHE'; + /** * The session is currently active and usable */ diff --git a/src/Socialbox/Enums/StandardError.php b/src/Socialbox/Enums/StandardError.php index ad0c31c..7f80aaf 100644 --- a/src/Socialbox/Enums/StandardError.php +++ b/src/Socialbox/Enums/StandardError.php @@ -17,15 +17,19 @@ enum StandardError : int // Authentication/Cryptography Errors case INVALID_PUBLIC_KEY = -3000; - case UNSUPPORTED_AUTHENTICATION_TYPE = -3001; - case ALREADY_AUTHENTICATED = -3002; - case AUTHENTICATION_REQUIRED = -3003; - case SESSION_NOT_FOUND = -3004; - case SESSION_REQUIRED = -3005; - case REGISTRATION_DISABLED = -3006; - case CAPTCHA_NOT_AVAILABLE = -3007; - case INCORRECT_CAPTCHA_ANSWER = -3008; - case CAPTCHA_EXPIRED = -3009; + + case SESSION_REQUIRED = -3001; + case SESSION_NOT_FOUND = -3002; + case SESSION_EXPIRED = -3003; + case SESSION_DHE_REQUIRED = -3004; + + case ALREADY_AUTHENTICATED = -3005; + case UNSUPPORTED_AUTHENTICATION_TYPE = -3006; + case AUTHENTICATION_REQUIRED = -3007; + case REGISTRATION_DISABLED = -3008; + case CAPTCHA_NOT_AVAILABLE = -3009; + case INCORRECT_CAPTCHA_ANSWER = -3010; + case CAPTCHA_EXPIRED = -3011; // General Error Messages case PEER_NOT_FOUND = -4000; diff --git a/src/Socialbox/Enums/StandardHeaders.php b/src/Socialbox/Enums/StandardHeaders.php index 9d707de..931280d 100644 --- a/src/Socialbox/Enums/StandardHeaders.php +++ b/src/Socialbox/Enums/StandardHeaders.php @@ -1,68 +1,32 @@ true, + case REQUEST_TYPE = 'Request-Type'; + case IDENTIFY_AS = 'Identify-As'; + case CLIENT_NAME = 'Client-Name'; + case CLIENT_VERSION = 'Client-Version'; + case PUBLIC_KEY = 'Public-Key'; - default => false, - }; - } + case SESSION_UUID = 'Session-UUID'; + case SIGNATURE = 'Signature'; - /** - * Retrieves an array of required headers. - * - * @return array An array containing only the headers that are marked as required. - */ - public static function getRequiredHeaders(): array - { - $results = []; - foreach(StandardHeaders::cases() as $header) + /** + * @return array + */ + public static function toArray(): array { - if($header->isRequired()) + $results = []; + foreach(StandardHeaders::cases() as $header) { $results[] = $header->value; } + + return $results; } - - return $results; - } - - /** - * @return array - */ - public static function toArray(): array - { - $results = []; - foreach(StandardHeaders::cases() as $header) - { - $results[] = $header->value; - } - - return $results; - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index b454ec2..a606815 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -12,17 +12,12 @@ use Socialbox\Classes\StandardMethods\Register; use Socialbox\Exceptions\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; +use Socialbox\Objects\ClientRequestOld; use Socialbox\Objects\RpcRequest; enum StandardMethods : string { case PING = 'ping'; - case CREATE_SESSION = 'createSession'; - case REGISTER = 'register'; - case IDENTIFY = 'identify'; - case GET_ME = 'getMe'; - case VERIFICATION_GET_IMAGE_CAPTCHA = 'verificationGetImageCaptcha'; - case VERIFICATION_ANSWER_IMAGE_CAPTCHA = 'verificationAnswerImageCaptcha'; /** * @param ClientRequest $request @@ -35,12 +30,6 @@ enum StandardMethods : string return match ($this) { self::PING => Ping::execute($request, $rpcRequest), - self::CREATE_SESSION => CreateSession::execute($request, $rpcRequest), - self::REGISTER => Register::execute($request, $rpcRequest), - self::IDENTIFY => Identify::execute($request, $rpcRequest), - self::GET_ME => GetMe::execute($request, $rpcRequest), - self::VERIFICATION_GET_IMAGE_CAPTCHA => VerificationGetImageCaptcha::execute($request, $rpcRequest), - self::VERIFICATION_ANSWER_IMAGE_CAPTCHA => VerificationAnswerImageCaptcha::execute($request, $rpcRequest), }; } } \ No newline at end of file diff --git a/src/Socialbox/Enums/Types/RequestType.php b/src/Socialbox/Enums/Types/RequestType.php new file mode 100644 index 0000000..0beef55 --- /dev/null +++ b/src/Socialbox/Enums/Types/RequestType.php @@ -0,0 +1,21 @@ +verbose(sprintf("Creating a new peer with username %s", $username)); $uuid = Uuid::v4()->toRfc4122(); - // If `enabled` is True, we insert the peer into the database as an activated account. - if($enabled) - { - try - { - $statement = Database::getConnection()->prepare('INSERT INTO `registered_peers` (uuid, username, enabled) VALUES (?, ?, ?)'); - $statement->bindParam(1, $uuid); - $statement->bindParam(2, $username); - $statement->bindParam(3, $enabled, PDO::PARAM_BOOL); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to create the peer in the database', $e); - } - - return $uuid; - } - - // Otherwise, we insert the peer into the database as a disabled account & the required verification flags. - $flags = []; - - if(Configuration::getRegistrationConfiguration()->isPasswordRequired()) - { - $flags[] = PeerFlags::VER_SET_PASSWORD; - } - - if(Configuration::getRegistrationConfiguration()->isOtpRequired()) - { - $flags[] = PeerFlags::VER_SET_OTP; - } - - if(Configuration::getRegistrationConfiguration()->isDisplayNameRequired()) - { - $flags[] = PeerFlags::VER_SET_DISPLAY_NAME; - } - - if(Configuration::getRegistrationConfiguration()->isEmailVerificationRequired()) - { - $flags[] = PeerFlags::VER_EMAIL; - } - - if(Configuration::getRegistrationConfiguration()->isSmsVerificationRequired()) - { - $flags[] = PeerFlags::VER_SMS; - } - - if(Configuration::getRegistrationConfiguration()->isPhoneCallVerificationRequired()) - { - $flags[] = PeerFlags::VER_PHONE_CALL; - } - - if(Configuration::getRegistrationConfiguration()->isImageCaptchaVerificationRequired()) - { - $flags[] = PeerFlags::VER_SOLVE_IMAGE_CAPTCHA; - } - try { - $implodedFlags = implode(',', array_map(fn($flag) => $flag->name, $flags)); - $statement = Database::getConnection()->prepare('INSERT INTO `registered_peers` (uuid, username, enabled, flags) VALUES (?, ?, ?, ?)'); + $statement = Database::getConnection()->prepare('INSERT INTO `registered_peers` (uuid, username, enabled) VALUES (?, ?, ?)'); $statement->bindParam(1, $uuid); $statement->bindParam(2, $username); $statement->bindParam(3, $enabled, PDO::PARAM_BOOL); - $statement->bindParam(4, $implodedFlags); $statement->execute(); } catch(PDOException $e) @@ -200,11 +142,10 @@ class RegisteredPeerManager * Retrieves a peer record by the given username. * * @param string $username The username of the peer to be retrieved. - * @return RegisteredPeerRecord The record of the peer associated with the given username. + * @return RegisteredPeerRecord|null The record of the peer associated with the given username. * @throws DatabaseOperationException If there is an error while querying the database. - * @throws StandardException If the peer does not exist. */ - public static function getPeerByUsername(string $username): RegisteredPeerRecord + public static function getPeerByUsername(string $username): ?RegisteredPeerRecord { Logger::getLogger()->verbose(sprintf("Retrieving peer %s from the database", $username)); @@ -218,7 +159,7 @@ class RegisteredPeerManager if($result === false) { - throw new StandardException(sprintf("The requested peer '%s' does not exist", $username), StandardError::PEER_NOT_FOUND); + return null; } return new RegisteredPeerRecord($result); @@ -365,4 +306,35 @@ class RegisteredPeerManager throw new DatabaseOperationException('Failed to remove the flag from the peer in the database', $e); } } + + /** + * + */ + public static function getPasswordAuthentication(string|RegisteredPeerRecord $peerUuid): ?SecurePasswordRecord + { + if($peerUuid instanceof RegisteredPeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + try + { + $statement = Database::getConnection()->prepare('SELECT * FROM `authentication_passwords` WHERE peer_uuid=?'); + $statement->bindParam(1, $peerUuid); + $statement->execute(); + + $result = $statement->fetch(PDO::FETCH_ASSOC); + + if($result === false) + { + return null; + } + + return new SecurePasswordRecord($result); + } + catch(PDOException | \DateMalformedStringException $e) + { + throw new DatabaseOperationException('Failed to get the secure password record from the database', $e); + } + } } \ No newline at end of file diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index f7565da..d2c2c23 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -7,10 +7,12 @@ use InvalidArgumentException; use PDO; use PDOException; + use Socialbox\Classes\Configuration; use Socialbox\Classes\Cryptography; use Socialbox\Classes\Database; use Socialbox\Classes\Logger; use Socialbox\Classes\Utilities; + use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\SessionState; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; @@ -31,25 +33,89 @@ * @throws InvalidArgumentException If the public key is empty or invalid. * @throws DatabaseOperationException If there is an error while creating the session in the database. */ - public static function createSession(string $publicKey): string + public static function createSession(string $publicKey, RegisteredPeerRecord $peer): string { if($publicKey === '') { - throw new InvalidArgumentException('The public key cannot be empty', 400); + throw new InvalidArgumentException('The public key cannot be empty'); } if(!Cryptography::validatePublicKey($publicKey)) { - throw new InvalidArgumentException('The given public key is invalid', 400); + throw new InvalidArgumentException('The given public key is invalid'); } $uuid = Uuid::v4()->toRfc4122(); + $flags = []; + + if($peer->isEnabled()) + { + if(RegisteredPeerManager::getPasswordAuthentication($peer)) + { + $flags[] = SessionFlags::VER_PASSWORD; + } + + if(Configuration::getRegistrationConfiguration()->isImageCaptchaVerificationRequired()) + { + $flags[] = SessionFlags::VER_IMAGE_CAPTCHA; + } + } + else + { + if(Configuration::getRegistrationConfiguration()->isDisplayNameRequired()) + { + $flags[] = SessionFlags::SET_DISPLAY_NAME; + } + + if(Configuration::getRegistrationConfiguration()->isEmailVerificationRequired()) + { + $flags[] = SessionFlags::VER_EMAIL; + } + + if(Configuration::getRegistrationConfiguration()->isSmsVerificationRequired()) + { + $flags[] = SessionFlags::VER_SMS; + } + + if(Configuration::getRegistrationConfiguration()->isPhoneCallVerificationRequired()) + { + $flags[] = SessionFlags::VER_PHONE_CALL; + } + + if(Configuration::getRegistrationConfiguration()->isImageCaptchaVerificationRequired()) + { + $flags[] = SessionFlags::VER_IMAGE_CAPTCHA; + } + + if(Configuration::getRegistrationConfiguration()->isPasswordRequired()) + { + $flags[] = SessionFlags::SET_PASSWORD; + } + + if(Configuration::getRegistrationConfiguration()->isOtpRequired()) + { + $flags[] = SessionFlags::SET_OTP; + } + } + + if(count($flags) > 0) + { + $implodedFlags = SessionFlags::toString($flags); + } + else + { + $implodedFlags = null; + } + + $peerUuid = $peer->getUuid(); try { - $statement = Database::getConnection()->prepare("INSERT INTO sessions (uuid, public_key) VALUES (?, ?)"); + $statement = Database::getConnection()->prepare("INSERT INTO sessions (uuid, peer_uuid, public_key, flags) VALUES (?, ?, ?, ?)"); $statement->bindParam(1, $uuid); - $statement->bindParam(2, $publicKey); + $statement->bindParam(2, $peerUuid); + $statement->bindParam(3, $publicKey); + $statement->bindParam(4, $implodedFlags); $statement->execute(); } catch(PDOException $e) @@ -219,6 +285,8 @@ $statement = Database::getConnection()->prepare('UPDATE sessions SET state=? WHERE uuid=?'); $statement->bindParam(1, $state_value); $statement->bindParam(2, $uuid); + + $statement->execute(); } catch(PDOException $e) { @@ -226,6 +294,34 @@ } } + /** + * Updates the encryption key for the specified session. + * + * @param string $uuid The unique identifier of the session for which the encryption key is to be set. + * @param string $encryptionKey The new encryption key to be assigned. + * @return void + * @throws DatabaseOperationException If the database operation fails. + */ + public static function setEncryptionKey(string $uuid, string $encryptionKey): void + { + Logger::getLogger()->verbose(sprintf('Setting the encryption key for %s', $uuid)); + + try + { + $state_value = SessionState::ACTIVE->value; + $statement = Database::getConnection()->prepare('UPDATE sessions SET state=?, encryption_key=? WHERE uuid=?'); + $statement->bindParam(1, $state_value); + $statement->bindParam(2, $encryptionKey); + $statement->bindParam(3, $uuid); + + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to set the encryption key', $e); + } + } + /** * Retrieves the flags associated with a specific session. * diff --git a/src/Socialbox/Objects/ClientRequest.php b/src/Socialbox/Objects/ClientRequest.php index ecb942a..5084b5f 100644 --- a/src/Socialbox/Objects/ClientRequest.php +++ b/src/Socialbox/Objects/ClientRequest.php @@ -1,142 +1,245 @@ headers = $headers; - $this->requests = $requests; - $this->requestHash = $requestHash; - } + private array $headers; + private RequestType $requestType; + private ?string $requestBody; - /** - * @return array - */ - public function getHeaders(): array - { - return $this->headers; - } + private string $clientName; + private string $clientVersion; + private ?string $identifyAs; + private ?string $sessionUuid; + private ?string $signature; - /** - * @return RpcRequest[] - */ - public function getRequests(): array - { - return $this->requests; - } - - public function getHash(): string - { - return $this->requestHash; - } - - public function getClientName(): string - { - return $this->headers[StandardHeaders::CLIENT_NAME->value]; - } - - public function getClientVersion(): string - { - return $this->headers[StandardHeaders::CLIENT_VERSION->value]; - } - - public function getSessionUuid(): ?string - { - if(!isset($this->headers[StandardHeaders::SESSION_UUID->value])) + public function __construct(array $headers, ?string $requestBody) { - return null; + $this->headers = $headers; + $this->requestBody = $requestBody; + + $this->clientName = $headers[StandardHeaders::CLIENT_NAME->value]; + $this->clientVersion = $headers[StandardHeaders::CLIENT_VERSION->value]; + $this->requestType = RequestType::from($headers[StandardHeaders::REQUEST_TYPE->value]); + $this->identifyAs = $headers[StandardHeaders::IDENTIFY_AS->value] ?? null; + $this->sessionUuid = $headers[StandardHeaders::SESSION_UUID->value] ?? null; + $this->signature = $headers[StandardHeaders::SIGNATURE->value] ?? null; } - return $this->headers[StandardHeaders::SESSION_UUID->value]; - } - - public function getFromPeer(): ?PeerAddress - { - if(!isset($this->headers[StandardHeaders::FROM_PEER->value])) + public function getHeaders(): array { - return null; + return $this->headers; } - return PeerAddress::fromAddress($this->headers[StandardHeaders::FROM_PEER->value]); - } - - public function getSignature(): ?string - { - if(!isset($this->headers[StandardHeaders::SIGNATURE->value])) + public function headerExists(StandardHeaders|string $header): bool { - return null; + if(is_string($header)) + { + return isset($this->headers[$header]); + } + + return isset($this->headers[$header->value]); } - return $this->headers[StandardHeaders::SIGNATURE->value]; - } - - /** - * @return bool - * @throws DatabaseOperationException - */ - public function verifySignature(): bool - { - $signature = $this->getSignature(); - $sessionUuid = $this->getSessionUuid(); - - if($signature == null || $sessionUuid == null) + public function getHeader(StandardHeaders|string $header): ?string { - return false; + if(!$this->headerExists($header)) + { + return null; + } + + if(is_string($header)) + { + return $this->headers[$header]; + } + + return $this->headers[$header->value]; } - try + public function getRequestBody(): ?string { - $session = SessionManager::getSession($sessionUuid); + return $this->requestBody; } - catch(StandardException $e) + + public function getClientName(): string { - if($e->getStandardError() == StandardError::SESSION_NOT_FOUND) + return $this->clientName; + } + + public function getClientVersion(): string + { + return $this->clientVersion; + } + + public function getRequestType(): RequestType + { + return $this->requestType; + } + + public function getIdentifyAs(): ?PeerAddress + { + if($this->identifyAs === null) + { + return null; + } + + return PeerAddress::fromAddress($this->identifyAs); + } + + public function getSessionUuid(): ?string + { + return $this->sessionUuid; + } + + public function getSession(): ?SessionRecord + { + if($this->sessionUuid === null) + { + return null; + } + + return SessionManager::getSession($this->sessionUuid); + } + + public function getSignature(): ?string + { + return $this->signature; + } + + private function verifySignature(string $decryptedContent): bool + { + if($this->getSignature() == null || $this->getSessionUuid() == null) { return false; } - throw new RuntimeException($e); + try + { + return Cryptography::verifyContent(hash('sha1', $decryptedContent), $this->getSignature(), $this->getSession()->getPublicKey()); + } + catch(CryptographyException) + { + return false; + } } - try + /** + * Handles a POST request, returning an array of RpcRequest objects + * expects a JSON encoded body with either a single RpcRequest object or an array of RpcRequest objects + * + * @return RpcRequest[] The parsed RpcRequest objects + * @throws RequestException Thrown if the request is invalid + */ + public function getRpcRequests(): array { - return Cryptography::verifyContent($this->getHash(), $signature, $session->getPublicKey()); + if($this->getSessionUuid() === null) + { + throw new RequestException("Session UUID required", 400); + } + + // Get the existing session + $session = $this->getSession(); + + // If we're awaiting a DHE, encryption is not possible at this point + if($session->getState() === SessionState::AWAITING_DHE) + { + throw new RequestException("DHE exchange required", 400); + } + + // If the session is not active, we can't serve these requests + if($session->getState() !== SessionState::ACTIVE) + { + throw new RequestException("Session is not active", 400); + } + + // Attempt to decrypt the content and verify the signature of the request + try + { + $decrypted = Cryptography::decryptTransport($this->requestBody, $session->getEncryptionKey()); + + if(!$this->verifySignature($decrypted)) + { + throw new RequestException("Invalid request signature", 401); + } + } + catch (CryptographyException $e) + { + throw new RequestException("Failed to decrypt request body", 400, $e); + } + + // At this stage, all checks has passed; we can try parsing the RPC request + try + { + // Decode the request body + $body = Utilities::jsonDecode($decrypted); + } + catch(InvalidArgumentException $e) + { + throw new RequestException("Invalid JSON in request body: " . $e->getMessage(), 400, $e); + } + + // If the body only contains a method, we assume it's a single request + if(isset($body['method'])) + { + return [$this->parseRequest($body)]; + } + + // Otherwise, we assume it's an array of requests + return array_map(fn($request) => $this->parseRequest($request), $body); } - catch(CryptographyException $e) + + /** + * Parses the raw request data into an RpcRequest object + * + * @param array $data The raw request data + * @return RpcRequest The parsed RpcRequest object + * @throws RequestException If the request is invalid + */ + private function parseRequest(array $data): RpcRequest { - return false; + if(!isset($data['method'])) + { + throw new RequestException("Missing 'method' key in request", 400); + } + + if(isset($data['id'])) + { + if(!is_string($data['id'])) + { + throw new RequestException("Invalid 'id' key in request: Expected string", 400); + } + + if(strlen($data['id']) === 0) + { + throw new RequestException("Invalid 'id' key in request: Expected non-empty string", 400); + } + + if(strlen($data['id']) > 8) + { + throw new RequestException("Invalid 'id' key in request: Expected string of length <= 8", 400); + } + } + + if(isset($data['parameters'])) + { + if(!is_array($data['parameters'])) + { + throw new RequestException("Invalid 'parameters' key in request: Expected array", 400); + } + } + + return new RpcRequest($data['method'], $data['id'] ?? null, $data['parameters'] ?? null); } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/Socialbox/Objects/ClientRequestOld.php b/src/Socialbox/Objects/ClientRequestOld.php new file mode 100644 index 0000000..036a26b --- /dev/null +++ b/src/Socialbox/Objects/ClientRequestOld.php @@ -0,0 +1,162 @@ +headers = $headers; + $this->requests = $requests; + $this->requestHash = $requestHash; + } + + /** + * @return array + */ + public function getHeaders(): array + { + return $this->headers; + } + + /** + * @return RpcRequest[] + */ + public function getRequests(): array + { + return $this->requests; + } + + public function getHash(): string + { + return $this->requestHash; + } + + public function getClientName(): string + { + return $this->headers[StandardHeaders::CLIENT_NAME->value]; + } + + public function getClientVersion(): string + { + return $this->headers[StandardHeaders::CLIENT_VERSION->value]; + } + + public function getSessionUuid(): ?string + { + if(!isset($this->headers[StandardHeaders::SESSION_UUID->value])) + { + return null; + } + + return $this->headers[StandardHeaders::SESSION_UUID->value]; + } + + public function getFromPeer(): ?PeerAddress + { + if(!isset($this->headers[StandardHeaders::FROM_PEER->value])) + { + return null; + } + + return PeerAddress::fromAddress($this->headers[StandardHeaders::FROM_PEER->value]); + } + + public function getSignature(): ?string + { + if(!isset($this->headers[StandardHeaders::SIGNATURE->value])) + { + return null; + } + + return $this->headers[StandardHeaders::SIGNATURE->value]; + } + + public function validateSession(): void + { + if($this->getSessionUuid() == null) + { + throw new StandardException(StandardError::SESSION_REQUIRED->getMessage(), StandardError::SESSION_REQUIRED); + } + + $session = SessionManager::getSession($this->getSessionUuid()); + + switch($session->getState()) + { + case SessionState::AWAITING_DHE: + throw new StandardException(StandardError::SESSION_DHE_REQUIRED->getMessage(), StandardError::SESSION_DHE_REQUIRED); + + case SessionState::EXPIRED: + throw new StandardException(StandardError::SESSION_EXPIRED->getMessage(), StandardError::SESSION_EXPIRED); + } + } + + /** + * @return bool + * @throws DatabaseOperationException + */ + public function verifySignature(): bool + { + $signature = $this->getSignature(); + $sessionUuid = $this->getSessionUuid(); + + if($signature == null || $sessionUuid == null) + { + return false; + } + + try + { + $session = SessionManager::getSession($sessionUuid); + } + catch(StandardException $e) + { + if($e->getStandardError() == StandardError::SESSION_NOT_FOUND) + { + return false; + } + + throw new RuntimeException($e); + } + + try + { + return Cryptography::verifyContent($this->getHash(), $signature, $session->getPublicKey()); + } + catch(CryptographyException $e) + { + return false; + } + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/SessionRecord.php b/src/Socialbox/Objects/Database/SessionRecord.php index ddce0cc..6f654a5 100644 --- a/src/Socialbox/Objects/Database/SessionRecord.php +++ b/src/Socialbox/Objects/Database/SessionRecord.php @@ -3,7 +3,6 @@ namespace Socialbox\Objects\Database; use DateTime; - use Socialbox\Classes\Utilities; use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\SessionState; use Socialbox\Interfaces\SerializableInterface; @@ -15,6 +14,7 @@ private bool $authenticated; private string $publicKey; private SessionState $state; + private ?string $encryptionKey; /** * @var SessionFlags[] */ @@ -40,7 +40,8 @@ $this->publicKey = $data['public_key']; $this->created = $data['created']; $this->lastRequest = $data['last_request']; - $this->flags = Utilities::unserializeList($data['flags']); + $this->encryptionKey = $data['encryption_key'] ?? null; + $this->flags = SessionFlags::fromString($data['flags']); if(SessionState::tryFrom($data['state']) == null) { @@ -108,6 +109,16 @@ return $this->state; } + /** + * Retrieves the encryption key associated with the instance. + * + * @return string|null Returns the encryption key as a string. + */ + public function getEncryptionKey(): ?string + { + return $this->encryptionKey; + } + /** * Retrieves the creation date and time of the object. * @@ -163,7 +174,7 @@ 'authenticated' => $this->authenticated, 'public_key' => $this->publicKey, 'state' => $this->state->value, - 'flags' => Utilities::serializeList($this->flags), + 'flags' => SessionFlags::toString($this->flags), 'created' => $this->created, 'last_request' => $this->lastRequest, ]; diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index d70ca08..f0ba517 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -3,40 +3,283 @@ namespace Socialbox; use Exception; + use InvalidArgumentException; use Socialbox\Classes\Configuration; + use Socialbox\Classes\Cryptography; use Socialbox\Classes\Logger; - use Socialbox\Classes\RpcHandler; use Socialbox\Classes\Utilities; + use Socialbox\Classes\Validator; + use Socialbox\Enums\SessionState; use Socialbox\Enums\StandardError; + use Socialbox\Enums\StandardHeaders; use Socialbox\Enums\StandardMethods; - use Socialbox\Exceptions\RpcException; + use Socialbox\Enums\Types\RequestType; + use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\RequestException; use Socialbox\Exceptions\StandardException; + use Socialbox\Managers\RegisteredPeerManager; + use Socialbox\Managers\SessionManager; + use Socialbox\Objects\ClientRequest; + use Socialbox\Objects\PeerAddress; class Socialbox { /** - * Handles the RPC (Remote Procedure Call) requests by parsing the client request, - * executing the appropriate methods, and returning the responses. + * Handles incoming client requests by validating required headers and processing + * the request based on its type. The method ensures proper handling of + * specific request types like RPC, session initiation, and DHE exchange, + * while returning an appropriate HTTP response for invalid or missing data. * * @return void */ - public static function handleRpc(): void + public static function handleRequest(): void { - try + $requestHeaders = Utilities::getRequestHeaders(); + + if(!isset($requestHeaders[StandardHeaders::REQUEST_TYPE->value])) { - $clientRequest = RpcHandler::getClientRequest(); - } - catch(RpcException $e) - { - Logger::getLogger()->error('Failed to parse the client request', $e); - http_response_code($e->getCode()); + http_response_code(400); + print('Missing required header: ' . StandardHeaders::REQUEST_TYPE->value); return; } - Logger::getLogger()->verbose(sprintf('Received %d RPC request(s) from %s', count($clientRequest->getRequests()), $_SERVER['REMOTE_ADDR'])); + if(!isset($requestHeaders[StandardHeaders::CLIENT_NAME->value])) + { + http_response_code(400); + print('Missing required header: ' . StandardHeaders::CLIENT_NAME->value); + return; + } + + if(!isset($requestHeaders[StandardHeaders::CLIENT_VERSION->value])) + { + http_response_code(400); + print('Missing required header: ' . StandardHeaders::CLIENT_VERSION->value); + return; + } + + $clientRequest = new ClientRequest($requestHeaders, file_get_contents('php://input') ?? null); + + // Handle the request type, only `init` and `dhe` are not encrypted using the session's encrypted key + // RPC Requests must be encrypted and signed by the client, vice versa for server responses. + switch(RequestType::tryFrom($clientRequest->getHeader(StandardHeaders::REQUEST_TYPE))) + { + case RequestType::INITIATE_SESSION: + self::handleInitiateSession($clientRequest); + break; + + case RequestType::DHE_EXCHANGE: + self::handleDheExchange($clientRequest); + break; + + case RequestType::RPC: + self::handleRpc($clientRequest); + break; + + default: + http_response_code(400); + print('Invalid Request-Type header'); + break; + } + } + + /** + * Processes a client request to initiate a session. Validates required headers, + * ensures the peer is authorized and enabled, and creates a new session UUID + * if all checks pass. Handles edge cases like missing headers, invalid inputs, + * or unauthorized peers. + * + * @param ClientRequest $clientRequest The request from the client containing + * the required headers and information. + * @return void + */ + private static function handleInitiateSession(ClientRequest $clientRequest): void + { + if(!$clientRequest->headerExists(StandardHeaders::PUBLIC_KEY)) + { + http_response_code(400); + print('Missing required header: ' . StandardHeaders::PUBLIC_KEY->value); + return; + } + + if(!$clientRequest->headerExists(StandardHeaders::IDENTIFY_AS)) + { + http_response_code(400); + print('Missing required header: ' . StandardHeaders::IDENTIFY_AS->value); + return; + } + + if(!Validator::validatePeerAddress($clientRequest->getHeader(StandardHeaders::IDENTIFY_AS))) + { + http_response_code(400); + print('Invalid Identify-As header: ' . $clientRequest->getHeader(StandardHeaders::IDENTIFY_AS)); + return; + } + + // TODO: Check if the peer address points to the domain of this server, if not we can't accept the request + + try + { + $registeredPeer = RegisteredPeerManager::getPeerByUsername($clientRequest->getIdentifyAs()->getUsername()); + + // If the peer is registered, check if it is enabled + if($registeredPeer !== null && !$registeredPeer->isEnabled()) + { + // Refuse to create a session if the peer is disabled/banned + // This also prevents multiple sessions from being created for the same peer + // A cron job should be used to clean up disabled peers + http_response_code(403); + print('Unauthorized: The requested peer is disabled/banned'); + return; + } + else + { + // Check if registration is enabled + if(!Configuration::getRegistrationConfiguration()->isRegistrationEnabled()) + { + http_response_code(403); + print('Unauthorized: Registration is disabled'); + return; + } + + // Register the peer if it is not already registered + $peerUuid = RegisteredPeerManager::createPeer(PeerAddress::fromAddress($clientRequest->getHeader(StandardHeaders::IDENTIFY_AS))->getUsername()); + // Retrieve the peer object + $registeredPeer = RegisteredPeerManager::getPeer($peerUuid); + } + + // Create the session UUID + $sessionUuid = SessionManager::createSession($clientRequest->getHeader(StandardHeaders::PUBLIC_KEY), $registeredPeer); + http_response_code(201); // Created + print($sessionUuid); // Return the session UUID + } + catch(InvalidArgumentException $e) + { + http_response_code(412); // Precondition failed + print($e->getMessage()); // Why the request failed + } + catch(Exception $e) + { + Logger::getLogger()->error('An internal error occurred while initiating the session', $e); + http_response_code(500); // Internal server error + if(Configuration::getSecurityConfiguration()->isDisplayInternalExceptions()) + { + print(Utilities::throwableToString($e)); + } + else + { + print('An internal error occurred'); + } + } + } + + /** + * Handles the Diffie-Hellman key exchange by decrypting the encrypted key passed on from the client using + * the server's private key and setting the encryption key to the session. + * + * 412: Headers malformed + * 400: Bad request + * 500: Internal server error + * 204: Success, no content. + * + * @param ClientRequest $clientRequest + * @return void + */ + private static function handleDheExchange(ClientRequest $clientRequest): void + { + // Check if the session UUID is set in the headers + if(!$clientRequest->headerExists(StandardHeaders::SESSION_UUID)) + { + Logger::getLogger()->verbose('Missing required header: ' . StandardHeaders::SESSION_UUID->value); + + http_response_code(412); + print('Missing required header: ' . StandardHeaders::SESSION_UUID->value); + return; + } + + // Check if the request body is empty + if(empty($clientRequest->getRequestBody())) + { + Logger::getLogger()->verbose('Bad request: The key exchange request body is empty'); + + http_response_code(400); + print('Bad request: The key exchange request body is empty'); + return; + } + + // Check if the session is awaiting a DHE exchange + if($clientRequest->getSession()->getState() !== SessionState::AWAITING_DHE) + { + Logger::getLogger()->verbose('Bad request: The session is not awaiting a DHE exchange'); + + http_response_code(400); + print('Bad request: The session is not awaiting a DHE exchange'); + return; + } + + try + { + // Attempt to decrypt the encrypted key passed on from the client + $encryptionKey = Cryptography::decryptContent($clientRequest->getRequestBody(), Configuration::getInstanceConfiguration()->getPrivateKey()); + } + catch (Exceptions\CryptographyException $e) + { + Logger::getLogger()->error(sprintf('Bad Request: Failed to decrypt the key for session %s', $clientRequest->getSessionUuid()), $e); + + http_response_code(400); + print('Bad Request: Cryptography error, make sure you have encrypted the key using the server\'s public key; ' . $e->getMessage()); + return; + } + + try + { + // Finally set the encryption key to the session + SessionManager::setEncryptionKey($clientRequest->getSessionUuid(), $encryptionKey); + } + catch (DatabaseOperationException $e) + { + Logger::getLogger()->error('Failed to set the encryption key for the session', $e); + http_response_code(500); + + if(Configuration::getSecurityConfiguration()->isDisplayInternalExceptions()) + { + print(Utilities::throwableToString($e)); + } + else + { + print('Internal Server Error: Failed to set the encryption key for the session'); + } + + return; + } + + Logger::getLogger()->info(sprintf('DHE exchange completed for session %s', $clientRequest->getSessionUuid())); + http_response_code(204); // Success, no content + } + + /** + * Handles incoming RPC requests from a client, processes each request, + * and returns the appropriate response(s) or error(s). + * + * @param ClientRequest $clientRequest The client's request containing one or multiple RPC calls. + * @return void + */ + private static function handleRpc(ClientRequest $clientRequest): void + { + try + { + $clientRequests = $clientRequest->getRpcRequests(); + } + catch (RequestException $e) + { + http_response_code($e->getCode()); + print($e->getMessage()); + return; + } + + Logger::getLogger()->verbose(sprintf('Received %d RPC request(s) from %s', count($clientRequests), $_SERVER['REMOTE_ADDR'])); $results = []; - foreach($clientRequest->getRequests() as $rpcRequest) + foreach($clientRequests as $rpcRequest) { $method = StandardMethods::tryFrom($rpcRequest->getMethod()); @@ -61,7 +304,7 @@ catch(Exception $e) { Logger::getLogger()->error('An internal error occurred while processing the RPC request', $e); - if(Configuration::getConfiguration()['security']['display_internal_exceptions']) + if(Configuration::getSecurityConfiguration()->isDisplayInternalExceptions()) { $response = $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, Utilities::throwableToString($e)); } @@ -79,21 +322,43 @@ } } + $response = null; + if(count($results) == 0) { - Logger::getLogger()->verbose('No results to return'); + $response = null; + } + elseif(count($results) == 1) + { + $response = json_encode($results[0], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + else + { + $response = json_encode($results, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + + if($response === null) + { http_response_code(204); return; } - if(count($results) == 1) + try { - Logger::getLogger()->verbose('Returning single result'); - print(json_encode($results[0])); + $encryptedResponse = Cryptography::encryptTransport($response, $clientRequest->getSession()->getEncryptionKey()); + $signature = Cryptography::signContent($response, Configuration::getInstanceConfiguration()->getPrivateKey(), true); + } + catch (Exceptions\CryptographyException $e) + { + Logger::getLogger()->error('Failed to encrypt the response', $e); + http_response_code(500); + print('Internal Server Error: Failed to encrypt the response'); return; } - Logger::getLogger()->verbose('Returning multiple results'); - print(json_encode($results)); + http_response_code(200); + header('Content-Type: application/octet-stream'); + header(StandardHeaders::SIGNATURE->value . ': ' . $signature); + print($encryptedResponse); } } \ No newline at end of file