From d2c9f13932f9651ca007053ebba7f94bb123f08b Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 21 Apr 2020 14:50:28 +0200 Subject: [PATCH] better password processing, master password to setup wizard --- .../activities/MyPreferenceFragment.kt | 4 +- .../dependencyInjection/AppComponent.kt | 1 + .../dependencyInjection/AppModule.kt | 1 + .../androidaps/setupwizard/SWDefinition.kt | 22 ++++- .../elements/SWEditEncryptedPassword.kt | 82 +++++++++++++++++++ .../androidaps/setupwizard/elements/SWItem.kt | 7 +- .../utils/protection/BiometricCheck.kt | 14 ++-- .../utils/protection/PasswordCheck.kt | 24 ++++-- .../utils/protection/ProtectionCheck.kt | 2 +- app/src/main/res/layout/passwordprompt.xml | 15 ++-- app/src/main/res/values/strings.xml | 3 + 11 files changed, 148 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/info/nightscout/androidaps/setupwizard/elements/SWEditEncryptedPassword.kt diff --git a/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt b/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt index c5da7fb7b0..a037329110 100644 --- a/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt @@ -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)) { diff --git a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppComponent.kt b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppComponent.kt index 2bcf212b2b..008ad75057 100644 --- a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppComponent.kt +++ b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppComponent.kt @@ -165,6 +165,7 @@ interface AppComponent : AndroidInjector { 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) diff --git a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppModule.kt b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppModule.kt index 76ff40ab2a..3e342bb038 100644 --- a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppModule.kt @@ -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 diff --git a/app/src/main/java/info/nightscout/androidaps/setupwizard/SWDefinition.kt b/app/src/main/java/info/nightscout/androidaps/setupwizard/SWDefinition.kt index 4af38741b5..5fca6651da 100644 --- a/app/src/main/java/info/nightscout/androidaps/setupwizard/SWDefinition.kt +++ b/app/src/main/java/info/nightscout/androidaps/setupwizard/SWDefinition.kt @@ -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) diff --git a/app/src/main/java/info/nightscout/androidaps/setupwizard/elements/SWEditEncryptedPassword.kt b/app/src/main/java/info/nightscout/androidaps/setupwizard/elements/SWEditEncryptedPassword.kt new file mode 100644 index 0000000000..d32a0f4770 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/setupwizard/elements/SWEditEncryptedPassword.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/setupwizard/elements/SWItem.kt b/app/src/main/java/info/nightscout/androidaps/setupwizard/elements/SWItem.kt index 374c981a8f..48e40868c3 100644 --- a/app/src/main/java/info/nightscout/androidaps/setupwizard/elements/SWItem.kt +++ b/app/src/main/java/info/nightscout/androidaps/setupwizard/elements/SWItem.kt @@ -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") diff --git a/app/src/main/java/info/nightscout/androidaps/utils/protection/BiometricCheck.kt b/app/src/main/java/info/nightscout/androidaps/utils/protection/BiometricCheck.kt index 7a9018d132..e55d6e999e 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/protection/BiometricCheck.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/protection/BiometricCheck.kt @@ -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, diff --git a/app/src/main/java/info/nightscout/androidaps/utils/protection/PasswordCheck.kt b/app/src/main/java/info/nightscout/androidaps/utils/protection/PasswordCheck.kt index 2df55c9f2f..5f80c54480 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/protection/PasswordCheck.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/protection/PasswordCheck.kt @@ -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(R.id.passwordprompt_pass) as EditText + val userInput = promptsView.findViewById(R.id.password_prompt_pass) as EditText + val userInput2 = promptsView.findViewById(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(R.id.passwordprompt_pass) as EditText + val userInput = promptsView.findViewById(R.id.password_prompt_pass) as EditText + val userInput2 = promptsView.findViewById(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) diff --git a/app/src/main/java/info/nightscout/androidaps/utils/protection/ProtectionCheck.kt b/app/src/main/java/info/nightscout/androidaps/utils/protection/ProtectionCheck.kt index a1cca9116e..e8dd73d7fb 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/protection/ProtectionCheck.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/protection/ProtectionCheck.kt @@ -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 -> diff --git a/app/src/main/res/layout/passwordprompt.xml b/app/src/main/res/layout/passwordprompt.xml index ed2298e4d8..e363256be9 100644 --- a/app/src/main/res/layout/passwordprompt.xml +++ b/app/src/main/res/layout/passwordprompt.xml @@ -3,18 +3,23 @@ android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" - android:padding="10dp" > + android:padding="10dp"> + android:inputType="textPassword"> - + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 89bf2971fd..fe93118992 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1805,4 +1805,7 @@ graphconfig Authorization failed Absolute insulin + Master password is used for backup encryption and to override security in application. Remember it or store on a safe place. + Passwords don\'t match + Current master password