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
This commit is contained in:
Joel Beckmeyer 2018-09-25 13:45:54 -04:00 committed by حسين
parent ac889abdd3
commit eea0e68dda
14 changed files with 340 additions and 115 deletions

View file

@ -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);
}
}

View file

@ -59,4 +59,25 @@ public class PasswordEntryTest extends TestCase {
assertTrue(entry.hasTotp()); assertTrue(entry.hasTotp());
assertEquals("JBSWY3DPEHPK3PXP", entry.getTotpSecret()); 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());
}
} }

View file

@ -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);
}
}

View file

@ -6,7 +6,8 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_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" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application android:allowBackup="true" <application android:allowBackup="true"

View file

@ -12,10 +12,14 @@ public class PasswordEntry {
private static final String[] USERNAME_FIELDS = new String[]{"login", "username"}; private static final String[] USERNAME_FIELDS = new String[]{"login", "username"};
private final String extraContent; private String extraContent;
private final String password; private final String password;
private final String username; private final String username;
private final String totpSecret; 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 { public PasswordEntry(final ByteArrayOutputStream os) throws UnsupportedEncodingException {
this(os.toString("UTF-8")); this(os.toString("UTF-8"));
@ -23,10 +27,13 @@ public class PasswordEntry {
public PasswordEntry(final String decryptedContent) { public PasswordEntry(final String decryptedContent) {
final String[] passContent = decryptedContent.split("\n", 2); final String[] passContent = decryptedContent.split("\n", 2);
content = decryptedContent;
password = passContent[0]; password = passContent[0];
extraContent = passContent.length > 1 ? passContent[1] : ""; totpSecret = findTotpSecret(content);
hotpSecret = findHotpSecret(content);
hotpCounter = findHotpCounter(content);
extraContent = findExtraContent(passContent);
username = findUsername(); username = findUsername();
totpSecret = findTotpSecret(decryptedContent);
} }
public String getPassword() { public String getPassword() {
@ -45,6 +52,14 @@ public class PasswordEntry {
return totpSecret; return totpSecret;
} }
public Long getHotpCounter() {
return hotpCounter;
}
public String getHotpSecret() {
return hotpSecret;
}
public boolean hasExtraContent() { public boolean hasExtraContent() {
return extraContent.length() != 0; return extraContent.length() != 0;
} }
@ -53,7 +68,24 @@ public class PasswordEntry {
return username != null; 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() { private String findUsername() {
final String[] extraLines = extraContent.split("\n"); final String[] extraLines = extraContent.split("\n");
@ -75,4 +107,31 @@ public class PasswordEntry {
} }
return null; 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;
}
} }

View file

@ -30,6 +30,7 @@ import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import com.zeapo.pwdstore.crypto.PgpActivity; import com.zeapo.pwdstore.crypto.PgpActivity;
import com.zeapo.pwdstore.git.GitActivity; import com.zeapo.pwdstore.git.GitActivity;
@ -606,6 +607,7 @@ public class PasswordStore extends AppCompatActivity {
protected void onActivityResult(int requestCode, int resultCode, protected void onActivityResult(int requestCode, int resultCode,
Intent data) { Intent data) {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
switch (requestCode) { switch (requestCode) {
case GitActivity.REQUEST_CLONE: case GitActivity.REQUEST_CLONE:
@ -613,11 +615,15 @@ public class PasswordStore extends AppCompatActivity {
settings.edit().putBoolean("repository_initialized", true).apply(); settings.edit().putBoolean("repository_initialized", true).apply();
break; break;
case REQUEST_CODE_DECRYPT_AND_VERIFY: 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)) { if (data != null && data.getBooleanExtra("needCommit", false)) {
if (data.getStringExtra("OPERATION").equals("EDIT")) {
commitChange(this.getResources().getString(R.string.edit_commit_text) + data.getExtras().getString("NAME")); commitChange(this.getResources().getString(R.string.edit_commit_text) + data.getExtras().getString("NAME"));
refreshListAdapter(); } else {
commitChange(this.getResources().getString(R.string.increment_commit_text) + data.getExtras().getString("NAME"));
} }
}
refreshListAdapter();
break; break;
case REQUEST_CODE_ENCRYPT: case REQUEST_CODE_ENCRYPT:
commitChange(this.getResources().getString(R.string.add_commit_text) + data.getExtras().getString("NAME") + this.getResources().getString(R.string.from_store)); commitChange(this.getResources().getString(R.string.add_commit_text) + data.getExtras().getString("NAME") + this.getResources().getString(R.string.from_store));

View file

@ -80,6 +80,12 @@ class UserPreference : AppCompatActivity() {
true 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 { findPreference("git_server_info").onPreferenceClickListener = Preference.OnPreferenceClickListener {
val intent = Intent(callingActivity, GitActivity::class.java) val intent = Intent(callingActivity, GitActivity::class.java)
intent.putExtra("Operation", GitActivity.EDIT_SERVER) 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("ssh_see_key").isEnabled = sharedPreferences.getBoolean("use_generated_key", false)
findPreference("git_delete_repo").isEnabled = !sharedPreferences.getBoolean("git_external", 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("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 keyPref = findPreference("openpgp_key_id_pref")
val selectedKeys: Array<String> = ArrayList<String>(sharedPreferences.getStringSet("openpgp_key_ids_set", HashSet<String>())).toTypedArray() val selectedKeys: Array<String> = ArrayList<String>(sharedPreferences.getStringSet("openpgp_key_ids_set", HashSet<String>())).toTypedArray()
if (selectedKeys.isEmpty()) { if (selectedKeys.isEmpty()) {

View file

@ -2,6 +2,7 @@ package com.zeapo.pwdstore.crypto
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.AlertDialog
import android.app.PendingIntent import android.app.PendingIntent
import android.content.* import android.content.*
import android.graphics.Typeface import android.graphics.Typeface
@ -17,12 +18,8 @@ import android.text.method.PasswordTransformationMethod
import android.util.Log import android.util.Log
import android.view.* import android.view.*
import android.widget.* import android.widget.*
import com.zeapo.pwdstore.PasswordEntry import com.zeapo.pwdstore.*
import com.zeapo.pwdstore.R import com.zeapo.pwdstore.utils.Otp
import com.zeapo.pwdstore.UserPreference
import com.zeapo.pwdstore.pwgenDialogFragment
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.Totp
import kotlinx.android.synthetic.main.decrypt_layout.* import kotlinx.android.synthetic.main.decrypt_layout.*
import kotlinx.android.synthetic.main.encrypt_layout.* import kotlinx.android.synthetic.main.encrypt_layout.*
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
@ -44,6 +41,10 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
private var passwordEntry: PasswordEntry? = null private var passwordEntry: PasswordEntry? = null
private var api: OpenPgpApi? = 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 operation: String by lazy { intent.getStringExtra("OPERATION") }
private val repoPath: String by lazy { intent.getStringExtra("REPO_PATH") } private val repoPath: String by lazy { intent.getStringExtra("REPO_PATH") }
@ -102,6 +103,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
} }
override fun onDestroy() { override fun onDestroy() {
checkAndIncrementHotp()
super.onDestroy() super.onDestroy()
mServiceConnection?.unbindFromService() mServiceConnection?.unbindFromService()
} }
@ -122,14 +124,21 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
override fun onOptionsItemSelected(item: MenuItem?): Boolean { override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) { when (item?.itemId) {
android.R.id.home -> { android.R.id.home -> {
if(passwordEntry?.hotpIsIncremented() == false) {
setResult(RESULT_CANCELED) setResult(RESULT_CANCELED)
}
finish() finish()
} }
R.id.copy_password -> copyPasswordToClipBoard() R.id.copy_password -> copyPasswordToClipBoard()
R.id.share_password_as_plaintext -> shareAsPlaintext() R.id.share_password_as_plaintext -> shareAsPlaintext()
R.id.edit_password -> editPassword() R.id.edit_password -> editPassword()
R.id.crypto_confirm_add -> encrypt() 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) else -> return super.onOptionsItemSelected(item)
} }
return true return true
@ -190,7 +199,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
val iStream = FileUtils.openInputStream(File(fullPath)) val iStream = FileUtils.openInputStream(File(fullPath))
val oStream = ByteArrayOutputStream() 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)) { when (result?.getIntExtra(RESULT_CODE, RESULT_CODE_ERROR)) {
RESULT_CODE_SUCCESS -> { RESULT_CODE_SUCCESS -> {
try { 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_layout.visibility = View.VISIBLE
crypto_extra_show.typeface = monoTypeface crypto_extra_show.typeface = monoTypeface
crypto_extra_show.text = entry.extraContent crypto_extra_show.text = entry.extraContent
crypto_totp_show.visibility = View.VISIBLE crypto_otp_show.visibility = View.VISIBLE
crypto_totp_show_label.visibility = View.VISIBLE crypto_otp_show_label.visibility = View.VISIBLE
crypto_copy_totp.visibility = View.VISIBLE crypto_copy_otp.visibility = View.VISIBLE
crypto_copy_totp.setOnClickListener { copyTotpToClipBoard(Totp.calculateCode(entry.totpSecret, Date().time / 1000)) } if (entry.hasTotp()) {
crypto_totp_show.typeface = monoTypeface crypto_copy_otp.setOnClickListener { copyOtpToClipBoard(Otp.calculateCode(entry.totpSecret, Date().time / (1000 * Otp.TIME_WINDOW))) }
crypto_totp_show.text = Totp.calculateCode(entry.totpSecret, Date().time / 1000); crypto_otp_show.text = Otp.calculateCode(entry.totpSecret, Date().time / (1000 * Otp.TIME_WINDOW))
} else { } else {
crypto_totp_show.visibility = View.GONE // we only want to calculate and show HOTP if the user requests it
crypto_totp_show_label.visibility = View.GONE crypto_copy_otp.setOnClickListener {
crypto_copy_totp.visibility = View.GONE 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
} else {
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)) { if (settings.getBoolean("copy_on_decrypt", true)) {
copyPasswordToClipBoard() copyPasswordToClipBoard()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "An Exception occurred", e) Log.e(TAG, "An Exception occurred", e)
} }
@ -282,23 +339,26 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
RESULT_CODE_ERROR -> handleError(result) RESULT_CODE_ERROR -> handleError(result)
} }
}) }
} }
/** /**
* Encrypts the password and the extra content * Encrypts the password and the extra content
*/ */
private fun encrypt() { private fun encrypt() {
val name = crypto_password_file_edit.text.toString().trim() // if HOTP was incremented, we leave fields as is; they have already been set
val pass = crypto_password_edit.text.toString() if(intent.getStringExtra("OPERATION") != "INCREMENT") {
val extra = crypto_extra_edit.text.toString() 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)) showToast(resources.getString(R.string.file_toast_text))
return return
} }
if (pass.isEmpty() && extra.isEmpty()) { if (editPass?.isEmpty() == true && editExtra?.isEmpty() == true) {
showToast(resources.getString(R.string.empty_toast_text)) showToast(resources.getString(R.string.empty_toast_text))
return return
} }
@ -312,13 +372,12 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
data.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true) data.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true)
// TODO Check if we could use PasswordEntry to generate the file // 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 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? -> api?.executeApiAsync(data, iStream, oStream, { result: Intent? -> when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
OpenPgpApi.RESULT_CODE_SUCCESS -> { OpenPgpApi.RESULT_CODE_SUCCESS -> {
try { try {
// TODO This might fail, we should check that the write is successful // TODO This might fail, we should check that the write is successful
@ -328,15 +387,16 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
val returnIntent = Intent() val returnIntent = Intent()
returnIntent.putExtra("CREATED_FILE", path) returnIntent.putExtra("CREATED_FILE", path)
returnIntent.putExtra("NAME", name) returnIntent.putExtra("NAME", editName)
// if coming from decrypt screen->edit button // if coming from decrypt screen->edit button
if (intent.getBooleanExtra("fromDecrypt", false)) { if (intent.getBooleanExtra("fromDecrypt", false)) {
data.putExtra("needCommit", true) returnIntent.putExtra("OPERATION", "EDIT")
returnIntent.putExtra("needCommit", true)
} }
setResult(RESULT_OK, returnIntent) setResult(RESULT_OK, returnIntent)
finish() finish()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "An Exception occurred", e) Log.e(TAG, "An Exception occurred", e)
} }
@ -378,6 +438,43 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
invalidateOptionsMenu() 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 * Get the Key ids from OpenKeychain
*/ */
@ -500,13 +597,12 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
showToast(resources.getString(R.string.clipboard_username_toast_text)) 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) val clip = ClipData.newPlainText("pgp_handler_result_pm", code)
clipboard.primaryClip = clip clipboard.primaryClip = clip
showToast(resources.getString(R.string.clipboard_totp_toast_text)) showToast(resources.getString(R.string.clipboard_otp_toast_text))
} }
private fun shareAsPlaintext() { private fun shareAsPlaintext() {
if (findViewById<View>(R.id.share_password_as_plaintext) == null) if (findViewById<View>(R.id.share_password_as_plaintext) == null)
return return
@ -586,6 +682,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
override fun onPostExecute(b: Boolean?) { override fun onPostExecute(b: Boolean?) {
if (skip) return if (skip) return
checkAndIncrementHotp()
// only clear the clipboard if we automatically copied the password to it // only clear the clipboard if we automatically copied the password to it
if (settings.getBoolean("copy_on_decrypt", true)) { if (settings.getBoolean("copy_on_decrypt", true)) {
@ -602,13 +699,15 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
} }
if (crypto_password_show != null) { if (crypto_password_show != null) {
passwordEntry = null
// clear password; if decrypt changed to encrypt layout via edit button, no need // 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_password_show.text = ""
crypto_extra_show.text = "" crypto_extra_show.text = ""
crypto_extra_show_layout.visibility = View.INVISIBLE crypto_extra_show_layout.visibility = View.INVISIBLE
crypto_container_decrypt.visibility = View.INVISIBLE crypto_container_decrypt.visibility = View.INVISIBLE
setResult(Activity.RESULT_CANCELED)
finish() finish()
} }
} }

View file

@ -13,18 +13,18 @@ import java.util.Arrays;
import javax.crypto.Mac; import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec; 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 String ALGORITHM = "HmacSHA1";
private static final int TIME_WINDOW = 30;
private static final int CODE_DIGITS = 6; private static final int CODE_DIGITS = 6;
private static final Base32 BASE_32 = new Base32(); 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); SecretKeySpec signingKey = new SecretKeySpec(BASE_32.decode(secret), ALGORITHM);
Mac mac = null; Mac mac = null;
@ -39,8 +39,7 @@ public class Totp {
return null; return null;
} }
long time = epochSeconds / TIME_WINDOW; byte[] digest = mac.doFinal(ByteBuffer.allocate(8).putLong(counter).array());
byte[] digest = mac.doFinal(ByteBuffer.allocate(8).putLong(time).array());
int offset = digest[digest.length - 1] & 0xf; int offset = digest[digest.length - 1] & 0xf;
byte[] code = Arrays.copyOfRange(digest, offset, offset + 4); byte[] code = Arrays.copyOfRange(digest, offset, offset + 4);
code[0] = (byte) (0x7f & code[0]); code[0] = (byte) (0x7f & code[0]);

View file

@ -3,15 +3,15 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context="com.zeapo.pwdstore.crypto.PgpActivity" android:background="@color/background"
android:orientation="vertical" android:orientation="vertical"
android:background="@color/background"> tools:context="com.zeapo.pwdstore.crypto.PgpActivity">
<LinearLayout <LinearLayout
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="16dp" android:orientation="vertical"
android:orientation="vertical"> android:padding="16dp">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -61,17 +61,17 @@
<ImageView <ImageView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:src="@drawable/divider"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:layout_marginTop="16dp"
android:src="@drawable/divider"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
<LinearLayout <LinearLayout
android:id="@+id/crypto_container_decrypt" android:id="@+id/crypto_container_decrypt"
android:orientation="vertical"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_vertical_margin" android:layout_marginTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
android:visibility="invisible"> android:visibility="invisible">
<GridLayout <GridLayout
@ -83,39 +83,40 @@
android:id="@+id/crypto_password_show_label" android:id="@+id/crypto_password_show_label"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textStyle="bold" android:layout_column="0"
android:textColor="@android:color/black"
android:text="@string/password"
android:layout_row="0" android:layout_row="0"
android:layout_column="0"/> android:text="@string/password"
android:textColor="@android:color/black"
android:textStyle="bold" />
<TextView <TextView
android:id="@+id/crypto_password_show" android:id="@+id/crypto_password_show"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:typeface="monospace"
android:textColor="@android:color/black"
android:layout_column="2" android:layout_column="2"
android:layout_row="0"/> android:layout_row="0"
android:textColor="@android:color/black"
android:typeface="monospace" />
<ProgressBar <ProgressBar
android:id="@+id/pbLoading" android:id="@+id/pbLoading"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="8dp" 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_column="0"
android:layout_columnSpan="3"/> android:layout_columnSpan="3"
android:layout_marginBottom="8dp"
android:layout_marginTop="8dp"
android:layout_row="1" />
<Button <Button
android:id="@+id/crypto_password_toggle_show" android:id="@+id/crypto_password_toggle_show"
android:layout_width="match_parent" android:layout_width="match_parent"
android:text="@string/show_password"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_row="2"
android:layout_column="0" android:layout_column="0"
android:layout_columnSpan="3"/> android:layout_columnSpan="3"
android:layout_row="2"
android:text="@string/show_password" />
</GridLayout> </GridLayout>
@ -129,13 +130,13 @@
android:id="@+id/crypto_copy_username" android:id="@+id/crypto_copy_username"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:contentDescription="@string/copy_username"
android:visibility="invisible"
android:background="@color/background" 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 <TextView
android:id="@+id/crypto_username_show_label" android:id="@+id/crypto_username_show_label"
@ -146,10 +147,10 @@
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:layout_toLeftOf="@id/crypto_copy_username" android:layout_toLeftOf="@id/crypto_copy_username"
android:layout_toStartOf="@id/crypto_copy_username" android:layout_toStartOf="@id/crypto_copy_username"
android:visibility="invisible"
android:text="@string/username" android:text="@string/username"
android:textColor="@android:color/black" android:textColor="@android:color/black"
android:textStyle="bold" /> android:textStyle="bold"
android:visibility="invisible" />
<TextView <TextView
android:id="@+id/crypto_username_show" android:id="@+id/crypto_username_show"
@ -160,45 +161,45 @@
android:layout_below="@id/crypto_username_show_label" android:layout_below="@id/crypto_username_show_label"
android:layout_toLeftOf="@id/crypto_copy_username" android:layout_toLeftOf="@id/crypto_copy_username"
android:layout_toStartOf="@id/crypto_copy_username" android:layout_toStartOf="@id/crypto_copy_username"
android:visibility="invisible"
android:textColor="@android:color/black" android:textColor="@android:color/black"
android:textIsSelectable="true" android:textIsSelectable="true"
android:typeface="monospace" /> android:typeface="monospace"
android:visibility="invisible" />
<ImageButton <ImageButton
android:id="@+id/crypto_copy_totp" android:id="@+id/crypto_copy_otp"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@id/crypto_username_show" android:layout_below="@id/crypto_username_show"
android:visibility="invisible"
android:contentDescription="@string/copy_totp"
android:background="@color/background" 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 <TextView
android:id="@+id/crypto_totp_show_label" android:id="@+id/crypto_otp_show_label"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="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: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:textColor="@android:color/black"
android:textStyle="bold" /> android:textStyle="bold" />
<TextView <TextView
android:id="@+id/crypto_totp_show" android:id="@+id/crypto_otp_show"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_below="@id/crypto_totp_show_label" android:layout_below="@id/crypto_otp_show_label"
android:layout_toLeftOf="@id/crypto_copy_totp" android:layout_toLeftOf="@id/crypto_copy_otp"
android:layout_toStartOf="@id/crypto_copy_totp" android:layout_toStartOf="@id/crypto_copy_otp"
android:textColor="@android:color/black" android:textColor="@android:color/black"
android:textIsSelectable="true" android:textIsSelectable="true"
android:typeface="monospace" /> android:typeface="monospace" />
@ -209,7 +210,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="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:text="@string/extra_content"
android:textColor="@android:color/black" android:textColor="@android:color/black"
android:textStyle="bold" /> android:textStyle="bold" />

View file

@ -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>

View file

@ -65,11 +65,11 @@
<string name="password">كلمة السر :</string> <string name="password">كلمة السر :</string>
<string name="extra_content">بيانات إضافية :</string> <string name="extra_content">بيانات إضافية :</string>
<string name="username">إسم المستخدم :</string> <string name="username">إسم المستخدم :</string>
<string name="totp">TOTP :</string> <string name="otp">OTP :</string>
<string name="edit_password">تعديل كلمة السر</string> <string name="edit_password">تعديل كلمة السر</string>
<string name="copy_password">نسخ كلمة السر</string> <string name="copy_password">نسخ كلمة السر</string>
<string name="copy_username">نسخ إسم المستخدم</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="share_as_plaintext">شارك كنص مجرد</string>
<string name="last_changed">آخِر تعديل %s</string> <string name="last_changed">آخِر تعديل %s</string>

View file

@ -23,14 +23,15 @@
<!-- git commits --> <!-- git commits -->
<string name="add_commit_text">[ANDROID PwdStore] Add &#160;</string> <string name="add_commit_text">[ANDROID PwdStore] Add &#160;</string>
<string name="edit_commit_text">[ANDROID PwdStore] Edit &#160;</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">&#160; from store.</string> <string name="from_store">&#160; from store.</string>
<!-- PGPHandler --> <!-- PGPHandler -->
<string name="provider_toast_text">No OpenPGP Provider selected!</string> <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_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_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="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> <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="password">Password:</string>
<string name="extra_content">Extra content:</string> <string name="extra_content">Extra content:</string>
<string name="username">Username:</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="edit_password">Edit password</string>
<string name="copy_password">Copy password</string> <string name="copy_password">Copy password</string>
<string name="copy_username">Copy username</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="share_as_plaintext">Share as plaintext</string>
<string name="last_changed">Last changed %s</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 --> <!-- Preferences -->
<string name="pref_git_title">Git</string> <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="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="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="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="remember_the_passphrase">Remember the passphrase in the app configuration (insecure)</string>
<string name="hackish_tools">Hackish tools</string> <string name="hackish_tools">Hackish tools</string>
<string name="abort_rebase">Abort rebase</string> <string name="abort_rebase">Abort rebase</string>
@ -224,4 +231,5 @@
<string name="crypto_password_edit_hint">p@ssw0rd!</string> <string name="crypto_password_edit_hint">p@ssw0rd!</string>
<string name="crypto_extra_edit_hint">username: something other extra content</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="get_last_changed_failed">Failed to get last changed date</string>
<string name="hotp_pending">Tap copy to calculate HOTP</string>
</resources> </resources>

View file

@ -16,6 +16,9 @@
<Preference <Preference
android:key="ssh_key_clear_passphrase" android:key="ssh_key_clear_passphrase"
android:title="@string/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 <Preference
android:key="ssh_see_key" android:key="ssh_see_key"
android:title="@string/pref_ssh_see_key_title" /> android:title="@string/pref_ssh_see_key_title" />