How to hash users passwords? - c#

please do not mark this as a duplicate because I've already checked multiple posts but none of them helped me.
Asp.net MVC - How to hash password
Hash and salt passwords in C#
I'm building a website that has a login page, and I read this post
https://www.codeproject.com/Articles/704865/Salted-Password-Hashing-Doing-it-Right#normalhashing
on how to make my login secure.
Now I understand that on each sign up I have to create CSPRNGs(Cryptographically secure pseudorandom number generator)
and add it to the password submitted by the user, then hash this mix and store it in the DB, and store the salt for each user because it's a random one for each user. My question is what is the easiest and the most simple way to do that.
In my controller I'm taking the username and the password via this method
public JsonResult Login(LoginModel data)
{
//some code...
//...
}
My login model contain
public class LoginModel
{
public string Username { get; set; }
public string Password { get; set; }
}
so I'm getting the username and password like (string a = data.Username, ...)
I already added this to my project but I don't know how to continue from here
using System.Security.Cryptography;
I know how to save in the DB for sure I just want to know, how to make a random salt (maybe using RNGCryptoServiceProvider), and how to add it to the user's password, and then hash the final result.
Thanks for any help.

The answer to every "How do I securely hash passwords" question is the same: YOU DON'T. You're in the business of making unique websites, not in the security business. You don't know what you're doing, and you won't recognize if you're doing something wrong. Until your database leaks out in one way or another, and then you're too late to do anything about it, compromising your users' security.
You use well-researched, battle-hardened, maintained code to store user logins. When you're on ASP.NET MVC, you use ASP.NET Identity. You DO NOT roll your own. Period.

You could go about adding salt and hashing the password by using a method such as this. Here we send a method a string. This is basically how entity framework hash's a password when you create a user using the login section.
public static string EncodePassword(string password)
{
byte[] salt;
byte[] buffer2;
if (password == null)
{
throw new ArgumentNullException("password");
}
using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, 0x10, 0x3e8))
{
salt = bytes.Salt;
buffer2 = bytes.GetBytes(0x20);
}
byte[] dst = new byte[0x31];
Buffer.BlockCopy(salt, 0, dst, 1, 0x10);
Buffer.BlockCopy(buffer2, 0, dst, 0x11, 0x20);
return Convert.ToBase64String(dst);
}//Entity Framework default way
The above uses
using System.Security.Cryptography; and system.Buffer

I did this way, and it's working fine.
I added a new class
public class Helper
{
public String CreateSalt(int size)
{
var rng = new RNGCryptoServiceProvider();
var buff = new byte[size];
rng.GetBytes(buff);
return Convert.ToBase64String(buff);
}
public String GenerateSha256Hash(string input, string salt)
{
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(input + salt);
SHA256Managed shaString = new SHA256Managed();
byte[] hash = shaString.ComputeHash(bytes);
return BitConverter.ToString(hash);
}
}
}
And in my controller I added an instance of this class
Helper myHelper = new Helper();
Then I'm passing values to
string salt = myHelper.CreateSalt(5);
string hashedPassword = myHelper.GenerateSha256Hash(*PasswordField*, salt);
hashedPassword = hashedPassword.Replace("-", string.Empty).Substring(0, 16);
I'm removing the "-"from the hashed password and then I'm taking the first 16 characters.
Thanks to #Luke Joshua Park, #gusto2 , #JamesS and #CodeCaster for their help.

Related

How do I sign with HashiCorp Vault

