Do you have to deal with Base64 format? Then this site is perfect for you! Use our super handy online tool to encode or decode your data.

Base64 Decoding in PowerShell: A Complete Guide

Somewhere in a log line, a config file or an error message, you run into it: a long run of letters and digits with the occasional plus or slash, and one or two equals signs parked suspiciously at the end. It looks like noise. It is not. It is Base64, and you already know what you want: the thing it is hiding.

Base64 is a translation, not a compression and not a lock. It rewrites any sequence of bytes into printable text, four characters per three input bytes (so encoded data runs about 33% larger than the original), using an alphabet of 64 characters plus the equals sign as trailing padding. The home page above walks through the alphabet, the bit math and the variants in full, so this article spends its time where PowerShell makes the difference: the one .NET method you will call, the rules it enforces, and the dozen or so corners of real work where decoding in PowerShell gets interesting.

The Method and Its Contract

PowerShell ships no Base64 cmdlet of its own. The work is done by a method on a .NET class that has been part of the framework since .NET Framework 1.1 in 2003, three years before PowerShell itself shipped:

$bytes = [System.Convert]::FromBase64String("SGVsbG8sIFdvcmxkIQ==")
[System.Text.Encoding]::UTF8.GetString($bytes)
# Hello, World!

That is the whole API: one string in, one byte array out. It works in every PowerShell on every operating system, in Windows PowerShell 5.1 and in PowerShell 7 on Windows, Linux and macOS, because it is simply .NET. The contract is short enough to memorize, so here it is as a table:

Input What you get back
$null An empty array, no error. PowerShell quietly turns $null into an empty string before the call
An empty string An empty array, no error
A valid payload A byte[], never a string, even when the data is text
An invalid payload A FormatException, wrapped for you in a MethodInvocationException

One warning before you write any error handling: that FormatException has a single message that covers three different sins. A character outside the alphabet, more than two padding characters, or a non-whitespace character hiding among the padding all produce exactly the same sentence. When you see it, the message will not tell you which one you committed, so you go back and read your input:

try {
  [System.Convert]::FromBase64String("SGV!G8s=")
}
catch {
  $real = $_.Exception.InnerException
  $real.GetType().Name
  # FormatException
  $real.Message
}

And the multiple-of-four rule has one edge that surprises people the first time they hit it. Four characters without any padding is perfectly valid, it just means the spare bits in the last character get discarded. Three characters is not a multiple of four, and is rejected:

[System.Convert]::FromBase64String("SGVs").Count
# 3: four characters without padding is fine
[System.Convert]::FromBase64String("SGV")
# FormatException: three characters is not a multiple of four

What the Decoder Will and Will Not Accept

The decoder is strict about the alphabet and generous about one specific thing. The valid characters are the 64 Base64 digits (A through Z, a through z, 0 through 9, plus and slash) and the equals sign as trailing padding. Exactly four whitespace characters are ignored wherever and however often they appear: the tab, the line feed, the carriage return and the space. The official .NET documentation lists them by their Unicode names, which tells you this is a documented guarantee and not a lucky accident.

In practice this is a superpower. MIME, the mail encoding that put Base64 on the map, wraps encoded lines at 76 characters, so a payload that traveled through mail, a ticket or a log file will usually arrive broken across many lines. The decoder does not care. Paste it as it is:

$wrapped = "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4gQmFzZTY0IHRleHQg`r`n" +
           "YXJyaXZlcyB3cmFwcGVkIGF0IHNldmVudHktc2l4IGNvbHVtbnMgaW4gbWFpbCwgc28gdGhlIGRl`r`n" +
           "Y29kZXIgbXVzdCBub3QgY2FyZS4="
[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($wrapped))
# The quick brown fox jumps over the lazy dog. Base64 text arrives
# wrapped at seventy-six columns in mail, so the decoder must not care.

Everything else that is not an alphabet character is a hard stop. The most common offenders in the wild are the non-breaking space (the favorite of text pasted out of web pages) and the byte-order mark (the invisible mark that follows you when a file was read with the wrong encoding). Neither is whitespace as far as this method is concerned, so both throw:

try {
  [System.Convert]::FromBase64String("SGVs`u{00A0}G8=")
}
catch {
  $_.Exception.InnerException.GetType().Name
  # FormatException
}

The strictness is deliberate, not crankiness. RFC 4648, the standard that codified Base64 in 2006, says implementations must reject non-alphabet characters unless the protocol explicitly allows leniency, because a decoder that quietly swallows foreign characters can be turned into a covert channel for smuggling data past anything that only inspects the alphabet. The .NET decoder follows the strict rule, and you usually want it to.

A Byte Array Is Not a String

The method stops on purpose at the byte array. What those bytes mean is a second decision that only you can make, and guessing it wrong is the most famous mistake in PowerShell Base64 work. The default assumption, UTF-8, is right for almost everything on the internet, and the round trip is two calls:

$bytes = [System.Convert]::FromBase64String("SGVsbG8sIFdvcmxkIQ==")
[System.Text.Encoding]::UTF8.GetString($bytes)
# Hello, World!

The encodings you will actually reach for, and what each one does when you guess wrong:

Encoding Use it when If you guess wrong
UTF8 Web APIs, JSON, JWTs, modern everything. The safe default Latin-1 or UTF-16 text comes back as mojibake
Unicode (UTF-16LE) The payload came from Windows tooling, a registry value, or a .NET string that was encoded before shipping Every character gets a gap around it, because you read one byte where two were meant
ASCII Classic HTTP Basic credentials and other guaranteed-7-bit protocols Anything above value 127 becomes a question mark
Latin1 Legacy European text that predates UTF-8 Multi-byte UTF-8 sequences split into several wrong letters
Default Almost never. It is the machine's system code page Your script behaves differently on every Windows region setting

The classic failure is UTF-8 text decoded as UTF-16. The bytes are real, the method is happy, and the result is still garbage:

# "SGk=" is the UTF-8 bytes of "Hi"
[System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String("SGk="))
# A single unreadable character: 2 bytes of UTF-8 read as one 2-byte UTF-16 unit

The practical rule: if the decoded text looks like every character has an invisible gap around it, or like a different alphabet, you are one encoding off. Ask where the data was produced, and when in doubt, trust UTF-8 but verify with your eyes on the first few characters. And decide the encoding before you decode, not after the mojibake appears in your log.

base64url: The Alphabet That Plays Nice in URLs

You will meet a cousin of Base64 in every API token, JWT and URL-embedded identifier you will ever touch. Standard Base64's plus and slash are legal in a URL only after percent-encoding, and the equals padding looks like a field separator. So RFC 4648 defined a URL- and filename-safe alphabet: the same 64 characters, except plus becomes hyphen and slash becomes underscore. Padding is usually dropped entirely, because the length of the data makes it unnecessary. The RFC is careful to say this variant should be called base64url and not just "base64", and the rest of this section follows that.

.NET does ship a dedicated class for it, System.Buffers.Text.Base64Url, with fast encode and decode methods. But every one of those methods takes or returns a span, and PowerShell cannot pass spans to .NET methods at all (more on that in the history section). So in PowerShell the practical recipe is the one that works in every version: swap the two characters, and restore the padding before handing the text to the standard decoder. The padding to add is whatever makes the length a multiple of four:

$token = "--__AQI"  # base64url, no padding
$standard = $token.Replace("-", "+").Replace("_", "/")
$pad = 4 - ($standard.Length % 4)
if ($pad -eq 4) { $pad = 0 }
$standard = $standard.PadRight($standard.Length + $pad, "=")
$bytes = [System.Convert]::FromBase64String($standard)
$bytes -join ","
# 251,239,255,1,2

Two pitfalls live in that little block. First, the padding math: a payload whose length is already a multiple of four needs no padding, and the -eq 4 guard is what keeps the expression honest. Second, direction: when you only decode, you add padding and swap; you never remove padding from standard Base64 input, because standard decoders expect it to be there. If the source is a JWT or an API token, it will be unpadded base64url, and the recipe above is exactly the shape you want.

Opening a JWT Without the Keys

A JSON Web Token is three base64url segments joined by dots: header, payload, signature. The first two are plain JSON, and Base64 is not encryption, so anyone with the token can read both. That is a feature, not a flaw: the token is designed to be inspected, and the signature is what makes it unforgeable. PowerShell makes the peek a three-liner:

$jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
$parts = $jwt.Split(".")
function Decode-UrlSegment([string]$segment) {
  $standard = $segment.Replace("-", "+").Replace("_", "/")
  $pad = 4 - ($standard.Length % 4)
  if ($pad -eq 4) { $pad = 0 }
  $standard = $standard.PadRight($standard.Length + $pad, "=")
  return [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($standard))
}
Decode-UrlSegment $parts[0] | ConvertFrom-Json | ConvertTo-Json -Compress
Decode-UrlSegment $parts[1] | ConvertFrom-Json
# name property:
(Decode-UrlSegment $parts[1] | ConvertFrom-Json).name
# John Doe

Three things to keep in mind. The third segment, the signature, is also base64url, but it decodes to binary signature bytes, not text, so do not expect pretty JSON there. The header usually just tells you which algorithm signed the token (HS256, RS256, ...), and a header that says none is a red flag, not a convenience. And reading the payload is not trusting it: base64 lets you see the claims, only the signature makes them authentic. If your job is to accept tokens, verify the signature with the issuer's key; if your job is to debug one, the code above is all you need.

Files, PEM and the Long Way to Bytes

The most common file shape is a text file holding Base64 of something bigger: a backup blob, a downloaded binary, a serialized object. The round trip is four lines, and the modern way to read the output is a real byte array, not a text guess:

$encoded = Get-Content -Path ./payload.b64 -Raw
$encoded = $encoded.Trim()
$bytes = [System.Convert]::FromBase64String($encoded)
[System.IO.File]::WriteAllBytes("./payload.bin", $bytes)
$bytes.Length
# how many bytes the text was carrying

Reading the original binary back is where PowerShell 6 and newer earn their keep. The -AsByteStream parameter reads raw bytes, and with -Raw it hands you a genuine byte[] in one shot:

$bytes = Get-Content -Path ./photo.png -AsByteStream -Raw
$encoded = [System.Convert]::ToBase64String($bytes)
Set-Content -Path ./photo.b64 -Value $encoded -NoNewline
$bytes.Length
# original size, before the 33 percent text tax

Leave off -Raw and you get a stream of individual byte objects (an Object[] when captured), which is fine for inspection but wrong for passing to .NET methods that expect an array. And Windows PowerShell 5.1 has no -AsByteStream at all, so on 5.1 the reliable read is [System.IO.File]::ReadAllBytes(), which exists everywhere.

PEM is the armored cousin you know from every certificate and private key: a standard Base64 body, usually wrapped at 64 characters, between -----BEGIN ... and -----END ... lines. The armor is text; the body is the payload. Strip the armor, join the lines, decode:

$pem = Get-Content -Path ./certificate.pem -Raw
$body = ($pem -split "`n") | Where-Object { $_ -notmatch "^-----" } | ForEach-Object { $_.Trim() }
$der = [System.Convert]::FromBase64String(($body -join ""))
$der.Length
# the certificate's binary DER size

Because the standard decoder ignores whitespace anyway, the -join "" is belt and braces rather than requirement, but keeping the script explicit about what it strips makes it behave the same on every machine and every line-ending convention. The other direction, wrapping DER bytes into PEM, is just the Base64 encoder plus two lines of text, and the encoding article on the sister site shows the 64-column wrap in full.

Certificates and the Windows Toolbox

Certificates are the heaviest Base64 citizens in day-to-day work, and PowerShell can hold the whole family. A PFX file is a binary bundle of certificate plus private key, and it is the format you most often find sitting around as Base64 text in config files and deployment scripts. Decoding it back into a live certificate is a one-liner with the .NET type, and it works cross-platform in PowerShell 7:

$bytes = [System.Convert]::FromBase64String($pfxText)
$password = ConvertTo-SecureString "secret" -AsPlainText -Force
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($bytes, $password)
$cert.Subject
# CN=example.org
$cert.NotAfter
# when it stops being true

PowerShell 7 also ships Get-PfxCertificate, which reads a PFX file straight from disk with a -Password parameter, so for on-disk files you can skip the manual decode entirely. A bare certificate (no key) is even simpler: the DER bytes go straight into the same X509Certificate2 type without any password.

Outside the language, two native tools are worth knowing. On Windows, certutil -decode infile.b64 outfile decodes a Base64 file with file-in/file-out semantics (add -f to overwrite), which makes it the go-to for quick fixes in a plain command prompt. Its sibling certutil -encode has a flag worth remembering: -unicodetext converts the input text to UTF-16 before Base64-encoding it, hiding an entire encoding decision inside one switch. On Linux and macOS the classic utility is base64 -d, which decodes a file or standard input with the same whitespace tolerance you already know from .NET.

