diff --git a/CHANGELOG.md b/CHANGELOG.md index d08a6ef83..ea345c667 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Group changes to describe their impact on the project, as follows: - Attended transfer instead of blind transfer if there is more than 1 call - Added hidden setting to disable video completely - Added hidden setting to prevent adding / editing / removing native contacts +- Added hidden setting to protect settings access using account password ### Changed - Account EXPIRES is now set to 1 month instead of 1 year for sip.linphone.org accounts diff --git a/app/src/main/java/org/linphone/activities/main/sidemenu/fragments/SideMenuFragment.kt b/app/src/main/java/org/linphone/activities/main/sidemenu/fragments/SideMenuFragment.kt index 8bd98bc41..34182931e 100644 --- a/app/src/main/java/org/linphone/activities/main/sidemenu/fragments/SideMenuFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/sidemenu/fragments/SideMenuFragment.kt @@ -31,20 +31,18 @@ import androidx.lifecycle.lifecycleScope import java.io.File import kotlinx.coroutines.launch import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R import org.linphone.activities.* import org.linphone.activities.assistant.AssistantActivity +import org.linphone.activities.main.MainActivity import org.linphone.activities.main.settings.SettingListenerStub import org.linphone.activities.main.sidemenu.viewmodels.SideMenuViewModel -import org.linphone.activities.navigateToAbout -import org.linphone.activities.navigateToAccountSettings -import org.linphone.activities.navigateToRecordings -import org.linphone.activities.navigateToSettings +import org.linphone.activities.main.viewmodels.DialogViewModel +import org.linphone.core.Factory import org.linphone.core.tools.Log import org.linphone.databinding.SideMenuFragmentBinding -import org.linphone.utils.Event -import org.linphone.utils.FileUtils -import org.linphone.utils.PermissionHelper +import org.linphone.utils.* class SideMenuFragment : GenericFragment() { private lateinit var viewModel: SideMenuViewModel @@ -87,7 +85,12 @@ class SideMenuFragment : GenericFragment() { Log.i("[Side Menu] Navigation to settings for account with identity: $identity") sharedViewModel.toggleDrawerEvent.value = Event(true) - navigateToAccountSettings(identity) + + if (corePreferences.askForAccountPasswordToAccessSettings) { + showPasswordDialog(goToAccountSettings = true, accountIdentity = identity) + } else { + navigateToAccountSettings(identity) + } } } @@ -102,7 +105,12 @@ class SideMenuFragment : GenericFragment() { binding.setSettingsClickListener { sharedViewModel.toggleDrawerEvent.value = Event(true) - navigateToSettings() + + if (corePreferences.askForAccountPasswordToAccessSettings) { + showPasswordDialog(goToSettings = true) + } else { + navigateToSettings() + } } binding.setRecordingsClickListener { @@ -172,4 +180,69 @@ class SideMenuFragment : GenericFragment() { startActivityForResult(chooserIntent, 0) } + + private fun showPasswordDialog( + goToSettings: Boolean = false, + goToAccountSettings: Boolean = false, + accountIdentity: String = "" + ) { + val dialogViewModel = DialogViewModel(getString(R.string.settings_password_protection_dialog_title)) + dialogViewModel.showIcon = true + dialogViewModel.iconResource = R.drawable.security_toggle_icon_green + dialogViewModel.showPassword = true + dialogViewModel.passwordTitle = getString(R.string.settings_password_protection_dialog_input_hint) + val dialog = DialogUtils.getDialog(requireContext(), dialogViewModel) + + dialogViewModel.showCancelButton { + dialog.dismiss() + } + + dialogViewModel.showOkButton( + { + val defaultAccount = coreContext.core.defaultAccount ?: coreContext.core.accountList.firstOrNull() + if (defaultAccount == null) { + Log.e("[Side Menu] No account found, can't check password input!") + (requireActivity() as MainActivity).showSnackBar(R.string.error_unexpected) + } else { + val authInfo = defaultAccount.findAuthInfo() + if (authInfo == null) { + Log.e("[Side Menu] No auth info found for account [${defaultAccount.params.identityAddress?.asString()}], can't check password input!") + (requireActivity() as MainActivity).showSnackBar(R.string.error_unexpected) + } else { + val expectedHash = authInfo.ha1 + if (expectedHash == null) { + Log.e("[Side Menu] No ha1 found in auth info, can't check password input!") + (requireActivity() as MainActivity).showSnackBar(R.string.error_unexpected) + } else { + val hashAlgorithm = authInfo.algorithm ?: "MD5" + val userId = (authInfo.userid ?: authInfo.username).orEmpty() + val realm = authInfo.realm.orEmpty() + val password = dialogViewModel.password + val computedHash = Factory.instance().computeHa1ForAlgorithm( + userId, + password, + realm, + hashAlgorithm + ) + if (computedHash != expectedHash) { + Log.e("[Side Menu] Computed hash [$computedHash] using userId [$userId], realm [$realm] and algorithm [$hashAlgorithm] doesn't match expected hash!") + (requireActivity() as MainActivity).showSnackBar(R.string.settings_password_protection_dialog_invalid_input) + } else { + if (goToSettings) { + navigateToSettings() + } else if (goToAccountSettings) { + navigateToAccountSettings(accountIdentity) + } + } + } + } + } + + dialog.dismiss() + }, + getString(R.string.settings_password_protection_dialog_ok_label) + ) + + dialog.show() + } } diff --git a/app/src/main/java/org/linphone/activities/main/viewmodels/DialogViewModel.kt b/app/src/main/java/org/linphone/activities/main/viewmodels/DialogViewModel.kt index 85299b292..3cc7440d5 100644 --- a/app/src/main/java/org/linphone/activities/main/viewmodels/DialogViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/viewmodels/DialogViewModel.kt @@ -46,6 +46,14 @@ class DialogViewModel(val message: String, val title: String = "") : ViewModel() val dismissEvent = MutableLiveData>() + var password: String = "" + + var passwordTitle: String = "" + + var passwordSubtitle: String = "" + + var showPassword: Boolean = false + init { doNotAskAgain.value = false showTitle = title.isNotEmpty() diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 578cdc7f9..28de260fb 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -521,6 +521,9 @@ class CorePreferences constructor(private val context: Context) { val autoRemoteProvisioningOnConfigUriHandler: Boolean get() = config.getBool("app", "auto_apply_provisioning_config_uri_handler", false) + val askForAccountPasswordToAccessSettings: Boolean + get() = config.getBool("app", "require_password_to_access_settings", false) + /* Default values related */ val echoCancellerCalibration: Int diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 88924b738..6c86477a0 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -24,9 +24,12 @@ import android.content.Context import android.net.ConnectivityManager import android.net.NetworkInfo import android.telephony.TelephonyManager.* +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException import java.text.DateFormat import java.text.SimpleDateFormat import java.util.* +import okhttp3.internal.and import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R @@ -257,5 +260,27 @@ class LinphoneUtils { return true // Legacy behavior } + + fun hashPassword( + userId: String, + password: String, + realm: String, + algorithm: String = "MD5" + ): String? { + val input = "$userId:$realm:$password" + try { + val digestEngine = MessageDigest.getInstance(algorithm) + val digest = digestEngine.digest(input.toByteArray()) + val hexString = StringBuffer() + for (i in digest.indices) { + hexString.append(Integer.toHexString(digest[i].and(0xFF))) + } + return hexString.toString() + } catch (nsae: NoSuchAlgorithmException) { + Log.e("[Side Menu] Can't compute hash using [$algorithm] algorithm!") + } + + return null + } } } diff --git a/app/src/main/res/layout/dialog.xml b/app/src/main/res/layout/dialog.xml index e2c95b406..b5c779d75 100644 --- a/app/src/main/res/layout/dialog.xml +++ b/app/src/main/res/layout/dialog.xml @@ -4,6 +4,7 @@ + @@ -54,6 +55,31 @@ android:text="@string/assistant_forgotten_password_link" android:visibility="@{viewModel.showSubscribeLinphoneOrgLink ? View.VISIBLE : View.GONE}" /> + + + + + + %s : Voulez-vous télécharger et appliquer la configuration depuis cette adresse ? Appliquer + Erreur inattendue… + Merci de saisir votre mot de passe ci-dessous pour accéder aux paramètres + Mot de passe + Valider + Le mot de passe est invalide ! \ 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 d6d4fa904..c5eab07c0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,6 +51,7 @@ Share logs link using… + Unexpected error… Today @@ -475,6 +476,10 @@ Primary Account Display Name Username + Please input your password below to access the settings + Password + Validate + Invalid password! Echo cancellation