AutopilotUtility.psm1

function ConvertFrom-AutopilotHash
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ParameterSetName="file")] [string] $CSVFile,
        [Parameter(Mandatory=$true, ParameterSetName="hash")] [string] $Hash
    )
    Begin {
        if ($PSCmdlet.ParameterSetName -eq "file") {
            $csvContents = Import-Csv -Path $CSVFile
            $hashes = $csvContents.'Hardware Hash'
        } else {
            $hashes = $Hash
        }
    }
    Process {
        $hashes | % {
            $binary = [Convert]::FromBase64String($_)
            # Validate the header
            if ($binary[0] -ne 79 -or $binary[1] -ne 65) {
                Write-Error "Invalid hash"
            }
            # Validate the checksum
            # TODO
            # Process the values
            $totalLength = [System.BitConverter]::ToUInt16($binary, 2)
            $data = [ordered]@{}
            $currentOffset = 4
            $diskSerialCount = 0
            $diskInfoCount = 0
            $displayResolutionCount = 0
            $gpuCount = 0
            $gpuNameCount = 0
            $networkCount = 0
            while ($currentOffset -lt $totalLength) {
                $type = $binary[$currentOffset]
                $length = [System.BitConverter]::ToUInt16($binary, $currentOffset + 2)
                switch ($type) {
                    1 {
                        $offset = $currentOffset + 4
                        # Two version strings, 8 bytes each
                        $versions = @()
                        for ($i = 0; $i -le 1; $i++) {
                            $minor = [System.BitConverter]::ToUInt16($binary, $offset)
                            $major = [System.BitConverter]::ToUInt16($binary, $offset + 2)
                            $rev = [System.BitConverter]::ToUInt16($binary, $offset + 4)
                            $build =  [System.BitConverter]::ToUInt16($binary, $offset + 6)
                            $offset += 8
                            $versions += [Version]::new($major, $minor, $build, $rev)
                        }
                        $data["ToolBuild"] = $versions[0]
                        $data["OSBuild"] = $versions[1]
                        # Need to figure out the next five bytes
                        # Last three bytes
                        switch ($binary[$currentOffset + $length - 3]) {
                            2 { $data["OSType"] = "FullOS" }
                            default { $data["OSType"] = $binary[$currentOffset + $length - 3] }
                        }
                        switch ($binary[$currentOffset + $length - 2]) {
                            0 { $data["OSCpuArchitecture"] = "X86" }
                            9 { $data["OSCpuArchitecture"] = "X64" }
                            12 { $data["OSCpuArchitecture"] = "ARM64" }
                            default { $data["OSCpuArchitecture"] = $binary[$currentOffset + $length - 2] }
                        }
                        $data["ToolVersion"] = $binary[$currentOffset + $length - 1]
                    }
                    2 {
                        switch ($binary[$currentOffset + 4]) {
                            0 { $data["ProcessorArchitecture"] = "X86" }
                            9 { $data["ProcessorArchitecture"] = "X64" }
                            12 { $data["ProcessorArchitecture"] = "ARM64" }
                            default { $data["ProcessorArchitecture"] = $binary[$currentOffset + 4] }
                        }
                        $data["ProcessorPackages"] = [System.BitConverter]::ToUInt16($binary, $currentOffset + 6)
                        $data["ProcessorCores"] = [System.BitConverter]::ToUInt16($binary, $currentOffset + 8)
                        $data["ProcessorThreads"] = [System.BitConverter]::ToUInt16($binary, $currentOffset + 10)
                        if ($binary[$currentOffset + 12] -eq 1) {
                            $data["ProcessorHyperthreading"] = $true
                        } else {
                            $data["ProcessorHyperthreading"] = $false
                        }
                    }
                    3 {
                        $data["ProcessorManufacturer"] = [System.Text.Encoding]::UTF8.GetString($binary, $currentOffset + 4, $length - 4)
                    }
                    4 {
                        $data["ProcessorModel"] = [System.Text.Encoding]::UTF8.GetString($binary, $currentOffset + 4, $length - 4)
                    }
                    5 {
                        $data["TotalPhysicalRam"] = [System.BitConverter]::ToUInt16($binary, $currentOffset + 4)
                        $data["SmbiosRamMaximumCapacity"] = [System.BitConverter]::ToUInt16($binary, $currentOffset + 12)
                        $data["SmbiosRamSlots"] = [System.BitConverter]::ToUInt16($binary, $currentOffset + 20)
                        $data["SmbiosRamArrayCount"] = [System.BitConverter]::ToUInt16($binary, $currentOffset + 22)
                        switch ($binary[$currentOffset + 24]) {
                            3 { $data["SmbiosRamErrorCorrection"] = "None" }
                            4 { $data["SmbiosRamErrorCorrection"] = "Parity" }
                            5 { $data["SmbiosRamErrorCorrection"] = "Single-bit ECC" }
                            6 { $data["SmbiosRamErrorCorrection"] = "Multi-bit ECC" }
                            7 { $data["SmbiosRamErrorCorrection"] = "CRC" }
                            default { $data["SmbiosRamErrorCorrection"] = $binary[$currentOffset + 24] }
                        }
                    }
                    6 {
                        # 06 00 10 00 88 00 00 00 00 00 00 00 0A 00 FF 01 = From VM
                        # 06 00 10 00 00 04 00 00 00 00 00 00 0B 00 00 01 = From P620
                        $diskInfoCount++
                        $data["Disk$diskInfoCount.DiskCapacity"] = [System.BitConverter]::ToUInt32($binary, $currentOffset + 4)
                        # BusType from https://docs.microsoft.com/en-us/previous-versions/windows/desktop/stormgmt/msft-disk
                        switch ($binary[$currentOffset + 12]) {
                            1 { $busType = "SCSI" }
                            2 { $busType = "ATAPI" }
                            3 { $busType = "ATA" }
                            8 { $busType = "RAID" }
                            10 { $busType = "SAS" }
                            11 { $busType = "SATA" }
                            17 { $busType = "NVMe" }
                            default { $busType = $binary[$currentOffset + 12]}
                        }
                        $data["Disk$diskInfoCount.StorageBusType"] = $busType
                        # Guessing on this one
                        if ($busType -eq "NVMe") {
                            $data["Disk$diskInfoCount.DiskType"] = "NVMe"
                        } else {
                            switch ($binary[$currentOffset + 14]) {
                                0 { $data["Disk$diskInfoCount.DiskType"] = "SSD" }
                                255 { $data["Disk$diskInfoCount.DiskType"] = "HDD" }
                            }
                        }
                    }
                    7 {
                        $diskSerialCount++
                        $data["Disk$diskSerialCount.DiskSerial"] = [System.Text.Encoding]::UTF8.GetString($binary, $currentOffset + 4, $length - 4)
                    }
                    8 {
                        # From VM:
                        # 08 00 1E 00 00 00 00 00 00 15 5D E9 DB 33 0C 00 00 00 56 00 4D 00 42 00 55 00 53 00 00 00
                        # From P620:
                        # 08 00 1A 00 0E 00 00 00 6C B3 11 40 77 8A 08 00 00 00 50 00 43 00 49 00 00 00
                        # 08 00 1A 00 0E 00 00 00 E0 4F 43 E6 0F 48 08 00 00 00 50 00 43 00 49 00 00 00
                        # 08 00 1A 00 0E 00 00 00 6C B3 11 40 77 8B 08 00 00 00 50 00 43 00 49 00 00 00
                        $networkCount++
                        switch ($binary[$currentOffset + 4]) {
                            1 { $medium = "Wireless Lan" }
                            9 { $medium = "Native 802.11" }
                            10 { $medium = "Bluetooth" }
                            14 { $medium = "Ethernet" }
                            default { $medium = $binary[$currentOffset + 4] }
                        }
                        $data["Network$networkCount.PhysicalMedium"] = $medium
                        $data["Network$networkCount.MacAddress"] = [System.BitConverter]::ToString($binary[($currentOffset + 8)..($currentOffset + 13)])
                    }
                    9 { 
                        $displayResolutionCount++
                        $data["Display$displayResolutionCount.SizePhysicalH"] = [System.BitConverter]::ToUInt16($binary, $currentOffset + 4)
                        $data["Display$displayResolutionCount.SizePhysicalV"] = [System.BitConverter]::ToUInt16($binary, $currentOffset + 6)
                        $data["Display$displayResolutionCount.ResolutionHorizontal"] = [System.BitConverter]::ToUInt16($binary, $currentOffset + 8)
                        $data["Display$displayResolutionCount.ResolutionVertical"] = [System.BitConverter]::ToUInt16($binary, $currentOffset + 10)
                    }
                    10 {
                        # 0A 00 09 00 03 01 00 00 00 on VM
                        $data["ChassisType"] = $binary[$currentOffset + 4]
                        $data["PowerPlatformRole"] = [Microsoft.PowerShell.Commands.PowerPlatformRole]$binary[$currentOffset + 5]
                    }
                    11 {
                        $data["OfflineDeviceId"] = [Convert]::ToBase64String($binary[($currentOffset + 14)..($currentOffset + $length - 4)])
                        # Doing some guessing here:
                        # 0B 00 2E 00 00 00 00 00 02 01 20 00 00 00 = UEFI_VARIABLE_TPM
                        # 0B 00 2E 00 00 00 00 00 01 00 20 00 00 00 = TPM_EK
                        switch ($binary[$currentOffset + 8]) {
                            1 { $data["OfficeDeviceIdType"] = "TPM_EK" }
                            2 { $data["OfficeDeviceIdType"] = "UEFI_VARIABLE_TPM" }
                            default { $data["OfficeDeviceIdType"] = $binary[$currentOffset + 8] }
                        }
                    }
                    12 {
                        $data["SmbiosUuid"] = [Guid]::new([byte[]]$binary[($currentOffset + 4)..($currentOffset + $length - 1)])
                    }
                    13 {
                        $data["TpmVersion"] = [System.Text.Encoding]::UTF8.GetString($binary, $currentOffset + 4, $length - 4)
                    }
                    14 {
                        $data["SmbiosSerial"] = [System.Text.Encoding]::UTF8.GetString($binary, $currentOffset + 4, $length - 4)
                    }
                    15 {
                        $data["SmbiosFirmwareVendor"] = [System.Text.Encoding]::UTF8.GetString($binary, $currentOffset + 4, $length - 4)
                    }
                    16 {
                        $data["SmbiosSystemManufacturer"] = [System.Text.Encoding]::UTF8.GetString($binary, $currentOffset + 4, $length - 4)
                    }
                    17 {
                        $data["SmbiosProductName"] = [System.Text.Encoding]::UTF8.GetString($binary, $currentOffset + 4, $length - 4)
                    }
                    18 {
                        $data["SmbiosSKUNumber"] = [System.Text.Encoding]::UTF8.GetString($binary, $currentOffset + 4, $length - 4)
                    }
                    19 {
                        $data["SmbiosSystemFamily"] = [System.Text.Encoding]::UTF8.GetString($binary, $currentOffset + 4, $length - 4)
                    }
                    20 {
                        $data["SmbiosFirmwareVendor2"] = [System.Text.Encoding]::UTF8.GetString($binary, $currentOffset + 4, $length - 4)
                    }
                    21 {
                        $data["SmbiosBoardProduct"] = [System.Text.Encoding]::UTF8.GetString($binary, $currentOffset + 4, $length - 4)
                    }
                    22 {
                        $data["SmbiosBoardVersion"] = [System.Text.Encoding]::UTF8.GetString($binary, $currentOffset + 4, $length - 4)
                    }
                    23 {
                        $data["SmbiosSystemVersion"] = [System.Text.Encoding]::UTF8.GetString($binary, $currentOffset + 4, $length - 4)
                    }
                    24 {
                        $data["ProductKeyID"] = [System.Text.Encoding]::UTF8.GetString($binary, $currentOffset + 4, $length - 4)
                    }
                    25 {
                        $data["TpmEkPub"] = [Convert]::ToBase64String($binary[($currentOffset + 4)..($currentOffset + $length - 4)])
                    }
                    26 {
                        $data["ProductKeyPkPn"] = [System.Text.Encoding]::UTF8.GetString($binary, $currentOffset + 4, $length - 4)
                    }
                    # 27 {
                    # # No idea on this one, doesn't appear to have much value
                    # # 1B 00 08 00 FF FF FF FF on P620
                    # # 1B 00 08 00 00 00 00 00 on VM
                    # # Write-Host "@ $currentOffset : $type = $length"
                    # }
                    28 {
                        $data["DiskSSNKernel"] = [System.Text.Encoding]::UTF8.GetString($binary, $currentOffset + 4, $length - 4)
                    }
                    29 {
                        # 1D 00 16 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 01 on VM
                        # 1D 00 16 00 00 E0 9B 7B 00 00 00 00 00 00 00 00 00 00 00 00 01 01 on P620
                        $gpuCount++
                        $data["Gpu$gpuCount.DedicatedVideoMemory"] = [System.BitConverter]::ToUInt32($binary, $currentOffset + 4)
                        $data["Gpu$gpuCount.DedicatedSystemMemory"] = [System.BitConverter]::ToUInt32($binary, $currentOffset + 8)
                        # Not sure on these flags, likely the last two bytes:
                        # <p n="Gpu1.RemovalPolicy" v="NoRemoval" />
                        # <p n="Gpu1.InLocalMachineContainer" v="true" />
                    }
                    30 {
                        $gpuNameCount++
                        $data["Gpu$gpuNameCount.VideoAdapter"] = [System.Text.Encoding]::UTF8.GetString($binary, $currentOffset + 4, $length - 4)
                    }
                    default {
                        $hex = [System.BitConverter]::ToString($binary[($currentOffset)..($currentOffset + $length - 1)])
                        Write-Host "@ $currentOffset : $type ( $length ) $hex"
                    }
                }
                $currentOffset += $length
            } 
            [PSCustomObject] $data
        }
    }
}