Commands in a Base64 Envelope

PowerShell has had a built-in reason to speak Base64 since version 1.0: the -EncodedCommand parameter of the host itself. You hand pwsh a Base64 string, it decodes the bytes as UTF-16LE, and the result is run as a command. The official purpose, straight from the documentation, is to submit commands that require complex quotation marks or curly braces without fighting the outer shell's quoting rules:

$command = "Write-Host encoded-hello"
$encoded = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($command))
# VwByAGkAdABlAC0ASABvAHMAdAAgAGUAbgBjAG8AZABlAGQALQBoAGUAbABsAG8A
pwsh -NoProfile -EncodedCommand $encoded
# encoded-hello

Read that last line carefully, because it is where everyone trips: the payload must be UTF-16LE, which is [System.Text.Encoding]::Unicode. If you encode the command as UTF-8 instead, PowerShell happily decodes it as UTF-16LE and runs a command made of mojibake, and the error message it produces is a perfect portrait of the mistake:

$wrong = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($command))
pwsh -NoProfile -EncodedCommand $wrong
# Error: a wall of garbled characters, "The term ... is not recognized..."

The same mechanism is why security teams care about Base64 in PowerShell. A long opaque token passed to -EncodedCommand is a common shape for automated tooling, which is exactly why endpoint protection products decode these payloads before they run: nothing about Base64 hides the command from a decoder, it only hides it from a human reading a process list. If you generate encoded commands for your own automation, keep the source command next to the token, because the token itself is not going to explain itself at 3 a.m.

Decoding When the Input Is Huge

For everyday sizes the single-method approach is the fast one. A five megabyte binary becomes a string of about six point nine million characters, and the decode of that string takes single-digit milliseconds on a modern machine. The .NET documentation's own note is that FromBase64String is designed to process a single string containing all the data, which is true, and which is also fine up to very large limits, because the method works on the string in place without meaningful extra copies.

When the payload is bigger than you would be comfortable holding in one string, or arrives as a stream (a download, a socket, a huge log), the documented tool is System.Security.Cryptography.FromBase64Transform wrapped in a CryptoStream: you feed it Base64 text and read decoded bytes out, and only a small buffer is alive at any moment. Note that TransformStream, the C# helper for this, is an extension method, and PowerShell does not see extension methods, so you instantiate the CryptoStream directly:

$inputStream = [System.IO.File]::OpenRead("./payload.b64")
$transform = [System.Security.Cryptography.FromBase64Transform]::new()
$stream = [System.Security.Cryptography.CryptoStream]::new(
  $inputStream, $transform, [System.Security.Cryptography.CryptoStreamMode]::Read)
$destination = [System.IO.File]::Create("./payload.bin")
$buffer = New-Object byte[] 65536
while (($read = $stream.Read($buffer, 0, $buffer.Length)) -gt 0) {
  $destination.Write($buffer, 0, $read)
}
$destination.Dispose()
$stream.Dispose()
$inputStream.Dispose()

For the ninety percent of jobs, the simple path is still the right one: read the whole text file with Get-Content -Raw, trim it, decode it, write the bytes. Reach for the stream version when the file is too big to keep in memory comfortably, or when the data is arriving piece by piece. And do not try to loop over lines and decode each line separately: Base64 groups of four characters do not respect your line breaks, so a line that splits a group in the middle will not decode on its own. Read the whole text, then decode once.

Pitfalls That Cost Afternoons

  • The charset guess. UTF-8 read as UTF-16, or Latin-1 read as UTF-8, produces confident mojibake. Decide the encoding from the source of the data, default to UTF-8, and look at the first few decoded characters before you trust the rest.
  • Invisible characters from the web. A non-breaking space or a byte-order mark pasted from a page or a rich-text email is a foreign character to the decoder and throws the generic FormatException. Run the input through .Trim() and a non-printing-character check before you decode.
  • Padding confusion. Standard Base64 arrives with = or == at the end; base64url from tokens arrives with none. Feeding one to the recipe built for the other is the most common silent breakage in API work, and the length-check in the base64url section is the guard.
  • The one message, three crimes. Because the FormatException message covers bad characters, excess padding and dirty padding all at once, catch blocks that only log the message send you in circles. Log the length of the input and the first offending region as well.
  • Expecting a string back. The result is always a byte array. The moment you start string-formating it directly you get a list of numbers, not text. Convert with an explicit encoding, once, at the end.
  • The 5.1 file default. Windows PowerShell 5.1 reads BOM-less files with the system's ANSI code page, while PowerShell 7 assumes UTF-8. If your script reads the Base64 text file on 5.1 and the file is UTF-8 with non-ASCII around the payload, the corruption happens before the decoder ever sees it.
  • Treating Base64 as a lock. It is a translation. A password, token or secret in Base64 is plain text wearing a costume, and every decoder on the planet, including this article, opens it in one line.

