diff --git a/CHANGELOG b/CHANGELOG index 8ae862fa1..54f830cb2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,16 @@ -KeePassDX(3.0.0) +KeePassDX(2.10.2) + * Fix search fields references #987 + * Fix Auto-Types with same key #997 + +KeePassDX(2.10.1) + * Fix parcelable with custom data #986 + +KeePassDX(2.10.0) + * Manage new database format 4.1 #956 + * Fix show button consistency #980 + * Fix persistent notification #979 + +KeePassDX(2.9.20) * Fix search with non-latin chars #971 * Fix action mode with search #972 (rollback ignore accents #945) * Fix timeout with 0s #974 diff --git a/app/build.gradle b/app/build.gradle index f9b64ee9b..e6e466b5d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.kunzisoft.keepass" minSdkVersion 15 targetSdkVersion 30 - versionCode = 74 - versionName = "2.9.20" + versionCode = 80 + versionName = "2.10.2" multiDexEnabled true testApplicationId = "com.kunzisoft.keepass.tests" diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/IconPickerAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/IconPickerAdapter.kt index 4508f5c21..7c618f3a0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/IconPickerAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/IconPickerAdapter.kt @@ -5,6 +5,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView +import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.kunzisoft.keepass.R import com.kunzisoft.keepass.database.element.icon.IconImageDraw @@ -95,6 +96,12 @@ class IconPickerAdapter(val context: Context, private val tint override fun onBindViewHolder(holder: CustomIconViewHolder, position: Int) { val icon = iconList[position] iconDrawableFactory?.assignDatabaseIcon(holder.iconImageView, icon, tintIcon) + icon.getIconImageToDraw().custom.name.let { iconName -> + holder.iconTextView.apply { + text = iconName + visibility = if (iconName.isNotEmpty()) View.VISIBLE else View.GONE + } + } holder.iconContainerView.isSelected = icon.selected holder.itemView.setOnClickListener { iconPickerListener?.onIconClickListener(icon) @@ -117,5 +124,6 @@ class IconPickerAdapter(val context: Context, private val tint inner class CustomIconViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { var iconContainerView: ViewGroup = itemView.findViewById(R.id.icon_container) var iconImageView: ImageView = itemView.findViewById(R.id.icon_image) + var iconTextView: TextView = itemView.findViewById(R.id.icon_name) } } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt index 8e238ba04..1c089e5f8 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt @@ -124,8 +124,10 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU override fun onResume() { super.onResume() - mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(requireContext()) - mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(requireContext()) + context?.let { + mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(it) + mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(it) + } keepConnection = false } @@ -175,34 +177,36 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU * Check unlock availability and change the current mode depending of device's state */ fun checkUnlockAvailability() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - allowOpenBiometricPrompt = true - if (PreferencesUtil.isBiometricUnlockEnable(requireContext())) { - mAdvancedUnlockInfoView?.setIconResource(R.drawable.fingerprint) + context?.let { context -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + allowOpenBiometricPrompt = true + if (PreferencesUtil.isBiometricUnlockEnable(context)) { + mAdvancedUnlockInfoView?.setIconResource(R.drawable.fingerprint) - // biometric not supported (by API level or hardware) so keep option hidden - // or manually disable - val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(requireContext()) - if (!PreferencesUtil.isAdvancedUnlockEnable(requireContext()) - || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE - || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) { - toggleMode(Mode.BIOMETRIC_UNAVAILABLE) - } else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) { - toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED) - } else { - // biometric is available but not configured, show icon but in disabled state with some information - if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) { - toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED) + // biometric not supported (by API level or hardware) so keep option hidden + // or manually disable + val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(context) + if (!PreferencesUtil.isAdvancedUnlockEnable(context) + || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE + || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) { + toggleMode(Mode.BIOMETRIC_UNAVAILABLE) + } else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) { + toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED) } else { - selectMode() + // biometric is available but not configured, show icon but in disabled state with some information + if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) { + toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED) + } else { + selectMode() + } + } + } else if (PreferencesUtil.isDeviceCredentialUnlockEnable(context)) { + mAdvancedUnlockInfoView?.setIconResource(R.drawable.bolt) + if (AdvancedUnlockManager.isDeviceSecure(context)) { + selectMode() + } else { + toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED) } - } - } else if (PreferencesUtil.isDeviceCredentialUnlockEnable(requireContext())) { - mAdvancedUnlockInfoView?.setIconResource(R.drawable.bolt) - if (AdvancedUnlockManager.isDeviceSecure(requireContext())) { - selectMode() - } else { - toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED) } } } @@ -260,7 +264,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU private fun openBiometricSetting() { mAdvancedUnlockInfoView?.setIconViewClickListener(false) { // ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices... - requireContext().startActivity(Intent(Settings.ACTION_SETTINGS)) + context?.startActivity(Intent(Settings.ACTION_SETTINGS)) } } @@ -295,9 +299,11 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU setAdvancedUnlockedTitleView(R.string.no_credentials_stored) setAdvancedUnlockedMessageView("") - mAdvancedUnlockInfoView?.setIconViewClickListener(false) { - onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS, - requireContext().getString(R.string.credential_before_click_advanced_unlock_button)) + context?.let { context -> + mAdvancedUnlockInfoView?.setIconViewClickListener(false) { + onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS, + context.getString(R.string.credential_before_click_advanced_unlock_button)) + } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt index e9102a69d..1816abd60 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt @@ -31,41 +31,47 @@ class DeleteNodesRunnable(context: Context, afterActionNodesFinish: AfterActionNodesFinish) : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { - private var mParent: Group? = null + private var mOldParent: Group? = null private var mCanRecycle: Boolean = false private var mNodesToDeleteBackup = ArrayList() override fun nodeAction() { - foreachNode@ for(currentNode in mNodesToDelete) { - mParent = currentNode.parent - mParent?.touch(modified = false, touchParents = true) + foreachNode@ for(nodeToDelete in mNodesToDelete) { + mOldParent = nodeToDelete.parent + mOldParent?.touch(modified = false, touchParents = true) - when (currentNode.type) { + when (nodeToDelete.type) { Type.GROUP -> { + val groupToDelete = nodeToDelete as Group // Create a copy to keep the old ref and remove it visually - mNodesToDeleteBackup.add(Group(currentNode as Group)) + mNodesToDeleteBackup.add(Group(groupToDelete)) // Remove Node from parent - mCanRecycle = database.canRecycle(currentNode) + mCanRecycle = database.canRecycle(groupToDelete) if (mCanRecycle) { - database.recycle(currentNode, context.resources) + groupToDelete.touch(modified = false, touchParents = true) + database.recycle(groupToDelete, context.resources) + groupToDelete.setPreviousParentGroup(mOldParent) } else { - database.deleteGroup(currentNode) + database.deleteGroup(groupToDelete) } } Type.ENTRY -> { + val entryToDelete = nodeToDelete as Entry // Create a copy to keep the old ref and remove it visually - mNodesToDeleteBackup.add(Entry(currentNode as Entry)) + mNodesToDeleteBackup.add(Entry(entryToDelete)) // Remove Node from parent - mCanRecycle = database.canRecycle(currentNode) + mCanRecycle = database.canRecycle(entryToDelete) if (mCanRecycle) { - database.recycle(currentNode, context.resources) + entryToDelete.touch(modified = false, touchParents = true) + database.recycle(entryToDelete, context.resources) + entryToDelete.setPreviousParentGroup(mOldParent) } else { - database.deleteEntry(currentNode) + database.deleteEntry(entryToDelete) } // Remove the oldest attachments - currentNode.getAttachments(database.attachmentPool).forEach { + entryToDelete.getAttachments(database.attachmentPool).forEach { database.removeAttachmentIfNotUsed(it) } } @@ -76,7 +82,7 @@ class DeleteNodesRunnable(context: Context, override fun nodeFinish(): ActionNodesValues { if (!result.isSuccess) { if (mCanRecycle) { - mParent?.let { + mOldParent?.let { mNodesToDeleteBackup.forEach { backupNode -> when (backupNode.type) { Type.GROUP -> { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt index 660f9c545..d93a0af93 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt @@ -52,8 +52,9 @@ class MoveNodesRunnable constructor( // and if not in the current group && groupToMove != mNewParent && !mNewParent.isContainedIn(groupToMove)) { - nodeToMove.touch(modified = true, touchParents = true) + groupToMove.touch(modified = true, touchParents = true) database.moveGroupTo(groupToMove, mNewParent) + groupToMove.setPreviousParentGroup(mOldParent) } else { // Only finish thread setError(MoveGroupDatabaseException()) @@ -66,8 +67,9 @@ class MoveNodesRunnable constructor( if (mOldParent != mNewParent // and root can contains entry && (mNewParent != database.rootGroup || database.rootCanContainsEntry())) { - nodeToMove.touch(modified = true, touchParents = true) + entryToMove.touch(modified = true, touchParents = true) database.moveEntryTo(entryToMove, mNewParent) + entryToMove.setPreviousParentGroup(mOldParent) } else { // Only finish thread setError(MoveEntryDatabaseException()) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorKDBX.kt index a6d8bf342..8524bf08e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorKDBX.kt @@ -45,8 +45,8 @@ class EntryCursorKDBX : EntryCursorUUID() { entry.expires )) - for (element in entry.customFields.entries) { - extraFieldCursor.addExtraField(entryId, element.key, element.value) + entry.doForEachDecodedCustomField { key, value -> + extraFieldCursor.addExtraField(entryId, key, value) } entryId++ diff --git a/app/src/main/java/com/kunzisoft/keepass/database/cursor/ExtraFieldCursor.kt b/app/src/main/java/com/kunzisoft/keepass/database/cursor/ExtraFieldCursor.kt index ea4533f15..83b3951fc 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/cursor/ExtraFieldCursor.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/cursor/ExtraFieldCursor.kt @@ -42,7 +42,7 @@ class ExtraFieldCursor : MatrixCursor(arrayOf( } fun populateExtraFieldInEntry(pwEntry: EntryKDBX) { - pwEntry.putExtraField(getString(getColumnIndex(COLUMN_LABEL)), + pwEntry.putField(getString(getColumnIndex(COLUMN_LABEL)), ProtectedString(getInt(getColumnIndex(COLUMN_PROTECTION)) > 0, getString(getColumnIndex(COLUMN_VALUE)))) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/CustomData.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/CustomData.kt new file mode 100644 index 000000000..4264e682f --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/CustomData.kt @@ -0,0 +1,66 @@ +package com.kunzisoft.keepass.database.element + +import android.os.Parcel +import android.os.Parcelable +import com.kunzisoft.keepass.utils.ParcelableUtil +import java.util.* + +class CustomData : Parcelable { + + private val mCustomDataItems = HashMap() + + constructor() + + constructor(toCopy: CustomData) { + mCustomDataItems.clear() + mCustomDataItems.putAll(toCopy.mCustomDataItems) + } + + constructor(parcel: Parcel) { + ParcelableUtil.readStringParcelableMap(parcel, CustomDataItem::class.java) + } + + fun get(key: String): CustomDataItem? { + return mCustomDataItems[key] + } + + fun put(customDataItem: CustomDataItem) { + mCustomDataItems[customDataItem.key] = customDataItem + } + + fun containsItemWithValue(value: String): Boolean { + return mCustomDataItems.any { mapEntry -> mapEntry.value.value.equals(value, true) } + } + + fun containsItemWithLastModificationTime(): Boolean { + return mCustomDataItems.any { mapEntry -> mapEntry.value.lastModificationTime != null } + } + + fun isNotEmpty(): Boolean { + return mCustomDataItems.isNotEmpty() + } + + fun doForEachItems(action: (CustomDataItem) -> Unit) { + for ((_, value) in mCustomDataItems) { + action.invoke(value) + } + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + ParcelableUtil.writeStringParcelableMap(parcel, flags, mCustomDataItems) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): CustomData { + return CustomData(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/CustomDataItem.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/CustomDataItem.kt new file mode 100644 index 000000000..b59ad0607 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/CustomDataItem.kt @@ -0,0 +1,43 @@ +package com.kunzisoft.keepass.database.element + +import android.os.Parcel +import android.os.Parcelable + +class CustomDataItem : Parcelable { + + val key: String + var value: String + var lastModificationTime: DateInstant? = null + + constructor(parcel: Parcel) { + key = parcel.readString() ?: "" + value = parcel.readString() ?: "" + lastModificationTime = parcel.readParcelable(DateInstant::class.java.classLoader) + } + + constructor(key: String, value: String, lastModificationTime: DateInstant? = null) { + this.key = key + this.value = value + this.lastModificationTime = lastModificationTime + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(key) + parcel.writeString(value) + parcel.writeParcelable(lastModificationTime, flags) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): CustomDataItem { + return CustomDataItem(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt index b7ca9a4ca..c9069bd40 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt @@ -42,7 +42,7 @@ import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.exception.* import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX -import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4 +import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40 import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB @@ -222,7 +222,7 @@ class Database { // Default compression not necessary if stored in header mDatabaseKDBX?.let { return it.compressionAlgorithm == CompressionAlgorithm.GZip - && it.kdbxVersion.isBefore(FILE_VERSION_32_4) + && it.kdbxVersion.isBefore(FILE_VERSION_40) } return false } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/DateInstant.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/DateInstant.kt index e39363b75..204495f9d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/DateInstant.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/DateInstant.kt @@ -55,7 +55,7 @@ class DateInstant : Parcelable { jDate = Date() } - protected constructor(parcel: Parcel) { + constructor(parcel: Parcel) { jDate = parcel.readSerializable() as Date } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/DeletedObject.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/DeletedObject.kt index f01254d2f..7306dde70 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/DeletedObject.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/DeletedObject.kt @@ -19,30 +19,37 @@ */ package com.kunzisoft.keepass.database.element +import android.os.Parcel +import android.os.ParcelUuid +import android.os.Parcelable import com.kunzisoft.keepass.database.element.database.DatabaseVersioned -import java.util.Date -import java.util.UUID +import java.util.* -class DeletedObject { +class DeletedObject : Parcelable { var uuid: UUID = DatabaseVersioned.UUID_ZERO - private var mDeletionTime: Date? = null + private var mDeletionTime: DateInstant? = null - fun getDeletionTime(): Date { + constructor() + + constructor(uuid: UUID, deletionTime: DateInstant = DateInstant()) { + this.uuid = uuid + this.mDeletionTime = deletionTime + } + + constructor(parcel: Parcel) { + uuid = parcel.readParcelable(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO + mDeletionTime = parcel.readParcelable(DateInstant::class.java.classLoader) + } + + fun getDeletionTime(): DateInstant { if (mDeletionTime == null) { - mDeletionTime = Date(System.currentTimeMillis()) + mDeletionTime = DateInstant(System.currentTimeMillis()) } return mDeletionTime!! } - fun setDeletionTime(deletionTime: Date) { - this.mDeletionTime = deletionTime - } - - constructor() - - constructor(uuid: UUID, deletionTime: Date = Date()) { - this.uuid = uuid + fun setDeletionTime(deletionTime: DateInstant) { this.mDeletionTime = deletionTime } @@ -59,4 +66,23 @@ class DeletedObject { override fun hashCode(): Int { return uuid.hashCode() } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeParcelable(ParcelUuid(uuid), flags) + parcel.writeParcelable(mDeletionTime, flags) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): DeletedObject { + return DeletedObject(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt index 3a21eb135..00e82c24c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt @@ -23,6 +23,7 @@ import android.os.Parcel import android.os.Parcelable import com.kunzisoft.keepass.database.element.binary.AttachmentPool import com.kunzisoft.keepass.database.element.database.DatabaseKDBX +import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface @@ -114,6 +115,20 @@ class Entry : Node, EntryVersionedInterface { entryKDBX?.icon = value } + var tags: Tags + get() = entryKDBX?.tags ?: Tags() + set(value) { + entryKDBX?.tags = value + } + + var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO + get() = entryKDBX?.previousParentGroup ?: DatabaseVersioned.UUID_ZERO + private set + + fun setPreviousParentGroup(previousParent: Group?) { + entryKDBX?.previousParentGroup = previousParent?.groupKDBX?.id ?: DatabaseVersioned.UUID_ZERO + } + override val type: Type get() = Type.ENTRY @@ -268,8 +283,8 @@ class Entry : Node, EntryVersionedInterface { fun getExtraFields(): List { val extraFields = ArrayList() entryKDBX?.let { - for (field in it.customFields) { - extraFields.add(Field(field.key, field.value)) + it.doForEachDecodedCustomField { key, value -> + extraFields.add(Field(key, value)) } } return extraFields @@ -279,7 +294,7 @@ class Entry : Node, EntryVersionedInterface { * Update or add an extra field to the list (standard or custom) */ fun putExtraField(field: Field) { - entryKDBX?.putExtraField(field.name, field.protectedValue) + entryKDBX?.putField(field.name, field.protectedValue) } private fun addExtraFields(fields: List) { @@ -295,7 +310,7 @@ class Entry : Node, EntryVersionedInterface { fun getOtpElement(): OtpElement? { entryKDBX?.let { return OtpEntryFields.parseFields { key -> - it.customFields[key]?.toString() + it.getField(key)?.toString() } } return null @@ -373,10 +388,6 @@ class Entry : Node, EntryVersionedInterface { return entryKDBX?.getSize(attachmentPool) ?: 0L } - fun containsCustomData(): Boolean { - return entryKDBX?.containsCustomData() ?: false - } - /* ------------ Converter diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Group.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Group.kt index c8f112c63..d2a891f4c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/Group.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Group.kt @@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.element import android.content.Context import android.os.Parcel import android.os.Parcelable +import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface @@ -134,6 +135,20 @@ class Group : Node, GroupVersionedInterface { groupKDBX?.icon = value } + var tags: Tags + get() = groupKDBX?.tags ?: Tags() + set(value) { + groupKDBX?.tags = value + } + + var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO + get() = groupKDBX?.previousParentGroup ?: DatabaseVersioned.UUID_ZERO + private set + + fun setPreviousParentGroup(previousParent: Group?) { + groupKDBX?.previousParentGroup = previousParent?.groupKDBX?.id ?: DatabaseVersioned.UUID_ZERO + } + override val type: Type get() = Type.GROUP @@ -394,10 +409,6 @@ class Group : Node, GroupVersionedInterface { groupKDBX?.isExpanded = expanded } - fun containsCustomData(): Boolean { - return groupKDBX?.containsCustomData() ?: false - } - /* ------------ Converter diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Tags.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Tags.kt new file mode 100644 index 000000000..bdda16cde --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Tags.kt @@ -0,0 +1,45 @@ +package com.kunzisoft.keepass.database.element + +import android.os.Parcel +import android.os.Parcelable + +class Tags: Parcelable { + + private val mTags = ArrayList() + + constructor() + + constructor(values: String): this() { + mTags.addAll(values.split(';')) + } + + constructor(parcel: Parcel) : this() { + parcel.readStringList(mTags) + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeStringList(mTags) + } + + override fun describeContents(): Int { + return 0 + } + + fun isEmpty(): Boolean { + return mTags.isEmpty() + } + + override fun toString(): String { + return mTags.joinToString(";") + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): Tags { + return Tags(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/binary/CustomIconPool.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/binary/CustomIconPool.kt index b6b71c37e..dd1824d4e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/binary/CustomIconPool.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/binary/CustomIconPool.kt @@ -1,8 +1,27 @@ package com.kunzisoft.keepass.database.element.binary +import com.kunzisoft.keepass.database.element.DateInstant +import com.kunzisoft.keepass.database.element.icon.IconImageCustom import java.util.* -class CustomIconPool(binaryCache: BinaryCache) : BinaryPool(binaryCache) { +class CustomIconPool(private val binaryCache: BinaryCache) : BinaryPool(binaryCache) { + + private val customIcons = HashMap() + + fun put(key: UUID? = null, + name: String, + lastModificationTime: DateInstant?, + smallSize: Boolean, + result: (IconImageCustom, BinaryData?) -> Unit) { + val keyBinary = super.put(key) { uniqueBinaryId -> + // Create a byte array for better performance with small data + binaryCache.getBinaryData(uniqueBinaryId, smallSize) + } + val uuid = keyBinary.keys.first() + val customIcon = IconImageCustom(uuid, name, lastModificationTime) + customIcons[uuid] = customIcon + result.invoke(customIcon, keyBinary.binary) + } override fun findUnusedKey(): UUID { var newUUID = UUID.randomUUID() @@ -11,4 +30,14 @@ class CustomIconPool(binaryCache: BinaryCache) : BinaryPool(binaryCache) { } return newUUID } + + fun any(predicate: (IconImageCustom)-> Boolean): Boolean { + return customIcons.any { predicate(it.value) } + } + + fun doForEachCustomIcon(action: (customIcon: IconImageCustom, binary: BinaryData) -> Unit) { + doForEachBinary { key, binary -> + action.invoke(customIcons[key] ?: IconImageCustom(key), binary) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt index 2de7265e8..0eef5ace4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt @@ -157,10 +157,6 @@ class DatabaseKDB : DatabaseVersioned() { return this.iconsManager.getIcon(iconId) } - override fun containsCustomData(): Boolean { - return false - } - override fun isInRecycleBin(group: GroupKDB): Boolean { var currentGroup: GroupKDB? = group val currentBackupGroup = backupGroup ?: return false diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt index 067e1438c..0368a8bba 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt @@ -32,6 +32,7 @@ import com.kunzisoft.keepass.database.crypto.VariantDictionary import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters +import com.kunzisoft.keepass.database.element.CustomData import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DeletedObject import com.kunzisoft.keepass.database.element.binary.BinaryData @@ -45,8 +46,9 @@ import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeVersioned import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig import com.kunzisoft.keepass.database.exception.UnknownKDF -import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_3 -import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4 +import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_31 +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.utils.StringUtil.removeSpaceChars import com.kunzisoft.keepass.utils.StringUtil.toHexString import com.kunzisoft.keepass.utils.UnsignedInt @@ -102,7 +104,7 @@ class DatabaseKDBX : DatabaseVersioned { */ var isRecycleBinEnabled = true var recycleBinUUID: UUID = UUID_ZERO - var recycleBinChanged = Date() + var recycleBinChanged = DateInstant() var entryTemplatesGroup = UUID_ZERO var entryTemplatesGroupChanged = DateInstant() var historyMaxItems = DEFAULT_HISTORY_MAX_ITEMS @@ -111,7 +113,7 @@ class DatabaseKDBX : DatabaseVersioned { var lastTopVisibleGroupUUID = UUID_ZERO var memoryProtection = MemoryProtectionConfig() val deletedObjects = ArrayList() - val customData = HashMap() + val customData = CustomData() var localizedAppName = "KeePassDX" @@ -128,7 +130,7 @@ class DatabaseKDBX : DatabaseVersioned { */ constructor(databaseName: String, rootName: String) { name = databaseName - kdbxVersion = FILE_VERSION_32_3 + kdbxVersion = FILE_VERSION_31 val group = createGroup().apply { title = rootName icon.standard = getStandardIcon(IconImageStandard.FOLDER_ID) @@ -139,8 +141,9 @@ class DatabaseKDBX : DatabaseVersioned { override val version: String get() { val kdbxStringVersion = when(kdbxVersion) { - FILE_VERSION_32_3 -> "3.1" - FILE_VERSION_32_4 -> "4.0" + FILE_VERSION_31 -> "3.1" + FILE_VERSION_40 -> "4.0" + FILE_VERSION_41 -> "4.1" else -> "UNKNOWN" } return "KeePass 2 - KDBX$kdbxStringVersion" @@ -188,7 +191,7 @@ class DatabaseKDBX : DatabaseVersioned { } CompressionAlgorithm.GZip -> { // Only in databaseV3.1, in databaseV4 the header is zipped during the save - if (kdbxVersion.isBefore(FILE_VERSION_32_4)) { + if (kdbxVersion.isBefore(FILE_VERSION_40)) { compressAllBinaries() } } @@ -196,7 +199,7 @@ class DatabaseKDBX : DatabaseVersioned { } CompressionAlgorithm.GZip -> { // In databaseV4 the header is zipped during the save, so not necessary here - if (kdbxVersion.isBefore(FILE_VERSION_32_4)) { + if (kdbxVersion.isBefore(FILE_VERSION_40)) { when (newCompression) { CompressionAlgorithm.None -> { decompressAllBinaries() @@ -314,9 +317,11 @@ class DatabaseKDBX : DatabaseVersioned { } fun addCustomIcon(customIconId: UUID? = null, + name: String, + lastModificationTime: DateInstant?, smallSize: Boolean, result: (IconImageCustom, BinaryData?) -> Unit) { - iconsManager.addCustomIcon(customIconId, smallSize, result) + iconsManager.addCustomIcon(customIconId, name, lastModificationTime, smallSize, result) } fun isCustomIconBinaryDuplicate(binary: BinaryData): Boolean { @@ -327,17 +332,43 @@ class DatabaseKDBX : DatabaseVersioned { return this.iconsManager.getIcon(iconUuid) } - fun putCustomData(label: String, value: String) { - this.customData[label] = value + /* + * Search methods + */ + + fun getEntryByTitle(title: String, recursionLevel: Int): EntryKDBX? { + return this.entryIndexes.values.find { entry -> + entry.decodeTitleKey(recursionLevel).equals(title, true) + } } - override fun containsCustomData(): Boolean { - return customData.isNotEmpty() + fun getEntryByUsername(username: String, recursionLevel: Int): EntryKDBX? { + return this.entryIndexes.values.find { entry -> + entry.decodeUsernameKey(recursionLevel).equals(username, true) + } + } + + fun getEntryByURL(url: String, recursionLevel: Int): EntryKDBX? { + return this.entryIndexes.values.find { entry -> + entry.decodeUrlKey(recursionLevel).equals(url, true) + } + } + + fun getEntryByPassword(password: String, recursionLevel: Int): EntryKDBX? { + return this.entryIndexes.values.find { entry -> + entry.decodePasswordKey(recursionLevel).equals(password, true) + } + } + + fun getEntryByNotes(notes: String, recursionLevel: Int): EntryKDBX? { + return this.entryIndexes.values.find { entry -> + entry.decodeNotesKey(recursionLevel).equals(notes, true) + } } fun getEntryByCustomData(customDataValue: String): EntryKDBX? { return entryIndexes.values.find { entry -> - entry.customData.containsValue(customDataValue) + entry.customData.containsItemWithValue(customDataValue) } } @@ -608,14 +639,14 @@ class DatabaseKDBX : DatabaseVersioned { } addGroupTo(recycleBinGroup, rootGroup) recycleBinUUID = recycleBinGroup.id - recycleBinChanged = recycleBinGroup.lastModificationTime.date + recycleBinChanged = recycleBinGroup.lastModificationTime } } fun removeRecycleBin() { if (recycleBin != null) { recycleBinUUID = UUID_ZERO - recycleBinChanged = DateInstant().date + recycleBinChanged = DateInstant() } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt index 6d812120b..790df515c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt @@ -271,26 +271,6 @@ abstract class DatabaseVersioned< return this.entryIndexes[id] } - fun getEntryByTitle(title: String): Entry? { - return this.entryIndexes.values.find { entry -> entry.title.equals(title, true) } - } - - fun getEntryByUsername(username: String): Entry? { - return this.entryIndexes.values.find { entry -> entry.username.equals(username, true) } - } - - fun getEntryByURL(url: String): Entry? { - return this.entryIndexes.values.find { entry -> entry.url.equals(url, true) } - } - - fun getEntryByPassword(password: String): Entry? { - return this.entryIndexes.values.find { entry -> entry.password.equals(password, true) } - } - - fun getEntryByNotes(notes: String): Entry? { - return this.entryIndexes.values.find { entry -> entry.notes.equals(notes, true) } - } - fun addEntryIndex(entry: Entry) { val entryId = entry.nodeId if (entryIndexes.containsKey(entryId)) { @@ -337,8 +317,6 @@ abstract class DatabaseVersioned< abstract fun getStandardIcon(iconId: Int): IconImageStandard - abstract fun containsCustomData(): Boolean - fun addGroupTo(newGroup: Group, parent: Group?) { // Add tree to parent tree parent?.addChildGroup(newGroup) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/entry/AutoType.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/entry/AutoType.kt index b4b2419a1..3c550db31 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/entry/AutoType.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/entry/AutoType.kt @@ -21,8 +21,6 @@ package com.kunzisoft.keepass.database.element.entry import android.os.Parcel import android.os.Parcelable - -import com.kunzisoft.keepass.utils.ParcelableUtil import com.kunzisoft.keepass.utils.UnsignedInt class AutoType : Parcelable { @@ -30,7 +28,7 @@ class AutoType : Parcelable { var enabled = true var obfuscationOptions = OBF_OPT_NONE var defaultSequence = "" - private var windowSeqPairs = LinkedHashMap() + private var windowSeqPairs = ArrayList() constructor() @@ -38,16 +36,15 @@ class AutoType : Parcelable { this.enabled = autoType.enabled this.obfuscationOptions = autoType.obfuscationOptions this.defaultSequence = autoType.defaultSequence - for ((key, value) in autoType.windowSeqPairs) { - this.windowSeqPairs[key] = value - } + this.windowSeqPairs.clear() + this.windowSeqPairs.addAll(autoType.windowSeqPairs) } constructor(parcel: Parcel) { this.enabled = parcel.readByte().toInt() != 0 this.obfuscationOptions = UnsignedInt(parcel.readInt()) this.defaultSequence = parcel.readString() ?: defaultSequence - this.windowSeqPairs = ParcelableUtil.readStringParcelableMap(parcel) + parcel.readTypedList(this.windowSeqPairs, AutoTypeItem.CREATOR) } override fun describeContents(): Int { @@ -58,15 +55,43 @@ class AutoType : Parcelable { dest.writeByte((if (enabled) 1 else 0).toByte()) dest.writeInt(obfuscationOptions.toKotlinInt()) dest.writeString(defaultSequence) - ParcelableUtil.writeStringParcelableMap(dest, windowSeqPairs) + dest.writeTypedList(windowSeqPairs) } - fun put(key: String, value: String) { - windowSeqPairs[key] = value + fun add(key: String, value: String) { + windowSeqPairs.add(AutoTypeItem(key, value)) } - fun entrySet(): Set> { - return windowSeqPairs.entries + fun doForEachAutoTypeItem(action: (key: String, value: String) -> Unit) { + windowSeqPairs.forEach { + action.invoke(it.key, it.value) + } + } + + private data class AutoTypeItem(var key: String, var value: String): Parcelable { + constructor(parcel: Parcel) : this( + parcel.readString() ?: "", + parcel.readString() ?: "") { + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(key) + parcel.writeString(value) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): AutoTypeItem { + return AutoTypeItem(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } } companion object { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDBX.kt index e8b32df9b..b57eb458c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDBX.kt @@ -20,12 +20,15 @@ package com.kunzisoft.keepass.database.element.entry import android.os.Parcel +import android.os.ParcelUuid import android.os.Parcelable -import com.kunzisoft.keepass.utils.UnsignedLong import com.kunzisoft.keepass.database.element.Attachment +import com.kunzisoft.keepass.database.element.CustomData import com.kunzisoft.keepass.database.element.DateInstant +import com.kunzisoft.keepass.database.element.Tags import com.kunzisoft.keepass.database.element.binary.AttachmentPool import com.kunzisoft.keepass.database.element.database.DatabaseKDBX +import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeIdUUID @@ -33,6 +36,7 @@ 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.ParcelableUtil +import com.kunzisoft.keepass.utils.UnsignedLong import java.util.* import kotlin.collections.ArrayList import kotlin.collections.LinkedHashMap @@ -45,16 +49,20 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte @Transient private var mDecodeRef = false - var customData = LinkedHashMap() + override var usageCount = UnsignedLong(0) + override var locationChanged = DateInstant() + override var customData = CustomData() var fields = LinkedHashMap() var binaries = LinkedHashMap() // Map var foregroundColor = "" var backgroundColor = "" var overrideURL = "" + override var tags = Tags() + override var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO + var qualityCheck = true var autoType = AutoType() var history = ArrayList() var additional = "" - var tags = "" override var expires: Boolean = false @@ -63,17 +71,18 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte constructor(parcel: Parcel) : super(parcel) { usageCount = UnsignedLong(parcel.readLong()) locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged - customData = ParcelableUtil.readStringParcelableMap(parcel) + customData = parcel.readParcelable(CustomData::class.java.classLoader) ?: CustomData() fields = ParcelableUtil.readStringParcelableMap(parcel, ProtectedString::class.java) binaries = ParcelableUtil.readStringIntMap(parcel) foregroundColor = parcel.readString() ?: foregroundColor backgroundColor = parcel.readString() ?: backgroundColor overrideURL = parcel.readString() ?: overrideURL + tags = parcel.readParcelable(Tags::class.java.classLoader) ?: tags + previousParentGroup = parcel.readParcelable(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO autoType = parcel.readParcelable(AutoType::class.java.classLoader) ?: autoType parcel.readTypedList(history, CREATOR) url = parcel.readString() ?: url additional = parcel.readString() ?: additional - tags = parcel.readString() ?: tags } override fun readParentParcelable(parcel: Parcel): GroupKDBX? { @@ -88,17 +97,18 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte super.writeToParcel(dest, flags) dest.writeLong(usageCount.toKotlinLong()) dest.writeParcelable(locationChanged, flags) - ParcelableUtil.writeStringParcelableMap(dest, customData) + dest.writeParcelable(customData, flags) ParcelableUtil.writeStringParcelableMap(dest, flags, fields) ParcelableUtil.writeStringIntMap(dest, binaries) dest.writeString(foregroundColor) dest.writeString(backgroundColor) dest.writeString(overrideURL) + dest.writeParcelable(tags, flags) + dest.writeParcelable(ParcelUuid(previousParentGroup), flags) dest.writeParcelable(autoType, flags) dest.writeTypedList(history) dest.writeString(url) dest.writeString(additional) - dest.writeString(tags) } /** @@ -109,9 +119,7 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte super.updateWith(source) usageCount = source.usageCount locationChanged = DateInstant(source.locationChanged) - // Add all custom elements in map - customData.clear() - customData.putAll(source.customData) + customData = CustomData(source.customData) fields.clear() fields.putAll(source.fields) binaries.clear() @@ -119,13 +127,14 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte foregroundColor = source.foregroundColor backgroundColor = source.backgroundColor overrideURL = source.overrideURL + tags = source.tags + previousParentGroup = source.previousParentGroup autoType = AutoType(source.autoType) history.clear() if (copyHistory) history.addAll(source.history) url = source.url additional = source.additional - tags = source.tags } fun startToManageFieldReferences(database: DatabaseKDBX) { @@ -218,10 +227,6 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte fields[STR_NOTES] = ProtectedString(protect, value) } - override var usageCount = UnsignedLong(0) - - override var locationChanged = DateInstant() - fun getSize(attachmentPool: AttachmentPool): Long { var size = FIXED_LENGTH_SIZE @@ -233,7 +238,7 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte size += getAttachmentsSize(attachmentPool) size += autoType.defaultSequence.length.toLong() - for ((key, value) in autoType.entrySet()) { + autoType.doForEachAutoTypeItem { key, value -> size += key.length.toLong() size += value.length.toLong() } @@ -243,7 +248,7 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte } size += overrideURL.length.toLong() - size += tags.length.toLong() + size += tags.toString().length return size } @@ -260,25 +265,32 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte || key == STR_NOTES) } - var customFields = LinkedHashMap() - get() { - field.clear() - for ((key, value) in fields) { - if (!isStandardField(key)) { - field[key] = ProtectedString(value.isProtected, decodeRefKey(mDecodeRef, key, 0)) - } + fun doForEachDecodedCustomField(action: (key: String, value: ProtectedString) -> Unit) { + val iterator = fields.entries.iterator() + while (iterator.hasNext()) { + val mapEntry = iterator.next() + if (!isStandardField(mapEntry.key)) { + action.invoke(mapEntry.key, + ProtectedString(mapEntry.value.isProtected, + decodeRefKey(mDecodeRef, mapEntry.key, 0) + ) + ) } - return field } + } + + fun getField(key: String): ProtectedString? { + return fields[key] + } + + fun putField(label: String, value: ProtectedString) { + fields[label] = value + } fun removeAllFields() { fields.clear() } - fun putExtraField(label: String, value: ProtectedString) { - fields[label] = value - } - /** * It's a list because history labels can be defined multiple times */ @@ -322,14 +334,6 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte return size } - override fun putCustomData(key: String, value: String) { - customData[key] = value - } - - override fun containsCustomData(): Boolean { - return customData.isNotEmpty() - } - fun addEntryToHistory(entry: EntryKDBX) { history.add(entry) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/entry/FieldReferencesEngine.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/entry/FieldReferencesEngine.kt index d60f36275..81ce52dee 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/entry/FieldReferencesEngine.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/entry/FieldReferencesEngine.kt @@ -19,16 +19,17 @@ */ package com.kunzisoft.keepass.database.element.entry +import android.util.Log import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.utils.UuidUtil -import java.util.* +import java.util.concurrent.ConcurrentHashMap class FieldReferencesEngine(private val mDatabase: DatabaseKDBX) { // Key : @: // Value : content - private var refsCache: MutableMap = HashMap() + private var refsCache = ConcurrentHashMap() fun clear() { refsCache.clear() @@ -53,52 +54,56 @@ class FieldReferencesEngine(private val mDatabase: DatabaseKDBX) { && numberInlineRef <= MAX_INLINE_REF) { numberInlineRef++ - textValue = fillReferencesUsingCache(textValue) - - val start = textValue.indexOf(STR_REF_START, offset, true) - if (start < 0) { - break - } - val end = textValue.indexOf(STR_REF_END, offset, true) - if (end <= start) { - break - } - - val reference = textValue.substring(start + STR_REF_START.length, end) - val fullReference = "$STR_REF_START$reference$STR_REF_END" - - if (!refsCache.containsKey(fullReference)) { - val result = findReferenceTarget(reference) - val entryFound = result.entry - val newRecursionLevel = recursionLevel + 1 - val data: String? = when (result.wanted) { - 'T' -> entryFound?.decodeTitleKey(newRecursionLevel) - 'U' -> entryFound?.decodeUsernameKey(newRecursionLevel) - 'A' -> entryFound?.decodeUrlKey(newRecursionLevel) - 'P' -> entryFound?.decodePasswordKey(newRecursionLevel) - 'N' -> entryFound?.decodeNotesKey(newRecursionLevel) - 'I' -> UuidUtil.toHexString(entryFound?.nodeId?.id) - else -> null - } - refsCache[fullReference] = data + try { textValue = fillReferencesUsingCache(textValue) - } - offset = end + val start = textValue.indexOf(STR_REF_START, offset, true) + if (start < 0) { + break + } + val end = textValue.indexOf(STR_REF_END, offset, true) + if (end <= start) { + break + } + + val reference = textValue.substring(start + STR_REF_START.length, end) + val fullReference = "$STR_REF_START$reference$STR_REF_END" + + if (!refsCache.containsKey(fullReference)) { + val newRecursionLevel = recursionLevel + 1 + val result = findReferenceTarget(reference, newRecursionLevel) + val entryFound = result.entry + val data: String? = when (result.wanted) { + 'T' -> entryFound?.decodeTitleKey(newRecursionLevel) + 'U' -> entryFound?.decodeUsernameKey(newRecursionLevel) + 'A' -> entryFound?.decodeUrlKey(newRecursionLevel) + 'P' -> entryFound?.decodePasswordKey(newRecursionLevel) + 'N' -> entryFound?.decodeNotesKey(newRecursionLevel) + 'I' -> UuidUtil.toHexString(entryFound?.nodeId?.id) + else -> null + } + refsCache[fullReference] = data + textValue = fillReferencesUsingCache(textValue) + } + + offset = end + } catch (e: Exception) { + Log.e(TAG, "Error when fill placeholders by reference", e) + } } return textValue } private fun fillReferencesUsingCache(text: String): String { var newText = text - for ((key, value) in refsCache) { + refsCache.keys.forEach { key -> // Replace by key if value not found - newText = newText.replace(key, value ?: key, true) + newText = newText.replace(key, refsCache[key] ?: key, true) } return newText } - private fun findReferenceTarget(reference: String): TargetResult { + private fun findReferenceTarget(reference: String, recursionLevel: Int): TargetResult { val targetResult = TargetResult(null, 'J') @@ -116,11 +121,11 @@ class FieldReferencesEngine(private val mDatabase: DatabaseKDBX) { val searchIn = Character.toUpperCase(reference[2]) val searchQuery = reference.substring(4) targetResult.entry = when (searchIn) { - 'T' -> mDatabase.getEntryByTitle(searchQuery) - 'U' -> mDatabase.getEntryByUsername(searchQuery) - 'A' -> mDatabase.getEntryByURL(searchQuery) - 'P' -> mDatabase.getEntryByPassword(searchQuery) - 'N' -> mDatabase.getEntryByNotes(searchQuery) + 'T' -> mDatabase.getEntryByTitle(searchQuery, recursionLevel) + 'U' -> mDatabase.getEntryByUsername(searchQuery, recursionLevel) + 'A' -> mDatabase.getEntryByURL(searchQuery, recursionLevel) + 'P' -> mDatabase.getEntryByPassword(searchQuery, recursionLevel) + 'N' -> mDatabase.getEntryByNotes(searchQuery, recursionLevel) 'I' -> { UuidUtil.fromHexString(searchQuery)?.let { uuid -> mDatabase.getEntryById(NodeIdUUID(uuid)) @@ -139,5 +144,7 @@ class FieldReferencesEngine(private val mDatabase: DatabaseKDBX) { private const val MAX_INLINE_REF = 10 private const val STR_REF_START = "{REF:" private const val STR_REF_END = "}" + + private val TAG = FieldReferencesEngine::class.java.name } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDBX.kt index 450524a04..d2bc674f2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDBX.kt @@ -20,8 +20,11 @@ package com.kunzisoft.keepass.database.element.group import android.os.Parcel +import android.os.ParcelUuid import android.os.Parcelable +import com.kunzisoft.keepass.database.element.CustomData import com.kunzisoft.keepass.database.element.DateInstant +import com.kunzisoft.keepass.database.element.Tags import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.node.NodeId @@ -33,14 +36,17 @@ import java.util.* class GroupKDBX : GroupVersioned, NodeKDBXInterface { - private val customData = HashMap() + override var usageCount = UnsignedLong(0) + override var locationChanged = DateInstant() + override var customData = CustomData() var notes = "" - var isExpanded = true var defaultAutoTypeSequence = "" var enableAutoType: Boolean? = null var enableSearching: Boolean? = null var lastTopVisibleEntry: UUID = DatabaseVersioned.UUID_ZERO + override var tags = Tags() + override var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO override var expires: Boolean = false @@ -60,7 +66,7 @@ class GroupKDBX : GroupVersioned, NodeKDBXInte constructor(parcel: Parcel) : super(parcel) { usageCount = UnsignedLong(parcel.readLong()) locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged - // TODO customData = ParcelableUtil.readStringParcelableMap(parcel); + customData = parcel.readParcelable(CustomData::class.java.classLoader) ?: CustomData() notes = parcel.readString() ?: notes isExpanded = parcel.readByte().toInt() != 0 defaultAutoTypeSequence = parcel.readString() ?: defaultAutoTypeSequence @@ -69,6 +75,8 @@ class GroupKDBX : GroupVersioned, NodeKDBXInte val isSearchingEnabled = parcel.readInt() enableSearching = if (isSearchingEnabled == -1) null else isSearchingEnabled == 1 lastTopVisibleEntry = parcel.readSerializable() as UUID + tags = parcel.readParcelable(Tags::class.java.classLoader) ?: tags + previousParentGroup = parcel.readParcelable(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO } override fun readParentParcelable(parcel: Parcel): GroupKDBX? { @@ -83,13 +91,15 @@ class GroupKDBX : GroupVersioned, NodeKDBXInte super.writeToParcel(dest, flags) dest.writeLong(usageCount.toKotlinLong()) dest.writeParcelable(locationChanged, flags) - // TODO ParcelableUtil.writeStringParcelableMap(dest, customData); + dest.writeParcelable(customData, flags) dest.writeString(notes) dest.writeByte((if (isExpanded) 1 else 0).toByte()) dest.writeString(defaultAutoTypeSequence) dest.writeInt(if (enableAutoType == null) -1 else if (enableAutoType!!) 1 else 0) dest.writeInt(if (enableSearching == null) -1 else if (enableSearching!!) 1 else 0) dest.writeSerializable(lastTopVisibleEntry) + dest.writeParcelable(tags, flags) + dest.writeParcelable(ParcelUuid(previousParentGroup), flags) } fun updateWith(source: GroupKDBX) { @@ -97,34 +107,21 @@ class GroupKDBX : GroupVersioned, NodeKDBXInte usageCount = source.usageCount locationChanged = DateInstant(source.locationChanged) // Add all custom elements in map - customData.clear() - for ((key, value) in source.customData) { - customData[key] = value - } + customData = CustomData(source.customData) notes = source.notes isExpanded = source.isExpanded defaultAutoTypeSequence = source.defaultAutoTypeSequence enableAutoType = source.enableAutoType enableSearching = source.enableSearching lastTopVisibleEntry = source.lastTopVisibleEntry + tags = source.tags + previousParentGroup = source.previousParentGroup } - override var usageCount = UnsignedLong(0) - - override var locationChanged = DateInstant() - override fun afterAssignNewParent() { locationChanged = DateInstant() } - override fun putCustomData(key: String, value: String) { - customData[key] = value - } - - override fun containsCustomData(): Boolean { - return customData.isNotEmpty() - } - companion object { @JvmField diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImageCustom.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImageCustom.kt index 5cc43c2c3..5efddadc2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImageCustom.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconImageCustom.kt @@ -22,27 +22,38 @@ package com.kunzisoft.keepass.database.element.icon import android.os.Parcel import android.os.ParcelUuid import android.os.Parcelable +import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import java.util.* class IconImageCustom : IconImageDraw { - var uuid: UUID + val uuid: UUID + var name: String = "" + var lastModificationTime: DateInstant? = null - constructor() { - uuid = DatabaseVersioned.UUID_ZERO + constructor(name: String = "", lastModificationTime: DateInstant? = null) { + this.uuid = DatabaseVersioned.UUID_ZERO + this.name = name + this.lastModificationTime = lastModificationTime } - constructor(uuid: UUID) { + constructor(uuid: UUID, name: String = "", lastModificationTime: DateInstant? = null) { this.uuid = uuid + this.name = name + this.lastModificationTime = lastModificationTime } constructor(parcel: Parcel) { uuid = parcel.readParcelable(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO + name = parcel.readString() ?: name + lastModificationTime = parcel.readParcelable(DateInstant::class.java.classLoader) } override fun writeToParcel(dest: Parcel, flags: Int) { dest.writeParcelable(ParcelUuid(uuid), flags) + dest.writeString(name) + dest.writeParcelable(lastModificationTime, flags) } override fun describeContents(): Int { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconsManager.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconsManager.kt index fc39c9129..58aafa702 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconsManager.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconsManager.kt @@ -20,6 +20,7 @@ package com.kunzisoft.keepass.database.element.icon import android.util.Log +import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.binary.BinaryCache import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.CustomIconPool @@ -27,7 +28,7 @@ import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.K import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS import java.util.* -class IconsManager(private val binaryCache: BinaryCache) { +class IconsManager(binaryCache: BinaryCache) { private val standardCache = List(NB_ICONS) { IconImageStandard(it) @@ -52,17 +53,15 @@ class IconsManager(private val binaryCache: BinaryCache) { fun buildNewCustomIcon(key: UUID? = null, result: (IconImageCustom, BinaryData?) -> Unit) { // Create a binary file for a brand new custom icon - addCustomIcon(key, false, result) + addCustomIcon(key, "", null, false, result) } fun addCustomIcon(key: UUID? = null, + name: String, + lastModificationTime: DateInstant?, smallSize: Boolean, result: (IconImageCustom, BinaryData?) -> Unit) { - val keyBinary = customCache.put(key) { uniqueBinaryId -> - // Create a byte array for better performance with small data - binaryCache.getBinaryData(uniqueBinaryId, smallSize) - } - result.invoke(IconImageCustom(keyBinary.keys.first()), keyBinary.binary) + customCache.put(key, name, lastModificationTime, smallSize, result) } fun getIcon(iconUuid: UUID): IconImageCustom { @@ -88,8 +87,12 @@ class IconsManager(private val binaryCache: BinaryCache) { } fun doForEachCustomIcon(action: (IconImageCustom, BinaryData) -> Unit) { - customCache.doForEachBinary { key, binary -> - action.invoke(IconImageCustom(key), binary) + customCache.doForEachCustomIcon(action) + } + + fun containsCustomIconWithNameOrLastModificationTime(): Boolean { + return customCache.any { customIcon -> + customIcon.name.isNotEmpty() || customIcon.lastModificationTime != null } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/node/NodeKDBXInterface.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/node/NodeKDBXInterface.kt index 222f25a53..3a9594640 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/node/NodeKDBXInterface.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/node/NodeKDBXInterface.kt @@ -19,17 +19,17 @@ */ package com.kunzisoft.keepass.database.element.node +import com.kunzisoft.keepass.database.element.CustomData import com.kunzisoft.keepass.database.element.DateInstant +import com.kunzisoft.keepass.database.element.Tags import com.kunzisoft.keepass.utils.UnsignedLong +import java.util.* interface NodeKDBXInterface : NodeTimeInterface { var usageCount: UnsignedLong - var locationChanged: DateInstant - - fun putCustomData(key: String, value: String) - - fun containsCustomData(): Boolean - + var customData: CustomData + var tags: Tags + var previousParentGroup: UUID } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/node/NodeVersionedInterface.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/node/NodeVersionedInterface.kt index c980b7d8e..19c4edfbf 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/node/NodeVersionedInterface.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/node/NodeVersionedInterface.kt @@ -25,15 +25,7 @@ import com.kunzisoft.keepass.database.element.icon.IconImage interface NodeVersionedInterface : NodeTimeInterface, Parcelable { var title: String - - /** - * @return Visual icon - */ var icon: IconImage - - /** - * @return Type of Node - */ val type: Type /** diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/DatabaseHeaderKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/DatabaseHeaderKDBX.kt index 0fa96ddff..271ab221f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/DatabaseHeaderKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/DatabaseHeaderKDBX.kt @@ -19,9 +19,9 @@ */ package com.kunzisoft.keepass.database.file -import com.kunzisoft.keepass.database.crypto.CrsAlgorithm import com.kunzisoft.encrypt.HashManager import com.kunzisoft.keepass.database.action.node.NodeHandler +import com.kunzisoft.keepass.database.crypto.CrsAlgorithm import com.kunzisoft.keepass.database.crypto.VariantDictionary import com.kunzisoft.keepass.database.crypto.kdf.AesKdf import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory @@ -91,41 +91,64 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader( this.masterSeed = ByteArray(32) } - private inner class NodeHasCustomData : NodeHandler() { - - internal var containsCustomData = false - + private open class NodeOperationHandler : NodeHandler() { + var containsCustomData = false override fun operate(node: T): Boolean { - if (node.containsCustomData()) { + if (node.customData.isNotEmpty()) { containsCustomData = true - return false } return true } } - private fun getMinKdbxVersion(databaseV4: DatabaseKDBX): UnsignedInt { + private inner class EntryOperationHandler: NodeOperationHandler() { + var passwordQualityEstimationDisabled = false + override fun operate(node: EntryKDBX): Boolean { + if (!node.qualityCheck) { + passwordQualityEstimationDisabled = true + } + return super.operate(node) + } + } + + private inner class GroupOperationHandler: NodeOperationHandler() { + var containsTags = false + override fun operate(node: GroupKDBX): Boolean { + if (!node.tags.isEmpty()) + containsTags = true + return super.operate(node) + } + } + + private fun getMinKdbxVersion(databaseKDBX: DatabaseKDBX): UnsignedInt { + val entryHandler = EntryOperationHandler() + val groupHandler = GroupOperationHandler() + databaseKDBX.rootGroup?.doForEachChildAndForIt(entryHandler, groupHandler) + + // https://keepass.info/help/kb/kdbx_4.1.html + val containsGroupWithTag = groupHandler.containsTags + val containsEntryWithPasswordQualityEstimationDisabled = entryHandler.passwordQualityEstimationDisabled + val containsCustomIconWithNameOrLastModificationTime = databaseKDBX.iconsManager.containsCustomIconWithNameOrLastModificationTime() + val containsHeaderCustomDataWithLastModificationTime = databaseKDBX.customData.containsItemWithLastModificationTime() + // https://keepass.info/help/kb/kdbx_4.html + // If AES is not use, it's at least 4.0 + val kdfIsNotAes = databaseKDBX.kdfParameters?.uuid != AesKdf.CIPHER_UUID + val containsHeaderCustomData = databaseKDBX.customData.isNotEmpty() + val containsNodeCustomData = entryHandler.containsCustomData || groupHandler.containsCustomData - // Return v4 if AES is not use - if (databaseV4.kdfParameters != null - && databaseV4.kdfParameters!!.uuid != AesKdf.CIPHER_UUID) { - return FILE_VERSION_32_4 - } - - if (databaseV4.rootGroup == null) { - return FILE_VERSION_32_3 - } - - val entryHandler = NodeHasCustomData() - val groupHandler = NodeHasCustomData() - databaseV4.rootGroup?.doForEachChildAndForIt(entryHandler, groupHandler) - return if (databaseV4.containsCustomData() - || entryHandler.containsCustomData - || groupHandler.containsCustomData) { - FILE_VERSION_32_4 + // Check each condition to determine version + return if (containsGroupWithTag + || containsEntryWithPasswordQualityEstimationDisabled + || containsCustomIconWithNameOrLastModificationTime + || containsHeaderCustomDataWithLastModificationTime) { + FILE_VERSION_41 + } else if (kdfIsNotAes + || containsHeaderCustomData + || containsNodeCustomData) { + FILE_VERSION_40 } else { - FILE_VERSION_32_3 + FILE_VERSION_31 } } @@ -167,7 +190,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader( private fun readHeaderField(dis: InputStream): Boolean { val fieldID = dis.read().toByte() - val fieldSize: Int = if (version.isBefore(FILE_VERSION_32_4)) { + val fieldSize: Int = if (version.isBefore(FILE_VERSION_40)) { dis.readBytes2ToUShort() } else { dis.readBytes4ToUInt().toKotlinInt() @@ -194,20 +217,20 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader( PwDbHeaderV4Fields.MasterSeed -> masterSeed = fieldData - PwDbHeaderV4Fields.TransformSeed -> if (version.isBefore(FILE_VERSION_32_4)) + PwDbHeaderV4Fields.TransformSeed -> if (version.isBefore(FILE_VERSION_40)) transformSeed = fieldData - PwDbHeaderV4Fields.TransformRounds -> if (version.isBefore(FILE_VERSION_32_4)) + PwDbHeaderV4Fields.TransformRounds -> if (version.isBefore(FILE_VERSION_40)) setTransformRound(fieldData) PwDbHeaderV4Fields.EncryptionIV -> encryptionIV = fieldData - PwDbHeaderV4Fields.InnerRandomstreamKey -> if (version.isBefore(FILE_VERSION_32_4)) + PwDbHeaderV4Fields.InnerRandomstreamKey -> if (version.isBefore(FILE_VERSION_40)) innerRandomStreamKey = fieldData PwDbHeaderV4Fields.StreamStartBytes -> streamStartBytes = fieldData - PwDbHeaderV4Fields.InnerRandomStreamID -> if (version.isBefore(FILE_VERSION_32_4)) + PwDbHeaderV4Fields.InnerRandomStreamID -> if (version.isBefore(FILE_VERSION_40)) setRandomStreamID(fieldData) PwDbHeaderV4Fields.KdfParameters -> databaseV4.kdfParameters = KdfParameters.deserialize(fieldData) @@ -283,7 +306,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader( */ private fun validVersion(version: UnsignedInt): Boolean { return version.toKotlinInt() and FILE_VERSION_CRITICAL_MASK.toKotlinInt() <= - FILE_VERSION_32_4.toKotlinInt() and FILE_VERSION_CRITICAL_MASK.toKotlinInt() + FILE_VERSION_40.toKotlinInt() and FILE_VERSION_CRITICAL_MASK.toKotlinInt() } companion object { @@ -292,8 +315,9 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader( val DBSIG_2 = UnsignedInt(-0x4ab40499) private val FILE_VERSION_CRITICAL_MASK = UnsignedInt(-0x10000) - val FILE_VERSION_32_3 = UnsignedInt(0x00030001) - val FILE_VERSION_32_4 = UnsignedInt(0x00040000) + val FILE_VERSION_31 = UnsignedInt(0x00030001) + val FILE_VERSION_40 = UnsignedInt(0x00040000) + val FILE_VERSION_41 = UnsignedInt(0x00040001) fun getCompressionFromFlag(flag: UnsignedInt): CompressionAlgorithm? { return when (flag.toKotlinInt()) { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/DatabaseKDBXXML.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/DatabaseKDBXXML.kt index 00898cac8..a9548a1e2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/DatabaseKDBXXML.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/DatabaseKDBXXML.kt @@ -79,8 +79,10 @@ object DatabaseKDBXXML { const val ElemFgColor = "ForegroundColor" const val ElemBgColor = "BackgroundColor" const val ElemOverrideUrl = "OverrideURL" + const val ElemQualityCheck = "QualityCheck" const val ElemTimes = "Times" const val ElemTags = "Tags" + const val ElemPreviousParentGroup = "PreviousParentGroup" const val ElemCreationTime = "CreationTime" const val ElemLastModTime = "LastModificationTime" diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt index c4f9a9f3a..2103fb149 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt @@ -26,9 +26,7 @@ import com.kunzisoft.keepass.database.crypto.CipherEngine import com.kunzisoft.keepass.database.crypto.CrsAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.HmacBlock -import com.kunzisoft.keepass.database.element.Attachment -import com.kunzisoft.keepass.database.element.DateInstant -import com.kunzisoft.keepass.database.element.DeletedObject +import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.LoadedKey import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm @@ -42,7 +40,7 @@ import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.exception.* import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX -import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4 +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 @@ -88,9 +86,12 @@ class DatabaseInputKDBX(cacheDirectory: File, private var ctxHistoryBase: EntryKDBX? = null private var ctxDeletedObject: DeletedObject? = null private var customIconID = DatabaseVersioned.UUID_ZERO + private var customIconName: String = "" + private var customIconLastModificationTime: DateInstant? = null private var customIconData: ByteArray? = null private var customDataKey: String? = null private var customDataValue: String? = null + private var customDataLastModificationTime: DateInstant? = null private var groupCustomDataKey: String? = null private var groupCustomDataValue: String? = null private var entryCustomDataKey: String? = null @@ -161,7 +162,7 @@ class DatabaseInputKDBX(cacheDirectory: File, } val plainInputStream: InputStream - if (mDatabase.kdbxVersion.isBefore(FILE_VERSION_32_4)) { + if (mDatabase.kdbxVersion.isBefore(FILE_VERSION_40)) { val dataDecrypted = CipherInputStream(databaseInputStream, cipher) val storedStartBytes: ByteArray? @@ -210,7 +211,7 @@ class DatabaseInputKDBX(cacheDirectory: File, else -> plainInputStream } - if (!mDatabase.kdbxVersion.isBefore(FILE_VERSION_32_4)) { + if (!mDatabase.kdbxVersion.isBefore(FILE_VERSION_40)) { readInnerHeader(inputStreamXml, header) } @@ -386,25 +387,25 @@ class DatabaseInputKDBX(cacheDirectory: File, } } } else if (name.equals(DatabaseKDBXXML.ElemSettingsChanged, ignoreCase = true)) { - mDatabase.settingsChanged = readPwTime(xpp) + mDatabase.settingsChanged = readDateInstant(xpp) } else if (name.equals(DatabaseKDBXXML.ElemDbName, ignoreCase = true)) { mDatabase.name = readString(xpp) } else if (name.equals(DatabaseKDBXXML.ElemDbNameChanged, ignoreCase = true)) { - mDatabase.nameChanged = readPwTime(xpp) + mDatabase.nameChanged = readDateInstant(xpp) } else if (name.equals(DatabaseKDBXXML.ElemDbDesc, ignoreCase = true)) { mDatabase.description = readString(xpp) } else if (name.equals(DatabaseKDBXXML.ElemDbDescChanged, ignoreCase = true)) { - mDatabase.descriptionChanged = readPwTime(xpp) + mDatabase.descriptionChanged = readDateInstant(xpp) } else if (name.equals(DatabaseKDBXXML.ElemDbDefaultUser, ignoreCase = true)) { mDatabase.defaultUserName = readString(xpp) } else if (name.equals(DatabaseKDBXXML.ElemDbDefaultUserChanged, ignoreCase = true)) { - mDatabase.defaultUserNameChanged = readPwTime(xpp) + mDatabase.defaultUserNameChanged = readDateInstant(xpp) } else if (name.equals(DatabaseKDBXXML.ElemDbColor, ignoreCase = true)) { mDatabase.color = readString(xpp) } else if (name.equals(DatabaseKDBXXML.ElemDbMntncHistoryDays, ignoreCase = true)) { mDatabase.maintenanceHistoryDays = readUInt(xpp, DEFAULT_HISTORY_DAYS) } else if (name.equals(DatabaseKDBXXML.ElemDbKeyChanged, ignoreCase = true)) { - mDatabase.keyLastChanged = readPwTime(xpp) + mDatabase.keyLastChanged = readDateInstant(xpp) } else if (name.equals(DatabaseKDBXXML.ElemDbKeyChangeRec, ignoreCase = true)) { mDatabase.keyChangeRecDays = readLong(xpp, -1) } else if (name.equals(DatabaseKDBXXML.ElemDbKeyChangeForce, ignoreCase = true)) { @@ -420,11 +421,11 @@ class DatabaseInputKDBX(cacheDirectory: File, } else if (name.equals(DatabaseKDBXXML.ElemRecycleBinUuid, ignoreCase = true)) { mDatabase.recycleBinUUID = readUuid(xpp) } else if (name.equals(DatabaseKDBXXML.ElemRecycleBinChanged, ignoreCase = true)) { - mDatabase.recycleBinChanged = readTime(xpp) + mDatabase.recycleBinChanged = readDateInstant(xpp) } else if (name.equals(DatabaseKDBXXML.ElemEntryTemplatesGroup, ignoreCase = true)) { mDatabase.entryTemplatesGroup = readUuid(xpp) } else if (name.equals(DatabaseKDBXXML.ElemEntryTemplatesGroupChanged, ignoreCase = true)) { - mDatabase.entryTemplatesGroupChanged = readPwTime(xpp) + mDatabase.entryTemplatesGroupChanged = readDateInstant(xpp) } else if (name.equals(DatabaseKDBXXML.ElemHistoryMaxItems, ignoreCase = true)) { mDatabase.historyMaxItems = readInt(xpp, -1) } else if (name.equals(DatabaseKDBXXML.ElemHistoryMaxSize, ignoreCase = true)) { @@ -468,6 +469,10 @@ class DatabaseInputKDBX(cacheDirectory: File, if (strData.isNotEmpty()) { customIconData = Base64.decode(strData, BASE_64_FLAG) } + } else if (name.equals(DatabaseKDBXXML.ElemName, ignoreCase = true)) { + customIconName = readString(xpp) + } else if (name.equals(DatabaseKDBXXML.ElemLastModTime, ignoreCase = true)) { + customIconLastModificationTime = readDateInstant(xpp) } else { readUnknown(xpp) } @@ -488,6 +493,8 @@ class DatabaseInputKDBX(cacheDirectory: File, customDataKey = readString(xpp) } else if (name.equals(DatabaseKDBXXML.ElemValue, ignoreCase = true)) { customDataValue = readString(xpp) + } else if (name.equals(DatabaseKDBXXML.ElemLastModTime, ignoreCase = true)) { + customDataLastModificationTime = readDateInstant(xpp) } else { readUnknown(xpp) } @@ -518,6 +525,10 @@ class DatabaseInputKDBX(cacheDirectory: File, ctxGroup?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt()) } else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) { ctxGroup?.icon?.custom = mDatabase.getCustomIcon(readUuid(xpp)) + } else if (name.equals(DatabaseKDBXXML.ElemTags, ignoreCase = true)) { + ctxGroup?.tags = readTags(xpp) + } else if (name.equals(DatabaseKDBXXML.ElemPreviousParentGroup, ignoreCase = true)) { + ctxGroup?.previousParentGroup = readUuid(xpp) } else if (name.equals(DatabaseKDBXXML.ElemTimes, ignoreCase = true)) { return switchContext(ctx, KdbContext.GroupTimes, xpp) } else if (name.equals(DatabaseKDBXXML.ElemIsExpanded, ignoreCase = true)) { @@ -562,6 +573,7 @@ class DatabaseInputKDBX(cacheDirectory: File, KdbContext.GroupCustomDataItem -> when { name.equals(DatabaseKDBXXML.ElemKey, ignoreCase = true) -> groupCustomDataKey = readString(xpp) name.equals(DatabaseKDBXXML.ElemValue, ignoreCase = true) -> groupCustomDataValue = readString(xpp) + name.equals(DatabaseKDBXXML.ElemLastModTime, ignoreCase = true) -> readDateInstant(xpp) // Ignore else -> readUnknown(xpp) } @@ -578,8 +590,12 @@ class DatabaseInputKDBX(cacheDirectory: File, ctxEntry?.backgroundColor = readString(xpp) } else if (name.equals(DatabaseKDBXXML.ElemOverrideUrl, ignoreCase = true)) { ctxEntry?.overrideURL = readString(xpp) + } else if (name.equals(DatabaseKDBXXML.ElemQualityCheck, ignoreCase = true)) { + ctxEntry?.qualityCheck = readBool(xpp, true) } else if (name.equals(DatabaseKDBXXML.ElemTags, ignoreCase = true)) { - ctxEntry?.tags = readString(xpp) + ctxEntry?.tags = readTags(xpp) + } else if (name.equals(DatabaseKDBXXML.ElemPreviousParentGroup, ignoreCase = true)) { + ctxEntry?.previousParentGroup = readUuid(xpp) } else if (name.equals(DatabaseKDBXXML.ElemTimes, ignoreCase = true)) { return switchContext(ctx, KdbContext.EntryTimes, xpp) } else if (name.equals(DatabaseKDBXXML.ElemString, ignoreCase = true)) { @@ -608,6 +624,7 @@ class DatabaseInputKDBX(cacheDirectory: File, KdbContext.EntryCustomDataItem -> when { name.equals(DatabaseKDBXXML.ElemKey, ignoreCase = true) -> entryCustomDataKey = readString(xpp) name.equals(DatabaseKDBXXML.ElemValue, ignoreCase = true) -> entryCustomDataValue = readString(xpp) + name.equals(DatabaseKDBXXML.ElemLastModTime, ignoreCase = true) -> readDateInstant(xpp) // Ignore else -> readUnknown(xpp) } @@ -620,13 +637,13 @@ class DatabaseInputKDBX(cacheDirectory: File, } when { - name.equals(DatabaseKDBXXML.ElemLastModTime, ignoreCase = true) -> tl?.lastModificationTime = readPwTime(xpp) - name.equals(DatabaseKDBXXML.ElemCreationTime, ignoreCase = true) -> tl?.creationTime = readPwTime(xpp) - name.equals(DatabaseKDBXXML.ElemLastAccessTime, ignoreCase = true) -> tl?.lastAccessTime = readPwTime(xpp) - name.equals(DatabaseKDBXXML.ElemExpiryTime, ignoreCase = true) -> tl?.expiryTime = readPwTime(xpp) + name.equals(DatabaseKDBXXML.ElemLastModTime, ignoreCase = true) -> tl?.lastModificationTime = readDateInstant(xpp) + name.equals(DatabaseKDBXXML.ElemCreationTime, ignoreCase = true) -> tl?.creationTime = readDateInstant(xpp) + name.equals(DatabaseKDBXXML.ElemLastAccessTime, ignoreCase = true) -> tl?.lastAccessTime = readDateInstant(xpp) + name.equals(DatabaseKDBXXML.ElemExpiryTime, ignoreCase = true) -> tl?.expiryTime = readDateInstant(xpp) name.equals(DatabaseKDBXXML.ElemExpires, ignoreCase = true) -> tl?.expires = readBool(xpp, false) name.equals(DatabaseKDBXXML.ElemUsageCount, ignoreCase = true) -> tl?.usageCount = readULong(xpp, UnsignedLong(0)) - name.equals(DatabaseKDBXXML.ElemLocationChanged, ignoreCase = true) -> tl?.locationChanged = readPwTime(xpp) + name.equals(DatabaseKDBXXML.ElemLocationChanged, ignoreCase = true) -> tl?.locationChanged = readDateInstant(xpp) else -> readUnknown(xpp) } } @@ -687,7 +704,7 @@ class DatabaseInputKDBX(cacheDirectory: File, KdbContext.DeletedObject -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) { ctxDeletedObject?.uuid = readUuid(xpp) } else if (name.equals(DatabaseKDBXXML.ElemDeletionTime, ignoreCase = true)) { - ctxDeletedObject?.setDeletionTime(readTime(xpp)) + ctxDeletedObject?.setDeletionTime(readDateInstant(xpp)) } else { readUnknown(xpp) } @@ -714,29 +731,34 @@ class DatabaseInputKDBX(cacheDirectory: File, } else if (ctx == KdbContext.CustomIcon && name.equals(DatabaseKDBXXML.ElemCustomIconItem, ignoreCase = true)) { val iconData = customIconData if (customIconID != DatabaseVersioned.UUID_ZERO && iconData != null) { - mDatabase.addCustomIcon(customIconID, isRAMSufficient.invoke(iconData.size.toLong())) { _, binary -> + mDatabase.addCustomIcon(customIconID, + customIconName, + customIconLastModificationTime, + isRAMSufficient.invoke(iconData.size.toLong())) { _, binary -> binary?.getOutputDataStream(mDatabase.binaryCache)?.use { outputStream -> outputStream.write(iconData) } } } - customIconID = DatabaseVersioned.UUID_ZERO + customIconName = "" + customIconLastModificationTime = null customIconData = null - return KdbContext.CustomIcons } else if (ctx == KdbContext.Binaries && name.equals(DatabaseKDBXXML.ElemBinaries, ignoreCase = true)) { return KdbContext.Meta } else if (ctx == KdbContext.CustomData && name.equals(DatabaseKDBXXML.ElemCustomData, ignoreCase = true)) { return KdbContext.Meta } else if (ctx == KdbContext.CustomDataItem && name.equals(DatabaseKDBXXML.ElemStringDictExItem, ignoreCase = true)) { - if (customDataKey != null && customDataValue != null) { - mDatabase.putCustomData(customDataKey!!, customDataValue!!) + customDataKey?.let { dataKey -> + customDataValue?.let { dataValue -> + mDatabase.customData.put(CustomDataItem(dataKey, + dataValue, customDataLastModificationTime)) + } } - customDataKey = null customDataValue = null - + customDataLastModificationTime = null return KdbContext.CustomData } else if (ctx == KdbContext.Group && name.equals(DatabaseKDBXXML.ElemGroup, ignoreCase = true)) { if (ctxGroup != null && ctxGroup?.id == DatabaseVersioned.UUID_ZERO) { @@ -758,13 +780,13 @@ class DatabaseInputKDBX(cacheDirectory: File, } else if (ctx == KdbContext.GroupCustomData && name.equals(DatabaseKDBXXML.ElemCustomData, ignoreCase = true)) { return KdbContext.Group } else if (ctx == KdbContext.GroupCustomDataItem && name.equals(DatabaseKDBXXML.ElemStringDictExItem, ignoreCase = true)) { - if (groupCustomDataKey != null && groupCustomDataValue != null) { - ctxGroup?.putCustomData(groupCustomDataKey!!, groupCustomDataValue!!) + groupCustomDataKey?.let { customDataKey -> + groupCustomDataValue?.let { customDataValue -> + ctxGroup?.customData?.put(CustomDataItem(customDataKey, customDataValue)) + } } - groupCustomDataKey = null groupCustomDataValue = null - return KdbContext.GroupCustomData } else if (ctx == KdbContext.Entry && name.equals(DatabaseKDBXXML.ElemEntry, ignoreCase = true)) { @@ -785,7 +807,7 @@ class DatabaseInputKDBX(cacheDirectory: File, return KdbContext.Entry } else if (ctx == KdbContext.EntryString && name.equals(DatabaseKDBXXML.ElemString, ignoreCase = true)) { if (ctxStringName != null && ctxStringValue != null) - ctxEntry?.putExtraField(ctxStringName!!, ctxStringValue!!) + ctxEntry?.putField(ctxStringName!!, ctxStringValue!!) ctxStringName = null ctxStringValue = null @@ -802,7 +824,7 @@ class DatabaseInputKDBX(cacheDirectory: File, return KdbContext.Entry } else if (ctx == KdbContext.EntryAutoTypeItem && name.equals(DatabaseKDBXXML.ElemAutoTypeItem, ignoreCase = true)) { if (ctxATName != null && ctxATSeq != null) - ctxEntry?.autoType?.put(ctxATName!!, ctxATSeq!!) + ctxEntry?.autoType?.add(ctxATName!!, ctxATSeq!!) ctxATName = null ctxATSeq = null @@ -810,13 +832,13 @@ class DatabaseInputKDBX(cacheDirectory: File, } else if (ctx == KdbContext.EntryCustomData && name.equals(DatabaseKDBXXML.ElemCustomData, ignoreCase = true)) { return KdbContext.Entry } else if (ctx == KdbContext.EntryCustomDataItem && name.equals(DatabaseKDBXXML.ElemStringDictExItem, ignoreCase = true)) { - if (entryCustomDataKey != null && entryCustomDataValue != null) { - ctxEntry?.putCustomData(entryCustomDataKey!!, entryCustomDataValue!!) + entryCustomDataKey?.let { customDataKey -> + entryCustomDataValue?.let { customDataValue -> + ctxEntry?.customData?.put(CustomDataItem(customDataKey, customDataValue)) + } } - entryCustomDataKey = null entryCustomDataValue = null - return KdbContext.EntryCustomData } else if (ctx == KdbContext.EntryHistory && name.equals(DatabaseKDBXXML.ElemHistory, ignoreCase = true)) { entryInHistory = false @@ -836,16 +858,11 @@ class DatabaseInputKDBX(cacheDirectory: File, } @Throws(IOException::class, XmlPullParserException::class) - private fun readPwTime(xpp: XmlPullParser): DateInstant { - return DateInstant(readTime(xpp)) - } - - @Throws(IOException::class, XmlPullParserException::class) - private fun readTime(xpp: XmlPullParser): Date { + private fun readDateInstant(xpp: XmlPullParser): DateInstant { val sDate = readString(xpp) var utcDate: Date? = null - if (mDatabase.kdbxVersion.isBefore(FILE_VERSION_32_4)) { + if (mDatabase.kdbxVersion.isBefore(FILE_VERSION_40)) { try { utcDate = DatabaseKDBXXML.DateFormatter.parse(sDate) } catch (e: ParseException) { @@ -863,7 +880,12 @@ class DatabaseInputKDBX(cacheDirectory: File, utcDate = DateKDBXUtil.convertKDBX4Time(seconds) } - return utcDate ?: Date(0L) + return DateInstant(utcDate ?: Date(0L)) + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun readTags(xpp: XmlPullParser): Tags { + return Tags(readString(xpp)) } @Throws(XmlPullParserException::class, IOException::class) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutputKDBX.kt index 3aa51a308..ecb83c808 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutputKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutputKDBX.kt @@ -27,7 +27,7 @@ import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.database.file.DatabaseHeader import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX -import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4 +import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40 import com.kunzisoft.keepass.stream.MacOutputStream import com.kunzisoft.keepass.utils.* import java.io.ByteArrayOutputStream @@ -76,7 +76,7 @@ constructor(private val databaseKDBX: DatabaseKDBX, writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CompressionFlags, uIntTo4Bytes(DatabaseHeaderKDBX.getFlagFromCompression(databaseKDBX.compressionAlgorithm))) writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.MasterSeed, header.masterSeed) - if (header.version.isBefore(FILE_VERSION_32_4)) { + if (header.version.isBefore(FILE_VERSION_40)) { writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.TransformSeed, header.transformSeed) writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.TransformRounds, longTo8Bytes(databaseKDBX.numberKeyEncryptionRounds)) } else { @@ -87,7 +87,7 @@ constructor(private val databaseKDBX: DatabaseKDBX, writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.EncryptionIV, header.encryptionIV) } - if (header.version.isBefore(FILE_VERSION_32_4)) { + if (header.version.isBefore(FILE_VERSION_40)) { writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.InnerRandomstreamKey, header.innerRandomStreamKey) writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.StreamStartBytes, header.streamStartBytes) writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.InnerRandomStreamID, uIntTo4Bytes(header.innerRandomStream!!.id)) @@ -121,7 +121,7 @@ constructor(private val databaseKDBX: DatabaseKDBX, @Throws(IOException::class) private fun writeHeaderFieldSize(size: Int) { - if (header.version.isBefore(FILE_VERSION_32_4)) { + if (header.version.isBefore(FILE_VERSION_40)) { mos.write2BytesUShort(size) } else { mos.write4BytesUInt(UnsignedInt(size)) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt index 0a258b37a..d844c1773 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt @@ -23,15 +23,16 @@ import android.util.Base64 import android.util.Log import android.util.Xml import com.kunzisoft.encrypt.StreamCipher -import com.kunzisoft.keepass.database.crypto.CrsAlgorithm import com.kunzisoft.keepass.database.action.node.NodeHandler import com.kunzisoft.keepass.database.crypto.CipherEngine +import com.kunzisoft.keepass.database.crypto.CrsAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory -import com.kunzisoft.keepass.database.element.DeletedObject +import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG +import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.entry.AutoType import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX @@ -41,7 +42,8 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.database.exception.UnknownKDF import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX -import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4 +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 @@ -83,7 +85,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, header = outputHeader(mOutputStream) - val osPlain: OutputStream = if (header!!.version.isBefore(FILE_VERSION_32_4)) { + val osPlain: OutputStream = if (header!!.version.isBefore(FILE_VERSION_40)) { val cos = attachStreamEncryptor(header!!, mOutputStream) cos.write(header!!.streamStartBytes) @@ -102,7 +104,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, else -> osPlain } - if (!header!!.version.isBefore(FILE_VERSION_32_4)) { + if (!header!!.version.isBefore(FILE_VERSION_40)) { outputInnerHeader(mDatabaseKDBX, header!!, xmlOutputStream) } @@ -234,40 +236,40 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, private fun writeMeta() { xml.startTag(null, DatabaseKDBXXML.ElemMeta) - writeObject(DatabaseKDBXXML.ElemGenerator, mDatabaseKDBX.localizedAppName) + writeString(DatabaseKDBXXML.ElemGenerator, mDatabaseKDBX.localizedAppName) if (hashOfHeader != null) { - writeObject(DatabaseKDBXXML.ElemHeaderHash, String(Base64.encode(hashOfHeader!!, BASE_64_FLAG))) + writeString(DatabaseKDBXXML.ElemHeaderHash, String(Base64.encode(hashOfHeader!!, BASE_64_FLAG))) } - writeObject(DatabaseKDBXXML.ElemDbName, mDatabaseKDBX.name, true) - writeObject(DatabaseKDBXXML.ElemDbNameChanged, mDatabaseKDBX.nameChanged.date) - writeObject(DatabaseKDBXXML.ElemDbDesc, mDatabaseKDBX.description, true) - writeObject(DatabaseKDBXXML.ElemDbDescChanged, mDatabaseKDBX.descriptionChanged.date) - writeObject(DatabaseKDBXXML.ElemDbDefaultUser, mDatabaseKDBX.defaultUserName, true) - writeObject(DatabaseKDBXXML.ElemDbDefaultUserChanged, mDatabaseKDBX.defaultUserNameChanged.date) - writeObject(DatabaseKDBXXML.ElemDbMntncHistoryDays, mDatabaseKDBX.maintenanceHistoryDays.toKotlinLong()) - writeObject(DatabaseKDBXXML.ElemDbColor, mDatabaseKDBX.color) - writeObject(DatabaseKDBXXML.ElemDbKeyChanged, mDatabaseKDBX.keyLastChanged.date) - writeObject(DatabaseKDBXXML.ElemDbKeyChangeRec, mDatabaseKDBX.keyChangeRecDays) - writeObject(DatabaseKDBXXML.ElemDbKeyChangeForce, mDatabaseKDBX.keyChangeForceDays) + writeString(DatabaseKDBXXML.ElemDbName, mDatabaseKDBX.name, true) + writeDateInstant(DatabaseKDBXXML.ElemDbNameChanged, mDatabaseKDBX.nameChanged) + writeString(DatabaseKDBXXML.ElemDbDesc, mDatabaseKDBX.description, true) + writeDateInstant(DatabaseKDBXXML.ElemDbDescChanged, mDatabaseKDBX.descriptionChanged) + writeString(DatabaseKDBXXML.ElemDbDefaultUser, mDatabaseKDBX.defaultUserName, true) + writeDateInstant(DatabaseKDBXXML.ElemDbDefaultUserChanged, mDatabaseKDBX.defaultUserNameChanged) + writeLong(DatabaseKDBXXML.ElemDbMntncHistoryDays, mDatabaseKDBX.maintenanceHistoryDays.toKotlinLong()) + writeString(DatabaseKDBXXML.ElemDbColor, mDatabaseKDBX.color) + writeDateInstant(DatabaseKDBXXML.ElemDbKeyChanged, mDatabaseKDBX.keyLastChanged) + writeLong(DatabaseKDBXXML.ElemDbKeyChangeRec, mDatabaseKDBX.keyChangeRecDays) + writeLong(DatabaseKDBXXML.ElemDbKeyChangeForce, mDatabaseKDBX.keyChangeForceDays) writeMemoryProtection(mDatabaseKDBX.memoryProtection) writeCustomIconList() - writeObject(DatabaseKDBXXML.ElemRecycleBinEnabled, mDatabaseKDBX.isRecycleBinEnabled) + writeBoolean(DatabaseKDBXXML.ElemRecycleBinEnabled, mDatabaseKDBX.isRecycleBinEnabled) writeUuid(DatabaseKDBXXML.ElemRecycleBinUuid, mDatabaseKDBX.recycleBinUUID) - writeObject(DatabaseKDBXXML.ElemRecycleBinChanged, mDatabaseKDBX.recycleBinChanged) + writeDateInstant(DatabaseKDBXXML.ElemRecycleBinChanged, mDatabaseKDBX.recycleBinChanged) writeUuid(DatabaseKDBXXML.ElemEntryTemplatesGroup, mDatabaseKDBX.entryTemplatesGroup) - writeObject(DatabaseKDBXXML.ElemEntryTemplatesGroupChanged, mDatabaseKDBX.entryTemplatesGroupChanged.date) - writeObject(DatabaseKDBXXML.ElemHistoryMaxItems, mDatabaseKDBX.historyMaxItems.toLong()) - writeObject(DatabaseKDBXXML.ElemHistoryMaxSize, mDatabaseKDBX.historyMaxSize) + writeDateInstant(DatabaseKDBXXML.ElemEntryTemplatesGroupChanged, mDatabaseKDBX.entryTemplatesGroupChanged) + writeLong(DatabaseKDBXXML.ElemHistoryMaxItems, mDatabaseKDBX.historyMaxItems.toLong()) + writeLong(DatabaseKDBXXML.ElemHistoryMaxSize, mDatabaseKDBX.historyMaxSize) writeUuid(DatabaseKDBXXML.ElemLastSelectedGroup, mDatabaseKDBX.lastSelectedGroupUUID) writeUuid(DatabaseKDBXXML.ElemLastTopVisibleGroup, mDatabaseKDBX.lastTopVisibleGroupUUID) // Seem to work properly if always in meta - if (header!!.version.isBefore(FILE_VERSION_32_4)) + if (header!!.version.isBefore(FILE_VERSION_40)) writeMetaBinaries() writeCustomData(mDatabaseKDBX.customData) @@ -309,7 +311,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, Log.e(TAG, "Unable to retrieve header", unknownKDF) } - if (header.version.isBefore(FILE_VERSION_32_4)) { + if (header.version.isBefore(FILE_VERSION_40)) { header.innerRandomStream = CrsAlgorithm.Salsa20 header.innerRandomStreamKey = ByteArray(32) } else { @@ -324,7 +326,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, throw DatabaseOutputException(e) } - if (header.version.isBefore(FILE_VERSION_32_4)) { + if (header.version.isBefore(FILE_VERSION_40)) { random.nextBytes(header.streamStartBytes) } @@ -353,19 +355,21 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, private fun startGroup(group: GroupKDBX) { xml.startTag(null, DatabaseKDBXXML.ElemGroup) writeUuid(DatabaseKDBXXML.ElemUuid, group.id) - writeObject(DatabaseKDBXXML.ElemName, group.title) - writeObject(DatabaseKDBXXML.ElemNotes, group.notes) - writeObject(DatabaseKDBXXML.ElemIcon, group.icon.standard.id.toLong()) + writeString(DatabaseKDBXXML.ElemName, group.title) + writeString(DatabaseKDBXXML.ElemNotes, group.notes) + writeLong(DatabaseKDBXXML.ElemIcon, group.icon.standard.id.toLong()) if (!group.icon.custom.isUnknown) { writeUuid(DatabaseKDBXXML.ElemCustomIconID, group.icon.custom.uuid) } + writeTags(group.tags) + writePreviousParentGroup(group.previousParentGroup) writeTimes(group) - writeObject(DatabaseKDBXXML.ElemIsExpanded, group.isExpanded) - writeObject(DatabaseKDBXXML.ElemGroupDefaultAutoTypeSeq, group.defaultAutoTypeSequence) - writeObject(DatabaseKDBXXML.ElemEnableAutoType, group.enableAutoType) - writeObject(DatabaseKDBXXML.ElemEnableSearching, group.enableSearching) + writeBoolean(DatabaseKDBXXML.ElemIsExpanded, group.isExpanded) + writeString(DatabaseKDBXXML.ElemGroupDefaultAutoTypeSeq, group.defaultAutoTypeSequence) + writeBoolean(DatabaseKDBXXML.ElemEnableAutoType, group.enableAutoType) + writeBoolean(DatabaseKDBXXML.ElemEnableSearching, group.enableSearching) writeUuid(DatabaseKDBXXML.ElemLastTopVisibleEntry, group.lastTopVisibleEntry) } @@ -380,24 +384,26 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, xml.startTag(null, DatabaseKDBXXML.ElemEntry) writeUuid(DatabaseKDBXXML.ElemUuid, entry.id) - writeObject(DatabaseKDBXXML.ElemIcon, entry.icon.standard.id.toLong()) + writeLong(DatabaseKDBXXML.ElemIcon, entry.icon.standard.id.toLong()) if (!entry.icon.custom.isUnknown) { writeUuid(DatabaseKDBXXML.ElemCustomIconID, entry.icon.custom.uuid) } - writeObject(DatabaseKDBXXML.ElemFgColor, entry.foregroundColor) - writeObject(DatabaseKDBXXML.ElemBgColor, entry.backgroundColor) - writeObject(DatabaseKDBXXML.ElemOverrideUrl, entry.overrideURL) - writeObject(DatabaseKDBXXML.ElemTags, entry.tags) + writeString(DatabaseKDBXXML.ElemFgColor, entry.foregroundColor) + writeString(DatabaseKDBXXML.ElemBgColor, entry.backgroundColor) + writeString(DatabaseKDBXXML.ElemOverrideUrl, entry.overrideURL) + // Write quality check only if false + if (!entry.qualityCheck) { + writeBoolean(DatabaseKDBXXML.ElemQualityCheck, entry.qualityCheck) + } + writeTags(entry.tags) + writePreviousParentGroup(entry.previousParentGroup) writeTimes(entry) - writeFields(entry.fields) writeEntryBinaries(entry.binaries) - if (entry.containsCustomData()) { - writeCustomData(entry.customData) - } + writeCustomData(entry.customData) writeAutoType(entry.autoType) if (!isHistory) { @@ -408,7 +414,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, } @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) - private fun writeObject(name: String, value: String, filterXmlChars: Boolean = false) { + private fun writeString(name: String, value: String, filterXmlChars: Boolean = false) { var xmlString = value xml.startTag(null, name) @@ -422,38 +428,37 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, } @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) - private fun writeObject(name: String, value: Date) { - if (header!!.version.isBefore(FILE_VERSION_32_4)) { - writeObject(name, DatabaseKDBXXML.DateFormatter.format(value)) + private fun writeDateInstant(name: String, value: DateInstant) { + val date = value.date + if (header!!.version.isBefore(FILE_VERSION_40)) { + writeString(name, DatabaseKDBXXML.DateFormatter.format(date)) } else { - val dt = DateTime(value) - val seconds = DateKDBXUtil.convertDateToKDBX4Time(dt) - val buf = longTo8Bytes(seconds) + val buf = longTo8Bytes(DateKDBXUtil.convertDateToKDBX4Time(DateTime(date))) val b64 = String(Base64.encode(buf, BASE_64_FLAG)) - writeObject(name, b64) + writeString(name, b64) } } @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) - private fun writeObject(name: String, value: Long) { - writeObject(name, value.toString()) + private fun writeLong(name: String, value: Long) { + writeString(name, value.toString()) } @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) - private fun writeObject(name: String, value: Boolean?) { + private fun writeBoolean(name: String, value: Boolean?) { val text: String = when { value == null -> DatabaseKDBXXML.ValNull value -> DatabaseKDBXXML.ValTrue else -> DatabaseKDBXXML.ValFalse } - writeObject(name, text) + writeString(name, text) } @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) private fun writeUuid(name: String, uuid: UUID) { val data = uuidTo16Bytes(uuid) - writeObject(name, String(Base64.encode(data, BASE_64_FLAG))) + writeString(name, String(Base64.encode(data, BASE_64_FLAG))) } /* @@ -514,34 +519,29 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, xml.endTag(null, DatabaseKDBXXML.ElemBinaries) } - @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) - private fun writeObject(name: String, keyName: String, keyValue: String, valueName: String, valueValue: String) { - xml.startTag(null, name) - - xml.startTag(null, keyName) - xml.text(safeXmlString(keyValue)) - xml.endTag(null, keyName) - - xml.startTag(null, valueName) - xml.text(safeXmlString(valueValue)) - xml.endTag(null, valueName) - - xml.endTag(null, name) - } - @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) private fun writeAutoType(autoType: AutoType) { xml.startTag(null, DatabaseKDBXXML.ElemAutoType) - writeObject(DatabaseKDBXXML.ElemAutoTypeEnabled, autoType.enabled) - writeObject(DatabaseKDBXXML.ElemAutoTypeObfuscation, autoType.obfuscationOptions.toKotlinLong()) + writeBoolean(DatabaseKDBXXML.ElemAutoTypeEnabled, autoType.enabled) + writeLong(DatabaseKDBXXML.ElemAutoTypeObfuscation, autoType.obfuscationOptions.toKotlinLong()) if (autoType.defaultSequence.isNotEmpty()) { - writeObject(DatabaseKDBXXML.ElemAutoTypeDefaultSeq, autoType.defaultSequence, true) + writeString(DatabaseKDBXXML.ElemAutoTypeDefaultSeq, autoType.defaultSequence, true) } - for ((key, value) in autoType.entrySet()) { - writeObject(DatabaseKDBXXML.ElemAutoTypeItem, DatabaseKDBXXML.ElemWindow, key, DatabaseKDBXXML.ElemKeystrokeSequence, value) + autoType.doForEachAutoTypeItem { key, value -> + xml.startTag(null, DatabaseKDBXXML.ElemAutoTypeItem) + + xml.startTag(null, DatabaseKDBXXML.ElemWindow) + xml.text(safeXmlString(key)) + xml.endTag(null, DatabaseKDBXXML.ElemWindow) + + xml.startTag(null, DatabaseKDBXXML.ElemKeystrokeSequence) + xml.text(safeXmlString(value)) + xml.endTag(null, DatabaseKDBXXML.ElemKeystrokeSequence) + + xml.endTag(null, DatabaseKDBXXML.ElemAutoTypeItem) } xml.endTag(null, DatabaseKDBXXML.ElemAutoType) @@ -592,7 +592,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, xml.startTag(null, DatabaseKDBXXML.ElemDeletedObject) writeUuid(DatabaseKDBXXML.ElemUuid, value.uuid) - writeObject(DatabaseKDBXXML.ElemDeletionTime, value.getDeletionTime()) + writeDateInstant(DatabaseKDBXXML.ElemDeletionTime, value.getDeletionTime()) xml.endTag(null, DatabaseKDBXXML.ElemDeletedObject) } @@ -632,43 +632,72 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, private fun writeMemoryProtection(value: MemoryProtectionConfig) { xml.startTag(null, DatabaseKDBXXML.ElemMemoryProt) - writeObject(DatabaseKDBXXML.ElemProtTitle, value.protectTitle) - writeObject(DatabaseKDBXXML.ElemProtUserName, value.protectUserName) - writeObject(DatabaseKDBXXML.ElemProtPassword, value.protectPassword) - writeObject(DatabaseKDBXXML.ElemProtURL, value.protectUrl) - writeObject(DatabaseKDBXXML.ElemProtNotes, value.protectNotes) + writeBoolean(DatabaseKDBXXML.ElemProtTitle, value.protectTitle) + writeBoolean(DatabaseKDBXXML.ElemProtUserName, value.protectUserName) + writeBoolean(DatabaseKDBXXML.ElemProtPassword, value.protectPassword) + writeBoolean(DatabaseKDBXXML.ElemProtURL, value.protectUrl) + writeBoolean(DatabaseKDBXXML.ElemProtNotes, value.protectNotes) xml.endTag(null, DatabaseKDBXXML.ElemMemoryProt) } @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) - private fun writeCustomData(customData: Map) { - xml.startTag(null, DatabaseKDBXXML.ElemCustomData) + private fun writeCustomData(customData: CustomData) { + if (customData.isNotEmpty()) { + xml.startTag(null, DatabaseKDBXXML.ElemCustomData) - for ((key, value) in customData) { - writeObject( - DatabaseKDBXXML.ElemStringDictExItem, - DatabaseKDBXXML.ElemKey, - key, - DatabaseKDBXXML.ElemValue, - value - ) + customData.doForEachItems { customDataItem -> + writeCustomDataItem(customDataItem) + } + + xml.endTag(null, DatabaseKDBXXML.ElemCustomData) + } + } + + private fun writeCustomDataItem(customDataItem: CustomDataItem) { + xml.startTag(null, DatabaseKDBXXML.ElemStringDictExItem) + + xml.startTag(null, DatabaseKDBXXML.ElemKey) + xml.text(safeXmlString(customDataItem.key)) + xml.endTag(null, DatabaseKDBXXML.ElemKey) + + xml.startTag(null, DatabaseKDBXXML.ElemValue) + xml.text(safeXmlString(customDataItem.value)) + xml.endTag(null, DatabaseKDBXXML.ElemValue) + + customDataItem.lastModificationTime?.let { lastModificationTime -> + writeDateInstant(DatabaseKDBXXML.ElemLastModTime, lastModificationTime) } - xml.endTag(null, DatabaseKDBXXML.ElemCustomData) + xml.endTag(null, DatabaseKDBXXML.ElemStringDictExItem) + } + + @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) + private fun writeTags(tags: Tags) { + if (!tags.isEmpty()) { + writeString(DatabaseKDBXXML.ElemTags, tags.toString()) + } + } + + @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) + private fun writePreviousParentGroup(previousParentGroup: UUID) { + if (!header!!.version.isBefore(FILE_VERSION_41) + && previousParentGroup != DatabaseVersioned.UUID_ZERO) { + writeUuid(DatabaseKDBXXML.ElemPreviousParentGroup, previousParentGroup) + } } @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) private fun writeTimes(node: NodeKDBXInterface) { xml.startTag(null, DatabaseKDBXXML.ElemTimes) - writeObject(DatabaseKDBXXML.ElemLastModTime, node.lastModificationTime.date) - writeObject(DatabaseKDBXXML.ElemCreationTime, node.creationTime.date) - writeObject(DatabaseKDBXXML.ElemLastAccessTime, node.lastAccessTime.date) - writeObject(DatabaseKDBXXML.ElemExpiryTime, node.expiryTime.date) - writeObject(DatabaseKDBXXML.ElemExpires, node.expires) - writeObject(DatabaseKDBXXML.ElemUsageCount, node.usageCount.toKotlinLong()) - writeObject(DatabaseKDBXXML.ElemLocationChanged, node.locationChanged.date) + writeDateInstant(DatabaseKDBXXML.ElemLastModTime, node.lastModificationTime) + writeDateInstant(DatabaseKDBXXML.ElemCreationTime, node.creationTime) + writeDateInstant(DatabaseKDBXXML.ElemLastAccessTime, node.lastAccessTime) + writeDateInstant(DatabaseKDBXXML.ElemExpiryTime, node.expiryTime) + writeBoolean(DatabaseKDBXXML.ElemExpires, node.expires) + writeLong(DatabaseKDBXXML.ElemUsageCount, node.usageCount.toKotlinLong()) + writeDateInstant(DatabaseKDBXXML.ElemLocationChanged, node.locationChanged) xml.endTag(null, DatabaseKDBXXML.ElemTimes) } @@ -709,9 +738,15 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, } catch (e: Exception) { Log.e(TAG, "Unable to write custom icon", e) } finally { - writeObject(DatabaseKDBXXML.ElemCustomIconItemData, + writeString(DatabaseKDBXXML.ElemCustomIconItemData, String(Base64.encode(customImageData, BASE_64_FLAG))) } + if (iconCustom.name.isNotEmpty()) { + writeString(DatabaseKDBXXML.ElemName, iconCustom.name) + } + iconCustom.lastModificationTime?.let { lastModificationTime -> + writeDateInstant(DatabaseKDBXXML.ElemLastModTime, lastModificationTime) + } xml.endTag(null, DatabaseKDBXXML.ElemCustomIconItem) } diff --git a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt index b24a1015d..cf1bfb179 100644 --- a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt @@ -280,8 +280,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress stopSelf() } else { // Restart the service to open lock notification - startService(Intent(applicationContext, - DatabaseTaskNotificationService::class.java)) + try { + startService(Intent(applicationContext, + DatabaseTaskNotificationService::class.java)) + } catch (e: IllegalStateException) {} } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/services/LockNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/LockNotificationService.kt index 8a9b9e855..f242c9db6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/services/LockNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/LockNotificationService.kt @@ -26,8 +26,9 @@ import com.kunzisoft.keepass.utils.unregisterLockReceiver abstract class LockNotificationService : NotificationService() { - private var onStart: Boolean = false - private var mLockReceiver: LockReceiver? = null + private var mLockReceiver: LockReceiver = LockReceiver { + actionOnLock() + } protected open fun actionOnLock() { // Stop the service in all cases @@ -36,30 +37,17 @@ abstract class LockNotificationService : NotificationService() { override fun onCreate() { super.onCreate() - // Register a lock receiver to stop notification service when lock on keyboard is performed - mLockReceiver = LockReceiver { - if (onStart) - actionOnLock() - } registerLockReceiver(mLockReceiver) } - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - onStart = true - return super.onStartCommand(intent, flags, startId) - } - override fun onTaskRemoved(rootIntent: Intent?) { - notificationManager?.cancel(notificationId) - + stopSelf() super.onTaskRemoved(rootIntent) } override fun onDestroy() { unregisterLockReceiver(mLockReceiver) - mLockReceiver = null - super.onDestroy() } } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/ParcelableUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/ParcelableUtil.kt index 50f103d41..d578df6ca 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/ParcelableUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/ParcelableUtil.kt @@ -42,8 +42,12 @@ object ParcelableUtil { val size = parcel.readInt() val map = HashMap(size) for (i in 0 until size) { - val key: K? = kClass.cast(parcel.readParcelable(kClass.classLoader)) - val value: V? = vClass.cast(parcel.readParcelable(vClass.classLoader)) + val key: K? = try { + parcel.readParcelable(kClass.classLoader) + } catch (e: Exception) { null } + val value: V? = try { + parcel.readParcelable(vClass.classLoader) + } catch (e: Exception) { null } if (key != null && value != null) map[key] = value } @@ -52,7 +56,7 @@ object ParcelableUtil { // For writing map with string key to a Parcel fun writeStringParcelableMap( - parcel: Parcel, flags: Int, map: LinkedHashMap) { + parcel: Parcel, flags: Int, map: HashMap) { parcel.writeInt(map.size) for ((key, value) in map) { parcel.writeString(key) @@ -76,7 +80,9 @@ object ParcelableUtil { val map = LinkedHashMap(size) for (i in 0 until size) { val key: String? = parcel.readString() - val value: V? = vClass.cast(parcel.readParcelable(vClass.classLoader)) + val value: V? = try { + parcel.readParcelable(vClass.classLoader) + } catch (e: Exception) { null } if (key != null && value != null) map[key] = value } diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/UnsignedInt.kt b/app/src/main/java/com/kunzisoft/keepass/utils/UnsignedInt.kt index 09853bf02..8ca151a24 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/UnsignedInt.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/UnsignedInt.kt @@ -19,8 +19,6 @@ */ package com.kunzisoft.keepass.utils -import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX - class UnsignedInt(private var unsignedValue: Int) { constructor(unsignedValue: UnsignedInt) : this(unsignedValue.toKotlinInt()) diff --git a/app/src/main/java/com/kunzisoft/keepass/view/EntryField.kt b/app/src/main/java/com/kunzisoft/keepass/view/EntryField.kt index 1c49ec078..464fe381f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/EntryField.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/EntryField.kt @@ -46,10 +46,10 @@ class EntryField @JvmOverloads constructor(context: Context, var hiddenProtectedValue: Boolean get() { - return showButtonView.isSelected + return !showButtonView.isSelected } set(value) { - showButtonView.isSelected = !value + showButtonView.isSelected = value changeProtectedValueParameters() } @@ -101,7 +101,7 @@ class EntryField @JvmOverloads constructor(context: Context, } else { setTextIsSelectable(true) } - applyHiddenStyle(isProtected && !showButtonView.isSelected) + applyHiddenStyle(isProtected && showButtonView.isSelected) if (!isProtected) linkify() } } diff --git a/app/src/main/res/layout/item_icon.xml b/app/src/main/res/layout/item_icon.xml index 015d6ae1b..b854e916e 100644 --- a/app/src/main/res/layout/item_icon.xml +++ b/app/src/main/res/layout/item_icon.xml @@ -17,17 +17,42 @@ You should have received a copy of the GNU General Public License along with KeePassDX. If not, see . --> - - - + android:layout_marginStart="16dp" + android:layout_marginLeft="16dp" + android:layout_marginEnd="16dp" + android:layout_marginRight="16dp" /> + + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index fa1746388..ea3ad4d50 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with KeePassDX. If not, see . --> - الصفحة الرئيسية + الصفحة الرئيسة قبول إضافة مجموعة التعمية @@ -106,8 +106,8 @@ إضافة حقول مخصصة نسخ حقل تأمين قاعدة البيانات - الأصداء - تنفيذ أندرويد لمدير كلمات السر «كي‌باس» + التغذية الراجعة + التنفيذ لمُدير كلمات المرور «كي‌ باس» على نظام أندرويد إضافة مدخلة تحرير مدخلة وظيفة اشتقاق المفتاح diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index ddc6051e6..9cf593557 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -22,4 +22,11 @@ ক্লিপবোর্ড ত্রুটি অনুমোদন ACTION_CREATE_DOCUMENT এবং ACTION_OPEN_DOCUMENT অভিপ্রায় গ্রহণ করে এমন একটি ফাইল ম্যানেজার ডাটাবেস ফাইলগুলো তৈরি করা, খোলা এবং সংরক্ষণ করতে প্রয়োজন। + ডিজিট + তথ্যভিত্তি + সরাও + হালনাগাদ + বাতিল + সত্যায়ন + পটভূমি \ No newline at end of file diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 63ff75ab3..a0136cd5c 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -25,7 +25,7 @@ Afegeix entrada Afegeix grup Algoritme de xifrat - Temps d\'espera de l\'aplicació + Temps d\'espera esgotat Temps d\'inactivitat abans de blocar la base de dades Aplicació Configuració de l\'aplicació @@ -289,4 +289,6 @@ L\'OTP existent no està reconegut per aquest formulari, la seva validació ja no pot generar correctament el token. No s\'ha pogut crear una base de dades amb aquesta contrasenya i arxiu de clau. No s\'ha pogut habilitar el servei d\'autocompletat. + Nodes fill + Funció de derivació de clau \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 06f994888..dde96db2e 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -24,7 +24,7 @@ Přidat záznam Přidat skupinu Šifrovací algoritmus - Časový limit + Časový limit překročen Doba nečinnosti, po které se aplikace zamkne Aplikace Nastavení aplikace @@ -548,4 +548,19 @@ Během nahrávání souboru došlo k chybě. Soubor, který se pokoušíte nahrát, je příliš velký. Info o jednorázovém hesle + Vlastnosti + Během exportu vlastností aplikace došlo k chybě + Vlastnosti aplikace byly exportovány + Během importu vlastností aplikace došlo k chybě + Vlastnosti aplikace byly importovány + Vlastnosti KeePassDX pro správu aplikačních nastavení + Pro export vlastností aplikace založte soubor + Exportovat vlastnosti aplikace + Pro import vlastostí aplikace zvolte soubor + Importovat vlastnosti aplikace + Během akce v databázi došlo k chybě. + Při odstraňování dat soboru došlo k chybě. + Datový soubor již existuje. + Sem skupinu přesunout nemůžete. + Toto slovo je rezervováno a nelze je použít. \ No newline at end of file diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 2f351e073..5cdf0e664 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -223,7 +223,7 @@ Brugerflade Øvrige Tastatur - Magikeyboard + Magi keyboard Aktiver et brugerdefineret tastatur, der udfylder adgangskoder og alle identitetsfelter Tillad ingen hovednøgle Tillader at trykke på knappen \"Åbn\", hvis der ikke er valgt nogen legitimationsoplysninger @@ -283,8 +283,8 @@ Tema, der bruges i programmet Ikonpakke Ikonpakke, der anvendes - Magikeyboard - Magikeyboard (KeePassDX) + Magi keyboard + Magi keyboard (KeePassDX) Magikeyboard indstillinger Post Udløbstid @@ -300,7 +300,7 @@ Taster Vibrerende tastetryk Hørbare tastetryk - Build %1$s + Byg %1$s Tidsudløb for at rydde indtastning Noter Valgstilstand @@ -426,7 +426,7 @@ Skjule brudte databaselinks Skjul brudte links på listen over seneste databaser Giv fil skriveadgang for at gemme databasændringer - Opsætning af engangs-adgangskode-styring (HOTP / TOTP) for at generere et token anmodet af tofaktor-autentisering (2FA). + Sæt op engangs-adgangskode-styring (HOTP / TOTP) for at generere et token anmodet af tofaktor-autentisering (2FA). Opsætning af OTP Databasefilen kunne ikke oprettes. Tilføj vedhæng @@ -537,7 +537,7 @@ Der opstod en fejl under udførelsen af en handling på databasen. Der opstod en fejl med at fjerne fildata. Den existerende OTP type kunne ikke genkendes, den kan være tiden er udløbet for at lave dette token. - Sammenkæd din adgangskode, med din scannede biometriske oplysninger eller enheds legitimationsoplysninger for, hurtigt at låse din database op. + Sammenkæd din adgangskode, med din scannede biometriske oplysninger eller enheds legitimationsoplysninger for hurtigt oplåsning af din database. Enter Varigheden af avanceret oplåsning, før indholdet slettes Giver dig mulighed for at bruge dine enhedsoplysninger for at åbne databasen @@ -561,4 +561,6 @@ Eksporter app-egenskaber Vælg en fil for at importere app-egenskaber Importer appegenskaber + Du kan flytte en gruppe her. + Dette ord er reseveret og kan ikke bruges. \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 62458a681..0b81b8806 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -30,17 +30,17 @@ Eintrag hinzufügen Gruppe hinzufügen Verschlüsselungsalgorithmus - Zeitüberschreitung - Inaktivitätszeit vor dem Sperren der Datenbank + Inaktivitätszeit + Zeit bis zum Sperren der Datenbank App - App-Einstellungen + Einstellungen Klammern Zum Erstellen, Öffnen und Speichern von Datenbankdateien wird ein Dateimanager benötigt, der die beabsichtigte Aktion ACTION_CREATE_DOCUMENT und ACTION_OPEN_DOCUMENT akzeptiert. Zwischenablage geleert Zwischenablage-Fehler Einige Geräte lassen keine Nutzung der Zwischenablage durch Apps zu. Leeren der Zwischenablage fehlgeschlagen - Zwischenablage-Timeout + Zwischenablage-Inaktivitätszeit Dauer der Speicherung in der Zwischenablage (falls vom Gerät unterstützt) %1$s in die Zwischenablage kopieren Datenbank-Schlüsseldatei erzeugen … @@ -71,7 +71,7 @@ Datenbank nicht lesbar. Sicherstellen, dass der Pfad korrekt ist. Namen eingeben. - Schlüsseldatei wählen. + Schlüsseldatei auswählen. Zu wenig Speicherplatz, um die ganze Datenbank zu laden. Mindestens eine Art der Passwortgenerierung muss ausgewählt sein. Die Passwörter stimmen nicht überein. @@ -83,7 +83,7 @@ Datei nicht gefunden. Bitte versuchen, sie über den Dateimanager zu öffnen. Dateimanager Passwort generieren - Passwort wiederholen + Passwort bestätigen Generiertes Passwort Name der Gruppe Schlüsseldatei @@ -104,14 +104,14 @@ Über Hauptschlüssel ändern Einstellungen - Datenbank-Einstellungen + Datenbankeinstellungen Löschen Spenden Bearbeiten Passwort verstecken Datenbank sperren Öffnen - Suchen + Suche Passwort anzeigen URL öffnen Bindestrich @@ -202,14 +202,14 @@ KeePassDX autom. Formularausfüllung Mit KeePassDX anmelden Standarddienst für automatisches Ausfüllen festlegen - Automatisches Ausfüllen aktivieren, um schnell Formulare in anderen Apps auszufüllen + Automatisches Ausfüllen aktivieren, um Formulare schnell in anderen Apps auszufüllen Zwischenablage Verschlüsselungsschlüssel löschen - Lösche alle Verschlüsselungsschlüssel, die mit der erweiterten Entsperrerkennung zusammenhängen + Alle Verschlüsselungsschlüssel löschen, die mit der modernen Entsperrerkennung zusammenhängen Auf dem Gerät läuft Android %1$s, eine Version ab %2$s wäre aber nötig. Keine entsprechende Hardware. Papierkorb-Nutzung - Verschiebt Gruppen oder Einträge in den Papierkorb, bevor sie gelöscht werden. + Verschiebt Gruppen oder Einträge in den Papierkorb, bevor sie gelöscht werden Feldschriftart Schriftart in Feldern ändern, um Lesbarkeit zu verbessern Zwischenablage vertrauen @@ -218,8 +218,8 @@ Datenbankbeschreibung Datenbankversion Text - Interface - Andere + Benutzeroberfläche + Sonstiges Tastatur Magikeyboard Eine eigene Tastatur zum einfachen Ausfüllen aller Passwort- und Identitätsfelder aktivieren @@ -297,8 +297,8 @@ Magikeyboard (KeePassDX) Magikeyboard-Einstellungen Eintrag - Timeout - Timeout zum Löschen der Tastatureingabe + Inaktivitätszeit + Zeit bis zum Löschen der Tastatureingabe Benachrichtigung Benachrichtigung anzeigen, wenn ein Eintrag abrufbar ist Eintrag @@ -379,8 +379,8 @@ Token muss %1$d bis %2$d Stellen enthalten. %1$s mit derselben UUID %2$s existiert bereits. Datenbank wird erstellt … - Sicherheits-Einstellungen - Hauptschlüssel-Einstellungen + Sicherheitseinstellungen + Hauptschlüsseleinstellungen Die Datenbank enthält UUID-Duplikate. Problem lösen, indem neue UUIDs für Duplikate generiert werden und danach fortfahren\? Datenbank geöffnet @@ -398,8 +398,8 @@ Ändern des Hauptschlüssels erforderlich (Tage) Erneuerung beim nächsten Mal erzwingen Änderung des Hauptschlüssels beim nächsten Mal erfordern (einmalig) - Vorgegebener Benutzername - Benutzerdefinierte Datenbankfarbe + Standard-Benutzername + Eigene Datenbankfarbe Kompression Keine Gzip @@ -426,7 +426,7 @@ Abgelaufene Einträge ausblenden Abgelaufene Einträge werden nicht angezeigt App-Design - Design, das in der App genutzt wird + In der App verwendetes Design Wald Göttlich @@ -443,7 +443,7 @@ Verwerfen Änderungen verwerfen\? Validieren - Automatisch Suchergebnisse nach Web-Domain oder Anwendungs-ID vorschlagen + Suchergebnisse automatisch nach Web-Domain oder Anwendungs-ID vorschlagen Automatische Suche Zeigt die Sperrtaste in der Benutzeroberfläche an Sperrtaste anzeigen @@ -454,7 +454,7 @@ Gemeinsame Infos durchsuchen Starten Sie die Anwendung, die das Formular enthält, neu, um die Sperrung zu aktivieren. Automatisches Ausfüllen sperren - Liste der Domains, auf denen ein automatisches Ausfüllen unterlassen wird + Liste der Domains, auf denen ein automatisches Ausfüllen verhindert wird Liste gesperrter Web-Domains Liste der Apps, in denen ein automatisches Ausfüllen verhindert wird Liste gesperrter Anwendungen @@ -497,11 +497,11 @@ Speichermodus Suchmodus Das Speichern eines neuen Elements in einer schreibgeschützten Datenbank ist nicht zulässig - Versuche bei manueller Eingabeauswahl gemeinsam genutzte Informationen zu speichern - Speichern von Daten anfordern, wenn ein Formular überprüft wird - Speichern von Daten anfordern + Suchdaten bei manueller Auswahl einer Eingabe wenn möglich speichern + Nachfragen, ob die Daten gespeichert werden sollen, wenn ein Formular ausgefüllt ist + Speichern von Daten abfragen Suchinformationen speichern - Datenbank nach der Auswahl des automatischen Ausfüllens schließen + Datenbank nach Auswahl eines Eintrags zum automatischen Ausfüllen schließen Versuche bei manueller Eingabeauswahl gemeinsam genutzte Informationen zu speichern Gemeinsam genutzte Informationen speichern Sollen alle ausgewählten Knoten wirklich aus dem Papierkorb gelöscht werden\? @@ -512,13 +512,13 @@ Schlüssel für modernes Entsperren löschen Moderne Entsperrerkennung Verknüpfen Sie Ihr Passwort mit Ihren gescannten Biometriedaten oder Daten zur Geräteanmeldung, um schnell Ihre Datenbank zu entsperren. - Erweiterte Entsperrung der Datenbank - Verfallzeit der erweiterten Entsperrung + Moderne Entsperrung der Datenbank + Verfallzeit der modernen Entsperrung Laufzeit der modernen Entsperrung bevor ihr Inhalt gelöscht wird Verfall der modernen Entsperrung - Keinen verschlüsselten Inhalt speichern, um moderne Entsperrung zu benutzen - Temporär moderne Entsperrung - Erlaubt Ihn die Geräteanmeldedaten zum Öffnen der Datenbank zu verwenden + Keine zur modernen Entsperrung verwendeten, verschlüsselten Inhalte speichern + Befristete moderne Entsperrung + Ermöglicht das Öffnen der Datenbank mit Ihren Geräte-Anmeldeinformationen Drücken, um Schlüssel für modernes Entsperren zu löschen Inhalt Datenbank mit moderner Entsperrerkennung öffnen @@ -527,7 +527,7 @@ Wähle Eintrag Zurück zur vorherigen Tastatur Benutzerdefinierte Felder - Alle Verschlüsselungsschlüssel, die mit der erweiterten Entsperrerkennung zusammenhängen, löschen\? + Alle Verschlüsselungsschlüssel, die mit der modernen Entsperrerkennung zusammenhängen, löschen\? Geräteanmeldedaten entsperren Geräteanmeldedaten Geben Sie das Passwort ein und klicken Sie dann auf diesen Knopf. @@ -543,9 +543,9 @@ Die Datei, die hochgeladen werden soll, ist zu groß. Die in Ihrer Datenbank enthaltenen Informationen wurden außerhalb der App geändert. Beim Löschen der Datei trat ein Fehler auf. - Die Datei gibt es bereits. + Die Datei existiert bereits. Beim Hochladen der Datei trat ein Fehler auf. - Importieren von Anwendungeneigenschaften + App-Eigenschaften importieren Beim Ausführen einer Aktion in der Datenbank ist ein Fehler aufgetreten. Der vorhandene Einmalpassworttyp wird von diesem Formular nicht erkannt, seine Validierung erzeugt das Token möglicherweise nicht mehr korrekt. Informationen zu Einmalpasswörtern @@ -554,8 +554,23 @@ App-Eigenschaften exportiert Fehler beim Importieren der App-Eigenschaften App-Eigenschaften importiert - Erstellen einer Datei zum Exportieren von App-Eigenschaften + Datei zum Exportieren von App-Eigenschaften erstellen App-Eigenschaften exportieren - Wählen Sie eine Datei zum Importieren von App-Eigenschaften + Datei zum Importieren von App-Eigenschaften auswählen Sie können hier keine Gruppe verschieben. + Ausfüllvorschläge + Dieses Wort ist reserviert und kann nicht verwendet werden. + Benutzerdefiniert + Standard + Helles oder dunkles Design auswählen + Designhelligkeit + GiB + MiB + KiB + B + Abgebrochen! + Vorschläge für automatisches Ausfüllen hinzugefügt. + Wenn möglich unmittelbar Vorschläge zum automatischen Ausfüllen auf einer kompatiblen Tastatur anzeigen + Eigenschaften + KeePassDX-Eigenschaften zur Verwaltung der App-Einstellungen \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index b67a05309..84af78f39 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -26,7 +26,7 @@ Añadir entrada Añadir grupo Algoritmo de cifrado - Tiempo de espera de la aplicación excedido + Tiempo de espera excedido Inactividad antes del bloqueo de aplicación Aplicación Configuración de la aplicación @@ -436,7 +436,7 @@ Base de datos abierta Adjuntar Cargue un archivo adjunto a la entrada para guardar datos externos importantes. - Las entradas caducadas no se muestran + Las entradas caducadas no se están mostrando La eliminación de datos no vinculados puede disminuir el tamaño de su base de datos, pero también puede eliminar los datos usados por los complementos de KeePass. Se supone que una base de datos KeePass contiene solo pequeños archivos de utilidad (como archivos de clave PGP). \n @@ -492,7 +492,7 @@ Ocultar las entradas expiradas Buscar información compartida Subir %1$s - Configurar la gestión de contraseñas de una sola vez (HOTP / TOTP) para generar un token solicitado para la autenticación de dos factores (2FA). + Configurar la gestión de contraseñas de un solo uso (HOTP / TOTP) para generar un token solicitado para la autenticación de dos factores (2FA). Establecer la contaseña de un solo uso Vincule su contraseña con su credencial biométrica o del dispositivo escaneada para desbloquear rápidamente su base de datos. Desbloqueo avanzado de la base de datos @@ -542,6 +542,26 @@ Recargar la base de datos El tipo de OTP existente no es reconocido por este formulario, su validación ya no puede generar correctamente el token. ¡Cancelado! - Los datos de archivo ya existen. - Ha habido un error al subir el archivo de datos. + Los datos del archivo ya existen. + Error al subir datos del archivo. + Propiedades de KeePassDX para gestionar la configuración de la aplicación + Información de contraseña de un solo uso + Personalizado + Estándar + Seleccionar temas oscuros o claros + Brillo del tema + Propiedades + Error al importar las propiedades de la aplicación + Error al exportar las propiedades de la aplicación + Propiedades de la aplicación exportadas + Propiedades de la aplicación importadas + Cree un archivo para exportar las propiedades de la aplicación + Exportar propiedades de la aplicación + Seleccione un archivo para importar las propiedades de la aplicación + Importar propiedades de la aplicación + Ocurrió un error al realizar una acción en la base de datos. + Ocurrió un error al eliminar los datos del archivo. + El archivo que está tratando de cargar es demasiado grande. + No puede mover un grupo aquí. + Esta palabra está reservada y no se puede usar. \ No newline at end of file diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 31f425d33..65a8c5fc8 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -18,7 +18,7 @@ along with KeePassDX. If not, see . --> - प्रतिक्रिया + प्प्रतिपुष्टिा होमपेज एंड्रॉयड पर आधारित KeePass पासवर्ड मैनेजर स्वीकार @@ -35,7 +35,7 @@ विस्तारित ASCII अनुमति दें क्लिपबोर्ड साफ कर दिया - क्लिपबोर्ड त्रुटि + क्लिपबोर्ड एरर सैमसंग के कुछ एंड्रॉइड फोन क्लिपबोर्ड का उपयोग नहीं करने देंगे। क्लिपबोर्ड को साफ़ नहीं किया जा सका क्लिपबोर्ड टाइमआउट @@ -104,7 +104,7 @@ काउंटर अंक एल्गोरिथ्म - OTP + ओटीप अमान्य ओटीपी गुप्त। कम से कम एक क्रेडेंशियल सेट किया जाना चाहिए। - \ No newline at end of file + diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index f0b3a81d9..89637a61c 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -367,7 +367,7 @@ Doprinos Ova oznaka već postoji. Za spremanje promjena u bazi podataka, datoteci dozvoli pisanje - Istek vremena aplikacije + Istek vremena Ponovo uklj/isklj vidljivosti lozinke Izbjegni u lozinkama koristiti znakove koji su izvan formata kodiranja teksta u datoteci baze podataka (neprepoznati znakovi pretvaraju se u isto slovo). Dodatni prolazi šifriranja pružaju veću zaštitu od brutalnih napada, ali stvarno mogu usporiti učitavanje i spremanje. @@ -555,4 +555,6 @@ Odaberi datoteku za uvoz svojstva aplikacije Uvezi svojstva aplikacije Došlo je do greške tijekom izvođenja radnje u bazi podataka. + Grupa se ne može ovdje premjestiti. + Ova je riječ rezervirana i ne može se koristiti. \ No newline at end of file diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 44ca19de3..6b4a97edc 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -160,7 +160,7 @@ ASCII Diperluas Tanda Kurung Aplikasi - Batas Waktu Aplikasi + Waktu habis Fungsi Derivasi Kunci Algoritma Enkripsi Enkripsi diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index efacdee98..9a5b3644c 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -564,4 +564,5 @@ Seleziona un file da cui importare le proprietà dell\'app Importa le proprietà dell\'app Questa parola è riservata e non può essere usata. + Non è possibile spostare un gruppo qui. \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 1ab676c90..1f4965fa0 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -31,7 +31,7 @@ 暗号化 暗号化アルゴリズム 鍵導出関数 - アプリのタイムアウト + タイムアウト この期間アプリの操作がなかった場合、データベースをロックします アプリ かっこ @@ -559,4 +559,6 @@ アプリのプロパティをエクスポートするファイルを作成します アプリのプロパティをエクスポートする データベースに対するアクションの実行中にエラーが発生しました。 + この単語は予約語のため使用できません。 + グループをここに移動できません。 \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 93d5ce1f5..fb9be9bd4 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -560,4 +560,6 @@ Wybierz plik, aby zaimportować właściwości aplikacji Importuj właściwości aplikacji Wystąpił błąd podczas wykonywania akcji w bazie danych. + Nie możesz przenieść tutaj grupy. + To słowo jest zastrzeżone i nie może być używane. \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index d092a2a09..b90f0759e 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -255,7 +255,7 @@ Solicitar uma pesquisa quando abrir a base de dados Pesquisa rápida Omite os grupos \"Backup\" e \"Cesto da reciclagem\" dos resultados da busca - Não procurar por entradas no backup ou na lixeira + Não procurar por entradas no backup ou no lixo Sobre Mascarar palavras-passe (***) por predefinição Esconder palavras-passe @@ -318,7 +318,7 @@ O período deve estar entre %1$d e %2$d segundos. O contador deve estar entre %1$d e %2$d. A chave secreta deve estar em formato Base32. - Mão pode copiar um grupo aqui. + Não pode copiar um grupo aqui. Ao menos uma credencial deve ser definida. Segredo OTP inválido. OTP @@ -400,7 +400,7 @@ Compilação %1$s A criar a chave da base de dados… Área de transferência - Mostrar nomes de utilizador em listas de entrada + Mostrar nomes de utilizador na lista entradas Mostrar nomes de utilizador Não foi possível carregar a chave. Tente descarregar o \"Uso de Memória\" do KDF. Não foi possível abrir a sua base de dados. @@ -441,7 +441,7 @@ Alguns aparelhos não deixam as apps usarem a área de transferência. Erro na área de transferência Permitir - ASCII Estendido + ASCII Extendido Parênteses App Inatividade antes de bloquear a base de dados @@ -452,4 +452,30 @@ Adicionar grupo Adicionar entrada Aceitar + Credencial do aparelho + Incapaz de inicializar o desbloqueio antecipado. + Erro de desbloqueio avançado: %1$s + Não conseguia reconhecer impressão de desbloqueio avançado + Não consegue ler a chave de desbloqueio avançada. Por favor, apague-a e repita o procedimento de desbloqueio de reconhecimento. + Extrair credencial de base de dados com dados de desbloqueio avançados + Base de dados aberta com reconhecimento avançado de desbloqueio + Advertência: Ainda precisa de se lembrar da sua palavra-passe principal se usar o reconhecimento avançado de desbloqueio. + Reconhecimento avançado de desbloqueio + Abrir o alerta de desbloqueio avançado para armazenar as credenciais + Abrir o alerta de desbloqueio avançado para desbloquear a base de dados + É necessária uma actualização de segurança biométrica. + O escaneamento biométrico é suportado, mas não configurado. + Acesso ao ficheiro revogado pelo gestor do ficheiro, fechar a base de dados e reabri-la a partir da sua localização. + Sobregravar as modificações externas, guardando a base de dados ou recarregando-a com as últimas alterações. + A informação contida no seu ficheiro de base de dados foi modificada fora da aplicação. + Apagar permanentemente todos os nós do caixote do lixo da reciclagem\? + Modo de registo + Modo Guardar + Modo de pesquisa + Apagar chave de desbloqueio avançada + Recarregar base de dados + Incapaz de reconstruir adequadamente a lista. + O URI da base de dados não pode ser recuperado. + O nome do campo já existe. + Salvar um novo item não é permitido numa base de dados só de leitura \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index f28203dc8..4efc04ef0 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -27,7 +27,7 @@ 延时 在锁定数据库前处于非活动状态的时长 应用 - 程序设置 + 应用设置 括号 需要一款接受意图操作 ACTION_CREATE_DOCUMENT 和 ACTION_OPEN_DOCUMENT 的文件管理器来创建、打开和保存数据库文件。 剪贴板已清空 @@ -46,14 +46,14 @@ 取消 备注 确认密码 - 新建时间 + 创建时间 过期时间 密钥文件 修改时间 密码 保存 名称 - 链接 + 网址 用户名 不支持Arcfour流式加密。 无法在KeePassDX中处理此URI。 @@ -96,7 +96,7 @@ 锁定数据库 打开 搜索 - 打开链接 + 打开网址 减号 从不 没有搜索结果 @@ -128,14 +128,14 @@ ASCII拓展区字符 允许 剪切板错误 - 一些设备不允许程序使用剪切板。 + 某些设备不允许应用程序使用剪贴板。 无法清空剪切板 主题 图标包 程序中使用的图标包 编辑条目 密钥推导函数 - 找不到条目。 + 找不到条目数据。 无法加载数据库。 无法加载密钥。尝试降低KDF的“内存使用”值。 无法启用自动填充服务。 @@ -153,10 +153,10 @@ 只读 可修改 搜索时忽略备份条目 - 搜索时忽略“备份”与“回收站”群组 + 从搜索结果中忽略“备份”和“回收站”群组 保护 只读 - KeePassDX需要写入权限以修改数据库。 + 根据您的文件管理器,KeePassDX 可能不允许在您的存储中写入数据。 最近文件历史 记住最近使用的文件名 加密所有数据时采用的算法。 @@ -178,8 +178,8 @@ 外观 常规 自动填充 - 使用KeePassDX自动填充 - 使用KeePassDX密码登录 + KeePassDX 自动填充 + 使用 KeePassDX 登录 剪贴板 剪贴板通知 锁定 @@ -334,7 +334,7 @@ 删除字段 UUID 无法移动条目到此处。 - 无法复制条目到此处。 + 您不能在此处复制条目。 显示条目数量 显示群组中的条目数 背景 @@ -350,17 +350,17 @@ 主密钥 安全 历史 - 设置一次性密码 - 一次性密码类型 + 设置 OTP + OTP 类型 密钥 时长(秒) 计数器 数字位数 算法 - 一次性密码 + OTP 错误的一次性密码密钥。 至少需要设置一个凭据。 - 这里不能复制组。 + 您无法在此处复制群组。 密钥必须是BASE32格式。 计数器必须在%1$d和%2$d之间。 时长必须在%1$d秒到%2$d秒之间。 @@ -369,11 +369,11 @@ 新建数据库… 安全设置 主密钥设置 - 数据库包含重复UUID。 - 通过为重复项生成新的UUID以解决问题? + 该数据库包含重复的 UUID。 + 通过为重复项生成新的 UUID 以解决问题? 数据库开启 使用设备的剪贴板来复制输入字段 - 使用高级解锁轻松打开数据库 + 使用高级解锁以便快速解锁数据库 数据压缩 数据压缩减少了数据库的大小 最大数量 @@ -444,7 +444,7 @@ 重新启动包含该表单的应用程序以激活拦截。 阻止自动填充 禁止在下列域名中自动填充凭证 - Web域名黑名单 + Web 域名黑名单 禁止应用程序自动填充的黑名单 应用拦截列表 过滤器 @@ -508,8 +508,8 @@ 用高级解锁识别打开数据库 警告:即使您使用高级解锁识别,您仍然需要记住您的主密码。 高级解锁识别 - 打开高级解锁提示来存储凭证 - 打开高级解锁提示来解锁数据库 + 点击以打开高级解锁提示来存储凭证 + 点击以使用生物识别解锁 删除高级解锁密钥 输入 退格键 @@ -550,15 +550,15 @@ 删除文件数据时发生了一个错误。 文件数据已存在。 属性 - 应用属性导出期间出错 - 已导出应用属性 - 导入应用属性期间出错 - 已导入应用属性 - 管理应用设置的 KeePassDX 属性 - 创建一个文件来导出应用属性 - 导出应用属性 - 选择一个文件来导入应用属性 - 导入应用属性 + 导出应用配置时出错 + 已导出应用配置 + 导入应用配置时出错 + 已导入应用配置 + 管理应用设置的 KeePassDX 配置 + 创建一个文件以导出应用配置 + 导出配置 + 选择一个文件以导入应用配置 + 导入配置 对数据库执行操作时发生了一个错误。 你不能把一个组移动到此处。 这个单词是保留的,不能使用。 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 6a0bba937..18faaca55 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -49,9 +49,9 @@ platform :android do lane :deploy_beta_free do upload_to_play_store( track: "beta", - skip_upload_metadata: "false", - skip_upload_images: "false", - skip_upload_screenshots: "false", + skip_upload_metadata: "true", + skip_upload_images: "true", + skip_upload_screenshots: "true", apk: "./app/build/outputs/apk/free/release/app-free-release.apk", validate_only: "false", ) @@ -62,9 +62,9 @@ platform :android do sh("cp", "-a", "./pro/.", "./") upload_to_play_store( track: "beta", - skip_upload_metadata: "false", - skip_upload_images: "false", - skip_upload_screenshots: "false", + skip_upload_metadata: "true", + skip_upload_images: "true", + skip_upload_screenshots: "true", apk: "./app/build/outputs/apk/pro/release/app-pro-release.apk", validate_only: "false", ) diff --git a/fastlane/metadata/android/en-US/changelogs/76.txt b/fastlane/metadata/android/en-US/changelogs/76.txt new file mode 100644 index 000000000..e6e0d5fbf --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/76.txt @@ -0,0 +1,3 @@ + * Manage new database format 4.1 #956 + * Fix show button consistency #980 + * Fix persistent notification #979 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/77.txt b/fastlane/metadata/android/en-US/changelogs/77.txt new file mode 100644 index 000000000..343800ee3 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/77.txt @@ -0,0 +1 @@ + * Fix parcelable with custom data #986 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/80.txt b/fastlane/metadata/android/en-US/changelogs/80.txt new file mode 100644 index 000000000..aa027e772 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/80.txt @@ -0,0 +1,2 @@ + * Fix search fields references #987 + * Fix Auto-Types with same key #997 \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/76.txt b/fastlane/metadata/android/fr-FR/changelogs/76.txt new file mode 100644 index 000000000..4999c12fb --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/76.txt @@ -0,0 +1,3 @@ + * Gestion du nouveau format de base de données 4.1 #956 + * Correction de la consistance du bouton de visibilité #980 + * Correction de la notification persistante #979 \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/77.txt b/fastlane/metadata/android/fr-FR/changelogs/77.txt new file mode 100644 index 000000000..79bdd3a47 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/77.txt @@ -0,0 +1 @@ + * Correction des parcelable avec données customisées #986 \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/80.txt b/fastlane/metadata/android/fr-FR/changelogs/80.txt new file mode 100644 index 000000000..193f200cd --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/80.txt @@ -0,0 +1,2 @@ + * Correction de la recherche des références de champs #987 + * Correction des Auto-Types avec la même clé #997 \ No newline at end of file