function add-string($binary, $offset, $type, $value) {
    $binary[$offset] = $type
    $lengthBytes = [byte[]] [System.BitConverter]::GetBytes([uint16]$value.Length + 5)
    $binary[$offset + 2] = $lengthBytes[0]
    $binary[$offset + 3] = $lengthBytes[1]
    $stringBytes = [System.Text.Encoding]::UTF8.GetBytes($value)
    [System.Buffer]::BlockCopy($stringBytes, 0, $binary, $offset + 4, $value.Length)
    return $offset + $value.Length + 5
}

function add-binary($binary, $offset, $type, $value) {
    $binary[$offset] = $type
    $lengthBytes = [byte[]] [System.BitConverter]::GetBytes([uint16]$value.Length + 4)
    $binary[$offset + 2] = $lengthBytes[0]
    $binary[$offset + 3] = $lengthBytes[1]
    [System.Buffer]::BlockCopy($value, 0, $binary, $offset + 4, $value.Length)
    return $offset + $value.Length + 4
}

function add-version($binary, $offset) {
    # Set type 1 and length 28 (fixed)
    $binary[$offset] = 1
    $binary[$offset + 2] = 28
    $offset += 4
    # Hard-code the version
    $major = [uint16]10
    $minor = [uint16]0
    $build = [uint16]22000
    $rev = [uint16]800
    # Convert it to bytes (minor, major, rev, build to get proper byte order)
    for ($i = 0; $i -le 1; $i++) {
        $partBytes = [byte[]] [System.BitConverter]::GetBytes([uint16]$minor)
        $binary[$offset] = $partBytes[0]
        $binary[$offset + 1] = $partBytes[1]
        $partBytes = [byte[]] [System.BitConverter]::GetBytes([uint16]$major)
        $binary[$offset + 2] = $partBytes[0]
        $binary[$offset + 3] = $partBytes[1]
        $partBytes = [byte[]] [System.BitConverter]::GetBytes([uint16]$rev)
        $binary[$offset + 4] = $partBytes[0]
        $binary[$offset + 5] = $partBytes[1]
        $partBytes = [byte[]] [System.BitConverter]::GetBytes([uint16]$build)
        $binary[$offset + 6] = $partBytes[0]
        $binary[$offset + 7] = $partBytes[1]
        $offset += 8
    }
    # Skip unknown five bytes for now
    $offset += 5
    # Add the architecture (hard-coded to x64 at this point)
    $binary[$offset] = 2
    $binary[$offset + 1] = 9
    $binary[$offset + 2] = 8
    return $offset + 3
}

