A function is implemented to generate a cryptographically secure random salt for each password. This ensures that identical passwords produce different hashes, preventing rainbow table attacks and improving overall security.
Argon2id-Based Hashing Strategy
Passwords are combined with a secret pepper from environment variables and hashed using Argon2id. Specific parameters (iterations, memory usage, parallelism, key length) are chosen to balance security and performance based on recommended practices.
Encoding and Storage Format
The resulting hash and salt are base64-encoded and stored together as a colon-separated string. This allows both values to be persisted securely and later retrieved for verification.
Password Validation Mechanism
During authentication, the stored value is split to extract the hash and salt. The provided password is re-hashed using the original salt, and the results are compared using a constant-time comparison function to prevent timing attacks.
User Creation with Hashed Passwords
When creating a user, the plaintext password is replaced with the hashed version before being stored in the database. The pepper is configured via environment variables and kept secret from external systems.
Security Considerations and Next Steps
The approach protects against common attacks and minimizes damage in case of database breaches. Future improvements include adding middleware, rate limiting, login protection, and access control for admin routes.
Okay, let's actually hash our passwords so we don't store it as plain text. We are technically storing it as bytes, but it's very easy to revert that into a string that is human readable. So let's tackle this. We're gonna create a function here called generate salt that takes in a size of type int, then it returns us a byte or an error. In here, I need to close it. In here, we're gonna say salt.
make by slice and the size and then we say blank error equals to rent read and then the salt if there is no error then we simply return the error and null so there's no error between the salt and null pretty simple then let's create a hash
password function that takes in the password and a paper. This is a string, both of them, returns a string or an error. Then we generate the salt. Generate salt 16, the length is 16. Sorry, the size is 16. Then we say hash equals to argon to id key.
ID key, and let me just get the import in place. Then we add a byte slice where we take the password together with the pepper. We provide the salt. Then specify how many iterations we should use. We're going to use two. Then we specify the memory that we're going to say 19 times 1024.
You're gonna say parallelism should be one, and then finally the key length should be 32. Oh, and it's not pepper, it's, oh, come on. It's pepper, there we go. Then we say encoded hash equals to F and D, sprint. And we will say, we have,
write it with two values we need to provide, so we're going to provide the hash and the salt. So we say base64, raw, standard encoding, encode to string, write the hash here. Let me break this up a little bit because we need to do the same, but for the salt and
Finally, we can just return this encoded salt and null because there should be no errors at this point. So the first thing that happens is we generate a salt. That's a random value that's unique for each password so that we can technically have two users with the same password where the hashes would be completely different. We then combine the password with our pepper that is going to be coming from our environmental variables.
are going to ID to hash the password and the paper along with the salt. So we have the numbers, the two iteration, we use 19 megabytes of memory and we have a parallelism of one, which is the recommended parameters for the balance between security and performance. So we can increase these numbers and have even more security, but the function will take longer to generate. So we are trying to strike a balance here and we are just using the recommendations from the Copenhagen book for these value.
Finally, we encode both the hash and the salt using base64 so we can store them together and separate it by a colon. And we need to store the salt so we can later use it to validate the password that we provide whenever we try to log into the admin interface. Great. Now, let's actually create the functionality to validate that a password is correct for
for the user we create. So we create a method on the user struct here called valid. Let's say validate pass word. We take in a pass word and a pepper just like we do in the hash function. And then let's just return a Boolean or an error. So we could also call this probably
Valid password. Yeah, I think probably valid password fits better with the naming since we are training a Boolean. So we get back to a false. Yeah, let's go with, let's go with valid passwords. Now we have, we are storing this with a colon so we can split on that colon to get out the hash value. So we can say parts strings split.
strings, and then we grab the password that we have on the user, which we will get returned as a password. Sorry, as a bite slice. And we have the colon here that we can then separate on. And then we know that we should have two parts because that's the way we hash the password and store it. So we can say length parts does not equal two. We will turn
false and say errors new invalid stored. Or let's say password stored in invalid format, something like this. Then we say the expected hash error and use the base64 encoding, get a package again and say raw standard encoding.
decode a string from the parts slice and this string that's split here always returns us a slice of strings so we can grab the first element. Then we will again say if there is an error we return false and just the error. Then we decode the salt and say again base 64 raw encoding was done on encoding decode
string and then we say parts and grab the second element again return false error if there is an error and there we go finally we can say new hash and then we simply grab what we have here all right
and where we will be passing in the password from up here. We could technically make this more clever, so say, provided password. So that we do here, provided password. And then we just use this constant time compare, so we say return. Subtle, again our package from the standard library, constant.
What is a constant time compare? We pass in the new hash and we pass in the expected hash. That should be equal to one. And we are going to return null here. So we get the Boolean value from this part here that this simply compares these two byte slices and make sure to have equal contents and basically returns to one. Right. So.
We split the stored password that we have on the user model. So this will get filled out whenever we use the find buy write. Then we decode both the hash and the salt using the base64 package. We then hash the provided password using the same salt. And then we compare these two using the solid constant time compare function.
And it's really important the last part here where we use this constant time comparison to prevent timing attacks. So the user cannot learn information about the password based on how long the comparison takes. Great. And this is basically what we need for storing and validating that our password is the correct one, right? But we are still storing.
the password in plain text. So let's go down here. Now we have to create user and then let's say hashed password or error. And then we simply hash password, pass in the data, password, pair password. And then we also need a pepper. So let me add this here. I'm gonna add this
outside of the create user data, because this is something extra that we are passing in. It's not really related to create user data directly. So let's just make it another argument. Let me say paper. Return an empty user struct and the error. And then finally, we have the hashed password that is a string that we can now replace this with hash, hashed password.
and we are ready to create users and authenticate users. Now, we still need to go into config, config, and then in here, let's create a pepper string, env, pepper, give that a save, and then we simply just provide a random value to our env variable. So here in env, we go down, say, pepper equals,
And let me just grab a value we can use for this. So we will have something like, let's just grab a value I've prepared here. So this is just a random value that is set in our NVIDIA variables that only the application knows about that adds to it. So this was a consistent right throughout every password that we store in our database. But this is
It's now we can actually create the user. We can validate the password, and we can do it in a secure manner. So even if our database is breached, the password does not spill out now. Again, we're only going to be having the admin user for our own block write. But if you're going to be using this to have a lot of other users in your system, this is how you would do it.
Because this is stored securely, we are protecting against a lot of the known attacks. So the attack surface is very slim. And we don't get all the data leaked in case our database gets hacked. So validate the input, create the user struct. It has the password we inserted into the database. Then when we log in, we will simply start by grabbing
user on the database by the email, and then use the validated password on the model struct, model user struct, against what we received from the UI. And then we can say it's true or false if the password is correct or not. But we still need to have a middleware. So we need some middleware that runs and checks all the
against certain types of routes so we can access the admin routes without having a login setup or without an authenticated cookie set. We also need to do some rate limiting, unlock ins is another protection we need to have. So in the next free episode, we're going to be doing some middleware. We're going to be doing some rate limiting, unlock ins, and we're also going to be borrowing the access to admin based on our only middleware.