2024-12-04 21:42:18 +00:00
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
|
|
|
|
namespace back_end.Services;
|
|
|
|
|
|
|
|
public class PasswordService : IPasswordService
|
|
|
|
{
|
2024-12-09 23:54:54 +00:00
|
|
|
// Since we can do pre-processing on startup, sticking the common passwords into a tree should cut the search time
|
|
|
|
// down by a lot, no more searching through 6456 common passwords before getting to 'password123!'!
|
|
|
|
// After the first step we go down to about 150 possible passwords which will more or less be instant after that!
|
2024-12-09 23:45:47 +00:00
|
|
|
|
|
|
|
private class PasswordTreeNode {
|
|
|
|
|
|
|
|
public Dictionary<char, PasswordTreeNode> children { get; set; }
|
|
|
|
public bool endOfWord { get; set; }
|
|
|
|
|
|
|
|
public PasswordTreeNode()
|
|
|
|
{
|
|
|
|
children = new Dictionary<char, PasswordTreeNode>();
|
|
|
|
endOfWord = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private class PasswordTree
|
|
|
|
{
|
|
|
|
private readonly PasswordTreeNode _root;
|
|
|
|
|
|
|
|
public PasswordTree()
|
|
|
|
{
|
|
|
|
_root = new PasswordTreeNode();
|
|
|
|
}
|
|
|
|
|
|
|
|
public void Insert(string password)
|
|
|
|
{
|
|
|
|
var currentNode = _root;
|
|
|
|
foreach (var c in password)
|
|
|
|
{
|
|
|
|
if (!currentNode.children.ContainsKey(c))
|
|
|
|
{
|
|
|
|
currentNode.children.Add(c, new PasswordTreeNode());
|
|
|
|
}
|
|
|
|
currentNode = currentNode.children[c];
|
|
|
|
}
|
|
|
|
currentNode.endOfWord = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public bool Search(string password)
|
|
|
|
{
|
|
|
|
var currentNode = _root;
|
|
|
|
foreach (var c in password)
|
|
|
|
{
|
|
|
|
// This is where the logic gets a bit confusing
|
|
|
|
// if we want to just check whether the password the user gives 'passwords' EXACTLY
|
|
|
|
// then this traversal is easy, we just do the following
|
|
|
|
if (!currentNode.children.ContainsKey(c)) return false;
|
|
|
|
currentNode = currentNode.children[c];
|
|
|
|
}
|
|
|
|
return currentNode.endOfWord;
|
|
|
|
/* However, I had the idea that instead of just checking the tree like this, I'd want it to loop through
|
|
|
|
* the password given:
|
|
|
|
*
|
|
|
|
* For example
|
|
|
|
* 123!passwords should loop through the first four characters (123!)
|
|
|
|
* and just continue looping through the above once it gets to p and return true for the common check
|
|
|
|
* I think this would just be done by popping off the first character, so we get down to just 'passwords'
|
|
|
|
* for the example which would work just fine, but for something like '123!someotherpasswords' it would
|
|
|
|
* not
|
|
|
|
* For this to work, I'd have to really slice up the stirng in multiple (if not all) of the possible spaces
|
|
|
|
* and then run this check in parallel since now I'd be giving the program a LOT more work
|
|
|
|
* I believe that that would be overengineering this however, as someotherpasswords is (marginally) more
|
|
|
|
* secure than the common password 'passwords', but still fun to think about!
|
|
|
|
*/
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-12-05 20:42:23 +00:00
|
|
|
private readonly List<string> _commonPasswords = new List<string>();
|
2024-12-04 21:42:18 +00:00
|
|
|
|
2024-12-09 23:45:47 +00:00
|
|
|
public PasswordService(string commonPasswordFilepath)
|
|
|
|
{
|
|
|
|
LoadCommonPasswords(commonPasswordFilepath);
|
|
|
|
}
|
|
|
|
|
2024-12-05 20:42:23 +00:00
|
|
|
// load common passwords on start up to save us from having to re-load the file over and over
|
|
|
|
// if the list would change, then this is a bad idea
|
2024-12-04 21:42:18 +00:00
|
|
|
public void LoadCommonPasswords(string filepath)
|
|
|
|
{
|
|
|
|
if (_commonPasswords.Count != 0) return;
|
|
|
|
try
|
|
|
|
{
|
|
|
|
_commonPasswords.AddRange(File.ReadAllLines(filepath));
|
|
|
|
}
|
|
|
|
catch (Exception e)
|
|
|
|
{
|
2024-12-05 20:42:23 +00:00
|
|
|
throw new Exception("Error loading common passwords: ", e);
|
2024-12-04 21:42:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public bool IsPasswordInvalid(string password)
|
|
|
|
{
|
|
|
|
// RegEx feels like cheating it's so good
|
|
|
|
Regex regex = new Regex("^(?=.*?[a-zA-Z])(?=.*?[0-9])(?=.*?[!£$^*#])[a-zA-Z0-9!£$^*#]{7,14}$");
|
2024-12-09 23:45:47 +00:00
|
|
|
return !(regex.IsMatch(password)
|
|
|
|
&& this.IsPasswordLengthValid(password)
|
|
|
|
&& this.IsPasswordContainingMinimumCharacters(password)
|
|
|
|
&& this.IsPasswordContainingOnlyLegalCharacters(password));
|
|
|
|
}
|
|
|
|
|
|
|
|
public bool IsPasswordLengthValid(string password) {
|
|
|
|
// check if all the characters between the start and end of the password add to 7-14 characters
|
|
|
|
Regex regex = new Regex("^.{7,14}$");
|
|
|
|
return regex.IsMatch(password);
|
|
|
|
}
|
|
|
|
|
|
|
|
public bool IsPasswordContainingMinimumCharacters(string password) {
|
|
|
|
// look ahead for the special characters and digits and match them at least once
|
|
|
|
Regex regex = new Regex("^(?=.*?[!£$^*#])(?=.*?[0-9]).*$");
|
|
|
|
return regex.IsMatch(password);
|
|
|
|
}
|
|
|
|
|
|
|
|
public bool IsPasswordContainingOnlyLegalCharacters(string password) {
|
|
|
|
// check for any characters not in the allowed list
|
|
|
|
Regex regex = new Regex("^[a-zA-Z0-9!£$^*#]*$");
|
|
|
|
return regex.IsMatch(password);
|
2024-12-04 21:42:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public bool IsPasswordCommon(string? password)
|
|
|
|
{
|
2024-12-05 20:42:23 +00:00
|
|
|
if (password == null) return true;
|
|
|
|
// since .Contains tries to checks the index, password123! and 123!password will return true
|
2024-12-04 21:42:18 +00:00
|
|
|
return _commonPasswords.Contains(password);
|
|
|
|
}
|
|
|
|
}
|