diff --git a/application/build.gradle.kts b/application/build.gradle.kts index df55c59..f204f77 100644 --- a/application/build.gradle.kts +++ b/application/build.gradle.kts @@ -17,6 +17,8 @@ dependencies { implementation(libs.bundles.data) implementation(libs.logging) + implementation(libs.commonsCodec) + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor") implementation("jakarta.xml.bind:jakarta.xml.bind-api") diff --git a/application/src/integrationTest/resources/application.yaml b/application/src/integrationTest/resources/application.yaml index 7c92dbb..f4a839e 100644 --- a/application/src/integrationTest/resources/application.yaml +++ b/application/src/integrationTest/resources/application.yaml @@ -23,3 +23,4 @@ simulator: # pskKey: coaps_secret_key psk-identity: 867787050253370 psk-key: ABCDEFGHIJKLMNOP + psk-secret: "123456" diff --git a/application/src/main/kotlin/org/gxf/crestdevicesimulator/configuration/CaliforniumConfiguration.kt b/application/src/main/kotlin/org/gxf/crestdevicesimulator/configuration/CaliforniumConfiguration.kt index 19b4c2c..604f968 100644 --- a/application/src/main/kotlin/org/gxf/crestdevicesimulator/configuration/CaliforniumConfiguration.kt +++ b/application/src/main/kotlin/org/gxf/crestdevicesimulator/configuration/CaliforniumConfiguration.kt @@ -29,6 +29,7 @@ class CaliforniumConfiguration(private val simulatorProperties: SimulatorPropert .set(CoapConfig.COAP_PORT, simulatorProperties.uri.port) .set(CoapConfig.COAP_SECURE_PORT, simulatorProperties.uri.port) .set(DtlsConfig.DTLS_ROLE, DtlsRole.CLIENT_ONLY) - .set(DtlsConfig.DTLS_CIPHER_SUITES, listOf(CipherSuite.TLS_PSK_WITH_AES_256_CCM_8)) + .set(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, false) + .set(DtlsConfig.DTLS_CIPHER_SUITES, listOf(CipherSuite.TLS_PSK_WITH_AES_128_CBC_SHA256)) } } diff --git a/application/src/main/kotlin/org/gxf/crestdevicesimulator/configuration/CoapClientConfiguration.kt b/application/src/main/kotlin/org/gxf/crestdevicesimulator/configuration/CoapClientConfiguration.kt index 6ebe814..ff0538f 100644 --- a/application/src/main/kotlin/org/gxf/crestdevicesimulator/configuration/CoapClientConfiguration.kt +++ b/application/src/main/kotlin/org/gxf/crestdevicesimulator/configuration/CoapClientConfiguration.kt @@ -4,7 +4,6 @@ package org.gxf.crestdevicesimulator.configuration -import org.eclipse.californium.elements.config.Configuration import org.gxf.crestdevicesimulator.simulator.data.entity.PreSharedKey import org.gxf.crestdevicesimulator.simulator.data.repository.PskRepository import org.springframework.context.annotation.Bean @@ -19,7 +18,7 @@ class CoapClientConfiguration(private val simulatorProperties: SimulatorProperti val savedKey = pskRepository.findById(simulatorProperties.pskIdentity) if (savedKey.isEmpty) { - val initialPreSharedKey = PreSharedKey(simulatorProperties.pskIdentity, simulatorProperties.pskKey) + val initialPreSharedKey = PreSharedKey(simulatorProperties.pskIdentity, simulatorProperties.pskKey, simulatorProperties.pskSecret) pskRepository.save(initialPreSharedKey) store.key = simulatorProperties.pskKey } else { diff --git a/application/src/main/kotlin/org/gxf/crestdevicesimulator/configuration/SimulatorProperties.kt b/application/src/main/kotlin/org/gxf/crestdevicesimulator/configuration/SimulatorProperties.kt index 9778390..57da455 100644 --- a/application/src/main/kotlin/org/gxf/crestdevicesimulator/configuration/SimulatorProperties.kt +++ b/application/src/main/kotlin/org/gxf/crestdevicesimulator/configuration/SimulatorProperties.kt @@ -12,6 +12,7 @@ class SimulatorProperties( val uri: URI, val pskIdentity: String, val pskKey: String, + val pskSecret: String, val messagePath: String, val produceValidCbor: Boolean, ) diff --git a/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/coap/CoapClientService.kt b/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/coap/CoapClientService.kt index 03d9a21..9314cbe 100644 --- a/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/coap/CoapClientService.kt +++ b/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/coap/CoapClientService.kt @@ -37,7 +37,7 @@ class CoapClientService( } return coapClient } - + private fun createDtlsConnector(advancedSingleIdentityPskStore: AdvancedSingleIdentityPskStore): DTLSConnector { val address = InetSocketAddress(0) val dtlsBuilder = DtlsConnectorConfig.builder(configuration) diff --git a/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/data/entity/PreSharedKey.kt b/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/data/entity/PreSharedKey.kt index 28b87e1..a7203bf 100644 --- a/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/data/entity/PreSharedKey.kt +++ b/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/data/entity/PreSharedKey.kt @@ -8,4 +8,4 @@ import jakarta.persistence.Entity import jakarta.persistence.Id @Entity -class PreSharedKey(@Id val identity: String, var preSharedKey: String) +class PreSharedKey(@Id val identity: String, var preSharedKey: String, var secret: String) diff --git a/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/response/PskExtractor.kt b/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/response/PskExtractor.kt new file mode 100644 index 0000000..0e0add3 --- /dev/null +++ b/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/response/PskExtractor.kt @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 + +package org.gxf.crestdevicesimulator.simulator.response + +object PskExtractor { + + /** + * Regex to split a valid PSK set command in 3 groups + * Group 0 containing everything + * Group 1 containing the next 16 chars after PSK: this is only the key + * Group 2 containing the next 64 chars after the key this is only the hash + */ + private val pskKeyHashSplitterRegex = "!PSK:([a-zA-Z0-9]{16})([a-zA-Z0-9]{64});PSK:[a-zA-Z0-9]{16}[a-zA-Z0-9]{64}SET".toRegex() + + fun hasPskCommand(command: String) = pskKeyHashSplitterRegex.matches(command) + + fun extractKeyFromCommand(command: String) = pskKeyHashSplitterRegex.findAll(command).first().groups[1]!!.value + + fun extractHashFromCommand(command: String) = pskKeyHashSplitterRegex.findAll(command).first().groups[2]!!.value +} diff --git a/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/response/PskKeyExtractor.kt b/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/response/PskKeyExtractor.kt deleted file mode 100644 index 0e213af..0000000 --- a/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/response/PskKeyExtractor.kt +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-FileCopyrightText: Contributors to the GXF project -// -// SPDX-License-Identifier: Apache-2.0 - -package org.gxf.crestdevicesimulator.simulator.response - -import org.springframework.stereotype.Component - -@Component -class PskKeyExtractor { - - private val pskCommandVerificationRegex = "PSK:[a-zA-Z0-9]{16};PSK:[a-zA-Z0-9]{16}SET".toRegex() - private val pskExtractorRegex = "(?<=PSK:)[a-zA-Z0-9]{16}".toRegex() - - fun hasPskCommand(command: String) = pskCommandVerificationRegex.matches(command) - - fun extractKeyFromCommand(command: String) = pskExtractorRegex.findAll(command).first().value -} diff --git a/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/response/ResponseHandler.kt b/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/response/ResponseHandler.kt index 0837b79..bd6f229 100644 --- a/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/response/ResponseHandler.kt +++ b/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/response/ResponseHandler.kt @@ -4,39 +4,18 @@ package org.gxf.crestdevicesimulator.simulator.response -import io.github.oshai.kotlinlogging.KotlinLogging import org.eclipse.californium.core.CoapResponse -import org.gxf.crestdevicesimulator.configuration.AdvancedSingleIdentityPskStore -import org.gxf.crestdevicesimulator.configuration.SimulatorProperties -import org.gxf.crestdevicesimulator.simulator.data.repository.PskRepository +import org.gxf.crestdevicesimulator.simulator.response.command.PskCommandHandler import org.springframework.stereotype.Component @Component -class ResponseHandler(private val simulatorProperties: SimulatorProperties, - private val pskRepository: PskRepository, - private val pskKeyExtractor: PskKeyExtractor, - private val pskStore: AdvancedSingleIdentityPskStore) { - - private val logger = KotlinLogging.logger {} +class ResponseHandler(private val pskCommandHandler: PskCommandHandler) { fun handleResponse(response: CoapResponse) { val body = String(response.payload) - if (pskKeyExtractor.hasPskCommand(body)) { - val newPsk = pskKeyExtractor.extractKeyFromCommand(body) - handlePskChange(newPsk) - } - } - - private fun handlePskChange(newPsk: String) { - val current = pskRepository.findById(simulatorProperties.pskIdentity) - if (current.isEmpty) { - logger.error { "No psk for identity: ${simulatorProperties.pskIdentity}" } + if (PskExtractor.hasPskCommand(body)) { + pskCommandHandler.handlePskChange(body) } - - logger.info { "Setting psk $newPsk for ${simulatorProperties.pskIdentity}" } - - pskRepository.save(current.get().apply { preSharedKey = newPsk }) - pskStore.key = newPsk } } diff --git a/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/response/command/PskCommandHandler.kt b/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/response/command/PskCommandHandler.kt new file mode 100644 index 0000000..c07899c --- /dev/null +++ b/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/response/command/PskCommandHandler.kt @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 + +package org.gxf.crestdevicesimulator.simulator.response.command + +import io.github.oshai.kotlinlogging.KotlinLogging +import org.apache.commons.codec.digest.DigestUtils +import org.gxf.crestdevicesimulator.configuration.AdvancedSingleIdentityPskStore +import org.gxf.crestdevicesimulator.configuration.SimulatorProperties +import org.gxf.crestdevicesimulator.simulator.data.repository.PskRepository +import org.gxf.crestdevicesimulator.simulator.response.PskExtractor +import org.gxf.crestdevicesimulator.simulator.response.command.exception.InvalidPskHashException +import org.springframework.stereotype.Service + +@Service +class PskCommandHandler(private val pskRepository: PskRepository, + private val simulatorProperties: SimulatorProperties, + private val pskStore: AdvancedSingleIdentityPskStore) { + + private val logger = KotlinLogging.logger {} + + fun handlePskChange(body: String) { + val newPsk = PskExtractor.extractKeyFromCommand(body) + val hash = PskExtractor.extractHashFromCommand(body) + + val preSharedKeyOptional = pskRepository.findById(simulatorProperties.pskIdentity) + + if (preSharedKeyOptional.isEmpty) { + logger.error { "No psk for identity: ${simulatorProperties.pskIdentity}" } + } + + logger.info { "Validating hash for identity: ${simulatorProperties.pskIdentity}" } + + val preSharedKey = preSharedKeyOptional.get() + val secret = preSharedKey.secret + val expectedHash = DigestUtils.sha256Hex("$secret$newPsk") + + if (expectedHash != hash) { + throw InvalidPskHashException("PSK set Hash for Identity ${simulatorProperties.pskIdentity} did not match") + } + + pskRepository.save(preSharedKey.apply { this.preSharedKey = newPsk }) + pskStore.key = newPsk + } +} diff --git a/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/response/command/exception/InvalidPskHashException.kt b/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/response/command/exception/InvalidPskHashException.kt new file mode 100644 index 0000000..6663db9 --- /dev/null +++ b/application/src/main/kotlin/org/gxf/crestdevicesimulator/simulator/response/command/exception/InvalidPskHashException.kt @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 + +package org.gxf.crestdevicesimulator.simulator.response.command.exception + +class InvalidPskHashException(message: String) : Exception(message) diff --git a/application/src/main/resources/application-dev.yaml b/application/src/main/resources/application-dev.yaml index 6bde69f..44c9cde 100644 --- a/application/src/main/resources/application-dev.yaml +++ b/application/src/main/resources/application-dev.yaml @@ -20,3 +20,4 @@ simulator: # pskKey: coaps_secret_key psk-identity: 867787050253370 psk-key: ABCDEFGHIJKLMNOP + psk-secret: "123456" diff --git a/application/src/main/resources/db/migration/V2__secret.sql b/application/src/main/resources/db/migration/V2__secret.sql new file mode 100644 index 0000000..c99bac5 --- /dev/null +++ b/application/src/main/resources/db/migration/V2__secret.sql @@ -0,0 +1,8 @@ +-- SPDX-FileCopyrightText: Contributors to the GXF project +-- +-- SPDX-License-Identifier: Apache-2.0 +-- No production data was set before this change so we can drop all existing data +delete from pre_shared_key; + +alter table pre_shared_key + add column secret varchar(255) not null; diff --git a/application/src/test/kotlin/PreSharedKeyKeyExtractorTest.kt b/application/src/test/kotlin/PreSharedKeyKeyExtractorTest.kt deleted file mode 100644 index 358bd69..0000000 --- a/application/src/test/kotlin/PreSharedKeyKeyExtractorTest.kt +++ /dev/null @@ -1,36 +0,0 @@ -import org.gxf.crestdevicesimulator.simulator.response.PskKeyExtractor -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test - -class PreSharedKeyKeyExtractorTest { - - private val pskKeyExtractor = PskKeyExtractor() - - private val validPskCommand = "PSK:1234567891234567;PSK:1234567891234567SET" - private val validPskCommandWithKeyWordsInKey = "PSK:PSKaSET1PSKd2SET;PSK:PSKaSET1PSKd2SETSET" - private val invalidKeySizePskCommand = "PSK:1234;PSK:1234SET" - private val notPskCommand = "NoPskCommandInThisString" - - - @Test - fun shouldReturnTrueWhenThereIsAPskCommandInString() { - val resultValid = pskKeyExtractor.hasPskCommand(validPskCommand) - val resultValidWithKeyWords = pskKeyExtractor.hasPskCommand(validPskCommandWithKeyWordsInKey) - val resultInvalidKeySize = pskKeyExtractor.hasPskCommand(invalidKeySizePskCommand) - val resultNoPskCommand = pskKeyExtractor.hasPskCommand(notPskCommand) - - assertTrue(resultValid) - assertTrue(resultValidWithKeyWords) - assertFalse(resultInvalidKeySize) - assertFalse(resultNoPskCommand) - } - - @Test - fun shouldReturnPskKeyFromValidPskCommand() { - val resultValid = pskKeyExtractor.extractKeyFromCommand(validPskCommand) - val resultValidWithKeyWords = pskKeyExtractor.extractKeyFromCommand(validPskCommandWithKeyWordsInKey) - - assertEquals("1234567891234567", resultValid) - assertEquals("PSKaSET1PSKd2SET", resultValidWithKeyWords) - } -} diff --git a/application/src/test/kotlin/SimulatorTests.kt b/application/src/test/kotlin/org/gxf/crestdevicesimulator/simulator/SimulatorTests.kt similarity index 95% rename from application/src/test/kotlin/SimulatorTests.kt rename to application/src/test/kotlin/org/gxf/crestdevicesimulator/simulator/SimulatorTests.kt index e584da2..c82d504 100644 --- a/application/src/test/kotlin/SimulatorTests.kt +++ b/application/src/test/kotlin/org/gxf/crestdevicesimulator/simulator/SimulatorTests.kt @@ -1,11 +1,11 @@ +package org.gxf.crestdevicesimulator.simulator + import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.dataformat.cbor.databind.CBORMapper import org.eclipse.californium.core.CoapClient import org.eclipse.californium.core.CoapResponse import org.eclipse.californium.core.coap.Request import org.gxf.crestdevicesimulator.configuration.SimulatorProperties -import org.gxf.crestdevicesimulator.simulator.CborFactory -import org.gxf.crestdevicesimulator.simulator.Simulator import org.gxf.crestdevicesimulator.simulator.coap.CoapClientService import org.gxf.crestdevicesimulator.simulator.response.ResponseHandler import org.junit.jupiter.api.Assertions.assertArrayEquals diff --git a/application/src/test/kotlin/org/gxf/crestdevicesimulator/simulator/response/PskExtractorTest.kt b/application/src/test/kotlin/org/gxf/crestdevicesimulator/simulator/response/PskExtractorTest.kt new file mode 100644 index 0000000..efa148f --- /dev/null +++ b/application/src/test/kotlin/org/gxf/crestdevicesimulator/simulator/response/PskExtractorTest.kt @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdevicesimulator.simulator.response + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + + +class PskExtractorTest { + + companion object { + private const val testHash = "1234567890123456123456789012345612345678901234561234567890123456" + + private const val validPskCommand = "!PSK:1234567891234567${testHash};PSK:1234567891234567${testHash}SET" + private const val validPskCommandWithKeyWordsInKey = "!PSK:PSKaSET1PSKd2SET${testHash};PSK:PSKaSET1PSKd2SET${testHash}SET" + private const val invalidKeySizePskCommand = "!PSK:1234${testHash};PSK:1234${testHash}SET" + private const val notPskCommand = "NoPskCommandInThisString" + } + + + @ParameterizedTest + @CsvSource( + "$validPskCommand, true", + "$validPskCommandWithKeyWordsInKey, true", + "$invalidKeySizePskCommand, false", + "$notPskCommand, false" + ) + fun shouldReturnTrueWhenThereIsAPskCommandInString(pskCommand: String, isValid: Boolean) { + val result = PskExtractor.hasPskCommand(pskCommand) + assertThat(result).isEqualTo(isValid) + } + + @ParameterizedTest + @CsvSource( + "$validPskCommand, 1234567891234567", + "$validPskCommandWithKeyWordsInKey, PSKaSET1PSKd2SET" + ) + fun shouldReturnPskKeyFromValidPskCommand(pskCommand: String, expectedKey: String) { + val result = PskExtractor.extractKeyFromCommand(pskCommand) + + assertThat(result).isEqualTo(expectedKey) + } + + @ParameterizedTest + @CsvSource( + "$validPskCommand, $testHash", + "$validPskCommandWithKeyWordsInKey, $testHash" + ) + fun shouldReturnHashFromValidPskCommand(pskCommand: String, expectedHash: String) { + val result = PskExtractor.extractHashFromCommand(pskCommand) + + assertThat(result).isEqualTo(expectedHash) + } +} diff --git a/application/src/test/kotlin/org/gxf/crestdevicesimulator/simulator/response/command/PskCommandHandlerTest.kt b/application/src/test/kotlin/org/gxf/crestdevicesimulator/simulator/response/command/PskCommandHandlerTest.kt new file mode 100644 index 0000000..5a6ce47 --- /dev/null +++ b/application/src/test/kotlin/org/gxf/crestdevicesimulator/simulator/response/command/PskCommandHandlerTest.kt @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdevicesimulator.simulator.response.command + +import org.apache.commons.codec.digest.DigestUtils +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.catchException +import org.gxf.crestdevicesimulator.configuration.AdvancedSingleIdentityPskStore +import org.gxf.crestdevicesimulator.configuration.SimulatorProperties +import org.gxf.crestdevicesimulator.simulator.data.entity.PreSharedKey +import org.gxf.crestdevicesimulator.simulator.data.repository.PskRepository +import org.gxf.crestdevicesimulator.simulator.response.command.exception.InvalidPskHashException +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Answers +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.junit.jupiter.MockitoExtension +import java.util.* + +@ExtendWith(MockitoExtension::class) +class PskCommandHandlerTest { + + @Mock + private lateinit var pskRepository: PskRepository + + @Mock + private lateinit var simulatorProperties: SimulatorProperties + + @Mock(answer = Answers.CALLS_REAL_METHODS) + private lateinit var pskStore: AdvancedSingleIdentityPskStore + + @InjectMocks + private lateinit var pskCommandHandler: PskCommandHandler + + private val newKey = "7654321987654321" + + private val oldKey = "1234567891234567" + + private val secret = "secret" + + private val identity = "1234" + + @BeforeEach + fun setup() { + `when`(simulatorProperties.pskIdentity).thenReturn(identity) + `when`(pskRepository.findById(any())).thenReturn(Optional.of(PreSharedKey(identity, oldKey, secret))) + pskStore.key = oldKey + } + + @Test + fun shouldSetNewPskInStoreWhenTheKeyIsValid() { + val expectedHash = DigestUtils.sha256Hex("$secret$newKey") + val pskCommand = "!PSK:$newKey$expectedHash;PSK:$newKey${expectedHash}SET" + + pskCommandHandler.handlePskChange(pskCommand) + + assertThat(pskStore.key).isEqualTo(newKey) + } + + @Test + fun shouldThrowErrorWhenHashDoesNotMatch() { + val invalidHash = DigestUtils.sha256Hex("invalid") + val pskCommand = "!PSK:$oldKey$invalidHash;PSK:$oldKey${invalidHash}SET" + + val thrownException = catchException { + pskCommandHandler.handlePskChange(pskCommand) + } + + assertThat(thrownException).isInstanceOf(InvalidPskHashException::class.java) + verify(pskRepository, never()).save(any()) + assertThat(pskStore.key).isEqualTo(oldKey) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index c89542e..0bf366b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,6 +19,8 @@ dependencyResolutionManagement { bundle("data", listOf("postgresql", "flyway")) library("logging", "io.github.oshai", "kotlin-logging-jvm").version("6.0.1") + + library("commonsCodec", "commons-codec", "commons-codec").version("1.16.0") } create("integrationTestLibs") { library("h2", "com.h2database", "h2").version("2.2.224")