Public/Start-WindowsActivation.ps1

#Requires -Version 5

<#
.Synopsis
Installs product keys or activates Windows
.DESCRIPTION
A drop in replacement for slmgr script. By default attempts activation using the product
key already installed on the machine. Use -UseKmsClientKey to also install the
KMS client setup key (GVLK) for the detected OS edition before activating. This is a
material licensing change and is therefore opt-in. Use -ProductKey to install an
explicit product key before activating it in the same operation.
.INPUTS
string[]. You can pass the computer names
.OUTPUTS
None if successful. Throws on error.
.EXAMPLE
Start-WindowsActivation -Verbose # Activates the local computer using its existing product key
.EXAMPLE
Start-WindowsActivation -UseKmsClientKey -Verbose # Installs the GVLK for the detected OS edition then activates
.EXAMPLE
Start-WindowsActivation -ProductKey XXXXX-XXXXX-XXXXX-XXXXX-XXXXX # Installs an explicit product key then activates
.EXAMPLE
Start-WindowsActivation -ActivationId aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee # Activates one licensing product
.EXAMPLE
Start-WindowsActivation -Computer WS01 -Credentials (Get-Credential) # Activates WS01 over WinRM
.EXAMPLE
Start-WindowsActivation -Computer WS01, WS02 -CacheDisabled # Disables the KMS cache on WS01 and WS02
.EXAMPLE
Start-WindowsActivation -Computer WS01 -KMSServerFQDN server.domain.net -KMSServerPort 2500 # Activates against a specific KMS server
.EXAMPLE
Start-WindowsActivation -ReArm # ReArm the trial period (guard clauses apply but cannot guarantee 100% safety)
.EXAMPLE
Start-WindowsActivation -ReArm -ApplicationId 11111111-2222-3333-4444-555555555555 # ReArm one application
.EXAMPLE
Start-WindowsActivation -ReArm -ActivationId aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee # ReArm one licensing product
.EXAMPLE
Start-WindowsActivation -Offline -ConfirmationID 123456-123456-123456-123456-123456-123456-123456-123456-123456 # Phone activation
.LINK
https://github.com/zbalkan/slmgr-ps
#>

