Made message signing in Cryptography use SHA512 as the message content for... #1

Closed
netkas wants to merge 421 commits from master into dev
44 changed files with 2971 additions and 2016 deletions
Showing only changes of commit 367399f0fd - Show all commits

View file

@ -2,7 +2,7 @@
<module type="WEB_MODULE" version="4"> <module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/.idea/dataSources" /> <excludeFolder url="file://$MODULE_DIR$/.idea/dataSources" />
<excludeFolder url="file://$MODULE_DIR$/build" /> <excludeFolder url="file://$MODULE_DIR$/build" />
</content> </content>

4
.idea/sqldialects.xml generated
View file

@ -2,14 +2,14 @@
<project version="4"> <project version="4">
<component name="SqlDialectMappings"> <component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/captcha_images.sql" dialect="MariaDB" /> <file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/captcha_images.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/external_sessions.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/registered_peers.sql" dialect="MariaDB" /> <file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/registered_peers.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/sessions.sql" dialect="MariaDB" /> <file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/sessions.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/variables.sql" dialect="MariaDB" /> <file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/variables.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/CaptchaManager.php" dialect="MariaDB" /> <file url="file://$PROJECT_DIR$/src/Socialbox/Managers/CaptchaManager.php" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/EncryptionRecordsManager.php" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/PasswordManager.php" dialect="MariaDB" /> <file url="file://$PROJECT_DIR$/src/Socialbox/Managers/PasswordManager.php" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/RegisteredPeerManager.php" dialect="MariaDB" /> <file url="file://$PROJECT_DIR$/src/Socialbox/Managers/RegisteredPeerManager.php" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/ResolvedServersManager.php" dialect="MariaDB" /> <file url="file://$PROJECT_DIR$/src/Socialbox/Managers/ResolvedDnsRecordsManager.php" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/SessionManager.php" dialect="MariaDB" /> <file url="file://$PROJECT_DIR$/src/Socialbox/Managers/SessionManager.php" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/VariableManager.php" dialect="MariaDB" /> <file url="file://$PROJECT_DIR$/src/Socialbox/Managers/VariableManager.php" dialect="MariaDB" />
</component> </component>

View file

@ -15,6 +15,7 @@
"ext-redis": "*", "ext-redis": "*",
"ext-memcached": "*", "ext-memcached": "*",
"ext-curl": "*", "ext-curl": "*",
"ext-gd": "*" "ext-gd": "*",
"ext-sodium": "*"
} }
} }

View file

@ -15,7 +15,7 @@ class DnsRecordCommand implements CliCommandInterface
{ {
$txt_record = sprintf('v=socialbox;sb-rpc=%s;sb-key=%s', $txt_record = sprintf('v=socialbox;sb-rpc=%s;sb-key=%s',
Configuration::getInstanceConfiguration()->getRpcEndpoint(), Configuration::getInstanceConfiguration()->getRpcEndpoint(),
Configuration::getInstanceConfiguration()->getPublicKey() Configuration::getCryptographyConfiguration()->getHostPublicKey()
); );
Logger::getLogger()->info('Please set the following DNS TXT record for the domain:'); Logger::getLogger()->info('Please set the following DNS TXT record for the domain:');
@ -33,7 +33,6 @@ Usage: socialbox dns-record
Displays the DNS TXT record that should be set for the domain. Displays the DNS TXT record that should be set for the domain.
HELP; HELP;
} }
/** /**

View file

@ -3,7 +3,6 @@
namespace Socialbox\Classes\CliCommands; namespace Socialbox\Classes\CliCommands;
use Exception; use Exception;
use LogLib\Log;
use PDOException; use PDOException;
use Socialbox\Abstracts\CacheLayer; use Socialbox\Abstracts\CacheLayer;
use Socialbox\Classes\Configuration; use Socialbox\Classes\Configuration;
@ -13,9 +12,8 @@
use Socialbox\Classes\Resources; use Socialbox\Classes\Resources;
use Socialbox\Enums\DatabaseObjects; use Socialbox\Enums\DatabaseObjects;
use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\CryptographyException;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Interfaces\CliCommandInterface; use Socialbox\Interfaces\CliCommandInterface;
use Socialbox\Managers\EncryptionRecordsManager; use Socialbox\Socialbox;
class InitializeCommand implements CliCommandInterface class InitializeCommand implements CliCommandInterface
{ {
@ -208,6 +206,7 @@
Logger::getLogger()->info('cache.database defaulting to 0'); Logger::getLogger()->info('cache.database defaulting to 0');
} }
Logger::getLogger()->info('Updating configuration...');
Configuration::getConfigurationLib()->save(); // Save Configuration::getConfigurationLib()->save(); // Save
Configuration::reload(); // Reload Configuration::reload(); // Reload
} }
@ -261,16 +260,17 @@
} }
if( if(
!Configuration::getInstanceConfiguration()->getPublicKey() || !Configuration::getCryptographyConfiguration()->getHostPublicKey() ||
!Configuration::getInstanceConfiguration()->getPrivateKey() || !Configuration::getCryptographyConfiguration()->getHostPrivateKey() ||
!Configuration::getInstanceConfiguration()->getEncryptionKeys() !Configuration::getCryptographyConfiguration()->getHostPublicKey()
) )
{ {
$expires = time() + 31536000;
try try
{ {
Logger::getLogger()->info('Generating new key pair...'); Logger::getLogger()->info('Generating new key pair (expires ' . date('Y-m-d H:i:s', $expires) . ')...');
$keyPair = Cryptography::generateKeyPair(); $signingKeyPair = Cryptography::generateSigningKeyPair();
$encryptionKeys = Cryptography::randomKeyS(230, 314, Configuration::getInstanceConfiguration()->getEncryptionKeysCount());
} }
catch (CryptographyException $e) catch (CryptographyException $e)
{ {
@ -278,40 +278,35 @@
return 1; return 1;
} }
Configuration::getConfigurationLib()->set('cryptography.host_keypair_expires', $expires);
Configuration::getConfigurationLib()->set('cryptography.host_private_key', $signingKeyPair->getPrivateKey());
Configuration::getConfigurationLib()->set('cryptography.host_public_key', $signingKeyPair->getPublicKey());
}
// If Internal Encryption keys are null or has less keys than configured, populate the configuration
// property with encryption keys.
if(
Configuration::getCryptographyConfiguration()->getInternalEncryptionKeys() === null ||
count(Configuration::getCryptographyConfiguration()->getInternalEncryptionKeys()) < Configuration::getCryptographyConfiguration()->getEncryptionKeysCount())
{
Logger::getLogger()->info('Generating internal encryption keys...');
$encryptionKeys = Configuration::getCryptographyConfiguration()->getInternalEncryptionKeys() ?? [];
while(count($encryptionKeys) < Configuration::getCryptographyConfiguration()->getEncryptionKeysCount())
{
$encryptionKeys[] = Cryptography::generateEncryptionKey(Configuration::getCryptographyConfiguration()->getEncryptionKeysAlgorithm());
}
Configuration::getConfigurationLib()->set('cryptography.internal_encryption_keys', $encryptionKeys);
}
Logger::getLogger()->info('Updating configuration...'); Logger::getLogger()->info('Updating configuration...');
Configuration::getConfigurationLib()->set('instance.private_key', $keyPair->getPrivateKey()); Configuration::getConfigurationLib()->save();;
Configuration::getConfigurationLib()->set('instance.public_key', $keyPair->getPublicKey()); Configuration::reload();
Configuration::getConfigurationLib()->set('instance.encryption_keys', $encryptionKeys);
Configuration::getConfigurationLib()->save(); // Save
Configuration::reload(); // Reload
Logger::getLogger()->info(sprintf('Set the DNS TXT record for the domain %s to the following value:', Configuration::getInstanceConfiguration()->getDomain()));
Logger::getLogger()->info(sprintf("v=socialbox;sb-rpc=%s;sb-key=%s;",
Configuration::getInstanceConfiguration()->getRpcEndpoint(), $keyPair->getPublicKey()
));
}
try
{
if(EncryptionRecordsManager::getRecordCount() < Configuration::getInstanceConfiguration()->getEncryptionRecordsCount())
{
Logger::getLogger()->info('Generating encryption records...');
EncryptionRecordsManager::generateRecords(Configuration::getInstanceConfiguration()->getEncryptionRecordsCount());
}
}
catch (CryptographyException $e)
{
Logger::getLogger()->error('Failed to generate encryption records due to a cryptography exception', $e);
return 1;
}
catch (DatabaseOperationException $e)
{
Logger::getLogger()->error('Failed to generate encryption records due to a database error', $e);
return 1;
}
// TODO: Create a host peer here?
Logger::getLogger()->info('Socialbox has been initialized successfully'); Logger::getLogger()->info('Socialbox has been initialized successfully');
Logger::getLogger()->info(sprintf('Set the DNS TXT record for the domain %s to the following value:', Configuration::getInstanceConfiguration()->getDomain()));
Logger::getLogger()->info(Socialbox::getDnsRecord());
if(getenv('SB_MODE') === 'automated') if(getenv('SB_MODE') === 'automated')
{ {
Configuration::getConfigurationLib()->set('instance.enabled', true); Configuration::getConfigurationLib()->set('instance.enabled', true);

View file

@ -1,117 +0,0 @@
<?php
namespace Socialbox\Classes\ClientCommands;
use Socialbox\Classes\Cryptography;
use Socialbox\Classes\Logger;
use Socialbox\Classes\Utilities;
use Socialbox\Exceptions\CryptographyException;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\ResolutionException;
use Socialbox\Exceptions\RpcException;
use Socialbox\Interfaces\CliCommandInterface;
use Socialbox\Objects\ClientSession;
use Socialbox\SocialClient;
class ConnectCommand implements CliCommandInterface
{
public static function execute(array $args): int
{
if(!isset($args['name']))
{
Logger::getLogger()->error('The name argument is required, this is the name of the session');
}
$workingDirectory = getcwd();
if(isset($args['directory']))
{
if(!is_dir($args['directory']))
{
Logger::getLogger()->error('The directory provided does not exist');
return 1;
}
$workingDirectory = $args['directory'];
}
$sessionFile = $workingDirectory . DIRECTORY_SEPARATOR . Utilities::sanitizeFileName($args['name']) . '.json';
if(!file_exists($sessionFile))
{
return self::createSession($args, $sessionFile);
}
Logger::getLogger()->info(sprintf('Session file already exists at %s', $sessionFile));
return 0;
}
private static function createSession(array $args, string $sessionFile): int
{
if(!isset($args['domain']))
{
Logger::getLogger()->error('The domain argument is required, this is the domain of the socialbox instance');
return 1;
}
try
{
$client = new SocialClient($args['domain']);
}
catch (DatabaseOperationException $e)
{
Logger::getLogger()->error('Failed to create the client session', $e);
return 1;
}
catch (ResolutionException $e)
{
Logger::getLogger()->error('Failed to resolve the domain', $e);
return 1;
}
try
{
$keyPair = Cryptography::generateKeyPair();
$session = $client->createSession($keyPair);
}
catch (CryptographyException | RpcException $e)
{
Logger::getLogger()->error('Failed to create the session', $e);
return 1;
}
$sessionData = new ClientSession([
'domain' => $args['domain'],
'session_uuid' => $session,
'public_key' => $keyPair->getPublicKey(),
'private_key' => $keyPair->getPrivateKey()
]);
$sessionData->save($sessionFile);
Logger::getLogger()->info(sprintf('Session created and saved to %s', $sessionFile));
return 0;
}
public static function getHelpMessage(): string
{
return <<<HELP
Usage: socialbox connect --name <name> --domain <domain> [--directory <directory>]
Creates a new session with the specified name and domain. The session will be saved to the current working directory by default, or to the specified directory if provided.
Options:
--name The name of the session to create.
--domain The domain of the socialbox instance.
--directory The directory where the session file should be saved.
Example:
socialbox connect --name mysession --domain socialbox.example.com
HELP;
}
public static function getShortHelpMessage(): string
{
return 'Connect Command - Creates a new session with the specified name and domain';
}
}

View file

@ -2,19 +2,21 @@
namespace Socialbox\Classes; namespace Socialbox\Classes;
use Socialbox\Classes\ClientCommands\StorageConfiguration;
use Socialbox\Classes\Configuration\CacheConfiguration; use Socialbox\Classes\Configuration\CacheConfiguration;
use Socialbox\Classes\Configuration\CryptographyConfiguration;
use Socialbox\Classes\Configuration\DatabaseConfiguration; use Socialbox\Classes\Configuration\DatabaseConfiguration;
use Socialbox\Classes\Configuration\InstanceConfiguration; use Socialbox\Classes\Configuration\InstanceConfiguration;
use Socialbox\Classes\Configuration\LoggingConfiguration; use Socialbox\Classes\Configuration\LoggingConfiguration;
use Socialbox\Classes\Configuration\RegistrationConfiguration; use Socialbox\Classes\Configuration\RegistrationConfiguration;
use Socialbox\Classes\Configuration\SecurityConfiguration; use Socialbox\Classes\Configuration\SecurityConfiguration;
use Socialbox\Classes\Configuration\StorageConfiguration;
class Configuration class Configuration
{ {
private static ?\ConfigLib\Configuration $configuration = null; private static ?\ConfigLib\Configuration $configuration = null;
private static ?InstanceConfiguration $instanceConfiguration = null; private static ?InstanceConfiguration $instanceConfiguration = null;
private static ?SecurityConfiguration $securityConfiguration = null; private static ?SecurityConfiguration $securityConfiguration = null;
private static ?CryptographyConfiguration $cryptographyConfiguration = null;
private static ?DatabaseConfiguration $databaseConfiguration = null; private static ?DatabaseConfiguration $databaseConfiguration = null;
private static ?LoggingConfiguration $loggingConfiguration = null; private static ?LoggingConfiguration $loggingConfiguration = null;
private static ?CacheConfiguration $cacheConfiguration = null; private static ?CacheConfiguration $cacheConfiguration = null;
@ -33,19 +35,47 @@
// Instance configuration // Instance configuration
$config->setDefault('instance.enabled', false); // False by default, requires the user to enable it. $config->setDefault('instance.enabled', false); // False by default, requires the user to enable it.
$config->setDefault('instance.name', "Socialbox Server");
$config->setDefault('instance.domain', null); $config->setDefault('instance.domain', null);
$config->setDefault('instance.rpc_endpoint', null); $config->setDefault('instance.rpc_endpoint', null);
$config->setDefault('instance.encryption_keys_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);
// Security Configuration // Security Configuration
$config->setDefault('security.display_internal_exceptions', false); $config->setDefault('security.display_internal_exceptions', false);
$config->setDefault('security.resolved_servers_ttl', 600); $config->setDefault('security.resolved_servers_ttl', 600);
$config->setDefault('security.captcha_ttl', 200); $config->setDefault('security.captcha_ttl', 200);
// Cryptography Configuration
// The Unix Timestamp for when the host's keypair should expire
// Setting this value to 0 means the keypair never expires
// Setting this value to null will automatically set the current unix timestamp + 1 year as the value
// This means at initialization, the key is automatically set to expire in a year.
$config->setDefault('cryptography.host_keypair_expires', null);
// The host's public/private keypair in base64 encoding, when null; the initialization process
// will automatically generate a new keypair
$config->setDefault('cryptography.host_public_key', null);
$config->setDefault('cryptography.host_private_key', null);
// The internal encryption keys used for encrypting data in the database when needed.
// When null, the initialization process will automatically generate a set of keys
// based on the `encryption_keys_count` and `encryption_keys_algorithm` configuration.
// This is an array of base64 encoded keys.
$config->setDefault('cryptography.internal_encryption_keys', null);
// The number of encryption keys to generate and set to `instance.encryption_keys` this will be used
// to randomly encrypt/decrypt sensitive data in the database, this includes hashes.
// The higher the number the higher performance impact it will have on the server
$config->setDefault('cryptography.encryption_keys_count', 10);
// The host's encryption algorithm, this will be used to generate a set of encryption keys
// This is for internal encryption, these keys are never shared outside this configuration.
// Recommendation: Higher security over performance
$config->setDefault('cryptography.encryption_keys_algorithm', 'xchacha20');
// The encryption algorithm to use for encrypted message transport between the client aand the server
// This is the encryption the server tells the client to use and the client must support it.
// Recommendation: Good balance between security and performance
// For universal support & performance, use aes256gcm for best performance or for best security use xchacha20
$config->setDefault('cryptography.transport_encryption_algorithm', 'chacha20');
// Database configuration // Database configuration
$config->setDefault('database.host', '127.0.0.1'); $config->setDefault('database.host', '127.0.0.1');
$config->setDefault('database.port', 3306); $config->setDefault('database.port', 3306);
@ -98,6 +128,7 @@
self::$configuration = $config; self::$configuration = $config;
self::$instanceConfiguration = new InstanceConfiguration(self::$configuration->getConfiguration()['instance']); self::$instanceConfiguration = new InstanceConfiguration(self::$configuration->getConfiguration()['instance']);
self::$securityConfiguration = new SecurityConfiguration(self::$configuration->getConfiguration()['security']); self::$securityConfiguration = new SecurityConfiguration(self::$configuration->getConfiguration()['security']);
self::$cryptographyConfiguration = new CryptographyConfiguration(self::$configuration->getConfiguration()['cryptography']);
self::$databaseConfiguration = new DatabaseConfiguration(self::$configuration->getConfiguration()['database']); self::$databaseConfiguration = new DatabaseConfiguration(self::$configuration->getConfiguration()['database']);
self::$loggingConfiguration = new LoggingConfiguration(self::$configuration->getConfiguration()['logging']); self::$loggingConfiguration = new LoggingConfiguration(self::$configuration->getConfiguration()['logging']);
self::$cacheConfiguration = new CacheConfiguration(self::$configuration->getConfiguration()['cache']); self::$cacheConfiguration = new CacheConfiguration(self::$configuration->getConfiguration()['cache']);
@ -140,6 +171,14 @@
return self::$configuration->getConfiguration(); return self::$configuration->getConfiguration();
} }
/**
* Retrieves the configuration library instance.
*
* This method returns the current Configuration instance from the ConfigLib namespace.
* If the configuration has not been initialized yet, it initializes it first.
*
* @return \ConfigLib\Configuration The configuration library instance.
*/
public static function getConfigurationLib(): \ConfigLib\Configuration public static function getConfigurationLib(): \ConfigLib\Configuration
{ {
if(self::$configuration === null) if(self::$configuration === null)
@ -180,6 +219,24 @@
return self::$securityConfiguration; return self::$securityConfiguration;
} }
/**
* Retrieves the cryptography configuration.
*
* This method returns the current CryptographyConfiguration instance.
* If the configuration has not been initialized yet, it initializes it first.
*
* @return CryptographyConfiguration|null The cryptography configuration instance or null if not available.
*/
public static function getCryptographyConfiguration(): ?CryptographyConfiguration
{
if(self::$cryptographyConfiguration === null)
{
self::initializeConfiguration();
}
return self::$cryptographyConfiguration;
}
/** /**
* Retrieves the current database configuration. * Retrieves the current database configuration.
* *

View file

@ -0,0 +1,111 @@
<?php
namespace Socialbox\Classes\Configuration;
class CryptographyConfiguration
{
private ?int $hostKeyPairExpires;
private ?string $hostPublicKey;
private ?string $hostPrivateKey;
private ?array $internalEncryptionKeys;
private int $encryptionKeysCount;
private string $encryptionKeysAlgorithm;
private string $transportEncryptionAlgorithm;
/**
* Constructor to initialize encryption and transport keys from provided data.
*
* @param array $data An associative array containing key-value pairs for encryption keys, algorithms, and expiration settings.
* @return void
*/
public function __construct(array $data)
{
$this->hostKeyPairExpires = $data['host_keypair_expires'] ?? null;
$this->hostPublicKey = $data['host_public_key'] ?? null;
$this->hostPrivateKey = $data['host_private_key'] ?? null;
$this->internalEncryptionKeys = $data['internal_encryption_keys'] ?? null;
$this->encryptionKeysCount = $data['encryption_keys_count'];
$this->encryptionKeysAlgorithm = $data['encryption_keys_algorithm'];
$this->transportEncryptionAlgorithm = $data['transport_encryption_algorithm'];
}
/**
* Retrieves the expiration timestamp of the host key pair.
*
* @return int|null The expiration timestamp of the host key pair, or null if not set.
*/
public function getHostKeyPairExpires(): ?int
{
return $this->hostKeyPairExpires;
}
/**
* Retrieves the host's public key.
*
* @return string|null The host's public key, or null if not set.
*/
public function getHostPublicKey(): ?string
{
return $this->hostPublicKey;
}
/**
* Retrieves the private key associated with the host.
*
* @return string|null The host's private key, or null if not set.
*/
public function getHostPrivateKey(): ?string
{
return $this->hostPrivateKey;
}
/**
* Retrieves the internal encryption keys.
*
* @return array|null Returns an array of internal encryption keys if set, or null if no keys are available.
*/
public function getInternalEncryptionKeys(): ?array
{
return $this->internalEncryptionKeys;
}
/**
* Retrieves a random internal encryption key from the available set of encryption keys.
*
* @return string|null Returns a randomly selected encryption key as a string, or null if no keys are available.
*/
public function getRandomInternalEncryptionKey(): ?string
{
return $this->internalEncryptionKeys[array_rand($this->internalEncryptionKeys)];
}
/**
* Retrieves the total count of encryption keys.
*
* @return int The number of encryption keys.
*/
public function getEncryptionKeysCount(): int
{
return $this->encryptionKeysCount;
}
/**
* Retrieves the algorithm used for the encryption keys.
*
* @return string The encryption keys algorithm.
*/
public function getEncryptionKeysAlgorithm(): string
{
return $this->encryptionKeysAlgorithm;
}
/**
* Retrieves the transport encryption algorithm being used.
*
* @return string The transport encryption algorithm.
*/
public function getTransportEncryptionAlgorithm(): string
{
return $this->transportEncryptionAlgorithm;
}
}

