Skip to content

Commit

Permalink
Merge pull request #13 from keemobile/refine/allow-untitled-fields
Browse files Browse the repository at this point in the history
Allow untitled fields while parsing entries
  • Loading branch information
Anvell authored Jan 22, 2024
2 parents 03abd81 + 17a0dbd commit 9feff36
Show file tree
Hide file tree
Showing 7 changed files with 80 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ internal object Defaults {
const val HistoryMaxItems = 10
const val HistoryMaxSize = 6 * 1024 * 1024
const val RecycleBinName = "Recycle Bin"
const val UntitledLabel = "Untitled"
}
16 changes: 11 additions & 5 deletions kotpass/src/main/kotlin/app/keemobile/kotpass/database/Decoder.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package app.keemobile.kotpass.database

import app.keemobile.kotpass.constants.Defaults
import app.keemobile.kotpass.cryptography.ContentEncryption
import app.keemobile.kotpass.cryptography.EncryptionSaltGenerator
import app.keemobile.kotpass.cryptography.KeyTransform
Expand All @@ -26,7 +27,8 @@ fun KeePassDatabase.Companion.decode(
inputStream: InputStream,
credentials: Credentials,
validateHashes: Boolean = true,
contentParser: XmlContentParser = DefaultXmlContentParser
contentParser: XmlContentParser = DefaultXmlContentParser,
untitledLabel: String = Defaults.UntitledLabel
): KeePassDatabase {
val headerBuffer = Buffer()

Expand Down Expand Up @@ -55,7 +57,8 @@ fun KeePassDatabase.Companion.decode(
XmlContext.Decode(
version = header.version,
encryption = saltGenerator,
binaries = meta.binaries
binaries = meta.binaries,
untitledLabel = untitledLabel
)
}
val headerHash = content.meta.headerHash
Expand Down Expand Up @@ -99,7 +102,8 @@ fun KeePassDatabase.Companion.decode(
XmlContext.Decode(
version = header.version,
encryption = saltGenerator,
binaries = innerHeader.binaries
binaries = innerHeader.binaries,
untitledLabel = untitledLabel
)
}
KeePassDatabase.Ver4x(credentials, header, content, innerHeader)
Expand All @@ -111,7 +115,8 @@ fun KeePassDatabase.Companion.decode(
fun KeePassDatabase.Companion.decodeFromXml(
inputStream: InputStream,
credentials: Credentials,
contentParser: XmlContentParser = DefaultXmlContentParser
contentParser: XmlContentParser = DefaultXmlContentParser,
untitledLabel: String = Defaults.UntitledLabel
): KeePassDatabase {
val header = DatabaseHeader.Ver4x.create()
var innerHeader = DatabaseInnerHeader.create()
Expand All @@ -123,7 +128,8 @@ fun KeePassDatabase.Companion.decodeFromXml(
XmlContext.Decode(
version = header.version,
encryption = saltGenerator,
binaries = meta.binaries
binaries = meta.binaries,
untitledLabel = untitledLabel
)
}
innerHeader = innerHeader.copy(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package app.keemobile.kotpass.models

import app.keemobile.kotpass.constants.Defaults
import app.keemobile.kotpass.cryptography.EncryptionSaltGenerator
import okio.ByteString

Expand All @@ -16,6 +17,7 @@ sealed class XmlContext {
class Decode(
override val version: FormatVersion,
val encryption: EncryptionSaltGenerator,
val binaries: Map<ByteString, BinaryData>
val binaries: Map<ByteString, BinaryData>,
val untitledLabel: String = Defaults.UntitledLabel
) : XmlContext()
}
38 changes: 35 additions & 3 deletions kotpass/src/main/kotlin/app/keemobile/kotpass/xml/Entry.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package app.keemobile.kotpass.xml

import app.keemobile.kotpass.builders.MutableEntry
import app.keemobile.kotpass.builders.buildEntry
import app.keemobile.kotpass.constants.Const
import app.keemobile.kotpass.constants.PredefinedIcon
Expand All @@ -24,6 +25,7 @@ internal fun unmarshalEntry(
context: XmlContext.Decode,
node: Node
): Entry {
val untitledFields = mutableListOf<EntryValue>()
val uuid = node
.firstOrNull(Tags.Uuid)
?.getUuid()
Expand Down Expand Up @@ -59,7 +61,12 @@ internal fun unmarshalEntry(
}
Tags.Entry.Fields.TagName -> {
val (name, value) = unmarshalField(context, childNode)
fields[name] = value

if (name != null) {
fields[name] = value
} else {
untitledFields += value
}
}
Tags.Entry.Tags -> {
childNode
Expand All @@ -84,6 +91,32 @@ internal fun unmarshalEntry(
}
}
}

if (untitledFields.isNotEmpty()) {
recoverUntitledFields(context, untitledFields)
}
}
}

/**
* Recovers up to [UInt.MAX_VALUE] untitled fields.
*/
private fun MutableEntry.recoverUntitledFields(
context: XmlContext.Decode,
untitledFields: List<EntryValue>
) {
for (value in untitledFields) {
var n = 1U
var name = context.untitledLabel

while (name in fields) {
name = "${context.untitledLabel} ($n)"
n++

if (n == UInt.MAX_VALUE) return
}

fields[name] = value
}
}

Expand All @@ -98,11 +131,10 @@ internal fun unmarshalEntries(
private fun unmarshalField(
context: XmlContext.Decode,
node: Node
): Pair<String, EntryValue> {
): Pair<String?, EntryValue> {
val key = node
.firstOrNull(Tags.Entry.Fields.ItemKey)
?.getText()
?: throw FormatError.InvalidXml("Invalid entry field without id.")
val protected = node
.firstOrNull(Tags.Entry.Fields.ItemValue)
?.get<String?>(FormatXml.Attributes.Protected)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package app.keemobile.kotpass.resources
package app.keemobile.kotpass.common

import app.keemobile.kotpass.database.Credentials
import app.keemobile.kotpass.database.KeePassDatabase
import app.keemobile.kotpass.database.decode
import app.keemobile.kotpass.xml.DefaultXmlContentParser
import app.keemobile.kotpass.xml.XmlContentParser
import org.jetbrains.annotations.TestOnly

@TestOnly
internal fun decodeFromResources(
path: String,
credentials: Credentials,
Expand Down
14 changes: 12 additions & 2 deletions kotpass/src/test/kotlin/app/keemobile/kotpass/models/EntrySpec.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package app.keemobile.kotpass.models

import app.keemobile.kotpass.builders.buildEntry
import app.keemobile.kotpass.common.decodeFromResources
import app.keemobile.kotpass.constants.BasicField
import app.keemobile.kotpass.constants.Defaults
import app.keemobile.kotpass.cryptography.EncryptedValue
import app.keemobile.kotpass.cryptography.EncryptionSaltGenerator
import app.keemobile.kotpass.database.Credentials
import app.keemobile.kotpass.database.modifiers.binaries
import app.keemobile.kotpass.database.traverse
import app.keemobile.kotpass.extensions.parseAsXml
import app.keemobile.kotpass.models.EntryValue.Plain
import app.keemobile.kotpass.resources.EntryRes
import app.keemobile.kotpass.resources.TimeDataRes
import app.keemobile.kotpass.resources.decodeFromResources
import app.keemobile.kotpass.xml.unmarshalEntry
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.collections.shouldBeEmpty
Expand All @@ -33,10 +35,18 @@ class EntrySpec : DescribeSpec({

entry.tags.size shouldBe 3
entry.tags.first() shouldBe "lorem"

entry.fields["custom1"] shouldNotBe null
entry.fields["custom2"] shouldNotBe null

entry.fields[Defaults.UntitledLabel] shouldBe Plain("content 1")
entry.fields["${Defaults.UntitledLabel} (1)"] shouldNotBe Plain("content 2")
entry.fields["${Defaults.UntitledLabel} (2)"] shouldBe Plain("content 2")
entry.fields["${Defaults.UntitledLabel} (3)"] shouldBe Plain("content 3")

entry.times shouldNotBe null
entry.times?.creationTime shouldBe TimeDataRes.ParsedDateTime

entry.history.size shouldBe 1
entry.binaries.size shouldBe 0
}
Expand All @@ -52,7 +62,7 @@ class EntrySpec : DescribeSpec({

it("Different order in fields should be reflected in equality check") {
val uuid = UUID.randomUUID()
val someValue = EntryValue.Plain("Some")
val someValue = Plain("Some")
val numberOfItems = 3
val items = (0..numberOfItems).associate { "$it" to someValue }
val reversed = (numberOfItems downTo 0).associate { "$it" to someValue }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package app.keemobile.kotpass.resources

import app.keemobile.kotpass.constants.Defaults

internal object EntryRes {
val BasicXml = """
<Entry>
Expand All @@ -24,6 +26,9 @@ internal object EntryRes {
<Key>custom2</Key>
<Value>dummy</Value>
</String>
<String>
<Value>content 1</Value>
</String>
<String>
<Key>Notes</Key>
<Value></Value>
Expand All @@ -44,6 +49,16 @@ internal object EntryRes {
<Key>UserName</Key>
<Value>User</Value>
</String>
<String>
<Value>content 2</Value>
</String>
<String>
<Value>content 3</Value>
</String>
<String>
<Key>${Defaults.UntitledLabel} (1)</Key>
<Value>Fizz</Value>
</String>
<AutoType>
<Enabled>True</Enabled>
<DataTransferObfuscation>0</DataTransferObfuscation>
Expand Down

0 comments on commit 9feff36

Please sign in to comment.