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 000000000..e48f1ab6d
--- /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 e8ddc04c1..3df296fe9 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 500644d1f..000000000
--- 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 98e2bfa40..1bb0b4878 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 b4029ba47..1a9d7fabf 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 3e9b2c4c0..94a81b8fd 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 168f92e1d..b913fa91d 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/Totp.java b/app/src/main/java/com/zeapo/pwdstore/utils/Otp.java
similarity index 85%
rename from app/src/main/java/com/zeapo/pwdstore/utils/Totp.java
rename to app/src/main/java/com/zeapo/pwdstore/utils/Otp.java
index 5e4326e2c..44e2aa64c 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 ac274e4e5..d08e0dbf9 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" />
+ android:layout_columnSpan="3"
+ android:layout_row="2"
+ android:text="@string/show_password" />
@@ -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" />
+ android:textStyle="bold"
+ android:visibility="invisible" />
+ android:typeface="monospace"
+ android:visibility="invisible" />
+ android:contentDescription="@string/copy_otp"
+ android:src="@drawable/ic_content_copy"
+ android:visibility="invisible" />
@@ -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 @@
-
\ No newline at end of file
+
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 000000000..d2cb597ea
--- /dev/null
+++ b/app/src/main/res/layout/otp_confirm_layout.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ 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 4a41cbba1..5b863e6dc 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -65,11 +65,11 @@
كلمة السر :
بيانات إضافية :
إسم المستخدم :
- TOTP :
+ OTP :
تعديل كلمة السر
نسخ كلمة السر
نسخ إسم المستخدم
- نسخ رمز الـ OTP
+ نسخ رمز الـ OTP
شارك كنص مجرد
آخِر تعديل %s
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2da5982ae..247275a14 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -23,14 +23,15 @@
[ANDROID PwdStore] Add
- [ANDROID PwdStore] Edit
+ "[ANDROID PwdStore] Edit "
+ "[ANDROID PwdStore] Increment HOTP counter for "
from store.
No OpenPGP Provider selected!
Password copied to clipboard, you have %d seconds to paste it somewhere.
Username copied to clipboard
- TOTP code copied to clipboard
+ OTP code copied to clipboard
Please provide a file name
You cannot use an empty password or empty extra content
@@ -93,13 +94,18 @@
Password:
Extra content:
Username:
- TOTP:
+ OTP:
Edit password
Copy password
Copy username
- Copy OTP code
+ Copy OTP code
Share as plaintext
Last changed %s
+ Attention
+ Update entry
+ Leave unchanged
+ 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.
+ Remember my choice
Git
@@ -217,6 +223,7 @@
Remote rejected non-fast-forward push. Check receive.denyNonFastForwards variable in config file of destination repository.
Error occurred during the push operation:
Clear ssh-key saved passphrase
+ Clear saved preference for HOTP incrementing
Remember the passphrase in the app configuration (insecure)
Hackish tools
Abort rebase
@@ -224,4 +231,5 @@
p@ssw0rd!
username: something other extra content
Failed to get last changed date
+ Tap copy to calculate HOTP
diff --git a/app/src/main/res/xml/preference.xml b/app/src/main/res/xml/preference.xml
index e232c6e19..3e8f34a9f 100644
--- a/app/src/main/res/xml/preference.xml
+++ b/app/src/main/res/xml/preference.xml
@@ -16,6 +16,9 @@
+