- Ajout du service PasswordSecurityService conforme NIST SP 800-63B - Vérification des mots de passe contre la base Have I Been Pwned - Validation : minimum 8 caractères, maximum 64 caractères - Pas d'exigences de composition obligatoires (conforme NIST) - Intégration dans LoginController et UserController - Génération de mots de passe sécurisés non compromis 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
328 lines
11 KiB
PHP
Executable File
328 lines
11 KiB
PHP
Executable File
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use PHPMailer\PHPMailer\PHPMailer;
|
|
use PHPMailer\PHPMailer\SMTP;
|
|
use PHPMailer\PHPMailer\Exception;
|
|
use App\Services\PasswordSecurityService;
|
|
|
|
require_once __DIR__ . '/EmailTemplates.php';
|
|
require_once __DIR__ . '/PasswordSecurityService.php';
|
|
|
|
class ApiService {
|
|
|
|
/**
|
|
* Envoie un email selon un type prédéfini
|
|
*
|
|
* @param string $email Email du destinataire
|
|
* @param string $name Nom du destinataire
|
|
* @param string $type Type d'email ('welcome', 'lostpwd', etc.)
|
|
* @param array $data Données supplémentaires pour le template
|
|
* @return int 1 si succès, 0 si échec
|
|
*/
|
|
public static function sendEmail(string $email, string $name, string $type, array $data = []): int {
|
|
require_once __DIR__ . '/../../vendor/autoload.php';
|
|
|
|
$name = ucwords($name);
|
|
try {
|
|
$mail = new PHPMailer(true);
|
|
|
|
// Récupération des paramètres SMTP depuis la configuration
|
|
$appConfig = AppConfig::getInstance();
|
|
$smtpConfig = $appConfig->getSmtpConfig();
|
|
$emailConfig = $appConfig->getEmailConfig();
|
|
|
|
// Configuration du serveur
|
|
$mail->isSMTP();
|
|
$mail->Host = $smtpConfig['host'];
|
|
$mail->SMTPAuth = $smtpConfig['auth'];
|
|
$mail->Username = $smtpConfig['user'];
|
|
$mail->Password = $smtpConfig['pass'];
|
|
$mail->SMTPSecure = $smtpConfig['secure'];
|
|
$mail->Port = $smtpConfig['port'];
|
|
|
|
// Configuration de base
|
|
$mail->setFrom($emailConfig['from'], 'GEOSECTOR');
|
|
$mail->addAddress($email, $name);
|
|
$mail->isHTML(true);
|
|
$mail->CharSet = 'UTF-8';
|
|
|
|
// Configuration selon le type d'email
|
|
switch ($type) {
|
|
case 'welcome':
|
|
$mail->Subject = 'Bienvenue sur GEOSECTOR';
|
|
$mail->Body = EmailTemplates::getWelcomeTemplate($name, $data['username'] ?? '', $data['password']);
|
|
break;
|
|
|
|
case 'welcome_username':
|
|
$mail->Subject = 'GEOSECTOR - Votre identifiant de connexion';
|
|
$mail->Body = EmailTemplates::getWelcomeUsernameTemplate($name, $data['username'] ?? '');
|
|
break;
|
|
|
|
case 'welcome_password':
|
|
$mail->Subject = 'GEOSECTOR - Votre mot de passe';
|
|
$mail->Body = EmailTemplates::getWelcomePasswordTemplate($name, $data['password'] ?? '');
|
|
break;
|
|
|
|
case 'lostpwd':
|
|
$mail->Subject = 'Réinitialisation de votre mot de passe GEOSECTOR';
|
|
$mail->Body = EmailTemplates::getLostPasswordTemplate($name, $data['username'] ?? '', $data['password']);
|
|
break;
|
|
|
|
case 'alert':
|
|
$mail->Subject = $data['subject'] ?? 'Alerte GEOSECTOR';
|
|
$mail->Body = EmailTemplates::getAlertTemplate($data['subject'] ?? 'Alerte', $data['message'] ?? '');
|
|
break;
|
|
|
|
case 'receipt':
|
|
$mail->Subject = 'Reçu de passage GEOSECTOR';
|
|
$mail->Body = EmailTemplates::getReceiptTemplate(
|
|
$name,
|
|
$data['date'] ?? date('d/m/Y'),
|
|
$data['address'] ?? '',
|
|
$data['amount'] ?? '0',
|
|
$data['paymentMethod'] ?? 'Espèces'
|
|
);
|
|
break;
|
|
|
|
default:
|
|
throw new Exception("Type d'email non reconnu");
|
|
}
|
|
|
|
$mail->send();
|
|
LogService::log("Email '$type' envoyé avec succès", [
|
|
'level' => 'info',
|
|
'email' => $email,
|
|
'type' => $type
|
|
]);
|
|
|
|
return 1;
|
|
} catch (Exception $e) {
|
|
LogService::log("Échec d'envoi d'email", [
|
|
'level' => 'error',
|
|
'error' => $e->getMessage(),
|
|
'email' => $email,
|
|
'type' => $type
|
|
]);
|
|
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Pour les données qui servent de clé de recherche (comme l'email)
|
|
public static function encryptSearchableData(string $data): string {
|
|
if (empty($data)) {
|
|
return '';
|
|
}
|
|
|
|
// Forcer un padding cohérent en ajoutant un caractère spécial de contrôle
|
|
$data = $data . "\x01"; // Garantit que même un bloc parfait reçoit du padding
|
|
|
|
$keyBase64 = AppConfig::getInstance()->getEncryptionKey();
|
|
$key = base64_decode($keyBase64); // Décoder la clé base64 en binaire
|
|
$iv = str_repeat("\0", 16); // IV fixe
|
|
|
|
// Expliciter les options de padding
|
|
$options = 0; // PKCS7 padding par défaut
|
|
$encrypted = openssl_encrypt($data, 'AES-256-CBC', $key, $options, $iv);
|
|
|
|
return base64_encode($encrypted);
|
|
}
|
|
|
|
// Pour les données qui ne servent pas de clé de recherche (comme le nom)
|
|
public static function encryptData(string $data): string {
|
|
if (empty($data)) {
|
|
return '';
|
|
}
|
|
|
|
$keyBase64 = AppConfig::getInstance()->getEncryptionKey();
|
|
$key = base64_decode($keyBase64); // Décoder la clé base64 en binaire
|
|
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('AES-256-CBC'));
|
|
$encrypted = openssl_encrypt($data, 'AES-256-CBC', $key, 0, $iv);
|
|
return base64_encode($iv . $encrypted);
|
|
}
|
|
|
|
public static function decryptSearchableData(string $encryptedData): string {
|
|
if (empty($encryptedData)) {
|
|
return '';
|
|
}
|
|
|
|
// Décoder la chaîne base64
|
|
$encrypted = base64_decode($encryptedData);
|
|
|
|
// Vérifier que le décodage base64 a fonctionné
|
|
if ($encrypted === false) {
|
|
return ''; // Échec du décodage, retourner une chaîne vide
|
|
}
|
|
|
|
// Méthode simple et robuste utilisant le même IV fixe que pour le chiffrement
|
|
$keyBase64 = AppConfig::getInstance()->getEncryptionKey();
|
|
$key = base64_decode($keyBase64); // Décoder la clé base64 en binaire
|
|
$iv = str_repeat("\0", 16); // IV fixe identique à celui utilisé pour le chiffrement
|
|
|
|
// Déchiffrer avec la méthode standard
|
|
$decrypted = openssl_decrypt($encrypted, 'AES-256-CBC', $key, 0, $iv);
|
|
|
|
// Si le déchiffrement a échoué, retourner une chaîne vide
|
|
if ($decrypted === false) {
|
|
return '';
|
|
}
|
|
|
|
// Supprimer uniquement le caractère de contrôle ajouté
|
|
if (substr($decrypted, -1) === "\x01") {
|
|
return substr($decrypted, 0, -1);
|
|
}
|
|
|
|
return $decrypted; // Pour la rétrocompatibilité avec les anciennes données
|
|
}
|
|
|
|
public static function decryptData(string $encryptedData): string {
|
|
if (empty($encryptedData)) {
|
|
return '';
|
|
}
|
|
|
|
$keyBase64 = AppConfig::getInstance()->getEncryptionKey();
|
|
$key = base64_decode($keyBase64); // Décoder la clé base64 en binaire
|
|
|
|
$data = base64_decode($encryptedData);
|
|
|
|
// Vérifier que le décodage base64 a fonctionné
|
|
if ($data === false) {
|
|
return $encryptedData; // Retourner la chaîne d'origine en cas d'échec
|
|
}
|
|
|
|
$ivLength = openssl_cipher_iv_length('AES-256-CBC');
|
|
|
|
// Vérifier que les données sont assez longues pour contenir l'IV
|
|
if (strlen($data) <= $ivLength) {
|
|
return $encryptedData; // Retourner la chaîne d'origine en cas d'échec
|
|
}
|
|
|
|
$iv = substr($data, 0, $ivLength);
|
|
$encrypted = substr($data, $ivLength);
|
|
|
|
$decrypted = openssl_decrypt($encrypted, 'AES-256-CBC', $key, 0, $iv);
|
|
|
|
// Si le déchiffrement échoue, retourner la chaîne d'origine
|
|
return $decrypted !== false ? $decrypted : $encryptedData;
|
|
}
|
|
|
|
/**
|
|
* Génère un nom d'utilisateur unique à partir du nom, du code postal et de la ville
|
|
*
|
|
* @param PDO $db Instance de la base de données
|
|
* @param string $name Nom de l'utilisateur
|
|
* @param string $postalCode Code postal
|
|
* @param string $cityName Nom de la ville
|
|
* @param int $minLength Longueur minimale du nom d'utilisateur (par défaut 10)
|
|
* @return string Nom d'utilisateur généré
|
|
*/
|
|
public static function generateUserName(PDO $db, string $name, string $postalCode, string $cityName, int $minLength = 10): string {
|
|
// Nettoyer et préparer les chaînes
|
|
$name = preg_replace('/[^a-zA-Z0-9]/', '', strtolower($name));
|
|
$postalCode = preg_replace('/[^0-9]/', '', $postalCode);
|
|
$cityName = preg_replace('/[^a-zA-Z0-9]/', '', strtolower($cityName));
|
|
|
|
// Extraire les premières lettres du nom (2 à 5 caractères)
|
|
$nameLength = min(5, max(2, rand(2, strlen($name))));
|
|
$namePart = substr($name, 0, $nameLength);
|
|
|
|
// Extraire une partie du code postal (2 à 3 chiffres)
|
|
$postalLength = min(3, max(2, rand(2, strlen($postalCode))));
|
|
$postalPart = substr($postalCode, 0, $postalLength);
|
|
|
|
// Extraire une partie du nom de la ville (2 à 4 caractères)
|
|
$cityLength = min(4, max(2, rand(2, strlen($cityName))));
|
|
$cityPart = substr($cityName, 0, $cityLength);
|
|
|
|
// Combiner les parties avec un séparateur aléatoire
|
|
$separators = ['', '.', '_', '-'];
|
|
$separator1 = $separators[array_rand($separators)];
|
|
$separator2 = $separators[array_rand($separators)];
|
|
|
|
// Ajouter un nombre aléatoire pour garantir l'unicité
|
|
$randomNum = rand(10, 999);
|
|
|
|
// Construire le nom d'utilisateur
|
|
$username = $namePart . $separator1 . $postalPart . $separator2 . $cityPart . $randomNum;
|
|
|
|
// S'assurer que la longueur minimale est respectée
|
|
while (strlen($username) < $minLength) {
|
|
$username .= rand(0, 9);
|
|
}
|
|
|
|
// Vérifier l'unicité du nom d'utilisateur dans la base de données
|
|
$isUnique = false;
|
|
$attempts = 0;
|
|
$originalUsername = $username;
|
|
|
|
while (!$isUnique && $attempts < 10) {
|
|
// Chiffrer le nom d'utilisateur pour la recherche
|
|
$encryptedUsername = self::encryptSearchableData($username);
|
|
|
|
// Vérifier si le nom d'utilisateur existe déjà
|
|
$stmt = $db->prepare('SELECT COUNT(*) as count FROM users WHERE encrypted_user_name = ?');
|
|
$stmt->execute([$encryptedUsername]);
|
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if ($result && $result['count'] == 0) {
|
|
$isUnique = true;
|
|
} else {
|
|
// Ajouter un nombre aléatoire supplémentaire
|
|
$username = $originalUsername . rand(100, 999);
|
|
$attempts++;
|
|
}
|
|
}
|
|
|
|
return $username;
|
|
}
|
|
|
|
/**
|
|
* Génère un mot de passe sécurisé aléatoire non compromis
|
|
* Utilise le service PasswordSecurityService pour vérifier contre HIBP
|
|
*
|
|
* @param int $minLength Longueur minimale du mot de passe (par défaut 12)
|
|
* @param int $maxLength Longueur maximale du mot de passe (par défaut 16)
|
|
* @return string Mot de passe généré
|
|
*/
|
|
public static function generateSecurePassword(int $minLength = 12, int $maxLength = 16): string {
|
|
$length = random_int($minLength, $maxLength);
|
|
|
|
// Utiliser le nouveau service pour générer un mot de passe non compromis
|
|
$password = PasswordSecurityService::generateSecurePassword($length, 10);
|
|
|
|
// Si le service échoue (très rare), utiliser l'ancienne méthode
|
|
if ($password === null) {
|
|
LogService::log('Fallback vers génération de mot de passe classique', [
|
|
'level' => 'warning',
|
|
'reason' => 'PasswordSecurityService a échoué'
|
|
]);
|
|
|
|
$lowercase = 'abcdefghijklmnopqrstuvwxyz';
|
|
$uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
$numbers = '0123456789';
|
|
$special = '!@#$%^&*()_+-=[]{}|;:,.<>?';
|
|
|
|
$password = '';
|
|
|
|
// Au moins un de chaque type
|
|
$password .= $lowercase[random_int(0, strlen($lowercase) - 1)];
|
|
$password .= $uppercase[random_int(0, strlen($uppercase) - 1)];
|
|
$password .= $numbers[random_int(0, strlen($numbers) - 1)];
|
|
$password .= $special[random_int(0, strlen($special) - 1)];
|
|
|
|
// Compléter avec des caractères aléatoires
|
|
$all = $lowercase . $uppercase . $numbers . $special;
|
|
for ($i = strlen($password); $i < $length; $i++) {
|
|
$password .= $all[random_int(0, strlen($all) - 1)];
|
|
}
|
|
|
|
// Mélanger le mot de passe
|
|
return str_shuffle($password);
|
|
}
|
|
|
|
return $password;
|
|
}
|
|
}
|