Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions koap-core/api/koap-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -494,16 +494,22 @@ public final class com/juul/koap/Message$Option$Observe$Registration$Register :
}

public final class com/juul/koap/Message$Option$Oscore : com/juul/koap/Message$Option {
public static final field Companion Lcom/juul/koap/Message$Option$Oscore$Companion;
public fun <init> ([B)V
public final fun component1 ()[B
public final fun copy ([B)Lcom/juul/koap/Message$Option$Oscore;
public static synthetic fun copy$default (Lcom/juul/koap/Message$Option$Oscore;[BILjava/lang/Object;)Lcom/juul/koap/Message$Option$Oscore;
public fun equals (Ljava/lang/Object;)Z
public final fun getValue ()[B
public fun hashCode ()I
public final fun parts ()Lcom/juul/koap/OscoreParts;
public fun toString ()Ljava/lang/String;
}

public final class com/juul/koap/Message$Option$Oscore$Companion {
public final fun fromParts ([B[B[B)Lcom/juul/koap/Message$Option$Oscore;
}

public final class com/juul/koap/Message$Option$ProxyScheme : com/juul/koap/Message$Option {
public fun <init> (Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
Expand Down Expand Up @@ -720,3 +726,18 @@ public final class com/juul/koap/MessageKt {
public static final fun isSuccess (Lcom/juul/koap/Message$Code$Response;)Z
}

public final class com/juul/koap/OscoreParts {
public fun <init> ([B[B[B)V
public final fun component1 ()[B
public final fun component2 ()[B
public final fun component3 ()[B
public final fun copy ([B[B[B)Lcom/juul/koap/OscoreParts;
public static synthetic fun copy$default (Lcom/juul/koap/OscoreParts;[B[B[BILjava/lang/Object;)Lcom/juul/koap/OscoreParts;
public fun equals (Ljava/lang/Object;)Z
public final fun getKid ()[B
public final fun getKidContext ()[B
public final fun getPartialIv ()[B
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

24 changes: 24 additions & 0 deletions koap-core/api/koap-core.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@
// - Show declarations: true

// Library unique name: <com.juul.koap:koap-core>
final class com.juul.koap/OscoreParts { // com.juul.koap/OscoreParts|null[0]
constructor <init>(kotlin/ByteArray, kotlin/ByteArray?, kotlin/ByteArray?) // com.juul.koap/OscoreParts.<init>|<init>(kotlin.ByteArray;kotlin.ByteArray?;kotlin.ByteArray?){}[0]

final val kid // com.juul.koap/OscoreParts.kid|{}kid[0]
final fun <get-kid>(): kotlin/ByteArray? // com.juul.koap/OscoreParts.kid.<get-kid>|<get-kid>(){}[0]
final val kidContext // com.juul.koap/OscoreParts.kidContext|{}kidContext[0]
final fun <get-kidContext>(): kotlin/ByteArray? // com.juul.koap/OscoreParts.kidContext.<get-kidContext>|<get-kidContext>(){}[0]
final val partialIv // com.juul.koap/OscoreParts.partialIv|{}partialIv[0]
final fun <get-partialIv>(): kotlin/ByteArray // com.juul.koap/OscoreParts.partialIv.<get-partialIv>|<get-partialIv>(){}[0]

final fun component1(): kotlin/ByteArray // com.juul.koap/OscoreParts.component1|component1(){}[0]
final fun component2(): kotlin/ByteArray? // com.juul.koap/OscoreParts.component2|component2(){}[0]
final fun component3(): kotlin/ByteArray? // com.juul.koap/OscoreParts.component3|component3(){}[0]
final fun copy(kotlin/ByteArray = ..., kotlin/ByteArray? = ..., kotlin/ByteArray? = ...): com.juul.koap/OscoreParts // com.juul.koap/OscoreParts.copy|copy(kotlin.ByteArray;kotlin.ByteArray?;kotlin.ByteArray?){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // com.juul.koap/OscoreParts.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // com.juul.koap/OscoreParts.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // com.juul.koap/OscoreParts.toString|toString(){}[0]
}

sealed class com.juul.koap/Header { // com.juul.koap/Header|null[0]
abstract val code // com.juul.koap/Header.code|{}code[0]
abstract fun <get-code>(): com.juul.koap/Message.Code // com.juul.koap/Header.code.<get-code>|<get-code>(){}[0]
Expand Down Expand Up @@ -509,7 +528,12 @@ sealed class com.juul.koap/Message { // com.juul.koap/Message|null[0]
final fun copy(kotlin/ByteArray = ...): com.juul.koap/Message.Option.Oscore // com.juul.koap/Message.Option.Oscore.copy|copy(kotlin.ByteArray){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // com.juul.koap/Message.Option.Oscore.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // com.juul.koap/Message.Option.Oscore.hashCode|hashCode(){}[0]
final fun parts(): com.juul.koap/OscoreParts // com.juul.koap/Message.Option.Oscore.parts|parts(){}[0]
final fun toString(): kotlin/String // com.juul.koap/Message.Option.Oscore.toString|toString(){}[0]

final object Companion { // com.juul.koap/Message.Option.Oscore.Companion|null[0]
final fun fromParts(kotlin/ByteArray, kotlin/ByteArray?, kotlin/ByteArray?): com.juul.koap/Message.Option.Oscore // com.juul.koap/Message.Option.Oscore.Companion.fromParts|fromParts(kotlin.ByteArray;kotlin.ByteArray?;kotlin.ByteArray?){}[0]
}
}

final class ProxyScheme : com.juul.koap/Message.Option { // com.juul.koap/Message.Option.ProxyScheme|null[0]
Expand Down
12 changes: 11 additions & 1 deletion koap-core/src/commonMain/kotlin/Message.kt
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,17 @@ sealed class Message {

override fun hashCode(): Int = value.contentHashCode()

override fun toString(): String = "Oscore(value=${value.toHexString()})"
override fun toString(): String {
val parts = oscorePartsFromOptionValue(value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oscorePartsFromOptionValue can throw on malformed input data. It may be surprising to a system that catches a parsing exception and then uses this toString method to log the failure - they won't be expecting the log invoke to throw which may cause a crash. I'd recommend ensuring that toString cannot throw as it is typically considered a safe operation.

return parts.toString()
}

fun parts(): OscoreParts = oscorePartsFromOptionValue(value)

companion object {
fun fromParts(partialIv: ByteArray, kidContext: ByteArray?, kid: ByteArray?): Oscore =
Oscore(oscoreOptionValue(OscoreParts(partialIv, kidContext, kid)))
}
}

/** RFC 9668 3.1. EDHOC */
Expand Down
92 changes: 92 additions & 0 deletions koap-core/src/commonMain/kotlin/Oscore.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.juul.koap

import com.juul.koap.Message.Option.Oscore

private val PARTIAL_IV_LENGTH_RANGE = 0..5
private const val PARTIAL_IV_LENGTH_MASK = 0x07
private const val KID_FLAG = 0x08
private const val KID_CONTEXT_FLAG = 0x10

data class OscoreParts(
val partialIv: ByteArray,
val kidContext: ByteArray?,
val kid: ByteArray?,
) {
override fun equals(other: Any?): Boolean =
this === other ||
(
other is OscoreParts &&
partialIv.contentEquals(other.partialIv) &&
kidContext.contentEquals(other.kidContext) &&
kid.contentEquals(other.kid)
)

override fun hashCode(): Int {
var result = partialIv.contentHashCode()
result = 31 * result + kidContext.contentHashCode()
result = 31 * result + kid.contentHashCode()
return result
}

override fun toString(): String {
val args = listOfNotNull(
if (partialIv.isNotEmpty()) "partialIv=${partialIv.toHexString()}" else null,
if (kidContext != null) "kidContext=${kidContext.toHexString()}" else null,
if (kid != null) "kid=${kid.toHexString()}" else null,
).joinToString(separator = ", ")
return "Oscore($args)"
}
}

/**
* Creates a binary OSCORE option value from an OscoreParts object.
*
* - [RFC 8613](https://tools.ietf.org/html/rfc8613#section-2) 2. The OSCORE Option
* - [RFC 8613](https://tools.ietf.org/html/rfc8613#section-6.1) 6.1. Encoding of the OSCORE Option Value
*/
internal fun oscoreOptionValue(oscore: OscoreParts): ByteArray {
val partialIvLength = oscore.partialIv.size
require(partialIvLength in PARTIAL_IV_LENGTH_RANGE) {
"Partial IV length of $partialIvLength is outside allowable range of $PARTIAL_IV_LENGTH_RANGE"
}
val kidContextLength = if (oscore.kidContext != null) oscore.kidContext.size else 0
val kidFlagBit = if (oscore.kid != null) KID_FLAG else 0
val kidContextFlagBit = if (oscore.kidContext != null) KID_CONTEXT_FLAG else 0
val flagBits = partialIvLength or kidFlagBit or kidContextFlagBit
if (flagBits == 0) {
// If the OSCORE flag bits are all zero (0x00), the option value SHALL be empty
return byteArrayOf()
}
var value = byteArrayOf(flagBits.toByte()) + oscore.partialIv
if (oscore.kidContext != null) {
value += byteArrayOf(kidContextLength.toByte()) + oscore.kidContext

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth a require length guard on kidContextLength here (similar to above) to ensure that it does not exceed the implicit 255 byte truncation and avoid returning a potentially corrupted result. Would be worth adding a test case for that.

}
if (oscore.kid != null) {
value += oscore.kid
}
return value
}

/**
* Parses an OscoreParts object from a binary OSCORE option value.
*/
internal fun oscorePartsFromOptionValue(value: ByteArray): OscoreParts {
// TODO: detect if value does not conform to expected oscore format
if (value.isEmpty()) {
return OscoreParts(byteArrayOf(), null, null)
}
val flagBits = value[0].toInt()
val partialIvLength = flagBits and PARTIAL_IV_LENGTH_MASK
val kidFlag = (flagBits and KID_FLAG) != 0
val kidContextFlag = (flagBits and KID_CONTEXT_FLAG) != 0
val partialIv = value.sliceArray(1 until 1 + partialIvLength)
val kidContextLength = if (kidContextFlag) value[1 + partialIvLength].toInt() else 0
val kidContext = if (kidContextFlag) {
value.sliceArray(2 + partialIvLength until 2 + partialIvLength + kidContextLength)
} else {
null
}
val kidOffset = (if (kidContextFlag) 2 else 1) + partialIvLength + kidContextLength
val kid = if (kidFlag) value.sliceArray(kidOffset until value.size) else null
return OscoreParts(partialIv, kidContext, kid)
}
38 changes: 32 additions & 6 deletions koap-core/src/commonTest/kotlin/DecoderTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ class DecoderTest {
encoded = """
90 # Option Delta: 9, Option Length: 0
""",
expected = Oscore(byteArrayOf()),
expected = Oscore.fromParts(byteArrayOf(), null, null),
)
}

Expand All @@ -292,12 +292,16 @@ class DecoderTest {
token = unprotectedCoapRequest.token,
options = listOf(
UriHost("localhost"),
Oscore(oscoreOptionValue),
Oscore.fromParts(partialIv = byteArrayOf(0x14), kidContext = null, kid = byteArrayOf()),
),
payload = ciphertext,
),
actual = protectedCoapRequest.decode<Message.Udp>(),
)
assertEquals(
actual = Oscore(oscoreOptionValue),
expected = Oscore.fromParts(partialIv = byteArrayOf(0x14), kidContext = null, kid = byteArrayOf()),
)
}

// Test Vector 5: OSCORE Request, Client
Expand All @@ -322,12 +326,16 @@ class DecoderTest {
token = unprotectedCoapRequest.token,
options = listOf(
UriHost("localhost"),
Oscore(oscoreOptionValue),
Oscore.fromParts(partialIv = byteArrayOf(0x14), kidContext = null, kid = byteArrayOf(0x00)),
),
payload = ciphertext,
),
actual = protectedCoapRequest.decode<Message.Udp>(),
)
assertEquals(
actual = Oscore(oscoreOptionValue),
expected = Oscore.fromParts(partialIv = byteArrayOf(0x14), kidContext = null, kid = byteArrayOf(0x00)),
)
}

// Test Vector 6: OSCORE Request, Client
Expand All @@ -353,12 +361,22 @@ class DecoderTest {
token = unprotectedCoapRequest.token,
options = listOf(
UriHost("localhost"),
Oscore(oscoreOptionValue),
Oscore.fromParts(
partialIv = byteArrayOf(0x14),
kidContext = "37CBF3210017A2D3".decodeHex().toByteArray(),
kid = byteArrayOf(),
),
),
payload = ciphertext,
),
actual = protectedCoapRequest.decode<Message.Udp>(),
)
assertEquals(
actual = Oscore(oscoreOptionValue),
expected = Oscore.fromParts(
partialIv = byteArrayOf(0x14), kidContext = "37CBF3210017A2D3".decodeHex().toByteArray(), kid = byteArrayOf(),
),
)
}

// Test Vector 7: OSCORE Response, Server
Expand All @@ -382,12 +400,16 @@ class DecoderTest {
id = unprotectedCoapResponse.id,
token = unprotectedCoapResponse.token,
options = listOf(
Oscore(oscoreOptionValue),
Oscore.fromParts(partialIv = byteArrayOf(), kidContext = null, kid = null),
),
payload = ciphertext,
),
actual = protectedCoapResponse.decode<Message.Udp>(),
)
assertEquals(
actual = Oscore(oscoreOptionValue),
expected = Oscore.fromParts(partialIv = byteArrayOf(), kidContext = null, kid = null),
)
}

// Test Vector 8: OSCORE Response with Partial IV, Server
Expand All @@ -411,12 +433,16 @@ class DecoderTest {
id = unprotectedCoapResponse.id,
token = unprotectedCoapResponse.token,
options = listOf(
Oscore(oscoreOptionValue),
Oscore.fromParts(partialIv = byteArrayOf(0x00), kidContext = null, kid = null),
),
payload = ciphertext,
),
actual = protectedCoapResponse.decode<Message.Udp>(),
)
assertEquals(
actual = Oscore(oscoreOptionValue),
expected = Oscore.fromParts(partialIv = byteArrayOf(0x00), kidContext = null, kid = null),
)
}

@Test
Expand Down
11 changes: 9 additions & 2 deletions koap-core/src/commonTest/kotlin/EncoderTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ class EncoderTest {
@Test
fun writeOscoreOptionWithEmptyValue() {
testWriteOption(
option = Oscore(byteArrayOf()),
option = Oscore.fromParts(byteArrayOf(), null, null),
expected = """
90 # Option Delta: 9, Option Length: 0
""",
Expand All @@ -385,7 +385,7 @@ class EncoderTest {
fun writeOscoreOptionTestVector4() {
// https://datatracker.ietf.org/doc/html/rfc8613#appendix-C.4 Test Vector 4
testWriteOption(
option = Oscore(byteArrayOf(0x09, 0x14)),
option = Oscore.fromParts(partialIv = byteArrayOf(0x14), kidContext = null, kid = byteArrayOf()),
expected = """
92 # Option Delta: 9, Option Length: 2
09 14 # Option Value: 09 14
Expand All @@ -403,6 +403,13 @@ class EncoderTest {
)
}

@Test
fun oscoreOptionIvParameterTooLongThrowsIllegalArgumentException() {
assertFailsWith<IllegalArgumentException> {
Oscore.fromParts(partialIv = byteArrayOf(1, 2, 3, 4, 5, 6), kid = null, kidContext = null)
}
}

@Test
fun observeOptionWithValueOutsideOfAllowableRangeThrowsIllegalArgumentException() {
assertFailsWith<IllegalArgumentException> {
Expand Down
7 changes: 6 additions & 1 deletion koap-core/src/commonTest/kotlin/MessageTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ class MessageTest {
assertToString(IfMatch("".encodeToByteArray()), "IfMatch(etag=)")
assertToString(Echo("echo".encodeToByteArray()), "Echo(value=65 63 68 6F)")
assertToString(RequestTag("tag".encodeToByteArray()), "RequestTag(tag=74 61 67)")
assertToString(Oscore(byteArrayOf(0x09, 0x14)), "Oscore(value=09 14)")
assertToString(Oscore(byteArrayOf()), "Oscore()")
assertToString(Oscore(byteArrayOf(0x01, 0x14)), "Oscore(partialIv=14)")
assertToString(Oscore(byteArrayOf(0x09, 0x14)), "Oscore(partialIv=14, kid=)")
assertToString(Oscore(byteArrayOf(0x09, 0x14, 0x15)), "Oscore(partialIv=14, kid=15)")
assertToString(Oscore(byteArrayOf(0x11, 0x14, 0x01, 0x16)), "Oscore(partialIv=14, kidContext=16)")
assertToString(Oscore(byteArrayOf(0x19, 0x14, 0x01, 0x16)), "Oscore(partialIv=14, kidContext=16, kid=)")
assertToString(IfNoneMatch, "IfNoneMatch")
assertToString(Edhoc, "Edhoc")
assertToString(UriHost("localhost"), "UriHost(uri=localhost)")
Expand Down
Loading