Skip to content

Commit 963bdc8

Browse files
committed
[PEM] Implement new PemDocument API
Fix tests because of different line endings: Base64.Pem uses CRLF and not just `\n`
1 parent dd58e1c commit 963bdc8

File tree

13 files changed

+288
-86
lines changed

13 files changed

+288
-86
lines changed

cryptography-providers/base/src/commonMain/kotlin/materials/keys.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,21 @@ import dev.whyoleg.cryptography.*
88
import dev.whyoleg.cryptography.serialization.asn1.*
99
import dev.whyoleg.cryptography.serialization.asn1.modules.*
1010
import dev.whyoleg.cryptography.serialization.pem.*
11+
import kotlinx.io.bytestring.unsafe.*
1112

13+
@OptIn(UnsafeByteStringApi::class)
1214
@CryptographyProviderApi
1315
public fun unwrapPem(label: PemLabel, key: ByteArray): ByteArray {
14-
return Pem.decode(key).ensurePemLabel(label).bytes
16+
val document = PemDocument.decode(key)
17+
check(document.label == label) { "Wrong PEM label, expected $label, actual ${document.label}" }
18+
UnsafeByteStringOperations.withByteArrayUnsafe(document.content) { return it }
1519
}
1620

21+
@OptIn(UnsafeByteStringApi::class)
1722
@CryptographyProviderApi
1823
public fun wrapPem(label: PemLabel, key: ByteArray): ByteArray {
19-
return Pem.encodeToByteArray(PemContent(label, key))
24+
val document = PemDocument(label, UnsafeByteStringOperations.wrapUnsafe(key))
25+
return document.encodeToByteArray()
2026
}
2127

2228
@CryptographyProviderApi

cryptography-providers/tests/src/commonMain/kotlin/compatibility/EcCompatibilityTest.kt

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ abstract class EcCompatibilityTest<PublicK : EC.PublicKey, PrivateK : EC.Private
4242
}
4343

4444
protected inline fun generateCurves(block: (curve: EC.Curve) -> Unit) {
45-
generate(block,
45+
generate(
46+
block,
4647
EC.Curve.P256, EC.Curve.P384, EC.Curve.P521,
4748
EC.Curve.secp256k1,
4849
EC.Curve.brainpoolP256r1, EC.Curve.brainpoolP384r1, EC.Curve.brainpoolP512r1,
@@ -91,10 +92,17 @@ abstract class EcCompatibilityTest<PublicK : EC.PublicKey, PrivateK : EC.Private
9192
EC.PublicKey.Format.RAW,
9293
EC.PublicKey.Format.RAW.Compressed,
9394
EC.PublicKey.Format.DER,
94-
EC.PublicKey.Format.PEM,
9595
-> {
9696
assertContentEquals(bytes, key.encodeToByteString(format), "Public Key $format encoding")
9797
}
98+
EC.PublicKey.Format.PEM -> {
99+
val expected = PemDocument.decode(bytes)
100+
val actual = PemDocument.decode(key.encodeToByteString(format))
101+
102+
assertEquals(expected.label, actual.label)
103+
assertEquals(PemLabel.PublicKey, actual.label)
104+
assertContentEquals(expected.content, actual.content, "Public Key $format content encoding")
105+
}
98106
}
99107
}
100108
val privateKeys = privateKeyDecoder.decodeFrom(
@@ -112,23 +120,25 @@ abstract class EcCompatibilityTest<PublicK : EC.PublicKey, PrivateK : EC.Private
112120
assertEcPrivateKeyEquals(byteString.toByteArray(), key.encodeToByteArray(format))
113121
}
114122
EC.PrivateKey.Format.PEM.SEC1 -> {
115-
val expected = Pem.decode(byteString)
116-
val actual = Pem.decode(key.encodeToByteString(format))
123+
val expected = PemDocument.decode(byteString)
124+
val actual = PemDocument.decode(key.encodeToByteString(format))
117125

126+
assertEquals(PemLabel.EcPrivateKey, actual.label)
118127
assertEquals(expected.label, actual.label)
119128

120-
assertEcPrivateKeyEquals(expected.bytes, actual.bytes)
129+
assertEcPrivateKeyEquals(expected.content.toByteArray(), actual.content.toByteArray())
121130
}
122131
EC.PrivateKey.Format.DER -> {
123132
assertPkcs8EcPrivateKeyEquals(byteString.toByteArray(), key.encodeToByteArray(format))
124133
}
125134
EC.PrivateKey.Format.PEM -> {
126-
val expected = Pem.decode(byteString)
127-
val actual = Pem.decode(key.encodeToByteString(format))
135+
val expected = PemDocument.decode(byteString)
136+
val actual = PemDocument.decode(key.encodeToByteString(format))
128137

129138
assertEquals(expected.label, actual.label)
139+
assertEquals(PemLabel.PrivateKey, actual.label)
130140

131-
assertPkcs8EcPrivateKeyEquals(expected.bytes, actual.bytes)
141+
assertPkcs8EcPrivateKeyEquals(expected.content.toByteArray(), actual.content.toByteArray())
132142
}
133143
}
134144
}

