Skip to content

Instantly share code, notes, and snippets.

@scriptingstudio
Created April 19, 2026 06:57
Show Gist options
  • Select an option

  • Save scriptingstudio/9208ef185073f3cb4a18c713c0e58229 to your computer and use it in GitHub Desktop.

Select an option

Save scriptingstudio/9208ef185073f3cb4a18c713c0e58229 to your computer and use it in GitHub Desktop.
Experimental encryption module for simple cases.
<#
.SYNOPSIS
Encrypt/decript strings.
.DESCRIPTION
AIO encryption dotsource mini-module.
The module supports AES and DPAPI encryption methods.
.FUNCTIONALITY
Encryption method: Protect-String
Decryption method: UnProtect-String
Data method: Get-CipherData
Configuration method: Set-AESKeyConfig
Configuration method: Set-MasterPassword
Configuration method: Get-MasterPassword
.OUTPUTS
System.String
.NOTES
- Before using the module System.Security assembly should be preloaded
- Format of encrypted Base64 string: A|D<encrypted_data>?<encrypted_id>
.LINK
- https://github.com/grey0ut/ProtectStrings
- https://learn.microsoft.com/en-us/dotnet/standard/security/cryptographic-services
#>
Class ProtectedString {
[string] $Encryption
[string] $CipherText
hidden [string] $DPAPIIdentity
hidden [System.Security.Cryptography.DataProtectionScope] $DPAPIScope
ProtectedString([string]$Encryption, [string]$CipherText) {
$this.Encryption = $Encryption
$this.CipherText = $CipherText
$this.DPAPIIdentity = $null
$this.DPAPIScope = 'CurrentUser'
} # end contructor
ProtectedString([string]$Protected) {
if (-not $Protected) {return}
switch ($Protected[0]) {
'A' {
$this.Encryption = 'AES'
$this.CipherText = $Protected.SubString(1)
$this.DPAPIIdentity = $null
}
'D' {
$this.CipherText,$DPAPIIdB64 = $Protected.SubString(1).split('?')
#$this.DPAPIScope
$DPAPIIdBytes = [System.Convert]::FromBase64String($DPAPIIdB64)
$this.DPAPIIdentity = [System.Text.Encoding]::UTF8.GetString($DPAPIIdBytes)
$this.Encryption = 'DPAPI'
}
default {
throw "ERROR: Does not appear to be a ProtectedString signature."
}
}
} # end contructor
[string] static Protect([string]$InputString, [string] $EncryptionType, [System.Security.Cryptography.DataProtectionScope]$Scope, $AESKey) {
if ($EncryptionType -eq 'DPAPI') {
$StringBytes = [System.Text.Encoding]::UTF8.GetBytes($InputString)
$DPAPIBytes = [System.Security.Cryptography.ProtectedData]::Protect($StringBytes, $null, $Scope)
$ConvertedString = [System.Convert]::ToBase64String($DPAPIBytes)
$Identity = '{0}\{1}' -f $env:COMPUTERNAME,$env:USERNAME
$jsonBytes = [System.Text.Encoding]::UTF8.GetBytes($Identity)
$EncodedId = [System.Convert]::ToBase64String($jsonBytes)
return 'D{0}?{1}' -f $ConvertedString, $EncodedId # seal output
#return 'D{0}?{1}?{2}' -f $ConvertedString, $Scope, $EncodedId
}
elseif ($EncryptionType -eq 'AES') {
$InitializationVector = [System.Byte[]]::new(16)
$RNG = [System.Security.Cryptography.RandomNumberGenerator]::Create()
$RNG.GetBytes($InitializationVector)
$AESProvider = [System.Security.Cryptography.AesCryptoServiceProvider]::Create()
$AESProvider.Key = $AESKey
$AESProvider.IV = $InitializationVector
$ClearTextBytes = [System.Text.Encoding]::UTF8.GetBytes($InputString)
$Encryptor = $AESProvider.CreateEncryptor()
$EncryptedBytes = $Encryptor.TransformFinalBlock($ClearTextBytes, 0, $ClearTextBytes.Length)
[byte[]]$FullData = $AESProvider.IV + $EncryptedBytes
$AESProvider.Dispose()
return 'A{0}' -f ([System.Convert]::ToBase64String($FullData)) # seal output
}
else {return ""}
} # end Protect
[string] static UnProtect([string]$InputString, $AESKey) {
if ($InputString[0] -eq 'A') {
try {
$AESProvider = [System.Security.Cryptography.AesCryptoServiceProvider]::Create()
$AESProvider.Key = $AESKey
$EncryptedBytes = [System.Convert]::FromBase64String($InputString.SubString(1))
$AESProvider.IV = $EncryptedBytes[0..15]
$UnencryptedBytes = $AESProvider.CreateDecryptor().TransformFinalBlock($EncryptedBytes, 16, $EncryptedBytes.Length - 16)
$AESProvider.Dispose()
return [System.Text.Encoding]::UTF8.GetString($UnencryptedBytes)
} catch {
Write-Warning "Failed to decrypt. Incorrect AES key. Check your Master Password."
return ''
}
}
elseif ($InputString[0] -eq 'D') {
try {
$encrypted,$DPAPIIdB64 = $InputString.SubString(1).split('?') # unseal input
$DPAPIBytes = [System.Convert]::FromBase64String($encrypted)
$DecryptedBytes = [System.Security.Cryptography.ProtectedData]::UnProtect($DPAPIBytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser) # $Scope
return [System.Text.Encoding]::UTF8.GetString($DecryptedBytes)
} catch {
Write-Warning "Unable to decrypt as this user/machine on this machine."
return ''
}
}
else {return ''}
} # end UnProtect
[object] static ConvertToAESKey([SecureString]$SecureStringInput, [bool]$ByteArray, $AESKeyConfig) {
# Test integrity
if ($null -eq $SecureStringInput -or $SecureStringInput.Length -eq 0) {return $null}
$Settings = $AESKeyConfig
# Temporarily plaintext our SecureStringInput
$Plaintext = [ProtectedString]::FromSecureString($SecureStringInput) # done!
$SaltBytes = [System.Convert]::FromBase64String($Settings.Salt)
$PBKDF2 = [Security.Cryptography.Rfc2898DeriveBytes]::new($Plaintext, $SaltBytes, $Settings.Iterations, $Settings.Algorithm)
# Generate an AES Key
$Key = $PBKDF2.GetBytes(32)
$SecureAESKey = if ($ByteArray) {return $Key} else {
ConvertTo-SecureString -String ([System.BitConverter]::ToString($Key)) -AsPlainText -Force
}
$HexString = [ProtectedString]::FromSecureString($SecureAESKey) # done!
if (-not $HexString) {return $null}
$normalized = $HexString -replace '\b0x\B|\\\x78|[\-\,:]' -replace '^:+|:+$|[x\\]'
return [regex]::matches($normalized, '(..)').foreach({[System.Convert]::ToByte($_.value,16)})
} # end ConvertToAESKey
# There is no -AsPlainText parameter in Windows Powershell's ConvertFrom-SecureString
[string] static FromSecureString([SecureString]$secstring) {
if ($null -eq $secstring -or $secstring.Length -eq 0) {return ""}
$BSTR = $null
try {
$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secstring)
return [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($BSTR)
} finally {
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR)
}
} # end FromSecureString
# Binary source data
[byte[]] static ToByte([string]$Protected) {
if ($Protected[0] -eq 'A') {
return [System.Convert]::FromBase64String($Protected.Substring(1))
}
elseif ($Protected[0] -eq 'D') {
return [System.Convert]::FromBase64String($Protected.SubString(1).split('?')[0])
}
else {
Write-Warning "ERROR: Does not appear to be a ProtectedString signature."
return $null
}
} # end ToByte
} # end ProtectedString class
function Protect-String {
[cmdletbinding()]
[alias('ptst')]
param (
[Parameter(Mandatory,ValueFromPipeline,Position = 0)]
[string[]] $InputString,
[ValidateSet('DPAPI','AES')]
[string] $Encryption = 'AES',
[System.Security.Cryptography.DataProtectionScope] $Scope = 'CurrentUser'
)
begin {
if (-not $Encryption) {$Encryption = 'AES'}
if ($Encryption -eq 'DPAPI') {
if ($isMacos -or $islinux -or ('@' | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString).Length -lt 100) {
throw "The operating system doesn't support DPAPI encryption. Please use AES instead which is default."
}
}
Initialize-RTConfig
$AESKey = if ($Encryption -eq 'AES') {
[ProtectedString]::ConvertToAESKey($ProtectedStringConfig.MasterPassword,$false, $ProtectedStringConfig.AESKeyConfig)
}
} # begin
process {
$InputString | ForEach-Object {
if (-not $_) {return}
try {
[ProtectedString]::Protect($_,$Encryption,$Scope,$AESKey)
} catch {
Write-Error $_
}
}
} # process
} # END Protect-String
function UnProtect-String {
[cmdletbinding()]
[alias('upst')]
param (
[Parameter(Mandatory,ValueFromPipeline,Position = 0)]
[string[]] $InputString
)
begin {Initialize-RTConfig}
process {
$InputString | ForEach-Object {
if (-not $_) {return}
if ($_[0] -notmatch '^[AD]') {
Write-Warning 'Input string could not be converted to a ProtectedString Object. Verify that it was produced by Protect-String.'
return
}
$AESKey = if ($_[0] -eq 'A') {
[ProtectedString]::ConvertToAESKey($ProtectedStringConfig.MasterPassword,$false, $ProtectedStringConfig.AESKeyConfig)
}
[ProtectedString]::UnProtect($_,$AESKey)
}
} # process
} # END UnProtect-String
function Set-AESKeyConfig {
[cmdletbinding(DefaultParameterSetName = 'SaltString')]
[alias('sakc')]
param (
[Parameter(Mandatory,Position = 0,ParameterSetName = "SaltString")]
[string] $SaltString,
[Parameter(Mandatory,Position = 0,ParameterSetName = "SaltBytes")]
[ValidateCount(16,256)]
[byte[]] $SaltBytes,
[alias('init')][switch] $Reset,
[int] $Iterations,
[ValidateSet('MD5','SHA1','SHA256','SHA384','SHA512')]
[alias('hash')][string] $Algorithm
)
Initialize-RTConfig
if ($Reset -or $null -eq $script:ProtectedStringConfig.AESKeyConfig -or $script:ProtectedStringConfig.AESKeyConfig.count -eq 0) {
$script:ProtectedStringConfig['AESKeyConfig'] = @{
Salt = $script:ProtectedStringConfig.defaultSalt
Iterations = 60000
Algorithm = 'SHA256'
}
}
$Settings = $script:ProtectedStringConfig.AESKeyConfig
if ($SaltString) {
if ([System.Text.Encoding]::UTF8.GetBytes($SaltString).Count -lt 16) {
Write-Warning "Salt must be at least 16 bytes in length. Reset to default."
$Settings.Salt = $defaultSalt
} else {
try {
$Settings.Salt = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($SaltString))
} catch {
throw $_
}
}
}
elseif ($SaltBytes) {
try {
$Settings.Salt = [System.Convert]::ToBase64String($SaltBytes)
} catch {
throw $_
}
}
if ($Iterations -gt 1) {$Settings.Iterations = $Iterations}
if ($Algorithm) {$Settings.Algorithm = $Algorithm}
} # END Set-AESKeyConfig
function Initialize-RTConfig {
if ($null -eq $script:ProtectedStringConfig) {
$script:ProtectedStringConfig = @{AESKeyConfig=@{}}
}
$cfg = $script:ProtectedStringConfig
if (-not $cfg.privateKey) {
$cfg['privateKey'] = 'Sharaboonda-Marte-[326]&<Kirgudoo-Bambarbia>+[dwapara/326]&tRump-NAZIpid0R&Zelia-deadf@shist'
}
if ($null -eq $cfg.masterPassword) {
$cfg['masterPassword'] = ConvertTo-SecureString -String $cfg.privateKey -AsPlainText -Force
}
if (-not $cfg.defaultSalt) {
$cfg['defaultSalt'] = 'fMK9w4HDtMO4d8Oa4pmAw6U+fknCqWvil4Q9w73DscOtw64='
}
if (-not $cfg.AESKeyConfig.Salt) {$cfg.AESKeyConfig['Salt'] = $cfg.defaultSalt}
if (-not $cfg.AESKeyConfig.Iterations) {$cfg.AESKeyConfig['Iterations'] = 60000}
if (-not $cfg.AESKeyConfig.Algorithm) {$cfg.AESKeyConfig['Algorithm'] = 'SHA256'}
} # END Initialize-RTConfig
function Get-RTConfig {
if ($null -eq $script:ProtectedStringConfig -or $script:ProtectedStringConfig.count -eq 0) {
Write-Warning "Runtime Config not created yet. It will automatically create on first run Protect-String function."
} else {
$script:ProtectedStringConfig
}
} # END Get-RTConfig
function Get-AESKeyConfig {
if ($null -eq $script:ProtectedStringConfig -or
-not $script:ProtectedStringConfig.ContainsKey('AESKeyConfig') -or
$script:ProtectedStringConfig.AESKeyConfig.count -eq 0) {
Write-Warning "AESKeyConfig not created yet. It will automatically create on first run Protect-String function."
} else {
$script:ProtectedStringConfig.AESKeyConfig
}
} # END Get-AESKeyConfig
function Set-MasterPassword ([string]$NewPassword) {
if ($NewPassword) {
Initialize-RTConfig
$cfg = $script:ProtectedStringConfig
$cfg['privateKey'] = $NewPassword
$cfg['masterPassword'] = ConvertTo-SecureString -String $NewPassword -AsPlainText -Force
}
} # END Set-MasterPassword
function Get-MasterPassword {
Initialize-RTConfig
[ProtectedString]::FromSecureString($script:ProtectedStringConfig.masterPassword)
} # END Get-MasterPassword
function Get-CipherData ([string]$CipherString) {
if ($CipherString) {[ProtectedString]::ToByte($CipherString)}
} # END Get-CipherData
# Internal helper: auto generator of runtime config
function Initialize-RTConfig {
if ($null -eq $script:ProtectedStringConfig) {
$script:ProtectedStringConfig = @{AESKeyConfig=@{}}
}
$cfg = $script:ProtectedStringConfig
if (-not $cfg.privateKey) {
$cfg['privateKey'] = 'Sharaboonda-Marte-[326]&<Kirgudoo-Bambarbia>+[dwapara/326]&tRump-NAZIpid0R&Zelia-deadf@shist'
}
if ($null -eq $cfg.masterPassword) {
$cfg['masterPassword'] = ConvertTo-SecureString -String $cfg.privateKey -AsPlainText -Force
}
if (-not $cfg.defaultSalt) {
$cfg['defaultSalt'] = 'fMK9w4HDtMO4d8Oa4pmAw6U+fknCqWvil4Q9w73DscOtw64='
}
if (-not $cfg.AESKeyConfig.Salt) {$cfg.AESKeyConfig['Salt'] = $cfg.defaultSalt}
if (-not $cfg.AESKeyConfig.Iterations) {$cfg.AESKeyConfig['Iterations'] = 60000}
if (-not $cfg.AESKeyConfig.Algorithm) {$cfg.AESKeyConfig['Algorithm'] = 'SHA256'}
} # END Initialize-RTConfig
function Remove-RTConfig {
if (Get-Variable ProtectedStringConfig -Scope script -ErrorAction 0) {
$script:ProtectedStringConfig.Clear()
Remove-Variable -Name ProtectedStringConfig -Scope script -Force
}
} # END Remove-RTConfig
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment