From 50d1dad7867e8aadd3e92772fe002ab53c26d568 Mon Sep 17 00:00:00 2001 From: Andrei Vereha Date: Thu, 11 Mar 2021 22:59:48 +0100 Subject: [PATCH] dash LTK: add test Now that we know this part is working, I'm adding a test with data from logs Moved the key exchange logic to its own class so it is easier to test --- .../dash/driver/comm/pair/KeyExchange.kt | 128 +++++++++++++++++ .../dash/driver/comm/pair/LTKExchanger.kt | 130 +++--------------- .../driver/comm/session/SessionEstablisher.kt | 4 +- .../dash/driver/comm/pair/KeyExchangeTest.kt | 26 ++++ 4 files changed, 174 insertions(+), 114 deletions(-) create mode 100644 omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/KeyExchange.kt create mode 100644 omnipod-dash/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/KeyExchangeTest.kt diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/KeyExchange.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/KeyExchange.kt new file mode 100644 index 0000000000..5f36fcf5c9 --- /dev/null +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/KeyExchange.kt @@ -0,0 +1,128 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair + +import com.google.crypto.tink.subtle.X25519 +import info.nightscout.androidaps.logging.AAPSLogger +import info.nightscout.androidaps.logging.AAPSLoggerTest +import info.nightscout.androidaps.logging.LTag +import info.nightscout.androidaps.plugins.pump.omnipod.dash.BuildConfig +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.MessageIOException +import info.nightscout.androidaps.utils.extensions.toHex +import org.spongycastle.crypto.engines.AESEngine +import org.spongycastle.crypto.macs.CMac +import org.spongycastle.crypto.params.KeyParameter +import java.security.SecureRandom + +class KeyExchange(private val aapsLogger: AAPSLogger, + var pdmPrivate: ByteArray = X25519.generatePrivateKey(), + val pdmNonce: ByteArray = ByteArray(NONCE_SIZE) + ) { + val pdmPublic = X25519.publicFromPrivate(pdmPrivate) + + var podPublic = ByteArray(PUBLIC_KEY_SIZE) + var podNonce = ByteArray(NONCE_SIZE) + + val podConf = ByteArray(CMAC_SIZE) + val pdmConf = ByteArray(CMAC_SIZE) + + var ltk = ByteArray(CMAC_SIZE) + + init { + if (pdmNonce.all { it == 0.toByte() }) { + // pdmNonce is in the constructor for tests + val random = SecureRandom() + random.nextBytes(pdmNonce) + } + } + + fun updatePodPublicData(payload: ByteArray) { + if (payload.size != PUBLIC_KEY_SIZE + NONCE_SIZE) { + throw MessageIOException("Invalid payload size") + } + podPublic = payload.copyOfRange(0, PUBLIC_KEY_SIZE) + podNonce = payload.copyOfRange(PUBLIC_KEY_SIZE, PUBLIC_KEY_SIZE + NONCE_SIZE) + generateKeys() + } + + fun validatePodConf(payload: ByteArray) { + if (!podConf.contentEquals(payload)) { + aapsLogger.warn( + LTag.PUMPBTCOMM, + "Received invalid podConf. Expected: ${podConf.toHex()}. Got: ${payload.toHex()}" + ) + throw MessageIOException("Invalid podConf value received") + } + } + + private fun generateKeys() { + val curveLTK = X25519.computeSharedSecret(pdmPrivate, podPublic) + + val firstKey = podPublic.copyOfRange(podPublic.size - 4, podPublic.size) + + pdmPublic.copyOfRange(pdmPublic.size - 4, pdmPublic.size) + + podNonce.copyOfRange(podNonce.size - 4, podNonce.size) + + pdmNonce.copyOfRange(pdmNonce.size - 4, pdmNonce.size) + aapsLogger.debug(LTag.PUMPBTCOMM, "First key for LTK: ${firstKey.toHex()}") + + val intermediateKey = ByteArray(CMAC_SIZE) + aesCmac(firstKey, curveLTK, intermediateKey) + + val ltkData = byteArrayOf(2.toByte()) + + INTERMEDIARY_KEY_MAGIC_STRING + + podNonce + + pdmNonce + + byteArrayOf(0.toByte(), 1.toByte()) + aesCmac(intermediateKey, ltkData, ltk) + + val confData = byteArrayOf(1.toByte()) + + INTERMEDIARY_KEY_MAGIC_STRING + + podNonce + + pdmNonce + + byteArrayOf(0.toByte(), 1.toByte()) + val confKey = ByteArray(CMAC_SIZE) + aesCmac(intermediateKey, confData, confKey) + + val pdmConfData = PDM_CONF_MAGIC_PREFIX + + pdmNonce + + podNonce + aesCmac(confKey, pdmConfData, pdmConf) + aapsLogger.debug(LTag.PUMPBTCOMM, "pdmConf: ${pdmConf.toHex()}") + + val podConfData = POD_CONF_MAGIC_PREFIX + + podNonce + + pdmNonce + aesCmac(confKey, podConfData, podConf) + aapsLogger.debug(LTag.PUMPBTCOMM, "podConf: ${podConf.toHex()}") + + if (BuildConfig.DEBUG) { + aapsLogger.debug(LTag.PUMPBTCOMM, "pdmPrivate: ${pdmPrivate.toHex()}") + aapsLogger.debug(LTag.PUMPBTCOMM, "pdmPublic: ${pdmPublic.toHex()}") + aapsLogger.debug(LTag.PUMPBTCOMM, "podPublic: ${podPublic.toHex()}") + aapsLogger.debug(LTag.PUMPBTCOMM, "pdmNonce: ${pdmNonce.toHex()}") + aapsLogger.debug(LTag.PUMPBTCOMM, "podNonce: ${podNonce.toHex()}") + + aapsLogger.debug(LTag.PUMPBTCOMM, "LTK, donna key: ${curveLTK.toHex()}") + aapsLogger.debug(LTag.PUMPBTCOMM, "Intermediate key: ${intermediateKey.toHex()}") + aapsLogger.debug(LTag.PUMPBTCOMM, "LTK: ${ltk.toHex()}") + aapsLogger.debug(LTag.PUMPBTCOMM, "Conf KEY: ${confKey.toHex()}") + } + } + + companion object { + + private const val PUBLIC_KEY_SIZE = 32 + private const val NONCE_SIZE = 16 + + const val CMAC_SIZE = 16 + + private val INTERMEDIARY_KEY_MAGIC_STRING = "TWIt".toByteArray() + private val PDM_CONF_MAGIC_PREFIX = "KC_2_U".toByteArray() + private val POD_CONF_MAGIC_PREFIX = "KC_2_V".toByteArray() + } +} + +private fun aesCmac(key: ByteArray, data: ByteArray, result: ByteArray) { + val aesEngine = AESEngine() + val mac = CMac(aesEngine) + mac.init(KeyParameter(key)) + mac.update(data, 0, data.size) + mac.doFinal(result, 0) +} diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/LTKExchanger.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/LTKExchanger.kt index cbecbad350..a7f8349aec 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/LTKExchanger.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/LTKExchanger.kt @@ -1,9 +1,7 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair -import com.google.crypto.tink.subtle.X25519 import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.logging.LTag -import info.nightscout.androidaps.plugins.pump.omnipod.dash.BuildConfig import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.MessageIOException import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageIO @@ -12,27 +10,17 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message. import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.StringLengthPrefixEncoding.Companion.parseKeys import info.nightscout.androidaps.utils.extensions.hexStringToByteArray import info.nightscout.androidaps.utils.extensions.toHex -import org.spongycastle.crypto.engines.AESEngine -import org.spongycastle.crypto.macs.CMac -import org.spongycastle.crypto.params.KeyParameter -import java.security.SecureRandom -internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgIO: MessageIO, val myId: Id, val podId: Id, val podAddress: Id) { +internal class LTKExchanger( + private val aapsLogger: AAPSLogger, + private val msgIO: MessageIO, + val myId: Id, + val podId: Id, + val podAddress: Id +) { - private val pdmPrivate = X25519.generatePrivateKey() - private val pdmPublic = X25519.publicFromPrivate(pdmPrivate) - private var podPublic = ByteArray(PUBLIC_KEY_SIZE) - private var podNonce = ByteArray(NONCE_SIZE) - private val pdmNonce = ByteArray(NONCE_SIZE) - private val pdmConf = ByteArray(CMAC_SIZE) - private val podConf = ByteArray(CMAC_SIZE) + private val keyExchange = KeyExchange(aapsLogger) private var seq: Byte = 1 - private var ltk = ByteArray(CMAC_SIZE) - - init { - val random = SecureRandom() - random.nextBytes(pdmNonce) - } fun negotiateLTK(): PairResult { // send SP1, SP2 @@ -48,7 +36,7 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI val podSps1 = msgIO.receiveMessage() processSps1FromPod(podSps1) // now we have all the data to generate: confPod, confPdm, ltk and noncePrefix - generateKeys() + seq++ // send SPS2 val sps2 = sps2() @@ -66,13 +54,13 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI // TODO: failing to read or validate p0 will lead to undefined state // It could be that: // - the pod answered with p0 and we did not receive/could not process the answer - // - the pod answered with some sort of error + // - the pod answered with some sort of error. This is very unlikely, because we already received(and validated) SPS2 from the pod // But if sps2 conf value is incorrect, then we would probablysee this when receiving the pod podSps2(to test) val p0 = msgIO.receiveMessage() validateP0(p0) return PairResult( - ltk = ltk, + ltk = keyExchange.ltk, msgSeq = seq ) } @@ -93,7 +81,7 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI private fun sps1(): PairMessage { val payload = StringLengthPrefixEncoding.formatKeys( arrayOf("SPS1="), - arrayOf(pdmPublic + pdmNonce) + arrayOf(keyExchange.pdmPublic + keyExchange.pdmNonce) ) return PairMessage( sequenceNumber = seq, @@ -107,17 +95,13 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI aapsLogger.debug(LTag.PUMPBTCOMM, "Received SPS1 from pod: ${msg.payload.toHex()}") val payload = parseKeys(arrayOf(SPS1), msg.payload)[0] - if (payload.size != 48) { - throw MessageIOException("Invalid payload size") - } - podPublic = payload.copyOfRange(0, PUBLIC_KEY_SIZE) - podNonce = payload.copyOfRange(PUBLIC_KEY_SIZE, PUBLIC_KEY_SIZE + NONCE_SIZE) + keyExchange.updatePodPublicData(payload) } private fun sps2(): PairMessage { val payload = StringLengthPrefixEncoding.formatKeys( arrayOf(SPS2), - arrayOf(pdmConf) + arrayOf(keyExchange.pdmConf) ) return PairMessage( sequenceNumber = seq, @@ -133,16 +117,10 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI val payload = parseKeys(arrayOf(SPS2), msg.payload)[0] aapsLogger.debug(LTag.PUMPBTCOMM, "SPS2 payload from pod: ${payload.toHex()}") - if (payload.size != CMAC_SIZE) { + if (payload.size != KeyExchange.CMAC_SIZE) { throw MessageIOException("Invalid payload size") } - if (!podConf.contentEquals(payload)) { - aapsLogger.warn( - LTag.PUMPBTCOMM, - "Received invalid podConf. Expected: ${podConf.toHex()}. Got: ${payload.toHex()}" - ) - throw MessageIOException("Invalid podConf value received") - } + keyExchange.validatePodConf(payload) } private fun sp2(): ByteArray { @@ -171,74 +149,10 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI } } - fun generateKeys() { - val curveLTK = X25519.computeSharedSecret(pdmPrivate, podPublic) - - val firstKey = podPublic.copyOfRange(podPublic.size - 4, podPublic.size) + - pdmPublic.copyOfRange(pdmPublic.size - 4, pdmPublic.size) + - podNonce.copyOfRange(podNonce.size - 4, podNonce.size) + - pdmNonce.copyOfRange(pdmNonce.size - 4, pdmNonce.size) - aapsLogger.debug(LTag.PUMPBTCOMM, "First key for LTK: ${firstKey.toHex()}") - - val intermediateKey = ByteArray(CMAC_SIZE) - aesCmac(firstKey, curveLTK, intermediateKey) - - val ltkData = byteArrayOf(2.toByte()) + - INTERMEDIAR_KEY_MAGIC_STRING + - podNonce + - pdmNonce + - byteArrayOf(0.toByte(), 1.toByte()) - aesCmac(intermediateKey, ltkData, ltk) - - val confData = byteArrayOf(1.toByte()) + - INTERMEDIAR_KEY_MAGIC_STRING + - podNonce + - pdmNonce + - byteArrayOf(0.toByte(), 1.toByte()) - val confKey = ByteArray(CMAC_SIZE) - aesCmac(intermediateKey, confData, confKey) - - val pdmConfData = PDM_CONF_MAGIC_PREFIX + - pdmNonce + - podNonce - aesCmac(confKey, pdmConfData, pdmConf) - aapsLogger.debug(LTag.PUMPBTCOMM, "pdmConf: ${pdmConf.toHex()}") - - val podConfData = POD_CONF_MAGIC_PREFIX + - podNonce + - pdmNonce - aesCmac(confKey, podConfData, podConf) - aapsLogger.debug(LTag.PUMPBTCOMM, "podConf: ${podConf.toHex()}") - - if (BuildConfig.DEBUG) { - aapsLogger.debug(LTag.PUMPBTCOMM, "pdmPrivate: ${pdmPrivate.toHex()}") - aapsLogger.debug(LTag.PUMPBTCOMM, "pdmPublic: ${pdmPublic.toHex()}") - aapsLogger.debug(LTag.PUMPBTCOMM, "podPublic: ${podPublic.toHex()}") - aapsLogger.debug(LTag.PUMPBTCOMM, "pdmNonce: ${pdmNonce.toHex()}") - aapsLogger.debug(LTag.PUMPBTCOMM, "podNonce: ${podNonce.toHex()}") - - aapsLogger.debug(LTag.PUMPBTCOMM, "LTK, donna key: ${curveLTK.toHex()}") - aapsLogger.debug(LTag.PUMPBTCOMM, "Intermediate key: ${intermediateKey.toHex()}") - aapsLogger.debug(LTag.PUMPBTCOMM, "LTK: ${ltk.toHex()}") - aapsLogger.debug(LTag.PUMPBTCOMM, "Conf KEY: ${confKey.toHex()}") - } - } - companion object { - private const val PUBLIC_KEY_SIZE = 32 - private const val NONCE_SIZE = 16 - private const val CONF_SIZE = 16 - - private const val CMAC_SIZE = 16 - - private val INTERMEDIAR_KEY_MAGIC_STRING = "TWIt".toByteArray() - private val PDM_CONF_MAGIC_PREFIX = "KC_2_U".toByteArray() - private val POD_CONF_MAGIC_PREFIX = "KC_2_V".toByteArray() - - private const val GET_POD_STATUS_HEX_COMMAND = "ffc32dbd08030e0100008a" - // TODO for now we are assuming this command is build out of constant parameters, - // use a proper command builder for that. + private const val GET_POD_STATUS_HEX_COMMAND = + "ffc32dbd08030e0100008a" // TODO for now we are assuming this command is build out of constant parameters, use a proper command builder for that. private const val SP1 = "SP1=" private const val SP2 = ",SP2=" @@ -249,11 +163,3 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI private val UNKNOWN_P0_PAYLOAD = byteArrayOf(0xa5.toByte()) } } - -private fun aesCmac(key: ByteArray, data: ByteArray, result: ByteArray) { - val aesEngine = AESEngine() - val mac = CMac(aesEngine) - mac.init(KeyParameter(key)) - mac.update(data, 0, data.size) - mac.doFinal(result, 0) -} diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/session/SessionEstablisher.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/session/SessionEstablisher.kt index 9eae68458e..da37dcfa71 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/session/SessionEstablisher.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/session/SessionEstablisher.kt @@ -14,8 +14,8 @@ import java.security.SecureRandom class SessionEstablisher( private val aapsLogger: AAPSLogger, private val msgIO: MessageIO, - private val ltk: ByteArray, - private val eapSqn: ByteArray, + ltk: ByteArray, + eapSqn: ByteArray, private val myId: Id, private val podId: Id, private var msgSeq: Byte diff --git a/omnipod-dash/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/KeyExchangeTest.kt b/omnipod-dash/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/KeyExchangeTest.kt new file mode 100644 index 0000000000..7d8fcf7edf --- /dev/null +++ b/omnipod-dash/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/KeyExchangeTest.kt @@ -0,0 +1,26 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair + +import info.nightscout.androidaps.logging.AAPSLoggerTest +import info.nightscout.androidaps.utils.extensions.toHex +import org.junit.Assert +import org.junit.Assert.* +import org.junit.Test +import org.spongycastle.util.encoders.Hex + +class KeyExchangeTest { + @Test fun testLTK() { + val aapsLogger = AAPSLoggerTest() + val ke = KeyExchange( + aapsLogger, + pdmPrivate= Hex.decode("27ec94b71a201c5e92698d668806ae5ba00594c307cf5566e60c1fc53a6f6bb6"), + pdmNonce= Hex.decode("edfdacb242c7f4e1d2bc4d93ca3c5706") + ) + val podPublicKey = Hex.decode("2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74") + val podNonce = Hex.decode("00000000000000000000000000000000") + ke.updatePodPublicData(podPublicKey+podNonce) + assertEquals(ke.pdmPublic.toHex(), "f2b6940243aba536a66e19fb9a39e37f1e76a1cd50ab59b3e05313b4fc93975e") + assertEquals(ke.pdmConf.toHex(), "5fc3b4da865e838ceaf1e9e8bb85d1ac") + ke.validatePodConf(Hex.decode("af4f10db5f96e5d9cd6cfc1f54f4a92f")) + assertEquals(ke.ltk.toHex(), "341e16d13f1cbf73b19d1c2964fee02b") + } +} \ No newline at end of file