mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Add OTP auto generate field for Magikeyboard
This commit is contained in:
@@ -44,6 +44,7 @@ import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.settings.SettingsAutofillActivity
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
@@ -69,7 +70,7 @@ class EntryActivity : LockingHideActivity() {
|
||||
private var mIsHistory: Boolean = false
|
||||
|
||||
private var mShowPassword: Boolean = false
|
||||
private var mOtpEntryFields: OtpEntryFields? = null
|
||||
private var mOtpElement: OtpElement? = null
|
||||
|
||||
private var clipboardHelper: ClipboardHelper? = null
|
||||
private var firstLaunchOfActivity: Boolean = false
|
||||
@@ -140,7 +141,9 @@ class EntryActivity : LockingHideActivity() {
|
||||
|
||||
mEntry?.let { entry ->
|
||||
// Init OTP
|
||||
mOtpEntryFields = OtpEntryFields(entry)
|
||||
mOtpElement = OtpEntryFields{ id ->
|
||||
entry.customFields[id]?.toString()
|
||||
}.otpElement
|
||||
|
||||
// Fill data in resume to update from EntryEditActivity
|
||||
fillEntryDataInContentsView(entry)
|
||||
@@ -228,11 +231,11 @@ class EntryActivity : LockingHideActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
mOtpEntryFields?.let { otpEntryFields ->
|
||||
entryContentsView?.assignOtp(otpEntryFields, entryProgress,
|
||||
mOtpElement?.let { otpElement ->
|
||||
entryContentsView?.assignOtp(otpElement, entryProgress,
|
||||
View.OnClickListener {
|
||||
clipboardHelper?.timeoutCopyToClipboard(
|
||||
otpEntryFields.token,
|
||||
otpElement.token,
|
||||
getString(R.string.copy_field, getString(R.string.entry_otp))
|
||||
)
|
||||
})
|
||||
|
||||
@@ -18,7 +18,6 @@ class FieldsAdapter(context: Context) : RecyclerView.Adapter<FieldsAdapter.Field
|
||||
var fields: MutableList<Field> = ArrayList()
|
||||
var onItemClickListener: OnItemClickListener? = null
|
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FieldViewHolder {
|
||||
val view = inflater.inflate(R.layout.keyboard_popup_fields_item, parent, false)
|
||||
return FieldViewHolder(view)
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
@@ -302,6 +303,10 @@ class EntryVersioned : NodeVersioned, PwEntryInterface<GroupVersioned> {
|
||||
------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Retrieve generated entry info,
|
||||
* Remove parameter fields and add auto generated elements in auto custom fields
|
||||
*/
|
||||
fun getEntryInfo(database: Database?, raw: Boolean = false): EntryInfo {
|
||||
val entryInfo = EntryInfo()
|
||||
if (raw)
|
||||
@@ -318,6 +323,12 @@ class EntryVersioned : NodeVersioned, PwEntryInterface<GroupVersioned> {
|
||||
entryInfo.customFields.add(
|
||||
Field(entry.key, entry.value))
|
||||
}
|
||||
// Add otpElement to generate token
|
||||
entryInfo.otpElement = OtpEntryFields { key ->
|
||||
customFields[key]?.toString()
|
||||
}.otpElement
|
||||
// Replace parameter fields by generated OTP fields
|
||||
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
|
||||
if (!raw)
|
||||
database?.stopManageEntry(this)
|
||||
return entryInfo
|
||||
|
||||
@@ -100,20 +100,25 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
val popupFieldsView = LayoutInflater.from(context)
|
||||
.inflate(R.layout.keyboard_popup_fields, FrameLayout(context))
|
||||
|
||||
popupCustomKeys?.dismiss()
|
||||
|
||||
popupCustomKeys = PopupWindow(context)
|
||||
popupCustomKeys?.width = WindowManager.LayoutParams.WRAP_CONTENT
|
||||
popupCustomKeys?.height = WindowManager.LayoutParams.WRAP_CONTENT
|
||||
popupCustomKeys?.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||
popupCustomKeys?.inputMethodMode = PopupWindow.INPUT_METHOD_NEEDED
|
||||
popupCustomKeys?.contentView = popupFieldsView
|
||||
dismissCustomKeys()
|
||||
popupCustomKeys = PopupWindow(context).apply {
|
||||
width = WindowManager.LayoutParams.WRAP_CONTENT
|
||||
height = WindowManager.LayoutParams.WRAP_CONTENT
|
||||
softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||
inputMethodMode = PopupWindow.INPUT_METHOD_NEEDED
|
||||
contentView = popupFieldsView
|
||||
}
|
||||
|
||||
val recyclerView = popupFieldsView.findViewById<androidx.recyclerview.widget.RecyclerView>(R.id.keyboard_popup_fields_list)
|
||||
fieldsAdapter = FieldsAdapter(this)
|
||||
fieldsAdapter?.onItemClickListener = object : FieldsAdapter.OnItemClickListener {
|
||||
override fun onItemClick(item: Field) {
|
||||
currentInputConnection.commitText(item.protectedValue.toString(), 1)
|
||||
if (entryInfoKey?.isAutoGeneratedField(item) == true) {
|
||||
entryInfoKey?.doForAutoGeneratedField(item) { valueGenerated ->
|
||||
currentInputConnection.commitText(valueGenerated, 1)
|
||||
}
|
||||
} else
|
||||
currentInputConnection.commitText(item.protectedValue.toString(), 1)
|
||||
}
|
||||
}
|
||||
recyclerView.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this, androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL, true)
|
||||
@@ -129,6 +134,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
}
|
||||
|
||||
private fun assignKeyboardView() {
|
||||
dismissCustomKeys()
|
||||
if (keyboardView != null) {
|
||||
if (entryInfoKey != null) {
|
||||
if (keyboardEntry != null) {
|
||||
@@ -138,7 +144,6 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
} else {
|
||||
if (keyboard != null) {
|
||||
hideEntryInfo()
|
||||
dismissCustomKeys()
|
||||
keyboardView?.keyboard = keyboard
|
||||
}
|
||||
}
|
||||
@@ -251,6 +256,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
KEY_FIELDS -> {
|
||||
if (entryInfoKey != null) {
|
||||
fieldsAdapter?.fields = entryInfoKey!!.customFields
|
||||
fieldsAdapter?.notifyDataSetChanged()
|
||||
}
|
||||
popupCustomKeys?.showAtLocation(keyboardView, Gravity.END or Gravity.TOP, 0, 0)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ package com.kunzisoft.keepass.model
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
||||
import java.util.ArrayList
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields.Companion.OTP_TOKEN_FIELD
|
||||
import java.util.*
|
||||
|
||||
class EntryInfo : Parcelable {
|
||||
|
||||
@@ -14,6 +15,7 @@ class EntryInfo : Parcelable {
|
||||
var url: String = ""
|
||||
var notes: String = ""
|
||||
var customFields: MutableList<Field> = ArrayList()
|
||||
var otpElement: OtpElement = OtpElement()
|
||||
|
||||
constructor()
|
||||
|
||||
@@ -25,6 +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
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
@@ -39,6 +42,7 @@ class EntryInfo : Parcelable {
|
||||
parcel.writeString(url)
|
||||
parcel.writeString(notes)
|
||||
parcel.writeArray(customFields.toTypedArray())
|
||||
parcel.writeParcelable(otpElement, flags)
|
||||
}
|
||||
|
||||
fun containsCustomFieldsProtected(): Boolean {
|
||||
@@ -49,6 +53,15 @@ class EntryInfo : Parcelable {
|
||||
return customFields.any { !it.protectedValue.isProtected }
|
||||
}
|
||||
|
||||
fun isAutoGeneratedField(field: Field): Boolean {
|
||||
return field.name == OTP_TOKEN_FIELD
|
||||
}
|
||||
|
||||
fun doForAutoGeneratedField(field: Field, action: (valueGenerated: String) -> Unit) {
|
||||
if (field.name == OTP_TOKEN_FIELD)
|
||||
action.invoke(otpElement.token)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmField
|
||||
|
||||
@@ -9,7 +9,7 @@ class Field : Parcelable {
|
||||
var name: String = ""
|
||||
var protectedValue: ProtectedString = ProtectedString()
|
||||
|
||||
constructor(name: String, value: ProtectedString) {
|
||||
constructor(name: String, value: ProtectedString = ProtectedString()) {
|
||||
this.name = name
|
||||
this.protectedValue = value
|
||||
}
|
||||
@@ -28,6 +28,21 @@ class Field : Parcelable {
|
||||
dest.writeParcelable(protectedValue, flags)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Field
|
||||
|
||||
if (name != other.name) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return name.hashCode()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmField
|
||||
|
||||
133
app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt
Normal file
133
app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt
Normal file
@@ -0,0 +1,133 @@
|
||||
package com.kunzisoft.keepass.otp
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.otp.TokenCalculator.DEFAULT_ALGORITHM
|
||||
|
||||
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 {
|
||||
|
||||
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()])
|
||||
|
||||
fun setSettings(seed: String, digits: Int, step: Int) {
|
||||
// TODO: Implement a way to set TOTP from device
|
||||
}
|
||||
|
||||
val token: String
|
||||
get() {
|
||||
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)
|
||||
}
|
||||
OtpType.UNDEFINED -> ""
|
||||
}
|
||||
}
|
||||
|
||||
val secondsRemaining: Int
|
||||
get() = step - (System.currentTimeMillis() / 1000 % step).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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class OtpType {
|
||||
UNDEFINED,
|
||||
HOTP, // counter based
|
||||
TOTP // time based
|
||||
}
|
||||
|
||||
enum class TokenType (var tokenDigits: Int) {
|
||||
Default(TokenCalculator.TOTP_DEFAULT_DIGITS),
|
||||
Steam(TokenCalculator.STEAM_DEFAULT_DIGITS);
|
||||
|
||||
companion object {
|
||||
fun getFromString(tokenType: String?): TokenType {
|
||||
if (tokenType == null)
|
||||
return Default
|
||||
return when (tokenType) {
|
||||
"S", "steam" -> Steam
|
||||
else -> Default
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.otp
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||
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
|
||||
@@ -32,88 +32,51 @@ import java.nio.charset.Charset
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class OtpEntryFields(private val entry: EntryVersioned) {
|
||||
class OtpEntryFields(private val getField: (id: String) -> String?) {
|
||||
|
||||
var type = OtpType.UNDEFINED
|
||||
private set // ie : HOTP or TOTP
|
||||
var name = "" // ie : user@email.com
|
||||
var issuer = "" // ie : Gitlab
|
||||
var secret: ByteArray? = null
|
||||
var otpElement: OtpElement = OtpElement()
|
||||
private set
|
||||
var counter = HOTP_INITIAL_COUNTER // ie : 5 - only for HOTP
|
||||
|
||||
private var type
|
||||
get() = otpElement.type
|
||||
set(value) {
|
||||
field = if (value < 0) HOTP_INITIAL_COUNTER else value
|
||||
otpElement.type = value
|
||||
}
|
||||
var step = TOTP_DEFAULT_PERIOD
|
||||
private set(step) = if (step <= 0 || step > 60) {
|
||||
field = TOTP_DEFAULT_PERIOD
|
||||
} else {
|
||||
field = step
|
||||
} // ie : 30 seconds - only for TOTP
|
||||
var digits = TokenType.Default.tokenDigits // ie : 6 - number of digits generated
|
||||
|
||||
private var tokenType
|
||||
get() = otpElement.tokenType
|
||||
set(value) {
|
||||
field = if (value <= 0) TokenType.Default.tokenDigits else value
|
||||
}
|
||||
var otpAlgorithm: HashAlgorithm = DEFAULT_ALGORITHM
|
||||
|
||||
val token: String
|
||||
get() {
|
||||
return when (type) {
|
||||
OtpType.HOTP -> HOTP(secret, counter.toLong(), digits, otpAlgorithm)
|
||||
OtpType.TOTP -> when (tokenType) {
|
||||
TokenType.Steam -> TOTP_Steam(secret, this.step, digits, otpAlgorithm)
|
||||
TokenType.Default -> TOTP_RFC6238(secret, this.step, digits, otpAlgorithm)
|
||||
}
|
||||
OtpType.UNDEFINED -> ""
|
||||
}
|
||||
otpElement.tokenType = value
|
||||
}
|
||||
|
||||
val secondsRemaining: Int
|
||||
get() = this.step - (System.currentTimeMillis() / 1000 % this.step).toInt()
|
||||
|
||||
enum class OtpType {
|
||||
UNDEFINED,
|
||||
HOTP, // counter based
|
||||
TOTP // time based
|
||||
}
|
||||
|
||||
private var tokenType = TokenType.Default // ie : default or Steam
|
||||
private enum class TokenType constructor(var tokenDigits: Int) {
|
||||
Default(TOTP_DEFAULT_DIGITS),
|
||||
Steam(STEAM_DEFAULT_DIGITS);
|
||||
|
||||
companion object {
|
||||
fun getFromString(tokenType: String?): TokenType {
|
||||
if (tokenType == null)
|
||||
return Default
|
||||
return when (tokenType) {
|
||||
"S", "steam" -> Steam
|
||||
else -> Default
|
||||
}
|
||||
}
|
||||
private var name
|
||||
get() = otpElement.name
|
||||
set(value) {
|
||||
otpElement.name = value
|
||||
}
|
||||
|
||||
private var issuer
|
||||
get() = otpElement.issuer
|
||||
set(value) {
|
||||
otpElement.issuer = value
|
||||
}
|
||||
|
||||
private var secret
|
||||
get() = otpElement.secret
|
||||
set(value) {
|
||||
otpElement.secret = value
|
||||
}
|
||||
|
||||
private fun setUTF8Secret(secret: String) {
|
||||
this.secret = secret.toByteArray(Charset.forName("UTF-8"))
|
||||
}
|
||||
|
||||
init {
|
||||
// OTP (HOTP/TOTP) from URL and field from KeePassXC
|
||||
var parse = parseOtpUri()
|
||||
// TOTP from key values (maybe plugin or old KeePassXC)
|
||||
if (!parse)
|
||||
parse = parseTotpKeyValues()
|
||||
// TOTP from custom field
|
||||
if (!parse)
|
||||
parse = parseTOTPFromField()
|
||||
// HOTP fields from KeePass 2
|
||||
if (!parse)
|
||||
parseHOTPFromField()
|
||||
}
|
||||
|
||||
fun shouldRefreshToken(): Boolean {
|
||||
return secondsRemaining == this.step
|
||||
}
|
||||
|
||||
fun setSettings(seed: String, digits: Int, step: Int) {
|
||||
// TODO: Implement a way to set TOTP from device
|
||||
private fun setHexSecret(secret: String) {
|
||||
try {
|
||||
this.secret = Hex.decodeHex(secret)
|
||||
} catch (e: DecoderException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setBase32Secret(secret: String) {
|
||||
@@ -124,6 +87,44 @@ class OtpEntryFields(private val entry: EntryVersioned) {
|
||||
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 {
|
||||
// OTP (HOTP/TOTP) from URL and field from KeePassXC
|
||||
var parse = parseOTPUri()
|
||||
// TOTP from key values (maybe plugin or old KeePassXC)
|
||||
if (!parse)
|
||||
parse = parseTOTPKeyValues()
|
||||
// TOTP from custom field
|
||||
if (!parse)
|
||||
parse = parseTOTPFromField()
|
||||
// HOTP fields from KeePass 2
|
||||
if (!parse)
|
||||
parseHOTPFromField()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a secret value from a URI. The format will be:
|
||||
*
|
||||
@@ -133,12 +134,12 @@ class OtpEntryFields(private val entry: EntryVersioned) {
|
||||
*
|
||||
* otpauth://hotp/user@example.com?secret=FFF...&counter=123
|
||||
*/
|
||||
private fun parseOtpUri(): Boolean {
|
||||
private fun parseOTPUri(): Boolean {
|
||||
val otpPlainText = getField(OTP_FIELD)
|
||||
if (otpPlainText != null && !otpPlainText.isEmpty()) {
|
||||
if (otpPlainText != null && otpPlainText.isNotEmpty()) {
|
||||
val uri = Uri.parse(otpPlainText)
|
||||
|
||||
if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.toLowerCase()) {
|
||||
if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.toLowerCase(Locale.ENGLISH)) {
|
||||
Log.e(TAG, "Invalid or missing scheme in uri")
|
||||
return false
|
||||
}
|
||||
@@ -172,7 +173,7 @@ class OtpEntryFields(private val entry: EntryVersioned) {
|
||||
|
||||
val algorithmParam = uri.getQueryParameter(ALGORITHM_URL_PARAM)
|
||||
if (algorithmParam != null && algorithmParam.isNotEmpty())
|
||||
otpAlgorithm = HashAlgorithm.valueOf(algorithmParam.toUpperCase(Locale.ENGLISH))
|
||||
algorithm = HashAlgorithm.valueOf(algorithmParam.toUpperCase(Locale.ENGLISH))
|
||||
|
||||
val issuerParam = uri.getQueryParameter(ISSUER_URL_PARAM)
|
||||
if (issuerParam != null && issuerParam.isNotEmpty())
|
||||
@@ -205,7 +206,7 @@ class OtpEntryFields(private val entry: EntryVersioned) {
|
||||
return false
|
||||
}
|
||||
|
||||
private fun parseTotpKeyValues(): Boolean {
|
||||
private fun parseTOTPKeyValues(): Boolean {
|
||||
val plainText = getField(OTP_FIELD)
|
||||
if (plainText != null && plainText.isNotEmpty()) {
|
||||
if (Pattern.matches(validKeyValueRegex, plainText)) {
|
||||
@@ -251,24 +252,19 @@ class OtpEntryFields(private val entry: EntryVersioned) {
|
||||
}
|
||||
|
||||
private fun parseHOTPFromField(): Boolean {
|
||||
val secretField = getField(HMACOTP_SECRET_KEY)
|
||||
val secretHexField = getField(HMACOTP_SECRET_HEX_KEY)
|
||||
val secretBase32Field = getField(HMACOTP_SECRET_BASE32_KEY)
|
||||
val secretBase64Field = getField(HMACOTP_SECRET_BASE64_KEY)
|
||||
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 -> secret = secretField.toByteArray(Charset.forName("UTF-8"))
|
||||
secretHexField != null -> try {
|
||||
secret = Hex.decodeHex(secretHexField)
|
||||
} catch (e: DecoderException) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
secretField != null -> setUTF8Secret(secretField)
|
||||
secretHexField != null -> setHexSecret(secretHexField)
|
||||
secretBase32Field != null -> setBase32Secret(secretBase32Field)
|
||||
secretBase64Field != null -> setBase64Secret(secretBase64Field)
|
||||
else -> return false
|
||||
}
|
||||
|
||||
val secretCounterField = getField(HMACOTP_SECRET_COUNTER_KEY)
|
||||
val secretCounterField = getField(HMACOTP_SECRET_COUNTER_FIELD)
|
||||
if (secretCounterField != null) {
|
||||
counter = secretCounterField.toInt()
|
||||
}
|
||||
@@ -277,11 +273,6 @@ class OtpEntryFields(private val entry: EntryVersioned) {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getField(id: String): String? {
|
||||
val field = entry.customFields[id]
|
||||
return field?.toString()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = OtpEntryFields::class.java.name
|
||||
@@ -307,16 +298,19 @@ class OtpEntryFields(private val entry: EntryVersioned) {
|
||||
private const val STEP_KEY = "step"
|
||||
|
||||
// HmacOtp KeePass2 values (https://keepass.info/help/base/placeholders.html#hmacotp)
|
||||
private const val HMACOTP_SECRET_KEY = "HmacOtp-Secret"
|
||||
private const val HMACOTP_SECRET_HEX_KEY = "HmacOtp-Secret-Hex"
|
||||
private const val HMACOTP_SECRET_BASE32_KEY = "HmacOtp-Secret-Base32"
|
||||
private const val HMACOTP_SECRET_BASE64_KEY = "HmacOtp-Secret-Base64"
|
||||
private const val HMACOTP_SECRET_COUNTER_KEY = "HmacOtp-Counter"
|
||||
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]+"
|
||||
@@ -343,5 +337,40 @@ class OtpEntryFields(private val entry: EntryVersioned) {
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,8 @@ import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
|
||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||
import com.kunzisoft.keepass.database.element.PwDate
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpType
|
||||
import java.util.*
|
||||
|
||||
class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
@@ -211,38 +212,39 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
}
|
||||
}
|
||||
|
||||
fun assignOtp(otpEntryFields: OtpEntryFields,
|
||||
fun assignOtp(otpElement: OtpElement,
|
||||
otpProgressView: ProgressBar?,
|
||||
onClickListener: OnClickListener) {
|
||||
if (otpEntryFields.type != OtpEntryFields.OtpType.UNDEFINED) {
|
||||
|
||||
if (otpElement.type != OtpType.UNDEFINED) {
|
||||
otpContainerView.visibility = View.VISIBLE
|
||||
|
||||
if (otpEntryFields.token.isEmpty()) {
|
||||
if (otpElement.token.isEmpty()) {
|
||||
otpView.text = context.getString(R.string.error_invalid_OTP)
|
||||
otpActionView.setColorFilter(ContextCompat.getColor(context, R.color.grey_dark))
|
||||
assignOtpCopyListener(null)
|
||||
} else {
|
||||
assignOtpCopyListener(onClickListener)
|
||||
otpView.text = otpEntryFields.token
|
||||
otpLabelView.text = otpEntryFields.type.name
|
||||
otpView.text = otpElement.token
|
||||
otpLabelView.text = otpElement.type.name
|
||||
|
||||
when (otpEntryFields.type) {
|
||||
when (otpElement.type) {
|
||||
// Only add token if HOTP
|
||||
OtpEntryFields.OtpType.HOTP -> {
|
||||
OtpType.HOTP -> {
|
||||
}
|
||||
// Refresh view if TOTP
|
||||
OtpEntryFields.OtpType.TOTP -> {
|
||||
OtpType.TOTP -> {
|
||||
otpProgressView?.apply {
|
||||
max = otpEntryFields.step
|
||||
progress = otpEntryFields.secondsRemaining
|
||||
max = otpElement.step
|
||||
progress = otpElement.secondsRemaining
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
otpContainerView.post(object : Runnable {
|
||||
override fun run() {
|
||||
if (otpEntryFields.shouldRefreshToken()) {
|
||||
otpView.text = otpEntryFields.token
|
||||
if (otpElement.shouldRefreshToken()) {
|
||||
otpView.text = otpElement.token
|
||||
}
|
||||
otpProgressView?.progress = otpEntryFields.secondsRemaining
|
||||
otpProgressView?.progress = otpElement.secondsRemaining
|
||||
otpContainerView.postDelayed(this, 1000)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user