Загрузить файл
  1. base64hub
  2. Base64 в коде
  3. Kotlin
Код

Base64 в Kotlin

В Kotlin есть мультиплатформенный объект kotlin.io.encoding.Base64, стабильный с Kotlin 2.2. На JVM можно пользоваться и java.util.Base64. Результаты ниже получены на Kotlin 2.4.21 и Java 21.

Закодировать и декодировать строку

Base64.encode принимает ByteArray и возвращает String. encodeToByteArray() и decodeToString() работают с UTF-8 в обе стороны:

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.util.Base64 работает так же, как на странице Base64 в Java:

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-safe 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())  // <<???>>

Четыре варианта:

ВариантКодированиеДекодирование
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 они всё ещё экспериментальные, так что им нужны opt-in и отдельные импорты:

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 КБ: потоковый вывод совпал с Base64.encode(readBytes()), а после обратного декодирования получился исходный файл байт в байт. Если не хочется экспериментального API, ту же работу делает java.util.Base64.getEncoder().wrap(outputStream).

Проверка и тексты исключений

Декодер Kotlin строже, чем в Java. Он отвергает ненулевые биты дополнения, которые Java и большинство других библиотек принимают. Любой сбой – это 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
aInput should have at least 2 symbols for Base64 decoding, startIndex: 0, endIndex: 1

Число в скобках – код символа в восьмеричной системе (137 – это _, 12 – перевод строки). Функция проверки умещается в одну строку:

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

Чтобы увидеть, где именно ломается конкретная строка, откройте валидатор Base64; обычные причины перечислены в статье об ошибках Base64.

Подводные камни

  • Строки без дополнения и экземпляр по умолчанию. У сегментов JWT и многих токенов нет =. Настройте PRESENT_OPTIONAL или ABSENT_OPTIONAL, тогда дописывать дополнение вручную не придётся.
  • Флаг DEFAULT в Android. android.util.Base64.encodeToString(bytes, Base64.DEFAULT) вставляет перевод строки через каждые 76 символов и в конце. В HTTP-заголовке это ломает запрос. Используйте NO_WRAP, а для токенов – NO_WRAP or URL_SAFE or NO_PADDING.
  • Кодировка String.toByteArray(). В Kotlin по умолчанию это UTF-8, в отличие от getBytes() в Java на старых JDK, так что "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)?

Для Base64.encode и Base64.decode в Kotlin 2.2 и новее – нет. Потоковым помощникам encodingWith и decodingWith в Kotlin 2.4 opt-in всё ещё нужен.

Как в Kotlin декодировать Base64 без дополнения?

Используйте настроенный экземпляр: Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT) для строк без дополнения или PRESENT_OPTIONAL, чтобы принимать оба варианта. Экземпляры по умолчанию требуют дополнения.

Что брать: android.util.Base64 или kotlin.io.encoding.Base64?

В новом коде API Kotlin работает на любой версии Android, потому что входит в стандартную библиотеку. android.util.Base64 добавляет переносы строк, если не передать Base64.NO_WRAP, и это частая причина сломанных заголовков.

Читайте также