Change ObjectNameResource and add OTP Dialog

This commit is contained in:
J-Jamet
2019-11-07 15:18:57 +01:00
parent 6e7c0d5073
commit 221f81f51e
20 changed files with 813 additions and 329 deletions

View File

@@ -141,9 +141,9 @@ class EntryActivity : LockingHideActivity() {
mEntry?.let { entry ->
// Init OTP
mOtpElement = OtpEntryFields{ id ->
mOtpElement = OtpEntryFields.parseFields{ id ->
entry.customFields[id]?.toString()
}.otpElement
}
// Fill data in resume to update from EntryEditActivity
fillEntryDataInContentsView(entry)

View File

@@ -29,6 +29,7 @@ import android.view.View
import android.widget.ScrollView
import androidx.appcompat.widget.Toolbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
@@ -304,6 +305,7 @@ class EntryEditActivity : LockingHideActivity(),
val inflater = menuInflater
inflater.inflate(R.menu.database_lock, menu)
MenuUtil.contributionMenuInflater(inflater, menu)
inflater.inflate(R.menu.edit_entry, menu)
entryEditActivityEducation?.let {
Handler().post { performedNextEducation(it) }
@@ -350,6 +352,15 @@ class EntryEditActivity : LockingHideActivity(),
return true
}
R.id.menu_add_otp -> {
SetOTPDialogFragment().apply {
createOTPElementListener = { otpElement ->
// TODO Add custom field
}
}.show(supportFragmentManager, "addOTPDialog")
return true
}
android.R.id.home -> finish()
}

View File

@@ -0,0 +1,186 @@
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.os.Bundle
import android.view.View
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpTokenType
import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.otp.TokenCalculator
class SetOTPDialogFragment : DialogFragment() {
var createOTPElementListener: ((OtpElement) -> Unit)? = null
var mOtpElement: OtpElement = OtpElement()
var otpTypeSpinner: Spinner? = null
var otpTokenTypeSpinner: Spinner? = null
var otpSecretTextView: EditText? = null
var otpPeriodContainer: View? = null
var otpPeriodTextView: EditText? = null
var otpCounterContainer: View? = null
var otpCounterTextView: EditText? = null
var otpDigitsTextView: EditText? = null
var otpAlgorithmSpinner: Spinner? = null
var totpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
var hotpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null)
otpTypeSpinner = root.findViewById(R.id.setup_otp_type)
otpTokenTypeSpinner = root.findViewById(R.id.setup_otp_token_type)
otpSecretTextView = root.findViewById(R.id.setup_otp_secret)
otpPeriodContainer= root.findViewById(R.id.setup_otp_period_title)
otpPeriodTextView = root.findViewById(R.id.setup_otp_period)
otpCounterContainer= root.findViewById(R.id.setup_otp_counter_title)
otpCounterTextView = root.findViewById(R.id.setup_otp_counter)
otpDigitsTextView = root.findViewById(R.id.setup_otp_digits)
otpAlgorithmSpinner = root.findViewById(R.id.setup_otp_algorithm)
context?.let { context ->
// Otp Token type selection
val hotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues()
hotpTokenTypeAdapter = ArrayAdapter(context,
android.R.layout.simple_spinner_item, hotpTokenTypeArray).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
val totpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(BuildConfig.CLOSED_STORE)
totpTokenTypeAdapter = ArrayAdapter(context,
android.R.layout.simple_spinner_item, totpTokenTypeArray).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
otpTokenTypeSpinner?.apply {
adapter = totpTokenTypeAdapter
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) {
when (adapter) {
hotpTokenTypeAdapter -> {
switchTokenType(OtpTokenType.RFC4226)
}
totpTokenTypeAdapter -> {
switchTokenType(OtpTokenType.RFC6238)
}
}
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
(parent?.selectedItem as OtpTokenType?)?.let {
switchTokenType(it)
}
}
}
}
// HOTP / TOTP Type selection
val otpTypeArray = OtpType.values()
otpTypeSpinner?.apply {
adapter = ArrayAdapter<OtpType>(context,
android.R.layout.simple_spinner_item, otpTypeArray).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) {
mOtpElement.type = null
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
(parent?.selectedItem as OtpType?)?.let {
switchType(it)
}
}
}
setSelection(1)
}
// OTP Algorithm
val otpAlgorithmArray = TokenCalculator.HashAlgorithm.values()
otpAlgorithmSpinner?.apply {
adapter = ArrayAdapter<TokenCalculator.HashAlgorithm>(context,
android.R.layout.simple_spinner_item, otpAlgorithmArray).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) {
mOtpElement.algorithm = TokenCalculator.HashAlgorithm.SHA1
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
mOtpElement.algorithm = parent?.selectedItem as TokenCalculator.HashAlgorithm
}
}
}
}
val builder = AlertDialog.Builder(activity)
builder.apply {
setTitle(R.string.entry_setup_otp)
setView(root)
.setPositiveButton(android.R.string.ok) { _, _ ->
// Set secret in OtpElement
otpSecretTextView?.text?.toString()?.let {
mOtpElement.setBase32Secret(it)
}
// Set counter in OtpElement
otpCounterTextView?.text?.toString()?.let {
mOtpElement.counter = it.toInt()
}
// Set period in OtpElement
otpPeriodTextView?.text?.toString()?.let {
mOtpElement.period = it.toInt()
}
createOTPElementListener?.invoke(mOtpElement)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
}
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun switchType(otpType: OtpType) {
mOtpElement.type = otpType
when (otpType) {
OtpType.HOTP -> {
otpPeriodContainer?.visibility = View.GONE
otpCounterContainer?.visibility = View.VISIBLE
otpTokenTypeSpinner?.adapter = hotpTokenTypeAdapter
}
OtpType.TOTP -> {
otpPeriodContainer?.visibility = View.VISIBLE
otpCounterContainer?.visibility = View.GONE
otpTokenTypeSpinner?.adapter = totpTokenTypeAdapter
}
}
upgradeParameters()
}
private fun switchTokenType(otpTokenType: OtpTokenType) {
mOtpElement.tokenType = otpTokenType
upgradeParameters()
}
private fun upgradeParameters() {
otpCounterTextView?.setText(mOtpElement.counter.toString())
otpPeriodTextView?.setText(mOtpElement.period.toString())
otpDigitsTextView?.setText(mOtpElement.digits.toString())
}
}

View File

@@ -19,7 +19,7 @@
*/
package com.kunzisoft.keepass.crypto.keyDerivation
import com.kunzisoft.keepass.database.ObjectNameResource
import com.kunzisoft.keepass.utils.ObjectNameResource
import java.io.IOException
import java.io.Serializable

View File

@@ -324,9 +324,9 @@ class EntryVersioned : NodeVersioned, PwEntryInterface<GroupVersioned> {
Field(entry.key, entry.value))
}
// Add otpElement to generate token
entryInfo.otpElement = OtpEntryFields { key ->
entryInfo.otpModel = OtpEntryFields.parseFields { key ->
customFields[key]?.toString()
}.otpElement
}.otpModel
// Replace parameter fields by generated OTP fields
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
if (!raw)

View File

@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.database.element
import android.content.res.Resources
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.ObjectNameResource
import com.kunzisoft.keepass.utils.ObjectNameResource
// Note: We can get away with using int's to store unsigned 32-bit ints
// since we won't do arithmetic on these values (also unlikely to

View File

@@ -26,7 +26,7 @@ import com.kunzisoft.keepass.crypto.engine.AesEngine
import com.kunzisoft.keepass.crypto.engine.ChaCha20Engine
import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.crypto.engine.TwofishEngine
import com.kunzisoft.keepass.database.ObjectNameResource
import com.kunzisoft.keepass.utils.ObjectNameResource
import java.util.UUID

View File

@@ -3,7 +3,7 @@ package com.kunzisoft.keepass.model
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields.Companion.OTP_TOKEN_FIELD
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
import java.util.*
class EntryInfo : Parcelable {
@@ -15,7 +15,7 @@ class EntryInfo : Parcelable {
var url: String = ""
var notes: String = ""
var customFields: MutableList<Field> = ArrayList()
var otpElement: OtpElement = OtpElement()
var otpModel: OtpModel = OtpModel()
constructor()
@@ -27,7 +27,7 @@ class EntryInfo : Parcelable {
url = parcel.readString() ?: url
notes = parcel.readString() ?: notes
parcel.readList(customFields, Field::class.java.classLoader)
otpElement = parcel.readParcelable(OtpElement::class.java.classLoader) ?: otpElement
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
}
override fun describeContents(): Int {
@@ -42,7 +42,7 @@ class EntryInfo : Parcelable {
parcel.writeString(url)
parcel.writeString(notes)
parcel.writeArray(customFields.toTypedArray())
parcel.writeParcelable(otpElement, flags)
parcel.writeParcelable(otpModel, flags)
}
fun containsCustomFieldsProtected(): Boolean {
@@ -59,7 +59,7 @@ class EntryInfo : Parcelable {
fun doForAutoGeneratedField(field: Field, action: (valueGenerated: String) -> Unit) {
if (field.name == OTP_TOKEN_FIELD)
action.invoke(otpElement.token)
action.invoke(OtpElement(otpModel).token)
}
companion object {

View File

@@ -0,0 +1,95 @@
package com.kunzisoft.keepass.model
import android.os.Parcel
import android.os.Parcelable
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
class OtpModel() : Parcelable {
var type: OtpType? = null // 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 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
constructor(parcel: Parcel) : this() {
val typeRead = parcel.readInt()
type = if (typeRead == -1) null else OtpType.values()[typeRead]
tokenType = OtpTokenType.values()[parcel.readInt()]
name = parcel.readString() ?: name
issuer = parcel.readString() ?: issuer
secret = parcel.createByteArray() ?: secret
counter = parcel.readInt()
period = parcel.readInt()
digits = parcel.readInt()
algorithm = TokenCalculator.HashAlgorithm.values()[parcel.readInt()]
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as OtpElement
if (type != other.type) return false
// Other values only for defined element
if (type != null) {
// 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
// Counter only for HOTP
if (type == OtpType.HOTP && counter != other.counter) return false
// Step only for TOTP
if (type == OtpType.TOTP && period != other.period) return false
if (digits != other.digits) return false
if (algorithm != other.algorithm) return false
}
return true
}
override fun describeContents(): Int {
return 0
}
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 + period
result = 31 * result + digits
result = 31 * result + algorithm.hashCode()
return result
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(type?.ordinal ?: -1)
parcel.writeInt(tokenType.ordinal)
parcel.writeString(name)
parcel.writeString(issuer)
parcel.writeByteArray(secret)
parcel.writeInt(counter)
parcel.writeInt(period)
parcel.writeInt(digits)
parcel.writeInt(algorithm.ordinal)
}
companion object CREATOR : Parcelable.Creator<OtpModel> {
override fun createFromParcel(parcel: Parcel): OtpModel {
return OtpModel(parcel)
}
override fun newArray(size: Int): Array<OtpModel?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -1,133 +1,158 @@
package com.kunzisoft.keepass.otp
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.otp.TokenCalculator.DEFAULT_ALGORITHM
import com.kunzisoft.keepass.model.OtpModel
import org.apache.commons.codec.DecoderException
import org.apache.commons.codec.binary.Base32
import org.apache.commons.codec.binary.Base64
import org.apache.commons.codec.binary.Hex
import java.nio.charset.Charset
import java.util.*
data class OtpElement(var type: OtpType = OtpType.UNDEFINED, // ie : HOTP or TOTP
var tokenType: TokenType = TokenType.Default,
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 step: Int = TokenCalculator.TOTP_DEFAULT_PERIOD, // ie : 30 seconds - only for TOTP
var digits: Int = TokenType.Default.tokenDigits,
var algorithm: TokenCalculator.HashAlgorithm = DEFAULT_ALGORITHM
) : Parcelable {
data class OtpElement(var otpModel: OtpModel = OtpModel()) {
constructor(parcel: Parcel) : this(
OtpType.values()[parcel.readInt()],
TokenType.values()[parcel.readInt()],
parcel.readString() ?: "",
parcel.readString() ?: "",
parcel.createByteArray() ?: ByteArray(0),
parcel.readInt(),
parcel.readInt(),
parcel.readInt(),
TokenCalculator.HashAlgorithm.values()[parcel.readInt()])
var type
get() = otpModel.type
set(value) {
otpModel.type = value
if (type == OtpType.HOTP) {
period = TokenCalculator.TOTP_DEFAULT_PERIOD
if (!OtpTokenType.getHotpTokenTypeValues().contains(tokenType))
tokenType = OtpTokenType.RFC4226
}
if (type == OtpType.TOTP) {
counter = TokenCalculator.HOTP_INITIAL_COUNTER
if (!OtpTokenType.getTotpTokenTypeValues().contains(tokenType))
tokenType = OtpTokenType.RFC6238
}
}
var tokenType
get() = otpModel.tokenType
set(value) {
otpModel.tokenType = value
otpModel.digits = otpModel.tokenType.tokenDigits
}
var name
get() = otpModel.name
set(value) {
otpModel.name = value
}
var issuer
get() = otpModel.issuer
set(value) {
otpModel.issuer = value
}
var secret
get() = otpModel.secret
set(value) {
otpModel.secret = value
}
var counter
get() = otpModel.counter
set(value) {
otpModel.counter = if (value < 0) TokenCalculator.HOTP_INITIAL_COUNTER else value
}
var period
get() = otpModel.period
set(value) {
otpModel.period = if (value <= 0 || value > 60) TokenCalculator.TOTP_DEFAULT_PERIOD else value
}
var digits
get() = otpModel.digits
set(value) {
otpModel.digits = if (value <= 0) OtpTokenType.RFC6238.tokenDigits else value
}
var algorithm
get() = otpModel.algorithm
set(value) {
otpModel.algorithm = value
}
fun setSettings(seed: String, digits: Int, step: Int) {
// TODO: Implement a way to set TOTP from device
}
fun setUTF8Secret(secret: String) {
otpModel.secret = secret.toByteArray(Charset.forName("UTF-8"))
}
fun setHexSecret(secret: String) {
try {
otpModel.secret = Hex.decodeHex(secret)
} catch (e: DecoderException) {
e.printStackTrace()
}
}
fun getBase32Secret(): String {
return Base32().encodeAsString(otpModel.secret)
}
fun setBase32Secret(secret: String) {
otpModel.secret = Base32().decode(secret.toByteArray())
}
fun setBase64Secret(secret: String) {
otpModel.secret = Base64().decode(secret.toByteArray())
}
val token: String
get() {
return when (type) {
if (type == null)
return ""
return when (type!!) {
OtpType.HOTP -> TokenCalculator.HOTP(secret, counter.toLong(), digits, algorithm)
OtpType.TOTP -> when (tokenType) {
TokenType.Steam -> TokenCalculator.TOTP_Steam(secret, this.step, digits, algorithm)
TokenType.Default -> TokenCalculator.TOTP_RFC6238(secret, this.step, digits, algorithm)
OtpTokenType.STEAM -> TokenCalculator.TOTP_Steam(secret, period, digits, algorithm)
else -> TokenCalculator.TOTP_RFC6238(secret, period, digits, algorithm)
}
OtpType.UNDEFINED -> ""
}
}
val secondsRemaining: Int
get() = step - (System.currentTimeMillis() / 1000 % step).toInt()
get() = otpModel.period - (System.currentTimeMillis() / 1000 % otpModel.period).toInt()
fun shouldRefreshToken(): Boolean {
return secondsRemaining == this.step
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as OtpElement
if (type != other.type) return false
// Other values only for defined element
if (type != OtpType.UNDEFINED) {
// 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
// Counter only for HOTP
if (type == OtpType.HOTP && counter != other.counter) return false
// Step only for TOTP
if (type == OtpType.TOTP && step != other.step) return false
if (digits != other.digits) return false
if (algorithm != other.algorithm) return false
}
return true
}
override fun describeContents(): Int {
return 0
}
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 + step
result = 31 * result + digits
result = 31 * result + algorithm.hashCode()
return result
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(type.ordinal)
parcel.writeInt(tokenType.ordinal)
parcel.writeString(name)
parcel.writeString(issuer)
parcel.writeByteArray(secret)
parcel.writeInt(counter)
parcel.writeInt(step)
parcel.writeInt(digits)
parcel.writeInt(algorithm.ordinal)
}
companion object CREATOR : Parcelable.Creator<OtpElement> {
override fun createFromParcel(parcel: Parcel): OtpElement {
return OtpElement(parcel)
}
override fun newArray(size: Int): Array<OtpElement?> {
return arrayOfNulls(size)
}
return secondsRemaining == otpModel.period
}
}
enum class OtpType {
UNDEFINED,
HOTP, // counter based
TOTP // time based
TOTP; // time based
}
enum class TokenType (var tokenDigits: Int) {
Default(TokenCalculator.TOTP_DEFAULT_DIGITS),
Steam(TokenCalculator.STEAM_DEFAULT_DIGITS);
enum class OtpTokenType (var tokenDigits: Int) {
RFC4226(TokenCalculator.OTP_DEFAULT_DIGITS), // HOTP
RFC6238(TokenCalculator.OTP_DEFAULT_DIGITS), // TOTP
// Proprietary
STEAM(TokenCalculator.STEAM_DEFAULT_DIGITS); // TOTP Steam
companion object {
fun getFromString(tokenType: String?): TokenType {
if (tokenType == null)
return Default
return when (tokenType) {
"S", "steam" -> Steam
else -> Default
fun getFromString(tokenType: String): OtpTokenType {
return when (tokenType.toLowerCase(Locale.ENGLISH)) {
"steam" -> STEAM
else -> RFC6238
}
}
fun getTotpTokenTypeValues(getProprietaryElements: Boolean = true): Array<OtpTokenType> {
return if (getProprietaryElements)
arrayOf(RFC6238, STEAM)
else
arrayOf(RFC6238)
}
fun getHotpTokenTypeValues(): Array<OtpTokenType> {
return arrayOf(RFC4226)
}
}
}

View File

@@ -22,119 +22,83 @@ package com.kunzisoft.keepass.otp
import android.net.Uri
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 org.apache.commons.codec.DecoderException
import org.apache.commons.codec.binary.Base32
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
class OtpEntryFields(private val getField: (id: String) -> String?) {
object OtpEntryFields {
var otpElement: OtpElement = OtpElement()
private set
private val TAG = OtpEntryFields::class.java.name
private var type
get() = otpElement.type
set(value) {
otpElement.type = value
}
// Field from KeePassXC
private const val OTP_FIELD = "otp"
private var tokenType
get() = otpElement.tokenType
set(value) {
otpElement.tokenType = value
}
// URL parameters (https://github.com/google/google-authenticator/wiki/Key-Uri-Format)
private const val OTP_SCHEME = "otpauth"
private const val TOTP_AUTHORITY = "totp" // time-based
private const val HOTP_AUTHORITY = "hotp" // counter-based
private const val ALGORITHM_URL_PARAM = "algorithm"
private const val ISSUER_URL_PARAM = "issuer"
private const val SECRET_URL_PARAM = "secret"
private const val DIGITS_URL_PARAM = "digits"
private const val PERIOD_URL_PARAM = "period"
private const val ENCODER_URL_PARAM = "encoder"
private const val COUNTER_URL_PARAM = "counter"
private var name
get() = otpElement.name
set(value) {
otpElement.name = value
}
// Key-values (maybe from plugin or old KeePassXC)
private const val SEED_KEY = "key"
private const val DIGITS_KEY = "size"
private const val STEP_KEY = "step"
private var issuer
get() = otpElement.issuer
set(value) {
otpElement.issuer = value
}
// HmacOtp KeePass2 values (https://keepass.info/help/base/placeholders.html#hmacotp)
private const val HMACOTP_SECRET_FIELD = "HmacOtp-Secret"
private const val HMACOTP_SECRET_HEX_FIELD = "HmacOtp-Secret-Hex"
private const val HMACOTP_SECRET_BASE32_FIELD = "HmacOtp-Secret-Base32"
private const val HMACOTP_SECRET_BASE64_FIELD = "HmacOtp-Secret-Base64"
private const val HMACOTP_SECRET_COUNTER_FIELD = "HmacOtp-Counter"
private var secret
get() = otpElement.secret
set(value) {
otpElement.secret = value
}
// Custom fields (maybe from plugin)
private const val TOTP_SEED_FIELD = "TOTP Seed"
private const val TOTP_SETTING_FIELD = "TOTP Settings"
private fun setUTF8Secret(secret: String) {
this.secret = secret.toByteArray(Charset.forName("UTF-8"))
}
// Token field, use dynamically to generate OTP token
const val OTP_TOKEN_FIELD = "OTP Token"
private fun setHexSecret(secret: String) {
try {
this.secret = Hex.decodeHex(secret)
} catch (e: DecoderException) {
e.printStackTrace()
}
}
// Logical breakdown of key=value regex. the final string is as follows:
// [^&=\s]+=[^&=\s]+(&[^&=\s]+=[^&=\s]+)*
private const val validKeyValue = "[^&=\\s]+"
private const val validKeyValuePair = "$validKeyValue=$validKeyValue"
private const val validKeyValueRegex = "$validKeyValuePair&($validKeyValuePair)*"
private fun setBase32Secret(secret: String) {
this.secret = Base32().decode(secret.toByteArray())
}
private fun setBase64Secret(secret: String) {
this.secret = Base64().decode(secret.toByteArray())
}
private var counter
get() = otpElement.counter
set(value) {
otpElement.counter = if (value < 0) HOTP_INITIAL_COUNTER else value
}
private var step
get() = otpElement.step
set(value) {
otpElement.step = if (value <= 0 || value > 60) TOTP_DEFAULT_PERIOD else value
}
private var digits
get() = otpElement.digits
set(value) {
otpElement.digits = if (value <= 0) TokenType.Default.tokenDigits else value
}
private var algorithm
get() = otpElement.algorithm
set(value) {
otpElement.algorithm = value
}
init {
/**
* Parse fields of an entry to retrieve an OtpElement
*/
fun parseFields(getField: (id: String) -> String?): OtpElement {
val otpElement = OtpElement()
// OTP (HOTP/TOTP) from URL and field from KeePassXC
var parse = parseOTPUri()
var parse = parseOTPUri(getField, otpElement)
// TOTP from key values (maybe plugin or old KeePassXC)
if (!parse)
parse = parseTOTPKeyValues()
parse = parseTOTPKeyValues(getField, otpElement)
// TOTP from custom field
if (!parse)
parse = parseTOTPFromField()
parse = parseTOTPFromField(getField, otpElement)
// HOTP fields from KeePass 2
if (!parse)
parseHOTPFromField()
parseHOTPFromField(getField, otpElement)
return otpElement
}
/**
* Parses a secret value from a URI. The format will be:
*
*
* otpauth://totp/user@example.com?secret=FFF...
*
*
* otpauth://hotp/user@example.com?secret=FFF...&counter=123
*/
private fun parseOTPUri(): Boolean {
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)
@@ -146,15 +110,15 @@ class OtpEntryFields(private val getField: (id: String) -> String?) {
val authority = uri.authority
if (TOTP_AUTHORITY == authority) {
type = OtpType.TOTP
otpElement.type = OtpType.TOTP
} else if (HOTP_AUTHORITY == authority) {
type = OtpType.HOTP
otpElement.type = OtpType.HOTP
val counterParameter = uri.getQueryParameter(COUNTER_URL_PARAM)
if (counterParameter != null) {
try {
counter = Integer.parseInt(counterParameter)
otpElement.counter = Integer.parseInt(counterParameter)
} catch (e: NumberFormatException) {
Log.e(TAG, "Invalid counter in uri")
return false
@@ -169,44 +133,79 @@ class OtpEntryFields(private val getField: (id: String) -> String?) {
val nameParam = validateAndGetNameInPath(uri.path)
if (nameParam != null && nameParam.isNotEmpty())
name = nameParam
val algorithmParam = uri.getQueryParameter(ALGORITHM_URL_PARAM)
if (algorithmParam != null && algorithmParam.isNotEmpty())
algorithm = HashAlgorithm.valueOf(algorithmParam.toUpperCase(Locale.ENGLISH))
otpElement.name = nameParam
val issuerParam = uri.getQueryParameter(ISSUER_URL_PARAM)
if (issuerParam != null && issuerParam.isNotEmpty())
issuer = issuerParam
otpElement.issuer = issuerParam
val secretParam = uri.getQueryParameter(SECRET_URL_PARAM)
if (secretParam != null && secretParam.isNotEmpty())
setBase32Secret(secretParam)
otpElement.setBase32Secret(secretParam)
val encoderParam = uri.getQueryParameter(ENCODER_URL_PARAM)
if (encoderParam != null && encoderParam.isNotEmpty()) {
tokenType = TokenType.getFromString(encoderParam)
digits = tokenType.tokenDigits
otpElement.tokenType = OtpTokenType.getFromString(encoderParam)
}
val digitsParam = uri.getQueryParameter(DIGITS_URL_PARAM)
if (digitsParam != null && digitsParam.isNotEmpty())
digits = digitsParam.toInt()
otpElement.digits = digitsParam.toInt()
val counterParam = uri.getQueryParameter(COUNTER_URL_PARAM)
if (counterParam != null && counterParam.isNotEmpty())
counter = counterParam.toInt()
otpElement.counter = counterParam.toInt()
val stepParam = uri.getQueryParameter(PERIOD_URL_PARAM)
if (stepParam != null && stepParam.isNotEmpty())
step = stepParam.toInt()
otpElement.period = stepParam.toInt()
val algorithmParam = uri.getQueryParameter(ALGORITHM_URL_PARAM)
if (algorithmParam != null && algorithmParam.isNotEmpty())
otpElement.algorithm = HashAlgorithm.valueOf(algorithmParam.toUpperCase(Locale.ENGLISH))
return true
}
return false
}
private fun parseTOTPKeyValues(): Boolean {
private fun buildOtpUri(otpElement: OtpElement): Uri {
val counterOrPeriodLabel: String
val counterOrPeriodValue: String
val otpAuthority = when (otpElement.type) {
OtpType.TOTP -> {
counterOrPeriodLabel = PERIOD_URL_PARAM
counterOrPeriodValue = otpElement.period.toString()
TOTP_AUTHORITY
}
else -> {
counterOrPeriodLabel = COUNTER_URL_PARAM
counterOrPeriodValue = otpElement.counter.toString()
HOTP_AUTHORITY
}
}
val accountName = "OTP"
val issuer = "None"
val otpLabel = "$issuer:$accountName"
val otpSecret = otpElement.getBase32Secret()
val otpDigits = otpElement.digits
val otpIssuer = otpElement.issuer
val otpAlgorithm = otpElement.algorithm.toString()
val uriString = "otpauth://$otpAuthority/$otpLabel" +
"?$SECRET_URL_PARAM=$otpSecret" +
"&$counterOrPeriodLabel=$counterOrPeriodValue" +
"&$DIGITS_URL_PARAM=$otpDigits" +
"&$ISSUER_URL_PARAM=$otpIssuer" +
"&$ALGORITHM_URL_PARAM=$otpAlgorithm" +
"\n"
// TODO steam with ENCODER_URL_PARAM
return Uri.parse(uriString)
}
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)) {
@@ -216,11 +215,11 @@ class OtpEntryFields(private val getField: (id: String) -> String?) {
var secretString = query[SEED_KEY]
if (secretString == null)
secretString = ""
setBase32Secret(secretString)
digits = query[DIGITS_KEY]?.toInt() ?: TOTP_DEFAULT_DIGITS
step = query[STEP_KEY]?.toInt() ?: TOTP_DEFAULT_PERIOD
otpElement.setBase32Secret(secretString)
otpElement.digits = query[DIGITS_KEY]?.toInt() ?: OTP_DEFAULT_DIGITS
otpElement.period = query[STEP_KEY]?.toInt() ?: TOTP_DEFAULT_PERIOD
type = OtpType.TOTP
otpElement.type = OtpType.TOTP
return true
} else {
// Malformed
@@ -230,9 +229,9 @@ class OtpEntryFields(private val getField: (id: String) -> String?) {
return false
}
private fun parseTOTPFromField(): Boolean {
private fun parseTOTPFromField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
val seedField = getField(TOTP_SEED_FIELD) ?: return false
setBase32Secret(seedField)
otpElement.setBase32Secret(seedField)
val settingsField = getField(TOTP_SETTING_FIELD)
if (settingsField != null) {
@@ -243,134 +242,96 @@ class OtpEntryFields(private val getField: (id: String) -> String?) {
// malformed
return false
}
step = matcher.group(1).toInt()
digits = TokenType.getFromString(matcher.group(2)).tokenDigits
otpElement.period = matcher.group(1).toInt()
otpElement.digits = OtpTokenType.getFromString(matcher.group(2)).tokenDigits
}
type = OtpType.TOTP
otpElement.type = OtpType.TOTP
return true
}
private fun parseHOTPFromField(): Boolean {
private fun parseHOTPFromField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
val secretField = getField(HMACOTP_SECRET_FIELD)
val secretHexField = getField(HMACOTP_SECRET_HEX_FIELD)
val secretBase32Field = getField(HMACOTP_SECRET_BASE32_FIELD)
val secretBase64Field = getField(HMACOTP_SECRET_BASE64_FIELD)
when {
secretField != null -> setUTF8Secret(secretField)
secretHexField != null -> setHexSecret(secretHexField)
secretBase32Field != null -> setBase32Secret(secretBase32Field)
secretBase64Field != null -> setBase64Secret(secretBase64Field)
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) {
counter = secretCounterField.toInt()
otpElement.counter = secretCounterField.toInt()
}
type = OtpType.HOTP
otpElement.type = OtpType.HOTP
return true
}
companion object {
private val TAG = OtpEntryFields::class.java.name
// Field from KeePassXC
private const val OTP_FIELD = "otp"
// URL parameters (https://github.com/google/google-authenticator/wiki/Key-Uri-Format)
private const val OTP_SCHEME = "otpauth"
private const val TOTP_AUTHORITY = "totp" // time-based
private const val HOTP_AUTHORITY = "hotp" // counter-based
private const val ALGORITHM_URL_PARAM = "algorithm"
private const val ISSUER_URL_PARAM = "issuer"
private const val SECRET_URL_PARAM = "secret"
private const val DIGITS_URL_PARAM = "digits"
private const val PERIOD_URL_PARAM = "period"
private const val ENCODER_URL_PARAM = "encoder"
private const val COUNTER_URL_PARAM = "counter"
// Key-values (maybe from plugin or old KeePassXC)
private const val SEED_KEY = "key"
private const val DIGITS_KEY = "size"
private const val STEP_KEY = "step"
// HmacOtp KeePass2 values (https://keepass.info/help/base/placeholders.html#hmacotp)
private const val HMACOTP_SECRET_FIELD = "HmacOtp-Secret"
private const val HMACOTP_SECRET_HEX_FIELD = "HmacOtp-Secret-Hex"
private const val HMACOTP_SECRET_BASE32_FIELD = "HmacOtp-Secret-Base32"
private const val HMACOTP_SECRET_BASE64_FIELD = "HmacOtp-Secret-Base64"
private const val HMACOTP_SECRET_COUNTER_FIELD = "HmacOtp-Counter"
// Custom fields (maybe from plugin)
private const val TOTP_SEED_FIELD = "TOTP Seed"
private const val TOTP_SETTING_FIELD = "TOTP Settings"
// Token field, use dynamically to generate OTP token
const val OTP_TOKEN_FIELD = "OTP Token"
// Logical breakdown of key=value regex. the final string is as follows:
// [^&=\s]+=[^&=\s]+(&[^&=\s]+=[^&=\s]+)*
private const val validKeyValue = "[^&=\\s]+"
private const val validKeyValuePair = "$validKeyValue=$validKeyValue"
private const val validKeyValueRegex = "$validKeyValuePair&($validKeyValuePair)*"
private fun validateAndGetNameInPath(path: String?): String? {
if (path == null || !path.startsWith("/")) {
return null
}
// path is "/name", so remove leading "/", and trailing white spaces
val name = path.substring(1).trim { it <= ' ' }
return if (name.isEmpty()) {
null // only white spaces.
} else name
private fun validateAndGetNameInPath(path: String?): String? {
if (path == null || !path.startsWith("/")) {
return null
}
// path is "/name", so remove leading "/", and trailing white spaces
val name = path.substring(1).trim { it <= ' ' }
return if (name.isEmpty()) {
null // only white spaces.
} else name
}
private fun breakDownKeyValuePairs(pairs: String): HashMap<String, String> {
val elements = pairs.split("&".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val output = HashMap<String, String>()
for (element in elements) {
val pair = element.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
output[pair[0]] = pair[1]
}
return output
private fun breakDownKeyValuePairs(pairs: String): HashMap<String, String> {
val elements = pairs.split("&".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val output = HashMap<String, String>()
for (element in elements) {
val pair = element.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
output[pair[0]] = pair[1]
}
return output
}
/**
Build new generated fields in a new list from [fieldsToParse] in parameter,
Remove parameters fields use to generate auto fields
*/
fun generateAutoFields(fieldsToParse: MutableList<Field>): MutableList<Field> {
val newCustomFields: MutableList<Field> = ArrayList(fieldsToParse)
// Remove parameter fields
val otpField = Field(OTP_FIELD)
val totpSeedField = Field(TOTP_SEED_FIELD)
val totpSettingField = Field(TOTP_SETTING_FIELD)
val hmacOtpSecretField = Field(HMACOTP_SECRET_FIELD)
val hmacOtpSecretHewField = Field(HMACOTP_SECRET_HEX_FIELD)
val hmacOtpSecretBase32Field = Field(HMACOTP_SECRET_BASE32_FIELD)
val hmacOtpSecretBase64Field = Field(HMACOTP_SECRET_BASE64_FIELD)
val hmacOtpSecretCounterField = Field(HMACOTP_SECRET_COUNTER_FIELD)
newCustomFields.remove(otpField)
newCustomFields.remove(totpSeedField)
newCustomFields.remove(totpSettingField)
newCustomFields.remove(hmacOtpSecretField)
newCustomFields.remove(hmacOtpSecretHewField)
newCustomFields.remove(hmacOtpSecretBase32Field)
newCustomFields.remove(hmacOtpSecretBase64Field)
newCustomFields.remove(hmacOtpSecretCounterField)
// Empty auto generated OTP Token field
if (fieldsToParse.contains(otpField)
|| fieldsToParse.contains(totpSeedField)
|| fieldsToParse.contains(hmacOtpSecretField)
|| fieldsToParse.contains(hmacOtpSecretHewField)
|| fieldsToParse.contains(hmacOtpSecretBase32Field)
|| fieldsToParse.contains(hmacOtpSecretBase64Field)
)
newCustomFields.add(Field(OTP_TOKEN_FIELD))
return newCustomFields
}
/**
* Build Otp field from an OtpElement
*/
fun buildOtpField(otpElement: OtpElement): Field {
return Field(OTP_FIELD, ProtectedString(true, buildOtpUri(otpElement).toString()))
}
/**
* Build new generated fields in a new list from [fieldsToParse] in parameter,
* Remove parameters fields use to generate auto fields
*/
fun generateAutoFields(fieldsToParse: MutableList<Field>): MutableList<Field> {
val newCustomFields: MutableList<Field> = ArrayList(fieldsToParse)
// Remove parameter fields
val otpField = Field(OTP_FIELD)
val totpSeedField = Field(TOTP_SEED_FIELD)
val totpSettingField = Field(TOTP_SETTING_FIELD)
val hmacOtpSecretField = Field(HMACOTP_SECRET_FIELD)
val hmacOtpSecretHewField = Field(HMACOTP_SECRET_HEX_FIELD)
val hmacOtpSecretBase32Field = Field(HMACOTP_SECRET_BASE32_FIELD)
val hmacOtpSecretBase64Field = Field(HMACOTP_SECRET_BASE64_FIELD)
val hmacOtpSecretCounterField = Field(HMACOTP_SECRET_COUNTER_FIELD)
newCustomFields.remove(otpField)
newCustomFields.remove(totpSeedField)
newCustomFields.remove(totpSettingField)
newCustomFields.remove(hmacOtpSecretField)
newCustomFields.remove(hmacOtpSecretHewField)
newCustomFields.remove(hmacOtpSecretBase32Field)
newCustomFields.remove(hmacOtpSecretBase64Field)
newCustomFields.remove(hmacOtpSecretCounterField)
// Empty auto generated OTP Token field
if (fieldsToParse.contains(otpField)
|| fieldsToParse.contains(totpSeedField)
|| fieldsToParse.contains(hmacOtpSecretField)
|| fieldsToParse.contains(hmacOtpSecretHewField)
|| fieldsToParse.contains(hmacOtpSecretBase32Field)
|| fieldsToParse.contains(hmacOtpSecretBase64Field)
)
newCustomFields.add(Field(OTP_TOKEN_FIELD))
return newCustomFields
}
}

View File

@@ -31,8 +31,8 @@ import javax.crypto.spec.SecretKeySpec;
public class TokenCalculator {
public static final int TOTP_DEFAULT_PERIOD = 30;
public static final int TOTP_DEFAULT_DIGITS = 6;
public static final int HOTP_INITIAL_COUNTER = 1;
public static final int OTP_DEFAULT_DIGITS = 6;
public static final int STEAM_DEFAULT_DIGITS = 5;
private static final char[] STEAMCHARS = new char[] {

View File

@@ -27,7 +27,7 @@ import android.view.ViewGroup
import android.widget.RadioButton
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.ObjectNameResource
import com.kunzisoft.keepass.utils.ObjectNameResource
import java.util.ArrayList

View File

@@ -17,7 +17,7 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database
package com.kunzisoft.keepass.utils
import android.content.res.Resources

View File

@@ -216,7 +216,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
otpProgressView: ProgressBar?,
onClickListener: OnClickListener) {
if (otpElement.type != OtpType.UNDEFINED) {
if (otpElement.type != null) {
otpContainerView.visibility = View.VISIBLE
if (otpElement.token.isEmpty()) {
@@ -226,7 +226,9 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
} else {
assignOtpCopyListener(onClickListener)
otpView.text = otpElement.token
otpLabelView.text = otpElement.type.name
otpElement.type?.name?.let {
otpLabelView.text = it
}
when (otpElement.type) {
// Only add token if HOTP
@@ -235,7 +237,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
// Refresh view if TOTP
OtpType.TOTP -> {
otpProgressView?.apply {
max = otpElement.step
max = otpElement.period
progress = otpElement.secondsRemaining
visibility = View.VISIBLE
}

View File

@@ -0,0 +1,9 @@
<vector
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M11,17c0,0.55 0.45,1 1,1s1,-0.45 1,-1 -0.45,-1 -1,-1 -1,0.45 -1,1zM11,3v4h2L13,5.08c3.39,0.49 6,3.39 6,6.92 0,3.87 -3.13,7 -7,7s-7,-3.13 -7,-7c0,-1.68 0.59,-3.22 1.58,-4.42L12,13l1.41,-1.41 -6.8,-6.8v0.02C4.42,6.45 3,9.05 3,12c0,4.97 4.02,9 9,9 4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9h-1zM18,12c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1 0.45,1 1,1 1,-0.45 1,-1zM6,12c0,0.55 0.45,1 1,1s1,-0.45 1,-1 -0.45,-1 -1,-1 -1,0.45 -1,1z"/>
</vector>

View File

@@ -0,0 +1,159 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:padding="@dimen/default_margin"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="noExcludeDescendants"
tools:targetApi="o">
<androidx.cardview.widget.CardView
android:id="@+id/card_view_otp_selection"
android:layout_margin="4dp"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/default_margin"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:id="@+id/setup_otp_type_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
android:text="@string/otp_type"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle"/>
<!-- HOTP / TOTP -->
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/setup_otp_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/setup_otp_type_label"
app:layout_constraintStart_toStartOf="parent"
android:minHeight="48dp" />
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/setup_otp_token_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="48dp"
app:layout_constraintTop_toBottomOf="@+id/setup_otp_type_label"
app:layout_constraintStart_toEndOf="@+id/setup_otp_type"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Secret -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/setup_otp_secret_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:passwordToggleEnabled="true"
app:passwordToggleTint="?attr/colorAccent">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/setup_otp_secret"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:hint="@string/otp_secret"
tools:targetApi="jelly_bean" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/card_view_otp_advanced"
android:layout_margin="4dp"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/default_margin"
android:orientation="vertical">
<!-- Period / Counter -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/setup_otp_period_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/setup_otp_period"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberSigned"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:hint="@string/otp_period"
tools:text="30"
android:maxLength="2"
tools:targetApi="jelly_bean" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/setup_otp_counter_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/setup_otp_counter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberSigned"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:hint="@string/otp_counter"
tools:text="1"
tools:targetApi="jelly_bean" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Digits -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/setup_otp_digits_title"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/setup_otp_digits"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberSigned"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:hint="@string/otp_digits"
tools:text="6"
android:maxLength="2"
tools:targetApi="jelly_bean" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Algorithm -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/otp_algorithm"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle"/>
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/setup_otp_algorithm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Jeremy Jamet / Kunzisoft.
This file is part of KeePass DX.
KeePass DX is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
KeePass DX is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/menu_add_otp"
android:icon="@drawable/ic_av_timer_white_24dp"
android:title="@string/entry_setup_otp"
android:orderInCategory="91"
app:showAsAction="ifRoom" />
</menu>

View File

@@ -87,6 +87,13 @@
<string name="entry_password">Password</string>
<string name="entry_save">Save</string>
<string name="entry_title">Title</string>
<string name="entry_setup_otp">Setup One-Time Password</string>
<string name="otp_type">OTP type</string>
<string name="otp_secret">Secret</string>
<string name="otp_period">Period</string>
<string name="otp_counter">Counter</string>
<string name="otp_digits">Digits</string>
<string name="otp_algorithm">Algorithm</string>
<string name="entry_otp">OTP</string>
<string name="entry_url">URL</string>
<string name="entry_user_name">Username</string>
@@ -402,7 +409,7 @@
<string name="html_text_dev_feature_upgrade">Do not forget to keep your app up to date by installing new versions.</string>
<string name="download">Download</string>
<string name="contribute">Contribute</string>
<!-- Algorithms -->
<!-- Encryption Algorithms -->
<string name="encryption_rijndael">Rijndael (AES)</string>
<string name="encryption_twofish">Twofish</string>
<string name="encryption_chacha20">ChaCha20</string>

View File

@@ -316,6 +316,8 @@
<style name="KeepassDXStyle.TextAppearance.LabelTextStyle" parent="KeepassDXStyle.TextAppearance">
<item name="android:textColor">?attr/colorAccent</item>
<item name="android:textSize">12sp</item>
<item name="android:paddingLeft">4dp</item>
<item name="android:paddingRight">4dp</item>
</style>
<style name="KeepassDXStyle.TextAppearance.LabelTableTextStyle" parent="KeepassDXStyle.TextAppearance">
<item name="android:textSize">12sp</item>