Merge pull request #18 from 0pen-dash/eap-aka

EAP AKA session establishment
This commit is contained in:
Andrei Vereha 2021-03-07 10:22:42 +01:00 committed by GitHub
commit bef32e70c1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 562 additions and 34 deletions

View file

@ -25,6 +25,10 @@ data class Id(val address: ByteArray) {
return "$asInt/${address.toHex()}" return "$asInt/${address.toHex()}"
} }
fun toLong(): Long {
return ByteBuffer.wrap(address).int.toLong() and 0xffffffffL
}
companion object { companion object {
private val PERIPHERAL_NODE_INDEX = 1 // TODO: understand the meaning of this value. It comes from preferences private val PERIPHERAL_NODE_INDEX = 1 // TODO: understand the meaning of this value. It comes from preferences

View file

@ -16,6 +16,7 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.Chara
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.SessionEstablisher
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
@ -75,7 +76,6 @@ class OmnipodDashBleManagerImpl @Inject constructor(
val discoverer = ServiceDiscoverer(aapsLogger, gatt, bleCommCallbacks) val discoverer = ServiceDiscoverer(aapsLogger, gatt, bleCommCallbacks)
val chars = discoverer.discoverServices() val chars = discoverer.discoverServices()
val bleIO = BleIO(aapsLogger, chars, incomingPackets, gatt, bleCommCallbacks) val bleIO = BleIO(aapsLogger, chars, incomingPackets, gatt, bleCommCallbacks)
aapsLogger.debug(LTag.PUMPBTCOMM, "Saying hello to the pod")
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandHello(CONTROLLER_ID).data) bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandHello(CONTROLLER_ID).data)
bleIO.readyToRead() bleIO.readyToRead()
return bleIO return bleIO
@ -125,13 +125,22 @@ class OmnipodDashBleManagerImpl @Inject constructor(
val msgIO = MessageIO(aapsLogger, bleIO) val msgIO = MessageIO(aapsLogger, bleIO)
val ltkExchanger = LTKExchanger(aapsLogger, msgIO) val ltkExchanger = LTKExchanger(aapsLogger, msgIO)
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()}") aapsLogger.info(LTag.PUMPCOMM, "Got LTK: ${ltk.ltk.toHex()}")
emitter.onNext(PodEvent.Connected(PodScanner.POD_ID_NOT_ACTIVATED)) // TODO supply actual pod id emitter.onNext(PodEvent.EstablishingSession)
val eapAkaExchanger = SessionEstablisher(aapsLogger, msgIO, ltk)
val sessionKeys = eapAkaExchanger.negotiateSessionKeys()
aapsLogger.info(LTag.PUMPCOMM, "CK: ${sessionKeys.ck.toHex()}")
aapsLogger.info(LTag.PUMPCOMM, "noncePrefix: ${sessionKeys.noncePrefix.toHex()}")
aapsLogger.info(LTag.PUMPCOMM, "SQN: ${sessionKeys.sqn.toHex()}")
emitter.onNext(PodEvent.Connected(ltk.podId.toLong())) // TODO supply actual pod id
emitter.onComplete() emitter.onComplete()
} catch (ex: Exception) { } catch (ex: Exception) {

View file

@ -45,18 +45,18 @@ class BleCommCallbacks(
} }
@Throws(InterruptedException::class) @Throws(InterruptedException::class)
fun waitForConnection(timeout_ms: Int) { fun waitForConnection(timeoutMs: Int) {
connected.await(timeout_ms.toLong(), TimeUnit.MILLISECONDS) connected.await(timeoutMs.toLong(), TimeUnit.MILLISECONDS)
} }
@Throws(InterruptedException::class) @Throws(InterruptedException::class)
fun waitForServiceDiscovery(timeout_ms: Int) { fun waitForServiceDiscovery(timeoutMs: Int) {
serviceDiscoveryComplete.await(timeout_ms.toLong(), TimeUnit.MILLISECONDS) serviceDiscoveryComplete.await(timeoutMs.toLong(), TimeUnit.MILLISECONDS)
} }
@Throws(InterruptedException::class, TimeoutException::class, CouldNotConfirmWriteException::class) @Throws(InterruptedException::class, TimeoutException::class, CouldNotConfirmWriteException::class)
fun confirmWrite(expectedPayload: ByteArray, timeout_ms: Int) { fun confirmWrite(expectedPayload: ByteArray, timeoutMs: Int) {
val received: CharacteristicWriteConfirmation = writeQueue.poll(timeout_ms.toLong(), TimeUnit.MILLISECONDS) val received: CharacteristicWriteConfirmation = writeQueue.poll(timeoutMs.toLong(), TimeUnit.MILLISECONDS)
?: throw TimeoutException() ?: throw TimeoutException()
when (received) { when (received) {
@ -116,9 +116,9 @@ class BleCommCallbacks(
} }
@Throws(InterruptedException::class, CouldNotConfirmDescriptorWriteException::class) @Throws(InterruptedException::class, CouldNotConfirmDescriptorWriteException::class)
fun confirmWriteDescriptor(descriptorUUID: String, timeout_ms: Int) { fun confirmWriteDescriptor(descriptorUUID: String, timeoutMs: Int) {
val confirmed: DescriptorWriteConfirmation = descriptorWriteQueue.poll( val confirmed: DescriptorWriteConfirmation = descriptorWriteQueue.poll(
timeout_ms.toLong(), timeoutMs.toLong(),
TimeUnit.MILLISECONDS TimeUnit.MILLISECONDS
) )
?: throw TimeoutException() ?: throw TimeoutException()

View file

@ -0,0 +1,3 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
class SessionEstablishmentException(val msg: String) : Exception(msg)

View file

@ -81,7 +81,15 @@ class BleIO(
* Called before sending a new message. * Called before sending a new message.
* The incoming queues should be empty, so we log when they are not. * The incoming queues should be empty, so we log when they are not.
*/ */
fun flushIncomingQueues() {} fun flushIncomingQueues() {
for (char in CharacteristicType.values()) {
do {
val found = incomingPackets[char]?.poll()?.also {
aapsLogger.warn(LTag.PUMPBTCOMM, "BleIO: ${char.name} queue not empty, flushing: {${it.toHex()}")
}
} while (found != null)
}
}
/** /**
* Enable intentions on the characteristics. * Enable intentions on the characteristics.

View file

@ -15,12 +15,12 @@ data class MessagePacket(
val sequenceNumber: Byte, val sequenceNumber: Byte,
val ack: Boolean = false, val ack: Boolean = false,
val ackNumber: Byte = 0.toByte(), val ackNumber: Byte = 0.toByte(),
val eqos: Short = 0.toShort(), // TODO: understand val eqos: Short = 0.toShort(), // TODO: understand. Seems to be set to 1 for commands
val priority: Boolean = false, val priority: Boolean = false,
val lastMessage: Boolean = false, val lastMessage: Boolean = false,
val gateway: Boolean = false, val gateway: Boolean = false,
val sas: Boolean = false, // TODO: understand val sas: Boolean = true, // TODO: understand, seems to always be true
val tfs: Boolean = false, // TODO: understand val tfs: Boolean = false, // TODO: understand, seems to be false
val version: Short = 0.toShort() val version: Short = 0.toShort()
) { ) {
@ -70,8 +70,8 @@ data class MessagePacket(
companion object { companion object {
private val MAGIC_PATTERN = "TW" // all messages start with this string private const val MAGIC_PATTERN = "TW" // all messages start with this string
private val HEADER_SIZE = 16 private const val HEADER_SIZE = 16
fun parse(payload: ByteArray): MessagePacket { fun parse(payload: ByteArray): MessagePacket {
if (payload.size < HEADER_SIZE) { if (payload.size < HEADER_SIZE) {

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)

View file

@ -39,11 +39,11 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI
fun negotiateLTK(): PairResult { fun negotiateLTK(): PairResult {
// send SP1, SP2 // send SP1, SP2
var sp1sp2 = sp1sp2(nodeId.address, sp2()) val sp1sp2 = sp1sp2(nodeId.address, sp2())
msgIO.sendMesssage(sp1sp2.messagePacket) msgIO.sendMesssage(sp1sp2.messagePacket)
seq++ seq++
var sps1 = sps1() val sps1 = sps1()
msgIO.sendMesssage(sps1.messagePacket) msgIO.sendMesssage(sps1.messagePacket)
// send SPS1 // send SPS1
@ -75,7 +75,9 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI
validateP0(p0) validateP0(p0)
return PairResult( return PairResult(
ltk = ltk ltk = ltk,
podId = nodeId,
seq = seq
) )
} }
@ -180,7 +182,7 @@ internal class LTKExchanger(private val aapsLogger: AAPSLogger, private val msgI
pdmPublic.copyOfRange(pdmPublic.size - 4, pdmPublic.size) + pdmPublic.copyOfRange(pdmPublic.size - 4, pdmPublic.size) +
podNonce.copyOfRange(podNonce.size - 4, podNonce.size) + podNonce.copyOfRange(podNonce.size - 4, podNonce.size) +
pdmNonce.copyOfRange(pdmNonce.size - 4, pdmNonce.size) pdmNonce.copyOfRange(pdmNonce.size - 4, pdmNonce.size)
aapsLogger.debug(LTag.PUMPBTCOMM, "LTK, first key: ${firstKey.toHex()}") aapsLogger.debug(LTag.PUMPBTCOMM, "First key for LTK: ${firstKey.toHex()}")
val intermediateKey = ByteArray(CMAC_SIZE) val intermediateKey = ByteArray(CMAC_SIZE)
aesCmac(firstKey, curveLTK, intermediateKey) aesCmac(firstKey, curveLTK, intermediateKey)

View file

@ -16,5 +16,5 @@ data class PairMessage(
payload = payload, payload = payload,
sequenceNumber = sequenceNumber, sequenceNumber = sequenceNumber,
sas = true // TODO: understand why this is true for PairMessages sas = true // TODO: understand why this is true for PairMessages
), )
) )

View file

@ -1,8 +1,9 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair 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.utils.extensions.toHex import info.nightscout.androidaps.utils.extensions.toHex
data class PairResult(val ltk: ByteArray) { data class PairResult(val ltk: ByteArray, val podId: Id, val seq: 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

@ -21,7 +21,8 @@ class BleDiscoveredDevice(val scanResult: ScanResult, private val scanRecord: Sc
throw DiscoveredInvalidPodException( throw DiscoveredInvalidPodException(
"The first exposed service UUID should be 4024, got " + extractUUID16( "The first exposed service UUID should be 4024, got " + extractUUID16(
serviceUuids[0] serviceUuids[0]
), serviceUuids ),
serviceUuids
) )
} }
// TODO understand what is serviceUUIDs[1]. 0x2470. Alarms? // TODO understand what is serviceUUIDs[1]. 0x2470. Alarms?
@ -42,7 +43,7 @@ class BleDiscoveredDevice(val scanResult: ScanResult, private val scanRecord: Sc
val podId = hexPodId.toLong(16) val podId = hexPodId.toLong(16)
if (this.podId != podId) { if (this.podId != podId) {
throw DiscoveredInvalidPodException( throw DiscoveredInvalidPodException(
"This is not the POD we are looking for. " + this.podId + " found: " + this.podId, "This is not the POD we are looking for: ${this.podId} . Found: $podId/$hexPodId",
serviceUUIDs serviceUUIDs
) )
} }

View file

@ -42,7 +42,7 @@ class PodScanner(private val logger: AAPSLogger, private val bluetoothAdapter: B
companion object { companion object {
const val SCAN_FOR_SERVICE_UUID = "00004024-0000-1000-8000-00805F9B34FB" const val SCAN_FOR_SERVICE_UUID = "00004024-0000-1000-8000-00805F9B34FB"
const val POD_ID_NOT_ACTIVATED = 0xFFFFFFFFL const val POD_ID_NOT_ACTIVATED = 0xFFFFFFFEL
private const val SCAN_DURATION_MS = 5000 private const val SCAN_DURATION_MS = 5000
} }
} }

View file

@ -0,0 +1,134 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session
import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.MessageIOException
import info.nightscout.androidaps.utils.extensions.toHex
import java.util.*
enum class EapAkaAttributeType(val type: Byte) {
AT_RAND(1),
AT_AUTN(2),
AT_RES(3),
AT_CUSTOM_IV(126);
companion object {
fun byValue(value: Byte): EapAkaAttributeType =
EapAkaAttributeType.values().firstOrNull { it.type == value }
?: throw IllegalArgumentException("Unknown EAP-AKA attribute type: $value")
}
}
sealed class EapAkaAttribute {
abstract fun toByteArray(): ByteArray
companion object {
const val SIZE_MULTIPLIER = 4 // The length for EAP-AKA attributes is a multiple of 4
fun parseAttributes(aapsLogger: AAPSLogger, payload: ByteArray): List<EapAkaAttribute> {
var tail = payload
val ret = LinkedList<EapAkaAttribute>()
while (tail.size > 0) {
if (tail.size < 2) {
throw MessageIOException("Could not parse EAP attributes: ${payload.toHex()}")
}
val size = SIZE_MULTIPLIER * tail[1].toInt()
if (tail.size < size) {
throw MessageIOException("Could not parse EAP attributes: ${payload.toHex()}")
}
val type = EapAkaAttributeType.byValue(tail[0])
when (type) {
EapAkaAttributeType.AT_RES ->
ret.add(EapAkaAttributeRes.parse(tail.copyOfRange(2, size)))
EapAkaAttributeType.AT_CUSTOM_IV ->
ret.add(EapAkaAttributeCustomIV.parse(tail.copyOfRange(2, size)))
else ->
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)
}
return ret
}
}
}
data class EapAkaAttributeRand(val payload: ByteArray) : EapAkaAttribute() {
init {
require(payload.size == 16) { "AT_RAND payload size has to be 16 bytes. Payload: ${payload.toHex()}" }
}
override fun toByteArray(): ByteArray {
return byteArrayOf(EapAkaAttributeType.AT_RAND.type, SIZE, 0, 0) + payload
}
companion object {
private const val SIZE = (20 / SIZE_MULTIPLIER).toByte() // type, size, 2 reserved bytes, payload=16
}
}
data class EapAkaAttributeAutn(val payload: ByteArray) : EapAkaAttribute() {
init {
require(payload.size == 16) { "AT_AUTN payload size has to be 16 bytes. Payload: ${payload.toHex()}" }
}
override fun toByteArray(): ByteArray {
return byteArrayOf(EapAkaAttributeType.AT_AUTN.type, SIZE, 0, 0) + payload
}
companion object {
private const val SIZE = (20 / SIZE_MULTIPLIER).toByte() // type, size, 2 reserved bytes, payload=16
}
}
data class EapAkaAttributeRes(val payload: ByteArray) : EapAkaAttribute() {
init {
require(payload.size == 8) { "AT_RES payload size has to be 8 bytes. Payload: ${payload.toHex()}" }
}
override fun toByteArray(): ByteArray {
return byteArrayOf(EapAkaAttributeType.AT_RES.type, SIZE, 0, PAYLOAD_SIZE_BITS) + payload
}
companion object {
fun parse(payload: ByteArray): EapAkaAttributeRes {
if (payload.size < 2 + 8) {
throw MessageIOException("Could not parse RES attribute: ${payload.toHex()}")
}
return EapAkaAttributeRes(payload.copyOfRange(2, 2 + 8))
}
private const val SIZE = (12 / SIZE_MULTIPLIER).toByte() // type, size, len in bits=2, payload=8
private const val PAYLOAD_SIZE_BITS = 64.toByte() // type, size, 2 reserved bytes, payload
}
}
data class EapAkaAttributeCustomIV(val payload: ByteArray) : EapAkaAttribute() {
init {
require(payload.size == 4) { "CUSTOM_IV payload size has to be 4 bytes. Payload: ${payload.toHex()}" }
}
override fun toByteArray(): ByteArray {
return byteArrayOf(EapAkaAttributeType.AT_CUSTOM_IV.type, SIZE, 0, 0) + payload
}
companion object {
fun parse(payload: ByteArray): EapAkaAttributeCustomIV {
if (payload.size < 2 + 4) {
throw MessageIOException("Could not parse CUSTOM_IV attribute: ${payload.toHex()}")
}
return EapAkaAttributeCustomIV(payload.copyOfRange(2, 2 + 4))
}
private const val SIZE = (8 / SIZE_MULTIPLIER).toByte() // type, size, 2 reserved bytes, payload=4
}
}

View file

@ -0,0 +1,89 @@
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.exceptions.MessageIOException
import info.nightscout.androidaps.utils.extensions.toHex
import okio.ByteString.Companion.toByteString
import java.nio.ByteBuffer
enum class EapCode(val code: Byte) {
REQUEST(1),
RESPONSE(2),
SUCCESS(3),
FAILURE(4);
companion object {
fun byValue(value: Byte): EapCode =
EapCode.values().firstOrNull { it.code == value }
?: throw IllegalArgumentException("Unknown EAP-AKA attribute type: $value")
}
}
data class EapMessage(
val code: EapCode,
val identifier: Byte,
val attributes: Array<EapAkaAttribute>,
) {
fun toByteArray(): ByteArray {
val serializedAttributes = attributes.flatMap { it.toByteArray().asIterable() }
val joinedAttributes = serializedAttributes.toTypedArray().toByteArray()
val attrSize = joinedAttributes.size
if (attrSize == 0) {
return byteArrayOf(code.code, identifier, 0, 4)
}
val totalSize = HEADER_SIZE + attrSize
var bb = ByteBuffer
.allocate(totalSize)
.put(code.code)
.put(identifier)
.put(((totalSize ushr 1) and 0XFF).toByte())
.put((totalSize and 0XFF).toByte())
.put(AKA_PACKET_TYPE)
.put(SUBTYPE_AKA_CHALLENGE)
.put(byteArrayOf(0, 0))
.put(joinedAttributes)
val ret = bb.array()
return ret.copyOfRange(0, ret.size)
}
companion object {
private const val HEADER_SIZE = 8
private const val SUBTYPE_AKA_CHALLENGE = 1.toByte()
private const val AKA_PACKET_TYPE = 0x17.toByte()
fun parse(aapsLogger: AAPSLogger, payload: ByteArray): EapMessage {
if (payload.size < 4) {
throw MessageIOException("Invalid eap payload: ${payload.toHex()}")
}
val totalSize = (payload[2].toInt() shl 1) or payload[3].toInt()
if (totalSize > payload.size) {
throw MessageIOException("Invalid eap payload. Too short: ${payload.toHex()}")
}
if (payload.size == 4) { // SUCCESS/FAILURE
return EapMessage(
code = EapCode.byValue(payload[0]),
identifier = payload[1],
attributes = arrayOf()
)
}
if (totalSize > 0 && payload[4] != AKA_PACKET_TYPE) {
throw MessageIOException("Invalid eap payload. Expected AKA packet type: ${payload.toHex()}")
}
val attributesPayload = payload.copyOfRange(8, totalSize)
aapsLogger.debug(LTag.PUMPBTCOMM, "EAP attributes: ${attributesPayload.toByteString()}")
return EapMessage(
code = EapCode.byValue(payload[0]),
identifier = payload[1],
attributes = EapAkaAttribute.parseAttributes(aapsLogger, attributesPayload).toTypedArray()
)
}
}
}

View file

@ -0,0 +1,103 @@
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.utils.extensions.toHex
import org.spongycastle.util.encoders.Hex
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
class Milenage(
private val aapsLogger: AAPSLogger,
private val k: ByteArray,
val sqn: ByteArray,
private val randParam: ByteArray? = null
) {
init {
require(k.size == KEY_SIZE) { "Milenage key has to be $KEY_SIZE bytes long. Received: ${k.toHex()}" }
require(sqn.size == SQN) { "Milenage SQN has to be $SQN long. Received: ${sqn.toHex()}" }
}
private val secretKeySpec = SecretKeySpec(k, "AES")
private val cipher: Cipher = Cipher.getInstance("AES/ECB/NoPadding")
init {
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec)
}
val rand = randParam ?: ByteArray(KEY_SIZE)
init {
if (randParam == null) {
val random = SecureRandom()
random.nextBytes(rand)
}
}
private val opc = cipher.doFinal(MILENAGE_OP) xor MILENAGE_OP
private val randOpcEncrypted = cipher.doFinal(rand xor opc)
private val randOpcEncryptedXorOpc = randOpcEncrypted xor opc
private val resAkInput = randOpcEncryptedXorOpc.copyOfRange(0, KEY_SIZE)
init {
resAkInput[15] = (resAkInput[15].toInt() xor 1).toByte()
}
private val resAk = cipher.doFinal(resAkInput) xor opc
val res = resAk.copyOfRange(8, 16)
private val ak = resAk.copyOfRange(0, 6)
private val ckInput = ByteArray(KEY_SIZE)
init {
for (i in 0..15) {
ckInput[(i + 12) % 16] = randOpcEncryptedXorOpc[i]
}
ckInput[15] = (ckInput[15].toInt() xor 2).toByte()
}
val ck = cipher.doFinal(ckInput) xor opc
private val sqnAmf = sqn + MILENAGE_AMF + sqn + MILENAGE_AMF
private val sqnAmfXorOpc = sqnAmf xor opc
private val macAInput = ByteArray(KEY_SIZE)
init {
for (i in 0..15) {
macAInput[(i + 8) % 16] = sqnAmfXorOpc[i]
}
}
private val macAFull = cipher.doFinal(macAInput xor randOpcEncrypted) xor opc
private val macA = macAFull.copyOfRange(0, 8)
val autn = (ak xor sqn) + MILENAGE_AMF + macA
init {
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage K: ${k.toHex()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage RAND: ${rand.toHex()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage SQN: ${sqn.toHex()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage CK: ${ck.toHex()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage AUTN: ${autn.toHex()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage RES: ${res.toHex()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage AK: ${ak.toHex()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage OPC: ${opc.toHex()}")
aapsLogger.debug(LTag.PUMPBTCOMM, "Milenage MacA: ${macA.toHex()}")
}
companion object {
private val MILENAGE_OP = Hex.decode("cdc202d5123e20f62b6d676ac72cb318")
private val MILENAGE_AMF = Hex.decode("b9b9")
private const val KEY_SIZE = 16
private const val SQN = 6
}
}
private infix fun ByteArray.xor(other: ByteArray): ByteArray {
val out = ByteArray(size)
for (i in indices) out[i] = (this[i].toInt() xor other[i].toInt()).toByte()
return out
}

View file

@ -0,0 +1,115 @@
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.OmnipodDashBleManagerImpl
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.MessagePacket
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.utils.extensions.toHex
import org.spongycastle.util.encoders.Hex
import java.security.SecureRandom
class SessionEstablisher(private val aapsLogger: AAPSLogger, private val msgIO: MessageIO, private val ltk: PairResult) {
var seq = ltk.seq
private val controllerIV = ByteArray(IV_SIZE)
private var nodeIV = ByteArray(IV_SIZE)
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, sqn)
init {
aapsLogger.debug(LTag.PUMPBTCOMM, "Starting EAP-AKA")
val random = SecureRandom()
random.nextBytes(controllerIV)
}
fun negotiateSessionKeys(): SessionKeys {
// send EAP-AKA challenge
seq++ //TODO: get from pod state. This only works for activating a new pod
var challenge = eapAkaChallenge()
msgIO.sendMesssage(challenge)
val challengeResponse = msgIO.receiveMessage()
processChallengeResponse(challengeResponse) //TODO: what do we have to answer if challenge response does not validate?
seq++
var success = eapSuccess()
msgIO.sendMesssage(success)
return SessionKeys(
ck=milenage.ck,
noncePrefix = controllerIV + nodeIV,
sqn=sqn
)
}
private fun eapAkaChallenge(): MessagePacket {
val attributes = arrayOf(
EapAkaAttributeAutn(milenage.autn),
EapAkaAttributeRand(milenage.rand),
EapAkaAttributeCustomIV(controllerIV)
)
val eapMsg = EapMessage(
code = EapCode.REQUEST,
identifier = 42, // TODO: find what value we need here, it's probably random
attributes = attributes
)
return MessagePacket(
type = MessageType.SESSION_ESTABLISHMENT,
sequenceNumber = seq,
source = controllerId,
destination = ltk.podId,
payload = eapMsg.toByteArray()
)
}
private fun processChallengeResponse(challengeResponse: MessagePacket) {
// TODO verify that identifier matches identifer from the Challenge
val eapMsg = EapMessage.parse(aapsLogger, challengeResponse.payload)
if (eapMsg.attributes.size != 2) {
aapsLogger.debug(LTag.PUMPBTCOMM, "EAP-AKA: got RES message: $eapMsg")
throw SessionEstablishmentException("Expecting two attributes, got: ${eapMsg.attributes.size}")
}
for (attr in eapMsg.attributes) {
when (attr) {
is EapAkaAttributeRes ->
if (!milenage.res.contentEquals(attr.payload)) {
throw SessionEstablishmentException("RES missmatch. Expected: ${milenage.res.toHex()} Actual: ${attr.payload.toHex()} ")
}
is EapAkaAttributeCustomIV ->
nodeIV = attr.payload.copyOfRange(0, IV_SIZE)
else ->
throw SessionEstablishmentException("Unknown attribute received: $attr")
}
}
}
private fun eapSuccess(): MessagePacket {
val eapMsg = EapMessage(
code = EapCode.SUCCESS,
attributes = arrayOf(),
identifier = 44 // TODO: find what value we need here
)
return MessagePacket(
type = MessageType.SESSION_ESTABLISHMENT,
sequenceNumber = seq,
source = controllerId,
destination = ltk.podId,
payload = eapMsg.toByteArray()
)
}
companion object {
private const val IV_SIZE = 4
}
}

View file

@ -0,0 +1,9 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session
data class SessionKeys(val ck: ByteArray, val noncePrefix: ByteArray, val sqn: ByteArray) {
init {
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

@ -47,7 +47,7 @@ class DashHistory @Inject constructor(
val id = ULID.random() val id = ULID.random()
when { when {
commandType == SET_BOLUS && bolusRecord == null -> commandType == SET_BOLUS && bolusRecord == null ->
return Single.error(IllegalArgumentException("bolusRecord missing on SET_BOLUS")) return Single.error(IllegalArgumentException("bolusRecord missing on SET_BOLUS"))
commandType == SET_TEMPORARY_BASAL && tempBasalRecord == null -> commandType == SET_TEMPORARY_BASAL && tempBasalRecord == null ->
return Single.error(IllegalArgumentException("tempBasalRecord missing on SET_TEMPORARY_BASAL")) return Single.error(IllegalArgumentException("tempBasalRecord missing on SET_TEMPORARY_BASAL"))

View file

@ -0,0 +1,50 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.session
import info.nightscout.androidaps.logging.AAPSLoggerTest
import info.nightscout.androidaps.utils.extensions.toHex
import org.junit.Assert
import org.junit.Test
import org.spongycastle.util.encoders.Hex
class MilenageTest {
@Test fun testMilenage() {
val aapsLogger = AAPSLoggerTest()
val m = Milenage(
aapsLogger,
Hex.decode("c0772899720972a314f557de66d571dd"),
byteArrayOf(0, 0, 0, 0, 0, 2),
Hex.decode("c2cd1248451103bd77a6c7ef88c441ba")
)
Assert.assertEquals(m.res.toHex(), "a40bc6d13861447e")
Assert.assertEquals(m.ck.toHex(), "55799fd26664cbf6e476525e2dee52c6")
Assert.assertEquals(m.autn.toHex(), "00c55c78e8d3b9b9e935860a7259f6c0")
}
@Test fun testMilenage2() {
val aapsLogger = AAPSLoggerTest()
val m = Milenage(
aapsLogger,
Hex.decode("78411ccad0fd0fb6f381a47fb3335ecb"),
byteArrayOf(0, 0, 0, 0, 0, 2), // 1 + 1
Hex.decode("4fc01ac1a94376ae3e052339c07d9e1f")
)
Assert.assertEquals(m.res.toHex(), "ec549e00fa668a19")
Assert.assertEquals(m.ck.toHex(), "ee3dac761fe358a9f476cc5ee81aa3e9")
Assert.assertEquals(m.autn.toHex(), "a3e7a71430c8b9b95245b33b3bd679c4")
}
@Test fun testMilenageIncrementedSQN() {
val aapsLogger = AAPSLoggerTest()
val m = Milenage(
aapsLogger,
Hex.decode("c0772899720972a314f557de66d571dd"),
// byteArrayOf(0,0,0,0,0x01,0x5d), this is in logs. SQN has to be incremented.
byteArrayOf(0, 0, 0, 0, 0x01, 0x5e),
Hex.decode("d71cc44820e5419f42c62ae97c035988")
)
Assert.assertEquals(m.res.toHex(), "5f807a379a5c5d30")
Assert.assertEquals(m.ck.toHex(), "8dd4b3ceb849a01766e37f9d86045c39")
Assert.assertEquals(m.autn.toHex(), "0e0264d056fcb9b9752227365a090955")
}
}