Hashing and GetString/GetBytes issue - c#

I have the below code to hash/store/retrieve data for passwords but my first unit test and it fails.
I beleive its the Encoding causing the problem because when GetBytes is called it returns byte[38], byte[36] when it should be 20 I think.
I have to convert to string as I'm storing it in a database.
Any ideas? Thanks
[Fact]
public void EncryptDecryptPasswordShouldMatch()
{
string password = "password";
string passwordKey = string.Empty;
string passwordSalt = string.Empty;
Helpers.CreatePasswordHash(password, out passwordSalt, out passwordKey);
Assert.True(Helpers.PasswordsMatch(passwordSalt, passwordKey, password));
}
public static bool PasswordsMatch(string passwordSalt, string passwordKey, string password)
{
byte[] salt = Encoding.UTF8.GetBytes(passwordSalt);
byte[] key = Encoding.UTF8.GetBytes(passwordKey);
using (var deriveBytes = new Rfc2898DeriveBytes(password, salt))
{
byte[] newKey = deriveBytes.GetBytes(20); // derive a 20-byte key
if (!newKey.SequenceEqual(key))
return false;
}
return true;
}
public static void CreatePasswordHash(string password, out string passwordSalt, out string passwordKey)
{
// specify that we want to randomly generate a 20-byte salt
using (var deriveBytes = new Rfc2898DeriveBytes(password, 20))
{
byte[] salt = deriveBytes.Salt;
byte[] key = deriveBytes.GetBytes(20); // derive a 20-byte key
passwordSalt = Encoding.UTF8.GetString(salt);
passwordKey = Encoding.UTF8.GetString(key);
}
}

Use Base64 to encode binary values to string, it can deal with arbitrary byte sequences. UTF-8 is for transforming between unicode text and bytes and not every valid sequence of bytes is valid for UTF-8. Use Utf-8 to turn the password(which is text) to bytes, but use Base64 for salt and hash.
Convert.ToBase64String and Convert.FromBase64String should do the trick.
Some additional notes:
Your terminology is really weird, don't call the hash key, call it hash.
I'd concatenate the hash and salt in your CreatePasswordHash function, so the caller doesn't have to bother with having two separate values.
Something like return Base64Encode(salt)+"$"+Base64Encode(hash) then use string.Split in the verification function.
It's recommended to use a constant time comparison to verify, but it seems unlikely your timing side-channel can actually be exploited.
Your iteration count is pretty low. I recommend increasing it to 10000.

Modify your code to use the Convert.FromBase64String method:
byte[] salt = Convert.FromBase64String(passwordSalt);
byte[] key = Convert.FromBase64String(passwordKey);
Modify your code to use the Convert.ToBase64String method:
passwordSalt = Convert.ToBase64String(salt);
passwordKey = Convert.ToBase64String(key);

UTF8 is not a way to turn any random bytes into a string. It is for encoding text; not just any bytes are valid UTF8 encoded values. You could use Base64 to and from conversions. Note that base64-encoded strings will take up ~4/3 times the characters of the raw bytes. Here's an example:
byte[] salt = deriveBytes.Salt;
byte[] key = deriveBytes.GetBytes(20); // derive a 20-byte key
passwordSalt = Convert.ToBase64String(salt);
passwordKey = Convert.ToBase64String(key);
And later:
byte[] salt = Convert.FromBase64String(passwordSalt);
byte[] key = Convert.FromBase64String(passwordKey);

Related