function add-processor($binary, $offset) {
    $processorArchitecture = 9
    $processorPackages = 1
    $processorCores = 1
    $processorThreads = 2
    $processorHyperthreading = 1
    # Set type 2 and length 16 (fixed)
    $binary[$offset] = 2
    $binary[$offset + 2] = 16
    $offset += 4
    # Set the processor info
    $partBytes = [byte[]] [System.BitConverter]::GetBytes([uint16]$processorArchitecture)
    $binary[$offset] = $partBytes[0]
    $binary[$offset + 1] = $partBytes[1]
    $partBytes = [byte[]] [System.BitConverter]::GetBytes([uint16]$processorPackages)
    $binary[$offset + 2] = $partBytes[0]
    $binary[$offset + 3] = $partBytes[1]
    $partBytes = [byte[]] [System.BitConverter]::GetBytes([uint16]$processorCores)
    $binary[$offset + 4] = $partBytes[0]
    $binary[$offset + 5] = $partBytes[1]
    $partBytes = [byte[]] [System.BitConverter]::GetBytes([uint16]$processorThreads)
    $binary[$offset + 6] = $partBytes[0]
    $binary[$offset + 7] = $partBytes[1]
    $partBytes = [byte[]] [System.BitConverter]::GetBytes([uint16]$processorHyperthreading)
    $binary[$offset + 8] = $partBytes[0]
    $binary[$offset + 9] = $partBytes[1]
    return $offset + 12
}

