-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
246 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3 changes: 3 additions & 0 deletions
3
app/src/main/java/tech/relaycorp/letro/utils/asn1/ASN1Exception.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package tech.relaycorp.letro.utils.asn1 | ||
|
||
class ASN1Exception(message: String, cause: Throwable? = null) : Exception(message, cause) |
73 changes: 73 additions & 0 deletions
73
app/src/main/java/tech/relaycorp/letro/utils/asn1/ASN1Utils.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
package tech.relaycorp.letro.utils.asn1 | ||
|
||
import java.io.IOException | ||
import org.bouncycastle.asn1.ASN1Encodable | ||
import org.bouncycastle.asn1.ASN1EncodableVector | ||
import org.bouncycastle.asn1.ASN1InputStream | ||
import org.bouncycastle.asn1.ASN1OctetString | ||
import org.bouncycastle.asn1.ASN1Sequence | ||
import org.bouncycastle.asn1.ASN1TaggedObject | ||
import org.bouncycastle.asn1.ASN1VisibleString | ||
import org.bouncycastle.asn1.DEROctetString | ||
import org.bouncycastle.asn1.DERSequence | ||
import org.bouncycastle.asn1.DERTaggedObject | ||
|
||
internal object ASN1Utils { | ||
fun makeSequence(items: List<ASN1Encodable>, explicitTagging: Boolean = true): DERSequence { | ||
val messagesVector = ASN1EncodableVector(items.size) | ||
val finalItems = if (explicitTagging) items else items.mapIndexed { index, item -> | ||
DERTaggedObject(false, index, item) | ||
} | ||
finalItems.forEach { messagesVector.add(it) } | ||
return DERSequence(messagesVector) | ||
} | ||
|
||
fun serializeSequence(items: List<ASN1Encodable>, explicitTagging: Boolean = true): ByteArray { | ||
return makeSequence(items, explicitTagging).encoded | ||
} | ||
|
||
@Throws(ASN1Exception::class) | ||
fun deserializeSequence(serialization: ByteArray): ASN1Sequence { | ||
if (serialization.isEmpty()) { | ||
throw ASN1Exception("Value is empty") | ||
} | ||
val asn1InputStream = ASN1InputStream(serialization) | ||
val asn1Value = try { | ||
asn1InputStream.readObject() | ||
} catch (_: IOException) { | ||
throw ASN1Exception("Value is not DER-encoded") | ||
} | ||
return try { | ||
ASN1Sequence.getInstance(asn1Value) | ||
} catch (_: IllegalArgumentException) { | ||
throw ASN1Exception("Value is not an ASN.1 sequence") | ||
} | ||
} | ||
|
||
@Throws(ASN1Exception::class) | ||
inline fun <reified T : ASN1Encodable> deserializeHomogeneousSequence( | ||
serialization: ByteArray | ||
): Array<T> { | ||
val sequence = deserializeSequence(serialization) | ||
return sequence.map { | ||
if (it !is T) { | ||
throw ASN1Exception( | ||
"Sequence contains an item of an unexpected type " + | ||
"(${it::class.java.simpleName})" | ||
) | ||
} | ||
@Suppress("USELESS_CAST") | ||
it as T | ||
}.toTypedArray() | ||
} | ||
|
||
@Throws(ASN1Exception::class) | ||
fun deserializeHeterogeneousSequence(serialization: ByteArray): Array<ASN1TaggedObject> = | ||
deserializeHomogeneousSequence(serialization) | ||
|
||
fun getVisibleString(visibleString: ASN1TaggedObject): ASN1VisibleString = | ||
ASN1VisibleString.getInstance(visibleString, false) | ||
|
||
fun getOctetString(octetString: ASN1TaggedObject): ASN1OctetString = | ||
DEROctetString.getInstance(octetString, false) | ||
} |
This file was deleted.
Oops, something went wrong.
158 changes: 158 additions & 0 deletions
158
app/src/test/java/tech/relaycorp/letro/utils/asn1/ASN1UtilsTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
package tech.relaycorp.letro.utils.asn1 | ||
|
||
import io.kotest.matchers.should | ||
import io.kotest.matchers.shouldBe | ||
import io.kotest.matchers.types.beInstanceOf | ||
import org.bouncycastle.asn1.ASN1Sequence | ||
import org.bouncycastle.asn1.ASN1StreamParser | ||
import org.bouncycastle.asn1.ASN1TaggedObject | ||
import org.bouncycastle.asn1.DEROctetString | ||
import org.bouncycastle.asn1.DEROctetStringParser | ||
import org.bouncycastle.asn1.DERVisibleString | ||
import org.bouncycastle.asn1.DLSequenceParser | ||
import org.junit.jupiter.api.Nested | ||
import org.junit.jupiter.api.Test | ||
import org.junit.jupiter.api.assertThrows | ||
|
||
internal class ASN1UtilsTest { | ||
val visibleString = DERVisibleString("foo") | ||
val octetString = DEROctetString("bar".toByteArray()) | ||
|
||
@Nested | ||
inner class MakeSequence { | ||
@Test | ||
fun `Values should be explicitly tagged by default`() { | ||
val sequence = ASN1Utils.makeSequence(listOf(visibleString, octetString)) | ||
|
||
sequence.size() shouldBe 2 | ||
|
||
val item1 = sequence.getObjectAt(0) | ||
item1 should beInstanceOf<DERVisibleString>() | ||
visibleString.string shouldBe (item1 as DERVisibleString).string | ||
|
||
val item2 = sequence.getObjectAt(1) | ||
item2 should beInstanceOf<DEROctetString>() | ||
octetString.octets shouldBe (item2 as DEROctetString).octets | ||
} | ||
|
||
@Test | ||
fun `Implicitly-tagged values should be supported`() { | ||
val sequence = ASN1Utils.makeSequence(listOf(visibleString, octetString), false) | ||
|
||
sequence.size() shouldBe 2 | ||
|
||
val item1 = ASN1Utils.getVisibleString(sequence.getObjectAt(0) as ASN1TaggedObject) | ||
visibleString.string shouldBe item1.string | ||
|
||
val item2 = ASN1Utils.getOctetString(sequence.getObjectAt(1) as ASN1TaggedObject) | ||
octetString.octets shouldBe item2.octets | ||
} | ||
} | ||
|
||
@Nested | ||
inner class SerializeSequence { | ||
@Test | ||
fun `Values should be explicitly tagged by default`() { | ||
val serialization = ASN1Utils.serializeSequence(listOf(visibleString, octetString)) | ||
|
||
val parser = ASN1StreamParser(serialization) | ||
val sequence = parser.readObject() as DLSequenceParser | ||
|
||
val item1 = sequence.readObject() | ||
item1 should beInstanceOf<DERVisibleString>() | ||
visibleString.string shouldBe (item1 as DERVisibleString).string | ||
|
||
val item2 = sequence.readObject() | ||
item2 should beInstanceOf<DEROctetStringParser>() | ||
octetString.octets shouldBe | ||
((item2 as DEROctetStringParser).loadedObject as DEROctetString).octets | ||
} | ||
|
||
@Test | ||
fun `Implicitly-tagged values should be supported`() { | ||
val serialization = | ||
ASN1Utils.serializeSequence(listOf(visibleString, octetString), false) | ||
|
||
val parser = ASN1StreamParser(serialization) | ||
val sequence = | ||
ASN1Sequence.getInstance(parser.readObject() as DLSequenceParser).toArray() | ||
|
||
val item1 = ASN1Utils.getVisibleString(sequence[0] as ASN1TaggedObject) | ||
visibleString.string shouldBe item1.string | ||
|
||
val item2 = ASN1Utils.getOctetString(sequence[1] as ASN1TaggedObject) | ||
octetString.octets shouldBe item2.octets | ||
} | ||
} | ||
|
||
@Nested | ||
inner class DeserializeSequence { | ||
@Test | ||
fun `Value should be refused if it's empty`() { | ||
val exception = assertThrows<ASN1Exception> { | ||
ASN1Utils.deserializeHeterogeneousSequence(byteArrayOf()) | ||
} | ||
|
||
"Value is empty" shouldBe exception.message | ||
} | ||
|
||
@Test | ||
fun `Value should be refused if it's not DER-encoded`() { | ||
val exception = assertThrows<ASN1Exception> { | ||
ASN1Utils.deserializeHeterogeneousSequence("a".toByteArray()) | ||
} | ||
|
||
"Value is not DER-encoded" shouldBe exception.message | ||
} | ||
|
||
@Test | ||
fun `Value should be refused if it's not a sequence`() { | ||
val serialization = DERVisibleString("hey").encoded | ||
|
||
val exception = assertThrows<ASN1Exception> { | ||
ASN1Utils.deserializeHeterogeneousSequence(serialization) | ||
} | ||
|
||
"Value is not an ASN.1 sequence" shouldBe exception.message | ||
} | ||
|
||
@Test | ||
fun `Explicitly tagged items should be deserialized with their corresponding types`() { | ||
val serialization = ASN1Utils.serializeSequence(listOf(visibleString, visibleString)) | ||
|
||
val sequence = ASN1Utils.deserializeHomogeneousSequence<DERVisibleString>(serialization) | ||
|
||
2 shouldBe sequence.size | ||
val value1Deserialized = sequence.first() | ||
visibleString shouldBe value1Deserialized | ||
val value2Deserialized = sequence.last() | ||
visibleString shouldBe value2Deserialized | ||
} | ||
|
||
@Test | ||
fun `Explicitly tagged items with unexpected types should be refused`() { | ||
val serialization = ASN1Utils.serializeSequence(listOf(visibleString, octetString)) | ||
|
||
val exception = assertThrows<ASN1Exception> { | ||
ASN1Utils.deserializeHomogeneousSequence<DERVisibleString>(serialization) | ||
} | ||
|
||
exception.message shouldBe | ||
"Sequence contains an item of an unexpected type " + | ||
"(${octetString::class.java.simpleName})" | ||
} | ||
|
||
@Test | ||
fun `Implicitly tagged items should be deserialized with their corresponding types`() { | ||
val serialization = | ||
ASN1Utils.serializeSequence(listOf(visibleString, octetString), false) | ||
|
||
val sequence = ASN1Utils.deserializeHeterogeneousSequence(serialization) | ||
|
||
2 shouldBe sequence.size | ||
visibleString.octets shouldBe | ||
ASN1Utils.getVisibleString(sequence.first()).octets | ||
octetString.octets shouldBe ASN1Utils.getOctetString(sequence[1]).octets | ||
} | ||
} | ||
} |