IDEA-200066 efficient implementation of load and save KeePass database

This commit is contained in:
Vladimir Krivosheev
2018-10-10 10:21:28 +02:00
parent 775ba3f258
commit e99e014c4e
14 changed files with 642 additions and 640 deletions
@@ -171,6 +171,7 @@ internal class KeePassCredentialStore constructor(internal val dbFile: Path,
val oldAttributes = toOldKey(requestor, userName)
db.rootGroup.getGroup(ROOT_GROUP_NAME)?.removeEntry(oldAttributes.serviceName, oldAttributes.userName)?.let {
fun createCredentials() = Credentials(userName, it.password?.get())
@Suppress("DEPRECATION")
set(CredentialAttributes(requestor, userName), createCredentials())
return createCredentials()
}
@@ -199,16 +200,6 @@ internal class KeePassCredentialStore constructor(internal val dbFile: Path,
}
}
fun copyTo(store: PasswordStorage) {
val group = db.rootGroup.getGroup(ROOT_GROUP_NAME) ?: return
for (entry in group.entries) {
val title = entry.title
if (title != null) {
store.set(CredentialAttributes(title, entry.userName), Credentials(entry.userName, entry.password?.get()))
}
}
}
/**
* [MasterKey.value] will be cleared on set
*/
@@ -89,16 +89,24 @@ private fun parseString(data: String, delimiter: Char): List<String> {
fun Credentials.serialize(storePassword: Boolean = true) = joinData(userName, if (storePassword) password else null)!!
@Suppress("FunctionName")
internal fun SecureString(value: CharSequence) = SecureString(Charsets.UTF_8.encode(CharBuffer.wrap(value)).toByteArray())
internal fun SecureString(value: CharSequence): SecureString = SecureStringImpl(value)
interface SecureString {
fun get(clearable: Boolean = true): OneTimeString
}
internal class SecureStringImpl(value: ByteArray) : SecureString {
constructor(value: CharSequence) : this(Charsets.UTF_8.encode(CharBuffer.wrap(value)).toByteArray())
internal class SecureString(value: ByteArray) {
companion object {
private val encryptionSupport = EncryptionSupport(SecretKeySpec(generateAesKey(), "AES"))
}
private val data = encryptionSupport.encrypt(value)
fun get(clearable: Boolean = true) = OneTimeString(encryptionSupport.decrypt(data), clearable = clearable)
override fun get(clearable: Boolean) = OneTimeString(getAsByteArray(), clearable = clearable)
fun getAsByteArray() = encryptionSupport.decrypt(data)
}
internal val ACCESS_TO_KEY_CHAIN_DENIED = Credentials(null, null as OneTimeString?)
@@ -55,7 +55,7 @@ final class HashedBlockInputStream extends InputStream {
private final InputStream inputStream;
private ByteArrayInputStream blockInputStream = new ByteArrayInputStream(new byte[0]);
private final MessageDigest md = KdbxHeaderKt.sha256MessageDigest();
private final MessageDigest md = KdbxKt.sha256MessageDigest();
HashedBlockInputStream(@NotNull InputStream inputStream) {
this.inputStream = inputStream;
@@ -53,7 +53,7 @@ final class HashedBlockOutputStream extends OutputStream {
private final ByteArrayOutputStream blockOutputStream = new ByteArrayOutputStream();
private boolean isClosed = false;
private final MessageDigest md = KdbxHeaderKt.sha256MessageDigest();
private final MessageDigest md = KdbxKt.sha256MessageDigest();
HashedBlockOutputStream(OutputStream outputStream) {
this.outputStream = outputStream;
@@ -87,7 +87,6 @@ final class HashedBlockOutputStream extends OutputStream {
outputStream.write(ZERO_HASH);
writeInt(0);
isClosed = true;
outputStream.flush();
outputStream.close();
}
+95 -62
View File
@@ -16,85 +16,118 @@
package com.intellij.credentialStore.kdbx
import com.intellij.credentialStore.SecureString
import com.intellij.util.element
import com.intellij.credentialStore.SecureStringImpl
import com.intellij.util.getOrCreate
import com.intellij.util.text.nullize
import org.jdom.Element
private const val VALUE_ELEMENT_NAME = "Value"
internal class KdbxEntry(private val element: Element, private val database: KeePassDatabase, @Volatile internal var group: KdbxGroup?) {
@Volatile var title: String? = element.removeProperty("Title")
internal class KdbxEntry(internal val entryElement: Element, private val database: KeePassDatabase, @Volatile internal var group: KdbxGroup?) {
var title: String?
get() = getProperty(KdbxEntryElementNames.title)
set(value) {
if (field != value) {
field = value
touch()
database.isDirty = true
}
setProperty(entryElement, value, KdbxEntryElementNames.title)
}
@Volatile var userName: String? = element.removeProperty("UserName")
var userName: String?
get() = getProperty(KdbxEntryElementNames.userName)
set(value) {
if (field != value) {
field = value
touch()
database.isDirty = true
}
setProperty(entryElement, value, KdbxEntryElementNames.userName)
}
@Volatile var password: SecureString? = element.removeProperty("Password")?.let(::SecureString)
set(value) {
if (field != value) {
field = value
touch()
database.isDirty = true
}
@Synchronized
private fun getProperty(propertyName: String): String? {
val valueElement = getPropertyElement(entryElement, propertyName)?.getChild(KdbxEntryElementNames.value)
if (valueElement == null) {
return null
}
fun toXml(): Element {
val element = element.clone()
element.setProperty("Title", title)
element.ensureProperty("URL")
element.setProperty("UserName", userName)
element.setProperty("Password", password?.get()?.toString())
element.ensureProperty("Notes")
return element
val value = valueElement.text.nullize()
if (isValueProtected(valueElement)) {
throw UnsupportedOperationException("$propertyName protection is not supported")
}
else {
return value
}
}
@Synchronized
private fun setProperty(entryElement: Element, value: String?, propertyName: String): Element? {
val normalizedValue = value.nullize()
var propertyElement = getPropertyElement(entryElement, propertyName)
if (propertyElement == null) {
if (normalizedValue == null) {
return null
}
propertyElement = createPropertyElement(entryElement, propertyName)
}
val valueElement = propertyElement.getOrCreate(KdbxEntryElementNames.value)
if (valueElement.text.nullize() == normalizedValue) {
return null
}
valueElement.text = value
if (entryElement === this.entryElement) {
touch()
}
return valueElement
}
var password: SecureString?
@Synchronized
get() {
val valueElement = getPropertyElement(entryElement, KdbxEntryElementNames.password)?.getChild(KdbxEntryElementNames.value) ?: return null
val value = valueElement.content.firstOrNull() ?: return null
if (value is SecureString) {
return value
}
valueElement.setAttribute(KdbxAttributeNames.protected, "True")
val result = UnsavedProtectedValue(SecureStringImpl(value.value))
valueElement.setContent(result)
return result
}
@Synchronized
set(value) {
if (value == null) {
val iterator = entryElement.getChildren(KdbxEntryElementNames.string).iterator()
for (element in iterator) {
if (element.getChildText(KdbxEntryElementNames.key) == KdbxEntryElementNames.password) {
iterator.remove()
touch()
}
}
return
}
val valueElement = getOrCreatePropertyElement(KdbxEntryElementNames.password).getOrCreate(KdbxEntryElementNames.value)
valueElement.setAttribute(KdbxAttributeNames.protected, "True")
val oldValue = valueElement.content.firstOrNull()
if (oldValue === value) {
return
}
valueElement.setContent(UnsavedProtectedValue(value as SecureStringImpl))
touch()
}
private fun getOrCreatePropertyElement(name: String) = getPropertyElement(entryElement, name) ?: createPropertyElement(entryElement, name)
@Synchronized
private fun touch() {
element.getOrCreate("Times").getOrCreate("LastModificationTime").text = formattedNow()
entryElement.getOrCreate("Times").getOrCreate("LastModificationTime").text = formattedNow()
database.isDirty = true
}
}
private fun Element.ensureProperty(name: String) {
val property = getPropertyContainer(name, false)
if (property == null) {
val container = element("String")
container.element("Key").addContent(name)
container.element("Value")
}
private fun getPropertyElement(element: Element, name: String): Element? {
return element.getChildren(KdbxEntryElementNames.string).firstOrNull { it.getChildText(KdbxEntryElementNames.key) == name }
}
private fun Element.getPropertyContainer(name: String, remove: Boolean): Element? {
val iterator = getChildren("String").iterator()
for (element in iterator) {
if (element.getChildText("Key") == name) {
if (remove) {
iterator.remove()
}
return element
}
}
return null
}
private fun Element.removeProperty(name: String) = getPropertyContainer(name, true)?.getChildText(VALUE_ELEMENT_NAME)
private fun Element.setProperty(name: String, value: String?) {
var item = getPropertyContainer(name, false)
if (item == null) {
item = element("String")
item.element("Key").addContent(name)
}
item.getOrCreate(VALUE_ELEMENT_NAME).text = value
private fun createPropertyElement(parentElement: Element, propertyName: String): Element {
val propertyElement = Element(KdbxEntryElementNames.string)
propertyElement.addContent(Element(KdbxEntryElementNames.key).setText(propertyName))
parentElement.addContent(propertyElement)
return propertyElement
}
+94 -99
View File
@@ -1,118 +1,117 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.credentialStore.kdbx
import com.intellij.credentialStore.LOG
import com.intellij.util.containers.ContainerUtil
import com.intellij.util.containers.Stack
import com.intellij.util.get
import com.intellij.util.getOrCreate
import com.intellij.util.remove
import gnu.trove.THashMap
import org.jdom.Element
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.ZonedDateTime
import java.time.format.DateTimeParseException
internal class KdbxGroup(private val element: Element, private val database: KeePassDatabase, @Volatile private var parent: KdbxGroup?) {
@Volatile var name: String = element.getChildText(NAME_ELEMENT_NAME) ?: "Unnamed"
internal class KdbxGroup(internal val element: Element, private val database: KeePassDatabase, @Volatile private var parent: KdbxGroup?) {
var name: String
@Synchronized
get() = element.getChildText(KdbxDbElementNames.name) ?: "Unnamed"
@Synchronized
set(value) {
if (field != value) {
field = value
database.isDirty = true
val nameElement = element.getOrCreate(KdbxDbElementNames.name)
if (nameElement.text == value) {
return
}
}
private val groups: MutableList<KdbxGroup>
val entries: MutableList<KdbxEntry>
@Volatile
private var locationChanged = element.get("Times")?.get("LocationChanged")?.text?.let(::parseTime) ?: 0
init {
locationChanged = element.get("Times")?.get("LocationChanged")?.text?.let(::parseTime) ?: 0
groups = ContainerUtil.createLockFreeCopyOnWriteList(element.remove(GROUP_ELEMENT_NAME) { KdbxGroup(it, database, this) })
entries = ContainerUtil.createLockFreeCopyOnWriteList(element.remove(ENTRY_ELEMENT_NAME) { KdbxEntry(it, database, this) })
}
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)
}
for (group in groups) {
element.addContent(group.toXml())
}
for (entry in entries) {
element.addContent(entry.toXml())
}
return element
}
fun addGroup(group: KdbxGroup): KdbxGroup {
if (group == database.rootGroup) {
throw IllegalStateException("Cannot set root group as child of another group")
}
group.parent?.removeGroup(group)
groups.add(group)
group.parent = this
group.locationChanged = LocalDateTime.now(ZoneOffset.UTC).toEpochSecond(ZoneOffset.UTC)
database.isDirty = true
return group
}
fun removeGroup(group: KdbxGroup): KdbxGroup {
if (groups.remove(group)) {
group.parent = null
nameElement.text = value
database.isDirty = true
}
return group
private val groups: MutableMap<String, KdbxGroup> = THashMap()
private val entries: MutableList<KdbxEntry> by lazy {
ContainerUtil.createLockFreeCopyOnWriteList(element.getChildren(KdbxDbElementNames.entry).map { KdbxEntry(it, database, this) })
}
private var locationChanged: Long
get() = element.getChild("Times")?.getChild("LocationChanged")?.text?.let(::parseTime) ?: 0
set(value) {
element.getOrCreate("Times").getOrCreate("LocationChanged").text = Instant.ofEpochMilli(value).atZone(ZoneOffset.UTC).format(dateFormatter)
}
@Synchronized
fun getGroup(name: String): KdbxGroup? {
var result = groups.get(name)
if (result != null) {
return result
}
val groupElement = element.content.firstOrNull { it is Element && it.getChildText(KdbxDbElementNames.name) == name } ?: return null
result = KdbxGroup(groupElement as Element, database, this)
groups.put(name, result)
return result
}
@Synchronized
private fun removeGroup(group: KdbxGroup) {
val removedGroup = groups.remove(group.name)
LOG.assertTrue(group === removedGroup)
element.content.remove(group.element)
group.parent = null
database.isDirty = true
}
@Synchronized
fun removeGroup(name: String) {
getGroup(name)?.let { removeGroup(it) }
}
fun getGroup(name: String): KdbxGroup? = groups.firstOrNull { it.name == name }
fun getOrCreateGroup(name: String): KdbxGroup = getGroup(name) ?: createGroup(name)
@Synchronized
fun getOrCreateGroup(name: String) = getGroup(name) ?: createGroup(name)
private fun createGroup(name: String): KdbxGroup {
val result = createGroup(database, this)
result.name = name
addGroup(result)
if (result == database.rootGroup) {
throw IllegalStateException("Cannot set root group as child of another group")
}
groups.put(result.name, result)
result.parent = this
result.locationChanged = LocalDateTime.now(ZoneOffset.UTC).toEpochSecond(ZoneOffset.UTC)
element.addContent(result.element)
database.isDirty = true
return result
}
fun getEntry(matcher: (entry: KdbxEntry) -> Boolean): KdbxEntry? = entries.firstOrNull(matcher)
@Synchronized
fun getEntry(matcher: (entry: KdbxEntry) -> Boolean) = entries.firstOrNull(matcher)
@Synchronized
fun addEntry(entry: KdbxEntry): KdbxEntry {
entry.group?.removeEntry(entry)
entries.add(entry)
entry.group = this
database.isDirty = true
element.addContent(entry.entryElement)
return entry
}
private fun removeEntry(entry: KdbxEntry): KdbxEntry {
if (entries.remove(entry)) {
entry.group = null
element.content.remove(entry.entryElement)
database.isDirty = true
}
return entry
}
fun getEntry(title: String, userName: String?): KdbxEntry? = getEntry { it.title == title && (it.userName == userName || userName == null) }
@Synchronized
fun getEntry(title: String, userName: String?): KdbxEntry? {
return getEntry {
it.title == title && (it.userName == userName || userName == null)
}
}
@Synchronized
fun getOrCreateEntry(title: String, userName: String?): KdbxEntry {
var entry = getEntry(title, userName)
if (entry == null) {
@@ -123,42 +122,38 @@ internal class KdbxGroup(private val element: Element, private val database: Kee
return entry
}
fun removeEntry(title: String, userName: String?): KdbxEntry? = getEntry(title, userName)?.let { removeEntry(it) }
val path: String
get() {
val parents = Stack<KdbxGroup>()
var parent: KdbxGroup = this
parents.push(this)
while (true) {
parent = parent.parent ?: break
parents.push(parent)
}
val result = StringBuilder("/")
while (parents.size > 0) {
result.append(parents.pop().name).append('/')
}
return result.toString()
@Synchronized
fun removeEntry(title: String, userName: String?): KdbxEntry? {
return getEntry(title, userName)?.let {
removeEntry(it)
}
override fun toString(): String = path
}
}
internal fun createGroup(db: KeePassDatabase, parent: KdbxGroup?): KdbxGroup {
val element = Element(GROUP_ELEMENT_NAME)
val element = Element(KdbxDbElementNames.group)
ensureElements(element, mandatoryGroupElements)
return KdbxGroup(element, db, parent)
}
private val mandatoryGroupElements: Map<Array<String>, ValueCreator> = linkedMapOf(
UUID_ELEMENT_NAME to UuidValueCreator(),
arrayOf("Notes") 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()
)
UUID_ELEMENT_NAME to UuidValueCreator(),
arrayOf("Notes") 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()
)
private fun parseTime(value: String): Long {
return try {
ZonedDateTime.parse(value).toEpochSecond()
}
catch (e: DateTimeParseException) {
0
}
}
+174 -18
View File
@@ -15,6 +15,8 @@
*/
package com.intellij.credentialStore.kdbx
import com.google.common.io.LittleEndianDataInputStream
import com.google.common.io.LittleEndianDataOutputStream
import com.intellij.credentialStore.createSecureRandom
import org.bouncycastle.crypto.engines.AESEngine
import org.bouncycastle.crypto.io.CipherInputStream
@@ -26,7 +28,9 @@ import org.bouncycastle.crypto.params.ParametersWithIV
import java.io.InputStream
import java.io.OutputStream
import java.nio.ByteBuffer
import java.security.MessageDigest
import java.security.DigestInputStream
import java.security.DigestOutputStream
import java.security.SecureRandom
import java.util.*
/**
@@ -40,6 +44,37 @@ import java.util.*
*/
private val AES_CIPHER = UUID.fromString("31C1F2E6-BF71-4350-BE58-05216AFC5AFF")
private const val FILE_VERSION_CRITICAL_MASK = 0xFFFF0000.toInt()
private const val SIG1 = 0x9AA2D903.toInt()
private const val SIG2 = 0xB54BFB67.toInt()
private const val FILE_VERSION_32 = 0x00030001
internal fun createProtectedStreamKey(random: SecureRandom) = random.generateSeed(32)
private object HeaderType {
const val END: Byte = 0
const val COMMENT: Byte = 1
const val CIPHER_ID: Byte = 2
const val COMPRESSION_FLAGS: Byte = 3
const val MASTER_SEED: Byte = 4
const val TRANSFORM_SEED: Byte = 5
const val TRANSFORM_ROUNDS: Byte = 6
const val ENCRYPTION_IV: Byte = 7
const val PROTECTED_STREAM_KEY: Byte = 8
const val STREAM_START_BYTES: Byte = 9
const val INNER_RANDOM_STREAM_ID: Byte = 10
}
private fun readSignature(input: LittleEndianDataInputStream): Boolean {
return input.readInt() == SIG1 && input.readInt() == SIG2
}
private fun verifyFileVersion(input: LittleEndianDataInputStream): Boolean {
return input.readInt() and FILE_VERSION_CRITICAL_MASK <= FILE_VERSION_32 and FILE_VERSION_CRITICAL_MASK
}
internal class KdbxHeader {
/**
* The ordinal 0 represents uncompressed and 1 GZip compressed
@@ -55,21 +90,19 @@ internal class KdbxHeader {
NONE, ARC_FOUR, SALSA_20
}
/* the cipher in use */
var cipherUuid = AES_CIPHER!!
private set
// the cipher in use
private var cipherUuid = AES_CIPHER
/* whether the data is compressed */
var compressionFlags = CompressionFlags.GZIP
private set
var masterSeed: ByteArray
var transformSeed: ByteArray
var transformRounds: Long = 6000
var encryptionIv: ByteArray
private var masterSeed: ByteArray
private var transformSeed: ByteArray
private var transformRounds: Long = 6000
private var encryptionIv: ByteArray
var protectedStreamKey: ByteArray
var protectedStreamAlgorithm = ProtectedStreamAlgorithm.SALSA_20
private set
private var protectedStreamAlgorithm = ProtectedStreamAlgorithm.SALSA_20
/* these bytes appear in cipher text immediately following the header */
var streamStartBytes = ByteArray(32)
@@ -82,7 +115,7 @@ internal class KdbxHeader {
masterSeed = random.generateSeed(32)
transformSeed = random.generateSeed(32)
encryptionIv = random.generateSeed(16)
protectedStreamKey = random.generateSeed(32)
protectedStreamKey = createProtectedStreamKey(random)
}
/**
@@ -103,7 +136,7 @@ internal class KdbxHeader {
return getEncryptedOutputStream(outputStream, finalKeyDigest, encryptionIv)
}
fun setCipherUuid(uuid: ByteArray) {
private fun setCipherUuid(uuid: ByteArray) {
val b = ByteBuffer.wrap(uuid)
val incoming = UUID(b.long, b.getLong(8))
if (incoming != AES_CIPHER) {
@@ -112,12 +145,115 @@ internal class KdbxHeader {
cipherUuid = incoming
}
fun setCompressionFlags(flags: Int) {
compressionFlags = CompressionFlags.values()[flags]
/**
* Populate a KdbxHeader from the input stream supplied
*/
internal fun readKdbxHeader(inputStream: InputStream) {
val digest = sha256MessageDigest()
// we do not close this stream, otherwise we lose our place in the underlying stream
val digestInputStream = DigestInputStream(inputStream, digest)
// we do not close this stream, otherwise we lose our place in the underlying stream
val input = LittleEndianDataInputStream(digestInputStream)
if (!readSignature(input)) {
throw KdbxException("Bad signature")
}
if (!verifyFileVersion(input)) {
throw IllegalStateException("File version did not match")
}
while (true) {
val headerType = input.readByte()
if (headerType == HeaderType.END) {
break
}
when (headerType) {
HeaderType.COMMENT -> getByteArray(input)
HeaderType.CIPHER_ID -> setCipherUuid(getByteArray(input))
HeaderType.COMPRESSION_FLAGS -> {
compressionFlags = CompressionFlags.values()[getInt(input)]
}
HeaderType.MASTER_SEED -> masterSeed = getByteArray(input)
HeaderType.TRANSFORM_SEED -> transformSeed = getByteArray(input)
HeaderType.TRANSFORM_ROUNDS -> transformRounds = getLong(input)
HeaderType.ENCRYPTION_IV -> encryptionIv = getByteArray(input)
HeaderType.PROTECTED_STREAM_KEY -> protectedStreamKey = getByteArray(input)
HeaderType.STREAM_START_BYTES -> streamStartBytes = getByteArray(input)
HeaderType.INNER_RANDOM_STREAM_ID -> {
protectedStreamAlgorithm = ProtectedStreamAlgorithm.values()[getInt(input)]
}
else -> throw IllegalStateException("Unknown File Header")
}
}
// consume length etc. following END flag
getByteArray(input)
headerHash = digest.digest()
}
fun setInnerRandomStreamId(innerRandomStreamId: Int) {
protectedStreamAlgorithm = ProtectedStreamAlgorithm.values()[innerRandomStreamId]
/**
* Write a KdbxHeader to the output stream supplied. The header is updated with the
* message digest of the written stream.
*/
fun writeKdbxHeader(outputStream: OutputStream) {
val messageDigest = sha256MessageDigest()
val digestOutputStream = DigestOutputStream(outputStream, messageDigest)
val output = LittleEndianDataOutputStream(digestOutputStream)
// write the magic number
output.writeInt(SIG1)
output.writeInt(SIG2)
// write a file version
output.writeInt(FILE_VERSION_32)
output.writeByte(HeaderType.CIPHER_ID.toInt())
output.writeShort(16)
val b = ByteArray(16)
val bb = ByteBuffer.wrap(b)
bb.putLong(cipherUuid.mostSignificantBits)
bb.putLong(8, cipherUuid.leastSignificantBits)
output.write(b)
output.writeByte(HeaderType.COMPRESSION_FLAGS.toInt())
output.writeShort(4)
output.writeInt(compressionFlags.ordinal)
output.writeByte(HeaderType.MASTER_SEED.toInt())
output.writeShort(masterSeed.size)
output.write(masterSeed)
output.writeByte(HeaderType.TRANSFORM_SEED.toInt())
output.writeShort(transformSeed.size)
output.write(transformSeed)
output.writeByte(HeaderType.TRANSFORM_ROUNDS.toInt())
output.writeShort(8)
output.writeLong(transformRounds)
output.writeByte(HeaderType.ENCRYPTION_IV.toInt())
output.writeShort(encryptionIv.size)
output.write(encryptionIv)
output.writeByte(HeaderType.PROTECTED_STREAM_KEY.toInt())
output.writeShort(protectedStreamKey.size)
output.write(protectedStreamKey)
output.writeByte(HeaderType.STREAM_START_BYTES.toInt())
output.writeShort(streamStartBytes.size)
output.write(streamStartBytes)
output.writeByte(HeaderType.INNER_RANDOM_STREAM_ID.toInt())
output.writeShort(4)
output.writeInt(protectedStreamAlgorithm.ordinal)
output.writeByte(HeaderType.END.toInt())
output.writeShort(0)
headerHash = digestOutputStream.messageDigest.digest()
}
}
@@ -141,8 +277,6 @@ private fun getFinalKeyDigest(key: ByteArray, masterSeed: ByteArray, transformSe
return md.digest(transformedKeyDigest)
}
fun sha256MessageDigest(): MessageDigest = MessageDigest.getInstance("SHA-256")
/**
* Create a decrypted input stream from an encrypted one
*/
@@ -161,4 +295,26 @@ private fun getEncryptedOutputStream(decryptedOutputStream: OutputStream, keyDat
val cipher = PaddedBufferedBlockCipher(CBCBlockCipher(AESEngine()))
cipher.init(true, keyAndIV)
return CipherOutputStream(decryptedOutputStream, cipher)
}
private fun getInt(input: LittleEndianDataInputStream): Int {
val fieldLength = input.readShort()
if (fieldLength.toInt() != 4) {
throw IllegalStateException("Int required but length was $fieldLength")
}
return input.readInt()
}
private fun getLong(input: LittleEndianDataInputStream): Long {
val fieldLength = input.readShort()
if (fieldLength.toInt() != 8) {
throw IllegalStateException("Long required but length was $fieldLength")
}
return input.readLong()
}
private fun getByteArray(input: LittleEndianDataInputStream): ByteArray {
val value = ByteArray(input.readShort().toInt())
input.readFully(value)
return value
}
@@ -1,267 +0,0 @@
/*
* Copyright 2015 Jo Rabin
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.credentialStore.kdbx
import com.google.common.io.LittleEndianDataInputStream
import com.google.common.io.LittleEndianDataOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.nio.ByteBuffer
import java.security.DigestInputStream
import java.security.DigestOutputStream
import java.util.*
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
/**
* A KDBX file is little-endian and consists of the following:
*
* 1. An unencrypted portion
*
* 1. 8 bytes Magic number
* 1. 4 bytes version
* 1. A header containing details of the encryption of the remainder of the file
*
* The header fields are encoded using a TLV style. The Type is an enumeration encoded in 1 byte.
* The length is encoded in 2 bytes and the value according to the length denoted. The sequence is
* terminated by a zero type with 0 length.
*
* 1. An encrypted portion
*
* 1. A sequence of bytes contained in the header. If they don't match, decryption has not worked.
* 1. A payload serialized in Hashed Block format.
*
* The methods in this class provide support for serializing and deserializing plain text payload content
* to and from the above format.
* @author jo
*/
internal object KdbxSerializer {
/**
* Provides the payload of a KDBX file as an unencrypted [InputStream].
* @param credentials credentials for decryption of the stream
* @param kdbxHeader a header instance to be populated with values from the stream
* @param inputStream a KDBX formatted input stream
* @return an unencrypted input stream, to be read and closed by the caller
*/
fun createUnencryptedInputStream(credentials: KeePassCredentials, kdbxHeader: KdbxHeader, inputStream: InputStream): InputStream {
readKdbxHeader(kdbxHeader, inputStream)
val decryptedInputStream = kdbxHeader.createDecryptedStream(credentials.key, inputStream)
checkStartBytes(kdbxHeader, decryptedInputStream)
val blockInputStream = HashedBlockInputStream(decryptedInputStream)
if (kdbxHeader.compressionFlags == KdbxHeader.CompressionFlags.NONE) {
return blockInputStream
}
return GZIPInputStream(blockInputStream)
}
/**
* Provides an [OutputStream] to be encoded and encrypted in KDBX format
* @param credentials credentials for encryption of the stream
* @param kdbxHeader a KDBX header to control the formatting and encryption operation
* @param outputStream output stream to contain the KDBX formatted output
* @return an unencrypted output stream, to be written to, flushed and closed by the caller
*/
fun createEncryptedOutputStream(credentials: KeePassCredentials, kdbxHeader: KdbxHeader, outputStream: OutputStream): OutputStream {
writeKdbxHeader(kdbxHeader, outputStream)
val encryptedOutputStream = kdbxHeader.createEncryptedStream(credentials.key, outputStream)
LittleEndianDataOutputStream(encryptedOutputStream).write(kdbxHeader.streamStartBytes)
val blockOutputStream = HashedBlockOutputStream(encryptedOutputStream)
if (kdbxHeader.compressionFlags == KdbxHeader.CompressionFlags.NONE) {
return blockOutputStream
}
return GZIPOutputStream(blockOutputStream)
}
}
private fun checkStartBytes(kdbxHeader: KdbxHeader, decryptedInputStream: InputStream) {
val startBytes = ByteArray(32)
LittleEndianDataInputStream(decryptedInputStream).readFully(startBytes)
if (!Arrays.equals(startBytes, kdbxHeader.streamStartBytes)) {
throw IncorrectMasterPasswordException()
}
}
internal class IncorrectMasterPasswordException(val isFileMissed: Boolean = false) : RuntimeException()
private const val SIG1 = 0x9AA2D903.toInt()
private const val SIG2 = 0xB54BFB67.toInt()
private const val FILE_VERSION_CRITICAL_MASK = 0xFFFF0000.toInt()
private const val FILE_VERSION_32 = 0x00030001
private object HeaderType {
internal const val END: Byte = 0
internal const val COMMENT: Byte = 1
internal const val CIPHER_ID: Byte = 2
internal const val COMPRESSION_FLAGS: Byte = 3
internal const val MASTER_SEED: Byte = 4
internal const val TRANSFORM_SEED: Byte = 5
internal const val TRANSFORM_ROUNDS: Byte = 6
internal const val ENCRYPTION_IV: Byte = 7
internal const val PROTECTED_STREAM_KEY: Byte = 8
internal const val STREAM_START_BYTES: Byte = 9
internal const val INNER_RANDOM_STREAM_ID: Byte = 10
}
private fun verifyMagicNumber(input: LittleEndianDataInputStream): Boolean {
val sig1 = input.readInt()
val sig2 = input.readInt()
return sig1 == SIG1 && sig2 == SIG2
}
private fun verifyFileVersion(input: LittleEndianDataInputStream): Boolean {
return input.readInt() and FILE_VERSION_CRITICAL_MASK <= FILE_VERSION_32 and FILE_VERSION_CRITICAL_MASK
}
/**
* Populate a KdbxHeader from the input stream supplied
* @param kdbxHeader a header to be populated
* @param inputStream an input stream
* @return the populated KdbxHeader
*/
internal fun readKdbxHeader(kdbxHeader: KdbxHeader, inputStream: InputStream): KdbxHeader {
val digest = sha256MessageDigest()
// we do not close this stream, otherwise we lose our place in the underlying stream
val digestInputStream = DigestInputStream(inputStream, digest)
// we do not close this stream, otherwise we lose our place in the underlying stream
val input = LittleEndianDataInputStream(digestInputStream)
if (!verifyMagicNumber(input)) {
throw IllegalStateException("Magic number did not match")
}
if (!verifyFileVersion(input)) {
throw IllegalStateException("File version did not match")
}
while (true) {
val headerType = input.readByte()
if (headerType == HeaderType.END) {
break
}
when (headerType) {
HeaderType.COMMENT -> getByteArray(input)
HeaderType.CIPHER_ID -> kdbxHeader.setCipherUuid(getByteArray(input))
HeaderType.COMPRESSION_FLAGS -> kdbxHeader.setCompressionFlags(getInt(input))
HeaderType.MASTER_SEED -> kdbxHeader.masterSeed = getByteArray(input)
HeaderType.TRANSFORM_SEED -> kdbxHeader.transformSeed = getByteArray(input)
HeaderType.TRANSFORM_ROUNDS -> kdbxHeader.transformRounds = getLong(input)
HeaderType.ENCRYPTION_IV -> kdbxHeader.encryptionIv = getByteArray(input)
HeaderType.PROTECTED_STREAM_KEY -> kdbxHeader.protectedStreamKey = getByteArray(input)
HeaderType.STREAM_START_BYTES -> kdbxHeader.streamStartBytes = getByteArray(input)
HeaderType.INNER_RANDOM_STREAM_ID -> kdbxHeader.setInnerRandomStreamId(getInt(input))
else -> throw IllegalStateException("Unknown File Header")
}
}
// consume length etc. following END flag
getByteArray(input)
kdbxHeader.headerHash = digest.digest()
return kdbxHeader
}
/**
* Write a KdbxHeader to the output stream supplied. The header is updated with the
* message digest of the written stream.
* @param kdbxHeader the header to write and update
* @param outputStream the output stream
*/
internal fun writeKdbxHeader(kdbxHeader: KdbxHeader, outputStream: OutputStream) {
val messageDigest = sha256MessageDigest()
val digestOutputStream = DigestOutputStream(outputStream, messageDigest)
val output = LittleEndianDataOutputStream(digestOutputStream)
// write the magic number
output.writeInt(SIG1)
output.writeInt(SIG2)
// write a file version
output.writeInt(FILE_VERSION_32)
output.writeByte(HeaderType.CIPHER_ID.toInt())
output.writeShort(16)
val b = ByteArray(16)
val bb = ByteBuffer.wrap(b)
bb.putLong(kdbxHeader.cipherUuid.mostSignificantBits)
bb.putLong(8, kdbxHeader.cipherUuid.leastSignificantBits)
output.write(b)
output.writeByte(HeaderType.COMPRESSION_FLAGS.toInt())
output.writeShort(4)
output.writeInt(kdbxHeader.compressionFlags.ordinal)
output.writeByte(HeaderType.MASTER_SEED.toInt())
output.writeShort(kdbxHeader.masterSeed.size)
output.write(kdbxHeader.masterSeed)
output.writeByte(HeaderType.TRANSFORM_SEED.toInt())
output.writeShort(kdbxHeader.transformSeed.size)
output.write(kdbxHeader.transformSeed)
output.writeByte(HeaderType.TRANSFORM_ROUNDS.toInt())
output.writeShort(8)
output.writeLong(kdbxHeader.transformRounds)
output.writeByte(HeaderType.ENCRYPTION_IV.toInt())
output.writeShort(kdbxHeader.encryptionIv.size)
output.write(kdbxHeader.encryptionIv)
output.writeByte(HeaderType.PROTECTED_STREAM_KEY.toInt())
output.writeShort(kdbxHeader.protectedStreamKey.size)
output.write(kdbxHeader.protectedStreamKey)
output.writeByte(HeaderType.STREAM_START_BYTES.toInt())
output.writeShort(kdbxHeader.streamStartBytes.size)
output.write(kdbxHeader.streamStartBytes)
output.writeByte(HeaderType.INNER_RANDOM_STREAM_ID.toInt())
output.writeShort(4)
output.writeInt(kdbxHeader.protectedStreamAlgorithm.ordinal)
output.writeByte(HeaderType.END.toInt())
output.writeShort(0)
kdbxHeader.headerHash = digestOutputStream.messageDigest.digest()
}
private fun getInt(input: LittleEndianDataInputStream): Int {
val fieldLength = input.readShort()
if (fieldLength.toInt() != 4) {
throw IllegalStateException("Int required but length was $fieldLength")
}
return input.readInt()
}
private fun getLong(input: LittleEndianDataInputStream): Long {
val fieldLength = input.readShort()
if (fieldLength.toInt() != 8) {
throw IllegalStateException("Long required but length was $fieldLength")
}
return input.readLong()
}
private fun getByteArray(input: LittleEndianDataInputStream): ByteArray {
val value = ByteArray(input.readShort().toInt())
input.readFully(value)
return value
}
@@ -1,8 +1,9 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.credentialStore.kdbx
import com.intellij.util.get
import com.google.common.io.LittleEndianDataOutputStream
import com.intellij.util.getOrCreate
import com.intellij.util.loadElement
import org.jdom.Element
import java.io.OutputStream
import java.nio.ByteBuffer
@@ -10,10 +11,16 @@ import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.*
import java.util.zip.GZIPOutputStream
internal const val ENTRY_ELEMENT_NAME = "Entry"
internal const val GROUP_ELEMENT_NAME = "Group"
internal const val NAME_ELEMENT_NAME = "Name"
internal object KdbxDbElementNames {
const val group = "Group"
const val entry = "Entry"
const val root = "Root"
const val name = "Name"
}
internal val LOCATION_CHANGED = arrayOf("Times", "LocationChanged")
internal val USAGE_COUNT_ELEMENT_NAME = arrayOf("Times", "UsageCount")
@@ -25,10 +32,10 @@ internal val CREATION_TIME_ELEMENT_NAME = arrayOf("Times", "CreationTime")
internal val LAST_ACCESS_TIME_ELEMENT_NAME = arrayOf("Times", "LastAccessTime")
internal val EXPIRY_TIME_ELEMENT_NAME = arrayOf("Times", "ExpiryTime")
private const val ROOT_ELEMENT_NAME = "Root"
internal var dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
// we should on each save change protectedStreamKey for security reasons (as KeeWeb also does)
// so, this requirement (is it really required?) can force us to re-encrypt all passwords on save
internal class KeePassDatabase(private val rootElement: Element = createEmptyDatabase()) {
@Volatile
var isDirty: Boolean = false
@@ -37,31 +44,44 @@ internal class KeePassDatabase(private val rootElement: Element = createEmptyDat
val rootGroup: KdbxGroup
init {
val rootElement = rootElement.get(ROOT_ELEMENT_NAME)
val groupElement = rootElement?.get("Group")
val rootElement = rootElement.getOrCreate(KdbxDbElementNames.root)
val groupElement = rootElement.getChild(KdbxDbElementNames.group)
if (groupElement == null) {
rootGroup = createGroup(this, null)
rootGroup.name = ROOT_ELEMENT_NAME
rootGroup.name = KdbxDbElementNames.root
rootElement.addContent(rootGroup.element)
}
else {
rootElement.removeChild("Group")
rootGroup = KdbxGroup(groupElement, this, null)
}
}
@Synchronized
fun save(credentials: KeePassCredentials, outputStream: 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))
kdbxHeader.writeKdbxHeader(outputStream)
val metaElement = rootElement.getOrCreate("Meta")
metaElement.getOrCreate("HeaderHash").text = Base64.getEncoder().encodeToString(kdbxHeader.headerHash)
metaElement.getOrCreate("MemoryProtection").getOrCreate("ProtectPassword").text = "True"
val encryptedOutputStream = kdbxHeader.createEncryptedStream(credentials.key, outputStream)
LittleEndianDataOutputStream(encryptedOutputStream).write(kdbxHeader.streamStartBytes)
var kdbxOutput: OutputStream = HashedBlockOutputStream(encryptedOutputStream)
if (kdbxHeader.compressionFlags == KdbxHeader.CompressionFlags.GZIP) {
kdbxOutput = GZIPOutputStream(kdbxOutput, 8 * 1024)
}
kdbxOutput.writer().use {
ProtectedXmlWriter(createSalsa20StreamCipher(kdbxHeader.protectedStreamKey)).printElement(it, rootElement, 0)
}
isDirty = false
}
fun createEntry(title: String): KdbxEntry {
val element = Element(ENTRY_ELEMENT_NAME)
val element = Element(KdbxDbElementNames.entry)
ensureElements(element, mandatoryEntryElements)
val result = KdbxEntry(element, this, null)
@@ -118,14 +138,53 @@ internal class DateValueCreator : ValueCreator {
internal class UuidValueCreator : ValueCreator {
override val value: String
get() = base64RandomUuid()
get() = base64FromUuid(UUID.randomUUID())
}
internal fun base64RandomUuid() = base64FromUuid(UUID.randomUUID())
private fun base64FromUuid(uuid: UUID): String {
val b = ByteBuffer.wrap(ByteArray(16))
b.putLong(uuid.mostSignificantBits)
b.putLong(uuid.leastSignificantBits)
return Base64.getEncoder().encodeToString(b.array())
}
private fun createEmptyDatabase(): Element {
val creationDate = formattedNow()
@Suppress("SpellCheckingInspection")
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>
</KeePassFile>""")
}
@@ -0,0 +1,107 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.credentialStore.kdbx
import com.intellij.configurationStore.JbXmlOutputter
import com.intellij.credentialStore.OneTimeString
import com.intellij.credentialStore.SecureString
import com.intellij.credentialStore.SecureStringImpl
import org.bouncycastle.crypto.SkippingStreamCipher
import org.jdom.Element
import org.jdom.Text
import java.io.Writer
import java.util.*
internal class ProtectedValue(private var encryptedValue: ByteArray, private var position: Int, private var streamCipher: SkippingStreamCipher) : Text(), SecureString {
@Synchronized
override fun get(clearable: Boolean): OneTimeString {
val output = ByteArray(encryptedValue.size)
decryptInto(output)
return OneTimeString(output, clearable = clearable)
}
@Synchronized
fun setNewStreamCipher(newStreamCipher: SkippingStreamCipher) {
val value = encryptedValue
decryptInto(value)
position = newStreamCipher.position.toInt()
newStreamCipher.processBytes(value, 0, value.size, value, 0)
streamCipher = newStreamCipher
}
@Synchronized
private fun decryptInto(out: ByteArray) {
streamCipher.seekTo(position.toLong())
streamCipher.processBytes(encryptedValue, 0, encryptedValue.size, out, 0)
}
override fun getText() = throw IllegalStateException("encodeToBase64 must be used for serialization")
fun encodeToBase64(): String {
return when {
encryptedValue.isEmpty() -> ""
else -> Base64.getEncoder().encodeToString(encryptedValue)
}
}
}
internal class UnsavedProtectedValue(val secureString: SecureStringImpl) : Text(), SecureString by secureString {
override fun getText() = throw IllegalStateException("Must be converted to ProtectedValue for serialization")
}
internal class ProtectedXmlWriter(private val streamCipher: SkippingStreamCipher) : JbXmlOutputter("\n", null, null, null) {
override fun writeContent(out: Writer, element: Element, level: Int): Boolean {
if (element.name == KdbxEntryElementNames.value) {
val value = element.content.firstOrNull()
if (value is SecureString) {
val protectedValue: ProtectedValue
if (value is ProtectedValue) {
value.setNewStreamCipher(streamCipher)
protectedValue = value
}
else {
val bytes = (value as UnsavedProtectedValue).secureString.getAsByteArray()
val position = streamCipher.position.toInt()
streamCipher.processBytes(bytes, 0, bytes.size, bytes, 0)
protectedValue = ProtectedValue(bytes, position, streamCipher)
element.setContent(protectedValue)
}
out.write('>'.toInt())
out.write(escapeElementEntities(protectedValue.encodeToBase64()))
return true
}
}
return super.writeContent(out, element, level)
}
}
internal fun isValueProtected(valueElement: Element) = valueElement.getAttributeValue(KdbxAttributeNames.protected).equals("true", ignoreCase = true)
internal class XmlProtectedValueTransformer(private val streamCipher: SkippingStreamCipher) {
private var position = 0
fun processEntries(parentElement: Element) {
// we must process in exact order
for (element in parentElement.content) {
if (element !is Element) {
continue
}
if (element.name == KdbxDbElementNames.group) {
processEntries(element)
}
else if (element.name == KdbxDbElementNames.entry) {
for (container in element.getChildren(KdbxEntryElementNames.string)) {
val valueElement = container.getChild(KdbxEntryElementNames.value) ?: continue
if (isValueProtected(valueElement)) {
val value = Base64.getDecoder().decode(valueElement.text)
valueElement.setContent(ProtectedValue(value, position, streamCipher))
position += value.size
}
}
}
}
}
}
+38 -154
View File
@@ -1,181 +1,65 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.credentialStore.kdbx
import com.google.common.io.LittleEndianDataInputStream
import com.intellij.openapi.util.JDOMUtil
import com.intellij.util.SmartList
import com.intellij.util.io.inputStream
import com.intellij.util.loadElement
import com.intellij.util.write
import org.bouncycastle.crypto.SkippingStreamCipher
import org.bouncycastle.crypto.engines.Salsa20Engine
import org.bouncycastle.crypto.params.KeyParameter
import org.bouncycastle.crypto.params.ParametersWithIV
import org.jdom.Element
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 java.util.zip.GZIPInputStream
// https://gist.github.com/lgg/e6ccc6e212d18dd2ecd8a8c116fb1e45
@Throws(IncorrectMasterPasswordException::class)
internal fun loadKdbx(file: Path, credentials: KeePassCredentials): KeePassDatabase {
return file.inputStream().use { inputStream ->
val kdbxHeader = KdbxHeader()
val element = KdbxSerializer.createUnencryptedInputStream(credentials, kdbxHeader, inputStream).use {
load(it, Salsa20Encryption(kdbxHeader.protectedStreamKey))
}
KeePassDatabase(element)
}
return file.inputStream().buffered().use { readKeePassDatabase(credentials, it) }
}
class KdbxPassword(password: ByteArray) : KeePassCredentials {
private fun readKeePassDatabase(credentials: KeePassCredentials, inputStream: InputStream): KeePassDatabase {
val kdbxHeader = KdbxHeader()
kdbxHeader.readKdbxHeader(inputStream)
val decryptedInputStream = kdbxHeader.createDecryptedStream(credentials.key, inputStream)
val startBytes = ByteArray(32)
LittleEndianDataInputStream(decryptedInputStream).readFully(startBytes)
if (!Arrays.equals(startBytes, kdbxHeader.streamStartBytes)) {
throw IncorrectMasterPasswordException()
}
var resultInputStream: InputStream = HashedBlockInputStream(decryptedInputStream)
if (kdbxHeader.compressionFlags == KdbxHeader.CompressionFlags.GZIP) {
resultInputStream = GZIPInputStream(resultInputStream)
}
val element = JDOMUtil.load(resultInputStream)
element.getChild(KdbxDbElementNames.root)?.let { rootElement ->
XmlProtectedValueTransformer(createSalsa20StreamCipher(kdbxHeader.protectedStreamKey)).processEntries(rootElement)
}
return KeePassDatabase(element)
}
internal class KdbxPassword(password: ByteArray) : KeePassCredentials {
override val key: ByteArray
init {
val md = MessageDigest.getInstance("SHA-256")
val md = sha256MessageDigest()
key = md.digest(md.digest(password))
}
}
interface KeePassCredentials {
val key: ByteArray
}
internal fun sha256MessageDigest() = MessageDigest.getInstance("SHA-256")
internal fun save(rootElement: Element, outputStream: OutputStream, encryption: KdbxEncryption) {
val meta = rootElement.getChild("Meta")?.getChild("MemoryProtection")
if (meta != null) {
val propertiesToProtect = SmartList<String>()
for (element in meta.children) {
val propertyName = element.name.removePrefix("Protect")
if (propertyName != element.name && element.text.equals("true", ignoreCase = true)) {
propertiesToProtect.add(propertyName)
}
}
// 0xE830094B97205D2A
private val SALSA20_IV = byteArrayOf(-24, 48, 9, 75, -105, 32, 93, 42)
rootElement.getChild("Root")?.getChild("Group")?.let { rootGroupElement ->
processEntries(rootGroupElement) { container, valueElement ->
val key = container.getChildText("Key") ?: return@processEntries
for (propertyName in propertiesToProtect) {
if (key == propertyName) {
valueElement.setAttribute("Protected", "True")
valueElement.text = Base64.getEncoder().encodeToString(encryption.encrypt(valueElement.text.toByteArray()))
}
}
}
}
}
rootElement.write(outputStream)
}
private fun load(inputStream: InputStream, encryption: KdbxEncryption): Element {
val rootElement = JDOMUtil.load(inputStream)
rootElement.getChild("Root")?.getChild("Group")?.let { rootGroupElement ->
processEntries(rootGroupElement) { _, valueElement ->
if (valueElement.getAttributeValue("Protected", "false").equals("true", ignoreCase = true)) {
valueElement.text = encryption.decrypt(Base64.getDecoder().decode(valueElement.text)).toString(Charsets.UTF_8)
valueElement.removeAttribute("Protected")
}
}
}
return rootElement
}
private fun processEntries(groupElement: Element, processor: (container: Element, valueElement: Element) -> Unit) {
// we must process in exact order
for (element in groupElement.children) {
if (element.name == GROUP_ELEMENT_NAME) {
processEntries(element, processor)
}
else if (element.name == ENTRY_ELEMENT_NAME) {
for (container in element.getChildren("String")) {
val valueElement = container.getChild("Value") ?: continue
processor(container, valueElement)
}
}
}
}
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>
</KeePassFile>""")
}
internal interface KdbxEncryption {
val key: ByteArray
fun decrypt(encryptedText: ByteArray): ByteArray
fun encrypt(decryptedText: ByteArray): ByteArray
}
private val SALSA20_IV = byteArrayOf(-24, 48, 9, 75, -105, 32, 93, 42) // 0xE830094B97205D2A
/**
* 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.
*/
internal class Salsa20Encryption(override val key: ByteArray) : KdbxEncryption {
private val salsa20 = Salsa20Engine()
init {
val keyParameter = KeyParameter(sha256MessageDigest().digest(key))
salsa20.init(true, ParametersWithIV(keyParameter, SALSA20_IV))
}
override fun decrypt(encryptedText: ByteArray): ByteArray {
val output = ByteArray(encryptedText.size)
salsa20.processBytes(encryptedText, 0, encryptedText.size, output, 0)
return output
}
override fun encrypt(decryptedText: ByteArray): ByteArray {
val output = ByteArray(decryptedText.size)
salsa20.processBytes(decryptedText, 0, decryptedText.size, output, 0)
return output
}
}
internal fun parseTime(value: String): Long {
return try {
ZonedDateTime.parse(value).toEpochSecond()
}
catch (e: DateTimeParseException) {
0
}
internal fun createSalsa20StreamCipher(key: ByteArray): SkippingStreamCipher {
val streamCipher = Salsa20Engine()
val keyParameter = KeyParameter(sha256MessageDigest().digest(key))
streamCipher.init(true /* doesn't matter, Salsa20 encryption and decryption is completely symmetrical */, ParametersWithIV(keyParameter, SALSA20_IV))
return streamCipher
}
@@ -0,0 +1,25 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.credentialStore.kdbx
internal object KdbxAttributeNames {
const val protected = "Protected"
}
internal object KdbxEntryElementNames {
const val title = "Title"
const val userName = "UserName"
const val password = "Password"
const val value = "Value"
const val key = "Key"
const val string = "String"
}
internal class IncorrectMasterPasswordException(val isFileMissed: Boolean = false) : RuntimeException()
internal interface KeePassCredentials {
val key: ByteArray
}
internal class KdbxException(message: String) : RuntimeException(message)
@@ -145,10 +145,10 @@ class KeePassCredentialStoreTest {
assertThat(pdbFile).doesNotExist()
assertThat(pdbPwdFile).doesNotExist()
}
private fun randomString() = UUID.randomUUID().toString()
}
private fun randomString() = UUID.randomUUID().toString()
// avoid this constructor in production sources to avoid m
@Suppress("TestFunctionName")
internal fun createStore(baseDir: Path): KeePassCredentialStore {
@@ -9,12 +9,12 @@ import com.intellij.credentialStore.kdbx.KeePassDatabase
import com.intellij.credentialStore.kdbx.loadKdbx
import com.intellij.testFramework.assertions.Assertions.assertThat
import com.intellij.testFramework.rules.InMemoryFsRule
import com.intellij.util.Base64
import com.intellij.util.io.*
import org.junit.Rule
import org.junit.Test
import java.awt.Component
import java.nio.file.Path
import java.util.*
private val testCredentialAttributes = CredentialAttributes("foo", "u")
@@ -138,7 +138,7 @@ internal class KeePassFileManagerTest {
// assert that other store not corrupted
fsRule.fs.getPath("/other/otherKey").move(fsRule.fs.getPath("/other/${MASTER_KEY_FILE_NAME}"))
otherStore.reload()
assertThat(otherStore.get(testCredentialAttributes)!!.getPasswordAsString()).isEqualTo("p")
assertThat(otherStore.get(testCredentialAttributes)!!.password!!.toString()).isEqualTo("p")
}
@Test
@@ -158,7 +158,19 @@ internal class KeePassFileManagerTest {
fun reuseExisting() {
val dbFile = fsRule.fs.getPath("/existingDb.kdbx")
@Suppress("SpellCheckingInspection")
dbFile.write(Base64.decode("A9mimmf7S7UBAAMAAhAAMcHy5r9xQ1C+WAUhavxa/wMEAAEAAAAEIAAWuDmigh57N8BZgj1w0GctkaqFTO7nPOroO5AnmUliFgUgAHCRnuiXHvLqSv2oJPMHI5QS9Ony+oIOww4kHcpz2wmEBggAcBcAAAAAAAAHEAB2bInsMKh/zNzzBaegBa/kCCAAw3NSQcu+oGXwJvY9Ht6NSCC19uGFN+sEP9n9E3tNCvoJIADD/kYwUqnCSzrzYXu3tcJcvBKzTinwcSZQ769wYZ/oPwoEAAIAAAAABAAA0K0KdKTDfpMiRxKSe1xkvCDuBDdlDe/hiU3YnkhhCZk48sB9OV4uZI1LPJAgQ8HogjxMjAhnNThYdjlFQ9TTfsb3wUXH/7IK8n69a8IoWuRp9fnchQFnnArXcZeimeUREb/3jcwTXrIHGg+AN7MPQCD/b47us2h07oaAVvGFLCaLU111mJNZrdjNvO9BKODUbqfFFVhJhMAazjsdVpRfV3dbm0apdgluJQ8eUFzrF4YZUUkenG869o/yngD2cm1BxmG2QZe2fel/PSWRRFZ0i6zJm3h9CnYStnDL2N6lm9MPqOm3pKUk0uOG8p9P4U4UHMOnwIFw4RReO5wnqpMN4DFUCfM1qqx0fuqc/sy3DEgpRg3ENsHU7AbidobJzIqGOeK9ywU6Rp5peJrLjbIszedVWVPvxwt/xgAHcaqfntV86XcG8MlJElLSsK4fh9gspySNeyP43wnLjAdFGHq5OtAkhTTwXzHSZTxhazTjClssAHYAyTEQrlYRFl4+4apzdq5g3crGl1vR0Ekj22ytIsQXu9HYVTZ6pk6ESQAIg1qieDZQl4B1eEaRNnyvkfhgPIUDLuG4ULMGm/L1dur2nhHlooqgR8gHo5MNclghOKZdOhJNOvsd7/1XDsziizOqbocjJtBCEOWSgI1Ht1fbmG1nO9C6MrzkkxK+xQaXZDG2fmitUw2flN3OMky/RJgMD4LVdZLoaHj3uq86USXjDc0ql7lHeRRmOhIJ3X8DZObJmyhlylBGYwmYB/PmOuoe28iM8/wQ4Xr8bSYco5kxOK8Bii5TotDXqhVlTdajCo5NN33nqeSc/f+5BGL/CmdxannLel1bnEJrY2ESYXrjzUSM0pgnJlLNxZYynw/XMtSPbXI8/m2ciAP9iXv5efJMOv8O6dmzlbiYz7efSEpIDiwHG7HAIVnVzmy26jhyHXchaQpouPnwj/QhzTL1lRv0qA2K5+wNIexkKQa4G4iZhSHVzZTHY12rpnLDTxWv2GXHCvwdY5AD6jTJazimlTpbVOf+UvDIPiY2ksmhnaZU9Lc/ItkInhtZwW0e0XtdcgunaVr5BvHTRRancrtRN23VAUlNzQ7Uror55JG3PxUThOX8XRmqJxMnIlgpNbv/tgqbks7zuCESLFjz0EM19QygatS+uHCWnsUp8sWl14bcrwGRoFsdPj/AFRGAv2xYwfMT8VBOZx1KpQ0vqxOx8t67pxpBUaH/Cqlh8Jje7vxXT7wrXQK3bVjpk2uYncAkd7ruk0y7X/Jzvyu9cfoEDn6EMOS0b5aE6VoywNHhbo3dGyZq3K25jOGvgLzRSj9ETBNL8DTccaNzkcyXBsj/gqUZ1rYxVZHPaJWi2Cgy07b+jV5FSRSMYkjNG90ADHKnqyuEG1Y7+pvIX1hOBGIVJ0HA9Ij6xYTZXkGRu6V+WGYEPnDJ4Pi+EYwD400nxtrxwpGiEHWYzyjACHB2BKS9J3BDh4S/"))
dbFile.write(Base64.getDecoder().decode(
"A9mimmf7S7UBAAMAAhAAMcHy5r9xQ1C+WAUhavxa/wMEAAEAAAAEIAAWuDmigh57N8BZgj1w0GctkaqFTO7nPOroO5AnmUliFgUgAHCRnuiXHvLqSv2oJPMHI5QS9Ony+oIOww4kHcpz2wmEBgg" +
"AcBcAAAAAAAAHEAB2bInsMKh/zNzzBaegBa/kCCAAw3NSQcu+oGXwJvY9Ht6NSCC19uGFN+sEP9n9E3tNCvoJIADD/kYwUqnCSzrzYXu3tcJcvBKzTinwcSZQ769wYZ/oPwoEAAIAAAAABAAA0K0" +
"KdKTDfpMiRxKSe1xkvCDuBDdlDe/hiU3YnkhhCZk48sB9OV4uZI1LPJAgQ8HogjxMjAhnNThYdjlFQ9TTfsb3wUXH/7IK8n69a8IoWuRp9fnchQFnnArXcZeimeUREb/3jcwTXrIHGg+AN7MPQCD/b" +
"47us2h07oaAVvGFLCaLU111mJNZrdjNvO9BKODUbqfFFVhJhMAazjsdVpRfV3dbm0apdgluJQ8eUFzrF4YZUUkenG869o/yngD2cm1BxmG2QZe2fel/PSWRRFZ0i6zJm3h9CnYStnDL2N6lm9MPqOm3p" +
"KUk0uOG8p9P4U4UHMOnwIFw4RReO5wnqpMN4DFUCfM1qqx0fuqc/sy3DEgpRg3ENsHU7AbidobJzIqGOeK9ywU6Rp5peJrLjbIszedVWVPvxwt/xgAHcaqfntV86XcG8MlJElLSsK4fh9gspySNeyP43" +
"wnLjAdFGHq5OtAkhTTwXzHSZTxhazTjClssAHYAyTEQrlYRFl4+4apzdq5g3crGl1vR0Ekj22ytIsQXu9HYVTZ6pk6ESQAIg1qieDZQl4B1eEaRNnyvkfhgPIUDLuG4ULMGm/L1dur2nhHlooqgR8gHo5" +
"MNclghOKZdOhJNOvsd7/1XDsziizOqbocjJtBCEOWSgI1Ht1fbmG1nO9C6MrzkkxK+xQaXZDG2fmitUw2flN3OMky/RJgMD4LVdZLoaHj3uq86USXjDc0ql7lHeRRmOhIJ3X8DZObJmyhlylBGYwmYB/Pm" +
"Ouoe28iM8/wQ4Xr8bSYco5kxOK8Bii5TotDXqhVlTdajCo5NN33nqeSc/f+5BGL/CmdxannLel1bnEJrY2ESYXrjzUSM0pgnJlLNxZYynw/XMtSPbXI8/m2ciAP9iXv5efJMOv8O6dmzlbiYz7efSEpIDi" +
"wHG7HAIVnVzmy26jhyHXchaQpouPnwj/QhzTL1lRv0qA2K5+wNIexkKQa4G4iZhSHVzZTHY12rpnLDTxWv2GXHCvwdY5AD6jTJazimlTpbVOf+UvDIPiY2ksmhnaZU9Lc/ItkInhtZwW0e0XtdcgunaVr5Bv" +
"HTRRancrtRN23VAUlNzQ7Uror55JG3PxUThOX8XRmqJxMnIlgpNbv/tgqbks7zuCESLFjz0EM19QygatS+uHCWnsUp8sWl14bcrwGRoFsdPj/AFRGAv2xYwfMT8VBOZx1KpQ0vqxOx8t67pxpBUaH/Cqlh8J" +
"je7vxXT7wrXQK3bVjpk2uYncAkd7ruk0y7X/Jzvyu9cfoEDn6EMOS0b5aE6VoywNHhbo3dGyZq3K25jOGvgLzRSj9ETBNL8DTccaNzkcyXBsj/gqUZ1rYxVZHPaJWi2Cgy07b+jV5FSRSMYkjNG90ADHKnqyuEG1" +
"Y7+pvIX1hOBGIVJ0HA9Ij6xYTZXkGRu6V+WGYEPnDJ4Pi+EYwD400nxtrxwpGiEHWYzyjACHB2BKS9J3BDh4S/"))
fun checkEntry(db: KeePassDatabase) {
assertThat(db.rootGroup.getEntry("foo", "foo")!!.password!!.get().toString()).isEqualTo("bar")