function Start-WindowsActivation
{
    [CmdletBinding(SupportsShouldProcess = $true,
        PositionalBinding = $false,
        ConfirmImpact = 'High',
        DefaultParameterSetName = 'ActivateWithKMS')]
    Param
    (
        # Type localhost or . for local computer or do not use the parameter
        [Parameter(Mandatory = $false,
            Position = 0,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ValueFromRemainingArguments = $false)]
        [Parameter(ParameterSetName = 'ActivateWithKMS')]
        [Parameter(ParameterSetName = 'Rearm')]
        [Parameter(ParameterSetName = 'Offline')]
        [AllowNull()]
        [string[]]
        $Computer = @('localhost'),

        # Define credentials other than current user if needed
        [Parameter(Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false)]
        [Parameter(ParameterSetName = 'ActivateWithKMS')]
        [Parameter(ParameterSetName = 'Rearm')]
        [Parameter(ParameterSetName = 'Offline')]
        [AllowNull()]
        [PSCredential]
        $Credentials,

        [Parameter(Mandatory = $false,
            Position = 1,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            ParameterSetName = 'ActivateWithKMS')]
        [Alias('KmsServer')]
        [ValidateNotNullOrEmpty()]
        [string]
        $KMSServerFQDN,

        [Parameter(Mandatory = $false,
            Position = 2,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            ParameterSetName = 'ActivateWithKMS')]
        [ValidateRange(1, 65535)]
        [int]
        $KMSServerPort = 1688,

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            ParameterSetName = 'Rearm')]
        [switch]
        $Rearm,

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            ParameterSetName = 'Rearm')]
        [Guid]
        $ApplicationId,

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            ParameterSetName = 'ActivateWithKMS')]
        [switch]
        $CacheDisabled,

        # Installs the KMS client setup key (GVLK) for the detected OS edition before
        # attempting activation. Only needed when the machine currently has a MAK or retail
        # key and must be switched to volume/KMS licensing. This is a material licensing change.
        [Parameter(Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            ParameterSetName = 'ActivateWithKMS')]
        [switch]
        $UseKmsClientKey,

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            ParameterSetName = 'ActivateWithKMS')]
        [string]
        $ProductKey,

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            ParameterSetName = 'ActivateWithKMS')]
        [Parameter(ParameterSetName = 'Offline')]
        [Parameter(ParameterSetName = 'Rearm')]
        [Guid]
        $ActivationId,

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            ParameterSetName = 'Offline')]
        [switch]$Offline,

        [Parameter(Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            ParameterSetName = 'Offline')]
        [ValidateScript(
            {
                # Accept 54 digits (9 groups × 6), optionally separated by dashes or spaces
                $stripped = $_ -replace '[\s\-]', ''
                if ($stripped -match '^\d{54}$')
                {
                    $true
                }
                else
                {
                    throw "$_ is not a valid Confirmation ID. Expected 54 digits (9 groups of 6), optionally separated by dashes or spaces."
                }
            })]
        [ValidateNotNullOrEmpty()]
        [string]
        $ConfirmationId

    )
    Begin
    {
        $hasProductKey = $PSBoundParameters.ContainsKey('ProductKey')
        $hasActivationId = $PSBoundParameters.ContainsKey('ActivationId')
        $hasApplicationId = $PSBoundParameters.ContainsKey('ApplicationId')
        if ($PSCmdlet.ParameterSetName -eq 'Rearm' -and -not $Rearm.IsPresent)
        {
            throw 'ApplicationId and ActivationId require the Rearm switch in the rearm parameter set.'
        }
        if ($hasApplicationId -and $hasActivationId)
        {
            throw 'ApplicationId and ActivationId cannot be used together for rearm.'
        }
        if ($UseKmsClientKey.IsPresent -and $hasProductKey)
        {
            throw 'UseKmsClientKey and ProductKey cannot be used together.'
        }
        if ($hasActivationId -and ($UseKmsClientKey.IsPresent -or $hasProductKey))
        {
            throw 'ActivationId cannot be combined with product-key installation because InstallProductKey is service-scoped.'
        }
        $hasInvalidProductKey = $ProductKey -notmatch '^[A-Za-z0-9]{5}(?:-[A-Za-z0-9]{5}){4}$'
        if ($hasProductKey -and $hasInvalidProductKey)
        {
            throw 'ProductKey must contain five groups of five alphanumeric characters separated by dashes.'
        }
        if ($PSBoundParameters.ContainsKey('KMSServerFQDN'))
        {
            $endpointParameters = @{ Endpoint = $KMSServerFQDN }
            if ($PSBoundParameters.ContainsKey('KMSServerPort'))
            {
                $endpointParameters['Port'] = $KMSServerPort
            }
            $resolvedKmsEndpoint = Resolve-KmsEndpoint @endpointParameters
        }
    }
    Process
    {
        $activationFailures = [System.Collections.Generic.List[System.Management.Automation.ErrorRecord]]::new()
        Write-Verbose "Enumerating computers: $($Computer.Count) computer(s)."
        foreach ($c in $Computer)
        {
            $action = switch ($PSCmdlet.ParameterSetName)
            {
                'Offline' { 'Apply an offline Windows confirmation ID' }
                'Rearm'
                {
                    if ($hasApplicationId) { "Rearm application $ApplicationId" }
                    elseif ($hasActivationId) { "Rearm licensing product $ActivationId" }
                    else { 'Rearm Windows' }
                }
                default { 'Activate Windows' }
            }
            if (-not $pscmdlet.ShouldProcess($c, $action))
            {
                continue
            }

            Write-Verbose "Creating new CimSession for computer $c"
            $session = $null
            try
            {
                $session = Get-Session -Computer $c -Credentials $Credentials -ErrorAction Stop

                Write-Verbose 'Connecting to SoftwareLicensingService...'
                $service = Get-CimInstance -CimSession $session -ClassName SoftwareLicensingService -ErrorAction Stop

                switch ($PSCmdlet.ParameterSetName)
                {
                    'Offline'
                    {
                        Write-Verbose 'Initiating offline activation operation'
                        $offlineParams = @{
                            CimSession     = $session
                            Service        = $service
                            ConfirmationId = $ConfirmationId
                        }
                        if ($PSBoundParameters.ContainsKey('ActivationId')) { $offlineParams['ActivationId'] = $ActivationId }
                        Invoke-OfflineActivation @offlineParams
                    }

                    'Rearm'
                    {
                        Write-Verbose 'Initiating ReArm operation'
                        $rearmParams = @{ CimSession = $session; Service = $service }
                        if ($hasApplicationId) { $rearmParams['ApplicationId'] = $ApplicationId }
                        if ($hasActivationId) { $rearmParams['ActivationId'] = $ActivationId }
                        Invoke-Rearm @rearmParams
                    }

                    'ActivateWithKMS'
                    {
                        if ($CacheDisabled.IsPresent)
                        {
                            Write-Verbose 'Disabling KMS host caching'
                            $service | Invoke-SppCimMethod `
                                -MethodName DisableKeyManagementServiceHostCaching `
                                -Arguments @{ DisableCaching = $true }
                        }

                        Write-Verbose 'Initiating Windows activation operation'
                        $kmsParams = @{ CimSession = $session; Service = $service }
                        if ($PSBoundParameters.ContainsKey('KMSServerFQDN'))
                        {
                            $kmsParams['KMSServerFQDN'] = $resolvedKmsEndpoint.Host
                            $kmsParams['KMSServerPort'] = $resolvedKmsEndpoint.Port
                        }
                        elseif ($PSBoundParameters.ContainsKey('KMSServerPort'))
                        {
                            $kmsParams['KMSServerPort'] = $KMSServerPort
                        }
                        if ($UseKmsClientKey.IsPresent) { $kmsParams['InstallKmsClientKey'] = $true }
                        if ($PSBoundParameters.ContainsKey('ProductKey')) { $kmsParams['ProductKey'] = $ProductKey }
                        if ($PSBoundParameters.ContainsKey('ActivationId')) { $kmsParams['ActivationId'] = $ActivationId }
                        Invoke-KMSActivation @kmsParams
                    }

                    default
                    {
                        throw 'Unknown parameter combination'
                    }
                }
            }
            catch
            {
                $activationFailures.Add($_)
            }
            finally
            {
                if ($null -ne $session)
                {
                    Remove-CimSession -CimSession $session -ErrorAction Ignore | Out-Null
                }
            }
        }

        if ($activationFailures.Count -gt 0)
        {
            foreach ($failure in $activationFailures)
            {
                Write-Error -ErrorRecord $failure
            }
            $PSCmdlet.ThrowTerminatingError($activationFailures[0])
        }
    }
}