diff options
31 files changed, 55 insertions, 865 deletions
diff --git a/app/build.gradle.kts b/app/build.gradle.kts index abd26932..85144bbc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -47,7 +47,6 @@ dependencies { implementation(projects.format.common) implementation(projects.passgen.diceware) implementation(projects.passgen.random) - implementation(projects.ssh) implementation(projects.ui.compose) 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 deleted file mode 100644 index c5abd487..00000000 --- a/app/src/main/java/app/passwordstore/injection/ssh/SSHKeyManagerModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -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 462924ac..c6c30b88 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt @@ -15,7 +15,6 @@ 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,6 +26,7 @@ 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 @@ -42,13 +42,11 @@ import de.Maxr1998.modernpreferences.helpers.onClick import de.Maxr1998.modernpreferences.helpers.pref import de.Maxr1998.modernpreferences.helpers.switch -class RepositorySettings( - private val activity: FragmentActivity, - private val sshKeyManager: SSHKeyManager, -) : SettingsProvider { +class RepositorySettings(private val activity: FragmentActivity) : SettingsProvider { + private val generateSshKey = activity.registerForActivityResult(StartActivityForResult()) { - showSshKeyPref?.visible = sshKeyManager.canShowPublicKey() + showSshKeyPref?.visible = SshKey.canShowSshPublicKey } private val hiltEntryPoint by unsafeLazy { @@ -113,7 +111,7 @@ class RepositorySettings( showSshKeyPref = pref(PreferenceKeys.SSH_SEE_KEY) { titleRes = R.string.pref_ssh_see_key_title - visible = PasswordRepository.isGitRepo() && sshKeyManager.canShowPublicKey() + visible = PasswordRepository.isGitRepo() && SshKey.canShowSshPublicKey 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 697d0156..30e2b1b0 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/SettingsActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/SettingsActivity.kt @@ -11,24 +11,19 @@ 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) @@ -40,7 +35,6 @@ 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 4f52b540..a42d6aa1 100644 --- a/app/src/main/java/app/passwordstore/ui/sshkeygen/ShowSshKeyFragment.kt +++ b/app/src/main/java/app/passwordstore/ui/sshkeygen/ShowSshKeyFragment.kt @@ -9,19 +9,14 @@ import android.content.Intent import android.os.Bundle import androidx.fragment.app.DialogFragment import app.passwordstore.R -import app.passwordstore.ssh.SSHKeyManager +import app.passwordstore.util.git.sshj.SshKey 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 = sshKeyManager.publicKey() + val publicKey = SshKey.sshPublicKey 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 34749e4c..68d5a40c 100644 --- a/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyGenActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyGenActivity.kt @@ -17,13 +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.coroutines.DispatcherProvider 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 @@ -34,13 +33,24 @@ import kotlin.coroutines.suspendCoroutine 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 sshKeyAlgorithm = SSHKeyAlgorithm.ECDSA + private var keyGenType = KeyGenType.Ecdsa private val binding by viewBinding(ActivitySshKeygenBinding::inflate) @GitPreferences @Inject lateinit var gitPrefs: SharedPreferences - @Inject lateinit var sshKeyManager: SSHKeyManager @Inject lateinit var dispatcherProvider: DispatcherProvider override fun onCreate(savedInstanceState: Bundle?) { @@ -49,7 +59,7 @@ class SshKeyGenActivity : AppCompatActivity() { supportActionBar?.setDisplayHomeAsUpEnabled(true) with(binding) { generate.setOnClickListener { - if (sshKeyManager.keyExists()) { + if (SshKey.exists) { MaterialAlertDialogBuilder(this@SshKeyGenActivity).run { setTitle(R.string.ssh_keygen_existing_title) setMessage(R.string.ssh_keygen_existing_message) @@ -70,18 +80,18 @@ class SshKeyGenActivity : AppCompatActivity() { keyTypeExplanation.setText(R.string.ssh_keygen_explanation_ecdsa) keyTypeGroup.addOnButtonCheckedListener { _, checkedId, isChecked -> if (isChecked) { - sshKeyAlgorithm = + keyGenType = when (checkedId) { - R.id.key_type_ed25519 -> SSHKeyAlgorithm.ED25519 - R.id.key_type_ecdsa -> SSHKeyAlgorithm.ECDSA - R.id.key_type_rsa -> SSHKeyAlgorithm.RSA + 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 (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 + 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 } ) } @@ -127,10 +137,9 @@ class SshKeyGenActivity : AppCompatActivity() { if (result !is Result.Success) throw UserNotAuthenticatedException(getString(R.string.biometric_auth_generic_failure)) } - sshKeyManager.generateKey(sshKeyAlgorithm, requireAuthentication) + keyGenType.generateKey(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 a5d276ae..99b3bf3f 100644 --- a/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyImportActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyImportActivity.kt @@ -10,21 +10,14 @@ 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.ssh.SSHKeyManager +import app.passwordstore.util.git.sshj.SshKey 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) { @@ -32,17 +25,15 @@ class SshKeyImportActivity : AppCompatActivity() { return@registerForActivityResult } runCatching { - 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() - } + SshKey.import(uri) + Toast.makeText( + this, + resources.getString(R.string.ssh_key_success_dialog_title), + Toast.LENGTH_LONG + ) + .show() + setResult(RESULT_OK) + finish() } .onFailure { e -> MaterialAlertDialogBuilder(this) @@ -55,8 +46,8 @@ class SshKeyImportActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (sshKeyManager.keyExists()) { - MaterialAlertDialogBuilder(this@SshKeyImportActivity).run { + if (SshKey.exists) { + MaterialAlertDialogBuilder(this).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 199fde94..89194b79 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 @@ -9,7 +9,6 @@ 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 @@ -21,6 +20,7 @@ import app.passwordstore.util.coroutines.DispatcherProvider import app.passwordstore.util.extensions.launchActivity 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 @@ -67,11 +67,7 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) { 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() + EntryPointAccessors.fromApplication<GitOperationEntryPoint>(callingActivity) protected val repository = PasswordRepository.repository!! protected val git = Git(repository) private val authActivity @@ -121,7 +117,8 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) { authMethod: SshAuthMethod, credentialsProvider: CredentialsProvider? = null ) { - sshSessionFactory = SshjSessionFactory(authMethod, hostKeyFile, sshKeyManager, hiltEntryPoint.dispatcherProvider()) + sshSessionFactory = + SshjSessionFactory(authMethod, hostKeyFile, hiltEntryPoint.dispatcherProvider()) commands.filterIsInstance<TransportCommand<*, *>>().forEach { command -> command.setTransportConfigCallback { transport: Transport -> (transport as? SshTransport)?.sshSessionFactory = sshSessionFactory @@ -169,8 +166,8 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) { suspend fun executeAfterAuthentication(authMode: AuthMode): Result<Unit, Throwable> { when (authMode) { AuthMode.SshKey -> - if (sshKeyManager.keyExists()) { - if (sshKeyManager.needsAuthentication()) { + if (SshKey.exists) { + if (SshKey.mustAuthenticate) { val result = withContext(hiltEntryPoint.dispatcherProvider().main()) { suspendCoroutine { cont -> @@ -247,8 +244,6 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) { @EntryPoint @InstallIn(SingletonComponent::class) interface GitOperationEntryPoint { - fun sshKeyManager(): SSHKeyManager - fun dispatcherProvider(): DispatcherProvider } } 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 86416cd6..5e11a636 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,7 +6,6 @@ package app.passwordstore.util.git.sshj import android.util.Base64 import androidx.appcompat.app.AppCompatActivity -import app.passwordstore.ssh.SSHKeyManager import app.passwordstore.util.coroutines.DispatcherProvider import app.passwordstore.util.git.operation.CredentialFinder import app.passwordstore.util.settings.AuthMode @@ -71,7 +70,6 @@ abstract class InteractivePasswordFinder(private val dispatcherProvider: Dispatc class SshjSessionFactory( private val authMethod: SshAuthMethod, private val hostKeyFile: File, - private val sshKeyManager: SSHKeyManager, private val dispatcherProvider: DispatcherProvider, ) : SshSessionFactory() { @@ -84,7 +82,7 @@ class SshjSessionFactory( tms: Int ): RemoteSession { return currentSession - ?: SshjSession(uri, uri.user, authMethod, hostKeyFile, dispatcherProvider, sshKeyManager).connect().also { + ?: SshjSession(uri, uri.user, authMethod, hostKeyFile, dispatcherProvider).connect().also { logcat { "New SSH connection created" } currentSession = it } @@ -129,7 +127,6 @@ private class SshjSession( private val authMethod: SshAuthMethod, private val hostKeyFile: File, private val dispatcherProvider: DispatcherProvider, - private val sshKeyManager: SSHKeyManager, ) : RemoteSession { private lateinit var ssh: SSHClient @@ -165,7 +162,10 @@ private class SshjSession( is SshAuthMethod.SshKey -> { val pubkeyAuth = AuthPublickey( - sshKeyManager.keyProvider(ssh, CredentialFinder(authMethod.activity, AuthMode.SshKey, dispatcherProvider)) + SshKey.provide( + ssh, + CredentialFinder(authMethod.activity, AuthMode.SshKey, dispatcherProvider) + ) ) ssh.auth(username, pubkeyAuth, passwordAuth) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 2ceea6d6..1c24ddac 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -194,5 +194,3 @@ include("passgen:random") include("sentry-stub") include("ui:compose") - -include("ssh") diff --git a/ssh/build.gradle.kts b/ssh/build.gradle.kts deleted file mode 100644 index a975efab..00000000 --- a/ssh/build.gradle.kts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright © The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -@file:Suppress("UnstableApiUsage") - -plugins { - id("com.github.android-password-store.android-library") - id("com.github.android-password-store.kotlin-android") -} - -android { - namespace = "app.passwordstore.ssh" - buildFeatures { androidResources = true } - sourceSets { getByName("test") { resources.srcDir("src/main/res/raw") } } -} - -dependencies { - implementation(libs.androidx.core.ktx) - implementation(libs.kotlinx.coroutines.android) - implementation(libs.kotlinx.coroutines.core) - implementation(libs.thirdparty.sshj) - implementation(libs.thirdparty.logcat) - implementation(libs.androidx.security) - implementation(libs.thirdparty.eddsa) - implementation(libs.thirdparty.kotlinResult) -} diff --git a/ssh/lint-baseline.xml b/ssh/lint-baseline.xml deleted file mode 100644 index 3a04da73..00000000 --- a/ssh/lint-baseline.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<issues format="6" by="lint 8.3.0-alpha14" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha14)" variant="all" version="8.3.0-alpha14"> - - <issue - id="InvalidPackage" - message="Invalid package reference in library; not included in Android: `javax.naming.directory`. Referenced from `org.bouncycastle.cert.dane.fetcher.JndiDANEFetcherFactory`."> - <location - file="$GRADLE_USER_HOME/caches/modules-2/files-2.1/org.bouncycastle/bcpkix-jdk18on/1.75/5adfef8a71a0933454739264b56283cc73dd2383/bcpkix-jdk18on-1.75.jar"/> - </issue> - - <issue - id="InvalidPackage" - message="Invalid package reference in library; not included in Android: `javax.naming`. Referenced from `org.bouncycastle.cert.dane.fetcher.JndiDANEFetcherFactory.1`."> - <location - file="$GRADLE_USER_HOME/caches/modules-2/files-2.1/org.bouncycastle/bcpkix-jdk18on/1.75/5adfef8a71a0933454739264b56283cc73dd2383/bcpkix-jdk18on-1.75.jar"/> - </issue> - - <issue - id="TrustAllX509TrustManager" - message="`checkServerTrusted` is empty, which could cause insecure network traffic due to trusting arbitrary TLS/SSL certificates presented by peers"> - <location - file="$GRADLE_USER_HOME/caches/modules-2/files-2.1/org.bouncycastle/bcpkix-jdk18on/1.75/5adfef8a71a0933454739264b56283cc73dd2383/bcpkix-jdk18on-1.75.jar"/> - </issue> - -</issues> diff --git a/ssh/src/main/kotlin/app/passwordstore/ssh/SSHKey.kt b/ssh/src/main/kotlin/app/passwordstore/ssh/SSHKey.kt deleted file mode 100644 index a9b7dba2..00000000 --- a/ssh/src/main/kotlin/app/passwordstore/ssh/SSHKey.kt +++ /dev/null @@ -1,5 +0,0 @@ -package app.passwordstore.ssh - -import java.io.File - -public data class SSHKey(val privateKey: File, val publicKey: File, val type: SSHKeyType) diff --git a/ssh/src/main/kotlin/app/passwordstore/ssh/SSHKeyAlgorithm.kt b/ssh/src/main/kotlin/app/passwordstore/ssh/SSHKeyAlgorithm.kt deleted file mode 100644 index 1849c04d..00000000 --- a/ssh/src/main/kotlin/app/passwordstore/ssh/SSHKeyAlgorithm.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.passwordstore.ssh - -public enum class SSHKeyAlgorithm { - RSA, - ECDSA, - ED25519, -} diff --git a/ssh/src/main/kotlin/app/passwordstore/ssh/SSHKeyManager.kt b/ssh/src/main/kotlin/app/passwordstore/ssh/SSHKeyManager.kt deleted file mode 100644 index e90eedee..00000000 --- a/ssh/src/main/kotlin/app/passwordstore/ssh/SSHKeyManager.kt +++ /dev/null @@ -1,267 +0,0 @@ -package app.passwordstore.ssh - -import android.content.Context -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Build -import android.provider.OpenableColumns -import android.security.keystore.KeyInfo -import androidx.core.content.edit -import app.passwordstore.ssh.generator.ECDSAKeyGenerator -import app.passwordstore.ssh.generator.ED25519KeyGenerator -import app.passwordstore.ssh.generator.RSAKeyGenerator -import app.passwordstore.ssh.provider.KeystoreNativeKeyProvider -import app.passwordstore.ssh.provider.KeystoreWrappedEd25519KeyProvider -import app.passwordstore.ssh.utils.Constants -import app.passwordstore.ssh.utils.Constants.ANDROIDX_SECURITY_KEYSET_PREF_NAME -import app.passwordstore.ssh.utils.Constants.KEYSTORE_ALIAS -import app.passwordstore.ssh.utils.Constants.PROVIDER_ANDROID_KEY_STORE -import app.passwordstore.ssh.utils.NullKeyException -import app.passwordstore.ssh.utils.SSHKeyNotFoundException -import app.passwordstore.ssh.utils.SSHKeyUtils -import app.passwordstore.ssh.utils.getEncryptedGitPrefs -import app.passwordstore.ssh.utils.sharedPrefs -import app.passwordstore.ssh.writer.ED25519KeyWriter -import app.passwordstore.ssh.writer.ImportedKeyWriter -import app.passwordstore.ssh.writer.KeystoreNativeKeyWriter -import com.github.michaelbull.result.getOrElse -import com.github.michaelbull.result.mapBoth -import com.github.michaelbull.result.runCatching -import java.io.File -import java.io.IOException -import java.io.InputStream -import java.security.KeyFactory -import java.security.KeyPair -import java.security.KeyStore -import java.security.PrivateKey -import javax.crypto.SecretKey -import javax.crypto.SecretKeyFactory -import logcat.asLog -import logcat.logcat -import net.schmizz.sshj.SSHClient -import net.schmizz.sshj.userauth.keyprovider.KeyProvider -import net.schmizz.sshj.userauth.password.PasswordFinder - -public class SSHKeyManager(private val applicationContext: Context) { - - private val androidKeystore: KeyStore by - lazy(LazyThreadSafetyMode.NONE) { - KeyStore.getInstance(PROVIDER_ANDROID_KEY_STORE).apply { load(null) } - } - private val isStrongBoxSupported by - lazy(LazyThreadSafetyMode.NONE) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) - applicationContext.packageManager.hasSystemFeature( - PackageManager.FEATURE_STRONGBOX_KEYSTORE - ) - else false - } - - // Let's make this suspend so that we can use datastore's non-blocking apis - private fun keyType(): SSHKeyType { - return SSHKeyType.fromValue( - applicationContext.sharedPrefs.getString(Constants.GIT_REMOTE_KEY_TYPE, null) - ) ?: throw NullKeyException() - } - - public fun keyExists(): Boolean { - return runCatching { keyType() }.mapBoth(success = { true }, failure = { false }) - } - - public fun canShowPublicKey(): Boolean = - runCatching { - keyType() in - listOf( - SSHKeyType.LegacyGenerated, - SSHKeyType.KeystoreNative, - SSHKeyType.KeystoreWrappedEd25519 - ) - } - .getOrElse { false } - - public fun publicKey(): String? = - runCatching { createNewSSHKey(keyType = keyType()).publicKey.readText() } - .getOrElse { - return null - } - - public fun needsAuthentication(): Boolean { - return runCatching { - val keyType = keyType() - if (keyType == SSHKeyType.KeystoreNative || keyType == SSHKeyType.KeystoreWrappedEd25519) - return false - - when (val key = androidKeystore.getKey(KEYSTORE_ALIAS, null)) { - is PrivateKey -> { - val factory = KeyFactory.getInstance(key.algorithm, PROVIDER_ANDROID_KEY_STORE) - factory.getKeySpec(key, KeyInfo::class.java).isUserAuthenticationRequired - } - is SecretKey -> { - val factory = SecretKeyFactory.getInstance(key.algorithm, PROVIDER_ANDROID_KEY_STORE) - (factory.getKeySpec(key, KeyInfo::class.java) as KeyInfo).isUserAuthenticationRequired - } - else -> throw SSHKeyNotFoundException() - } - } - .getOrElse { error -> - // It is fine to swallow the exception here since it will reappear when the key - // is used for SSH authentication and can then be shown in the UI. - logcat { error.asLog() } - false - } - } - - public suspend fun importKey(uri: Uri) { - // First check whether the content at uri is likely an SSH private key. - val fileSize = - applicationContext.contentResolver - .query(uri, arrayOf(OpenableColumns.SIZE), null, null, null) - ?.use { cursor -> - // Cursor returns only a single row. - cursor.moveToFirst() - cursor.getInt(0) - } ?: throw IOException(applicationContext.getString(R.string.ssh_key_does_not_exist)) - // We assume that an SSH key's ideal size is > 0 bytes && < 100 kilobytes. - require(fileSize in 1 until SSH_KEY_MAX_FILE_SIZE) { - applicationContext.getString(R.string.ssh_key_import_error_not_an_ssh_key_message) - } - val sshKeyInputStream = - applicationContext.contentResolver.openInputStream(uri) - ?: throw IOException(applicationContext.getString(R.string.ssh_key_does_not_exist)) - - importKey(sshKeyInputStream) - } - - private suspend fun importKey(sshKeyInputStream: InputStream) { - val lines = sshKeyInputStream.bufferedReader().readLines() - // The file must have more than 2 lines, and the first and last line must have private key - // markers. - check(SSHKeyUtils.isValid(lines)) { - applicationContext.getString(R.string.ssh_key_import_error_not_an_ssh_key_message) - } - // At this point, we are reasonably confident that we have actually been provided a private - // key and delete the old key. - deleteKey() - val sshKey = createNewSSHKey(keyType = SSHKeyType.Imported) - saveImportedKey(lines.joinToString("\n"), sshKey) - } - - public suspend fun generateKey(algorithm: SSHKeyAlgorithm, requiresAuthentication: Boolean) { - deleteKey() - val (sshKeyGenerator, sshKeyType) = - when (algorithm) { - SSHKeyAlgorithm.RSA -> Pair(RSAKeyGenerator(), SSHKeyType.KeystoreNative) - SSHKeyAlgorithm.ECDSA -> - Pair(ECDSAKeyGenerator(isStrongBoxSupported), SSHKeyType.KeystoreNative) - SSHKeyAlgorithm.ED25519 -> Pair(ED25519KeyGenerator(), SSHKeyType.KeystoreWrappedEd25519) - } - val keyPair = sshKeyGenerator.generateKey(requiresAuthentication) - val sshKeyFile = createNewSSHKey(keyType = sshKeyType) - saveGeneratedKey(keyPair, sshKeyFile, requiresAuthentication) - } - - private suspend fun saveGeneratedKey( - keyPair: KeyPair, - sshKey: SSHKey, - requiresAuthentication: Boolean - ) { - val sshKeyWriter = - when (sshKey.type) { - SSHKeyType.Imported -> - throw UnsupportedOperationException("KeyType imported is not supported with a KeyPair") - SSHKeyType.KeystoreNative -> KeystoreNativeKeyWriter() - SSHKeyType.KeystoreWrappedEd25519 -> - ED25519KeyWriter(applicationContext, requiresAuthentication) - SSHKeyType.LegacyGenerated -> - error("saveGeneratedKey should not be called with a legacy generated key") - } - - sshKeyWriter.writeKeyPair(keyPair, sshKey) - setSSHKeyType(sshKey.type) - } - - private suspend fun saveImportedKey(key: String, sshKey: SSHKey) { - val sshKeyWriter = - when (sshKey.type) { - SSHKeyType.Imported -> ImportedKeyWriter(key) - SSHKeyType.KeystoreNative -> - throw UnsupportedOperationException( - "KeyType KeystoreNative is not supported with a string key" - ) - SSHKeyType.KeystoreWrappedEd25519 -> - throw UnsupportedOperationException( - "KeyType KeystoreWrappedEd25519 is not supported with a string key" - ) - SSHKeyType.LegacyGenerated -> - error("saveImportedKey should not be called with a legacy generated key") - } - - sshKeyWriter.writeKeyPair(KeyPair(null, null), sshKey) - setSSHKeyType(SSHKeyType.Imported) - } - - private fun deleteKey() { - androidKeystore.deleteEntry(KEYSTORE_ALIAS) - // Remove Tink key set used by AndroidX's EncryptedFile. - applicationContext - .getSharedPreferences(ANDROIDX_SECURITY_KEYSET_PREF_NAME, Context.MODE_PRIVATE) - .edit { clear() } - // If there's no keyType(), we'll just use SSHKeyType.Imported, since they key is going to be - // deleted, it does not really matter what the key type is. - // The other way to handle this is to return if the keyType() throws an exception. - val sshKey = - runCatching { createNewSSHKey(keyType = keyType()) } - .getOrElse { createNewSSHKey(keyType = SSHKeyType.Imported) } - if (sshKey.privateKey.isFile) { - sshKey.privateKey.delete() - } - if (sshKey.publicKey.isFile) { - sshKey.publicKey.delete() - } - - clearSSHKeyPreferences() - } - - public fun keyProvider(client: SSHClient, passphraseFinder: PasswordFinder): KeyProvider? { - val sshKeyFile = - runCatching { createNewSSHKey(keyType = keyType()) } - .getOrElse { - return null - } - - return when (sshKeyFile.type) { - SSHKeyType.LegacyGenerated, - SSHKeyType.Imported -> client.loadKeys(sshKeyFile.privateKey.absolutePath, passphraseFinder) - SSHKeyType.KeystoreNative -> KeystoreNativeKeyProvider(androidKeystore) - SSHKeyType.KeystoreWrappedEd25519 -> - KeystoreWrappedEd25519KeyProvider(applicationContext, sshKeyFile) - } - } - - private fun setSSHKeyType(sshKeyType: SSHKeyType) { - applicationContext.sharedPrefs.edit { - putString(Constants.GIT_REMOTE_KEY_TYPE, sshKeyType.value) - } - } - - private fun clearSSHKeyPreferences() { - applicationContext.getEncryptedGitPrefs().edit { remove(Constants.SSH_KEY_LOCAL_PASSPHRASE) } - applicationContext.sharedPrefs.edit { remove(Constants.GIT_REMOTE_KEY_TYPE) } - } - - private fun createNewSSHKey( - keyType: SSHKeyType, - privateKeyFileName: String = Constants.PRIVATE_SSH_KEY_FILE_NAME, - publicKeyFileName: String = Constants.PUBLIC_SSH_KEY_FILE_NAME - ): SSHKey { - val privateKeyFile = File(applicationContext.filesDir, privateKeyFileName) - val publicKeyFile = File(applicationContext.filesDir, publicKeyFileName) - - return SSHKey(privateKeyFile, publicKeyFile, keyType) - } - - private companion object { - - private const val SSH_KEY_MAX_FILE_SIZE = 100_000 - } -} diff --git a/ssh/src/main/kotlin/app/passwordstore/ssh/SSHKeyType.kt b/ssh/src/main/kotlin/app/passwordstore/ssh/SSHKeyType.kt deleted file mode 100644 index b9318297..00000000 --- a/ssh/src/main/kotlin/app/passwordstore/ssh/SSHKeyType.kt +++ /dev/null @@ -1,15 +0,0 @@ -package app.passwordstore.ssh - -public enum class SSHKeyType(internal val value: String) { - Imported("imported"), - KeystoreNative("keystore_native"), - KeystoreWrappedEd25519("keystore_wrapped_ed25519"), - // Behaves like `Imported`, but allows to view the public key. - LegacyGenerated("legacy_generated"), - ; - - public companion object { - - public fun fromValue(type: String?): SSHKeyType? = entries.associateBy { it.value }[type] - } -} diff --git a/ssh/src/main/kotlin/app/passwordstore/ssh/generator/ECDSAKeyGenerator.kt b/ssh/src/main/kotlin/app/passwordstore/ssh/generator/ECDSAKeyGenerator.kt deleted file mode 100644 index b32d0933..00000000 --- a/ssh/src/main/kotlin/app/passwordstore/ssh/generator/ECDSAKeyGenerator.kt +++ /dev/null @@ -1,53 +0,0 @@ -package app.passwordstore.ssh.generator - -import android.os.Build -import android.security.keystore.KeyGenParameterSpec -import android.security.keystore.KeyProperties -import app.passwordstore.ssh.utils.Constants.KEYSTORE_ALIAS -import app.passwordstore.ssh.utils.Constants.PROVIDER_ANDROID_KEY_STORE -import java.security.KeyPair -import java.security.KeyPairGenerator - -public class ECDSAKeyGenerator(private val isStrongBoxSupported: Boolean) : SSHKeyGenerator { - - override suspend fun generateKey(requiresAuthentication: Boolean): KeyPair { - val algorithm = KeyProperties.KEY_ALGORITHM_EC - - val parameterSpec = - KeyGenParameterSpec.Builder(KEYSTORE_ALIAS, KeyProperties.PURPOSE_SIGN).run { - setKeySize(ECDSA_KEY_SIZE) - setAlgorithmParameterSpec(java.security.spec.ECGenParameterSpec("secp256r1")) - setDigests(KeyProperties.DIGEST_SHA256) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - setIsStrongBoxBacked(isStrongBoxSupported) - } - if (requiresAuthentication) { - setUserAuthenticationRequired(true) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - setUserAuthenticationParameters( - SSHKeyGenerator.USER_AUTHENTICATION_TIMEOUT, - KeyProperties.AUTH_DEVICE_CREDENTIAL - ) - } else { - @Suppress("DEPRECATION") - setUserAuthenticationValidityDurationSeconds( - SSHKeyGenerator.USER_AUTHENTICATION_TIMEOUT - ) - } - } - build() - } - - val keyPair = - KeyPairGenerator.getInstance(algorithm, PROVIDER_ANDROID_KEY_STORE).run { - initialize(parameterSpec) - generateKeyPair() - } - - return keyPair - } - - private companion object { - private const val ECDSA_KEY_SIZE = 256 - } -} diff --git a/ssh/src/main/kotlin/app/passwordstore/ssh/generator/ED25519KeyGenerator.kt b/ssh/src/main/kotlin/app/passwordstore/ssh/generator/ED25519KeyGenerator.kt deleted file mode 100644 index 418e9ad9..00000000 --- a/ssh/src/main/kotlin/app/passwordstore/ssh/generator/ED25519KeyGenerator.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.passwordstore.ssh.generator - -import java.security.KeyPair -import net.i2p.crypto.eddsa.KeyPairGenerator - -public class ED25519KeyGenerator : SSHKeyGenerator { - - override suspend fun generateKey(requiresAuthentication: Boolean): KeyPair { - // Generate the ed25519 key pair and encrypt the private key. - return KeyPairGenerator().generateKeyPair() - } -} diff --git a/ssh/src/main/kotlin/app/passwordstore/ssh/generator/RSAKeyGenerator.kt b/ssh/src/main/kotlin/app/passwordstore/ssh/generator/RSAKeyGenerator.kt deleted file mode 100644 index 54d9ae03..00000000 --- a/ssh/src/main/kotlin/app/passwordstore/ssh/generator/RSAKeyGenerator.kt +++ /dev/null @@ -1,54 +0,0 @@ -package app.passwordstore.ssh.generator - -import android.os.Build -import android.security.keystore.KeyGenParameterSpec -import android.security.keystore.KeyProperties -import app.passwordstore.ssh.utils.Constants.KEYSTORE_ALIAS -import app.passwordstore.ssh.utils.Constants.PROVIDER_ANDROID_KEY_STORE -import java.security.KeyPair -import java.security.KeyPairGenerator - -public class RSAKeyGenerator : SSHKeyGenerator { - - override suspend fun generateKey(requiresAuthentication: Boolean): KeyPair { - val algorithm = KeyProperties.KEY_ALGORITHM_RSA - // Generate Keystore-backed private key. - val parameterSpec = - KeyGenParameterSpec.Builder(KEYSTORE_ALIAS, KeyProperties.PURPOSE_SIGN).run { - setKeySize(RSA_KEY_SIZE) - setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1) - setDigests( - KeyProperties.DIGEST_SHA1, - KeyProperties.DIGEST_SHA256, - KeyProperties.DIGEST_SHA512, - ) - if (requiresAuthentication) { - setUserAuthenticationRequired(true) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - setUserAuthenticationParameters( - SSHKeyGenerator.USER_AUTHENTICATION_TIMEOUT, - KeyProperties.AUTH_DEVICE_CREDENTIAL - ) - } else { - @Suppress("DEPRECATION") - setUserAuthenticationValidityDurationSeconds( - SSHKeyGenerator.USER_AUTHENTICATION_TIMEOUT - ) - } - } - build() - } - - val keyPair = - KeyPairGenerator.getInstance(algorithm, PROVIDER_ANDROID_KEY_STORE).run { - initialize(parameterSpec) - generateKeyPair() - } - - return keyPair - } - - private companion object { - private const val RSA_KEY_SIZE = 3072 - } -} diff --git a/ssh/src/main/kotlin/app/passwordstore/ssh/generator/SSHKeyGenerator.kt b/ssh/src/main/kotlin/app/passwordstore/ssh/generator/SSHKeyGenerator.kt deleted file mode 100644 index 09a64481..00000000 --- a/ssh/src/main/kotlin/app/passwordstore/ssh/generator/SSHKeyGenerator.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.passwordstore.ssh.generator - -import java.security.KeyPair - -public interface SSHKeyGenerator { - public suspend fun generateKey(requiresAuthentication: Boolean): KeyPair - - public companion object { - public const val USER_AUTHENTICATION_TIMEOUT: Int = 30 - } -} diff --git a/ssh/src/main/kotlin/app/passwordstore/ssh/provider/KeystoreNativeKeyProvider.kt b/ssh/src/main/kotlin/app/passwordstore/ssh/provider/KeystoreNativeKeyProvider.kt deleted file mode 100644 index bc556e71..00000000 --- a/ssh/src/main/kotlin/app/passwordstore/ssh/provider/KeystoreNativeKeyProvider.kt +++ /dev/null @@ -1,37 +0,0 @@ -package app.passwordstore.ssh.provider - -import app.passwordstore.ssh.utils.Constants.KEYSTORE_ALIAS -import app.passwordstore.ssh.utils.sshPrivateKey -import app.passwordstore.ssh.utils.sshPublicKey -import com.github.michaelbull.result.getOrElse -import com.github.michaelbull.result.runCatching -import java.io.IOException -import java.security.KeyStore -import java.security.PrivateKey -import java.security.PublicKey -import logcat.asLog -import logcat.logcat -import net.schmizz.sshj.common.KeyType -import net.schmizz.sshj.userauth.keyprovider.KeyProvider - -internal class KeystoreNativeKeyProvider(private val androidKeystore: KeyStore) : KeyProvider { - - override fun getPublic(): PublicKey = - runCatching { androidKeystore.sshPublicKey!! } - .getOrElse { error -> - logcat { error.asLog() } - throw IOException("Failed to get public key '$KEYSTORE_ALIAS' from Android Keystore", error) - } - - override fun getPrivate(): PrivateKey = - runCatching { androidKeystore.sshPrivateKey!! } - .getOrElse { error -> - logcat { error.asLog() } - throw IOException( - "Failed to access private key '$KEYSTORE_ALIAS' from Android Keystore", - error - ) - } - - override fun getType(): KeyType = KeyType.fromKey(public) -} diff --git a/ssh/src/main/kotlin/app/passwordstore/ssh/provider/KeystoreWrappedEd25519KeyProvider.kt b/ssh/src/main/kotlin/app/passwordstore/ssh/provider/KeystoreWrappedEd25519KeyProvider.kt deleted file mode 100644 index 31a57998..00000000 --- a/ssh/src/main/kotlin/app/passwordstore/ssh/provider/KeystoreWrappedEd25519KeyProvider.kt +++ /dev/null @@ -1,55 +0,0 @@ -package app.passwordstore.ssh.provider - -import android.content.Context -import app.passwordstore.ssh.SSHKey -import app.passwordstore.ssh.utils.SSHKeyUtils.getOrCreateWrappedPrivateKeyFile -import app.passwordstore.ssh.utils.parseStringPublicKey -import com.github.michaelbull.result.getOrElse -import com.github.michaelbull.result.runCatching -import java.io.IOException -import java.security.PrivateKey -import java.security.PublicKey -import kotlinx.coroutines.runBlocking -import logcat.asLog -import logcat.logcat -import net.i2p.crypto.eddsa.EdDSAPrivateKey -import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable -import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec -import net.schmizz.sshj.common.KeyType -import net.schmizz.sshj.userauth.keyprovider.KeyProvider - -internal class KeystoreWrappedEd25519KeyProvider( - private val context: Context, - private val sshKeyFile: SSHKey -) : KeyProvider { - - override fun getPublic(): PublicKey = - runCatching { sshKeyFile.publicKey.readText().parseStringPublicKey()!! } - .getOrElse { error -> - logcat { error.asLog() } - throw IOException("Failed to get the public key for wrapped ed25519 key", error) - } - - override fun getPrivate(): PrivateKey = - runCatching { - // The current MasterKey API does not allow getting a reference to an existing - // one - // without specifying the KeySpec for a new one. However, the value for passed - // here - // for `requireAuthentication` is not used as the key already exists at this - // point. - val encryptedPrivateKeyFile = runBlocking { - getOrCreateWrappedPrivateKeyFile(context, false, sshKeyFile.privateKey) - } - val rawPrivateKey = encryptedPrivateKeyFile.openFileInput().use { it.readBytes() } - EdDSAPrivateKey( - EdDSAPrivateKeySpec(rawPrivateKey, EdDSANamedCurveTable.ED_25519_CURVE_SPEC) - ) - } - .getOrElse { error -> - logcat { error.asLog() } - throw IOException("Failed to unwrap wrapped ed25519 key", error) - } - - override fun getType(): KeyType = KeyType.fromKey(public) -} diff --git a/ssh/src/main/kotlin/app/passwordstore/ssh/utils/Constants.kt b/ssh/src/main/kotlin/app/passwordstore/ssh/utils/Constants.kt deleted file mode 100644 index ccc33094..00000000 --- a/ssh/src/main/kotlin/app/passwordstore/ssh/utils/Constants.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.passwordstore.ssh.utils - -internal object Constants { - const val ANDROIDX_SECURITY_KEYSET_PREF_NAME = "androidx_sshkey_keyset_prefs" - const val GIT_REMOTE_KEY_TYPE = "git_remote_key_type" - const val KEYSTORE_ALIAS = "sshkey" - const val PRIVATE_SSH_KEY_FILE_NAME = ".ssh_key" - const val PROVIDER_ANDROID_KEY_STORE = "AndroidKeyStore" - const val PUBLIC_SSH_KEY_FILE_NAME = ".ssh_key.pub" - const val SSH_KEY_LOCAL_PASSPHRASE = "ssh_key_local_passphrase" -} diff --git a/ssh/src/main/kotlin/app/passwordstore/ssh/utils/Exceptions.kt b/ssh/src/main/kotlin/app/passwordstore/ssh/utils/Exceptions.kt deleted file mode 100644 index db921ab6..00000000 --- a/ssh/src/main/kotlin/app/passwordstore/ssh/utils/Exceptions.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.passwordstore.ssh.utils - -public sealed class SSHException(message: String? = null, cause: Throwable? = null) : - Exception(message, cause) - -public class NullKeyException(message: String? = "keyType was null", cause: Throwable? = null) : - SSHException(message, cause) - -public class SSHKeyNotFoundException( - message: String? = "SSH key does not exist in Keystore", - cause: Throwable? = null -) : SSHException(message, cause) diff --git a/ssh/src/main/kotlin/app/passwordstore/ssh/utils/Extensions.kt b/ssh/src/main/kotlin/app/passwordstore/ssh/utils/Extensions.kt deleted file mode 100644 index e1c337c1..00000000 --- a/ssh/src/main/kotlin/app/passwordstore/ssh/utils/Extensions.kt +++ /dev/null @@ -1,49 +0,0 @@ -package app.passwordstore.ssh.utils - -import android.content.Context -import android.content.SharedPreferences -import android.util.Base64 -import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKey -import app.passwordstore.ssh.utils.Constants.KEYSTORE_ALIAS -import java.security.KeyStore -import java.security.PrivateKey -import java.security.PublicKey -import net.schmizz.sshj.common.Buffer -import net.schmizz.sshj.common.KeyType - -/** Get the default [SharedPreferences] instance */ -internal val Context.sharedPrefs: SharedPreferences - get() = getSharedPreferences("app.passwordstore_preferences", 0) -internal val KeyStore.sshPrivateKey - get() = getKey(KEYSTORE_ALIAS, null) as? PrivateKey -internal val KeyStore.sshPublicKey - get() = getCertificate(KEYSTORE_ALIAS)?.publicKey - -internal fun String.parseStringPublicKey(): PublicKey? { - val sshKeyParts = this.split("""\s+""".toRegex()) - if (sshKeyParts.size < 2) return null - return Buffer.PlainBuffer(Base64.decode(sshKeyParts[1], Base64.NO_WRAP)).readPublicKey() -} - -internal fun PublicKey.createStringPublicKey(): String { - val rawPublicKey = Buffer.PlainBuffer().putPublicKey(this).compactData - val keyType = KeyType.fromKey(this) - return "$keyType ${Base64.encodeToString(rawPublicKey, Base64.NO_WRAP)}" -} - -/** Wrapper for [getEncryptedPrefs] to avoid open-coding the file name at each call site */ -internal fun Context.getEncryptedGitPrefs() = getEncryptedPrefs("git_operation") - -/** Get an instance of [EncryptedSharedPreferences] with the given [fileName] */ -private fun Context.getEncryptedPrefs(fileName: String): SharedPreferences { - val masterKeyAlias = - MasterKey.Builder(applicationContext).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build() - return EncryptedSharedPreferences.create( - applicationContext, - fileName, - masterKeyAlias, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) -} diff --git a/ssh/src/main/kotlin/app/passwordstore/ssh/utils/SSHKeyUtils.kt b/ssh/src/main/kotlin/app/passwordstore/ssh/utils/SSHKeyUtils.kt deleted file mode 100644 index d192ea21..00000000 --- a/ssh/src/main/kotlin/app/passwordstore/ssh/utils/SSHKeyUtils.kt +++ /dev/null @@ -1,50 +0,0 @@ -package app.passwordstore.ssh.utils - -import android.content.Context -import androidx.security.crypto.EncryptedFile -import androidx.security.crypto.MasterKey -import java.io.File -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -internal object SSHKeyUtils { - - private const val USER_AUTHENTICATION_VALIDITY_DURATION = 15 - - fun isValid(lines: List<String>): Boolean { - return lines.size > 2 && - Regex("BEGIN .* PRIVATE KEY").containsMatchIn(lines.first()) && - Regex("END .* PRIVATE KEY").containsMatchIn(lines.last()) - } - - suspend fun getOrCreateWrappedPrivateKeyFile( - context: Context, - requiresAuthentication: Boolean, - privateKeyFile: File - ) = - withContext(Dispatchers.IO) { - EncryptedFile.Builder( - context, - privateKeyFile, - getOrCreateWrappingMasterKey(context, requiresAuthentication), - EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB - ) - .run { - setKeysetPrefName(Constants.ANDROIDX_SECURITY_KEYSET_PREF_NAME) - build() - } - } - - private suspend fun getOrCreateWrappingMasterKey( - context: Context, - requireAuthentication: Boolean - ) = - withContext(Dispatchers.IO) { - MasterKey.Builder(context, Constants.KEYSTORE_ALIAS).run { - setKeyScheme(MasterKey.KeyScheme.AES256_GCM) - setRequestStrongBoxBacked(true) - setUserAuthenticationRequired(requireAuthentication, USER_AUTHENTICATION_VALIDITY_DURATION) - build() - } - } -} diff --git a/ssh/src/main/kotlin/app/passwordstore/ssh/writer/ED25519KeyWriter.kt b/ssh/src/main/kotlin/app/passwordstore/ssh/writer/ED25519KeyWriter.kt deleted file mode 100644 index ac2c983c..00000000 --- a/ssh/src/main/kotlin/app/passwordstore/ssh/writer/ED25519KeyWriter.kt +++ /dev/null @@ -1,38 +0,0 @@ -package app.passwordstore.ssh.writer - -import android.content.Context -import app.passwordstore.ssh.SSHKey -import app.passwordstore.ssh.utils.SSHKeyUtils.getOrCreateWrappedPrivateKeyFile -import app.passwordstore.ssh.utils.createStringPublicKey -import java.io.File -import java.security.KeyPair -import java.security.PrivateKey -import java.security.PublicKey -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import net.i2p.crypto.eddsa.EdDSAPrivateKey - -public class ED25519KeyWriter( - private val context: Context, - private val requiresAuthentication: Boolean, -) : SSHKeyWriter { - - override suspend fun writeKeyPair(keyPair: KeyPair, sshKeyFile: SSHKey) { - writePrivateKey(keyPair.private, sshKeyFile.privateKey) - writePublicKey(keyPair.public, sshKeyFile.publicKey) - } - - private suspend fun writePrivateKey(privateKey: PrivateKey, privateKeyFile: File) { - withContext(Dispatchers.IO) { - val encryptedPrivateKeyFile = - getOrCreateWrappedPrivateKeyFile(context, requiresAuthentication, privateKeyFile) - encryptedPrivateKeyFile.openFileOutput().use { os -> - os.write((privateKey as EdDSAPrivateKey).seed) - } - } - } - - private suspend fun writePublicKey(publicKey: PublicKey, publicKeyFile: File) { - publicKeyFile.writeText(publicKey.createStringPublicKey()) - } -} diff --git a/ssh/src/main/kotlin/app/passwordstore/ssh/writer/ImportedKeyWriter.kt b/ssh/src/main/kotlin/app/passwordstore/ssh/writer/ImportedKeyWriter.kt deleted file mode 100644 index 809a3d60..00000000 --- a/ssh/src/main/kotlin/app/passwordstore/ssh/writer/ImportedKeyWriter.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.passwordstore.ssh.writer - -import app.passwordstore.ssh.SSHKey -import java.security.KeyPair - -public class ImportedKeyWriter(private val privateKey: String) : SSHKeyWriter { - - override suspend fun writeKeyPair(keyPair: KeyPair, sshKeyFile: SSHKey) { - // Write the string key instead of the key from the key pair - sshKeyFile.privateKey.writeText(privateKey) - } -} diff --git a/ssh/src/main/kotlin/app/passwordstore/ssh/writer/KeystoreNativeKeyWriter.kt b/ssh/src/main/kotlin/app/passwordstore/ssh/writer/KeystoreNativeKeyWriter.kt deleted file mode 100644 index 24cc02b9..00000000 --- a/ssh/src/main/kotlin/app/passwordstore/ssh/writer/KeystoreNativeKeyWriter.kt +++ /dev/null @@ -1,14 +0,0 @@ -package app.passwordstore.ssh.writer - -import app.passwordstore.ssh.SSHKey -import app.passwordstore.ssh.utils.createStringPublicKey -import java.security.KeyPair - -public class KeystoreNativeKeyWriter : SSHKeyWriter { - - override suspend fun writeKeyPair(keyPair: KeyPair, sshKeyFile: SSHKey) { - // Android Keystore manages the private key for us - // Write public key in SSH format to .ssh_key.pub. - sshKeyFile.publicKey.writeText(keyPair.public.createStringPublicKey()) - } -} diff --git a/ssh/src/main/kotlin/app/passwordstore/ssh/writer/SSHKeyWriter.kt b/ssh/src/main/kotlin/app/passwordstore/ssh/writer/SSHKeyWriter.kt deleted file mode 100644 index c5866086..00000000 --- a/ssh/src/main/kotlin/app/passwordstore/ssh/writer/SSHKeyWriter.kt +++ /dev/null @@ -1,9 +0,0 @@ -package app.passwordstore.ssh.writer - -import app.passwordstore.ssh.SSHKey -import java.security.KeyPair - -public interface SSHKeyWriter { - - public suspend fun writeKeyPair(keyPair: KeyPair, sshKeyFile: SSHKey) -} diff --git a/ssh/src/main/res/values/strings.xml b/ssh/src/main/res/values/strings.xml deleted file mode 100644 index f35fe0ac..00000000 --- a/ssh/src/main/res/values/strings.xml +++ /dev/null @@ -1,5 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <string name="ssh_key_does_not_exist">Unable to open the ssh private key, please check that the file exists</string> - <string name="ssh_key_import_error_not_an_ssh_key_message">Selected file does not appear to be an SSH private key.</string> -</resources> |