StringEncrypt.psm1

################################################################################
#
# StringEncrypt Web API client for PowerShell.
# Encrypt strings and emit language-specific decryptors.
#
# Version : PowerShell SDK v1.0.0
# PowerShell : Windows PowerShell 5.1 / PowerShell 7+
# Author : Bartosz Wójcik (support@pelock.com)
# Project : https://www.stringencrypt.com
# Homepage : https://www.pelock.com
#
################################################################################

Set-StrictMode -Version Latest

if ($PSVersionTable.PSVersion.Major -lt 6) {
    [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
}

Add-Type -AssemblyName System.IO.Compression | Out-Null

$script:SeApiUrl = 'https://www.stringencrypt.com/api.php'
$script:SeUserAgent = 'pelock/stringencrypt (+https://www.stringencrypt.com)'
$script:SeTimeoutSec = 3600

enum Command {
    encrypt
    is_demo
    info
}

enum Language {
    cpp
    csharp
    vbnet
    delphi
    java
    js
    python
    ruby
    autoit
    powershell
    haskell
    masm
    fasm
    go
    rust
    swift
    kotlin
    lua
    dart
    php
    objc
    nasm
}

enum NewLine {
    lf
    crlf
    cr
}

class ErrorCode {
    static [int] $SUCCESS = 0
    static [int] $EMPTY_LABEL = 1
    static [int] $LENGTH_LABEL = 2
    static [int] $EMPTY_STRING = 3
    static [int] $EMPTY_BYTES = 4
    static [int] $EMPTY_INPUT = 5
    static [int] $LENGTH_STRING = 6
    static [int] $INVALID_LANG = 7
    static [int] $INVALID_LOCALE = 8
    static [int] $CMD_MIN = 9
    static [int] $CMD_MAX = 10
    static [int] $LENGTH_BYTES = 11
    static [int] $DEMO = 100
}

function ConvertTo-SeFormValue {
    param($Value)

    if ($Value -is [bool]) {
        if ($Value) { return '1' }
        return '0'
    }

    if ($Value -is [Enum]) {
        return $Value.ToString()
    }

    return [string]$Value
}

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

    $parts = foreach ($key in $Fields.Keys) {
        $name = [uri]::EscapeDataString([string]$key)
        $raw = $Fields[$key]
        if ($raw -is [byte[]]) {
            $value = ($raw | ForEach-Object { '%{0:X2}' -f $_ }) -join ''
            '{0}={1}' -f $name, $value
        }
        else {
            $value = [uri]::EscapeDataString((ConvertTo-SeFormValue -Value $raw))
            '{0}={1}' -f $name, $value
        }
    }

    return ($parts -join '&')
}

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

    $data = [Convert]::FromBase64String($CompressedBase64)
    if ($data.Length -lt 6) {
        return $null
    }

    $payloadLength = $data.Length - 6
    $ms = New-Object System.IO.MemoryStream($data, 2, $payloadLength)
    try {
        $deflate = New-Object System.IO.Compression.DeflateStream($ms, [System.IO.Compression.CompressionMode]::Decompress)
        try {
            $reader = New-Object System.IO.StreamReader($deflate, [System.Text.Encoding]::UTF8)
            try {
                return $reader.ReadToEnd()
            }
            finally {
                $reader.Dispose()
            }
        }
        finally {
            $deflate.Dispose()
        }
    }
    finally {
        $ms.Dispose()
    }
}

class StringEncrypt {
    static [string] $DEFAULT_API_URL = 'https://www.stringencrypt.com/api.php'

    hidden [bool] $_preferCurl = $false
    hidden [bool] $_decompressEncryptSource = $true
    hidden [object] $_command = $null
    hidden [string] $_apiKey = ''
    hidden [string] $_label = 'Label'
    hidden [string] $_inputString = $null
    hidden [byte[]] $_inputBytes = $null
    hidden [bool] $_compression = $false
    hidden [Language] $_language = [Language]::php
    hidden [object] $_highlight = $false
    hidden [int] $_cmdMin = 1
    hidden [int] $_cmdMax = 3
    hidden [bool] $_local = $false
    hidden [bool] $_unicode = $true
    hidden [string] $_langLocale = 'en_US.utf8'
    hidden [string] $_ansiEncoding = 'WINDOWS-1250'
    hidden [NewLine] $_newLines = [NewLine]::lf
    hidden [string] $_template = $null
    hidden [bool] $_returnTemplate = $false
    hidden [bool] $_includeTags = $false
    hidden [bool] $_includeExample = $false
    hidden [bool] $_includeDebugComments = $false

    StringEncrypt() {
        $this.Initialize('', $false)
    }

    StringEncrypt([string]$ApiKey) {
        $this.Initialize($ApiKey, $false)
    }

    StringEncrypt([string]$ApiKey, [bool]$PreferCurl) {
        $this.Initialize($ApiKey, $PreferCurl)
    }

    hidden [void] Initialize([string]$ApiKey, [bool]$PreferCurl) {
        $this._apiKey = $ApiKey
        $this._preferCurl = $PreferCurl
        $this.Reset()
        $this._label = 'Label'
    }

    [object] IsDemo() {
        $previous = $this._command
        $this.SetCommand([Command]::is_demo) | Out-Null
        $result = $this.Send()
        $this._command = $previous
        return $result
    }

    [object] EncryptFileContents([string]$FilePath, [string]$Label) {
        if (-not (Test-Path -LiteralPath $FilePath -PathType Leaf)) {
            return $false
        }

        try {
            $raw = [System.IO.File]::ReadAllBytes((Resolve-Path -LiteralPath $FilePath).Path)
        }
        catch {
            return $false
        }

        if ($null -eq $raw -or $raw.Length -eq 0) {
            return $false
        }

        $savedCommand = $this._command
        $savedString = $this._inputString
        $savedBytes = $this._inputBytes
        $savedLabel = $this._label

        $this.SetCommand([Command]::encrypt).SetBytes($raw).SetLabel($Label) | Out-Null
        $result = $this.Send()

        $this._command = $savedCommand
        $this._inputString = $savedString
        $this._inputBytes = $savedBytes
        $this._label = $savedLabel

        return $result
    }

    [object] EncryptString([string]$String, [string]$Label) {
        $savedCommand = $this._command
        $savedString = $this._inputString
        $savedBytes = $this._inputBytes
        $savedLabel = $this._label

        $this.SetCommand([Command]::encrypt).SetString($String).SetLabel($Label) | Out-Null
        $result = $this.Send()

        $this._command = $savedCommand
        $this._inputString = $savedString
        $this._inputBytes = $savedBytes
        $this._label = $savedLabel

        return $result
    }

    [bool] GetPreferCurl() { return $this._preferCurl }
    [StringEncrypt] SetPreferCurl([bool]$PreferCurl) { $this._preferCurl = $PreferCurl; return $this }

    [bool] GetDecompressEncryptSource() { return $this._decompressEncryptSource }
    [StringEncrypt] SetDecompressEncryptSource([bool]$DecompressEncryptSource) {
        $this._decompressEncryptSource = $DecompressEncryptSource
        return $this
    }

    [object] GetCommand() { return $this._command }
    [StringEncrypt] SetCommand([Command]$Command) { $this._command = $Command; return $this }

    [string] GetLabel() { return $this._label }
    [StringEncrypt] SetLabel([string]$Label) { $this._label = $Label; return $this }

    [StringEncrypt] SetString([string]$String) {
        $this._inputString = $String
        $this._inputBytes = $null
        return $this
    }

    [StringEncrypt] SetBytes([byte[]]$Bytes) {
        $this._inputBytes = $Bytes
        $this._inputString = $null
        return $this
    }

    [bool] GetCompression() { return $this._compression }
    [StringEncrypt] SetCompression([bool]$Compression) { $this._compression = $Compression; return $this }

    [Language] GetLanguage() { return $this._language }
    [StringEncrypt] SetLanguage([Language]$Language) { $this._language = $Language; return $this }

    [object] GetHighlight() { return $this._highlight }
    [StringEncrypt] SetHighlight($Highlight) { $this._highlight = $Highlight; return $this }

    [int] GetCmdMin() { return $this._cmdMin }
    [StringEncrypt] SetCmdMin([int]$CmdMin) { $this._cmdMin = $CmdMin; return $this }

    [int] GetCmdMax() { return $this._cmdMax }
    [StringEncrypt] SetCmdMax([int]$CmdMax) { $this._cmdMax = $CmdMax; return $this }

    [bool] GetLocal() { return $this._local }
    [StringEncrypt] SetLocal([bool]$Local) { $this._local = $Local; return $this }

    [bool] GetUnicode() { return $this._unicode }
    [StringEncrypt] SetUnicode([bool]$Unicode) { $this._unicode = $Unicode; return $this }

    [string] GetLangLocale() { return $this._langLocale }
    [StringEncrypt] SetLangLocale([string]$LangLocale) { $this._langLocale = $LangLocale; return $this }

    [string] GetAnsiEncoding() { return $this._ansiEncoding }
    [StringEncrypt] SetAnsiEncoding([string]$AnsiEncoding) { $this._ansiEncoding = $AnsiEncoding; return $this }

    [NewLine] GetNewLines() { return $this._newLines }
    [StringEncrypt] SetNewLines([NewLine]$NewLines) { $this._newLines = $NewLines; return $this }

    [string] GetTemplate() { return $this._template }
    [StringEncrypt] SetTemplate([string]$Template) { $this._template = $Template; return $this }

    [bool] GetReturnTemplate() { return $this._returnTemplate }
    [StringEncrypt] SetReturnTemplate([bool]$ReturnTemplate) { $this._returnTemplate = $ReturnTemplate; return $this }

    [bool] GetIncludeTags() { return $this._includeTags }
    [StringEncrypt] SetIncludeTags([bool]$IncludeTags) { $this._includeTags = $IncludeTags; return $this }

    [bool] GetIncludeExample() { return $this._includeExample }
    [StringEncrypt] SetIncludeExample([bool]$IncludeExample) { $this._includeExample = $IncludeExample; return $this }