View file

@ -5,13 +5,9 @@
class InstanceConfiguration class InstanceConfiguration
{ {
private bool $enabled; private bool $enabled;
private string $name;
private ?string $domain; private ?string $domain;
private ?string $rpcEndpoint; private ?string $rpcEndpoint;
private int $encryptionKeysCount;
private int $encryptionRecordsCount;
private ?string $privateKey;
private ?string $publicKey;
private ?array $encryptionKeys;
/** /**
* Constructor that initializes object properties with the provided data. * Constructor that initializes object properties with the provided data.
@ -22,13 +18,9 @@
public function __construct(array $data) public function __construct(array $data)
{ {
$this->enabled = (bool)$data['enabled']; $this->enabled = (bool)$data['enabled'];
$this->name = $data['name'];
$this->domain = $data['domain']; $this->domain = $data['domain'];
$this->rpcEndpoint = $data['rpc_endpoint']; $this->rpcEndpoint = $data['rpc_endpoint'];
$this->encryptionKeysCount = $data['encryption_keys_count'];
$this->encryptionRecordsCount = $data['encryption_records_count'];
$this->privateKey = $data['private_key'];
$this->publicKey = $data['public_key'];
$this->encryptionKeys = $data['encryption_keys'];
} }
/** /**
@ -41,6 +33,11 @@
return $this->enabled; return $this->enabled;
} }
public function getName(): string
{
return $this->name;
}
/** /**
* Retrieves the domain. * Retrieves the domain.
* *
@ -58,62 +55,4 @@
{ {
return $this->rpcEndpoint; return $this->rpcEndpoint;
} }
/**
* Retrieves the number of encryption keys.
*
* @return int The number of encryption keys.
*/
public function getEncryptionKeysCount(): int
{
return $this->encryptionKeysCount;
}
/**
* Retrieves the number of encryption records.
*
* @return int The number of encryption records.
*/
public function getEncryptionRecordsCount(): int
{
return $this->encryptionRecordsCount;
}
/**
* Retrieves the private key.
*
* @return string|null The private key.
*/
public function getPrivateKey(): ?string
{
return $this->privateKey;
}
/**
* Retrieves the public key.
*
* @return string|null The public key.
*/
public function getPublicKey(): ?string
{
return $this->publicKey;
}
/**
* Retrieves the encryption keys.
*
* @return array|null The encryption keys.
*/
public function getEncryptionKeys(): ?array
{
return $this->encryptionKeys;
}
/**
* @return string
*/
public function getRandomEncryptionKey(): string
{
return $this->encryptionKeys[array_rand($this->encryptionKeys)];
}
} }

View file

@ -1,6 +1,6 @@
<?php <?php
namespace Socialbox\Classes\ClientCommands; namespace Socialbox\Classes\Configuration;
class StorageConfiguration class StorageConfiguration
{ {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,41 @@
<?php
namespace Socialbox\Classes;
use InvalidArgumentException;
use Socialbox\Objects\DnsRecord;
class DnsHelper
{
/**
* Generates a TXT formatted string containing the provided RPC endpoint, public key, and expiration time.
*
* @param string $rpcEndpoint The RPC endpoint to include in the TXT string.
* @param string $publicKey The public key to include in the TXT string.
* @param int $expirationTime The expiration time in seconds to include in the TXT string.
*
* @return string A formatted TXT string containing the input data.
*/
public static function generateTxt(string $rpcEndpoint, string $publicKey, int $expirationTime): string
{
return sprintf('v=socialbox;sb-rpc=%s;sb-key=%s;sb-exp=%d', $rpcEndpoint, $publicKey, $expirationTime);
}
/**
* Parses a TXT record string and extracts its components into a DnsRecord object.
*
* @param string $txtRecord The TXT record string to be parsed.
* @return DnsRecord The extracted DnsRecord object containing the RPC endpoint, public key, and expiration time.
* @throws InvalidArgumentException If the TXT record format is invalid.
*/
public static function parseTxt(string $txtRecord): DnsRecord
{
$pattern = '/v=socialbox;sb-rpc=(?P<rpcEndpoint>https?:\/\/[^;]+);sb-key=(?P<publicSigningKey>[^;]+);sb-exp=(?P<expirationTime>\d+)/';
if (preg_match($pattern, $txtRecord, $matches))
{
return new DnsRecord($matches['rpcEndpoint'], $matches['publicSigningKey'], (int)$matches['expirationTime']);
}
throw new InvalidArgumentException('Invalid TXT record format.');
}
}

View file

@ -2,9 +2,7 @@ create table authentication_passwords
( (
peer_uuid varchar(36) not null comment 'The Universal Unique Identifier for the peer that is associated with this password record' peer_uuid varchar(36) not null comment 'The Universal Unique Identifier for the peer that is associated with this password record'
primary key comment 'The primary unique index of the peer uuid', primary key comment 'The primary unique index of the peer uuid',
iv mediumtext not null comment 'The Initial Vector of the password record', hash mediumtext not null comment 'The encrypted hash of the password',
encrypted_password mediumtext not null comment 'The encrypted password data',
encrypted_tag mediumtext not null comment 'The encrypted tag of the password record',
updated timestamp default current_timestamp() not null comment 'The Timestamp for when this record was last updated', updated timestamp default current_timestamp() not null comment 'The Timestamp for when this record was last updated',
constraint authentication_passwords_peer_uuid_uindex constraint authentication_passwords_peer_uuid_uindex
unique (peer_uuid) comment 'The primary unique index of the peer uuid', unique (peer_uuid) comment 'The primary unique index of the peer uuid',

View file

@ -1,8 +0,0 @@
create table encryption_records
(
data mediumtext not null comment 'The data column',
iv mediumtext not null comment 'The initialization vector column',
tag mediumtext not null comment 'The authentication tag used to verify if the data was tampered'
)
comment 'Table for housing encryption records for the server';

View file

@ -0,0 +1,19 @@
create table resolved_dns_records
(
domain varchar(512) not null comment 'The domain name'
primary key comment 'Unique Index for the server domain',
rpc_endpoint text not null comment 'The endpoint of the RPC server',
public_key text not null comment 'The Public Key of the server',
expires bigint not null comment 'The Unix Timestamp for when the server''s keypair expires',
updated timestamp default current_timestamp() not null comment 'The Timestamp for when this record was last updated',
constraint resolved_dns_records_domain_uindex
unique (domain) comment 'Unique Index for the server domain',
constraint resolved_dns_records_pk
unique (domain) comment 'Unique Index for the server domain'
)
comment 'A table for housing DNS resolutions';
create index resolved_dns_records_updated_index
on resolved_dns_records (updated)
comment 'The index for the updated column';

View file

@ -1,12 +0,0 @@
create table resolved_servers
(
domain varchar(512) not null comment 'The domain name'
primary key comment 'Unique Index for the server domain',
endpoint text not null comment 'The endpoint of the RPC server',
public_key text not null comment 'The Public Key of the server',
updated timestamp default current_timestamp() not null comment 'The TImestamp for when this record was last updated',
constraint resolved_servers_domain_uindex
unique (domain) comment 'Unique Index for the server domain'
)
comment 'A table for housing DNS resolutions';

View file

@ -6,9 +6,14 @@ create table sessions
client_name varchar(256) not null comment 'The name of the client that is using this session', client_name varchar(256) not null comment 'The name of the client that is using this session',
client_version varchar(16) not null comment 'The version of the client', client_version varchar(16) not null comment 'The version of the client',
authenticated tinyint(1) default 0 not null comment 'Indicates if the session is currently authenticated as the peer', authenticated tinyint(1) default 0 not null comment 'Indicates if the session is currently authenticated as the peer',
public_key text not null comment 'The client''s public key provided when creating the session', client_public_signing_key text not null comment 'The client''s public signing key used for signing decrypted messages',
client_public_encryption_key text not null comment 'The Public Key of the client''s encryption key',
server_public_encryption_key text not null comment 'The server''s public encryption key for this session',
server_private_encryption_key text not null comment 'The server''s private encryption key for this session',
private_shared_secret text null comment 'The shared secret encryption key between the Client & Server',
client_transport_encryption_key text null comment 'The encryption key for sending messages to the client',
server_transport_encryption_key text null comment 'The encryption key for sending messages to the server',
state enum ('AWAITING_DHE', 'ACTIVE', 'CLOSED', 'EXPIRED') default 'AWAITING_DHE' not null comment 'The status of the session', state enum ('AWAITING_DHE', 'ACTIVE', 'CLOSED', 'EXPIRED') default 'AWAITING_DHE' not null comment 'The status of the session',
encryption_key text null comment 'The key used for encryption that is obtained through a DHE',
flags text null comment 'The current flags that is set to the session', flags text null comment 'The current flags that is set to the session',
created timestamp default current_timestamp() not null comment 'The Timestamp for when the session was last created', created timestamp default current_timestamp() not null comment 'The Timestamp for when the session was last created',
last_request timestamp null comment 'The Timestamp for when the last request was made using this session', last_request timestamp null comment 'The Timestamp for when the last request was made using this session',

View file

@ -2,11 +2,10 @@
namespace Socialbox\Classes; namespace Socialbox\Classes;
use Socialbox\Enums\Options\ClientOptions; use Socialbox\Enums\StandardError;
use Socialbox\Enums\StandardHeaders; use Socialbox\Enums\StandardHeaders;
use Socialbox\Enums\Types\RequestType; use Socialbox\Enums\Types\RequestType;
use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\CryptographyException;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\ResolutionException; use Socialbox\Exceptions\ResolutionException;
use Socialbox\Exceptions\RpcException; use Socialbox\Exceptions\RpcException;
use Socialbox\Objects\ExportedSession; use Socialbox\Objects\ExportedSession;
@ -14,6 +13,7 @@
use Socialbox\Objects\PeerAddress; use Socialbox\Objects\PeerAddress;
use Socialbox\Objects\RpcRequest; use Socialbox\Objects\RpcRequest;
use Socialbox\Objects\RpcResult; use Socialbox\Objects\RpcResult;
use Socialbox\Objects\Standard\ServerInformation;
class RpcClient class RpcClient
{ {
@ -22,9 +22,14 @@
private bool $bypassSignatureVerification; private bool $bypassSignatureVerification;
private PeerAddress $peerAddress; private PeerAddress $peerAddress;
private KeyPair $keyPair; private string $serverPublicSigningKey;
private string $encryptionKey; private string $serverPublicEncryptionKey;
private string $serverPublicKey; private KeyPair $clientSigningKeyPair;
private KeyPair $clientEncryptionKeyPair;
private string $privateSharedSecret;
private string $clientTransportEncryptionKey;
private string $serverTransportEncryptionKey;
private ServerInformation $serverInformation;
private string $rpcEndpoint; private string $rpcEndpoint;
private string $sessionUuid; private string $sessionUuid;
@ -42,14 +47,41 @@
$this->bypassSignatureVerification = false; $this->bypassSignatureVerification = false;
// If an exported session is provided, no need to re-connect. // If an exported session is provided, no need to re-connect.
// Just use the session details provided.
if($exportedSession !== null) if($exportedSession !== null)
{ {
// Check if the server keypair has expired from the exported session
if(time() > $exportedSession->getServerKeypairExpires())
{
throw new RpcException('The server keypair has expired, a new session must be created');
}
$this->peerAddress = PeerAddress::fromAddress($exportedSession->getPeerAddress()); $this->peerAddress = PeerAddress::fromAddress($exportedSession->getPeerAddress());
$this->keyPair = new KeyPair($exportedSession->getPublicKey(), $exportedSession->getPrivateKey());
$this->encryptionKey = $exportedSession->getEncryptionKey();
$this->serverPublicKey = $exportedSession->getServerPublicKey();
$this->rpcEndpoint = $exportedSession->getRpcEndpoint(); $this->rpcEndpoint = $exportedSession->getRpcEndpoint();
$this->sessionUuid = $exportedSession->getSessionUuid(); $this->sessionUuid = $exportedSession->getSessionUuid();
$this->serverPublicSigningKey = $exportedSession->getServerPublicSigningKey();
$this->serverPublicEncryptionKey = $exportedSession->getServerPublicEncryptionKey();
$this->clientSigningKeyPair = new KeyPair($exportedSession->getClientPublicSigningKey(), $exportedSession->getClientPrivateSigningKey());
$this->clientEncryptionKeyPair = new KeyPair($exportedSession->getClientPublicEncryptionKey(), $exportedSession->getClientPrivateEncryptionKey());
$this->privateSharedSecret = $exportedSession->getPrivateSharedSecret();
$this->clientTransportEncryptionKey = $exportedSession->getClientTransportEncryptionKey();
$this->serverTransportEncryptionKey = $exportedSession->getServerTransportEncryptionKey();
// Still solve the server information
$this->serverInformation = self::getServerInformation();
// Check if the active keypair has expired
if(time() > $this->serverInformation->getServerKeypairExpires())
{
throw new RpcException('The server keypair has expired but the server has not provided a new keypair, contact the server administrator');
}
// Check if the transport encryption algorithm has changed
if($this->serverInformation->getTransportEncryptionAlgorithm() !== $exportedSession->getTransportEncryptionAlgorithm())
{
throw new RpcException('The server has changed its transport encryption algorithm, a new session must be created');
}
return; return;
} }
@ -62,51 +94,61 @@
// Set the initial properties // Set the initial properties
$this->peerAddress = $peerAddress; $this->peerAddress = $peerAddress;
// If the username is `host` and the domain is the same as this server's domain, we use our keypair
// Essentially this is a special case for the server to contact another server
if($this->peerAddress->isHost())
{
$this->keyPair = new KeyPair(Configuration::getInstanceConfiguration()->getPublicKey(), Configuration::getInstanceConfiguration()->getPrivateKey());
}
// Otherwise we generate a random keypair
else
{
$this->keyPair = Cryptography::generateKeyPair();
}
$this->encryptionKey = Cryptography::generateEncryptionKey();
// Resolve the domain and get the server's Public Key & RPC Endpoint // Resolve the domain and get the server's Public Key & RPC Endpoint
try
{
$resolvedServer = ServerResolver::resolveDomain($this->peerAddress->getDomain(), false); $resolvedServer = ServerResolver::resolveDomain($this->peerAddress->getDomain(), false);
}
catch (DatabaseOperationException $e)
{
throw new ResolutionException('Failed to resolve domain: ' . $e->getMessage(), 0, $e);
}
$this->serverPublicKey = $resolvedServer->getPublicKey(); // Import the RPC Endpoint & the server's public key.
$this->rpcEndpoint = $resolvedServer->getEndpoint(); $this->serverPublicSigningKey = $resolvedServer->getPublicSigningKey();
$this->rpcEndpoint = $resolvedServer->getRpcEndpoint();
if(empty($this->serverPublicKey)) if(empty($this->serverPublicSigningKey))
{ {
throw new ResolutionException('Failed to resolve domain: No public key found for the server'); throw new ResolutionException('Failed to resolve domain: No public key found for the server');
} }
// Attempt to create an encrypted session with the server // Resolve basic server information
$this->sessionUuid = $this->createSession(); $this->serverInformation = self::getServerInformation();
// Check if the server keypair has expired
if(time() > $this->serverInformation->getServerKeypairExpires())
{
throw new RpcException('The server keypair has expired but the server has not provided a new keypair, contact the server administrator');
}
// If the username is `host` and the domain is the same as this server's domain, we use our signing keypair
// Essentially this is a special case for the server to contact another server
if($this->peerAddress->isHost())
{
$this->clientSigningKeyPair = new KeyPair(Configuration::getCryptographyConfiguration()->getHostPublicKey(), Configuration::getCryptographyConfiguration()->getHostPrivateKey());
}
// Otherwise we generate a random signing keypair
else
{
$this->clientSigningKeyPair = Cryptography::generateSigningKeyPair();
}
// Always use a random encryption keypair
$this->clientEncryptionKeyPair = Cryptography::generateEncryptionKeyPair();
// Create a session with the server, with the method we obtain the Session UUID
// And the server's public encryption key.
$this->createSession();
// Generate a transport encryption key on our end using the server's preferred algorithm
$this->clientTransportEncryptionKey = Cryptography::generateEncryptionKey($this->serverInformation->getTransportEncryptionAlgorithm());
// Preform the DHE so that transport encryption keys can be exchanged
$this->sendDheExchange(); $this->sendDheExchange();
} }
/** /**
* Creates a new session by sending an HTTP GET request to the RPC endpoint. * Initiates a new session with the server and retrieves the session UUID.
* The request includes specific headers required for session initiation.
* *
* @return string Returns the session UUID received from the server. * @return string The session UUID provided by the server upon successful session creation.
* @throws RpcException If the server response is invalid, the session creation fails, or no session UUID is returned. * @throws RpcException If the session cannot be created, if the server does not provide a valid response,
* or critical headers like encryption public key are missing in the server's response.
*/ */
private function createSession(): string private function createSession(): void
{ {
$ch = curl_init(); $ch = curl_init();
@ -116,28 +158,45 @@
StandardHeaders::CLIENT_NAME->value . ': ' . self::CLIENT_NAME, StandardHeaders::CLIENT_NAME->value . ': ' . self::CLIENT_NAME,
StandardHeaders::CLIENT_VERSION->value . ': ' . self::CLIENT_VERSION, StandardHeaders::CLIENT_VERSION->value . ': ' . self::CLIENT_VERSION,
StandardHeaders::IDENTIFY_AS->value . ': ' . $this->peerAddress->getAddress(), StandardHeaders::IDENTIFY_AS->value . ': ' . $this->peerAddress->getAddress(),
// Always provide our generated encrypted public key
StandardHeaders::ENCRYPTION_PUBLIC_KEY->value . ': ' . $this->clientEncryptionKeyPair->getPublicKey()
]; ];
// If we're not connecting as the host, we need to provide our public key // If we're not connecting as the host, we need to provide our public key
// Otherwise, the server will obtain the public key itself from DNS records rather than trusting the client // Otherwise, the server will obtain the public key itself from DNS records rather than trusting the client
if(!$this->peerAddress->isHost()) if(!$this->peerAddress->isHost())
{ {
$headers[] = StandardHeaders::PUBLIC_KEY->value . ': ' . $this->keyPair->getPublicKey(); $headers[] = StandardHeaders::SIGNING_PUBLIC_KEY->value . ': ' . $this->clientSigningKeyPair->getPublicKey();
} }
$responseHeaders = [];
curl_setopt($ch, CURLOPT_URL, $this->rpcEndpoint); curl_setopt($ch, CURLOPT_URL, $this->rpcEndpoint);
curl_setopt($ch, CURLOPT_HTTPGET, true); curl_setopt($ch, CURLOPT_HTTPGET, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); // Capture the response headers to get the encryption public key
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($curl, $header) use (&$responseHeaders)
{
$len = strlen($header);
$header = explode(':', $header, 2);
if (count($header) < 2) // ignore invalid headers
{
return $len;
}
$responseHeaders[strtolower(trim($header[0]))][] = trim($header[1]);
return $len;
});
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch); $response = curl_exec($ch);
// If the response is false, the request failed
if($response === false) if($response === false)
{ {
curl_close($ch); curl_close($ch);
throw new RpcException(sprintf('Failed to create the session at %s, no response received', $this->rpcEndpoint)); throw new RpcException(sprintf('Failed to create the session at %s, no response received', $this->rpcEndpoint));
} }
// If the response code is not 201, the request failed
$responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if($responseCode !== 201) if($responseCode !== 201)
{ {
@ -151,14 +210,44 @@
throw new RpcException(sprintf('Failed to create the session at %s, server responded with ' . $responseCode . ': ' . $response, $this->rpcEndpoint)); throw new RpcException(sprintf('Failed to create the session at %s, server responded with ' . $responseCode . ': ' . $response, $this->rpcEndpoint));
} }
// If the response is empty, the server did not provide a session UUID
if(empty($response)) if(empty($response))
{ {
curl_close($ch); curl_close($ch);
throw new RpcException(sprintf('Failed to create the session at %s, server did not return a session UUID', $this->rpcEndpoint)); throw new RpcException(sprintf('Failed to create the session at %s, server did not return a session UUID', $this->rpcEndpoint));
} }
// Get the Encryption Public Key from the server's response headers
$serverPublicEncryptionKey = $responseHeaders[strtolower(StandardHeaders::ENCRYPTION_PUBLIC_KEY->value)][0] ?? null;
// null check
if($serverPublicEncryptionKey === null)
{
curl_close($ch); curl_close($ch);
return $response; throw new RpcException('Failed to create session at %s, the server did not return a public encryption key');
}
// Validate the server's encryption public key
if(!Cryptography::validatePublicEncryptionKey($serverPublicEncryptionKey))
{
curl_close($ch);
throw new RpcException('The server did not provide a valid encryption public key');
}
// If the server did not provide an encryption public key, the response is invalid
// We can't preform the DHE without the server's encryption key.
if ($serverPublicEncryptionKey === null)
{
curl_close($ch);
throw new RpcException('The server did not provide a signature for the response');
}
curl_close($ch);
// Set the server's encryption key
$this->serverPublicEncryptionKey = $serverPublicEncryptionKey;
// Set the session UUID
$this->sessionUuid = $response;
} }
/** /**
@ -168,15 +257,26 @@
*/ */
private function sendDheExchange(): void private function sendDheExchange(): void
{ {
// First preform the DHE
try
{
$this->privateSharedSecret = Cryptography::performDHE($this->serverPublicEncryptionKey, $this->clientEncryptionKeyPair->getPrivateKey());
}
catch(CryptographyException $e)
{
throw new RpcException('Failed to preform DHE: ' . $e->getMessage(), StandardError::CRYPTOGRAPHIC_ERROR->value, $e);
}
// Request body should contain the encrypted key, the client's public key, and the session UUID // Request body should contain the encrypted key, the client's public key, and the session UUID
// Upon success the server should return 204 without a body // Upon success the server should return 204 without a body
try try
{ {
$encryptedKey = Cryptography::encryptContent($this->encryptionKey, $this->serverPublicKey); $encryptedKey = Cryptography::encryptShared($this->clientTransportEncryptionKey, $this->privateSharedSecret);
$signature = Cryptography::signMessage($this->clientTransportEncryptionKey, $this->clientSigningKeyPair->getPrivateKey());
} }
catch (CryptographyException $e) catch (CryptographyException $e)
{ {
throw new RpcException('Failed to encrypt DHE exchange data', 0, $e); throw new RpcException('Failed to encrypt DHE exchange data', StandardError::CRYPTOGRAPHIC_ERROR->value, $e);
} }
$ch = curl_init(); $ch = curl_init();
@ -186,6 +286,7 @@
curl_setopt($ch, CURLOPT_HTTPHEADER, [ curl_setopt($ch, CURLOPT_HTTPHEADER, [
StandardHeaders::REQUEST_TYPE->value . ': ' . RequestType::DHE_EXCHANGE->value, StandardHeaders::REQUEST_TYPE->value . ': ' . RequestType::DHE_EXCHANGE->value,
StandardHeaders::SESSION_UUID->value . ': ' . $this->sessionUuid, StandardHeaders::SESSION_UUID->value . ': ' . $this->sessionUuid,
StandardHeaders::SIGNATURE->value . ': ' . $signature
]); ]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $encryptedKey); curl_setopt($ch, CURLOPT_POSTFIELDS, $encryptedKey);
@ -194,18 +295,29 @@
if($response === false) if($response === false)
{ {
curl_close($ch); curl_close($ch);
throw new RpcException('Failed to send DHE exchange, no response received'); throw new RpcException('Failed to send DHE exchange, no response received', StandardError::CRYPTOGRAPHIC_ERROR->value);
} }
$responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if($responseCode !== 204) if($responseCode !== 200)
{ {
curl_close($ch); curl_close($ch);
throw new RpcException('Failed to send DHE exchange, server responded with ' . $responseCode . ': ' . $response); throw new RpcException('Failed to send DHE exchange, server responded with ' . $responseCode . ': ' . $response, StandardError::CRYPTOGRAPHIC_ERROR->value);
} }
try
{
$this->serverTransportEncryptionKey = Cryptography::decryptShared($response, $this->privateSharedSecret);
}
catch(CryptographyException $e)
{
throw new RpcException('Failed to decrypt DHE exchange data', 0, $e);
}
finally
{
curl_close($ch); curl_close($ch);
} }
}
/** /**
* Sends an RPC request with the given JSON data. * Sends an RPC request with the given JSON data.
@ -218,8 +330,16 @@
{ {
try try
{ {
$encryptedData = Cryptography::encryptTransport($jsonData, $this->encryptionKey); $encryptedData = Cryptography::encryptMessage(
$signature = Cryptography::signContent($jsonData, $this->keyPair->getPrivateKey(), true); message: $jsonData,
encryptionKey: $this->serverTransportEncryptionKey,
algorithm: $this->serverInformation->getTransportEncryptionAlgorithm()
);
$signature = Cryptography::signMessage(
message: $jsonData,
privateKey: $this->clientSigningKeyPair->getPrivateKey(),
);
} }
catch (CryptographyException $e) catch (CryptographyException $e)
{ {
@ -289,7 +409,11 @@
try try
{ {
$decryptedResponse = Cryptography::decryptTransport($responseString, $this->encryptionKey); $decryptedResponse = Cryptography::decryptMessage(
encryptedMessage: $responseString,
encryptionKey: $this->clientTransportEncryptionKey,
algorithm: $this->serverInformation->getTransportEncryptionAlgorithm()
);
} }
catch (CryptographyException $e) catch (CryptographyException $e)
{ {
@ -298,7 +422,7 @@
if (!$this->bypassSignatureVerification) if (!$this->bypassSignatureVerification)
{ {
$signature = $headers['signature'][0] ?? null; $signature = $headers[strtolower(StandardHeaders::SIGNATURE->value)][0] ?? null;
if ($signature === null) if ($signature === null)
{ {
throw new RpcException('The server did not provide a signature for the response'); throw new RpcException('The server did not provide a signature for the response');
@ -306,7 +430,11 @@
try try
{ {
if (!Cryptography::verifyContent($decryptedResponse, $signature, $this->serverPublicKey, true)) if(!Cryptography::verifyMessage(
message: $decryptedResponse,
signature: $signature,
publicKey: $this->serverPublicSigningKey,
))
{ {
throw new RpcException('Failed to verify the response signature'); throw new RpcException('Failed to verify the response signature');
} }
@ -333,6 +461,59 @@
} }
} }
/**
* Retrieves server information by making an RPC request.
*
* @return ServerInformation The parsed server information received in the response.
* @throws RpcException If the request fails, no response is received, or the server returns an error status code or invalid data.
*/
public function getServerInformation(): ServerInformation
{
$ch = curl_init();
// Basic session details
$headers = [
StandardHeaders::REQUEST_TYPE->value . ': ' . RequestType::INFO->value,
StandardHeaders::CLIENT_NAME->value . ': ' . self::CLIENT_NAME,
StandardHeaders::CLIENT_VERSION->value . ': ' . self::CLIENT_VERSION,
];
curl_setopt($ch, CURLOPT_URL, $this->rpcEndpoint);
curl_setopt($ch, CURLOPT_HTTPGET, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch);
if($response === false)
{
curl_close($ch);
throw new RpcException(sprintf('Failed to get server information at %s, no response received', $this->rpcEndpoint));
}
$responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if($responseCode !== 200)
{
curl_close($ch);
if(empty($response))
{
throw new RpcException(sprintf('Failed to get server information at %s, server responded with ' . $responseCode, $this->rpcEndpoint));
}
throw new RpcException(sprintf('Failed to get server information at %s, server responded with ' . $responseCode . ': ' . $response, $this->rpcEndpoint));
}
if(empty($response))
{
curl_close($ch);
throw new RpcException(sprintf('Failed to get server information at %s, server returned an empty response', $this->rpcEndpoint));
}
curl_close($ch);
return ServerInformation::fromArray(json_decode($response, true));
}
/** /**
* Sends an RPC request and retrieves the corresponding RPC response. * Sends an RPC request and retrieves the corresponding RPC response.
* *
@ -395,12 +576,19 @@
{ {
return new ExportedSession([ return new ExportedSession([
'peer_address' => $this->peerAddress->getAddress(), 'peer_address' => $this->peerAddress->getAddress(),
'private_key' => $this->keyPair->getPrivateKey(),
'public_key' => $this->keyPair->getPublicKey(),
'encryption_key' => $this->encryptionKey,
'server_public_key' => $this->serverPublicKey,
'rpc_endpoint' => $this->rpcEndpoint, 'rpc_endpoint' => $this->rpcEndpoint,
'session_uuid' => $this->sessionUuid 'session_uuid' => $this->sessionUuid,
'transport_encryption_algorithm' => $this->serverInformation->getTransportEncryptionAlgorithm(),
'server_keypair_expires' => $this->serverInformation->getServerKeypairExpires(),
'server_public_signing_key' => $this->serverPublicSigningKey,
'server_public_encryption_key' => $this->serverPublicEncryptionKey,
'client_public_signing_key' => $this->clientSigningKeyPair->getPublicKey(),
'client_private_signing_key' => $this->clientSigningKeyPair->getPrivateKey(),
'client_public_encryption_key' => $this->clientEncryptionKeyPair->getPublicKey(),
'client_private_encryption_key' => $this->clientEncryptionKeyPair->getPrivateKey(),
'private_shared_secret' => $this->privateSharedSecret,
'client_transport_encryption_key' => $this->clientTransportEncryptionKey,
'server_transport_encryption_key' => $this->serverTransportEncryptionKey
]); ]);
} }
} }

View file

@ -1,96 +0,0 @@
<?php
namespace Socialbox\Classes;
use DateTime;
use Random\RandomException;
use Socialbox\Exceptions\CryptographyException;
use Socialbox\Objects\Database\EncryptionRecord;
use Socialbox\Objects\Database\SecurePasswordRecord;
class SecuredPassword
{
public const string ENCRYPTION_ALGORITHM = 'aes-256-gcm';
public const int ITERATIONS = 500000; // Increased iterations for PBKDF2
public const int KEY_LENGTH = 256; // Increased key length
public const int PEPPER_LENGTH = 64;
/**
* Encrypts a password using a derived key and other cryptographic elements
* to ensure secure storage.
*
* @param string $peerUuid The unique identifier of the peer associated with the password.
* @param string $password The plain text password to be secured.
* @param EncryptionRecord $record The encryption record containing information such as
* the key, salt, and pepper required for encryption.
* @return SecurePasswordRecord Returns an object containing the encrypted password
* along with associated cryptographic data such as IV and tag.
* @throws CryptographyException Throws an exception if password encryption or
* cryptographic element generation fails.
* @throws \DateMalformedStringException
*/
public static function securePassword(string $peerUuid, string $password, EncryptionRecord $record): SecurePasswordRecord
{
$decrypted = $record->decrypt();
$saltedPassword = $decrypted->getSalt() . $password;
$derivedKey = hash_pbkdf2('sha512', $saltedPassword, $decrypted->getPepper(), self::ITERATIONS, self::KEY_LENGTH / 8, true);
try
{
$iv = random_bytes(openssl_cipher_iv_length(self::ENCRYPTION_ALGORITHM));
}
catch (RandomException $e)
{
throw new CryptographyException("Failed to generate IV for password encryption", $e);
}
$tag = null;
$encryptedPassword = openssl_encrypt($derivedKey, self::ENCRYPTION_ALGORITHM, base64_decode($decrypted->getKey()), OPENSSL_RAW_DATA, $iv, $tag);
if ($encryptedPassword === false)
{
throw new CryptographyException("Password encryption failed");
}
return new SecurePasswordRecord([
'peer_uuid' => $peerUuid,
'iv' => base64_encode($iv),
'encrypted_password' => base64_encode($encryptedPassword),
'encrypted_tag' => base64_encode($tag),
'updated' => (new DateTime())->setTimestamp(time())
]);
}
/**
* Verifies the provided password against the secured data and encryption records.
*
* @param string $input The user-provided password to be verified.
* @param SecurePasswordRecord $secured An array containing encrypted data required for verification.
* @param EncryptionRecord[] $encryptionRecords An array of encryption records used to perform decryption and validation.
* @return bool Returns true if the password matches the secured data; otherwise, returns false.
* @throws CryptographyException
*/
public static function verifyPassword(string $input, SecurePasswordRecord $secured, array $encryptionRecords): bool
{
foreach ($encryptionRecords as $record)
{
$decrypted = $record->decrypt();
$saltedInput = $decrypted->getSalt() . $input;
$derivedKey = hash_pbkdf2('sha512', $saltedInput, $decrypted->getPepper(), self::ITERATIONS, self::KEY_LENGTH / 8, true);
// Validation by re-encrypting and comparing
$encryptedTag = base64_decode($secured->getEncryptedTag());
$reEncryptedPassword = openssl_encrypt($derivedKey,
self::ENCRYPTION_ALGORITHM, base64_decode($decrypted->getKey()), OPENSSL_RAW_DATA,
base64_decode($secured->getIv()), $encryptedTag
);
if ($reEncryptedPassword !== false && hash_equals($reEncryptedPassword, base64_decode($secured->getEncryptedPassword())))
{
return true;
}
}
return false;
}
}

View file

@ -2,32 +2,31 @@
namespace Socialbox\Classes; namespace Socialbox\Classes;
use Socialbox\Exceptions\DatabaseOperationException; use InvalidArgumentException;
use Socialbox\Exceptions\ResolutionException; use Socialbox\Exceptions\ResolutionException;
use Socialbox\Managers\ResolvedServersManager; use Socialbox\Managers\ResolvedDnsRecordsManager;
use Socialbox\Objects\ResolvedServer; use Socialbox\Objects\DnsRecord;
class ServerResolver class ServerResolver
{ {
private const string PATTERN = '/v=socialbox;sb-rpc=(https?:\/\/[^;]+);sb-key=([^;]+)/';
/** /**
* Resolves a given domain to fetch the RPC endpoint and public key from its DNS TXT records. * Resolves a domain by retrieving and parsing its DNS TXT records.
* Optionally checks a database for cached resolution data before performing a DNS query.
* *
* @param string $domain The domain to be resolved. * @param string $domain The domain name to resolve.
* @return ResolvedServer An instance of ResolvedServer containing the endpoint and public key. * @param bool $useDatabase Whether to check the database for cached resolution data; defaults to true.
* @throws ResolutionException If the DNS TXT records cannot be resolved or if required information is missing. * @return DnsRecord The parsed DNS record for the given domain.
* @throws DatabaseOperationException * @throws ResolutionException If the DNS TXT records cannot be retrieved or parsed.
*/ */
public static function resolveDomain(string $domain, bool $useDatabase=true): ResolvedServer public static function resolveDomain(string $domain, bool $useDatabase=true): DnsRecord
{ {
// First query the database to check if the domain is already resolved // Check the database if enabled
if ($useDatabase) if ($useDatabase)
{ {
$resolvedServer = ResolvedServersManager::getResolvedServer($domain); $resolvedServer = ResolvedDnsRecordsManager::getDnsRecord($domain);
if ($resolvedServer !== null) if ($resolvedServer !== null)
{ {
return $resolvedServer->toResolvedServer(); return $resolvedServer;
} }
} }
@ -38,23 +37,23 @@
} }
$fullRecord = self::concatenateTxtRecords($txtRecords); $fullRecord = self::concatenateTxtRecords($txtRecords);
if (preg_match(self::PATTERN, $fullRecord, $matches))
try
{ {
$endpoint = trim($matches[1]); // Parse the TXT record using DnsHelper
$publicKey = trim(str_replace(' ', '', $matches[2])); $record = DnsHelper::parseTxt($fullRecord);
if (empty($endpoint))
// Cache the resolved server record in the database
if($useDatabase)
{ {
throw new ResolutionException(sprintf("Failed to resolve RPC endpoint for %s", $domain)); ResolvedDnsRecordsManager::addResolvedServer($domain, $record);
} }
if (empty($publicKey))
{ return $record;
throw new ResolutionException(sprintf("Failed to resolve public key for %s", $domain));
} }
return new ResolvedServer($endpoint, $publicKey); catch (InvalidArgumentException $e)
}
else
{ {
throw new ResolutionException(sprintf("Failed to find valid SocialBox record for %s", $domain)); throw new ResolutionException(sprintf("Failed to find valid SocialBox record for %s: %s", $domain, $e->getMessage()));
} }
} }
@ -64,9 +63,9 @@
* @param string $domain The domain name to fetch TXT records for. * @param string $domain The domain name to fetch TXT records for.
* @return array|false An array of DNS TXT records on success, or false on failure. * @return array|false An array of DNS TXT records on success, or false on failure.
*/ */
private static function dnsGetTxtRecords(string $domain) private static function dnsGetTxtRecords(string $domain): array|false
{ {
return dns_get_record($domain, DNS_TXT); return @dns_get_record($domain, DNS_TXT);
} }
/** /**

View file

@ -38,14 +38,14 @@
if($decodedImage === false) if($decodedImage === false)
{ {
return $rpcRequest->produceError(StandardError::BAD_REQUEST, "Failed to decode JPEG image base64 data"); return $rpcRequest->produceError(StandardError::RPC_BAD_REQUEST, "Failed to decode JPEG image base64 data");
} }
$sanitizedImage = Utilities::resizeImage(Utilities::sanitizeJpeg($decodedImage), 126, 126); $sanitizedImage = Utilities::resizeImage(Utilities::sanitizeJpeg($decodedImage), 126, 126);
} }
catch(Exception $e) catch(Exception $e)
{ {
throw new StandardException('Failed to process JPEG image: ' . $e->getMessage(), StandardError::BAD_REQUEST, $e); throw new StandardException('Failed to process JPEG image: ' . $e->getMessage(), StandardError::RPC_BAD_REQUEST, $e);
} }
try try

View file

@ -122,22 +122,37 @@
return $headers; return $headers;
} }
/** public static function throwableToString(Throwable $e, int $level=0): string
* Converts a Throwable object into a formatted string.
*
* @param Throwable $e The throwable to be converted into a string.
* @return string The formatted string representation of the throwable, including the exception class, message, file, line, and stack trace.
*/
public static function throwableToString(Throwable $e): string
{ {
return sprintf( // Indentation for nested exceptions
"%s: %s in %s:%d\nStack trace:\n%s", $indentation = str_repeat(' ', $level);
get_class($e),
$e->getMessage(), // Basic information about the Throwable
$e->getFile(), $type = get_class($e);
$e->getLine(), $message = $e->getMessage() ?: 'No message';
$e->getTraceAsString() $file = $e->getFile() ?: 'Unknown file';
$line = $e->getLine() ?? 'Unknown line';
// Compose the base string representation of this Throwable
$result = sprintf("%s%s: %s\n%s in %s on line %s\n",
$indentation, $type, $message, $indentation, $file, $line
); );
// Append stack trace if available
$stackTrace = $e->getTraceAsString();
if (!empty($stackTrace))
{
$result .= $indentation . " Stack trace:\n" . $indentation . " " . str_replace("\n", "\n" . $indentation . " ", $stackTrace) . "\n";
}
// Recursively append the cause if it exists
$previous = $e->getPrevious();
if ($previous)
{
$result .= $indentation . "Caused by:\n" . self::throwableToString($previous, $level + 1);
}
return $result;
} }
/** /**

View file

@ -5,8 +5,7 @@
enum DatabaseObjects : string enum DatabaseObjects : string
{ {
case VARIABLES = 'variables.sql'; case VARIABLES = 'variables.sql';
case ENCRYPTION_RECORDS = 'encryption_records.sql'; case RESOLVED_DNS_RECORDS = 'resolved_dns_records.sql';
case RESOLVED_SERVERS = 'resolved_servers.sql';
case REGISTERED_PEERS = 'registered_peers.sql'; case REGISTERED_PEERS = 'registered_peers.sql';
@ -24,7 +23,7 @@
{ {
return match ($this) return match ($this)
{ {
self::VARIABLES, self::ENCRYPTION_RECORDS, self::RESOLVED_SERVERS => 0, self::VARIABLES, self::RESOLVED_DNS_RECORDS => 0,
self::REGISTERED_PEERS => 1, self::REGISTERED_PEERS => 1,
self::AUTHENTICATION_PASSWORDS, self::CAPTCHA_IMAGES, self::SESSIONS, self::EXTERNAL_SESSIONS => 2, self::AUTHENTICATION_PASSWORDS, self::CAPTCHA_IMAGES, self::SESSIONS, self::EXTERNAL_SESSIONS => 2,
}; };

View file

@ -7,16 +7,21 @@ enum StandardError : int
// Fallback Codes // Fallback Codes
case UNKNOWN = -1; case UNKNOWN = -1;
// Server/Request Errors
case INTERNAL_SERVER_ERROR = -100;
case SERVER_UNAVAILABLE = -101;
case BAD_REQUEST = -102;
case FORBIDDEN = -103;
case UNAUTHORIZED = -104;
case RESOLUTION_FAILED = -105;
case CRYPTOGRAPHIC_ERROR = -106;
// RPC Errors // RPC Errors
case RPC_METHOD_NOT_FOUND = -1000; case RPC_METHOD_NOT_FOUND = -1000;
case RPC_INVALID_ARGUMENTS = -1001; case RPC_INVALID_ARGUMENTS = -1001;
CASE RPC_BAD_REQUEST = -1002;
// Server Errors
case INTERNAL_SERVER_ERROR = -2000;
case SERVER_UNAVAILABLE = -2001;
// Client Errors // Client Errors
case BAD_REQUEST = -3000;
case METHOD_NOT_ALLOWED = -3001; case METHOD_NOT_ALLOWED = -3001;
// Authentication/Cryptography Errors // Authentication/Cryptography Errors

View file

@ -8,10 +8,12 @@
enum StandardHeaders : string enum StandardHeaders : string
{ {
case REQUEST_TYPE = 'Request-Type'; case REQUEST_TYPE = 'Request-Type';
case ERROR_CODE = 'Error-Code';
case IDENTIFY_AS = 'Identify-As'; case IDENTIFY_AS = 'Identify-As';
case CLIENT_NAME = 'Client-Name'; case CLIENT_NAME = 'Client-Name';
case CLIENT_VERSION = 'Client-Version'; case CLIENT_VERSION = 'Client-Version';
case PUBLIC_KEY = 'Public-Key'; case SIGNING_PUBLIC_KEY = 'Signing-Public-Key';
case ENCRYPTION_PUBLIC_KEY = 'Encryption-Public-Key';
case SESSION_UUID = 'Session-UUID'; case SESSION_UUID = 'Session-UUID';
case SIGNATURE = 'Signature'; case SIGNATURE = 'Signature';

View file

@ -4,6 +4,11 @@
enum RequestType : string enum RequestType : string
{ {
/**
* Represents the action of getting server information (Non-RPC Request)
*/
case INFO = 'info';
/** /**
* Represents the action of initiating a session. * Represents the action of initiating a session.
*/ */

