Merge branch 'dash' into bart/create-responses

This commit is contained in:
Bart Sopers 2021-03-31 11:21:59 +02:00
commit 12dad6662c
12 changed files with 107 additions and 36 deletions

2
.gitignore vendored
View file

@ -10,8 +10,6 @@ build/
.idea/* .idea/*
!.idea/codeStyles/ !.idea/codeStyles/
full/ full/
debug/
release/
app/com.crashlytics.settings.json app/com.crashlytics.settings.json
app/session_analytics.tap app/session_analytics.tap
.project .project

View file

@ -55,6 +55,7 @@ buildscript {
// in the individual module build.gradle files // in the individual module build.gradle files
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version"
classpath 'com.hiya:jacoco-android:0.2' classpath 'com.hiya:jacoco-android:0.2'
} }
} }

View file

@ -1,6 +1,7 @@
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-allopen'
apply plugin: 'com.hiya.jacoco-android' apply plugin: 'com.hiya.jacoco-android'
apply plugin: "io.gitlab.arturbosch.detekt" // TODO move to `subprojects` section in global build.gradle apply plugin: "io.gitlab.arturbosch.detekt" // TODO move to `subprojects` section in global build.gradle
apply plugin: "org.jlleitschuh.gradle.ktlint" // TODO move to `subprojects` section in global build.gradle apply plugin: "org.jlleitschuh.gradle.ktlint" // TODO move to `subprojects` section in global build.gradle
@ -16,6 +17,10 @@ android {
} }
} }
allOpen {
annotation 'info.nightscout.androidaps.plugins.pump.omnipod.dash.annotations.OpenClass'
}
detekt { // TODO move to `subprojects` section in global build.gradle detekt { // TODO move to `subprojects` section in global build.gradle
toolVersion = "1.15.0-RC2" toolVersion = "1.15.0-RC2"
config = files("./detekt-config.yml") // TODO move to global space and use "../detekt-config.yml" config = files("./detekt-config.yml") // TODO move to global space and use "../detekt-config.yml"

View file

@ -0,0 +1,15 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.annotations
/**
* This is the actual annotation that makes the class open. Don't use it directly, only through [OpenForTesting]
* which has a NOOP replacement in production.
*/
@Target(AnnotationTarget.ANNOTATION_CLASS)
annotation class OpenClass
/**
* Annotate a class with [OpenForTesting] if it should be extendable for testing.
*/
@OpenClass
@Target(AnnotationTarget.CLASS)
annotation class OpenForTesting

View file

@ -43,9 +43,9 @@ class OmnipodDashManagerImpl @Inject constructor(
private val observePodReadyForActivationPart2: Observable<PodEvent> private val observePodReadyForActivationPart2: Observable<PodEvent>
get() = Observable.defer { get() = Observable.defer {
if (podStateManager.activationProgress.isAtLeast(ActivationProgress.PHASE_1_COMPLETED) && podStateManager.activationProgress.isBefore( if (podStateManager.activationProgress.isAtLeast(ActivationProgress.PHASE_1_COMPLETED) &&
ActivationProgress.COMPLETED podStateManager.activationProgress.isBefore(ActivationProgress.COMPLETED)
)) { ) {
Observable.empty() Observable.empty()
} else { } else {
// TODO introduce specialized Exception // TODO introduce specialized Exception

View file

@ -1,39 +1,35 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair
import com.google.crypto.tink.subtle.X25519
import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.pump.omnipod.dash.BuildConfig import info.nightscout.androidaps.plugins.pump.omnipod.dash.BuildConfig
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.MessageIOException import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.MessageIOException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.util.RandomByteGenerator
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.util.X25519KeyGenerator
import info.nightscout.androidaps.utils.extensions.toHex import info.nightscout.androidaps.utils.extensions.toHex
import org.spongycastle.crypto.engines.AESEngine import org.spongycastle.crypto.engines.AESEngine
import org.spongycastle.crypto.macs.CMac import org.spongycastle.crypto.macs.CMac
import org.spongycastle.crypto.params.KeyParameter import org.spongycastle.crypto.params.KeyParameter
import java.security.SecureRandom
class KeyExchange( class KeyExchange(
private val aapsLogger: AAPSLogger, private val aapsLogger: AAPSLogger,
var pdmPrivate: ByteArray = X25519.generatePrivateKey(), private val x25519: X25519KeyGenerator,
val pdmNonce: ByteArray = ByteArray(NONCE_SIZE) randomByteGenerator: RandomByteGenerator
) { ) {
val pdmPublic = X25519.publicFromPrivate(pdmPrivate)
val pdmNonce: ByteArray = randomByteGenerator.nextBytes(NONCE_SIZE)
val pdmPrivate: ByteArray = x25519.generatePrivateKey()
val pdmPublic = x25519.publicFromPrivate(pdmPrivate)
var podPublic = ByteArray(PUBLIC_KEY_SIZE) var podPublic = ByteArray(PUBLIC_KEY_SIZE)
var podNonce = ByteArray(NONCE_SIZE) private set
var podNonce: ByteArray = ByteArray(NONCE_SIZE)
val podConf = ByteArray(CMAC_SIZE) val podConf = ByteArray(CMAC_SIZE)
val pdmConf = ByteArray(CMAC_SIZE) val pdmConf = ByteArray(CMAC_SIZE)
var ltk = ByteArray(CMAC_SIZE) var ltk = ByteArray(CMAC_SIZE)
init {
if (pdmNonce.all { it == 0.toByte() }) {
// pdmNonce is in the constructor for tests
val random = SecureRandom()
random.nextBytes(pdmNonce)
}
}
fun updatePodPublicData(payload: ByteArray) { fun updatePodPublicData(payload: ByteArray) {
if (payload.size != PUBLIC_KEY_SIZE + NONCE_SIZE) { if (payload.size != PUBLIC_KEY_SIZE + NONCE_SIZE) {
throw MessageIOException("Invalid payload size") throw MessageIOException("Invalid payload size")
@ -54,7 +50,7 @@ class KeyExchange(
} }
private fun generateKeys() { private fun generateKeys() {
val curveLTK = X25519.computeSharedSecret(pdmPrivate, podPublic) val curveLTK = x25519.computeSharedSecret(pdmPrivate, podPublic)
val firstKey = podPublic.copyOfRange(podPublic.size - 4, podPublic.size) + val firstKey = podPublic.copyOfRange(podPublic.size - 4, podPublic.size) +
pdmPublic.copyOfRange(pdmPublic.size - 4, pdmPublic.size) + pdmPublic.copyOfRange(pdmPublic.size - 4, pdmPublic.size) +

View file

@ -8,6 +8,8 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.
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.StringLengthPrefixEncoding 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.comm.message.StringLengthPrefixEncoding.Companion.parseKeys
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.util.RandomByteGenerator
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.util.X25519KeyGenerator
import info.nightscout.androidaps.utils.extensions.hexStringToByteArray import info.nightscout.androidaps.utils.extensions.hexStringToByteArray
import info.nightscout.androidaps.utils.extensions.toHex import info.nightscout.androidaps.utils.extensions.toHex
@ -19,7 +21,7 @@ internal class LTKExchanger(
val podAddress: Id val podAddress: Id
) { ) {
private val keyExchange = KeyExchange(aapsLogger) private val keyExchange = KeyExchange(aapsLogger, X25519KeyGenerator(), RandomByteGenerator())
private var seq: Byte = 1 private var seq: Byte = 1
fun negotiateLTK(): PairResult { fun negotiateLTK(): PairResult {

View file

@ -0,0 +1,11 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.util
import info.nightscout.androidaps.plugins.pump.omnipod.dash.annotations.OpenForTesting
import java.security.SecureRandom
@OpenForTesting
class RandomByteGenerator {
private val secureRandom = SecureRandom()
fun nextBytes(length: Int): ByteArray = ByteArray(length).also(secureRandom::nextBytes)
}

View file

@ -0,0 +1,13 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.util
import com.google.crypto.tink.subtle.X25519
import info.nightscout.androidaps.plugins.pump.omnipod.dash.annotations.OpenForTesting
@OpenForTesting
class X25519KeyGenerator {
fun generatePrivateKey(): ByteArray = X25519.generatePrivateKey()
fun publicFromPrivate(privateKey: ByteArray): ByteArray = X25519.publicFromPrivate(privateKey)
fun computeSharedSecret(privateKey: ByteArray, publicKey: ByteArray): ByteArray =
X25519.computeSharedSecret(privateKey, publicKey)
}

View file

@ -467,16 +467,15 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
private fun updateRefreshStatusButton() { private fun updateRefreshStatusButton() {
buttonBinding.buttonRefreshStatus.isEnabled = buttonBinding.buttonRefreshStatus.isEnabled =
podStateManager.isUniqueIdSet && podStateManager.activationProgress.isAtLeast( podStateManager.isUniqueIdSet &&
ActivationProgress.PHASE_1_COMPLETED podStateManager.activationProgress.isAtLeast(ActivationProgress.PHASE_1_COMPLETED) &&
) &&
isQueueEmpty() isQueueEmpty()
} }
private fun updateResumeDeliveryButton() { private fun updateResumeDeliveryButton() {
if (podStateManager.isPodRunning && (podStateManager.isSuspended || commandQueue.isCustomCommandInQueue( if (podStateManager.isPodRunning &&
CommandResumeDelivery::class.java (podStateManager.isSuspended || commandQueue.isCustomCommandInQueue(CommandResumeDelivery::class.java))
))) { ) {
buttonBinding.buttonResumeDelivery.visibility = View.VISIBLE buttonBinding.buttonResumeDelivery.visibility = View.VISIBLE
buttonBinding.buttonResumeDelivery.isEnabled = isQueueEmpty() buttonBinding.buttonResumeDelivery.isEnabled = isQueueEmpty()
} else { } else {
@ -485,9 +484,12 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
} }
private fun updateSilenceAlertsButton() { private fun updateSilenceAlertsButton() {
if (isAutomaticallySilenceAlertsEnabled() && podStateManager.isPodRunning && (podStateManager.activeAlerts!!.size > 0 || commandQueue.isCustomCommandInQueue( if (isAutomaticallySilenceAlertsEnabled() && podStateManager.isPodRunning &&
CommandAcknowledgeAlerts::class.java (
))) { podStateManager.activeAlerts!!.size > 0 ||
commandQueue.isCustomCommandInQueue(CommandAcknowledgeAlerts::class.java)
)
) {
buttonBinding.buttonSilenceAlerts.visibility = View.VISIBLE buttonBinding.buttonSilenceAlerts.visibility = View.VISIBLE
buttonBinding.buttonSilenceAlerts.isEnabled = isQueueEmpty() buttonBinding.buttonSilenceAlerts.isEnabled = isQueueEmpty()
} else { } else {
@ -497,9 +499,10 @@ class OmnipodDashOverviewFragment : DaggerFragment() {
private fun updateSuspendDeliveryButton() { private fun updateSuspendDeliveryButton() {
// If the Pod is currently suspended, we show the Resume delivery button instead. // If the Pod is currently suspended, we show the Resume delivery button instead.
if (isSuspendDeliveryButtonEnabled() && podStateManager.isPodRunning && (!podStateManager.isSuspended || commandQueue.isCustomCommandInQueue( if (isSuspendDeliveryButtonEnabled() &&
CommandSuspendDelivery::class.java podStateManager.isPodRunning &&
))) { (!podStateManager.isSuspended || commandQueue.isCustomCommandInQueue(CommandSuspendDelivery::class.java))
) {
buttonBinding.buttonSuspendDelivery.visibility = View.VISIBLE buttonBinding.buttonSuspendDelivery.visibility = View.VISIBLE
buttonBinding.buttonSuspendDelivery.isEnabled = buttonBinding.buttonSuspendDelivery.isEnabled =
podStateManager.isPodRunning && !podStateManager.isSuspended && isQueueEmpty() podStateManager.isPodRunning && !podStateManager.isSuspended && isQueueEmpty()

View file

@ -0,0 +1,8 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.annotations
/**
* Annotate a class with [OpenForTesting] if it should be extendable for testing.
* In production the class remains final.
*/
@Target(AnnotationTarget.CLASS)
annotation class OpenForTesting

View file

@ -1,19 +1,38 @@
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.logging.AAPSLoggerTest import info.nightscout.androidaps.logging.AAPSLoggerTest
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.util.RandomByteGenerator
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.util.X25519KeyGenerator
import info.nightscout.androidaps.utils.extensions.toHex import info.nightscout.androidaps.utils.extensions.toHex
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito
import org.mockito.Mockito.mock
import org.mockito.Mockito.spy
import org.spongycastle.util.encoders.Hex import org.spongycastle.util.encoders.Hex
class KeyExchangeTest { class KeyExchangeTest {
val keyGenerator = X25519KeyGenerator()
val keyGeneratorSpy = spy(keyGenerator)
var randomByteGenerator: RandomByteGenerator = mock(RandomByteGenerator::class.java)
@Test fun testLTK() { @Test fun testLTK() {
val aapsLogger = AAPSLoggerTest() val aapsLogger = AAPSLoggerTest()
Mockito.doReturn(Hex.decode("27ec94b71a201c5e92698d668806ae5ba00594c307cf5566e60c1fc53a6f6bb6"))
.`when`(keyGeneratorSpy).generatePrivateKey()
val pdmNonce = Hex.decode("edfdacb242c7f4e1d2bc4d93ca3c5706")
Mockito.`when`(randomByteGenerator.nextBytes(anyInt())).thenReturn(pdmNonce)
val ke = KeyExchange( val ke = KeyExchange(
aapsLogger, aapsLogger,
pdmPrivate = Hex.decode("27ec94b71a201c5e92698d668806ae5ba00594c307cf5566e60c1fc53a6f6bb6"), keyGeneratorSpy,
pdmNonce = Hex.decode("edfdacb242c7f4e1d2bc4d93ca3c5706") randomByteGenerator
) )
val podPublicKey = Hex.decode("2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74") val podPublicKey = Hex.decode("2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74")
val podNonce = Hex.decode("00000000000000000000000000000000") val podNonce = Hex.decode("00000000000000000000000000000000")