Remove HOTP/TOTP support (#806)
This commit is contained in:
parent
ffcbabc2f4
commit
e7463ec24c
17 changed files with 13 additions and 484 deletions
|
@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
|
|||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
- **BREAKING**: Remove support for HOTP/TOTP secrets - Please use FIDO keys or a dedicated app like [Aegis](https://github.com/beemdevelopment/Aegis) or [andOTP](https://github.com/andOTP/andOTP).
|
||||
- Reduce Autofill false positives on username fields by removing "name" from list of heuristic terms
|
||||
- Reduced app size
|
||||
- Improve IME experience with server config screen
|
||||
|
|
|
@ -123,4 +123,7 @@ dependencies {
|
|||
androidTestImplementation deps.testing.androidx.junit
|
||||
androidTestImplementation deps.testing.androidx.espresso_core
|
||||
androidTestImplementation deps.testing.androidx.espresso_intents
|
||||
|
||||
testImplementation deps.testing.junit
|
||||
testImplementation deps.testing.kotlin_test_junit
|
||||
}
|
||||
|
|
|
@ -4,26 +4,18 @@
|
|||
*/
|
||||
package com.zeapo.pwdstore
|
||||
|
||||
import android.net.Uri
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.UnsupportedEncodingException
|
||||
|
||||
/**
|
||||
* A single entry in password store.
|
||||
*/
|
||||
class PasswordEntry(private val content: String) {
|
||||
class PasswordEntry(content: String) {
|
||||
|
||||
val password: String
|
||||
val username: String?
|
||||
val digits: String
|
||||
val totpSecret: String?
|
||||
val totpPeriod: Long
|
||||
val totpAlgorithm: String
|
||||
val hotpSecret: String?
|
||||
val hotpCounter: Long?
|
||||
var extraContent: String
|
||||
private set
|
||||
private var isIncremented = false
|
||||
|
||||
@Throws(UnsupportedEncodingException::class)
|
||||
constructor(os: ByteArrayOutputStream) : this(os.toString("UTF-8"))
|
||||
|
@ -31,12 +23,6 @@ class PasswordEntry(private val content: String) {
|
|||
init {
|
||||
val passContent = content.split("\n".toRegex(), 2).toTypedArray()
|
||||
password = passContent[0]
|
||||
digits = findOtpDigits(content)
|
||||
totpSecret = findTotpSecret(content)
|
||||
totpPeriod = findTotpPeriod(content)
|
||||
totpAlgorithm = findTotpAlgorithm(content)
|
||||
hotpSecret = findHotpSecret(content)
|
||||
hotpCounter = findHotpCounter(content)
|
||||
extraContent = findExtraContent(passContent)
|
||||
username = findUsername()
|
||||
}
|
||||
|
@ -49,27 +35,6 @@ class PasswordEntry(private val content: String) {
|
|||
return username != null
|
||||
}
|
||||
|
||||
fun hasTotp(): Boolean {
|
||||
return totpSecret != null
|
||||
}
|
||||
|
||||
fun hasHotp(): Boolean {
|
||||
return hotpSecret != null && hotpCounter != null
|
||||
}
|
||||
|
||||
fun hotpIsIncremented(): Boolean {
|
||||
return isIncremented
|
||||
}
|
||||
|
||||
fun incrementHotp() {
|
||||
content.split("\n".toRegex()).forEach { line ->
|
||||
if (line.startsWith("otpauth://hotp/")) {
|
||||
extraContent = extraContent.replaceFirst("counter=[0-9]+".toRegex(), "counter=${hotpCounter!! + 1}")
|
||||
isIncremented = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val extraContentWithoutUsername by lazy {
|
||||
var usernameFound = false
|
||||
extraContent.splitToSequence("\n").filter { line ->
|
||||
|
@ -93,73 +58,8 @@ class PasswordEntry(private val content: String) {
|
|||
return null
|
||||
}
|
||||
|
||||
private fun findTotpSecret(decryptedContent: String): String? {
|
||||
decryptedContent.split("\n".toRegex()).forEach { line ->
|
||||
if (line.startsWith("otpauth://totp/")) {
|
||||
return Uri.parse(line).getQueryParameter("secret")
|
||||
}
|
||||
if (line.startsWith("totp:", ignoreCase = true)) {
|
||||
return line.split(": *".toRegex(), 2).toTypedArray()[1]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun findOtpDigits(decryptedContent: String): String {
|
||||
decryptedContent.split("\n".toRegex()).forEach { line ->
|
||||
if ((line.startsWith("otpauth://totp/") ||
|
||||
line.startsWith("otpauth://hotp/")) &&
|
||||
Uri.parse(line).getQueryParameter("digits") != null) {
|
||||
return Uri.parse(line).getQueryParameter("digits")!!
|
||||
}
|
||||
}
|
||||
return "6"
|
||||
}
|
||||
|
||||
private fun findTotpPeriod(decryptedContent: String): Long {
|
||||
decryptedContent.split("\n".toRegex()).forEach { line ->
|
||||
if (line.startsWith("otpauth://totp/") &&
|
||||
Uri.parse(line).getQueryParameter("period") != null) {
|
||||
return java.lang.Long.parseLong(Uri.parse(line).getQueryParameter("period")!!)
|
||||
}
|
||||
}
|
||||
return 30
|
||||
}
|
||||
|
||||
private fun findTotpAlgorithm(decryptedContent: String): String {
|
||||
decryptedContent.split("\n".toRegex()).forEach { line ->
|
||||
if (line.startsWith("otpauth://totp/") &&
|
||||
Uri.parse(line).getQueryParameter("algorithm") != null) {
|
||||
return Uri.parse(line).getQueryParameter("algorithm")!!
|
||||
}
|
||||
}
|
||||
return "sha1"
|
||||
}
|
||||
|
||||
private fun findHotpSecret(decryptedContent: String): String? {
|
||||
decryptedContent.split("\n".toRegex()).forEach { line ->
|
||||
if (line.startsWith("otpauth://hotp/")) {
|
||||
return Uri.parse(line).getQueryParameter("secret")
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun findHotpCounter(decryptedContent: String): Long? {
|
||||
decryptedContent.split("\n".toRegex()).forEach { line ->
|
||||
if (line.startsWith("otpauth://hotp/")) {
|
||||
return java.lang.Long.parseLong(Uri.parse(line).getQueryParameter("counter")!!)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun findExtraContent(passContent: Array<String>): String {
|
||||
val extraContent = if (passContent.size > 1) passContent[1] else ""
|
||||
// if there is a HOTP URI, we must return the extra content with the counter incremented
|
||||
return if (hasHotp()) {
|
||||
extraContent.replaceFirst("counter=[0-9]+".toRegex(), "counter=" + (hotpCounter!!).toString())
|
||||
} else extraContent
|
||||
return if (passContent.size > 1) passContent[1] else ""
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -636,16 +636,12 @@ class PasswordStore : AppCompatActivity() {
|
|||
when (requestCode) {
|
||||
// if we get here with a RESULT_OK then it's probably OK :)
|
||||
BaseGitActivity.REQUEST_CLONE -> settings.edit { putBoolean("repository_initialized", true) }
|
||||
// if went from decrypt->edit and user saved changes or HOTP counter was
|
||||
// incremented, we need to commitChange
|
||||
// if went from decrypt->edit and user saved changes, we need to commitChange
|
||||
REQUEST_CODE_DECRYPT_AND_VERIFY -> {
|
||||
if (data != null && data.getBooleanExtra("needCommit", false)) {
|
||||
if (data.getStringExtra("OPERATION") == "EDIT") {
|
||||
commitChange(resources.getString(R.string.git_commit_edit_text,
|
||||
data.extras!!.getString("LONG_NAME")))
|
||||
} else {
|
||||
commitChange(resources.getString(R.string.git_commit_increment_text,
|
||||
data.extras!!.getString("LONG_NAME")))
|
||||
}
|
||||
}
|
||||
refreshPasswordList()
|
||||
|
|
|
@ -91,7 +91,6 @@ class UserPreference : AppCompatActivity() {
|
|||
val sshKeyPreference = findPreference<Preference>("ssh_key")
|
||||
val sshKeygenPreference = findPreference<Preference>("ssh_keygen")
|
||||
clearSavedPassPreference = findPreference("clear_saved_pass")
|
||||
val clearHotpIncrementPreference = findPreference<Preference>("hotp_remember_clear_choice")
|
||||
val viewSshKeyPreference = findPreference<Preference>("ssh_see_key")
|
||||
val deleteRepoPreference = findPreference<Preference>("git_delete_repo")
|
||||
val externalGitRepositoryPreference = findPreference<Preference>("git_external")
|
||||
|
@ -138,7 +137,6 @@ class UserPreference : AppCompatActivity() {
|
|||
selectExternalGitRepositoryPreference?.summary = sharedPreferences.getString("git_external_repo", getString(R.string.no_repo_selected))
|
||||
viewSshKeyPreference?.isVisible = sharedPreferences.getBoolean("use_generated_key", false)
|
||||
deleteRepoPreference?.isVisible = !sharedPreferences.getBoolean("git_external", false)
|
||||
clearHotpIncrementPreference?.isVisible = sharedPreferences.getBoolean("hotp_remember_check", false)
|
||||
clearClipboard20xPreference?.isVisible = sharedPreferences.getString("general_show_time", "45")?.toInt() != 0
|
||||
val selectedKeys = (sharedPreferences.getStringSet("openpgp_key_ids_set", null)
|
||||
?: HashSet()).toTypedArray()
|
||||
|
@ -197,12 +195,6 @@ class UserPreference : AppCompatActivity() {
|
|||
true
|
||||
}
|
||||
|
||||
clearHotpIncrementPreference?.onPreferenceClickListener = ClickListener {
|
||||
sharedPreferences.edit { putBoolean("hotp_remember_check", false) }
|
||||
it.isVisible = false
|
||||
true
|
||||
}
|
||||
|
||||
openkeystoreIdPreference?.onPreferenceClickListener = ClickListener {
|
||||
sharedPreferences.edit { putString("ssh_openkeystore_keyid", null) }
|
||||
it.isVisible = false
|
||||
|
|
|
@ -21,14 +21,12 @@ import android.text.InputType
|
|||
import android.text.TextUtils
|
||||
import android.text.format.DateUtils
|
||||
import android.text.method.PasswordTransformationMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.Button
|
||||
import android.widget.CheckBox
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
@ -41,7 +39,6 @@ import androidx.preference.PreferenceManager
|
|||
import com.github.ajalt.timberkt.Timber.e
|
||||
import com.github.ajalt.timberkt.Timber.i
|
||||
import com.github.ajalt.timberkt.Timber.tag
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.zeapo.pwdstore.ClipboardService
|
||||
import com.zeapo.pwdstore.PasswordEntry
|
||||
|
@ -51,15 +48,11 @@ import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences
|
|||
import com.zeapo.pwdstore.autofill.oreo.DirectoryStructure
|
||||
import com.zeapo.pwdstore.ui.dialogs.PasswordGeneratorDialogFragment
|
||||
import com.zeapo.pwdstore.ui.dialogs.XkPasswordGeneratorDialogFragment
|
||||
import com.zeapo.pwdstore.utils.Otp
|
||||
import kotlinx.android.synthetic.main.decrypt_layout.crypto_container_decrypt
|
||||
import kotlinx.android.synthetic.main.decrypt_layout.crypto_copy_otp
|
||||
import kotlinx.android.synthetic.main.decrypt_layout.crypto_copy_username
|
||||
import kotlinx.android.synthetic.main.decrypt_layout.crypto_extra_show
|
||||
import kotlinx.android.synthetic.main.decrypt_layout.crypto_extra_show_layout
|
||||
import kotlinx.android.synthetic.main.decrypt_layout.crypto_extra_toggle_show
|
||||
import kotlinx.android.synthetic.main.decrypt_layout.crypto_otp_show
|
||||
import kotlinx.android.synthetic.main.decrypt_layout.crypto_otp_show_label
|
||||
import kotlinx.android.synthetic.main.decrypt_layout.crypto_password_category_decrypt
|
||||
import kotlinx.android.synthetic.main.decrypt_layout.crypto_password_file
|
||||
import kotlinx.android.synthetic.main.decrypt_layout.crypto_password_last_changed
|
||||
|
@ -93,7 +86,6 @@ import java.io.ByteArrayInputStream
|
|||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.nio.charset.Charset
|
||||
import java.util.Date
|
||||
|
||||
class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
||||
private val clipboard by lazy { getSystemService<ClipboardManager>() }
|
||||
|
@ -284,7 +276,6 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
checkAndIncrementHotp()
|
||||
super.onDestroy()
|
||||
mServiceConnection?.unbindFromService()
|
||||
}
|
||||
|
@ -304,23 +295,12 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
if (passwordEntry?.hotpIsIncremented() == false) {
|
||||
setResult(RESULT_CANCELED)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
R.id.crypto_cancel_add, android.R.id.home -> 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_confirm_add_and_copy -> encrypt(true)
|
||||
R.id.crypto_cancel_add -> {
|
||||
if (passwordEntry?.hotpIsIncremented() == false) {
|
||||
setResult(RESULT_CANCELED)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
|
@ -463,84 +443,6 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||
}
|
||||
}
|
||||
|
||||
if (entry.hasTotp() || entry.hasHotp()) {
|
||||
crypto_extra_show_layout.visibility = View.VISIBLE
|
||||
crypto_extra_show.typeface = monoTypeface
|
||||
crypto_extra_show.text = entry.extraContent
|
||||
|
||||
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 * entry.totpPeriod),
|
||||
entry.totpAlgorithm,
|
||||
entry.digits)
|
||||
)
|
||||
}
|
||||
crypto_otp_show.text =
|
||||
Otp.calculateCode(
|
||||
entry.totpSecret,
|
||||
Date().time / (1000 * entry.totpPeriod),
|
||||
entry.totpAlgorithm,
|
||||
entry.digits)
|
||||
} 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@PgpActivity)
|
||||
val checkLayout = checkInflater.inflate(R.layout.otp_confirm_layout, null)
|
||||
val rememberCheck: CheckBox =
|
||||
checkLayout.findViewById(R.id.hotp_remember_checkbox)
|
||||
val dialogBuilder = MaterialAlertDialogBuilder(this@PgpActivity)
|
||||
dialogBuilder.setView(checkLayout)
|
||||
dialogBuilder.setMessage(R.string.dialog_update_body)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.dialog_update_positive) { _, _ ->
|
||||
run {
|
||||
calculateAndCommitHotp(entry)
|
||||
if (rememberCheck.isChecked) {
|
||||
settings.edit {
|
||||
putBoolean("hotp_remember_check", true)
|
||||
putBoolean("hotp_remember_choice", true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.dialog_update_negative) { _, _ ->
|
||||
run {
|
||||
calculateHotp(entry)
|
||||
settings.edit {
|
||||
putBoolean("hotp_remember_check", true)
|
||||
putBoolean("hotp_remember_choice", false)
|
||||
}
|
||||
}
|
||||
}
|
||||
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)) {
|
||||
copyPasswordToClipBoard()
|
||||
}
|
||||
|
@ -559,12 +461,9 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||
* Encrypts the password and the extra content
|
||||
*/
|
||||
private fun encrypt(copy: Boolean = false) {
|
||||
// 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 (editName?.isEmpty() == true) {
|
||||
showSnackbar(resources.getString(R.string.file_toast_text))
|
||||
|
@ -687,43 +586,6 @@ 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, "sha1", entry.digits))
|
||||
crypto_otp_show.text = Otp.calculateCode(entry.hotpSecret, entry.hotpCounter + 1, "sha1", entry.digits)
|
||||
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
|
||||
*/
|
||||
|
@ -861,13 +723,6 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||
showSnackbar(resources.getString(R.string.clipboard_username_toast_text))
|
||||
}
|
||||
|
||||
private fun copyOtpToClipBoard(code: String) {
|
||||
val clipboard = clipboard ?: return
|
||||
val clip = ClipData.newPlainText("pgp_handler_result_pm", code)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
showSnackbar(resources.getString(R.string.clipboard_otp_toast_text))
|
||||
}
|
||||
|
||||
private fun shareAsPlaintext() {
|
||||
if (findViewById<View>(R.id.share_password_as_plaintext) == null)
|
||||
return
|
||||
|
@ -957,13 +812,8 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||
|
||||
fun doOnPostExecute() {
|
||||
if (skip) return
|
||||
checkAndIncrementHotp()
|
||||
|
||||
if (crypto_password_show != null) {
|
||||
// clear password; if decrypt changed to encrypt layout via edit button, no need
|
||||
if (passwordEntry?.hotpIsIncremented() == false) {
|
||||
setResult(RESULT_CANCELED)
|
||||
}
|
||||
passwordEntry = null
|
||||
crypto_password_show.text = ""
|
||||
crypto_extra_show.text = ""
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
package com.zeapo.pwdstore.utils;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class Otp {
|
||||
|
||||
private static final Base32 BASE_32 = new Base32();
|
||||
|
||||
private Otp() {
|
||||
}
|
||||
|
||||
public static String calculateCode(
|
||||
String secret, long counter, String algorithm, String digits) {
|
||||
String[] steam = {
|
||||
"2", "3", "4", "5", "6", "7", "8", "9", "B", "C", "D", "F", "G", "H", "J", "K", "M",
|
||||
"N", "P", "Q", "R", "T", "V", "W", "X", "Y"
|
||||
};
|
||||
String ALGORITHM = "Hmac" + algorithm.toUpperCase();
|
||||
SecretKeySpec signingKey = new SecretKeySpec(BASE_32.decode(secret), ALGORITHM);
|
||||
|
||||
Mac mac;
|
||||
try {
|
||||
mac = Mac.getInstance(ALGORITHM);
|
||||
mac.init(signingKey);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Timber.tag("TOTP").e(e, "%s unavailable - should never happen", ALGORITHM);
|
||||
return null;
|
||||
} catch (InvalidKeyException e) {
|
||||
Timber.tag("TOTP").e(e, "Key is malformed");
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] digest = mac.doFinal(ByteBuffer.allocate(8).putLong(counter).array());
|
||||
int offset = digest[digest.length - 1] & 0xf;
|
||||
byte[] code = Arrays.copyOfRange(digest, offset, offset + 4);
|
||||
code[0] = (byte) (0x7f & code[0]);
|
||||
String strCode = new BigInteger(code).toString();
|
||||
if (digits.equals("s")) {
|
||||
StringBuilder output = new StringBuilder();
|
||||
int bigInt = new BigInteger(code).intValue();
|
||||
for (int i = 0; i != 5; i++) {
|
||||
output.append(steam[bigInt % 26]);
|
||||
bigInt /= 26;
|
||||
}
|
||||
return output.toString();
|
||||
} else return strCode.substring(strCode.length() - Integer.parseInt(digits));
|
||||
}
|
||||
}
|
|
@ -160,56 +160,17 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/crypto_username_show_label"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/crypto_copy_otp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/crypto_username_show"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:background="?android:attr/windowBackground"
|
||||
android:contentDescription="@string/copy_otp"
|
||||
android:src="@drawable/ic_content_copy"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/crypto_otp_show_label"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/crypto_otp_show_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/crypto_username_show"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_toStartOf="@id/crypto_copy_otp"
|
||||
android:text="@string/otp"
|
||||
android:textColor="?android:attr/textColor"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/crypto_username_show" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/crypto_otp_show"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/crypto_otp_show_label"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_toStartOf="@id/crypto_copy_otp"
|
||||
android:textColor="?android:attr/textColor"
|
||||
android:textIsSelectable="true"
|
||||
android:typeface="monospace"
|
||||
app:layout_constraintTop_toBottomOf="@id/crypto_otp_show_label" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/crypto_extra_show_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/crypto_otp_show"
|
||||
android:layout_below="@id/crypto_username_show"
|
||||
android:layout_alignParentStart="true"
|
||||
android:text="@string/extra_content"
|
||||
android:textColor="?android:attr/textColor"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/crypto_otp_show" />
|
||||
app:layout_constraintTop_toBottomOf="@id/crypto_username_show" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/crypto_extra_show"
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
||||
~ SPDX-License-Identifier: GPL-3.0-only
|
||||
-->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
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_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/dialog_update_check"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -63,7 +63,6 @@
|
|||
<string name="edit_password">تعديل كلمة السر</string>
|
||||
<string name="copy_password">نسخ كلمة السر</string>
|
||||
<string name="copy_username">نسخ إسم المستخدم</string>
|
||||
<string name="copy_otp">نسخ رمز الـ OTP</string>
|
||||
<string name="share_as_plaintext">شارك كنص مجرد</string>
|
||||
<string name="last_changed">آخِر تعديل %s</string>
|
||||
<!-- Preferences -->
|
||||
|
|
|
@ -34,7 +34,6 @@
|
|||
<string name="clipboard_password_toast_text">Heslo zkopírováno do schránky, máte %d sekund na jeho zkopírování.</string>
|
||||
<string name="clipboard_password_no_clear_toast_text">Heslo zkopírováno do schránky</string>
|
||||
<string name="clipboard_username_toast_text">Jméno zkopírováno do schránky</string>
|
||||
<string name="clipboard_otp_toast_text">OTP kód zkopírován do schránky</string>
|
||||
<string name="file_toast_text">Zadejte prosím jméno souboru</string>
|
||||
<string name="path_toast_text">Prosím zadejte cestu k souboru</string>
|
||||
<string name="empty_toast_text">Nelze zadat prázdné heslo nebo další obsah</string>
|
||||
|
@ -102,7 +101,6 @@
|
|||
<string name="edit_password">Editovat heslo</string>
|
||||
<string name="copy_password">Kopírovat heslo</string>
|
||||
<string name="copy_username">Kopírovat jméno</string>
|
||||
<string name="copy_otp">Kopírovat OTP kód</string>
|
||||
<string name="share_as_plaintext">Sdílet v nešifrované podobě</string>
|
||||
<string name="last_changed">Naposled změněno %s</string>
|
||||
<!-- Preferences -->
|
||||
|
|
|
@ -24,14 +24,12 @@
|
|||
<string name="git_commit_edit_text">"%1$s editada usando Android Password Store."</string>
|
||||
<string name="git_commit_remove_text">"%1$s eliminada del almacén usando Android Password Store."</string>
|
||||
<string name="git_commit_move_text">"Renombrado %1$s a %2$s usando Android Password Store.."</string>
|
||||
<string name="git_commit_increment_text">"Contador de HOTP incrementado para %1$s."</string>
|
||||
|
||||
<!-- PGPHandler -->
|
||||
<string name="provider_toast_text">¡No se ha seleccionado ningún proveedor OpenGPG!</string>
|
||||
<string name="clipboard_password_toast_text">Contraseña copiada al portapapeles, tienes %d segundos para pegarla.</string>
|
||||
<string name="clipboard_password_no_clear_toast_text">Contraseña copiada al portapapeles</string>
|
||||
<string name="clipboard_username_toast_text">Nombre de usuario copiado al portapapeles</string>
|
||||
<string name="clipboard_otp_toast_text">Código OTP copiado al portapapeles</string>
|
||||
<string name="file_toast_text">Por favor selecciona un nombre de archivo</string>
|
||||
<string name="empty_toast_text">No puedes dejar la contraseña y el contenido extra ambos vacíos</string>
|
||||
|
||||
|
@ -95,14 +93,8 @@
|
|||
<string name="edit_password">Editar contraseña</string>
|
||||
<string name="copy_password">Copiar contraseña</string>
|
||||
<string name="copy_username">Copiar nombre de usuario</string>
|
||||
<string name="copy_otp">Copiar código OTP</string>
|
||||
<string name="share_as_plaintext">Compartir como texto plano</string>
|
||||
<string name="last_changed">Última modificación %s</string>
|
||||
<string name="dialog_update_title">Atención</string>
|
||||
<string name="dialog_update_positive">Actualizar registro</string>
|
||||
<string name="dialog_update_negative">Dejar sin cambios</string>
|
||||
<string name="dialog_update_body">El contador HOTP será incrementado. Este cambio será confirmado. Si presionas "Dejar sin cambios", el código HOTP será mostrado, pero el contador no se cambiará.</string>
|
||||
<string name="dialog_update_check">Recordar elección</string>
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="pref_edit_server_info">Editar ajustes del servidor Git</string>
|
||||
|
@ -216,12 +208,10 @@
|
|||
<string name="git_push_nff_error">La subida fue rechazada por el servidor, Ejecuta \'Descargar desde servidor\' antes de subir o pulsa \'Sincronizar con servidor\' para realizar ambas acciones.</string>
|
||||
<string name="git_push_generic_error">El envío fue rechazado por el servidor, la razón:</string>
|
||||
<string name="jgit_error_push_dialog_text">Ocurrió un error durante el envío:</string>
|
||||
<string name="hotp_remember_clear_choice">Limpiar preferencia para incremento HOTP</string>
|
||||
<string name="remember_the_passphrase">Recordar contraseñagit (inseguro)</string>
|
||||
<string name="hackish_tools">Hackish tools</string>
|
||||
<string name="abort_rebase">Abortar rebase</string>
|
||||
<string name="commit_hash">Hash del commit</string>
|
||||
<string name="crypto_extra_edit_hint">Username: Nombre de usuario\n… o algún contenido extra</string>
|
||||
<string name="get_last_changed_failed">Error al obtener la fecha de último cambio</string>
|
||||
<string name="hotp_pending">Presiona copiar para calcular el HOTP</string>
|
||||
</resources>
|
||||
|
|
|
@ -31,13 +31,11 @@
|
|||
<string name="git_commit_edit_text">Editer %1$s depuis le dépôt.</string>
|
||||
<string name="git_commit_remove_text">Retirer %1$s de la mémoire.</string>
|
||||
<string name="git_commit_move_text">Renommer %1$sà %2$s. </string>
|
||||
<string name="git_commit_increment_text">Incrémenter le compteur HOTP pour %1$s.</string>
|
||||
|
||||
<!-- PGPHandler -->
|
||||
<string name="provider_toast_text">Aucun prestataire OpenPGP sélectionné !</string>
|
||||
<string name="clipboard_password_toast_text">Mot de passe copié dans le presse papier, vous avez %d secondes pour coller celui-ci.</string>
|
||||
<string name="clipboard_username_toast_text">Nom d\'utilisateur copié</string>
|
||||
<string name="clipboard_otp_toast_text">Code OTP copié dans le presse-papier</string>
|
||||
<string name="file_toast_text">Renseignez un nom de fichier</string>
|
||||
<string name="empty_toast_text">Vous ne pouvez pas utiliser un mot de passe vide ou des données supplémentaires vide</string>
|
||||
|
||||
|
@ -103,14 +101,8 @@
|
|||
<string name="edit_password">Éditer le mot de passe</string>
|
||||
<string name="copy_password">Copier le mot de passe</string>
|
||||
<string name="copy_username">Copier le nom d\'utilisateur</string>
|
||||
<string name="copy_otp">Copier le code OTP</string>
|
||||
<string name="share_as_plaintext">Partager en clair</string>
|
||||
<string name="last_changed">Dernière modification le %s</string>
|
||||
<string name="dialog_update_title">Attention</string>
|
||||
<string name="dialog_update_positive">Mettre à jour l\'entrée</string>
|
||||
<string name="dialog_update_negative">Laisser inchangé</string>
|
||||
<string name="dialog_update_body">Le compteur HOTP est sur le point d’être incrémenté. Ce changement sera engagé. Si vous appuyez sur \"Laisser inchangé\", le code HOTP sera affiché mais le compteur ne sera pas modifié</string>
|
||||
<string name="dialog_update_check">Se souvenir de mon choix</string>
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="pref_edit_server_info">Editer les paramètres du serveur Git</string>
|
||||
|
@ -217,11 +209,9 @@
|
|||
<string name="git_push_generic_error">Poussée rejetée par le dépôt distant, raison:</string>
|
||||
<string name="git_push_other_error">Pousser au dépôt distant sans avance rapide rejetée. Vérifiez la variable receive.denyNonFastForwards dans le fichier de configuration du répertoire de destination.</string>
|
||||
<string name="jgit_error_push_dialog_text">Une erreur s\'est produite lors de l\'opération de poussée:</string>
|
||||
<string name="hotp_remember_clear_choice">Effacer les préférences enregistrées pour l’incrémentation HOTP</string>
|
||||
<string name="remember_the_passphrase">Se rappeler de la phrase secrète dans la configuration de l\'application (peu sûr)</string>
|
||||
<string name="hackish_tools">Outils de hack</string>
|
||||
<string name="commit_hash">Commettre la clé</string>
|
||||
<string name="crypto_extra_edit_hint">nom d\'utilisateur: quelque chose d\'autre contenu supplémentaire</string>
|
||||
<string name="get_last_changed_failed">Failed to get last changed date</string>
|
||||
<string name="hotp_pending">Appuyez sur copie pour calculer le HOTP</string>
|
||||
</resources>
|
||||
|
|
|
@ -31,14 +31,12 @@
|
|||
<string name="git_commit_edit_text">Отредактирован %1$s из хранилища.</string>
|
||||
<string name="git_commit_remove_text">Удалить %1$sиз хранилища.</string>
|
||||
<string name="git_commit_move_text">Переименовать %1$sв%2$s.</string>
|
||||
<string name="git_commit_increment_text">Увеличить значение HOTP счетчика для %1$s</string>
|
||||
|
||||
<!-- PGPHandler -->
|
||||
<string name="provider_toast_text">Не выбран провайдер OpenPGP!</string>
|
||||
<string name="clipboard_password_toast_text">Пароль скопирован в буфер обмена, у вас есть %d секунд чтобы вставить его.</string>
|
||||
<string name="clipboard_password_no_clear_toast_text">Пароль скопирован в буфер обмена</string>
|
||||
<string name="clipboard_username_toast_text">Имя пользователя скопировано в буфер обмена</string>
|
||||
<string name="clipboard_otp_toast_text">OTP код скопирован в буфер обмена</string>
|
||||
<string name="file_toast_text">Пожалуйста, укажите имя файла</string>
|
||||
<string name="path_toast_text">Пожалуйста, задайте путь к файлу</string>
|
||||
<string name="empty_toast_text">Вы не можете использовать пустой пароль или пустое поле информации</string>
|
||||
|
@ -108,14 +106,8 @@
|
|||
<string name="edit_password">Редактировать пароль</string>
|
||||
<string name="copy_password">Скопировать пароль</string>
|
||||
<string name="copy_username">Скопировать имя пользователя</string>
|
||||
<string name="copy_otp">Скопировать OTP код</string>
|
||||
<string name="share_as_plaintext">Поделиться в открытом виде</string>
|
||||
<string name="last_changed">Последние изменение %s</string>
|
||||
<string name="dialog_update_title">Внимание</string>
|
||||
<string name="dialog_update_positive">Обновить запись</string>
|
||||
<string name="dialog_update_negative">Оставить без изменений</string>
|
||||
<string name="dialog_update_body">HOTP счетчик будет увеличен. Это изменение будет сохранено. Если вы нажмете \"Оставить без изменений\", тогда HOTP код будет показан, но счетчик не изменится.</string>
|
||||
<string name="dialog_update_check">Запомнить мой выбор</string>
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="pref_edit_server_info">Изменить настройки сервера git</string>
|
||||
|
@ -278,7 +270,6 @@
|
|||
<string name="git_push_generic_error">Запись изменений была отклонена удаленным репозиторием, причина:</string>
|
||||
<string name="git_push_other_error">Удаленный репозиторий отклонил запись изменений без быстрой перемотки вперед. Проверьте переменную receive.denyNonFastForwards в файле конфигурации репозитория назначения.</string>
|
||||
<string name="jgit_error_push_dialog_text">В хоте операции записи изменений возникла ошибка:</string>
|
||||
<string name="hotp_remember_clear_choice">Очистить сохраненные настройки для увеличения HOTP</string>
|
||||
<string name="remember_the_passphrase">Заполнить парольную фразу в конфигурации приложнеия (небезопасно)</string>
|
||||
<string name="hackish_tools">Костыльные инструменты</string>
|
||||
<string name="abort_rebase">Прервать перебазирование и записать изменения в новую ветку</string>
|
||||
|
@ -286,7 +277,6 @@
|
|||
<string name="commit_hash">Хэш-сумма изменений</string>
|
||||
<string name="crypto_extra_edit_hint">имя пользователя: какой-то другой дополнительный контент</string>
|
||||
<string name="get_last_changed_failed">Failed to get last changed date</string>
|
||||
<string name="hotp_pending">Нажмите скопировать чтобы расчитать HOTP</string>
|
||||
<string name="openkeychain_ssh_api_connect_fail">Ошибка при подключении к сервису OpenKeychain SSH API</string>
|
||||
<string name="no_ssh_api_provider">Не найдено SSH API провайдеров. OpenKeychain установлен?</string>
|
||||
<string name="ssh_api_pending_intent_failed">Ожидаемое намерение SSH API не удалось</string>
|
||||
|
|
|
@ -42,14 +42,12 @@
|
|||
<string name="git_commit_edit_text">Edit password for %1$s using android password store.</string>
|
||||
<string name="git_commit_remove_text">Remove %1$s from store.</string>
|
||||
<string name="git_commit_move_text">Rename %1$s to %2$s.</string>
|
||||
<string name="git_commit_increment_text">Increment HOTP counter for %1$s.</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_password_no_clear_toast_text">Password copied to clipboard</string>
|
||||
<string name="clipboard_username_toast_text">Username 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="path_toast_text">Please provide a file path</string>
|
||||
<string name="empty_toast_text">You cannot use an empty password or empty extra content</string>
|
||||
|
@ -121,18 +119,11 @@
|
|||
<string name="password">Password:</string>
|
||||
<string name="extra_content">Extra content:</string>
|
||||
<string name="username">Username:</string>
|
||||
<string name="otp" translatable="false">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_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_repository_title">Repository</string>
|
||||
|
@ -316,7 +307,6 @@
|
|||
<string name="jgit_error_push_dialog_text">Error occurred during the push operation:</string>
|
||||
<string name="clear_saved_passphrase_ssh">Clear saved passphrase for local SSH key</string>
|
||||
<string name="clear_saved_passphrase_https">Clear saved HTTPS password</string>
|
||||
<string name="hotp_remember_clear_choice">Clear saved preference for HOTP incrementing</string>
|
||||
<string name="remember_the_passphrase">Remember key passphrase</string>
|
||||
<string name="hackish_tools">Hackish tools</string>
|
||||
<string name="abort_rebase">Abort rebase and push new branch</string>
|
||||
|
@ -324,7 +314,6 @@
|
|||
<string name="commit_hash">Commit hash</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>
|
||||
<string name="openkeychain_ssh_api_connect_fail">Failed to connect to OpenKeychain SSH API service.</string>
|
||||
<string name="no_ssh_api_provider">No SSH API provider found. Is OpenKeychain installed?</string>
|
||||
<string name="ssh_api_pending_intent_failed">SSH API pending intent failed</string>
|
||||
|
|
|
@ -54,9 +54,6 @@
|
|||
app:key="ssh_keygen"
|
||||
app:title="@string/pref_ssh_keygen_title" />
|
||||
<Preference app:key="clear_saved_pass" />
|
||||
<Preference
|
||||
app:key="hotp_remember_clear_choice"
|
||||
app:title="@string/hotp_remember_clear_choice" />
|
||||
<Preference
|
||||
app:key="ssh_openkeystore_clear_keyid"
|
||||
app:title="@string/ssh_openkeystore_clear_keyid" />
|
||||
|
|
|
@ -60,45 +60,4 @@ class PasswordEntryTest {
|
|||
assertFalse(PasswordEntry("\n").hasUsername())
|
||||
assertFalse(PasswordEntry("").hasUsername())
|
||||
}
|
||||
|
||||
@Test fun testNoTotpUriPresent() {
|
||||
val entry = PasswordEntry("secret\nextra\nlogin: username\ncontent")
|
||||
assertFalse(entry.hasTotp())
|
||||
assertNull(entry.totpSecret)
|
||||
}
|
||||
|
||||
@Test fun testTotpUriInPassword() {
|
||||
val entry = PasswordEntry("otpauth://totp/test?secret=JBSWY3DPEHPK3PXP")
|
||||
assertTrue(entry.hasTotp())
|
||||
assertEquals("JBSWY3DPEHPK3PXP", entry.totpSecret)
|
||||
}
|
||||
|
||||
@Test fun testTotpUriInContent() {
|
||||
val entry = PasswordEntry(
|
||||
"secret\nusername: test\notpauth://totp/test?secret=JBSWY3DPEHPK3PXP")
|
||||
assertTrue(entry.hasTotp())
|
||||
assertEquals("JBSWY3DPEHPK3PXP", entry.totpSecret)
|
||||
}
|
||||
|
||||
@Test fun testNoHotpUriPresent() {
|
||||
val entry = PasswordEntry("secret\nextra\nlogin: username\ncontent")
|
||||
assertFalse(entry.hasHotp())
|
||||
assertNull(entry.hotpSecret)
|
||||
assertNull(entry.hotpCounter)
|
||||
}
|
||||
|
||||
@Test fun testHotpUriInPassword() {
|
||||
val entry = PasswordEntry("otpauth://hotp/test?secret=JBSWY3DPEHPK3PXP&counter=25")
|
||||
assertTrue(entry.hasHotp())
|
||||
assertEquals("JBSWY3DPEHPK3PXP", entry.hotpSecret)
|
||||
assertEquals(25, entry.hotpCounter)
|
||||
}
|
||||
|
||||
@Test fun testHotpUriInContent() {
|
||||
val entry = PasswordEntry(
|
||||
"secret\nusername: test\notpauth://hotp/test?secret=JBSWY3DPEHPK3PXP&counter=25")
|
||||
assertTrue(entry.hasHotp())
|
||||
assertEquals("JBSWY3DPEHPK3PXP", entry.hotpSecret)
|
||||
assertEquals(25, entry.hotpCounter)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue