diff --git a/app/src/main/java/dev/lowrespalmtree/comet/Identities.kt b/app/src/main/java/dev/lowrespalmtree/comet/Identities.kt index 587f404..6f673d8 100644 --- a/app/src/main/java/dev/lowrespalmtree/comet/Identities.kt +++ b/app/src/main/java/dev/lowrespalmtree/comet/Identities.kt @@ -4,8 +4,10 @@ import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import android.util.Log import androidx.room.* +import java.lang.IllegalArgumentException import java.security.KeyPairGenerator import java.security.KeyStore +import javax.security.auth.x500.X500Principal object Identities { @Entity @@ -58,11 +60,20 @@ object Identities { Database.INSTANCE.identityDao().delete(*identities) } - fun generateClientCert(alias: String) { + fun generateClientCert(alias: String, commonName: String) { val algo = KeyProperties.KEY_ALGORITHM_RSA val kpg = KeyPairGenerator.getInstance(algo, "AndroidKeyStore") val purposes = KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY val spec = KeyGenParameterSpec.Builder(alias, purposes) + .apply { + if (commonName.isNotEmpty()) { + try { + setCertificateSubject(X500Principal("CN=$commonName")) + } catch (e: IllegalArgumentException) { + Log.e(TAG, "generateClientCert: bad common name: ${e.message}") + } + } + } .setDigests(KeyProperties.DIGEST_SHA256) .build() kpg.initialize(spec) diff --git a/app/src/main/java/dev/lowrespalmtree/comet/IdentitiesFragment.kt b/app/src/main/java/dev/lowrespalmtree/comet/IdentitiesFragment.kt index f336cb9..1d6caf0 100644 --- a/app/src/main/java/dev/lowrespalmtree/comet/IdentitiesFragment.kt +++ b/app/src/main/java/dev/lowrespalmtree/comet/IdentitiesFragment.kt @@ -82,27 +82,39 @@ class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityEditD vm.saveIdentity(identity) } - 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() + /** + * Open the new identity wizard. + * + * There is a first dialog to ask the user about the desired subject common name, + * then the certificate is generated and the edition dialog is opened. + */ + private fun openIdentityWizard() { + InputDialog(requireContext(), getString(R.string.input_common_name)) + .show( + onOk = { text -> + toast(requireContext(), R.string.generating_keypair) + vm.newIdentity.observe(viewLifecycleOwner) { identity -> + if (identity == null) + return@observe + vm.newIdentity.removeObservers(viewLifecycleOwner) + vm.newIdentity.value = null + IdentityEditDialog(requireContext(), identity, this).show() + } + vm.createNewIdentity(text) + }, + onDismiss = {} + ) } class IdentitiesViewModel : ViewModel() { val identities: MutableLiveData> by lazy { MutableLiveData>() } val newIdentity: MutableLiveData by lazy { MutableLiveData() } - fun createNewIdentity() { + fun createNewIdentity(commonName: String) { viewModelScope.launch(Dispatchers.IO) { val alias = "identity-${UUID.randomUUID()}" - Identities.generateClientCert(alias) - val newIdentityId = Identities.insert(alias) + Identities.generateClientCert(alias, commonName) + val newIdentityId = Identities.insert(alias, commonName) newIdentity.postValue(Identities.get(newIdentityId)) } .invokeOnCompletion { refreshIdentities() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2e24815..b5e01a5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -44,6 +44,7 @@ Confirm Are you sure you want to delete this identity? The client certificate cannot be retrieved afterwards. Edit - Active URL paths + Active URL path + Enter a name to use as the certificate\'s subject common name. This can be left empty. \ No newline at end of file