thread safe KdbxGroup — group

This commit is contained in:
Vladimir Krivosheev
2016-08-23 17:57:15 +02:00
parent 07f988d3ad
commit 0647314374
7 changed files with 170 additions and 209 deletions
@@ -131,7 +131,7 @@ internal class FileCredentialStore(keyToValue: Map<CredentialAttributes, Credent
db.rootGroup.getGroup(GROUP_NAME)?.removeEntry(attributes.serviceName, attributes.accountName)
}
else {
db.rootGroup.getOrCreateGroup(GROUP_NAME).getOrCreateEntry(attributes.serviceName, attributes.accountName).password = credentials.password
db.rootGroup.getOrCreateGroup(GROUP_NAME).getOrCreateEntry(attributes.serviceName, attributes.accountName ?: credentials.user).password = credentials.password
}
if (db.isDirty) {
@@ -18,7 +18,6 @@ package com.intellij.credentialStore.kdbx
import com.intellij.util.element
import com.intellij.util.getOrCreate
import org.jdom.Element
import java.time.format.DateTimeParseException
import java.util.*
import kotlin.reflect.KProperty
@@ -48,22 +47,14 @@ class KdbxEntry(val element: Element, internal val database: KeePassDatabase) {
}
}
val parent: KdbxGroup?
get() = element.parentElement?.let { KdbxGroup(it, database) }
val uuid: UUID
get() = uuidFromBase64(element.getChildText(UUID_ELEMENT_NAME)!!)
var userName: String? by PropertyDelegate("UserName")
var password: String? by PropertyDelegate("Password")
var url: String? by PropertyDelegate("URL")
var title: String? by PropertyDelegate("Title")
var notes: String? by PropertyDelegate("Notes")
var icon: Icon?
get() = DomIconWrapper(element.getChild(ICON_ELEMENT_NAME)!!)
set(value) {
@@ -72,30 +63,6 @@ class KdbxEntry(val element: Element, internal val database: KeePassDatabase) {
database.isDirty = true
}
val lastAccessTime: Date
get() = getTime(LAST_ACCESS_TIME_ELEMENT_NAME)
val creationTime: Date
get() = getTime(CREATION_TIME_ELEMENT_NAME)
val expiryTime: Date
get() = getTime(EXPIRY_TIME_ELEMENT_NAME)
val lastModificationTime: Date
get() = getTime(LAST_MODIFICATION_TIME_ELEMENT_NAME)
private fun getTime(name: String): Date {
element.getChildText(name)?.let {
try {
dateFormatter.parse(it)
}
catch (e: DateTimeParseException) {
return Date(0)
}
}
return Date(0)
}
private fun getPropertyContainer(name: String): Element? {
for (element in element.getChildren("String")) {
if (element.getChildText("Key") == name) {
@@ -122,18 +89,6 @@ class KdbxEntry(val element: Element, internal val database: KeePassDatabase) {
result = 31 * result + database.hashCode()
return result
}
val path: String
get() {
val parent = this.parent
var result = ""
if (parent != null) {
result = parent.path
}
return result + title
}
override fun toString() = this.path
}
private class PropertyDelegate(private val name: String) {
+81 -40
View File
@@ -1,55 +1,79 @@
package com.intellij.credentialStore.kdbx
import com.intellij.util.SmartList
import com.intellij.util.containers.ContainerUtil
import com.intellij.util.containers.Stack
import com.intellij.util.get
import com.intellij.util.getOrCreate
import org.jdom.Element
import java.util.*
import org.jdom.filter.ElementFilter
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneOffset
class KdbxGroup(private val element: Element, private val database: KeePassDatabase) {
val isRootGroup: Boolean
get() = parent != null && element.parentElement?.name == "Root"
var name: String
get() = element.getChildText(NAME_ELEMENT_NAME)
class KdbxGroup(private val element: Element, private val database: KeePassDatabase, private var parent: KdbxGroup?) {
@Volatile var name = element.getChildText(NAME_ELEMENT_NAME) ?: "Unnamed"
set(value) {
element.getOrCreate(NAME_ELEMENT_NAME).text = value
database.isDirty = true
if (field != value) {
field = value
database.isDirty = true
}
}
val uuid: UUID
get() = uuidFromBase64(element.getChildText(UUID_ELEMENT_NAME))
private val subGroups: MutableList<KdbxGroup>
@Suppress("ConvertLambdaToReference")
var icon: Icon?
get() = element.getChild(ICON_ELEMENT_NAME)?.let { DomIconWrapper(it) }
set(value) {
element.getOrCreate(ICON_ELEMENT_NAME).text = value!!.index.toString()
database.isDirty = true
private @Volatile var locationChanged = element.get("Times")?.get("LocationChanged")?.text?.let(::parseTime) ?: 0
init {
locationChanged = element.get("Times")?.get("LocationChanged")?.text?.let(::parseTime) ?: 0
val groups = SmartList<KdbxGroup>()
val groupIterator = element.getContent(ElementFilter(GROUP_ELEMENT_NAME)).iterator()
while (groupIterator.hasNext()) {
val child = groupIterator.next()
groups.add(KdbxGroup(child, database, this))
groupIterator.remove()
}
subGroups = ContainerUtil.createLockFreeCopyOnWriteList<KdbxGroup>(groups)
}
fun toXml(): Element {
val element = element.clone()
element.getOrCreate(NAME_ELEMENT_NAME).text = name
val locationChangedElement = element.getOrCreate("Times").getOrCreate("LocationChanged")
if (locationChanged == 0L) {
element.get("Times")?.get("CreationTime")?.text?.let {
locationChangedElement.text = it
}
}
else {
locationChangedElement.text = Instant.ofEpochMilli(locationChanged).atZone(ZoneOffset.UTC).format(dateFormatter)
}
val parent: KdbxGroup?
get() {
val parent = element.parentElement ?: return null
return if (isRootGroup) null else KdbxGroup(parent, database)
for (group in subGroups) {
element.addContent(group.toXml())
}
return element
}
fun addGroup(group: KdbxGroup): KdbxGroup {
if (group.isRootGroup) {
if (group == database.rootGroup) {
throw IllegalStateException("Cannot set root group as child of another group")
}
// skip if this is a new group with no parent
group.parent?.removeGroup(group)
element.addContent(group.element)
subGroups.add(group)
group.locationChanged = LocalDateTime.now(ZoneOffset.UTC).toEpochSecond(ZoneOffset.UTC)
group.element.getOrCreate("Times").getOrCreate("LocationChanged").text = formattedNow()
database.isDirty = true
return group
}
fun removeGroup(group: KdbxGroup): KdbxGroup {
element.removeContent(group.element)
database.isDirty = true
if (subGroups.remove(group)) {
database.isDirty = true
}
return group
}
@@ -57,25 +81,20 @@ class KdbxGroup(private val element: Element, private val database: KeePassDatab
getGroup(name)?.let { removeGroup(it) }
}
fun getGroup(name: String) = getGroup { it.name == name }
fun getGroup(name: String) = subGroups.firstOrNull { it.name == name }
fun getOrCreateGroup(name: String) = getGroup(name) ?: createGroup(name)
fun createGroup(name: String) = addGroup(database.createGroup(name))
fun createGroup(name: String): KdbxGroup {
val result = createGroup(database, this)
result.name = name
addGroup(result)
return result
}
val entries: List<KdbxEntry>
get() = element.getChildren(ENTRY_ELEMENT_NAME).map { KdbxEntry(it, database) }
private inline fun getGroup(matcher: (KdbxGroup) -> Boolean): KdbxGroup? {
for (groupElement in element.getChildren(GROUP_ELEMENT_NAME)) {
val item = KdbxGroup(groupElement, database)
if (matcher(item)) {
return item
}
}
return null
}
fun getEntry(matcher: (entry: KdbxEntry) -> Boolean): KdbxEntry? {
for (entryElement in element.getChildren(ENTRY_ELEMENT_NAME)) {
val entry = KdbxEntry(entryElement, database)
@@ -147,4 +166,26 @@ class KdbxGroup(private val element: Element, private val database: KeePassDatab
result = 31 * result + database.hashCode()
return result
}
}
}
internal fun createGroup(db: KeePassDatabase, parent: KdbxGroup?): KdbxGroup {
val element = Element(GROUP_ELEMENT_NAME)
ensureElements(element, mandatoryGroupElements)
val result = KdbxGroup(element, db, parent)
return result
}
private const val NOTES_ELEMENT_NAME = "Notes"
private val mandatoryGroupElements: Map<String, ValueCreator> = linkedMapOf (
UUID_ELEMENT_NAME to UuidValueCreator(),
NOTES_ELEMENT_NAME to ConstantValueCreator(""),
ICON_ELEMENT_NAME to ConstantValueCreator("0"),
CREATION_TIME_ELEMENT_NAME to DateValueCreator(),
LAST_MODIFICATION_TIME_ELEMENT_NAME to DateValueCreator(),
LAST_ACCESS_TIME_ELEMENT_NAME to DateValueCreator(),
EXPIRY_TIME_ELEMENT_NAME to DateValueCreator(),
EXPIRES_ELEMENT_NAME to ConstantValueCreator("False"),
USAGE_COUNT_ELEMENT_NAME to ConstantValueCreator("0"),
LOCATION_CHANGED to DateValueCreator()
)
@@ -1,29 +1,32 @@
package com.intellij.credentialStore.kdbx
import com.intellij.util.get
import com.intellij.util.getOrCreate
import org.jdom.Element
import org.jdom.xpath.XPath
import org.linguafranca.pwdb.kdbx.KdbxSerializer
import java.io.OutputStream
import java.nio.ByteBuffer
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.*
private const val LOCATION_CHANGED = "Times/LocationChanged"
private const val USAGE_COUNT_ELEMENT_NAME = "Times/UsageCount"
private const val EXPIRES_ELEMENT_NAME = "Times/Expires"
private const val TIMES_ELEMENT_NAME = "Times"
internal const val LOCATION_CHANGED = "Times/LocationChanged"
internal const val USAGE_COUNT_ELEMENT_NAME = "Times/UsageCount"
internal const val EXPIRES_ELEMENT_NAME = "Times/Expires"
internal const val GROUP_ELEMENT_NAME = "Group"
internal const val ENTRY_ELEMENT_NAME = "Entry"
internal const val ICON_ELEMENT_NAME = "IconID"
internal const val UUID_ELEMENT_NAME = "UUID"
internal const val NAME_ELEMENT_NAME = "Name"
private const val NOTES_ELEMENT_NAME = "Notes"
internal const val LAST_MODIFICATION_TIME_ELEMENT_NAME = "Times/LastModificationTime"
internal const val CREATION_TIME_ELEMENT_NAME = "Times/CreationTime"
internal const val LAST_ACCESS_TIME_ELEMENT_NAME = "Times/LastAccessTime"
internal const val EXPIRY_TIME_ELEMENT_NAME = "Times/ExpiryTime"
private const val ROOT_ELEMENT_NAME = "Root"
internal var dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
class KeePassDatabase(private val rootElement: Element = createEmptyDatabase()) {
@@ -33,40 +36,32 @@ class KeePassDatabase(private val rootElement: Element = createEmptyDatabase())
@Volatile var isDirty: Boolean = false
internal set
var name: String?
get() = dbMeta.getChildText("DatabaseName")
set(name) {
dbMeta.getOrCreate("DatabaseName").text = name
dbMeta.getOrCreate("DatabaseNameChanged").text = formattedNow()
isDirty = true
val rootGroup: KdbxGroup
init {
val rootElement = rootElement.get(ROOT_ELEMENT_NAME)
val groupElement = rootElement?.get("Group")
if (groupElement == null) {
rootGroup = createGroup(this, null)
rootGroup.name = ROOT_ELEMENT_NAME
}
else {
rootElement!!.removeChild("Group")
rootGroup = KdbxGroup(groupElement, this, null)
}
}
fun save(credentials: KeePassCredentials, outputStream: OutputStream) {
KdbxStreamFormat().save(rootElement, credentials, outputStream)
val element = rootElement.clone()
element.getOrCreate(ROOT_ELEMENT_NAME).addContent(rootGroup.toXml())
val kdbxHeader = KdbxHeader()
KdbxSerializer.createEncryptedOutputStream(credentials, kdbxHeader, outputStream).use {
element.getOrCreate("HeaderHash").text = Base64.getEncoder().encodeToString(kdbxHeader.headerHash)
save(element, it, Salsa20Encryption(kdbxHeader.protectedStreamKey))
}
isDirty = false
}
fun save(streamFormat: KdbxStreamFormat, credentials: KeePassCredentials, outputStream: OutputStream) {
streamFormat.save(rootElement, credentials, outputStream)
isDirty = false
}
// fun shouldProtect(name: String): Boolean {
// val protectionElement = DomHelper.getElement("MemoryProtection/Protect$name", dbMeta, false) ?: return false
// return protectionElement.textContent.toBoolean()
// }
val rootGroup: KdbxGroup
get() = KdbxGroup(rootElement.getChild("Root").getChild("Group"), this)
fun createGroup(name: String): KdbxGroup {
val element = Element(GROUP_ELEMENT_NAME)
ensureElements(element, mandatoryGroupElements)
val result = KdbxGroup(element, this)
result.name = name
return result
}
fun createEntry(title: String): KdbxEntry {
val element = Element(ENTRY_ELEMENT_NAME)
ensureElements(element, mandatoryEntryElements)
@@ -110,12 +105,11 @@ class DomIconWrapper(private val element: Element) : Icon {
override fun hashCode() = index
}
private val mandatoryEntryElements: Map<String, ValueCreator> = mapOf (
private val mandatoryEntryElements: Map<String, ValueCreator> = linkedMapOf (
UUID_ELEMENT_NAME to UuidValueCreator(),
ICON_ELEMENT_NAME to ConstantValueCreator("0"),
TIMES_ELEMENT_NAME to ConstantValueCreator(""),
LAST_MODIFICATION_TIME_ELEMENT_NAME to DateValueCreator(),
CREATION_TIME_ELEMENT_NAME to DateValueCreator(),
LAST_MODIFICATION_TIME_ELEMENT_NAME to DateValueCreator(),
LAST_ACCESS_TIME_ELEMENT_NAME to DateValueCreator(),
EXPIRY_TIME_ELEMENT_NAME to DateValueCreator(),
EXPIRES_ELEMENT_NAME to ConstantValueCreator("False"),
@@ -123,22 +117,7 @@ private val mandatoryEntryElements: Map<String, ValueCreator> = mapOf (
LOCATION_CHANGED to DateValueCreator()
)
private val mandatoryGroupElements: Map<String, ValueCreator> = mapOf (
UUID_ELEMENT_NAME to UuidValueCreator(),
NAME_ELEMENT_NAME to ConstantValueCreator(""),
NOTES_ELEMENT_NAME to ConstantValueCreator(""),
ICON_ELEMENT_NAME to ConstantValueCreator("0"),
TIMES_ELEMENT_NAME to ConstantValueCreator(""),
LAST_MODIFICATION_TIME_ELEMENT_NAME to DateValueCreator(),
CREATION_TIME_ELEMENT_NAME to DateValueCreator(),
LAST_ACCESS_TIME_ELEMENT_NAME to DateValueCreator(),
EXPIRY_TIME_ELEMENT_NAME to DateValueCreator(),
EXPIRES_ELEMENT_NAME to ConstantValueCreator("False"),
USAGE_COUNT_ELEMENT_NAME to ConstantValueCreator("0"),
LOCATION_CHANGED to DateValueCreator()
)
private fun ensureElements(element: Element, childElements: Map<String, ValueCreator>) {
internal fun ensureElements(element: Element, childElements: Map<String, ValueCreator>) {
for ((elementPath, value) in childElements) {
var result = XPath.newInstance(elementPath).selectSingleNode(element)
if (result == null) {
@@ -156,7 +135,7 @@ private fun createHierarchically(elementPath: String, startElement: Element): El
return currentElement
}
internal fun formattedNow() = LocalDateTime.now().format(dateFormatter)
internal fun formattedNow() = LocalDateTime.now(ZoneOffset.UTC).format(dateFormatter)
interface ValueCreator {
val value: String
+48 -70
View File
@@ -17,7 +17,6 @@ package com.intellij.credentialStore.kdbx
import com.intellij.openapi.util.JDOMUtil
import com.intellij.util.SmartList
import com.intellij.util.getOrCreate
import com.intellij.util.inputStream
import com.intellij.util.loadElement
import org.bouncycastle.crypto.engines.Salsa20Engine
@@ -29,6 +28,8 @@ import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Path
import java.security.MessageDigest
import java.time.ZonedDateTime
import java.time.format.DateTimeParseException
import java.util.*
import javax.xml.bind.DatatypeConverter
@@ -57,18 +58,9 @@ class KdbxStreamFormat {
return load(it, encryption)
}
}
fun save(element: Element, credentials: KeePassCredentials, outputStream: OutputStream) {
val kdbxHeader = KdbxHeader()
KdbxSerializer.createEncryptedOutputStream(credentials, kdbxHeader, outputStream).use {
val rootElement = element.clone()
rootElement.getOrCreate("HeaderHash").text = Base64.getEncoder().encodeToString(kdbxHeader.headerHash)
save(rootElement, it, Salsa20Encryption(kdbxHeader.protectedStreamKey))
}
}
}
private fun save(rootElement: Element, outputStream: OutputStream, encryption: KdbxEncryption) {
internal fun save(rootElement: Element, outputStream: OutputStream, encryption: KdbxEncryption) {
val meta = rootElement.getChild("Meta")?.getChild("MemoryProtection")
if (meta != null) {
val propertiesToProtect = SmartList<String>()
@@ -125,67 +117,44 @@ private fun processEntries(groupElement: Element, processor: (container: Element
internal fun createEmptyDatabase(): Element {
val creationDate = formattedNow()
return loadElement("""<KeePassFile>
<Meta>
<Generator>IJ</Generator>
<HeaderHash></HeaderHash>
<DatabaseName>New Database</DatabaseName>
<DatabaseNameChanged>${creationDate}</DatabaseNameChanged>
<DatabaseDescription>Empty Database</DatabaseDescription>
<DatabaseDescriptionChanged>${creationDate}</DatabaseDescriptionChanged>
<DefaultUserName/>
<DefaultUserNameChanged>${creationDate}</DefaultUserNameChanged>
<MaintenanceHistoryDays>365</MaintenanceHistoryDays>
<Color/>
<MasterKeyChanged>${creationDate}</MasterKeyChanged>
<MasterKeyChangeRec>-1</MasterKeyChangeRec>
<MasterKeyChangeForce>-1</MasterKeyChangeForce>
<MemoryProtection>
<ProtectTitle>False</ProtectTitle>
<ProtectUserName>False</ProtectUserName>
<ProtectPassword>True</ProtectPassword>
<ProtectURL>False</ProtectURL>
<ProtectNotes>False</ProtectNotes>
</MemoryProtection>
<CustomIcons/>
<RecycleBinEnabled>True</RecycleBinEnabled>
<RecycleBinUUID>AAAAAAAAAAAAAAAAAAAAAA==</RecycleBinUUID>
<RecycleBinChanged>${creationDate}</RecycleBinChanged>
<EntryTemplatesGroup>AAAAAAAAAAAAAAAAAAAAAA==</EntryTemplatesGroup>
<EntryTemplatesGroupChanged>${creationDate}</EntryTemplatesGroupChanged>
<LastSelectedGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastSelectedGroup>
<LastTopVisibleGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleGroup>
<HistoryMaxItems>10</HistoryMaxItems>
<HistoryMaxSize>6291456</HistoryMaxSize>
<Binaries/>
<CustomData/>
</Meta>
<Root>
<Group>
<UUID>${base64RandomUuid()}</UUID>
<Name>Root</Name>
<Notes/>
<IconID>48</IconID>
<Times>
<LastModificationTime>${creationDate}</LastModificationTime>
<CreationTime>${creationDate}</CreationTime>
<LastAccessTime>${creationDate}</LastAccessTime>
<ExpiryTime>${creationDate}</ExpiryTime>
<Expires>False</Expires>
<UsageCount>0</UsageCount>
<LocationChanged>${creationDate}</LocationChanged>
</Times>
<IsExpanded>True</IsExpanded>
<DefaultAutoTypeSequence/>
<EnableAutoType>True</EnableAutoType>
<EnableSearching>True</EnableSearching>
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
</Group>
<DeletedObjects/>
</Root>
<Meta>
<Generator>IJ</Generator>
<HeaderHash></HeaderHash>
<DatabaseName>New Database</DatabaseName>
<DatabaseNameChanged>${creationDate}</DatabaseNameChanged>
<DatabaseDescription>Empty Database</DatabaseDescription>
<DatabaseDescriptionChanged>${creationDate}</DatabaseDescriptionChanged>
<DefaultUserName/>
<DefaultUserNameChanged>${creationDate}</DefaultUserNameChanged>
<MaintenanceHistoryDays>365</MaintenanceHistoryDays>
<Color/>
<MasterKeyChanged>${creationDate}</MasterKeyChanged>
<MasterKeyChangeRec>-1</MasterKeyChangeRec>
<MasterKeyChangeForce>-1</MasterKeyChangeForce>
<MemoryProtection>
<ProtectTitle>False</ProtectTitle>
<ProtectUserName>False</ProtectUserName>
<ProtectPassword>True</ProtectPassword>
<ProtectURL>False</ProtectURL>
<ProtectNotes>False</ProtectNotes>
</MemoryProtection>
<CustomIcons/>
<RecycleBinEnabled>True</RecycleBinEnabled>
<RecycleBinUUID>AAAAAAAAAAAAAAAAAAAAAA==</RecycleBinUUID>
<RecycleBinChanged>${creationDate}</RecycleBinChanged>
<EntryTemplatesGroup>AAAAAAAAAAAAAAAAAAAAAA==</EntryTemplatesGroup>
<EntryTemplatesGroupChanged>${creationDate}</EntryTemplatesGroupChanged>
<LastSelectedGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastSelectedGroup>
<LastTopVisibleGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleGroup>
<HistoryMaxItems>10</HistoryMaxItems>
<HistoryMaxSize>6291456</HistoryMaxSize>
<Binaries/>
<CustomData/>
</Meta>
</KeePassFile>""")
}
private interface KdbxEncryption {
internal interface KdbxEncryption {
val key: ByteArray
fun decrypt(encryptedText: ByteArray): ByteArray
@@ -205,7 +174,7 @@ private fun createSalsa20(key: ByteArray): Salsa20Engine {
/**
* Salsa20 doesn't quite fit the KeePass memory model - all encrypted items have to be en/decrypted in order of encryption, i.e. in document order and at the same time.
*/
private class Salsa20Encryption(override val key: ByteArray) : KdbxEncryption {
internal class Salsa20Encryption(override val key: ByteArray) : KdbxEncryption {
private val salsa20 = createSalsa20(key)
override fun decrypt(encryptedText: ByteArray): ByteArray {
@@ -220,3 +189,12 @@ private class Salsa20Encryption(override val key: ByteArray) : KdbxEncryption {
return output
}
}
internal fun parseTime(value: String): Long {
try {
return ZonedDateTime.parse(value).toEpochSecond()
}
catch (e: DateTimeParseException) {
return 0
}
}
@@ -3,6 +3,7 @@ package com.intellij.credentialStore.linux
import com.intellij.credentialStore.CredentialAttributes
import com.intellij.credentialStore.CredentialStore
import com.intellij.credentialStore.Credentials
import com.intellij.credentialStore.FileCredentialStore
import com.intellij.credentialStore.macOs.KeyChainCredentialStore
import com.intellij.openapi.util.SystemInfo
import com.intellij.testFramework.UsefulTestCase
@@ -50,6 +51,11 @@ class NativeKeychainTest {
testEmptyAccountName(SecretCredentialStore("com.intellij.test"))
}
@Test
fun `keepass - testEmptyAccountName`() {
testEmptyAccountName(FileCredentialStore())
}
private fun doTest(store: CredentialStore) {
val pass = BigInteger(128, Random()).toString(32)
store.setPassword(CredentialAttributes(TEST_SERVICE_NAME, "test"), pass)
@@ -72,6 +72,8 @@ fun Element.getOrCreate(name: String): Element {
return element
}
fun Element.get(name: String): Element? = getChild(name)
fun Element.element(name: String): Element {
val element = Element(name)
addContent(element)