i don't know if this question is very easy and I just didn't figure it out how to sign with HashiCorp-VaultĀ“s Api VaultSharp, but I am despairing.
The entire Documentation with examples can be found here: https://github.com/rajanadar/VaultSharp
Encryption and Decryption works fine. Only Signing is a problem.
Code for Encryption:
public byte[] EncryptData(byte[] data, string keyName)
{
SecretsEngine transitSecretsEngine = new SecretsEngine
{
Type = SecretsEngineType.Transit,
Path = path
};
Client.V1.System.MountSecretBackendAsync(transitSecretsEngine).Wait();
Client.V1.Secrets.Transit.CreateEncryptionKeyAsync(keyName, new CreateKeyRequestOptions()
{
Exportable = true
}, path).Wait();
EncryptRequestOptions encryptOptions = new EncryptRequestOptions
{
Base64EncodedPlainText = Convert.ToBase64String(data),
ConvergentEncryption = true,
};
Secret<EncryptionResponse> encryptionResponse = Client.V1.Secrets.Transit.EncryptAsync(keyName,
encryptOptions, path).Result;
string cipherText = encryptionResponse.Data.CipherText;
return Encoding.Unicode.GetBytes(cipherText);
}
Code for Decryption:
public byte[] DecryptData(string ciphertext, string keyName)
{
DecryptRequestOptions decryptOptions = new DecryptRequestOptions
{
CipherText = ciphertext,
};
Secret<DecryptionResponse> decryptionResponse = Client.V1.Secrets.Transit.DecryptAsync(keyName,
decryptOptions, path).Result;
return Convert.FromBase64String(decryptionResponse.Data.Base64EncodedPlainText);
}
Here is my Code Trial for signing:
public byte[] Sign(byte[] plaintextBytes, string keyName)
{
byte[] hash = ComputeHash(plaintextBytes,SHA256.Create());
GCKMS.SignatureOptions options = new GCKMS.SignatureOptions()
{
Digest = Convert.ToBase64String(hash),
};
Secret<GCKMS.SignatureResponse> result = Client.V1.Secrets.GoogleCloudKMS.SignAsync(keyName,
options).Result;
return Encoding.Unicode.GetBytes(result.Data.Signature);
}
The Error is:
VaultSharp.Core.VaultApiException: {"errors":["no handler for route
'gcpkms/sign/Manuel'"]}
Last but not least my Code for validating the signature:
public bool ValidateSignature(byte[] plaintextByte, byte[] signature, string keyName)
{
GCKMS.VerificationOptions option = new GCKMS.VerificationOptions
{
Digest = Encoding.Unicode.GetString(ComputeHash(plaintextByte)),
Signature = Encoding.Unicode.GetString(signature)
};
Secret<GCKMS.VerificationResponse> result =
Client.V1.Secrets.GoogleCloudKMS.VerifyAsync(keyName, option).Result;
return result.Data.Valid;
}
I am not sure but this could be because I don't use a SecretsEngine with a Path. I could not find any SecretsEngine for GoogleCloudKms.
Useful information:
I generate the Path with Guid.NewGuid().ToString();.
ComputeHash is a self written Function that computes the Hash with a give Algorithm. The
default algorithm is SHA256.
GCMS is a short version of the Namespace VaultSharp.V1.SecretsEngines.GoogleCloudKMS
Any ideas and suggestions are very welcome.
Thanks in advance!
Although Vault offers convenient signature with Transit, the C# wrapper you are using does not support it.
Google KMS does offer signature, but its interface is more complex: you have to do the hash yourself and keep track of the key versions.
What I suggest is that you play a trick on your API wrapper:
Leave your encryption and decryption code as-is
Write to the the Transit backend as if it was a KV store version 1
Get your signature by sending your payload as the input parameter
You still have to base64 your data before sending it to Vault, to avoid binary encoding issues.
So assuming that:
You want to sign the text StackOverflow
The transit back-end is mounted under transit
Your signature key is named my-key
This should get you started:
var value = new Dictionary<string, object> { "input", Convert.ToBase64String(Encoding.UTF8.GetBytes("StackOverflow")) } };
var writtenValue = await vaultClient.V1.Secrets.KeyValue.V1.WriteSecretAsync("sign/my-key", value, "transit");

Invalid length for a Base-64 char array or string : C#

I am new to C# ,Here in my web API project I have some code for hashing the user password using SHA3 .
In API I have 2 methods
First method is used for when new User create an account I just hashing the password and store it in the table.
The second method is for when the same user log In again I fetch the hashed password(string) and verify the current password with it .
Here I have faced the error as I mentioned in the title and I referred some related post in SO also but I could't solve this issue .
I have verified that the stored and fetched string has the same characters also the generated and stored string has the same characters.
I don't know where I did the mistakes .
Password : Abcd#123
Hashed String : k/OMmdnW6FZ+zsOrE2rkdy8YEUH/rep5dlcRIwnG8Vc7kQ81VL8dEQv2Clyp7iRhb0HSfKtgOLBj5g/YbqHq7FKDj5epafNwasE=
Calling Confirm method
bool isPasswordPassed = false;
if (mHashedPassword != " " && mUserPassword != " ")
{
isPasswordPassed = Hashing.Confirm(mUserPassword, mHashedPassword, Supported_HA.SHA512);
}
Confirm
public static bool Confirm(string plainText, string hashValue, Supported_HA hash)
{
byte[] hashBytes = Convert.FromBase64String(hashValue);//This line passing the error as in my title.
......
.......
.....
}
But It works fine when I check the code like this ...
check(mUserPassword){
string a = Hashing.ComputeHash(mUserPassword, Supported_HA.SHA512, null);
bool b = Hashing.Confirm(mUserPassword, a, Supported_HA.SHA512);
}
Here I am passing the password to generate hash and the confirm hash but it returns TRUE
Can anyone help me to solve this .
Reference : https://www.youtube.com/watch?v=0dgTf9TUDHU
It may be an encoding conversion Base64 Unicode. you should pass the encoding
var plainTextBytes = System.Text.Encoding.Unicode.GetBytes(plainText);
string hashValue = Convert.ToBase64String(plainTextBytes);
then in the Confirm method
byte[] hashBytes = Convert.FromBase64String(hashValue);
should work. Regards

Asp.net MVC - How to hash password

How do I hash an users input(password) to database and then later read the hashed password during login?
I believe the solution is to hash the password upon register, where the password is saved as hashed inside db. Later upon Login, it should un-hash and compare its password with users password-input.
But I don't know how to do it.
I allowed password to have nvarchar(MAX)in db since hashed password are usually long.
[Required]
[StringLength(MAX, MinimumLength = 3, ErrorMessage = "min 3, max 50 letters")]
public string Password { get; set; }
Register:
[HttpPost]
public ActionResult Register(User user) {
if (ModelState.IsValid) {
var u = new User {
UserName = user.UserName,
Password = user.Password
};
db.Users.Add(u);
db.SaveChanges();
return RedirectToAction("Login");
}
}return View();
}
Login:
public ActionResult Login() {
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(User u) {
if (ModelState.IsValid)
{
using (UserEntities db = new UserEntities()) {
//un-hash password?
var v = db.Users.Where(a => a.UserName.Equals(u.UserName) && a.Password.Equals(u.Password)).FirstOrDefault();
if (v != null) {
return RedirectToAction("Index", "Home"); //after login
}
}
}return View(u);
}
I'm using database first.
You should never need to unhash a password. A cryptographic hash function is supposed to be a one-way operation.
(And that's precisely why it is called hashing and not encrypting. If unhashing passwords was to be a normal procedure in your flow of operations, then it would not be hashing and unhashing, it would be encrypting and decrypting. So, hashing is a different thing from encryption, precisely because unhashing is not supposed to ever happen.)
Hashing provides security, because nobody can steal your user's passwords even if they manage to view the contents of your database.
When the user registers, compute the hash of their password, store the hash in the database, and forget the password forever.
When the user logs in, compute the hash of the password they entered, (forget that password too,) and see if the hash matches the hash stored in the database.
This is the mechanism used by most websites out there, and that's precisely why if you successfully go through the "I forgot my password" procedure, they will still not show you your password: they don't have it; they cannot retrieve it even if they wanted to. Instead, they send you a password reset link.
As for how to compute a hash from a string, the interwebz abound with answers to that question, for example: MD5 (MSDN); SHA-256 (MSDN); SHA-512 (MSDN)
Use the System.Web.Helpers.Crypto NuGet package from Microsoft.
You hash a password like this:
var hash = Crypto.HashPassword("foo");
You verify a password like this:
var verified = Crypto.VerifyHashedPassword(hash, "foo");
When it comes to security don't try to reinvent the wheel. Use Claims based authentication.
If you still must manage usernames and passwords use Hash-based message authentication code (HMAC)
I would also recommend investing sometime and reading Enterprise Security Best Practices. There are already smarter people who solved this problems why reinvent the wheel. And .NET has all the goodies there.
Example below:
using System.Security.Cryptography;
using System.Text;
//--------------------MyHmac.cs-------------------
public static class MyHmac
{
private const int SaltSize = 32;
public static byte[] GenerateSalt()
{
using (var rng = new RNGCryptoServiceProvider())
{
var randomNumber = new byte[SaltSize];
rng.GetBytes(randomNumber);
return randomNumber;
}
}
public static byte[] ComputeHMAC_SHA256(byte[] data, byte[] salt)
{
using (var hmac = new HMACSHA256(salt))
{
return hmac.ComputeHash(data);
}
}
}
//-------------------Program.cs---------------------------
string orgMsg = "Original Message";
string otherMsg = "Other Message";
Console.WriteLine("HMAC SHA256 Demo in .NET");
Console.WriteLine("----------------------");
Console.WriteLine();
var salt = MyHmac.GenerateSalt();
var hmac1 = MyHmac.ComputeHMAC_SHA256(Encoding.UTF8.GetBytes(orgMsg), salt);
var hmac2 = MyHmac.ComputeHMAC_SHA256(Encoding.UTF8.GetBytes(otherMsg), salt);
Console.WriteLine("Original Message Hash:{0}", Convert.ToBase64String(hmac1));
Console.WriteLine("Other Message Hash:{0}", Convert.ToBase64String(hmac2));
NOTE: Salts do not have to be kept secret and can be stored alongside the hash itself. It's to increase security from rainbow table attack.
Please don't post same question twice. Duplicate from here.

Password hash and verification don't match

I'm writing custom forms authentication for ASP.NET MVC 5 (no, I don't want to use ASP.NET Identity). I'm trying to hash my passwords using a randomly-generated salt and then hashing salt+password using SHA512. Here are the methods I've written:
private static User SetPassword(User newUser, string password)
{
var rand = RNGCryptoServiceProvider.Create();
var saltBytes = new byte[128];
var passwordBytes = Encoding.UTF8.GetBytes(password);
rand.GetNonZeroBytes(saltBytes);
var passHash = SHA512Managed.Create().ComputeHash(saltBytes.Concat(passwordBytes).ToArray());
var hash = Encoding.UTF8.GetString(passHash);
var salt = Encoding.UTF8.GetString(saltBytes);
newUser.PasswordHash = hash;
newUser.Salt = salt;
return newUser;
}
private static bool ValidatePassword(User user, string passwordTry)
{
var actualPasswordBytes = Encoding.UTF8.GetBytes(user.PasswordHash);
var passwordTryBytes = Encoding.UTF8.GetBytes(passwordTry);
var saltBytes = Encoding.UTF8.GetBytes(user.Salt);
var passwordTryHashBytes = SHA512Managed.Create().ComputeHash(saltBytes.Concat(passwordTryBytes).ToArray());
if (passwordTryHashBytes == actualPasswordBytes)
{
return true;
}
return false;
}
If I step through the code, the registration (SetPassword()) method appears to work successfully. The user's record gets set with a UTF-8 encoded password hash and salt.
If I step through the password validation method, everything also appears to be operating normally. The user's record (the salt and hash) are checked against the password try.
The problem is, when I register with a password and then try to log in as that user, the password validation fails. I'm probably not understanding how one of the Cryptography classes works... can anyone explain why this doesn't work?
The following code will always fail because both objects do not point to the same reference:
if (passwordTryHashBytes == actualPasswordBytes)
Try using LINQ's SequenceEqual() method
passwordTryHashBytes.SequenceEqual(actualPasswordBytes)

How to create a password reset link?

Which way would you suggest to create a secure password reset link in MVC and C#? I mean, I'll create a random token, right? How do I encode it before to sending to user? Is MD5 good enough? Do you know any other secure way?
I mean, I'll create a random token, right?
There are two approaches:
Using a cryptographically secure random series of bytes, which are saved to the database (optionally hashed too) and also sent to the user by e-mail.
The disadvantage to this approach is you need to extend your database design (schema) to have a column to store this data. You should also store the UTC date+time the bytes were generated in order to have the password reset code expire.
Another disadvantage (or an advantage) is that a user can only have at-most 1 pending password-reset.
Using a private key to sign a HMAC message containing minimal details needed to reset the user's password, and this message can include an expiry date+time as well.
This approach avoids needing to store anything in your database, but it also means you cannot revoke any validly-generated password-reset code, which is why it's important to use a short expiry time (about 5 minutes, I reckon).
You could store revocation information in the database (as well as preventing multiple pending password-resets) but this removes all of the advantages of the stateless nature of signed HMACs for authentication.
Approach 1: Cryptographically secure random password reset code
Use System.Security.Cryptography.RandomNumberGenerator which is a cryptographically-secure RNG.
Don't use System.Random, it isn't cryptographically secure.
Use it to generate random bytes and then convert those bytes to human-readable characters that will survive e-mail and being copied and pasted around (i.e. by using Base16 or Base64 encoding).
Then store those same random bytes (or a hash of them, though this doesn't aid security all that much).
And just include that Base16 or Base64 string in the email.
You could have a single clickable link in the email which includes the password reset code in the querystring, however doing so violates HTTP's guidelines on what a GET request should be capable of (as clicking a link is always a GET request, but GET requests should not cause state-changes in persisted data, only POST, PUT, and PATCH requests should do that - which necessitates having the user manually copy the code and submit a POST web-form - which isn't the best user-experience.
Actually, a better approach is to have that link open a page with the password reset code in the querystring, and then that page still has a <form method="POST"> but it's to submit the user's new password, instead of pregenerating a new password for them - thus not violating HTTP's guidelines as no change-of-state is made until the final POST with the new password.
Like so:
Extend your databases' Users table to include columns for the password-reset code:
ALTER TABLE dbo.Users ADD
PasswordResetCode binary(12) NULL,
PasswordResetStart datetime2(7) NULL;
Do something like this in your web application's code:
[HttpGet]
[HttpHead]
public IActionResult GetPasswordResetForm()
{
// Return a <form> allowing the user to confirm they want to reset their password, which POSTs to the action below.
}
static readonly TimeSpan _passwordResetExpiry = TimeSpan.FromMinutes( 5 );
[HttpPost]
public IActionResult SendPasswordResetCode()
{
// 1. Get a cryptographically secure random number:
// using System.Security.Cryptography;
Byte[] bytes;
String bytesBase64Url; // NOTE: This is Base64Url-encoded, not Base64-encoded, so it is safe to use this in a URL, but be sure to convert it to Base64 first when decoding it.
using( RandomNumberGenerator rng = new RandomNumberGenerator() ) {
bytes = new Byte[12]; // Use a multiple of 3 (e.g. 3, 6, 12) to prevent output with trailing padding '=' characters in Base64).
rng.GetBytes( bytes );
// The `.Replace()` methods convert the Base64 string returned from `ToBase64String` to Base64Url.
bytesBase64Url = Convert.ToBase64String( bytes ).Replace( '+', '-' ).Replace( '/', '_' );
}
// 2. Update the user's database row:
using( SqlConnection c = new SqlConnection( CONNECTION_STRING ) )
using( SqlCommand cmd = c.CreateCommand() )
{
cmd.CommandText = "UPDATE dbo.Users SET PasswordResetCode = #code, PasswordResetStart = SYSUTCDATETIME() WHERE UserId = #userId";
SqlParameter pCode = cmd.Parameters.Add( cmd.CreateParameter() );
pCode.ParameterName = "#code";
pCode.SqlDbType = SqlDbType.Binary;
pCode.Value = bytes;
SqlParameter pUserId = cmd.Parameters.Add( cmd.CreateParameter() );
pCode.ParameterName = "#userId";
pCode.SqlDbType = SqlDbType.Int;
pCode.Value = userId;
cmd.ExecuteNonQuery();
}
// 3. Send the email:
{
const String fmt = #"Greetings {0},
I am Ziltoid... the omniscient.
I have come from far across the omniverse.
You shall fetch me your universe's ultimate cup of coffee... uh... I mean, you can reset your password at {1}
You have {2:N0} Earth minutes,
Make it perfect!";
// e.g. "https://example.com/ResetPassword/123/ABCDEF"
String link = "https://example.com/" + this.Url.Action(
controller: nameof(PasswordResetController),
action: nameof(this.ResetPassword),
params: new { userId = userId, codeBase64 = bytesBase64Url }
);
String body = String.Format( CultureInfo.InvariantCulture, fmt, userName, link, _passwordResetExpiry.TotalMinutes );
this.emailService.SendEmail( user.Email, subject: "Password reset link", body );
}
}
[HttpGet( "/PasswordReset/ResetPassword/{userId}/{codeBase64Url}" )]
public IActionResult ResetPassword( Int32 userId, String codeBase64Url )
{
// Lookup the user and see if they have a password reset pending that also matches the code:
String codeBase64 = codeBase64Url.Replace( '-', '+' ).Replace( '_', '/' );
Byte[] providedCode = Convert.FromBase64String( codeBase64 );
if( providedCode.Length != 12 ) return this.BadRequest( "Invalid code." );
using( SqlConnection c = new SqlConnection( CONNECTION_STRING ) )
using( SqlCommand cmd = c.CreateCommand() )
{
cmd.CommandText = "SELECT UserId, PasswordResetCode, PasswordResetStart FROM dbo.Users SET WHERE UserId = #userId";
SqlParameter pUserId = cmd.Parameters.Add( cmd.CreateParameter() );
pCode.ParameterName = "#userId";
pCode.SqlDbType = SqlDbType.Int;
pCode.Value = userId;
using( SqlDataReader rdr = cmd.ExecuteReader() )
{
if( !rdr.Read() )
{
// UserId doesn't exist in the database.
return this.NotFound( "The UserId is invalid." );
}
if( rdr.IsDBNull( 1 ) || rdr.IsDBNull( 2 ) )
{
return this.Conflict( "There is no pending password reset." );
}
Byte[] expectedCode = rdr.GetBytes( 1 );
DateTime? start = rdr.GetDateTime( 2 );
if( !Enumerable.SequenceEqual( providedCode, expectedCode ) )
{
return this.BadRequest( "Incorrect code." );
}
// Now return a new form (with the same password reset code) which allows the user to POST their new desired password to the `SetNewPassword` action` below.
}
}
[HttpPost( "/PasswordReset/ResetPassword/{userId}/{codeBase64}" )]
public IActionResult SetNewPassword( Int32 userId, String codeBase64, [FromForm] String newPassword, [FromForm] String confirmNewPassword )
{
// 1. Use the same code as above to verify `userId` and `codeBase64`, and that `PasswordResetStart` was less than 5 minutes (or `_passwordResetExpiry`) ago.
// 2. Validate that `newPassword` and `confirmNewPassword` are the same.
// 3. Reset `dbo.Users.Password` by hashing `newPassword`, and clear `PasswordResetCode` and `PasswordResetStart`
// 4. Send the user a confirmatory e-mail informing them that their password was reset, consider including the current request's IP address and user-agent info in that e-mail message as well.
// 5. And then perform a HTTP 303 redirect to the login page - or issue a new session token cookie and redirect them to the home-page.
}
}
Approach 2: HMAC code
This approach requires no changes to your database nor to persist new state, but it does require you to understand how HMAC works.
Basically it's a short structured message (rather than being random unpredictable bytes) that contains enough information to allow the system to identify the user whose password should be reset, including an expiry timestamp - to prevent forgery this message is cryptographically-signed with a private-key known only to your application code: this prevents attackers from generating their own password reset codes (which obviously wouldn't be good!).
Here's how you can generate a HMAC code for password reset, as well as how to verify it:
private static readonly Byte[] _privateKey = new Byte[] { 0xDE, 0xAD, 0xBE, 0xEF }; // NOTE: You should use a private-key that's a LOT longer than just 4 bytes.
private static readonly TimeSpan _passwordResetExpiry = TimeSpan.FromMinutes( 5 );
private const Byte _version = 1; // Increment this whenever the structure of the message changes.
public static String CreatePasswordResetHmacCode( Int32 userId )
{
Byte[] message = Enumerable.Empty<Byte>()
.Append( _version )
.Concat( BitConverter.GetBytes( userId ) )
.Concat( BitConverter.GetBytes( DateTime.UtcNow.ToBinary() ) )
.ToArray();
using( HMACSHA256 hmacSha256 = new HMACSHA256( key: _privateKey ) )
{
Byte[] hash = hmacSha256.ComputeHash( buffer: message, offset: 0, count: message.Length );
Byte[] outputMessage = message.Concat( hash ).ToArray();
String outputCodeB64 = Convert.ToBase64( outputMessage );
String outputCode = outputCodeB64.Replace( '+', '-' ).Replace( '/', '_' );
return outputCode;
}
}
public static Boolean VerifyPasswordResetHmacCode( String codeBase64Url, out Int32 userId )
{
String base64 = codeBase64Url.Replace( '-', '+' ).Replace( '_', '/' );
Byte[] message = Convert.FromBase64String( base64 );
Byte version = message[0];
if( version < _version ) return false;
userId = BitConverter.ToInt32( message, startIndex: 1 ); // Reads bytes message[1,2,3,4]
Int64 createdUtcBinary = BitConverter.ToInt64( message, startIndex: 1 + sizeof(Int32) ); // Reads bytes message[5,6,7,8,9,10,11,12]
DateTime createdUtc = DateTime.FromBinary( createdUtcBinary );
if( createdUtc.Add( _passwordResetExpiry ) < DateTime.UtcNow ) return false;
const Int32 _messageLength = 1 + sizeof(Int32) + sizeof(Int64); // 1 + 4 + 8 == 13
using( HMACSHA256 hmacSha256 = new HMACSHA256( key: _privateKey ) )
{
Byte[] hash = hmacSha256.ComputeHash( message, offset: 0, count: _messageLength );
Byte[] messageHash = message.Skip( _messageLength ).ToArray();
return Enumerable.SequenceEquals( hash, messageHash );
}
}
Used like so:
// Note there is no `UserId` URL parameter anymore because it's embedded in `code`:
[HttpGet( "/PasswordReset/ResetPassword/{codeBase64Url}" )]
public IActionResult ConfirmResetPassword( String codeBase64Url )
{
if( !VerifyPasswordResetHmacCode( codeBase64Url, out Int32 userId ) )
{
// Message is invalid, such as the HMAC hash being incorrect, or the code has expired.
return this.BadRequest( "Invalid, tampered, or expired code used." );
}
else
{
// Return a web-page with a <form> to POST the code.
// Render the `codeBase64Url` to an <input type="hidden" /> to avoid the user inadvertently altering it.
// Do not reset the user's password in a GET request because GET requests must be "safe". If you send a password-reset link by SMS text message or even by email, then software bot (like link-preview generators) may follow the link and inadvertently reset the user's password!
}
}
[HttpPost( "/PasswordReset/ResetPassword" )]
public IActionResult ConfirmResetPassword( [FromForm] ConfirmResetPasswordForm model )
{
if( !VerifyPasswordResetHmacCode( model.CodeBase64Url, out Int32 userId ) )
{
return this.BadRequest( "Invalid, tampered, or expired code used." );
}
else
{
// Reset the user's password here.
}
}
Actually, I wouldn't do any of these.
I faced the same issue and I decided to send a reset token and to do this I used a JWT token.
On that token (which is encrypted) you can set an expiry. Simply create a reset token including the Customers email address as a claim and then set your expiry, store this in your database (in its encrypted form) and encode it and place on the link as URL parameter.
Then when you receive the request you can verify the token is valid. You can then unpack it look at the email address and then proceed to direct them to your secure password reset area for their account. (you can include other claims such as username etc).
To get the JWT implemnetation you can type Install-Package JWT
I do not think you need an encrypted string for this purpose. I think creating one string with Guid would be enough.
string thatString=Guid.NewGuid("n").ToString();
Save that in your db table against that particular user account. Create a link for the user which has this string and send it to them. When they click on it, it will take them to an action method and their you get the corresponding user record associated with this temp string we stored and show the form for user to update the password.
And if you have a doubt whether Guid's are unique, checkout this.
Better than using a random number is to salt then hash. Here is a snippet from a security guru:
#using System.Security.Cryptography;
static byte[] GenerateSaltedHash(byte[] plainText, byte[] salt)
{
HashAlgorithm algorithm = new SHA256Managed();
byte[] plainTextWithSaltBytes =
new byte[plainText.Length + salt.Length];
for (int i = 0; i < plainText.Length; i++)
{
plainTextWithSaltBytes[i] = plainText[i];
}
for (int i = 0; i < salt.Length; i++)
{
plainTextWithSaltBytes[plainText.Length + i] = salt[i];
}
return algorithm.ComputeHash(plainTextWithSaltBytes);
}
You can see more on his answer here: https://stackoverflow.com/a/2138588/1026459
Basically just create a password. Salt and hash it here, and then compare it when the user returns. The linked answer also contains a comparison method and a more in depth explanation of salt/hashing.

Categories