mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
153 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
123288e745 | ||
|
|
11aae77caf | ||
|
|
8212cede6e | ||
|
|
a3c51884f4 | ||
|
|
b8890aca7f | ||
|
|
014b0cce14 | ||
|
|
c40b255022 | ||
|
|
1742d265f3 | ||
|
|
ff185f6505 | ||
|
|
346b517c9d | ||
|
|
949905f6e2 | ||
|
|
b9e26fecfd | ||
|
|
de69a78a98 | ||
|
|
1c341c34a3 | ||
|
|
33beb57e9d | ||
|
|
66eeadca0b | ||
|
|
a10d1c98a8 | ||
|
|
59ead4986f | ||
|
|
09f6c18189 | ||
|
|
0f3ad7c8b1 | ||
|
|
0487dea7fc | ||
|
|
8cac1ee284 | ||
|
|
196620e1bd | ||
|
|
43d6c76873 | ||
|
|
b864c39a0d | ||
|
|
818b975111 | ||
|
|
d5fbc8393f | ||
|
|
7b5e9d2344 | ||
|
|
7fc2d95886 | ||
|
|
78d3b369bb | ||
|
|
bb3620680b | ||
|
|
d4a45655ca | ||
|
|
c9c739fd52 | ||
|
|
2b359cc592 | ||
|
|
151b7a323d | ||
|
|
1063dc2b63 | ||
|
|
f9f59a6eb1 | ||
|
|
73156cc337 | ||
|
|
7d53607f49 | ||
|
|
7539945465 | ||
|
|
51df8e7bb1 | ||
|
|
17029ce67c | ||
|
|
8cedc313cf | ||
|
|
5afe3acac1 | ||
|
|
9887b58b71 | ||
|
|
ec8363ba6a | ||
|
|
fcfb71f13b | ||
|
|
3a12e431ff | ||
|
|
bc4ed8e123 | ||
|
|
445e9540a5 | ||
|
|
bbc2a2a9dd | ||
|
|
5117bc78b6 | ||
|
|
10bf149a07 | ||
|
|
0a976bd012 | ||
|
|
df31c43e59 | ||
|
|
c5d30b9b23 | ||
|
|
2e18beff27 | ||
|
|
25e0cec2cc | ||
|
|
16cc4c5c97 | ||
|
|
e5cfb6b7eb | ||
|
|
a882ba07e9 | ||
|
|
801f3f99aa | ||
|
|
2338b9b57d | ||
|
|
8ba396c693 | ||
|
|
1164022765 | ||
|
|
b0c5519da5 | ||
|
|
f5073238d8 | ||
|
|
3ffa89bfaf | ||
|
|
26d8b2fa22 | ||
|
|
6b17502694 | ||
|
|
a69d57a4f4 | ||
|
|
430bc6150f | ||
|
|
b888615e0d | ||
|
|
9fae343668 | ||
|
|
db467889b0 | ||
|
|
153b8d1f37 | ||
|
|
87858762d4 | ||
|
|
50d3282a65 | ||
|
|
aee0500b38 | ||
|
|
f2ef6eb94e | ||
|
|
151a5a7e73 | ||
|
|
6a088c58de | ||
|
|
23e7bf9f89 | ||
|
|
59f24206ad | ||
|
|
e45ef019c0 | ||
|
|
2830d3c8fa | ||
|
|
088816dfab | ||
|
|
453a29b81c | ||
|
|
e5cb160aa4 | ||
|
|
844588a0d4 | ||
|
|
cfcfd47705 | ||
|
|
f416c0ec7d | ||
|
|
78f707c07c | ||
|
|
d28a59a2fe | ||
|
|
edf3525a3f | ||
|
|
2b7fe35305 | ||
|
|
d5819ea4d0 | ||
|
|
1099126def | ||
|
|
4ba3a797e3 | ||
|
|
51b9bb88e5 | ||
|
|
fa376148bd | ||
|
|
452b9677da | ||
|
|
02a779f9a2 | ||
|
|
ea60645247 | ||
|
|
b444a13285 | ||
|
|
0c6b2a13eb | ||
|
|
e10bdc1169 | ||
|
|
7512cffca3 | ||
|
|
203440e9b8 | ||
|
|
145030e854 | ||
|
|
2b81dfb100 | ||
|
|
c96ace5281 | ||
|
|
520c6b60be | ||
|
|
492382d552 | ||
|
|
7ca55dd531 | ||
|
|
ce4ba73fc4 | ||
|
|
5622d92cbb | ||
|
|
8de1c5fd36 | ||
|
|
6c84fea8dc | ||
|
|
0b94070086 | ||
|
|
2f209182f5 | ||
|
|
d7bc572f3e | ||
|
|
4985b49194 | ||
|
|
a792df2021 | ||
|
|
a42ec74723 | ||
|
|
de3dbe3b36 | ||
|
|
874fdb7da0 | ||
|
|
3bf0de3888 | ||
|
|
d4020c5e0f | ||
|
|
6175fc00ad | ||
|
|
76e996f429 | ||
|
|
fd222b73ce | ||
|
|
a8cc0b1edf | ||
|
|
ea2f3545a6 | ||
|
|
0cf136712a | ||
|
|
34a453873a | ||
|
|
0acac3b096 | ||
|
|
37141410e0 | ||
|
|
69b0e276e3 | ||
|
|
ede6070e43 | ||
|
|
a61b1d4337 | ||
|
|
0f8c71a9df | ||
|
|
cb0b6e010d | ||
|
|
23dc7be1ab | ||
|
|
4b14ad07d2 | ||
|
|
8a4bf7896f | ||
|
|
208ea29643 | ||
|
|
7c52ec731a | ||
|
|
c6ee38e435 | ||
|
|
65253cc5b9 | ||
|
|
d1a1a23cbc | ||
|
|
e8bb3a5ba7 | ||
|
|
22b8f82770 |
14
CHANGELOG
14
CHANGELOG
@@ -1,3 +1,17 @@
|
|||||||
|
KeePassDX(2.9.17)
|
||||||
|
* Import / Export app properties #839
|
||||||
|
* Force twofish padding compatibility #955
|
||||||
|
* Better timeout preference #579
|
||||||
|
|
||||||
|
KeePassDX(2.9.16)
|
||||||
|
* Fix small bugs #948
|
||||||
|
|
||||||
|
KeePassDX(2.9.15)
|
||||||
|
* Fix themes #935 #926
|
||||||
|
* Decrease default clipboard time #934
|
||||||
|
* Better opening performance #929 #933
|
||||||
|
* Fix memory usage setting #941
|
||||||
|
|
||||||
KeePassDX(2.9.14)
|
KeePassDX(2.9.14)
|
||||||
* Add custom icons #96
|
* Add custom icons #96
|
||||||
* Dark Themes #532 #714
|
* Dark Themes #532 #714
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 14
|
minSdkVersion 15
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode = 65
|
versionCode = 71
|
||||||
versionName = "2.9.14"
|
versionName = "2.9.17"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
@@ -29,12 +29,6 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
path "src/main/jni/CMakeLists.txt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled = false
|
minifyEnabled = false
|
||||||
@@ -126,8 +120,6 @@ dependencies {
|
|||||||
kapt "androidx.room:room-compiler:$room_version"
|
kapt "androidx.room:room-compiler:$room_version"
|
||||||
// Autofill
|
// Autofill
|
||||||
implementation "androidx.autofill:autofill:1.1.0"
|
implementation "androidx.autofill:autofill:1.1.0"
|
||||||
// Crypto
|
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01'
|
|
||||||
// Time
|
// Time
|
||||||
implementation 'joda-time:joda-time:2.10.6'
|
implementation 'joda-time:joda-time:2.10.6'
|
||||||
// Color
|
// Color
|
||||||
@@ -137,6 +129,8 @@ dependencies {
|
|||||||
// Apache Commons
|
// Apache Commons
|
||||||
implementation 'commons-io:commons-io:2.8.0'
|
implementation 'commons-io:commons-io:2.8.0'
|
||||||
implementation 'commons-codec:commons-codec:1.15'
|
implementation 'commons-codec:commons-codec:1.15'
|
||||||
|
// Encrypt lib
|
||||||
|
implementation project(path: ':crypto')
|
||||||
// Icon pack
|
// Icon pack
|
||||||
implementation project(path: ':icon-pack-classic')
|
implementation project(path: ':icon-pack-classic')
|
||||||
implementation project(path: ':icon-pack-material')
|
implementation project(path: ':icon-pack-material')
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX 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.
|
|
||||||
*
|
|
||||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.tests.crypto
|
|
||||||
|
|
||||||
import org.junit.Assert.assertArrayEquals
|
|
||||||
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.Random
|
|
||||||
|
|
||||||
import junit.framework.TestCase
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.finalkey.AndroidAESKeyTransformer
|
|
||||||
import com.kunzisoft.keepass.crypto.finalkey.NativeAESKeyTransformer
|
|
||||||
|
|
||||||
class AESKeyTest : TestCase() {
|
|
||||||
private lateinit var mRand: Random
|
|
||||||
|
|
||||||
@Throws(Exception::class)
|
|
||||||
override fun setUp() {
|
|
||||||
super.setUp()
|
|
||||||
|
|
||||||
mRand = Random()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun testAES() {
|
|
||||||
// Test both an old and an even number to test my flip variable
|
|
||||||
testAESFinalKey(5)
|
|
||||||
testAESFinalKey(6)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun testAESFinalKey(rounds: Long) {
|
|
||||||
val seed = ByteArray(32)
|
|
||||||
val key = ByteArray(32)
|
|
||||||
val nativeKey: ByteArray?
|
|
||||||
val androidKey: ByteArray?
|
|
||||||
|
|
||||||
mRand.nextBytes(seed)
|
|
||||||
mRand.nextBytes(key)
|
|
||||||
|
|
||||||
val androidAESKey = AndroidAESKeyTransformer()
|
|
||||||
androidKey = androidAESKey.transformMasterKey(seed, key, rounds)
|
|
||||||
|
|
||||||
val nativeAESKey = NativeAESKeyTransformer()
|
|
||||||
nativeKey = nativeAESKey.transformMasterKey(seed, key, rounds)
|
|
||||||
|
|
||||||
assertArrayEquals("Does not match", androidKey, nativeKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX 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.
|
|
||||||
*
|
|
||||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.tests.crypto
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
|
||||||
|
|
||||||
import junit.framework.TestCase
|
|
||||||
|
|
||||||
import java.security.InvalidAlgorithmParameterException
|
|
||||||
import java.security.InvalidKeyException
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.util.Random
|
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.IllegalBlockSizeException
|
|
||||||
import javax.crypto.NoSuchPaddingException
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
import org.junit.Assert.assertArrayEquals
|
|
||||||
|
|
||||||
class AESTest : TestCase() {
|
|
||||||
|
|
||||||
private val mRand = Random()
|
|
||||||
|
|
||||||
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, IllegalBlockSizeException::class, BadPaddingException::class, InvalidAlgorithmParameterException::class)
|
|
||||||
fun testEncrypt() {
|
|
||||||
// Test above below and at the blocksize
|
|
||||||
testFinal(15)
|
|
||||||
testFinal(16)
|
|
||||||
testFinal(17)
|
|
||||||
|
|
||||||
// Test random larger sizes
|
|
||||||
val size = mRand.nextInt(494) + 18
|
|
||||||
testFinal(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, IllegalBlockSizeException::class, BadPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
|
||||||
private fun testFinal(dataSize: Int) {
|
|
||||||
|
|
||||||
// Generate some input
|
|
||||||
val input = ByteArray(dataSize)
|
|
||||||
mRand.nextBytes(input)
|
|
||||||
|
|
||||||
// Generate key
|
|
||||||
val keyArray = ByteArray(32)
|
|
||||||
mRand.nextBytes(keyArray)
|
|
||||||
val key = SecretKeySpec(keyArray, "AES")
|
|
||||||
|
|
||||||
// Generate IV
|
|
||||||
val ivArray = ByteArray(16)
|
|
||||||
mRand.nextBytes(ivArray)
|
|
||||||
val iv = IvParameterSpec(ivArray)
|
|
||||||
|
|
||||||
val android = CipherFactory.getInstance("AES/CBC/PKCS5Padding", true)
|
|
||||||
android.init(Cipher.ENCRYPT_MODE, key, iv)
|
|
||||||
val outAndroid = android.doFinal(input, 0, dataSize)
|
|
||||||
|
|
||||||
val nat = CipherFactory.getInstance("AES/CBC/PKCS5Padding")
|
|
||||||
nat.init(Cipher.ENCRYPT_MODE, key, iv)
|
|
||||||
val outNative = nat.doFinal(input, 0, dataSize)
|
|
||||||
|
|
||||||
assertArrayEquals("Arrays differ on size: $dataSize", outAndroid, outNative)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePassDX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
@@ -19,44 +19,32 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests.crypto
|
package com.kunzisoft.keepass.tests.crypto
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.utils.readBytesLength
|
||||||
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
import org.junit.Assert.assertArrayEquals
|
import org.junit.Assert.assertArrayEquals
|
||||||
|
import org.junit.Test
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.IOException
|
import java.util.*
|
||||||
import java.security.InvalidAlgorithmParameterException
|
|
||||||
import java.security.InvalidKeyException
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.util.Random
|
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException
|
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.CipherInputStream
|
||||||
import javax.crypto.CipherOutputStream
|
import javax.crypto.CipherOutputStream
|
||||||
import javax.crypto.IllegalBlockSizeException
|
|
||||||
import javax.crypto.NoSuchPaddingException
|
|
||||||
|
|
||||||
import junit.framework.TestCase
|
class EncryptionTest {
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.AesEngine
|
|
||||||
import com.kunzisoft.keepass.stream.BetterCipherInputStream
|
|
||||||
import com.kunzisoft.keepass.stream.LittleEndianDataInputStream
|
|
||||||
|
|
||||||
class CipherTest : TestCase() {
|
|
||||||
private val rand = Random()
|
private val rand = Random()
|
||||||
|
|
||||||
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidAlgorithmParameterException::class, IllegalBlockSizeException::class, BadPaddingException::class)
|
@Test
|
||||||
fun testCipherFactory() {
|
fun testCipherFactory() {
|
||||||
val key = ByteArray(32)
|
val key = ByteArray(32)
|
||||||
|
rand.nextBytes(key)
|
||||||
|
|
||||||
val iv = ByteArray(16)
|
val iv = ByteArray(16)
|
||||||
|
rand.nextBytes(iv)
|
||||||
|
|
||||||
val plaintext = ByteArray(1024)
|
val plaintext = ByteArray(1024)
|
||||||
|
|
||||||
rand.nextBytes(key)
|
|
||||||
rand.nextBytes(iv)
|
|
||||||
rand.nextBytes(plaintext)
|
rand.nextBytes(plaintext)
|
||||||
|
|
||||||
val aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID)
|
val aes = EncryptionAlgorithm.AESRijndael.cipherEngine
|
||||||
val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv)
|
val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv)
|
||||||
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
|
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
|
||||||
|
|
||||||
@@ -66,20 +54,20 @@ class CipherTest : TestCase() {
|
|||||||
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
|
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidAlgorithmParameterException::class, IllegalBlockSizeException::class, BadPaddingException::class, IOException::class)
|
@Test
|
||||||
fun testCipherStreams() {
|
fun testCipherStreams() {
|
||||||
val MESSAGE_LENGTH = 1024
|
val length = 1024
|
||||||
|
|
||||||
val key = ByteArray(32)
|
val key = ByteArray(32)
|
||||||
val iv = ByteArray(16)
|
|
||||||
|
|
||||||
val plaintext = ByteArray(MESSAGE_LENGTH)
|
|
||||||
|
|
||||||
rand.nextBytes(key)
|
rand.nextBytes(key)
|
||||||
|
|
||||||
|
val iv = ByteArray(16)
|
||||||
rand.nextBytes(iv)
|
rand.nextBytes(iv)
|
||||||
|
|
||||||
|
val plaintext = ByteArray(length)
|
||||||
rand.nextBytes(plaintext)
|
rand.nextBytes(plaintext)
|
||||||
|
|
||||||
val aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID)
|
val aes = EncryptionAlgorithm.AESRijndael.cipherEngine
|
||||||
val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv)
|
val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv)
|
||||||
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
|
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
|
||||||
|
|
||||||
@@ -91,10 +79,9 @@ class CipherTest : TestCase() {
|
|||||||
val secrettext = bos.toByteArray()
|
val secrettext = bos.toByteArray()
|
||||||
|
|
||||||
val bis = ByteArrayInputStream(secrettext)
|
val bis = ByteArrayInputStream(secrettext)
|
||||||
val cis = BetterCipherInputStream(bis, decrypt)
|
val cis = CipherInputStream(bis, decrypt)
|
||||||
val lis = LittleEndianDataInputStream(cis)
|
|
||||||
|
|
||||||
val decrypttext = lis.readBytes(MESSAGE_LENGTH)
|
val decrypttext = cis.readBytesLength(length)
|
||||||
|
|
||||||
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
|
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,9 @@ package com.kunzisoft.keepass.tests.stream
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.utils.readAllBytes
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryByte
|
import com.kunzisoft.keepass.database.element.binary.BinaryCache
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryFile
|
import com.kunzisoft.keepass.database.element.binary.BinaryFile
|
||||||
import com.kunzisoft.keepass.stream.readAllBytes
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import junit.framework.TestCase.assertEquals
|
import junit.framework.TestCase.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -25,11 +24,11 @@ class BinaryDataTest {
|
|||||||
private val fileB = File(cacheDirectory, TEST_FILE_CACHE_B)
|
private val fileB = File(cacheDirectory, TEST_FILE_CACHE_B)
|
||||||
private val fileC = File(cacheDirectory, TEST_FILE_CACHE_C)
|
private val fileC = File(cacheDirectory, TEST_FILE_CACHE_C)
|
||||||
|
|
||||||
private val loadedKey = Database.LoadedKey.generateNewCipherKey()
|
private val binaryCache = BinaryCache()
|
||||||
|
|
||||||
private fun saveBinary(asset: String, binaryData: BinaryFile) {
|
private fun saveBinary(asset: String, binaryData: BinaryFile) {
|
||||||
context.assets.open(asset).use { assetInputStream ->
|
context.assets.open(asset).use { assetInputStream ->
|
||||||
binaryData.getOutputDataStream(loadedKey).use { binaryOutputStream ->
|
binaryData.getOutputDataStream(binaryCache).use { binaryOutputStream ->
|
||||||
assetInputStream.readAllBytes(DEFAULT_BUFFER_SIZE) { buffer ->
|
assetInputStream.readAllBytes(DEFAULT_BUFFER_SIZE) { buffer ->
|
||||||
binaryOutputStream.write(buffer)
|
binaryOutputStream.write(buffer)
|
||||||
}
|
}
|
||||||
@@ -65,11 +64,11 @@ class BinaryDataTest {
|
|||||||
saveBinary(TEST_TEXT_ASSET, binaryA)
|
saveBinary(TEST_TEXT_ASSET, binaryA)
|
||||||
saveBinary(TEST_TEXT_ASSET, binaryB)
|
saveBinary(TEST_TEXT_ASSET, binaryB)
|
||||||
saveBinary(TEST_TEXT_ASSET, binaryC)
|
saveBinary(TEST_TEXT_ASSET, binaryC)
|
||||||
binaryA.compress(loadedKey)
|
binaryA.compress(binaryCache)
|
||||||
binaryB.compress(loadedKey)
|
binaryB.compress(binaryCache)
|
||||||
assertEquals("Compress text length failed.", binaryA.getSize(), binaryB.getSize())
|
assertEquals("Compress text length failed.", binaryA.getSize(), binaryB.getSize())
|
||||||
assertEquals("Compress text MD5 failed.", binaryA.binaryHash(), binaryB.binaryHash())
|
assertEquals("Compress text MD5 failed.", binaryA.binaryHash(), binaryB.binaryHash())
|
||||||
binaryB.decompress(loadedKey)
|
binaryB.decompress(binaryCache)
|
||||||
assertEquals("Decompress text length failed.", binaryB.getSize(), binaryC.getSize())
|
assertEquals("Decompress text length failed.", binaryB.getSize(), binaryC.getSize())
|
||||||
assertEquals("Decompress text MD5 failed.", binaryB.binaryHash(), binaryC.binaryHash())
|
assertEquals("Decompress text MD5 failed.", binaryB.binaryHash(), binaryC.binaryHash())
|
||||||
}
|
}
|
||||||
@@ -82,29 +81,46 @@ class BinaryDataTest {
|
|||||||
saveBinary(TEST_IMAGE_ASSET, binaryA)
|
saveBinary(TEST_IMAGE_ASSET, binaryA)
|
||||||
saveBinary(TEST_IMAGE_ASSET, binaryB)
|
saveBinary(TEST_IMAGE_ASSET, binaryB)
|
||||||
saveBinary(TEST_IMAGE_ASSET, binaryC)
|
saveBinary(TEST_IMAGE_ASSET, binaryC)
|
||||||
binaryA.compress(loadedKey)
|
binaryA.compress(binaryCache)
|
||||||
binaryB.compress(loadedKey)
|
binaryB.compress(binaryCache)
|
||||||
assertEquals("Compress image length failed.", binaryA.getSize(), binaryA.getSize())
|
assertEquals("Compress image length failed.", binaryA.getSize(), binaryA.getSize())
|
||||||
assertEquals("Compress image failed.", binaryA.binaryHash(), binaryA.binaryHash())
|
assertEquals("Compress image failed.", binaryA.binaryHash(), binaryA.binaryHash())
|
||||||
binaryB = BinaryFile(fileB, true)
|
binaryB = BinaryFile(fileB, true)
|
||||||
binaryB.decompress(loadedKey)
|
binaryB.decompress(binaryCache)
|
||||||
assertEquals("Decompress image length failed.", binaryB.getSize(), binaryC.getSize())
|
assertEquals("Decompress image length failed.", binaryB.getSize(), binaryC.getSize())
|
||||||
assertEquals("Decompress image failed.", binaryB.binaryHash(), binaryC.binaryHash())
|
assertEquals("Decompress image failed.", binaryB.binaryHash(), binaryC.binaryHash())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testCompressBytes() {
|
fun testCompressBytes() {
|
||||||
|
// Test random byte array
|
||||||
val byteArray = ByteArray(50)
|
val byteArray = ByteArray(50)
|
||||||
Random.nextBytes(byteArray)
|
Random.nextBytes(byteArray)
|
||||||
val binaryA = BinaryByte(byteArray)
|
testCompressBytes(byteArray)
|
||||||
val binaryB = BinaryByte(byteArray)
|
|
||||||
val binaryC = BinaryByte(byteArray)
|
// Test empty byte array
|
||||||
binaryA.compress(loadedKey)
|
testCompressBytes(ByteArray(0))
|
||||||
binaryB.compress(loadedKey)
|
}
|
||||||
|
|
||||||
|
private fun testCompressBytes(byteArray: ByteArray) {
|
||||||
|
val binaryA = binaryCache.getBinaryData("0", true)
|
||||||
|
binaryA.getOutputDataStream(binaryCache).use { outputStream ->
|
||||||
|
outputStream.write(byteArray)
|
||||||
|
}
|
||||||
|
val binaryB = binaryCache.getBinaryData("1", true)
|
||||||
|
binaryB.getOutputDataStream(binaryCache).use { outputStream ->
|
||||||
|
outputStream.write(byteArray)
|
||||||
|
}
|
||||||
|
val binaryC = binaryCache.getBinaryData("2", true)
|
||||||
|
binaryC.getOutputDataStream(binaryCache).use { outputStream ->
|
||||||
|
outputStream.write(byteArray)
|
||||||
|
}
|
||||||
|
binaryA.compress(binaryCache)
|
||||||
|
binaryB.compress(binaryCache)
|
||||||
assertEquals("Compress bytes decompressed failed.", binaryA.isCompressed, true)
|
assertEquals("Compress bytes decompressed failed.", binaryA.isCompressed, true)
|
||||||
assertEquals("Compress bytes length failed.", binaryA.getSize(), binaryA.getSize())
|
assertEquals("Compress bytes length failed.", binaryA.getSize(), binaryA.getSize())
|
||||||
assertEquals("Compress bytes failed.", binaryA.binaryHash(), binaryA.binaryHash())
|
assertEquals("Compress bytes failed.", binaryA.binaryHash(), binaryA.binaryHash())
|
||||||
binaryB.decompress(loadedKey)
|
binaryB.decompress(binaryCache)
|
||||||
assertEquals("Decompress bytes decompressed failed.", binaryB.isCompressed, false)
|
assertEquals("Decompress bytes decompressed failed.", binaryB.isCompressed, false)
|
||||||
assertEquals("Decompress bytes length failed.", binaryB.getSize(), binaryC.getSize())
|
assertEquals("Decompress bytes length failed.", binaryB.getSize(), binaryC.getSize())
|
||||||
assertEquals("Decompress bytes failed.", binaryB.binaryHash(), binaryC.binaryHash())
|
assertEquals("Decompress bytes failed.", binaryB.binaryHash(), binaryC.binaryHash())
|
||||||
@@ -115,7 +131,7 @@ class BinaryDataTest {
|
|||||||
val binaryA = BinaryFile(fileA)
|
val binaryA = BinaryFile(fileA)
|
||||||
saveBinary(TEST_TEXT_ASSET, binaryA)
|
saveBinary(TEST_TEXT_ASSET, binaryA)
|
||||||
assert(streamAreEquals(context.assets.open(TEST_TEXT_ASSET),
|
assert(streamAreEquals(context.assets.open(TEST_TEXT_ASSET),
|
||||||
binaryA.getInputDataStream(loadedKey)))
|
binaryA.getInputDataStream(binaryCache)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -123,7 +139,7 @@ class BinaryDataTest {
|
|||||||
val binaryA = BinaryFile(fileA)
|
val binaryA = BinaryFile(fileA)
|
||||||
saveBinary(TEST_IMAGE_ASSET, binaryA)
|
saveBinary(TEST_IMAGE_ASSET, binaryA)
|
||||||
assert(streamAreEquals(context.assets.open(TEST_IMAGE_ASSET),
|
assert(streamAreEquals(context.assets.open(TEST_IMAGE_ASSET),
|
||||||
binaryA.getInputDataStream(loadedKey)))
|
binaryA.getInputDataStream(binaryCache)))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun streamAreEquals(inputStreamA: InputStream,
|
private fun streamAreEquals(inputStreamA: InputStream,
|
||||||
|
|||||||
@@ -20,9 +20,7 @@
|
|||||||
package com.kunzisoft.keepass.tests.utils
|
package com.kunzisoft.keepass.tests.utils
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.stream.*
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
|
||||||
import com.kunzisoft.keepass.utils.UnsignedLong
|
|
||||||
import junit.framework.TestCase
|
import junit.framework.TestCase
|
||||||
import org.junit.Assert.assertArrayEquals
|
import org.junit.Assert.assertArrayEquals
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
@@ -54,7 +52,7 @@ class ValuesTest : TestCase() {
|
|||||||
val orig = ByteArray(8)
|
val orig = ByteArray(8)
|
||||||
setArray(orig, value, 8)
|
setArray(orig, value, 8)
|
||||||
|
|
||||||
assertArrayEquals(orig, longTo8Bytes(bytes64ToLong(orig)))
|
assertArrayEquals(orig, uLongTo8Bytes(bytes64ToULong(orig)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteIntZero() {
|
fun testReadWriteIntZero() {
|
||||||
@@ -133,7 +131,7 @@ class ValuesTest : TestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun testReadWriteByte(value: Byte) {
|
private fun testReadWriteByte(value: Byte) {
|
||||||
val dest: Byte = UnsignedInt(UnsignedInt.fromKotlinByte(value)).toKotlinByte()
|
val dest: Byte = UnsignedInt(value.toInt() and 0xFF).toKotlinByte()
|
||||||
assert(value == dest)
|
assert(value == dest)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,13 +142,11 @@ class ValuesTest : TestCase() {
|
|||||||
expected.set(2008, 1, 2, 3, 4, 5)
|
expected.set(2008, 1, 2, 3, 4, 5)
|
||||||
|
|
||||||
val actual = Calendar.getInstance()
|
val actual = Calendar.getInstance()
|
||||||
dateTo5Bytes(expected.time, cal)?.let { buf ->
|
actual.time = DateInstant(bytes5ToDate(dateTo5Bytes(expected.time, cal), cal)).date
|
||||||
actual.time = bytes5ToDate(buf, cal).date
|
|
||||||
}
|
|
||||||
|
|
||||||
val jDate = DateInstant(System.currentTimeMillis())
|
val jDate = DateInstant(System.currentTimeMillis())
|
||||||
val intermediate = DateInstant(jDate)
|
val intermediate = DateInstant(jDate)
|
||||||
val cDate = bytes5ToDate(dateTo5Bytes(intermediate.date)!!)
|
val cDate = DateInstant(bytes5ToDate(dateTo5Bytes(intermediate.date)))
|
||||||
|
|
||||||
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR))
|
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR))
|
||||||
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH))
|
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH))
|
||||||
@@ -183,12 +179,10 @@ class ValuesTest : TestCase() {
|
|||||||
ulongBytes[i] = -1
|
ulongBytes[i] = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
val bos = ByteArrayOutputStream()
|
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||||
val leos = LittleEndianDataOutputStream(bos)
|
byteArrayOutputStream.write(UnsignedLong.MAX_BYTES)
|
||||||
leos.writeLong(UnsignedLong.MAX_VALUE)
|
byteArrayOutputStream.close()
|
||||||
leos.close()
|
val uLongMax = byteArrayOutputStream.toByteArray()
|
||||||
|
|
||||||
val uLongMax = bos.toByteArray()
|
|
||||||
|
|
||||||
assertArrayEquals(ulongBytes, uLongMax)
|
assertArrayEquals(ulongBytes, uLongMax)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|||||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||||
@@ -93,6 +94,8 @@ class EntryActivity : LockingActivity() {
|
|||||||
private var clipboardHelper: ClipboardHelper? = null
|
private var clipboardHelper: ClipboardHelper? = null
|
||||||
private var mFirstLaunchOfActivity: Boolean = false
|
private var mFirstLaunchOfActivity: Boolean = false
|
||||||
|
|
||||||
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var iconColor: Int = 0
|
private var iconColor: Int = 0
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -125,7 +128,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
historyView = findViewById(R.id.history_container)
|
historyView = findViewById(R.id.history_container)
|
||||||
entryContentsView = findViewById(R.id.entry_contents)
|
entryContentsView = findViewById(R.id.entry_contents)
|
||||||
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
||||||
entryContentsView?.setAttachmentCipherKey(mDatabase?.loadedCipherKey)
|
entryContentsView?.setAttachmentCipherKey(mDatabase)
|
||||||
entryProgress = findViewById(R.id.entry_progress)
|
entryProgress = findViewById(R.id.entry_progress)
|
||||||
lockView = findViewById(R.id.lock_button)
|
lockView = findViewById(R.id.lock_button)
|
||||||
|
|
||||||
@@ -140,6 +143,9 @@ class EntryActivity : LockingActivity() {
|
|||||||
clipboardHelper = ClipboardHelper(this)
|
clipboardHelper = ClipboardHelper(this)
|
||||||
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
|
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
|
||||||
|
|
||||||
|
// Init SAF manager
|
||||||
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
|
||||||
// Init attachment service binder manager
|
// Init attachment service binder manager
|
||||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
|
|
||||||
@@ -344,7 +350,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
|
|
||||||
// Manage attachments
|
// Manage attachments
|
||||||
entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
||||||
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
mExternalFileHelper?.createDocument(attachmentItem.name)?.let { requestCode ->
|
||||||
mAttachmentsToDownload[requestCode] = attachmentItem
|
mAttachmentsToDownload[requestCode] = attachmentItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -380,7 +386,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
||||||
if (createdFileUri != null) {
|
if (createdFileUri != null) {
|
||||||
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
|
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
|
||||||
mAttachmentFileBinderManager
|
mAttachmentFileBinderManager
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import com.kunzisoft.keepass.activities.dialogs.*
|
|||||||
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
|
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
|
||||||
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
|
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
@@ -103,7 +103,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
private var lockView: View? = null
|
private var lockView: View? = null
|
||||||
|
|
||||||
// To manage attachments
|
// To manage attachments
|
||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||||
private var mAllowMultipleAttachments: Boolean = false
|
private var mAllowMultipleAttachments: Boolean = false
|
||||||
private var mTempAttachments = ArrayList<EntryAttachmentState>()
|
private var mTempAttachments = ArrayList<EntryAttachmentState>()
|
||||||
@@ -202,7 +202,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
// Build fragment to manage entry modification
|
// Build fragment to manage entry modification
|
||||||
entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment?
|
entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment?
|
||||||
if (entryEditFragment == null) {
|
if (entryEditFragment == null) {
|
||||||
entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo, mDatabase?.loadedCipherKey)
|
entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo)
|
||||||
}
|
}
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager.beginTransaction()
|
||||||
.replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG)
|
.replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG)
|
||||||
@@ -241,7 +241,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
// To retrieve attachment
|
// To retrieve attachment
|
||||||
mSelectFileHelper = SelectFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
|
|
||||||
// Save button
|
// Save button
|
||||||
@@ -458,8 +458,8 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
/**
|
/**
|
||||||
* Add a new attachment
|
* Add a new attachment
|
||||||
*/
|
*/
|
||||||
private fun addNewAttachment(item: MenuItem) {
|
private fun addNewAttachment() {
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.onMenuItemClick(item)
|
mExternalFileHelper?.openDocument()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?) {
|
override fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?) {
|
||||||
@@ -485,7 +485,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
|
|
||||||
private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) {
|
private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) {
|
||||||
val compression = mDatabase?.compressionForNewEntry() ?: false
|
val compression = mDatabase?.compressionForNewEntry() ?: false
|
||||||
mDatabase?.buildNewBinaryAttachment(UriUtil.getBinaryDir(this), compression)?.let { binaryAttachment ->
|
mDatabase?.buildNewBinaryAttachment(compression)?.let { binaryAttachment ->
|
||||||
val entryAttachment = Attachment(fileName, binaryAttachment)
|
val entryAttachment = Attachment(fileName, binaryAttachment)
|
||||||
// Ask to replace the current attachment
|
// Ask to replace the current attachment
|
||||||
if ((mDatabase?.allowMultipleAttachments != true && entryEditFragment?.containsAttachment() == true) ||
|
if ((mDatabase?.allowMultipleAttachments != true && entryEditFragment?.containsAttachment() == true) ||
|
||||||
@@ -505,7 +505,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
entryEditFragment?.icon = icon
|
entryEditFragment?.icon = icon
|
||||||
}
|
}
|
||||||
|
|
||||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||||
uri?.let { attachmentToUploadUri ->
|
uri?.let { attachmentToUploadUri ->
|
||||||
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
|
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
|
||||||
documentFile.name?.let { fileName ->
|
documentFile.name?.let { fileName ->
|
||||||
@@ -655,7 +655,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
|
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
|
||||||
attachmentView,
|
attachmentView,
|
||||||
{
|
{
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(attachmentView)
|
mExternalFileHelper?.openDocument()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(entryEditActivityEducation)
|
performedNextEducation(entryEditActivityEducation)
|
||||||
@@ -683,7 +683,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_add_attachment -> {
|
R.id.menu_add_attachment -> {
|
||||||
addNewAttachment(item)
|
addNewAttachment()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_add_otp -> {
|
R.id.menu_add_otp -> {
|
||||||
|
|||||||
@@ -42,8 +42,9 @@ import com.google.android.material.snackbar.Snackbar
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
@@ -82,7 +83,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
|
|
||||||
private var mDatabaseFileUri: Uri? = null
|
private var mDatabaseFileUri: Uri? = null
|
||||||
|
|
||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||||
|
|
||||||
@@ -103,14 +104,9 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
||||||
|
|
||||||
// Open database button
|
// Open database button
|
||||||
mSelectFileHelper = SelectFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
||||||
openDatabaseButtonView?.apply {
|
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
|
||||||
setOnClickListener(it)
|
|
||||||
setOnLongClickListener(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// History list
|
// History list
|
||||||
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
||||||
@@ -162,29 +158,31 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
|
|
||||||
// Observe list of databases
|
// Observe list of databases
|
||||||
databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles ->
|
databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles ->
|
||||||
when (databaseFiles.databaseFileAction) {
|
try {
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
|
when (databaseFiles.databaseFileAction) {
|
||||||
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
|
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
|
||||||
}
|
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
|
|
||||||
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
|
|
||||||
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
|
|
||||||
}
|
}
|
||||||
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
|
||||||
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity))
|
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
|
||||||
}
|
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
|
}
|
||||||
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
|
}
|
||||||
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
|
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
|
||||||
}
|
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
|
||||||
}
|
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
|
}
|
||||||
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
|
}
|
||||||
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
|
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
|
||||||
|
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
|
||||||
|
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
databaseFilesViewModel.consumeAction()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to observe database action", e)
|
||||||
}
|
}
|
||||||
databaseFilesViewModel.consumeAction()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observe default database
|
// Observe default database
|
||||||
@@ -202,6 +200,8 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential()
|
val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential()
|
||||||
databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri)
|
databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri)
|
||||||
}
|
}
|
||||||
|
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
||||||
|
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity))
|
||||||
}
|
}
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
val database = Database.getInstance()
|
val database = Database.getInstance()
|
||||||
@@ -230,7 +230,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
* Create a new file by calling the content provider
|
* Create a new file by calling the content provider
|
||||||
*/
|
*/
|
||||||
private fun createNewFile() {
|
private fun createNewFile() {
|
||||||
createDocument(this, getString(R.string.database_file_name_default) +
|
mExternalFileHelper?.createDocument( getString(R.string.database_file_name_default) +
|
||||||
getString(R.string.database_file_extension_default), "application/x-keepass")
|
getString(R.string.database_file_extension_default), "application/x-keepass")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +282,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
// Show open and create button or special mode
|
// Show open and create button or special mode
|
||||||
when (mSpecialMode) {
|
when (mSpecialMode) {
|
||||||
SpecialMode.DEFAULT -> {
|
SpecialMode.DEFAULT -> {
|
||||||
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
if (ExternalFileHelper.allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
||||||
// There is an activity which can handle this intent.
|
// There is an activity which can handle this intent.
|
||||||
createDatabaseButtonView?.visibility = View.VISIBLE
|
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||||
} else{
|
} else{
|
||||||
@@ -355,14 +355,14 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
launchPasswordActivityWithPath(uri)
|
launchPasswordActivityWithPath(uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the created URI from the file manager
|
// Retrieve the created URI from the file manager
|
||||||
onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
|
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
|
||||||
mDatabaseFileUri = databaseFileCreatedUri
|
mDatabaseFileUri = databaseFileCreatedUri
|
||||||
if (mDatabaseFileUri != null) {
|
if (mDatabaseFileUri != null) {
|
||||||
AssignMasterKeyDialogFragment.getInstance(true)
|
AssignMasterKeyDialogFragment.getInstance(true)
|
||||||
@@ -390,9 +390,10 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
||||||
// If no recent files
|
// If no recent files
|
||||||
val createDatabaseEducationPerformed =
|
val createDatabaseEducationPerformed =
|
||||||
createDatabaseButtonView != null && createDatabaseButtonView!!.visibility == View.VISIBLE
|
createDatabaseButtonView != null
|
||||||
|
&& createDatabaseButtonView!!.visibility == View.VISIBLE
|
||||||
&& mAdapterDatabaseHistory != null
|
&& mAdapterDatabaseHistory != null
|
||||||
&& mAdapterDatabaseHistory!!.itemCount > 0
|
&& mAdapterDatabaseHistory!!.itemCount == 0
|
||||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
||||||
createDatabaseButtonView!!,
|
createDatabaseButtonView!!,
|
||||||
{
|
{
|
||||||
@@ -407,9 +408,9 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
openDatabaseButtonView != null
|
openDatabaseButtonView != null
|
||||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
||||||
openDatabaseButtonView!!,
|
openDatabaseButtonView!!,
|
||||||
{tapTargetView ->
|
{ tapTargetView ->
|
||||||
tapTargetView?.let {
|
tapTargetView?.let {
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
mExternalFileHelper?.openDocument()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ import androidx.fragment.app.commit
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
|
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
@@ -66,7 +67,7 @@ class IconPickerActivity : LockingActivity() {
|
|||||||
|
|
||||||
private var mDatabase: Database? = null
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -84,15 +85,11 @@ class IconPickerActivity : LockingActivity() {
|
|||||||
|
|
||||||
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
|
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
|
||||||
|
|
||||||
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
|
||||||
uploadButton = findViewById(R.id.icon_picker_upload)
|
uploadButton = findViewById(R.id.icon_picker_upload)
|
||||||
if (mDatabase?.allowCustomIcons == true) {
|
if (mDatabase?.allowCustomIcons == true) {
|
||||||
uploadButton.setOnClickListener {
|
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
|
||||||
}
|
|
||||||
uploadButton.setOnLongClickListener {
|
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.onLongClick(it)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
uploadButton.visibility = View.GONE
|
uploadButton.visibility = View.GONE
|
||||||
}
|
}
|
||||||
@@ -124,8 +121,6 @@ class IconPickerActivity : LockingActivity() {
|
|||||||
// Focus view to reinitialize timeout
|
// Focus view to reinitialize timeout
|
||||||
findViewById<ViewGroup>(R.id.icon_picker_container)?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
findViewById<ViewGroup>(R.id.icon_picker_container)?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
||||||
|
|
||||||
mSelectFileHelper = SelectFileHelper(this)
|
|
||||||
|
|
||||||
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
|
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
|
||||||
mIconImage.standard = iconStandard
|
mIconImage.standard = iconStandard
|
||||||
// Remove the custom icon if a standard one is selected
|
// Remove the custom icon if a standard one is selected
|
||||||
@@ -229,21 +224,26 @@ class IconPickerActivity : LockingActivity() {
|
|||||||
if (documentFile.length() > MAX_ICON_SIZE) {
|
if (documentFile.length() > MAX_ICON_SIZE) {
|
||||||
iconCustomState.errorStringId = R.string.error_file_to_big
|
iconCustomState.errorStringId = R.string.error_file_to_big
|
||||||
} else {
|
} else {
|
||||||
mDatabase?.buildNewCustomIcon(UriUtil.getBinaryDir(this@IconPickerActivity)) { customIcon, binary ->
|
mDatabase?.buildNewCustomIcon { customIcon, binary ->
|
||||||
if (customIcon != null) {
|
if (customIcon != null) {
|
||||||
iconCustomState.iconCustom = customIcon
|
iconCustomState.iconCustom = customIcon
|
||||||
BinaryDatabaseManager.resizeBitmapAndStoreDataInBinaryFile(contentResolver,
|
mDatabase?.let { database ->
|
||||||
iconToUploadUri, binary)
|
BinaryDatabaseManager.resizeBitmapAndStoreDataInBinaryFile(
|
||||||
when {
|
contentResolver,
|
||||||
binary == null -> {
|
database,
|
||||||
}
|
iconToUploadUri,
|
||||||
binary.getSize() <= 0 -> {
|
binary)
|
||||||
}
|
when {
|
||||||
mDatabase?.isCustomIconBinaryDuplicate(binary) == true -> {
|
binary == null -> {
|
||||||
iconCustomState.errorStringId = R.string.error_duplicate_file
|
}
|
||||||
}
|
binary.getSize() <= 0 -> {
|
||||||
else -> {
|
}
|
||||||
iconCustomState.error = false
|
database.isCustomIconBinaryDuplicate(binary) -> {
|
||||||
|
iconCustomState.errorStringId = R.string.error_duplicate_file
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
iconCustomState.error = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (iconCustomState.error) {
|
if (iconCustomState.error) {
|
||||||
@@ -276,7 +276,7 @@ class IconPickerActivity : LockingActivity() {
|
|||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||||
addCustomIcon(uri)
|
addCustomIcon(uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ import kotlin.math.max
|
|||||||
|
|
||||||
class ImageViewerActivity : LockingActivity() {
|
class ImageViewerActivity : LockingActivity() {
|
||||||
|
|
||||||
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -59,23 +61,29 @@ class ImageViewerActivity : LockingActivity() {
|
|||||||
resources.displayMetrics.heightPixels * 2
|
resources.displayMetrics.heightPixels * 2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mDatabase = Database.getInstance()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
progressView.visibility = View.VISIBLE
|
progressView.visibility = View.VISIBLE
|
||||||
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
|
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
|
||||||
|
|
||||||
supportActionBar?.title = attachment.name
|
supportActionBar?.title = attachment.name
|
||||||
supportActionBar?.subtitle = Formatter.formatFileSize(this, attachment.binaryData.getSize())
|
|
||||||
|
|
||||||
BinaryDatabaseManager.loadBitmap(
|
val size = attachment.binaryData.getSize()
|
||||||
attachment.binaryData,
|
supportActionBar?.subtitle = Formatter.formatFileSize(this, size)
|
||||||
Database.getInstance().loadedCipherKey,
|
|
||||||
mImagePreviewMaxWidth
|
mDatabase?.let { database ->
|
||||||
) { bitmapLoaded ->
|
BinaryDatabaseManager.loadBitmap(
|
||||||
if (bitmapLoaded == null) {
|
database,
|
||||||
finish()
|
attachment.binaryData,
|
||||||
} else {
|
mImagePreviewMaxWidth
|
||||||
progressView.visibility = View.GONE
|
) { bitmapLoaded ->
|
||||||
imageView.setImageBitmap(bitmapLoaded)
|
if (bitmapLoaded == null) {
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
progressView.visibility = View.GONE
|
||||||
|
imageView.setImageBitmap(bitmapLoaded)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} ?: finish()
|
} ?: finish()
|
||||||
|
|||||||
@@ -42,10 +42,7 @@ import androidx.fragment.app.commit
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.*
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
@@ -95,7 +92,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
private var mDatabaseKeyFileUri: Uri? = null
|
private var mDatabaseKeyFileUri: Uri? = null
|
||||||
|
|
||||||
private var mRememberKeyFile: Boolean = false
|
private var mRememberKeyFile: Boolean = false
|
||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mPermissionAsked = false
|
private var mPermissionAsked = false
|
||||||
private var readOnly: Boolean = false
|
private var readOnly: Boolean = false
|
||||||
@@ -138,13 +135,8 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
|
|
||||||
mSelectFileHelper = SelectFileHelper(this@PasswordActivity)
|
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity)
|
||||||
keyFileSelectionView?.apply {
|
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
|
||||||
setOnClickListener(it)
|
|
||||||
setOnLongClickListener(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
||||||
passwordView?.addTextChangedListener(object : TextWatcher {
|
passwordView?.addTextChangedListener(object : TextWatcher {
|
||||||
@@ -702,8 +694,8 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
}
|
}
|
||||||
|
|
||||||
var keyFileResult = false
|
var keyFileResult = false
|
||||||
mSelectFileHelper?.let {
|
mExternalFileHelper?.let {
|
||||||
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
keyFileResult = it.onOpenDocumentResult(requestCode, resultCode, data
|
||||||
) { uri ->
|
) { uri ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
mDatabaseKeyFileUri = uri
|
mDatabaseKeyFileUri = uri
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ import android.text.SpannableStringBuilder
|
|||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
@@ -60,7 +60,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
private var mListener: AssignPasswordDialogListener? = null
|
private var mListener: AssignPasswordDialogListener? = null
|
||||||
|
|
||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
||||||
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
||||||
@@ -133,11 +133,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||||
|
|
||||||
mSelectFileHelper = SelectFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
keyFileSelectionView?.apply {
|
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
setOnClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
|
||||||
setOnLongClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
val dialog = builder.create()
|
val dialog = builder.create()
|
||||||
|
|
||||||
@@ -289,7 +286,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||||
uri?.let { pathUri ->
|
uri?.let { pathUri ->
|
||||||
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||||
keyFileSelectionView?.error = null
|
keyFileSelectionView?.error = null
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
|
|
||||||
// Elements to modify the current entry
|
// Elements to modify the current entry
|
||||||
private var mEntryInfo = EntryInfo()
|
private var mEntryInfo = EntryInfo()
|
||||||
private var mBinaryCipherKey: Database.LoadedKey? = null
|
|
||||||
private var mLastFocusedEditField: FocusedEditField? = null
|
private var mLastFocusedEditField: FocusedEditField? = null
|
||||||
private var mExtraViewToRequestFocus: EditText? = null
|
private var mExtraViewToRequestFocus: EditText? = null
|
||||||
|
|
||||||
@@ -122,7 +121,9 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
|
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
|
||||||
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
|
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
|
||||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
|
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
|
||||||
attachmentsAdapter.binaryCipherKey = arguments?.getSerializable(KEY_BINARY_CIPHER_KEY) as? Database.LoadedKey?
|
// TODO retrieve current database with its unique key
|
||||||
|
attachmentsAdapter.database = Database.getInstance()
|
||||||
|
//attachmentsAdapter.database = arguments?.getInt(KEY_DATABASE)
|
||||||
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
|
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
|
||||||
if (previousSize > 0 && newSize == 0) {
|
if (previousSize > 0 && newSize == 0) {
|
||||||
attachmentsContainerView.collapse(true)
|
attachmentsContainerView.collapse(true)
|
||||||
@@ -502,7 +503,6 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
populateEntryWithViews()
|
populateEntryWithViews()
|
||||||
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
|
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
|
||||||
outState.putSerializable(KEY_BINARY_CIPHER_KEY, mBinaryCipherKey)
|
|
||||||
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
|
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
|
||||||
|
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
@@ -510,15 +510,16 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
|
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
|
||||||
const val KEY_BINARY_CIPHER_KEY = "KEY_BINARY_CIPHER_KEY"
|
const val KEY_DATABASE = "KEY_DATABASE"
|
||||||
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
|
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
|
||||||
|
|
||||||
fun getInstance(entryInfo: EntryInfo?,
|
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment {
|
||||||
loadedKey: Database.LoadedKey?): EntryEditFragment {
|
//database: Database?): EntryEditFragment {
|
||||||
return EntryEditFragment().apply {
|
return EntryEditFragment().apply {
|
||||||
arguments = Bundle().apply {
|
arguments = Bundle().apply {
|
||||||
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
|
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
|
||||||
putSerializable(KEY_BINARY_CIPHER_KEY, loadedKey)
|
// TODO Unique database key database.key
|
||||||
|
putInt(KEY_DATABASE, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX 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.
|
||||||
|
*
|
||||||
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.helpers
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
|
class ExternalFileHelper {
|
||||||
|
|
||||||
|
private var activity: FragmentActivity? = null
|
||||||
|
private var fragment: Fragment? = null
|
||||||
|
|
||||||
|
constructor(context: FragmentActivity) {
|
||||||
|
this.activity = context
|
||||||
|
this.fragment = null
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Fragment) {
|
||||||
|
this.activity = context.activity
|
||||||
|
this.fragment = context
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openDocument(getContent: Boolean = false,
|
||||||
|
typeString: String = "*/*") {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
try {
|
||||||
|
if (getContent) {
|
||||||
|
openActivityWithActionGetContent(typeString)
|
||||||
|
} else {
|
||||||
|
openActivityWithActionOpenDocument(typeString)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to open document", e)
|
||||||
|
showFileManagerDialogFragment()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showFileManagerDialogFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||||
|
private fun openActivityWithActionOpenDocument(typeString: String) {
|
||||||
|
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = typeString
|
||||||
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
if (fragment != null)
|
||||||
|
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||||
|
else
|
||||||
|
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||||
|
private fun openActivityWithActionGetContent(typeString: String) {
|
||||||
|
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = typeString
|
||||||
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
if (fragment != null)
|
||||||
|
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||||
|
else
|
||||||
|
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To use in onActivityResultCallback in Fragment or Activity
|
||||||
|
* @param onFileSelected Callback retrieve from data
|
||||||
|
* @return true if requestCode was captured, false elsewhere
|
||||||
|
*/
|
||||||
|
fun onOpenDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
|
||||||
|
onFileSelected: ((uri: Uri?) -> Unit)?): Boolean {
|
||||||
|
|
||||||
|
when (requestCode) {
|
||||||
|
FILE_BROWSE -> {
|
||||||
|
if (resultCode == RESULT_OK) {
|
||||||
|
val filename = data?.dataString
|
||||||
|
var keyUri: Uri? = null
|
||||||
|
if (filename != null) {
|
||||||
|
keyUri = UriUtil.parse(filename)
|
||||||
|
}
|
||||||
|
onFileSelected?.invoke(keyUri)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
GET_CONTENT, OPEN_DOC -> {
|
||||||
|
if (resultCode == RESULT_OK) {
|
||||||
|
if (data != null) {
|
||||||
|
val uri = data.data
|
||||||
|
if (uri != null) {
|
||||||
|
try {
|
||||||
|
// try to persist read and write permissions
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
activity?.contentResolver?.apply {
|
||||||
|
takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// nop
|
||||||
|
}
|
||||||
|
onFileSelected?.invoke(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show Browser dialog to select file picker app
|
||||||
|
*/
|
||||||
|
private fun showFileManagerDialogFragment() {
|
||||||
|
try {
|
||||||
|
if (fragment != null) {
|
||||||
|
fragment?.parentFragmentManager
|
||||||
|
} else {
|
||||||
|
activity?.supportFragmentManager
|
||||||
|
}?.let { fragmentManager ->
|
||||||
|
FileManagerDialogFragment().show(fragmentManager, "browserDialog")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Can't open BrowserDialog", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createDocument(titleString: String,
|
||||||
|
typeString: String = "application/octet-stream"): Int? {
|
||||||
|
val idCode = getUnusedCreateFileRequestCode()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
try {
|
||||||
|
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = typeString
|
||||||
|
putExtra(Intent.EXTRA_TITLE, titleString)
|
||||||
|
}
|
||||||
|
if (fragment != null)
|
||||||
|
fragment?.startActivityForResult(intent, idCode)
|
||||||
|
else
|
||||||
|
activity?.startActivityForResult(intent, idCode)
|
||||||
|
return idCode
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to create document", e)
|
||||||
|
showFileManagerDialogFragment()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showFileManagerDialogFragment()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To use in onActivityResultCallback in Fragment or Activity
|
||||||
|
* @param onFileCreated Callback retrieve from data
|
||||||
|
* @return true if requestCode was captured, false elsewhere
|
||||||
|
*/
|
||||||
|
fun onCreateDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
|
||||||
|
onFileCreated: (fileCreated: Uri?)->Unit) {
|
||||||
|
// Retrieve the created URI from the file manager
|
||||||
|
if (fileRequestCodes.contains(requestCode) && resultCode == RESULT_OK) {
|
||||||
|
onFileCreated.invoke(data?.data)
|
||||||
|
fileRequestCodes.remove(requestCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val TAG = "OpenFileHelper"
|
||||||
|
|
||||||
|
private const val GET_CONTENT = 25745
|
||||||
|
private const val OPEN_DOC = 25845
|
||||||
|
private const val FILE_BROWSE = 25645
|
||||||
|
|
||||||
|
private var CREATE_FILE_REQUEST_CODE_DEFAULT = 3853
|
||||||
|
private var fileRequestCodes = ArrayList<Int>()
|
||||||
|
|
||||||
|
private fun getUnusedCreateFileRequestCode(): Int {
|
||||||
|
val newCreateFileRequestCode = CREATE_FILE_REQUEST_CODE_DEFAULT++
|
||||||
|
fileRequestCodes.add(newCreateFileRequestCode)
|
||||||
|
return newCreateFileRequestCode
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager,
|
||||||
|
typeString: String = "application/octet-stream"): Boolean {
|
||||||
|
return when {
|
||||||
|
// To check if a custom file manager can manage the ACTION_CREATE_DOCUMENT
|
||||||
|
Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT -> {
|
||||||
|
packageManager.queryIntentActivities(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = typeString
|
||||||
|
}, PackageManager.MATCH_DEFAULT_ONLY).isNotEmpty()
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.setOpenDocumentClickListener(externalFileHelper: ExternalFileHelper?) {
|
||||||
|
externalFileHelper?.let { fileHelper ->
|
||||||
|
setOnClickListener {
|
||||||
|
fileHelper.openDocument()
|
||||||
|
}
|
||||||
|
setOnLongClickListener {
|
||||||
|
fileHelper.openDocument(true)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} ?: kotlin.run {
|
||||||
|
setOnClickListener(null)
|
||||||
|
setOnLongClickListener(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX 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.
|
|
||||||
*
|
|
||||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities.helpers
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.Activity.RESULT_OK
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
|
||||||
|
|
||||||
class SelectFileHelper {
|
|
||||||
|
|
||||||
private var activity: Activity? = null
|
|
||||||
private var fragment: Fragment? = null
|
|
||||||
|
|
||||||
val selectFileOnClickViewListener: SelectFileOnClickViewListener
|
|
||||||
get() = SelectFileOnClickViewListener()
|
|
||||||
|
|
||||||
constructor(context: Activity) {
|
|
||||||
this.activity = context
|
|
||||||
this.fragment = null
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Fragment) {
|
|
||||||
this.activity = context.activity
|
|
||||||
this.fragment = context
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class SelectFileOnClickViewListener :
|
|
||||||
View.OnClickListener,
|
|
||||||
View.OnLongClickListener,
|
|
||||||
MenuItem.OnMenuItemClickListener {
|
|
||||||
|
|
||||||
private fun onAbstractClick(longClick: Boolean = false) {
|
|
||||||
try {
|
|
||||||
if (longClick) {
|
|
||||||
try {
|
|
||||||
openActivityWithActionGetContent()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
openActivityWithActionOpenDocument()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
openActivityWithActionOpenDocument()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
openActivityWithActionGetContent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Enable to start the file picker activity", e)
|
|
||||||
// Open browser dialog
|
|
||||||
if (lookForOpenIntentsFilePicker())
|
|
||||||
showBrowserDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
|
||||||
onAbstractClick()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongClick(v: View?): Boolean {
|
|
||||||
onAbstractClick(true)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
|
||||||
onAbstractClick()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
private fun openActivityWithActionOpenDocument() {
|
|
||||||
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = "*/*"
|
|
||||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
if (fragment != null)
|
|
||||||
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
|
||||||
else
|
|
||||||
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
private fun openActivityWithActionGetContent() {
|
|
||||||
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = "*/*"
|
|
||||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
if (fragment != null)
|
|
||||||
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
|
||||||
else
|
|
||||||
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun lookForOpenIntentsFilePicker(): Boolean {
|
|
||||||
var showBrowser = false
|
|
||||||
try {
|
|
||||||
if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
|
|
||||||
val intent = Intent(OPEN_INTENTS_FILE_BROWSE)
|
|
||||||
if (fragment != null)
|
|
||||||
fragment?.startActivityForResult(intent, FILE_BROWSE)
|
|
||||||
else
|
|
||||||
activity?.startActivityForResult(intent, FILE_BROWSE)
|
|
||||||
} else {
|
|
||||||
showBrowser = true
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "Enable to start OPEN_INTENTS_FILE_BROWSE", e)
|
|
||||||
showBrowser = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return showBrowser
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether the specified action can be used as an intent. This
|
|
||||||
* method queries the package manager for installed packages that can
|
|
||||||
* respond to an intent with the specified action. If no suitable package is
|
|
||||||
* found, this method returns false.
|
|
||||||
*
|
|
||||||
* @param context The application's environment.
|
|
||||||
* @param action The Intent action to check for availability.
|
|
||||||
*
|
|
||||||
* @return True if an Intent with the specified action can be sent and
|
|
||||||
* responded to, false otherwise.
|
|
||||||
*/
|
|
||||||
private fun isIntentAvailable(context: Context, action: String): Boolean {
|
|
||||||
val packageManager = context.packageManager
|
|
||||||
val intent = Intent(action)
|
|
||||||
val list = packageManager.queryIntentActivities(intent,
|
|
||||||
PackageManager.MATCH_DEFAULT_ONLY)
|
|
||||||
return list.size > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show Browser dialog to select file picker app
|
|
||||||
*/
|
|
||||||
private fun showBrowserDialog() {
|
|
||||||
try {
|
|
||||||
val fileManagerDialogFragment = FileManagerDialogFragment()
|
|
||||||
fragment?.let {
|
|
||||||
fileManagerDialogFragment.show(it.parentFragmentManager, "browserDialog")
|
|
||||||
} ?: fileManagerDialogFragment.show((activity as FragmentActivity).supportFragmentManager, "browserDialog")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Can't open BrowserDialog", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To use in onActivityResultCallback in Fragment or Activity
|
|
||||||
* @param keyFileCallback Callback retrieve from data
|
|
||||||
* @return true if requestCode was captured, false elsechere
|
|
||||||
*/
|
|
||||||
fun onActivityResultCallback(
|
|
||||||
requestCode: Int,
|
|
||||||
resultCode: Int,
|
|
||||||
data: Intent?,
|
|
||||||
keyFileCallback: ((uri: Uri?) -> Unit)?): Boolean {
|
|
||||||
|
|
||||||
when (requestCode) {
|
|
||||||
FILE_BROWSE -> {
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
val filename = data?.dataString
|
|
||||||
var keyUri: Uri? = null
|
|
||||||
if (filename != null) {
|
|
||||||
keyUri = UriUtil.parse(filename)
|
|
||||||
}
|
|
||||||
keyFileCallback?.invoke(keyUri)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
GET_CONTENT, OPEN_DOC -> {
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
if (data != null) {
|
|
||||||
val uri = data.data
|
|
||||||
if (uri != null) {
|
|
||||||
try {
|
|
||||||
// try to persist read and write permissions
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
|
||||||
activity?.contentResolver?.apply {
|
|
||||||
takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// nop
|
|
||||||
}
|
|
||||||
keyFileCallback?.invoke(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val TAG = "OpenFileHelper"
|
|
||||||
|
|
||||||
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"
|
|
||||||
|
|
||||||
private const val GET_CONTENT = 25745
|
|
||||||
private const val OPEN_DOC = 25845
|
|
||||||
private const val FILE_BROWSE = 25645
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -37,12 +37,12 @@ object Stylish {
|
|||||||
* Initialize the class with a theme preference
|
* Initialize the class with a theme preference
|
||||||
* @param context Context to retrieve the theme preference
|
* @param context Context to retrieve the theme preference
|
||||||
*/
|
*/
|
||||||
fun init(context: Context) {
|
fun load(context: Context) {
|
||||||
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
|
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
|
||||||
themeString = PreferencesUtil.getStyle(context)
|
themeString = PreferencesUtil.getStyle(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
|
fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
|
||||||
val systemNightMode = when (PreferencesUtil.getStyleBrightness(context)) {
|
val systemNightMode = when (PreferencesUtil.getStyleBrightness(context)) {
|
||||||
context.getString(R.string.list_style_brightness_light) -> false
|
context.getString(R.string.list_style_brightness_light) -> false
|
||||||
context.getString(R.string.list_style_brightness_night) -> true
|
context.getString(R.string.list_style_brightness_night) -> true
|
||||||
@@ -84,12 +84,16 @@ object Stylish {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun defaultStyle(context: Context): String {
|
||||||
|
return context.getString(R.string.list_style_name_light)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assign the style to the class attribute
|
* Assign the style to the class attribute
|
||||||
* @param styleString Style id String
|
* @param styleString Style id String
|
||||||
*/
|
*/
|
||||||
fun assignStyle(context: Context, styleString: String) {
|
fun assignStyle(context: Context, styleString: String) {
|
||||||
themeString = retrieveEquivalentSystemStyle(context, styleString)
|
PreferencesUtil.setStyle(context, styleString)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import kotlin.math.max
|
|||||||
class EntryAttachmentsItemsAdapter(context: Context)
|
class EntryAttachmentsItemsAdapter(context: Context)
|
||||||
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
|
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
|
||||||
|
|
||||||
var binaryCipherKey: Database.LoadedKey? = null
|
var database: Database? = null
|
||||||
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
|
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
|
||||||
var onBinaryPreviewLoaded: ((item: EntryAttachmentState) -> Unit)? = null
|
var onBinaryPreviewLoaded: ((item: EntryAttachmentState) -> Unit)? = null
|
||||||
|
|
||||||
@@ -82,21 +82,23 @@ class EntryAttachmentsItemsAdapter(context: Context)
|
|||||||
if (entryAttachmentState.previewState == AttachmentState.NULL) {
|
if (entryAttachmentState.previewState == AttachmentState.NULL) {
|
||||||
entryAttachmentState.previewState = AttachmentState.IN_PROGRESS
|
entryAttachmentState.previewState = AttachmentState.IN_PROGRESS
|
||||||
// Load the bitmap image
|
// Load the bitmap image
|
||||||
BinaryDatabaseManager.loadBitmap(
|
database?.let { database ->
|
||||||
entryAttachmentState.attachment.binaryData,
|
BinaryDatabaseManager.loadBitmap(
|
||||||
binaryCipherKey,
|
database,
|
||||||
mImagePreviewMaxWidth
|
entryAttachmentState.attachment.binaryData,
|
||||||
) { imageLoaded ->
|
mImagePreviewMaxWidth
|
||||||
if (imageLoaded == null) {
|
) { imageLoaded ->
|
||||||
entryAttachmentState.previewState = AttachmentState.ERROR
|
if (imageLoaded == null) {
|
||||||
visibility = View.GONE
|
entryAttachmentState.previewState = AttachmentState.ERROR
|
||||||
onBinaryPreviewLoaded?.invoke(entryAttachmentState)
|
visibility = View.GONE
|
||||||
} else {
|
onBinaryPreviewLoaded?.invoke(entryAttachmentState)
|
||||||
entryAttachmentState.previewState = AttachmentState.COMPLETE
|
} else {
|
||||||
setImageBitmap(imageLoaded)
|
entryAttachmentState.previewState = AttachmentState.COMPLETE
|
||||||
if (visibility != View.VISIBLE) {
|
setImageBitmap(imageLoaded)
|
||||||
expand(true, resources.getDimensionPixelSize(R.dimen.item_file_info_height)) {
|
if (visibility != View.VISIBLE) {
|
||||||
onBinaryPreviewLoaded?.invoke(entryAttachmentState)
|
expand(true, resources.getDimensionPixelSize(R.dimen.item_file_info_height)) {
|
||||||
|
onBinaryPreviewLoaded?.invoke(entryAttachmentState)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,8 +125,9 @@ class EntryAttachmentsItemsAdapter(context: Context)
|
|||||||
} else {
|
} else {
|
||||||
holder.binaryFileTitle.setTextColor(mTitleColor)
|
holder.binaryFileTitle.setTextColor(mTitleColor)
|
||||||
}
|
}
|
||||||
holder.binaryFileSize.text = Formatter.formatFileSize(context,
|
|
||||||
entryAttachmentState.attachment.binaryData.getSize())
|
val size = entryAttachmentState.attachment.binaryData.getSize()
|
||||||
|
holder.binaryFileSize.text = Formatter.formatFileSize(context, size)
|
||||||
holder.binaryFileCompression.apply {
|
holder.binaryFileCompression.apply {
|
||||||
if (entryAttachmentState.attachment.binaryData.isCompressed) {
|
if (entryAttachmentState.attachment.binaryData.isCompressed) {
|
||||||
text = CompressionAlgorithm.GZip.getName(context.resources)
|
text = CompressionAlgorithm.GZip.getName(context.resources)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class App : MultiDexApplication() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
Stylish.init(this)
|
Stylish.load(this)
|
||||||
PRNGFixes.apply()
|
PRNGFixes.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import android.content.Intent
|
|||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||||
@@ -76,7 +77,11 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
mServiceConnection!!,
|
mServiceConnection!!,
|
||||||
Context.BIND_ABOVE_CLIENT)
|
Context.BIND_ABOVE_CLIENT)
|
||||||
if (mBinder == null) {
|
if (mBinder == null) {
|
||||||
applicationContext.startService(mIntentAdvancedUnlockService)
|
try {
|
||||||
|
applicationContext.startService(mIntentAdvancedUnlockService)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to start cipher action", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,5 +178,7 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction)
|
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) {
|
||||||
|
private val TAG = CipherDatabaseAction::class.java.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX 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.
|
|
||||||
*
|
|
||||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.crypto
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.AesEngine
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.ChaCha20Engine
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.TwofishEngine
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.security.Security
|
|
||||||
import java.util.*
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.NoSuchPaddingException
|
|
||||||
|
|
||||||
object CipherFactory {
|
|
||||||
|
|
||||||
private var blacklistInit = false
|
|
||||||
private var blacklisted: Boolean = false
|
|
||||||
|
|
||||||
init {
|
|
||||||
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
|
|
||||||
Security.addProvider(BouncyCastleProvider())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deviceBlacklisted(): Boolean {
|
|
||||||
if (!blacklistInit) {
|
|
||||||
blacklistInit = true
|
|
||||||
// The Acer Iconia A500 is special and seems to always crash in the native crypto libraries
|
|
||||||
blacklisted = Build.MODEL == "A500"
|
|
||||||
}
|
|
||||||
return blacklisted
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hasNativeImplementation(transformation: String): Boolean {
|
|
||||||
return transformation == "AES/CBC/PKCS5Padding"
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class)
|
|
||||||
fun getInstance(transformation: String, androidOverride: Boolean = false): Cipher {
|
|
||||||
// Return the native AES if it is possible
|
|
||||||
return if (!deviceBlacklisted() && !androidOverride && hasNativeImplementation(transformation) && NativeLib.loaded()) {
|
|
||||||
Cipher.getInstance(transformation, AESProvider())
|
|
||||||
} else {
|
|
||||||
Cipher.getInstance(transformation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate appropriate cipher based on KeePass 2.x UUID's
|
|
||||||
*/
|
|
||||||
@Throws(NoSuchAlgorithmException::class)
|
|
||||||
fun getInstance(uuid: UUID): CipherEngine {
|
|
||||||
return when (uuid) {
|
|
||||||
AesEngine.CIPHER_UUID -> AesEngine()
|
|
||||||
TwofishEngine.CIPHER_UUID -> TwofishEngine()
|
|
||||||
ChaCha20Engine.CIPHER_UUID -> ChaCha20Engine()
|
|
||||||
else -> throw NoSuchAlgorithmException("UUID unrecognized.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX 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.
|
|
||||||
*
|
|
||||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.crypto
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
|
||||||
import com.kunzisoft.keepass.stream.longTo8Bytes
|
|
||||||
import java.io.IOException
|
|
||||||
import java.security.DigestOutputStream
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.util.*
|
|
||||||
import javax.crypto.Mac
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
object CryptoUtil {
|
|
||||||
|
|
||||||
fun resizeKey(inBytes: ByteArray, inOffset: Int, cbIn: Int, cbOut: Int): ByteArray {
|
|
||||||
if (cbOut == 0) return ByteArray(0)
|
|
||||||
|
|
||||||
val hash: ByteArray = if (cbOut <= 32) {
|
|
||||||
hashSha256(inBytes, inOffset, cbIn)
|
|
||||||
} else {
|
|
||||||
hashSha512(inBytes, inOffset, cbIn)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cbOut == hash.size) {
|
|
||||||
return hash
|
|
||||||
}
|
|
||||||
|
|
||||||
val ret = ByteArray(cbOut)
|
|
||||||
if (cbOut < hash.size) {
|
|
||||||
System.arraycopy(hash, 0, ret, 0, cbOut)
|
|
||||||
} else {
|
|
||||||
var pos = 0
|
|
||||||
var r: Long = 0
|
|
||||||
while (pos < cbOut) {
|
|
||||||
val hmac: Mac
|
|
||||||
try {
|
|
||||||
hmac = Mac.getInstance("HmacSHA256")
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
val pbR = longTo8Bytes(r)
|
|
||||||
val part = hmac.doFinal(pbR)
|
|
||||||
|
|
||||||
val copy = min(cbOut - pos, part.size)
|
|
||||||
System.arraycopy(part, 0, ret, pos, copy)
|
|
||||||
pos += copy
|
|
||||||
r++
|
|
||||||
|
|
||||||
Arrays.fill(part, 0.toByte())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Arrays.fill(hash, 0.toByte())
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hashSha256(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
|
|
||||||
return hashGen("SHA-256", data, offset, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hashSha512(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
|
|
||||||
return hashGen("SHA-512", data, offset, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hashGen(transform: String, data: ByteArray, offset: Int, count: Int): ByteArray {
|
|
||||||
val hash: MessageDigest
|
|
||||||
try {
|
|
||||||
hash = MessageDigest.getInstance(transform)
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
val nos = NullOutputStream()
|
|
||||||
val dos = DigestOutputStream(nos, hash)
|
|
||||||
|
|
||||||
try {
|
|
||||||
dos.write(data, offset, count)
|
|
||||||
dos.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
return hash.digest()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX 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.
|
|
||||||
*
|
|
||||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.crypto
|
|
||||||
|
|
||||||
import org.bouncycastle.crypto.StreamCipher
|
|
||||||
import org.bouncycastle.crypto.engines.ChaCha7539Engine
|
|
||||||
import org.bouncycastle.crypto.engines.Salsa20Engine
|
|
||||||
import org.bouncycastle.crypto.params.KeyParameter
|
|
||||||
import org.bouncycastle.crypto.params.ParametersWithIV
|
|
||||||
|
|
||||||
object StreamCipherFactory {
|
|
||||||
|
|
||||||
private val SALSA_IV = byteArrayOf(0xE8.toByte(), 0x30, 0x09, 0x4B, 0x97.toByte(), 0x20, 0x5D, 0x2A)
|
|
||||||
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun getInstance(alg: CrsAlgorithm?, key: ByteArray): StreamCipher {
|
|
||||||
return when {
|
|
||||||
alg === CrsAlgorithm.Salsa20 -> getSalsa20(key)
|
|
||||||
alg === CrsAlgorithm.ChaCha20 -> getChaCha20(key)
|
|
||||||
else -> throw Exception("Invalid random cipher")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSalsa20(key: ByteArray): StreamCipher {
|
|
||||||
// Build stream cipher key
|
|
||||||
val key32 = CryptoUtil.hashSha256(key)
|
|
||||||
|
|
||||||
val keyParam = KeyParameter(key32)
|
|
||||||
val ivParam = ParametersWithIV(keyParam, SALSA_IV)
|
|
||||||
|
|
||||||
val cipher = Salsa20Engine()
|
|
||||||
cipher.init(true, ivParam)
|
|
||||||
|
|
||||||
return cipher
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getChaCha20(key: ByteArray): StreamCipher {
|
|
||||||
// Build stream cipher key
|
|
||||||
val hash = CryptoUtil.hashSha512(key)
|
|
||||||
val key32 = ByteArray(32)
|
|
||||||
val iv = ByteArray(12)
|
|
||||||
|
|
||||||
System.arraycopy(hash, 0, key32, 0, 32)
|
|
||||||
System.arraycopy(hash, 32, iv, 0, 12)
|
|
||||||
|
|
||||||
val keyParam = KeyParameter(key32)
|
|
||||||
val ivParam = ParametersWithIV(keyParam, iv)
|
|
||||||
|
|
||||||
val cipher = ChaCha7539Engine()
|
|
||||||
cipher.init(true, ivParam)
|
|
||||||
|
|
||||||
return cipher
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX 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.
|
|
||||||
*
|
|
||||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.crypto.engine
|
|
||||||
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
|
||||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
|
||||||
import java.security.InvalidAlgorithmParameterException
|
|
||||||
import java.security.InvalidKeyException
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.util.*
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.NoSuchPaddingException
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
class AesEngine : CipherEngine() {
|
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
|
||||||
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray, androidOverride: Boolean): Cipher {
|
|
||||||
val cipher = CipherFactory.getInstance("AES/CBC/PKCS5Padding", androidOverride)
|
|
||||||
cipher.init(opmode, SecretKeySpec(key, "AES"), IvParameterSpec(IV))
|
|
||||||
return cipher
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPwEncryptionAlgorithm(): EncryptionAlgorithm {
|
|
||||||
return EncryptionAlgorithm.AESRijndael
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
val CIPHER_UUID: UUID = bytes16ToUuid(
|
|
||||||
byteArrayOf(0x31.toByte(),
|
|
||||||
0xC1.toByte(),
|
|
||||||
0xF2.toByte(),
|
|
||||||
0xE6.toByte(),
|
|
||||||
0xBF.toByte(),
|
|
||||||
0x71.toByte(),
|
|
||||||
0x43.toByte(),
|
|
||||||
0x50.toByte(),
|
|
||||||
0xBE.toByte(),
|
|
||||||
0x58.toByte(),
|
|
||||||
0x05.toByte(),
|
|
||||||
0x21.toByte(),
|
|
||||||
0x6A.toByte(),
|
|
||||||
0xFC.toByte(),
|
|
||||||
0x5A.toByte(),
|
|
||||||
0xFF.toByte()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX 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.
|
|
||||||
*
|
|
||||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.crypto.engine
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
|
||||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
|
||||||
import java.security.InvalidAlgorithmParameterException
|
|
||||||
import java.security.InvalidKeyException
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.util.*
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.NoSuchPaddingException
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
class TwofishEngine : CipherEngine() {
|
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
|
||||||
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray, androidOverride: Boolean): Cipher {
|
|
||||||
val cipher: Cipher = if (opmode == Cipher.ENCRYPT_MODE) {
|
|
||||||
CipherFactory.getInstance("Twofish/CBC/ZeroBytePadding", androidOverride)
|
|
||||||
} else {
|
|
||||||
CipherFactory.getInstance("Twofish/CBC/NoPadding", androidOverride)
|
|
||||||
}
|
|
||||||
cipher.init(opmode, SecretKeySpec(key, "AES"), IvParameterSpec(IV))
|
|
||||||
|
|
||||||
return cipher
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPwEncryptionAlgorithm(): EncryptionAlgorithm {
|
|
||||||
return EncryptionAlgorithm.Twofish
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
val CIPHER_UUID: UUID = bytes16ToUuid(
|
|
||||||
byteArrayOf(0xAD.toByte(),
|
|
||||||
0x68.toByte(),
|
|
||||||
0xF2.toByte(),
|
|
||||||
0x9F.toByte(),
|
|
||||||
0x57.toByte(),
|
|
||||||
0x6F.toByte(),
|
|
||||||
0x4B.toByte(),
|
|
||||||
0xB9.toByte(),
|
|
||||||
0xA3.toByte(),
|
|
||||||
0x6A.toByte(),
|
|
||||||
0xD4.toByte(),
|
|
||||||
0x7A.toByte(),
|
|
||||||
0xF9.toByte(),
|
|
||||||
0x65.toByte(),
|
|
||||||
0x34.toByte(),
|
|
||||||
0x6C.toByte()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX 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.
|
|
||||||
*
|
|
||||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.crypto.finalkey
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory.deviceBlacklisted
|
|
||||||
|
|
||||||
object AESKeyTransformerFactory : KeyTransformer() {
|
|
||||||
override fun transformMasterKey(seed: ByteArray?, key: ByteArray?, rounds: Long?): ByteArray? {
|
|
||||||
// Prefer the native final key implementation
|
|
||||||
val keyTransformer = if (!deviceBlacklisted()
|
|
||||||
&& NativeAESKeyTransformer.available()) {
|
|
||||||
NativeAESKeyTransformer()
|
|
||||||
} else {
|
|
||||||
// Fall back on the android crypto implementation
|
|
||||||
AndroidAESKeyTransformer()
|
|
||||||
}
|
|
||||||
return keyTransformer.transformMasterKey(seed, key, rounds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX 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.
|
|
||||||
*
|
|
||||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.crypto.keyDerivation;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.NativeLib;
|
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class Argon2Native {
|
|
||||||
|
|
||||||
enum CType {
|
|
||||||
ARGON2_D(0),
|
|
||||||
ARGON2_I(1),
|
|
||||||
ARGON2_ID(2);
|
|
||||||
|
|
||||||
int cValue = 0;
|
|
||||||
|
|
||||||
CType(int i) {
|
|
||||||
cValue = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] transformKey(Argon2Kdf.Type type, byte[] password, byte[] salt, UnsignedInt parallelism,
|
|
||||||
UnsignedInt memory, UnsignedInt iterations, byte[] secretKey,
|
|
||||||
byte[] associatedData, UnsignedInt version) throws IOException {
|
|
||||||
NativeLib.INSTANCE.init();
|
|
||||||
|
|
||||||
CType cType = CType.ARGON2_D;
|
|
||||||
if (type.equals(Argon2Kdf.Type.ARGON2_ID))
|
|
||||||
cType = CType.ARGON2_ID;
|
|
||||||
|
|
||||||
return nTransformMasterKey(
|
|
||||||
cType.cValue,
|
|
||||||
password,
|
|
||||||
salt,
|
|
||||||
parallelism.toKotlinInt(),
|
|
||||||
memory.toKotlinInt(),
|
|
||||||
iterations.toKotlinInt(),
|
|
||||||
secretKey,
|
|
||||||
associatedData,
|
|
||||||
version.toKotlinInt());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static native byte[] nTransformMasterKey(int type, byte[] password, byte[] salt, int parallelism,
|
|
||||||
int memory, int iterations, byte[] secretKey,
|
|
||||||
byte[] associatedData, int version) throws IOException;
|
|
||||||
}
|
|
||||||
@@ -25,6 +25,8 @@ import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
|||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
@@ -55,7 +57,10 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
mReadonly,
|
mReadonly,
|
||||||
context.contentResolver,
|
context.contentResolver,
|
||||||
UriUtil.getBinaryDir(context),
|
UriUtil.getBinaryDir(context),
|
||||||
Database.LoadedKey.generateNewCipherKey(),
|
{ memoryWanted ->
|
||||||
|
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||||
|
},
|
||||||
|
LoadedKey.generateNewCipherKey(),
|
||||||
mFixDuplicateUUID,
|
mFixDuplicateUUID,
|
||||||
progressTaskUpdater)
|
progressTaskUpdater)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,18 +25,21 @@ import android.content.Context.BIND_NOT_FOREGROUND
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
@@ -48,8 +51,8 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
|||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
|
||||||
@@ -251,11 +254,16 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun start(bundle: Bundle? = null, actionTask: String) {
|
private fun start(bundle: Bundle? = null, actionTask: String) {
|
||||||
activity.stopService(intentDatabaseTask)
|
try {
|
||||||
if (bundle != null)
|
activity.stopService(intentDatabaseTask)
|
||||||
intentDatabaseTask.putExtras(bundle)
|
if (bundle != null)
|
||||||
intentDatabaseTask.action = actionTask
|
intentDatabaseTask.putExtras(bundle)
|
||||||
activity.startService(intentDatabaseTask)
|
intentDatabaseTask.action = actionTask
|
||||||
|
activity.startService(intentDatabaseTask)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to perform database action", e)
|
||||||
|
Toast.makeText(activity, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -591,4 +599,8 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
, ACTION_DATABASE_SAVE)
|
, ACTION_DATABASE_SAVE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = ProgressDatabaseTaskProvider::class.java.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,8 @@ package com.kunzisoft.keepass.database.action
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
@@ -33,10 +35,10 @@ class ReloadDatabaseRunnable(private val context: Context,
|
|||||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||||
: ActionRunnable() {
|
: ActionRunnable() {
|
||||||
|
|
||||||
private var tempCipherKey: Database.LoadedKey? = null
|
private var tempCipherKey: LoadedKey? = null
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
tempCipherKey = mDatabase.loadedCipherKey
|
tempCipherKey = mDatabase.binaryCache.loadedCipherKey
|
||||||
// Clear before we load
|
// Clear before we load
|
||||||
mDatabase.clear(UriUtil.getBinaryDir(context))
|
mDatabase.clear(UriUtil.getBinaryDir(context))
|
||||||
mDatabase.wasReloaded = true
|
mDatabase.wasReloaded = true
|
||||||
@@ -46,7 +48,10 @@ class ReloadDatabaseRunnable(private val context: Context,
|
|||||||
try {
|
try {
|
||||||
mDatabase.reloadData(context.contentResolver,
|
mDatabase.reloadData(context.contentResolver,
|
||||||
UriUtil.getBinaryDir(context),
|
UriUtil.getBinaryDir(context),
|
||||||
tempCipherKey ?: Database.LoadedKey.generateNewCipherKey(),
|
{ memoryWanted ->
|
||||||
|
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||||
|
},
|
||||||
|
tempCipherKey ?: LoadedKey.generateNewCipherKey(),
|
||||||
progressTaskUpdater)
|
progressTaskUpdater)
|
||||||
} catch (e: LoadDatabaseException) {
|
} catch (e: LoadDatabaseException) {
|
||||||
setError(e)
|
setError(e)
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX 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.
|
||||||
|
*
|
||||||
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.database.crypto
|
||||||
|
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.CipherFactory
|
||||||
|
import java.security.InvalidAlgorithmParameterException
|
||||||
|
import java.security.InvalidKeyException
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.NoSuchPaddingException
|
||||||
|
|
||||||
|
class AesEngine : CipherEngine() {
|
||||||
|
|
||||||
|
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
||||||
|
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
|
||||||
|
return CipherFactory.getAES(opmode, key, IV)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getEncryptionAlgorithm(): EncryptionAlgorithm {
|
||||||
|
return EncryptionAlgorithm.AESRijndael
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,19 +17,14 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.engine
|
package com.kunzisoft.keepass.database.crypto
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
import com.kunzisoft.encrypt.CipherFactory
|
||||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
|
||||||
import java.security.InvalidAlgorithmParameterException
|
import java.security.InvalidAlgorithmParameterException
|
||||||
import java.security.InvalidKeyException
|
import java.security.InvalidKeyException
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import java.util.*
|
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.NoSuchPaddingException
|
import javax.crypto.NoSuchPaddingException
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
class ChaCha20Engine : CipherEngine() {
|
class ChaCha20Engine : CipherEngine() {
|
||||||
|
|
||||||
@@ -38,34 +33,11 @@ class ChaCha20Engine : CipherEngine() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
||||||
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray, androidOverride: Boolean): Cipher {
|
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
|
||||||
val cipher = Cipher.getInstance("Chacha7539", BouncyCastleProvider())
|
return CipherFactory.getChacha20(opmode, key, IV)
|
||||||
cipher.init(opmode, SecretKeySpec(key, "ChaCha7539"), IvParameterSpec(IV))
|
|
||||||
return cipher
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPwEncryptionAlgorithm(): EncryptionAlgorithm {
|
override fun getEncryptionAlgorithm(): EncryptionAlgorithm {
|
||||||
return EncryptionAlgorithm.ChaCha20
|
return EncryptionAlgorithm.ChaCha20
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
val CIPHER_UUID: UUID = bytes16ToUuid(
|
|
||||||
byteArrayOf(0xD6.toByte(),
|
|
||||||
0x03.toByte(),
|
|
||||||
0x8A.toByte(),
|
|
||||||
0x2B.toByte(),
|
|
||||||
0x8B.toByte(),
|
|
||||||
0x6F.toByte(),
|
|
||||||
0x4C.toByte(),
|
|
||||||
0xB5.toByte(),
|
|
||||||
0xA5.toByte(),
|
|
||||||
0x24.toByte(),
|
|
||||||
0x33.toByte(),
|
|
||||||
0x9A.toByte(),
|
|
||||||
0x31.toByte(),
|
|
||||||
0xDB.toByte(),
|
|
||||||
0xB5.toByte(),
|
|
||||||
0x9A.toByte()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -17,9 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.engine
|
package com.kunzisoft.keepass.database.crypto
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
|
||||||
|
|
||||||
import java.security.InvalidAlgorithmParameterException
|
import java.security.InvalidAlgorithmParameterException
|
||||||
import java.security.InvalidKeyException
|
import java.security.InvalidKeyException
|
||||||
@@ -38,14 +36,12 @@ abstract class CipherEngine {
|
|||||||
return 16
|
return 16
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
// Used only with padding workaround
|
||||||
abstract fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray, androidOverride: Boolean): Cipher
|
var forcePaddingCompatibility = false
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
||||||
fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
|
abstract fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher
|
||||||
return getCipher(opmode, key, IV, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract fun getPwEncryptionAlgorithm(): EncryptionAlgorithm
|
abstract fun getEncryptionAlgorithm(): EncryptionAlgorithm
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -17,11 +17,13 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto
|
package com.kunzisoft.keepass.database.crypto
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.HashManager
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
import com.kunzisoft.encrypt.StreamCipher
|
||||||
|
|
||||||
enum class CrsAlgorithm constructor(val id: UnsignedInt) {
|
enum class CrsAlgorithm(val id: UnsignedInt) {
|
||||||
|
|
||||||
Null(UnsignedInt(0)),
|
Null(UnsignedInt(0)),
|
||||||
ArcFourVariant(UnsignedInt(1)),
|
ArcFourVariant(UnsignedInt(1)),
|
||||||
@@ -30,6 +32,15 @@ enum class CrsAlgorithm constructor(val id: UnsignedInt) {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getCipher(algorithm: CrsAlgorithm?, key: ByteArray): StreamCipher {
|
||||||
|
return when (algorithm) {
|
||||||
|
Salsa20 -> HashManager.getSalsa20(key)
|
||||||
|
ChaCha20 -> HashManager.getChaCha20(key)
|
||||||
|
else -> throw Exception("Invalid random cipher")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun fromId(num: UnsignedInt): CrsAlgorithm? {
|
fun fromId(num: UnsignedInt): CrsAlgorithm? {
|
||||||
for (e in values()) {
|
for (e in values()) {
|
||||||
if (e.id == num) {
|
if (e.id == num) {
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX 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.
|
||||||
|
*
|
||||||
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.database.crypto
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.utils.bytes16ToUuid
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
enum class EncryptionAlgorithm {
|
||||||
|
|
||||||
|
AESRijndael,
|
||||||
|
Twofish,
|
||||||
|
ChaCha20;
|
||||||
|
|
||||||
|
val cipherEngine: CipherEngine
|
||||||
|
get() {
|
||||||
|
return when (this) {
|
||||||
|
AESRijndael -> AesEngine()
|
||||||
|
Twofish -> TwofishEngine()
|
||||||
|
ChaCha20 -> ChaCha20Engine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val uuid: UUID
|
||||||
|
get() {
|
||||||
|
return when (this) {
|
||||||
|
AESRijndael -> AES_UUID
|
||||||
|
Twofish -> TWOFISH_UUID
|
||||||
|
ChaCha20 -> CHACHA20_UUID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return when (this) {
|
||||||
|
AESRijndael -> "Rijndael (AES)"
|
||||||
|
Twofish -> "Twofish"
|
||||||
|
ChaCha20 -> "ChaCha20"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate appropriate cipher based on KeePass 2.x UUID's
|
||||||
|
*/
|
||||||
|
@Throws(NoSuchAlgorithmException::class)
|
||||||
|
fun getFrom(uuid: UUID): EncryptionAlgorithm {
|
||||||
|
return when (uuid) {
|
||||||
|
AES_UUID -> AESRijndael
|
||||||
|
TWOFISH_UUID -> Twofish
|
||||||
|
CHACHA20_UUID -> ChaCha20
|
||||||
|
else -> throw NoSuchAlgorithmException("UUID unrecognized.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val AES_UUID: UUID by lazy {
|
||||||
|
bytes16ToUuid(
|
||||||
|
byteArrayOf(0x31.toByte(),
|
||||||
|
0xC1.toByte(),
|
||||||
|
0xF2.toByte(),
|
||||||
|
0xE6.toByte(),
|
||||||
|
0xBF.toByte(),
|
||||||
|
0x71.toByte(),
|
||||||
|
0x43.toByte(),
|
||||||
|
0x50.toByte(),
|
||||||
|
0xBE.toByte(),
|
||||||
|
0x58.toByte(),
|
||||||
|
0x05.toByte(),
|
||||||
|
0x21.toByte(),
|
||||||
|
0x6A.toByte(),
|
||||||
|
0xFC.toByte(),
|
||||||
|
0x5A.toByte(),
|
||||||
|
0xFF.toByte()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private val TWOFISH_UUID: UUID by lazy {
|
||||||
|
bytes16ToUuid(
|
||||||
|
byteArrayOf(0xAD.toByte(),
|
||||||
|
0x68.toByte(),
|
||||||
|
0xF2.toByte(),
|
||||||
|
0x9F.toByte(),
|
||||||
|
0x57.toByte(),
|
||||||
|
0x6F.toByte(),
|
||||||
|
0x4B.toByte(),
|
||||||
|
0xB9.toByte(),
|
||||||
|
0xA3.toByte(),
|
||||||
|
0x6A.toByte(),
|
||||||
|
0xD4.toByte(),
|
||||||
|
0x7A.toByte(),
|
||||||
|
0xF9.toByte(),
|
||||||
|
0x65.toByte(),
|
||||||
|
0x34.toByte(),
|
||||||
|
0x6C.toByte()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private val CHACHA20_UUID: UUID by lazy {
|
||||||
|
bytes16ToUuid(
|
||||||
|
byteArrayOf(0xD6.toByte(),
|
||||||
|
0x03.toByte(),
|
||||||
|
0x8A.toByte(),
|
||||||
|
0x2B.toByte(),
|
||||||
|
0x8B.toByte(),
|
||||||
|
0x6F.toByte(),
|
||||||
|
0x4C.toByte(),
|
||||||
|
0xB5.toByte(),
|
||||||
|
0xA5.toByte(),
|
||||||
|
0x24.toByte(),
|
||||||
|
0x33.toByte(),
|
||||||
|
0x9A.toByte(),
|
||||||
|
0x31.toByte(),
|
||||||
|
0xDB.toByte(),
|
||||||
|
0xB5.toByte(),
|
||||||
|
0x9A.toByte()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,35 +17,40 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.stream
|
package com.kunzisoft.keepass.database.crypto
|
||||||
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.security.DigestOutputStream
|
import java.security.InvalidKeyException
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
object HmacBlockStream {
|
object HmacBlock {
|
||||||
fun getHmacKey64(key: ByteArray, blockIndex: Long): ByteArray {
|
|
||||||
|
fun getHmacSha256(blockKey: ByteArray): Mac {
|
||||||
|
val hmac: Mac
|
||||||
|
try {
|
||||||
|
hmac = Mac.getInstance("HmacSHA256")
|
||||||
|
val signingKey = SecretKeySpec(blockKey, "HmacSHA256")
|
||||||
|
hmac.init(signingKey)
|
||||||
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
|
throw IOException("No HmacAlogirthm")
|
||||||
|
} catch (e: InvalidKeyException) {
|
||||||
|
throw IOException("Invalid Hmac Key")
|
||||||
|
}
|
||||||
|
return hmac
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getHmacKey64(key: ByteArray, blockIndex: ByteArray): ByteArray {
|
||||||
val hash: MessageDigest
|
val hash: MessageDigest
|
||||||
try {
|
try {
|
||||||
hash = MessageDigest.getInstance("SHA-512")
|
hash = MessageDigest.getInstance("SHA-512")
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
throw RuntimeException(e)
|
throw RuntimeException(e)
|
||||||
}
|
}
|
||||||
|
hash.update(blockIndex)
|
||||||
val nos = NullOutputStream()
|
hash.update(key)
|
||||||
val dos = DigestOutputStream(nos, hash)
|
|
||||||
val leos = LittleEndianDataOutputStream(dos)
|
|
||||||
|
|
||||||
try {
|
|
||||||
leos.writeLong(blockIndex)
|
|
||||||
leos.write(key)
|
|
||||||
leos.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
//assert(hashKey.length == 64);
|
|
||||||
return hash.digest()
|
return hash.digest()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX 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.
|
||||||
|
*
|
||||||
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.database.crypto
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.CipherFactory
|
||||||
|
import java.security.InvalidAlgorithmParameterException
|
||||||
|
import java.security.InvalidKeyException
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.NoSuchPaddingException
|
||||||
|
|
||||||
|
class TwofishEngine : CipherEngine() {
|
||||||
|
|
||||||
|
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
||||||
|
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
|
||||||
|
return CipherFactory.getTwofish(opmode, key, IV, forcePaddingCompatibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getEncryptionAlgorithm(): EncryptionAlgorithm {
|
||||||
|
return EncryptionAlgorithm.Twofish
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,13 +17,10 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.utils
|
package com.kunzisoft.keepass.database.crypto
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.stream.*
|
import java.io.*
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -55,12 +52,12 @@ open class VariantDictionary {
|
|||||||
return dict[name]?.value as UnsignedInt?
|
return dict[name]?.value as UnsignedInt?
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setUInt64(name: String, value: Long) {
|
fun setUInt64(name: String, value: UnsignedLong) {
|
||||||
putType(VdType.UInt64, name, value)
|
putType(VdType.UInt64, name, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUInt64(name: String): Long? {
|
fun getUInt64(name: String): UnsignedLong? {
|
||||||
return dict[name]?.value as Long?
|
return dict[name]?.value as UnsignedLong?
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setBool(name: String, value: Boolean) {
|
fun setBool(name: String, value: Boolean) {
|
||||||
@@ -115,22 +112,21 @@ open class VariantDictionary {
|
|||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun deserialize(data: ByteArray): VariantDictionary {
|
fun deserialize(data: ByteArray): VariantDictionary {
|
||||||
val inputStream = LittleEndianDataInputStream(ByteArrayInputStream(data))
|
val inputStream = ByteArrayInputStream(data)
|
||||||
return deserialize(inputStream)
|
return deserialize(inputStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun serialize(kdfParameters: KdfParameters): ByteArray {
|
fun serialize(variantDictionary: VariantDictionary): ByteArray {
|
||||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||||
val outputStream = LittleEndianDataOutputStream(byteArrayOutputStream)
|
serialize(variantDictionary, byteArrayOutputStream)
|
||||||
serialize(kdfParameters, outputStream)
|
|
||||||
return byteArrayOutputStream.toByteArray()
|
return byteArrayOutputStream.toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun deserialize(inputStream: LittleEndianDataInputStream): VariantDictionary {
|
fun deserialize(inputStream: InputStream): VariantDictionary {
|
||||||
val dictionary = VariantDictionary()
|
val dictionary = VariantDictionary()
|
||||||
val version = inputStream.readUShort()
|
val version = inputStream.readBytes2ToUShort()
|
||||||
if (version and VdmCritical > VdVersion and VdmCritical) {
|
if (version and VdmCritical > VdVersion and VdmCritical) {
|
||||||
throw IOException("Invalid format")
|
throw IOException("Invalid format")
|
||||||
}
|
}
|
||||||
@@ -143,14 +139,14 @@ open class VariantDictionary {
|
|||||||
if (bType == VdType.None) {
|
if (bType == VdType.None) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
val nameLen = inputStream.readUInt().toKotlinInt()
|
val nameLen = inputStream.readBytes4ToUInt().toKotlinInt()
|
||||||
val nameBuf = inputStream.readBytes(nameLen)
|
val nameBuf = inputStream.readBytesLength(nameLen)
|
||||||
if (nameLen != nameBuf.size) {
|
if (nameLen != nameBuf.size) {
|
||||||
throw IOException("Invalid format")
|
throw IOException("Invalid format")
|
||||||
}
|
}
|
||||||
val name = String(nameBuf, UTF8Charset)
|
val name = String(nameBuf, UTF8Charset)
|
||||||
val valueLen = inputStream.readUInt().toKotlinInt()
|
val valueLen = inputStream.readBytes4ToUInt().toKotlinInt()
|
||||||
val valueBuf = inputStream.readBytes(valueLen)
|
val valueBuf = inputStream.readBytesLength(valueLen)
|
||||||
if (valueLen != valueBuf.size) {
|
if (valueLen != valueBuf.size) {
|
||||||
throw IOException("Invalid format")
|
throw IOException("Invalid format")
|
||||||
}
|
}
|
||||||
@@ -159,7 +155,7 @@ open class VariantDictionary {
|
|||||||
dictionary.setUInt32(name, bytes4ToUInt(valueBuf))
|
dictionary.setUInt32(name, bytes4ToUInt(valueBuf))
|
||||||
}
|
}
|
||||||
VdType.UInt64 -> if (valueLen == 8) {
|
VdType.UInt64 -> if (valueLen == 8) {
|
||||||
dictionary.setUInt64(name, bytes64ToLong(valueBuf))
|
dictionary.setUInt64(name, bytes64ToULong(valueBuf))
|
||||||
}
|
}
|
||||||
VdType.Bool -> if (valueLen == 1) {
|
VdType.Bool -> if (valueLen == 1) {
|
||||||
dictionary.setBool(name, valueBuf[0] != 0.toByte())
|
dictionary.setBool(name, valueBuf[0] != 0.toByte())
|
||||||
@@ -181,48 +177,47 @@ open class VariantDictionary {
|
|||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun serialize(variantDictionary: VariantDictionary,
|
fun serialize(variantDictionary: VariantDictionary,
|
||||||
outputStream: LittleEndianDataOutputStream?) {
|
outputStream: OutputStream?) {
|
||||||
if (outputStream == null) {
|
if (outputStream == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
outputStream.writeUShort(VdVersion)
|
outputStream.write2BytesUShort(VdVersion)
|
||||||
for ((name, vd) in variantDictionary.dict) {
|
for ((name, vd) in variantDictionary.dict) {
|
||||||
val nameBuf = name.toByteArray(UTF8Charset)
|
val nameBuf = name.toByteArray(UTF8Charset)
|
||||||
outputStream.write(vd.type.toInt())
|
outputStream.writeByte(vd.type)
|
||||||
outputStream.writeInt(nameBuf.size)
|
outputStream.write4BytesUInt(UnsignedInt(nameBuf.size))
|
||||||
outputStream.write(nameBuf)
|
outputStream.write(nameBuf)
|
||||||
var buf: ByteArray
|
var buf: ByteArray
|
||||||
when (vd.type) {
|
when (vd.type) {
|
||||||
VdType.UInt32 -> {
|
VdType.UInt32 -> {
|
||||||
outputStream.writeInt(4)
|
outputStream.write4BytesUInt(UnsignedInt(4))
|
||||||
outputStream.writeUInt((vd.value as UnsignedInt))
|
outputStream.write4BytesUInt(vd.value as UnsignedInt)
|
||||||
}
|
}
|
||||||
VdType.UInt64 -> {
|
VdType.UInt64 -> {
|
||||||
outputStream.writeInt(8)
|
outputStream.write4BytesUInt(UnsignedInt(8))
|
||||||
outputStream.writeLong(vd.value as Long)
|
outputStream.write8BytesLong(vd.value as UnsignedLong)
|
||||||
}
|
}
|
||||||
VdType.Bool -> {
|
VdType.Bool -> {
|
||||||
outputStream.writeInt(1)
|
outputStream.write4BytesUInt(UnsignedInt(1))
|
||||||
val bool = if (vd.value as Boolean) 1.toByte() else 0.toByte()
|
outputStream.writeBooleanByte(vd.value as Boolean)
|
||||||
outputStream.write(bool.toInt())
|
|
||||||
}
|
}
|
||||||
VdType.Int32 -> {
|
VdType.Int32 -> {
|
||||||
outputStream.writeInt(4)
|
outputStream.write4BytesUInt(UnsignedInt(4))
|
||||||
outputStream.writeInt(vd.value as Int)
|
outputStream.write4BytesUInt(UnsignedInt(vd.value as Int))
|
||||||
}
|
}
|
||||||
VdType.Int64 -> {
|
VdType.Int64 -> {
|
||||||
outputStream.writeInt(8)
|
outputStream.write4BytesUInt(UnsignedInt(8))
|
||||||
outputStream.writeLong(vd.value as Long)
|
outputStream.write8BytesLong(vd.value as Long)
|
||||||
}
|
}
|
||||||
VdType.String -> {
|
VdType.String -> {
|
||||||
val value = vd.value as String
|
val value = vd.value as String
|
||||||
buf = value.toByteArray(UTF8Charset)
|
buf = value.toByteArray(UTF8Charset)
|
||||||
outputStream.writeInt(buf.size)
|
outputStream.write4BytesUInt(UnsignedInt(buf.size))
|
||||||
outputStream.write(buf)
|
outputStream.write(buf)
|
||||||
}
|
}
|
||||||
VdType.ByteArray -> {
|
VdType.ByteArray -> {
|
||||||
buf = vd.value as ByteArray
|
buf = vd.value as ByteArray
|
||||||
outputStream.writeInt(buf.size)
|
outputStream.write4BytesUInt(UnsignedInt(buf.size))
|
||||||
outputStream.write(buf)
|
outputStream.write(buf)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -17,13 +17,12 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.keyDerivation
|
package com.kunzisoft.keepass.database.crypto.kdf
|
||||||
|
|
||||||
import android.content.res.Resources
|
import com.kunzisoft.encrypt.HashManager
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.utils.UnsignedLong
|
||||||
import com.kunzisoft.keepass.crypto.CryptoUtil
|
import com.kunzisoft.encrypt.aes.AESTransformer
|
||||||
import com.kunzisoft.keepass.crypto.finalkey.AESKeyTransformerFactory
|
import com.kunzisoft.keepass.utils.bytes16ToUuid
|
||||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -38,32 +37,28 @@ class AesKdf : KdfEngine() {
|
|||||||
get() {
|
get() {
|
||||||
return KdfParameters(uuid!!).apply {
|
return KdfParameters(uuid!!).apply {
|
||||||
setParamUUID()
|
setParamUUID()
|
||||||
setUInt64(PARAM_ROUNDS, defaultKeyRounds)
|
setUInt64(PARAM_ROUNDS, UnsignedLong(defaultKeyRounds))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val defaultKeyRounds: Long = 500000L
|
override val defaultKeyRounds = 500000L
|
||||||
|
|
||||||
override fun getName(resources: Resources): String {
|
|
||||||
return resources.getString(R.string.kdf_AES)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun transform(masterKey: ByteArray, kdfParameters: KdfParameters): ByteArray {
|
override fun transform(masterKey: ByteArray, kdfParameters: KdfParameters): ByteArray {
|
||||||
|
|
||||||
var seed = kdfParameters.getByteArray(PARAM_SEED)
|
var seed = kdfParameters.getByteArray(PARAM_SEED)
|
||||||
if (seed != null && seed.size != 32) {
|
if (seed != null && seed.size != 32) {
|
||||||
seed = CryptoUtil.hashSha256(seed)
|
seed = HashManager.hashSha256(seed)
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentMasterKey = masterKey
|
var currentMasterKey = masterKey
|
||||||
if (currentMasterKey.size != 32) {
|
if (currentMasterKey.size != 32) {
|
||||||
currentMasterKey = CryptoUtil.hashSha256(currentMasterKey)
|
currentMasterKey = HashManager.hashSha256(currentMasterKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
val rounds = kdfParameters.getUInt64(PARAM_ROUNDS)
|
val rounds = kdfParameters.getUInt64(PARAM_ROUNDS)?.toKotlinLong()
|
||||||
|
|
||||||
return AESKeyTransformerFactory.transformMasterKey(seed, currentMasterKey, rounds) ?: ByteArray(0)
|
return AESTransformer.transformKey(seed, currentMasterKey, rounds) ?: ByteArray(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun randomize(kdfParameters: KdfParameters) {
|
override fun randomize(kdfParameters: KdfParameters) {
|
||||||
@@ -76,11 +71,15 @@ class AesKdf : KdfEngine() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getKeyRounds(kdfParameters: KdfParameters): Long {
|
override fun getKeyRounds(kdfParameters: KdfParameters): Long {
|
||||||
return kdfParameters.getUInt64(PARAM_ROUNDS) ?: defaultKeyRounds
|
return kdfParameters.getUInt64(PARAM_ROUNDS)?.toKotlinLong() ?: defaultKeyRounds
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setKeyRounds(kdfParameters: KdfParameters, keyRounds: Long) {
|
override fun setKeyRounds(kdfParameters: KdfParameters, keyRounds: Long) {
|
||||||
kdfParameters.setUInt64(PARAM_ROUNDS, keyRounds)
|
kdfParameters.setUInt64(PARAM_ROUNDS, UnsignedLong(keyRounds))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "AES"
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.keyDerivation
|
package com.kunzisoft.keepass.database.crypto.kdf
|
||||||
|
|
||||||
import android.content.res.Resources
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
import com.kunzisoft.keepass.utils.UnsignedLong
|
||||||
|
import com.kunzisoft.encrypt.argon2.Argon2Transformer
|
||||||
|
import com.kunzisoft.encrypt.argon2.Argon2Type
|
||||||
|
import com.kunzisoft.keepass.utils.bytes16ToUuid
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -48,40 +48,30 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override val defaultKeyRounds: Long
|
override val defaultKeyRounds: Long
|
||||||
get() = DEFAULT_ITERATIONS
|
get() = DEFAULT_ITERATIONS.toKotlinLong()
|
||||||
|
|
||||||
override fun getName(resources: Resources): String {
|
|
||||||
return resources.getString(type.nameId)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun transform(masterKey: ByteArray, kdfParameters: KdfParameters): ByteArray {
|
override fun transform(masterKey: ByteArray, kdfParameters: KdfParameters): ByteArray {
|
||||||
|
|
||||||
val salt = kdfParameters.getByteArray(PARAM_SALT)
|
val salt = kdfParameters.getByteArray(PARAM_SALT) ?: ByteArray(0)
|
||||||
val parallelism = kdfParameters.getUInt32(PARAM_PARALLELISM)?.let {
|
val parallelism = kdfParameters.getUInt32(PARAM_PARALLELISM)?.toKotlinLong() ?: DEFAULT_PARALLELISM.toKotlinLong()
|
||||||
UnsignedInt(it)
|
val memory = kdfParameters.getUInt64(PARAM_MEMORY)?.toKotlinLong()?.div(MEMORY_BLOCK_SIZE) ?: DEFAULT_MEMORY.toKotlinLong()
|
||||||
}
|
val iterations = kdfParameters.getUInt64(PARAM_ITERATIONS)?.toKotlinLong() ?: DEFAULT_ITERATIONS.toKotlinLong()
|
||||||
val memory = kdfParameters.getUInt64(PARAM_MEMORY)?.div(MEMORY_BLOCK_SIZE)?.let {
|
val version = kdfParameters.getUInt32(PARAM_VERSION)?.toKotlinInt() ?: MAX_VERSION.toKotlinInt()
|
||||||
UnsignedInt.fromKotlinLong(it)
|
|
||||||
}
|
|
||||||
val iterations = kdfParameters.getUInt64(PARAM_ITERATIONS)?.let {
|
|
||||||
UnsignedInt.fromKotlinLong(it)
|
|
||||||
}
|
|
||||||
val version = kdfParameters.getUInt32(PARAM_VERSION)?.let {
|
|
||||||
UnsignedInt(it)
|
|
||||||
}
|
|
||||||
val secretKey = kdfParameters.getByteArray(PARAM_SECRET_KEY)
|
|
||||||
val assocData = kdfParameters.getByteArray(PARAM_ASSOC_DATA)
|
|
||||||
|
|
||||||
return Argon2Native.transformKey(
|
// Not used
|
||||||
type,
|
// val secretKey = kdfParameters.getByteArray(PARAM_SECRET_KEY)
|
||||||
|
// val assocData = kdfParameters.getByteArray(PARAM_ASSOC_DATA)
|
||||||
|
|
||||||
|
val argonType = if (type == Type.ARGON2_ID) Argon2Type.ARGON2_ID else Argon2Type.ARGON2_D
|
||||||
|
|
||||||
|
return Argon2Transformer.transformKey(
|
||||||
|
argonType,
|
||||||
masterKey,
|
masterKey,
|
||||||
salt,
|
salt,
|
||||||
parallelism,
|
parallelism,
|
||||||
memory,
|
memory,
|
||||||
iterations,
|
iterations,
|
||||||
secretKey,
|
|
||||||
assocData,
|
|
||||||
version)
|
version)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,32 +85,32 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getKeyRounds(kdfParameters: KdfParameters): Long {
|
override fun getKeyRounds(kdfParameters: KdfParameters): Long {
|
||||||
return kdfParameters.getUInt64(PARAM_ITERATIONS) ?: defaultKeyRounds
|
return kdfParameters.getUInt64(PARAM_ITERATIONS)?.toKotlinLong() ?: defaultKeyRounds
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setKeyRounds(kdfParameters: KdfParameters, keyRounds: Long) {
|
override fun setKeyRounds(kdfParameters: KdfParameters, keyRounds: Long) {
|
||||||
kdfParameters.setUInt64(PARAM_ITERATIONS, keyRounds)
|
kdfParameters.setUInt64(PARAM_ITERATIONS, UnsignedLong(keyRounds))
|
||||||
}
|
}
|
||||||
|
|
||||||
override val minKeyRounds: Long
|
override val minKeyRounds: Long
|
||||||
get() = MIN_ITERATIONS
|
get() = MIN_ITERATIONS.toKotlinLong()
|
||||||
|
|
||||||
override val maxKeyRounds: Long
|
override val maxKeyRounds: Long
|
||||||
get() = MAX_ITERATIONS
|
get() = MAX_ITERATIONS.toKotlinLong()
|
||||||
|
|
||||||
override fun getMemoryUsage(kdfParameters: KdfParameters): Long {
|
override fun getMemoryUsage(kdfParameters: KdfParameters): Long {
|
||||||
return kdfParameters.getUInt64(PARAM_MEMORY) ?: defaultMemoryUsage
|
return kdfParameters.getUInt64(PARAM_MEMORY)?.toKotlinLong() ?: defaultMemoryUsage
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setMemoryUsage(kdfParameters: KdfParameters, memory: Long) {
|
override fun setMemoryUsage(kdfParameters: KdfParameters, memory: Long) {
|
||||||
kdfParameters.setUInt64(PARAM_MEMORY, memory)
|
kdfParameters.setUInt64(PARAM_MEMORY, UnsignedLong(memory))
|
||||||
}
|
}
|
||||||
|
|
||||||
override val defaultMemoryUsage: Long
|
override val defaultMemoryUsage: Long
|
||||||
get() = DEFAULT_MEMORY
|
get() = DEFAULT_MEMORY.toKotlinLong()
|
||||||
|
|
||||||
override val minMemoryUsage: Long
|
override val minMemoryUsage: Long
|
||||||
get() = MIN_MEMORY
|
get() = MIN_MEMORY.toKotlinLong()
|
||||||
|
|
||||||
override val maxMemoryUsage: Long
|
override val maxMemoryUsage: Long
|
||||||
get() = MAX_MEMORY
|
get() = MAX_MEMORY
|
||||||
@@ -135,16 +125,20 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
|
|||||||
kdfParameters.setUInt32(PARAM_PARALLELISM, UnsignedInt.fromKotlinLong(parallelism))
|
kdfParameters.setUInt32(PARAM_PARALLELISM, UnsignedInt.fromKotlinLong(parallelism))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$type"
|
||||||
|
}
|
||||||
|
|
||||||
override val defaultParallelism: Long
|
override val defaultParallelism: Long
|
||||||
get() = DEFAULT_PARALLELISM.toKotlinLong()
|
get() = DEFAULT_PARALLELISM.toKotlinLong()
|
||||||
|
|
||||||
override val minParallelism: Long
|
override val minParallelism: Long
|
||||||
get() = MIN_PARALLELISM
|
get() = MIN_PARALLELISM.toKotlinLong()
|
||||||
|
|
||||||
override val maxParallelism: Long
|
override val maxParallelism: Long
|
||||||
get() = MAX_PARALLELISM
|
get() = MAX_PARALLELISM.toKotlinLong()
|
||||||
|
|
||||||
enum class Type(val CIPHER_UUID: UUID, @StringRes val nameId: Int) {
|
enum class Type(val CIPHER_UUID: UUID, private val typeName: String) {
|
||||||
ARGON2_D(bytes16ToUuid(
|
ARGON2_D(bytes16ToUuid(
|
||||||
byteArrayOf(0xEF.toByte(),
|
byteArrayOf(0xEF.toByte(),
|
||||||
0x63.toByte(),
|
0x63.toByte(),
|
||||||
@@ -161,7 +155,7 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
|
|||||||
0x03.toByte(),
|
0x03.toByte(),
|
||||||
0xE3.toByte(),
|
0xE3.toByte(),
|
||||||
0x0A.toByte(),
|
0x0A.toByte(),
|
||||||
0x0C.toByte())), R.string.kdf_Argon2d),
|
0x0C.toByte())), "Argon2d"),
|
||||||
ARGON2_ID(bytes16ToUuid(
|
ARGON2_ID(bytes16ToUuid(
|
||||||
byteArrayOf(0x9E.toByte(),
|
byteArrayOf(0x9E.toByte(),
|
||||||
0x29.toByte(),
|
0x29.toByte(),
|
||||||
@@ -178,7 +172,11 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
|
|||||||
0xC6.toByte(),
|
0xC6.toByte(),
|
||||||
0xF0.toByte(),
|
0xF0.toByte(),
|
||||||
0xA1.toByte(),
|
0xA1.toByte(),
|
||||||
0xE6.toByte())), R.string.kdf_Argon2id);
|
0xE6.toByte())), "Argon2id");
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return typeName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -194,21 +192,17 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
|
|||||||
private val MIN_VERSION = UnsignedInt(0x10)
|
private val MIN_VERSION = UnsignedInt(0x10)
|
||||||
private val MAX_VERSION = UnsignedInt(0x13)
|
private val MAX_VERSION = UnsignedInt(0x13)
|
||||||
|
|
||||||
private const val MIN_SALT = 8
|
private val DEFAULT_ITERATIONS = UnsignedLong(2L)
|
||||||
private val MAX_SALT = UnsignedInt.MAX_VALUE.toKotlinLong()
|
private val MIN_ITERATIONS = UnsignedLong(1L)
|
||||||
|
private val MAX_ITERATIONS = UnsignedLong(4294967295L)
|
||||||
|
|
||||||
private const val MIN_ITERATIONS: Long = 1L
|
private val DEFAULT_MEMORY = UnsignedLong((1024L * 1024L))
|
||||||
private const val MAX_ITERATIONS = 4294967295L
|
private val MIN_MEMORY = UnsignedLong(1024L * 8L)
|
||||||
|
|
||||||
private const val MIN_MEMORY = (1024 * 8).toLong()
|
|
||||||
private val MAX_MEMORY = UnsignedInt.MAX_VALUE.toKotlinLong()
|
private val MAX_MEMORY = UnsignedInt.MAX_VALUE.toKotlinLong()
|
||||||
private const val MEMORY_BLOCK_SIZE: Long = 1024L
|
private const val MEMORY_BLOCK_SIZE: Long = 1024L
|
||||||
|
|
||||||
private const val MIN_PARALLELISM: Long = 1L
|
|
||||||
private const val MAX_PARALLELISM: Long = ((1 shl 24) - 1).toLong()
|
|
||||||
|
|
||||||
private const val DEFAULT_ITERATIONS: Long = 2L
|
|
||||||
private const val DEFAULT_MEMORY = (1024 * 1024).toLong()
|
|
||||||
private val DEFAULT_PARALLELISM = UnsignedInt(2)
|
private val DEFAULT_PARALLELISM = UnsignedInt(2)
|
||||||
|
private val MIN_PARALLELISM = UnsignedInt.fromKotlinLong(1L)
|
||||||
|
private val MAX_PARALLELISM = UnsignedInt.fromKotlinLong(((1 shl 24) - 1))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,17 +17,15 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.keyDerivation
|
package com.kunzisoft.keepass.database.crypto.kdf
|
||||||
|
|
||||||
import com.kunzisoft.keepass.utils.ObjectNameResource
|
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
|
|
||||||
// TODO Parcelable
|
// TODO Parcelable
|
||||||
abstract class KdfEngine : ObjectNameResource, Serializable {
|
abstract class KdfEngine : Serializable {
|
||||||
|
|
||||||
var uuid: UUID? = null
|
var uuid: UUID? = null
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.keyDerivation
|
package com.kunzisoft.keepass.database.crypto.kdf
|
||||||
|
|
||||||
object KdfFactory {
|
object KdfFactory {
|
||||||
var aesKdf = AesKdf()
|
var aesKdf = AesKdf()
|
||||||
@@ -17,11 +17,11 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.keyDerivation
|
package com.kunzisoft.keepass.database.crypto.kdf
|
||||||
|
|
||||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
import com.kunzisoft.keepass.utils.bytes16ToUuid
|
||||||
import com.kunzisoft.keepass.stream.uuidTo16Bytes
|
import com.kunzisoft.keepass.utils.uuidTo16Bytes
|
||||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
import com.kunzisoft.keepass.database.crypto.VariantDictionary
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -21,8 +21,8 @@ package com.kunzisoft.keepass.database.element
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryByte
|
import com.kunzisoft.keepass.database.element.binary.BinaryByte
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryData
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
|
|
||||||
|
|
||||||
data class Attachment(var name: String,
|
data class Attachment(var name: String,
|
||||||
|
|||||||
@@ -23,19 +23,27 @@ import android.content.ContentResolver
|
|||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.utils.readBytes4ToUInt
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
import com.kunzisoft.keepass.database.element.database.*
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryCache
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||||
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||||
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconsManager
|
import com.kunzisoft.keepass.database.element.icon.IconsManager
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
|
||||||
import com.kunzisoft.keepass.database.exception.*
|
import com.kunzisoft.keepass.database.exception.*
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||||
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
|
||||||
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB
|
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB
|
||||||
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX
|
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX
|
||||||
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
|
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
|
||||||
@@ -44,15 +52,11 @@ import com.kunzisoft.keepass.database.search.SearchHelper
|
|||||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.stream.readBytes4ToUInt
|
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolder
|
import com.kunzisoft.keepass.utils.SingletonHolder
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.security.Key
|
|
||||||
import java.security.SecureRandom
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.crypto.KeyGenerator
|
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
|
||||||
@@ -70,7 +74,7 @@ class Database {
|
|||||||
var isReadOnly = false
|
var isReadOnly = false
|
||||||
|
|
||||||
val iconDrawableFactory = IconDrawableFactory(
|
val iconDrawableFactory = IconDrawableFactory(
|
||||||
{ loadedCipherKey },
|
{ binaryCache },
|
||||||
{ iconId -> iconsManager.getBinaryForCustomIcon(iconId) }
|
{ iconId -> iconsManager.getBinaryForCustomIcon(iconId) }
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -92,18 +96,18 @@ class Database {
|
|||||||
* Cipher key regenerated when the database is loaded and closed
|
* Cipher key regenerated when the database is loaded and closed
|
||||||
* Can be used to temporarily store database elements
|
* Can be used to temporarily store database elements
|
||||||
*/
|
*/
|
||||||
var loadedCipherKey: LoadedKey?
|
var binaryCache: BinaryCache
|
||||||
private set(value) {
|
private set(value) {
|
||||||
mDatabaseKDB?.loadedCipherKey = value
|
mDatabaseKDB?.binaryCache = value
|
||||||
mDatabaseKDBX?.loadedCipherKey = value
|
mDatabaseKDBX?.binaryCache = value
|
||||||
}
|
}
|
||||||
get() {
|
get() {
|
||||||
return mDatabaseKDB?.loadedCipherKey ?: mDatabaseKDBX?.loadedCipherKey
|
return mDatabaseKDB?.binaryCache ?: mDatabaseKDBX?.binaryCache ?: BinaryCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val iconsManager: IconsManager
|
private val iconsManager: IconsManager
|
||||||
get() {
|
get() {
|
||||||
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager()
|
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager(binaryCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun doForEachStandardIcons(action: (IconImageStandard) -> Unit) {
|
fun doForEachStandardIcons(action: (IconImageStandard) -> Unit) {
|
||||||
@@ -125,9 +129,8 @@ class Database {
|
|||||||
return iconsManager.getIcon(iconId)
|
return iconsManager.getIcon(iconId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildNewCustomIcon(cacheDirectory: File,
|
fun buildNewCustomIcon(result: (IconImageCustom?, BinaryData?) -> Unit) {
|
||||||
result: (IconImageCustom?, BinaryData?) -> Unit) {
|
mDatabaseKDBX?.buildNewCustomIcon(null, result)
|
||||||
mDatabaseKDBX?.buildNewCustomIcon(cacheDirectory, null, result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {
|
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {
|
||||||
@@ -136,7 +139,7 @@ class Database {
|
|||||||
|
|
||||||
fun removeCustomIcon(customIcon: IconImageCustom) {
|
fun removeCustomIcon(customIcon: IconImageCustom) {
|
||||||
iconDrawableFactory.clearFromCache(customIcon)
|
iconDrawableFactory.clearFromCache(customIcon)
|
||||||
iconsManager.removeCustomIcon(customIcon.uuid)
|
iconsManager.removeCustomIcon(binaryCache, customIcon.uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
val allowName: Boolean
|
val allowName: Boolean
|
||||||
@@ -219,7 +222,7 @@ class Database {
|
|||||||
// Default compression not necessary if stored in header
|
// Default compression not necessary if stored in header
|
||||||
mDatabaseKDBX?.let {
|
mDatabaseKDBX?.let {
|
||||||
return it.compressionAlgorithm == CompressionAlgorithm.GZip
|
return it.compressionAlgorithm == CompressionAlgorithm.GZip
|
||||||
&& it.kdbxVersion.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()
|
&& it.kdbxVersion.isBefore(FILE_VERSION_32_4)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -232,12 +235,9 @@ class Database {
|
|||||||
val allowNoMasterKey: Boolean
|
val allowNoMasterKey: Boolean
|
||||||
get() = mDatabaseKDBX != null
|
get() = mDatabaseKDBX != null
|
||||||
|
|
||||||
val allowEncryptionAlgorithmModification: Boolean
|
fun getEncryptionAlgorithmName(): String {
|
||||||
get() = availableEncryptionAlgorithms.size > 1
|
return mDatabaseKDB?.encryptionAlgorithm?.toString()
|
||||||
|
?: mDatabaseKDBX?.encryptionAlgorithm?.toString()
|
||||||
fun getEncryptionAlgorithmName(resources: Resources): String {
|
|
||||||
return mDatabaseKDB?.encryptionAlgorithm?.getName(resources)
|
|
||||||
?: mDatabaseKDBX?.encryptionAlgorithm?.getName(resources)
|
|
||||||
?: ""
|
?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +250,7 @@ class Database {
|
|||||||
algorithm?.let {
|
algorithm?.let {
|
||||||
mDatabaseKDBX?.encryptionAlgorithm = algorithm
|
mDatabaseKDBX?.encryptionAlgorithm = algorithm
|
||||||
mDatabaseKDBX?.setDataEngine(algorithm.cipherEngine)
|
mDatabaseKDBX?.setDataEngine(algorithm.cipherEngine)
|
||||||
mDatabaseKDBX?.dataCipher = algorithm.dataCipher
|
mDatabaseKDBX?.cipherUuid = algorithm.uuid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,8 +272,8 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getKeyDerivationName(resources: Resources): String {
|
fun getKeyDerivationName(): String {
|
||||||
return kdfEngine?.getName(resources) ?: ""
|
return kdfEngine?.toString() ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var numberKeyEncryptionRounds: Long
|
var numberKeyEncryptionRounds: Long
|
||||||
@@ -381,25 +381,12 @@ class Database {
|
|||||||
|
|
||||||
fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
|
fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
|
||||||
val newDatabase = DatabaseKDBX(databaseName, rootName)
|
val newDatabase = DatabaseKDBX(databaseName, rootName)
|
||||||
newDatabase.loadedCipherKey = LoadedKey.generateNewCipherKey()
|
|
||||||
setDatabaseKDBX(newDatabase)
|
setDatabaseKDBX(newDatabase)
|
||||||
this.fileUri = databaseUri
|
this.fileUri = databaseUri
|
||||||
// Set Database state
|
// Set Database state
|
||||||
this.loaded = true
|
this.loaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
class LoadedKey(val key: Key, val iv: ByteArray): Serializable {
|
|
||||||
companion object {
|
|
||||||
const val BINARY_CIPHER = "Blowfish/CBC/PKCS5Padding"
|
|
||||||
|
|
||||||
fun generateNewCipherKey(): LoadedKey {
|
|
||||||
val iv = ByteArray(8)
|
|
||||||
SecureRandom().nextBytes(iv)
|
|
||||||
return LoadedKey(KeyGenerator.getInstance("Blowfish").generateKey(), iv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri,
|
private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri,
|
||||||
openDatabaseKDB: (InputStream) -> DatabaseKDB,
|
openDatabaseKDB: (InputStream) -> DatabaseKDB,
|
||||||
@@ -453,6 +440,7 @@ class Database {
|
|||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
contentResolver: ContentResolver,
|
contentResolver: ContentResolver,
|
||||||
cacheDirectory: File,
|
cacheDirectory: File,
|
||||||
|
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||||
tempCipherKey: LoadedKey,
|
tempCipherKey: LoadedKey,
|
||||||
fixDuplicateUUID: Boolean,
|
fixDuplicateUUID: Boolean,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||||
@@ -474,7 +462,7 @@ class Database {
|
|||||||
// Read database stream for the first time
|
// Read database stream for the first time
|
||||||
readDatabaseStream(contentResolver, uri,
|
readDatabaseStream(contentResolver, uri,
|
||||||
{ databaseInputStream ->
|
{ databaseInputStream ->
|
||||||
DatabaseInputKDB(cacheDirectory)
|
DatabaseInputKDB(cacheDirectory, isRAMSufficient)
|
||||||
.openDatabase(databaseInputStream,
|
.openDatabase(databaseInputStream,
|
||||||
mainCredential.masterPassword,
|
mainCredential.masterPassword,
|
||||||
keyFileInputStream,
|
keyFileInputStream,
|
||||||
@@ -483,7 +471,7 @@ class Database {
|
|||||||
fixDuplicateUUID)
|
fixDuplicateUUID)
|
||||||
},
|
},
|
||||||
{ databaseInputStream ->
|
{ databaseInputStream ->
|
||||||
DatabaseInputKDBX(cacheDirectory)
|
DatabaseInputKDBX(cacheDirectory, isRAMSufficient)
|
||||||
.openDatabase(databaseInputStream,
|
.openDatabase(databaseInputStream,
|
||||||
mainCredential.masterPassword,
|
mainCredential.masterPassword,
|
||||||
keyFileInputStream,
|
keyFileInputStream,
|
||||||
@@ -507,6 +495,7 @@ class Database {
|
|||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
fun reloadData(contentResolver: ContentResolver,
|
fun reloadData(contentResolver: ContentResolver,
|
||||||
cacheDirectory: File,
|
cacheDirectory: File,
|
||||||
|
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||||
tempCipherKey: LoadedKey,
|
tempCipherKey: LoadedKey,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||||
|
|
||||||
@@ -515,14 +504,14 @@ class Database {
|
|||||||
fileUri?.let { oldDatabaseUri ->
|
fileUri?.let { oldDatabaseUri ->
|
||||||
readDatabaseStream(contentResolver, oldDatabaseUri,
|
readDatabaseStream(contentResolver, oldDatabaseUri,
|
||||||
{ databaseInputStream ->
|
{ databaseInputStream ->
|
||||||
DatabaseInputKDB(cacheDirectory)
|
DatabaseInputKDB(cacheDirectory, isRAMSufficient)
|
||||||
.openDatabase(databaseInputStream,
|
.openDatabase(databaseInputStream,
|
||||||
masterKey,
|
masterKey,
|
||||||
tempCipherKey,
|
tempCipherKey,
|
||||||
progressTaskUpdater)
|
progressTaskUpdater)
|
||||||
},
|
},
|
||||||
{ databaseInputStream ->
|
{ databaseInputStream ->
|
||||||
DatabaseInputKDBX(cacheDirectory)
|
DatabaseInputKDBX(cacheDirectory, isRAMSufficient)
|
||||||
.openDatabase(databaseInputStream,
|
.openDatabase(databaseInputStream,
|
||||||
masterKey,
|
masterKey,
|
||||||
tempCipherKey,
|
tempCipherKey,
|
||||||
@@ -576,8 +565,7 @@ class Database {
|
|||||||
|
|
||||||
val attachmentPool: AttachmentPool
|
val attachmentPool: AttachmentPool
|
||||||
get() {
|
get() {
|
||||||
// Binary pool is functionally only in KDBX
|
return mDatabaseKDB?.attachmentPool ?: mDatabaseKDBX?.attachmentPool ?: AttachmentPool(binaryCache)
|
||||||
return mDatabaseKDBX?.binaryPool ?: AttachmentPool()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val allowMultipleAttachments: Boolean
|
val allowMultipleAttachments: Boolean
|
||||||
@@ -589,11 +577,10 @@ class Database {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildNewBinaryAttachment(cacheDirectory: File,
|
fun buildNewBinaryAttachment(compressed: Boolean = false,
|
||||||
compressed: Boolean = false,
|
|
||||||
protected: Boolean = false): BinaryData? {
|
protected: Boolean = false): BinaryData? {
|
||||||
return mDatabaseKDB?.buildNewAttachment(cacheDirectory)
|
return mDatabaseKDB?.buildNewAttachment()
|
||||||
?: mDatabaseKDBX?.buildNewAttachment(cacheDirectory, compressed, protected)
|
?: mDatabaseKDBX?.buildNewAttachment( false, compressed, protected)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAttachmentIfNotUsed(attachment: Attachment) {
|
fun removeAttachmentIfNotUsed(attachment: Attachment) {
|
||||||
@@ -668,6 +655,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun clear(filesDirectory: File? = null) {
|
fun clear(filesDirectory: File? = null) {
|
||||||
|
binaryCache.clear()
|
||||||
iconsManager.clearCache()
|
iconsManager.clearCache()
|
||||||
iconDrawableFactory.clearCache()
|
iconDrawableFactory.clearCache()
|
||||||
// Delete the cache of the database if present
|
// Delete the cache of the database if present
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.database.element
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.database.AttachmentPool
|
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
@@ -311,7 +311,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
|
|
||||||
fun getAttachments(attachmentPool: AttachmentPool, inHistory: Boolean = false): List<Attachment> {
|
fun getAttachments(attachmentPool: AttachmentPool, inHistory: Boolean = false): List<Attachment> {
|
||||||
val attachments = ArrayList<Attachment>()
|
val attachments = ArrayList<Attachment>()
|
||||||
entryKDB?.getAttachment()?.let {
|
entryKDB?.getAttachment(attachmentPool)?.let {
|
||||||
attachments.add(it)
|
attachments.add(it)
|
||||||
}
|
}
|
||||||
entryKDBX?.getAttachments(attachmentPool, inHistory)?.let {
|
entryKDBX?.getAttachments(attachmentPool, inHistory)?.let {
|
||||||
@@ -336,7 +336,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun putAttachment(attachment: Attachment, attachmentPool: AttachmentPool) {
|
private fun putAttachment(attachment: Attachment, attachmentPool: AttachmentPool) {
|
||||||
entryKDB?.putAttachment(attachment)
|
entryKDB?.putAttachment(attachment, attachmentPool)
|
||||||
entryKDBX?.putAttachment(attachment, attachmentPool)
|
entryKDBX?.putAttachment(attachment, attachmentPool)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,16 +466,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
companion object CREATOR : Parcelable.Creator<Entry> {
|
|
||||||
override fun createFromParcel(parcel: Parcel): Entry {
|
|
||||||
return Entry(parcel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<Entry?> {
|
|
||||||
return arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
const val PMS_TAN_ENTRY = "<TAN>"
|
const val PMS_TAN_ENTRY = "<TAN>"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -484,5 +475,16 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
fun newExtraFieldNameAllowed(field: Field): Boolean {
|
fun newExtraFieldNameAllowed(field: Field): Boolean {
|
||||||
return EntryKDBX.newCustomNameAllowed(field.name)
|
return EntryKDBX.newCustomNameAllowed(field.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val CREATOR: Parcelable.Creator<Entry> = object : Parcelable.Creator<Entry> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): Entry {
|
||||||
|
return Entry(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<Entry?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.element.database
|
package com.kunzisoft.keepass.database.element.binary
|
||||||
|
|
||||||
class AttachmentPool : BinaryPool<Int>() {
|
class AttachmentPool(binaryCache: BinaryCache) : BinaryPool<Int>(binaryCache) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility method to find an unused key in the pool
|
* Utility method to find an unused key in the pool
|
||||||
@@ -17,51 +17,62 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.element.database
|
package com.kunzisoft.keepass.database.element.binary
|
||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import android.util.Base64
|
||||||
import com.kunzisoft.keepass.stream.readAllBytes
|
import android.util.Base64InputStream
|
||||||
|
import android.util.Base64OutputStream
|
||||||
|
import com.kunzisoft.keepass.utils.readAllBytes
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryCache.Companion.UNKNOWN
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.util.zip.GZIPOutputStream
|
import java.util.zip.GZIPOutputStream
|
||||||
|
|
||||||
class BinaryByte : BinaryData {
|
class BinaryByte : BinaryData {
|
||||||
|
|
||||||
private var mDataByte: ByteArray = ByteArray(0)
|
private var mDataByteId: String
|
||||||
|
|
||||||
/**
|
private fun getByteArray(binaryCache: BinaryCache): ByteArray {
|
||||||
* Empty protected binary
|
val keyData = binaryCache.getByteArray(mDataByteId)
|
||||||
*/
|
mDataByteId = keyData.key
|
||||||
constructor() : super()
|
return keyData.data
|
||||||
|
}
|
||||||
|
|
||||||
constructor(byteArray: ByteArray,
|
constructor() : super() {
|
||||||
|
mDataByteId = UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(id: String,
|
||||||
compressed: Boolean = false,
|
compressed: Boolean = false,
|
||||||
protected: Boolean = false) : super(compressed, protected) {
|
protected: Boolean = false) : super(compressed, protected) {
|
||||||
this.mDataByte = byteArray
|
mDataByteId = id
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(parcel: Parcel) : super(parcel) {
|
constructor(parcel: Parcel) : super(parcel) {
|
||||||
val byteArray = ByteArray(parcel.readInt())
|
mDataByteId = parcel.readString() ?: UNKNOWN
|
||||||
parcel.readByteArray(byteArray)
|
}
|
||||||
mDataByte = byteArray
|
|
||||||
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
|
super.writeToParcel(dest, flags)
|
||||||
|
dest.writeString(mDataByteId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun getInputDataStream(cipherKey: Database.LoadedKey): InputStream {
|
override fun getInputDataStream(binaryCache: BinaryCache): InputStream {
|
||||||
return ByteArrayInputStream(mDataByte)
|
return Base64InputStream(ByteArrayInputStream(getByteArray(binaryCache)), Base64.NO_WRAP)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun getOutputDataStream(cipherKey: Database.LoadedKey): OutputStream {
|
override fun getOutputDataStream(binaryCache: BinaryCache): OutputStream {
|
||||||
return ByteOutputStream()
|
return BinaryCountingOutputStream(Base64OutputStream(ByteOutputStream(binaryCache), Base64.NO_WRAP))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun compress(cipherKey: Database.LoadedKey) {
|
override fun compress(binaryCache: BinaryCache) {
|
||||||
if (!isCompressed) {
|
if (!isCompressed) {
|
||||||
GZIPOutputStream(getOutputDataStream(cipherKey)).use { outputStream ->
|
GZIPOutputStream(getOutputDataStream(binaryCache)).use { outputStream ->
|
||||||
getInputDataStream(cipherKey).use { inputStream ->
|
getInputDataStream(binaryCache).use { inputStream ->
|
||||||
inputStream.readAllBytes { buffer ->
|
inputStream.readAllBytes { buffer ->
|
||||||
outputStream.write(buffer)
|
outputStream.write(buffer)
|
||||||
}
|
}
|
||||||
@@ -72,10 +83,10 @@ class BinaryByte : BinaryData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun decompress(cipherKey: Database.LoadedKey) {
|
override fun decompress(binaryCache: BinaryCache) {
|
||||||
if (isCompressed) {
|
if (isCompressed) {
|
||||||
getUnGzipInputDataStream(cipherKey).use { inputStream ->
|
getUnGzipInputDataStream(binaryCache).use { inputStream ->
|
||||||
getOutputDataStream(cipherKey).use { outputStream ->
|
getOutputDataStream(binaryCache).use { outputStream ->
|
||||||
inputStream.readAllBytes { buffer ->
|
inputStream.readAllBytes { buffer ->
|
||||||
outputStream.write(buffer)
|
outputStream.write(buffer)
|
||||||
}
|
}
|
||||||
@@ -86,36 +97,8 @@ class BinaryByte : BinaryData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun clear() {
|
override fun clear(binaryCache: BinaryCache) {
|
||||||
mDataByte = ByteArray(0)
|
binaryCache.removeByteArray(mDataByteId)
|
||||||
}
|
|
||||||
|
|
||||||
override fun dataExists(): Boolean {
|
|
||||||
return mDataByte.isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getSize(): Long {
|
|
||||||
return mDataByte.size.toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hash of the raw encrypted file in temp folder, only to compare binary data
|
|
||||||
*/
|
|
||||||
override fun binaryHash(): Int {
|
|
||||||
return if (dataExists())
|
|
||||||
mDataByte.contentHashCode()
|
|
||||||
else
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return mDataByte.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
|
||||||
super.writeToParcel(dest, flags)
|
|
||||||
dest.writeInt(mDataByte.size)
|
|
||||||
dest.writeByteArray(mDataByte)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
@@ -123,31 +106,29 @@ class BinaryByte : BinaryData {
|
|||||||
if (other !is BinaryByte) return false
|
if (other !is BinaryByte) return false
|
||||||
if (!super.equals(other)) return false
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
if (!mDataByte.contentEquals(other.mDataByte)) return false
|
if (mDataByteId != other.mDataByteId) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = super.hashCode()
|
var result = super.hashCode()
|
||||||
result = 31 * result + mDataByte.contentHashCode()
|
result = 31 * result + mDataByteId.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom OutputStream to calculate the size and hash of binary file
|
* Custom OutputStream to calculate the size and hash of binary file
|
||||||
*/
|
*/
|
||||||
private inner class ByteOutputStream : ByteArrayOutputStream() {
|
private inner class ByteOutputStream(private val binaryCache: BinaryCache) : ByteArrayOutputStream() {
|
||||||
override fun close() {
|
override fun close() {
|
||||||
mDataByte = this.toByteArray()
|
binaryCache.setByteArray(mDataByteId, this.toByteArray())
|
||||||
super.close()
|
super.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val TAG = BinaryByte::class.java.name
|
private val TAG = BinaryByte::class.java.name
|
||||||
const val MAX_BINARY_BYTES = 10240
|
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val CREATOR: Parcelable.Creator<BinaryByte> = object : Parcelable.Creator<BinaryByte> {
|
val CREATOR: Parcelable.Creator<BinaryByte> = object : Parcelable.Creator<BinaryByte> {
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.kunzisoft.keepass.database.element.binary
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class BinaryCache {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cipher key generated when the database is loaded, and destroyed when the database is closed
|
||||||
|
* Can be used to temporarily store database elements
|
||||||
|
*/
|
||||||
|
var loadedCipherKey: LoadedKey = LoadedKey.generateNewCipherKey()
|
||||||
|
|
||||||
|
var cacheDirectory: File? = null
|
||||||
|
|
||||||
|
private val voidBinary = KeyByteArray(UNKNOWN, ByteArray(0))
|
||||||
|
|
||||||
|
fun getBinaryData(binaryId: String,
|
||||||
|
smallSize: Boolean = false,
|
||||||
|
compression: Boolean = false,
|
||||||
|
protection: Boolean = false): BinaryData {
|
||||||
|
val cacheDir = cacheDirectory
|
||||||
|
return if (smallSize || cacheDir == null) {
|
||||||
|
BinaryByte(binaryId, compression, protection)
|
||||||
|
} else {
|
||||||
|
val fileInCache = File(cacheDir, binaryId)
|
||||||
|
BinaryFile(fileInCache, compression, protection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similar to file storage but much faster TODO SparseArray
|
||||||
|
private val byteArrayList = HashMap<String, ByteArray>()
|
||||||
|
|
||||||
|
fun getByteArray(key: String): KeyByteArray {
|
||||||
|
if (key == UNKNOWN) {
|
||||||
|
return voidBinary
|
||||||
|
}
|
||||||
|
if (!byteArrayList.containsKey(key)) {
|
||||||
|
val newItem = KeyByteArray(key, ByteArray(0))
|
||||||
|
byteArrayList[newItem.key] = newItem.data
|
||||||
|
return newItem
|
||||||
|
}
|
||||||
|
return KeyByteArray(key, byteArrayList[key]!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setByteArray(key: String, data: ByteArray): KeyByteArray {
|
||||||
|
if (key == UNKNOWN) {
|
||||||
|
return voidBinary
|
||||||
|
}
|
||||||
|
byteArrayList[key] = data
|
||||||
|
return KeyByteArray(key, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeByteArray(key: String?) {
|
||||||
|
key?.let {
|
||||||
|
byteArrayList.remove(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
byteArrayList.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val UNKNOWN = "UNKNOWN"
|
||||||
|
}
|
||||||
|
|
||||||
|
data class KeyByteArray(val key: String, val data: ByteArray) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is KeyByteArray) return false
|
||||||
|
|
||||||
|
if (key != other.key) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return key.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX 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.
|
||||||
|
*
|
||||||
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.database.element.binary
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import org.apache.commons.io.output.CountingOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.util.zip.GZIPInputStream
|
||||||
|
import java.util.zip.GZIPOutputStream
|
||||||
|
|
||||||
|
abstract class BinaryData : Parcelable {
|
||||||
|
|
||||||
|
var isCompressed: Boolean = false
|
||||||
|
protected set
|
||||||
|
var isProtected: Boolean = false
|
||||||
|
protected set
|
||||||
|
var isCorrupted: Boolean = false
|
||||||
|
private var mLength: Long = 0
|
||||||
|
private var mBinaryHash = 0
|
||||||
|
|
||||||
|
protected constructor(compressed: Boolean = false, protected: Boolean = false) {
|
||||||
|
this.isCompressed = compressed
|
||||||
|
this.isProtected = protected
|
||||||
|
this.mLength = 0
|
||||||
|
this.mBinaryHash = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
protected constructor(parcel: Parcel) {
|
||||||
|
isCompressed = parcel.readByte().toInt() != 0
|
||||||
|
isProtected = parcel.readByte().toInt() != 0
|
||||||
|
isCorrupted = parcel.readByte().toInt() != 0
|
||||||
|
mLength = parcel.readLong()
|
||||||
|
mBinaryHash = parcel.readInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
|
dest.writeByte((if (isCompressed) 1 else 0).toByte())
|
||||||
|
dest.writeByte((if (isProtected) 1 else 0).toByte())
|
||||||
|
dest.writeByte((if (isCorrupted) 1 else 0).toByte())
|
||||||
|
dest.writeLong(mLength)
|
||||||
|
dest.writeInt(mBinaryHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
abstract fun getInputDataStream(binaryCache: BinaryCache): InputStream
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
abstract fun getOutputDataStream(binaryCache: BinaryCache): OutputStream
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getUnGzipInputDataStream(binaryCache: BinaryCache): InputStream {
|
||||||
|
return if (isCompressed) {
|
||||||
|
GZIPInputStream(getInputDataStream(binaryCache))
|
||||||
|
} else {
|
||||||
|
getInputDataStream(binaryCache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getGzipOutputDataStream(binaryCache: BinaryCache): OutputStream {
|
||||||
|
return if (isCompressed) {
|
||||||
|
GZIPOutputStream(getOutputDataStream(binaryCache))
|
||||||
|
} else {
|
||||||
|
getOutputDataStream(binaryCache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
abstract fun compress(binaryCache: BinaryCache)
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
abstract fun decompress(binaryCache: BinaryCache)
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun dataExists(): Boolean {
|
||||||
|
return mLength > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getSize(): Long {
|
||||||
|
return mLength
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun binaryHash(): Int {
|
||||||
|
return mBinaryHash
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
abstract fun clear(binaryCache: BinaryCache)
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is BinaryData) return false
|
||||||
|
|
||||||
|
if (isCompressed != other.isCompressed) return false
|
||||||
|
if (isProtected != other.isProtected) return false
|
||||||
|
if (isCorrupted != other.isCorrupted) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = isCompressed.hashCode()
|
||||||
|
result = 31 * result + isProtected.hashCode()
|
||||||
|
result = 31 * result + isCorrupted.hashCode()
|
||||||
|
result = 31 * result + mLength.hashCode()
|
||||||
|
result = 31 * result + mBinaryHash
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom OutputStream to calculate the size and hash of binary file
|
||||||
|
*/
|
||||||
|
protected inner class BinaryCountingOutputStream(out: OutputStream): CountingOutputStream(out) {
|
||||||
|
|
||||||
|
private val mMessageDigest: MessageDigest
|
||||||
|
init {
|
||||||
|
mLength = 0
|
||||||
|
mMessageDigest = MessageDigest.getInstance("MD5")
|
||||||
|
mBinaryHash = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeWrite(n: Int) {
|
||||||
|
super.beforeWrite(n)
|
||||||
|
mLength = byteCount
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(idx: Int) {
|
||||||
|
super.write(idx)
|
||||||
|
mMessageDigest.update(idx.toByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(bts: ByteArray) {
|
||||||
|
super.write(bts)
|
||||||
|
mMessageDigest.update(bts)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(bts: ByteArray, st: Int, end: Int) {
|
||||||
|
super.write(bts, st, end)
|
||||||
|
mMessageDigest.update(bts, st, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
super.close()
|
||||||
|
mLength = byteCount
|
||||||
|
val bytes = mMessageDigest.digest()
|
||||||
|
mBinaryHash = ByteBuffer.wrap(bytes).int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = BinaryData::class.java.name
|
||||||
|
|
||||||
|
fun canMemoryBeAllocatedInRAM(context: Context, memoryWanted: Long): Boolean {
|
||||||
|
val memoryInfo = ActivityManager.MemoryInfo()
|
||||||
|
(context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo(memoryInfo)
|
||||||
|
val availableMemory = memoryInfo.availMem
|
||||||
|
return availableMemory > memoryWanted * 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -17,19 +17,15 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.element.database
|
package com.kunzisoft.keepass.database.element.binary
|
||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Base64InputStream
|
import android.util.Base64InputStream
|
||||||
import android.util.Base64OutputStream
|
import android.util.Base64OutputStream
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.utils.readAllBytes
|
||||||
import com.kunzisoft.keepass.stream.readAllBytes
|
|
||||||
import org.apache.commons.io.output.CountingOutputStream
|
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.util.zip.GZIPOutputStream
|
import java.util.zip.GZIPOutputStream
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.CipherInputStream
|
import javax.crypto.CipherInputStream
|
||||||
@@ -39,44 +35,43 @@ import javax.crypto.spec.IvParameterSpec
|
|||||||
class BinaryFile : BinaryData {
|
class BinaryFile : BinaryData {
|
||||||
|
|
||||||
private var mDataFile: File? = null
|
private var mDataFile: File? = null
|
||||||
private var mLength: Long = 0
|
|
||||||
private var mBinaryHash = 0
|
|
||||||
// Cipher to encrypt temp file
|
// Cipher to encrypt temp file
|
||||||
@Transient
|
@Transient
|
||||||
private var cipherEncryption: Cipher = Cipher.getInstance(Database.LoadedKey.BINARY_CIPHER)
|
private var cipherEncryption: Cipher = Cipher.getInstance(LoadedKey.BINARY_CIPHER)
|
||||||
@Transient
|
@Transient
|
||||||
private var cipherDecryption: Cipher = Cipher.getInstance(Database.LoadedKey.BINARY_CIPHER)
|
private var cipherDecryption: Cipher = Cipher.getInstance(LoadedKey.BINARY_CIPHER)
|
||||||
|
|
||||||
constructor() : super()
|
|
||||||
|
|
||||||
constructor(dataFile: File,
|
constructor(dataFile: File,
|
||||||
compressed: Boolean = false,
|
compressed: Boolean = false,
|
||||||
protected: Boolean = false) : super(compressed, protected) {
|
protected: Boolean = false) : super(compressed, protected) {
|
||||||
this.mDataFile = dataFile
|
this.mDataFile = dataFile
|
||||||
this.mLength = 0
|
|
||||||
this.mBinaryHash = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(parcel: Parcel) : super(parcel) {
|
constructor(parcel: Parcel) : super(parcel) {
|
||||||
parcel.readString()?.let {
|
parcel.readString()?.let {
|
||||||
mDataFile = File(it)
|
mDataFile = File(it)
|
||||||
}
|
}
|
||||||
mLength = parcel.readLong()
|
}
|
||||||
mBinaryHash = parcel.readInt()
|
|
||||||
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
|
super.writeToParcel(dest, flags)
|
||||||
|
dest.writeString(mDataFile?.absolutePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun getInputDataStream(cipherKey: Database.LoadedKey): InputStream {
|
override fun getInputDataStream(binaryCache: BinaryCache): InputStream {
|
||||||
return buildInputStream(mDataFile, cipherKey)
|
return buildInputStream(mDataFile, binaryCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun getOutputDataStream(cipherKey: Database.LoadedKey): OutputStream {
|
override fun getOutputDataStream(binaryCache: BinaryCache): OutputStream {
|
||||||
return buildOutputStream(mDataFile, cipherKey)
|
return buildOutputStream(mDataFile, binaryCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun buildInputStream(file: File?, cipherKey: Database.LoadedKey): InputStream {
|
private fun buildInputStream(file: File?, binaryCache: BinaryCache): InputStream {
|
||||||
|
val cipherKey = binaryCache.loadedCipherKey
|
||||||
return when {
|
return when {
|
||||||
file != null && file.length() > 0 -> {
|
file != null && file.length() > 0 -> {
|
||||||
cipherDecryption.init(Cipher.DECRYPT_MODE, cipherKey.key, IvParameterSpec(cipherKey.iv))
|
cipherDecryption.init(Cipher.DECRYPT_MODE, cipherKey.key, IvParameterSpec(cipherKey.iv))
|
||||||
@@ -87,7 +82,8 @@ class BinaryFile : BinaryData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun buildOutputStream(file: File?, cipherKey: Database.LoadedKey): OutputStream {
|
private fun buildOutputStream(file: File?, binaryCache: BinaryCache): OutputStream {
|
||||||
|
val cipherKey = binaryCache.loadedCipherKey
|
||||||
return when {
|
return when {
|
||||||
file != null -> {
|
file != null -> {
|
||||||
cipherEncryption.init(Cipher.ENCRYPT_MODE, cipherKey.key, IvParameterSpec(cipherKey.iv))
|
cipherEncryption.init(Cipher.ENCRYPT_MODE, cipherKey.key, IvParameterSpec(cipherKey.iv))
|
||||||
@@ -98,14 +94,14 @@ class BinaryFile : BinaryData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun compress(cipherKey: Database.LoadedKey) {
|
override fun compress(binaryCache: BinaryCache) {
|
||||||
mDataFile?.let { concreteDataFile ->
|
mDataFile?.let { concreteDataFile ->
|
||||||
// To compress, create a new binary with file
|
// To compress, create a new binary with file
|
||||||
if (!isCompressed) {
|
if (!isCompressed) {
|
||||||
// Encrypt the new gzipped temp file
|
// Encrypt the new gzipped temp file
|
||||||
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
||||||
getInputDataStream(cipherKey).use { inputStream ->
|
getInputDataStream(binaryCache).use { inputStream ->
|
||||||
GZIPOutputStream(buildOutputStream(fileBinaryCompress, cipherKey)).use { outputStream ->
|
GZIPOutputStream(buildOutputStream(fileBinaryCompress, binaryCache)).use { outputStream ->
|
||||||
inputStream.readAllBytes { buffer ->
|
inputStream.readAllBytes { buffer ->
|
||||||
outputStream.write(buffer)
|
outputStream.write(buffer)
|
||||||
}
|
}
|
||||||
@@ -123,13 +119,13 @@ class BinaryFile : BinaryData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun decompress(cipherKey: Database.LoadedKey) {
|
override fun decompress(binaryCache: BinaryCache) {
|
||||||
mDataFile?.let { concreteDataFile ->
|
mDataFile?.let { concreteDataFile ->
|
||||||
if (isCompressed) {
|
if (isCompressed) {
|
||||||
// Encrypt the new ungzipped temp file
|
// Encrypt the new ungzipped temp file
|
||||||
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
||||||
getUnGzipInputDataStream(cipherKey).use { inputStream ->
|
getUnGzipInputDataStream(binaryCache).use { inputStream ->
|
||||||
buildOutputStream(fileBinaryDecompress, cipherKey).use { outputStream ->
|
buildOutputStream(fileBinaryDecompress, binaryCache).use { outputStream ->
|
||||||
inputStream.readAllBytes { buffer ->
|
inputStream.readAllBytes { buffer ->
|
||||||
outputStream.write(buffer)
|
outputStream.write(buffer)
|
||||||
}
|
}
|
||||||
@@ -146,39 +142,15 @@ class BinaryFile : BinaryData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
override fun clear(binaryCache: BinaryCache) {
|
||||||
override fun clear() {
|
|
||||||
if (mDataFile != null && !mDataFile!!.delete())
|
if (mDataFile != null && !mDataFile!!.delete())
|
||||||
throw IOException("Unable to delete temp file " + mDataFile!!.absolutePath)
|
throw IOException("Unable to delete temp file " + mDataFile!!.absolutePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dataExists(): Boolean {
|
|
||||||
return mDataFile != null && mLength > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getSize(): Long {
|
|
||||||
return mLength
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hash of the raw encrypted file in temp folder, only to compare binary data
|
|
||||||
*/
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
override fun binaryHash(): Int {
|
|
||||||
return mBinaryHash
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return mDataFile.toString()
|
return mDataFile.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
|
||||||
super.writeToParcel(dest, flags)
|
|
||||||
dest.writeString(mDataFile?.absolutePath)
|
|
||||||
dest.writeLong(mLength)
|
|
||||||
dest.writeInt(mBinaryHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is BinaryFile) return false
|
if (other !is BinaryFile) return false
|
||||||
@@ -190,53 +162,10 @@ class BinaryFile : BinaryData {
|
|||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = super.hashCode()
|
var result = super.hashCode()
|
||||||
result = 31 * result + (mDataFile?.hashCode() ?: 0)
|
result = 31 * result + (mDataFile?.hashCode() ?: 0)
|
||||||
result = 31 * result + mLength.hashCode()
|
|
||||||
result = 31 * result + mBinaryHash
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom OutputStream to calculate the size and hash of binary file
|
|
||||||
*/
|
|
||||||
private inner class BinaryCountingOutputStream(out: OutputStream): CountingOutputStream(out) {
|
|
||||||
|
|
||||||
private val mMessageDigest: MessageDigest
|
|
||||||
init {
|
|
||||||
mLength = 0
|
|
||||||
mMessageDigest = MessageDigest.getInstance("MD5")
|
|
||||||
mBinaryHash = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun beforeWrite(n: Int) {
|
|
||||||
super.beforeWrite(n)
|
|
||||||
mLength = byteCount
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun write(idx: Int) {
|
|
||||||
super.write(idx)
|
|
||||||
mMessageDigest.update(idx.toByte())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun write(bts: ByteArray) {
|
|
||||||
super.write(bts)
|
|
||||||
mMessageDigest.update(bts)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun write(bts: ByteArray, st: Int, end: Int) {
|
|
||||||
super.write(bts, st, end)
|
|
||||||
mMessageDigest.update(bts, st, end)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
super.close()
|
|
||||||
mLength = byteCount
|
|
||||||
val bytes = mMessageDigest.digest()
|
|
||||||
mBinaryHash = ByteBuffer.wrap(bytes).int
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val TAG = BinaryFile::class.java.name
|
private val TAG = BinaryFile::class.java.name
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
@@ -17,20 +17,19 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.element.database
|
package com.kunzisoft.keepass.database.element.binary
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
abstract class BinaryPool<T> {
|
abstract class BinaryPool<T>(private val mBinaryCache: BinaryCache) {
|
||||||
|
|
||||||
protected val pool = LinkedHashMap<T, BinaryData>()
|
protected val pool = LinkedHashMap<T, BinaryData>()
|
||||||
|
|
||||||
// To build unique file id
|
// To build unique file id
|
||||||
private var creationId: String = System.currentTimeMillis().toString()
|
private var creationId: Long = System.currentTimeMillis()
|
||||||
private var poolId: String = abs(javaClass.simpleName.hashCode()).toString()
|
private var poolId: Int = abs(javaClass.simpleName.hashCode())
|
||||||
private var binaryFileIncrement = 0L
|
private var binaryFileIncrement = 0L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -196,7 +195,7 @@ abstract class BinaryPool<T> {
|
|||||||
* Different from doForEach, provide an ordered index to each binary
|
* Different from doForEach, provide an ordered index to each binary
|
||||||
*/
|
*/
|
||||||
private fun doForEachBinaryWithoutDuplication(action: (keyBinary: KeyBinary<T>) -> Unit,
|
private fun doForEachBinaryWithoutDuplication(action: (keyBinary: KeyBinary<T>) -> Unit,
|
||||||
conditionToAdd: (binary: BinaryData) -> Boolean) {
|
conditionToAdd: (binary: BinaryData) -> Boolean) {
|
||||||
orderedBinariesWithoutDuplication(conditionToAdd).forEach { keyBinary ->
|
orderedBinariesWithoutDuplication(conditionToAdd).forEach { keyBinary ->
|
||||||
action.invoke(keyBinary)
|
action.invoke(keyBinary)
|
||||||
}
|
}
|
||||||
@@ -227,7 +226,7 @@ abstract class BinaryPool<T> {
|
|||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun clear() {
|
fun clear() {
|
||||||
doForEachBinary { _, binary ->
|
doForEachBinary { _, binary ->
|
||||||
binary.clear()
|
binary.clear(mBinaryCache)
|
||||||
}
|
}
|
||||||
pool.clear()
|
pool.clear()
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.kunzisoft.keepass.database.element.database
|
package com.kunzisoft.keepass.database.element.binary
|
||||||
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class CustomIconPool : BinaryPool<UUID>() {
|
class CustomIconPool(binaryCache: BinaryCache) : BinaryPool<UUID>(binaryCache) {
|
||||||
|
|
||||||
override fun findUnusedKey(): UUID {
|
override fun findUnusedKey(): UUID {
|
||||||
var newUUID = UUID.randomUUID()
|
var newUUID = UUID.randomUUID()
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.kunzisoft.keepass.database.element.binary
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
import java.security.Key
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import javax.crypto.KeyGenerator
|
||||||
|
|
||||||
|
class LoadedKey(val key: Key, val iv: ByteArray): Serializable {
|
||||||
|
companion object {
|
||||||
|
const val BINARY_CIPHER = "Blowfish/CBC/PKCS5Padding"
|
||||||
|
|
||||||
|
fun generateNewCipherKey(): LoadedKey {
|
||||||
|
val iv = ByteArray(8)
|
||||||
|
SecureRandom().nextBytes(iv)
|
||||||
|
return LoadedKey(KeyGenerator.getInstance("Blowfish").generateKey(), iv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX 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.
|
|
||||||
*
|
|
||||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.database.element.database
|
|
||||||
|
|
||||||
import android.os.Parcel
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
import java.util.zip.GZIPInputStream
|
|
||||||
import java.util.zip.GZIPOutputStream
|
|
||||||
|
|
||||||
abstract class BinaryData : Parcelable {
|
|
||||||
|
|
||||||
var isCompressed: Boolean = false
|
|
||||||
protected set
|
|
||||||
var isProtected: Boolean = false
|
|
||||||
protected set
|
|
||||||
var isCorrupted: Boolean = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Empty protected binary
|
|
||||||
*/
|
|
||||||
protected constructor()
|
|
||||||
|
|
||||||
protected constructor(compressed: Boolean = false, protected: Boolean = false) {
|
|
||||||
this.isCompressed = compressed
|
|
||||||
this.isProtected = protected
|
|
||||||
}
|
|
||||||
|
|
||||||
protected constructor(parcel: Parcel) {
|
|
||||||
isCompressed = parcel.readByte().toInt() != 0
|
|
||||||
isProtected = parcel.readByte().toInt() != 0
|
|
||||||
isCorrupted = parcel.readByte().toInt() != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
abstract fun getInputDataStream(cipherKey: Database.LoadedKey): InputStream
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
abstract fun getOutputDataStream(cipherKey: Database.LoadedKey): OutputStream
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun getUnGzipInputDataStream(cipherKey: Database.LoadedKey): InputStream {
|
|
||||||
return if (isCompressed) {
|
|
||||||
GZIPInputStream(getInputDataStream(cipherKey))
|
|
||||||
} else {
|
|
||||||
getInputDataStream(cipherKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun getGzipOutputDataStream(cipherKey: Database.LoadedKey): OutputStream {
|
|
||||||
return if (isCompressed) {
|
|
||||||
GZIPOutputStream(getOutputDataStream(cipherKey))
|
|
||||||
} else {
|
|
||||||
getOutputDataStream(cipherKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
abstract fun compress(cipherKey: Database.LoadedKey)
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
abstract fun decompress(cipherKey: Database.LoadedKey)
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
abstract fun clear()
|
|
||||||
|
|
||||||
abstract fun dataExists(): Boolean
|
|
||||||
|
|
||||||
abstract fun getSize(): Long
|
|
||||||
|
|
||||||
abstract fun binaryHash(): Int
|
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
|
||||||
dest.writeByte((if (isCompressed) 1 else 0).toByte())
|
|
||||||
dest.writeByte((if (isProtected) 1 else 0).toByte())
|
|
||||||
dest.writeByte((if (isCorrupted) 1 else 0).toByte())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (other !is BinaryData) return false
|
|
||||||
|
|
||||||
if (isCompressed != other.isCompressed) return false
|
|
||||||
if (isProtected != other.isProtected) return false
|
|
||||||
if (isCorrupted != other.isCorrupted) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = isCompressed.hashCode()
|
|
||||||
result = 31 * result + isProtected.hashCode()
|
|
||||||
result = 31 * result + isCorrupted.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val TAG = BinaryData::class.java.name
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -19,23 +19,20 @@
|
|||||||
|
|
||||||
package com.kunzisoft.keepass.database.element.database
|
package com.kunzisoft.keepass.database.element.database
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.finalkey.AESKeyTransformerFactory
|
import com.kunzisoft.encrypt.HashManager
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.encrypt.aes.AESTransformer
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
|
||||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.security.DigestOutputStream
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
@@ -45,9 +42,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
|
|
||||||
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
||||||
|
|
||||||
// Only to generate unique file name
|
|
||||||
private var binaryPool = AttachmentPool()
|
|
||||||
|
|
||||||
override val version: String
|
override val version: String
|
||||||
get() = "KeePass 1"
|
get() = "KeePass 1"
|
||||||
|
|
||||||
@@ -80,6 +74,7 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
get() {
|
get() {
|
||||||
val list = ArrayList<EncryptionAlgorithm>()
|
val list = ArrayList<EncryptionAlgorithm>()
|
||||||
list.add(EncryptionAlgorithm.AESRijndael)
|
list.add(EncryptionAlgorithm.AESRijndael)
|
||||||
|
list.add(EncryptionAlgorithm.Twofish)
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,24 +140,11 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun makeFinalKey(masterSeed: ByteArray, masterSeed2: ByteArray, numRounds: Long) {
|
fun makeFinalKey(masterSeed: ByteArray, transformSeed: ByteArray, numRounds: Long) {
|
||||||
|
|
||||||
// Write checksum Checksum
|
|
||||||
val messageDigest: MessageDigest
|
|
||||||
try {
|
|
||||||
messageDigest = MessageDigest.getInstance("SHA-256")
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw IOException("SHA-256 not implemented here.")
|
|
||||||
}
|
|
||||||
|
|
||||||
val nos = NullOutputStream()
|
|
||||||
val dos = DigestOutputStream(nos, messageDigest)
|
|
||||||
|
|
||||||
// Encrypt the master key a few times to make brute-force key-search harder
|
// Encrypt the master key a few times to make brute-force key-search harder
|
||||||
dos.write(masterSeed)
|
val transformedKey = AESTransformer.transformKey(transformSeed, masterKey, numRounds) ?: ByteArray(0)
|
||||||
dos.write(AESKeyTransformerFactory.transformMasterKey(masterSeed2, masterKey, numRounds) ?: ByteArray(0))
|
// Write checksum Checksum
|
||||||
|
finalKey = HashManager.hashSha256(masterSeed, transformedKey)
|
||||||
finalKey = messageDigest.digest()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createGroup(): GroupKDB {
|
override fun createGroup(): GroupKDB {
|
||||||
@@ -275,11 +257,10 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
addEntryTo(entry, origParent)
|
addEntryTo(entry, origParent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildNewAttachment(cacheDirectory: File): BinaryData {
|
fun buildNewAttachment(): BinaryData {
|
||||||
// Generate an unique new file
|
// Generate an unique new file
|
||||||
return binaryPool.put { uniqueBinaryId ->
|
return attachmentPool.put { uniqueBinaryId ->
|
||||||
val fileInCache = File(cacheDirectory, uniqueBinaryId)
|
binaryCache.getBinaryData(uniqueBinaryId, false)
|
||||||
BinaryFile(fileInCache)
|
|
||||||
}.binary
|
}.binary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,16 +22,21 @@ package com.kunzisoft.keepass.database.element.database
|
|||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.kunzisoft.encrypt.HashManager
|
||||||
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
import com.kunzisoft.keepass.utils.longTo8Bytes
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.crypto.CryptoUtil
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.AesEngine
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
|
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
|
import com.kunzisoft.keepass.database.crypto.AesEngine
|
||||||
|
import com.kunzisoft.keepass.database.crypto.CipherEngine
|
||||||
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.database.crypto.VariantDictionary
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.DeletedObject
|
import com.kunzisoft.keepass.database.element.DeletedObject
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
@@ -39,33 +44,31 @@ import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
|||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
|
||||||
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
||||||
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_3
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_3
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
|
||||||
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
|
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
|
||||||
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
|
||||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
|
||||||
import org.apache.commons.codec.binary.Hex
|
import org.apache.commons.codec.binary.Hex
|
||||||
import org.w3c.dom.Node
|
import org.w3c.dom.Node
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import javax.crypto.Mac
|
||||||
import javax.xml.XMLConstants
|
import javax.xml.XMLConstants
|
||||||
import javax.xml.parsers.DocumentBuilderFactory
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
import javax.xml.parsers.ParserConfigurationException
|
import javax.xml.parsers.ParserConfigurationException
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
|
||||||
class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||||
|
|
||||||
var hmacKey: ByteArray? = null
|
var hmacKey: ByteArray? = null
|
||||||
private set
|
private set
|
||||||
var dataCipher = AesEngine.CIPHER_UUID
|
var cipherUuid = EncryptionAlgorithm.AESRijndael.uuid
|
||||||
private var dataEngine: CipherEngine = AesEngine()
|
private var dataEngine: CipherEngine = AesEngine()
|
||||||
var compressionAlgorithm = CompressionAlgorithm.GZip
|
var compressionAlgorithm = CompressionAlgorithm.GZip
|
||||||
var kdfParameters: KdfParameters? = null
|
var kdfParameters: KdfParameters? = null
|
||||||
@@ -108,8 +111,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
val deletedObjects = ArrayList<DeletedObject>()
|
val deletedObjects = ArrayList<DeletedObject>()
|
||||||
val customData = HashMap<String, String>()
|
val customData = HashMap<String, String>()
|
||||||
|
|
||||||
var binaryPool = AttachmentPool()
|
|
||||||
|
|
||||||
var localizedAppName = "KeePassDX"
|
var localizedAppName = "KeePassDX"
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -186,7 +187,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
CompressionAlgorithm.GZip -> {
|
CompressionAlgorithm.GZip -> {
|
||||||
// Only in databaseV3.1, in databaseV4 the header is zipped during the save
|
// Only in databaseV3.1, in databaseV4 the header is zipped during the save
|
||||||
if (kdbxVersion.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong()) {
|
if (kdbxVersion.isBefore(FILE_VERSION_32_4)) {
|
||||||
compressAllBinaries()
|
compressAllBinaries()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,9 +195,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
CompressionAlgorithm.GZip -> {
|
CompressionAlgorithm.GZip -> {
|
||||||
// In databaseV4 the header is zipped during the save, so not necessary here
|
// In databaseV4 the header is zipped during the save, so not necessary here
|
||||||
if (kdbxVersion.toKotlinLong() >= FILE_VERSION_32_4.toKotlinLong()) {
|
if (kdbxVersion.isBefore(FILE_VERSION_32_4)) {
|
||||||
decompressAllBinaries()
|
|
||||||
} else {
|
|
||||||
when (newCompression) {
|
when (newCompression) {
|
||||||
CompressionAlgorithm.None -> {
|
CompressionAlgorithm.None -> {
|
||||||
decompressAllBinaries()
|
decompressAllBinaries()
|
||||||
@@ -204,18 +203,18 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
CompressionAlgorithm.GZip -> {
|
CompressionAlgorithm.GZip -> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
decompressAllBinaries()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun compressAllBinaries() {
|
private fun compressAllBinaries() {
|
||||||
binaryPool.doForEachBinary { _, binary ->
|
attachmentPool.doForEachBinary { _, binary ->
|
||||||
try {
|
try {
|
||||||
val cipherKey = loadedCipherKey
|
|
||||||
?: throw IOException("Unable to retrieve cipher key to compress binaries")
|
|
||||||
// To compress, create a new binary with file
|
// To compress, create a new binary with file
|
||||||
binary.compress(cipherKey)
|
binary.compress(binaryCache)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to compress $binary", e)
|
Log.e(TAG, "Unable to compress $binary", e)
|
||||||
}
|
}
|
||||||
@@ -223,11 +222,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun decompressAllBinaries() {
|
private fun decompressAllBinaries() {
|
||||||
binaryPool.doForEachBinary { _, binary ->
|
attachmentPool.doForEachBinary { _, binary ->
|
||||||
try {
|
try {
|
||||||
val cipherKey = loadedCipherKey
|
binary.decompress(binaryCache)
|
||||||
?: throw IOException("Unable to retrieve cipher key to decompress binaries")
|
|
||||||
binary.decompress(cipherKey)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to decompress $binary", e)
|
Log.e(TAG, "Unable to decompress $binary", e)
|
||||||
}
|
}
|
||||||
@@ -310,17 +307,15 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
return this.iconsManager.getIcon(iconId)
|
return this.iconsManager.getIcon(iconId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildNewCustomIcon(cacheDirectory: File,
|
fun buildNewCustomIcon(customIconId: UUID? = null,
|
||||||
customIconId: UUID? = null,
|
|
||||||
result: (IconImageCustom, BinaryData?) -> Unit) {
|
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||||
iconsManager.buildNewCustomIcon(cacheDirectory, customIconId, result)
|
iconsManager.buildNewCustomIcon(customIconId, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addCustomIcon(cacheDirectory: File,
|
fun addCustomIcon(customIconId: UUID? = null,
|
||||||
customIconId: UUID? = null,
|
smallSize: Boolean,
|
||||||
dataSize: Int,
|
|
||||||
result: (IconImageCustom, BinaryData?) -> Unit) {
|
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||||
iconsManager.addCustomIcon(cacheDirectory, customIconId, dataSize, result)
|
iconsManager.addCustomIcon(customIconId, smallSize, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isCustomIconBinaryDuplicate(binary: BinaryData): Boolean {
|
fun isCustomIconBinaryDuplicate(binary: BinaryData): Boolean {
|
||||||
@@ -352,14 +347,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
masterKey = getFileKey(keyInputStream)
|
masterKey = getFileKey(keyInputStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
val messageDigest: MessageDigest
|
return HashManager.hashSha256(masterKey)
|
||||||
try {
|
|
||||||
messageDigest = MessageDigest.getInstance("SHA-256")
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw IOException("No SHA-256 implementation")
|
|
||||||
}
|
|
||||||
|
|
||||||
return messageDigest.digest(masterKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
@@ -370,13 +358,13 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
|
|
||||||
var transformedMasterKey = kdfEngine.transform(masterKey, keyDerivationFunctionParameters)
|
var transformedMasterKey = kdfEngine.transform(masterKey, keyDerivationFunctionParameters)
|
||||||
if (transformedMasterKey.size != 32) {
|
if (transformedMasterKey.size != 32) {
|
||||||
transformedMasterKey = CryptoUtil.hashSha256(transformedMasterKey)
|
transformedMasterKey = HashManager.hashSha256(transformedMasterKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
val cmpKey = ByteArray(65)
|
val cmpKey = ByteArray(65)
|
||||||
System.arraycopy(masterSeed, 0, cmpKey, 0, 32)
|
System.arraycopy(masterSeed, 0, cmpKey, 0, 32)
|
||||||
System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32)
|
System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32)
|
||||||
finalKey = CryptoUtil.resizeKey(cmpKey, 0, 64, dataEngine.keyLength())
|
finalKey = resizeKey(cmpKey, dataEngine.keyLength())
|
||||||
|
|
||||||
val messageDigest: MessageDigest
|
val messageDigest: MessageDigest
|
||||||
try {
|
try {
|
||||||
@@ -391,6 +379,47 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resizeKey(inBytes: ByteArray, cbOut: Int): ByteArray {
|
||||||
|
if (cbOut == 0) return ByteArray(0)
|
||||||
|
|
||||||
|
val messageDigest = if (cbOut <= 32) HashManager.getHash256() else HashManager.getHash512()
|
||||||
|
messageDigest.update(inBytes, 0, 64)
|
||||||
|
val hash: ByteArray = messageDigest.digest()
|
||||||
|
|
||||||
|
if (cbOut == hash.size) {
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
val ret = ByteArray(cbOut)
|
||||||
|
if (cbOut < hash.size) {
|
||||||
|
System.arraycopy(hash, 0, ret, 0, cbOut)
|
||||||
|
} else {
|
||||||
|
var pos = 0
|
||||||
|
var r: Long = 0
|
||||||
|
while (pos < cbOut) {
|
||||||
|
val hmac: Mac
|
||||||
|
try {
|
||||||
|
hmac = Mac.getInstance("HmacSHA256")
|
||||||
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
val pbR = longTo8Bytes(r)
|
||||||
|
val part = hmac.doFinal(pbR)
|
||||||
|
|
||||||
|
val copy = min(cbOut - pos, part.size)
|
||||||
|
System.arraycopy(part, 0, ret, pos, copy)
|
||||||
|
pos += copy
|
||||||
|
r++
|
||||||
|
|
||||||
|
Arrays.fill(part, 0.toByte())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Arrays.fill(hash, 0.toByte())
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
override fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
override fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
||||||
try {
|
try {
|
||||||
val documentBuilderFactory = DocumentBuilderFactory.newInstance()
|
val documentBuilderFactory = DocumentBuilderFactory.newInstance()
|
||||||
@@ -488,17 +517,13 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun checkKeyFileHash(data: String, hash: String): Boolean {
|
private fun checkKeyFileHash(data: String, hash: String): Boolean {
|
||||||
val digest: MessageDigest?
|
|
||||||
var success = false
|
var success = false
|
||||||
try {
|
try {
|
||||||
digest = MessageDigest.getInstance("SHA-256")
|
|
||||||
digest?.reset()
|
|
||||||
// hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key.
|
// hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key.
|
||||||
val dataDigest = digest.digest(Hex.decodeHex(data.toCharArray()))
|
val dataDigest = HashManager.hashSha256(Hex.decodeHex(data.toCharArray()))
|
||||||
.copyOfRange(0, 4)
|
.copyOfRange(0, 4).toHexString()
|
||||||
.toHexString()
|
|
||||||
success = dataDigest == hash
|
success = dataDigest == hash
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
return success
|
return success
|
||||||
@@ -641,13 +666,12 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
return publicCustomData.size() > 0
|
return publicCustomData.size() > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildNewAttachment(cacheDirectory: File,
|
fun buildNewAttachment(smallSize: Boolean,
|
||||||
compression: Boolean,
|
compression: Boolean,
|
||||||
protection: Boolean,
|
protection: Boolean,
|
||||||
binaryPoolId: Int? = null): BinaryData {
|
binaryPoolId: Int? = null): BinaryData {
|
||||||
return binaryPool.put(binaryPoolId) { uniqueBinaryId ->
|
return attachmentPool.put(binaryPoolId) { uniqueBinaryId ->
|
||||||
val fileInCache = File(cacheDirectory, uniqueBinaryId)
|
binaryCache.getBinaryData(uniqueBinaryId, smallSize, compression, protection)
|
||||||
BinaryFile(fileInCache, compression, protection)
|
|
||||||
}.binary
|
}.binary
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,7 +689,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
// Build binaries to remove with all binaries known
|
// Build binaries to remove with all binaries known
|
||||||
val binariesToRemove = ArrayList<BinaryData>()
|
val binariesToRemove = ArrayList<BinaryData>()
|
||||||
if (binaries.isEmpty()) {
|
if (binaries.isEmpty()) {
|
||||||
binaryPool.doForEachBinary { _, binary ->
|
attachmentPool.doForEachBinary { _, binary ->
|
||||||
binariesToRemove.add(binary)
|
binariesToRemove.add(binary)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -674,7 +698,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
// Remove binaries from the list
|
// Remove binaries from the list
|
||||||
rootGroup?.doForEachChild(object : NodeHandler<EntryKDBX>() {
|
rootGroup?.doForEachChild(object : NodeHandler<EntryKDBX>() {
|
||||||
override fun operate(node: EntryKDBX): Boolean {
|
override fun operate(node: EntryKDBX): Boolean {
|
||||||
node.getAttachments(binaryPool, true).forEach {
|
node.getAttachments(attachmentPool, true).forEach {
|
||||||
binariesToRemove.remove(it.binaryData)
|
binariesToRemove.remove(it.binaryData)
|
||||||
}
|
}
|
||||||
return binariesToRemove.isNotEmpty()
|
return binariesToRemove.isNotEmpty()
|
||||||
@@ -683,9 +707,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
// Effective removing
|
// Effective removing
|
||||||
binariesToRemove.forEach {
|
binariesToRemove.forEach {
|
||||||
try {
|
try {
|
||||||
binaryPool.remove(it)
|
attachmentPool.remove(it)
|
||||||
if (clear)
|
if (clear)
|
||||||
it.clear()
|
it.clear(binaryCache)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Unable to clean binaries", e)
|
Log.w(TAG, "Unable to clean binaries", e)
|
||||||
}
|
}
|
||||||
@@ -701,7 +725,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
override fun clearCache() {
|
override fun clearCache() {
|
||||||
try {
|
try {
|
||||||
super.clearCache()
|
super.clearCache()
|
||||||
binaryPool.clear()
|
attachmentPool.clear()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to clear cache", e)
|
Log.e(TAG, "Unable to clear cache", e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,15 +19,16 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.element.database
|
package com.kunzisoft.keepass.database.element.database
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.encrypt.HashManager
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryCache
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupVersioned
|
import com.kunzisoft.keepass.database.element.group.GroupVersioned
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconsManager
|
import com.kunzisoft.keepass.database.element.icon.IconsManager
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
import org.apache.commons.codec.binary.Hex
|
import org.apache.commons.codec.binary.Hex
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
@@ -35,7 +36,6 @@ import java.io.IOException
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.UnsupportedEncodingException
|
import java.io.UnsupportedEncodingException
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
abstract class DatabaseVersioned<
|
abstract class DatabaseVersioned<
|
||||||
@@ -48,21 +48,22 @@ abstract class DatabaseVersioned<
|
|||||||
// Algorithm used to encrypt the database
|
// Algorithm used to encrypt the database
|
||||||
protected var algorithm: EncryptionAlgorithm? = null
|
protected var algorithm: EncryptionAlgorithm? = null
|
||||||
|
|
||||||
abstract val kdfEngine: KdfEngine?
|
abstract val kdfEngine: com.kunzisoft.keepass.database.crypto.kdf.KdfEngine?
|
||||||
|
|
||||||
abstract val kdfAvailableList: List<KdfEngine>
|
abstract val kdfAvailableList: List<com.kunzisoft.keepass.database.crypto.kdf.KdfEngine>
|
||||||
|
|
||||||
var masterKey = ByteArray(32)
|
var masterKey = ByteArray(32)
|
||||||
var finalKey: ByteArray? = null
|
var finalKey: ByteArray? = null
|
||||||
protected set
|
protected set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* To manage binaries in faster way
|
||||||
* Cipher key generated when the database is loaded, and destroyed when the database is closed
|
* Cipher key generated when the database is loaded, and destroyed when the database is closed
|
||||||
* Can be used to temporarily store database elements
|
* Can be used to temporarily store database elements
|
||||||
*/
|
*/
|
||||||
var loadedCipherKey: Database.LoadedKey? = null
|
var binaryCache = BinaryCache()
|
||||||
|
val iconsManager = IconsManager(binaryCache)
|
||||||
val iconsManager = IconsManager()
|
var attachmentPool = AttachmentPool(binaryCache)
|
||||||
|
|
||||||
var changeDuplicateId = false
|
var changeDuplicateId = false
|
||||||
|
|
||||||
@@ -99,42 +100,21 @@ abstract class DatabaseVersioned<
|
|||||||
protected fun getCompositeKey(key: String, keyfileInputStream: InputStream): ByteArray {
|
protected fun getCompositeKey(key: String, keyfileInputStream: InputStream): ByteArray {
|
||||||
val fileKey = getFileKey(keyfileInputStream)
|
val fileKey = getFileKey(keyfileInputStream)
|
||||||
val passwordKey = getPasswordKey(key)
|
val passwordKey = getPasswordKey(key)
|
||||||
|
return HashManager.hashSha256(passwordKey, fileKey)
|
||||||
val messageDigest: MessageDigest
|
|
||||||
try {
|
|
||||||
messageDigest = MessageDigest.getInstance("SHA-256")
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw IOException("SHA-256 not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
messageDigest.update(passwordKey)
|
|
||||||
|
|
||||||
return messageDigest.digest(fileKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
protected fun getPasswordKey(key: String): ByteArray {
|
protected fun getPasswordKey(key: String): ByteArray {
|
||||||
val messageDigest: MessageDigest
|
|
||||||
try {
|
|
||||||
messageDigest = MessageDigest.getInstance("SHA-256")
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw IOException("SHA-256 not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
val bKey: ByteArray = try {
|
val bKey: ByteArray = try {
|
||||||
key.toByteArray(charset(passwordEncoding))
|
key.toByteArray(charset(passwordEncoding))
|
||||||
} catch (e: UnsupportedEncodingException) {
|
} catch (e: UnsupportedEncodingException) {
|
||||||
key.toByteArray()
|
key.toByteArray()
|
||||||
}
|
}
|
||||||
|
return HashManager.hashSha256(bKey)
|
||||||
messageDigest.update(bKey, 0, bKey.size)
|
|
||||||
|
|
||||||
return messageDigest.digest()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
|
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
|
||||||
|
|
||||||
val keyData = keyInputStream.readBytes()
|
val keyData = keyInputStream.readBytes()
|
||||||
|
|
||||||
// Check XML key file
|
// Check XML key file
|
||||||
@@ -152,13 +132,8 @@ abstract class DatabaseVersioned<
|
|||||||
// Key is not base 64, treat it as binary data
|
// Key is not base 64, treat it as binary data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash file as binary data
|
// Hash file as binary data
|
||||||
try {
|
return HashManager.hashSha256(keyData)
|
||||||
return MessageDigest.getInstance("SHA-256").digest(keyData)
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw IOException("SHA-256 not supported")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ package com.kunzisoft.keepass.database.element.entry
|
|||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryData
|
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.KEY_ID
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.KEY_ID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
@@ -56,7 +57,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
|
|
||||||
/** A string describing what is in binaryData */
|
/** A string describing what is in binaryData */
|
||||||
var binaryDescription = ""
|
var binaryDescription = ""
|
||||||
var binaryData: BinaryData? = null
|
private var binaryDataId: Int? = null
|
||||||
|
|
||||||
// Determine if this is a MetaStream entry
|
// Determine if this is a MetaStream entry
|
||||||
val isMetaStream: Boolean
|
val isMetaStream: Boolean
|
||||||
@@ -89,7 +90,8 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
url = parcel.readString() ?: url
|
url = parcel.readString() ?: url
|
||||||
notes = parcel.readString() ?: notes
|
notes = parcel.readString() ?: notes
|
||||||
binaryDescription = parcel.readString() ?: binaryDescription
|
binaryDescription = parcel.readString() ?: binaryDescription
|
||||||
binaryData = parcel.readParcelable(BinaryData::class.java.classLoader)
|
val rawBinaryDataId = parcel.readInt()
|
||||||
|
binaryDataId = if (rawBinaryDataId == -1) null else rawBinaryDataId
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun readParentParcelable(parcel: Parcel): GroupKDB? {
|
override fun readParentParcelable(parcel: Parcel): GroupKDB? {
|
||||||
@@ -108,7 +110,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
dest.writeString(url)
|
dest.writeString(url)
|
||||||
dest.writeString(notes)
|
dest.writeString(notes)
|
||||||
dest.writeString(binaryDescription)
|
dest.writeString(binaryDescription)
|
||||||
dest.writeParcelable(binaryData, flags)
|
dest.writeInt(binaryDataId ?: -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateWith(source: EntryKDB) {
|
fun updateWith(source: EntryKDB) {
|
||||||
@@ -119,7 +121,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
url = source.url
|
url = source.url
|
||||||
notes = source.notes
|
notes = source.notes
|
||||||
binaryDescription = source.binaryDescription
|
binaryDescription = source.binaryDescription
|
||||||
binaryData = source.binaryData
|
binaryDataId = source.binaryDataId
|
||||||
}
|
}
|
||||||
|
|
||||||
override var username = ""
|
override var username = ""
|
||||||
@@ -138,26 +140,39 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
override val type: Type
|
override val type: Type
|
||||||
get() = Type.ENTRY
|
get() = Type.ENTRY
|
||||||
|
|
||||||
fun getAttachment(): Attachment? {
|
fun getAttachment(attachmentPool: AttachmentPool): Attachment? {
|
||||||
val binary = binaryData
|
binaryDataId?.let { poolId ->
|
||||||
return if (binary != null)
|
attachmentPool[poolId]?.let { binary ->
|
||||||
Attachment(binaryDescription, binary)
|
return Attachment(binaryDescription, binary)
|
||||||
else null
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun containsAttachment(): Boolean {
|
fun containsAttachment(): Boolean {
|
||||||
return binaryData != null
|
return binaryDataId != null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun putAttachment(attachment: Attachment) {
|
fun getBinary(attachmentPool: AttachmentPool): BinaryData? {
|
||||||
|
this.binaryDataId?.let {
|
||||||
|
return attachmentPool[it]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putBinary(binaryData: BinaryData, attachmentPool: AttachmentPool) {
|
||||||
|
this.binaryDataId = attachmentPool.put(binaryData)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putAttachment(attachment: Attachment, attachmentPool: AttachmentPool) {
|
||||||
this.binaryDescription = attachment.name
|
this.binaryDescription = attachment.name
|
||||||
this.binaryData = attachment.binaryData
|
this.binaryDataId = attachmentPool.put(attachment.binaryData)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAttachment(attachment: Attachment? = null) {
|
fun removeAttachment(attachment: Attachment? = null) {
|
||||||
if (attachment == null || this.binaryDescription == attachment.name) {
|
if (attachment == null || this.binaryDescription == attachment.name) {
|
||||||
this.binaryDescription = ""
|
this.binaryDescription = ""
|
||||||
this.binaryData = null
|
this.binaryDataId = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ package com.kunzisoft.keepass.database.element.entry
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.utils.UnsignedLong
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.database.AttachmentPool
|
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
@@ -32,7 +33,6 @@ import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
|||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.utils.ParcelableUtil
|
import com.kunzisoft.keepass.utils.ParcelableUtil
|
||||||
import com.kunzisoft.keepass.utils.UnsignedLong
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.collections.LinkedHashMap
|
import kotlin.collections.LinkedHashMap
|
||||||
@@ -56,32 +56,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
var additional = ""
|
var additional = ""
|
||||||
var tags = ""
|
var tags = ""
|
||||||
|
|
||||||
fun getSize(attachmentPool: AttachmentPool): Long {
|
|
||||||
var size = FIXED_LENGTH_SIZE
|
|
||||||
|
|
||||||
for (entry in fields.entries) {
|
|
||||||
size += entry.key.length.toLong()
|
|
||||||
size += entry.value.length().toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
size += getAttachmentsSize(attachmentPool)
|
|
||||||
|
|
||||||
size += autoType.defaultSequence.length.toLong()
|
|
||||||
for ((key, value) in autoType.entrySet()) {
|
|
||||||
size += key.length.toLong()
|
|
||||||
size += value.length.toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
for (entry in history) {
|
|
||||||
size += entry.getSize(attachmentPool)
|
|
||||||
}
|
|
||||||
|
|
||||||
size += overrideURL.length.toLong()
|
|
||||||
size += tags.length.toLong()
|
|
||||||
|
|
||||||
return size
|
|
||||||
}
|
|
||||||
|
|
||||||
override var expires: Boolean = false
|
override var expires: Boolean = false
|
||||||
|
|
||||||
constructor() : super()
|
constructor() : super()
|
||||||
@@ -102,6 +76,14 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
tags = parcel.readString() ?: tags
|
tags = parcel.readString() ?: tags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun readParentParcelable(parcel: Parcel): GroupKDBX? {
|
||||||
|
return parcel.readParcelable(GroupKDBX::class.java.classLoader)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeParentParcelable(parent: GroupKDBX?, parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeParcelable(parent, flags)
|
||||||
|
}
|
||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
super.writeToParcel(dest, flags)
|
super.writeToParcel(dest, flags)
|
||||||
dest.writeLong(usageCount.toKotlinLong())
|
dest.writeLong(usageCount.toKotlinLong())
|
||||||
@@ -164,14 +146,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
return NodeIdUUID(nodeId.id)
|
return NodeIdUUID(nodeId.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun readParentParcelable(parcel: Parcel): GroupKDBX? {
|
|
||||||
return parcel.readParcelable(GroupKDBX::class.java.classLoader)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun writeParentParcelable(parent: GroupKDBX?, parcel: Parcel, flags: Int) {
|
|
||||||
parcel.writeParcelable(parent, flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode a reference key with the FieldReferencesEngine
|
* Decode a reference key with the FieldReferencesEngine
|
||||||
* @param decodeRef
|
* @param decodeRef
|
||||||
@@ -228,6 +202,32 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
|
|
||||||
override var locationChanged = DateInstant()
|
override var locationChanged = DateInstant()
|
||||||
|
|
||||||
|
fun getSize(attachmentPool: AttachmentPool): Long {
|
||||||
|
var size = FIXED_LENGTH_SIZE
|
||||||
|
|
||||||
|
for (entry in fields.entries) {
|
||||||
|
size += entry.key.length.toLong()
|
||||||
|
size += entry.value.length().toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
size += getAttachmentsSize(attachmentPool)
|
||||||
|
|
||||||
|
size += autoType.defaultSequence.length.toLong()
|
||||||
|
for ((key, value) in autoType.entrySet()) {
|
||||||
|
size += key.length.toLong()
|
||||||
|
size += value.length.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (entry in history) {
|
||||||
|
size += entry.getSize(attachmentPool)
|
||||||
|
}
|
||||||
|
|
||||||
|
size += overrideURL.length.toLong()
|
||||||
|
size += tags.length.toLong()
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
fun afterChangeParent() {
|
fun afterChangeParent() {
|
||||||
locationChanged = DateInstant()
|
locationChanged = DateInstant()
|
||||||
}
|
}
|
||||||
@@ -338,8 +338,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
|
|
||||||
override fun touch(modified: Boolean, touchParents: Boolean) {
|
override fun touch(modified: Boolean, touchParents: Boolean) {
|
||||||
super.touch(modified, touchParents)
|
super.touch(modified, touchParents)
|
||||||
// TODO unsigned long
|
usageCount.plusOne()
|
||||||
usageCount = UnsignedLong(usageCount.toKotlinLong() + 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -350,6 +349,8 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
const val STR_URL = "URL"
|
const val STR_URL = "URL"
|
||||||
const val STR_NOTES = "Notes"
|
const val STR_NOTES = "Notes"
|
||||||
|
|
||||||
|
private const val FIXED_LENGTH_SIZE: Long = 128 // Approximate fixed length size
|
||||||
|
|
||||||
fun newCustomNameAllowed(name: String): Boolean {
|
fun newCustomNameAllowed(name: String): Boolean {
|
||||||
return !(name.equals(STR_TITLE, true)
|
return !(name.equals(STR_TITLE, true)
|
||||||
|| name.equals(STR_USERNAME, true)
|
|| name.equals(STR_USERNAME, true)
|
||||||
@@ -368,7 +369,5 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
return arrayOfNulls(size)
|
return arrayOfNulls(size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val FIXED_LENGTH_SIZE: Long = 128 // Approximate fixed length size
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ abstract class EntryVersioned
|
|||||||
|
|
||||||
constructor(parcel: Parcel) : super(parcel)
|
constructor(parcel: Parcel) : super(parcel)
|
||||||
|
|
||||||
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
|
super.writeToParcel(dest, flags)
|
||||||
|
}
|
||||||
|
|
||||||
override fun nodeIndexInParentForNaturalOrder(): Int {
|
override fun nodeIndexInParentForNaturalOrder(): Int {
|
||||||
if (nodeIndexInParentForNaturalOrder == -1) {
|
if (nodeIndexInParentForNaturalOrder == -1) {
|
||||||
val numberOfGroups = parent?.getChildGroups()?.size
|
val numberOfGroups = parent?.getChildGroups()?.size
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.element.icon
|
|||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
|
||||||
class IconImage() : IconImageDraw(), Parcelable {
|
class IconImage() : IconImageDraw() {
|
||||||
|
|
||||||
var standard: IconImageStandard = IconImageStandard()
|
var standard: IconImageStandard = IconImageStandard()
|
||||||
var custom: IconImageCustom = IconImageCustom()
|
var custom: IconImageCustom = IconImageCustom()
|
||||||
|
|||||||
@@ -20,11 +20,12 @@
|
|||||||
package com.kunzisoft.keepass.database.element.icon
|
package com.kunzisoft.keepass.database.element.icon
|
||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
|
import android.os.ParcelUuid
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class IconImageCustom : Parcelable, IconImageDraw {
|
class IconImageCustom : IconImageDraw {
|
||||||
|
|
||||||
var uuid: UUID
|
var uuid: UUID
|
||||||
|
|
||||||
@@ -37,17 +38,17 @@ class IconImageCustom : Parcelable, IconImageDraw {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(parcel: Parcel) {
|
constructor(parcel: Parcel) {
|
||||||
uuid = parcel.readSerializable() as UUID
|
uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
|
dest.writeParcelable(ParcelUuid(uuid), flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
|
||||||
dest.writeSerializable(uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
val prime = 31
|
val prime = 31
|
||||||
var result = 1
|
var result = 1
|
||||||
|
|||||||
@@ -19,7 +19,9 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.element.icon
|
package com.kunzisoft.keepass.database.element.icon
|
||||||
|
|
||||||
abstract class IconImageDraw {
|
import android.os.Parcelable
|
||||||
|
|
||||||
|
abstract class IconImageDraw : Parcelable {
|
||||||
|
|
||||||
var selected = false
|
var selected = false
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import android.os.Parcel
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
|
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
|
||||||
|
|
||||||
class IconImageStandard : Parcelable, IconImageDraw {
|
class IconImageStandard : IconImageDraw {
|
||||||
|
|
||||||
val id: Int
|
val id: Int
|
||||||
|
|
||||||
|
|||||||
@@ -20,22 +20,19 @@
|
|||||||
package com.kunzisoft.keepass.database.element.icon
|
package com.kunzisoft.keepass.database.element.icon
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryByte
|
import com.kunzisoft.keepass.database.element.binary.BinaryCache
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryByte.Companion.MAX_BINARY_BYTES
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryData
|
import com.kunzisoft.keepass.database.element.binary.CustomIconPool
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryFile
|
|
||||||
import com.kunzisoft.keepass.database.element.database.CustomIconPool
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.KEY_ID
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.KEY_ID
|
||||||
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
|
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
|
||||||
import java.io.File
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class IconsManager {
|
class IconsManager(private val binaryCache: BinaryCache) {
|
||||||
|
|
||||||
private val standardCache = List(NB_ICONS) {
|
private val standardCache = List(NB_ICONS) {
|
||||||
IconImageStandard(it)
|
IconImageStandard(it)
|
||||||
}
|
}
|
||||||
private val customCache = CustomIconPool()
|
private val customCache = CustomIconPool(binaryCache)
|
||||||
|
|
||||||
fun getIcon(iconId: Int): IconImageStandard {
|
fun getIcon(iconId: Int): IconImageStandard {
|
||||||
val searchIconId = if (IconImageStandard.isCorrectIconId(iconId)) iconId else KEY_ID
|
val searchIconId = if (IconImageStandard.isCorrectIconId(iconId)) iconId else KEY_ID
|
||||||
@@ -52,25 +49,18 @@ class IconsManager {
|
|||||||
* Custom
|
* Custom
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun buildNewCustomIcon(cacheDirectory: File,
|
fun buildNewCustomIcon(key: UUID? = null,
|
||||||
key: UUID? = null,
|
|
||||||
result: (IconImageCustom, BinaryData?) -> Unit) {
|
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||||
// Create a binary file for a brand new custom icon
|
// Create a binary file for a brand new custom icon
|
||||||
addCustomIcon(cacheDirectory, key, -1, result)
|
addCustomIcon(key, false, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addCustomIcon(cacheDirectory: File,
|
fun addCustomIcon(key: UUID? = null,
|
||||||
key: UUID? = null,
|
smallSize: Boolean,
|
||||||
dataSize: Int,
|
|
||||||
result: (IconImageCustom, BinaryData?) -> Unit) {
|
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||||
val keyBinary = customCache.put(key) { uniqueBinaryId ->
|
val keyBinary = customCache.put(key) { uniqueBinaryId ->
|
||||||
// Create a byte array for better performance with small data
|
// Create a byte array for better performance with small data
|
||||||
if (dataSize in 1..MAX_BINARY_BYTES) {
|
binaryCache.getBinaryData(uniqueBinaryId, smallSize)
|
||||||
BinaryByte()
|
|
||||||
} else {
|
|
||||||
val fileInCache = File(cacheDirectory, uniqueBinaryId)
|
|
||||||
BinaryFile(fileInCache)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
result.invoke(IconImageCustom(keyBinary.keys.first()), keyBinary.binary)
|
result.invoke(IconImageCustom(keyBinary.keys.first()), keyBinary.binary)
|
||||||
}
|
}
|
||||||
@@ -83,11 +73,11 @@ class IconsManager {
|
|||||||
return customCache.isBinaryDuplicate(binaryData)
|
return customCache.isBinaryDuplicate(binaryData)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeCustomIcon(iconUuid: UUID) {
|
fun removeCustomIcon(binaryCache: BinaryCache, iconUuid: UUID) {
|
||||||
val binary = customCache[iconUuid]
|
val binary = customCache[iconUuid]
|
||||||
customCache.remove(iconUuid)
|
customCache.remove(iconUuid)
|
||||||
try {
|
try {
|
||||||
binary?.clear()
|
binary?.clear(binaryCache)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Unable to remove custom icon binary", e)
|
Log.w(TAG, "Unable to remove custom icon binary", e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
package com.kunzisoft.keepass.database.element.node
|
package com.kunzisoft.keepass.database.element.node
|
||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
|
import android.os.ParcelUuid
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -35,12 +36,12 @@ class NodeIdUUID : NodeId<UUID> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(parcel: Parcel) {
|
constructor(parcel: Parcel) {
|
||||||
id = parcel.readSerializable() as UUID
|
id = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
super.writeToParcel(dest, flags)
|
super.writeToParcel(dest, flags)
|
||||||
dest.writeSerializable(id)
|
dest.writeParcelable(ParcelUuid(id), flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX 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.
|
|
||||||
*
|
|
||||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.database.element.security
|
|
||||||
|
|
||||||
import android.content.res.Resources
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.AesEngine
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.ChaCha20Engine
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.TwofishEngine
|
|
||||||
import com.kunzisoft.keepass.utils.ObjectNameResource
|
|
||||||
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
enum class EncryptionAlgorithm : ObjectNameResource {
|
|
||||||
|
|
||||||
AESRijndael,
|
|
||||||
Twofish,
|
|
||||||
ChaCha20;
|
|
||||||
|
|
||||||
val cipherEngine: CipherEngine
|
|
||||||
get() {
|
|
||||||
return when (this) {
|
|
||||||
AESRijndael -> AesEngine()
|
|
||||||
Twofish -> TwofishEngine()
|
|
||||||
ChaCha20 -> ChaCha20Engine()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val dataCipher: UUID
|
|
||||||
get() {
|
|
||||||
return when (this) {
|
|
||||||
AESRijndael -> AesEngine.CIPHER_UUID
|
|
||||||
Twofish -> TwofishEngine.CIPHER_UUID
|
|
||||||
ChaCha20 -> ChaCha20Engine.CIPHER_UUID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getName(resources: Resources): String {
|
|
||||||
return when (this) {
|
|
||||||
AESRijndael -> resources.getString(R.string.encryption_rijndael)
|
|
||||||
Twofish -> resources.getString(R.string.encryption_twofish)
|
|
||||||
ChaCha20 -> resources.getString(R.string.encryption_chacha20)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,9 +21,9 @@
|
|||||||
|
|
||||||
package com.kunzisoft.keepass.database.file
|
package com.kunzisoft.keepass.database.file
|
||||||
|
|
||||||
import com.kunzisoft.keepass.stream.readBytesLength
|
|
||||||
import com.kunzisoft.keepass.stream.readBytes4ToUInt
|
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
import com.kunzisoft.keepass.utils.readBytes4ToUInt
|
||||||
|
import com.kunzisoft.keepass.utils.readBytesLength
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
|
|||||||
@@ -19,30 +19,26 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.file
|
package com.kunzisoft.keepass.database.file
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CrsAlgorithm
|
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.AesKdf
|
import com.kunzisoft.encrypt.HashManager
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
|
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
|
import com.kunzisoft.keepass.database.crypto.VariantDictionary
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.AesKdf
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||||
import com.kunzisoft.keepass.database.exception.VersionDatabaseException
|
import com.kunzisoft.keepass.database.exception.VersionDatabaseException
|
||||||
import com.kunzisoft.keepass.stream.*
|
import com.kunzisoft.keepass.stream.CopyInputStream
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.utils.UnsignedLong
|
|
||||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.security.DigestInputStream
|
import java.security.DigestInputStream
|
||||||
import java.security.InvalidKeyException
|
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import javax.crypto.Mac
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader() {
|
class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader() {
|
||||||
var innerRandomStreamKey: ByteArray = ByteArray(32)
|
var innerRandomStreamKey: ByteArray = ByteArray(32)
|
||||||
@@ -140,33 +136,27 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
|||||||
*/
|
*/
|
||||||
@Throws(IOException::class, VersionDatabaseException::class)
|
@Throws(IOException::class, VersionDatabaseException::class)
|
||||||
fun loadFromFile(inputStream: InputStream): HeaderAndHash {
|
fun loadFromFile(inputStream: InputStream): HeaderAndHash {
|
||||||
val messageDigest: MessageDigest
|
val messageDigest: MessageDigest = HashManager.getHash256()
|
||||||
try {
|
|
||||||
messageDigest = MessageDigest.getInstance("SHA-256")
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw IOException("No SHA-256 implementation")
|
|
||||||
}
|
|
||||||
|
|
||||||
val headerBOS = ByteArrayOutputStream()
|
val headerBOS = ByteArrayOutputStream()
|
||||||
val copyInputStream = CopyInputStream(inputStream, headerBOS)
|
val copyInputStream = CopyInputStream(inputStream, headerBOS)
|
||||||
val digestInputStream = DigestInputStream(copyInputStream, messageDigest)
|
val digestInputStream = DigestInputStream(copyInputStream, messageDigest)
|
||||||
val littleEndianDataInputStream = LittleEndianDataInputStream(digestInputStream)
|
|
||||||
|
|
||||||
val sig1 = littleEndianDataInputStream.readUInt()
|
val sig1 = digestInputStream.readBytes4ToUInt()
|
||||||
val sig2 = littleEndianDataInputStream.readUInt()
|
val sig2 = digestInputStream.readBytes4ToUInt()
|
||||||
|
|
||||||
if (!matchesHeader(sig1, sig2)) {
|
if (!matchesHeader(sig1, sig2)) {
|
||||||
throw VersionDatabaseException()
|
throw VersionDatabaseException()
|
||||||
}
|
}
|
||||||
|
|
||||||
version = littleEndianDataInputStream.readUInt() // Erase previous value
|
version = digestInputStream.readBytes4ToUInt() // Erase previous value
|
||||||
if (!validVersion(version)) {
|
if (!validVersion(version)) {
|
||||||
throw VersionDatabaseException()
|
throw VersionDatabaseException()
|
||||||
}
|
}
|
||||||
|
|
||||||
var done = false
|
var done = false
|
||||||
while (!done) {
|
while (!done) {
|
||||||
done = readHeaderField(littleEndianDataInputStream)
|
done = readHeaderField(digestInputStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
val hash = messageDigest.digest()
|
val hash = messageDigest.digest()
|
||||||
@@ -174,13 +164,13 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun readHeaderField(dis: LittleEndianDataInputStream): Boolean {
|
private fun readHeaderField(dis: InputStream): Boolean {
|
||||||
val fieldID = dis.read().toByte()
|
val fieldID = dis.read().toByte()
|
||||||
|
|
||||||
val fieldSize: Int = if (version.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong()) {
|
val fieldSize: Int = if (version.isBefore(FILE_VERSION_32_4)) {
|
||||||
dis.readUShort()
|
dis.readBytes2ToUShort()
|
||||||
} else {
|
} else {
|
||||||
dis.readUInt().toKotlinInt()
|
dis.readBytes4ToUInt().toKotlinInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
var fieldData: ByteArray? = null
|
var fieldData: ByteArray? = null
|
||||||
@@ -204,20 +194,20 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
|||||||
|
|
||||||
PwDbHeaderV4Fields.MasterSeed -> masterSeed = fieldData
|
PwDbHeaderV4Fields.MasterSeed -> masterSeed = fieldData
|
||||||
|
|
||||||
PwDbHeaderV4Fields.TransformSeed -> if (version.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong())
|
PwDbHeaderV4Fields.TransformSeed -> if (version.isBefore(FILE_VERSION_32_4))
|
||||||
transformSeed = fieldData
|
transformSeed = fieldData
|
||||||
|
|
||||||
PwDbHeaderV4Fields.TransformRounds -> if (version.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong())
|
PwDbHeaderV4Fields.TransformRounds -> if (version.isBefore(FILE_VERSION_32_4))
|
||||||
setTransformRound(fieldData)
|
setTransformRound(fieldData)
|
||||||
|
|
||||||
PwDbHeaderV4Fields.EncryptionIV -> encryptionIV = fieldData
|
PwDbHeaderV4Fields.EncryptionIV -> encryptionIV = fieldData
|
||||||
|
|
||||||
PwDbHeaderV4Fields.InnerRandomstreamKey -> if (version.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong())
|
PwDbHeaderV4Fields.InnerRandomstreamKey -> if (version.isBefore(FILE_VERSION_32_4))
|
||||||
innerRandomStreamKey = fieldData
|
innerRandomStreamKey = fieldData
|
||||||
|
|
||||||
PwDbHeaderV4Fields.StreamStartBytes -> streamStartBytes = fieldData
|
PwDbHeaderV4Fields.StreamStartBytes -> streamStartBytes = fieldData
|
||||||
|
|
||||||
PwDbHeaderV4Fields.InnerRandomStreamID -> if (version.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong())
|
PwDbHeaderV4Fields.InnerRandomStreamID -> if (version.isBefore(FILE_VERSION_32_4))
|
||||||
setRandomStreamID(fieldData)
|
setRandomStreamID(fieldData)
|
||||||
|
|
||||||
PwDbHeaderV4Fields.KdfParameters -> databaseV4.kdfParameters = KdfParameters.deserialize(fieldData)
|
PwDbHeaderV4Fields.KdfParameters -> databaseV4.kdfParameters = KdfParameters.deserialize(fieldData)
|
||||||
@@ -244,14 +234,14 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
|||||||
throw IOException("Invalid cipher ID.")
|
throw IOException("Invalid cipher ID.")
|
||||||
}
|
}
|
||||||
|
|
||||||
databaseV4.dataCipher = bytes16ToUuid(pbId)
|
databaseV4.cipherUuid = bytes16ToUuid(pbId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setTransformRound(roundsByte: ByteArray) {
|
private fun setTransformRound(roundsByte: ByteArray) {
|
||||||
assignAesKdfEngineIfNotExists()
|
assignAesKdfEngineIfNotExists()
|
||||||
val rounds = bytes64ToLong(roundsByte)
|
val rounds = bytes64ToULong(roundsByte)
|
||||||
databaseV4.kdfParameters?.setUInt64(AesKdf.PARAM_ROUNDS, rounds)
|
databaseV4.kdfParameters?.setUInt64(AesKdf.PARAM_ROUNDS, rounds)
|
||||||
databaseV4.numberKeyEncryptionRounds = rounds
|
databaseV4.numberKeyEncryptionRounds = rounds.toKotlinLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
@@ -323,23 +313,5 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
|||||||
fun matchesHeader(sig1: UnsignedInt, sig2: UnsignedInt): Boolean {
|
fun matchesHeader(sig1: UnsignedInt, sig2: UnsignedInt): Boolean {
|
||||||
return sig1 == PWM_DBSIG_1 && (sig2 == DBSIG_PRE2 || sig2 == DBSIG_2)
|
return sig1 == PWM_DBSIG_1 && (sig2 == DBSIG_PRE2 || sig2 == DBSIG_2)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun computeHeaderHmac(header: ByteArray, key: ByteArray): ByteArray {
|
|
||||||
val blockKey = HmacBlockStream.getHmacKey64(key, UnsignedLong.MAX_VALUE)
|
|
||||||
|
|
||||||
val hmac: Mac
|
|
||||||
try {
|
|
||||||
hmac = Mac.getInstance("HmacSHA256")
|
|
||||||
val signingKey = SecretKeySpec(blockKey, "HmacSHA256")
|
|
||||||
hmac.init(signingKey)
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw IOException("No HmacAlogirthm")
|
|
||||||
} catch (e: InvalidKeyException) {
|
|
||||||
throw IOException("Invalid Hmac Key")
|
|
||||||
}
|
|
||||||
|
|
||||||
return hmac.doFinal(header)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX 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.
|
|
||||||
*
|
|
||||||
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.database.file;
|
|
||||||
|
|
||||||
import org.joda.time.DateTime;
|
|
||||||
import org.joda.time.DateTimeZone;
|
|
||||||
import org.joda.time.Duration;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
public class DateKDBXUtil {
|
|
||||||
private static final DateTime dotNetEpoch = new DateTime(1, 1, 1, 0, 0, 0, DateTimeZone.UTC);
|
|
||||||
private static final DateTime javaEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeZone.UTC);
|
|
||||||
|
|
||||||
private static final long epochOffset;
|
|
||||||
|
|
||||||
static {
|
|
||||||
epochOffset = (javaEpoch.getMillis() - dotNetEpoch.getMillis()) / 1000L;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Date convertKDBX4Time(long seconds) {
|
|
||||||
DateTime dt = dotNetEpoch.plus(seconds * 1000L);
|
|
||||||
// Switch corrupted dates to a more recent date that won't cause issues on the client
|
|
||||||
if (dt.isBefore(javaEpoch)) {
|
|
||||||
return javaEpoch.toDate();
|
|
||||||
}
|
|
||||||
return dt.toDate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static long convertDateToKDBX4Time(DateTime dt) {
|
|
||||||
Duration duration = new Duration( javaEpoch, dt );
|
|
||||||
long seconds = ( duration.getMillis() / 1000L );
|
|
||||||
return seconds + epochOffset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX 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.
|
||||||
|
*
|
||||||
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.database.file
|
||||||
|
|
||||||
|
import org.joda.time.DateTime
|
||||||
|
import org.joda.time.DateTimeZone
|
||||||
|
import org.joda.time.Duration
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object DateKDBXUtil {
|
||||||
|
|
||||||
|
private val dotNetEpoch = DateTime(1, 1, 1, 0, 0, 0, DateTimeZone.UTC)
|
||||||
|
private val javaEpoch = DateTime(1970, 1, 1, 0, 0, 0, DateTimeZone.UTC)
|
||||||
|
private val epochOffset = (javaEpoch.millis - dotNetEpoch.millis) / 1000L
|
||||||
|
|
||||||
|
fun convertKDBX4Time(seconds: Long): Date {
|
||||||
|
val dt = dotNetEpoch.plus(seconds * 1000L)
|
||||||
|
// Switch corrupted dates to a more recent date that won't cause issues on the client
|
||||||
|
return if (dt.isBefore(javaEpoch)) {
|
||||||
|
javaEpoch.toDate()
|
||||||
|
} else dt.toDate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertDateToKDBX4Time(dt: DateTime?): Long {
|
||||||
|
val duration = Duration(javaEpoch, dt)
|
||||||
|
val seconds = duration.millis / 1000L
|
||||||
|
return seconds + epochOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,15 +19,21 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.file.input
|
package com.kunzisoft.keepass.database.file.input
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import android.util.Log
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
abstract class DatabaseInput<PwDb : DatabaseVersioned<*, *, *, *>>
|
abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>>
|
||||||
(protected val cacheDirectory: File) {
|
(protected val cacheDirectory: File,
|
||||||
|
protected val isRAMSufficient: (memoryWanted: Long) -> Boolean) {
|
||||||
|
|
||||||
|
private var startTimeKey = System.currentTimeMillis()
|
||||||
|
private var startTimeContent = System.currentTimeMillis()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a versioned database file, return contents in a new DatabaseVersioned.
|
* Load a versioned database file, return contents in a new DatabaseVersioned.
|
||||||
@@ -43,15 +49,39 @@ abstract class DatabaseInput<PwDb : DatabaseVersioned<*, *, *, *>>
|
|||||||
abstract fun openDatabase(databaseInputStream: InputStream,
|
abstract fun openDatabase(databaseInputStream: InputStream,
|
||||||
password: String?,
|
password: String?,
|
||||||
keyfileInputStream: InputStream?,
|
keyfileInputStream: InputStream?,
|
||||||
loadedCipherKey: Database.LoadedKey,
|
loadedCipherKey: LoadedKey,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?,
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
fixDuplicateUUID: Boolean = false): PwDb
|
fixDuplicateUUID: Boolean = false): D
|
||||||
|
|
||||||
|
|
||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
abstract fun openDatabase(databaseInputStream: InputStream,
|
abstract fun openDatabase(databaseInputStream: InputStream,
|
||||||
masterKey: ByteArray,
|
masterKey: ByteArray,
|
||||||
loadedCipherKey: Database.LoadedKey,
|
loadedCipherKey: LoadedKey,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?,
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
fixDuplicateUUID: Boolean = false): PwDb
|
fixDuplicateUUID: Boolean = false): D
|
||||||
|
|
||||||
|
protected fun startKeyTimer(progressTaskUpdater: ProgressTaskUpdater?) {
|
||||||
|
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
|
||||||
|
Log.d(TAG, "Start retrieving database key...")
|
||||||
|
startTimeKey = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun stopKeyTimer() {
|
||||||
|
Log.d(TAG, "Stop retrieving database key... ${System.currentTimeMillis() - startTimeKey} ms")
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun startContentTimer(progressTaskUpdater: ProgressTaskUpdater?) {
|
||||||
|
progressTaskUpdater?.updateMessage(R.string.decrypting_db)
|
||||||
|
Log.d(TAG, "Start decrypting database content...")
|
||||||
|
startTimeContent = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun stopContentTimer() {
|
||||||
|
Log.d(TAG, "Stop retrieving database content... ${System.currentTimeMillis() - startTimeContent} ms")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = DatabaseInput::class.java.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,34 +20,34 @@
|
|||||||
|
|
||||||
package com.kunzisoft.keepass.database.file.input
|
package com.kunzisoft.keepass.database.file.input
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.encrypt.HashManager
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
|
||||||
import com.kunzisoft.keepass.database.exception.*
|
import com.kunzisoft.keepass.database.exception.*
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||||
import com.kunzisoft.keepass.stream.*
|
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
|
import com.kunzisoft.keepass.utils.*
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.security.*
|
import java.security.DigestInputStream
|
||||||
|
import java.security.MessageDigest
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.NoSuchPaddingException
|
import javax.crypto.CipherInputStream
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a KDB database file.
|
* Load a KDB database file.
|
||||||
*/
|
*/
|
||||||
class DatabaseInputKDB(cacheDirectory: File)
|
class DatabaseInputKDB(cacheDirectory: File,
|
||||||
: DatabaseInput<DatabaseKDB>(cacheDirectory) {
|
isRAMSufficient: (memoryWanted: Long) -> Boolean)
|
||||||
|
: DatabaseInput<DatabaseKDB>(cacheDirectory, isRAMSufficient) {
|
||||||
|
|
||||||
private lateinit var mDatabase: DatabaseKDB
|
private lateinit var mDatabase: DatabaseKDB
|
||||||
|
|
||||||
@@ -55,11 +55,11 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
override fun openDatabase(databaseInputStream: InputStream,
|
override fun openDatabase(databaseInputStream: InputStream,
|
||||||
password: String?,
|
password: String?,
|
||||||
keyfileInputStream: InputStream?,
|
keyfileInputStream: InputStream?,
|
||||||
loadedCipherKey: Database.LoadedKey,
|
loadedCipherKey: LoadedKey,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?,
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
fixDuplicateUUID: Boolean): DatabaseKDB {
|
fixDuplicateUUID: Boolean): DatabaseKDB {
|
||||||
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
||||||
mDatabase.loadedCipherKey = loadedCipherKey
|
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
|
||||||
mDatabase.retrieveMasterKey(password, keyfileInputStream)
|
mDatabase.retrieveMasterKey(password, keyfileInputStream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,11 +67,11 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
override fun openDatabase(databaseInputStream: InputStream,
|
override fun openDatabase(databaseInputStream: InputStream,
|
||||||
masterKey: ByteArray,
|
masterKey: ByteArray,
|
||||||
loadedCipherKey: Database.LoadedKey,
|
loadedCipherKey: LoadedKey,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?,
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
fixDuplicateUUID: Boolean): DatabaseKDB {
|
fixDuplicateUUID: Boolean): DatabaseKDB {
|
||||||
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
||||||
mDatabase.loadedCipherKey = loadedCipherKey
|
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
|
||||||
mDatabase.masterKey = masterKey
|
mDatabase.masterKey = masterKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,6 +83,7 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
assignMasterKey: (() -> Unit)? = null): DatabaseKDB {
|
assignMasterKey: (() -> Unit)? = null): DatabaseKDB {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
startKeyTimer(progressTaskUpdater)
|
||||||
// Load entire file, most of it's encrypted.
|
// Load entire file, most of it's encrypted.
|
||||||
val fileSize = databaseInputStream.available()
|
val fileSize = databaseInputStream.available()
|
||||||
|
|
||||||
@@ -105,8 +106,8 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
throw VersionDatabaseException()
|
throw VersionDatabaseException()
|
||||||
}
|
}
|
||||||
|
|
||||||
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
|
|
||||||
mDatabase = DatabaseKDB()
|
mDatabase = DatabaseKDB()
|
||||||
|
mDatabase.binaryCache.cacheDirectory = cacheDirectory
|
||||||
|
|
||||||
mDatabase.changeDuplicateId = fixDuplicateUUID
|
mDatabase.changeDuplicateId = fixDuplicateUUID
|
||||||
assignMasterKey?.invoke()
|
assignMasterKey?.invoke()
|
||||||
@@ -130,45 +131,23 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
header.transformSeed,
|
header.transformSeed,
|
||||||
mDatabase.numberKeyEncryptionRounds)
|
mDatabase.numberKeyEncryptionRounds)
|
||||||
|
|
||||||
progressTaskUpdater?.updateMessage(R.string.decrypting_db)
|
stopKeyTimer()
|
||||||
// Initialize Rijndael algorithm
|
startContentTimer(progressTaskUpdater)
|
||||||
|
|
||||||
val cipher: Cipher = try {
|
val cipher: Cipher = try {
|
||||||
when {
|
mDatabase.encryptionAlgorithm
|
||||||
mDatabase.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael -> {
|
.cipherEngine.getCipher(Cipher.DECRYPT_MODE,
|
||||||
CipherFactory.getInstance("AES/CBC/PKCS5Padding")
|
mDatabase.finalKey ?: ByteArray(0),
|
||||||
}
|
header.encryptionIV)
|
||||||
mDatabase.encryptionAlgorithm === EncryptionAlgorithm.Twofish -> {
|
} catch (e: Exception) {
|
||||||
CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING")
|
throw IOException("Algorithm not supported.", e)
|
||||||
}
|
|
||||||
else -> throw IOException("Encryption algorithm is not supported")
|
|
||||||
}
|
|
||||||
} catch (e1: NoSuchAlgorithmException) {
|
|
||||||
throw IOException("No such algorithm")
|
|
||||||
} catch (e1: NoSuchPaddingException) {
|
|
||||||
throw IOException("No such pdading")
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE,
|
|
||||||
SecretKeySpec(mDatabase.finalKey, "AES"),
|
|
||||||
IvParameterSpec(header.encryptionIV))
|
|
||||||
} catch (e1: InvalidKeyException) {
|
|
||||||
throw IOException("Invalid key")
|
|
||||||
} catch (e1: InvalidAlgorithmParameterException) {
|
|
||||||
throw IOException("Invalid algorithm parameter.")
|
|
||||||
}
|
|
||||||
|
|
||||||
val messageDigest: MessageDigest
|
|
||||||
try {
|
|
||||||
messageDigest = MessageDigest.getInstance("SHA-256")
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw IOException("No SHA-256 algorithm")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt content
|
// Decrypt content
|
||||||
|
val messageDigest: MessageDigest = HashManager.getHash256()
|
||||||
val cipherInputStream = BufferedInputStream(
|
val cipherInputStream = BufferedInputStream(
|
||||||
DigestInputStream(
|
DigestInputStream(
|
||||||
BetterCipherInputStream(databaseInputStream, cipher),
|
CipherInputStream(databaseInputStream, cipher),
|
||||||
messageDigest
|
messageDigest
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -224,7 +203,7 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
}
|
}
|
||||||
0x0003 -> {
|
0x0003 -> {
|
||||||
newGroup?.let { group ->
|
newGroup?.let { group ->
|
||||||
group.creationTime = cipherInputStream.readBytes5ToDate()
|
group.creationTime = DateInstant(cipherInputStream.readBytes5ToDate())
|
||||||
} ?:
|
} ?:
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
var iconId = cipherInputStream.readBytes4ToUInt().toKotlinInt()
|
var iconId = cipherInputStream.readBytes4ToUInt().toKotlinInt()
|
||||||
@@ -237,7 +216,7 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
}
|
}
|
||||||
0x0004 -> {
|
0x0004 -> {
|
||||||
newGroup?.let { group ->
|
newGroup?.let { group ->
|
||||||
group.lastModificationTime = cipherInputStream.readBytes5ToDate()
|
group.lastModificationTime = DateInstant(cipherInputStream.readBytes5ToDate())
|
||||||
} ?:
|
} ?:
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
entry.title = cipherInputStream.readBytesToString(fieldSize)
|
entry.title = cipherInputStream.readBytesToString(fieldSize)
|
||||||
@@ -245,7 +224,7 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
}
|
}
|
||||||
0x0005 -> {
|
0x0005 -> {
|
||||||
newGroup?.let { group ->
|
newGroup?.let { group ->
|
||||||
group.lastAccessTime = cipherInputStream.readBytes5ToDate()
|
group.lastAccessTime = DateInstant(cipherInputStream.readBytes5ToDate())
|
||||||
} ?:
|
} ?:
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
entry.url = cipherInputStream.readBytesToString(fieldSize)
|
entry.url = cipherInputStream.readBytesToString(fieldSize)
|
||||||
@@ -253,7 +232,7 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
}
|
}
|
||||||
0x0006 -> {
|
0x0006 -> {
|
||||||
newGroup?.let { group ->
|
newGroup?.let { group ->
|
||||||
group.expiryTime = cipherInputStream.readBytes5ToDate()
|
group.expiryTime = DateInstant(cipherInputStream.readBytes5ToDate())
|
||||||
} ?:
|
} ?:
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
entry.username = cipherInputStream.readBytesToString(fieldSize)
|
entry.username = cipherInputStream.readBytesToString(fieldSize)
|
||||||
@@ -280,22 +259,22 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
group.groupFlags = cipherInputStream.readBytes4ToUInt().toKotlinInt()
|
group.groupFlags = cipherInputStream.readBytes4ToUInt().toKotlinInt()
|
||||||
} ?:
|
} ?:
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
entry.creationTime = cipherInputStream.readBytes5ToDate()
|
entry.creationTime = DateInstant(cipherInputStream.readBytes5ToDate())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
0x000A -> {
|
0x000A -> {
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
entry.lastModificationTime = cipherInputStream.readBytes5ToDate()
|
entry.lastModificationTime = DateInstant(cipherInputStream.readBytes5ToDate())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
0x000B -> {
|
0x000B -> {
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
entry.lastAccessTime = cipherInputStream.readBytes5ToDate()
|
entry.lastAccessTime = DateInstant(cipherInputStream.readBytes5ToDate())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
0x000C -> {
|
0x000C -> {
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
entry.expiryTime = cipherInputStream.readBytes5ToDate()
|
entry.expiryTime = DateInstant(cipherInputStream.readBytes5ToDate())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
0x000D -> {
|
0x000D -> {
|
||||||
@@ -306,11 +285,9 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
0x000E -> {
|
0x000E -> {
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
if (fieldSize > 0) {
|
if (fieldSize > 0) {
|
||||||
val binaryAttachment = mDatabase.buildNewAttachment(cacheDirectory)
|
val binaryData = mDatabase.buildNewAttachment()
|
||||||
entry.binaryData = binaryAttachment
|
entry.putBinary(binaryData, mDatabase.attachmentPool)
|
||||||
val cipherKey = mDatabase.loadedCipherKey
|
BufferedOutputStream(binaryData.getOutputDataStream(mDatabase.binaryCache)).use { outputStream ->
|
||||||
?: throw IOException("Unable to retrieve cipher key to load binaries")
|
|
||||||
BufferedOutputStream(binaryAttachment.getOutputDataStream(cipherKey)).use { outputStream ->
|
|
||||||
cipherInputStream.readBytes(fieldSize) { buffer ->
|
cipherInputStream.readBytes(fieldSize) { buffer ->
|
||||||
outputStream.write(buffer)
|
outputStream.write(buffer)
|
||||||
}
|
}
|
||||||
@@ -343,6 +320,8 @@ class DatabaseInputKDB(cacheDirectory: File)
|
|||||||
}
|
}
|
||||||
constructTreeFromIndex()
|
constructTreeFromIndex()
|
||||||
|
|
||||||
|
stopContentTimer()
|
||||||
|
|
||||||
} catch (e: LoadDatabaseException) {
|
} catch (e: LoadDatabaseException) {
|
||||||
mDatabase.clearCache()
|
mDatabase.clearCache()
|
||||||
throw e
|
throw e
|
||||||
|
|||||||
@@ -21,15 +21,16 @@ package com.kunzisoft.keepass.database.file.input
|
|||||||
|
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.encrypt.StreamCipher
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
import com.kunzisoft.keepass.database.crypto.CipherEngine
|
||||||
import com.kunzisoft.keepass.crypto.StreamCipherFactory
|
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
|
||||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.database.crypto.HmacBlock
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.DeletedObject
|
import com.kunzisoft.keepass.database.element.DeletedObject
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryData
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
|
||||||
@@ -41,13 +42,13 @@ import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
|||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.database.exception.*
|
import com.kunzisoft.keepass.database.exception.*
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||||
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseKDBXXML
|
import com.kunzisoft.keepass.database.file.DatabaseKDBXXML
|
||||||
import com.kunzisoft.keepass.database.file.DateKDBXUtil
|
import com.kunzisoft.keepass.database.file.DateKDBXUtil
|
||||||
import com.kunzisoft.keepass.stream.*
|
import com.kunzisoft.keepass.stream.HashedBlockInputStream
|
||||||
|
import com.kunzisoft.keepass.stream.HmacBlockInputStream
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.utils.UnsignedLong
|
|
||||||
import org.bouncycastle.crypto.StreamCipher
|
|
||||||
import org.xmlpull.v1.XmlPullParser
|
import org.xmlpull.v1.XmlPullParser
|
||||||
import org.xmlpull.v1.XmlPullParserException
|
import org.xmlpull.v1.XmlPullParserException
|
||||||
import org.xmlpull.v1.XmlPullParserFactory
|
import org.xmlpull.v1.XmlPullParserFactory
|
||||||
@@ -61,10 +62,12 @@ import java.util.*
|
|||||||
import java.util.zip.GZIPInputStream
|
import java.util.zip.GZIPInputStream
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.CipherInputStream
|
import javax.crypto.CipherInputStream
|
||||||
|
import javax.crypto.Mac
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class DatabaseInputKDBX(cacheDirectory: File)
|
class DatabaseInputKDBX(cacheDirectory: File,
|
||||||
: DatabaseInput<DatabaseKDBX>(cacheDirectory) {
|
isRAMSufficient: (memoryWanted: Long) -> Boolean)
|
||||||
|
: DatabaseInput<DatabaseKDBX>(cacheDirectory, isRAMSufficient) {
|
||||||
|
|
||||||
private var randomStream: StreamCipher? = null
|
private var randomStream: StreamCipher? = null
|
||||||
private lateinit var mDatabase: DatabaseKDBX
|
private lateinit var mDatabase: DatabaseKDBX
|
||||||
@@ -97,11 +100,11 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
override fun openDatabase(databaseInputStream: InputStream,
|
override fun openDatabase(databaseInputStream: InputStream,
|
||||||
password: String?,
|
password: String?,
|
||||||
keyfileInputStream: InputStream?,
|
keyfileInputStream: InputStream?,
|
||||||
loadedCipherKey: Database.LoadedKey,
|
loadedCipherKey: LoadedKey,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?,
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
fixDuplicateUUID: Boolean): DatabaseKDBX {
|
fixDuplicateUUID: Boolean): DatabaseKDBX {
|
||||||
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
||||||
mDatabase.loadedCipherKey = loadedCipherKey
|
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
|
||||||
mDatabase.retrieveMasterKey(password, keyfileInputStream)
|
mDatabase.retrieveMasterKey(password, keyfileInputStream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,11 +112,11 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
override fun openDatabase(databaseInputStream: InputStream,
|
override fun openDatabase(databaseInputStream: InputStream,
|
||||||
masterKey: ByteArray,
|
masterKey: ByteArray,
|
||||||
loadedCipherKey: Database.LoadedKey,
|
loadedCipherKey: LoadedKey,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?,
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
fixDuplicateUUID: Boolean): DatabaseKDBX {
|
fixDuplicateUUID: Boolean): DatabaseKDBX {
|
||||||
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
||||||
mDatabase.loadedCipherKey = loadedCipherKey
|
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
|
||||||
mDatabase.masterKey = masterKey
|
mDatabase.masterKey = masterKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,8 +127,9 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
fixDuplicateUUID: Boolean,
|
fixDuplicateUUID: Boolean,
|
||||||
assignMasterKey: (() -> Unit)? = null): DatabaseKDBX {
|
assignMasterKey: (() -> Unit)? = null): DatabaseKDBX {
|
||||||
try {
|
try {
|
||||||
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
|
startKeyTimer(progressTaskUpdater)
|
||||||
mDatabase = DatabaseKDBX()
|
mDatabase = DatabaseKDBX()
|
||||||
|
mDatabase.binaryCache.cacheDirectory = cacheDirectory
|
||||||
|
|
||||||
mDatabase.changeDuplicateId = fixDuplicateUUID
|
mDatabase.changeDuplicateId = fixDuplicateUUID
|
||||||
|
|
||||||
@@ -140,26 +144,29 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
assignMasterKey?.invoke()
|
assignMasterKey?.invoke()
|
||||||
mDatabase.makeFinalKey(header.masterSeed)
|
mDatabase.makeFinalKey(header.masterSeed)
|
||||||
|
|
||||||
progressTaskUpdater?.updateMessage(R.string.decrypting_db)
|
stopKeyTimer()
|
||||||
|
startContentTimer(progressTaskUpdater)
|
||||||
|
|
||||||
val engine: CipherEngine
|
val engine: CipherEngine
|
||||||
val cipher: Cipher
|
val cipher: Cipher
|
||||||
try {
|
try {
|
||||||
engine = CipherFactory.getInstance(mDatabase.dataCipher)
|
engine = EncryptionAlgorithm.getFrom(mDatabase.cipherUuid).cipherEngine
|
||||||
|
engine.forcePaddingCompatibility = true
|
||||||
mDatabase.setDataEngine(engine)
|
mDatabase.setDataEngine(engine)
|
||||||
mDatabase.encryptionAlgorithm = engine.getPwEncryptionAlgorithm()
|
mDatabase.encryptionAlgorithm = engine.getEncryptionAlgorithm()
|
||||||
cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV)
|
cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV)
|
||||||
|
engine.forcePaddingCompatibility = false
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw InvalidAlgorithmDatabaseException(e)
|
throw InvalidAlgorithmDatabaseException(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isPlain: InputStream
|
val plainInputStream: InputStream
|
||||||
if (mDatabase.kdbxVersion.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
|
if (mDatabase.kdbxVersion.isBefore(FILE_VERSION_32_4)) {
|
||||||
|
|
||||||
val decrypted = attachCipherStream(databaseInputStream, cipher)
|
val dataDecrypted = CipherInputStream(databaseInputStream, cipher)
|
||||||
val dataDecrypted = LittleEndianDataInputStream(decrypted)
|
|
||||||
val storedStartBytes: ByteArray?
|
val storedStartBytes: ByteArray?
|
||||||
try {
|
try {
|
||||||
storedStartBytes = dataDecrypted.readBytes(32)
|
storedStartBytes = dataDecrypted.readBytesLength(32)
|
||||||
if (storedStartBytes.size != 32) {
|
if (storedStartBytes.size != 32) {
|
||||||
throw InvalidCredentialsDatabaseException()
|
throw InvalidCredentialsDatabaseException()
|
||||||
}
|
}
|
||||||
@@ -171,47 +178,57 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
throw InvalidCredentialsDatabaseException()
|
throw InvalidCredentialsDatabaseException()
|
||||||
}
|
}
|
||||||
|
|
||||||
isPlain = HashedBlockInputStream(dataDecrypted)
|
plainInputStream = HashedBlockInputStream(dataDecrypted)
|
||||||
} else { // KDBX 4
|
} else { // KDBX 4
|
||||||
val isData = LittleEndianDataInputStream(databaseInputStream)
|
val storedHash = databaseInputStream.readBytesLength(32)
|
||||||
val storedHash = isData.readBytes(32)
|
if (!storedHash.contentEquals(hashOfHeader)) {
|
||||||
if (!Arrays.equals(storedHash, hashOfHeader)) {
|
|
||||||
throw InvalidCredentialsDatabaseException()
|
throw InvalidCredentialsDatabaseException()
|
||||||
}
|
}
|
||||||
|
|
||||||
val hmacKey = mDatabase.hmacKey ?: throw LoadDatabaseException()
|
val hmacKey = mDatabase.hmacKey ?: throw LoadDatabaseException()
|
||||||
val headerHmac = DatabaseHeaderKDBX.computeHeaderHmac(pbHeader, hmacKey)
|
|
||||||
val storedHmac = isData.readBytes(32)
|
val blockKey = HmacBlock.getHmacKey64(hmacKey, UnsignedLong.MAX_BYTES)
|
||||||
|
val hmac: Mac = HmacBlock.getHmacSha256(blockKey)
|
||||||
|
val headerHmac = hmac.doFinal(pbHeader)
|
||||||
|
|
||||||
|
val storedHmac = databaseInputStream.readBytesLength(32)
|
||||||
if (storedHmac.size != 32) {
|
if (storedHmac.size != 32) {
|
||||||
throw InvalidCredentialsDatabaseException()
|
throw InvalidCredentialsDatabaseException()
|
||||||
}
|
}
|
||||||
// Mac doesn't match
|
// Mac doesn't match
|
||||||
if (!Arrays.equals(headerHmac, storedHmac)) {
|
if (!headerHmac.contentEquals(storedHmac)) {
|
||||||
throw InvalidCredentialsDatabaseException()
|
throw InvalidCredentialsDatabaseException()
|
||||||
}
|
}
|
||||||
|
|
||||||
val hmIs = HmacBlockInputStream(isData, true, hmacKey)
|
val hmIs = HmacBlockInputStream(databaseInputStream, true, hmacKey)
|
||||||
|
|
||||||
isPlain = attachCipherStream(hmIs, cipher)
|
plainInputStream = CipherInputStream(hmIs, cipher)
|
||||||
}
|
}
|
||||||
|
|
||||||
val inputStreamXml: InputStream
|
val inputStreamXml: InputStream = when (mDatabase.compressionAlgorithm) {
|
||||||
inputStreamXml = when (mDatabase.compressionAlgorithm) {
|
CompressionAlgorithm.GZip -> GZIPInputStream(plainInputStream)
|
||||||
CompressionAlgorithm.GZip -> GZIPInputStream(isPlain)
|
else -> plainInputStream
|
||||||
else -> isPlain
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mDatabase.kdbxVersion.toKotlinLong() >= DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
|
if (!mDatabase.kdbxVersion.isBefore(FILE_VERSION_32_4)) {
|
||||||
loadInnerHeader(inputStreamXml, header)
|
readInnerHeader(inputStreamXml, header)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
|
randomStream = CrsAlgorithm.getCipher(header.innerRandomStream, header.innerRandomStreamKey)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw LoadDatabaseException(e)
|
throw LoadDatabaseException(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
readDocumentStreamed(createPullParser(inputStreamXml))
|
val xmlPullParserFactory = XmlPullParserFactory.newInstance().apply {
|
||||||
|
isNamespaceAware = false
|
||||||
|
}
|
||||||
|
val xmlPullParser = xmlPullParserFactory.newPullParser().apply {
|
||||||
|
setInput(inputStreamXml, null)
|
||||||
|
}
|
||||||
|
readDocumentStreamed(xmlPullParser)
|
||||||
|
|
||||||
|
stopContentTimer()
|
||||||
|
|
||||||
} catch (e: LoadDatabaseException) {
|
} catch (e: LoadDatabaseException) {
|
||||||
throw e
|
throw e
|
||||||
@@ -231,63 +248,55 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
return mDatabase
|
return mDatabase
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun attachCipherStream(inputStream: InputStream, cipher: Cipher): InputStream {
|
|
||||||
return CipherInputStream(inputStream, cipher)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun loadInnerHeader(inputStream: InputStream, header: DatabaseHeaderKDBX) {
|
private fun readInnerHeader(dataInputStream: InputStream,
|
||||||
val lis = LittleEndianDataInputStream(inputStream)
|
header: DatabaseHeaderKDBX) {
|
||||||
|
|
||||||
while (true) {
|
var readStream = true
|
||||||
if (!readInnerHeader(lis, header)) break
|
while (readStream) {
|
||||||
}
|
val fieldId = dataInputStream.read().toByte()
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
val size = dataInputStream.readBytes4ToUInt().toKotlinInt()
|
||||||
private fun readInnerHeader(dataInputStream: LittleEndianDataInputStream,
|
if (size < 0) throw IOException("Corrupted file")
|
||||||
header: DatabaseHeaderKDBX): Boolean {
|
|
||||||
val fieldId = dataInputStream.read().toByte()
|
|
||||||
|
|
||||||
val size = dataInputStream.readUInt().toKotlinInt()
|
var data = ByteArray(0)
|
||||||
if (size < 0) throw IOException("Corrupted file")
|
try {
|
||||||
|
if (size > 0) {
|
||||||
var data = ByteArray(0)
|
if (fieldId != DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary) {
|
||||||
if (size > 0) {
|
data = dataInputStream.readBytesLength(size)
|
||||||
if (fieldId != DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary) {
|
}
|
||||||
// TODO OOM here
|
}
|
||||||
data = dataInputStream.readBytes(size)
|
} catch (e: Exception) {
|
||||||
|
// OOM only if corrupted file
|
||||||
|
throw IOException("Corrupted file")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var result = true
|
readStream = true
|
||||||
when (fieldId) {
|
when (fieldId) {
|
||||||
DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader -> {
|
DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader -> {
|
||||||
result = false
|
readStream = false
|
||||||
}
|
}
|
||||||
DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID -> {
|
DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID -> {
|
||||||
header.setRandomStreamID(data)
|
header.setRandomStreamID(data)
|
||||||
}
|
}
|
||||||
DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey -> {
|
DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey -> {
|
||||||
header.innerRandomStreamKey = data
|
header.innerRandomStreamKey = data
|
||||||
}
|
}
|
||||||
DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary -> {
|
DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary -> {
|
||||||
// Read in a file
|
// Read in a file
|
||||||
val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
||||||
val byteLength = size - 1
|
val byteLength = size - 1
|
||||||
// No compression at this level
|
// No compression at this level
|
||||||
val protectedBinary = mDatabase.buildNewAttachment(cacheDirectory, false, protectedFlag)
|
val protectedBinary = mDatabase.buildNewAttachment(
|
||||||
val cipherKey = mDatabase.loadedCipherKey
|
isRAMSufficient.invoke(byteLength.toLong()), false, protectedFlag)
|
||||||
?: throw IOException("Unable to retrieve cipher key to load binaries")
|
protectedBinary.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->
|
||||||
protectedBinary.getOutputDataStream(cipherKey).use { outputStream ->
|
dataInputStream.readBytes(byteLength) { buffer ->
|
||||||
dataInputStream.readBytes(byteLength) { buffer ->
|
outputStream.write(buffer)
|
||||||
outputStream.write(buffer)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum class KdbContext {
|
private enum class KdbContext {
|
||||||
@@ -703,12 +712,11 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
} else if (ctx == KdbContext.CustomIcons && name.equals(DatabaseKDBXXML.ElemCustomIcons, ignoreCase = true)) {
|
} else if (ctx == KdbContext.CustomIcons && name.equals(DatabaseKDBXXML.ElemCustomIcons, ignoreCase = true)) {
|
||||||
return KdbContext.Meta
|
return KdbContext.Meta
|
||||||
} else if (ctx == KdbContext.CustomIcon && name.equals(DatabaseKDBXXML.ElemCustomIconItem, ignoreCase = true)) {
|
} else if (ctx == KdbContext.CustomIcon && name.equals(DatabaseKDBXXML.ElemCustomIconItem, ignoreCase = true)) {
|
||||||
if (customIconID != DatabaseVersioned.UUID_ZERO && customIconData != null) {
|
val iconData = customIconData
|
||||||
mDatabase.addCustomIcon(cacheDirectory, customIconID, customIconData!!.size) { _, binary ->
|
if (customIconID != DatabaseVersioned.UUID_ZERO && iconData != null) {
|
||||||
mDatabase.loadedCipherKey?.let { cipherKey ->
|
mDatabase.addCustomIcon(customIconID, isRAMSufficient.invoke(iconData.size.toLong())) { _, binary ->
|
||||||
binary?.getOutputDataStream(cipherKey)?.use { outputStream ->
|
binary?.getOutputDataStream(mDatabase.binaryCache)?.use { outputStream ->
|
||||||
outputStream.write(customIconData)
|
outputStream.write(iconData)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -784,7 +792,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
return KdbContext.Entry
|
return KdbContext.Entry
|
||||||
} else if (ctx == KdbContext.EntryBinary && name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
|
} else if (ctx == KdbContext.EntryBinary && name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
|
||||||
if (ctxBinaryName != null && ctxBinaryValue != null) {
|
if (ctxBinaryName != null && ctxBinaryValue != null) {
|
||||||
ctxEntry?.putAttachment(Attachment(ctxBinaryName!!, ctxBinaryValue!!), mDatabase.binaryPool)
|
ctxEntry?.putAttachment(Attachment(ctxBinaryName!!, ctxBinaryValue!!), mDatabase.attachmentPool)
|
||||||
}
|
}
|
||||||
ctxBinaryName = null
|
ctxBinaryName = null
|
||||||
ctxBinaryValue = null
|
ctxBinaryValue = null
|
||||||
@@ -837,7 +845,13 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
val sDate = readString(xpp)
|
val sDate = readString(xpp)
|
||||||
var utcDate: Date? = null
|
var utcDate: Date? = null
|
||||||
|
|
||||||
if (mDatabase.kdbxVersion.toKotlinLong() >= DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
|
if (mDatabase.kdbxVersion.isBefore(FILE_VERSION_32_4)) {
|
||||||
|
try {
|
||||||
|
utcDate = DatabaseKDBXXML.DateFormatter.parse(sDate)
|
||||||
|
} catch (e: ParseException) {
|
||||||
|
// Catch with null test below
|
||||||
|
}
|
||||||
|
} else {
|
||||||
var buf = Base64.decode(sDate, BASE_64_FLAG)
|
var buf = Base64.decode(sDate, BASE_64_FLAG)
|
||||||
if (buf.size != 8) {
|
if (buf.size != 8) {
|
||||||
val buf8 = ByteArray(8)
|
val buf8 = ByteArray(8)
|
||||||
@@ -847,14 +861,6 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
|
|
||||||
val seconds = bytes64ToLong(buf)
|
val seconds = bytes64ToLong(buf)
|
||||||
utcDate = DateKDBXUtil.convertKDBX4Time(seconds)
|
utcDate = DateKDBXUtil.convertKDBX4Time(seconds)
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
try {
|
|
||||||
utcDate = DatabaseKDBXXML.DateFormatter.parse(sDate)
|
|
||||||
} catch (e: ParseException) {
|
|
||||||
// Catch with null test below
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return utcDate ?: Date(0L)
|
return utcDate ?: Date(0L)
|
||||||
@@ -978,11 +984,14 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
xpp.next() // Consume end tag
|
xpp.next() // Consume end tag
|
||||||
val id = Integer.parseInt(ref)
|
val id = Integer.parseInt(ref)
|
||||||
// A ref is not necessarily an index in Database V3.1
|
// A ref is not necessarily an index in Database V3.1
|
||||||
var binaryRetrieve = mDatabase.binaryPool[id]
|
var binaryRetrieve = mDatabase.attachmentPool[id]
|
||||||
// Create empty binary if not retrieved in pool
|
// Create empty binary if not retrieved in pool
|
||||||
if (binaryRetrieve == null) {
|
if (binaryRetrieve == null) {
|
||||||
binaryRetrieve = mDatabase.buildNewAttachment(cacheDirectory,
|
binaryRetrieve = mDatabase.buildNewAttachment(
|
||||||
compression = false, protection = false, binaryPoolId = id)
|
smallSize = false,
|
||||||
|
compression = false,
|
||||||
|
protection = false,
|
||||||
|
binaryPoolId = id)
|
||||||
}
|
}
|
||||||
return binaryRetrieve
|
return binaryRetrieve
|
||||||
}
|
}
|
||||||
@@ -1018,17 +1027,16 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
// Build the new binary and compress
|
// Build the new binary and compress
|
||||||
val binaryAttachment = mDatabase.buildNewAttachment(cacheDirectory, compressed, protected, binaryId)
|
val binaryAttachment = mDatabase.buildNewAttachment(
|
||||||
val binaryCipherKey = mDatabase.loadedCipherKey
|
isRAMSufficient.invoke(base64.length.toLong()), compressed, protected, binaryId)
|
||||||
?: throw IOException("Unable to retrieve cipher key to load binaries")
|
|
||||||
try {
|
try {
|
||||||
binaryAttachment.getOutputDataStream(binaryCipherKey).use { outputStream ->
|
binaryAttachment.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->
|
||||||
outputStream.write(Base64.decode(base64, BASE_64_FLAG))
|
outputStream.write(Base64.decode(base64, BASE_64_FLAG))
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to read base 64 attachment", e)
|
Log.e(TAG, "Unable to read base 64 attachment", e)
|
||||||
binaryAttachment.isCorrupted = true
|
binaryAttachment.isCorrupted = true
|
||||||
binaryAttachment.getOutputDataStream(binaryCipherKey).use { outputStream ->
|
binaryAttachment.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->
|
||||||
outputStream.write(base64.toByteArray())
|
outputStream.write(base64.toByteArray())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1057,9 +1065,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected)
|
val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected)
|
||||||
if (protect != null && protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)) {
|
if (protect != null && protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)) {
|
||||||
Base64.decode(xpp.safeNextText(), BASE_64_FLAG)?.let { data ->
|
Base64.decode(xpp.safeNextText(), BASE_64_FLAG)?.let { data ->
|
||||||
val plainText = ByteArray(data.size)
|
return randomStream?.processBytes(data)
|
||||||
randomStream?.processBytes(data, 0, data.size, plainText, 0)
|
|
||||||
return plainText
|
|
||||||
}
|
}
|
||||||
return ByteArray(0)
|
return ByteArray(0)
|
||||||
}
|
}
|
||||||
@@ -1083,17 +1089,6 @@ class DatabaseInputKDBX(cacheDirectory: File)
|
|||||||
private val TAG = DatabaseInputKDBX::class.java.name
|
private val TAG = DatabaseInputKDBX::class.java.name
|
||||||
|
|
||||||
private val DEFAULT_HISTORY_DAYS = UnsignedInt(365)
|
private val DEFAULT_HISTORY_DAYS = UnsignedInt(365)
|
||||||
|
|
||||||
@Throws(XmlPullParserException::class)
|
|
||||||
private fun createPullParser(readerStream: InputStream): XmlPullParser {
|
|
||||||
val xmlPullParserFactory = XmlPullParserFactory.newInstance()
|
|
||||||
xmlPullParserFactory.isNamespaceAware = false
|
|
||||||
|
|
||||||
val xpp = xmlPullParserFactory.newPullParser()
|
|
||||||
xpp.setInput(readerStream, null)
|
|
||||||
|
|
||||||
return xpp
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.file.output
|
package com.kunzisoft.keepass.database.file.output
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.utils.uIntTo4Bytes
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||||
import com.kunzisoft.keepass.stream.uIntTo4Bytes
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
|||||||
@@ -19,30 +19,29 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.file.output
|
package com.kunzisoft.keepass.database.file.output
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
|
import com.kunzisoft.encrypt.HashManager
|
||||||
|
import com.kunzisoft.keepass.database.crypto.HmacBlock
|
||||||
|
import com.kunzisoft.keepass.database.crypto.VariantDictionary
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||||
import com.kunzisoft.keepass.stream.*
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
|
||||||
import com.kunzisoft.keepass.utils.UnsignedLong
|
import com.kunzisoft.keepass.stream.MacOutputStream
|
||||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
import com.kunzisoft.keepass.utils.*
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.security.DigestOutputStream
|
import java.security.DigestOutputStream
|
||||||
import java.security.InvalidKeyException
|
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import javax.crypto.Mac
|
import javax.crypto.Mac
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
class DatabaseHeaderOutputKDBX @Throws(DatabaseOutputException::class)
|
class DatabaseHeaderOutputKDBX @Throws(IOException::class)
|
||||||
constructor(private val databaseKDBX: DatabaseKDBX,
|
constructor(private val databaseKDBX: DatabaseKDBX,
|
||||||
private val header: DatabaseHeaderKDBX,
|
private val header: DatabaseHeaderKDBX,
|
||||||
outputStream: OutputStream) {
|
outputStream: OutputStream) {
|
||||||
|
|
||||||
private val los: LittleEndianDataOutputStream
|
|
||||||
private val mos: MacOutputStream
|
private val mos: MacOutputStream
|
||||||
private val dos: DigestOutputStream
|
private val dos: DigestOutputStream
|
||||||
lateinit var headerHmac: ByteArray
|
lateinit var headerHmac: ByteArray
|
||||||
@@ -51,14 +50,6 @@ constructor(private val databaseKDBX: DatabaseKDBX,
|
|||||||
private set
|
private set
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
val md: MessageDigest
|
|
||||||
try {
|
|
||||||
md = MessageDigest.getInstance("SHA-256")
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw DatabaseOutputException("SHA-256 not implemented here.", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
databaseKDBX.makeFinalKey(header.masterSeed)
|
databaseKDBX.makeFinalKey(header.masterSeed)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
@@ -66,34 +57,26 @@ constructor(private val databaseKDBX: DatabaseKDBX,
|
|||||||
}
|
}
|
||||||
|
|
||||||
val hmacKey = databaseKDBX.hmacKey ?: throw DatabaseOutputException("HmacKey is not defined")
|
val hmacKey = databaseKDBX.hmacKey ?: throw DatabaseOutputException("HmacKey is not defined")
|
||||||
val hmac: Mac
|
val blockKey = HmacBlock.getHmacKey64(hmacKey, UnsignedLong.MAX_BYTES)
|
||||||
try {
|
val hmac: Mac = HmacBlock.getHmacSha256(blockKey)
|
||||||
hmac = Mac.getInstance("HmacSHA256")
|
|
||||||
val signingKey = SecretKeySpec(HmacBlockStream.getHmacKey64(hmacKey, UnsignedLong.MAX_VALUE), "HmacSHA256")
|
|
||||||
hmac.init(signingKey)
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw DatabaseOutputException(e)
|
|
||||||
} catch (e: InvalidKeyException) {
|
|
||||||
throw DatabaseOutputException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
dos = DigestOutputStream(outputStream, md)
|
val messageDigest: MessageDigest = HashManager.getHash256()
|
||||||
|
dos = DigestOutputStream(outputStream, messageDigest)
|
||||||
mos = MacOutputStream(dos, hmac)
|
mos = MacOutputStream(dos, hmac)
|
||||||
los = LittleEndianDataOutputStream(mos)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun output() {
|
fun output() {
|
||||||
|
|
||||||
los.writeUInt(DatabaseHeader.PWM_DBSIG_1)
|
mos.write4BytesUInt(DatabaseHeader.PWM_DBSIG_1)
|
||||||
los.writeUInt(DatabaseHeaderKDBX.DBSIG_2)
|
mos.write4BytesUInt(DatabaseHeaderKDBX.DBSIG_2)
|
||||||
los.writeUInt(header.version)
|
mos.write4BytesUInt(header.version)
|
||||||
|
|
||||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CipherID, uuidTo16Bytes(databaseKDBX.dataCipher))
|
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CipherID, uuidTo16Bytes(databaseKDBX.cipherUuid))
|
||||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CompressionFlags, uIntTo4Bytes(DatabaseHeaderKDBX.getFlagFromCompression(databaseKDBX.compressionAlgorithm)))
|
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CompressionFlags, uIntTo4Bytes(DatabaseHeaderKDBX.getFlagFromCompression(databaseKDBX.compressionAlgorithm)))
|
||||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.MasterSeed, header.masterSeed)
|
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.MasterSeed, header.masterSeed)
|
||||||
|
|
||||||
if (header.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
|
if (header.version.isBefore(FILE_VERSION_32_4)) {
|
||||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.TransformSeed, header.transformSeed)
|
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.TransformSeed, header.transformSeed)
|
||||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.TransformRounds, longTo8Bytes(databaseKDBX.numberKeyEncryptionRounds))
|
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.TransformRounds, longTo8Bytes(databaseKDBX.numberKeyEncryptionRounds))
|
||||||
} else {
|
} else {
|
||||||
@@ -104,7 +87,7 @@ constructor(private val databaseKDBX: DatabaseKDBX,
|
|||||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.EncryptionIV, header.encryptionIV)
|
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.EncryptionIV, header.encryptionIV)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
|
if (header.version.isBefore(FILE_VERSION_32_4)) {
|
||||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.InnerRandomstreamKey, header.innerRandomStreamKey)
|
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.InnerRandomstreamKey, header.innerRandomStreamKey)
|
||||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.StreamStartBytes, header.streamStartBytes)
|
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.StreamStartBytes, header.streamStartBytes)
|
||||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.InnerRandomStreamID, uIntTo4Bytes(header.innerRandomStream!!.id))
|
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.InnerRandomStreamID, uIntTo4Bytes(header.innerRandomStream!!.id))
|
||||||
@@ -112,14 +95,13 @@ constructor(private val databaseKDBX: DatabaseKDBX,
|
|||||||
|
|
||||||
if (databaseKDBX.containsPublicCustomData()) {
|
if (databaseKDBX.containsPublicCustomData()) {
|
||||||
val bos = ByteArrayOutputStream()
|
val bos = ByteArrayOutputStream()
|
||||||
val los = LittleEndianDataOutputStream(bos)
|
VariantDictionary.serialize(databaseKDBX.publicCustomData, bos)
|
||||||
VariantDictionary.serialize(databaseKDBX.publicCustomData, los)
|
|
||||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.PublicCustomData, bos.toByteArray())
|
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.PublicCustomData, bos.toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.EndOfHeader, EndHeaderValue)
|
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.EndOfHeader, EndHeaderValue)
|
||||||
|
|
||||||
los.flush()
|
mos.flush()
|
||||||
hashOfHeader = dos.messageDigest.digest()
|
hashOfHeader = dos.messageDigest.digest()
|
||||||
headerHmac = mos.mac
|
headerHmac = mos.mac
|
||||||
}
|
}
|
||||||
@@ -127,11 +109,11 @@ constructor(private val databaseKDBX: DatabaseKDBX,
|
|||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun writeHeaderField(fieldId: Byte, pbData: ByteArray?) {
|
private fun writeHeaderField(fieldId: Byte, pbData: ByteArray?) {
|
||||||
// Write the field id
|
// Write the field id
|
||||||
los.write(fieldId.toInt())
|
mos.write(fieldId.toInt())
|
||||||
|
|
||||||
if (pbData != null) {
|
if (pbData != null) {
|
||||||
writeHeaderFieldSize(pbData.size)
|
writeHeaderFieldSize(pbData.size)
|
||||||
los.write(pbData)
|
mos.write(pbData)
|
||||||
} else {
|
} else {
|
||||||
writeHeaderFieldSize(0)
|
writeHeaderFieldSize(0)
|
||||||
}
|
}
|
||||||
@@ -139,10 +121,10 @@ constructor(private val databaseKDBX: DatabaseKDBX,
|
|||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun writeHeaderFieldSize(size: Int) {
|
private fun writeHeaderFieldSize(size: Int) {
|
||||||
if (header.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
|
if (header.version.isBefore(FILE_VERSION_32_4)) {
|
||||||
los.writeUShort(size)
|
mos.write2BytesUShort(size)
|
||||||
} else {
|
} else {
|
||||||
los.writeInt(size)
|
mos.write4BytesUInt(UnsignedInt(size))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,16 +19,16 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.file.output
|
package com.kunzisoft.keepass.database.file.output
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
import com.kunzisoft.encrypt.HashManager
|
||||||
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
import com.kunzisoft.keepass.utils.write2BytesUShort
|
||||||
|
import com.kunzisoft.keepass.utils.write4BytesUInt
|
||||||
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
|
||||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||||
import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream
|
|
||||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
|
||||||
import java.io.BufferedOutputStream
|
import java.io.BufferedOutputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -37,8 +37,6 @@ import java.security.*
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.CipherOutputStream
|
import javax.crypto.CipherOutputStream
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||||
outputStream: OutputStream)
|
outputStream: OutputStream)
|
||||||
@@ -67,30 +65,21 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
|
|
||||||
val finalKey = getFinalKey(header)
|
val finalKey = getFinalKey(header)
|
||||||
|
|
||||||
val cipher: Cipher
|
val cipher: Cipher = try {
|
||||||
cipher = try {
|
mDatabaseKDB.encryptionAlgorithm
|
||||||
when {
|
.cipherEngine.getCipher(Cipher.ENCRYPT_MODE,
|
||||||
mDatabaseKDB.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael->
|
finalKey ?: ByteArray(0),
|
||||||
CipherFactory.getInstance("AES/CBC/PKCS5Padding")
|
header.encryptionIV)
|
||||||
mDatabaseKDB.encryptionAlgorithm === EncryptionAlgorithm.Twofish ->
|
|
||||||
CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING")
|
|
||||||
else ->
|
|
||||||
throw Exception()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw DatabaseOutputException("Algorithm not supported.", e)
|
throw IOException("Algorithm not supported.", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
cipher.init(Cipher.ENCRYPT_MODE,
|
|
||||||
SecretKeySpec(finalKey, "AES"),
|
|
||||||
IvParameterSpec(header.encryptionIV))
|
|
||||||
val cos = CipherOutputStream(mOutputStream, cipher)
|
val cos = CipherOutputStream(mOutputStream, cipher)
|
||||||
val bos = BufferedOutputStream(cos)
|
val bos = BufferedOutputStream(cos)
|
||||||
outputPlanGroupAndEntries(bos)
|
outputPlanGroupAndEntries(bos)
|
||||||
bos.flush()
|
bos.flush()
|
||||||
bos.close()
|
bos.close()
|
||||||
|
|
||||||
} catch (e: InvalidKeyException) {
|
} catch (e: InvalidKeyException) {
|
||||||
throw DatabaseOutputException("Invalid key", e)
|
throw DatabaseOutputException("Invalid key", e)
|
||||||
} catch (e: InvalidAlgorithmParameterException) {
|
} catch (e: InvalidAlgorithmParameterException) {
|
||||||
@@ -116,11 +105,11 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
header.signature2 = DatabaseHeaderKDB.DBSIG_2
|
header.signature2 = DatabaseHeaderKDB.DBSIG_2
|
||||||
header.flags = DatabaseHeaderKDB.FLAG_SHA2
|
header.flags = DatabaseHeaderKDB.FLAG_SHA2
|
||||||
|
|
||||||
when {
|
when (mDatabaseKDB.encryptionAlgorithm) {
|
||||||
mDatabaseKDB.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael -> {
|
EncryptionAlgorithm.AESRijndael -> {
|
||||||
header.flags = UnsignedInt(header.flags.toKotlinInt() or DatabaseHeaderKDB.FLAG_RIJNDAEL.toKotlinInt())
|
header.flags = UnsignedInt(header.flags.toKotlinInt() or DatabaseHeaderKDB.FLAG_RIJNDAEL.toKotlinInt())
|
||||||
}
|
}
|
||||||
mDatabaseKDB.encryptionAlgorithm === EncryptionAlgorithm.Twofish -> {
|
EncryptionAlgorithm.Twofish -> {
|
||||||
header.flags = UnsignedInt(header.flags.toKotlinInt() or DatabaseHeaderKDB.FLAG_TWOFISH.toKotlinInt())
|
header.flags = UnsignedInt(header.flags.toKotlinInt() or DatabaseHeaderKDB.FLAG_TWOFISH.toKotlinInt())
|
||||||
}
|
}
|
||||||
else -> throw DatabaseOutputException("Unsupported algorithm.")
|
else -> throw DatabaseOutputException("Unsupported algorithm.")
|
||||||
@@ -133,26 +122,11 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
|
|
||||||
setIVs(header)
|
setIVs(header)
|
||||||
|
|
||||||
// Content checksum
|
|
||||||
val messageDigest: MessageDigest?
|
|
||||||
try {
|
|
||||||
messageDigest = MessageDigest.getInstance("SHA-256")
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw DatabaseOutputException("SHA-256 not implemented here.", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header checksum
|
// Header checksum
|
||||||
val headerDigest: MessageDigest
|
val headerDigest: MessageDigest = HashManager.getHash256()
|
||||||
try {
|
|
||||||
headerDigest = MessageDigest.getInstance("SHA-256")
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw DatabaseOutputException("SHA-256 not implemented here.", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
var nos = NullOutputStream()
|
|
||||||
val headerDos = DigestOutputStream(nos, headerDigest)
|
|
||||||
|
|
||||||
// Output header for the purpose of calculating the header checksum
|
// Output header for the purpose of calculating the header checksum
|
||||||
|
val headerDos = DigestOutputStream(NullOutputStream(), headerDigest)
|
||||||
var pho = DatabaseHeaderOutputKDB(header, headerDos)
|
var pho = DatabaseHeaderOutputKDB(header, headerDos)
|
||||||
try {
|
try {
|
||||||
pho.outputStart()
|
pho.outputStart()
|
||||||
@@ -165,9 +139,11 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
val headerHash = headerDigest.digest()
|
val headerHash = headerDigest.digest()
|
||||||
headerHashBlock = getHeaderHashBuffer(headerHash)
|
headerHashBlock = getHeaderHashBuffer(headerHash)
|
||||||
|
|
||||||
|
// Content checksum
|
||||||
|
val messageDigest: MessageDigest = HashManager.getHash256()
|
||||||
|
|
||||||
// Output database for the purpose of calculating the content checksum
|
// Output database for the purpose of calculating the content checksum
|
||||||
nos = NullOutputStream()
|
val dos = DigestOutputStream(NullOutputStream(), messageDigest)
|
||||||
val dos = DigestOutputStream(nos, messageDigest)
|
|
||||||
val bos = BufferedOutputStream(dos)
|
val bos = BufferedOutputStream(dos)
|
||||||
try {
|
try {
|
||||||
outputPlanGroupAndEntries(bos)
|
outputPlanGroupAndEntries(bos)
|
||||||
@@ -177,7 +153,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
throw DatabaseOutputException("Failed to generate checksum.", e)
|
throw DatabaseOutputException("Failed to generate checksum.", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
header.contentsHash = messageDigest!!.digest()
|
header.contentsHash = messageDigest.digest()
|
||||||
|
|
||||||
// Output header for real output, containing content hash
|
// Output header for real output, containing content hash
|
||||||
pho = DatabaseHeaderOutputKDB(header, outputStream)
|
pho = DatabaseHeaderOutputKDB(header, outputStream)
|
||||||
@@ -195,17 +171,19 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
return header
|
return header
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("CAST_NEVER_SUCCEEDS")
|
class NullOutputStream : OutputStream() {
|
||||||
|
override fun write(oneByte: Int) {}
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(DatabaseOutputException::class)
|
@Throws(DatabaseOutputException::class)
|
||||||
fun outputPlanGroupAndEntries(outputStream: OutputStream) {
|
fun outputPlanGroupAndEntries(outputStream: OutputStream) {
|
||||||
val littleEndianDataOutputStream = LittleEndianDataOutputStream(outputStream)
|
|
||||||
|
|
||||||
// useHeaderHash
|
// useHeaderHash
|
||||||
if (headerHashBlock != null) {
|
if (headerHashBlock != null) {
|
||||||
try {
|
try {
|
||||||
littleEndianDataOutputStream.writeUShort(0x0000)
|
outputStream.write2BytesUShort(0x0000)
|
||||||
littleEndianDataOutputStream.writeInt(headerHashBlock!!.size)
|
outputStream.write4BytesUInt(UnsignedInt(headerHashBlock!!.size))
|
||||||
littleEndianDataOutputStream.write(headerHashBlock!!)
|
outputStream.write(headerHashBlock!!)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
throw DatabaseOutputException("Failed to output header hash.", e)
|
throw DatabaseOutputException("Failed to output header hash.", e)
|
||||||
}
|
}
|
||||||
@@ -217,7 +195,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
}
|
}
|
||||||
// Entries
|
// Entries
|
||||||
mDatabaseKDB.doForEachEntryInIndex { entry ->
|
mDatabaseKDB.doForEachEntryInIndex { entry ->
|
||||||
EntryOutputKDB(entry, outputStream, mDatabaseKDB.loadedCipherKey).output()
|
EntryOutputKDB(mDatabaseKDB, entry, outputStream).output()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,24 +230,22 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun writeExtData(headerDigest: ByteArray, os: OutputStream) {
|
private fun writeExtData(headerDigest: ByteArray, outputStream: OutputStream) {
|
||||||
val los = LittleEndianDataOutputStream(os)
|
writeExtDataField(outputStream, 0x0001, headerDigest, headerDigest.size)
|
||||||
|
|
||||||
writeExtDataField(los, 0x0001, headerDigest, headerDigest.size)
|
|
||||||
val headerRandom = ByteArray(32)
|
val headerRandom = ByteArray(32)
|
||||||
val rand = SecureRandom()
|
val rand = SecureRandom()
|
||||||
rand.nextBytes(headerRandom)
|
rand.nextBytes(headerRandom)
|
||||||
writeExtDataField(los, 0x0002, headerRandom, headerRandom.size)
|
writeExtDataField(outputStream, 0x0002, headerRandom, headerRandom.size)
|
||||||
writeExtDataField(los, 0xFFFF, null, 0)
|
writeExtDataField(outputStream, 0xFFFF, null, 0)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun writeExtDataField(los: LittleEndianDataOutputStream, fieldType: Int, data: ByteArray?, fieldSize: Int) {
|
private fun writeExtDataField(outputStream: OutputStream, fieldType: Int, data: ByteArray?, fieldSize: Int) {
|
||||||
los.writeUShort(fieldType)
|
outputStream.write2BytesUShort(fieldType)
|
||||||
los.writeInt(fieldSize)
|
outputStream.write4BytesUInt(UnsignedInt(fieldSize))
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
los.write(data)
|
outputStream.write(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,12 +22,12 @@ package com.kunzisoft.keepass.database.file.output
|
|||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Xml
|
import android.util.Xml
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
import com.kunzisoft.encrypt.StreamCipher
|
||||||
import com.kunzisoft.keepass.crypto.CrsAlgorithm
|
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
|
||||||
import com.kunzisoft.keepass.crypto.StreamCipherFactory
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
|
import com.kunzisoft.keepass.database.crypto.CipherEngine
|
||||||
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
||||||
import com.kunzisoft.keepass.database.element.DeletedObject
|
import com.kunzisoft.keepass.database.element.DeletedObject
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
@@ -41,11 +41,12 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString
|
|||||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||||
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||||
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseKDBXXML
|
import com.kunzisoft.keepass.database.file.DatabaseKDBXXML
|
||||||
import com.kunzisoft.keepass.database.file.DateKDBXUtil
|
import com.kunzisoft.keepass.database.file.DateKDBXUtil
|
||||||
import com.kunzisoft.keepass.stream.*
|
import com.kunzisoft.keepass.stream.HashedBlockOutputStream
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
import com.kunzisoft.keepass.stream.HmacBlockOutputStream
|
||||||
import org.bouncycastle.crypto.StreamCipher
|
import com.kunzisoft.keepass.utils.*
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import org.xmlpull.v1.XmlSerializer
|
import org.xmlpull.v1.XmlSerializer
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -75,15 +76,14 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
engine = CipherFactory.getInstance(mDatabaseKDBX.dataCipher)
|
engine = EncryptionAlgorithm.getFrom(mDatabaseKDBX.cipherUuid).cipherEngine
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
throw DatabaseOutputException("No such cipher", e)
|
throw DatabaseOutputException("No such cipher", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
header = outputHeader(mOutputStream)
|
header = outputHeader(mOutputStream)
|
||||||
|
|
||||||
val osPlain: OutputStream
|
val osPlain: OutputStream = if (header!!.version.isBefore(FILE_VERSION_32_4)) {
|
||||||
osPlain = if (header!!.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
|
|
||||||
val cos = attachStreamEncryptor(header!!, mOutputStream)
|
val cos = attachStreamEncryptor(header!!, mOutputStream)
|
||||||
cos.write(header!!.streamStartBytes)
|
cos.write(header!!.streamStartBytes)
|
||||||
|
|
||||||
@@ -95,19 +95,19 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
attachStreamEncryptor(header!!, HmacBlockOutputStream(mOutputStream, mDatabaseKDBX.hmacKey!!))
|
attachStreamEncryptor(header!!, HmacBlockOutputStream(mOutputStream, mDatabaseKDBX.hmacKey!!))
|
||||||
}
|
}
|
||||||
|
|
||||||
val osXml: OutputStream
|
val xmlOutputStream: OutputStream
|
||||||
try {
|
try {
|
||||||
osXml = when(mDatabaseKDBX.compressionAlgorithm) {
|
xmlOutputStream = when(mDatabaseKDBX.compressionAlgorithm) {
|
||||||
CompressionAlgorithm.GZip -> GZIPOutputStream(osPlain)
|
CompressionAlgorithm.GZip -> GZIPOutputStream(osPlain)
|
||||||
else -> osPlain
|
else -> osPlain
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header!!.version.toKotlinLong() >= DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
|
if (!header!!.version.isBefore(FILE_VERSION_32_4)) {
|
||||||
outputInnerHeader(mDatabaseKDBX, header!!, osXml)
|
outputInnerHeader(mDatabaseKDBX, header!!, xmlOutputStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
outputDatabase(osXml)
|
outputDatabase(xmlOutputStream)
|
||||||
osXml.close()
|
xmlOutputStream.close()
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
throw DatabaseOutputException(e)
|
throw DatabaseOutputException(e)
|
||||||
} catch (e: IllegalStateException) {
|
} catch (e: IllegalStateException) {
|
||||||
@@ -122,45 +122,42 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun outputInnerHeader(database: DatabaseKDBX,
|
private fun outputInnerHeader(database: DatabaseKDBX,
|
||||||
header: DatabaseHeaderKDBX,
|
header: DatabaseHeaderKDBX,
|
||||||
outputStream: OutputStream) {
|
dataOutputStream: OutputStream) {
|
||||||
val dataOutputStream = LittleEndianDataOutputStream(outputStream)
|
|
||||||
|
|
||||||
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID)
|
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID)
|
||||||
dataOutputStream.writeInt(4)
|
dataOutputStream.write4BytesUInt(UnsignedInt(4))
|
||||||
if (header.innerRandomStream == null)
|
if (header.innerRandomStream == null)
|
||||||
throw IOException("Can't write innerRandomStream")
|
throw IOException("Can't write innerRandomStream")
|
||||||
dataOutputStream.writeUInt(header.innerRandomStream!!.id)
|
dataOutputStream.write4BytesUInt(header.innerRandomStream!!.id)
|
||||||
|
|
||||||
val streamKeySize = header.innerRandomStreamKey.size
|
val streamKeySize = header.innerRandomStreamKey.size
|
||||||
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey)
|
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey)
|
||||||
dataOutputStream.writeInt(streamKeySize)
|
dataOutputStream.write4BytesUInt(UnsignedInt(streamKeySize))
|
||||||
dataOutputStream.write(header.innerRandomStreamKey)
|
dataOutputStream.write(header.innerRandomStreamKey)
|
||||||
|
|
||||||
database.loadedCipherKey?.let { binaryCipherKey ->
|
val binaryCache = database.binaryCache
|
||||||
database.binaryPool.doForEachOrderedBinaryWithoutDuplication { _, binary ->
|
database.attachmentPool.doForEachOrderedBinaryWithoutDuplication { _, binary ->
|
||||||
// Force decompression to add binary in header
|
// Force decompression to add binary in header
|
||||||
binary.decompress(binaryCipherKey)
|
binary.decompress(binaryCache)
|
||||||
// Write type binary
|
// Write type binary
|
||||||
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
|
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
|
||||||
// Write size
|
// Write size
|
||||||
dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(binary.getSize() + 1))
|
dataOutputStream.write4BytesUInt(UnsignedInt.fromKotlinLong(binary.getSize() + 1))
|
||||||
// Write protected flag
|
// Write protected flag
|
||||||
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
|
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
|
||||||
if (binary.isProtected) {
|
if (binary.isProtected) {
|
||||||
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
||||||
}
|
}
|
||||||
dataOutputStream.writeByte(flag)
|
dataOutputStream.writeByte(flag)
|
||||||
|
|
||||||
binary.getInputDataStream(binaryCipherKey).use { inputStream ->
|
binary.getInputDataStream(binaryCache).use { inputStream ->
|
||||||
inputStream.readAllBytes { buffer ->
|
inputStream.readAllBytes { buffer ->
|
||||||
dataOutputStream.write(buffer)
|
dataOutputStream.write(buffer)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} ?: Log.e(TAG, "Unable to retrieve cipher key to write head binaries")
|
}
|
||||||
|
|
||||||
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader)
|
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader)
|
||||||
dataOutputStream.writeInt(0)
|
dataOutputStream.write4BytesUInt(UnsignedInt(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
@@ -270,7 +267,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
writeUuid(DatabaseKDBXXML.ElemLastTopVisibleGroup, mDatabaseKDBX.lastTopVisibleGroupUUID)
|
writeUuid(DatabaseKDBXXML.ElemLastTopVisibleGroup, mDatabaseKDBX.lastTopVisibleGroupUUID)
|
||||||
|
|
||||||
// Seem to work properly if always in meta
|
// Seem to work properly if always in meta
|
||||||
if (header!!.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong())
|
if (header!!.version.isBefore(FILE_VERSION_32_4))
|
||||||
writeMetaBinaries()
|
writeMetaBinaries()
|
||||||
|
|
||||||
writeCustomData(mDatabaseKDBX.customData)
|
writeCustomData(mDatabaseKDBX.customData)
|
||||||
@@ -282,8 +279,6 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
private fun attachStreamEncryptor(header: DatabaseHeaderKDBX, os: OutputStream): CipherOutputStream {
|
private fun attachStreamEncryptor(header: DatabaseHeaderKDBX, os: OutputStream): CipherOutputStream {
|
||||||
val cipher: Cipher
|
val cipher: Cipher
|
||||||
try {
|
try {
|
||||||
//mDatabaseKDBX.makeFinalKey(header.masterSeed, mDatabaseKDBX.kdfParameters);
|
|
||||||
|
|
||||||
cipher = engine!!.getCipher(Cipher.ENCRYPT_MODE, mDatabaseKDBX.finalKey!!, header.encryptionIV)
|
cipher = engine!!.getCipher(Cipher.ENCRYPT_MODE, mDatabaseKDBX.finalKey!!, header.encryptionIV)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw DatabaseOutputException("Invalid algorithm.", e)
|
throw DatabaseOutputException("Invalid algorithm.", e)
|
||||||
@@ -314,7 +309,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
Log.e(TAG, "Unable to retrieve header", unknownKDF)
|
Log.e(TAG, "Unable to retrieve header", unknownKDF)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
|
if (header.version.isBefore(FILE_VERSION_32_4)) {
|
||||||
header.innerRandomStream = CrsAlgorithm.Salsa20
|
header.innerRandomStream = CrsAlgorithm.Salsa20
|
||||||
header.innerRandomStreamKey = ByteArray(32)
|
header.innerRandomStreamKey = ByteArray(32)
|
||||||
} else {
|
} else {
|
||||||
@@ -324,12 +319,12 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
random.nextBytes(header.innerRandomStreamKey)
|
random.nextBytes(header.innerRandomStreamKey)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
|
randomStream = CrsAlgorithm.getCipher(header.innerRandomStream, header.innerRandomStreamKey)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw DatabaseOutputException(e)
|
throw DatabaseOutputException(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
|
if (header.version.isBefore(FILE_VERSION_32_4)) {
|
||||||
random.nextBytes(header.streamStartBytes)
|
random.nextBytes(header.streamStartBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,21 +333,20 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
|
|
||||||
@Throws(DatabaseOutputException::class)
|
@Throws(DatabaseOutputException::class)
|
||||||
override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDBX {
|
override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDBX {
|
||||||
|
|
||||||
val header = DatabaseHeaderKDBX(mDatabaseKDBX)
|
|
||||||
setIVs(header)
|
|
||||||
|
|
||||||
val pho = DatabaseHeaderOutputKDBX(mDatabaseKDBX, header, outputStream)
|
|
||||||
try {
|
try {
|
||||||
|
val header = DatabaseHeaderKDBX(mDatabaseKDBX)
|
||||||
|
setIVs(header)
|
||||||
|
|
||||||
|
val pho = DatabaseHeaderOutputKDBX(mDatabaseKDBX, header, outputStream)
|
||||||
pho.output()
|
pho.output()
|
||||||
|
|
||||||
|
hashOfHeader = pho.hashOfHeader
|
||||||
|
headerHmac = pho.headerHmac
|
||||||
|
|
||||||
|
return header
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
throw DatabaseOutputException("Failed to output the header.", e)
|
throw DatabaseOutputException("Failed to output the header.", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
hashOfHeader = pho.hashOfHeader
|
|
||||||
headerHmac = pho.headerHmac
|
|
||||||
|
|
||||||
return header
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
@@ -429,7 +423,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
|
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
private fun writeObject(name: String, value: Date) {
|
private fun writeObject(name: String, value: Date) {
|
||||||
if (header!!.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
|
if (header!!.version.isBefore(FILE_VERSION_32_4)) {
|
||||||
writeObject(name, DatabaseKDBXXML.DateFormatter.format(value))
|
writeObject(name, DatabaseKDBXXML.DateFormatter.format(value))
|
||||||
} else {
|
} else {
|
||||||
val dt = DateTime(value)
|
val dt = DateTime(value)
|
||||||
@@ -494,31 +488,30 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
// With kdbx4, don't use this method because binaries are in header file
|
// With kdbx4, don't use this method because binaries are in header file
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
private fun writeMetaBinaries() {
|
private fun writeMetaBinaries() {
|
||||||
mDatabaseKDBX.loadedCipherKey?.let { binaryCipherKey ->
|
xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
|
// Use indexes because necessarily (binary header ref is the order)
|
||||||
// Use indexes because necessarily (binary header ref is the order)
|
val binaryCache = mDatabaseKDBX.binaryCache
|
||||||
mDatabaseKDBX.binaryPool.doForEachOrderedBinaryWithoutDuplication { index, binary ->
|
mDatabaseKDBX.attachmentPool.doForEachOrderedBinaryWithoutDuplication { index, binary ->
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
||||||
xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
|
xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
|
||||||
if (binary.getSize() > 0) {
|
if (binary.getSize() > 0) {
|
||||||
if (binary.isCompressed) {
|
if (binary.isCompressed) {
|
||||||
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
|
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// Write the XML
|
// Write the XML
|
||||||
binary.getInputDataStream(binaryCipherKey).use { inputStream ->
|
binary.getInputDataStream(binaryCache).use { inputStream ->
|
||||||
inputStream.readAllBytes { buffer ->
|
inputStream.readAllBytes { buffer ->
|
||||||
xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
|
xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to write binary", e)
|
Log.e(TAG, "Unable to write binary", e)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
|
||||||
}
|
}
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemBinaries)
|
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
||||||
} ?: Log.e(TAG, "Unable to retrieve cipher key to write binaries")
|
}
|
||||||
|
xml.endTag(null, DatabaseKDBXXML.ElemBinaries)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
@@ -584,12 +577,8 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
if (protect) {
|
if (protect) {
|
||||||
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
|
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
|
||||||
val data = value.toString().toByteArray()
|
val data = value.toString().toByteArray()
|
||||||
val dataLength = data.size
|
val encoded = randomStream?.processBytes(data) ?: ByteArray(0)
|
||||||
if (data.isNotEmpty()) {
|
xml.text(String(Base64.encode(encoded, BASE_64_FLAG)))
|
||||||
val encoded = ByteArray(dataLength)
|
|
||||||
randomStream!!.processBytes(data, 0, dataLength, encoded, 0)
|
|
||||||
xml.text(String(Base64.encode(encoded, BASE_64_FLAG)))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
xml.text(value.toString())
|
xml.text(value.toString())
|
||||||
}
|
}
|
||||||
@@ -612,7 +601,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
private fun writeEntryBinaries(binaries: LinkedHashMap<String, Int>) {
|
private fun writeEntryBinaries(binaries: LinkedHashMap<String, Int>) {
|
||||||
for ((label, poolId) in binaries) {
|
for ((label, poolId) in binaries) {
|
||||||
// Retrieve the right index with the poolId, don't use ref because of header in DatabaseV4
|
// Retrieve the right index with the poolId, don't use ref because of header in DatabaseV4
|
||||||
mDatabaseKDBX.binaryPool.getBinaryIndexFromKey(poolId)?.toString()?.let { indexString ->
|
mDatabaseKDBX.attachmentPool.getBinaryIndexFromKey(poolId)?.toString()?.let { indexString ->
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemKey)
|
xml.startTag(null, DatabaseKDBXXML.ElemKey)
|
||||||
xml.text(safeXmlString(label))
|
xml.text(safeXmlString(label))
|
||||||
@@ -699,39 +688,38 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
|
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
private fun writeCustomIconList() {
|
private fun writeCustomIconList() {
|
||||||
mDatabaseKDBX.loadedCipherKey?.let { cipherKey ->
|
var firstElement = true
|
||||||
var firstElement = true
|
val binaryCache = mDatabaseKDBX.binaryCache
|
||||||
mDatabaseKDBX.iconsManager.doForEachCustomIcon { iconCustom, binary ->
|
mDatabaseKDBX.iconsManager.doForEachCustomIcon { iconCustom, binary ->
|
||||||
if (binary.dataExists()) {
|
if (binary.dataExists()) {
|
||||||
// Write the parent tag
|
// Write the parent tag
|
||||||
if (firstElement) {
|
if (firstElement) {
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemCustomIcons)
|
xml.startTag(null, DatabaseKDBXXML.ElemCustomIcons)
|
||||||
firstElement = false
|
firstElement = false
|
||||||
}
|
|
||||||
|
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemCustomIconItem)
|
|
||||||
|
|
||||||
writeUuid(DatabaseKDBXXML.ElemCustomIconItemID, iconCustom.uuid)
|
|
||||||
var customImageData = ByteArray(0)
|
|
||||||
try {
|
|
||||||
binary.getInputDataStream(cipherKey).use { inputStream ->
|
|
||||||
customImageData = inputStream.readBytes()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to write custom icon", e)
|
|
||||||
} finally {
|
|
||||||
writeObject(DatabaseKDBXXML.ElemCustomIconItemData,
|
|
||||||
String(Base64.encode(customImageData, BASE_64_FLAG)))
|
|
||||||
}
|
|
||||||
|
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemCustomIconItem)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
xml.startTag(null, DatabaseKDBXXML.ElemCustomIconItem)
|
||||||
|
|
||||||
|
writeUuid(DatabaseKDBXXML.ElemCustomIconItemID, iconCustom.uuid)
|
||||||
|
var customImageData = ByteArray(0)
|
||||||
|
try {
|
||||||
|
binary.getInputDataStream(binaryCache).use { inputStream ->
|
||||||
|
customImageData = inputStream.readBytes()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to write custom icon", e)
|
||||||
|
} finally {
|
||||||
|
writeObject(DatabaseKDBXXML.ElemCustomIconItemData,
|
||||||
|
String(Base64.encode(customImageData, BASE_64_FLAG)))
|
||||||
|
}
|
||||||
|
|
||||||
|
xml.endTag(null, DatabaseKDBXXML.ElemCustomIconItem)
|
||||||
}
|
}
|
||||||
// Close the parent tag
|
}
|
||||||
if (!firstElement) {
|
// Close the parent tag
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemCustomIcons)
|
if (!firstElement) {
|
||||||
}
|
xml.endTag(null, DatabaseKDBXXML.ElemCustomIcons)
|
||||||
} ?: Log.e(TAG, "Unable to retrieve cipher key to write custom icons")
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun safeXmlString(text: String): String {
|
private fun safeXmlString(text: String): String {
|
||||||
|
|||||||
@@ -19,13 +19,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.file.output
|
package com.kunzisoft.keepass.database.file.output
|
||||||
|
|
||||||
import android.util.Log
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||||
import com.kunzisoft.keepass.stream.*
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.utils.StringDatabaseKDBUtils
|
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
@@ -33,9 +30,9 @@ import java.nio.charset.Charset
|
|||||||
/**
|
/**
|
||||||
* Output the GroupKDB to the stream
|
* Output the GroupKDB to the stream
|
||||||
*/
|
*/
|
||||||
class EntryOutputKDB(private val mEntry: EntryKDB,
|
class EntryOutputKDB(private val mDatabase: DatabaseKDB,
|
||||||
private val mOutputStream: OutputStream,
|
private val mEntry: EntryKDB,
|
||||||
private val mCipherKey: Database.LoadedKey?) {
|
private val mOutputStream: OutputStream) {
|
||||||
|
|
||||||
//NOTE: Need be to careful about using ints. The actual type written to file is a unsigned int
|
//NOTE: Need be to careful about using ints. The actual type written to file is a unsigned int
|
||||||
@Throws(DatabaseOutputException::class)
|
@Throws(DatabaseOutputException::class)
|
||||||
@@ -59,15 +56,15 @@ class EntryOutputKDB(private val mEntry: EntryKDB,
|
|||||||
// Title
|
// Title
|
||||||
//byte[] title = mEntry.title.getBytes("UTF-8");
|
//byte[] title = mEntry.title.getBytes("UTF-8");
|
||||||
mOutputStream.write(TITLE_FIELD_TYPE)
|
mOutputStream.write(TITLE_FIELD_TYPE)
|
||||||
StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.title)
|
writeStringToStream(mOutputStream, mEntry.title)
|
||||||
|
|
||||||
// URL
|
// URL
|
||||||
mOutputStream.write(URL_FIELD_TYPE)
|
mOutputStream.write(URL_FIELD_TYPE)
|
||||||
StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.url)
|
writeStringToStream(mOutputStream, mEntry.url)
|
||||||
|
|
||||||
// Username
|
// Username
|
||||||
mOutputStream.write(USERNAME_FIELD_TYPE)
|
mOutputStream.write(USERNAME_FIELD_TYPE)
|
||||||
StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.username)
|
writeStringToStream(mOutputStream, mEntry.username)
|
||||||
|
|
||||||
// Password
|
// Password
|
||||||
mOutputStream.write(PASSWORD_FIELD_TYPE)
|
mOutputStream.write(PASSWORD_FIELD_TYPE)
|
||||||
@@ -75,7 +72,7 @@ class EntryOutputKDB(private val mEntry: EntryKDB,
|
|||||||
|
|
||||||
// Additional
|
// Additional
|
||||||
mOutputStream.write(ADDITIONAL_FIELD_TYPE)
|
mOutputStream.write(ADDITIONAL_FIELD_TYPE)
|
||||||
StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.notes)
|
writeStringToStream(mOutputStream, mEntry.notes)
|
||||||
|
|
||||||
// Create date
|
// Create date
|
||||||
writeDate(CREATE_FIELD_TYPE, dateTo5Bytes(mEntry.creationTime.date))
|
writeDate(CREATE_FIELD_TYPE, dateTo5Bytes(mEntry.creationTime.date))
|
||||||
@@ -91,25 +88,23 @@ class EntryOutputKDB(private val mEntry: EntryKDB,
|
|||||||
|
|
||||||
// Binary description
|
// Binary description
|
||||||
mOutputStream.write(BINARY_DESC_FIELD_TYPE)
|
mOutputStream.write(BINARY_DESC_FIELD_TYPE)
|
||||||
StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mEntry.binaryDescription)
|
writeStringToStream(mOutputStream, mEntry.binaryDescription)
|
||||||
|
|
||||||
// Binary
|
// Binary
|
||||||
mCipherKey?.let { cipherKey ->
|
mOutputStream.write(BINARY_DATA_FIELD_TYPE)
|
||||||
mOutputStream.write(BINARY_DATA_FIELD_TYPE)
|
val binaryData = mEntry.getBinary(mDatabase.attachmentPool)
|
||||||
val binaryData = mEntry.binaryData
|
val binaryDataLength = binaryData?.getSize() ?: 0L
|
||||||
val binaryDataLength = binaryData?.getSize() ?: 0L
|
// Write data length
|
||||||
// Write data length
|
mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromKotlinLong(binaryDataLength)))
|
||||||
mOutputStream.write(uIntTo4Bytes(UnsignedInt.fromKotlinLong(binaryDataLength)))
|
// Write data
|
||||||
// Write data
|
if (binaryDataLength > 0) {
|
||||||
if (binaryDataLength > 0) {
|
binaryData?.getInputDataStream(mDatabase.binaryCache).use { inputStream ->
|
||||||
binaryData?.getInputDataStream(cipherKey).use { inputStream ->
|
inputStream?.readAllBytes { buffer ->
|
||||||
inputStream?.readAllBytes { buffer ->
|
mOutputStream.write(buffer)
|
||||||
mOutputStream.write(buffer)
|
|
||||||
}
|
|
||||||
inputStream?.close()
|
|
||||||
}
|
}
|
||||||
|
inputStream?.close()
|
||||||
}
|
}
|
||||||
} ?: Log.e(TAG, "Unable to retrieve cipher key to write entry binary")
|
}
|
||||||
|
|
||||||
// End
|
// End
|
||||||
mOutputStream.write(END_FIELD_TYPE)
|
mOutputStream.write(END_FIELD_TYPE)
|
||||||
|
|||||||
@@ -19,13 +19,13 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.file.output
|
package com.kunzisoft.keepass.database.file.output
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
import com.kunzisoft.keepass.utils.dateTo5Bytes
|
||||||
|
import com.kunzisoft.keepass.utils.uIntTo4Bytes
|
||||||
|
import com.kunzisoft.keepass.utils.uShortTo2Bytes
|
||||||
|
import com.kunzisoft.keepass.utils.writeStringToStream
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||||
import com.kunzisoft.keepass.stream.dateTo5Bytes
|
|
||||||
import com.kunzisoft.keepass.stream.uIntTo4Bytes
|
|
||||||
import com.kunzisoft.keepass.stream.uShortTo2Bytes
|
|
||||||
import com.kunzisoft.keepass.utils.StringDatabaseKDBUtils
|
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ class GroupOutputKDB(private val mGroup: GroupKDB,
|
|||||||
|
|
||||||
// Name
|
// Name
|
||||||
mOutputStream.write(NAME_FIELD_TYPE)
|
mOutputStream.write(NAME_FIELD_TYPE)
|
||||||
StringDatabaseKDBUtils.writeStringToStream(mOutputStream, mGroup.title)
|
writeStringToStream(mOutputStream, mGroup.title)
|
||||||
|
|
||||||
// Create date
|
// Create date
|
||||||
mOutputStream.write(CREATE_FIELD_TYPE)
|
mOutputStream.write(CREATE_FIELD_TYPE)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.database.search;
|
|||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static com.kunzisoft.keepass.stream.StreamBytesUtilsKt.uuidTo16Bytes;
|
import static com.kunzisoft.keepass.utils.StreamBytesUtilsKt.uuidTo16Bytes;
|
||||||
|
|
||||||
public class UuidUtil {
|
public class UuidUtil {
|
||||||
|
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ import androidx.core.content.res.ResourcesCompat
|
|||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import androidx.core.widget.ImageViewCompat
|
import androidx.core.widget.ImageViewCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.binary.BinaryCache
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryData
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
|
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -48,7 +48,7 @@ import kotlin.collections.HashMap
|
|||||||
/**
|
/**
|
||||||
* Factory class who build database icons dynamically, can assign an icon of IconPack, or a custom icon to an ImageView with a tint
|
* Factory class who build database icons dynamically, can assign an icon of IconPack, or a custom icon to an ImageView with a tint
|
||||||
*/
|
*/
|
||||||
class IconDrawableFactory(private val retrieveCipherKey : () -> Database.LoadedKey?,
|
class IconDrawableFactory(private val retrieveBinaryCache : () -> BinaryCache?,
|
||||||
private val retrieveCustomIconBinary : (iconId: UUID) -> BinaryData?) {
|
private val retrieveCustomIconBinary : (iconId: UUID) -> BinaryData?) {
|
||||||
|
|
||||||
/** customIconMap
|
/** customIconMap
|
||||||
@@ -69,7 +69,8 @@ class IconDrawableFactory(private val retrieveCipherKey : () -> Database.LoadedK
|
|||||||
private fun getIconSuperDrawable(context: Context, iconDraw: IconImageDraw, width: Int, tintColor: Int = Color.WHITE): SuperDrawable {
|
private fun getIconSuperDrawable(context: Context, iconDraw: IconImageDraw, width: Int, tintColor: Int = Color.WHITE): SuperDrawable {
|
||||||
val icon = iconDraw.getIconImageToDraw()
|
val icon = iconDraw.getIconImageToDraw()
|
||||||
val customIconBinary = retrieveCustomIconBinary(icon.custom.uuid)
|
val customIconBinary = retrieveCustomIconBinary(icon.custom.uuid)
|
||||||
if (customIconBinary != null && customIconBinary.dataExists()) {
|
val binaryCache = retrieveBinaryCache()
|
||||||
|
if (binaryCache != null && customIconBinary != null && customIconBinary.dataExists()) {
|
||||||
getIconDrawable(context.resources, icon.custom, customIconBinary)?.let {
|
getIconDrawable(context.resources, icon.custom, customIconBinary)?.let {
|
||||||
return SuperDrawable(it)
|
return SuperDrawable(it)
|
||||||
}
|
}
|
||||||
@@ -87,13 +88,13 @@ class IconDrawableFactory(private val retrieveCipherKey : () -> Database.LoadedK
|
|||||||
*/
|
*/
|
||||||
private fun getIconDrawable(resources: Resources, icon: IconImageCustom, iconCustomBinary: BinaryData?): Drawable? {
|
private fun getIconDrawable(resources: Resources, icon: IconImageCustom, iconCustomBinary: BinaryData?): Drawable? {
|
||||||
val patternIcon = PatternIcon(resources)
|
val patternIcon = PatternIcon(resources)
|
||||||
val cipherKey = retrieveCipherKey()
|
val binaryManager = retrieveBinaryCache()
|
||||||
if (cipherKey != null) {
|
if (binaryManager != null) {
|
||||||
val draw: Drawable? = customIconMap[icon.uuid]?.get()
|
val draw: Drawable? = customIconMap[icon.uuid]?.get()
|
||||||
if (draw == null) {
|
if (draw == null) {
|
||||||
iconCustomBinary?.let { binaryFile ->
|
iconCustomBinary?.let { binaryFile ->
|
||||||
try {
|
try {
|
||||||
var bitmap: Bitmap? = BitmapFactory.decodeStream(binaryFile.getInputDataStream(cipherKey))
|
var bitmap: Bitmap? = BitmapFactory.decodeStream(binaryFile.getInputDataStream(binaryManager))
|
||||||
bitmap?.let { bitmapIcon ->
|
bitmap?.let { bitmapIcon ->
|
||||||
bitmap = resize(bitmapIcon, patternIcon)
|
bitmap = resize(bitmapIcon, patternIcon)
|
||||||
val createdDraw = BitmapDrawable(resources, bitmap)
|
val createdDraw = BitmapDrawable(resources, bitmap)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.model
|
|||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryByte
|
import com.kunzisoft.keepass.database.element.binary.BinaryByte
|
||||||
import com.kunzisoft.keepass.utils.readEnum
|
import com.kunzisoft.keepass.utils.readEnum
|
||||||
import com.kunzisoft.keepass.utils.writeEnum
|
import com.kunzisoft.keepass.utils.writeEnum
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import kotlinx.coroutines.*
|
|
||||||
|
|
||||||
class AdvancedUnlockNotificationService : NotificationService() {
|
class AdvancedUnlockNotificationService : NotificationService() {
|
||||||
|
|
||||||
@@ -18,9 +17,6 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
|||||||
|
|
||||||
private var mActionTaskBinder = AdvancedUnlockBinder()
|
private var mActionTaskBinder = AdvancedUnlockBinder()
|
||||||
|
|
||||||
private var notificationTimeoutMilliSecs: Long = 0
|
|
||||||
private var mTimerJob: Job? = null
|
|
||||||
|
|
||||||
inner class AdvancedUnlockBinder: Binder() {
|
inner class AdvancedUnlockBinder: Binder() {
|
||||||
fun getCipherDatabase(databaseUri: Uri): CipherDatabaseEntity? {
|
fun getCipherDatabase(databaseUri: Uri): CipherDatabaseEntity? {
|
||||||
return mTempCipherDao.firstOrNull { it.databaseUri == databaseUri.toString()}
|
return mTempCipherDao.firstOrNull { it.databaseUri == databaseUri.toString()}
|
||||||
@@ -80,23 +76,11 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
|||||||
|
|
||||||
when (intent?.action) {
|
when (intent?.action) {
|
||||||
ACTION_TIMEOUT -> {
|
ACTION_TIMEOUT -> {
|
||||||
notificationTimeoutMilliSecs = PreferencesUtil.getAdvancedUnlockTimeout(this)
|
val notificationTimeoutMilliSecs = PreferencesUtil.getAdvancedUnlockTimeout(this)
|
||||||
// Not necessarily a foreground service
|
// Not necessarily a foreground service
|
||||||
if (mTimerJob == null && notificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
|
if (mTimerJob == null && notificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
|
||||||
mTimerJob = CoroutineScope(Dispatchers.Main).launch {
|
defineTimerJob(notificationBuilder, notificationTimeoutMilliSecs) {
|
||||||
val maxPos = 100
|
stopSelf()
|
||||||
val posDurationMills = notificationTimeoutMilliSecs / maxPos
|
|
||||||
for (pos in maxPos downTo 0) {
|
|
||||||
notificationBuilder.setProgress(maxPos, pos, false)
|
|
||||||
startForeground(notificationId, notificationBuilder.build())
|
|
||||||
delay(posDurationMills)
|
|
||||||
if (pos <= 0) {
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notificationManager?.cancel(notificationId)
|
|
||||||
mTimerJob = null
|
|
||||||
cancel()
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
startForeground(notificationId, notificationBuilder.build())
|
startForeground(notificationId, notificationBuilder.build())
|
||||||
@@ -118,7 +102,6 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
mTempCipherDao.clear()
|
mTempCipherDao.clear()
|
||||||
mTimerJob?.cancel()
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,13 +26,14 @@ import android.net.Uri
|
|||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.model.AttachmentState
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.StreamDirection
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
@@ -48,6 +49,8 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
|
|
||||||
private val mainScope = CoroutineScope(Dispatchers.Main)
|
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
|
private var mDatabase: Database? = Database.getInstance()
|
||||||
|
|
||||||
override fun retrieveChannelId(): String {
|
override fun retrieveChannelId(): String {
|
||||||
return CHANNEL_ATTACHMENT_ID
|
return CHANNEL_ATTACHMENT_ID
|
||||||
}
|
}
|
||||||
@@ -170,7 +173,8 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
putExtra(FILE_URI_KEY, attachmentNotification.uri)
|
putExtra(FILE_URI_KEY, attachmentNotification.uri)
|
||||||
}, PendingIntent.FLAG_CANCEL_CURRENT)
|
}, PendingIntent.FLAG_CANCEL_CURRENT)
|
||||||
|
|
||||||
val fileName = DocumentFile.fromSingleUri(this, attachmentNotification.uri)?.name ?: ""
|
val fileName = UriUtil.getFileData(this, attachmentNotification.uri)?.name
|
||||||
|
?: attachmentNotification.uri.path
|
||||||
|
|
||||||
val builder = buildNewNotification().apply {
|
val builder = buildNewNotification().apply {
|
||||||
when (attachmentNotification.entryAttachmentState.streamDirection) {
|
when (attachmentNotification.entryAttachmentState.streamDirection) {
|
||||||
@@ -285,11 +289,14 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
// Add action to the list on start
|
// Add action to the list on start
|
||||||
attachmentNotificationList.add(attachmentNotification)
|
attachmentNotificationList.add(attachmentNotification)
|
||||||
|
|
||||||
mainScope.launch {
|
mDatabase?.let { database ->
|
||||||
AttachmentFileAction(attachmentNotification,
|
mainScope.launch {
|
||||||
contentResolver).apply {
|
AttachmentFileAction(attachmentNotification,
|
||||||
listener = attachmentFileActionListener
|
database,
|
||||||
}.executeAction()
|
contentResolver).apply {
|
||||||
|
listener = attachmentFileActionListener
|
||||||
|
}.executeAction()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -313,6 +320,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
|
|
||||||
private class AttachmentFileAction(
|
private class AttachmentFileAction(
|
||||||
private val attachmentNotification: AttachmentNotification,
|
private val attachmentNotification: AttachmentNotification,
|
||||||
|
private val database: Database,
|
||||||
private val contentResolver: ContentResolver) {
|
private val contentResolver: ContentResolver) {
|
||||||
|
|
||||||
private val updateMinFrequency = 1000
|
private val updateMinFrequency = 1000
|
||||||
@@ -345,6 +353,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
when (streamDirection) {
|
when (streamDirection) {
|
||||||
StreamDirection.UPLOAD -> {
|
StreamDirection.UPLOAD -> {
|
||||||
BinaryDatabaseManager.uploadToDatabase(
|
BinaryDatabaseManager.uploadToDatabase(
|
||||||
|
database,
|
||||||
attachmentNotification.uri,
|
attachmentNotification.uri,
|
||||||
attachment.binaryData,
|
attachment.binaryData,
|
||||||
contentResolver,
|
contentResolver,
|
||||||
@@ -358,6 +367,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
}
|
}
|
||||||
StreamDirection.DOWNLOAD -> {
|
StreamDirection.DOWNLOAD -> {
|
||||||
BinaryDatabaseManager.downloadFromDatabase(
|
BinaryDatabaseManager.downloadFromDatabase(
|
||||||
|
database,
|
||||||
attachmentNotification.uri,
|
attachmentNotification.uri,
|
||||||
attachment.binaryData,
|
attachment.binaryData,
|
||||||
contentResolver,
|
contentResolver,
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
override val notificationId = 485
|
override val notificationId = 485
|
||||||
private var mEntryInfo: EntryInfo? = null
|
private var mEntryInfo: EntryInfo? = null
|
||||||
private var clipboardHelper: ClipboardHelper? = null
|
private var clipboardHelper: ClipboardHelper? = null
|
||||||
private var notificationTimeoutMilliSecs: Long = 0
|
private var mNotificationTimeoutMilliSecs: Long = 0
|
||||||
private var cleanCopyNotificationTimerTask: Thread? = null
|
|
||||||
|
|
||||||
override fun retrieveChannelId(): String {
|
override fun retrieveChannelId(): String {
|
||||||
return CHANNEL_CLIPBOARD_ID
|
return CHANNEL_CLIPBOARD_ID
|
||||||
@@ -70,7 +69,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
mEntryInfo = intent?.getParcelableExtra(EXTRA_ENTRY_INFO)
|
mEntryInfo = intent?.getParcelableExtra(EXTRA_ENTRY_INFO)
|
||||||
|
|
||||||
//Get settings
|
//Get settings
|
||||||
notificationTimeoutMilliSecs = PreferencesUtil.getClipboardTimeout(this)
|
mNotificationTimeoutMilliSecs = PreferencesUtil.getClipboardTimeout(this)
|
||||||
|
|
||||||
when {
|
when {
|
||||||
intent == null -> Log.w(TAG, "null intent")
|
intent == null -> Log.w(TAG, "null intent")
|
||||||
@@ -78,7 +77,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
newNotification(mEntryInfo?.title, constructListOfField(intent))
|
newNotification(mEntryInfo?.title, constructListOfField(intent))
|
||||||
}
|
}
|
||||||
ACTION_CLEAN_CLIPBOARD == intent.action -> {
|
ACTION_CLEAN_CLIPBOARD == intent.action -> {
|
||||||
stopTask(cleanCopyNotificationTimerTask)
|
mTimerJob?.cancel()
|
||||||
cleanClipboard()
|
cleanClipboard()
|
||||||
stopNotificationAndSendLockIfNeeded()
|
stopNotificationAndSendLockIfNeeded()
|
||||||
}
|
}
|
||||||
@@ -121,7 +120,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun newNotification(title: String?, fieldsToAdd: ArrayList<ClipboardEntryNotificationField>) {
|
private fun newNotification(title: String?, fieldsToAdd: ArrayList<ClipboardEntryNotificationField>) {
|
||||||
stopTask(cleanCopyNotificationTimerTask)
|
mTimerJob?.cancel()
|
||||||
|
|
||||||
val builder = buildNewNotification()
|
val builder = buildNewNotification()
|
||||||
.setSmallIcon(R.drawable.notification_ic_clipboard_key_24dp)
|
.setSmallIcon(R.drawable.notification_ic_clipboard_key_24dp)
|
||||||
@@ -147,7 +146,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun copyField(fieldToCopy: ClipboardEntryNotificationField, nextFields: ArrayList<ClipboardEntryNotificationField>) {
|
private fun copyField(fieldToCopy: ClipboardEntryNotificationField, nextFields: ArrayList<ClipboardEntryNotificationField>) {
|
||||||
stopTask(cleanCopyNotificationTimerTask)
|
mTimerJob?.cancel()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var generatedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
|
var generatedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
|
||||||
@@ -170,40 +169,23 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
this, 0, cleanIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
this, 0, cleanIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
builder.setDeleteIntent(cleanPendingIntent)
|
builder.setDeleteIntent(cleanPendingIntent)
|
||||||
|
|
||||||
val myNotificationId = notificationId
|
if (mNotificationTimeoutMilliSecs != NEVER) {
|
||||||
|
defineTimerJob(builder, mNotificationTimeoutMilliSecs, {
|
||||||
if (notificationTimeoutMilliSecs != NEVER) {
|
val newGeneratedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
|
||||||
cleanCopyNotificationTimerTask = Thread {
|
// New auto generated value
|
||||||
val maxPos = 100
|
if (generatedValue != newGeneratedValue) {
|
||||||
val posDurationMills = notificationTimeoutMilliSecs / maxPos
|
generatedValue = newGeneratedValue
|
||||||
for (pos in maxPos downTo 0) {
|
clipboardHelper?.copyToClipboard(fieldToCopy.label, generatedValue)
|
||||||
val newGeneratedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
|
|
||||||
// New auto generated value
|
|
||||||
if (generatedValue != newGeneratedValue) {
|
|
||||||
generatedValue = newGeneratedValue
|
|
||||||
clipboardHelper?.copyToClipboard(fieldToCopy.label, generatedValue)
|
|
||||||
}
|
|
||||||
builder.setProgress(maxPos, pos, false)
|
|
||||||
notificationManager?.notify(myNotificationId, builder.build())
|
|
||||||
try {
|
|
||||||
Thread.sleep(posDurationMills)
|
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (pos <= 0) {
|
|
||||||
stopNotificationAndSendLockIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
stopTask(cleanCopyNotificationTimerTask)
|
}) {
|
||||||
notificationManager?.cancel(myNotificationId)
|
stopNotificationAndSendLockIfNeeded()
|
||||||
// Clean password only if no next field
|
// Clean password only if no next field
|
||||||
if (nextFields.size <= 0)
|
if (nextFields.size <= 0)
|
||||||
cleanClipboard()
|
cleanClipboard()
|
||||||
}
|
}
|
||||||
cleanCopyNotificationTimerTask?.start()
|
|
||||||
} else {
|
} else {
|
||||||
// No timer
|
// No timer
|
||||||
notificationManager?.notify(myNotificationId, builder.build())
|
notificationManager?.notify(notificationId, builder.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -228,10 +210,6 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
cleanClipboard()
|
cleanClipboard()
|
||||||
|
|
||||||
stopTask(cleanCopyNotificationTimerTask)
|
|
||||||
cleanCopyNotificationTimerTask = null
|
|
||||||
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,7 @@ import com.kunzisoft.keepass.utils.LOCK_ACTION
|
|||||||
class KeyboardEntryNotificationService : LockNotificationService() {
|
class KeyboardEntryNotificationService : LockNotificationService() {
|
||||||
|
|
||||||
override val notificationId = 486
|
override val notificationId = 486
|
||||||
private var cleanNotificationTimerTask: Thread? = null
|
private var mNotificationTimeoutMilliSecs: Long = 0
|
||||||
private var notificationTimeoutMilliSecs: Long = 0
|
|
||||||
|
|
||||||
private var pendingDeleteIntent: PendingIntent? = null
|
private var pendingDeleteIntent: PendingIntent? = null
|
||||||
|
|
||||||
@@ -61,7 +60,7 @@ class KeyboardEntryNotificationService : LockNotificationService() {
|
|||||||
super.onStartCommand(intent, flags, startId)
|
super.onStartCommand(intent, flags, startId)
|
||||||
|
|
||||||
//Get settings
|
//Get settings
|
||||||
notificationTimeoutMilliSecs = PreferenceManager.getDefaultSharedPreferences(this)
|
mNotificationTimeoutMilliSecs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
.getString(getString(R.string.keyboard_entry_timeout_key),
|
.getString(getString(R.string.keyboard_entry_timeout_key),
|
||||||
getString(R.string.timeout_default))?.toLong() ?: TimeoutHelper.DEFAULT_TIMEOUT
|
getString(R.string.timeout_default))?.toLong() ?: TimeoutHelper.DEFAULT_TIMEOUT
|
||||||
|
|
||||||
@@ -107,27 +106,12 @@ class KeyboardEntryNotificationService : LockNotificationService() {
|
|||||||
notificationManager?.cancel(notificationId)
|
notificationManager?.cancel(notificationId)
|
||||||
notificationManager?.notify(notificationId, builder.build())
|
notificationManager?.notify(notificationId, builder.build())
|
||||||
|
|
||||||
stopTask(cleanNotificationTimerTask)
|
|
||||||
// Timeout only if notification clear is available
|
// Timeout only if notification clear is available
|
||||||
if (PreferencesUtil.isClearKeyboardNotificationEnable(this)) {
|
if (PreferencesUtil.isClearKeyboardNotificationEnable(this)) {
|
||||||
if (notificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
|
if (mNotificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
|
||||||
cleanNotificationTimerTask = Thread {
|
defineTimerJob(builder, mNotificationTimeoutMilliSecs) {
|
||||||
val maxPos = 100
|
stopNotificationAndSendLockIfNeeded()
|
||||||
val posDurationMills = notificationTimeoutMilliSecs / maxPos
|
|
||||||
for (pos in maxPos downTo 0) {
|
|
||||||
builder.setProgress(maxPos, pos, false)
|
|
||||||
notificationManager?.notify(notificationId, builder.build())
|
|
||||||
try {
|
|
||||||
Thread.sleep(posDurationMills)
|
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (pos <= 0) {
|
|
||||||
stopNotificationAndSendLockIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
cleanNotificationTimerTask?.start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,8 +126,6 @@ class KeyboardEntryNotificationService : LockNotificationService() {
|
|||||||
// Remove the entry from the keyboard
|
// Remove the entry from the keyboard
|
||||||
MagikIME.removeEntry(this)
|
MagikIME.removeEntry(this)
|
||||||
|
|
||||||
stopTask(cleanNotificationTimerTask)
|
|
||||||
cleanNotificationTimerTask = null
|
|
||||||
pendingDeleteIntent?.cancel()
|
pendingDeleteIntent?.cancel()
|
||||||
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|||||||
@@ -50,11 +50,6 @@ abstract class LockNotificationService : NotificationService() {
|
|||||||
return super.onStartCommand(intent, flags, startId)
|
return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun stopTask(task: Thread?) {
|
|
||||||
if (task != null && task.isAlive)
|
|
||||||
task.interrupt()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
notificationManager?.cancel(notificationId)
|
notificationManager?.cancel(notificationId)
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.core.app.NotificationCompat
|
|||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
|
||||||
abstract class NotificationService : Service() {
|
abstract class NotificationService : Service() {
|
||||||
@@ -18,6 +19,8 @@ abstract class NotificationService : Service() {
|
|||||||
protected var notificationManager: NotificationManagerCompat? = null
|
protected var notificationManager: NotificationManagerCompat? = null
|
||||||
private var colorNotificationAccent: Int = 0
|
private var colorNotificationAccent: Int = 0
|
||||||
|
|
||||||
|
protected var mTimerJob: Job? = null
|
||||||
|
|
||||||
protected abstract val notificationId: Int
|
protected abstract val notificationId: Int
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
@@ -71,7 +74,33 @@ abstract class NotificationService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun defineTimerJob(builder: NotificationCompat.Builder,
|
||||||
|
timeoutMilliseconds: Long,
|
||||||
|
actionAfterASecond: (() -> Unit)? = null,
|
||||||
|
actionEnd: () -> Unit) {
|
||||||
|
mTimerJob?.cancel()
|
||||||
|
mTimerJob = CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
val timeoutInSeconds = timeoutMilliseconds / 1000L
|
||||||
|
for (currentTime in timeoutInSeconds downTo 0) {
|
||||||
|
actionAfterASecond?.invoke()
|
||||||
|
builder.setProgress(100,
|
||||||
|
(currentTime * 100 / timeoutInSeconds).toInt(),
|
||||||
|
false)
|
||||||
|
startForeground(notificationId, builder.build())
|
||||||
|
delay(1000)
|
||||||
|
if (currentTime <= 0) {
|
||||||
|
actionEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notificationManager?.cancel(notificationId)
|
||||||
|
mTimerJob = null
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
mTimerJob?.cancel()
|
||||||
|
mTimerJob = null
|
||||||
notificationManager?.cancel(notificationId)
|
notificationManager?.cancel(notificationId)
|
||||||
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|||||||
@@ -20,9 +20,12 @@
|
|||||||
package com.kunzisoft.keepass.settings
|
package com.kunzisoft.keepass.settings
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DurationDialogFragmentCompat
|
||||||
|
|
||||||
class MagikeyboardSettingsFragment : PreferenceFragmentCompat() {
|
class MagikeyboardSettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
@@ -30,4 +33,31 @@ class MagikeyboardSettingsFragment : PreferenceFragmentCompat() {
|
|||||||
// Load the preferences from an XML resource
|
// Load the preferences from an XML resource
|
||||||
setPreferencesFromResource(R.xml.preferences_keyboard, rootKey)
|
setPreferencesFromResource(R.xml.preferences_keyboard, rootKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDisplayPreferenceDialog(preference: Preference?) {
|
||||||
|
|
||||||
|
var otherDialogFragment = false
|
||||||
|
|
||||||
|
var dialogFragment: DialogFragment? = null
|
||||||
|
// Main Preferences
|
||||||
|
when (preference?.key) {
|
||||||
|
getString(R.string.keyboard_entry_timeout_key) -> {
|
||||||
|
dialogFragment = DurationDialogFragmentCompat.newInstance(preference.key)
|
||||||
|
}
|
||||||
|
else -> otherDialogFragment = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dialogFragment != null) {
|
||||||
|
dialogFragment.setTargetFragment(this, 0)
|
||||||
|
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
|
||||||
|
}
|
||||||
|
// Could not be handled here. Try with the super method.
|
||||||
|
else if (otherDialogFragment) {
|
||||||
|
super.onDisplayPreferenceDialog(preference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import android.view.autofill.AutofillManager
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
@@ -46,6 +47,7 @@ import com.kunzisoft.keepass.education.Education
|
|||||||
import com.kunzisoft.keepass.icons.IconPackChooser
|
import com.kunzisoft.keepass.icons.IconPackChooser
|
||||||
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
||||||
import com.kunzisoft.keepass.settings.preference.IconPackListPreference
|
import com.kunzisoft.keepass.settings.preference.IconPackListPreference
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DurationDialogFragmentCompat
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
|
|
||||||
@@ -90,6 +92,20 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findPreference<Preference>(getString(R.string.import_app_properties_key))?.setOnPreferenceClickListener { _ ->
|
||||||
|
(activity as? SettingsActivity?)?.apply {
|
||||||
|
importAppProperties()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
findPreference<Preference>(getString(R.string.export_app_properties_key))?.setOnPreferenceClickListener { _ ->
|
||||||
|
(activity as? SettingsActivity?)?.apply {
|
||||||
|
exportAppProperties()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,10 +404,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
Stylish.assignStyle(activity, styleIdString)
|
Stylish.assignStyle(activity, styleIdString)
|
||||||
// Relaunch the current activity to redraw theme
|
// Relaunch the current activity to redraw theme
|
||||||
(activity as? SettingsActivity?)?.apply {
|
(activity as? SettingsActivity?)?.apply {
|
||||||
keepCurrentScreen()
|
relaunchCurrentScreen()
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
activity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
styleEnabled
|
styleEnabled
|
||||||
@@ -399,10 +412,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
|
|
||||||
findPreference<ListPreference>(getString(R.string.setting_style_brightness_key))?.setOnPreferenceChangeListener { _, _ ->
|
findPreference<ListPreference>(getString(R.string.setting_style_brightness_key))?.setOnPreferenceChangeListener { _, _ ->
|
||||||
(activity as? SettingsActivity?)?.apply {
|
(activity as? SettingsActivity?)?.apply {
|
||||||
keepCurrentScreen()
|
relaunchCurrentScreen()
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
activity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -440,6 +450,31 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDisplayPreferenceDialog(preference: Preference?) {
|
||||||
|
|
||||||
|
var otherDialogFragment = false
|
||||||
|
|
||||||
|
var dialogFragment: DialogFragment? = null
|
||||||
|
// Main Preferences
|
||||||
|
when (preference?.key) {
|
||||||
|
getString(R.string.app_timeout_key),
|
||||||
|
getString(R.string.clipboard_timeout_key),
|
||||||
|
getString(R.string.temp_advanced_unlock_timeout_key) -> {
|
||||||
|
dialogFragment = DurationDialogFragmentCompat.newInstance(preference.key)
|
||||||
|
}
|
||||||
|
else -> otherDialogFragment = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dialogFragment != null) {
|
||||||
|
dialogFragment.setTargetFragment(this, 0)
|
||||||
|
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
|
||||||
|
}
|
||||||
|
// Could not be handled here. Try with the super method.
|
||||||
|
else if (otherDialogFragment) {
|
||||||
|
super.onDisplayPreferenceDialog(preference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
@@ -470,7 +505,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val REQUEST_CODE_AUTOFILL = 5201
|
private const val REQUEST_CODE_AUTOFILL = 5201
|
||||||
|
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,10 +31,10 @@ import com.kunzisoft.androidclearchroma.ChromaUtil
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.settings.preference.*
|
import com.kunzisoft.keepass.settings.preference.*
|
||||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.*
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.*
|
||||||
@@ -217,12 +217,12 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
|||||||
if (mDatabase.loaded) {
|
if (mDatabase.loaded) {
|
||||||
// Encryption Algorithm
|
// Encryption Algorithm
|
||||||
mEncryptionAlgorithmPref = findPreference<DialogListExplanationPreference>(getString(R.string.encryption_algorithm_key))?.apply {
|
mEncryptionAlgorithmPref = findPreference<DialogListExplanationPreference>(getString(R.string.encryption_algorithm_key))?.apply {
|
||||||
summary = mDatabase.getEncryptionAlgorithmName(resources)
|
summary = mDatabase.getEncryptionAlgorithmName()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Key derivation function
|
// Key derivation function
|
||||||
mKeyDerivationPref = findPreference<DialogListExplanationPreference>(getString(R.string.key_derivation_function_key))?.apply {
|
mKeyDerivationPref = findPreference<DialogListExplanationPreference>(getString(R.string.key_derivation_function_key))?.apply {
|
||||||
summary = mDatabase.getKeyDerivationName(resources)
|
summary = mDatabase.getKeyDerivationName()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Round encryption
|
// Round encryption
|
||||||
@@ -398,7 +398,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
|||||||
mDatabase.encryptionAlgorithm = oldEncryption
|
mDatabase.encryptionAlgorithm = oldEncryption
|
||||||
oldEncryption
|
oldEncryption
|
||||||
}
|
}
|
||||||
mEncryptionAlgorithmPref?.summary = algorithmToShow.getName(resources)
|
mEncryptionAlgorithmPref?.summary = algorithmToShow.toString()
|
||||||
}
|
}
|
||||||
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK -> {
|
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK -> {
|
||||||
val oldKeyDerivationEngine = data.getSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY) as KdfEngine
|
val oldKeyDerivationEngine = data.getSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY) as KdfEngine
|
||||||
@@ -410,7 +410,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
|||||||
mDatabase.kdfEngine = oldKeyDerivationEngine
|
mDatabase.kdfEngine = oldKeyDerivationEngine
|
||||||
oldKeyDerivationEngine
|
oldKeyDerivationEngine
|
||||||
}
|
}
|
||||||
mKeyDerivationPref?.summary = kdfEngineToShow.getName(resources)
|
mKeyDerivationPref?.summary = kdfEngineToShow.toString()
|
||||||
|
|
||||||
mRoundPref?.summary = kdfEngineToShow.defaultKeyRounds.toString()
|
mRoundPref?.summary = kdfEngineToShow.defaultKeyRounds.toString()
|
||||||
// Disable memory and parallelism if not available
|
// Disable memory and parallelism if not available
|
||||||
@@ -576,7 +576,6 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
|
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,14 +21,17 @@ package com.kunzisoft.keepass.settings
|
|||||||
|
|
||||||
import android.app.backup.BackupManager
|
import android.app.backup.BackupManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
||||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||||
|
import com.kunzisoft.keepass.education.Education
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -134,28 +137,46 @@ object PreferencesUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getStyle(context: Context): String {
|
fun getStyle(context: Context): String {
|
||||||
val stylishPrefKey = context.getString(R.string.setting_style_key)
|
val defaultStyleString = Stylish.defaultStyle(context)
|
||||||
val defaultStyleString = context.getString(R.string.list_style_name_light)
|
|
||||||
val styleString = PreferenceManager.getDefaultSharedPreferences(context)
|
val styleString = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
.getString(stylishPrefKey, defaultStyleString)
|
.getString(context.getString(R.string.setting_style_key), defaultStyleString)
|
||||||
?: defaultStyleString
|
?: defaultStyleString
|
||||||
return Stylish.retrieveEquivalentLightStyle(context, styleString)
|
// Return the system style
|
||||||
|
return Stylish.retrieveEquivalentSystemStyle(context, styleString)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setStyle(context: Context, styleString: String) {
|
||||||
|
var tempThemeString = styleString
|
||||||
|
if (tempThemeString in BuildConfig.STYLES_DISABLED) {
|
||||||
|
tempThemeString = Stylish.defaultStyle(context)
|
||||||
|
}
|
||||||
|
// Store light style to show selection in array list
|
||||||
|
tempThemeString = Stylish.retrieveEquivalentLightStyle(context, tempThemeString)
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.edit()
|
||||||
|
.putString(context.getString(R.string.setting_style_key), tempThemeString)
|
||||||
|
.apply()
|
||||||
|
Stylish.load(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getStyleBrightness(context: Context): String? {
|
fun getStyleBrightness(context: Context): String? {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getString(context.getString(R.string.setting_style_brightness_key),
|
return prefs.getString(context.getString(R.string.setting_style_brightness_key),
|
||||||
context.resources.getString(R.string.list_style_brightness_follow_system))
|
context.getString(R.string.list_style_brightness_follow_system))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the text size in % (1 for 100%)
|
* Retrieve the text size in % (1 for 100%)
|
||||||
*/
|
*/
|
||||||
fun getListTextSize(context: Context): Float {
|
fun getListTextSize(context: Context): Float {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val index = try {
|
||||||
val listSizeString = prefs.getString(context.getString(R.string.list_size_key),
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
context.getString(R.string.list_size_string_medium))
|
val listSizeString = prefs.getString(context.getString(R.string.list_size_key),
|
||||||
val index = context.resources.getStringArray(R.array.list_size_string_values).indexOf(listSizeString)
|
context.getString(R.string.list_size_string_medium))
|
||||||
|
context.resources.getStringArray(R.array.list_size_string_values).indexOf(listSizeString)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
1
|
||||||
|
}
|
||||||
val typedArray = context.resources.obtainTypedArray(R.array.list_size_values)
|
val typedArray = context.resources.obtainTypedArray(R.array.list_size_values)
|
||||||
val listSize = typedArray.getFloat(index, 1.0F)
|
val listSize = typedArray.getFloat(index, 1.0F)
|
||||||
typedArray.recycle()
|
typedArray.recycle()
|
||||||
@@ -207,7 +228,7 @@ object PreferencesUtil {
|
|||||||
return try {
|
return try {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
(prefs.getString(context.getString(R.string.app_timeout_key),
|
(prefs.getString(context.getString(R.string.app_timeout_key),
|
||||||
context.getString(R.string.clipboard_timeout_default)) ?: "300000").toLong()
|
context.getString(R.string.timeout_default)) ?: "300000").toLong()
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
TimeoutHelper.DEFAULT_TIMEOUT
|
TimeoutHelper.DEFAULT_TIMEOUT
|
||||||
}
|
}
|
||||||
@@ -289,11 +310,13 @@ object PreferencesUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getListSort(context: Context): SortNodeEnum {
|
fun getListSort(context: Context): SortNodeEnum {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
try {
|
||||||
prefs.getString(context.getString(R.string.sort_node_key),
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
SortNodeEnum.DB.name)?.let {
|
prefs.getString(context.getString(R.string.sort_node_key),
|
||||||
return SortNodeEnum.valueOf(it)
|
SortNodeEnum.DB.name)?.let {
|
||||||
}
|
return SortNodeEnum.valueOf(it)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {}
|
||||||
return SortNodeEnum.DB
|
return SortNodeEnum.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,4 +540,133 @@ object PreferencesUtil {
|
|||||||
.putStringSet(context.getString(R.string.autofill_web_domain_blocklist_key), setItems)
|
.putStringSet(context.getString(R.string.autofill_web_domain_blocklist_key), setItems)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAppProperties(context: Context): Properties {
|
||||||
|
val properties = Properties()
|
||||||
|
for ((name, value) in PreferenceManager.getDefaultSharedPreferences(context).all) {
|
||||||
|
properties[name] = value.toString()
|
||||||
|
}
|
||||||
|
for ((name, value) in Education.getEducationSharedPreferences(context).all) {
|
||||||
|
properties[name] = value.toString()
|
||||||
|
}
|
||||||
|
return properties
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getStringSetFromProperties(value: String): Set<String> {
|
||||||
|
return value.removePrefix("[")
|
||||||
|
.removeSuffix("]")
|
||||||
|
.split(", ")
|
||||||
|
.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun putPropertiesInPreferences(properties: Properties,
|
||||||
|
preferences: SharedPreferences,
|
||||||
|
putProperty: (editor: SharedPreferences.Editor,
|
||||||
|
name: String,
|
||||||
|
value: String) -> Unit) {
|
||||||
|
preferences.edit().apply {
|
||||||
|
for ((name, value) in properties) {
|
||||||
|
try {
|
||||||
|
putProperty(this, name as String, value as String)
|
||||||
|
} catch (e:Exception) {
|
||||||
|
Log.e("PreferencesUtil", "Error when trying to parse app property $name=$value", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAppProperties(context: Context, properties: Properties) {
|
||||||
|
putPropertiesInPreferences(properties,
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context)) { editor, name, value ->
|
||||||
|
when (name) {
|
||||||
|
context.getString(R.string.allow_no_password_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.delete_entered_password_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.enable_read_only_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.enable_auto_save_database_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.omit_backup_search_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.auto_focus_search_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.subdomain_search_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.app_timeout_key) -> editor.putString(name, value.toLong().toString())
|
||||||
|
context.getString(R.string.lock_database_screen_off_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.lock_database_back_root_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.lock_database_show_button_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.password_length_key) -> editor.putInt(name, value.toInt())
|
||||||
|
context.getString(R.string.list_password_generator_options_key) -> editor.putStringSet(name, getStringSetFromProperties(value))
|
||||||
|
context.getString(R.string.hide_password_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.allow_copy_password_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.remember_database_locations_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.show_recent_files_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.hide_broken_locations_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.remember_keyfile_locations_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.biometric_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.device_credential_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.biometric_auto_open_prompt_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.temp_advanced_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.temp_advanced_unlock_timeout_key) -> editor.putString(name, value.toLong().toString())
|
||||||
|
|
||||||
|
context.getString(R.string.magic_keyboard_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.clipboard_notifications_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.clear_clipboard_notification_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.clipboard_timeout_key) -> editor.putString(name, value.toLong().toString())
|
||||||
|
context.getString(R.string.settings_autofill_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_notification_entry_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_notification_entry_clear_close_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_entry_timeout_key) -> editor.putString(name, value.toLong().toString())
|
||||||
|
context.getString(R.string.keyboard_selection_entry_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_search_share_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_save_search_info_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_auto_go_action_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_key_vibrate_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_key_sound_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_previous_database_credentials_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_previous_fill_in_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.keyboard_previous_lock_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.autofill_close_database_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.autofill_auto_search_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.autofill_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.autofill_save_search_info_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.autofill_ask_to_save_data_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.autofill_application_id_blocklist_key) -> editor.putStringSet(name, getStringSetFromProperties(value))
|
||||||
|
context.getString(R.string.autofill_web_domain_blocklist_key) -> editor.putStringSet(name, getStringSetFromProperties(value))
|
||||||
|
|
||||||
|
context.getString(R.string.setting_style_key) -> setStyle(context, value)
|
||||||
|
context.getString(R.string.setting_style_brightness_key) -> editor.putString(name, value)
|
||||||
|
context.getString(R.string.setting_icon_pack_choose_key) -> editor.putString(name, value)
|
||||||
|
context.getString(R.string.list_entries_show_username_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.list_groups_show_number_entries_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.list_size_key) -> editor.putString(name, value)
|
||||||
|
context.getString(R.string.monospace_font_fields_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.hide_expired_entries_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.show_uuid_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.enable_education_screens_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
|
||||||
|
context.getString(R.string.sort_node_key) -> editor.putString(name, value)
|
||||||
|
context.getString(R.string.sort_group_before_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.sort_ascending_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.sort_recycle_bin_bottom_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.allow_copy_password_first_time_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
putPropertiesInPreferences(properties,
|
||||||
|
Education.getEducationSharedPreferences(context)) { editor, name, value ->
|
||||||
|
when (name) {
|
||||||
|
context.getString(R.string.education_create_db_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_select_db_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_unlock_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_read_only_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_biometric_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_search_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_new_node_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_sort_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_lock_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_copy_username_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_entry_edit_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_password_generator_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_entry_new_field_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_add_attachment_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.education_setup_OTP_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,22 +24,27 @@ import android.app.backup.BackupManager
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
open class SettingsActivity
|
open class SettingsActivity
|
||||||
: LockingActivity(),
|
: LockingActivity(),
|
||||||
@@ -48,6 +53,8 @@ open class SettingsActivity
|
|||||||
PasswordEncodingDialogFragment.Listener {
|
PasswordEncodingDialogFragment.Listener {
|
||||||
|
|
||||||
private var backupManager: BackupManager? = null
|
private var backupManager: BackupManager? = null
|
||||||
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
private var appPropertiesFileCreationRequestCode: Int? = null
|
||||||
|
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
@@ -70,6 +77,8 @@ open class SettingsActivity
|
|||||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||||
toolbar = findViewById(R.id.toolbar)
|
toolbar = findViewById(R.id.toolbar)
|
||||||
|
|
||||||
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
|
||||||
if (savedInstanceState?.getString(TITLE_KEY).isNullOrEmpty())
|
if (savedInstanceState?.getString(TITLE_KEY).isNullOrEmpty())
|
||||||
toolbar?.setTitle(R.string.settings)
|
toolbar?.setTitle(R.string.settings)
|
||||||
else
|
else
|
||||||
@@ -216,6 +225,13 @@ open class SettingsActivity
|
|||||||
hideOrShowLockButton(key)
|
hideOrShowLockButton(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun relaunchCurrentScreen() {
|
||||||
|
keepCurrentScreen()
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To keep the current screen when activity is reloaded
|
* To keep the current screen when activity is reloaded
|
||||||
*/
|
*/
|
||||||
@@ -235,6 +251,58 @@ open class SettingsActivity
|
|||||||
replaceFragment(key, reload)
|
replaceFragment(key, reload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun importAppProperties() {
|
||||||
|
mExternalFileHelper?.openDocument()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exportAppProperties() {
|
||||||
|
appPropertiesFileCreationRequestCode = mExternalFileHelper?.createDocument(getString(R.string.app_properties_file_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
// Import app properties result
|
||||||
|
try {
|
||||||
|
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { selectedfileUri ->
|
||||||
|
selectedfileUri?.let { uri ->
|
||||||
|
val appProperties = Properties()
|
||||||
|
contentResolver?.openInputStream(uri)?.use { inputStream ->
|
||||||
|
appProperties.load(inputStream)
|
||||||
|
}
|
||||||
|
PreferencesUtil.setAppProperties(this, appProperties)
|
||||||
|
|
||||||
|
// Restart the current activity
|
||||||
|
relaunchCurrentScreen()
|
||||||
|
Toast.makeText(this, R.string.success_import_app_properties, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(this, R.string.error_import_app_properties, Toast.LENGTH_LONG).show()
|
||||||
|
Log.e(TAG, "Unable to import app properties", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export app properties result
|
||||||
|
try {
|
||||||
|
if (requestCode == appPropertiesFileCreationRequestCode) {
|
||||||
|
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
||||||
|
createdFileUri?.let { uri ->
|
||||||
|
contentResolver?.openOutputStream(uri)?.use { outputStream ->
|
||||||
|
PreferencesUtil
|
||||||
|
.getAppProperties(this)
|
||||||
|
.store(outputStream, getString(R.string.description_app_properties))
|
||||||
|
}
|
||||||
|
Toast.makeText(this, R.string.success_export_app_properties, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appPropertiesFileCreationRequestCode = null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(this, R.string.error_export_app_properties, Toast.LENGTH_LONG).show()
|
||||||
|
Log.e(LockingActivity.TAG, "Unable to export app properties", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
|
|
||||||
@@ -244,6 +312,8 @@ open class SettingsActivity
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
private val TAG = SettingsActivity::class.java.name
|
||||||
|
|
||||||
private const val SHOW_LOCK = "SHOW_LOCK"
|
private const val SHOW_LOCK = "SHOW_LOCK"
|
||||||
private const val TITLE_KEY = "TITLE_KEY"
|
private const val TITLE_KEY = "TITLE_KEY"
|
||||||
private const val TAG_NESTED = "TAG_NESTED"
|
private const val TAG_NESTED = "TAG_NESTED"
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX 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.
|
||||||
|
*
|
||||||
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.settings.preference
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.TypedArray
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import androidx.preference.DialogPreference
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
|
class DurationDialogPreference @JvmOverloads constructor(context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
|
||||||
|
defStyleRes: Int = defStyleAttr)
|
||||||
|
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) {
|
||||||
|
|
||||||
|
private var mDuration: Long = 0L
|
||||||
|
|
||||||
|
override fun getDialogLayoutResource(): Int {
|
||||||
|
return R.layout.pref_dialog_duration
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current duration of preference
|
||||||
|
*/
|
||||||
|
fun getDuration(): Long {
|
||||||
|
return if (mDuration >= 0) mDuration else -1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign [duration] of preference
|
||||||
|
*/
|
||||||
|
fun setDuration(duration: Long) {
|
||||||
|
persistString(duration.toString())
|
||||||
|
notifyChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSetInitialValue(restorePersistedValue: Boolean, defaultValue: Any?) {
|
||||||
|
if (restorePersistedValue) {
|
||||||
|
mDuration = getPersistedString(mDuration.toString()).toLongOrNull() ?: mDuration
|
||||||
|
} else {
|
||||||
|
mDuration = defaultValue?.toString()?.toLongOrNull() ?: mDuration
|
||||||
|
persistString(mDuration.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onGetDefaultValue(a: TypedArray?, index: Int): Any {
|
||||||
|
return try {
|
||||||
|
a?.getString(index)?.toLongOrNull() ?: mDuration
|
||||||
|
} catch (e: Exception) {
|
||||||
|
mDuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Was previously a string
|
||||||
|
override fun persistString(value: String?): Boolean {
|
||||||
|
mDuration = value?.toLongOrNull() ?: mDuration
|
||||||
|
return super.persistString(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ import android.content.Context
|
|||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import androidx.preference.DialogPreference
|
import androidx.preference.DialogPreference
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
|
|
||||||
open class InputKdfNumberPreference @JvmOverloads constructor(context: Context,
|
open class InputKdfNumberPreference @JvmOverloads constructor(context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class InputKdfSizePreference @JvmOverloads constructor(context: Context,
|
|||||||
|
|
||||||
override fun setSummary(summary: CharSequence) {
|
override fun setSummary(summary: CharSequence) {
|
||||||
if (summary == UNKNOWN_VALUE_STRING) {
|
if (summary == UNKNOWN_VALUE_STRING) {
|
||||||
super.setSummary("")
|
super.setSummary(summary)
|
||||||
} else {
|
} else {
|
||||||
var summaryString = summary
|
var summaryString = summary
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -20,11 +20,11 @@
|
|||||||
package com.kunzisoft.keepass.settings.preferencedialogfragment
|
package com.kunzisoft.keepass.settings.preferencedialogfragment
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import android.view.View
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.adapter.ListRadioItemAdapter
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.adapter.ListRadioItemAdapter
|
||||||
|
|
||||||
class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
|
class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
|
||||||
@@ -58,15 +58,13 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
|
|||||||
|
|
||||||
if (positiveResult) {
|
if (positiveResult) {
|
||||||
database?.let { database ->
|
database?.let { database ->
|
||||||
if (database.allowEncryptionAlgorithmModification) {
|
if (algorithmSelected != null) {
|
||||||
if (algorithmSelected != null) {
|
val newAlgorithm = algorithmSelected
|
||||||
val newAlgorithm = algorithmSelected
|
val oldAlgorithm = database.encryptionAlgorithm
|
||||||
val oldAlgorithm = database.encryptionAlgorithm
|
database.encryptionAlgorithm = newAlgorithm
|
||||||
database.encryptionAlgorithm = newAlgorithm
|
|
||||||
|
|
||||||
if (oldAlgorithm != null && newAlgorithm != null)
|
if (oldAlgorithm != null && newAlgorithm != null)
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseSaveEncryption(oldAlgorithm, newAlgorithm, mDatabaseAutoSaveEnable)
|
mProgressDatabaseTaskProvider?.startDatabaseSaveEncryption(oldAlgorithm, newAlgorithm, mDatabaseAutoSaveEnable)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user