0
votes

I made a database in Unity with C#, and for some reason it is not working. I made a static class with all the variables static and made save, load, check, create and reset functions. I also made a script that sits on a ScriptHolder GameObject and all the Buttons and InputFields reference. Here is my code:

Database Class:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public static class Database
{
    public enum Roles{
        Teacher,
        Student
    }
    public static Dictionary<string, string> logins = new Dictionary<string, string>();
    public static List<Roles> roles = new List<Roles>();

    public static void AddUser(string username, string password, Roles role)
    {
        logins.Add(username, password);
        roles.Add(role);
    }

    public static void SaveUsers()
    {
        LoadUsers();
        PlayerPrefs.DeleteAll();
        if(logins.Count != roles.Count)
        {
            throw new System.Exception("Roles count and login count do not match!");
        }
        int counter = 0;
        foreach (KeyValuePair<string, string> login in logins)
        {
            PlayerPrefs.SetString(login.Key, login.Value);
            PlayerPrefs.SetString("usernames", "");
            PlayerPrefs.SetString("usernames", PlayerPrefs.GetString("usernames") + "/" + login.Key);
            PlayerPrefs.SetInt(login.Key, (int)roles[counter]);
            counter += 1;
            Debug.Log(PlayerPrefs.GetString("usernames") + "/" + login.Key);
        }
    }

    public static void LoadUsers()
    {
        logins = new Dictionary<string, string>();
        roles = new List<Roles>();
        foreach (string key in PlayerPrefs.GetString("usernames").Split('/'))
        {
            logins.Add(key, PlayerPrefs.GetString(key));
            roles.Add((Roles)PlayerPrefs.GetInt(key));
        }

    }

    public static void FactorySettings()
    {
        PlayerPrefs.DeleteAll();
        LoadUsers();
        logins = new Dictionary<string, string>();
    }

    public static bool CheckUser(string username, string password)
    {
        if (logins.ContainsKey(username))
        {
            if (password == logins[username])
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        else
        {
            return false;
        }
    }
}

ScriptHolder:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using TMPro;

public class LoginProcess : MonoBehaviour
{
    public TextMeshProUGUI username;
    public TextMeshProUGUI password;

    public Slider loadingSlider;

    public GameObject error;
    public GameObject loading;

    public float progress = 0f;

    public Dictionary<string, string> show = Database.logins;

    public void Login()
    {
        if(Database.CheckUser(username.text, password.text))
        {
            StartCoroutine(LoadAsync(1));
            loading.SetActive(true);
        }
        else
        {
            error.SetActive(false);
        }
    }

    public void Create()
    {
        Database.AddUser(username.text, password.text, Database.Roles.Teacher);
        Database.SaveUsers();
        Database.LoadUsers();
    }

    public void _Reset()
    {
        Database.FactorySettings();
    }

    public void Start()
    {
        Database.LoadUsers();
    }

    IEnumerator LoadAsync (int index)
    {
        AsyncOperation operation = SceneManager.LoadSceneAsync(index);

        while (!operation.isDone)
        {
            float _progress = Mathf.Clamp01(operation.progress / .9f);

            progress = _progress;

            yield return null;
        }
    }
    public void Update()
    {
        if (progress != 0)
        {
            loadingSlider.value = progress;
        }

        if (Input.GetMouseButtonDown(0) && error.activeSelf)
        {
            error.SetActive(true);
        }
    }
}

My Error:

ArgumentException: An item with the same key has already been added. Key: System.Collections.Generic.Dictionary'2[TKey,TValue].TryInsert (TKey key, TValue value, System.Collections.Generic.InsertionBehavior behavior) (at <599589bf4ce248909b8a14cbe4a2034e>:0) System.Collections.Generic.Dictionary'2[TKey,TValue].Add (TKey key, TValue value) (at <599589bf4ce248909b8a14cbe4a2034e>:0)

1

1 Answers

1
votes

In general: it is a bad idea to use PlayerPrefs for sensitive data .. they are stored on the device as raw text so you will be able to read out all passwords or modify it in order to e.g. change a users Role!


What don't you understand in the error?

Apparently you are trying to .Add a key-value-pair to the Dictionary when the given key already exists in that Dictionary.

There is no check in AddUser to catch such a case so maybe you should rather do something like

public static void AddUser(string username, string password, Roles role)
{
    // in general check if a string is empty!
    if(username.IsNullOrWhitespace())
    {
        // TODO: SHOW AN ERROR IN THE GUI THAT USERNAME MAY NOT BE EMPTY
        Debug.LogWarning("username may not be empty!", this);
        return;
    }

    if(logins.ContainsKey(username))
    {
        // TODO: SHOW AN ERROR IN THE GUI THAT THIS USER ALREADY EXISTS
        Debug.LogWarning($"A User with name \"{username}\" already exists! Please chose another username.", this);
        return;
    }

    logins.Add(username, password);
    roles.Add(role);
}

In general there are some really strange orders in your code for example

Database.AddUser(username.text, password.text, Database.Roles.Teacher); Database.SaveUsers(); Database.LoadUsers();

You add the user username.text but in SaveUsers the first thing you do is calling LoadUsers which resets both dictionaries and then loads already existing users ... so the one you just created is lost.


Or if you do FactorySettings

PlayerPrefs.DeleteAll();
LoadUsers();
logins = new Dictionary<string, string>();

You first delete all PlayerPRefs, then LoadUsers .. knowing there should be no outcome anyway except resetting logins and roles and then you reset logins again. This is quite redundant. You could just say

PlayerPrefs.DeleteAll();
logins.Clear();
roles.Clear();

Or have a look at the SaveUsers method again: In your foreach loop you do

 PlayerPrefs.SetString(login.Key, login.Value);
 PlayerPrefs.SetString("usernames", "");
 PlayerPrefs.SetString("usernames", PlayerPrefs.GetString("usernames") + "/" + login.Key);

so apparently you anyway store each username, password pair individually .. so why even bother using the other thing?

Now to the other thing: You reset PlayerPrefs.SetString("usernames", ""); in every iteration of the loop ... so there will always only be exactly one username/password stored!


Also it seems a bit odd/unsecure for me that for matching a username with a password you are using a Dictionary but when it comes to this users role (which is almost equally important) you just use a List and access it by index.


So Overall

I would rather use a proper class like e.g.

[Serializable]
public class User
{
    private const SECRET_SALT = "1234My_Example";

    public string Name;

    // Never store a naked Password!
    // Rather store the hash and everytime you want to check the password
    // compare the hashes!
    public string PasswordHash;

    // As said actually you should also not store this as a simple modifiable value
    // Since it almost has the same security impact as a password!
    public Roles Role;

    public User(string name, string password, Roles role)
    {
        Name = name;
        // Instead of the raw password store a hash
        PasswordHash = GetHash(password);
        Role = role;
    }

    // Compare the hash of the given password attempt to the stored one
    public bool CheckPassword(string attemptedPassword, string hash)
    {
        var base64AttemptedHash = GetHash(attemptedPassword);

        return base64AttemptedHash.Equals(PasswordHash);
    }

    private static string GetHash(string password)
    {
        // Use the secret salt so users can not simply edit the stored file
        // and add a users password brute-forcing the known blank hashing methods
        var unhashedBytes = Encoding.Unicode.GetBytes(SECRET_SALT + password);

        var sha256 = new SHA256Managed();
        var hashedBytes = sha256.ComputeHash(unhashedBytes);

        return Convert.ToBase64String(hashedBytes);
    }
}

And then have e.g. this as the DataBase.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Cryptography;
using System.Text;
using UnityEditor;
using UnityEngine;


public static class DataBase
{
    // Do not make the Dictionary public otherwise it can be modified by any class!
    // Rather only provide specific setters and getters
    private static Dictionary<string, User> _users = new Dictionary<string, User>();

    // You might even want to give this bit a less obvious name ;)
    private const string fileName = "DataBaseCredentials.cfg";

    private static string filePath;

    // This method will be automatically called on app start
    [InitializeOnLoadMethod]
    private static void Initialize()
    {
        filePath = Path.Combine(Application.persistentDataPath, fileName);

        if (!Directory.Exists(Application.persistentDataPath))
        {
            Directory.CreateDirectory(Application.persistentDataPath);
        }

        if (!File.Exists(filePath))
        {
            File.Create(filePath);
        }

        LoadUsers();
    }

    private static void LoadUsers()
    {
        Debug.Log("DataBase: Loading Users from " + filePath);

        using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
        {
            if (stream.Length == 0) return;

            var bf = new BinaryFormatter();
            _users = (Dictionary<string, User>)bf.Deserialize(stream);
        }
    }

    private static void SaveUsers()
    {
        Debug.Log("DataBase: Storing Users to " + filePath);

        using (var stream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write))
        {
            var bf = new BinaryFormatter();
            bf.Serialize(stream, _users);
        }

        LoadUsers();
    }

    public static User GetUserByName(string username)
    {
        if (string.IsNullOrWhiteSpace(username))
        {
            Debug.LogWarning("username may not be empty!");
            return null;
        }

        if (!_users.ContainsKey(username))
        {
            Debug.LogWarning($"A user with name \"{username}\" does not exist!");
            return null;
        }

        return _users[username];
    }

    public static bool LogIn(string username, string password)
    {
        var user = GetUserByName(username);
        return user == null ? false : user.CheckPassword(password);
    }

    public static void AddUser(string username, string password, Roles role)
    {
        // Check the name
        if (string.IsNullOrWhiteSpace(username))
        {
            Debug.LogWarning("username may not be empty!");
            return;
        }

        if (_users.ContainsKey(username))
        {
            Debug.LogWarning($"A user with name \"{username}\" already exists! Chose another username!");
            return;
        }

        _users.Add(username, new User(username, password, role));

        SaveUsers();
    }

    public static void FactorySettings()
    {
        Debug.Log("FactorySettings!");

        _users.Clear();
        SaveUsers();
    }
}