Habits That Keep Scripts Honest

  • Trim external input before decoding. One .Trim() removes more production incidents than any error handler.
  • Validate before you decode when the source is untrusted: after stripping the four allowed whitespace characters, the string should match only alphabet characters with at most two trailing equals signs. A quick regex check turns a mystery exception into a clean rejected-input message.
  • Keep the bytes as bytes until the very last step. Decode once, hand the byte[] to the file API or the encoder that needs it, and only then convert to text with a deliberate encoding.
  • Log lengths, not payloads. The size of the input and the size of the decoded output tells you almost everything about a decode failure, without pasting possibly sensitive data into the log.
  • For anything that crosses a wire, record which alphabet it is in, standard or base64url, and which padding convention, in the same line of code that decodes it. Future you is the consumer of that note.

How PowerShell Inherited Its Decoder

The shortest true history of Base64 in PowerShell is that PowerShell never wrote one. The method you use, Convert.FromBase64String, shipped with .NET Framework 1.1 in 2003, and every PowerShell since version 1.0 in November 2006 has simply exposed the .NET it runs on. The project was called Monad while it was being built, first shown publicly at the Professional Developers Conference in October 2003, and by the time it was released the .NET encoder-decoder pair it wraps was already three years old and in daily use.

The format itself was standardized the same year the shell launched. RFC 4648, published in October 2006, is the document that fixed the alphabet, the padding rules, the strict-decode expectation and the base64url variant, and it still describes exactly the behavior FromBase64String implements today. When PowerShell went open-source and cross-platform in August 2016 as PowerShell Core, the decoder came along for the ride on Linux and macOS with no changes, because there was nothing to change.

The one genuine add-on is the community-maintained Microsoft.PowerShell.TextUtility module from the PowerShell Gallery, whose ConvertFrom-Base64 cmdlet wraps the same .NET method and adds a -AsByteArray switch plus a text default that decodes as UTF-8. Install it with Install-Module -Name Microsoft.PowerShell.TextUtility if you prefer the cmdlet shape. It is worth knowing that the module is now archived and no longer actively maintained, which is another reason the built-in method remains the recommendation for new scripts.

Facts Worth Remembering

  • The decoder ignores tabs, line feeds, carriage returns and spaces anywhere in the input. A hundred wrapped lines decode exactly like one long line.
  • $null and the empty string both decode to an empty array without complaint, which makes FromBase64String unusually forgiving at the edge.
  • The single FormatException message covers three different failure modes. When it fires, the input, not the message, is where the answer is.
  • "SABpAA==" is the string Hi in PowerShell's own internal encoding, UTF-16LE. It is twice as long as the UTF-8 encoding of the same two letters, and that ratio is the fingerprint of Windows-native text in any Base64 you will read.
  • -EncodedCommand has existed since the first PowerShell release, and its payload is mandated to be UTF-16LE, not UTF-8. Encode with the wrong encoding and the shell cheerfully runs your mojibake.
  • .NET's newer span-based Base64 helpers, including the Base64Url class, cannot be called from PowerShell at all: spans are byref-like types, and PowerShell refuses them. The two-character swap is not a workaround out of laziness, it is the only pure-PowerShell path.
  • Get-Content -AsByteStream without -Raw gives you a stream of byte objects, not a byte array. Add -Raw and the type is exactly what the .NET methods expect.

The Long Way Round

Everything in this article is about taking a Base64 string and getting your data back. The mirror operation, turning data into Base64, looks like a one-liner until you meet the fact that PowerShell strings are not bytes, that UTF-16 doubles your size, that line wrapping has two conventional widths, and that base64url output needs its own two-character surgery. That direction gets its own full treatment, with its own traps and its own history, in the related article on the sister site, Base64 encoding in PowerShell, which this page links to below.

Last updated: 2026-08-29

Related article: Base64 Encoding in PowerShell: A Complete Guide