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

Closed
netkas wants to merge 421 commits from master into dev
13 changed files with 857 additions and 134 deletions
Showing only changes of commit 86435a3d0b - Show all commits

1
.idea/sqldialects.xml generated
View file

@ -6,6 +6,7 @@
<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/RegisteredPeerManager.php" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/ResolvedServersManager.php" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/SessionManager.php" dialect="MariaDB" />

View file

@ -1,152 +1,172 @@
<?php
namespace Socialbox\Classes\CliCommands;
namespace Socialbox\Classes\CliCommands;
use Exception;
use PDOException;
use Socialbox\Abstracts\CacheLayer;
use Socialbox\Classes\Configuration;
use Socialbox\Classes\Cryptography;
use Socialbox\Classes\Database;
use Socialbox\Classes\Logger;
use Socialbox\Classes\Resources;
use Socialbox\Enums\DatabaseObjects;
use Socialbox\Exceptions\CryptographyException;
use Socialbox\Interfaces\CliCommandInterface;
use Exception;
use PDOException;
use Socialbox\Abstracts\CacheLayer;
use Socialbox\Classes\Configuration;
use Socialbox\Classes\Cryptography;
use Socialbox\Classes\Database;
use Socialbox\Classes\Logger;
use Socialbox\Classes\Resources;
use Socialbox\Enums\DatabaseObjects;
use Socialbox\Exceptions\CryptographyException;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Interfaces\CliCommandInterface;
use Socialbox\Managers\EncryptionRecordsManager;
class InitializeCommand implements CliCommandInterface
{
/**
* @inheritDoc
*/
public static function execute(array $args): int
class InitializeCommand implements CliCommandInterface
{
if(Configuration::getInstanceConfiguration()->isEnabled() === false && !isset($args['force']))
/**
* @inheritDoc
*/
public static function execute(array $args): int
{
$required_configurations = [
'database.host', 'database.port', 'database.username', 'database.password', 'database.name',
'instance.enabled', 'instance.domain', 'registration.*'
];
Logger::getLogger()->error('Socialbox is disabled. Use --force to initialize the instance or set `instance.enabled` to True in the configuration');
Logger::getLogger()->info('The reason you are required to do this is to allow you to configure the instance before enabling it');
Logger::getLogger()->info('The following configurations are required to be set before enabling the instance:');
foreach($required_configurations as $config)
if(Configuration::getInstanceConfiguration()->isEnabled() === false && !isset($args['force']))
{
Logger::getLogger()->info(sprintf(' - %s', $config));
}
$required_configurations = [
'database.host', 'database.port', 'database.username', 'database.password', 'database.name',
'instance.enabled', 'instance.domain', 'registration.*'
];
Logger::getLogger()->info('instance.private_key & instance.public_key are automatically generated if not set');
Logger::getLogger()->info('instance.domain is required to be set to the domain name of the instance');
Logger::getLogger()->info('instance.rpc_endpoint is required to be set to the publicly accessible http rpc endpoint of this server');
Logger::getLogger()->info('registration.* are required to be set to allow users to register to the instance');
Logger::getLogger()->info('You will be given a DNS TXT record to set for the public key after the initialization process');
Logger::getLogger()->info('The configuration file can be edited using ConfigLib:');
Logger::getLogger()->info(' configlib --conf socialbox -e nano');
Logger::getLogger()->info('Or manually at:');
Logger::getLogger()->info(sprintf(' %s', Configuration::getConfigurationLib()->getPath()));
return 1;
}
if(Configuration::getInstanceConfiguration()->getDomain() === null)
{
Logger::getLogger()->error('instance.domain is required but was not set');
return 1;
}
if(Configuration::getInstanceConfiguration()->getRpcEndpoint() === null)
{
Logger::getLogger()->error('instance.rpc_endpoint is required but was not set');
return 1;
}
Logger::getLogger()->info('Initializing Socialbox...');
if(Configuration::getCacheConfiguration()->isEnabled())
{
Logger::getLogger()->verbose('Clearing cache layer...');
CacheLayer::getInstance()->clear();
}
foreach(DatabaseObjects::casesOrdered() as $object)
{
Logger::getLogger()->verbose("Initializing database object {$object->value}");
try
{
Database::getConnection()->exec(file_get_contents(Resources::getDatabaseResource($object)));
}
catch (PDOException $e)
{
// Check if the error code is for "table already exists"
if ($e->getCode() === '42S01')
Logger::getLogger()->error('Socialbox is disabled. Use --force to initialize the instance or set `instance.enabled` to True in the configuration');
Logger::getLogger()->info('The reason you are required to do this is to allow you to configure the instance before enabling it');
Logger::getLogger()->info('The following configurations are required to be set before enabling the instance:');
foreach($required_configurations as $config)
{
Logger::getLogger()->warning("Database object {$object->value} already exists, skipping...");
continue;
Logger::getLogger()->info(sprintf(' - %s', $config));
}
else
Logger::getLogger()->info('instance.private_key & instance.public_key are automatically generated if not set');
Logger::getLogger()->info('instance.domain is required to be set to the domain name of the instance');
Logger::getLogger()->info('instance.rpc_endpoint is required to be set to the publicly accessible http rpc endpoint of this server');
Logger::getLogger()->info('registration.* are required to be set to allow users to register to the instance');
Logger::getLogger()->info('You will be given a DNS TXT record to set for the public key after the initialization process');
Logger::getLogger()->info('The configuration file can be edited using ConfigLib:');
Logger::getLogger()->info(' configlib --conf socialbox -e nano');
Logger::getLogger()->info('Or manually at:');
Logger::getLogger()->info(sprintf(' %s', Configuration::getConfigurationLib()->getPath()));
return 1;
}
if(Configuration::getInstanceConfiguration()->getDomain() === null)
{
Logger::getLogger()->error('instance.domain is required but was not set');
return 1;
}
if(Configuration::getInstanceConfiguration()->getRpcEndpoint() === null)
{
Logger::getLogger()->error('instance.rpc_endpoint is required but was not set');
return 1;
}
Logger::getLogger()->info('Initializing Socialbox...');
if(Configuration::getCacheConfiguration()->isEnabled())
{
Logger::getLogger()->verbose('Clearing cache layer...');
CacheLayer::getInstance()->clear();
}
foreach(DatabaseObjects::casesOrdered() as $object)
{
Logger::getLogger()->verbose("Initializing database object {$object->value}");
try
{
Database::getConnection()->exec(file_get_contents(Resources::getDatabaseResource($object)));
}
catch (PDOException $e)
{
// Check if the error code is for "table already exists"
if ($e->getCode() === '42S01')
{
Logger::getLogger()->warning("Database object {$object->value} already exists, skipping...");
continue;
}
else
{
Logger::getLogger()->error("Failed to initialize database object {$object->value}: {$e->getMessage()}", $e);
return 1;
}
}
catch(Exception $e)
{
Logger::getLogger()->error("Failed to initialize database object {$object->value}: {$e->getMessage()}", $e);
return 1;
}
}
catch(Exception $e)
{
Logger::getLogger()->error("Failed to initialize database object {$object->value}: {$e->getMessage()}", $e);
return 1;
}
}
if(
!Configuration::getInstanceConfiguration()->getPublicKey() ||
!Configuration::getInstanceConfiguration()->getPrivateKey() ||
!Configuration::getInstanceConfiguration()->getEncryptionKey()
)
{
if(
!Configuration::getInstanceConfiguration()->getPublicKey() ||
!Configuration::getInstanceConfiguration()->getPrivateKey() ||
!Configuration::getInstanceConfiguration()->getEncryptionKeys()
)
{
try
{
Logger::getLogger()->info('Generating new key pair...');
$keyPair = Cryptography::generateKeyPair();
$encryptionKeys = Cryptography::randomKeyS(230, 314, Configuration::getInstanceConfiguration()->getEncryptionKeysCount());
}
catch (CryptographyException $e)
{
Logger::getLogger()->error('Failed to generate cryptography values', $e);
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()
));
}
try
{
Logger::getLogger()->info('Generating new key pair...');
$keyPair = Cryptography::generateKeyPair();
$encryptionKey = Cryptography::randomBytes(230, 314);
if(EncryptionRecordsManager::getRecordCount() < Configuration::getInstanceConfiguration()->getEncryptionRecordsCount())
{
Logger::getLogger()->info('Generating encryption records...');
EncryptionRecordsManager::generateRecords(Configuration::getInstanceConfiguration()->getEncryptionRecordsCount());
}
}
catch (CryptographyException $e)
{
Logger::getLogger()->error('Failed to generate cryptography values', $e);
return 1;
Logger::getLogger()->error('Failed to generate encryption records due to a cryptography exception', $e);
}
catch (DatabaseOperationException $e)
{
Logger::getLogger()->error('Failed to generate encryption records due to a database error', $e);
}
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_key', $encryptionKey);
Configuration::getConfigurationLib()->save();
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()
));
// TODO: Create a host peer here?
Logger::getLogger()->info('Socialbox has been initialized successfully');
return 0;
}
// TODO: Create a host peer here?
Logger::getLogger()->info('Socialbox has been initialized successfully');
return 0;
}
/**
* @inheritDoc
*/
public static function getHelpMessage(): string
{
return "Initialize Command - Initializes Socialbox for first-runs\n" .
"Usage: socialbox init [arguments]\n\n" .
"Arguments:\n" .
" --force - Forces the initialization process to run even the instance is disabled\n";
}
/**
* @inheritDoc
*/
public static function getHelpMessage(): string
{
return "Initialize Command - Initializes Socialbox for first-runs\n" .
"Usage: socialbox init [arguments]\n\n" .
"Arguments:\n" .
" --force - Forces the initialization process to run even the instance is disabled\n";
}
/**
* @inheritDoc
*/
public static function getShortHelpMessage(): string
{
return "Initializes Socialbox for first-runs";
}
}
/**
* @inheritDoc
*/
public static function getShortHelpMessage(): string
{
return "Initializes Socialbox for first-runs";
}
}

View file

@ -34,9 +34,11 @@ class Configuration
$config->setDefault('instance.enabled', false); // False by default, requires the user to enable it.
$config->setDefault('instance.domain', null);
$config->setDefault('instance.rpc_endpoint', null);
$config->setDefault('instance.encryption_keys_count', 5);
$config->setDefault('instance.encryption_record_count', 5);
$config->setDefault('instance.private_key', null);
$config->setDefault('instance.public_key', null);
$config->setDefault('instance.encryption_key', null);
$config->setDefault('instance.encryption_keys', null);
// Security Configuration
$config->setDefault('security.display_internal_exceptions', false);
@ -89,6 +91,25 @@ class Configuration
self::$registrationConfiguration = new RegistrationConfiguration(self::$configuration->getConfiguration()['registration']);
}
/**
* Resets all configuration instances by setting them to null and then
* reinitializes the configurations.
*
* @return void
*/
public static function reload(): void
{
self::$configuration = null;
self::$instanceConfiguration = null;
self::$securityConfiguration = null;
self::$databaseConfiguration = null;
self::$loggingConfiguration = null;
self::$cacheConfiguration = null;
self::$registrationConfiguration = null;
self::initializeConfiguration();
}
/**
* Retrieves the current configuration array. If the configuration is not initialized,
* it triggers the initialization process.

View file

@ -7,9 +7,11 @@
private bool $enabled;
private ?string $domain;
private ?string $rpcEndpoint;
private int $encryptionKeysCount;
private int $encryptionRecordsCount;
private ?string $privateKey;
private ?string $publicKey;
private ?string $encryptionKey;
private ?array $encryptionKeys;
/**
* Constructor that initializes object properties with the provided data.
@ -22,9 +24,11 @@
$this->enabled = (bool)$data['enabled'];
$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->encryptionKey = $data['encryption_key'];
$this->encryptionKeys = $data['encryption_keys'];
}
/**
@ -55,6 +59,26 @@
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.
*
@ -76,12 +100,20 @@
}
/**
* Retrieves the encryption key.
* Retrieves the encryption keys.
*
* @return string|null The encryption key.
* @return array|null The encryption keys.
*/
public function getEncryptionKey(): ?string
public function getEncryptionKeys(): ?array
{
return $this->encryptionKey;
return $this->encryptionKeys;
}
/**
* @return string
*/
public function getRandomEncryptionKey(): string
{
return $this->encryptionKeys[array_rand($this->encryptionKeys)];
}
}

