Refactor KDB cipher and better tests

This commit is contained in:
J-Jamet
2021-03-22 10:33:32 +01:00
parent 3bf0de3888
commit 874fdb7da0
11 changed files with 143 additions and 200 deletions

View File

@@ -20,7 +20,7 @@
package com.kunzisoft.keepass.database.crypto
import com.kunzisoft.encrypt.CipherEngineFactory
import com.kunzisoft.encrypt.CipherFactory
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
@@ -31,7 +31,7 @@ class AesEngine : CipherEngine() {
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
return CipherEngineFactory.getAES(opmode, key, IV)
return CipherFactory.getAES(opmode, key, IV)
}
override fun getEncryptionAlgorithm(): EncryptionAlgorithm {

View File

@@ -19,7 +19,7 @@
*/
package com.kunzisoft.keepass.database.crypto
import com.kunzisoft.encrypt.CipherEngineFactory
import com.kunzisoft.encrypt.CipherFactory
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
@@ -34,7 +34,7 @@ class ChaCha20Engine : CipherEngine() {
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
return CipherEngineFactory.getChacha20(opmode, key, IV)
return CipherFactory.getChacha20(opmode, key, IV)
}
override fun getEncryptionAlgorithm(): EncryptionAlgorithm {

View File

@@ -19,7 +19,7 @@
*/
package com.kunzisoft.keepass.database.crypto
import com.kunzisoft.encrypt.CipherEngineFactory
import com.kunzisoft.encrypt.CipherFactory
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
@@ -30,7 +30,7 @@ class TwofishEngine : CipherEngine() {
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
return CipherEngineFactory.getTwofish(opmode, key, IV)
return CipherFactory.getTwofish(opmode, key, IV)
}
override fun getEncryptionAlgorithm(): EncryptionAlgorithm {

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.database.file.input
import com.kunzisoft.encrypt.CipherFactory
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.element.Database
@@ -35,12 +34,11 @@ import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
import com.kunzisoft.keepass.stream.*
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import java.io.*
import java.security.*
import java.security.DigestInputStream
import java.security.MessageDigest
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
/**
@@ -131,32 +129,14 @@ class DatabaseInputKDB(cacheDirectory: File)
mDatabase.numberKeyEncryptionRounds)
progressTaskUpdater?.updateMessage(R.string.decrypting_db)
// Initialize Rijndael algorithm
val cipher: Cipher = try {
// TODO Encapsulate
when {
mDatabase.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael -> {
CipherFactory.getInstance("AES/CBC/PKCS5Padding")
}
mDatabase.encryptionAlgorithm === EncryptionAlgorithm.Twofish -> {
CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING")
}
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 cipher: Cipher = try {
mDatabase.encryptionAlgorithm
.cipherEngine.getCipher(Cipher.DECRYPT_MODE,
mDatabase.finalKey ?: ByteArray(0),
header.encryptionIV)
} catch (e: Exception) {
throw IOException("Algorithm not supported.", e)
}
val messageDigest: MessageDigest

View File

@@ -156,7 +156,7 @@ class DatabaseInputKDBX(cacheDirectory: File)
val isPlain: InputStream
if (mDatabase.kdbxVersion.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
val decrypted = attachCipherStream(databaseInputStream, cipher)
val decrypted = CipherInputStream(databaseInputStream, cipher)
val dataDecrypted = LittleEndianDataInputStream(decrypted)
val storedStartBytes: ByteArray?
try {
@@ -193,11 +193,10 @@ class DatabaseInputKDBX(cacheDirectory: File)
val hmIs = HmacBlockInputStream(isData, true, hmacKey)
isPlain = attachCipherStream(hmIs, cipher)
isPlain = CipherInputStream(hmIs, cipher)
}
val inputStreamXml: InputStream
inputStreamXml = when (mDatabase.compressionAlgorithm) {
val inputStreamXml: InputStream = when (mDatabase.compressionAlgorithm) {
CompressionAlgorithm.GZip -> GZIPInputStream(isPlain)
else -> isPlain
}
@@ -232,10 +231,6 @@ class DatabaseInputKDBX(cacheDirectory: File)
return mDatabase
}
private fun attachCipherStream(inputStream: InputStream, cipher: Cipher): InputStream {
return CipherInputStream(inputStream, cipher)
}
@Throws(IOException::class)
private fun loadInnerHeader(inputStream: InputStream, header: DatabaseHeaderKDBX) {
val lis = LittleEndianDataInputStream(inputStream)

View File

@@ -19,7 +19,6 @@
*/
package com.kunzisoft.keepass.database.file.output
import com.kunzisoft.encrypt.CipherFactory
import com.kunzisoft.encrypt.UnsignedInt
import com.kunzisoft.encrypt.stream.LittleEndianDataOutputStream
import com.kunzisoft.encrypt.stream.NullOutputStream
@@ -37,8 +36,7 @@ import java.security.*
import java.util.*
import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import javax.crypto.NoSuchPaddingException
class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
outputStream: OutputStream)
@@ -67,31 +65,27 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
val finalKey = getFinalKey(header)
val cipher: Cipher
cipher = try {
when {
// TODO Encapsulate
mDatabaseKDB.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael->
CipherFactory.getInstance("AES/CBC/PKCS5Padding")
mDatabaseKDB.encryptionAlgorithm === EncryptionAlgorithm.Twofish ->
CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING")
else ->
throw Exception()
}
} catch (e: Exception) {
throw DatabaseOutputException("Algorithm not supported.", e)
val cipher: Cipher = try {
mDatabaseKDB.encryptionAlgorithm
.cipherEngine.getCipher(Cipher.ENCRYPT_MODE,
finalKey ?: ByteArray(0),
header.encryptionIV)
} catch (e1: NoSuchAlgorithmException) {
throw IOException("No such algorithm")
} catch (e1: NoSuchPaddingException) {
throw IOException("No such padding")
} catch (e1: InvalidKeyException) {
throw IOException("Invalid key")
} catch (e1: InvalidAlgorithmParameterException) {
throw IOException("Invalid algorithm parameter.")
}
try {
cipher.init(Cipher.ENCRYPT_MODE,
SecretKeySpec(finalKey, "AES"),
IvParameterSpec(header.encryptionIV))
val cos = CipherOutputStream(mOutputStream, cipher)
val bos = BufferedOutputStream(cos)
outputPlanGroupAndEntries(bos)
bos.flush()
bos.close()
} catch (e: InvalidKeyException) {
throw DatabaseOutputException("Invalid key", e)
} catch (e: InvalidAlgorithmParameterException) {

View File

@@ -23,23 +23,82 @@ import com.kunzisoft.encrypt.aes.AndroidAESKeyTransformer
import com.kunzisoft.encrypt.aes.NativeAESKeyTransformer
import org.junit.Assert.assertArrayEquals
import org.junit.Test
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.util.*
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream
class AESTest {
private val mRand = Random()
@Test
fun testAES() {
// Test both an old and an even number to test my flip variable
testAESFinalKey(5)
testAESFinalKey(6)
fun testAESByteArray() {
// Generate random input
val input = ByteArray(mRand.nextInt(494) + 18)
mRand.nextBytes(input)
// Generate key
val keyArray = ByteArray(32)
mRand.nextBytes(keyArray)
// Generate IV
val ivArray = ByteArray(16)
mRand.nextBytes(ivArray)
val androidEncrypt = CipherFactory.getAES(Cipher.ENCRYPT_MODE, keyArray, ivArray).doFinal(input)
val nativeEncrypt = CipherFactory.getAES(Cipher.ENCRYPT_MODE, keyArray, ivArray, true).doFinal(input)
assertArrayEquals("Check AES encryption", androidEncrypt, nativeEncrypt)
val androidDecrypt = CipherFactory.getAES(Cipher.DECRYPT_MODE, keyArray, ivArray).doFinal(androidEncrypt)
val nativeDecrypt = CipherFactory.getAES(Cipher.DECRYPT_MODE, keyArray, ivArray, true).doFinal(nativeEncrypt)
assertArrayEquals("Check AES encryption/decryption", androidDecrypt, nativeDecrypt)
val androidMixDecrypt = CipherFactory.getAES(Cipher.DECRYPT_MODE, keyArray, ivArray).doFinal(nativeEncrypt)
val nativeMixDecrypt = CipherFactory.getAES(Cipher.DECRYPT_MODE, keyArray, ivArray, true).doFinal(androidEncrypt)
assertArrayEquals("Check AES mix encryption/decryption", androidMixDecrypt, nativeMixDecrypt)
}
private fun testAESFinalKey(rounds: Long) {
@Test
fun testAESStream() {
// Generate random input
val input = ByteArray(mRand.nextInt(494) + 18)
mRand.nextBytes(input)
// Generate key
val keyArray = ByteArray(32)
mRand.nextBytes(keyArray)
// Generate IV
val ivArray = ByteArray(16)
mRand.nextBytes(ivArray)
val androidEncrypt = CipherFactory.getAES(Cipher.ENCRYPT_MODE, keyArray, ivArray)
val androidDecrypt = CipherFactory.getAES(Cipher.DECRYPT_MODE, keyArray, ivArray)
val androidOutputStream = ByteArrayOutputStream()
CipherInputStream(ByteArrayInputStream(input), androidEncrypt).use { cipherInputStream ->
CipherOutputStream(androidOutputStream, androidDecrypt).use { outputStream ->
outputStream.write(cipherInputStream.readBytes())
}
}
val androidOut = androidOutputStream.toByteArray()
val nativeEncrypt = CipherFactory.getAES(Cipher.ENCRYPT_MODE, keyArray, ivArray)
val nativeDecrypt = CipherFactory.getAES(Cipher.DECRYPT_MODE, keyArray, ivArray)
val nativeOutputStream = ByteArrayOutputStream()
CipherInputStream(ByteArrayInputStream(input), nativeEncrypt).use { cipherInputStream ->
CipherOutputStream(nativeOutputStream, nativeDecrypt).use { outputStream ->
outputStream.write(cipherInputStream.readBytes())
}
}
val nativeOut = nativeOutputStream.toByteArray()
assertArrayEquals("Check AES encryption/decryption", androidOut, nativeOut)
}
@Test
fun testAESKDF() {
val seed = ByteArray(32)
val key = ByteArray(32)
val nativeKey: ByteArray?
@@ -49,49 +108,11 @@ class AESTest {
mRand.nextBytes(key)
val androidAESKey = AndroidAESKeyTransformer()
androidKey = androidAESKey.transformMasterKey(seed, key, rounds)
androidKey = androidAESKey.transformMasterKey(seed, key, 60000)
val nativeAESKey = NativeAESKeyTransformer()
nativeKey = nativeAESKey.transformMasterKey(seed, key, rounds)
nativeKey = nativeAESKey.transformMasterKey(seed, key, 60000)
assertArrayEquals("Does not match", androidKey, nativeKey)
}
@Test
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)
}
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)
}
}

View File

@@ -1,45 +0,0 @@
package com.kunzisoft.encrypt
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import javax.crypto.Cipher
import javax.crypto.NoSuchPaddingException
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class CipherEngineFactory {
companion object {
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
fun getAES(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
// TODO native
val androidOverride = false
val cipher = CipherFactory.getInstance("AES/CBC/PKCS5Padding", androidOverride)
cipher.init(opmode, SecretKeySpec(key, "AES"), IvParameterSpec(IV))
return cipher
}
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
fun getTwofish(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
// TODO native
val androidOverride = false
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
}
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
fun getChacha20(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
val cipher = Cipher.getInstance("Chacha7539", BouncyCastleProvider())
cipher.init(opmode, SecretKeySpec(key, "ChaCha7539"), IvParameterSpec(IV))
return cipher
}
}
}

View File

@@ -1,61 +1,51 @@
/*
* 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.encrypt
import android.os.Build
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.security.Security
import javax.crypto.Cipher
import javax.crypto.NoSuchPaddingException
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
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()) {
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
fun getAES(opmode: Int, key: ByteArray, IV: ByteArray, forceNative: Boolean = false): Cipher {
val transformation = "AES/CBC/PKCS5Padding"
val cipher = if (forceNative || (!NativeBlockList.isBlocked && NativeLib.loaded())) {
Cipher.getInstance(transformation, AESProvider())
} else {
Cipher.getInstance(transformation)
}
cipher.init(opmode, SecretKeySpec(key, "AES"), IvParameterSpec(IV))
return cipher
}
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
fun getTwofish(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
val cipher: Cipher = if (opmode == Cipher.ENCRYPT_MODE) {
Cipher.getInstance("Twofish/CBC/ZeroBytePadding")
} else {
Cipher.getInstance("Twofish/CBC/NoPadding")
}
// TODO Verify KDB TwoFish
// CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING")
cipher.init(opmode, SecretKeySpec(key, "AES"), IvParameterSpec(IV))
return cipher
}
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
fun getChacha20(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
val cipher = Cipher.getInstance("Chacha7539", BouncyCastleProvider())
cipher.init(opmode, SecretKeySpec(key, "ChaCha7539"), IvParameterSpec(IV))
return cipher
}
}

View File

@@ -0,0 +1,9 @@
package com.kunzisoft.encrypt
import android.os.Build
object NativeBlockList {
val isBlocked: Boolean by lazy {
Build.MODEL == "A500"
}
}

View File

@@ -19,13 +19,12 @@
*/
package com.kunzisoft.encrypt.aes
import com.kunzisoft.encrypt.CipherFactory.deviceBlacklisted
import com.kunzisoft.encrypt.NativeBlockList
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()) {
val keyTransformer = if (!NativeBlockList.isBlocked && NativeAESKeyTransformer.available()) {
NativeAESKeyTransformer()
} else {
// Fall back on the android crypto implementation