diff --git a/lib/security/auth.dart b/lib/security/auth.dart index 9bd59ce..5feaebf 100644 --- a/lib/security/auth.dart +++ b/lib/security/auth.dart @@ -112,8 +112,12 @@ class AuthController extends ChangeNotifier { Future register(String username, String password) async { final name = username.trim(); if (name.isEmpty) return 'Please choose a username.'; - if (password.length < 6) { - return 'Password must be at least 6 characters.'; + if (password.length < 10) { + return 'Password must be at least 10 characters.'; + } + if (!RegExp(r'[A-Za-z]').hasMatch(password) || + !RegExp(r'[0-9]').hasMatch(password)) { + return 'Password must include a letter and a number.'; } if (_accounts.any((a) => a.username.toLowerCase() == name.toLowerCase())) { return 'That username is already taken.'; @@ -205,3 +209,4 @@ class _Lockout { final int fails; final int untilMillis; } + diff --git a/test/security_test.dart b/test/security_test.dart index f59ff6d..39194a5 100644 --- a/test/security_test.dart +++ b/test/security_test.dart @@ -34,13 +34,27 @@ void main() { }); group('auth lockout', () { + test('rejects weak passwords for encrypted local accounts', () async { + SharedPreferences.setMockInitialValues({}); + final auth = AuthController(await SharedPreferences.getInstance()); + + expect( + await auth.register('short', 'abc123'), + 'Password must be at least 10 characters.', + ); + expect( + await auth.register('letters', 'abcdefghij'), + 'Password must include a letter and a number.', + ); + }); + test('locks after repeated failures and unlocks once time passes', () async { SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); var now = DateTime(2026, 7, 28, 12); final auth = AuthController(prefs, clock: () => now); - expect(await auth.register('bob', 'correct1'), isNull); + expect(await auth.register('bob', 'correct1234'), isNull); auth.logout(); for (var i = 0; i < 5; i++) { @@ -49,12 +63,13 @@ void main() { // Sixth attempt is refused by the lockout, even with the right password. expect(await auth.login('bob', 'wrong'), contains('Too many attempts')); expect(auth.lockRemainingSeconds('bob'), greaterThan(0)); - expect(await auth.login('bob', 'correct1'), contains('Too many attempts')); + expect( + await auth.login('bob', 'correct1234'), contains('Too many attempts')); // Move past the lockout window. now = now.add(const Duration(seconds: 40)); expect(auth.lockRemainingSeconds('bob'), 0); - expect(await auth.login('bob', 'correct1'), isNull); + expect(await auth.login('bob', 'correct1234'), isNull); expect(auth.isLoggedIn, isTrue); }); }); @@ -85,3 +100,4 @@ void main() { }); }); } +