diff --git a/packages/komodo_defi_framework/lib/src/config/kdf_startup_config.dart b/packages/komodo_defi_framework/lib/src/config/kdf_startup_config.dart index 53d43204..62a66292 100644 --- a/packages/komodo_defi_framework/lib/src/config/kdf_startup_config.dart +++ b/packages/komodo_defi_framework/lib/src/config/kdf_startup_config.dart @@ -6,6 +6,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:komodo_coins/komodo_coins.dart'; @@ -90,7 +91,7 @@ class KdfStartupConfig { return KdfStartupConfig._( walletName: walletName, walletPassword: walletPassword, - rpcPassword: rpcPassword ?? generatePassword(), + rpcPassword: rpcPassword ?? SecurityUtils.generatePasswordSecure(32), seed: seed, dbDir: dbPath, userHome: userHomePath, @@ -136,7 +137,7 @@ class KdfStartupConfig { walletName: null, walletPassword: null, seed: null, - rpcPassword: rpcPassword ?? generatePassword(), + rpcPassword: rpcPassword ?? SecurityUtils.generatePasswordSecure(32), userHome: home, dbDir: dbDir, allowWeakPassword: true, @@ -208,20 +209,4 @@ class KdfStartupConfig { } static JsonList? _memoizedCoins; - - static String generatePassword() { - var result = ''; - while (!_validateRPCPassword(result)) { - result = SecurityUtils.generatePasswordSecure(32); - } - return result; - } - - static bool _validateRPCPassword(String src) { - final exp = - RegExp(r'^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[^A-Za-z0-9]).{8,32}$'); - if (!exp.hasMatch(src)) return false; - if (RegExp(r'(.)\1\1').hasMatch(src)) return false; - return true; - } } diff --git a/packages/komodo_defi_types/lib/src/utils/security_utils.dart b/packages/komodo_defi_types/lib/src/utils/security_utils.dart index 31651f25..5f678dd7 100644 --- a/packages/komodo_defi_types/lib/src/utils/security_utils.dart +++ b/packages/komodo_defi_types/lib/src/utils/security_utils.dart @@ -1,70 +1,143 @@ import 'dart:math'; +import 'package:characters/characters.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +/// Enum representing different types of password validation errors +enum PasswordValidationError { + containsPassword, + tooShort, + missingDigit, + missingLowercase, + missingUppercase, + missingSpecialCharacter, + consecutiveCharacters, + none; + + bool get isValid => this == PasswordValidationError.none; +} + // ignore: one_member_abstracts abstract class SecurityUtils { static String generatePasswordSecure( int length, { bool extendedSpecialCharacters = false, - }) => - _generateSecurePassword( + }) { + var result = ''; + while (!SecurityUtils.checkPasswordRequirements(result).isValid) { + result = _generateSecurePassword( length, extendedSpecialCharacters: extendedSpecialCharacters, ); -} + } -// TODO: unit tests - -String _generateSecurePassword( - int length, { - bool extendedSpecialCharacters = false, -}) { - const upperCaseLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - const lowerCaseLetters = 'abcdefghijklmnopqrstuvwxyz'; - const digits = '0123456789'; + return result; + } - // Standard special characters that are generally safe in most contexts, - // including JSON - const specialCharacters = '@'; - // const specialCharacters = r"*.!@#$%^(){}:;',.?/~`_+-=|"; + /// /// Validates password according to KDF password policy + /// + /// Password requirements: + /// - At least 8 characters long + /// - Can't contain the word "password" + /// - At least 1 digit + /// - At least 1 lowercase character + /// - At least 1 uppercase character + /// - At least 1 special character + /// - No same character 3 times in a row + static PasswordValidationError checkPasswordRequirements(String password) { + // Use Unicode-aware character counting + if (password.characters.length < 8) { + return PasswordValidationError.tooShort; + } + + if (password + .toLowerCase() + .contains(RegExp('password', caseSensitive: false, unicode: true))) { + return PasswordValidationError.containsPassword; + } + + // Check for digits (any numerical digit in any script) + if (!RegExp(r'.*\p{N}.*', unicode: true).hasMatch(password)) { + return PasswordValidationError.missingDigit; + } + + // Check for lowercase (any lowercase letter in any script) + if (!RegExp(r'.*\p{Ll}.*', unicode: true).hasMatch(password)) { + return PasswordValidationError.missingLowercase; + } + + // Check for uppercase (any uppercase letter in any script) + if (!RegExp(r'.*\p{Lu}.*', unicode: true).hasMatch(password)) { + return PasswordValidationError.missingUppercase; + } + + // Check for special characters + if (!RegExp(r'.*[^\p{L}\p{N}].*', unicode: true).hasMatch(password)) { + return PasswordValidationError.missingSpecialCharacter; + } + + // Unicode-aware check for consecutive repeated characters using Characters class + final charactersList = password.characters.toList(); + for (var i = 0; i < charactersList.length - 2; i++) { + if (charactersList[i] == charactersList[i + 1] && + charactersList[i] == charactersList[i + 2]) { + return PasswordValidationError.consecutiveCharacters; + } + } + + return PasswordValidationError.none; + } - const extendedSpecial = r'~`$^*+=<>?'; + static String _generateSecurePassword( + int length, { + bool extendedSpecialCharacters = false, + }) { + const upperCaseLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + const lowerCaseLetters = 'abcdefghijklmnopqrstuvwxyz'; + const digits = '0123456789'; + + // Standard special characters that are generally safe in most contexts, + // including JSON + const specialCharacters = '@'; + // const specialCharacters = r"*.!@#$%^(){}:;',.?/~`_+-=|"; + + const extendedSpecial = r'~`$^*+=<>?'; + + final allCharacters = upperCaseLetters + + lowerCaseLetters + + digits + + specialCharacters + + (extendedSpecialCharacters ? extendedSpecial : ''); + + // Ensure the password length is at least 8 characters + if (length < 8) { + throw ArgumentError('Password length must be at least 8 characters.'); + } + + // Random number generator + final random = Random.secure(); + + // Pick one character from each category to ensure password strength + final password = [ + upperCaseLetters[random.nextInt(upperCaseLetters.length)], + lowerCaseLetters[random.nextInt(lowerCaseLetters.length)], + digits[random.nextInt(digits.length)], + specialCharacters[random.nextInt(specialCharacters.length)], + if (extendedSpecialCharacters) + extendedSpecial[random.nextInt(extendedSpecial.length)], + ]; - final allCharacters = upperCaseLetters + - lowerCaseLetters + - digits + - specialCharacters + - (extendedSpecialCharacters ? extendedSpecial : ''); + // Fill the rest of the password length with random characters from the pool + for (var i = password.length; i < length; i++) { + password.add(allCharacters[random.nextInt(allCharacters.length)]); + } - // Ensure the password length is at least 8 characters - if (length < 8) { - throw ArgumentError('Password length must be at least 8 characters.'); - } + // Shuffle the password to ensure randomness + password.shuffle(random); - // Random number generator - final random = Random.secure(); - - // Pick one character from each category to ensure password strength - final password = [ - upperCaseLetters[random.nextInt(upperCaseLetters.length)], - lowerCaseLetters[random.nextInt(lowerCaseLetters.length)], - digits[random.nextInt(digits.length)], - specialCharacters[random.nextInt(specialCharacters.length)], - if (extendedSpecialCharacters) - extendedSpecial[random.nextInt(extendedSpecial.length)], - ]; - - // Fill the rest of the password length with random characters from the pool - for (var i = 4; i < length; i++) { - password.add(allCharacters[random.nextInt(allCharacters.length)]); + // Join the list into a string and return it + return password.join(); } - - // Shuffle the password to ensure randomness - password.shuffle(random); - - // Join the list into a string and return it - return password.join(); } extension CensoredJsonMap on JsonMap { diff --git a/packages/komodo_defi_types/test/utils/security_utils_test.dart b/packages/komodo_defi_types/test/utils/security_utils_test.dart new file mode 100644 index 00000000..68df925f --- /dev/null +++ b/packages/komodo_defi_types/test/utils/security_utils_test.dart @@ -0,0 +1,667 @@ +import 'dart:math'; + +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:test/test.dart'; + +void main() { + group('Password validation tests', () { + test('Too short passwords should fail', () { + expect( + SecurityUtils.checkPasswordRequirements('Abc1!'), + PasswordValidationError.tooShort, + ); + expect( + SecurityUtils.checkPasswordRequirements(''), + PasswordValidationError.tooShort, + ); + expect( + SecurityUtils.checkPasswordRequirements('A1b!'), + PasswordValidationError.tooShort, + ); + }); + + test('Passwords containing "password" should fail', () { + expect( + SecurityUtils.checkPasswordRequirements('myPassword123!'), + PasswordValidationError.containsPassword, + ); + expect( + SecurityUtils.checkPasswordRequirements('PASSWORDabc123!'), + PasswordValidationError.containsPassword, + ); + expect( + SecurityUtils.checkPasswordRequirements('pAsSwOrD123!'), + PasswordValidationError.containsPassword, + ); + expect( + SecurityUtils.checkPasswordRequirements('My-password-is-secure!123'), + PasswordValidationError.containsPassword, + ); + }); + + test('Passwords without digits should fail', () { + expect( + SecurityUtils.checkPasswordRequirements('StrongPass!'), + PasswordValidationError.missingDigit, + ); + expect( + SecurityUtils.checkPasswordRequirements('NoDigitsHere!@#'), + PasswordValidationError.missingDigit, + ); + }); + + test('Passwords without lowercase should fail', () { + expect( + SecurityUtils.checkPasswordRequirements('STRONG123!'), + PasswordValidationError.missingLowercase, + ); + expect( + SecurityUtils.checkPasswordRequirements('ALL123CAPS!@#'), + PasswordValidationError.missingLowercase, + ); + }); + + test('Passwords without uppercase should fail', () { + expect( + SecurityUtils.checkPasswordRequirements('strong123!'), + PasswordValidationError.missingUppercase, + ); + expect( + SecurityUtils.checkPasswordRequirements('all123lower!@#'), + PasswordValidationError.missingUppercase, + ); + }); + + test('Passwords without special characters should fail', () { + expect( + SecurityUtils.checkPasswordRequirements('Strong123'), + PasswordValidationError.missingSpecialCharacter, + ); + expect( + SecurityUtils.checkPasswordRequirements('NoSpecial1Characters2'), + PasswordValidationError.missingSpecialCharacter, + ); + }); + + test('Multiple validation errors should return most critical first', () { + expect( + SecurityUtils.checkPasswordRequirements('pass'), + PasswordValidationError.tooShort, + ); + expect( + SecurityUtils.checkPasswordRequirements('passwordddd'), + PasswordValidationError.containsPassword, + ); + expect( + SecurityUtils.checkPasswordRequirements('Abcaaa1234*%'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + SecurityUtils.checkPasswordRequirements('Abcde123'), + PasswordValidationError.missingSpecialCharacter, + ); + }); + + test('Edge cases with spaces and special formatting', () { + expect( + SecurityUtils.checkPasswordRequirements('Pass 123!'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('Tab\t123!A'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('Line\nBreak123!A'), + PasswordValidationError.none, + ); + }); + + test('Passwords with numbers in various positions', () { + expect( + SecurityUtils.checkPasswordRequirements('1AbcSpecial!'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('Abc1Special!'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('AbcSpecial!1'), + PasswordValidationError.none, + ); + }); + + test('Various special characters', () { + expect( + SecurityUtils.checkPasswordRequirements('AbcDef123@'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('Abc_Def123#'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements(r'AbcDef123$'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('AbcDef123%'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('AbcDef123&'), + PasswordValidationError.none, + ); + }); + + test('Valid passwords should not fail', () { + expect( + SecurityUtils.checkPasswordRequirements('Very!hard!pass!77'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('Komodo2024!'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('Complex!P4ssword123'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements(r'!P4ssword#$@'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('Mix3d_Ch4r4ct3rs!'), + PasswordValidationError.none, + ); + }); + + test('Password specifically mentioned in the issue should be rejected', () { + // Should fail (has consecutive characters) + expect( + SecurityUtils.checkPasswordRequirements('Very!hard!pass!777'), + PasswordValidationError.consecutiveCharacters, + ); + }); + + test( + 'Passwords with three or more consecutive identical ' + 'characters should fail', () { + expect( + SecurityUtils.checkPasswordRequirements('Strong111Security!'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + SecurityUtils.checkPasswordRequirements('Secure222!A'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + SecurityUtils.checkPasswordRequirements('A1!Secure333'), + PasswordValidationError.consecutiveCharacters, + ); + + expect( + SecurityUtils.checkPasswordRequirements('aaaStrong1!'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + SecurityUtils.checkPasswordRequirements('Strong1!bbb'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + SecurityUtils.checkPasswordRequirements('Strong1!CCC'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + SecurityUtils.checkPasswordRequirements('Strong1!!!Secure'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + SecurityUtils.checkPasswordRequirements('Strong1###Secure'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + SecurityUtils.checkPasswordRequirements(r'Strong1$$$Secure'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + SecurityUtils.checkPasswordRequirements('Strong1!aaaaa'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + SecurityUtils.checkPasswordRequirements('Strong1!44444'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + SecurityUtils.checkPasswordRequirements('Strong1!!!!!'), + PasswordValidationError.consecutiveCharacters, + ); + }); + + test( + 'Valid passwords with two consecutive identical characters should pass', + () { + expect( + SecurityUtils.checkPasswordRequirements('Strong11Secured!'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('Strong!!Secured1'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('aaStrong1!Secured'), + PasswordValidationError.none, + ); + }); + + test('Special case - passwords with unicode characters', () { + expect( + SecurityUtils.checkPasswordRequirements('Пароль123!'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('密码Abc123!'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('Mötley123!'), + PasswordValidationError.none, + ); + }); + + test('Extended Unicode character password tests', () { + expect( + SecurityUtils.checkPasswordRequirements('علي123!Abc'), // Arabic + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('こんにちは123!Ab'), // Japanese + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('안녕하세요123!Ab'), // Korean + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('Привет123!Ab'), // Cyrillic + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('Γειά123!Aa'), // Greek + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('שלום123!Aa'), // Hebrew + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('नमस्ते123!Ab'), // Devanagari + PasswordValidationError.none, + ); + }); + + test('Unicode edge cases and challenging patterns', () { + expect( + SecurityUtils.checkPasswordRequirements( + 'Раssw0rd!', + ), // Cyrillic 'Р' (not Latin 'P') + PasswordValidationError.none, + ); + + expect( + SecurityUtils.checkPasswordRequirements('Pass\u200Bword123!'), + PasswordValidationError.none, + ); + + expect( + // a + combining acute accent + SecurityUtils.checkPasswordRequirements('Pa\u0301ssword123!'), + PasswordValidationError.none, + ); + + expect( + SecurityUtils.checkPasswordRequirements('Strong🔑123!A'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('A1!🎮🎲🎯aa'), + PasswordValidationError.none, + ); + + expect( + SecurityUtils.checkPasswordRequirements('Strоng123!'), + PasswordValidationError.none, + ); + }); + + test('Unicode sequential characters detection', () { + expect( + SecurityUtils.checkPasswordRequirements('Strong爱爱爱123!'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + SecurityUtils.checkPasswordRequirements('Strong😊😊😊123!'), + PasswordValidationError.consecutiveCharacters, + ); + + // Characters that look similar but are actually different code points + expect( + SecurityUtils.checkPasswordRequirements('StrongАААbc123!'), + PasswordValidationError.consecutiveCharacters, + ); + }); + + test('Bidirectional text and special Unicode formatting', () { + // Right-to-left marks and embedding + expect( + SecurityUtils.checkPasswordRequirements( + 'Pass\u200Eword123!A', + ), // Contains LTR mark + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements( + 'Pass\u200Fword123!A', + ), // Contains RTL mark + PasswordValidationError.none, + ); + + // Mixed directionality + expect( + SecurityUtils.checkPasswordRequirements( + 'Abcהמסיסמ123!', + ), // Hebrew mixed with Latin + PasswordValidationError.none, + ); + + // Special spaces + expect( + SecurityUtils.checkPasswordRequirements( + 'Pass\u2007word123!A', + ), // Figure space + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements( + 'Pass\u00A0word123!A', + ), // Non-breaking space + PasswordValidationError.none, + ); + }); + + test('Advanced emoji password tests in valid passwords', () { + expect( + SecurityUtils.checkPasswordRequirements('Strong123!🔒'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('🔑Abcasba123!'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('Pass🔥123!A'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('Abc123!🌟✨🚀'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('🎮🎯A1!abaa'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('Strong👨‍👩‍👧‍👦123!'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('A1!👍🏽Strong1234'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('Pass🇺🇸123!A'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('Strong123A🎯'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('Strong1A🎯🎯🎯'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + SecurityUtils.checkPasswordRequirements('🔥🔥🔥Strong1A!'), + PasswordValidationError.consecutiveCharacters, + ); + }); + test('Complex emoji sequences and ZWJ', () { + expect( + // ZWJ sequence (man technologist) + SecurityUtils.checkPasswordRequirements('Strong123A👨‍💻'), + PasswordValidationError.none, + ); + expect( + // Complex ZWJ sequence + SecurityUtils.checkPasswordRequirements('Strong123A👁️‍🗨️'), + PasswordValidationError.none, + ); + expect( + // Emoji presentation selector + SecurityUtils.checkPasswordRequirements('Strong123A☺️'), + PasswordValidationError.none, + ); + }); + + test('Mixed emoji and text patterns', () { + expect( + SecurityUtils.checkPasswordRequirements('Aaba🔒1🔑!🚀'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('Se🔒cure123!'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('St🔑r🔒ng1!'), + PasswordValidationError.none, + ); + expect( + // Should not trigger containsPassword + SecurityUtils.checkPasswordRequirements('p🔑ssw🔒rd123A!'), + PasswordValidationError.none, + ); + expect( + SecurityUtils.checkPasswordRequirements('🔒🚀🎮��Aa1!'), + PasswordValidationError.none, + ); + }); + + test('Limited fuzzy testing', () { + final random = Random(); + const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123' + r'456789!@#$%^&*()'; + + for (int i = 0; i < 10; i++) { + final int length = random.nextInt(15) + 1; + final StringBuffer passwordBuffer = StringBuffer(); + + for (int j = 0; j < length; j++) { + passwordBuffer.write(chars[random.nextInt(chars.length)]); + } + + // Test the random password - we don't assert specific errors, + // just verify the validator properly handles random input + SecurityUtils.checkPasswordRequirements(passwordBuffer.toString()); + } + + final List problematicInputs = [ + // Password too short + 'a', + // Repeated characters + 'aaaPassword1!', + 'Password111!', + 'Password!!!1', + // Mixed borderline cases + 'pass A1!', + 'PASS a1!', + 'Pass A!', + 'Pass A1', + // Contains "password" + 'MyPasswordIs1!', + 'password123A!', + '!PASSWORDabc1', + ]; + + for (final String input in problematicInputs) { + SecurityUtils.checkPasswordRequirements(input); + } + }); + }); + + group('Password generation tests', () { + test('Should throw error when length is less than 8', () { + expect( + () => SecurityUtils.generatePasswordSecure(7), + throwsA(isA()), + ); + expect( + () => SecurityUtils.generatePasswordSecure(0), + throwsA(isA()), + ); + expect( + () => SecurityUtils.generatePasswordSecure(-1), + throwsA(isA()), + ); + }); + + test('Should generate password of specified length', () { + final password8 = SecurityUtils.generatePasswordSecure(8); + final password16 = SecurityUtils.generatePasswordSecure(16); + final password32 = SecurityUtils.generatePasswordSecure(32); + + expect(password8.length, equals(8)); + expect(password16.length, equals(16)); + expect(password32.length, equals(32)); + }); + + test('Should include all required character types', () { + final password = SecurityUtils.generatePasswordSecure(12); + + // Check for at least one uppercase letter + expect(RegExp(r'[A-Z]').hasMatch(password), isTrue); + + // Check for at least one lowercase letter + expect(RegExp(r'[a-z]').hasMatch(password), isTrue); + + // Check for at least one digit + expect(RegExp(r'[0-9]').hasMatch(password), isTrue); + + // Check for at least one special character + expect(RegExp(r'[@]').hasMatch(password), isTrue); + }); + + test('Should include extended special characters when flag is set', () { + final password = SecurityUtils.generatePasswordSecure( + 20, + extendedSpecialCharacters: true, + ); + + expect(RegExp(r'[~`$^*+=<>?]').hasMatch(password), isTrue); + expect(password.length, equals(20)); + }); + + test('Generated passwords should be different (randomness check)', () { + final passwords = {}; + + // Generate multiple passwords and check they're unique + for (var i = 0; i < 10; i++) { + passwords.add(SecurityUtils.generatePasswordSecure(12)); + } + + // If passwords are truly random, they should all be different + expect(passwords.length, equals(10)); + }); + + test('Generated passwords should pass checkPasswordRequirements validation', + () { + for (var i = 0; i < 10; i++) { + final password = SecurityUtils.generatePasswordSecure(12); + final validationResult = + SecurityUtils.checkPasswordRequirements(password); + expect(validationResult, equals(PasswordValidationError.none)); + } + + // Test with extended characters too + for (var i = 0; i < 10; i++) { + final password = SecurityUtils.generatePasswordSecure( + 12, + extendedSpecialCharacters: true, + ); + final validationResult = + SecurityUtils.checkPasswordRequirements(password); + expect(validationResult, equals(PasswordValidationError.none)); + } + }); + + test('Should not generate passwords with consecutive identical characters', + () { + // Generate multiple passwords and confirm none have 3+ consecutive identical characters + for (var i = 0; i < 20; i++) { + final password = SecurityUtils.generatePasswordSecure(32); + + // Check for consecutive identical characters with regular expressions + final hasConsecutiveChars = RegExp(r'(.)\1{2,}').hasMatch(password); + + expect(hasConsecutiveChars, isFalse); + } + }); + + test('Should generate valid passwords at minimum length (8)', () { + final password = SecurityUtils.generatePasswordSecure(8); + expect(password.length, equals(8)); + expect( + SecurityUtils.checkPasswordRequirements(password), + equals(PasswordValidationError.none), + ); + }); + + test('Should generate valid passwords at large lengths', () { + final password = SecurityUtils.generatePasswordSecure(100); + expect(password.length, equals(100)); + expect( + SecurityUtils.checkPasswordRequirements(password), + equals(PasswordValidationError.none), + ); + }); + + test( + 'Should generate exact length password with extended special characters', + () { + final password12 = SecurityUtils.generatePasswordSecure( + 12, + extendedSpecialCharacters: true, + ); + final password16 = SecurityUtils.generatePasswordSecure( + 16, + extendedSpecialCharacters: true, + ); + final password24 = SecurityUtils.generatePasswordSecure( + 24, + extendedSpecialCharacters: true, + ); + + expect(password12.length, equals(12)); + expect(password16.length, equals(16)); + expect(password24.length, equals(24)); + + expect( + SecurityUtils.checkPasswordRequirements(password12), + equals(PasswordValidationError.none), + ); + }); + }); +}