Upload a file
  1. base64hub
  2. Base64 in Code
  3. Kotlin
Code

Base64 in Kotlin

Kotlin ships a multiplatform kotlin.io.encoding.Base64 object, stable since Kotlin 2.2. On the JVM you can also use java.util.Base64. Outputs below come from Kotlin 2.4.21 on Java 21.

Encode and decode a string

Base64.encode takes a ByteArray and returns a String. encodeToByteArray() and decodeToString() handle UTF-8 in both directions:

import kotlin.io.encoding.Base64

val enc = Base64.encode("héllo wörld".encodeToByteArray())
println(enc)                                // aMOpbGxvIHfDtnJsZA==
println(Base64.decode(enc).decodeToString()) // héllo wörld

Base64 itself is the standard instance (Base64.Default). There are also Base64.UrlSafe and Base64.Mime.

On Kotlin older than 2.2, the API needs @OptIn(ExperimentalEncodingApi::class). If you are stuck on an old version, java.util.Base64 works the same as in Base64 in Java:

java.util.Base64.getEncoder().encodeToString("hi".toByteArray())  // aGk=

Typical uses are HTTP headers and data URIs:

val token = Base64.encode("user:pass".encodeToByteArray())
println("Authorization: Basic $token")       // Authorization: Basic dXNlcjpwYXNz

val png = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47)
println("data:image/png;base64," + Base64.encode(png))  // data:image/png;base64,iVBORw==

URL-safe Base64 and padding options

Base64.UrlSafe swaps +/ for -_. Padding is controlled by withPadding, which returns a new configured instance:

val d = "<<???>>".encodeToByteArray()

println(Base64.UrlSafe.encode(d))                  // PDw_Pz8-Pg==

val urlNoPad = Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT)
println(urlNoPad.encode(d))                        // PDw_Pz8-Pg
println(urlNoPad.decode("PDw_Pz8-Pg").decodeToString())  // <<???>>

The four options:

OptionEncodingDecoding
PRESENT (default)writes =requires =
ABSENTno =rejects =
PRESENT_OPTIONALwrites =accepts both
ABSENT_OPTIONALno =accepts both

The default instances are strict about it:

Base64.UrlSafe.decode("PDw_Pz8-Pg")
// IllegalArgumentException: The padding option is set to PRESENT,
// but the input is not properly padded

For JWT parts and other tokens, ABSENT_OPTIONAL or PRESENT_OPTIONAL is usually the right choice, since producers disagree about padding. Background: Base64 vs Base64URL and padding explained.

Reading a JWT payload, for example:

val jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.sig"
val url = Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL)
println(url.decode(jwt.split(".")[1]).decodeToString())   // {"sub":"1234"}

MIME line wrapping

Base64.Mime writes 76-character lines separated by \r\n and ignores line separators while decoding:

Base64.Mime.encode("x".repeat(60).encodeToByteArray())
// "eHh4eHh4...eHh4" (76 chars) + "\r\n" + "eHh4"

Base64.Mime.decode("aGVs\r\nbG8K").decodeToString()  // "hello\n"

To compare, paste the same input into the MIME Base64 encoder.

Encode and decode a file

Small files fit in memory:

import java.io.File

println(Base64.encode(File("hello.txt").readBytes()))   // aGVsbG8K
File("copy.txt").writeBytes(Base64.decode("aGVsbG8K"))

Streaming large files

On the JVM, encodingWith and decodingWith wrap streams. They are still experimental in Kotlin 2.4, so they need an opt-in and their own imports:

import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.io.encoding.decodingWith
import kotlin.io.encoding.encodingWith
import java.io.File

@OptIn(ExperimentalEncodingApi::class)
fun encodeFile(src: File, dst: File) {
    dst.outputStream().encodingWith(Base64).use { out ->
        src.inputStream().use { it.copyTo(out) }
    }
}

@OptIn(ExperimentalEncodingApi::class)
fun decodeFile(src: File, dst: File) {
    src.inputStream().decodingWith(Base64).use { inp ->
        dst.outputStream().use { inp.copyTo(it) }
    }
}

Closing the encoding stream, which the use block does for you, writes the last group and padding. The streamed output of a 300 KB test file was identical to Base64.encode(readBytes()), and the round trip gave back the same bytes. If you prefer a non-experimental route, java.util.Base64.getEncoder().wrap(outputStream) does the same job.

Validation and exception messages

The Kotlin decoder is stricter than Java’s. It rejects non-zero padding bits, which Java and most other libraries accept. Every failure is an IllegalArgumentException:

Input to Base64.decodeMessage
PDw_Pz8-Pg==Invalid symbol '_'(137) at index 3
"aGVs\nbG8K"Invalid symbol '\n'(12) at index 4
aGk=!!Symbol '!'(41) at index 3 is prohibited after the pad character
aGk==Symbol '='(75) at index 3 is prohibited after the pad character
aGl=The pad bits must be zeros
aInput should have at least 2 symbols for Base64 decoding, startIndex: 0, endIndex: 1

The number in parentheses is the character code in octal (137 is _, 12 is a newline). A validation helper is a one-liner:

fun isBase64(s: String) = runCatching { Base64.decode(s) }.isSuccess

To see where a specific string breaks, use the Base64 validator; the Base64 errors guide lists the usual causes.

Pitfalls

  • Unpadded input with the default instance. JWT segments and many tokens have no =. Configure PRESENT_OPTIONAL or ABSENT_OPTIONAL instead of padding strings by hand.
  • Android DEFAULT flag. android.util.Base64.encodeToString(bytes, Base64.DEFAULT) inserts a newline every 76 characters and at the end. In an HTTP header that breaks the request. Use NO_WRAP, or NO_WRAP or URL_SAFE or NO_PADDING for tokens.
  • String.toByteArray() charset. It defaults to UTF-8 in Kotlin, unlike Java’s getBytes() on older JDKs, so "héllo".toByteArray() is safe.
  • Decoding binary to a string. decodeToString() on image bytes replaces invalid sequences with U+FFFD, corrupting the data. Keep the ByteArray.

Version notes

  • Kotlin 1.8.20: kotlin.io.encoding.Base64 added as experimental.
  • Kotlin 2.0.20: withPadding and PaddingOption.
  • Kotlin 2.2: the core API became stable; the JVM stream helpers remain experimental.

FAQ

Do I still need @OptIn(ExperimentalEncodingApi::class)?

Not for Base64.encode and Base64.decode on Kotlin 2.2 or newer. The stream helpers encodingWith and decodingWith still require the opt-in in Kotlin 2.4.

How do I decode Base64 without padding in Kotlin?

Use a configured instance: Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT) for unpadded input, or PRESENT_OPTIONAL to accept both. The default instances require padding.

Should I use android.util.Base64 or kotlin.io.encoding.Base64?

In new code, the Kotlin API works on every Android version because it is part of the standard library. android.util.Base64 adds line breaks unless you pass Base64.NO_WRAP, a frequent source of broken headers.

Keep reading