Added option to protect settings by account password
This commit is contained in:
parent
29ee69fc7b
commit
182d80a630
8 changed files with 155 additions and 9 deletions
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue