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">
<component name="NewModuleRootManager">
<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$/build" />
</content>

4
.idea/sqldialects.xml generated
View file

@ -2,14 +2,14 @@
<project version="4">
<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/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/sessions.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/variables.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/CaptchaManager.php" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/EncryptionRecordsManager.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/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/VariableManager.php" dialect="MariaDB" />
</component>

View file

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

View file

@ -1,46 +1,45 @@
<?php
namespace Socialbox\Classes\CliCommands;
namespace Socialbox\Classes\CliCommands;
use Socialbox\Classes\Configuration;
use Socialbox\Classes\Logger;
use Socialbox\Interfaces\CliCommandInterface;
use Socialbox\Classes\Configuration;
use Socialbox\Classes\Logger;
use Socialbox\Interfaces\CliCommandInterface;
class DnsRecordCommand implements CliCommandInterface
{
/**
* @inheritDoc
*/
public static function execute(array $args): int
class DnsRecordCommand implements CliCommandInterface
{
$txt_record = sprintf('v=socialbox;sb-rpc=%s;sb-key=%s',
Configuration::getInstanceConfiguration()->getRpcEndpoint(),
Configuration::getInstanceConfiguration()->getPublicKey()
);
/**
* @inheritDoc
*/
public static function execute(array $args): int
{
$txt_record = sprintf('v=socialbox;sb-rpc=%s;sb-key=%s',
Configuration::getInstanceConfiguration()->getRpcEndpoint(),
Configuration::getCryptographyConfiguration()->getHostPublicKey()
);
Logger::getLogger()->info('Please set the following DNS TXT record for the domain:');
Logger::getLogger()->info(sprintf(' %s', $txt_record));
return 0;
}
Logger::getLogger()->info('Please set the following DNS TXT record for the domain:');
Logger::getLogger()->info(sprintf(' %s', $txt_record));
return 0;
}
/**
* @inheritDoc
*/
public static function getHelpMessage(): string
{
return <<<HELP
/**
* @inheritDoc
*/
public static function getHelpMessage(): string
{
return <<<HELP
Usage: socialbox dns-record
Displays the DNS TXT record that should be set for the domain.
HELP;
}
}
/**
* @inheritDoc
*/
public static function getShortHelpMessage(): string
{
return 'Displays the DNS TXT record that should be set for the domain.';
}
}
/**
* @inheritDoc
*/
public static function getShortHelpMessage(): string
{
return 'Displays the DNS TXT record that should be set for the domain.';
}
}

View file

