OTP errors implementation

This commit is contained in:
J-Jamet
2019-11-09 15:19:14 +01:00
parent 23b21ea154
commit 7f5406ac98
7 changed files with 209 additions and 102 deletions

View File

@@ -6,12 +6,10 @@ import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.Spinner
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.OtpModel
@@ -28,11 +26,13 @@ class SetOTPDialogFragment : DialogFragment() {
private var otpTypeSpinner: Spinner? = null
private var otpTokenTypeSpinner: Spinner? = null
private var otpSecretContainer: TextInputLayout? = null
private var otpSecretTextView: EditText? = null
private var otpPeriodContainer: View? = null
private var otpPeriodContainer: TextInputLayout? = null
private var otpPeriodTextView: EditText? = null
private var otpCounterContainer: View? = null
private var otpCounterContainer: TextInputLayout? = null
private var otpCounterTextView: EditText? = null
private var otpDigitsContainer: TextInputLayout? = null
private var otpDigitsTextView: EditText? = null
private var otpAlgorithmSpinner: Spinner? = null
@@ -65,12 +65,14 @@ class SetOTPDialogFragment : DialogFragment() {
val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null) as ViewGroup?
otpTypeSpinner = root?.findViewById(R.id.setup_otp_type)
otpTokenTypeSpinner = root?.findViewById(R.id.setup_otp_token_type)
otpSecretContainer = root?.findViewById(R.id.setup_otp_secret_label)
otpSecretTextView = root?.findViewById(R.id.setup_otp_secret)
otpAlgorithmSpinner = root?.findViewById(R.id.setup_otp_algorithm)
otpPeriodContainer= root?.findViewById(R.id.setup_otp_period_title)
otpPeriodContainer= root?.findViewById(R.id.setup_otp_period_label)
otpPeriodTextView = root?.findViewById(R.id.setup_otp_period)
otpCounterContainer= root?.findViewById(R.id.setup_otp_counter_title)
otpCounterContainer= root?.findViewById(R.id.setup_otp_counter_label)
otpCounterTextView = root?.findViewById(R.id.setup_otp_counter)
otpDigitsContainer = root?.findViewById(R.id.setup_otp_digits_label)
otpDigitsTextView = root?.findViewById(R.id.setup_otp_digits)
// HOTP / TOTP Type selection
@@ -116,6 +118,7 @@ class SetOTPDialogFragment : DialogFragment() {
setView(root)
.setPositiveButton(android.R.string.ok) { _, _ ->
createOTPElementListener?.invoke(mOtpElement)
// TODO prevent dismiss if error
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
}
@@ -165,8 +168,13 @@ class SetOTPDialogFragment : DialogFragment() {
// Set secret in OtpElement
otpSecretTextView?.addTextChangedListener(object: TextWatcher {
override fun afterTextChanged(s: Editable?) {
s?.toString()?.let {
mOtpElement.setBase32Secret(it)
s?.toString()?.let { userString ->
try {
mOtpElement.setBase32Secret(userString)
otpSecretContainer?.error = null
} catch (exception: Exception) {
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
}
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
@@ -176,8 +184,14 @@ class SetOTPDialogFragment : DialogFragment() {
// Set counter in OtpElement
otpCounterTextView?.addTextChangedListener(object: TextWatcher {
override fun afterTextChanged(s: Editable?) {
s?.toString()?.toIntOrNull()?.let {
mOtpElement.counter = it
s?.toString()?.toLongOrNull()?.let {
try {
mOtpElement.counter = it
otpCounterContainer?.error = null
} catch (exception: Exception) {
otpCounterContainer?.error = getString(R.string.error_otp_counter,
0, Long.MAX_VALUE)
}
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
@@ -188,7 +202,13 @@ class SetOTPDialogFragment : DialogFragment() {
otpPeriodTextView?.addTextChangedListener(object: TextWatcher {
override fun afterTextChanged(s: Editable?) {
s?.toString()?.toIntOrNull()?.let {
mOtpElement.period = it
try {
mOtpElement.period = it
otpPeriodContainer?.error = null
} catch (exception: Exception) {
otpPeriodContainer?.error = getString(R.string.error_otp_period,
0, 60)
}
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
@@ -199,7 +219,13 @@ class SetOTPDialogFragment : DialogFragment() {
otpDigitsTextView?.addTextChangedListener(object: TextWatcher {
override fun afterTextChanged(s: Editable?) {
s?.toString()?.toIntOrNull()?.let {
mOtpElement.digits = it
try {
mOtpElement.digits = it
otpDigitsContainer?.error = null
} catch (exception: Exception) {
otpDigitsContainer?.error = getString(R.string.error_otp_digits,
6, 10)
}
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

View File

@@ -6,19 +6,19 @@ import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpTokenType
import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.otp.TokenCalculator
import com.kunzisoft.keepass.otp.TokenCalculator.DEFAULT_ALGORITHM
import com.kunzisoft.keepass.otp.TokenCalculator.OTP_DEFAULT_ALGORITHM
class OtpModel() : Parcelable {
var type: OtpType = OtpType.HOTP // ie : HOTP or TOTP
var tokenType: OtpTokenType = OtpTokenType.RFC4226
var name: String = "" // ie : user@email.com
var issuer: String = "" // ie : Gitlab
var secret: ByteArray = ByteArray(0) // Seed
var counter: Int = TokenCalculator.HOTP_INITIAL_COUNTER // ie : 5 - only for HOTP
var type: OtpType = OtpType.TOTP // ie : HOTP or TOTP
var tokenType: OtpTokenType = OtpTokenType.RFC6238
var name: String = "OTP" // ie : user@email.com
var issuer: String = "None" // ie : Gitlab
var secret: ByteArray? = null // Seed
var counter: Long = TokenCalculator.HOTP_INITIAL_COUNTER // ie : 5 - only for HOTP
var period: Int = TokenCalculator.TOTP_DEFAULT_PERIOD // ie : 30 seconds - only for TOTP
var digits: Int = TokenCalculator.OTP_DEFAULT_DIGITS
var algorithm: TokenCalculator.HashAlgorithm = DEFAULT_ALGORITHM
var algorithm: TokenCalculator.HashAlgorithm = OTP_DEFAULT_ALGORITHM
constructor(parcel: Parcel) : this() {
val typeRead = parcel.readInt()
@@ -27,7 +27,7 @@ class OtpModel() : Parcelable {
name = parcel.readString() ?: name
issuer = parcel.readString() ?: issuer
secret = parcel.createByteArray() ?: secret
counter = parcel.readInt()
counter = parcel.readLong()
period = parcel.readInt()
digits = parcel.readInt()
algorithm = TokenCalculator.HashAlgorithm.values()[parcel.readInt()]
@@ -42,7 +42,8 @@ class OtpModel() : Parcelable {
if (type != other.type) return false
// Token type is important only if it's a TOTP
if (type == OtpType.TOTP && tokenType != other.tokenType) return false
if (!secret.contentEquals(other.secret)) return false
if (secret == null || other.secret == null) return false
if (!secret!!.contentEquals(other.secret!!)) return false
// Counter only for HOTP
if (type == OtpType.HOTP && counter != other.counter) return false
// Step only for TOTP
@@ -60,8 +61,10 @@ class OtpModel() : Parcelable {
override fun hashCode(): Int {
var result = type.hashCode()
result = 31 * result + tokenType.hashCode()
result = 31 * result + secret.contentHashCode()
result = 31 * result + counter
result = 31 * result + name.hashCode()
result = 31 * result + issuer.hashCode()
result = 31 * result + (secret?.contentHashCode() ?: 0)
result = 31 * result + counter.hashCode()
result = 31 * result + period
result = 31 * result + digits
result = 31 * result + algorithm.hashCode()
@@ -74,7 +77,7 @@ class OtpModel() : Parcelable {
parcel.writeString(name)
parcel.writeString(issuer)
parcel.writeByteArray(secret)
parcel.writeInt(counter)
parcel.writeLong(counter)
parcel.writeInt(period)
parcel.writeInt(digits)
parcel.writeInt(algorithm.ordinal)

View File

@@ -7,6 +7,7 @@ import org.apache.commons.codec.binary.Base64
import org.apache.commons.codec.binary.Hex
import java.nio.charset.Charset
import java.util.*
import java.util.regex.Pattern
data class OtpElement(var otpModel: OtpModel = OtpModel()) {
@@ -30,17 +31,17 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
otpModel.tokenType = value
when (tokenType) {
OtpTokenType.RFC4226 -> {
otpModel.algorithm = TokenCalculator.DEFAULT_ALGORITHM
otpModel.algorithm = TokenCalculator.OTP_DEFAULT_ALGORITHM
otpModel.digits = TokenCalculator.OTP_DEFAULT_DIGITS
otpModel.counter = TokenCalculator.HOTP_INITIAL_COUNTER
}
OtpTokenType.RFC6238 -> {
otpModel.algorithm = TokenCalculator.DEFAULT_ALGORITHM
otpModel.algorithm = TokenCalculator.OTP_DEFAULT_ALGORITHM
otpModel.digits = TokenCalculator.OTP_DEFAULT_DIGITS
otpModel.period = TokenCalculator.TOTP_DEFAULT_PERIOD
}
OtpTokenType.STEAM -> {
otpModel.algorithm = TokenCalculator.DEFAULT_ALGORITHM
otpModel.algorithm = TokenCalculator.OTP_DEFAULT_ALGORITHM
otpModel.digits = TokenCalculator.STEAM_DEFAULT_DIGITS
otpModel.period = TokenCalculator.TOTP_DEFAULT_PERIOD
}
@@ -61,26 +62,35 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
var secret
get() = otpModel.secret
set(value) {
private set(value) {
otpModel.secret = value
}
var counter
get() = otpModel.counter
set(value) {
otpModel.counter = if (value < 0) TokenCalculator.HOTP_INITIAL_COUNTER else value
otpModel.counter = if (value < 0 && value > Long.MAX_VALUE) {
TokenCalculator.HOTP_INITIAL_COUNTER
throw NumberFormatException()
} else value
}
var period
get() = otpModel.period
set(value) {
otpModel.period = if (value <= 0 || value > 60) TokenCalculator.TOTP_DEFAULT_PERIOD else value
otpModel.period = if (value <= 0 || value > 60) {
TokenCalculator.TOTP_DEFAULT_PERIOD
throw NumberFormatException()
} else value
}
var digits
get() = otpModel.digits
set(value) {
otpModel.digits = if (value <= 0) TokenCalculator.OTP_DEFAULT_DIGITS else value
otpModel.digits = if (value <= 0|| value > 10) {
TokenCalculator.OTP_DEFAULT_DIGITS
throw NumberFormatException()
} else value
}
var algorithm
@@ -90,33 +100,45 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
}
fun setUTF8Secret(secret: String) {
otpModel.secret = secret.toByteArray(Charset.forName("UTF-8"))
if (secret.isNotEmpty())
otpModel.secret = secret.toByteArray(Charset.forName("UTF-8"))
else
throw DecoderException()
}
fun setHexSecret(secret: String) {
try {
if (secret.isNotEmpty())
otpModel.secret = Hex.decodeHex(secret)
} catch (e: DecoderException) {
e.printStackTrace()
}
else
throw DecoderException()
}
fun getBase32Secret(): String {
return Base32().encodeAsString(otpModel.secret)
return otpModel.secret?.let {
Base32().encodeAsString(it)
} ?: ""
}
fun setBase32Secret(secret: String) {
otpModel.secret = Base32().decode(secret.toByteArray())
if (secret.isNotEmpty() && checkBase32Secret(secret))
otpModel.secret = Base32().decode(secret.toByteArray())
else
throw DecoderException()
}
fun setBase64Secret(secret: String) {
otpModel.secret = Base64().decode(secret.toByteArray())
if (secret.isNotEmpty() && checkBase64Secret(secret))
otpModel.secret = Base64().decode(secret.toByteArray())
else
throw DecoderException()
}
val token: String
get() {
if (secret == null)
return ""
return when (type) {
OtpType.HOTP -> TokenCalculator.HOTP(secret, counter.toLong(), digits, algorithm)
OtpType.HOTP -> TokenCalculator.HOTP(secret, counter, digits, algorithm)
OtpType.TOTP -> when (tokenType) {
OtpTokenType.STEAM -> TokenCalculator.TOTP_Steam(secret, period, digits, algorithm)
else -> TokenCalculator.TOTP_RFC6238(secret, period, digits, algorithm)
@@ -130,6 +152,16 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
fun shouldRefreshToken(): Boolean {
return secondsRemaining == otpModel.period
}
companion object {
fun checkBase32Secret(secret: String): Boolean {
return (Pattern.matches("^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=)?$", secret))
}
fun checkBase64Secret(secret: String): Boolean {
return (Pattern.matches("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", secret))
}
}
}
enum class OtpType {
@@ -144,10 +176,18 @@ enum class OtpTokenType {
// Proprietary
STEAM; // TOTP Steam
override fun toString(): String {
return when (this) {
STEAM -> "steam"
else -> super.toString()
}
}
companion object {
fun getFromString(tokenType: String): OtpTokenType {
return when (tokenType.toLowerCase(Locale.ENGLISH)) {
"s", "steam" -> STEAM
"hotp" -> RFC4226
else -> RFC6238
}
}

View File

@@ -25,6 +25,7 @@ import android.util.Log
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.otp.TokenCalculator.*
import java.lang.Exception
import java.lang.StringBuilder
import java.net.URLEncoder
import java.util.*
@@ -105,7 +106,7 @@ object OtpEntryFields {
private fun parseOTPUri(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
val otpPlainText = getField(OTP_FIELD)
if (otpPlainText != null && otpPlainText.isNotEmpty()) {
val uri = Uri.parse(otpPlainText)
val uri = Uri.parse(replaceChars(otpPlainText))
if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.toLowerCase(Locale.ENGLISH)) {
Log.e(TAG, "Invalid or missing scheme in uri")
@@ -122,12 +123,11 @@ object OtpEntryFields {
val counterParameter = uri.getQueryParameter(COUNTER_URL_PARAM)
if (counterParameter != null) {
try {
otpElement.counter = Integer.parseInt(counterParameter)
otpElement.counter = counterParameter.toLongOrNull() ?: HOTP_INITIAL_COUNTER
} catch (e: NumberFormatException) {
Log.e(TAG, "Invalid counter in uri")
return false
}
}
} else {
@@ -144,8 +144,13 @@ object OtpEntryFields {
otpElement.issuer = issuerParam
val secretParam = uri.getQueryParameter(SECRET_URL_PARAM)
if (secretParam != null && secretParam.isNotEmpty())
otpElement.setBase32Secret(secretParam)
if (secretParam != null && secretParam.isNotEmpty()) {
try {
otpElement.setBase32Secret(secretParam)
} catch (exception: Exception) {
otpElement.setBase32Secret("")
}
}
val encoderParam = uri.getQueryParameter(ENCODER_URL_PARAM)
if (encoderParam != null && encoderParam.isNotEmpty())
@@ -153,15 +158,27 @@ object OtpEntryFields {
val digitsParam = uri.getQueryParameter(DIGITS_URL_PARAM)
if (digitsParam != null && digitsParam.isNotEmpty())
otpElement.digits = digitsParam.toIntOrNull() ?: OTP_DEFAULT_DIGITS
otpElement.digits = try {
digitsParam.toIntOrNull() ?: OTP_DEFAULT_DIGITS
} catch (exception: Exception) {
OTP_DEFAULT_DIGITS
}
val counterParam = uri.getQueryParameter(COUNTER_URL_PARAM)
if (counterParam != null && counterParam.isNotEmpty())
otpElement.counter = counterParam.toIntOrNull() ?: HOTP_INITIAL_COUNTER
otpElement.counter = try {
counterParam.toLongOrNull() ?: HOTP_INITIAL_COUNTER
} catch (exception: Exception) {
HOTP_INITIAL_COUNTER
}
val stepParam = uri.getQueryParameter(PERIOD_URL_PARAM)
if (stepParam != null && stepParam.isNotEmpty())
otpElement.period = stepParam.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
otpElement.period = try {
stepParam.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
} catch (exception: Exception) {
TOTP_DEFAULT_PERIOD
}
val algorithmParam = uri.getQueryParameter(ALGORITHM_URL_PARAM)
if (algorithmParam != null && algorithmParam.isNotEmpty()) {
@@ -191,18 +208,15 @@ object OtpEntryFields {
}
}
val issuer =
if (otpElement.issuer.isEmpty()) {
if (title == null || title.isEmpty())
"None"
else
URLEncoder.encode(title, "UTF-8")
} else
URLEncoder.encode(otpElement.issuer, "UTF-8")
val accountName =
if (username == null || username.isEmpty())
"OTP"
if (title != null && title.isNotEmpty())
replaceCharsForUrl(title)
else
URLEncoder.encode(username, "UTF-8")
replaceCharsForUrl(otpElement.issuer)
val accountName =
if (username != null && username.isNotEmpty())
replaceCharsForUrl(username)
else
replaceCharsForUrl(otpElement.name)
val uriString = StringBuilder("otpauth://$otpAuthority/$issuer:$accountName" +
"?$SECRET_URL_PARAM=${otpElement.getBase32Secret()}" +
"&$counterOrPeriodLabel=$counterOrPeriodValue" +
@@ -217,22 +231,34 @@ object OtpEntryFields {
return Uri.parse(uriString.toString())
}
private fun replaceCharsForUrl(parameter: String): String {
return URLEncoder.encode(replaceChars(parameter), "UTF-8")
}
private fun replaceChars(parameter: String): String {
return parameter.replace("([\\r|\\n|\\t|\\s|\\u00A0]+)", "")
}
private fun parseTOTPKeyValues(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
val plainText = getField(OTP_FIELD)
if (plainText != null && plainText.isNotEmpty()) {
if (Pattern.matches(validKeyValueRegex, plainText)) {
// KeeOtp string format
val query = breakDownKeyValuePairs(plainText)
try {
// KeeOtp string format
val query = breakDownKeyValuePairs(plainText)
var secretString = query[SEED_KEY]
if (secretString == null)
secretString = ""
otpElement.setBase32Secret(secretString)
otpElement.digits = query[DIGITS_KEY]?.toIntOrNull() ?: OTP_DEFAULT_DIGITS
otpElement.period = query[STEP_KEY]?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
var secretString = query[SEED_KEY]
if (secretString == null)
secretString = ""
otpElement.setBase32Secret(secretString)
otpElement.digits = query[DIGITS_KEY]?.toIntOrNull() ?: OTP_DEFAULT_DIGITS
otpElement.period = query[STEP_KEY]?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
otpElement.type = OtpType.TOTP
return true
otpElement.type = OtpType.TOTP
return true
} catch (exception: Exception) {
return false
}
} else {
// Malformed
return false
@@ -243,21 +269,24 @@ object OtpEntryFields {
private fun parseTOTPFromField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
val seedField = getField(TOTP_SEED_FIELD) ?: return false
otpElement.setBase32Secret(seedField)
try {
otpElement.setBase32Secret(seedField)
val settingsField = getField(TOTP_SETTING_FIELD)
if (settingsField != null) {
// Regex match, sync with shortNameToEncoder
val pattern = Pattern.compile("(\\d+);((?:\\d+)|S)")
val matcher = pattern.matcher(settingsField)
if (!matcher.matches()) {
// malformed
return false
val settingsField = getField(TOTP_SETTING_FIELD)
if (settingsField != null) {
// Regex match, sync with shortNameToEncoder
val pattern = Pattern.compile("(\\d+);((?:\\d+)|S)")
val matcher = pattern.matcher(settingsField)
if (!matcher.matches()) {
// malformed
return false
}
otpElement.period = matcher.group(1).toIntOrNull() ?: TOTP_DEFAULT_PERIOD
otpElement.tokenType = OtpTokenType.getFromString(matcher.group(2))
}
otpElement.period = matcher.group(1).toIntOrNull() ?: TOTP_DEFAULT_PERIOD
otpElement.tokenType = OtpTokenType.getFromString(matcher.group(2))
} catch (exception: Exception) {
return false
}
otpElement.type = OtpType.TOTP
return true
}
@@ -267,17 +296,21 @@ object OtpEntryFields {
val secretHexField = getField(HMACOTP_SECRET_HEX_FIELD)
val secretBase32Field = getField(HMACOTP_SECRET_BASE32_FIELD)
val secretBase64Field = getField(HMACOTP_SECRET_BASE64_FIELD)
when {
secretField != null -> otpElement.setUTF8Secret(secretField)
secretHexField != null -> otpElement.setHexSecret(secretHexField)
secretBase32Field != null -> otpElement.setBase32Secret(secretBase32Field)
secretBase64Field != null -> otpElement.setBase64Secret(secretBase64Field)
else -> return false
}
try {
when {
secretField != null -> otpElement.setUTF8Secret(secretField)
secretHexField != null -> otpElement.setHexSecret(secretHexField)
secretBase32Field != null -> otpElement.setBase32Secret(secretBase32Field)
secretBase64Field != null -> otpElement.setBase64Secret(secretBase64Field)
else -> return false
}
val secretCounterField = getField(HMACOTP_SECRET_COUNTER_FIELD)
if (secretCounterField != null) {
otpElement.counter = secretCounterField.toIntOrNull() ?: HOTP_INITIAL_COUNTER
val secretCounterField = getField(HMACOTP_SECRET_COUNTER_FIELD)
if (secretCounterField != null) {
otpElement.counter = secretCounterField.toLongOrNull() ?: HOTP_INITIAL_COUNTER
}
} catch (exception: Exception) {
return false
}
otpElement.type = OtpType.HOTP

View File

@@ -31,9 +31,10 @@ import javax.crypto.spec.SecretKeySpec;
public class TokenCalculator {
public static final int TOTP_DEFAULT_PERIOD = 30;
public static final int HOTP_INITIAL_COUNTER = 1;
public static final long HOTP_INITIAL_COUNTER = 1;
public static final int OTP_DEFAULT_DIGITS = 6;
public static final int STEAM_DEFAULT_DIGITS = 5;
public static final HashAlgorithm OTP_DEFAULT_ALGORITHM = HashAlgorithm.SHA1;
private static final char[] STEAMCHARS = new char[] {
'2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C',
@@ -49,13 +50,11 @@ public class TokenCalculator {
try {
return valueOf(hash);
} catch (Exception e) {
return SHA1;
return OTP_DEFAULT_ALGORITHM;
}
}
}
public static final HashAlgorithm DEFAULT_ALGORITHM = HashAlgorithm.SHA1;
private static byte[] generateHash(HashAlgorithm algorithm, byte[] key, byte[] data)
throws NoSuchAlgorithmException, InvalidKeyException {
String algo = "Hmac" + algorithm.toString();

View File

@@ -56,7 +56,7 @@
<!-- Secret -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/setup_otp_secret_title"
android:id="@+id/setup_otp_secret_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:passwordToggleEnabled="true"
@@ -106,7 +106,7 @@
android:orientation="horizontal">
<!-- Period / Counter -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/setup_otp_period_title"
android:id="@+id/setup_otp_period_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="visible"
@@ -123,10 +123,11 @@
android:hint="@string/otp_period"
tools:text="30"
android:maxLength="2"
android:digits="0123456789"
tools:targetApi="jelly_bean" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/setup_otp_counter_title"
android:id="@+id/setup_otp_counter_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="gone"
@@ -154,7 +155,7 @@
<!-- Digits -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/setup_otp_digits_title"
android:id="@+id/setup_otp_digits_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
@@ -170,6 +171,7 @@
android:hint="@string/otp_digits"
tools:text="6"
android:maxLength="2"
android:digits="0123456789"
tools:targetApi="jelly_bean" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -121,6 +121,10 @@
<string name="error_copy_entry_here">You can not copy an entry here.</string>
<string name="error_copy_group_here">You can not copy a group here.</string>
<string name="error_create_database_file">Unable to create database with this password and key file.</string>
<string name="error_otp_secret_key">Secret key lust be in Base32 format.</string>
<string name="error_otp_counter">Counter must be between %1$d and %2$d.</string>
<string name="error_otp_period">Period must be between %1$d and %2$d seconds.</string>
<string name="error_otp_digits">Token must contains %1$d to %2$d digits.</string>
<string name="field_name">Field name</string>
<string name="field_value">Field value</string>
<string name="file_not_found_content">Could not find file. Try reopening it from your file browser.</string>