PowerShellProObfuscator.psm1

################################################################################
#
# PowerShell Pro Obfuscator — PowerShell Gallery Web API client
#
# Version : v1.0.0
# Language : PowerShell
# Author : Bartosz Wójcik
# Web page : https://www.pelock.com
#
################################################################################

Set-StrictMode -Version Latest

$script:PpoApiUrl = 'https://www.pelock.com/api/powershell-pro-obfuscator/v1'
$script:PpoUserAgent = 'PELock PowerShell Pro Obfuscator'
$script:PpoTimeoutSec = 3600

$script:PpoErrorMessages = @{
    0 = 'Success'
    1 = 'In demo mode you can only obfuscate source code that is 1000 chars max.'
    2 = 'Please enter valid PowerShell source code sample'
    3 = 'An error occurred while parsing the input source code'
    4 = 'An error occurred while applying obfuscation to the parsed code'
    5 = 'An error occurred while trying to generate output code'
}

# PascalCase switch name -> API form field
$script:PpoStrategyMap = [ordered]@{
    SelfDefending         = 'self_defending'
    DetectDebugger        = 'detect_debugger'
    ProtectionLinker      = 'protection_linker'
    RenameVariables       = 'rename_variables'
    RenameParameters      = 'rename_parameters'
    RenameFunctions       = 'rename_functions'
    ShuffleFunctions      = 'shuffle_functions'
    ControlFlowFlatten    = 'control_flow_flatten'
    StateMachine          = 'state_machine'
    VmStrategy            = 'vm_strategy'
    EncryptIntegers       = 'encrypt_integers'
    EncryptFloating       = 'encrypt_floating'
    IntegersToFloating    = 'integers_to_floating'
    IntegersToArrays      = 'integers_to_arrays'
    FloatsToArrays        = 'floats_to_arrays'
    AffineIntegerMask     = 'affine_integer_mask'
    EncryptStrings        = 'encrypt_strings'
    SplitStrings          = 'split_strings'
    StringCharArrayVault  = 'string_char_array_vault'
    ReflectInvokeCommands = 'reflect_invoke_commands'
    InsertDeadCode        = 'insert_dead_code'
    DecoyFunctions        = 'decoy_functions'
    OpaqueBranches        = 'opaque_branches'
    ScriptblockDecoys     = 'scriptblock_decoys'
    LiteralPadding        = 'literal_padding'
    TryFinallyNoise       = 'try_finally_noise'
    EventStub             = 'event_stub'
    FakeDotSourceMarkers  = 'fake_dot_source_markers'
    ComplexifyBooleans    = 'complexify_booleans'
    RemoveComments        = 'remove_comments'
}

function Compress-PpoSource {
    param([Parameter(Mandatory)][string]$Source)

    $bytes = [System.Text.Encoding]::UTF8.GetBytes($Source)
    $ms = New-Object System.IO.MemoryStream
    try {
        $gzip = New-Object System.IO.Compression.GZipStream($ms, [System.IO.Compression.CompressionMode]::Compress, $true)
        try {
            $gzip.Write($bytes, 0, $bytes.Length)
        }
        finally {
            $gzip.Dispose()
        }
        return [Convert]::ToBase64String($ms.ToArray())
    }
    finally {
        $ms.Dispose()
    }
}

function Expand-PpoSource {
    param([Parameter(Mandatory)][string]$CompressedBase64)

    $bytes = [Convert]::FromBase64String($CompressedBase64)
    $ms = New-Object System.IO.MemoryStream(, $bytes)
    try {
        $gzip = New-Object System.IO.Compression.GZipStream($ms, [System.IO.Compression.CompressionMode]::Decompress)
        try {
            $reader = New-Object System.IO.StreamReader($gzip, [System.Text.Encoding]::UTF8)
            try {
                return $reader.ReadToEnd()
            }
            finally {
                $reader.Dispose()
            }
        }
        finally {
            $gzip.Dispose()
        }
    }
    finally {
        $ms.Dispose()
    }
}

function ConvertTo-PpoFormBody {
    param([Parameter(Mandatory)][hashtable]$Fields)

    $parts = foreach ($key in $Fields.Keys) {
        $name = [uri]::EscapeDataString([string]$key)
        $value = [uri]::EscapeDataString([string]$Fields[$key])
        '{0}={1}' -f $name, $value
    }
    return ($parts -join '&')
}

function Invoke-PpoApiRequest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$Fields
    )

    $body = ConvertTo-PpoFormBody -Fields $Fields
    $headers = @{
        'User-Agent' = $script:PpoUserAgent
    }

    try {
        return Invoke-RestMethod -Uri $script:PpoApiUrl -Method Post -Body $body `
            -ContentType 'application/x-www-form-urlencoded' -Headers $headers `
            -TimeoutSec $script:PpoTimeoutSec
    }
    catch {
        throw ("Cannot connect to the Web API interface, check your network connection. {0}" -f $_.Exception.Message)
    }
}

function Resolve-PpoStrategies {
    [CmdletBinding()]
    param(
        [string]$ActivationCode,
        [hashtable]$StrategySwitches
    )

    $enabled = @{}
    foreach ($name in $script:PpoStrategyMap.Keys) {
        $enabled[$name] = $false
    }

    $hasKey = -not [string]::IsNullOrWhiteSpace($ActivationCode)
    if (-not $hasKey) {
        $enabled['RenameVariables'] = $true
        return $enabled
    }

    $any = $false
    foreach ($name in $script:PpoStrategyMap.Keys) {
        if ($StrategySwitches.ContainsKey($name) -and [bool]$StrategySwitches[$name]) {
            $enabled[$name] = $true
            $any = $true
        }
    }

    if (-not $any) {
        foreach ($name in $script:PpoStrategyMap.Keys) {
            $enabled[$name] = $true
        }
    }

    return $enabled
}

function Add-PpoStrategyFields {
    param(
        [Parameter(Mandatory)][hashtable]$Fields,
        [Parameter(Mandatory)][hashtable]$Enabled
    )

    foreach ($name in $script:PpoStrategyMap.Keys) {
        if ($Enabled[$name]) {
            $Fields[$script:PpoStrategyMap[$name]] = '1'
        }
    }
}

function Get-PpoApiErrorMessage {
    param([int]$ErrorCode)

    if ($script:PpoErrorMessages.ContainsKey($ErrorCode)) {
        return $script:PpoErrorMessages[$ErrorCode]
    }
    return ("Unknown API error code: {0}" -f $ErrorCode)
}

function Get-PpoResultProperty {
    param(
        [Parameter(Mandatory)]$Result,
        [Parameter(Mandatory)][string]$Name
    )

    $prop = $Result.PSObject.Properties[$Name]
    if ($null -eq $prop) {
        return $null
    }
    return $prop.Value
}

function Write-PpoObfuscateStatus {
    param([Parameter(Mandatory)]$Result)

    $demoVal = Get-PpoResultProperty -Result $Result -Name 'demo'
    $isDemo = $false
    if ($null -ne $demoVal) {
        $isDemo = [bool]$demoVal
    }

    if (-not $isDemo) {
        Write-Host '[i] file successfully obfuscated'
        $expiredVal = Get-PpoResultProperty -Result $Result -Name 'expired'
        if ($null -ne $expiredVal -and [bool]$expiredVal) {
            Write-Host '[!] your activation code has expired. Thank you for using PowerShell Pro Obfuscator :)'
        }
        else {
            $creditsLeft = Get-PpoResultProperty -Result $Result -Name 'credits_left'
            if ($null -ne $creditsLeft -and [int]$creditsLeft -gt 0) {
                $left = [int]$creditsLeft
                $creditsTotal = Get-PpoResultProperty -Result $Result -Name 'credits_total'
                $total = if ($null -ne $creditsTotal) { [int]$creditsTotal } else { 0 }
                Write-Host ("[i] you have {0} usage credits left (out of total {1})" -f $left, $total)
            }
        }
    }
    else {
        Write-Host '[i] file successfully obfuscated (demo mode)'
    }
}

<#
.SYNOPSIS
    Validates a PowerShell Pro Obfuscator activation code against the Web API.
 
.DESCRIPTION
    Calls command=login on https://www.pelock.com/api/powershell-pro-obfuscator/v1
    and returns the JSON response as a PowerShell object (credits, expiry, etc.).
 
.PARAMETER ActivationCode
    License / activation key from PELock.
 
.EXAMPLE
    Connect-PowerShellProObfuscator -ActivationCode 'YOUR-KEY'
#>

function Connect-PowerShellProObfuscator {
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$ActivationCode
    )

    $fields = @{
        command = 'login'
        key     = $ActivationCode
    }

    $result = Invoke-PpoApiRequest -Fields $fields
    if ($null -eq $result) {
        throw 'Empty response from the Web API.'
    }

    return $result
}

<#
.SYNOPSIS
    Obfuscates a PowerShell script via the PowerShell Pro Obfuscator Web API.
 
