Knowing your operating system details can be crucial, i.e. to ensure that your operating system is up to date. You can view these items with the following command:
winver.exe
This command opens a dialog on windows machines reporting all the important information like build number, version, and SKU.
But how would you automate this, and where do you find the information displayed in the dialog?
Reading Operating System Info From Registry
The information about your operating system details is stored in the Windows Registry at this place: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion. PowerShell reads it for you in a one-liner:
# read operating system info:
Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
Make sure you quote the path because the path contains blanks. Quoting is necessary whenever an argument contains special characters that otherwise would confuse the PowerShell parser. If in doubt, use quotes generously.
The result looks similar to this:
SystemRoot : C:\Windows
BaseBuildRevisionNumber : 1
BuildBranch : 19h1_release
BuildGUID : ffffffff-ffff-ffff-ffff-ffffffffffff
BuildLab : 18362.19h1_release.190318-1202
BuildLabEx : 18362.1.amd64fre.19h1_release.190318-1202
CompositionEditionID : Enterprise
CurrentBuild : 18363
CurrentBuildNumber : 18363
CurrentMajorVersionNumber : 10
CurrentMinorVersionNumber : 0
CurrentType : Multiprocessor Free
CurrentVersion : 6.3
EditionID : Professional
EditionSubManufacturer :
EditionSubstring :
EditionSubVersion :
InstallationType : Client
InstallDate : 1567507361
ProductName : Windows 10 Pro
ReleaseId : 1909
SoftwareType : System
UBR : 720
PathName : C:\Windows
ProductId : 00330-50000-00000-AAOEM
DigitalProductId : {164, 0, 0, 0...}
DigitalProductId4 : {248, 4, 0, 0...}
InstallTime : 132119809618946052
RegisteredOwner : [email protected]
RegisteredOrganization : psconf.eu
PSPath : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion
PSParentPath : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT
PSChildName : CurrentVersion
PSDrive : HKLM
PSProvider : Microsoft.PowerShell.Core\Registry
Selecting Properties
To mimic the dialog, use Select-Object
to pick the properties you need:
# read operating system info:
Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' |
# pick selected properties:
Select-Object -Property CurrentBuild,CurrentVersion,ProductId, ReleaseID, UBR
Now the result is much easier to read:
CurrentBuild : 18363
CurrentVersion : 6.3
ProductId : 00330-50000-00000-AAOEM
ReleaseId : 1909
UBR : 720
Using Native Registry Paths
The example uses the built-in drive HKLM: to access the registry hive HKEY_LOCAL_MACHINE. Actually, drives are not necessary to access registry keys, though. All you need is the name of the Provider that can access the information store:
Get-PSDrive -Name HKLM
It turns out that the built-in provider Registry is responsible for accessing registry data:
Name Used (GB) Free (GB) Provider Root CurrentLocation
---- --------- --------- -------- ---- ---------------
HKLM Registry HKEY_LOCAL_MACHINE
By prepending the provider name to a native registry path and two colons, you can access registry keys without drive letters:
# read operating system info:
Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion' |
# pick selected properties:
Select-Object -Property CurrentBuild,CurrentVersion,ProductId, ReleaseID, UBR
This can be very useful, especially when you need to access information in a hive not represented by a built-in drive letter, i.e. HKEY_USERS. With the provider approach, you can access any registry key anywhere. This command shows the root of the registry with all hives:
Get-ChildItem -Path Registry::
Hive:
Name Property
---- --------
HKEY_LOCAL_MACHINE
HKEY_CURRENT_USER
HKEY_CLASSES_ROOT EditFlags : {0, 0, 1, 0}
HKEY_CURRENT_CONFIG
HKEY_USERS
HKEY_PERFORMANCE_DATA Global : {80, 0, 69, 0...}
Costly : {80, 0, 69, 0...}
Implementation: Get-OSInfo
Let’s bake this finding into a PowerShell function and make sure the function returns only the most important properties yet still provides access to any other information as well.
function Get-OSInfo
{
<#
.SYNOPSIS
Reads operating system information similar to winver.exe
.EXAMPLE
Get-OSInfo
returns properties found in winver.exe dialogs
.EXAMPLE
Get-OSInfo | Select-Object -Property *
returns all information about your windows operating system
#>
# turn this function into an advanced function, and add all
# common parameters:
[CmdletBinding()]
param()
#region Define the VISIBLE properties
# define the properties that should be visible by default
# keep this below 5 to produce table output:
[string[]]$visible = 'ProductName','ReleaseId','CurrentBuild','UBR','RegisteredOwner','RegisteredOrganization'
[Management.Automation.PSMemberInfo[]]$visibleProperties = [System.Management.Automation.PSPropertySet]::new('DefaultDisplayPropertySet',$visible)
#endregion
#region read software from all four keys in Windows Registry:
# read all OS info...
Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' |
# add the visible default properties:
Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $visibleProperties -PassThru
#endregion
}
Showing Only Few Default Properties
When you run Get-OSInfo
, only a few default properties are displayed, producing a clean output:
Get-OSInfo
ProductName : Windows 10 Pro
ReleaseId : 1909
CurrentBuild : 18363
UBR : 720
RegisteredOwner : [email protected]
RegisteredOrganization : psconf.eu
Still, you can display any property you want when you add Select-Object
:
Get-OSInfo | Select-Object -Property InstallDate, PathName, EditionID
InstallDate PathName EditionID
----------- -------- ---------
1567507361 C:\Windows Professional
If you’d like to get more information on the techniques used to define the default properties, here are all the details.
Prettifying Properties
While most properties read from the registry are self-explaining, some use awkward formats:
Get-OSInfo | Select-Object -Property InstallDate, InstallTime, DigitalProductId, DigitalProductId4
Obviously, they are stored in some internal format:
InstallDate InstallTime DigitalProductId DigitalProductId4
----------- ----------- ---------------- -----------------
1567507361 132119809618946052 {164, 0, 0, 0...} {248, 4, 0, 0...}
If you know what this format is, you can replace these properties with calculated properties that decrypt or convert the information.
InstallDate
The registry value InstallDate represents the number of seconds passed since 01/01/1970. To get to the true install date, use code like this:
# read the native install date:
$Seconds = Get-OSInfo | Select-Object -ExpandProperty InstallDate
# get 01/01/1970:
$1970 = Get-Date -Day 1 -Month 1 -Year 1970
# add the seconds:
$InstallDate = [System.TimeZone]::CurrentTimeZone.ToLocalTime($1970).AddSeconds($Seconds)
# output the results:
$InstallDate
There are plenty of approaches how to add the seconds to 01/01/1970:
# read the native install date:
$Seconds = Get-OSInfo | Select-Object -ExpandProperty InstallDate
# get 01/01/1970:
$1970 = Get-Date "1970-01-01 00:00:00.000Z"
# get timespan for the seconds:
$timespan = [TimeSpan]::FromSeconds($Seconds)
# add the timespan to the date:
$InstallDate = $1970 + $timespan
# output result:
$InstallDate
It turns out my operating system was installed on september 4, 2019.
InstallTime
This registry value contains the ticks that have passed since 01/01/1601. Ticks are the smallest time interval known by Windows. Each tick represents 100 nanoseconds, so this value is typically a huge integer. The Windows Registry uses the data type REG_QWORD to store it as a 64-bit value.
Converting ticks to the real date is trivial:
# read the ticks:
$ticks = Get-OSInfo | Select-Object -ExpandProperty InstallTime
# convert to date:
$InstallTime = [DateTime]::FromFileTimeUtc($ticks)
# return result:
$InstallTime
DigitalProductId
The Windows product id is sensitive information which is why it is encoded as a byte array.
There are many example scripts and even key recovery tools floating around that claim to retrieve the Windows product key by decoding the byte array found in DigitalProductId - however many of them return invalid keys. That’s because Microsoft changed the encoding scheme after Windows 7, and many tools and scripts are still using the old algorithm.
Originally, I used one of these old recovery algorithms but had doubts about its output. I wasn’t able to double-check the product key because I didn’t know my key. So I started a discussion at twitter and asked for help. What a great community we have! So many added helpful comments, tossed in ideas, and invested work.
At the end, we came up with a simple way to validate the result of any product key recovery tool. And we came up with a working PowerShell way to recover Windows product keys. Thank you all so much. I updated the code below accordingly.
Validating Recovered Keys
The motivation to decode DigitalProductId typically is that you lost your original product key, so there is no easy way for you to test whether a retrieved key is actually correct. And there wasn’t one for me when I published my initial key decoder because I, too, did not have my original Windows key at hand.
Except, there is an easy way to validate recovered product keys.
Philip (@AdamarBE) suggested querying the WMI class SoftwareLicensingProduct which returns licensing information for most Microsoft products including Windows. Part of this is a property called PartialProductKey, so while WMI can’t give you the complete Windows product key, it can give you its ending part, so you can double-check whether key recovery scripts and tools are any good:
Get-CimInstance -ClassName SoftwareLicensingProduct | Where-Object PartialProductKey | Select-Object -Property Name, PartialProductKey
The call takes almost forever, though. When it eventually completes, you get back all Microsoft products with their partial product keys:
Name PartialProductKey
---- -----------------
Office 19, Office19ProPlus2019R_Grace edition 8MBCX
Office 19, Office19ProPlus2019MSDNR_Retail edition VFVPT
Office 16, Office16ProPlusMSDNR_Retail edition 8HWDP
Windows(R), Professional edition WFG6P
You can speed this up tremendously by using a WMI Query so you are only requesting the property PartialProductKey and skip the rest. This way, WMI doesn’t need to calculate information that we won’t use anyway:
(Get-CimInstance -Query 'Select PartialProductKey From SoftwareLicensingProduct Where Name LIKE "Windows%" AND LicenseStatus>0').PartialProductKey
Decoding Product ID
Philip also pointed me to a github project where Pavel (mrpeardotnet) figured out a c# solution to calculate the correct key both for Windows 7 boxes and newer ones including Windows 10.
I extracted the c# beef from his solution and added it to a PowerShell function. Sure, I could have spend some hours translating the c# sources to PowerShell, but why? PowerShell comes with Add-Type
, and this cmdlet can compile c# code on the fly.
So here is a product key recovery function that is guaranteed to work for old and new Windows operating systems including Windows 10 (if you come across a case where it fails, please leave a comment below):
function Get-WindowsProductKey
{
# test whether this is Windows 7 or older:
function Test-Win7
{
$OSVersion = [System.Environment]::OSVersion.Version
($OSVersion.Major -eq 6 -and $OSVersion.Minor -lt 2) -or
$OSVersion.Major -le 6
}
# implement decoder
$code = @'
// original implementation: https://github.com/mrpeardotnet/WinProdKeyFinder
using System;
using System.Collections;
public static class Decoder
{
public static string DecodeProductKeyWin7(byte[] digitalProductId)
{
const int keyStartIndex = 52;
const int keyEndIndex = keyStartIndex + 15;
var digits = new[]
{
'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'M', 'P', 'Q', 'R',
'T', 'V', 'W', 'X', 'Y', '2', '3', '4', '6', '7', '8', '9',
};
const int decodeLength = 29;
const int decodeStringLength = 15;
var decodedChars = new char[decodeLength];
var hexPid = new ArrayList();
for (var i = keyStartIndex; i <= keyEndIndex; i++)
{
hexPid.Add(digitalProductId[i]);
}
for (var i = decodeLength - 1; i >= 0; i--)
{
// Every sixth char is a separator.
if ((i + 1) % 6 == 0)
{
decodedChars[i] = '-';
}
else
{
// Do the actual decoding.
var digitMapIndex = 0;
for (var j = decodeStringLength - 1; j >= 0; j--)
{
var byteValue = (digitMapIndex << 8) | (byte)hexPid[j];
hexPid[j] = (byte)(byteValue / 24);
digitMapIndex = byteValue % 24;
decodedChars[i] = digits[digitMapIndex];
}
}
}
return new string(decodedChars);
}
public static string DecodeProductKey(byte[] digitalProductId)
{
var key = String.Empty;
const int keyOffset = 52;
var isWin8 = (byte)((digitalProductId[66] / 6) & 1);
digitalProductId[66] = (byte)((digitalProductId[66] & 0xf7) | (isWin8 & 2) * 4);
const string digits = "BCDFGHJKMPQRTVWXY2346789";
var last = 0;
for (var i = 24; i >= 0; i--)
{
var current = 0;
for (var j = 14; j >= 0; j--)
{
current = current*256;
current = digitalProductId[j + keyOffset] + current;
digitalProductId[j + keyOffset] = (byte)(current/24);
current = current%24;
last = current;
}
key = digits[current] + key;
}
var keypart1 = key.Substring(1, last);
var keypart2 = key.Substring(last + 1, key.Length - (last + 1));
key = keypart1 + "N" + keypart2;
for (var i = 5; i < key.Length; i += 6)
{
key = key.Insert(i, "-");
}
return key;
}
}
'@
# compile c#:
Add-Type -TypeDefinition $code
# get raw product key:
$digitalId = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name DigitalProductId).DigitalProductId
$isWin7 = Test-Win7
if ($isWin7)
{
# use static c# method:
[Decoder]::DecodeProductKeyWin7($digitalId)
}
else
{
# use static c# method:
[Decoder]::DecodeProductKey($digitalId)
}
}
If you are Corona-bored enough to take the time and translate the c# parts above into native PowerShell, please share and leave a comment below!
DigitalProductId4
The byte array in this registry value supposingly applies to 64bit operating systems. The same algorithm can be applied and yields the same product id.
$id = (Get-OSInfo).DigitalProductId4
Convert-ByteToProductKey -Bytes $id
A Better Get-OSInfo
Let’s integrate the logic described above to have Get-OSInfo
return the decoded information right away:
function Get-OSInfo
{
<#
.SYNOPSIS
Reads operating system information similar to winver.exe
.EXAMPLE
Get-OSInfo
returns properties found in winver.exe dialogs
.EXAMPLE
Get-OSInfo | Select-Object -Property *
returns all information about your windows operating system
#>
# turn this function into an advanced function, and add all
# common parameters:
[CmdletBinding()]
param()
#region define helper function to decode product key
function Get-WindowsProductKey
{
# test whether this is Windows 7 or older:
function Test-Win7
{
$OSVersion = [System.Environment]::OSVersion.Version
($OSVersion.Major -eq 6 -and $OSVersion.Minor -lt 2) -or
$OSVersion.Major -le 6
}
# implement decoder
$code = @'
// original implementation: https://github.com/mrpeardotnet/WinProdKeyFinder
using System;
using System.Collections;
public static class Decoder
{
public static string DecodeProductKeyWin7(byte[] digitalProductId)
{
const int keyStartIndex = 52;
const int keyEndIndex = keyStartIndex + 15;
var digits = new[]
{
'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'M', 'P', 'Q', 'R',
'T', 'V', 'W', 'X', 'Y', '2', '3', '4', '6', '7', '8', '9',
};
const int decodeLength = 29;
const int decodeStringLength = 15;
var decodedChars = new char[decodeLength];
var hexPid = new ArrayList();
for (var i = keyStartIndex; i <= keyEndIndex; i++)
{
hexPid.Add(digitalProductId[i]);
}
for (var i = decodeLength - 1; i >= 0; i--)
{
// Every sixth char is a separator.
if ((i + 1) % 6 == 0)
{
decodedChars[i] = '-';
}
else
{
// Do the actual decoding.
var digitMapIndex = 0;
for (var j = decodeStringLength - 1; j >= 0; j--)
{
var byteValue = (digitMapIndex << 8) | (byte)hexPid[j];
hexPid[j] = (byte)(byteValue / 24);
digitMapIndex = byteValue % 24;
decodedChars[i] = digits[digitMapIndex];
}
}
}
return new string(decodedChars);
}
public static string DecodeProductKey(byte[] digitalProductId)
{
var key = String.Empty;
const int keyOffset = 52;
var isWin8 = (byte)((digitalProductId[66] / 6) & 1);
digitalProductId[66] = (byte)((digitalProductId[66] & 0xf7) | (isWin8 & 2) * 4);
const string digits = "BCDFGHJKMPQRTVWXY2346789";
var last = 0;
for (var i = 24; i >= 0; i--)
{
var current = 0;
for (var j = 14; j >= 0; j--)
{
current = current*256;
current = digitalProductId[j + keyOffset] + current;
digitalProductId[j + keyOffset] = (byte)(current/24);
current = current%24;
last = current;
}
key = digits[current] + key;
}
var keypart1 = key.Substring(1, last);
var keypart2 = key.Substring(last + 1, key.Length - (last + 1));
key = keypart1 + "N" + keypart2;
for (var i = 5; i < key.Length; i += 6)
{
key = key.Insert(i, "-");
}
return key;
}
}
'@
# compile c#:
Add-Type -TypeDefinition $code
# get raw product key:
$digitalId = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name DigitalProductId).DigitalProductId
$isWin7 = Test-Win7
if ($isWin7)
{
# use static c# method:
[Decoder]::DecodeProductKeyWin7($digitalId)
}
else
{
# use static c# method:
[Decoder]::DecodeProductKey($digitalId)
}
}
#endregion
#region Define the VISIBLE properties
# define the properties that should be visible by default
# keep this below 5 to produce table output:
[string[]]$visible = 'ProductName','ReleaseId','CurrentBuild','UBR','RegisteredOwner','RegisteredOrganization'
[Management.Automation.PSMemberInfo[]]$visibleProperties = [System.Management.Automation.PSPropertySet]::new('DefaultDisplayPropertySet',$visible)
#endregion
#region read software from all four keys in Windows Registry:
# read all OS info...
Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' |
Foreach-Object {
# calculate information:
$1970 = Get-Date "1970-01-01 00:00:00.000Z"
$timespan = [TimeSpan]::FromSeconds($_.InstallDate)
$installDate = $1970 + $timespan
$installTime = [DateTime]::FromFileTimeUtc($_.InstallTime)
$digitalproductid = Get-WindowsProductKey -Bytes $_.DigitalProductId
$digitalproductid4 = Get-WindowsProductKey -Bytes $_.DigitalProductId4
# replace raw property content with cooked values:
$_.InstallDate = $installDate
$_.InstallTime = $installTime
$_.DigitalProductId = $DigitalProductId
$_.DigitalProductId4 = $DigitalProductId4
#return object:
$_
} |
# add the visible default properties:
Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $visibleProperties -PassThru
#endregion
}
Now the information returned by Get-OSInfo
is always meaningful, and there is no need anymore to post-process raw values:
Get-OSInfo | Select-Object -Property installdate, installtime, digital*
InstallDate InstallTime DigitalProductId DigitalProductId4
----------- ----------- ---------------- -----------------
03.09.2019 11:42:41 03.09.2019 10:42:41 NF6HC-XXXX-XXXX-WWXV9-WFG6P NF6HC-XXXX-XXXX-WWXV9-WFG6P