Skip to content

Instantly share code, notes, and snippets.

@Adanos020
Last active March 21, 2018 17:29
Show Gist options
  • Save Adanos020/b53c42825ddd5d25e6661fddedc03b55 to your computer and use it in GitHub Desktop.
Save Adanos020/b53c42825ddd5d25e6661fddedc03b55 to your computer and use it in GitHub Desktop.
/*
* A simple database of user accounts.
*
* Author: Adam Gasior
*/
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <string>
#include <unordered_map>
#include <vector>
// Utility functions.
/**
* \brief Generates a hash for a given string.
*
* Takes each character of a given string and performs series of bitwise operations
* on it and large prime numbers resulting in a huge number. Its purpose is to
* represent any string of characters as a unique number, however collisions are
* inevitable.
*
* \param str A string to hash.
*
* \return The resulting hash.
*/
long long hash(std::string str)
{
// Taking some not too big prime.
long long hash = 37;
// XOR-ing each character's multiply by a large prime with our previous prime's
// multiply by another large prime.
for (char& c : str)
{
hash = (hash * 54059) ^ (c * 76963);
}
return hash;
}
/**
* \brief Replaces characters of a string from one to other set of characters.
*
* Takes a string and based on a character's index in one character array replaces it with
* a character of the same index from the other array. Used for keyword encryption.
*
* \param str The string to encrypt.
* \param chars Set of characters from which index of <code>str</code>'s character is taken.
* \param sybst Set of characters with which the characters of <code>str</code> are replaced.
*
* \return Encrypted version of <code>str</code>.
*/
std::string replace(std::string str, std::string chars, std::string subst)
{
// These two strings must be of equal length.
assert(chars.length() == subst.length());
// Iterating through str and chars to find the index of a str's character in chars.
for (char& c : str) for (int i = 0; i < chars.length(); ++i)
{
if (c == chars[i])
{
c = subst[i]; // If the character is found in chars then a str's character is
break; // replaced with its corresponding version in subst.
}
}
return str;
}
/**
* \brief Generates a random string of a given length, used as salt.
*
* \param length Desired length of the random string.
*
* \return A random string.
*/
std::string randstr(size_t length)
{
// Initialising an empty string.
std::string res = "";
// Generating specified number of characters.
for (size_t i = 0; i < length; ++i)
{
res += [](char c) -> char {
return (c != ',' && c != '\n' ? c : c + 1); // Making sure that the resulting string
} // doesn't contain a comma or a new line
((char) rand() % 255 + 1); // character.
}
return res;
}
/**
* \brief Splits a string into an array of subsequent its parts.
*
* Takes a string and every time it encounters a delimiting character it adds a substring
* between the previous and the current delimiter to an array. After the string ends, it
* returns the array of segments without the delimiters.
*
* \param str The string to split.
* \param delim Delimiter.
*
* \return The array of string segments.
*/
std::vector<std::string> split(std::string str, char delim)
{
// Creating the array of segments.
std::vector<std::string> segms;
// Initialising the auxiliary string storing current str's segment.
std::string segm = "";
// Iterating through str.
for (char c : str)
{
// If a delimiter is encountered, adding the segment to the array and
// clearing it.
if (c == delim)
{
segms.push_back(segm);
segm = "";
}
else segm += c; // If not a delimiter, then it can be appended to the segment.
}
// After iterating the last resulting segment can be added to the array,
// given it is not empty.
if (segm != "") segms.push_back(segm);
return segms;
}
// Structures.
/**
* Data of a user account.
*/
struct UserAccount
{
public:
std::string username_enc; ///< Encrypted username.
std::string salt_enc; ///< Encrypted salt.
long long hashed_pword; ///< Hash of salted password.
std::string name_enc; ///< Encrypted first name.
std::string surname_enc; ///< Encrypted surname.
std::string age_enc; ///< Encrypted age.
std::string favFood_enc; ///< Encrypted favourite food.
public:
/**
* \brief Creates a user account.
*
* Encrypts user account data, salts and hashes the password and construct a user
* account object.
*
* \param username Username.
* \param password Password that will be salted and hashed.
* \param name First name.
* \param surname Surname.
* \param age Age.
* \param favFood Favourite food.
*/
UserAccount(std::string username = "",
std::string password = "",
std::string name = "",
std::string surname = "",
std::string age = "",
std::string favFood = "")
{
this->username_enc = encrypt(username); // Encrypting the username.
this->salt_enc = randstr(rand() % 50 + 50); // Generating the salt.
this->hashed_pword = hash(this->salt_enc + password); // Salting and hashing the password.
this->name_enc = encrypt(name); // Encrypting the first name.
this->surname_enc = encrypt(surname); // Encrypting the surname.
this->age_enc = encrypt(age); // Encrypting the age.
this->favFood_enc = encrypt(favFood); // Encrypting favourite food.
this->salt_enc = encrypt(this->salt_enc); // Encrypting the salt.
}
/**
* \brief Represents the account as a string containing the encrypted data. Used to save the
* \brief data into a file.
*
* \returns The string representation of the account.
*/
std::string to_string() const
{
return username_enc + "," + salt_enc + "," + std::to_string(hashed_pword) + "," + name_enc
+ "," + surname_enc + "," + age_enc + "," + favFood_enc;
}
/**
* \brief Parses a string to initialise data of the account.
*
* \param str The string to parse.
*/
void parse_string(std::string str)
{
// Splitting the array into legible data.
auto entries = split(str, ',');
// There must be exactly 7 entries as there are 7 fields to initialise.
assert(entries.size() == 7);
username_enc = entries[0]; // Assigning the encrypted username.
salt_enc = entries[1]; // Assigning the encrypted salt.
hashed_pword = atoll(entries[2].c_str()); // Assigning the hash of salted pasword.
name_enc = entries[3]; // Assigning the encrypted first name.
surname_enc = entries[4]; // Assigning the encrypted surname.
age_enc = entries[5]; // Assigning the age.
favFood_enc = entries[6]; // Assigning the encrypted favourite food.
}
private:
/**
* \brief Encrypts a string using keyword encryption.
*
* \return The encrypted string.
*/
std::string encrypt(std::string str)
{
// Setting up the available characters.
std::string chars = "0123456789abcdefghijklmnopqrtsuvwxyz";
// Setting up the keyword mixed into chars.
std::string subst = "rocksmith2014356789abdefgjlnpquvwxtz";
// Encrypting.
return replace(str, chars, subst);
}
};
/**
* Database storing data of all users.
*/
struct UserDatabase
{
private:
std::unordered_map<std::string, UserAccount> users; ///< Hash map of all users.
public:
/**
* \brief Creates a user account and adds it to the database.
*
* \param username Username.
* \param password Password that will be salted and hashed.
* \param name First name.
* \param surname Surname.
* \param age Age.
* \param favFood Favourite food.
*/
void add_user(std::string username,
std::string password,
std::string name,
std::string surname,
std::string age,
std::string favFood)
{
users[encrypt(username)] = UserAccount(username, password, name, surname, age, favFood);
}
/**
* \brief Logs into a user account with matching username and password.
*
* If the usernam and password are found in the database then it prints the user data to the console.
*
* \param username Username.
* \param password Password.
*/
void login(std::string username, std::string password)
{
// Checking if the username is found.
if (users.find(encrypt(username)) == users.end())
{
puts("Invalid login or password."); // Printing an ambiguous message to make it
return; // harder to hack into an account.
}
auto user = users[encrypt(username)];
long long hashed_pword = hash(decrypt(user.salt_enc) + password);
// Checking if password is correct.
if (hashed_pword != user.hashed_pword)
{
puts("Invalid login or password."); // Printing an ambiguous message to make it
return; // harder to hack into an account.
}
// Now print the greeting message.
puts("-----------------------");
puts(("Hello, " + decrypt(user.name_enc) + " " + decrypt(user.surname_enc) + "!").c_str());
puts(("You are " + decrypt(user.age_enc) + " years old.").c_str());
puts(("Your favourite food is " + decrypt(user.favFood_enc) + ".").c_str());
}
/**
* \brief Loads the database from a text file.
*
* \param file The input file stream from which the database will be read.
*/
void load(std::ifstream& file)
{
std::string line;
while (std::getline(file, line))
{
UserAccount acc = UserAccount();
acc.parse_string(line);
users[acc.username_enc] = acc;
}
}
/**
* \brief Saves the database to a text file.
*
* \param file The output file stream to which the database will be saved.
*/
void save(std::ofstream& file)
{
for (auto& user : users)
{
file << user.second.to_string() << '\n';
}
}
private:
/**
* \brief Encrypts a string using keyword encryption.
*
* \return The encrypted string.
*/
std::string encrypt(std::string str)
{
// Setting up the available characters.
std::string chars = "0123456789abcdefghijklmnopqrtsuvwxyz";
// Setting up the keyword mixed into chars.
std::string subst = "rocksmith2014356789abdefgjlnpquvwxtz";
// Encrypting.
return replace(str, chars, subst);
}
/**
* \brief Decrypts a string using keyword decryption. It's hidden for safety reasons.
*
* \return The decrypted string.
*/
std::string decrypt(std::string str)
{
// Setting up the keyword mixed into chars.
std::string chars = "rocksmith2014356789abdefgjlnpquvwxtz";
// Setting up the available characters.
std::string subst = "0123456789abcdefghijklmnopqrtsuvwxyz";
// Decrypting.
return replace(str, chars, subst);
}
};
// User interface functions.
/**
* \brief Displays a list of options a user can choose from.
*/
void display_menu();
/**
* \brief Asks the user for the option number.
*/
unsigned int read_option();
/**
* \brief Reads an unsigned integer and handles all input errors.
*/
unsigned int safe_read_int();
/**
* \brief Performs task assigned to an option chosen by the user.
*
* \param option Number of the option assigned to a task.
* \param database Reference to a database to perform a task on.
*
* \return <code>true</code> if the program has to be running after the task is completed,
* <code>false</code> otherwise.
*/
bool perform_task(unsigned int option, UserDatabase& database);
/**
* \brief Asks the user for login detais and gives an access to their account.
*
* \param database Reference to a database to perform a task on.
*/
void login(UserDatabase& database);
/**
* \brief Asks the user for login details and account data to create them an account.
*
* \param database Reference to a database to perform a task on.
*/
void add_user(UserDatabase& database);
/**
* \brief The program's main function.
*
* Initialises the user database and runs the program's main loop.
*/
int main()
{
// Setting the random number generator's seed to current time.
srand(time(0));
// Creating the database.
auto ub = UserDatabase();
// Loading data.
std::ifstream file("data.txt");
if (file.good()) ub.load(file);
// Main loop.
while (true)
{
// Displaying menu.
display_menu();
// Reading option number.
unsigned int opt = read_option();
// Performing a task and exiting if told to do so.
if (!perform_task(opt, ub)) break;
}
return 0;
}
void display_menu()
{
puts("-----------------------");
puts("What do you want to do?");
puts("0. Exit.");
puts("1. Login.");
puts("2. Register.");
}
unsigned int read_option()
{
return safe_read_int();
}
unsigned int safe_read_int()
{
// Setting a negative number to keep the loop going until correct data is entered.
int num = -1;
while (num < 0)
{
// Printing prompt.
printf("~> ");
// Reading the value.
scanf(" %d", &num);
// If the input is incorrect, the buffer needs to be cleared.
char c;
while ((c = getchar()) != '\n' && c != EOF) {}
}
return (unsigned int) num;
}
bool perform_task(unsigned int option, UserDatabase& database)
{
switch (option)
{
case 0: // Saving the database to a text file and exiting.
{
std::ofstream file("data.txt");
file.clear();
database.save(file);
file.close();
return false;
}
case 1: // Logging in.
login(database);
break;
case 2: // Registering a new user.
add_user(database);
break;
default:
puts("Unknown option, please enter it again.");
break;
}
return true;
}
void login(UserDatabase& database)
{
// Setting up username and password buffers.
char username[51];
char password[51];
// Reading username.
puts("Please enter your username.");
printf("~> ");
scanf(" %s", username);
// Reading password.
puts("Please enter your password.");
printf("~> ");
scanf(" %s", password);
// Logging in.
database.login(username, password);
}
void add_user(UserDatabase& database)
{
// Setting up buffers for login and account data.
char username[51];
char password[51];
char name[51];
char surname[51];
std::string age;
char favFood[51];
// Reading username.
puts("Please enter your username.");
printf("~> ");
scanf(" %s", username);
// Reading password.
puts("Please enter your password.");
printf("~> ");
scanf(" %s", password);
// Reading first name.
puts("Please enter your first name.");
printf("~> ");
scanf(" %s", name);
// Reading last name.
puts("Please enter your last name.");
printf("~> ");
scanf(" %s", surname);
// Reading age.
puts("Please enter your age.");
age = std::to_string(safe_read_int());
// Reading favourite food.
puts("Please enter your favourite food.");
printf("~> ");
scanf(" %s", favFood);
// Registering a new user.
database.add_user(username, password, name, surname, age, favFood);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment