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());
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.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />
<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE"
tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application android:allowBackup="true"

View file

@ -12,10 +12,14 @@ public class PasswordEntry {
private static final String[] USERNAME_FIELDS = new String[]{"login", "username"};
private final String extraContent;
private String extraContent;
private final String password;
private final String username;
private final String totpSecret;
private final String hotpSecret;
private final Long hotpCounter;
private final String content;
private boolean isIncremented = false;
public PasswordEntry(final ByteArrayOutputStream os) throws UnsupportedEncodingException {
this(os.toString("UTF-8"));
@ -23,11 +27,14 @@ public class PasswordEntry {
public PasswordEntry(final String decryptedContent) {
final String[] passContent = decryptedContent.split("\n", 2);
content = decryptedContent;
password = passContent[0];
extraContent = passContent.length > 1 ? passContent[1] : "";
totpSecret = findTotpSecret(content);
hotpSecret = findHotpSecret(content);
hotpCounter = findHotpCounter(content);
extraContent = findExtraContent(passContent);
username = findUsername();
totpSecret = findTotpSecret(decryptedContent);
}
}
public String getPassword() {
return password;
@ -45,6 +52,14 @@ public class PasswordEntry {
return totpSecret;
}
public Long getHotpCounter() {
return hotpCounter;
}
public String getHotpSecret() {
return hotpSecret;
}
public boolean hasExtraContent() {
return extraContent.length() != 0;
}
@ -53,7 +68,24 @@ public class PasswordEntry {
return username != null;
}
public boolean hasTotp() { return totpSecret != null; }
public boolean hasTotp() {
return totpSecret != null;
}
public boolean hasHotp() {
return hotpSecret != null && hotpCounter != null;
}
public boolean hotpIsIncremented() { return isIncremented; }
public void incrementHotp() {
for (String line : content.split("\n")) {
if (line.startsWith("otpauth://hotp/")) {
extraContent = extraContent.replaceFirst("counter=[0-9]+", "counter=" + Long.toString(hotpCounter + 1));
isIncremented = true;
}
}
}
private String findUsername() {
final String[] extraLines = extraContent.split("\n");
@ -75,4 +107,31 @@ public class PasswordEntry {
}
return null;
}
private String findHotpSecret(String decryptedContent) {
for (String line : decryptedContent.split("\n")) {
if (line.startsWith("otpauth://hotp/")) {
return Uri.parse(line).getQueryParameter("secret");
}
}
return null;
}
private Long findHotpCounter(String decryptedContent) {
for (String line : decryptedContent.split("\n")) {
if (line.startsWith("otpauth://hotp/")) {
return Long.parseLong(Uri.parse(line).getQueryParameter("counter"));
}
}
return null;
}
private String findExtraContent(String [] passContent) {
String extraContent = passContent.length > 1 ? passContent[1] : "";
// if there is a HOTP URI, we must return the extra content with the counter incremented
if (hasHotp()) {
return extraContent.replaceFirst("counter=[0-9]+", "counter=" + Long.toString(hotpCounter));
}
return extraContent;
}
}

View file

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

View file

@ -80,6 +80,12 @@ class UserPreference : AppCompatActivity() {
true
}
findPreference("hotp_remember_clear_choice").onPreferenceClickListener = Preference.OnPreferenceClickListener {
sharedPreferences.edit().putBoolean("hotp_remember_check", false).apply()
it.isEnabled = false
true
}
findPreference("git_server_info").onPreferenceClickListener = Preference.OnPreferenceClickListener {
val intent = Intent(callingActivity, GitActivity::class.java)
intent.putExtra("Operation", GitActivity.EDIT_SERVER)
@ -161,6 +167,7 @@ class UserPreference : AppCompatActivity() {
findPreference("ssh_see_key").isEnabled = sharedPreferences.getBoolean("use_generated_key", false)
findPreference("git_delete_repo").isEnabled = !sharedPreferences.getBoolean("git_external", false)
findPreference("ssh_key_clear_passphrase").isEnabled = sharedPreferences.getString("ssh_key_passphrase", null)?.isNotEmpty() ?: false
findPreference("hotp_remember_clear_choice").isEnabled = sharedPreferences.getBoolean("hotp_remember_check", false)
val keyPref = findPreference("openpgp_key_id_pref")
val selectedKeys: Array<String> = ArrayList<String>(sharedPreferences.getStringSet("openpgp_key_ids_set", HashSet<String>())).toTypedArray()
if (selectedKeys.isEmpty()) {

View file

@ -2,6 +2,7 @@ package com.zeapo.pwdstore.crypto
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.app.PendingIntent
import android.content.*
import android.graphics.Typeface
@ -17,12 +18,8 @@ import android.text.method.PasswordTransformationMethod
import android.util.Log
import android.view.*
import android.widget.*
import com.zeapo.pwdstore.PasswordEntry
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.UserPreference
import com.zeapo.pwdstore.pwgenDialogFragment
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.Totp
import com.zeapo.pwdstore.*
import com.zeapo.pwdstore.utils.Otp
import kotlinx.android.synthetic.main.decrypt_layout.*
import kotlinx.android.synthetic.main.encrypt_layout.*
import org.apache.commons.io.FileUtils
@ -44,6 +41,10 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
private var passwordEntry: PasswordEntry? = null
private var api: OpenPgpApi? = null
private var editName: String? = null
private var editPass: String? = null
private var editExtra: String? = null
private val operation: String by lazy { intent.getStringExtra("OPERATION") }
private val repoPath: String by lazy { intent.getStringExtra("REPO_PATH") }
@ -102,6 +103,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
}
override fun onDestroy() {
checkAndIncrementHotp()
super.onDestroy()
mServiceConnection?.unbindFromService()
}
@ -122,14 +124,21 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
android.R.id.home -> {
setResult(RESULT_CANCELED)
if(passwordEntry?.hotpIsIncremented() == false) {
setResult(RESULT_CANCELED)
}
finish()
}
R.id.copy_password -> copyPasswordToClipBoard()
R.id.share_password_as_plaintext -> shareAsPlaintext()
R.id.edit_password -> editPassword()
R.id.crypto_confirm_add -> encrypt()
R.id.crypto_cancel_add -> setResult(RESULT_CANCELED)
R.id.crypto_cancel_add -> {
if(passwordEntry?.hotpIsIncremented() == false) {
setResult(RESULT_CANCELED)
}
finish()
}
else -> return super.onOptionsItemSelected(item)
}
return true
@ -190,7 +199,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
val iStream = FileUtils.openInputStream(File(fullPath))
val oStream = ByteArrayOutputStream()
api?.executeApiAsync(data, iStream, oStream, { result: Intent? ->
api?.executeApiAsync(data, iStream, oStream) { result: Intent? ->
when (result?.getIntExtra(RESULT_CODE, RESULT_CODE_ERROR)) {
RESULT_CODE_SUCCESS -> {
try {
@ -253,27 +262,75 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
}
}
if (entry.hasTotp()) {
if (entry.hasTotp() || entry.hasHotp()) {
crypto_extra_show_layout.visibility = View.VISIBLE
crypto_extra_show.typeface = monoTypeface
crypto_extra_show.text = entry.extraContent
crypto_totp_show.visibility = View.VISIBLE
crypto_totp_show_label.visibility = View.VISIBLE
crypto_copy_totp.visibility = View.VISIBLE
crypto_otp_show.visibility = View.VISIBLE
crypto_otp_show_label.visibility = View.VISIBLE
crypto_copy_otp.visibility = View.VISIBLE
if (entry.hasTotp()) {
crypto_copy_otp.setOnClickListener { copyOtpToClipBoard(Otp.calculateCode(entry.totpSecret, Date().time / (1000 * Otp.TIME_WINDOW))) }
crypto_otp_show.text = Otp.calculateCode(entry.totpSecret, Date().time / (1000 * Otp.TIME_WINDOW))
} else {
// we only want to calculate and show HOTP if the user requests it
crypto_copy_otp.setOnClickListener {
if (settings.getBoolean("hotp_remember_check", false)) {
if (settings.getBoolean("hotp_remember_choice", false)) {
calculateAndCommitHotp(entry)
} else {
calculateHotp(entry)
}
} else {
// show a dialog asking permission to update the HOTP counter in the entry
val checkInflater = LayoutInflater.from(this)
val checkLayout = checkInflater.inflate(R.layout.otp_confirm_layout, null)
val rememberCheck : CheckBox = checkLayout.findViewById(R.id.hotp_remember_checkbox)
val dialogBuilder = AlertDialog.Builder(this)
dialogBuilder.setView(checkLayout)
dialogBuilder.setMessage(R.string.dialog_update_body)
.setCancelable(false)
.setPositiveButton(R.string.dialog_update_positive, DialogInterface.OnClickListener { dialog, id ->
run {
calculateAndCommitHotp(entry)
if (rememberCheck.isChecked()) {
val editor = settings.edit()
editor.putBoolean("hotp_remember_check", true)
editor.putBoolean("hotp_remember_choice", true)
editor.commit()
}
}
})
.setNegativeButton(R.string.dialog_update_negative, DialogInterface.OnClickListener { dialog, id ->
run {
calculateHotp(entry)
val editor = settings.edit()
editor.putBoolean("hotp_remember_check", true)
editor.putBoolean("hotp_remember_choice", false)
editor.commit()
}
})
val updateDialog = dialogBuilder.create()
updateDialog.setTitle(R.string.dialog_update_title)
updateDialog.show()
}
}
crypto_otp_show.setText(R.string.hotp_pending)
}
crypto_otp_show.typeface = monoTypeface
crypto_copy_totp.setOnClickListener { copyTotpToClipBoard(Totp.calculateCode(entry.totpSecret, Date().time / 1000)) }
crypto_totp_show.typeface = monoTypeface
crypto_totp_show.text = Totp.calculateCode(entry.totpSecret, Date().time / 1000);
} else {
crypto_totp_show.visibility = View.GONE
crypto_totp_show_label.visibility = View.GONE
crypto_copy_totp.visibility = View.GONE
crypto_otp_show.visibility = View.GONE
crypto_otp_show_label.visibility = View.GONE
crypto_copy_otp.visibility = View.GONE
}
if (settings.getBoolean("copy_on_decrypt", true)) {
copyPasswordToClipBoard()
}
} catch (e: Exception) {
Log.e(TAG, "An Exception occurred", e)
}
@ -282,23 +339,26 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
RESULT_CODE_ERROR -> handleError(result)
}
})
}
}
/**
* Encrypts the password and the extra content
*/
private fun encrypt() {
val name = crypto_password_file_edit.text.toString().trim()
val pass = crypto_password_edit.text.toString()
val extra = crypto_extra_edit.text.toString()
// if HOTP was incremented, we leave fields as is; they have already been set
if(intent.getStringExtra("OPERATION") != "INCREMENT") {
editName = crypto_password_file_edit.text.toString().trim()
editPass = crypto_password_edit.text.toString()
editExtra = crypto_extra_edit.text.toString()
}
if (name.isEmpty()) {
if (editName?.isEmpty() == true) {
showToast(resources.getString(R.string.file_toast_text))
return
}
if (pass.isEmpty() && extra.isEmpty()) {
if (editPass?.isEmpty() == true && editExtra?.isEmpty() == true) {
showToast(resources.getString(R.string.empty_toast_text))
return
}
@ -312,13 +372,12 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
data.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true)
// TODO Check if we could use PasswordEntry to generate the file
val iStream = ByteArrayInputStream("$pass\n$extra".toByteArray(Charset.forName("UTF-8")))
val iStream = ByteArrayInputStream("$editPass\n$editExtra".toByteArray(Charset.forName("UTF-8")))
val oStream = ByteArrayOutputStream()
val path = if (intent.getStringExtra("OPERATION") == "EDIT") fullPath else "$fullPath/$name.gpg"
val path = if (intent.getBooleanExtra("fromDecrypt", false)) fullPath else "$fullPath/$editName.gpg"
api?.executeApiAsync(data, iStream, oStream, { result: Intent? ->
when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
api?.executeApiAsync(data, iStream, oStream, { result: Intent? -> when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
OpenPgpApi.RESULT_CODE_SUCCESS -> {
try {
// TODO This might fail, we should check that the write is successful
@ -328,15 +387,16 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
val returnIntent = Intent()
returnIntent.putExtra("CREATED_FILE", path)
returnIntent.putExtra("NAME", name)
returnIntent.putExtra("NAME", editName)
// if coming from decrypt screen->edit button
if (intent.getBooleanExtra("fromDecrypt", false)) {
data.putExtra("needCommit", true)
returnIntent.putExtra("OPERATION", "EDIT")
returnIntent.putExtra("needCommit", true)
}
setResult(RESULT_OK, returnIntent)
finish()
} catch (e: Exception) {
Log.e(TAG, "An Exception occurred", e)
}
@ -378,6 +438,43 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
invalidateOptionsMenu()
}
/**
* Writes updated HOTP counter to edit fields and encrypts
*/
private fun checkAndIncrementHotp() {
// we do not want to increment the HOTP counter if the user has edited the entry or has not
// generated an HOTP code
if(intent.getStringExtra("OPERATION") != "EDIT" && passwordEntry?.hotpIsIncremented() == true) {
editName = name.trim()
editPass = passwordEntry?.password
editExtra = passwordEntry?.extraContent
val data = Intent(this, PgpActivity::class.java)
data.putExtra("OPERATION", "INCREMENT")
data.putExtra("fromDecrypt", true)
intent = data
encrypt()
}
}
private fun calculateHotp(entry : PasswordEntry) {
copyOtpToClipBoard(Otp.calculateCode(entry.hotpSecret, entry.hotpCounter + 1))
crypto_otp_show.text = Otp.calculateCode(entry.hotpSecret, entry.hotpCounter + 1)
crypto_extra_show.text = entry.extraContent
}
private fun calculateAndCommitHotp(entry : PasswordEntry) {
calculateHotp(entry)
entry.incrementHotp()
// we must set the result before encrypt() is called, since in
// some cases it is called during the finish() sequence
val returnIntent = Intent()
returnIntent.putExtra("NAME", name.trim())
returnIntent.putExtra("OPERATION", "INCREMENT")
returnIntent.putExtra("needCommit", true)
setResult(RESULT_OK, returnIntent)
}
/**
* Get the Key ids from OpenKeychain
*/
@ -500,13 +597,12 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
showToast(resources.getString(R.string.clipboard_username_toast_text))
}
private fun copyTotpToClipBoard(code: String) {
private fun copyOtpToClipBoard(code: String) {
val clip = ClipData.newPlainText("pgp_handler_result_pm", code)
clipboard.primaryClip = clip
showToast(resources.getString(R.string.clipboard_totp_toast_text))
showToast(resources.getString(R.string.clipboard_otp_toast_text))
}
private fun shareAsPlaintext() {
if (findViewById<View>(R.id.share_password_as_plaintext) == null)
return
@ -586,6 +682,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
override fun onPostExecute(b: Boolean?) {
if (skip) return
checkAndIncrementHotp()
// only clear the clipboard if we automatically copied the password to it
if (settings.getBoolean("copy_on_decrypt", true)) {
@ -602,13 +699,15 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
}
if (crypto_password_show != null) {
passwordEntry = null
// clear password; if decrypt changed to encrypt layout via edit button, no need
if(passwordEntry?.hotpIsIncremented() == false) {
setResult(Activity.RESULT_CANCELED)
}
passwordEntry = null
crypto_password_show.text = ""
crypto_extra_show.text = ""
crypto_extra_show_layout.visibility = View.INVISIBLE
crypto_container_decrypt.visibility = View.INVISIBLE
setResult(Activity.RESULT_CANCELED)
finish()
}
}

View file

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

View file

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

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

View file

@ -23,14 +23,15 @@
<!-- git commits -->
<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>
<!-- PGPHandler -->
<string name="provider_toast_text">No OpenPGP Provider selected!</string>
<string name="clipboard_password_toast_text">Password copied to clipboard, you have %d seconds to paste it somewhere.</string>
<string name="clipboard_username_toast_text">Username copied to clipboard</string>
<string name="clipboard_totp_toast_text">TOTP code copied to clipboard</string>
<string name="clipboard_otp_toast_text">OTP code copied to clipboard</string>
<string name="file_toast_text">Please provide a file name</string>
<string name="empty_toast_text">You cannot use an empty password or empty extra content</string>
@ -93,13 +94,18 @@
<string name="password">Password:</string>
<string name="extra_content">Extra content:</string>
<string name="username">Username:</string>
<string name="totp">TOTP:</string>
<string name="otp">OTP:</string>
<string name="edit_password">Edit password</string>
<string name="copy_password">Copy password</string>
<string name="copy_username">Copy username</string>
<string name="copy_totp">Copy OTP code</string>
<string name="copy_otp">Copy OTP code</string>
<string name="share_as_plaintext">Share as plaintext</string>
<string name="last_changed">Last changed %s</string>
<string name="dialog_update_title">Attention</string>
<string name="dialog_update_positive">Update entry</string>
<string name="dialog_update_negative">Leave unchanged</string>
<string name="dialog_update_body">The HOTP counter is about to be incremented. This change will be committed. If you press "Leave unchanged", the HOTP code will be shown, but the counter will not be changed.</string>
<string name="dialog_update_check">Remember my choice</string>
<!-- Preferences -->
<string name="pref_git_title">Git</string>
@ -217,6 +223,7 @@
<string name="git_push_other_error">Remote rejected non-fast-forward push. Check receive.denyNonFastForwards variable in config file of destination repository.</string>
<string name="jgit_error_push_dialog_text">Error occurred during the push operation:</string>
<string name="ssh_key_clear_passphrase">Clear ssh-key saved passphrase</string>
<string name="hotp_remember_clear_choice">Clear saved preference for HOTP incrementing</string>
<string name="remember_the_passphrase">Remember the passphrase in the app configuration (insecure)</string>
<string name="hackish_tools">Hackish tools</string>
<string name="abort_rebase">Abort rebase</string>
@ -224,4 +231,5 @@
<string name="crypto_password_edit_hint">p@ssw0rd!</string>
<string name="crypto_extra_edit_hint">username: something other extra content</string>
<string name="get_last_changed_failed">Failed to get last changed date</string>
<string name="hotp_pending">Tap copy to calculate HOTP</string>
</resources>

View file

@ -16,6 +16,9 @@
<Preference
android:key="ssh_key_clear_passphrase"
android:title="@string/ssh_key_clear_passphrase" />
<Preference
android:key="hotp_remember_clear_choice"
android:title="@string/hotp_remember_clear_choice" />
<Preference
android:key="ssh_see_key"
android:title="@string/pref_ssh_see_key_title" />