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 Encoding in PowerShell: A Complete Guide

You have a string, a file, a certificate or a token, and the other side of the wire wants it as a long run of letters and digits: printable, paste-able into an email, a URL or a config file, without a single byte of binary to break the transport. That is Base64. It is a translation, not a compression and not a lock: three bytes of input become four characters of output, so the text you ship runs about 33% larger than what started it, using an alphabet of 64 characters plus the equals sign as trailing padding.

The home page above covers the alphabet, the bit math and the variants in detail. This article covers the encoding direction from the PowerShell side: the one .NET method you will call, the missing step that trips up everyone on their first script, the line-wrap conventions that differ by protocol, the URL-safe alphabet, and the handful of real jobs where encoding in PowerShell rewards the careful and punishes the careless.

The Method and the Missing Step

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

$bytes = [System.Text.Encoding]::UTF8.GetBytes("Hello")
[System.Convert]::ToBase64String($bytes)
# SGVsbG8=

That is the whole API: one byte array in, one string out, in every PowerShell on every operating system, because it is simply .NET. The missing step is the first line of the example, and it is where beginners lose their first hour. The method does not accept your string. It accepts bytes, and the question "which bytes does my string mean" is an encoding question that only you can answer. Here is the contract of the overloads you can actually call from PowerShell:

What you pass What you get
byte[] One long line of standard Base64, with = padding where the length requires it
byte[] plus InsertLineBreaks The same data, broken at 76 characters with CRLF between lines
byte[], offset, count Only the requested slice of the array, encoded
A string such as "Hello" A conversion exception. PowerShell cannot turn a string into a byte array on its own
$null An ArgumentNullException, wrapped for you in a MethodInvocationException

Notice what is not in that table: there is no overload that says "encode this text". Encoding text in PowerShell is always a two-step process. You decide the encoding, you produce the bytes, and only then does the Base64 method enter the conversation. Keep those two decisions visibly separate in the script, because the second one is invisible and the first one is where the bugs live.

Encoding Text: Pick the Encoding First

The safe default for anything that crosses the modern internet is UTF-8. Web APIs, JSON, JWTs, everything a browser or a server wrote in the last decade will expect UTF-8 bytes underneath the Base64, and the two-step pattern is the habit you want to build:

$text = "Hello, PowerShell!"
$bytes = [System.Text.Encoding]::UTF8.GetBytes($text)
$encoded = [System.Convert]::ToBase64String($bytes)
# SGVsbG8sIFBvd2VyU2hlbGwh

When you reach for a different encoding, you are usually serving a legacy system, and the table below is the practical guide:

Encoding Use it when If you pick the wrong one
UTF8 Web APIs, JSON, JWTs, modern everything. The default choice The decoder on the other side sees mojibake instead of your text
Unicode (UTF-16LE) The consumer is a Windows or .NET component that encodes .NET strings, or -EncodedCommand Your payload is twice as long as the consumer expects, and full of surprises
ASCII Classic 7-bit protocols such as HTTP Basic credentials Anything above value 127 is replaced before the encoding ever happens
Latin1 Legacy European systems that predate UTF-8 One byte per character, and every non-Latin-1 character becomes a question mark

A useful debugging trick works in both directions: the padding and the length of the Base64 tell you how many bytes were encoded, and the way the decoded text looks tells you which two-byte or one-byte world it came from. A payload that is suspiciously even in size and full of alternating normal and blank-looking characters is usually UTF-16 wearing a UTF-8 costume, or the reverse.

The UTF-16 Surprise

PowerShell stores strings as UTF-16 internally, and that fact leaks into Base64 work in one specific, very common place: you write the Base64 for a consumer that is itself a .NET or Windows component, and you encode with Unicode because that is what .NET strings are. It is a correct instinct for some consumers and a size-doubling mistake for all the rest. The same four visible characters, two encodings:

$same = "Café"
[System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($same))
# Q2Fmw6k=  five bytes
[System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($same))
# QwBhAGYA6QA=  ten bytes

Same text, double the size, and the two strings are not interchangeable: a consumer that expects one and receives the other will not fail loudly, it will just read garbage. The rule that keeps this from biting you is to treat the encoding as part of the protocol, not a local detail. If the receiving system is a browser, a REST API or a modern server, it is UTF-8 unless the documentation says otherwise. If it is the PowerShell host itself via -EncodedCommand, or a .NET string in a Windows-only pipeline, it is UTF-16LE. When the protocol does not say, ask the other side what it will call GetString with, because that is the question that actually decides.

Numbers, Bytes and Everything Else

The method is declared to take a byte array, but PowerShell's type conversion is generous about what counts as one, and knowing the edges saves you from surprises:

[System.Convert]::ToBase64String([byte[]](1, 2, 3, 250, 251))
# AQID+vs=
[System.Convert]::ToBase64String([char[]]"Café")
# Q2Fm6Q==  one byte per character, the character's value as a number
[System.Convert]::ToBase64String([int[]](72, 101, 108, 108, 111))
# SGVsbG8=
[System.Convert]::ToBase64String(123)
# ew==  a single number is accepted where a whole array is expected

Two edges in that block deserve attention. Character arrays convert one byte per character using the character's numeric value, which for Latin text is exactly what the legacy systems that use this trick expect, and for anything beyond it silently produces the wrong bytes. And an integer larger than 255 does not raise an error when it is converted to a byte, it wraps around, so 256 encodes as 0 and your data is quietly corrupted in a way no exception will ever mention. If your source is numbers, make the cast explicit: [byte[]](1, 2, 3) says exactly what it means.

The other edge is the one that is loud instead of quiet: pass the method a string and PowerShell's conversion engine gives up, because there is no way to know which bytes a string means. Pass it $null and .NET throws before doing anything. Both are correct behavior, and both are the reason the two-step pattern from the first section is the only pattern worth having.

Line Wrapping: 76, 64 and None

By default the encoder produces one long line, no matter how much data you give it. For a file of a few kilobytes that is fine. For data that will be read by a human, pasted into an email, or compared in a source-control diff, a wall of eight million characters is a practical problem, and the convention is to wrap. PowerShell and the protocols it serves know three widths, and they are not interchangeable:

Width Who expects it Line ending
76 characters MIME, mail and most text transports. The default of InsertLineBreaks CRLF
64 characters PEM files: certificates, private keys and the rest of the -----BEGIN family Conventionally LF
None APIs, tokens, config files, anything where the payload is machine-processed No line at all

The built-in wrap is a one-parameter change, and it is the one you want for mail-style payloads:

$text = "The quick brown fox jumps over the lazy dog. Base64 output arrives wrapped at different widths depending on who is reading it."
$wrapped = [System.Convert]::ToBase64String(
  [System.Text.Encoding]::UTF8.GetBytes($text),
  [Base64FormattingOptions]::InsertLineBreaks)
# 76 characters per line, CRLF between them, exactly as MIME expects

PEM is the exception to the built-in, because OpenSSL and the entire -----BEGIN ecosystem wrap at 64 characters, and no .NET flag produces that width. The loop is short and it is the standard recipe:

$der = [System.IO.File]::ReadAllBytes("./certificate.der")
$b64 = [System.Convert]::ToBase64String($der)
$lines = for ($i = 0; $i -lt $b64.Length; $i += 64) {
  $b64.Substring($i, [Math]::Min(64, $b64.Length - $i))
}
$pem = @("-----BEGIN CERTIFICATE-----") + @($lines) + @("-----END CERTIFICATE-----")
Set-Content -Path "./certificate.pem" -Value ($pem -join "`n")

The reason the width matters at all is that Base64 groups of four characters do not respect line breaks, so a decoder may ignore the breaks entirely or it may enforce them. The decoder this site uses ignores them, but strict consumers, and there are many of them in the certificate and mail worlds, treat an unexpected break as a foreign character and reject the payload. When you choose a width, you are making a contract with the consumer, and it is worth a comment in the script naming who you are contracting with.

base64url: Two Characters and a Padding Decision

Standard Base64's plus and slash are legal inside a URL only after percent-encoding, and the equals padding reads 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, and padding is usually dropped because the length of the data makes it unnecessary. Every API token and JWT you have ever handled is written in this variant, which the standard insists on calling base64url and not just base64.

PowerShell's standard encoder produces the standard alphabet, so the conversion to base64url is two character swaps and a padding decision:

$bytes = [System.Text.Encoding]::UTF8.GetBytes("example token payload")
$standard = [System.Convert]::ToBase64String($bytes)
$url = $standard.Replace("+", "-").Replace("/", "_").TrimEnd("=")
# the URL-safe form, padding removed

The padding removal is safe in the base64url world because the consumer recomputes what the padding would have been from the length of the string. That is not true everywhere, though, so make the decision explicit: drop the padding for tokens, JWT segments and URL embedding, keep it for anything that feeds a strict standard-alphabet consumer, and write down which you chose. The .NET runtime does ship a dedicated class for this alphabet, System.Buffers.Text.Base64Url, but its methods all take or return spans, and PowerShell cannot pass spans to .NET at all, so the two-swap recipe is not a cop-out. It is the only path the language offers, and it works in every version.

Minting a JWT

A JSON Web Token is the flagship real-world use of base64url, and it is also a good full test of the encoding pipeline, because a JWT is three encoded segments joined by dots: the header, the payload and the signature. The first two are compact JSON in base64url, and the third is the binary output of a hash over the exact text of the first two. Here is a complete HS256 token built in PowerShell, start to finish:

$header = @{ alg = "HS256"; typ = "JWT" } | ConvertTo-Json -Compress
$payload = @{ sub = "1234567890"; name = "John Doe"; iat = 1516239022 } | ConvertTo-Json -Compress
function UrlEncode64([byte[]]$bytes) {
  $standard = [System.Convert]::ToBase64String($bytes).TrimEnd("=")
  return $standard.Replace("+", "-").Replace("/", "_")
}
$left = (UrlEncode64 ([System.Text.Encoding]::UTF8.GetBytes($header))) + "." + (UrlEncode64 ([System.Text.Encoding]::UTF8.GetBytes($payload)))
$hmac = [System.Security.Cryptography.HMACSHA256]::new([System.Text.Encoding]::UTF8.GetBytes("secret"))
$signature = UrlEncode64 ($hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($left)))
$jwt = $left + "." + $signature
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ.jfy-T6NvLEkGF2tT-gmlobsc72x5KdFo4o9eQi3dZYY

Look at the signature segment: hyphens and an underscore in the wild, exactly the base64url fingerprints you are told to expect. Three things about that example will save you from production incidents. First, the signature is computed over the exact JSON text, including its key order and spacing, so the JSON you sign and the JSON you verify against must be byte-for-byte the same. PowerShell's ConvertTo-Json decides the key order for you, and it is not alphabetical, so do not hand-reorder a token's segments or reformat them between signing and checking. Second, -Compress is not cosmetic: a token whose header or payload contains a single space is a token that will never verify against a compliant implementation, because the standard form is compact. Third, the timestamp iat is seconds since the Unix epoch, and a payload built from Get-Date without converting will be years out of range. The decode direction, peeking into a token someone else minted, is covered in the related article on the sister site.

Files and the Byte Stream

Files are the most common payload of all, and the pipeline is short. Read the file as bytes, encode, write text. The two lines that matter are the read, which must be a byte read, and the write, which usually must not add a trailing newline:

$bytes = [System.IO.File]::ReadAllBytes("./photo.png")
$encoded = [System.Convert]::ToBase64String($bytes)
Set-Content -Path "./photo.b64" -Value $encoded -NoNewline
$encoded.Length
# the text size you are about to ship

Two practical notes. The first is arithmetic: Base64 makes everything bigger, and for a 10 megabyte file the text you ship is about 13.4 megabytes. If the transport has a size limit, or if this text is going into an email body or a URL, do the math before you encode, not after the error. The second is the trailing newline: Set-Content adds one by default, and while the decoder used by this site and most modern decoders ignore it, some strict consumers do not. -NoNewline costs you nothing and removes the question.

PowerShell 6 and newer offer a second read that stays in the language: Get-Content -AsByteStream -Raw returns the file as a single byte array in one call, which is a tidy alternative to the .NET ReadAllBytes and behaves identically for this purpose. On Windows PowerShell 5.1, which has no -AsByteStream, the .NET read is the only option, and it is the one that behaves the same on every version of the shell.

Certificates: From PEM and PFX to Text

Certificates are the heaviest encoding citizens in day-to-day operations, because deployments love to carry them as text. A PEM certificate is a wrapped Base64 body between armor lines, and the recipe from the wrapping section is the whole export:

$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new([System.IO.File]::ReadAllBytes("./certificate.der"))
$cert.Subject
# CN=example.org
$b64 = [System.Convert]::ToBase64String($cert.RawData)
# one long line of the certificate's binary form

The PFX format is the other workhorse: a single binary file holding the certificate together with its private key, which is why it is the format you most often find sitting around as Base64 text inside deployment scripts and configuration stores. Encoding one is the plain file pipeline from the previous section, and the reading direction in PowerShell 7 is a one-cmdlet affair:

$pfxBytes = [System.IO.File]::ReadAllBytes("./certificate.pfx")
$pfxB64 = [System.Convert]::ToBase64String($pfxBytes)
# the text form of the bundle, ready for a config file
Get-PfxCertificate -FilePath "./certificate.pfx" -Password (ConvertTo-SecureString "secret" -AsPlainText -Force)
# the live certificate, no manual decode needed

One sentence of security, said plainly because Base64 invites the opposite assumption: a PFX in Base64 is a private key in text. The encoding changes the shape of the secret and nothing about its secrecy, so a Base64 PFX pasted into a chat window, a ticket or a commit is a private key pasted into a chat window, a ticket or a commit. Treat the text form with exactly the care the binary form gets, and prefer the certificate store or a secrets manager over either.

Basic Auth, Data URIs and the Old Habits

Base64 is older than the standards document that named it. The MIME family of RFCs from 1998 put it in email, and HTTP Basic authentication put it in every header exchange on the early web, where the client still encodes the credential pair as one Base64 string:

$credential = [System.Text.Encoding]::UTF8.GetBytes("alice:s3cret!")
[System.Convert]::ToBase64String($credential)
# YWxpY2U6czNjcmV0IQ==
# sent as: Authorization: Basic YWxpY2U6czNjcmV0IQ==

The standard alphabet is the right one here, plus and slash included, because a header is not a URL and does not need the safe alphabet. The same mechanism appears in data URIs, the way a document embeds its own binary inline, and the shape is a literal prefix plus the standard Base64 of the bytes:

$dataUri = "data:application/octet-stream;base64," + [System.Convert]::ToBase64String([byte[]](1, 2, 3, 250, 251))
# data:application/octet-stream;base64,AQID+vs=

Both habits are worth knowing less as things you will build and more as things you will meet: when a header or a link contains a long Base64 run, these two formats are the first ones to check, and both are plain decodings away from what they say. Which is the point of the format, and the reason the decode side of this site exists.

Encoded Commands and the Windows Toolbox

PowerShell has carried a built-in reason to encode 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 runs as a command. The documented purpose is commands that fight the outer shell's quoting, and the encoding side is two lines:

$command = "Write-Host 'Hello from the encoded side'"
$encoded = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($command))
# VwByAGkAdABlAC0ASABvAHMAdAAgACcASABlAGwAbABvACAAZgByAG8AbQAgAHQAaABlACAAZQBuAGMAbwBkAGUAZAAgAHMAaQBkAGUAJwA=
pwsh -NoProfile -EncodedCommand $encoded
# Hello from the encoded side

Read the encoding line carefully, because it is the one everyone gets wrong: the payload must be UTF-16LE, which is the Unicode encoding, not UTF-8. Encode with the wrong one and the host decodes your bytes as UTF-16LE anyway and runs a command made of mojibake, producing an error that is a perfect portrait of the mistake. The decode article covers that failure in full, and the fix on this side is a single word: Unicode.

Outside the language, the native tools each carry their own quiet encoding decision. On Windows, certutil -encode infile outfile.b64 produces a standard Base64 file with the armor lines that PEM expects, -f overwrites an existing output, and the flag worth remembering is -unicodetext, which converts the input text to UTF-16 before encoding it, hiding an entire encoding choice inside one switch. On Linux and macOS the classic utility is base64 -w 0 file, where the -w 0 is the load-bearing part: without it GNU base64 wraps at 76 characters and hands you a MIME-style file when you wanted one line.

Encoding When the Output Is Huge

For everyday sizes, the read-all-encode-all pipeline is the fast and simple one, and it is the right one until the file is too large to hold in memory comfortably or the data is arriving piece by piece from a download or a socket. Then the documented tool is the streaming pair: System.Security.Cryptography.ToBase64Transform wrapped in a CryptoStream, where you write raw bytes in and Base64 text comes out, with only a small buffer alive at any moment:

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

One difference from the one-shot method is worth writing down: the stream produces one continuous line with no wrapping at all, no matter how large the input. A decoder that ignores whitespace does not care, but if the final destination is a PEM file, run the 64-column loop from the wrapping section over the result afterwards. And a C# reader may reach for a helper called TransformStream that does this in one call: it is an extension method, and PowerShell does not see extension methods, so the explicit CryptoStream above is the form the language supports.

Where Encoded Payloads Go Wrong

  • Encoding the string, not the bytes. ToBase64String("Hello") throws a conversion exception, which is the method telling you that the first decision, the encoding, has not been made. Make it visible in the script and the error disappears.
  • UTF-16 where UTF-8 was promised. The payload is twice as long as expected and the consumer reads garbage. The encoding is part of the protocol, and for almost every wire on the modern internet the protocol says UTF-8.
  • The 5.1 read. Windows PowerShell 5.1 reads a BOM-less text file with the machine's ANSI code page before your script ever sees it, so a UTF-8 source file can be corrupted before the encoding step. On 5.1, read text with an explicit UTF-8 read and check the first characters of the result.
  • The wrong wrap width. MIME wants 76, PEM wants 64, APIs want none, and a strict consumer treats an unexpected line break as a foreign character. Pick the width from the consumer and say so in a comment.
  • Padding on the wrong side of the swap. Dropping the equals signs is correct for base64url tokens and wrong for a consumer that expects standard padding. The alphabet swap and the padding decision are two choices, not one.
  • Reformatting what you signed. A JWT signature covers the exact JSON text, key order and spacing included. Reorder the claims or add a space and the token stops verifying, with no error message anywhere near the cause.
  • Values past 255. Converting an integer to a byte wraps silently, so 256 encodes as 0. If your source data is numbers, cast explicitly and let a mistake be a mistake you see.
  • Believing the costume. Base64 is not encryption and not compression: it is a translation that grows the data by a third. A secret in Base64 is a secret in plain text, and a file in Base64 is a file that needs 33% more room.

Rules for Encoders You Can Trust

  • Produce the bytes deliberately. The first line of any encoding script should be an explicit GetBytes or a byte read, never a hope that PowerShell will convert a string into the right bytes.
  • Name the alphabet and the width in a comment next to the code that makes the choice: standard or base64url, wrapped at 76, 64 or not at all. The consumer is a person reading the script in six months, and that person is you.
  • Write the text file with -NoNewline unless the consumer specifically expects a trailing break, and choose the line ending (LF or CRLF) the way the consumer's documentation expects it.
  • Test the round trip while you build: encode, decode, compare the bytes. A thirty-second Compare-Object over the two byte arrays catches encoding mistakes, wrap mistakes and byte-order mistakes all at once, while the cause is still fresh.
  • Log sizes, not payloads. The byte count before and the character count after should sit at a ratio of about 1.33, and when they do not, the size mismatch tells you where to look without the log ever containing the data.

How PowerShell Inherited Its Encoder

The shortest true history of Base64 encoding in PowerShell is that PowerShell never wrote one. The method you call, Convert.ToBase64String, 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 release time the .NET encoder it wraps was already three years old and carrying web traffic.

The format was standardized the same year the shell launched. RFC 4648, published in October 2006, fixed the alphabet, the padding rules, the strictness of decoding and the base64url variant, and it still describes exactly the behavior the .NET pair implements. The MIME RFCs that came before it, in 1998, had already put the 76-character wrap into email, which is why that width is the default of InsertLineBreaks to this day. When PowerShell went open-source and cross-platform in August 2016 as PowerShell Core, the encoder came along to Linux and macOS unchanged, because there was nothing to change.

What did change later happened in .NET, and mostly outside PowerShell's reach. The runtime gained faster, span-based Base64 helpers in recent versions, including the Base64Url class and the Try-prefixed decode methods, but spans are byref-like types and PowerShell refuses to pass them, so none of those shortcuts are callable from a script. The community answer is the Microsoft.PowerShell.TextUtility module from the PowerShell Gallery, whose ConvertTo-Base64 wraps the same .NET method and adds a -Text parameter with a UTF-8 default and an -InsertBreakLines switch for the 76-column wrap. Install it with Install-Module -Name Microsoft.PowerShell.TextUtility if you prefer the cmdlet shape, and note that the module is archived and no longer actively maintained, which is one more reason the built-in method remains the recommendation for new scripts.

The Numbers and Names to Keep

  • Every three input bytes become four output characters, so encoded data runs about 33% larger than the original, and no padding is ever more than two equals signs.
  • The default output is one unbroken line. InsertLineBreaks wraps at 76 characters with CRLF, the MIME convention from 1998. PEM wants 64, and no built-in flag produces that width.
  • base64url is standard Base64 with plus become hyphen and slash become underscore, padding usually dropped, and it is the alphabet of every JWT and API token.
  • "Café" is five bytes in UTF-8 and ten in UTF-16LE. The same visible text, double the size, and the two encodings are not interchangeable across the wire.
  • -EncodedCommand has existed since the first PowerShell release, and its payload must be UTF-16LE, not UTF-8. The single word that fixes the most common mistake on this side is Unicode.
  • certutil -encode can hide an encoding decision inside -unicodetext, and GNU base64 needs -w 0 to give you one line instead of the 76-column wrap.
  • .NET's span-based Base64 helpers, including Base64Url, are not callable from PowerShell at all, because PowerShell cannot pass span types to .NET methods. The two-character swap is the whole recipe, in every version.
  • A single byte, 123, encodes as ew==: the smallest possible example of the rule that the length of the output tells you the length of the input.

Flipping the Arrow

Everything in this article is about taking data you hold and turning it into a Base64 string. The mirror operation, taking a string and getting your data back, has its own cast of problems: a decoder that ignores four kinds of whitespace, one error message covering three crimes, a JWT to peek into, a certificate to unwrap, and an -EncodedCommand to explain. That direction gets its own full treatment, with its own traps and its own history, in the related article on the sister site, Base64 decoding in PowerShell, linked below.

Last updated: 2026-08-29

Related article: Base64 Decoding in PowerShell: A Complete Guide