View file

@ -1,205 +0,0 @@
<?php
namespace Socialbox\Managers;
use PDOException;
use Random\RandomException;
use Socialbox\Classes\Configuration;
use Socialbox\Classes\Database;
use Socialbox\Classes\SecuredPassword;
use Socialbox\Exceptions\CryptographyException;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Objects\Database\EncryptionRecord;
class EncryptionRecordsManager
{
private const int KEY_LENGTH = 256; // Increased key length
/**
* Retrieves the total count of records in the encryption_records table.
*
* @return int The number of records in the encryption_records table.
* @throws DatabaseOperationException If a database operation error occurs while fetching the record count.
*/
public static function getRecordCount(): int
{
try
{
$stmt = Database::getConnection()->prepare('SELECT COUNT(*) FROM encryption_records');
$stmt->execute();
return $stmt->fetchColumn();
}
catch (PDOException $e)
{
throw new DatabaseOperationException('Failed to retrieve encryption record count', $e);
}
}
/**
* Inserts a new encryption record into the encryption_records table.
*
* @param EncryptionRecord $record The encryption record to insert, containing data, IV, and tag.
* @return void
* @throws DatabaseOperationException If the insertion into the database fails.
*/
private static function insertRecord(EncryptionRecord $record): void
{
try
{
$stmt = Database::getConnection()->prepare('INSERT INTO encryption_records (data, iv, tag) VALUES (?, ?, ?)');
$data = $record->getData();
$stmt->bindParam(1, $data);
$iv = $record->getIv();
$stmt->bindParam(2, $iv);
$tag = $record->getTag();
$stmt->bindParam(3, $tag);
$stmt->execute();
}
catch(PDOException $e)
{
throw new DatabaseOperationException('Failed to insert encryption record into the database', $e);
}
}
/**
* Retrieves a random encryption record from the database.
*
* @return EncryptionRecord An instance of EncryptionRecord containing the data of a randomly selected record.
* @throws DatabaseOperationException If an error occurs while attempting to retrieve the record from the database.
*/
public static function getRandomRecord(): EncryptionRecord
{
try
{
$stmt = Database::getConnection()->prepare('SELECT * FROM encryption_records ORDER BY RAND() LIMIT 1');
$stmt->execute();
$data = $stmt->fetch();
return new EncryptionRecord($data);
}
catch(PDOException $e)
{
throw new DatabaseOperationException('Failed to retrieve a random encryption record', $e);
}
}
/**
* Retrieves all encryption records from the database.
*
* @return EncryptionRecord[] An array of EncryptionRecord instances, each representing a record from the database.
* @throws DatabaseOperationException If an error occurs while attempting to retrieve the records from the database.
*/
public static function getAllRecords(): array
{
try
{
$stmt = Database::getConnection()->prepare('SELECT * FROM encryption_records');
$stmt->execute();
$data = $stmt->fetchAll();
$records = [];
foreach ($data as $record)
{
$records[] = new EncryptionRecord($record);
}
return $records;
}
catch(PDOException $e)
{
throw new DatabaseOperationException('Failed to retrieve all encryption records', $e);
}
}
/**
* Generates encryption records and inserts them into the database until the specified total count is reached.
*
* @param int $count The total number of encryption records desired in the database.
* @return int The number of new records that were created and inserted.
* @throws CryptographyException
* @throws DatabaseOperationException
*/
public static function generateRecords(int $count): int
{
$currentCount = self::getRecordCount();
if($currentCount >= $count)
{
return 0;
}
$created = 0;
for($i = 0; $i < $count - $currentCount; $i++)
{
self::insertRecord(self::generateEncryptionRecord());
$created++;
}
return $created;
}
/**
* Generates a new encryption record containing a key, pepper, and salt.
*
* @return EncryptionRecord An instance of EncryptionRecord containing an encrypted structure
* with the generated key, pepper, and salt.
* @throws CryptographyException If random byte generation fails during the creation of the encryption record.
*/
private static function generateEncryptionRecord(): EncryptionRecord
{
try
{
$key = random_bytes(self::KEY_LENGTH / 8);
$pepper = bin2hex(random_bytes(SecuredPassword::PEPPER_LENGTH / 2));
$salt = bin2hex(random_bytes(self::KEY_LENGTH / 16));
}
catch (RandomException $e)
{
throw new CryptographyException("Random bytes generation failed", $e->getCode(), $e);
}
return self::encrypt(['key' => base64_encode($key), 'pepper' => $pepper, 'salt' => $salt,]);
}
/**
* Encrypts the given vault item and returns an EncryptionRecord containing the encrypted data.
*
* @param array $vaultItem The associative array representing the vault item to be encrypted.
* @return EncryptionRecord An instance of EncryptionRecord containing the encrypted vault data, initialization vector (IV), and authentication tag.
* @throws CryptographyException If the initialization vector generation or vault encryption process fails.
*/
private static function encrypt(array $vaultItem): EncryptionRecord
{
$serializedVault = json_encode($vaultItem);
try
{
$iv = random_bytes(openssl_cipher_iv_length(SecuredPassword::ENCRYPTION_ALGORITHM));
}
catch (RandomException $e)
{
throw new CryptographyException("IV generation failed", $e->getCode(), $e);
}
$tag = null;
$encryptedVault = openssl_encrypt($serializedVault, SecuredPassword::ENCRYPTION_ALGORITHM,
Configuration::getInstanceConfiguration()->getRandomEncryptionKey(), OPENSSL_RAW_DATA, $iv, $tag
);
if ($encryptedVault === false)
{
throw new CryptographyException("Vault encryption failed");
}
return new EncryptionRecord([
'data' => base64_encode($encryptedVault),
'iv' => base64_encode($iv),
'tag' => base64_encode($tag),
]);
}
}

View file

