文字列のエンコードとデコード
Base64.encode は ByteArray を受け取って String を返します。UTF-8 の変換は、encodeToByteArray() と decodeToString() が両方向で担当します。
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 自体が標準のインスタンス(Base64.Default)です。ほかに Base64.UrlSafe と Base64.Mime があります。
Kotlin 2.2 より古いバージョンでは、この API に @OptIn(ExperimentalEncodingApi::class) が必要です。古いバージョンから移行できない場合は、Java での Base64 と同じように java.util.Base64 が使えます。
java.util.Base64.getEncoder().encodeToString("hi".toByteArray()) // aGk=
典型的な用途は HTTP ヘッダーと Data URI です。
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セーフな Base64 とパディングのオプション
Base64.UrlSafe は +/ を -_ に置き換えます。パディングは、設定済みの新しいインスタンスを返す withPadding で制御します。
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()) // <<???>>
4 つのオプション:
| オプション | エンコード時 | デコード時 |
|---|---|---|
PRESENT(デフォルト) | = を付ける | = を要求 |
ABSENT | = なし | = を拒否 |
PRESENT_OPTIONAL | = を付ける | 両方を受理 |
ABSENT_OPTIONAL | = なし | 両方を受理 |
デフォルトのインスタンスはこの点に厳格です。
Base64.UrlSafe.decode("PDw_Pz8-Pg")
// IllegalArgumentException: The padding option is set to PRESENT,
// but the input is not properly padded
JWT のパートやその他のトークンでは、生成側によってパディングの扱いがまちまちなので、たいていは ABSENT_OPTIONAL か PRESENT_OPTIONAL が適切です。背景については Base64 と Base64URL の違い と パディングの解説 をご覧ください。
たとえば JWT のペイロードを読むには次のようにします。
val jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.sig"
val url = Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL)
println(url.decode(jwt.split(".")[1]).decodeToString()) // {"sub":"1234"}
MIME の折り返し
Base64.Mime は 76 文字の行を \r\n で区切って出力し、デコード時には行区切りを無視します。
Base64.Mime.encode("x".repeat(60).encodeToByteArray())
// "eHh4eHh4...eHh4" (76 chars) + "\r\n" + "eHh4"
Base64.Mime.decode("aGVs\r\nbG8K").decodeToString() // "hello\n"
比べるには、同じ入力を MIME Base64 エンコーダー に貼り付けてください。
ファイルのエンコードとデコード
小さなファイルはメモリに収まります。
import java.io.File
println(Base64.encode(File("hello.txt").readBytes())) // aGVsbG8K
File("copy.txt").writeBytes(Base64.decode("aGVsbG8K"))
大きなファイルのストリーム処理
JVM では、encodingWith と decodingWith でストリームをラップします。Kotlin 2.4 でもまだ実験的機能なので、オプトインと個別のインポートが必要です。
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) }
}
}
エンコード用のストリームを閉じると、最後のグループとパディングが書き出されます。閉じる処理は use ブロックが代わりに行います。300 KB のテストファイルをストリーム処理した出力は Base64.encode(readBytes()) と同一で、往復変換でも元と同じバイト列が戻りました。実験的でない方法がよければ、java.util.Base64.getEncoder().wrap(outputStream) で同じことができます。
検証と例外メッセージ
Kotlin のデコーダーは Java のものより厳格です。Java を含むほとんどのライブラリが受け付ける 0 でないパディングビットを拒否します。失敗はすべて IllegalArgumentException です。
Base64.decode への入力 | メッセージ |
|---|---|
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 |
a | Input should have at least 2 symbols for Base64 decoding, startIndex: 0, endIndex: 1 |
括弧内の数値は文字コードを 8 進数で表したものです(137 は _、12 は改行)。検証用のヘルパーは 1 行で書けます。
fun isBase64(s: String) = runCatching { Base64.decode(s) }.isSuccess
特定の文字列がどこで壊れているかを見るには Base64 バリデーター を使ってください。よくある原因は Base64 のエラーの解説 にまとめています。
落とし穴
- パディングのない入力をデフォルトのインスタンスに渡す。 JWT のセグメントや多くのトークンには
=がありません。文字列を手作業でパディングするのではなく、PRESENT_OPTIONALかABSENT_OPTIONALを設定してください。 - Android の
DEFAULTフラグ。android.util.Base64.encodeToString(bytes, Base64.DEFAULT)は 76 文字ごとと末尾に改行を挿入します。HTTP ヘッダーではこれがリクエストを壊します。NO_WRAPを、トークンにはNO_WRAP or URL_SAFE or NO_PADDINGを使ってください。 String.toByteArray()の文字セット。 古い JDK での Java のgetBytes()と違い、Kotlin ではデフォルトが UTF-8 なので、"héllo".toByteArray()は安全です。- バイナリを文字列にデコードする。 画像のバイト列に
decodeToString()を使うと、不正なシーケンスがU+FFFDに置き換えられてデータが壊れます。ByteArrayのまま扱ってください。
バージョンごとの注意
- Kotlin 1.8.20:
kotlin.io.encoding.Base64が実験的機能として追加 - Kotlin 2.0.20:
withPaddingとPaddingOption - Kotlin 2.2:コア API が安定版に。JVM のストリーム用ヘルパーは実験的機能のまま
よくある質問
@OptIn(ExperimentalEncodingApi::class) はまだ必要ですか?
Kotlin 2.2 以降の Base64.encode と Base64.decode には不要です。ストリーム用ヘルパーの encodingWith と decodingWith は、Kotlin 2.4 でもまだオプトインが必要です。
Kotlin でパディングのない Base64 をデコードするには?
設定済みのインスタンスを使います。パディングのない入力には Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT) を、両方を受け付けるなら PRESENT_OPTIONAL を使ってください。デフォルトのインスタンスはパディングを要求します。
android.util.Base64 と kotlin.io.encoding.Base64 のどちらを使うべきですか?
新しいコードなら、Kotlin の API は標準ライブラリの一部なので、どの Android バージョンでも動作します。android.util.Base64 は Base64.NO_WRAP を渡さないと改行を入れるため、ヘッダーが壊れる原因としてよく知られています。