.DESCRIPTION
    Remote API client equivalent of PowerShellProObfuscatorCmd.exe.
    Without -ActivationCode only RenameVariables is applied (demo, 1000 char limit).
    With -ActivationCode and no strategy switches, all strategies are enabled.
    With -ActivationCode and one or more strategy switches, only those are enabled.
 
.PARAMETER InputFilePath
    Path to the input .ps1 file.
 
.PARAMETER OutputFilePath
    Path for the obfuscated .ps1 output.
 
.PARAMETER Source
    In-memory PowerShell source. When used without -OutputFilePath, the obfuscated
    script is returned as a string.
 
.PARAMETER ActivationCode
    License key. Omit for demo mode.
 
.PARAMETER EnableCompression
    Gzip+Base64 the source payload (compression=1), matching the PHP/Python SDKs.
 
.EXAMPLE
    Invoke-PowerShellProObfuscate -InputFilePath .\in.ps1 -OutputFilePath .\out.ps1 -ActivationCode 'KEY' -RenameVariables -EncryptStrings
 
.EXAMPLE
    $out = Invoke-PowerShellProObfuscate -Source 'Write-Host 1' -ActivationCode 'KEY'
#>

function Invoke-PowerShellProObfuscate {
    [CmdletBinding(DefaultParameterSetName = 'File')]
    [OutputType([string])]
    param(
        [Parameter(Mandatory, ParameterSetName = 'File')]
        [ValidateNotNullOrEmpty()]
        [string]$InputFilePath,

        [Parameter(Mandatory, ParameterSetName = 'File')]
        [Parameter(ParameterSetName = 'Source')]
        [ValidateNotNullOrEmpty()]
        [string]$OutputFilePath,

        [Parameter(Mandatory, ParameterSetName = 'Source', ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$Source,

        [Parameter()]
        [string]$ActivationCode,

        [Parameter()]
        [switch]$EnableCompression,

        [Parameter()][switch]$SelfDefending,
        [Parameter()][switch]$DetectDebugger,
        [Parameter()][switch]$ProtectionLinker,
        [Parameter()][switch]$RenameVariables,
        [Parameter()][switch]$RenameParameters,
        [Parameter()][switch]$RenameFunctions,
        [Parameter()][switch]$ShuffleFunctions,
        [Parameter()][switch]$ControlFlowFlatten,
        [Parameter()][switch]$StateMachine,
        [Parameter()][switch]$VmStrategy,
        [Parameter()][switch]$EncryptIntegers,
        [Parameter()][switch]$EncryptFloating,
        [Parameter()][switch]$IntegersToFloating,
        [Parameter()][switch]$IntegersToArrays,
        [Parameter()][switch]$FloatsToArrays,
        [Parameter()][switch]$AffineIntegerMask,
        [Parameter()][switch]$EncryptStrings,
        [Parameter()][switch]$SplitStrings,
        [Parameter()][switch]$StringCharArrayVault,
        [Parameter()][switch]$ReflectInvokeCommands,
        [Parameter()][switch]$InsertDeadCode,
        [Parameter()][switch]$DecoyFunctions,
        [Parameter()][switch]$OpaqueBranches,
        [Parameter()][switch]$ScriptblockDecoys,
        [Parameter()][switch]$LiteralPadding,
        [Parameter()][switch]$TryFinallyNoise,
        [Parameter()][switch]$EventStub,
        [Parameter()][switch]$FakeDotSourceMarkers,
        [Parameter()][switch]$ComplexifyBooleans,
        [Parameter()][switch]$RemoveComments
    )

    begin {
        $strategySwitches = @{
            SelfDefending         = $SelfDefending.IsPresent
            DetectDebugger        = $DetectDebugger.IsPresent
            ProtectionLinker      = $ProtectionLinker.IsPresent
            RenameVariables       = $RenameVariables.IsPresent
            RenameParameters      = $RenameParameters.IsPresent
            RenameFunctions       = $RenameFunctions.IsPresent
            ShuffleFunctions      = $ShuffleFunctions.IsPresent
            ControlFlowFlatten    = $ControlFlowFlatten.IsPresent
            StateMachine          = $StateMachine.IsPresent
            VmStrategy            = $VmStrategy.IsPresent
            EncryptIntegers       = $EncryptIntegers.IsPresent
            EncryptFloating       = $EncryptFloating.IsPresent
            IntegersToFloating    = $IntegersToFloating.IsPresent
            IntegersToArrays      = $IntegersToArrays.IsPresent
            FloatsToArrays        = $FloatsToArrays.IsPresent
            AffineIntegerMask     = $AffineIntegerMask.IsPresent
            EncryptStrings        = $EncryptStrings.IsPresent
            SplitStrings          = $SplitStrings.IsPresent
            StringCharArrayVault  = $StringCharArrayVault.IsPresent
            ReflectInvokeCommands = $ReflectInvokeCommands.IsPresent
            InsertDeadCode        = $InsertDeadCode.IsPresent
            DecoyFunctions        = $DecoyFunctions.IsPresent
            OpaqueBranches        = $OpaqueBranches.IsPresent
            ScriptblockDecoys     = $ScriptblockDecoys.IsPresent
            LiteralPadding        = $LiteralPadding.IsPresent
            TryFinallyNoise       = $TryFinallyNoise.IsPresent
            EventStub             = $EventStub.IsPresent
            FakeDotSourceMarkers  = $FakeDotSourceMarkers.IsPresent
            ComplexifyBooleans    = $ComplexifyBooleans.IsPresent
            RemoveComments        = $RemoveComments.IsPresent
        }
    }

    process {
        if ($PSCmdlet.ParameterSetName -eq 'File') {
            if (-not (Test-Path -LiteralPath $InputFilePath -PathType Leaf)) {
                throw ("Cannot read input file ""{0}""" -f $InputFilePath)
            }

            try {
                $textSourceCode = [System.IO.File]::ReadAllText((Resolve-Path -LiteralPath $InputFilePath).Path)
            }
            catch {
                throw ("Cannot read input file ""{0}"". {1}" -f $InputFilePath, $_.Exception.Message)
            }

            Write-Host ("[i] input file successfully read ""{0}""" -f $InputFilePath)
        }
        else {
            $textSourceCode = $Source
        }

        $textSourceCode = $textSourceCode.Trim().Replace("`r`n", "`n")
        if ([string]::IsNullOrEmpty($textSourceCode)) {
            throw (Get-PpoApiErrorMessage -ErrorCode 1)
        }

        $enabled = Resolve-PpoStrategies -ActivationCode $ActivationCode -StrategySwitches $strategySwitches

        $fields = @{
            command     = 'obfuscate'
            key         = if ($ActivationCode) { $ActivationCode } else { '' }
            compression = '0'
            source      = $textSourceCode
        }

        if ($EnableCompression) {
            $fields['source'] = Compress-PpoSource -Source $textSourceCode
            $fields['compression'] = '1'
        }

        Add-PpoStrategyFields -Fields $fields -Enabled $enabled

        Write-Host '[i] obfuscation in progress, please wait...'
        Write-Verbose ("POST {0}" -f $script:PpoApiUrl)

        $result = Invoke-PpoApiRequest -Fields $fields
        if ($null -eq $result) {
            throw 'Empty response from the Web API.'
        }

        $errorCode = 0
        if ($null -ne $result.error) {
            $errorCode = [int]$result.error
        }

        if ($errorCode -ne 0) {
            throw (Get-PpoApiErrorMessage -ErrorCode $errorCode)
        }

        $outputCode = [string]$result.output
        if ($EnableCompression -and -not [string]::IsNullOrEmpty($outputCode)) {
            $outputCode = Expand-PpoSource -CompressedBase64 $outputCode
        }

        if ([string]::IsNullOrEmpty($outputCode)) {
            throw (Get-PpoApiErrorMessage -ErrorCode 5)
        }

        Write-PpoObfuscateStatus -Result $result

        if (-not [string]::IsNullOrWhiteSpace($OutputFilePath)) {
            try {
                $outDir = Split-Path -Parent $OutputFilePath
                if (-not [string]::IsNullOrWhiteSpace($outDir) -and -not (Test-Path -LiteralPath $outDir)) {
                    New-Item -ItemType Directory -Path $outDir -Force | Out-Null
                }
                [System.IO.File]::WriteAllText($OutputFilePath, $outputCode, [System.Text.UTF8Encoding]::new($false))
            }
            catch {
                throw ("Cannot create output file ""{0}"". {1}" -f $OutputFilePath, $_.Exception.Message)
            }

            Write-Host ("[i] obfuscated file saved to ""{0}""" -f $OutputFilePath)
        }

        if ($PSCmdlet.ParameterSetName -eq 'Source' -and [string]::IsNullOrWhiteSpace($OutputFilePath)) {
            return $outputCode
        }

        if ($PSCmdlet.ParameterSetName -eq 'Source' -and -not [string]::IsNullOrWhiteSpace($OutputFilePath)) {
            return $outputCode
        }
    }
}

Export-ModuleMember -Function @(
    'Invoke-PowerShellProObfuscate',
    'Connect-PowerShellProObfuscator'
)