From c3eb4f0a414b4bceb79b3c19eec2f69528ced02b Mon Sep 17 00:00:00 2001 From: Mohamed Zenadi Date: Thu, 27 Jul 2017 00:40:16 +0100 Subject: initial work on pgp activity --- .../java/com/zeapo/pwdstore/PasswordStore.java | 14 +- .../java/com/zeapo/pwdstore/crypto/PgpActivity.kt | 533 +++++++++++++++++++++ .../java/com/zeapo/pwdstore/crypto/PgpHandler.java | 10 +- 3 files changed, 547 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt (limited to 'app/src/main/java') diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java index 6c232ab8..665e1676 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java @@ -30,6 +30,7 @@ import android.view.MenuItem; import android.view.View; import android.widget.TextView; +import com.zeapo.pwdstore.crypto.PgpActivity; import com.zeapo.pwdstore.crypto.PgpHandler; import com.zeapo.pwdstore.git.GitActivity; import com.zeapo.pwdstore.git.GitAsyncTask; @@ -395,10 +396,11 @@ public class PasswordStore extends AppCompatActivity { } public void decryptPassword(PasswordItem item) { - Intent intent = new Intent(this, PgpHandler.class); + Intent intent = new Intent(this, PgpActivity.class); intent.putExtra("NAME", item.toString()); intent.putExtra("FILE_PATH", item.getFile().getAbsolutePath()); - intent.putExtra("Operation", "DECRYPT"); + intent.putExtra("REPO_PATH", PasswordRepository.getRepositoryDirectory(getApplicationContext()).getAbsolutePath()); + intent.putExtra("OPERATION", "DECRYPT"); // Adds shortcut if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { @@ -459,9 +461,10 @@ public class PasswordStore extends AppCompatActivity { File currentDir = getCurrentDir(); Log.i("PWDSTR", "Adding file to : " + currentDir.getAbsolutePath()); - Intent intent = new Intent(this, PgpHandler.class); - intent.putExtra("FILE_PATH", getCurrentDir().getAbsolutePath()); - intent.putExtra("Operation", "ENCRYPT"); + Intent intent = new Intent(this, PgpActivity.class); + intent.putExtra("PARENT_PATH", getCurrentDir().getAbsolutePath()); + intent.putExtra("REPO_PATH", PasswordRepository.getRepositoryDirectory(getApplicationContext()).getAbsolutePath()); + intent.putExtra("OPERATION", "ENCRYPT"); startActivityForResult(intent, PgpHandler.REQUEST_CODE_ENCRYPT); } @@ -620,6 +623,7 @@ public class PasswordStore extends AppCompatActivity { break; } + // TODO move this to an async task for (String string : data.getStringArrayListExtra("Files")) { File source = new File(string); if (!source.exists()) { diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt new file mode 100644 index 00000000..749d0be3 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt @@ -0,0 +1,533 @@ +package com.zeapo.pwdstore.crypto + +import android.app.Activity +import android.app.PendingIntent +import android.content.* +import android.graphics.Typeface +import android.os.AsyncTask +import android.os.Bundle +import android.os.Handler +import android.os.SystemClock +import android.preference.PreferenceManager +import android.support.v7.app.AppCompatActivity +import android.text.TextUtils +import android.text.method.PasswordTransformationMethod +import android.util.Log +import android.view.* +import android.widget.* +import com.zeapo.pwdstore.PasswordEntry +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.UserPreference +import com.zeapo.pwdstore.pwgenDialogFragment +import kotlinx.android.synthetic.main.decrypt_layout.* +import kotlinx.android.synthetic.main.encrypt_layout.* +import org.apache.commons.io.FileUtils +import org.openintents.openpgp.IOpenPgpService2 +import org.openintents.openpgp.OpenPgpError +import org.openintents.openpgp.util.OpenPgpApi +import org.openintents.openpgp.util.OpenPgpApi.* +import org.openintents.openpgp.util.OpenPgpServiceConnection +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.nio.charset.Charset + +class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { + val clipboard: ClipboardManager by lazy { + getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + } + var passwordEntry: PasswordEntry? = 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 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?) { + super.onCreate(savedInstanceState) + window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) + + // some persistence + val providerPackageName = settings.getString("openpgp_provider_list", "") + + if (TextUtils.isEmpty(providerPackageName)) { + Toast.makeText(this, this.resources.getString(R.string.provider_toast_text), Toast.LENGTH_LONG).show() + val intent = Intent(this, UserPreference::class.java) + startActivityForResult(intent, OPEN_PGP_BOUND) + } else { + // bind to service + mServiceConnection = OpenPgpServiceConnection(this, providerPackageName, this) + mServiceConnection?.bindToService() + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + when (operation) { + "DECRYPT" -> { + setContentView(R.layout.decrypt_layout) + crypto_password_category_decrypt.text = "$cat/" + crypto_password_file.text = name + } + "ENCRYPT" -> { + setContentView(R.layout.encrypt_layout) + title = getString(R.string.new_password_title) + crypto_password_category.text = "$cat/" + } + } + } + + override fun onDestroy() { + super.onDestroy() + mServiceConnection?.unbindFromService() + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + // Inflate the menu; this adds items to the action bar if it is present. + // Do not use the value `operation` in this case as it is not valid when editing + val menuId = when (intent.getStringExtra("OPERATION")) { + "ENCRYPT" -> R.menu.pgp_handler_new_password + "DECRYPT" -> R.menu.pgp_handler + else -> R.menu.pgp_handler + } + + menuInflater.inflate(menuId, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + when (item?.itemId) { + android.R.id.home -> { + setResult(RESULT_CANCELED) + finish() + } + R.id.copy_password -> copyPasswordToClipBoard() + R.id.share_password_as_plaintext -> shareAsPlaintext() + R.id.edit_password -> editPassword() + R.id.crypto_confirm_add -> encrypt() + R.id.crypto_cancel_add -> setResult(RESULT_CANCELED) + else -> return super.onOptionsItemSelected(item) + } + return true + } + + fun handleClick(view: View) { + when (view.id) { + R.id.generate_password -> { + val df = pwgenDialogFragment() + df.show(fragmentManager, "generator") + Log.wtf(TAG, "This should not happen.... PgpHandler.java#handleClick(View) default reached.") + } + else -> Log.wtf(TAG, "This should not happen.... PgpHandler.java#handleClick(View) default reached.") + }// should not happen + } + + + fun showToast(message: String) { + runOnUiThread({ Toast.makeText(this, message, Toast.LENGTH_SHORT).show() }) + } + + fun handleError(error: OpenPgpError) { + showToast("Error from OpenKeyChain : " + error.message) + Log.e(TAG, "onError getErrorId:" + error.errorId) + Log.e(TAG, "onError getMessage:" + error.message) + } + + private fun decryptAndVerify(): Unit { + val data = Intent() + data.action = ACTION_DECRYPT_VERIFY + + val iStream = FileUtils.openInputStream(File(path)) + val oStream = ByteArrayOutputStream() + + val api = OpenPgpApi(this, mServiceConnection?.service) + + 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 + + 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) + } + } + RESULT_CODE_USER_INTERACTION_REQUIRED -> { + Log.i(TAG, "RESULT_CODE_USER_INTERACTION_REQUIRED") + + val pi: PendingIntent = result.getParcelableExtra(RESULT_INTENT) + try { + this@PgpActivity.startIntentSenderFromChild( + this@PgpActivity, pi.intentSender, REQUEST_DECRYPT, + null, 0, 0, 0) + } catch (e: IntentSender.SendIntentException) { + Log.e(TAG, "SendIntentException", e) + } + } + RESULT_CODE_ERROR -> { + // TODO show what kind of error it is + /* For example: + * No suitable key found -> no key in OpenKeyChain + * + * Check in open-pgp-lib how their definitions and error code + */ + val error: OpenPgpError = result.getParcelableExtra(RESULT_ERROR) + handleError(error) + } + } + + }) + } + + /** + * Encrypts the password and the extra content + */ + private fun encrypt(): Unit { + val name = crypto_password_file_edit.text.toString() + val pass = crypto_password_edit.text.toString() + val extra = crypto_extra_edit.text.toString() + + if (name.isEmpty()) { + showToast(resources.getString(R.string.file_toast_text)) + return + } + + if (pass.isEmpty() && extra.isEmpty()) { + showToast(resources.getString(R.string.empty_toast_text)) + return + } + + val data = Intent() + data.action = OpenPgpApi.ACTION_ENCRYPT + + // EXTRA_KEY_IDS requires long[] + val longKeys = keyIDs.map { it.toLong() } + data.putExtra(OpenPgpApi.EXTRA_KEY_IDS, longKeys.toLongArray()) + data.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true) + + // TODO Check if we could use PasswordEntry to generate the file + 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" + + api.executeApiAsync(data, iStream, oStream, { result: Intent? -> + when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { + OpenPgpApi.RESULT_CODE_SUCCESS -> { + try { + // TODO This might fail, we should check that the write is successful + val outputStream = FileUtils.openOutputStream(File(path)) + outputStream.write(oStream.toByteArray()) + outputStream.close() + + val returnIntent = Intent() + returnIntent.putExtra("CREATED_FILE", path) + returnIntent.putExtra("NAME", name) + + // if coming from decrypt screen->edit button + if (intent.getBooleanExtra("fromDecrypt", false)) { + data.putExtra("needCommit", true) + } + + setResult(RESULT_OK, returnIntent) + finish() + } catch (e: Exception) { + Log.e(TAG, "An Exception occurred", e) + } + } + OpenPgpApi.RESULT_CODE_ERROR -> { + // TODO show what kind of error it is + /* For example: + * No suitable key found -> no key in OpenKeyChain + * + * Check in open-pgp-lib how their definitions and error code + */ + val error: OpenPgpError = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR) + handleError(error) + } + } + + }) + } + + + /** + * Opens EncryptActivity with the information for this file to be edited + */ + fun editPassword() { + setContentView(R.layout.encrypt_layout) + + title = getString(R.string.edit_password_title) + + val monoTypeface = Typeface.createFromAsset(assets, "fonts/sourcecodepro.ttf") + crypto_password_edit.setText(passwordEntry?.password) + crypto_password_edit.typeface = monoTypeface + crypto_extra_edit.setText(passwordEntry?.extraContent) + crypto_extra_edit.typeface = monoTypeface + + crypto_password_category.text = "$cat/" + crypto_password_file_edit.setText(name) + crypto_password_file_edit.isEnabled = false + + delayTask?.skip = true + + val data = Intent(this, PgpActivity::class.java) + data.putExtra("OPERATION", "ENCRYPT") + data.putExtra("fromDecrypt", true) + intent = data + invalidateOptionsMenu() + + } + + override fun onError(e: Exception?) {} + + /** + * The action to take when the PGP service is bound + */ + override fun onBound(service: IOpenPgpService2?) { + if (operation == "DECRYPT") decryptAndVerify() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + Log.d(TAG, "onActivityResult resultCode: " + resultCode) + + if (data == null) { + setResult(Activity.RESULT_CANCELED, null) + finish() + return + } + + // try again after user interaction + if (resultCode == Activity.RESULT_OK) { + when (requestCode) { + REQUEST_DECRYPT -> decryptAndVerify() + else -> { + setResult(Activity.RESULT_OK) + finish() + } + } + } else if (resultCode == Activity.RESULT_CANCELED) { + setResult(Activity.RESULT_CANCELED, data) + finish() + } + } + + private inner class HoldToShowPasswordTransformation constructor(button: Button, private val onToggle: Runnable) : + PasswordTransformationMethod(), View.OnTouchListener { + private var shown = false + + init { + button.setOnTouchListener(this) + } + + override fun getTransformation(charSequence: CharSequence, view: View): CharSequence { + return if (shown) charSequence else super.getTransformation("12345", view) + } + + override fun onTouch(view: View, motionEvent: MotionEvent): Boolean { + when (motionEvent.action) { + MotionEvent.ACTION_DOWN -> { + shown = true + onToggle.run() + } + MotionEvent.ACTION_UP -> { + shown = false + onToggle.run() + } + } + return false + } + } + + fun copyPasswordToClipBoard() { + + if (findViewById(R.id.crypto_password_show) == null) + return + + setTimer() + + val clip = ClipData.newPlainText("pgp_handler_result_pm", passwordEntry?.password) + clipboard.primaryClip = clip + + var clearAfter = 45 + try { + clearAfter = Integer.parseInt(settings.getString("general_show_time", "45")) + } catch (e: NumberFormatException) { + // ignore and keep default + } + + showToast(this.resources.getString(R.string.clipboard_password_toast_text, clearAfter)) + } + + fun copyUsernameToClipBoard(username: String) { + val clip = ClipData.newPlainText("pgp_handler_result_pm", username) + clipboard.primaryClip = clip + showToast(resources.getString(R.string.clipboard_username_toast_text)) + } + + + fun shareAsPlaintext() { + if (findViewById(R.id.share_password_as_plaintext) == null) + return + + val sendIntent = Intent() + sendIntent.action = Intent.ACTION_SEND + sendIntent.putExtra(Intent.EXTRA_TEXT, passwordEntry?.password) + sendIntent.type = "text/plain" + startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_plaintext_password_to)))//Always show a picker to give the user a chance to cancel + } + + fun setTimer() { + delayTask?.skip = true + + // launch a new one + delayTask = DelayShow(this) + delayTask?.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) + } + + inner class DelayShow(val activity: PgpActivity) : AsyncTask() { + internal val pb: ProgressBar by lazy { pbLoading } + internal var skip = false + internal var showTime: Int = 0 + + val settings: SharedPreferences by lazy { + PreferenceManager.getDefaultSharedPreferences(activity) + } + + override fun onPreExecute() { + try { + showTime = Integer.parseInt(settings.getString("general_show_time", "45")) + } catch (e: NumberFormatException) { + showTime = 45 + } + + val container = findViewById(R.id.crypto_container_decrypt) as LinearLayout + container.visibility = View.VISIBLE + + val extraText = findViewById(R.id.crypto_extra_show) as TextView + + if (extraText.text.isNotEmpty()) + findViewById(R.id.crypto_extra_show_layout).visibility = View.VISIBLE + + if (showTime == 0) { + // treat 0 as forever, and the user must exit and/or clear clipboard on their own + cancel(true) + } else { + this.pb.max = showTime + } + } + + override fun doInBackground(vararg params: Void): Boolean? { + var current = 0 + while (current < showTime) { + SystemClock.sleep(1000) + current++ + publishProgress(current) + } + return true + } + + override fun onPostExecute(b: Boolean?) { + if (skip) return + + // only clear the clipboard if we automatically copied the password to it + if (settings.getBoolean("copy_on_decrypt", true)) { + Log.d("DELAY_SHOW", "Clearing the clipboard") + val clip = ClipData.newPlainText("pgp_handler_result_pm", "") + clipboard.primaryClip = clip + if (settings.getBoolean("clear_clipboard_20x", false)) { + val handler = Handler() + for (i in 0..18) { + val count = i.toString() + handler.postDelayed({ clipboard.primaryClip = ClipData.newPlainText(count, count) }, (i * 500).toLong()) + } + } + } + + if (crypto_password_show != null) { + passwordEntry = null + // clear password; if decrypt changed to encrypt layout via edit button, no need + crypto_password_show.text = "" + crypto_extra_show.text = "" + crypto_extra_show_layout.visibility = View.INVISIBLE + crypto_container_decrypt.visibility = View.INVISIBLE + setResult(Activity.RESULT_CANCELED) + finish() + } + } + + override fun onProgressUpdate(vararg values: Int?) { + this.pb.progress = values[0] ?: 0 + } + } + + companion object { + val OPEN_PGP_BOUND = 101 + val REQUEST_EDIT = 201 + val REQUEST_DECRYPT = 202 + + val TAG = "PgpActivity" + + private var delayTask: DelayShow? = null + } +} + diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpHandler.java b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpHandler.java index c4013dc6..4975a6b8 100644 --- a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpHandler.java +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpHandler.java @@ -201,7 +201,7 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne || findViewById(R.id.crypto_container).getVisibility() != View.VISIBLE) return; - CharSequence category = ((TextView) findViewById(R.id.crypto_password_category)).getText(); + CharSequence category = ((TextView) findViewById(R.id.crypto_password_category_decrypt)).getText(); CharSequence file = ((TextView) findViewById(R.id.crypto_password_file)).getText(); CharSequence password = ((TextView) findViewById(R.id.crypto_password_show)).getText(); CharSequence extra = ((TextView) findViewById(R.id.crypto_extra_show)).getText(); @@ -211,7 +211,7 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne ((EditText) findViewById(R.id.crypto_password_edit)).setTypeface(monoTypeface); ((EditText) findViewById(R.id.crypto_extra_edit)).setTypeface(monoTypeface); - ((TextView) findViewById(R.id.crypto_password_category)).setText(category); + ((TextView) findViewById(R.id.crypto_password_category_decrypt)).setText(category); ((EditText) findViewById(R.id.crypto_password_file_edit)).setText(file); ((EditText) findViewById(R.id.crypto_password_edit)).setText(password); ((EditText) findViewById(R.id.crypto_extra_edit)).setText(extra); @@ -748,7 +748,7 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne .replace(PasswordRepository.getRepositoryDirectory(getApplicationContext()).getAbsolutePath(), ""); String cat = new File(path).getParentFile().getName(); - ((TextView) findViewById(R.id.crypto_password_category)).setText(cat + "/"); + ((TextView) findViewById(R.id.crypto_password_category_decrypt)).setText(cat + "/"); decryptAndVerify(new Intent()); break; } @@ -760,7 +760,7 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne String cat = extra.getString("FILE_PATH"); cat = cat.replace(PasswordRepository.getRepositoryDirectory(getApplicationContext()).getAbsolutePath(), ""); cat = cat + "/"; - ((TextView) findViewById(R.id.crypto_password_category)).setText(cat); + ((TextView) findViewById(R.id.crypto_password_category_decrypt)).setText(cat); break; } case "GET_KEY_ID": @@ -778,7 +778,7 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne String cat = new File(extra.getString("FILE_PATH").replace(PasswordRepository.getRepositoryDirectory(getApplicationContext()).getAbsolutePath(), "")) .getParentFile().getName(); - ((TextView) findViewById(R.id.crypto_password_category)).setText(cat + "/"); + ((TextView) findViewById(R.id.crypto_password_category_decrypt)).setText(cat + "/"); edit(new Intent()); break; } -- cgit v1.2.3