mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Merge branch 'develop' into feature/Passkeys
This commit is contained in:
@@ -36,7 +36,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
// Time
|
||||
implementation 'joda-time:joda-time:2.10.13'
|
||||
implementation 'joda-time:joda-time:2.13.0'
|
||||
// Apache Commons
|
||||
implementation 'commons-io:commons-io:2.8.0'
|
||||
implementation 'commons-codec:commons-codec:1.15'
|
||||
|
||||
@@ -866,8 +866,8 @@ open class Database {
|
||||
|
||||
fun createVirtualGroupFromSearch(
|
||||
searchParameters: SearchParameters,
|
||||
fromGroup: NodeId<*>? = null,
|
||||
max: Int = Integer.MAX_VALUE
|
||||
fromGroup: NodeId<*>? = null,
|
||||
max: Int = Integer.MAX_VALUE
|
||||
): Group? {
|
||||
return mSearchHelper.createVirtualGroupWithSearchResult(this,
|
||||
searchParameters, fromGroup, max)
|
||||
|
||||
@@ -21,27 +21,27 @@ package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.utils.readSerializableCompat
|
||||
import com.kunzisoft.keepass.utils.readEnum
|
||||
import com.kunzisoft.keepass.utils.readSerializableCompat
|
||||
import com.kunzisoft.keepass.utils.writeEnum
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import org.joda.time.Duration
|
||||
import org.joda.time.Instant
|
||||
import org.joda.time.LocalDate
|
||||
import org.joda.time.LocalDateTime
|
||||
import org.joda.time.LocalTime
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import org.joda.time.format.DateTimeFormat
|
||||
import org.joda.time.format.DateTimeFormatter
|
||||
|
||||
|
||||
class DateInstant : Parcelable {
|
||||
|
||||
private var jDate: Date = Date()
|
||||
private var mInstant: Instant = Instant.now()
|
||||
private var mType: Type = Type.DATE_TIME
|
||||
|
||||
val date: Date
|
||||
get() = jDate
|
||||
val instant: Instant
|
||||
get() = mInstant
|
||||
|
||||
var type: Type
|
||||
get() = mType
|
||||
@@ -50,42 +50,37 @@ class DateInstant : Parcelable {
|
||||
}
|
||||
|
||||
constructor(source: DateInstant) {
|
||||
this.jDate = Date(source.jDate.time)
|
||||
this.mInstant = Instant(source.mInstant)
|
||||
this.mType = source.mType
|
||||
}
|
||||
|
||||
constructor(date: Date, type: Type = Type.DATE_TIME) {
|
||||
jDate = Date(date.time)
|
||||
constructor(instant: Instant, type: Type = Type.DATE_TIME) {
|
||||
mInstant = Instant(instant)
|
||||
mType = type
|
||||
}
|
||||
|
||||
constructor(millis: Long, type: Type = Type.DATE_TIME) {
|
||||
jDate = Date(millis)
|
||||
mType = type
|
||||
}
|
||||
|
||||
private fun parse(value: String, type: Type): Date {
|
||||
private fun parse(value: String, type: Type): Instant {
|
||||
return when (type) {
|
||||
Type.DATE -> dateFormat.parse(value) ?: jDate
|
||||
Type.TIME -> timeFormat.parse(value) ?: jDate
|
||||
else -> dateTimeFormat.parse(value) ?: jDate
|
||||
Type.DATE -> Instant(dateFormat.parseDateTime(value) ?: DateTime())
|
||||
Type.TIME -> Instant(timeFormat.parseDateTime(value) ?: DateTime())
|
||||
else -> Instant(dateTimeFormat.parseDateTime(value) ?: DateTime())
|
||||
}
|
||||
}
|
||||
|
||||
constructor(string: String, type: Type = Type.DATE_TIME) {
|
||||
try {
|
||||
jDate = parse(string, type)
|
||||
mInstant = parse(string, type)
|
||||
mType = type
|
||||
} catch (e: Exception) {
|
||||
// Retry with second format
|
||||
try {
|
||||
when (type) {
|
||||
Type.TIME -> {
|
||||
jDate = parse(string, Type.DATE)
|
||||
mInstant = parse(string, Type.DATE)
|
||||
mType = Type.DATE
|
||||
}
|
||||
else -> {
|
||||
jDate = parse(string, Type.TIME)
|
||||
mInstant = parse(string, Type.TIME)
|
||||
mType = Type.TIME
|
||||
}
|
||||
}
|
||||
@@ -93,11 +88,11 @@ class DateInstant : Parcelable {
|
||||
// Retry with third format
|
||||
when (type) {
|
||||
Type.DATE, Type.TIME -> {
|
||||
jDate = parse(string, Type.DATE_TIME)
|
||||
mInstant = parse(string, Type.DATE_TIME)
|
||||
mType = Type.DATE_TIME
|
||||
}
|
||||
else -> {
|
||||
jDate = parse(string, Type.DATE)
|
||||
mInstant = parse(string, Type.DATE)
|
||||
mType = Type.DATE
|
||||
}
|
||||
}
|
||||
@@ -110,11 +105,11 @@ class DateInstant : Parcelable {
|
||||
}
|
||||
|
||||
constructor() {
|
||||
jDate = Date()
|
||||
mInstant = Instant.now()
|
||||
}
|
||||
|
||||
constructor(parcel: Parcel) {
|
||||
jDate = parcel.readSerializableCompat() ?: jDate
|
||||
mInstant = parcel.readSerializableCompat() ?: mInstant
|
||||
mType = parcel.readEnum<Type>() ?: mType
|
||||
}
|
||||
|
||||
@@ -123,47 +118,82 @@ class DateInstant : Parcelable {
|
||||
}
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
dest.writeSerializable(jDate)
|
||||
dest.writeSerializable(mInstant)
|
||||
dest.writeEnum(mType)
|
||||
}
|
||||
|
||||
fun getYearInt(): Int {
|
||||
val dateFormat = SimpleDateFormat("yyyy", Locale.ENGLISH)
|
||||
return dateFormat.format(date).toInt()
|
||||
fun setDate(year: Int, month: Int, day: Int) {
|
||||
mInstant = DateTime(mInstant, DateTimeZone.getDefault())
|
||||
.withYear(year)
|
||||
.withMonthOfYear(month)
|
||||
.withDayOfMonth(day)
|
||||
.toInstant()
|
||||
}
|
||||
|
||||
fun getMonthInt(): Int {
|
||||
val dateFormat = SimpleDateFormat("MM", Locale.ENGLISH)
|
||||
return dateFormat.format(date).toInt()
|
||||
fun setTime(hour: Int, minute: Int) {
|
||||
mInstant = DateTime(mInstant, DateTimeZone.getDefault())
|
||||
.withHourOfDay(hour)
|
||||
.withMinuteOfHour(minute)
|
||||
.toInstant()
|
||||
}
|
||||
|
||||
fun getYear(): Int {
|
||||
return mInstant.toDateTime().year
|
||||
}
|
||||
|
||||
fun getMonth(): Int {
|
||||
return mInstant.toDateTime().monthOfYear
|
||||
}
|
||||
|
||||
fun getDay(): Int {
|
||||
val dateFormat = SimpleDateFormat("dd", Locale.ENGLISH)
|
||||
return dateFormat.format(date).toInt()
|
||||
return mInstant.toDateTime().dayOfMonth
|
||||
}
|
||||
|
||||
fun getHour(): Int {
|
||||
return mInstant.toDateTime().hourOfDay
|
||||
}
|
||||
|
||||
fun getMinute(): Int {
|
||||
return mInstant.toDateTime().minuteOfHour
|
||||
}
|
||||
|
||||
fun getSecond(): Int {
|
||||
return mInstant.toDateTime().secondOfMinute
|
||||
}
|
||||
|
||||
// If expireDate is before NEVER_EXPIRE date less 1 month (to be sure)
|
||||
// it is not expires
|
||||
fun isNeverExpires(): Boolean {
|
||||
return LocalDateTime(jDate)
|
||||
.isBefore(
|
||||
LocalDateTime.fromDateFields(NEVER_EXPIRES.date)
|
||||
.minusMonths(1))
|
||||
return mInstant.isBefore(NEVER_EXPIRES.instant.minus(Duration.standardDays(30)))
|
||||
}
|
||||
|
||||
fun isCurrentlyExpire(): Boolean {
|
||||
return when (type) {
|
||||
Type.DATE -> LocalDate.fromDateFields(jDate).isBefore(LocalDate.now())
|
||||
Type.TIME -> LocalTime.fromDateFields(jDate).isBefore(LocalTime.now())
|
||||
else -> LocalDateTime.fromDateFields(jDate).isBefore(LocalDateTime.now())
|
||||
Type.DATE -> LocalDate.fromDateFields(mInstant.toDate()).isBefore(LocalDate.now())
|
||||
Type.TIME -> LocalTime.fromDateFields(mInstant.toDate()).isBefore(LocalTime.now())
|
||||
else -> LocalDateTime.fromDateFields(mInstant.toDate()).isBefore(LocalDateTime.now())
|
||||
}
|
||||
}
|
||||
|
||||
fun toDotNetSeconds(): Long {
|
||||
val duration = Duration(JAVA_EPOCH_DATE_TIME, mInstant)
|
||||
val seconds = duration.millis / 1000L
|
||||
return seconds + EPOCH_OFFSET
|
||||
}
|
||||
|
||||
fun toJavaMilliseconds(): Long {
|
||||
return mInstant.millis
|
||||
}
|
||||
|
||||
fun toDateTimeSecondsFormat(): String {
|
||||
return dateTimeSecondsFormat.print(mInstant)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return when (type) {
|
||||
Type.DATE -> dateFormat.format(jDate)
|
||||
Type.TIME -> timeFormat.format(jDate)
|
||||
else -> dateTimeFormat.format(jDate)
|
||||
Type.DATE -> dateFormat.print(mInstant)
|
||||
Type.TIME -> timeFormat.print(mInstant)
|
||||
else -> dateTimeFormat.print(mInstant)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,47 +201,78 @@ class DateInstant : Parcelable {
|
||||
if (this === other) return true
|
||||
if (other !is DateInstant) return false
|
||||
|
||||
if (jDate != other.jDate) return false
|
||||
if (mType != other.mType) return false
|
||||
|
||||
if (mType == Type.DATE || mType == Type.DATE_TIME) {
|
||||
if (getYear() != other.getYear()) return false
|
||||
if (getMonth() != other.getMonth()) return false
|
||||
if (getDay() != other.getDay()) return false
|
||||
if (getHour() != other.getHour()) return false
|
||||
}
|
||||
if (mType == Type.TIME || mType == Type.DATE_TIME) {
|
||||
if (getMinute() != other.getMinute()) return false
|
||||
if (getSecond() != other.getSecond()) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = jDate.hashCode()
|
||||
var result = mInstant.hashCode()
|
||||
result = 31 * result + mType.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
fun isBefore(dateInstant: DateInstant): Boolean {
|
||||
return this.mInstant.isBefore(dateInstant.mInstant)
|
||||
}
|
||||
|
||||
fun isAfter(dateInstant: DateInstant): Boolean {
|
||||
return this.mInstant.isAfter(dateInstant.mInstant)
|
||||
}
|
||||
|
||||
fun compareTo(other: DateInstant): Int {
|
||||
return mInstant.compareTo(other.mInstant)
|
||||
}
|
||||
|
||||
enum class Type {
|
||||
DATE_TIME, DATE, TIME
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val NEVER_EXPIRES = DateInstant(Calendar.getInstance().apply {
|
||||
set(Calendar.YEAR, 2999)
|
||||
set(Calendar.MONTH, 11)
|
||||
set(Calendar.DAY_OF_MONTH, 28)
|
||||
set(Calendar.HOUR, 23)
|
||||
set(Calendar.MINUTE, 59)
|
||||
set(Calendar.SECOND, 59)
|
||||
}.time)
|
||||
val IN_ONE_MONTH_DATE_TIME = DateInstant(
|
||||
Instant.now().plus(Duration.standardDays(30)).toDate(), Type.DATE_TIME)
|
||||
val IN_ONE_MONTH_DATE = DateInstant(
|
||||
Instant.now().plus(Duration.standardDays(30)).toDate(), Type.DATE)
|
||||
val IN_ONE_HOUR_TIME = DateInstant(
|
||||
Instant.now().plus(Duration.standardHours(1)).toDate(), Type.TIME)
|
||||
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
|
||||
private val NEVER_EXPIRES_DATE_TIME = DateTime(2999, 11, 28, 23, 59, 59, DateTimeZone.UTC)
|
||||
|
||||
private val dateTimeFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.ROOT).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
val NEVER_EXPIRES = DateInstant(NEVER_EXPIRES_DATE_TIME.toInstant())
|
||||
val IN_ONE_MONTH_DATE_TIME = DateInstant(
|
||||
Instant.now().plus(Duration.standardDays(30)), Type.DATE_TIME)
|
||||
val IN_ONE_MONTH_DATE = DateInstant(
|
||||
Instant.now().plus(Duration.standardDays(30)), Type.DATE)
|
||||
val IN_ONE_HOUR_TIME = DateInstant(
|
||||
Instant.now().plus(Duration.standardHours(1)), Type.TIME)
|
||||
|
||||
val dateTimeSecondsFormat: DateTimeFormatter =
|
||||
DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
||||
.withZoneUTC()
|
||||
var dateTimeFormat: DateTimeFormatter =
|
||||
DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm'Z'")
|
||||
.withZoneUTC()
|
||||
var dateFormat: DateTimeFormatter =
|
||||
DateTimeFormat.forPattern("yyyy-MM-dd'Z'")
|
||||
.withZoneUTC()
|
||||
var timeFormat: DateTimeFormatter =
|
||||
DateTimeFormat.forPattern("HH:mm'Z'")
|
||||
.withZoneUTC()
|
||||
|
||||
fun fromDotNetSeconds(seconds: Long): DateInstant {
|
||||
val dt = DOT_NET_EPOCH_DATE_TIME.plus(seconds * 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())
|
||||
}
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'Z'", Locale.ROOT).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
private val timeFormat = SimpleDateFormat("HH:mm'Z'", Locale.ROOT).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
|
||||
fun fromDateTimeSecondsFormat(value: String): DateInstant {
|
||||
return DateInstant(dateTimeSecondsFormat.parseDateTime(value).toInstant())
|
||||
}
|
||||
|
||||
@JvmField
|
||||
|
||||
@@ -26,14 +26,17 @@ import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.node.*
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.GroupInfo
|
||||
import com.kunzisoft.keepass.utils.readBooleanCompat
|
||||
import com.kunzisoft.keepass.utils.readParcelableCompat
|
||||
import com.kunzisoft.keepass.utils.writeBooleanCompat
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import java.util.UUID
|
||||
|
||||
class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
|
||||
@@ -45,8 +48,11 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
// Virtual group is used to defined a detached database group
|
||||
var isVirtual = false
|
||||
|
||||
// To optimize number of children call
|
||||
var numberOfChildEntries: Int = 0
|
||||
private set
|
||||
var recursiveNumberOfChildEntries: Int = 0
|
||||
private set
|
||||
|
||||
/**
|
||||
* Use this constructor to copy a Group
|
||||
@@ -84,20 +90,6 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
isVirtual = parcel.readBooleanCompat()
|
||||
}
|
||||
|
||||
enum class ChildFilter {
|
||||
META_STREAM, EXPIRED;
|
||||
|
||||
companion object {
|
||||
fun getDefaults(showExpiredEntries: Boolean): Array<ChildFilter> {
|
||||
return if (showExpiredEntries) {
|
||||
arrayOf(META_STREAM)
|
||||
} else {
|
||||
arrayOf(META_STREAM, EXPIRED)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<Group> {
|
||||
override fun createFromParcel(parcel: Parcel): Group {
|
||||
return Group(parcel)
|
||||
@@ -280,20 +272,6 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
ArrayList()
|
||||
}
|
||||
|
||||
fun getFilteredChildGroups(filters: Array<ChildFilter>): List<Group> {
|
||||
return groupKDB?.getChildGroups()?.map {
|
||||
Group(it).apply {
|
||||
this.refreshNumberOfChildEntries(filters)
|
||||
}
|
||||
} ?:
|
||||
groupKDBX?.getChildGroups()?.map {
|
||||
Group(it).apply {
|
||||
this.refreshNumberOfChildEntries(filters)
|
||||
}
|
||||
} ?:
|
||||
ArrayList()
|
||||
}
|
||||
|
||||
override fun getChildEntries(): List<Entry> {
|
||||
return groupKDB?.getChildEntries()?.map {
|
||||
Entry(it)
|
||||
@@ -312,53 +290,32 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
return entriesInfo
|
||||
}
|
||||
|
||||
fun getFilteredChildEntries(filters: Array<ChildFilter>): List<Entry> {
|
||||
val withoutMetaStream = filters.contains(ChildFilter.META_STREAM)
|
||||
val showExpiredEntries = !filters.contains(ChildFilter.EXPIRED)
|
||||
|
||||
// TODO Change KDB parser to remove meta entries
|
||||
return groupKDB?.getChildEntries()?.filter {
|
||||
(!withoutMetaStream || (withoutMetaStream && !it.isMetaStream()))
|
||||
&& (!it.isCurrentlyExpires or showExpiredEntries)
|
||||
}?.map {
|
||||
Entry(it)
|
||||
} ?:
|
||||
groupKDBX?.getChildEntries()?.filter {
|
||||
!it.isCurrentlyExpires or showExpiredEntries
|
||||
}?.map {
|
||||
Entry(it)
|
||||
} ?:
|
||||
ArrayList()
|
||||
}
|
||||
|
||||
fun refreshNumberOfChildEntries(filters: Array<ChildFilter> = emptyArray()) {
|
||||
this.numberOfChildEntries = getFilteredChildEntries(filters).size
|
||||
this.recursiveNumberOfChildEntries = getFilteredChildEntriesInGroups(filters)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the cumulative number of entries in the current group and its children
|
||||
*/
|
||||
private fun getFilteredChildEntriesInGroups(filters: Array<ChildFilter>): Int {
|
||||
private fun getNumberOfChildEntriesInGroups(filter: (Node) -> Boolean): Int {
|
||||
var counter = 0
|
||||
getChildGroups().forEach { childGroup ->
|
||||
counter += childGroup.getFilteredChildEntriesInGroups(filters)
|
||||
getChildGroups().filter(filter).forEach { childGroup ->
|
||||
counter += childGroup.getNumberOfChildEntriesInGroups(filter)
|
||||
}
|
||||
return getFilteredChildEntries(filters).size + counter
|
||||
return getChildEntries().filter(filter).size + counter
|
||||
}
|
||||
|
||||
fun getNumberOfChildEntries(
|
||||
recursive: Boolean = false,
|
||||
filter: (Node) -> Boolean = { true }
|
||||
): Int {
|
||||
numberOfChildEntries = getChildEntries().filter(filter).size
|
||||
recursiveNumberOfChildEntries = getNumberOfChildEntriesInGroups(filter)
|
||||
return if (recursive) recursiveNumberOfChildEntries else numberOfChildEntries
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter entries and return children
|
||||
* @return List of direct children (one level below) as NodeVersioned
|
||||
*/
|
||||
fun getChildren(): List<Node> {
|
||||
return getChildGroups() + getChildEntries()
|
||||
}
|
||||
|
||||
fun getFilteredChildren(filters: Array<ChildFilter>): List<Node> {
|
||||
val nodes = getFilteredChildGroups(filters) + getFilteredChildEntries(filters)
|
||||
refreshNumberOfChildEntries(filters)
|
||||
return nodes
|
||||
fun getChildren(filter: ((Node) -> Boolean) = { true }): List<Node> {
|
||||
return getChildGroups().filter(filter) + getChildEntries().filter(filter)
|
||||
}
|
||||
|
||||
override fun addChildGroup(group: Group) {
|
||||
|
||||
@@ -23,7 +23,6 @@ package com.kunzisoft.keepass.database.element
|
||||
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import java.util.*
|
||||
|
||||
enum class SortNodeEnum {
|
||||
DB, TITLE, USERNAME, CREATION_TIME, LAST_MODIFY_TIME, LAST_ACCESS_TIME;
|
||||
@@ -173,8 +172,8 @@ enum class SortNodeEnum {
|
||||
) : NodeComparator<G, T>(database, sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
val creationCompare = object1.creationTime.date
|
||||
.compareTo(object2.creationTime.date)
|
||||
val creationCompare = object1.creationTime
|
||||
.compareTo(object2.creationTime)
|
||||
return if (creationCompare == 0)
|
||||
NodeNaturalComparator<G, T>(database, sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
@@ -192,8 +191,8 @@ enum class SortNodeEnum {
|
||||
) : NodeComparator<G, T>(database, sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
val lastModificationCompare = object1.lastModificationTime.date
|
||||
.compareTo(object2.lastModificationTime.date)
|
||||
val lastModificationCompare = object1.lastModificationTime
|
||||
.compareTo(object2.lastModificationTime)
|
||||
return if (lastModificationCompare == 0)
|
||||
NodeNaturalComparator<G, T>(database, sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
@@ -211,8 +210,8 @@ enum class SortNodeEnum {
|
||||
) : NodeComparator<G, T>(database, sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
val lastAccessCompare = object1.lastAccessTime.date
|
||||
.compareTo(object2.lastAccessTime.date)
|
||||
val lastAccessCompare = object1.lastAccessTime
|
||||
.compareTo(object2.lastAccessTime)
|
||||
return if (lastAccessCompare == 0)
|
||||
NodeNaturalComparator<G, T>(database, sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
|
||||
@@ -36,13 +36,12 @@ import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.utils.UnsignedLong
|
||||
import com.kunzisoft.keepass.utils.readParcelableCompat
|
||||
import com.kunzisoft.keepass.utils.readStringIntMap
|
||||
import com.kunzisoft.keepass.utils.readStringParcelableMap
|
||||
import com.kunzisoft.keepass.utils.writeStringIntMap
|
||||
import com.kunzisoft.keepass.utils.writeStringParcelableMap
|
||||
import com.kunzisoft.keepass.utils.UnsignedLong
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
|
||||
class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
|
||||
@@ -363,18 +362,16 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
}
|
||||
|
||||
fun removeOldestEntryFromHistory(): EntryKDBX? {
|
||||
var min: Date? = null
|
||||
var min: DateInstant? = null
|
||||
var index = -1
|
||||
|
||||
for (i in history.indices) {
|
||||
val entry = history[i]
|
||||
val lastMod = entry.lastModificationTime.date
|
||||
if (min == null || lastMod.before(min)) {
|
||||
val lastModification = entry.lastModificationTime
|
||||
if (min == null || lastModification.isBefore(min)) {
|
||||
index = i
|
||||
min = lastMod
|
||||
min = lastModification
|
||||
}
|
||||
}
|
||||
|
||||
return if (index != -1) {
|
||||
history.removeAt(index)
|
||||
} else null
|
||||
|
||||
@@ -154,7 +154,10 @@ class Template : Parcelable {
|
||||
val EXPIRATION_ATTRIBUTE = TemplateAttribute(
|
||||
TemplateField.LABEL_EXPIRATION,
|
||||
TemplateAttributeType.DATETIME,
|
||||
false)
|
||||
false,
|
||||
TemplateAttributeOption().apply {
|
||||
setExpirable(true)
|
||||
})
|
||||
val NOTES_ATTRIBUTE = TemplateAttribute(
|
||||
TemplateField.LABEL_NOTES,
|
||||
TemplateAttributeType.TEXT,
|
||||
|
||||
@@ -152,6 +152,18 @@ class TemplateAttributeOption() : Parcelable {
|
||||
mOptions[DATETIME_FORMAT_ATTR] = DATETIME_FORMAT_VALUE_TIME
|
||||
}
|
||||
|
||||
fun getExpirable(): Boolean {
|
||||
return try {
|
||||
mOptions[DATETIME_EXPIRABLE_ATTR]?.toBoolean() ?: DATETIME_EXPIRABLE_VALUE_DEFAULT
|
||||
} catch (e: Exception) {
|
||||
DATETIME_EXPIRABLE_VALUE_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
fun setExpirable(value: Boolean) {
|
||||
mOptions[DATETIME_EXPIRABLE_ATTR] = value.toString().lowercase()
|
||||
}
|
||||
|
||||
fun get(label: String): String? {
|
||||
return mOptions[label]
|
||||
}
|
||||
@@ -246,6 +258,15 @@ class TemplateAttributeOption() : Parcelable {
|
||||
private const val DATETIME_FORMAT_VALUE_DATE = "date"
|
||||
private const val DATETIME_FORMAT_VALUE_TIME = "time"
|
||||
|
||||
/**
|
||||
* Applicable to type DATETIME
|
||||
* Define if a datetime is expirable
|
||||
* Boolean ("true" or "false")
|
||||
* "false" if not defined
|
||||
*/
|
||||
private const val DATETIME_EXPIRABLE_ATTR = "expirable"
|
||||
private const val DATETIME_EXPIRABLE_VALUE_DEFAULT = false
|
||||
|
||||
private fun removeSpecialChars(string: String): String {
|
||||
return string.filterNot { "{,:}".indexOf(it) > -1 }
|
||||
}
|
||||
|
||||
@@ -216,6 +216,11 @@ class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database)
|
||||
attribute.options.associatePasswordGenerator()
|
||||
}
|
||||
|
||||
// Add expiration
|
||||
if (attribute.label.equals(TEMPLATE_ATTRIBUTE_EXPIRES, true)) {
|
||||
attribute.options.setExpirable(true)
|
||||
}
|
||||
|
||||
// Add default value
|
||||
if (defaultValues.containsKey(attribute.label)) {
|
||||
attribute.options.default = defaultValues[attribute.label]!!
|
||||
|
||||
@@ -19,9 +19,6 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.file
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
object DatabaseKDBXXML {
|
||||
|
||||
const val ElemDocNode = "KeePassFile"
|
||||
@@ -128,8 +125,4 @@ object DatabaseKDBXXML {
|
||||
|
||||
const val ElemCustomData = "CustomData"
|
||||
const val ElemStringDictExItem = "Item"
|
||||
|
||||
val DateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX 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.
|
||||
*
|
||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.file
|
||||
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import org.joda.time.Duration
|
||||
import java.util.*
|
||||
|
||||
object DateKDBXUtil {
|
||||
|
||||
private val dotNetEpoch = DateTime(1, 1, 1, 0, 0, 0, DateTimeZone.UTC)
|
||||
private val javaEpoch = DateTime(1970, 1, 1, 0, 0, 0, DateTimeZone.UTC)
|
||||
private val epochOffset = (javaEpoch.millis - dotNetEpoch.millis) / 1000L
|
||||
|
||||
fun convertKDBX4Time(seconds: Long): Date {
|
||||
val dt = dotNetEpoch.plus(seconds * 1000L)
|
||||
// Switch corrupted dates to a more recent date that won't cause issues on the client
|
||||
return if (dt.isBefore(javaEpoch)) {
|
||||
javaEpoch.toDate()
|
||||
} else dt.toDate()
|
||||
}
|
||||
|
||||
fun convertDateToKDBX4Time(date: Date): Long {
|
||||
val duration = Duration(javaEpoch, DateTime(date))
|
||||
val seconds = duration.millis / 1000L
|
||||
return seconds + epochOffset
|
||||
}
|
||||
}
|
||||
@@ -165,7 +165,7 @@ class DatabaseInputKDB(database: DatabaseKDB)
|
||||
}
|
||||
0x0003 -> {
|
||||
newGroup?.let { group ->
|
||||
group.creationTime = DateInstant(cipherInputStream.readBytes5ToDate())
|
||||
group.creationTime = cipherInputStream.readBytes5ToDate()
|
||||
} ?:
|
||||
newEntry?.let { entry ->
|
||||
var iconId = cipherInputStream.readBytes4ToUInt().toKotlinInt()
|
||||
@@ -178,7 +178,7 @@ class DatabaseInputKDB(database: DatabaseKDB)
|
||||
}
|
||||
0x0004 -> {
|
||||
newGroup?.let { group ->
|
||||
group.lastModificationTime = DateInstant(cipherInputStream.readBytes5ToDate())
|
||||
group.lastModificationTime = cipherInputStream.readBytes5ToDate()
|
||||
} ?:
|
||||
newEntry?.let { entry ->
|
||||
entry.title = cipherInputStream.readBytesToString(fieldSize)
|
||||
@@ -186,7 +186,7 @@ class DatabaseInputKDB(database: DatabaseKDB)
|
||||
}
|
||||
0x0005 -> {
|
||||
newGroup?.let { group ->
|
||||
group.lastAccessTime = DateInstant(cipherInputStream.readBytes5ToDate())
|
||||
group.lastAccessTime = cipherInputStream.readBytes5ToDate()
|
||||
} ?:
|
||||
newEntry?.let { entry ->
|
||||
entry.url = cipherInputStream.readBytesToString(fieldSize)
|
||||
@@ -194,7 +194,7 @@ class DatabaseInputKDB(database: DatabaseKDB)
|
||||
}
|
||||
0x0006 -> {
|
||||
newGroup?.let { group ->
|
||||
group.expiryTime = DateInstant(cipherInputStream.readBytes5ToDate())
|
||||
group.expiryTime = cipherInputStream.readBytes5ToDate()
|
||||
} ?:
|
||||
newEntry?.let { entry ->
|
||||
entry.username = cipherInputStream.readBytesToString(fieldSize)
|
||||
@@ -221,22 +221,22 @@ class DatabaseInputKDB(database: DatabaseKDB)
|
||||
group.groupFlags = cipherInputStream.readBytes4ToUInt().toKotlinInt()
|
||||
} ?:
|
||||
newEntry?.let { entry ->
|
||||
entry.creationTime = DateInstant(cipherInputStream.readBytes5ToDate())
|
||||
entry.creationTime = cipherInputStream.readBytes5ToDate()
|
||||
}
|
||||
}
|
||||
0x000A -> {
|
||||
newEntry?.let { entry ->
|
||||
entry.lastModificationTime = DateInstant(cipherInputStream.readBytes5ToDate())
|
||||
entry.lastModificationTime = cipherInputStream.readBytes5ToDate()
|
||||
}
|
||||
}
|
||||
0x000B -> {
|
||||
newEntry?.let { entry ->
|
||||
entry.lastAccessTime = DateInstant(cipherInputStream.readBytes5ToDate())
|
||||
entry.lastAccessTime = cipherInputStream.readBytes5ToDate()
|
||||
}
|
||||
}
|
||||
0x000C -> {
|
||||
newEntry?.let { entry ->
|
||||
entry.expiryTime = DateInstant(cipherInputStream.readBytes5ToDate())
|
||||
entry.expiryTime = cipherInputStream.readBytes5ToDate()
|
||||
}
|
||||
}
|
||||
0x000D -> {
|
||||
|
||||
@@ -42,7 +42,6 @@ import com.kunzisoft.keepass.database.exception.*
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
|
||||
import com.kunzisoft.keepass.database.file.DatabaseKDBXXML
|
||||
import com.kunzisoft.keepass.database.file.DateKDBXUtil
|
||||
import com.kunzisoft.keepass.stream.HashedBlockInputStream
|
||||
import com.kunzisoft.keepass.stream.HmacBlockInputStream
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
@@ -827,11 +826,10 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
private fun readDateInstant(xpp: XmlPullParser): DateInstant {
|
||||
val sDate = readString(xpp)
|
||||
var utcDate: Date? = null
|
||||
|
||||
var utcDate = DateInstant()
|
||||
if (mDatabase.kdbxVersion.isBefore(FILE_VERSION_40)) {
|
||||
try {
|
||||
utcDate = DatabaseKDBXXML.DateFormatter.parse(sDate)
|
||||
utcDate = DateInstant.fromDateTimeSecondsFormat(sDate)
|
||||
} catch (e: ParseException) {
|
||||
// Catch with null test below
|
||||
}
|
||||
@@ -842,12 +840,10 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
System.arraycopy(buf, 0, buf8, 0, min(buf.size, 8))
|
||||
buf = buf8
|
||||
}
|
||||
|
||||
val seconds = bytes64ToLong(buf)
|
||||
utcDate = DateKDBXUtil.convertKDBX4Time(seconds)
|
||||
utcDate = DateInstant.fromDotNetSeconds(seconds)
|
||||
}
|
||||
|
||||
return DateInstant(utcDate ?: Date(0L))
|
||||
return utcDate
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
|
||||
@@ -41,7 +41,6 @@ import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
|
||||
import com.kunzisoft.keepass.database.file.DatabaseKDBXXML
|
||||
import com.kunzisoft.keepass.database.file.DateKDBXUtil
|
||||
import com.kunzisoft.keepass.stream.HashedBlockOutputStream
|
||||
import com.kunzisoft.keepass.stream.HmacBlockOutputStream
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
@@ -411,14 +410,15 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX)
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
private fun writeDateInstant(name: String, value: DateInstant) {
|
||||
val date = value.date
|
||||
private fun writeDateInstant(name: String, date: DateInstant) {
|
||||
if (header!!.version.isBefore(FILE_VERSION_40)) {
|
||||
writeString(name, DatabaseKDBXXML.DateFormatter.format(date))
|
||||
writeString(name, date.toDateTimeSecondsFormat())
|
||||
} else {
|
||||
val buf = longTo8Bytes(DateKDBXUtil.convertDateToKDBX4Time(date))
|
||||
val b64 = String(Base64.encode(buf, BASE64_FLAG))
|
||||
writeString(name, b64)
|
||||
writeString(name, String(
|
||||
Base64.encode(
|
||||
longTo8Bytes(date.toDotNetSeconds()), BASE64_FLAG)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,16 +75,16 @@ class EntryOutputKDB(private val mDatabase: DatabaseKDB,
|
||||
writeStringToStream(mOutputStream, mEntry.notes)
|
||||
|
||||
// Create date
|
||||
writeDate(CREATE_FIELD_TYPE, dateTo5Bytes(mEntry.creationTime.date))
|
||||
writeDate(CREATE_FIELD_TYPE, dateTo5Bytes(mEntry.creationTime))
|
||||
|
||||
// Modification date
|
||||
writeDate(MOD_FIELD_TYPE, dateTo5Bytes(mEntry.lastModificationTime.date))
|
||||
writeDate(MOD_FIELD_TYPE, dateTo5Bytes(mEntry.lastModificationTime))
|
||||
|
||||
// Access date
|
||||
writeDate(ACCESS_FIELD_TYPE, dateTo5Bytes(mEntry.lastAccessTime.date))
|
||||
writeDate(ACCESS_FIELD_TYPE, dateTo5Bytes(mEntry.lastAccessTime))
|
||||
|
||||
// Expiration date
|
||||
writeDate(EXPIRE_FIELD_TYPE, dateTo5Bytes(mEntry.expiryTime.date))
|
||||
writeDate(EXPIRE_FIELD_TYPE, dateTo5Bytes(mEntry.expiryTime))
|
||||
|
||||
// Binary description
|
||||
mOutputStream.write(BINARY_DESC_FIELD_TYPE)
|
||||
|
||||
@@ -48,22 +48,22 @@ class GroupOutputKDB(private val mGroup: GroupKDB,
|
||||
// Create date
|
||||
mOutputStream.write(CREATE_FIELD_TYPE)
|
||||
mOutputStream.write(DATE_FIELD_SIZE)
|
||||
mOutputStream.write(dateTo5Bytes(mGroup.creationTime.date))
|
||||
mOutputStream.write(dateTo5Bytes(mGroup.creationTime))
|
||||
|
||||
// Modification date
|
||||
mOutputStream.write(MOD_FIELD_TYPE)
|
||||
mOutputStream.write(DATE_FIELD_SIZE)
|
||||
mOutputStream.write(dateTo5Bytes(mGroup.lastModificationTime.date))
|
||||
mOutputStream.write(dateTo5Bytes(mGroup.lastModificationTime))
|
||||
|
||||
// Access date
|
||||
mOutputStream.write(ACCESS_FIELD_TYPE)
|
||||
mOutputStream.write(DATE_FIELD_SIZE)
|
||||
mOutputStream.write(dateTo5Bytes(mGroup.lastAccessTime.date))
|
||||
mOutputStream.write(dateTo5Bytes(mGroup.lastAccessTime))
|
||||
|
||||
// Expiration date
|
||||
mOutputStream.write(EXPIRE_FIELD_TYPE)
|
||||
mOutputStream.write(DATE_FIELD_SIZE)
|
||||
mOutputStream.write(dateTo5Bytes(mGroup.expiryTime.date))
|
||||
mOutputStream.write(dateTo5Bytes(mGroup.expiryTime))
|
||||
|
||||
// Image ID
|
||||
mOutputStream.write(IMAGEID_FIELD_TYPE)
|
||||
|
||||
@@ -187,34 +187,34 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
|
||||
fun merge(databaseToMerge: DatabaseKDBX) {
|
||||
|
||||
// Merge settings
|
||||
if (database.nameChanged.date.before(databaseToMerge.nameChanged.date)) {
|
||||
if (database.nameChanged.isBefore(databaseToMerge.nameChanged)) {
|
||||
database.name = databaseToMerge.name
|
||||
database.nameChanged = databaseToMerge.nameChanged
|
||||
}
|
||||
if (database.descriptionChanged.date.before(databaseToMerge.descriptionChanged.date)) {
|
||||
if (database.descriptionChanged.isBefore(databaseToMerge.descriptionChanged)) {
|
||||
database.description = databaseToMerge.description
|
||||
database.descriptionChanged = databaseToMerge.descriptionChanged
|
||||
}
|
||||
if (database.defaultUserNameChanged.date.before(databaseToMerge.defaultUserNameChanged.date)) {
|
||||
if (database.defaultUserNameChanged.isBefore(databaseToMerge.defaultUserNameChanged)) {
|
||||
database.defaultUserName = databaseToMerge.defaultUserName
|
||||
database.defaultUserNameChanged = databaseToMerge.defaultUserNameChanged
|
||||
}
|
||||
if (database.keyLastChanged.date.before(databaseToMerge.keyLastChanged.date)) {
|
||||
if (database.keyLastChanged.isBefore(databaseToMerge.keyLastChanged)) {
|
||||
database.keyChangeRecDays = databaseToMerge.keyChangeRecDays
|
||||
database.keyChangeForceDays = databaseToMerge.keyChangeForceDays
|
||||
database.isKeyChangeForceOnce = databaseToMerge.isKeyChangeForceOnce
|
||||
database.keyLastChanged = databaseToMerge.keyLastChanged
|
||||
}
|
||||
if (database.recycleBinChanged.date.before(databaseToMerge.recycleBinChanged.date)) {
|
||||
if (database.recycleBinChanged.isBefore(databaseToMerge.recycleBinChanged)) {
|
||||
database.isRecycleBinEnabled = databaseToMerge.isRecycleBinEnabled
|
||||
database.recycleBinUUID = databaseToMerge.recycleBinUUID
|
||||
database.recycleBinChanged = databaseToMerge.recycleBinChanged
|
||||
}
|
||||
if (database.entryTemplatesGroupChanged.date.before(databaseToMerge.entryTemplatesGroupChanged.date)) {
|
||||
if (database.entryTemplatesGroupChanged.isBefore(databaseToMerge.entryTemplatesGroupChanged)) {
|
||||
database.entryTemplatesGroup = databaseToMerge.entryTemplatesGroup
|
||||
database.entryTemplatesGroupChanged = databaseToMerge.entryTemplatesGroupChanged
|
||||
}
|
||||
if (database.settingsChanged.date.before(databaseToMerge.settingsChanged.date)) {
|
||||
if (database.settingsChanged.isBefore(databaseToMerge.settingsChanged)) {
|
||||
database.color = databaseToMerge.color
|
||||
database.compressionAlgorithm = databaseToMerge.compressionAlgorithm
|
||||
database.historyMaxItems = databaseToMerge.historyMaxItems
|
||||
@@ -245,8 +245,7 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
|
||||
}
|
||||
|
||||
// Merge root group
|
||||
if (rootGroup.lastModificationTime.date
|
||||
.before(rootGroupToMerge.lastModificationTime.date)) {
|
||||
if (rootGroup.lastModificationTime.isBefore(rootGroupToMerge.lastModificationTime)) {
|
||||
rootGroup.updateWith(rootGroupToMerge, updateParents = false)
|
||||
}
|
||||
// Merge children
|
||||
@@ -293,7 +292,7 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
|
||||
val customIconToMerge = databaseToMerge.iconsManager.getIcon(customIconUuid)
|
||||
val customIconModificationToMerge = customIconToMerge?.lastModificationTime
|
||||
if (customIconModification != null && customIconModificationToMerge != null) {
|
||||
if (customIconModification.date.before(customIconModificationToMerge.date)) {
|
||||
if (customIconModification.isBefore(customIconModificationToMerge)) {
|
||||
customIcon.updateWith(customIconToMerge)
|
||||
}
|
||||
} else if (customIconModificationToMerge != null) {
|
||||
@@ -310,19 +309,17 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
|
||||
val databaseIcon = database.iconsManager.getIcon(deletedObjectId)
|
||||
val databaseIconModificationTime = databaseIcon?.lastModificationTime
|
||||
if (databaseEntry != null
|
||||
&& deletedObject.deletionTime.date
|
||||
.after(databaseEntry.lastModificationTime.date)) {
|
||||
&& deletedObject.deletionTime.isAfter(databaseEntry.lastModificationTime)) {
|
||||
database.removeEntryFrom(databaseEntry, databaseEntry.parent)
|
||||
}
|
||||
if (databaseGroup != null
|
||||
&& deletedObject.deletionTime.date
|
||||
.after(databaseGroup.lastModificationTime.date)) {
|
||||
&& deletedObject.deletionTime.isAfter(databaseGroup.lastModificationTime)) {
|
||||
database.removeGroupFrom(databaseGroup, databaseGroup.parent)
|
||||
}
|
||||
if (databaseIcon != null
|
||||
&& (
|
||||
databaseIconModificationTime == null
|
||||
|| (deletedObject.deletionTime.date.after(databaseIconModificationTime.date))
|
||||
|| (deletedObject.deletionTime.isAfter(databaseIconModificationTime))
|
||||
)
|
||||
) {
|
||||
database.removeCustomIcon(deletedObjectId)
|
||||
@@ -343,8 +340,7 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
|
||||
val customDataItemModification = customDataItem.lastModificationTime
|
||||
val customDataItemToMergeModification = customDataItemToMerge.lastModificationTime
|
||||
if (customDataItemModification != null && customDataItemToMergeModification != null) {
|
||||
if (customDataItemModification.date
|
||||
.before(customDataItemToMergeModification.date)) {
|
||||
if (customDataItemModification.isBefore(customDataItemToMergeModification)) {
|
||||
customData.put(customDataItemToMerge)
|
||||
}
|
||||
} else {
|
||||
@@ -399,8 +395,7 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
|
||||
// If it's a deleted object, but another instance was updated
|
||||
// If entry parent to add exists and in current database
|
||||
if ((deletedObject == null
|
||||
|| deletedObject.deletionTime.date
|
||||
.before(entryToMerge.lastModificationTime.date))
|
||||
|| deletedObject.deletionTime.isBefore(entryToMerge.lastModificationTime))
|
||||
&& parentEntryToMerge != null) {
|
||||
database.addEntryTo(entryToMerge, parentEntryToMerge)
|
||||
}
|
||||
@@ -408,8 +403,7 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
|
||||
// Merge independently custom data
|
||||
mergeCustomData(entry.customData, entryToMerge.customData)
|
||||
// Merge by modification time
|
||||
if (entry.lastModificationTime.date
|
||||
.before(entryToMerge.lastModificationTime.date)
|
||||
if (entry.lastModificationTime.isBefore(entryToMerge.lastModificationTime)
|
||||
) {
|
||||
addHistory(entry, entryToMerge)
|
||||
if (parentEntryToMerge == entry.parent) {
|
||||
@@ -421,8 +415,7 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
|
||||
database.addEntryTo(entryToMerge, parentEntryToMerge)
|
||||
}
|
||||
}
|
||||
} else if (entry.lastModificationTime.date
|
||||
.after(entryToMerge.lastModificationTime.date)
|
||||
} else if (entry.lastModificationTime.isAfter(entryToMerge.lastModificationTime)
|
||||
) {
|
||||
addHistory(entryToMerge, entry)
|
||||
}
|
||||
@@ -477,8 +470,7 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
|
||||
if (group == null) {
|
||||
// If group parent to add exists and in current database
|
||||
if ((deletedObject == null
|
||||
|| deletedObject.deletionTime.date
|
||||
.before(groupToMerge.lastModificationTime.date))
|
||||
|| deletedObject.deletionTime.isBefore(groupToMerge.lastModificationTime))
|
||||
&& parentGroupToMerge != null) {
|
||||
database.addGroupTo(groupToMerge, parentGroupToMerge)
|
||||
}
|
||||
@@ -486,8 +478,7 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
|
||||
// Merge independently custom data
|
||||
mergeCustomData(group.customData, groupToMerge.customData)
|
||||
// Merge by modification time
|
||||
if (group.lastModificationTime.date
|
||||
.before(groupToMerge.lastModificationTime.date)
|
||||
if (group.lastModificationTime.isBefore(groupToMerge.lastModificationTime)
|
||||
) {
|
||||
if (parentGroupToMerge == group.parent) {
|
||||
group.updateWith(groupToMerge, false)
|
||||
|
||||
@@ -78,7 +78,7 @@ class SearchHelper {
|
||||
)
|
||||
}
|
||||
|
||||
searchGroup?.refreshNumberOfChildEntries()
|
||||
searchGroup?.getNumberOfChildEntries()
|
||||
return searchGroup
|
||||
}
|
||||
|
||||
@@ -148,7 +148,15 @@ class SearchHelper {
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInUrls) {
|
||||
if (checkSearchQuery(entry.url, searchParameters))
|
||||
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
|
||||
})
|
||||
return true
|
||||
}
|
||||
if (searchParameters.searchInNotes) {
|
||||
@@ -176,7 +184,10 @@ class SearchHelper {
|
||||
return false
|
||||
}
|
||||
|
||||
private fun checkSearchQuery(stringToCheck: String, searchParameters: SearchParameters): Boolean {
|
||||
private fun checkSearchQuery(
|
||||
stringToCheck: String,
|
||||
searchParameters: SearchParameters,
|
||||
specialComparison: ((check: String, word: String) -> Boolean)? = null): Boolean {
|
||||
/*
|
||||
// TODO Search settings
|
||||
var removeAccents = true <- Too much time, to study
|
||||
@@ -196,7 +207,8 @@ class SearchHelper {
|
||||
var searchFound = true
|
||||
searchParameters.searchQuery.split(" ").forEach { word ->
|
||||
searchFound = searchFound
|
||||
&& stringToCheck.contains(word, !searchParameters.caseSensitive)
|
||||
&& (specialComparison?.invoke(stringToCheck, word)
|
||||
?: stringToCheck.contains(word, !searchParameters.caseSensitive))
|
||||
}
|
||||
searchFound
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ class EntryInfo : NodeInfo {
|
||||
}
|
||||
creditCard?.expiration?.let {
|
||||
expires = true
|
||||
expiryTime = DateInstant(creditCard.expiration.millis)
|
||||
expiryTime = DateInstant(creditCard.expiration.toInstant())
|
||||
}
|
||||
creditCard?.number?.let {
|
||||
addUniqueField(Field(TemplateField.LABEL_NUMBER, ProtectedString(false, it)))
|
||||
|
||||
@@ -22,12 +22,15 @@ 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.database.element.Field
|
||||
import com.kunzisoft.keepass.otp.TokenCalculator.*
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.otp.TokenCalculator.HOTP_INITIAL_COUNTER
|
||||
import com.kunzisoft.keepass.otp.TokenCalculator.HashAlgorithm
|
||||
import com.kunzisoft.keepass.otp.TokenCalculator.OTP_DEFAULT_DIGITS
|
||||
import com.kunzisoft.keepass.otp.TokenCalculator.TOTP_DEFAULT_PERIOD
|
||||
import com.kunzisoft.keepass.utils.StringUtil.removeLineChars
|
||||
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object OtpEntryFields {
|
||||
@@ -40,6 +43,7 @@ object OtpEntryFields {
|
||||
// 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 STEAM_AUTHORITY = "steam" // time-based
|
||||
private const val HOTP_AUTHORITY = "hotp" // counter-based
|
||||
private const val ALGORITHM_URL_PARAM = "algorithm"
|
||||
private const val ISSUER_URL_PARAM = "issuer"
|
||||
@@ -50,7 +54,9 @@ object OtpEntryFields {
|
||||
private const val COUNTER_URL_PARAM = "counter"
|
||||
|
||||
// OTPauth URI
|
||||
private const val REGEX_OTP_AUTH = "^otpauth://([ht]otp)/?(?:([^:?#]*): *)?([^:?#]*)\\?([^#]+)$"
|
||||
private const val REGEX_OTP_AUTH = "^otpauth://(" +
|
||||
"$TOTP_AUTHORITY|$STEAM_AUTHORITY|$HOTP_AUTHORITY" +
|
||||
")/?(?:([^:?#]*): *)?([^:?#]*)\\?([^#]+)$"
|
||||
|
||||
// Key-values (maybe from plugin or old KeePassXC)
|
||||
private const val SEED_KEY = "key"
|
||||
@@ -116,9 +122,7 @@ object OtpEntryFields {
|
||||
* Tell if [otpUri] is a valid Otp URI
|
||||
*/
|
||||
fun isOTPUri(otpUri: String): Boolean {
|
||||
if (Pattern.matches(REGEX_OTP_AUTH, otpUri))
|
||||
return true
|
||||
return false
|
||||
return Pattern.matches(REGEX_OTP_AUTH, otpUri)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,7 +139,7 @@ object OtpEntryFields {
|
||||
* Parses a secret value from a URI. The format will be:
|
||||
*
|
||||
* otpauth://totp/user@example.com?secret=FFF...
|
||||
*
|
||||
* otpauth://steam/user@example.com?secret=FFF...
|
||||
* otpauth://hotp/user@example.com?secret=FFF...&counter=123
|
||||
*/
|
||||
private fun parseOTPUri(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
|
||||
@@ -149,7 +153,7 @@ object OtpEntryFields {
|
||||
}
|
||||
|
||||
val authority = uri.authority
|
||||
if (TOTP_AUTHORITY == authority) {
|
||||
if (TOTP_AUTHORITY == authority || STEAM_AUTHORITY == authority) {
|
||||
otpElement.type = OtpType.TOTP
|
||||
|
||||
} else if (HOTP_AUTHORITY == authority) {
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.utils
|
||||
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.Instant
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
@@ -93,7 +96,7 @@ fun InputStream.readBytes2ToUShort(): Int {
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun InputStream.readBytes5ToDate(): Date {
|
||||
fun InputStream.readBytes5ToDate(): DateInstant {
|
||||
return bytes5ToDate(readBytesLength(5))
|
||||
}
|
||||
|
||||
@@ -211,7 +214,7 @@ fun bytes16ToUuid(buf: ByteArray): UUID {
|
||||
* Unpack date from 5 byte format. The five bytes at 'offset' are unpacked
|
||||
* to a java.util.Date instance.
|
||||
*/
|
||||
fun bytes5ToDate(buf: ByteArray, calendar: Calendar = Calendar.getInstance()): Date {
|
||||
fun bytes5ToDate(buf: ByteArray): DateInstant {
|
||||
val dateSize = 5
|
||||
val cDate = ByteArray(dateSize)
|
||||
System.arraycopy(buf, 0, cDate, 0, dateSize)
|
||||
@@ -232,11 +235,14 @@ fun bytes5ToDate(buf: ByteArray, calendar: Calendar = Calendar.getInstance()): D
|
||||
val minute = dw4 and 0x0000000F shl 2 or (dw5 shr 6)
|
||||
val second = dw5 and 0x0000003F
|
||||
|
||||
// File format is a 1 based month, java Calendar uses a zero based month
|
||||
// File format is a 1 based day, java Calendar uses a 1 based day
|
||||
calendar.set(year, month - 1, day, hour, minute, second)
|
||||
|
||||
return calendar.time
|
||||
return DateInstant(Instant.ofEpochMilli(DateTime(
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
second
|
||||
).millis))
|
||||
}
|
||||
|
||||
|
||||
@@ -284,19 +290,15 @@ fun uuidTo16Bytes(uuid: UUID): ByteArray {
|
||||
return buf
|
||||
}
|
||||
|
||||
fun dateTo5Bytes(date: Date, calendar: Calendar = Calendar.getInstance()): ByteArray {
|
||||
fun dateTo5Bytes(dateInstant: DateInstant): ByteArray {
|
||||
val year = dateInstant.getYear()
|
||||
val month = dateInstant.getMonth()
|
||||
val day = dateInstant.getDay()
|
||||
val hour = dateInstant.getHour()
|
||||
val minute = dateInstant.getMinute()
|
||||
val second = dateInstant.getSecond()
|
||||
|
||||
val buf = ByteArray(5)
|
||||
calendar.time = date
|
||||
|
||||
val year = calendar.get(Calendar.YEAR)
|
||||
// File format is a 1 based month, java Calendar uses a zero based month
|
||||
val month = calendar.get(Calendar.MONTH) + 1
|
||||
// File format is a 1 based day, java Calendar uses a 1 based day
|
||||
val day = calendar.get(Calendar.DAY_OF_MONTH)
|
||||
val hour = calendar.get(Calendar.HOUR_OF_DAY)
|
||||
val minute = calendar.get(Calendar.MINUTE)
|
||||
val second = calendar.get(Calendar.SECOND)
|
||||
|
||||
buf[0] = UnsignedInt(year shr 6 and 0x0000003F).toKotlinByte()
|
||||
buf[1] = UnsignedInt(year and 0x0000003F shl 2 or (month shr 2 and 0x00000003)).toKotlinByte()
|
||||
buf[2] = (month and 0x00000003 shl 6
|
||||
|
||||
@@ -22,6 +22,8 @@ package com.kunzisoft.keepass.tests.utils
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import junit.framework.TestCase
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.Instant
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.*
|
||||
@@ -136,28 +138,54 @@ class ValuesTest : TestCase() {
|
||||
}
|
||||
|
||||
fun testDate() {
|
||||
val cal = Calendar.getInstance()
|
||||
val expected = DateInstant(
|
||||
Instant.ofEpochMilli(
|
||||
DateTime(
|
||||
2008,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5
|
||||
).millis))
|
||||
|
||||
val expected = Calendar.getInstance()
|
||||
expected.set(2008, 1, 2, 3, 4, 5)
|
||||
val actual = DateInstant(bytes5ToDate(dateTo5Bytes(expected)))
|
||||
|
||||
val actual = Calendar.getInstance()
|
||||
actual.time = DateInstant(bytes5ToDate(dateTo5Bytes(expected.time, cal), cal)).date
|
||||
|
||||
val jDate = DateInstant(System.currentTimeMillis())
|
||||
val jDate = DateInstant()
|
||||
val intermediate = DateInstant(jDate)
|
||||
val cDate = DateInstant(bytes5ToDate(dateTo5Bytes(intermediate.date)))
|
||||
val cDate = DateInstant(bytes5ToDate(dateTo5Bytes(intermediate)))
|
||||
|
||||
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR))
|
||||
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH))
|
||||
assertEquals("Day mismatch: ", 2, actual.get(Calendar.DAY_OF_MONTH))
|
||||
assertEquals("Hour mismatch: ", 3, actual.get(Calendar.HOUR_OF_DAY))
|
||||
assertEquals("Minute mismatch: ", 4, actual.get(Calendar.MINUTE))
|
||||
assertEquals("Second mismatch: ", 5, actual.get(Calendar.SECOND))
|
||||
assertEquals("Year mismatch: ", 2008, actual.getYear())
|
||||
assertEquals("Month mismatch: ", 1, actual.getMonth())
|
||||
assertEquals("Day mismatch: ", 2, actual.getDay())
|
||||
assertEquals("Hour mismatch: ", 3, actual.getHour())
|
||||
assertEquals("Minute mismatch: ", 4, actual.getMinute())
|
||||
assertEquals("Second mismatch: ", 5, actual.getSecond())
|
||||
assertTrue("jDate and intermediate not equal", jDate == intermediate)
|
||||
assertTrue("jDate $jDate and cDate $cDate not equal", cDate == jDate)
|
||||
}
|
||||
|
||||
fun testDateCompare() {
|
||||
val dateInstantA = DateInstant().apply {
|
||||
setDate(2024, 12, 2)
|
||||
setTime(5, 13)
|
||||
}
|
||||
val dateInstantB = DateInstant().apply {
|
||||
setDate(2024, 12, 2)
|
||||
setTime(5, 10)
|
||||
}
|
||||
val dateInstantC = DateInstant().apply {
|
||||
setDate(2024, 12, 2)
|
||||
setTime(5, 10)
|
||||
}
|
||||
assertTrue(dateInstantA.compareTo(dateInstantB) > 0)
|
||||
assertTrue(dateInstantB.compareTo(dateInstantA) < 0)
|
||||
assertTrue(dateInstantB.compareTo(dateInstantC) == 0)
|
||||
assertTrue(dateInstantA.isAfter(dateInstantB))
|
||||
assertTrue(dateInstantB.isBefore(dateInstantA))
|
||||
assertFalse(dateInstantB.isBefore(dateInstantC))
|
||||
}
|
||||
|
||||
fun testUUID() {
|
||||
val bUUID = ByteArray(16)
|
||||
Random().nextBytes(bUUID)
|
||||
|
||||
Reference in New Issue
Block a user