From eea0e68dda1eb7248c6d458f52baeedb318b466a Mon Sep 17 00:00:00 2001 From: Joel Beckmeyer Date: Tue, 25 Sep 2018 13:45:54 -0400 Subject: 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 --- .../java/com/zeapo/pwdstore/OtpTest.java | 12 ++ .../java/com/zeapo/pwdstore/PasswordEntryTest.java | 21 +++ .../java/com/zeapo/pwdstore/TotpTest.java | 12 -- app/src/main/AndroidManifest.xml | 3 +- .../java/com/zeapo/pwdstore/PasswordEntry.java | 69 +++++++- .../java/com/zeapo/pwdstore/PasswordStore.java | 12 +- .../main/java/com/zeapo/pwdstore/UserPreference.kt | 7 + .../java/com/zeapo/pwdstore/crypto/PgpActivity.kt | 173 ++++++++++++++++----- .../main/java/com/zeapo/pwdstore/utils/Otp.java | 49 ++++++ .../main/java/com/zeapo/pwdstore/utils/Totp.java | 50 ------ app/src/main/res/layout/decrypt_layout.xml | 91 +++++------ app/src/main/res/layout/otp_confirm_layout.xml | 21 +++ app/src/main/res/values-ar/strings.xml | 4 +- app/src/main/res/values/strings.xml | 16 +- app/src/main/res/xml/preference.xml | 3 + 15 files changed, 384 insertions(+), 159 deletions(-) create mode 100644 app/src/androidTest/java/com/zeapo/pwdstore/OtpTest.java delete mode 100644 app/src/androidTest/java/com/zeapo/pwdstore/TotpTest.java create mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/Otp.java delete mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/Totp.java create mode 100644 app/src/main/res/layout/otp_confirm_layout.xml 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 @@ - + 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 = ArrayList(sharedPreferences.getStringSet("openpgp_key_ids_set", HashSet())).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) } @@ -378,6 +438,43 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { invalidateOptionsMenu() } + /** + * 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 */ @@ -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(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/Otp.java b/app/src/main/java/com/zeapo/pwdstore/utils/Otp.java new file mode 100644 index 00000000..44e2aa64 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/Otp.java @@ -0,0 +1,49 @@ +package com.zeapo.pwdstore.utils; + +import android.util.Log; + +import org.apache.commons.codec.binary.Base32; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +public class Otp { + public static final int TIME_WINDOW = 30; + + private static final String ALGORITHM = "HmacSHA1"; + private static final int CODE_DIGITS = 6; + + private static final Base32 BASE_32 = new Base32(); + + private Otp() { + } + + public static String calculateCode(String secret, long counter) { + SecretKeySpec signingKey = new SecretKeySpec(BASE_32.decode(secret), ALGORITHM); + + Mac mac = null; + try { + mac = Mac.getInstance(ALGORITHM); + mac.init(signingKey); + } catch (NoSuchAlgorithmException e) { + Log.e("TOTP", ALGORITHM + " unavailable - should never happen", e); + return null; + } catch (InvalidKeyException e) { + Log.e("TOTP", "Key is malformed", e); + return null; + } + + 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]); + String strCode = new BigInteger(code).toString(); + return strCode.substring(strCode.length() - CODE_DIGITS); + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Totp.java b/app/src/main/java/com/zeapo/pwdstore/utils/Totp.java deleted file mode 100644 index 5e4326e2..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/utils/Totp.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.zeapo.pwdstore.utils; - -import android.util.Log; - -import org.apache.commons.codec.binary.Base32; - -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; - -public class Totp { - - 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() { - } - - public static String calculateCode(String secret, long epochSeconds) { - SecretKeySpec signingKey = new SecretKeySpec(BASE_32.decode(secret), ALGORITHM); - - Mac mac = null; - try { - mac = Mac.getInstance(ALGORITHM); - mac.init(signingKey); - } catch (NoSuchAlgorithmException e) { - Log.e("TOTP", ALGORITHM + " unavailable - should never happen", e); - return null; - } catch (InvalidKeyException e) { - Log.e("TOTP", "Key is malformed", e); - return null; - } - - long time = epochSeconds / TIME_WINDOW; - byte[] digest = mac.doFinal(ByteBuffer.allocate(8).putLong(time).array()); - int offset = digest[digest.length - 1] & 0xf; - byte[] code = Arrays.copyOfRange(digest, offset, offset + 4); - code[0] = (byte) (0x7f & code[0]); - String strCode = new BigInteger(code).toString(); - return strCode.substring(strCode.length() - CODE_DIGITS); - } -} 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"> + android:orientation="vertical" + android:padding="16dp"> + android:text="@string/password" + android:textColor="@android:color/black" + android:textStyle="bold" /> + + android:layout_row="0" + android:textColor="@android:color/black" + android:typeface="monospace" /> + android:layout_columnSpan="3" + android:layout_marginBottom="8dp" + android:layout_marginTop="8dp" + android:layout_row="1" />