Hashes and Salts; what they are and the simple concept of it in Luau

Hashes and Salts. You may of heard of them, but you may not know what they are. So here I will be explaining to you what hashes and salts are.


Disclaimer: if you do not know what hashes or salts are, I recommend these 2 videos:
Hashes Explained and Salts explained.


The purpose of Hashes

Moving on, first we need to know what the purposes of hashes and salts are.

Lets say, You have an account system on your website. You will of course need to store your user’s passwords. But how should you do it?

That is where Hashes come to play. Their purpose is to convert strings into what may look like a random line of numbers and letters, a hash. But here is the catch. Hashes cannot be converted back to their original string.
So how can they be useful to store? You may ask. Well, when the User inputs their password, it would be hashed and then compared to the stored hash for that user. If the hashes are the same, then the password will be the same, allowing the user to enter.

The purpose of Salts

Salts are just a random string of letters and characters that are attached and unique to the user. An example salt would be: iaih124naow240124. Instead of hashing the password, it hashes the password+the salt. so a password like password123! would be password123!iaih124naow240124, which would make it longer, and thus more secure.


A basic diagram of how it works:


How to make it in Roblox:

DISCLAIMER

This is not a proper hashing algorithm, but just a cashe-backed string asigner. I will update this with a proper bit32-orientated algorithm into my SimpleBit Module.

As of making this, Roblox does not have a built-in hashing fuction. So here I will be showing you how to make a simple hash function.

Step 1: What to create
First, we will need to make a RemoteEvent object.
Lets parent it to ReplicatedStorage and name it: HashEvent

Step 2: Create the script
We will create a Script object.
It will be parented to ServerScriptService. Lets name it HashScript

Step3: Creating a hash system.
Unlike regular hashing algorithms, that usually have hundreds to thousands of line, we will be making a more simpler hashing system. Inputed strings are hashed and then their hash is stored.

To get the first things out of the way. We need to add some variables.

local HashFunction = game.ReplicatedStorage.HashEvent -- The Event object that we will use to connect to the client.

local StoredHashes = {} -- This table stores all of the hashes.
local HashLengh = 5 -- how long our hash will be in terms of characters

Next, we would need a function to generate hashes.


local function GenerateHash(player:Player,Input:string) -- this will generate a random hash we can assign to our string
	local LegalCharacters = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz123456789" -- the characters that our hash will consist of
	local Hash = ""
	
	for HashStepsDone=1,HashLengh do 
		local RandomNumber = math.random(1,LegalCharacters) -- creates a random number for a character
		local RandomCharacter = string.sub(LegalCharacters,RandomNumber,RandomNumber) -- gets the character
		Hash..=RandomCharacter -- adds the random character onto our hash
	end
	if not HashLengh[Hash] then
		HashLengh[Hash] = Input
		HashEvent:FireClient(player,Hash)
	else
		GenerateHash(Input)
	end
end

Just incase there are any errors, we will pcall the function.

local LegalCharacters = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz123456789"

A list of characters that the hash can consist of.

