mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Merge branch 'master' into feature/Passkeys
This commit is contained in:
@@ -19,12 +19,12 @@ android {
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
testOptions {
|
||||
|
||||
@@ -38,7 +38,12 @@ import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.template.Template
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateEngine
|
||||
import com.kunzisoft.keepass.database.exception.*
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseInputException
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.MergeDatabaseKDBException
|
||||
import com.kunzisoft.keepass.database.exception.SignatureDatabaseException
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
|
||||
@@ -51,11 +56,17 @@ import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import com.kunzisoft.keepass.utils.SingletonHolder
|
||||
import com.kunzisoft.keepass.utils.StringUtil.toFormattedColorInt
|
||||
import com.kunzisoft.keepass.utils.StringUtil.toFormattedColorString
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
import com.kunzisoft.keepass.utils.readAllBytes
|
||||
import com.kunzisoft.keepass.utils.readBytes4ToUInt
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
open class Database {
|
||||
@@ -875,6 +886,7 @@ open class Database {
|
||||
|
||||
fun createVirtualGroupFromSearchInfo(
|
||||
searchInfoString: String,
|
||||
searchInfoByDomain: Boolean,
|
||||
max: Int = Integer.MAX_VALUE
|
||||
): Group? {
|
||||
return mSearchHelper.createVirtualGroupWithSearchResult(this,
|
||||
@@ -884,6 +896,7 @@ open class Database {
|
||||
searchInUsernames = false
|
||||
searchInPasswords = false
|
||||
searchInUrls = true
|
||||
searchByDomain = searchInfoByDomain
|
||||
searchInNotes = true
|
||||
searchInOTP = false
|
||||
searchInOther = true
|
||||
|
||||
@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.utils.readEnum
|
||||
import com.kunzisoft.keepass.utils.readSerializableCompat
|
||||
import com.kunzisoft.keepass.utils.writeEnum
|
||||
@@ -60,11 +61,11 @@ class DateInstant : Parcelable {
|
||||
}
|
||||
|
||||
private fun parse(value: String, type: Type): Instant {
|
||||
return when (type) {
|
||||
Type.DATE -> Instant(dateFormat.parseDateTime(value) ?: DateTime())
|
||||
Type.TIME -> Instant(timeFormat.parseDateTime(value) ?: DateTime())
|
||||
else -> Instant(dateTimeFormat.parseDateTime(value) ?: DateTime())
|
||||
}
|
||||
return Instant(when (type) {
|
||||
Type.DATE_TIME -> dateTimeFormat.parseDateTime(value) ?: DateTime()
|
||||
Type.DATE -> dateFormat.parseDateTime(value) ?: DateTime()
|
||||
Type.TIME -> timeFormat.parseDateTime(value) ?: DateTime()
|
||||
})
|
||||
}
|
||||
|
||||
constructor(string: String, type: Type = Type.DATE_TIME) {
|
||||
@@ -175,20 +176,14 @@ class DateInstant : Parcelable {
|
||||
}
|
||||
}
|
||||
|
||||
fun toDotNetSeconds(): Long {
|
||||
val duration = Duration(JAVA_EPOCH_DATE_TIME, mInstant)
|
||||
val seconds = duration.millis / 1000L
|
||||
return seconds + EPOCH_OFFSET
|
||||
}
|
||||
|
||||
fun toJavaMilliseconds(): Long {
|
||||
/**
|
||||
* Returns:
|
||||
* the number of milliseconds since 1970-01-01T00:00:00Z
|
||||
*/
|
||||
fun toMilliseconds(): Long {
|
||||
return mInstant.millis
|
||||
}
|
||||
|
||||
fun toDateTimeSecondsFormat(): String {
|
||||
return dateTimeSecondsFormat.print(mInstant)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return when (type) {
|
||||
Type.DATE -> dateFormat.print(mInstant)
|
||||
@@ -239,6 +234,8 @@ class DateInstant : Parcelable {
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = DateInstant::class.java.name
|
||||
|
||||
private val DOT_NET_EPOCH_DATE_TIME = DateTime(1, 1, 1, 0, 0, 0, DateTimeZone.UTC)
|
||||
private val JAVA_EPOCH_DATE_TIME = DateTime(1970, 1, 1, 0, 0, 0, DateTimeZone.UTC)
|
||||
private val EPOCH_OFFSET = (JAVA_EPOCH_DATE_TIME.millis - DOT_NET_EPOCH_DATE_TIME.millis) / 1000L
|
||||
@@ -252,27 +249,42 @@ class DateInstant : Parcelable {
|
||||
val IN_ONE_HOUR_TIME = DateInstant(
|
||||
Instant.now().plus(Duration.standardHours(1)), Type.TIME)
|
||||
|
||||
val dateTimeSecondsFormat: DateTimeFormatter =
|
||||
private val ISO8601Format: DateTimeFormatter =
|
||||
DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
||||
.withZoneUTC()
|
||||
var dateTimeFormat: DateTimeFormatter =
|
||||
private var dateTimeFormat: DateTimeFormatter =
|
||||
DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm'Z'")
|
||||
.withZoneUTC()
|
||||
var dateFormat: DateTimeFormatter =
|
||||
private var dateFormat: DateTimeFormatter =
|
||||
DateTimeFormat.forPattern("yyyy-MM-dd'Z'")
|
||||
.withZoneUTC()
|
||||
var timeFormat: DateTimeFormatter =
|
||||
private var timeFormat: DateTimeFormatter =
|
||||
DateTimeFormat.forPattern("HH:mm'Z'")
|
||||
.withZoneUTC()
|
||||
|
||||
fun fromDotNetSeconds(seconds: Long): DateInstant {
|
||||
val dt = DOT_NET_EPOCH_DATE_TIME.plus(seconds * 1000L)
|
||||
fun Long.fromDotNetSeconds(): DateInstant {
|
||||
val dt = DOT_NET_EPOCH_DATE_TIME.plus(this * 1000L)
|
||||
// Switch corrupted dates to a more recent date that won't cause issues on the client
|
||||
return DateInstant((if (dt.isBefore(JAVA_EPOCH_DATE_TIME)) { JAVA_EPOCH_DATE_TIME } else dt).toInstant())
|
||||
}
|
||||
|
||||
fun fromDateTimeSecondsFormat(value: String): DateInstant {
|
||||
return DateInstant(dateTimeSecondsFormat.parseDateTime(value).toInstant())
|
||||
fun DateInstant.toDotNetSeconds(): Long {
|
||||
val duration = Duration(JAVA_EPOCH_DATE_TIME, mInstant)
|
||||
val seconds = duration.millis / 1000L
|
||||
return seconds + EPOCH_OFFSET
|
||||
}
|
||||
|
||||
fun String.fromISO8601Format(): DateInstant {
|
||||
return DateInstant(try {
|
||||
ISO8601Format.parseDateTime(this).toInstant()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to parse date time $this", e)
|
||||
Instant.now()
|
||||
})
|
||||
}
|
||||
|
||||
fun DateInstant.toISO8601Format(): String {
|
||||
return ISO8601Format.print(this.instant)
|
||||
}
|
||||
|
||||
@JvmField
|
||||
|
||||
@@ -26,6 +26,8 @@ import com.kunzisoft.keepass.database.crypto.CipherEngine
|
||||
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.HmacBlock
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.DateInstant.Companion.fromDotNetSeconds
|
||||
import com.kunzisoft.keepass.database.element.DateInstant.Companion.fromISO8601Format
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData.Companion.BASE64_FLAG
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
@@ -829,7 +831,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
var utcDate = DateInstant()
|
||||
if (mDatabase.kdbxVersion.isBefore(FILE_VERSION_40)) {
|
||||
try {
|
||||
utcDate = DateInstant.fromDateTimeSecondsFormat(sDate)
|
||||
utcDate = sDate.fromISO8601Format()
|
||||
} catch (e: ParseException) {
|
||||
// Catch with null test below
|
||||
}
|
||||
@@ -841,7 +843,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
buf = buf8
|
||||
}
|
||||
val seconds = bytes64ToLong(buf)
|
||||
utcDate = DateInstant.fromDotNetSeconds(seconds)
|
||||
utcDate = seconds.fromDotNetSeconds()
|
||||
}
|
||||
return utcDate
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ import com.kunzisoft.encrypt.StreamCipher
|
||||
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.DateInstant.Companion.toDotNetSeconds
|
||||
import com.kunzisoft.keepass.database.element.DateInstant.Companion.toISO8601Format
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData.Companion.BASE64_FLAG
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
@@ -412,7 +414,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX)
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
private fun writeDateInstant(name: String, date: DateInstant) {
|
||||
if (header!!.version.isBefore(FILE_VERSION_40)) {
|
||||
writeString(name, date.toDateTimeSecondsFormat())
|
||||
writeString(name, date.toISO8601Format())
|
||||
} else {
|
||||
writeString(name, String(
|
||||
Base64.encode(
|
||||
|
||||
@@ -26,6 +26,7 @@ import com.kunzisoft.keepass.database.element.node.NodeHandler
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_FIELD
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import com.kunzisoft.keepass.utils.inTheSameDomainAs
|
||||
|
||||
class SearchHelper {
|
||||
|
||||
@@ -149,14 +150,14 @@ class SearchHelper {
|
||||
}
|
||||
if (searchParameters.searchInUrls) {
|
||||
if (checkSearchQuery(entry.url, searchParameters) { stringToCheck, word ->
|
||||
// domain.org
|
||||
stringToCheck.equals(word, !searchParameters.caseSensitive) ||
|
||||
// subdomain.domain.org
|
||||
stringToCheck.endsWith(".$word", !searchParameters.caseSensitive) ||
|
||||
// https://domain.org
|
||||
stringToCheck.endsWith("\\/$word", !searchParameters.caseSensitive)
|
||||
// Don't allow mydomain.org
|
||||
})
|
||||
if (searchParameters.searchByDomain) {
|
||||
try {
|
||||
stringToCheck.inTheSameDomainAs(word, sameSubDomain = true)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
} else null
|
||||
})
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInNotes) {
|
||||
@@ -187,7 +188,7 @@ class SearchHelper {
|
||||
private fun checkSearchQuery(
|
||||
stringToCheck: String,
|
||||
searchParameters: SearchParameters,
|
||||
specialComparison: ((check: String, word: String) -> Boolean)? = null): Boolean {
|
||||
specialComparison: ((check: String, word: String) -> Boolean?)? = null): Boolean {
|
||||
/*
|
||||
// TODO Search settings
|
||||
var removeAccents = true <- Too much time, to study
|
||||
@@ -204,13 +205,10 @@ class SearchHelper {
|
||||
}
|
||||
regex.matches(stringToCheck)
|
||||
} else {
|
||||
var searchFound = true
|
||||
searchParameters.searchQuery.split(" ").forEach { word ->
|
||||
searchFound = searchFound
|
||||
&& (specialComparison?.invoke(stringToCheck, word)
|
||||
?: stringToCheck.contains(word, !searchParameters.caseSensitive))
|
||||
searchParameters.searchQuery.split(" ").any { word ->
|
||||
specialComparison?.invoke(stringToCheck, word)
|
||||
?: stringToCheck.contains(word, !searchParameters.caseSensitive)
|
||||
}
|
||||
searchFound
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ class SearchParameters() : Parcelable{
|
||||
var searchInSearchableGroup = true
|
||||
var searchInRecycleBin = false
|
||||
var searchInTemplates = false
|
||||
var searchByDomain = false
|
||||
|
||||
constructor(parcel: Parcel) : this() {
|
||||
searchQuery = parcel.readString() ?: searchQuery
|
||||
|
||||
@@ -84,6 +84,10 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
&& webScheme == null
|
||||
&& otpString == null
|
||||
}
|
||||
|
||||
fun isASearchByDomain(): Boolean {
|
||||
return toString() == webDomain && webDomain != null
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
|
||||
@@ -216,6 +216,8 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
|
||||
const val MIN_OTP_DIGITS = 4
|
||||
const val MAX_OTP_DIGITS = 18
|
||||
|
||||
const val MIN_OTP_SECRET = 8
|
||||
|
||||
fun isValidCounter(counter: Long): Boolean {
|
||||
return counter in MIN_HOTP_COUNTER..MAX_HOTP_COUNTER
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import java.io.*
|
||||
import java.net.MalformedURLException
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
|
||||
fun String.parseUri(): Uri? {
|
||||
@@ -48,6 +50,48 @@ fun Context.getBinaryDir(): File {
|
||||
}
|
||||
}
|
||||
|
||||
fun String.buildURLFromDomain(): URL? {
|
||||
return try {
|
||||
URL(this)
|
||||
} catch (e: MalformedURLException) {
|
||||
try {
|
||||
URL("https://$this")
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun URL.inTheSameDomainAs(url: URL?, sameSubDomain: Boolean = false): Boolean {
|
||||
val hostA = this.host
|
||||
val hostB = url?.host
|
||||
if (hostA == null)
|
||||
return false
|
||||
if (hostB == null)
|
||||
return false
|
||||
// Each domains are equals (ie: domain.org)
|
||||
if (hostA.equals(hostB, ignoreCase = true))
|
||||
return true
|
||||
// If we need exactly the same subdomain, it's not a match
|
||||
if (sameSubDomain)
|
||||
return false
|
||||
// If contains subdomains (ie: subdomain.domain.org)
|
||||
if (hostB.endsWith(".$hostA", ignoreCase = true))
|
||||
return true
|
||||
if (hostA.endsWith(".$hostB", ignoreCase = true))
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
fun String.inTheSameDomainAs(value: String?, sameSubDomain: Boolean = false): Boolean {
|
||||
// Don't need to construct URL object if strings are equals
|
||||
if (this.equals(value, true))
|
||||
return true
|
||||
// Checks the real domains
|
||||
return this.buildURLFromDomain()
|
||||
?.inTheSameDomainAs(value?.buildURLFromDomain(), sameSubDomain) == true
|
||||
}
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun ContentResolver.getUriInputStream(fileUri: Uri?): InputStream? {
|
||||
if (fileUri == null)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.kunzisoft.keepass.tests.utils
|
||||
|
||||
import com.kunzisoft.keepass.utils.inTheSameDomainAs
|
||||
import junit.framework.TestCase
|
||||
|
||||
class UriHelperTest: TestCase() {
|
||||
|
||||
fun testBuildURL() {
|
||||
val expected = "domain.org"
|
||||
|
||||
assertTrue(expected.inTheSameDomainAs("domain.org", sameSubDomain = false))
|
||||
assertTrue(expected.inTheSameDomainAs("http://domain.org", sameSubDomain = false))
|
||||
assertTrue(expected.inTheSameDomainAs("https://domain.org", sameSubDomain = false))
|
||||
assertTrue(expected.inTheSameDomainAs("domain.org/login", sameSubDomain = false))
|
||||
assertTrue(expected.inTheSameDomainAs("http://domain.org/login", sameSubDomain = false))
|
||||
assertTrue(expected.inTheSameDomainAs("https://domain.org/login", sameSubDomain = false))
|
||||
|
||||
assertTrue(expected.inTheSameDomainAs("https://www.domain.org", sameSubDomain = false))
|
||||
assertTrue(expected.inTheSameDomainAs("www.domain.org", sameSubDomain = false))
|
||||
assertTrue(expected.inTheSameDomainAs("ww.domain.org", sameSubDomain = false))
|
||||
|
||||
assertTrue(expected.inTheSameDomainAs("domain.org", sameSubDomain = true))
|
||||
assertTrue(expected.inTheSameDomainAs("http://domain.org", sameSubDomain = true))
|
||||
assertTrue(expected.inTheSameDomainAs("https://domain.org", sameSubDomain = true))
|
||||
assertTrue(expected.inTheSameDomainAs("domain.org/login", sameSubDomain = true))
|
||||
assertTrue(expected.inTheSameDomainAs("http://domain.org/login", sameSubDomain = true))
|
||||
assertTrue(expected.inTheSameDomainAs("https://domain.org/login", sameSubDomain = true))
|
||||
|
||||
assertFalse(expected.inTheSameDomainAs("https://www.domain.org", sameSubDomain = true))
|
||||
assertFalse(expected.inTheSameDomainAs("www.domain.org", sameSubDomain = true))
|
||||
assertFalse(expected.inTheSameDomainAs("ww.domain.org", sameSubDomain = true))
|
||||
|
||||
assertFalse(expected.inTheSameDomainAs("domain.com", sameSubDomain = false))
|
||||
assertFalse(expected.inTheSameDomainAs("omain.org", sameSubDomain = false))
|
||||
assertFalse(expected.inTheSameDomainAs("odomain.org", sameSubDomain = false))
|
||||
assertFalse(expected.inTheSameDomainAs("tcp://domain.org", sameSubDomain = false))
|
||||
assertFalse(expected.inTheSameDomainAs("dom.org/domain.org", sameSubDomain = false))
|
||||
|
||||
assertFalse(expected.inTheSameDomainAs("domain.com", sameSubDomain = true))
|
||||
assertFalse(expected.inTheSameDomainAs("omain.org", sameSubDomain = true))
|
||||
assertFalse(expected.inTheSameDomainAs("odomain.org", sameSubDomain = true))
|
||||
assertFalse(expected.inTheSameDomainAs("tcp://domain.org", sameSubDomain = true))
|
||||
assertFalse(expected.inTheSameDomainAs("dom.org/domain.org", sameSubDomain = true))
|
||||
|
||||
assertFalse(expected.inTheSameDomainAs("https://example.com/domain.org", sameSubDomain = true))
|
||||
assertFalse(expected.inTheSameDomainAs("https://example.com/www.domain.org", sameSubDomain = false))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user