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:
parent
ac889abdd3
commit
eea0e68dda
14 changed files with 340 additions and 115 deletions
12
app/src/androidTest/java/com/zeapo/pwdstore/OtpTest.java
Normal file
12
app/src/androidTest/java/com/zeapo/pwdstore/OtpTest.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,11 +27,14 @@ 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() {
|
||||||
return password;
|
return password;
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)) {
|
||||||
commitChange(this.getResources().getString(R.string.edit_commit_text) + data.getExtras().getString("NAME"));
|
if (data.getStringExtra("OPERATION").equals("EDIT")) {
|
||||||
refreshListAdapter();
|
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;
|
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));
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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 -> {
|
||||||
setResult(RESULT_CANCELED)
|
if(passwordEntry?.hotpIsIncremented() == false) {
|
||||||
|
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
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
crypto_totp_show.visibility = View.GONE
|
crypto_otp_show.visibility = View.GONE
|
||||||
crypto_totp_show_label.visibility = View.GONE
|
crypto_otp_show_label.visibility = View.GONE
|
||||||
crypto_copy_totp.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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]);
|
|
@ -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" />
|
||||||
|
@ -230,4 +231,4 @@
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
21
app/src/main/res/layout/otp_confirm_layout.xml
Normal file
21
app/src/main/res/layout/otp_confirm_layout.xml
Normal 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>
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -23,14 +23,15 @@
|
||||||
|
|
||||||
<!-- git commits -->
|
<!-- git commits -->
|
||||||
<string name="add_commit_text">[ANDROID PwdStore] Add  </string>
|
<string name="add_commit_text">[ANDROID PwdStore] Add  </string>
|
||||||
<string name="edit_commit_text">[ANDROID PwdStore] Edit  </string>
|
<string name="edit_commit_text">"[ANDROID PwdStore] Edit "</string>
|
||||||
|
<string name="increment_commit_text">"[ANDROID PwdStore] Increment HOTP counter for "</string>
|
||||||
<string name="from_store">  from store.</string>
|
<string name="from_store">  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>
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
Loading…
Reference in a new issue