Skip to main content

Decode DigitalProductId

Overview

  • How to use it? Instructions can be found here.
  • The DigitalProductId (DPID) is a binary blob stored in the Windows Registry that contains licensing information for an installed product, including the encoded product key, product ID, edition, and hardware binding data.
  • It is typically found under HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion (and similar registry paths for Office).
  • Parsing the DPID lets you recover the installed product key and inspect other activation-related fields.
  • When validating a key with PidGenX.dll, it produces DigitalProductId blobs which can be parsed to view key details.

DigitalProductId versions

The DigitalProductId comes in three different versions.

  • DigitalProductId Version 2 (50 bytes)
  • DigitalProductId Version 3 (164 bytes)
  • DigitalProductId Version 4 (1272 bytes)

DigitalProductId structure

DigitalProductId version 2 (50 bytes)

FieldOffsetSizeData TypeDescription
ProductId050 bytesString (Unicode)Null-terminated Product ID string.

DigitalProductId version 3 (164 bytes)

FieldOffsetSizeData TypeDescription
uiSize04 bytesUInt32Total blob size. Always 164.
MajorVersion42 bytesUInt16DPID major version.
MinorVersion62 bytesUInt16DPID minor version.
ProductId824 bytesString (ASCII)Null-terminated Product ID string.
KeyIndex324 bytesUInt32Key index value.
EditionId3616 bytesString (ASCII)Null-terminated edition string (e.g. Professional).
CDKeyBytes5216 bytesByte[]15 bytes of encoded key data. The 16th byte is padding and always zero.
CloneStatus684 bytesUInt32Clone status flag.
Time724 bytesUInt32Timestamp of key installation.
Random764 bytesUInt32Random value.
Lt804 bytesUInt32License type.
In modern PidGenX.dll, it has the following meanings:
0 = Retail
1 = Upgrade
2 = OEM
3 = Volume
4 = PGS:TB
LicenseData[0]844 bytesUInt32License data field 0.
LicenseData[1]884 bytesUInt32License data field 1.
OemId928 bytesString (ASCII)OEM identifier string.
OEMs can use this field to set a custom string.
BundleId1004 bytesUInt32Bundle identifier.
HardwareIdStatic1048 bytesString (ASCII)Used with legacy Pidgen.dll. Modern versions do not use it.
HardwareIdTypeStatic1124 bytesUInt32^
BiosChecksumStatic1164 bytesUInt32^
VolumeSerialStatic1204 bytesUInt32^
TotalRamStatic1244 bytesUInt32^
VideoBiosChecksumStatic1284 bytesUInt32^
HardwareIdDynamic1328 bytesString (ASCII)^
HardwareIdTypeDynamic1404 bytesUInt32^
BiosChecksumDynamic1444 bytesUInt32^
VolumeSerialDynamic1484 bytesUInt32^
TotalRamDynamic1524 bytesUInt32^
VideoBiosChecksumDynamic1564 bytesUInt32^
CRC321604 bytesUInt32CRC32 checksum over bytes 0-159.

DigitalProductId version 4 (1272 bytes)

FieldOffsetSizeData TypeDescription
uiSize04 bytesUInt32Total blob size. Always 1272.
MajorVersion42 bytesUInt16DPID major version.
MinorVersion62 bytesUInt16DPID minor version.
AdvancedPid8128 bytesString (Unicode)Null-terminated AdvancedPid string.
ActivationId136128 bytesString (Unicode)Null-terminated Activation ID (GUID).
OemId26416 bytesString (Unicode)OEM identifier string.
OEMs can use this field to set a custom string.
EditionType280520 bytesString (Unicode)Edition type string.
IsUpgrade8001 byteByte1 if this is an upgrade key, 0 otherwise.
ReservedBytes8017 bytesByte[]Reserved, always zero.
CDKeyBytes80816 bytesByte[]15 bytes of encoded key data. The 16th byte is padding and always zero.
CDKey256Hash82432 bytesByte[]SHA256 hash of the 16 CDKeyBytes.
Hash25685632 bytesByte[]SHA256 hash of the entire blob (with this field zeroed out).
EditionId888128 bytesString (Unicode)Edition ID string.
KeyType1016128 bytesString (Unicode)Key type string (e.g. Retail, Volume:MAK).
EULA1144128 bytesString (Unicode)EULA string.

How to decode DigitalProductId?

Decoding the DigitalProductId means reading the raw binary blob from the Windows Registry and converting the byte arrays back into readable numbers and strings using their fixed offsets. The steps below walk through how this is done.

Step 1: Read the registry blob

The DigitalProductId is typically stored under HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion. Depending on your Windows version, there might be a DigitalProductId and a DigitalProductId4. You can retrieve them natively as byte arrays using PowerShell:

$path = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
$props = Get-ItemProperty -Path $path
$blob3 = $props.DigitalProductId
$blob4 = $props.DigitalProductId4

Step 2: Determine the version

The total size of the blob tells you which version of the structure you are dealing with:

  • 50 bytes: Version 2
  • 164 bytes: Version 3
  • 1272 bytes: Version 4
$size3 = $blob3.Length
$size4 = $blob4.Length
Write-Host "Blob3 size: $size3 bytes"
Write-Host "Blob4 size: $size4 bytes"

For versions 3 and 4, you can also verify this by reading the first 4 bytes (the uiSize field) using [BitConverter]:

$uiSize3 = [BitConverter]::ToUInt32($blob3, 0)
$uiSize4 = [BitConverter]::ToUInt32($blob4, 0)
note

In some Windows versions, the registry may add padding, which increases the total blob size. You can read the uiSize field to accurately determine the expected size.

Step 3: Extract fields by offset

Once you know the version, you can pull any piece of information out by reading bytes at the specified offset.

Extracting Integers (UInt16 / UInt32)

To read a number, such as the KeyIndex in a v3 blob (offset 32, 4 bytes), pass the byte array and the offset directly to [BitConverter]:

$keyIndex = [BitConverter]::ToUInt32($blob3, 32)

Extracting ASCII strings

Let's read the ProductId from a v3 blob (offset 8, 24 bytes). Strings in the DPID are null-terminated, meaning they end with a zero byte (0x00). We decode the bytes into ASCII, then split by the null character to discard the empty padding:

$rawAscii = [System.Text.Encoding]::ASCII.GetString($blob3, 8, 24)
$productId = $rawAscii.Split([char]0)[0].Trim()

Extracting Unicode strings

In v4 blobs, strings are stored as Unicode (UTF-16LE). To read the EditionType (offset 280, 520 bytes), the logic is identical to ASCII, but we use the Unicode encoding class:

$rawUnicode = [System.Text.Encoding]::Unicode.GetString($blob4, 280, 520)
$editionType = $rawUnicode.Split([char]0)[0].Trim()

Extracting Byte arrays

If you just need a subset of bytes, like the 16 CDKeyBytes (offset 808 in a v4 blob), you can slice the array or copy it. This specific field contains the encoded product key, which is handled differently.

$cdKeyBytes = New-Object byte[] 16
[Array]::Copy($blob4, 808, $cdKeyBytes, 0, 16)

By combining these methods, you can parse the entire blob manually.


How to decode Product Key from DigitalProductId?

The product key is not stored as plain text anywhere in the registry. Instead, it is packed into exactly 15 bytes called CDKeyBytes using a Base24 encoding algorithm. Decoding it means reversing that algorithm to get the familiar XXXXX-XXXXX-XXXXX-XXXXX-XXXXX key back.

The steps below walk through this using the all-zero byte pattern as a simple example so the math is easy to follow.

Step 1: Extract CDKeyBytes

The 15 key bytes live at a different offset depending on the DPID version:

  • v3 blob: offset 52, length 16
  • v4 blob: offset 808, length 16

Only the first 15 bytes carry key data. The 16th byte is always zero. We extract all 16 bytes because later we will convert this array into a large positive integer, and .NET requires a trailing zero byte to ensure the number isn't treated as negative.

$bytes = New-Object byte[] 16
[Array]::Copy($blob3, 52, $bytes, 0, 16)

Step 2: Check the N-injection flag

Some product keys contain the letter N (e.g. 98NKM-KC6J9-2QGVJ-GMQJT-MPV6T). These are called PKey2009 keys and were introduced in Windows 8. The N is not part of the Base24 alphabet. It is a special marker injected during decoding. For a deeper look at how PKey2009 keys work, see the Decode PKey2009 Keys guide.

To indicate that an N must be injected, Microsoft sets a single bit inside the raw CDKeyBytes data: bit 3 of the 15th byte (the last usable byte). Think of it as a tiny on/off switch hidden inside the key bytes:

  • Switch ON (1): inject an N into the decoded key.
  • Switch OFF (0): no N, decode normally.

You read the switch, then turn it off (clear the bit) before decoding, so it doesn't interfere with the Base24 math:

$lastByte = $bytes[14] # The 15th byte (array index 14)
$injectN = ($lastByte -shr 3) -band 1 # shift right 3 bits, then check lowest bit
$bytes[14] = $lastByte -band 0xF7 # turn off bit 3

Example from live registry: Let's say the 15th byte is 0x09 (binary 0000 1001). Because bit 3 is a 1, the switch is ON ($injectN = 1). Before decoding, this switch must be turned OFF so it doesn't corrupt the actual key data. By applying a bitwise AND (-band 0xF7), we can safely force bit 3 to zero while keeping all other bits exactly as they were. This turns 0000 1001 into 0000 0001 (0x01), making the byte clean and ready for the math ahead.

Step 3: Base24 decode

The Base24 alphabet has 24 characters (BCDFGHJKMPQRTVWXY2346789), each corresponding to a remainder value from 0 to 23:

0 1 2 3 4 5 6 7 8 9 10 11
B C D F G H J K M P Q R

12 13 14 15 16 17 18 19 20 21 22 23
T V W X Y 2 3 4 6 7 8 9

Characters like A, E, I, O, U, 0, 1, 5 are intentionally excluded to avoid visual confusion.

How do we get the starting number? The 16 extracted bytes are read as a single little-endian integer. By passing the byte array (after clearing bit 3 in Step 2) into [System.Numerics.BigInteger]::new($bytes), it automatically evaluates the array as the massive decimal value 7409378619882335270147970423590124.

This number essentially represents a 128-bit integer. The algorithm repeatedly divides that number by 24, and each division produces one character of the key (the remainder is an index into the 24-character alphabet). This runs 25 times, building the key one character at a time from right to left.

The first few manual steps of dividing our example key look like this:

start: 7409378619882335270147970423590124
iteration 1: 7409378619882335270147970423590124 ÷ 24 = 308724109161763969589498767649588 (remainder 12) -> T
iteration 2: 308724109161763969589498767649588 ÷ 24 = 12863504548406832066229115318732 (remainder 20) -> 6
iteration 3: 12863504548406832066229115318732 ÷ 24 = 535979356183618002759546471613 (remainder 20) -> 6
iteration 4: 535979356183618002759546471613 ÷ 24 = 22332473174317416781647769650 (remainder 13) -> V
iteration 5: 22332473174317416781647769650 ÷ 24 = 930519715596559032568657068 (remainder 18) -> 3
... and so on for all 25 iterations.

In PowerShell, we implement this as a simple loop:

$alphabet = 'BCDFGHJKMPQRTVWXY2346789'
$key = ''
$lastRemainder = 0

# Convert the 16 bytes into a large integer
$bigInt = [System.Numerics.BigInteger]::new($bytes)

for ($i = 0; $i -lt 25; $i++) {
$remainder = [int]($bigInt % 24)
$key = $alphabet[$remainder] + $key
$bigInt = [System.Numerics.BigInteger]::Divide($bigInt, 24)

if ($i -eq 24) { $lastRemainder = $remainder }
}

After 25 iterations you have a 25-character raw string. $lastRemainder holds the final remainder generated, which is also the position at which the N will be inserted (if needed).

Example from live registry: Running our massive decimal value through the loop produces:

lastRemainder : 5
Raw string : HVK7JGPHTMC97JM9MPGT3V66T

Step 4: Insert N (if needed)

If $injectN is 1, drop the first character of the raw string and insert N at the position stored in $lastRemainder:

if ($injectN -eq 1) {
$key = $key.Substring(1).Insert($lastRemainder, 'N')
}

Why drop the first character? The N-injection flag was hidden inside the 15th byte, which skewed the Base24 result. Removing the leading character corrects for that, and the N takes the slot that was freed. The position encoded in $lastRemainder ensures the N lands in the right spot when the key is read back.

Example from live registry: $injectN = 1 and $lastRemainder = 5, so N is inserted at position 5:

HVK7JGPHTMC97JM9MPGT3V66T -> drop first H -> VK7JGPHTMC97JM9MPGT3V66T -> insert N at [5] -> VK7JGNPHTMC97JM9MPGT3V66T

Step 5: Format into groups

Finally, split the 25-character string into five groups of 5 and join them with dashes to get the familiar product key format.

Live result from registry:

VK7JGNPHTMC97JM9MPGT3V66T -> VK7JG-NPHTM-C97JM-9MPGT-3V66T
info
  • If the product key is cleared using the slmgr /cpky command, or if a MAK (Multiple Activation Key) is installed, the CDKeyBytes array will be zeroed out. This results in the placeholder key: BBBBB-BBBBB-BBBBB-BBBBB-BBBBB.
  • When a MAK key is installed for an MSI-based Office version, Office does not update the DPID in the registry and will continue showing the previous key's details.

How to verify integrity?

DigitalProductId version 3: CRC32

A standard CRC32 is computed over the first 160 bytes (offsets 0-159). The result must match the 4-byte value stored at offset 160.

# Extract the first 160 bytes
$data = New-Object byte[] 160
[Array]::Copy($blob3, 0, $data, 0, 160)

# Compute CRC32 (Requires a custom implementation, check BIN\Scripts\Libs\DigitalProductId.ps1 for Get-CustomCRC32)
$computed = Get-CustomCRC32 -Bytes $data

# Read the stored 4-byte CRC32 at offset 160
$stored = [BitConverter]::ToUInt32($blob3, 160)

$computed -eq $stored

DigitalProductId version 4: SHA256

Two SHA256 checks are done:

  1. CDKey256Hash (offset 824): SHA256 of the 16 CDKeyBytes (offset 808-823). Must match the 32 bytes at offset 824.
  2. Hash256 (offset 856): SHA256 of the entire 1272-byte blob with bytes 856-887 zeroed out first. Must match the 32 bytes at offset 856.
$sha256 = [System.Security.Cryptography.SHA256]::Create()

# Check 1: Hash of the 16 CDKeyBytes
$cdKeyBytes = New-Object byte[] 16
[Array]::Copy($blob4, 808, $cdKeyBytes, 0, 16)
$computed1 = $sha256.ComputeHash($cdKeyBytes)

$stored1 = New-Object byte[] 32
[Array]::Copy($blob4, 824, $stored1, 0, 32)

# Verify
[BitConverter]::ToString($computed1) -eq [BitConverter]::ToString($stored1)

# Check 2: Hash of the entire blob
$copy = $blob4.Clone()
for ($i = 856; $i -le 887; $i++) { $copy[$i] = 0 } # Zero out the hash field itself
$computed2 = $sha256.ComputeHash($copy)

$stored2 = New-Object byte[] 32
[Array]::Copy($blob4, 856, $stored2, 0, 32)

# Verify
[BitConverter]::ToString($computed2) -eq [BitConverter]::ToString($stored2)
info

Running slmgr /cpky to clear the product key from the registry zeroes out the CDKeyBytes array. While Windows updates the CRC32 in version 3 and the Hash256 in version 4 to reflect these zeroes, it doesn't update the inner CDKey256Hash. As a result, the CDKey256Hash integrity check will fail on systems where the key has been manually cleared.


tip

To view all fields of a DigitalProductId from the registry or PidGenX.dll export files, see the Scan DPID in Files option.


Feedback / Troubleshooting