Skip to content
Merged
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
16 changes: 16 additions & 0 deletions koap-core/api/koap-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,11 @@ public final class com/juul/koap/Message$Option$Echo : com/juul/koap/Message$Opt
public fun toString ()Ljava/lang/String;
}

public final class com/juul/koap/Message$Option$Edhoc : com/juul/koap/Message$Option {
public static final field INSTANCE Lcom/juul/koap/Message$Option$Edhoc;
public fun toString ()Ljava/lang/String;
}

public abstract class com/juul/koap/Message$Option$Format : com/juul/koap/Message$Option {
public abstract fun getNumber ()I
}
Expand Down Expand Up @@ -475,6 +480,17 @@ public final class com/juul/koap/Message$Option$Observe$Registration$Register :
public fun toString ()Ljava/lang/String;
}

public final class com/juul/koap/Message$Option$Oscore : com/juul/koap/Message$Option {
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 fun toString ()Ljava/lang/String;
}

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
17 changes: 17 additions & 0 deletions koap-core/api/koap-core.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,19 @@ sealed class com.juul.koap/Message { // com.juul.koap/Message|null[0]
}
}

final class Oscore : com.juul.koap/Message.Option { // com.juul.koap/Message.Option.Oscore|null[0]
constructor <init>(kotlin/ByteArray) // com.juul.koap/Message.Option.Oscore.<init>|<init>(kotlin.ByteArray){}[0]

final val value // com.juul.koap/Message.Option.Oscore.value|{}value[0]
final fun <get-value>(): kotlin/ByteArray // com.juul.koap/Message.Option.Oscore.value.<get-value>|<get-value>(){}[0]

final fun component1(): kotlin/ByteArray // com.juul.koap/Message.Option.Oscore.component1|component1(){}[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 toString(): kotlin/String // com.juul.koap/Message.Option.Oscore.toString|toString(){}[0]
}

final class ProxyScheme : com.juul.koap/Message.Option { // com.juul.koap/Message.Option.ProxyScheme|null[0]
constructor <init>(kotlin/String) // com.juul.koap/Message.Option.ProxyScheme.<init>|<init>(kotlin.String){}[0]

Expand Down Expand Up @@ -704,6 +717,10 @@ sealed class com.juul.koap/Message { // com.juul.koap/Message|null[0]
}
}

final object Edhoc : com.juul.koap/Message.Option { // com.juul.koap/Message.Option.Edhoc|null[0]
final fun toString(): kotlin/String // com.juul.koap/Message.Option.Edhoc.toString|toString(){}[0]
}

final object IfNoneMatch : com.juul.koap/Message.Option { // com.juul.koap/Message.Option.IfNoneMatch|null[0]
final fun toString(): kotlin/String // com.juul.koap/Message.Option.IfNoneMatch.toString|toString(){}[0]
}
Expand Down
4 changes: 4 additions & 0 deletions koap-core/src/commonMain/kotlin/Decoder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import com.juul.koap.Message.Option.Block2
import com.juul.koap.Message.Option.ContentFormat
import com.juul.koap.Message.Option.ETag
import com.juul.koap.Message.Option.Echo
import com.juul.koap.Message.Option.Edhoc
import com.juul.koap.Message.Option.Format
import com.juul.koap.Message.Option.HopLimit
import com.juul.koap.Message.Option.IfMatch
Expand All @@ -41,6 +42,7 @@ import com.juul.koap.Message.Option.LocationQuery
import com.juul.koap.Message.Option.MaxAge
import com.juul.koap.Message.Option.NoResponse
import com.juul.koap.Message.Option.Observe
import com.juul.koap.Message.Option.Oscore
import com.juul.koap.Message.Option.ProxyScheme
import com.juul.koap.Message.Option.ProxyUri
import com.juul.koap.Message.Option.QBlock1
Expand Down Expand Up @@ -406,6 +408,7 @@ internal fun ByteArrayReader.readOption(preceding: Format?): Option? {
6 -> Observe(readNumberOfLength(length))
7 -> UriPort(readNumberOfLength(length))
8 -> LocationPath(readUtf8(length))
9 -> Oscore(readByteArray(length))
11 -> UriPath(readUtf8(length))
12 -> ContentFormat(readNumberOfLength(length))
14 -> MaxAge(readNumberOfLength(length))
Expand All @@ -414,6 +417,7 @@ internal fun ByteArrayReader.readOption(preceding: Format?): Option? {
17 -> Accept(readNumberOfLength(length))
19 -> blockOf<QBlock1>(readNumberOfLength(length))
20 -> LocationQuery(readUtf8(length))
21 -> Edhoc
23 -> blockOf<Block2>(readNumberOfLength(length))
27 -> blockOf<Block1>(readNumberOfLength(length))
28 -> Size2(readNumberOfLength(length))
Expand Down
32 changes: 28 additions & 4 deletions koap-core/src/commonMain/kotlin/Message.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ private val PROXY_SCHEME_LENGTH_RANGE = 1..255
private val SIZE_RANGE = UINT_RANGE // Size1, Size2
private val OBSERVE_RANGE = 0..16_777_215 // 3-byte unsigned int
internal val BLOCK_NUMBER_RANGE = 0..0xF_FF_FF
private val OSCORE_LENGTH_RANGE = 0..255
private val HOP_LIMIT_RANGE = 1..255
private val ECHO_SIZE_RANGE = 1..40
private val REQUEST_TAG_SIZE_RANGE = 0..8
Expand Down Expand Up @@ -88,7 +89,7 @@ sealed class Message {
* | 6 | [Observe][Observe] | uint | 0-3 | [RFC 7641](https://tools.ietf.org/html/rfc7641#section-2) Observing Resources 2 |
* | 7 | [Uri-Port][UriPort] | uint | 0-2 | [RFC 7252](https://tools.ietf.org/html/rfc7252#section-5.10.1) CoAP 5.10.1 |
* | 8 | [Location-Path][LocationPath] | string | 0-255 | [RFC 7252](https://tools.ietf.org/html/rfc7252#section-5.10.7) CoAP 5.10.7 |
* | 9 | OSCORE | opaque | 0-255 | [RFC 8613](https://tools.ietf.org/html/rfc8613#section-2) OSCORE 2 |
* | 9 | [OSCORE][Oscore] | opaque | 0-255 | [RFC 8613](https://tools.ietf.org/html/rfc8613#section-2) OSCORE 2 |
* | 11 | [Uri-Path][UriPath] | string | 0-255 | [RFC 7252](https://tools.ietf.org/html/rfc7252#section-5.10.1) CoAP 5.10.1 |
* | 12 | [Content-Format][ContentFormat] | uint | 0-2 | [RFC 7252](https://tools.ietf.org/html/rfc7252#section-5.10.3) CoAP 5.10.3 |
* | 14 | [Max-Age][MaxAge] | uint | 0-4 | [RFC 7252](https://tools.ietf.org/html/rfc7252#section-5.10.5) CoAP 5.10.5 |
Expand All @@ -97,7 +98,7 @@ sealed class Message {
* | 17 | [Accept][Accept] | uint | 0-2 | [RFC 7252](https://tools.ietf.org/html/rfc7252#section-5.10.4) CoAP 5.10.4 |
* | 19 | [Q-Block1][QBlock1] | uint | 0-3 | [RFC 9177](https://tools.ietf.org/html/rfc9177#section-4) Block-Wise Robust 4 |
* | 20 | [Location-Query][LocationQuery] | string | 0-255 | [RFC 7252](https://tools.ietf.org/html/rfc7252#section-5.10.7) CoAP 5.10.7 |
* | 21 | EDHOC | empty | 0 | [RFC 9668](https://tools.ietf.org/html/rfc9668#section-3.1) EDHOC 3.1 |
* | 21 | [EDHOC][Edhoc] | empty | 0 | [RFC 9668](https://tools.ietf.org/html/rfc9668#section-3.1) EDHOC 3.1 |
* | 23 | [Block2] | uint | 0-3 | [RFC 7959](https://tools.ietf.org/html/rfc7959#section-2.1) Block-Wise 2.1 |
* | 27 | [Block1] | uint | 0-3 | [RFC 7959](https://tools.ietf.org/html/rfc7959#section-2.1) Block-Wise 2.1 |
* | 28 | [Size2] | uint | 0-4 | [RFC 7959](https://tools.ietf.org/html/rfc7959#section-4) Block-Wise 4 |
Expand Down Expand Up @@ -296,7 +297,7 @@ sealed class Message {

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

override fun toString(): String = "ETag(etag=${etag.toHexString()}"
override fun toString(): String = "ETag(etag=${etag.toHexString()})"
}

/** RFC 7252 5.10.7. Location-Path and Location-Query */
Expand Down Expand Up @@ -336,7 +337,7 @@ sealed class Message {

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

override fun toString(): String = "IfMatch(etag=${etag.toHexString()}"
override fun toString(): String = "IfMatch(etag=${etag.toHexString()})"
}

/** RFC 7252 5.10.8.2. If-None-Match */
Expand Down Expand Up @@ -514,6 +515,29 @@ sealed class Message {
}
}

/** RFC 8613 2. OSCORE */
data class Oscore(
val value: ByteArray,
) : Option() {
init {
require(value.size in OSCORE_LENGTH_RANGE) {
"Oscore length of ${value.size} is outside allowable range of $OSCORE_LENGTH_RANGE"
}
}

override fun equals(other: Any?): Boolean =
this === other || (other is Oscore && value.contentEquals(other.value))

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

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

/** RFC 9668 3.1. EDHOC */
object Edhoc : Option() {
override fun toString(): String = "Edhoc"
}

/** RFC 8768 3. Hop-Limit */
data class HopLimit(
val hops: Long,
Expand Down
4 changes: 4 additions & 0 deletions koap-core/src/commonMain/kotlin/ToFormat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.juul.koap.Message.Option.Block2
import com.juul.koap.Message.Option.ContentFormat
import com.juul.koap.Message.Option.ETag
import com.juul.koap.Message.Option.Echo
import com.juul.koap.Message.Option.Edhoc
import com.juul.koap.Message.Option.Format
import com.juul.koap.Message.Option.Format.empty
import com.juul.koap.Message.Option.Format.opaque
Expand All @@ -20,6 +21,7 @@ import com.juul.koap.Message.Option.LocationQuery
import com.juul.koap.Message.Option.MaxAge
import com.juul.koap.Message.Option.NoResponse
import com.juul.koap.Message.Option.Observe
import com.juul.koap.Message.Option.Oscore
import com.juul.koap.Message.Option.ProxyScheme
import com.juul.koap.Message.Option.ProxyUri
import com.juul.koap.Message.Option.QBlock1
Expand All @@ -43,6 +45,7 @@ internal fun Option.toFormat(): Format =
is Observe -> uint(6, option.value)
is UriPort -> uint(7, option.port)
is LocationPath -> string(8, option.uri)
is Oscore -> opaque(9, option.value)
is UriPath -> string(11, option.uri)
is ContentFormat -> uint(12, option.format)
is MaxAge -> uint(14, option.seconds)
Expand All @@ -51,6 +54,7 @@ internal fun Option.toFormat(): Format =
is Accept -> uint(17, option.format)
is QBlock1 -> uint(19, option.intValue.toLong())
is LocationQuery -> string(20, option.uri)
is Edhoc -> empty(21)
is Block2 -> uint(23, option.intValue.toLong())
is Block1 -> uint(27, option.intValue.toLong())
is Size2 -> uint(28, option.bytes)
Expand Down
173 changes: 173 additions & 0 deletions koap-core/src/commonTest/kotlin/DecoderTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@ package com.juul.koap.test

import com.juul.koap.Message
import com.juul.koap.Message.Code.Method.GET
import com.juul.koap.Message.Code.Method.POST
import com.juul.koap.Message.Code.Response.Changed
import com.juul.koap.Message.Option.Block.Size.Bert
import com.juul.koap.Message.Option.Block1
import com.juul.koap.Message.Option.Block2
import com.juul.koap.Message.Option.Echo
import com.juul.koap.Message.Option.Edhoc
import com.juul.koap.Message.Option.HopLimit
import com.juul.koap.Message.Option.NoResponse
import com.juul.koap.Message.Option.NoResponse.NotInterestedIn.Response4xx
import com.juul.koap.Message.Option.NoResponse.NotInterestedIn.Response5xx
import com.juul.koap.Message.Option.Observe
import com.juul.koap.Message.Option.Observe.Registration.Deregister
import com.juul.koap.Message.Option.Observe.Registration.Register
import com.juul.koap.Message.Option.Oscore
import com.juul.koap.Message.Option.QBlock1
import com.juul.koap.Message.Option.QBlock2
import com.juul.koap.Message.Option.RequestTag
Expand Down Expand Up @@ -253,6 +257,175 @@ class DecoderTest {
)
}

@Test
fun decodeOscoreOptionWithEmptyValue() {
testReadOption(
encoded = """
90 # Option Delta: 9, Option Length: 0
""",
expected = Oscore(byteArrayOf()),
)
}

// Test Vector 4: OSCORE Request, Client
// https://datatracker.ietf.org/doc/html/rfc8613#appendix-C.4
@Test
fun decodeOscoreOptionTestVector4() {
val unprotectedCoapRequest = "44015d1f00003974396c6f63616c686f737483747631".decodeHex().toByteArray().decode<Message.Udp>()

val oscoreOptionValue = "0914".decodeHex().toByteArray()
val ciphertext = "612f1092f1776f1c1668b3825e".decodeHex().toByteArray()
val protectedCoapRequest = "44025d1f00003974396c6f63616c686f7374620914ff612f1092f1776f1c1668b3825e".decodeHex().toByteArray()

assertEquals(
expected = Message.Udp(
type = unprotectedCoapRequest.type,

// "the Outer Code SHALL be set to 0.02 (POST) for requests [..]"
// https://datatracker.ietf.org/doc/html/rfc8613#section-4.2
code = POST,

id = unprotectedCoapRequest.id,
token = unprotectedCoapRequest.token,
options = listOf(
UriHost("localhost"),
Oscore(oscoreOptionValue),
),
payload = ciphertext,
),
actual = protectedCoapRequest.decode<Message.Udp>(),
)
}

// Test Vector 5: OSCORE Request, Client
// https://datatracker.ietf.org/doc/html/rfc8613#appendix-C.5
@Test
fun decodeOscoreOptionTestVector5() {
val unprotectedCoapRequest = "440171c30000b932396c6f63616c686f737483747631".decodeHex().toByteArray().decode<Message.Udp>()

val oscoreOptionValue = "091400".decodeHex().toByteArray()
val ciphertext = "4ed339a5a379b0b8bc731fffb0".decodeHex().toByteArray()
val protectedCoapRequest = "440271c30000b932396c6f63616c686f737463091400ff4ed339a5a379b0b8bc731fffb0".decodeHex().toByteArray()

assertEquals(
expected = Message.Udp(
type = unprotectedCoapRequest.type,

// "the Outer Code SHALL be set to 0.02 (POST) for requests [..]"
// https://datatracker.ietf.org/doc/html/rfc8613#section-4.2
code = POST,

id = unprotectedCoapRequest.id,
token = unprotectedCoapRequest.token,
options = listOf(
UriHost("localhost"),
Oscore(oscoreOptionValue),
),
payload = ciphertext,
),
actual = protectedCoapRequest.decode<Message.Udp>(),
)
}

// Test Vector 6: OSCORE Request, Client
// https://datatracker.ietf.org/doc/html/rfc8613#appendix-C.6
@Test
fun decodeOscoreOptionTestVector6() {
val unprotectedCoapRequest = "44012f8eef9bbf7a396c6f63616c686f737483747631".decodeHex().toByteArray().decode<Message.Udp>()

val oscoreOptionValue = "19140837cbf3210017a2d3".decodeHex().toByteArray()
val ciphertext = "72cd7273fd331ac45cffbe55c3".decodeHex().toByteArray()
val protectedCoapRequest =
"44022f8eef9bbf7a396c6f63616c686f73746b19140837cbf3210017a2d3ff72cd7273fd331ac45cffbe55c3".decodeHex().toByteArray()

assertEquals(
expected = Message.Udp(
type = unprotectedCoapRequest.type,

// "the Outer Code SHALL be set to 0.02 (POST) for requests [..]"
// https://datatracker.ietf.org/doc/html/rfc8613#section-4.2
code = POST,

id = unprotectedCoapRequest.id,
token = unprotectedCoapRequest.token,
options = listOf(
UriHost("localhost"),
Oscore(oscoreOptionValue),
),
payload = ciphertext,
),
actual = protectedCoapRequest.decode<Message.Udp>(),
)
}

// Test Vector 7: OSCORE Response, Server
// https://datatracker.ietf.org/doc/html/rfc8613#appendix-C.7
@Test
fun decodeOscoreOptionTestVector7() {
val unprotectedCoapResponse = "64455d1f00003974ff48656c6c6f20576f726c6421".decodeHex().toByteArray().decode<Message.Udp>()

val oscoreOptionValue = "".decodeHex().toByteArray()
val ciphertext = "dbaad1e9a7e7b2a813d3c31524378303cdafae119106".decodeHex().toByteArray()
val protectedCoapResponse = "64445d1f0000397490ffdbaad1e9a7e7b2a813d3c31524378303cdafae119106".decodeHex().toByteArray()

assertEquals(
expected = Message.Udp(
type = unprotectedCoapResponse.type,

// "the Outer Code SHALL be set to [..] 2.04 (Changed) for responses"
// https://datatracker.ietf.org/doc/html/rfc8613#section-4.2
code = Changed,

id = unprotectedCoapResponse.id,
token = unprotectedCoapResponse.token,
options = listOf(
Oscore(oscoreOptionValue),
),
payload = ciphertext,
),
actual = protectedCoapResponse.decode<Message.Udp>(),
)
}

// Test Vector 8: OSCORE Response with Partial IV, Server
// https://datatracker.ietf.org/doc/html/rfc8613#appendix-C.8
@Test
fun decodeOscoreOptionTestVector8() {
val unprotectedCoapResponse = "64455d1f00003974ff48656c6c6f20576f726c6421".decodeHex().toByteArray().decode<Message.Udp>()

val oscoreOptionValue = "0100".decodeHex().toByteArray()
val ciphertext = "4d4c13669384b67354b2b6175ff4b8658c666a6cf88e".decodeHex().toByteArray()
val protectedCoapResponse = "64445d1f00003974920100ff4d4c13669384b67354b2b6175ff4b8658c666a6cf88e".decodeHex().toByteArray()

assertEquals(
expected = Message.Udp(
type = unprotectedCoapResponse.type,

// "the Outer Code SHALL be set to [..] 2.04 (Changed) for responses"
// https://datatracker.ietf.org/doc/html/rfc8613#section-4.2
code = Changed,

id = unprotectedCoapResponse.id,
token = unprotectedCoapResponse.token,
options = listOf(
Oscore(oscoreOptionValue),
),
payload = ciphertext,
),
actual = protectedCoapResponse.decode<Message.Udp>(),
)
}

@Test
fun decodeEdhocOption() {
testReadOption(
encoded = """
D0 08 # Option Delta: 21, Option Length: 0
""",
expected = Edhoc,
)
}

@Test
fun decodeHopLimit() {
testReadOption(
Expand Down
Loading
Loading