diff options
author | Joel Beckmeyer <joel@thebeckmeyers.xyz> | 2018-09-25 13:45:54 -0400 |
---|---|---|
committer | حسين <zidhussein@gmail.com> | 2018-09-25 18:45:54 +0100 |
commit | eea0e68dda1eb7248c6d458f52baeedb318b466a (patch) | |
tree | 73ff2b2f121b8db3097671f0e0639906edc2abea | |
parent | ac889abdd3d71ffb7f064a384c375ec22e7734c4 (diff) |
Display HOTP code if password contains HOTP secret, unify HOTP and TOTP code (#413)
* Display HOTP code if password contains HOTP secret, unify HOTP and TOTP code
* Add ability to show HOTP instead of showing every decrypt
* Fix off by 1 error
* fix return intent logic so that edits and HOTP increments are properly committed
* fix linting errors
* Fix broken logic for case when a password is created
* add ability to choose if password entry will be updated on HOTP code calculation
-rw-r--r-- | app/src/androidTest/java/com/zeapo/pwdstore/OtpTest.java | 12 | ||||
-rw-r--r-- | app/src/androidTest/java/com/zeapo/pwdstore/PasswordEntryTest.java | 21 | ||||
-rw-r--r-- | app/src/androidTest/java/com/zeapo/pwdstore/TotpTest.java | 12 | ||||
-rw-r--r-- | app/src/main/AndroidManifest.xml | 3 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/PasswordEntry.java | 69 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/PasswordStore.java | 12 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/UserPreference.kt | 7 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt | 173 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/utils/Otp.java (renamed from app/src/main/java/com/zeapo/pwdstore/utils/Totp.java) | 11 | ||||
-rw-r--r-- | app/src/main/res/layout/decrypt_layout.xml | 91 | ||||
-rw-r--r-- | app/src/main/res/layout/otp_confirm_layout.xml | 21 | ||||
-rw-r--r-- | app/src/main/res/values-ar/strings.xml | 4 | ||||
-rw-r--r-- | app/src/main/res/values/strings.xml | 16 | ||||
-rw-r--r-- | app/src/main/res/xml/preference.xml | 3 |
14 files changed, 340 insertions, 115 deletions
diff --git a/app/src/androidTest/java/com/zeapo/pwdstore/OtpTest.java b/app/src/androidTest/java/com/zeapo/pwdstore/OtpTest.java new file mode 100644 index 00000000..e48f1ab6 --- /dev/null +++ b/app/src/androidTest/java/com/zeapo/pwdstore/OtpTest.java @@ -0,0 +1,12 @@ +package com.zeapo.pwdstore; + +import com.zeapo.pwdstore.utils.Otp; + +import junit.framework.TestCase; + +public class OtpTest extends TestCase { + public void testOtp() { + String code = Otp.calculateCode("JBSWY3DPEHPK3PXP", 0L); + assertEquals("282760", code); + } +} diff --git a/app/src/androidTest/java/com/zeapo/pwdstore/PasswordEntryTest.java b/app/src/androidTest/java/com/zeapo/pwdstore/PasswordEntryTest.java index e8ddc04c..3df296fe 100644 --- a/app/src/androidTest/java/com/zeapo/pwdstore/PasswordEntryTest.java +++ b/app/src/androidTest/java/com/zeapo/pwdstore/PasswordEntryTest.java @@ -59,4 +59,25 @@ public class PasswordEntryTest extends TestCase { assertTrue(entry.hasTotp()); assertEquals("JBSWY3DPEHPK3PXP", entry.getTotpSecret()); } + + public void testNoHotpUriPresent() { + PasswordEntry entry = new PasswordEntry("secret\nextra\nlogin: username\ncontent"); + assertFalse(entry.hasHotp()); + assertNull(entry.getHotpSecret()); + assertNull(entry.getHotpCounter()); + } + + public void testHotpUriInPassword() { + PasswordEntry entry = new PasswordEntry("otpauth://hotp/test?secret=JBSWY3DPEHPK3PXP&counter=25"); + assertTrue(entry.hasHotp()); + assertEquals("JBSWY3DPEHPK3PXP", entry.getHotpSecret()); + assertEquals(new Long(25 ), entry.getHotpCounter()); + } + + public void testHotpUriInContent() { + PasswordEntry entry = new PasswordEntry("secret\nusername: test\notpauth://hotp/test?secret=JBSWY3DPEHPK3PXP&counter=25"); + assertTrue(entry.hasHotp()); + assertEquals("JBSWY3DPEHPK3PXP", entry.getHotpSecret()); + assertEquals(new Long(25), entry.getHotpCounter()); + } }
\ No newline at end of file diff --git a/app/src/androidTest/java/com/zeapo/pwdstore/TotpTest.java b/app/src/androidTest/java/com/zeapo/pwdstore/TotpTest.java deleted file mode 100644 index 500644d1..00000000 --- a/app/src/androidTest/java/com/zeapo/pwdstore/TotpTest.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.zeapo.pwdstore; - -import com.zeapo.pwdstore.utils.Totp; - -import junit.framework.TestCase; - -public class TotpTest extends TestCase { - public void testTotp() { - String code = Totp.calculateCode("JBSWY3DPEHPK3PXP", 0L); - assertEquals("282760", code); - } -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 98e2bfa4..1bb0b487 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,7 +6,8 @@ <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> - <uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" /> + <uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" + tools:ignore="ProtectedPermissions"/> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <application android:allowBackup="true" diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordEntry.java b/app/src/main/java/com/zeapo/pwdstore/PasswordEntry.java index ba689fe9..590a779e 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordEntry.java +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordEntry.java @@ -12,10 +12,14 @@ public class PasswordEntry { private static final String[] USERNAME_FIELDS = new String[]{"login", "username"}; - private final String extraContent; + private String extraContent; private final String password; private final String username; private final String totpSecret; + private final String hotpSecret; + private final Long hotpCounter; + private final String content; + private boolean isIncremented = false; public PasswordEntry(final ByteArrayOutputStream os) throws UnsupportedEncodingException { this(os.toString("UTF-8")); @@ -23,11 +27,14 @@ public class PasswordEntry { public PasswordEntry(final String decryptedContent) { final String[] passContent = decryptedContent.split("\n", 2); + content = decryptedContent; password = passContent[0]; - extraContent = passContent.length > 1 ? passContent[1] : ""; + totpSecret = findTotpSecret(content); + hotpSecret = findHotpSecret(content); + hotpCounter = findHotpCounter(content); + extraContent = findExtraContent(passContent); username = findUsername(); - totpSecret = findTotpSecret(decryptedContent); - } + } public String getPassword() { return password; @@ -45,6 +52,14 @@ public class PasswordEntry { return totpSecret; } + public Long getHotpCounter() { + return hotpCounter; + } + + public String getHotpSecret() { + return hotpSecret; + } + public boolean hasExtraContent() { return extraContent.length() != 0; } @@ -53,7 +68,24 @@ public class PasswordEntry { return username != null; } - public boolean hasTotp() { return totpSecret != null; } + public boolean hasTotp() { + return totpSecret != null; + } + + public boolean hasHotp() { + return hotpSecret != null && hotpCounter != null; + } + + public boolean hotpIsIncremented() { return isIncremented; } + + public void incrementHotp() { + for (String line : content.split("\n")) { + if (line.startsWith("otpauth://hotp/")) { + extraContent = extraContent.replaceFirst("counter=[0-9]+", "counter=" + Long.toString(hotpCounter + 1)); + isIncremented = true; + } + } + } private String findUsername() { final String[] extraLines = extraContent.split("\n"); @@ -75,4 +107,31 @@ public class PasswordEntry { } return null; } + + private String findHotpSecret(String decryptedContent) { + for (String line : decryptedContent.split("\n")) { + if (line.startsWith("otpauth://hotp/")) { + return Uri.parse(line).getQueryParameter("secret"); + } + } + return null; + } + + private Long findHotpCounter(String decryptedContent) { + for (String line : decryptedContent.split("\n")) { + if (line.startsWith("otpauth://hotp/")) { + return Long.parseLong(Uri.parse(line).getQueryParameter("counter")); + } + } + return null; + } + + private String findExtraContent(String [] passContent) { + String extraContent = passContent.length > 1 ? passContent[1] : ""; + // if there is a HOTP URI, we must return the extra content with the counter incremented + if (hasHotp()) { + return extraContent.replaceFirst("counter=[0-9]+", "counter=" + Long.toString(hotpCounter)); + } + return extraContent; + } } diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java index b4029ba4..1a9d7fab 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.Menu; import android.view.MenuItem; import android.view.View; import android.widget.TextView; +import android.widget.Toast; import com.zeapo.pwdstore.crypto.PgpActivity; import com.zeapo.pwdstore.git.GitActivity; @@ -606,6 +607,7 @@ public class PasswordStore extends AppCompatActivity { protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK) { switch (requestCode) { case GitActivity.REQUEST_CLONE: @@ -613,11 +615,15 @@ public class PasswordStore extends AppCompatActivity { settings.edit().putBoolean("repository_initialized", true).apply(); break; case REQUEST_CODE_DECRYPT_AND_VERIFY: - // if went from decrypt->edit and user saved changes, we need to commitChange + // if went from decrypt->edit and user saved changes or HOTP counter was incremented, we need to commitChange if (data != null && data.getBooleanExtra("needCommit", false)) { - commitChange(this.getResources().getString(R.string.edit_commit_text) + data.getExtras().getString("NAME")); - refreshListAdapter(); + if (data.getStringExtra("OPERATION").equals("EDIT")) { + commitChange(this.getResources().getString(R.string.edit_commit_text) + data.getExtras().getString("NAME")); + } else { + commitChange(this.getResources().getString(R.string.increment_commit_text) + data.getExtras().getString("NAME")); + } } + refreshListAdapter(); break; case REQUEST_CODE_ENCRYPT: commitChange(this.getResources().getString(R.string.add_commit_text) + data.getExtras().getString("NAME") + this.getResources().getString(R.string.from_store)); diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt index 3e9b2c4c..94a81b8f 100644 --- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt +++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt @@ -80,6 +80,12 @@ class UserPreference : AppCompatActivity() { true } + findPreference("hotp_remember_clear_choice").onPreferenceClickListener = Preference.OnPreferenceClickListener { + sharedPreferences.edit().putBoolean("hotp_remember_check", false).apply() + it.isEnabled = false + true + } + findPreference("git_server_info").onPreferenceClickListener = Preference.OnPreferenceClickListener { val intent = Intent(callingActivity, GitActivity::class.java) intent.putExtra("Operation", GitActivity.EDIT_SERVER) @@ -161,6 +167,7 @@ class UserPreference : AppCompatActivity() { findPreference("ssh_see_key").isEnabled = sharedPreferences.getBoolean("use_generated_key", false) findPreference("git_delete_repo").isEnabled = !sharedPreferences.getBoolean("git_external", false) findPreference("ssh_key_clear_passphrase").isEnabled = sharedPreferences.getString("ssh_key_passphrase", null)?.isNotEmpty() ?: false + findPreference("hotp_remember_clear_choice").isEnabled = sharedPreferences.getBoolean("hotp_remember_check", false) val keyPref = findPreference("openpgp_key_id_pref") val selectedKeys: Array<String> = ArrayList<String>(sharedPreferences.getStringSet("openpgp_key_ids_set", HashSet<String>())).toTypedArray() if (selectedKeys.isEmpty()) { 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 168f92e1..b913fa91 100644 --- a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt @@ -2,6 +2,7 @@ package com.zeapo.pwdstore.crypto import android.annotation.SuppressLint import android.app.Activity +import android.app.AlertDialog import android.app.PendingIntent import android.content.* import android.graphics.Typeface @@ -17,12 +18,8 @@ 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 com.zeapo.pwdstore.utils.PasswordRepository -import com.zeapo.pwdstore.utils.Totp +import com.zeapo.pwdstore.* +import com.zeapo.pwdstore.utils.Otp import kotlinx.android.synthetic.main.decrypt_layout.* import kotlinx.android.synthetic.main.encrypt_layout.* import org.apache.commons.io.FileUtils @@ -44,6 +41,10 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { private var passwordEntry: PasswordEntry? = null private var api: OpenPgpApi? = null + private var editName: String? = null + private var editPass: String? = null + private var editExtra: String? = null + private val operation: String by lazy { intent.getStringExtra("OPERATION") } private val repoPath: String by lazy { intent.getStringExtra("REPO_PATH") } @@ -102,6 +103,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { } override fun onDestroy() { + checkAndIncrementHotp() super.onDestroy() mServiceConnection?.unbindFromService() } @@ -122,14 +124,21 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { override fun onOptionsItemSelected(item: MenuItem?): Boolean { when (item?.itemId) { android.R.id.home -> { - setResult(RESULT_CANCELED) + if(passwordEntry?.hotpIsIncremented() == false) { + 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) + R.id.crypto_cancel_add -> { + if(passwordEntry?.hotpIsIncremented() == false) { + setResult(RESULT_CANCELED) + } + finish() + } else -> return super.onOptionsItemSelected(item) } return true @@ -190,7 +199,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { val iStream = FileUtils.openInputStream(File(fullPath)) val oStream = ByteArrayOutputStream() - 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 { @@ -253,27 +262,75 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { } } - if (entry.hasTotp()) { + if (entry.hasTotp() || entry.hasHotp()) { crypto_extra_show_layout.visibility = View.VISIBLE crypto_extra_show.typeface = monoTypeface crypto_extra_show.text = entry.extraContent - crypto_totp_show.visibility = View.VISIBLE - crypto_totp_show_label.visibility = View.VISIBLE - crypto_copy_totp.visibility = View.VISIBLE + crypto_otp_show.visibility = View.VISIBLE + crypto_otp_show_label.visibility = View.VISIBLE + crypto_copy_otp.visibility = View.VISIBLE + + if (entry.hasTotp()) { + crypto_copy_otp.setOnClickListener { copyOtpToClipBoard(Otp.calculateCode(entry.totpSecret, Date().time / (1000 * Otp.TIME_WINDOW))) } + crypto_otp_show.text = Otp.calculateCode(entry.totpSecret, Date().time / (1000 * Otp.TIME_WINDOW)) + } else { + // we only want to calculate and show HOTP if the user requests it + crypto_copy_otp.setOnClickListener { + if (settings.getBoolean("hotp_remember_check", false)) { + if (settings.getBoolean("hotp_remember_choice", false)) { + calculateAndCommitHotp(entry) + } else { + calculateHotp(entry) + } + } else { + // show a dialog asking permission to update the HOTP counter in the entry + val checkInflater = LayoutInflater.from(this) + val checkLayout = checkInflater.inflate(R.layout.otp_confirm_layout, null) + val rememberCheck : CheckBox = checkLayout.findViewById(R.id.hotp_remember_checkbox) + val dialogBuilder = AlertDialog.Builder(this) + dialogBuilder.setView(checkLayout) + dialogBuilder.setMessage(R.string.dialog_update_body) + .setCancelable(false) + .setPositiveButton(R.string.dialog_update_positive, DialogInterface.OnClickListener { dialog, id -> + run { + calculateAndCommitHotp(entry) + if (rememberCheck.isChecked()) { + val editor = settings.edit() + editor.putBoolean("hotp_remember_check", true) + editor.putBoolean("hotp_remember_choice", true) + editor.commit() + } + } + }) + .setNegativeButton(R.string.dialog_update_negative, DialogInterface.OnClickListener { dialog, id -> + run { + calculateHotp(entry) + val editor = settings.edit() + editor.putBoolean("hotp_remember_check", true) + editor.putBoolean("hotp_remember_choice", false) + editor.commit() + } + }) + val updateDialog = dialogBuilder.create() + updateDialog.setTitle(R.string.dialog_update_title) + updateDialog.show() + } + } + crypto_otp_show.setText(R.string.hotp_pending) + } + crypto_otp_show.typeface = monoTypeface - crypto_copy_totp.setOnClickListener { copyTotpToClipBoard(Totp.calculateCode(entry.totpSecret, Date().time / 1000)) } - crypto_totp_show.typeface = monoTypeface - crypto_totp_show.text = Totp.calculateCode(entry.totpSecret, Date().time / 1000); } else { - crypto_totp_show.visibility = View.GONE - crypto_totp_show_label.visibility = View.GONE - crypto_copy_totp.visibility = View.GONE + crypto_otp_show.visibility = View.GONE + crypto_otp_show_label.visibility = View.GONE + crypto_copy_otp.visibility = View.GONE } if (settings.getBoolean("copy_on_decrypt", true)) { copyPasswordToClipBoard() } + } catch (e: Exception) { Log.e(TAG, "An Exception occurred", e) } @@ -282,23 +339,26 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { RESULT_CODE_ERROR -> handleError(result) } - }) + } } /** * Encrypts the password and the extra content */ private fun encrypt() { - val name = crypto_password_file_edit.text.toString().trim() - val pass = crypto_password_edit.text.toString() - val extra = crypto_extra_edit.text.toString() + // if HOTP was incremented, we leave fields as is; they have already been set + if(intent.getStringExtra("OPERATION") != "INCREMENT") { + editName = crypto_password_file_edit.text.toString().trim() + editPass = crypto_password_edit.text.toString() + editExtra = crypto_extra_edit.text.toString() + } - if (name.isEmpty()) { + if (editName?.isEmpty() == true) { showToast(resources.getString(R.string.file_toast_text)) return } - if (pass.isEmpty() && extra.isEmpty()) { + if (editPass?.isEmpty() == true && editExtra?.isEmpty() == true) { showToast(resources.getString(R.string.empty_toast_text)) return } @@ -312,13 +372,12 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { 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 iStream = ByteArrayInputStream("$editPass\n$editExtra".toByteArray(Charset.forName("UTF-8"))) val oStream = ByteArrayOutputStream() - val path = if (intent.getStringExtra("OPERATION") == "EDIT") fullPath else "$fullPath/$name.gpg" + val path = if (intent.getBooleanExtra("fromDecrypt", false)) fullPath else "$fullPath/$editName.gpg" - api?.executeApiAsync(data, iStream, oStream, { result: Intent? -> - when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { + 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 @@ -328,15 +387,16 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { val returnIntent = Intent() returnIntent.putExtra("CREATED_FILE", path) - returnIntent.putExtra("NAME", name) + returnIntent.putExtra("NAME", editName) // if coming from decrypt screen->edit button if (intent.getBooleanExtra("fromDecrypt", false)) { - data.putExtra("needCommit", true) + returnIntent.putExtra("OPERATION", "EDIT") + returnIntent.putExtra("needCommit", true) } - setResult(RESULT_OK, returnIntent) finish() + } catch (e: Exception) { Log.e(TAG, "An Exception occurred", e) } @@ -379,6 +439,43 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { } /** + * Writes updated HOTP counter to edit fields and encrypts + */ + private fun checkAndIncrementHotp() { + // we do not want to increment the HOTP counter if the user has edited the entry or has not + // generated an HOTP code + if(intent.getStringExtra("OPERATION") != "EDIT" && passwordEntry?.hotpIsIncremented() == true) { + editName = name.trim() + editPass = passwordEntry?.password + editExtra = passwordEntry?.extraContent + + val data = Intent(this, PgpActivity::class.java) + data.putExtra("OPERATION", "INCREMENT") + data.putExtra("fromDecrypt", true) + intent = data + encrypt() + } + } + + private fun calculateHotp(entry : PasswordEntry) { + copyOtpToClipBoard(Otp.calculateCode(entry.hotpSecret, entry.hotpCounter + 1)) + crypto_otp_show.text = Otp.calculateCode(entry.hotpSecret, entry.hotpCounter + 1) + crypto_extra_show.text = entry.extraContent + } + + private fun calculateAndCommitHotp(entry : PasswordEntry) { + calculateHotp(entry) + entry.incrementHotp() + // we must set the result before encrypt() is called, since in + // some cases it is called during the finish() sequence + val returnIntent = Intent() + returnIntent.putExtra("NAME", name.trim()) + returnIntent.putExtra("OPERATION", "INCREMENT") + returnIntent.putExtra("needCommit", true) + setResult(RESULT_OK, returnIntent) + } + + /** * Get the Key ids from OpenKeychain */ private fun getKeyIds(receivedIntent: Intent? = null) { @@ -500,13 +597,12 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { showToast(resources.getString(R.string.clipboard_username_toast_text)) } - private fun copyTotpToClipBoard(code: String) { + private fun copyOtpToClipBoard(code: String) { val clip = ClipData.newPlainText("pgp_handler_result_pm", code) clipboard.primaryClip = clip - showToast(resources.getString(R.string.clipboard_totp_toast_text)) + showToast(resources.getString(R.string.clipboard_otp_toast_text)) } - private fun shareAsPlaintext() { if (findViewById<View>(R.id.share_password_as_plaintext) == null) return @@ -586,6 +682,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { override fun onPostExecute(b: Boolean?) { if (skip) return + checkAndIncrementHotp() // only clear the clipboard if we automatically copied the password to it if (settings.getBoolean("copy_on_decrypt", true)) { @@ -602,13 +699,15 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { } if (crypto_password_show != null) { - passwordEntry = null // clear password; if decrypt changed to encrypt layout via edit button, no need + if(passwordEntry?.hotpIsIncremented() == false) { + setResult(Activity.RESULT_CANCELED) + } + passwordEntry = null 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() } } diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Totp.java b/app/src/main/java/com/zeapo/pwdstore/utils/Otp.java index 5e4326e2..44e2aa64 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/Totp.java +++ b/app/src/main/java/com/zeapo/pwdstore/utils/Otp.java @@ -13,18 +13,18 @@ import java.util.Arrays; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; -public class Totp { +public class Otp { + public static final int TIME_WINDOW = 30; private static final String ALGORITHM = "HmacSHA1"; - private static final int TIME_WINDOW = 30; private static final int CODE_DIGITS = 6; private static final Base32 BASE_32 = new Base32(); - private Totp() { + private Otp() { } - public static String calculateCode(String secret, long epochSeconds) { + public static String calculateCode(String secret, long counter) { SecretKeySpec signingKey = new SecretKeySpec(BASE_32.decode(secret), ALGORITHM); Mac mac = null; @@ -39,8 +39,7 @@ public class Totp { return null; } - long time = epochSeconds / TIME_WINDOW; - byte[] digest = mac.doFinal(ByteBuffer.allocate(8).putLong(time).array()); + byte[] digest = mac.doFinal(ByteBuffer.allocate(8).putLong(counter).array()); int offset = digest[digest.length - 1] & 0xf; byte[] code = Arrays.copyOfRange(digest, offset, offset + 4); code[0] = (byte) (0x7f & code[0]); diff --git a/app/src/main/res/layout/decrypt_layout.xml b/app/src/main/res/layout/decrypt_layout.xml index ac274e4e..d08e0dbf 100644 --- a/app/src/main/res/layout/decrypt_layout.xml +++ b/app/src/main/res/layout/decrypt_layout.xml @@ -3,15 +3,15 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context="com.zeapo.pwdstore.crypto.PgpActivity" + android:background="@color/background" android:orientation="vertical" - android:background="@color/background"> + tools:context="com.zeapo.pwdstore.crypto.PgpActivity"> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" - android:padding="16dp" - android:orientation="vertical"> + android:orientation="vertical" + android:padding="16dp"> <LinearLayout android:layout_width="match_parent" @@ -61,17 +61,17 @@ <ImageView android:layout_width="match_parent" android:layout_height="wrap_content" - android:src="@drawable/divider" - android:layout_marginTop="16dp" android:layout_marginBottom="16dp" + android:layout_marginTop="16dp" + android:src="@drawable/divider" tools:ignore="ContentDescription" /> <LinearLayout android:id="@+id/crypto_container_decrypt" - android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/activity_vertical_margin" + android:orientation="vertical" android:visibility="invisible"> <GridLayout @@ -83,39 +83,40 @@ android:id="@+id/crypto_password_show_label" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textStyle="bold" - android:textColor="@android:color/black" - android:text="@string/password" + android:layout_column="0" android:layout_row="0" - android:layout_column="0"/> + android:text="@string/password" + android:textColor="@android:color/black" + android:textStyle="bold" /> + <TextView android:id="@+id/crypto_password_show" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:typeface="monospace" - android:textColor="@android:color/black" android:layout_column="2" - android:layout_row="0"/> + android:layout_row="0" + android:textColor="@android:color/black" + android:typeface="monospace" /> <ProgressBar android:id="@+id/pbLoading" + style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="8dp" - android:layout_marginTop="8dp" - android:layout_marginBottom="8dp" - style="?android:attr/progressBarStyleHorizontal" - android:layout_row="1" android:layout_column="0" - android:layout_columnSpan="3"/> + android:layout_columnSpan="3" + android:layout_marginBottom="8dp" + android:layout_marginTop="8dp" + android:layout_row="1" /> <Button android:id="@+id/crypto_password_toggle_show" android:layout_width="match_parent" - android:text="@string/show_password" android:layout_height="wrap_content" - android:layout_row="2" android:layout_column="0" - android:layout_columnSpan="3"/> + android:layout_columnSpan="3" + android:layout_row="2" + android:text="@string/show_password" /> </GridLayout> @@ -129,13 +130,13 @@ android:id="@+id/crypto_copy_username" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentRight="true" android:layout_alignParentEnd="true" + android:layout_alignParentRight="true" android:layout_alignParentTop="true" - android:contentDescription="@string/copy_username" - android:visibility="invisible" android:background="@color/background" - android:src="@drawable/ic_content_copy"/> + android:contentDescription="@string/copy_username" + android:src="@drawable/ic_content_copy" + android:visibility="invisible" /> <TextView android:id="@+id/crypto_username_show_label" @@ -146,10 +147,10 @@ android:layout_alignParentTop="true" android:layout_toLeftOf="@id/crypto_copy_username" android:layout_toStartOf="@id/crypto_copy_username" - android:visibility="invisible" android:text="@string/username" android:textColor="@android:color/black" - android:textStyle="bold" /> + android:textStyle="bold" + android:visibility="invisible" /> <TextView android:id="@+id/crypto_username_show" @@ -160,45 +161,45 @@ android:layout_below="@id/crypto_username_show_label" android:layout_toLeftOf="@id/crypto_copy_username" android:layout_toStartOf="@id/crypto_copy_username" - android:visibility="invisible" android:textColor="@android:color/black" android:textIsSelectable="true" - android:typeface="monospace" /> + android:typeface="monospace" + android:visibility="invisible" /> <ImageButton - android:id="@+id/crypto_copy_totp" + android:id="@+id/crypto_copy_otp" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentRight="true" android:layout_alignParentEnd="true" + android:layout_alignParentRight="true" android:layout_below="@id/crypto_username_show" - android:visibility="invisible" - android:contentDescription="@string/copy_totp" android:background="@color/background" - android:src="@drawable/ic_content_copy"/> + android:contentDescription="@string/copy_otp" + android:src="@drawable/ic_content_copy" + android:visibility="invisible" /> <TextView - android:id="@+id/crypto_totp_show_label" + android:id="@+id/crypto_otp_show_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" - android:layout_toLeftOf="@id/crypto_copy_totp" - android:layout_toStartOf="@id/crypto_copy_totp" android:layout_below="@id/crypto_username_show" - android:text="@string/totp" + android:layout_toLeftOf="@id/crypto_copy_otp" + android:layout_toStartOf="@id/crypto_copy_otp" + android:text="@string/otp" android:textColor="@android:color/black" android:textStyle="bold" /> <TextView - android:id="@+id/crypto_totp_show" + android:id="@+id/crypto_otp_show" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" - android:layout_below="@id/crypto_totp_show_label" - android:layout_toLeftOf="@id/crypto_copy_totp" - android:layout_toStartOf="@id/crypto_copy_totp" + android:layout_below="@id/crypto_otp_show_label" + android:layout_toLeftOf="@id/crypto_copy_otp" + android:layout_toStartOf="@id/crypto_copy_otp" android:textColor="@android:color/black" android:textIsSelectable="true" android:typeface="monospace" /> @@ -209,7 +210,7 @@ android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" - android:layout_below="@id/crypto_totp_show" + android:layout_below="@id/crypto_otp_show" android:text="@string/extra_content" android:textColor="@android:color/black" android:textStyle="bold" /> @@ -230,4 +231,4 @@ </LinearLayout> -</ScrollView>
\ No newline at end of file +</ScrollView> diff --git a/app/src/main/res/layout/otp_confirm_layout.xml b/app/src/main/res/layout/otp_confirm_layout.xml new file mode 100644 index 00000000..d2cb597e --- /dev/null +++ b/app/src/main/res/layout/otp_confirm_layout.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <CheckBox + android:id="@+id/hotp_remember_checkbox" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="16dp" + android:layout_marginRight="16dp" + android:layout_marginStart="16dp" + android:layout_marginTop="8dp" + android:text="@string/dialog_update_check" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + +</android.support.constraint.ConstraintLayout>
\ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 4a41cbba..5b863e6d 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -65,11 +65,11 @@ <string name="password">كلمة السر :</string> <string name="extra_content">بيانات إضافية :</string> <string name="username">إسم المستخدم :</string> - <string name="totp">TOTP :</string> + <string name="otp">OTP :</string> <string name="edit_password">تعديل كلمة السر</string> <string name="copy_password">نسخ كلمة السر</string> <string name="copy_username">نسخ إسم المستخدم</string> - <string name="copy_totp">نسخ رمز الـ OTP</string> + <string name="copy_otp">نسخ رمز الـ OTP</string> <string name="share_as_plaintext">شارك كنص مجرد</string> <string name="last_changed">آخِر تعديل %s</string> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2da5982a..247275a1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,14 +23,15 @@ <!-- git commits --> <string name="add_commit_text">[ANDROID PwdStore] Add  </string> - <string name="edit_commit_text">[ANDROID PwdStore] Edit  </string> + <string name="edit_commit_text">"[ANDROID PwdStore] Edit "</string> + <string name="increment_commit_text">"[ANDROID PwdStore] Increment HOTP counter for "</string> <string name="from_store">  from store.</string> <!-- PGPHandler --> <string name="provider_toast_text">No OpenPGP Provider selected!</string> <string name="clipboard_password_toast_text">Password copied to clipboard, you have %d seconds to paste it somewhere.</string> <string name="clipboard_username_toast_text">Username copied to clipboard</string> - <string name="clipboard_totp_toast_text">TOTP code copied to clipboard</string> + <string name="clipboard_otp_toast_text">OTP code copied to clipboard</string> <string name="file_toast_text">Please provide a file name</string> <string name="empty_toast_text">You cannot use an empty password or empty extra content</string> @@ -93,13 +94,18 @@ <string name="password">Password:</string> <string name="extra_content">Extra content:</string> <string name="username">Username:</string> - <string name="totp">TOTP:</string> + <string name="otp">OTP:</string> <string name="edit_password">Edit password</string> <string name="copy_password">Copy password</string> <string name="copy_username">Copy username</string> - <string name="copy_totp">Copy OTP code</string> + <string name="copy_otp">Copy OTP code</string> <string name="share_as_plaintext">Share as plaintext</string> <string name="last_changed">Last changed %s</string> + <string name="dialog_update_title">Attention</string> + <string name="dialog_update_positive">Update entry</string> + <string name="dialog_update_negative">Leave unchanged</string> + <string name="dialog_update_body">The HOTP counter is about to be incremented. This change will be committed. If you press "Leave unchanged", the HOTP code will be shown, but the counter will not be changed.</string> + <string name="dialog_update_check">Remember my choice</string> <!-- Preferences --> <string name="pref_git_title">Git</string> @@ -217,6 +223,7 @@ <string name="git_push_other_error">Remote rejected non-fast-forward push. Check receive.denyNonFastForwards variable in config file of destination repository.</string> <string name="jgit_error_push_dialog_text">Error occurred during the push operation:</string> <string name="ssh_key_clear_passphrase">Clear ssh-key saved passphrase</string> + <string name="hotp_remember_clear_choice">Clear saved preference for HOTP incrementing</string> <string name="remember_the_passphrase">Remember the passphrase in the app configuration (insecure)</string> <string name="hackish_tools">Hackish tools</string> <string name="abort_rebase">Abort rebase</string> @@ -224,4 +231,5 @@ <string name="crypto_password_edit_hint">p@ssw0rd!</string> <string name="crypto_extra_edit_hint">username: something other extra content</string> <string name="get_last_changed_failed">Failed to get last changed date</string> + <string name="hotp_pending">Tap copy to calculate HOTP</string> </resources> diff --git a/app/src/main/res/xml/preference.xml b/app/src/main/res/xml/preference.xml index e232c6e1..3e8f34a9 100644 --- a/app/src/main/res/xml/preference.xml +++ b/app/src/main/res/xml/preference.xml @@ -17,6 +17,9 @@ android:key="ssh_key_clear_passphrase" android:title="@string/ssh_key_clear_passphrase" /> <Preference + android:key="hotp_remember_clear_choice" + android:title="@string/hotp_remember_clear_choice" /> + <Preference android:key="ssh_see_key" android:title="@string/pref_ssh_see_key_title" /> <Preference |