for HashStepsDone=1,HashLengh do 
		local RandomNumber = math.random(1,#LegalCharacters) -- creates a random number for a character
		local RandomCharacter = string.sub(LegalCharacters,RandomNumber,RandomNumber) -- gets the character
		Hash..=RandomCharacter -- adds the random character onto our hash
	end

Adds a specified amount of random character from the list of characters to the hash.

if not StoredHashes[Hash] then
			HashLengh[Hash] = Input
			HashEvent:FireClient(player,Hash)
		else
			GenerateHash(Input)
		end

Checks if there is already a value with the same hash.

This may look good already, but there is one thing that we need to add.
You see, every time a hash is added, even if the value is same, it will still make a seperate hash. To combat this, we will add this loop:

for hash, stringValue in pairs(StoredHashes) do -- checks if there is already a hash for that object
			task.wait()
			if stringValue == Input then
				HashEvent:FireClient(player,hash)
			end
		end

It goes through the stored hashes and checks if there is a hash with the same value. And if there is, it returns that hash.

We would also have to modify our hash checker, to check if the same hash has the same value.

if not StoredHashes[Hash] then
			if StoredHashes[Hash] == Input then
			HashEvent:FireClient(player,Hash)
end
		else
			if StoredHashes[Hash] == Input then
				HashEvent:FireClient(player,Hash)
			else
				GenerateHash(Input)
			end
		end

And of course, we would need to connect our function to the RemoteEvent
HashEvent.OnServerEvent:Connect(GenerateHash)

Output example:
"abc123""8ph7v"

Entire script
local HashEvent = game.ReplicatedStorage.HashEvent -- The function object that we will use to connect to the client.

local StoredHashes = {} -- This table stores all of the hashes.
local HashLengh = 5 -- how long our hash will be in terms of characters

function GenerateHash(player:Player,Input:string) -- this will generate a random hash we can assign to our string
	local success, errorMessage = pcall(function()
		local LegalCharacters = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz123456789" -- the characters that our hash will consist of
		local Hash = ""

		for hash, stringValue in pairs(StoredHashes) do -- checks if there is already a hash for that object
			task.wait()
			if stringValue == Input then
				HashEvent:FireClient(player,hash)
			end
		end
		for HashStepsDone=1,HashLengh do 
			local RandomNumber = math.random(1,#LegalCharacters) -- creates a random number for a character
			local RandomCharacter = string.sub(LegalCharacters,RandomNumber,RandomNumber) -- gets the character
			Hash..=RandomCharacter -- adds the random character onto our hash
		end
		if not StoredHashes[Hash] then
			StoredHashes[Hash] = Input
			HashEvent:FireClient(player,Hash)
		else
			if StoredHashes[Hash] == Input then
				HashEvent:FireClient(player,Hash)
			else
				GenerateHash(player,Input)
			end
		end
	end)
	if not success then warn(errorMessage) end
end

HashEvent.OnServerEvent:Connect(GenerateHash)

REMEMBER!

Like I said, this is a cashe-backed up string assigner and should NOT be used for security.

It is mainly just to showcase what simple Hashes would look like.
I will be making a new updated tutorial on a proper hashing algorithm that would work on the lua Bit32 libary.


19 Likes

Completely forgot to mention Peppers. They are not that important but I will explain them anyways.

It is a string of randomness, just like a salt, but instead of being stored with the user’s data, it is stored in a seperate, secure memory.

I honestly don’t know why they are called salts and peppers, but I guess it is because it adds some flavour or texture to the data, like what salt and pepper does to food.

A pepper isn’t unique per user aswell. The entire website/program usually has a same pepper that is not stored on the database. But they usually, in structure, look like a salt.

Here is an example:

Password: Password123
Salt: adoi14nhea021
Pepper: 24waji241jbfjaio4js5o

What is actually hashed: Password123adoi14nhea02124waji241jbfjaio4js5o

What the hacker sees:

User: AUserNameICantThinkOfYet
Password: 249fhiadi31w0edhah9wj9wajd (meant to be a hash)
Salt: adoi14nhea021

As you can see, the hacker has no access to the pepper. So the only option is to guess randomly what the pepper is.

Cool! But how would you decode it? No use of doing it without knowing how to decode.

The point is that you can’t decode it, otherwise in the case of a breach, hackers could use the decode method and get access to accounts

3 Likes

Hashing to its core meaning is just encription that can’t be descripted. If you are storing something that needs to be descripted, then it would need to be encripted, not hashed.

Instead of decoding. The data sent, for example, passwords, are hashed, then if the hash is the same as the one stored in the database, the password would be correct.

2 Likes

I have fixed a flaw by adding a debounce and break.
Basically, the script would return 3 or 4 calls if the string already had a hash, it is meant to return 1.
Here is the modified script:

local HashEvent = game.ReplicatedStorage.HashEvent -- The function object that we will use to connect to the client.

local StoredHashes = {} -- This table stores all of the hashes.
local HashLengh = 10 -- how long our hash will be in terms of characters

function GenerateHash(player:Player,Input:string) -- this will generate a random hash we can assign to our string
	local success, errorMessage = pcall(function()
		local LegalCharacters = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz123456789" -- the characters that our hash will consist of
		local Hash = ""
		local Complete = false

		for hash, stringValue in pairs(StoredHashes) do -- checks if there is already a hash for that object
			task.wait()
			if stringValue == Input then
				HashEvent:FireClient(player,hash)
				Complete = true
				break
			end
		end
		if Complete == false then
			for HashStepsDone=1,HashLengh do 
				local RandomNumber = math.random(1,#LegalCharacters) -- creates a random number for a character
				local RandomCharacter = string.sub(LegalCharacters,RandomNumber,RandomNumber) -- gets the character
				Hash..=RandomCharacter -- adds the random character onto our hash
			end
			if not StoredHashes[Hash] then
				StoredHashes[Hash] = Input
				HashEvent:FireClient(player,Hash)
			else
				if StoredHashes[Hash] == Input then
					HashEvent:FireClient(player,Hash)
				else
					GenerateHash(player,Input)
				end
			end
		end
	end)
	if not success then warn(errorMessage) end
end

HashEvent.OnServerEvent:Connect(GenerateHash)

The Complete variable is the debounce variable if you haven’t noticed.

2 Likes

I think this is useless on roblox remotes because hackes can do remote spy thing , so he should know ours password

What is this hash’s collision resistance? (How hard is it to craft an input that matches a desired output?)

Sorry for bumping, but, this post is largely misinformation. Cryptography is a complicated field but it is extremely important. Your implementation of a “hash function” in Lua is nothing more than a fancy random-string generator. In other words, your example was very silly and actually made no contribution to security, especially when dealing with sensitive passwords.

What you got correct is that hashing greatly differs from encryption. Hashing is a one way function (which cannot be reversed) whilst encryption is a two way function (designed to be encrypted AND decrypted). By design, hashes are designed so that the same input will always produce the same output (known as the digest). Depending on which hashing function you’re using, your digest will always be a fixed length.

Also, your description of salts is slightly off. While yes, salts are appended to the input (a password in this case) to hashed, they aren’t there to make the password itself longer. The main purpose of salts is to defend against repeating password hashes. When you add a random salt, you’re ensuring that two different users ultimately have different password hashes, even if they are using the same password. Salts also help to safeguard against password-cracking attacks using structures known as “rainbow tables”. A rainbow table is a big list of hashes from commonly used passwords (like “qwerty” and “password”).

An example of how you would actually go about creating hashes (and salts) for passwords is as follows:

  1. Prompt the player to create a password. Send this password to the server.
  2. Create some random data. This will be your salt. Append that to the password.
  3. Hash the password and salt together using a function like SHA-256 (There’s several options, just make sure you follow the guidelines set by NIST FIPS 180-4 and NIST FIPS 202.
  4. Store the UserId, hash digest, and salt together using things like DataStoreService.

An example of how you would actually use hashes after creating them is as follows:

  1. Prompt the player to enter their password. Send their response to the server.
  2. Go back to where you stored the player’s hash digest and salt.
  3. Append the salt you stored before to the password the player provided.
  4. Using your appended password + salt, run the hash function you chose before (like SHA-256).
  5. Compare the output (digest) of this with the one that matches DataStore records. If they match, you know the password is correct. Otherwise, the password provided was incorrect.

A couple things to note:

  • Stay away from things like MD5 and SHA-1. The earliest versions of SHA (which stands for Secure Hashing Algorithm) was designed by the US National Security Agency a very long time ago. These are no longer considered secure by today’s processing power.
  • There are extremely rare edge cases where two different inputs can return the same hash. This is known as a collision. Again, this is extremely rare and you don’t really need to worry about this much with modern algorithms.
  • Depending on where you are working with hashes, you don’t want to send anything out as plaintext (like the passwords to hash). This is because someone else can “sniff” that data in an attack known as a man-in-the-middle (MITM) attack. I don’t think this is a concern within Roblox, but it’s definitely important when working with websites. This is solved when you use HTTPS (originally SSL over HTTP but is usually TLS over HTTP now).

Feel free to checkout @boatbomber’s HashLib module. His implementation features over a dozen different algorithms and is really optimized.

Hopefully this was all helpful! Cryptography is a super cool field and I hope it’s something you continue to dive into.

12 Likes

See my main reply to OP. He hasn’t actually shown a hash- it’s just a random text generator.

I figured as such but I didn’t spend too long actually trying to understand the code. I thought there was something weird going on like the random generation being just for salting/etc, and as long as it was consistent throughout the game state then hashes generated during one server would work correctly.

But part of the reason why I asked about collision resistance was to catch OP in the act if they actually had 0 idea what they were doing trying to make a hash function.

Like all systems, this one has a flaw. The hashes will vary per server, so storing hashed server data would mean that each hash would have to be stored in DataStoreService for this system to work.

This was the big red flag I saw that prompted me to correct OP at 1am.

1 Like

Looking closer at the OP’s explanation this isn’t hashing at all. It’s literally a UUID storage. Whenever an object doesn’t have a UUID it generates one and caches it so that object gets the same UUID in the future.

1 Like

Yes I know. I will update with a proper bit32-orientated hashing algorithm on my SimpleBit module.

Oh, so you’re the SimpleBit dude. That explains a lot.

You keep mentioning this. SimpleBit is not a math module. The 32Bit Adder was just an addon to experiment with and misconsceptions were understood.

If you are going to keep mentioning the module, atleast look fully into it instead of just some functions in it meant for experiments.

I’d like to add out that you could just send the server the prompted password and let the server check it. This is called sanity checks and are way better.

Both of these technically aren’t correct, they can be converted back, but it would take forever. Also I’m not too experienced with hashes and stuff but your function doesn’t look like a hash but rather a random string generator? Hashes seem to use algorithms instead.

So you can’t say it’s not correct, that’s basically saying something you don’t know yourself.

The only way to ‘convert’ hashes are through brute force attacks, aka hashing every possible combination of characters and checking if the hashes match

Yes, it is more to get the basic concept that hashes are a set lenght and cannot be decoded.

And like I mentioned here, I will work on a Bit32-orientated hashing algorithm instead of a cashe-backed string assigner that the basic concept was.