Skip to content
ecdsa.com

PEM ⇄ DER · Kotlin

How to convert a certificate and a private key between PEM and DER in Kotlin

To convert a certificate or a key between PEM and DER in Kotlin, take the text between the BEGIN and END lines and decode it with java.util.Base64's MIME decoder; to go back, base64-encode and chunk the result into 64-character lines. This recipe does both as two extension functions and checks the round trip byte for byte.

Tested with Kotlin 2.4.21 (Kotlin/JVM, OpenJDK 25.0.4.1 (Temurin), Gradle 9.8.1) and OpenSSL 3.0.13 (reads the DER output).

Pem.kt — PEM ⇄ DER as extension functions
import java.util.Base64
/** The DER bytes inside the PEM block with the given label ("CERTIFICATE", "PRIVATE KEY", ...). */
fun String.pemToDer(label: String): ByteArray {
val body = substringAfter("-----BEGIN $label-----", missingDelimiterValue = "")
.substringBefore("-----END $label-----", missingDelimiterValue = "")
require(body.isNotBlank()) { "no $label block" }
return Base64.getMimeDecoder().decode(body) // the MIME decoder skips the line breaks
}
/** DER bytes as a PEM block: base64 in 64-character lines between BEGIN and END. */
fun ByteArray.derToPem(label: String): String = buildString {
append("-----BEGIN $label-----\n")
Base64.getEncoder().encodeToString(this@derToPem).chunked(64).forEach { append(it).append('\n') }
append("-----END $label-----\n")
}

How it works

  1. String.pemToDer(label) uses substringAfter / substringBefore with an empty fallback, so a missing block fails the require instead of decoding the whole file.
  2. Base64.getMimeDecoder() skips line breaks; the result is the DER — 563 bytes for the certificate, 138 for the PKCS#8 key.
  3. ByteArray.derToPem(label) chunks the base64 into 64-character lines, so the output is identical to OpenSSL's PEM.
  4. The DER feeds the JCA directly: KeyFactory.getInstance("EC").generatePrivate(PKCS8EncodedKeySpec(der)) reports format PKCS#8.

Gotchas

  • The label must match exactly: asking for EC PRIVATE KEY in a PRIVATE KEY file fails (no EC PRIVATE KEY block) — they are different structures, not spellings.
  • Base64.getDecoder() (the basic one) throws on the line breaks of PEM; only the MIME decoder ignores them.

Related recipes