diff --git a/.gitignore b/.gitignore index 74eb164a98..2575eddbbc 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,6 @@ build/ .idea/* !.idea/codeStyles/ full/ -debug/ -release/ app/com.crashlytics.settings.json app/session_analytics.tap .project diff --git a/build.gradle b/build.gradle index 3b9caa5038..913ee7cc76 100644 --- a/build.gradle +++ b/build.gradle @@ -55,6 +55,7 @@ buildscript { // in the individual module build.gradle files classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version" classpath 'com.hiya:jacoco-android:0.2' } } diff --git a/omnipod-dash/build.gradle b/omnipod-dash/build.gradle index 9d48cfa1c1..e48b09f3c7 100644 --- a/omnipod-dash/build.gradle +++ b/omnipod-dash/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-allopen' apply plugin: 'com.hiya.jacoco-android' 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 @@ -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 toolVersion = "1.15.0-RC2" config = files("./detekt-config.yml") // TODO move to global space and use "../detekt-config.yml" diff --git a/omnipod-dash/src/debug/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/annotations/OpenForTesting.kt b/omnipod-dash/src/debug/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/annotations/OpenForTesting.kt new file mode 100644 index 0000000000..689d6a467c --- /dev/null +++ b/omnipod-dash/src/debug/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/annotations/OpenForTesting.kt @@ -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 diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/OmnipodDashManagerImpl.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/OmnipodDashManagerImpl.kt index 8c76ed2cc0..3c6a937684 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/OmnipodDashManagerImpl.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/OmnipodDashManagerImpl.kt @@ -43,9 +43,9 @@ class OmnipodDashManagerImpl @Inject constructor( private val observePodReadyForActivationPart2: Observable get() = Observable.defer { - if (podStateManager.activationProgress.isAtLeast(ActivationProgress.PHASE_1_COMPLETED) && podStateManager.activationProgress.isBefore( - ActivationProgress.COMPLETED - )) { + if (podStateManager.activationProgress.isAtLeast(ActivationProgress.PHASE_1_COMPLETED) && + podStateManager.activationProgress.isBefore(ActivationProgress.COMPLETED) + ) { Observable.empty() } else { // TODO introduce specialized Exception diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/KeyExchange.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/KeyExchange.kt index 90130a29f0..0328be4a5d 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/KeyExchange.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/KeyExchange.kt @@ -1,39 +1,35 @@ 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.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 org.spongycastle.crypto.engines.AESEngine import org.spongycastle.crypto.macs.CMac import org.spongycastle.crypto.params.KeyParameter -import java.security.SecureRandom class KeyExchange( private val aapsLogger: AAPSLogger, - var pdmPrivate: ByteArray = X25519.generatePrivateKey(), - val pdmNonce: ByteArray = ByteArray(NONCE_SIZE) + private val x25519: X25519KeyGenerator, + 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 podNonce = ByteArray(NONCE_SIZE) + private set + var podNonce: ByteArray = ByteArray(NONCE_SIZE) val podConf = ByteArray(CMAC_SIZE) val pdmConf = ByteArray(CMAC_SIZE) var ltk = ByteArray(CMAC_SIZE) - init { - if (pdmNonce.all { it == 0.toByte() }) { - // pdmNonce is in the constructor for tests - val random = SecureRandom() - random.nextBytes(pdmNonce) - } - } - fun updatePodPublicData(payload: ByteArray) { if (payload.size != PUBLIC_KEY_SIZE + NONCE_SIZE) { throw MessageIOException("Invalid payload size") @@ -54,7 +50,7 @@ class KeyExchange( } private fun generateKeys() { - val curveLTK = X25519.computeSharedSecret(pdmPrivate, podPublic) + val curveLTK = x25519.computeSharedSecret(pdmPrivate, podPublic) val firstKey = podPublic.copyOfRange(podPublic.size - 4, podPublic.size) + pdmPublic.copyOfRange(pdmPublic.size - 4, pdmPublic.size) + 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 index a7f8349aec..f30f7e5c6a 100644 --- 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 @@ -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.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.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.toHex @@ -19,7 +21,7 @@ internal class LTKExchanger( val podAddress: Id ) { - private val keyExchange = KeyExchange(aapsLogger) + private val keyExchange = KeyExchange(aapsLogger, X25519KeyGenerator(), RandomByteGenerator()) private var seq: Byte = 1 fun negotiateLTK(): PairResult { diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/util/RandomByteGenerator.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/util/RandomByteGenerator.kt new file mode 100644 index 0000000000..ca6beb17dd --- /dev/null +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/util/RandomByteGenerator.kt @@ -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) +} diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/util/X25519KeyGenerator.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/util/X25519KeyGenerator.kt new file mode 100644 index 0000000000..160f620df6 --- /dev/null +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/pod/util/X25519KeyGenerator.kt @@ -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) +} diff --git a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/OmnipodDashOverviewFragment.kt b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/OmnipodDashOverviewFragment.kt index b2351d4f75..29aef1aa5e 100644 --- a/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/OmnipodDashOverviewFragment.kt +++ b/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/ui/OmnipodDashOverviewFragment.kt @@ -467,16 +467,15 @@ class OmnipodDashOverviewFragment : DaggerFragment() { private fun updateRefreshStatusButton() { buttonBinding.buttonRefreshStatus.isEnabled = - podStateManager.isUniqueIdSet && podStateManager.activationProgress.isAtLeast( - ActivationProgress.PHASE_1_COMPLETED - ) && - isQueueEmpty() + podStateManager.isUniqueIdSet && + podStateManager.activationProgress.isAtLeast(ActivationProgress.PHASE_1_COMPLETED) && + isQueueEmpty() } private fun updateResumeDeliveryButton() { - if (podStateManager.isPodRunning && (podStateManager.isSuspended || commandQueue.isCustomCommandInQueue( - CommandResumeDelivery::class.java - ))) { + if (podStateManager.isPodRunning && + (podStateManager.isSuspended || commandQueue.isCustomCommandInQueue(CommandResumeDelivery::class.java)) + ) { buttonBinding.buttonResumeDelivery.visibility = View.VISIBLE buttonBinding.buttonResumeDelivery.isEnabled = isQueueEmpty() } else { @@ -485,9 +484,12 @@ class OmnipodDashOverviewFragment : DaggerFragment() { } private fun updateSilenceAlertsButton() { - if (isAutomaticallySilenceAlertsEnabled() && podStateManager.isPodRunning && (podStateManager.activeAlerts!!.size > 0 || commandQueue.isCustomCommandInQueue( - CommandAcknowledgeAlerts::class.java - ))) { + if (isAutomaticallySilenceAlertsEnabled() && podStateManager.isPodRunning && + ( + podStateManager.activeAlerts!!.size > 0 || + commandQueue.isCustomCommandInQueue(CommandAcknowledgeAlerts::class.java) + ) + ) { buttonBinding.buttonSilenceAlerts.visibility = View.VISIBLE buttonBinding.buttonSilenceAlerts.isEnabled = isQueueEmpty() } else { @@ -497,9 +499,10 @@ class OmnipodDashOverviewFragment : DaggerFragment() { private fun updateSuspendDeliveryButton() { // If the Pod is currently suspended, we show the Resume delivery button instead. - if (isSuspendDeliveryButtonEnabled() && podStateManager.isPodRunning && (!podStateManager.isSuspended || commandQueue.isCustomCommandInQueue( - CommandSuspendDelivery::class.java - ))) { + if (isSuspendDeliveryButtonEnabled() && + podStateManager.isPodRunning && + (!podStateManager.isSuspended || commandQueue.isCustomCommandInQueue(CommandSuspendDelivery::class.java)) + ) { buttonBinding.buttonSuspendDelivery.visibility = View.VISIBLE buttonBinding.buttonSuspendDelivery.isEnabled = podStateManager.isPodRunning && !podStateManager.isSuspended && isQueueEmpty() diff --git a/omnipod-dash/src/release/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/annotations/OpenForTesting.kt b/omnipod-dash/src/release/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/annotations/OpenForTesting.kt new file mode 100644 index 0000000000..6cf2180e50 --- /dev/null +++ b/omnipod-dash/src/release/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/annotations/OpenForTesting.kt @@ -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 diff --git a/omnipod-dash/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/KeyExchangeTest.kt b/omnipod-dash/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/KeyExchangeTest.kt index 8ba8f7242f..50e04f6e2a 100644 --- a/omnipod-dash/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/KeyExchangeTest.kt +++ b/omnipod-dash/src/test/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/driver/comm/pair/KeyExchangeTest.kt @@ -1,19 +1,38 @@ package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.pair 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 org.junit.Assert.assertEquals 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 class KeyExchangeTest { + val keyGenerator = X25519KeyGenerator() + val keyGeneratorSpy = spy(keyGenerator) + + var randomByteGenerator: RandomByteGenerator = mock(RandomByteGenerator::class.java) + @Test fun testLTK() { 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( aapsLogger, - pdmPrivate = Hex.decode("27ec94b71a201c5e92698d668806ae5ba00594c307cf5566e60c1fc53a6f6bb6"), - pdmNonce = Hex.decode("edfdacb242c7f4e1d2bc4d93ca3c5706") + keyGeneratorSpy, + randomByteGenerator ) val podPublicKey = Hex.decode("2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74") val podNonce = Hex.decode("00000000000000000000000000000000")