From 11bea0e5859e17b56758d0dab39bac4ab7a40667 Mon Sep 17 00:00:00 2001 From: dece Date: Fri, 4 Feb 2022 12:54:18 +0100 Subject: [PATCH] Identities: add deletion and fix sync issues - Deletion works and clear the corresponding keystore entries. - No more sync issues with the listing when creating/updating/deleting. --- .../dev/lowrespalmtree/comet/Identities.kt | 23 ++++++++++ .../lowrespalmtree/comet/IdentitiesAdapter.kt | 10 +++- .../comet/IdentitiesFragment.kt | 46 +++++++++++++++++-- .../lowrespalmtree/comet/IdentityDialog.kt | 2 +- .../comet/IdentityEditorFragment.kt | 41 ----------------- .../dev/lowrespalmtree/comet/utils/Network.kt | 2 +- .../java/dev/lowrespalmtree/comet/utils/Ui.kt | 17 ++++++- app/src/main/res/layout/dialog_confirm.xml | 13 ++++++ app/src/main/res/menu/identity.xml | 5 ++ app/src/main/res/navigation/main.xml | 11 +---- app/src/main/res/values/colors.xml | 2 +- app/src/main/res/values/strings.xml | 10 ++-- 12 files changed, 120 insertions(+), 62 deletions(-) delete mode 100644 app/src/main/java/dev/lowrespalmtree/comet/IdentityEditorFragment.kt create mode 100644 app/src/main/res/layout/dialog_confirm.xml create mode 100644 app/src/main/res/menu/identity.xml diff --git a/app/src/main/java/dev/lowrespalmtree/comet/Identities.kt b/app/src/main/java/dev/lowrespalmtree/comet/Identities.kt index fac08bd..209153e 100644 --- a/app/src/main/java/dev/lowrespalmtree/comet/Identities.kt +++ b/app/src/main/java/dev/lowrespalmtree/comet/Identities.kt @@ -5,6 +5,7 @@ import android.security.keystore.KeyProperties import android.util.Log import androidx.room.* import java.security.KeyPairGenerator +import java.security.KeyStore object Identities { @Entity @@ -43,6 +44,9 @@ object Identities { @Query("SELECT * FROM IdentityUsage WHERE :identityId = identityId") suspend fun getUsagesFor(identityId: Int): List + + @Delete + suspend fun delete(vararg identities: Identity) } suspend fun insert(key: String, name: String? = null): Long = @@ -57,6 +61,14 @@ object Identities { suspend fun update(vararg identities: Identity) = Database.INSTANCE.identityDao().update(*identities) + suspend fun delete(vararg identities: Identity) { + for (identity in identities) { + if (identity.key.isNotEmpty()) + deleteClientCert(identity.key) + } + Database.INSTANCE.identityDao().delete(*identities) + } + fun generateClientCert(alias: String) { val algo = KeyProperties.KEY_ALGORITHM_RSA val kpg = KeyPairGenerator.getInstance(algo, "AndroidKeyStore") @@ -69,5 +81,16 @@ object Identities { Log.i(TAG, "generateClientCert: key pair with alias \"$alias\" has been generated") } + private fun deleteClientCert(alias: String) { + val keyStore = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) } + Log.i(TAG, keyStore.aliases().toList().joinToString { it }) + if (keyStore.containsAlias(alias)) { + keyStore.deleteEntry(alias) + Log.i(TAG, "deleteClientCert: deleted entry with alias \"$alias\"") + } else { + Log.i(TAG, "deleteClientCert: no such alias \"$alias\"") + } + } + private const val TAG = "Identities" } \ No newline at end of file diff --git a/app/src/main/java/dev/lowrespalmtree/comet/IdentitiesAdapter.kt b/app/src/main/java/dev/lowrespalmtree/comet/IdentitiesAdapter.kt index a8d42d3..fd95ce7 100644 --- a/app/src/main/java/dev/lowrespalmtree/comet/IdentitiesAdapter.kt +++ b/app/src/main/java/dev/lowrespalmtree/comet/IdentitiesAdapter.kt @@ -2,6 +2,7 @@ package dev.lowrespalmtree.comet import android.annotation.SuppressLint import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import dev.lowrespalmtree.comet.Identities.Identity @@ -14,6 +15,7 @@ class IdentitiesAdapter(private val listener: Listener) : interface Listener { fun onIdentityClick(identity: Identity) + fun onIdentityLongClick(identity: Identity, view: View) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = @@ -29,7 +31,13 @@ class IdentitiesAdapter(private val listener: Listener) : val item = identities[position] holder.binding.labelText.text = item.name.orEmpty() holder.binding.keyText.text = item.key - holder.binding.container.setOnClickListener { listener.onIdentityClick(item) } + holder.binding.container.setOnClickListener { + listener.onIdentityClick(item) + } + holder.binding.container.setOnLongClickListener { + listener.onIdentityLongClick(item, holder.itemView); + true + } } override fun getItemCount(): Int = identities.size diff --git a/app/src/main/java/dev/lowrespalmtree/comet/IdentitiesFragment.kt b/app/src/main/java/dev/lowrespalmtree/comet/IdentitiesFragment.kt index 4569f62..5c98097 100644 --- a/app/src/main/java/dev/lowrespalmtree/comet/IdentitiesFragment.kt +++ b/app/src/main/java/dev/lowrespalmtree/comet/IdentitiesFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.PopupMenu import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.MutableLiveData @@ -13,6 +14,7 @@ import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import dev.lowrespalmtree.comet.Identities.Identity import dev.lowrespalmtree.comet.databinding.FragmentIdentitiesBinding +import dev.lowrespalmtree.comet.utils.confirm import dev.lowrespalmtree.comet.utils.toast import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -50,6 +52,32 @@ class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityDialo IdentityDialog(requireContext(), identity, this).show() } + override fun onIdentityLongClick(identity: Identity, view: View) { + PopupMenu(requireContext(), view) + .apply { + menuInflater.inflate(R.menu.identity, this.menu) + setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.item_edit -> { + IdentityDialog( + requireContext(), + identity, + this@IdentitiesFragment + ).show() + } + R.id.item_delete -> { + confirm(requireContext(), R.string.confirm_identity_delete) { + vm.deleteIdentity(identity) + } + } + else -> {} + } + true + } + } + .show() + } + override fun onSaveIdentity(identity: Identity) { vm.saveIdentity(identity) } @@ -57,7 +85,10 @@ class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityDialo private fun openNewIdentityEditor() { toast(requireContext(), R.string.generating_keypair) vm.newIdentity.observe(viewLifecycleOwner) { identity -> + if (identity == null) + return@observe vm.newIdentity.removeObservers(viewLifecycleOwner) + vm.newIdentity.value = null IdentityDialog(requireContext(), identity, this).show() } vm.createNewIdentity() @@ -74,16 +105,25 @@ class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityDialo val newIdentityId = Identities.insert(alias) newIdentity.postValue(Identities.get(newIdentityId)) } + .invokeOnCompletion { refreshIdentities() } } fun refreshIdentities() { - viewModelScope.launch(Dispatchers.IO) { - identities.postValue(Identities.getAll()) - } + viewModelScope.launch(Dispatchers.IO) { identities.postValue(Identities.getAll()) } } fun saveIdentity(identity: Identity) { viewModelScope.launch(Dispatchers.IO) { Identities.update(identity) } + .invokeOnCompletion { refreshIdentities() } + } + + fun deleteIdentity(identity: Identity) { + viewModelScope.launch(Dispatchers.IO) { Identities.delete(identity) } + .invokeOnCompletion { refreshIdentities() } } } + + companion object { + private const val TAG = "IdentitiesFragment" + } } \ No newline at end of file diff --git a/app/src/main/java/dev/lowrespalmtree/comet/IdentityDialog.kt b/app/src/main/java/dev/lowrespalmtree/comet/IdentityDialog.kt index 3d373b0..d97a31e 100644 --- a/app/src/main/java/dev/lowrespalmtree/comet/IdentityDialog.kt +++ b/app/src/main/java/dev/lowrespalmtree/comet/IdentityDialog.kt @@ -18,7 +18,7 @@ class IdentityDialog( fun show() { binding = DialogIdentityBinding.inflate(LayoutInflater.from(context)) - binding.labelInput.setText(identity.name) + binding.labelInput.setText(identity.name.orEmpty()) binding.aliasText.text = identity.key AlertDialog.Builder(context) .setTitle(R.string.edit_identity) diff --git a/app/src/main/java/dev/lowrespalmtree/comet/IdentityEditorFragment.kt b/app/src/main/java/dev/lowrespalmtree/comet/IdentityEditorFragment.kt deleted file mode 100644 index 5b109db..0000000 --- a/app/src/main/java/dev/lowrespalmtree/comet/IdentityEditorFragment.kt +++ /dev/null @@ -1,41 +0,0 @@ -package dev.lowrespalmtree.comet - -import android.os.Bundle -import androidx.fragment.app.viewModels -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.preference.EditTextPreference -import androidx.preference.PreferenceFragmentCompat -import dev.lowrespalmtree.comet.Identities.Identity -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlin.coroutines.coroutineContext - -class IdentityEditorFragment : PreferenceFragmentCompat() { - private val vm: IdentityEditorViewModel by viewModels() - private lateinit var namePref: EditTextPreference - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - setPreferencesFromResource(R.xml.identity_preferences, rootKey) - namePref = findPreference("name")!! - -// vm.identity.observe(viewLifecycleOwner) { -// namePref.apply { -// // TODO -// } -// } -// -// arguments?.getLong("id")?.also { vm.loadIdentity(it) } - } - - class IdentityEditorViewModel : ViewModel() { - val identity: MutableLiveData by lazy { MutableLiveData() } - - fun loadIdentity(id: Long) { - viewModelScope.launch(Dispatchers.IO) { - identity.postValue(Identities.get(id)) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/dev/lowrespalmtree/comet/utils/Network.kt b/app/src/main/java/dev/lowrespalmtree/comet/utils/Network.kt index 9c182c2..5d8403c 100644 --- a/app/src/main/java/dev/lowrespalmtree/comet/utils/Network.kt +++ b/app/src/main/java/dev/lowrespalmtree/comet/utils/Network.kt @@ -8,4 +8,4 @@ fun isConnectedToNetwork(context: Context): Boolean { val connManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val caps = connManager.getNetworkCapabilities(connManager.activeNetwork) ?: return false return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) -} +} \ No newline at end of file diff --git a/app/src/main/java/dev/lowrespalmtree/comet/utils/Ui.kt b/app/src/main/java/dev/lowrespalmtree/comet/utils/Ui.kt index 41f2a20..20a0262 100644 --- a/app/src/main/java/dev/lowrespalmtree/comet/utils/Ui.kt +++ b/app/src/main/java/dev/lowrespalmtree/comet/utils/Ui.kt @@ -1,10 +1,14 @@ package dev.lowrespalmtree.comet.utils +import android.app.AlertDialog import android.content.Context import android.util.TypedValue +import android.view.LayoutInflater import android.widget.Toast import androidx.annotation.AttrRes +import androidx.annotation.StringRes import dev.lowrespalmtree.comet.R +import dev.lowrespalmtree.comet.databinding.DialogConfirmBinding fun toast(context: Context, stringId: Int, length: Int = Toast.LENGTH_SHORT) = Toast.makeText(context, stringId, length).show() @@ -15,4 +19,15 @@ fun getDrawableFromAttr(context: Context, @AttrRes attr: Int) = .resourceId fun getFancySelectBgRes(context: Context) = - getDrawableFromAttr(context, R.attr.selectableItemBackground) \ No newline at end of file + getDrawableFromAttr(context, R.attr.selectableItemBackground) + +fun confirm(context: Context, @StringRes prompt: Int, onOk: () -> Unit) { + val binding = DialogConfirmBinding.inflate(LayoutInflater.from(context)) + binding.textView.setText(prompt) + AlertDialog.Builder(context) + .setTitle(R.string.confirm) + .setView(binding.root) + .setPositiveButton(android.R.string.ok) { _, _ -> onOk() } + .create() + .show() +} \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_confirm.xml b/app/src/main/res/layout/dialog_confirm.xml new file mode 100644 index 0000000..7b52893 --- /dev/null +++ b/app/src/main/res/layout/dialog_confirm.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/identity.xml b/app/src/main/res/menu/identity.xml new file mode 100644 index 0000000..b28e651 --- /dev/null +++ b/app/src/main/res/menu/identity.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/main.xml b/app/src/main/res/navigation/main.xml index 0f44285..6999214 100644 --- a/app/src/main/res/navigation/main.xml +++ b/app/src/main/res/navigation/main.xml @@ -31,16 +31,7 @@ - - - - + android:label="IdentitiesFragment" /> #002b36 #fdf6e3 Protocol Connection timeout (seconds) @@ -39,6 +40,9 @@ Generating client certificate… Keystore reference Edit identity - + Delete + Confirm + Are you sure you want to delete this identity? The client certificate cannot be retrieved afterwards. + Edit \ No newline at end of file