From cbb96397d1f5cffef7984b08a426646aaea93e5b Mon Sep 17 00:00:00 2001 From: Fabian Henneke Date: Tue, 1 Sep 2020 10:12:27 +0200 Subject: Add Keystore backend for SSH public key authentication (#1070) --- .../zeapo/pwdstore/sshkeygen/SshKeyGenActivity.kt | 99 ++++++++++++++++------ 1 file changed, 74 insertions(+), 25 deletions(-) (limited to 'app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeyGenActivity.kt') diff --git a/app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeyGenActivity.kt b/app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeyGenActivity.kt index 98bda9d7..810a8925 100644 --- a/app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeyGenActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeyGenActivity.kt @@ -5,6 +5,7 @@ package com.zeapo.pwdstore.sshkeygen import android.os.Bundle +import android.security.keystore.UserNotAuthenticatedException import android.view.MenuItem import android.view.View import android.view.inputmethod.InputMethodManager @@ -13,22 +14,34 @@ import androidx.core.content.edit import androidx.core.content.getSystemService import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.jcraft.jsch.JSch -import com.jcraft.jsch.KeyPair import com.zeapo.pwdstore.R import com.zeapo.pwdstore.databinding.ActivitySshKeygenBinding +import com.zeapo.pwdstore.git.sshj.SshKey +import com.zeapo.pwdstore.utils.BiometricAuthenticator import com.zeapo.pwdstore.utils.getEncryptedPrefs -import com.zeapo.pwdstore.utils.sharedPrefs +import com.zeapo.pwdstore.utils.keyguardManager import com.zeapo.pwdstore.utils.viewBinding -import java.io.File -import java.io.FileOutputStream +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +private enum class KeyGenType(val generateKey: suspend (requireAuthentication: Boolean) -> Unit) { + Rsa({ requireAuthentication -> + SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Rsa, requireAuthentication) + }), + Ecdsa({ requireAuthentication -> + SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Ecdsa, requireAuthentication) + }), + Ed25519({ requireAuthentication -> + SshKey.generateKeystoreWrappedEd25519Key(requireAuthentication) + }), +} + class SshKeyGenActivity : AppCompatActivity() { - private var keyLength = 4096 + private var keyGenType = KeyGenType.Ecdsa private val binding by viewBinding(ActivitySshKeygenBinding::inflate) override fun onCreate(savedInstanceState: Bundle?) { @@ -37,17 +50,45 @@ class SshKeyGenActivity : AppCompatActivity() { supportActionBar?.setDisplayHomeAsUpEnabled(true) with(binding) { generate.setOnClickListener { - lifecycleScope.launch { generate(passphrase.text.toString(), comment.text.toString()) } + if (SshKey.exists) { + MaterialAlertDialogBuilder(this@SshKeyGenActivity).run { + setTitle(R.string.ssh_keygen_existing_title) + setMessage(R.string.ssh_keygen_existing_message) + setPositiveButton(R.string.ssh_keygen_existing_replace) { _, _ -> + lifecycleScope.launch { + generate() + } + } + setNegativeButton(R.string.ssh_keygen_existing_keep) { _, _ -> + finish() + } + show() + } + } else { + lifecycleScope.launch { + generate() + } + } } - keyLengthGroup.check(R.id.key_length_4096) - keyLengthGroup.addOnButtonCheckedListener { _, checkedId, isChecked -> + keyTypeGroup.check(R.id.key_type_ecdsa) + keyTypeExplanation.setText(R.string.ssh_keygen_explanation_ecdsa) + keyTypeGroup.addOnButtonCheckedListener { _, checkedId, isChecked -> if (isChecked) { - when (checkedId) { - R.id.key_length_2048 -> keyLength = 2048 - R.id.key_length_4096 -> keyLength = 4096 + keyGenType = when (checkedId) { + R.id.key_type_ed25519 -> KeyGenType.Ed25519 + R.id.key_type_ecdsa -> KeyGenType.Ecdsa + R.id.key_type_rsa -> KeyGenType.Rsa + else -> throw IllegalStateException("Impossible key type selection") } + keyTypeExplanation.setText(when (keyGenType) { + KeyGenType.Ed25519 -> R.string.ssh_keygen_explanation_ed25519 + KeyGenType.Ecdsa -> R.string.ssh_keygen_explanation_ecdsa + KeyGenType.Rsa -> R.string.ssh_keygen_explanation_rsa + }) } } + keyRequireAuthentication.isEnabled = keyguardManager.isDeviceSecure + keyRequireAuthentication.isChecked = keyRequireAuthentication.isEnabled } } @@ -62,21 +103,27 @@ class SshKeyGenActivity : AppCompatActivity() { } } - private suspend fun generate(passphrase: String, comment: String) { + private suspend fun generate() { + binding.generate.apply { + text = getString(R.string.ssh_key_gen_generating_progress) + isEnabled = false + } binding.generate.text = getString(R.string.ssh_key_gen_generating_progress) val e = try { withContext(Dispatchers.IO) { - val kp = KeyPair.genKeyPair(JSch(), KeyPair.RSA, keyLength) - var file = File(filesDir, ".ssh_key") - var out = FileOutputStream(file, false) - if (passphrase.isNotEmpty()) { - kp?.writePrivateKey(out, passphrase.toByteArray()) - } else { - kp?.writePrivateKey(out) + val requireAuthentication = binding.keyRequireAuthentication.isChecked + if (requireAuthentication) { + val result = withContext(Dispatchers.Main) { + suspendCoroutine { cont -> + BiometricAuthenticator.authenticate(this@SshKeyGenActivity, R.string.biometric_prompt_title_ssh_keygen) { + cont.resume(it) + } + } + } + if (result !is BiometricAuthenticator.Result.Success) + throw UserNotAuthenticatedException(getString(R.string.biometric_auth_generic_failure)) } - file = File(filesDir, ".ssh_key.pub") - out = FileOutputStream(file, false) - kp?.writePublicKey(out, comment) + keyGenType.generateKey(requireAuthentication) } null } catch (e: Exception) { @@ -87,11 +134,13 @@ class SshKeyGenActivity : AppCompatActivity() { remove("ssh_key_local_passphrase") } } - binding.generate.text = getString(R.string.ssh_keygen_generating_done) + binding.generate.apply { + text = getString(R.string.ssh_keygen_generate) + isEnabled = true + } if (e == null) { val df = ShowSshKeyFragment() df.show(supportFragmentManager, "public_key") - sharedPrefs.edit { putBoolean("use_generated_key", true) } } else { MaterialAlertDialogBuilder(this) .setTitle(getString(R.string.error_generate_ssh_key)) -- cgit v1.2.3