Merge branch 'dash' into bart/create-responses
This commit is contained in:
commit
12dad6662c
12 changed files with 107 additions and 36 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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) +
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in a new issue