diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml
index 016146b..80dc28a 100644
--- a/.idea/sqldialects.xml
+++ b/.idea/sqldialects.xml
@@ -6,6 +6,7 @@
+
diff --git a/src/Socialbox/Classes/CliCommands/InitializeCommand.php b/src/Socialbox/Classes/CliCommands/InitializeCommand.php
index 8498fea..aa3c145 100644
--- a/src/Socialbox/Classes/CliCommands/InitializeCommand.php
+++ b/src/Socialbox/Classes/CliCommands/InitializeCommand.php
@@ -1,152 +1,172 @@
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";
- }
-}
\ No newline at end of file
+ /**
+ * @inheritDoc
+ */
+ public static function getShortHelpMessage(): string
+ {
+ return "Initializes Socialbox for first-runs";
+ }
+ }
\ No newline at end of file
diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php
index c26c9fc..7537424 100644
--- a/src/Socialbox/Classes/Configuration.php
+++ b/src/Socialbox/Classes/Configuration.php
@@ -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.
diff --git a/src/Socialbox/Classes/Configuration/InstanceConfiguration.php b/src/Socialbox/Classes/Configuration/InstanceConfiguration.php
index 4ebafc6..7d8b3d2 100644
--- a/src/Socialbox/Classes/Configuration/InstanceConfiguration.php
+++ b/src/Socialbox/Classes/Configuration/InstanceConfiguration.php
@@ -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)];
}
}
\ No newline at end of file
diff --git a/src/Socialbox/Classes/Cryptography.php b/src/Socialbox/Classes/Cryptography.php
index 7872d60..28e0b17 100644
--- a/src/Socialbox/Classes/Cryptography.php
+++ b/src/Socialbox/Classes/Cryptography.php
@@ -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;
+ }
}
\ No newline at end of file
diff --git a/src/Socialbox/Classes/SecuredPassword.php b/src/Socialbox/Classes/SecuredPassword.php
new file mode 100644
index 0000000..30e031f
--- /dev/null
+++ b/src/Socialbox/Classes/SecuredPassword.php
@@ -0,0 +1,96 @@
+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;
+ }
+ }
diff --git a/src/Socialbox/Classes/StandardMethods/Identify.php b/src/Socialbox/Classes/StandardMethods/Identify.php
new file mode 100644
index 0000000..42d324d
--- /dev/null
+++ b/src/Socialbox/Classes/StandardMethods/Identify.php
@@ -0,0 +1,73 @@
+containsParameter('username'))
+ {
+ return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing parameter \'username\'');
+ }
+
+ // Check if the username is valid
+ if(!Validator::validateUsername($rpcRequest->getParameter('username')))
+ {
+ return $rpcRequest->produceError(StandardError::INVALID_USERNAME, StandardError::INVALID_USERNAME->getMessage());
+ }
+
+ // Check if the request has a Session UUID
+ if($request->getSessionUuid() === null)
+ {
+ return $rpcRequest->produceError(StandardError::SESSION_REQUIRED);
+ }
+
+ try
+ {
+ // Get the session and check if it's already authenticated
+ $session = SessionManager::getSession($request->getSessionUuid());
+
+ // If the session is already authenticated, return an error
+ if($session->getPeerUuid() !== null)
+ {
+ return $rpcRequest->produceError(StandardError::ALREADY_AUTHENTICATED);
+ }
+
+ // If the username does not exist, return an error
+ if(!RegisteredPeerManager::usernameExists($rpcRequest->getParameter('username')))
+ {
+ return $rpcRequest->produceError(StandardError::NOT_REGISTERED, StandardError::NOT_REGISTERED->getMessage());
+ }
+
+ // Create session to be identified as the provided username
+ SessionManager::updatePeer($session->getUuid(), $rpcRequest->getParameter('username'));
+
+ // Set the required session flags
+ $initialFlags = [];
+ }
+ catch(DatabaseOperationException $e)
+ {
+ throw new StandardException("There was an unexpected error while trying to register", StandardError::INTERNAL_SERVER_ERROR, $e);
+ }
+
+ // Return true to indicate the operation was a success
+ return $rpcRequest->produceResponse(true);
+ }
+ }
\ No newline at end of file
diff --git a/src/Socialbox/Managers/EncryptionRecordsManager.php b/src/Socialbox/Managers/EncryptionRecordsManager.php
new file mode 100644
index 0000000..3d2cc90
--- /dev/null
+++ b/src/Socialbox/Managers/EncryptionRecordsManager.php
@@ -0,0 +1,205 @@
+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),
+ ]);
+ }
+ }
\ No newline at end of file
diff --git a/src/Socialbox/Objects/Database/DecryptedRecord.php b/src/Socialbox/Objects/Database/DecryptedRecord.php
new file mode 100644
index 0000000..3757949
--- /dev/null
+++ b/src/Socialbox/Objects/Database/DecryptedRecord.php
@@ -0,0 +1,41 @@
+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;
+ }
+ }
\ No newline at end of file
diff --git a/src/Socialbox/Objects/Database/EncryptionRecord.php b/src/Socialbox/Objects/Database/EncryptionRecord.php
new file mode 100644
index 0000000..e2aad9c
--- /dev/null
+++ b/src/Socialbox/Objects/Database/EncryptionRecord.php
@@ -0,0 +1,84 @@
+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");
+ }
+ }
\ No newline at end of file
diff --git a/src/Socialbox/Objects/Database/SecurePasswordRecord.php b/src/Socialbox/Objects/Database/SecurePasswordRecord.php
new file mode 100644
index 0000000..fb12350
--- /dev/null
+++ b/src/Socialbox/Objects/Database/SecurePasswordRecord.php
@@ -0,0 +1,84 @@
+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;
+ }
+ }
\ No newline at end of file
diff --git a/tests/Socialbox/Classes/SecuredPasswordTest.php b/tests/Socialbox/Classes/SecuredPasswordTest.php
new file mode 100644
index 0000000..85de779
--- /dev/null
+++ b/tests/Socialbox/Classes/SecuredPasswordTest.php
@@ -0,0 +1,22 @@
+assertTrue(SecuredPassword::verifyPassword('password!', $securedPassword, EncryptionRecordsManager::getAllRecords()));
+ }
+ }
diff --git a/tests/test.php b/tests/test.php
new file mode 100644
index 0000000..3d3dd60
--- /dev/null
+++ b/tests/test.php
@@ -0,0 +1,24 @@
+