better password processing, master password to setup wizard

This commit is contained in:
Milos Kozak 2020-04-21 14:50:28 +02:00
parent 8cc51abae9
commit d2c9f13932
11 changed files with 148 additions and 27 deletions

View file

@ -322,7 +322,9 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang
context?.let { context ->
if (preference != null) {
if (preference.key == resourceHelper.gs(R.string.key_master_password)) {
passwordCheck.setPassword(context, R.string.master_password, R.string.key_master_password)
passwordCheck.queryPassword(context, R.string.current_master_password, R.string.key_master_password, {
passwordCheck.setPassword(context, R.string.master_password, R.string.key_master_password)
})
return true
}
if (preference.key == resourceHelper.gs(R.string.key_settings_password)) {

View file

@ -165,6 +165,7 @@ interface AppComponent : AndroidInjector<MainApp> {
fun injectSWButton(swButton: SWButton)
fun injectSWEditNumberWithUnits(swEditNumberWithUnits: SWEditNumberWithUnits)
fun injectSWEditString(swEditString: SWEditString)
fun injectSWEditEncryptedPassword(swSWEditEncryptedPassword: SWEditEncryptedPassword)
fun injectSWEditUrl(swEditUrl: SWEditUrl)
fun injectSWFragment(swFragment: SWFragment)
fun injectSSWHtmlLink(swHtmlLink: SWHtmlLink)

View file

@ -246,6 +246,7 @@ open class AppModule {
@ContributesAndroidInjector fun swButtonInjector(): SWButton
@ContributesAndroidInjector fun swEditNumberWithUnitsInjector(): SWEditNumberWithUnits
@ContributesAndroidInjector fun swEditStringInjector(): SWEditString
@ContributesAndroidInjector fun swEditEncryptedPasswordInjector(): SWEditEncryptedPassword
@ContributesAndroidInjector fun swEditUrlInjector(): SWEditUrl
@ContributesAndroidInjector fun swFragmentInjector(): SWFragment
@ContributesAndroidInjector fun swHtmlLinkInjector(): SWHtmlLink

View file

@ -33,6 +33,7 @@ import info.nightscout.androidaps.plugins.profile.ns.NSProfilePlugin
import info.nightscout.androidaps.setupwizard.elements.*
import info.nightscout.androidaps.setupwizard.events.EventSWUpdate
import info.nightscout.androidaps.utils.AndroidPermission
import info.nightscout.androidaps.utils.CryptoUtil
import info.nightscout.androidaps.utils.LocaleHelper.update
import info.nightscout.androidaps.utils.extensions.isRunningTest
import info.nightscout.androidaps.utils.protection.ProtectionCheck
@ -60,7 +61,8 @@ class SWDefinition @Inject constructor(
private val nsProfilePlugin: NSProfilePlugin,
private val protectionCheck: ProtectionCheck,
private val importExportPrefs: ImportExportPrefs,
private val androidPermission: AndroidPermission
private val androidPermission: AndroidPermission,
private val cryptoUtil: CryptoUtil
) {
lateinit var activity: AppCompatActivity
@ -203,9 +205,18 @@ class SWDefinition @Inject constructor(
.add(SWInfotext(injector)
.label(R.string.patient_name_summary))
.add(SWEditString(injector)
.validator(SWTextValidator { text: String -> text.length > 0 })
.preferenceId(R.string.key_patient_name)
.updateDelay(5))
.validator(SWTextValidator(String::isNotEmpty))
.preferenceId(R.string.key_patient_name))
private val screenMasterPassword = SWScreen(injector, R.string.master_password)
.skippable(false)
.add(SWInfotext(injector)
.label(R.string.master_password))
.add(SWEditEncryptedPassword(injector, cryptoUtil)
.preferenceId(R.string.key_master_password))
.add(SWBreak(injector))
.add(SWInfotext(injector)
.label(R.string.master_password_summary))
.validator(SWValidator { !cryptoUtil.checkPassword("", sp.getString(R.string.key_master_password, "")) })
private val screenAge = SWScreen(injector, R.string.patientage)
.skippable(false)
.add(SWBreak(injector))
@ -393,6 +404,7 @@ class SWDefinition @Inject constructor(
.add(if (isRunningTest()) null else screenPermissionBattery) // cannot mock ask battery optimization
.add(screenPermissionBt)
.add(screenPermissionStore)
.add(screenMasterPassword)
.add(screenImport)
.add(screenUnits)
.add(displaySettings)
@ -420,6 +432,7 @@ class SWDefinition @Inject constructor(
.add(if (isRunningTest()) null else screenPermissionBattery) // cannot mock ask battery optimization
.add(screenPermissionBt)
.add(screenPermissionStore)
.add(screenMasterPassword)
.add(screenImport)
.add(screenUnits)
.add(displaySettings)
@ -442,6 +455,7 @@ class SWDefinition @Inject constructor(
.add(screenEula)
.add(if (isRunningTest()) null else screenPermissionBattery) // cannot mock ask battery optimization
.add(screenPermissionStore)
.add(screenMasterPassword)
.add(screenImport)
.add(screenUnits)
.add(displaySettings)

View file

@ -0,0 +1,82 @@
package info.nightscout.androidaps.setupwizard.elements
import android.graphics.Typeface
import android.text.Editable
import android.text.InputType
import android.text.TextWatcher
import android.view.View
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import dagger.android.HasAndroidInjector
import info.nightscout.androidaps.R
import info.nightscout.androidaps.setupwizard.SWTextValidator
import info.nightscout.androidaps.utils.CryptoUtil
class SWEditEncryptedPassword(injector: HasAndroidInjector, private val cryptoUtil: CryptoUtil) : SWItem(injector, Type.STRING) {
private var validator: SWTextValidator = SWTextValidator(String::isNotEmpty)
private var updateDelay = 0L
override fun generateDialog(layout: LinearLayout) {
val context = layout.context
val l = TextView(context)
l.id = View.generateViewId()
label?.let { l.setText(it) }
l.setTypeface(l.typeface, Typeface.BOLD)
layout.addView(l)
val c = TextView(context)
c.id = View.generateViewId()
comment?.let { c.setText(it) }
c.setTypeface(c.typeface, Typeface.ITALIC)
layout.addView(c)
val editText = EditText(context)
editText.id = View.generateViewId()
editText.inputType = InputType.TYPE_CLASS_TEXT
editText.maxLines = 1
editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
layout.addView(editText)
val c2 = TextView(context)
c2.id = View.generateViewId()
c2.setText(R.string.confirm)
layout.addView(c2)
val editText2 = EditText(context)
editText2.id = View.generateViewId()
editText2.inputType = InputType.TYPE_CLASS_TEXT
editText2.maxLines = 1
editText2.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
layout.addView(editText2)
super.generateDialog(layout)
val watcher = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
sp.remove(preferenceId)
scheduleChange(updateDelay)
if (validator.isValid(editText.text.toString()) && validator.isValid(editText2.text.toString()) && editText.text.toString() == editText2.text.toString())
save(s.toString(), updateDelay)
}
override fun afterTextChanged(s: Editable) {}
}
editText.addTextChangedListener(watcher)
editText2.addTextChangedListener(watcher)
}
fun preferenceId(preferenceId: Int): SWEditEncryptedPassword {
this.preferenceId = preferenceId
return this
}
fun validator(validator: SWTextValidator): SWEditEncryptedPassword {
this.validator = validator
return this
}
override fun save(value: String, updateDelay: Long) {
sp.putString(preferenceId, cryptoUtil.hashPassword(value))
scheduleChange(updateDelay)
}
}

View file

@ -10,6 +10,7 @@ import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.bus.RxBusWrapper
import info.nightscout.androidaps.setupwizard.events.EventSWUpdate
import info.nightscout.androidaps.utils.resources.ResourceHelper
import info.nightscout.androidaps.utils.sharedPreferences.SP
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
@ -20,7 +21,7 @@ open class SWItem(val injector: HasAndroidInjector, var type: Type) {
@Inject lateinit var aapsLogger: AAPSLogger
@Inject lateinit var rxBus: RxBusWrapper
@Inject lateinit var resourceHelper: ResourceHelper
@Inject lateinit var sp: info.nightscout.androidaps.utils.sharedPreferences.SP
@Inject lateinit var sp: SP
private val eventWorker = Executors.newSingleThreadScheduledExecutor()
private var scheduledEventPost: ScheduledFuture<*>? = null
@ -55,7 +56,7 @@ open class SWItem(val injector: HasAndroidInjector, var type: Type) {
return this
}
fun save(value: String, updateDelay: Long) {
open fun save(value: String, updateDelay: Long) {
sp.putString(preferenceId, value)
scheduleChange(updateDelay)
}
@ -69,7 +70,7 @@ open class SWItem(val injector: HasAndroidInjector, var type: Type) {
open fun generateDialog(layout: LinearLayout) {}
open fun processVisibility() {}
private fun scheduleChange(updateDelay: Long) {
fun scheduleChange(updateDelay: Long) {
class PostRunnable : Runnable {
override fun run() {
aapsLogger.debug(LTag.CORE, "Firing EventPreferenceChange")

View file

@ -8,7 +8,7 @@ import info.nightscout.androidaps.utils.ToastUtils
import java.util.concurrent.Executors
object BiometricCheck {
fun biometricPrompt(activity: FragmentActivity, title: Int, ok: Runnable?, cancel: Runnable? = null, fail: Runnable? = null) {
fun biometricPrompt(activity: FragmentActivity, title: Int, ok: Runnable?, cancel: Runnable? = null, fail: Runnable? = null, passwordCheck: PasswordCheck) {
val executor = Executors.newSingleThreadExecutor()
val biometricPrompt = BiometricPrompt(activity, executor, object : BiometricPrompt.AuthenticationCallback() {
@ -23,15 +23,19 @@ object BiometricCheck {
BiometricConstants.ERROR_LOCKOUT_PERMANENT,
BiometricConstants.ERROR_USER_CANCELED -> {
ToastUtils.showToastInUiThread(activity.baseContext, errString.toString())
fail?.run()
// fallback to master password
passwordCheck.queryPassword(activity, R.string.master_password, R.string.key_master_password, { ok?.run() }, { cancel?.run() }, { fail?.run() })
}
BiometricConstants.ERROR_NEGATIVE_BUTTON ->
cancel?.run()
BiometricConstants.ERROR_NO_DEVICE_CREDENTIAL ->
// call ok, because it's not possible to bypass it when biometrics is setup, hw not present and no pin set
ok?.run()
BiometricConstants.ERROR_NO_DEVICE_CREDENTIAL -> {
ToastUtils.showToastInUiThread(activity.baseContext, errString.toString())
// no pin set
// fallback to master password
passwordCheck.queryPassword(activity, R.string.master_password, R.string.key_master_password, { ok?.run() }, { cancel?.run() }, { fail?.run() })
}
BiometricConstants.ERROR_NO_SPACE,
BiometricConstants.ERROR_HW_UNAVAILABLE,

View file

@ -16,19 +16,19 @@ import javax.inject.Inject
import javax.inject.Singleton
// since androidx.autofill.HintConstants are not available
val AUTOFILL_HINT_NEW_PASSWORD = "newPassword"
const val AUTOFILL_HINT_NEW_PASSWORD = "newPassword"
@Singleton
class PasswordCheck @Inject constructor(
val sp: SP,
val cryptoUtil: CryptoUtil
private val cryptoUtil: CryptoUtil
) {
@SuppressLint("InflateParams")
fun queryPassword(context: Context, @StringRes labelId: Int, @StringRes preference: Int, ok: ( (String) -> Unit)?, cancel: (()->Unit)? = null, fail: (()->Unit)? = null) {
fun queryPassword(context: Context, @StringRes labelId: Int, @StringRes preference: Int, ok: ((String) -> Unit)?, cancel: (() -> Unit)? = null, fail: (() -> Unit)? = null) {
val password = sp.getString(preference, "")
if (password == "") {
ok?.invoke("")
ok?.invoke("")
return
}
@ -36,7 +36,11 @@ class PasswordCheck @Inject constructor(
val alertDialogBuilder = AlertDialogHelper.Builder(context)
alertDialogBuilder.setView(promptsView)
val userInput = promptsView.findViewById<View>(R.id.passwordprompt_pass) as EditText
val userInput = promptsView.findViewById<View>(R.id.password_prompt_pass) as EditText
val userInput2 = promptsView.findViewById<View>(R.id.password_prompt_pass_confirm) as EditText
userInput2.visibility = View.GONE
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val autoFillHintPasswordKind = context.getString(preference)
userInput.setAutofillHints(View.AUTOFILL_HINT_PASSWORD, "aaps_${autoFillHintPasswordKind}")
@ -64,12 +68,13 @@ class PasswordCheck @Inject constructor(
}
@SuppressLint("InflateParams")
fun setPassword(context: Context, @StringRes labelId: Int, @StringRes preference: Int, ok: ( (String) -> Unit)? = null, cancel: (()->Unit)? = null, clear: (()->Unit)? = null) {
fun setPassword(context: Context, @StringRes labelId: Int, @StringRes preference: Int, ok: ((String) -> Unit)? = null, cancel: (() -> Unit)? = null, clear: (() -> Unit)? = null) {
val promptsView = LayoutInflater.from(context).inflate(R.layout.passwordprompt, null)
val alertDialogBuilder = AlertDialogHelper.Builder(context)
alertDialogBuilder.setView(promptsView)
val userInput = promptsView.findViewById<View>(R.id.passwordprompt_pass) as EditText
val userInput = promptsView.findViewById<View>(R.id.password_prompt_pass) as EditText
val userInput2 = promptsView.findViewById<View>(R.id.password_prompt_pass_confirm) as EditText
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val autoFillHintPasswordKind = context.getString(preference)
@ -82,7 +87,10 @@ class PasswordCheck @Inject constructor(
.setCustomTitle(AlertDialogHelper.buildCustomTitle(context, context.getString(labelId), R.drawable.ic_header_key))
.setPositiveButton(context.getString(R.string.ok)) { _, _ ->
val enteredPassword = userInput.text.toString()
if (enteredPassword.isNotEmpty()) {
val enteredPassword2 = userInput2.text.toString()
if (enteredPassword != enteredPassword2) {
ToastUtils.errorToast(context, context.getString(R.string.passwords_dont_match))
} else if (enteredPassword.isNotEmpty()) {
sp.putString(preference, cryptoUtil.hashPassword(enteredPassword))
ToastUtils.okToast(context, context.getString(R.string.password_set))
ok?.invoke(enteredPassword)

View file

@ -56,7 +56,7 @@ class ProtectionCheck @Inject constructor(
ProtectionType.NONE ->
ok?.run()
ProtectionType.BIOMETRIC ->
BiometricCheck.biometricPrompt(activity, titleResourceIDs[protection.ordinal], ok, cancel, fail)
BiometricCheck.biometricPrompt(activity, titleResourceIDs[protection.ordinal], ok, cancel, fail, passwordCheck)
ProtectionType.MASTER_PASSWORD ->
passwordCheck.queryPassword(activity, R.string.master_password, R.string.key_master_password, { ok?.run() }, { cancel?.run() }, { fail?.run() })
ProtectionType.CUSTOM_PASSWORD ->

View file

@ -3,18 +3,23 @@
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:padding="10dp" >
android:padding="10dp">
<EditText
android:id="@+id/passwordprompt_pass"
android:id="@+id/password_prompt_pass"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password_hint"
android:inputType="textPassword"
>
android:inputType="textPassword">
<requestFocus />
</EditText>
<EditText
android:id="@+id/password_prompt_pass_confirm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password_hint"
android:inputType="textPassword"/>
</LinearLayout>

View file

@ -1805,4 +1805,7 @@
<string name="key_graphconfig" translatable="false">graphconfig</string>
<string name="authorizationfailed">Authorization failed</string>
<string name="overview_show_absinsulin">Absolute insulin</string>
<string name="master_password_summary">Master password is used for backup encryption and to override security in application. Remember it or store on a safe place.</string>
<string name="passwords_dont_match">Passwords don\'t match</string>
<string name="current_master_password">Current master password</string>
</resources>