diff options
author | Harsh Shandilya <me@msfjarvis.dev> | 2023-12-15 17:40:11 +0530 |
---|---|---|
committer | Harsh Shandilya <me@msfjarvis.dev> | 2023-12-15 18:53:58 +0530 |
commit | ced2008a85250a20e409364da9f8e640347adb1f (patch) | |
tree | ff0b10762d7a72b34c151362898070002122a75b /ssh | |
parent | 4d5b32d98b0c97a790eb429844c79ef5d37d9e7a (diff) |
Revert "Refactor SSHKey into a separate module (#2450)"
This reverts commit 97b3577a463966e93d24649ff348fc4bb6825e50.
Diffstat (limited to 'ssh')
21 files changed, 0 insertions, 768 deletions
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> |