Home » Replicating SQL Server 2025’s PBKDF2 hashing algorithm using T-SQL

Replicating SQL Server 2025’s PBKDF2 hashing algorithm using T-SQL

by Vlad Drumea
0 comments 16 minutes read

In this post I talk some more about SQL Server 2025’s new PBKDF2 and demo a method to replicate it using T-SQL.

Intro

Back in June I wrote a post about SQL Server 2025’s new PBKDF2 hashing algorithm, in which I’ve mentioned that “I don’t yet have way to build the new PBKDF2 password hash from parts”.
So, almost 5 months later, after some more reading and a decent amount of trial and error, I finally have it.

SQL Server 2025’s PBKDF2 hash structure

The password hash you see in sys.sql_loginspassword_hash column is a 70‑byte binary comprised of:

  1. 0x0300  (hash version marker for SQL Server 2025) .
  2. 4‑byte salt (generated with CRYPT_GEN_RANDOM(4) ).
  3. 64‑byte PBKDF2 HMAC‑SHA‑512 derived key that is the result of multiple SHA‑512 HMAC iterations.

Performance considerations

Touching on this before going further:
Pure T‑SQL PBKDF2 is slow (each iteration runs a full SHA‑512 HMAC).
And, while the host’s single-core performance has a big impact in the execution time (more on that later), I do not recommend actually using this outside of testing purposes even with the best CPU available.

For production purposes, you’d typically use the built‑in CREATE LOGIN command, or PWDENCRYPT if you need to generate PBKDF2 hashes outside of the ones used for SQL logins.

On pre-2025 versions of SQL Server you could resort to a CLR-based implementation.
This is just a PoC meant for learning and testing. And, while it allows you to create the SQL Server 2025 version of the hash on older versions of SQL Server, I don’t see why you’d want actually to do that.

Objects involved

Note: I generally try to lump all the logic into a single procedure. But, in this case, I opted to split the logic between a main stored procedure and 2 helper scalar functions.
This helps make things easier to read and manage. Especially for me, since I’m not anywhere near a cryptography expert.

The DDL for the objects used to replicate SQL Server 2025’s PBKDF2 in T-SQL can be found in my blog’s GitHub repo.

sp_Pbkdf2HashMssql2025

A stored procedure that acts as the main entry point of the clear text password.
It calls the helper UDFs multiple times, with each iteration building on the results of the previous one, and eventually returns the SQL Server 2025-equivalent PBKDF2 hash.

Parameters

ParameterDescription
@ClearTextPasswordThe password the user typed (max 128 Unicode characters) and that should be hashed.
@IterationsHow many times the algorithm should repeat (defaults to 100,000 based on the iterations count specified in this MS blog post).
@OutHash (output)This is the output returned by the procedure.
It should have the same structure as the password hashes stored in sys.sql_logins

Steps

Note that the first 2 steps are the same as the perquisites for the pre-2025 hashing algorithm.

  1. Create a random salt.
    This is a random piece of data (4 bytes in this case) that is mixed into the hash.
    It’s done so that two different users with the same password get a different hash, and it defeats pre‑computed rainbow‑table attacks.
  2. Turn the password into raw bytes by casting it to VARBINARY.
    This is done because the subsequent operations cannot be done a string, but on the binary representation of said string
  3. Set up the PBKDF2 block index (@BlockIdx).
    This is 1 represented as a 4 byte big-endian binary value. And it’s needed for the initial HMAC (U1).
  4. First call to fn_HmacSha512.
    SET @U = [dbo].[fn_HmacSha512](@PasswordBin, @Salt + @BlockIdx);
    This is where the real cryptographic work starts and is the initial HMAC (U1).
    The fn_HmacSha512 function mixes the password (the “key”) with the message (salt + block‑index) and runs SHA‑512.
  5. Initialize the accumulator (@T).
    PBKDF2 defines a variable T that will hold the XOR of all the HMAC results.
    We start by setting T = U1
  6. Repeat the HMAC many times (the loop).
    The loop runs exactly the number of times (c) specified via @Iterations (e.g. 100,000).
    Each pass making the derived key exponentially harder to reverse.
    For each remaining iteration (from 2 up to @Iterations):
    • Re‑hash the previous HMAC: Ui = HMAC‑SHA‑512( password‑bytes , Ui-1 )
      SET @U = [dbo].[fn_HmacSha512](@PasswordBin, @U);
    • XOR the new Ui into the accumulator: T = T XOR Ui (byte by byte).
      SET @T = [dbo].[fn_XorVarbinary](@T, @U);
  7. At the end of the loop, @T is the derived key and it now contains the XOR of all the HMAC outputs (U1 XOR U2 XOR …. Uc).
    It is a 64‑byte block representing the cryptographic core of the login hash.
  8. Assemble the final 70‑byte login hash in the format that SQL Server 2025 expects (version marker + salt + derived key) and return it as an output.

