diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0613db3eb..79fa6bea8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ android:allowBackup="true" android:fullBackupContent="@xml/backup" android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent" + android:largeHeap="true" android:theme="@style/KeepassDXStyle.Night"> - coordinatorLayout?.let { coordinatorLayout -> + coordinatorLayout?.let { coordinatorLayout -> + result.exception?.errorId?.let { errorId -> Snackbar.make(coordinatorLayout, errorId, Snackbar.LENGTH_LONG).asError().show() + } ?: result.message?.let { message -> + Snackbar.make(coordinatorLayout, message, Snackbar.LENGTH_LONG).asError().show() } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt index 95dde2367..2593baa95 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt @@ -58,7 +58,7 @@ import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager import com.kunzisoft.keepass.database.action.ProgressDialogThread import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.exception.LoadDatabaseDuplicateUuidException +import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.education.PasswordActivityEducation import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY @@ -189,7 +189,7 @@ class PasswordActivity : StylishActivity() { resultError = resultException.getLocalizedMessage(resources) // Relaunch loading if we need to fix UUID - if (resultException is LoadDatabaseDuplicateUuidException) { + if (resultException is DuplicateUuidDatabaseException) { showLoadDatabaseDuplicateUuidMessage { var databaseUri: Uri? = null diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt index 26e2d3601..7cbb194e5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt @@ -58,7 +58,7 @@ open class AssignPasswordInDatabaseRunnable ( database.retrieveMasterKey(mMasterPassword, uriInputStream) } catch (e: Exception) { erase(mBackupKey) - setError(e.message) + setError(e) } super.onStartRun() diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt index 45eb28f5e..2c80fdcdf 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt @@ -46,7 +46,7 @@ class CreateDatabaseRunnable(context: Context, } } catch (e: Exception) { mDatabase.closeAndClear() - setError(e.message) + setError(e) } super.onStartRun() diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt index 8404fd650..993c50437 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt @@ -25,7 +25,7 @@ import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.exception.LoadDatabaseDuplicateUuidException +import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService import com.kunzisoft.keepass.settings.PreferencesUtil @@ -62,7 +62,7 @@ class LoadDatabaseRunnable(private val context: Context, mFixDuplicateUUID, progressTaskUpdater) } - catch (e: LoadDatabaseDuplicateUuidException) { + catch (e: DuplicateUuidDatabaseException) { mDuplicateUuidAction?.invoke(result) setError(e) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/SaveDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/SaveDatabaseRunnable.kt index 092744fb8..4c8e2f689 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/SaveDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/SaveDatabaseRunnable.kt @@ -21,9 +21,8 @@ package com.kunzisoft.keepass.database.action import android.content.Context import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.exception.DatabaseOutputException +import com.kunzisoft.keepass.database.exception.DatabaseException import com.kunzisoft.keepass.tasks.ActionRunnable -import java.io.IOException open class SaveDatabaseRunnable(protected var context: Context, protected var database: Database, @@ -38,10 +37,8 @@ open class SaveDatabaseRunnable(protected var context: Context, if (saveDatabase && result.isSuccess) { try { database.saveData(context.contentResolver) - } catch (e: IOException) { - setError(e.message) - } catch (e: DatabaseOutputException) { - setError(e.message) + } catch (e: DatabaseException) { + setError(e) } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/UpdateCompressionBinariesDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/UpdateCompressionBinariesDatabaseRunnable.kt new file mode 100644 index 000000000..9e58d2b19 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/UpdateCompressionBinariesDatabaseRunnable.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.database.action + +import android.content.Context +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.PwCompressionAlgorithm + +class UpdateCompressionBinariesDatabaseRunnable ( + context: Context, + database: Database, + private val oldCompressionAlgorithm: PwCompressionAlgorithm, + private val newCompressionAlgorithm: PwCompressionAlgorithm, + saveDatabase: Boolean) + : SaveDatabaseRunnable(context, database, saveDatabase) { + + override fun onStartRun() { + // Set new compression + if (database.allowDataCompression) { + try { + database.apply { + updateDataBinaryCompression(oldCompressionAlgorithm, newCompressionAlgorithm) + compressionAlgorithm = newCompressionAlgorithm + } + } catch (e: Exception) { + setError(e) + } + } + + super.onStartRun() + } + + override fun onFinishRun() { + super.onFinishRun() + + if (database.allowDataCompression) { + if (!result.isSuccess) { + try { + database.apply { + compressionAlgorithm = oldCompressionAlgorithm + updateDataBinaryCompression(newCompressionAlgorithm, oldCompressionAlgorithm) + } + } catch (e: Exception) { + setError(e) + } + } + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyNodesRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyNodesRunnable.kt index e76479992..4188548af 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyNodesRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyNodesRunnable.kt @@ -22,8 +22,8 @@ package com.kunzisoft.keepass.database.action.node import android.content.Context import android.util.Log import com.kunzisoft.keepass.database.element.* -import com.kunzisoft.keepass.database.exception.CopyDatabaseEntryException -import com.kunzisoft.keepass.database.exception.CopyDatabaseGroupException +import com.kunzisoft.keepass.database.exception.CopyEntryDatabaseException +import com.kunzisoft.keepass.database.exception.CopyGroupDatabaseException class CopyNodesRunnable constructor( context: Context, @@ -42,7 +42,7 @@ class CopyNodesRunnable constructor( when (currentNode.type) { Type.GROUP -> { Log.e(TAG, "Copy not allowed for group")// Only finish thread - setError(CopyDatabaseGroupException()) + setError(CopyGroupDatabaseException()) break@foreachNode } Type.ENTRY -> { @@ -57,12 +57,12 @@ class CopyNodesRunnable constructor( mEntriesCopied.add(entryCopied) } else { Log.e(TAG, "Unable to create a copy of the entry") - setError(CopyDatabaseEntryException()) + setError(CopyEntryDatabaseException()) break@foreachNode } } else { // Only finish thread - setError(CopyDatabaseEntryException()) + setError(CopyEntryDatabaseException()) break@foreachNode } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt index ae8335f6b..bb80cbf46 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt @@ -22,9 +22,8 @@ package com.kunzisoft.keepass.database.action.node import android.content.Context import android.util.Log import com.kunzisoft.keepass.database.element.* -import com.kunzisoft.keepass.database.exception.LoadDatabaseException -import com.kunzisoft.keepass.database.exception.MoveDatabaseEntryException -import com.kunzisoft.keepass.database.exception.MoveDatabaseGroupException +import com.kunzisoft.keepass.database.exception.EntryDatabaseException +import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException class MoveNodesRunnable constructor( context: Context, @@ -53,7 +52,7 @@ class MoveNodesRunnable constructor( database.moveGroupTo(groupToMove, mNewParent) } else { // Only finish thread - setError(MoveDatabaseGroupException()) + setError(MoveGroupDatabaseException()) break@foreachNode } } @@ -67,7 +66,7 @@ class MoveNodesRunnable constructor( database.moveEntryTo(entryToMove, mNewParent) } else { // Only finish thread - setError(MoveDatabaseEntryException()) + setError(EntryDatabaseException()) break@foreachNode } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/BinaryPool.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/BinaryPool.kt index 66fb7620f..8622bcbc0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/BinaryPool.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/BinaryPool.kt @@ -20,25 +20,27 @@ package com.kunzisoft.keepass.database.element import android.util.SparseArray -import com.kunzisoft.keepass.database.element.security.ProtectedBinary +import com.kunzisoft.keepass.database.element.security.BinaryAttachment +import java.io.IOException class BinaryPool { - private val pool = SparseArray() + private val pool = SparseArray() - operator fun get(key: Int): ProtectedBinary? { + operator fun get(key: Int): BinaryAttachment? { return pool[key] } - fun put(key: Int, value: ProtectedBinary) { + fun put(key: Int, value: BinaryAttachment) { pool.put(key, value) } - fun doForEachBinary(action: (key: Int, binary: ProtectedBinary) -> Unit) { + fun doForEachBinary(action: (key: Int, binary: BinaryAttachment) -> Unit) { for (i in 0 until pool.size()) { action.invoke(i, pool.get(pool.keyAt(i))) } } + @Throws(IOException::class) fun clear() { doForEachBinary { _, binary -> binary.clear() @@ -46,9 +48,9 @@ class BinaryPool { pool.clear() } - fun add(protectedBinary: ProtectedBinary) { - if (findKey(protectedBinary) == null) { - pool.put(findUnusedKey(), protectedBinary) + fun add(fileBinary: BinaryAttachment) { + if (findKey(fileBinary) == null) { + pool.put(findUnusedKey(), fileBinary) } } @@ -59,7 +61,7 @@ class BinaryPool { return unusedKey } - fun findKey(pb: ProtectedBinary): Int? { + fun findKey(pb: BinaryAttachment): Int? { for (i in 0 until pool.size()) { if (pool.get(pool.keyAt(i)) == pb) return i } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt index 1f7131925..e371976f4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt @@ -139,6 +139,11 @@ class Database { } } + fun updateDataBinaryCompression(oldCompression: PwCompressionAlgorithm, + newCompression: PwCompressionAlgorithm) { + pwDatabaseV4?.changeBinaryCompression(oldCompression, newCompression) + } + val allowNoMasterKey: Boolean get() = pwDatabaseV4 != null @@ -317,7 +322,7 @@ class Database { inputStream = UriUtil.getUriInputStream(contentResolver, uri) } catch (e: Exception) { Log.e("KPD", "Database::loadData", e) - throw LoadDatabaseFileNotFoundException() + throw FileNotFoundDatabaseException() } // Pass KeyFile Uri as InputStreams @@ -327,7 +332,7 @@ class Database { keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile) } catch (e: Exception) { Log.e("KPD", "Database::loadData", e) - throw LoadDatabaseFileNotFoundException() + throw FileNotFoundDatabaseException() } } @@ -366,7 +371,7 @@ class Database { progressTaskUpdater)) // Header not recognized - else -> throw LoadDatabaseSignatureException() + else -> throw SignatureDatabaseException() } this.mSearchHelper = SearchDbHelper(omitBackup) @@ -432,16 +437,20 @@ class Database { return entry } - @Throws(IOException::class, DatabaseOutputException::class) + @Throws(DatabaseOutputException::class) fun saveData(contentResolver: ContentResolver) { - this.fileUri?.let { - saveData(contentResolver, it) + try { + this.fileUri?.let { + saveData(contentResolver, it) + } + } catch (e: Exception) { + Log.e(TAG, "Unable to save database", e) + throw DatabaseOutputException(e) } } @Throws(IOException::class, DatabaseOutputException::class) private fun saveData(contentResolver: ContentResolver, uri: Uri) { - val errorMessage = "Failed to store database." if (uri.scheme == "file") { uri.path?.let { filename -> @@ -454,8 +463,7 @@ class Database { ?: pwDatabaseV4?.let { PwDbV4Output(it, fileOutputStream) } pmo?.output() } catch (e: Exception) { - Log.e(TAG, errorMessage, e) - throw IOException(errorMessage, e) + throw IOException(e) } finally { fileOutputStream?.close() } @@ -468,7 +476,7 @@ class Database { } if (!tempFile.renameTo(File(filename))) { - throw IOException(errorMessage) + throw IOException() } } } else { @@ -480,8 +488,7 @@ class Database { ?: pwDatabaseV4?.let { PwDbV4Output(it, outputStream) } pmo?.output() } catch (e: Exception) { - Log.e(TAG, errorMessage, e) - throw IOException(errorMessage, e) + throw IOException(e) } finally { outputStream?.close() } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwCompressionAlgorithm.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwCompressionAlgorithm.kt index c4bd02208..735bfb018 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwCompressionAlgorithm.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwCompressionAlgorithm.kt @@ -20,17 +20,30 @@ package com.kunzisoft.keepass.database.element import android.content.res.Resources +import android.os.Parcel +import android.os.Parcelable import com.kunzisoft.keepass.R import com.kunzisoft.keepass.utils.ObjectNameResource +import com.kunzisoft.keepass.utils.readEnum +import com.kunzisoft.keepass.utils.writeEnum + // Note: We can get away with using int's to store unsigned 32-bit ints // since we won't do arithmetic on these values (also unlikely to // reach negative ids). -enum class PwCompressionAlgorithm : ObjectNameResource { +enum class PwCompressionAlgorithm : ObjectNameResource, Parcelable { None, GZip; + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeEnum(this) + } + + override fun describeContents(): Int { + return 0 + } + override fun getName(resources: Resources): String { return when (this) { None -> resources.getString(R.string.compression_none) @@ -38,4 +51,14 @@ enum class PwCompressionAlgorithm : ObjectNameResource { } } + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): PwCompressionAlgorithm { + return parcel.readEnum() ?: None + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabase.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabase.kt index a25b304df..544c714b7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabase.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabase.kt @@ -20,8 +20,8 @@ package com.kunzisoft.keepass.database.element import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine -import com.kunzisoft.keepass.database.exception.LoadDatabaseDuplicateUuidException -import com.kunzisoft.keepass.database.exception.LoadDatabaseKeyFileEmptyException +import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException +import com.kunzisoft.keepass.database.exception.KeyFileEmptyDatabaseException import org.apache.commons.io.IOUtils import java.io.* import java.security.MessageDigest @@ -134,7 +134,7 @@ abstract class PwDatabase< } when (keyData.size.toLong()) { - 0L -> throw LoadDatabaseKeyFileEmptyException() + 0L -> throw KeyFileEmptyDatabaseException() 32L -> return keyData 64L -> try { return hexStringToByteArray(String(keyData)) @@ -247,7 +247,7 @@ abstract class PwDatabase< group.parent?.addChildGroup(group) this.groupIndexes[newGroupId] = group } else { - throw LoadDatabaseDuplicateUuidException(Type.GROUP, groupId) + throw DuplicateUuidDatabaseException(Type.GROUP, groupId) } } else { this.groupIndexes[groupId] = group @@ -296,7 +296,7 @@ abstract class PwDatabase< entry.parent?.addChildEntry(entry) this.entryIndexes[newEntryId] = entry } else { - throw LoadDatabaseDuplicateUuidException(Type.ENTRY, entryId) + throw DuplicateUuidDatabaseException(Type.ENTRY, entryId) } } else { this.entryIndexes[entryId] = entry diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV4.kt index 361297145..99f48f596 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV4.kt @@ -31,6 +31,8 @@ import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters import com.kunzisoft.keepass.database.element.PwDatabaseV3.Companion.BACKUP_FOLDER_TITLE import com.kunzisoft.keepass.database.exception.UnknownKDF +import com.kunzisoft.keepass.database.file.PwDbHeaderV4.Companion.FILE_VERSION_32_3 +import com.kunzisoft.keepass.database.file.PwDbHeaderV4.Companion.FILE_VERSION_32_4 import com.kunzisoft.keepass.utils.VariantDictionary import org.w3c.dom.Node import org.w3c.dom.Text @@ -56,6 +58,7 @@ class PwDatabaseV4 : PwDatabase { private var numKeyEncRounds: Long = 0 var publicCustomData = VariantDictionary() + var kdbxVersion: Long = 0 var name = "" var nameChanged = PwDate() // TODO change setting date @@ -91,7 +94,7 @@ class PwDatabaseV4 : PwDatabase { val customIcons = ArrayList() val customData = HashMap() - var binPool = BinaryPool() + var binaryPool = BinaryPool() var localizedAppName = "KeePassDX" @@ -116,7 +119,14 @@ class PwDatabaseV4 : PwDatabase { } override val version: String - get() = "KeePass 2" + get() { + val kdbxStringVersion = when(kdbxVersion) { + FILE_VERSION_32_3 -> "3.1" + FILE_VERSION_32_4 -> "4.0" + else -> "UNKNOWN" + } + return "KeePass 2 - KDBX$kdbxStringVersion" + } override val kdfEngine: KdfEngine? get() = try { @@ -151,6 +161,39 @@ class PwDatabaseV4 : PwDatabase { return list } + fun changeBinaryCompression(oldCompression: PwCompressionAlgorithm, + newCompression: PwCompressionAlgorithm) { + binaryPool.doForEachBinary { key, binary -> + + try { + when (oldCompression) { + PwCompressionAlgorithm.None -> { + when (newCompression) { + PwCompressionAlgorithm.None -> { + } + PwCompressionAlgorithm.GZip -> { + // To compress, create a new binary with file + binary.compress() + } + } + } + PwCompressionAlgorithm.GZip -> { + when (newCompression) { + PwCompressionAlgorithm.None -> { + // To decompress, create a new binary with file + binary.decompress() + } + PwCompressionAlgorithm.GZip -> { + } + } + } + } + } catch (e: Exception) { + Log.e(TAG, "Unable to change compression for $key") + } + } + } + override val availableEncryptionAlgorithms: List get() { val list = ArrayList() @@ -491,8 +534,12 @@ class PwDatabaseV4 : PwDatabase { } override fun clearCache() { - super.clearCache() - binPool.clear() + try { + super.clearCache() + binaryPool.clear() + } catch (e: Exception) { + Log.e(TAG, "Unable to clear cache", e) + } } companion object { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt index f4c39930e..5702e1d0b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt @@ -21,7 +21,7 @@ package com.kunzisoft.keepass.database.element import android.os.Parcel import android.os.Parcelable -import com.kunzisoft.keepass.database.element.security.ProtectedBinary +import com.kunzisoft.keepass.database.element.security.BinaryAttachment import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.utils.ParcelableUtil import java.util.* @@ -49,7 +49,7 @@ class PwEntryV4 : PwEntry, PwNodeV4Interface { var iconCustom = PwIconCustom.UNKNOWN_ICON private var customData = HashMap() var fields = HashMap() - val binaries = HashMap() + var binaries = HashMap() var foregroundColor = "" var backgroundColor = "" var overrideURL = "" @@ -98,7 +98,7 @@ class PwEntryV4 : PwEntry, PwNodeV4Interface { locationChanged = parcel.readParcelable(PwDate::class.java.classLoader) ?: locationChanged customData = ParcelableUtil.readStringParcelableMap(parcel) fields = ParcelableUtil.readStringParcelableMap(parcel, ProtectedString::class.java) - // TODO binaries = ParcelableUtil.readStringParcelableMap(parcel, ProtectedBinary.class); + binaries = ParcelableUtil.readStringParcelableMap(parcel, BinaryAttachment::class.java) foregroundColor = parcel.readString() ?: foregroundColor backgroundColor = parcel.readString() ?: backgroundColor overrideURL = parcel.readString() ?: overrideURL @@ -116,7 +116,7 @@ class PwEntryV4 : PwEntry, PwNodeV4Interface { dest.writeParcelable(locationChanged, flags) ParcelableUtil.writeStringParcelableMap(dest, customData) ParcelableUtil.writeStringParcelableMap(dest, flags, fields) - // TODO ParcelableUtil.writeStringParcelableMap(dest, flags, binaries); + ParcelableUtil.writeStringParcelableMap(dest, flags, binaries) dest.writeString(foregroundColor) dest.writeString(backgroundColor) dest.writeString(overrideURL) @@ -274,7 +274,7 @@ class PwEntryV4 : PwEntry, PwNodeV4Interface { fields[label] = value } - fun putProtectedBinary(key: String, value: ProtectedBinary) { + fun putProtectedBinary(key: String, value: BinaryAttachment) { binaries[key] = value } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/security/BinaryAttachment.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/security/BinaryAttachment.kt new file mode 100644 index 000000000..a3b8379c8 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/security/BinaryAttachment.kt @@ -0,0 +1,184 @@ +/* + * Copyright 2018 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.database.element.security + +import android.os.Parcel +import android.os.Parcelable +import com.kunzisoft.keepass.database.element.PwDatabaseV4.Companion.BUFFER_SIZE_BYTES +import com.kunzisoft.keepass.stream.ReadBytes +import com.kunzisoft.keepass.stream.readFromStream +import java.io.* +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream + +class BinaryAttachment : Parcelable { + + var isCompressed: Boolean? = null + private set + var isProtected: Boolean = false + private set + private var dataFile: File? = null + + fun length(): Long { + if (dataFile != null) + return dataFile!!.length() + return 0 + } + + /** + * Empty protected binary + */ + constructor() { + this.isCompressed = null + this.isProtected = false + this.dataFile = null + } + + constructor(dataFile: File, enableProtection: Boolean = false, compressed: Boolean? = null) { + this.isCompressed = compressed + this.isProtected = enableProtection + this.dataFile = dataFile + } + + private constructor(parcel: Parcel) { + val compressedByte = parcel.readByte().toInt() + isCompressed = if (compressedByte == 2) null else compressedByte != 0 + isProtected = parcel.readByte().toInt() != 0 + dataFile = File(parcel.readString()) + } + + @Throws(IOException::class) + fun getInputDataStream(): InputStream { + return when { + dataFile != null -> FileInputStream(dataFile!!) + else -> ByteArrayInputStream(ByteArray(0)) + } + } + + @Throws(IOException::class) + fun compress() { + if (dataFile != null) { + // To compress, create a new binary with file + if (isCompressed != true) { + val fileBinaryCompress = File(dataFile!!.parent, dataFile!!.name + "_temp") + val outputStream = GZIPOutputStream(FileOutputStream(fileBinaryCompress)) + readFromStream(getInputDataStream(), BUFFER_SIZE_BYTES, + object : ReadBytes { + override fun read(buffer: ByteArray) { + outputStream.write(buffer) + } + }) + outputStream.close() + + // Remove unGzip file + if (dataFile!!.delete()) { + if (fileBinaryCompress.renameTo(dataFile)) { + // Harmonize with database compression + isCompressed = true + } + } + } + } + } + + @Throws(IOException::class) + fun decompress() { + if (dataFile != null) { + if (isCompressed != false) { + val fileBinaryDecompress = File(dataFile!!.parent, dataFile!!.name + "_temp") + val outputStream = FileOutputStream(fileBinaryDecompress) + readFromStream(GZIPInputStream(getInputDataStream()), BUFFER_SIZE_BYTES, + object : ReadBytes { + override fun read(buffer: ByteArray) { + outputStream.write(buffer) + } + }) + outputStream.close() + + // Remove gzip file + if (dataFile!!.delete()) { + if (fileBinaryDecompress.renameTo(dataFile)) { + // Harmonize with database compression + isCompressed = false + } + } + } + } + } + + @Throws(IOException::class) + fun clear() { + if (dataFile != null && !dataFile!!.delete()) + throw IOException("Unable to delete temp file " + dataFile!!.absolutePath) + } + + override fun equals(other: Any?): Boolean { + if (this === other) + return true + if (other == null || javaClass != other.javaClass) + return false + if (other !is BinaryAttachment) + return false + + var sameData = false + if (dataFile != null && dataFile == other.dataFile) + sameData = true + + return isCompressed == other.isCompressed + && isProtected == other.isProtected + && sameData + } + + override fun hashCode(): Int { + + var result = 0 + result = 31 * result + if (isCompressed == null) 2 else if (isCompressed!!) 1 else 0 + result = 31 * result + if (isProtected) 1 else 0 + result = 31 * result + dataFile!!.hashCode() + return result + } + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeByte((if (isCompressed == null) 2 else if (isCompressed!!) 1 else 0).toByte()) + dest.writeByte((if (isProtected) 1 else 0).toByte()) + dest.writeString(dataFile?.absolutePath) + } + + companion object { + + private val TAG = BinaryAttachment::class.java.name + + @JvmField + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): BinaryAttachment { + return BinaryAttachment(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } + +} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/security/ProtectedBinary.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/security/ProtectedBinary.kt deleted file mode 100644 index 02ba53f6d..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/security/ProtectedBinary.kt +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright 2018 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.element.security - -import android.os.Parcel -import android.os.Parcelable -import android.util.Log - -import java.io.ByteArrayInputStream -import java.io.File -import java.io.FileInputStream -import java.io.IOException -import java.io.InputStream -import java.util.Arrays - -class ProtectedBinary : Parcelable { - - var isProtected: Boolean = false - private set - private var data: ByteArray? = null - private var dataFile: File? = null - - fun length(): Long { - if (data != null) - return data!!.size.toLong() - if (dataFile != null) - return dataFile!!.length() - return 0 - } - - /** - * Empty protected binary - */ - constructor() { - this.isProtected = false - this.data = null - this.dataFile = null - } - - constructor(protectedBinary: ProtectedBinary) { - this.isProtected = protectedBinary.isProtected - this.data = protectedBinary.data - this.dataFile = protectedBinary.dataFile - } - - constructor(enableProtection: Boolean, data: ByteArray?) { - this.isProtected = enableProtection - this.data = data - this.dataFile = null - } - - constructor(enableProtection: Boolean, dataFile: File) { - this.isProtected = enableProtection - this.data = null - this.dataFile = dataFile - } - - private constructor(parcel: Parcel) { - isProtected = parcel.readByte().toInt() != 0 - data = ByteArray(parcel.readInt()) - parcel.readByteArray(data) - dataFile = File(parcel.readString()) - } - - @Throws(IOException::class) - fun getData(): InputStream? { - return when { - data != null -> ByteArrayInputStream(data) - dataFile != null -> FileInputStream(dataFile!!) - else -> null - } - } - - fun clear() { - data = null - if (dataFile != null && !dataFile!!.delete()) - Log.e(TAG, "Unable to delete temp file " + dataFile!!.absolutePath) - } - - override fun equals(other: Any?): Boolean { - if (this === other) - return true - if (other == null || javaClass != other.javaClass) - return false - if (other !is ProtectedBinary) - return false - - var sameData = false - if (data != null && Arrays.equals(data, other.data)) - sameData = true - else if (dataFile != null && dataFile == other.dataFile) - sameData = true - else if (data == null && other.data == null - && dataFile == null && other.dataFile == null) - sameData = true - - return isProtected == other.isProtected && sameData - } - - override fun hashCode(): Int { - - var result = 0 - result = 31 * result + if (isProtected) 1 else 0 - result = 31 * result + dataFile!!.hashCode() - result = 31 * result + Arrays.hashCode(data) - return result - } - - override fun describeContents(): Int { - return 0 - } - - override fun writeToParcel(dest: Parcel, flags: Int) { - dest.writeByte((if (isProtected) 1 else 0).toByte()) - dest.writeInt(data?.size ?: 0) - dest.writeByteArray(data) - dest.writeString(dataFile?.absolutePath) - } - - companion object { - - private val TAG = ProtectedBinary::class.java.name - - @JvmField - val CREATOR: Parcelable.Creator = object : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): ProtectedBinary { - return ProtectedBinary(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } - } - -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseException.kt index e16dadb92..51033b5e9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseException.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseException.kt @@ -12,7 +12,8 @@ abstract class DatabaseException : Exception { var parameters: (Array)? = null constructor() : super() - + constructor(message: String) : super(message) + constructor(message: String, throwable: Throwable) : super(message, throwable) constructor(throwable: Throwable) : super(throwable) fun getLocalizedMessage(resources: Resources): String { @@ -26,33 +27,25 @@ open class LoadDatabaseException : DatabaseException { @StringRes override var errorId: Int = R.string.error_load_database - constructor() : super() - - constructor(vararg params: String) : super() { - parameters = params - } - constructor(throwable: Throwable) : super(throwable) } -class LoadDatabaseArcFourException : LoadDatabaseException { +class ArcFourDatabaseException : LoadDatabaseException { @StringRes override var errorId: Int = R.string.error_arc4 - constructor() : super() constructor(exception: Throwable) : super(exception) } -class LoadDatabaseFileNotFoundException : LoadDatabaseException { +class FileNotFoundDatabaseException : LoadDatabaseException { @StringRes override var errorId: Int = R.string.file_not_found_content - constructor() : super() constructor(exception: Throwable) : super(exception) } -class LoadDatabaseInvalidAlgorithmException : LoadDatabaseException { +class InvalidAlgorithmDatabaseException : LoadDatabaseException { @StringRes override var errorId: Int = R.string.invalid_algorithm @@ -60,102 +53,97 @@ class LoadDatabaseInvalidAlgorithmException : LoadDatabaseException { constructor(exception: Throwable) : super(exception) } -class LoadDatabaseDuplicateUuidException: LoadDatabaseException { +class DuplicateUuidDatabaseException: LoadDatabaseException { @StringRes override var errorId: Int = R.string.invalid_db_same_uuid - constructor(type: Type, uuid: PwNodeId<*>) : super() { parameters = arrayOf(type.name, uuid.toString()) } constructor(exception: Throwable) : super(exception) } -class LoadDatabaseIOException : LoadDatabaseException { +class IODatabaseException : LoadDatabaseException { @StringRes override var errorId: Int = R.string.error_load_database - constructor() : super() constructor(exception: Throwable) : super(exception) } -class LoadDatabaseKDFMemoryException : LoadDatabaseException { +class KDFMemoryDatabaseException : LoadDatabaseException { @StringRes override var errorId: Int = R.string.error_load_database_KDF_memory - constructor() : super() constructor(exception: Throwable) : super(exception) } -class LoadDatabaseSignatureException : LoadDatabaseException { +class SignatureDatabaseException : LoadDatabaseException { @StringRes override var errorId: Int = R.string.invalid_db_sig - constructor() : super() constructor(exception: Throwable) : super(exception) } -class LoadDatabaseVersionException : LoadDatabaseException { +class VersionDatabaseException : LoadDatabaseException { @StringRes override var errorId: Int = R.string.unsupported_db_version - constructor() : super() constructor(exception: Throwable) : super(exception) } -class LoadDatabaseInvalidCredentialsException : LoadDatabaseException { +class InvalidCredentialsDatabaseException : LoadDatabaseException { @StringRes override var errorId: Int = R.string.invalid_credentials - constructor() : super() constructor(exception: Throwable) : super(exception) } -class LoadDatabaseKeyFileEmptyException : LoadDatabaseException { +class KeyFileEmptyDatabaseException : LoadDatabaseException { @StringRes override var errorId: Int = R.string.keyfile_is_empty constructor() : super() constructor(exception: Throwable) : super(exception) } -class LoadDatabaseNoMemoryException: LoadDatabaseException { +class NoMemoryDatabaseException: LoadDatabaseException { @StringRes override var errorId: Int = R.string.error_out_of_memory constructor() : super() constructor(exception: Throwable) : super(exception) } -class MoveDatabaseEntryException: LoadDatabaseException { +class EntryDatabaseException: LoadDatabaseException { @StringRes override var errorId: Int = R.string.error_move_entry_here constructor() : super() constructor(exception: Throwable) : super(exception) } -class MoveDatabaseGroupException: LoadDatabaseException { +class MoveGroupDatabaseException: LoadDatabaseException { @StringRes override var errorId: Int = R.string.error_move_folder_in_itself constructor() : super() constructor(exception: Throwable) : super(exception) } -class CopyDatabaseEntryException: LoadDatabaseException { +class CopyEntryDatabaseException: LoadDatabaseException { @StringRes override var errorId: Int = R.string.error_copy_entry_here constructor() : super() constructor(exception: Throwable) : super(exception) } -class CopyDatabaseGroupException: LoadDatabaseException { +class CopyGroupDatabaseException: LoadDatabaseException { @StringRes override var errorId: Int = R.string.error_copy_group_here constructor() : super() constructor(exception: Throwable) : super(exception) } -class DatabaseOutputException : Exception { +// TODO Output Exception +open class DatabaseOutputException : DatabaseException { + @StringRes + override var errorId: Int = R.string.error_save_database constructor(string: String) : super(string) - constructor(string: String, e: Exception) : super(string, e) - constructor(e: Exception) : super(e) } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/PwDbHeaderV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/PwDbHeaderV4.kt index c711d221f..bde2a9f33 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/PwDbHeaderV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/PwDbHeaderV4.kt @@ -25,7 +25,7 @@ import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters import com.kunzisoft.keepass.database.NodeHandler import com.kunzisoft.keepass.database.element.* -import com.kunzisoft.keepass.database.exception.LoadDatabaseVersionException +import com.kunzisoft.keepass.database.exception.VersionDatabaseException import com.kunzisoft.keepass.stream.CopyInputStream import com.kunzisoft.keepass.stream.HmacBlockStream import com.kunzisoft.keepass.stream.LEDataInputStream @@ -130,9 +130,9 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() { /** Assumes the input stream is at the beginning of the .kdbx file * @param inputStream * @throws IOException - * @throws LoadDatabaseVersionException + * @throws VersionDatabaseException */ - @Throws(IOException::class, LoadDatabaseVersionException::class) + @Throws(IOException::class, VersionDatabaseException::class) fun loadFromFile(inputStream: InputStream): HeaderAndHash { val messageDigest: MessageDigest try { @@ -150,12 +150,12 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() { val sig2 = littleEndianDataInputStream.readInt() if (!matchesHeader(sig1, sig2)) { - throw LoadDatabaseVersionException() + throw VersionDatabaseException() } version = littleEndianDataInputStream.readUInt() // Erase previous value if (!validVersion(version)) { - throw LoadDatabaseVersionException() + throw VersionDatabaseException() } var done = false diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV3.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV3.kt index 71798855f..e968d7183 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV3.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV3.kt @@ -93,11 +93,11 @@ class ImporterV3 : Importer() { hdr.loadFromFile(filebuf, 0) if (hdr.signature1 != PwDbHeader.PWM_DBSIG_1 || hdr.signature2 != PwDbHeaderV3.DBSIG_2) { - throw LoadDatabaseSignatureException() + throw SignatureDatabaseException() } if (!hdr.matchesVersion()) { - throw LoadDatabaseVersionException() + throw VersionDatabaseException() } progressTaskUpdater?.updateMessage(R.string.retrieving_db_key) @@ -108,7 +108,7 @@ class ImporterV3 : Importer() { when { hdr.flags and PwDbHeaderV3.FLAG_RIJNDAEL != 0 -> mDatabaseToOpen.encryptionAlgorithm = PwEncryptionAlgorithm.AESRijndael hdr.flags and PwDbHeaderV3.FLAG_TWOFISH != 0 -> mDatabaseToOpen.encryptionAlgorithm = PwEncryptionAlgorithm.Twofish - else -> throw LoadDatabaseInvalidAlgorithmException() + else -> throw InvalidAlgorithmDatabaseException() } mDatabaseToOpen.numberKeyEncryptionRounds = hdr.numKeyEncRounds.toLong() @@ -151,7 +151,7 @@ class ImporterV3 : Importer() { } catch (e1: IllegalBlockSizeException) { throw IOException("Invalid block size") } catch (e1: BadPaddingException) { - throw LoadDatabaseInvalidCredentialsException() + throw InvalidCredentialsDatabaseException() } val md: MessageDigest @@ -170,7 +170,7 @@ class ImporterV3 : Importer() { if (!Arrays.equals(hash, hdr.contentsHash)) { Log.w(TAG, "Database file did not decrypt correctly. (checksum code is broken)") - throw LoadDatabaseInvalidCredentialsException() + throw InvalidCredentialsDatabaseException() } // New manual root because V3 contains multiple root groups (here available with getRootGroups()) @@ -223,9 +223,9 @@ class ImporterV3 : Importer() { } catch (e: LoadDatabaseException) { throw e } catch (e: IOException) { - throw LoadDatabaseIOException(e) + throw IODatabaseException(e) } catch (e: OutOfMemoryError) { - throw LoadDatabaseNoMemoryException(e) + throw NoMemoryDatabaseException(e) } catch (e: Exception) { throw LoadDatabaseException(e) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV4.kt index dd9f8838e..9c39f0940 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV4.kt @@ -26,19 +26,14 @@ import com.kunzisoft.keepass.crypto.StreamCipherFactory import com.kunzisoft.keepass.crypto.engine.CipherEngine import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.PwDatabaseV4.Companion.BASE_64_FLAG -import com.kunzisoft.keepass.database.element.PwDatabaseV4.Companion.BUFFER_SIZE_BYTES -import com.kunzisoft.keepass.database.element.security.ProtectedBinary +import com.kunzisoft.keepass.database.element.security.BinaryAttachment import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.exception.* import com.kunzisoft.keepass.database.file.KDBX4DateUtil import com.kunzisoft.keepass.database.file.PwDbHeaderV4 -import com.kunzisoft.keepass.stream.BetterCipherInputStream -import com.kunzisoft.keepass.stream.HashedBlockInputStream -import com.kunzisoft.keepass.stream.HmacBlockInputStream -import com.kunzisoft.keepass.stream.LEDataInputStream +import com.kunzisoft.keepass.stream.* import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils -import org.apache.commons.io.IOUtils import org.spongycastle.crypto.StreamCipher import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException @@ -48,6 +43,7 @@ import java.nio.charset.Charset import java.text.ParseException import java.util.* import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream import javax.crypto.Cipher import kotlin.math.min @@ -58,10 +54,9 @@ class ImporterV4(private val streamDir: File, private lateinit var mDatabase: PwDatabaseV4 private var hashOfHeader: ByteArray? = null - private var version: Long = 0 private val unusedCacheFileName: String - get() = mDatabase.binPool.findUnusedKey().toString() + get() = mDatabase.binaryPool.findUnusedKey().toString() private var readNextNode = true private val ctxGroups = Stack() @@ -70,7 +65,7 @@ class ImporterV4(private val streamDir: File, private var ctxStringName: String? = null private var ctxStringValue: ProtectedString? = null private var ctxBinaryName: String? = null - private var ctxBinaryValue: ProtectedBinary? = null + private var ctxBinaryValue: BinaryAttachment? = null private var ctxATName: String? = null private var ctxATSeq: String? = null private var entryInHistory = false @@ -102,7 +97,7 @@ class ImporterV4(private val streamDir: File, val header = PwDbHeaderV4(mDatabase) val headerAndHash = header.loadFromFile(databaseInputStream) - version = header.version + mDatabase.kdbxVersion = header.version hashOfHeader = headerAndHash.hash val pbHeader = headerAndHash.header @@ -120,11 +115,11 @@ class ImporterV4(private val streamDir: File, mDatabase.encryptionAlgorithm = engine.getPwEncryptionAlgorithm() cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV) } catch (e: Exception) { - throw LoadDatabaseInvalidAlgorithmException(e) + throw InvalidAlgorithmDatabaseException(e) } val isPlain: InputStream - if (version < PwDbHeaderV4.FILE_VERSION_32_4) { + if (mDatabase.kdbxVersion < PwDbHeaderV4.FILE_VERSION_32_4) { val decrypted = attachCipherStream(databaseInputStream, cipher) val dataDecrypted = LEDataInputStream(decrypted) @@ -132,14 +127,14 @@ class ImporterV4(private val streamDir: File, try { storedStartBytes = dataDecrypted.readBytes(32) if (storedStartBytes == null || storedStartBytes.size != 32) { - throw LoadDatabaseInvalidCredentialsException() + throw InvalidCredentialsDatabaseException() } } catch (e: IOException) { - throw LoadDatabaseInvalidCredentialsException() + throw InvalidCredentialsDatabaseException() } if (!Arrays.equals(storedStartBytes, header.streamStartBytes)) { - throw LoadDatabaseInvalidCredentialsException() + throw InvalidCredentialsDatabaseException() } isPlain = HashedBlockInputStream(dataDecrypted) @@ -147,18 +142,18 @@ class ImporterV4(private val streamDir: File, val isData = LEDataInputStream(databaseInputStream) val storedHash = isData.readBytes(32) if (!Arrays.equals(storedHash, hashOfHeader)) { - throw LoadDatabaseInvalidCredentialsException() + throw InvalidCredentialsDatabaseException() } val hmacKey = mDatabase.hmacKey ?: throw LoadDatabaseException() val headerHmac = PwDbHeaderV4.computeHeaderHmac(pbHeader, hmacKey) val storedHmac = isData.readBytes(32) if (storedHmac == null || storedHmac.size != 32) { - throw LoadDatabaseInvalidCredentialsException() + throw InvalidCredentialsDatabaseException() } // Mac doesn't match if (!Arrays.equals(headerHmac, storedHmac)) { - throw LoadDatabaseInvalidCredentialsException() + throw InvalidCredentialsDatabaseException() } val hmIs = HmacBlockInputStream(isData, true, hmacKey) @@ -172,14 +167,14 @@ class ImporterV4(private val streamDir: File, else -> isPlain } - if (version >= PwDbHeaderV4.FILE_VERSION_32_4) { + if (mDatabase.kdbxVersion >= PwDbHeaderV4.FILE_VERSION_32_4) { loadInnerHeader(inputStreamXml, header) } randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey) if (randomStream == null) { - throw LoadDatabaseArcFourException() + throw ArcFourDatabaseException() } readDocumentStreamed(createPullParser(inputStreamXml)) @@ -187,14 +182,14 @@ class ImporterV4(private val streamDir: File, } catch (e: LoadDatabaseException) { throw e } catch (e: XmlPullParserException) { - throw LoadDatabaseIOException(e) + throw IODatabaseException(e) } catch (e: IOException) { if (e.message?.contains("Hash failed with code") == true) - throw LoadDatabaseKDFMemoryException(e) + throw KDFMemoryDatabaseException(e) else - throw LoadDatabaseIOException(e) + throw IODatabaseException(e) } catch (e: OutOfMemoryError) { - throw LoadDatabaseNoMemoryException(e) + throw NoMemoryDatabaseException(e) } catch (e: Exception) { throw LoadDatabaseException(e) } @@ -241,10 +236,14 @@ class ImporterV4(private val streamDir: File, // Read in a file val file = File(streamDir, unusedCacheFileName) FileOutputStream(file).use { outputStream -> - dataInputStream.readBytes(byteLength) { outputStream.write(it) } + dataInputStream.readBytes(byteLength, object : ReadBytes { + override fun read(buffer: ByteArray) { + outputStream.write(buffer) + } + }) } - val protectedBinary = ProtectedBinary(protectedFlag, file) - mDatabase.binPool.add(protectedBinary) + val protectedBinary = BinaryAttachment(file, protectedFlag) + mDatabase.binaryPool.add(protectedBinary) } else -> { return false @@ -432,9 +431,9 @@ class ImporterV4(private val streamDir: File, KdbContext.Binaries -> if (name.equals(PwDatabaseV4XML.ElemBinary, ignoreCase = true)) { val key = xpp.getAttributeValue(null, PwDatabaseV4XML.AttrId) if (key != null) { - val pbData = readProtectedBinary(xpp) + val pbData = readBinary(xpp) val id = Integer.parseInt(key) - mDatabase.binPool.put(id, pbData!!) + mDatabase.binaryPool.put(id, pbData!!) } else { readUnknown(xpp) } @@ -606,7 +605,7 @@ class ImporterV4(private val streamDir: File, KdbContext.EntryBinary -> if (name.equals(PwDatabaseV4XML.ElemKey, ignoreCase = true)) { ctxBinaryName = readString(xpp) } else if (name.equals(PwDatabaseV4XML.ElemValue, ignoreCase = true)) { - ctxBinaryValue = readProtectedBinary(xpp) + ctxBinaryValue = readBinary(xpp) } KdbContext.EntryAutoType -> if (name.equals(PwDatabaseV4XML.ElemAutoTypeEnabled, ignoreCase = true)) { @@ -806,7 +805,7 @@ class ImporterV4(private val streamDir: File, val sDate = readString(xpp) var utcDate: Date? = null - if (version >= PwDbHeaderV4.FILE_VERSION_32_4) { + if (mDatabase.kdbxVersion >= PwDbHeaderV4.FILE_VERSION_32_4) { var buf = Base64.decode(sDate, BASE_64_FLAG) if (buf.size != 8) { val buf8 = ByteArray(8) @@ -939,50 +938,51 @@ class ImporterV4(private val streamDir: File, } @Throws(XmlPullParserException::class, IOException::class) - private fun readProtectedBinary(xpp: XmlPullParser): ProtectedBinary? { + private fun readBinary(xpp: XmlPullParser): BinaryAttachment? { + + // Reference Id to a binary already present in binary pool val ref = xpp.getAttributeValue(null, PwDatabaseV4XML.AttrRef) if (ref != null) { xpp.next() // Consume end tag val id = Integer.parseInt(ref) - return mDatabase.binPool[id] + return mDatabase.binaryPool[id] } - var compressed = false - var protected = false + // New binary to retrieve + else { + var compressed: Boolean? = null + var protected = false - if (xpp.attributeCount > 0) { - val compress = xpp.getAttributeValue(null, PwDatabaseV4XML.AttrCompressed) - if (compress != null) { - compressed = compress.equals(PwDatabaseV4XML.ValTrue, ignoreCase = true) + if (xpp.attributeCount > 0) { + val compress = xpp.getAttributeValue(null, PwDatabaseV4XML.AttrCompressed) + if (compress != null) { + compressed = compress.equals(PwDatabaseV4XML.ValTrue, ignoreCase = true) + } + + val protect = xpp.getAttributeValue(null, PwDatabaseV4XML.AttrProtected) + if (protect != null) { + protected = protect.equals(PwDatabaseV4XML.ValTrue, ignoreCase = true) + } } - val protect = xpp.getAttributeValue(null, PwDatabaseV4XML.AttrProtected) - if (protect != null) { - protected = protect.equals(PwDatabaseV4XML.ValTrue, ignoreCase = true) - } - } + val base64 = readString(xpp) + if (base64.isEmpty()) + return BinaryAttachment() + val data = Base64.decode(base64, BASE_64_FLAG) - val base64 = readString(xpp) - if (base64.isEmpty()) - return ProtectedBinary() - val data = Base64.decode(base64, BASE_64_FLAG) - - return if (!compressed && data.size <= BUFFER_SIZE_BYTES) { - // Small data, don't need a file - ProtectedBinary(protected, data) - } else { val file = File(streamDir, unusedCacheFileName) - if (compressed) { - FileOutputStream(file).use { outputStream -> - IOUtils.copy(GZIPInputStream(ByteArrayInputStream(data)), outputStream) - } - } else { - FileOutputStream(file).use { outputStream -> + return FileOutputStream(file).use { outputStream -> + // Force compression in this specific case + if (mDatabase.compressionAlgorithm == PwCompressionAlgorithm.GZip + && compressed == false) { + GZIPOutputStream(outputStream).write(data) + BinaryAttachment(file, protected, true) + } else { outputStream.write(data) + BinaryAttachment(file, protected) } } - ProtectedBinary(protected, file) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbHeaderOutputV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbHeaderOutputV4.kt index 6698e02d8..fcc86f68f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbHeaderOutputV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbHeaderOutputV4.kt @@ -54,7 +54,7 @@ constructor(private val db: PwDatabaseV4, private val header: PwDbHeaderV4, os: try { md = MessageDigest.getInstance("SHA-256") } catch (e: NoSuchAlgorithmException) { - throw DatabaseOutputException("SHA-256 not implemented here.") + throw DatabaseOutputException("SHA-256 not implemented here.", e) } try { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbInnerHeaderOutputV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbInnerHeaderOutputV4.kt index 70868ba5e..4c3d59e85 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbInnerHeaderOutputV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbInnerHeaderOutputV4.kt @@ -22,10 +22,10 @@ package com.kunzisoft.keepass.database.file.save import com.kunzisoft.keepass.database.element.PwDatabaseV4 import com.kunzisoft.keepass.database.element.PwDatabaseV4.Companion.BUFFER_SIZE_BYTES import com.kunzisoft.keepass.database.file.PwDbHeaderV4 -import com.kunzisoft.keepass.stream.ActionReadBytes +import com.kunzisoft.keepass.stream.ReadBytes import com.kunzisoft.keepass.stream.LEDataOutputStream +import com.kunzisoft.keepass.stream.readFromStream import java.io.IOException -import java.io.InputStream import java.io.OutputStream import kotlin.experimental.or @@ -48,7 +48,7 @@ class PwDbInnerHeaderOutputV4(private val database: PwDatabaseV4, dataOutputStream.writeInt(streamKeySize) dataOutputStream.write(header.innerRandomStreamKey) - database.binPool.doForEachBinary { _, protectedBinary -> + database.binaryPool.doForEachBinary { _, protectedBinary -> var flag = PwDbHeaderV4.KdbxBinaryFlags.None if (protectedBinary.isProtected) { flag = flag or PwDbHeaderV4.KdbxBinaryFlags.Protected @@ -58,32 +58,16 @@ class PwDbInnerHeaderOutputV4(private val database: PwDatabaseV4, dataOutputStream.writeInt(protectedBinary.length().toInt() + 1) // TODO verify dataOutputStream.write(flag.toInt()) - protectedBinary.getData()?.let { - readBytes(it, ActionReadBytes { buffer -> - dataOutputStream.write(buffer) - }) - } ?: throw IOException("Can't write protected binary") + readFromStream(protectedBinary.getInputDataStream(), BUFFER_SIZE_BYTES, + object : ReadBytes { + override fun read(buffer: ByteArray) { + dataOutputStream.write(buffer) + } + } + ) } dataOutputStream.write(PwDbHeaderV4.PwDbInnerHeaderV4Fields.EndOfHeader.toInt()) dataOutputStream.writeInt(0) } - - @Throws(IOException::class) - fun readBytes(inputStream: InputStream, actionReadBytes: ActionReadBytes) { - val buffer = ByteArray(BUFFER_SIZE_BYTES) - var read = 0 - while (read != -1) { - read = inputStream.read(buffer, 0, buffer.size) - if (read != -1) { - val optimizedBuffer: ByteArray = if (buffer.size == read) { - buffer - } else { - buffer.copyOf(read) - } - actionReadBytes.doAction(optimizedBuffer) - } - } - } - } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbOutput.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbOutput.kt index a670182ca..6d59bd776 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbOutput.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbOutput.kt @@ -34,7 +34,7 @@ abstract class PwDbOutput
protected constructor(protected v try { random = SecureRandom.getInstance("SHA1PRNG") } catch (e: NoSuchAlgorithmException) { - throw DatabaseOutputException("Does not support secure random number generation.") + throw DatabaseOutputException("Does not support secure random number generation.", e) } random.nextBytes(header.encryptionIV) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV4Output.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV4Output.kt index 49496d2f4..c452eaba6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV4Output.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV4Output.kt @@ -31,15 +31,13 @@ import com.kunzisoft.keepass.database.NodeHandler import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.PwDatabaseV4.Companion.BASE_64_FLAG import com.kunzisoft.keepass.database.element.PwDatabaseV4.Companion.BUFFER_SIZE_BYTES -import com.kunzisoft.keepass.database.element.security.ProtectedBinary +import com.kunzisoft.keepass.database.element.security.BinaryAttachment import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.database.exception.UnknownKDF import com.kunzisoft.keepass.database.file.KDBX4DateUtil import com.kunzisoft.keepass.database.file.PwDbHeaderV4 -import com.kunzisoft.keepass.stream.HashedBlockOutputStream -import com.kunzisoft.keepass.stream.HmacBlockOutputStream -import com.kunzisoft.keepass.stream.LEDataOutputStream +import com.kunzisoft.keepass.stream.* import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils import org.joda.time.DateTime import org.spongycastle.crypto.StreamCipher @@ -48,12 +46,14 @@ import java.io.* import java.security.NoSuchAlgorithmException import java.security.SecureRandom import java.util.* +import java.util.zip.GZIPInputStream import java.util.zip.GZIPOutputStream import javax.crypto.Cipher import javax.crypto.CipherOutputStream -class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputStream) : PwDbOutput(outputStream) { +class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, + outputStream: OutputStream) : PwDbOutput(outputStream) { private var randomStream: StreamCipher? = null private lateinit var xml: XmlSerializer @@ -75,17 +75,16 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt header = outputHeader(mOS) val osPlain: OutputStream - if (header!!.version < PwDbHeaderV4.FILE_VERSION_32_4) { + osPlain = if (header!!.version < PwDbHeaderV4.FILE_VERSION_32_4) { val cos = attachStreamEncryptor(header!!, mOS) cos.write(header!!.streamStartBytes) - osPlain = HashedBlockOutputStream(cos) + HashedBlockOutputStream(cos) } else { mOS.write(hashOfHeader!!) mOS.write(headerHmac!!) - val hbos = HmacBlockOutputStream(mOS, mDatabaseV4.hmacKey) - osPlain = attachStreamEncryptor(header!!, hbos) + attachStreamEncryptor(header!!, HmacBlockOutputStream(mOS, mDatabaseV4.hmacKey)) } val osXml: OutputStream @@ -114,11 +113,11 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt } @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) - private fun outputDatabase(os: OutputStream) { + private fun outputDatabase(outputStream: OutputStream) { xml = Xml.newSerializer() - xml.setOutput(os, "UTF-8") + xml.setOutput(outputStream, "UTF-8") xml.startDocument("UTF-8", true) xml.startTag(null, PwDatabaseV4XML.ElemDocNode) @@ -210,18 +209,19 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt writeCustomIconList() writeObject(PwDatabaseV4XML.ElemRecycleBinEnabled, mDatabaseV4.isRecycleBinEnabled) - writeObject(PwDatabaseV4XML.ElemRecycleBinUuid, mDatabaseV4.recycleBinUUID) + writeUuid(PwDatabaseV4XML.ElemRecycleBinUuid, mDatabaseV4.recycleBinUUID) writeObject(PwDatabaseV4XML.ElemRecycleBinChanged, mDatabaseV4.recycleBinChanged) - writeObject(PwDatabaseV4XML.ElemEntryTemplatesGroup, mDatabaseV4.entryTemplatesGroup) + writeUuid(PwDatabaseV4XML.ElemEntryTemplatesGroup, mDatabaseV4.entryTemplatesGroup) writeObject(PwDatabaseV4XML.ElemEntryTemplatesGroupChanged, mDatabaseV4.entryTemplatesGroupChanged.date) writeObject(PwDatabaseV4XML.ElemHistoryMaxItems, mDatabaseV4.historyMaxItems.toLong()) writeObject(PwDatabaseV4XML.ElemHistoryMaxSize, mDatabaseV4.historyMaxSize) - writeObject(PwDatabaseV4XML.ElemLastSelectedGroup, mDatabaseV4.lastSelectedGroupUUID) - writeObject(PwDatabaseV4XML.ElemLastTopVisibleGroup, mDatabaseV4.lastTopVisibleGroupUUID) + writeUuid(PwDatabaseV4XML.ElemLastSelectedGroup, mDatabaseV4.lastSelectedGroupUUID) + writeUuid(PwDatabaseV4XML.ElemLastTopVisibleGroup, mDatabaseV4.lastTopVisibleGroupUUID) + + // Seem to work properly if always in meta + // if (header!!.version < PwDbHeaderV4.FILE_VERSION_32_4) + writeMetaBinaries() - if (header!!.version < PwDbHeaderV4.FILE_VERSION_32_4) { - writeBinPool() - } writeCustomData(mDatabaseV4.customData) xml.endTag(null, PwDatabaseV4XML.ElemMeta) @@ -306,13 +306,13 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) private fun startGroup(group: PwGroupV4) { xml.startTag(null, PwDatabaseV4XML.ElemGroup) - writeObject(PwDatabaseV4XML.ElemUuid, group.id) + writeUuid(PwDatabaseV4XML.ElemUuid, group.id) writeObject(PwDatabaseV4XML.ElemName, group.title) writeObject(PwDatabaseV4XML.ElemNotes, group.notes) writeObject(PwDatabaseV4XML.ElemIcon, group.icon.iconId.toLong()) if (group.iconCustom != PwIconCustom.UNKNOWN_ICON) { - writeObject(PwDatabaseV4XML.ElemCustomIconID, group.iconCustom.uuid) + writeUuid(PwDatabaseV4XML.ElemCustomIconID, group.iconCustom.uuid) } writeTimes(group) @@ -320,7 +320,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt writeObject(PwDatabaseV4XML.ElemGroupDefaultAutoTypeSeq, group.defaultAutoTypeSequence) writeObject(PwDatabaseV4XML.ElemEnableAutoType, group.enableAutoType) writeObject(PwDatabaseV4XML.ElemEnableSearching, group.enableSearching) - writeObject(PwDatabaseV4XML.ElemLastTopVisibleEntry, group.lastTopVisibleEntry) + writeUuid(PwDatabaseV4XML.ElemLastTopVisibleEntry, group.lastTopVisibleEntry) } @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) @@ -333,11 +333,11 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt xml.startTag(null, PwDatabaseV4XML.ElemEntry) - writeObject(PwDatabaseV4XML.ElemUuid, entry.id) + writeUuid(PwDatabaseV4XML.ElemUuid, entry.id) writeObject(PwDatabaseV4XML.ElemIcon, entry.icon.iconId.toLong()) if (entry.iconCustom != PwIconCustom.UNKNOWN_ICON) { - writeObject(PwDatabaseV4XML.ElemCustomIconID, entry.iconCustom.uuid) + writeUuid(PwDatabaseV4XML.ElemCustomIconID, entry.iconCustom.uuid) } writeObject(PwDatabaseV4XML.ElemFgColor, entry.foregroundColor) @@ -348,7 +348,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt writeTimes(entry) writeFields(entry.fields) - writeList(entry.binaries) + writeEntryBinaries(entry.binaries) writeAutoType(entry.autoType) if (!isHistory) { @@ -358,81 +358,6 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt xml.endTag(null, PwDatabaseV4XML.ElemEntry) } - - @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) - private fun writeObject(key: String, binary: ProtectedBinary) { - - xml.startTag(null, PwDatabaseV4XML.ElemBinary) - xml.startTag(null, PwDatabaseV4XML.ElemKey) - xml.text(safeXmlString(key)) - xml.endTag(null, PwDatabaseV4XML.ElemKey) - - xml.startTag(null, PwDatabaseV4XML.ElemValue) - val ref = mDatabaseV4.binPool.findKey(binary) - if (ref != null) { - xml.attribute(null, PwDatabaseV4XML.AttrRef, ref.toString()) - } else { - writeBinary(binary) - } - xml.endTag(null, PwDatabaseV4XML.ElemValue) - - xml.endTag(null, PwDatabaseV4XML.ElemBinary) - } - - @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) - private fun writeBinary(value: ProtectedBinary) { - - val valLength = value.length().toInt() - if (valLength > 0) { - val buffer = ByteArray(valLength) - if (valLength == value.getData()!!.read(buffer, 0, valLength)) { - - if (value.isProtected) { - xml.attribute(null, PwDatabaseV4XML.AttrProtected, PwDatabaseV4XML.ValTrue) - - val encoded = ByteArray(valLength) - randomStream!!.processBytes(buffer, 0, valLength, encoded, 0) - xml.text(String(Base64.encode(encoded, BASE_64_FLAG))) - - } else { - if (mDatabaseV4.compressionAlgorithm === PwCompressionAlgorithm.GZip) { - xml.attribute(null, PwDatabaseV4XML.AttrCompressed, PwDatabaseV4XML.ValTrue) - - val byteArrayOutputStream = ByteArrayOutputStream() - val gzipOutputStream = GZIPOutputStream(byteArrayOutputStream) - copyStream(ByteArrayInputStream(buffer), gzipOutputStream) - // IOUtils.copy(ByteArrayInputStream(ByteArrayInputStream(buffer)), gzipOutputStream) - gzipOutputStream.close() - - xml.text(String(Base64.encode(byteArrayOutputStream.toByteArray(), BASE_64_FLAG))) - - } else { - xml.text(String(Base64.encode(buffer, BASE_64_FLAG))) - } - } - } else { - Log.e(TAG, "Unable to read the stream of the protected binary") - } - } - } - - @Throws(IOException::class) - fun copyStream(inputStream: InputStream, out: OutputStream) { - val buffer = ByteArray(BUFFER_SIZE_BYTES) - try { - var read = inputStream.read(buffer) - while (read != -1) { - out.write(buffer, 0, read) - read = inputStream.read(buffer) - if (Thread.interrupted()) { - throw InterruptedException() - } - } - } catch (error: OutOfMemoryError) { - throw IOException(error) - } - } - @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) private fun writeObject(name: String, value: String, filterXmlChars: Boolean = false) { var xmlString = value @@ -477,11 +402,52 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt } @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) - private fun writeObject(name: String, uuid: UUID) { + private fun writeUuid(name: String, uuid: UUID) { val data = DatabaseInputOutputUtils.uuidToBytes(uuid) writeObject(name, String(Base64.encode(data, BASE_64_FLAG))) } + @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) + private fun writeMetaBinaries() { + xml.startTag(null, PwDatabaseV4XML.ElemBinaries) + + mDatabaseV4.binaryPool.doForEachBinary { key, binary -> + xml.startTag(null, PwDatabaseV4XML.ElemBinary) + xml.attribute(null, PwDatabaseV4XML.AttrId, key.toString()) + + // Force binary compression from database (compression was harmonized during import) + xml.attribute(null, PwDatabaseV4XML.AttrCompressed, + if (mDatabaseV4.compressionAlgorithm === PwCompressionAlgorithm.GZip) { + PwDatabaseV4XML.ValTrue + } else { + PwDatabaseV4XML.ValFalse + } + ) + + // Force decompression in this specific case + val binaryInputStream = if (mDatabaseV4.compressionAlgorithm == PwCompressionAlgorithm.None + && binary.isCompressed == true) { + GZIPInputStream(binary.getInputDataStream()) + } else { + binary.getInputDataStream() + } + + // Write the XML + readFromStream(binaryInputStream, BUFFER_SIZE_BYTES, + object : ReadBytes { + override fun read(buffer: ByteArray) { + val charArray = String(Base64.encode(buffer, BASE_64_FLAG)).toCharArray() + xml.text(charArray, 0, charArray.size) + } + } + ) + + xml.endTag(null, PwDatabaseV4XML.ElemBinary) + } + + xml.endTag(null, PwDatabaseV4XML.ElemBinaries) + } + @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) private fun writeObject(name: String, keyName: String, keyValue: String, valueName: String, valueValue: String) { xml.startTag(null, name) @@ -565,20 +531,58 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt private fun writeDeletedObject(value: PwDeletedObject) { xml.startTag(null, PwDatabaseV4XML.ElemDeletedObject) - writeObject(PwDatabaseV4XML.ElemUuid, value.uuid) + writeUuid(PwDatabaseV4XML.ElemUuid, value.uuid) writeObject(PwDatabaseV4XML.ElemDeletionTime, value.deletionTime) xml.endTag(null, PwDatabaseV4XML.ElemDeletedObject) } @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) - private fun writeList(binaries: Map) { - for ((key, value) in binaries) { - writeObject(key, value) + private fun writeEntryBinaries(binaries: Map) { + for ((key, binary) in binaries) { + xml.startTag(null, PwDatabaseV4XML.ElemBinary) + xml.startTag(null, PwDatabaseV4XML.ElemKey) + xml.text(safeXmlString(key)) + xml.endTag(null, PwDatabaseV4XML.ElemKey) + + xml.startTag(null, PwDatabaseV4XML.ElemValue) + val ref = mDatabaseV4.binaryPool.findKey(binary) + if (ref != null) { + xml.attribute(null, PwDatabaseV4XML.AttrRef, ref.toString()) + } else { + val binaryLength = binary.length() + if (binaryLength > 0) { + + if (binary.isProtected) { + xml.attribute(null, PwDatabaseV4XML.AttrProtected, PwDatabaseV4XML.ValTrue) + + readFromStream(binary.getInputDataStream(), BUFFER_SIZE_BYTES, + object : ReadBytes { + override fun read(buffer: ByteArray) { + val encoded = ByteArray(buffer.size) + randomStream!!.processBytes(buffer, 0, encoded.size, encoded, 0) + val charArray = String(Base64.encode(encoded, BASE_64_FLAG)).toCharArray() + xml.text(charArray, 0, charArray.size) + } + }) + } else { + readFromStream(binary.getInputDataStream(), BUFFER_SIZE_BYTES, + object : ReadBytes { + override fun read(buffer: ByteArray) { + val charArray = String(Base64.encode(buffer, BASE_64_FLAG)).toCharArray() + xml.text(charArray, 0, charArray.size) + } + } + ) + } + } + } + xml.endTag(null, PwDatabaseV4XML.ElemValue) + + xml.endTag(null, PwDatabaseV4XML.ElemBinary) } } - @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) private fun writeDeletedObjects(value: List) { xml.startTag(null, PwDatabaseV4XML.ElemDeletedObjects) @@ -658,7 +662,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt for (icon in customIcons) { xml.startTag(null, PwDatabaseV4XML.ElemCustomIconItem) - writeObject(PwDatabaseV4XML.ElemCustomIconItemID, icon.uuid) + writeUuid(PwDatabaseV4XML.ElemCustomIconItemID, icon.uuid) writeObject(PwDatabaseV4XML.ElemCustomIconItemData, String(Base64.encode(icon.imageData, BASE_64_FLAG))) xml.endTag(null, PwDatabaseV4XML.ElemCustomIconItem) @@ -667,22 +671,6 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt xml.endTag(null, PwDatabaseV4XML.ElemCustomIcons) } - @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) - private fun writeBinPool() { - xml.startTag(null, PwDatabaseV4XML.ElemBinaries) - - mDatabaseV4.binPool.doForEachBinary { key, binary -> - xml.startTag(null, PwDatabaseV4XML.ElemBinary) - xml.attribute(null, PwDatabaseV4XML.AttrId, key.toString()) - - writeBinary(binary) - - xml.endTag(null, PwDatabaseV4XML.ElemBinary) - } - - xml.endTag(null, PwDatabaseV4XML.ElemBinaries) - } - private fun safeXmlString(text: String): String { if (text.isEmpty()) { return text diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt index 56fedf46b..0edb42bde 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt @@ -8,10 +8,7 @@ import android.os.Bundle import android.os.IBinder import com.kunzisoft.keepass.R import com.kunzisoft.keepass.app.database.CipherDatabaseEntity -import com.kunzisoft.keepass.database.action.AssignPasswordInDatabaseRunnable -import com.kunzisoft.keepass.database.action.CreateDatabaseRunnable -import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable -import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable +import com.kunzisoft.keepass.database.action.* import com.kunzisoft.keepass.database.action.node.* import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.settings.PreferencesUtil @@ -107,11 +104,11 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat ACTION_DATABASE_COPY_NODES_TASK -> buildDatabaseCopyNodesActionTask(intent) ACTION_DATABASE_MOVE_NODES_TASK -> buildDatabaseMoveNodesActionTask(intent) ACTION_DATABASE_DELETE_NODES_TASK -> buildDatabaseDeleteNodesActionTask(intent) + ACTION_DATABASE_UPDATE_COMPRESSION_TASK -> buildDatabaseUpdateCompressionActionTask(intent) ACTION_DATABASE_UPDATE_NAME_TASK, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK, ACTION_DATABASE_UPDATE_COLOR_TASK, - ACTION_DATABASE_UPDATE_COMPRESSION_TASK, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK, ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK, @@ -409,6 +406,25 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat } } + private fun buildDatabaseUpdateCompressionActionTask(intent: Intent): ActionRunnable? { + return if (intent.hasExtra(OLD_ELEMENT_KEY) + && intent.hasExtra(NEW_ELEMENT_KEY) + && intent.hasExtra(SAVE_DATABASE_KEY)) { + return UpdateCompressionBinariesDatabaseRunnable(this, + Database.getInstance(), + intent.getParcelableExtra(OLD_ELEMENT_KEY), + intent.getParcelableExtra(NEW_ELEMENT_KEY), + intent.getBooleanExtra(SAVE_DATABASE_KEY, false) + ).apply { + mAfterSaveDatabase = { result -> + result.data = intent.extras + } + } + } else { + null + } + } + private fun buildDatabaseUpdateElementActionTask(intent: Intent): ActionRunnable? { return if (intent.hasExtra(SAVE_DATABASE_KEY)) { return SaveDatabaseRunnable(this, diff --git a/app/src/main/java/com/kunzisoft/keepass/stream/ActionReadBytes.java b/app/src/main/java/com/kunzisoft/keepass/stream/ActionReadBytes.java deleted file mode 100644 index d72ecb2a4..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/stream/ActionReadBytes.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.kunzisoft.keepass.stream; - -import java.io.IOException; - -public interface ActionReadBytes { - /** - * Called after each buffer fill - * @param buffer filled - */ - void doAction(byte[] buffer) throws IOException; -} diff --git a/app/src/main/java/com/kunzisoft/keepass/stream/LEDataInputStream.java b/app/src/main/java/com/kunzisoft/keepass/stream/LEDataInputStream.java index 7af2f021e..ead0413de 100644 --- a/app/src/main/java/com/kunzisoft/keepass/stream/LEDataInputStream.java +++ b/app/src/main/java/com/kunzisoft/keepass/stream/LEDataInputStream.java @@ -124,7 +124,7 @@ public class LEDataInputStream extends InputStream { return buf; } - public void readBytes(int length, ActionReadBytes actionReadBytes) throws IOException { + public void readBytes(int length, ReadBytes readBytes) throws IOException { int bufferSize = 256 * 3; // TODO Buffer size byte[] buffer = new byte[bufferSize]; @@ -146,7 +146,7 @@ public class LEDataInputStream extends InputStream { } else { optimizedBuffer = buffer; } - actionReadBytes.doAction(optimizedBuffer); + readBytes.read(optimizedBuffer); offset += read; } } diff --git a/app/src/main/java/com/kunzisoft/keepass/stream/ReadBytes.kt b/app/src/main/java/com/kunzisoft/keepass/stream/ReadBytes.kt new file mode 100644 index 000000000..3c03dabd4 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/stream/ReadBytes.kt @@ -0,0 +1,30 @@ +package com.kunzisoft.keepass.stream + +import java.io.IOException +import java.io.InputStream + +interface ReadBytes { + /** + * Called after each buffer fill + * @param buffer filled + */ + @Throws(IOException::class) + fun read(buffer: ByteArray) +} + +@Throws(IOException::class) +fun readFromStream(inputStream: InputStream, bufferSize: Int, readBytes: ReadBytes) { + val buffer = ByteArray(bufferSize) + var read = 0 + while (read != -1) { + read = inputStream.read(buffer, 0, buffer.size) + if (read != -1) { + val optimizedBuffer: ByteArray = if (buffer.size == read) { + buffer + } else { + buffer.copyOf(read) + } + readBytes.read(optimizedBuffer) + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/tasks/ActionRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/tasks/ActionRunnable.kt index a0eba5888..e75362079 100644 --- a/app/src/main/java/com/kunzisoft/keepass/tasks/ActionRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/tasks/ActionRunnable.kt @@ -20,7 +20,8 @@ package com.kunzisoft.keepass.tasks import android.os.Bundle -import com.kunzisoft.keepass.database.exception.LoadDatabaseException +import com.kunzisoft.keepass.database.exception.DatabaseException +import java.lang.Exception /** * Callback after a task is completed. @@ -44,24 +45,29 @@ abstract class ActionRunnable: Runnable { */ abstract fun onFinishRun() - protected fun setError(message: String? = null) { - setError(null, message) - } - - protected fun setError(exception: LoadDatabaseException?, - message: String? = null) { + protected fun setError(message: String) { result.isSuccess = false - result.exception = exception + result.exception = null result.message = message } + protected fun setError(exception: Exception) { + result.isSuccess = false + result.exception = null + result.message = exception.message + } + protected fun setError(exception: DatabaseException) { + result.isSuccess = false + result.exception = exception + result.message = exception.message + } /** * Class to manage result from ActionRunnable */ data class Result(var isSuccess: Boolean = true, var message: String? = null, - var exception: LoadDatabaseException? = null, + var exception: DatabaseException? = null, var data: Bundle? = null) } diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/ParcelableUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/ParcelableUtil.kt index c37c5cdc1..ada214fc3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/ParcelableUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/ParcelableUtil.kt @@ -96,3 +96,9 @@ object ParcelableUtil { return map } } + +inline fun > Parcel.readEnum() = + readString()?.let { enumValueOf(it) } + +inline fun > Parcel.writeEnum(value: T?) = + writeString(value?.name) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ba2a2d611..8000d4462 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -121,6 +121,7 @@ You can not copy an entry here. You can not copy a group here. Unable to create database with this password and key file. + Could not save database. Secret key must be in Base32 format. Counter must be between %1$d and %2$d. Period must be between %1$d and %2$d seconds.