diff --git a/build.gradle b/build.gradle index a3026de30d..fd870b3ad6 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,7 @@ buildscript { commonscodec_version = '1.15' jodatime_version = '2.10.10' work_version = '2.5.0' + tink_version = '1.5.0' junit_version = '4.13.2' mockitoVersion = '3.7.7' diff --git a/core/core_dependencies.gradle b/core/core_dependencies.gradle index 552aa32c15..e5f68665f6 100644 --- a/core/core_dependencies.gradle +++ b/core/core_dependencies.gradle @@ -46,6 +46,7 @@ dependencies { //CryptoUtil api 'com.madgag.spongycastle:core:1.58.0.0' + api "com.google.crypto.tink:tink-android:$tink_version" // Graphview cannot be upgraded api "com.jjoe64:graphview:4.0.1" diff --git a/omnipod-dash/build.gradle b/omnipod-dash/build.gradle index 45c11a93bf..bf7d1c99c1 100644 --- a/omnipod-dash/build.gradle +++ b/omnipod-dash/build.gradle @@ -21,7 +21,5 @@ dependencies { implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-rxjava2:$room_version" kapt "androidx.room:room-compiler:$room_version" - implementation 'com.github.guepardoapps:kulid:1.1.2.0' - -} \ No newline at end of file +} diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/OmnipodDashBleManagerImpl.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/OmnipodDashBleManagerImpl.kt index 3dda0461dc..be03f69525 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/OmnipodDashBleManagerImpl.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/OmnipodDashBleManagerImpl.kt @@ -13,12 +13,13 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command. import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.* import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.BleIO import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType -import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.ltk.LTKExchanger import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageIO +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair.LTKExchanger import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.scan.PodScanner import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.status.ConnectionStatus import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.event.PodEvent import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.base.Command +import info.nightscout.androidaps.utils.extensions.toHex import io.reactivex.Observable import org.apache.commons.lang3.NotImplementedException import java.util.concurrent.BlockingQueue @@ -96,8 +97,10 @@ class OmnipodDashBleManagerImpl @Inject constructor(private val context: Context val ltkExchanger = LTKExchanger(aapsLogger, msgIO) emitter.onNext(PodEvent.Pairing) - val ltk = ltkExchanger.negotiateLTKAndNonce() - aapsLogger.info(LTag.PUMPCOMM, "Got LTK and Nonce Prefix: ${ltk}") + val ltk = ltkExchanger.negotiateLTK() + + aapsLogger.info(LTag.PUMPCOMM, "Got LTK: ${ltk.ltk.toHex()}") + emitter.onNext(PodEvent.Connected(PodScanner.POD_ID_NOT_ACTIVATED)) // TODO supply actual pod id emitter.onComplete() @@ -112,7 +115,7 @@ class OmnipodDashBleManagerImpl @Inject constructor(private val context: Context companion object { - private const val CONNECT_TIMEOUT_MS = 5000 + private const val CONNECT_TIMEOUT_MS = 7000 const val CONTROLLER_ID = 4242 // TODO read from preferences or somewhere else. } diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/command/BleCommand.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/command/BleCommand.kt index a51e738df4..e437bd23a9 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/command/BleCommand.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/command/BleCommand.kt @@ -1,5 +1,7 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command +import info.nightscout.androidaps.utils.extensions.toHex + open class BleCommand(val data: ByteArray) { constructor(type: BleCommandType) : this(byteArrayOf(type.value)) @@ -17,6 +19,10 @@ open class BleCommand(val data: ByteArray) { return true } + override fun toString(): String { + return "Raw command: [${data.toHex()}]" + } + override fun hashCode(): Int { return data.contentHashCode() } diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/exceptions/CouldNotParseMessageException.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/exceptions/CouldNotParseMessageException.kt new file mode 100644 index 0000000000..5e10a0bb1e --- /dev/null +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/exceptions/CouldNotParseMessageException.kt @@ -0,0 +1,5 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions + +import info.nightscout.androidaps.utils.extensions.toHex + +class CouldNotParseMessageException(val payload: ByteArray) : Exception("Could not parse message payload: ${payload.toHex()}") \ No newline at end of file diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/exceptions/MessageIOException.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/exceptions/MessageIOException.kt new file mode 100644 index 0000000000..5645cb009b --- /dev/null +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/exceptions/MessageIOException.kt @@ -0,0 +1,6 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions + +class MessageIOException : Exception { + constructor(msg: String) : super(msg) + constructor(cause: Throwable) : super("Caught Exception during Message I/O", cause) +} \ No newline at end of file diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/io/BleIO.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/io/BleIO.kt index a763193b77..3340d1daf1 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/io/BleIO.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/io/BleIO.kt @@ -50,7 +50,6 @@ class BleIO(private val aapsLogger: AAPSLogger, private val chars: Map BleCommandSuccess() - is PayloadJoinerActionReject -> BleCommandFail() - } - bleIO.sendAndConfirmPacket(CharacteristicType.CMD, finalCmd.data) - val fullPayload = joiner.bytes() - return MessagePacket.parse(fullPayload) } } \ No newline at end of file diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/message/MessagePacket.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/message/MessagePacket.kt index d9cd686b4c..6a6d800823 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/message/MessagePacket.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/message/MessagePacket.kt @@ -1,6 +1,7 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.CouldNotParseMessageException import java.nio.ByteBuffer /*** @@ -69,9 +70,56 @@ data class MessagePacket( companion object { private val MAGIC_PATTERN = "TW" // all messages start with this string + private val HEADER_SIZE = 16 fun parse(payload: ByteArray): MessagePacket { - TODO("implement message header parsing") + if (payload.size < HEADER_SIZE) { + throw CouldNotParseMessageException(payload) + } + if (payload.copyOfRange(0, 2).decodeToString() != MAGIC_PATTERN) { + throw CouldNotParseMessageException(payload) + } + val f1 = Flag(payload[2].toInt()) + val sas = f1.get(3) != 0 + val tfs = f1.get(4) != 0 + val version = ((f1.get(0) shl 2) or (f1.get(1) shl 1) or (f1.get(2) shl 0)).toShort() + val eqos = (f1.get(7) or (f1.get(6) shl 1) or (f1.get(5) shl 2)).toShort() + + val f2 = Flag(payload[3].toInt()) + val ack = f2.get(0) != 0 + val priority = f2.get(1) != 0 + val lastMessage = f2.get(2) != 0 + val gateway = f2.get(3) != 0 + val type = MessageType.byValue((f1.get(7) or (f1.get(6) shl 1) or (f1.get(5) shl 2) or (f1.get(4) shl 3)).toByte()) + if (version.toInt() != 0) { + throw CouldNotParseMessageException(payload) + } + val sequenceNumber = payload[4] + val ackNumber = payload[5] + val size = (payload[6].toInt() shl 3) or (payload[7].toUnsignedInt() ushr 5) + if (size + HEADER_SIZE > payload.size) { + throw CouldNotParseMessageException(payload) + } + val payloadEnd = 16 + size + + if (type == MessageType.ENCRYPTED) 8 + else 0 + + return MessagePacket( + type = type, + ack = ack, + eqos = eqos, + priority = priority, + lastMessage = lastMessage, + gateway = gateway, + sas = sas, + tfs = tfs, + version = version, + sequenceNumber = sequenceNumber, + ackNumber = ackNumber, + source = Id(payload.copyOfRange(8, 12)), + destination = Id(payload.copyOfRange(12, 16)), + payload = payload.copyOfRange(16, payloadEnd), + ) } } } @@ -85,8 +133,14 @@ private class Flag(var value: Int = 0) { value = value or mask } - fun get(idx: Byte): Boolean { + fun get(idx: Byte): Int { val mask = 1 shl (7 - idx) - return value and mask != 0 + if (value and mask == 0) { + return 0 + } + return 1 + } -} \ No newline at end of file +} + +internal fun Byte.toUnsignedInt() = this.toInt() and 0xff \ No newline at end of file diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/message/PayloadJoiner.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/message/PayloadJoiner.kt index d2f754e396..2a6d772ac5 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/message/PayloadJoiner.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/message/PayloadJoiner.kt @@ -1,31 +1,109 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io -import java.io.ByteArrayOutputStream +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.CrcMismatchException +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.IncorrectPacketException +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.crc32 +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.BlePacket +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.FirstBlePacket +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.LastBlePacket +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.LastOptionalPlusOneBlePacket +import java.lang.Integer.min +import java.nio.ByteBuffer +import java.util.* -sealed class PayloadJoinerAction +class PayloadJoiner(private val firstPacket: ByteArray) { -class PayloadJoinerActionAccept : PayloadJoinerAction() -class PayloadJoinerActionReject(val idx: Byte) : PayloadJoinerAction() + var oneExtraPacket: Boolean = false + val fullFragments: Int + var crc: Long = 0 + private var expectedIndex = 0 + private val fragments: LinkedList = LinkedList() -class PayloadJoiner { + init { + if (firstPacket.size < FirstBlePacket.HEADER_SIZE_WITH_MIDDLE_PACKETS) { + throw IncorrectPacketException(0, firstPacket) + } + fullFragments = firstPacket[1].toInt() + when { + // Without middle packets + firstPacket.size < FirstBlePacket.HEADER_SIZE_WITHOUT_MIDDLE_PACKETS -> + throw IncorrectPacketException(0, firstPacket) - var oneExtra: Boolean = false + fullFragments == 0 -> { + crc = ByteBuffer.wrap(firstPacket.copyOfRange(2, 6)).int.toUnsignedLong() + val rest = firstPacket[6] + val end = min(rest + FirstBlePacket.HEADER_SIZE_WITHOUT_MIDDLE_PACKETS, BlePacket.MAX_SIZE) + oneExtraPacket = rest + FirstBlePacket.HEADER_SIZE_WITHOUT_MIDDLE_PACKETS > end + if (end > firstPacket.size) { + throw IncorrectPacketException(0, firstPacket) + } + fragments.add(firstPacket.copyOfRange(FirstBlePacket.HEADER_SIZE_WITHOUT_MIDDLE_PACKETS, end)) + } - private val payload = ByteArrayOutputStream() + // With middle packets + firstPacket.size < BlePacket.MAX_SIZE -> + throw IncorrectPacketException(0, firstPacket) - fun start(payload: ByteArray): Int { - TODO("not implemented") + else -> { + fragments.add(firstPacket.copyOfRange(FirstBlePacket.HEADER_SIZE_WITH_MIDDLE_PACKETS, BlePacket.MAX_SIZE)) + } + } } - fun accumulate(payload: ByteArray): PayloadJoinerAction { - TODO("not implemented") + fun accumulate(packet: ByteArray) { + if (packet.size < 3) { // idx, size, at least 1 byte of payload + throw IncorrectPacketException((expectedIndex + 1).toByte(), packet) + } + val idx = packet[0].toInt() + if (idx != expectedIndex + 1) { + throw IncorrectPacketException((expectedIndex + 1).toByte(), packet) + } + expectedIndex++ + when { + idx < fullFragments -> { // this is a middle fragment + if (packet.size < BlePacket.MAX_SIZE) { + throw IncorrectPacketException(idx.toByte(), packet) + } + fragments.add(packet.copyOfRange(1, BlePacket.MAX_SIZE)) + } + + idx == fullFragments -> { // this is the last fragment + if (packet.size < LastBlePacket.HEADER_SIZE) { + throw IncorrectPacketException(idx.toByte(), packet) + } + crc = ByteBuffer.wrap(packet.copyOfRange(2, 6)).int.toUnsignedLong() + val rest = packet[1].toInt() + val end = min(rest + LastBlePacket.HEADER_SIZE, BlePacket.MAX_SIZE) + oneExtraPacket = rest + LastBlePacket.HEADER_SIZE > end + if (packet.size < end) { + throw IncorrectPacketException(idx.toByte(), packet) + } + fragments.add(packet.copyOfRange(LastBlePacket.HEADER_SIZE, packet.size)) + } + + idx > fullFragments -> { // this is the extra fragment + val size = packet[1].toInt() + if (packet.size < LastOptionalPlusOneBlePacket.HEADER_SIZE + size) { + throw IncorrectPacketException(idx.toByte(), packet) + } + + fragments.add(packet.copyOfRange(LastOptionalPlusOneBlePacket.HEADER_SIZE, LastOptionalPlusOneBlePacket.HEADER_SIZE + size)) + } + } } - fun finalize(): PayloadJoinerAction { - TODO("not implemented") + fun finalize(): ByteArray { + val totalLen = fragments.fold(0, { acc, elem -> acc + elem.size }) + val bb = ByteBuffer.allocate(totalLen) + fragments.map { fragment -> bb.put(fragment) } + bb.flip() + val bytes = bb.array() + if (bytes.crc32() != crc) { + throw CrcMismatchException(bytes.crc32(), crc, bytes) + } + return bytes.copyOfRange(0, bytes.size) } - fun bytes(): ByteArray { - TODO("not implemented") - } -} \ No newline at end of file +} + +internal fun Int.toUnsignedLong() = this.toLong() and 0xffffffffL \ No newline at end of file diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/message/PayloadSplitter.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/message/PayloadSplitter.kt index d74032e6fe..bd5e5811db 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/message/PayloadSplitter.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/message/PayloadSplitter.kt @@ -25,6 +25,7 @@ internal class PayloadSplitter(private val payload: ByteArray) { ret.add(LastOptionalPlusOneBlePacket( index = 1, payload = payload.copyOfRange(end, payload.size), + size = (payload.size - end).toByte(), )) } return ret @@ -53,9 +54,10 @@ internal class PayloadSplitter(private val payload: ByteArray) { payload = payload.copyOfRange(middleFragments * MiddleBlePacket.CAPACITY + FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS, middleFragments * MiddleBlePacket.CAPACITY + FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS + end), crc32 = crc32, )) - if (rest > 14) { + if (rest > LastBlePacket.CAPACITY) { ret.add(LastOptionalPlusOneBlePacket( index = (middleFragments + 2).toByte(), + size = (rest - LastBlePacket.CAPACITY).toByte(), payload = payload.copyOfRange(middleFragments * MiddleBlePacket.CAPACITY + FirstBlePacket.CAPACITY_WITH_MIDDLE_PACKETS + LastBlePacket.CAPACITY, payload.size), )) } @@ -63,7 +65,7 @@ internal class PayloadSplitter(private val payload: ByteArray) { } } -private fun ByteArray.crc32(): Long { +internal fun ByteArray.crc32(): Long { val crc = CRC32() crc.update(this) return crc.value diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/message/StringLengthPrefixEncoding.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/message/StringLengthPrefixEncoding.kt index b23e689d9d..7ec6048aed 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/message/StringLengthPrefixEncoding.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/message/StringLengthPrefixEncoding.kt @@ -1,5 +1,7 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.MessageIOException +import info.nightscout.androidaps.utils.extensions.toHex import java.nio.ByteBuffer /*** @@ -9,8 +11,33 @@ class StringLengthPrefixEncoding { companion object { - fun parseKeys(keys: List): List { - TODO("not implemented") + private val LENGTH_BYTES = 2 + + fun parseKeys(keys: Array, payload: ByteArray): Array { + val ret = Array(keys.size, { ByteArray(0) }) + var remaining = payload + for ((index, key) in keys.withIndex()) { + when { + remaining.size < key.length -> + throw MessageIOException("Payload too short: ${payload.toHex()} for key: ${key}") + !(remaining.copyOfRange(0, key.length).decodeToString() == key) -> + throw MessageIOException("Key not found: ${key} in ${payload.toHex()}") + // last key can be empty, no length + index == keys.size - 1 && remaining.size == key.length -> + return ret + + remaining.size < key.length + LENGTH_BYTES -> + throw MessageIOException("Length not found: for ${key} in ${payload.toHex()}") + } + remaining = remaining.copyOfRange(key.length, remaining.size) + val length = (remaining[0].toUnsignedInt() shl 1) or remaining[1].toUnsignedInt() + if (length > remaining.size) { + throw MessageIOException("Payload too short, looking for length ${length} for ${key} in ${payload.toHex()}") + } + ret[index] = remaining.copyOfRange(LENGTH_BYTES, LENGTH_BYTES + length) + remaining = remaining.copyOfRange(LENGTH_BYTES + length, remaining.size) + } + return ret } fun formatKeys(keys: Array, payloads: Array): ByteArray { diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/packet/BlePacket.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/packet/BlePacket.kt index b9bd30a252..eb020c5b28 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/packet/BlePacket.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/packet/BlePacket.kt @@ -7,8 +7,8 @@ sealed class BlePacket { abstract fun asByteArray(): ByteArray companion object { - const val MAX_BLE_PACKET_LEN = 20 - const val MAX_BLE_BUFFER_LEN = MAX_BLE_PACKET_LEN + 1 // we use this as the size allocated for the ByteBuffer + + const val MAX_SIZE = 20 } } @@ -16,7 +16,7 @@ data class FirstBlePacket(val totalFragments: Byte, val payload: ByteArray, val override fun asByteArray(): ByteArray { val bb = ByteBuffer - .allocate(MAX_BLE_BUFFER_LEN) + .allocate(MAX_SIZE) .put(0) // index .put(totalFragments) // # of fragments except FirstBlePacket and LastOptionalPlusOneBlePacket crc32?.let { @@ -26,15 +26,22 @@ data class FirstBlePacket(val totalFragments: Byte, val payload: ByteArray, val bb.put(size) } bb.put(payload) - val ret = ByteArray(bb.position()) + + val pos = bb.position() + val ret = ByteArray(MAX_SIZE) bb.flip() - bb.get(ret) + bb.get(ret, 0, pos) + return ret } companion object { - internal const val CAPACITY_WITHOUT_MIDDLE_PACKETS = 13 // we are using all fields - internal const val CAPACITY_WITH_MIDDLE_PACKETS = 18 // we are not using crc32 or size + + internal const val HEADER_SIZE_WITHOUT_MIDDLE_PACKETS = 7 // we are using all fields + internal const val HEADER_SIZE_WITH_MIDDLE_PACKETS = 2 + + internal const val CAPACITY_WITHOUT_MIDDLE_PACKETS = MAX_SIZE - HEADER_SIZE_WITHOUT_MIDDLE_PACKETS // we are using all fields + internal const val CAPACITY_WITH_MIDDLE_PACKETS = MAX_SIZE - HEADER_SIZE_WITH_MIDDLE_PACKETS // we are not using crc32 or size internal const val CAPACITY_WITH_THE_OPTIONAL_PLUS_ONE_PACKET = 18 } } @@ -46,6 +53,7 @@ data class MiddleBlePacket(val index: Byte, val payload: ByteArray) : BlePacket( } companion object { + internal const val CAPACITY = 19 } } @@ -54,26 +62,34 @@ data class LastBlePacket(val index: Byte, val size: Byte, val payload: ByteArray override fun asByteArray(): ByteArray { val bb = ByteBuffer - .allocate(MAX_BLE_BUFFER_LEN) + .allocate(MAX_SIZE) .put(index) .put(size) .putInt(crc32.toInt()) .put(payload) - val ret = ByteArray(bb.position()) + val pos = bb.position() + val ret = ByteArray(MAX_SIZE) bb.flip() - bb.get(ret) + bb.get(ret, 0, pos) return ret } companion object { - internal const val CAPACITY = 14 + + internal const val HEADER_SIZE = 6 + internal const val CAPACITY = MAX_SIZE - HEADER_SIZE } } -data class LastOptionalPlusOneBlePacket(val index: Byte, val payload: ByteArray) : BlePacket() { +data class LastOptionalPlusOneBlePacket(val index: Byte, val payload: ByteArray, val size: Byte) : BlePacket() { override fun asByteArray(): ByteArray { - return byteArrayOf(index) + payload + return byteArrayOf(index, size) + payload + ByteArray(MAX_SIZE - payload.size - 2) + } + + companion object { + + internal const val HEADER_SIZE = 2 } } 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 new file mode 100644 index 0000000000..cc76bd232f --- /dev/null +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/LTKExchanger.kt @@ -0,0 +1,256 @@ +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.OmnipodDashBleManagerImpl +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.MessagePacket +import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.StringLengthPrefixEncoding +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) { + + 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 controllerId = Id.fromInt(OmnipodDashBleManagerImpl.CONTROLLER_ID) + val nodeId = controllerId.increment() + private var seq: Byte = 1 + private var ltk = ByteArray(CMAC_SIZE) + + init { + val random = SecureRandom() + random.nextBytes(pdmNonce) + } + + fun negotiateLTK(): PairResult { + // send SP1, SP2 + var sp1sp2 = sp1sp2(nodeId.address, sp2()) + msgIO.sendMesssage(sp1sp2.messagePacket) + + seq++ + var sps1 = sps1() + msgIO.sendMesssage(sps1.messagePacket) + // send SPS1 + + // read SPS1 + 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() + msgIO.sendMesssage(sps2.messagePacket) + // read SPS2 + + val podSps2 = msgIO.receiveMessage() + validatePodSps2(podSps2) + + seq++ + // send SP0GP0 + msgIO.sendMesssage(sp0gp0().messagePacket) + // read P0 + + // 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 + // 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 + ) + } + + private fun sp1sp2(sp1: ByteArray, sp2: ByteArray): PairMessage { + val payload = StringLengthPrefixEncoding.formatKeys( + arrayOf(SP1, SP2), + arrayOf(sp1, sp2), + ) + return PairMessage( + sequenceNumber = seq, + source = controllerId, + destination = nodeId, + payload = payload + ) + } + + private fun sps1(): PairMessage { + val payload = StringLengthPrefixEncoding.formatKeys( + arrayOf("SPS1="), + arrayOf(pdmPublic + pdmNonce), + ) + return PairMessage( + sequenceNumber = seq, + source = controllerId, + destination = nodeId, + payload = payload + ) + } + + private fun processSps1FromPod(msg: MessagePacket) { + 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) + } + + private fun sps2(): PairMessage { + val payload = StringLengthPrefixEncoding.formatKeys( + arrayOf(SPS2), + arrayOf(pdmConf), + ) + return PairMessage( + sequenceNumber = seq, + source = controllerId, + destination = nodeId, + payload = payload + ) + } + + private fun validatePodSps2(msg: MessagePacket) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Received SPS2 from pod: ${msg.payload.toHex()}") + + val payload = parseKeys(arrayOf(SPS2), msg.payload)[0] + aapsLogger.debug(LTag.PUMPBTCOMM, "SPS2 payload from pod: ${payload.toHex()}") + + if (payload.size != 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") + } + } + + private fun sp2(): ByteArray { + // This is GetPodStatus command, with page 0 parameter. + // We could replace that in the future with the serialized GetPodStatus() + return GET_POD_STATUS_HEX_COMMAND.hexStringToByteArray() + } + + private fun sp0gp0(): PairMessage { + val payload = SP0GP0.toByteArray() + return PairMessage( + sequenceNumber = seq, + source = controllerId, + destination = nodeId, + payload = payload + ) + } + + private fun validateP0(msg: MessagePacket) { + aapsLogger.debug(LTag.PUMPBTCOMM, "Received P0 from pod: ${msg.payload.toHex()}") + + val payload = parseKeys(arrayOf(P0), msg.payload)[0] + aapsLogger.debug(LTag.PUMPBTCOMM, "P0 payload from pod: ${payload.toHex()}") + if (!payload.contentEquals(UNKNOWN_P0_PAYLOAD)) { + throw MessageIOException("Invalid P0 payload received") + } + } + + 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, "LTK, first key: ${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 val PUBLIC_KEY_SIZE = 32 + private val NONCE_SIZE = 16 + private val CONF_SIZE = 16 + + private 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 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 val SP1 = "SP1=" + private val SP2 = ",SP2=" + private val SPS1 = "SPS1=" + private val SPS2 = "SPS2=" + private val SP0GP0 = "SP0,GP0" + private val P0 = "P0=" + 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) +} \ No newline at end of file diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/ltk/PairMessage.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/PairMessage.kt similarity index 98% rename from omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/ltk/PairMessage.kt rename to omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/PairMessage.kt index af7fea3adb..f36baea723 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/ltk/PairMessage.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/PairMessage.kt @@ -1,4 +1,4 @@ -package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.ltk +package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessagePacket diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/PairResult.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/PairResult.kt new file mode 100644 index 0000000000..2a659e63fd --- /dev/null +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/PairResult.kt @@ -0,0 +1,9 @@ +package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair + +import info.nightscout.androidaps.utils.extensions.toHex + +data class PairResult(val ltk: ByteArray) { + init { + require(ltk.size == 16) { "LTK length must be 16 bytes. Received LTK: ${ltk.toHex()}" } + } +} diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/scan/BleDiscoveredDevice.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/scan/BleDiscoveredDevice.kt index 1b4a2cee2a..bf3d3dd74c 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/scan/BleDiscoveredDevice.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/scan/BleDiscoveredDevice.kt @@ -31,7 +31,6 @@ class BleDiscoveredDevice(val scanResult: ScanResult, private val scanRecord: Sc @Throws(DiscoveredInvalidPodException::class) private fun validatePodId() { - val scanRecord = scanResult.scanRecord val serviceUUIDs = scanRecord.serviceUuids val hexPodId = extractUUID16(serviceUUIDs[3]) + extractUUID16(serviceUUIDs[4]) val podId = hexPodId.toLong(16) diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/util/Functions.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/util/Functions.kt index 96d8fbf667..54ad9b345f 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/util/Functions.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/util/Functions.kt @@ -33,7 +33,7 @@ fun mapProfileToBasalProgram(profile: Profile): BasalProgram { if (previousBasalValue != null) { entries.add( BasalProgram.Segment( - (previousBasalValue!!.timeAsSeconds / 1800).toShort(), + (previousBasalValue.timeAsSeconds / 1800).toShort(), startSlotIndex, (PumpType.Omnipod_Dash.determineCorrectBasalSize(previousBasalValue.value) * 100).roundToInt() )