Created
April 19, 2026 06:57
-
-
Save scriptingstudio/9208ef185073f3cb4a18c713c0e58229 to your computer and use it in GitHub Desktop.
Experimental encryption module for simple cases.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .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