View file

@ -276,7 +276,7 @@ class Cryptography
* @return string A hexadecimal string representing the random byte sequence.
* @throws CryptographyException If the random byte generation fails.
*/
public static function randomBytes(int $minLength, int $maxLength): string
public static function randomKey(int $minLength, int $maxLength): string
{
try
{
@ -287,4 +287,24 @@ class Cryptography
throw new CryptographyException('Failed to generate random bytes: ' . $e->getMessage());
}
}
/**
* Generates an array of random keys, each with a length within the specified range.
*
* @param int $minLength The minimum length for each random key.
* @param int $maxLength The maximum length for each random key.
* @param int $amount The number of random keys to generate.
* @return array An array of randomly generated keys.
* @throws CryptographyException If the random key generation fails.
*/
public static function randomKeys(int $minLength, int $maxLength, int $amount): array
{
$keys = [];
for($i = 0; $i < $amount; $i++)
{
$keys[] = self::randomKey($minLength, $maxLength);
}
return $keys;
}
}

View file

@ -0,0 +1,96 @@
<?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

@ -0,0 +1,73 @@
<?php
namespace Socialbox\Classes\StandardMethods;
use Socialbox\Abstracts\Method;
use Socialbox\Classes\Configuration;
use Socialbox\Classes\Validator;
use Socialbox\Enums\StandardError;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\StandardException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\RegisteredPeerManager;
use Socialbox\Managers\SessionManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
class Identify extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
// Check if the username parameter exists
if(!$rpcRequest->containsParameter('username'))
{
return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing parameter \'username\'');
}
// Check if the username is valid
if(!Validator::validateUsername($rpcRequest->getParameter('username')))
{
return $rpcRequest->produceError(StandardError::INVALID_USERNAME, StandardError::INVALID_USERNAME->getMessage());
}
// Check if the request has a Session UUID
if($request->getSessionUuid() === null)
{
return $rpcRequest->produceError(StandardError::SESSION_REQUIRED);
}
try
{
// Get the session and check if it's already authenticated
$session = SessionManager::getSession($request->getSessionUuid());
// If the session is already authenticated, return an error
if($session->getPeerUuid() !== null)
{
return $rpcRequest->produceError(StandardError::ALREADY_AUTHENTICATED);
}
// If the username does not exist, return an error
if(!RegisteredPeerManager::usernameExists($rpcRequest->getParameter('username')))
{
return $rpcRequest->produceError(StandardError::NOT_REGISTERED, StandardError::NOT_REGISTERED->getMessage());
}
// Create session to be identified as the provided username
SessionManager::updatePeer($session->getUuid(), $rpcRequest->getParameter('username'));
// Set the required session flags
$initialFlags = [];
}
catch(DatabaseOperationException $e)
{
throw new StandardException("There was an unexpected error while trying to register", StandardError::INTERNAL_SERVER_ERROR, $e);
}
// Return true to indicate the operation was a success
return $rpcRequest->produceResponse(true);
}
}