@ -3,7 +3,6 @@
namespace Socialbox\Classes\CliCommands;
use Exception;
use LogLib\Log;
use PDOException;
use Socialbox\Abstracts\CacheLayer;
use Socialbox\Classes\Configuration;
@ -13,9 +12,8 @@
use Socialbox\Classes\Resources;
use Socialbox\Enums\DatabaseObjects;
use Socialbox\Exceptions\CryptographyException;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Interfaces\CliCommandInterface;
use Socialbox\Managers\EncryptionRecordsManager;
use Socialbox\Socialbox;
class InitializeCommand implements CliCommandInterface
{
@ -208,6 +206,7 @@
Logger::getLogger()->info('cache.database defaulting to 0');
}
Logger::getLogger()->info('Updating configuration...');
Configuration::getConfigurationLib()->save(); // Save
Configuration::reload(); // Reload
}
@ -261,16 +260,17 @@
}
if(
!Configuration::getInstanceConfiguration()->getPublicKey() ||
!Configuration::getInstanceConfiguration()->getPrivateKey() ||
!Configuration::getInstanceConfiguration()->getEncryptionKeys()
!Configuration::getCryptographyConfiguration()->getHostPublicKey() ||
!Configuration::getCryptographyConfiguration()->getHostPrivateKey() ||
!Configuration::getCryptographyConfiguration()->getHostPublicKey()
)
{
$expires = time() + 31536000;
try
{
Logger::getLogger()->info('Generating new key pair...');
$keyPair = Cryptography::generateKeyPair();
$encryptionKeys = Cryptography::randomKeyS(230, 314, Configuration::getInstanceConfiguration()->getEncryptionKeysCount());
Logger::getLogger()->info('Generating new key pair (expires ' . date('Y-m-d H:i:s', $expires) . ')...');
$signingKeyPair = Cryptography::generateSigningKeyPair();
}
catch (CryptographyException $e)
{
@ -278,40 +278,35 @@
return 1;
}
Logger::getLogger()->info('Updating configuration...');
Configuration::getConfigurationLib()->set('instance.private_key', $keyPair->getPrivateKey());
Configuration::getConfigurationLib()->set('instance.public_key', $keyPair->getPublicKey());
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()
));
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());
}
try
// 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())
{
if(EncryptionRecordsManager::getRecordCount() < Configuration::getInstanceConfiguration()->getEncryptionRecordsCount())
Logger::getLogger()->info('Generating internal encryption keys...');
$encryptionKeys = Configuration::getCryptographyConfiguration()->getInternalEncryptionKeys() ?? [];
while(count($encryptionKeys) < Configuration::getCryptographyConfiguration()->getEncryptionKeysCount())
{
Logger::getLogger()->info('Generating encryption records...');
EncryptionRecordsManager::generateRecords(Configuration::getInstanceConfiguration()->getEncryptionRecordsCount());
$encryptionKeys[] = Cryptography::generateEncryptionKey(Configuration::getCryptographyConfiguration()->getEncryptionKeysAlgorithm());
}
}
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;
Configuration::getConfigurationLib()->set('cryptography.internal_encryption_keys', $encryptionKeys);
}
// TODO: Create a host peer here?
Logger::getLogger()->info('Updating configuration...');
Configuration::getConfigurationLib()->save();;
Configuration::reload();
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')
{
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;
use Socialbox\Classes\ClientCommands\StorageConfiguration;
use Socialbox\Classes\Configuration\CacheConfiguration;
use Socialbox\Classes\Configuration\CryptographyConfiguration;
use Socialbox\Classes\Configuration\DatabaseConfiguration;
use Socialbox\Classes\Configuration\InstanceConfiguration;
use Socialbox\Classes\Configuration\LoggingConfiguration;
use Socialbox\Classes\Configuration\RegistrationConfiguration;
use Socialbox\Classes\Configuration\SecurityConfiguration;
use Socialbox\Classes\Configuration\StorageConfiguration;
class Configuration
{
private static ?\ConfigLib\Configuration $configuration = null;
private static ?InstanceConfiguration $instanceConfiguration = null;
private static ?SecurityConfiguration $securityConfiguration = null;
private static ?CryptographyConfiguration $cryptographyConfiguration = null;
private static ?DatabaseConfiguration $databaseConfiguration = null;
private static ?LoggingConfiguration $loggingConfiguration = null;
private static ?CacheConfiguration $cacheConfiguration = null;
@ -33,19 +35,47 @@
// Instance configuration
$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.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
$config->setDefault('security.display_internal_exceptions', false);
$config->setDefault('security.resolved_servers_ttl', 600);
$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
$config->setDefault('database.host', '127.0.0.1');
$config->setDefault('database.port', 3306);
@ -98,6 +128,7 @@
self::$configuration = $config;
self::$instanceConfiguration = new InstanceConfiguration(self::$configuration->getConfiguration()['instance']);
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::$loggingConfiguration = new LoggingConfiguration(self::$configuration->getConfiguration()['logging']);
self::$cacheConfiguration = new CacheConfiguration(self::$configuration->getConfiguration()['cache']);
@ -140,6 +171,14 @@
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
{
if(self::$configuration === null)
@ -180,6 +219,24 @@
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.
*

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
{
private bool $enabled;
private string $name;
private ?string $domain;
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.
@ -22,13 +18,9 @@
public function __construct(array $data)
{
$this->enabled = (bool)$data['enabled'];
$this->name = $data['name'];
$this->domain = $data['domain'];
$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;
}
public function getName(): string
{
return $this->name;
}
/**
* Retrieves the domain.
*
@ -58,62 +55,4 @@
{
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
namespace Socialbox\Classes\ClientCommands;
namespace Socialbox\Classes\Configuration;
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

@ -1,11 +1,9 @@
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',
iv mediumtext not null comment 'The Initial Vector of the password record',
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',
hash mediumtext not null comment 'The encrypted hash of the password',
updated timestamp default current_timestamp() not null comment 'The Timestamp for when this record was last updated',
constraint authentication_passwords_peer_uuid_uindex
unique (peer_uuid) comment 'The primary unique index of the peer uuid',
constraint authentication_passwords_registered_peers_uuid_fk

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

@ -1,17 +1,22 @@
create table sessions
(
uuid varchar(36) default uuid() not null comment 'The Unique Primary index for the session UUID'
uuid varchar(36) default uuid() not null comment 'The Unique Primary index for the session UUID'
primary key,
peer_uuid varchar(36) not null comment 'The peer the session is identified as, null if the session isn''t identified as a peer',
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',
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',
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',
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',
peer_uuid varchar(36) not null comment 'The peer the session is identified as, null if the session isn''t identified as a peer',
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',
authenticated tinyint(1) default 0 not null comment 'Indicates if the session is currently authenticated as the peer',
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',
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',
last_request timestamp null comment 'The Timestamp for when the last request was made using this session',
constraint sessions_uuid_uindex
unique (uuid) comment 'The Unique Primary index for the session UUID',
constraint sessions_registered_peers_uuid_fk

View file

@ -2,11 +2,10 @@
namespace Socialbox\Classes;
use Socialbox\Enums\Options\ClientOptions;
use Socialbox\Enums\StandardError;
use Socialbox\Enums\StandardHeaders;
use Socialbox\Enums\Types\RequestType;
use Socialbox\Exceptions\CryptographyException;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\ResolutionException;
use Socialbox\Exceptions\RpcException;
use Socialbox\Objects\ExportedSession;
@ -14,6 +13,7 @@
use Socialbox\Objects\PeerAddress;
use Socialbox\Objects\RpcRequest;
use Socialbox\Objects\RpcResult;
use Socialbox\Objects\Standard\ServerInformation;
class RpcClient
{
@ -22,9 +22,14 @@
private bool $bypassSignatureVerification;
private PeerAddress $peerAddress;
private KeyPair $keyPair;
private string $encryptionKey;
private string $serverPublicKey;
private string $serverPublicSigningKey;
private string $serverPublicEncryptionKey;
private KeyPair $clientSigningKeyPair;
private KeyPair $clientEncryptionKeyPair;
private string $privateSharedSecret;
private string $clientTransportEncryptionKey;
private string $serverTransportEncryptionKey;
private ServerInformation $serverInformation;
private string $rpcEndpoint;
private string $sessionUuid;
@ -42,14 +47,41 @@
$this->bypassSignatureVerification = false;
// If an exported session is provided, no need to re-connect.
// Just use the session details provided.
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->keyPair = new KeyPair($exportedSession->getPublicKey(), $exportedSession->getPrivateKey());
$this->encryptionKey = $exportedSession->getEncryptionKey();
$this->serverPublicKey = $exportedSession->getServerPublicKey();
$this->rpcEndpoint = $exportedSession->getRpcEndpoint();
$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;
}
@ -62,51 +94,61 @@
// Set the initial properties
$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
try
{
$resolvedServer = ServerResolver::resolveDomain($this->peerAddress->getDomain(), false);
}
catch (DatabaseOperationException $e)
{
throw new ResolutionException('Failed to resolve domain: ' . $e->getMessage(), 0, $e);
}
$resolvedServer = ServerResolver::resolveDomain($this->peerAddress->getDomain(), false);
$this->serverPublicKey = $resolvedServer->getPublicKey();
$this->rpcEndpoint = $resolvedServer->getEndpoint();
// Import the RPC Endpoint & the server's public key.
$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');
}
// Attempt to create an encrypted session with the server
$this->sessionUuid = $this->createSession();
// Resolve basic server information
$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();
}
/**
* Creates a new session by sending an HTTP GET request to the RPC endpoint.
* The request includes specific headers required for session initiation.
* Initiates a new session with the server and retrieves the session UUID.
*
* @return string Returns the session UUID received from the server.
* @throws RpcException If the server response is invalid, the session creation fails, or no session UUID is returned.
* @return string The session UUID provided by the server upon successful session creation.
* @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();
@ -116,28 +158,45 @@
StandardHeaders::CLIENT_NAME->value . ': ' . self::CLIENT_NAME,
StandardHeaders::CLIENT_VERSION->value . ': ' . self::CLIENT_VERSION,
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
// Otherwise, the server will obtain the public key itself from DNS records rather than trusting the client
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_HTTPGET, 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);
// If the response is false, the request failed
if($response === false)
{
curl_close($ch);
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);
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));
}
// If the response is empty, the server did not provide a session UUID
if(empty($response))
{
curl_close($ch);
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);
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);
return $response;
// Set the server's encryption key
$this->serverPublicEncryptionKey = $serverPublicEncryptionKey;
// Set the session UUID
$this->sessionUuid = $response;
}
/**
@ -168,15 +257,26 @@
*/
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
// Upon success the server should return 204 without a body
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)
{
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();
@ -186,6 +286,7 @@
curl_setopt($ch, CURLOPT_HTTPHEADER, [
StandardHeaders::REQUEST_TYPE->value . ': ' . RequestType::DHE_EXCHANGE->value,
StandardHeaders::SESSION_UUID->value . ': ' . $this->sessionUuid,
StandardHeaders::SIGNATURE->value . ': ' . $signature
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $encryptedKey);
@ -194,17 +295,28 @@
if($response === false)
{
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);
if($responseCode !== 204)
if($responseCode !== 200)
{
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);
}
curl_close($ch);
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);
}
}
/**
@ -218,8 +330,16 @@
{
try
{
$encryptedData = Cryptography::encryptTransport($jsonData, $this->encryptionKey);
$signature = Cryptography::signContent($jsonData, $this->keyPair->getPrivateKey(), true);
$encryptedData = Cryptography::encryptMessage(
message: $jsonData,
encryptionKey: $this->serverTransportEncryptionKey,
algorithm: $this->serverInformation->getTransportEncryptionAlgorithm()
);
$signature = Cryptography::signMessage(
message: $jsonData,
privateKey: $this->clientSigningKeyPair->getPrivateKey(),
);
}
catch (CryptographyException $e)
{
@ -289,7 +409,11 @@
try
{
$decryptedResponse = Cryptography::decryptTransport($responseString, $this->encryptionKey);
$decryptedResponse = Cryptography::decryptMessage(
encryptedMessage: $responseString,
encryptionKey: $this->clientTransportEncryptionKey,
algorithm: $this->serverInformation->getTransportEncryptionAlgorithm()
);
}
catch (CryptographyException $e)
{
@ -298,7 +422,7 @@
if (!$this->bypassSignatureVerification)
{
$signature = $headers['signature'][0] ?? null;
$signature = $headers[strtolower(StandardHeaders::SIGNATURE->value)][0] ?? null;
if ($signature === null)
{
throw new RpcException('The server did not provide a signature for the response');
@ -306,7 +430,11 @@
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');
}
@ -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.
*
@ -395,12 +576,19 @@
{
return new ExportedSession([
'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,
'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;
use Socialbox\Exceptions\DatabaseOperationException;
use InvalidArgumentException;
use Socialbox\Exceptions\ResolutionException;
use Socialbox\Managers\ResolvedServersManager;
use Socialbox\Objects\ResolvedServer;
use Socialbox\Managers\ResolvedDnsRecordsManager;
use Socialbox\Objects\DnsRecord;
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.
* @return ResolvedServer An instance of ResolvedServer containing the endpoint and public key.
* @throws ResolutionException If the DNS TXT records cannot be resolved or if required information is missing.
* @throws DatabaseOperationException
* @param string $domain The domain name to resolve.
* @param bool $useDatabase Whether to check the database for cached resolution data; defaults to true.
* @return DnsRecord The parsed DNS record for the given domain.
* @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
if($useDatabase)
// Check the database if enabled
if ($useDatabase)
{
$resolvedServer = ResolvedServersManager::getResolvedServer($domain);
if($resolvedServer !== null)
$resolvedServer = ResolvedDnsRecordsManager::getDnsRecord($domain);
if ($resolvedServer !== null)
{
return $resolvedServer->toResolvedServer();
return $resolvedServer;
}
}
@ -38,23 +37,23 @@
}
$fullRecord = self::concatenateTxtRecords($txtRecords);
if (preg_match(self::PATTERN, $fullRecord, $matches))
try
{
$endpoint = trim($matches[1]);
$publicKey = trim(str_replace(' ', '', $matches[2]));
if (empty($endpoint))
// Parse the TXT record using DnsHelper
$record = DnsHelper::parseTxt($fullRecord);
// 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))
{
throw new ResolutionException(sprintf("Failed to resolve public key for %s", $domain));
}
return new ResolvedServer($endpoint, $publicKey);
return $record;
}
else
catch (InvalidArgumentException $e)
{
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.
* @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)
{
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);
}
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

View file

@ -122,22 +122,37 @@
return $headers;
}
/**
* 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
public static function throwableToString(Throwable $e, int $level=0): string
{
return sprintf(
"%s: %s in %s:%d\nStack trace:\n%s",
get_class($e),
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$e->getTraceAsString()
// Indentation for nested exceptions
$indentation = str_repeat(' ', $level);
// Basic information about the Throwable
$type = get_class($e);
$message = $e->getMessage() ?: 'No message';
$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
{
case VARIABLES = 'variables.sql';
case ENCRYPTION_RECORDS = 'encryption_records.sql';
case RESOLVED_SERVERS = 'resolved_servers.sql';
case RESOLVED_DNS_RECORDS = 'resolved_dns_records.sql';
case REGISTERED_PEERS = 'registered_peers.sql';
@ -24,7 +23,7 @@
{
return match ($this)
{
self::VARIABLES, self::ENCRYPTION_RECORDS, self::RESOLVED_SERVERS => 0,
self::VARIABLES, self::RESOLVED_DNS_RECORDS => 0,
self::REGISTERED_PEERS => 1,
self::AUTHENTICATION_PASSWORDS, self::CAPTCHA_IMAGES, self::SESSIONS, self::EXTERNAL_SESSIONS => 2,
};

View file

@ -7,16 +7,21 @@ enum StandardError : int
// Fallback Codes
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
case RPC_METHOD_NOT_FOUND = -1000;
case RPC_INVALID_ARGUMENTS = -1001;
// Server Errors
case INTERNAL_SERVER_ERROR = -2000;
case SERVER_UNAVAILABLE = -2001;
CASE RPC_BAD_REQUEST = -1002;
// Client Errors
case BAD_REQUEST = -3000;
case METHOD_NOT_ALLOWED = -3001;
// Authentication/Cryptography Errors

View file

@ -8,10 +8,12 @@
enum StandardHeaders : string
{
case REQUEST_TYPE = 'Request-Type';
case ERROR_CODE = 'Error-Code';
case IDENTIFY_AS = 'Identify-As';
case CLIENT_NAME = 'Client-Name';
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 SIGNATURE = 'Signature';

View file

@ -4,6 +4,11 @@
enum RequestType : string
{
/**
* Represents the action of getting server information (Non-RPC Request)
*/
case INFO = 'info';
/**
* 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;
use DateTime;
use PDO;
use PDOException;
use Socialbox\Classes\Configuration;
use Socialbox\Classes\Cryptography;
use Socialbox\Classes\Database;
use Socialbox\Classes\SecuredPassword;
use Socialbox\Exceptions\CryptographyException;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Objects\Database\RegisteredPeerRecord;
use Socialbox\Objects\Database\SecurePasswordRecord;
class PasswordManager
{
@ -34,154 +36,139 @@
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);
}
}
/**
* Sets a password for a given user or peer record by securely encrypting it
* and storing it in the authentication_passwords database table.
* Sets a secured password for the given peer UUID or registered peer record.
*
* @param string|RegisteredPeerRecord $peerUuid The UUID of the peer or an instance of RegisteredPeerRecord.
* @param string $password The plaintext password to be securely stored.
* @throws CryptographyException If an error occurs while securing the password.
* @throws DatabaseOperationException If an error occurs while attempting to store the password in the database.
* @throws \DateMalformedStringException If the updated timestamp cannot be formatted.
* @param string|RegisteredPeerRecord $peerUuid The unique identifier or registered peer record of the user.
* @param string $hash The plaintext password to be securely stored.
* @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)
{
$peerUuid = $peerUuid->getUuid();
}
$encryptionRecord = EncryptionRecordsManager::getRandomRecord();
$securedPassword = SecuredPassword::securePassword($peerUuid, $password, $encryptionRecord);
// Throws an exception if the hash is invalid
Cryptography::validatePasswordHash($hash);
$encryptionKey = Configuration::getCryptographyConfiguration()->getRandomInternalEncryptionKey();
$securedPassword = Cryptography::encryptMessage($hash, $encryptionKey, Configuration::getCryptographyConfiguration()->getEncryptionKeysAlgorithm());
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);
$iv = $securedPassword->getIv();
$stmt->bindParam(':iv', $iv);
$encryptedPassword = $securedPassword->getEncryptedPassword();
$stmt->bindParam(':encrypted_password', $encryptedPassword);
$encryptedTag = $securedPassword->getEncryptedTag();
$stmt->bindParam(':encrypted_tag', $encryptedTag);
$stmt->bindParam(':hash', $securedPassword);
$stmt->execute();
}
catch(\PDOException $e)
catch(PDOException $e)
{
throw new DatabaseOperationException(sprintf('Failed to set password for user %s', $peerUuid), $e);
}
}
/**
* Updates the password for a given peer identified by their UUID or a RegisteredPeerRecord.
* 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 $newPassword The new password to be set for the peer.
* @throws CryptographyException If an error occurs while securing the new password.
* @throws DatabaseOperationException If the update operation fails due to a database error.
* @throws \DateMalformedStringException If the updated timestamp cannot be formatted.
* @returns void
* @param string|RegisteredPeerRecord $peerUuid The unique identifier or registered peer record of the user.
* @param string $hash The new password to be stored securely.
* @return void
* @throws DatabaseOperationException If an error occurs while updating the password in the database.
* @throws CryptographyException If an error occurs while encrypting the password or validating the hash.
*/
public static function updatePassword(string|RegisteredPeerRecord $peerUuid, string $newPassword): void
public static function updatePassword(string|RegisteredPeerRecord $peerUuid, string $hash): void
{
if($peerUuid instanceof RegisteredPeerRecord)
{
$peerUuid = $peerUuid->getUuid();
}
Cryptography::validatePasswordHash($hash);
$encryptionRecord = EncryptionRecordsManager::getRandomRecord();
$securedPassword = SecuredPassword::securePassword($peerUuid, $newPassword, $encryptionRecord);
$encryptionKey = Configuration::getCryptographyConfiguration()->getRandomInternalEncryptionKey();
$securedPassword = Cryptography::encryptMessage($hash, $encryptionKey, Configuration::getCryptographyConfiguration()->getEncryptionKeysAlgorithm());
try
{
$stmt = Database::getConnection()->prepare("UPDATE authentication_passwords SET iv=:iv, encrypted_password=:encrypted_password, encrypted_tag=:encrypted_tag, updated=:updated WHERE peer_uuid=:peer_uuid");
$stmt->bindParam(":peer_uuid", $peerUuid);
$iv = $securedPassword->getIv();
$stmt->bindParam(':iv', $iv);
$encryptedPassword = $securedPassword->getEncryptedPassword();
$stmt->bindParam(':encrypted_password', $encryptedPassword);
$encryptedTag = $securedPassword->getEncryptedTag();
$stmt->bindParam(':encrypted_tag', $encryptedTag);
$updated = $securedPassword->getUpdated()->format('Y-m-d H:i:s');
$stmt = Database::getConnection()->prepare("UPDATE authentication_passwords SET hash=:hash, updated=:updated WHERE peer_uuid=:peer_uuid");
$updated = (new DateTime())->setTimestamp(time());
$stmt->bindParam(':hash', $securedPassword);
$stmt->bindParam(':updated', $updated);
$stmt->bindParam(':peer_uuid', $peerUuid);
$stmt->execute();
}
catch(\PDOException $e)
catch(PDOException $e)
{
throw new DatabaseOperationException(sprintf('Failed to update password for user %s', $peerUuid), $e);
}
}
/**
* Retrieves the password record associated with the given peer UUID.
* 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.
* @return SecurePasswordRecord|null Returns a SecurePasswordRecord if found, or null if no record is present.
* @throws DatabaseOperationException If a database operation error occurs during the retrieval process.
* @param string|RegisteredPeerRecord $peerUuid The unique identifier of the peer, or an instance of RegisteredPeerRecord.
* @param string $hash The password hash to verify.
* @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)
{
$peerUuid = $peerUuid->getUuid();
}
Cryptography::validatePasswordHash($hash);
try
{
$statement = Database::getConnection()->prepare("SELECT * FROM authentication_passwords WHERE peer_uuid=:peer_uuid LIMIT 1");
$statement->bindParam(':peer_uuid', $peerUuid);
$stmt = Database::getConnection()->prepare('SELECT hash FROM authentication_passwords WHERE peer_uuid=:uuid');
$stmt->bindParam(':uuid', $peerUuid);
$stmt->execute();
$statement->execute();
$data = $statement->fetch(PDO::FETCH_ASSOC);
if ($data === false)
$record = $stmt->fetch(PDO::FETCH_ASSOC);
if($record === false)
{
return null;
return false;
}
return SecurePasswordRecord::fromArray($data);
}
catch(\PDOException $e)
{
throw new DatabaseOperationException(sprintf('Failed to retrieve password record for user %s', $peerUuid), $e);
}
}
$encryptedHash = $record['hash'];
$decryptedHash = null;
foreach(Configuration::getCryptographyConfiguration()->getInternalEncryptionKeys() as $key)
{
try
{
$decryptedHash = Cryptography::decryptMessage($encryptedHash, $key, Configuration::getCryptographyConfiguration()->getEncryptionKeysAlgorithm());
}
catch(CryptographyException)
{
continue;
}
}
/**
* 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;
}
if($decryptedHash === null)
{
throw new CryptographyException('Cannot decrypt hashed password');
}
$encryptionRecords = EncryptionRecordsManager::getAllRecords();
return SecuredPassword::verifyPassword($password, $securedPassword, $encryptionRecords);
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\Objects\Database\RegisteredPeerRecord;
use Socialbox\Objects\Database\SessionRecord;
use Socialbox\Objects\KeyPair;
use Symfony\Component\Uid\Uuid;
class SessionManager
{
/**
* 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
public static function createSession(RegisteredPeerRecord $peer, string $clientName, string $clientVersion, string $clientPublicSigningKey, string $clientPublicEncryptionKey, KeyPair $serverEncryptionKeyPair): 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();
$flags = [];
// TODO: Update this to support `host` peers
if($peer->isEnabled())
{
$flags[] = SessionFlags::AUTHENTICATION_REQUIRED;
@ -119,13 +112,18 @@
try
{
$statement = Database::getConnection()->prepare("INSERT INTO sessions (uuid, peer_uuid, client_name, client_version, public_key, flags) VALUES (?, ?, ?, ?, ?, ?)");
$statement->bindParam(1, $uuid);
$statement->bindParam(2, $peerUuid);
$statement->bindParam(3, $clientName);
$statement->bindParam(4, $clientVersion);
$statement->bindParam(5, $publicKey);
$statement->bindParam(6, $implodedFlags);
$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(':uuid', $uuid);
$statement->bindParam(':peer_uuid', $peerUuid);
$statement->bindParam(':client_name', $clientName);
$statement->bindParam(':client_version', $clientVersion);
$statement->bindParam(':client_public_signing_key', $clientPublicSigningKey);
$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();
}
catch(PDOException $e)
@ -186,7 +184,6 @@
// Convert the timestamp fields to DateTime objects
$data['created'] = new DateTime($data['created']);
if(isset($data['last_request']) && $data['last_request'] !== null)
{
$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.
*
@ -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 $encryptionKey The new encryption key to be assigned.
* @param string $uuid The unique identifier for the session to update.
* @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
* @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));
try
{
$state_value = SessionState::ACTIVE->value;
$statement = Database::getConnection()->prepare('UPDATE sessions SET state=?, encryption_key=? WHERE uuid=?');
$statement->bindParam(1, $state_value);
$statement->bindParam(2, $encryptionKey);
$statement->bindParam(3, $uuid);
$statement = 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(':state', $state_value);
$statement->bindParam(':private_shared_secret', $privateSharedSecret);
$statement->bindParam(':client_transport_encryption_key', $clientEncryptionKey);
$statement->bindParam(':server_transport_encryption_key', $serverEncryptionKey);
$statement->bindParam(':uuid', $uuid);
$statement->execute();
}

View file

@ -2,10 +2,7 @@
namespace Socialbox\Objects;
use InvalidArgumentException;
use Socialbox\Classes\Cryptography;
use Socialbox\Classes\Utilities;
use Socialbox\Enums\SessionState;
use Socialbox\Enums\StandardHeaders;
use Socialbox\Enums\Types\RequestType;
use Socialbox\Exceptions\CryptographyException;
@ -18,7 +15,7 @@
class ClientRequest
{
private array $headers;
private RequestType $requestType;
private ?RequestType $requestType;
private ?string $requestBody;
private ?string $clientName;
@ -27,6 +24,14 @@
private ?string $sessionUuid;
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)
{
$this->headers = $headers;
@ -34,17 +39,28 @@
$this->clientName = $headers[StandardHeaders::CLIENT_NAME->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->sessionUuid = $headers[StandardHeaders::SESSION_UUID->value] ?? null;
$this->signature = $headers[StandardHeaders::SIGNATURE->value] ?? null;
}
/**
* Retrieves the headers.
*
* @return array Returns an array of headers.
*/
public function getHeaders(): array
{
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
{
if(is_string($header))
@ -55,6 +71,12 @@
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
{
if(!$this->headerExists($header))
@ -70,26 +92,51 @@
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
{
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
{
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
{
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;
}
/**
* 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
{
if($this->identifyAs === null)
@ -100,11 +147,21 @@
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
{
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
{
if($this->sessionUuid === null)
@ -115,6 +172,11 @@
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
{
$session = $this->getSession();
@ -127,11 +189,22 @@
return RegisteredPeerManager::getPeer($session->getPeerUuid());
}
/**
* Retrieves the signature value.
*
* @return string|null The signature value or null if not set
*/
public function getSignature(): ?string
{
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
{
if($this->getSignature() == null || $this->getSessionUuid() == null)
@ -141,7 +214,11 @@
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)
{
@ -156,52 +233,12 @@
* @return RpcRequest[] The parsed RpcRequest objects
* @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);
}
// 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);
throw new RequestException('Malformed JSON', 400);
}
// 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

@ -1,110 +1,144 @@
<?php
namespace Socialbox\Objects\Database;
namespace Socialbox\Objects\Database;
use DateTime;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Objects\ResolvedServer;
use DateTime;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Objects\DnsRecord;
class ResolvedServerRecord implements SerializableInterface
{
private string $domain;
private string $endpoint;
private string $publicKey;
private DateTime $updated;
/**
* Constructs a new instance of the class.
*
* @param array $data An associative array containing the domain, endpoint, public_key, and updated values.
* @throws \DateMalformedStringException
*/
public function __construct(array $data)
class ResolvedServerRecord implements SerializableInterface
{
$this->domain = (string)$data['domain'];
$this->endpoint = (string)$data['endpoint'];
$this->publicKey = (string)$data['public_key'];
private string $domain;
private string $endpoint;
private string $publicKey;
private DateTime $expires;
private DateTime $updated;
if(is_null($data['updated']))
/**
* Constructs a new instance of the class.
*
* @param array $data An associative array containing the domain, endpoint, public_key, and updated values.
* @throws \DateMalformedStringException
*/
public function __construct(array $data)
{
$this->updated = new DateTime();
$this->domain = (string)$data['domain'];
$this->endpoint = (string)$data['endpoint'];
$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']))
{
$this->updated = new DateTime();
}
elseif (is_int($data['updated']))
{
$this->updated = (new DateTime())->setTimestamp($data['updated']);
}
elseif (is_string($data['updated']))
{
$this->updated = new DateTime($data['updated']);
}
else
{
$this->updated = $data['updated'];
}
}
elseif (is_string($data['updated']))
/**
* Retrieves the domain value.
*
* @return string The domain as a string.
*/
public function getDomain(): string
{
$this->updated = new DateTime($data['updated']);
return $this->domain;
}
else
/**
* Retrieves the configured endpoint.
*
* @return string The endpoint as a string.
*/
public function getEndpoint(): string
{
$this->updated = $data['updated'];
return $this->endpoint;
}
}
/**
*
* @return string The domain value.
*/
public function getDomain(): string
{
return $this->domain;
}
/**
* Retrieves the public key.
*
* @return string The public key as a string.
*/
public function getPublicKey(): string
{
return $this->publicKey;
}
/**
*
* @return string The endpoint value.
*/
public function getEndpoint(): string
{
return $this->endpoint;
}
/**
* Retrieves the expiration timestamp.
*
* @return DateTime The DateTime object representing the expiration time.
*/
public function getExpires(): DateTime
{
return $this->expires;
}
/**
*
* @return string The public key.
*/
public function getPublicKey(): string
{
return $this->publicKey;
}
/**
* Retrieves the timestamp of the last update.
*
* @return DateTime The DateTime object representing the last update time.
*/
public function getUpdated(): DateTime
{
return $this->updated;
}
/**
* Retrieves the timestamp of the last update.
*
* @return DateTime The DateTime object representing the last update time.
*/
public function getUpdated(): DateTime
{
return $this->updated;
}
/**
* Fetches the DNS record based on the provided endpoint, public key, and expiration time.
*
* @return DnsRecord An instance of the DnsRecord containing the endpoint, public key, and expiration timestamp.
*/
public function getDnsRecord(): DnsRecord
{
return new DnsRecord($this->endpoint, $this->publicKey, $this->expires->getTimestamp());
}
/**
* Converts the record to a ResolvedServer object.
*
* @return ResolvedServer The ResolvedServer object.
*/
public function toResolvedServer(): ResolvedServer
{
return new ResolvedServer($this->endpoint, $this->publicKey);
}
/**
* @inheritDoc
*/
public static function fromArray(array $data): object
{
return new self($data);
}
/**
* @inheritDoc
* @throws \DateMalformedStringException
*/
public static function fromArray(array $data): object
{
return new self($data);
}
/**
* @inheritDoc
*/
public function toArray(): array
{
return [
'domain' => $this->domain,
'endpoint' => $this->endpoint,
'public_key' => $this->publicKey,
'updated' => $this->updated->format('Y-m-d H:i:s')
];
}
}
/**
* @inheritDoc
*/
public function toArray(): array
{
return [
'domain' => $this->domain,
'endpoint' => $this->endpoint,
'public_key' => $this->publicKey,
'updated' => $this->updated->format('Y-m-d H:i:s')
];
}
}

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 $clientVersion;
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 ?string $encryptionKey;
/**
* @var SessionFlags[]
*/
@ -42,10 +46,14 @@
$this->clientName = $data['client_name'];
$this->clientVersion = $data['client_version'];
$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->lastRequest = $data['last_request'];
$this->encryptionKey = $data['encryption_key'] ?? null;
$this->flags = SessionFlags::fromString($data['flags']);
if(SessionState::tryFrom($data['state']) == null)
@ -99,9 +107,55 @@
*
* @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;
}
/**
* 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.
*
@ -194,6 +238,11 @@
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
{
return new \Socialbox\Objects\Standard\SessionState([
@ -207,10 +256,7 @@
/**
* Creates a new instance of the class using the provided array data.
*
* @param array $data An associative array of data used to initialize the object properties.
* @return object Returns a newly created object instance.
* @inheritDoc
*/
public static function fromArray(array $data): object
{
@ -218,10 +264,7 @@
}
/**
* Converts the object's properties to an associative array.
*
* @return array An associative array representing the object's data, including keys 'uuid', 'peer_uuid',
* 'authenticated', 'public_key', 'state', 'flags', 'created', and 'last_request'.
* @inheritDoc
*/
public function toArray(): array
{
@ -229,7 +272,12 @@
'uuid' => $this->uuid,
'peer_uuid' => $this->peerUuid,
'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,
'flags' => SessionFlags::toString($this->flags),
'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;
use Socialbox\Interfaces\SerializableInterface;
/**
* Represents an exported session containing cryptographic keys, identifiers, and endpoints.
*/
class ExportedSession
class ExportedSession implements SerializableInterface
{
private string $peerAddress;
private string $privateKey;
private string $publicKey;
private string $encryptionKey;
private string $serverPublicKey;
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.
* Expected keys:
* - 'peer_address': The address of the peer.
* - 'private_key': The private key for secure communication.
* - 'public_key': The public key for secure communication.
* - 'encryption_key': The encryption key used for communication.
* - 'server_public_key': The server's public key.
* - 'rpc_endpoint': The RPC endpoint for network communication.
* - 'session_uuid': The unique identifier for the session.
* @param array $data Associative array containing the required properties such as:
* 'peer_address', 'rpc_endpoint', 'session_uuid',
* 'server_public_signing_key', 'server_public_encryption_key',
* 'client_public_signing_key', 'client_private_signing_key',
* 'client_public_encryption_key', 'client_private_encryption_key',
* 'private_shared_secret', 'client_transport_encryption_key',
* 'server_transport_encryption_key'.
*
* @return void
*/
public function __construct(array $data)
{
$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->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
{
@ -52,47 +66,7 @@
}
/**
* Retrieves the private key.
*
* @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.
* Retrieves 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.
*/
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
{
return [
'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,
'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.
*
* @param array $data The input data used to construct the ExportedSession instance.
* @return ExportedSession The new ExportedSession instance created from the given data.
* @inheritDoc
*/
public static function fromArray(array $data): ExportedSession
{

View file

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

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 Socialbox\Classes\Configuration;
use Socialbox\Classes\Cryptography;
use Socialbox\Classes\DnsHelper;
use Socialbox\Classes\Logger;
use Socialbox\Classes\ServerResolver;
use Socialbox\Classes\Utilities;
@ -16,6 +17,7 @@
use Socialbox\Enums\StandardHeaders;
use Socialbox\Enums\StandardMethods;
use Socialbox\Enums\Types\RequestType;
use Socialbox\Exceptions\CryptographyException;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\RequestException;
use Socialbox\Exceptions\StandardException;
@ -23,34 +25,37 @@
use Socialbox\Managers\SessionManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\PeerAddress;
use Socialbox\Objects\Standard\ServerInformation;
use Throwable;
class Socialbox
{
/**
* Handles incoming client requests by validating required headers and processing
* the request based on its type. The method ensures proper handling of
* specific request types like RPC, session initiation, and DHE exchange,
* while returning an appropriate HTTP response for invalid or missing data.
* Handles incoming client requests by parsing request headers, determining the request type,
* and routing the request to the appropriate handler method. Implements error handling for
* missing or invalid request types.
*
* @return void
*/
public static function handleRequest(): void
{
$requestHeaders = Utilities::getRequestHeaders();
if(!isset($requestHeaders[StandardHeaders::REQUEST_TYPE->value]))
{
http_response_code(400);
print('Missing required header: ' . StandardHeaders::REQUEST_TYPE->value);
self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::REQUEST_TYPE->value);
return;
}
$clientRequest = new ClientRequest($requestHeaders, file_get_contents('php://input') ?? null);
// Handle the request type, only `init` and `dhe` are not encrypted using the session's encrypted key
// Handle the request type, only `init` and `dhe` are not encrypted using the session's encrypted key
// RPC Requests must be encrypted and signed by the client, vice versa for server responses.
switch(RequestType::tryFrom($clientRequest->getHeader(StandardHeaders::REQUEST_TYPE)))
switch($clientRequest->getRequestType())
{
case RequestType::INFO:
self::handleInformationRequest();
break;
case RequestType::INITIATE_SESSION:
self::handleInitiateSession($clientRequest);
break;
@ -64,58 +69,66 @@
break;
default:
http_response_code(400);
print('Invalid Request-Type header');
break;
self::returnError(400, StandardError::BAD_REQUEST, 'Invalid Request-Type header');
}
}
/**
* Validates the headers in an initialization request to ensure that all
* required information is present and properly formatted. This includes
* 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.
* Handles an information request by setting the appropriate HTTP response code,
* content type headers, and printing the server information in JSON format.
*
* @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.
*/
private static function validateInitHeaders(ClientRequest $clientRequest): bool
{
if(!$clientRequest->getClientName())
{
http_response_code(400);
print('Missing required header: ' . StandardHeaders::CLIENT_NAME->value);
self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::CLIENT_NAME->value);
return false;
}
if(!$clientRequest->getClientVersion())
{
http_response_code(400);
print('Missing required header: ' . StandardHeaders::CLIENT_VERSION->value);
self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::CLIENT_VERSION->value);
return false;
}
if(!$clientRequest->headerExists(StandardHeaders::PUBLIC_KEY))
if(!$clientRequest->headerExists(StandardHeaders::SIGNING_PUBLIC_KEY))
{
http_response_code(400);
print('Missing required header: ' . StandardHeaders::PUBLIC_KEY->value);
self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::SIGNING_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;
}
if(!$clientRequest->headerExists(StandardHeaders::IDENTIFY_AS))
{
http_response_code(400);
print('Missing required header: ' . StandardHeaders::IDENTIFY_AS->value);
self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::IDENTIFY_AS->value);
return false;
}
if(!Validator::validatePeerAddress($clientRequest->getHeader(StandardHeaders::IDENTIFY_AS)))
{
http_response_code(400);
print('Invalid Identify-As header: ' . $clientRequest->getHeader(StandardHeaders::IDENTIFY_AS));
self::returnError(400, StandardError::BAD_REQUEST, 'Invalid Identify-As header: ' . $clientRequest->getHeader(StandardHeaders::IDENTIFY_AS));
return false;
}
@ -123,24 +136,25 @@
}
/**
* Processes a client request to initiate a session. Validates required headers,
* ensures the peer is authorized and enabled, and creates a new session UUID
* if all checks pass. Handles edge cases like missing headers, invalid inputs,
* or unauthorized peers.
* Handles the initiation of a session for a client request. This involves validating headers,
* verifying peer identities, resolving domains, registering peers if necessary, and finally
* creating a session while providing the required session UUID as a response.
*
* @param ClientRequest $clientRequest The request from the client containing
* the required headers and information.
* @param ClientRequest $clientRequest The incoming client request containing all necessary headers
* and identification information required to initiate the session.
* @return void
*/
private static function handleInitiateSession(ClientRequest $clientRequest): void
{
// This is only called for the `init` request type
if(!self::validateInitHeaders($clientRequest))
{
return;
}
// 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($clientRequest->getIdentifyAs()->getDomain() === Configuration::getInstanceConfiguration()->getDomain())
@ -148,9 +162,8 @@
// Prevent the peer from identifying as the host unless it's coming from an external domain
if($clientRequest->getIdentifyAs()->getUsername() === ReservedUsernames::HOST->value)
{
http_response_code(403);
print('Unauthorized: The requested peer is not allowed to identify as the host');
return;
self::returnError(403, StandardError::FORBIDDEN, 'Unauthorized: Not allowed to identify as the host');
return;
}
}
// If the peer is identifying as an external domain
@ -159,64 +172,49 @@
// Only allow the host to identify as an external peer
if($clientRequest->getIdentifyAs()->getUsername() !== ReservedUsernames::HOST->value)
{
http_response_code(403);
print('Unauthorized: The requested peer is not allowed to identify as an external peer');
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');
return;
}
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());
// Override the public key with the resolved server's public key
$publicKey = $resolvedServer->getPublicKey();
}
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;
// Override the public signing key with the resolved server's public key
// Encryption key can be left as is.
$clientPublicSigningKey = $resolvedServer->getPublicSigningKey();
}
catch (Exception $e)
{
Logger::getLogger()->error('An internal error occurred while resolving the host domain', $e);
http_response_code(500);
if(Configuration::getSecurityConfiguration()->isDisplayInternalExceptions())
{
print(Utilities::throwableToString($e));
}
else
{
print('An internal error occurred');
}
self::returnError(502, StandardError::RESOLUTION_FAILED, 'Conflict: Failed to resolve the host domain: ' . $e->getMessage(), $e);
return;
}
}
try
{
// Check if we have a registered peer with the same address
$registeredPeer = RegisteredPeerManager::getPeerByAddress($clientRequest->getIdentifyAs());
// If the peer is registered, check if it is enabled
if($registeredPeer !== null && !$registeredPeer->isEnabled())
{
// Refuse to create a session if the peer is disabled/banned
// This also prevents multiple sessions from being created for the same peer
// A cron job should be used to clean up disabled peers
http_response_code(403);
print('Unauthorized: The requested peer is disabled/banned');
// Refuse to create a session if the peer is disabled/banned, this usually happens when
// a peer gets banned or more commonly when a client attempts to register as this peer but
// destroyed the session before it was created.
// This is to prevent multiple sessions from being created for the same peer, this is cleaned up
// with a cron job using `socialbox clean-sessions`
self::returnError(403, StandardError::FORBIDDEN, 'Unauthorized: The requested peer is disabled/banned');
return;
}
// Otherwise the peer isn't registered, so we need to register it
else
{
// Check if registration is enabled
if(!Configuration::getRegistrationConfiguration()->isRegistrationEnabled())
{
http_response_code(403);
print('Unauthorized: Registration is disabled');
self::returnError(401, StandardError::UNAUTHORIZED, 'Unauthorized: Registration is disabled');
return;
}
@ -226,141 +224,220 @@
$registeredPeer = RegisteredPeerManager::getPeer($peerUuid);
}
// Create the session UUID
$sessionUuid = SessionManager::createSession($publicKey, $registeredPeer, $clientRequest->getClientName(), $clientRequest->getClientVersion());
// Generate server's encryption keys for this session
$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
header('Content-Type: text/plain');
header(StandardHeaders::ENCRYPTION_PUBLIC_KEY->value . ': ' . $serverEncryptionKey->getPublicKey());
print($sessionUuid); // Return the session UUID
}
catch(InvalidArgumentException $e)
{
http_response_code(412); // Precondition failed
print($e->getMessage()); // Why the request failed
// This is usually thrown due to an invalid input
self::returnError(400, StandardError::BAD_REQUEST, $e->getMessage(), $e);
}
catch(Exception $e)
{
Logger::getLogger()->error('An internal error occurred while initiating the session', $e);
http_response_code(500); // Internal server error
if(Configuration::getSecurityConfiguration()->isDisplayInternalExceptions())
{
print(Utilities::throwableToString($e));
}
else
{
print('An internal error occurred');
}
self::returnError(500, StandardError::INTERNAL_SERVER_ERROR, 'An internal error occurred while initiating the session', $e);
}
}
/**
* Handles the Diffie-Hellman key exchange by decrypting the encrypted key passed on from the client using
* the server's private key and setting the encryption key to the session.
* Handles the Diffie-Hellman Ephemeral (DHE) key exchange process between the client and server,
* 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
* 400: Bad request
* 500: Internal server error
* 204: Success, no content.
* @param ClientRequest $clientRequest The request object containing headers, body, and session details
* required to perform the DHE exchange.
*
* @param ClientRequest $clientRequest
* @return 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))
{
Logger::getLogger()->verbose('Missing required header: ' . StandardHeaders::SESSION_UUID->value);
http_response_code(412);
print('Missing required header: ' . StandardHeaders::SESSION_UUID->value);
self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::SESSION_UUID->value);
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()))
{
Logger::getLogger()->verbose('Bad request: The key exchange request body is empty');
http_response_code(400);
print('Bad request: The key exchange request body is empty');
self::returnError(400, StandardError::BAD_REQUEST, 'Bad request: The key exchange request body is empty');
return;
}
// Check if the session is awaiting a DHE exchange
if($clientRequest->getSession()->getState() !== SessionState::AWAITING_DHE)
// Check if the session is awaiting a DHE exchange, forbidden if not
$session = $clientRequest->getSession();
if($session->getState() !== SessionState::AWAITING_DHE)
{
Logger::getLogger()->verbose('Bad request: The session is not awaiting a DHE exchange');
http_response_code(400);
print('Bad request: The session is not awaiting a DHE exchange');
self::returnError(403, StandardError::FORBIDDEN, 'Bad request: The session is not awaiting a DHE exchange');
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
{
// Attempt to decrypt the encrypted key passed on from the client
$encryptionKey = Cryptography::decryptContent($clientRequest->getRequestBody(), Configuration::getInstanceConfiguration()->getPrivateKey());
$sharedSecret = Cryptography::performDHE($session->getClientPublicEncryptionKey(), $session->getServerPrivateEncryptionKey());
}
catch (Exceptions\CryptographyException $e)
catch (CryptographyException $e)
{
Logger::getLogger()->error(sprintf('Bad Request: Failed to decrypt the key for session %s', $clientRequest->getSessionUuid()), $e);
http_response_code(400);
print('Bad Request: Cryptography error, make sure you have encrypted the key using the server\'s public key; ' . $e->getMessage());
Logger::getLogger()->error('Failed to perform DHE exchange', $e);
self::returnError(422, StandardError::CRYPTOGRAPHIC_ERROR, 'DHE exchange failed', $e);
return;
}
// STAGE 1: CLIENT -> SERVER
try
{
// Finally set the encryption key to the session
SessionManager::setEncryptionKey($clientRequest->getSessionUuid(), $encryptionKey);
// Attempt to decrypt the encrypted key passed on from the client using the shared secret
$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)
{
Logger::getLogger()->error('Failed to set the encryption key for the session', $e);
http_response_code(500);
if(Configuration::getSecurityConfiguration()->isDisplayInternalExceptions())
{
print(Utilities::throwableToString($e));
}
else
{
print('Internal Server Error: Failed to set the encryption key for the session');
}
self::returnError(500, StandardError::INTERNAL_SERVER_ERROR, 'Failed to set the encryption key for the session', $e);
return;
}
Logger::getLogger()->info(sprintf('DHE exchange completed for session %s', $clientRequest->getSessionUuid()));
http_response_code(204); // Success, no content
// Return the encrypted transport key for the server back to the client.
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,
* and returns the appropriate response(s) or error(s).
* Handles a Remote Procedure Call (RPC) request, ensuring proper decryption,
* 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
*/
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))
{
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);
print('Missing required header: ' . StandardHeaders::SESSION_UUID->value);
if(!$clientRequest->headerExists(StandardHeaders::SIGNATURE))
{
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;
}
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)
{
http_response_code($e->getCode());
print($e->getMessage());
self::returnError($e->getCode(), $e->getStandardError(), $e->getMessage());
return;
}
@ -442,16 +519,24 @@
return;
}
$session = $clientRequest->getSession();
try
{
$encryptedResponse = Cryptography::encryptTransport($response, $clientRequest->getSession()->getEncryptionKey());
$signature = Cryptography::signContent($response, Configuration::getInstanceConfiguration()->getPrivateKey(), true);
$encryptedResponse = Cryptography::encryptMessage(
message: $response,
encryptionKey: $session->getClientTransportEncryptionKey(),
algorithm: Configuration::getCryptographyConfiguration()->getTransportEncryptionAlgorithm()
);
$signature = Cryptography::signMessage(
message: $response,
privateKey: Configuration::getCryptographyConfiguration()->getHostPrivateKey()
);
}
catch (Exceptions\CryptographyException $e)
{
Logger::getLogger()->error('Failed to encrypt the response', $e);
http_response_code(500);
print('Internal Server Error: Failed to encrypt the response');
self::returnError(500, StandardError::INTERNAL_SERVER_ERROR, 'Failed to encrypt the server response', $e);
return;
}
@ -460,4 +545,69 @@
header(StandardHeaders::SIGNATURE->value . ': ' . $signature);
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);
}
}
}