mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Merge branch 'feature/OTP_Links' into develop
This commit is contained in:
@@ -150,6 +150,13 @@
|
|||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="text/plain" />
|
<data android:mimeType="text/plain" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="otpauth" android:host="totp" />
|
||||||
|
<data android:scheme="otpauth" android:host="hotp" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity"
|
android:name="com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity"
|
||||||
|
|||||||
@@ -23,15 +23,17 @@ import android.app.Activity
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity to search or select entry in database,
|
* Activity to search or select entry in database,
|
||||||
@@ -42,22 +44,35 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
var sharedWebDomain: String? = null
|
var sharedWebDomain: String? = null
|
||||||
|
var otpString: String? = null
|
||||||
|
|
||||||
when (intent?.action) {
|
when (intent?.action) {
|
||||||
Intent.ACTION_SEND -> {
|
Intent.ACTION_SEND -> {
|
||||||
if ("text/plain" == intent.type) {
|
if ("text/plain" == intent.type) {
|
||||||
// Retrieve web domain
|
// Retrieve web domain or OTP
|
||||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
|
||||||
sharedWebDomain = Uri.parse(it).host
|
if (OtpEntryFields.isOTPUri(extra))
|
||||||
|
otpString = extra
|
||||||
|
else
|
||||||
|
sharedWebDomain = Uri.parse(extra).host
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Intent.ACTION_VIEW -> {
|
||||||
|
// Retrieve OTP
|
||||||
|
intent.dataString?.let { extra ->
|
||||||
|
if (OtpEntryFields.isOTPUri(extra))
|
||||||
|
otpString = extra
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build search param
|
|
||||||
|
// Build domain search param
|
||||||
val searchInfo = SearchInfo().apply {
|
val searchInfo = SearchInfo().apply {
|
||||||
webDomain = sharedWebDomain
|
this.webDomain = sharedWebDomain
|
||||||
|
this.otpString = otpString
|
||||||
}
|
}
|
||||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
searchInfo.webDomain = concreteWebDomain
|
searchInfo.webDomain = concreteWebDomain
|
||||||
@@ -68,6 +83,8 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun launch(searchInfo: SearchInfo) {
|
private fun launch(searchInfo: SearchInfo) {
|
||||||
|
|
||||||
|
if (!searchInfo.containsOnlyNullValues()) {
|
||||||
// Setting to integrate Magikeyboard
|
// Setting to integrate Magikeyboard
|
||||||
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
|
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
|
||||||
|
|
||||||
@@ -79,7 +96,18 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
|
|||||||
searchInfo,
|
searchInfo,
|
||||||
{ items ->
|
{ items ->
|
||||||
// Items found
|
// Items found
|
||||||
if (searchShareForMagikeyboard) {
|
if (searchInfo.otpString != null) {
|
||||||
|
if (!readOnly) {
|
||||||
|
GroupActivity.launchForSaveResult(this,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(applicationContext,
|
||||||
|
R.string.autofill_read_only_save,
|
||||||
|
Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
} else if (searchShareForMagikeyboard) {
|
||||||
if (items.size == 1) {
|
if (items.size == 1) {
|
||||||
// Automatically populate keyboard
|
// Automatically populate keyboard
|
||||||
val entryPopulate = items[0]
|
val entryPopulate = items[0]
|
||||||
@@ -102,7 +130,18 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
if (readOnly || searchShareForMagikeyboard) {
|
if (searchInfo.otpString != null) {
|
||||||
|
if (!readOnly) {
|
||||||
|
GroupActivity.launchForSaveResult(this,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(applicationContext,
|
||||||
|
R.string.autofill_read_only_save,
|
||||||
|
Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
} else if (readOnly || searchShareForMagikeyboard) {
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||||
readOnly,
|
readOnly,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
@@ -115,7 +154,17 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// If database not open
|
// If database not open
|
||||||
if (searchShareForMagikeyboard) {
|
if (searchInfo.otpString != null) {
|
||||||
|
if (!readOnly) {
|
||||||
|
FileDatabaseSelectActivity.launchForSaveResult(this,
|
||||||
|
searchInfo)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(applicationContext,
|
||||||
|
R.string.autofill_read_only_save,
|
||||||
|
Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
} else if (searchShareForMagikeyboard) {
|
||||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
|
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
} else {
|
} else {
|
||||||
@@ -124,6 +173,7 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -468,6 +468,19 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Save Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun launchForSaveResult(context: Context,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
|
EntrySelectionHelper.startActivityForSaveModeResult(context,
|
||||||
|
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Keyboard Launch
|
* Keyboard Launch
|
||||||
|
|||||||
@@ -1370,8 +1370,20 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{ searchInfo ->
|
||||||
// Nothing with Save Info, only pass by search first
|
// Save info used with OTP
|
||||||
|
if (!readOnly) {
|
||||||
|
GroupActivity.launchForSaveResult(activity,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(activity.applicationContext,
|
||||||
|
R.string.autofill_read_only_save,
|
||||||
|
Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
{ searchInfo ->
|
||||||
SearchHelper.checkAutoSearchInfo(activity,
|
SearchHelper.checkAutoSearchInfo(activity,
|
||||||
|
|||||||
@@ -795,6 +795,25 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Save Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForSaveResult(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForSaveModeResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Keyboard Launch
|
* Keyboard Launch
|
||||||
@@ -877,8 +896,11 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
searchInfo)
|
searchInfo)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{ // Save Action
|
{ searchInfo -> // Save Action
|
||||||
// Not directly used, a search is performed before
|
PasswordActivity.launchForSaveResult(activity,
|
||||||
|
databaseUri, keyFile,
|
||||||
|
searchInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{ searchInfo -> // Keyboard Selection Action
|
{ searchInfo -> // Keyboard Selection Action
|
||||||
PasswordActivity.launchForKeyboardResult(activity,
|
PasswordActivity.launchForKeyboardResult(activity,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import com.kunzisoft.keepass.database.element.icon.IconImage
|
|||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
@@ -125,7 +126,22 @@ class EntryInfo : Parcelable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun saveSearchInfo(database: Database?, searchInfo: SearchInfo) {
|
fun saveSearchInfo(database: Database?, searchInfo: SearchInfo) {
|
||||||
searchInfo.webDomain?.let { webDomain ->
|
searchInfo.otpString?.let { otpString ->
|
||||||
|
// Replace the OTP field
|
||||||
|
OtpEntryFields.parseOTPUri(otpString)?.let { otpElement ->
|
||||||
|
if (title.isEmpty())
|
||||||
|
title = otpElement.issuer
|
||||||
|
if (username.isEmpty())
|
||||||
|
username = otpElement.name
|
||||||
|
// Add OTP field
|
||||||
|
val mutableCustomFields = customFields as ArrayList<Field>
|
||||||
|
val otpField = OtpEntryFields.buildOtpField(otpElement, null, null)
|
||||||
|
if (mutableCustomFields.contains(otpField)) {
|
||||||
|
mutableCustomFields.remove(otpField)
|
||||||
|
}
|
||||||
|
mutableCustomFields.add(otpField)
|
||||||
|
}
|
||||||
|
} ?: searchInfo.webDomain?.let { webDomain ->
|
||||||
// If unable to save web domain in custom field or URL not populated, save in URL
|
// If unable to save web domain in custom field or URL not populated, save in URL
|
||||||
val scheme = searchInfo.webScheme
|
val scheme = searchInfo.webScheme
|
||||||
val webScheme = if (scheme.isNullOrEmpty()) "http" else scheme
|
val webScheme = if (scheme.isNullOrEmpty()) "http" else scheme
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package com.kunzisoft.keepass.model
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.ObjectNameResource
|
import com.kunzisoft.keepass.utils.ObjectNameResource
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -35,6 +37,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
get() {
|
get() {
|
||||||
return if (webDomain == null) null else field
|
return if (webDomain == null) null else field
|
||||||
}
|
}
|
||||||
|
var otpString: String? = null
|
||||||
|
|
||||||
constructor()
|
constructor()
|
||||||
|
|
||||||
@@ -42,6 +45,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
applicationId = toCopy?.applicationId
|
applicationId = toCopy?.applicationId
|
||||||
webDomain = toCopy?.webDomain
|
webDomain = toCopy?.webDomain
|
||||||
webScheme = toCopy?.webScheme
|
webScheme = toCopy?.webScheme
|
||||||
|
otpString = toCopy?.otpString
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(parcel: Parcel) {
|
private constructor(parcel: Parcel) {
|
||||||
@@ -51,6 +55,8 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
webDomain = if (readDomain.isNullOrEmpty()) null else readDomain
|
webDomain = if (readDomain.isNullOrEmpty()) null else readDomain
|
||||||
val readScheme = parcel.readString()
|
val readScheme = parcel.readString()
|
||||||
webScheme = if (readScheme.isNullOrEmpty()) null else readScheme
|
webScheme = if (readScheme.isNullOrEmpty()) null else readScheme
|
||||||
|
val readOtp = parcel.readString()
|
||||||
|
otpString = if (readOtp.isNullOrEmpty()) null else readOtp
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int {
|
||||||
@@ -61,14 +67,23 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
parcel.writeString(applicationId ?: "")
|
parcel.writeString(applicationId ?: "")
|
||||||
parcel.writeString(webDomain ?: "")
|
parcel.writeString(webDomain ?: "")
|
||||||
parcel.writeString(webScheme ?: "")
|
parcel.writeString(webScheme ?: "")
|
||||||
|
parcel.writeString(otpString ?: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getName(resources: Resources): String {
|
override fun getName(resources: Resources): String {
|
||||||
|
otpString?.let { otpString ->
|
||||||
|
OtpEntryFields.parseOTPUri(otpString)?.let { otpElement ->
|
||||||
|
return "${otpElement.type} (${Uri.decode(otpElement.issuer)}:${Uri.decode(otpElement.name)})"
|
||||||
|
}
|
||||||
|
}
|
||||||
return toString()
|
return toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun containsOnlyNullValues(): Boolean {
|
fun containsOnlyNullValues(): Boolean {
|
||||||
return applicationId == null && webDomain == null && webScheme == null
|
return applicationId == null
|
||||||
|
&& webDomain == null
|
||||||
|
&& webScheme == null
|
||||||
|
&& otpString == null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
@@ -80,6 +95,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
if (applicationId != other.applicationId) return false
|
if (applicationId != other.applicationId) return false
|
||||||
if (webDomain != other.webDomain) return false
|
if (webDomain != other.webDomain) return false
|
||||||
if (webScheme != other.webScheme) return false
|
if (webScheme != other.webScheme) return false
|
||||||
|
if (otpString != other.otpString) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -88,11 +104,12 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
var result = applicationId?.hashCode() ?: 0
|
var result = applicationId?.hashCode() ?: 0
|
||||||
result = 31 * result + (webDomain?.hashCode() ?: 0)
|
result = 31 * result + (webDomain?.hashCode() ?: 0)
|
||||||
result = 31 * result + (webScheme?.hashCode() ?: 0)
|
result = 31 * result + (webScheme?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (otpString?.hashCode() ?: 0)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return webDomain ?: applicationId ?: ""
|
return otpString ?: webDomain ?: applicationId ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -216,13 +216,17 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
|
|||||||
return secret.isNotEmpty() && checkBase64Secret(secret)
|
return secret.isNotEmpty() && checkBase64Secret(secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun replaceSpaceChars(parameter: String): String {
|
fun removeLineChars(parameter: String): String {
|
||||||
|
return parameter.replace("[\\r|\\n|\\t|\\u00A0]+".toRegex(), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeSpaceChars(parameter: String): String {
|
||||||
return parameter.replace("[\\r|\\n|\\t|\\s|\\u00A0]+".toRegex(), "")
|
return parameter.replace("[\\r|\\n|\\t|\\s|\\u00A0]+".toRegex(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun replaceBase32Chars(parameter: String): String {
|
fun replaceBase32Chars(parameter: String): String {
|
||||||
// Add 'A' at end if not Base32 length
|
// Add 'A' at end if not Base32 length
|
||||||
var parameterNewSize = replaceSpaceChars(parameter.toUpperCase(Locale.ENGLISH))
|
var parameterNewSize = removeSpaceChars(parameter.toUpperCase(Locale.ENGLISH))
|
||||||
while (parameterNewSize.length % 8 != 0) {
|
while (parameterNewSize.length % 8 != 0) {
|
||||||
parameterNewSize += 'A'
|
parameterNewSize += 'A'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ import android.net.Uri
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.model.Field
|
import com.kunzisoft.keepass.model.Field
|
||||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.replaceSpaceChars
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.removeLineChars
|
||||||
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.removeSpaceChars
|
||||||
import com.kunzisoft.keepass.otp.TokenCalculator.*
|
import com.kunzisoft.keepass.otp.TokenCalculator.*
|
||||||
import java.net.URLEncoder
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
@@ -49,6 +49,9 @@ object OtpEntryFields {
|
|||||||
private const val ENCODER_URL_PARAM = "encoder"
|
private const val ENCODER_URL_PARAM = "encoder"
|
||||||
private const val COUNTER_URL_PARAM = "counter"
|
private const val COUNTER_URL_PARAM = "counter"
|
||||||
|
|
||||||
|
// OTPauth URI
|
||||||
|
private const val REGEX_OTP_AUTH = "^(?:otpauth://([ht]otp)/)(?:(?:([^:?#]*): *)?([^:?#]*))(?:\\?([^#]+))$"
|
||||||
|
|
||||||
// Key-values (maybe from plugin or old KeePassXC)
|
// Key-values (maybe from plugin or old KeePassXC)
|
||||||
private const val SEED_KEY = "key"
|
private const val SEED_KEY = "key"
|
||||||
private const val DIGITS_KEY = "size"
|
private const val DIGITS_KEY = "size"
|
||||||
@@ -91,7 +94,25 @@ object OtpEntryFields {
|
|||||||
// HOTP fields from KeePass 2
|
// HOTP fields from KeePass 2
|
||||||
if (parseHOTPFromField(getField, otpElement))
|
if (parseHOTPFromField(getField, otpElement))
|
||||||
return otpElement
|
return otpElement
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tell if [otpUri] is a valid Otp URI
|
||||||
|
*/
|
||||||
|
fun isOTPUri(otpUri: String): Boolean {
|
||||||
|
if (Pattern.matches(REGEX_OTP_AUTH, otpUri))
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get OtpElement from [otpUri]
|
||||||
|
*/
|
||||||
|
fun parseOTPUri(otpUri: String): OtpElement? {
|
||||||
|
val otpElement = OtpElement()
|
||||||
|
if (parseOTPUri({ key -> if (key == OTP_FIELD) otpUri else null }, otpElement))
|
||||||
|
return otpElement
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,8 +125,8 @@ object OtpEntryFields {
|
|||||||
*/
|
*/
|
||||||
private fun parseOTPUri(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
|
private fun parseOTPUri(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
|
||||||
val otpPlainText = getField(OTP_FIELD)
|
val otpPlainText = getField(OTP_FIELD)
|
||||||
if (otpPlainText != null && otpPlainText.isNotEmpty()) {
|
if (otpPlainText != null && otpPlainText.isNotEmpty() && isOTPUri(otpPlainText)) {
|
||||||
val uri = Uri.parse(replaceSpaceChars(otpPlainText))
|
val uri = Uri.parse(removeSpaceChars(otpPlainText))
|
||||||
|
|
||||||
if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.toLowerCase(Locale.ENGLISH)) {
|
if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.toLowerCase(Locale.ENGLISH)) {
|
||||||
Log.e(TAG, "Invalid or missing scheme in uri")
|
Log.e(TAG, "Invalid or missing scheme in uri")
|
||||||
@@ -135,12 +156,19 @@ object OtpEntryFields {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val nameParam = validateAndGetNameInPath(uri.path)
|
val nameParam = validateAndGetNameInPath(uri.path)
|
||||||
if (nameParam != null && nameParam.isNotEmpty())
|
if (nameParam != null && nameParam.isNotEmpty()) {
|
||||||
otpElement.name = nameParam
|
val userIdArray = nameParam.split(":", "%3A")
|
||||||
|
if (userIdArray.size > 1) {
|
||||||
|
otpElement.issuer = removeLineChars(userIdArray[0])
|
||||||
|
otpElement.name = removeLineChars(userIdArray[1])
|
||||||
|
} else {
|
||||||
|
otpElement.name = removeLineChars(nameParam)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val issuerParam = uri.getQueryParameter(ISSUER_URL_PARAM)
|
val issuerParam = uri.getQueryParameter(ISSUER_URL_PARAM)
|
||||||
if (issuerParam != null && issuerParam.isNotEmpty())
|
if (issuerParam != null && issuerParam.isNotEmpty())
|
||||||
otpElement.issuer = issuerParam
|
otpElement.issuer = removeLineChars(issuerParam)
|
||||||
|
|
||||||
val secretParam = uri.getQueryParameter(SECRET_URL_PARAM)
|
val secretParam = uri.getQueryParameter(SECRET_URL_PARAM)
|
||||||
if (secretParam != null && secretParam.isNotEmpty()) {
|
if (secretParam != null && secretParam.isNotEmpty()) {
|
||||||
@@ -211,15 +239,15 @@ object OtpEntryFields {
|
|||||||
}
|
}
|
||||||
val issuer =
|
val issuer =
|
||||||
if (title != null && title.isNotEmpty())
|
if (title != null && title.isNotEmpty())
|
||||||
replaceCharsForUrl(title)
|
encodeParameter(title)
|
||||||
else
|
else
|
||||||
replaceCharsForUrl(otpElement.issuer)
|
encodeParameter(otpElement.issuer)
|
||||||
val accountName =
|
val accountName =
|
||||||
if (username != null && username.isNotEmpty())
|
if (username != null && username.isNotEmpty())
|
||||||
replaceCharsForUrl(username)
|
encodeParameter(username)
|
||||||
else
|
else
|
||||||
replaceCharsForUrl(otpElement.name)
|
encodeParameter(otpElement.name)
|
||||||
val uriString = StringBuilder("otpauth://$otpAuthority/$issuer:$accountName" +
|
val uriString = StringBuilder("otpauth://$otpAuthority/$issuer%3A$accountName" +
|
||||||
"?$SECRET_URL_PARAM=${otpElement.getBase32Secret()}" +
|
"?$SECRET_URL_PARAM=${otpElement.getBase32Secret()}" +
|
||||||
"&$counterOrPeriodLabel=$counterOrPeriodValue" +
|
"&$counterOrPeriodLabel=$counterOrPeriodValue" +
|
||||||
"&$DIGITS_URL_PARAM=${otpElement.digits}" +
|
"&$DIGITS_URL_PARAM=${otpElement.digits}" +
|
||||||
@@ -233,8 +261,8 @@ object OtpEntryFields {
|
|||||||
return Uri.parse(uriString.toString())
|
return Uri.parse(uriString.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun replaceCharsForUrl(parameter: String): String {
|
private fun encodeParameter(parameter: String): String {
|
||||||
return URLEncoder.encode(replaceSpaceChars(parameter), "UTF-8")
|
return Uri.encode(OtpElement.removeLineChars(parameter))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseTOTPKeyValues(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
|
private fun parseTOTPKeyValues(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
|
||||||
@@ -321,7 +349,7 @@ object OtpEntryFields {
|
|||||||
// path is "/name", so remove leading "/", and trailing white spaces
|
// path is "/name", so remove leading "/", and trailing white spaces
|
||||||
val name = path.substring(1).trim { it <= ' ' }
|
val name = path.substring(1).trim { it <= ' ' }
|
||||||
return if (name.isEmpty()) {
|
return if (name.isEmpty()) {
|
||||||
null // only white spaces.
|
null
|
||||||
} else name
|
} else name
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,7 +366,7 @@ object OtpEntryFields {
|
|||||||
/**
|
/**
|
||||||
* Build Otp field from an OtpElement
|
* Build Otp field from an OtpElement
|
||||||
*/
|
*/
|
||||||
fun buildOtpField(otpElement: OtpElement, title: String?, username: String?): Field {
|
fun buildOtpField(otpElement: OtpElement, title: String? = null, username: String? = null): Field {
|
||||||
return Field(OTP_FIELD, ProtectedString(true,
|
return Field(OTP_FIELD, ProtectedString(true,
|
||||||
buildOtpUri(otpElement, title, username).toString()))
|
buildOtpUri(otpElement, title, username).toString()))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user