cryptography-providers/tests/src/commonMain/kotlin/compatibility/RsaBasedCompatibilityTest.kt

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import dev.whyoleg.cryptography.*
88
import dev.whyoleg.cryptography.algorithms.*
99
import dev.whyoleg.cryptography.providers.tests.*
1010
import dev.whyoleg.cryptography.providers.tests.compatibility.api.*
11+
import dev.whyoleg.cryptography.serialization.pem.*
1112
import kotlinx.serialization.*
13+
import kotlin.test.*
1214

1315
private val publicKeyFormats = listOf(
1416
RSA.PublicKey.Format.JWK,
@@ -89,13 +91,25 @@ abstract class RsaBasedCompatibilityTest<PublicK : RSA.PublicKey, PrivateK : RSA
8991
supports = ::supportsKeyFormat
9092
) { key, format, bytes ->
9193
when (format) {
92-
RSA.PublicKey.Format.DER,
93-
RSA.PublicKey.Format.PEM,
94-
RSA.PublicKey.Format.DER.PKCS1,
95-
RSA.PublicKey.Format.PEM.PKCS1,
96-
->
94+
RSA.PublicKey.Format.DER, RSA.PublicKey.Format.DER.PKCS1 -> {
9795
assertContentEquals(bytes, key.encodeToByteString(format), "Public Key $format encoding")
98-
RSA.PublicKey.Format.JWK -> {}
96+
}
97+
RSA.PublicKey.Format.PEM, RSA.PublicKey.Format.PEM.PKCS1 -> {
98+
val expected = PemDocument.decode(bytes)
99+
val actual = PemDocument.decode(key.encodeToByteString(format))
100+
101+
val expectedLabel = when (format) {
102+
RSA.PublicKey.Format.PEM -> PemLabel.PublicKey
103+
RSA.PublicKey.Format.PEM.PKCS1 -> PemLabel.RsaPublicKey
104+
else -> {}
105+
}
106+
107+
assertEquals(expected.label, actual.label)
108+
assertEquals(expectedLabel, actual.label)
109+
110+
assertContentEquals(expected.content, actual.content, "Public Key $format content encoding")
111+
}
112+
RSA.PublicKey.Format.JWK -> {}
99113

100114
}
101115
}
@@ -105,13 +119,26 @@ abstract class RsaBasedCompatibilityTest<PublicK : RSA.PublicKey, PrivateK : RSA
105119
supports = ::supportsKeyFormat
106120
) { key, format, bytes ->
107121
when (format) {
108-
RSA.PrivateKey.Format.DER,
109-
RSA.PrivateKey.Format.PEM,
110-
RSA.PrivateKey.Format.DER.PKCS1,
111-
RSA.PrivateKey.Format.PEM.PKCS1,
112-
->
122+
RSA.PrivateKey.Format.DER, RSA.PrivateKey.Format.DER.PKCS1 -> {
113123
assertContentEquals(bytes, key.encodeToByteString(format), "Private Key $format encoding")
114-
RSA.PrivateKey.Format.JWK -> {}
124+
}
125+
RSA.PrivateKey.Format.PEM, RSA.PrivateKey.Format.PEM.PKCS1 -> {
126+
val expected = PemDocument.decode(bytes)
127+
val actual = PemDocument.decode(key.encodeToByteString(format))
128+
129+
val expectedLabel = when (format) {
130+
RSA.PrivateKey.Format.PEM -> PemLabel.PrivateKey
131+
RSA.PrivateKey.Format.PEM.PKCS1 -> PemLabel.RsaPrivateKey
132+
else -> {}
133+
}
134+
135+
assertEquals(expected.label, actual.label)
136+
assertEquals(expectedLabel, actual.label)
137+
138+
assertContentEquals(expected.content, actual.content, "Private Key $format content encoding")
139+
}
140+
141+
RSA.PrivateKey.Format.JWK -> {}
115142
}
116143
}
117144
put(keyReference, publicKeys to privateKeys)

