commit
9f0ac545f3
21 changed files with 523 additions and 17 deletions
|
@ -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"
|
||||
|
|
|
@ -272,6 +272,13 @@
|
|||
android:label="@string/pairing_information"
|
||||
android:theme="@style/AppTheme" />
|
||||
<activity android:name=".activities.RequestDexcomPermissionActivity" />
|
||||
<activity android:name=".plugins.general.smsCommunicator.activities.SmsCommunicatorOtpActivity">
|
||||
<intent-filter>
|
||||
<action android:name="info.nightscout.androidaps.plugins.general.smsCommunicator.activities.SmsCommunicatorOtpActivity" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Medtronic service and activities -->
|
||||
<service
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import info.nightscout.androidaps.activities.*
|
|||
import info.nightscout.androidaps.historyBrowser.HistoryBrowseActivity
|
||||
import info.nightscout.androidaps.plugins.general.maintenance.activities.LogSettingActivity
|
||||
import info.nightscout.androidaps.plugins.general.overview.activities.QuickWizardListActivity
|
||||
import info.nightscout.androidaps.plugins.general.smsCommunicator.activities.SmsCommunicatorOtpActivity
|
||||
import info.nightscout.androidaps.plugins.pump.common.dialog.RileyLinkBLEScanActivity
|
||||
import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.dialog.RileyLinkStatusActivity
|
||||
import info.nightscout.androidaps.plugins.pump.danaR.activities.DanaRHistoryActivity
|
||||
|
@ -43,6 +44,7 @@ abstract class ActivitiesModule {
|
|||
@ContributesAndroidInjector abstract fun contributesRileyLinkBLEScanActivity(): RileyLinkBLEScanActivity
|
||||
@ContributesAndroidInjector abstract fun contributesSetupWizardActivity(): SetupWizardActivity
|
||||
@ContributesAndroidInjector abstract fun contributesSingleFragmentActivity(): SingleFragmentActivity
|
||||
@ContributesAndroidInjector abstract fun contributesSmsCommunicatorOtpActivity(): SmsCommunicatorOtpActivity
|
||||
@ContributesAndroidInjector abstract fun contributesStatsActivity(): StatsActivity
|
||||
@ContributesAndroidInjector abstract fun contributesSurveyActivity(): SurveyActivity
|
||||
@ContributesAndroidInjector abstract fun contributesTDDStatsActivity(): TDDStatsActivity
|
||||
|
|
|
@ -74,9 +74,7 @@ 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 contributesTidepoolFragment(): TidepoolFragment
|
||||
@ContributesAndroidInjector abstract fun contributesTreatmentsFragment(): TreatmentsFragment
|
||||
|
|
|
@ -6,6 +6,8 @@ import info.nightscout.androidaps.R
|
|||
import info.nightscout.androidaps.logging.AAPSLogger
|
||||
import info.nightscout.androidaps.logging.LTag
|
||||
import info.nightscout.androidaps.utils.DateUtil
|
||||
import info.nightscout.androidaps.plugins.general.smsCommunicator.otp.OneTimePassword
|
||||
import info.nightscout.androidaps.plugins.general.smsCommunicator.otp.OneTimePasswordValidationResult
|
||||
import info.nightscout.androidaps.utils.resources.ResourceHelper
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -19,6 +21,7 @@ class AuthRequest internal constructor(
|
|||
@Inject lateinit var aapsLogger: AAPSLogger
|
||||
@Inject lateinit var smsCommunicatorPlugin: SmsCommunicatorPlugin
|
||||
@Inject lateinit var resourceHelper: ResourceHelper
|
||||
@Inject lateinit var otp : OneTimePassword
|
||||
|
||||
private val date = DateUtil.now()
|
||||
private var processed = false
|
||||
|
@ -28,12 +31,20 @@ class AuthRequest internal constructor(
|
|||
smsCommunicatorPlugin.sendSMS(Sms(requester.phoneNumber, requestText))
|
||||
}
|
||||
|
||||
private fun codeIsValid(toValidate: String) : Boolean {
|
||||
return if (otp.isEnabled()) {
|
||||
otp.checkOTP(toValidate) == OneTimePasswordValidationResult.OK
|
||||
} else {
|
||||
confirmCode == toValidate
|
||||
}
|
||||
}
|
||||
|
||||
fun action(codeReceived: String) {
|
||||
if (processed) {
|
||||
aapsLogger.debug(LTag.SMS, "Already processed")
|
||||
return
|
||||
}
|
||||
if (confirmCode != codeReceived) {
|
||||
if (!codeIsValid(codeReceived)) {
|
||||
processed = true
|
||||
aapsLogger.debug(LTag.SMS, "Wrong code")
|
||||
smsCommunicatorPlugin.sendSMS(Sms(requester.phoneNumber, resourceHelper.gs(R.string.sms_wrongcode)))
|
||||
|
|
|
@ -28,6 +28,7 @@ import info.nightscout.androidaps.plugins.general.nsclient.events.EventNSClientR
|
|||
import info.nightscout.androidaps.plugins.general.overview.events.EventNewNotification
|
||||
import info.nightscout.androidaps.plugins.general.overview.notifications.Notification
|
||||
import info.nightscout.androidaps.plugins.general.smsCommunicator.events.EventSmsCommunicatorUpdateGui
|
||||
import info.nightscout.androidaps.plugins.general.smsCommunicator.otp.OneTimePassword
|
||||
import info.nightscout.androidaps.plugins.iob.iobCobCalculator.GlucoseStatus
|
||||
import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobCalculatorPlugin
|
||||
import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin
|
||||
|
@ -58,7 +59,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)
|
||||
|
@ -898,6 +900,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()
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
package info.nightscout.androidaps.plugins.general.smsCommunicator.activities
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import com.google.common.primitives.Ints.min
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||
import info.nightscout.androidaps.R
|
||||
import info.nightscout.androidaps.activities.NoSplashAppCompatActivity
|
||||
import info.nightscout.androidaps.plugins.bus.RxBusWrapper
|
||||
import info.nightscout.androidaps.plugins.general.smsCommunicator.SmsCommunicatorPlugin
|
||||
import info.nightscout.androidaps.plugins.general.smsCommunicator.otp.OneTimePassword
|
||||
import info.nightscout.androidaps.plugins.general.smsCommunicator.otp.OneTimePasswordValidationResult
|
||||
import info.nightscout.androidaps.utils.FabricPrivacy
|
||||
import info.nightscout.androidaps.utils.OKDialog
|
||||
import info.nightscout.androidaps.utils.ToastUtils
|
||||
import info.nightscout.androidaps.utils.resources.ResourceHelper
|
||||
import kotlinx.android.synthetic.main.activity_smscommunicator_otp.*
|
||||
import net.glxn.qrgen.android.QRCode
|
||||
import javax.inject.Inject
|
||||
|
||||
class SmsCommunicatorOtpActivity : NoSplashAppCompatActivity() {
|
||||
@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 onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_smscommunicator_otp)
|
||||
|
||||
smscommunicator_otp_verify_edit.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
OKDialog.showConfirmation(this,
|
||||
resourceHelper.gs(R.string.smscommunicator_otp_reset_title),
|
||||
resourceHelper.gs(R.string.smscommunicator_otp_reset_prompt),
|
||||
Runnable {
|
||||
otp.ensureKey(true)
|
||||
updateGui()
|
||||
ToastUtils.showToastInUiThread(this, R.string.smscommunicator_otp_reset_successful)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
updateGui()
|
||||
}
|
||||
|
||||
fun updateGui() {
|
||||
val displayMetrics = Resources.getSystem().displayMetrics
|
||||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
package info.nightscout.androidaps.plugins.general.smsCommunicator.otp
|
||||
|
||||
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.DateUtil
|
||||
import info.nightscout.androidaps.utils.resources.ResourceHelper
|
||||
import info.nightscout.androidaps.utils.sharedPreferences.SP
|
||||
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, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).replace(":", "").trim()
|
||||
if (userName.isEmpty())
|
||||
userName = defaultUserName
|
||||
return userName
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure if private key for TOTP is generated, creating it when necessary or requested
|
||||
*/
|
||||
fun ensureKey(forceNewKey: Boolean = false) {
|
||||
val keyBytes: ByteArray
|
||||
val strSecret = sp.getString(R.string.key_smscommunicator_otp_secret, "").trim()
|
||||
if (strSecret.isEmpty() || forceNewKey) {
|
||||
val keyGenerator = KeyGenerator.getInstance(totp.algorithm)
|
||||
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_password, "").trim()
|
||||
}
|
||||
|
||||
private fun generateOneTimePassword(counter: Long): String =
|
||||
key?.let { String.format("%06d", totp.generateOneTimePassword(key, counter)) } ?: ""
|
||||
|
||||
/**
|
||||
* 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) != pin) {
|
||||
return OneTimePasswordValidationResult.ERROR_WRONG_PIN
|
||||
}
|
||||
|
||||
val counter: Long = DateUtil.now() / 30000L
|
||||
|
||||
val acceptableTokens: MutableList<String> = 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 == candidate }) {
|
||||
return OneTimePasswordValidationResult.OK
|
||||
}
|
||||
|
||||
return OneTimePasswordValidationResult.ERROR_WRONG_OTP
|
||||
}
|
||||
|
||||
/**
|
||||
* Return URI used to provision Authenticator apps
|
||||
*/
|
||||
fun provisioningURI(): String? =
|
||||
key?.let { "otpauth://totp/AndroidAPS:" + URLEncoder.encode(name(), "utf-8") + "?secret=" + BaseEncoding.base32().encode(it.encoded).replace("=", "") + "&issuer=AndroidAPS" }
|
||||
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package info.nightscout.androidaps.plugins.general.smsCommunicator.otp
|
||||
|
||||
enum class OneTimePasswordValidationResult {
|
||||
OK,
|
||||
ERROR_WRONG_LENGTH,
|
||||
ERROR_WRONG_PIN,
|
||||
ERROR_WRONG_OTP
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -93,4 +93,6 @@ public interface EditTextValidator {
|
|||
|
||||
int TEST_MULTI_PHONE = 19;
|
||||
|
||||
int TEST_PIN_STRENGTH = 20;
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
104
app/src/main/res/layout/activity_smscommunicator_otp.xml
Normal file
104
app/src/main/res/layout/activity_smscommunicator_otp.xml
Normal file
|
@ -0,0 +1,104 @@
|
|||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".plugins.general.smsCommunicator.activities.SmsCommunicatorOtpActivity">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/smscommunicator_otp_layout"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
style="@style/section_header_label"
|
||||
android:text="@string/smscommunicator_otp_step1_install_header" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5sp"
|
||||
android:layout_marginBottom="5sp"
|
||||
android:paddingStart="15sp"
|
||||
android:paddingEnd="15sp"
|
||||
android:text="@string/smscommunicator_otp_install_info" />
|
||||
|
||||
<TextView
|
||||
style="@style/section_header_label"
|
||||
android:text="@string/smscommunicator_otp_step2_provisioning_header" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/smscommunicator_otp_provisioning"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:scaleType="center" />
|
||||
|
||||
<TextView
|
||||
style="@style/warning_label"
|
||||
android:text="" />
|
||||
|
||||
<TextView
|
||||
style="@style/section_header_label"
|
||||
android:text="@string/smscommunicator_otp_step3_test_header" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="15dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:text="@string/smscommunicator_otp_verify_label" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/smscommunicator_otp_verify_edit"
|
||||
android:layout_width="140sp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="000 000 000"
|
||||
android:inputType="number"
|
||||
android:maxLength="12"
|
||||
android:textAlignment="center"
|
||||
android:textSize="19sp"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/smscommunicator_otp_verify_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="15dp"
|
||||
android:paddingEnd="5dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
style="@style/section_header_label"
|
||||
android:text="@string/smscommunicator_otp_reset_header" />
|
||||
|
||||
<TextView
|
||||
style="@style/warning_label"
|
||||
android:text="@string/smscommunicator_otp_reset_warning" />
|
||||
|
||||
<info.nightscout.androidaps.utils.SingleClickButton
|
||||
android:id="@+id/actions_smscommunicator_otp_reset"
|
||||
style="?android:attr/buttonStyle"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginTop="3dp"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_marginBottom="3dp"
|
||||
android:text="@string/smscommunicator_otp_reset_btn"
|
||||
android:textColor="@color/colorTreatmentButton" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
</FrameLayout>
|
|
@ -23,6 +23,7 @@
|
|||
<enum name="httpsUrl" value="17" />
|
||||
<enum name="minLength" value="18" />
|
||||
<enum name="multiPhone" value="19" />
|
||||
<enum name="pinStrength" value="20" />
|
||||
</attr>
|
||||
<attr name="testErrorString" format="string" />
|
||||
<attr name="emptyErrorString" format="string" />
|
||||
|
|
|
@ -1703,4 +1703,37 @@
|
|||
<string name="loop_tbrexecution_time_label">Temp basal execution time</string>
|
||||
<string name="insight_alert_notification_channel">Insight Pump Alerts</string>
|
||||
|
||||
<!-- SMS Communicator & OTP Authenticator -->
|
||||
|
||||
<string name="key_smscommunicator_otp_enabled" translatable="false">smscommunicator_otp_enabled</string>
|
||||
<string name="key_smscommunicator_otp_name" translatable="false">smscommunicator_otp_name</string>
|
||||
<string name="key_smscommunicator_otp_password" translatable="false">smscommunicator_otp_password</string>
|
||||
<string name="key_smscommunicator_otp_secret" translatable="false">smscommunicator_otp_secret</string>
|
||||
|
||||
<string name="smscommunicator_default_user_display_name" comment="This is default user display name, shown by Authenticators">User</string>
|
||||
<string name="smscommunicator_code_from_authenticator_for" comment="This is continuation of sentence: To [ACTION] reply with code">from Authenticator app for: %1$s</string>
|
||||
|
||||
<string name="smscommunicator_otp_enabled">Enable Authenticator</string>
|
||||
<string name="smscommunicator_otp_enabled_summary">Authenticate commands using One Time Passwords generated by Google Authenticator or similar 2FA apps.</string>
|
||||
<string name="smscommunicator_otp_pin">Additional PIN at token end</string>
|
||||
<string name="smscommunicator_otp_pin_summary">Additional digits that should be memorised and glued at end of each generated One Time Password</string>
|
||||
<string name="smscommunicator_otp_name">User name for Authenticator</string>
|
||||
<string name="smscommunicator_otp_name_summary">User name displayed along with generated OTP on Authenticator App</string>
|
||||
|
||||
<string name="smscomunicator_tab_otp_label">Authenticator setup</string>
|
||||
|
||||
<string name="smscommunicator_otp_verify_label">OTP to check:</string>
|
||||
<string name="smscommunicator_otp_reset_btn">Reset Authenticators</string>
|
||||
<string name="smscommunicator_otp_reset_title">Reset Authenticator Key</string>
|
||||
<string name="smscommunicator_otp_reset_prompt">Are you sure to reset Authenticator key? It will render all currently configured Authenticators invalid, and you will need to set them up again.</string>
|
||||
<string name="smscommunicator_otp_reset_successful">New Authenticator Key was generated! Please use updated QRCode to provision authenticators.</string>
|
||||
|
||||
<string name="smscommunicator_otp_step1_install_header">1. Install Authenticator</string>
|
||||
<string name="smscommunicator_otp_step2_provisioning_header">2. Scan code to setup AndroidAPS OTP codes</string>
|
||||
<string name="smscommunicator_otp_step3_test_header">3. Test One-Time-Password</string>
|
||||
<string name="smscommunicator_otp_reset_header">Reset Authenticators</string>
|
||||
|
||||
<string name="smscommunicator_otp_install_info">On each follower phone install Authenticator app that support RFC 6238 TOTP tokens. Popular free apps are:\n • Authy\n • Google Authenticator\n • LastPass Authenticator\n • FreeOTP Authenticator</string>
|
||||
<string name="smscommunicator_otp_provisioning_warning">DO NOT SHARE this code online!\nUse it only to setup Authenticator App on follower phones.</string>
|
||||
<string name="smscommunicator_otp_reset_warning">By reseting authenticator you make all already provisioned authenticators invalid. You will need to set up them again!</string>
|
||||
</resources>
|
||||
|
|
|
@ -47,4 +47,24 @@
|
|||
|
||||
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
|
||||
|
||||
<style name="section_header_label">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:layout_marginTop">15dp</item>
|
||||
<item name="android:paddingStart">15dp</item>
|
||||
<item name="android:paddingEnd">15dp</item>
|
||||
<item name="android:textColor">@color/colorAccent</item>
|
||||
</style>
|
||||
|
||||
<style name="warning_label">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:layout_marginTop">5dp</item>
|
||||
<item name="android:layout_marginBottom">5dp</item>
|
||||
<item name="android:paddingStart">15dp</item>
|
||||
<item name="android:paddingEnd">15dp</item>
|
||||
<item name="android:textAlignment">center</item>
|
||||
<item name="android:textColor">#ff0000</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
<string name="error_mustbe4digitnumber">Must be 4 digit number</string>
|
||||
<string name="error_mustbe6digitnumber">Must be 6 digit number</string>
|
||||
<string name="error_not_a_minimum_length">Not a minimum length</string>
|
||||
<string name="error_pin_not_valid">Pin should be 3 to 6 digits, not same or in series</string>
|
||||
|
||||
<string name="fourdigitnumber" translatable="false">^\\d{4}</string>
|
||||
|
||||
|
|
|
@ -27,6 +27,36 @@
|
|||
validate:minNumber="3"
|
||||
validate:testType="numericRange" />
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="true"
|
||||
android:enabled="false"
|
||||
android:key="@string/key_smscommunicator_otp_enabled"
|
||||
android:summary="@string/smscommunicator_otp_enabled_summary"
|
||||
android:title="@string/smscommunicator_otp_enabled"
|
||||
app:isPreferenceVisible="false" />
|
||||
|
||||
<Preference
|
||||
android:dependency="@string/key_smscommunicator_remotecommandsallowed"
|
||||
android:key="otpsetup"
|
||||
android:title="@string/smscomunicator_tab_otp_label">
|
||||
<intent android:action="info.nightscout.androidaps.plugins.general.smsCommunicator.activities.SmsCommunicatorOtpActivity" />
|
||||
</Preference>
|
||||
|
||||
<info.nightscout.androidaps.utils.textValidator.ValidatingEditTextPreference
|
||||
android:dependency="@string/key_smscommunicator_remotecommandsallowed"
|
||||
android:key="@string/key_smscommunicator_otp_password"
|
||||
android:summary="@string/smscommunicator_otp_pin_summary"
|
||||
android:title="@string/smscommunicator_otp_pin"
|
||||
validate:testType="pinStrength" />
|
||||
|
||||
<info.nightscout.androidaps.utils.textValidator.ValidatingEditTextPreference
|
||||
android:dependency="@string/key_smscommunicator_remotecommandsallowed"
|
||||
android:key="@string/key_smscommunicator_otp_name"
|
||||
android:summary="@string/smscommunicator_otp_name_summary"
|
||||
android:title="@string/smscommunicator_otp_name"
|
||||
validate:testType="personName" />
|
||||
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
|
@ -2,10 +2,11 @@ package info.nightscout.androidaps.plugins.general.smsCommunicator
|
|||
|
||||
import dagger.android.AndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import info.nightscout.androidaps.TestBase
|
||||
import info.nightscout.androidaps.Constants
|
||||
import info.nightscout.androidaps.R
|
||||
import info.nightscout.androidaps.TestBase
|
||||
import info.nightscout.androidaps.logging.AAPSLogger
|
||||
import info.nightscout.androidaps.plugins.general.smsCommunicator.otp.OneTimePassword
|
||||
import info.nightscout.androidaps.utils.DateUtil
|
||||
import info.nightscout.androidaps.utils.T
|
||||
import info.nightscout.androidaps.utils.resources.ResourceHelper
|
||||
|
@ -22,12 +23,13 @@ import org.powermock.core.classloader.annotations.PrepareForTest
|
|||
import org.powermock.modules.junit4.PowerMockRunner
|
||||
|
||||
@RunWith(PowerMockRunner::class)
|
||||
@PrepareForTest(SmsCommunicatorPlugin::class, DateUtil::class)
|
||||
@PrepareForTest(SmsCommunicatorPlugin::class, DateUtil::class, OneTimePassword::class)
|
||||
class AuthRequestTest : TestBase() {
|
||||
|
||||
@Mock lateinit var aapsLogger: AAPSLogger
|
||||
@Mock lateinit var smsCommunicatorPlugin: SmsCommunicatorPlugin
|
||||
@Mock lateinit var resourceHelper: ResourceHelper
|
||||
@Mock lateinit var otp: OneTimePassword
|
||||
|
||||
var injector: HasAndroidInjector = HasAndroidInjector {
|
||||
AndroidInjector {
|
||||
|
@ -35,6 +37,7 @@ class AuthRequestTest : TestBase() {
|
|||
it.aapsLogger = aapsLogger
|
||||
it.resourceHelper = resourceHelper
|
||||
it.smsCommunicatorPlugin = smsCommunicatorPlugin
|
||||
it.otp = otp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import android.telephony.SmsManager
|
|||
import dagger.android.AndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import info.nightscout.androidaps.Constants
|
||||
import info.nightscout.androidaps.MainApp
|
||||
import info.nightscout.androidaps.R
|
||||
import info.nightscout.androidaps.TestBaseWithProfile
|
||||
import info.nightscout.androidaps.data.IobTotal
|
||||
|
@ -15,23 +14,18 @@ import info.nightscout.androidaps.interfaces.CommandQueueProvider
|
|||
import info.nightscout.androidaps.interfaces.Constraint
|
||||
import info.nightscout.androidaps.interfaces.PluginType
|
||||
import info.nightscout.androidaps.plugins.aps.loop.LoopPlugin
|
||||
import info.nightscout.androidaps.plugins.bus.RxBusWrapper
|
||||
import info.nightscout.androidaps.plugins.configBuilder.ConstraintChecker
|
||||
import info.nightscout.androidaps.plugins.configBuilder.ProfileFunction
|
||||
import info.nightscout.androidaps.plugins.general.smsCommunicator.otp.OneTimePassword
|
||||
import info.nightscout.androidaps.plugins.iob.iobCobCalculator.CobInfo
|
||||
import info.nightscout.androidaps.plugins.iob.iobCobCalculator.GlucoseStatus
|
||||
import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobCalculatorPlugin
|
||||
import info.nightscout.androidaps.plugins.profile.local.LocalProfilePlugin
|
||||
import info.nightscout.androidaps.plugins.pump.virtual.VirtualPumpPlugin
|
||||
import info.nightscout.androidaps.plugins.treatments.TreatmentService
|
||||
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.DefaultValueHelper
|
||||
import info.nightscout.androidaps.utils.FabricPrivacy
|
||||
import info.nightscout.androidaps.utils.T
|
||||
import info.nightscout.androidaps.utils.XdripCalibrations
|
||||
import info.nightscout.androidaps.utils.*
|
||||
import info.nightscout.androidaps.utils.sharedPreferences.SP
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
|
@ -49,7 +43,7 @@ import org.powermock.modules.junit4.PowerMockRunner
|
|||
import java.util.*
|
||||
|
||||
@RunWith(PowerMockRunner::class)
|
||||
@PrepareForTest(ConstraintChecker::class, FabricPrivacy::class, VirtualPumpPlugin::class, XdripCalibrations::class, SmsManager::class, RxBusWrapper::class, CommandQueue::class, LocalProfilePlugin::class, DateUtil::class, IobCobCalculatorPlugin::class, MainApp::class)
|
||||
@PrepareForTest(ConstraintChecker::class, FabricPrivacy::class, VirtualPumpPlugin::class, XdripCalibrations::class, SmsManager::class, CommandQueue::class, LocalProfilePlugin::class, DateUtil::class, IobCobCalculatorPlugin::class, OneTimePassword::class)
|
||||
class SmsCommunicatorPluginTest : TestBaseWithProfile() {
|
||||
|
||||
@Mock lateinit var sp: SP
|
||||
|
@ -63,6 +57,7 @@ class SmsCommunicatorPluginTest : TestBaseWithProfile() {
|
|||
@Mock lateinit var defaultValueHelper: DefaultValueHelper
|
||||
@Mock lateinit var localProfilePlugin: LocalProfilePlugin
|
||||
@Mock lateinit var treatmentService: TreatmentService
|
||||
@Mock lateinit var otp: OneTimePassword
|
||||
|
||||
var injector: HasAndroidInjector = HasAndroidInjector {
|
||||
AndroidInjector {
|
||||
|
@ -80,6 +75,7 @@ class SmsCommunicatorPluginTest : TestBaseWithProfile() {
|
|||
it.aapsLogger = aapsLogger
|
||||
it.smsCommunicatorPlugin = smsCommunicatorPlugin
|
||||
it.resourceHelper = resourceHelper
|
||||
it.otp = otp
|
||||
}
|
||||
if (it is GlucoseStatus) {
|
||||
it.aapsLogger = aapsLogger
|
||||
|
@ -109,7 +105,7 @@ class SmsCommunicatorPluginTest : TestBaseWithProfile() {
|
|||
`when`(SmsManager.getDefault()).thenReturn(smsManager)
|
||||
`when`(sp.getString(R.string.key_smscommunicator_allowednumbers, "")).thenReturn("1234;5678")
|
||||
|
||||
smsCommunicatorPlugin = SmsCommunicatorPlugin(injector, aapsLogger, resourceHelper, sp, constraintChecker, rxBus, profileFunction, fabricPrivacy, activePlugin, commandQueue, loopPlugin, iobCobCalculatorPlugin)
|
||||
smsCommunicatorPlugin = SmsCommunicatorPlugin(injector, aapsLogger, resourceHelper, sp, constraintChecker, rxBus, profileFunction, fabricPrivacy, activePlugin, commandQueue, loopPlugin, iobCobCalculatorPlugin, otp)
|
||||
smsCommunicatorPlugin.setPluginEnabled(PluginType.GENERAL, true)
|
||||
Mockito.doAnswer { invocation: InvocationOnMock ->
|
||||
val callback = invocation.getArgument<Callback>(1)
|
||||
|
|
Loading…
Reference in a new issue