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
String.pemToDer(label)usessubstringAfter/substringBeforewith an empty fallback, so a missing block fails therequireinstead of decoding the whole file.Base64.getMimeDecoder()skips line breaks; the result is the DER — 563 bytes for the certificate, 138 for the PKCS#8 key.ByteArray.derToPem(label)chunks the base64 into 64-character lines, so the output is identical to OpenSSL's PEM.- The DER feeds the JCA directly:
KeyFactory.getInstance("EC").generatePrivate(PKCS8EncodedKeySpec(der))reports formatPKCS#8.
Gotchas
- The label must match exactly: asking for
EC PRIVATE KEYin aPRIVATE KEYfile 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
- How to convert a certificate and a private key between PEM and DER in Java
- How to convert a certificate and a private key between PEM and DER in Swift
- How to convert a certificate and a private key between PEM and DER with OpenSSL
- How to export a public key as PEM in Kotlin
- How to decode an X.509 certificate in Kotlin