cryptography-providers/tests/src/commonMain/kotlin/support.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ fun AlgorithmTestScope<out EC<*, *, *>>.supportsPrivateKeyDecoding(
122122
EC.PrivateKey.Format.RAW -> return false
123123
EC.PrivateKey.Format.DER -> decodePki(key)
124124
EC.PrivateKey.Format.DER.SEC1 -> key
125-
EC.PrivateKey.Format.PEM -> decodePki(Pem.decode(key).byteString)
126-
EC.PrivateKey.Format.PEM.SEC1 -> Pem.decode(key).byteString
125+
EC.PrivateKey.Format.PEM -> decodePki(PemDocument.decode(key).content)
126+
EC.PrivateKey.Format.PEM.SEC1 -> PemDocument.decode(key).content
127127
}
128128
)
129129
}

cryptography-serialization/pem/api/cryptography-serialization-pem.api

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,40 @@ public final class dev/whyoleg/cryptography/serialization/pem/PemContentKt {
2020
public static final fun ensurePemLabel-fi-TaOo (Ldev/whyoleg/cryptography/serialization/pem/PemContent;Ljava/lang/String;)Ldev/whyoleg/cryptography/serialization/pem/PemContent;
2121
}
2222

23+
public final class dev/whyoleg/cryptography/serialization/pem/PemDocument {
24+
public static final field Companion Ldev/whyoleg/cryptography/serialization/pem/PemDocument$Companion;
25+
public synthetic fun <init> (Ljava/lang/String;Lkotlinx/io/bytestring/ByteString;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
26+
public synthetic fun <init> (Ljava/lang/String;[BLkotlin/jvm/internal/DefaultConstructorMarker;)V
27+
public final fun encodeToByteArray ()[B
28+
public final fun encodeToByteString ()Lkotlinx/io/bytestring/ByteString;
29+
public final fun encodeToSink (Lkotlinx/io/Sink;)V
30+
public final fun encodeToString ()Ljava/lang/String;
31+
public fun equals (Ljava/lang/Object;)Z
32+
public final fun getContent ()Lkotlinx/io/bytestring/ByteString;
33+
public final fun getLabel-2EFq_Wg ()Ljava/lang/String;
34+
public fun hashCode ()I
35+
public fun toString ()Ljava/lang/String;
36+
}
37+
38+
public final class dev/whyoleg/cryptography/serialization/pem/PemDocument$Companion {
39+
public final fun decode (Ljava/lang/String;)Ldev/whyoleg/cryptography/serialization/pem/PemDocument;
40+
public final fun decode (Lkotlinx/io/Source;)Ldev/whyoleg/cryptography/serialization/pem/PemDocument;
41+
public final fun decode (Lkotlinx/io/bytestring/ByteString;)Ldev/whyoleg/cryptography/serialization/pem/PemDocument;
42+
public final fun decode ([B)Ldev/whyoleg/cryptography/serialization/pem/PemDocument;
43+
public final fun decodeToSequence (Ljava/lang/String;)Lkotlin/sequences/Sequence;
44+
public final fun decodeToSequence (Lkotlinx/io/Source;)Lkotlin/sequences/Sequence;
45+
public final fun decodeToSequence (Lkotlinx/io/bytestring/ByteString;)Lkotlin/sequences/Sequence;
46+
public final fun decodeToSequence ([B)Lkotlin/sequences/Sequence;
47+
}
48+
2349
public final class dev/whyoleg/cryptography/serialization/pem/PemLabel {
2450
public static final field Companion Ldev/whyoleg/cryptography/serialization/pem/PemLabel$Companion;
2551
public static final synthetic fun box-impl (Ljava/lang/String;)Ldev/whyoleg/cryptography/serialization/pem/PemLabel;
2652
public static fun constructor-impl (Ljava/lang/String;)Ljava/lang/String;
2753
public fun equals (Ljava/lang/Object;)Z
2854
public static fun equals-impl (Ljava/lang/String;Ljava/lang/Object;)Z
2955
public static final fun equals-impl0 (Ljava/lang/String;Ljava/lang/String;)Z
30-
public final fun getRepresentation ()Ljava/lang/String;
56+
public final fun getValue ()Ljava/lang/String;
3157
public fun hashCode ()I
3258
public static fun hashCode-impl (Ljava/lang/String;)I
3359
public fun toString ()Ljava/lang/String;

cryptography-serialization/pem/api/cryptography-serialization-pem.klib.api

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,40 @@ final class dev.whyoleg.cryptography.serialization.pem/PemContent { // dev.whyol
1818
final fun <get-label>(): dev.whyoleg.cryptography.serialization.pem/PemLabel // dev.whyoleg.cryptography.serialization.pem/PemContent.label.<get-label>|<get-label>(){}[0]
1919
}
2020

21+
final class dev.whyoleg.cryptography.serialization.pem/PemDocument { // dev.whyoleg.cryptography.serialization.pem/PemDocument|null[0]
22+
constructor <init>(dev.whyoleg.cryptography.serialization.pem/PemLabel, kotlin/ByteArray) // dev.whyoleg.cryptography.serialization.pem/PemDocument.<init>|<init>(dev.whyoleg.cryptography.serialization.pem.PemLabel;kotlin.ByteArray){}[0]
23+
constructor <init>(dev.whyoleg.cryptography.serialization.pem/PemLabel, kotlinx.io.bytestring/ByteString) // dev.whyoleg.cryptography.serialization.pem/PemDocument.<init>|<init>(dev.whyoleg.cryptography.serialization.pem.PemLabel;kotlinx.io.bytestring.ByteString){}[0]
24+
25+
final val content // dev.whyoleg.cryptography.serialization.pem/PemDocument.content|{}content[0]
26+
final fun <get-content>(): kotlinx.io.bytestring/ByteString // dev.whyoleg.cryptography.serialization.pem/PemDocument.content.<get-content>|<get-content>(){}[0]
27+
final val label // dev.whyoleg.cryptography.serialization.pem/PemDocument.label|{}label[0]
28+
final fun <get-label>(): dev.whyoleg.cryptography.serialization.pem/PemLabel // dev.whyoleg.cryptography.serialization.pem/PemDocument.label.<get-label>|<get-label>(){}[0]
29+
30+
final fun encodeToByteArray(): kotlin/ByteArray // dev.whyoleg.cryptography.serialization.pem/PemDocument.encodeToByteArray|encodeToByteArray(){}[0]
31+
final fun encodeToByteString(): kotlinx.io.bytestring/ByteString // dev.whyoleg.cryptography.serialization.pem/PemDocument.encodeToByteString|encodeToByteString(){}[0]
32+
final fun encodeToSink(kotlinx.io/Sink) // dev.whyoleg.cryptography.serialization.pem/PemDocument.encodeToSink|encodeToSink(kotlinx.io.Sink){}[0]
33+
final fun encodeToString(): kotlin/String // dev.whyoleg.cryptography.serialization.pem/PemDocument.encodeToString|encodeToString(){}[0]
34+
final fun equals(kotlin/Any?): kotlin/Boolean // dev.whyoleg.cryptography.serialization.pem/PemDocument.equals|equals(kotlin.Any?){}[0]
35+
final fun hashCode(): kotlin/Int // dev.whyoleg.cryptography.serialization.pem/PemDocument.hashCode|hashCode(){}[0]
36+
final fun toString(): kotlin/String // dev.whyoleg.cryptography.serialization.pem/PemDocument.toString|toString(){}[0]
37+
38+
final object Companion { // dev.whyoleg.cryptography.serialization.pem/PemDocument.Companion|null[0]
39+
final fun decode(kotlin/ByteArray): dev.whyoleg.cryptography.serialization.pem/PemDocument // dev.whyoleg.cryptography.serialization.pem/PemDocument.Companion.decode|decode(kotlin.ByteArray){}[0]
40+
final fun decode(kotlin/String): dev.whyoleg.cryptography.serialization.pem/PemDocument // dev.whyoleg.cryptography.serialization.pem/PemDocument.Companion.decode|decode(kotlin.String){}[0]
41+
final fun decode(kotlinx.io.bytestring/ByteString): dev.whyoleg.cryptography.serialization.pem/PemDocument // dev.whyoleg.cryptography.serialization.pem/PemDocument.Companion.decode|decode(kotlinx.io.bytestring.ByteString){}[0]
42+
final fun decode(kotlinx.io/Source): dev.whyoleg.cryptography.serialization.pem/PemDocument // dev.whyoleg.cryptography.serialization.pem/PemDocument.Companion.decode|decode(kotlinx.io.Source){}[0]
43+
final fun decodeToSequence(kotlin/ByteArray): kotlin.sequences/Sequence<dev.whyoleg.cryptography.serialization.pem/PemDocument> // dev.whyoleg.cryptography.serialization.pem/PemDocument.Companion.decodeToSequence|decodeToSequence(kotlin.ByteArray){}[0]
44+
final fun decodeToSequence(kotlin/String): kotlin.sequences/Sequence<dev.whyoleg.cryptography.serialization.pem/PemDocument> // dev.whyoleg.cryptography.serialization.pem/PemDocument.Companion.decodeToSequence|decodeToSequence(kotlin.String){}[0]
45+
final fun decodeToSequence(kotlinx.io.bytestring/ByteString): kotlin.sequences/Sequence<dev.whyoleg.cryptography.serialization.pem/PemDocument> // dev.whyoleg.cryptography.serialization.pem/PemDocument.Companion.decodeToSequence|decodeToSequence(kotlinx.io.bytestring.ByteString){}[0]
46+
final fun decodeToSequence(kotlinx.io/Source): kotlin.sequences/Sequence<dev.whyoleg.cryptography.serialization.pem/PemDocument> // dev.whyoleg.cryptography.serialization.pem/PemDocument.Companion.decodeToSequence|decodeToSequence(kotlinx.io.Source){}[0]
47+
}
48+
}
49+
2150
final value class dev.whyoleg.cryptography.serialization.pem/PemLabel { // dev.whyoleg.cryptography.serialization.pem/PemLabel|null[0]
2251
constructor <init>(kotlin/String) // dev.whyoleg.cryptography.serialization.pem/PemLabel.<init>|<init>(kotlin.String){}[0]
2352

24-
final val representation // dev.whyoleg.cryptography.serialization.pem/PemLabel.representation|{}representation[0]
25-
final fun <get-representation>(): kotlin/String // dev.whyoleg.cryptography.serialization.pem/PemLabel.representation.<get-representation>|<get-representation>(){}[0]
53+
final val value // dev.whyoleg.cryptography.serialization.pem/PemLabel.value|{}value[0]
54+
final fun <get-value>(): kotlin/String // dev.whyoleg.cryptography.serialization.pem/PemLabel.value.<get-value>|<get-value>(){}[0]
2655

2756
final fun equals(kotlin/Any?): kotlin/Boolean // dev.whyoleg.cryptography.serialization.pem/PemLabel.equals|equals(kotlin.Any?){}[0]
2857
final fun hashCode(): kotlin/Int // dev.whyoleg.cryptography.serialization.pem/PemLabel.hashCode|hashCode(){}[0]

cryptography-serialization/pem/src/commonMain/kotlin/Pem.kt

Lines changed: 6 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,45 +5,19 @@
55
package dev.whyoleg.cryptography.serialization.pem
66

77
import kotlinx.io.bytestring.*
8-
import kotlin.io.encoding.*
98

10-
@Deprecated("Renamed to Pem", ReplaceWith("Pem"), DeprecationLevel.ERROR)
9+
@Suppress("DEPRECATION_ERROR")
10+
@Deprecated("Migrate to `PemDocument`", level = DeprecationLevel.ERROR)
1111
public typealias PEM = Pem
1212

13+
@Suppress("DEPRECATION_ERROR")
14+
@Deprecated("Migrate to `PemDocument`", level = DeprecationLevel.ERROR)
1315
public object Pem {
14-
private const val BEGIN_PREFIX = "-----BEGIN "
15-
private const val END_PREFIX = "-----END "
16-
private const val SUFFIX = "-----"
17-
1816
public fun encodeToByteString(content: PemContent): ByteString = encode(content).encodeToByteString()
1917
public fun encodeToByteArray(content: PemContent): ByteArray = encode(content).encodeToByteArray()
20-
public fun encode(content: PemContent): String = buildString {
21-
append(BEGIN_PREFIX).append(content.label.representation).appendLine(SUFFIX)
22-
Base64.encode(content.bytes).chunked(64).joinTo(this, separator = "\n", postfix = "\n")
23-
append(END_PREFIX).append(content.label.representation).appendLine(SUFFIX)
24-
}
18+
public fun encode(content: PemContent): String = PemDocument(content.label, content.byteString).encodeToString()
2519

2620
public fun decode(byteString: ByteString): PemContent = decode(byteString.decodeToString())
2721
public fun decode(bytes: ByteArray): PemContent = decode(bytes.decodeToString())
28-
public fun decode(string: String): PemContent {
29-
val lines = string.split("\n")
30-
val beginLine = lines.indexOfFirst { it.startsWith(BEGIN_PREFIX) }
31-
check(beginLine != -1) { "Invalid PEM format: missing BEGIN label" }
32-
val endLine = lines.indexOfFirst { it.startsWith(END_PREFIX) }
33-
check(endLine != -1) { "Invalid PEM format: missing END label" }
34-
35-
val beginLabel = lines[beginLine].substringAfter(BEGIN_PREFIX).substringBefore(SUFFIX).trim()
36-
check(beginLabel.isNotBlank()) { "Invalid PEM format: BEGIN label is empty" }
37-
val endLabel = lines[endLine].substringAfter(END_PREFIX).substringBefore(SUFFIX).trim()
38-
check(endLabel.isNotBlank()) { "Invalid PEM format: BEGIN label is empty" }
39-
40-
check(beginLabel == endLabel) { "Invalid PEM format: BEGIN=`$beginLabel`, END=`$endLabel`" }
41-
42-
val contentText = lines.subList(beginLine + 1, endLine).joinToString("")
43-
44-
return PemContent(
45-
label = PemLabel(beginLabel),
46-
bytes = Base64.decode(contentText)
47-
)
48-
}
22+
public fun decode(string: String): PemContent = PemDocument.decode(string).let { PemContent(it.label, it.content) }
4923
}

0 commit comments

Comments
 (0)