TSQL md5 hash different to C# .NET md5

.net, encoding, sql-server, t-sql, unicode

Solution

If you are dealing with `NVARCHAR` / `NCHAR` data (which is stored as UTF-16 Little Endian), then you would use the `Unicode` encoding, not `BigEndianUnicode`. In .NET, UTF-16 is called `Unicode` while other Unicode encodings are referred to by their actual names: UTF7, UTF8, and UTF32. Hence, `Unicode` by itself is `Little Endian` as opposed to `BigEndianUnicode`. UPDATE: Please see the section at the end regarding UCS-2 and Supplementary Characters.

On the database side:

SELECT HASHBYTES('MD5', N'è') AS [HashBytesNVARCHAR]
-- FAC02CD988801F0495D35611223782CF

On the .NET side:

System.Text.Encoding.ASCII.GetBytes("è")
// D1457B72C3FB323A2671125AEF3EAB5D

System.Text.Encoding.UTF7.GetBytes("è")
// F63A0999FE759C5054613DDE20346193

System.Text.Encoding.UTF8.GetBytes("è")
// 0A35E149DBBB2D10D744BF675C7744B1

System.Text.Encoding.UTF32.GetBytes("è")
// 86D29922AC56CF022B639187828137F8

System.Text.Encoding.BigEndianUnicode.GetBytes("è")
// 407256AC97E4C5AEBCA825DEB3D2E89C

System.Text.Encoding.Unicode.GetBytes("è")  // this one matches HASHBYTES('MD5', N'è')
// FAC02CD988801F0495D35611223782CF

However, this question pertains to `VARCHAR` / `CHAR` data, which is ASCII, and so things are a bit more complicated.

On the database side:

SELECT HASHBYTES('MD5', 'è') AS [HashBytesVARCHAR]
-- 785D512BE4316D578E6650613B45E934

We already see the .NET side above. From those hashed values there should be two questions:

- Why don't any of them match the `HASHBYTES` value?

- Why does the "sqlteam.com" article linked in @Eric J.'s answer show that three of them (`ASCII`, `UTF7`, and `UTF8`) all match the `HASHBYTES` value?

There is one answer that covers both questions: Code Pages. The test done in the "sqlteam" article used "safe" ASCII characters that are in the 0 - 127 range (in terms of the int / decimal value) that do not vary between Code Pages. But the 128 - 255 range -- where we find the "è" character -- is the Extended set that does vary by Code Page (which makes sense as this is the reason for having Code Pages).

Now try:

SELECT HASHBYTES('MD5', 'è' COLLATE SQL_Latin1_General_CP1255_CI_AS) AS [HashBytes]
-- D1457B72C3FB323A2671125AEF3EAB5D

That matches the `ASCII` hashed value (and again, because the "sqlteam" article / test used values in the 0 - 127 range, they did not see any changes when using `COLLATE`). Great, now we finally found a way to match `VARCHAR` / `CHAR` data. All good?

Well, not really. Let's take a look-see at what we were actually hashing:

SELECT 'è' AS [TheChar],
       ASCII('è') AS [TheASCIIvalue],
       'è' COLLATE SQL_Latin1_General_CP1255_CI_AS AS [CharCP1255],
       ASCII('è' COLLATE SQL_Latin1_General_CP1255_CI_AS) AS [TheASCIIvalueCP1255];

Returns:

TheChar TheASCIIvalue   CharCP1255  TheASCIIvalueCP1255
è       232             ?           63

A `?` ? Just to verify, run:

SELECT CHAR(63) AS [WhatIs63?];
-- ?

Ah, so Code Page 1255 doesn't have the `è` character, so it gets translated as everyone's favorite `?`. But then why did that match the MD5 hashed value in .NET when using the ASCII encoding? Could it be that we weren't actually matching the hashed value of `è`, but instead were matching the hashed value of `?`:

SELECT HASHBYTES('MD5', '?') AS [HashBytesVARCHAR]
-- 0xD1457B72C3FB323A2671125AEF3EAB5D

Yup. The true ASCII character set is just the first 128 characters (values 0 - 127). And as we just saw, the `è` is 232. So, using the `ASCII` encoding in .NET is not that helpful. Nor was using `COLLATE` on the T-SQL side.

Is it possible to get a better encoding on the .NET side? Yes, by using Encoding.GetEncoding(Int32), which allows for specifying the Code Page. The Code Page to use can be discovered using the following query (use `sys.columns` when working with a column instead of a literal or variable):

SELECT sd.[collation_name],
       COLLATIONPROPERTY(sd.[collation_name], 'CodePage') AS [CodePage]
FROM   sys.databases sd
WHERE  sd.[name] = DB_NAME(); -- replace function with N'{db_name}' if not running in the DB

The query above returns (for me):

Latin1_General_100_CI_AS_SC    1252

So, let's try Code Page 1252:

System.Text.Encoding.GetEncoding(1252).GetBytes("è") // Matches HASHBYTES('MD5', 'è')
// 785D512BE4316D578E6650613B45E934

Woo hoo! We have a match for `VARCHAR` data that uses our default SQL Server collation :). Of course, if the data is coming from a database or field set to a different collation, then `GetEncoding(1252)` might not work and you will have to find the actual matching Code Page using the query shown above (a Code Page is used across many Collations, so a different Collation does not necessarily imply a different Code Page).

To see what the possible Code Page values are, and what culture / locale they pertain to, please see the list of Code Pages here (list is in the "Remarks" section).

Additional info related to what is actually stored in `NVARCHAR` / `NCHAR` fields:

Any UTF-16 character (2 or 4 bytes) can be stored, though the default behavior of the built-in functions assumes that all characters are UCS-2 (2 bytes each), which is a subset of UTF-16. Starting in SQL Server 2012, it is possible to access a set of Windows collations that support the 4 byte characters known as Supplementary Characters. Using one of these Windows collations ending in `_SC`, either specified for a column or directly in a query, will allow the built-in functions to properly handle the 4 byte characters.

-- The database's collation is set to: SQL_Latin1_General_CP1_CI_AS
SELECT  N'' AS [SupplementaryCharacter],
        LEN(N'') AS [LEN],
        DATALENGTH(N'') AS [DATALENGTH],
        UNICODE(N'') AS [UNICODE],
        LEFT(N'', 1) AS [LEFT],
        HASHBYTES('MD5', N'') AS [HASHBYTES];

SELECT  N'' AS [SupplementaryCharacter],
        LEN(N'' COLLATE Latin1_General_100_CI_AS_SC) AS [LEN],
        DATALENGTH(N'' COLLATE Latin1_General_100_CI_AS_SC) AS [DATALENGTH],
        UNICODE(N'' COLLATE Latin1_General_100_CI_AS_SC) AS [UNICODE],
        LEFT(N'' COLLATE Latin1_General_100_CI_AS_SC, 1) AS [LEFT],
        HASHBYTES('MD5', N'' COLLATE Latin1_General_100_CI_AS_SC) AS [HASHBYTES];

Returns:

SupplementaryChar   LEN   DATALENGTH   UNICODE   LEFT   HASHBYTES
                  2     4             55393    �     0x7A04F43DA81E3150F539C6B99F4B8FA9
                  1     4            165739         0x7A04F43DA81E3150F539C6B99F4B8FA9

As you can see, neither `DATALENGTH` nor `HASHBYTES` are affected. For more information, please see the MSDN page for Collation and Unicode Support (specifically the "Supplementary Characters" section).

Problem

I've generated an md5 hash as below: ``` DECLARE @varchar varchar(400) SET @varchar = 'è' SELECT CONVERT(VARCHAR(2000), HASHBYTES( 'MD5', @varchar ), 2) ``` Which outputs: ``` 785D512BE4316D578E6650613B45E934 ``` However generating an MD5 hash using: ``` System.Text.Encoding.UTF8.GetBytes("è") ``` generates: ``` 0a35e149dbbb2d10d744bf675c7744b1 ``` The encoding in the C# .NET method is set to UTF8 and I had assumed that varchar was also UTF8, any ideas on what I'm doing wrong?

Original source

Related problems