mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IDEA-200066 efficient implementation of load and save KeePass database
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user