diff options
author | Aditya Wasan <adityawasan55@gmail.com> | 2023-04-02 14:04:33 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-02 18:04:33 +0000 |
commit | 97b3577a463966e93d24649ff348fc4bb6825e50 (patch) | |
tree | 8662fa55b2ab85fba58b3d8f7e3fe036bc0475f6 /app | |
parent | 577d6ab55a331fe842fed25fdf96b22bac345d90 (diff) |
Refactor SSHKey into a separate module (#2450)
* refactor(ssh): add `ssh` module
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* refactor(ssh): add `SSHKey` data class
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* refactor(ssh): add `SSHKeyType` enum
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* refactor(ssh): add `SSHKeyAlgorithm` class
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* refactor(ssh): add class to generate `RSA` key
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* chore(ssh): add required dependencies
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* refactor(ssh): add `ECDSAKeyGenerator` and remove constants
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* refactor(ssh): add utilities
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* feat(ssh): add `SSHKeyWriter`
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* refactor(ssh): make ssh key generators suspending
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* fix(ssh): fix explicit API violations
* feat: complete `ED25519KeyWriter` implementation
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* factor(ssh/writer): update writer interface
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* feat(ssh/provider): add providers for different key types
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* feat(ssh): add SSHKeyManager for common key functionality
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* feat(ssh): add remaining methods to reach feature parity with old SSH implementation
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* wip(app): start using SSHKeyManager instead of SSHKey class
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
* refactor(ssh): update package name
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* chore(ssh): fix detekt warnings
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
* chore: fixes across the board
---------
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
Diffstat (limited to 'app')
9 files changed, 113 insertions, 55 deletions
diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5d005676..c6ca5871 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation(projects.formatCommon) implementation(projects.passgen.diceware) implementation(projects.passgen.random) + implementation(projects.ssh) implementation(projects.uiCompose) implementation(libs.androidx.activity.ktx) implementation(libs.androidx.activity.compose) diff --git a/app/src/main/java/app/passwordstore/injection/ssh/SSHKeyManagerModule.kt b/app/src/main/java/app/passwordstore/injection/ssh/SSHKeyManagerModule.kt new file mode 100644 index 00000000..c5abd487 --- /dev/null +++ b/app/src/main/java/app/passwordstore/injection/ssh/SSHKeyManagerModule.kt @@ -0,0 +1,21 @@ +package app.passwordstore.injection.ssh + +import android.content.Context +import app.passwordstore.ssh.SSHKeyManager +import dagger.Module +import dagger.Provides +import dagger.Reusable +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object SSHKeyManagerModule { + + @Provides + @Reusable + fun provideSSHKeyManager(@ApplicationContext context: Context): SSHKeyManager { + return SSHKeyManager(context) + } +} diff --git a/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt b/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt index 8bd61f41..c1ec8cbb 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt @@ -9,13 +9,14 @@ import android.content.Intent import android.content.SharedPreferences import android.content.pm.ShortcutManager import android.os.Build -import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.core.content.edit import androidx.core.content.getSystemService import androidx.fragment.app.FragmentActivity import app.passwordstore.R import app.passwordstore.data.repo.PasswordRepository import app.passwordstore.injection.prefs.GitPreferences +import app.passwordstore.ssh.SSHKeyManager import app.passwordstore.ui.git.config.GitConfigActivity import app.passwordstore.ui.git.config.GitServerConfigActivity import app.passwordstore.ui.proxy.ProxySelectorActivity @@ -27,7 +28,6 @@ import app.passwordstore.util.extensions.launchActivity import app.passwordstore.util.extensions.sharedPrefs import app.passwordstore.util.extensions.snackbar import app.passwordstore.util.extensions.unsafeLazy -import app.passwordstore.util.git.sshj.SshKey import app.passwordstore.util.settings.GitSettings import app.passwordstore.util.settings.PreferenceKeys import com.github.michaelbull.result.onFailure @@ -43,11 +43,13 @@ import de.Maxr1998.modernpreferences.helpers.onClick import de.Maxr1998.modernpreferences.helpers.pref import de.Maxr1998.modernpreferences.helpers.switch -class RepositorySettings(private val activity: FragmentActivity) : SettingsProvider { - +class RepositorySettings( + private val activity: FragmentActivity, + private val sshKeyManager: SSHKeyManager, +) : SettingsProvider { private val generateSshKey = - activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - showSshKeyPref?.visible = SshKey.canShowSshPublicKey + activity.registerForActivityResult(StartActivityForResult()) { + showSshKeyPref?.visible = sshKeyManager.canShowPublicKey() } private val hiltEntryPoint by unsafeLazy { @@ -112,7 +114,7 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi showSshKeyPref = pref(PreferenceKeys.SSH_SEE_KEY) { titleRes = R.string.pref_ssh_see_key_title - visible = PasswordRepository.isGitRepo() && SshKey.canShowSshPublicKey + visible = PasswordRepository.isGitRepo() && sshKeyManager.canShowPublicKey() onClick { ShowSshKeyFragment().show(activity.supportFragmentManager, "public_key") true diff --git a/app/src/main/java/app/passwordstore/ui/settings/SettingsActivity.kt b/app/src/main/java/app/passwordstore/ui/settings/SettingsActivity.kt index 30e2b1b0..697d0156 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/SettingsActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/SettingsActivity.kt @@ -11,19 +11,24 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.os.BundleCompat import app.passwordstore.R import app.passwordstore.databinding.ActivityPreferenceRecyclerviewBinding +import app.passwordstore.ssh.SSHKeyManager import app.passwordstore.util.extensions.viewBinding import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import de.Maxr1998.modernpreferences.Preference import de.Maxr1998.modernpreferences.PreferencesAdapter import de.Maxr1998.modernpreferences.helpers.screen import de.Maxr1998.modernpreferences.helpers.subScreen +import javax.inject.Inject +@AndroidEntryPoint class SettingsActivity : AppCompatActivity() { + @Inject lateinit var sshKeyManager: SSHKeyManager + private lateinit var repositorySettings: RepositorySettings private val miscSettings = MiscSettings(this) private val autofillSettings = AutofillSettings(this) private val passwordSettings = PasswordSettings(this) - private val repositorySettings = RepositorySettings(this) private val generalSettings = GeneralSettings(this) private val pgpSettings = PGPSettings(this) @@ -35,6 +40,7 @@ class SettingsActivity : AppCompatActivity() { super.onCreate(savedInstanceState) setContentView(binding.root) Preference.Config.dialogBuilderFactory = { context -> MaterialAlertDialogBuilder(context) } + repositorySettings = RepositorySettings(this, sshKeyManager) val screen = screen(this) { subScreen { diff --git a/app/src/main/java/app/passwordstore/ui/sshkeygen/ShowSshKeyFragment.kt b/app/src/main/java/app/passwordstore/ui/sshkeygen/ShowSshKeyFragment.kt index a42d6aa1..4f52b540 100644 --- a/app/src/main/java/app/passwordstore/ui/sshkeygen/ShowSshKeyFragment.kt +++ b/app/src/main/java/app/passwordstore/ui/sshkeygen/ShowSshKeyFragment.kt @@ -9,14 +9,19 @@ import android.content.Intent import android.os.Bundle import androidx.fragment.app.DialogFragment import app.passwordstore.R -import app.passwordstore.util.git.sshj.SshKey +import app.passwordstore.ssh.SSHKeyManager import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +@AndroidEntryPoint class ShowSshKeyFragment : DialogFragment() { + @Inject lateinit var sshKeyManager: SSHKeyManager + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val activity = requireActivity() - val publicKey = SshKey.sshPublicKey + val publicKey = sshKeyManager.publicKey() return MaterialAlertDialogBuilder(requireActivity()).run { setMessage(getString(R.string.ssh_keygen_message, publicKey)) setTitle(R.string.your_public_key) diff --git a/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyGenActivity.kt b/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyGenActivity.kt index 67528d24..eea2d659 100644 --- a/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyGenActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyGenActivity.kt @@ -17,11 +17,12 @@ import androidx.lifecycle.lifecycleScope import app.passwordstore.R import app.passwordstore.databinding.ActivitySshKeygenBinding import app.passwordstore.injection.prefs.GitPreferences +import app.passwordstore.ssh.SSHKeyAlgorithm +import app.passwordstore.ssh.SSHKeyManager import app.passwordstore.util.auth.BiometricAuthenticator import app.passwordstore.util.auth.BiometricAuthenticator.Result import app.passwordstore.util.extensions.keyguardManager import app.passwordstore.util.extensions.viewBinding -import app.passwordstore.util.git.sshj.SshKey import com.github.michaelbull.result.fold import com.github.michaelbull.result.runCatching import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -33,24 +34,13 @@ 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) - }), -} - @AndroidEntryPoint class SshKeyGenActivity : AppCompatActivity() { - private var keyGenType = KeyGenType.Ecdsa + private var sshKeyAlgorithm = SSHKeyAlgorithm.ECDSA private val binding by viewBinding(ActivitySshKeygenBinding::inflate) @GitPreferences @Inject lateinit var gitPrefs: SharedPreferences + @Inject lateinit var sshKeyManager: SSHKeyManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -58,7 +48,7 @@ class SshKeyGenActivity : AppCompatActivity() { supportActionBar?.setDisplayHomeAsUpEnabled(true) with(binding) { generate.setOnClickListener { - if (SshKey.exists) { + if (sshKeyManager.keyExists()) { MaterialAlertDialogBuilder(this@SshKeyGenActivity).run { setTitle(R.string.ssh_keygen_existing_title) setMessage(R.string.ssh_keygen_existing_message) @@ -79,18 +69,18 @@ class SshKeyGenActivity : AppCompatActivity() { keyTypeExplanation.setText(R.string.ssh_keygen_explanation_ecdsa) keyTypeGroup.addOnButtonCheckedListener { _, checkedId, isChecked -> if (isChecked) { - keyGenType = + sshKeyAlgorithm = when (checkedId) { - R.id.key_type_ed25519 -> KeyGenType.Ed25519 - R.id.key_type_ecdsa -> KeyGenType.Ecdsa - R.id.key_type_rsa -> KeyGenType.Rsa + R.id.key_type_ed25519 -> SSHKeyAlgorithm.ED25519 + R.id.key_type_ecdsa -> SSHKeyAlgorithm.ECDSA + R.id.key_type_rsa -> SSHKeyAlgorithm.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 + when (sshKeyAlgorithm) { + SSHKeyAlgorithm.ED25519 -> R.string.ssh_keygen_explanation_ed25519 + SSHKeyAlgorithm.ECDSA -> R.string.ssh_keygen_explanation_ecdsa + SSHKeyAlgorithm.RSA -> R.string.ssh_keygen_explanation_rsa } ) } @@ -136,9 +126,10 @@ class SshKeyGenActivity : AppCompatActivity() { if (result !is Result.Success) throw UserNotAuthenticatedException(getString(R.string.biometric_auth_generic_failure)) } - keyGenType.generateKey(requireAuthentication) + sshKeyManager.generateKey(sshKeyAlgorithm, requireAuthentication) } } + // Check if we still need this gitPrefs.edit { remove("ssh_key_local_passphrase") } binding.generate.apply { text = getString(R.string.ssh_keygen_generate) diff --git a/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyImportActivity.kt b/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyImportActivity.kt index 99b3bf3f..a5d276ae 100644 --- a/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyImportActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyImportActivity.kt @@ -10,14 +10,21 @@ import android.os.Bundle import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope import app.passwordstore.R -import app.passwordstore.util.git.sshj.SshKey +import app.passwordstore.ssh.SSHKeyManager import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.runCatching import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.launch +@AndroidEntryPoint class SshKeyImportActivity : AppCompatActivity() { + @Inject lateinit var sshKeyManager: SSHKeyManager + private val sshKeyImportAction = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? -> if (uri == null) { @@ -25,15 +32,17 @@ class SshKeyImportActivity : AppCompatActivity() { return@registerForActivityResult } runCatching { - SshKey.import(uri) - Toast.makeText( - this, - resources.getString(R.string.ssh_key_success_dialog_title), - Toast.LENGTH_LONG - ) - .show() - setResult(RESULT_OK) - finish() + lifecycleScope.launch { + sshKeyManager.importKey(uri) + Toast.makeText( + this@SshKeyImportActivity, + resources.getString(R.string.ssh_key_success_dialog_title), + Toast.LENGTH_LONG + ) + .show() + setResult(RESULT_OK) + finish() + } } .onFailure { e -> MaterialAlertDialogBuilder(this) @@ -46,8 +55,8 @@ class SshKeyImportActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (SshKey.exists) { - MaterialAlertDialogBuilder(this).run { + if (sshKeyManager.keyExists()) { + MaterialAlertDialogBuilder(this@SshKeyImportActivity).run { setTitle(R.string.ssh_keygen_existing_title) setMessage(R.string.ssh_keygen_existing_message) setPositiveButton(R.string.ssh_keygen_existing_replace) { _, _ -> importSshKey() } diff --git a/app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt b/app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt index 92f17890..07ce7245 100644 --- a/app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt +++ b/app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt @@ -10,6 +10,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.FragmentActivity import app.passwordstore.R import app.passwordstore.data.repo.PasswordRepository +import app.passwordstore.ssh.SSHKeyManager import app.passwordstore.ui.sshkeygen.SshKeyGenActivity import app.passwordstore.ui.sshkeygen.SshKeyImportActivity import app.passwordstore.util.auth.BiometricAuthenticator @@ -19,7 +20,6 @@ import app.passwordstore.util.auth.BiometricAuthenticator.Result.Retry import app.passwordstore.util.auth.BiometricAuthenticator.Result.Success import app.passwordstore.util.git.GitCommandExecutor import app.passwordstore.util.git.sshj.SshAuthMethod -import app.passwordstore.util.git.sshj.SshKey import app.passwordstore.util.git.sshj.SshjSessionFactory import app.passwordstore.util.settings.AuthMode import com.github.michaelbull.result.Err @@ -28,6 +28,10 @@ import com.github.michaelbull.result.Result import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.runCatching import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.Dispatchers @@ -62,6 +66,12 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) { open val requiresAuth: Boolean = true private val hostKeyFile = callingActivity.filesDir.resolve(".host_key") private var sshSessionFactory: SshjSessionFactory? = null + private val hiltEntryPoint = + EntryPointAccessors.fromApplication( + callingActivity.applicationContext, + GitOperationEntryPoint::class.java + ) + private val sshKeyManager = hiltEntryPoint.sshKeyManager() protected val repository = PasswordRepository.repository!! protected val git = Git(repository) private val authActivity @@ -115,7 +125,7 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) { authMethod: SshAuthMethod, credentialsProvider: CredentialsProvider? = null ) { - sshSessionFactory = SshjSessionFactory(authMethod, hostKeyFile) + sshSessionFactory = SshjSessionFactory(authMethod, hostKeyFile, sshKeyManager) commands.filterIsInstance<TransportCommand<*, *>>().forEach { command -> command.setTransportConfigCallback { transport: Transport -> (transport as? SshTransport)?.sshSessionFactory = sshSessionFactory @@ -163,8 +173,8 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) { suspend fun executeAfterAuthentication(authMode: AuthMode): Result<Unit, Throwable> { when (authMode) { AuthMode.SshKey -> - if (SshKey.exists) { - if (SshKey.mustAuthenticate) { + if (sshKeyManager.keyExists()) { + if (sshKeyManager.needsAuthentication()) { val result = withContext(Dispatchers.Main) { suspendCoroutine<BiometricAuthenticator.Result> { cont -> @@ -231,4 +241,10 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) { /** Timeout in seconds before [TransportCommand] will abort a stalled IO operation. */ private const val CONNECT_TIMEOUT = 10 } + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface GitOperationEntryPoint { + fun sshKeyManager(): SSHKeyManager + } } diff --git a/app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt b/app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt index 58af8495..c6e648c5 100644 --- a/app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt +++ b/app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt @@ -6,6 +6,7 @@ package app.passwordstore.util.git.sshj import android.util.Base64 import androidx.appcompat.app.AppCompatActivity +import app.passwordstore.ssh.SSHKeyManager import app.passwordstore.util.git.operation.CredentialFinder import app.passwordstore.util.settings.AuthMode import com.github.michaelbull.result.getOrElse @@ -65,8 +66,11 @@ abstract class InteractivePasswordFinder : PasswordFinder { final override fun shouldRetry(resource: Resource<*>?) = true } -class SshjSessionFactory(private val authMethod: SshAuthMethod, private val hostKeyFile: File) : - SshSessionFactory() { +class SshjSessionFactory( + private val authMethod: SshAuthMethod, + private val hostKeyFile: File, + private val sshKeyManager: SSHKeyManager, +) : SshSessionFactory() { private var currentSession: SshjSession? = null @@ -77,7 +81,7 @@ class SshjSessionFactory(private val authMethod: SshAuthMethod, private val host tms: Int ): RemoteSession { return currentSession - ?: SshjSession(uri, uri.user, authMethod, hostKeyFile).connect().also { + ?: SshjSession(uri, uri.user, authMethod, hostKeyFile, sshKeyManager).connect().also { logcat { "New SSH connection created" } currentSession = it } @@ -120,7 +124,8 @@ private class SshjSession( uri: URIish, private val username: String, private val authMethod: SshAuthMethod, - private val hostKeyFile: File + private val hostKeyFile: File, + private val sshKeyManager: SSHKeyManager, ) : RemoteSession { private lateinit var ssh: SSHClient @@ -154,7 +159,9 @@ private class SshjSession( } is SshAuthMethod.SshKey -> { val pubkeyAuth = - AuthPublickey(SshKey.provide(ssh, CredentialFinder(authMethod.activity, AuthMode.SshKey))) + AuthPublickey( + sshKeyManager.keyProvider(ssh, CredentialFinder(authMethod.activity, AuthMode.SshKey)) + ) ssh.auth(username, pubkeyAuth, passwordAuth) } } |