Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop

This commit is contained in:
J-Jamet
2020-11-13 10:03:45 +01:00
9 changed files with 249 additions and 80 deletions

View File

@@ -150,6 +150,13 @@
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</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
android:name="com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity"

View File

@@ -23,15 +23,17 @@ import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil
/**
* Activity to search or select entry in database,
@@ -42,22 +44,35 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
var sharedWebDomain: String? = null
var otpString: String? = null
when (intent?.action) {
Intent.ACTION_SEND -> {
if ("text/plain" == intent.type) {
// Retrieve web domain
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
sharedWebDomain = Uri.parse(it).host
// Retrieve web domain or OTP
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
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 -> {}
}
// Build search param
// Build domain search param
val searchInfo = SearchInfo().apply {
webDomain = sharedWebDomain
this.webDomain = sharedWebDomain
this.otpString = otpString
}
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
@@ -68,62 +83,97 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
}
private fun launch(searchInfo: SearchInfo) {
// Setting to integrate Magikeyboard
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
// If database is open
val database = Database.getInstance()
val readOnly = database.isReadOnly
SearchHelper.checkAutoSearchInfo(this,
database,
searchInfo,
{ items ->
// Items found
if (searchShareForMagikeyboard) {
if (items.size == 1) {
// Automatically populate keyboard
val entryPopulate = items[0]
populateKeyboardAndMoveAppToBackground(this,
entryPopulate,
intent)
if (!searchInfo.containsOnlyNullValues()) {
// Setting to integrate Magikeyboard
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
// If database is open
val database = Database.getInstance()
val readOnly = database.isReadOnly
SearchHelper.checkAutoSearchInfo(this,
database,
searchInfo,
{ items ->
// Items found
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) {
// Automatically populate keyboard
val entryPopulate = items[0]
populateKeyboardAndMoveAppToBackground(this,
entryPopulate,
intent)
} else {
// Select the one we want
GroupActivity.launchForKeyboardSelectionResult(this,
readOnly,
searchInfo,
true)
}
} else {
// Select the one we want
GroupActivity.launchForKeyboardSelectionResult(this,
GroupActivity.launchForSearchResult(this,
readOnly,
searchInfo,
true)
}
} else {
GroupActivity.launchForSearchResult(this,
readOnly,
searchInfo,
true)
},
{
// Show the database UI to select the entry
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,
readOnly,
searchInfo,
false)
} else {
GroupActivity.launchForSaveResult(this,
searchInfo,
false)
}
},
{
// If database not open
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,
searchInfo)
} else {
FileDatabaseSelectActivity.launchForSearchResult(this,
searchInfo)
}
}
},
{
// Show the database UI to select the entry
if (readOnly || searchShareForMagikeyboard) {
GroupActivity.launchForKeyboardSelectionResult(this,
readOnly,
searchInfo,
false)
} else {
GroupActivity.launchForSaveResult(this,
searchInfo,
false)
}
},
{
// If database not open
if (searchShareForMagikeyboard) {
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
searchInfo)
} else {
FileDatabaseSelectActivity.launchForSearchResult(this,
searchInfo)
}
}
)
)
}
finish()
}
}

View File

@@ -468,6 +468,19 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
searchInfo)
}
/*
* -------------------------
* Save Launch
* -------------------------
*/
fun launchForSaveResult(context: Context,
searchInfo: SearchInfo) {
EntrySelectionHelper.startActivityForSaveModeResult(context,
Intent(context, FileDatabaseSelectActivity::class.java),
searchInfo)
}
/*
* -------------------------
* Keyboard Launch

View File

@@ -1370,8 +1370,20 @@ class GroupActivity : LockingActivity(),
}
)
},
{
// Nothing with Save Info, only pass by search first
{ searchInfo ->
// 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 ->
SearchHelper.checkAutoSearchInfo(activity,

View File

@@ -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
@@ -877,8 +896,11 @@ open class PasswordActivity : SpecialModeActivity() {
searchInfo)
onLaunchActivitySpecialMode()
},
{ // Save Action
// Not directly used, a search is performed before
{ searchInfo -> // Save Action
PasswordActivity.launchForSaveResult(activity,
databaseUri, keyFile,
searchInfo)
onLaunchActivitySpecialMode()
},
{ searchInfo -> // Keyboard Selection Action
PasswordActivity.launchForKeyboardResult(activity,

View File

@@ -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.security.ProtectedString
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
import kotlin.collections.ArrayList
@@ -125,7 +126,22 @@ class EntryInfo : Parcelable {
}
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
val scheme = searchInfo.webScheme
val webScheme = if (scheme.isNullOrEmpty()) "http" else scheme

View File

@@ -2,8 +2,10 @@ package com.kunzisoft.keepass.model
import android.content.Context
import android.content.res.Resources
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.ObjectNameResource
import kotlinx.coroutines.CoroutineScope
@@ -35,6 +37,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
get() {
return if (webDomain == null) null else field
}
var otpString: String? = null
constructor()
@@ -42,6 +45,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
applicationId = toCopy?.applicationId
webDomain = toCopy?.webDomain
webScheme = toCopy?.webScheme
otpString = toCopy?.otpString
}
private constructor(parcel: Parcel) {
@@ -51,6 +55,8 @@ class SearchInfo : ObjectNameResource, Parcelable {
webDomain = if (readDomain.isNullOrEmpty()) null else readDomain
val readScheme = parcel.readString()
webScheme = if (readScheme.isNullOrEmpty()) null else readScheme
val readOtp = parcel.readString()
otpString = if (readOtp.isNullOrEmpty()) null else readOtp
}
override fun describeContents(): Int {
@@ -61,14 +67,23 @@ class SearchInfo : ObjectNameResource, Parcelable {
parcel.writeString(applicationId ?: "")
parcel.writeString(webDomain ?: "")
parcel.writeString(webScheme ?: "")
parcel.writeString(otpString ?: "")
}
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()
}
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 {
@@ -80,6 +95,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
if (applicationId != other.applicationId) return false
if (webDomain != other.webDomain) return false
if (webScheme != other.webScheme) return false
if (otpString != other.otpString) return false
return true
}
@@ -88,11 +104,12 @@ class SearchInfo : ObjectNameResource, Parcelable {
var result = applicationId?.hashCode() ?: 0
result = 31 * result + (webDomain?.hashCode() ?: 0)
result = 31 * result + (webScheme?.hashCode() ?: 0)
result = 31 * result + (otpString?.hashCode() ?: 0)
return result
}
override fun toString(): String {
return webDomain ?: applicationId ?: ""
return otpString ?: webDomain ?: applicationId ?: ""
}
companion object {

View File

@@ -216,13 +216,17 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
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(), "")
}
fun replaceBase32Chars(parameter: String): String {
// 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) {
parameterNewSize += 'A'
}

View File

@@ -24,9 +24,9 @@ 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.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 java.net.URLEncoder
import java.util.*
import java.util.regex.Pattern
@@ -49,6 +49,9 @@ object OtpEntryFields {
private const val ENCODER_URL_PARAM = "encoder"
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)
private const val SEED_KEY = "key"
private const val DIGITS_KEY = "size"
@@ -91,7 +94,25 @@ object OtpEntryFields {
// HOTP fields from KeePass 2
if (parseHOTPFromField(getField, 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
}
@@ -104,8 +125,8 @@ object OtpEntryFields {
*/
private fun parseOTPUri(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
val otpPlainText = getField(OTP_FIELD)
if (otpPlainText != null && otpPlainText.isNotEmpty()) {
val uri = Uri.parse(replaceSpaceChars(otpPlainText))
if (otpPlainText != null && otpPlainText.isNotEmpty() && isOTPUri(otpPlainText)) {
val uri = Uri.parse(removeSpaceChars(otpPlainText))
if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.toLowerCase(Locale.ENGLISH)) {
Log.e(TAG, "Invalid or missing scheme in uri")
@@ -135,12 +156,19 @@ object OtpEntryFields {
}
val nameParam = validateAndGetNameInPath(uri.path)
if (nameParam != null && nameParam.isNotEmpty())
otpElement.name = nameParam
if (nameParam != null && nameParam.isNotEmpty()) {
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)
if (issuerParam != null && issuerParam.isNotEmpty())
otpElement.issuer = issuerParam
otpElement.issuer = removeLineChars(issuerParam)
val secretParam = uri.getQueryParameter(SECRET_URL_PARAM)
if (secretParam != null && secretParam.isNotEmpty()) {
@@ -211,15 +239,15 @@ object OtpEntryFields {
}
val issuer =
if (title != null && title.isNotEmpty())
replaceCharsForUrl(title)
encodeParameter(title)
else
replaceCharsForUrl(otpElement.issuer)
encodeParameter(otpElement.issuer)
val accountName =
if (username != null && username.isNotEmpty())
replaceCharsForUrl(username)
encodeParameter(username)
else
replaceCharsForUrl(otpElement.name)
val uriString = StringBuilder("otpauth://$otpAuthority/$issuer:$accountName" +
encodeParameter(otpElement.name)
val uriString = StringBuilder("otpauth://$otpAuthority/$issuer%3A$accountName" +
"?$SECRET_URL_PARAM=${otpElement.getBase32Secret()}" +
"&$counterOrPeriodLabel=$counterOrPeriodValue" +
"&$DIGITS_URL_PARAM=${otpElement.digits}" +
@@ -233,8 +261,8 @@ object OtpEntryFields {
return Uri.parse(uriString.toString())
}
private fun replaceCharsForUrl(parameter: String): String {
return URLEncoder.encode(replaceSpaceChars(parameter), "UTF-8")
private fun encodeParameter(parameter: String): String {
return Uri.encode(OtpElement.removeLineChars(parameter))
}
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
val name = path.substring(1).trim { it <= ' ' }
return if (name.isEmpty()) {
null // only white spaces.
null
} else name
}
@@ -338,7 +366,7 @@ object OtpEntryFields {
/**
* 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,
buildOtpUri(otpElement, title, username).toString()))
}