diff --git a/assets/translations/en.json b/assets/translations/en.json index 23fb34409a..97b126eecd 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -126,7 +126,7 @@ "walletCreationEmptySeedError": "Seed should not be empty", "walletCreationExistNameError": "Wallet name exists", "walletCreationNameLengthError": "Name length should be between 1 and 40", - "walletCreationFormatPasswordError": "Password must contain at least 12 characters, with at least one lower-case, one upper-case and one special symbol.", + "walletCreationFormatPasswordError": "Password must contain at least 8 characters, with at least one digit, one lower-case, one upper-case and one special symbol. The password can't contain the same character 3 times in a row. The password can't contain the word 'password'", "walletCreationConfirmPasswordError": "Your passwords do not match. Please try again.", "incorrectPassword": "Incorrect password", "importSeedEnterSeedPhraseHint": "Enter seed", @@ -366,6 +366,16 @@ "currentPassword": "Current password", "walletNotFound": "Wallet not found!", "passwordIsEmpty": "Password is empty", + "passwordContainsTheWordPassword": "Password cannot contain the word 'password'", + "passwordTooShort": "Password must be at least 8 characters long", + "passwordMissingDigit": "Password must contain at least 1 digit", + "passwordMissingLowercase": "Password must contain at least 1 lowercase character", + "passwordMissingUppercase": "Password must contain at least 1 uppercase character", + "passwordMissingSpecialCharacter": "Password must contain at least 1 special character", + "passwordConsecutiveCharacters": "Password cannot contain the same character 3 times in a row", + "passwordSecurity": "Password Security", + "allowWeakPassword": "Allow weak password", + "allowWeakPasswordDescription": "For support and debugging purposes only. Bypasses password security requirements.", "dexBalanceNotSufficientError": "{} balance is not sufficient. {} {} required", "dexEnterPriceError": "Please enter price", "dexZeroPriceError": "Price must be greater than 0", diff --git a/lib/bloc/auth_bloc/auth_bloc.dart b/lib/bloc/auth_bloc/auth_bloc.dart index cc94cc9d36..c7fa2a0c6d 100644 --- a/lib/bloc/auth_bloc/auth_bloc.dart +++ b/lib/bloc/auth_bloc/auth_bloc.dart @@ -6,6 +6,7 @@ import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:logging/logging.dart'; import 'package:web_dex/app_config/app_config.dart'; +import 'package:web_dex/bloc/settings/settings_repository.dart'; import 'package:web_dex/blocs/wallets_repository.dart'; import 'package:web_dex/model/authorize_mode.dart'; import 'package:web_dex/model/kdf_auth_metadata_extension.dart'; @@ -19,7 +20,7 @@ part 'auth_bloc_state.dart'; class AuthBloc extends Bloc { /// Handles [AuthBlocEvent]s and emits [AuthBlocState]s. /// [_kdfSdk] is an instance of [KomodoDefiSdk] used for authentication. - AuthBloc(this._kdfSdk, this._walletsRepository) + AuthBloc(this._kdfSdk, this._walletsRepository, this._settingsRepository) : super(AuthBlocState.initial()) { on(_onAuthChanged); on(_onClearState); @@ -33,6 +34,7 @@ class AuthBloc extends Bloc { final KomodoDefiSdk _kdfSdk; final WalletsRepository _walletsRepository; + final SettingsRepository _settingsRepository; StreamSubscription? _authChangesSubscription; final _log = Logger('AuthBloc'); @@ -42,6 +44,11 @@ class AuthBloc extends Bloc { await super.close(); } + Future _areWeakPasswordsAllowed() async { + final settings = await _settingsRepository.loadSettings(); + return settings.weakPasswordsAllowed; + } + Future _onLogout( AuthSignOutRequested event, Emitter emit, @@ -68,8 +75,11 @@ class AuthBloc extends Bloc { ); } - _log.info('login from a wallet'); + _log.info('login from a wallet'); emit(AuthBlocState.loading()); + + final weakPasswordsAllowed = await _areWeakPasswordsAllowed(); + await _kdfSdk.auth.signIn( walletName: event.wallet.name, password: event.password, @@ -77,6 +87,7 @@ class AuthBloc extends Bloc { derivationMethod: event.wallet.config.type == WalletType.hdwallet ? DerivationMethod.hdWallet : DerivationMethod.iguana, + allowWeakPassword: weakPasswordsAllowed, ), ); final KdfUser? currentUser = await _kdfSdk.auth.currentUser; @@ -84,7 +95,7 @@ class AuthBloc extends Bloc { return emit(AuthBlocState.error(AuthException.notSignedIn())); } - _log.info('logged in from a wallet'); + _log.info('logged in from a wallet'); emit(AuthBlocState.loggedIn(currentUser)); _listenToAuthStateChanges(); } catch (e, s) { @@ -135,7 +146,10 @@ class AuthBloc extends Bloc { return; } - _log.info('register from a wallet'); + _log.info('register from a wallet'); + + final weakPasswordsAllowed = await _areWeakPasswordsAllowed(); + await _kdfSdk.auth.register( password: event.password, walletName: event.wallet.name, @@ -143,10 +157,11 @@ class AuthBloc extends Bloc { derivationMethod: event.wallet.config.type == WalletType.hdwallet ? DerivationMethod.hdWallet : DerivationMethod.iguana, + allowWeakPassword: weakPasswordsAllowed, ), ); - _log.info('registered from a wallet'); + _log.info('registered from a wallet'); await _kdfSdk.setWalletType(event.wallet.config.type); await _kdfSdk.confirmSeedBackup(hasBackup: false); await _kdfSdk.addActivatedCoins(enabledByDefaultCoins); @@ -179,7 +194,10 @@ class AuthBloc extends Bloc { return; } - _log.info('restore from a wallet'); + _log.info('restore from a wallet'); + + final weakPasswordsAllowed = await _areWeakPasswordsAllowed(); + await _kdfSdk.auth.register( password: event.password, walletName: event.wallet.name, @@ -188,10 +206,11 @@ class AuthBloc extends Bloc { derivationMethod: event.wallet.config.type == WalletType.hdwallet ? DerivationMethod.hdWallet : DerivationMethod.iguana, + allowWeakPassword: weakPasswordsAllowed, ), ); - _log.info('restored from a wallet'); + _log.info('restored from a wallet'); await _kdfSdk.setWalletType(event.wallet.config.type); await _kdfSdk.confirmSeedBackup(hasBackup: event.wallet.config.hasBackup); await _kdfSdk.addActivatedCoins(enabledByDefaultCoins); diff --git a/lib/bloc/settings/settings_bloc.dart b/lib/bloc/settings/settings_bloc.dart index 48f859f927..f3844ff35a 100644 --- a/lib/bloc/settings/settings_bloc.dart +++ b/lib/bloc/settings/settings_bloc.dart @@ -18,6 +18,7 @@ class SettingsBloc extends Bloc { on(_onThemeModeChanged); on(_onMarketMakerBotSettingsChanged); on(_onTestCoinsEnabledChanged); + on(_onWeakPasswordsAllowedChanged); } late StoredSettings _storedSettings; @@ -56,4 +57,14 @@ class SettingsBloc extends Bloc { ); emitter(state.copyWith(testCoinsEnabled: event.testCoinsEnabled)); } + + Future _onWeakPasswordsAllowedChanged( + WeakPasswordsAllowedChanged event, + Emitter emitter, + ) async { + await _settingsRepo.updateSettings( + _storedSettings.copyWith(weakPasswordsAllowed: event.weakPasswordsAllowed), + ); + emitter(state.copyWith(weakPasswordsAllowed: event.weakPasswordsAllowed)); + } } diff --git a/lib/bloc/settings/settings_event.dart b/lib/bloc/settings/settings_event.dart index d307388e02..8299043ea1 100644 --- a/lib/bloc/settings/settings_event.dart +++ b/lib/bloc/settings/settings_event.dart @@ -27,3 +27,11 @@ class MarketMakerBotSettingsChanged extends SettingsEvent { @override List get props => [settings]; } + +class WeakPasswordsAllowedChanged extends SettingsEvent { + const WeakPasswordsAllowedChanged({required this.weakPasswordsAllowed}); + final bool weakPasswordsAllowed; + + @override + List get props => [weakPasswordsAllowed]; +} diff --git a/lib/bloc/settings/settings_state.dart b/lib/bloc/settings/settings_state.dart index e793b44808..8f9afc48ce 100644 --- a/lib/bloc/settings/settings_state.dart +++ b/lib/bloc/settings/settings_state.dart @@ -8,6 +8,7 @@ class SettingsState extends Equatable { required this.themeMode, required this.mmBotSettings, required this.testCoinsEnabled, + required this.weakPasswordsAllowed, }); factory SettingsState.fromStored(StoredSettings stored) { @@ -15,29 +16,34 @@ class SettingsState extends Equatable { themeMode: stored.mode, mmBotSettings: stored.marketMakerBotSettings, testCoinsEnabled: stored.testCoinsEnabled, + weakPasswordsAllowed: stored.weakPasswordsAllowed, ); } final ThemeMode themeMode; final MarketMakerBotSettings mmBotSettings; final bool testCoinsEnabled; + final bool weakPasswordsAllowed; @override List get props => [ themeMode, mmBotSettings, testCoinsEnabled, + weakPasswordsAllowed, ]; SettingsState copyWith({ ThemeMode? mode, MarketMakerBotSettings? marketMakerBotSettings, bool? testCoinsEnabled, + bool? weakPasswordsAllowed, }) { return SettingsState( themeMode: mode ?? themeMode, mmBotSettings: marketMakerBotSettings ?? mmBotSettings, testCoinsEnabled: testCoinsEnabled ?? this.testCoinsEnabled, + weakPasswordsAllowed: weakPasswordsAllowed ?? this.weakPasswordsAllowed, ); } } diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 71f8bd61d3..5243588ef2 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -363,6 +363,16 @@ abstract class LocaleKeys { static const currentPassword = 'currentPassword'; static const walletNotFound = 'walletNotFound'; static const passwordIsEmpty = 'passwordIsEmpty'; + static const passwordContainsTheWordPassword = 'passwordContainsTheWordPassword'; + static const passwordTooShort = 'passwordTooShort'; + static const passwordMissingDigit = 'passwordMissingDigit'; + static const passwordMissingLowercase = 'passwordMissingLowercase'; + static const passwordMissingUppercase = 'passwordMissingUppercase'; + static const passwordMissingSpecialCharacter = 'passwordMissingSpecialCharacter'; + static const passwordConsecutiveCharacters = 'passwordConsecutiveCharacters'; + static const passwordSecurity = 'passwordSecurity'; + static const allowWeakPassword = 'allowWeakPassword'; + static const allowWeakPasswordDescription = 'allowWeakPasswordDescription'; static const dexBalanceNotSufficientError = 'dexBalanceNotSufficientError'; static const dexEnterPriceError = 'dexEnterPriceError'; static const dexZeroPriceError = 'dexZeroPriceError'; diff --git a/lib/main.dart b/lib/main.dart index 179a5358a0..0ed0bfa910 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -153,7 +153,8 @@ class MyApp extends StatelessWidget { return MultiBlocProvider( providers: [ BlocProvider( - create: (_) => AuthBloc(komodoDefiSdk, walletsRepository), + create: (_) => + AuthBloc(komodoDefiSdk, walletsRepository, SettingsRepository()), ), ], child: BetterFeedback( diff --git a/lib/model/stored_settings.dart b/lib/model/stored_settings.dart index 2d66e9ea28..15055d23f6 100644 --- a/lib/model/stored_settings.dart +++ b/lib/model/stored_settings.dart @@ -9,12 +9,14 @@ class StoredSettings { required this.analytics, required this.marketMakerBotSettings, required this.testCoinsEnabled, + required this.weakPasswordsAllowed, }); final ThemeMode mode; final AnalyticsSettings analytics; final MarketMakerBotSettings marketMakerBotSettings; final bool testCoinsEnabled; + final bool weakPasswordsAllowed; static StoredSettings initial() { return StoredSettings( @@ -22,6 +24,7 @@ class StoredSettings { analytics: AnalyticsSettings.initial(), marketMakerBotSettings: MarketMakerBotSettings.initial(), testCoinsEnabled: true, + weakPasswordsAllowed: false, ); } @@ -35,6 +38,7 @@ class StoredSettings { json[storedMarketMakerSettingsKey], ), testCoinsEnabled: json['testCoinsEnabled'] ?? true, + weakPasswordsAllowed: json['weakPasswordsAllowed'] ?? false, ); } @@ -44,6 +48,7 @@ class StoredSettings { storedAnalyticsSettingsKey: analytics.toJson(), storedMarketMakerSettingsKey: marketMakerBotSettings.toJson(), 'testCoinsEnabled': testCoinsEnabled, + 'weakPasswordsAllowed': weakPasswordsAllowed, }; } @@ -52,6 +57,7 @@ class StoredSettings { AnalyticsSettings? analytics, MarketMakerBotSettings? marketMakerBotSettings, bool? testCoinsEnabled, + bool? weakPasswordsAllowed, }) { return StoredSettings( mode: mode ?? this.mode, @@ -59,6 +65,7 @@ class StoredSettings { marketMakerBotSettings: marketMakerBotSettings ?? this.marketMakerBotSettings, testCoinsEnabled: testCoinsEnabled ?? this.testCoinsEnabled, + weakPasswordsAllowed: weakPasswordsAllowed ?? this.weakPasswordsAllowed, ); } } diff --git a/lib/shared/utils/validators.dart b/lib/shared/utils/validators.dart index 3e7dbfbbad..29357ffd98 100644 --- a/lib/shared/utils/validators.dart +++ b/lib/shared/utils/validators.dart @@ -1,15 +1,113 @@ +import 'package:characters/characters.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; +/// Enum representing different types of password validation errors +enum PasswordValidationError { + containsPassword, + tooShort, + missingDigit, + missingLowercase, + missingUppercase, + missingSpecialCharacter, + consecutiveCharacters, + none +} + +/// Converts a password validation error to a localized error message +String? passwordErrorMessage(PasswordValidationError error) { + switch (error) { + case PasswordValidationError.containsPassword: + return LocaleKeys.passwordContainsTheWordPassword.tr(); + case PasswordValidationError.tooShort: + return LocaleKeys.passwordTooShort.tr(); + case PasswordValidationError.missingDigit: + return LocaleKeys.passwordMissingDigit.tr(); + case PasswordValidationError.missingLowercase: + return LocaleKeys.passwordMissingLowercase.tr(); + case PasswordValidationError.missingUppercase: + return LocaleKeys.passwordMissingUppercase.tr(); + case PasswordValidationError.missingSpecialCharacter: + return LocaleKeys.passwordMissingSpecialCharacter.tr(); + case PasswordValidationError.consecutiveCharacters: + return LocaleKeys.passwordConsecutiveCharacters.tr(); + case PasswordValidationError.none: + return null; + } +} + String? validateConfirmPassword(String password, String confirmPassword) { return password != confirmPassword ? LocaleKeys.walletCreationConfirmPasswordError.tr() : null; } -/// unit test: [testValidatePassword] -String? validatePassword(String password, String errorText) { +String? validatePasswordLegacy(String password, String errorText) { final RegExp exp = RegExp(r'^(?:(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9\s])).{12,}$'); return password.isEmpty || !password.contains(exp) ? errorText : null; } + +/// 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 +String? validatePassword(String password) { + return passwordErrorMessage(checkPasswordRequirements(password)); +} + +/// Internal validation method that returns the enum error type +PasswordValidationError checkPasswordRequirements(String password) { + // As suggested by CodeRabbitAI: + // password.length counts UTF-16 code units, so a single emoji or accented + // glyph can be reported as 2 – 4 characters, letting users create visually + // short (and possibly weak) passwords that still pass the length check. + // Switch to password.characters.length, already available via the characters + // package you import. + 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 (int i = 0; i < charactersList.length - 2; i++) { + if (charactersList[i] == charactersList[i + 1] && + charactersList[i] == charactersList[i + 2]) { + return PasswordValidationError.consecutiveCharacters; + } + } + + return PasswordValidationError.none; +} diff --git a/lib/views/settings/widgets/general_settings/general_settings.dart b/lib/views/settings/widgets/general_settings/general_settings.dart index b9f9e2880a..8ce028c380 100644 --- a/lib/views/settings/widgets/general_settings/general_settings.dart +++ b/lib/views/settings/widgets/general_settings/general_settings.dart @@ -8,6 +8,7 @@ import 'package:web_dex/views/settings/widgets/general_settings/settings_downloa import 'package:web_dex/views/settings/widgets/general_settings/settings_manage_analytics.dart'; import 'package:web_dex/views/settings/widgets/general_settings/settings_manage_test_coins.dart'; import 'package:web_dex/views/settings/widgets/general_settings/settings_manage_trading_bot.dart'; +import 'package:web_dex/views/settings/widgets/general_settings/settings_manage_weak_passwords.dart'; import 'package:web_dex/views/settings/widgets/general_settings/settings_reset_activated_coins.dart'; import 'package:web_dex/views/settings/widgets/general_settings/settings_theme_switcher.dart'; import 'package:web_dex/views/settings/widgets/general_settings/show_swap_data.dart'; @@ -28,6 +29,8 @@ class GeneralSettings extends StatelessWidget { const SizedBox(height: 25), const SettingsManageTestCoins(), const SizedBox(height: 25), + const SettingsManageWeakPasswords(), + const SizedBox(height: 25), if (!kIsWalletOnly) const HiddenWithoutWallet( child: SettingsManageTradingBot(), diff --git a/lib/views/settings/widgets/general_settings/settings_manage_weak_passwords.dart b/lib/views/settings/widgets/general_settings/settings_manage_weak_passwords.dart new file mode 100644 index 0000000000..ebf7bec83a --- /dev/null +++ b/lib/views/settings/widgets/general_settings/settings_manage_weak_passwords.dart @@ -0,0 +1,48 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/bloc/settings/settings_bloc.dart'; +import 'package:web_dex/bloc/settings/settings_event.dart'; +import 'package:web_dex/bloc/settings/settings_state.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/views/settings/widgets/common/settings_section.dart'; + +class SettingsManageWeakPasswords extends StatelessWidget { + const SettingsManageWeakPasswords({super.key}); + + @override + Widget build(BuildContext context) { + return SettingsSection( + title: LocaleKeys.passwordSecurity.tr(), + child: const AllowWeakPasswordsSwitcher(), + ); + } +} + +class AllowWeakPasswordsSwitcher extends StatelessWidget { + const AllowWeakPasswordsSwitcher({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) => Row( + children: [ + UiSwitcher( + key: const Key('allow-weak-passwords-switcher'), + value: state.weakPasswordsAllowed, + onChanged: (value) => _onSwitcherChanged(context, value), + ), + const SizedBox(width: 15), + Text(LocaleKeys.allowWeakPassword.tr()), + ], + ), + ); + } + + void _onSwitcherChanged(BuildContext context, bool value) { + context + .read() + .add(WeakPasswordsAllowedChanged(weakPasswordsAllowed: value)); + } +} diff --git a/lib/views/settings/widgets/security_settings/password_update_page.dart b/lib/views/settings/widgets/security_settings/password_update_page.dart index 4fe34ec2fe..1a9d4439d6 100644 --- a/lib/views/settings/widgets/security_settings/password_update_page.dart +++ b/lib/views/settings/widgets/security_settings/password_update_page.dart @@ -9,6 +9,7 @@ import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/bloc/security_settings/security_settings_bloc.dart'; import 'package:web_dex/bloc/security_settings/security_settings_event.dart'; +import 'package:web_dex/bloc/settings/settings_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/wallet.dart'; @@ -278,7 +279,7 @@ class _NewField extends StatelessWidget { final TextEditingController controller; final bool isObscured; - final Function(bool) onVisibilityChange; + final void Function(bool) onVisibilityChange; @override Widget build(BuildContext context) { @@ -286,15 +287,24 @@ class _NewField extends StatelessWidget { hintText: LocaleKeys.enterNewPassword.tr(), controller: controller, isObscured: isObscured, - validator: (String? password) => validatePassword( - password ?? '', - LocaleKeys.walletCreationFormatPasswordError.tr(), - ), + validator: (password) => _validatePassword(password, context), suffixIcon: PasswordVisibilityControl( onVisibilityChange: onVisibilityChange, ), ); } + + String? _validatePassword(String? passwordText, BuildContext context) { + final settingsBlocState = context.read().state; + final allowWeakPassword = settingsBlocState.weakPasswordsAllowed; + final password = passwordText ?? ''; + + if (allowWeakPassword) { + return null; + } + + return validatePassword(password); + } } class _ConfirmField extends StatelessWidget { diff --git a/lib/views/wallets_manager/widgets/creation_password_fields.dart b/lib/views/wallets_manager/widgets/creation_password_fields.dart index 7390380d60..137cdc6515 100644 --- a/lib/views/wallets_manager/widgets/creation_password_fields.dart +++ b/lib/views/wallets_manager/widgets/creation_password_fields.dart @@ -1,6 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/settings/settings_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/shared/utils/validators.dart'; import 'package:web_dex/shared/widgets/password_visibility_control.dart'; @@ -90,12 +92,16 @@ class _CreationPasswordFieldsState extends State { ); } - // Password validator String? _validatePasswordField(String? passwordFieldInput) { - return validatePassword( - passwordFieldInput ?? '', - LocaleKeys.walletCreationFormatPasswordError.tr(), - ); + final settingsBlocState = context.read().state; + final allowWeakPassword = settingsBlocState.weakPasswordsAllowed; + final password = passwordFieldInput ?? ''; + + if (allowWeakPassword) { + return null; + } + + return validatePassword(password); } String? _validateConfirmPasswordField(String? confirmPasswordFieldInput) { diff --git a/test_units/main.dart b/test_units/main.dart index 55955f9458..7f2f44924d 100644 --- a/test_units/main.dart +++ b/test_units/main.dart @@ -54,7 +54,7 @@ void main() { group('Password:', () { testValidateRPCPassword(); - testValidatePassword(); + testcheckPasswordRequirements(); }); group('Sorting:', () { diff --git a/test_units/tests/password/validate_password_test.dart b/test_units/tests/password/validate_password_test.dart index b9bc2f8b42..fbd53195a3 100644 --- a/test_units/tests/password/validate_password_test.dart +++ b/test_units/tests/password/validate_password_test.dart @@ -1,18 +1,509 @@ +import 'dart:math'; + import 'package:test/test.dart'; import 'package:web_dex/shared/utils/validators.dart'; -void testValidatePassword() { - test('Get from Coin usd price and return formatted string', () { - const errorMsg = 'Error'; - expect(validatePassword('passwordwith Space', errorMsg), errorMsg); - expect(validatePassword('passwordwith Space!', errorMsg), null); - expect(validatePassword('passwordwith_Space', errorMsg), null); - expect(validatePassword('passwordwith_Space!', errorMsg), null); - expect(validatePassword('ABCdec123123!', errorMsg), null); - expect(validatePassword('123123', errorMsg), errorMsg); - expect(validatePassword('ABCDEF', errorMsg), errorMsg); - expect(validatePassword('abcdef', errorMsg), errorMsg); - expect(validatePassword('!@#%', errorMsg), errorMsg); - expect(validatePassword('', errorMsg), errorMsg); +void testcheckPasswordRequirements() { + group('Password validation tests', () { + test('Too short passwords should fail', () { + expect( + checkPasswordRequirements('Abc1!'), + PasswordValidationError.tooShort, + ); + expect(checkPasswordRequirements(''), PasswordValidationError.tooShort); + expect( + checkPasswordRequirements('A1b!'), + PasswordValidationError.tooShort, + ); + }); + + test('Passwords containing "password" should fail', () { + expect( + checkPasswordRequirements('myPassword123!'), + PasswordValidationError.containsPassword, + ); + expect( + checkPasswordRequirements('PASSWORDabc123!'), + PasswordValidationError.containsPassword, + ); + expect( + checkPasswordRequirements('pAsSwOrD123!'), + PasswordValidationError.containsPassword, + ); + expect( + checkPasswordRequirements('My-password-is-secure!123'), + PasswordValidationError.containsPassword, + ); + }); + + test('Passwords without digits should fail', () { + expect( + checkPasswordRequirements('StrongPass!'), + PasswordValidationError.missingDigit, + ); + expect( + checkPasswordRequirements('NoDigitsHere!@#'), + PasswordValidationError.missingDigit, + ); + }); + + test('Passwords without lowercase should fail', () { + expect( + checkPasswordRequirements('STRONG123!'), + PasswordValidationError.missingLowercase, + ); + expect( + checkPasswordRequirements('ALL123CAPS!@#'), + PasswordValidationError.missingLowercase, + ); + }); + + test('Passwords without uppercase should fail', () { + expect( + checkPasswordRequirements('strong123!'), + PasswordValidationError.missingUppercase, + ); + expect( + checkPasswordRequirements('all123lower!@#'), + PasswordValidationError.missingUppercase, + ); + }); + + test('Passwords without special characters should fail', () { + expect( + checkPasswordRequirements('Strong123'), + PasswordValidationError.missingSpecialCharacter, + ); + expect( + checkPasswordRequirements('NoSpecial1Characters2'), + PasswordValidationError.missingSpecialCharacter, + ); + }); + + test('Multiple validation errors should return most critical first', () { + expect( + checkPasswordRequirements('pass'), + PasswordValidationError.tooShort, + ); + expect( + checkPasswordRequirements('passwordddd'), + PasswordValidationError.containsPassword, + ); + expect( + checkPasswordRequirements('Abcaaa1234*%'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + checkPasswordRequirements('Abcde123'), + PasswordValidationError.missingSpecialCharacter, + ); + }); + + test('Edge cases with spaces and special formatting', () { + expect( + checkPasswordRequirements('Pass 123!'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('Tab\t123!A'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('Line\nBreak123!A'), + PasswordValidationError.none, + ); + }); + + test('Passwords with numbers in various positions', () { + expect( + checkPasswordRequirements('1AbcSpecial!'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('Abc1Special!'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('AbcSpecial!1'), + PasswordValidationError.none, + ); + }); + + test('Various special characters', () { + expect( + checkPasswordRequirements('AbcDef123@'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('Abc_Def123#'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements(r'AbcDef123$'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('AbcDef123%'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('AbcDef123&'), + PasswordValidationError.none, + ); + }); + + test('Valid passwords should not fail', () { + expect( + checkPasswordRequirements('Very!hard!pass!77'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('Komodo2024!'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('Complex!P4ssword123'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements(r'!P4ssword#$@'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('Mix3d_Ch4r4ct3rs!'), + PasswordValidationError.none, + ); + }); + + test('Password specifically mentioned in the issue should be rejected', () { + // Should fail (has consecutive characters) + expect( + checkPasswordRequirements('Very!hard!pass!777'), + PasswordValidationError.consecutiveCharacters, + ); + }); + + test( + 'Passwords with three or more consecutive identical ' + 'characters should fail', () { + expect( + checkPasswordRequirements('Strong111Security!'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + checkPasswordRequirements('Secure222!A'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + checkPasswordRequirements('A1!Secure333'), + PasswordValidationError.consecutiveCharacters, + ); + + expect( + checkPasswordRequirements('aaaStrong1!'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + checkPasswordRequirements('Strong1!bbb'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + checkPasswordRequirements('Strong1!CCC'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + checkPasswordRequirements('Strong1!!!Secure'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + checkPasswordRequirements('Strong1###Secure'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + checkPasswordRequirements(r'Strong1$$$Secure'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + checkPasswordRequirements('Strong1!aaaaa'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + checkPasswordRequirements('Strong1!44444'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + checkPasswordRequirements('Strong1!!!!!'), + PasswordValidationError.consecutiveCharacters, + ); + }); + + test( + 'Valid passwords with two consecutive identical characters should pass', + () { + expect( + checkPasswordRequirements('Strong11Secured!'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('Strong!!Secured1'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('aaStrong1!Secured'), + PasswordValidationError.none, + ); + }); + + test('Special case - passwords with unicode characters', () { + expect( + checkPasswordRequirements('Пароль123!'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('密码Abc123!'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('Mötley123!'), + PasswordValidationError.none, + ); + }); + + test('Extended Unicode character password tests', () { + expect( + checkPasswordRequirements('علي123!Abc'), // Arabic + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('こんにちは123!Ab'), // Japanese + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('안녕하세요123!Ab'), // Korean + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('Привет123!Ab'), // Cyrillic + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('Γειά123!Aa'), // Greek + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('שלום123!Aa'), // Hebrew + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('नमस्ते123!Ab'), // Devanagari + PasswordValidationError.none, + ); + }); + + test('Unicode edge cases and challenging patterns', () { + expect( + checkPasswordRequirements('Раssw0rd!'), // Cyrillic 'Р' (not Latin 'P') + PasswordValidationError.none, + ); + + expect( + checkPasswordRequirements('Pass\u200Bword123!'), + PasswordValidationError.none, + ); + + expect( + // a + combining acute accent + checkPasswordRequirements('Pa\u0301ssword123!'), + PasswordValidationError.none, + ); + + expect( + checkPasswordRequirements('Strong🔑123!A'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('A1!🎮🎲🎯aa'), + PasswordValidationError.none, + ); + + expect( + checkPasswordRequirements('Strоng123!'), + PasswordValidationError.none, + ); + }); + + test('Unicode sequential characters detection', () { + expect( + checkPasswordRequirements('Strong爱爱爱123!'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + checkPasswordRequirements('Strong😊😊😊123!'), + PasswordValidationError.consecutiveCharacters, + ); + + // Characters that look similar but are actually different code points + expect( + checkPasswordRequirements('StrongАААbc123!'), + PasswordValidationError.consecutiveCharacters, + ); + }); + + test('Bidirectional text and special Unicode formatting', () { + // Right-to-left marks and embedding + expect( + checkPasswordRequirements('Pass\u200Eword123!A'), // Contains LTR mark + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('Pass\u200Fword123!A'), // Contains RTL mark + PasswordValidationError.none, + ); + + // Mixed directionality + expect( + checkPasswordRequirements('Abcהמסיסמ123!'), // Hebrew mixed with Latin + PasswordValidationError.none, + ); + + // Special spaces + expect( + checkPasswordRequirements('Pass\u2007word123!A'), // Figure space + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('Pass\u00A0word123!A'), // Non-breaking space + PasswordValidationError.none, + ); + }); + + test('Advanced emoji password tests in valid passwords', () { + expect( + checkPasswordRequirements('Strong123!🔒'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('🔑Abcasba123!'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('Pass🔥123!A'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('Abc123!🌟✨🚀'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('🎮🎯A1!abaa'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('Strong👨‍👩‍👧‍👦123!'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('A1!👍🏽Strong1234'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('Pass🇺🇸123!A'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('Strong123A🎯'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('Strong1A🎯🎯🎯'), + PasswordValidationError.consecutiveCharacters, + ); + expect( + checkPasswordRequirements('🔥🔥🔥Strong1A!'), + PasswordValidationError.consecutiveCharacters, + ); + }); + test('Complex emoji sequences and ZWJ', () { + expect( + // ZWJ sequence (man technologist) + checkPasswordRequirements('Strong123A👨‍💻'), + PasswordValidationError.none, + ); + expect( + // Complex ZWJ sequence + checkPasswordRequirements('Strong123A👁️‍🗨️'), + PasswordValidationError.none, + ); + expect( + // Emoji presentation selector + checkPasswordRequirements('Strong123A☺️'), + PasswordValidationError.none, + ); + }); + + test('Mixed emoji and text patterns', () { + expect( + checkPasswordRequirements('Aaba🔒1🔑!🚀'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('Se🔒cure123!'), + PasswordValidationError.none, + ); + expect( + checkPasswordRequirements('St🔑r🔒ng1!'), + PasswordValidationError.none, + ); + expect( + // Should not trigger containsPassword + checkPasswordRequirements('p🔑ssw🔒rd123A!'), + PasswordValidationError.none, + ); + expect( + 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 + 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) { + checkPasswordRequirements(input); + } + }); }); }