Merge branch 'develop' into feature/Passkeys

This commit is contained in:
J-Jamet
2024-11-18 16:40:46 +01:00
111 changed files with 3412 additions and 1036 deletions

View File

@@ -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'

View File

@@ -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)

View File

@@ -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

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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

View File

@@ -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,

View File

@@ -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 }
}

View File

@@ -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]!!

View File

@@ -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")
}
}

View File

@@ -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
}
}

View File

@@ -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 -> {

View File

@@ -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)

View File

@@ -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)
)
)
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)))

View File

@@ -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) {

View File

@@ -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

View File

@@ -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)