The code


fn_HmacSha512

A scalar UDF that implements the standard HMAC construction using the SHA‑512 hash algorithm.

It takes a password (in this case the contents of the @PasswordBin variable) and a message (the contents of the previously populated @U variable).
Pads the key to a 128‑byte block, XORs it with the standard inner/outer constants, hashes the inner combination, then hashes the outer combination.
And finally returns the 64‑byte HMAC‑SHA‑512 digest that PBKDF2 relies on.

Parameters

ParameterDescription
@KeyThe secret key. In this case the password (already turned into a binary string).
@MsgThe message we want to protect.
For PBKDF2 this is either salt + block‑index or the previous HMAC output.

Steps

  1. Make sure the key fits the SHA‑512 block size.
    If the key is longer than 128 bytes it’s hashed once as per RFC 2104.
    This is done to ensure that the @Key is <= 128 bytes.
  2. Pad the (now‑short) key to a full block.
    HMAC works with a full‑size block (128 bytes for SHA‑512). So, if the key is shorter, we pad the right side with zero bytes until it reaches exactly 128 bytes.
    This padded key (@KeyPadded) is what we’ll XOR with the inner and outer pad constants.
  3. Build the inner padding (@InnerPadding) and outer padding (@OuterPadding)
    HMAC uses two fixed 1‑byte constants: 0x36 for the inner pad and 0x5C for the outer pad.
    Since, in SQL Server, XOR cannot be applied between two VARBINARY values, it first converts that single‑byte binary to TINYINT so that ^ can be applied.
    It loops 128 times, constructing the two 128‑byte binary strings stored in @InnerPadding and @OuterPadding.
  4. Perform the inner hash.
    Concatenates @InnerPadding with the supplied @Msg and calls HASHBYTES.
    HASHBYTES(N'SHA2_512', @InnerPadding + @Msg)
    The result is a SHA‑512 digest.
  5. And the outer hash.
    Takes the @OuterPadding, appends the inner hash, and hashes the whole thing.
    HASHBYTES(N'SHA2_512', @OuterPadding + HASHBYTES(N'SHA2_512', @InnerPadding + @Msg));
  6. The resulting 64-byte HMAC is then returned and stored in the @U variable of the sp_Pbkdf2HashMssql2025 procedure.

The code


fn_XorVarbinary

A scalar UDF that takes two binary values (the contents of the @T variable from the previous iteration and the @U variable from the current iteration), walks through them byte‑by‑byte, XORs each pair of bytes (padding the shorter value with zeros), and returns the combined binary result.
This makes it possible to perform the XOR step required by PBKDF2 entirely within T‑SQL.

Parameters

ParameterDescription
@TThe contents of the @T variable passed by sp_Pbkdf2HashMssql2025.
@UThe contents of the @U variable passed by sp_Pbkdf2HashMssql2025.

Steps

  1. Finds out how many bytes each input actually contains.
  2. Chooses the longer of the two lengths.
    The XOR result must be as long as the longer operand.
  3. Loop over every position.
    This walks through the binary data one byte at a time.
  4. Extract the current byte from each operand.
    If we’ve run past the end of a shorter operand we substitute a zero byte (in the CASE statement), which leaves the other operand’s byte unchanged after the XOR.
  5. The actual XOR of the two bytes that gets appended to the @Result.
  6. Returns @Result and passes its contents to the @T variable of the sp_Pbkdf2HashMssql2025 procedure.

