feat: Implémentation authentification NIST SP 800-63B v3.0.8
- 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>
This commit is contained in:
@@ -216,10 +216,12 @@ class _UserFormState extends State<UserForm> {
|
||||
}
|
||||
}
|
||||
|
||||
// Nettoyer une chaîne pour l'username
|
||||
// Nettoyer une chaîne pour l'username (NIST: garder plus de caractères)
|
||||
String _cleanString(String input) {
|
||||
// Avec NIST, on peut garder plus de caractères, mais pour la génération automatique
|
||||
// on reste sur des caractères simples pour éviter les problèmes
|
||||
return input.toLowerCase()
|
||||
.replaceAll(RegExp(r'[^a-z0-9]'), ''); // Garder seulement lettres et chiffres
|
||||
.replaceAll(RegExp(r'[^a-z0-9\s]'), ''); // Garder lettres, chiffres et espaces
|
||||
}
|
||||
|
||||
// Extraire une partie aléatoire d'une chaîne
|
||||
@@ -235,7 +237,7 @@ class _UserFormState extends State<UserForm> {
|
||||
return cleaned.substring(0, length);
|
||||
}
|
||||
|
||||
// Générer un username selon l'algorithme spécifié
|
||||
// Générer un username selon les nouvelles normes NIST
|
||||
String _generateUsername() {
|
||||
// Récupérer les données nécessaires
|
||||
final nom = _nameController.text.isNotEmpty ? _nameController.text : _sectNameController.text;
|
||||
@@ -248,22 +250,21 @@ class _UserFormState extends State<UserForm> {
|
||||
final villePart = _extractRandomPart(ville, 2, 4);
|
||||
final nombreAleatoire = 10 + _random.nextInt(990); // 10 à 999
|
||||
|
||||
// Choisir des séparateurs aléatoires (uniquement ceux autorisés: ., _, -)
|
||||
final separateurs = ['', '.', '_', '-'];
|
||||
// Choisir des séparateurs aléatoires (on peut maintenant utiliser des espaces aussi)
|
||||
final separateurs = ['', '.', '_', '-', ' '];
|
||||
final sep1 = separateurs[_random.nextInt(separateurs.length)];
|
||||
final sep2 = separateurs[_random.nextInt(separateurs.length)];
|
||||
|
||||
// Assembler l'username
|
||||
String username = '$nomPart$sep1$cpPart$sep2$villePart$nombreAleatoire';
|
||||
|
||||
// Si trop court, ajouter des chiffres pour atteindre minimum 10 caractères
|
||||
while (username.length < 10) {
|
||||
// Si trop court, ajouter des chiffres pour atteindre minimum 8 caractères (NIST)
|
||||
while (username.length < 8) {
|
||||
username += _random.nextInt(10).toString();
|
||||
}
|
||||
|
||||
// S'assurer que l'username ne contient que des caractères autorisés (a-z, 0-9, ., -, _)
|
||||
// Normalement déjà le cas avec notre algorithme, mais au cas où
|
||||
username = username.toLowerCase().replaceAll(RegExp(r'[^a-z0-9._-]'), '');
|
||||
// Avec NIST, on n'a plus besoin de nettoyer les caractères spéciaux
|
||||
// Tous les caractères sont acceptés
|
||||
|
||||
return username;
|
||||
}
|
||||
@@ -341,7 +342,7 @@ class _UserFormState extends State<UserForm> {
|
||||
}
|
||||
}
|
||||
|
||||
// Valider le mot de passe selon les règles
|
||||
// Valider le mot de passe selon les normes NIST SP 800-63B
|
||||
String? _validatePassword(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
// Pour un nouveau membre, le mot de passe est obligatoire si le champ est affiché
|
||||
@@ -351,85 +352,101 @@ class _UserFormState extends State<UserForm> {
|
||||
return null; // En modification, vide = garder l'ancien
|
||||
}
|
||||
|
||||
// Faire un trim pour retirer les espaces en début/fin
|
||||
final trimmedValue = value.trim();
|
||||
if (trimmedValue.isEmpty) {
|
||||
return "Le mot de passe ne peut pas être vide";
|
||||
// Vérifier la longueur minimale (NIST: 8 caractères)
|
||||
if (value.length < 8) {
|
||||
return "Le mot de passe doit contenir au moins 8 caractères";
|
||||
}
|
||||
|
||||
// Vérifier qu'il n'y a pas d'espaces dans le mot de passe
|
||||
if (trimmedValue.contains(' ')) {
|
||||
return "Le mot de passe ne doit pas contenir d'espaces";
|
||||
// Vérifier la longueur maximale (NIST: 64 caractères)
|
||||
if (value.length > 64) {
|
||||
return "Le mot de passe ne doit pas dépasser 64 caractères";
|
||||
}
|
||||
|
||||
// Vérifier la longueur
|
||||
if (trimmedValue.length < 12) {
|
||||
return "Le mot de passe doit contenir au moins 12 caractères";
|
||||
}
|
||||
if (trimmedValue.length > 16) {
|
||||
return "Le mot de passe ne doit pas dépasser 16 caractères";
|
||||
}
|
||||
|
||||
// Vérifier qu'il n'est pas égal au username (après trim des deux)
|
||||
if (trimmedValue == _usernameController.text.trim()) {
|
||||
return "Le mot de passe ne doit pas être identique au nom d'utilisateur";
|
||||
}
|
||||
|
||||
// Vérifier la présence d'au moins une minuscule
|
||||
if (!trimmedValue.contains(RegExp(r'[a-z]'))) {
|
||||
return "Le mot de passe doit contenir au moins une lettre minuscule";
|
||||
}
|
||||
|
||||
// Vérifier la présence d'au moins une majuscule
|
||||
if (!trimmedValue.contains(RegExp(r'[A-Z]'))) {
|
||||
return "Le mot de passe doit contenir au moins une lettre majuscule";
|
||||
}
|
||||
|
||||
// Vérifier la présence d'au moins un chiffre
|
||||
if (!trimmedValue.contains(RegExp(r'[0-9]'))) {
|
||||
return "Le mot de passe doit contenir au moins un chiffre";
|
||||
}
|
||||
|
||||
// Vérifier la présence d'au moins un caractère spécial
|
||||
if (!trimmedValue.contains(RegExp(r'[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]'))) {
|
||||
return "Le mot de passe doit contenir au moins un caractère spécial (!@#\$%^&*()_+-=[]{}|;:,.<>?)";
|
||||
}
|
||||
// NIST: Pas de vérification de composition obligatoire
|
||||
// Les espaces sont autorisés, tous les caractères sont acceptés
|
||||
// L'API vérifiera contre Have I Been Pwned
|
||||
// Pas de vérification password == username (demande client)
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Générer un mot de passe aléatoire respectant les règles
|
||||
// Générer un mot de passe selon les normes NIST (phrases de passe recommandées)
|
||||
String _generatePassword() {
|
||||
const String lowercase = 'abcdefghijklmnopqrstuvwxyz';
|
||||
const String uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
const String digits = '0123456789';
|
||||
const String special = '!@#\$%^&*()_+-=[]{}|;:,.<>?';
|
||||
// Listes de mots pour créer des phrases de passe mémorables
|
||||
final sujets = [
|
||||
'Mon chat', 'Le chien', 'Ma voiture', 'Mon vélo', 'La maison',
|
||||
'Mon jardin', 'Le soleil', 'La lune', 'Mon café', 'Le train',
|
||||
'Ma pizza', 'Le gâteau', 'Mon livre', 'La musique', 'Mon film'
|
||||
];
|
||||
|
||||
// Longueur aléatoire entre 12 et 16
|
||||
final length = 12 + _random.nextInt(5);
|
||||
final noms = [
|
||||
'Félix', 'Max', 'Luna', 'Bella', 'Charlie', 'Rocky', 'Maya',
|
||||
'Oscar', 'Ruby', 'Leo', 'Emma', 'Jack', 'Sophie', 'Milo', 'Zoé'
|
||||
];
|
||||
|
||||
// S'assurer d'avoir au moins un caractère de chaque type
|
||||
List<String> password = [];
|
||||
password.add(lowercase[_random.nextInt(lowercase.length)]);
|
||||
password.add(uppercase[_random.nextInt(uppercase.length)]);
|
||||
password.add(digits[_random.nextInt(digits.length)]);
|
||||
password.add(special[_random.nextInt(special.length)]);
|
||||
final verbes = [
|
||||
'aime', 'mange', 'court', 'saute', 'danse', 'chante', 'joue',
|
||||
'dort', 'rêve', 'vole', 'nage', 'lit', 'écrit', 'peint', 'cuisine'
|
||||
];
|
||||
|
||||
// Compléter avec des caractères aléatoires
|
||||
const String allChars = lowercase + uppercase + digits + special;
|
||||
for (int i = password.length; i < length; i++) {
|
||||
password.add(allChars[_random.nextInt(allChars.length)]);
|
||||
final complements = [
|
||||
'dans le jardin', 'sous la pluie', 'avec joie', 'très vite', 'tout le temps',
|
||||
'en été', 'le matin', 'la nuit', 'au soleil', 'dans la neige',
|
||||
'sur la plage', 'à Paris', 'en vacances', 'avec passion', 'doucement'
|
||||
];
|
||||
|
||||
// Choisir un type de phrase aléatoirement
|
||||
final typePhrase = _random.nextInt(3);
|
||||
String phrase;
|
||||
|
||||
switch (typePhrase) {
|
||||
case 0:
|
||||
// Type: Sujet + nom propre + verbe + complément
|
||||
final sujet = sujets[_random.nextInt(sujets.length)];
|
||||
final nom = noms[_random.nextInt(noms.length)];
|
||||
final verbe = verbes[_random.nextInt(verbes.length)];
|
||||
final complement = complements[_random.nextInt(complements.length)];
|
||||
phrase = '$sujet $nom $verbe $complement';
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// Type: Nom propre + a + nombre + ans + point d'exclamation
|
||||
final nom = noms[_random.nextInt(noms.length)];
|
||||
final age = 1 + _random.nextInt(20);
|
||||
phrase = '$nom a $age ans!';
|
||||
break;
|
||||
|
||||
default:
|
||||
// Type: Sujet + verbe + nombre + complément
|
||||
final sujet = sujets[_random.nextInt(sujets.length)];
|
||||
final verbe = verbes[_random.nextInt(verbes.length)];
|
||||
final nombre = 1 + _random.nextInt(100);
|
||||
final complement = complements[_random.nextInt(complements.length)];
|
||||
phrase = '$sujet $verbe $nombre fois $complement';
|
||||
}
|
||||
|
||||
// Mélanger les caractères
|
||||
password.shuffle(_random);
|
||||
// Ajouter éventuellement un caractère spécial à la fin
|
||||
if (_random.nextBool()) {
|
||||
final speciaux = ['!', '?', '.', '...', '♥', '☀', '★', '♪'];
|
||||
phrase += speciaux[_random.nextInt(speciaux.length)];
|
||||
}
|
||||
|
||||
return password.join('');
|
||||
// S'assurer que la phrase fait au moins 8 caractères (elle le sera presque toujours)
|
||||
if (phrase.length < 8) {
|
||||
phrase += ' ${1000 + _random.nextInt(9000)}';
|
||||
}
|
||||
|
||||
// Tronquer si trop long (max 64 caractères selon NIST)
|
||||
if (phrase.length > 64) {
|
||||
phrase = phrase.substring(0, 64);
|
||||
}
|
||||
|
||||
return phrase;
|
||||
}
|
||||
|
||||
// Méthode publique pour récupérer le mot de passe si défini
|
||||
String? getPassword() {
|
||||
final password = _passwordController.text.trim();
|
||||
final password = _passwordController.text; // NIST: ne pas faire de trim, les espaces sont autorisés
|
||||
return password.isNotEmpty ? password : null;
|
||||
}
|
||||
|
||||
@@ -437,7 +454,7 @@ class _UserFormState extends State<UserForm> {
|
||||
UserModel? validateAndGetUser() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
return widget.user?.copyWith(
|
||||
username: _usernameController.text.trim(), // Appliquer trim
|
||||
username: _usernameController.text, // NIST: ne pas faire de trim sur username
|
||||
firstName: _firstNameController.text.trim(),
|
||||
name: _nameController.text.trim(),
|
||||
sectName: _sectNameController.text.trim(),
|
||||
@@ -450,7 +467,7 @@ class _UserFormState extends State<UserForm> {
|
||||
) ??
|
||||
UserModel(
|
||||
id: 0,
|
||||
username: _usernameController.text.trim(), // Appliquer trim
|
||||
username: _usernameController.text, // NIST: ne pas faire de trim sur username
|
||||
firstName: _firstNameController.text.trim(),
|
||||
name: _nameController.text.trim(),
|
||||
sectName: _sectNameController.text.trim(),
|
||||
@@ -729,30 +746,23 @@ class _UserFormState extends State<UserForm> {
|
||||
)
|
||||
: null,
|
||||
helperText: canEditUsername
|
||||
? "Min. 10 caractères (a-z, 0-9, . - _)"
|
||||
? "8 à 64 caractères. Tous les caractères sont acceptés, y compris les espaces et accents."
|
||||
: null,
|
||||
helperMaxLines: 2,
|
||||
validator: canEditUsername
|
||||
? (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Veuillez entrer le nom d'utilisateur";
|
||||
}
|
||||
// Faire un trim pour retirer les espaces en début/fin
|
||||
final trimmedValue = value.trim();
|
||||
if (trimmedValue.isEmpty) {
|
||||
return "Le nom d'utilisateur ne peut pas être vide";
|
||||
// Vérifier la longueur minimale (NIST: 8 caractères)
|
||||
if (value.length < 8) {
|
||||
return "Le nom d'utilisateur doit contenir au moins 8 caractères";
|
||||
}
|
||||
// Vérifier qu'il n'y a pas d'espaces dans l'username
|
||||
if (trimmedValue.contains(' ')) {
|
||||
return "Le nom d'utilisateur ne doit pas contenir d'espaces";
|
||||
}
|
||||
// Vérifier la longueur minimale
|
||||
if (trimmedValue.length < 10) {
|
||||
return "Le nom d'utilisateur doit contenir au moins 10 caractères";
|
||||
}
|
||||
// Vérifier les caractères autorisés (a-z, 0-9, ., -, _)
|
||||
if (!RegExp(r'^[a-z0-9._-]+$').hasMatch(trimmedValue)) {
|
||||
return "Caractères autorisés : lettres minuscules, chiffres, . - _";
|
||||
// Vérifier la longueur maximale (NIST: 64 caractères)
|
||||
if (value.length > 64) {
|
||||
return "Le nom d'utilisateur ne doit pas dépasser 64 caractères";
|
||||
}
|
||||
// Pas de vérification sur le type de caractères (NIST: tous acceptés)
|
||||
return null;
|
||||
}
|
||||
: null,
|
||||
@@ -800,7 +810,8 @@ class _UserFormState extends State<UserForm> {
|
||||
),
|
||||
helperText: widget.user?.id != 0
|
||||
? "Laissez vide pour conserver le mot de passe actuel"
|
||||
: "12-16 car. avec min/maj, chiffres et spéciaux (!@#\$%^&*()_+-=[]{}|;:,.<>?)",
|
||||
: "8 à 64 caractères. Phrases de passe recommandées (ex: Mon chat Félix a 3 ans!)",
|
||||
helperMaxLines: 3,
|
||||
validator: _validatePassword,
|
||||
),
|
||||
),
|
||||
@@ -837,30 +848,23 @@ class _UserFormState extends State<UserForm> {
|
||||
)
|
||||
: null,
|
||||
helperText: canEditUsername
|
||||
? "Min. 10 caractères (a-z, 0-9, . - _)"
|
||||
? "8 à 64 caractères. Tous les caractères sont acceptés, y compris les espaces et accents."
|
||||
: null,
|
||||
helperMaxLines: 2,
|
||||
validator: canEditUsername
|
||||
? (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Veuillez entrer le nom d'utilisateur";
|
||||
}
|
||||
// Faire un trim pour retirer les espaces en début/fin
|
||||
final trimmedValue = value.trim();
|
||||
if (trimmedValue.isEmpty) {
|
||||
return "Le nom d'utilisateur ne peut pas être vide";
|
||||
// Vérifier la longueur minimale (NIST: 8 caractères)
|
||||
if (value.length < 8) {
|
||||
return "Le nom d'utilisateur doit contenir au moins 8 caractères";
|
||||
}
|
||||
// Vérifier qu'il n'y a pas d'espaces dans l'username
|
||||
if (trimmedValue.contains(' ')) {
|
||||
return "Le nom d'utilisateur ne doit pas contenir d'espaces";
|
||||
}
|
||||
// Vérifier la longueur minimale
|
||||
if (trimmedValue.length < 10) {
|
||||
return "Le nom d'utilisateur doit contenir au moins 10 caractères";
|
||||
}
|
||||
// Vérifier les caractères autorisés (a-z, 0-9, ., -, _)
|
||||
if (!RegExp(r'^[a-z0-9._-]+$').hasMatch(trimmedValue)) {
|
||||
return "Caractères autorisés : lettres minuscules, chiffres, . - _";
|
||||
// Vérifier la longueur maximale (NIST: 64 caractères)
|
||||
if (value.length > 64) {
|
||||
return "Le nom d'utilisateur ne doit pas dépasser 64 caractères";
|
||||
}
|
||||
// Pas de vérification sur le type de caractères (NIST: tous acceptés)
|
||||
return null;
|
||||
}
|
||||
: null,
|
||||
@@ -906,7 +910,8 @@ class _UserFormState extends State<UserForm> {
|
||||
),
|
||||
helperText: widget.user?.id != 0
|
||||
? "Laissez vide pour conserver le mot de passe actuel"
|
||||
: "12-16 car. avec min/maj, chiffres et spéciaux (!@#\$%^&*()_+-=[]{}|;:,.<>?)",
|
||||
: "8 à 64 caractères. Phrases de passe recommandées (ex: Mon chat Félix a 3 ans!)",
|
||||
helperMaxLines: 3,
|
||||
validator: _validatePassword,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Reference in New Issue
Block a user