    [bool] GetIncludeDebugComments() { return $this._includeDebugComments }
    [StringEncrypt] SetIncludeDebugComments([bool]$IncludeDebugComments) {
        $this._includeDebugComments = $IncludeDebugComments
        return $this
    }

    [StringEncrypt] Reset() {
        $this._command = $null
        $this._label = '$label'
        $this._inputString = $null
        $this._inputBytes = $null
        $this._compression = $false
        $this._language = [Language]::php
        $this._highlight = $false
        $this._cmdMin = 1
        $this._cmdMax = 3
        $this._local = $false
        $this._unicode = $true
        $this._langLocale = 'en_US.utf8'
        $this._ansiEncoding = 'WINDOWS-1250'
        $this._newLines = [NewLine]::lf
        $this._template = $null
        $this._returnTemplate = $false
        $this._includeTags = $false
        $this._includeExample = $false
        $this._includeDebugComments = $false
        return $this
    }

    [hashtable] ToRequestArray() {
        if ($null -eq $this._command) {
            throw 'Command must be set (use SetCommand()).'
        }

        switch ([string]$this._command) {
            'info' { return $this.BuildInfoParams() }
            'is_demo' { return $this.BuildIsDemoParams() }
            'encrypt' { return $this.BuildEncryptParams() }
            default { throw 'Command must be set (use SetCommand()).' }
        }

        throw 'Command must be set (use SetCommand()).'
    }

    [object] Send() {
        return $this.Send($null)
    }

    [object] Send([object]$UseCurl) {
        $params = $this.ToRequestArray()
        $body = ConvertTo-SeFormBody -Fields $params
        $headers = @{ 'User-Agent' = $script:SeUserAgent }

        try {
            $result = Invoke-RestMethod -Uri $script:SeApiUrl -Method Post -Body $body `
                -ContentType 'application/x-www-form-urlencoded' -Headers $headers `
                -TimeoutSec $script:SeTimeoutSec
        }
        catch {
            return $false
        }

        if ($null -eq $result) {
            return $false
        }

        return $this.ApplyDecryptorSourceDecompression($result)
    }

    hidden [object] ApplyDecryptorSourceDecompression($Response) {
        if ([string]$this._command -ne 'encrypt' -or -not $this._compression -or -not $this._decompressEncryptSource) {
            return $Response
        }

        $errorProperty = $Response.PSObject.Properties['error']
        if ($null -eq $errorProperty -or [int]$errorProperty.Value -ne [ErrorCode]::SUCCESS) {
            return $Response
        }

        $sourceProperty = $Response.PSObject.Properties['source']
        if ($null -eq $sourceProperty -or [string]::IsNullOrEmpty([string]$sourceProperty.Value)) {
            return $Response
        }

        try {
            $plain = Expand-SeZlib -CompressedBase64 ([string]$sourceProperty.Value)
            if ($null -ne $plain) {
                $Response.source = $plain
            }
        }
        catch {
        }

        return $Response
    }

    hidden [hashtable] BuildInfoParams() {
        return @{
            command = 'info'
            code    = $this._apiKey
        }
    }

    hidden [hashtable] BuildIsDemoParams() {
        return @{
            command = 'is_demo'
            code    = $this._apiKey
        }
    }

    hidden [hashtable] BuildEncryptParams() {
        $p = @{
            command                 = 'encrypt'
            code                    = $this._apiKey
            label                   = $this._label
            compression             = $this._compression
            lang                    = [string]$this._language
            cmd_min                 = $this._cmdMin
            cmd_max                 = $this._cmdMax
            local                   = $this._local
            unicode                 = $this._unicode
            lang_locale             = $this._langLocale
            ansi_encoding           = $this._ansiEncoding
            new_lines               = [string]$this._newLines
            return_template         = $this._returnTemplate
            include_tags            = $this._includeTags
            include_example         = $this._includeExample
            include_debug_comments  = $this._includeDebugComments
        }

        if ($null -ne $this._inputString) {
            $p['string'] = $this._inputString
        }
        elseif ($null -ne $this._inputBytes) {
            $p['bytes'] = $this._inputBytes
        }

        if ($this._highlight -ne $false) {
            $p['highlight'] = $this._highlight
        }

        if ($null -ne $this._template) {
            $p['template'] = $this._template
        }

        return $p
    }
}

function New-StringEncrypt {
    <#
    .SYNOPSIS
        Creates a StringEncrypt Web API client.

    .PARAMETER ApiKey
        Activation key. Empty string runs demo mode.

    .EXAMPLE
        $client = New-StringEncrypt -ApiKey 'YOUR-API-KEY-HERE'
        $result = $client.EncryptString('Hello!', '$label')
    #>

    [CmdletBinding()]
    [OutputType([StringEncrypt])]
    param(
        [string]$ApiKey
    )

    if ($null -eq $ApiKey) {
        return [StringEncrypt]::new('')
    }

    return [StringEncrypt]::new($ApiKey)
}

Export-ModuleMember -Function @('New-StringEncrypt')