function New-AutopilotHash
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)] [string] $Manufacturer,
        [Parameter(Mandatory=$true)] [string] $Model,
        [Parameter(Mandatory=$true)] [string] $Serial,
        [Parameter(Mandatory=$false)] [string] $UUID = "",
        [Parameter(Mandatory=$false)] [string] $SKUNumber = "",
        [Parameter(Mandatory=$false)] [string] $SystemFamily = "",
        [Parameter(Mandatory=$false)] [string] $TPMEkPub = "",
        [Parameter(Mandatory=$false)] [string] $TPMVersion = "",
        [Parameter(Mandatory=$false)] [string[]] $MacAddress = @(),
        [Parameter(Mandatory=$false)] [UInt32] $PhysicalMedium = 14,
        [Parameter(Mandatory=$false)] [string] $OutputFile = ""
    )
    Process {
        # Initialize the byte array (3000 bytes become 4000 when base64-encoded)
        $binary = New-Object Byte[](3000)
        $binary[0] = 79
        $binary[1] = 65
        $currentOffset = 4

        # Add the version info (type 1)
        $currentOffset = add-version -binary $binary -offset $currentOffset
        # Add the CPU info (type 2)
        $currentOffset = add-processor -binary $binary -offset $currentOffset
        # Add the manufacturer
        $currentOffset = add-string -binary $binary -offset $currentOffset -type 16 -value $Manufacturer
        # Add the model
        $currentOffset = add-string -binary $binary -offset $currentOffset -type 17 -value $Model
        # Add the serial
        $currentOffset = add-string -binary $binary -offset $currentOffset -type 14 -value $Serial
        # Add the UUID, if specified
        if ($UUID -ne "") {
            $guidBytes = ([System.Guid] $UUID).ToByteArray()
            $currentOffset = add-binary -binary $binary -offset $currentOffset -type 12 -value $guidBytes
        }
        # Add the TPMVersion, if specified
        if ($TPMVersion -ne "") {
            $currentOffset = add-string -binary $binary -offset $currentOffset -type 13 -value $TPMVersion
        }
        # Add the SKUNumber, if specified
        if ($SKUNumber -ne "") {
            $currentOffset = add-string -binary $binary -offset $currentOffset -type 18 -value $SKUNumber
        }
        # Add the SystemFamily, if specified
        if ($SystemFamily -ne "") {
            $currentOffset = add-string -binary $binary -offset $currentOffset -type 19 -value $SystemFamily
        }
        # Add the TPMEkPub, if specified
        if ($TPMEkPub -ne "") {
            $currentOffset = add-string -binary $binary -offset $currentOffset -type 25 -value $TPMEkPub
        }
        # Add any MAC addresses specified
        if ($MacAddress.Count -gt 0) {
            $hexAny = 8
            $pcbBinary = 0
            $pdwFlags = 0
            $MacAddress | % {
                # Convert it to a byte array
                $macStripped = $_.Replace(":", "").Replace("-", "").Replace(" ", "")
                $macBytes = New-Object Byte[](6)
                [void][AutopilotUtility.Native]::CryptStringToBinary($macStripped, $macStripped.Length, $hexAny, $macBytes, [ref]$pcbBinary, 0, [ref]$pdwFlags)
                # Add the physical medium to the beginning
                $fullBytes = New-Object Byte[](10)  
                $fullBytes[0] = $PhysicalMedium
                [System.Buffer]::BlockCopy($macBytes, 0, $fullBytes, 4, 6)
                # Add the entry
                $currentOffset = add-binary -binary $binary -offset $currentOffset -type 8 -value $fullBytes
            }
        }

        # Finalize with the length and checksum
        $lengthBytes = [byte[]] [System.BitConverter]::GetBytes([uint16]$currentOffset)
        $binary[2] = $lengthBytes[0]
        $binary[3] = $lengthBytes[1]
        $binary[$currentOffset] = 67
        $binary[$currentOffset + 1] = 83
        $binary[$currentOffset + 2] = 36
        $binary[$currentOffset + 3] = 0
        $hash = [System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash($binary, 0, $currentOffset)
        [System.Buffer]::BlockCopy($hash, 0, $binary, $currentOffset + 4, $hash.Length)

        # Write out the hex buffer
        if ($DebugPreference -eq "Continue") {
            $binary | Format-Hex | Out-Host
        }

        # Build the output object
        $hash = [Convert]::ToBase64String($binary)

        $c = New-Object psobject -Property @{
            "Device Serial Number" = $Serial
            "Windows Product ID" = ""
            "Hardware Hash" = $hash
            "Manufacturer name" = $Manufacturer
            "Device model" = $Model
        }

        # Write the output file if specified. Otherwise, just return the object.
        if ($OutputFile -ne "") {
            $c | Select "Device Serial Number", "Windows Product ID", "Hardware Hash" | ConvertTo-CSV -NoTypeInformation | % {$_ -replace '"',''} | Out-File $OutputFile
        } else {
            $c
        }
    }
}

Add-Type -MemberDefinition @"
[DllImport("crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptStringToBinary(
    [MarshalAs(UnmanagedType.LPWStr)] string pszString,
    uint cchString,
    uint dwFlags,
    byte[] pbBinary,
    ref uint pcbBinary,
    uint pdwSkip,
    ref uint pdwFlags);
"@
 -Namespace "AutopilotUtility" -Name "Native"