Skip to content

Commit

Permalink
Add a check for wrong file selection during private key import
Browse files Browse the repository at this point in the history
Also added some tests for PK import.
  • Loading branch information
gujjwal00 committed Mar 19, 2023
1 parent 5976505 commit 9bfdc85
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 10 deletions.
27 changes: 26 additions & 1 deletion app/src/androidTest/java/com/gaurav/avnc/Helpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,31 @@

package com.gaurav.avnc

import android.app.Activity
import android.app.Instrumentation
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.os.SystemClock
import android.view.View
import android.widget.ProgressBar
import androidx.core.net.toUri
import androidx.preference.PreferenceManager
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.ViewAssertion
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.platform.app.InstrumentationRegistry
import junit.framework.AssertionFailedError
import org.hamcrest.core.IsNot.not
import org.junit.Assert
import java.io.BufferedWriter
import java.io.File

/**
* Global accessors
Expand Down Expand Up @@ -93,4 +101,21 @@ class ProgressAssertion(private val test: (Int) -> Boolean) : ViewAssertion {
if (view !is ProgressBar) throw AssertionFailedError("View is not a ProgressBar")
Assert.assertTrue("Progress test failed for '${view.progress}'", test(view.progress))
}
}
}

/**
* Helper for testing file selection by user.
*
* It creates a temporary file, populated by invoking [fileWriter]
* Then Intent response is hooked up to return that file.
*
* Note: [Intents.init] must have been called before using this function
*/
fun setupFileOpenIntent(fileWriter: BufferedWriter.() -> Unit) {
val file = File.createTempFile("avnc", "test")
file.bufferedWriter().use { it.fileWriter() }
Intents.intending(IntentMatchers.hasAction(Intent.ACTION_OPEN_DOCUMENT))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent().setData(file.toUri())))
}

fun setupFileOpenIntent(fileContent: String) = setupFileOpenIntent { write(fileContent) }
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,26 @@

package com.gaurav.avnc.ui.home

import androidx.core.content.edit
import androidx.test.espresso.Espresso.closeSoftKeyboard
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ScrollToAction
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.gaurav.avnc.*
import com.gaurav.avnc.model.ServerProfile
import org.hamcrest.core.AllOf.allOf
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class ProfileEditorTest {
class BasicEditorTest {

private val testProfile = ServerProfile(name = "foo", host = "bar")

Expand Down Expand Up @@ -68,6 +73,70 @@ class ProfileEditorTest {
hasDescendant(withText(testProfile.host)))
).checkWillBeDisplayed()
}
}

//TODO add more tests
@RunWith(AndroidJUnit4::class)
class PrivateKeyTest {

private val SAMPLE_KEY = """
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIOlzJCBaTq2RpkLatMn+PMwsQ/ncijqK5M6y5kPns+BAoAoGCCqGSM49
AwEHoUQDQgAE5vfo9Is5+pnk+6dC3NlXA9rKNy0X6PdifyeI2OKKTGVA4cdhEOBh
MXTgJKarp26FiFnsFuWEGOO61Q/Plmv8Wg==
-----END EC PRIVATE KEY-----
""".trimIndent()

private val SAMPLE_ENCRYPTED_KEY = """
-----BEGIN EC PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,D18B845F2B9D4B9FEB3472809F270AD2
fNNjQEFap5uf3+gaQNI+A2ait7WZDjxPO6sT6MfOTQ34/HRxm7El4FWhhYMfIiGl
5j/A30Je2OMbOEqTpiKZKYxKxWIZ27te8HpxsQkC9xvU9rxPPsd0Vt/7na5rbx1S
Iqfy2ows7NJY3f3s/GlMY2ZmaAwk6TJ3DAJ+YK0KMrc=
-----END EC PRIVATE KEY-----
""".trimIndent()

@Rule
@JvmField
val activityRule = ActivityScenarioRule(HomeActivity::class.java)

@Before
fun before() {
Intents.init()
targetPrefs.edit { putBoolean("prefer_advanced_editor", true) }
onView(withContentDescription(R.string.desc_add_new_server_btn)).doClick()
onView(withId(R.id.use_ssh_tunnel)).perform(ScrollToAction())
onView(withId(R.id.use_ssh_tunnel)).doClick()
onView(withId(R.id.key_import_btn)).perform(ScrollToAction())
}

@After
fun after() {
targetPrefs.edit { putBoolean("prefer_advanced_editor", false) }
Intents.release()
}

@Test
fun importValidPK() {
setupFileOpenIntent(SAMPLE_KEY)
onView(withId(R.id.key_import_btn)).doClick()
onView(withText(R.string.msg_imported)).checkWillBeDisplayed()
onView(withId(R.id.ssh_key_password)).checkIsNotDisplayed()
}

@Test
fun importEncryptedPK() {
setupFileOpenIntent(SAMPLE_ENCRYPTED_KEY)
onView(withId(R.id.key_import_btn)).doClick()
onView(withText(R.string.msg_imported)).checkWillBeDisplayed()
onView(withId(R.id.ssh_key_password)).checkWillBeDisplayed()
}

@Test
fun importInvalidFile() {
setupFileOpenIntent("This is not a Private Key, right?")
onView(withId(R.id.key_import_btn)).doClick()
onView(withText(R.string.msg_invalid_key_file)).checkWillBeDisplayed()
}
}
23 changes: 16 additions & 7 deletions app/src/main/java/com/gaurav/avnc/ui/home/ProfileEditorFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package com.gaurav.avnc.ui.home
import android.app.Dialog
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
Expand Down Expand Up @@ -304,20 +305,28 @@ class ProfileEditorFragment : DialogFragment() {
return

lifecycleScope.launch(Dispatchers.IO) {

val text = requireContext().contentResolver.openInputStream(uri).use { it?.reader()?.readText() ?: "" }
val pem = runCatching { PEMDecoder.parsePEM(text.toCharArray()) }
val encrypted = runCatching { PEMDecoder.isPEMEncrypted(pem.getOrNull()) }.getOrNull() ?: false
var key = ""
var encrypted = false
val result = runCatching {
requireContext().contentResolver.openAssetFileDescriptor(uri, "r")!!.use {
// Valid key files are only few KBs. So if selected file is too big,
// user has accidentally selected something else.
check(it.length < 200 * 1024) { "File is too big [${it.length}]" }
key = it.createInputStream().use { s -> s.reader().use { r -> r.readText() } }
}
encrypted = PEMDecoder.isPEMEncrypted(PEMDecoder.parsePEM(key.toCharArray()))
}

lifecycleScope.launchWhenCreated {
if (pem.isSuccess) {
profile.sshPrivateKey = text
result.onSuccess {
profile.sshPrivateKey = key
binding.keyImportBtn.setText(R.string.title_change)
binding.keyImportBtn.error = null
binding.isPrivateKeyEncrypted = encrypted
Snackbar.make(requireView(), R.string.msg_imported, Snackbar.LENGTH_SHORT).show()
} else {
}.onFailure {
Snackbar.make(requireView(), R.string.msg_invalid_key_file, Snackbar.LENGTH_LONG).show()
Log.e("ProfileEditor", "Error importing Private Key", it)
}
}
}
Expand Down

0 comments on commit 9bfdc85

Please sign in to comment.