Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
}
169 changes: 121 additions & 48 deletions packages/komodo_defi_types/lib/src/utils/security_utils.dart
Original file line number Diff line number Diff line change
@@ -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'~`$^*+=<>?';

Comment thread
takenagain marked this conversation as resolved.
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 = <String>[
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 = <String>[
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 {
Expand Down
Loading
Loading