From 0f0452a1c4c40ea8da18e80143f1991475052421 Mon Sep 17 00:00:00 2001 From: Dominik Dzienia Date: Mon, 10 Feb 2020 16:54:18 +0100 Subject: [PATCH] Support for Time-based One-time Passwords (TOTP) in SMS Communicator --- app/build.gradle | 3 + .../info/nightscout/androidaps/Constants.java | 16 +++ .../info/nightscout/androidaps/MainApp.java | 2 + .../dependencyInjection/FragmentsModule.kt | 7 +- .../general/smsCommunicator/AuthRequest.kt | 14 +- .../SmsCommunicatorFragment.kt | 91 ++++++------ .../smsCommunicator/SmsCommunicatorPlugin.kt | 35 +++-- .../fragments/SmsCommunicatorLogFragment.kt | 80 +++++++++++ .../fragments/SmsCommunicatorOtpFragment.kt | 115 +++++++++++++++ .../utils/OneTimePasswordValidationResult.kt | 8 ++ .../androidaps/utils/OneTimePasswords.kt | 135 ++++++++++++++++++ .../textValidator/DefaultEditTextValidator.kt | 1 + .../textValidator/EditTextValidator.java | 2 + .../validators/PinStrengthValidator.kt | 41 ++++++ .../res/layout/smscommunicator_fragment.xml | 51 +++++-- .../layout/smscommunicator_fragment_log.xml | 19 +++ .../layout/smscommunicator_fragment_otp.xml | 104 ++++++++++++++ app/src/main/res/values/fet_attrs.xml | 1 + app/src/main/res/values/strings.xml | 34 +++++ app/src/main/res/values/styles.xml | 20 +++ app/src/main/res/values/validator.xml | 1 + app/src/main/res/xml/pref_smscommunicator.xml | 19 +++ app/src/test/java/info/AAPSMocker.java | 5 + .../SmsCommunicatorPluginTest.java | 7 +- 24 files changed, 739 insertions(+), 72 deletions(-) create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/fragments/SmsCommunicatorLogFragment.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/fragments/SmsCommunicatorOtpFragment.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/utils/OneTimePasswordValidationResult.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/utils/OneTimePasswords.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/utils/textValidator/validators/PinStrengthValidator.kt create mode 100644 app/src/main/res/layout/smscommunicator_fragment_log.xml create mode 100644 app/src/main/res/layout/smscommunicator_fragment_otp.xml diff --git a/app/build.gradle b/app/build.gradle index a75c70b9ce..0dd1e440cb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -282,6 +282,9 @@ dependencies { implementation 'com.github.DavidProdinger:weekdays-selector:1.1.0' + implementation 'com.github.kenglxn.QRGen:android:2.6.0' + implementation 'com.eatthepath:java-otp:0.2.0' + testImplementation "junit:junit:4.12" testImplementation "org.json:json:20190722" testImplementation "org.mockito:mockito-core:2.8.47" diff --git a/app/src/main/java/info/nightscout/androidaps/Constants.java b/app/src/main/java/info/nightscout/androidaps/Constants.java index fb322a8239..38767ebfb9 100644 --- a/app/src/main/java/info/nightscout/androidaps/Constants.java +++ b/app/src/main/java/info/nightscout/androidaps/Constants.java @@ -87,4 +87,20 @@ public class Constants { public static final double STATS_RANGE_HIGH_MMOL = 10.0; + // One Time Password + + /** + * Size of generated key for TOTP Authenticator token, in bits + * rfc6238 suggest at least 160 for SHA1 based TOTP, but it ts too weak + * with 512 generated QRCode to provision authenticator is too detailed + * 256 is chosen as both secure enough and small enough for easy-scannable QRCode + */ + public static final int OTP_GENERATED_KEY_LENGTH_BITS = 256; + + /** + * How many old TOTP tokens still accept. + * Each token is 30s valid, but copying and SMS transmision of it can take additional seconds, + * so we add leeway to still accept given amount of older tokens + */ + public static final int OTP_ACCEPT_OLD_TOKENS_COUNT = 1; } diff --git a/app/src/main/java/info/nightscout/androidaps/MainApp.java b/app/src/main/java/info/nightscout/androidaps/MainApp.java index d1906dc828..e88ce6a6a6 100644 --- a/app/src/main/java/info/nightscout/androidaps/MainApp.java +++ b/app/src/main/java/info/nightscout/androidaps/MainApp.java @@ -106,6 +106,7 @@ import info.nightscout.androidaps.services.Intents; import info.nightscout.androidaps.utils.ActivityMonitor; import info.nightscout.androidaps.utils.FabricPrivacy; import info.nightscout.androidaps.utils.LocaleHelper; +import info.nightscout.androidaps.utils.OneTimePassword; import info.nightscout.androidaps.utils.SP; import info.nightscout.androidaps.utils.resources.ResourceHelper; import io.fabric.sdk.android.Fabric; @@ -137,6 +138,7 @@ public class MainApp extends DaggerApplication { @Inject FabricPrivacy fabricPrivacy; @Inject ResourceHelper resourceHelper; @Inject VersionCheckerUtils versionCheckersUtils; + @Inject OneTimePassword oneTimePassword; @Inject ActionsPlugin actionsPlugin; @Inject AutomationPlugin automationPlugin; diff --git a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/FragmentsModule.kt b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/FragmentsModule.kt index b5d7c34848..f93ab67608 100644 --- a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/FragmentsModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/FragmentsModule.kt @@ -27,6 +27,8 @@ import info.nightscout.androidaps.plugins.general.nsclient.NSClientFragment import info.nightscout.androidaps.plugins.general.overview.OverviewFragment import info.nightscout.androidaps.plugins.general.overview.dialogs.EditQuickWizardDialog import info.nightscout.androidaps.plugins.general.smsCommunicator.SmsCommunicatorFragment +import info.nightscout.androidaps.plugins.general.smsCommunicator.fragments.SmsCommunicatorLogFragment +import info.nightscout.androidaps.plugins.general.smsCommunicator.fragments.SmsCommunicatorOtpFragment import info.nightscout.androidaps.plugins.general.tidepool.TidepoolFragment import info.nightscout.androidaps.plugins.profile.local.LocalProfileFragment import info.nightscout.androidaps.plugins.profile.ns.NSProfileFragment @@ -69,8 +71,9 @@ abstract class FragmentsModule { @ContributesAndroidInjector abstract fun contributesMedtronicFragment(): MedtronicFragment @ContributesAndroidInjector abstract fun contributesNSProfileFragment(): NSProfileFragment @ContributesAndroidInjector abstract fun contributesNSClientFragment(): NSClientFragment - @ContributesAndroidInjector - abstract fun contributesSmsCommunicatorFragment(): SmsCommunicatorFragment + @ContributesAndroidInjector abstract fun contributesSmsCommunicatorFragment(): SmsCommunicatorFragment + @ContributesAndroidInjector abstract fun contributesSmsCommunicatorLogFragment(): SmsCommunicatorLogFragment + @ContributesAndroidInjector abstract fun contributesSmsCommunicatorOtpFragment(): SmsCommunicatorOtpFragment @ContributesAndroidInjector abstract fun contributesTidepoolFragment(): TidepoolFragment @ContributesAndroidInjector abstract fun contributesTreatmentsFragment(): TreatmentsFragment diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/AuthRequest.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/AuthRequest.kt index 128652e714..29e28b3970 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/AuthRequest.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/AuthRequest.kt @@ -5,10 +5,12 @@ import info.nightscout.androidaps.R import info.nightscout.androidaps.logging.L import info.nightscout.androidaps.logging.StacktraceLoggerWrapper import info.nightscout.androidaps.utils.DateUtil +import info.nightscout.androidaps.utils.OneTimePassword +import info.nightscout.androidaps.utils.OneTimePasswordValidationResult import info.nightscout.androidaps.utils.resources.ResourceHelper import org.slf4j.LoggerFactory -class AuthRequest internal constructor(val plugin: SmsCommunicatorPlugin, val resourceHelper: ResourceHelper, var requester: Sms, requestText: String, var confirmCode: String, val action: SmsAction) { +class AuthRequest internal constructor(val plugin: SmsCommunicatorPlugin, val resourceHelper: ResourceHelper, val otp: OneTimePassword, var requester: Sms, requestText: String, var confirmCode: String, val action: SmsAction) { private val log = StacktraceLoggerWrapper.getLogger(L.SMS) private val date = DateUtil.now() @@ -18,12 +20,20 @@ class AuthRequest internal constructor(val plugin: SmsCommunicatorPlugin, val re plugin.sendSMS(Sms(requester.phoneNumber, requestText)) } + private fun codeIsValid(toValidate: String) : Boolean { + if (otp.isEnabled()) { + return otp.checkOTP(toValidate) == OneTimePasswordValidationResult.OK + } else { + return confirmCode.equals(toValidate) + } + } + fun action(codeReceived: String) { if (processed) { if (L.isEnabled(L.SMS)) log.debug("Already processed") return } - if (confirmCode != codeReceived) { + if (!codeIsValid(codeReceived)) { processed = true if (L.isEnabled(L.SMS)) log.debug("Wrong code") plugin.sendSMS(Sms(requester.phoneNumber, resourceHelper.gs(R.string.sms_wrongcode))) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/SmsCommunicatorFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/SmsCommunicatorFragment.kt index 94dd9965f8..700dc76fd9 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/SmsCommunicatorFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/SmsCommunicatorFragment.kt @@ -4,75 +4,78 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentTransaction import dagger.android.support.DaggerFragment import info.nightscout.androidaps.R import info.nightscout.androidaps.plugins.bus.RxBusWrapper -import info.nightscout.androidaps.plugins.general.smsCommunicator.events.EventSmsCommunicatorUpdateGui -import info.nightscout.androidaps.utils.DateUtil +import info.nightscout.androidaps.plugins.general.smsCommunicator.fragments.SmsCommunicatorLogFragment +import info.nightscout.androidaps.plugins.general.smsCommunicator.fragments.SmsCommunicatorOtpFragment import info.nightscout.androidaps.utils.FabricPrivacy -import info.nightscout.androidaps.utils.HtmlHelper -import info.nightscout.androidaps.utils.extensions.plusAssign -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.CompositeDisposable +import info.nightscout.androidaps.utils.OneTimePassword +import info.nightscout.androidaps.utils.resources.ResourceHelper import kotlinx.android.synthetic.main.smscommunicator_fragment.* -import java.util.* import javax.inject.Inject -import kotlin.math.max class SmsCommunicatorFragment : DaggerFragment() { - @Inject lateinit var fabricPrivacy : FabricPrivacy @Inject lateinit var rxBus: RxBusWrapper + @Inject lateinit var resourceHelper: ResourceHelper + @Inject lateinit var fabricPrivacy: FabricPrivacy + @Inject lateinit var otp: OneTimePassword @Inject lateinit var smsCommunicatorPlugin: SmsCommunicatorPlugin - private val disposable = CompositeDisposable() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.smscommunicator_fragment, container, false) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + smscomunicator_tab_log.setOnClickListener { + setFragment(SmsCommunicatorLogFragment()) + setBackgroundColorOnSelected(it) + } + smscomunicator_tab_otp.setOnClickListener { + setFragment(SmsCommunicatorOtpFragment()) + setBackgroundColorOnSelected(it) + } + + setFragment(SmsCommunicatorLogFragment()) + setBackgroundColorOnSelected(smscomunicator_tab_log) + } + @Synchronized override fun onResume() { super.onResume() - disposable += rxBus - .toObservable(EventSmsCommunicatorUpdateGui::class.java) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ updateGui() }) { fabricPrivacy.logException(it) } updateGui() } - @Synchronized - override fun onPause() { - super.onPause() - disposable.clear() - } - fun updateGui() { - class CustomComparator : Comparator { - override fun compare(object1: Sms, object2: Sms): Int { - return (object1.date - object2.date).toInt() + if (otp.isEnabled()) { + smscomunicator_tab_otp.visibility = View.VISIBLE + } else { + if (smscomunicator_tab_otp.visibility != View.GONE) { + setFragment(SmsCommunicatorLogFragment()) + setBackgroundColorOnSelected(smscomunicator_tab_log) + smscomunicator_tab_otp.visibility = View.GONE } } - Collections.sort(smsCommunicatorPlugin.messages, CustomComparator()) - val messagesToShow = 40 - val start = max(0, smsCommunicatorPlugin.messages.size - messagesToShow) - var logText = "" - for (x in start until smsCommunicatorPlugin.messages.size) { - val sms = smsCommunicatorPlugin.messages[x] - when { - sms.ignored -> { - logText += DateUtil.timeString(sms.date) + " <<< " + "░ " + sms.phoneNumber + " " + sms.text + "
" - } - - sms.received -> { - logText += DateUtil.timeString(sms.date) + " <<< " + (if (sms.processed) "● " else "○ ") + sms.phoneNumber + " " + sms.text + "
" - } - - sms.sent -> { - logText += DateUtil.timeString(sms.date) + " >>> " + (if (sms.processed) "● " else "○ ") + sms.phoneNumber + " " + sms.text + "
" - } - } - } - smscommunicator_log?.text = HtmlHelper.fromHtml(logText) } + + private fun setFragment(selectedFragment: Fragment) { + val ft = childFragmentManager.beginTransaction() + ft.replace(R.id.smscomunicator_fragment_container, selectedFragment) + ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) + ft.addToBackStack(null) + ft.commit() + } + + private fun setBackgroundColorOnSelected(selected: View) { + smscomunicator_tab_log.setBackgroundColor(resourceHelper.gc(R.color.defaultbackground)) + smscomunicator_tab_otp.setBackgroundColor(resourceHelper.gc(R.color.defaultbackground)) + selected.setBackgroundColor(resourceHelper.gc(R.color.tabBgColorSelected)) + } + } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/SmsCommunicatorPlugin.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/SmsCommunicatorPlugin.kt index dbd4b68074..70d4bac717 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/SmsCommunicatorPlugin.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/SmsCommunicatorPlugin.kt @@ -56,7 +56,8 @@ class SmsCommunicatorPlugin @Inject constructor( private val activePlugin: ActivePluginProvider, private val commandQueue: CommandQueueProvider, private val loopPlugin: LoopPlugin, - private val iobCobCalculatorPlugin: IobCobCalculatorPlugin + private val iobCobCalculatorPlugin: IobCobCalculatorPlugin, + private var otp: OneTimePassword ) : PluginBase(PluginDescription() .mainType(PluginType.GENERAL) .fragmentClass(SmsCommunicatorFragment::class.java.name) @@ -354,7 +355,7 @@ class SmsCommunicatorPlugin @Inject constructor( val passCode = generatePasscode() val reply = String.format(resourceHelper.gs(R.string.smscommunicator_suspendreplywithcode), duration, passCode) receivedSms.processed = true - messageToConfirm = AuthRequest(this, resourceHelper, receivedSms, reply, passCode, object : SmsAction(duration) { + messageToConfirm = AuthRequest(this, resourceHelper, otp, receivedSms, reply, passCode, object : SmsAction(duration) { override fun run() { commandQueue.cancelTempBasal(true, object : Callback() { override fun run() { @@ -469,7 +470,7 @@ class SmsCommunicatorPlugin @Inject constructor( val reply = String.format(resourceHelper.gs(R.string.smscommunicator_profilereplywithcode), list[pindex - 1], percentage, passCode) receivedSms.processed = true val finalPercentage = percentage - messageToConfirm = AuthRequest(this, resourceHelper, receivedSms, reply, passCode, object : SmsAction(list[pindex - 1] as String, finalPercentage) { + messageToConfirm = AuthRequest(this, resourceHelper, otp, receivedSms, reply, passCode, object : SmsAction(list[pindex - 1] as String, finalPercentage) { override fun run() { activePlugin.activeTreatments.doProfileSwitch(store, list[pindex - 1] as String, 0, finalPercentage, 0, DateUtil.now()) sendSMS(Sms(receivedSms.phoneNumber, resourceHelper.gs(R.string.profileswitchcreated))) @@ -486,7 +487,7 @@ class SmsCommunicatorPlugin @Inject constructor( val passCode = generatePasscode() val reply = String.format(resourceHelper.gs(R.string.smscommunicator_basalstopreplywithcode), passCode) receivedSms.processed = true - messageToConfirm = AuthRequest(this, resourceHelper, receivedSms, reply, passCode, object : SmsAction() { + messageToConfirm = AuthRequest(this, resourceHelper, otp, receivedSms, reply, passCode, object : SmsAction() { override fun run() { commandQueue.cancelTempBasal(true, object : Callback() { override fun run() { @@ -516,7 +517,7 @@ class SmsCommunicatorPlugin @Inject constructor( val passCode = generatePasscode() val reply = String.format(resourceHelper.gs(R.string.smscommunicator_basalpctreplywithcode), tempBasalPct, duration, passCode) receivedSms.processed = true - messageToConfirm = AuthRequest(this, resourceHelper, receivedSms, reply, passCode, object : SmsAction(tempBasalPct, duration) { + messageToConfirm = AuthRequest(this, resourceHelper, otp, receivedSms, reply, passCode, object : SmsAction(tempBasalPct, duration) { override fun run() { commandQueue.tempBasalPercent(anInteger(), secondInteger(), true, profile, object : Callback() { override fun run() { @@ -548,7 +549,7 @@ class SmsCommunicatorPlugin @Inject constructor( val passCode = generatePasscode() val reply = String.format(resourceHelper.gs(R.string.smscommunicator_basalreplywithcode), tempBasal, duration, passCode) receivedSms.processed = true - messageToConfirm = AuthRequest(this, resourceHelper, receivedSms, reply, passCode, object : SmsAction(tempBasal, duration) { + messageToConfirm = AuthRequest(this, resourceHelper, otp, receivedSms, reply, passCode, object : SmsAction(tempBasal, duration) { override fun run() { commandQueue.tempBasalAbsolute(aDouble(), secondInteger(), true, profile, object : Callback() { override fun run() { @@ -575,7 +576,7 @@ class SmsCommunicatorPlugin @Inject constructor( val passCode = generatePasscode() val reply = String.format(resourceHelper.gs(R.string.smscommunicator_extendedstopreplywithcode), passCode) receivedSms.processed = true - messageToConfirm = AuthRequest(this, resourceHelper, receivedSms, reply, passCode, object : SmsAction() { + messageToConfirm = AuthRequest(this, resourceHelper, otp, receivedSms, reply, passCode, object : SmsAction() { override fun run() { commandQueue.cancelExtended(object : Callback() { override fun run() { @@ -603,7 +604,7 @@ class SmsCommunicatorPlugin @Inject constructor( val passCode = generatePasscode() val reply = String.format(resourceHelper.gs(R.string.smscommunicator_extendedreplywithcode), extended, duration, passCode) receivedSms.processed = true - messageToConfirm = AuthRequest(this, resourceHelper, receivedSms, reply, passCode, object : SmsAction(extended, duration) { + messageToConfirm = AuthRequest(this, resourceHelper, otp, receivedSms, reply, passCode, object : SmsAction(extended, duration) { override fun run() { commandQueue.extendedBolus(aDouble(), secondInteger(), object : Callback() { override fun run() { @@ -638,7 +639,7 @@ class SmsCommunicatorPlugin @Inject constructor( else String.format(resourceHelper.gs(R.string.smscommunicator_bolusreplywithcode), bolus, passCode) receivedSms.processed = true - messageToConfirm = AuthRequest(this, resourceHelper, receivedSms, reply, passCode, object : SmsAction(bolus) { + messageToConfirm = AuthRequest(this, resourceHelper, otp, receivedSms, reply, passCode, object : SmsAction(bolus) { override fun run() { val detailedBolusInfo = DetailedBolusInfo() detailedBolusInfo.insulin = aDouble() @@ -714,7 +715,7 @@ class SmsCommunicatorPlugin @Inject constructor( val passCode = generatePasscode() val reply = String.format(resourceHelper.gs(R.string.smscommunicator_carbsreplywithcode), grams, DateUtil.timeString(time), passCode) receivedSms.processed = true - messageToConfirm = AuthRequest(this, resourceHelper, receivedSms, reply, passCode, object : SmsAction(grams, time) { + messageToConfirm = AuthRequest(this, resourceHelper, otp, receivedSms, reply, passCode, object : SmsAction(grams, time) { override fun run() { val detailedBolusInfo = DetailedBolusInfo() detailedBolusInfo.carbs = anInteger().toDouble() @@ -746,7 +747,7 @@ class SmsCommunicatorPlugin @Inject constructor( val passCode = generatePasscode() val reply = String.format(resourceHelper.gs(R.string.smscommunicator_temptargetwithcode), splitted[1].toUpperCase(Locale.getDefault()), passCode) receivedSms.processed = true - messageToConfirm = AuthRequest(this, resourceHelper, receivedSms, reply, passCode, object : SmsAction() { + messageToConfirm = AuthRequest(this, resourceHelper, otp, receivedSms, reply, passCode, object : SmsAction() { override fun run() { val units = profileFunction.getUnits() var keyDuration = 0 @@ -801,7 +802,7 @@ class SmsCommunicatorPlugin @Inject constructor( val passCode = generatePasscode() val reply = String.format(resourceHelper.gs(R.string.smscommunicator_temptargetcancel), passCode) receivedSms.processed = true - messageToConfirm = AuthRequest(this, resourceHelper, receivedSms, reply, passCode, object : SmsAction() { + messageToConfirm = AuthRequest(this, resourceHelper, otp, receivedSms, reply, passCode, object : SmsAction() { override fun run() { val tempTarget = TempTarget() .source(Source.USER) @@ -825,7 +826,7 @@ class SmsCommunicatorPlugin @Inject constructor( val passCode = generatePasscode() val reply = String.format(resourceHelper.gs(R.string.smscommunicator_stopsmswithcode), passCode) receivedSms.processed = true - messageToConfirm = AuthRequest(this, resourceHelper, receivedSms, reply, passCode, object : SmsAction() { + messageToConfirm = AuthRequest(this, resourceHelper, otp, receivedSms, reply, passCode, object : SmsAction() { override fun run() { sp.putBoolean(R.string.key_smscommunicator_remotecommandsallowed, false) val replyText = String.format(resourceHelper.gs(R.string.smscommunicator_stoppedsms)) @@ -841,7 +842,7 @@ class SmsCommunicatorPlugin @Inject constructor( val passCode = generatePasscode() val reply = String.format(resourceHelper.gs(R.string.smscommunicator_calibrationreplywithcode), cal, passCode) receivedSms.processed = true - messageToConfirm = AuthRequest(this, resourceHelper, receivedSms, reply, passCode, object : SmsAction(cal) { + messageToConfirm = AuthRequest(this, resourceHelper, otp, receivedSms, reply, passCode, object : SmsAction(cal) { override fun run() { val result = XdripCalibrations.sendIntent(aDouble) if (result) sendSMSToAllNumbers(Sms(receivedSms.phoneNumber, resourceHelper.gs(R.string.smscommunicator_calibrationsent))) else sendSMS(Sms(receivedSms.phoneNumber, resourceHelper.gs(R.string.smscommunicator_calibrationfailed))) @@ -898,6 +899,12 @@ class SmsCommunicatorPlugin @Inject constructor( } private fun generatePasscode(): String { + + if (otp.isEnabled()) { + // this not realy generate password - rather info to use Authenticator TOTP instead + return resourceHelper.gs(R.string.smscommunicator_code_from_authenticator_for, otp.name()); + } + val startChar1 = 'A'.toInt() // on iphone 1st char is uppercase :) var passCode = Character.toString((startChar1 + Math.random() * ('z' - 'a' + 1)).toChar()) val startChar2: Int = if (Math.random() > 0.5) 'a'.toInt() else 'A'.toInt() diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/fragments/SmsCommunicatorLogFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/fragments/SmsCommunicatorLogFragment.kt new file mode 100644 index 0000000000..778b7fa6a3 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/fragments/SmsCommunicatorLogFragment.kt @@ -0,0 +1,80 @@ +package info.nightscout.androidaps.plugins.general.smsCommunicator.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import dagger.android.support.DaggerFragment +import info.nightscout.androidaps.R +import info.nightscout.androidaps.plugins.bus.RxBusWrapper +import info.nightscout.androidaps.plugins.general.smsCommunicator.Sms +import info.nightscout.androidaps.plugins.general.smsCommunicator.SmsCommunicatorPlugin +import info.nightscout.androidaps.plugins.general.smsCommunicator.events.EventSmsCommunicatorUpdateGui +import info.nightscout.androidaps.utils.DateUtil +import info.nightscout.androidaps.utils.FabricPrivacy +import info.nightscout.androidaps.utils.HtmlHelper +import info.nightscout.androidaps.utils.extensions.plusAssign +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.smscommunicator_fragment_log.* +import java.util.* +import javax.inject.Inject +import kotlin.math.max + +class SmsCommunicatorLogFragment : DaggerFragment() { + @Inject lateinit var fabricPrivacy : FabricPrivacy + @Inject lateinit var rxBus: RxBusWrapper + @Inject lateinit var smsCommunicatorPlugin: SmsCommunicatorPlugin + + private val disposable = CompositeDisposable() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.smscommunicator_fragment_log, container, false) + } + + @Synchronized + override fun onResume() { + super.onResume() + disposable += rxBus + .toObservable(EventSmsCommunicatorUpdateGui::class.java) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ updateGui() }) { fabricPrivacy.logException(it) } + updateGui() + } + + @Synchronized + override fun onPause() { + super.onPause() + disposable.clear() + } + + fun updateGui() { + class CustomComparator : Comparator { + override fun compare(object1: Sms, object2: Sms): Int { + return (object1.date - object2.date).toInt() + } + } + Collections.sort(smsCommunicatorPlugin.messages, CustomComparator()) + val messagesToShow = 40 + val start = max(0, smsCommunicatorPlugin.messages.size - messagesToShow) + var logText = "" + for (x in start until smsCommunicatorPlugin.messages.size) { + val sms = smsCommunicatorPlugin.messages[x] + when { + sms.ignored -> { + logText += DateUtil.timeString(sms.date) + " <<< " + "░ " + sms.phoneNumber + " " + sms.text + "
" + } + + sms.received -> { + logText += DateUtil.timeString(sms.date) + " <<< " + (if (sms.processed) "● " else "○ ") + sms.phoneNumber + " " + sms.text + "
" + } + + sms.sent -> { + logText += DateUtil.timeString(sms.date) + " >>> " + (if (sms.processed) "● " else "○ ") + sms.phoneNumber + " " + sms.text + "
" + } + } + } + smscommunicator_log?.text = HtmlHelper.fromHtml(logText) + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/fragments/SmsCommunicatorOtpFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/fragments/SmsCommunicatorOtpFragment.kt new file mode 100644 index 0000000000..18f6700a9a --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/fragments/SmsCommunicatorOtpFragment.kt @@ -0,0 +1,115 @@ +package info.nightscout.androidaps.plugins.general.smsCommunicator.fragments + +import android.app.Activity +import android.content.res.Resources +import android.graphics.Color +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.google.common.primitives.Ints.min +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import dagger.android.support.DaggerFragment +import info.nightscout.androidaps.R +import info.nightscout.androidaps.plugins.bus.RxBusWrapper +import info.nightscout.androidaps.plugins.general.smsCommunicator.SmsCommunicatorPlugin +import info.nightscout.androidaps.utils.FabricPrivacy +import info.nightscout.androidaps.utils.OKDialog +import info.nightscout.androidaps.utils.OneTimePassword +import info.nightscout.androidaps.utils.OneTimePasswordValidationResult +import info.nightscout.androidaps.utils.ToastUtils +import info.nightscout.androidaps.utils.resources.ResourceHelper +import kotlinx.android.synthetic.main.smscommunicator_fragment_otp.* +import net.glxn.qrgen.android.QRCode +import javax.inject.Inject + +class SmsCommunicatorOtpFragment : DaggerFragment() { + @Inject lateinit var fabricPrivacy: FabricPrivacy + @Inject lateinit var rxBus: RxBusWrapper + @Inject lateinit var smsCommunicatorPlugin: SmsCommunicatorPlugin + @Inject lateinit var otp: OneTimePassword + @Inject lateinit var resourceHelper: ResourceHelper + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.smscommunicator_fragment_otp, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + smscommunicator_otp_verify_edit.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + if (s != null) { + val checkResult = otp.checkOTP(s.toString()) + + smscommunicator_otp_verify_label.text = when (checkResult) { + OneTimePasswordValidationResult.OK -> "OK" + OneTimePasswordValidationResult.ERROR_WRONG_LENGTH -> "INVALID SIZE!" + OneTimePasswordValidationResult.ERROR_WRONG_PIN -> "WRONG PIN" + OneTimePasswordValidationResult.ERROR_WRONG_OTP -> "WRONG OTP" + } + + smscommunicator_otp_verify_label.setTextColor(when (checkResult) { + OneTimePasswordValidationResult.OK -> Color.GREEN + OneTimePasswordValidationResult.ERROR_WRONG_LENGTH -> Color.YELLOW + OneTimePasswordValidationResult.ERROR_WRONG_PIN -> Color.RED + OneTimePasswordValidationResult.ERROR_WRONG_OTP -> Color.RED + }) + + } else { + smscommunicator_otp_verify_label.text = "EMPTY"; + smscommunicator_otp_verify_label.setTextColor(Color.YELLOW) + } + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + } + }) + + actions_smscommunicator_otp_reset.setOnClickListener { + if (this.activity != null) { + OKDialog.showConfirmation(this.activity!!, + resourceHelper.gs(R.string.smscommunicator_otp_reset_title), + resourceHelper.gs(R.string.smscommunicator_otp_reset_prompt), + Runnable { + otp.ensureKey(true) + updateGui() + ToastUtils.showToastInUiThread(this.context, R.string.smscommunicator_otp_reset_successful) + }) + } + } + + } + + @Synchronized + override fun onResume() { + super.onResume() + updateGui() + } + + fun updateGui() { + val displayMetrics = Resources.getSystem().getDisplayMetrics() + val width = displayMetrics.widthPixels + val height = displayMetrics.heightPixels + + // ensure QRCode is big enough to fit on screen + val dim = (min(width, height) * 0.85).toInt() + val provURI = otp.provisioningURI(); + + if (provURI != null) { + val myBitmap = QRCode.from(provURI).withErrorCorrection(ErrorCorrectionLevel.H).withSize(dim, dim).bitmap() + smscommunicator_otp_provisioning.setImageBitmap(myBitmap) + smscommunicator_otp_provisioning.visibility = View.VISIBLE + } else { + smscommunicator_otp_provisioning.visibility = View.GONE + } + + smscommunicator_otp_verify_edit.text = smscommunicator_otp_verify_edit.text + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/utils/OneTimePasswordValidationResult.kt b/app/src/main/java/info/nightscout/androidaps/utils/OneTimePasswordValidationResult.kt new file mode 100644 index 0000000000..449460f9f3 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/utils/OneTimePasswordValidationResult.kt @@ -0,0 +1,8 @@ +package info.nightscout.androidaps.utils + +enum class OneTimePasswordValidationResult { + OK, + ERROR_WRONG_LENGTH, + ERROR_WRONG_PIN, + ERROR_WRONG_OTP +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/utils/OneTimePasswords.kt b/app/src/main/java/info/nightscout/androidaps/utils/OneTimePasswords.kt new file mode 100644 index 0000000000..d70c61582f --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/utils/OneTimePasswords.kt @@ -0,0 +1,135 @@ +package info.nightscout.androidaps.utils + +import android.util.Base64 +import com.eatthepath.otp.TimeBasedOneTimePasswordGenerator +import com.google.common.io.BaseEncoding +import info.nightscout.androidaps.Constants +import info.nightscout.androidaps.R +import info.nightscout.androidaps.utils.resources.ResourceHelper +import info.nightscout.androidaps.utils.sharedPreferences.SP +import org.joda.time.DateTimeUtils +import java.net.URLEncoder +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class OneTimePassword @Inject constructor( + private val sp: SP, + private val resourceHelper: ResourceHelper +) { + + private var key: SecretKey? = null + private var pin: String = "" + private val totp = TimeBasedOneTimePasswordGenerator() + + init { + instance = this + configure() + } + + companion object { + private lateinit var instance: OneTimePassword + @JvmStatic + fun getInstance(): OneTimePassword = instance + } + + /** + * If OTP Authenticator support is enabled by user + */ + fun isEnabled(): Boolean { + return sp.getBoolean(R.string.key_smscommunicator_otp_enabled, false); + } + + /** + * Name of master device (target of OTP) + */ + fun name(): String { + val defaultUserName = resourceHelper.gs(R.string.smscommunicator_default_user_display_name) + var userName = sp.getString(R.string.key_smscommunicator_otp_name, defaultUserName).trim() + if (userName.length == 0) + userName = defaultUserName + return userName + } + + /** + * Make sure if private key for TOTP is generated, creating it when necessary or requested + */ + fun ensureKey(forceNewKey: Boolean = false) { + var keyBytes: ByteArray = byteArrayOf() + val strSecret = sp.getString(R.string.key_smscommunicator_otp_secret, "").trim() + if (strSecret.length == 0 || forceNewKey) { + val keyGenerator = KeyGenerator.getInstance(totp.getAlgorithm()); + keyGenerator.init(Constants.OTP_GENERATED_KEY_LENGTH_BITS); + val generatedKey = keyGenerator.generateKey(); + keyBytes = generatedKey.encoded + sp.putString(R.string.key_smscommunicator_otp_secret, Base64.encodeToString(keyBytes, Base64.NO_WRAP + Base64.NO_PADDING)) + } else { + keyBytes = Base64.decode(strSecret, Base64.DEFAULT); + } + key = SecretKeySpec(keyBytes, 0, keyBytes.size, "SHA1") + } + + private fun configure() { + ensureKey() + pin = sp.getString(R.string.key_smscommunicator_otp_pin, "").trim() + } + + private fun generateOneTimePassword(counter: Long): String { + if (key != null) { + return String.format("%06d", totp.generateOneTimePassword(key, counter)) + } else { + return "" + } + } + + /** + * Check if given OTP+PIN is valid + */ + fun checkOTP(otp: String): OneTimePasswordValidationResult { + configure() + val normalisedOtp = otp.replace(" ", "").replace("-", "").trim() + + if (pin.length < 3) { + return OneTimePasswordValidationResult.ERROR_WRONG_PIN + } + + if (normalisedOtp.length != (6 + pin.length)) { + return OneTimePasswordValidationResult.ERROR_WRONG_LENGTH + } + + if (!normalisedOtp.substring(6).equals(pin)) { + return OneTimePasswordValidationResult.ERROR_WRONG_PIN + } + + val milis: Long = DateTimeUtils.currentTimeMillis() + val counter: Long = (milis / 30000L) + + var acceptableTokens: MutableList = mutableListOf(generateOneTimePassword(counter)) + for (i in 0 until Constants.OTP_ACCEPT_OLD_TOKENS_COUNT) { + acceptableTokens.add(generateOneTimePassword(counter - i - 1)) + } + val candidateOtp = normalisedOtp.substring(0, 6) + + if (acceptableTokens.any { candidate -> candidateOtp.equals(candidate) }) { + return OneTimePasswordValidationResult.OK + } + + return OneTimePasswordValidationResult.ERROR_WRONG_OTP + } + + /** + * Return URI used to provision Authenticator apps + */ + fun provisioningURI(): String? { + val keyImm = key + if (keyImm != null) { + return "otpauth://totp/AndroidAPS:" + URLEncoder.encode(name(), "utf-8") + "?secret=" + BaseEncoding.base32().encode(keyImm.encoded).replace("=", "") + "&issuer=AndroidAPS" + } else { + return null + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/utils/textValidator/DefaultEditTextValidator.kt b/app/src/main/java/info/nightscout/androidaps/utils/textValidator/DefaultEditTextValidator.kt index 27c5e7d86b..4c15100edd 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/textValidator/DefaultEditTextValidator.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/textValidator/DefaultEditTextValidator.kt @@ -117,6 +117,7 @@ class DefaultEditTextValidator : EditTextValidator { EditTextValidator.TEST_EMAIL -> toAdd = EmailValidator(if (TextUtils.isEmpty(testErrorString)) context.getString(R.string.error_email_address_not_valid) else testErrorString) EditTextValidator.TEST_PHONE -> toAdd = PhoneValidator(if (TextUtils.isEmpty(testErrorString)) context.getString(R.string.error_phone_not_valid) else testErrorString) EditTextValidator.TEST_MULTI_PHONE -> toAdd = MultiPhoneValidator(if (TextUtils.isEmpty(testErrorString)) context.getString(R.string.error_phone_not_valid) else testErrorString) + EditTextValidator.TEST_PIN_STRENGTH -> toAdd = PinStrengthValidator(if (TextUtils.isEmpty(testErrorString)) context.getString(R.string.error_pin_not_valid) else testErrorString) EditTextValidator.TEST_DOMAINNAME -> toAdd = DomainValidator(if (TextUtils.isEmpty(testErrorString)) context.getString(R.string.error_domain_not_valid) else testErrorString) EditTextValidator.TEST_IPADDRESS -> toAdd = IpAddressValidator(if (TextUtils.isEmpty(testErrorString)) context.getString(R.string.error_ip_not_valid) else testErrorString) EditTextValidator.TEST_WEBURL -> toAdd = WebUrlValidator(if (TextUtils.isEmpty(testErrorString)) context.getString(R.string.error_url_not_valid) else testErrorString) diff --git a/app/src/main/java/info/nightscout/androidaps/utils/textValidator/EditTextValidator.java b/app/src/main/java/info/nightscout/androidaps/utils/textValidator/EditTextValidator.java index b14c4b0837..0c93c0d5f3 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/textValidator/EditTextValidator.java +++ b/app/src/main/java/info/nightscout/androidaps/utils/textValidator/EditTextValidator.java @@ -93,4 +93,6 @@ public interface EditTextValidator { int TEST_MULTI_PHONE = 19; + int TEST_PIN_STRENGTH = 20; + } diff --git a/app/src/main/java/info/nightscout/androidaps/utils/textValidator/validators/PinStrengthValidator.kt b/app/src/main/java/info/nightscout/androidaps/utils/textValidator/validators/PinStrengthValidator.kt new file mode 100644 index 0000000000..35907e33ff --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/utils/textValidator/validators/PinStrengthValidator.kt @@ -0,0 +1,41 @@ +package info.nightscout.androidaps.utils.textValidator.validators + +import android.widget.EditText + +class PinStrengthValidator(val _customErrorMessage: String?) : Validator(_customErrorMessage) { + + val regex = "[0-9]{3,6}".toRegex() + + override fun isValid(editText: EditText): Boolean { + return try { + val value = editText.text.toString() + if (!regex.matches(value)) + return false + + var last = ' ' + var rising = true + var falling = true + var same = true + value.forEachIndexed { i, c -> + if (i > 0) { + if (last != c) { + same = false + } + + if (last.toInt() + 1 != c.toInt()) { + falling = false + } + + if (last.toInt() != c.toInt() + 1) { + rising = false + } + } + last = c + } + + !rising && !falling && !same + } catch (e: NumberFormatException) { + false + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/smscommunicator_fragment.xml b/app/src/main/res/layout/smscommunicator_fragment.xml index c10b129605..7e392d0ec6 100644 --- a/app/src/main/res/layout/smscommunicator_fragment.xml +++ b/app/src/main/res/layout/smscommunicator_fragment.xml @@ -4,15 +4,48 @@ android:layout_height="match_parent" tools:context=".plugins.general.smsCommunicator.SmsCommunicatorFragment"> - + - - + + + + + + + + + + + diff --git a/app/src/main/res/layout/smscommunicator_fragment_log.xml b/app/src/main/res/layout/smscommunicator_fragment_log.xml new file mode 100644 index 0000000000..582c249661 --- /dev/null +++ b/app/src/main/res/layout/smscommunicator_fragment_log.xml @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/smscommunicator_fragment_otp.xml b/app/src/main/res/layout/smscommunicator_fragment_otp.xml new file mode 100644 index 0000000000..48310c78d9 --- /dev/null +++ b/app/src/main/res/layout/smscommunicator_fragment_otp.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/fet_attrs.xml b/app/src/main/res/values/fet_attrs.xml index 855baef5b1..87519166be 100644 --- a/app/src/main/res/values/fet_attrs.xml +++ b/app/src/main/res/values/fet_attrs.xml @@ -23,6 +23,7 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1f15a23d70..fdc9ded8a1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1702,4 +1702,38 @@ Temp basal request time Temp basal execution time + + + smscommunicator_otp_enabled + smscommunicator_otp_name + smscommunicator_otp_pin + smscommunicator_otp_secret + + User + from Authenticator app for: %1$s + + Enable Authenticator + Authenticate commands using One Time Passwords generated by Google Authenticator or similar 2FA apps. + Additional PIN at token end + Additional digits that should be memorised and glued at end of each generated One Time Password + User name for Authenticator + User name displayed along with generated OTP on Authenticator App + + Operations Log + Authenticator + + OTP to check: + Reset Authenticators + Reset Authenticator Key + Are you sure to reset Authenticator key? It will render all currently configured Authenticators invalid, and you will need to set them up again. + New Authenticator Key was generated! Please use updated QRCode to provision authenticators. + + 1. Install Authenticator + 2. Scan code to setup AndroidAPS OTP codes + 3. Test One-Time-Password + Reset Authenticators + + On each follower phone install Authenticator app that support RFC 6238 TOTP tokens. Popular free apps are:\n • Google Authenticator\n • LastPass Authenticator\n • FreeOTP Authenticator + DO NOT SHARE this code online!\nUse it only to setup Authenticator App on follower phones. + By reseting authenticator you make all already provisioned authenticators invalid. You will need to set up them again! diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index d2cacff3b7..68b00e21c4 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -49,4 +49,24 @@ + + + diff --git a/app/src/main/res/values/validator.xml b/app/src/main/res/values/validator.xml index cc05a05625..15c40b743c 100644 --- a/app/src/main/res/values/validator.xml +++ b/app/src/main/res/values/validator.xml @@ -17,6 +17,7 @@ Must be 4 digit number Must be 6 digit number Not a minimum length + Pin should be 3 to 6 digits, not same or in series ^\\d{4} diff --git a/app/src/main/res/xml/pref_smscommunicator.xml b/app/src/main/res/xml/pref_smscommunicator.xml index 1ac0971a42..8b9f6f429c 100644 --- a/app/src/main/res/xml/pref_smscommunicator.xml +++ b/app/src/main/res/xml/pref_smscommunicator.xml @@ -27,6 +27,25 @@ validate:minNumber="3" validate:testType="numericRange" /> + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/info/AAPSMocker.java b/app/src/test/java/info/AAPSMocker.java index 0a11093683..b0d3ec038c 100644 --- a/app/src/test/java/info/AAPSMocker.java +++ b/app/src/test/java/info/AAPSMocker.java @@ -9,6 +9,7 @@ import org.json.JSONObject; import org.junit.Assert; import org.powermock.api.mockito.PowerMockito; +import java.util.Base64; import java.util.Locale; import info.nightscout.androidaps.Constants; @@ -27,6 +28,7 @@ import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobCalculatorP import info.nightscout.androidaps.plugins.treatments.TreatmentService; import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin; import info.nightscout.androidaps.queue.CommandQueue; +import info.nightscout.androidaps.utils.OneTimePassword; import info.nightscout.androidaps.utils.SP; import static org.mockito.ArgumentMatchers.any; @@ -294,4 +296,7 @@ public class AAPSMocker { return iobCobCalculatorPlugin; } + public static void mockOTP() { + PowerMockito.when(OneTimePassword.getInstance().isEnabled()).thenReturn(false); + } } diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/general/smsCommunicator/SmsCommunicatorPluginTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/general/smsCommunicator/SmsCommunicatorPluginTest.java index 7ff422d736..24434f632b 100644 --- a/app/src/test/java/info/nightscout/androidaps/plugins/general/smsCommunicator/SmsCommunicatorPluginTest.java +++ b/app/src/test/java/info/nightscout/androidaps/plugins/general/smsCommunicator/SmsCommunicatorPluginTest.java @@ -2,6 +2,8 @@ package info.nightscout.androidaps.plugins.general.smsCommunicator; import android.telephony.SmsManager; +import com.google.firebase.auth.OAuthCredential; + import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -41,6 +43,7 @@ import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin; import info.nightscout.androidaps.queue.Callback; import info.nightscout.androidaps.queue.CommandQueue; import info.nightscout.androidaps.utils.DateUtil; +import info.nightscout.androidaps.utils.OneTimePassword; import info.nightscout.androidaps.utils.SP; import info.nightscout.androidaps.utils.T; import info.nightscout.androidaps.utils.XdripCalibrations; @@ -68,6 +71,7 @@ import static org.powermock.api.mockito.PowerMockito.when; public class SmsCommunicatorPluginTest { private SmsCommunicatorPlugin smsCommunicatorPlugin; + private OneTimePassword otp; private LoopPlugin loopPlugin; private boolean hasBeenRun = false; @@ -87,7 +91,7 @@ public class SmsCommunicatorPluginTest { Assert.assertTrue(smsCommunicatorPlugin.isCommand("BOLUS", "")); smsCommunicatorPlugin.setMessageToConfirm(null); Assert.assertFalse(smsCommunicatorPlugin.isCommand("BLB", "")); - smsCommunicatorPlugin.setMessageToConfirm(new AuthRequest(smsCommunicatorPlugin, new Sms("1234", "ddd"), "RequestText", "ccode", new SmsAction() { + smsCommunicatorPlugin.setMessageToConfirm(new AuthRequest(smsCommunicatorPlugin, new Sms("1234", "ddd"), otp, "RequestText", "ccode", new SmsAction() { @Override public void run() { } @@ -892,6 +896,7 @@ public class SmsCommunicatorPluginTest { AAPSMocker.mockConfigBuilder(); AAPSMocker.mockCommandQueue(); AAPSMocker.mockNSUpload(); + AAPSMocker.mockOTP(); ConstraintChecker constraintChecker = AAPSMocker.mockConstraintsChecker(); BgReading reading = new BgReading();