Added option to protect settings by account password

This commit is contained in:
Sylvain Berfini 2023-03-31 15:04:21 +02:00
parent 29ee69fc7b
commit 182d80a630
8 changed files with 155 additions and 9 deletions

View file

@ -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

View file

@ -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<SideMenuFragmentBinding>() {
private lateinit var viewModel: SideMenuViewModel
@ -87,7 +85,12 @@ class SideMenuFragment : GenericFragment<SideMenuFragmentBinding>() {
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<SideMenuFragmentBinding>() {
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<SideMenuFragmentBinding>() {
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()
}
}

View file

@ -46,6 +46,14 @@ class DialogViewModel(val message: String, val title: String = "") : ViewModel()
val dismissEvent = MutableLiveData<Event<Boolean>>()
var password: String = ""
var passwordTitle: String = ""
var passwordSubtitle: String = ""
var showPassword: Boolean = false
init {
doNotAskAgain.value = false
showTitle = title.isNotEmpty()

View file

@ -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

View file

@ -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
}
}
}

View file

@ -4,6 +4,7 @@
<data>
<import type="android.view.View"/>
<import type="android.text.InputType"/>
<variable
name="viewModel"
type="org.linphone.activities.main.viewmodels.DialogViewModel" />
@ -54,6 +55,31 @@
android:text="@string/assistant_forgotten_password_link"
android:visibility="@{viewModel.showSubscribeLinphoneOrgLink ? View.VISIBLE : View.GONE}" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/settings_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/settings_margin"
android:hint="@{viewModel.passwordTitle}"
android:textColorHint="@color/white_color"
android:visibility="@{viewModel.showPassword ? View.VISIBLE : View.GONE}"
app:helperTextTextColor="@color/white_color"
app:helperText="@{viewModel.passwordSubtitle}"
app:helperTextEnabled="@{viewModel.passwordSubtitle.length() > 0}">
<org.linphone.views.SettingTextInputEditText
android:id="@+id/settings_input"
android:text="@={viewModel.password}"
android:imeOptions="actionDone"
android:inputType="@{InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD}"
android:maxLines="1"
android:background="@color/transparent_color"
android:textColor="@color/white_color"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -774,4 +774,9 @@
<string name="chat_room_last_message_sender_format">%s :</string>
<string name="dialog_apply_remote_provisioning_title">Voulez-vous télécharger et appliquer la configuration depuis cette adresse ?</string>
<string name="dialog_apply_remote_provisioning_button">Appliquer</string>
<string name="error_unexpected">Erreur inattendue…</string>
<string name="settings_password_protection_dialog_title">Merci de saisir votre mot de passe ci-dessous pour accéder aux paramètres</string>
<string name="settings_password_protection_dialog_input_hint">Mot de passe</string>
<string name="settings_password_protection_dialog_ok_label">Valider</string>
<string name="settings_password_protection_dialog_invalid_input">Le mot de passe est invalide !</string>
</resources>

View file

@ -51,6 +51,7 @@
<string name="share_uploaded_logs_link">Share logs link using…</string>
<!-- Errors -->
<string name="error_unexpected">Unexpected error…</string>
<!-- Date & Time -->
<string name="today">Today</string>
@ -475,6 +476,10 @@
<string name="settings_primary_account_title">Primary Account</string>
<string name="settings_primary_account_display_name_title">Display Name</string>
<string name="settings_primary_account_username_title">Username</string>
<string name="settings_password_protection_dialog_title">Please input your password below to access the settings</string>
<string name="settings_password_protection_dialog_input_hint">Password</string>
<string name="settings_password_protection_dialog_ok_label">Validate</string>
<string name="settings_password_protection_dialog_invalid_input">Invalid password!</string>
<!-- Audio settings -->
<string name="audio_settings_echo_cancellation_title">Echo cancellation</string>