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 @@ +