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 Ruby: A Complete Guide

Somewhere between you and the original data there is a wall of characters: upper and lower case letters, digits, maybe a plus or a slash or a hyphen, and perhaps an equals sign parked at the end. Your editor has no idea what file type that is. Your database stuffed it into a text column. It arrived in an HTTP header, a URL, a YAML key, or a support ticket with a .b64 attachment. You recognize it in an instant - Base64 - and now you need the bytes back. In Ruby, that need is one require statement and one method call away.

In case the format is new to you, here is the thirty-second version. Base64 rewrites raw data three bytes at a time: each group of three bytes becomes four characters drawn from a 64 symbol alphabet, and when the input does not divide evenly by three, one or two = characters are appended as padding so the output always lands on a multiple of four. Decoding is the reverse trip - four characters in, three bytes out - so the result is always smaller than the input, about three quarters the size. The home page of this site walks through every bit of the format in detail, so this guide spends its energy where it belongs: on the Ruby side of the job.

The good news: every Ruby installation ships the complete decoding toolkit. The Base64 module needs nothing installed, and its three decoders are so small that you can read their entire source in a single sitting. The caution: the decoder you reach for first is also the one that never complains, which is a beautiful property for email and a terrible property for security. By the end of this guide you will know exactly what each decoder accepts, how to turn the bytes it hands back into text Ruby will let you use, and what to do with every payload a Ruby developer actually decodes - JWTs, auth headers, data URIs, email bodies, PEM armor, files, config blobs and giant ones.

Meet the Toolbox

Everything starts with a require. There is no installation step, no platform quirks, no native extension to build:

require "base64"
puts Base64::VERSION
# => 0.2.0 on a stock Ruby 3.3, for example

Here is the whole decoding side of the toolkit in one table, in order of how often you will reach for each method:

Decoder Treatment of foreign characters Padding rules When something is wrong
Base64.decode64(str) ignores everything that is not in the standard alphabet, including line breaks and spaces anything at all, even wrong padding nothing - it never raises, it just returns what it could decode
Base64.strict_decode64(str) rejects any character outside the standard alphabet must be present and exactly correct raises ArgumentError
Base64.urlsafe_decode64(str) accepts the URL-safe alphabet and the standard one, rejects everything else optional, but if present it must be correct raises ArgumentError

If you like to know what your tools are doing under the hood, the entire decoding side of the module is a thin wrapper around two templates of the core pack/unpack machinery, which is implemented in C inside the Ruby core:

# the entire decoding side of the module, condensed
def decode64(str)
  str.unpack1("m")
end
def strict_decode64(str)
  str.unpack1("m0")
end

The m template is the lenient reader, m0 is the strict one, and that single character difference explains the whole personality gap between the first two decoders. Because the heavy lifting happens at core speed, the module stays pure Ruby while still chewing through megabytes in single-digit milliseconds.

decode64: The Chameleon

Base64.decode64 is the decoder that says yes to everything. Feed it a clean payload and it decodes it. Feed it a MIME-style blob full of line breaks and it shrugs. Feed it a string that is not Base64 at all and it hands back whatever it could squeeze out, without a single warning:

require "base64"
Base64.decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.decode64("Zm9vCmJh\ncgptYW4=\n")
# => "foo\nbar\nman"

That second line is the whole personality in one example. The decoder skips everything that is not part of the standard alphabet - line breaks, spaces, the odd control character - and decodes the rest. This is exactly the behavior MIME Base64 is supposed to have, which is why decode64 is the right tool for anything that traveled through email.

The flip side is what makes it dangerous. Because the decoder never complains, it also never tells you when the input was wrong:

Base64.decode64("not base64 at all!")
# => ten bytes of perfectly plausible looking garbage
Base64.decode64("====")
# => ""

The first example finds the characters that happen to be valid alphabet letters, decodes them, and hands back bytes you might be tempted to write straight to a file. The second example returns an empty string for a string of four padding characters. Nothing is raised, nothing is logged. If your input is untrusted, that silence is a feature you want to switch off - which is what the next two decoders are for.

One more quirk worth knowing, because it is the kind of thing that hides in production for months: decoding stops at the first = character. Anything after the padding is not an error, it is simply never read:

Base64.decode64("aGVsbG8=Zm9vYmFy")
# => "hello"   the "Zm9vYmFy" part is invisible to the decoder

strict_decode64: The Gatekeeper

Base64.strict_decode64 is the decoder with a clipboard. It accepts only the standard alphabet (A to Z, a to z, 0 to 9, plus, slash), it demands that any padding be exactly right, and it refuses to produce a single byte if any rule is broken:

Base64.strict_decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.strict_decode64("aGVsbG8gd29ybGQ")
# => raises ArgumentError
Base64.strict_decode64("Zm9vCmJh\ncgptYW4=")
# => raises ArgumentError

The last line is the telling one: the same payload that decode64 happily decoded now raises because of a single line break. Missing padding, extra padding, a hyphen, an underscore, a space - all of it is a crime, and the whole payload goes down with it:

begin
  Base64.strict_decode64("aGVsbG8")
rescue ArgumentError => e
  puts e.message
end
# => invalid base64

The gatekeeper even polices corners of the format you would not think to check. When a Base64 string ends with padding, some of the bits in the last character are never used, and the RFC says a conforming encoder must set those bits to zero. Ruby verifies:

Base64.strict_decode64("QQ==")
# => "A"
Base64.strict_decode64("QR==")
# => raises ArgumentError (the pad bits are not zero)

The second string would decode to the same byte as the first if the decoder were sloppy. Ruby is not sloppy. In practice this makes strict_decode64 the right default for any input you did not encode yourself: it converts typos, truncation and the wrong alphabet into loud, catchable errors instead of quiet corruption.

urlsafe_decode64: The Diplomat

Base64.urlsafe_decode64 exists for payloads that travel where + and / are reserved words: URLs, tokens, database identifiers. Internally it translates the URL-safe alphabet (hyphen and underscore) back to the standard one, normalizes the padding, and hands the result to the strict decoder:

Base64.urlsafe_decode64("SGVsbG8gd29ybGQ")
# => "Hello world"
Base64.urlsafe_decode64("SGVsbG8gd29ybGQ=")
# => raises ArgumentError (that padding is wrong for that length)

The first example shows its most useful trait: unpadded input is fine. If the string has no padding and its length is not a multiple of four, the decoder adds the missing = characters for you - which is exactly what JSON Web Tokens, the biggest consumer of URL-safe Base64, produce. If padding is present, though, it must be correct, just like with the strict decoder.

There is one quirk the documentation does not shout about: the diplomat speaks both languages. Because the method rewrites hyphens and underscores before doing a strict decode, it also accepts strings from the standard alphabet:

Base64.urlsafe_decode64("aGVsbG8=")
# => "hello"   the standard alphabet is accepted too

That leniency is convenient, but it means you cannot use this method to tell which alphabet a payload came from. If that matters to you, inspect the characters yourself before decoding.

And unlike decode64, the diplomat has no mercy for whitespace. A line break anywhere in a URL-safe payload raises ArgumentError, so if your input comes from a wrapped file, strip the line breaks first.

Bytes Are Not Text: The Encoding Step

Here is the step that trips up even experienced developers, because Ruby makes it visible. A decoded Base64 string is always tagged with the ASCII-8BIT encoding (a.k.a. BINARY), whether the original data was a PNG, a JWT payload, or a love letter in UTF-8:

bin = Base64.decode64(Base64.strict_encode64("h\u{e9}llo"))
puts bin.encoding
# => ASCII-8BIT
puts bin.bytes
# => [104, 195, 169, 108, 108, 111]

If the payload is binary - a picture, a zip file, a hash - you keep it exactly like that and write it with File.binwrite. No conversion, no questions. If the payload is text, the bytes are almost certainly UTF-8, and you need to tell Ruby so:

text = Base64.decode64(payload)
text.force_encoding("UTF-8")
if text.valid_encoding?
  puts text
else
  puts "not valid UTF-8 after all"
end

The two calls do different jobs. force_encoding only relabels the bytes; valid_encoding? then verifies that they form real UTF-8. Run them in that order, because validating a BINARY string first has nothing to validate. And one small comparison trap to remember for life: Ruby only compares a BINARY string equal to a UTF-8 string when both are pure ASCII, so relabel before you compare decoded text against your original:

decoded = Base64.decode64("aMOpbGxv")
puts decoded == "h\u{e9}llo"
# => false   same bytes, different tags
decoded.force_encoding("UTF-8")
puts decoded == "h\u{e9}llo"
# => true

JWTs: Reading a Token Nobody Signed for You to Read

A JSON Web Token is three Base64 strings stapled together with dots: header, payload, signature. The first two are URL-safe, unpadded Base64 of JSON documents, which means a token is readable by anyone who ever sees it - including you, with no library at all:

require "base64"
require "json"
token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIn0.dW5zaWduZWQ"
header_part, payload_part = token.split(".")[0, 2]
JSON.parse(Base64.urlsafe_decode64(payload_part))
# => {"sub"=>"1234567890", "name"=>"Alice"}

For real work you will use the jwt gem, which handles the part that actually protects you - the signature - and the claim validations:

# Gemfile: gem "jwt"
require "jwt"
token = JWT.encode(
  { sub: "1234567890", name: "Alice", exp: Time.now.to_i + 3600 },
  "my-secret-key",
  "HS256"
)
payload, header = JWT.decode(token, "my-secret-key", true, algorithm: "HS256")
puts payload["name"]
# => Alice

Two security notes belong here, because both have cost people real incidents. First, the payload is not encrypted; decoding it is reading, not cracking, and the signature is the only protection, so never treat a decoded payload as trusted input. Second, pin the algorithm in JWT.decode exactly as shown. Omitting it lets the token's own header decide how it is verified, and that one bit of flexibility is what the famous JWT algorithm-confusion attacks exploit.

Basic Auth: The Password Hidden in Plain Sight

The oldest authentication header on the web is Base64 itself. HTTP Basic auth sends the credentials as user:password, encoded, after the word Basic - and the header rides along on every request, so it shows up in every log you will ever debug. Decoding one is a strip-and-split job:

require "base64"
header_value = "Basic YWxpY2U6czNjcjN0IQ=="
b64 = header_value.sub("Basic ", "")
decoded = Base64.decode64(b64)
user, password = decoded.split(":", 2)
puts user
# => alice
puts password
# => s3cr3t!

The limit of 2 in split matters: a password may legally contain colons, and you only ever want to cut at the first one. Ruby's own standard library builds this header the other way around, in Net::HTTP, using the core pack template directly:

require "net/http"
request = Net::HTTP::Get.new("https://example.org/api")
request.basic_auth("alice", "s3cr3t!")
puts request["Authorization"]
# => Basic YWxpY2U6szNjcjN0IQ==

And the security note that has to be said even though it is obvious: Base64 is a translator, not a lock. Basic auth is only acceptable over HTTPS. The encoding exists so that credentials do not have to be printable text on the wire, not so that they are secret.

Data URIs: The Image That Is Not a File

A data URI hides a whole file inside a URL: a media type, the word base64, a comma, and the encoded bytes. Browsers render them in img tags and CSS, and single-file HTML apps love them because there is no second request to make. Building one in Ruby takes a line:

require "base64"
png = File.binread("logo.png")
data_uri = "data:image/png;base64,#{Base64.strict_encode64(png)}"

Decoding one is the reverse, with two details that trip people up. The comma is the separator, so split exactly once, and the media type part can be anything, including nothing:

data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..."
media_part, b64 = data_uri.split(",", 2)
puts media_part
# => data:image/png;base64
bytes = Base64.strict_decode64(b64)
File.binwrite("restored.png", bytes)

Use strict_decode64 here, not decode64: a data URI payload is a single clean line, and you want a loud error if it is corrupted. Also keep the size tax in mind - every image you inline grows by about a third - so data URIs are perfect for favicons, small logos and fonts, and a bad idea for hero photos.

Email: Sixty-Character Lines and the Mail Gem

Base64 was invented for email, and the scars show. SMTP was designed for short lines of seven-bit text, so MIME Base64 wraps its output into short lines, and a compliant decoder must ignore the line breaks. Ruby's decode64 behaves exactly that way, so a wrapped MIME body is easy food:

body = "Zm9vCmJh\ncgptYW4=\n"
Base64.decode64(body)
# => "foo\nbar\nman"

You will rarely write that by hand. The mail gem does the whole MIME job for you: attachments are Base64 encoded automatically, the lines are wrapped, and the correct headers are attached:

# Gemfile: gem "mail"
require "mail"
message = Mail.new do |m|
  m.from = "dev@example.org"
  m.to = "ops@example.org"
  m.subject = "Binary report"
  m.add_file("report.bin")
end
puts message.encoded
# the attachment part carries Content-Transfer-Encoding: base64

The same trick hides inside email headers. A non-ASCII subject line arrives as an RFC 2047 encoded word: a charset, the letter B, and Base64 between question marks. Decoding one by hand is a small exercise in string surgery:

header_value = "=?UTF-8?B?w7wgc2VjcmV0cw==?="
charset, kind, b64 = header_value.sub(/\A=\?/, "").sub(/\?=$/, "").split("?")
text = Base64.decode64(b64).force_encoding(charset)
puts text
# => ΓΌ secrets

PEM: Keys and Certificates in Armor

Keys and certificates spend most of their lives inside PEM armor: a BEGIN line, a block of Base64, and an END line. The armor comes from the 1980s - Privacy-Enhanced Mail is where the whole Base64 lineage starts - but it is still the format your .crt and .key files wear today.

Decoding a PEM file by hand is just stripping the armor and letting the lenient decoder chew through the line breaks:

require "base64"
pem = File.read("server.key")
body = pem.lines
  .reject { |line| line.start_with?("-----") || line.strip.empty? }
  .join
key_bytes = Base64.decode64(body)

For actual use you will usually skip the manual step and hand the whole PEM string to OpenSSL, which reads the armor itself:

require "openssl"
key = OpenSSL::PKey.read(File.read("server.key"))
puts key.class
# => OpenSSL::PKey::RSA, or whatever the key turns out to be

The only interoperability detail worth knowing: PEM lines are classically 64 characters long, and the decoder ignores line breaks regardless, so a 60-character wrapper or one giant line will decode just as well.

Files and the .b64 Convention

The most common file format in the Base64 world is a plain text file with a .b64 (or sometimes .base64) extension holding one encoded payload. Reading one is a three-step round trip:

require "base64"
encoded = File.read("payload.b64")
bytes = Base64.decode64(encoded)
File.binwrite("payload.bin", bytes)

Use File.binwrite on the way out - a decoded PNG or zip is binary, and text-mode writing would corrupt it on platforms that translate line endings. If your .b64 file came from a tool that wrapped lines, decode64 handles the line breaks for free. If you want to validate instead of tolerate, read the file in binary mode and strip the line breaks before a strict decode:

encoded = File.binread("payload.b64")
clean = encoded.delete("\r\n")
bytes = Base64.strict_decode64(clean)

The binary read matters on Windows, where text mode rewrites CRLF line endings as LF - exactly the kind of mutation you do not want happening inside a string you are about to validate.

URL-Safe Base64: Payloads That Travel in Links

This is the decoder's take on the URL-safe variant, because the choice you make here changes which of the three decoders you reach for. URL-safe Base64 (RFC 4648, section 5) swaps the two characters that URLs do not like - + becomes -, / becomes _ - and usually drops the padding as well. In Ruby you will meet it in query parameters, cookie values, API identifiers, YouTube-style video IDs, and of course JWTs.

Here is how the three decoders behave on the same inputs, because the differences are exactly where bugs are born:

Input decode64 strict_decode64 urlsafe_decode64
aGVsbG8= (standard, padded) "hello" "hello" "hello"
aGVsbG8 (no padding) "hello" ArgumentError "hello"
SGVsbG8gd29ybGQ- (hyphen in last group) "Hello world" (one byte short!) ArgumentError 12 bytes, the correct answer
aGVsbG8=\n (trailing line break) "hello" ArgumentError ArgumentError
aGVs!bG8= (stray exclamation mark) "hello" ArgumentError ArgumentError

Row three is the row that bites people. A URL-safe payload decoded with the standard decoder quietly loses its last byte instead of raising anything, because decode64 simply ignores the hyphen. If a payload can come from a URL, decode it with urlsafe_decode64.

One practical note: if you ever need to move a URL-safe payload to a context that only understands the standard alphabet (a library, a foreign system), the classic interop trick - translate the alphabet and add the padding yourself - is three lines:

def standardize_urlsafe(b64)
  b64 = b64.tr("-_", "+/")
  b64 += "=" * ((4 - b64.length % 4) % 4)
  b64
end
Base64.strict_decode64(standardize_urlsafe("SGVsbG8gd29ybGQ"))
# => "Hello world"