The code


Generating a SQL Server 2025-like PBKDF2 hash using sp_Pbkdf2HashMssql2025

To generate a SQL Server 2025-like PBKDF2 using sp_Pbkdf2HashMssql2025’s T-SQL implementation, just run the following after creating the 3 objects:


Performance

Note, I’m not running SET STATISTICS TIME ON because that will end up returning time statistics for every loop.
I’ll be using the old-school method of just looking at the execution duration in SSMS :).

The duration is consistently 100 seconds (+/-1 second) on both my VM-based SQL Server 2025 Preview instance as well as my container-based one.
The execution time drops down 37 seconds on my PC where single-core speeds are higher (the CPU is an AMD Ryzen 9 5950X).

For comparison’s sake, using PWDENCRYPT to generate the same version of the hash in SQL Server 2025 takes 150 milliseconds on the VM and container-based instance, and 0ms on my PC.

(1 row affected)

SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 150 ms.

Update:
The eagle-eyed Sean Bloch pointed out in a LinkedIn comment that I’ve defined the input variables in fn_XorVarbinary’s as VARBINARY(MAX), which was correct since I’ve missed updating them after my “just get this working first” faze.

Addressing that as well as switching the @Msg variable in fn_HmacSha512 to VARBINARY(64) instead of 256, improved the execution times as follows:

  • VM and container – 67 seconds
  • PC – 28 seconds

Adding WITH SCHEMABINDING to the two functions brought an additional drop in execution times:

  • VM and container – 65 seconds
  • PC – 27 seconds

Testing the resulting PBKDF2 hash

So far, so good, but all this is pointless if the resulting PBKDF2 isn’t valid.

To make sure that the resulting hash is actually a valid SQL Server 2025 PBKDF2 hash, I’ll run the following two tests.

Testing the PBKDF2 hash with PWDCOMPARE

Similar to what I do in my Cracking SQL Server login passwords online post, I use PWDCOMPARE to check the resulting PBKDF2 hash against the plain text password used to generate the hash.

And the generated PBKDF2 hash matches the password when compared via PWDCOMPARE.

Double-checking with a 128 character password to make sure there’s no silent truncation going on.


Bonus: what happens if I use another value for @Iterations

I’ll be honest, I initially expected to have to mess around with diferent values for the @Iterations paramter.
I was half expecting the folks over at Microsoft to mention 100k more as a rounded down value, but the actual one to be slightly higher than that.

So, when I saw that 100k just works I decided to do a soundness check.
For this, I ran another test but with @Iterations set to 100001.

But PWDCOMPARE returned false this time.
Meaning that 100k is indeed the correct number of iterations and everything was working as expected.

Using the resulting PBKDF2 hash as a SQL login’s password hash in SQL Server 2025

I considered this as being the ultimate test.

If a SQL login that uses the PBKDF2 hash generated with sp_Pbkdf2HashMssql2025 can be used to actually log into a SQL Server 2025 instance, then it means that I really did nail this.

First, I create a new login on my SQL Server 2025 Preview instance.

Then I use a similar approach to the one in my migrating sa’s password post to update test_login’s password hash with one generated through sp_Pbkdf2HashMssql2025.

And yes, I am using that 128 character password for this test too.

And this is the resulting T-SQL which paste and execute in another query editor tab.

So… can I now use the long password to connect to the SQL Server instance?

I open a PowerShell window and connect via sqlcmd.

And I can authenticate successfully with test_login and the password whose hash was updated via sp_Pbkdf2HashMssql2025.


Conclusion

SQL Server 2025’s PBKDF2 hashing algorithm works and then applying it in a pure T-SQL implementation was a very interesting exercise from which I’ve learned a lot.

Also, congrats on making it to the end, I know this ended up being a very lengthy post.

You may also like

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.

This site uses Akismet to reduce spam. Learn how your comment data is processed.