Skip to content

Commit 1b30b45

Browse files
committed
[PEM] Document PemDocument and PemLabel
1 parent 1df4720 commit 1b30b45

File tree

2 files changed

+308
-3
lines changed

2 files changed

+308
-3
lines changed

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

Lines changed: 270 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,111 @@ import kotlinx.io.bytestring.*
99
import kotlinx.io.bytestring.unsafe.*
1010
import kotlin.io.encoding.*
1111

12+
/**
13+
* Represents a single PEM (Privacy-Enhanced Mail) document as defined by [RFC 7468](https://datatracker.ietf.org/doc/html/rfc7468),
14+
* consisting of a textual [label], and the binary [content] it encapsulates
15+
*
16+
* To encode the document into a PEM-encoded string use one of
17+
* [encodeToString], [encodeToByteArray], [encodeToByteString], or [encodeToSink] depending on the desired output type
18+
*
19+
* Encoding produces the canonical PEM form:
20+
*
21+
* ```text
22+
* -----BEGIN {label}-----
23+
* Base64-encoded {content} with line breaks every 64 characters
24+
* -----END {label}-----
25+
* ```
26+
*
27+
* Creation of document instances can be done using one of the following methods:
28+
*
29+
* - using constructor, accepting [label] and [content]
30+
* - using [PemDocument.decode] for decoding from a string or binary input
31+
* - using [PemDocument.decodeToSequence] for decoding multiple documents from a single string or binary input
32+
*
33+
* [PemDocument] is an **immutable**, **thread-safe** value object, with structural equality and hash code that uses both [label] and [content].
34+
*
35+
* @constructor Creates a new [PemDocument] with the provided [label] and [content]
36+
* @property label Case-sensitive encapsulation label (for example, `"CERTIFICATE"` or `"PRIVATE KEY"`)
37+
* @property content Raw binary payload (commonly DER or any arbitrary bytes) that is base64-armored when encoded to PEM
38+
*/
1239
public class PemDocument(
1340
public val label: PemLabel,
1441
public val content: ByteString,
1542
) {
43+
/**
44+
* Creates a new [PemDocument] with the provided [label] and [content]
45+
*/
1646
public constructor(
1747
label: PemLabel,
1848
content: ByteArray,
1949
) : this(label, ByteString(content))
2050

51+
/**
52+
* Encodes this document into a [String] in [PEM](https://datatracker.ietf.org/doc/html/rfc7468) format
53+
*
54+
* The output uses the form:
55+
*
56+
* ```text
57+
* -----BEGIN {label}-----
58+
* Base64-encoded {content} with line breaks every 64 characters
59+
* -----END {label}-----
60+
* ```
61+
*
62+
* @return the PEM-encoded string
63+
*/
2164
public fun encodeToString(): String = encodeToByteArrayImpl().decodeToString()
2265

66+
/**
67+
* Encodes this document into a [ByteArray] as a string in [PEM](https://datatracker.ietf.org/doc/html/rfc7468) format
68+
*
69+
* The output uses the form:
70+
*
71+
* ```text
72+
* -----BEGIN {label}-----
73+
* Base64-encoded {content} with line breaks every 64 characters
74+
* -----END {label}-----
75+
* ```
76+
*
77+
* @return the bytes representing PEM-encoded string
78+
*/
2379
public fun encodeToByteArray(): ByteArray = encodeToByteArrayImpl()
2480

81+
/**
82+
* Encodes this document into a [ByteString] as a string in [PEM](https://datatracker.ietf.org/doc/html/rfc7468) format
83+
*
84+
* The output uses the form:
85+
*
86+
* ```text
87+
* -----BEGIN {label}-----
88+
* Base64-encoded {content} with line breaks every 64 characters
89+
* -----END {label}-----
90+
* ```
91+
*
92+
* @return the bytes representing PEM-encoded string
93+
*/
2594
@OptIn(UnsafeByteStringApi::class)
2695
public fun encodeToByteString(): ByteString = UnsafeByteStringOperations.wrapUnsafe(encodeToByteArrayImpl())
2796

97+
/**
98+
* Encodes this document to the provided [sink] as a string in [PEM](https://datatracker.ietf.org/doc/html/rfc7468) format
99+
*
100+
* The output uses the form:
101+
*
102+
* ```text
103+
* -----BEGIN {label}-----
104+
* Base64-encoded {content} with line breaks every 64 characters
105+
* -----END {label}-----
106+
* ```
107+
*
108+
* @param sink the destination to write bytes representing PEM-encoded string into
109+
*/
28110
public fun encodeToSink(sink: Sink): Unit = sink.write(encodeToByteArrayImpl())
29111

112+
/**
113+
* Returns `true` if [other] is a [PemDocument] with the same [label] and [content]
114+
*
115+
* [content] should contain exactly the same byte sequence
116+
*/
30117
override fun equals(other: Any?): Boolean {
31118
if (this === other) return true
32119
if (other !is PemDocument) return false
@@ -37,56 +124,236 @@ public class PemDocument(
37124
return true
38125
}
39126

127+
/**
128+
* Returns a hash code consistent with [equals], computed from [label] and [content]
129+
*/
40130
override fun hashCode(): Int {
41131
var result = label.hashCode()
42132
result = 31 * result + content.hashCode()
43133
return result
44134
}
45135

136+
/**
137+
* Returns a concise debug representation of this document including its [label] and [content]
138+
*
139+
* **Avoid logging if the [content] is sensitive**
140+
*/
46141
override fun toString(): String {
47142
return "PemDocument(label=$label, content=$content)"
48143
}
49144

50145
public companion object {
51-
// decode will skip comments and everything else which is not label or content
52-
53-
// will decode only the first one, even if there is something else after it
146+
/**
147+
* Decodes the first [PEM](https://datatracker.ietf.org/doc/html/rfc7468) document found in [text]
148+
*
149+
* The input should be in the form:
150+
*
151+
* ```text
152+
* -----BEGIN {label}-----
153+
* Base64-encoded {content} with line breaks every 64 characters
154+
* -----END {label}-----
155+
* ```
156+
*
157+
* Any content before `-----BEGIN {label}-----` and after `-----END {label}-----` is ignored
158+
*
159+
* Only the first complete document is decoded. For decoding of multiple documents, use [PemDocument.decodeToSequence]
160+
*
161+
* @param text the textual input that may contain a PEM document
162+
* @return the decoded [PemDocument]
163+
* @throws IllegalArgumentException if no PEM documents present in [text], or the PEM encoding is invalid
164+
*/
54165
public fun decode(text: String): PemDocument {
55166
return tryDecodeFromString(text, startIndex = 0, saveEndIndex = {}) ?: throwPemMissingBeginLabel()
56167
}
57168

169+
/**
170+
* Lazily decodes all [PEM](https://datatracker.ietf.org/doc/html/rfc7468) documents found in [text]
171+
*
172+
* The input should be in the form:
173+
*
174+
* ```text
175+
* -----BEGIN {LABEL1}-----
176+
* Base64-encoded {content} with line breaks every 64 characters
177+
* -----END {LABEL1}-----
178+
*
179+
* -----BEGIN {LABEL2}-----
180+
* Base64-encoded {content} with line breaks every 64 characters
181+
* -----END {LABEL2}-----
182+
* ```
183+
*
184+
* Any content before the first `-----BEGIN {label}-----` and after the last `-----END {label}-----` is ignored,
185+
* as well as any content after each document and before the next `-----BEGIN {label}-----`.
186+
* The sequence yields each discovered [PemDocument] in order
187+
*
188+
* @param text the textual input that may contain multiple PEM documents
189+
* @return a sequence of decoded [PemDocument]s, empty sequence if no PEM documents present
190+
* @throws IllegalArgumentException if the PEM encoding of any document is invalid
191+
*/
58192
public fun decodeToSequence(text: String): Sequence<PemDocument> = sequence {
59193
var startIndex = 0
60194
while (startIndex < text.length) {
61195
yield(tryDecodeFromString(text, startIndex) { startIndex = it } ?: break)
62196
}
63197
}
64198

199+
/**
200+
* Decodes the first [PEM](https://datatracker.ietf.org/doc/html/rfc7468) document found in [bytes].
201+
* The [bytes] are treated as an encoded string
202+
*
203+
* The input should be in the form:
204+
*
205+
* ```text
206+
* -----BEGIN {label}-----
207+
* Base64-encoded {content} with line breaks every 64 characters
208+
* -----END {label}-----
209+
* ```
210+
*
211+
* Any content before `-----BEGIN {label}-----` and after `-----END {label}-----` is ignored
212+
*
213+
* Only the first complete document is decoded. For decoding of multiple documents, use [PemDocument.decodeToSequence]
214+
*
215+
* @param bytes the byte array that may contain a PEM document
216+
* @return the decoded [PemDocument]
217+
* @throws IllegalArgumentException if no PEM documents present in [bytes], or the PEM encoding is invalid
218+
*/
65219
@OptIn(UnsafeByteStringApi::class)
66220
public fun decode(bytes: ByteArray): PemDocument {
67221
return decode(UnsafeByteStringOperations.wrapUnsafe(bytes))
68222
}
69223

224+
/**
225+
* Lazily decodes all [PEM](https://datatracker.ietf.org/doc/html/rfc7468) documents found in [bytes].
226+
* The [bytes] are treated as an encoded string
227+
*
228+
* The input should be in the form:
229+
*
230+
* ```text
231+
* -----BEGIN {LABEL1}-----
232+
* Base64-encoded {content} with line breaks every 64 characters
233+
* -----END {LABEL1}-----
234+
*
235+
* -----BEGIN {LABEL2}-----
236+
* Base64-encoded {content} with line breaks every 64 characters
237+
* -----END {LABEL2}-----
238+
* ```
239+
*
240+
* Any content before the first `-----BEGIN {label}-----` and after the last `-----END {label}-----` is ignored,
241+
* as well as any content after each document and before the next `-----BEGIN {label}-----`.
242+
* The sequence yields each discovered [PemDocument] in order
243+
*
244+
* @param bytes the byte array that may contain multiple PEM documents
245+
* @return a sequence of decoded [PemDocument]s, empty sequence if no PEM documents present
246+
* @throws IllegalArgumentException if the PEM encoding of any document is invalid
247+
*/
70248
@OptIn(UnsafeByteStringApi::class)
71249
public fun decodeToSequence(bytes: ByteArray): Sequence<PemDocument> {
72250
return decodeToSequence(UnsafeByteStringOperations.wrapUnsafe(bytes))
73251
}
74252

253+
/**
254+
* Decodes the first [PEM](https://datatracker.ietf.org/doc/html/rfc7468) document found in [bytes].
255+
* The [bytes] are treated as an encoded string
256+
*
257+
* The input should be in the form:
258+
*
259+
* ```text
260+
* -----BEGIN {label}-----
261+
* Base64-encoded {content} with line breaks every 64 characters
262+
* -----END {label}-----
263+
* ```
264+
*
265+
* Any content before `-----BEGIN {label}-----` and after `-----END {label}-----` is ignored
266+
*
267+
* Only the first complete document is decoded. For decoding of multiple documents, use [PemDocument.decodeToSequence]
268+
*
269+
* @param bytes the byte array that may contain a PEM document
270+
* @return the decoded [PemDocument]
271+
* @throws IllegalArgumentException if no PEM documents present in [bytes], or the PEM encoding is invalid
272+
*/
75273
public fun decode(bytes: ByteString): PemDocument {
76274
return tryDecodeFromByteString(bytes, startIndex = 0, saveEndIndex = {}) ?: throwPemMissingBeginLabel()
77275
}
78276

277+
/**
278+
* Lazily decodes all [PEM](https://datatracker.ietf.org/doc/html/rfc7468) documents found in [bytes].
279+
* The [bytes] are treated as an encoded string
280+
*
281+
* The input should be in the form:
282+
*
283+
* ```text
284+
* -----BEGIN {LABEL1}-----
285+
* Base64-encoded {content} with line breaks every 64 characters
286+
* -----END {LABEL1}-----
287+
*
288+
* -----BEGIN {LABEL2}-----
289+
* Base64-encoded {content} with line breaks every 64 characters
290+
* -----END {LABEL2}-----
291+
* ```
292+
*
293+
* Any content before the first `-----BEGIN {label}-----` and after the last `-----END {label}-----` is ignored,
294+
* as well as any content after each document and before the next `-----BEGIN {label}-----`.
295+
* The sequence yields each discovered [PemDocument] in order
296+
*
297+
* @param bytes the byte array that may contain multiple PEM documents
298+
* @return a sequence of decoded [PemDocument]s, empty sequence if no PEM documents present
299+
* @throws IllegalArgumentException if the PEM encoding of any document is invalid
300+
*/
79301
public fun decodeToSequence(bytes: ByteString): Sequence<PemDocument> = sequence {
80302
var startIndex = 0
81303
while (startIndex < bytes.size) {
82304
yield(tryDecodeFromByteString(bytes, startIndex) { startIndex = it } ?: break)
83305
}
84306
}
85307

308+
/**
309+
* Decodes the first [PEM](https://datatracker.ietf.org/doc/html/rfc7468) document found in [source].
310+
* The [source] is treated as an encoded string and is consumed up to and including the decoded document
311+
*
312+
*
313+
* The input should be in the form:
314+
*
315+
* ```text
316+
* -----BEGIN {label}-----
317+
* Base64-encoded {content} with line breaks every 64 characters
318+
* -----END {label}-----
319+
* ```
320+
*
321+
* Any content before `-----BEGIN {label}-----` and after `-----END {label}-----` is ignored
322+
*
323+
* Only the first complete document is decoded. For decoding of multiple documents, use [PemDocument.decodeToSequence]
324+
*
325+
* @param source the source to read from
326+
* @return the decoded [PemDocument]
327+
* @throws IllegalArgumentException if no PEM documents present in [source], or the PEM encoding is invalid
328+
*/
86329
public fun decode(source: Source): PemDocument {
87330
return tryDecodeFromSource(source) ?: throwPemMissingBeginLabel()
88331
}
89332

333+
/**
334+
* Lazily decodes all [PEM](https://datatracker.ietf.org/doc/html/rfc7468) documents found in [source].
335+
* The [source] is treated as an encoded string and is consumed up to and including the last decoded document, which was consumed from the sequence
336+
*
337+
* The input should be in the form:
338+
*
339+
* ```text
340+
* -----BEGIN {LABEL1}-----
341+
* Base64-encoded {content} with line breaks every 64 characters
342+
* -----END {LABEL1}-----
343+
*
344+
* -----BEGIN {LABEL2}-----
345+
* Base64-encoded {content} with line breaks every 64 characters
346+
* -----END {LABEL2}-----
347+
* ```
348+
*
349+
* Any content before the first `-----BEGIN {label}-----` and after the last `-----END {label}-----` is ignored,
350+
* as well as any content after each document and before the next `-----BEGIN {label}-----`.
351+
* The sequence yields each discovered [PemDocument] in order
352+
*
353+
* @param source the source to read from
354+
* @return a sequence of decoded [PemDocument]s, empty sequence if no PEM documents present
355+
* @throws IllegalArgumentException if the PEM encoding of any document is invalid
356+
*/
90357
public fun decodeToSequence(source: Source): Sequence<PemDocument> = sequence {
91358
while (!source.exhausted()) {
92359
yield(tryDecodeFromSource(source) ?: break)

0 commit comments

Comments
 (0)