dash ble: implement sessions and sending commands

Tested with the fake pod:
```
DEBU[0185] got command. CRC: 82b2. Type: 7 :: GET_VERSION
DEBU[0185] got command. CRC: 0003. Type: 3 :: SET_UNIQUE_ID
DEBU[0186] got command. CRC: 019a. Type: 19 :: PROGRAM_ALERTS
DEBU[0186] got command. CRC: 0385. Type: 19 :: PROGRAM_ALERTS
DEBU[0187] got command. CRC: 81f3. Type: 1a :: PROGRAM_INSULIN
DEBU[0187] got command. CRC: 8178. Type: e :: GET_STATUS
```
This commit is contained in:
Andrei Vereha 2021-03-06 11:02:21 +01:00
parent 5647007190
commit 5211d4ddc6
22 changed files with 270 additions and 103 deletions

View file

@ -7,22 +7,24 @@ import android.bluetooth.BluetoothProfile
import android.content.Context import android.content.Context
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.callbacks.BleCommCallbacks import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.BleCommCallbacks
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command.BleCommandHello import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command.BleCommandHello
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.endecrypt.EnDecrypt
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.* 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.BleIO
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageIO 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.pair.LTKExchanger
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.scan.PodScanner import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.scan.PodScanner
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session.EapSqn
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session.Session
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session.SessionEstablisher import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session.SessionEstablisher
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session.SessionKeys
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.status.ConnectionStatus 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.event.PodEvent
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.base.Command import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.base.Command
import info.nightscout.androidaps.utils.extensions.toHex import info.nightscout.androidaps.utils.extensions.toHex
import io.reactivex.Observable import io.reactivex.Observable
import org.apache.commons.lang3.NotImplementedException
import java.util.concurrent.BlockingQueue import java.util.concurrent.BlockingQueue
import java.util.concurrent.LinkedBlockingDeque import java.util.concurrent.LinkedBlockingDeque
import java.util.concurrent.TimeoutException import java.util.concurrent.TimeoutException
@ -38,6 +40,8 @@ class OmnipodDashBleManagerImpl @Inject constructor(
private val bluetoothManager: BluetoothManager = private val bluetoothManager: BluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
private val bluetoothAdapter: BluetoothAdapter = bluetoothManager.adapter private val bluetoothAdapter: BluetoothAdapter = bluetoothManager.adapter
private var sessionKeys: SessionKeys? = null
private var msgIO: MessageIO? = null
@Throws( @Throws(
FailedToConnectException::class, FailedToConnectException::class,
@ -60,12 +64,8 @@ class OmnipodDashBleManagerImpl @Inject constructor(
) )
val bleCommCallbacks = BleCommCallbacks(aapsLogger, incomingPackets) val bleCommCallbacks = BleCommCallbacks(aapsLogger, incomingPackets)
aapsLogger.debug(LTag.PUMPBTCOMM, "Connecting to $podAddress") aapsLogger.debug(LTag.PUMPBTCOMM, "Connecting to $podAddress")
var autoConnect = true val autoConnect = false // TODO: check what to use here
if (BuildConfig.DEBUG) {
autoConnect = false
// TODO: remove this in the future
// it's easier to start testing from scratch on each run.
}
val gatt = podDevice.connectGatt(context, autoConnect, bleCommCallbacks, BluetoothDevice.TRANSPORT_LE) val gatt = podDevice.connectGatt(context, autoConnect, bleCommCallbacks, BluetoothDevice.TRANSPORT_LE)
bleCommCallbacks.waitForConnection(CONNECT_TIMEOUT_MS) bleCommCallbacks.waitForConnection(CONNECT_TIMEOUT_MS)
val connectionState = bluetoothManager.getConnectionState(podDevice, BluetoothProfile.GATT) val connectionState = bluetoothManager.getConnectionState(podDevice, BluetoothProfile.GATT)
@ -81,9 +81,39 @@ class OmnipodDashBleManagerImpl @Inject constructor(
return bleIO return bleIO
} }
override fun sendCommand(cmd: Command): Observable<PodEvent> { override fun sendCommand(cmd: Command): Observable<PodEvent> = Observable.create { emitter ->
// TODO try {
return Observable.error(NotImplementedException("sendCommand is not yet implemented")) val keys = sessionKeys
val mIO = msgIO
if (keys == null || mIO == null) {
//TODO handle reconnects
throw Exception("Not connected")
}
emitter.onNext(PodEvent.CommandSending(cmd))
// TODO switch to RX
emitter.onNext(PodEvent.CommandSent(cmd))
val enDecrypt = EnDecrypt(
aapsLogger,
keys.nonce,
keys.ck,
)
val session = Session(
aapsLogger = aapsLogger,
msgIO = mIO,
myId = Id.fromInt(CONTROLLER_ID),
podId = Id.fromInt(CONTROLLER_ID).increment(),
sessionKeys = keys,
enDecrypt = enDecrypt
)
val response = session.sendCommand(cmd)
emitter.onNext(PodEvent.ResponseReceived(response))
emitter.onComplete()
} catch (ex: Exception) {
emitter.tryOnError(ex)
}
} }
override fun getStatus(): ConnectionStatus { override fun getStatus(): ConnectionStatus {
@ -107,7 +137,6 @@ class OmnipodDashBleManagerImpl @Inject constructor(
// emit PodEvent.AlreadyConnected, complete the observable and return from this method // emit PodEvent.AlreadyConnected, complete the observable and return from this method
try { try {
// TODO: this is wrong and I know it
aapsLogger.info(LTag.PUMPBTCOMM, "starting new pod activation") aapsLogger.info(LTag.PUMPBTCOMM, "starting new pod activation")
val podScanner = PodScanner(aapsLogger, bluetoothAdapter) val podScanner = PodScanner(aapsLogger, bluetoothAdapter)
@ -123,24 +152,30 @@ class OmnipodDashBleManagerImpl @Inject constructor(
val bleIO = connect(podAddress) val bleIO = connect(podAddress)
emitter.onNext(PodEvent.BluetoothConnected(podAddress)) emitter.onNext(PodEvent.BluetoothConnected(podAddress))
val msgIO = MessageIO(aapsLogger, bleIO) val mIO = MessageIO(aapsLogger, bleIO)
val ltkExchanger = LTKExchanger(aapsLogger, msgIO) val myId = Id.fromInt(CONTROLLER_ID)
val podId = myId.increment()
val ltkExchanger = LTKExchanger(aapsLogger, mIO)
emitter.onNext(PodEvent.Pairing) emitter.onNext(PodEvent.Pairing)
val ltk = ltkExchanger.negotiateLTK() val ltk = ltkExchanger.negotiateLTK()
aapsLogger.info(LTag.PUMPCOMM, "Got LTK: ${ltk.ltk.toHex()}")
emitter.onNext(PodEvent.EstablishingSession) emitter.onNext(PodEvent.EstablishingSession)
val eapAkaExchanger = SessionEstablisher(aapsLogger, msgIO, ltk) val eapSqn = EapSqn(1)
val sessionKeys = eapAkaExchanger.negotiateSessionKeys() aapsLogger.info(LTag.PUMPCOMM, "Got LTK: ${ltk.ltk.toHex()}")
aapsLogger.info(LTag.PUMPCOMM, "CK: ${sessionKeys.ck.toHex()}") val eapAkaExchanger = SessionEstablisher(aapsLogger, mIO, ltk, eapSqn)
aapsLogger.info(LTag.PUMPCOMM, "noncePrefix: ${sessionKeys.noncePrefix.toHex()}") val keys = eapAkaExchanger.negotiateSessionKeys()
aapsLogger.info(LTag.PUMPCOMM, "SQN: ${sessionKeys.sqn.toHex()}") aapsLogger.info(LTag.PUMPCOMM, "CK: ${keys.ck.toHex()}")
aapsLogger.info(LTag.PUMPCOMM, "msgSequenceNumber: ${keys.msgSequenceNumber}")
aapsLogger.info(LTag.PUMPCOMM, "Nonce: ${keys.nonce}")
emitter.onNext(PodEvent.Connected(ltk.podId.toLong())) // TODO supply actual pod id sessionKeys = keys
msgIO = mIO
emitter.onNext(PodEvent.Connected(ltk.podId.toLong()))
emitter.onComplete() emitter.onComplete()
} catch (ex: Exception) { } catch (ex: Exception) {

View file

@ -30,7 +30,7 @@ class BleCommCallbacks(
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
super.onConnectionStateChange(gatt, status, newState) super.onConnectionStateChange(gatt, status, newState)
aapsLogger.debug(LTag.PUMPBTCOMM, "OnConnectionStateChange discovered with status/state$status/$newState") aapsLogger.debug(LTag.PUMPBTCOMM, "OnConnectionStateChange with status/state: $status/$newState")
if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) { if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) {
connected.countDown() connected.countDown()
} }

View file

@ -2,6 +2,15 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command
import info.nightscout.androidaps.utils.extensions.toHex import info.nightscout.androidaps.utils.extensions.toHex
class BleCommandRTS : BleCommand(BleCommandType.RTS)
class BleCommandCTS : BleCommand(BleCommandType.CTS)
class BleCommandAbort : BleCommand(BleCommandType.ABORT)
class BleCommandSuccess : BleCommand(BleCommandType.SUCCESS)
class BleCommandFail : BleCommand(BleCommandType.FAIL)
open class BleCommand(val data: ByteArray) { open class BleCommand(val data: ByteArray) {
constructor(type: BleCommandType) : this(byteArrayOf(type.value)) constructor(type: BleCommandType) : this(byteArrayOf(type.value))
@ -26,14 +35,4 @@ open class BleCommand(val data: ByteArray) {
override fun hashCode(): Int { override fun hashCode(): Int {
return data.contentHashCode() return data.contentHashCode()
} }
} }
class BleCommandRTS : BleCommand(BleCommandType.RTS)
class BleCommandCTS : BleCommand(BleCommandType.CTS)
class BleCommandAbort : BleCommand(BleCommandType.ABORT)
class BleCommandSuccess : BleCommand(BleCommandType.SUCCESS)
class BleCommandFail : BleCommand(BleCommandType.FAIL)

View file

@ -1,24 +1,16 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.endecrypt
import java.nio.ByteBuffer import java.nio.ByteBuffer
class CryptSequence(var sqn: Long) { class CryptSequence(var sqn: Long) {
fun incrementForEapAka():ByteArray { fun incrementForEnDecrypt(fromPdmToPod: Boolean): ByteArray {
sqn++
return ByteBuffer.allocate(8)
.putLong(sqn)
.array()
.copyOfRange(2, 8)
}
fun incrementForEnDecrypt(podReceiving: Boolean):ByteArray{
sqn++ sqn++
val ret = ByteBuffer.allocate(8) val ret = ByteBuffer.allocate(8)
.putLong(sqn) .putLong(sqn)
.array() .array()
.copyOfRange(3, 8) .copyOfRange(3, 8)
if (podReceiving) { if (fromPdmToPod) {
ret[0] = (ret[0].toInt() and 127).toByte() ret[0] = (ret[0].toInt() and 127).toByte()
} else { } else {
ret[0] = (ret[0].toInt() or 128).toByte() ret[0] = (ret[0].toInt() or 128).toByte()

View file

@ -18,7 +18,7 @@ class EnDecrypt(private val aapsLogger: AAPSLogger, private val nonce: Nonce, pr
val payload = msg.payload val payload = msg.payload
val header = msg.asByteArray().copyOfRange(0, 16) val header = msg.asByteArray().copyOfRange(0, 16)
val n = nonce.increment() val n = nonce.increment(false)
aapsLogger.debug(LTag.PUMPBTCOMM, "Decrypt header ${header.toHex()} payload: ${payload.toHex()}") aapsLogger.debug(LTag.PUMPBTCOMM, "Decrypt header ${header.toHex()} payload: ${payload.toHex()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "Decrypt NONCE ${n.toHex()}") aapsLogger.debug(LTag.PUMPBTCOMM, "Decrypt NONCE ${n.toHex()}")
cipher.init( cipher.init(
@ -38,7 +38,7 @@ class EnDecrypt(private val aapsLogger: AAPSLogger, private val nonce: Nonce, pr
val payload = headerMessage.payload val payload = headerMessage.payload
val header = headerMessage.asByteArray(true).copyOfRange(0, 16) val header = headerMessage.asByteArray(true).copyOfRange(0, 16)
val n = nonce.increment() val n = nonce.increment(true)
aapsLogger.debug(LTag.PUMPBTCOMM, "Encrypt header ${header.toHex()} payload: ${payload.toHex()}") aapsLogger.debug(LTag.PUMPBTCOMM, "Encrypt header ${header.toHex()} payload: ${payload.toHex()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "Encrypt NONCE ${n.toHex()}") aapsLogger.debug(LTag.PUMPBTCOMM, "Encrypt NONCE ${n.toHex()}")
val encryptedPayload = ByteArray(payload.size + MAC_SIZE) val encryptedPayload = ByteArray(payload.size + MAC_SIZE)

View file

@ -0,0 +1,23 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.endecrypt
import java.nio.ByteBuffer
data class Nonce(val prefix: ByteArray, var sqn: Long) {
init {
require(prefix.size == 8) { "Nonce prefix should be 8 bytes long" }
}
fun increment(podReceiving: Boolean): ByteArray {
sqn++
val ret = ByteBuffer.allocate(8)
.putLong(sqn)
.array()
.copyOfRange(3, 8)
if (podReceiving) {
ret[0] = (ret[0].toInt() and 127).toByte()
} else {
ret[0] = (ret[0].toInt() or 128).toByte()
}
return prefix + ret
}
}

View file

@ -1,6 +1,5 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
open class FailedToConnectException : Exception { open class FailedToConnectException : Exception {
constructor() : super() constructor(message: String?) : super("Failed to connect: ${message ?: ""}")
constructor(message: String?) : super(message)
} }

View file

@ -123,6 +123,6 @@ class BleIO(
companion object { companion object {
private const val DEFAULT_IO_TIMEOUT_MS = 1000 private const val DEFAULT_IO_TIMEOUT_MS = 10000
} }
} }

View file

@ -12,7 +12,7 @@ import info.nightscout.androidaps.utils.extensions.toHex
class MessageIO(private val aapsLogger: AAPSLogger, private val bleIO: BleIO) { class MessageIO(private val aapsLogger: AAPSLogger, private val bleIO: BleIO) {
fun sendMesssage(msg: MessagePacket) { fun sendMessage(msg: MessagePacket) {
bleIO.flushIncomingQueues() bleIO.flushIncomingQueues()
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandRTS().data) bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandRTS().data)
val expectCTS = bleIO.receivePacket(CharacteristicType.CMD) val expectCTS = bleIO.receivePacket(CharacteristicType.CMD)
@ -33,7 +33,6 @@ class MessageIO(private val aapsLogger: AAPSLogger, private val bleIO: BleIO) {
throw UnexpectedCommandException(BleCommand(expectSuccess)) throw UnexpectedCommandException(BleCommand(expectSuccess))
} }
// TODO: handle NACKS/FAILS/etc // TODO: handle NACKS/FAILS/etc
bleIO.flushIncomingQueues()
} }
fun receiveMessage(): MessagePacket { fun receiveMessage(): MessagePacket {

View file

@ -24,7 +24,7 @@ data class MessagePacket(
val version: Short = 0.toShort() val version: Short = 0.toShort()
) { ) {
fun asByteArray(): ByteArray { fun asByteArray(forEncryption: Boolean = false): ByteArray {
val bb = ByteBuffer.allocate(16 + payload.size) val bb = ByteBuffer.allocate(16 + payload.size)
bb.put(MAGIC_PATTERN.toByteArray()) bb.put(MAGIC_PATTERN.toByteArray())
@ -52,9 +52,10 @@ data class MessagePacket(
bb.put(f2.value.toByte()) bb.put(f2.value.toByte())
bb.put(this.sequenceNumber) bb.put(this.sequenceNumber)
bb.put(this.ackNumber) bb.put(this.ackNumber)
val size = payload.size -
bb.put((this.payload.size ushr 3).toByte()) if (type == MessageType.ENCRYPTED && !forEncryption) 8 else 0
bb.put((this.payload.size shl 5).toByte()) bb.put((size ushr 3).toByte())
bb.put((size shl 5).toByte())
bb.put(this.source.address) bb.put(this.source.address)
bb.put(this.destination.address) bb.put(this.destination.address)
@ -103,7 +104,7 @@ data class MessagePacket(
throw CouldNotParseMessageException(payload) throw CouldNotParseMessageException(payload)
} }
val payloadEnd = 16 + size + val payloadEnd = 16 + size +
if (type == MessageType.ENCRYPTED) 8 if (type == MessageType.ENCRYPTED) 8 // TAG
else 0 else 0
return MessagePacket( return MessagePacket(

View file

@ -29,7 +29,7 @@ class PayloadJoiner(private val firstPacket: ByteArray) {
firstPacket.size < FirstBlePacket.HEADER_SIZE_WITHOUT_MIDDLE_PACKETS -> firstPacket.size < FirstBlePacket.HEADER_SIZE_WITHOUT_MIDDLE_PACKETS ->
throw IncorrectPacketException(0, firstPacket) throw IncorrectPacketException(0, firstPacket)
fullFragments == 0 -> { fullFragments == 0 -> {
crc = ByteBuffer.wrap(firstPacket.copyOfRange(2, 6)).int.toUnsignedLong() crc = ByteBuffer.wrap(firstPacket.copyOfRange(2, 6)).int.toUnsignedLong()
val rest = firstPacket[6] val rest = firstPacket[6]
val end = min(rest + FirstBlePacket.HEADER_SIZE_WITHOUT_MIDDLE_PACKETS, BlePacket.MAX_SIZE) val end = min(rest + FirstBlePacket.HEADER_SIZE_WITHOUT_MIDDLE_PACKETS, BlePacket.MAX_SIZE)
@ -41,10 +41,10 @@ class PayloadJoiner(private val firstPacket: ByteArray) {
} }
// With middle packets // With middle packets
firstPacket.size < BlePacket.MAX_SIZE -> firstPacket.size < BlePacket.MAX_SIZE ->
throw IncorrectPacketException(0, firstPacket) throw IncorrectPacketException(0, firstPacket)
else -> { else -> {
fragments.add( fragments.add(
firstPacket.copyOfRange( firstPacket.copyOfRange(
FirstBlePacket.HEADER_SIZE_WITH_MIDDLE_PACKETS, FirstBlePacket.HEADER_SIZE_WITH_MIDDLE_PACKETS,
@ -65,7 +65,7 @@ class PayloadJoiner(private val firstPacket: ByteArray) {
} }
expectedIndex++ expectedIndex++
when { when {
idx < fullFragments -> { // this is a middle fragment idx < fullFragments -> { // this is a middle fragment
if (packet.size < BlePacket.MAX_SIZE) { if (packet.size < BlePacket.MAX_SIZE) {
throw IncorrectPacketException(idx.toByte(), packet) throw IncorrectPacketException(idx.toByte(), packet)
} }
@ -86,7 +86,7 @@ class PayloadJoiner(private val firstPacket: ByteArray) {
fragments.add(packet.copyOfRange(LastBlePacket.HEADER_SIZE, packet.size)) fragments.add(packet.copyOfRange(LastBlePacket.HEADER_SIZE, packet.size))
} }
idx > fullFragments -> { // this is the extra fragment idx > fullFragments -> { // this is the extra fragment
val size = packet[1].toInt() val size = packet[1].toInt()
if (packet.size < LastOptionalPlusOneBlePacket.HEADER_SIZE + size) { if (packet.size < LastOptionalPlusOneBlePacket.HEADER_SIZE + size) {
throw IncorrectPacketException(idx.toByte(), packet) throw IncorrectPacketException(idx.toByte(), packet)

View file

@ -18,15 +18,15 @@ class StringLengthPrefixEncoding {
var remaining = payload var remaining = payload
for ((index, key) in keys.withIndex()) { for ((index, key) in keys.withIndex()) {
when { when {
remaining.size < key.length -> remaining.size < key.length ->
throw MessageIOException("Payload too short: ${payload.toHex()} for key: $key") throw MessageIOException("Payload too short: ${payload.toHex()} for key: $key")
!(remaining.copyOfRange(0, key.length).decodeToString() == key) -> !(remaining.copyOfRange(0, key.length).decodeToString() == key) ->
throw MessageIOException("Key not found: $key in ${payload.toHex()}") throw MessageIOException("Key not found: $key in ${payload.toHex()}")
// last key can be empty, no length // last key can be empty, no length
index == keys.size - 1 && remaining.size == key.length -> index == keys.size - 1 && remaining.size == key.length ->
return ret return ret
remaining.size < key.length + LENGTH_BYTES -> remaining.size < key.length + LENGTH_BYTES ->
throw MessageIOException("Length not found: for $key in ${payload.toHex()}") throw MessageIOException("Length not found: for $key in ${payload.toHex()}")
} }
remaining = remaining.copyOfRange(key.length, remaining.size) remaining = remaining.copyOfRange(key.length, remaining.size)
@ -43,14 +43,17 @@ class StringLengthPrefixEncoding {
fun formatKeys(keys: Array<String>, payloads: Array<ByteArray>): ByteArray { fun formatKeys(keys: Array<String>, payloads: Array<ByteArray>): ByteArray {
val payloadTotalSize = payloads.fold(0) { acc, i -> acc + i.size } val payloadTotalSize = payloads.fold(0) { acc, i -> acc + i.size }
val keyTotalSize = keys.fold(0) { acc, i -> acc + i.length } val keyTotalSize = keys.fold(0) { acc, i -> acc + i.length }
val zeros = payloads.fold(0) { acc, i -> acc + if (i.size == 0) 1 else 0 }
val bb = ByteBuffer.allocate(2 * keys.size + keyTotalSize + payloadTotalSize) val bb = ByteBuffer.allocate(2 * (keys.size - zeros) + keyTotalSize + payloadTotalSize)
for (idx in keys.indices) { for (idx in keys.indices) {
val k = keys[idx] val k = keys[idx]
val payload = payloads[idx] val payload = payloads[idx]
bb.put(k.toByteArray()) bb.put(k.toByteArray())
bb.putShort(payload.size.toShort()) if (payload.size > 0) {
bb.put(payload) bb.putShort(payload.size.toShort())
bb.put(payload)
}
} }
val ret = ByteArray(bb.position()) val ret = ByteArray(bb.position())

View file

@ -40,11 +40,11 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI
fun negotiateLTK(): PairResult { fun negotiateLTK(): PairResult {
// send SP1, SP2 // send SP1, SP2
val sp1sp2 = sp1sp2(nodeId.address, sp2()) val sp1sp2 = sp1sp2(nodeId.address, sp2())
msgIO.sendMesssage(sp1sp2.messagePacket) msgIO.sendMessage(sp1sp2.messagePacket)
seq++ seq++
val sps1 = sps1() val sps1 = sps1()
msgIO.sendMesssage(sps1.messagePacket) msgIO.sendMessage(sps1.messagePacket)
// send SPS1 // send SPS1
// read SPS1 // read SPS1
@ -55,7 +55,7 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI
seq++ seq++
// send SPS2 // send SPS2
val sps2 = sps2() val sps2 = sps2()
msgIO.sendMesssage(sps2.messagePacket) msgIO.sendMessage(sps2.messagePacket)
// read SPS2 // read SPS2
val podSps2 = msgIO.receiveMessage() val podSps2 = msgIO.receiveMessage()
@ -63,7 +63,7 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI
seq++ seq++
// send SP0GP0 // send SP0GP0
msgIO.sendMesssage(sp0gp0().messagePacket) msgIO.sendMessage(sp0gp0().messagePacket)
// read P0 // read P0
// TODO: failing to read or validate p0 will lead to undefined state // TODO: failing to read or validate p0 will lead to undefined state
@ -77,7 +77,7 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI
return PairResult( return PairResult(
ltk = ltk, ltk = ltk,
podId = nodeId, podId = nodeId,
seq = seq msgSeq = seq,
) )
} }

View file

@ -3,7 +3,7 @@ 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.Id
import info.nightscout.androidaps.utils.extensions.toHex import info.nightscout.androidaps.utils.extensions.toHex
data class PairResult(val ltk: ByteArray, val podId: Id, val seq: Byte) { data class PairResult(val ltk: ByteArray, val podId: Id, val msgSeq: Byte) {
init { init {
require(ltk.size == 16) { "LTK length must be 16 bytes. Received LTK: ${ltk.toHex()}" } require(ltk.size == 16) { "LTK length must be 16 bytes. Received LTK: ${ltk.toHex()}" }
} }

View file

@ -44,7 +44,7 @@ sealed class EapAkaAttribute {
ret.add(EapAkaAttributeRes.parse(tail.copyOfRange(2, size))) ret.add(EapAkaAttributeRes.parse(tail.copyOfRange(2, size)))
EapAkaAttributeType.AT_CUSTOM_IV -> EapAkaAttributeType.AT_CUSTOM_IV ->
ret.add(EapAkaAttributeCustomIV.parse(tail.copyOfRange(2, size))) ret.add(EapAkaAttributeCustomIV.parse(tail.copyOfRange(2, size)))
else -> else ->
throw MessageIOException("Could not parse EAP attributes: ${payload.toHex()}. Expecting only AT_RES or CUSTOM_IV attribute types from the POD") throw MessageIOException("Could not parse EAP attributes: ${payload.toHex()}. Expecting only AT_RES or CUSTOM_IV attribute types from the POD")
} }
tail = tail.copyOfRange(size, tail.size) tail = tail.copyOfRange(size, tail.size)

View file

@ -1,4 +1,18 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session
class EapSqn { import java.nio.ByteBuffer
/***
* Eap-Aka start session sequence.
* Incremented for each new session
*/
class EapSqn(var sqn: Long) {
fun increment(): ByteArray {
sqn++
return ByteBuffer.allocate(8)
.putLong(sqn)
.array()
.copyOfRange(2, 8)
}
} }

View file

@ -0,0 +1,98 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session
import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.Id
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.endecrypt.EnDecrypt
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.MessageType
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.plugins.pump.omnipod.dash.driver.pod.command.base.Command
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.NakResponse
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.response.Response
import info.nightscout.androidaps.utils.extensions.toHex
class Session(
private val aapsLogger: AAPSLogger,
private val msgIO: MessageIO,
private val myId: Id,
private val podId: Id,
val sessionKeys: SessionKeys,
val enDecrypt: EnDecrypt
) {
/**
* Used for commands:
* -> command with retries
* <- response, ACK TODO: retries?
* -> ACK
*/
fun sendCommand(cmd: Command): Response {
sessionKeys.msgSequenceNumber++
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending command: ${cmd.encoded.toHex()}")
val msg = getCmdMessage(cmd)
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending command(wrapped): ${msg.payload.toHex()}")
msgIO.sendMessage(msg)
val responseMsg = msgIO.receiveMessage()
val decrypted = enDecrypt.decrypt(responseMsg)
aapsLogger.debug(LTag.PUMPBTCOMM, "Received response: $decrypted")
val response = parseResponse(decrypted)
sessionKeys.msgSequenceNumber++
val ack = getAck(responseMsg)
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending ACK: ${ack.payload.toHex()}")
msgIO.sendMessage(ack)
return response
}
private fun parseResponse(decrypted: MessagePacket): Response {
val payload = parseKeys(arrayOf(RESPONSE_PREFIX), decrypted.payload)[0]
return NakResponse(payload)
}
private fun getAck(response: MessagePacket): MessagePacket {
val msg = MessagePacket(
type = MessageType.ENCRYPTED,
sequenceNumber = sessionKeys.msgSequenceNumber,
source = myId,
destination = podId,
payload = ByteArray(0),
eqos = 1,
ack = true,
ackNumber = response.sequenceNumber.inc(),
)
return enDecrypt.encrypt((msg))
}
private fun getCmdMessage(cmd: Command): MessagePacket {
val wrapped = StringLengthPrefixEncoding.formatKeys(
arrayOf(COMMAND_PREFIX, COMMAND_SUFFIX),
arrayOf(cmd.encoded, ByteArray(0))
)
aapsLogger.debug(LTag.PUMPBTCOMM, "Sending command: ${wrapped.toHex()}")
val msg = MessagePacket(
type = MessageType.ENCRYPTED,
sequenceNumber = sessionKeys.msgSequenceNumber,
source = myId,
destination = podId,
payload = wrapped,
eqos = 1
)
return enDecrypt.encrypt(msg)
}
companion object {
private const val COMMAND_PREFIX = "S0.0="
private const val COMMAND_SUFFIX = ",G0.0"
private const val RESPONSE_PREFIX = "0.0="
}
}

View file

@ -4,25 +4,29 @@ 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.driver.comm.Id 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.OmnipodDashBleManagerImpl
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.endecrypt.Nonce
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.SessionEstablishmentException import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.SessionEstablishmentException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageIO 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.MessagePacket
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageType import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair.PairResult import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair.PairResult
import info.nightscout.androidaps.utils.extensions.toHex import info.nightscout.androidaps.utils.extensions.toHex
import org.spongycastle.util.encoders.Hex
import java.security.SecureRandom import java.security.SecureRandom
class SessionEstablisher(private val aapsLogger: AAPSLogger, private val msgIO: MessageIO, private val ltk: PairResult) { class SessionEstablisher(
private val aapsLogger: AAPSLogger,
private val msgIO: MessageIO,
private val ltk: PairResult,
private val eapSqn: EapSqn
) {
var seq = ltk.seq var sequenceNumber = ltk.msgSeq
private val controllerIV = ByteArray(IV_SIZE) private val controllerIV = ByteArray(IV_SIZE)
private var nodeIV = ByteArray(IV_SIZE) private var nodeIV = ByteArray(IV_SIZE)
private val controllerId = Id.fromInt(OmnipodDashBleManagerImpl.CONTROLLER_ID) private val controllerId = Id.fromInt(OmnipodDashBleManagerImpl.CONTROLLER_ID)
private val sqn = byteArrayOf(0, 0, 0, 0, 0, 2) private val milenage = Milenage(aapsLogger, ltk.ltk, eapSqn.increment())
private val milenage = Milenage(aapsLogger, ltk.ltk, sqn)
init { init {
aapsLogger.debug(LTag.PUMPBTCOMM, "Starting EAP-AKA") aapsLogger.debug(LTag.PUMPBTCOMM, "Starting EAP-AKA")
@ -32,21 +36,24 @@ class SessionEstablisher(private val aapsLogger: AAPSLogger, private val msgIO:
fun negotiateSessionKeys(): SessionKeys { fun negotiateSessionKeys(): SessionKeys {
// send EAP-AKA challenge // send EAP-AKA challenge
seq++ //TODO: get from pod state. This only works for activating a new pod sequenceNumber++ //TODO: get from pod state. This only works for activating a new pod
var challenge = eapAkaChallenge() var challenge = eapAkaChallenge()
msgIO.sendMesssage(challenge) msgIO.sendMessage(challenge)
val challengeResponse = msgIO.receiveMessage() val challengeResponse = msgIO.receiveMessage()
processChallengeResponse(challengeResponse) //TODO: what do we have to answer if challenge response does not validate? processChallengeResponse(challengeResponse) //TODO: what do we have to answer if challenge response does not validate?
seq++ sequenceNumber++
var success = eapSuccess() var success = eapSuccess()
msgIO.sendMesssage(success) msgIO.sendMessage(success)
return SessionKeys( return SessionKeys(
ck=milenage.ck, ck = milenage.ck,
noncePrefix = controllerIV + nodeIV, nonce = Nonce(
sqn=sqn prefix = controllerIV + nodeIV,
sqn = 0
),
msgSequenceNumber = sequenceNumber
) )
} }
@ -64,7 +71,7 @@ class SessionEstablisher(private val aapsLogger: AAPSLogger, private val msgIO:
) )
return MessagePacket( return MessagePacket(
type = MessageType.SESSION_ESTABLISHMENT, type = MessageType.SESSION_ESTABLISHMENT,
sequenceNumber = seq, sequenceNumber = sequenceNumber,
source = controllerId, source = controllerId,
destination = ltk.podId, destination = ltk.podId,
payload = eapMsg.toByteArray() payload = eapMsg.toByteArray()
@ -86,7 +93,7 @@ class SessionEstablisher(private val aapsLogger: AAPSLogger, private val msgIO:
} }
is EapAkaAttributeCustomIV -> is EapAkaAttributeCustomIV ->
nodeIV = attr.payload.copyOfRange(0, IV_SIZE) nodeIV = attr.payload.copyOfRange(0, IV_SIZE)
else -> else ->
throw SessionEstablishmentException("Unknown attribute received: $attr") throw SessionEstablishmentException("Unknown attribute received: $attr")
} }
} }
@ -101,7 +108,7 @@ class SessionEstablisher(private val aapsLogger: AAPSLogger, private val msgIO:
return MessagePacket( return MessagePacket(
type = MessageType.SESSION_ESTABLISHMENT, type = MessageType.SESSION_ESTABLISHMENT,
sequenceNumber = seq, sequenceNumber = sequenceNumber,
source = controllerId, source = controllerId,
destination = ltk.podId, destination = ltk.podId,
payload = eapMsg.toByteArray() payload = eapMsg.toByteArray()

View file

@ -1,9 +1,9 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session
data class SessionKeys(val ck: ByteArray, val noncePrefix: ByteArray, val sqn: ByteArray) { import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.endecrypt.Nonce
data class SessionKeys(val ck: ByteArray, val nonce: Nonce, var msgSequenceNumber: Byte) {
init { init {
require(ck.size == 16) { "CK has to be 16 bytes long" } require(ck.size == 16) { "CK has to be 16 bytes long" }
require(noncePrefix.size == 8) { "noncePrefix has to be 8 bytes long" }
require(sqn.size == 6) { "SQN has to be 6 bytes long" }
} }
} }

View file

@ -18,5 +18,5 @@ sealed class PodEvent {
/* Message exchange events */ /* Message exchange events */
class CommandSending(val command: Command) : PodEvent() class CommandSending(val command: Command) : PodEvent()
class CommandSent(val command: Command) : PodEvent() class CommandSent(val command: Command) : PodEvent()
class ResponseReceived(val response: Response) : PodEvent() class ResponseReceived(val response:Response) : PodEvent()
} }

View file

@ -324,13 +324,8 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
) )
} }
podInfoBinding.podActiveAlerts.text = if (podStateManager.activeAlerts!!.size > 0) { podInfoBinding.podActiveAlerts.text = PLACEHOLDER
// TODO
// TextUtils.join(System.lineSeparator(), omnipodUtil.getTranslatedActiveAlerts(podStateManager))
"TODO"
} else {
PLACEHOLDER
}
} }
if (errors.size == 0) { if (errors.size == 0) {

View file

@ -6,16 +6,18 @@ import info.nightscout.androidaps.data.PumpEnactResult
import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.plugins.pump.omnipod.common.R import info.nightscout.androidaps.plugins.pump.omnipod.common.R
import info.nightscout.androidaps.plugins.pump.omnipod.common.ui.wizard.deactivation.viewmodel.action.DeactivatePodViewModel import info.nightscout.androidaps.plugins.pump.omnipod.common.ui.wizard.deactivation.viewmodel.action.DeactivatePodViewModel
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.OmnipodDashManager
import io.reactivex.Single import io.reactivex.Single
import javax.inject.Inject import javax.inject.Inject
class DashDeactivatePodViewModel @Inject constructor( class DashDeactivatePodViewModel @Inject constructor(
private val omnipodManager: OmnipodDashManager,
injector: HasAndroidInjector, injector: HasAndroidInjector,
logger: AAPSLogger logger: AAPSLogger
) : DeactivatePodViewModel(injector, logger) { ) : DeactivatePodViewModel(injector, logger) {
override fun doExecuteAction(): Single<PumpEnactResult> = Single.just( override fun doExecuteAction(): Single<PumpEnactResult> = Single.just(
PumpEnactResult(injector).success(false).comment("TODO") PumpEnactResult(injector).success(true).comment("TODO")
) // TODO ) // TODO
override fun discardPod() { override fun discardPod() {