[Serializable]
public class User
{
    public string Name;
    public string PasswordHash;
    public Roles Role;


    public User(string name, string password, Roles role)
    {
        Name = name;

        // Never store a naked Password!
        // Rather store the hash and everytime you want to check the password
        // compare the hashes!
        PasswordHash = GetHash(password);

        // As said actually you should also not store this as a simple modifiable value
        // Since it almost has the same security impact as a password!
        Role = role;
    }

    private static string GetHash(string password)
    {
        var unhashedBytes = Encoding.Unicode.GetBytes(password);

        var sha256 = new SHA256Managed();
        var hashedBytes = sha256.ComputeHash(unhashedBytes);

        return Convert.ToBase64String(hashedBytes);
    }

    public bool CheckPassword(string attemptedPassword)
    {
        var base64AttemptedHash = GetHash(attemptedPassword);

        return base64AttemptedHash.Equals(PasswordHash);
    }
}

public enum Roles
{
    Teacher,
    Student
}

I know BinaryFormatter creates quite huge files and you could also do this e.g. as JSON but this was the easiest way for showing how to (de)serialize a Dictionary into a system file.


And just for a little Demo Class

using UnityEditor;
using UnityEngine;

public class UsersManager : MonoBehaviour
{
    public string username;
    public string password;
    public Roles role;

    public User User;
}

[CustomEditor(typeof(UsersManager))]
public class UsersManagerEditor : Editor
{
    private UsersManager manager;

    private void OnEnable()
    {
        manager = (UsersManager)target;
    }

    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        if (GUILayout.Button("GetUserByName"))
        {
            manager.User = DataBase.GetUserByName(manager.username);
        }

        if (GUILayout.Button("AddUser"))
        {
            DataBase.AddUser(manager.username, manager.password, manager.role);
        }

        if (GUILayout.Button("CheckPassword"))
        {
            manager.User = DataBase.GetUserByName(manager.username);

            if (manager.User != null)
            {

                if (manager.User.CheckPassword(manager.password))
                {
                    Debug.Log("Password CORRECT!");
                }
                else
                {
                    Debug.LogWarning("PASSWORD WRONG!");
                }
            }
        }

        if (GUILayout.Button("FactorySettings"))
        {
            DataBase.FactorySettings();
        }
    }
}

enter image description here