diff --git a/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionCreateChannel.php b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionCreateChannel.php new file mode 100644 index 0000000..9f5b7af --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionCreateChannel.php @@ -0,0 +1,263 @@ +isExternal()) + { + return self::handleExternal($request, $rpcRequest); + } + + return self::handleInternal($request, $rpcRequest); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('An error occurred while checking the request type', StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + /** + * @param ClientRequest $request + * @param RpcRequest $rpcRequest + * @return SerializableInterface|null + * @throws StandardRpcException + */ + private static function handleInternal(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface + { + if(!$rpcRequest->containsParameter('receiving_peer')) + { + throw new MissingRpcArgumentException('receiving_peer'); + } + elseif(!Validator::validatePeerAddress($rpcRequest->getParameter('receiving_peer'))) + { + throw new InvalidRpcArgumentException('receiving_peer', 'Invalid Receiving Peer Address'); + } + + if(!$rpcRequest->containsParameter('public_encryption_key')) + { + throw new MissingRpcArgumentException('public_encryption_key'); + } + elseif(!Cryptography::validatePublicEncryptionKey($rpcRequest->getParameter('public_encryption_key'))) + { + throw new InvalidRpcArgumentException('public_encryption_key', 'The given public encryption key is invalid'); + } + + $receivingPeerAddress = PeerAddress::fromAddress($rpcRequest->getParameter('receiving_peer')); + Socialbox::resolvePeer($receivingPeerAddress); + + try + { + $callingPeer = $request->getPeer(); + $callingPeerAddress = PeerAddress::fromAddress($callingPeer->getAddress()); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('There was an error while trying to obtain the calling peer', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + try + { + $uuid = EncryptionChannelManager::createChannel( + callingPeer: $callingPeerAddress, + receivingPeer: $receivingPeerAddress, + callingPublicEncryptionKey: $rpcRequest->getParameter('public_encryption_ke') + ); + } + catch(InvalidArgumentException $e) + { + throw new InvalidRpcArgumentException(null, $e); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('There was an error while trying to create a new encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // If the receiver is in an external server, we must notify the external server as a client + if($receivingPeerAddress->isExternal()) + { + // Obtain the RPC Client, if for any reason it fails; we set the encryption channel as declined. + try + { + $rpcClient = Socialbox::getExternalSession($receivingPeerAddress->getDomain()); + $externalUuid = $rpcClient->encryptionCreateChannel( + receivingPeer: $receivingPeerAddress, + publicEncryptionKey: $rpcRequest->getParameter('public_encryption_key'), + channelUuid: $uuid, + identifiedAs: $callingPeerAddress + ); + } + catch(Exception $e) + { + try + { + EncryptionChannelManager::declineChannel($uuid, true); + } + catch(DatabaseOperationException $e) + { + Logger::getLogger()->error('Error declining channel as server', $e); + } + + if($e instanceof RpcException) + { + throw StandardRpcException::fromRpcException($e); + } + + throw new StandardRpcException('There was an error while trying to notify the external server of the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Check for sanity reasons + if($externalUuid !== $uuid) + { + try + { + EncryptionChannelManager::declineChannel($uuid, true); + } + catch(DatabaseOperationException $e) + { + Logger::getLogger()->error('Error declining channel as server', $e); + } + + throw new StandardRpcException('The external server did not return the correct UUID', StandardError::UUID_MISMATCH); + } + } + + return null; + } + + /** + * @param ClientRequest $request + * @param RpcRequest $rpcRequest + * @return SerializableInterface|null + * @throws StandardRpcException + */ + private static function handleExternal(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface + { + if($request->getIdentifyAs() === null) + { + return $rpcRequest->produceError(StandardError::BAD_REQUEST, 'Missing IdentifyAs request header'); + } + + $callingPeer = $request->getIdentifyAs(); + Socialbox::resolvePeer($callingPeer); + + if(!$rpcRequest->containsParameter('receiving_peer')) + { + throw new MissingRpcArgumentException('receiving_peer'); + } + elseif(!Validator::validatePeerAddress($rpcRequest->getParameter('receiving_peer'))) + { + throw new InvalidRpcArgumentException('receiving_peer', 'Invalid Receiving Peer Address'); + } + + if(!$rpcRequest->containsParameter('public_encryption_key')) + { + throw new MissingRpcArgumentException('public_encryption_key'); + } + elseif(!Cryptography::validatePublicEncryptionKey($rpcRequest->getParameter('public_encryption_key'))) + { + throw new InvalidRpcArgumentException('public_encryption_key', 'The given public encryption key is invalid'); + } + + // Check for an additional required parameter 'channel_uuid' + if(!$rpcRequest->containsParameter('channel_uuid')) + { + throw new MissingRpcArgumentException('channel_uuid'); + } + elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid'))) + { + throw new InvalidRpcArgumentException('channel_uuid', 'The given UUID is not a valid UUID v4 format'); + } + + // Check if the UUID already is used on this server + try + { + if(EncryptionChannelManager::channelUuidExists($rpcRequest->getParameter('channel_uuid'))) + { + return $rpcRequest->produceError(StandardError::UUID_CONFLICT, 'The given UUID already exists with another existing encryption channel on this server'); + } + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('There was an error while checking the existence of the channel UUID', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + $receivingPeerAddress = PeerAddress::fromAddress($rpcRequest->getParameter('receiving_peer')); + if($receivingPeerAddress->isExternal()) + { + return $rpcRequest->produceError(StandardError::PEER_NOT_FOUND, 'The receiving peer does not belong to this server'); + } + + try + { + $receivingPeer = RegisteredPeerManager::getPeerByAddress($rpcRequest->getParameter('receiving_peer')); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('There was an error while trying to obtain the receiving peer', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($receivingPeer === null) + { + return $rpcRequest->produceError(StandardError::PEER_NOT_FOUND, 'The receiving peer does not exist on this server'); + } + + try + { + $uuid = EncryptionChannelManager::createChannel( + callingPeer: $callingPeer, + receivingPeer: $receivingPeerAddress, + callingPublicEncryptionKey: $rpcRequest->getParameter('public_encryption_key'), + channelUUid: $rpcRequest->getParameter('channel_uuid') + ); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('There was an error while trying to create the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($uuid !== $rpcRequest->getParameter('channel_uuid')) + { + try + { + EncryptionChannelManager::declineChannel($rpcRequest->getParameter('channel_uuid'), true); + } + catch(DatabaseOperationException $e) + { + Logger::getLogger()->error('There was an error while trying to decline the encryption channel as a server', $e); + } + + return $rpcRequest->produceError(StandardError::UUID_MISMATCH, 'The created UUID in the server does not match the UUID that was received'); + } + + return $rpcRequest->produceResponse($uuid); + } + } \ No newline at end of file diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index b07adaf..66eb290 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -18,9 +18,6 @@ use Socialbox\Classes\StandardMethods\Core\ResolvePeer; use Socialbox\Classes\StandardMethods\Core\ResolveSignature; use Socialbox\Classes\StandardMethods\Core\VerifySignature; - use Socialbox\Classes\StandardMethods\Encryption\EncryptionAcceptChannel; - use Socialbox\Classes\StandardMethods\Encryption\EncryptionCloseChannel; - use Socialbox\Classes\StandardMethods\Encryption\EncryptionCreateChannel; use Socialbox\Classes\StandardMethods\ServerDocuments\AcceptCommunityGuidelines; use Socialbox\Classes\StandardMethods\ServerDocuments\AcceptPrivacyPolicy; use Socialbox\Classes\StandardMethods\ServerDocuments\AcceptTermsOfService; @@ -77,8 +74,17 @@ case GET_SESSION_STATE = 'getSessionState'; case PING = 'ping'; case RESOLVE_PEER = 'resolvePeer'; - case RESOLVE_PEER_SIGNATURE = 'resolvePeerSignature'; - case VERIFY_PEER_SIGNATURE = 'verifyPeerSignature'; + case RESOLVE_SIGNATURE = 'resolveSignature'; + case VERIFY_SIGNATURE = 'verifySignature'; + + // Encryption Channel Methods + case ENCRYPTION_ACCEPT_CHANNEL = 'encryptionAcceptChannel'; + case ENCRYPTION_CHANNEL_EXISTS = 'encryptionChannelExists'; + case ENCRYPTION_CHANNEL_SEND = 'encryptionChannelSend'; + case ENCRYPTION_CLOSE_CHANNEL = 'encryptionCloseChannel'; + case ENCRYPTION_CREATE_CHANNEL = 'encryptionCreateChannel'; + case ENCRYPTION_DECLINE_CHANNEL = 'encryptionDeclineChannel'; + case ENCRYPTION_GET_CHANNEL = 'encryptionGetChannel'; // ServerDocument Methods case ACCEPT_COMMUNITY_GUIDELINES = 'acceptCommunityGuidelines'; @@ -184,8 +190,8 @@ self::GET_SESSION_STATE => GetSessionState::execute($request, $rpcRequest), self::PING => Ping::execute($request, $rpcRequest), self::RESOLVE_PEER => ResolvePeer::execute($request, $rpcRequest), - self::RESOLVE_PEER_SIGNATURE => ResolveSignature::execute($request, $rpcRequest), - self::VERIFY_PEER_SIGNATURE => VerifySignature::execute($request, $rpcRequest), + self::RESOLVE_SIGNATURE => ResolveSignature::execute($request, $rpcRequest), + self::VERIFY_SIGNATURE => VerifySignature::execute($request, $rpcRequest), // Server Document Methods self::ACCEPT_PRIVACY_POLICY => AcceptPrivacyPolicy::execute($request, $rpcRequest), @@ -457,8 +463,8 @@ self::GET_SESSION_STATE, self::PING, self::RESOLVE_PEER, - self::RESOLVE_PEER_SIGNATURE, - self::VERIFY_PEER_SIGNATURE + self::RESOLVE_SIGNATURE, + self::VERIFY_SIGNATURE ]; } diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index 516ba87..45474ec 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -16,12 +16,14 @@ use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\ResolutionException; use Socialbox\Exceptions\RpcException; + use Socialbox\Objects\Client\EncryptionChannelInstance; use Socialbox\Objects\Client\EncryptionChannelSecret; use Socialbox\Objects\Client\ExportedSession; use Socialbox\Objects\Client\SignatureKeyPair; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\RpcRequest; use Socialbox\Objects\Standard\Contact; + use Socialbox\Objects\Standard\EncryptionChannel; use Socialbox\Objects\Standard\ImageCaptchaVerification; use Socialbox\Objects\Standard\InformationFieldState; use Socialbox\Objects\Standard\Peer; @@ -76,6 +78,113 @@ return $uuid; } + /** + * Creates a new encryption channel with the given peer, generates a new encryption key pair and sends the public + * key to the receiving peer. The private key is stored locally and is never sent to the server. + * + * @param PeerAddress|string $receivingPeer The address of the peer to create the channel with + * @return string The UUID of the encryption channel + * @throws CryptographyException Thrown if there was an error while generating the encryption key pair + */ + public function newEncryptionChannel(string|PeerAddress $receivingPeer): string + { + if($receivingPeer instanceof PeerAddress) + { + $receivingPeer = $receivingPeer->getAddress(); + } + + $encryptionKeyPair = Cryptography::generateEncryptionKeyPair(); + $encryptionChannelUuid = $this->encryptionCreateChannel($receivingPeer, $encryptionKeyPair->getPublicKey()); + $this->addEncryptionChannelSecret(new EncryptionChannelSecret([ + 'channel_uuid' => $encryptionChannelUuid, + 'receiver' => $receivingPeer, + 'local_public_encryption_key' => $encryptionKeyPair->getPublicKey(), + 'local_private_encryption_key' => $encryptionKeyPair->getPrivateKey() + ])); + + return $encryptionChannelUuid; + } + + /** + * Waits for the encryption channel to be accepted by the receiving peer, returns True if the channel was accepted + * or False if the channel was not accepted within the timeout period. + * + * @param string $channelUuid + * @param int|null $timeout + * @return bool + */ + public function waitForEncryptionChannel(string $channelUuid, ?int $timeout=30): bool + { + if($this->getEncryptionChannelSecret($channelUuid) === null) + { + throw new InvalidArgumentException('Encryption Channel was not created with newEncryptionChannel() or defined with addEncryptionChannelSecret()'); + } + + $start = time(); + while(true) + { + if($timeout !== null && time() - $start > $timeout) + { + break; + } + + $encryptionChannel = $this->encryptionGetChannel($channelUuid); + if($encryptionChannel->getReceivingPublicEncryptionKey() !== null) + { + $this->getEncryptionChannelSecret($channelUuid)->setReceivingPublicEncryptionKey($encryptionChannel->getReceivingPublicEncryptionKey()); + return true; + } + + sleep(1); + } + + return false; + } + + /** + * Accepts an encryption channel with the given UUID, generates a new encryption key pair and sends the public key + * to the calling peer. The private key is stored locally and is never sent to the server. + * + * @param string $channelUuid The UUID of the encryption channel to accept + * @return bool Returns True if the channel was accepted + * @throws CryptographyException Thrown if there was an error while generating the encryption key pair + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function acceptEncryptionChannel(string $channelUuid): bool + { + $encryptionChannel = $this->encryptionGetChannel($channelUuid); + $encryptionKeyPair = Cryptography::generateEncryptionKeyPair(); + $this->encryptionAcceptChannel($channelUuid, $encryptionKeyPair->getPublicKey(), $encryptionChannel->getRecipient()); + + $this->addEncryptionChannelSecret(new EncryptionChannelSecret([ + 'channel_uuid' => $channelUuid, + 'receiver' => $encryptionChannel->getCallingPeer(), + 'local_public_encryption_key' => $encryptionKeyPair->getPublicKey(), + 'local_private_encryption_key' => $encryptionKeyPair->getPrivateKey(), + 'receiving_public_encryption_key' => $encryptionChannel->getCallingPublicEncryptionKey() + ])); + + return true; + } + + /** + * Creates a new EncryptionChannelInstance object for the given channel UUID. + * + * @param string $channelUuid The UUID of the encryption channel + * @return EncryptionChannelInstance The EncryptionChannelInstance object + * @throws InvalidArgumentException Thrown if the encryption channel secret does not exist + */ + public function createEncryptionChannelInstance(string $channelUuid): EncryptionChannelInstance + { + if($this->getEncryptionChannelSecret($channelUuid) === null) + { + throw new InvalidArgumentException('Encryption Channel was not created with newEncryptionChannel() or defined with addEncryptionChannelSecret()'); + } + + $encryptionChannelSecret = $this->getEncryptionChannelSecret($channelUuid); + return new EncryptionChannelInstance($this, $encryptionChannelSecret); + } + /** * Adds a new peer to the AddressBook, returns True upon success or False if the contact already exists in * the address book. @@ -356,7 +465,7 @@ * @return Signature|null The signature as a Signature object, or null if the signature does not exist * @throws RpcException Thrown if there was an error with the RPC request */ - public function resolvePeerSignature(PeerAddress|string $peer, string $signatureUuid): ?Signature + public function resolveSignature(PeerAddress|string $peer, string $signatureUuid): ?Signature { if($peer instanceof PeerAddress) { @@ -364,7 +473,7 @@ } $result = $this->sendRequest( - new RpcRequest(StandardMethods::RESOLVE_PEER_SIGNATURE, parameters: [ + new RpcRequest(StandardMethods::RESOLVE_SIGNATURE, parameters: [ 'peer' => $peer, 'signature_uuid' => $signatureUuid ]) @@ -381,7 +490,7 @@ /** * Verifies signature authenticity by resolving the signature UUID and comparing the given parameters with the * signature data, returns True if the signature is verified. This is a decentralized method, meaning that any - * signature UUID can be verified for as longas the $peer parameter is the address of the peer that created the + * signature UUID can be verified for as long as the $peer parameter is the address of the peer that created the * signature. * * @param PeerAddress|string $peer The address of the peer to verify the signature for @@ -393,7 +502,7 @@ * @return SignatureVerificationStatus the status of the verification * @throws RpcException Thrown if there was an error with the RPC request */ - public function verifyPeerSignature(PeerAddress|string $peer, string $signatureUuid, string $signaturePublicKey, string $signature, string $sha512, ?int $signatureTime=null): SignatureVerificationStatus + public function verifySignature(PeerAddress|string $peer, string $signatureUuid, string $signaturePublicKey, string $signature, string $sha512, ?int $signatureTime=null): SignatureVerificationStatus { if($peer instanceof PeerAddress) { @@ -401,15 +510,115 @@ } return SignatureVerificationStatus::tryFrom($this->sendRequest( - new RpcRequest(StandardMethods::VERIFY_PEER_SIGNATURE, parameters: [ + new RpcRequest(StandardMethods::VERIFY_SIGNATURE, parameters: [ 'peer' => $peer, 'signature_uuid' => $signatureUuid, - 'signature_public_key' => $signaturePublicKey, 'signature' => $signature, 'sha512' => $sha512, 'signature_time' => $signatureTime ]) - )->getResponse()->getResult()) ?? SignatureVerificationStatus::INVALID; + )->getResponse()->getResult()) ?? SignatureVerificationStatus::ERROR; + } + + public function encryptionAcceptChannel(string $channelUuid, string $publicEncryptionKey, PeerAddress|string|null $identifiedAs=null): bool + { + + if($identifiedAs instanceof PeerAddress) + { + $identifiedAs = $identifiedAs->getAddress(); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::ENCRYPTION_ACCEPT_CHANNEL, parameters: [ + 'channel_uuid' => $channelUuid, + 'public_encryption_key' => $publicEncryptionKey + ]), true, $identifiedAs + )->getResponse()->getResult(); + } + + public function encryptionChannelExists(string $channelUuid): bool + { + return $this->sendRequest( + new RpcRequest(StandardMethods::ENCRYPTION_CHANNEL_EXISTS, parameters: [ + 'channel_uuid' => $channelUuid + ]) + )->getResponse()->getResult(); + } + + public function encryptionChannelSend(string $channelUuid, string $checksum, string $data, PeerAddress|string|null $identifiedAs=null, ?string $messageUuid=null, ?int $timestamp=null): string + { + if($identifiedAs instanceof PeerAddress) + { + $identifiedAs = $identifiedAs->getAddress(); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::ENCRYPTION_CHANNEL_SEND, parameters: [ + 'channel_uuid' => $channelUuid, + 'checksum' => $checksum, + 'data' => $data, + 'uuid' => $messageUuid, + 'timestamp' => $timestamp + ]), true, $identifiedAs + )->getResponse()->getResult(); + } + + public function encryptionCloseChannel(string $channelUuid, PeerAddress|string|null $identifiedAs=null): bool + { + if($identifiedAs instanceof PeerAddress) + { + $identifiedAs = $identifiedAs->getAddress(); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::ENCRYPTION_CLOSE_CHANNEL, parameters: [ + 'channel_uuid' => $channelUuid + ]), true, $identifiedAs + )->getResponse()->getResult(); + } + + public function encryptionCreateChannel(string|PeerAddress $receivingPeer, string $publicEncryptionKey, ?string $channelUuid=null, PeerAddress|string|null $identifiedAs=null): string + { + if($receivingPeer instanceof PeerAddress) + { + $receivingPeer = $receivingPeer->getAddress(); + } + + if($identifiedAs instanceof PeerAddress) + { + $identifiedAs = $identifiedAs->getAddress(); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::ENCRYPTION_CREATE_CHANNEL, parameters: [ + 'receiving_peer' => $receivingPeer, + 'public_encryption_key' => $publicEncryptionKey, + 'channel_uuid' => $channelUuid + ]), true, $identifiedAs + )->getResponse()->getResult(); + } + + public function encryptionDeclineChannel(string $channelUuid, PeerAddress|string|null $identifiedAs=null): bool + { + if($identifiedAs instanceof PeerAddress) + { + $identifiedAs = $identifiedAs->getAddress(); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::ENCRYPTION_DECLINE_CHANNEL, parameters: [ + 'channel_uuid' => $channelUuid + ]), true, $identifiedAs + )->getResponse()->getResult(); + } + + public function encryptionGetChannel(string $channelUuid): EncryptionChannel + { + return new EncryptionChannel($this->sendRequest( + new RpcRequest(StandardMethods::ENCRYPTION_GET_CHANNEL, parameters: [ + 'channel_uuid' => $channelUuid + ]) + )->getResponse()->getResult()); } /**