A Secret Scripting Solution
This month, I'm going to skip the typical wrap-up issue. I have a few general items, but mostly I want to focus on a solution to last month's scripting challenge.
Subscription Management
While I truly appreciate everyone who subscribes to this newsletter, I recognize that circumstances change. If you need to manage you subscription, either cancelling or upgrading, look for links in the footer of every email.

Until Buttondown, the newsletter service, adds a self-service subscription management page, this is the best way to manage your subscription. If you are a premium subscriber who needs to cancel, definitely use the link so that connection to Stripe is properly handled. I can manually change and cancel subscriptions, but it's much easier for everyone if you use the processes via the links, especially for premium subscriptions.
PowerShell+DevOps Global Summit
We're a little over a month away from the PowerShell+DevOps Global Summit. I'll be helping to run the OnRamp program and will also be presenting in the main track. This is the event for PowerShell professionals. Check out the list of speakers and sessions. This year may be the last time Don Jones and I will be at the same event. Jeffrey Snover is also making an increasingly rare appearance. Ticket sales are always limited so don't wait too long to register.
Scripting Challenge Solution
Last month I gave you a PowerShell scripting challenge.
Given a block of text, write a set of PowerShell functions to obfuscate and de-obfuscate the text by shifting characters in the text a fixed number of places, either positive or negative. The functions should accept pipeline input.
I even gave you an encoded sample text to work with. Let's look at a few ways you might tackle the problem. Remember, the challenge itself is irrelevant. It's what you learn from the process that matters.
Let's define a sample string of text.
$text = "I, AM the walrus!"
PS C:\> $text.ToCharArray() | Select -first 5
I
,
A
M
You may be wondering about the ToCharArray()
method. Every string character has a corresponding [char]
equivalent. The [char]
value is a integer representation of the character.
PS C:\> [char]$a = "a"
PS C:\> $a
a
PS C:\> $a.GetType().FullName
System.Char
PS C:\> $a -as [int]
97
PS C:\> [char]97
a
PS C:\> [char]98
b
PS C:\> [int]$a+3
100
PS C:\> [int]$a+3 -as [char]
d
PowerShell automatically presents [char]
values as their string values.
PS C:\> [int[]]($text.ToCharArray()) | Select -First 5
73
44
32
65
77
So when I speak of shifting characters, I'm referring to shifting the integer values of the characters.
$shift = 4
I'll shift each [char]
value by 4.
$shifted = [int[]]$text.ToCharArray() | ForEach-Object {$_ + $shift}
The variable $shifted
now contains the shifted integer values. I can turn them back into an array of [char]
values and join them back into a string.
PS C:\> $out = [char[]]$shifted -join ''
PS C:\> $out
B%:Fma^pZeknl␦
Assuming you know the shift value, you can easily reverse the process. Note that I am subtracting the shift value.
PS C:\> $shifted = [int[]]$out.ToCharArray() | ForEach-Object {$_ - $shift}
PS C:\> $in = [char[]]$shifted -join ''
PS C:\> $in
I, AM the walrus!
How about adding a little complexity to the process? Let's also reverse the string after shifting. I know this wasn't part of the original challenge, but I always like to push boundaries.
PS C:\> $text = "I, AM the walrus too!"
PS C:\> $out = ($text.ToCharArray().foreach({([int]$_ + $shift )-as [char]})) -join ''
PS C:\> $out
B%:Fma^pZeknlmhh␦
Since every string is technically an array of characters, I can build a new string be getting each element starting from the end of the array and joining them together.
PS C:\> $revOut= $out[-1..-$out.length] -join ''
PS C:\> $revout
␦hhmlnkeZp^amF:%B
Do you have an idea on how to reverse the process?
PS C:\> $revIn = ($revOut)[-1..-$revOut.length] -join ''
PS C:\> ($revIn.ToCharArray().foreach({([int]$_ - $shift )-as [char]})) -join ''
I, AM the walrus too!
It shouldn't be much work to wrap these steps into a set of PowerShell functions.
System.Text.Encoding
However, I want to explore another approach using the System.Text.Encoding
class. This class allows you to convert strings into an array of bytes.
$b = [System.Text.Encoding]::UTF8.GetBytes($Text)
The bytes value should correspond to the integer [char]
values. This means I can shift the bytes and convert them back to a string.
#shift 4
$c = $b | foreach {$_+4}
#convert to a string
$d = [System.Text.Encoding]::UTF8.GetChars($c) -join ''
The variable $d
should contain the shifted string, M0$EQ$xli${epvyw$xss%
. To go back to the original string, I can reverse the process.
$e = [System.Text.Encoding]::UTf8.GetBytes($d)
#shift the bytes by subtracting the shifted value
$f = $e | foreach {$_ -4}
#create a string
$out = [System.Text.Encoding]::UTf8.GetChars($f) -join ''
The variable $out
should contain the original string, I, AM the walrus!
.
If I want to reverse the string, here's another approach that I think is even simpler. The variable $c
contains the shifted bytes of the original string. I'm going to add them to a generic list.
$list = [System.Collections.Generic.List[byte]]::new()
$list.AddRange([byte[]]$c)
Now for the cool part. Look how easy it is to reverse the list.
$list.Reverse()
I can convert the list back to an array of characters and join them into a string.
$d = [System.Text.Encoding]::UTF8.GetChars($list) -join ''
# %ssx$wyvpe{$ilx$QE$0M
It is just as easy to convert the reversed string back to the original.
$e = [System.Text.Encoding]::UTf8.GetBytes($d)
#reverse
$list = [System.Collections.Generic.List[byte]]::new()
$list.AddRange([byte[]]$e)
$list.Reverse()
#shift the bytes
$f = $list | foreach {$_ -4}
#create a string
$plaintext = [System.Text.Encoding]::UTf8.GetChars($f) -join ''
# I, AM the walrus too!
Here are the functions I built around these ideas.
Function ConvertTo-SecretText {
[cmdletbinding()]
Param(
[Parameter(Position = 0, ValueFromPipeline, Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$Text,
[ValidateRange(-20,20)]
[int]$Shift = 5,
[switch]$Reverse
)
Process {
Write-Verbose "Text: $Text"
Write-Information $PSBoundParameters -tag parameters
Write-Information "Shift: $shift" -tag parameters
$list = [System.Collections.Generic.List[byte]]::new()
$ShiftedBytes = [System.Text.Encoding]::UTF8.GetBytes($Text) |
ForEach-Object {$_ + $Shift}
#verify shifted bytes are in an acceptable range
$Test = $ShiftedBytes | Where-Object {$_ -gt 126 -OR $_ -lt 10}
If ($Test.count -gt 0) {
Write-Warning "Shifted bytes are out of range for this Text. Please choose a different shift value."
}
else {
Write-Information -MessageData @{Shifted = $shiftedBytes} -tag data
$list.AddRange([byte[]]$ShiftedBytes)
if ($Reverse) {
$list.Reverse()
}
Write-Information -MessageData @{List = $list} -tag data
[System.Text.Encoding]::UTF8.GetString($list) -join ''
}
}
}
Function ConvertFrom-SecretText {
[cmdletbinding()]
Param(
[Parameter(Position = 0, ValueFromPipeline, Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$Text,
[ValidateRange(-20,20)]
[int]$Shift = 5,
[switch]$Reverse
)
Process {
Write-Verbose "Text: $Text"
Write-Information $PSBoundParameters -tag parameters
Write-Information "Shift: -$shift" -tag parameters
$list = [System.Collections.Generic.List[byte]]::new()
$ShiftedBytes = [System.Text.Encoding]::UTF8.GetBytes($Text) |
ForEach-Object {$_ -(+ $Shift)}
Write-Information -MessageData @{Shifted=$shiftedBytes}
Try {
$list.AddRange([byte[]]$ShiftedBytes)
if ($Reverse) {
$list.Reverse()
}
Write-Information -MessageData @{List=$list} -tag data
#convert back to a string
[System.Text.Encoding]::UTF8.GetString($list) -join ''
}
Catch {
Write-Warning "Shifted bytes are out of range for this Text. Please choose a different shift value."
}
}
}
One thing I am taking into account in the functions that I didn't mention earlier, is that there is a limit to how far you can shift. If you shift too far, you'll end up with non-printable characters. I'm adding a limit and displaying a warning if the shift value is too great. Take note that I'm also using Write-Information
in the functions to capture details when running the commands.
Let's try them out.
PS C:\> $s = ConvertTo-SecretText -Text "PowerShell is a career-focused force multiplier" -Shift 6 -InformationVariable iv
PS C:\> $s
Vu}kxYnkrr&oy&g&igxkkx3lui{ykj&luxik&s{rzovrokx
PS C:\> $iv
System.Management.Automation.PSBoundParametersDictionary
Shift: 6
System.Collections.Hashtable
System.Collections.Hashtable
PS C:\> $iv[0].MessageData
Key Value
--- -----
Text PowerShell is a career-focused force multiplier
Shift 6
InformationVariable iv
PS C:\> $iv[2].MessageData
Name Value
---- -----
Shifted {86, 117, 125, 107…}
Since I know the original shifted value, I can easily reveal the secret.
PS C:\> $s | ConvertFrom-SecretText -Shift 6 -Verbose
VERBOSE: Text: Vu}kxYnkrr&oy&g&igxkkx3lui{ykj&luxik&s{rzovrokx
PowerShell is a career-focused force multiplier
The secret text I shared last month can be decoded using the functions or the techniques I shared earlier. Since you wouldn't know the shifted value, you could try a range of possible values.
$secret = Get-Content Drop:\behind-the-pipeline\secret.txt
-10..10 | foreach {
write-host $_ -ForegroundColor magenta
ConvertFrom-SecretText -Text $secret -Shift $_
}

It looks like -7 is the magic number.
PS C:\> $secret | ConvertFrom-SecretText -Shift -7
Every action you take in PowerShell occurs within the context of objects.
As data moves from one command to the next, it moves as one or more
identifiable objects. An object, then, is a collection of data that
represents an item. An object is made up of three types of data: the
objects type, its methods, and its properties.
Summary
How did you do? I hope you learned a new tidbit of PowerShell from my solutions that you can apply to your work. If you have questions or comments about the challenge or my solutions, please feel free to leave a comment on the web archive.