mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Change ObjectNameResource and add OTP Dialog
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
95
app/src/main/java/com/kunzisoft/keepass/model/OtpModel.kt
Normal file
95
app/src/main/java/com/kunzisoft/keepass/model/OtpModel.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
9
app/src/main/res/drawable/ic_av_timer_white_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_av_timer_white_24dp.xml
Normal 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>
|
||||
159
app/src/main/res/layout/fragment_set_otp.xml
Normal file
159
app/src/main/res/layout/fragment_set_otp.xml
Normal 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>
|
||||
27
app/src/main/res/menu/edit_entry.xml
Normal file
27
app/src/main/res/menu/edit_entry.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user