Choosing a strategy for calculating a unique and reliable device identifier can be a frustrating and somewhat more difficult decision than you might initially think.
There’s no silver bullet and like many things in the world of programming, pros and cons need to be determined and trade-offs need to be made.
In this article, I look at some of the common pitfalls around generating a device identifier and offer up my opinion on what makes a good one.
The approach you take in regards to calculating and verifying a device identifier will vary depending on your specific use case.
Let’s consider a couple of the most common use cases before diving into the finer details of what makes up a device identifier value.
A very common use case for a device identifier is to identify specific devices that are licensed to use your software application.
When formulating an identifier for licensing, you need to decide if you want to use an overall identifier value, or if you want to split the identifier up into multiple parts (you can still generate a single identifier value and choose a delimiter character that indicates where to split the string).
Why would you want to split your identifier up?
Well as you know, people tend to change their hardware from time to time. Perhaps one of their components failed, or they have decided to upgrade their system.
By having an identifier that is split up, you can choose to only invalidate a license if more than one piece of hardware has changed.
Another use case for a device identifier is for device monitoring.
Suppose you have written a service that communicates the device status to a server and you want to uniquely identify each client device. In this case, you will want something more than just the device name to ensure that you are continuing to monitor and store data against the correct device, even if the device name changes.
Of course, there are other use cases, but the common theme of the requirements usually revolves around the need to uniquely identify a device to ensure that we are working with the correct one and to do this consistently.
When it comes to generating a device identifier, we first need to decide what properties will be used to build it.
Note that I am focusing primarily on Windows devices, however, I do touch on cross-platform considerations towards the end of the article.
Let’s take a look at some of the most common system properties that tend to be used.
In the sub-sections that follow, I will cover what each property relates to and discuss some of the pros and cons where appropriate. I have included both the PowerShell command needed to retrieve the property on a Windows device, along with an example output value for reference.
Note that the reliability of each system property covered below cannot be guaranteed and your mileage may vary in this respect. My statements are mainly based on my personal experience across different devices over several years. It’s not possible to know all of the facts in this regard given the myriad device types, operating system versions, hardware manufacturers, and associated components.
Motherboard serial number
Get-CimInstance Win32_BaseBoard | Select-Object SerialNumber
Initially, the serial number of the motherboard seems like a really good candidate for generating a device identifier.
After all, the motherboard is at the heart of a device. If the motherboard is changed then that probably means that it has either given up, or the user is building what we would consider a brand new device.
The fundamental problem I’ve seen with the motherboard serial number is that it isn’t always specified by the manufacturer. Therefore you may get an empty value when you try to retrieve it. This is a shame given that it is the core piece of hardware for any device.
Get-CimInstance Win32_Processor | Select-Object ProcessorId
Another property that some developers try to use as an identifier is the processor ID.
The main problem with the processor ID is its lack of uniqueness. The processor ID uniquely identifies a specific make and model of processor, so there are likely to be many clashes with other devices.
Another problem with processor ID comes to light when an application is installed on a virtual machine running within the context of some type 2 hypervisors. An example of such a virtual machine hypervisor is Oracle VirtualBox. When you try to retrieve the processor ID on a VirtualBox VM it will be returned as 0. This is certainly not ideal since virtual machine usage is very widespread.
Get-CimInstance Win32_NetworkAdapterConfiguration | Select-Object MacAddress
... (multiple results)
At a first glance, using the MAC address of a device can seem like a good idea. However, it really isn’t!
For a start, although MAC addresses do well on the uniqueness front, they can be very easily spoofed using free software such as TMAC.
Additionally, it is very common for devices to have multiple network adapters, each featuring its own MAC address. For example, a laptop may have an ethernet adapter, a wireless adapter, and may also be set up to connect to a VPN. As a result, your identifier would change depending on which network adapter is currently in use!
There are ways to get around some of these issues, such as excluding specific types of adapters when retrieving the MAC address value, but I would personally advise against using a MAC address as an identifier.
Another possibility is to generate your own unique identifier (e.g. a GUID) and store this in a file somewhere on the device.
Depending on the use case, this approach can have its merits. You need to think about where the file containing the token will be stored and what permissions will be set on the file.
If the operating system is reinstalled on the device and the user chooses to not keep their files, the file token will no longer be usable.
Using the machine/device name as an identifier is usually a bad idea.
A device can be easily renamed and this may need to happen for very legitimate reasons. So regardless of your use case, relying on the name of a machine usually doesn’t work.
Hard drive serial number
Get-CimInstance Win32_DiskDrive | Select-Object SerialNumber
A very popular property to use for a device identifier is the serial number of the hard drive. Personally, I believe this is one of the best available properties to base an identifier on.
For a start, I’ve never seen an example of where a hard drive serial number has not been specified by the manufacturer. That’s not to say that this has never been the case, but generally speaking, this value should always be available to use.
Secondly, each hard drive manufacturer tends to use its own naming scheme for serial numbers. As a result, for all practical purposes, the value can be considered unique. Like other components, there are ways of cloning the serial number of a hard drive, but this isn’t particularly straightforward to do for everyday users without knowledge of the software needed to accomplish this.
The main downsides to using the hard drive serial number that I see are two-fold. Firstly, as with any hardware component, it’s not possible to 100% guarantee the availability and uniqueness of the value. Secondly, if a user has a failed drive and wants to copy their data onto another drive to get up and running again, the serial number of the drive will be different. For some use cases, this may be acceptable, but in others, it may not (e.g. if you don’t want to be contacted when someone replaces a faulty hard drive to get a new license key for your application).
Having said that, I feel that overall the hard drive serial number makes for a highly reliable identifier that is only subject to change when the component itself is swapped out.
Get-CimInstance Win32_ComputerSystemProduct | Select-Object UUID
The system UUID (also known as the ‘Computer System Product’) is stored on the motherboard and therefore survives operating system reinstallations.
This value should always be unique and cannot be easily changed as it is typically stored along with the BIOS in EEPROM memory.
I believe the system UUID makes an ideal candidate for a reliable device identifier as even the name itself suggests its purpose; to uniquely identify a system.
As usual, there are reports here and there that some manufacturers do not use unique values for this property, but I’ve never personally seen an example of this. Out of all the system properties that I’ve covered, I believe this one stands the best chance of being universally unique.
Note, it’s worth mentioning that on Windows there is another similar system property called ‘Machine GUID’ that can be retrieved from the following Registry location (of course your application will require permissions to read from the Registry): HKLM\SOFTWARE\Microsoft\Cryptography
Building an identifier
Now that we have reviewed the main system properties that are potential candidates for generating a device identifier, let’s look at how we should go about building an identifier value.
In the following sub-sections, I will first of all offer up my advice regarding choosing the properties that make up an identifier. I’ll then show you how to build up the identifier in a .NET application.
My advice in regards to the properties used to build an identifier is to keep things as simple as possible.
In most cases, what you will be trying to do is create an identifier that is unique, but doesn’t break easily if hardware changes. In my opinion, I believe that combining the system UUID along with the system drive serial number results in such an identifier.
I think in most cases it’s reasonable to treat a device as a different system if the hard drive has been changed. It’s certainly easier to reason about this compared to something like RAM being changed or a MAC address changing due to a new network card being installed.
If this approach doesn’t work for you, I would recommend combining the system UUID with another property that you feel works better for your specific use case.
In short, choose at least two properties to make up your identifier to help ensure uniqueness; don’t rely on a single property unless you have a very good reason for doing so.
If you decide to use less unique properties, such as the processor ID, you will need to consider combining a greater number of properties to increase the overall probability of uniqueness beyond any reasonable doubt.
You should choose properties that are as likely as possible to be unique, always available, and unchanging.
After making your decision, confirm that it is a good one by running as many tests as you can across different devices to ensure both uniqueness and consistency in the generated value.
System property retrieval
Depending on your programming language and framework there will be different ways to obtain the properties needed to build your identifier.
For example, if you are using C# and .NET on Windows you can use WMI queries to look up properties.
This can be achieved via the ManagementObjectSearcher class.
To make things easier, and to keep your code concise and readable, I highly recommend a very nice little library called DeviceId written by Matthew King.
The sole purpose of the library is to help you generate a unique device identifier.
With a release size of around 24 KB, DeviceId is a lightweight dependency that won’t bloat your project. Also, as of version 6, DeviceId is split up into different NuGet packages, so you can pick out only the things you need.
Using DeviceId, we can build up a device identifier in a fluent manner as follows.
string identifier = new DeviceIdBuilder() .AddSystemDriveSerialNumber() .AddSystemUUID() .UseFormatter(new HashDeviceIdFormatter(() => SHA256.Create(), new HexByteArrayEncoder())) .ToString() .ToUpper();
In the above example, an instance of the
DeviceIdBuilder class is used to generate an identifier as a string.
The serial number of the system drive (i.e. the drive containing the operating system installation) is added to the identifier. This is important, since there may be secondary drives attached to the device.
The system UUID is also added to help ensure that the identifier is unique across devices.
UseFormatter method is used to customise the format of the overall identifier value.
By default, DeviceId formats the identifier by applying a SHA256 hash and using Base64 encoding. I generally prefer to use upper-case Hexidecimal encoding by using the
HexByteArrayEncoder class and the
ToUpper method (as shown above). I find that this results in a much more readable identifier. However, this is down to personal preference and depends on whether you care about the identifier being readable or are simply looking for the most compact representation possible.
In regards to formatting, you should think about whether an overall hash of the device identifier is what you need.
For example, if you are producing an identifier for licensing purposes, you may want to build up an identifier that is made up of a few different system properties and allow one of two of them to change when validating the identifier.
In this case, it may make more sense to create your own formatting logic that takes a hashed version of each of the system properties and combines these using a separator, such as a colon. Your validation logic could then split the string on the separator and ensure that at least a specified number of property values match.
With DeviceId, you can implement a custom class that implements the
IDeviceIdFormatter interface to customise how the identifier is formatted.
I’ve been focusing mostly on Windows devices, however, depending on the nature of your application and the platforms you are targeting, you may need to consider other operating systems.
One of the nice things about the DeviceId library is its cross-platform support.
This means that you can generate a customised identifier depending on the operating system using code like that shown below (taken from the DeviceId GitHub ReadMe).
string deviceId = new DeviceIdBuilder() .AddMachineName() .AddOsVersion() .OnWindows(windows => windows .AddProcessorId() .AddMotherboardSerialNumber() .AddSystemDriveSerialNumber()) .OnLinux(linux => linux .AddMotherboardSerialNumber() .AddSystemDriveSerialNumber()) .OnMac(mac => mac .AddSystemDriveSerialNumber() .AddPlatformSerialNumber()) .ToString();
I think this is really neat and means that you don’t need to spend time figuring out how to retrieve specific details about the device on each platform. All of the work needed to get the information reliably has been done for you.
Note that you’ll need to target .NET Core or .NET 5+ in order for the cross-platform logic to work.
DeviceId offers the ability to choose from lots of other system properties and other ways of hashing and formatting the identifier depending on your specific requirements. I encourage you to check out the DeviceId GitHub repository and give it a star!
Checking the identifier
Depending on your use case, after generating an identifier you’ll probably want a way of validating it at a later point in time.
This usually involves storing the identifier for a device in a database, or perhaps in a signed license file if you are using the identifier for that purpose.
You’ll typically want to validate that the device identifier is correct by running the same logic that was originally used to calculate the identifier and compare the latest calculated value to the stored identifier value.
It is important to bear in mind that looking up system properties can be an expensive operation.
On Windows, WMI queries can be quite slow, especially the first time that a property is looked up. For this reason, it’s usually a good idea to cache your identifier in memory whenever your application is first started and use the stored value for the lifetime of the application. It’s normally safe to do this on client devices, as the identifier that you build up shouldn’t change while your application is running, providing you have chosen the properties that it consists of carefully.
In summary, I started this article by covering some of the possible use cases for needing a device identifier.
I then moved on to looking at some of the system properties that are typically available to choose from when it comes to deciding how an identifier will be generated.
After reviewing the available properties and weighing up some of the pros and cons of each, I covered how to build a device identifier from within a .NET application and calculate its final value using DeviceId.
Lastly, I covered some of the considerations in relation to checking and validating a device identifier.
I hope you enjoyed this post! Comments are always welcome and I respond to all questions.
If you like my content and it helped you out, please check out the button below 🙂