pbkdf2 - How do I verify hashed user password (C#)?

When a User registers, I run the password they input through CreateHash() - see below.
I then get passed back a Dictionary<string, string> with the Hash and the Salt in it which I then store in the SQL database each as varchar(256).
When a User attempts to log in, I then retrieve the salt from the DB and pass it as well as the inputted password into GetHash() - see below
The hash that I am getting back does not match what is in the database.
What might I be doing wrong?
public class EncryptionHelper
{
public const int SALT_SIZE = 128;
public const int HASH_SIZE = 128;
public const int ITERATIONS = 100000;
// On declaring a new password for example
public static Dictionary<string, string> CreateHash(string input)
{
Dictionary<string, string> hashAndSalt = new Dictionary<string, string>();
RNGCryptoServiceProvider provider = new RNGCryptoServiceProvider();
byte[] salt = new byte[SALT_SIZE];
provider.GetBytes(salt);
Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(input, salt, ITERATIONS);
hashAndSalt.Add("Hash", Encoding.Default.GetString(pbkdf2.GetBytes(HASH_SIZE)));
hashAndSalt.Add("Salt", Encoding.Default.GetString(salt));
return hashAndSalt;
}
// To check if Users password input is correct for example
public static string GetHash(string saltString, string passwordString)
{
byte[] salt = Encoding.ASCII.GetBytes(saltString);
Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(passwordString, salt, ITERATIONS);
return Encoding.Default.GetString(pbkdf2.GetBytes(HASH_SIZE));
}
}
I think you have two problems here.
As the documentation for Encoding.Default states, the encoding this returns can vary between computers or even on a single computer. This probably isn't your issue here, but you should be deliberate when you choose an encoding.
Most text encodings are not what I'd call "round trippable" for anything other than the text they are designed to encode.
The second option is likely your problem here. Let's assume that Encoding.UTF8 is being used.
Imagine that provider.GetBytes(salt) generates the following bytes (represented as hexadecimal):
78 73 AB 7A 4F 61 E9 3E 8A 96
We can convert it to a string and back using this code:
byte[] salt = new byte[10];
provider.GetBytes(salt);
string saltText = Encoding.UTF8.GetString(salt);
salt = Encoding.UTF8.GetBytes(saltText);
Now let's look at the output as hexadecimal:
78 73 EF BF BD 7A 4F 61 EF BF BD 3E EF BF BD EF BF BD
What's happened? Why haven't we got the same thing out as we put in?
Well, we've tried to interpret the bytes as UTF8, but some of it doesn't decode correctly into a string (because it's not UTF8-encoded text). This means that the string we get doesn't represent the binary data. We then try to convert the string back to binary data, and get a completely different result.
To solve this, you need to use an encoding intended to accurately represent binary data. There are a couple of common options:
Hexadecimal (as I used above).
Base64
Since the base64 option is built-in, let's use that.
public static Dictionary<string, string> CreateHash(string input)
{
Dictionary<string, string> hashAndSalt = new Dictionary<string, string>();
RNGCryptoServiceProvider provider = new RNGCryptoServiceProvider();
byte[] salt = new byte[SALT_SIZE];
provider.GetBytes(salt);
Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(input, salt, ITERATIONS);
hashAndSalt.Add("Hash", Convert.ToBase64String(pbkdf2.GetBytes(HASH_SIZE)));
hashAndSalt.Add("Salt", Convert.ToBase64String(salt));
return hashAndSalt;
}
// To check if Users password input is correct for example
public static string GetHash(string saltString, string passwordString)
{
byte[] salt = Convert.FromBase64String(saltString);
Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(passwordString, salt, ITERATIONS);
return Convert.ToBase64String(pbkdf2.GetBytes(HASH_SIZE));
}
And now, because we're using base64 rather than string encoding that can't handle the binary data, we get the same hash!
If you want hexadecimal you can:
Use Convert.ToHexString and Convert.FromHexString, assuming you're using .NET 5 or newer.
Refer to these answers to implement methods to convert to and from hex if you're using an older version.
My recommendation would be to store these values as binary data in the database instead of strings. You don't need to convert them to string and it makes more sense to keep them as binary.
Encoding.Default.GetString() will interpret the byte array as if it already contains string data. This is not right; the array contains random bytes. Instead, you want to get the Base64-encoded version of the bytes. Using the base-64 encoded version will allow you to get back the same byte array for the salt.
I also suggest using a Tuple instead of a Dictionary, which will save a couple allocations:
public static (string salt, string hash) CreateHash(string input)
{
var provider = new RNGCryptoServiceProvider();
byte[] salt = new byte[SALT_SIZE];
provider.GetBytes(salt);
var pbkdf2 = new Rfc2898DeriveBytes(input, salt, ITERATIONS);
var hash = pbkdf2.GetBytes(HASH_SIZE);
var saltString = Convert.ToBase64String(salt);
var hashString = Convert.ToBase64String(hash);
return (saltString, hashString);
}
public static string GetHash(string saltString, string passwordString)
{
byte[] salt = Convert.FromBase64String(saltString);
var pbkdf2 = new Rfc2898DeriveBytes(passwordString, salt, ITERATIONS);
return Convert.ToBase64String(pbkdf2.GetBytes(HASH_SIZE));
}
public static bool ValidateLogin(string username, string password)
{
//made up the GetUserCredential() method
(string saltString, string hashString) cred = GetUserCredential(username);
//add code here to return false if credential lookup fails
return GetHash(cred.saltString, password) == cred.hashString;
}
See it work here:
https://dotnetfiddle.net/aiJnS2
Encoding.Default might be an ANSI encoding which is prone to losing data since it is based on ANSI code pages. I'd use Base64 as text representation of the hash.
public static string GetHash(string saltString, string passwordString)
{
var salt = Encoding.ASCII.GetBytes(saltString);
var pbkdf2 = new Rfc2898DeriveBytes(passwordString, salt, ITERATIONS);
return Convert.ToBase64String(pbkdf2.GetBytes(HASH_SIZE));
}
It's because you gave two different salts to the two Rfc2898DeriveBytes instances.
In the CreateHash(), Encoding.Default.GetString(salt) tries to encode bytes to a string. It should be avoided because it may fail or convert bytes in an irreversible way.(In the GetHash(), Encoding.ASCII.GetBytes(saltString) will be different from the salt you generated before at CreateHash().)
Use an encoding such as the base16 or base64. See this question for the first.
How do you convert a byte array to a hexadecimal string, and vice versa?.

How to properly hash a password

I want to re-create the MyBB hashing process so I can use its database to authenticate users on a 3rd party app (written on C#).
MyBB uses:
md5(md5($salt).password)
My problem is that the result I get on C# is nowhere similar to the one MyBB gets.
What I've done on C#:
public string HashPass(string password, string salt)
{
MD5 md5 = new MD5CryptoServiceProvider();
byte[] saltHash =md5.ComputeHash(System.Text.Encoding.ASCII.GetBytes(salt));
string passwordAndSalt = password + System.Text.Encoding.ASCII.GetString(saltHash);
byte[] finalHash = md5.ComputeHash(System.Text.Encoding.ASCII.GetBytes(passwordAndSalt));
string final = System.Text.Encoding.ASCII.GetString(finalHash);
return final;
}
The result I get from using that function for password "Test123" and salt "0fYR6mEE" (gathered from MyBB db) is: "??R?????s??" while the actual result should look like "VaHffsyzJeEa4dB3bbMWeUlJObAfN5I9rf1CuNRXCa6xPJTzXL".
Most likely I'm missing something obvious, sorry about that.
There are unknowns here. Which encoding does MyBB use to the password bytes? It could be ASCII, ANSI, or UTF8 or it could get the string bytes directly, i.e., without encoding. So I will write it partly as pseudo code
byte[] passwordBytes = GetBytes(password); // Where you have to define GetBytes
byte[] saltBytes = System.Convert.FromBase64String(salt); // Assuming it is given as base64
// Merge the password bytes and the salt bytes
var mergedBytes = new byte[passwordBytes.Length + saltBytes.Length];
Array.Copy(passwordBytes, mergedBytes, passwordBytes.Length);
Array.Copy(saltBytes, 0, mergedBytes, passwordBytes.Length, saltBytes.Length);
var md5 = new MD5CryptoServiceProvider();
byte[] finalHash = md5.ComputeHash(mergedBytes);
string final = System.Convert.ToBase64String(finalHash);
Note that I'm merging the password bytes and the salt bytes, not the password string and the salt string. Then the MD5 it taken only once from these merged bytes.
But I'm not sure what md5(md5($salt).password) does. Is md5() returning the the hash as base64 string already? Maybe you would have to convert the salt from base64 to bytes[], then get the MD% hash, convert it into a base64 string and then concatenate it with the password string. Then get the bytes from this combined string, do the hash again and convert the result to a base64 string again.
You would have to dig deeper into the source code of MyBB to be sure.

SHA256 hash of String in C# does not agree with hash on website

If I hash the string "password" in C# using SHA256 using the below method I get this as my output:
e201065d0554652615c320c00a1d5bc8edca469d72c2790e24152d0c1e2b6189
But this website(SHA-256 produces a 256-bit (32-byte) hash value) tells me the has is:
5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
I'm clearly having a data format issue or something similar. Any ideas why this C# SHA256Managed method is returning something different? I'm sending the method "password" as the input parameter.
private static string CalculateSHA256Hash(string text)
{
UnicodeEncoding UE = new UnicodeEncoding();
byte[] hashValue;
byte[] message = UE.GetBytes(text);
SHA256Managed hashString = new SHA256Managed();
string hex = "";
hashValue = hashString.ComputeHash(message);
foreach (byte x in hashValue)
{
hex += String.Format("{0:x2}", x);
}
return hex;
}
UnicodeEncoding is UTF-16, guaranteed to inflate all your characters to two to four bytes to represent their Unicode code point (see also the Remarks section on MSDN).
Use (the static) Encoding.UTF8 instead, so most characters will fit in one byte. It depends on which system you need to mimic whether that will work for all strings.
After computing the hash using Encoding.UTF8 as stated on the answer above, I would also return a string as follows:
hex = BitConverter.ToString(hashValue).Replace("-", "");
return hex;

Error in converting from Encrypted value

I have a encryption method GetDecryptedSSN(). I tested it’s correctness by the following test. It works fine
//////////TEST 2//////////
byte[] encryptedByteWithIBMEncoding2 = DecryptionServiceHelper.GetEncryptedSSN("123456789");
string clearTextSSN2 = DecryptionServiceHelper.GetDecryptedSSN(encryptedByteWithIBMEncoding2);
But when I do a conversion to ASCII String and then back, it is not working correctly. What is the problem in the conversion logic?
//////////TEST 1//////////
//String -- > ASCII Byte --> IBM Byte -- > encryptedByteWithIBMEncoding
byte[] encryptedByteWithIBMEncoding = DecryptionServiceHelper.GetEncryptedSSN("123456789");
//encryptedByteWithIBMEncoding --> Encrypted Byte ASCII
string EncodingFormat = "IBM037";
byte[] encryptedByteWithASCIIEncoding = Encoding.Convert(Encoding.GetEncoding(EncodingFormat), Encoding.ASCII,
encryptedByteWithIBMEncoding);
//Encrypted Byte ASCII - ASCII Encoded string
string encodedEncryptedStringInASCII = System.Text.ASCIIEncoding.ASCII.GetString(encryptedByteWithASCIIEncoding);
//UpdateSSN(encodedEncryptedStringInASCII);
byte[] dataInBytesASCII = System.Text.ASCIIEncoding.ASCII.GetBytes(encodedEncryptedStringInASCII);
byte[] bytesInIBM = Encoding.Convert(Encoding.ASCII, Encoding.GetEncoding(EncodingFormat),
dataInBytesASCII);
string clearTextSSN = DecryptionServiceHelper.GetDecryptedSSN(bytesInIBM);
Helper Class
public static class DecryptionServiceHelper
{
public const string EncodingFormat = "IBM037";
public const string SSNPrefix = "0000000";
public const string Encryption = "E";
public const string Decryption = "D";
public static byte[] GetEncryptedSSN(string clearTextSSN)
{
return GetEncryptedID(SSNPrefix + clearTextSSN);
}
public static string GetDecryptedSSN(byte[] encryptedSSN)
{
return GetDecryptedID(encryptedSSN);
}
private static byte[] GetEncryptedID(string id)
{
ServiceProgram input = new ServiceProgram();
input.RequestText = Encodeto64(id);
input.RequestType = Encryption;
ProgramInterface inputRequest = new ProgramInterface();
inputRequest.Test__Request = input;
using (MY_Service operation = new MY_Service())
{
return ((operation.MY_Operation(inputRequest)).Test__Response.ResponseText);
}
}
private static string GetDecryptedID(byte[] id)
{
ServiceProgram input = new ServiceProgram();
input.RequestText = id;
input.RequestType = Decryption;
ProgramInterface request = new ProgramInterface();
request.Test__Request = input;
using (MY_Service operationD = new MY_Service())
{
ProgramInterface1 response = operationD.MY_Operation(request);
byte[] encodedBytes = Encoding.Convert(Encoding.GetEncoding(EncodingFormat), Encoding.ASCII,
response.Test__Response.ResponseText);
return System.Text.ASCIIEncoding.ASCII.GetString(encodedBytes);
}
}
private static byte[] Encodeto64(string toEncode)
{
byte[] dataInBytes = System.Text.ASCIIEncoding.ASCII.GetBytes(toEncode);
Encoding encoding = Encoding.GetEncoding(EncodingFormat);
return Encoding.Convert(Encoding.ASCII, encoding, dataInBytes);
}
}
REFERENCE:
Getting incorrect decryption value using AesCryptoServiceProvider
This is the problem, I suspect:
string encodedEncryptedStringInASCII =
System.Text.ASCIIEncoding.ASCII.GetString(encryptedByteWithASCIIEncoding);
(It's not entirely clear because of all the messing around with encodings beforehand, which seems pointless to me, but...)
The result of encryption is not "text encoded in ASCII" - so you shouldn't try to treat it that way. (You haven't said what kind of encryption you're using, but it would be very odd for it to produce ASCII text.)
It's just an arbitrary byte array. In order to represent that in text only using the ASCII character set, the most common approach is to use base64. So the above code would become:
string encryptedText = Convert.ToBase64(encryptedByteWithIBMEncoding);
Then later, you'd convert it back to a byte array ready for decryption as:
encryptedByteWithIBMEncoding = Convert.FromBase64String(encryptedText);
I would strongly advise you to avoid messing around with the encodings like this if you can help it though. It's not clear why ASCII needs to get involved at all. If you really want to encode your original text as IBM037 before encryption, you should just use:
Encoding encoding = Encoding.GetEncoding("IBM037");
string unencryptedBinary = encoding.GetBytes(textInput);
Personally I'd usually use UTF-8, as an encoding which can handle any character data rather than just a limited subset, but that's up to you. I think you're making the whole thing much more complicated than it needs to be though.
A typical "encrypt a string, getting a string result" workflow is:
Convert input text to bytes using UTF-8. The result is a byte array.
Encrypt result of step 1. The result is a byte array.
Convert result of step 2 into base64. The result is a string.
To decrypt:
Convert the string from base64. The result is a byte array.
Decrypt the result of step 1. The result is a byte array.
Convert the result of step 2 back to a string using the same encoding as step 1 of the encryption process.
In DecryptionServiceHelper.GetEncryptedSSN you are encoding the text in IBM037 format BEFORE encrypting.
So the following piece of code is not correct as you are converting the encrypted bytes to ASCII assuming that its in the IBM037 format. That's wrong as the encrypted bytes is not in IBM037 format (the text was encoded before encryption)
//encryptedByteWithIBMEncoding --> Encrypted Byte ASCII
string EncodingFormat = "IBM037";
byte[] encryptedByteWithASCIIEncoding = Encoding.Convert(Encoding.GetEncoding(EncodingFormat), Encoding.ASCII,
encryptedByteWithIBMEncoding);
One possible solution is to encode the encrypted text using IBM037 format, that should fix the issue I guess.

Why isn't my PHP SHA256 hash equivalent to C# SHA256Managed hash

Why aren't these the same?
php:
$hash = hash('sha256', $userData['salt'] . hash('sha256', $password) );
c#
public static string ComputeHash(string plainText, string salt)
{
// Convert plain text into a byte array.
byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);
byte[] saltBytes = Encoding.UTF8.GetBytes(salt);
SHA256Managed hash = new SHA256Managed();
// Compute hash value of salt.
byte[] plainHash = hash.ComputeHash(plainTextBytes);
byte[] concat = new byte[plainHash.Length + saltBytes.Length];
System.Buffer.BlockCopy(saltBytes, 0, concat, 0, saltBytes.Length);
System.Buffer.BlockCopy(plainHash, 0, concat, saltBytes.Length, plainHash.Length);
byte[] tHashBytes = hash.ComputeHash(concat);
// Convert result into a base64-encoded string.
string hashValue = Convert.ToBase64String(tHashBytes);
// Return the result.
return hashValue;
}
C# is outputting a base64 ecoded string, and PHP is outputting a number in hex. A better comparison might be to pass the parameter true to the end of the hash function of PHP and base64 the result:
$hash = base64_encode(
hash('sha256', $userData['salt'] . hash('sha256', $password), true )
);
Because they're different. Your C# code encodes the computed hash in Base64 encoding at the end. PHP just returns a string of hexadecimal digits.
First suspect:
Encoding.UTF8.GetBytes(plainText);
C# uses UTF-8, your PHP probably doesn't, but you could be lucky if you use strictly letters from the US-ASCII subset.
Second suspect:
Convert.ToBase64String(tHashBytes);
There's nothing about Base64 in your PHP.
Since PHP will give you a hex-encoded result, you should switch to Hex in your C#, too. See this answer for solutions.
Well I'm no C# programmer, but one thing that leaps out at me is this:
// Convert result into a base64-encoded string.
string hashValue = Convert.ToBase64String(tHashBytes);
Are you base64-encoding the final output in C#? Because you're not in PHP...
C#
string toHash = "abcdefg";
SHA256Managed hash = new SHA256Managed();
byte[] signatureData = hash.ComputeHash(new UnicodeEncoding().GetBytes(toHash));
string hashResult = System.Convert.ToBase64String(signatureData);
PHP
print base64_encode(hash("sha256",mb_convert_encoding("abcdefg","UTF-16LE"),true));
Write like top code,They are the same
C#
string toHash = "abcdefg";
SHA256Managed hash = new SHA256Managed();
byte[] signatureData = hash.ComputeHash(new UnicodeEncoding().GetBytes(toHash));
string hashResult = System.Convert.ToBase64String(signatureData);
PHP
print base64_encode(hash("sha256",mb_convert_encoding("abcdefg","UTF-16LE"),true));
Works for me.

Categories