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
This commit is contained in:
Andrei Vereha 2021-03-11 22:59:48 +01:00
parent 27daa4554a
commit 50d1dad786
4 changed files with 174 additions and 114 deletions

View file

@ -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)
}

View file

@ -1,9 +1,7 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair 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.AAPSLogger
import info.nightscout.androidaps.logging.LTag 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.Id
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.MessageIOException import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.MessageIOException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageIO 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.plugins.pump.omnipod.dash.driver.comm.message.StringLengthPrefixEncoding.Companion.parseKeys
import info.nightscout.androidaps.utils.extensions.hexStringToByteArray import info.nightscout.androidaps.utils.extensions.hexStringToByteArray
import info.nightscout.androidaps.utils.extensions.toHex 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 keyExchange = KeyExchange(aapsLogger)
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 var seq: Byte = 1 private var seq: Byte = 1
private var ltk = ByteArray(CMAC_SIZE)
init {
val random = SecureRandom()
random.nextBytes(pdmNonce)
}
fun negotiateLTK(): PairResult { fun negotiateLTK(): PairResult {
// send SP1, SP2 // send SP1, SP2
@ -48,7 +36,7 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI
val podSps1 = msgIO.receiveMessage() val podSps1 = msgIO.receiveMessage()
processSps1FromPod(podSps1) processSps1FromPod(podSps1)
// now we have all the data to generate: confPod, confPdm, ltk and noncePrefix // now we have all the data to generate: confPod, confPdm, ltk and noncePrefix
generateKeys()
seq++ seq++
// send SPS2 // send SPS2
val sps2 = 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 // TODO: failing to read or validate p0 will lead to undefined state
// It could be that: // It could be that:
// - the pod answered with p0 and we did not receive/could not process the answer // - 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) // But if sps2 conf value is incorrect, then we would probablysee this when receiving the pod podSps2(to test)
val p0 = msgIO.receiveMessage() val p0 = msgIO.receiveMessage()
validateP0(p0) validateP0(p0)
return PairResult( return PairResult(
ltk = ltk, ltk = keyExchange.ltk,
msgSeq = seq msgSeq = seq
) )
} }
@ -93,7 +81,7 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI
private fun sps1(): PairMessage { private fun sps1(): PairMessage {
val payload = StringLengthPrefixEncoding.formatKeys( val payload = StringLengthPrefixEncoding.formatKeys(
arrayOf("SPS1="), arrayOf("SPS1="),
arrayOf(pdmPublic + pdmNonce) arrayOf(keyExchange.pdmPublic + keyExchange.pdmNonce)
) )
return PairMessage( return PairMessage(
sequenceNumber = seq, 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()}") aapsLogger.debug(LTag.PUMPBTCOMM, "Received SPS1 from pod: ${msg.payload.toHex()}")
val payload = parseKeys(arrayOf(SPS1), msg.payload)[0] val payload = parseKeys(arrayOf(SPS1), msg.payload)[0]
if (payload.size != 48) { keyExchange.updatePodPublicData(payload)
throw MessageIOException("Invalid payload size")
}
podPublic = payload.copyOfRange(0, PUBLIC_KEY_SIZE)
podNonce = payload.copyOfRange(PUBLIC_KEY_SIZE, PUBLIC_KEY_SIZE + NONCE_SIZE)
} }
private fun sps2(): PairMessage { private fun sps2(): PairMessage {
val payload = StringLengthPrefixEncoding.formatKeys( val payload = StringLengthPrefixEncoding.formatKeys(
arrayOf(SPS2), arrayOf(SPS2),
arrayOf(pdmConf) arrayOf(keyExchange.pdmConf)
) )
return PairMessage( return PairMessage(
sequenceNumber = seq, sequenceNumber = seq,
@ -133,16 +117,10 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI
val payload = parseKeys(arrayOf(SPS2), msg.payload)[0] val payload = parseKeys(arrayOf(SPS2), msg.payload)[0]
aapsLogger.debug(LTag.PUMPBTCOMM, "SPS2 payload from pod: ${payload.toHex()}") 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") throw MessageIOException("Invalid payload size")
} }
if (!podConf.contentEquals(payload)) { keyExchange.validatePodConf(payload)
aapsLogger.warn(
LTag.PUMPBTCOMM,
"Received invalid podConf. Expected: ${podConf.toHex()}. Got: ${payload.toHex()}"
)
throw MessageIOException("Invalid podConf value received")
}
} }
private fun sp2(): ByteArray { 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 { companion object {
private const val PUBLIC_KEY_SIZE = 32 private const val GET_POD_STATUS_HEX_COMMAND =
private const val NONCE_SIZE = 16 "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 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 SP1 = "SP1=" private const val SP1 = "SP1="
private const val SP2 = ",SP2=" 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 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)
}

View file

@ -14,8 +14,8 @@ import java.security.SecureRandom
class SessionEstablisher( class SessionEstablisher(
private val aapsLogger: AAPSLogger, private val aapsLogger: AAPSLogger,
private val msgIO: MessageIO, private val msgIO: MessageIO,
private val ltk: ByteArray, ltk: ByteArray,
private val eapSqn: ByteArray, eapSqn: ByteArray,
private val myId: Id, private val myId: Id,
private val podId: Id, private val podId: Id,
private var msgSeq: Byte private var msgSeq: Byte

View file

@ -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")
}
}