@ -2,13 +2,15 @@
namespace Socialbox\Managers; namespace Socialbox\Managers;
use DateTime;
use PDO; use PDO;
use PDOException;
use Socialbox\Classes\Configuration;
use Socialbox\Classes\Cryptography;
use Socialbox\Classes\Database; use Socialbox\Classes\Database;
use Socialbox\Classes\SecuredPassword;
use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\CryptographyException;
use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Objects\Database\RegisteredPeerRecord; use Socialbox\Objects\Database\RegisteredPeerRecord;
use Socialbox\Objects\Database\SecurePasswordRecord;
class PasswordManager class PasswordManager
{ {
@ -34,154 +36,139 @@
return $stmt->fetchColumn() > 0; return $stmt->fetchColumn() > 0;
} }
catch (\PDOException $e) catch (PDOException $e)
{ {
throw new DatabaseOperationException('An error occurred while checking the password usage in the database', $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 * Sets a secured password for the given peer UUID or registered peer record.
* 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|RegisteredPeerRecord $peerUuid The unique identifier or registered peer record of the user.
* @param string $password The plaintext password to be securely stored. * @param string $hash 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 * @return void
* @throws DatabaseOperationException If an error occurs while storing the password in the database.
* @throws CryptographyException If an error occurs during password encryption or hashing.
*/ */
public static function setPassword(string|RegisteredPeerRecord $peerUuid, string $password): void public static function setPassword(string|RegisteredPeerRecord $peerUuid, string $hash): void
{ {
if($peerUuid instanceof RegisteredPeerRecord) if($peerUuid instanceof RegisteredPeerRecord)
{ {
$peerUuid = $peerUuid->getUuid(); $peerUuid = $peerUuid->getUuid();
} }
$encryptionRecord = EncryptionRecordsManager::getRandomRecord(); // Throws an exception if the hash is invalid
$securedPassword = SecuredPassword::securePassword($peerUuid, $password, $encryptionRecord); Cryptography::validatePasswordHash($hash);
$encryptionKey = Configuration::getCryptographyConfiguration()->getRandomInternalEncryptionKey();
$securedPassword = Cryptography::encryptMessage($hash, $encryptionKey, Configuration::getCryptographyConfiguration()->getEncryptionKeysAlgorithm());
try 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 = Database::getConnection()->prepare("INSERT INTO authentication_passwords (peer_uuid, hash) VALUES (:peer_uuid, :hash)");
$stmt->bindParam(":peer_uuid", $peerUuid); $stmt->bindParam(":peer_uuid", $peerUuid);
$stmt->bindParam(':hash', $securedPassword);
$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(); $stmt->execute();
} }
catch(\PDOException $e) catch(PDOException $e)
{ {
throw new DatabaseOperationException(sprintf('Failed to set password for user %s', $peerUuid), $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. * Updates the secured password associated with the given peer UUID.
* *
* @param string|RegisteredPeerRecord $peerUuid The UUID of the peer or an instance of RegisteredPeerRecord. * @param string|RegisteredPeerRecord $peerUuid The unique identifier or registered peer record of the user.
* @param string $newPassword The new password to be set for the peer. * @param string $hash The new password to be stored securely.
* @throws CryptographyException If an error occurs while securing the new password. * @return void
* @throws DatabaseOperationException If the update operation fails due to a database error. * @throws DatabaseOperationException If an error occurs while updating the password in the database.
* @throws \DateMalformedStringException If the updated timestamp cannot be formatted. * @throws CryptographyException If an error occurs while encrypting the password or validating the hash.
* @returns void
*/ */
public static function updatePassword(string|RegisteredPeerRecord $peerUuid, string $newPassword): void public static function updatePassword(string|RegisteredPeerRecord $peerUuid, string $hash): void
{ {
if($peerUuid instanceof RegisteredPeerRecord) if($peerUuid instanceof RegisteredPeerRecord)
{ {
$peerUuid = $peerUuid->getUuid(); $peerUuid = $peerUuid->getUuid();
} }
Cryptography::validatePasswordHash($hash);
$encryptionRecord = EncryptionRecordsManager::getRandomRecord(); $encryptionKey = Configuration::getCryptographyConfiguration()->getRandomInternalEncryptionKey();
$securedPassword = SecuredPassword::securePassword($peerUuid, $newPassword, $encryptionRecord); $securedPassword = Cryptography::encryptMessage($hash, $encryptionKey, Configuration::getCryptographyConfiguration()->getEncryptionKeysAlgorithm());
try 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 = Database::getConnection()->prepare("UPDATE authentication_passwords SET hash=:hash, updated=:updated WHERE peer_uuid=:peer_uuid");
$stmt->bindParam(":peer_uuid", $peerUuid); $updated = (new DateTime())->setTimestamp(time());
$stmt->bindParam(':hash', $securedPassword);
$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->bindParam(':updated', $updated);
$stmt->bindParam(':peer_uuid', $peerUuid);
$stmt->execute(); $stmt->execute();
} }
catch(\PDOException $e) catch(PDOException $e)
{ {
throw new DatabaseOperationException(sprintf('Failed to update password for user %s', $peerUuid), $e); throw new DatabaseOperationException(sprintf('Failed to update password for user %s', $peerUuid), $e);
} }
} }
/** /**
* Retrieves the password record associated with the given peer UUID. * Verifies a given password against a stored password hash for a specific peer.
* *
* @param string|RegisteredPeerRecord $peerUuid The UUID of the peer or an instance of RegisteredPeerRecord. * @param string|RegisteredPeerRecord $peerUuid The unique identifier of the peer, or an instance of RegisteredPeerRecord.
* @return SecurePasswordRecord|null Returns a SecurePasswordRecord if found, or null if no record is present. * @param string $hash The password hash to verify.
* @throws DatabaseOperationException If a database operation error occurs during the retrieval process. * @return bool Returns true if the password matches the stored hash; false otherwise.
* @throws CryptographyException If the password hash is invalid or an error occurs during the cryptographic operation.
* @throws DatabaseOperationException If an error occurs during the database operation.
*/ */
private static function getPassword(string|RegisteredPeerRecord $peerUuid): ?SecurePasswordRecord public static function verifyPassword(string|RegisteredPeerRecord $peerUuid, string $hash): bool
{ {
if($peerUuid instanceof RegisteredPeerRecord) if($peerUuid instanceof RegisteredPeerRecord)
{ {
$peerUuid = $peerUuid->getUuid(); $peerUuid = $peerUuid->getUuid();
} }
Cryptography::validatePasswordHash($hash);
try try
{ {
$statement = Database::getConnection()->prepare("SELECT * FROM authentication_passwords WHERE peer_uuid=:peer_uuid LIMIT 1"); $stmt = Database::getConnection()->prepare('SELECT hash FROM authentication_passwords WHERE peer_uuid=:uuid');
$statement->bindParam(':peer_uuid', $peerUuid); $stmt->bindParam(':uuid', $peerUuid);
$stmt->execute();
$statement->execute(); $record = $stmt->fetch(PDO::FETCH_ASSOC);
$data = $statement->fetch(PDO::FETCH_ASSOC); if($record === false)
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; return false;
} }
$encryptionRecords = EncryptionRecordsManager::getAllRecords(); $encryptedHash = $record['hash'];
return SecuredPassword::verifyPassword($password, $securedPassword, $encryptionRecords); $decryptedHash = null;
foreach(Configuration::getCryptographyConfiguration()->getInternalEncryptionKeys() as $key)
{
try
{
$decryptedHash = Cryptography::decryptMessage($encryptedHash, $key, Configuration::getCryptographyConfiguration()->getEncryptionKeysAlgorithm());
}
catch(CryptographyException)
{
continue;
}
}
if($decryptedHash === null)
{
throw new CryptographyException('Cannot decrypt hashed password');
}
return Cryptography::verifyPassword($hash, $decryptedHash);
}
catch(PDOException $e)
{
throw new DatabaseOperationException('An error occurred while verifying the password', $e);
}
} }
} }

View file

@ -0,0 +1,184 @@
<?php
namespace Socialbox\Managers;
use DateTime;
use Exception;
use PDOException;
use Socialbox\Classes\Database;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Objects\DnsRecord;
class ResolvedDnsRecordsManager
{
/**
* Checks whether a resolved server record exists in the database for the provided domain.
*
* @param string $domain The domain name to check for existence in the resolved records.
* @return bool True if the resolved server record exists, otherwise false.
* @throws DatabaseOperationException If the process encounters a database error.
*/
public static function resolvedServerExists(string $domain): bool
{
try
{
$statement = Database::getConnection()->prepare("SELECT COUNT(*) FROM resolved_dns_records WHERE domain=?");
$statement->bindParam(1, $domain);
$statement->execute();
return $statement->fetchColumn() > 0;
}
catch(PDOException $e)
{
throw new DatabaseOperationException('Failed to check if a resolved server exists in the database', $e);
}
}
/**
* Deletes a resolved server record from the database for the provided domain.
*
* @param string $domain The domain name of the resolved server to be deleted.
* @return void
* @throws DatabaseOperationException If the deletion process encounters a database error.
*/
public static function deleteResolvedServer(string $domain): void
{
try
{
$statement = Database::getConnection()->prepare("DELETE FROM resolved_dns_records WHERE domain=?");
$statement->bindParam(1, $domain);
$statement->execute();
}
catch(PDOException $e)
{
throw new DatabaseOperationException('Failed to delete a resolved server from the database', $e);
}
}
/**
* Retrieves the last updated timestamp of a resolved server from the database for a given domain.
*
* This method queries the database to fetch the timestamp indicating when the resolved server
* associated with the specified domain was last updated.
*
* @param string $domain The domain name for which the last updated timestamp is to be retrieved.
* @return DateTime The DateTime object representing the last updated timestamp of the resolved server.
*
* @throws DatabaseOperationException If the operation to retrieve the updated timestamp from the
* database fails.
*/
public static function getResolvedServerUpdated(string $domain): DateTime
{
try
{
$statement = Database::getConnection()->prepare("SELECT updated FROM resolved_dns_records WHERE domain=?");
$statement->bindParam(1, $domain);
$statement->execute();
$result = $statement->fetchColumn();
return new DateTime($result);
}
catch(Exception $e)
{
throw new DatabaseOperationException('Failed to get the updated date of a resolved server from the database', $e);
}
}
/**
* Retrieves a DNS record for the specified domain from the database.
*
* This method fetches the DNS record details, such as the RPC endpoint, public key,
* and expiration details, associated with the provided domain. If no record is found,
* it returns null.
*
* @param string $domain The domain name for which the DNS record is to be retrieved.
* @return DnsRecord|null The DNS record object if found, or null if no record exists for the given domain.
*
* @throws DatabaseOperationException If the operation to retrieve the DNS record from
* the database fails.
*/
public static function getDnsRecord(string $domain): ?DnsRecord
{
try
{
$statement = Database::getConnection()->prepare("SELECT * FROM resolved_dns_records WHERE domain=?");
$statement->bindParam(1, $domain);
$statement->execute();
$result = $statement->fetch();
if($result === false)
{
return null;
}
return DnsRecord::fromArray([
'rpc_endpoint' => $result['rpc_endpoint'],
'public_key' => $result['public_key'],
'expires' => $result['expires']
]);
}
catch(PDOException $e)
{
throw new DatabaseOperationException('Failed to get a resolved server from the database', $e);
}
}
/**
* Adds or updates a resolved server in the database based on the provided domain and DNS record.
*
* If a resolved server for the given domain already exists in the database, the server's details
* will be updated. Otherwise, a new record will be inserted into the database.
*
* @param string $domain The domain name associated with the resolved server.
* @param DnsRecord $dnsRecord An object containing DNS record details such as the RPC endpoint,
* public key, and expiration details.
* @return void
* @throws DatabaseOperationException If the operation to add or update the resolved server in
* the database fails.
*/
public static function addResolvedServer(string $domain, DnsRecord $dnsRecord): void
{
$endpoint = $dnsRecord->getRpcEndpoint();
$publicKey = $dnsRecord->getPublicSigningKey();
if(self::resolvedServerExists($domain))
{
$statement = Database::getConnection()->prepare("UPDATE resolved_dns_records SET rpc_endpoint=?, public_key=?, expires=?, updated=? WHERE domain=?");
$statement->bindParam(1, $endpoint);
$statement->bindParam(2, $publicKey);
$expires = (new DateTime())->setTimestamp($dnsRecord->getExpires());
$statement->bindParam(3, $expires);
$updated = new DateTime();
$statement->bindParam(4, $updated);
$statement->bindParam(5, $domain);
$statement->execute();
if($statement->rowCount() === 0)
{
throw new DatabaseOperationException('Failed to update a resolved server in the database');
}
return;
}
try
{
$statement = Database::getConnection()->prepare("INSERT INTO resolved_dns_records (domain, rpc_endpoint, public_key, expires, updated) VALUES (?, ?, ?, ?, ?)");
$statement->bindParam(1, $domain);
$statement->bindParam(2, $endpoint);
$statement->bindParam(3, $publicKey);
$expires = (new DateTime())->setTimestamp($dnsRecord->getExpires());
$statement->bindParam(4, $expires);
$updated = new DateTime();
$statement->bindParam(5, $updated);
$statement->execute();
if($statement->rowCount() === 0)
{
throw new DatabaseOperationException('Failed to add a resolved server to the database');
}
}
catch(PDOException $e)
{
throw new DatabaseOperationException('Failed to add a resolved server to the database', $e);
}
}
}

View file

@ -1,152 +0,0 @@
<?php
namespace Socialbox\Managers;
use DateTime;
use Exception;
use PDOException;
use Socialbox\Classes\Database;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Objects\Database\ResolvedServerRecord;
use Socialbox\Objects\ResolvedServer;
class ResolvedServersManager
{
/**
* Checks if a resolved server exists in the database for the given domain.
*
* @param string $domain The domain to check in the resolved_servers table.
* @return bool True if the server exists in the database, otherwise false.
* @throws DatabaseOperationException If there is an error during the database operation.
*/
public static function resolvedServerExists(string $domain): bool
{
try
{
$statement = Database::getConnection()->prepare("SELECT COUNT(*) FROM resolved_servers WHERE domain=?");
$statement->bindParam(1, $domain);
$statement->execute();
return $statement->fetchColumn() > 0;
}
catch(PDOException $e)
{
throw new DatabaseOperationException('Failed to check if a resolved server exists in the database', $e);
}
}
/**
* Deletes a resolved server from the database.
*
* @param string $domain The domain name of the server to be deleted.
* @return void
* @throws DatabaseOperationException If the deletion operation fails.
*/
public static function deleteResolvedServer(string $domain): void
{
try
{
$statement = Database::getConnection()->prepare("DELETE FROM resolved_servers WHERE domain=?");
$statement->bindParam(1, $domain);
$statement->execute();
}
catch(PDOException $e)
{
throw new DatabaseOperationException('Failed to delete a resolved server from the database', $e);
}
}
/**
* Retrieves the last updated date of a resolved server based on its domain.
*
* @param string $domain The domain of the resolved server.
* @return DateTime The last updated date and time of the resolved server.
*/
public static function getResolvedServerUpdated(string $domain): DateTime
{
try
{
$statement = Database::getConnection()->prepare("SELECT updated FROM resolved_servers WHERE domain=?");
$statement->bindParam(1, $domain);
$statement->execute();
$result = $statement->fetchColumn();
return new DateTime($result);
}
catch(Exception $e)
{
throw new DatabaseOperationException('Failed to get the updated date of a resolved server from the database', $e);
}
}
/**
* Retrieves the resolved server record from the database for a given domain.
*
* @param string $domain The domain name for which to retrieve the resolved server record.
* @return ResolvedServerRecord|null The resolved server record associated with the given domain.
* @throws DatabaseOperationException If there is an error retrieving the resolved server record from the database.
* @throws \DateMalformedStringException If the date string is malformed.
*/
public static function getResolvedServer(string $domain): ?ResolvedServerRecord
{
try
{
$statement = Database::getConnection()->prepare("SELECT * FROM resolved_servers WHERE domain=?");
$statement->bindParam(1, $domain);
$statement->execute();
$result = $statement->fetch();
if($result === false)
{
return null;
}
return ResolvedServerRecord::fromArray($result);
}
catch(PDOException $e)
{
throw new DatabaseOperationException('Failed to get a resolved server from the database', $e);
}
}
/**
* Adds or updates a resolved server in the database.
*
* @param string $domain The domain name of the resolved server.
* @param ResolvedServer $resolvedServer The resolved server object containing endpoint and public key.
* @return void
* @throws DatabaseOperationException If a database operation fails.
*/
public static function addResolvedServer(string $domain, ResolvedServer $resolvedServer): void
{
$endpoint = $resolvedServer->getEndpoint();
$publicKey = $resolvedServer->getPublicKey();
if(self::resolvedServerExists($domain))
{
try
{
$statement = Database::getConnection()->prepare("UPDATE resolved_servers SET endpoint=?, public_key=?, updated=NOW() WHERE domain=?");
$statement->bindParam(1, $endpoint);
$statement->bindParam(2, $publicKey);
$statement->bindParam(3, $domain);
$statement->execute();
}
catch(PDOException $e)
{
throw new DatabaseOperationException('Failed to update a resolved server in the database', $e);
}
}
try
{
$statement = Database::getConnection()->prepare("INSERT INTO resolved_servers (domain, endpoint, public_key) VALUES (?, ?, ?)");
$statement->bindParam(1, $domain);
$statement->bindParam(2, $endpoint);
$statement->bindParam(3, $publicKey);
$statement->execute();
}
catch(PDOException $e)
{
throw new DatabaseOperationException('Failed to add a resolved server to the database', $e);
}
}
}

View file

@ -19,34 +19,27 @@
use Socialbox\Exceptions\StandardException; use Socialbox\Exceptions\StandardException;
use Socialbox\Objects\Database\RegisteredPeerRecord; use Socialbox\Objects\Database\RegisteredPeerRecord;
use Socialbox\Objects\Database\SessionRecord; use Socialbox\Objects\Database\SessionRecord;
use Socialbox\Objects\KeyPair;
use Symfony\Component\Uid\Uuid; use Symfony\Component\Uid\Uuid;
class SessionManager class SessionManager
{ {
/** public static function createSession(RegisteredPeerRecord $peer, string $clientName, string $clientVersion, string $clientPublicSigningKey, string $clientPublicEncryptionKey, KeyPair $serverEncryptionKeyPair): string
* Creates a new session with the given public key.
*
* @param string $publicKey The public key to associate with the new session.
* @param RegisteredPeerRecord $peer
*
* @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, RegisteredPeerRecord $peer, string $clientName, string $clientVersion): string
{ {
if($publicKey === '') if($clientPublicSigningKey === '' || Cryptography::validatePublicSigningKey($clientPublicSigningKey) === false)
{ {
throw new InvalidArgumentException('The public key cannot be empty'); throw new InvalidArgumentException('The public key is not a valid Ed25519 public key');
} }
if(!Cryptography::validatePublicKey($publicKey)) if($clientPublicEncryptionKey === '' || Cryptography::validatePublicEncryptionKey($clientPublicEncryptionKey) === false)
{ {
throw new InvalidArgumentException('The given public key is invalid'); throw new InvalidArgumentException('The public key is not a valid X25519 public key');
} }
$uuid = Uuid::v4()->toRfc4122(); $uuid = Uuid::v4()->toRfc4122();
$flags = []; $flags = [];
// TODO: Update this to support `host` peers
if($peer->isEnabled()) if($peer->isEnabled())
{ {
$flags[] = SessionFlags::AUTHENTICATION_REQUIRED; $flags[] = SessionFlags::AUTHENTICATION_REQUIRED;
@ -119,13 +112,18 @@
try try
{ {
$statement = Database::getConnection()->prepare("INSERT INTO sessions (uuid, peer_uuid, client_name, client_version, public_key, flags) VALUES (?, ?, ?, ?, ?, ?)"); $statement = Database::getConnection()->prepare("INSERT INTO sessions (uuid, peer_uuid, client_name, client_version, client_public_signing_key, client_public_encryption_key, server_public_encryption_key, server_private_encryption_key, flags) VALUES (:uuid, :peer_uuid, :client_name, :client_version, :client_public_signing_key, :client_public_encryption_key, :server_public_encryption_key, :server_private_encryption_key, :flags)");
$statement->bindParam(1, $uuid); $statement->bindParam(':uuid', $uuid);
$statement->bindParam(2, $peerUuid); $statement->bindParam(':peer_uuid', $peerUuid);
$statement->bindParam(3, $clientName); $statement->bindParam(':client_name', $clientName);
$statement->bindParam(4, $clientVersion); $statement->bindParam(':client_version', $clientVersion);
$statement->bindParam(5, $publicKey); $statement->bindParam(':client_public_signing_key', $clientPublicSigningKey);
$statement->bindParam(6, $implodedFlags); $statement->bindParam(':client_public_encryption_key', $clientPublicEncryptionKey);
$serverPublicEncryptionKey = $serverEncryptionKeyPair->getPublicKey();
$statement->bindParam(':server_public_encryption_key', $serverPublicEncryptionKey);
$serverPrivateEncryptionKey = $serverEncryptionKeyPair->getPrivateKey();
$statement->bindParam(':server_private_encryption_key', $serverPrivateEncryptionKey);
$statement->bindParam(':flags', $implodedFlags);
$statement->execute(); $statement->execute();
} }
catch(PDOException $e) catch(PDOException $e)
@ -186,7 +184,6 @@
// Convert the timestamp fields to DateTime objects // Convert the timestamp fields to DateTime objects
$data['created'] = new DateTime($data['created']); $data['created'] = new DateTime($data['created']);
if(isset($data['last_request']) && $data['last_request'] !== null) if(isset($data['last_request']) && $data['last_request'] !== null)
{ {
$data['last_request'] = new DateTime($data['last_request']); $data['last_request'] = new DateTime($data['last_request']);
@ -205,53 +202,6 @@
} }
} }
/**
* Update the authenticated peer associated with the given session UUID.
*
* @param string $uuid The UUID of the session to update.
* @param RegisteredPeerRecord|string $registeredPeerUuid
* @return void
* @throws DatabaseOperationException
*/
public static function updatePeer(string $uuid, RegisteredPeerRecord|string $registeredPeerUuid): void
{
if($registeredPeerUuid instanceof RegisteredPeerRecord)
{
$registeredPeerUuid = $registeredPeerUuid->getUuid();
}
Logger::getLogger()->verbose(sprintf("Assigning peer %s to session %s", $registeredPeerUuid, $uuid));
try
{
$statement = Database::getConnection()->prepare("UPDATE sessions SET peer_uuid=? WHERE uuid=?");
$statement->bindParam(1, $registeredPeerUuid);
$statement->bindParam(2, $uuid);
$statement->execute();
}
catch (PDOException $e)
{
throw new DatabaseOperationException('Failed to update authenticated peer', $e);
}
}
public static function updateAuthentication(string $uuid, bool $authenticated): void
{
Logger::getLogger()->verbose(sprintf("Marking 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);
}
}
/** /**
* Updates the last request timestamp for a given session by its UUID. * Updates the last request timestamp for a given session by its UUID.
* *
@ -305,24 +255,28 @@
} }
/** /**
* Updates the encryption key for the specified session. * Updates the encryption keys and session state for a specific session UUID in the database.
* *
* @param string $uuid The unique identifier of the session for which the encryption key is to be set. * @param string $uuid The unique identifier for the session to update.
* @param string $encryptionKey The new encryption key to be assigned. * @param string $privateSharedSecret The private shared secret to secure communication.
* @param string $clientEncryptionKey The client's encryption key used for transport security.
* @param string $serverEncryptionKey The server's encryption key used for transport security.
* @return void * @return void
* @throws DatabaseOperationException If the database operation fails. * @throws DatabaseOperationException If an error occurs during the database operation.
*/ */
public static function setEncryptionKey(string $uuid, string $encryptionKey): void public static function setEncryptionKeys(string $uuid, string $privateSharedSecret, string $clientEncryptionKey, string $serverEncryptionKey): void
{ {
Logger::getLogger()->verbose(sprintf('Setting the encryption key for %s', $uuid)); Logger::getLogger()->verbose(sprintf('Setting the encryption key for %s', $uuid));
try try
{ {
$state_value = SessionState::ACTIVE->value; $state_value = SessionState::ACTIVE->value;
$statement = Database::getConnection()->prepare('UPDATE sessions SET state=?, encryption_key=? WHERE uuid=?'); $statement = Database::getConnection()->prepare('UPDATE sessions SET state=:state, private_shared_secret=:private_shared_secret, client_transport_encryption_key=:client_transport_encryption_key, server_transport_encryption_key=:server_transport_encryption_key WHERE uuid=:uuid');
$statement->bindParam(1, $state_value); $statement->bindParam(':state', $state_value);
$statement->bindParam(2, $encryptionKey); $statement->bindParam(':private_shared_secret', $privateSharedSecret);
$statement->bindParam(3, $uuid); $statement->bindParam(':client_transport_encryption_key', $clientEncryptionKey);
$statement->bindParam(':server_transport_encryption_key', $serverEncryptionKey);
$statement->bindParam(':uuid', $uuid);
$statement->execute(); $statement->execute();
} }

View file

@ -2,10 +2,7 @@
namespace Socialbox\Objects; namespace Socialbox\Objects;
use InvalidArgumentException;
use Socialbox\Classes\Cryptography; use Socialbox\Classes\Cryptography;
use Socialbox\Classes\Utilities;
use Socialbox\Enums\SessionState;
use Socialbox\Enums\StandardHeaders; use Socialbox\Enums\StandardHeaders;
use Socialbox\Enums\Types\RequestType; use Socialbox\Enums\Types\RequestType;
use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\CryptographyException;
@ -18,7 +15,7 @@
class ClientRequest class ClientRequest
{ {
private array $headers; private array $headers;
private RequestType $requestType; private ?RequestType $requestType;
private ?string $requestBody; private ?string $requestBody;
private ?string $clientName; private ?string $clientName;
@ -27,6 +24,14 @@
private ?string $sessionUuid; private ?string $sessionUuid;
private ?string $signature; private ?string $signature;
/**
* Initializes the instance with the provided request headers and optional request body.
*
* @param array $headers An associative array of request headers used to set properties such as client name, version, and others.
* @param string|null $requestBody The optional body of the request, or null if not provided.
*
* @return void
*/
public function __construct(array $headers, ?string $requestBody) public function __construct(array $headers, ?string $requestBody)
{ {
$this->headers = $headers; $this->headers = $headers;
@ -34,17 +39,28 @@
$this->clientName = $headers[StandardHeaders::CLIENT_NAME->value] ?? null; $this->clientName = $headers[StandardHeaders::CLIENT_NAME->value] ?? null;
$this->clientVersion = $headers[StandardHeaders::CLIENT_VERSION->value] ?? null; $this->clientVersion = $headers[StandardHeaders::CLIENT_VERSION->value] ?? null;
$this->requestType = RequestType::from($headers[StandardHeaders::REQUEST_TYPE->value]); $this->requestType = RequestType::tryFrom($headers[StandardHeaders::REQUEST_TYPE->value]);
$this->identifyAs = $headers[StandardHeaders::IDENTIFY_AS->value] ?? null; $this->identifyAs = $headers[StandardHeaders::IDENTIFY_AS->value] ?? null;
$this->sessionUuid = $headers[StandardHeaders::SESSION_UUID->value] ?? null; $this->sessionUuid = $headers[StandardHeaders::SESSION_UUID->value] ?? null;
$this->signature = $headers[StandardHeaders::SIGNATURE->value] ?? null; $this->signature = $headers[StandardHeaders::SIGNATURE->value] ?? null;
} }
/**
* Retrieves the headers.
*
* @return array Returns an array of headers.
*/
public function getHeaders(): array public function getHeaders(): array
{ {
return $this->headers; return $this->headers;
} }
/**
* Checks if the specified header exists in the collection of headers.
*
* @param StandardHeaders|string $header The header to check, either as a StandardHeaders enum or a string.
* @return bool Returns true if the header exists, otherwise false.
*/
public function headerExists(StandardHeaders|string $header): bool public function headerExists(StandardHeaders|string $header): bool
{ {
if(is_string($header)) if(is_string($header))
@ -55,6 +71,12 @@
return isset($this->headers[$header->value]); return isset($this->headers[$header->value]);
} }
/**
* Retrieves the value of a specified header.
*
* @param StandardHeaders|string $header The header to retrieve, provided as either a StandardHeaders enum or a string key.
* @return string|null Returns the header value if it exists, or null if the header does not exist.
*/
public function getHeader(StandardHeaders|string $header): ?string public function getHeader(StandardHeaders|string $header): ?string
{ {
if(!$this->headerExists($header)) if(!$this->headerExists($header))
@ -70,26 +92,51 @@
return $this->headers[$header->value]; return $this->headers[$header->value];
} }
/**
* Retrieves the request body.
*
* @return string|null Returns the request body as a string if available, or null if not set.
*/
public function getRequestBody(): ?string public function getRequestBody(): ?string
{ {
return $this->requestBody; return $this->requestBody;
} }
/**
* Retrieves the name of the client.
*
* @return string|null Returns the client's name if set, or null if not available.
*/
public function getClientName(): ?string public function getClientName(): ?string
{ {
return $this->clientName; return $this->clientName;
} }
/**
* Retrieves the client version.
*
* @return string|null Returns the client version if available, or null if not set.
*/
public function getClientVersion(): ?string public function getClientVersion(): ?string
{ {
return $this->clientVersion; return $this->clientVersion;
} }
public function getRequestType(): RequestType /**
* Retrieves the request type associated with the current instance.
*
* @return RequestType|null Returns the associated RequestType if available, or null if not set.
*/
public function getRequestType(): ?RequestType
{ {
return $this->requestType; return $this->requestType;
} }
/**
* Retrieves the peer address the instance identifies as.
*
* @return PeerAddress|null Returns a PeerAddress instance if the identification address is set, or null otherwise.
*/
public function getIdentifyAs(): ?PeerAddress public function getIdentifyAs(): ?PeerAddress
{ {
if($this->identifyAs === null) if($this->identifyAs === null)
@ -100,11 +147,21 @@
return PeerAddress::fromAddress($this->identifyAs); return PeerAddress::fromAddress($this->identifyAs);
} }
/**
* Retrieves the UUID of the current session.
*
* @return string|null Returns the session UUID if available, or null if it is not set.
*/
public function getSessionUuid(): ?string public function getSessionUuid(): ?string
{ {
return $this->sessionUuid; return $this->sessionUuid;
} }
/**
* Retrieves the current session associated with the session UUID.
*
* @return SessionRecord|null Returns the associated SessionRecord if the session UUID exists, or null if no session UUID is set.
*/
public function getSession(): ?SessionRecord public function getSession(): ?SessionRecord
{ {
if($this->sessionUuid === null) if($this->sessionUuid === null)
@ -115,6 +172,11 @@
return SessionManager::getSession($this->sessionUuid); return SessionManager::getSession($this->sessionUuid);
} }
/**
* Retrieves the associated peer for the current session.
*
* @return RegisteredPeerRecord|null Returns the associated RegisteredPeerRecord if available, or null if no session exists.
*/
public function getPeer(): ?RegisteredPeerRecord public function getPeer(): ?RegisteredPeerRecord
{ {
$session = $this->getSession(); $session = $this->getSession();
@ -127,11 +189,22 @@
return RegisteredPeerManager::getPeer($session->getPeerUuid()); return RegisteredPeerManager::getPeer($session->getPeerUuid());
} }
/**
* Retrieves the signature value.
*
* @return string|null The signature value or null if not set
*/
public function getSignature(): ?string public function getSignature(): ?string
{ {
return $this->signature; return $this->signature;
} }
/**
* Verifies the signature of the provided decrypted content.
*
* @param string $decryptedContent The decrypted content to verify the signature against.
* @return bool True if the signature is valid, false otherwise.
*/
private function verifySignature(string $decryptedContent): bool private function verifySignature(string $decryptedContent): bool
{ {
if($this->getSignature() == null || $this->getSessionUuid() == null) if($this->getSignature() == null || $this->getSessionUuid() == null)
@ -141,7 +214,11 @@
try try
{ {
return Cryptography::verifyContent($decryptedContent, $this->getSignature(), $this->getSession()->getPublicKey(), true); return Cryptography::verifyMessage(
message: $decryptedContent,
signature: $this->getSignature(),
publicKey: $this->getSession()->getClientPublicSigningKey()
);
} }
catch(CryptographyException) catch(CryptographyException)
{ {
@ -156,52 +233,12 @@
* @return RpcRequest[] The parsed RpcRequest objects * @return RpcRequest[] The parsed RpcRequest objects
* @throws RequestException Thrown if the request is invalid * @throws RequestException Thrown if the request is invalid
*/ */
public function getRpcRequests(): array public function getRpcRequests(string $json): array
{ {
if($this->getSessionUuid() === null) $body = json_decode($json, true);
if($body === false)
{ {
throw new RequestException("Session UUID required", 400); throw new RequestException('Malformed JSON', 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 the body only contains a method, we assume it's a single request

View file

@ -1,41 +0,0 @@
<?php
namespace Socialbox\Objects\Database;
class DecryptedRecord
{
private string $key;
private string $pepper;
private string $salt;
public function __construct(array $data)
{
$this->key = $data['key'];
$this->pepper = $data['pepper'];
$this->salt = $data['salt'];
}
/**
* @return string
*/
public function getKey(): string
{
return $this->key;
}
/**
* @return string
*/
public function getPepper(): string
{
return $this->pepper;
}
/**
* @return string
*/
public function getSalt(): string
{
return $this->salt;
}
}

View file

@ -1,83 +0,0 @@
<?php
namespace Socialbox\Objects\Database;
use Socialbox\Classes\Configuration;
use Socialbox\Classes\SecuredPassword;
use Socialbox\Exceptions\CryptographyException;
class EncryptionRecord
{
private string $data;
private string $iv;
private string $tag;
/**
* Public constructor for the EncryptionRecord
*
* @param array $data
*/
public function __construct(array $data)
{
$this->data = $data['data'];
$this->iv = $data['iv'];
$this->tag = $data['tag'];
}
/**
* Retrieves the stored data.
*
* @return string The stored data.
*/
public function getData(): string
{
return $this->data;
}
/**
* Retrieves the initialization vector (IV).
*
* @return string The initialization vector.
*/
public function getIv(): string
{
return $this->iv;
}
/**
* Retrieves the tag.
*
* @return string The tag.
*/
public function getTag(): string
{
return $this->tag;
}
/**
* Decrypts the encrypted record using available encryption keys.
*
* Iterates through the configured encryption keys to attempt decryption of the data.
* If successful, returns a DecryptedRecord object with the decrypted data.
* Throws an exception if decryption fails with all available keys.
*
* @return DecryptedRecord The decrypted record containing the original data.
* @throws CryptographyException If decryption fails with all provided keys.
*/
public function decrypt(): DecryptedRecord
{
foreach(Configuration::getInstanceConfiguration()->getEncryptionKeys() as $encryptionKey)
{
$decryptedVault = openssl_decrypt(base64_decode($this->data), SecuredPassword::ENCRYPTION_ALGORITHM,
$encryptionKey, OPENSSL_RAW_DATA, base64_decode($this->iv), base64_decode($this->tag)
);
if ($decryptedVault !== false)
{
return new DecryptedRecord(json_decode($decryptedVault, true));
}
}
throw new CryptographyException("Decryption failed");
}
}

View file

@ -4,13 +4,14 @@ namespace Socialbox\Objects\Database;
use DateTime; use DateTime;
use Socialbox\Interfaces\SerializableInterface; use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Objects\ResolvedServer; use Socialbox\Objects\DnsRecord;
class ResolvedServerRecord implements SerializableInterface class ResolvedServerRecord implements SerializableInterface
{ {
private string $domain; private string $domain;
private string $endpoint; private string $endpoint;
private string $publicKey; private string $publicKey;
private DateTime $expires;
private DateTime $updated; private DateTime $updated;
/** /**
@ -25,10 +26,31 @@ class ResolvedServerRecord implements SerializableInterface
$this->endpoint = (string)$data['endpoint']; $this->endpoint = (string)$data['endpoint'];
$this->publicKey = (string)$data['public_key']; $this->publicKey = (string)$data['public_key'];
if(is_null($data['expires']))
{
$this->expires = new DateTime();
}
elseif (is_int($data['expires']))
{
$this->expires = (new DateTime())->setTimestamp($data['expires']);
}
elseif (is_string($data['expires']))
{
$this->expires = new DateTime($data['expires']);
}
else
{
$this->expires = $data['expires'];
}
if(is_null($data['updated'])) if(is_null($data['updated']))
{ {
$this->updated = new DateTime(); $this->updated = new DateTime();
} }
elseif (is_int($data['updated']))
{
$this->updated = (new DateTime())->setTimestamp($data['updated']);
}
elseif (is_string($data['updated'])) elseif (is_string($data['updated']))
{ {
$this->updated = new DateTime($data['updated']); $this->updated = new DateTime($data['updated']);
@ -40,8 +62,9 @@ class ResolvedServerRecord implements SerializableInterface
} }
/** /**
* Retrieves the domain value.
* *
* @return string The domain value. * @return string The domain as a string.
*/ */
public function getDomain(): string public function getDomain(): string
{ {
@ -49,8 +72,9 @@ class ResolvedServerRecord implements SerializableInterface
} }
/** /**
* Retrieves the configured endpoint.
* *
* @return string The endpoint value. * @return string The endpoint as a string.
*/ */
public function getEndpoint(): string public function getEndpoint(): string
{ {
@ -58,14 +82,25 @@ class ResolvedServerRecord implements SerializableInterface
} }
/** /**
* Retrieves the public key.
* *
* @return string The public key. * @return string The public key as a string.
*/ */
public function getPublicKey(): string public function getPublicKey(): string
{ {
return $this->publicKey; return $this->publicKey;
} }
/**
* Retrieves the expiration timestamp.
*
* @return DateTime The DateTime object representing the expiration time.
*/
public function getExpires(): DateTime
{
return $this->expires;
}
/** /**
* Retrieves the timestamp of the last update. * Retrieves the timestamp of the last update.
* *
@ -77,18 +112,17 @@ class ResolvedServerRecord implements SerializableInterface
} }
/** /**
* Converts the record to a ResolvedServer object. * Fetches the DNS record based on the provided endpoint, public key, and expiration time.
* *
* @return ResolvedServer The ResolvedServer object. * @return DnsRecord An instance of the DnsRecord containing the endpoint, public key, and expiration timestamp.
*/ */
public function toResolvedServer(): ResolvedServer public function getDnsRecord(): DnsRecord
{ {
return new ResolvedServer($this->endpoint, $this->publicKey); return new DnsRecord($this->endpoint, $this->publicKey, $this->expires->getTimestamp());
} }
/** /**
* @inheritDoc * @inheritDoc
* @throws \DateMalformedStringException
*/ */
public static function fromArray(array $data): object public static function fromArray(array $data): object
{ {

View file

@ -1,108 +0,0 @@
<?php
namespace Socialbox\Objects\Database;
use DateTime;
class SecurePasswordRecord
{
private string $peerUuid;
private string $iv;
private string $encryptedPassword;
private string $encryptedTag;
private DateTime $updated;
/**
* Constructor to initialize the object with provided data.
*
* @param array $data An associative array containing keys:
* - 'peer_uuid': The UUID of the peer.
* - 'iv': The initialization vector.
* - 'encrypted_password': The encrypted password.
* - 'encrypted_tag': The encrypted tag.
*
* @throws \DateMalformedStringException
*/
public function __construct(array $data)
{
$this->peerUuid = $data['peer_uuid'];
$this->iv = $data['iv'];
$this->encryptedPassword = $data['encrypted_password'];
$this->encryptedTag = $data['encrypted_tag'];
if($data['updated'] instanceof DateTime)
{
$this->updated = $data['updated'];
}
else
{
$this->updated = new DateTime($data['updated']);
}
}
/**
* Retrieves the UUID of the peer.
*
* @return string The UUID of the peer.
*/
public function getPeerUuid(): string
{
return $this->peerUuid;
}
/**
* Retrieves the initialization vector (IV) value.
*
* @return string The initialization vector.
*/
public function getIv(): string
{
return $this->iv;
}
/**
* Retrieves the encrypted password.
*
* @return string The encrypted password.
*/
public function getEncryptedPassword(): string
{
return $this->encryptedPassword;
}
/**
* Retrieves the encrypted tag.
*
* @return string The encrypted tag.
*/
public function getEncryptedTag(): string
{
return $this->encryptedTag;
}
/**
* Retrieves the updated timestamp.
*
* @return DateTime The updated timestamp.
*/
public function getUpdated(): DateTime
{
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);
}
}

View file

@ -15,9 +15,13 @@
private string $clientName; private string $clientName;
private string $clientVersion; private string $clientVersion;
private bool $authenticated; private bool $authenticated;
private string $publicKey; private string $clientPublicSigningKey;
public string $clientPublicEncryptionKey;
private string $serverPublicEncryptionKey;
private string $serverPrivateEncryptionKey;
private ?string $clientTransportEncryptionKey;
private ?string $serverTransportEncryptionKey;
private SessionState $state; private SessionState $state;
private ?string $encryptionKey;
/** /**
* @var SessionFlags[] * @var SessionFlags[]
*/ */
@ -42,10 +46,14 @@
$this->clientName = $data['client_name']; $this->clientName = $data['client_name'];
$this->clientVersion = $data['client_version']; $this->clientVersion = $data['client_version'];
$this->authenticated = $data['authenticated'] ?? false; $this->authenticated = $data['authenticated'] ?? false;
$this->publicKey = $data['public_key']; $this->clientPublicSigningKey = $data['client_public_signing_key'];
$this->clientPublicEncryptionKey = $data['client_public_encryption_key'];
$this->serverPublicEncryptionKey = $data['server_public_encryption_key'];
$this->serverPrivateEncryptionKey = $data['server_private_encryption_key'];
$this->clientTransportEncryptionKey = $data['client_transport_encryption_key'] ?? null;
$this->serverTransportEncryptionKey = $data['server_transport_encryption_key'] ?? null;
$this->created = $data['created']; $this->created = $data['created'];
$this->lastRequest = $data['last_request']; $this->lastRequest = $data['last_request'];
$this->encryptionKey = $data['encryption_key'] ?? null;
$this->flags = SessionFlags::fromString($data['flags']); $this->flags = SessionFlags::fromString($data['flags']);
if(SessionState::tryFrom($data['state']) == null) if(SessionState::tryFrom($data['state']) == null)
@ -99,9 +107,55 @@
* *
* @return string Returns the public key as a string. * @return string Returns the public key as a string.
*/ */
public function getPublicKey(): string public function getClientPublicSigningKey(): string
{ {
return $this->publicKey; return $this->clientPublicSigningKey;
}
/**
* Retrieves the encryption key associated with the instance.
*
* @return string|null Returns the encryption key as a string, or null if not set.
*/
public function getClientPublicEncryptionKey(): ?string
{
return $this->clientPublicEncryptionKey;
}
/**
* @return string
*/
public function getServerPublicEncryptionKey(): string
{
return $this->serverPublicEncryptionKey;
}
/**
* @return string
*/
public function getServerPrivateEncryptionKey(): string
{
return $this->serverPrivateEncryptionKey;
}
/**
* Retrieves the client encryption key associated with the instance.
*
* @return string|null Returns the client encryption key as a string, or null if not set.
*/
public function getClientTransportEncryptionKey(): ?string
{
return $this->clientTransportEncryptionKey;
}
/**
* Retrieves the server encryption key associated with the instance.
*
* @return string|null Returns the server encryption key as a string, or null if not set.
*/
public function getServerTransportEncryptionKey(): ?string
{
return $this->serverTransportEncryptionKey;
} }
/** /**
@ -114,16 +168,6 @@
return $this->state; 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. * Retrieves the creation date and time of the object.
* *
@ -194,6 +238,11 @@
return $this->clientVersion; return $this->clientVersion;
} }
/**
* Converts the current session state into a standard session state object.
*
* @return \Socialbox\Objects\Standard\SessionState The standardized session state object.
*/
public function toStandardSessionState(): \Socialbox\Objects\Standard\SessionState public function toStandardSessionState(): \Socialbox\Objects\Standard\SessionState
{ {
return new \Socialbox\Objects\Standard\SessionState([ return new \Socialbox\Objects\Standard\SessionState([
@ -207,10 +256,7 @@
/** /**
* Creates a new instance of the class using the provided array data. * @inheritDoc
*
* @param array $data An associative array of data used to initialize the object properties.
* @return object Returns a newly created object instance.
*/ */
public static function fromArray(array $data): object public static function fromArray(array $data): object
{ {
@ -218,10 +264,7 @@
} }
/** /**
* Converts the object's properties to an associative array. * @inheritDoc
*
* @return array An associative array representing the object's data, including keys 'uuid', 'peer_uuid',
* 'authenticated', 'public_key', 'state', 'flags', 'created', and 'last_request'.
*/ */
public function toArray(): array public function toArray(): array
{ {
@ -229,7 +272,12 @@
'uuid' => $this->uuid, 'uuid' => $this->uuid,
'peer_uuid' => $this->peerUuid, 'peer_uuid' => $this->peerUuid,
'authenticated' => $this->authenticated, 'authenticated' => $this->authenticated,
'public_key' => $this->publicKey, 'client_public_signing_key' => $this->clientPublicSigningKey,
'client_public_encryption_key' => $this->clientPublicEncryptionKey,
'server_public_encryption_key' => $this->serverPublicEncryptionKey,
'server_private_encryption_key' => $this->serverPrivateEncryptionKey,
'client_transport_encryption_key' => $this->clientTransportEncryptionKey,
'server_transport_encryption_key' => $this->serverTransportEncryptionKey,
'state' => $this->state->value, 'state' => $this->state->value,
'flags' => SessionFlags::toString($this->flags), 'flags' => SessionFlags::toString($this->flags),
'created' => $this->created, 'created' => $this->created,

View file

@ -0,0 +1,67 @@
<?php
namespace Socialbox\Objects;
class DnsRecord
{
private string $rpcEndpoint;
private string $publicSigningKey;
private int $expires;
/**
* Constructor for initializing the class with required parameters.
*
* @param string $rpcEndpoint The RPC endpoint.
* @param string $publicSigningKey The public signing key.
* @param int $expires The expiration time in seconds.
* @return void
*/
public function __construct(string $rpcEndpoint, string $publicSigningKey, int $expires)
{
$this->rpcEndpoint = $rpcEndpoint;
$this->publicSigningKey = $publicSigningKey;
$this->expires = $expires;
}
/**
* Retrieves the RPC endpoint.
*
* @return string The RPC endpoint.
*/
public function getRpcEndpoint(): string
{
return $this->rpcEndpoint;
}
/**
* Retrieves the public signing key.
*
* @return string Returns the public signing key as a string.
*/
public function getPublicSigningKey(): string
{
return $this->publicSigningKey;
}
/**
* Retrieves the expiration time.
*
* @return int The expiration timestamp as an integer.
*/
public function getExpires(): int
{
return $this->expires;
}
/**
* Creates a new instance of DnsRecord from the provided array of data.
*
* @param array $data An associative array containing the keys 'rpc_endpoint', 'public_key', and 'expires'
* required to instantiate a DnsRecord object.
* @return DnsRecord Returns a new DnsRecord instance populated with the data from the array.
*/
public static function fromArray(array $data): DnsRecord
{
return new DnsRecord($data['rpc_endpoint'], $data['public_key'], $data['expires']);
}
}

View file

@ -2,49 +2,63 @@
namespace Socialbox\Objects; namespace Socialbox\Objects;
use Socialbox\Interfaces\SerializableInterface;
/** /**
* Represents an exported session containing cryptographic keys, identifiers, and endpoints. * Represents an exported session containing cryptographic keys, identifiers, and endpoints.
*/ */
class ExportedSession class ExportedSession implements SerializableInterface
{ {
private string $peerAddress; private string $peerAddress;
private string $privateKey;
private string $publicKey;
private string $encryptionKey;
private string $serverPublicKey;
private string $rpcEndpoint; private string $rpcEndpoint;
private string $sessionUuid; private string $sessionUUID;
private string $transportEncryptionAlgorithm;
private int $serverKeypairExpires;
private string $serverPublicSigningKey;
private string $serverPublicEncryptionKey;
private string $clientPublicSigningKey;
private string $clientPrivateSigningKey;
private string $clientPublicEncryptionKey;
private string $clientPrivateEncryptionKey;
private string $privateSharedSecret;
private string $clientTransportEncryptionKey;
private string $serverTransportEncryptionKey;
/** /**
* Initializes a new instance of the class with the provided data. * Constructor method to initialize class properties from the provided data array.
* *
* @param array $data An associative array containing the configuration data. * @param array $data Associative array containing the required properties such as:
* Expected keys: * 'peer_address', 'rpc_endpoint', 'session_uuid',
* - 'peer_address': The address of the peer. * 'server_public_signing_key', 'server_public_encryption_key',
* - 'private_key': The private key for secure communication. * 'client_public_signing_key', 'client_private_signing_key',
* - 'public_key': The public key for secure communication. * 'client_public_encryption_key', 'client_private_encryption_key',
* - 'encryption_key': The encryption key used for communication. * 'private_shared_secret', 'client_transport_encryption_key',
* - 'server_public_key': The server's public key. * 'server_transport_encryption_key'.
* - 'rpc_endpoint': The RPC endpoint for network communication.
* - 'session_uuid': The unique identifier for the session.
* *
* @return void * @return void
*/ */
public function __construct(array $data) public function __construct(array $data)
{ {
$this->peerAddress = $data['peer_address']; $this->peerAddress = $data['peer_address'];
$this->privateKey = $data['private_key'];
$this->publicKey = $data['public_key'];
$this->encryptionKey = $data['encryption_key'];
$this->serverPublicKey = $data['server_public_key'];
$this->rpcEndpoint = $data['rpc_endpoint']; $this->rpcEndpoint = $data['rpc_endpoint'];
$this->sessionUuid = $data['session_uuid']; $this->sessionUUID = $data['session_uuid'];
$this->transportEncryptionAlgorithm = $data['transport_encryption_algorithm'];
$this->serverKeypairExpires = $data['server_keypair_expires'];
$this->serverPublicSigningKey = $data['server_public_signing_key'];
$this->serverPublicEncryptionKey = $data['server_public_encryption_key'];
$this->clientPublicSigningKey = $data['client_public_signing_key'];
$this->clientPrivateSigningKey = $data['client_private_signing_key'];
$this->clientPublicEncryptionKey = $data['client_public_encryption_key'];
$this->clientPrivateEncryptionKey = $data['client_private_encryption_key'];
$this->privateSharedSecret = $data['private_shared_secret'];
$this->clientTransportEncryptionKey = $data['client_transport_encryption_key'];
$this->serverTransportEncryptionKey = $data['server_transport_encryption_key'];
} }
/** /**
* Retrieves the address of the peer. * Retrieves the peer address associated with the current instance.
* *
* @return string The peer's address as a string. * @return string The peer address.
*/ */
public function getPeerAddress(): string public function getPeerAddress(): string
{ {
@ -52,47 +66,7 @@
} }
/** /**
* Retrieves the private key. * Retrieves the RPC endpoint.
*
* @return string The private key.
*/
public function getPrivateKey(): string
{
return $this->privateKey;
}
/**
* Retrieves the public key.
*
* @return string The public key.
*/
public function getPublicKey(): string
{
return $this->publicKey;
}
/**
* Retrieves the encryption key.
*
* @return string The encryption key.
*/
public function getEncryptionKey(): string
{
return $this->encryptionKey;
}
/**
* Retrieves the public key of the server.
*
* @return string The server's public key.
*/
public function getServerPublicKey(): string
{
return $this->serverPublicKey;
}
/**
* Retrieves the RPC endpoint URL.
* *
* @return string The RPC endpoint. * @return string The RPC endpoint.
*/ */
@ -102,38 +76,150 @@
} }
/** /**
* Retrieves the unique identifier for the current session. * Retrieves the session UUID associated with the current instance.
* *
* @return string The session UUID. * @return string The session UUID.
*/ */
public function getSessionUuid(): string public function getSessionUUID(): string
{ {
return $this->sessionUuid; return $this->sessionUUID;
} }
/** /**
* Converts the current instance into an array representation. * Retrieves the transport encryption algorithm being used.
* *
* @return array An associative array containing the instance properties and their respective values. * @return string The transport encryption algorithm.
*/
public function getTransportEncryptionAlgorithm(): string
{
return $this->transportEncryptionAlgorithm;
}
/**
* Retrieves the expiration time of the server key pair.
*
* @return int The expiration timestamp of the server key pair.
*/
public function getServerKeypairExpires(): int
{
return $this->serverKeypairExpires;
}
/**
* Retrieves the public signing key of the server.
*
* @return string The server's public signing key.
*/
public function getServerPublicSigningKey(): string
{
return $this->serverPublicSigningKey;
}
/**
* Retrieves the server's public encryption key.
*
* @return string The server's public encryption key.
*/
public function getServerPublicEncryptionKey(): string
{
return $this->serverPublicEncryptionKey;
}
/**
* Retrieves the client's public signing key.
*
* @return string The client's public signing key.
*/
public function getClientPublicSigningKey(): string
{
return $this->clientPublicSigningKey;
}
/**
* Retrieves the client's private signing key.
*
* @return string The client's private signing key.
*/
public function getClientPrivateSigningKey(): string
{
return $this->clientPrivateSigningKey;
}
/**
* Retrieves the public encryption key of the client.
*
* @return string The client's public encryption key.
*/
public function getClientPublicEncryptionKey(): string
{
return $this->clientPublicEncryptionKey;
}
/**
* Retrieves the client's private encryption key.
*
* @return string The client's private encryption key.
*/
public function getClientPrivateEncryptionKey(): string
{
return $this->clientPrivateEncryptionKey;
}
/**
* Retrieves the private shared secret associated with the current instance.
*
* @return string The private shared secret.
*/
public function getPrivateSharedSecret(): string
{
return $this->privateSharedSecret;
}
/**
* Retrieves the client transport encryption key.
*
* @return string The client transport encryption key.
*/
public function getClientTransportEncryptionKey(): string
{
return $this->clientTransportEncryptionKey;
}
/**
* Retrieves the server transport encryption key associated with the current instance.
*
* @return string The server transport encryption key.
*/
public function getServerTransportEncryptionKey(): string
{
return $this->serverTransportEncryptionKey;
}
/**
* @inheritDoc
*/ */
public function toArray(): array public function toArray(): array
{ {
return [ return [
'peer_address' => $this->peerAddress, 'peer_address' => $this->peerAddress,
'private_key' => $this->privateKey,
'public_key' => $this->publicKey,
'encryption_key' => $this->encryptionKey,
'server_public_key' => $this->serverPublicKey,
'rpc_endpoint' => $this->rpcEndpoint, 'rpc_endpoint' => $this->rpcEndpoint,
'session_uuid' => $this->sessionUuid 'session_uuid' => $this->sessionUUID,
'transport_encryption_algorithm' => $this->transportEncryptionAlgorithm,
'server_keypair_expires' => $this->serverKeypairExpires,
'server_public_signing_key' => $this->serverPublicSigningKey,
'server_public_encryption_key' => $this->serverPublicEncryptionKey,
'client_public_signing_key' => $this->clientPublicSigningKey,
'client_private_signing_key' => $this->clientPrivateSigningKey,
'client_public_encryption_key' => $this->clientPublicEncryptionKey,
'client_private_encryption_key' => $this->clientPrivateEncryptionKey,
'private_shared_secret' => $this->privateSharedSecret,
'client_transport_encryption_key' => $this->clientTransportEncryptionKey,
'server_transport_encryption_key' => $this->serverTransportEncryptionKey,
]; ];
} }
/** /**
* Creates an instance of ExportedSession from the provided array. * @inheritDoc
*
* @param array $data The input data used to construct the ExportedSession instance.
* @return ExportedSession The new ExportedSession instance created from the given data.
*/ */
public static function fromArray(array $data): ExportedSession public static function fromArray(array $data): ExportedSession
{ {

View file

@ -2,24 +2,10 @@
namespace Socialbox\Objects; namespace Socialbox\Objects;
use Socialbox\Objects\Standard\ServerInformation;
class ResolvedServer class ResolvedServer
{ {
private string $endpoint; private DnsRecord $dnsRecord;
private string $publicKey; private ServerInformation $serverInformation;
public function __construct(string $endpoint, string $publicKey)
{
$this->endpoint = $endpoint;
$this->publicKey = $publicKey;
}
public function getEndpoint(): string
{
return $this->endpoint;
}
public function getPublicKey(): string
{
return $this->publicKey;
}
} }

View file

@ -0,0 +1,25 @@
<?php
namespace Socialbox\Objects;
class ResolvedServer
{
private string $endpoint;
private string $publicKey;
public function __construct(string $endpoint, string $publicKey)
{
$this->endpoint = $endpoint;
$this->publicKey = $publicKey;
}
public function getEndpoint(): string
{
return $this->endpoint;
}
public function getPublicKey(): string
{
return $this->publicKey;
}
}

View file

@ -0,0 +1,75 @@
<?php
namespace Socialbox\Objects\Standard;
use Socialbox\Interfaces\SerializableInterface;
class ServerInformation implements SerializableInterface
{
private string $serverName;
private int $serverKeypairExpires;
private string $transportEncryptionAlgorithm;
/**
* Constructor method to initialize the object with provided data.
*
* @param array $data The array containing initialization parameters, including 'server_name', 'server_keypair_expires', and 'transport_encryption_algorithm'.
* @return void
*/
public function __construct(array $data)
{
$this->serverName = $data['server_name'];
$this->serverKeypairExpires = $data['server_keypair_expires'];
$this->transportEncryptionAlgorithm = $data['transport_encryption_algorithm'];
}
/**
* Retrieves the name of the server.
*
* @return string The server name.
*/
public function getServerName(): string
{
return $this->serverName;
}
/**
* Retrieves the expiration time of the server key pair.
*
* @return int The expiration timestamp of the server key pair.
*/
public function getServerKeypairExpires(): int
{
return $this->serverKeypairExpires;
}
/**
* Retrieves the transport encryption algorithm being used.
*
* @return string The transport encryption algorithm.
*/
public function getTransportEncryptionAlgorithm(): string
{
return $this->transportEncryptionAlgorithm;
}
/**
* @inheritDoc
*/
public static function fromArray(array $data): ServerInformation
{
return new self($data);
}
/**
* @inheritDoc
*/
public function toArray(): array
{
return [
'server_name' => $this->serverName,
'server_keypair_expires' => $this->serverKeypairExpires,
'transport_encryption_algorithm' => $this->transportEncryptionAlgorithm,
];
}
}

View file

@ -6,6 +6,7 @@
use InvalidArgumentException; use InvalidArgumentException;
use Socialbox\Classes\Configuration; use Socialbox\Classes\Configuration;
use Socialbox\Classes\Cryptography; use Socialbox\Classes\Cryptography;
use Socialbox\Classes\DnsHelper;
use Socialbox\Classes\Logger; use Socialbox\Classes\Logger;
use Socialbox\Classes\ServerResolver; use Socialbox\Classes\ServerResolver;
use Socialbox\Classes\Utilities; use Socialbox\Classes\Utilities;
@ -16,6 +17,7 @@
use Socialbox\Enums\StandardHeaders; use Socialbox\Enums\StandardHeaders;
use Socialbox\Enums\StandardMethods; use Socialbox\Enums\StandardMethods;
use Socialbox\Enums\Types\RequestType; use Socialbox\Enums\Types\RequestType;
use Socialbox\Exceptions\CryptographyException;
use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\RequestException; use Socialbox\Exceptions\RequestException;
use Socialbox\Exceptions\StandardException; use Socialbox\Exceptions\StandardException;
@ -23,25 +25,24 @@
use Socialbox\Managers\SessionManager; use Socialbox\Managers\SessionManager;
use Socialbox\Objects\ClientRequest; use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\PeerAddress; use Socialbox\Objects\PeerAddress;
use Socialbox\Objects\Standard\ServerInformation;
use Throwable;
class Socialbox class Socialbox
{ {
/** /**
* Handles incoming client requests by validating required headers and processing * Handles incoming client requests by parsing request headers, determining the request type,
* the request based on its type. The method ensures proper handling of * and routing the request to the appropriate handler method. Implements error handling for
* specific request types like RPC, session initiation, and DHE exchange, * missing or invalid request types.
* while returning an appropriate HTTP response for invalid or missing data.
* *
* @return void * @return void
*/ */
public static function handleRequest(): void public static function handleRequest(): void
{ {
$requestHeaders = Utilities::getRequestHeaders(); $requestHeaders = Utilities::getRequestHeaders();
if(!isset($requestHeaders[StandardHeaders::REQUEST_TYPE->value])) if(!isset($requestHeaders[StandardHeaders::REQUEST_TYPE->value]))
{ {
http_response_code(400); self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::REQUEST_TYPE->value);
print('Missing required header: ' . StandardHeaders::REQUEST_TYPE->value);
return; return;
} }
@ -49,8 +50,12 @@
// Handle the request type, only `init` and `dhe` are not encrypted using the session's encrypted key // 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. // RPC Requests must be encrypted and signed by the client, vice versa for server responses.
switch(RequestType::tryFrom($clientRequest->getHeader(StandardHeaders::REQUEST_TYPE))) switch($clientRequest->getRequestType())
{ {
case RequestType::INFO:
self::handleInformationRequest();
break;
case RequestType::INITIATE_SESSION: case RequestType::INITIATE_SESSION:
self::handleInitiateSession($clientRequest); self::handleInitiateSession($clientRequest);
break; break;
@ -64,58 +69,66 @@
break; break;
default: default:
http_response_code(400); self::returnError(400, StandardError::BAD_REQUEST, 'Invalid Request-Type header');
print('Invalid Request-Type header');
break;
} }
} }
/** /**
* Validates the headers in an initialization request to ensure that all * Handles an information request by setting the appropriate HTTP response code,
* required information is present and properly formatted. This includes * content type headers, and printing the server information in JSON format.
* checking for headers such as Client Name, Client Version, Public Key,
* and Identify-As, as well as validating the Identify-As header value.
* If any validation fails, a corresponding HTTP response code and message
* are returned.
* *
* @param ClientRequest $clientRequest The client request containing headers to validate. * @return void
*/
private static function handleInformationRequest(): void
{
http_response_code(200);
header('Content-Type: application/json');
Logger::getLogger()->info(json_encode(self::getServerInformation()->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
print(json_encode(self::getServerInformation()->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
}
/**
* Validates the initial headers of a client request to ensure all required headers exist
* and contain valid values. If any validation fails, an error response is returned.
* *
* @param ClientRequest $clientRequest The client request containing headers to be validated.
* @return bool Returns true if all required headers are valid, otherwise false. * @return bool Returns true if all required headers are valid, otherwise false.
*/ */
private static function validateInitHeaders(ClientRequest $clientRequest): bool private static function validateInitHeaders(ClientRequest $clientRequest): bool
{ {
if(!$clientRequest->getClientName()) if(!$clientRequest->getClientName())
{ {
http_response_code(400); self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::CLIENT_NAME->value);
print('Missing required header: ' . StandardHeaders::CLIENT_NAME->value);
return false; return false;
} }
if(!$clientRequest->getClientVersion()) if(!$clientRequest->getClientVersion())
{ {
http_response_code(400); self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::CLIENT_VERSION->value);
print('Missing required header: ' . StandardHeaders::CLIENT_VERSION->value);
return false; return false;
} }
if(!$clientRequest->headerExists(StandardHeaders::PUBLIC_KEY)) if(!$clientRequest->headerExists(StandardHeaders::SIGNING_PUBLIC_KEY))
{ {
http_response_code(400); self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::SIGNING_PUBLIC_KEY->value);
print('Missing required header: ' . StandardHeaders::PUBLIC_KEY->value); return false;
}
if(!$clientRequest->headerExists(StandardHeaders::ENCRYPTION_PUBLIC_KEY))
{
self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::ENCRYPTION_PUBLIC_KEY->value);
return false; return false;
} }
if(!$clientRequest->headerExists(StandardHeaders::IDENTIFY_AS)) if(!$clientRequest->headerExists(StandardHeaders::IDENTIFY_AS))
{ {
http_response_code(400); self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::IDENTIFY_AS->value);
print('Missing required header: ' . StandardHeaders::IDENTIFY_AS->value);
return false; return false;
} }
if(!Validator::validatePeerAddress($clientRequest->getHeader(StandardHeaders::IDENTIFY_AS))) if(!Validator::validatePeerAddress($clientRequest->getHeader(StandardHeaders::IDENTIFY_AS)))
{ {
http_response_code(400); self::returnError(400, StandardError::BAD_REQUEST, 'Invalid Identify-As header: ' . $clientRequest->getHeader(StandardHeaders::IDENTIFY_AS));
print('Invalid Identify-As header: ' . $clientRequest->getHeader(StandardHeaders::IDENTIFY_AS));
return false; return false;
} }
@ -123,24 +136,25 @@
} }
/** /**
* Processes a client request to initiate a session. Validates required headers, * Handles the initiation of a session for a client request. This involves validating headers,
* ensures the peer is authorized and enabled, and creates a new session UUID * verifying peer identities, resolving domains, registering peers if necessary, and finally
* if all checks pass. Handles edge cases like missing headers, invalid inputs, * creating a session while providing the required session UUID as a response.
* or unauthorized peers.
* *
* @param ClientRequest $clientRequest The request from the client containing * @param ClientRequest $clientRequest The incoming client request containing all necessary headers
* the required headers and information. * and identification information required to initiate the session.
* @return void * @return void
*/ */
private static function handleInitiateSession(ClientRequest $clientRequest): void private static function handleInitiateSession(ClientRequest $clientRequest): void
{ {
// This is only called for the `init` request type
if(!self::validateInitHeaders($clientRequest)) if(!self::validateInitHeaders($clientRequest))
{ {
return; return;
} }
// We always accept the client's public key at first // We always accept the client's public key at first
$publicKey = $clientRequest->getHeader(StandardHeaders::PUBLIC_KEY); $clientPublicSigningKey = $clientRequest->getHeader(StandardHeaders::SIGNING_PUBLIC_KEY);
$clientPublicEncryptionKey = $clientRequest->getHeader(StandardHeaders::ENCRYPTION_PUBLIC_KEY);
// If the peer is identifying as the same domain // If the peer is identifying as the same domain
if($clientRequest->getIdentifyAs()->getDomain() === Configuration::getInstanceConfiguration()->getDomain()) if($clientRequest->getIdentifyAs()->getDomain() === Configuration::getInstanceConfiguration()->getDomain())
@ -148,8 +162,7 @@
// Prevent the peer from identifying as the host unless it's coming from an external domain // Prevent the peer from identifying as the host unless it's coming from an external domain
if($clientRequest->getIdentifyAs()->getUsername() === ReservedUsernames::HOST->value) if($clientRequest->getIdentifyAs()->getUsername() === ReservedUsernames::HOST->value)
{ {
http_response_code(403); self::returnError(403, StandardError::FORBIDDEN, 'Unauthorized: Not allowed to identify as the host');
print('Unauthorized: The requested peer is not allowed to identify as the host');
return; return;
} }
} }
@ -159,64 +172,49 @@
// Only allow the host to identify as an external peer // Only allow the host to identify as an external peer
if($clientRequest->getIdentifyAs()->getUsername() !== ReservedUsernames::HOST->value) if($clientRequest->getIdentifyAs()->getUsername() !== ReservedUsernames::HOST->value)
{ {
http_response_code(403); self::returnError(403, StandardError::FORBIDDEN, 'Forbidden: Any external peer must identify as the host, only the host can preform actions on behalf of it\'s peers');
print('Unauthorized: The requested peer is not allowed to identify as an external peer');
return; return;
} }
try try
{ {
// We need to obtain the public key of the host, since we can't trust the client // We need to obtain the public key of the host, since we can't trust the client (Use database)
$resolvedServer = ServerResolver::resolveDomain($clientRequest->getIdentifyAs()->getDomain()); $resolvedServer = ServerResolver::resolveDomain($clientRequest->getIdentifyAs()->getDomain());
// Override the public key with the resolved server's public key // Override the public signing key with the resolved server's public key
$publicKey = $resolvedServer->getPublicKey(); // Encryption key can be left as is.
} $clientPublicSigningKey = $resolvedServer->getPublicSigningKey();
catch (Exceptions\ResolutionException $e)
{
Logger::getLogger()->error('Failed to resolve the host domain', $e);
http_response_code(409);
print('Conflict: Failed to resolve the host domain: ' . $e->getMessage());
return;
} }
catch (Exception $e) catch (Exception $e)
{ {
Logger::getLogger()->error('An internal error occurred while resolving the host domain', $e); self::returnError(502, StandardError::RESOLUTION_FAILED, 'Conflict: Failed to resolve the host domain: ' . $e->getMessage(), $e);
http_response_code(500);
if(Configuration::getSecurityConfiguration()->isDisplayInternalExceptions())
{
print(Utilities::throwableToString($e));
}
else
{
print('An internal error occurred');
}
return; return;
} }
} }
try try
{ {
// Check if we have a registered peer with the same address
$registeredPeer = RegisteredPeerManager::getPeerByAddress($clientRequest->getIdentifyAs()); $registeredPeer = RegisteredPeerManager::getPeerByAddress($clientRequest->getIdentifyAs());
// If the peer is registered, check if it is enabled // If the peer is registered, check if it is enabled
if($registeredPeer !== null && !$registeredPeer->isEnabled()) if($registeredPeer !== null && !$registeredPeer->isEnabled())
{ {
// Refuse to create a session if the peer is disabled/banned // Refuse to create a session if the peer is disabled/banned, this usually happens when
// This also prevents multiple sessions from being created for the same peer // a peer gets banned or more commonly when a client attempts to register as this peer but
// A cron job should be used to clean up disabled peers // destroyed the session before it was created.
http_response_code(403); // This is to prevent multiple sessions from being created for the same peer, this is cleaned up
print('Unauthorized: The requested peer is disabled/banned'); // with a cron job using `socialbox clean-sessions`
self::returnError(403, StandardError::FORBIDDEN, 'Unauthorized: The requested peer is disabled/banned');
return; return;
} }
// Otherwise the peer isn't registered, so we need to register it
else else
{ {
// Check if registration is enabled // Check if registration is enabled
if(!Configuration::getRegistrationConfiguration()->isRegistrationEnabled()) if(!Configuration::getRegistrationConfiguration()->isRegistrationEnabled())
{ {
http_response_code(403); self::returnError(401, StandardError::UNAUTHORIZED, 'Unauthorized: Registration is disabled');
print('Unauthorized: Registration is disabled');
return; return;
} }
@ -226,141 +224,220 @@
$registeredPeer = RegisteredPeerManager::getPeer($peerUuid); $registeredPeer = RegisteredPeerManager::getPeer($peerUuid);
} }
// Create the session UUID // Generate server's encryption keys for this session
$sessionUuid = SessionManager::createSession($publicKey, $registeredPeer, $clientRequest->getClientName(), $clientRequest->getClientVersion()); $serverEncryptionKey = Cryptography::generateEncryptionKeyPair();
// Create the session passing on the registered peer, client name, version, and public keys
$sessionUuid = SessionManager::createSession($registeredPeer, $clientRequest->getClientName(), $clientRequest->getClientVersion(), $clientPublicSigningKey, $clientPublicEncryptionKey, $serverEncryptionKey);
// The server responds back with the session UUID & The server's public encryption key as the header
http_response_code(201); // Created http_response_code(201); // Created
header('Content-Type: text/plain');
header(StandardHeaders::ENCRYPTION_PUBLIC_KEY->value . ': ' . $serverEncryptionKey->getPublicKey());
print($sessionUuid); // Return the session UUID print($sessionUuid); // Return the session UUID
} }
catch(InvalidArgumentException $e) catch(InvalidArgumentException $e)
{ {
http_response_code(412); // Precondition failed // This is usually thrown due to an invalid input
print($e->getMessage()); // Why the request failed self::returnError(400, StandardError::BAD_REQUEST, $e->getMessage(), $e);
} }
catch(Exception $e) catch(Exception $e)
{ {
Logger::getLogger()->error('An internal error occurred while initiating the session', $e); self::returnError(500, StandardError::INTERNAL_SERVER_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 * Handles the Diffie-Hellman Ephemeral (DHE) key exchange process between the client and server,
* the server's private key and setting the encryption key to the session. * ensuring secure transport encryption key negotiation. The method validates request headers,
* session state, and cryptographic operations, and updates the session with the resulting keys
* and state upon successful negotiation.
* *
* 412: Headers malformed * @param ClientRequest $clientRequest The request object containing headers, body, and session details
* 400: Bad request * required to perform the DHE exchange.
* 500: Internal server error
* 204: Success, no content.
* *
* @param ClientRequest $clientRequest
* @return void * @return void
*/ */
private static function handleDheExchange(ClientRequest $clientRequest): void private static function handleDheExchange(ClientRequest $clientRequest): void
{ {
// Check if the session UUID is set in the headers // Check if the session UUID is set in the headers, bad request if not
if(!$clientRequest->headerExists(StandardHeaders::SESSION_UUID)) if(!$clientRequest->headerExists(StandardHeaders::SESSION_UUID))
{ {
Logger::getLogger()->verbose('Missing required header: ' . StandardHeaders::SESSION_UUID->value); self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::SESSION_UUID->value);
http_response_code(412);
print('Missing required header: ' . StandardHeaders::SESSION_UUID->value);
return; return;
} }
// Check if the request body is empty if(!$clientRequest->headerExists(StandardHeaders::SIGNATURE))
{
self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::SIGNATURE->value);
return;
}
if(empty($clientRequest->getHeader(StandardHeaders::SIGNATURE)))
{
self::returnError(400, StandardError::BAD_REQUEST, 'Bad request: The signature is empty');
return;
}
// Check if the request body is empty, bad request if so
if(empty($clientRequest->getRequestBody())) if(empty($clientRequest->getRequestBody()))
{ {
Logger::getLogger()->verbose('Bad request: The key exchange request body is empty'); self::returnError(400, StandardError::BAD_REQUEST, 'Bad request: The key exchange request body is empty');
http_response_code(400);
print('Bad request: The key exchange request body is empty');
return; return;
} }
// Check if the session is awaiting a DHE exchange // Check if the session is awaiting a DHE exchange, forbidden if not
if($clientRequest->getSession()->getState() !== SessionState::AWAITING_DHE) $session = $clientRequest->getSession();
if($session->getState() !== SessionState::AWAITING_DHE)
{ {
Logger::getLogger()->verbose('Bad request: The session is not awaiting a DHE exchange'); self::returnError(403, StandardError::FORBIDDEN, '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; return;
} }
// DHE STAGE: CLIENT -> SERVER
// Server & Client: Begin the DHE exchange using the exchanged public keys.
// On the client's side, same method but with the server's public key & client's private key
try try
{ {
// Attempt to decrypt the encrypted key passed on from the client $sharedSecret = Cryptography::performDHE($session->getClientPublicEncryptionKey(), $session->getServerPrivateEncryptionKey());
$encryptionKey = Cryptography::decryptContent($clientRequest->getRequestBody(), Configuration::getInstanceConfiguration()->getPrivateKey());
} }
catch (Exceptions\CryptographyException $e) catch (CryptographyException $e)
{ {
Logger::getLogger()->error(sprintf('Bad Request: Failed to decrypt the key for session %s', $clientRequest->getSessionUuid()), $e); Logger::getLogger()->error('Failed to perform DHE exchange', $e);
self::returnError(422, StandardError::CRYPTOGRAPHIC_ERROR, 'DHE exchange failed', $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; return;
} }
// STAGE 1: CLIENT -> SERVER
try try
{ {
// Finally set the encryption key to the session // Attempt to decrypt the encrypted key passed on from the client using the shared secret
SessionManager::setEncryptionKey($clientRequest->getSessionUuid(), $encryptionKey); $clientTransportEncryptionKey = Cryptography::decryptShared($clientRequest->getRequestBody(), $sharedSecret);
}
catch (CryptographyException $e)
{
self::returnError(400, StandardError::CRYPTOGRAPHIC_ERROR, 'Failed to decrypt the key', $e);
return;
}
// Get the signature from the client and validate it against the decrypted key
$clientSignature = $clientRequest->getHeader(StandardHeaders::SIGNATURE);
if(!Cryptography::verifyMessage($clientTransportEncryptionKey, $clientSignature, $session->getClientPublicSigningKey()))
{
self::returnError(401, StandardError::UNAUTHORIZED, 'Invalid signature');
return;
}
// Validate the encryption key given by the client
if(!Cryptography::validateEncryptionKey($clientTransportEncryptionKey, Configuration::getCryptographyConfiguration()->getTransportEncryptionAlgorithm()))
{
self::returnError(400, StandardError::BAD_REQUEST, 'The transport encryption key is invalid and does not meet the server\'s requirements');
return;
}
// Receive stage complete, now we move on to the server's response
// STAGE 2: SERVER -> CLIENT
try
{
// Generate the server's transport encryption key (our side)
$serverTransportEncryptionKey = Cryptography::generateEncryptionKey(Configuration::getCryptographyConfiguration()->getTransportEncryptionAlgorithm());
// Sign the shared secret using the server's private key
$signature = Cryptography::signMessage($serverTransportEncryptionKey, Configuration::getCryptographyConfiguration()->getHostPrivateKey());
// Encrypt the server's transport key using the shared secret
$encryptedServerTransportKey = Cryptography::encryptShared($serverTransportEncryptionKey, $sharedSecret);
}
catch (CryptographyException $e)
{
Logger::getLogger()->error('Failed to generate the server\'s transport encryption key', $e);
self::returnError(500, StandardError::INTERNAL_SERVER_ERROR, 'There was an error while trying to process the DHE exchange', $e);
return;
}
// Now update the session details with all the encryption keys and the state
try
{
SessionManager::setEncryptionKeys($clientRequest->getSessionUuid(), $sharedSecret, $clientTransportEncryptionKey, $serverTransportEncryptionKey);
SessionManager::updateState($clientRequest->getSessionUuid(), SessionState::ACTIVE);
} }
catch (DatabaseOperationException $e) catch (DatabaseOperationException $e)
{ {
Logger::getLogger()->error('Failed to set the encryption key for the session', $e); Logger::getLogger()->error('Failed to set the encryption key for the session', $e);
http_response_code(500); self::returnError(500, StandardError::INTERNAL_SERVER_ERROR, 'Failed to set the encryption key for the session', $e);
if(Configuration::getSecurityConfiguration()->isDisplayInternalExceptions())
{
print(Utilities::throwableToString($e));
}
else
{
print('Internal Server Error: Failed to set the encryption key for the session');
}
return; return;
} }
Logger::getLogger()->info(sprintf('DHE exchange completed for session %s', $clientRequest->getSessionUuid())); // Return the encrypted transport key for the server back to the client.
http_response_code(204); // Success, no content http_response_code(200);
header('Content-Type: application/octet-stream');
header(StandardHeaders::SIGNATURE->value . ': ' . $signature);
print($encryptedServerTransportKey);
} }
/** /**
* Handles incoming RPC requests from a client, processes each request, * Handles a Remote Procedure Call (RPC) request, ensuring proper decryption,
* and returns the appropriate response(s) or error(s). * signature verification, and response encryption, while processing one or more
* RPC methods as specified in the request.
*
* @param ClientRequest $clientRequest The RPC client request containing headers, body, and session information.
* *
* @param ClientRequest $clientRequest The client's request containing one or multiple RPC calls.
* @return void * @return void
*/ */
private static function handleRpc(ClientRequest $clientRequest): void private static function handleRpc(ClientRequest $clientRequest): void
{ {
// Client: Encrypt the request body using the server's encryption key & sign it using the client's private key
// Server: Decrypt the request body using the servers's encryption key & verify the signature using the client's public key
// Server: Encrypt the response using the client's encryption key & sign it using the server's private key
if(!$clientRequest->headerExists(StandardHeaders::SESSION_UUID)) if(!$clientRequest->headerExists(StandardHeaders::SESSION_UUID))
{ {
Logger::getLogger()->verbose('Missing required header: ' . StandardHeaders::SESSION_UUID->value); self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::SESSION_UUID->value);
return;
}
http_response_code(412); if(!$clientRequest->headerExists(StandardHeaders::SIGNATURE))
print('Missing required header: ' . StandardHeaders::SESSION_UUID->value); {
self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::SIGNATURE->value);
return;
}
// Get the client session
$session = $clientRequest->getSession();
// Verify if the session is active
if($session->getState() !== SessionState::ACTIVE)
{
self::returnError(403, StandardError::FORBIDDEN, 'Session is not active');
return; return;
} }
try try
{ {
$clientRequests = $clientRequest->getRpcRequests(); // Attempt to decrypt the request body using the server's encryption key
$decryptedContent = Cryptography::decryptMessage($clientRequest->getRequestBody(), $session->getServerTransportEncryptionKey(), Configuration::getCryptographyConfiguration()->getTransportEncryptionAlgorithm());
}
catch(CryptographyException $e)
{
self::returnError(400, StandardError::CRYPTOGRAPHIC_ERROR, 'Failed to decrypt request', $e);
return;
}
// Attempt to verify the decrypted content using the client's public signing key
if(!Cryptography::verifyMessage($decryptedContent, $clientRequest->getSignature(), $session->getClientPublicSigningKey()))
{
self::returnError(400, StandardError::CRYPTOGRAPHIC_ERROR, 'Signature verification failed');
return;
}
try
{
$clientRequests = $clientRequest->getRpcRequests($decryptedContent);
} }
catch (RequestException $e) catch (RequestException $e)
{ {
http_response_code($e->getCode()); self::returnError($e->getCode(), $e->getStandardError(), $e->getMessage());
print($e->getMessage());
return; return;
} }
@ -442,16 +519,24 @@
return; return;
} }
$session = $clientRequest->getSession();
try try
{ {
$encryptedResponse = Cryptography::encryptTransport($response, $clientRequest->getSession()->getEncryptionKey()); $encryptedResponse = Cryptography::encryptMessage(
$signature = Cryptography::signContent($response, Configuration::getInstanceConfiguration()->getPrivateKey(), true); message: $response,
encryptionKey: $session->getClientTransportEncryptionKey(),
algorithm: Configuration::getCryptographyConfiguration()->getTransportEncryptionAlgorithm()
);
$signature = Cryptography::signMessage(
message: $response,
privateKey: Configuration::getCryptographyConfiguration()->getHostPrivateKey()
);
} }
catch (Exceptions\CryptographyException $e) catch (Exceptions\CryptographyException $e)
{ {
Logger::getLogger()->error('Failed to encrypt the response', $e); self::returnError(500, StandardError::INTERNAL_SERVER_ERROR, 'Failed to encrypt the server response', $e);
http_response_code(500);
print('Internal Server Error: Failed to encrypt the response');
return; return;
} }
@ -460,4 +545,69 @@
header(StandardHeaders::SIGNATURE->value . ': ' . $signature); header(StandardHeaders::SIGNATURE->value . ': ' . $signature);
print($encryptedResponse); print($encryptedResponse);
} }
/**
* Sends an error response by setting the HTTP response code, headers, and printing an error message.
* Optionally includes exception details in the response if enabled in the configuration.
* Logs the error message and any associated exception.
*
* @param int $responseCode The HTTP response code to send.
* @param StandardError $standardError The standard error containing error details.
* @param string|null $message An optional error message to display. Defaults to the message from the StandardError instance.
* @param Throwable|null $e An optional throwable to include in logs and the response, if enabled.
*
* @return void
*/
private static function returnError(int $responseCode, StandardError $standardError, ?string $message=null, ?Throwable $e=null): void
{
if($message === null)
{
$message = $standardError->getMessage();
}
http_response_code($responseCode);
header('Content-Type: text/plain');
header(StandardHeaders::ERROR_CODE->value . ': ' . $standardError->value);
print($message);
if(Configuration::getSecurityConfiguration()->isDisplayInternalExceptions() && $e !== null)
{
print(PHP_EOL . PHP_EOL . Utilities::throwableToString($e));
}
if($e !== null)
{
Logger::getLogger()->error($message, $e);
}
}
/**
* Retrieves the server information by assembling data from the configuration settings.
*
* @return ServerInformation An instance of ServerInformation containing details such as server name, hashing algorithm,
* transport AES mode, and AES key length.
*/
public static function getServerInformation(): ServerInformation
{
return ServerInformation::fromArray([
'server_name' => Configuration::getInstanceConfiguration()->getName(),
'server_keypair_expires' => Configuration::getCryptographyConfiguration()->getHostKeyPairExpires(),
'transport_encryption_algorithm' => Configuration::getCryptographyConfiguration()->getTransportEncryptionAlgorithm()
]);
}
/**
* Retrieves the DNS record by generating a TXT record using the RPC endpoint,
* host public key, and host key pair expiration from the configuration.
*
* @return string The generated DNS TXT record.
*/
public static function getDnsRecord(): string
{
return DnsHelper::generateTxt(
Configuration::getInstanceConfiguration()->getRpcEndpoint(),
Configuration::getCryptographyConfiguration()->getHostPublicKey(),
Configuration::getCryptographyConfiguration()->getHostKeyPairExpires()
);
}
} }

View file

@ -0,0 +1,439 @@
<?php
namespace Socialbox\Classes;
use Exception;
use PHPUnit\Framework\TestCase;
use Socialbox\Exceptions\CryptographyException;
use Socialbox\Objects\KeyPair;
class CryptographyTest extends TestCase
{
/**
* Test that generateEncryptionKeyPair generates a KeyPair with valid keys.
*/
public function testGenerateEncryptionKeyPairProducesValidKeyPair(): void
{
$keyPair = Cryptography::generateEncryptionKeyPair();
$this->assertInstanceOf(KeyPair::class, $keyPair);
$this->assertNotEmpty($keyPair->getPublicKey());
$this->assertNotEmpty($keyPair->getPrivateKey());
}
/**
* Test that the generated public key starts with the defined encryption key type prefix.
*/
public function testGeneratedPublicKeyHasEncryptionPrefix(): void
{
$keyPair = Cryptography::generateEncryptionKeyPair();
$this->assertStringStartsWith('enc:', $keyPair->getPublicKey());
}
/**
* Test that the generated private key starts with the defined encryption key type prefix.
*/
public function testGeneratedPrivateKeyHasEncryptionPrefix(): void
{
$keyPair = Cryptography::generateEncryptionKeyPair();
$this->assertStringStartsWith('enc:', $keyPair->getPrivateKey());
}
/**
* Test that the generated keys are of different base64-encoded string values.
*/
public function testPublicAndPrivateKeysAreDifferent(): void
{
$keyPair = Cryptography::generateEncryptionKeyPair();
$this->assertNotEquals($keyPair->getPublicKey(), $keyPair->getPrivateKey());
}
/**
* Test that generateSigningKeyPair generates a KeyPair with valid keys.
*/
public function testGenerateSigningKeyPairProducesValidKeyPair(): void
{
$keyPair = Cryptography::generateSigningKeyPair();
$this->assertInstanceOf(KeyPair::class, $keyPair);
$this->assertNotEmpty($keyPair->getPublicKey());
$this->assertNotEmpty($keyPair->getPrivateKey());
}
/**
* Test that the generated public key starts with the defined signing key type prefix.
*/
public function testGeneratedPublicKeyHasSigningPrefix(): void
{
$keyPair = Cryptography::generateSigningKeyPair();
$this->assertStringStartsWith('sig:', $keyPair->getPublicKey());
}
/**
* Test that the generated private key starts with the defined signing key type prefix.
*/
public function testGeneratedPrivateKeyHasSigningPrefix(): void
{
$keyPair = Cryptography::generateSigningKeyPair();
$this->assertStringStartsWith('sig:', $keyPair->getPrivateKey());
}
/**
* Test that performDHE successfully calculates a shared secret with valid keys.
*/
public function testPerformDHESuccessfullyCalculatesSharedSecret(): void
{
$aliceKeyPair = Cryptography::generateEncryptionKeyPair();
$aliceSigningKeyPair = Cryptography::generateSigningKeyPair();
$bobKeyPair = Cryptography::generateEncryptionKeyPair();
$bobSigningKeyPair = Cryptography::generateSigningKeyPair();
// Alice performs DHE with Bob
$aliceSharedSecret = Cryptography::performDHE($bobKeyPair->getPublicKey(), $aliceKeyPair->getPrivateKey());
// Bob performs DHE with Alice
$bobSharedSecret = Cryptography::performDHE($aliceKeyPair->getPublicKey(), $bobKeyPair->getPrivateKey());
$this->assertEquals($aliceSharedSecret, $bobSharedSecret);
// Alice sends "Hello, Bob!" to Bob, signing the message and encrypting it with the shared secret
$message = "Hello, Bob!";
$aliceSignature = Cryptography::signMessage($message, $aliceSigningKeyPair->getPrivateKey());
$encryptedMessage = Cryptography::encryptShared($message, $aliceSharedSecret);
// Bob decrypts the message and verifies the signature
$decryptedMessage = Cryptography::decryptShared($encryptedMessage, $bobSharedSecret);
$isValid = Cryptography::verifyMessage($decryptedMessage, $aliceSignature, $aliceSigningKeyPair->getPublicKey());
$this->assertEquals($message, $decryptedMessage);
$this->assertTrue($isValid);
// Bob sends "Hello, Alice!" to Alice, signing the message and encrypting it with the shared secret
$message = "Hello, Alice!";
$bobSignature = Cryptography::signMessage($message, $bobSigningKeyPair->getPrivateKey());
$encryptedMessage = Cryptography::encryptShared($message, $bobSharedSecret);
// Alice decrypts the message and verifies the signature
$decryptedMessage = Cryptography::decryptShared($encryptedMessage, $aliceSharedSecret);
$isValid = Cryptography::verifyMessage($decryptedMessage, $bobSignature, $bobSigningKeyPair->getPublicKey());
$this->assertEquals($message, $decryptedMessage);
$this->assertTrue($isValid);
}
/**
* Test that performDHE throws an exception when an invalid public key is used.
*/
public function testPerformDHEThrowsExceptionForInvalidPublicKey(): void
{
$encryptionKeyPair = Cryptography::generateEncryptionKeyPair();
$invalidPublicKey = 'invalid_key';
$this->expectException(CryptographyException::class);
$this->expectExceptionMessage('Invalid key type. Expected enc:');
Cryptography::performDHE($invalidPublicKey, $encryptionKeyPair->getPrivateKey());
}
/**
* Test that performDHE throws an exception when an invalid private key is used.
*/
public function testPerformDHEThrowsExceptionForInvalidPrivateKey(): void
{
$encryptionKeyPair = Cryptography::generateEncryptionKeyPair();
$invalidPrivateKey = 'invalid_key';
$this->expectException(CryptographyException::class);
$this->expectExceptionMessage('Invalid key type. Expected enc:');
Cryptography::performDHE($encryptionKeyPair->getPublicKey(), $invalidPrivateKey);
}
/**
* Test that encrypt correctly encrypts a message with a valid shared secret.
*/
public function testEncryptSuccessfullyEncryptsMessage(): void
{
$sharedSecret = Cryptography::performDHE(
Cryptography::generateEncryptionKeyPair()->getPublicKey(),
Cryptography::generateEncryptionKeyPair()->getPrivateKey()
);
$message = "Test message";
$encryptedMessage = Cryptography::encryptShared($message, $sharedSecret);
$this->assertNotEmpty($encryptedMessage);
$this->assertNotEquals($message, $encryptedMessage);
}
/**
* Test that encrypt throws an exception when given an invalid shared secret.
*/
public function testEncryptThrowsExceptionForInvalidSharedSecret(): void
{
$invalidSharedSecret = "invalid_secret";
$message = "Test message";
$this->expectException(CryptographyException::class);
$this->expectExceptionMessage("Encryption failed");
Cryptography::encryptShared($message, $invalidSharedSecret);
}
/**
* Test that the encrypted message is different from the original message.
*/
public function testEncryptProducesDifferentMessage(): void
{
$sharedSecret = Cryptography::performDHE(
Cryptography::generateEncryptionKeyPair()->getPublicKey(),
Cryptography::generateEncryptionKeyPair()->getPrivateKey()
);
$message = "Another test message";
$encryptedMessage = Cryptography::encryptShared($message, $sharedSecret);
$this->assertNotEquals($message, $encryptedMessage);
}
/**
* Test that decrypt successfully decrypts an encrypted message with a valid shared secret.
*/
public function testDecryptSuccessfullyDecryptsMessage(): void
{
$sharedSecret = Cryptography::performDHE(
Cryptography::generateEncryptionKeyPair()->getPublicKey(),
Cryptography::generateEncryptionKeyPair()->getPrivateKey()
);
$message = "Decryption test message";
$encryptedMessage = Cryptography::encryptShared($message, $sharedSecret);
$decryptedMessage = Cryptography::decryptShared($encryptedMessage, $sharedSecret);
$this->assertEquals($message, $decryptedMessage);
}
/**
* Test that decrypt throws an exception when given an invalid shared secret.
*/
public function testDecryptThrowsExceptionForInvalidSharedSecret(): void
{
$sharedSecret = Cryptography::performDHE(
Cryptography::generateEncryptionKeyPair()->getPublicKey(),
Cryptography::generateEncryptionKeyPair()->getPrivateKey()
);
$invalidSharedSecret = "invalid_shared_secret";
$message = "Decryption failure case";
$encryptedMessage = Cryptography::encryptShared($message, $sharedSecret);
$this->expectException(CryptographyException::class);
$this->expectExceptionMessage("Decryption failed");
Cryptography::decryptShared($encryptedMessage, $invalidSharedSecret);
}
/**
* Test that decrypt throws an exception when the encrypted data is tampered with.
*/
public function testDecryptThrowsExceptionForTamperedEncryptedMessage(): void
{
$sharedSecret = Cryptography::performDHE(
Cryptography::generateEncryptionKeyPair()->getPublicKey(),
Cryptography::generateEncryptionKeyPair()->getPrivateKey()
);
$message = "Tampered message";
$encryptedMessage = Cryptography::encryptShared($message, $sharedSecret);
$tamperedMessage = $encryptedMessage . "tampered_data";
$this->expectException(CryptographyException::class);
$this->expectExceptionMessage("Decryption failed");
Cryptography::decryptShared($tamperedMessage, $sharedSecret);
}
/**
* Test that sign successfully signs a message with a valid private key.
*/
public function testSignSuccessfullySignsMessage(): void
{
$keyPair = Cryptography::generateSigningKeyPair();
$message = "Message to sign";
$signature = Cryptography::signMessage($message, $keyPair->getPrivateKey());
$this->assertNotEmpty($signature);
}
/**
* Test that sign throws an exception when an invalid private key is used.
*/
public function testSignThrowsExceptionForInvalidPrivateKey(): void
{
$invalidPrivateKey = "invalid_key";
$message = "Message to sign";
$this->expectException(CryptographyException::class);
$this->expectExceptionMessage("Failed to sign message");
Cryptography::signMessage($message, $invalidPrivateKey);
}
/**
* Test that verify successfully validates a correct signature with a valid message and public key.
*/
public function testVerifySuccessfullyValidatesSignature(): void
{
$keyPair = Cryptography::generateSigningKeyPair();
$message = "Message to verify";
$signature = Cryptography::signMessage($message, $keyPair->getPrivateKey());
$isValid = Cryptography::verifyMessage($message, $signature, $keyPair->getPublicKey());
$this->assertTrue($isValid);
}
/**
* Test that verify fails for an invalid signature.
*/
public function testVerifyFailsForInvalidSignature(): void
{
$keyPair = Cryptography::generateSigningKeyPair();
$message = "Message to verify";
$signature = "invalid_signature";
$this->expectException(Exception::class);
Cryptography::verifyMessage($message, $signature, $keyPair->getPublicKey());
}
/**
* Test that verify throws an exception for an invalid public key.
*/
public function testVerifyThrowsExceptionForInvalidPublicKey(): void
{
$keyPair = Cryptography::generateSigningKeyPair();
$message = "Message to verify";
$signature = Cryptography::signMessage($message, $keyPair->getPrivateKey());
$invalidPublicKey = "invalid_public_key";
$this->expectException(CryptographyException::class);
$this->expectExceptionMessage("Failed to verify signature");
Cryptography::verifyMessage($message, $signature, $invalidPublicKey);
}
/**
* Test that verify throws an exception for a public key with the wrong type prefix.
*/
public function testVerifyThrowsExceptionForInvalidKeyType(): void
{
$encryptionKeyPair = Cryptography::generateEncryptionKeyPair();
$message = "Message to verify";
$signature = "invalid_signature";
$this->expectException(CryptographyException::class);
$this->expectExceptionMessage("Invalid key type. Expected sig:");
Cryptography::verifyMessage($message, $signature, $encryptionKeyPair->getPublicKey());
}
/**
* Test that generateTransportKey creates a valid transport key for the default algorithm.
*/
public function testGenerateTransportKeyCreatesValidKeyForDefaultAlgo(): void
{
$transportKey = Cryptography::generateEncryptionKey();
$decodedKey = sodium_base642bin($transportKey, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING, true);
$this->assertNotEmpty($transportKey);
$this->assertEquals(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES, strlen($decodedKey));
}
/**
* Test that generateTransportKey creates valid keys for specific supported algorithms.
*/
public function testGenerateTransportKeyCreatesValidKeysForAlgorithms(): void
{
$algorithms = [
'xchacha20' => SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES,
'chacha20' => SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES,
'aes256gcm' => SODIUM_CRYPTO_AEAD_AES256GCM_KEYBYTES
];
foreach ($algorithms as $algorithm => $expectedKeyLength) {
$transportKey = Cryptography::generateEncryptionKey($algorithm);
$decodedKey = sodium_base642bin($transportKey, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING, true);
$this->assertNotEmpty($transportKey);
$this->assertEquals($expectedKeyLength, strlen($decodedKey));
}
}
/**
* Test that generateTransportKey throws an exception when given an invalid algorithm.
*/
public function testGenerateTransportKeyThrowsExceptionForInvalidAlgorithm(): void
{
$this->expectException(CryptographyException::class);
$this->expectExceptionMessage("Unsupported algorithm");
Cryptography::generateEncryptionKey("invalid_algorithm");
}
/**
* Test that generateTransportKey creates valid keys for other supported algorithms.
*/
public function testGenerateTransportKeyCreatesValidKeyForOtherSupportedAlgorithms(): void
{
$algorithms = [
'xchacha20' => SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES,
'chacha20' => SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES,
'aes256gcm' => SODIUM_CRYPTO_AEAD_AES256GCM_KEYBYTES
];
foreach ($algorithms as $algorithm => $expectedKeyLength) {
$transportKey = Cryptography::generateEncryptionKey($algorithm);
$decodedKey = sodium_base642bin($transportKey, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING, true);
$this->assertNotEmpty($transportKey);
$this->assertEquals($expectedKeyLength, strlen($decodedKey));
}
}
/**
* Test that generateTransportKey throws an exception for unsupported algorithms.
*/
public function testGenerateTransportKeyThrowsExceptionForUnsupportedAlgorithm(): void
{
$this->expectException(CryptographyException::class);
$this->expectExceptionMessage("Unsupported algorithm");
Cryptography::generateEncryptionKey('invalid_algo');
}
public function testEncryptTransportMessageSuccessfullyEncryptsMessage(): void
{
$algorithms = [
'xchacha20' => SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES,
'chacha20' => SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES,
'aes256gcm' => SODIUM_CRYPTO_AEAD_AES256GCM_KEYBYTES
];
foreach ($algorithms as $algorithm => $keyLength) {
$transportKey = Cryptography::generateEncryptionKey($algorithm);
$this->assertNotEmpty($transportKey);
$this->assertEquals($keyLength, strlen(sodium_base642bin($transportKey, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING, true)));
$message = "Test message";
$encryptedMessage = Cryptography::encryptMessage($message, $transportKey);
$decryptedMessage = Cryptography::decryptMessage($encryptedMessage, $transportKey);
$this->assertEquals($message, $decryptedMessage);
}
}
}