better password processing, master password to setup wizard
This commit is contained in:
parent
8cc51abae9
commit
d2c9f13932
11 changed files with 148 additions and 27 deletions
|
@ -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)) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue