From 0647314374c79f0c6429fcb3d31183a0e78ca042 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Tue, 23 Aug 2016 17:56:16 +0200 Subject: [PATCH] =?UTF-8?q?thread=20safe=20KdbxGroup=20=E2=80=94=20group?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/FileCredentialStore.kt | 2 +- .../credential-store/src/kdbx/KdbxEntry.kt | 45 ------- .../credential-store/src/kdbx/KdbxGroup.kt | 121 ++++++++++++------ .../src/kdbx/KeePassDatabase.kt | 85 +++++------- platform/credential-store/src/kdbx/kdbx.kt | 118 +++++++---------- .../test/NativeKeychainTest.kt | 6 + .../src/com/intellij/jdom.kt | 2 + 7 files changed, 170 insertions(+), 209 deletions(-) diff --git a/platform/credential-store/src/FileCredentialStore.kt b/platform/credential-store/src/FileCredentialStore.kt index c4622f8a5d47..0345471b11cd 100644 --- a/platform/credential-store/src/FileCredentialStore.kt +++ b/platform/credential-store/src/FileCredentialStore.kt @@ -131,7 +131,7 @@ internal class FileCredentialStore(keyToValue: Map - @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() + 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(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 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 } -} \ No newline at end of file +} + +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 = 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() +) \ No newline at end of file diff --git a/platform/credential-store/src/kdbx/KeePassDatabase.kt b/platform/credential-store/src/kdbx/KeePassDatabase.kt index aa8dd30137a2..48d4f2206ba9 100644 --- a/platform/credential-store/src/kdbx/KeePassDatabase.kt +++ b/platform/credential-store/src/kdbx/KeePassDatabase.kt @@ -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 = mapOf ( +private val mandatoryEntryElements: Map = 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 = mapOf ( LOCATION_CHANGED to DateValueCreator() ) -private val mandatoryGroupElements: Map = 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) { +internal fun ensureElements(element: Element, childElements: Map) { 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 diff --git a/platform/credential-store/src/kdbx/kdbx.kt b/platform/credential-store/src/kdbx/kdbx.kt index e4183c366db0..bb686d6aaa42 100644 --- a/platform/credential-store/src/kdbx/kdbx.kt +++ b/platform/credential-store/src/kdbx/kdbx.kt @@ -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() @@ -125,67 +117,44 @@ private fun processEntries(groupElement: Element, processor: (container: Element internal fun createEmptyDatabase(): Element { val creationDate = formattedNow() return loadElement(""" - - IJ - - New Database - ${creationDate} - Empty Database - ${creationDate} - - ${creationDate} - 365 - - ${creationDate} - -1 - -1 - - False - False - True - False - False - - - True - AAAAAAAAAAAAAAAAAAAAAA== - ${creationDate} - AAAAAAAAAAAAAAAAAAAAAA== - ${creationDate} - AAAAAAAAAAAAAAAAAAAAAA== - AAAAAAAAAAAAAAAAAAAAAA== - 10 - 6291456 - - - - - - ${base64RandomUuid()} - Root - - 48 - - ${creationDate} - ${creationDate} - ${creationDate} - ${creationDate} - False - 0 - ${creationDate} - - True - - True - True - AAAAAAAAAAAAAAAAAAAAAA== - - - + + IJ + + New Database + ${creationDate} + Empty Database + ${creationDate} + + ${creationDate} + 365 + + ${creationDate} + -1 + -1 + + False + False + True + False + False + + + True + AAAAAAAAAAAAAAAAAAAAAA== + ${creationDate} + AAAAAAAAAAAAAAAAAAAAAA== + ${creationDate} + AAAAAAAAAAAAAAAAAAAAAA== + AAAAAAAAAAAAAAAAAAAAAA== + 10 + 6291456 + + + """) } -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 + } +} \ No newline at end of file diff --git a/platform/credential-store/test/NativeKeychainTest.kt b/platform/credential-store/test/NativeKeychainTest.kt index 2704021c0960..0c4c0864facb 100644 --- a/platform/credential-store/test/NativeKeychainTest.kt +++ b/platform/credential-store/test/NativeKeychainTest.kt @@ -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) diff --git a/platform/projectModel-impl/src/com/intellij/jdom.kt b/platform/projectModel-impl/src/com/intellij/jdom.kt index 547ab8f2c271..569b38fcb6a2 100644 --- a/platform/projectModel-impl/src/com/intellij/jdom.kt +++ b/platform/projectModel-impl/src/com/intellij/jdom.kt @@ -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)