You will rarely need it - urlsafe_decode64 already pads for you - but it is the pattern to recognize in other people's code, and the pattern to reach for when the standard alphabet is what the other side expects.

Config, Environment Variables and Databases

Base64 shows up in configuration whenever binary data has to sit inside a text document. A .env file, a YAML config, or a JSON settings blob cannot safely carry raw bytes, so the bytes get encoded, and something in your application has to decode them at startup:

require "base64"
b64 = ENV.fetch("APP_LOGO")
bytes = Base64.decode64(b64)
File.binwrite("logo.png", bytes)

YAML gets a special mention, because the format has a native binary tag. When you dump a BINARY string, Psych writes it out as a !binary scalar holding Base64, and loading it gives your bytes back intact - no manual encoding at all:

require "yaml"
yaml_text = YAML.dump({ "logo" => File.binread("logo.png") })
puts yaml_text.lines.first(2)
# => "---"
# => "logo: !binary |-"
data = YAML.load(yaml_text)
puts data["logo"].encoding
# => ASCII-8BIT

In databases the rule of thumb is: if your database has a real binary type, use it. Base64-in-a-TEXT-column is the pattern you reach for when the storage layer only speaks strings - some document stores, JSON-shaped APIs, or a legacy schema you cannot change - and the price is the one-third size tax on the column, plus the discipline to decode on the way in and re-encode on the way out at every boundary.

Big Inputs, Steady Memory

The module is buffer-based: a decode call reads the whole string at once and returns the whole result. There is no streaming decoder in the standard library, so the honest advice for large payloads is to plan the memory. The good news is that decoding only ever makes things smaller - the output is at most three quarters of the input - so the input string is your only big allocation.

If a payload is large enough to worry you, you can decode it in four-character groups, because Base64 groups of four are self-contained and the final partial group carries its own padding:

require "base64"
def decode_in_chunks(b64)
  b64.scan(/.{1,4}/).reduce("") do |result, group|
    result + Base64.strict_decode64(group)
  end
end
restored = decode_in_chunks(Base64.strict_encode64("a" * 1_000_000))
puts restored.length
# => 1000000

This works on clean, unwrapped input - the same rules strict_decode64 enforces - because a lone trailing group is only valid with its padding present. For the truly huge files, multi-gigabyte archives and the like, the pattern is to read the file in slices, decode each slice, and stream the bytes to disk, so only one slice is ever in memory at a time.

One-Liners for the Terminal

You do not need a script file to decode things in the shell. Ruby can require the module on the fly:

ruby -rbase64 -e 'puts Base64.decode64(ARGV[0])' "aGVsbG8gd29ybGQ="
# => hello world

And for files, read from the argument instead of argv:

ruby -rbase64 -e 'print Base64.decode64(File.read(ARGV[0]))' payload.b64 > payload.bin

Two gotchas live here. First, if you pipe through echo or any text command, a trailing line break rides along, and strict_decode64 will raise on it - use decode64, or chomp the input:

echo "aGVsbG8gd29ybGQ=" | ruby -rbase64 -e 'print Base64.strict_decode64(STDIN.read.chomp)'

Second, keep print instead of puts for binary output, because puts appends a line break of its own and would corrupt the last byte of your restored file.

Pitfalls Ruby Developers Actually Hit

  • decode64 never raises. Garbage in, garbage out. If your input is untrusted and you silently accept corrupted bytes, the bug will surface weeks later in a corrupted file, not at the decode line. Default to a strict decoder for anything you did not encode yourself.
  • strict_decode64 and the trailing line break. Text files, echo pipes and copy-paste all like to end with a newline, and the strict decoder raises ArgumentError on it. chomp the input first - or read it in binary mode and delete the line breaks.
  • Forgetting the encoding step. A decoded string is BINARY until you say otherwise. Force UTF-8 (and check validity) before treating the result as text, or you will get mojibake and Encoding::CompatibilityError the moment you mix it with UTF-8 strings.
  • Comparing BINARY with UTF-8. Same bytes, different tags, and == says false - unless the string happens to be pure ASCII. Relabel before you compare.
  • URL-safe input through the wrong decoder. Hyphens and underscores are silently dropped by decode64, so a URL-safe payload comes back one byte short and corrupted, with no error at all. Use urlsafe_decode64.
  • Data after the padding is invisible. decode64 stops at the first =. Great for MIME, terrible for catching a payload that was truncated and then re-padded by some other tool.
  • Non-canonical padding is quietly accepted. A string like QR== carries pad bits that a proper encoder would have zeroed; decode64 decodes it happily while strict_decode64 rejects it. Nothing will ever tell you your encoder was lying.
  • Text-mode file reads on Windows rewrite line endings before you ever see them. Read .b64 files in binary mode when you intend to validate them.

Good Habits for the Decode Side

  • Pick the decoder from the source of the data: strict_decode64 for anything untrusted (and rescue ArgumentError as your invalid-input branch), urlsafe_decode64 for URL-born payloads, decode64 only for formats that are genuinely lenient, like MIME bodies.
  • The moment bytes are decoded, decide their identity: binary (keep ASCII-8BIT, write with File.binwrite) or text (force_encoding to UTF-8, then valid_encoding? before use).
  • Never decode and trust. A JWT payload is readable precisely because it is Base64; the signature decides whether it is real. A Base64 string in a config file is data, not proof.
  • When you write validators, test them against the boring cases: the empty string, unpadded input, wrapped input, URL-safe input, and wrong padding. Those are the cases that separate the three decoders.

A Brief History of Base64 in Ruby

The Base64 module has been part of Ruby's standard library for over fifteen years, and the way it ships has changed more than you might expect:

  • 2008, Ruby 1.8.7: the module ships with encode64, decode64, plus two methods that no longer exist - b64encode (wrapping at a chosen line length) and decode_b (RFC 2047 email header decoding). Old books and even some old gems still reference them, and calling either today is a NoMethodError.
  • 2010-2011, the 1.9 line: strict_encode64, strict_decode64, urlsafe_encode64 and urlsafe_decode64 arrive, and the two legacy methods are retired.
  • 2015, Ruby 2.3: urlsafe_encode64 gains the padding: keyword, letting you emit unpadded output for tokens and URLs.
  • 2020, Ruby 3.0: base64 is extracted from the standard library into its own gem, version 0.1.0, under the ruby/base64 repository. It ships as a default gem, so require "base64" still just works.
  • 2023, Ruby 3.3: version 0.2.0 adds Base64::VERSION and a much richer documentation set.
  • 2024, Ruby 3.4: the gem is reclassified from a default gem to a bundled gem. The practical consequence: in Bundler-based projects on Ruby 3.4 and later, list gem "base64" in your Gemfile (or install it with gem install base64).
  • 2025, Ruby 4.0: version 0.3.0 lands, adding RBS type signatures among other maintenance.

Through all of it, one fact never changed: the module is a few dozen lines of pure Ruby sitting on top of the core pack and unpack templates. No C extension, no dependencies, nothing to build - and a download count in the hundreds of millions on rubygems.org.

Ruby Trivia for the Curious

  • The entire decoding side of the module is two method bodies: str.unpack1("m") and str.unpack1("m0"). You can delete the require and write it yourself.
  • Ruby's own Net::HTTP does not even use the Base64 module for Basic auth - it calls the pack template directly: "user:pass".pack("m0").
  • Rails' signed and encrypted cookies are Base64 strings under the hood: ActiveSupport's message codec picks strict_encode64 for regular cookies and urlsafe_encode64 with padding: false for URL-safe signed IDs. You have probably decoded one without knowing it.
  • Every digest class has a base64digest method - Digest::SHA256.base64digest("hello") - a one-liner for checksums that need to live in text.
  • YAML's !binary tag is Base64. Dump a BINARY string with Psych and the format quietly does the encoding for you.
  • decode64 does not care whether your lines are 60, 64 or 76 characters, or one giant line. The m template skips the line breaks, so wrapped and unwrapped input decode identically.

Keep Going

You now have the full decoding toolkit: a lenient reader for MIME-shaped blobs, a strict gatekeeper for everything untrusted, a URL-safe diplomat for tokens and links, and the encoding step that turns the resulting bytes into text Ruby will let you use. The reverse direction - deciding which of Ruby's three encoders to feed your bytes into, and controlling the alphabet, the padding and the line breaks - carries its own set of surprises, starting with a trailing newline nobody asked for. That side of the street is covered in depth in the Base64 encoding article, linked below.

Last updated: 2026-08-29

Related article: Base64 Encoding in Ruby: A Complete Guide