View file

@ -0,0 +1,205 @@
<?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

@ -0,0 +1,41 @@
<?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

@ -0,0 +1,84 @@
<?php
namespace Socialbox\Objects\Database;
use Socialbox\Classes\Configuration;
use Socialbox\Classes\SecuredPassword;
use Socialbox\Exceptions\CryptographyException;
use Socialbox\Managers\EncryptionRecordsManager;
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

@ -0,0 +1,84 @@
<?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'];
$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;
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Socialbox\Classes;
use PHPUnit\Framework\TestCase;
use Socialbox\Managers\EncryptionRecordsManager;
class SecuredPasswordTest extends TestCase
{
public function testVerifyPassword()
{
print("Getting random encryption record\n");
$encryptionRecord = EncryptionRecordsManager::getRandomRecord();
var_dump($encryptionRecord);
print("Securing password\n");
$securedPassword = SecuredPassword::securePassword('123-123-123', 'password!', $encryptionRecord);
print("Verifying password\n");
$this->assertTrue(SecuredPassword::verifyPassword('password!', $securedPassword, EncryptionRecordsManager::getAllRecords()));
}
}

24
tests/test.php Normal file
View file

@ -0,0 +1,24 @@
<?php
use Socialbox\Classes\SecuredPassword;
use Socialbox\Managers\EncryptionRecordsManager;
require 'ncc';
import('net.nosial.socialbox');
print("Getting random encryption record\n");
$encryptionRecord = EncryptionRecordsManager::getRandomRecord();
var_dump($encryptionRecord);
print("Securing password\n");
$securedPassword = SecuredPassword::securePassword('123-123-123', 'password!', $encryptionRecord);
print("Verifying password\n");
if(SecuredPassword::verifyPassword('password!', $securedPassword, EncryptionRecordsManager::getAllRecords()))
{
print("Password verified\n");
}
else
{
print("Password not verified\n");
}