diff --git a/app/build.gradle b/app/build.gradle index bb8f0183..d168c7c6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,6 +12,8 @@ android { targetSdkVersion 25 versionCode 88 versionName "1.2.0.68" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } compileOptions { sourceCompatibility JavaVersion.VERSION_1_7 @@ -55,21 +57,35 @@ android { } dependencies { - compile 'com.android.support:appcompat-v7:25.3.1' - compile 'com.android.support:recyclerview-v7:25.3.1' - compile 'com.android.support:cardview-v7:25.3.1' - compile 'com.android.support:design:25.3.1' + compile 'com.android.support:appcompat-v7:25.4.0' + compile 'com.android.support:recyclerview-v7:25.4.0' + compile 'com.android.support:cardview-v7:25.4.0' + compile 'com.android.support:design:25.4.0' + compile 'com.android.support:support-annotations:25.4.0' compile 'org.sufficientlysecure:openpgp-api:11.0' compile 'com.nononsenseapps:filepicker:2.4.2' compile('org.eclipse.jgit:org.eclipse.jgit:3.7.1.201504261725-r') { exclude group: 'org.apache.httpcomponents', module: 'httpclient' } - compile 'com.jcraft:jsch:0.1.53' + compile 'com.jcraft:jsch:0.1.54' compile 'org.apache.commons:commons-io:1.3.2' compile 'com.jayway.android.robotium:robotium-solo:5.3.1' compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" compile 'com.android.support.constraint:constraint-layout:1.0.2' + + // Testing-only dependencies + androidTestCompile 'junit:junit:4.12' + androidTestCompile 'org.mockito:mockito-core:2.8.47' + androidTestCompile 'com.android.support.test:runner:1.0.0' + androidTestCompile 'com.android.support.test:rules:1.0.0' + androidTestCompile 'com.android.support.test.espresso:espresso-core:3.0.0' + androidTestCompile 'com.android.support.test.espresso:espresso-intents:3.0.0' + + } repositories { mavenCentral() + + // temp. solution until we use use gradle 4.0 + maven { url 'https://maven.google.com' } } diff --git a/app/src/androidTest/assets/store/dir1/f1.gpg b/app/src/androidTest/assets/store/dir1/f1.gpg new file mode 100644 index 00000000..e69de29b diff --git a/app/src/androidTest/assets/store/dir1/f11.gpg b/app/src/androidTest/assets/store/dir1/f11.gpg new file mode 100644 index 00000000..e69de29b diff --git a/app/src/androidTest/assets/store/dir2/f2.gpg b/app/src/androidTest/assets/store/dir2/f2.gpg new file mode 100644 index 00000000..e69de29b diff --git a/app/src/androidTest/assets/store/dir3/dir4/f4.gpg b/app/src/androidTest/assets/store/dir3/dir4/f4.gpg new file mode 100644 index 00000000..e69de29b diff --git a/app/src/androidTest/assets/store/dir3/f3.gpg b/app/src/androidTest/assets/store/dir3/f3.gpg new file mode 100644 index 00000000..e69de29b diff --git a/app/src/androidTest/assets/store/name1.gpg b/app/src/androidTest/assets/store/name1.gpg new file mode 100644 index 00000000..e69de29b diff --git a/app/src/androidTest/assets/store/name2.gpg b/app/src/androidTest/assets/store/name2.gpg new file mode 100644 index 00000000..e69de29b diff --git a/app/src/androidTest/java/com/zeapo/pwdstore/DecryptTest.kt b/app/src/androidTest/java/com/zeapo/pwdstore/DecryptTest.kt new file mode 100644 index 00000000..52e1e65d --- /dev/null +++ b/app/src/androidTest/java/com/zeapo/pwdstore/DecryptTest.kt @@ -0,0 +1,97 @@ +package com.zeapo.pwdstore + +import android.content.Context +import android.content.Intent +import android.support.test.InstrumentationRegistry +import android.support.test.filters.LargeTest +import android.support.test.rule.ActivityTestRule +import android.support.test.runner.AndroidJUnit4 +import android.util.Log +import com.zeapo.pwdstore.crypto.PgpActivity +import kotlinx.android.synthetic.main.decrypt_layout.* +import org.apache.commons.io.IOUtils +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.io.File +import java.io.FileOutputStream +import java.io.IOException + + +@RunWith(AndroidJUnit4::class) +@LargeTest +class WelcomeActivityTest { + @Rule @JvmField + var mActivityRule: ActivityTestRule = ActivityTestRule(PgpActivity::class.java, true, false) + + @Test + fun pathShouldDecompose() { + val path = "/data/my.app.com/files/store/cat1/name.gpg" + val repoPath = "/data/my.app.com/files/store" + assertEquals("/cat1/name.gpg", PgpActivity.getRelativePath(path, repoPath)) + assertEquals("/cat1/", PgpActivity.getParentPath(path, repoPath)) + assertEquals("name", PgpActivity.getName(path, repoPath)) + assertEquals("name", PgpActivity.getName(path, "$repoPath/")) + } + + @Test + fun activityShouldShowName() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val name = "name" + val parentPath = "/cat1/" + val repoPath = "${context.filesDir}/store/" + val path = "$repoPath/cat1/name.gpg" + + + val intent = Intent(context, PgpActivity::class.java) + intent.putExtra("OPERATION", "DECRYPT") + intent.putExtra("FILE_PATH", path) + intent.putExtra("REPO_PATH", repoPath) + + copyAssets(context, "store", context.filesDir.absolutePath) + + val activity: PgpActivity = mActivityRule.launchActivity(intent) + + val categoryView = activity.crypto_password_category_decrypt + assertNotNull(categoryView) + assertEquals(parentPath, categoryView.text) + + val nameView = activity.crypto_password_file + assertNotNull(nameView) + assertEquals(name, nameView.text) + } + + companion object { + fun copyAssets(context: Context, source: String, destination: String) { + val assetManager = context.assets + val files: Array? = assetManager.list(source) + + files?.map { filename -> + val destPath = "$destination/$filename" + val sourcePath = "$source/$filename" + if (assetManager.list(filename).isNotEmpty()) { + File(destPath).mkdir() + copyAssets(context, "$source/$filename", destPath) + } else { + try { + val input = assetManager.open(sourcePath) + val outFile = File(destination, filename) + val output = FileOutputStream(outFile) + IOUtils.copy(input, output) + input.close() + output.flush() + output.close() + } catch (e: IOException) { + Log.e("tag", "Failed to copy asset file: " + filename, e) + } + } + } + } + } + +} + + + diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt index e48b9a03..96cca034 100644 --- a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt @@ -37,24 +37,17 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager } var passwordEntry: PasswordEntry? = null + var api : OpenPgpApi? = null - val name: String by lazy { intent.getStringExtra("NAME") } - val repoPath: String by lazy { intent.getStringExtra("REPO_PATH") } - val path: String by lazy { intent.getStringExtra("FILE_PATH") } - val parentPath: String by lazy { - // when encrypting we pass "PARENT_PATH" as we do not have a file - if (operation == "ENCRYPT") intent.getStringExtra("PARENT_PATH") - else File(path).parentFile.absolutePath - } - val cat: String by lazy { parentPath.replace(repoPath, "") } val operation: String by lazy { intent.getStringExtra("OPERATION") } + val repoPath: String by lazy { intent.getStringExtra("REPO_PATH") } - val settings: SharedPreferences by lazy { - PreferenceManager.getDefaultSharedPreferences(this) - } - val keyIDs: MutableSet by lazy { - settings.getStringSet("openpgp_key_ids_set", emptySet()) - } + val path: String by lazy { intent.getStringExtra("FILE_PATH") } + val name: String by lazy { getName(path, repoPath) } + val relativeParentPath: String by lazy { getParentPath(path, repoPath) } + + val settings: SharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) } + val keyIDs: MutableSet by lazy { settings.getStringSet("openpgp_key_ids_set", emptySet()) } var mServiceConnection: OpenPgpServiceConnection? = null override fun onCreate(savedInstanceState: Bundle?) { @@ -78,13 +71,13 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { when (operation) { "DECRYPT", "EDIT" -> { setContentView(R.layout.decrypt_layout) - crypto_password_category_decrypt.text = "$cat/" + crypto_password_category_decrypt.text = relativeParentPath crypto_password_file.text = name } "ENCRYPT" -> { setContentView(R.layout.encrypt_layout) title = getString(R.string.new_password_title) - crypto_password_category.text = "$cat/" + crypto_password_category.text = relativeParentPath } } } @@ -178,6 +171,10 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { Log.e(TAG, "onError getMessage:" + error.message) } + fun initOpenPgpApi() { + api = api ?: OpenPgpApi(this, mServiceConnection?.service) + } + private fun decryptAndVerify(receivedIntent: Intent? = null): Unit { val data = receivedIntent ?: Intent() data.action = ACTION_DECRYPT_VERIFY @@ -185,64 +182,62 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { val iStream = FileUtils.openInputStream(File(path)) val oStream = ByteArrayOutputStream() - val api = OpenPgpApi(this, mServiceConnection?.service) - - api.executeApiAsync(data, iStream, oStream, { result: Intent? -> + api?.executeApiAsync(data, iStream, oStream, { result: Intent? -> when (result?.getIntExtra(RESULT_CODE, RESULT_CODE_ERROR)) { RESULT_CODE_SUCCESS -> { try { - val showPassword = settings.getBoolean("show_password", true) - val showExtraContent = settings.getBoolean("show_extra_content", true) - - crypto_container_decrypt.visibility = View.VISIBLE - - val monoTypeface = Typeface.createFromAsset(assets, "fonts/sourcecodepro.ttf") - val entry = PasswordEntry(oStream) - - passwordEntry = entry - - if (operation == "EDIT") { - editPassword() - return@executeApiAsync - } - - crypto_password_show.typeface = monoTypeface - crypto_password_show.text = entry.password - - crypto_password_toggle_show.visibility = if (showPassword) View.GONE else View.VISIBLE - crypto_password_show.transformationMethod = if (showPassword) { - null - } else { - HoldToShowPasswordTransformation( - crypto_password_toggle_show, - Runnable { crypto_password_show.text = entry.password } - ) - } - - if (entry.hasExtraContent()) { - crypto_extra_show_layout.visibility = if (showExtraContent) View.VISIBLE else View.GONE - - crypto_extra_show.typeface = monoTypeface - crypto_extra_show.text = entry.extraContent - - if (entry.hasUsername()) { - crypto_username_show.visibility = View.VISIBLE - crypto_username_show_label.visibility = View.VISIBLE - crypto_copy_username.visibility = View.VISIBLE - - crypto_copy_username.setOnClickListener { copyUsernameToClipBoard(entry.username) } - crypto_username_show.typeface = monoTypeface - crypto_username_show.text = entry.username - } else { - crypto_username_show.visibility = View.GONE - crypto_username_show_label.visibility = View.GONE - crypto_copy_username.visibility = View.GONE - } - } - - if (settings.getBoolean("copy_on_decrypt", true)) { - copyPasswordToClipBoard() - } +// val showPassword = settings.getBoolean("show_password", true) +// val showExtraContent = settings.getBoolean("show_extra_content", true) +// +// crypto_container_decrypt.visibility = View.VISIBLE +// +// val monoTypeface = Typeface.createFromAsset(assets, "fonts/sourcecodepro.ttf") +// val entry = PasswordEntry(oStream) +// +// passwordEntry = entry +// +// if (operation == "EDIT") { +// editPassword() +// return@executeApiAsync +// } +// +// crypto_password_show.typeface = monoTypeface +// crypto_password_show.text = entry.password +// +// crypto_password_toggle_show.visibility = if (showPassword) View.GONE else View.VISIBLE +// crypto_password_show.transformationMethod = if (showPassword) { +// null +// } else { +// HoldToShowPasswordTransformation( +// crypto_password_toggle_show, +// Runnable { crypto_password_show.text = entry.password } +// ) +// } +// +// if (entry.hasExtraContent()) { +// crypto_extra_show_layout.visibility = if (showExtraContent) View.VISIBLE else View.GONE +// +// crypto_extra_show.typeface = monoTypeface +// crypto_extra_show.text = entry.extraContent +// +// if (entry.hasUsername()) { +// crypto_username_show.visibility = View.VISIBLE +// crypto_username_show_label.visibility = View.VISIBLE +// crypto_copy_username.visibility = View.VISIBLE +// +// crypto_copy_username.setOnClickListener { copyUsernameToClipBoard(entry.username) } +// crypto_username_show.typeface = monoTypeface +// crypto_username_show.text = entry.username +// } else { +// crypto_username_show.visibility = View.GONE +// crypto_username_show_label.visibility = View.GONE +// crypto_copy_username.visibility = View.GONE +// } +// } +// +// if (settings.getBoolean("copy_on_decrypt", true)) { +// copyPasswordToClipBoard() +// } } catch (e: Exception) { Log.e(TAG, "An Exception occurred", e) } @@ -284,10 +279,9 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { val iStream = ByteArrayInputStream("$pass\n$extra".toByteArray(Charset.forName("UTF-8"))) val oStream = ByteArrayOutputStream() - val api = OpenPgpApi(this, mServiceConnection?.service) - val path = "$parentPath/$name.gpg" + val path = "$repoPath/$relativeParentPath/$name.gpg" - api.executeApiAsync(data, iStream, oStream, { result: Intent? -> + api?.executeApiAsync(data, iStream, oStream, { result: Intent? -> when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { OpenPgpApi.RESULT_CODE_SUCCESS -> { try { @@ -332,7 +326,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { crypto_extra_edit.setText(passwordEntry?.extraContent) crypto_extra_edit.typeface = monoTypeface - crypto_password_category.text = "$cat/" + crypto_password_category.text = relativeParentPath crypto_password_file_edit.setText(name) crypto_password_file_edit.isEnabled = false @@ -351,8 +345,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { fun getKeyIds(receivedIntent: Intent? = null) { val data = receivedIntent ?: Intent() data.action = OpenPgpApi.ACTION_GET_KEY_IDS - val api = OpenPgpApi(this, mServiceConnection?.service) - api.executeApiAsync(data, null, null, { result: Intent? -> + api?.executeApiAsync(data, null, null, { result: Intent? -> when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { OpenPgpApi.RESULT_CODE_SUCCESS -> { try { @@ -382,6 +375,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { * The action to take when the PGP service is bound */ override fun onBound(service: IOpenPgpService2?) { + initOpenPgpApi() when (operation) { "EDIT", "DECRYPT" -> decryptAndVerify() "GET_KEY_ID" -> getKeyIds() @@ -572,6 +566,29 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { private var delayTask: DelayShow? = null + /** + * Gets the relative path to the repository + */ + fun getRelativePath(fullPath: String, repositoryPath: String): String = + fullPath.replace(repositoryPath, "").replace("//", "/") + + /** + * Gets the Parent path, relative to the repository + */ + fun getParentPath(fullPath: String, repositoryPath: String) : String { + val relativePath = getRelativePath(fullPath, repositoryPath) + val index = relativePath.lastIndexOf("/") + return "/${relativePath.substring(startIndex = 0, endIndex = index + 1)}/".replace("//", "/") + } + + /** + * Gets the name of the password (excluding .gpg) + */ + fun getName(fullPath: String, repositoryPath: String) : String { + val relativePath = getRelativePath(fullPath, repositoryPath) + val index = relativePath.lastIndexOf("/") + return relativePath.substring(index + 1).replace("\\.gpg$".toRegex(), "") + } } }