mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
OTP errors implementation
This commit is contained in:
@@ -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) {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user